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.
- package/README.md +2 -2
- package/dist/ai/agent.d.ts +6 -7
- package/dist/ai/agent.js +27 -11
- package/dist/ai/personas.js +48 -46
- package/dist/ai/system-prompt.js +1 -1
- package/dist/ai/tools/account-mutex.d.ts +1 -0
- package/dist/ai/tools/account-mutex.js +16 -0
- package/dist/ai/tools/index.js +4 -12
- package/dist/ai/tools/ingest.d.ts +1 -1
- package/dist/ai/tools/ingest.js +282 -242
- package/dist/ai/tools/merchants.js +1 -28
- package/dist/ai/tools/read.js +8 -8
- package/dist/ai/tools/record.js +3 -36
- 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/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 +5 -5
- 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/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/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,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,
|
|
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) {
|
package/dist/ai/tools/read.js
CHANGED
|
@@ -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 {
|
|
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: "
|
|
66
|
-
description: "List clarification
|
|
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
|
|
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
|
-
|
|
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 "
|
|
143
|
-
const rows =
|
|
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
|
|
148
|
+
return "No questions. The picture is clear.";
|
|
149
149
|
return filtered
|
|
150
150
|
.map((r) => {
|
|
151
151
|
const targets = [
|
package/dist/ai/tools/record.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/dist/ai/tools/resolve.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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);
|
package/dist/ai/tools/scan.js
CHANGED
|
@@ -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
|
}
|
package/dist/ai/tools/types.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type Database from "libsql";
|
|
2
2
|
import type { ToolDefinition } from "../provider.js";
|
|
3
|
-
import type {
|
|
4
|
-
|
|
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
|
|
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
|
|
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
|
|
25
|
+
/** Called when the model declares the session is done (scan or record). */
|
|
25
26
|
onComplete?: (summary: string) => void;
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
2
|
-
|
|
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 {
|
|
3
|
-
|
|
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(
|
|
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(
|
|
23
|
+
console.log(formatSummary(summary));
|
|
8
24
|
}
|
|
9
25
|
catch (err) {
|
|
10
|
-
|
|
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
|
-
|
|
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
|
|
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));
|