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 +6 -2
- package/dist/ai/tools.js +2 -0
- package/dist/cli/backup.js +3 -3
- package/dist/cli/commands.js +8 -1
- package/dist/cli/doctor.js +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +1 -0
- package/dist/plaid/link.d.ts +2 -1
- package/dist/plaid/link.js +9 -1
- package/dist/plaid/sync.js +2 -2
- package/dist/server.js +2 -2
- package/package.json +1 -1
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.`;
|
package/dist/cli/backup.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
package/dist/cli/commands.js
CHANGED
|
@@ -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
|
-
|
|
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')
|
package/dist/cli/doctor.js
CHANGED
|
@@ -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-
|
|
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
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();
|
package/dist/plaid/link.d.ts
CHANGED
|
@@ -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 */
|
package/dist/plaid/link.js
CHANGED
|
@@ -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:
|
|
18
|
+
country_codes: getCountryCodes(),
|
|
11
19
|
language: "en",
|
|
12
20
|
});
|
|
13
21
|
return resp.data.link_token;
|
package/dist/plaid/sync.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { plaidClient } from "./client.js";
|
|
2
|
-
import {
|
|
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:
|
|
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 {
|
|
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:
|
|
201
|
+
country_codes: getCountryCodes(),
|
|
202
202
|
options: { include_optional_metadata: true },
|
|
203
203
|
});
|
|
204
204
|
institutionLogo = data.institution.logo || null;
|