plasalid 0.6.10 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +4 -7
  2. package/dist/accounts/taxonomy.d.ts +0 -23
  3. package/dist/accounts/taxonomy.js +15 -15
  4. package/dist/ai/agent.d.ts +4 -4
  5. package/dist/ai/agent.js +9 -8
  6. package/dist/ai/context.d.ts +0 -2
  7. package/dist/ai/context.js +2 -2
  8. package/dist/ai/memory.d.ts +1 -0
  9. package/dist/ai/memory.js +4 -0
  10. package/dist/ai/personas.js +3 -6
  11. package/dist/ai/provider.d.ts +1 -0
  12. package/dist/ai/thinking.d.ts +0 -6
  13. package/dist/ai/thinking.js +29 -4
  14. package/dist/ai/tools/index.d.ts +5 -1
  15. package/dist/ai/tools/index.js +21 -15
  16. package/dist/ai/tools/ingest.js +94 -110
  17. package/dist/ai/tools/resolve.js +15 -44
  18. package/dist/cli/commands/accounts.d.ts +4 -1
  19. package/dist/cli/commands/accounts.js +39 -20
  20. package/dist/cli/commands/scan.js +47 -47
  21. package/dist/cli/commands/status.js +81 -14
  22. package/dist/cli/commands/transactions.d.ts +3 -1
  23. package/dist/cli/commands/transactions.js +37 -34
  24. package/dist/cli/format.d.ts +0 -1
  25. package/dist/cli/format.js +1 -1
  26. package/dist/cli/helper.d.ts +11 -0
  27. package/dist/cli/helper.js +24 -0
  28. package/dist/cli/index.js +14 -10
  29. package/dist/cli/ink/AccountsBrowser.d.ts +7 -0
  30. package/dist/cli/ink/AccountsBrowser.js +149 -0
  31. package/dist/cli/ink/ListBrowser.d.ts +38 -0
  32. package/dist/cli/ink/ListBrowser.js +154 -0
  33. package/dist/cli/ink/TransactionsBrowser.d.ts +6 -0
  34. package/dist/cli/ink/TransactionsBrowser.js +87 -0
  35. package/dist/cli/ink/hooks/useFooterText.js +31 -14
  36. package/dist/cli/ink/runBrowser.d.ts +7 -0
  37. package/dist/cli/ink/runBrowser.js +24 -0
  38. package/dist/cli/ux.d.ts +4 -5
  39. package/dist/cli/ux.js +87 -66
  40. package/dist/db/connection.d.ts +0 -2
  41. package/dist/db/connection.js +0 -5
  42. package/dist/db/queries/files.d.ts +11 -0
  43. package/dist/db/queries/files.js +16 -0
  44. package/dist/db/queries/recurrences.d.ts +7 -0
  45. package/dist/db/queries/recurrences.js +21 -0
  46. package/dist/db/queries/transactions.d.ts +28 -4
  47. package/dist/db/queries/transactions.js +68 -15
  48. package/dist/db/queries/unknowns.d.ts +3 -5
  49. package/dist/db/queries/unknowns.js +4 -4
  50. package/dist/db/schema.js +8 -0
  51. package/dist/lib/runPasses.d.ts +30 -0
  52. package/dist/lib/runPasses.js +15 -0
  53. package/dist/resolver/pipeline.d.ts +6 -6
  54. package/dist/resolver/pipeline.js +50 -22
  55. package/dist/scanner/inspectors/similarities.js +14 -16
  56. package/package.json +2 -2
@@ -178,6 +178,32 @@ const ACCOUNT_LABELS = {
178
178
  update_account_metadata: "Updating account metadata",
179
179
  record_transaction: "Posting transaction",
180
180
  };
181
+ /**
182
+ * Run a write inside an audit-wrapping transaction. When the caller has a
183
+ * correlation id, the write + action_log insert land atomically; otherwise
184
+ * it's just the write. The write closure can return an AuditRecord (logged)
185
+ * or null (no audit row this call — used when an update was a no-op).
186
+ */
187
+ function writeWithAudit(db, ctx, write) {
188
+ if (!ctx?.correlationId) {
189
+ write();
190
+ return;
191
+ }
192
+ const op = db.transaction(() => {
193
+ const audit = write();
194
+ if (!audit)
195
+ return;
196
+ appendAction(db, {
197
+ correlation_id: ctx.correlationId,
198
+ command: ctx.command ?? "record",
199
+ user_input: ctx.userInput ?? null,
200
+ action_type: audit.actionType,
201
+ target_id: audit.targetId,
202
+ payload: audit.payload,
203
+ });
204
+ });
205
+ op();
206
+ }
181
207
  async function accountExecute(db, name, input, ctx) {
182
208
  switch (name) {
183
209
  case "create_account": {
@@ -186,7 +212,7 @@ async function accountExecute(db, name, input, ctx) {
186
212
  }
187
213
  return await runAccountExclusive(() => {
188
214
  try {
189
- const create = () => {
215
+ writeWithAudit(db, ctx, () => {
190
216
  createAccount(db, {
191
217
  id: input.id,
192
218
  name: input.name,
@@ -200,25 +226,12 @@ async function accountExecute(db, name, input, ctx) {
200
226
  statement_day: input.statement_day ?? null,
201
227
  metadata: input.metadata ?? null,
202
228
  });
203
- };
204
- if (ctx?.correlationId) {
205
- const tx = db.transaction(() => {
206
- create();
207
- const row = findAccountById(db, input.id);
208
- appendAction(db, {
209
- correlation_id: ctx.correlationId,
210
- command: ctx.command ?? "record",
211
- user_input: ctx.userInput ?? null,
212
- action_type: "create_account",
213
- target_id: input.id,
214
- payload: { row },
215
- });
216
- });
217
- tx();
218
- }
219
- else {
220
- create();
221
- }
229
+ return {
230
+ actionType: "create_account",
231
+ targetId: input.id,
232
+ payload: { row: findAccountById(db, input.id) },
233
+ };
234
+ });
222
235
  return `Account created: ${input.id} (${input.name}, ${input.type}).`;
223
236
  }
224
237
  catch (err) {
@@ -233,7 +246,7 @@ async function accountExecute(db, name, input, ctx) {
233
246
  return await runAccountExclusive(() => {
234
247
  try {
235
248
  let changed = false;
236
- const apply = () => {
249
+ writeWithAudit(db, ctx, () => {
237
250
  const result = updateAccountMetadata(db, input.account_id, {
238
251
  due_day: input.due_day,
239
252
  statement_day: input.statement_day,
@@ -243,30 +256,15 @@ async function accountExecute(db, name, input, ctx) {
243
256
  metadata: input.metadata,
244
257
  });
245
258
  changed = result.changed;
246
- return result;
247
- };
248
- if (ctx?.correlationId) {
249
- const tx = db.transaction(() => {
250
- const result = apply();
251
- if (!result.changed)
252
- return;
253
- appendAction(db, {
254
- correlation_id: ctx.correlationId,
255
- command: ctx.command ?? "record",
256
- user_input: ctx.userInput ?? null,
257
- action_type: "update_account_metadata",
258
- target_id: input.account_id,
259
- payload: { before: result.before, after: result.after },
260
- });
261
- });
262
- tx();
263
- }
264
- else {
265
- apply();
266
- }
267
- return changed
268
- ? `Updated ${input.account_id}.`
269
- : "Nothing to update.";
259
+ if (!result.changed)
260
+ return null;
261
+ return {
262
+ actionType: "update_account_metadata",
263
+ targetId: input.account_id,
264
+ payload: { before: result.before, after: result.after },
265
+ };
266
+ });
267
+ return changed ? `Updated ${input.account_id}.` : "Nothing to update.";
270
268
  }
271
269
  catch (err) {
272
270
  if (String(err.message).includes("not found")) {
@@ -296,38 +294,35 @@ async function accountExecute(db, name, input, ctx) {
296
294
  })),
297
295
  };
298
296
  try {
299
- let transactionId;
300
297
  if (ctx.buffer) {
301
- transactionId = ctx.buffer.appendTransaction(txInput);
298
+ const transactionId = ctx.buffer.appendTransaction(txInput);
299
+ return `Posted transaction ${transactionId} (${input.date}).`;
302
300
  }
303
- else if (ctx.correlationId) {
304
- const validated = validateTransaction(txInput);
305
- const tx = db.transaction(() => {
306
- insertTransactionRows(db, validated);
307
- appendAction(db, {
308
- correlation_id: ctx.correlationId,
309
- command: ctx.command ?? "record",
310
- user_input: ctx.userInput ?? null,
311
- action_type: "record_transaction",
312
- target_id: validated.id,
313
- payload: {
314
- transaction: {
315
- date: validated.date,
316
- description: validated.description,
317
- source_page: validated.source_page ?? null,
318
- raw_descriptor: validated.raw_descriptor ?? null,
319
- },
320
- postings: validated.postings,
321
- },
322
- });
323
- });
324
- tx();
325
- transactionId = validated.id;
326
- }
327
- else {
328
- transactionId = recordTransaction(db, txInput);
301
+ // No-audit path uses recordTransaction (validates + inserts in one go).
302
+ // Audit path validates ahead so the validated id can be returned without
303
+ // re-reading from disk after the transaction commits.
304
+ if (!ctx.correlationId) {
305
+ const transactionId = recordTransaction(db, txInput);
306
+ return `Posted transaction ${transactionId} (${input.date}).`;
329
307
  }
330
- return `Posted transaction ${transactionId} (${input.date}).`;
308
+ const validated = validateTransaction(txInput);
309
+ writeWithAudit(db, ctx, () => {
310
+ insertTransactionRows(db, validated);
311
+ return {
312
+ actionType: "record_transaction",
313
+ targetId: validated.id,
314
+ payload: {
315
+ transaction: {
316
+ date: validated.date,
317
+ description: validated.description,
318
+ source_page: validated.source_page ?? null,
319
+ raw_descriptor: validated.raw_descriptor ?? null,
320
+ },
321
+ postings: validated.postings,
322
+ },
323
+ };
324
+ });
325
+ return `Posted transaction ${validated.id} (${input.date}).`;
331
326
  }
332
327
  catch (err) {
333
328
  return `Could not post transaction: ${err.message}`;
@@ -511,40 +506,22 @@ async function resolveExecute(db, name, input, ctx) {
511
506
  return closeUnknown(db, input);
512
507
  if (name !== "ask_user")
513
508
  return undefined;
514
- if (!ctx)
515
- return "ask_user is only available inside an agent session.";
516
- let id;
517
- if (input.unknown_id) {
518
- id = String(input.unknown_id);
519
- if (!getUnknownTarget(db, id))
520
- return `Unknown ${id} not found.`;
509
+ if (!ctx?.promptUser) {
510
+ return "ask_user requires an interactive resolve session.";
521
511
  }
522
- else {
523
- id = recordUnknown(db, {
512
+ const id = input.unknown_id
513
+ ? String(input.unknown_id)
514
+ : recordUnknown(db, {
524
515
  file_id: ctx.fileId ?? null,
525
516
  transaction_id: input.transaction_id ?? null,
526
517
  account_id: input.account_id ?? null,
527
518
  prompt: input.prompt,
528
519
  options: input.options,
529
520
  });
530
- }
531
- if (ctx.interactive && ctx.promptUser) {
532
- const answer = await ctx.promptUser(input.prompt, input.options, input.facts);
533
- resolveUnknown(db, id, answer);
534
- const siblings = Array.isArray(input.related_unknown_ids)
535
- ? input.related_unknown_ids
536
- : [];
537
- let propagated = 0;
538
- for (const sibId of siblings) {
539
- if (sibId === id)
540
- continue;
541
- if (resolveUnknown(db, String(sibId), answer))
542
- propagated++;
543
- }
544
- const totalResolved = 1 + propagated;
545
- return `User answered: ${sanitizeForPrompt(answer)}${totalResolved > 1 ? ` (applied to ${totalResolved} unknown${totalResolved === 1 ? "" : "s"})` : ""}`;
546
- }
547
- return `Question recorded for later (${id}). Awaiting user input — do not act on assumptions about this answer.`;
521
+ if (!getUnknownTarget(db, id))
522
+ return `Unknown ${id} not found.`;
523
+ const answer = await ctx.promptUser(input.prompt, input.options, input.facts);
524
+ return applyAnswerToGroup(db, id, answer, input.related_unknown_ids);
548
525
  }
549
526
  function closeUnknown(db, input) {
550
527
  const primary = String(input.unknown_id ?? "");
@@ -553,18 +530,25 @@ function closeUnknown(db, input) {
553
530
  return "close_unknown requires unknown_id and answer.";
554
531
  if (!getUnknownTarget(db, primary))
555
532
  return `Unknown ${primary} not found.`;
556
- resolveUnknown(db, primary, answer);
557
- let count = 1;
558
- const siblings = Array.isArray(input.related_unknown_ids)
559
- ? input.related_unknown_ids
560
- : [];
533
+ return applyAnswerToGroup(db, primary, answer, input.related_unknown_ids);
534
+ }
535
+ function applyAnswerToGroup(db, primaryId, answer, rawSiblings) {
536
+ resolveUnknown(db, primaryId, answer);
537
+ const siblings = Array.isArray(rawSiblings) ? rawSiblings.map(String) : [];
538
+ const resolved = [primaryId];
539
+ const notFound = [];
561
540
  for (const sibId of siblings) {
562
- if (sibId === primary)
541
+ if (sibId === primaryId)
563
542
  continue;
564
- if (resolveUnknown(db, String(sibId), answer))
565
- count++;
543
+ if (resolveUnknown(db, sibId, answer))
544
+ resolved.push(sibId);
545
+ else
546
+ notFound.push(sibId);
566
547
  }
567
- return `Closed ${count} unknown${count === 1 ? "" : "s"}.`;
548
+ const preface = `Resolved ${resolved.length} unknown${resolved.length === 1 ? "" : "s"} with: ${sanitizeForPrompt(answer)}`;
549
+ if (notFound.length === 0)
550
+ return preface;
551
+ return `${preface}. NOT FOUND: ${notFound.join(", ")} — these ids did not exist; do not re-close them.`;
568
552
  }
569
553
  export const resolveIngestTools = {
570
554
  DEFS: RESOLVE_DEFS,
@@ -103,15 +103,6 @@ const DEFS = [
103
103
  required: ["from_id", "to_id"],
104
104
  },
105
105
  },
106
- {
107
- name: "mark_resolve_done",
108
- description: "Call once the current unknown's resolution has been applied. The summary is shown to the user. The pipeline will then mark the unknown resolved and move to the next one.",
109
- input_schema: {
110
- type: "object",
111
- properties: { summary: { type: "string" } },
112
- required: ["summary"],
113
- },
114
- },
115
106
  ];
116
107
  const LABELS = {
117
108
  update_transaction: "Updating transaction",
@@ -120,9 +111,8 @@ const LABELS = {
120
111
  record_recurrence: "Recording recurrence",
121
112
  link_transaction_to_recurrence: "Linking transaction to recurrence",
122
113
  merge_accounts: "Merging accounts",
123
- mark_resolve_done: "Finalizing unknown",
124
114
  };
125
- async function execute(db, name, input, ctx) {
115
+ async function execute(db, name, input) {
126
116
  switch (name) {
127
117
  case "update_transaction": {
128
118
  const changed = updateTransaction(db, input.transaction_id, {
@@ -150,43 +140,24 @@ async function execute(db, name, input, ctx) {
150
140
  : `Deleted transaction ${input.transaction_id} and its postings.`;
151
141
  }
152
142
  case "record_recurrence": {
153
- try {
154
- const id = recordRecurrence(db, {
155
- account_id: input.account_id,
156
- description: input.description,
157
- frequency: input.frequency,
158
- amount_typical: input.amount_typical ?? null,
159
- currency: input.currency,
160
- transaction_ids: input.transaction_ids || [],
161
- notes: input.notes ?? null,
162
- });
163
- return `Recorded recurrence ${id} ("${sanitizeForPrompt(input.description)}", ${input.frequency}); linked ${(input.transaction_ids || []).length} transaction(s).`;
164
- }
165
- catch (err) {
166
- return `Could not record recurrence: ${err.message}`;
167
- }
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).`;
168
153
  }
169
154
  case "link_transaction_to_recurrence": {
170
- try {
171
- linkTransactionToRecurrence(db, input.transaction_id, input.recurrence_id);
172
- return `Linked transaction ${input.transaction_id} → recurrence ${input.recurrence_id}.`;
173
- }
174
- catch (err) {
175
- return `Could not link: ${err.message}`;
176
- }
155
+ linkTransactionToRecurrence(db, input.transaction_id, input.recurrence_id);
156
+ return `Linked transaction ${input.transaction_id} → recurrence ${input.recurrence_id}.`;
177
157
  }
178
158
  case "merge_accounts": {
179
- try {
180
- const moved = mergeAccounts(db, input.from_id, input.to_id);
181
- return `Merged ${input.from_id} → ${input.to_id}; moved ${moved} posting(s).`;
182
- }
183
- catch (err) {
184
- return `Could not merge: ${err.message}`;
185
- }
186
- }
187
- case "mark_resolve_done": {
188
- ctx?.onComplete?.(input.summary || "");
189
- return `Unknown done. Summary: ${sanitizeForPrompt(input.summary || "")}`;
159
+ const moved = mergeAccounts(db, input.from_id, input.to_id);
160
+ return `Merged ${input.from_id} → ${input.to_id}; moved ${moved} posting(s).`;
190
161
  }
191
162
  default:
192
163
  return undefined;
@@ -1 +1,4 @@
1
- export declare function showAccounts(): void;
1
+ export interface ShowAccountsOptions {
2
+ noInteractive?: boolean;
3
+ }
4
+ export declare function showAccounts(opts?: ShowAccountsOptions): Promise<void>;
@@ -18,30 +18,33 @@ const TYPE_RANK = {
18
18
  expense: 3,
19
19
  equity: 4,
20
20
  };
21
- function compactMeta(a) {
22
- const meta = [];
23
- if (a.bank_name)
24
- meta.push(a.bank_name);
25
- if (a.due_day)
26
- meta.push(`due ${a.due_day}`);
27
- if (a.statement_day)
28
- meta.push(`stmt ${a.statement_day}`);
29
- if (a.points_balance)
30
- meta.push(`${a.points_balance.toLocaleString()} pts`);
31
- if (a.currency && a.currency !== "THB")
32
- meta.push(a.currency);
33
- // Subtype only when there's no other signal yet (e.g. "cash", "salary").
34
- if (meta.length === 0 && a.subtype)
35
- meta.push(a.subtype);
36
- return meta;
37
- }
38
- export function showAccounts() {
21
+ export async function showAccounts(opts = {}) {
39
22
  const db = getDb();
40
- const raw = getAccountBalances(db);
41
- if (raw.length === 0) {
23
+ const accounts = getAccountBalances(db);
24
+ if (accounts.length === 0) {
42
25
  console.log(chalk.yellow("No accounts yet. Drop your bank/credit card statements into ~/.plasalid/data/ and run `plasalid scan`."));
43
26
  return;
44
27
  }
28
+ const interactive = !opts.noInteractive && Boolean(process.stdout.isTTY) && Boolean(process.stdin.isTTY);
29
+ if (interactive) {
30
+ const [{ runBrowser }, { AccountsBrowser }, { createElement }, { listPostings },] = await Promise.all([
31
+ import("../ink/runBrowser.js"),
32
+ import("../ink/AccountsBrowser.js"),
33
+ import("react"),
34
+ import("../../db/queries/transactions.js"),
35
+ ]);
36
+ const recentTransactionsByAccount = new Map();
37
+ for (const a of accounts) {
38
+ const rows = listPostings(db, { account_id: a.id, limit: 10 });
39
+ if (rows.length > 0)
40
+ recentTransactionsByAccount.set(a.id, rows);
41
+ }
42
+ await runBrowser(createElement(AccountsBrowser, { accounts, recentTransactionsByAccount }));
43
+ return;
44
+ }
45
+ printAccountsPlain(accounts);
46
+ }
47
+ function printAccountsPlain(raw) {
45
48
  const byId = new Map(raw.map((a) => [a.id, a]));
46
49
  const depthCache = new Map();
47
50
  const depthOf = (id) => {
@@ -92,3 +95,19 @@ export function showAccounts() {
92
95
  chalk.dim(" · ") +
93
96
  chalk.bold(`Net worth ${formatSignedAmount(netWorth)}`));
94
97
  }
98
+ function compactMeta(a) {
99
+ const meta = [];
100
+ if (a.bank_name)
101
+ meta.push(a.bank_name);
102
+ if (a.due_day)
103
+ meta.push(`due ${a.due_day}`);
104
+ if (a.statement_day)
105
+ meta.push(`stmt ${a.statement_day}`);
106
+ if (a.points_balance)
107
+ meta.push(`${a.points_balance.toLocaleString()} pts`);
108
+ if (a.currency && a.currency !== "THB")
109
+ meta.push(a.currency);
110
+ if (meta.length === 0 && a.subtype)
111
+ meta.push(a.subtype);
112
+ return meta;
113
+ }
@@ -11,8 +11,9 @@ export async function runScanCommand(opts) {
11
11
  return;
12
12
  }
13
13
  }
14
- const useInk = !!process.stdout.isTTY;
15
- const events = useInk ? await buildInkEvents(opts.parallel ?? 3) : buildPlainTextEvents();
14
+ const events = process.stdout.isTTY
15
+ ? await inkScanEvents(opts.parallel ?? 3)
16
+ : plainScanEvents();
16
17
  const summary = await runScan({
17
18
  regex: opts.regex,
18
19
  force: opts.force,
@@ -28,24 +29,50 @@ function logDecryptProgress(e) {
28
29
  : chalk.red("✗");
29
30
  console.log(` ${marker} [${e.index + 1}/${e.total}] ${e.fileName} (${e.outcome})`);
30
31
  }
31
- /** Ink-based events (TTY mode) */
32
- async function buildInkEvents(parallel) {
33
- // Lazy-load ink + react so this module stays importable in non-TTY contexts
34
- // (and so test environments without React don't choke on the JSX).
35
- const { render } = await import("ink");
36
- const { createElement } = await import("react");
37
- const { ScanDashboard, ScanDashboardController } = await import("../ink/scan_dashboard.js");
38
- const controller = new ScanDashboardController();
39
- let inkInstance = null;
40
- let mountedFiles = 0;
32
+ /**
33
+ * Hooks every mode shares: the decrypt phase, commit notice, and inspector
34
+ * summary all render the same way in TTY and non-TTY runs. Each mode-specific
35
+ * factory below spreads this base and overrides the scan-phase hooks
36
+ * (`scanStart` / `scanProgress` / `scanEnd`) to render differently.
37
+ *
38
+ * Returns `Partial<ScanRunEvents>` because the scan-phase hooks are filled in
39
+ * by the caller — composition, not inheritance.
40
+ */
41
+ function baseScanEvents() {
42
+ let decryptTotal = 0;
41
43
  return {
42
44
  decryptStart: (count) => {
45
+ decryptTotal = count;
43
46
  if (count > 0)
44
47
  console.log(chalk.dim(`Decrypting ${count} file(s)...`));
45
48
  },
46
49
  decryptProgress: logDecryptProgress,
47
50
  decryptDone: (e) => {
51
+ if (decryptTotal === 0)
52
+ return;
48
53
  console.log(chalk.dim(`Decrypted ${e.decrypted}, skipped ${e.skipped}, failed ${e.failed}.`));
54
+ },
55
+ committing: () => { console.log(chalk.dim("Committing...")); },
56
+ inspecting: (r) => {
57
+ if (r.total > 0)
58
+ console.log(chalk.dim(`Inspectors flagged ${r.total} unknown(s).`));
59
+ },
60
+ };
61
+ }
62
+ /** TTY mode: mount the Ink dashboard during the scan phase. */
63
+ async function inkScanEvents(parallel) {
64
+ // Lazy-load ink + react so this module stays importable in non-TTY contexts.
65
+ const { render } = await import("ink");
66
+ const { createElement } = await import("react");
67
+ const { ScanDashboard, ScanDashboardController } = await import("../ink/scan_dashboard.js");
68
+ const controller = new ScanDashboardController();
69
+ let inkInstance = null;
70
+ let mountedFiles = 0;
71
+ const base = baseScanEvents();
72
+ return {
73
+ ...base,
74
+ decryptDone: (e) => {
75
+ base.decryptDone?.(e);
49
76
  console.log("");
50
77
  mountedFiles = e.decrypted;
51
78
  if (e.decrypted > 0) {
@@ -68,33 +95,16 @@ async function buildInkEvents(parallel) {
68
95
  inkInstance = null;
69
96
  }
70
97
  if (mountedFiles > 0)
71
- console.log(chalk.dim("Committing..."));
72
- },
73
- inspecting: (result) => {
74
- if (mountedFiles > 0 && result.total > 0) {
75
- console.log(chalk.dim(`Inspectors flagged ${result.total} unknown(s).`));
76
- }
98
+ base.committing?.();
77
99
  },
78
100
  };
79
101
  }
80
- /** Plain-text progress (non-TTY or fallback) */
81
- function buildPlainTextEvents() {
82
- let decryptTotal = 0;
102
+ /** Non-TTY mode: print one line per file as it progresses. */
103
+ function plainScanEvents() {
83
104
  // De-dupe scan-progress chatter: only print when the step text changes per file.
84
105
  const lastStepByFile = new Map();
85
106
  return {
86
- decryptStart: (count) => {
87
- decryptTotal = count;
88
- if (count > 0)
89
- console.log(chalk.dim(`Decrypting ${count} file(s)...`));
90
- },
91
- decryptProgress: logDecryptProgress,
92
- decryptDone: (e) => {
93
- if (decryptTotal === 0)
94
- return;
95
- console.log(chalk.dim(`Decrypted ${e.decrypted}, skipped ${e.skipped}, failed ${e.failed}.`));
96
- console.log("");
97
- },
107
+ ...baseScanEvents(),
98
108
  scanStart: (e) => {
99
109
  console.log(`${chalk.cyan("→")} ${e.fileName} ${chalk.dim("starting...")}`);
100
110
  },
@@ -106,20 +116,10 @@ function buildPlainTextEvents() {
106
116
  },
107
117
  scanEnd: (e) => {
108
118
  lastStepByFile.delete(e.fileName);
109
- if (e.status === "scanned") {
110
- console.log(`${chalk.green("✓")} ${e.fileName} ${chalk.dim(`(${e.transactions} transactions, ${e.unknowns} unknowns)`)}`);
111
- }
112
- else {
113
- console.log(`${chalk.red("✗")} ${e.fileName} ${chalk.dim(`— ${e.error ?? "failed"}`)}`);
114
- }
115
- },
116
- committing: () => {
117
- console.log(chalk.dim("Committing..."));
118
- },
119
- inspecting: (result) => {
120
- if (result.total > 0) {
121
- console.log(chalk.dim(`Inspectors flagged ${result.total} unknown(s).`));
122
- }
119
+ const line = e.status === "scanned"
120
+ ? `${chalk.green("✓")} ${e.fileName} ${chalk.dim(`(${e.transactions} transactions, ${e.unknowns} unknowns)`)}`
121
+ : `${chalk.red("✗")} ${e.fileName} ${chalk.dim(`— ${e.error ?? "failed"}`)}`;
122
+ console.log(line);
123
123
  },
124
124
  };
125
125
  }