ray-finance 0.2.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/.claude/settings.local.json +16 -0
- package/.env.example +13 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +19 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +9 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +5 -0
- package/.github/workflows/ci.yml +21 -0
- package/CHANGELOG.md +16 -0
- package/CODE_OF_CONDUCT.md +31 -0
- package/CONTRIBUTING.md +41 -0
- package/Dockerfile +8 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/SECURITY.md +36 -0
- package/SPEC.md +374 -0
- package/dist/ai/agent.d.ts +2 -0
- package/dist/ai/agent.js +80 -0
- package/dist/ai/audit.d.ts +3 -0
- package/dist/ai/audit.js +6 -0
- package/dist/ai/context.d.ts +6 -0
- package/dist/ai/context.js +89 -0
- package/dist/ai/insights.d.ts +3 -0
- package/dist/ai/insights.js +378 -0
- package/dist/ai/memory.d.ts +14 -0
- package/dist/ai/memory.js +12 -0
- package/dist/ai/redactor.d.ts +2 -0
- package/dist/ai/redactor.js +92 -0
- package/dist/ai/system-prompt.d.ts +2 -0
- package/dist/ai/system-prompt.js +85 -0
- package/dist/ai/tools.d.ts +4 -0
- package/dist/ai/tools.js +695 -0
- package/dist/alerts/index.d.ts +11 -0
- package/dist/alerts/index.js +95 -0
- package/dist/auth/anthropic.d.ts +7 -0
- package/dist/auth/anthropic.js +85 -0
- package/dist/auth/pkce.d.ts +5 -0
- package/dist/auth/pkce.js +10 -0
- package/dist/auth/store.d.ts +12 -0
- package/dist/auth/store.js +51 -0
- package/dist/cli/backup.d.ts +2 -0
- package/dist/cli/backup.js +85 -0
- package/dist/cli/chat.d.ts +1 -0
- package/dist/cli/chat.js +97 -0
- package/dist/cli/commands.d.ts +13 -0
- package/dist/cli/commands.js +201 -0
- package/dist/cli/format.d.ts +12 -0
- package/dist/cli/format.js +119 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +176 -0
- package/dist/cli/scheduler.d.ts +2 -0
- package/dist/cli/scheduler.js +114 -0
- package/dist/cli/setup.d.ts +1 -0
- package/dist/cli/setup.js +168 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +60 -0
- package/dist/daily-sync.d.ts +7 -0
- package/dist/daily-sync.js +94 -0
- package/dist/db/connection.d.ts +5 -0
- package/dist/db/connection.js +37 -0
- package/dist/db/encryption.d.ts +3 -0
- package/dist/db/encryption.js +24 -0
- package/dist/db/helpers.d.ts +16 -0
- package/dist/db/helpers.js +45 -0
- package/dist/db/schema.d.ts +2 -0
- package/dist/db/schema.js +194 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plaid/client.d.ts +2 -0
- package/dist/plaid/client.js +22 -0
- package/dist/plaid/link.d.ts +8 -0
- package/dist/plaid/link.js +23 -0
- package/dist/plaid/sync.d.ts +18 -0
- package/dist/plaid/sync.js +186 -0
- package/dist/public/link.html +161 -0
- package/dist/queries/index.d.ts +163 -0
- package/dist/queries/index.js +411 -0
- package/dist/scoring/index.d.ts +53 -0
- package/dist/scoring/index.js +375 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +140 -0
- package/docker-compose.yml +9 -0
- package/package.json +55 -0
- package/site/next-env.d.ts +6 -0
- package/site/next.config.ts +7 -0
- package/site/package-lock.json +1661 -0
- package/site/package.json +24 -0
- package/site/postcss.config.mjs +7 -0
- package/site/public/favicon.png +0 -0
- package/site/public/ray-og.jpg +0 -0
- package/site/public/robots.txt +4 -0
- package/site/public/sitemap.xml +8 -0
- package/site/src/app/copy-command.tsx +30 -0
- package/site/src/app/globals.css +87 -0
- package/site/src/app/layout.tsx +64 -0
- package/site/src/app/page.tsx +841 -0
- package/site/src/app/pii-scramble.tsx +190 -0
- package/site/src/app/reveal.tsx +29 -0
- package/site/tsconfig.json +21 -0
- package/src/ai/agent.ts +106 -0
- package/src/ai/audit.ts +11 -0
- package/src/ai/context.ts +93 -0
- package/src/ai/insights.ts +474 -0
- package/src/ai/memory.ts +21 -0
- package/src/ai/redactor.ts +102 -0
- package/src/ai/system-prompt.ts +90 -0
- package/src/ai/tools.ts +716 -0
- package/src/alerts/index.ts +123 -0
- package/src/cli/backup.ts +113 -0
- package/src/cli/chat.ts +105 -0
- package/src/cli/commands.ts +240 -0
- package/src/cli/format.ts +149 -0
- package/src/cli/index.ts +193 -0
- package/src/cli/scheduler.ts +116 -0
- package/src/cli/setup.ts +189 -0
- package/src/config.ts +81 -0
- package/src/daily-sync.ts +155 -0
- package/src/db/connection.ts +38 -0
- package/src/db/encryption.ts +29 -0
- package/src/db/helpers.ts +47 -0
- package/src/db/schema.ts +196 -0
- package/src/index.ts +3 -0
- package/src/plaid/client.ts +25 -0
- package/src/plaid/link.ts +25 -0
- package/src/plaid/sync.ts +219 -0
- package/src/public/link.html +161 -0
- package/src/queries/index.ts +586 -0
- package/src/scoring/index.ts +468 -0
- package/src/server.ts +162 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { getNetWorth, getAccountBalances, getDebts, getBudgetStatuses, getGoals, compareSpending, formatMoney, categoryLabel, } from "../queries/index.js";
|
|
3
|
+
import { getLatestScore } from "../scoring/index.js";
|
|
4
|
+
const MAX_CHARS = 6000;
|
|
5
|
+
export function computeInsights(db) {
|
|
6
|
+
// Fresh install guard
|
|
7
|
+
const txCount = db.prepare(`SELECT COUNT(*) as cnt FROM transactions`).get();
|
|
8
|
+
if (txCount.cnt === 0) {
|
|
9
|
+
return `## Current Financial Briefing\nAccounts connected — run \`ray sync\` to pull transactions. No financial data to analyze yet.`;
|
|
10
|
+
}
|
|
11
|
+
const sections = [];
|
|
12
|
+
// 1. Financial Snapshot (priority 1 — always kept)
|
|
13
|
+
sections.push({ priority: 1, text: buildSnapshot(db) });
|
|
14
|
+
// 2. Spending Intelligence (priority 2 — always kept)
|
|
15
|
+
sections.push({ priority: 2, text: buildSpending(db) });
|
|
16
|
+
// 3. Goal & Savings Pace (priority 4)
|
|
17
|
+
const goals = buildGoals(db);
|
|
18
|
+
if (goals)
|
|
19
|
+
sections.push({ priority: 4, text: goals });
|
|
20
|
+
// 4. Upcoming & Proactive (priority 5)
|
|
21
|
+
const upcoming = buildUpcoming(db);
|
|
22
|
+
if (upcoming)
|
|
23
|
+
sections.push({ priority: 5, text: upcoming });
|
|
24
|
+
// 5. Anomaly Detection (priority 6 — nice to have)
|
|
25
|
+
const anomalies = buildAnomalies(db);
|
|
26
|
+
if (anomalies)
|
|
27
|
+
sections.push({ priority: 6, text: anomalies });
|
|
28
|
+
// 6. Behavioral Score (priority 7 — nice to have)
|
|
29
|
+
const score = buildScore(db);
|
|
30
|
+
if (score)
|
|
31
|
+
sections.push({ priority: 7, text: score });
|
|
32
|
+
// Token budget: drop lowest-priority sections first if over budget
|
|
33
|
+
sections.sort((a, b) => a.priority - b.priority);
|
|
34
|
+
let combined = "";
|
|
35
|
+
const included = [];
|
|
36
|
+
for (const s of sections) {
|
|
37
|
+
if ((combined + s.text).length > MAX_CHARS && s.priority > 2)
|
|
38
|
+
break;
|
|
39
|
+
included.push(s.text);
|
|
40
|
+
combined = included.join("\n\n");
|
|
41
|
+
}
|
|
42
|
+
return `## Current Financial Briefing (auto-generated)\n\n${combined}`;
|
|
43
|
+
}
|
|
44
|
+
function buildSnapshot(db) {
|
|
45
|
+
const nw = getNetWorth(db);
|
|
46
|
+
const lines = [];
|
|
47
|
+
let nwLine = `Net worth: ${formatMoney(nw.net_worth)}`;
|
|
48
|
+
if (nw.prev_net_worth !== null) {
|
|
49
|
+
const change = nw.net_worth - nw.prev_net_worth;
|
|
50
|
+
nwLine += ` (${change >= 0 ? "+" : "-"}${formatMoney(Math.abs(change))} today)`;
|
|
51
|
+
}
|
|
52
|
+
lines.push(nwLine);
|
|
53
|
+
// Account balances — cap at 5
|
|
54
|
+
const accounts = getAccountBalances(db).slice(0, 5);
|
|
55
|
+
if (accounts.length > 0) {
|
|
56
|
+
lines.push(accounts.map(a => `${a.name} (${a.type}): ${a.type === "credit" ? "-" : ""}${formatMoney(a.balance)}`).join(" | "));
|
|
57
|
+
}
|
|
58
|
+
// Debt summary
|
|
59
|
+
const debts = getDebts(db);
|
|
60
|
+
if (debts.totalDebt > 0) {
|
|
61
|
+
const rates = debts.debts.filter(d => d.rate > 0);
|
|
62
|
+
let debtLine = `Total debt: ${formatMoney(debts.totalDebt)}`;
|
|
63
|
+
if (rates.length > 0) {
|
|
64
|
+
const weightedRate = rates.reduce((s, d) => s + d.rate * d.balance, 0) / rates.reduce((s, d) => s + d.balance, 0);
|
|
65
|
+
debtLine += ` (avg ${weightedRate.toFixed(1)}% APR)`;
|
|
66
|
+
}
|
|
67
|
+
lines.push(debtLine);
|
|
68
|
+
}
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
function buildSpending(db) {
|
|
72
|
+
const now = new Date();
|
|
73
|
+
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
74
|
+
const today = now.toISOString().slice(0, 10);
|
|
75
|
+
const dayOfMonth = now.getDate();
|
|
76
|
+
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
77
|
+
const daysLeft = daysInMonth - dayOfMonth;
|
|
78
|
+
// This month's total spending
|
|
79
|
+
const thisMonthSpend = db.prepare(`SELECT COALESCE(SUM(amount), 0) as total FROM transactions
|
|
80
|
+
WHERE amount > 0 AND date BETWEEN ? AND ? AND pending = 0
|
|
81
|
+
AND category NOT IN ('TRANSFER_OUT', 'TRANSFER_IN', 'LOAN_PAYMENTS')`).get(monthStart.toISOString().slice(0, 10), today);
|
|
82
|
+
const lines = [];
|
|
83
|
+
let spendLine = `SPENDING: ${formatMoney(thisMonthSpend.total)} this month`;
|
|
84
|
+
// Compare to last month (same day-of-month)
|
|
85
|
+
const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
|
86
|
+
const lastMonthSameDay = new Date(now.getFullYear(), now.getMonth() - 1, Math.min(dayOfMonth, new Date(now.getFullYear(), now.getMonth(), 0).getDate()));
|
|
87
|
+
// Only compare if we have >30 days of history
|
|
88
|
+
const oldestTx = db.prepare(`SELECT MIN(date) as d FROM transactions`).get();
|
|
89
|
+
const hasEnoughHistory = oldestTx.d && (new Date(today).getTime() - new Date(oldestTx.d).getTime()) > 30 * 24 * 60 * 60 * 1000;
|
|
90
|
+
if (hasEnoughHistory) {
|
|
91
|
+
const cmp = compareSpending(db, lastMonthStart.toISOString().slice(0, 10), lastMonthSameDay.toISOString().slice(0, 10), monthStart.toISOString().slice(0, 10), today);
|
|
92
|
+
if (cmp.period1Total > 0) {
|
|
93
|
+
const pctStr = cmp.pctChange >= 0 ? `${cmp.pctChange}% above` : `${Math.abs(cmp.pctChange)}% below`;
|
|
94
|
+
spendLine += ` (${pctStr} last month`;
|
|
95
|
+
// Top driver
|
|
96
|
+
const topDrivers = cmp.categories.filter(c => c.diff > 0).slice(0, 3);
|
|
97
|
+
if (topDrivers.length > 0 && cmp.pctChange > 0) {
|
|
98
|
+
spendLine += `, driven by ${topDrivers.map(d => `${categoryLabel(d.category)} +${formatMoney(d.diff)}`).join(", ")}`;
|
|
99
|
+
}
|
|
100
|
+
spendLine += ")";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
lines.push(spendLine);
|
|
104
|
+
// Budget status
|
|
105
|
+
const budgets = getBudgetStatuses(db);
|
|
106
|
+
const alertBudgets = budgets.filter(b => b.pct_used >= 80).slice(0, 3);
|
|
107
|
+
for (const b of alertBudgets) {
|
|
108
|
+
if (b.over_budget) {
|
|
109
|
+
lines.push(`Budget OVER: ${categoryLabel(b.category)} — ${formatMoney(b.spent)} / ${formatMoney(b.budget)} (${formatMoney(Math.abs(b.remaining))} over)`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
lines.push(`Budget alert: ${categoryLabel(b.category)} at ${b.pct_used}% of limit with ${daysLeft} days remaining`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Daily discretionary remaining
|
|
116
|
+
if (daysLeft > 0) {
|
|
117
|
+
const totalBudget = budgets.reduce((s, b) => s + b.budget, 0);
|
|
118
|
+
if (totalBudget > 0) {
|
|
119
|
+
const totalRemaining = budgets.reduce((s, b) => s + Math.max(0, b.remaining), 0);
|
|
120
|
+
lines.push(`Daily discretionary budget remaining: ${formatMoney(totalRemaining / daysLeft)}/day`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return lines.join("\n");
|
|
124
|
+
}
|
|
125
|
+
function buildGoals(db) {
|
|
126
|
+
const goals = getGoals(db);
|
|
127
|
+
const active = goals.filter(g => g.progress_pct < 100);
|
|
128
|
+
if (active.length === 0)
|
|
129
|
+
return null;
|
|
130
|
+
const lines = active.slice(0, 3).map(g => {
|
|
131
|
+
let line = `${g.name}: ${formatMoney(g.current)} / ${formatMoney(g.target)} (${g.progress_pct}%)`;
|
|
132
|
+
if (g.target_date) {
|
|
133
|
+
line += ` — need ${formatMoney(g.monthly_needed)}/mo`;
|
|
134
|
+
}
|
|
135
|
+
return line;
|
|
136
|
+
});
|
|
137
|
+
return `GOALS: ${lines.join(" | ")}`;
|
|
138
|
+
}
|
|
139
|
+
function buildUpcoming(db) {
|
|
140
|
+
const parts = [];
|
|
141
|
+
// Recurring bills due in next 7 days
|
|
142
|
+
const now = new Date();
|
|
143
|
+
const todayDay = now.getDate();
|
|
144
|
+
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
145
|
+
const endDay = todayDay + 7;
|
|
146
|
+
let bills = [];
|
|
147
|
+
if (endDay <= daysInMonth) {
|
|
148
|
+
bills = db.prepare(`SELECT name, amount, day_of_month FROM recurring_bills WHERE day_of_month BETWEEN ? AND ?`).all(todayDay + 1, endDay);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// Wraparound: rest of this month + start of next
|
|
152
|
+
const thisMonthBills = db.prepare(`SELECT name, amount, day_of_month FROM recurring_bills WHERE day_of_month BETWEEN ? AND ?`).all(todayDay + 1, daysInMonth);
|
|
153
|
+
const nextMonthBills = db.prepare(`SELECT name, amount, day_of_month FROM recurring_bills WHERE day_of_month BETWEEN 1 AND ?`).all(endDay - daysInMonth);
|
|
154
|
+
bills = [...thisMonthBills, ...nextMonthBills];
|
|
155
|
+
}
|
|
156
|
+
// Also handle bills on day 31 in shorter months
|
|
157
|
+
if (daysInMonth < 31) {
|
|
158
|
+
const endOfMonthBills = db.prepare(`SELECT name, amount, day_of_month FROM recurring_bills WHERE day_of_month > ? AND day_of_month NOT IN (SELECT day_of_month FROM recurring_bills WHERE day_of_month BETWEEN ? AND ?)`).all(daysInMonth, todayDay + 1, Math.min(endDay, daysInMonth));
|
|
159
|
+
// These bills fall on the last day of the month
|
|
160
|
+
if (daysInMonth >= todayDay + 1 && daysInMonth <= endDay) {
|
|
161
|
+
bills.push(...endOfMonthBills);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (bills.length > 0) {
|
|
165
|
+
const billStrs = bills.slice(0, 5).map(b => {
|
|
166
|
+
const daysUntil = b.day_of_month > todayDay
|
|
167
|
+
? b.day_of_month - todayDay
|
|
168
|
+
: daysInMonth - todayDay + b.day_of_month;
|
|
169
|
+
return `${b.name} (${formatMoney(b.amount)}) due in ${daysUntil} days`;
|
|
170
|
+
});
|
|
171
|
+
parts.push(`UPCOMING: ${billStrs.join(", ")}`);
|
|
172
|
+
}
|
|
173
|
+
// Low balance warning
|
|
174
|
+
const avgMonthlyExpenses = db.prepare(`SELECT COALESCE(SUM(amount), 0) / 3.0 as avg FROM transactions
|
|
175
|
+
WHERE amount > 0 AND date > date('now', '-90 days') AND pending = 0
|
|
176
|
+
AND category NOT IN ('TRANSFER_OUT', 'TRANSFER_IN')`).get();
|
|
177
|
+
const lowAccounts = db.prepare(`SELECT name, current_balance FROM accounts WHERE type = 'depository' AND current_balance < ?`).all(avgMonthlyExpenses.avg);
|
|
178
|
+
for (const a of lowAccounts.slice(0, 2)) {
|
|
179
|
+
parts.push(`LOW BALANCE: ${a.name} at ${formatMoney(a.current_balance)} (below 1 month of avg expenses)`);
|
|
180
|
+
}
|
|
181
|
+
// Credit utilization
|
|
182
|
+
const creditCards = db.prepare(`SELECT name, current_balance, available_balance FROM accounts
|
|
183
|
+
WHERE type = 'credit' AND current_balance > 0 AND available_balance IS NOT NULL`).all();
|
|
184
|
+
for (const card of creditCards) {
|
|
185
|
+
const limit = card.current_balance + card.available_balance;
|
|
186
|
+
if (limit > 0) {
|
|
187
|
+
const utilization = card.current_balance / limit;
|
|
188
|
+
if (utilization > 0.3) {
|
|
189
|
+
parts.push(`CREDIT: ${card.name} at ${Math.round(utilization * 100)}% utilization (${formatMoney(card.current_balance)} / ${formatMoney(limit)} limit)`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
194
|
+
}
|
|
195
|
+
function buildAnomalies(db) {
|
|
196
|
+
const parts = [];
|
|
197
|
+
// Large transactions in last 7 days (>$200)
|
|
198
|
+
const largeTx = db.prepare(`SELECT name, merchant_name, amount, date FROM transactions
|
|
199
|
+
WHERE amount > 200 AND date > date('now', '-7 days') AND pending = 0
|
|
200
|
+
AND category NOT IN ('TRANSFER_OUT', 'TRANSFER_IN', 'LOAN_PAYMENTS', 'RENT_AND_UTILITIES')
|
|
201
|
+
ORDER BY amount DESC LIMIT 3`).all();
|
|
202
|
+
for (const tx of largeTx) {
|
|
203
|
+
parts.push(`Large charge: ${formatMoney(tx.amount)} at ${tx.merchant_name || tx.name} (${tx.date})`);
|
|
204
|
+
}
|
|
205
|
+
// Spending velocity (only after day 5)
|
|
206
|
+
const now = new Date();
|
|
207
|
+
const dayOfMonth = now.getDate();
|
|
208
|
+
if (dayOfMonth >= 5) {
|
|
209
|
+
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10);
|
|
210
|
+
const today = now.toISOString().slice(0, 10);
|
|
211
|
+
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
212
|
+
const thisMonth = db.prepare(`SELECT COALESCE(SUM(amount), 0) as total FROM transactions
|
|
213
|
+
WHERE amount > 0 AND date BETWEEN ? AND ? AND pending = 0
|
|
214
|
+
AND category NOT IN ('TRANSFER_OUT', 'TRANSFER_IN', 'LOAN_PAYMENTS')`).get(monthStart, today);
|
|
215
|
+
const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1).toISOString().slice(0, 10);
|
|
216
|
+
const lastMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0).toISOString().slice(0, 10);
|
|
217
|
+
const lastMonth = db.prepare(`SELECT COALESCE(SUM(amount), 0) as total FROM transactions
|
|
218
|
+
WHERE amount > 0 AND date BETWEEN ? AND ? AND pending = 0
|
|
219
|
+
AND category NOT IN ('TRANSFER_OUT', 'TRANSFER_IN', 'LOAN_PAYMENTS')`).get(lastMonthStart, lastMonthEnd);
|
|
220
|
+
if (lastMonth.total > 0) {
|
|
221
|
+
const projected = (thisMonth.total / dayOfMonth) * daysInMonth;
|
|
222
|
+
const velocity = projected / lastMonth.total;
|
|
223
|
+
if (velocity > 1.2) {
|
|
224
|
+
parts.push(`PACE ALERT: On track to spend ${formatMoney(projected)} this month (${Math.round((velocity - 1) * 100)}% above last month's ${formatMoney(lastMonth.total)})`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
229
|
+
}
|
|
230
|
+
// ─── CLI Briefing (colored, for terminal display on launch) ─── //
|
|
231
|
+
export function cliBriefing(db) {
|
|
232
|
+
const txCount = db.prepare(`SELECT COUNT(*) as cnt FROM transactions`).get();
|
|
233
|
+
if (txCount.cnt === 0)
|
|
234
|
+
return null;
|
|
235
|
+
const now = new Date();
|
|
236
|
+
const dayOfMonth = now.getDate();
|
|
237
|
+
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
238
|
+
const daysLeft = daysInMonth - dayOfMonth;
|
|
239
|
+
const lines = [];
|
|
240
|
+
// Net worth headline
|
|
241
|
+
const nw = getNetWorth(db);
|
|
242
|
+
let nwLine = chalk.white(` ${fmtMoney(nw.net_worth)}`);
|
|
243
|
+
if (nw.prev_net_worth !== null) {
|
|
244
|
+
const change = nw.net_worth - nw.prev_net_worth;
|
|
245
|
+
nwLine += change >= 0
|
|
246
|
+
? chalk.green(` +${fmtMoney(change)}`)
|
|
247
|
+
: chalk.red(` -${fmtMoney(Math.abs(change))}`);
|
|
248
|
+
}
|
|
249
|
+
lines.push(chalk.dim(" net worth") + nwLine);
|
|
250
|
+
lines.push("");
|
|
251
|
+
// Spending vs last month
|
|
252
|
+
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
253
|
+
const today = now.toISOString().slice(0, 10);
|
|
254
|
+
const thisMonthSpend = db.prepare(`SELECT COALESCE(SUM(amount), 0) as total FROM transactions
|
|
255
|
+
WHERE amount > 0 AND date BETWEEN ? AND ? AND pending = 0
|
|
256
|
+
AND category NOT IN ('TRANSFER_OUT', 'TRANSFER_IN', 'LOAN_PAYMENTS')`).get(monthStart.toISOString().slice(0, 10), today);
|
|
257
|
+
const oldestTx = db.prepare(`SELECT MIN(date) as d FROM transactions`).get();
|
|
258
|
+
const hasHistory = oldestTx.d && (new Date(today).getTime() - new Date(oldestTx.d).getTime()) > 30 * 24 * 60 * 60 * 1000;
|
|
259
|
+
if (hasHistory) {
|
|
260
|
+
const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
|
261
|
+
const lastMonthSameDay = new Date(now.getFullYear(), now.getMonth() - 1, Math.min(dayOfMonth, new Date(now.getFullYear(), now.getMonth(), 0).getDate()));
|
|
262
|
+
const cmp = compareSpending(db, lastMonthStart.toISOString().slice(0, 10), lastMonthSameDay.toISOString().slice(0, 10), monthStart.toISOString().slice(0, 10), today);
|
|
263
|
+
if (cmp.period1Total > 0) {
|
|
264
|
+
const diff = cmp.period2Total - cmp.period1Total;
|
|
265
|
+
const arrow = diff <= 0 ? chalk.green(`${fmtMoney(Math.abs(diff))} less`) : chalk.red(`${fmtMoney(diff)} more`);
|
|
266
|
+
lines.push(chalk.dim(" spending") + chalk.white(` ${fmtMoney(thisMonthSpend.total)} this month`) + chalk.dim(` · `) + arrow + chalk.dim(` than this point last month`));
|
|
267
|
+
// Top movers (up to 3, show both ups and downs)
|
|
268
|
+
const movers = cmp.categories
|
|
269
|
+
.filter(c => Math.abs(c.diff) > 10)
|
|
270
|
+
.sort((a, b) => Math.abs(b.diff) - Math.abs(a.diff))
|
|
271
|
+
.slice(0, 4);
|
|
272
|
+
if (movers.length > 0) {
|
|
273
|
+
const moverStrs = movers.map(m => {
|
|
274
|
+
const label = categoryLabel(m.category);
|
|
275
|
+
const color = m.diff <= 0 ? chalk.green : chalk.red;
|
|
276
|
+
const sign = m.diff <= 0 ? "-" : "+";
|
|
277
|
+
return `${chalk.dim(label)} ${color(`${sign}${fmtMoney(Math.abs(m.diff))}`)}`;
|
|
278
|
+
});
|
|
279
|
+
lines.push(" " + moverStrs.join(chalk.dim(" · ")));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
lines.push(chalk.dim(" spending") + chalk.white(` ${fmtMoney(thisMonthSpend.total)} this month`) + chalk.dim(` · ${daysLeft} days left`));
|
|
285
|
+
}
|
|
286
|
+
// Budget alerts (only if something is hot)
|
|
287
|
+
const budgets = getBudgetStatuses(db);
|
|
288
|
+
const hot = budgets.filter(b => b.pct_used >= 90).slice(0, 2);
|
|
289
|
+
if (hot.length > 0) {
|
|
290
|
+
lines.push("");
|
|
291
|
+
for (const b of hot) {
|
|
292
|
+
const pct = Math.round(b.pct_used);
|
|
293
|
+
const color = b.over_budget ? chalk.red : chalk.yellow;
|
|
294
|
+
const bar = miniBar(b.pct_used);
|
|
295
|
+
lines.push(` ${bar} ${color(categoryLabel(b.category))} ${chalk.dim(`${pct}%`)}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Goals (compact)
|
|
299
|
+
const goals = getGoals(db).filter(g => g.progress_pct < 100).slice(0, 2);
|
|
300
|
+
if (goals.length > 0) {
|
|
301
|
+
lines.push("");
|
|
302
|
+
for (const g of goals) {
|
|
303
|
+
const bar = miniBar(g.progress_pct);
|
|
304
|
+
const pace = g.target_date
|
|
305
|
+
? chalk.dim(` · need ${fmtMoney(g.monthly_needed)}/mo`)
|
|
306
|
+
: "";
|
|
307
|
+
lines.push(` ${bar} ${chalk.white(g.name)} ${chalk.dim(`${fmtMoney(g.current)}/${fmtMoney(g.target)}`)}${pace}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Upcoming bills
|
|
311
|
+
const todayDay = now.getDate();
|
|
312
|
+
const endDay = todayDay + 7;
|
|
313
|
+
let bills = [];
|
|
314
|
+
if (endDay <= daysInMonth) {
|
|
315
|
+
bills = db.prepare(`SELECT name, amount, day_of_month FROM recurring_bills WHERE day_of_month BETWEEN ? AND ?`).all(todayDay + 1, endDay);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
const a = db.prepare(`SELECT name, amount, day_of_month FROM recurring_bills WHERE day_of_month BETWEEN ? AND ?`).all(todayDay + 1, daysInMonth);
|
|
319
|
+
const b = db.prepare(`SELECT name, amount, day_of_month FROM recurring_bills WHERE day_of_month BETWEEN 1 AND ?`).all(endDay - daysInMonth);
|
|
320
|
+
bills = [...a, ...b];
|
|
321
|
+
}
|
|
322
|
+
if (bills.length > 0) {
|
|
323
|
+
lines.push("");
|
|
324
|
+
const billStrs = bills.slice(0, 3).map(b => {
|
|
325
|
+
const daysUntil = b.day_of_month > todayDay ? b.day_of_month - todayDay : daysInMonth - todayDay + b.day_of_month;
|
|
326
|
+
return chalk.dim(`${b.name} ${fmtMoney(b.amount)}`) + chalk.dim(` in ${daysUntil}d`);
|
|
327
|
+
});
|
|
328
|
+
lines.push(` ${chalk.dim("upcoming")} ${billStrs.join(chalk.dim(" · "))}`);
|
|
329
|
+
}
|
|
330
|
+
// Score
|
|
331
|
+
const score = getLatestScore(db);
|
|
332
|
+
if (score) {
|
|
333
|
+
lines.push("");
|
|
334
|
+
const scoreColor = score.score >= 70 ? chalk.green : score.score >= 40 ? chalk.yellow : chalk.red;
|
|
335
|
+
let scoreLine = ` ${chalk.dim("score")} ${scoreColor(String(score.score))}${chalk.dim("/100")}`;
|
|
336
|
+
const streaks = [];
|
|
337
|
+
if (score.no_restaurant_streak > 0)
|
|
338
|
+
streaks.push(`${score.no_restaurant_streak}d no dining`);
|
|
339
|
+
if (score.on_pace_streak > 0)
|
|
340
|
+
streaks.push(`${score.on_pace_streak}d on pace`);
|
|
341
|
+
if (streaks.length > 0)
|
|
342
|
+
scoreLine += chalk.dim(` · ${streaks.join(" · ")}`);
|
|
343
|
+
lines.push(scoreLine);
|
|
344
|
+
}
|
|
345
|
+
return lines.join("\n");
|
|
346
|
+
}
|
|
347
|
+
function fmtMoney(n) {
|
|
348
|
+
return "$" + Math.abs(n).toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 0 });
|
|
349
|
+
}
|
|
350
|
+
function miniBar(pct) {
|
|
351
|
+
const width = 8;
|
|
352
|
+
const clamped = Math.max(0, Math.min(100, pct));
|
|
353
|
+
const filled = Math.round((clamped / 100) * width);
|
|
354
|
+
const empty = width - filled;
|
|
355
|
+
const color = pct > 100 ? chalk.red : pct > 80 ? chalk.yellow : chalk.green;
|
|
356
|
+
return color("▓".repeat(filled)) + chalk.dim("░".repeat(empty));
|
|
357
|
+
}
|
|
358
|
+
function buildScore(db) {
|
|
359
|
+
const score = getLatestScore(db);
|
|
360
|
+
if (!score)
|
|
361
|
+
return null;
|
|
362
|
+
let line = `SCORE: ${score.score}/100`;
|
|
363
|
+
const streaks = [];
|
|
364
|
+
if (score.no_restaurant_streak > 0)
|
|
365
|
+
streaks.push(`${score.no_restaurant_streak}-day no-restaurant streak`);
|
|
366
|
+
if (score.no_shopping_streak > 0)
|
|
367
|
+
streaks.push(`${score.no_shopping_streak}-day no-shopping streak`);
|
|
368
|
+
if (score.on_pace_streak > 0)
|
|
369
|
+
streaks.push(`${score.on_pace_streak}-day on-pace streak`);
|
|
370
|
+
if (streaks.length > 0)
|
|
371
|
+
line += ` — ${streaks.join(", ")}`;
|
|
372
|
+
// Recent achievements
|
|
373
|
+
const achievements = db.prepare(`SELECT name FROM achievements ORDER BY unlocked_at DESC LIMIT 3`).all();
|
|
374
|
+
if (achievements.length > 0) {
|
|
375
|
+
line += ` | Recent: ${achievements.map(a => a.name).join(", ")}`;
|
|
376
|
+
}
|
|
377
|
+
return line;
|
|
378
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type Database from "better-sqlite3-multiple-ciphers";
|
|
2
|
+
export declare function getConversationHistory(db: Database.Database, limit?: number): {
|
|
3
|
+
role: string;
|
|
4
|
+
content: string;
|
|
5
|
+
created_at: string;
|
|
6
|
+
}[];
|
|
7
|
+
export declare function saveMessage(db: Database.Database, role: "user" | "assistant", content: string): void;
|
|
8
|
+
export declare function getMemories(db: Database.Database): {
|
|
9
|
+
id: number;
|
|
10
|
+
content: string;
|
|
11
|
+
category: string;
|
|
12
|
+
created_at: string;
|
|
13
|
+
}[];
|
|
14
|
+
export declare function saveMemory(db: Database.Database, content: string, category?: string): void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function getConversationHistory(db, limit = 20) {
|
|
2
|
+
return db.prepare(`SELECT role, content, created_at FROM conversation_history ORDER BY id DESC LIMIT ?`).all(limit).reverse();
|
|
3
|
+
}
|
|
4
|
+
export function saveMessage(db, role, content) {
|
|
5
|
+
db.prepare(`INSERT INTO conversation_history (role, content) VALUES (?, ?)`).run(role, content);
|
|
6
|
+
}
|
|
7
|
+
export function getMemories(db) {
|
|
8
|
+
return db.prepare(`SELECT id, content, category, created_at FROM memories ORDER BY created_at DESC`).all();
|
|
9
|
+
}
|
|
10
|
+
export function saveMemory(db, content, category = "general") {
|
|
11
|
+
db.prepare(`INSERT INTO memories (content, category) VALUES (?, ?)`).run(content, category);
|
|
12
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { config } from "../config.js";
|
|
2
|
+
import { readContext } from "./context.js";
|
|
3
|
+
/**
|
|
4
|
+
* Builds a list of PII terms to redact from outbound API calls.
|
|
5
|
+
* Extracts: user name, family/partner names, employer names from context.
|
|
6
|
+
*/
|
|
7
|
+
function buildRedactions() {
|
|
8
|
+
const entries = [];
|
|
9
|
+
const seen = new Set();
|
|
10
|
+
function add(real, token) {
|
|
11
|
+
const trimmed = real.trim();
|
|
12
|
+
if (trimmed.length < 2 || seen.has(trimmed.toLowerCase()))
|
|
13
|
+
return;
|
|
14
|
+
seen.add(trimmed.toLowerCase());
|
|
15
|
+
entries.push({ real: trimmed, token });
|
|
16
|
+
}
|
|
17
|
+
// User's name (and first name if multi-word)
|
|
18
|
+
const userName = config.userName;
|
|
19
|
+
if (userName && userName !== "User") {
|
|
20
|
+
add(userName, "[USER]");
|
|
21
|
+
const parts = userName.split(/\s+/);
|
|
22
|
+
if (parts.length > 1) {
|
|
23
|
+
add(parts[0], "[USER_FIRST]");
|
|
24
|
+
add(parts[parts.length - 1], "[USER_LAST]");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Parse context.md for family and employer names
|
|
28
|
+
const context = readContext();
|
|
29
|
+
if (context) {
|
|
30
|
+
// Extract names from ## Family section
|
|
31
|
+
const familyMatch = context.match(/## Family\n([\s\S]*?)(?=\n##|$)/);
|
|
32
|
+
if (familyMatch) {
|
|
33
|
+
const lines = familyMatch[1].split("\n").filter(l => l.trim().startsWith("-"));
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
const text = line.replace(/^-\s*/, "").trim();
|
|
36
|
+
// Skip the user's own name and placeholder lines
|
|
37
|
+
if (!text || text.startsWith("(") || text.toLowerCase() === userName.toLowerCase())
|
|
38
|
+
continue;
|
|
39
|
+
// Extract name — could be "Partner: Jane" or "Jane (wife)" or just "Jane Smith"
|
|
40
|
+
const nameMatch = text.match(/^(?:partner|spouse|wife|husband|child|kid|son|daughter|dependent)[:\s]+(.+)/i)
|
|
41
|
+
|| text.match(/^([A-Z][a-z]+(?: [A-Z][a-z]+)*)/);
|
|
42
|
+
if (nameMatch) {
|
|
43
|
+
const name = nameMatch[1].replace(/\s*\(.*\)/, "").trim();
|
|
44
|
+
if (name && name.toLowerCase() !== userName.toLowerCase()) {
|
|
45
|
+
add(name, "[PARTNER]");
|
|
46
|
+
const nameParts = name.split(/\s+/);
|
|
47
|
+
if (nameParts.length > 1) {
|
|
48
|
+
add(nameParts[0], "[PARTNER_FIRST]");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Extract employer from ## Income section
|
|
55
|
+
const incomeMatch = context.match(/## Income\n([\s\S]*?)(?=\n##|$)/);
|
|
56
|
+
if (incomeMatch) {
|
|
57
|
+
const lines = incomeMatch[1].split("\n").filter(l => l.trim().startsWith("-"));
|
|
58
|
+
for (const line of lines) {
|
|
59
|
+
const text = line.replace(/^-\s*/, "").trim();
|
|
60
|
+
if (!text || text.startsWith("("))
|
|
61
|
+
continue;
|
|
62
|
+
// Match patterns like "Employer: Acme Corp", "Salary from Acme Corp", "$85k/year from Acme Corp"
|
|
63
|
+
const employerMatch = text.match(/(?:employer|works? (?:at|for)|employed (?:at|by))[:\s]+([A-Z][\w\s&.,-]+?)(?:\s*[-–—|,;(\n]|$)/i)
|
|
64
|
+
|| text.match(/\bfrom ([A-Z][A-Za-z\s&.,-]+?)(?:\s*[-–—|,;(\n]|$)/)
|
|
65
|
+
|| text.match(/\bat ([A-Z][A-Za-z\s&.,-]+?)(?:\s*[-–—|,;(\n]|$)/);
|
|
66
|
+
if (employerMatch) {
|
|
67
|
+
add(employerMatch[1].trim(), "[EMPLOYER]");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Sort longest first so "John Smith" is replaced before "John"
|
|
73
|
+
entries.sort((a, b) => b.real.length - a.real.length);
|
|
74
|
+
return entries;
|
|
75
|
+
}
|
|
76
|
+
export function redact(text) {
|
|
77
|
+
const redactions = buildRedactions();
|
|
78
|
+
let result = text;
|
|
79
|
+
for (const { real, token } of redactions) {
|
|
80
|
+
result = result.replaceAll(real, token);
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
export function unredact(text) {
|
|
85
|
+
const redactions = buildRedactions();
|
|
86
|
+
let result = text;
|
|
87
|
+
// Reverse: replace tokens with real values (shortest tokens last to avoid partial matches)
|
|
88
|
+
for (const { real, token } of redactions) {
|
|
89
|
+
result = result.replaceAll(token, real);
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { config } from "../config.js";
|
|
2
|
+
import { getMemories } from "./memory.js";
|
|
3
|
+
import { readContext, isContextEmpty } from "./context.js";
|
|
4
|
+
import { computeInsights } from "./insights.js";
|
|
5
|
+
export function buildSystemPrompt(db) {
|
|
6
|
+
const memories = getMemories(db);
|
|
7
|
+
const now = new Date();
|
|
8
|
+
const dateStr = now.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", year: "numeric" });
|
|
9
|
+
const context = readContext();
|
|
10
|
+
const name = config.userName;
|
|
11
|
+
let prompt = `You are Ray, a personal financial advisor for ${name}. You have access to their real bank, investment, and debt data. You are thoughtful, precise, and candid about money — like a trusted friend who happens to be a financial expert.
|
|
12
|
+
|
|
13
|
+
Today is ${dateStr}.
|
|
14
|
+
|
|
15
|
+
## Personality
|
|
16
|
+
- You are not a chatbot. You are a sharp, opinionated CFO who's been watching ${name}'s money all day. Talk like a person, not a customer service rep.
|
|
17
|
+
- Lead with the insight, not the data. Don't say "Here's the breakdown:" — say what the breakdown means. "You crushed dining this month — down $114. That's the biggest swing."
|
|
18
|
+
- Be specific with numbers — always cite actual balances, amounts, and percentages from the data.
|
|
19
|
+
- Have a point of view. Instead of "here are your options", say what you'd actually do and why. You can present alternatives, but lead with your recommendation.
|
|
20
|
+
- Be proactive: if you notice something concerning or interesting in the briefing data, bring it up even if not asked. A good CFO doesn't wait to be asked.
|
|
21
|
+
- Be concise. 2-4 sentences for simple questions. Don't pad responses with filler.
|
|
22
|
+
- Skip transitions and preamble. Don't start with "Great question!" or "Let me look into that." Just answer.
|
|
23
|
+
- Be warm but direct. Celebrate wins genuinely. Flag problems without sugarcoating.
|
|
24
|
+
|
|
25
|
+
## Approach
|
|
26
|
+
1. You already have a financial briefing with current data. Use it — don't re-fetch what you already know. Call tools for deeper dives or data not in the briefing.
|
|
27
|
+
2. Connect the dots between data points. Don't just report numbers — tell ${name} what it means for them specifically, referencing their goals and context.
|
|
28
|
+
3. When comparing periods, use percentage changes and absolute differences.
|
|
29
|
+
4. End with what to do, not just what happened. A good CFO always has a next step.
|
|
30
|
+
|
|
31
|
+
## Formatting (terminal output)
|
|
32
|
+
- Plain text only. No markdown syntax — no asterisks, no hashtags, no backticks.
|
|
33
|
+
- Use line breaks, dashes, and simple alignment for structure.
|
|
34
|
+
- Use ALL CAPS or dashes for emphasis instead of bold/italic.
|
|
35
|
+
|
|
36
|
+
## Tools
|
|
37
|
+
- Always use tools to look up current data. Never guess balances, spending, or dates.
|
|
38
|
+
- When the user shares something worth remembering (a preference, life event, financial goal context), use save_memory.
|
|
39
|
+
- When circumstances change (new decisions, completed goals, changed balances, updated strategy), use update_context to persist the change.
|
|
40
|
+
- For date-based queries, figure out the right date range from context (e.g., "this month" = first of current month to today).
|
|
41
|
+
|
|
42
|
+
## Privacy
|
|
43
|
+
- Never reveal account numbers, routing numbers, or Plaid access tokens.
|
|
44
|
+
- You can discuss balances, transactions, and spending freely — that's what you're here for.`;
|
|
45
|
+
if (isContextEmpty()) {
|
|
46
|
+
prompt += `\n\n## Onboarding Mode
|
|
47
|
+
${name} just connected their financial accounts and needs help setting up their financial profile. This is your first conversation.
|
|
48
|
+
|
|
49
|
+
Instructions:
|
|
50
|
+
1. Start by calling these tools to review their synced data: get_accounts, get_transactions (last 30 days), get_spending_summary, get_debts
|
|
51
|
+
2. Present a concise summary of what you found — accounts, recent spending patterns, any debts
|
|
52
|
+
3. Then ask about gaps ONE TOPIC AT A TIME (not all at once). Topics to cover:
|
|
53
|
+
- Family situation (partner, dependents)
|
|
54
|
+
- Income details (salary, side income, frequency)
|
|
55
|
+
- Financial goals (short-term and long-term)
|
|
56
|
+
- Current challenges or concerns
|
|
57
|
+
- Budget targets or spending limits they want
|
|
58
|
+
- Any upcoming life changes (job change, move, baby, etc.)
|
|
59
|
+
4. After each answer, call update_context with the "section" param to save that section of their context
|
|
60
|
+
5. Also use save_memory for notable individual facts
|
|
61
|
+
6. Keep it to 1-2 questions per turn — be conversational, not interrogative
|
|
62
|
+
7. After gathering enough info, write a Strategy section summarizing priorities and next steps
|
|
63
|
+
8. If the user says "skip" or changes topic, gracefully stop onboarding and help with whatever they need
|
|
64
|
+
|
|
65
|
+
This onboarding block will automatically disappear once the context file is filled in.`;
|
|
66
|
+
}
|
|
67
|
+
else if (context) {
|
|
68
|
+
prompt += `\n\n## ${name}'s Financial Context\n${context}`;
|
|
69
|
+
}
|
|
70
|
+
// Financial intelligence briefing — computed insights injected before memories
|
|
71
|
+
try {
|
|
72
|
+
const insights = computeInsights(db);
|
|
73
|
+
if (insights) {
|
|
74
|
+
prompt += `\n\n${insights}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Don't let insight computation failure break the conversation
|
|
79
|
+
}
|
|
80
|
+
if (memories.length > 0) {
|
|
81
|
+
prompt += `\n\n## Things I remember about ${name}\n`;
|
|
82
|
+
prompt += memories.map(m => `- ${m.content}`).join("\n");
|
|
83
|
+
}
|
|
84
|
+
return prompt;
|
|
85
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type Database from "better-sqlite3-multiple-ciphers";
|
|
2
|
+
import type { Tool } from "@anthropic-ai/sdk/resources/messages.js";
|
|
3
|
+
export declare const toolDefinitions: Tool[];
|
|
4
|
+
export declare function executeTool(db: Database.Database, toolName: string, toolInput: any): Promise<string>;
|