plasalid 0.4.1 → 0.5.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 +15 -14
- package/dist/ai/agent.d.ts +15 -2
- package/dist/ai/agent.js +21 -2
- package/dist/ai/memory.d.ts +2 -0
- package/dist/ai/memory.js +2 -2
- package/dist/ai/personas.d.ts +2 -1
- package/dist/ai/personas.js +115 -45
- package/dist/ai/prompt-sections.d.ts +5 -0
- package/dist/ai/prompt-sections.js +26 -8
- package/dist/ai/system-prompt.d.ts +11 -0
- package/dist/ai/system-prompt.js +21 -6
- package/dist/ai/thinking.js +1 -1
- package/dist/ai/tools/common.js +2 -5
- package/dist/ai/tools/index.js +28 -8
- package/dist/ai/tools/ingest.d.ts +2 -1
- package/dist/ai/tools/ingest.js +262 -151
- package/dist/ai/tools/merchants.d.ts +2 -0
- package/dist/ai/tools/merchants.js +117 -0
- package/dist/ai/tools/read.js +31 -29
- package/dist/ai/tools/record.d.ts +2 -0
- package/dist/ai/tools/record.js +188 -0
- package/dist/ai/tools/review.js +77 -80
- package/dist/ai/tools/scan.js +1 -1
- package/dist/ai/tools/types.d.ts +15 -6
- package/dist/cli/commands/accounts.js +33 -25
- package/dist/cli/commands/record.d.ts +4 -0
- package/dist/cli/commands/record.js +119 -0
- package/dist/cli/commands/revert.js +1 -1
- package/dist/cli/commands/scan.js +15 -19
- package/dist/cli/commands/status.js +6 -9
- package/dist/cli/commands/transactions.js +36 -41
- package/dist/cli/format.d.ts +2 -0
- package/dist/cli/format.js +7 -2
- package/dist/cli/index.js +19 -7
- package/dist/cli/ink/hooks/useFooterText.js +2 -1
- package/dist/cli/ink/scan_dashboard.d.ts +1 -1
- package/dist/cli/ink/scan_dashboard.js +2 -2
- package/dist/cli/setup.d.ts +0 -1
- package/dist/cli/setup.js +2 -8
- package/dist/currency.d.ts +3 -0
- package/dist/currency.js +12 -1
- package/dist/db/queries/account_balance.d.ts +83 -4
- package/dist/db/queries/account_balance.js +239 -20
- package/dist/db/queries/action_log.d.ts +29 -0
- package/dist/db/queries/action_log.js +27 -0
- package/dist/db/queries/concerns.d.ts +10 -7
- package/dist/db/queries/concerns.js +20 -16
- package/dist/db/queries/journal.d.ts +1 -0
- package/dist/db/queries/merchants.d.ts +42 -0
- package/dist/db/queries/merchants.js +120 -0
- package/dist/db/queries/recurrences.d.ts +3 -3
- package/dist/db/queries/recurrences.js +32 -34
- package/dist/db/queries/search.d.ts +5 -4
- package/dist/db/queries/search.js +16 -12
- package/dist/db/queries/transactions.d.ts +167 -0
- package/dist/db/queries/transactions.js +320 -0
- package/dist/db/schema.js +51 -9
- package/dist/reviewer/pipeline.d.ts +4 -4
- package/dist/reviewer/pipeline.js +4 -4
- package/dist/reviewer/prompts.js +4 -4
- package/dist/scanner/buffer.d.ts +24 -21
- package/dist/scanner/buffer.js +18 -18
- package/dist/scanner/pipeline.d.ts +3 -2
- package/dist/scanner/pipeline.js +33 -36
- package/dist/scanner/prompts.js +3 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
1
|
<h1 align="center">Plasalid</h1>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<strong>
|
|
4
|
+
<strong>The Harness for Personal Finance</strong>
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
Turn the
|
|
8
|
+
Turn the financial documents you already receive into a structured, AI-readable context.
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
<br />
|
|
13
13
|
|
|
14
|
-
In markets like Thailand there's no Plaid: no public API that gives apps a unified view of every account, no easy way to assemble a complete picture of your money. Knowing where you stand means logging into five bank apps one by one — and most people just don't bother. Plasalid
|
|
15
|
-
|
|
16
|
-
Plasalid ships with a built-in chat so you can start asking questions right away, but the data layer is the product. A local MCP / API server is coming next, so any AI tool — Claude Desktop, your own scripts, dashboards, automations — can read the same journal without further integration. Local, private, yours.
|
|
14
|
+
In markets like Thailand there's no Plaid: no public API that gives apps a unified view of every account, no easy way to assemble a complete picture of your money. Knowing where you stand means logging into five bank apps one by one — and most people just don't bother. Plasalid goes further than a local Plaid — it's a harness layer: drop your bank and credit-card statement PDFs into a folder and Plasalid parses every transaction, balance into a double-entry database on your own machine, ready for any AI to plug into. No cloud aggregator. No upstream account to trust. One source of truth for every account.
|
|
17
15
|
|
|
18
16
|
## Features
|
|
19
17
|
|
|
20
|
-
Plasalid is a chain of three stages: **Scan → Review → Chat.** Today's chat is one consumer
|
|
18
|
+
Plasalid is a chain of three stages: **Scan → Review → Chat.** Underneath sits a three-layer ledger: hierarchical accounts (small, stable, colon-path ids like `expense:food:groceries`), deduplicated merchants (raw statement descriptors collapse to one canonical name with a learned default category), and balanced transactions with postings. Today's chat is one consumer; the same data will power a local MCP / API server next.
|
|
21
19
|
|
|
22
20
|
### Scan — parse without blocking
|
|
23
21
|
|
|
24
|
-
- **Drop PDFs in, get
|
|
25
|
-
- **
|
|
22
|
+
- **Drop PDFs in, get balanced transactions out.** The scanner infers account type, masks account numbers, converts Buddhist-Era dates, and posts a double-entry record for every transaction.
|
|
23
|
+
- **Merchants as first-class.** Statement descriptors (`STARBUCKS #1234 BKK`, `Starbucks #5678 BANGKOK`) normalize to one canonical merchant. Categorize a merchant once; future statements use the cached default category — the LLM skips re-categorizing known merchants.
|
|
24
|
+
- **Never pauses to ask you.** Ambiguous rows post best-guess transactions with a structured *concern* attached; lines the scanner can't confidently categorize land in `expense:uncategorized` for the review cleanup pass; unparseable rows are skipped, not guessed. A missing row is better than a wrong row — review clears them up later.
|
|
26
25
|
- **Encrypted PDFs handled inline.** Statement password-protected? Plasalid prompts you once, remembers the password (AES-GCM at rest) under a filename pattern, and unlocks next month's statement silently.
|
|
27
26
|
|
|
28
27
|
### Review — see the whole picture
|
|
29
28
|
|
|
29
|
+
- **Uncategorized cleanup.** Every posting parked in `expense:uncategorized` shows up here; categorizing one teaches the merchant's default account for next time, so a single answer can resolve dozens of rows across future months.
|
|
30
30
|
- **Connects related transactions.** A transfer that lands on both a bank statement and a credit-card statement is surfaced as one pair; merge on confirmation.
|
|
31
|
-
- **Recurrences as first-class data.** Spotify, salary, rent get their own `recurrences` rows with cadence (weekly / biweekly / monthly / annually) and next-expected dates, linked back to every member
|
|
31
|
+
- **Recurrences as first-class data.** Spotify, salary, rent get their own `recurrences` rows with cadence (weekly / biweekly / monthly / annually) and next-expected dates, linked back to every member transaction. Not a UI category — a structured fact any AI consumer can read.
|
|
32
32
|
- **Step-by-step clarification.** Re-poses every scan-noted concern as one focused question; loops until concerns are clear or you skip them. `--dry-run` previews everything; writes only after you confirm.
|
|
33
33
|
|
|
34
34
|
### Chat — ask questions about your data
|
|
35
35
|
|
|
36
|
-
- **Reads your real
|
|
36
|
+
- **Reads your real transactions and postings.** Not generic categories. "Where did ฿14k go in March?" gets an answer drawn from actual postings against real expense categories, with figures, dates, account names, and merchants cited; nothing invented.
|
|
37
37
|
- **One of many possible consumers.** A local MCP / API server is coming next so external AI tools (Claude Desktop, your own scripts, dashboards) read the same data without sync, login, or upload.
|
|
38
38
|
|
|
39
39
|
### Built to be plugged into
|
|
@@ -70,7 +70,7 @@ Other day-to-day commands:
|
|
|
70
70
|
- `plasalid scan <regex>` — only scan files whose path matches the regex.
|
|
71
71
|
- `plasalid scan <regex> --force` — re-scan matching files (replaces prior records).
|
|
72
72
|
- `plasalid review --dry-run` — preview the picture (correlated transactions, recurrences, open concerns) without writing; re-run without `--dry-run` to step through fixes interactively.
|
|
73
|
-
- `plasalid revert <regex>` — delete scanned files matching the regex and every
|
|
73
|
+
- `plasalid revert <regex>` — delete scanned files matching the regex and every transaction derived from them.
|
|
74
74
|
|
|
75
75
|
## Commands
|
|
76
76
|
|
|
@@ -82,9 +82,10 @@ plasalid setup # Configure API key, encryption, and data di
|
|
|
82
82
|
plasalid data # Open the Plasalid data folder in your OS file explorer
|
|
83
83
|
plasalid accounts # Show the chart of accounts with balances
|
|
84
84
|
plasalid status # Net worth and this-month income/expense totals
|
|
85
|
-
plasalid transactions # List
|
|
85
|
+
plasalid transactions # List transactions and their postings (filter by --account, --from, --to, --query, --limit)
|
|
86
|
+
plasalid record <utterance> # Add a manual transaction, account, balance, or merchant from a plain-language line
|
|
86
87
|
plasalid scan [regex] [--force] # Scan new PDFs; --force cascade-deletes prior records before re-scanning
|
|
87
|
-
plasalid revert <regex> # Delete scanned files matching <regex> and their
|
|
88
|
+
plasalid revert <regex> # Delete scanned files matching <regex> and their transactions
|
|
88
89
|
plasalid review [--dry-run] # Connect related transactions, learn recurring rhythms, resolve open concerns (--account, --from, --to also accepted)
|
|
89
90
|
```
|
|
90
91
|
|
|
@@ -133,7 +134,7 @@ Plasalid stores everything in `~/.plasalid/`:
|
|
|
133
134
|
data/ # Drop any PDFs here (subfolders allowed; AI classifies)
|
|
134
135
|
```
|
|
135
136
|
|
|
136
|
-
`db.sqlite` holds the
|
|
137
|
+
`db.sqlite` holds the three-layer ledger (hierarchical accounts, deduplicated merchants with learned default categories, transactions and postings), scan history, open concerns awaiting review, recurring transactions (Spotify, salary, rent — recognized during review and linked from each member transaction), an action log for record-mode audit, persisted long-term memories, and AES-GCM-encrypted PDF passwords keyed by filename pattern. Everything is wrapped in libsql's AES-256 page encryption.
|
|
137
138
|
|
|
138
139
|
### Environment Variables
|
|
139
140
|
|
package/dist/ai/agent.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type Database from "libsql";
|
|
2
|
-
import { type ScanPromptOptions, type ReviewPromptOptions } from "./system-prompt.js";
|
|
2
|
+
import { type ScanPromptOptions, type ReviewPromptOptions, type RecordPromptOptions } from "./system-prompt.js";
|
|
3
3
|
import { type AgentExecutionContext } from "./tools/index.js";
|
|
4
4
|
import type { NormalizedMessage } from "./provider.js";
|
|
5
5
|
export type ProgressCallback = (event: {
|
|
@@ -30,7 +30,20 @@ export declare function runScanAgent(opts: {
|
|
|
30
30
|
signal?: AbortSignal;
|
|
31
31
|
}): Promise<string>;
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
33
|
+
* Record-time agent loop. Takes one natural-language utterance and walks the
|
|
34
|
+
* record tool profile (read tools + account/entry writers + adjust_balance +
|
|
35
|
+
* clarify). Single-shot — does not persist conversation history.
|
|
36
|
+
*/
|
|
37
|
+
export declare function runRecordAgent(opts: {
|
|
38
|
+
db: Database.Database;
|
|
39
|
+
initialMessages: NormalizedMessage[];
|
|
40
|
+
prompt: RecordPromptOptions;
|
|
41
|
+
agentCtx: AgentExecutionContext;
|
|
42
|
+
onProgress?: ProgressCallback;
|
|
43
|
+
signal?: AbortSignal;
|
|
44
|
+
}): Promise<string>;
|
|
45
|
+
/**
|
|
46
|
+
* Review-time agent loop. Surveys the existing transactions with the review
|
|
34
47
|
* tool profile (read tools + write/merge/delete primitives + recurrence
|
|
35
48
|
* detection/recording).
|
|
36
49
|
*/
|
package/dist/ai/agent.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { config } from "../config.js";
|
|
2
|
-
import { buildChatSystemPrompt, buildScanSystemPrompt, buildReviewSystemPrompt, } from "./system-prompt.js";
|
|
2
|
+
import { buildChatSystemPrompt, buildScanSystemPrompt, buildReviewSystemPrompt, buildRecordSystemPrompt, } from "./system-prompt.js";
|
|
3
3
|
import { getToolDefinitions, executeTool } from "./tools/index.js";
|
|
4
4
|
import { getConversationHistory, saveMessage } from "./memory.js";
|
|
5
5
|
import { redact, unredact } from "./redactor.js";
|
|
@@ -136,7 +136,26 @@ export async function runScanAgent(opts) {
|
|
|
136
136
|
return text;
|
|
137
137
|
}
|
|
138
138
|
/**
|
|
139
|
-
*
|
|
139
|
+
* Record-time agent loop. Takes one natural-language utterance and walks the
|
|
140
|
+
* record tool profile (read tools + account/entry writers + adjust_balance +
|
|
141
|
+
* clarify). Single-shot — does not persist conversation history.
|
|
142
|
+
*/
|
|
143
|
+
export async function runRecordAgent(opts) {
|
|
144
|
+
const systemPrompt = redact(buildRecordSystemPrompt(opts.db, opts.prompt));
|
|
145
|
+
const { text } = await runAgent({
|
|
146
|
+
db: opts.db,
|
|
147
|
+
systemPrompt,
|
|
148
|
+
tools: getToolDefinitions("record"),
|
|
149
|
+
initialMessages: opts.initialMessages,
|
|
150
|
+
agentCtx: opts.agentCtx,
|
|
151
|
+
onProgress: opts.onProgress,
|
|
152
|
+
signal: opts.signal,
|
|
153
|
+
maxToolSteps: 30,
|
|
154
|
+
});
|
|
155
|
+
return text;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Review-time agent loop. Surveys the existing transactions with the review
|
|
140
159
|
* tool profile (read tools + write/merge/delete primitives + recurrence
|
|
141
160
|
* detection/recording).
|
|
142
161
|
*/
|
package/dist/ai/memory.d.ts
CHANGED
|
@@ -10,8 +10,10 @@ export interface Memory {
|
|
|
10
10
|
category: string;
|
|
11
11
|
created_at: string;
|
|
12
12
|
}
|
|
13
|
+
/** Conversation history */
|
|
13
14
|
export declare function getConversationHistory(db: Database.Database, limit?: number): ConversationMessage[];
|
|
14
15
|
export declare function saveMessage(db: Database.Database, role: "user" | "assistant", content: string): void;
|
|
16
|
+
/** Memories */
|
|
15
17
|
export declare function getMemories(db: Database.Database): Memory[];
|
|
16
18
|
/**
|
|
17
19
|
* Idempotent on (category, content): a verbatim repeat is a no-op. Semantic
|
package/dist/ai/memory.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
/** Conversation history */
|
|
2
2
|
export function getConversationHistory(db, limit = 20) {
|
|
3
3
|
return db.prepare(`SELECT role, content, created_at FROM conversation_history ORDER BY id DESC LIMIT ?`).all(limit).reverse();
|
|
4
4
|
}
|
|
5
5
|
export function saveMessage(db, role, content) {
|
|
6
6
|
db.prepare(`INSERT INTO conversation_history (role, content) VALUES (?, ?)`).run(role, content);
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
/** Memories */
|
|
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
|
}
|
package/dist/ai/personas.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Persona text constants for the
|
|
2
|
+
* Persona text constants for the four agent profiles. These are pure prose —
|
|
3
3
|
* no logic, no template assembly. The system-prompt builders import them and
|
|
4
4
|
* concat them with section helpers from prompt-sections.ts.
|
|
5
5
|
*
|
|
@@ -7,4 +7,5 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function chatPersona(name: string): string;
|
|
9
9
|
export declare const SCAN_PERSONA: string;
|
|
10
|
+
export declare const RECORD_PERSONA: string;
|
|
10
11
|
export declare const REVIEW_PERSONA: string;
|
package/dist/ai/personas.js
CHANGED
|
@@ -1,58 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Persona text constants for the
|
|
2
|
+
* Persona text constants for the four agent profiles. These are pure prose —
|
|
3
3
|
* no logic, no template assembly. The system-prompt builders import them and
|
|
4
4
|
* concat them with section helpers from prompt-sections.ts.
|
|
5
5
|
*
|
|
6
6
|
* Edit a persona's voice or rules here without touching the builders.
|
|
7
7
|
*/
|
|
8
8
|
export function chatPersona(name) {
|
|
9
|
-
return `Your name is Plasalid ("ปลาสลิด") —
|
|
9
|
+
return `Your name is Plasalid ("ปลาสลิด") — ${name}'s local personal-finance harness. You answer ${name}'s questions about their own ledger by calling the read tools below. Local data only — no third-party aggregator, no upstream sync, no cloud.
|
|
10
10
|
|
|
11
|
-
## How you
|
|
12
|
-
- Lead with the
|
|
13
|
-
- Always cite
|
|
14
|
-
-
|
|
15
|
-
- Be
|
|
16
|
-
- Be concise. 2-4 sentences for simple questions. Skip preamble like "Great question!" or "Let me look that up." Just answer.
|
|
17
|
-
- Warm but direct. Celebrate wins genuinely. Flag problems without sugarcoating.
|
|
11
|
+
## How you answer
|
|
12
|
+
- Lead with the number, not preamble. "Dining was ฿2,400 in March, up ฿900 from February." — not "Here's the breakdown."
|
|
13
|
+
- Always cite real figures, dates, account names, and merchant names from tool results. Never invent data.
|
|
14
|
+
- Stick to what was asked. The harness reports; recommendations are ${name}'s call. If ${name} explicitly asks "what should I do?", you can offer options drawn from the data — never proactive unsolicited advice.
|
|
15
|
+
- Be concise. 2–4 sentences for simple questions. Skip "Great question!", "Let me look that up.", and similar openers.
|
|
18
16
|
|
|
19
17
|
## How you work
|
|
20
|
-
1. Call the read tools to look up current data — never guess balances, dates, or
|
|
21
|
-
2.
|
|
22
|
-
3. For
|
|
23
|
-
4.
|
|
24
|
-
5. For questions about ${name} themselves (name, family, employer, household), answer from the "## About ${name}" block — it's authoritative. If a fact isn't there, say so plainly; don't redirect biographical questions to \`plasalid scan\`.
|
|
25
|
-
6. Default currency is THB unless an account is explicitly in another. Don't mix currencies.
|
|
18
|
+
1. Call the read tools to look up current data — never guess balances, dates, transactions, or postings.
|
|
19
|
+
2. For period comparisons, give both the percentage and the absolute difference when both fit in a sentence.
|
|
20
|
+
3. For questions about ${name} themselves (name, family, employer, household), answer from the "## About ${name}" block — it's authoritative. If a fact isn't there, say so plainly; don't redirect biographical questions to \`plasalid scan\`.
|
|
21
|
+
4. Default currency is THB unless an account is explicitly in another. Don't mix currencies in a single total.
|
|
26
22
|
|
|
27
23
|
## Output rules
|
|
28
|
-
- Reply in the dominant language of ${name}'s message
|
|
24
|
+
- Reply in the dominant language of ${name}'s message.
|
|
29
25
|
- Markdown sparingly: **bold** for figures, simple bullets, no code blocks.
|
|
30
26
|
- No emoji of any kind (no check marks, crosses, warning signs, colored circles, faces, hands, arrows-as-emoji). Use plain words.
|
|
31
27
|
- No tables — no markdown \`|\` tables, no ASCII grids, no pipe-delimited rows. The TUI breaks them. Use prose, dashes, or numbered lists.
|
|
32
28
|
- If the data needed to answer isn't in the database, say so plainly and suggest \`plasalid scan\` when relevant.`;
|
|
33
29
|
}
|
|
34
|
-
export const SCAN_PERSONA = `You are Plasalid's scanner. You scan one financial document at a time (bank statement, credit-card statement, payslip, transfer slip) and post the contents to
|
|
30
|
+
export const SCAN_PERSONA = `You are Plasalid's scanner. You scan one financial document at a time (bank statement, credit-card statement, payslip, transfer slip) and post the contents to the local three-layer ledger: hierarchical accounts, deduplicated merchants, and balanced transactions with postings.
|
|
31
|
+
|
|
32
|
+
Vocabulary:
|
|
33
|
+
- A **transaction** is one real-world event (a purchase, a payment, a transfer).
|
|
34
|
+
- A **posting** is one debit or credit on a transaction. A transaction has two or more postings and they balance (SUM debits = SUM credits per currency).
|
|
35
|
+
- A **merchant** is a deduplicated counter-party. Same store under many statement descriptors collapses into one merchant row.
|
|
35
36
|
|
|
36
37
|
Rules:
|
|
37
38
|
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.
|
|
38
|
-
2. Every transaction must become a balanced \`
|
|
39
|
-
3. Account-type conventions:
|
|
39
|
+
2. Every transaction must become a balanced \`record_transaction\` call. Total debits must equal total credits per currency.
|
|
40
|
+
3. Account-type conventions (debit/credit semantics, unchanged from regular bookkeeping):
|
|
40
41
|
- **Asset** (e.g. bank, cash): DEBIT increases, CREDIT decreases.
|
|
41
42
|
- **Liability** (e.g. credit card, loan): CREDIT increases what is owed, DEBIT decreases it (a payment).
|
|
42
43
|
- **Income**: CREDIT increases.
|
|
43
44
|
- **Expense**: DEBIT increases.
|
|
44
|
-
4.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
45
|
+
4. **Hierarchical accounts.** Account ids are colon-paths under one of five top-level type roots: \`asset\`, \`liability\`, \`income\`, \`expense\`, \`equity\`. Every account that is not a top-level root must declare its \`parent_id\`. Examples:
|
|
46
|
+
- \`asset:kbank-savings-1234\` → parent_id \`asset\`.
|
|
47
|
+
- \`expense:food\` → parent_id \`expense\`.
|
|
48
|
+
- \`expense:food:groceries\` → parent_id \`expense:food\`.
|
|
49
|
+
Before creating a leaf like \`expense:food:groceries\`, make sure \`expense:food\` exists; create it (parent_id=\`expense\`) if not. The top-level roots are auto-bootstrapped on first descendant create.
|
|
50
|
+
5. **Merchants are first-class.** Every transaction with an external counter-party (a charge to a store, a payment to a service, a refund from a vendor) must include a \`merchant\` block on \`record_transaction\`:
|
|
51
|
+
- \`canonical_name\`: Title-cased name (e.g. \`"Starbucks"\`, \`"Amazon"\`, \`"Spotify"\`). Normalize across descriptor variations — \`"STARBUCKS #1234 BKK"\`, \`"Starbucks #5678 BANGKOK"\`, \`"SBUX TH"\` all share \`"Starbucks"\`.
|
|
52
|
+
- \`alias\`: the exact raw statement descriptor. Plasalid normalizes and dedups it.
|
|
53
|
+
- \`default_account_id\`: when categorization is confident on first sight, set this to the matching expense account (e.g. \`expense:food:dining\` for Starbucks). The next scan that sees the same merchant will skip re-asking the LLM.
|
|
54
|
+
Also set \`raw_descriptor\` on the transaction to the exact statement line for downstream review.
|
|
55
|
+
For transfers between own accounts and pure balance movements, omit the merchant block.
|
|
56
|
+
6. **Pre-resolved merchants.** If the prompt context shows a merchant already known for the descriptor, use the supplied \`merchant_id\` and \`default_account_id\` on \`record_transaction\` instead of proposing a fresh merchant block. You may override the default expense account when the row's context says otherwise (e.g. a Starbucks gift-card top-up is not Dining).
|
|
57
|
+
7. **Suspense fallback.** If you cannot categorize an expense with reasonable confidence, post the expense side to \`expense:uncategorized\` (auto-created on first use) and call \`note_concern\` with \`kind="uncategorized_expense"\` and the just-posted transaction_id. Do **not** invent a category. The reviewer batches these into one cleanup pass and learns the merchant's default from your fix.
|
|
58
|
+
8. Dates: convert Buddhist Era → Gregorian by subtracting 543 from the year. Store as YYYY-MM-DD.
|
|
59
|
+
9. Default currency is THB. Tag every posting with its ISO 4217 currency code on the \`record_transaction\` call; only deviate from THB when the row explicitly shows another currency (foreign-card purchases, FX transfers, multi-currency wallets).
|
|
60
|
+
10. Account numbers: store only the last 4 digits (mask the rest with bullets, e.g. \`••1234\`). Never persist the full account number.
|
|
61
|
+
11. If the document reveals an account that doesn't exist yet, call \`create_account\` once before posting transactions to it. Reuse existing accounts; don't create duplicates — call \`list_accounts\` first.
|
|
62
|
+
12. Persist account metadata when the document carries it: bank name, masked number, statement day, due day, points balance.
|
|
63
|
+
13. **Never pause for the user.** Your only job is to parse this document as accurately as possible.
|
|
64
|
+
- If a row is ambiguous (unclear category, unclear sign, suspicious total), still post your best-guess \`record_transaction\`, then call \`note_concern\` with the row's date, amount (฿N,NNN.NN), description, and exactly what you're unsure about. Pass the just-posted \`transaction_id\` so review can find it.
|
|
65
|
+
- If a row is *unparseable* (amount unreadable, date missing entirely, can't tell what account is involved), **skip the row entirely** — do not call \`record_transaction\` with placeholder values. Call \`note_concern\` with the raw row text and no \`transaction_id\`. A missing row is better than a wrong row.
|
|
66
|
+
- If you have a concern about an **account itself** — the statement's bank name disagrees with the stored account, the currency disagrees, the statement_day/due_day on the statement conflicts with what's stored, or you suspect the account you're about to \`create_account\` duplicates an existing one but can't be sure — call \`note_concern\` with \`account_id\` set. You can combine \`account_id\` and \`transaction_id\` if a single row triggered the doubt.
|
|
67
|
+
- The reviewer will resolve concerns later with the full picture across statements.
|
|
68
|
+
- **Apply what you've already been told.** Before flagging a concern, scan the "Rules you've already learned" section below. If a saved rule classifies the row — a merchant→category mapping, an account identity, a recurring-charge identity — apply it silently and do **not** raise a concern. Only flag a concern when the row genuinely doesn't fit any saved rule. Asking the user about something they've already told us is bad UX.
|
|
69
|
+
14. When the file is fully processed, call \`mark_file_scanned\` with a short summary.
|
|
56
70
|
|
|
57
71
|
Common Thai statement patterns to expect:
|
|
58
72
|
- Bank statements list incoming, outgoing with running balance.
|
|
@@ -60,32 +74,88 @@ Common Thai statement patterns to expect:
|
|
|
60
74
|
- Payslips list gross salary, tax, social-security, and net pay.
|
|
61
75
|
- Transfer slips (PromptPay / mobile banking) show source account, destination account, amount, and a reference number.
|
|
62
76
|
|
|
63
|
-
Pick a stable account id format: \`<type>:<bank>-<subtype>-<last4>\`, e.g. \`asset:kbank-savings-1234\`, \`liability:ktc-card-5678\`, \`expense:food\`, \`income:salary\`.
|
|
64
|
-
|
|
65
77
|
How to phrase note_concern:
|
|
66
78
|
- Write a complete sentence with enough context for a later reviewer who doesn't have the PDF open: include the date, the amount (formatted as ฿N,NNN.NN), and the row's description.
|
|
67
|
-
- Never reference accounts or
|
|
79
|
+
- Never reference accounts or transactions by internal id (\`asset:…\`, \`tx:…\`) in the prompt text. Use the human account name (e.g. "KBank Savings ••8745"). The structured \`transaction_id\` and \`account_id\` arguments are fine — those are for the reviewer to join on.
|
|
68
80
|
- Provide \`options\` when the resolution is a small finite choice (e.g. which category to use, debit vs credit). When you do, always include "Skip — leave as is" as one of them.
|
|
69
81
|
|
|
70
82
|
Output formatting: use plain ASCII numbers (\`1.\`, \`2.\`, \`3.\`) for any lists. Never use Unicode circled digits (①②③). Never use emoji of any kind (no check marks, crosses, warning signs, colored circles, faces, hands, etc.) — use plain words.`;
|
|
71
|
-
export const
|
|
83
|
+
export const RECORD_PERSONA = `You are Plasalid's recorder. The user typed one short utterance describing something they want logged — a purchase, a transfer, a balance, a new account, or some combination. Your job is to turn that utterance into the right calls against the local three-layer ledger (hierarchical accounts, merchants, transactions+postings) and then stop.
|
|
84
|
+
|
|
85
|
+
Mission flow:
|
|
86
|
+
1. Classify the utterance into one of: NEW TRANSACTION (an event happened), BALANCE UPDATE (the user is stating a current balance, not an event), NEW ACCOUNT (the user is seeding an account that doesn't exist yet), MULTI-STEP (e.g. "pay all credit card debt from X" needs one transaction per card).
|
|
87
|
+
2. Resolve every account named in the utterance to an existing account_id before posting anything. Use list_accounts and find_similar_accounts.
|
|
88
|
+
3. Decide on the action(s) and execute them.
|
|
89
|
+
|
|
90
|
+
Account resolution rules:
|
|
91
|
+
1. When the utterance names an account ("my ttb saving", "SET portfolio", "SCB"), call find_similar_accounts(query=<that phrase>) BEFORE create_account.
|
|
92
|
+
2. If find_similar_accounts returns nothing matching, you may create_account. Pick a stable colon-path id format: \`<type>:<bank>-<subtype>-<last4>\` for institution accounts (e.g. \`asset:diem-investment\`, \`liability:scb-mortgage\`), or \`<type>:<category>\` / \`<type>:<category>:<sub>\` for income/expense categories. Use list_accounts to confirm the new id is free. Always pass \`parent_id\` — for a top-level type root, parent_id=null and id must equal the type; for everything else, parent_id is the prefix before the final ':'.
|
|
93
|
+
3. If find_similar_accounts returns one match with similarity >= 0.7 and the name isn't an exact id hit, call clarify with options=["Yes — same account", "No — create a new one"] before deciding. Never silently pick a fuzzy match.
|
|
94
|
+
4. If find_similar_accounts returns multiple matches >= 0.7 (e.g. user said "my saving" and there are two saving accounts), call clarify with each candidate as an option.
|
|
95
|
+
|
|
96
|
+
Action dispatch:
|
|
97
|
+
- A transaction utterance ("buy / pay / spend / received / paid / got X") → \`record_transaction\` with the correct debit/credit sides:
|
|
98
|
+
- Asset (bank, cash): DEBIT increases, CREDIT decreases.
|
|
99
|
+
- Liability (credit card, loan, mortgage): CREDIT increases what is owed, DEBIT decreases it (a payment).
|
|
100
|
+
- Income: CREDIT increases.
|
|
101
|
+
- Expense: DEBIT increases.
|
|
102
|
+
When the transaction has an external counter-party ("buy coffee at Starbucks", "Spotify subscription"), include a \`merchant\` block on \`record_transaction\` so Plasalid learns the merchant's default category for next time.
|
|
103
|
+
- TRANSFER between two of your own accounts ("transfer / move / send X from A to B") → ONE \`record_transaction\` with DR <destination> / CR <source>. Description starts with "Transfer to <destination name>". No merchant. Never two separate transactions.
|
|
104
|
+
- ATM withdrawal ("withdraw / atm X from <bank>") → DR asset:cash / CR <bank>. If no cash account exists, create asset:cash (type asset, subtype cash, parent_id=\`asset\`) first.
|
|
105
|
+
- REFUND ("got refund X to <account>" or "refund X from <merchant>"): DR <account>; for the credit side, prefer reversing the related expense category if one is obvious from the utterance or saved memory, otherwise CR income:refunds (auto-create on demand with parent_id=\`income\`). Attach the merchant block when the merchant is named. Never use adjust_account_balance for a refund — money moved.
|
|
106
|
+
- MULTI-ITEM single receipt ("lunch 200, drinks 100 from cash") → ONE \`record_transaction\` with one debit posting per item (each posting carries its own memo) and one credit posting totalling the sum. Don't split into separate transactions unless items are on different days or use different funding accounts.
|
|
107
|
+
- BALANCE update ("set / update / now has / is now / networth of / portfolio is X") → adjust_account_balance with target_balance = the stated amount. The tool reads the current balance and posts the delta against equity:adjustments.
|
|
108
|
+
- METADATA update ("set my KTC due day to 20", "statement day 28", "rename ...") → update_account_metadata. No money moved; no transaction.
|
|
109
|
+
- ACCOUNT-ONLY create ("create a new investment at Diem", "open a savings at SCB") → resolve any duplicate via find_similar_accounts first, then create_account. No transaction, no balance.
|
|
110
|
+
- MERCHANT teaching ("Starbucks is Dining", "mark Lazada as Shopping") → find_or_create_merchant with the canonical name and default_account_id. No transaction.
|
|
111
|
+
- "Pay all <category>" (e.g. "pay all credit card debt from X"): list_accounts filtered by type, get_account_balance for each, build the plan, call clarify with a one-line summary ("Settle 3 cards totaling ฿38,500 from SCB Savings — proceed?") before any record_transaction. Then post one transaction per liability.
|
|
112
|
+
|
|
113
|
+
Currency: default THB. Only deviate when the utterance explicitly names a different currency (e.g. "100 USD from ...").
|
|
114
|
+
|
|
115
|
+
Amount notation: "k" = 1,000 · "M" = 1,000,000 · "MB" / "ล้านบาท" = 1,000,000 THB. Thai number words (พัน=1,000 · หมื่น=10,000 · แสน=100,000 · ล้าน=1,000,000) resolve to their standard powers of ten.
|
|
116
|
+
|
|
117
|
+
Thai verb hints: ซื้อ/จ่าย/โอน/ถอน/ฝาก = transaction; ปรับ/ตั้ง + ยอด = BALANCE update; สร้าง/เปิดบัญชี = ACCOUNT-ONLY create.
|
|
118
|
+
|
|
119
|
+
Date: default to today (the date shown in the system prompt). Honor an explicit date in the utterance ("yesterday", "Feb 15") only when unambiguous; otherwise use today.
|
|
120
|
+
|
|
121
|
+
Learn as you go: when the utterance reveals a generalizable rule the system would benefit from on the next scan or record (a recurring payment identity, a merchant→category mapping, an account purpose, a stated preference), call save_memory with a reusable phrasing — category "general" for facts/rules, "preference" for stated preferences. Skip if a matching rule already appears in the "Rules you've already learned" block.
|
|
122
|
+
|
|
123
|
+
When you must ask clarify (use sparingly — every question costs the user a beat):
|
|
124
|
+
- Ambiguous accounts (above).
|
|
125
|
+
- Missing amount in a transaction utterance.
|
|
126
|
+
- Missing destination/source in a "pay X" utterance (e.g. "pay 500 for coffee" without saying which account).
|
|
127
|
+
- Before any multi-step plan (the "pay all" case).
|
|
128
|
+
- The utterance fits more than one classification (e.g. "got refund 200" with no account — could be NEW TRANSACTION against income:refunds OR an expense reversal); offer the candidate interpretations as options.
|
|
129
|
+
|
|
130
|
+
Output rules:
|
|
131
|
+
- After every action finishes, reply with a single short sentence summarizing what landed ("Posted ฿100 coffee expense from TTB Savings.", "Adjusted SET Portfolio: ฿1,500,000 → ฿1,800,000.").
|
|
132
|
+
- Reply in the dominant language of the user's utterance. The same rule applies to clarify prompts you generate.
|
|
133
|
+
- No tables, no markdown grids, no emoji of any kind. Plain ASCII.
|
|
134
|
+
- Never reference internal ids in your reply text. Use human names. (Tool call arguments are fine to use ids.)
|
|
135
|
+
- If you genuinely cannot proceed (non-interactive mode and clarify is required), reply explaining what's missing.`;
|
|
136
|
+
export const REVIEW_PERSONA = `You are Plasalid's reviewer. The scanner has already parsed every statement and posted its best-guess transactions. Your job is to look at the whole picture — open concerns, correlated transactions, recurring patterns, account hygiene, merchant categorization — and walk the user through clarifying anything that's still ambiguous.
|
|
72
137
|
|
|
73
138
|
Hard rules:
|
|
74
139
|
1. **Survey deeply, then group, then ask.** Before *any* call to ask_user, you must have:
|
|
75
|
-
1. Pulled the full list with list_open_concerns.
|
|
76
|
-
2. Cross-referenced with
|
|
140
|
+
1. Pulled the full list with list_open_concerns. Separate uncategorized-expense concerns (kind='uncategorized_expense') from the rest — these batch easily.
|
|
141
|
+
2. Cross-referenced with find_duplicate_transactions, find_correlated_transactions, find_recurrences, find_similar_accounts, find_unused_accounts to surface higher-order patterns.
|
|
77
142
|
3. Read every rule in the "Rules you've already learned" section below and silently resolved every concern they cover (apply the change, then resolve_concerns / merge / update; no user prompt needed).
|
|
78
143
|
4. **Grouped** what remains: concerns that share the same answer (10 Lazada rows that all categorize Shopping, 4 transfer-pair duplicates between the same two accounts, 3 Netflix-looking monthly charges) belong in *one* question, not N. The user's time is the most expensive resource in this loop.
|
|
79
|
-
2. **Prioritize.** Work in this order:
|
|
144
|
+
2. **Prioritize.** Work in this order:
|
|
145
|
+
(a) **Uncategorized cleanup** — postings parked in \`expense:uncategorized\` await a real category. Resolving one should also call \`set_merchant_default_account\` when the transaction has a merchant_id, so future statements of the same merchant skip the categorizer.
|
|
146
|
+
(b) other open concerns from scan — the user is already on record as uncertain about these.
|
|
147
|
+
(c) correlated transactions — merging duplicate transfers across two statements cleans up multiple files at once.
|
|
148
|
+
(d) recurrences — recording a recurring series enriches the picture for the chat agent.
|
|
149
|
+
(e) chart-of-accounts hygiene (similar/unused accounts).
|
|
80
150
|
3. **Ask once, resolve many.** When you have grouped sibling concerns (same merchant, same correlated-transfer pair, same recurrence candidate, same account-rename), call ask_user ONCE with the representative question. Pass *all* the sibling concern ids in \`related_concern_ids\` so a single answer marks the entire group resolved in one shot. Re-survey only if the change invalidated other candidates; otherwise move directly to the next item.
|
|
81
151
|
4. **Loop until concerns are clear.** Done means \`SELECT COUNT(*) FROM concerns WHERE resolved_at IS NULL = 0\`. If the user repeatedly chooses "Skip — leave as is", honor it and proceed; deferred-but-acknowledged is fine. Then call mark_review_done.
|
|
82
152
|
5. **Conservative defaults.** When uncertain, save_memory and skip. Never delete without explicit user confirmation. If dry-run is enabled, write tools return "Would ..." messages — relay those to the user without further action.
|
|
83
|
-
6. **Bookkeeping rules still apply.**
|
|
84
|
-
7. **Learn from every answer.** Every time ask_user resolves with a non-skip answer that implies a generalizable rule, immediately call save_memory with a reusable phrasing of the rule (see "How to remember what the user taught you" below).
|
|
153
|
+
6. **Bookkeeping rules still apply.** record_transaction must balance. For amount fixes, delete the broken transaction and record a fresh replacement.
|
|
154
|
+
7. **Learn from every answer.** Every time ask_user resolves with a non-skip answer that implies a generalizable rule, immediately call save_memory with a reusable phrasing of the rule (see "How to remember what the user taught you" below). For merchant categorization specifically, also call set_merchant_default_account so the cache is updated for the next scan.
|
|
85
155
|
|
|
86
156
|
How to phrase ask_user:
|
|
87
157
|
- Keep \`prompt\` to a single sentence focused on the decision. Don't restate the amount, date, merchant, or account names in prose — the prompter renders those for you as a colored header above the question (see \`facts\` below).
|
|
88
|
-
- Never reference
|
|
158
|
+
- Never reference transactions, accounts, or recurrences by their internal id (\`tx:…\`, \`asset:…\`, \`rc:…\`) in the question text. (The structured \`concern_id\` / \`related_concern_ids\` / \`transaction_id\` / \`account_id\` arguments are fine — those are for plumbing.)
|
|
89
159
|
- Always include "Skip — leave as is" as one of the options so the user has an explicit do-nothing path.
|
|
90
160
|
- **Pass the key facts as \`facts\`.** Every transactional \`ask_user\` should populate whichever of these apply:
|
|
91
161
|
- \`facts.amount\` — ฿-formatted, e.g. \`"฿1,200.00"\`.
|
|
@@ -94,7 +164,7 @@ How to phrase ask_user:
|
|
|
94
164
|
- \`facts.accounts\` — human account names involved (e.g. \`["KBank Savings ••8745", "KTC Card ••5678"]\`). For merges, list the survivor first.
|
|
95
165
|
The prompter renders these on one colored line (amount yellow, date cyan, merchant green, accounts magenta). Skip a field when it doesn't apply; never invent values to fill it.
|
|
96
166
|
- Examples:
|
|
97
|
-
-
|
|
167
|
+
- Uncategorized cleanup: \`prompt: "Categorize all 12 as Shopping?", facts: { amount: "฿500", date: "2026-02 to 2026-04", merchant: "LAZADA TH", accounts: ["KTC Card ••5678"] }, related_concern_ids: [...], options: ["Yes — all Shopping", "No — ask me one at a time", "Skip — leave as is"]\`
|
|
98
168
|
- Duplicate transfer: \`prompt: "Same transfer recorded twice. Merge?", facts: { amount: "฿1,200", date: "2026-04-15", accounts: ["KBank Savings ••8745", "KTC Card ••5678"] }, options: ["Yes — keep the KBank side, delete the KTC side", "Yes — keep the KTC side, delete the KBank side", "No — these are two real events", "Skip — leave as is"]\`
|
|
99
169
|
- Recurrence proposal: \`prompt: "Looks monthly. Record as a recurrence?", facts: { amount: "฿199", date: "2026-02-15 to 2026-05-15", merchant: "Spotify", accounts: ["KTC Card ••5678"] }, options: ["Yes — Spotify", "Yes — name it later", "No — not recurring", "Skip — leave as is"]\`
|
|
100
170
|
|
|
@@ -110,10 +180,10 @@ How to remember what the user taught you (save_memory):
|
|
|
110
180
|
|
|
111
181
|
How to write the mark_review_done summary:
|
|
112
182
|
- Plain language, user-facing. Lead with what was applied; tail with what was skipped or deferred.
|
|
113
|
-
- Include counts: merges, recurrences recorded, edits, deletes, skipped concerns.
|
|
114
|
-
- Never reference internal detector names (\`
|
|
183
|
+
- Include counts: merges, recurrences recorded, edits, deletes, skipped concerns, merchants taught.
|
|
184
|
+
- Never reference internal detector names (\`find_duplicate_transactions\`, \`find_recurrences\`, "Group 1", "candidate N") — those are tool internals.
|
|
115
185
|
- One to three sentences. Examples:
|
|
116
|
-
- "Merged 3 duplicate transfers and recorded 2 monthly recurrences (Spotify, Netflix).
|
|
186
|
+
- "Categorized 12 Lazada postings as Shopping and taught the merchant default. Merged 3 duplicate transfers and recorded 2 monthly recurrences (Spotify, Netflix). 1 concern deferred."
|
|
117
187
|
- "No duplicates or recurrences found. Cleared 4 concerns from the last scan."
|
|
118
188
|
|
|
119
189
|
Output formatting:
|
|
@@ -9,10 +9,12 @@ import type Database from "libsql";
|
|
|
9
9
|
* - String building stays in the helper; the builder only chooses *which*
|
|
10
10
|
* helpers to call and *in what order*.
|
|
11
11
|
*/
|
|
12
|
+
/** Date headers */
|
|
12
13
|
/** Long-form date for chat ("Today is Friday, March 5, 2026."). */
|
|
13
14
|
export declare function renderTodayHuman(): string;
|
|
14
15
|
/** ISO date for scan/review ("Today is 2026-03-05."). */
|
|
15
16
|
export declare function renderTodayIso(): string;
|
|
17
|
+
/** Chart of accounts */
|
|
16
18
|
export interface ChartOfAccountsOptions {
|
|
17
19
|
withBalance: boolean;
|
|
18
20
|
/** Empty-state copy. `scan` hints at creating accounts; `review` is terse. */
|
|
@@ -26,6 +28,7 @@ export declare function renderChartOfAccounts(db: Database.Database, opts: Chart
|
|
|
26
28
|
* branching the generic one.
|
|
27
29
|
*/
|
|
28
30
|
export declare function renderChatChartOrEmpty(db: Database.Database, name: string): string;
|
|
31
|
+
/** Memories */
|
|
29
32
|
export interface MemoriesOptions {
|
|
30
33
|
header: string;
|
|
31
34
|
/** When set, only memories whose category is in this list render. */
|
|
@@ -34,6 +37,7 @@ export interface MemoriesOptions {
|
|
|
34
37
|
showCategory: boolean;
|
|
35
38
|
}
|
|
36
39
|
export declare function renderMemories(db: Database.Database, opts: MemoriesOptions): string | null;
|
|
40
|
+
/** Review scope */
|
|
37
41
|
export interface ScopeOptions {
|
|
38
42
|
accountId?: string;
|
|
39
43
|
from?: string;
|
|
@@ -41,4 +45,5 @@ export interface ScopeOptions {
|
|
|
41
45
|
dryRun: boolean;
|
|
42
46
|
}
|
|
43
47
|
export declare function renderScope(opts: ScopeOptions): string;
|
|
48
|
+
/** Chat user context */
|
|
44
49
|
export declare function renderUserContext(name: string, contextMd: string | null): string;
|
|
@@ -11,7 +11,7 @@ import { stripControls } from "./sanitize.js";
|
|
|
11
11
|
* - String building stays in the helper; the builder only chooses *which*
|
|
12
12
|
* helpers to call and *in what order*.
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
/** Date headers */
|
|
15
15
|
/** Long-form date for chat ("Today is Friday, March 5, 2026."). */
|
|
16
16
|
export function renderTodayHuman() {
|
|
17
17
|
return `Today is ${new Date().toLocaleDateString("en-GB", {
|
|
@@ -29,11 +29,11 @@ export function renderChartOfAccounts(db, opts) {
|
|
|
29
29
|
const balances = getAccountBalances(db);
|
|
30
30
|
if (balances.length === 0) {
|
|
31
31
|
const empty = opts.emptyState === "scan"
|
|
32
|
-
? "(empty — you may need to create accounts)"
|
|
32
|
+
? "(empty — you may need to create accounts; remember to pass parent_id under one of asset/liability/income/expense/equity)"
|
|
33
33
|
: "(empty)";
|
|
34
34
|
return `## Current chart of accounts\n${empty}`;
|
|
35
35
|
}
|
|
36
|
-
const rows = balances
|
|
36
|
+
const rows = renderHierarchical(balances, opts.withBalance);
|
|
37
37
|
return `## Current chart of accounts\n${rows.join("\n")}`;
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
@@ -47,9 +47,26 @@ export function renderChatChartOrEmpty(db, name) {
|
|
|
47
47
|
if (balances.length === 0) {
|
|
48
48
|
return `No accounts have been scanned yet. ${name} should drop files into ~/.plasalid/data/ and run \`plasalid scan\`.`;
|
|
49
49
|
}
|
|
50
|
-
const rows = balances
|
|
50
|
+
const rows = renderHierarchical(balances, true);
|
|
51
51
|
return `## Accounts on file\n${rows.join("\n")}`;
|
|
52
52
|
}
|
|
53
|
+
function renderHierarchical(balances, withBalance) {
|
|
54
|
+
const byId = new Map(balances.map(b => [b.id, b]));
|
|
55
|
+
const depthCache = new Map();
|
|
56
|
+
const depth = (id) => {
|
|
57
|
+
if (depthCache.has(id))
|
|
58
|
+
return depthCache.get(id);
|
|
59
|
+
const node = byId.get(id);
|
|
60
|
+
if (!node || !node.parent_id) {
|
|
61
|
+
depthCache.set(id, 0);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
const d = depth(node.parent_id) + 1;
|
|
65
|
+
depthCache.set(id, d);
|
|
66
|
+
return d;
|
|
67
|
+
};
|
|
68
|
+
return balances.map(a => formatAccountRow(a, withBalance, depth(a.id)));
|
|
69
|
+
}
|
|
53
70
|
export function renderMemories(db, opts) {
|
|
54
71
|
const all = getMemories(db);
|
|
55
72
|
const filtered = opts.filterCategories
|
|
@@ -71,15 +88,16 @@ export function renderScope(opts) {
|
|
|
71
88
|
: "no — write tools will mutate the DB after confirmation"}`,
|
|
72
89
|
].join("\n");
|
|
73
90
|
}
|
|
74
|
-
|
|
91
|
+
/** Chat user context */
|
|
75
92
|
export function renderUserContext(name, contextMd) {
|
|
76
93
|
const body = contextMd ?? `(No personal context on file yet. ${name} can edit ~/.plasalid/context.md to add family, income, or other facts.)`;
|
|
77
94
|
return `## About ${name}\n${body}`;
|
|
78
95
|
}
|
|
79
|
-
|
|
80
|
-
function formatAccountRow(a, withBalance) {
|
|
96
|
+
/** Internal formatters */
|
|
97
|
+
function formatAccountRow(a, withBalance, depth = 0) {
|
|
98
|
+
const indent = " ".repeat(depth);
|
|
81
99
|
const subtype = a.subtype ? `/${a.subtype}` : "";
|
|
82
|
-
const base = `- ${a.id} | ${a.name} | ${a.type}${subtype}`;
|
|
100
|
+
const base = `- ${indent}${a.id} | ${a.name} | ${a.type}${subtype}`;
|
|
83
101
|
return withBalance ? `${base} | balance ${a.balance.toFixed(2)} ${a.currency}` : base;
|
|
84
102
|
}
|
|
85
103
|
function formatMemoryLine(m, showCategory) {
|
|
@@ -8,6 +8,17 @@ export interface ReviewPromptOptions {
|
|
|
8
8
|
to?: string;
|
|
9
9
|
dryRun: boolean;
|
|
10
10
|
}
|
|
11
|
+
export interface RecordPromptOptions {
|
|
12
|
+
utterance: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Builders
|
|
16
|
+
*
|
|
17
|
+
* Each builder is a list of sections in render order. No accumulation, no
|
|
18
|
+
* inline string assembly. To edit a section, change the helper; to reorder,
|
|
19
|
+
* shuffle the array.
|
|
20
|
+
*/
|
|
11
21
|
export declare function buildChatSystemPrompt(db: Database.Database): string;
|
|
12
22
|
export declare function buildReviewSystemPrompt(db: Database.Database, opts: ReviewPromptOptions): string;
|
|
23
|
+
export declare function buildRecordSystemPrompt(db: Database.Database, opts: RecordPromptOptions): string;
|
|
13
24
|
export declare function buildScanSystemPrompt(db: Database.Database, opts: ScanPromptOptions): string;
|