ray-finance 0.3.6 → 0.3.7

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 CHANGED
@@ -42,7 +42,7 @@ Tell Ray about your family, goals, and financial strategy once. From then on, ev
42
42
 
43
43
  ### Set it and forget it
44
44
 
45
- - **Bank sync via Plaid** — Connect checking, savings, credit cards, investments, and loans.
45
+ - **Bank sync via Plaid** — Connect checking, savings, credit cards, investments, and loans. Supports 18 countries: 🇺🇸 United States, 🇬🇧 United Kingdom, 🇨🇦 Canada, 🇫🇷 France, 🇩🇪 Germany, 🇪🇸 Spain, 🇮🇹 Italy, 🇳🇱 Netherlands, 🇮🇪 Ireland, 🇵🇱 Poland, 🇩🇰 Denmark, 🇳🇴 Norway, 🇸🇪 Sweden, 🇪🇪 Estonia, 🇱🇹 Lithuania, 🇱🇻 Latvia, 🇵🇹 Portugal, and 🇧🇪 Belgium.
46
46
  - **Scheduled daily sync** — Automatic bank sync via launchd (macOS) or cron (Linux).
47
47
  - **Auto-recategorization** — Define rules to automatically re-label transactions.
48
48
  - **Export/import** — Back up and restore your financial data.
@@ -111,7 +111,7 @@ Run `ray --help` to see all available commands.
111
111
  | `ray setup` | Configure API keys and preferences |
112
112
  | `ray link` | Connect a new bank account |
113
113
  | `ray add` | Add a manual account (home, car, crypto, etc.) |
114
- | `ray remove` | Remove a manual account |
114
+ | `ray remove` | Remove a linked bank or manual account |
115
115
  | `ray sync` | Pull latest transactions and balances |
116
116
  | `ray status` | Quick financial dashboard |
117
117
  | `ray accounts` | Linked accounts with balances |
@@ -197,6 +197,10 @@ RAY_API_KEY= # Ray API key (managed mode, replaces the above)
197
197
 
198
198
  Have an idea? [Open a PR](https://github.com/cdinnison/ray-finance/pulls).
199
199
 
200
+ ## Support
201
+
202
+ Questions, feedback, or need help getting set up? Email [clark@rayfinance.app](mailto:clark@rayfinance.app) or [open an issue](https://github.com/cdinnison/ray-finance/issues).
203
+
200
204
  ## Contributing
201
205
 
202
206
  ```bash
package/dist/ai/tools.js CHANGED
@@ -411,6 +411,8 @@ export async function executeTool(db, toolName, toolInput) {
411
411
  updates.push("target_date = ?");
412
412
  params.push(toolInput.target_date);
413
413
  }
414
+ if (updates.length === 0)
415
+ return `Goal "${toolInput.name}" exists but no changes provided.`;
414
416
  params.push(existing.id);
415
417
  db.prepare(`UPDATE goals SET ${updates.join(", ")} WHERE id = ?`).run(...params);
416
418
  return `Goal "${toolInput.name}" updated.`;
@@ -12,7 +12,7 @@ export function runExport(outputPath) {
12
12
  exported_at: new Date().toISOString(),
13
13
  context: readContext(),
14
14
  memories: db.prepare("SELECT content, category FROM memories").all(),
15
- goals: db.prepare("SELECT name, target_amount, current_amount, deadline, status FROM goals").all(),
15
+ goals: db.prepare("SELECT name, target_amount, current_amount, target_date, status FROM goals").all(),
16
16
  budgets: db.prepare("SELECT category, monthly_limit, period FROM budgets").all(),
17
17
  recat_rules: db.prepare("SELECT match_field, match_pattern, target_category, target_subcategory, label FROM recategorization_rules").all(),
18
18
  settings: db.prepare("SELECT key, value FROM settings").all(),
@@ -54,10 +54,10 @@ export function runImport(inputPath) {
54
54
  }
55
55
  // Restore goals (skip if name already exists)
56
56
  const existingGoal = db.prepare("SELECT 1 FROM goals WHERE name = ?");
57
- const insertGoal = db.prepare("INSERT INTO goals (name, target_amount, current_amount, deadline, status) VALUES (?, ?, ?, ?, ?)");
57
+ const insertGoal = db.prepare("INSERT INTO goals (name, target_amount, current_amount, target_date, status) VALUES (?, ?, ?, ?, ?)");
58
58
  for (const g of backup.goals) {
59
59
  if (!existingGoal.get(g.name)) {
60
- insertGoal.run(g.name, g.target_amount, g.current_amount, g.deadline, g.status);
60
+ insertGoal.run(g.name, g.target_amount, g.current_amount, g.target_date ?? g.deadline ?? null, g.status);
61
61
  }
62
62
  }
63
63
  // Restore budgets
@@ -190,7 +190,14 @@ export function showTransactions(options = {}) {
190
190
  export async function showSpending(period = "this_month") {
191
191
  const db = getDb();
192
192
  const { resolvePeriod } = await import("../db/helpers.js");
193
- const { start, end } = resolvePeriod(period);
193
+ let start, end;
194
+ try {
195
+ ({ start, end } = resolvePeriod(period));
196
+ }
197
+ catch {
198
+ console.log(`\nUnknown period "${period}". Use: this_month, last_month, last_30, last_90, or START:END`);
199
+ return;
200
+ }
194
201
  const rows = db.prepare(`SELECT category, SUM(amount) as total, COUNT(*) as count FROM transactions
195
202
  WHERE amount > 0 AND date BETWEEN ? AND ? AND pending = 0
196
203
  AND category NOT IN ('TRANSFER_OUT', 'TRANSFER_IN', 'LOAN_PAYMENTS')
@@ -103,7 +103,7 @@ export async function runDoctor() {
103
103
  }
104
104
  // ── Encryption ──
105
105
  if (config.dbEncryptionKey) {
106
- checks.push({ label: "Encryption", status: "ok", detail: "AES-256-CBC enabled" });
106
+ checks.push({ label: "Encryption", status: "ok", detail: "AES-256-GCM enabled" });
107
107
  }
108
108
  else {
109
109
  checks.push({ label: "Encryption", status: "warn", detail: "No encryption key set. Data stored in plaintext." });
package/dist/config.d.ts CHANGED
@@ -13,6 +13,7 @@ export interface RayConfig {
13
13
  userName: string;
14
14
  thinkingBudget: number;
15
15
  syncSchedule: string;
16
+ plaidCountries: string[];
16
17
  }
17
18
  export declare const RAY_PROXY_BASE = "https://api.rayfinance.app/v1";
18
19
  export declare function useManaged(): boolean;
package/dist/config.js CHANGED
@@ -37,6 +37,7 @@ function buildConfig() {
37
37
  userName: file.userName || process.env.RAY_USER_NAME || "User",
38
38
  thinkingBudget: file.thinkingBudget ?? (Number(process.env.RAY_THINKING_BUDGET) || 8000),
39
39
  syncSchedule: file.syncSchedule || "",
40
+ plaidCountries: file.plaidCountries || ["US", "GB", "CA"],
40
41
  };
41
42
  }
42
43
  export const config = buildConfig();
@@ -1,4 +1,5 @@
1
- import { Products } from "plaid";
1
+ import { CountryCode, Products } from "plaid";
2
+ export declare function getCountryCodes(): CountryCode[];
2
3
  /** Create a link token for initializing Plaid Link */
3
4
  export declare function createLinkToken(products?: Products[]): Promise<string>;
4
5
  /** Exchange a public token from Plaid Link for an access token */
@@ -1,5 +1,13 @@
1
1
  import { plaidClient } from "./client.js";
2
2
  import { CountryCode, Products } from "plaid";
3
+ import { config } from "../config.js";
4
+ export function getCountryCodes() {
5
+ const codes = config.plaidCountries
6
+ .map(c => c.toUpperCase())
7
+ .filter(c => c in CountryCode)
8
+ .map(c => CountryCode[c]);
9
+ return codes.length > 0 ? codes : [CountryCode.Us];
10
+ }
3
11
  /** Create a link token for initializing Plaid Link */
4
12
  export async function createLinkToken(products = [Products.Transactions]) {
5
13
  const resp = await plaidClient.linkTokenCreate({
@@ -7,7 +15,7 @@ export async function createLinkToken(products = [Products.Transactions]) {
7
15
  client_name: "Ray Finance",
8
16
  products,
9
17
  optional_products: [Products.Investments, Products.Liabilities],
10
- country_codes: [CountryCode.Us],
18
+ country_codes: getCountryCodes(),
11
19
  language: "en",
12
20
  });
13
21
  return resp.data.link_token;
@@ -1,5 +1,5 @@
1
1
  import { plaidClient } from "./client.js";
2
- import { CountryCode } from "plaid";
2
+ import { getCountryCodes } from "./link.js";
3
3
  /** Check if a Plaid API error is "product not supported/enabled" — safe to ignore */
4
4
  export function isProductNotSupported(err) {
5
5
  const data = err?.response?.data;
@@ -26,7 +26,7 @@ export async function refreshProducts(db, itemId, accessToken) {
26
26
  try {
27
27
  const { data } = await plaidClient.institutionsGetById({
28
28
  institution_id: resp.data.item.institution_id,
29
- country_codes: [CountryCode.Us],
29
+ country_codes: getCountryCodes(),
30
30
  options: { include_optional_metadata: true },
31
31
  });
32
32
  db.prepare(`UPDATE institutions SET logo = ?, primary_color = ? WHERE item_id = ?`)
package/dist/server.js CHANGED
@@ -5,7 +5,7 @@ import { randomUUID } from "crypto";
5
5
  import { createLinkToken, exchangeToken } from "./plaid/link.js";
6
6
  import { syncBalances, syncTransactions, syncInvestments, syncInvestmentTransactions, syncLiabilities, syncRecurring, isProductNotSupported } from "./plaid/sync.js";
7
7
  import { plaidClient } from "./plaid/client.js";
8
- import { CountryCode } from "plaid";
8
+ import { getCountryCodes } from "./plaid/link.js";
9
9
  import { encryptPlaidToken } from "./db/encryption.js";
10
10
  import { config } from "./config.js";
11
11
  import { getDb } from "./db/connection.js";
@@ -198,7 +198,7 @@ export function startLinkServer() {
198
198
  try {
199
199
  const { data } = await plaidClient.institutionsGetById({
200
200
  institution_id: req.body.institution_id,
201
- country_codes: [CountryCode.Us],
201
+ country_codes: getCountryCodes(),
202
202
  options: { include_optional_metadata: true },
203
203
  });
204
204
  institutionLogo = data.institution.logo || null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ray-finance",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
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",