@warmio/mcp 4.1.0 → 4.2.0
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 +50 -19
- package/dist/config-paths.d.ts +2 -0
- package/dist/config-paths.js +23 -0
- package/dist/http.js +1 -1
- package/dist/index.js +1 -1
- package/dist/install.js +24 -10
- package/dist/schemas.d.ts +1 -0
- package/dist/schemas.js +8 -10
- package/dist/warm-server.js +4 -117
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,8 @@ npx @warmio/mcp
|
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
The installer detects supported MCP clients, prompts for your Warm API key, and writes the local
|
|
19
|
-
`stdio` server config automatically.
|
|
19
|
+
`stdio` server config automatically. The key is stored once in your local Warm profile instead of
|
|
20
|
+
being duplicated into every MCP client config.
|
|
20
21
|
|
|
21
22
|
## Requirements
|
|
22
23
|
|
|
@@ -26,28 +27,31 @@ The installer detects supported MCP clients, prompts for your Warm API key, and
|
|
|
26
27
|
|
|
27
28
|
## Manual `stdio` Config
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
The installer stores your API key in the Warm config directory and generated MCP client configs can
|
|
31
|
+
stay secret-free:
|
|
30
32
|
|
|
31
33
|
```json
|
|
32
34
|
{
|
|
33
35
|
"mcpServers": {
|
|
34
36
|
"warm": {
|
|
35
37
|
"command": "npx",
|
|
36
|
-
"args": ["-y", "@warmio/mcp", "--server"]
|
|
37
|
-
"env": {
|
|
38
|
-
"WARM_API_KEY": "your_warm_api_key"
|
|
39
|
-
}
|
|
38
|
+
"args": ["-y", "@warmio/mcp", "--server"]
|
|
40
39
|
}
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
42
|
```
|
|
44
43
|
|
|
44
|
+
Optional auth overrides:
|
|
45
|
+
|
|
46
|
+
- `WARM_API_KEY`
|
|
47
|
+
- `WARM_API_KEY_FILE`
|
|
48
|
+
|
|
45
49
|
## Self-hosted Streamable HTTP
|
|
46
50
|
|
|
47
51
|
Run the HTTP server locally or behind your own reverse proxy:
|
|
48
52
|
|
|
49
53
|
```bash
|
|
50
|
-
npx @warmio/mcp http --host
|
|
54
|
+
npx @warmio/mcp http --host 127.0.0.1 --port 3000 --path /mcp
|
|
51
55
|
```
|
|
52
56
|
|
|
53
57
|
Environment overrides:
|
|
@@ -56,6 +60,7 @@ Environment overrides:
|
|
|
56
60
|
- `WARM_MCP_HTTP_PORT`
|
|
57
61
|
- `WARM_MCP_HTTP_PATH`
|
|
58
62
|
- `WARM_MCP_ALLOWED_HOSTS`
|
|
63
|
+
- `WARM_API_KEY_FILE`
|
|
59
64
|
|
|
60
65
|
On Windows, prefer:
|
|
61
66
|
|
|
@@ -64,10 +69,7 @@ On Windows, prefer:
|
|
|
64
69
|
"mcpServers": {
|
|
65
70
|
"warm": {
|
|
66
71
|
"command": "cmd",
|
|
67
|
-
"args": ["/c", "npx", "-y", "@warmio/mcp", "--server"]
|
|
68
|
-
"env": {
|
|
69
|
-
"WARM_API_KEY": "your_warm_api_key"
|
|
70
|
-
}
|
|
72
|
+
"args": ["/c", "npx", "-y", "@warmio/mcp", "--server"]
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
}
|
|
@@ -77,12 +79,12 @@ On Windows, prefer:
|
|
|
77
79
|
|
|
78
80
|
Warm's published/documented MCP surface is the following four-tool core:
|
|
79
81
|
|
|
80
|
-
| Tool
|
|
81
|
-
|
|
82
|
-
| `get_accounts`
|
|
83
|
-
| `get_transactions`
|
|
82
|
+
| Tool | Description |
|
|
83
|
+
| --------------------- | ----------------------------------------------- |
|
|
84
|
+
| `get_accounts` | List connected accounts with current balances |
|
|
85
|
+
| `get_transactions` | Page through transactions with an opaque cursor |
|
|
84
86
|
| `get_financial_state` | Return the current typed financial state bundle |
|
|
85
|
-
| `verify_key`
|
|
87
|
+
| `verify_key` | Validate the configured API key |
|
|
86
88
|
|
|
87
89
|
## Strict Contract
|
|
88
90
|
|
|
@@ -127,7 +129,8 @@ Input:
|
|
|
127
129
|
{
|
|
128
130
|
"limit": 100,
|
|
129
131
|
"cursor": "opaque-cursor-from-a-prior-page",
|
|
130
|
-
"last_knowledge": "2026-03-11T00:00:00.000Z"
|
|
132
|
+
"last_knowledge": "2026-03-11T00:00:00.000Z",
|
|
133
|
+
"search": "coffee"
|
|
131
134
|
}
|
|
132
135
|
```
|
|
133
136
|
|
|
@@ -159,7 +162,7 @@ Returns:
|
|
|
159
162
|
Cursor model:
|
|
160
163
|
|
|
161
164
|
1. Omit `cursor` on the first call.
|
|
162
|
-
2. Keep `limit` fixed while following a cursor chain.
|
|
165
|
+
2. Keep `limit` and any filters such as `search` fixed while following a cursor chain.
|
|
163
166
|
3. If `pagination.next_cursor` is non-null, pass it unchanged to fetch the next page.
|
|
164
167
|
4. Stop when `next_cursor` is `null`.
|
|
165
168
|
5. Do not combine `cursor` with `last_knowledge`.
|
|
@@ -229,7 +232,35 @@ Returns:
|
|
|
229
232
|
"build": 20
|
|
230
233
|
},
|
|
231
234
|
"message": null
|
|
232
|
-
}
|
|
235
|
+
},
|
|
236
|
+
"liabilities": [
|
|
237
|
+
{
|
|
238
|
+
"account_id": "acc_loan_1",
|
|
239
|
+
"type": "student",
|
|
240
|
+
"balance": 12450.22,
|
|
241
|
+
"apr_percentage": 5.2,
|
|
242
|
+
"minimum_payment": 145,
|
|
243
|
+
"next_payment_due_date": "2026-03-22",
|
|
244
|
+
"is_overdue": false
|
|
245
|
+
}
|
|
246
|
+
],
|
|
247
|
+
"holdings": [
|
|
248
|
+
{
|
|
249
|
+
"account_id": "acc_inv_1",
|
|
250
|
+
"security_name": "Vanguard Total Stock Market ETF",
|
|
251
|
+
"symbol": "VTI",
|
|
252
|
+
"type": "etf",
|
|
253
|
+
"quantity": 12.5,
|
|
254
|
+
"value": 3541.25,
|
|
255
|
+
"cost_basis": 3010
|
|
256
|
+
}
|
|
257
|
+
],
|
|
258
|
+
"category_spending": [
|
|
259
|
+
{
|
|
260
|
+
"category": "FOOD_AND_DRINK",
|
|
261
|
+
"amount": 182.55
|
|
262
|
+
}
|
|
263
|
+
]
|
|
233
264
|
}
|
|
234
265
|
```
|
|
235
266
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as os from 'node:os';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export function getWarmConfigDir() {
|
|
4
|
+
if (process.env.WARM_CONFIG_DIR?.trim()) {
|
|
5
|
+
return process.env.WARM_CONFIG_DIR.trim();
|
|
6
|
+
}
|
|
7
|
+
if (process.platform === 'win32') {
|
|
8
|
+
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Warm');
|
|
9
|
+
}
|
|
10
|
+
if (process.env.XDG_CONFIG_HOME?.trim()) {
|
|
11
|
+
return path.join(process.env.XDG_CONFIG_HOME.trim(), 'warm');
|
|
12
|
+
}
|
|
13
|
+
if (process.platform === 'darwin') {
|
|
14
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'Warm');
|
|
15
|
+
}
|
|
16
|
+
return path.join(os.homedir(), '.config', 'warm');
|
|
17
|
+
}
|
|
18
|
+
export function getWarmApiKeyPath() {
|
|
19
|
+
if (process.env.WARM_API_KEY_FILE?.trim()) {
|
|
20
|
+
return process.env.WARM_API_KEY_FILE.trim();
|
|
21
|
+
}
|
|
22
|
+
return path.join(getWarmConfigDir(), 'api_key');
|
|
23
|
+
}
|
package/dist/http.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
|
|
2
2
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
3
|
import { createWarmServer } from './server.js';
|
|
4
|
-
const DEFAULT_HTTP_HOST = '
|
|
4
|
+
const DEFAULT_HTTP_HOST = '127.0.0.1';
|
|
5
5
|
const DEFAULT_HTTP_PORT = 3000;
|
|
6
6
|
const DEFAULT_HTTP_PATH = '/mcp';
|
|
7
7
|
function parsePort(value, fallback) {
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ function printUsage() {
|
|
|
9
9
|
console.log('');
|
|
10
10
|
console.log(' warm-mcp [install] [--force] [--no-validate]');
|
|
11
11
|
console.log(' warm-mcp stdio');
|
|
12
|
-
console.log(' warm-mcp http [--host
|
|
12
|
+
console.log(' warm-mcp http [--host 127.0.0.1] [--port 3000] [--path /mcp]');
|
|
13
13
|
console.log(' [--allowed-hosts host1,host2]');
|
|
14
14
|
console.log('');
|
|
15
15
|
console.log(' Aliases:');
|
package/dist/install.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
|
3
3
|
import { dirname, join, resolve } from 'path';
|
|
4
4
|
import { homedir, platform } from 'os';
|
|
5
5
|
import { verifyWarmApiKey } from './server.js';
|
|
6
|
+
import { getWarmApiKeyPath } from './config-paths.js';
|
|
6
7
|
const HOME = homedir();
|
|
7
8
|
const CWD = process.cwd();
|
|
8
9
|
function isRecord(value) {
|
|
@@ -31,7 +32,11 @@ const GLOBAL_CLIENTS = [
|
|
|
31
32
|
configPath: join(HOME, '.codeium', 'windsurf', 'mcp_config.json'),
|
|
32
33
|
format: 'json',
|
|
33
34
|
},
|
|
34
|
-
{
|
|
35
|
+
{
|
|
36
|
+
name: 'OpenCode',
|
|
37
|
+
configPath: join(HOME, '.config', 'opencode', 'opencode.json'),
|
|
38
|
+
format: 'json',
|
|
39
|
+
},
|
|
35
40
|
{ name: 'Codex CLI', configPath: join(HOME, '.codex', 'config.toml'), format: 'toml' },
|
|
36
41
|
{
|
|
37
42
|
name: 'Antigravity',
|
|
@@ -44,6 +49,7 @@ const PROJECT_CONFIGS = ['.mcp.json', '.cursor/mcp.json', '.vscode/mcp.json'];
|
|
|
44
49
|
const MCP_CONFIG = platform() === 'win32'
|
|
45
50
|
? { command: 'cmd', args: ['/c', 'npx', '-y', '@warmio/mcp', '--server'] }
|
|
46
51
|
: { command: 'npx', args: ['-y', '@warmio/mcp', '--server'] };
|
|
52
|
+
const WARM_API_KEY_PATH = getWarmApiKeyPath();
|
|
47
53
|
function detectProjectClients() {
|
|
48
54
|
const found = [];
|
|
49
55
|
for (const name of PROJECT_CONFIGS) {
|
|
@@ -80,7 +86,7 @@ function isConfigured(client) {
|
|
|
80
86
|
return false;
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
|
-
function configureJson(client
|
|
89
|
+
function configureJson(client) {
|
|
84
90
|
let config = {};
|
|
85
91
|
if (existsSync(client.configPath)) {
|
|
86
92
|
try {
|
|
@@ -96,23 +102,25 @@ function configureJson(client, apiKey) {
|
|
|
96
102
|
const servers = config.mcpServers;
|
|
97
103
|
const existing = isRecord(servers.warm) ? servers.warm : undefined;
|
|
98
104
|
const existingEnv = isRecord(existing?.env) ? existing.env : {};
|
|
105
|
+
const nextEnv = { ...existingEnv };
|
|
106
|
+
delete nextEnv.WARM_API_KEY;
|
|
99
107
|
if (client.isProjectLevel && existing?.command) {
|
|
100
108
|
servers.warm = {
|
|
101
109
|
...existing,
|
|
102
|
-
env:
|
|
110
|
+
...(Object.keys(nextEnv).length > 0 ? { env: nextEnv } : {}),
|
|
103
111
|
};
|
|
104
112
|
}
|
|
105
113
|
else {
|
|
106
114
|
servers.warm = {
|
|
107
115
|
...existing,
|
|
108
116
|
...MCP_CONFIG,
|
|
109
|
-
env:
|
|
117
|
+
...(Object.keys(nextEnv).length > 0 ? { env: nextEnv } : {}),
|
|
110
118
|
};
|
|
111
119
|
}
|
|
112
120
|
mkdirSync(dirname(client.configPath), { recursive: true });
|
|
113
121
|
writeFileSync(client.configPath, JSON.stringify(config, null, 2) + '\n');
|
|
114
122
|
}
|
|
115
|
-
function configureToml(client
|
|
123
|
+
function configureToml(client) {
|
|
116
124
|
let content = '';
|
|
117
125
|
if (existsSync(client.configPath)) {
|
|
118
126
|
content = readFileSync(client.configPath, 'utf-8');
|
|
@@ -124,7 +132,7 @@ function configureToml(client, apiKey) {
|
|
|
124
132
|
const tomlArgs = platform() === 'win32'
|
|
125
133
|
? '["/c", "npx", "-y", "@warmio/mcp", "--server"]'
|
|
126
134
|
: '["-y", "@warmio/mcp", "--server"]';
|
|
127
|
-
const warmBlock = `[mcp_servers.warm]\ncommand = "${tomlCommand}"\nargs = ${tomlArgs}\n
|
|
135
|
+
const warmBlock = `[mcp_servers.warm]\ncommand = "${tomlCommand}"\nargs = ${tomlArgs}\n`;
|
|
128
136
|
const warmBlockPattern = /\n?\[mcp_servers\.warm\][\s\S]*?(?=\n\[[^\n]+\]|\s*$)/g;
|
|
129
137
|
let nextContent = content.replace(warmBlockPattern, '').trimEnd();
|
|
130
138
|
if (nextContent.length > 0) {
|
|
@@ -134,12 +142,12 @@ function configureToml(client, apiKey) {
|
|
|
134
142
|
mkdirSync(dirname(client.configPath), { recursive: true });
|
|
135
143
|
writeFileSync(client.configPath, nextContent.endsWith('\n') ? nextContent : `${nextContent}\n`);
|
|
136
144
|
}
|
|
137
|
-
function configure(client
|
|
145
|
+
function configure(client) {
|
|
138
146
|
if (client.format === 'json') {
|
|
139
|
-
configureJson(client
|
|
147
|
+
configureJson(client);
|
|
140
148
|
return;
|
|
141
149
|
}
|
|
142
|
-
configureToml(client
|
|
150
|
+
configureToml(client);
|
|
143
151
|
}
|
|
144
152
|
function shortPath(filePath) {
|
|
145
153
|
return filePath.replace(HOME, '~').replace(CWD, '.');
|
|
@@ -185,6 +193,10 @@ async function validateApiKey(apiKey) {
|
|
|
185
193
|
return true;
|
|
186
194
|
}
|
|
187
195
|
}
|
|
196
|
+
function storeApiKey(apiKey) {
|
|
197
|
+
mkdirSync(dirname(WARM_API_KEY_PATH), { recursive: true });
|
|
198
|
+
writeFileSync(WARM_API_KEY_PATH, `${apiKey}\n`, { mode: 0o600 });
|
|
199
|
+
}
|
|
188
200
|
export async function install(options = {}) {
|
|
189
201
|
const force = options.force ?? false;
|
|
190
202
|
const shouldValidateApiKey = options.validateApiKey ?? true;
|
|
@@ -222,11 +234,12 @@ export async function install(options = {}) {
|
|
|
222
234
|
return;
|
|
223
235
|
}
|
|
224
236
|
}
|
|
237
|
+
storeApiKey(apiKey);
|
|
225
238
|
console.log(' Configuring...');
|
|
226
239
|
console.log('');
|
|
227
240
|
needsSetup.forEach((client) => {
|
|
228
241
|
try {
|
|
229
|
-
configure(client
|
|
242
|
+
configure(client);
|
|
230
243
|
console.log(` ${client.name.padEnd(22)} done`);
|
|
231
244
|
}
|
|
232
245
|
catch (error) {
|
|
@@ -235,6 +248,7 @@ export async function install(options = {}) {
|
|
|
235
248
|
}
|
|
236
249
|
});
|
|
237
250
|
console.log('');
|
|
251
|
+
console.log(` Stored API key at ${shortPath(WARM_API_KEY_PATH)}`);
|
|
238
252
|
console.log(' All set! Restart your MCP clients and try:');
|
|
239
253
|
console.log(' "What\'s my net worth?"');
|
|
240
254
|
console.log('');
|
package/dist/schemas.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export declare const getTransactionsInputSchema: z.ZodObject<{
|
|
|
41
41
|
limit: z.ZodDefault<z.ZodInt>;
|
|
42
42
|
cursor: z.ZodOptional<z.ZodString>;
|
|
43
43
|
last_knowledge: z.ZodOptional<z.ZodString>;
|
|
44
|
+
search: z.ZodOptional<z.ZodString>;
|
|
44
45
|
}, z.core.$strict>;
|
|
45
46
|
export declare const transactionSchema: z.ZodObject<{
|
|
46
47
|
id: z.ZodNullable<z.ZodString>;
|
package/dist/schemas.js
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
import * as z from 'zod/v4';
|
|
2
|
-
const dateSchema = z
|
|
3
|
-
.string()
|
|
4
|
-
.regex(/^\d{4}-\d{2}-\d{2}$/, 'Expected date in YYYY-MM-DD format.');
|
|
2
|
+
const dateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Expected date in YYYY-MM-DD format.');
|
|
5
3
|
const dateTimeSchema = z.string().datetime({ offset: true });
|
|
6
4
|
export const emptyInputSchema = z.object({}).strict().default({});
|
|
7
|
-
export const accountTypeSchema = z.enum([
|
|
8
|
-
'depository',
|
|
9
|
-
'credit',
|
|
10
|
-
'loan',
|
|
11
|
-
'investment',
|
|
12
|
-
'other',
|
|
13
|
-
]);
|
|
5
|
+
export const accountTypeSchema = z.enum(['depository', 'credit', 'loan', 'investment', 'other']);
|
|
14
6
|
export const accountSchema = z
|
|
15
7
|
.object({
|
|
16
8
|
name: z.string(),
|
|
@@ -42,6 +34,12 @@ const getTransactionsInputObjectSchema = z
|
|
|
42
34
|
last_knowledge: dateTimeSchema
|
|
43
35
|
.optional()
|
|
44
36
|
.describe('Incremental sync checkpoint from a prior get_transactions response. Cannot be combined with cursor.'),
|
|
37
|
+
search: z
|
|
38
|
+
.string()
|
|
39
|
+
.min(1)
|
|
40
|
+
.max(120)
|
|
41
|
+
.optional()
|
|
42
|
+
.describe('Optional merchant or description search string. Keep it unchanged while following a cursor chain.'),
|
|
45
43
|
})
|
|
46
44
|
.strict()
|
|
47
45
|
.superRefine((value, ctx) => {
|
package/dist/warm-server.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
|
-
import * as os from 'node:os';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
3
|
import { emptyInputSchema, getAccountsOutputSchema, getFinancialStateOutputSchema, getTransactionsInputSchema, getTransactionsOutputSchema, verifyKeyOutputSchema, } from './schemas.js';
|
|
4
|
+
import { getWarmApiKeyPath } from './config-paths.js';
|
|
6
5
|
const DEFAULT_API_URL = process.env.WARM_API_URL || 'https://warm.io';
|
|
7
6
|
const DEFAULT_REQUEST_TIMEOUT_MS = (() => {
|
|
8
7
|
const raw = Number(process.env.WARM_API_TIMEOUT_MS || 10_000);
|
|
@@ -63,70 +62,6 @@ function normalizeAccount(account) {
|
|
|
63
62
|
mask: account.mask ?? null,
|
|
64
63
|
};
|
|
65
64
|
}
|
|
66
|
-
function normalizeSnapshot(snapshot) {
|
|
67
|
-
const date = snapshot.snapshot_date || snapshot.period_end || null;
|
|
68
|
-
if (!date) {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
const totalAssets = snapshot.total_assets ??
|
|
72
|
-
(snapshot.total_cash ?? 0) + (snapshot.investment_value ?? 0) + (snapshot.asset_value ?? 0);
|
|
73
|
-
const totalLiabilities = snapshot.total_liabilities ?? snapshot.total_debt ?? snapshot.liability_value ?? 0;
|
|
74
|
-
return {
|
|
75
|
-
date,
|
|
76
|
-
net_worth: roundMoney(snapshot.net_worth ?? totalAssets - totalLiabilities),
|
|
77
|
-
total_assets: roundMoney(totalAssets),
|
|
78
|
-
total_liabilities: roundMoney(totalLiabilities),
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
function normalizeRecurring(stream) {
|
|
82
|
-
return {
|
|
83
|
-
merchant: stream.merchant_name || stream.description || 'Unknown',
|
|
84
|
-
amount: roundMoney(Math.abs(stream.average_amount ?? stream.last_amount ?? 0)),
|
|
85
|
-
frequency: stream.frequency || 'UNKNOWN',
|
|
86
|
-
next_date: stream.next_date ?? null,
|
|
87
|
-
type: stream.stream_type ?? null,
|
|
88
|
-
active: stream.is_active !== false,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
function normalizeGoal(goal) {
|
|
92
|
-
return {
|
|
93
|
-
name: goal.name || 'Unnamed Goal',
|
|
94
|
-
target: roundMoney(goal.target ?? 0),
|
|
95
|
-
current: roundMoney(goal.current ?? 0),
|
|
96
|
-
progress_percent: roundMoney(goal.progress_percent ?? 0),
|
|
97
|
-
target_date: goal.target_date ?? null,
|
|
98
|
-
status: goal.status ?? null,
|
|
99
|
-
category: goal.category ?? null,
|
|
100
|
-
monthly_contribution_needed: goal.monthly_contribution_needed == null
|
|
101
|
-
? null
|
|
102
|
-
: roundMoney(goal.monthly_contribution_needed),
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
function normalizeLiability(liability) {
|
|
106
|
-
return {
|
|
107
|
-
account_id: liability.account_id || '',
|
|
108
|
-
type: liability.type || 'unknown',
|
|
109
|
-
balance: liability.balance == null ? null : roundMoney(liability.balance),
|
|
110
|
-
apr_percentage: liability.apr_percentage ?? liability.interest_rate_percentage ?? null,
|
|
111
|
-
minimum_payment: liability.minimum_payment == null ? null : roundMoney(liability.minimum_payment),
|
|
112
|
-
next_payment_due_date: liability.next_payment_due_date ?? null,
|
|
113
|
-
is_overdue: liability.is_overdue ?? null,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
function normalizeHolding(holding) {
|
|
117
|
-
return {
|
|
118
|
-
account_id: holding.account_id || '',
|
|
119
|
-
security_name: holding.security_name ?? null,
|
|
120
|
-
symbol: holding.symbol ?? null,
|
|
121
|
-
type: holding.type ?? null,
|
|
122
|
-
quantity: holding.quantity ?? 0,
|
|
123
|
-
value: holding.value == null ? null : roundMoney(holding.value),
|
|
124
|
-
cost_basis: holding.cost_basis == null ? null : roundMoney(holding.cost_basis),
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
function asGeneratedAt(...values) {
|
|
128
|
-
return values.find((value) => Boolean(value)) || new Date().toISOString();
|
|
129
|
-
}
|
|
130
65
|
function createWarmApiClientConfig(options) {
|
|
131
66
|
return {
|
|
132
67
|
apiUrl: options.apiUrl || DEFAULT_API_URL,
|
|
@@ -143,7 +78,7 @@ export function getConfiguredApiKey() {
|
|
|
143
78
|
cachedApiKey = process.env.WARM_API_KEY.trim();
|
|
144
79
|
return cachedApiKey;
|
|
145
80
|
}
|
|
146
|
-
const configPath =
|
|
81
|
+
const configPath = getWarmApiKeyPath();
|
|
147
82
|
try {
|
|
148
83
|
cachedApiKey = fs.readFileSync(configPath, 'utf-8').trim() || null;
|
|
149
84
|
}
|
|
@@ -229,6 +164,7 @@ export function createWarmApiClient(options = {}) {
|
|
|
229
164
|
limit: String(input.limit),
|
|
230
165
|
cursor: input.cursor,
|
|
231
166
|
last_knowledge: input.last_knowledge,
|
|
167
|
+
search: input.search,
|
|
232
168
|
}, options);
|
|
233
169
|
const nextCursor = response.pagination?.next_cursor ?? null;
|
|
234
170
|
return {
|
|
@@ -251,56 +187,7 @@ export function createWarmApiClient(options = {}) {
|
|
|
251
187
|
};
|
|
252
188
|
}
|
|
253
189
|
async function getFinancialState() {
|
|
254
|
-
|
|
255
|
-
apiRequest('/api/export', { dataset: 'snapshots' }, options),
|
|
256
|
-
apiRequest('/api/export', { dataset: 'recurring' }, options),
|
|
257
|
-
apiRequest('/api/export', { dataset: 'budgets' }, options),
|
|
258
|
-
apiRequest('/api/export', { dataset: 'goals' }, options),
|
|
259
|
-
apiRequest('/api/export', { dataset: 'health' }, options),
|
|
260
|
-
apiRequest('/api/export', { dataset: 'liabilities' }, options),
|
|
261
|
-
apiRequest('/api/export', { dataset: 'holdings' }, options),
|
|
262
|
-
]);
|
|
263
|
-
// Extract category spending from the most recent snapshot's spending_by_category
|
|
264
|
-
const snapshots = snapshotsResponse.snapshots || [];
|
|
265
|
-
const latestWithCategories = snapshots.find((s) => Array.isArray(s.spending_by_category));
|
|
266
|
-
const categorySpending = (latestWithCategories?.spending_by_category || []).map((c) => ({
|
|
267
|
-
category: c.category || 'Unknown',
|
|
268
|
-
amount: roundMoney(Math.abs(c.amount ?? 0)),
|
|
269
|
-
}));
|
|
270
|
-
return {
|
|
271
|
-
generated_at: asGeneratedAt(snapshotsResponse.generated_at, recurringResponse.generated_at, budgetsResponse.generated_at, goalsResponse.generated_at, healthResponse.generated_at),
|
|
272
|
-
snapshots: snapshots
|
|
273
|
-
.map(normalizeSnapshot)
|
|
274
|
-
.filter((snapshot) => snapshot !== null),
|
|
275
|
-
recurring: (recurringResponse.recurring_transactions || []).map(normalizeRecurring),
|
|
276
|
-
budgets: (budgetsResponse.budgets || []).map((budget) => ({
|
|
277
|
-
name: budget.name || 'Unnamed Budget',
|
|
278
|
-
amount: roundMoney(budget.amount ?? 0),
|
|
279
|
-
spent: roundMoney(budget.spent ?? 0),
|
|
280
|
-
remaining: roundMoney(budget.remaining ?? 0),
|
|
281
|
-
percent_used: roundMoney(budget.percent_used ?? 0),
|
|
282
|
-
period: budget.period || 'monthly',
|
|
283
|
-
status: budget.status ?? null,
|
|
284
|
-
})),
|
|
285
|
-
goals: (goalsResponse.goals || []).map(normalizeGoal),
|
|
286
|
-
health: {
|
|
287
|
-
score: healthResponse.score ?? null,
|
|
288
|
-
label: healthResponse.label ?? null,
|
|
289
|
-
data_completeness: healthResponse.data_completeness ?? null,
|
|
290
|
-
pillars: healthResponse.pillars
|
|
291
|
-
? {
|
|
292
|
-
spend: healthResponse.pillars.spend ?? null,
|
|
293
|
-
save: healthResponse.pillars.save ?? null,
|
|
294
|
-
borrow: healthResponse.pillars.borrow ?? null,
|
|
295
|
-
build: healthResponse.pillars.build ?? null,
|
|
296
|
-
}
|
|
297
|
-
: null,
|
|
298
|
-
message: healthResponse.message ?? null,
|
|
299
|
-
},
|
|
300
|
-
liabilities: (liabilitiesResponse.liabilities || []).map(normalizeLiability),
|
|
301
|
-
holdings: (holdingsResponse.holdings || []).map(normalizeHolding),
|
|
302
|
-
category_spending: categorySpending,
|
|
303
|
-
};
|
|
190
|
+
return apiRequest('/api/financial-state', {}, options);
|
|
304
191
|
}
|
|
305
192
|
async function verifyKey() {
|
|
306
193
|
const response = await apiRequest('/api/verify', {}, options);
|