@fargowealth/fargo-ai-cli 1.3.0 → 1.4.1
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/CHANGELOG.md +22 -0
- package/README.md +17 -0
- package/node_modules/@fargowealth/website-connector-runtime/README.md +15 -0
- package/node_modules/@fargowealth/website-connector-runtime/package.json +24 -0
- package/node_modules/@fargowealth/website-connector-runtime/src/index.d.mts +136 -0
- package/node_modules/@fargowealth/website-connector-runtime/src/index.mjs +825 -0
- package/package.json +5 -1
- package/src/constants.mjs +1 -1
- package/src/website-browser-profile.mjs +50 -7
- package/src/website-cli.mjs +81 -6
- package/src/website-connector-definition.d.mts +1 -58
- package/src/website-connector-definition.mjs +1 -339
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fargowealth/fargo-ai-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Agent-neutral Fargo AI CLI, MCP client, and LEGO project tooling",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,9 +17,13 @@
|
|
|
17
17
|
"node": ">=20.19.0"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"@fargowealth/website-connector-runtime": "^1.1.0",
|
|
20
21
|
"fflate": "^0.8.2",
|
|
21
22
|
"ignore": "^7.0.6"
|
|
22
23
|
},
|
|
24
|
+
"bundleDependencies": [
|
|
25
|
+
"@fargowealth/website-connector-runtime"
|
|
26
|
+
],
|
|
23
27
|
"license": "UNLICENSED",
|
|
24
28
|
"publishConfig": {
|
|
25
29
|
"access": "public"
|
package/src/constants.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.
|
|
1
|
+
export const CLI_VERSION = "1.4.1";
|
|
2
2
|
export const DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
3
3
|
export const DEFAULT_AUTH_URL = "https://auth.fargowealth.app";
|
|
4
4
|
export const DEFAULT_API_URL = "https://mcp.fargowealth.app";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { access, chmod, mkdir, readFile, unlink } from "node:fs/promises";
|
|
2
|
+
import { access, chmod, mkdir, readFile, readdir, unlink } from "node:fs/promises";
|
|
3
3
|
import { homedir, platform } from "node:os";
|
|
4
4
|
import { delimiter, join, resolve } from "node:path";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
@@ -52,7 +52,7 @@ export function buildWebsiteBrowserProfile(spec, state, capturedAt = new Date())
|
|
|
52
52
|
? array(state.cookies).filter((cookie) => cookieAllowed(cookie.domain, allowedHosts)).map(normalizeCookie)
|
|
53
53
|
: [];
|
|
54
54
|
const suffixesByOrigin = new Map();
|
|
55
|
-
for (const source of captureSpec.auth.headerSources) {
|
|
55
|
+
for (const source of [...captureSpec.auth.headerSources, ...captureSpec.auth.secretSources]) {
|
|
56
56
|
const suffixes = suffixesByOrigin.get(source.origin) ?? new Set();
|
|
57
57
|
suffixes.add(source.keySuffix);
|
|
58
58
|
suffixesByOrigin.set(source.origin, suffixes);
|
|
@@ -91,6 +91,34 @@ export function defaultLocalWebsiteProfileDirectory(connectorId, options = {}) {
|
|
|
91
91
|
return join(options.xdgConfigHome ?? process.env.XDG_CONFIG_HOME ?? join(homeDirectory, ".config"), "fargo", "website-profiles", safeId, profileKey);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
export async function resolveLocalWebsiteProfileDirectory(connectorId, options = {}) {
|
|
95
|
+
if (options.profileDirectory) return resolve(options.profileDirectory);
|
|
96
|
+
const fallback = defaultLocalWebsiteProfileDirectory(connectorId, options);
|
|
97
|
+
const connectorDirectory = resolve(fallback, "..");
|
|
98
|
+
let entries;
|
|
99
|
+
try {
|
|
100
|
+
entries = await readdir(connectorDirectory, { withFileTypes: true });
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error?.code === "ENOENT") return fallback;
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
const connectionProfiles = entries
|
|
106
|
+
.filter((entry) => entry.isDirectory() && /^bgc_[A-Za-z0-9_-]{43}$/.test(entry.name))
|
|
107
|
+
.map((entry) => join(connectorDirectory, entry.name));
|
|
108
|
+
if (connectionProfiles.length === 0) return fallback;
|
|
109
|
+
if (connectionProfiles.length === 1) return connectionProfiles[0];
|
|
110
|
+
throw new Error(`Multiple local Profiles exist for ${connectorId}; pass --profile-dir <path>`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function documentIsReadyAtOrigin(value, expectedOrigin) {
|
|
114
|
+
if (!["interactive", "complete"].includes(value?.readyState)) return false;
|
|
115
|
+
try {
|
|
116
|
+
return new URL(value.url).origin === expectedOrigin;
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
94
122
|
export function parseCaptureSpec(value) {
|
|
95
123
|
const source = record(value, "Website Profile capture spec is invalid");
|
|
96
124
|
if (source.profileSchemaVersion !== WEBSITE_BROWSER_PROFILE_SCHEMA_VERSION) throw new Error("Website Profile schema is not supported");
|
|
@@ -117,11 +145,21 @@ export function parseCaptureSpec(value) {
|
|
|
117
145
|
}
|
|
118
146
|
return { keySuffix, origin };
|
|
119
147
|
});
|
|
148
|
+
const secretSources = array(auth.secretSources).map((item) => {
|
|
149
|
+
const binding = record(item, "Website Profile secret selector is invalid");
|
|
150
|
+
const origin = exactHttpsOrigin(binding.origin);
|
|
151
|
+
const keySuffix = String(binding.keySuffix || "");
|
|
152
|
+
if (!allowedOrigins.includes(origin) || !keySuffix || keySuffix.length > 240) {
|
|
153
|
+
throw new Error("Website Profile secret selector is invalid");
|
|
154
|
+
}
|
|
155
|
+
return { keySuffix, origin };
|
|
156
|
+
});
|
|
120
157
|
if (headerSources.length > 12) throw new Error("Website Profile storage selector is invalid");
|
|
158
|
+
if (secretSources.length > 12) throw new Error("Website Profile secret selector is invalid");
|
|
121
159
|
if (auth.sendCookies !== true && headerSources.length === 0) throw new Error("Website Profile authentication recipe is empty");
|
|
122
160
|
return {
|
|
123
161
|
allowedOrigins,
|
|
124
|
-
auth: { headerSources, sendCookies: auth.sendCookies === true },
|
|
162
|
+
auth: { headerSources, secretSources, sendCookies: auth.sendCookies === true },
|
|
125
163
|
connectorId,
|
|
126
164
|
...(localProfileKey ? { localProfileKey } : {}),
|
|
127
165
|
loginUrl: loginUrl.toString(),
|
|
@@ -132,7 +170,9 @@ export function parseCaptureSpec(value) {
|
|
|
132
170
|
async function originStorage(client, sessionId, spec) {
|
|
133
171
|
await client.call("Page.enable", {}, sessionId);
|
|
134
172
|
await client.call("Runtime.enable", {}, sessionId);
|
|
135
|
-
const origins = [...new Set(
|
|
173
|
+
const origins = [...new Set(
|
|
174
|
+
[...spec.auth.headerSources, ...spec.auth.secretSources].map((source) => source.origin)
|
|
175
|
+
)];
|
|
136
176
|
const result = [];
|
|
137
177
|
for (const origin of origins) {
|
|
138
178
|
await client.call("Page.navigate", { url: `${origin}/` }, sessionId);
|
|
@@ -174,7 +214,7 @@ async function waitForDocument(client, sessionId, expectedOrigin) {
|
|
|
174
214
|
returnByValue: true
|
|
175
215
|
}, sessionId).catch(() => ({}));
|
|
176
216
|
const value = result?.result?.value;
|
|
177
|
-
if (
|
|
217
|
+
if (documentIsReadyAtOrigin(value, expectedOrigin)) return;
|
|
178
218
|
await delay(100);
|
|
179
219
|
}
|
|
180
220
|
throw new Error(`Timed out while reading local storage for ${expectedOrigin}`);
|
|
@@ -185,7 +225,10 @@ async function waitForLoginConfirmation(spec, options) {
|
|
|
185
225
|
const input = options.stdin ?? process.stdin;
|
|
186
226
|
const output = options.stderr ?? process.stderr;
|
|
187
227
|
if (!input.isTTY) throw new Error("Interactive login requires a TTY; use --no-prompt only for an already authenticated local Profile");
|
|
188
|
-
|
|
228
|
+
const nextStep = options.purpose === "local-execution"
|
|
229
|
+
? "Complete login, then press Enter here to run locally; nothing will be uploaded."
|
|
230
|
+
: "Complete login, then press Enter here to upload only the allowlisted session state.";
|
|
231
|
+
output.write(`Fargo opened an isolated local Chrome Profile for ${spec.connectorId}.\n${nextStep}\n`);
|
|
189
232
|
const prompt = createInterface({ input, output });
|
|
190
233
|
try {
|
|
191
234
|
await prompt.question("");
|
|
@@ -311,7 +354,7 @@ function normalizeCookie(cookie) {
|
|
|
311
354
|
|
|
312
355
|
function cookieAllowed(domain, allowedHosts) {
|
|
313
356
|
const host = String(domain || "").toLowerCase().replace(/^\./, "");
|
|
314
|
-
return allowedHosts.some((allowed) =>
|
|
357
|
+
return allowedHosts.some((allowed) => allowed === host || allowed.endsWith(`.${host}`));
|
|
315
358
|
}
|
|
316
359
|
|
|
317
360
|
function exactHttpsOrigin(value) {
|
package/src/website-cli.mjs
CHANGED
|
@@ -3,23 +3,33 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { callMcpTool } from "./mcp-client.mjs";
|
|
5
5
|
import { mcpToolPayload } from "./mcp-result.mjs";
|
|
6
|
-
import { captureLocalWebsiteProfile } from "./website-browser-profile.mjs";
|
|
7
|
-
import {
|
|
6
|
+
import { captureLocalWebsiteProfile, resolveLocalWebsiteProfileDirectory } from "./website-browser-profile.mjs";
|
|
7
|
+
import {
|
|
8
|
+
assertWebsiteConnectorRuntimeCompatibility,
|
|
9
|
+
executeWebsiteConnectorOperation,
|
|
10
|
+
parseWebsiteConnectorDefinition,
|
|
11
|
+
websiteConnectorRuntimeDescriptor
|
|
12
|
+
} from "./website-connector-definition.mjs";
|
|
8
13
|
|
|
9
14
|
const WEBSITE_BRIDGE_APP_ID = "website-bridge";
|
|
10
15
|
const WEBSITE_BRIDGE_URL = "https://website-bridge.fargowealth.app";
|
|
11
16
|
|
|
12
17
|
export async function runWebsiteCommand(args, stdout, store, options = {}) {
|
|
13
18
|
const action = args[1];
|
|
14
|
-
if (action === "
|
|
19
|
+
if (action === "help" || args.includes("--help") || args.includes("-h")) {
|
|
15
20
|
stdout.write(WEBSITE_HELP);
|
|
16
21
|
return;
|
|
17
22
|
}
|
|
18
23
|
if (action === "validate") {
|
|
19
24
|
const definition = await readDefinition(args);
|
|
25
|
+
assertWebsiteConnectorRuntimeCompatibility(definition);
|
|
20
26
|
stdout.write(`${JSON.stringify(validationSummary(definition))}\n`);
|
|
21
27
|
return;
|
|
22
28
|
}
|
|
29
|
+
if (action === "runtime") {
|
|
30
|
+
stdout.write(`${JSON.stringify(websiteConnectorRuntimeDescriptor())}\n`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
23
33
|
if (action === "release") {
|
|
24
34
|
const definition = await readDefinition(args);
|
|
25
35
|
const expectedRevision = optionalInteger(args, "--expected-revision");
|
|
@@ -41,13 +51,50 @@ export async function runWebsiteCommand(args, stdout, store, options = {}) {
|
|
|
41
51
|
return;
|
|
42
52
|
}
|
|
43
53
|
if (action === "call") {
|
|
54
|
+
const mode = websiteCallMode(args);
|
|
44
55
|
const write = args.includes("--write");
|
|
56
|
+
if (mode === "local") {
|
|
57
|
+
const definition = await readDefinition(args);
|
|
58
|
+
const connectorId = optionValue(args, "--connector")?.trim();
|
|
59
|
+
if (connectorId && connectorId !== definition.connectorId) {
|
|
60
|
+
throw new Error("--connector must match the Connector definition in --file");
|
|
61
|
+
}
|
|
62
|
+
const operationId = requiredOption(args, "--operation");
|
|
63
|
+
const operation = definition.operations.find((candidate) => candidate.operationId === operationId);
|
|
64
|
+
if (!operation) throw new Error("website_operation_not_found");
|
|
65
|
+
if (!operation.readOnly && (!write || !args.includes("--confirm"))) {
|
|
66
|
+
throw new Error("Local write operations require --write --confirm");
|
|
67
|
+
}
|
|
68
|
+
let storageState;
|
|
69
|
+
if (operation.requiresConnection) {
|
|
70
|
+
const capture = options.captureWebsiteProfile ?? captureLocalWebsiteProfile;
|
|
71
|
+
storageState = await capture(localProfileSpec(definition), {
|
|
72
|
+
chromePath: optionValue(args, "--chrome-path"),
|
|
73
|
+
noPrompt: args.includes("--no-prompt"),
|
|
74
|
+
profileDirectory: await resolveLocalWebsiteProfileDirectory(definition.connectorId, {
|
|
75
|
+
profileDirectory: optionValue(args, "--profile-dir")
|
|
76
|
+
}),
|
|
77
|
+
purpose: "local-execution",
|
|
78
|
+
settleMs: optionalInteger(args, "--settle-ms"),
|
|
79
|
+
stderr: options.stderr,
|
|
80
|
+
stdin: options.stdin
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const execute = options.executeWebsiteOperation ?? executeWebsiteConnectorOperation;
|
|
84
|
+
const result = await execute(definition, operationId, await readArguments(args), {
|
|
85
|
+
fetchImpl: options.fetchImpl,
|
|
86
|
+
readOnlyOnly: !write,
|
|
87
|
+
...(storageState ? { storageState } : {})
|
|
88
|
+
});
|
|
89
|
+
stdout.write(`${JSON.stringify({ ...result, executionMode: "local" })}\n`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
45
92
|
const result = await callWebsiteBridge(store, write ? "website_connector_execute" : "website_connector_read", {
|
|
46
93
|
connectorId: requiredOption(args, "--connector"),
|
|
47
94
|
operationId: requiredOption(args, "--operation"),
|
|
48
95
|
arguments: await readArguments(args)
|
|
49
96
|
}, { ...options, confirmation: args.includes("--confirm") });
|
|
50
|
-
stdout.write(`${JSON.stringify(result)}\n`);
|
|
97
|
+
stdout.write(`${JSON.stringify({ ...result, executionMode: "remote" })}\n`);
|
|
51
98
|
return;
|
|
52
99
|
}
|
|
53
100
|
if (action === "connection-status") {
|
|
@@ -151,10 +198,36 @@ function validationSummary(definition) {
|
|
|
151
198
|
displayName: definition.displayName,
|
|
152
199
|
operationCount: definition.operations.length,
|
|
153
200
|
requiresConnection: definition.operations.some((operation) => operation.requiresConnection),
|
|
201
|
+
runtime: websiteConnectorRuntimeDescriptor(),
|
|
154
202
|
sha256: createHash("sha256").update(canonical).digest("hex")
|
|
155
203
|
};
|
|
156
204
|
}
|
|
157
205
|
|
|
206
|
+
function websiteCallMode(args) {
|
|
207
|
+
if (args.includes("--local")) return "local";
|
|
208
|
+
const mode = optionValue(args, "--mode") ?? "remote";
|
|
209
|
+
if (!["auto", "local", "remote"].includes(mode)) throw new Error("--mode must be auto, local, or remote");
|
|
210
|
+
return mode === "auto" ? (optionValue(args, "--file") ? "local" : "remote") : mode;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function localProfileSpec(definition) {
|
|
214
|
+
if (definition.auth.kind !== "browser_storage" || !definition.loginUrl) {
|
|
215
|
+
throw new Error("Website Connector does not define a local browser Profile");
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
allowedOrigins: definition.allowedOrigins,
|
|
219
|
+
auth: {
|
|
220
|
+
headerSources: definition.auth.headerSources.map(({ keySuffix, origin }) => ({ keySuffix, origin })),
|
|
221
|
+
secretSources: definition.auth.secretSources.map(({ keySuffix, origin }) => ({ keySuffix, origin })),
|
|
222
|
+
sendCookies: definition.auth.sendCookies
|
|
223
|
+
},
|
|
224
|
+
connection: {},
|
|
225
|
+
connectorId: definition.connectorId,
|
|
226
|
+
loginUrl: definition.loginUrl,
|
|
227
|
+
profileSchemaVersion: "fargo.browser-profile/v1"
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
158
231
|
function optionalInteger(args, name) {
|
|
159
232
|
const raw = optionValue(args, name);
|
|
160
233
|
if (raw === undefined) return undefined;
|
|
@@ -264,12 +337,13 @@ function validatedUploadUrl(value, options) {
|
|
|
264
337
|
return url.toString();
|
|
265
338
|
}
|
|
266
339
|
|
|
267
|
-
export const WEBSITE_USAGE = "Usage: fargo website validate|release|list|operations|call|connect|sync|connection-status|authorize|authorize-complete|binding";
|
|
340
|
+
export const WEBSITE_USAGE = "Usage: fargo website validate|runtime|release|list|operations|call|connect|sync|connection-status|authorize|authorize-complete|binding";
|
|
268
341
|
|
|
269
342
|
export const WEBSITE_HELP = `Usage: fargo website <command> [options]
|
|
270
343
|
|
|
271
344
|
Connector authoring:
|
|
272
345
|
validate --file <path>
|
|
346
|
+
runtime
|
|
273
347
|
release --file <path> [--expected-revision <n>] --confirm
|
|
274
348
|
list
|
|
275
349
|
operations --connector <id>
|
|
@@ -278,7 +352,8 @@ Connection and execution:
|
|
|
278
352
|
connect --connector <id> [--chrome-path <path>] [--profile-dir <path>]
|
|
279
353
|
sync --connector <id> [--no-prompt] [--settle-ms <n>]
|
|
280
354
|
connection-status --connector <id>
|
|
281
|
-
call --connector <id> --operation <id> [--args <json> | --args-file <path>]
|
|
355
|
+
call --connector <id> --operation <id> [--args <json> | --args-file <path>] [--mode auto|remote]
|
|
356
|
+
call --file <path> --operation <id> --mode local [--profile-dir <path>] [--no-prompt]
|
|
282
357
|
call --connector <id> --operation <id> --write [--confirm]
|
|
283
358
|
|
|
284
359
|
Cloud login fallback:
|
|
@@ -1,58 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
additionalProperties: false;
|
|
3
|
-
properties: Record<string, {
|
|
4
|
-
enum?: Array<string | number | boolean>;
|
|
5
|
-
maxLength?: number;
|
|
6
|
-
type: "string" | "number" | "integer" | "boolean";
|
|
7
|
-
}>;
|
|
8
|
-
required: string[];
|
|
9
|
-
type: "object";
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export type WebsiteConnectorAuth =
|
|
13
|
-
| { kind: "none" }
|
|
14
|
-
| {
|
|
15
|
-
kind: "browser_storage";
|
|
16
|
-
sendCookies: boolean;
|
|
17
|
-
headerSources: Array<{ keySuffix: string; name: string; origin: string; prefix?: string }>;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export type WebsiteConnectorOperation = {
|
|
21
|
-
operationId: string;
|
|
22
|
-
description: string;
|
|
23
|
-
readOnly: boolean;
|
|
24
|
-
requiresConnection: boolean;
|
|
25
|
-
riskLevel: "low" | "medium" | "high" | "critical";
|
|
26
|
-
inputSchema: WebsiteConnectorInputSchema;
|
|
27
|
-
request: {
|
|
28
|
-
origin: string;
|
|
29
|
-
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
30
|
-
path: string;
|
|
31
|
-
query: Record<string, string>;
|
|
32
|
-
headers: Record<string, string>;
|
|
33
|
-
body?: unknown;
|
|
34
|
-
};
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export type WebsiteConnectorDefinition = {
|
|
38
|
-
schemaVersion: "fargo.website-connector/v1";
|
|
39
|
-
connectorId: string;
|
|
40
|
-
displayName: string;
|
|
41
|
-
allowedOrigins: string[];
|
|
42
|
-
loginUrl?: string;
|
|
43
|
-
auth: WebsiteConnectorAuth;
|
|
44
|
-
operations: WebsiteConnectorOperation[];
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export const WEBSITE_CONNECTOR_SCHEMA_VERSION: "fargo.website-connector/v1";
|
|
48
|
-
export class WebsiteConnectorDefinitionError extends Error {
|
|
49
|
-
code: string;
|
|
50
|
-
path: string;
|
|
51
|
-
}
|
|
52
|
-
export function parseWebsiteConnectorDefinition(value: unknown): WebsiteConnectorDefinition;
|
|
53
|
-
export function connectorToolName(connectorId: string, operationId: string): string;
|
|
54
|
-
export function renderWebsiteOperation(definition: unknown, operationId: string, rawArguments: unknown): {
|
|
55
|
-
definition: WebsiteConnectorDefinition;
|
|
56
|
-
operation: WebsiteConnectorOperation;
|
|
57
|
-
request: { body?: unknown; headers: Record<string, string>; method: string; url: string };
|
|
58
|
-
};
|
|
1
|
+
export * from "@fargowealth/website-connector-runtime";
|