plasalid 0.4.1 → 0.5.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 (65) hide show
  1. package/README.md +15 -14
  2. package/dist/ai/agent.d.ts +15 -2
  3. package/dist/ai/agent.js +21 -2
  4. package/dist/ai/memory.d.ts +2 -0
  5. package/dist/ai/memory.js +2 -2
  6. package/dist/ai/personas.d.ts +2 -1
  7. package/dist/ai/personas.js +115 -45
  8. package/dist/ai/prompt-sections.d.ts +5 -0
  9. package/dist/ai/prompt-sections.js +26 -8
  10. package/dist/ai/system-prompt.d.ts +11 -0
  11. package/dist/ai/system-prompt.js +21 -6
  12. package/dist/ai/thinking.js +1 -1
  13. package/dist/ai/tools/common.js +2 -5
  14. package/dist/ai/tools/index.js +28 -8
  15. package/dist/ai/tools/ingest.d.ts +2 -1
  16. package/dist/ai/tools/ingest.js +262 -151
  17. package/dist/ai/tools/merchants.d.ts +2 -0
  18. package/dist/ai/tools/merchants.js +117 -0
  19. package/dist/ai/tools/read.js +31 -29
  20. package/dist/ai/tools/record.d.ts +2 -0
  21. package/dist/ai/tools/record.js +188 -0
  22. package/dist/ai/tools/review.js +77 -80
  23. package/dist/ai/tools/scan.js +1 -1
  24. package/dist/ai/tools/types.d.ts +15 -6
  25. package/dist/cli/commands/accounts.js +33 -25
  26. package/dist/cli/commands/record.d.ts +4 -0
  27. package/dist/cli/commands/record.js +119 -0
  28. package/dist/cli/commands/revert.js +1 -1
  29. package/dist/cli/commands/scan.js +15 -19
  30. package/dist/cli/commands/status.js +6 -9
  31. package/dist/cli/commands/transactions.js +36 -41
  32. package/dist/cli/format.d.ts +2 -0
  33. package/dist/cli/format.js +7 -2
  34. package/dist/cli/index.js +19 -7
  35. package/dist/cli/ink/scan_dashboard.d.ts +1 -1
  36. package/dist/cli/ink/scan_dashboard.js +2 -2
  37. package/dist/cli/setup.d.ts +0 -1
  38. package/dist/cli/setup.js +2 -8
  39. package/dist/currency.d.ts +3 -0
  40. package/dist/currency.js +12 -1
  41. package/dist/db/queries/account_balance.d.ts +83 -4
  42. package/dist/db/queries/account_balance.js +239 -20
  43. package/dist/db/queries/action_log.d.ts +29 -0
  44. package/dist/db/queries/action_log.js +27 -0
  45. package/dist/db/queries/concerns.d.ts +10 -7
  46. package/dist/db/queries/concerns.js +20 -16
  47. package/dist/db/queries/journal.d.ts +1 -0
  48. package/dist/db/queries/merchants.d.ts +42 -0
  49. package/dist/db/queries/merchants.js +120 -0
  50. package/dist/db/queries/recurrences.d.ts +3 -3
  51. package/dist/db/queries/recurrences.js +32 -34
  52. package/dist/db/queries/search.d.ts +5 -4
  53. package/dist/db/queries/search.js +16 -12
  54. package/dist/db/queries/transactions.d.ts +167 -0
  55. package/dist/db/queries/transactions.js +320 -0
  56. package/dist/db/schema.js +51 -9
  57. package/dist/reviewer/pipeline.d.ts +4 -4
  58. package/dist/reviewer/pipeline.js +4 -4
  59. package/dist/reviewer/prompts.js +4 -4
  60. package/dist/scanner/buffer.d.ts +24 -21
  61. package/dist/scanner/buffer.js +18 -18
  62. package/dist/scanner/pipeline.d.ts +3 -2
  63. package/dist/scanner/pipeline.js +33 -36
  64. package/dist/scanner/prompts.js +3 -3
  65. package/package.json +2 -2
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "crypto";
2
2
  import { getDb } from "../db/connection.js";
3
3
  import { countOpenConcerns, } from "../db/queries/concerns.js";
4
- import { correlatePairs } from "../db/queries/journal.js";
4
+ import { correlatePairs } from "../db/queries/transactions.js";
5
5
  import { runScanAgent } from "../ai/agent.js";
6
6
  import { buildDocumentBlock } from "./pdf.js";
7
7
  import { buildScanUserMessage } from "./prompts.js";
@@ -12,7 +12,7 @@ import { decryptQueue, confirmProceedAfterFailures, } from "./decrypt_queue.js";
12
12
  export function compileMatcher(input) {
13
13
  return new RegExp(input, "i");
14
14
  }
15
- // ── Orchestration ───────────────────────────────────────────────────────────
15
+ /** Orchestration */
16
16
  export async function runScan(opts = {}) {
17
17
  const db = getDb();
18
18
  const matcher = opts.regex ? compileMatcher(opts.regex) : null;
@@ -98,7 +98,7 @@ async function scanOneFile(db, file, events) {
98
98
  events?.scanEnd?.({
99
99
  fileName: file.fileName,
100
100
  status: "scanned",
101
- entries: buffer.journalEntries.length,
101
+ transactions: buffer.transactions.length,
102
102
  concerns: buffer.concerns.length,
103
103
  });
104
104
  return { decryptedFile: file, buffer, agentText: text };
@@ -108,14 +108,14 @@ async function scanOneFile(db, file, events) {
108
108
  events?.scanEnd?.({
109
109
  fileName: file.fileName,
110
110
  status: "failed",
111
- entries: 0,
111
+ transactions: 0,
112
112
  concerns: 0,
113
113
  error: message,
114
114
  });
115
115
  return { decryptedFile: file, buffer, error: message, agentText: "" };
116
116
  }
117
117
  }
118
- // ── Phase 3: cross-file correlation ─────────────────────────────────────────
118
+ /** Phase 3: cross-file correlation */
119
119
  /**
120
120
  * For every pair of buffered entries that look like the same money movement
121
121
  * across two different files, append a mirror concern to each side's buffer.
@@ -126,22 +126,22 @@ function applyCrossFileCorrelations(results) {
126
126
  for (const res of results) {
127
127
  if (res.error)
128
128
  continue;
129
- for (const be of res.buffer.journalEntries) {
129
+ for (const bt of res.buffer.transactions) {
130
130
  all.push({
131
131
  file: res,
132
- entryId: be.entry_id,
133
- lines: be.input.lines,
134
- date: be.input.date,
135
- description: be.input.description,
132
+ transactionId: bt.transaction_id,
133
+ postings: bt.input.postings,
134
+ date: bt.input.date,
135
+ description: bt.input.description,
136
136
  });
137
137
  }
138
138
  }
139
139
  const candidates = all.map(e => {
140
- const debit = e.lines.reduce((s, l) => s + (l.debit ?? 0), 0);
141
- const currency = e.lines.find(l => l.currency)?.currency ?? "THB";
142
- const ids = Array.from(new Set(e.lines.map(l => l.account_id)));
140
+ const debit = e.postings.reduce((s, p) => s + (p.debit ?? 0), 0);
141
+ const currency = e.postings.find(p => p.currency)?.currency ?? "THB";
142
+ const ids = Array.from(new Set(e.postings.map(p => p.account_id)));
143
143
  return {
144
- id: e.entryId,
144
+ id: e.transactionId,
145
145
  date: e.date,
146
146
  description: e.description,
147
147
  amount: Math.round(debit * 100) / 100,
@@ -151,55 +151,52 @@ function applyCrossFileCorrelations(results) {
151
151
  };
152
152
  });
153
153
  const pairs = correlatePairs(candidates, { toleranceDays: 3 });
154
- const byEntry = new Map(all.map(a => [a.entryId, a]));
154
+ const byTransaction = new Map(all.map(a => [a.transactionId, a]));
155
155
  for (const pair of pairs) {
156
- const a = byEntry.get(pair.a.id);
157
- const b = byEntry.get(pair.b.id);
156
+ const a = byTransaction.get(pair.a.id);
157
+ const b = byTransaction.get(pair.b.id);
158
158
  if (!a || !b)
159
159
  continue;
160
160
  if (a.file === b.file)
161
- continue; // same-file pairs are within-statement dupes; review's own detectors will handle.
161
+ continue;
162
162
  const amountStr = `฿${pair.amount.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
163
163
  a.file.buffer.appendConcern({
164
- entry_id: a.entryId,
164
+ transaction_id: a.transactionId,
165
165
  account_id: null,
166
166
  prompt: `Looks like the matching half of this ${amountStr} movement on ${a.date} was also recorded in ${b.file.decryptedFile.fileName} on ${b.date}. Merge during review?`,
167
- options: ["Yes — merge into one entry", "No — these are two real events", "Skip — leave as is"],
167
+ options: ["Yes — merge into one transaction", "No — these are two real events", "Skip — leave as is"],
168
168
  });
169
169
  b.file.buffer.appendConcern({
170
- entry_id: b.entryId,
170
+ transaction_id: b.transactionId,
171
171
  account_id: null,
172
172
  prompt: `Looks like the matching half of this ${amountStr} movement on ${b.date} was also recorded in ${a.file.decryptedFile.fileName} on ${a.date}. Merge during review?`,
173
- options: ["Yes — merge into one entry", "No — these are two real events", "Skip — leave as is"],
173
+ options: ["Yes — merge into one transaction", "No — these are two real events", "Skip — leave as is"],
174
174
  });
175
175
  }
176
- return pairs.filter(p => byEntry.get(p.a.id)?.file !== byEntry.get(p.b.id)?.file).length;
176
+ return pairs.filter(p => byTransaction.get(p.a.id)?.file !== byTransaction.get(p.b.id)?.file).length;
177
177
  }
178
- // ── Phase 4: commit ─────────────────────────────────────────────────────────
178
+ /** Phase 4: commit */
179
179
  function commitAll(db, decryptResult, scanResults) {
180
180
  const out = [];
181
- // Skipped files: keep them in the summary with their existing concern count.
182
181
  for (const skipped of decryptResult.skipped) {
183
182
  out.push({
184
183
  name: skipped.file.name,
185
184
  relPath: skipped.file.relPath,
186
185
  status: "skipped",
187
- entries: 0,
186
+ transactions: 0,
188
187
  concerns: countOpenConcerns(db, { file_id: skipped.existingScannedFileId }),
189
188
  });
190
189
  }
191
- // Files that failed to decrypt never reached an agent.
192
190
  for (const failed of decryptResult.failed) {
193
191
  out.push({
194
192
  name: failed.file.name,
195
193
  relPath: failed.file.relPath,
196
194
  status: "failed",
197
- entries: 0,
195
+ transactions: 0,
198
196
  concerns: 0,
199
197
  error: failed.error,
200
198
  });
201
199
  }
202
- // Scanned files: per-file transaction. Replaces prior records when needed.
203
200
  for (const res of scanResults) {
204
201
  const { decryptedFile, buffer, error, agentText } = res;
205
202
  if (error) {
@@ -207,7 +204,7 @@ function commitAll(db, decryptResult, scanResults) {
207
204
  name: decryptedFile.fileName,
208
205
  relPath: decryptedFile.relPath,
209
206
  status: "failed",
210
- entries: 0,
207
+ transactions: 0,
211
208
  concerns: buffer.concerns.length,
212
209
  error,
213
210
  });
@@ -228,7 +225,7 @@ function commitAll(db, decryptResult, scanResults) {
228
225
  name: decryptedFile.fileName,
229
226
  relPath: decryptedFile.relPath,
230
227
  status: decryptedFile.replacesPriorScannedFileId ? "replaced" : "scanned",
231
- entries: counts.entries,
228
+ transactions: counts.transactions,
232
229
  concerns: counts.concerns,
233
230
  });
234
231
  }
@@ -237,7 +234,7 @@ function commitAll(db, decryptResult, scanResults) {
237
234
  name: decryptedFile.fileName,
238
235
  relPath: decryptedFile.relPath,
239
236
  status: "failed",
240
- entries: 0,
237
+ transactions: 0,
241
238
  concerns: buffer.concerns.length,
242
239
  error: err?.message ?? "commit failed",
243
240
  });
@@ -245,7 +242,7 @@ function commitAll(db, decryptResult, scanResults) {
245
242
  }
246
243
  return out;
247
244
  }
248
- // ── Summary assembly ────────────────────────────────────────────────────────
245
+ /** Summary assembly */
249
246
  function buildSummary(total, details, _decrypt) {
250
247
  const summary = {
251
248
  total,
@@ -265,15 +262,15 @@ function buildSummary(total, details, _decrypt) {
265
262
  function buildAbortedSummary(total, decrypt) {
266
263
  const details = [
267
264
  ...decrypt.skipped.map(s => ({
268
- name: s.file.name, relPath: s.file.relPath, status: "skipped", entries: 0, concerns: 0,
265
+ name: s.file.name, relPath: s.file.relPath, status: "skipped", transactions: 0, concerns: 0,
269
266
  })),
270
267
  ...decrypt.failed.map(f => ({
271
- name: f.file.name, relPath: f.file.relPath, status: "failed", entries: 0, concerns: 0, error: f.error,
268
+ name: f.file.name, relPath: f.file.relPath, status: "failed", transactions: 0, concerns: 0, error: f.error,
272
269
  })),
273
270
  ];
274
271
  return buildSummary(total, details, decrypt);
275
272
  }
276
- // ── Low-level DB helpers ────────────────────────────────────────────────────
273
+ /** Low-level DB helpers */
277
274
  function deleteScannedFile(db, id) {
278
275
  db.prepare(`DELETE FROM scanned_files WHERE id = ?`).run(id);
279
276
  }
@@ -11,10 +11,10 @@ export function buildScanUserMessage(opts) {
11
11
  `Steps:`,
12
12
  `1. Call list_accounts to see what already exists.`,
13
13
  `2. Infer the primary account type (asset / liability / income / expense) from the document's header, account type field, and transaction patterns.`,
14
- `3. If this document references an account that isn't yet in the chart, call create_account once. Mask the account number to the last 4 digits.`,
14
+ `3. If this document references an account that isn't yet in the chart, call create_account once (pass parent_id under the matching top-level type root). Mask the account number to the last 4 digits.`,
15
15
  `4. Persist any document-level metadata you find (statement_day, due_day, points_balance, etc.) using update_account_metadata.`,
16
- `5. For every transaction in the document, call record_journal_entry with balanced debit/credit lines. Use existing accounts where possible; create expense/income accounts as needed.`,
17
- `6. Never pause to ask the user. If a row is ambiguous, post your best-guess entry first, then call note_concern with details and the new entry_id. If a row is truly unparseable, skip it and call note_concern with the raw row text (no entry_id). A missing row is better than a wrong row.`,
16
+ `5. For every transaction in the document, call record_transaction with balanced debit/credit postings. Attach a merchant block (canonical_name + alias + default_account_id when categorization is confident) for any external counter-party. Reuse existing accounts; create expense categories under their parent (e.g. expense:food before expense:food:groceries) as needed. When you cannot categorize confidently, post the expense side to expense:uncategorized and call note_concern with kind="uncategorized_expense".`,
17
+ `6. Never pause to ask the user. If a row is ambiguous, post your best-guess transaction first, then call note_concern with details and the new transaction_id. If a row is truly unparseable, skip it and call note_concern with the raw row text (no transaction_id). A missing row is better than a wrong row.`,
18
18
  `7. When you are done, call mark_file_scanned with a short summary.`,
19
19
  ].join("\n");
20
20
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "plasalid",
3
- "version": "0.4.1",
4
- "description": "A local-first AI that reads every line of your transactions and coaches you the best move.",
3
+ "version": "0.5.0",
4
+ "description": "Plasalid The Harness for Personal Finance",
5
5
  "keywords": [
6
6
  "finance",
7
7
  "personal-finance",