ray-finance 0.3.1 → 0.3.2

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.
@@ -11,4 +11,6 @@ export declare function showSpending(period?: string): Promise<void>;
11
11
  export declare function showBudgets(): void;
12
12
  export declare function showGoals(): void;
13
13
  export declare function showScore(): void;
14
+ export declare function runAdd(): Promise<void>;
15
+ export declare function runRemove(): Promise<void>;
14
16
  export declare function showAlerts(): void;
@@ -5,6 +5,7 @@ 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 { addManualAccount, getManualAccounts, removeManualAccount, scrapeRedfinEstimate } from "../property.js";
8
9
  import { heading, progressBar, formatMoney, formatMoneyColored, dim, formatDuration, formatError, renderLogo, institutionName } from "./format.js";
9
10
  export async function runSync() {
10
11
  const ora = (await import("ora")).default;
@@ -26,6 +27,7 @@ export async function runSync() {
26
27
  export async function runLink() {
27
28
  const open = (await import("open")).default;
28
29
  const ora = (await import("ora")).default;
30
+ const readline = await import("readline");
29
31
  const { url, waitForComplete, stop } = startLinkServer();
30
32
  console.log(`\n${heading("Link Account")}\n`);
31
33
  console.log(`Opening Plaid Link in your browser...\n`);
@@ -35,6 +37,37 @@ export async function runLink() {
35
37
  await waitForComplete();
36
38
  stop();
37
39
  spinner.succeed("Bank account linked successfully!");
40
+ // Check if a mortgage was linked and we don't already have a property account
41
+ const db = getDb();
42
+ const hasMortgage = db.prepare(`SELECT 1 FROM accounts WHERE type = 'loan' AND subtype = 'mortgage' LIMIT 1`).get();
43
+ const hasProperty = db.prepare(`SELECT 1 FROM accounts WHERE type = 'other' AND subtype = 'property' LIMIT 1`).get();
44
+ if (hasMortgage && !hasProperty) {
45
+ console.log(`\n${dim("Mortgage detected.")} Track your home value for accurate net worth.`);
46
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
47
+ const ask = (q) => new Promise(resolve => rl.question(q, resolve));
48
+ const listingUrl = (await ask(`${dim("Paste a Redfin URL (or press Enter to skip):")} `)).trim();
49
+ if (listingUrl) {
50
+ const name = (await ask(`${dim("Name (e.g. Primary Residence):")} `)).trim() || "Primary Residence";
51
+ rl.close();
52
+ const propSpinner = ora("Fetching home value...").start();
53
+ try {
54
+ const value = await scrapeRedfinEstimate(listingUrl);
55
+ if (value) {
56
+ addManualAccount(db, name, "asset", value, listingUrl);
57
+ propSpinner.succeed(`${name}: ${rawFormatMoney(value)} — updates automatically on sync.`);
58
+ }
59
+ else {
60
+ propSpinner.fail("Could not determine home value from that URL. Try 'ray add' later.");
61
+ }
62
+ }
63
+ catch {
64
+ propSpinner.fail("Failed to fetch home value. Try 'ray add' later.");
65
+ }
66
+ }
67
+ else {
68
+ rl.close();
69
+ }
70
+ }
38
71
  }
39
72
  export async function showAccounts() {
40
73
  const db = getDb();
@@ -239,6 +272,108 @@ export function showScore() {
239
272
  }
240
273
  console.log();
241
274
  }
275
+ export async function runAdd() {
276
+ const ora = (await import("ora")).default;
277
+ const inquirer = (await import("inquirer")).default;
278
+ const db = getDb();
279
+ const theme = {
280
+ prefix: { idle: " ", done: chalk.green(" ✓") },
281
+ style: { highlight: (text) => chalk.yellowBright(text) },
282
+ };
283
+ console.log(`\n${heading("Add Account")}`);
284
+ console.log(dim(" Track something not linked via Plaid — a home, car, crypto, loan, etc.\n"));
285
+ const { name } = await inquirer.prompt([{ theme,
286
+ type: "input",
287
+ name: "name",
288
+ message: "Name",
289
+ validate: (v) => v.trim() ? true : "Required",
290
+ }]);
291
+ const { type } = await inquirer.prompt([{ theme,
292
+ type: "list",
293
+ name: "type",
294
+ message: "Type",
295
+ choices: [
296
+ { name: "Asset — something you own (adds to net worth)", value: "asset" },
297
+ { name: "Liability — something you owe (subtracts from net worth)", value: "liability" },
298
+ ],
299
+ }]);
300
+ let finalBalance = 0;
301
+ let listingUrl;
302
+ // For assets: offer Redfin auto-tracking
303
+ if (type === "asset") {
304
+ const { redfin } = await inquirer.prompt([{ theme,
305
+ type: "input",
306
+ name: "redfin",
307
+ message: `Redfin URL ${dim("(optional — auto-tracks home value)")}`,
308
+ }]);
309
+ const url = redfin.trim();
310
+ if (url) {
311
+ try {
312
+ const parsed = new URL(url);
313
+ if (!parsed.hostname.includes("redfin")) {
314
+ console.log(chalk.yellow(" Only Redfin URLs are supported."));
315
+ }
316
+ else {
317
+ listingUrl = url;
318
+ const spinner = ora("Fetching Redfin Estimate...").start();
319
+ const scraped = await scrapeRedfinEstimate(url);
320
+ if (scraped) {
321
+ finalBalance = scraped;
322
+ spinner.succeed(`Redfin Estimate: ${chalk.bold(rawFormatMoney(scraped))} ${dim("— updates on each sync")}`);
323
+ }
324
+ else {
325
+ spinner.warn("Could not fetch estimate.");
326
+ listingUrl = undefined;
327
+ }
328
+ }
329
+ }
330
+ catch {
331
+ console.log(chalk.yellow(" Invalid URL."));
332
+ }
333
+ }
334
+ }
335
+ // Manual value if no Redfin
336
+ if (!listingUrl) {
337
+ const { balance } = await inquirer.prompt([{ theme,
338
+ type: "input",
339
+ name: "balance",
340
+ message: "Current value ($)",
341
+ validate: (v) => {
342
+ const n = parseFloat(v.replace(/[$,]/g, ""));
343
+ return isNaN(n) ? "Enter a number" : true;
344
+ },
345
+ }]);
346
+ finalBalance = parseFloat(balance.replace(/[$,]/g, ""));
347
+ }
348
+ addManualAccount(db, name.trim(), type, finalBalance, listingUrl);
349
+ const label = type === "asset" ? chalk.green("asset") : chalk.red("liability");
350
+ console.log(`\n ${chalk.green("+")} ${chalk.bold(name.trim())} ${rawFormatMoney(finalBalance)} ${label}\n`);
351
+ }
352
+ export async function runRemove() {
353
+ const readline = await import("readline");
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.");
358
+ return;
359
+ }
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}`);
366
+ }
367
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
368
+ const answer = (await new Promise(resolve => rl.question(`\n Remove which? (number, or Enter to cancel): `, resolve))).trim();
369
+ rl.close();
370
+ const idx = parseInt(answer, 10) - 1;
371
+ if (isNaN(idx) || idx < 0 || idx >= accounts.length)
372
+ return;
373
+ removeManualAccount(db, accounts[idx].account_id);
374
+ console.log(chalk.green(`\n Removed ${accounts[idx].name}.`));
375
+ console.log();
376
+ }
242
377
  export function showAlerts() {
243
378
  const db = getDb();
244
379
  const alerts = generateAlerts(db);
package/dist/cli/index.js CHANGED
@@ -60,6 +60,22 @@ program
60
60
  const { runLink } = await import("./commands.js");
61
61
  await runLink();
62
62
  });
63
+ program
64
+ .command("add")
65
+ .description("Add a manual account (home, car, crypto, etc.)")
66
+ .action(async () => {
67
+ ensureConfigured();
68
+ const { runAdd } = await import("./commands.js");
69
+ await runAdd();
70
+ });
71
+ program
72
+ .command("remove")
73
+ .description("Remove a manual account")
74
+ .action(async () => {
75
+ ensureConfigured();
76
+ const { runRemove } = await import("./commands.js");
77
+ await runRemove();
78
+ });
63
79
  program
64
80
  .command("accounts")
65
81
  .description("Show linked accounts and balances")
@@ -221,6 +237,8 @@ program.configureHelp({
221
237
  formatHelp: () => helpScreen([
222
238
  { name: "setup", desc: "Configure Ray (API keys, preferences)" },
223
239
  { name: "link", desc: "Link a new financial account via Plaid" },
240
+ { name: "add", desc: "Add a manual account (home, car, crypto, etc.)" },
241
+ { name: "remove", desc: "Remove a manual account" },
224
242
  { name: "sync", desc: "Sync transactions from linked banks" },
225
243
  { name: "accounts", desc: "Show linked accounts and balances" },
226
244
  { name: "status", desc: "Show financial overview" },
@@ -3,6 +3,7 @@ import { calculateDailyScore, checkAchievements } from "./scoring/index.js";
3
3
  import { decryptPlaidToken } from "./db/encryption.js";
4
4
  import { config } from "./config.js";
5
5
  import { institutionName } from "./cli/format.js";
6
+ import { refreshPropertyValues, hasListingUrls } from "./property.js";
6
7
  /** Run the daily sync for a single database */
7
8
  export async function runDailySync(db) {
8
9
  const institutions = db
@@ -98,6 +99,15 @@ export async function runDailySync(db) {
98
99
  console.error(` Error syncing ${inst.name}: ${err.message}`);
99
100
  }
100
101
  }
102
+ // Refresh property values from listing URLs if configured
103
+ if (hasListingUrls(db)) {
104
+ try {
105
+ await refreshPropertyValues(db);
106
+ }
107
+ catch {
108
+ // Non-fatal
109
+ }
110
+ }
101
111
  // Snapshot net worth
102
112
  const assets = db
103
113
  .prepare(`SELECT COALESCE(SUM(current_balance), 0) as total FROM accounts WHERE type IN ('depository', 'investment', 'other')`)
@@ -0,0 +1,23 @@
1
+ import type BetterSqlite3 from "libsql";
2
+ type Database = BetterSqlite3.Database;
3
+ /** Scrape the Redfin Estimate from a Redfin listing URL */
4
+ export declare function scrapeRedfinEstimate(url: string): Promise<number | null>;
5
+ /** Add a manual account */
6
+ export declare function addManualAccount(db: Database, name: string, type: "asset" | "liability", balance: number, listingUrl?: string): {
7
+ accountId: string;
8
+ };
9
+ /** Remove a manual account */
10
+ export declare function removeManualAccount(db: Database, id: string): void;
11
+ /** List all manual accounts */
12
+ export declare function getManualAccounts(db: Database): {
13
+ account_id: string;
14
+ name: string;
15
+ type: string;
16
+ current_balance: number;
17
+ listing_url: string | null;
18
+ }[];
19
+ /** Refresh all property values from stored listing URLs (called during daily sync) */
20
+ export declare function refreshPropertyValues(db: Database): Promise<void>;
21
+ /** Check if any listing URLs are configured */
22
+ export declare function hasListingUrls(db: Database): boolean;
23
+ export {};
@@ -0,0 +1,80 @@
1
+ import { createHash } from "crypto";
2
+ const MANUAL_ITEM_ID = "manual-assets";
3
+ const LISTING_URL_PREFIX = "listing_url:";
4
+ function accountId(name) {
5
+ const hash = createHash("sha256").update(name).digest("hex").slice(0, 8);
6
+ return `manual-${hash}`;
7
+ }
8
+ /** Scrape the Redfin Estimate from a Redfin listing URL */
9
+ export async function scrapeRedfinEstimate(url) {
10
+ const resp = await fetch(url, {
11
+ headers: {
12
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
13
+ "Accept": "text/html",
14
+ },
15
+ });
16
+ if (!resp.ok)
17
+ return null;
18
+ const html = await resp.text();
19
+ // Prefer Redfin Estimate (current market value) over sold/list price
20
+ const match = html.match(/EstimateValueHeader[^>]*>.*?class="price[^"]*">\$([\d,]+)/) ||
21
+ html.match(/statsValue price[^>]*>[^$]*\$([\d,]+)/) ||
22
+ html.match(/Redfin Estimate[^$]*\$([\d,]+)/) ||
23
+ html.match(/"estimatedValue":\s*([\d.]+)/);
24
+ if (!match)
25
+ return null;
26
+ const value = parseFloat(match[1].replace(/,/g, ""));
27
+ return isNaN(value) ? null : value;
28
+ }
29
+ /** Ensure the manual institution exists */
30
+ function ensureManualInstitution(db) {
31
+ db.prepare(`INSERT INTO institutions (item_id, access_token, name, products) VALUES (?, 'manual', 'Manual Accounts', '[]')
32
+ ON CONFLICT(item_id) DO NOTHING`).run(MANUAL_ITEM_ID);
33
+ }
34
+ /** Add a manual account */
35
+ export function addManualAccount(db, name, type, balance, listingUrl) {
36
+ ensureManualInstitution(db);
37
+ const id = accountId(name);
38
+ const dbType = type === "asset" ? "other" : "loan";
39
+ const subtype = listingUrl ? "property" : (type === "asset" ? "other" : "other");
40
+ db.prepare(`INSERT INTO accounts (account_id, item_id, name, type, subtype, current_balance, updated_at)
41
+ VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
42
+ ON CONFLICT(account_id) DO UPDATE SET current_balance = excluded.current_balance, updated_at = datetime('now')`).run(id, MANUAL_ITEM_ID, name, dbType, subtype, balance);
43
+ if (listingUrl) {
44
+ db.prepare(`INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(LISTING_URL_PREFIX + id, listingUrl);
45
+ }
46
+ return { accountId: id };
47
+ }
48
+ /** Remove a manual account */
49
+ export function removeManualAccount(db, id) {
50
+ db.prepare(`DELETE FROM accounts WHERE account_id = ?`).run(id);
51
+ db.prepare(`DELETE FROM settings WHERE key = ?`).run(LISTING_URL_PREFIX + id);
52
+ }
53
+ /** List all manual accounts */
54
+ export function getManualAccounts(db) {
55
+ const rows = db.prepare(`SELECT account_id, name, type, current_balance FROM accounts WHERE item_id = ?`).all(MANUAL_ITEM_ID);
56
+ return rows.map(r => {
57
+ const urlRow = db.prepare(`SELECT value FROM settings WHERE key = ?`).get(LISTING_URL_PREFIX + r.account_id);
58
+ return { ...r, listing_url: urlRow?.value ?? null };
59
+ });
60
+ }
61
+ /** Refresh all property values from stored listing URLs (called during daily sync) */
62
+ export async function refreshPropertyValues(db) {
63
+ const urls = db.prepare(`SELECT key, value FROM settings WHERE key LIKE ?`).all(LISTING_URL_PREFIX + "%");
64
+ for (const { key, value: url } of urls) {
65
+ const id = key.slice(LISTING_URL_PREFIX.length);
66
+ try {
67
+ const val = await scrapeRedfinEstimate(url);
68
+ if (val) {
69
+ db.prepare(`UPDATE accounts SET current_balance = ?, updated_at = datetime('now') WHERE account_id = ?`).run(val, id);
70
+ }
71
+ }
72
+ catch {
73
+ // Non-fatal
74
+ }
75
+ }
76
+ }
77
+ /** Check if any listing URLs are configured */
78
+ export function hasListingUrls(db) {
79
+ return !!(db.prepare(`SELECT 1 FROM settings WHERE key LIKE ? LIMIT 1`).get(LISTING_URL_PREFIX + "%"));
80
+ }
@@ -61,24 +61,16 @@
61
61
  button:disabled { opacity: 0.3; cursor: not-allowed; }
62
62
  .success { color: #34d399; font-weight: 500; }
63
63
  .error { color: #f87171; font-size: 13px; margin-top: 16px; }
64
- .checkmark {
65
- width: 40px;
66
- height: 40px;
67
- border: 2px solid #34d399;
68
- border-radius: 50%;
69
- display: flex;
70
- align-items: center;
71
- justify-content: center;
72
- margin: 0 auto 20px;
73
- }
74
- .checkmark svg {
75
- width: 20px;
76
- height: 20px;
64
+ .success-title-check {
65
+ width: 18px;
66
+ height: 18px;
77
67
  stroke: #34d399;
78
68
  fill: none;
79
69
  stroke-width: 2.5;
80
70
  stroke-linecap: round;
81
71
  stroke-linejoin: round;
72
+ vertical-align: -2px;
73
+ margin-right: 6px;
82
74
  }
83
75
  .institution-logo {
84
76
  width: 48px;
@@ -110,15 +102,14 @@
110
102
  <div id="error" class="error" style="display:none"></div>
111
103
  </div>
112
104
  <div id="success-view" style="display:none">
113
- <div class="checkmark"><svg viewBox="0 0 24 24"><polyline points="4 12 10 18 20 6"/></svg></div>
114
105
  <img id="institution-logo" class="institution-logo" style="display:none" alt="">
115
- <h1 class="success" id="success-title">Account Connected</h1>
106
+ <h1 class="success" id="success-title"><svg class="success-title-check" viewBox="0 0 24 24"><polyline points="4 12 10 18 20 6"/></svg>Account Connected</h1>
116
107
  <p>Syncing transactions. Data stays on your machine. You can close this page.</p>
117
108
  </div>
118
109
  </div>
119
110
  <div class="trust">
120
111
  <svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
121
- <span>End-to-end encrypted</span>
112
+ <span>Secured by Plaid</span>
122
113
  </div>
123
114
 
124
115
  <script src="https://cdn.plaid.com/link/v2/stable/link-initialize.js"></script>
@@ -152,7 +143,8 @@
152
143
  linkHandler = Plaid.create({
153
144
  token: link_token,
154
145
  onSuccess: async (publicToken, metadata) => {
155
- document.getElementById('connect-view').innerHTML = '<div class="spinner"></div><h1>Linking</h1><p>Exchanging credentials...</p>';
146
+ document.getElementById('connect-view').innerHTML = '<div class="spinner"></div><h1>Linking</h1><p>Syncing ' + (metadata.institution?.name || 'account') + '...</p>';
147
+ document.getElementById('connect-view').style.display = 'block';
156
148
  try {
157
149
  const resp = await fetch('/api/exchange', {
158
150
  method: 'POST',
@@ -170,7 +162,8 @@
170
162
  document.getElementById('success-view').style.display = 'block';
171
163
  const instName = result.institution_name || metadata.institution?.name;
172
164
  if (instName) {
173
- document.getElementById('success-title').textContent = instName + ' Connected';
165
+ const titleEl = document.getElementById('success-title');
166
+ titleEl.innerHTML = titleEl.querySelector('svg').outerHTML + instName + ' Connected';
174
167
  }
175
168
  if (result.institution_logo) {
176
169
  const logoEl = document.getElementById('institution-logo');
@@ -1,9 +1,9 @@
1
1
  export function getNetWorth(db) {
2
2
  const home = db
3
- .prepare(`SELECT current_balance as value FROM accounts WHERE type = 'other' AND subtype = 'property' LIMIT 1`)
3
+ .prepare(`SELECT COALESCE(SUM(current_balance), 0) as value FROM accounts WHERE type = 'other' AND subtype = 'property'`)
4
4
  .get();
5
5
  const mortgage = db
6
- .prepare(`SELECT current_balance as value FROM accounts WHERE type = 'loan' AND subtype = 'mortgage' LIMIT 1`)
6
+ .prepare(`SELECT COALESCE(SUM(current_balance), 0) as value FROM accounts WHERE type = 'loan' AND subtype = 'mortgage'`)
7
7
  .get();
8
8
  const investments = db
9
9
  .prepare(`SELECT COALESCE(SUM(current_balance), 0) as total FROM accounts WHERE type = 'investment'`)
@@ -25,8 +25,8 @@ export function getNetWorth(db) {
25
25
  const prev = db
26
26
  .prepare(`SELECT net_worth FROM net_worth_history WHERE date <= ? ORDER BY date DESC LIMIT 1`)
27
27
  .get(yesterday);
28
- const homeVal = home?.value || 0;
29
- const mortgageVal = mortgage?.value || 0;
28
+ const homeVal = home.value;
29
+ const mortgageVal = mortgage.value;
30
30
  return {
31
31
  net_worth: assets.total - liabilities.total,
32
32
  assets: assets.total,
package/dist/server.js CHANGED
@@ -97,6 +97,23 @@ export function startLinkServer() {
97
97
  // Fetch actual enabled products from Plaid
98
98
  const itemResp = await plaidClient.itemGet({ access_token: accessToken });
99
99
  const products = (itemResp.data.item.products || []);
100
+ // Remove duplicate institution if re-linking the same one (Plaid gives a new item_id each time)
101
+ const institutionId = req.body.institution_id;
102
+ if (institutionId) {
103
+ const existing = db.prepare(`SELECT item_id FROM institutions WHERE name = ? AND item_id != ?`).all(institution_name, itemId);
104
+ for (const old of existing) {
105
+ const oldAccounts = db.prepare(`SELECT account_id FROM accounts WHERE item_id = ?`).all(old.item_id);
106
+ for (const acct of oldAccounts) {
107
+ db.prepare(`DELETE FROM transactions WHERE account_id = ?`).run(acct.account_id);
108
+ db.prepare(`DELETE FROM holdings WHERE account_id = ?`).run(acct.account_id);
109
+ db.prepare(`DELETE FROM investment_transactions WHERE account_id = ?`).run(acct.account_id);
110
+ db.prepare(`DELETE FROM liabilities WHERE account_id = ?`).run(acct.account_id);
111
+ db.prepare(`DELETE FROM recurring WHERE account_id = ?`).run(acct.account_id);
112
+ }
113
+ db.prepare(`DELETE FROM accounts WHERE item_id = ?`).run(old.item_id);
114
+ db.prepare(`DELETE FROM institutions WHERE item_id = ?`).run(old.item_id);
115
+ }
116
+ }
100
117
  db.prepare(`INSERT INTO institutions (item_id, access_token, name, products)
101
118
  VALUES (?, ?, ?, ?)
102
119
  ON CONFLICT(item_id) DO UPDATE SET access_token = excluded.access_token, products = excluded.products`).run(itemId, encryptedToken, institution_name || "Account", JSON.stringify(products));
@@ -183,9 +200,16 @@ export function startLinkServer() {
183
200
  }
184
201
  catch { }
185
202
  }
203
+ // Check if this institution has a mortgage (prompt user for home value)
204
+ const hasMortgage = !!(db.prepare(`SELECT 1 FROM accounts WHERE item_id = ? AND type = 'loan' AND subtype = 'mortgage' LIMIT 1`).get(itemId));
205
+ const hasPropertyAccount = !!(db.prepare(`SELECT 1 FROM accounts WHERE account_id = 'manual-home' LIMIT 1`).get());
186
206
  // Clean up session
187
207
  linkSessions.delete(session_id);
188
- res.json({ success: true, institution_name: institution_name, institution_logo: institutionLogo });
208
+ res.json({
209
+ success: true,
210
+ institution_name: institution_name,
211
+ institution_logo: institutionLogo,
212
+ });
189
213
  // Signal completion
190
214
  resolveComplete();
191
215
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ray-finance",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
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",