financy 0.3.0 → 0.4.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 +165 -1
- package/dist/bin.js +219 -7
- package/dist/{chunk-VE5WZRGU.js → chunk-LZSZIBYL.js} +239 -5
- package/dist/lib.d.ts +1 -0
- package/dist/lib.js +20 -5
- package/dist/server-WGN36UXM.js +459 -0
- package/package.json +2 -2
- package/skills/financy-setup/SKILL.md +33 -4
- package/dist/server-RM4BAVES.js +0 -207
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "financy",
|
|
6
|
-
version: "0.
|
|
6
|
+
version: "0.4.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: {
|
|
@@ -109,7 +109,7 @@ var authFailed = (message = "authentication failed \u2014 check your credentials
|
|
|
109
109
|
var planNotEligible = () => new CliError(
|
|
110
110
|
EXIT.PLAN,
|
|
111
111
|
"NOT_AVAILABLE_ON_PLAN",
|
|
112
|
-
"the API is available on Starter and
|
|
112
|
+
"the API is available on every paid plan, including Starter, Pro, and Ultra \u2014 upgrade in Financy \u2192 Settings \u2192 Plan"
|
|
113
113
|
);
|
|
114
114
|
var forbidden = (detail) => new CliError(EXIT.PLAN, "FORBIDDEN", `the API denied this request (403): ${detail}`);
|
|
115
115
|
var insufficientCredits = (cost, balance) => new CliError(
|
|
@@ -120,6 +120,29 @@ var insufficientCredits = (cost, balance) => new CliError(
|
|
|
120
120
|
);
|
|
121
121
|
var noRefreshableConnections = (message = "no connections are eligible for refresh") => new CliError(EXIT.UNEXPECTED, "NO_REFRESHABLE_CONNECTIONS", message);
|
|
122
122
|
var apiUnavailable = (message = "the Open-Finance API is unavailable \u2014 check your connection and try again") => new CliError(EXIT.API, "API_UNAVAILABLE", message);
|
|
123
|
+
var PLAN_TYPES = /* @__PURE__ */ new Set([
|
|
124
|
+
"UPGRADE_REQUIRED",
|
|
125
|
+
"INTEGRATION_LIMIT_REACHED",
|
|
126
|
+
"NOT_ENTITLED",
|
|
127
|
+
"ENTITLEMENT_NOT_PROVISIONED"
|
|
128
|
+
]);
|
|
129
|
+
function integrationsExitCode(type, status) {
|
|
130
|
+
if (type === "SIGN_IN_AGAIN") return EXIT.AUTH;
|
|
131
|
+
if (PLAN_TYPES.has(type) || status === 403) return EXIT.PLAN;
|
|
132
|
+
if (status === 404) return EXIT.NOT_FOUND;
|
|
133
|
+
if (status === 400 || status === 422) return EXIT.USAGE;
|
|
134
|
+
if (status === 429 || status >= 500) return EXIT.API;
|
|
135
|
+
return EXIT.UNEXPECTED;
|
|
136
|
+
}
|
|
137
|
+
function integrationsError(body, status) {
|
|
138
|
+
const hint = body.action?.url ? ` \u2014 ${body.action.url}` : body.type === "UPGRADE_REQUIRED" ? " \u2014 upgrade in Financy \u2192 Settings \u2192 Plan" : "";
|
|
139
|
+
return new CliError(integrationsExitCode(body.type, status), body.type, `${body.message}${hint}`, {
|
|
140
|
+
retryable: body.retryable === true,
|
|
141
|
+
userActionRequired: body.userActionRequired === true,
|
|
142
|
+
...body.action ? { action: body.action } : {},
|
|
143
|
+
...body.requestId ? { requestId: body.requestId } : {}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
123
146
|
|
|
124
147
|
// src/config.ts
|
|
125
148
|
import { homedir } from "os";
|
|
@@ -141,6 +164,16 @@ function sanitizeCredential(raw) {
|
|
|
141
164
|
var DEFAULT_AUTH_TOKEN_URL = "https://api.open-finance.ai/oauth/token";
|
|
142
165
|
var DEFAULT_API_BASE_URL = "https://api.open-finance.ai/v2";
|
|
143
166
|
var DEFAULT_CHAT_BASE_URL = "https://api.open-finance.ai/chat/chat";
|
|
167
|
+
var DEFAULT_INTEGRATIONS_BASE_URL = "https://api.open-finance.ai/integrations";
|
|
168
|
+
function resolveIntegrationsBaseUrl(env) {
|
|
169
|
+
if (env.FINANCY_INTEGRATIONS_URL) return env.FINANCY_INTEGRATIONS_URL;
|
|
170
|
+
if (!env.FINANCY_API_URL) return DEFAULT_INTEGRATIONS_BASE_URL;
|
|
171
|
+
try {
|
|
172
|
+
return `${new URL(env.FINANCY_API_URL).origin}/integrations`;
|
|
173
|
+
} catch {
|
|
174
|
+
return DEFAULT_INTEGRATIONS_BASE_URL;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
144
177
|
function resolveConfigDir(env) {
|
|
145
178
|
if (env.FINANCY_CONFIG_DIR) return env.FINANCY_CONFIG_DIR;
|
|
146
179
|
const base = env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
@@ -151,6 +184,7 @@ function resolveEndpoints(env) {
|
|
|
151
184
|
authTokenUrl: env.FINANCY_AUTH_URL ?? DEFAULT_AUTH_TOKEN_URL,
|
|
152
185
|
apiBaseUrl: env.FINANCY_API_URL ?? DEFAULT_API_BASE_URL,
|
|
153
186
|
chatBaseUrl: env.FINANCY_CHAT_URL ?? DEFAULT_CHAT_BASE_URL,
|
|
187
|
+
integrationsBaseUrl: resolveIntegrationsBaseUrl(env),
|
|
154
188
|
configDir: resolveConfigDir(env)
|
|
155
189
|
};
|
|
156
190
|
}
|
|
@@ -320,13 +354,14 @@ async function getAccessToken(config, now) {
|
|
|
320
354
|
}
|
|
321
355
|
return mintAndCache(config);
|
|
322
356
|
}
|
|
323
|
-
|
|
357
|
+
var isAuthFailure = (error) => error.code === "AUTH_FAILED";
|
|
358
|
+
async function authorized(config, now, request, remintOn = isAuthFailure) {
|
|
324
359
|
if (config.bearerToken) return request(config.bearerToken);
|
|
325
360
|
const token = await getAccessToken(config, now);
|
|
326
361
|
try {
|
|
327
362
|
return await request(token);
|
|
328
363
|
} catch (error) {
|
|
329
|
-
if (error instanceof CliError && error
|
|
364
|
+
if (error instanceof CliError && remintOn(error)) {
|
|
330
365
|
return await request(await mintAndCache(config));
|
|
331
366
|
}
|
|
332
367
|
throw error;
|
|
@@ -410,6 +445,195 @@ async function getConnections(config, token) {
|
|
|
410
445
|
return getPage(config, token, "/connections");
|
|
411
446
|
}
|
|
412
447
|
|
|
448
|
+
// src/integrations-api.ts
|
|
449
|
+
function isApiError(value) {
|
|
450
|
+
return value !== null && typeof value === "object" && typeof value.type === "string" && typeof value.message === "string";
|
|
451
|
+
}
|
|
452
|
+
function integrationsUrl(config, path, query) {
|
|
453
|
+
const url = new URL(config.integrationsBaseUrl.replace(/\/+$/, "") + path);
|
|
454
|
+
for (const [key, value] of Object.entries(query)) {
|
|
455
|
+
if (value !== void 0) url.searchParams.set(key, value);
|
|
456
|
+
}
|
|
457
|
+
return url.toString();
|
|
458
|
+
}
|
|
459
|
+
async function send(config, token, method, path, options = {}) {
|
|
460
|
+
const res = await httpFetch(integrationsUrl(config, path, options.query ?? {}), {
|
|
461
|
+
method,
|
|
462
|
+
headers: {
|
|
463
|
+
authorization: `Bearer ${token}`,
|
|
464
|
+
accept: "application/json",
|
|
465
|
+
"user-agent": USER_AGENT,
|
|
466
|
+
...options.body === void 0 ? {} : { "content-type": "application/json" },
|
|
467
|
+
...options.headers
|
|
468
|
+
},
|
|
469
|
+
...options.body === void 0 ? {} : { body: JSON.stringify(options.body) },
|
|
470
|
+
signal: config.abortSignal
|
|
471
|
+
});
|
|
472
|
+
const parsed = await res.json().catch(() => void 0);
|
|
473
|
+
if (res.ok) {
|
|
474
|
+
debugBody(path, parsed);
|
|
475
|
+
return { body: parsed ?? {}, replayed: res.headers.get("idempotent-replayed") === "true" };
|
|
476
|
+
}
|
|
477
|
+
if (isApiError(parsed)) throw integrationsError(parsed, res.status);
|
|
478
|
+
if (res.status === 401) throw authFailed();
|
|
479
|
+
throw apiUnavailable(`the integrations API returned HTTP ${res.status}`);
|
|
480
|
+
}
|
|
481
|
+
async function integrationsGet(config, token, path, query = {}) {
|
|
482
|
+
return (await send(config, token, "GET", path, { query })).body;
|
|
483
|
+
}
|
|
484
|
+
async function integrationsPost(config, token, path, body, headers = {}) {
|
|
485
|
+
return send(config, token, "POST", path, { body, headers });
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// src/core/integrations.ts
|
|
489
|
+
var PATH_SEGMENT = /^[A-Za-z0-9_-]{1,128}$/;
|
|
490
|
+
function segment(name, value) {
|
|
491
|
+
if (!PATH_SEGMENT.test(value)) {
|
|
492
|
+
throw new CliError(EXIT.USAGE, "INVALID_ARGUMENT", `'${value}' is not a valid ${name}`);
|
|
493
|
+
}
|
|
494
|
+
return value;
|
|
495
|
+
}
|
|
496
|
+
var isScopeError = (error) => error.code === "INSUFFICIENT_SCOPE";
|
|
497
|
+
async function integrationsAuthorized(config, now, request) {
|
|
498
|
+
try {
|
|
499
|
+
return await authorized(config, now, request, (e) => e.code === "AUTH_FAILED" || isScopeError(e));
|
|
500
|
+
} catch (error) {
|
|
501
|
+
if (error instanceof CliError && isScopeError(error)) {
|
|
502
|
+
throw new CliError(
|
|
503
|
+
EXIT.PLAN,
|
|
504
|
+
"INSUFFICIENT_SCOPE",
|
|
505
|
+
"this API key doesn't have integration access yet \u2014 integrations need a Starter plan or above, and the key may need refreshing: rotate it in Financy \u2192 Settings \u2192 API, then run financy setup",
|
|
506
|
+
error.extra
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
throw error;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
var get = (config, now, path, query = {}) => integrationsAuthorized(config, now, (token) => integrationsGet(config, token, path, query));
|
|
513
|
+
async function cursorList(config, now, path, query, all) {
|
|
514
|
+
const data = [];
|
|
515
|
+
let cursor = query.cursor;
|
|
516
|
+
let freshness;
|
|
517
|
+
do {
|
|
518
|
+
const page = await get(config, now, path, { ...query, cursor });
|
|
519
|
+
data.push(...Array.isArray(page.items) ? page.items : []);
|
|
520
|
+
freshness = page.freshness;
|
|
521
|
+
cursor = page.nextCursor ?? void 0;
|
|
522
|
+
} while (all && cursor);
|
|
523
|
+
return {
|
|
524
|
+
data,
|
|
525
|
+
count: data.length,
|
|
526
|
+
nextPage: all ? null : cursor ?? null,
|
|
527
|
+
...freshness === void 0 ? {} : { freshness }
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function fetchIntegrations(config, now) {
|
|
531
|
+
return cursorList(config, now, "/", {}, false);
|
|
532
|
+
}
|
|
533
|
+
async function fetchIntegration(config, now, id) {
|
|
534
|
+
const body = await get(config, now, `/${segment("integration id", id)}`);
|
|
535
|
+
return { data: body.integration };
|
|
536
|
+
}
|
|
537
|
+
async function fetchReceivables(config, now, args) {
|
|
538
|
+
const ids = (args.integrations ?? []).flatMap((v) => v.split(",")).filter((v) => v.length > 0);
|
|
539
|
+
const body = await get(config, now, "/receivables", {
|
|
540
|
+
integrationIds: ids.length > 0 ? ids.map((id) => segment("integration id", id)).join(",") : void 0,
|
|
541
|
+
asOf: args.asOf,
|
|
542
|
+
format: args.detailed ? "detailed" : void 0,
|
|
543
|
+
customerLimit: args.customerLimit,
|
|
544
|
+
includeProforma: args.includeProforma === void 0 ? void 0 : String(args.includeProforma)
|
|
545
|
+
});
|
|
546
|
+
return { data: body };
|
|
547
|
+
}
|
|
548
|
+
function fetchInvoicingDocuments(config, now, integrationId, args) {
|
|
549
|
+
return cursorList(
|
|
550
|
+
config,
|
|
551
|
+
now,
|
|
552
|
+
`/${segment("integration id", integrationId)}/documents`,
|
|
553
|
+
{
|
|
554
|
+
status: args.status,
|
|
555
|
+
type: args.type,
|
|
556
|
+
counterpartyId: args.customer,
|
|
557
|
+
issuedFrom: args.from,
|
|
558
|
+
issuedTo: args.to,
|
|
559
|
+
receivableOnly: args.receivableOnly ? "true" : void 0,
|
|
560
|
+
number: args.number,
|
|
561
|
+
limit: args.limit,
|
|
562
|
+
cursor: args.cursor
|
|
563
|
+
},
|
|
564
|
+
!!args.all
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
async function fetchInvoicingDocument(config, now, integrationId, documentId) {
|
|
568
|
+
const body = await get(
|
|
569
|
+
config,
|
|
570
|
+
now,
|
|
571
|
+
`/${segment("integration id", integrationId)}/documents/${segment("document id", documentId)}`
|
|
572
|
+
);
|
|
573
|
+
return { data: body.document, ...body.freshness === void 0 ? {} : { freshness: body.freshness } };
|
|
574
|
+
}
|
|
575
|
+
function fetchInvoicingCustomers(config, now, integrationId, args) {
|
|
576
|
+
return cursorList(
|
|
577
|
+
config,
|
|
578
|
+
now,
|
|
579
|
+
`/${segment("integration id", integrationId)}/customers`,
|
|
580
|
+
{ q: args.search, limit: args.limit, cursor: args.cursor },
|
|
581
|
+
!!args.all
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
async function fetchInvoicingCustomer(config, now, integrationId, customerId) {
|
|
585
|
+
const body = await get(
|
|
586
|
+
config,
|
|
587
|
+
now,
|
|
588
|
+
`/${segment("integration id", integrationId)}/customers/${segment("customer id", customerId)}`
|
|
589
|
+
);
|
|
590
|
+
return { data: body.customer, ...body.freshness === void 0 ? {} : { freshness: body.freshness } };
|
|
591
|
+
}
|
|
592
|
+
function fetchProposals(config, now, args) {
|
|
593
|
+
return cursorList(
|
|
594
|
+
config,
|
|
595
|
+
now,
|
|
596
|
+
"/proposals",
|
|
597
|
+
{
|
|
598
|
+
integrationId: args.integration === void 0 ? void 0 : segment("integration id", args.integration),
|
|
599
|
+
status: args.status,
|
|
600
|
+
clientReference: args.reference,
|
|
601
|
+
limit: args.limit
|
|
602
|
+
},
|
|
603
|
+
false
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
async function fetchProposal(config, now, id) {
|
|
607
|
+
const body = await get(config, now, `/proposals/${segment("proposal id", id)}`);
|
|
608
|
+
return { data: body.proposal };
|
|
609
|
+
}
|
|
610
|
+
function assertDraftBody(body) {
|
|
611
|
+
const obj = body;
|
|
612
|
+
if (body === null || typeof body !== "object" || Array.isArray(body) || typeof obj.action !== "string" || !Array.isArray(obj.items)) {
|
|
613
|
+
throw new CliError(
|
|
614
|
+
EXIT.USAGE,
|
|
615
|
+
"INVALID_DRAFT",
|
|
616
|
+
'a draft is a JSON object with an "action" (e.g. "create_tax_invoice") and an "items" array'
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
return body;
|
|
620
|
+
}
|
|
621
|
+
async function createDraft(config, now, integrationId, body, idempotencyKey) {
|
|
622
|
+
const draft = assertDraftBody(body);
|
|
623
|
+
const path = `/${segment("integration id", integrationId)}/proposals`;
|
|
624
|
+
const headers = idempotencyKey ? { "idempotency-key": idempotencyKey } : {};
|
|
625
|
+
const res = await integrationsAuthorized(
|
|
626
|
+
config,
|
|
627
|
+
now,
|
|
628
|
+
(token) => integrationsPost(config, token, path, draft, headers)
|
|
629
|
+
);
|
|
630
|
+
return {
|
|
631
|
+
data: res.body.proposal,
|
|
632
|
+
replayed: res.replayed,
|
|
633
|
+
...idempotencyKey ? { idempotencyKey } : {}
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
413
637
|
// src/core/status.ts
|
|
414
638
|
var STALE_THRESHOLD_DAYS = 2;
|
|
415
639
|
function toRow(c) {
|
|
@@ -569,5 +793,15 @@ export {
|
|
|
569
793
|
fetchTransaction,
|
|
570
794
|
fetchCategories,
|
|
571
795
|
fetchProviders,
|
|
572
|
-
fetchBranches
|
|
796
|
+
fetchBranches,
|
|
797
|
+
fetchIntegrations,
|
|
798
|
+
fetchIntegration,
|
|
799
|
+
fetchReceivables,
|
|
800
|
+
fetchInvoicingDocuments,
|
|
801
|
+
fetchInvoicingDocument,
|
|
802
|
+
fetchInvoicingCustomers,
|
|
803
|
+
fetchInvoicingCustomer,
|
|
804
|
+
fetchProposals,
|
|
805
|
+
fetchProposal,
|
|
806
|
+
createDraft
|
|
573
807
|
};
|
package/dist/lib.d.ts
CHANGED
package/dist/lib.js
CHANGED
|
@@ -43,7 +43,7 @@ var authFailed = (message = "authentication failed \u2014 check your credentials
|
|
|
43
43
|
var planNotEligible = () => new CliError(
|
|
44
44
|
EXIT.PLAN,
|
|
45
45
|
"NOT_AVAILABLE_ON_PLAN",
|
|
46
|
-
"the API is available on Starter and
|
|
46
|
+
"the API is available on every paid plan, including Starter, Pro, and Ultra \u2014 upgrade in Financy \u2192 Settings \u2192 Plan"
|
|
47
47
|
);
|
|
48
48
|
var forbidden = (detail) => new CliError(EXIT.PLAN, "FORBIDDEN", `the API denied this request (403): ${detail}`);
|
|
49
49
|
var insufficientCredits = (cost, balance) => new CliError(
|
|
@@ -59,6 +59,16 @@ var apiUnavailable = (message = "the Open-Finance API is unavailable \u2014 chec
|
|
|
59
59
|
var DEFAULT_AUTH_TOKEN_URL = "https://api.open-finance.ai/oauth/token";
|
|
60
60
|
var DEFAULT_API_BASE_URL = "https://api.open-finance.ai/v2";
|
|
61
61
|
var DEFAULT_CHAT_BASE_URL = "https://api.open-finance.ai/chat/chat";
|
|
62
|
+
var DEFAULT_INTEGRATIONS_BASE_URL = "https://api.open-finance.ai/integrations";
|
|
63
|
+
function resolveIntegrationsBaseUrl(env) {
|
|
64
|
+
if (env.FINANCY_INTEGRATIONS_URL) return env.FINANCY_INTEGRATIONS_URL;
|
|
65
|
+
if (!env.FINANCY_API_URL) return DEFAULT_INTEGRATIONS_BASE_URL;
|
|
66
|
+
try {
|
|
67
|
+
return `${new URL(env.FINANCY_API_URL).origin}/integrations`;
|
|
68
|
+
} catch {
|
|
69
|
+
return DEFAULT_INTEGRATIONS_BASE_URL;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
62
72
|
function resolveConfigDir(env) {
|
|
63
73
|
if (env.FINANCY_CONFIG_DIR) return env.FINANCY_CONFIG_DIR;
|
|
64
74
|
const base = env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
@@ -69,6 +79,7 @@ function resolveEndpoints(env) {
|
|
|
69
79
|
authTokenUrl: env.FINANCY_AUTH_URL ?? DEFAULT_AUTH_TOKEN_URL,
|
|
70
80
|
apiBaseUrl: env.FINANCY_API_URL ?? DEFAULT_API_BASE_URL,
|
|
71
81
|
chatBaseUrl: env.FINANCY_CHAT_URL ?? DEFAULT_CHAT_BASE_URL,
|
|
82
|
+
integrationsBaseUrl: resolveIntegrationsBaseUrl(env),
|
|
72
83
|
configDir: resolveConfigDir(env)
|
|
73
84
|
};
|
|
74
85
|
}
|
|
@@ -155,7 +166,7 @@ function notConfigured(result) {
|
|
|
155
166
|
// package.json
|
|
156
167
|
var package_default = {
|
|
157
168
|
name: "financy",
|
|
158
|
-
version: "0.
|
|
169
|
+
version: "0.4.0",
|
|
159
170
|
description: "Your Open-Finance accounts, balances and transactions in the terminal \u2014 with an embedded MCP server and agent skills",
|
|
160
171
|
type: "module",
|
|
161
172
|
bin: {
|
|
@@ -320,13 +331,14 @@ async function getAccessToken(config, now) {
|
|
|
320
331
|
}
|
|
321
332
|
return mintAndCache(config);
|
|
322
333
|
}
|
|
323
|
-
|
|
334
|
+
var isAuthFailure = (error) => error.code === "AUTH_FAILED";
|
|
335
|
+
async function authorized(config, now, request, remintOn = isAuthFailure) {
|
|
324
336
|
if (config.bearerToken) return request(config.bearerToken);
|
|
325
337
|
const token = await getAccessToken(config, now);
|
|
326
338
|
try {
|
|
327
339
|
return await request(token);
|
|
328
340
|
} catch (error) {
|
|
329
|
-
if (error instanceof CliError && error
|
|
341
|
+
if (error instanceof CliError && remintOn(error)) {
|
|
330
342
|
return await request(await mintAndCache(config));
|
|
331
343
|
}
|
|
332
344
|
throw error;
|
|
@@ -686,8 +698,11 @@ function errorEnvelope(error) {
|
|
|
686
698
|
return { error: { code: e.code, message: e.message, ...e.extra } };
|
|
687
699
|
}
|
|
688
700
|
async function callTool(name, args, io) {
|
|
701
|
+
return dispatchTool(BY_NAME, name, args, io);
|
|
702
|
+
}
|
|
703
|
+
async function dispatchTool(byName, name, args, io) {
|
|
689
704
|
try {
|
|
690
|
-
const tool =
|
|
705
|
+
const tool = byName.get(name);
|
|
691
706
|
if (!tool) {
|
|
692
707
|
throw new CliError(EXIT.USAGE, "UNKNOWN_TOOL", `no tool named '${name}'`);
|
|
693
708
|
}
|