ray-finance 0.3.5 → 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/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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ray-finance",
3
- "version": "0.3.5",
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",