ray-finance 0.2.5 → 0.3.1
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 +30 -29
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +34 -14
- package/dist/cli/format.d.ts +4 -0
- package/dist/cli/format.js +23 -0
- package/dist/cli/index.js +24 -1
- package/dist/daily-sync.js +38 -18
- package/dist/db/schema.js +53 -0
- package/dist/demo/data.d.ts +252 -0
- package/dist/demo/data.js +272 -0
- package/dist/demo/logos.d.ts +8 -0
- package/dist/demo/logos.js +58 -0
- package/dist/demo/seed.d.ts +1 -0
- package/dist/demo/seed.js +125 -0
- package/dist/plaid/sync.d.ts +6 -0
- package/dist/plaid/sync.js +171 -13
- package/dist/public/link.html +64 -43
- package/dist/server.js +42 -21
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src=".github/ray-logo.png" alt="Ray" width="
|
|
2
|
+
<img src=".github/ray-logo.png" alt="Ray" width="108" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
@@ -14,40 +14,17 @@
|
|
|
14
14
|
|
|
15
15
|
<br />
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
net worth $45,230 +$120
|
|
21
|
-
|
|
22
|
-
spending $2,340 this month · $340 less vs last month
|
|
23
|
-
Dining -$114 · Shopping -$142 · Groceries -$73
|
|
24
|
-
|
|
25
|
-
▓▓▓▓▓▓▓░ Dining 92%
|
|
26
|
-
|
|
27
|
-
▓▓▓▓░░░░ Emergency fund $18,200/$40,000
|
|
28
|
-
|
|
29
|
-
upcoming Netflix $16 in 3d · Comcast $142 in 6d
|
|
30
|
-
|
|
31
|
-
score 72/100 · 5d no dining · 3d on pace
|
|
32
|
-
|
|
33
|
-
──────────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
❯ if I quit my job to freelance, how long can I survive?
|
|
36
|
-
|
|
37
|
-
Based on your last 3 months: you burn $4,820/mo after
|
|
38
|
-
fixed costs. With $18,200 in savings, that's
|
|
39
|
-
3.8 months of runway at current spend.
|
|
40
|
-
|
|
41
|
-
Cut dining and shopping to last-month levels and
|
|
42
|
-
you stretch to 5.1 months. Land one $8k contract
|
|
43
|
-
in that window and you never dip below $10k.
|
|
44
|
-
```
|
|
17
|
+
<p align="center">
|
|
18
|
+
<img src=".github/ray-demo.png" alt="Ray demo" />
|
|
19
|
+
</p>
|
|
45
20
|
|
|
46
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.
|
|
47
22
|
|
|
48
23
|
## Features
|
|
49
24
|
|
|
50
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.
|
|
51
28
|
- **Bank sync via Plaid** — Connect checking, savings, credit cards, investments, and loans
|
|
52
29
|
- **Encrypted local database** — All data stays on your machine in an AES-256 encrypted SQLite database
|
|
53
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.
|
|
@@ -65,6 +42,26 @@ Open Ray and it shows your net worth, spending vs last month, budget pacing, and
|
|
|
65
42
|
npm install -g ray-finance
|
|
66
43
|
```
|
|
67
44
|
|
|
45
|
+
## Try It
|
|
46
|
+
|
|
47
|
+
Explore Ray with realistic fake data — no bank accounts needed.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
ray demo # seed a demo database
|
|
51
|
+
ray --demo status # financial overview
|
|
52
|
+
ray --demo accounts # linked accounts with balances
|
|
53
|
+
ray --demo spending # spending breakdown by category
|
|
54
|
+
ray --demo budgets # budget tracking
|
|
55
|
+
ray --demo goals # financial goal progress
|
|
56
|
+
ray --demo score # daily score, streaks, achievements
|
|
57
|
+
ray --demo alerts # financial alerts
|
|
58
|
+
ray --demo transactions # recent transactions
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The dashboard commands work with no setup at all. To also try the AI chat with demo data, run `ray setup` first and add an [Anthropic API key](https://console.anthropic.com) or Ray API key — then `ray --demo` will start an interactive session where you can ask questions about the fake portfolio.
|
|
62
|
+
|
|
63
|
+
When you're ready to connect real accounts, run `ray link`.
|
|
64
|
+
|
|
68
65
|
## Quick Start
|
|
69
66
|
|
|
70
67
|
```bash
|
|
@@ -98,6 +95,8 @@ Run `ray --help` to see all available commands.
|
|
|
98
95
|
| Command | Description |
|
|
99
96
|
|---------|-------------|
|
|
100
97
|
| `ray` | Interactive AI chat with your financial advisor |
|
|
98
|
+
| `ray demo` | Seed a demo database with realistic fake data |
|
|
99
|
+
| `ray --demo <cmd>` | Run any command against demo data |
|
|
101
100
|
| `ray setup` | Configure API keys and preferences |
|
|
102
101
|
| `ray link` | Connect a new bank account |
|
|
103
102
|
| `ray sync` | Pull latest transactions and balances |
|
|
@@ -155,6 +154,7 @@ Ray stores everything in `~/.ray/`:
|
|
|
155
154
|
context.md # Persistent financial context for AI
|
|
156
155
|
data/
|
|
157
156
|
finance.db # Encrypted SQLite database
|
|
157
|
+
demo.db # Demo database (created by `ray demo`)
|
|
158
158
|
sync.log # Daily sync output
|
|
159
159
|
```
|
|
160
160
|
|
|
@@ -174,6 +174,7 @@ RAY_API_KEY= # Ray API key (managed mode, replaces the above)
|
|
|
174
174
|
|
|
175
175
|
## Roadmap
|
|
176
176
|
|
|
177
|
+
- [ ] Bring your own model — use any LLM provider (OpenAI, Ollama, open-source models, etc.)
|
|
177
178
|
- [ ] Daily digest email — morning summary of your finances
|
|
178
179
|
|
|
179
180
|
Have an idea? [Open a PR](https://github.com/cdinnison/ray-finance/pulls).
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare function runSync(): Promise<void>;
|
|
2
2
|
export declare function runLink(): Promise<void>;
|
|
3
|
-
export declare function showAccounts(): void
|
|
3
|
+
export declare function showAccounts(): Promise<void>;
|
|
4
4
|
export declare function showStatus(): void;
|
|
5
5
|
export declare function showTransactions(options?: {
|
|
6
6
|
limit?: number;
|
package/dist/cli/commands.js
CHANGED
|
@@ -5,7 +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 { heading, progressBar, formatMoney, formatMoneyColored, dim, formatDuration, formatError } from "./format.js";
|
|
8
|
+
import { heading, progressBar, formatMoney, formatMoneyColored, dim, formatDuration, formatError, renderLogo, institutionName } from "./format.js";
|
|
9
9
|
export async function runSync() {
|
|
10
10
|
const ora = (await import("ora")).default;
|
|
11
11
|
const spinner = ora("Syncing transactions...").start();
|
|
@@ -36,9 +36,9 @@ export async function runLink() {
|
|
|
36
36
|
stop();
|
|
37
37
|
spinner.succeed("Bank account linked successfully!");
|
|
38
38
|
}
|
|
39
|
-
export function showAccounts() {
|
|
39
|
+
export async function showAccounts() {
|
|
40
40
|
const db = getDb();
|
|
41
|
-
const institutions = db.prepare(`SELECT i.name as institution, i.item_id, i.created_at,
|
|
41
|
+
const institutions = db.prepare(`SELECT i.name as institution, i.item_id, i.created_at, i.logo, i.primary_color,
|
|
42
42
|
a.name, a.type, a.subtype, a.mask, a.current_balance, a.currency
|
|
43
43
|
FROM institutions i
|
|
44
44
|
LEFT JOIN accounts a ON a.item_id = i.item_id AND a.hidden = 0
|
|
@@ -48,20 +48,40 @@ export function showAccounts() {
|
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
50
|
console.log(`\n${heading("Linked Accounts")}\n`);
|
|
51
|
-
|
|
51
|
+
// Group rows by institution
|
|
52
|
+
const groups = new Map();
|
|
52
53
|
for (const row of institutions) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
const key = row.item_id;
|
|
55
|
+
if (!groups.has(key))
|
|
56
|
+
groups.set(key, []);
|
|
57
|
+
groups.get(key).push(row);
|
|
58
|
+
}
|
|
59
|
+
// Compute column widths across all accounts for alignment
|
|
60
|
+
const allAccounts = institutions.filter(r => r.name);
|
|
61
|
+
const maxName = Math.max(...allAccounts.map(r => `${r.name}${r.mask ? ` ••${r.mask}` : ""}`.length), 0);
|
|
62
|
+
const maxLabel = Math.max(...allAccounts.map(r => (r.subtype || r.type || "").length), 0);
|
|
63
|
+
for (const [, rows] of groups) {
|
|
64
|
+
const first = rows[0];
|
|
65
|
+
// Logo inline with institution name
|
|
66
|
+
let logoStr = "";
|
|
67
|
+
if (first.logo) {
|
|
68
|
+
const logo = await renderLogo(first.logo);
|
|
69
|
+
if (logo)
|
|
70
|
+
logoStr = logo.replace(/\n/g, "") + " ";
|
|
56
71
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
72
|
+
console.log(`${logoStr}${institutionName(first.institution, first.primary_color)}`);
|
|
73
|
+
for (const row of rows) {
|
|
74
|
+
if (!row.name) {
|
|
75
|
+
console.log(dim(" No accounts found"));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const nameWithMask = `${row.name}${row.mask ? ` ••${row.mask}` : ""}`;
|
|
79
|
+
const label = row.subtype || row.type || "";
|
|
80
|
+
const balance = row.current_balance != null ? rawFormatMoney(row.current_balance) : "—";
|
|
81
|
+
const namePad = nameWithMask.padEnd(maxName + 2);
|
|
82
|
+
const labelPad = label.padEnd(maxLabel + 2);
|
|
83
|
+
console.log(` ${namePad}${dim(labelPad)}${balance}`);
|
|
60
84
|
}
|
|
61
|
-
const mask = row.mask ? ` ••${row.mask}` : "";
|
|
62
|
-
const balance = row.current_balance != null ? rawFormatMoney(row.current_balance) : "—";
|
|
63
|
-
const label = row.subtype || row.type || "";
|
|
64
|
-
console.log(` ${row.name}${dim(mask)} ${dim(label)} ${balance}`);
|
|
65
85
|
}
|
|
66
86
|
console.log("");
|
|
67
87
|
}
|
package/dist/cli/format.d.ts
CHANGED
|
@@ -27,3 +27,7 @@ export declare function helpScreen(commands: {
|
|
|
27
27
|
/** Colorize AI response text for the terminal */
|
|
28
28
|
export declare function formatResponse(text: string): string;
|
|
29
29
|
export declare const DISCLAIMER: string;
|
|
30
|
+
/** Render a base64-encoded PNG as compact ANSI art (3 rows) */
|
|
31
|
+
export declare function renderLogo(base64: string): Promise<string>;
|
|
32
|
+
/** Color an institution name using its Plaid primary_color */
|
|
33
|
+
export declare function institutionName(name: string, primaryColor: string | null): string;
|
package/dist/cli/format.js
CHANGED
|
@@ -199,3 +199,26 @@ export function formatResponse(text) {
|
|
|
199
199
|
}
|
|
200
200
|
export const DISCLAIMER = "Ray is an AI tool, not a licensed financial advisor. Output is informational, " +
|
|
201
201
|
"may be inaccurate, and does not constitute financial advice.";
|
|
202
|
+
// ─── Institution Logo Rendering ─── //
|
|
203
|
+
/** Render a base64-encoded PNG as compact ANSI art (3 rows) */
|
|
204
|
+
export async function renderLogo(base64) {
|
|
205
|
+
try {
|
|
206
|
+
const terminalImage = (await import("terminal-image")).default;
|
|
207
|
+
const buffer = Buffer.from(base64, "base64");
|
|
208
|
+
const rendered = await terminalImage.buffer(buffer, { height: 1, preserveAspectRatio: true });
|
|
209
|
+
return rendered.trimEnd();
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return "";
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** Color an institution name using its Plaid primary_color */
|
|
216
|
+
export function institutionName(name, primaryColor) {
|
|
217
|
+
if (primaryColor) {
|
|
218
|
+
try {
|
|
219
|
+
return chalk.hex(primaryColor).bold(name);
|
|
220
|
+
}
|
|
221
|
+
catch { }
|
|
222
|
+
}
|
|
223
|
+
return chalk.bold(name);
|
|
224
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
// --demo flag: use dedicated demo database (must run before config import)
|
|
5
|
+
const isDemoMode = process.argv.includes("--demo");
|
|
6
|
+
if (isDemoMode) {
|
|
7
|
+
process.argv = process.argv.filter(a => a !== "--demo");
|
|
8
|
+
}
|
|
2
9
|
import { Command } from "commander";
|
|
3
10
|
import { createRequire } from "module";
|
|
4
11
|
import { config, isConfigured, useManaged, RAY_PROXY_BASE } from "../config.js";
|
|
5
12
|
import { helpScreen } from "./format.js";
|
|
13
|
+
// Override config for demo mode (demo DB is unencrypted)
|
|
14
|
+
if (isDemoMode) {
|
|
15
|
+
config.dbPath = resolve(homedir(), ".ray", "data", "demo.db");
|
|
16
|
+
config.dbEncryptionKey = "";
|
|
17
|
+
}
|
|
6
18
|
const require = createRequire(import.meta.url);
|
|
7
19
|
const { version } = require("../../package.json");
|
|
8
20
|
const program = new Command();
|
|
@@ -54,7 +66,7 @@ program
|
|
|
54
66
|
.action(async () => {
|
|
55
67
|
ensureConfigured();
|
|
56
68
|
const { showAccounts } = await import("./commands.js");
|
|
57
|
-
showAccounts();
|
|
69
|
+
await showAccounts();
|
|
58
70
|
});
|
|
59
71
|
program
|
|
60
72
|
.command("status")
|
|
@@ -181,6 +193,14 @@ program
|
|
|
181
193
|
const { runDoctor } = await import("./doctor.js");
|
|
182
194
|
await runDoctor();
|
|
183
195
|
});
|
|
196
|
+
program
|
|
197
|
+
.command("demo")
|
|
198
|
+
.description("Seed a demo database with realistic fake data")
|
|
199
|
+
.action(async () => {
|
|
200
|
+
const demoPath = resolve(homedir(), ".ray", "data", "demo.db");
|
|
201
|
+
const { seedDemoDb } = await import("../demo/seed.js");
|
|
202
|
+
seedDemoDb(demoPath);
|
|
203
|
+
});
|
|
184
204
|
program
|
|
185
205
|
.command("completions")
|
|
186
206
|
.description("Install shell completions")
|
|
@@ -189,6 +209,8 @@ program
|
|
|
189
209
|
installCompletions();
|
|
190
210
|
});
|
|
191
211
|
function ensureConfigured() {
|
|
212
|
+
if (isDemoMode)
|
|
213
|
+
return;
|
|
192
214
|
if (!isConfigured()) {
|
|
193
215
|
console.error("Ray is not configured. Run 'ray setup' first.");
|
|
194
216
|
process.exit(1);
|
|
@@ -213,6 +235,7 @@ program.configureHelp({
|
|
|
213
235
|
{ name: "billing", desc: "Manage your Ray subscription" },
|
|
214
236
|
{ name: "update", desc: "Update Ray to the latest version" },
|
|
215
237
|
{ name: "doctor", desc: "Check system health" },
|
|
238
|
+
{ name: "demo", desc: "Seed a demo database with fake data" },
|
|
216
239
|
{ name: "completions", desc: "Install shell completions" },
|
|
217
240
|
]),
|
|
218
241
|
});
|
package/dist/daily-sync.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { syncTransactions, syncBalances, syncInvestments, syncLiabilities, syncRecurring, isProductNotSupported, } from "./plaid/sync.js";
|
|
1
|
+
import { syncTransactions, syncBalances, syncInvestments, syncInvestmentTransactions, syncLiabilities, syncRecurring, isProductNotSupported, refreshProducts, } from "./plaid/sync.js";
|
|
2
2
|
import { calculateDailyScore, checkAchievements } from "./scoring/index.js";
|
|
3
3
|
import { decryptPlaidToken } from "./db/encryption.js";
|
|
4
4
|
import { config } from "./config.js";
|
|
5
|
+
import { institutionName } from "./cli/format.js";
|
|
5
6
|
/** Run the daily sync for a single database */
|
|
6
7
|
export async function runDailySync(db) {
|
|
7
8
|
const institutions = db
|
|
8
|
-
.prepare(`SELECT item_id, access_token, name, products, cursor FROM institutions`)
|
|
9
|
+
.prepare(`SELECT item_id, access_token, name, products, cursor, primary_color FROM institutions`)
|
|
9
10
|
.all();
|
|
10
11
|
if (institutions.length === 0) {
|
|
11
12
|
console.log("No linked institutions.");
|
|
@@ -31,8 +32,15 @@ export async function runDailySync(db) {
|
|
|
31
32
|
console.error(` Skipping ${inst.name}: failed to decrypt access token (wrong key or corrupt data)`);
|
|
32
33
|
continue;
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
let products = JSON.parse(inst.products);
|
|
36
|
+
// Refresh products list from Plaid if needed
|
|
37
|
+
try {
|
|
38
|
+
products = await refreshProducts(db, inst.item_id, accessToken);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Non-fatal — use stored products
|
|
42
|
+
}
|
|
43
|
+
console.log(`Syncing: ${institutionName(inst.name, inst.primary_color)} (${products.join(", ")})`);
|
|
36
44
|
try {
|
|
37
45
|
instSynced++;
|
|
38
46
|
// Always sync balances
|
|
@@ -45,22 +53,34 @@ export async function runDailySync(db) {
|
|
|
45
53
|
console.log(` Transactions: +${txResult.added} ~${txResult.modified} -${txResult.removed}`);
|
|
46
54
|
}
|
|
47
55
|
// Sync investments
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
if (products.includes("investments")) {
|
|
57
|
+
try {
|
|
58
|
+
const invResult = await syncInvestments(db, accessToken);
|
|
59
|
+
console.log(` Investments: ${invResult.holdings} holdings, ${invResult.securities} securities`);
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
if (!isProductNotSupported(e))
|
|
63
|
+
console.error(` Investments error: ${e.message}`);
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const invTxResult = await syncInvestmentTransactions(db, accessToken);
|
|
67
|
+
console.log(` Investment transactions: ${invTxResult.transactions}`);
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
if (!isProductNotSupported(e))
|
|
71
|
+
console.error(` Investment transactions error: ${e.message}`);
|
|
72
|
+
}
|
|
55
73
|
}
|
|
56
74
|
// Sync liabilities
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
75
|
+
if (products.includes("liabilities")) {
|
|
76
|
+
try {
|
|
77
|
+
await syncLiabilities(db, accessToken);
|
|
78
|
+
console.log(` Liabilities: synced`);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
if (!isProductNotSupported(e))
|
|
82
|
+
console.error(` Liabilities error: ${e.message}`);
|
|
83
|
+
}
|
|
64
84
|
}
|
|
65
85
|
// Sync recurring transaction streams
|
|
66
86
|
if (products.includes("transactions")) {
|
package/dist/db/schema.js
CHANGED
|
@@ -189,6 +189,22 @@ export function migrate(db) {
|
|
|
189
189
|
created_at TEXT DEFAULT (datetime('now'))
|
|
190
190
|
);
|
|
191
191
|
|
|
192
|
+
CREATE TABLE IF NOT EXISTS investment_transactions (
|
|
193
|
+
investment_transaction_id TEXT PRIMARY KEY,
|
|
194
|
+
account_id TEXT NOT NULL REFERENCES accounts(account_id),
|
|
195
|
+
security_id TEXT,
|
|
196
|
+
date TEXT NOT NULL,
|
|
197
|
+
name TEXT NOT NULL,
|
|
198
|
+
quantity REAL,
|
|
199
|
+
amount REAL NOT NULL,
|
|
200
|
+
price REAL,
|
|
201
|
+
fees REAL,
|
|
202
|
+
type TEXT,
|
|
203
|
+
subtype TEXT,
|
|
204
|
+
iso_currency_code TEXT,
|
|
205
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
206
|
+
);
|
|
207
|
+
|
|
192
208
|
CREATE TABLE IF NOT EXISTS ai_audit_log (
|
|
193
209
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
194
210
|
tool_name TEXT NOT NULL,
|
|
@@ -198,11 +214,48 @@ export function migrate(db) {
|
|
|
198
214
|
created_at TEXT DEFAULT (datetime('now'))
|
|
199
215
|
);
|
|
200
216
|
`);
|
|
217
|
+
// Migrate: add logo and primary_color to institutions
|
|
218
|
+
const instCols = db.prepare(`PRAGMA table_info(institutions)`).all();
|
|
219
|
+
if (!instCols.some(c => c.name === "logo")) {
|
|
220
|
+
db.exec(`ALTER TABLE institutions ADD COLUMN logo TEXT`);
|
|
221
|
+
db.exec(`ALTER TABLE institutions ADD COLUMN primary_color TEXT`);
|
|
222
|
+
}
|
|
201
223
|
// Migrate: rename goals.deadline -> target_date for existing databases
|
|
202
224
|
const goalCols = db.prepare(`PRAGMA table_info(goals)`).all();
|
|
203
225
|
if (goalCols.some(c => c.name === "deadline") && !goalCols.some(c => c.name === "target_date")) {
|
|
204
226
|
db.exec(`ALTER TABLE goals RENAME COLUMN deadline TO target_date`);
|
|
205
227
|
}
|
|
228
|
+
// Migrate: add balance_limit to accounts
|
|
229
|
+
const acctCols = db.prepare(`PRAGMA table_info(accounts)`).all();
|
|
230
|
+
if (!acctCols.some(c => c.name === "balance_limit")) {
|
|
231
|
+
db.exec(`ALTER TABLE accounts ADD COLUMN balance_limit REAL`);
|
|
232
|
+
}
|
|
233
|
+
// Migrate: add vesting columns to holdings
|
|
234
|
+
const holdCols = db.prepare(`PRAGMA table_info(holdings)`).all();
|
|
235
|
+
if (!holdCols.some(c => c.name === "vested_value")) {
|
|
236
|
+
db.exec(`ALTER TABLE holdings ADD COLUMN vested_value REAL`);
|
|
237
|
+
db.exec(`ALTER TABLE holdings ADD COLUMN vested_quantity REAL`);
|
|
238
|
+
}
|
|
239
|
+
// Migrate: expand liabilities with type-specific columns
|
|
240
|
+
const liabCols = db.prepare(`PRAGMA table_info(liabilities)`).all();
|
|
241
|
+
if (!liabCols.some(c => c.name === "last_payment_amount")) {
|
|
242
|
+
db.exec(`ALTER TABLE liabilities ADD COLUMN last_payment_amount REAL`);
|
|
243
|
+
db.exec(`ALTER TABLE liabilities ADD COLUMN last_payment_date TEXT`);
|
|
244
|
+
db.exec(`ALTER TABLE liabilities ADD COLUMN credit_limit REAL`);
|
|
245
|
+
db.exec(`ALTER TABLE liabilities ADD COLUMN last_statement_issue_date TEXT`);
|
|
246
|
+
db.exec(`ALTER TABLE liabilities ADD COLUMN is_overdue INTEGER`);
|
|
247
|
+
db.exec(`ALTER TABLE liabilities ADD COLUMN apr_type TEXT`);
|
|
248
|
+
db.exec(`ALTER TABLE liabilities ADD COLUMN maturity_date TEXT`);
|
|
249
|
+
db.exec(`ALTER TABLE liabilities ADD COLUMN loan_type TEXT`);
|
|
250
|
+
db.exec(`ALTER TABLE liabilities ADD COLUMN property_address TEXT`);
|
|
251
|
+
db.exec(`ALTER TABLE liabilities ADD COLUMN escrow_balance REAL`);
|
|
252
|
+
db.exec(`ALTER TABLE liabilities ADD COLUMN loan_status TEXT`);
|
|
253
|
+
db.exec(`ALTER TABLE liabilities ADD COLUMN loan_name TEXT`);
|
|
254
|
+
db.exec(`ALTER TABLE liabilities ADD COLUMN repayment_plan TEXT`);
|
|
255
|
+
db.exec(`ALTER TABLE liabilities ADD COLUMN expected_payoff_date TEXT`);
|
|
256
|
+
db.exec(`ALTER TABLE liabilities ADD COLUMN ytd_interest_paid REAL`);
|
|
257
|
+
db.exec(`ALTER TABLE liabilities ADD COLUMN ytd_principal_paid REAL`);
|
|
258
|
+
}
|
|
206
259
|
// Migrate: rebuild recurring table to use Plaid stream schema
|
|
207
260
|
const recCols = db.prepare(`PRAGMA table_info(recurring)`).all();
|
|
208
261
|
if (!recCols.some(c => c.name === "stream_id")) {
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
export declare const institutions: {
|
|
2
|
+
item_id: string;
|
|
3
|
+
access_token: string;
|
|
4
|
+
name: string;
|
|
5
|
+
products: string;
|
|
6
|
+
logo: string;
|
|
7
|
+
primary_color: string;
|
|
8
|
+
}[];
|
|
9
|
+
export declare const accounts: ({
|
|
10
|
+
account_id: string;
|
|
11
|
+
item_id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
official_name: string;
|
|
14
|
+
type: string;
|
|
15
|
+
subtype: string;
|
|
16
|
+
mask: string;
|
|
17
|
+
current_balance: number;
|
|
18
|
+
available_balance: number;
|
|
19
|
+
currency: string;
|
|
20
|
+
balance_limit: null;
|
|
21
|
+
} | {
|
|
22
|
+
account_id: string;
|
|
23
|
+
item_id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
official_name: string;
|
|
26
|
+
type: string;
|
|
27
|
+
subtype: string;
|
|
28
|
+
mask: string;
|
|
29
|
+
current_balance: number;
|
|
30
|
+
available_balance: null;
|
|
31
|
+
currency: string;
|
|
32
|
+
balance_limit: null;
|
|
33
|
+
} | {
|
|
34
|
+
account_id: string;
|
|
35
|
+
item_id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
official_name: string;
|
|
38
|
+
type: string;
|
|
39
|
+
subtype: string;
|
|
40
|
+
mask: string;
|
|
41
|
+
current_balance: number;
|
|
42
|
+
available_balance: number;
|
|
43
|
+
currency: string;
|
|
44
|
+
balance_limit: number;
|
|
45
|
+
} | {
|
|
46
|
+
account_id: string;
|
|
47
|
+
item_id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
official_name: null;
|
|
50
|
+
type: string;
|
|
51
|
+
subtype: string;
|
|
52
|
+
mask: null;
|
|
53
|
+
current_balance: number;
|
|
54
|
+
available_balance: null;
|
|
55
|
+
currency: string;
|
|
56
|
+
balance_limit: null;
|
|
57
|
+
})[];
|
|
58
|
+
export declare const transactions: ({
|
|
59
|
+
transaction_id: string;
|
|
60
|
+
account_id: string;
|
|
61
|
+
amount: number;
|
|
62
|
+
date: string;
|
|
63
|
+
name: string;
|
|
64
|
+
merchant_name: string;
|
|
65
|
+
category: string;
|
|
66
|
+
subcategory: string;
|
|
67
|
+
pending: number;
|
|
68
|
+
payment_channel: string;
|
|
69
|
+
} | {
|
|
70
|
+
transaction_id: string;
|
|
71
|
+
account_id: string;
|
|
72
|
+
amount: number;
|
|
73
|
+
date: string;
|
|
74
|
+
name: string;
|
|
75
|
+
merchant_name: null;
|
|
76
|
+
category: string;
|
|
77
|
+
subcategory: string;
|
|
78
|
+
pending: number;
|
|
79
|
+
payment_channel: string;
|
|
80
|
+
})[];
|
|
81
|
+
export declare const securities: {
|
|
82
|
+
security_id: string;
|
|
83
|
+
name: string;
|
|
84
|
+
ticker: string;
|
|
85
|
+
type: string;
|
|
86
|
+
close_price: number;
|
|
87
|
+
close_price_as_of: string;
|
|
88
|
+
}[];
|
|
89
|
+
export declare const holdings: ({
|
|
90
|
+
account_id: string;
|
|
91
|
+
security_id: string;
|
|
92
|
+
quantity: number;
|
|
93
|
+
value: number;
|
|
94
|
+
cost_basis: number;
|
|
95
|
+
price: number;
|
|
96
|
+
price_as_of: string;
|
|
97
|
+
vested_value: null;
|
|
98
|
+
vested_quantity: null;
|
|
99
|
+
} | {
|
|
100
|
+
account_id: string;
|
|
101
|
+
security_id: string;
|
|
102
|
+
quantity: number;
|
|
103
|
+
value: number;
|
|
104
|
+
cost_basis: number;
|
|
105
|
+
price: number;
|
|
106
|
+
price_as_of: string;
|
|
107
|
+
vested_value: number;
|
|
108
|
+
vested_quantity: number;
|
|
109
|
+
})[];
|
|
110
|
+
export declare const liabilities: ({
|
|
111
|
+
account_id: string;
|
|
112
|
+
type: string;
|
|
113
|
+
interest_rate: number;
|
|
114
|
+
origination_date: null;
|
|
115
|
+
original_balance: null;
|
|
116
|
+
current_balance: number;
|
|
117
|
+
minimum_payment: number;
|
|
118
|
+
next_payment_due: string;
|
|
119
|
+
last_payment_amount: number;
|
|
120
|
+
last_payment_date: string;
|
|
121
|
+
credit_limit: number;
|
|
122
|
+
last_statement_issue_date: string;
|
|
123
|
+
is_overdue: number;
|
|
124
|
+
apr_type: string;
|
|
125
|
+
maturity_date: null;
|
|
126
|
+
loan_type: null;
|
|
127
|
+
property_address: null;
|
|
128
|
+
escrow_balance: null;
|
|
129
|
+
loan_status: null;
|
|
130
|
+
loan_name: null;
|
|
131
|
+
repayment_plan: null;
|
|
132
|
+
expected_payoff_date: null;
|
|
133
|
+
ytd_interest_paid: null;
|
|
134
|
+
ytd_principal_paid: null;
|
|
135
|
+
} | {
|
|
136
|
+
account_id: string;
|
|
137
|
+
type: string;
|
|
138
|
+
interest_rate: number;
|
|
139
|
+
origination_date: string;
|
|
140
|
+
original_balance: number;
|
|
141
|
+
current_balance: number;
|
|
142
|
+
minimum_payment: number;
|
|
143
|
+
next_payment_due: string;
|
|
144
|
+
last_payment_amount: number;
|
|
145
|
+
last_payment_date: string;
|
|
146
|
+
credit_limit: null;
|
|
147
|
+
last_statement_issue_date: null;
|
|
148
|
+
is_overdue: number;
|
|
149
|
+
apr_type: string;
|
|
150
|
+
maturity_date: string;
|
|
151
|
+
loan_type: string;
|
|
152
|
+
property_address: string;
|
|
153
|
+
escrow_balance: number;
|
|
154
|
+
loan_status: string;
|
|
155
|
+
loan_name: string;
|
|
156
|
+
repayment_plan: null;
|
|
157
|
+
expected_payoff_date: string;
|
|
158
|
+
ytd_interest_paid: number;
|
|
159
|
+
ytd_principal_paid: number;
|
|
160
|
+
})[];
|
|
161
|
+
export declare const recurring: ({
|
|
162
|
+
stream_id: string;
|
|
163
|
+
account_id: string;
|
|
164
|
+
merchant_name: string;
|
|
165
|
+
description: string;
|
|
166
|
+
frequency: string;
|
|
167
|
+
category: string;
|
|
168
|
+
subcategory: string;
|
|
169
|
+
avg_amount: number;
|
|
170
|
+
last_amount: number;
|
|
171
|
+
first_date: string;
|
|
172
|
+
last_date: string;
|
|
173
|
+
is_active: number;
|
|
174
|
+
status: string;
|
|
175
|
+
stream_type: string;
|
|
176
|
+
} | {
|
|
177
|
+
stream_id: string;
|
|
178
|
+
account_id: string;
|
|
179
|
+
merchant_name: null;
|
|
180
|
+
description: string;
|
|
181
|
+
frequency: string;
|
|
182
|
+
category: string;
|
|
183
|
+
subcategory: string;
|
|
184
|
+
avg_amount: number;
|
|
185
|
+
last_amount: number;
|
|
186
|
+
first_date: string;
|
|
187
|
+
last_date: string;
|
|
188
|
+
is_active: number;
|
|
189
|
+
status: string;
|
|
190
|
+
stream_type: string;
|
|
191
|
+
})[];
|
|
192
|
+
export declare const budgets: {
|
|
193
|
+
category: string;
|
|
194
|
+
monthly_limit: number;
|
|
195
|
+
period: string;
|
|
196
|
+
}[];
|
|
197
|
+
export declare const goals: {
|
|
198
|
+
name: string;
|
|
199
|
+
target_amount: number;
|
|
200
|
+
current_amount: number;
|
|
201
|
+
target_date: string;
|
|
202
|
+
status: string;
|
|
203
|
+
}[];
|
|
204
|
+
export declare const dailyScores: {
|
|
205
|
+
date: string;
|
|
206
|
+
score: number;
|
|
207
|
+
restaurant_count: number;
|
|
208
|
+
shopping_count: number;
|
|
209
|
+
food_spend: number;
|
|
210
|
+
total_spend: number;
|
|
211
|
+
zero_spend: number;
|
|
212
|
+
no_restaurant_streak: number;
|
|
213
|
+
no_shopping_streak: number;
|
|
214
|
+
on_pace_streak: number;
|
|
215
|
+
}[];
|
|
216
|
+
export declare const achievements: {
|
|
217
|
+
key: string;
|
|
218
|
+
name: string;
|
|
219
|
+
description: string;
|
|
220
|
+
unlocked_at: string;
|
|
221
|
+
}[];
|
|
222
|
+
export declare const netWorthHistory: {
|
|
223
|
+
date: string;
|
|
224
|
+
total_assets: number;
|
|
225
|
+
total_liabilities: number;
|
|
226
|
+
net_worth: number;
|
|
227
|
+
}[];
|
|
228
|
+
export declare const investmentTransactions: {
|
|
229
|
+
investment_transaction_id: string;
|
|
230
|
+
account_id: string;
|
|
231
|
+
security_id: string;
|
|
232
|
+
date: string;
|
|
233
|
+
name: string;
|
|
234
|
+
quantity: number;
|
|
235
|
+
amount: number;
|
|
236
|
+
price: number;
|
|
237
|
+
fees: number;
|
|
238
|
+
type: string;
|
|
239
|
+
subtype: string;
|
|
240
|
+
iso_currency_code: string;
|
|
241
|
+
}[];
|
|
242
|
+
export declare const recurringBills: {
|
|
243
|
+
name: string;
|
|
244
|
+
amount: number;
|
|
245
|
+
day_of_month: number;
|
|
246
|
+
type: string;
|
|
247
|
+
account_id: string;
|
|
248
|
+
}[];
|
|
249
|
+
export declare const memories: {
|
|
250
|
+
content: string;
|
|
251
|
+
category: string;
|
|
252
|
+
}[];
|