ray-finance 0.2.2 → 0.2.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.
Files changed (82) hide show
  1. package/README.md +38 -11
  2. package/dist/ai/agent.js +16 -3
  3. package/dist/ai/context.js +6 -2
  4. package/dist/ai/insights.js +26 -3
  5. package/dist/ai/redactor.js +11 -0
  6. package/dist/ai/system-prompt.js +2 -2
  7. package/dist/ai/tools.js +4 -0
  8. package/dist/cli/backup.js +18 -9
  9. package/dist/cli/chat.js +146 -40
  10. package/dist/cli/format.d.ts +2 -0
  11. package/dist/cli/format.js +25 -0
  12. package/dist/cli/index.js +12 -2
  13. package/dist/cli/setup.js +7 -1
  14. package/dist/daily-sync.js +19 -4
  15. package/dist/db/connection.js +9 -1
  16. package/dist/db/encryption.js +18 -7
  17. package/dist/db/schema.js +6 -1
  18. package/dist/public/link.html +47 -24
  19. package/dist/public/ray-logo-dark.png +0 -0
  20. package/dist/queries/index.js +8 -8
  21. package/dist/server.js +33 -1
  22. package/package.json +7 -2
  23. package/.claude/settings.local.json +0 -16
  24. package/.env.example +0 -13
  25. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -19
  26. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -9
  27. package/.github/PULL_REQUEST_TEMPLATE.md +0 -5
  28. package/.github/workflows/ci.yml +0 -21
  29. package/CHANGELOG.md +0 -16
  30. package/CODE_OF_CONDUCT.md +0 -31
  31. package/CONTRIBUTING.md +0 -41
  32. package/Dockerfile +0 -8
  33. package/SECURITY.md +0 -36
  34. package/SPEC.md +0 -374
  35. package/docker-compose.yml +0 -9
  36. package/site/next-env.d.ts +0 -6
  37. package/site/next.config.ts +0 -7
  38. package/site/package-lock.json +0 -1661
  39. package/site/package.json +0 -24
  40. package/site/postcss.config.mjs +0 -7
  41. package/site/public/ray-og.jpg +0 -0
  42. package/site/public/robots.txt +0 -4
  43. package/site/public/sitemap.xml +0 -8
  44. package/site/src/app/copy-command.tsx +0 -31
  45. package/site/src/app/globals.css +0 -87
  46. package/site/src/app/layout.tsx +0 -64
  47. package/site/src/app/page.tsx +0 -841
  48. package/site/src/app/pii-scramble.tsx +0 -190
  49. package/site/src/app/reveal.tsx +0 -29
  50. package/site/tsconfig.json +0 -21
  51. package/src/ai/agent.ts +0 -106
  52. package/src/ai/audit.ts +0 -11
  53. package/src/ai/context.ts +0 -93
  54. package/src/ai/insights.ts +0 -474
  55. package/src/ai/memory.ts +0 -21
  56. package/src/ai/redactor.ts +0 -102
  57. package/src/ai/system-prompt.ts +0 -90
  58. package/src/ai/tools.ts +0 -716
  59. package/src/alerts/index.ts +0 -123
  60. package/src/cli/backup.ts +0 -113
  61. package/src/cli/chat.ts +0 -105
  62. package/src/cli/commands.ts +0 -240
  63. package/src/cli/format.ts +0 -149
  64. package/src/cli/index.ts +0 -193
  65. package/src/cli/scheduler.ts +0 -116
  66. package/src/cli/setup.ts +0 -189
  67. package/src/config.ts +0 -81
  68. package/src/daily-sync.ts +0 -155
  69. package/src/db/connection.ts +0 -38
  70. package/src/db/encryption.ts +0 -29
  71. package/src/db/helpers.ts +0 -47
  72. package/src/db/schema.ts +0 -196
  73. package/src/index.ts +0 -3
  74. package/src/plaid/client.ts +0 -25
  75. package/src/plaid/link.ts +0 -25
  76. package/src/plaid/sync.ts +0 -219
  77. package/src/public/link.html +0 -161
  78. package/src/queries/index.ts +0 -586
  79. package/src/scoring/index.ts +0 -468
  80. package/src/server.ts +0 -162
  81. package/tsconfig.json +0 -16
  82. /package/{site → dist}/public/favicon.png +0 -0
@@ -1,5 +1,7 @@
1
1
  import { syncTransactions, syncBalances, syncInvestments, syncLiabilities, } from "./plaid/sync.js";
2
2
  import { calculateDailyScore, checkAchievements } from "./scoring/index.js";
3
+ import { decryptPlaidToken } from "./db/encryption.js";
4
+ import { config } from "./config.js";
3
5
  /** Run the daily sync for a single database */
4
6
  export async function runDailySync(db) {
5
7
  const institutions = db
@@ -14,25 +16,38 @@ export async function runDailySync(db) {
14
16
  console.log(`Skipping ${inst.name} (manual entry)`);
15
17
  continue;
16
18
  }
19
+ // Decrypt the stored access token
20
+ let accessToken;
21
+ try {
22
+ if (!config.plaidTokenSecret) {
23
+ console.error(` Skipping ${inst.name}: no plaidTokenSecret configured`);
24
+ continue;
25
+ }
26
+ accessToken = decryptPlaidToken(inst.access_token, config.plaidTokenSecret);
27
+ }
28
+ catch {
29
+ console.error(` Skipping ${inst.name}: failed to decrypt access token (wrong key or corrupt data)`);
30
+ continue;
31
+ }
17
32
  const products = JSON.parse(inst.products);
18
33
  console.log(`Syncing: ${inst.name} (${products.join(", ")})`);
19
34
  try {
20
35
  // Always sync balances
21
- const accountCount = await syncBalances(db, inst.access_token);
36
+ const accountCount = await syncBalances(db, accessToken);
22
37
  console.log(` Accounts: ${accountCount}`);
23
38
  // Sync transactions if available
24
39
  if (products.includes("transactions")) {
25
- const txResult = await syncTransactions(db, inst.item_id, inst.access_token, inst.cursor);
40
+ const txResult = await syncTransactions(db, inst.item_id, accessToken, inst.cursor);
26
41
  console.log(` Transactions: +${txResult.added} ~${txResult.modified} -${txResult.removed}`);
27
42
  }
28
43
  // Sync investments if available
29
44
  if (products.includes("investments")) {
30
- const invResult = await syncInvestments(db, inst.access_token);
45
+ const invResult = await syncInvestments(db, accessToken);
31
46
  console.log(` Investments: ${invResult.holdings} holdings, ${invResult.securities} securities`);
32
47
  }
33
48
  // Sync liabilities if available
34
49
  if (products.includes("liabilities")) {
35
- await syncLiabilities(db, inst.access_token);
50
+ await syncLiabilities(db, accessToken);
36
51
  console.log(` Liabilities: synced`);
37
52
  }
38
53
  }
@@ -14,7 +14,15 @@ function openDb(dbPath, encryptionKey) {
14
14
  const hexKey = Buffer.from(encryptionKey, "utf8").toString("hex");
15
15
  db.pragma(`key="x'${hexKey}'"`);
16
16
  }
17
- db.pragma("journal_mode = WAL");
17
+ // Verify the key works before proceeding
18
+ try {
19
+ db.pragma("journal_mode = WAL");
20
+ }
21
+ catch (err) {
22
+ db.close();
23
+ throw new Error("Failed to open database. Wrong encryption key or corrupt database file. " +
24
+ "If you changed your encryption key, restore from backup or delete ~/.ray/data/finance.db to start fresh.");
25
+ }
18
26
  db.pragma("foreign_keys = ON");
19
27
  migrate(db);
20
28
  try {
@@ -1,23 +1,34 @@
1
1
  import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
2
- const SCRYPT_SALT = "ray-finance-plaid-token"; // Static salt is fine — secret is already high-entropy
3
2
  const SCRYPT_KEYLEN = 32;
3
+ const SALT_LEN = 16;
4
4
  export function generateKey() {
5
5
  return randomBytes(32).toString("hex");
6
6
  }
7
- function deriveKey(secret) {
8
- return scryptSync(secret, SCRYPT_SALT, SCRYPT_KEYLEN);
7
+ function deriveKey(secret, salt) {
8
+ return scryptSync(secret, salt, SCRYPT_KEYLEN);
9
9
  }
10
10
  export function encryptPlaidToken(token, secret) {
11
- const key = deriveKey(secret);
11
+ const salt = randomBytes(SALT_LEN);
12
+ const key = deriveKey(secret, salt);
12
13
  const iv = randomBytes(16);
13
14
  const cipher = createCipheriv("aes-256-gcm", key, iv);
14
15
  const encrypted = Buffer.concat([cipher.update(token, "utf8"), cipher.final()]);
15
16
  const authTag = cipher.getAuthTag();
16
- return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
17
+ return `${salt.toString("hex")}:${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
17
18
  }
18
19
  export function decryptPlaidToken(encrypted, secret) {
19
- const [ivHex, authTagHex, dataHex] = encrypted.split(":");
20
- const key = deriveKey(secret);
20
+ const parts = encrypted.split(":");
21
+ // Support legacy 3-part format (static salt) and new 4-part format (random salt)
22
+ let salt, ivHex, authTagHex, dataHex;
23
+ if (parts.length === 3) {
24
+ salt = Buffer.from("ray-finance-plaid-token", "utf8");
25
+ [ivHex, authTagHex, dataHex] = parts;
26
+ }
27
+ else {
28
+ [, ivHex, authTagHex, dataHex] = parts;
29
+ salt = Buffer.from(parts[0], "hex");
30
+ }
31
+ const key = deriveKey(secret, salt);
21
32
  const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivHex, "hex"));
22
33
  decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
23
34
  return decipher.update(Buffer.from(dataHex, "hex")) + decipher.final("utf8");
package/dist/db/schema.js CHANGED
@@ -100,7 +100,7 @@ export function migrate(db) {
100
100
  name TEXT NOT NULL,
101
101
  target_amount REAL NOT NULL,
102
102
  current_amount REAL DEFAULT 0,
103
- deadline TEXT,
103
+ target_date TEXT,
104
104
  status TEXT DEFAULT 'active'
105
105
  );
106
106
 
@@ -191,4 +191,9 @@ export function migrate(db) {
191
191
  created_at TEXT DEFAULT (datetime('now'))
192
192
  );
193
193
  `);
194
+ // Migrate: rename goals.deadline -> target_date for existing databases
195
+ const goalCols = db.prepare(`PRAGMA table_info(goals)`).all();
196
+ if (goalCols.some(c => c.name === "deadline") && !goalCols.some(c => c.name === "target_date")) {
197
+ db.exec(`ALTER TABLE goals RENAME COLUMN deadline TO target_date`);
198
+ }
194
199
  }
@@ -2,18 +2,15 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
- <link rel="preconnect" href="https://fonts.googleapis.com">
6
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
7
- <link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@700&display=swap" rel="stylesheet">
8
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
9
- <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><circle cx='50' cy='50' r='40' fill='%231a1a1a'/></svg>">
6
+ <link rel="icon" href="/favicon.png">
10
7
  <title>Ray — Connect Account</title>
11
8
  <style>
12
- * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ * { margin: 0; padding: 0; box-sizing: border-box; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
13
10
  body {
14
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
15
- background: #fafafa;
16
- color: #1a1a1a;
11
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
12
+ background: #fafaf9;
13
+ color: #1c1917;
17
14
  display: flex;
18
15
  align-items: center;
19
16
  justify-content: center;
@@ -22,36 +19,37 @@
22
19
  }
23
20
  .card {
24
21
  background: #fff;
25
- border-radius: 12px;
26
- padding: 40px 32px;
22
+ border: 1px solid rgba(214, 211, 209, 0.6);
23
+ border-radius: 16px;
24
+ padding: 48px 40px;
27
25
  max-width: 400px;
28
26
  width: 100%;
29
27
  text-align: center;
30
- box-shadow: 0 1px 3px rgba(0,0,0,0.08);
31
28
  }
32
- h1 { font-size: 22px; font-weight: 600; margin-bottom: 8px; }
33
- p { font-size: 15px; color: #666; margin-bottom: 24px; line-height: 1.5; }
29
+ .logo { margin-bottom: 32px; }
30
+ .logo img { height: 24px; }
31
+ h1 { font-size: 20px; font-weight: 600; margin-bottom: 8px; color: #1c1917; }
32
+ p { font-size: 15px; color: #78716c; margin-bottom: 24px; line-height: 1.6; }
34
33
  button {
35
- background: #1a1a1a;
34
+ background: #1c1917;
36
35
  color: #fff;
37
36
  border: none;
38
37
  padding: 14px 32px;
39
38
  border-radius: 9999px;
40
- font-size: 16px;
39
+ font-size: 15px;
41
40
  font-weight: 500;
42
41
  cursor: pointer;
43
42
  width: 100%;
44
43
  transition: background 0.2s;
45
44
  }
46
- button:hover { background: #333; }
47
- button:disabled { background: #ccc; cursor: not-allowed; }
48
- .success { color: #28a745; font-weight: 600; }
45
+ button:hover { background: #292524; }
46
+ button:disabled { background: #d6d3d1; cursor: not-allowed; }
47
+ .success { color: #6ab318; font-weight: 600; }
49
48
  .error { color: #dc3545; font-size: 14px; margin-top: 16px; }
50
- .logo { font-size: 28px; font-weight: 700; margin-bottom: 24px; color: #1a1a1a; font-family: 'Geist Mono', monospace; }
51
49
  .checkmark {
52
50
  width: 48px;
53
51
  height: 48px;
54
- border: 2.5px solid #28a745;
52
+ border: 2.5px solid #6ab318;
55
53
  border-radius: 50%;
56
54
  display: flex;
57
55
  align-items: center;
@@ -61,7 +59,7 @@
61
59
  .checkmark svg {
62
60
  width: 24px;
63
61
  height: 24px;
64
- stroke: #28a745;
62
+ stroke: #6ab318;
65
63
  fill: none;
66
64
  stroke-width: 2.5;
67
65
  stroke-linecap: round;
@@ -74,12 +72,24 @@
74
72
  object-fit: contain;
75
73
  margin-bottom: 16px;
76
74
  }
75
+ .spinner {
76
+ display: inline-block;
77
+ width: 20px;
78
+ height: 20px;
79
+ border: 2px solid #d6d3d1;
80
+ border-top-color: #78716c;
81
+ border-radius: 50%;
82
+ animation: spin 0.6s linear infinite;
83
+ margin-bottom: 16px;
84
+ }
85
+ @keyframes spin { to { transform: rotate(360deg); } }
77
86
  </style>
78
87
  </head>
79
88
  <body>
80
89
  <div class="card">
81
- <div class="logo">RAY</div>
90
+ <div class="logo"><img src="/ray-logo-dark.png" alt="Ray"></div>
82
91
  <div id="connect-view">
92
+ <div class="spinner"></div>
83
93
  <p>Opening Plaid...</p>
84
94
  <div id="error" class="error" style="display:none"></div>
85
95
  </div>
@@ -97,13 +107,26 @@
97
107
  let linkHandler = null;
98
108
 
99
109
  async function initPlaid() {
110
+ // Wait for Plaid SDK to load
111
+ if (typeof Plaid === 'undefined') {
112
+ let attempts = 0;
113
+ await new Promise((resolve, reject) => {
114
+ const check = setInterval(() => {
115
+ if (typeof Plaid !== 'undefined') { clearInterval(check); resolve(); }
116
+ else if (++attempts > 50) { clearInterval(check); reject(new Error('Failed to load Plaid SDK. Check your ad blocker or network connection.')); }
117
+ }, 100);
118
+ });
119
+ }
100
120
  try {
101
121
  const res = await fetch('/api/link-token', {
102
122
  method: 'POST',
103
123
  headers: { 'Content-Type': 'application/json' },
104
124
  body: JSON.stringify({ session_id: sessionId }),
105
125
  });
106
- if (!res.ok) throw new Error('Session expired');
126
+ if (!res.ok) {
127
+ const data = await res.json().catch(() => ({}));
128
+ throw new Error(data.error || 'Failed to connect. Please run "ray link" again.');
129
+ }
107
130
  const { link_token } = await res.json();
108
131
 
109
132
  linkHandler = Plaid.create({
@@ -145,7 +168,7 @@
145
168
  // Auto-open Plaid Link
146
169
  linkHandler.open();
147
170
  } catch (e) {
148
- showError('This link has expired. Please run "ray link" again.');
171
+ showError(e.message || 'This link has expired. Please run "ray link" again.');
149
172
  }
150
173
  }
151
174
 
Binary file
@@ -263,13 +263,13 @@ export function forecastBalance(db, accountId, months = 6) {
263
263
  }
264
264
  // --- Portfolio ---
265
265
  export function getPortfolio(db) {
266
- const rows = db.prepare(`SELECT a.name as account, s.name as security, s.ticker_symbol as ticker,
267
- h.quantity, h.institution_value as value, h.cost_basis,
268
- (h.institution_value - COALESCE(h.cost_basis, h.institution_value)) as gain_loss
266
+ const rows = db.prepare(`SELECT a.name as account, s.name as security, s.ticker,
267
+ h.quantity, h.value, h.cost_basis,
268
+ (h.value - COALESCE(h.cost_basis, h.value)) as gain_loss
269
269
  FROM holdings h
270
270
  JOIN accounts a ON h.account_id = a.account_id
271
271
  LEFT JOIN securities s ON h.security_id = s.security_id
272
- ORDER BY h.institution_value DESC`).all();
272
+ ORDER BY h.value DESC`).all();
273
273
  const totalValue = rows.reduce((s, r) => s + (r.value || 0), 0);
274
274
  const totalCostBasis = rows.reduce((s, r) => s + (r.cost_basis || 0), 0);
275
275
  return {
@@ -289,12 +289,12 @@ export function getPortfolio(db) {
289
289
  }
290
290
  // --- Investment performance ---
291
291
  export function getInvestmentPerformance(db) {
292
- const rows = db.prepare(`SELECT s.name as security, s.ticker_symbol as ticker,
293
- h.institution_value as value, h.cost_basis
292
+ const rows = db.prepare(`SELECT s.name as security, s.ticker,
293
+ h.value, h.cost_basis
294
294
  FROM holdings h
295
295
  LEFT JOIN securities s ON h.security_id = s.security_id
296
- WHERE h.institution_value IS NOT NULL
297
- ORDER BY h.institution_value DESC`).all();
296
+ WHERE h.value IS NOT NULL
297
+ ORDER BY h.value DESC`).all();
298
298
  const totalValue = rows.reduce((s, r) => s + (r.value || 0), 0);
299
299
  const totalCost = rows.reduce((s, r) => s + (r.cost_basis || r.value || 0), 0);
300
300
  const totalReturn = totalValue - totalCost;
package/dist/server.js CHANGED
@@ -13,9 +13,41 @@ const __filename = fileURLToPath(import.meta.url);
13
13
  const __dirname = dirname(__filename);
14
14
  // Session map for Plaid Link — maps sessionId → true (single-user, no chatId needed)
15
15
  const linkSessions = new Map();
16
+ // Simple rate limiter: track request counts per IP
17
+ const rateLimits = new Map();
18
+ const RATE_LIMIT_WINDOW = 60_000; // 1 minute
19
+ const RATE_LIMIT_MAX = 10; // max requests per window
20
+ function isRateLimited(ip) {
21
+ const now = Date.now();
22
+ const entry = rateLimits.get(ip);
23
+ if (!entry || now > entry.resetAt) {
24
+ rateLimits.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW });
25
+ return false;
26
+ }
27
+ entry.count++;
28
+ return entry.count > RATE_LIMIT_MAX;
29
+ }
16
30
  export function startLinkServer() {
17
31
  const app = express();
18
32
  app.use(express.json());
33
+ // Only allow requests from localhost origin
34
+ app.use((req, res, next) => {
35
+ const origin = req.headers.origin || req.headers.referer || "";
36
+ const ip = req.ip || req.socket.remoteAddress || "";
37
+ if (req.path.startsWith("/api/")) {
38
+ // Check origin for API routes
39
+ if (origin && !origin.startsWith(`http://localhost:${config.port}`) && !origin.startsWith(`http://127.0.0.1:${config.port}`)) {
40
+ res.status(403).json({ error: "Forbidden" });
41
+ return;
42
+ }
43
+ // Rate limit API routes
44
+ if (isRateLimited(ip)) {
45
+ res.status(429).json({ error: "Too many requests" });
46
+ return;
47
+ }
48
+ }
49
+ next();
50
+ });
19
51
  app.use(express.static(resolve(__dirname, "public")));
20
52
  const sessionId = randomUUID();
21
53
  linkSessions.set(sessionId, true);
@@ -120,7 +152,7 @@ export function startLinkServer() {
120
152
  res.status(500).json({ error: "Failed to link account" });
121
153
  }
122
154
  });
123
- const server = app.listen(config.port);
155
+ const server = app.listen(config.port, "127.0.0.1");
124
156
  const url = `http://localhost:${config.port}/link/${sessionId}`;
125
157
  // Auto-expire after 30 minutes
126
158
  const timeout = setTimeout(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ray-finance",
3
- "version": "0.2.2",
3
+ "version": "0.2.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",
@@ -25,11 +25,15 @@
25
25
  "engines": {
26
26
  "node": ">=18"
27
27
  },
28
+ "files": [
29
+ "dist/"
30
+ ],
28
31
  "bin": {
29
32
  "ray": "./dist/cli/index.js"
30
33
  },
31
34
  "scripts": {
32
35
  "build": "tsc && rm -rf dist/public && cp -r src/public dist/public",
36
+ "test": "vitest run",
33
37
  "prepublishOnly": "npm run build",
34
38
  "sync": "tsx src/daily-sync.ts"
35
39
  },
@@ -50,6 +54,7 @@
50
54
  "@types/express": "^4.17.0",
51
55
  "@types/node": "^22.0.0",
52
56
  "tsx": "^4.7.0",
53
- "typescript": "^5.5.0"
57
+ "typescript": "^5.5.0",
58
+ "vitest": "^3.2.4"
54
59
  }
55
60
  }
@@ -1,16 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(npm install:*)",
5
- "Bash(gh api:*)",
6
- "mcp__paper__create_artboard",
7
- "mcp__paper__write_html",
8
- "mcp__paper__get_screenshot",
9
- "mcp__paper__delete_nodes",
10
- "Bash(npx next:*)",
11
- "Bash(npx tsc:*)",
12
- "Bash(git add:*)",
13
- "Bash(git commit:*)"
14
- ]
15
- }
16
- }
package/.env.example DELETED
@@ -1,13 +0,0 @@
1
- # Anthropic API key for AI chat — https://console.anthropic.com
2
- ANTHROPIC_API_KEY=
3
-
4
- # Plaid credentials for bank sync — https://dashboard.plaid.com
5
- PLAID_CLIENT_ID=
6
- PLAID_SECRET=
7
- PLAID_ENV=production
8
-
9
- # Database encryption key (any strong passphrase)
10
- DB_ENCRYPTION_KEY=
11
-
12
- # Separate key for encrypting stored Plaid access tokens (any strong passphrase)
13
- PLAID_TOKEN_SECRET=
@@ -1,19 +0,0 @@
1
- ---
2
- name: Bug Report
3
- about: Report a bug
4
- labels: bug
5
- ---
6
-
7
- **What happened?**
8
-
9
- **What did you expect?**
10
-
11
- **Steps to reproduce**
12
- 1.
13
- 2.
14
- 3.
15
-
16
- **Environment**
17
- - OS:
18
- - Node.js version:
19
- - Ray version (`ray --version`):
@@ -1,9 +0,0 @@
1
- ---
2
- name: Feature Request
3
- about: Suggest a feature
4
- labels: enhancement
5
- ---
6
-
7
- **What problem does this solve?**
8
-
9
- **Describe the solution you'd like**
@@ -1,5 +0,0 @@
1
- ## What
2
-
3
- ## Why
4
-
5
- ## Test plan
@@ -1,21 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- pull_request:
7
- branches: [main]
8
-
9
- jobs:
10
- build:
11
- runs-on: ubuntu-latest
12
- strategy:
13
- matrix:
14
- node-version: [18, 20, 22]
15
- steps:
16
- - uses: actions/checkout@v4
17
- - uses: actions/setup-node@v4
18
- with:
19
- node-version: ${{ matrix.node-version }}
20
- - run: npm install
21
- - run: npm run build
package/CHANGELOG.md DELETED
@@ -1,16 +0,0 @@
1
- # Changelog
2
-
3
- ## 0.2.0
4
-
5
- Initial open source release.
6
-
7
- - CLI interface with 13 commands (`ray`, `setup`, `sync`, `link`, `status`, `transactions`, `spending`, `budgets`, `goals`, `score`, `alerts`, `export`, `import`)
8
- - AI financial advisor powered by Claude with 13+ tools
9
- - Plaid integration for bank sync (checking, savings, credit cards, investments, loans)
10
- - Encrypted local SQLite database (AES-256)
11
- - Daily financial scoring (0-100) with streaks and 14 achievements
12
- - Budget tracking with overage alerts
13
- - Financial goal tracking
14
- - Transaction auto-recategorization via user-defined rules
15
- - Conversation memory and persistent financial context
16
- - Data export/import for backup and restore
@@ -1,31 +0,0 @@
1
- # Code of Conduct
2
-
3
- ## Our Pledge
4
-
5
- We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
6
-
7
- ## Our Standards
8
-
9
- Examples of behavior that contributes to a positive environment:
10
-
11
- - Being respectful of differing viewpoints and experiences
12
- - Giving and gracefully accepting constructive feedback
13
- - Focusing on what is best for the community
14
- - Showing empathy towards other community members
15
-
16
- Examples of unacceptable behavior:
17
-
18
- - Trolling, insulting/derogatory comments, and personal or political attacks
19
- - Public or private harassment
20
- - Publishing others' private information without explicit permission
21
- - Other conduct which could reasonably be considered inappropriate in a professional setting
22
-
23
- ## Enforcement
24
-
25
- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project maintainer at clark@rayfinance.app.
26
-
27
- All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances.
28
-
29
- ## Attribution
30
-
31
- This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1.
package/CONTRIBUTING.md DELETED
@@ -1,41 +0,0 @@
1
- # Contributing
2
-
3
- Thanks for your interest in contributing to Ray.
4
-
5
- ## Development Setup
6
-
7
- ```bash
8
- git clone https://github.com/cdinnison/ray-finance.git
9
- cd ray-finance
10
- npm install
11
- npm run build
12
- npm link
13
- ```
14
-
15
- You'll need Plaid and Anthropic API keys to test. Copy `.env.example` to `.env` and fill in your credentials.
16
-
17
- ## Making Changes
18
-
19
- 1. Fork the repo and create a branch from `main`
20
- 2. Make your changes
21
- 3. Run `npm run build` to verify the project compiles
22
- 4. Open a pull request
23
-
24
- ## Code Style
25
-
26
- - TypeScript with strict mode
27
- - ES modules (`"type": "module"`)
28
- - Prefer simple, direct code over abstractions
29
- - No unnecessary dependencies
30
-
31
- ## Reporting Bugs
32
-
33
- Open a GitHub issue with:
34
- - What you expected to happen
35
- - What actually happened
36
- - Steps to reproduce
37
- - Your Node.js version and OS
38
-
39
- ## Security Issues
40
-
41
- See [SECURITY.md](SECURITY.md) for reporting security vulnerabilities. Do not open public issues for security bugs.
package/Dockerfile DELETED
@@ -1,8 +0,0 @@
1
- FROM node:20-slim
2
- WORKDIR /app
3
- COPY package*.json ./
4
- RUN npm ci --production
5
- COPY dist/ ./dist/
6
- COPY src/public/ ./dist/public/
7
- EXPOSE 3000
8
- CMD ["node", "dist/index.js"]
package/SECURITY.md DELETED
@@ -1,36 +0,0 @@
1
- # Security
2
-
3
- ## Architecture
4
-
5
- Ray is local-first. All financial data is stored on your machine in an encrypted SQLite database. No data is sent to Ray servers because there are no Ray servers.
6
-
7
- ### Encryption
8
-
9
- - **Database encryption**: The SQLite database is encrypted at rest using AES-256 via [SQLCipher](https://www.zetetic.net/sqlcipher/) (better-sqlite3-multiple-ciphers). The encryption key is provided during setup and stored in your local config.
10
- - **Plaid token encryption**: Plaid access tokens are encrypted separately using AES-256-GCM with scrypt key derivation before being stored in the database.
11
- - **File permissions**: Config and database files are created with `0600` permissions (owner read/write only).
12
-
13
- ### Data Flow
14
-
15
- Ray makes outbound API calls to three services:
16
-
17
- | Service | Purpose | When |
18
- |---------|---------|------|
19
- | Plaid | Sync bank transactions and balances | `ray sync`, `ray link` |
20
- | Anthropic | AI-powered chat responses | `ray` (interactive chat) |
21
-
22
- No telemetry, analytics, or usage data is collected or transmitted.
23
-
24
- ### PII Handling
25
-
26
- When sending data to the Anthropic API for AI chat, Ray redacts personally identifiable information (account numbers, routing numbers) before transmission and restores it in the response for display.
27
-
28
- ## Reporting a Vulnerability
29
-
30
- If you discover a security vulnerability, please report it responsibly:
31
-
32
- 1. **Do not** open a public GitHub issue
33
- 2. Email **clark@rayfinance.app** with details
34
- 3. Include steps to reproduce if possible
35
-
36
- I will respond within 48 hours and work with you to address the issue before any public disclosure.