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.
- package/README.md +4 -7
- package/dist/accounts/taxonomy.d.ts +0 -23
- package/dist/accounts/taxonomy.js +15 -15
- package/dist/ai/agent.d.ts +4 -4
- package/dist/ai/agent.js +9 -8
- package/dist/ai/context.d.ts +0 -2
- package/dist/ai/context.js +2 -2
- package/dist/ai/memory.d.ts +1 -0
- package/dist/ai/memory.js +4 -0
- package/dist/ai/personas.js +3 -6
- package/dist/ai/provider.d.ts +1 -0
- package/dist/ai/thinking.d.ts +0 -6
- package/dist/ai/thinking.js +29 -4
- package/dist/ai/tools/index.d.ts +5 -1
- package/dist/ai/tools/index.js +21 -15
- package/dist/ai/tools/ingest.js +94 -110
- package/dist/ai/tools/resolve.js +15 -44
- package/dist/cli/commands/accounts.d.ts +4 -1
- package/dist/cli/commands/accounts.js +39 -20
- package/dist/cli/commands/scan.js +47 -47
- package/dist/cli/commands/status.js +81 -14
- package/dist/cli/commands/transactions.d.ts +3 -1
- package/dist/cli/commands/transactions.js +37 -34
- package/dist/cli/format.d.ts +0 -1
- package/dist/cli/format.js +1 -1
- package/dist/cli/helper.d.ts +11 -0
- package/dist/cli/helper.js +24 -0
- package/dist/cli/index.js +14 -10
- package/dist/cli/ink/AccountsBrowser.d.ts +7 -0
- package/dist/cli/ink/AccountsBrowser.js +149 -0
- package/dist/cli/ink/ListBrowser.d.ts +38 -0
- package/dist/cli/ink/ListBrowser.js +154 -0
- package/dist/cli/ink/TransactionsBrowser.d.ts +6 -0
- package/dist/cli/ink/TransactionsBrowser.js +87 -0
- package/dist/cli/ink/hooks/useFooterText.js +31 -14
- package/dist/cli/ink/runBrowser.d.ts +7 -0
- package/dist/cli/ink/runBrowser.js +24 -0
- package/dist/cli/ux.d.ts +4 -5
- package/dist/cli/ux.js +87 -66
- package/dist/db/connection.d.ts +0 -2
- package/dist/db/connection.js +0 -5
- package/dist/db/queries/files.d.ts +11 -0
- package/dist/db/queries/files.js +16 -0
- package/dist/db/queries/recurrences.d.ts +7 -0
- package/dist/db/queries/recurrences.js +21 -0
- package/dist/db/queries/transactions.d.ts +28 -4
- package/dist/db/queries/transactions.js +68 -15
- package/dist/db/queries/unknowns.d.ts +3 -5
- package/dist/db/queries/unknowns.js +4 -4
- package/dist/db/schema.js +8 -0
- package/dist/lib/runPasses.d.ts +30 -0
- package/dist/lib/runPasses.js +15 -0
- package/dist/resolver/pipeline.d.ts +6 -6
- package/dist/resolver/pipeline.js +50 -22
- package/dist/scanner/inspectors/similarities.js +14 -16
- package/package.json +2 -2
package/dist/ai/tools/ingest.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
523
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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 ===
|
|
541
|
+
if (sibId === primaryId)
|
|
563
542
|
continue;
|
|
564
|
-
if (resolveUnknown(db,
|
|
565
|
-
|
|
543
|
+
if (resolveUnknown(db, sibId, answer))
|
|
544
|
+
resolved.push(sibId);
|
|
545
|
+
else
|
|
546
|
+
notFound.push(sibId);
|
|
566
547
|
}
|
|
567
|
-
|
|
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,
|
package/dist/ai/tools/resolve.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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;
|
|
@@ -18,30 +18,33 @@ const TYPE_RANK = {
|
|
|
18
18
|
expense: 3,
|
|
19
19
|
equity: 4,
|
|
20
20
|
};
|
|
21
|
-
function
|
|
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
|
|
41
|
-
if (
|
|
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
|
|
15
|
-
|
|
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
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
81
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
}
|