ray-finance 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
- An open-source CLI that connects to your bank and already knows your finances before you ask.
6
+ An open-source AI financial advisor that learns your situation and gets smarter every conversation.
7
7
  </p>
8
8
 
9
9
  <p align="center">
@@ -15,26 +15,37 @@
15
15
  <br />
16
16
 
17
17
  <p align="center">
18
- <img src=".github/ray-demo.png" alt="Ray demo" />
18
+ <img src=".github/ray-demo.png" alt="Ray demo" width="100%" />
19
19
  </p>
20
20
 
21
- Open Ray and it shows your net worth, spending vs last month, budget pacing, and upcoming bills before you type a word. Ask a question and it answers from your real data, not guesses. Local-first. Encrypted. Open source.
21
+ Tell Ray about your family, goals, and financial strategy once. From then on, every answer is grounded in your real situation — not generic advice. It connects to your bank, tracks your net worth and spending, and gives you a financial briefing before you type a word. Open source. Local-first. Encrypted.
22
22
 
23
23
  ## Features
24
24
 
25
- - **It already knows** Every conversation starts with a real-time financial briefing. Net worth, spending velocity, budget alerts, goal pace, upcoming bills, and your daily score. No "let me look that up."
26
- - **Persistent context** — Ray maintains a financial profile that evolves with you: income, goals, strategy, key decisions, and open items. It updates this context as your situation changes, so every conversation picks up where the last one left off.
27
- - **Long-term memory** — Important details from conversations are saved as memories. Mention you're saving for a house or switching jobs and Ray remembers without you repeating yourself.
28
- - **Bank sync via Plaid** — Connect checking, savings, credit cards, investments, and loans
29
- - **Encrypted local database** — All data stays on your machine in an AES-256 encrypted SQLite database
30
- - **Daily scoring** — A 0-100 behavior score with streaks and 14 unlockable achievements. No restaurants for a week? That's Kitchen Hero. Five zero-spend days? Monk Mode.
25
+ ### It gets smarter every conversation
26
+
27
+ - **Your situation, always loaded** — Every conversation starts with your financial profile: family, income, goals, strategy, key decisions, and open items. Ray reads it all before you type a word.
28
+ - **Self-updating context** — Got a raise? Had a baby? Decided to pay off debt aggressively? Ray updates your profile automatically when your situation changes.
29
+ - **Long-term memory** — Mention you're saving for a house or that you cancelled a subscription. Ray remembers across every future conversation.
30
+
31
+ ### Stay on track without trying
32
+
31
33
  - **CFO personality** — Ray doesn't list options. It tells you what it would do and why, references your goals, and flags problems you haven't noticed yet.
32
- - **Budgets and goals** — Track spending limits by category and progress toward financial goals
34
+ - **Daily scoring** — A 0-100 behavior score with streaks and 14 unlockable achievements. No restaurants for a week? That's Kitchen Hero. Five zero-spend days? Monk Mode.
35
+ - **Budgets and goals** — Track spending limits by category and progress toward financial goals.
36
+ - **Smart alerts** — Large transactions, low balances, budget overruns.
37
+
38
+ ### Your data never leaves your machine
39
+
40
+ - **Encrypted local database** — All data stays on your machine in an AES-256 encrypted SQLite database.
33
41
  - **PII masking** — Names, account numbers, and identifying details are scrubbed before anything reaches the AI. Your data is analyzed, not exposed.
34
- - **Smart alerts** — Large transactions, low balances, budget overruns
35
- - **Auto-recategorization** Define rules to automatically re-label transactions
36
- - **Scheduled daily sync** — Automatic bank sync via launchd (macOS) or cron (Linux)
37
- - **Export/import** — Back up and restore your financial data
42
+
43
+ ### Set it and forget it
44
+
45
+ - **Bank sync via Plaid** — Connect checking, savings, credit cards, investments, and loans.
46
+ - **Scheduled daily sync** — Automatic bank sync via launchd (macOS) or cron (Linux).
47
+ - **Auto-recategorization** — Define rules to automatically re-label transactions.
48
+ - **Export/import** — Back up and restore your financial data.
38
49
 
39
50
  ## Install
40
51
 
@@ -70,7 +81,7 @@ ray setup
70
81
 
71
82
  The setup wizard offers two modes:
72
83
 
73
- ### Quick setup (managed)
84
+ ### Pro (quick setup)
74
85
 
75
86
  We handle the API keys. Your data stays local. $10/mo.
76
87
 
@@ -79,7 +90,7 @@ We handle the API keys. Your data stays local. $10/mo.
79
90
  3. Link your accounts — checking, savings, credit cards, investments, loans, mortgage
80
91
  4. Done — daily sync auto-scheduled at 6am
81
92
 
82
- ### Self-hosted
93
+ ### Bring your own keys
83
94
 
84
95
  Bring your own Anthropic and Plaid credentials. Free forever.
85
96
 
@@ -99,17 +110,24 @@ Run `ray --help` to see all available commands.
99
110
  | `ray --demo <cmd>` | Run any command against demo data |
100
111
  | `ray setup` | Configure API keys and preferences |
101
112
  | `ray link` | Connect a new bank account |
113
+ | `ray add` | Add a manual account (home, car, crypto, etc.) |
114
+ | `ray remove` | Remove a manual account |
102
115
  | `ray sync` | Pull latest transactions and balances |
103
116
  | `ray status` | Quick financial dashboard |
117
+ | `ray accounts` | Linked accounts with balances |
104
118
  | `ray transactions` | Recent transactions (filterable by category, merchant) |
105
119
  | `ray spending [period]` | Spending breakdown by category |
106
120
  | `ray budgets` | Budget status and overruns |
107
121
  | `ray goals` | Financial goal progress |
122
+ | `ray bills` | Upcoming bills |
123
+ | `ray recap [period]` | Monthly spending recap |
108
124
  | `ray score` | Daily score, streaks, and achievements |
109
125
  | `ray alerts` | Active financial alerts |
110
126
  | `ray export [path]` | Export data to a backup file |
111
127
  | `ray import <path>` | Restore from a backup file |
112
128
  | `ray billing` | Manage your Ray subscription (managed mode only) |
129
+ | `ray update` | Update Ray to the latest version |
130
+ | `ray doctor` | Check system health |
113
131
 
114
132
  ## How It Works
115
133
 
package/dist/ai/tools.js CHANGED
@@ -378,7 +378,7 @@ export async function executeTool(db, toolName, toolInput) {
378
378
  }
379
379
  case "set_budget": {
380
380
  db.prepare(`INSERT INTO budgets (category, monthly_limit) VALUES (?, ?)
381
- ON CONFLICT(category) DO UPDATE SET monthly_limit = excluded.monthly_limit`).run(toolInput.category, toolInput.monthly_limit);
381
+ ON CONFLICT(category, period) DO UPDATE SET monthly_limit = excluded.monthly_limit`).run(toolInput.category, toolInput.monthly_limit);
382
382
  return `Budget set: ${categoryLabel(toolInput.category)} at ${formatMoney(toolInput.monthly_limit)}/month`;
383
383
  }
384
384
  case "get_goals": {
@@ -352,26 +352,58 @@ export async function runAdd() {
352
352
  export async function runRemove() {
353
353
  const readline = await import("readline");
354
354
  const db = getDb();
355
- const accounts = getManualAccounts(db);
356
- if (accounts.length === 0) {
357
- console.log("\nNo manual accounts. Use 'ray add' to create one.");
355
+ const entries = [];
356
+ // Linked institutions (exclude manual-assets)
357
+ const institutions = db.prepare(`SELECT item_id, name FROM institutions WHERE item_id != 'manual-assets' ORDER BY created_at`).all();
358
+ for (const inst of institutions) {
359
+ entries.push({ kind: "institution", item_id: inst.item_id, name: inst.name });
360
+ }
361
+ // Manual accounts
362
+ const manuals = getManualAccounts(db);
363
+ for (const a of manuals) {
364
+ entries.push({ kind: "manual", account_id: a.account_id, name: a.name, balance: a.current_balance, type: a.type, listing_url: a.listing_url });
365
+ }
366
+ if (entries.length === 0) {
367
+ console.log("\nNo accounts to remove. Use 'ray link' or 'ray add' to add one.");
358
368
  return;
359
369
  }
360
- console.log(`\n${heading("Manual Accounts")}\n`);
361
- for (let i = 0; i < accounts.length; i++) {
362
- const a = accounts[i];
363
- const typeLabel = a.type === "loan" || a.type === "credit" ? "liability" : "asset";
364
- const url = a.listing_url ? dim(` ${a.listing_url}`) : "";
365
- console.log(` ${dim(`${i + 1}.`)} ${a.name} ${rawFormatMoney(a.current_balance)} (${typeLabel})${url}`);
370
+ console.log(`\n${heading("Accounts")}\n`);
371
+ for (let i = 0; i < entries.length; i++) {
372
+ const e = entries[i];
373
+ if (e.kind === "institution") {
374
+ const acctCount = db.prepare(`SELECT COUNT(*) as c FROM accounts WHERE item_id = ?`).get(e.item_id).c;
375
+ console.log(` ${dim(`${i + 1}.`)} ${e.name} ${dim(`(${acctCount} account${acctCount !== 1 ? "s" : ""}, linked)`)}`);
376
+ }
377
+ else {
378
+ const typeLabel = e.type === "loan" || e.type === "credit" ? "liability" : "asset";
379
+ const url = e.listing_url ? dim(` — ${e.listing_url}`) : "";
380
+ console.log(` ${dim(`${i + 1}.`)} ${e.name} ${rawFormatMoney(e.balance)} (${typeLabel})${url}`);
381
+ }
366
382
  }
367
383
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
368
384
  const answer = (await new Promise(resolve => rl.question(`\n Remove which? (number, or Enter to cancel): `, resolve))).trim();
369
385
  rl.close();
370
386
  const idx = parseInt(answer, 10) - 1;
371
- if (isNaN(idx) || idx < 0 || idx >= accounts.length)
387
+ if (isNaN(idx) || idx < 0 || idx >= entries.length)
372
388
  return;
373
- removeManualAccount(db, accounts[idx].account_id);
374
- console.log(chalk.green(`\n Removed ${accounts[idx].name}.`));
389
+ const entry = entries[idx];
390
+ if (entry.kind === "manual") {
391
+ removeManualAccount(db, entry.account_id);
392
+ }
393
+ else {
394
+ // Remove all data for this institution
395
+ const accounts = db.prepare(`SELECT account_id FROM accounts WHERE item_id = ?`).all(entry.item_id);
396
+ for (const acct of accounts) {
397
+ db.prepare(`DELETE FROM transactions WHERE account_id = ?`).run(acct.account_id);
398
+ db.prepare(`DELETE FROM holdings WHERE account_id = ?`).run(acct.account_id);
399
+ db.prepare(`DELETE FROM investment_transactions WHERE account_id = ?`).run(acct.account_id);
400
+ db.prepare(`DELETE FROM liabilities WHERE account_id = ?`).run(acct.account_id);
401
+ db.prepare(`DELETE FROM recurring WHERE account_id = ?`).run(acct.account_id);
402
+ }
403
+ db.prepare(`DELETE FROM accounts WHERE item_id = ?`).run(entry.item_id);
404
+ db.prepare(`DELETE FROM institutions WHERE item_id = ?`).run(entry.item_id);
405
+ }
406
+ console.log(chalk.green(`\n Removed ${entry.name}.`));
375
407
  console.log();
376
408
  }
377
409
  export function showAlerts() {
package/dist/cli/index.js CHANGED
@@ -70,7 +70,7 @@ program
70
70
  });
71
71
  program
72
72
  .command("remove")
73
- .description("Remove a manual account")
73
+ .description("Remove a linked bank or manual account")
74
74
  .action(async () => {
75
75
  ensureConfigured();
76
76
  const { runRemove } = await import("./commands.js");
@@ -192,25 +192,36 @@ program
192
192
  const open = (await import("open")).default;
193
193
  console.log("Opening billing portal...");
194
194
  try {
195
- const resp = await fetch(`${RAY_PROXY_BASE.replace("/v1", "")}/stripe/portal`, {
195
+ const resp = await fetch(`${RAY_PROXY_BASE}/stripe/portal`, {
196
196
  method: "POST",
197
197
  headers: {
198
198
  "content-type": "application/json",
199
199
  "Authorization": `Bearer ${config.rayApiKey}`,
200
200
  },
201
201
  });
202
+ if (!resp.ok) {
203
+ const text = await resp.text().catch(() => "");
204
+ const msg = (() => { try {
205
+ return JSON.parse(text).error;
206
+ }
207
+ catch {
208
+ return text;
209
+ } })();
210
+ console.error(`Could not open billing portal (${resp.status}): ${msg || "unknown error"}`);
211
+ return;
212
+ }
202
213
  const { url } = await resp.json();
203
214
  // Only open URLs from trusted domains
204
215
  const parsed = new URL(url);
205
216
  if (!parsed.hostname.endsWith("stripe.com") && !parsed.hostname.endsWith("rayfinance.app")) {
206
- console.error("Unexpected billing URL. Visit https://rayfinance.app/billing");
217
+ console.error("Unexpected billing URL.");
207
218
  }
208
219
  else {
209
220
  await open(url);
210
221
  }
211
222
  }
212
- catch {
213
- console.error("Could not open billing portal. Visit https://rayfinance.app/billing");
223
+ catch (err) {
224
+ console.error("Could not open billing portal:", err.message);
214
225
  }
215
226
  });
216
227
  program
@@ -249,7 +260,7 @@ program.configureHelp({
249
260
  { name: "setup", desc: "Configure Ray (API keys, preferences)" },
250
261
  { name: "link", desc: "Link a new financial account via Plaid" },
251
262
  { name: "add", desc: "Add a manual account (home, car, crypto, etc.)" },
252
- { name: "remove", desc: "Remove a manual account" },
263
+ { name: "remove", desc: "Remove a linked bank or manual account" },
253
264
  { name: "sync", desc: "Sync transactions from linked banks" },
254
265
  { name: "accounts", desc: "Show linked accounts and balances" },
255
266
  { name: "status", desc: "Show financial overview" },
package/dist/cli/setup.js CHANGED
@@ -31,7 +31,7 @@ export async function runSetup() {
31
31
  message: "How would you like to set up Ray?",
32
32
  choices: [
33
33
  { name: "Quick setup — we handle the API keys, your data stays local", value: "managed" },
34
- { name: "Self-hostedbring your own Anthropic and Plaid credentials", value: "selfhosted" },
34
+ { name: "Bring your own keys use your own Anthropic and Plaid credentials", value: "selfhosted" },
35
35
  ],
36
36
  }]);
37
37
  let canLink = false;
package/dist/server.js CHANGED
@@ -75,7 +75,15 @@ export function startLinkServer() {
75
75
  }
76
76
  catch (error) {
77
77
  console.error("Link token error:", error.message);
78
- res.status(500).json({ error: "Failed to create link token" });
78
+ const plaidStatus = error?.response?.status;
79
+ if (plaidStatus === 400 || plaidStatus === 401 || plaidStatus === 403) {
80
+ res.status(500).json({
81
+ error: "Plaid credentials error — make sure you're using production (not sandbox) keys. Check PLAID_CLIENT_ID and PLAID_SECRET in ~/.ray/config.json.",
82
+ });
83
+ }
84
+ else {
85
+ res.status(500).json({ error: "Failed to create link token: " + (error.message || "unknown error") });
86
+ }
79
87
  }
80
88
  });
81
89
  // Exchange public token
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ray-finance",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Local-first CLI that turns your bank data into a personal AI financial advisor",
5
5
  "type": "module",
6
6
  "license": "MIT",