plasalid 0.4.1 → 0.5.0
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 -14
- package/dist/ai/agent.d.ts +15 -2
- package/dist/ai/agent.js +21 -2
- package/dist/ai/memory.d.ts +2 -0
- package/dist/ai/memory.js +2 -2
- package/dist/ai/personas.d.ts +2 -1
- package/dist/ai/personas.js +115 -45
- package/dist/ai/prompt-sections.d.ts +5 -0
- package/dist/ai/prompt-sections.js +26 -8
- package/dist/ai/system-prompt.d.ts +11 -0
- package/dist/ai/system-prompt.js +21 -6
- package/dist/ai/thinking.js +1 -1
- package/dist/ai/tools/common.js +2 -5
- package/dist/ai/tools/index.js +28 -8
- package/dist/ai/tools/ingest.d.ts +2 -1
- package/dist/ai/tools/ingest.js +262 -151
- package/dist/ai/tools/merchants.d.ts +2 -0
- package/dist/ai/tools/merchants.js +117 -0
- package/dist/ai/tools/read.js +31 -29
- package/dist/ai/tools/record.d.ts +2 -0
- package/dist/ai/tools/record.js +188 -0
- package/dist/ai/tools/review.js +77 -80
- package/dist/ai/tools/scan.js +1 -1
- package/dist/ai/tools/types.d.ts +15 -6
- package/dist/cli/commands/accounts.js +33 -25
- package/dist/cli/commands/record.d.ts +4 -0
- package/dist/cli/commands/record.js +119 -0
- package/dist/cli/commands/revert.js +1 -1
- package/dist/cli/commands/scan.js +15 -19
- package/dist/cli/commands/status.js +6 -9
- package/dist/cli/commands/transactions.js +36 -41
- package/dist/cli/format.d.ts +2 -0
- package/dist/cli/format.js +7 -2
- package/dist/cli/index.js +19 -7
- package/dist/cli/ink/scan_dashboard.d.ts +1 -1
- package/dist/cli/ink/scan_dashboard.js +2 -2
- package/dist/cli/setup.d.ts +0 -1
- package/dist/cli/setup.js +2 -8
- package/dist/currency.d.ts +3 -0
- package/dist/currency.js +12 -1
- package/dist/db/queries/account_balance.d.ts +83 -4
- package/dist/db/queries/account_balance.js +239 -20
- package/dist/db/queries/action_log.d.ts +29 -0
- package/dist/db/queries/action_log.js +27 -0
- package/dist/db/queries/concerns.d.ts +10 -7
- package/dist/db/queries/concerns.js +20 -16
- package/dist/db/queries/journal.d.ts +1 -0
- package/dist/db/queries/merchants.d.ts +42 -0
- package/dist/db/queries/merchants.js +120 -0
- package/dist/db/queries/recurrences.d.ts +3 -3
- package/dist/db/queries/recurrences.js +32 -34
- package/dist/db/queries/search.d.ts +5 -4
- package/dist/db/queries/search.js +16 -12
- package/dist/db/queries/transactions.d.ts +167 -0
- package/dist/db/queries/transactions.js +320 -0
- package/dist/db/schema.js +51 -9
- package/dist/reviewer/pipeline.d.ts +4 -4
- package/dist/reviewer/pipeline.js +4 -4
- package/dist/reviewer/prompts.js +4 -4
- package/dist/scanner/buffer.d.ts +24 -21
- package/dist/scanner/buffer.js +18 -18
- package/dist/scanner/pipeline.d.ts +3 -2
- package/dist/scanner/pipeline.js +33 -36
- package/dist/scanner/prompts.js +3 -3
- package/package.json +2 -2
package/dist/scanner/pipeline.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import { getDb } from "../db/connection.js";
|
|
3
3
|
import { countOpenConcerns, } from "../db/queries/concerns.js";
|
|
4
|
-
import { correlatePairs } from "../db/queries/
|
|
4
|
+
import { correlatePairs } from "../db/queries/transactions.js";
|
|
5
5
|
import { runScanAgent } from "../ai/agent.js";
|
|
6
6
|
import { buildDocumentBlock } from "./pdf.js";
|
|
7
7
|
import { buildScanUserMessage } from "./prompts.js";
|
|
@@ -12,7 +12,7 @@ import { decryptQueue, confirmProceedAfterFailures, } from "./decrypt_queue.js";
|
|
|
12
12
|
export function compileMatcher(input) {
|
|
13
13
|
return new RegExp(input, "i");
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
/** Orchestration */
|
|
16
16
|
export async function runScan(opts = {}) {
|
|
17
17
|
const db = getDb();
|
|
18
18
|
const matcher = opts.regex ? compileMatcher(opts.regex) : null;
|
|
@@ -98,7 +98,7 @@ async function scanOneFile(db, file, events) {
|
|
|
98
98
|
events?.scanEnd?.({
|
|
99
99
|
fileName: file.fileName,
|
|
100
100
|
status: "scanned",
|
|
101
|
-
|
|
101
|
+
transactions: buffer.transactions.length,
|
|
102
102
|
concerns: buffer.concerns.length,
|
|
103
103
|
});
|
|
104
104
|
return { decryptedFile: file, buffer, agentText: text };
|
|
@@ -108,14 +108,14 @@ async function scanOneFile(db, file, events) {
|
|
|
108
108
|
events?.scanEnd?.({
|
|
109
109
|
fileName: file.fileName,
|
|
110
110
|
status: "failed",
|
|
111
|
-
|
|
111
|
+
transactions: 0,
|
|
112
112
|
concerns: 0,
|
|
113
113
|
error: message,
|
|
114
114
|
});
|
|
115
115
|
return { decryptedFile: file, buffer, error: message, agentText: "" };
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
-
|
|
118
|
+
/** Phase 3: cross-file correlation */
|
|
119
119
|
/**
|
|
120
120
|
* For every pair of buffered entries that look like the same money movement
|
|
121
121
|
* across two different files, append a mirror concern to each side's buffer.
|
|
@@ -126,22 +126,22 @@ function applyCrossFileCorrelations(results) {
|
|
|
126
126
|
for (const res of results) {
|
|
127
127
|
if (res.error)
|
|
128
128
|
continue;
|
|
129
|
-
for (const
|
|
129
|
+
for (const bt of res.buffer.transactions) {
|
|
130
130
|
all.push({
|
|
131
131
|
file: res,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
date:
|
|
135
|
-
description:
|
|
132
|
+
transactionId: bt.transaction_id,
|
|
133
|
+
postings: bt.input.postings,
|
|
134
|
+
date: bt.input.date,
|
|
135
|
+
description: bt.input.description,
|
|
136
136
|
});
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
const candidates = all.map(e => {
|
|
140
|
-
const debit = e.
|
|
141
|
-
const currency = e.
|
|
142
|
-
const ids = Array.from(new Set(e.
|
|
140
|
+
const debit = e.postings.reduce((s, p) => s + (p.debit ?? 0), 0);
|
|
141
|
+
const currency = e.postings.find(p => p.currency)?.currency ?? "THB";
|
|
142
|
+
const ids = Array.from(new Set(e.postings.map(p => p.account_id)));
|
|
143
143
|
return {
|
|
144
|
-
id: e.
|
|
144
|
+
id: e.transactionId,
|
|
145
145
|
date: e.date,
|
|
146
146
|
description: e.description,
|
|
147
147
|
amount: Math.round(debit * 100) / 100,
|
|
@@ -151,55 +151,52 @@ function applyCrossFileCorrelations(results) {
|
|
|
151
151
|
};
|
|
152
152
|
});
|
|
153
153
|
const pairs = correlatePairs(candidates, { toleranceDays: 3 });
|
|
154
|
-
const
|
|
154
|
+
const byTransaction = new Map(all.map(a => [a.transactionId, a]));
|
|
155
155
|
for (const pair of pairs) {
|
|
156
|
-
const a =
|
|
157
|
-
const b =
|
|
156
|
+
const a = byTransaction.get(pair.a.id);
|
|
157
|
+
const b = byTransaction.get(pair.b.id);
|
|
158
158
|
if (!a || !b)
|
|
159
159
|
continue;
|
|
160
160
|
if (a.file === b.file)
|
|
161
|
-
continue;
|
|
161
|
+
continue;
|
|
162
162
|
const amountStr = `฿${pair.amount.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
163
163
|
a.file.buffer.appendConcern({
|
|
164
|
-
|
|
164
|
+
transaction_id: a.transactionId,
|
|
165
165
|
account_id: null,
|
|
166
166
|
prompt: `Looks like the matching half of this ${amountStr} movement on ${a.date} was also recorded in ${b.file.decryptedFile.fileName} on ${b.date}. Merge during review?`,
|
|
167
|
-
options: ["Yes — merge into one
|
|
167
|
+
options: ["Yes — merge into one transaction", "No — these are two real events", "Skip — leave as is"],
|
|
168
168
|
});
|
|
169
169
|
b.file.buffer.appendConcern({
|
|
170
|
-
|
|
170
|
+
transaction_id: b.transactionId,
|
|
171
171
|
account_id: null,
|
|
172
172
|
prompt: `Looks like the matching half of this ${amountStr} movement on ${b.date} was also recorded in ${a.file.decryptedFile.fileName} on ${a.date}. Merge during review?`,
|
|
173
|
-
options: ["Yes — merge into one
|
|
173
|
+
options: ["Yes — merge into one transaction", "No — these are two real events", "Skip — leave as is"],
|
|
174
174
|
});
|
|
175
175
|
}
|
|
176
|
-
return pairs.filter(p =>
|
|
176
|
+
return pairs.filter(p => byTransaction.get(p.a.id)?.file !== byTransaction.get(p.b.id)?.file).length;
|
|
177
177
|
}
|
|
178
|
-
|
|
178
|
+
/** Phase 4: commit */
|
|
179
179
|
function commitAll(db, decryptResult, scanResults) {
|
|
180
180
|
const out = [];
|
|
181
|
-
// Skipped files: keep them in the summary with their existing concern count.
|
|
182
181
|
for (const skipped of decryptResult.skipped) {
|
|
183
182
|
out.push({
|
|
184
183
|
name: skipped.file.name,
|
|
185
184
|
relPath: skipped.file.relPath,
|
|
186
185
|
status: "skipped",
|
|
187
|
-
|
|
186
|
+
transactions: 0,
|
|
188
187
|
concerns: countOpenConcerns(db, { file_id: skipped.existingScannedFileId }),
|
|
189
188
|
});
|
|
190
189
|
}
|
|
191
|
-
// Files that failed to decrypt never reached an agent.
|
|
192
190
|
for (const failed of decryptResult.failed) {
|
|
193
191
|
out.push({
|
|
194
192
|
name: failed.file.name,
|
|
195
193
|
relPath: failed.file.relPath,
|
|
196
194
|
status: "failed",
|
|
197
|
-
|
|
195
|
+
transactions: 0,
|
|
198
196
|
concerns: 0,
|
|
199
197
|
error: failed.error,
|
|
200
198
|
});
|
|
201
199
|
}
|
|
202
|
-
// Scanned files: per-file transaction. Replaces prior records when needed.
|
|
203
200
|
for (const res of scanResults) {
|
|
204
201
|
const { decryptedFile, buffer, error, agentText } = res;
|
|
205
202
|
if (error) {
|
|
@@ -207,7 +204,7 @@ function commitAll(db, decryptResult, scanResults) {
|
|
|
207
204
|
name: decryptedFile.fileName,
|
|
208
205
|
relPath: decryptedFile.relPath,
|
|
209
206
|
status: "failed",
|
|
210
|
-
|
|
207
|
+
transactions: 0,
|
|
211
208
|
concerns: buffer.concerns.length,
|
|
212
209
|
error,
|
|
213
210
|
});
|
|
@@ -228,7 +225,7 @@ function commitAll(db, decryptResult, scanResults) {
|
|
|
228
225
|
name: decryptedFile.fileName,
|
|
229
226
|
relPath: decryptedFile.relPath,
|
|
230
227
|
status: decryptedFile.replacesPriorScannedFileId ? "replaced" : "scanned",
|
|
231
|
-
|
|
228
|
+
transactions: counts.transactions,
|
|
232
229
|
concerns: counts.concerns,
|
|
233
230
|
});
|
|
234
231
|
}
|
|
@@ -237,7 +234,7 @@ function commitAll(db, decryptResult, scanResults) {
|
|
|
237
234
|
name: decryptedFile.fileName,
|
|
238
235
|
relPath: decryptedFile.relPath,
|
|
239
236
|
status: "failed",
|
|
240
|
-
|
|
237
|
+
transactions: 0,
|
|
241
238
|
concerns: buffer.concerns.length,
|
|
242
239
|
error: err?.message ?? "commit failed",
|
|
243
240
|
});
|
|
@@ -245,7 +242,7 @@ function commitAll(db, decryptResult, scanResults) {
|
|
|
245
242
|
}
|
|
246
243
|
return out;
|
|
247
244
|
}
|
|
248
|
-
|
|
245
|
+
/** Summary assembly */
|
|
249
246
|
function buildSummary(total, details, _decrypt) {
|
|
250
247
|
const summary = {
|
|
251
248
|
total,
|
|
@@ -265,15 +262,15 @@ function buildSummary(total, details, _decrypt) {
|
|
|
265
262
|
function buildAbortedSummary(total, decrypt) {
|
|
266
263
|
const details = [
|
|
267
264
|
...decrypt.skipped.map(s => ({
|
|
268
|
-
name: s.file.name, relPath: s.file.relPath, status: "skipped",
|
|
265
|
+
name: s.file.name, relPath: s.file.relPath, status: "skipped", transactions: 0, concerns: 0,
|
|
269
266
|
})),
|
|
270
267
|
...decrypt.failed.map(f => ({
|
|
271
|
-
name: f.file.name, relPath: f.file.relPath, status: "failed",
|
|
268
|
+
name: f.file.name, relPath: f.file.relPath, status: "failed", transactions: 0, concerns: 0, error: f.error,
|
|
272
269
|
})),
|
|
273
270
|
];
|
|
274
271
|
return buildSummary(total, details, decrypt);
|
|
275
272
|
}
|
|
276
|
-
|
|
273
|
+
/** Low-level DB helpers */
|
|
277
274
|
function deleteScannedFile(db, id) {
|
|
278
275
|
db.prepare(`DELETE FROM scanned_files WHERE id = ?`).run(id);
|
|
279
276
|
}
|
package/dist/scanner/prompts.js
CHANGED
|
@@ -11,10 +11,10 @@ export function buildScanUserMessage(opts) {
|
|
|
11
11
|
`Steps:`,
|
|
12
12
|
`1. Call list_accounts to see what already exists.`,
|
|
13
13
|
`2. Infer the primary account type (asset / liability / income / expense) from the document's header, account type field, and transaction patterns.`,
|
|
14
|
-
`3. If this document references an account that isn't yet in the chart, call create_account once. Mask the account number to the last 4 digits.`,
|
|
14
|
+
`3. If this document references an account that isn't yet in the chart, call create_account once (pass parent_id under the matching top-level type root). Mask the account number to the last 4 digits.`,
|
|
15
15
|
`4. Persist any document-level metadata you find (statement_day, due_day, points_balance, etc.) using update_account_metadata.`,
|
|
16
|
-
`5. For every transaction in the document, call
|
|
17
|
-
`6. Never pause to ask the user. If a row is ambiguous, post your best-guess
|
|
16
|
+
`5. For every transaction in the document, call record_transaction with balanced debit/credit postings. Attach a merchant block (canonical_name + alias + default_account_id when categorization is confident) for any external counter-party. Reuse existing accounts; create expense categories under their parent (e.g. expense:food before expense:food:groceries) as needed. When you cannot categorize confidently, post the expense side to expense:uncategorized and call note_concern with kind="uncategorized_expense".`,
|
|
17
|
+
`6. Never pause to ask the user. If a row is ambiguous, post your best-guess transaction first, then call note_concern with details and the new transaction_id. If a row is truly unparseable, skip it and call note_concern with the raw row text (no transaction_id). A missing row is better than a wrong row.`,
|
|
18
18
|
`7. When you are done, call mark_file_scanned with a short summary.`,
|
|
19
19
|
].join("\n");
|
|
20
20
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plasalid",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Plasalid — The Harness for Personal Finance",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"finance",
|
|
7
7
|
"personal-finance",
|