ray-finance 0.3.3 → 0.3.4

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/cli/chat.js CHANGED
@@ -5,7 +5,35 @@ import { banner, formatResponse, formatDuration, formatError } from "./format.js
5
5
  function rawReadLine(prompt, belowLines) {
6
6
  return new Promise((resolve) => {
7
7
  let buf = "";
8
+ let cursor = 0; // cursor position within buf
8
9
  const out = process.stdout;
10
+ const promptLen = stripAnsi(prompt).length;
11
+ // Redraws the buffer from the prompt onward and repositions the cursor
12
+ const redraw = () => {
13
+ out.write("\r" + prompt + buf + "\x1b[K"); // clear to end of line
14
+ // Move cursor back to the correct position
15
+ const back = buf.length - cursor;
16
+ if (back > 0)
17
+ out.write(`\x1b[${back}D`);
18
+ };
19
+ // Find the start of the previous word boundary
20
+ const wordLeft = () => {
21
+ let p = cursor;
22
+ while (p > 0 && buf[p - 1] === " ")
23
+ p--; // skip trailing spaces
24
+ while (p > 0 && buf[p - 1] !== " ")
25
+ p--; // skip word chars
26
+ return p;
27
+ };
28
+ // Find the end of the next word boundary
29
+ const wordRight = () => {
30
+ let p = cursor;
31
+ while (p < buf.length && buf[p] !== " ")
32
+ p++; // skip word chars
33
+ while (p < buf.length && buf[p] === " ")
34
+ p++; // skip trailing spaces
35
+ return p;
36
+ };
9
37
  // Render: prompt on current line, then content below, then move cursor back
10
38
  out.write(prompt);
11
39
  if (belowLines.length > 0) {
@@ -32,9 +60,51 @@ function rawReadLine(prompt, belowLines) {
32
60
  resolve("\x03");
33
61
  return;
34
62
  }
63
+ // Ctrl+A — beginning of line
64
+ if (code === 1) {
65
+ if (cursor > 0) {
66
+ out.write(`\x1b[${cursor}D`);
67
+ cursor = 0;
68
+ }
69
+ continue;
70
+ }
71
+ // Ctrl+E — end of line
72
+ if (code === 5) {
73
+ if (cursor < buf.length) {
74
+ out.write(`\x1b[${buf.length - cursor}C`);
75
+ cursor = buf.length;
76
+ }
77
+ continue;
78
+ }
79
+ // Ctrl+K — delete from cursor to end of line
80
+ if (code === 11) {
81
+ buf = buf.slice(0, cursor);
82
+ out.write("\x1b[K");
83
+ continue;
84
+ }
85
+ // Ctrl+U — delete from cursor to beginning of line
86
+ if (code === 21) {
87
+ buf = buf.slice(cursor);
88
+ cursor = 0;
89
+ redraw();
90
+ continue;
91
+ }
92
+ // Ctrl+W — delete word backward
93
+ if (code === 23) {
94
+ if (cursor > 0) {
95
+ const target = wordLeft();
96
+ buf = buf.slice(0, target) + buf.slice(cursor);
97
+ cursor = target;
98
+ redraw();
99
+ }
100
+ continue;
101
+ }
35
102
  // Enter
36
103
  if (code === 13) {
37
104
  cleanup();
105
+ // Move cursor to end of buf first
106
+ if (cursor < buf.length)
107
+ out.write(`\x1b[${buf.length - cursor}C`);
38
108
  // Move past the below-content lines, then newline
39
109
  for (let j = 0; j < belowLines.length; j++)
40
110
  out.write("\x1b[1B");
@@ -44,31 +114,136 @@ function rawReadLine(prompt, belowLines) {
44
114
  }
45
115
  // Backspace
46
116
  if (code === 127 || code === 8) {
47
- if (buf.length > 0) {
48
- buf = buf.slice(0, -1);
49
- out.write("\b \b");
117
+ if (cursor > 0) {
118
+ buf = buf.slice(0, cursor - 1) + buf.slice(cursor);
119
+ cursor--;
120
+ redraw();
50
121
  }
51
122
  continue;
52
123
  }
53
- // Skip escape sequences (arrow keys etc.)
124
+ // Escape sequences (arrow keys, Option+key, etc.)
54
125
  if (code === 27) {
126
+ // Option+Backspace — ESC followed by DEL (0x7f)
127
+ if (i + 1 < chunk.length && chunk.charCodeAt(i + 1) === 127) {
128
+ i++; // consume the DEL
129
+ if (cursor > 0) {
130
+ const target = wordLeft();
131
+ buf = buf.slice(0, target) + buf.slice(cursor);
132
+ cursor = target;
133
+ redraw();
134
+ }
135
+ continue;
136
+ }
137
+ // Option+b / Option+f — ESC followed by 'b' or 'f'
138
+ if (i + 1 < chunk.length && chunk[i + 1] === "b") {
139
+ i++;
140
+ const target = wordLeft();
141
+ if (target < cursor) {
142
+ out.write(`\x1b[${cursor - target}D`);
143
+ cursor = target;
144
+ }
145
+ continue;
146
+ }
147
+ if (i + 1 < chunk.length && chunk[i + 1] === "f") {
148
+ i++;
149
+ const target = wordRight();
150
+ if (target > cursor) {
151
+ out.write(`\x1b[${target - cursor}C`);
152
+ cursor = target;
153
+ }
154
+ continue;
155
+ }
55
156
  if (i + 1 < chunk.length && chunk[i + 1] === "[") {
56
- i += 2;
57
- while (i < chunk.length && chunk.charCodeAt(i) < 64)
157
+ i += 2; // skip past ESC [
158
+ // Collect any intermediate bytes (modifiers like "1;3")
159
+ let seq = "";
160
+ while (i < chunk.length && chunk.charCodeAt(i) < 64) {
161
+ seq += chunk[i];
58
162
  i++;
163
+ }
164
+ if (i < chunk.length) {
165
+ const final = chunk[i];
166
+ // Modifier keys: ;3 = Option, ;5 = Ctrl, ;9 = Cmd (Kitty protocol)
167
+ const isWordMod = seq === "1;3" || seq === "1;5" || seq === "1;9";
168
+ const isCmd = seq === "1;9";
169
+ if (final === "D") {
170
+ if (isWordMod) {
171
+ // Option/Ctrl/Cmd+Left — word backward
172
+ const target = wordLeft();
173
+ if (target < cursor) {
174
+ out.write(`\x1b[${cursor - target}D`);
175
+ cursor = target;
176
+ }
177
+ }
178
+ else if (cursor > 0) {
179
+ cursor--;
180
+ out.write("\x1b[D");
181
+ }
182
+ }
183
+ else if (final === "C") {
184
+ if (isWordMod) {
185
+ // Option/Ctrl/Cmd+Right — word forward
186
+ const target = wordRight();
187
+ if (target > cursor) {
188
+ out.write(`\x1b[${target - cursor}C`);
189
+ cursor = target;
190
+ }
191
+ }
192
+ else if (cursor < buf.length) {
193
+ cursor++;
194
+ out.write("\x1b[C");
195
+ }
196
+ }
197
+ else if (final === "H") {
198
+ // Home
199
+ if (cursor > 0) {
200
+ out.write(`\x1b[${cursor}D`);
201
+ cursor = 0;
202
+ }
203
+ }
204
+ else if (final === "F") {
205
+ // End
206
+ if (cursor < buf.length) {
207
+ out.write(`\x1b[${buf.length - cursor}C`);
208
+ cursor = buf.length;
209
+ }
210
+ }
211
+ else if (final === "u") {
212
+ // Kitty keyboard protocol: ESC [ codepoint ; modifier u
213
+ const parts = seq.split(";");
214
+ const codepoint = parseInt(parts[0], 10);
215
+ const mod = parts.length > 1 ? parseInt(parts[1], 10) : 1;
216
+ const hasCmd = (mod - 1) & 8; // super/cmd bit
217
+ const hasCtrl = (mod - 1) & 4; // ctrl bit
218
+ if (codepoint === 127 && (hasCmd || hasCtrl)) {
219
+ // Cmd+Backspace / Ctrl+Backspace — delete to line start
220
+ if (cursor > 0) {
221
+ buf = buf.slice(cursor);
222
+ cursor = 0;
223
+ redraw();
224
+ }
225
+ }
226
+ }
227
+ // Ignore other sequences (up/down, etc.)
228
+ }
59
229
  }
60
230
  continue;
61
231
  }
62
232
  // Printable characters
63
233
  if (code >= 32) {
64
- buf += chunk[i];
65
- out.write(chunk[i]);
234
+ buf = buf.slice(0, cursor) + chunk[i] + buf.slice(cursor);
235
+ cursor++;
236
+ redraw();
66
237
  }
67
238
  }
68
239
  };
69
240
  process.stdin.on("data", onData);
70
241
  });
71
242
  }
243
+ /** Strip ANSI escape codes to get visible length */
244
+ function stripAnsi(str) {
245
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
246
+ }
72
247
  const THINKING_PHRASES = [
73
248
  "Thinking...",
74
249
  "Crunching numbers...",
@@ -14,3 +14,5 @@ export declare function showScore(): void;
14
14
  export declare function runAdd(): Promise<void>;
15
15
  export declare function runRemove(): Promise<void>;
16
16
  export declare function showAlerts(): void;
17
+ export declare function showBills(days?: number): void;
18
+ export declare function showRecap(period?: string): void;
@@ -1,6 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import { getDb } from "../db/connection.js";
3
- import { getNetWorth, getTransactionsFiltered, getBudgetStatuses, getGoals, getCashFlowThisMonth, formatMoney as rawFormatMoney, categoryLabel, } from "../queries/index.js";
3
+ import { getNetWorth, getTransactionsFiltered, getBudgetStatuses, getGoals, getCashFlowThisMonth, compareSpending, getNetWorthTrend, formatMoney as rawFormatMoney, categoryLabel, } from "../queries/index.js";
4
4
  import { getLatestScore, getAchievements, getMonthlySavings } from "../scoring/index.js";
5
5
  import { generateAlerts } from "../alerts/index.js";
6
6
  import { runDailySync } from "../daily-sync.js";
@@ -388,3 +388,138 @@ export function showAlerts() {
388
388
  }
389
389
  console.log();
390
390
  }
391
+ export function showBills(days = 7) {
392
+ const db = getDb();
393
+ const now = new Date();
394
+ const todayDay = now.getDate();
395
+ const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
396
+ const endDay = todayDay + days;
397
+ let bills = [];
398
+ if (endDay <= daysInMonth) {
399
+ bills = db.prepare(`SELECT name, amount, day_of_month FROM recurring_bills WHERE day_of_month BETWEEN ? AND ? ORDER BY day_of_month`).all(todayDay + 1, endDay);
400
+ }
401
+ else {
402
+ // Wraparound into next month
403
+ const thisMonth = db.prepare(`SELECT name, amount, day_of_month FROM recurring_bills WHERE day_of_month BETWEEN ? AND ? ORDER BY day_of_month`).all(todayDay + 1, daysInMonth);
404
+ const nextMonth = db.prepare(`SELECT name, amount, day_of_month FROM recurring_bills WHERE day_of_month BETWEEN 1 AND ? ORDER BY day_of_month`).all(endDay - daysInMonth);
405
+ bills = [...thisMonth, ...nextMonth];
406
+ }
407
+ if (bills.length === 0) {
408
+ console.log(`\nNo upcoming bills in the next ${days} days.`);
409
+ return;
410
+ }
411
+ console.log(`\n${heading("Upcoming Bills")} ${dim(`next ${days} days`)}\n`);
412
+ const maxName = Math.max(...bills.map(b => b.name.length));
413
+ let total = 0;
414
+ for (const b of bills) {
415
+ // Calculate the actual date for this bill
416
+ let billDate;
417
+ if (b.day_of_month > todayDay) {
418
+ billDate = new Date(now.getFullYear(), now.getMonth(), b.day_of_month);
419
+ }
420
+ else {
421
+ billDate = new Date(now.getFullYear(), now.getMonth() + 1, b.day_of_month);
422
+ }
423
+ const dateStr = billDate.toLocaleDateString("en-US", { month: "short", day: "numeric" });
424
+ console.log(` ${dim(dateStr.padEnd(8))}${b.name.padEnd(maxName + 2)}${rawFormatMoney(b.amount).padStart(10)}`);
425
+ total += b.amount;
426
+ }
427
+ console.log(`\n ${dim("Total due:".padEnd(maxName + 10))}${chalk.bold(rawFormatMoney(total))}`);
428
+ console.log();
429
+ }
430
+ export function showRecap(period = "last_month") {
431
+ const db = getDb();
432
+ const now = new Date();
433
+ const y = now.getFullYear();
434
+ const m = now.getMonth();
435
+ let start, end, label;
436
+ let prevStart, prevEnd;
437
+ if (period === "this_month") {
438
+ start = new Date(y, m, 1).toISOString().slice(0, 10);
439
+ end = now.toISOString().slice(0, 10);
440
+ label = now.toLocaleDateString("en-US", { month: "long", year: "numeric" }) + " (so far)";
441
+ prevStart = new Date(y, m - 1, 1).toISOString().slice(0, 10);
442
+ prevEnd = new Date(y, m, 0).toISOString().slice(0, 10);
443
+ }
444
+ else {
445
+ // last_month
446
+ start = new Date(y, m - 1, 1).toISOString().slice(0, 10);
447
+ end = new Date(y, m, 0).toISOString().slice(0, 10);
448
+ const lastMonth = new Date(y, m - 1, 1);
449
+ label = lastMonth.toLocaleDateString("en-US", { month: "long", year: "numeric" });
450
+ prevStart = new Date(y, m - 2, 1).toISOString().slice(0, 10);
451
+ prevEnd = new Date(y, m - 1, 0).toISOString().slice(0, 10);
452
+ }
453
+ // Spending this period
454
+ const spending = db.prepare(`SELECT SUM(amount) as total, COUNT(*) as count FROM transactions
455
+ WHERE amount > 0 AND date BETWEEN ? AND ? AND pending = 0
456
+ AND category NOT IN ('TRANSFER_OUT', 'TRANSFER_IN', 'LOAN_PAYMENTS')`).get(start, end);
457
+ // Income this period
458
+ const income = db.prepare(`SELECT COALESCE(SUM(ABS(amount)), 0) as total FROM transactions
459
+ WHERE amount < 0 AND date BETWEEN ? AND ? AND pending = 0
460
+ AND category NOT IN ('TRANSFER_IN')`).get(start, end);
461
+ const totalSpent = spending.total || 0;
462
+ const txnCount = spending.count || 0;
463
+ if (txnCount === 0) {
464
+ console.log(`\nNo transaction data for ${label}.`);
465
+ return;
466
+ }
467
+ console.log(`\n${heading("Recap")} ${dim(label)}\n`);
468
+ // ── Spending summary with comparison ──
469
+ const cmp = compareSpending(db, prevStart, prevEnd, start, end);
470
+ let spendLine = ` Spent ${chalk.bold(rawFormatMoney(totalSpent))} across ${txnCount} transactions`;
471
+ if (cmp.period1Total > 0) {
472
+ const pct = Math.abs(cmp.pctChange);
473
+ const dir = cmp.pctChange <= 0 ? chalk.green(`${pct}% less`) : chalk.red(`${pct}% more`);
474
+ spendLine += ` — ${dir} than prior month`;
475
+ }
476
+ console.log(spendLine);
477
+ // ── Income ──
478
+ if (income.total > 0) {
479
+ const net = income.total - totalSpent;
480
+ const savingsRate = Math.round((net / income.total) * 100);
481
+ console.log(` Earned ${chalk.bold(rawFormatMoney(income.total))} Net: ${formatMoneyColored(net)} ${dim(`(${savingsRate}% savings rate)`)}`);
482
+ }
483
+ // ── Biggest movers ──
484
+ const movers = cmp.categories.filter(c => Math.abs(c.diff) >= 10).slice(0, 3);
485
+ if (movers.length > 0) {
486
+ console.log(`\n ${heading("Biggest Movers")}`);
487
+ for (const mv of movers) {
488
+ const arrow = mv.diff > 0 ? chalk.red("↑") : chalk.green("↓");
489
+ const diffStr = mv.diff > 0 ? chalk.red("+" + rawFormatMoney(mv.diff)) : chalk.green("-" + rawFormatMoney(Math.abs(mv.diff)));
490
+ console.log(` ${arrow} ${categoryLabel(mv.category).padEnd(18)} ${rawFormatMoney(mv.period2).padStart(10)} ${diffStr}`);
491
+ }
492
+ }
493
+ // ── Top categories ──
494
+ const topCats = db.prepare(`SELECT category, SUM(amount) as total FROM transactions
495
+ WHERE amount > 0 AND date BETWEEN ? AND ? AND pending = 0
496
+ AND category NOT IN ('TRANSFER_OUT', 'TRANSFER_IN', 'LOAN_PAYMENTS')
497
+ GROUP BY category ORDER BY total DESC LIMIT 5`).all(start, end);
498
+ if (topCats.length > 0) {
499
+ console.log(`\n ${heading("Top Categories")}`);
500
+ for (const c of topCats) {
501
+ const pct = Math.round((c.total / totalSpent) * 100);
502
+ console.log(` ${categoryLabel(c.category).padEnd(18)} ${rawFormatMoney(c.total).padStart(10)} ${dim(`${pct}%`)}`);
503
+ }
504
+ }
505
+ // ── Net worth change over the period ──
506
+ const nwTrend = getNetWorthTrend(db, 60);
507
+ const nwAtStart = nwTrend.find(d => d.date >= start);
508
+ const nwAtEnd = [...nwTrend].reverse().find(d => d.date <= end);
509
+ if (nwAtStart && nwAtEnd) {
510
+ const nwChange = nwAtEnd.net_worth - nwAtStart.net_worth;
511
+ const arrow = nwChange >= 0 ? chalk.green("↑") : chalk.red("↓");
512
+ console.log(`\n ${heading("Net Worth")}`);
513
+ console.log(` ${rawFormatMoney(nwAtStart.net_worth)} → ${chalk.bold(rawFormatMoney(nwAtEnd.net_worth))} ${arrow} ${formatMoneyColored(nwChange)}`);
514
+ }
515
+ // ── Goals progress ──
516
+ const goals = getGoals(db);
517
+ const activeGoals = goals.filter(g => g.progress_pct < 100);
518
+ if (activeGoals.length > 0) {
519
+ console.log(`\n ${heading("Goals")}`);
520
+ for (const g of activeGoals) {
521
+ console.log(` ${g.name.padEnd(20)} ${progressBar(g.progress_pct, 12)} ${dim(rawFormatMoney(g.current) + " / " + rawFormatMoney(g.target))}`);
522
+ }
523
+ }
524
+ console.log();
525
+ }
@@ -45,12 +45,13 @@ const COMMANDS = [
45
45
  { name: "goals", desc: "Show financial goals" },
46
46
  { name: "score", desc: "Show daily financial score and streaks" },
47
47
  { name: "alerts", desc: "Show financial alerts" },
48
+ { name: "bills", desc: "Show upcoming bills" },
49
+ { name: "recap", desc: "Monthly spending recap" },
48
50
  { name: "export", desc: "Export data to a backup file" },
49
51
  { name: "import", desc: "Restore data from a backup file" },
50
52
  { name: "billing", desc: "Manage your Ray subscription" },
51
53
  { name: "update", desc: "Update Ray to the latest version" },
52
54
  { name: "doctor", desc: "Check system health" },
53
- { name: "completions", desc: "Install shell completions" },
54
55
  ];
55
56
  const SPENDING_PERIODS = ["this_month", "last_month", "last_30", "last_90"];
56
57
  function generateZsh() {
@@ -137,7 +137,7 @@ export async function runDoctor() {
137
137
  checks.push({ label: "Shell completions", status: "ok", detail: completionPath });
138
138
  }
139
139
  else {
140
- checks.push({ label: "Shell completions", status: "warn", detail: `Not installed. Run ${chalk.bold("ray completions")}` });
140
+ checks.push({ label: "Shell completions", status: "warn", detail: "Not installed" });
141
141
  }
142
142
  // ── Node version ──
143
143
  const nodeVersion = process.version;
package/dist/cli/index.js CHANGED
@@ -144,6 +144,24 @@ program
144
144
  const { showAlerts } = await import("./commands.js");
145
145
  showAlerts();
146
146
  });
147
+ program
148
+ .command("bills")
149
+ .description("Show upcoming bills")
150
+ .option("-d, --days <number>", "Number of days ahead", "7")
151
+ .action(async (opts) => {
152
+ ensureConfigured();
153
+ const { showBills } = await import("./commands.js");
154
+ showBills(Number(opts.days));
155
+ });
156
+ program
157
+ .command("recap")
158
+ .description("Monthly spending recap")
159
+ .argument("[period]", "Period: this_month, last_month", "last_month")
160
+ .action(async (period) => {
161
+ ensureConfigured();
162
+ const { showRecap } = await import("./commands.js");
163
+ showRecap(period);
164
+ });
147
165
  program
148
166
  .command("export")
149
167
  .description("Export user data (goals, budgets, memories, context) to a backup file")
@@ -168,7 +186,7 @@ program
168
186
  .action(async () => {
169
187
  ensureConfigured();
170
188
  if (!useManaged()) {
171
- console.log("You're using self-hosted keys. No subscription to manage.");
189
+ console.log("You're using your own keys. No subscription to manage.");
172
190
  return;
173
191
  }
174
192
  const open = (await import("open")).default;
@@ -217,13 +235,6 @@ program
217
235
  const { seedDemoDb } = await import("../demo/seed.js");
218
236
  seedDemoDb(demoPath);
219
237
  });
220
- program
221
- .command("completions")
222
- .description("Install shell completions")
223
- .action(async () => {
224
- const { installCompletions } = await import("./completions.js");
225
- installCompletions();
226
- });
227
238
  function ensureConfigured() {
228
239
  if (isDemoMode)
229
240
  return;
@@ -248,13 +259,14 @@ program.configureHelp({
248
259
  { name: "goals", desc: "Show financial goals" },
249
260
  { name: "score", desc: "Show daily financial score and streaks" },
250
261
  { name: "alerts", desc: "Show financial alerts" },
262
+ { name: "bills", desc: "Show upcoming bills" },
263
+ { name: "recap", desc: "Monthly spending recap" },
251
264
  { name: "export", desc: "Export data to a backup file" },
252
265
  { name: "import", desc: "Restore data from a backup file" },
253
266
  { name: "billing", desc: "Manage your Ray subscription" },
254
267
  { name: "update", desc: "Update Ray to the latest version" },
255
268
  { name: "doctor", desc: "Check system health" },
256
269
  { name: "demo", desc: "Seed a demo database with fake data" },
257
- { name: "completions", desc: "Install shell completions" },
258
270
  ]),
259
271
  });
260
272
  import("./updater.js").then(m => m.checkForUpdate(version)).catch(() => { });
@@ -145,7 +145,7 @@ export async function runDailySync(db) {
145
145
  ? db.prepare(`UPDATE transactions SET category = ?, subcategory = ? WHERE ${rule.match_field} LIKE ? AND category != ?`).run(rule.target_category, rule.target_subcategory, rule.match_pattern, rule.target_category)
146
146
  : db.prepare(`UPDATE transactions SET category = ? WHERE ${rule.match_field} LIKE ? AND category != ?`).run(rule.target_category, rule.match_pattern, rule.target_category);
147
147
  if (result.changes > 0) {
148
- console.log(` Recategorized ${result.changes} txn(s): ${rule.label}`);
148
+ console.log(` Recategorized ${result.changes} txn(s): ${rule.label || rule.match_pattern}`);
149
149
  totalRecat += result.changes;
150
150
  }
151
151
  }
@@ -406,6 +406,15 @@ export function categoryLabel(cat) {
406
406
  GOVERNMENT_AND_NON_PROFIT: "Gov/Nonprofit",
407
407
  MEDICAL: "Medical",
408
408
  BANK_FEES: "Bank Fees",
409
+ EDUCATION: "Education",
410
+ INSURANCE: "Insurance",
411
+ BUSINESS: "Business",
412
+ INCOME: "Income",
413
+ TRANSFER_IN: "Transfer In",
414
+ TRANSFER_OUT: "Transfer Out",
415
+ HOME_IMPROVEMENT: "Home Improvement",
416
+ TRAVEL: "Travel",
417
+ OTHER: "Other",
409
418
  };
410
- return labels[cat] || cat;
419
+ return labels[cat] || cat.split("_").map(w => w.charAt(0) + w.slice(1).toLowerCase()).join(" ");
411
420
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ray-finance",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
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",