plasalid 0.3.4 → 0.4.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.
Files changed (54) hide show
  1. package/README.md +29 -40
  2. package/dist/accounts/taxonomy.d.ts +1 -1
  3. package/dist/accounts/taxonomy.js +2 -2
  4. package/dist/ai/agent.d.ts +6 -5
  5. package/dist/ai/agent.js +7 -6
  6. package/dist/ai/memory.d.ts +12 -5
  7. package/dist/ai/memory.js +12 -0
  8. package/dist/ai/personas.d.ts +10 -0
  9. package/dist/ai/personas.js +123 -0
  10. package/dist/ai/prompt-sections.d.ts +44 -0
  11. package/dist/ai/prompt-sections.js +89 -0
  12. package/dist/ai/system-prompt.d.ts +3 -3
  13. package/dist/ai/system-prompt.js +44 -165
  14. package/dist/ai/tools/index.js +12 -7
  15. package/dist/ai/tools/ingest.d.ts +2 -1
  16. package/dist/ai/tools/ingest.js +220 -83
  17. package/dist/ai/tools/read.js +31 -0
  18. package/dist/ai/tools/review.d.ts +2 -0
  19. package/dist/ai/tools/review.js +362 -0
  20. package/dist/ai/tools/scan.js +4 -2
  21. package/dist/ai/tools/types.d.ts +23 -3
  22. package/dist/cli/commands/review.d.ts +2 -0
  23. package/dist/cli/commands/review.js +15 -0
  24. package/dist/cli/commands/scan.d.ts +4 -2
  25. package/dist/cli/commands/scan.js +147 -19
  26. package/dist/cli/index.js +11 -8
  27. package/dist/cli/ink/scan_dashboard.d.ts +38 -0
  28. package/dist/cli/ink/scan_dashboard.js +62 -0
  29. package/dist/cli/ux.d.ts +2 -1
  30. package/dist/cli/ux.js +36 -2
  31. package/dist/db/queries/account_balance.d.ts +1 -0
  32. package/dist/db/queries/concerns.d.ts +47 -0
  33. package/dist/db/queries/concerns.js +87 -0
  34. package/dist/db/queries/journal.d.ts +74 -8
  35. package/dist/db/queries/journal.js +131 -19
  36. package/dist/db/queries/recurrences.d.ts +33 -0
  37. package/dist/db/queries/recurrences.js +130 -0
  38. package/dist/db/schema.js +25 -2
  39. package/dist/reviewer/pipeline.d.ts +18 -0
  40. package/dist/reviewer/pipeline.js +46 -0
  41. package/dist/reviewer/prompts.d.ts +12 -0
  42. package/dist/reviewer/prompts.js +22 -0
  43. package/dist/scanner/account_mutex.d.ts +1 -0
  44. package/dist/scanner/account_mutex.js +16 -0
  45. package/dist/scanner/buffer.d.ts +48 -0
  46. package/dist/scanner/buffer.js +63 -0
  47. package/dist/scanner/concurrency.d.ts +14 -0
  48. package/dist/scanner/concurrency.js +31 -0
  49. package/dist/scanner/decrypt_queue.d.ts +57 -0
  50. package/dist/scanner/decrypt_queue.js +96 -0
  51. package/dist/scanner/pipeline.d.ts +46 -18
  52. package/dist/scanner/pipeline.js +250 -97
  53. package/dist/scanner/prompts.js +1 -1
  54. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { findAccountById, getAccountBalances, getNetWorth, getPeriodTotals, } from "../../db/queries/account_balance.js";
2
2
  import { listJournalLines } from "../../db/queries/journal.js";
3
+ import { listOpenConcerns } from "../../db/queries/concerns.js";
3
4
  import { searchJournalLines } from "../../db/queries/search.js";
4
5
  import { formatCurrencyAmount } from "../../currency.js";
5
6
  import { sanitizeForPrompt, sanitizeForPromptCell } from "../sanitize.js";
@@ -60,6 +61,17 @@ const DEFS = [
60
61
  required: ["from", "to"],
61
62
  },
62
63
  },
64
+ {
65
+ name: "list_open_concerns",
66
+ description: "List clarification requests recorded by the scanner that have not been resolved yet. Each row carries the prompt, optional candidate answers, and the file/entry/account it was attached to. The reviewer uses this to drive the step-by-step clarification loop.",
67
+ input_schema: {
68
+ type: "object",
69
+ properties: {
70
+ limit: { type: "number", default: 50 },
71
+ },
72
+ required: [],
73
+ },
74
+ },
63
75
  ];
64
76
  const LABELS = {
65
77
  get_account_balance: "Looking up balance",
@@ -67,6 +79,7 @@ const LABELS = {
67
79
  list_journal_entries: "Listing journal entries",
68
80
  search_transactions: "Searching transactions",
69
81
  get_period_totals: "Summing period totals",
82
+ list_open_concerns: "Listing open concerns",
70
83
  };
71
84
  async function execute(db, name, input, _ctx) {
72
85
  switch (name) {
@@ -116,6 +129,24 @@ async function execute(db, name, input, _ctx) {
116
129
  const totals = getPeriodTotals(db, input.from, input.to);
117
130
  return `Income ${formatTHB(totals.income)} · Expenses ${formatTHB(totals.expenses)} · Net ${formatTHB(totals.income - totals.expenses)}`;
118
131
  }
132
+ case "list_open_concerns": {
133
+ const rows = listOpenConcerns(db, input.limit ?? 50);
134
+ if (rows.length === 0)
135
+ return "No open concerns. The picture is clear.";
136
+ return rows
137
+ .map(r => {
138
+ const targets = [
139
+ r.entry_id ? `entry=${r.entry_id}` : null,
140
+ r.account_id ? `account=${r.account_id}` : null,
141
+ !r.entry_id && !r.account_id && r.file_id ? `file=${r.file_id}` : null,
142
+ ].filter(Boolean).join(" ");
143
+ const options = r.options_json
144
+ ? ` [options: ${JSON.parse(r.options_json).map(o => sanitizeForPrompt(o)).join(" | ")}]`
145
+ : "";
146
+ return `${r.id} ${targets} — ${sanitizeForPrompt(r.prompt)}${options}`;
147
+ })
148
+ .join("\n");
149
+ }
119
150
  default:
120
151
  return undefined;
121
152
  }
@@ -0,0 +1,2 @@
1
+ import type { ToolModule } from "./types.js";
2
+ export declare const reviewTools: ToolModule;
@@ -0,0 +1,362 @@
1
+ import { deleteAccount, findSimilarAccounts, findUnusedAccounts, mergeAccounts, renameAccount, } from "../../db/queries/account_balance.js";
2
+ import { deleteJournalEntry, findCorrelatedEntries, findDuplicateEntries, updateJournalEntry, updateJournalLine, } from "../../db/queries/journal.js";
3
+ import { findRecurrenceCandidates, linkEntryToRecurrence, recordRecurrence, } from "../../db/queries/recurrences.js";
4
+ import { formatCurrencyAmount } from "../../currency.js";
5
+ import { sanitizeForPrompt } from "../sanitize.js";
6
+ function formatTHB(amount) {
7
+ return formatCurrencyAmount(amount, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
8
+ }
9
+ const DEFS = [
10
+ {
11
+ name: "update_journal_entry",
12
+ description: "Header-only update: date, description, or source_page. To change amounts, delete the entry and record a new one.",
13
+ input_schema: {
14
+ type: "object",
15
+ properties: {
16
+ entry_id: { type: "string" },
17
+ date: { type: "string" },
18
+ description: { type: "string" },
19
+ source_page: { type: "number" },
20
+ },
21
+ required: ["entry_id"],
22
+ },
23
+ },
24
+ {
25
+ name: "update_journal_line",
26
+ description: "Safe single-line edit: re-categorize (account_id) or update memo. Refuses changes to debit/credit/currency — delete and re-record the entry for those.",
27
+ input_schema: {
28
+ type: "object",
29
+ properties: {
30
+ line_id: { type: "string" },
31
+ account_id: { type: "string" },
32
+ memo: { type: "string" },
33
+ },
34
+ required: ["line_id"],
35
+ },
36
+ },
37
+ {
38
+ name: "delete_journal_entry",
39
+ description: "Delete an entry and (via cascade) all its lines. The primitive for removing duplicates.",
40
+ input_schema: {
41
+ type: "object",
42
+ properties: { entry_id: { type: "string" } },
43
+ required: ["entry_id"],
44
+ },
45
+ },
46
+ {
47
+ name: "rename_account",
48
+ description: "Rename an account. Leaves lines and metadata untouched.",
49
+ input_schema: {
50
+ type: "object",
51
+ properties: { account_id: { type: "string" }, name: { type: "string" } },
52
+ required: ["account_id", "name"],
53
+ },
54
+ },
55
+ {
56
+ name: "merge_accounts",
57
+ description: "Move every journal line on `from_id` over to `to_id`, then delete the source account. Use to collapse duplicate accounts.",
58
+ input_schema: {
59
+ type: "object",
60
+ properties: { from_id: { type: "string" }, to_id: { type: "string" } },
61
+ required: ["from_id", "to_id"],
62
+ },
63
+ },
64
+ {
65
+ name: "delete_account",
66
+ description: "Delete an account that has no journal lines. Refuses if any line still references it — merge first.",
67
+ input_schema: {
68
+ type: "object",
69
+ properties: { account_id: { type: "string" } },
70
+ required: ["account_id"],
71
+ },
72
+ },
73
+ {
74
+ name: "find_duplicate_entries",
75
+ description: "Heuristic: groups journal entries by total amount and a configurable date tolerance. Returns groups with two or more candidate dupes.",
76
+ input_schema: {
77
+ type: "object",
78
+ properties: {
79
+ tolerance_days: { type: "number", default: 2 },
80
+ account_id: { type: "string" },
81
+ min_amount: { type: "number" },
82
+ },
83
+ required: [],
84
+ },
85
+ },
86
+ {
87
+ name: "find_similar_accounts",
88
+ description: "Pairwise Levenshtein similarity on account names. Returns pairs above the threshold, sorted highest first.",
89
+ input_schema: {
90
+ type: "object",
91
+ properties: { threshold: { type: "number", default: 0.85 } },
92
+ required: [],
93
+ },
94
+ },
95
+ {
96
+ name: "find_unused_accounts",
97
+ description: "Accounts with zero linked journal lines.",
98
+ input_schema: { type: "object", properties: {}, required: [] },
99
+ },
100
+ {
101
+ name: "find_correlated_entries",
102
+ description: "Surface pairs of entries that look like the same money movement recorded against different accounts (a transfer recorded once on each statement). Pairs are filtered: same amount + currency, within `tolerance_days` of each other, and the two entries share no account_ids (overlap → duplicate, not correlation).",
103
+ input_schema: {
104
+ type: "object",
105
+ properties: {
106
+ from: { type: "string", description: "ISO date inclusive lower bound (YYYY-MM-DD)." },
107
+ to: { type: "string", description: "ISO date inclusive upper bound (YYYY-MM-DD)." },
108
+ tolerance_days: { type: "number", default: 3, description: "Max day gap between paired entries." },
109
+ min_amount: { type: "number", default: 0 },
110
+ },
111
+ required: [],
112
+ },
113
+ },
114
+ {
115
+ name: "find_recurrences",
116
+ description: "Detect candidate recurring transactions by grouping unlinked entries on the same account + amount + side (debit/credit), then classifying cadence (weekly/biweekly/monthly/annually/irregular) from the median gap between consecutive dates. Skips entries already linked to a recurrence.",
117
+ input_schema: {
118
+ type: "object",
119
+ properties: {
120
+ account_id: { type: "string", description: "Limit to one account; omit for all." },
121
+ min_occurrences: { type: "number", default: 3, description: "Minimum sightings to qualify." },
122
+ },
123
+ required: [],
124
+ },
125
+ },
126
+ {
127
+ name: "record_recurrence",
128
+ description: "Create a recurrences row and link every supplied journal entry to it. Computes first_seen_date, last_seen_date, and next_expected_date from the member entries. Use this after the user confirms a recurrence candidate.",
129
+ input_schema: {
130
+ type: "object",
131
+ properties: {
132
+ account_id: { type: "string", description: "The account this recurs on." },
133
+ description: { type: "string", description: "Human label, e.g. 'Spotify subscription', 'Salary', 'Rent'." },
134
+ frequency: { type: "string", enum: ["weekly", "biweekly", "monthly", "annually"] },
135
+ amount_typical: { type: "number", description: "Representative amount (typically the matching amount of the member entries)." },
136
+ currency: { type: "string", default: "THB" },
137
+ entry_ids: { type: "array", items: { type: "string" }, description: "Journal entry ids to link to this recurrence." },
138
+ notes: { type: "string", description: "Optional context the chat agent can read later." },
139
+ },
140
+ required: ["account_id", "description", "frequency", "entry_ids"],
141
+ },
142
+ },
143
+ {
144
+ name: "link_entry_to_recurrence",
145
+ description: "Attach a single newly-seen entry to an existing recurrence. Recomputes last_seen_date and next_expected_date on the recurrence.",
146
+ input_schema: {
147
+ type: "object",
148
+ properties: {
149
+ entry_id: { type: "string" },
150
+ recurrence_id: { type: "string" },
151
+ },
152
+ required: ["entry_id", "recurrence_id"],
153
+ },
154
+ },
155
+ {
156
+ name: "mark_review_done",
157
+ description: "Call when the review pass is complete. The summary is shown to the user.",
158
+ input_schema: {
159
+ type: "object",
160
+ properties: { summary: { type: "string" } },
161
+ required: ["summary"],
162
+ },
163
+ },
164
+ ];
165
+ const LABELS = {
166
+ update_journal_entry: "Updating journal entry",
167
+ update_journal_line: "Updating journal line",
168
+ delete_journal_entry: "Deleting journal entry",
169
+ rename_account: "Renaming account",
170
+ merge_accounts: "Merging accounts",
171
+ delete_account: "Deleting account",
172
+ find_duplicate_entries: "Finding duplicate entries",
173
+ find_similar_accounts: "Finding similar accounts",
174
+ find_unused_accounts: "Finding unused accounts",
175
+ find_correlated_entries: "Finding correlated entries",
176
+ find_recurrences: "Finding recurrences",
177
+ record_recurrence: "Recording recurrence",
178
+ link_entry_to_recurrence: "Linking entry to recurrence",
179
+ mark_review_done: "Finalizing review",
180
+ };
181
+ async function execute(db, name, input, ctx) {
182
+ switch (name) {
183
+ case "update_journal_entry": {
184
+ if (ctx?.dryRun)
185
+ return `Would update entry ${input.entry_id}: ${JSON.stringify(input)}`;
186
+ const changed = updateJournalEntry(db, input.entry_id, {
187
+ date: input.date,
188
+ description: input.description,
189
+ source_page: input.source_page,
190
+ });
191
+ return changed === 0
192
+ ? `Entry ${input.entry_id} not found or no fields to update.`
193
+ : `Updated entry ${input.entry_id}.`;
194
+ }
195
+ case "update_journal_line": {
196
+ if (ctx?.dryRun)
197
+ return `Would update line ${input.line_id}: ${JSON.stringify(input)}`;
198
+ const changed = updateJournalLine(db, input.line_id, {
199
+ account_id: input.account_id,
200
+ memo: input.memo,
201
+ });
202
+ return changed === 0
203
+ ? `Line ${input.line_id} not found or no fields to update.`
204
+ : `Updated line ${input.line_id}.`;
205
+ }
206
+ case "delete_journal_entry": {
207
+ if (ctx?.dryRun)
208
+ return `Would delete entry ${input.entry_id} (and its lines).`;
209
+ const changed = deleteJournalEntry(db, input.entry_id);
210
+ return changed === 0
211
+ ? `Entry ${input.entry_id} not found.`
212
+ : `Deleted entry ${input.entry_id} and its lines.`;
213
+ }
214
+ case "rename_account": {
215
+ if (ctx?.dryRun)
216
+ return `Would rename ${input.account_id} → "${input.name}".`;
217
+ const changed = renameAccount(db, input.account_id, input.name);
218
+ return changed === 0
219
+ ? `Account ${input.account_id} not found.`
220
+ : `Renamed ${input.account_id} → "${sanitizeForPrompt(input.name)}".`;
221
+ }
222
+ case "merge_accounts": {
223
+ if (ctx?.dryRun)
224
+ return `Would merge ${input.from_id} → ${input.to_id}.`;
225
+ try {
226
+ const moved = mergeAccounts(db, input.from_id, input.to_id);
227
+ return `Merged ${input.from_id} → ${input.to_id}; moved ${moved} line(s).`;
228
+ }
229
+ catch (err) {
230
+ return `Could not merge: ${err.message}`;
231
+ }
232
+ }
233
+ case "delete_account": {
234
+ if (ctx?.dryRun)
235
+ return `Would delete account ${input.account_id}.`;
236
+ try {
237
+ deleteAccount(db, input.account_id);
238
+ return `Deleted account ${input.account_id}.`;
239
+ }
240
+ catch (err) {
241
+ return `Could not delete: ${err.message}`;
242
+ }
243
+ }
244
+ case "find_duplicate_entries": {
245
+ const groups = findDuplicateEntries(db, {
246
+ toleranceDays: input.tolerance_days,
247
+ accountId: input.account_id,
248
+ minAmount: input.min_amount,
249
+ });
250
+ if (groups.length === 0)
251
+ return "No candidate duplicate groups found.";
252
+ return groups
253
+ .map((g, i) => {
254
+ const header = `Group ${i + 1} — ${formatTHB(g[0].amount)}`;
255
+ const lines = g.map((e, j) => {
256
+ const accounts = e.account_names.length > 0
257
+ ? e.account_names.map(n => sanitizeForPrompt(n)).join(", ")
258
+ : "(no lines)";
259
+ return ` ${j + 1}. ${e.date} "${sanitizeForPrompt(e.description)}" — ${accounts} [${e.id}]`;
260
+ });
261
+ return `${header}\n${lines.join("\n")}`;
262
+ })
263
+ .join("\n\n");
264
+ }
265
+ case "find_similar_accounts": {
266
+ const pairs = findSimilarAccounts(db, input.threshold);
267
+ if (pairs.length === 0)
268
+ return "No similar account pairs above threshold.";
269
+ return pairs
270
+ .map(p => `${p.similarity}: ${p.a.id} (${sanitizeForPrompt(p.a.name)}) <-> ${p.b.id} (${sanitizeForPrompt(p.b.name)})`)
271
+ .join("\n");
272
+ }
273
+ case "find_unused_accounts": {
274
+ const rows = findUnusedAccounts(db);
275
+ if (rows.length === 0)
276
+ return "No unused accounts.";
277
+ return rows.map(a => `${a.id} | ${sanitizeForPrompt(a.name)} | ${a.type}`).join("\n");
278
+ }
279
+ case "find_correlated_entries": {
280
+ const pairs = findCorrelatedEntries(db, {
281
+ from: input.from,
282
+ to: input.to,
283
+ toleranceDays: input.tolerance_days,
284
+ minAmount: input.min_amount,
285
+ });
286
+ if (pairs.length === 0)
287
+ return "No correlated entry pairs found.";
288
+ return pairs
289
+ .map((p, i) => {
290
+ const accountsA = p.a.account_names.length > 0
291
+ ? p.a.account_names.map(n => sanitizeForPrompt(n)).join(", ")
292
+ : "(no lines)";
293
+ const accountsB = p.b.account_names.length > 0
294
+ ? p.b.account_names.map(n => sanitizeForPrompt(n)).join(", ")
295
+ : "(no lines)";
296
+ return [
297
+ `Pair ${i + 1} — ${formatTHB(p.amount)} ${p.currency} (gap ${p.day_gap} day${p.day_gap === 1 ? "" : "s"})`,
298
+ ` A: ${p.a.date} "${sanitizeForPrompt(p.a.description)}" — ${accountsA} [${p.a.id}]`,
299
+ ` B: ${p.b.date} "${sanitizeForPrompt(p.b.description)}" — ${accountsB} [${p.b.id}]`,
300
+ ].join("\n");
301
+ })
302
+ .join("\n\n");
303
+ }
304
+ case "find_recurrences": {
305
+ const candidates = findRecurrenceCandidates(db, {
306
+ accountId: input.account_id,
307
+ minOccurrences: input.min_occurrences,
308
+ });
309
+ if (candidates.length === 0)
310
+ return "No recurrence candidates found.";
311
+ return candidates
312
+ .map((c, i) => {
313
+ const dates = c.entries.map(e => e.date).join(", ");
314
+ const ids = c.entries.map(e => e.id).join(", ");
315
+ return [
316
+ `Candidate ${i + 1} — ${formatTHB(c.amount)} ${c.currency} on ${sanitizeForPrompt(c.account_name)} (${c.side})`,
317
+ ` Sightings (${c.entries.length}): ${dates}`,
318
+ ` Median gap: ${c.median_days_between} day(s) → implied ${c.implied_frequency}`,
319
+ ` Entry ids: ${ids}`,
320
+ ].join("\n");
321
+ })
322
+ .join("\n\n");
323
+ }
324
+ case "record_recurrence": {
325
+ if (ctx?.dryRun)
326
+ return `Would record ${input.frequency} recurrence "${input.description}" linking ${(input.entry_ids || []).length} entries on ${input.account_id}.`;
327
+ try {
328
+ const id = recordRecurrence(db, {
329
+ account_id: input.account_id,
330
+ description: input.description,
331
+ frequency: input.frequency,
332
+ amount_typical: input.amount_typical ?? null,
333
+ currency: input.currency,
334
+ entry_ids: input.entry_ids || [],
335
+ notes: input.notes ?? null,
336
+ });
337
+ return `Recorded recurrence ${id} ("${sanitizeForPrompt(input.description)}", ${input.frequency}); linked ${(input.entry_ids || []).length} entry(ies).`;
338
+ }
339
+ catch (err) {
340
+ return `Could not record recurrence: ${err.message}`;
341
+ }
342
+ }
343
+ case "link_entry_to_recurrence": {
344
+ if (ctx?.dryRun)
345
+ return `Would link entry ${input.entry_id} → recurrence ${input.recurrence_id}.`;
346
+ try {
347
+ linkEntryToRecurrence(db, input.entry_id, input.recurrence_id);
348
+ return `Linked entry ${input.entry_id} → recurrence ${input.recurrence_id}.`;
349
+ }
350
+ catch (err) {
351
+ return `Could not link: ${err.message}`;
352
+ }
353
+ }
354
+ case "mark_review_done": {
355
+ ctx?.onComplete?.(input.summary || "");
356
+ return `Review complete. Summary: ${sanitizeForPrompt(input.summary || "")}`;
357
+ }
358
+ default:
359
+ return undefined;
360
+ }
361
+ }
362
+ export const reviewTools = { DEFS, LABELS, execute };
@@ -18,7 +18,9 @@ const LABELS = {
18
18
  async function execute(_db, name, input, ctx) {
19
19
  if (name !== "mark_file_scanned")
20
20
  return undefined;
21
- ctx?.onComplete?.(input.summary || "");
22
- return `Marked file as scanned. Summary: ${sanitizeForPrompt(input.summary || "")}`;
21
+ const summary = input.summary || "";
22
+ ctx?.buffer?.markDone(summary);
23
+ ctx?.onComplete?.(summary);
24
+ return `Marked file as scanned. Summary: ${sanitizeForPrompt(summary)}`;
23
25
  }
24
26
  export const scanTools = { DEFS, LABELS, execute };
@@ -1,6 +1,19 @@
1
1
  import type Database from "libsql";
2
2
  import type { ToolDefinition } from "../provider.js";
3
- export type ToolProfile = "scan" | "chat" | "reconcile";
3
+ import type { BufferedWriteContext } from "../../scanner/buffer.js";
4
+ export type ToolProfile = "scan" | "chat" | "review";
5
+ /**
6
+ * Structured highlights the review agent can pass to ask_user. The prompter
7
+ * renders them as a single colored header line above the question (each
8
+ * category gets its own chalk color), so the user can scan amount / date /
9
+ * merchant / accounts at a glance without parsing prose.
10
+ */
11
+ export interface PromptUserFacts {
12
+ amount?: string;
13
+ date?: string;
14
+ merchant?: string;
15
+ accounts?: string[];
16
+ }
4
17
  export interface AgentExecutionContext {
5
18
  /** Set during scan so `record_journal_entry` can stamp `source_file_id`. */
6
19
  fileId?: string;
@@ -9,9 +22,16 @@ export interface AgentExecutionContext {
9
22
  /** When true, mutating tools become no-ops that return a "would do X" preview. */
10
23
  dryRun?: boolean;
11
24
  /** Synchronously prompt the user (only invoked when interactive === true). */
12
- promptUser?: (prompt: string, options?: string[]) => Promise<string>;
13
- /** Called when the model declares the session is done (scan or reconcile). */
25
+ promptUser?: (prompt: string, options?: string[], facts?: PromptUserFacts) => Promise<string>;
26
+ /** Called when the model declares the session is done (scan or review). */
14
27
  onComplete?: (summary: string) => void;
28
+ /**
29
+ * Scan-only: when set, journal entries and concerns are queued here instead
30
+ * of being written directly to the DB. Account writes still hit the DB
31
+ * eagerly (serialized via account_mutex) so concurrent scan agents share
32
+ * the same chart of accounts.
33
+ */
34
+ buffer?: BufferedWriteContext;
15
35
  }
16
36
  /**
17
37
  * A tool module owns a slice of tool definitions, the spinner labels that go
@@ -0,0 +1,2 @@
1
+ import { type ReviewOptions } from "../../reviewer/pipeline.js";
2
+ export declare function runReviewCommand(opts: ReviewOptions): Promise<void>;
@@ -0,0 +1,15 @@
1
+ import chalk from "chalk";
2
+ import { runReview } from "../../reviewer/pipeline.js";
3
+ export async function runReviewCommand(opts) {
4
+ try {
5
+ const result = await runReview(opts);
6
+ if (result.summary) {
7
+ console.log("");
8
+ console.log(chalk.bold(result.summary));
9
+ }
10
+ }
11
+ catch (err) {
12
+ console.error(chalk.red(`Review failed: ${err.message}`));
13
+ process.exitCode = 1;
14
+ }
15
+ }
@@ -1,4 +1,6 @@
1
- export declare function runScanCommand(opts: {
1
+ export interface ScanCommandOptions {
2
2
  regex?: string;
3
3
  force?: boolean;
4
- }): Promise<void>;
4
+ parallel?: number;
5
+ }
6
+ export declare function runScanCommand(opts: ScanCommandOptions): Promise<void>;