plasalid 0.7.1 → 0.7.2

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 (141) hide show
  1. package/README.md +2 -2
  2. package/dist/ai/agent.d.ts +6 -7
  3. package/dist/ai/agent.js +27 -11
  4. package/dist/ai/personas.js +48 -46
  5. package/dist/ai/system-prompt.js +1 -1
  6. package/dist/ai/tools/account-mutex.d.ts +1 -0
  7. package/dist/ai/tools/account-mutex.js +16 -0
  8. package/dist/ai/tools/index.js +4 -12
  9. package/dist/ai/tools/ingest.d.ts +1 -1
  10. package/dist/ai/tools/ingest.js +282 -242
  11. package/dist/ai/tools/merchants.js +1 -28
  12. package/dist/ai/tools/read.js +8 -8
  13. package/dist/ai/tools/record.js +3 -36
  14. package/dist/ai/tools/resolve.js +25 -22
  15. package/dist/ai/tools/scan.js +0 -1
  16. package/dist/ai/tools/types.d.ts +14 -21
  17. package/dist/cli/commands/record.js +1 -82
  18. package/dist/cli/commands/resolve.d.ts +5 -2
  19. package/dist/cli/commands/resolve.js +36 -5
  20. package/dist/cli/commands/revert.js +4 -2
  21. package/dist/cli/commands/rules.js +2 -2
  22. package/dist/cli/commands/scan.js +199 -128
  23. package/dist/cli/commands/status.js +5 -5
  24. package/dist/cli/index.js +8 -29
  25. package/dist/cli/ink/ScanDashboard.d.ts +49 -0
  26. package/dist/cli/ink/ScanDashboard.js +214 -0
  27. package/dist/cli/ink/scan_dashboard.d.ts +40 -25
  28. package/dist/cli/ink/scan_dashboard.js +139 -44
  29. package/dist/db/queries/account-balance.d.ts +1 -1
  30. package/dist/db/queries/questions.d.ts +62 -0
  31. package/dist/db/queries/questions.js +110 -0
  32. package/dist/db/queries/transactions.d.ts +1 -1
  33. package/dist/db/queries/unknowns.d.ts +17 -15
  34. package/dist/db/queries/unknowns.js +35 -39
  35. package/dist/db/schema.js +6 -28
  36. package/dist/scanner/audit/auditor.d.ts +31 -0
  37. package/dist/scanner/audit/auditor.js +72 -0
  38. package/dist/scanner/audit/engine.d.ts +10 -0
  39. package/dist/scanner/audit/engine.js +98 -0
  40. package/dist/scanner/audit/eventBus.d.ts +60 -0
  41. package/dist/scanner/audit/eventBus.js +35 -0
  42. package/dist/scanner/audit/passes/index.d.ts +11 -0
  43. package/dist/scanner/audit/passes/index.js +9 -0
  44. package/dist/scanner/audit/passes/types.d.ts +23 -0
  45. package/dist/scanner/audit/passes/types.js +1 -0
  46. package/dist/scanner/audit/types.d.ts +27 -0
  47. package/dist/scanner/audit/types.js +1 -0
  48. package/dist/scanner/auditor.d.ts +51 -0
  49. package/dist/scanner/auditor.js +80 -0
  50. package/dist/scanner/buffer/engine.d.ts +9 -0
  51. package/dist/scanner/buffer/engine.js +110 -0
  52. package/dist/scanner/buffer/sharedBuffer.d.ts +78 -0
  53. package/dist/scanner/buffer/sharedBuffer.js +130 -0
  54. package/dist/scanner/buffer/types.d.ts +67 -0
  55. package/dist/scanner/buffer/types.js +1 -0
  56. package/dist/scanner/buffer.d.ts +45 -38
  57. package/dist/scanner/buffer.js +93 -61
  58. package/dist/scanner/bus/engine.d.ts +11 -0
  59. package/dist/scanner/bus/engine.js +42 -0
  60. package/dist/scanner/bus/types.d.ts +53 -0
  61. package/dist/scanner/bus/types.js +1 -0
  62. package/dist/scanner/bus.d.ts +38 -0
  63. package/dist/scanner/bus.js +37 -0
  64. package/dist/scanner/chunk-worker.d.ts +19 -0
  65. package/dist/scanner/chunk-worker.js +67 -0
  66. package/dist/scanner/chunkWorker.d.ts +20 -0
  67. package/dist/scanner/chunkWorker.js +59 -0
  68. package/dist/scanner/chunker/chunker.d.ts +7 -0
  69. package/dist/scanner/chunker/chunker.js +60 -0
  70. package/dist/scanner/chunker.d.ts +7 -0
  71. package/dist/scanner/chunker.js +60 -0
  72. package/dist/scanner/converge.d.ts +29 -0
  73. package/dist/scanner/converge.js +15 -0
  74. package/dist/scanner/decrypt.d.ts +10 -0
  75. package/dist/scanner/decrypt.js +80 -0
  76. package/dist/scanner/engine/scanEngine.d.ts +24 -0
  77. package/dist/scanner/engine/scanEngine.js +87 -0
  78. package/dist/scanner/engine/types.d.ts +90 -0
  79. package/dist/scanner/engine/types.js +1 -0
  80. package/dist/scanner/engine.d.ts +90 -0
  81. package/dist/scanner/engine.js +84 -0
  82. package/dist/scanner/file-worker.d.ts +33 -0
  83. package/dist/scanner/file-worker.js +28 -0
  84. package/dist/scanner/fileWorker.d.ts +33 -0
  85. package/dist/scanner/fileWorker.js +22 -0
  86. package/dist/scanner/hooks/types.d.ts +25 -0
  87. package/dist/scanner/hooks/types.js +1 -0
  88. package/dist/scanner/hooks.d.ts +23 -0
  89. package/dist/scanner/hooks.js +1 -0
  90. package/dist/scanner/parse.d.ts +10 -0
  91. package/dist/scanner/parse.js +47 -0
  92. package/dist/scanner/passes/index.d.ts +8 -0
  93. package/dist/scanner/passes/index.js +6 -0
  94. package/dist/scanner/passes/types.d.ts +22 -0
  95. package/dist/scanner/passes/types.js +1 -0
  96. package/dist/scanner/pdf/chunker.d.ts +7 -0
  97. package/dist/scanner/pdf/chunker.js +60 -0
  98. package/dist/scanner/pdf/password-store.d.ts +34 -0
  99. package/dist/scanner/pdf/password-store.js +83 -0
  100. package/dist/scanner/pdf/pdf-unlock.d.ts +17 -0
  101. package/dist/scanner/pdf/pdf-unlock.js +50 -0
  102. package/dist/scanner/pdf/pdf.d.ts +17 -0
  103. package/dist/scanner/pdf/pdf.js +36 -0
  104. package/dist/scanner/pdf/state-machine.d.ts +60 -0
  105. package/dist/scanner/pdf/state-machine.js +64 -0
  106. package/dist/scanner/pdf/unlock.d.ts +22 -0
  107. package/dist/scanner/pdf/unlock.js +121 -0
  108. package/dist/scanner/phase-decrypt.d.ts +10 -0
  109. package/dist/scanner/phase-decrypt.js +80 -0
  110. package/dist/scanner/phase-parse.d.ts +10 -0
  111. package/dist/scanner/phase-parse.js +46 -0
  112. package/dist/scanner/phases/chunk.d.ts +8 -0
  113. package/dist/scanner/phases/chunk.js +13 -0
  114. package/dist/scanner/phases/commit.d.ts +12 -0
  115. package/dist/scanner/phases/commit.js +140 -0
  116. package/dist/scanner/phases/decrypt.d.ts +10 -0
  117. package/dist/scanner/phases/decrypt.js +80 -0
  118. package/dist/scanner/phases/parse.d.ts +10 -0
  119. package/dist/scanner/phases/parse.js +46 -0
  120. package/dist/scanner/phases/resolve.d.ts +10 -0
  121. package/dist/scanner/phases/resolve.js +17 -0
  122. package/dist/scanner/phases/review.d.ts +10 -0
  123. package/dist/scanner/phases/review.js +12 -0
  124. package/dist/scanner/progress.d.ts +14 -0
  125. package/dist/scanner/progress.js +21 -0
  126. package/dist/scanner/resolver-memory.d.ts +8 -0
  127. package/dist/scanner/resolver-memory.js +24 -0
  128. package/dist/scanner/resolver.d.ts +39 -0
  129. package/dist/scanner/resolver.js +196 -0
  130. package/dist/scanner/result.d.ts +17 -0
  131. package/dist/scanner/result.js +19 -0
  132. package/dist/scanner/run-passes.d.ts +30 -0
  133. package/dist/scanner/run-passes.js +15 -0
  134. package/dist/scanner/unlock.js +1 -1
  135. package/dist/scanner/worker.d.ts +19 -0
  136. package/dist/scanner/worker.js +67 -0
  137. package/dist/scanner/workers/chunkWorker.d.ts +20 -0
  138. package/dist/scanner/workers/chunkWorker.js +65 -0
  139. package/dist/scanner/workers/fileWorker.d.ts +32 -0
  140. package/dist/scanner/workers/fileWorker.js +22 -0
  141. package/package.json +1 -1
@@ -1,5 +1,4 @@
1
1
  import { upsertMerchant, findMerchantByAlias, findMerchantById, setMerchantDefaultAccount, } from "../../db/queries/merchants.js";
2
- import { appendAction } from "../../db/queries/action-log.js";
3
2
  import { sanitizeForPrompt } from "../sanitize.js";
4
3
  /**
5
4
  * Merchant tools
@@ -65,30 +64,14 @@ const LABELS = {
65
64
  find_merchant_by_descriptor: "Looking up merchant",
66
65
  set_merchant_default_account: "Updating merchant default",
67
66
  };
68
- async function execute(db, name, input, ctx) {
67
+ async function execute(db, name, input, _ctx) {
69
68
  switch (name) {
70
69
  case "find_or_create_merchant": {
71
- const existing = db
72
- .prepare(`SELECT id FROM merchants WHERE canonical_name = ?`)
73
- .get(input.canonical_name);
74
70
  const merchant = upsertMerchant(db, {
75
71
  canonical_name: input.canonical_name,
76
72
  alias: input.alias,
77
73
  default_account_id: input.default_account_id,
78
74
  });
79
- if (ctx?.correlationId && !existing) {
80
- appendAction(db, {
81
- correlation_id: ctx.correlationId,
82
- command: ctx.command ?? "record",
83
- user_input: ctx.userInput ?? null,
84
- action_type: "create_merchant",
85
- target_id: merchant.id,
86
- payload: {
87
- canonical_name: merchant.canonical_name,
88
- default_account_id: merchant.default_account_id,
89
- },
90
- });
91
- }
92
75
  const defaultStr = merchant.default_account_id
93
76
  ? ` (default → ${merchant.default_account_id})`
94
77
  : "";
@@ -109,16 +92,6 @@ async function execute(db, name, input, ctx) {
109
92
  return `Merchant ${input.merchant_id} not found.`;
110
93
  try {
111
94
  const result = setMerchantDefaultAccount(db, input.merchant_id, input.account_id);
112
- if (ctx?.correlationId) {
113
- appendAction(db, {
114
- correlation_id: ctx.correlationId,
115
- command: ctx.command ?? "record",
116
- user_input: ctx.userInput ?? null,
117
- action_type: "update_merchant_default",
118
- target_id: input.merchant_id,
119
- payload: { before: result.before, after: result.after },
120
- });
121
- }
122
95
  return `Merchant ${input.merchant_id}: default ${result.before ?? "(none)"} → ${result.after}.`;
123
96
  }
124
97
  catch (err) {
@@ -1,6 +1,6 @@
1
1
  import { findAccountById, getAccountBalances, getNetWorth, getPeriodTotals, } from "../../db/queries/account-balance.js";
2
2
  import { listPostings } from "../../db/queries/transactions.js";
3
- import { listOpenUnknowns } from "../../db/queries/unknowns.js";
3
+ import { listQuestions } from "../../db/queries/questions.js";
4
4
  import { searchPostings } from "../../db/queries/search.js";
5
5
  import { formatAmount } from "../../currency.js";
6
6
  import { sanitizeForPrompt, sanitizeForPromptCell } from "../sanitize.js";
@@ -62,15 +62,15 @@ const DEFS = [
62
62
  },
63
63
  },
64
64
  {
65
- name: "list_open_unknowns",
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/transaction/account it was attached to. The resolver uses this to drive the step-by-step clarification loop.",
65
+ name: "list_questions",
66
+ description: "List clarification questions recorded by the scanner that have not been resolved yet. Each row carries the prompt, optional candidate answers, and the file/transaction/account it was attached to. The resolver uses this to drive the step-by-step clarification loop.",
67
67
  input_schema: {
68
68
  type: "object",
69
69
  properties: {
70
70
  limit: { type: "number", default: 50 },
71
71
  kind: {
72
72
  type: "string",
73
- description: "Optional filter by unknown kind (e.g. 'uncategorized_expense').",
73
+ description: "Optional filter by question kind (e.g. 'uncategorized_expense').",
74
74
  },
75
75
  },
76
76
  required: [],
@@ -83,7 +83,7 @@ const LABELS = {
83
83
  list_postings: "Listing postings",
84
84
  search_transactions: "Searching transactions",
85
85
  get_period_totals: "Summing period totals",
86
- list_open_unknowns: "Listing open unknowns",
86
+ list_questions: "Listing questions",
87
87
  };
88
88
  async function execute(db, name, input, _ctx) {
89
89
  switch (name) {
@@ -139,13 +139,13 @@ async function execute(db, name, input, _ctx) {
139
139
  const totals = getPeriodTotals(db, input.from, input.to);
140
140
  return `Income ${formatAmount(totals.income)} · Expenses ${formatAmount(totals.expenses)} · Net ${formatAmount(totals.income - totals.expenses)}`;
141
141
  }
142
- case "list_open_unknowns": {
143
- const rows = listOpenUnknowns(db, input.limit ?? 50);
142
+ case "list_questions": {
143
+ const rows = listQuestions(db, input.limit ?? 50);
144
144
  const filtered = input.kind
145
145
  ? rows.filter((r) => r.kind === input.kind)
146
146
  : rows;
147
147
  if (filtered.length === 0)
148
- return "No open unknowns. The picture is clear.";
148
+ return "No questions. The picture is clear.";
149
149
  return filtered
150
150
  .map((r) => {
151
151
  const targets = [
@@ -1,6 +1,5 @@
1
1
  import { findAccountById, findAccountsByFuzzyName, getAccountBalances, ensureStructuralAccount, renameAccount, deleteAccount, } from "../../db/queries/account-balance.js";
2
2
  import { validateTransaction, insertTransactionRows, } from "../../db/queries/transactions.js";
3
- import { appendAction } from "../../db/queries/action-log.js";
4
3
  import { formatAmount } from "../../currency.js";
5
4
  import { sanitizeForPrompt } from "../sanitize.js";
6
5
  const EQUITY_ADJUST_ID = "equity:adjustments";
@@ -11,9 +10,7 @@ function todayIso() {
11
10
  * Record-only tool definitions
12
11
  *
13
12
  * `find_similar_accounts` and `clarify` are reads / prompts; `adjust_account_balance`,
14
- * `rename_account`, and `delete_account` mutate the DB. Of those, only
15
- * `adjust_account_balance` writes an action_log row (with `action_type='adjust_balance'`);
16
- * rename and delete are simple shape changes without an audit entry.
13
+ * `rename_account`, and `delete_account` mutate the DB.
17
14
  */
18
15
  const DEFS = [
19
16
  {
@@ -81,7 +78,7 @@ const DEFS = [
81
78
  },
82
79
  {
83
80
  name: "clarify",
84
- description: "Ask the user a clarifying question and return their answer as a string. Use when the utterance is ambiguous (multiple matching accounts, missing amount, unclear date, can't tell expense vs transfer, plan confirmation before a multi-step action). Unlike resolve's ask_user, this does NOT write to the unknowns table — record-time questions are transient.",
81
+ description: "Ask the user a clarifying question and return their answer as a string. Use when the utterance is ambiguous (multiple matching accounts, missing amount, unclear date, can't tell expense vs transfer, plan confirmation before a multi-step action). Unlike resolve's ask_user, this does NOT write to the questions table — record-time questions are transient.",
85
82
  input_schema: {
86
83
  type: "object",
87
84
  properties: {
@@ -200,40 +197,10 @@ async function adjustAccountBalance(db, input, ctx) {
200
197
  }
201
198
  try {
202
199
  const tx = db.transaction(() => {
203
- const equityExisted = !!findAccountById(db, EQUITY_ADJUST_ID);
204
- if (!equityExisted) {
200
+ if (!findAccountById(db, EQUITY_ADJUST_ID)) {
205
201
  ensureStructuralAccount(db, "equity:adjustments");
206
- if (ctx.correlationId) {
207
- appendAction(db, {
208
- correlation_id: ctx.correlationId,
209
- command: ctx.command ?? "record",
210
- user_input: ctx.userInput ?? null,
211
- action_type: "create_account",
212
- target_id: EQUITY_ADJUST_ID,
213
- payload: { row: findAccountById(db, EQUITY_ADJUST_ID) },
214
- });
215
- }
216
202
  }
217
203
  insertTransactionRows(db, validated);
218
- if (ctx.correlationId) {
219
- appendAction(db, {
220
- correlation_id: ctx.correlationId,
221
- command: ctx.command ?? "record",
222
- user_input: ctx.userInput ?? null,
223
- action_type: "adjust_balance",
224
- target_id: validated.id,
225
- payload: {
226
- account_id: account.id,
227
- before_balance: current,
228
- after_balance: target,
229
- transaction: {
230
- date: validated.date,
231
- description: validated.description,
232
- },
233
- postings: validated.postings,
234
- },
235
- });
236
- }
237
204
  });
238
205
  tx();
239
206
  }
@@ -2,13 +2,6 @@ import { deleteTransaction, updateTransaction, updatePosting, } from "../../db/q
2
2
  import { mergeAccounts } from "../../db/queries/account-balance.js";
3
3
  import { linkTransactionToRecurrence, recordRecurrence, } from "../../db/queries/recurrences.js";
4
4
  import { sanitizeForPrompt } from "../sanitize.js";
5
- /**
6
- * Resolve-mode tools: the mutation primitives an agent calls to APPLY the
7
- * answer to an open unknown. Inspection has already happened (scanner inspectors
8
- * wrote the unknowns); discovery tools (find_duplicate_transactions,
9
- * find_recurrences, etc.) don't live here — the resolver iterates unknowns
10
- * and asks the user, it doesn't search.
11
- */
12
5
  const DEFS = [
13
6
  {
14
7
  name: "update_transaction",
@@ -48,7 +41,7 @@ const DEFS = [
48
41
  },
49
42
  {
50
43
  name: "record_recurrence",
51
- description: "Create a recurrences row and link every supplied transaction to it. Computes first_seen_date, last_seen_date, and next_expected_date from the member transactions. Use this after the user confirms a recurrence_candidate unknown.",
44
+ description: "Create a recurrences row and link every supplied transaction to it. Computes first_seen_date, last_seen_date, and next_expected_date from the member transactions. Use this after the user confirms a recurrence_candidate question.",
52
45
  input_schema: {
53
46
  type: "object",
54
47
  properties: {
@@ -96,7 +89,7 @@ const DEFS = [
96
89
  },
97
90
  {
98
91
  name: "merge_accounts",
99
- description: "Move every posting on `from_id` over to `to_id`, then delete the source account. Use to apply a similar_accounts unknown's 'Merge A into B' resolution. Refuses if the source still has child accounts.",
92
+ description: "Move every posting on `from_id` over to `to_id`, then delete the source account. Use to apply a similar_accounts question's 'Merge A into B' resolution. Refuses if the source still has child accounts.",
100
93
  input_schema: {
101
94
  type: "object",
102
95
  properties: { from_id: { type: "string" }, to_id: { type: "string" } },
@@ -112,7 +105,7 @@ const LABELS = {
112
105
  link_transaction_to_recurrence: "Linking transaction to recurrence",
113
106
  merge_accounts: "Merging accounts",
114
107
  };
115
- async function execute(db, name, input) {
108
+ async function execute(db, name, input, _ctx) {
116
109
  switch (name) {
117
110
  case "update_transaction": {
118
111
  const changed = updateTransaction(db, input.transaction_id, {
@@ -140,20 +133,30 @@ async function execute(db, name, input) {
140
133
  : `Deleted transaction ${input.transaction_id} and its postings.`;
141
134
  }
142
135
  case "record_recurrence": {
143
- const id = recordRecurrence(db, {
144
- account_id: input.account_id,
145
- description: input.description,
146
- frequency: input.frequency,
147
- amount_typical: input.amount_typical ?? null,
148
- currency: input.currency,
149
- transaction_ids: input.transaction_ids || [],
150
- notes: input.notes ?? null,
151
- });
152
- return `Recorded recurrence ${id} ("${sanitizeForPrompt(input.description)}", ${input.frequency}); linked ${(input.transaction_ids || []).length} transaction(s).`;
136
+ try {
137
+ const id = recordRecurrence(db, {
138
+ account_id: input.account_id,
139
+ description: input.description,
140
+ frequency: input.frequency,
141
+ amount_typical: input.amount_typical ?? null,
142
+ currency: input.currency,
143
+ transaction_ids: input.transaction_ids || [],
144
+ notes: input.notes ?? null,
145
+ });
146
+ return `Recorded recurrence ${id} ("${sanitizeForPrompt(input.description)}", ${input.frequency}); linked ${(input.transaction_ids || []).length} transaction(s).`;
147
+ }
148
+ catch (err) {
149
+ return `Could not record recurrence: ${err.message}`;
150
+ }
153
151
  }
154
152
  case "link_transaction_to_recurrence": {
155
- linkTransactionToRecurrence(db, input.transaction_id, input.recurrence_id);
156
- return `Linked transaction ${input.transaction_id} → recurrence ${input.recurrence_id}.`;
153
+ try {
154
+ linkTransactionToRecurrence(db, input.transaction_id, input.recurrence_id);
155
+ return `Linked transaction ${input.transaction_id} → recurrence ${input.recurrence_id}.`;
156
+ }
157
+ catch (err) {
158
+ return `Could not link: ${err.message}`;
159
+ }
157
160
  }
158
161
  case "merge_accounts": {
159
162
  const moved = mergeAccounts(db, input.from_id, input.to_id);
@@ -19,7 +19,6 @@ async function execute(_db, name, input, ctx) {
19
19
  if (name !== "mark_file_scanned")
20
20
  return undefined;
21
21
  const summary = input.summary || "";
22
- ctx?.buffer?.markDone(summary);
23
22
  ctx?.onComplete?.(summary);
24
23
  return `Marked file as scanned. Summary: ${sanitizeForPrompt(summary)}`;
25
24
  }
@@ -1,9 +1,10 @@
1
1
  import type Database from "libsql";
2
2
  import type { ToolDefinition } from "../provider.js";
3
- import type { BufferedWriteContext } from "../../scanner/buffer.js";
4
- export type ToolProfile = "scan" | "chat" | "resolve" | "record";
3
+ import type { ScanProgress } from "../../scanner/progress.js";
4
+ import type { ClosedQuestion } from "../../db/queries/questions.js";
5
+ export type ToolProfile = "scan" | "chat" | "record" | "resolve";
5
6
  /**
6
- * Structured highlights the resolve agent can pass to ask_user. The prompter
7
+ * Structured highlights an interactive agent can pass to ask_user. The prompter
7
8
  * renders them as a single colored header line above the question (each
8
9
  * category gets its own chalk color), so the user can scan amount / date /
9
10
  * merchant / accounts at a glance without parsing prose.
@@ -15,30 +16,22 @@ export interface PromptUserFacts {
15
16
  accounts?: string[];
16
17
  }
17
18
  export interface AgentExecutionContext {
18
- /** Set during scan so `record_transaction` can stamp `source_file_id`. */
19
+ /** Set during scan so writes can be stamped with `source_file_id`. */
19
20
  fileId?: string;
20
21
  /** When false, ask_user returns a marker and the caller halts after the run. */
21
22
  interactive: boolean;
22
23
  /** Synchronously prompt the user (only invoked when interactive === true). */
23
24
  promptUser?: (prompt: string, options?: string[], facts?: PromptUserFacts) => Promise<string>;
24
- /** Called when the model declares the session is done (scan or resolve). */
25
+ /** Called when the model declares the session is done (scan or record). */
25
26
  onComplete?: (summary: string) => void;
26
- /**
27
- * Which top-level command this agent serves. Mutating tools branch on this
28
- * to decide whether to append an action_log row (currently only "record").
29
- */
30
- command?: "scan" | "resolve" | "record";
31
- /** Per-invocation id grouping every action_log row from one CLI run. */
32
- correlationId?: string;
33
- /** The raw user utterance / file path that started this invocation. */
34
- userInput?: string;
35
- /**
36
- * Scan-only: when set, transactions and unknowns are queued here instead of
37
- * being written directly to the DB. Account and merchant writes still hit
38
- * the DB eagerly (serialized via their own mutexes) so concurrent scan
39
- * agents share the same chart of accounts and merchant directory.
40
- */
41
- buffer?: BufferedWriteContext;
27
+ /** Scan-only: tag questions inserted during this scan run. */
28
+ scanId?: string;
29
+ /** Scan-only: per-chunk progress sink for dashboard ticks. */
30
+ progress?: ScanProgress;
31
+ /** Scan-only: the chunk this agent invocation is processing. */
32
+ chunkId?: string;
33
+ /** Resolve-only: notified for each closed question so the caller can synthesize memory rules. */
34
+ onQuestionClosed?: (closed: ClosedQuestion) => void;
42
35
  }
43
36
  /**
44
37
  * A tool module owns a slice of tool definitions, the spinner labels that go
@@ -1,10 +1,7 @@
1
1
  import chalk from "chalk";
2
- import { randomUUID } from "crypto";
3
2
  import { getDb } from "../../db/connection.js";
4
3
  import { runRecordAgent } from "../../ai/agent.js";
5
4
  import { makePromptUser, makeAgentOnProgress, statusSpinner } from "../ux.js";
6
- import { listActions } from "../../db/queries/action-log.js";
7
- import { formatAmount } from "../../currency.js";
8
5
  export async function runRecordCommand(opts) {
9
6
  const utterance = opts.utterance.trim();
10
7
  if (!utterance) {
@@ -16,7 +13,6 @@ export async function runRecordCommand(opts) {
16
13
  const spinner = statusSpinner("Thinking...");
17
14
  const promptUser = makePromptUser(spinner);
18
15
  const onProgress = makeAgentOnProgress(spinner);
19
- const correlationId = `cr:${randomUUID()}`;
20
16
  const initialMessages = [
21
17
  { role: "user", content: utterance },
22
18
  ];
@@ -26,9 +22,6 @@ export async function runRecordCommand(opts) {
26
22
  initialMessages,
27
23
  prompt: { utterance },
28
24
  agentCtx: {
29
- command: "record",
30
- correlationId,
31
- userInput: utterance,
32
25
  interactive: !!process.stdout.isTTY,
33
26
  promptUser,
34
27
  },
@@ -39,83 +32,9 @@ export async function runRecordCommand(opts) {
39
32
  console.log("");
40
33
  console.log(text);
41
34
  }
42
- renderActionSummary(correlationId);
43
35
  }
44
36
  catch (err) {
45
- spinner.fail(err?.message ?? "Record failed.");
37
+ spinner.fail(err instanceof Error ? err.message : "Record failed.");
46
38
  process.exitCode = 1;
47
39
  }
48
40
  }
49
- function renderActionSummary(correlationId) {
50
- const actions = listActions(getDb(), { correlationId });
51
- if (actions.length === 0)
52
- return;
53
- console.log("");
54
- console.log(chalk.dim(`Logged ${actions.length} action${actions.length === 1 ? "" : "s"} (${correlationId}):`));
55
- for (const a of actions) {
56
- console.log(chalk.dim(` · ${describeAction(a)}`));
57
- }
58
- }
59
- function describeAction(a) {
60
- const payload = safeJson(a.payload_json);
61
- switch (a.action_type) {
62
- case "create_account": {
63
- const name = payload?.row?.name ? ` — ${payload.row.name}` : "";
64
- return `create_account ${a.target_id}${name}`;
65
- }
66
- case "update_account_metadata": {
67
- const fields = payload?.after && typeof payload.after === "object"
68
- ? Object.keys(payload.after).join(", ")
69
- : "";
70
- return `update_account_metadata ${a.target_id}${fields ? ` — ${fields}` : ""}`;
71
- }
72
- case "record_transaction": {
73
- const date = payload?.transaction?.date ?? "";
74
- const desc = payload?.transaction?.description ?? "";
75
- const total = totalDebit(payload?.postings);
76
- const amount = total != null
77
- ? ` ${formatTotal(total, currencyOf(payload?.postings))}`
78
- : "";
79
- return `record_transaction ${a.target_id} — ${[date, desc].filter(Boolean).join(" ")}${amount}`;
80
- }
81
- case "adjust_balance": {
82
- const before = payload?.before_balance;
83
- const after = payload?.after_balance;
84
- const currency = currencyOf(payload?.postings);
85
- if (typeof before === "number" && typeof after === "number") {
86
- return `adjust_balance ${payload?.account_id ?? a.target_id} — ${formatTotal(before, currency)} → ${formatTotal(after, currency)}`;
87
- }
88
- return `adjust_balance ${a.target_id}`;
89
- }
90
- case "create_merchant": {
91
- const name = payload?.canonical_name ?? "";
92
- return `create_merchant ${a.target_id}${name ? ` — ${name}` : ""}`;
93
- }
94
- case "update_merchant_default": {
95
- return `update_merchant_default ${a.target_id} — ${payload?.before ?? "(none)"} → ${payload?.after ?? "(none)"}`;
96
- }
97
- default:
98
- return `${a.action_type} ${a.target_id}`;
99
- }
100
- }
101
- function safeJson(s) {
102
- try {
103
- return JSON.parse(s);
104
- }
105
- catch {
106
- return null;
107
- }
108
- }
109
- function totalDebit(postings) {
110
- if (!Array.isArray(postings))
111
- return null;
112
- return postings.reduce((sum, p) => sum + (Number(p?.debit) || 0), 0);
113
- }
114
- function currencyOf(postings) {
115
- if (Array.isArray(postings) && postings[0]?.currency)
116
- return String(postings[0].currency);
117
- return "THB";
118
- }
119
- function formatTotal(amount, currency) {
120
- return formatAmount(amount, currency);
121
- }
@@ -1,2 +1,5 @@
1
- import { type ResolveOptions } from "../../resolver/pipeline.js";
2
- export declare function runResolveCommand(opts: ResolveOptions): Promise<void>;
1
+ /**
2
+ * Zero-arg resolver. Hands every question to the resolver (deterministic
3
+ * passes first, then the LLM agent) and prints a colored summary on completion.
4
+ */
5
+ export declare function runResolveCommand(): Promise<void>;
@@ -1,13 +1,44 @@
1
1
  import chalk from "chalk";
2
- import { runResolve } from "../../resolver/pipeline.js";
3
- export async function runResolveCommand(opts) {
2
+ import { getDb } from "../../db/connection.js";
3
+ import { runResolve } from "../../scanner/resolver.js";
4
+ import { makePromptUser, makeAgentOnProgress, statusSpinner } from "../ux.js";
5
+ /**
6
+ * Zero-arg resolver. Hands every question to the resolver (deterministic
7
+ * passes first, then the LLM agent) and prints a colored summary on completion.
8
+ */
9
+ export async function runResolveCommand() {
10
+ const db = getDb();
11
+ const spinner = statusSpinner("Resolving...");
12
+ const promptUser = makePromptUser(spinner);
13
+ const onProgress = makeAgentOnProgress(spinner);
4
14
  try {
5
- const summary = await runResolve(opts);
15
+ const summary = await runResolve({
16
+ db,
17
+ interactive: !!process.stdout.isTTY,
18
+ promptUser,
19
+ onProgress,
20
+ });
21
+ spinner.succeed("Resolve done.");
6
22
  console.log("");
7
- console.log(chalk.bold(summary));
23
+ console.log(formatSummary(summary));
8
24
  }
9
25
  catch (err) {
10
- console.error(chalk.red(`Resolve failed: ${err.message}`));
26
+ spinner.fail(err instanceof Error ? err.message : "Resolve failed.");
11
27
  process.exitCode = 1;
12
28
  }
13
29
  }
30
+ function formatSummary(summary) {
31
+ if (summary.total === 0) {
32
+ return chalk.dim("No questions.");
33
+ }
34
+ const tally = Object.entries(summary.tally)
35
+ .map(([k, v]) => `${k}×${v}`)
36
+ .join(", ");
37
+ const lines = [
38
+ chalk.bold(`Resolved ${summary.resolved}/${summary.total} questions${tally ? ` (${tally})` : ""}.`),
39
+ ];
40
+ if (summary.remaining > 0) {
41
+ lines.push(chalk.yellow(`${summary.remaining} question(s) remain.`));
42
+ }
43
+ return lines.join("\n");
44
+ }
@@ -3,7 +3,9 @@ import inquirer from "inquirer";
3
3
  import { relative, sep } from "path";
4
4
  import { getDb } from "../../db/connection.js";
5
5
  import { getDataDir } from "../../config.js";
6
- import { compileMatcher } from "../../scanner/pipeline.js";
6
+ function compileMatcher(input) {
7
+ return new RegExp(input, "i");
8
+ }
7
9
  function pathToRelPath(absolutePath) {
8
10
  return relative(getDataDir(), absolutePath).split(sep).join("/");
9
11
  }
@@ -43,7 +45,7 @@ export async function runRevertCommand(regex) {
43
45
  matches = findRevertMatches(getDb(), regex);
44
46
  }
45
47
  catch (err) {
46
- console.error(chalk.red(`Invalid regex: ${err.message}`));
48
+ console.error(chalk.red(`Invalid regex: ${err instanceof Error ? err.message : String(err)}`));
47
49
  process.exitCode = 1;
48
50
  return;
49
51
  }
@@ -29,7 +29,7 @@ export function renderRules(db) {
29
29
  const rules = collectRules(db);
30
30
  if (rules.length === 0) {
31
31
  return ("No rules yet.\n\n" +
32
- chalk.dim("Rules accumulate as you resolve unknowns. Run `plasalid resolve` after a scan."));
32
+ chalk.dim("Rules accumulate as you resolve questions. Run `plasalid resolve` after a scan."));
33
33
  }
34
34
  const width = Math.max(...rules.map((r) => r.displayId.length));
35
35
  const lines = [chalk.bold(`Rules (${rules.length}):`)];
@@ -46,7 +46,7 @@ export function forgetRules(db, pattern) {
46
46
  re = new RegExp(`^${pattern}$`);
47
47
  }
48
48
  catch (err) {
49
- return { ok: false, error: `Invalid regex /${pattern}/: ${err.message}` };
49
+ return { ok: false, error: `Invalid regex /${pattern}/: ${err instanceof Error ? err.message : String(err)}` };
50
50
  }
51
51
  const snapshot = collectRules(db);
52
52
  const hits = snapshot.filter((r) => re.test(r.displayId));