@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fargowealth/fargo-ai-cli",
3
- "version": "1.3.0",
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.3.0";
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(spec.auth.headerSources.map((source) => source.origin))];
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 (value?.readyState === "complete" && new URL(value.url).origin === expectedOrigin) return;
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
- output.write(`Fargo opened an isolated local Chrome Profile for ${spec.connectorId}.\nComplete login, then press Enter here to upload only the allowlisted session state.\n`);
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) => host === allowed || host.endsWith(`.${allowed}`));
357
+ return allowedHosts.some((allowed) => allowed === host || allowed.endsWith(`.${host}`));
315
358
  }
316
359
 
317
360
  function exactHttpsOrigin(value) {
@@ -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 { parseWebsiteConnectorDefinition } from "./website-connector-definition.mjs";
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 === "--help" || action === "-h" || action === "help") {
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 type WebsiteConnectorInputSchema = {
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";