Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6731f33
fix(transactions): Limit a transaction to one category
nfebe Jul 15, 2026
4013f94
feat(ai): Act on many transactions at once and in the user's currency
nfebe Jul 15, 2026
818d59d
docs: Lead the readme with who Trakli is for
nfebe Jul 18, 2026
d7efd1a
fix(sync): Mark soft deleted records as synced
nfebe Jul 22, 2026
6366d8b
fix(transfers): Keep decimal precision in converted transfer amounts
nfebe Jul 22, 2026
7394f78
fix(imports): Reject rows without a positive amount and restore trans…
nfebe Jul 22, 2026
7e60b32
fix(transactions): Stop updates from overwriting an existing client id
nfebe Jul 22, 2026
dd71820
chore(deps): Update eloquent-holdings so live holdings price at creation
nfebe Jul 22, 2026
8b6a8a6
docs: Rewrite the readme pitch to match the site copy
nfebe Jul 22, 2026
0f4de2d
fix(ai): Scope every table the assistant can query
nfebe Jul 29, 2026
ec6591a
refactor(ai): Move proposed-action override policy out of the controller
nfebe Jul 29, 2026
1621b1a
feat(ai): Give budgets, refunds, reminders and groups AI access
nfebe Jul 29, 2026
6cd4848
chore(deps): Update eloquent-agents so model resources resolve
nfebe Jul 29, 2026
fdaf419
fix(api): Accept boolean flags sent as text by form requests
nfebe Jul 30, 2026
afc46a6
refactor(entitlements): Adopt the shared entitlements package
nfebe Jul 30, 2026
eb31273
fix(schema): Detect drifted columns before they break writes
nfebe Aug 5, 2026
268652d
feat(exports): Download transactions and statements as files
nfebe Aug 5, 2026
401b34f
fix(exports): Limit each format to what it can actually render
nfebe Aug 5, 2026
7c82198
ci(delivery): Move the hosted image build out of this repository
nfebe Aug 12, 2026
f2302a0
chore(release): Bump to v2.0.0-beta.2 and record the changelog
nfebe Aug 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 0 additions & 137 deletions .github/workflows/build-hosted-image.yml

This file was deleted.

3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,8 @@ jobs:
- name: Run database migrations
run: flatrun deployment exec trakli-staging app -- php artisan migrate

- name: Verify schema conformance
run: flatrun deployment exec trakli-staging app -- php artisan schema:verify

- name: Optimize application caches
run: flatrun deployment exec trakli-staging app -- php artisan optimize
103 changes: 74 additions & 29 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,53 +33,98 @@ without tools leaves a feature the AI is blind to. When you add an Eloquent
model / table that holds user data (or a meaningful new field on one), in the
same change also do the following.

### 1. Read tool
### 1. Declare the resource on the model

Add a read tool under `app/Ai/Tools/Read/` so the assistant can see the data,
scoped to the authenticated user (`$context->user`). Mirror
`app/Ai/Tools/Read/ListWalletsTool.php` (or `ListHoldingsTool.php`): extend
`Whilesmart\Agents\Tools\AbstractTool`, `permission()` returns
`ToolPermission::READ`, return a plain array.
Implement `Whilesmart\Agents\Contracts\HasAgentResource` and return an
`AgentResource` describing the model once: its name and aliases, the column that
reads as a row's label, how rows are tied to an owner, the fields an agent may
read, and the fields it may write. `app/Models/Transfer.php` and
`app/Models/Budget.php` are the worked examples.

### 2. Write tool (when users create/change it conversationally)
Mark internal plumbing (`user_id`, `owner_type`) with `ResourceField::internal()`
so it never reaches an answer, and give foreign keys a `references` so a raw id
can be resolved to a name.

If users would naturally say "add a ...", add a write tool under
`app/Ai/Tools/Write/` extending `AbstractWriteTool`. Write tools only *propose*
an action the user confirms; execution goes through `ProposedActionExecutor`
(add the new `*.create` action type there). Mirror
`app/Ai/Tools/Write/RecordTransactionTool.php`.
Ownership is what keeps one user out of another's data, and every form fails
closed:

### 3. Register the tool
| Declaration | Use for |
|---|---|
| `ownerKey: 'user_id'` | The ordinary case |
| `ownerConstants: ['owner_type' => User::class]` | Polymorphic owners, where the id alone is ambiguous (budgets, holdings) |
| `scopeThrough: new ThroughScope(...)` | No owner column; a parent record owns it (refunds, recurring rules) |
| `global: true` | Reference data belonging to nobody (exchange rates) |

Add the class to the `tools` array in `config/agents.php`. A tool that isn't
listed there is never offered to the assistant.
Add the model to `resources.models` in `config/agents.php`. A model that is not
listed there is invisible to the assistant no matter what it declares.

### 4. smartql.yml (if the table should be queryable)
That alone gives it a `list_<resource>` read tool, scoped to the acting user and
returning only the non-internal fields. Do **not** hand-write a read tool as
well. A model that already has one (transactions, wallets, categories, parties)
declares `readTool: false`: it is listed for the schema and for scoping its
children, not for a duplicate tool.

If the assistant should be able to query the table ad hoc (totals, filters,
joins), add it to `smartql.yml` under `semantic_layer.entities`: the real table
name, a description, `aliases` the user might say, and the columns with types and
descriptions. Add new *columns* on existing tables too (e.g. a new enum field),
and any `relationships`. The SmartQL tool can only reach declared tables/columns.
### 2. Write tool (when users create it conversationally)

### 5. Analytics (when relevant)
If users would naturally say "add a ...", set `writeEnabled: true` and list the
fields under `writable` with their validation rules. `CreateResourceTool` builds
the tool from that declaration, so most models need no write-tool class at all
(groups and reminders work this way).

Write a bespoke tool extending `AbstractWriteTool` only when creating the record
means more than setting columns: linking two records, or syncing a pivot. See
`CreateBudgetTool` (targets), `RecordRefundTool` and `CreateRecurringRuleTool`,
and register those in the `tools` array of `config/agents.php`.

Either way, a write tool only *proposes* an action the user confirms. Execution
goes through `ProposedActionExecutor`, so add the new `*.create` action type
there, and add its editable fields to `AiController::allowedOverrideKeys()`. Keep
the field that proves ownership out of that list: an overridable
`transaction_id` would let a confirmed action point at someone else's record.

### 3. smartql.yml

Regenerate the semantic layer rather than editing it by hand:

```
php artisan agents:export-schema --output=storage/app/exported.yml
```

The entities, relationships, `allowed_tables` and `required_filters` come from
the resource declarations. Merge the result into `smartql.yml`, which also holds
the parts with no model behind them: connection and LLM settings, business
rules, prompt examples, and the `holdings` and `categorizables` entities.

`SmartqlSchemaTest` fails if the file drifts from the models, if a readable table
has no tenant filter, or if a polymorphic owner is missing its type pin.

### 4. Analytics (when relevant)

If the model feeds a headline number, expose it through a `GetStatsTool` section
(`app/Ai/Tools/Read/GetStatsTool.php` + `StatsService`) rather than expecting the
assistant to compute it.

### 5. Tell the assistant it exists

A tool the system prompt never mentions goes unused. Add a short section to
`TrakliHarness::systemPrompt()` saying what the model is for and which tool
reaches it, in the style of the existing Transfers and Budgets sections.

## Reference: holdings

`whilesmart/eloquent-holdings` is the worked example: `ListHoldingsTool`
(read), the `holdings` entity in `smartql.yml`, the `position` section in
`whilesmart/eloquent-holdings` is the one model that cannot declare a resource,
because it lives in a package: `ListHoldingsTool` (read), a hand-written
`holdings` entity in `smartql.yml`, and the `position` section in
`GetStatsTool`/`StatsService` for net worth. A record/write tool for holdings is
the outstanding piece and should follow the same pattern.
the outstanding piece.

## Checklist for a new model

- [ ] Read tool in `app/Ai/Tools/Read/`, user-scoped
- [ ] Write tool in `app/Ai/Tools/Write/` (+ `ProposedActionExecutor` action) if user-created
- [ ] Registered in `config/agents.php`
- [ ] `smartql.yml` entity / new columns / relationships
- [ ] `agentResource()` on the model, with ownership and internal fields declared
- [ ] Listed in `resources.models` in `config/agents.php`
- [ ] `writeEnabled` + `writable` fields if user-created, or a bespoke write tool
(+ `ProposedActionExecutor` action and `allowedOverrideKeys` entry)
- [ ] `smartql.yml` regenerated and merged
- [ ] Stats section if it drives a headline figure
- [ ] A section in the harness system prompt
- [ ] Tests covering the tool through the user boundary
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,52 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.0-beta.2] - 2026-08-12

Codename: Ailanthus, second beta.

### Added

- Transactions and period statements download as CSV, XLSX or PDF; a transaction
download carries the same filters as the listing, and a statement carries the
same figures as the analytics screen
- The assistant reaches budgets, refunds, reminders, groups and transfers: it can
say how a budget is doing and record money coming back as a refund rather than
fresh income
- The assistant proposes a change across many transactions at once, confirmed as
one sweep instead of a card per row
- Reminder columns that existed only in the conformance spec now have a migration
behind them

### Changed

- The assistant answers in the user's own currency, converting at the same rates
the rest of the app uses, and asks when the currency cannot be known
- Confirmation cards name the record being changed rather than its internal id
- A transaction takes a single category; one saved earlier keeps its first
category and drops the rest when saved again
- Feature gating moved to the shared entitlements package; behaviour is unchanged,
with every feature allowed and nothing metered
- An export larger than a format can render is refused up front, naming the limit
that applied and what the other formats allow

### Fixed

- A database that falls behind its migrations answers with a clear error naming
what is missing, instead of accepting writes it cannot complete
- Every table the assistant can read is scoped to the person asking, and transfer
rows no longer inflate the income and spending it reports
- Boolean flags sent as text by form and multipart requests are accepted, so
recurring transactions can be created from the mobile app
- Transfers keep their decimal precision, so cents no longer vanish and small
conversion rates no longer produce zero amounts
- A second device can no longer overwrite the client id created by the first
- Imported rows without a usable amount fail with a reason instead of importing as
zero, and transfer rows import as transfers again
- Deleted records report as changed, so clients apply the deletion
- A holding created with automatic pricing is priced at creation instead of
reporting zero until the next repricing run

## [2.0.0-beta.1] - 2026-07-11

Codename: Ailanthus, first of the tree-name series.
Expand Down
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,45 @@
[![Tests](https://github.com/trakli/webservice/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/trakli/webservice/actions/workflows/tests.yml)
[![Lint & Sniffs](https://github.com/trakli/webservice/actions/workflows/lint-sniffs.yml/badge.svg?branch=main)](https://github.com/trakli/webservice/actions/workflows/lint-sniffs.yml)

## Overview
Trakli gets your money under control by showing you where it actually goes. Every wallet, balance, and
holding you have, in as many accounts and currencies as you keep them, adds up to your real net worth,
not just what you spent this month. And you barely type any of it: say what happened, or hand over a
receipt, and the assistant does the logging.

Trakli is a personal income tracking application built using Laravel. The application allows users to manage and categorize their income and expenses under various groups and categories.
This is the Laravel API behind it, and what the web and mobile apps talk to.

## Features

AI native:

- **An assistant that acts:** say what happened ("log $40 dinner at Mama's, split under food") and it
records, categorizes, transfers, and cleans up your books in bulk, proposing every change for you to
approve before anything is written.
- **Reads your paperwork:** hand it receipts, statements, CSVs, or PDFs and it turns them into
transactions.
- **Reports on a canvas:** ask for a report, watch it get built, then take it with you.
- **MCP built in:** Trakli runs an MCP server, so Claude or any MCP client can read your books and record
transactions through tokens you mint and revoke yourself.

For pro users:

- **Wallets that match reality:** cash, mobile money, bank and card, side by side.
- **Any currency, at your rate:** forty-eight of them, held at once; transfers convert at the rate you
set, not one scraped from a market you cannot get.
- **Offline-first:** the phone works with no signal, and changes settle cleanly when it reconnects.
- **Yours to run:** open source end to end; audit it, fork it, self-host it, with no one else holding the
ledger of what you earn.

A full finance app, not a spreadsheet:

- **Transactions:** Income and expenses across multiple wallets, with attachments and recurring rules.
- **Transfers:** Move money between wallets, including cross-currency at user-set rates.
- **Holdings:** Crypto, stocks, property, any asset; crypto priced live, everything else at the price you set.
- **Budgets:** Scoped to categories, groups, or wallets; weekly / monthly / yearly / custom range; optional rollover; threshold and forecast alerts.
- **Refunds:** Mark an income as refunding an earlier expense; matching budgets adjust automatically.
- **Reminders:** Bills, budget alerts, and custom events with pause, resume, and snooze.
- **Imports:** Pull transactions from CSVs, PDFs, and photos of receipts.
- **Insights & AI:** Dashboard stats, digest emails, and a chat assistant for your finances.
- **Offline-first:** Changes made on mobile sync cleanly when the device reconnects.
- **Insights:** Dashboard stats and digest emails.

## Setup instructions

Expand Down
13 changes: 13 additions & 0 deletions app/Ai/BlockBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,19 @@ public function proposedAction(array $proposal): array
return array_merge(['type' => 'proposed_action'], $proposal);
}

/**
* Several actions proposed together and confirmed as one unit, so a request
* touching many records costs the user one decision instead of one per row.
* Members keep their own confirm/reject urls for the odd one out.
*
* @param array<string, mixed> $batch
* @return array<string, mixed>
*/
public function proposedActionBatch(array $batch): array
{
return array_merge(['type' => 'proposed_action_batch'], $batch);
}

/**
* A canvas artifact: a titled document whose body is an ordered list of
* blocks (markdown sections interleaved with charts/kpis/tables) composed by
Expand Down
Loading
Loading