ray-finance 0.2.4 → 0.3.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/dist/ai/agent.d.ts +10 -2
- package/dist/ai/agent.js +42 -4
- package/dist/ai/audit.d.ts +1 -1
- package/dist/ai/insights.d.ts +1 -1
- package/dist/ai/insights.js +1 -1
- package/dist/ai/memory.d.ts +1 -1
- package/dist/ai/system-prompt.d.ts +1 -1
- package/dist/ai/system-prompt.js +8 -6
- package/dist/ai/tools.d.ts +1 -1
- package/dist/ai/tools.js +8 -3
- package/dist/alerts/index.d.ts +1 -1
- package/dist/alerts/index.js +16 -27
- package/dist/cli/chat.js +70 -16
- package/dist/cli/commands.d.ts +1 -0
- package/dist/cli/commands.js +58 -4
- package/dist/cli/completions.d.ts +1 -0
- package/dist/cli/completions.js +174 -0
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +171 -0
- package/dist/cli/format.d.ts +19 -0
- package/dist/cli/format.js +80 -0
- package/dist/cli/index.js +34 -0
- package/dist/cli/setup.js +63 -16
- package/dist/cli/updater.d.ts +3 -0
- package/dist/cli/updater.js +107 -0
- package/dist/daily-sync.d.ts +6 -2
- package/dist/daily-sync.js +55 -11
- package/dist/db/connection.d.ts +1 -1
- package/dist/db/connection.js +6 -6
- package/dist/db/schema.d.ts +1 -1
- package/dist/db/schema.js +89 -5
- package/dist/plaid/link.js +1 -0
- package/dist/plaid/sync.d.ts +14 -1
- package/dist/plaid/sync.js +236 -13
- package/dist/public/link.html +64 -43
- package/dist/queries/index.d.ts +1 -1
- package/dist/queries/index.js +1 -1
- package/dist/scoring/index.d.ts +1 -1
- package/dist/server.js +46 -4
- package/package.json +4 -3
package/dist/ai/agent.d.ts
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
|
-
import type Database from "
|
|
2
|
-
|
|
1
|
+
import type Database from "libsql";
|
|
2
|
+
/** Human-readable labels for tool calls shown in the spinner */
|
|
3
|
+
export declare const TOOL_LABELS: Record<string, string>;
|
|
4
|
+
export type ProgressCallback = (event: {
|
|
5
|
+
phase: "tool" | "responding";
|
|
6
|
+
toolName?: string;
|
|
7
|
+
toolCount: number;
|
|
8
|
+
elapsedMs: number;
|
|
9
|
+
}) => void;
|
|
10
|
+
export declare function handleMessage(db: Database.Database, userMessage: string, onProgress?: ProgressCallback): Promise<string>;
|
package/dist/ai/agent.js
CHANGED
|
@@ -11,7 +11,23 @@ const anthropic = new Anthropic(useManaged()
|
|
|
11
11
|
function supportsThinking(model) {
|
|
12
12
|
return /sonnet-4|opus-4/i.test(model);
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
/** Human-readable labels for tool calls shown in the spinner */
|
|
15
|
+
export const TOOL_LABELS = {
|
|
16
|
+
get_net_worth: "Checking net worth",
|
|
17
|
+
get_accounts: "Reviewing accounts",
|
|
18
|
+
get_transactions: "Looking at transactions",
|
|
19
|
+
get_spending_summary: "Analyzing spending",
|
|
20
|
+
get_budgets: "Reviewing budgets",
|
|
21
|
+
set_budget: "Setting budget",
|
|
22
|
+
get_goals: "Checking goals",
|
|
23
|
+
set_goal: "Setting goal",
|
|
24
|
+
get_score: "Calculating score",
|
|
25
|
+
get_recurring: "Finding recurring charges",
|
|
26
|
+
get_alerts: "Checking alerts",
|
|
27
|
+
save_memory: "Remembering that",
|
|
28
|
+
update_context: "Updating your profile",
|
|
29
|
+
};
|
|
30
|
+
export async function handleMessage(db, userMessage, onProgress) {
|
|
15
31
|
// Save incoming message
|
|
16
32
|
saveMessage(db, "user", userMessage);
|
|
17
33
|
// Load conversation context, truncated to fit token budget
|
|
@@ -56,6 +72,8 @@ export async function handleMessage(db, userMessage) {
|
|
|
56
72
|
// Initial API call
|
|
57
73
|
let response = await anthropic.messages.create(apiParams);
|
|
58
74
|
// Agentic tool loop
|
|
75
|
+
const startTime = Date.now();
|
|
76
|
+
let toolCount = 0;
|
|
59
77
|
while (response.stop_reason === "tool_use") {
|
|
60
78
|
// Filter out thinking blocks before adding to messages
|
|
61
79
|
const assistantContent = response.content.filter((b) => b.type !== "thinking");
|
|
@@ -63,6 +81,13 @@ export async function handleMessage(db, userMessage) {
|
|
|
63
81
|
const toolResults = [];
|
|
64
82
|
for (const block of assistantContent) {
|
|
65
83
|
if (block.type === "tool_use") {
|
|
84
|
+
toolCount++;
|
|
85
|
+
onProgress?.({
|
|
86
|
+
phase: "tool",
|
|
87
|
+
toolName: block.name,
|
|
88
|
+
toolCount,
|
|
89
|
+
elapsedMs: Date.now() - startTime,
|
|
90
|
+
});
|
|
66
91
|
const result = await executeTool(db, block.name, block.input);
|
|
67
92
|
logToolCall(db, block.name, block.input, result, response.usage?.output_tokens);
|
|
68
93
|
toolResults.push({
|
|
@@ -73,6 +98,11 @@ export async function handleMessage(db, userMessage) {
|
|
|
73
98
|
}
|
|
74
99
|
}
|
|
75
100
|
messages.push({ role: "user", content: toolResults });
|
|
101
|
+
onProgress?.({
|
|
102
|
+
phase: "responding",
|
|
103
|
+
toolCount,
|
|
104
|
+
elapsedMs: Date.now() - startTime,
|
|
105
|
+
});
|
|
76
106
|
response = await anthropic.messages.create(apiParams);
|
|
77
107
|
}
|
|
78
108
|
// Extract text response (filter out thinking blocks), restore PII for display
|
|
@@ -83,11 +113,19 @@ export async function handleMessage(db, userMessage) {
|
|
|
83
113
|
return responseText || "I looked into that but couldn't formulate a response. Could you try rephrasing?";
|
|
84
114
|
}
|
|
85
115
|
catch (error) {
|
|
86
|
-
|
|
116
|
+
if (error.status === 403) {
|
|
117
|
+
return "Your API key was rejected. This usually means your subscription is inactive. Run `ray billing` to check your payment status, or `ray setup` to reconfigure.";
|
|
118
|
+
}
|
|
119
|
+
if (error.status === 401) {
|
|
120
|
+
return "Invalid API key. Run `ray setup` to reconfigure your credentials.";
|
|
121
|
+
}
|
|
122
|
+
if (error.status === 429) {
|
|
123
|
+
return "Rate limited. Wait a moment and try again.";
|
|
124
|
+
}
|
|
87
125
|
const safeMessage = error.status
|
|
88
126
|
? `API error (${error.status})`
|
|
89
|
-
: "internal error";
|
|
90
|
-
console.error("AI
|
|
127
|
+
: error.message || "internal error";
|
|
128
|
+
console.error("AI error:", safeMessage);
|
|
91
129
|
return "Sorry, I had trouble processing that. Could you try again?";
|
|
92
130
|
}
|
|
93
131
|
}
|
package/dist/ai/audit.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type Database from "
|
|
1
|
+
import type Database from "libsql";
|
|
2
2
|
export declare function logToolCall(db: Database.Database, toolName: string, inputParams: any, resultSummary: string, tokensUsed?: number): void;
|
|
3
3
|
export declare function getAuditLog(db: Database.Database, limit?: number): any[];
|
package/dist/ai/insights.d.ts
CHANGED
package/dist/ai/insights.js
CHANGED
|
@@ -53,7 +53,7 @@ function buildSnapshot(db) {
|
|
|
53
53
|
// Account balances — cap at 5
|
|
54
54
|
const accounts = getAccountBalances(db).slice(0, 5);
|
|
55
55
|
if (accounts.length > 0) {
|
|
56
|
-
lines.push(accounts.map(a => `${a.name} (${a.type}): ${a.type
|
|
56
|
+
lines.push(accounts.map(a => `${a.name} (${a.type}): ${["credit", "loan"].includes(a.type) ? "-" : ""}${formatMoney(a.balance)}`).join(" | "));
|
|
57
57
|
}
|
|
58
58
|
// Debt summary
|
|
59
59
|
const debts = getDebts(db);
|
package/dist/ai/memory.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type Database from "
|
|
1
|
+
import type Database from "libsql";
|
|
2
2
|
export declare function buildSystemPrompt(db: Database.Database): string;
|
package/dist/ai/system-prompt.js
CHANGED
|
@@ -38,6 +38,7 @@ Today is ${dateStr}.
|
|
|
38
38
|
- When the user shares something worth remembering (a preference, life event, financial goal context), use save_memory.
|
|
39
39
|
- When circumstances change (new decisions, completed goals, changed balances, updated strategy), use update_context to persist the change.
|
|
40
40
|
- For date-based queries, figure out the right date range from context (e.g., "this month" = first of current month to today).
|
|
41
|
+
- If you notice transactions suggesting unlinked accounts (e.g., mortgage payments, car loans, investment transfers) that aren't in the linked accounts, mention it once and suggest \`ray link\`. If the user says they don't have that account, save it to context.
|
|
41
42
|
|
|
42
43
|
## Privacy
|
|
43
44
|
- Never reveal account numbers, routing numbers, or Plaid access tokens.
|
|
@@ -49,18 +50,19 @@ ${name} just connected their financial accounts and needs help setting up their
|
|
|
49
50
|
Instructions:
|
|
50
51
|
1. Start by calling these tools to review their synced data: get_accounts, get_transactions (last 30 days), get_spending_summary, get_debts
|
|
51
52
|
2. Present a concise summary of what you found — accounts, recent spending patterns, any debts
|
|
52
|
-
3.
|
|
53
|
+
3. Check for missing account types: if you see mortgage payments but no mortgage account, car payments but no auto loan, investment contributions but no investment account, etc. — ask if they'd like to link those too (they can run \`ray link\`). If they say they don't have one, save that to context so you don't ask again.
|
|
54
|
+
4. Then ask about gaps ONE TOPIC AT A TIME (not all at once). Topics to cover:
|
|
53
55
|
- Family situation (partner, dependents)
|
|
54
56
|
- Income details (salary, side income, frequency)
|
|
55
57
|
- Financial goals (short-term and long-term)
|
|
56
58
|
- Current challenges or concerns
|
|
57
59
|
- Budget targets or spending limits they want
|
|
58
60
|
- Any upcoming life changes (job change, move, baby, etc.)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
5. After each answer, call update_context with the "section" param to save that section of their context
|
|
62
|
+
6. Also use save_memory for notable individual facts
|
|
63
|
+
7. Keep it to 1-2 questions per turn — be conversational, not interrogative
|
|
64
|
+
8. After gathering enough info, write a Strategy section summarizing priorities and next steps
|
|
65
|
+
9. If the user says "skip" or changes topic, gracefully stop onboarding and help with whatever they need
|
|
64
66
|
|
|
65
67
|
This onboarding block will automatically disappear once the context file is filled in.`;
|
|
66
68
|
}
|
package/dist/ai/tools.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type Database from "
|
|
1
|
+
import type Database from "libsql";
|
|
2
2
|
import type { Tool } from "@anthropic-ai/sdk/resources/messages.js";
|
|
3
3
|
export declare const toolDefinitions: Tool[];
|
|
4
4
|
export declare function executeTool(db: Database.Database, toolName: string, toolInput: any): Promise<string>;
|
package/dist/ai/tools.js
CHANGED
|
@@ -332,7 +332,7 @@ export async function executeTool(db, toolName, toolInput) {
|
|
|
332
332
|
const accounts = getAccountBalances(db);
|
|
333
333
|
if (accounts.length === 0)
|
|
334
334
|
return "No accounts linked yet.";
|
|
335
|
-
return accounts.map(a => `${a.name} (${a.type}): ${a.type
|
|
335
|
+
return accounts.map(a => `${a.name} (${a.type}): ${["credit", "loan"].includes(a.type) ? "-" : ""}${formatMoney(a.balance)}`).join("\n");
|
|
336
336
|
}
|
|
337
337
|
case "get_transactions": {
|
|
338
338
|
const txns = getTransactionsFiltered(db, {
|
|
@@ -432,10 +432,15 @@ export async function executeTool(db, toolName, toolInput) {
|
|
|
432
432
|
return result;
|
|
433
433
|
}
|
|
434
434
|
case "get_recurring": {
|
|
435
|
-
const rows = db.prepare(`SELECT merchant_name, avg_amount, frequency, last_date
|
|
435
|
+
const rows = db.prepare(`SELECT merchant_name, description, avg_amount, last_amount, frequency, last_date, stream_type
|
|
436
|
+
FROM recurring WHERE is_active = 1 ORDER BY stream_type, avg_amount DESC`).all();
|
|
436
437
|
if (rows.length === 0)
|
|
437
438
|
return "No recurring transactions detected yet.";
|
|
438
|
-
return rows.map(r =>
|
|
439
|
+
return rows.map(r => {
|
|
440
|
+
const name = r.merchant_name || r.description;
|
|
441
|
+
const arrow = r.stream_type === "inflow" ? "+" : "-";
|
|
442
|
+
return `${arrow} ${name}: ${formatMoney(Math.abs(r.avg_amount))} (${r.frequency.toLowerCase()}, last: ${r.last_date})`;
|
|
443
|
+
}).join("\n");
|
|
439
444
|
}
|
|
440
445
|
case "get_alerts": {
|
|
441
446
|
const alerts = generateAlerts(db);
|
package/dist/alerts/index.d.ts
CHANGED
package/dist/alerts/index.js
CHANGED
|
@@ -59,36 +59,25 @@ export function generateAlerts(db) {
|
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
// Subscription price changes
|
|
62
|
+
// Subscription price changes — uses Plaid's recurring transaction streams
|
|
63
63
|
const recurring = db
|
|
64
|
-
.prepare(`SELECT merchant_name,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
GROUP BY merchant_name HAVING COUNT(*) >= 2
|
|
69
|
-
)
|
|
70
|
-
AND amount > 0 AND pending = 0
|
|
71
|
-
ORDER BY merchant_name, date DESC`)
|
|
64
|
+
.prepare(`SELECT merchant_name, description, avg_amount, last_amount, frequency, status
|
|
65
|
+
FROM recurring
|
|
66
|
+
WHERE is_active = 1 AND stream_type = 'outflow' AND status = 'MATURE'
|
|
67
|
+
AND last_amount IS NOT NULL AND avg_amount IS NOT NULL`)
|
|
72
68
|
.all();
|
|
73
|
-
const byMerchant = {};
|
|
74
69
|
for (const r of recurring) {
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
type: "price_change",
|
|
87
|
-
severity: "info",
|
|
88
|
-
message: `Price change: ${merchant} went from $${charges[1].amount} to $${charges[0].amount}`,
|
|
89
|
-
data: { merchant, previous: charges[1].amount, current: charges[0].amount },
|
|
90
|
-
});
|
|
91
|
-
}
|
|
70
|
+
if (r.avg_amount === 0)
|
|
71
|
+
continue;
|
|
72
|
+
const diff = Math.abs(r.last_amount - r.avg_amount);
|
|
73
|
+
if (diff > 0.5 && diff / Math.abs(r.avg_amount) > 0.05) {
|
|
74
|
+
const name = r.merchant_name || r.description;
|
|
75
|
+
alerts.push({
|
|
76
|
+
type: "price_change",
|
|
77
|
+
severity: "info",
|
|
78
|
+
message: `Price change: ${name} went from $${Math.abs(r.avg_amount).toFixed(2)} to $${Math.abs(r.last_amount).toFixed(2)} (${r.frequency.toLowerCase()})`,
|
|
79
|
+
data: { merchant: name, previous: r.avg_amount, current: r.last_amount, frequency: r.frequency },
|
|
80
|
+
});
|
|
92
81
|
}
|
|
93
82
|
}
|
|
94
83
|
return alerts;
|
package/dist/cli/chat.js
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { getDb } from "../db/connection.js";
|
|
3
|
-
import { handleMessage } from "../ai/agent.js";
|
|
4
2
|
import { config } from "../config.js";
|
|
5
|
-
import {
|
|
6
|
-
import { cliBriefing } from "../ai/insights.js";
|
|
7
|
-
import { banner, formatResponse } from "./format.js";
|
|
3
|
+
import { banner, formatResponse, formatDuration, formatError } from "./format.js";
|
|
8
4
|
/** Raw-mode line reader that renders content below the cursor while waiting for input */
|
|
9
5
|
function rawReadLine(prompt, belowLines) {
|
|
10
6
|
return new Promise((resolve) => {
|
|
11
7
|
let buf = "";
|
|
12
8
|
const out = process.stdout;
|
|
13
|
-
// Render: prompt on current line, then content below, then
|
|
9
|
+
// Render: prompt on current line, then content below, then move cursor back
|
|
14
10
|
out.write(prompt);
|
|
15
11
|
if (belowLines.length > 0) {
|
|
16
|
-
// Save cursor, render below, restore
|
|
17
|
-
out.write("\x1b[s");
|
|
18
12
|
out.write("\n" + belowLines.join("\n"));
|
|
19
|
-
|
|
13
|
+
// Move cursor back up to the prompt line and to the end of prompt text
|
|
14
|
+
out.write(`\x1b[${belowLines.length}A`);
|
|
15
|
+
out.write("\r" + prompt);
|
|
20
16
|
}
|
|
21
17
|
process.stdin.setRawMode(true);
|
|
22
18
|
process.stdin.resume();
|
|
@@ -73,8 +69,25 @@ function rawReadLine(prompt, belowLines) {
|
|
|
73
69
|
process.stdin.on("data", onData);
|
|
74
70
|
});
|
|
75
71
|
}
|
|
72
|
+
const THINKING_PHRASES = [
|
|
73
|
+
"Thinking...",
|
|
74
|
+
"Crunching numbers...",
|
|
75
|
+
"Reviewing your accounts...",
|
|
76
|
+
"Analyzing...",
|
|
77
|
+
"Looking into that...",
|
|
78
|
+
"Pulling up your data...",
|
|
79
|
+
"Checking the numbers...",
|
|
80
|
+
"On it...",
|
|
81
|
+
];
|
|
82
|
+
function getThinkingText() {
|
|
83
|
+
return THINKING_PHRASES[Math.floor(Math.random() * THINKING_PHRASES.length)];
|
|
84
|
+
}
|
|
76
85
|
export async function startChat() {
|
|
77
86
|
const ora = (await import("ora")).default;
|
|
87
|
+
const { getDb } = await import("../db/connection.js");
|
|
88
|
+
const { handleMessage, TOOL_LABELS } = await import("../ai/agent.js");
|
|
89
|
+
const { isContextEmpty } = await import("../ai/context.js");
|
|
90
|
+
const { cliBriefing } = await import("../ai/insights.js");
|
|
78
91
|
const db = getDb();
|
|
79
92
|
// Show logo + briefing
|
|
80
93
|
console.log("");
|
|
@@ -111,8 +124,8 @@ export async function startChat() {
|
|
|
111
124
|
}
|
|
112
125
|
// Auto-trigger onboarding for new users
|
|
113
126
|
if (isContextEmpty()) {
|
|
114
|
-
console.log(chalk.
|
|
115
|
-
const spinner = ora({ text: "Reviewing your accounts...", color: "
|
|
127
|
+
console.log(chalk.yellowBright("Welcome! Let me review your accounts and help set up your financial profile.\n"));
|
|
128
|
+
const spinner = ora({ text: "Reviewing your accounts...", color: "yellow", discardStdin: false }).start();
|
|
116
129
|
try {
|
|
117
130
|
const response = await handleMessage(db, "I just connected my financial accounts. Help me set up my financial profile.");
|
|
118
131
|
spinner.stop();
|
|
@@ -120,10 +133,35 @@ export async function startChat() {
|
|
|
120
133
|
}
|
|
121
134
|
catch (err) {
|
|
122
135
|
spinner.stop();
|
|
123
|
-
console.error(
|
|
136
|
+
console.error(formatError(err, "Onboarding error"));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Background re-sync for recently linked accounts (Plaid backfill can take hours)
|
|
140
|
+
let bgSyncTimer = null;
|
|
141
|
+
const oldestAccount = db.prepare(`SELECT MIN(created_at) as ts FROM institutions`).get();
|
|
142
|
+
if (oldestAccount?.ts) {
|
|
143
|
+
const ageMs = Date.now() - new Date(oldestAccount.ts + "Z").getTime();
|
|
144
|
+
if (ageMs < 6 * 60 * 60 * 1000) { // linked within last 6 hours
|
|
145
|
+
const { runDailySync } = await import("../daily-sync.js");
|
|
146
|
+
bgSyncTimer = setInterval(async () => {
|
|
147
|
+
// Silence all output during background sync
|
|
148
|
+
const origWrite = process.stdout.write;
|
|
149
|
+
const origErr = process.stderr.write;
|
|
150
|
+
process.stdout.write = () => true;
|
|
151
|
+
process.stderr.write = () => true;
|
|
152
|
+
try {
|
|
153
|
+
await runDailySync(db);
|
|
154
|
+
}
|
|
155
|
+
catch { }
|
|
156
|
+
process.stdout.write = origWrite;
|
|
157
|
+
process.stderr.write = origErr;
|
|
158
|
+
}, 15 * 60 * 1000); // every 15 minutes
|
|
159
|
+
bgSyncTimer.unref(); // don't prevent process exit
|
|
124
160
|
}
|
|
125
161
|
}
|
|
126
162
|
const shutdown = () => {
|
|
163
|
+
if (bgSyncTimer)
|
|
164
|
+
clearInterval(bgSyncTimer);
|
|
127
165
|
console.log(chalk.dim("\nGoodbye!"));
|
|
128
166
|
process.exit(0);
|
|
129
167
|
};
|
|
@@ -159,6 +197,7 @@ export async function startChat() {
|
|
|
159
197
|
if (syncStr)
|
|
160
198
|
parts.push(syncStr);
|
|
161
199
|
parts.push(hints[hintIdx]);
|
|
200
|
+
parts.push("ctrl+c to exit");
|
|
162
201
|
hintIdx = (hintIdx + 1) % hints.length;
|
|
163
202
|
return parts.join(" · ");
|
|
164
203
|
};
|
|
@@ -173,8 +212,14 @@ export async function startChat() {
|
|
|
173
212
|
console.log(rule);
|
|
174
213
|
const input = await rawReadLine(chalk.dim("❯ "), [rule, footerText]);
|
|
175
214
|
const trimmed = input.trim();
|
|
176
|
-
if (!trimmed)
|
|
215
|
+
if (!trimmed) {
|
|
216
|
+
// Clear the prompt frame (top rule + prompt + bottom rule + footer)
|
|
217
|
+
process.stdout.write("\x1b[3A\r");
|
|
218
|
+
for (let i = 0; i < 4; i++)
|
|
219
|
+
process.stdout.write("\x1b[2K\x1b[1B");
|
|
220
|
+
process.stdout.write("\x1b[4A\r");
|
|
177
221
|
continue;
|
|
222
|
+
}
|
|
178
223
|
// Replace prompt frame with gray-background user message
|
|
179
224
|
// Move up 4 lines (footer, bottom rule, prompt, top rule) and clear them
|
|
180
225
|
process.stdout.write("\x1b[4A\r");
|
|
@@ -189,15 +234,24 @@ export async function startChat() {
|
|
|
189
234
|
shutdown();
|
|
190
235
|
break;
|
|
191
236
|
}
|
|
192
|
-
const spinner = ora({ text:
|
|
237
|
+
const spinner = ora({ text: getThinkingText(), color: "cyan", discardStdin: false }).start();
|
|
238
|
+
const onProgress = ({ phase, toolName, toolCount, elapsedMs }) => {
|
|
239
|
+
if (phase === "tool" && toolName) {
|
|
240
|
+
const label = TOOL_LABELS[toolName] || toolName;
|
|
241
|
+
spinner.text = `${label}... ${chalk.dim(`(${toolCount} ${toolCount === 1 ? "tool" : "tools"}, ${formatDuration(elapsedMs)})`)}`;
|
|
242
|
+
}
|
|
243
|
+
else if (phase === "responding" && toolCount > 0) {
|
|
244
|
+
spinner.text = `Composing response... ${chalk.dim(`(${toolCount} tools, ${formatDuration(elapsedMs)})`)}`;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
193
247
|
try {
|
|
194
|
-
const response = await handleMessage(db, trimmed);
|
|
248
|
+
const response = await handleMessage(db, trimmed, onProgress);
|
|
195
249
|
spinner.stop();
|
|
196
250
|
console.log(`\n${formatResponse(response)}\n`);
|
|
197
251
|
}
|
|
198
252
|
catch (err) {
|
|
199
253
|
spinner.stop();
|
|
200
|
-
console.error(
|
|
254
|
+
console.error(formatError(err));
|
|
201
255
|
}
|
|
202
256
|
}
|
|
203
257
|
}
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare function runSync(): Promise<void>;
|
|
2
2
|
export declare function runLink(): Promise<void>;
|
|
3
|
+
export declare function showAccounts(): Promise<void>;
|
|
3
4
|
export declare function showStatus(): void;
|
|
4
5
|
export declare function showTransactions(options?: {
|
|
5
6
|
limit?: number;
|
package/dist/cli/commands.js
CHANGED
|
@@ -5,17 +5,22 @@ import { getLatestScore, getAchievements, getMonthlySavings } from "../scoring/i
|
|
|
5
5
|
import { generateAlerts } from "../alerts/index.js";
|
|
6
6
|
import { runDailySync } from "../daily-sync.js";
|
|
7
7
|
import { startLinkServer } from "../server.js";
|
|
8
|
-
import { heading, progressBar, formatMoney, formatMoneyColored, dim } from "./format.js";
|
|
8
|
+
import { heading, progressBar, formatMoney, formatMoneyColored, dim, formatDuration, formatError, renderLogo, institutionName } from "./format.js";
|
|
9
9
|
export async function runSync() {
|
|
10
10
|
const ora = (await import("ora")).default;
|
|
11
11
|
const spinner = ora("Syncing transactions...").start();
|
|
12
|
+
const startTime = Date.now();
|
|
12
13
|
try {
|
|
13
14
|
const db = getDb();
|
|
14
|
-
await runDailySync(db);
|
|
15
|
-
|
|
15
|
+
const result = await runDailySync(db);
|
|
16
|
+
const elapsed = formatDuration(Date.now() - startTime);
|
|
17
|
+
const parts = [elapsed];
|
|
18
|
+
if (result.transactionsAdded > 0)
|
|
19
|
+
parts.push(`${result.transactionsAdded} new transactions`);
|
|
20
|
+
spinner.succeed(`Sync complete. ${chalk.dim(`(${parts.join(", ")})`)}`);
|
|
16
21
|
}
|
|
17
22
|
catch (err) {
|
|
18
|
-
spinner.fail(
|
|
23
|
+
spinner.fail(formatError(err, "Sync failed"));
|
|
19
24
|
}
|
|
20
25
|
}
|
|
21
26
|
export async function runLink() {
|
|
@@ -31,6 +36,55 @@ export async function runLink() {
|
|
|
31
36
|
stop();
|
|
32
37
|
spinner.succeed("Bank account linked successfully!");
|
|
33
38
|
}
|
|
39
|
+
export async function showAccounts() {
|
|
40
|
+
const db = getDb();
|
|
41
|
+
const institutions = db.prepare(`SELECT i.name as institution, i.item_id, i.created_at, i.logo, i.primary_color,
|
|
42
|
+
a.name, a.type, a.subtype, a.mask, a.current_balance, a.currency
|
|
43
|
+
FROM institutions i
|
|
44
|
+
LEFT JOIN accounts a ON a.item_id = i.item_id AND a.hidden = 0
|
|
45
|
+
ORDER BY i.created_at, a.type, a.current_balance DESC`).all();
|
|
46
|
+
if (institutions.length === 0) {
|
|
47
|
+
console.log("\nNo accounts linked. Run 'ray link' to connect one.\n");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.log(`\n${heading("Linked Accounts")}\n`);
|
|
51
|
+
// Group rows by institution
|
|
52
|
+
const groups = new Map();
|
|
53
|
+
for (const row of institutions) {
|
|
54
|
+
const key = row.item_id;
|
|
55
|
+
if (!groups.has(key))
|
|
56
|
+
groups.set(key, []);
|
|
57
|
+
groups.get(key).push(row);
|
|
58
|
+
}
|
|
59
|
+
// Compute column widths across all accounts for alignment
|
|
60
|
+
const allAccounts = institutions.filter(r => r.name);
|
|
61
|
+
const maxName = Math.max(...allAccounts.map(r => `${r.name}${r.mask ? ` ••${r.mask}` : ""}`.length), 0);
|
|
62
|
+
const maxLabel = Math.max(...allAccounts.map(r => (r.subtype || r.type || "").length), 0);
|
|
63
|
+
for (const [, rows] of groups) {
|
|
64
|
+
const first = rows[0];
|
|
65
|
+
// Logo inline with institution name
|
|
66
|
+
let logoStr = "";
|
|
67
|
+
if (first.logo) {
|
|
68
|
+
const logo = await renderLogo(first.logo);
|
|
69
|
+
if (logo)
|
|
70
|
+
logoStr = logo.replace(/\n/g, "") + " ";
|
|
71
|
+
}
|
|
72
|
+
console.log(`${logoStr}${institutionName(first.institution, first.primary_color)}`);
|
|
73
|
+
for (const row of rows) {
|
|
74
|
+
if (!row.name) {
|
|
75
|
+
console.log(dim(" No accounts found"));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const nameWithMask = `${row.name}${row.mask ? ` ••${row.mask}` : ""}`;
|
|
79
|
+
const label = row.subtype || row.type || "";
|
|
80
|
+
const balance = row.current_balance != null ? rawFormatMoney(row.current_balance) : "—";
|
|
81
|
+
const namePad = nameWithMask.padEnd(maxName + 2);
|
|
82
|
+
const labelPad = label.padEnd(maxLabel + 2);
|
|
83
|
+
console.log(` ${namePad}${dim(labelPad)}${balance}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
console.log("");
|
|
87
|
+
}
|
|
34
88
|
export function showStatus() {
|
|
35
89
|
const db = getDb();
|
|
36
90
|
const nw = getNetWorth(db);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function installCompletions(): void;
|