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 +34 -16
- package/dist/ai/tools.js +1 -1
- package/dist/cli/commands.js +44 -12
- package/dist/cli/index.js +17 -6
- package/dist/cli/setup.js +1 -1
- package/dist/server.js +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
An open-source
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
29
|
-
- **
|
|
30
|
-
|
|
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
|
-
- **
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- **
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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": {
|
package/dist/cli/commands.js
CHANGED
|
@@ -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
|
|
356
|
-
|
|
357
|
-
|
|
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("
|
|
361
|
-
for (let i = 0; i <
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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 >=
|
|
387
|
+
if (isNaN(idx) || idx < 0 || idx >= entries.length)
|
|
372
388
|
return;
|
|
373
|
-
|
|
374
|
-
|
|
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
|
|
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.
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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
|