plasalid 0.5.7 → 0.6.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.
Files changed (97) hide show
  1. package/README.md +9 -9
  2. package/dist/accounts/taxonomy.d.ts +1 -1
  3. package/dist/accounts/taxonomy.js +2 -2
  4. package/dist/ai/agent.d.ts +8 -9
  5. package/dist/ai/agent.js +21 -20
  6. package/dist/ai/errors.d.ts +16 -0
  7. package/dist/ai/errors.js +47 -0
  8. package/dist/ai/personas.d.ts +1 -1
  9. package/dist/ai/personas.js +69 -66
  10. package/dist/ai/prompt-sections.d.ts +4 -5
  11. package/dist/ai/prompt-sections.js +11 -11
  12. package/dist/ai/providers/anthropic.js +10 -4
  13. package/dist/ai/providers/openai.js +70 -56
  14. package/dist/ai/redactor.js +77 -51
  15. package/dist/ai/system-prompt.d.ts +2 -3
  16. package/dist/ai/system-prompt.js +5 -5
  17. package/dist/ai/tools/common.js +13 -5
  18. package/dist/ai/tools/index.js +15 -15
  19. package/dist/ai/tools/ingest.d.ts +2 -2
  20. package/dist/ai/tools/ingest.js +210 -87
  21. package/dist/ai/tools/merchants.js +27 -12
  22. package/dist/ai/tools/read.js +36 -20
  23. package/dist/ai/tools/record.js +79 -19
  24. package/dist/ai/tools/resolve.d.ts +2 -0
  25. package/dist/ai/tools/resolve.js +195 -0
  26. package/dist/ai/tools/types.d.ts +5 -7
  27. package/dist/cli/commands/accounts.js +2 -2
  28. package/dist/cli/commands/record.js +4 -2
  29. package/dist/cli/commands/resolve.d.ts +2 -0
  30. package/dist/cli/commands/resolve.js +13 -0
  31. package/dist/cli/commands/scan.js +18 -22
  32. package/dist/cli/commands/status.js +4 -2
  33. package/dist/cli/index.js +9 -9
  34. package/dist/cli/ink/hooks/useFooterText.js +1 -1
  35. package/dist/cli/ink/hooks/useTextInput.js +60 -69
  36. package/dist/cli/ink/scan_dashboard.d.ts +2 -2
  37. package/dist/cli/ink/scan_dashboard.js +3 -3
  38. package/dist/cli/setup.js +6 -3
  39. package/dist/cli/ux.js +1 -1
  40. package/dist/db/queries/account-balance.d.ts +140 -0
  41. package/dist/db/queries/account-balance.js +355 -0
  42. package/dist/db/queries/account_balance.d.ts +0 -1
  43. package/dist/db/queries/account_balance.js +0 -10
  44. package/dist/db/queries/action-log.d.ts +29 -0
  45. package/dist/db/queries/action-log.js +27 -0
  46. package/dist/db/queries/action_log.d.ts +1 -1
  47. package/dist/db/queries/concerns.d.ts +10 -0
  48. package/dist/db/queries/concerns.js +21 -0
  49. package/dist/db/queries/transactions.d.ts +3 -22
  50. package/dist/db/queries/transactions.js +4 -5
  51. package/dist/db/queries/unknowns.d.ts +62 -0
  52. package/dist/db/queries/unknowns.js +114 -0
  53. package/dist/db/schema.js +3 -3
  54. package/dist/resolver/pipeline.d.ts +16 -0
  55. package/dist/resolver/pipeline.js +38 -0
  56. package/dist/resolver/prompts.d.ts +8 -0
  57. package/dist/resolver/prompts.js +26 -0
  58. package/dist/scanner/account-mutex.d.ts +1 -0
  59. package/dist/scanner/account-mutex.js +16 -0
  60. package/dist/scanner/buffer.d.ts +10 -10
  61. package/dist/scanner/buffer.js +15 -15
  62. package/dist/scanner/concurrency.d.ts +10 -7
  63. package/dist/scanner/concurrency.js +3 -16
  64. package/dist/scanner/decrypt-queue.d.ts +57 -0
  65. package/dist/scanner/decrypt-queue.js +114 -0
  66. package/dist/scanner/decrypt_queue.js +56 -38
  67. package/dist/scanner/detectors/correlations.d.ts +2 -0
  68. package/dist/scanner/detectors/correlations.js +51 -0
  69. package/dist/scanner/detectors/duplicates.d.ts +2 -0
  70. package/dist/scanner/detectors/duplicates.js +75 -0
  71. package/dist/scanner/detectors/index.d.ts +18 -0
  72. package/dist/scanner/detectors/index.js +39 -0
  73. package/dist/scanner/detectors/recurrences.d.ts +2 -0
  74. package/dist/scanner/detectors/recurrences.js +49 -0
  75. package/dist/scanner/detectors/similar_accounts.d.ts +2 -0
  76. package/dist/scanner/detectors/similar_accounts.js +64 -0
  77. package/dist/scanner/detectors/similarities.d.ts +2 -0
  78. package/dist/scanner/detectors/similarities.js +73 -0
  79. package/dist/scanner/detectors/types.d.ts +16 -0
  80. package/dist/scanner/detectors/types.js +1 -0
  81. package/dist/scanner/inspectors/correlations.d.ts +2 -0
  82. package/dist/scanner/inspectors/correlations.js +47 -0
  83. package/dist/scanner/inspectors/duplicates.d.ts +2 -0
  84. package/dist/scanner/inspectors/duplicates.js +75 -0
  85. package/dist/scanner/inspectors/index.d.ts +19 -0
  86. package/dist/scanner/inspectors/index.js +39 -0
  87. package/dist/scanner/inspectors/recurrences.d.ts +2 -0
  88. package/dist/scanner/inspectors/recurrences.js +49 -0
  89. package/dist/scanner/inspectors/similarities.d.ts +2 -0
  90. package/dist/scanner/inspectors/similarities.js +73 -0
  91. package/dist/scanner/inspectors/types.d.ts +16 -0
  92. package/dist/scanner/inspectors/types.js +1 -0
  93. package/dist/scanner/pdf-unlock.js +3 -1
  94. package/dist/scanner/pipeline.d.ts +6 -4
  95. package/dist/scanner/pipeline.js +63 -102
  96. package/dist/scanner/prompts.js +2 -2
  97. package/package.json +2 -1
@@ -1,5 +1,5 @@
1
1
  import { getMemories } from "./memory.js";
2
- import { getAccountBalances } from "../db/queries/account_balance.js";
2
+ import { getAccountBalances, } from "../db/queries/account-balance.js";
3
3
  import { stripControls } from "./sanitize.js";
4
4
  /**
5
5
  * Small, single-purpose renderers that produce one Markdown-ish section each.
@@ -21,7 +21,7 @@ export function renderTodayHuman() {
21
21
  year: "numeric",
22
22
  })}.`;
23
23
  }
24
- /** ISO date for scan/review ("Today is 2026-03-05."). */
24
+ /** ISO date for scan/resolve ("Today is 2026-03-05."). */
25
25
  export function renderTodayIso() {
26
26
  return `Today is ${new Date().toISOString().slice(0, 10)}.`;
27
27
  }
@@ -51,7 +51,7 @@ export function renderChatChartOrEmpty(db, name) {
51
51
  return `## Accounts on file\n${rows.join("\n")}`;
52
52
  }
53
53
  function renderHierarchical(balances, withBalance) {
54
- const byId = new Map(balances.map(b => [b.id, b]));
54
+ const byId = new Map(balances.map((b) => [b.id, b]));
55
55
  const depthCache = new Map();
56
56
  const depth = (id) => {
57
57
  if (depthCache.has(id))
@@ -65,16 +65,16 @@ function renderHierarchical(balances, withBalance) {
65
65
  depthCache.set(id, d);
66
66
  return d;
67
67
  };
68
- return balances.map(a => formatAccountRow(a, withBalance, depth(a.id)));
68
+ return balances.map((a) => formatAccountRow(a, withBalance, depth(a.id)));
69
69
  }
70
70
  export function renderMemories(db, opts) {
71
71
  const all = getMemories(db);
72
72
  const filtered = opts.filterCategories
73
- ? all.filter(m => opts.filterCategories.includes(m.category))
73
+ ? all.filter((m) => opts.filterCategories.includes(m.category))
74
74
  : all;
75
75
  if (filtered.length === 0)
76
76
  return null;
77
- const lines = filtered.map(m => formatMemoryLine(m, opts.showCategory));
77
+ const lines = filtered.map((m) => formatMemoryLine(m, opts.showCategory));
78
78
  return `## ${opts.header}\n${lines.join("\n")}`;
79
79
  }
80
80
  export function renderScope(opts) {
@@ -83,14 +83,12 @@ export function renderScope(opts) {
83
83
  `- account: ${opts.accountId ?? "all"}`,
84
84
  `- from: ${opts.from ?? "all time"}`,
85
85
  `- to: ${opts.to ?? "now"}`,
86
- `- dry run: ${opts.dryRun
87
- ? "yes — write tools will not mutate the DB"
88
- : "no — write tools will mutate the DB after confirmation"}`,
89
86
  ].join("\n");
90
87
  }
91
88
  /** Chat user context */
92
89
  export function renderUserContext(name, contextMd) {
93
- const body = contextMd ?? `(No personal context on file yet. ${name} can edit ~/.plasalid/context.md to add family, income, or other facts.)`;
90
+ const body = contextMd ??
91
+ `(No personal context on file yet. ${name} can edit ~/.plasalid/context.md to add family, income, or other facts.)`;
94
92
  return `## About ${name}\n${body}`;
95
93
  }
96
94
  /** Internal formatters */
@@ -98,7 +96,9 @@ function formatAccountRow(a, withBalance, depth = 0) {
98
96
  const indent = " ".repeat(depth);
99
97
  const subtype = a.subtype ? `/${a.subtype}` : "";
100
98
  const base = `- ${indent}${a.id} | ${a.name} | ${a.type}${subtype}`;
101
- return withBalance ? `${base} | balance ${a.balance.toFixed(2)} ${a.currency}` : base;
99
+ return withBalance
100
+ ? `${base} | balance ${a.balance.toFixed(2)} ${a.currency}`
101
+ : base;
102
102
  }
103
103
  function formatMemoryLine(m, showCategory) {
104
104
  return showCategory
@@ -1,4 +1,5 @@
1
1
  import Anthropic from "@anthropic-ai/sdk";
2
+ import { classifyProviderError } from "../errors.js";
2
3
  export function createAnthropicProvider(opts) {
3
4
  const client = new Anthropic(opts.baseURL
4
5
  ? { apiKey: opts.apiKey, baseURL: opts.baseURL }
@@ -17,10 +18,15 @@ export function createAnthropicProvider(opts) {
17
18
  if (params.thinking) {
18
19
  apiParams.thinking = params.thinking;
19
20
  }
20
- const response = await client.messages.create(apiParams, {
21
- signal: params.signal,
22
- });
23
- // Filter thinking blocks and normalize content
21
+ let response;
22
+ try {
23
+ response = await client.messages.create(apiParams, {
24
+ signal: params.signal,
25
+ });
26
+ }
27
+ catch (e) {
28
+ classifyProviderError(e, params.signal);
29
+ }
24
30
  const content = [];
25
31
  for (const block of response.content) {
26
32
  if (block.type === "thinking")
@@ -1,4 +1,30 @@
1
1
  import OpenAI from "openai";
2
+ import { classifyProviderError } from "../errors.js";
3
+ function isMaxTokensRejection(e) {
4
+ const err = e;
5
+ return err.status === 400 && (err.message?.includes("max_tokens") ?? false);
6
+ }
7
+ /**
8
+ * Some OpenAI-compatible endpoints (older models, Ollama, vLLM) accept `max_tokens`;
9
+ * newer OpenAI models require `max_completion_tokens`. Try the former, fall back on a
10
+ * 400 that explicitly names the parameter.
11
+ */
12
+ async function createCompletionWithTokenFallback(client, body, options) {
13
+ const base = {
14
+ model: body.model,
15
+ messages: body.messages,
16
+ tools: body.tools,
17
+ };
18
+ try {
19
+ return await client.chat.completions.create({ ...base, max_tokens: body.maxTokens }, options);
20
+ }
21
+ catch (e) {
22
+ if (isMaxTokensRejection(e)) {
23
+ return await client.chat.completions.create({ ...base, max_completion_tokens: body.maxTokens }, options);
24
+ }
25
+ throw e;
26
+ }
27
+ }
2
28
  export function createOpenAICompatibleProvider(opts) {
3
29
  const client = new OpenAI({
4
30
  apiKey: opts.apiKey,
@@ -8,63 +34,54 @@ export function createOpenAICompatibleProvider(opts) {
8
34
  name: "openai-compatible",
9
35
  supportsThinking: false,
10
36
  async sendMessage(params) {
11
- const messages = convertMessages(params.system, params.messages);
12
37
  const tools = convertTools(params.tools);
13
- // Try max_tokens first (broadest compat: Ollama, vLLM, older OpenAI models),
14
- // fall back to max_completion_tokens if rejected (newer OpenAI models require it)
38
+ const body = {
39
+ model: params.model,
40
+ maxTokens: params.maxTokens,
41
+ messages: convertMessages(params.system, params.messages),
42
+ tools: tools.length > 0 ? tools : undefined,
43
+ };
15
44
  let response;
16
45
  try {
17
- response = await client.chat.completions.create({
18
- model: params.model,
19
- max_tokens: params.maxTokens,
20
- messages,
21
- tools: tools.length > 0 ? tools : undefined,
22
- }, { signal: params.signal });
46
+ response = await createCompletionWithTokenFallback(client, body, { signal: params.signal });
23
47
  }
24
48
  catch (e) {
25
- if (e.status === 400 && e.message?.includes("max_tokens")) {
26
- response = await client.chat.completions.create({
27
- model: params.model,
28
- max_completion_tokens: params.maxTokens,
29
- messages,
30
- tools: tools.length > 0 ? tools : undefined,
31
- }, { signal: params.signal });
32
- }
33
- else {
34
- throw e;
35
- }
36
- }
37
- const choice = response.choices[0];
38
- if (!choice) {
39
- return { content: [], stopReason: "end_turn" };
40
- }
41
- const content = [];
42
- if (choice.message.content) {
43
- content.push({ type: "text", text: choice.message.content });
49
+ classifyProviderError(e, params.signal);
44
50
  }
45
- if (choice.message.tool_calls) {
46
- for (const tc of choice.message.tool_calls) {
47
- if (tc.type !== "function")
48
- continue;
49
- content.push({
50
- type: "tool_use",
51
- id: tc.id,
52
- name: tc.function.name,
53
- input: parseArguments(tc.function.arguments),
54
- });
55
- }
56
- }
57
- const hasToolCalls = content.some((b) => b.type === "tool_use");
58
- return {
59
- content,
60
- stopReason: hasToolCalls ? "tool_use" : "end_turn",
61
- usage: response.usage
62
- ? { input_tokens: response.usage.prompt_tokens, output_tokens: response.usage.completion_tokens }
63
- : undefined,
64
- };
51
+ return normalizeResponse(response);
65
52
  },
66
53
  };
67
54
  }
55
+ function normalizeResponse(response) {
56
+ const choice = response.choices[0];
57
+ if (!choice) {
58
+ return { content: [], stopReason: "end_turn" };
59
+ }
60
+ const content = [];
61
+ if (choice.message.content) {
62
+ content.push({ type: "text", text: choice.message.content });
63
+ }
64
+ if (choice.message.tool_calls) {
65
+ for (const tc of choice.message.tool_calls) {
66
+ if (tc.type !== "function")
67
+ continue;
68
+ content.push({
69
+ type: "tool_use",
70
+ id: tc.id,
71
+ name: tc.function.name,
72
+ input: parseArguments(tc.function.arguments),
73
+ });
74
+ }
75
+ }
76
+ const hasToolCalls = content.some((b) => b.type === "tool_use");
77
+ return {
78
+ content,
79
+ stopReason: hasToolCalls ? "tool_use" : "end_turn",
80
+ usage: response.usage
81
+ ? { input_tokens: response.usage.prompt_tokens, output_tokens: response.usage.completion_tokens }
82
+ : undefined,
83
+ };
84
+ }
68
85
  function convertMessages(system, messages) {
69
86
  const result = [
70
87
  { role: "system", content: system },
@@ -104,14 +121,11 @@ function convertMessages(system, messages) {
104
121
  .join("\n");
105
122
  const toolCalls = blocks
106
123
  .filter((b) => b.type === "tool_use")
107
- .map((b) => {
108
- const tu = b;
109
- return {
110
- id: tu.id,
111
- type: "function",
112
- function: { name: tu.name, arguments: JSON.stringify(tu.input) },
113
- };
114
- });
124
+ .map((tu) => ({
125
+ id: tu.id,
126
+ type: "function",
127
+ function: { name: tu.name, arguments: JSON.stringify(tu.input) },
128
+ }));
115
129
  result.push({
116
130
  role: "assistant",
117
131
  content: textParts || null,
@@ -1,75 +1,101 @@
1
1
  import { config } from "../config.js";
2
2
  import { readContext } from "./context.js";
3
+ const SECTION_RULES = [
4
+ {
5
+ heading: "Family",
6
+ token: "[PARTNER]",
7
+ stripParen: true,
8
+ skipIfUser: true,
9
+ patterns: [
10
+ /^(?:partner|spouse|wife|husband|child|kid|son|daughter|dependent)[:\s]+(.+)/i,
11
+ /^([\p{Lu}\p{Lo}][\p{L}\s]+)/u,
12
+ ],
13
+ },
14
+ {
15
+ heading: "Income",
16
+ token: "[EMPLOYER]",
17
+ patterns: [
18
+ /(?:employer|works? (?:at|for)|employed (?:at|by))[:\s]+([A-Z][\w\s&.,-]+?)(?:\s*[-–—|,;(\n]|$)/i,
19
+ /\bfrom ([A-Z][A-Za-z\s&.,-]+?)(?:\s*[-–—|,;(\n]|$)/,
20
+ /\bat ([A-Z][A-Za-z\s&.,-]+?)(?:\s*[-–—|,;(\n]|$)/,
21
+ ],
22
+ },
23
+ ];
24
+ // Patterns for numeric / identifier PII commonly found in Thai financial data.
25
+ const NUMERIC_PII_PATTERNS = [
26
+ // Thai national ID with dashes: 1-2345-67890-12-3
27
+ [/\b\d-\d{4}-\d{5}-\d{2}-\d\b/g, "[NATID]"],
28
+ // Thai national ID without dashes (13 digits) — must precede the generic ACCT pattern.
29
+ [/\b\d{13}\b/g, "[NATID]"],
30
+ // Thai mobile numbers: 0[689]xxxxxxxx (10 digits starting 06/08/09)
31
+ [/\b0[689]\d{8}\b/g, "[PHONE]"],
32
+ // 16-digit credit card (with optional separators)
33
+ [/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, "[CARD]"],
34
+ // 10–12 digit account / routing numbers at a word boundary
35
+ [/\b\d{10,12}\b(?=\s|$|[,.])/g, "[ACCT]"],
36
+ ];
37
+ function extractSectionLines(context, heading) {
38
+ const re = new RegExp(`## ${heading}\\n([\\s\\S]*?)(?=\\n##|$)`);
39
+ const match = context.match(re);
40
+ if (!match)
41
+ return [];
42
+ return match[1]
43
+ .split("\n")
44
+ .filter((l) => l.trim().startsWith("-"))
45
+ .map((l) => l.replace(/^-\s*/, "").trim())
46
+ .filter((text) => text.length > 0 && !text.startsWith("("));
47
+ }
48
+ function applyRule(rule, context, userName, push) {
49
+ for (const line of extractSectionLines(context, rule.heading)) {
50
+ if (rule.skipIfUser && line.toLowerCase() === userName.toLowerCase())
51
+ continue;
52
+ for (const pattern of rule.patterns) {
53
+ const match = line.match(pattern);
54
+ if (!match)
55
+ continue;
56
+ let name = match[1].trim();
57
+ if (rule.stripParen)
58
+ name = name.replace(/\s*\(.*\)/, "").trim();
59
+ if (!name)
60
+ break;
61
+ if (rule.skipIfUser && name.toLowerCase() === userName.toLowerCase())
62
+ break;
63
+ push(name, rule.token);
64
+ break;
65
+ }
66
+ }
67
+ }
3
68
  function buildRedactions() {
4
69
  const entries = [];
5
70
  const seen = new Set();
6
- function add(real, token) {
71
+ const push = (real, token) => {
7
72
  const trimmed = real.trim();
8
- if (trimmed.length < 2 || seen.has(trimmed.toLowerCase()))
73
+ if (trimmed.length < 2)
74
+ return;
75
+ const key = trimmed.toLowerCase();
76
+ if (seen.has(key))
9
77
  return;
10
- seen.add(trimmed.toLowerCase());
78
+ seen.add(key);
11
79
  entries.push({ real: trimmed, token });
12
- }
80
+ };
13
81
  const userName = config.userName;
14
82
  if (userName && userName !== "User") {
15
- add(userName, "[USER]");
83
+ push(userName, "[USER]");
16
84
  const parts = userName.split(/\s+/);
17
85
  if (parts.length > 1) {
18
- add(parts[0], "[USER_FIRST]");
19
- add(parts[parts.length - 1], "[USER_LAST]");
86
+ push(parts[0], "[USER_FIRST]");
87
+ push(parts[parts.length - 1], "[USER_LAST]");
20
88
  }
21
89
  }
22
90
  const context = readContext();
23
91
  if (context) {
24
- const familyMatch = context.match(/## Family\n([\s\S]*?)(?=\n##|$)/);
25
- if (familyMatch) {
26
- const lines = familyMatch[1].split("\n").filter(l => l.trim().startsWith("-"));
27
- for (const line of lines) {
28
- const text = line.replace(/^-\s*/, "").trim();
29
- if (!text || text.startsWith("(") || text.toLowerCase() === userName.toLowerCase())
30
- continue;
31
- const nameMatch = text.match(/^(?:partner|spouse|wife|husband|child|kid|son|daughter|dependent)[:\s]+(.+)/i)
32
- || text.match(/^([\p{Lu}\p{Lo}][\p{L}\s]+)/u);
33
- if (nameMatch) {
34
- const name = nameMatch[1].replace(/\s*\(.*\)/, "").trim();
35
- if (name && name.toLowerCase() !== userName.toLowerCase()) {
36
- add(name, "[PARTNER]");
37
- }
38
- }
39
- }
40
- }
41
- const incomeMatch = context.match(/## Income\n([\s\S]*?)(?=\n##|$)/);
42
- if (incomeMatch) {
43
- const lines = incomeMatch[1].split("\n").filter(l => l.trim().startsWith("-"));
44
- for (const line of lines) {
45
- const text = line.replace(/^-\s*/, "").trim();
46
- if (!text || text.startsWith("("))
47
- continue;
48
- const employerMatch = text.match(/(?:employer|works? (?:at|for)|employed (?:at|by))[:\s]+([A-Z][\w\s&.,-]+?)(?:\s*[-–—|,;(\n]|$)/i)
49
- || text.match(/\bfrom ([A-Z][A-Za-z\s&.,-]+?)(?:\s*[-–—|,;(\n]|$)/)
50
- || text.match(/\bat ([A-Z][A-Za-z\s&.,-]+?)(?:\s*[-–—|,;(\n]|$)/);
51
- if (employerMatch) {
52
- add(employerMatch[1].trim(), "[EMPLOYER]");
53
- }
54
- }
92
+ for (const rule of SECTION_RULES) {
93
+ applyRule(rule, context, userName, push);
55
94
  }
56
95
  }
57
96
  entries.sort((a, b) => b.real.length - a.real.length);
58
97
  return entries;
59
98
  }
60
- // Patterns for numeric / identifier PII commonly found in Thai financial data.
61
- const NUMERIC_PII_PATTERNS = [
62
- // Thai national ID with dashes: 1-2345-67890-12-3
63
- [/\b\d-\d{4}-\d{5}-\d{2}-\d\b/g, "[NATID]"],
64
- // Thai national ID without dashes (13 digits) — must precede the generic ACCT pattern.
65
- [/\b\d{13}\b/g, "[NATID]"],
66
- // Thai mobile numbers: 0[689]xxxxxxxx (10 digits starting 06/08/09)
67
- [/\b0[689]\d{8}\b/g, "[PHONE]"],
68
- // 16-digit credit card (with optional separators)
69
- [/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, "[CARD]"],
70
- // 10–12 digit account / routing numbers at a word boundary
71
- [/\b\d{10,12}\b(?=\s|$|[,.])/g, "[ACCT]"],
72
- ];
73
99
  export function redact(text) {
74
100
  const redactions = buildRedactions();
75
101
  let result = text;
@@ -2,11 +2,10 @@ import type Database from "libsql";
2
2
  export interface ScanPromptOptions {
3
3
  fileName: string;
4
4
  }
5
- export interface ReviewPromptOptions {
5
+ export interface ResolvePromptOptions {
6
6
  accountId?: string;
7
7
  from?: string;
8
8
  to?: string;
9
- dryRun: boolean;
10
9
  }
11
10
  export interface RecordPromptOptions {
12
11
  utterance: string;
@@ -19,6 +18,6 @@ export interface RecordPromptOptions {
19
18
  * shuffle the array.
20
19
  */
21
20
  export declare function buildChatSystemPrompt(db: Database.Database): string;
22
- export declare function buildReviewSystemPrompt(db: Database.Database, opts: ReviewPromptOptions): string;
21
+ export declare function buildResolveSystemPrompt(db: Database.Database, opts: ResolvePromptOptions): string;
23
22
  export declare function buildRecordSystemPrompt(db: Database.Database, opts: RecordPromptOptions): string;
24
23
  export declare function buildScanSystemPrompt(db: Database.Database, opts: ScanPromptOptions): string;
@@ -1,6 +1,6 @@
1
1
  import { config } from "../config.js";
2
2
  import { readContext } from "./context.js";
3
- import { chatPersona, SCAN_PERSONA, REVIEW_PERSONA, RECORD_PERSONA } from "./personas.js";
3
+ import { chatPersona, SCAN_PERSONA, RESOLVE_PERSONA, RECORD_PERSONA } from "./personas.js";
4
4
  import { getThaiTaxonomyHint } from "../accounts/taxonomy.js";
5
5
  import { renderChartOfAccounts, renderChatChartOrEmpty, renderMemories, renderScope, renderTodayHuman, renderTodayIso, renderUserContext, } from "./prompt-sections.js";
6
6
  /**
@@ -23,11 +23,11 @@ export function buildChatSystemPrompt(db) {
23
23
  }),
24
24
  ]);
25
25
  }
26
- export function buildReviewSystemPrompt(db, opts) {
26
+ export function buildResolveSystemPrompt(db, opts) {
27
27
  return joinSections([
28
- REVIEW_PERSONA,
28
+ RESOLVE_PERSONA,
29
29
  renderTodayIso(),
30
- renderChartOfAccounts(db, { withBalance: true, emptyState: "review" }),
30
+ renderChartOfAccounts(db, { withBalance: true, emptyState: "resolve" }),
31
31
  renderScope(opts),
32
32
  renderMemories(db, {
33
33
  header: "Rules you've already learned (apply directly; do not re-ask the user)",
@@ -56,7 +56,7 @@ export function buildScanSystemPrompt(db, opts) {
56
56
  `## File context\nFile: ${opts.fileName}`,
57
57
  `## Taxonomy hints\n${getThaiTaxonomyHint()}`,
58
58
  renderMemories(db, {
59
- header: "Rules you've already learned (apply silently before raising a concern)",
59
+ header: "Rules you've already learned (apply silently before raising an unknown)",
60
60
  filterCategories: ["scanning_hint", "general"],
61
61
  showCategory: false,
62
62
  }),
@@ -1,5 +1,5 @@
1
1
  import { saveMemory, getMemories } from "../memory.js";
2
- import { getAccountBalances } from "../../db/queries/account_balance.js";
2
+ import { getAccountBalances } from "../../db/queries/account-balance.js";
3
3
  import { formatAmount } from "../../currency.js";
4
4
  import { sanitizeForPrompt, sanitizeForPromptCell } from "../sanitize.js";
5
5
  import { ACCOUNT_TYPE_DESCRIPTIONS } from "../../accounts/taxonomy.js";
@@ -11,7 +11,11 @@ const DEFS = [
11
11
  input_schema: {
12
12
  type: "object",
13
13
  properties: {
14
- type: { type: "string", enum: ACCOUNT_TYPES, description: "Filter by account type." },
14
+ type: {
15
+ type: "string",
16
+ enum: ACCOUNT_TYPES,
17
+ description: "Filter by account type.",
18
+ },
15
19
  },
16
20
  required: [],
17
21
  },
@@ -23,7 +27,11 @@ const DEFS = [
23
27
  type: "object",
24
28
  properties: {
25
29
  content: { type: "string", description: "What to remember." },
26
- category: { type: "string", description: "Category: general, scanning_hint, preference, life_event.", default: "general" },
30
+ category: {
31
+ type: "string",
32
+ description: "Category: general, scanning_hint, preference, life_event.",
33
+ default: "general",
34
+ },
27
35
  },
28
36
  required: ["content"],
29
37
  },
@@ -46,7 +54,7 @@ async function execute(db, name, input, _ctx) {
46
54
  if (accounts.length === 0)
47
55
  return "No accounts in the chart of accounts yet.";
48
56
  return accounts
49
- .map(a => {
57
+ .map((a) => {
50
58
  const meta = [];
51
59
  if (a.bank_name)
52
60
  meta.push(sanitizeForPrompt(a.bank_name));
@@ -70,7 +78,7 @@ async function execute(db, name, input, _ctx) {
70
78
  if (memories.length === 0)
71
79
  return "No memories saved yet.";
72
80
  return memories
73
- .map(m => `[${m.category}] ${sanitizeForPrompt(m.content)} (saved ${m.created_at})`)
81
+ .map((m) => `[${m.category}] ${sanitizeForPrompt(m.content)} (saved ${m.created_at})`)
74
82
  .join("\n");
75
83
  }
76
84
  default:
@@ -1,8 +1,8 @@
1
1
  import { commonTools } from "./common.js";
2
2
  import { readTools } from "./read.js";
3
- import { accountIngestTools, scanConcernTools, reviewIngestTools } from "./ingest.js";
3
+ import { accountIngestTools, scanUnknownTools, resolveIngestTools } from "./ingest.js";
4
4
  import { scanTools } from "./scan.js";
5
- import { reviewTools } from "./review.js";
5
+ import { resolveTools } from "./resolve.js";
6
6
  import { recordTools } from "./record.js";
7
7
  import { merchantTools } from "./merchants.js";
8
8
  /**
@@ -11,17 +11,17 @@ import { merchantTools } from "./merchants.js";
11
11
  * central switch.
12
12
  *
13
13
  * `accountIngestTools` (create_account / update_account_metadata /
14
- * record_transaction) ships with scan, review, and record — they're the
15
- * shared write primitives. `scanConcernTools` (note_concern) is scan-only;
16
- * record uses `clarify` from `recordTools` for transient prompts, review uses
17
- * `ask_user` from `reviewIngestTools` for resolve-in-place clarifications.
18
- * `merchantTools` ships with scan, review, and record so any write profile can
14
+ * record_transaction) ships with scan, resolve, and record — they're the
15
+ * shared write primitives. `scanUnknownTools` (note_unknown) is scan-only;
16
+ * record uses `clarify` from `recordTools` for transient prompts, resolve uses
17
+ * `ask_user` from `resolveIngestTools` for resolve-in-place clarifications.
18
+ * `merchantTools` ships with scan, resolve, and record so any write profile can
19
19
  * upsert / look up / re-cache merchants alongside the posting flow.
20
20
  */
21
21
  const PROFILES = {
22
- scan: [commonTools, accountIngestTools, scanConcernTools, scanTools, merchantTools],
22
+ scan: [commonTools, accountIngestTools, scanUnknownTools, scanTools, merchantTools],
23
23
  chat: [commonTools, readTools],
24
- review: [commonTools, readTools, accountIngestTools, reviewIngestTools, reviewTools, merchantTools],
24
+ resolve: [commonTools, readTools, accountIngestTools, resolveIngestTools, resolveTools, merchantTools],
25
25
  record: [commonTools, readTools, accountIngestTools, recordTools, merchantTools],
26
26
  };
27
27
  export function getToolDefinitions(profile) {
@@ -32,10 +32,10 @@ export async function executeTool(db, name, input, ctx) {
32
32
  commonTools,
33
33
  readTools,
34
34
  accountIngestTools,
35
- scanConcernTools,
36
- reviewIngestTools,
35
+ scanUnknownTools,
36
+ resolveIngestTools,
37
37
  scanTools,
38
- reviewTools,
38
+ resolveTools,
39
39
  recordTools,
40
40
  merchantTools,
41
41
  ]) {
@@ -50,10 +50,10 @@ export const TOOL_LABELS = {
50
50
  ...commonTools.LABELS,
51
51
  ...readTools.LABELS,
52
52
  ...accountIngestTools.LABELS,
53
- ...scanConcernTools.LABELS,
54
- ...reviewIngestTools.LABELS,
53
+ ...scanUnknownTools.LABELS,
54
+ ...resolveIngestTools.LABELS,
55
55
  ...scanTools.LABELS,
56
- ...reviewTools.LABELS,
56
+ ...resolveTools.LABELS,
57
57
  ...recordTools.LABELS,
58
58
  ...merchantTools.LABELS,
59
59
  };
@@ -1,4 +1,4 @@
1
1
  import type { ToolModule } from "./types.js";
2
2
  export declare const accountIngestTools: ToolModule;
3
- export declare const scanConcernTools: ToolModule;
4
- export declare const reviewIngestTools: ToolModule;
3
+ export declare const scanUnknownTools: ToolModule;
4
+ export declare const resolveIngestTools: ToolModule;