plasalid 0.6.9 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +3 -5
  2. package/dist/accounts/taxonomy.d.ts +0 -23
  3. package/dist/accounts/taxonomy.js +15 -15
  4. package/dist/ai/agent.d.ts +4 -4
  5. package/dist/ai/agent.js +9 -8
  6. package/dist/ai/context.d.ts +0 -2
  7. package/dist/ai/context.js +2 -2
  8. package/dist/ai/memory.d.ts +1 -0
  9. package/dist/ai/memory.js +4 -0
  10. package/dist/ai/personas.js +3 -6
  11. package/dist/ai/provider.d.ts +1 -0
  12. package/dist/ai/thinking.d.ts +0 -6
  13. package/dist/ai/thinking.js +29 -4
  14. package/dist/ai/tools/index.d.ts +5 -1
  15. package/dist/ai/tools/index.js +21 -15
  16. package/dist/ai/tools/ingest.js +94 -110
  17. package/dist/ai/tools/resolve.js +15 -44
  18. package/dist/cli/commands/accounts.d.ts +4 -1
  19. package/dist/cli/commands/accounts.js +39 -20
  20. package/dist/cli/commands/scan.js +47 -47
  21. package/dist/cli/commands/status.js +81 -14
  22. package/dist/cli/commands/transactions.d.ts +3 -1
  23. package/dist/cli/commands/transactions.js +37 -34
  24. package/dist/cli/format.d.ts +0 -1
  25. package/dist/cli/format.js +1 -1
  26. package/dist/cli/helper.d.ts +11 -0
  27. package/dist/cli/helper.js +24 -0
  28. package/dist/cli/index.js +14 -10
  29. package/dist/cli/ink/AccountsBrowser.d.ts +7 -0
  30. package/dist/cli/ink/AccountsBrowser.js +149 -0
  31. package/dist/cli/ink/ListBrowser.d.ts +38 -0
  32. package/dist/cli/ink/ListBrowser.js +154 -0
  33. package/dist/cli/ink/TransactionsBrowser.d.ts +6 -0
  34. package/dist/cli/ink/TransactionsBrowser.js +87 -0
  35. package/dist/cli/ink/hooks/useFooterText.js +30 -11
  36. package/dist/cli/ink/runBrowser.d.ts +7 -0
  37. package/dist/cli/ink/runBrowser.js +24 -0
  38. package/dist/cli/ux.d.ts +4 -5
  39. package/dist/cli/ux.js +87 -66
  40. package/dist/db/connection.d.ts +0 -2
  41. package/dist/db/connection.js +0 -5
  42. package/dist/db/queries/files.d.ts +11 -0
  43. package/dist/db/queries/files.js +16 -0
  44. package/dist/db/queries/recurrences.d.ts +7 -0
  45. package/dist/db/queries/recurrences.js +21 -0
  46. package/dist/db/queries/transactions.d.ts +28 -4
  47. package/dist/db/queries/transactions.js +68 -15
  48. package/dist/db/queries/unknowns.d.ts +3 -5
  49. package/dist/db/queries/unknowns.js +4 -4
  50. package/dist/db/schema.js +8 -0
  51. package/dist/lib/runPasses.d.ts +30 -0
  52. package/dist/lib/runPasses.js +15 -0
  53. package/dist/resolver/pipeline.d.ts +6 -6
  54. package/dist/resolver/pipeline.js +50 -22
  55. package/dist/scanner/inspectors/similarities.js +14 -16
  56. package/package.json +2 -2
package/README.md CHANGED
@@ -32,8 +32,6 @@ The data ledger also serves as a harness, open to any AI agent that connects to
32
32
 
33
33
  ## Features
34
34
 
35
- ![](https://github.com/phureewat29/plasalid/.github/plasalid-demo.gif)
36
-
37
35
  ### Unified ledger from any financial documents
38
36
 
39
37
  - **Drop PDFs, get a complete ledger.** Bank statements, credit-card statements, payslips, brokerage statements, and etc. — Plasalid uses AI to parse every transaction, balance, and holding into double-entry ledger.
@@ -89,11 +87,11 @@ plasalid # Interactive chat with your data
89
87
  plasalid setup # Configure API key, encryption, and data directory
90
88
  plasalid data # Open the Plasalid data folder in your file explorer
91
89
  plasalid accounts # Show the chart of accounts with balances
92
- plasalid status # Net worth and this-month income/expense totals
93
90
  plasalid transactions # List transactions and their postings (filter by --account, --from, --to, --query, --limit)
94
- plasalid record <utterance> # Add a manual transaction, account, balance, or merchant from a plain-language line
91
+ plasalid status # Net worth and this-month income/expense totals
92
+ plasalid record [utterance] # Add a manual transaction, account, balance, or merchant from a plain-language line
95
93
  plasalid scan [regex] [--force] # Scan new PDFs; --force cascade-deletes prior records before re-scanning
96
- plasalid revert <regex> # Delete scanned files matching <regex> and their transactions
94
+ plasalid revert [regex] # Delete scanned files matching <regex> and their transactions
97
95
  plasalid resolve # Walk every open unknown and apply your decision (--account, --from, --to, --kind also accepted)
98
96
  ```
99
97
 
@@ -1,28 +1,5 @@
1
1
  export type AccountType = "asset" | "liability" | "income" | "expense" | "equity";
2
- export type InstitutionKind = "bank" | "card_issuer" | "wallet" | "payment_rail" | "broker" | "crypto_exchange" | "insurer" | "gov" | "telco" | "utility";
3
- export interface ThaiInstitution {
4
- code: string;
5
- label: string;
6
- kind: InstitutionKind;
7
- /** Optional disambiguating note for the AI (mergers, rebrands, regulatory status). */
8
- notes?: string;
9
- }
10
- export declare const THAI_BANKS: ThaiInstitution[];
11
- export declare const THAI_CARD_ISSUERS: ThaiInstitution[];
12
- export declare const THAI_WALLETS: ThaiInstitution[];
13
- export declare const THAI_PAYMENT_RAILS: ThaiInstitution[];
14
- export declare const THAI_BROKERS: ThaiInstitution[];
15
- export declare const THAI_CRYPTO_EXCHANGES: ThaiInstitution[];
16
- export declare const THAI_INSURERS: ThaiInstitution[];
17
- export declare const THAI_GOV: ThaiInstitution[];
18
- export declare const THAI_UTILITIES: ThaiInstitution[];
19
- export declare const THAI_TELCOS: ThaiInstitution[];
20
- export declare const ALL_THAI_INSTITUTIONS: ThaiInstitution[];
21
2
  export declare const ACCOUNT_TYPE_DESCRIPTIONS: Record<AccountType, string>;
22
- export declare const SUGGESTED_ASSET_SUBTYPES: string[];
23
- export declare const SUGGESTED_LIABILITY_SUBTYPES: string[];
24
- export declare const SUGGESTED_EXPENSE_SUBTYPES: string[];
25
- export declare const SUGGESTED_INCOME_SUBTYPES: string[];
26
3
  /**
27
4
  * Stringified Thai taxonomy block for the scan/resolve system prompts.
28
5
  * Lists known Thai institutions and suggested subtypes so the model picks
@@ -1,4 +1,4 @@
1
- export const THAI_BANKS = [
1
+ const THAI_BANKS = [
2
2
  { code: "KBANK", label: "Kasikornbank", kind: "bank" },
3
3
  { code: "SCB", label: "Siam Commercial Bank", kind: "bank" },
4
4
  { code: "BBL", label: "Bangkok Bank", kind: "bank" },
@@ -16,7 +16,7 @@ export const THAI_BANKS = [
16
16
  { code: "ICBC-TH", label: "ICBC (Thai)", kind: "bank", notes: "Subsidiary of ICBC China." },
17
17
  { code: "BAAC", label: "Bank for Agriculture and Agricultural Cooperatives", kind: "bank", notes: "State-owned, rural finance." },
18
18
  ];
19
- export const THAI_CARD_ISSUERS = [
19
+ const THAI_CARD_ISSUERS = [
20
20
  { code: "KTC", label: "Krungthai Card", kind: "card_issuer", notes: "Listed subsidiary of KTB." },
21
21
  { code: "AEON", label: "AEON Thana Sinsap", kind: "card_issuer" },
22
22
  { code: "FIRSTCHOICE", label: "Krungsri First Choice", kind: "card_issuer" },
@@ -26,7 +26,7 @@ export const THAI_CARD_ISSUERS = [
26
26
  { code: "DINERS", label: "Diners Club Thailand", kind: "card_issuer" },
27
27
  { code: "UOB-TH", label: "UOB Thailand (Cards)", kind: "card_issuer", notes: "Same legal entity as the bank UOB-TH; now issues both its own card line (UOB Yolo, UOB Premier) and the migrated Citi consumer cards." },
28
28
  ];
29
- export const THAI_WALLETS = [
29
+ const THAI_WALLETS = [
30
30
  { code: "TRUEMONEY", label: "TrueMoney Wallet", kind: "wallet" },
31
31
  { code: "LINEPAY", label: "Rabbit LINE Pay", kind: "wallet" },
32
32
  { code: "SHOPEEPAY", label: "ShopeePay", kind: "wallet" },
@@ -35,10 +35,10 @@ export const THAI_WALLETS = [
35
35
  { code: "MPAY", label: "mPay", kind: "wallet", notes: "AIS-operated." },
36
36
  { code: "PAOTANG", label: "Paotang", kind: "wallet", notes: "Krungthai-operated; government-benefits and tax e-wallet." },
37
37
  ];
38
- export const THAI_PAYMENT_RAILS = [
38
+ const THAI_PAYMENT_RAILS = [
39
39
  { code: "PROMPTPAY", label: "PromptPay", kind: "payment_rail", notes: "National 24/7 interbank rail; appears on transfer slips, not an issuer." },
40
40
  ];
41
- export const THAI_BROKERS = [
41
+ const THAI_BROKERS = [
42
42
  { code: "INNOVESTX", label: "InnovestX Securities", kind: "broker", notes: "Former SCBS; SCBX subsidiary." },
43
43
  { code: "BLS", label: "Bualuang Securities", kind: "broker", notes: "BBL subsidiary." },
44
44
  { code: "KS", label: "Kasikorn Securities", kind: "broker", notes: "KBANK subsidiary." },
@@ -54,7 +54,7 @@ export const THAI_BROKERS = [
54
54
  { code: "DBSVICKERS", label: "DBS Vickers Securities (Thailand)", kind: "broker" },
55
55
  { code: "KTBST", label: "Krungthai Xspring Securities", kind: "broker", notes: "Formerly KTBST; KTB-affiliated." },
56
56
  ];
57
- export const THAI_CRYPTO_EXCHANGES = [
57
+ const THAI_CRYPTO_EXCHANGES = [
58
58
  { code: "BITKUB", label: "Bitkub Exchange", kind: "crypto_exchange", notes: "SEC-licensed; dominant market share." },
59
59
  { code: "UPBIT-TH", label: "Upbit Thailand", kind: "crypto_exchange", notes: "SEC-licensed; subsidiary of South Korean Upbit." },
60
60
  { code: "ORBIX", label: "Orbix Trade", kind: "crypto_exchange", notes: "Former Satang Pro; rebranded under KBank ownership." },
@@ -65,7 +65,7 @@ export const THAI_CRYPTO_EXCHANGES = [
65
65
  { code: "GMO-Z-EX", label: "Z.com EX (GMO-Z.com)", kind: "crypto_exchange", notes: "Japanese GMO subsidiary." },
66
66
  { code: "ZIPMEX", label: "Zipmex", kind: "crypto_exchange", notes: "Defunct since Nov 2023; statements only appear in historical files." },
67
67
  ];
68
- export const THAI_INSURERS = [
68
+ const THAI_INSURERS = [
69
69
  // Life
70
70
  { code: "AIA-TH", label: "AIA Thailand", kind: "insurer", notes: "Life; market leader." },
71
71
  { code: "MUANG-THAI-LIFE", label: "Muang Thai Life Insurance", kind: "insurer", notes: "Life." },
@@ -83,7 +83,7 @@ export const THAI_INSURERS = [
83
83
  { code: "VIRIYAH", label: "Viriyah Insurance", kind: "insurer", notes: "Non-life; #1 motor insurer." },
84
84
  { code: "TOKIO-MARINE", label: "Tokio Marine (Thailand)", kind: "insurer", notes: "Non-life." },
85
85
  ];
86
- export const THAI_GOV = [
86
+ const THAI_GOV = [
87
87
  { code: "REVDEPT", label: "Revenue Department (กรมสรรพากร)", kind: "gov", notes: "Income tax, VAT, excise." },
88
88
  { code: "SSO", label: "Social Security Office (สำนักงานประกันสังคม)", kind: "gov", notes: "Employee + self-employed social security contributions." },
89
89
  { code: "BOT", label: "Bank of Thailand", kind: "gov", notes: "Central bank; rarely on consumer statements outside of regulatory letters." },
@@ -91,21 +91,21 @@ export const THAI_GOV = [
91
91
  { code: "LDT", label: "Department of Lands (กรมที่ดิน)", kind: "gov", notes: "Land registration, property tax." },
92
92
  { code: "CUSTOMS", label: "Customs Department (กรมศุลกากร)", kind: "gov", notes: "Import/export duties." },
93
93
  ];
94
- export const THAI_UTILITIES = [
94
+ const THAI_UTILITIES = [
95
95
  { code: "MEA", label: "Metropolitan Electricity Authority (กฟน.)", kind: "utility", notes: "Electricity for Bangkok, Nonthaburi, Samut Prakan." },
96
96
  { code: "PEA", label: "Provincial Electricity Authority (กฟภ.)", kind: "utility", notes: "Electricity for the rest of Thailand outside MEA's area." },
97
97
  { code: "MWA", label: "Metropolitan Waterworks Authority (กปน.)", kind: "utility", notes: "Water for Bangkok, Nonthaburi, Samut Prakan." },
98
98
  { code: "PWA", label: "Provincial Waterworks Authority (กปภ.)", kind: "utility", notes: "Water for the rest of Thailand." },
99
99
  { code: "EGAT", label: "Electricity Generating Authority of Thailand (กฟผ.)", kind: "utility", notes: "Power generation; rarely appears on consumer bills directly." },
100
100
  ];
101
- export const THAI_TELCOS = [
101
+ const THAI_TELCOS = [
102
102
  { code: "AIS", label: "Advanced Info Service (AIS)", kind: "telco" },
103
103
  { code: "TRUE-CORP", label: "True Corporation", kind: "telco", notes: "Merged entity of True + dtac since March 2023." },
104
104
  { code: "TRUEMOVE", label: "TrueMove H", kind: "telco", notes: "Brand retained under TRUE-CORP per NBTC ruling." },
105
105
  { code: "DTAC", label: "dtac", kind: "telco", notes: "Brand retained under TRUE-CORP per NBTC ruling." },
106
106
  { code: "NT", label: "National Telecom (NT)", kind: "telco", notes: "Former TOT; state-owned, minimal consumer presence." },
107
107
  ];
108
- export const ALL_THAI_INSTITUTIONS = [
108
+ const ALL_THAI_INSTITUTIONS = [
109
109
  ...THAI_BANKS,
110
110
  ...THAI_CARD_ISSUERS,
111
111
  ...THAI_WALLETS,
@@ -124,7 +124,7 @@ export const ACCOUNT_TYPE_DESCRIPTIONS = {
124
124
  expense: "Spending categories (food, transport, utilities, etc.).",
125
125
  equity: "Owner's equity / opening balance equity (for ledger adjustments).",
126
126
  };
127
- export const SUGGESTED_ASSET_SUBTYPES = [
127
+ const SUGGESTED_ASSET_SUBTYPES = [
128
128
  "bank",
129
129
  "cash",
130
130
  "wallet",
@@ -133,7 +133,7 @@ export const SUGGESTED_ASSET_SUBTYPES = [
133
133
  "crypto",
134
134
  "receivable",
135
135
  ];
136
- export const SUGGESTED_LIABILITY_SUBTYPES = [
136
+ const SUGGESTED_LIABILITY_SUBTYPES = [
137
137
  "credit_card",
138
138
  "home_loan",
139
139
  "auto_loan",
@@ -142,7 +142,7 @@ export const SUGGESTED_LIABILITY_SUBTYPES = [
142
142
  "revolving",
143
143
  "deferred_income",
144
144
  ];
145
- export const SUGGESTED_EXPENSE_SUBTYPES = [
145
+ const SUGGESTED_EXPENSE_SUBTYPES = [
146
146
  "food",
147
147
  "transport",
148
148
  "utilities",
@@ -159,7 +159,7 @@ export const SUGGESTED_EXPENSE_SUBTYPES = [
159
159
  "insurance",
160
160
  "other",
161
161
  ];
162
- export const SUGGESTED_INCOME_SUBTYPES = [
162
+ const SUGGESTED_INCOME_SUBTYPES = [
163
163
  "salary",
164
164
  "bonus",
165
165
  "freelance",
@@ -41,10 +41,10 @@ export declare function runRecordAgent(opts: {
41
41
  signal?: AbortSignal;
42
42
  }): Promise<string>;
43
43
  /**
44
- * Resolve-time agent loop. The pipeline calls this once per open unknown with
45
- * that unknown's id/prompt/options in the initial messages. The agent surfaces
46
- * the question, applies the user's chosen answer via mutation tools, and
47
- * finishes with mark_resolve_done.
44
+ * Resolve-time agent loop. The pipeline hands every open unknown in the
45
+ * initial message and drives the loop until `countOpenUnknowns()` reaches 0.
46
+ * Each invocation should close as many rows as possible (via ask_user /
47
+ * close_unknown); the pipeline re-invokes if any remain.
48
48
  */
49
49
  export declare function runResolveAgent(opts: {
50
50
  db: Database.Database;
package/dist/ai/agent.js CHANGED
@@ -16,6 +16,8 @@ async function runAgent({ db, systemPrompt, tools, initialMessages, agentCtx, on
16
16
  throw new AbortedError();
17
17
  };
18
18
  const stepLimit = maxToolSteps ?? MAX_TOOL_STEPS;
19
+ const startTime = Date.now();
20
+ let toolCount = 0;
19
21
  throwIfAborted();
20
22
  let response = await provider.sendMessage({
21
23
  model: config.model,
@@ -26,8 +28,6 @@ async function runAgent({ db, systemPrompt, tools, initialMessages, agentCtx, on
26
28
  thinking: useThinking ? { type: "enabled", budget_tokens: config.thinkingBudget } : undefined,
27
29
  signal,
28
30
  });
29
- const startTime = Date.now();
30
- let toolCount = 0;
31
31
  while (response.stopReason === "tool_use" && toolCount < stepLimit) {
32
32
  throwIfAborted();
33
33
  messages.push({ role: "assistant", content: response.content });
@@ -40,7 +40,8 @@ async function runAgent({ db, systemPrompt, tools, initialMessages, agentCtx, on
40
40
  toolResults.push({
41
41
  type: "tool_result",
42
42
  tool_use_id: block.id,
43
- content: redact(result),
43
+ content: redact(result.content),
44
+ ...(result.isError ? { is_error: true } : {}),
44
45
  });
45
46
  }
46
47
  }
@@ -155,10 +156,10 @@ export async function runRecordAgent(opts) {
155
156
  return text;
156
157
  }
157
158
  /**
158
- * Resolve-time agent loop. The pipeline calls this once per open unknown with
159
- * that unknown's id/prompt/options in the initial messages. The agent surfaces
160
- * the question, applies the user's chosen answer via mutation tools, and
161
- * finishes with mark_resolve_done.
159
+ * Resolve-time agent loop. The pipeline hands every open unknown in the
160
+ * initial message and drives the loop until `countOpenUnknowns()` reaches 0.
161
+ * Each invocation should close as many rows as possible (via ask_user /
162
+ * close_unknown); the pipeline re-invokes if any remain.
162
163
  */
163
164
  export async function runResolveAgent(opts) {
164
165
  const systemPrompt = redact(buildResolveSystemPrompt(opts.db, opts.prompt));
@@ -170,7 +171,7 @@ export async function runResolveAgent(opts) {
170
171
  agentCtx: opts.agentCtx,
171
172
  onProgress: opts.onProgress,
172
173
  signal: opts.signal,
173
- maxToolSteps: 30,
174
+ maxToolSteps: 60,
174
175
  });
175
176
  return text;
176
177
  }
@@ -1,4 +1,2 @@
1
- export declare function getContextPath(): string;
2
1
  export declare function readContext(): string;
3
- export declare function writeContext(content: string): void;
4
2
  export declare function createContextTemplate(userName: string): void;
@@ -1,7 +1,7 @@
1
1
  import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "fs";
2
2
  import { dirname, resolve } from "path";
3
3
  import { getPlasalidDir } from "../config.js";
4
- export function getContextPath() {
4
+ function getContextPath() {
5
5
  return resolve(getPlasalidDir(), "context.md");
6
6
  }
7
7
  export function readContext() {
@@ -15,7 +15,7 @@ export function readContext() {
15
15
  return "";
16
16
  }
17
17
  }
18
- export function writeContext(content) {
18
+ function writeContext(content) {
19
19
  const p = getContextPath();
20
20
  const dir = dirname(p);
21
21
  if (!existsSync(dir))
@@ -15,6 +15,7 @@ export declare function getConversationHistory(db: Database.Database, limit?: nu
15
15
  export declare function saveMessage(db: Database.Database, role: "user" | "assistant", content: string): void;
16
16
  /** Memories */
17
17
  export declare function getMemories(db: Database.Database): Memory[];
18
+ export declare function countMemories(db: Database.Database): number;
18
19
  /**
19
20
  * Idempotent on (category, content): a verbatim repeat is a no-op. Semantic
20
21
  * dedup (different wording for the same rule) is the agent's job — the persona
package/dist/ai/memory.js CHANGED
@@ -9,6 +9,10 @@ export function saveMessage(db, role, content) {
9
9
  export function getMemories(db) {
10
10
  return db.prepare(`SELECT id, content, category, created_at FROM memories ORDER BY created_at DESC`).all();
11
11
  }
12
+ export function countMemories(db) {
13
+ const row = db.prepare(`SELECT COUNT(*) AS n FROM memories`).get();
14
+ return row.n;
15
+ }
12
16
  /**
13
17
  * Idempotent on (category, content): a verbatim repeat is a no-op. Semantic
14
18
  * dedup (different wording for the same rule) is the agent's job — the persona
@@ -40,7 +40,7 @@ Vocabulary:
40
40
 
41
41
  Rules:
42
42
  1. Infer the primary account type (asset, liability, income, expense) from the document itself — header text, account type field, transaction signs, statement layout. Do not rely on the filename or directory.
43
- 2. Every transaction must become a balanced \`record_transaction\` call. Total debits must equal total credits per currency.
43
+ 2. Try to make every \`record_transaction\` call balanced — total debits should equal total credits per currency. If you genuinely can't pair a row, post what the document shows and the system will append a closing entry on \`equity:adjustments\` automatically. Do not invent counter-postings to force balance.
44
44
  3. Account-type conventions (debit/credit semantics, unchanged from regular bookkeeping):
45
45
  - **Asset** (e.g. bank, cash): DEBIT increases, CREDIT decreases.
46
46
  - **Liability** (e.g. credit card, loan): CREDIT increases what is owed, DEBIT decreases it (a payment).
@@ -176,12 +176,9 @@ For each group, call \`ask_user\` ONCE, passing every sibling's id in \`related_
176
176
 
177
177
  **Step 5 — Learn and finalize.** After every non-skip user answer that implies a generalizable rule (e.g. "Lazada on KTC Card → Shopping"), call \`save_memory(content=<rule>, category="scanning_hint")\` so the next scan applies it silently. For merchant categorization, also call \`set_merchant_default_account\`. Phrase rules as reusable classifications, not one-event records (GOOD: "Lazada Thailand on KTC Card ••5678 → expense:shopping." BAD: "On 2026-03-15 the user said Shopping.").
178
178
 
179
- **Closing invariant.** Every unknown in the input list must have \`resolved_at\` set by the end. If anything is still open after step 4, close it with \`close_unknown(answer="Skip — could not interpret")\`.
179
+ **Closing invariant.** Every unknown in the input list must have \`resolved_at\` set by the end. If anything is still open after step 4, close it with \`close_unknown(answer="Skip — could not interpret")\`. The pipeline reads the DB after you finish — if any unknown is still open it will re-invoke you with the leftovers, so always finish each row before yielding.
180
180
 
181
- End with one \`mark_resolve_done\` call. Summary format: a single sentence with counts. Examples:
182
- - "Applied 9 from memory; resolved 2 groups (5 unknowns) by user answer; deferred 1 via Skip."
183
- - "All 14 unknowns applied silently from memory rules."
184
- - "Resolved 1 group (3 Lazada postings) as Shopping; saved the merchant default."
181
+ **Tool errors.** If a tool result comes back marked as an error (e.g. a malformed id, a row that no longer exists, a constraint violation), do NOT call \`close_unknown\` for the affected row. Either fix the input and retry the same mutation, or close that one row with \`close_unknown(answer="Skip — tool error: <short reason>")\` so the loop can move on. Never close a row whose underlying mutation failed.
185
182
 
186
183
  Unknown kind → mutation tool map (use after a user answer in step 4):
187
184
  - \`uncategorized\` / \`uncategorized_expense\` → \`update_posting(account_id=...)\` for each posting on the transaction. If the transaction has a merchant_id, also \`set_merchant_default_account\`.
@@ -38,6 +38,7 @@ export interface NormalizedToolResult {
38
38
  type: "tool_result";
39
39
  tool_use_id: string;
40
40
  content: string;
41
+ is_error?: boolean;
41
42
  }
42
43
  export interface ToolDefinition {
43
44
  name: string;
@@ -1,7 +1 @@
1
- /**
2
- * Idle/thinking phrases used by the chat hook and the scan spinner when the
3
- * AI is composing a response (no specific tool to label). Kept in one place so
4
- * both surfaces stay in sync.
5
- */
6
- export declare const THINKING_PHRASES: string[];
7
1
  export declare function pickThinking(): string;
@@ -3,12 +3,37 @@
3
3
  * AI is composing a response (no specific tool to label). Kept in one place so
4
4
  * both surfaces stay in sync.
5
5
  */
6
- export const THINKING_PHRASES = [
6
+ const THINKING_PHRASES = [
7
7
  "Thinking...",
8
- "Looking through your transactions...",
9
- "Checking your accounts...",
8
+ "Working through the numbers...",
9
+ "Doing the math...",
10
+ "Running the numbers...",
10
11
  "Crunching the numbers...",
11
- "Pulling up your data...",
12
+ "Connecting the dots...",
13
+ "Following the money...",
14
+ "Reading between the lines...",
15
+ "Putting the pieces together...",
16
+ "Lining things up...",
17
+ "Sifting through the details...",
18
+ "Weighing it up...",
19
+ "Tracing the trail...",
20
+ "Cross-checking...",
21
+ "Adding it up...",
22
+ "Sorting through it...",
23
+ "Considering the angles...",
24
+ "Taking a closer look...",
25
+ "Squaring things up...",
26
+ "Tallying things up...",
27
+ "Squinting at the numbers...",
28
+ "Doing math, the slow kind...",
29
+ "Computing... probably correctly...",
30
+ "Pondering quietly...",
31
+ "Joining the dots...",
32
+ "Making sense of it...",
33
+ "Sharpening the pencil...",
34
+ "Catching up on the details...",
35
+ "Comparing notes...",
36
+ "Pulling the threads together...",
12
37
  ];
13
38
  export function pickThinking() {
14
39
  return THINKING_PHRASES[Math.floor(Math.random() * THINKING_PHRASES.length)];
@@ -3,6 +3,10 @@ import type { ToolDefinition } from "../provider.js";
3
3
  import type { AgentExecutionContext, ToolProfile } from "./types.js";
4
4
  export type { AgentExecutionContext, ToolProfile } from "./types.js";
5
5
  export declare function getToolDefinitions(profile: ToolProfile): ToolDefinition[];
6
- export declare function executeTool(db: Database.Database, name: string, input: any, ctx?: AgentExecutionContext): Promise<string>;
6
+ export interface ExecuteToolResult {
7
+ content: string;
8
+ isError: boolean;
9
+ }
10
+ export declare function executeTool(db: Database.Database, name: string, input: any, ctx?: AgentExecutionContext): Promise<ExecuteToolResult>;
7
11
  /** Human-readable labels shown in the spinner during tool calls. */
8
12
  export declare const TOOL_LABELS: Record<string, string>;
@@ -27,23 +27,29 @@ const PROFILES = {
27
27
  export function getToolDefinitions(profile) {
28
28
  return PROFILES[profile].flatMap(m => m.DEFS);
29
29
  }
30
+ const MODULES = [
31
+ commonTools,
32
+ readTools,
33
+ accountIngestTools,
34
+ scanUnknownTools,
35
+ resolveIngestTools,
36
+ scanTools,
37
+ resolveTools,
38
+ recordTools,
39
+ merchantTools,
40
+ ];
30
41
  export async function executeTool(db, name, input, ctx) {
31
- for (const mod of [
32
- commonTools,
33
- readTools,
34
- accountIngestTools,
35
- scanUnknownTools,
36
- resolveIngestTools,
37
- scanTools,
38
- resolveTools,
39
- recordTools,
40
- merchantTools,
41
- ]) {
42
- const result = await mod.execute(db, name, input, ctx);
43
- if (result !== undefined)
44
- return result;
42
+ try {
43
+ for (const mod of MODULES) {
44
+ const result = await mod.execute(db, name, input, ctx);
45
+ if (result !== undefined)
46
+ return { content: result, isError: false };
47
+ }
48
+ return { content: `Unknown tool: ${name}`, isError: true };
49
+ }
50
+ catch (err) {
51
+ return { content: err?.message ?? String(err), isError: true };
45
52
  }
46
- return `Unknown tool: ${name}`;
47
53
  }
48
54
  /** Human-readable labels shown in the spinner during tool calls. */
49
55
  export const TOOL_LABELS = {