ray-finance 0.1.0 → 0.1.1
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 +9 -1
- package/dist/ai/agent.js +42 -4
- package/dist/ai/system-prompt.js +8 -6
- package/dist/cli/chat.js +56 -15
- package/dist/cli/commands.js +9 -4
- package/dist/cli/format.d.ts +15 -0
- package/dist/cli/format.js +57 -0
- package/dist/cli/setup.js +46 -11
- package/dist/daily-sync.d.ts +5 -1
- package/dist/daily-sync.js +6 -1
- package/package.json +1 -1
package/dist/ai/agent.d.ts
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
import type Database from "libsql";
|
|
2
|
-
|
|
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/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/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,28 @@ 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
|
+
try {
|
|
148
|
+
await runDailySync(db);
|
|
149
|
+
}
|
|
150
|
+
catch { }
|
|
151
|
+
}, 15 * 60 * 1000); // every 15 minutes
|
|
152
|
+
bgSyncTimer.unref(); // don't prevent process exit
|
|
124
153
|
}
|
|
125
154
|
}
|
|
126
155
|
const shutdown = () => {
|
|
156
|
+
if (bgSyncTimer)
|
|
157
|
+
clearInterval(bgSyncTimer);
|
|
127
158
|
console.log(chalk.dim("\nGoodbye!"));
|
|
128
159
|
process.exit(0);
|
|
129
160
|
};
|
|
@@ -159,6 +190,7 @@ export async function startChat() {
|
|
|
159
190
|
if (syncStr)
|
|
160
191
|
parts.push(syncStr);
|
|
161
192
|
parts.push(hints[hintIdx]);
|
|
193
|
+
parts.push("ctrl+c to exit");
|
|
162
194
|
hintIdx = (hintIdx + 1) % hints.length;
|
|
163
195
|
return parts.join(" · ");
|
|
164
196
|
};
|
|
@@ -189,15 +221,24 @@ export async function startChat() {
|
|
|
189
221
|
shutdown();
|
|
190
222
|
break;
|
|
191
223
|
}
|
|
192
|
-
const spinner = ora({ text:
|
|
224
|
+
const spinner = ora({ text: getThinkingText(), color: "cyan", discardStdin: false }).start();
|
|
225
|
+
const onProgress = ({ phase, toolName, toolCount, elapsedMs }) => {
|
|
226
|
+
if (phase === "tool" && toolName) {
|
|
227
|
+
const label = TOOL_LABELS[toolName] || toolName;
|
|
228
|
+
spinner.text = `${label}... ${chalk.dim(`(${toolCount} ${toolCount === 1 ? "tool" : "tools"}, ${formatDuration(elapsedMs)})`)}`;
|
|
229
|
+
}
|
|
230
|
+
else if (phase === "responding" && toolCount > 0) {
|
|
231
|
+
spinner.text = `Composing response... ${chalk.dim(`(${toolCount} tools, ${formatDuration(elapsedMs)})`)}`;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
193
234
|
try {
|
|
194
|
-
const response = await handleMessage(db, trimmed);
|
|
235
|
+
const response = await handleMessage(db, trimmed, onProgress);
|
|
195
236
|
spinner.stop();
|
|
196
237
|
console.log(`\n${formatResponse(response)}\n`);
|
|
197
238
|
}
|
|
198
239
|
catch (err) {
|
|
199
240
|
spinner.stop();
|
|
200
|
-
console.error(
|
|
241
|
+
console.error(formatError(err));
|
|
201
242
|
}
|
|
202
243
|
}
|
|
203
244
|
}
|
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 } 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() {
|
package/dist/cli/format.d.ts
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
export declare const colors: {
|
|
2
|
+
money: {
|
|
3
|
+
positive: (t: string) => string;
|
|
4
|
+
negative: (t: string) => string;
|
|
5
|
+
};
|
|
6
|
+
accent: (t: string) => string;
|
|
7
|
+
muted: (t: string) => string;
|
|
8
|
+
error: (t: string) => string;
|
|
9
|
+
success: (t: string) => string;
|
|
10
|
+
warning: (t: string) => string;
|
|
11
|
+
info: (t: string) => string;
|
|
12
|
+
};
|
|
13
|
+
export declare function formatDuration(ms: number): string;
|
|
14
|
+
export declare function formatCount(n: number): string;
|
|
15
|
+
export declare function formatError(error: any, context?: string): string;
|
|
1
16
|
export declare function formatMoney(n: number): string;
|
|
2
17
|
export declare function formatMoneyColored(n: number, invert?: boolean): string;
|
|
3
18
|
export declare function progressBar(pct: number, width?: number): string;
|
package/dist/cli/format.js
CHANGED
|
@@ -1,4 +1,61 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
+
// ─── Color Palette ─── //
|
|
3
|
+
export const colors = {
|
|
4
|
+
money: {
|
|
5
|
+
positive: (t) => chalk.green(t),
|
|
6
|
+
negative: (t) => chalk.red(t),
|
|
7
|
+
},
|
|
8
|
+
accent: (t) => chalk.cyan(t),
|
|
9
|
+
muted: (t) => chalk.dim(t),
|
|
10
|
+
error: (t) => chalk.red(t),
|
|
11
|
+
success: (t) => chalk.green(t),
|
|
12
|
+
warning: (t) => chalk.yellow(t),
|
|
13
|
+
info: (t) => chalk.blue(t),
|
|
14
|
+
};
|
|
15
|
+
// ─── Number & Duration Formatting ─── //
|
|
16
|
+
export function formatDuration(ms) {
|
|
17
|
+
const s = Math.floor(ms / 1000);
|
|
18
|
+
if (s < 1)
|
|
19
|
+
return "< 1s";
|
|
20
|
+
if (s < 60)
|
|
21
|
+
return `${s}s`;
|
|
22
|
+
const m = Math.floor(s / 60);
|
|
23
|
+
const rem = s % 60;
|
|
24
|
+
if (m < 60)
|
|
25
|
+
return rem > 0 ? `${m}m ${rem}s` : `${m}m`;
|
|
26
|
+
const h = Math.floor(m / 60);
|
|
27
|
+
return `${h}h ${m % 60}m`;
|
|
28
|
+
}
|
|
29
|
+
export function formatCount(n) {
|
|
30
|
+
return n.toLocaleString();
|
|
31
|
+
}
|
|
32
|
+
// ─── Error Display ─── //
|
|
33
|
+
const ERROR_MAP = {
|
|
34
|
+
"401": { msg: "Invalid API key.", action: "Run `ray setup` to reconfigure your credentials." },
|
|
35
|
+
"403": { msg: "API key rejected.", action: "Run `ray billing` to check status, or `ray setup` to reconfigure." },
|
|
36
|
+
"429": { msg: "Rate limited.", action: "Wait a moment and try again." },
|
|
37
|
+
network: { msg: "Could not reach the server.", action: "Check your internet connection." },
|
|
38
|
+
plaid: { msg: "Bank connection issue.", action: "Run `ray link` to reconnect." },
|
|
39
|
+
decrypt: { msg: "Could not decrypt your data.", action: "Check your encryption key in `ray setup`." },
|
|
40
|
+
};
|
|
41
|
+
export function formatError(error, context) {
|
|
42
|
+
let key = "unknown";
|
|
43
|
+
if (error.status)
|
|
44
|
+
key = String(error.status);
|
|
45
|
+
else if (error.code === "ENOTFOUND" || error.code === "ECONNREFUSED" || error.code === "ETIMEDOUT")
|
|
46
|
+
key = "network";
|
|
47
|
+
else if (error.message?.includes("Plaid") || error.message?.includes("plaid"))
|
|
48
|
+
key = "plaid";
|
|
49
|
+
else if (error.message?.includes("decrypt"))
|
|
50
|
+
key = "decrypt";
|
|
51
|
+
const mapped = ERROR_MAP[key];
|
|
52
|
+
if (mapped) {
|
|
53
|
+
return `${chalk.red("✗")} ${mapped.msg} ${chalk.dim(mapped.action)}`;
|
|
54
|
+
}
|
|
55
|
+
const safeMsg = error.message || "Something went wrong.";
|
|
56
|
+
return `${chalk.red("✗")} ${context ? context + ": " : ""}${safeMsg}`;
|
|
57
|
+
}
|
|
58
|
+
// ─── Money Formatting ─── //
|
|
2
59
|
export function formatMoney(n) {
|
|
3
60
|
const abs = Math.abs(n);
|
|
4
61
|
const formatted = "$" + abs.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
package/dist/cli/setup.js
CHANGED
|
@@ -3,6 +3,9 @@ import { existsSync, mkdirSync } from "fs";
|
|
|
3
3
|
import { dirname } from "path";
|
|
4
4
|
import { config, saveConfig, isConfigured, RAY_PROXY_BASE } from "../config.js";
|
|
5
5
|
import { heading, dim } from "./format.js";
|
|
6
|
+
function stepHeader(current, total) {
|
|
7
|
+
return chalk.dim(`Step ${current}/${total}`);
|
|
8
|
+
}
|
|
6
9
|
export async function runSetup() {
|
|
7
10
|
const inquirer = (await import("inquirer")).default;
|
|
8
11
|
console.log(`\n${heading("Ray Finance Setup")}\n`);
|
|
@@ -27,12 +30,14 @@ export async function runSetup() {
|
|
|
27
30
|
}]);
|
|
28
31
|
let canLink = false;
|
|
29
32
|
if (setupMode === "managed") {
|
|
33
|
+
console.log(stepHeader(1, 3));
|
|
30
34
|
const { userName } = await inquirer.prompt([{
|
|
31
35
|
type: "input",
|
|
32
36
|
name: "userName",
|
|
33
37
|
message: "Your name:",
|
|
34
38
|
default: config.userName !== "User" ? config.userName : undefined,
|
|
35
39
|
}]);
|
|
40
|
+
console.log(stepHeader(2, 3));
|
|
36
41
|
const { hasKey } = await inquirer.prompt([{
|
|
37
42
|
type: "list",
|
|
38
43
|
name: "hasKey",
|
|
@@ -88,6 +93,7 @@ export async function runSetup() {
|
|
|
88
93
|
canLink = true;
|
|
89
94
|
}
|
|
90
95
|
else {
|
|
96
|
+
console.log(stepHeader(1, 4));
|
|
91
97
|
const answers = await inquirer.prompt([
|
|
92
98
|
{
|
|
93
99
|
type: "input",
|
|
@@ -157,18 +163,47 @@ export async function runSetup() {
|
|
|
157
163
|
const { createContextTemplate } = await import("../ai/context.js");
|
|
158
164
|
createContextTemplate(config.userName);
|
|
159
165
|
console.log(`\n${chalk.green("✓")} Config saved`);
|
|
160
|
-
|
|
166
|
+
console.log(`${chalk.green("✓")} Database initialized`);
|
|
167
|
+
console.log(`${chalk.green("✓")} Ready to go\n`);
|
|
168
|
+
// Ask to link first account
|
|
161
169
|
if (canLink) {
|
|
162
|
-
console.log();
|
|
163
|
-
const {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
console.log(stepHeader(setupMode === "managed" ? 3 : 4, setupMode === "managed" ? 3 : 4));
|
|
171
|
+
const { wantLink } = await inquirer.prompt([{
|
|
172
|
+
type: "confirm",
|
|
173
|
+
name: "wantLink",
|
|
174
|
+
message: "Link your first bank account now?",
|
|
175
|
+
default: true,
|
|
176
|
+
}]);
|
|
177
|
+
if (wantLink) {
|
|
178
|
+
const { runLink } = await import("./commands.js");
|
|
179
|
+
await runLink();
|
|
180
|
+
// Sync immediately after linking
|
|
181
|
+
const ora = (await import("ora")).default;
|
|
182
|
+
const spinner = ora("Syncing your transactions...").start();
|
|
183
|
+
try {
|
|
184
|
+
const { getDb } = await import("../db/connection.js");
|
|
185
|
+
const { runDailySync } = await import("../daily-sync.js");
|
|
186
|
+
await runDailySync(getDb());
|
|
187
|
+
spinner.succeed("Transactions synced!");
|
|
188
|
+
console.log(chalk.dim(" Note: some banks take a few hours to deliver all transactions."));
|
|
189
|
+
console.log(chalk.dim(" Ray will re-sync in the background to pick up anything missing.\n"));
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
spinner.fail(`Sync failed: ${err.message}`);
|
|
193
|
+
}
|
|
194
|
+
// Auto-schedule daily sync at 6am
|
|
195
|
+
if (!config.syncSchedule) {
|
|
196
|
+
saveConfig({ syncSchedule: "06:00" });
|
|
197
|
+
const { installSyncSchedule } = await import("./scheduler.js");
|
|
198
|
+
installSyncSchedule("06:00");
|
|
199
|
+
console.log(`${chalk.green("✓")} Daily sync scheduled at 6:00 AM`);
|
|
200
|
+
}
|
|
201
|
+
// Go straight into chat
|
|
202
|
+
console.log();
|
|
203
|
+
const { startChat } = await import("./chat.js");
|
|
204
|
+
await startChat();
|
|
205
|
+
return;
|
|
171
206
|
}
|
|
172
207
|
}
|
|
173
|
-
console.log(
|
|
208
|
+
console.log(`Run ${chalk.bold("ray")} to start chatting.\n`);
|
|
174
209
|
}
|
package/dist/daily-sync.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type BetterSqlite3 from "libsql";
|
|
2
2
|
type Database = BetterSqlite3.Database;
|
|
3
|
+
export interface SyncResult {
|
|
4
|
+
transactionsAdded: number;
|
|
5
|
+
institutionsSynced: number;
|
|
6
|
+
}
|
|
3
7
|
/** Run the daily sync for a single database */
|
|
4
|
-
export declare function runDailySync(db: Database): Promise<
|
|
8
|
+
export declare function runDailySync(db: Database): Promise<SyncResult>;
|
|
5
9
|
/** Run daily sync (cron / CLI entry point) */
|
|
6
10
|
export declare function runDailySyncAll(): Promise<void>;
|
|
7
11
|
export {};
|
package/dist/daily-sync.js
CHANGED
|
@@ -9,8 +9,10 @@ export async function runDailySync(db) {
|
|
|
9
9
|
.all();
|
|
10
10
|
if (institutions.length === 0) {
|
|
11
11
|
console.log("No linked institutions.");
|
|
12
|
-
return;
|
|
12
|
+
return { transactionsAdded: 0, institutionsSynced: 0 };
|
|
13
13
|
}
|
|
14
|
+
let totalAdded = 0;
|
|
15
|
+
let instSynced = 0;
|
|
14
16
|
for (const inst of institutions) {
|
|
15
17
|
if (inst.access_token === "manual") {
|
|
16
18
|
console.log(`Skipping ${inst.name} (manual entry)`);
|
|
@@ -32,12 +34,14 @@ export async function runDailySync(db) {
|
|
|
32
34
|
const products = JSON.parse(inst.products);
|
|
33
35
|
console.log(`Syncing: ${inst.name} (${products.join(", ")})`);
|
|
34
36
|
try {
|
|
37
|
+
instSynced++;
|
|
35
38
|
// Always sync balances
|
|
36
39
|
const accountCount = await syncBalances(db, accessToken);
|
|
37
40
|
console.log(` Accounts: ${accountCount}`);
|
|
38
41
|
// Sync transactions if available
|
|
39
42
|
if (products.includes("transactions")) {
|
|
40
43
|
const txResult = await syncTransactions(db, inst.item_id, accessToken, inst.cursor);
|
|
44
|
+
totalAdded += txResult.added;
|
|
41
45
|
console.log(` Transactions: +${txResult.added} ~${txResult.modified} -${txResult.removed}`);
|
|
42
46
|
}
|
|
43
47
|
// Sync investments if available
|
|
@@ -100,6 +104,7 @@ export async function runDailySync(db) {
|
|
|
100
104
|
console.log(`Auto-recategorized ${totalRecat} transaction(s).`);
|
|
101
105
|
}
|
|
102
106
|
console.log("Sync complete.");
|
|
107
|
+
return { transactionsAdded: totalAdded, institutionsSynced: instSynced };
|
|
103
108
|
}
|
|
104
109
|
/** Run daily sync (cron / CLI entry point) */
|
|
105
110
|
export async function runDailySyncAll() {
|