financy 0.1.2 → 0.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 +19 -1
- package/dist/bin.js +122 -50
- package/dist/{server-5G6DYQRD.js → chunk-74AQOXZZ.js} +23 -42
- package/dist/{chunk-XODXLY7E.js → chunk-QYWDNIM2.js} +155 -61
- package/dist/lib.d.ts +50 -0
- package/dist/lib.js +712 -0
- package/dist/remote/start.js +179 -0
- package/dist/server-N5ZPO432.js +37 -0
- package/package.json +13 -2
- package/skills/financy-setup/SKILL.md +26 -5
|
@@ -3,12 +3,20 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "financy",
|
|
6
|
-
version: "0.
|
|
6
|
+
version: "0.2.0",
|
|
7
7
|
description: "Your Open-Finance accounts, balances and transactions in the terminal \u2014 with an embedded MCP server and agent skills",
|
|
8
8
|
type: "module",
|
|
9
9
|
bin: {
|
|
10
10
|
financy: "dist/bin.js"
|
|
11
11
|
},
|
|
12
|
+
types: "./dist/lib.d.ts",
|
|
13
|
+
exports: {
|
|
14
|
+
".": {
|
|
15
|
+
types: "./dist/lib.d.ts",
|
|
16
|
+
import: "./dist/lib.js"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
12
20
|
license: "Apache-2.0",
|
|
13
21
|
author: "Open Finance AI <hello@open-finance.ai>",
|
|
14
22
|
homepage: "https://github.com/open-finance-ai/financy#readme",
|
|
@@ -42,7 +50,9 @@ var package_default = {
|
|
|
42
50
|
node: ">=20"
|
|
43
51
|
},
|
|
44
52
|
scripts: {
|
|
53
|
+
prebuild: `node -e "require('node:fs').rmSync('dist',{recursive:true,force:true})"`,
|
|
45
54
|
build: "tsup",
|
|
55
|
+
"build:lambda": "tsup --config tsup.lambda.config.ts",
|
|
46
56
|
test: "vitest run",
|
|
47
57
|
"test:watch": "vitest",
|
|
48
58
|
typecheck: "tsc --noEmit",
|
|
@@ -52,7 +62,8 @@ var package_default = {
|
|
|
52
62
|
},
|
|
53
63
|
dependencies: {
|
|
54
64
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
55
|
-
commander: "^12.1.0"
|
|
65
|
+
commander: "^12.1.0",
|
|
66
|
+
jose: "^6.2.9"
|
|
56
67
|
},
|
|
57
68
|
devDependencies: {
|
|
58
69
|
"@eslint/js": "^10.0.1",
|
|
@@ -71,10 +82,64 @@ var package_default = {
|
|
|
71
82
|
var VERSION = package_default.version;
|
|
72
83
|
var USER_AGENT = `financy-cli/${VERSION}`;
|
|
73
84
|
|
|
85
|
+
// src/exit-codes.ts
|
|
86
|
+
var EXIT = {
|
|
87
|
+
OK: 0,
|
|
88
|
+
UNEXPECTED: 1,
|
|
89
|
+
USAGE: 2,
|
|
90
|
+
AUTH: 3,
|
|
91
|
+
PLAN: 4,
|
|
92
|
+
CREDITS: 5,
|
|
93
|
+
NOT_FOUND: 6,
|
|
94
|
+
API: 7
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// src/errors.ts
|
|
98
|
+
var CliError = class extends Error {
|
|
99
|
+
code;
|
|
100
|
+
exitCode;
|
|
101
|
+
extra;
|
|
102
|
+
constructor(exitCode, code, message, extra = {}) {
|
|
103
|
+
super(message);
|
|
104
|
+
this.name = "CliError";
|
|
105
|
+
this.code = code;
|
|
106
|
+
this.exitCode = exitCode;
|
|
107
|
+
this.extra = extra;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
var authFailed = (message = "authentication failed \u2014 check your credentials or run financy setup") => new CliError(EXIT.AUTH, "AUTH_FAILED", message);
|
|
111
|
+
var planNotEligible = () => new CliError(
|
|
112
|
+
EXIT.PLAN,
|
|
113
|
+
"NOT_AVAILABLE_ON_PLAN",
|
|
114
|
+
"the API is available on Starter and Pro plans \u2014 upgrade in Financy \u2192 Settings \u2192 Plan"
|
|
115
|
+
);
|
|
116
|
+
var forbidden = (detail) => new CliError(EXIT.PLAN, "FORBIDDEN", `the API denied this request (403): ${detail}`);
|
|
117
|
+
var insufficientCredits = (cost, balance) => new CliError(
|
|
118
|
+
EXIT.CREDITS,
|
|
119
|
+
"INSUFFICIENT_CREDITS",
|
|
120
|
+
`an initiated refresh costs ${cost} credits; you have ${balance}`,
|
|
121
|
+
{ cost, balance }
|
|
122
|
+
);
|
|
123
|
+
var noRefreshableConnections = (message = "no connections are eligible for refresh") => new CliError(EXIT.UNEXPECTED, "NO_REFRESHABLE_CONNECTIONS", message);
|
|
124
|
+
var apiUnavailable = (message = "the Open-Finance API is unavailable \u2014 check your connection and try again") => new CliError(EXIT.API, "API_UNAVAILABLE", message);
|
|
125
|
+
|
|
74
126
|
// src/config.ts
|
|
75
127
|
import { homedir } from "os";
|
|
76
128
|
import { join } from "path";
|
|
77
129
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
130
|
+
|
|
131
|
+
// src/credentials.ts
|
|
132
|
+
function isControl(ch) {
|
|
133
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
134
|
+
return code < 32 || code === 127;
|
|
135
|
+
}
|
|
136
|
+
function sanitizeCredential(raw) {
|
|
137
|
+
const stripped = Array.from(raw).filter((ch) => !isControl(ch)).join("").trim();
|
|
138
|
+
const quoted = /^(["'])(.*)\1$/.exec(stripped);
|
|
139
|
+
return (quoted?.[2] ?? stripped).trim();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/config.ts
|
|
78
143
|
var DEFAULT_AUTH_TOKEN_URL = "https://api.open-finance.ai/oauth/token";
|
|
79
144
|
var DEFAULT_API_BASE_URL = "https://api.open-finance.ai/v2";
|
|
80
145
|
var DEFAULT_CHAT_BASE_URL = "https://api.open-finance.ai/chat/chat";
|
|
@@ -94,14 +159,39 @@ function resolveEndpoints(env) {
|
|
|
94
159
|
function configFile(configDir) {
|
|
95
160
|
return join(configDir, "config.json");
|
|
96
161
|
}
|
|
162
|
+
function decodeConfigBytes(bytes) {
|
|
163
|
+
if (bytes[0] === 255 && bytes[1] === 254) return bytes.subarray(2).toString("utf16le");
|
|
164
|
+
if (bytes[0] === 254 && bytes[1] === 255) {
|
|
165
|
+
return Buffer.from(bytes.subarray(2)).swap16().toString("utf16le");
|
|
166
|
+
}
|
|
167
|
+
if (bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) {
|
|
168
|
+
return bytes.subarray(3).toString("utf8");
|
|
169
|
+
}
|
|
170
|
+
return bytes.toString("utf8");
|
|
171
|
+
}
|
|
97
172
|
async function readCredentialsFile(configDir) {
|
|
173
|
+
let bytes;
|
|
98
174
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return
|
|
175
|
+
bytes = await readFile(configFile(configDir));
|
|
176
|
+
} catch (error) {
|
|
177
|
+
const code = error.code;
|
|
178
|
+
if (code === "ENOENT") return { status: "missing" };
|
|
179
|
+
return { status: "unreadable", detail: code ?? String(error) };
|
|
180
|
+
}
|
|
181
|
+
let parsed;
|
|
182
|
+
try {
|
|
183
|
+
parsed = JSON.parse(decodeConfigBytes(bytes));
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return { status: "malformed", detail: `not valid JSON (${error.message})` };
|
|
186
|
+
}
|
|
187
|
+
const credentials = parsed?.profiles?.default;
|
|
188
|
+
if (!credentials || typeof credentials !== "object") {
|
|
189
|
+
return {
|
|
190
|
+
status: "malformed",
|
|
191
|
+
detail: 'no "profiles.default" object \u2014 expected {"profiles":{"default":{\u2026}}}'
|
|
192
|
+
};
|
|
104
193
|
}
|
|
194
|
+
return { status: "ok", credentials };
|
|
105
195
|
}
|
|
106
196
|
async function writeCredentialsFile(configDir, credentials) {
|
|
107
197
|
await mkdir(configDir, { recursive: true, mode: 448 });
|
|
@@ -111,63 +201,48 @@ async function writeCredentialsFile(configDir, credentials) {
|
|
|
111
201
|
{ mode: 384 }
|
|
112
202
|
);
|
|
113
203
|
}
|
|
204
|
+
function bearerConfig(env, bearerToken, abortSignal) {
|
|
205
|
+
return {
|
|
206
|
+
...resolveEndpoints(env),
|
|
207
|
+
clientId: "",
|
|
208
|
+
clientSecret: "",
|
|
209
|
+
userId: "",
|
|
210
|
+
bearerToken,
|
|
211
|
+
abortSignal
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function pick(envValue, fileValue) {
|
|
215
|
+
const raw = envValue ?? fileValue;
|
|
216
|
+
if (raw === void 0) return void 0;
|
|
217
|
+
const value = sanitizeCredential(raw);
|
|
218
|
+
return value === "" ? void 0 : value;
|
|
219
|
+
}
|
|
114
220
|
async function loadConfig(env) {
|
|
115
221
|
const endpoints = resolveEndpoints(env);
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
const
|
|
222
|
+
const result = await readCredentialsFile(endpoints.configDir);
|
|
223
|
+
const file = result.status === "ok" ? result.credentials : {};
|
|
224
|
+
const fileProblem = result.status === "missing" || result.status === "ok" ? void 0 : `${configFile(endpoints.configDir)} could not be read: ${result.detail}`;
|
|
225
|
+
const clientId = pick(env.FINANCY_CLIENT_ID, file.clientId);
|
|
226
|
+
const clientSecret = pick(env.FINANCY_CLIENT_SECRET, file.clientSecret);
|
|
227
|
+
const userId = pick(env.FINANCY_USER_ID, file.userId);
|
|
120
228
|
const missing = [];
|
|
121
229
|
if (!clientId) missing.push("FINANCY_CLIENT_ID");
|
|
122
230
|
if (!clientSecret) missing.push("FINANCY_CLIENT_SECRET");
|
|
123
231
|
if (!userId) missing.push("FINANCY_USER_ID");
|
|
124
|
-
if (!clientId || !clientSecret || !userId) return { ok: false, missing };
|
|
232
|
+
if (!clientId || !clientSecret || !userId) return { ok: false, missing, fileProblem };
|
|
125
233
|
return {
|
|
126
234
|
ok: true,
|
|
127
235
|
config: { clientId, clientSecret, userId, ...endpoints }
|
|
128
236
|
};
|
|
129
237
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
CREDITS: 5,
|
|
139
|
-
NOT_FOUND: 6,
|
|
140
|
-
API: 7
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
// src/errors.ts
|
|
144
|
-
var CliError = class extends Error {
|
|
145
|
-
code;
|
|
146
|
-
exitCode;
|
|
147
|
-
extra;
|
|
148
|
-
constructor(exitCode, code, message, extra = {}) {
|
|
149
|
-
super(message);
|
|
150
|
-
this.name = "CliError";
|
|
151
|
-
this.code = code;
|
|
152
|
-
this.exitCode = exitCode;
|
|
153
|
-
this.extra = extra;
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
var authFailed = (message = "authentication failed \u2014 check your credentials or run financy setup") => new CliError(EXIT.AUTH, "AUTH_FAILED", message);
|
|
157
|
-
var planNotEligible = () => new CliError(
|
|
158
|
-
EXIT.PLAN,
|
|
159
|
-
"NOT_AVAILABLE_ON_PLAN",
|
|
160
|
-
"the API is available on Starter and Pro plans \u2014 upgrade in Financy \u2192 Settings \u2192 Plan"
|
|
161
|
-
);
|
|
162
|
-
var forbidden = (detail) => new CliError(EXIT.PLAN, "FORBIDDEN", `the API denied this request (403): ${detail}`);
|
|
163
|
-
var insufficientCredits = (cost, balance) => new CliError(
|
|
164
|
-
EXIT.CREDITS,
|
|
165
|
-
"INSUFFICIENT_CREDITS",
|
|
166
|
-
`an initiated refresh costs ${cost} credits; you have ${balance}`,
|
|
167
|
-
{ cost, balance }
|
|
168
|
-
);
|
|
169
|
-
var noRefreshableConnections = (message = "no connections are eligible for refresh") => new CliError(EXIT.UNEXPECTED, "NO_REFRESHABLE_CONNECTIONS", message);
|
|
170
|
-
var apiUnavailable = (message = "the Open-Finance API is unavailable \u2014 check your connection and try again") => new CliError(EXIT.API, "API_UNAVAILABLE", message);
|
|
238
|
+
function notConfigured(result) {
|
|
239
|
+
const suffix = result.fileProblem ? ` \u2014 ${result.fileProblem}` : "";
|
|
240
|
+
return new CliError(
|
|
241
|
+
EXIT.AUTH,
|
|
242
|
+
"NOT_CONFIGURED",
|
|
243
|
+
`missing ${result.missing.join(", ")} \u2014 run financy setup${suffix}`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
171
246
|
|
|
172
247
|
// src/http.ts
|
|
173
248
|
async function httpFetch(url, init) {
|
|
@@ -258,6 +333,7 @@ async function getAccessToken(config, now) {
|
|
|
258
333
|
return mintAndCache(config);
|
|
259
334
|
}
|
|
260
335
|
async function authorized(config, now, request) {
|
|
336
|
+
if (config.bearerToken) return request(config.bearerToken);
|
|
261
337
|
const token = await getAccessToken(config, now);
|
|
262
338
|
try {
|
|
263
339
|
return await request(token);
|
|
@@ -297,7 +373,10 @@ async function getPage(config, token, path, query = {}) {
|
|
|
297
373
|
for (const [key, value] of Object.entries(query)) {
|
|
298
374
|
if (value !== void 0) url.searchParams.set(key, value);
|
|
299
375
|
}
|
|
300
|
-
const res = await httpFetch(url.toString(), {
|
|
376
|
+
const res = await httpFetch(url.toString(), {
|
|
377
|
+
headers: authHeaders(token),
|
|
378
|
+
signal: config.abortSignal
|
|
379
|
+
});
|
|
301
380
|
await ensureOk(res);
|
|
302
381
|
const raw = await res.json().catch(() => ({}));
|
|
303
382
|
debugBody(path, raw);
|
|
@@ -310,7 +389,10 @@ function normalizePage(raw) {
|
|
|
310
389
|
return { items, nextPage: obj.nextPage ?? null, count: obj.count ?? items.length };
|
|
311
390
|
}
|
|
312
391
|
async function getById(config, token, path) {
|
|
313
|
-
const res = await httpFetch(config.apiBaseUrl + path, {
|
|
392
|
+
const res = await httpFetch(config.apiBaseUrl + path, {
|
|
393
|
+
headers: authHeaders(token),
|
|
394
|
+
signal: config.abortSignal
|
|
395
|
+
});
|
|
314
396
|
if (res.status === 404) return null;
|
|
315
397
|
await ensureOk(res);
|
|
316
398
|
const body = await res.json();
|
|
@@ -321,7 +403,8 @@ async function getById(config, token, path) {
|
|
|
321
403
|
async function postRefresh(config, token) {
|
|
322
404
|
const res = await httpFetch(config.chatBaseUrl + "/connections/refresh", {
|
|
323
405
|
method: "POST",
|
|
324
|
-
headers: authHeaders(token)
|
|
406
|
+
headers: authHeaders(token),
|
|
407
|
+
signal: config.abortSignal
|
|
325
408
|
});
|
|
326
409
|
if (res.status === 401) throw authFailed();
|
|
327
410
|
if (res.status === 402) {
|
|
@@ -445,12 +528,20 @@ function fetchTransactions(config, now, args) {
|
|
|
445
528
|
!!args.all
|
|
446
529
|
);
|
|
447
530
|
}
|
|
448
|
-
|
|
531
|
+
var BARE_TRANSACTION_ID = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
|
|
532
|
+
function fetchTransaction(config, now, key) {
|
|
533
|
+
if (BARE_TRANSACTION_ID.test(key)) {
|
|
534
|
+
throw new CliError(
|
|
535
|
+
EXIT.USAGE,
|
|
536
|
+
"INVALID_ARGUMENT",
|
|
537
|
+
`'${key}' is a transaction's \`id\`, but this lookup is keyed by its \`SK\` \u2014 use the \`SK\` field from the same transaction (it looks like "TRANSACTION#TYPE#\u2026#${key}")`
|
|
538
|
+
);
|
|
539
|
+
}
|
|
449
540
|
return getOne(
|
|
450
541
|
config,
|
|
451
542
|
now,
|
|
452
|
-
`/data/transactions/${encodeURIComponent(
|
|
453
|
-
notFound("TRANSACTION_NOT_FOUND", `no transaction ${
|
|
543
|
+
`/data/transactions/${encodeURIComponent(key)}`,
|
|
544
|
+
notFound("TRANSACTION_NOT_FOUND", `no transaction ${key} in your scope`)
|
|
454
545
|
);
|
|
455
546
|
}
|
|
456
547
|
function fetchCategories(config, now) {
|
|
@@ -468,13 +559,16 @@ function fetchBranches(config, now) {
|
|
|
468
559
|
}
|
|
469
560
|
|
|
470
561
|
export {
|
|
562
|
+
sanitizeCredential,
|
|
563
|
+
EXIT,
|
|
564
|
+
CliError,
|
|
471
565
|
resolveEndpoints,
|
|
472
566
|
readCredentialsFile,
|
|
473
567
|
writeCredentialsFile,
|
|
568
|
+
bearerConfig,
|
|
474
569
|
loadConfig,
|
|
570
|
+
notConfigured,
|
|
475
571
|
VERSION,
|
|
476
|
-
EXIT,
|
|
477
|
-
CliError,
|
|
478
572
|
getConnections,
|
|
479
573
|
mintToken,
|
|
480
574
|
STALE_THRESHOLD_DAYS,
|
package/dist/lib.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
interface Credentials {
|
|
2
|
+
clientId: string;
|
|
3
|
+
clientSecret: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
}
|
|
6
|
+
interface Endpoints {
|
|
7
|
+
authTokenUrl: string;
|
|
8
|
+
apiBaseUrl: string;
|
|
9
|
+
chatBaseUrl: string;
|
|
10
|
+
configDir: string;
|
|
11
|
+
}
|
|
12
|
+
type Config = Credentials & Endpoints & {
|
|
13
|
+
bearerToken?: string;
|
|
14
|
+
abortSignal?: AbortSignal;
|
|
15
|
+
};
|
|
16
|
+
declare function bearerConfig(env: NodeJS.ProcessEnv, bearerToken: string, abortSignal?: AbortSignal): Config;
|
|
17
|
+
|
|
18
|
+
/** JSON-Schema fragment for a tool's arguments (kept plain so tests need no SDK/zod). */
|
|
19
|
+
type JsonSchema = {
|
|
20
|
+
type: 'object';
|
|
21
|
+
properties: Record<string, unknown>;
|
|
22
|
+
required?: string[];
|
|
23
|
+
};
|
|
24
|
+
interface ToolContext {
|
|
25
|
+
config: Config;
|
|
26
|
+
now: Date;
|
|
27
|
+
}
|
|
28
|
+
interface ToolDef {
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
inputSchema: JsonSchema;
|
|
32
|
+
run: (args: Record<string, unknown>, ctx: ToolContext) => Promise<unknown>;
|
|
33
|
+
}
|
|
34
|
+
declare const TOOLS: ToolDef[];
|
|
35
|
+
interface CallToolIO {
|
|
36
|
+
env: NodeJS.ProcessEnv;
|
|
37
|
+
now?: Date;
|
|
38
|
+
config?: Config;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Dispatch a tool by name: resolve config (unconfigured → the structured
|
|
42
|
+
* NOT_CONFIGURED error, on every tool), run it, and return its envelope. Errors
|
|
43
|
+
* are returned as the `{error:{code,message}}` envelope rather than thrown, so
|
|
44
|
+
* the server surfaces them to the agent as machine-readable tool output.
|
|
45
|
+
*/
|
|
46
|
+
declare function callTool(name: string, args: Record<string, unknown>, io: CallToolIO): Promise<unknown>;
|
|
47
|
+
|
|
48
|
+
declare const VERSION: string;
|
|
49
|
+
|
|
50
|
+
export { type CallToolIO, type Config, type JsonSchema, TOOLS, type ToolContext, type ToolDef, VERSION, bearerConfig, callTool };
|