plasalid 0.6.10 → 0.7.1
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.
- package/README.md +4 -7
- package/dist/accounts/taxonomy.d.ts +0 -23
- package/dist/accounts/taxonomy.js +15 -15
- package/dist/ai/agent.d.ts +4 -4
- package/dist/ai/agent.js +9 -8
- package/dist/ai/context.d.ts +0 -2
- package/dist/ai/context.js +2 -2
- package/dist/ai/memory.d.ts +1 -0
- package/dist/ai/memory.js +4 -0
- package/dist/ai/personas.js +3 -6
- package/dist/ai/provider.d.ts +1 -0
- package/dist/ai/thinking.d.ts +0 -6
- package/dist/ai/thinking.js +29 -4
- package/dist/ai/tools/index.d.ts +5 -1
- package/dist/ai/tools/index.js +21 -15
- package/dist/ai/tools/ingest.js +94 -110
- package/dist/ai/tools/resolve.js +15 -44
- package/dist/cli/commands/accounts.d.ts +4 -1
- package/dist/cli/commands/accounts.js +39 -20
- package/dist/cli/commands/scan.js +47 -47
- package/dist/cli/commands/status.js +81 -14
- package/dist/cli/commands/transactions.d.ts +3 -1
- package/dist/cli/commands/transactions.js +37 -34
- package/dist/cli/format.d.ts +0 -1
- package/dist/cli/format.js +1 -1
- package/dist/cli/helper.d.ts +11 -0
- package/dist/cli/helper.js +24 -0
- package/dist/cli/index.js +14 -10
- package/dist/cli/ink/AccountsBrowser.d.ts +7 -0
- package/dist/cli/ink/AccountsBrowser.js +149 -0
- package/dist/cli/ink/ListBrowser.d.ts +38 -0
- package/dist/cli/ink/ListBrowser.js +154 -0
- package/dist/cli/ink/TransactionsBrowser.d.ts +6 -0
- package/dist/cli/ink/TransactionsBrowser.js +87 -0
- package/dist/cli/ink/hooks/useFooterText.js +31 -14
- package/dist/cli/ink/runBrowser.d.ts +7 -0
- package/dist/cli/ink/runBrowser.js +24 -0
- package/dist/cli/ux.d.ts +4 -5
- package/dist/cli/ux.js +87 -66
- package/dist/db/connection.d.ts +0 -2
- package/dist/db/connection.js +0 -5
- package/dist/db/queries/files.d.ts +11 -0
- package/dist/db/queries/files.js +16 -0
- package/dist/db/queries/recurrences.d.ts +7 -0
- package/dist/db/queries/recurrences.js +21 -0
- package/dist/db/queries/transactions.d.ts +28 -4
- package/dist/db/queries/transactions.js +68 -15
- package/dist/db/queries/unknowns.d.ts +3 -5
- package/dist/db/queries/unknowns.js +4 -4
- package/dist/db/schema.js +8 -0
- package/dist/lib/runPasses.d.ts +30 -0
- package/dist/lib/runPasses.js +15 -0
- package/dist/resolver/pipeline.d.ts +6 -6
- package/dist/resolver/pipeline.js +50 -22
- package/dist/scanner/inspectors/similarities.js +14 -16
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -26,14 +26,11 @@ Moreover, Plasalid comes with a built-in agentic chat that queries the data dire
|
|
|
26
26
|
The data ledger also serves as a harness, open to any AI agent that connects to it, so the picture you assemble once is reusable across whatever tools you choose to use.
|
|
27
27
|
|
|
28
28
|
<p align="center">
|
|
29
|
-
<img src=".github/plasalid-demo.
|
|
29
|
+
<img src=".github/plasalid-demo.png" alt="demo" width="100%" />
|
|
30
30
|
</p>
|
|
31
31
|
|
|
32
|
-
|
|
33
32
|
## Features
|
|
34
33
|
|
|
35
|
-

|
|
36
|
-
|
|
37
34
|
### Unified ledger from any financial documents
|
|
38
35
|
|
|
39
36
|
- **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 +86,11 @@ plasalid # Interactive chat with your data
|
|
|
89
86
|
plasalid setup # Configure API key, encryption, and data directory
|
|
90
87
|
plasalid data # Open the Plasalid data folder in your file explorer
|
|
91
88
|
plasalid accounts # Show the chart of accounts with balances
|
|
92
|
-
plasalid status # Net worth and this-month income/expense totals
|
|
93
89
|
plasalid transactions # List transactions and their postings (filter by --account, --from, --to, --query, --limit)
|
|
94
|
-
plasalid
|
|
90
|
+
plasalid status # Net worth and this-month income/expense totals
|
|
91
|
+
plasalid record [utterance] # Add a manual transaction, account, balance, or merchant from a plain-language line
|
|
95
92
|
plasalid scan [regex] [--force] # Scan new PDFs; --force cascade-deletes prior records before re-scanning
|
|
96
|
-
plasalid revert
|
|
93
|
+
plasalid revert [regex] # Delete scanned files matching <regex> and their transactions
|
|
97
94
|
plasalid resolve # Walk every open unknown and apply your decision (--account, --from, --to, --kind also accepted)
|
|
98
95
|
```
|
|
99
96
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
+
const SUGGESTED_INCOME_SUBTYPES = [
|
|
163
163
|
"salary",
|
|
164
164
|
"bonus",
|
|
165
165
|
"freelance",
|
package/dist/ai/agent.d.ts
CHANGED
|
@@ -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
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
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:
|
|
174
|
+
maxToolSteps: 60,
|
|
174
175
|
});
|
|
175
176
|
return text;
|
|
176
177
|
}
|
package/dist/ai/context.d.ts
CHANGED
package/dist/ai/context.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
18
|
+
function writeContext(content) {
|
|
19
19
|
const p = getContextPath();
|
|
20
20
|
const dir = dirname(p);
|
|
21
21
|
if (!existsSync(dir))
|
package/dist/ai/memory.d.ts
CHANGED
|
@@ -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
|
package/dist/ai/personas.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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\`.
|
package/dist/ai/provider.d.ts
CHANGED
package/dist/ai/thinking.d.ts
CHANGED
|
@@ -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;
|
package/dist/ai/thinking.js
CHANGED
|
@@ -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
|
-
|
|
6
|
+
const THINKING_PHRASES = [
|
|
7
7
|
"Thinking...",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
8
|
+
"Working through the numbers...",
|
|
9
|
+
"Doing the math...",
|
|
10
|
+
"Running the numbers...",
|
|
10
11
|
"Crunching the numbers...",
|
|
11
|
-
"
|
|
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)];
|
package/dist/ai/tools/index.d.ts
CHANGED
|
@@ -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
|
|
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>;
|
package/dist/ai/tools/index.js
CHANGED
|
@@ -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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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 = {
|