plasalid 0.7.1 → 0.7.3
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 +15 -15
- package/dist/accounts/taxonomy.d.ts +1 -1
- package/dist/accounts/taxonomy.js +1 -1
- package/dist/ai/agent.d.ts +9 -10
- package/dist/ai/agent.js +31 -15
- package/dist/ai/personas.d.ts +1 -1
- package/dist/ai/personas.js +57 -55
- package/dist/ai/prompt-sections.d.ts +4 -4
- package/dist/ai/prompt-sections.js +1 -1
- package/dist/ai/system-prompt.d.ts +2 -2
- package/dist/ai/system-prompt.js +5 -5
- package/dist/ai/tools/account-mutex.d.ts +1 -0
- package/dist/ai/tools/account-mutex.js +16 -0
- package/dist/ai/tools/clarify.d.ts +2 -0
- package/dist/ai/tools/clarify.js +169 -0
- package/dist/ai/tools/index.js +10 -18
- package/dist/ai/tools/ingest.d.ts +2 -2
- package/dist/ai/tools/ingest.js +284 -244
- package/dist/ai/tools/merchants.js +1 -28
- package/dist/ai/tools/read.js +8 -8
- package/dist/ai/tools/record.js +7 -40
- package/dist/ai/tools/resolve.js +25 -22
- package/dist/ai/tools/scan.js +0 -1
- package/dist/ai/tools/types.d.ts +14 -21
- package/dist/cli/commands/clarify.d.ts +5 -0
- package/dist/cli/commands/clarify.js +44 -0
- package/dist/cli/commands/record.js +1 -82
- package/dist/cli/commands/resolve.d.ts +5 -2
- package/dist/cli/commands/resolve.js +36 -5
- package/dist/cli/commands/revert.js +4 -2
- package/dist/cli/commands/rules.js +2 -2
- package/dist/cli/commands/scan.js +199 -128
- package/dist/cli/commands/status.js +6 -6
- package/dist/cli/index.js +8 -29
- package/dist/cli/ink/ScanDashboard.d.ts +49 -0
- package/dist/cli/ink/ScanDashboard.js +214 -0
- package/dist/cli/ink/scan_dashboard.d.ts +40 -25
- package/dist/cli/ink/scan_dashboard.js +139 -44
- package/dist/cli/setup.js +1 -1
- package/dist/cli/ux.js +1 -1
- package/dist/db/queries/account-balance.d.ts +1 -1
- package/dist/db/queries/questions.d.ts +62 -0
- package/dist/db/queries/questions.js +110 -0
- package/dist/db/queries/transactions.d.ts +1 -1
- package/dist/db/queries/unknowns.d.ts +17 -15
- package/dist/db/queries/unknowns.js +35 -39
- package/dist/db/schema.js +6 -28
- package/dist/scanner/audit/auditor.d.ts +31 -0
- package/dist/scanner/audit/auditor.js +72 -0
- package/dist/scanner/audit/engine.d.ts +10 -0
- package/dist/scanner/audit/engine.js +98 -0
- package/dist/scanner/audit/eventBus.d.ts +60 -0
- package/dist/scanner/audit/eventBus.js +35 -0
- package/dist/scanner/audit/passes/index.d.ts +11 -0
- package/dist/scanner/audit/passes/index.js +9 -0
- package/dist/scanner/audit/passes/types.d.ts +23 -0
- package/dist/scanner/audit/passes/types.js +1 -0
- package/dist/scanner/audit/types.d.ts +27 -0
- package/dist/scanner/audit/types.js +1 -0
- package/dist/scanner/auditor.d.ts +51 -0
- package/dist/scanner/auditor.js +80 -0
- package/dist/scanner/buffer/engine.d.ts +9 -0
- package/dist/scanner/buffer/engine.js +110 -0
- package/dist/scanner/buffer/sharedBuffer.d.ts +78 -0
- package/dist/scanner/buffer/sharedBuffer.js +130 -0
- package/dist/scanner/buffer/types.d.ts +67 -0
- package/dist/scanner/buffer/types.js +1 -0
- package/dist/scanner/buffer.d.ts +45 -38
- package/dist/scanner/buffer.js +93 -61
- package/dist/scanner/bus/engine.d.ts +11 -0
- package/dist/scanner/bus/engine.js +42 -0
- package/dist/scanner/bus/types.d.ts +53 -0
- package/dist/scanner/bus/types.js +1 -0
- package/dist/scanner/bus.d.ts +38 -0
- package/dist/scanner/bus.js +37 -0
- package/dist/scanner/chunk-worker.d.ts +19 -0
- package/dist/scanner/chunk-worker.js +67 -0
- package/dist/scanner/chunkWorker.d.ts +20 -0
- package/dist/scanner/chunkWorker.js +59 -0
- package/dist/scanner/chunker/chunker.d.ts +7 -0
- package/dist/scanner/chunker/chunker.js +60 -0
- package/dist/scanner/chunker.d.ts +7 -0
- package/dist/scanner/chunker.js +60 -0
- package/dist/scanner/clarifier-memory.d.ts +8 -0
- package/dist/scanner/clarifier-memory.js +24 -0
- package/dist/scanner/clarifier.d.ts +39 -0
- package/dist/scanner/clarifier.js +196 -0
- package/dist/scanner/converge.d.ts +29 -0
- package/dist/scanner/converge.js +15 -0
- package/dist/scanner/decrypt.d.ts +10 -0
- package/dist/scanner/decrypt.js +80 -0
- package/dist/scanner/engine/scanEngine.d.ts +24 -0
- package/dist/scanner/engine/scanEngine.js +87 -0
- package/dist/scanner/engine/types.d.ts +90 -0
- package/dist/scanner/engine/types.js +1 -0
- package/dist/scanner/engine.d.ts +90 -0
- package/dist/scanner/engine.js +84 -0
- package/dist/scanner/file-worker.d.ts +33 -0
- package/dist/scanner/file-worker.js +28 -0
- package/dist/scanner/fileWorker.d.ts +33 -0
- package/dist/scanner/fileWorker.js +22 -0
- package/dist/scanner/hooks/types.d.ts +25 -0
- package/dist/scanner/hooks/types.js +1 -0
- package/dist/scanner/hooks.d.ts +23 -0
- package/dist/scanner/hooks.js +1 -0
- package/dist/scanner/parse.d.ts +10 -0
- package/dist/scanner/parse.js +47 -0
- package/dist/scanner/passes/index.d.ts +8 -0
- package/dist/scanner/passes/index.js +6 -0
- package/dist/scanner/passes/types.d.ts +22 -0
- package/dist/scanner/passes/types.js +1 -0
- package/dist/scanner/pdf/chunker.d.ts +7 -0
- package/dist/scanner/pdf/chunker.js +60 -0
- package/dist/scanner/pdf/password-store.d.ts +34 -0
- package/dist/scanner/pdf/password-store.js +83 -0
- package/dist/scanner/pdf/pdf-unlock.d.ts +17 -0
- package/dist/scanner/pdf/pdf-unlock.js +50 -0
- package/dist/scanner/pdf/pdf.d.ts +17 -0
- package/dist/scanner/pdf/pdf.js +36 -0
- package/dist/scanner/pdf/state-machine.d.ts +60 -0
- package/dist/scanner/pdf/state-machine.js +64 -0
- package/dist/scanner/pdf/unlock.d.ts +22 -0
- package/dist/scanner/pdf/unlock.js +121 -0
- package/dist/scanner/phase-decrypt.d.ts +10 -0
- package/dist/scanner/phase-decrypt.js +80 -0
- package/dist/scanner/phase-parse.d.ts +10 -0
- package/dist/scanner/phase-parse.js +46 -0
- package/dist/scanner/phases/chunk.d.ts +8 -0
- package/dist/scanner/phases/chunk.js +13 -0
- package/dist/scanner/phases/commit.d.ts +12 -0
- package/dist/scanner/phases/commit.js +140 -0
- package/dist/scanner/phases/decrypt.d.ts +10 -0
- package/dist/scanner/phases/decrypt.js +80 -0
- package/dist/scanner/phases/parse.d.ts +10 -0
- package/dist/scanner/phases/parse.js +46 -0
- package/dist/scanner/phases/resolve.d.ts +10 -0
- package/dist/scanner/phases/resolve.js +17 -0
- package/dist/scanner/phases/review.d.ts +10 -0
- package/dist/scanner/phases/review.js +12 -0
- package/dist/scanner/progress.d.ts +14 -0
- package/dist/scanner/progress.js +21 -0
- package/dist/scanner/resolver-memory.d.ts +8 -0
- package/dist/scanner/resolver-memory.js +24 -0
- package/dist/scanner/resolver.d.ts +39 -0
- package/dist/scanner/resolver.js +196 -0
- package/dist/scanner/result.d.ts +17 -0
- package/dist/scanner/result.js +19 -0
- package/dist/scanner/run-passes.d.ts +30 -0
- package/dist/scanner/run-passes.js +15 -0
- package/dist/scanner/unlock.js +1 -1
- package/dist/scanner/worker.d.ts +19 -0
- package/dist/scanner/worker.js +67 -0
- package/dist/scanner/workers/chunkWorker.d.ts +20 -0
- package/dist/scanner/workers/chunkWorker.js +65 -0
- package/dist/scanner/workers/fileWorker.d.ts +32 -0
- package/dist/scanner/workers/fileWorker.js +22 -0
- package/package.json +1 -1
|
@@ -1,165 +1,236 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import {
|
|
2
|
+
import { getDb } from "../../db/connection.js";
|
|
3
|
+
import { runScan } from "../../scanner/engine.js";
|
|
3
4
|
export async function runScanCommand(opts) {
|
|
4
|
-
if (opts.regex
|
|
5
|
+
if (opts.regex) {
|
|
5
6
|
try {
|
|
6
7
|
new RegExp(opts.regex, "i");
|
|
7
8
|
}
|
|
8
9
|
catch (err) {
|
|
9
|
-
console.error(chalk.red(`Invalid regex: ${err.message}`));
|
|
10
|
+
console.error(chalk.red(`Invalid regex: ${err instanceof Error ? err.message : String(err)}`));
|
|
10
11
|
process.exitCode = 1;
|
|
11
12
|
return;
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
15
|
+
const parallel = opts.parallel ?? 5;
|
|
16
|
+
const isTTY = !!process.stdout.isTTY;
|
|
17
|
+
const hooks = isTTY ? await buildTtyHooks() : buildPlainHooks();
|
|
18
|
+
const result = await runScan(getDb(), {
|
|
18
19
|
regex: opts.regex,
|
|
19
20
|
force: opts.force,
|
|
20
21
|
interactive: true,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
renderScanSummary(summary);
|
|
22
|
+
maxFileWorkers: parallel,
|
|
23
|
+
}, hooks);
|
|
24
|
+
renderSummary(result.state);
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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;
|
|
26
|
+
/* TTY mode — Ink dashboard with one in-place row per file. */
|
|
27
|
+
async function buildTtyHooks() {
|
|
28
|
+
const { render } = await import("ink");
|
|
29
|
+
const { createElement } = await import("react");
|
|
30
|
+
const { ScanDashboard, createScanDashboardController } = await import("../ink/ScanDashboard.js");
|
|
31
|
+
const controller = createScanDashboardController();
|
|
32
|
+
let inkInstance = null;
|
|
33
|
+
let unsubscribeProgress = null;
|
|
34
|
+
const chunkLookup = new Map();
|
|
43
35
|
return {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
console.log(chalk.dim(
|
|
36
|
+
afterDecrypt: (s) => {
|
|
37
|
+
const total = s.decrypted.length + s.skipped.length + s.failed.length;
|
|
38
|
+
if (total === 0) {
|
|
39
|
+
console.log(chalk.dim("No files to scan."));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
console.log(chalk.dim(`Decrypted ${s.decrypted.length}, skipped ${s.skipped.length}, failed ${s.failed.length}.`));
|
|
43
|
+
},
|
|
44
|
+
afterChunk: (s) => {
|
|
45
|
+
if (s.chunks.length === 0)
|
|
46
|
+
return;
|
|
47
|
+
console.log(chalk.dim(`Chunked into ${s.chunks.length} page(s). Mounting dashboard…`));
|
|
48
48
|
},
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
beforeParse: (s) => {
|
|
50
|
+
for (const c of s.chunks)
|
|
51
|
+
chunkLookup.set(c.chunkId, {
|
|
52
|
+
fileId: c.fileId,
|
|
53
|
+
pageNumber: c.pageNumber,
|
|
54
|
+
});
|
|
55
|
+
if (s.decrypted.length === 0)
|
|
52
56
|
return;
|
|
53
|
-
|
|
57
|
+
process.stdout.write("\x1b[2J\x1b[H\x1b[?25l");
|
|
58
|
+
const files = s.decrypted.map((d) => ({
|
|
59
|
+
fileId: d.path,
|
|
60
|
+
fileName: d.fileName,
|
|
61
|
+
totalPages: s.chunks.filter((c) => c.fileId === d.path).length,
|
|
62
|
+
}));
|
|
63
|
+
inkInstance = render(createElement(ScanDashboard, {
|
|
64
|
+
controller,
|
|
65
|
+
files,
|
|
66
|
+
}), {
|
|
67
|
+
exitOnCtrlC: false,
|
|
68
|
+
patchConsole: false,
|
|
69
|
+
});
|
|
70
|
+
unsubscribeProgress = s.progress.subscribe((event) => {
|
|
71
|
+
const map = chunkLookup.get(event.chunkId);
|
|
72
|
+
if (!map)
|
|
73
|
+
return;
|
|
74
|
+
controller.publish({
|
|
75
|
+
type: event.kind === "tx" ? "chunk-tx" : "chunk-question",
|
|
76
|
+
fileId: map.fileId,
|
|
77
|
+
pageNumber: map.pageNumber,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
54
80
|
},
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
81
|
+
onWorkerStart: (_id, chunk) => {
|
|
82
|
+
controller.publish({
|
|
83
|
+
type: "chunk-start",
|
|
84
|
+
fileId: chunk.fileId,
|
|
85
|
+
fileName: chunk.fileName,
|
|
86
|
+
pageNumber: chunk.pageNumber,
|
|
87
|
+
totalPages: chunk.totalPages,
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
onWorkerEnd: (_id, chunk, ok) => {
|
|
91
|
+
controller.publish({
|
|
92
|
+
type: "chunk-end",
|
|
93
|
+
fileId: chunk.fileId,
|
|
94
|
+
pageNumber: chunk.pageNumber,
|
|
95
|
+
ok,
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
afterParse: () => {
|
|
99
|
+
unsubscribeProgress?.();
|
|
100
|
+
unsubscribeProgress = null;
|
|
101
|
+
},
|
|
102
|
+
beforeClarify: () => {
|
|
103
|
+
controller.publish({ type: "phase-set", phase: "clarify" });
|
|
104
|
+
},
|
|
105
|
+
afterClarify: () => {
|
|
106
|
+
controller.publish({ type: "phase-set", phase: "done" });
|
|
107
|
+
inkInstance?.unmount();
|
|
108
|
+
inkInstance = null;
|
|
109
|
+
process.stdout.write("\x1b[?25h");
|
|
59
110
|
},
|
|
60
111
|
};
|
|
61
112
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
113
|
+
const FINALIZE_RULES = [
|
|
114
|
+
{ when: (t) => t.failedChunks === 0, kind: "success" },
|
|
115
|
+
{ when: (t) => t.failedChunks === t.totalChunks, kind: "all-failed" },
|
|
116
|
+
{ when: () => true, kind: "partial" },
|
|
117
|
+
];
|
|
118
|
+
const FINALIZE_RENDER = {
|
|
119
|
+
success: (t) => ` ${chalk.green("ok")} ${t.fileName} ${chalk.dim(`${t.completedChunks} of ${t.totalChunks} pages · ${t.txAdded} transactions${t.questionsAdded > 0 ? `, ${t.questionsAdded} questions` : ""}`)}`,
|
|
120
|
+
"all-failed": (t) => ` ${chalk.red("fail")} ${t.fileName} ${chalk.dim("every chunk failed")}`,
|
|
121
|
+
partial: (t) => ` ${chalk.yellow("partial")} ${t.fileName} ${chalk.dim(`${t.completedChunks} of ${t.totalChunks} pages · ${t.failedChunks} chunks failed · ${t.txAdded} transactions`)}`,
|
|
122
|
+
};
|
|
123
|
+
function classifyFinalize(t) {
|
|
124
|
+
for (const r of FINALIZE_RULES)
|
|
125
|
+
if (r.when(t))
|
|
126
|
+
return r.kind;
|
|
127
|
+
return "partial";
|
|
128
|
+
}
|
|
129
|
+
function buildPlainHooks() {
|
|
130
|
+
const tallies = new Map();
|
|
131
|
+
const fileIdByChunkId = new Map();
|
|
132
|
+
let unsubscribeProgress = null;
|
|
133
|
+
const finalize = (fileId) => {
|
|
134
|
+
const t = tallies.get(fileId);
|
|
135
|
+
if (!t || t.completedChunks + t.failedChunks < t.totalChunks)
|
|
136
|
+
return;
|
|
137
|
+
console.log(FINALIZE_RENDER[classifyFinalize(t)](t));
|
|
138
|
+
};
|
|
72
139
|
return {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (e.decrypted > 0) {
|
|
79
|
-
inkInstance = render(createElement(ScanDashboard, { controller, totalFiles: e.decrypted, parallel }));
|
|
140
|
+
afterDecrypt: (s) => {
|
|
141
|
+
const total = s.decrypted.length + s.skipped.length + s.failed.length;
|
|
142
|
+
if (total === 0) {
|
|
143
|
+
console.log("No files to scan.");
|
|
144
|
+
return;
|
|
80
145
|
}
|
|
146
|
+
console.log(`Decrypted ${s.decrypted.length}, skipped ${s.skipped.length}, failed ${s.failed.length}.`);
|
|
81
147
|
},
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
type: "scan-end",
|
|
86
|
-
fileName: e.fileName,
|
|
87
|
-
status: e.status,
|
|
88
|
-
transactions: e.transactions,
|
|
89
|
-
unknowns: e.unknowns,
|
|
90
|
-
error: e.error,
|
|
91
|
-
}),
|
|
92
|
-
committing: () => {
|
|
93
|
-
if (inkInstance) {
|
|
94
|
-
inkInstance.unmount();
|
|
95
|
-
inkInstance = null;
|
|
96
|
-
}
|
|
97
|
-
if (mountedFiles > 0)
|
|
98
|
-
base.committing?.();
|
|
148
|
+
afterChunk: (s) => {
|
|
149
|
+
if (s.chunks.length > 0)
|
|
150
|
+
console.log(`Chunked into ${s.chunks.length} page(s).`);
|
|
99
151
|
},
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
152
|
+
beforeParse: (s) => {
|
|
153
|
+
for (const c of s.chunks)
|
|
154
|
+
fileIdByChunkId.set(c.chunkId, c.fileId);
|
|
155
|
+
unsubscribeProgress = s.progress.subscribe((event) => {
|
|
156
|
+
const fileId = fileIdByChunkId.get(event.chunkId);
|
|
157
|
+
if (!fileId)
|
|
158
|
+
return;
|
|
159
|
+
const t = tallies.get(fileId);
|
|
160
|
+
if (!t)
|
|
161
|
+
return;
|
|
162
|
+
if (event.kind === "tx")
|
|
163
|
+
t.txAdded++;
|
|
164
|
+
else
|
|
165
|
+
t.questionsAdded++;
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
onWorkerStart: (_id, chunk) => {
|
|
169
|
+
if (!tallies.has(chunk.fileId)) {
|
|
170
|
+
tallies.set(chunk.fileId, {
|
|
171
|
+
fileName: chunk.fileName,
|
|
172
|
+
totalChunks: chunk.totalPages,
|
|
173
|
+
completedChunks: 0,
|
|
174
|
+
failedChunks: 0,
|
|
175
|
+
txAdded: 0,
|
|
176
|
+
questionsAdded: 0,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
110
179
|
},
|
|
111
|
-
|
|
112
|
-
|
|
180
|
+
onWorkerEnd: (_id, chunk, ok) => {
|
|
181
|
+
const t = tallies.get(chunk.fileId);
|
|
182
|
+
if (!t)
|
|
113
183
|
return;
|
|
114
|
-
|
|
115
|
-
|
|
184
|
+
if (ok)
|
|
185
|
+
t.completedChunks++;
|
|
186
|
+
else
|
|
187
|
+
t.failedChunks++;
|
|
188
|
+
finalize(chunk.fileId);
|
|
116
189
|
},
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
console.log(
|
|
190
|
+
afterParse: () => {
|
|
191
|
+
unsubscribeProgress?.();
|
|
192
|
+
unsubscribeProgress = null;
|
|
193
|
+
},
|
|
194
|
+
beforeClarify: () => {
|
|
195
|
+
console.log("Clarifying...");
|
|
123
196
|
},
|
|
124
197
|
};
|
|
125
198
|
}
|
|
126
|
-
|
|
127
|
-
function renderScanSummary(summary) {
|
|
128
|
-
console.log("");
|
|
129
|
-
const headline = `Scanned ${summary.total} file(s) — ` +
|
|
130
|
-
`${summary.scanned + summary.replaced} ok, ` +
|
|
131
|
-
`${summary.failed} failed, ` +
|
|
132
|
-
`${summary.unknowns} unknown${summary.unknowns === 1 ? "" : "s"} flagged`;
|
|
133
|
-
console.log(chalk.bold(headline));
|
|
199
|
+
function renderSummary(state) {
|
|
134
200
|
console.log("");
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
case "replaced": {
|
|
144
|
-
const tag = chalk.dim(`${d.transactions} transactions${d.unknowns > 0 ? ` · ${d.unknowns} unknowns` : ""} (replaces prior)`);
|
|
145
|
-
console.log(` ${chalk.cyan("↻")} ${label} ${tag}`);
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
case "skipped": {
|
|
149
|
-
console.log(` ${chalk.dim("•")} ${label} ${chalk.dim("(already scanned)")}`);
|
|
150
|
-
break;
|
|
151
|
-
}
|
|
152
|
-
case "failed": {
|
|
153
|
-
console.log(` ${chalk.red("✗")} ${label} ${chalk.dim(`— ${d.error ?? "failed"}`)}`);
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
201
|
+
const txCount = countTransactions(state);
|
|
202
|
+
console.log(chalk.bold(`Scanned ${state.decrypted.length} file(s) → ${txCount} transactions.`));
|
|
203
|
+
const r = state.clarifySummary;
|
|
204
|
+
if (r && r.total > 0) {
|
|
205
|
+
console.log(`Clarified ${r.clarified}/${r.total} questions.`);
|
|
206
|
+
if (r.remaining > 0) {
|
|
207
|
+
console.log(chalk.yellow(`${r.remaining} question(s) remain — run ${chalk.cyan("plasalid clarify")} to finish them.`));
|
|
156
208
|
}
|
|
157
209
|
}
|
|
158
|
-
|
|
159
|
-
|
|
210
|
+
if (state.errors.length > 0) {
|
|
211
|
+
console.log(chalk.yellow(`${state.errors.length} phase error(s):`));
|
|
212
|
+
for (const e of state.errors) {
|
|
213
|
+
console.log(chalk.dim(` - [${e.phase}] ${e.target ?? ""} ${e.error?.message ?? ""}`));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (txCount > 0) {
|
|
160
217
|
console.log("");
|
|
161
|
-
console.log(
|
|
162
|
-
? " — to walk every open unknown and apply your decision."
|
|
163
|
-
: " — no unknowns surfaced this run; nothing to do.")}`);
|
|
218
|
+
console.log(`Next: run ${chalk.cyan("plasalid")} to chat with your ledger about what just landed.`);
|
|
164
219
|
}
|
|
165
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Snapshot transaction count attributable to this scan. Reads from
|
|
223
|
+
* scanned_files via the file ids assigned in decryptPhase.
|
|
224
|
+
*/
|
|
225
|
+
function countTransactions(state) {
|
|
226
|
+
const ids = state.decrypted
|
|
227
|
+
.map((d) => d.scannedFileId)
|
|
228
|
+
.filter((s) => !!s);
|
|
229
|
+
if (ids.length === 0)
|
|
230
|
+
return 0;
|
|
231
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
232
|
+
const row = getDb()
|
|
233
|
+
.prepare(`SELECT COUNT(*) AS n FROM transactions WHERE source_file_id IN (${placeholders})`)
|
|
234
|
+
.get(...ids);
|
|
235
|
+
return row.n;
|
|
236
|
+
}
|
|
@@ -4,7 +4,7 @@ import { getNetWorth } from "../../db/queries/account-balance.js";
|
|
|
4
4
|
import { countTransactions } from "../../db/queries/transactions.js";
|
|
5
5
|
import { getRecurringSummary } from "../../db/queries/recurrences.js";
|
|
6
6
|
import { countScannedFiles } from "../../db/queries/files.js";
|
|
7
|
-
import {
|
|
7
|
+
import { countQuestions } from "../../db/queries/questions.js";
|
|
8
8
|
import { countMemories } from "../../ai/memory.js";
|
|
9
9
|
import { formatAmount } from "../../currency.js";
|
|
10
10
|
import { visibleLength } from "../format.js";
|
|
@@ -38,7 +38,7 @@ function systemRows(db) {
|
|
|
38
38
|
const tx = countTransactions(db);
|
|
39
39
|
const files = countScannedFiles(db);
|
|
40
40
|
const memories = countMemories(db);
|
|
41
|
-
const
|
|
41
|
+
const questions = countQuestions(db);
|
|
42
42
|
const rows = [
|
|
43
43
|
{
|
|
44
44
|
label: "Transactions",
|
|
@@ -63,11 +63,11 @@ function systemRows(db) {
|
|
|
63
63
|
if (memories > 0) {
|
|
64
64
|
rows.push({ label: "Memories", value: formatInteger(memories) });
|
|
65
65
|
}
|
|
66
|
-
if (
|
|
66
|
+
if (questions > 0) {
|
|
67
67
|
rows.push({
|
|
68
|
-
label: "
|
|
69
|
-
value: chalk.yellow(formatInteger(
|
|
70
|
-
suffix: chalk.dim("run `plasalid
|
|
68
|
+
label: "Questions",
|
|
69
|
+
value: chalk.yellow(formatInteger(questions)),
|
|
70
|
+
suffix: chalk.dim("run `plasalid clarify`"),
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
73
|
return rows;
|
package/dist/cli/index.js
CHANGED
|
@@ -118,23 +118,6 @@ program
|
|
|
118
118
|
const { runScanCommand } = await import("./commands/scan.js");
|
|
119
119
|
await runScanCommand({ regex: regexes[0], force: !!opts.force, parallel });
|
|
120
120
|
});
|
|
121
|
-
program
|
|
122
|
-
.command("resolve")
|
|
123
|
-
.description("Walk every open unknown from the last scan one at a time and apply your decision (categorize, merge duplicates, link recurrences, skip).")
|
|
124
|
-
.option("-a, --account <id>", "Limit to unknowns attached to a single account")
|
|
125
|
-
.option("--from <date>", "Only consider entries on or after this date (YYYY-MM-DD)")
|
|
126
|
-
.option("--to <date>", "Only consider entries on or before this date (YYYY-MM-DD)")
|
|
127
|
-
.option("-k, --kind <kind>", "Filter by unknown kind (uncategorized_expense, duplicate, correlation, recurrence_candidate, similar_accounts)")
|
|
128
|
-
.action(async (opts) => {
|
|
129
|
-
ensureConfigured();
|
|
130
|
-
const { runResolveCommand } = await import("./commands/resolve.js");
|
|
131
|
-
await runResolveCommand({
|
|
132
|
-
accountId: opts.account,
|
|
133
|
-
from: opts.from,
|
|
134
|
-
to: opts.to,
|
|
135
|
-
kind: opts.kind,
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
121
|
program
|
|
139
122
|
.command("rules")
|
|
140
123
|
.description("List rules the system has learned")
|
|
@@ -152,12 +135,12 @@ program
|
|
|
152
135
|
forgetRule(regex);
|
|
153
136
|
});
|
|
154
137
|
program
|
|
155
|
-
.command("
|
|
156
|
-
.description("
|
|
157
|
-
.action(async (
|
|
138
|
+
.command("clarify")
|
|
139
|
+
.description("Clarify every question across the ledger")
|
|
140
|
+
.action(async () => {
|
|
158
141
|
ensureConfigured();
|
|
159
|
-
const {
|
|
160
|
-
await
|
|
142
|
+
const { runClarifyCommand } = await import("./commands/clarify.js");
|
|
143
|
+
await runClarifyCommand();
|
|
161
144
|
});
|
|
162
145
|
program.configureHelp({
|
|
163
146
|
formatHelp: () => helpScreen([
|
|
@@ -170,7 +153,7 @@ program.configureHelp({
|
|
|
170
153
|
desc: "Open the data folder in your OS file explorer (alias: open)",
|
|
171
154
|
},
|
|
172
155
|
{ name: "accounts", desc: "Browse the chart of accounts (interactive TTY) or list them (piped)" },
|
|
173
|
-
{ name: "status", desc: "Show financial and system status (net worth, recurring,
|
|
156
|
+
{ name: "status", desc: "Show financial and system status (net worth, recurring, questions)" },
|
|
174
157
|
{
|
|
175
158
|
name: "transactions",
|
|
176
159
|
desc: "Browse transactions (interactive TTY) or list them (piped/--no-interactive)",
|
|
@@ -183,10 +166,6 @@ program.configureHelp({
|
|
|
183
166
|
name: "scan",
|
|
184
167
|
desc: "Scan new PDFs (optionally by regex; --force to re-scan)",
|
|
185
168
|
},
|
|
186
|
-
{
|
|
187
|
-
name: "resolve",
|
|
188
|
-
desc: "Walk open unknowns one at a time and apply your decision",
|
|
189
|
-
},
|
|
190
169
|
{
|
|
191
170
|
name: "rules",
|
|
192
171
|
desc: "List rules the system has learned",
|
|
@@ -196,8 +175,8 @@ program.configureHelp({
|
|
|
196
175
|
desc: "Delete learned rules whose ids match <regex> (anchored)",
|
|
197
176
|
},
|
|
198
177
|
{
|
|
199
|
-
name: "
|
|
200
|
-
desc: "
|
|
178
|
+
name: "clarify",
|
|
179
|
+
desc: "Clarify every question across the ledger",
|
|
201
180
|
},
|
|
202
181
|
]),
|
|
203
182
|
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events the CLI publishes into the dashboard. The CLI subscribes to the
|
|
3
|
+
* scanner's ScanProgress sink and routes per-chunk ticks here via chunkLookup.
|
|
4
|
+
*/
|
|
5
|
+
export type CurrentPhase = "parse" | "clarify" | "done";
|
|
6
|
+
export type DashboardEvent = {
|
|
7
|
+
type: "chunk-start";
|
|
8
|
+
fileId: string;
|
|
9
|
+
fileName: string;
|
|
10
|
+
pageNumber: number;
|
|
11
|
+
totalPages: number;
|
|
12
|
+
} | {
|
|
13
|
+
type: "chunk-tx";
|
|
14
|
+
fileId: string;
|
|
15
|
+
pageNumber: number;
|
|
16
|
+
} | {
|
|
17
|
+
type: "chunk-question";
|
|
18
|
+
fileId: string;
|
|
19
|
+
pageNumber: number;
|
|
20
|
+
} | {
|
|
21
|
+
type: "chunk-end";
|
|
22
|
+
fileId: string;
|
|
23
|
+
pageNumber: number;
|
|
24
|
+
ok: boolean;
|
|
25
|
+
} | {
|
|
26
|
+
type: "phase-set";
|
|
27
|
+
phase: CurrentPhase;
|
|
28
|
+
};
|
|
29
|
+
export interface ScanDashboardController {
|
|
30
|
+
publish(event: DashboardEvent): void;
|
|
31
|
+
subscribe(handler: (e: DashboardEvent) => void): () => void;
|
|
32
|
+
}
|
|
33
|
+
export declare function createScanDashboardController(): ScanDashboardController;
|
|
34
|
+
export interface FileSeed {
|
|
35
|
+
readonly fileId: string;
|
|
36
|
+
readonly fileName: string;
|
|
37
|
+
readonly totalPages: number;
|
|
38
|
+
}
|
|
39
|
+
interface Props {
|
|
40
|
+
controller: ScanDashboardController;
|
|
41
|
+
files: ReadonlyArray<FileSeed>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Tree-layout scan dashboard. Header carries the only animated element (one
|
|
45
|
+
* `<Spinner>`). All other status indicators are static glyphs that only
|
|
46
|
+
* redraw when their data changes.
|
|
47
|
+
*/
|
|
48
|
+
export declare function ScanDashboard(props: Props): import("react/jsx-runtime").JSX.Element;
|
|
49
|
+
export {};
|