@fargowealth/fargo-ai-cli 0.7.2 → 0.7.3

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.3
4
+
5
+ - Added one-command Agent installation through `fargo agent install --agent`
6
+ for WorkBuddy, Claude Code, Codex, and Cursor.
7
+ - The installer completes CLI-managed OAuth + PKCE, writes a local stdio MCP
8
+ connection, and reuses the same private rotating credential on later runs.
9
+ - Kept `fargo workbuddy install` as a compatibility alias.
10
+
3
11
  ## 0.7.2
4
12
 
5
13
  - Added `fargo lego deploy --help` with the complete Worker Static Assets
package/README.md CHANGED
@@ -9,23 +9,25 @@ Requires Node.js 20.19 or newer.
9
9
  ## Install or upgrade
10
10
 
11
11
  ```sh
12
- npm install -g @fargowealth/fargo-ai-cli@0.7.2
12
+ npm install -g @fargowealth/fargo-ai-cli@0.7.3
13
13
  fargo version
14
14
  ```
15
15
 
16
- Use native hosted Fargo MCP for ordinary online tools. Authenticate the CLI
17
- only when an Agent first needs local files:
16
+ Install the Fargo MCP connection with one command for the selected Agent:
18
17
 
19
18
  ```sh
20
- fargo auth login --agent codex
21
- fargo auth login --agent claude-code
22
- fargo auth login --agent cursor
19
+ npx -y @fargowealth/fargo-ai-cli@latest agent install --agent=codex --open-browser=always --output=jsonl
20
+ npx -y @fargowealth/fargo-ai-cli@latest agent install --agent=claude-code --open-browser=always --output=jsonl
21
+ npx -y @fargowealth/fargo-ai-cli@latest agent install --agent=cursor --open-browser=always --output=jsonl
22
+ npx -y @fargowealth/fargo-ai-cli@latest agent install --agent=workbuddy --open-browser=always --output=jsonl
23
23
  fargo auth status
24
24
  ```
25
25
 
26
- Aliases `cc` and `claude` resolve to `claude-code`. Login uses OAuth
27
- Authorization Code + PKCE over an ephemeral IPv4 loopback callback. The CLI
28
- validates state, uses PKCE S256, closes the listener, and never prints tokens.
26
+ Run only the line for the Agent being configured. The command completes OAuth
27
+ Authorization Code + PKCE over an ephemeral IPv4 loopback callback, writes the
28
+ Agent's user-level local stdio MCP entry, and reports `installed=true`. It is
29
+ normally run once; the rotating credential is then refreshed automatically and
30
+ reused by both MCP and local CLI operations.
29
31
 
30
32
  ## MCP boundary
31
33
 
@@ -34,9 +36,10 @@ validates state, uses PKCE S256, closes the listener, and never prints tokens.
34
36
  - CLI owns local walking, exclusions, ZIP creation, hashing, binary transfer,
35
37
  deploy composition, and migration-file ordering.
36
38
  - CLI has no generic R2 or Cloudflare administration commands.
37
- - `fargo mcp serve` is the local stdio bridge for WorkBuddy.
38
- - `fargo mcp tools` and `fargo mcp call` are fallbacks for Agents without a
39
- native remote MCP client.
39
+ - `fargo mcp serve` is the local stdio bridge for WorkBuddy, Claude Code,
40
+ Codex, and Cursor.
41
+ - `fargo mcp tools` and `fargo mcp call` support scripts and one-shot
42
+ diagnostics without starting an MCP host.
40
43
 
41
44
  ```sh
42
45
  fargo mcp tools
@@ -175,14 +178,16 @@ blocked. `--table-prefix` is a local assertion and the service rejects it if it
175
178
  does not match the App's authoritative prefix. Migration execution remains
176
179
  server-side and is never coupled to deploy.
177
180
 
178
- ## WorkBuddy adapter
181
+ ## Compatibility and repair
179
182
 
180
183
  ```sh
181
184
  npx -y @fargowealth/fargo-ai-cli@latest workbuddy install --open-browser=always --output=jsonl
182
185
  ```
183
186
 
184
- The installer preserves existing MCP servers and adds the local Fargo stdio
185
- bridge. Device Authorization Grant remains an explicit fallback:
187
+ The legacy WorkBuddy command is an alias for the unified Agent installer.
188
+ Installers preserve unrelated MCP servers. Rerun the unified command only to
189
+ upgrade or repair a Fargo entry. Device Authorization Grant remains an explicit
190
+ fallback:
186
191
 
187
192
  ```sh
188
193
  fargo auth device --open-browser=always --output=jsonl
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fargowealth/fargo-ai-cli",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Agent-neutral Fargo AI CLI, MCP client, and LEGO project tooling",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,241 @@
1
+ import { constants as fsConstants } from "node:fs";
2
+ import { access, mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, isAbsolute, join, resolve } from "node:path";
5
+ import { spawn } from "node:child_process";
6
+ import { normalizeAgentType } from "./auth.mjs";
7
+ import { CLI_VERSION } from "./constants.mjs";
8
+
9
+ const PACKAGE_NAME = "@fargowealth/fargo-ai-cli";
10
+ const SERVER_NAME = "fargo-ai";
11
+
12
+ export async function installAgentMcp(rawAgent, options = {}) {
13
+ const agent = normalizeAgentType(rawAgent);
14
+ const platform = options.platform ?? process.platform;
15
+ const env = options.env ?? process.env;
16
+ const homeDirectory = options.home ?? homedir();
17
+ const packageSpec = `${PACKAGE_NAME}@${options.packageVersion ?? CLI_VERSION}`;
18
+ const npxCommand = options.npxCommand ?? (platform === "darwin"
19
+ ? await resolveNpxCommand({ env, execPath: options.execPath, platform })
20
+ : "npx");
21
+ const server = mcpServerConfig({ agent, env, homeDirectory, npxCommand, packageSpec, platform });
22
+
23
+ if (agent === "workbuddy" || agent === "cursor") {
24
+ return installJsonMcp(agent, server, {
25
+ ...options,
26
+ configPath: options.configPath ?? defaultAgentConfigPath(agent, homeDirectory)
27
+ });
28
+ }
29
+
30
+ return installHostCliMcp(agent, server, options);
31
+ }
32
+
33
+ export async function installWorkBuddyMcp(options = {}) {
34
+ return installAgentMcp("workbuddy", options);
35
+ }
36
+
37
+ export function defaultAgentConfigPath(rawAgent, homeDirectory = homedir()) {
38
+ const agent = normalizeAgentType(rawAgent);
39
+ if (agent === "workbuddy") return join(homeDirectory, ".workbuddy-ai", "mcp.json");
40
+ if (agent === "cursor") return join(homeDirectory, ".cursor", "mcp.json");
41
+ return undefined;
42
+ }
43
+
44
+ export async function resolveNpxCommand(options = {}) {
45
+ const platform = options.platform ?? process.platform;
46
+ const env = options.env ?? process.env;
47
+ const execPath = options.execPath ?? process.execPath;
48
+ const executableName = platform === "win32" ? "npx.cmd" : "npx";
49
+ const pathDelimiter = platform === "win32" ? ";" : ":";
50
+ const candidates = [
51
+ join(dirname(execPath), executableName),
52
+ ...String(env.PATH ?? env.Path ?? env.path ?? "").split(pathDelimiter).filter(Boolean).map((entry) => join(entry, executableName))
53
+ ];
54
+ for (const candidate of candidates) {
55
+ const absoluteCandidate = isAbsolute(candidate) ? candidate : resolve(candidate);
56
+ try {
57
+ await access(absoluteCandidate, platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK);
58
+ return absoluteCandidate;
59
+ } catch {
60
+ // Continue until an executable visible to the GUI host is found.
61
+ }
62
+ }
63
+ throw new Error("Unable to locate an absolute npx executable for the Agent MCP configuration");
64
+ }
65
+
66
+ function mcpServerConfig({ agent, env, homeDirectory, npxCommand, packageSpec, platform }) {
67
+ const credentialDirectory = platform === "win32"
68
+ ? join(env.APPDATA || homeDirectory, "Fargo AI")
69
+ : join(homeDirectory, ".fargo");
70
+ const serverEnv = {
71
+ FARGO_CREDENTIAL_PATH: join(credentialDirectory, "credentials.json"),
72
+ FARGO_CREDENTIAL_STORE: "file",
73
+ FARGO_REFRESH_LOCK_PATH: join(credentialDirectory, "oauth-refresh.lock")
74
+ };
75
+ if (platform === "darwin") serverEnv.PATH = buildMacPath(npxCommand);
76
+ if (platform === "win32") {
77
+ const windowsPath = env.PATH ?? env.Path ?? env.path;
78
+ if (windowsPath) serverEnv.PATH = windowsPath;
79
+ }
80
+ return {
81
+ agent,
82
+ args: ["-y", packageSpec, "mcp", "serve"],
83
+ command: npxCommand,
84
+ env: serverEnv,
85
+ packageSpec
86
+ };
87
+ }
88
+
89
+ async function installJsonMcp(agent, server, options) {
90
+ const config = await readConfig(options.configPath, agent);
91
+ const nextServer = {
92
+ ...(agent === "workbuddy" ? { type: "stdio" } : {}),
93
+ command: server.command,
94
+ args: server.args,
95
+ ...(agent === "workbuddy" ? { timeout: 60000 } : {}),
96
+ env: server.env
97
+ };
98
+ const previousServer = config.mcpServers?.[SERVER_NAME];
99
+ const changed = JSON.stringify(previousServer) !== JSON.stringify(nextServer);
100
+
101
+ if (changed) {
102
+ config.mcpServers = { ...(config.mcpServers ?? {}), [SERVER_NAME]: nextServer };
103
+ await writeConfig(options.configPath, config);
104
+ }
105
+
106
+ return installationResult(agent, server.packageSpec, changed, {
107
+ config_path: options.configPath,
108
+ trust_required: agent === "workbuddy"
109
+ });
110
+ }
111
+
112
+ async function installHostCliMcp(agent, server, options) {
113
+ const command = agent === "codex" ? "codex" : "claude";
114
+ const removeArgs = agent === "codex"
115
+ ? ["mcp", "remove", SERVER_NAME]
116
+ : ["mcp", "remove", "--scope", "user", SERVER_NAME];
117
+ const removeResult = await runHostCommand(command, removeArgs, options);
118
+ if (removeResult.exitCode !== 0 && !isMissingServer(removeResult)) {
119
+ throw new Error(`${agentLabel(agent)} could not replace the existing ${SERVER_NAME} MCP entry: ${commandError(removeResult)}`);
120
+ }
121
+
122
+ const environmentArgs = Object.entries(server.env).flatMap(([key, value]) => agent === "codex"
123
+ ? ["--env", `${key}=${value}`]
124
+ : ["-e", `${key}=${value}`]);
125
+ const addArgs = agent === "codex"
126
+ ? ["mcp", "add", ...environmentArgs, SERVER_NAME, "--", server.command, ...server.args]
127
+ : ["mcp", "add", "--transport", "stdio", "--scope", "user", SERVER_NAME, ...environmentArgs, "--", server.command, ...server.args];
128
+ const addResult = await runHostCommand(command, addArgs, options);
129
+ if (addResult.exitCode !== 0) {
130
+ throw new Error(`${agentLabel(agent)} could not add the ${SERVER_NAME} CLI MCP entry: ${commandError(addResult)}`);
131
+ }
132
+
133
+ return installationResult(agent, server.packageSpec, true, {
134
+ configured_by: command,
135
+ trust_required: false
136
+ });
137
+ }
138
+
139
+ async function runHostCommand(command, args, options) {
140
+ if (options.runCommand) return normalizeCommandResult(await options.runCommand(command, args));
141
+ return new Promise((resolveResult, reject) => {
142
+ const child = spawn(command, args, { env: options.env ?? process.env, windowsHide: true });
143
+ let stdout = "";
144
+ let stderr = "";
145
+ child.stdout?.on("data", (chunk) => { stdout += chunk.toString(); });
146
+ child.stderr?.on("data", (chunk) => { stderr += chunk.toString(); });
147
+ child.on("error", (error) => {
148
+ if (error?.code === "ENOENT") {
149
+ reject(new Error(`Required ${agentCommandLabel(command)} command '${command}' was not found; run the installer from that Agent environment`));
150
+ return;
151
+ }
152
+ reject(error);
153
+ });
154
+ child.on("close", (exitCode) => resolveResult({ exitCode: exitCode ?? 1, stderr, stdout }));
155
+ });
156
+ }
157
+
158
+ function normalizeCommandResult(result) {
159
+ return {
160
+ exitCode: Number(result?.exitCode ?? result?.code ?? 0),
161
+ stderr: String(result?.stderr ?? ""),
162
+ stdout: String(result?.stdout ?? "")
163
+ };
164
+ }
165
+
166
+ function isMissingServer(result) {
167
+ return /not found|no .*mcp server|does not exist|unknown server/i.test(`${result.stdout}\n${result.stderr}`);
168
+ }
169
+
170
+ function commandError(result) {
171
+ return (result.stderr || result.stdout || `exit ${result.exitCode}`).trim().slice(0, 300);
172
+ }
173
+
174
+ function installationResult(agent, packageSpec, changed, extra) {
175
+ return {
176
+ type: "agent_installation_complete",
177
+ agent,
178
+ installed: true,
179
+ changed,
180
+ server_name: SERVER_NAME,
181
+ package: packageSpec,
182
+ ...extra,
183
+ next_action: agent === "workbuddy"
184
+ ? "Open WorkBuddy Custom Connectors, trust and enable fargo-ai, then start a new task."
185
+ : `Restart ${agentLabel(agent)} or start a new Agent session, then call ping_who_am_i.`
186
+ };
187
+ }
188
+
189
+ function agentLabel(agent) {
190
+ if (agent === "claude-code") return "Claude Code";
191
+ if (agent === "codex") return "Codex";
192
+ if (agent === "cursor") return "Cursor";
193
+ return "WorkBuddy";
194
+ }
195
+
196
+ function agentCommandLabel(command) {
197
+ return command === "claude" ? "Claude Code" : "Codex";
198
+ }
199
+
200
+ function buildMacPath(npxCommand) {
201
+ return [...new Set([
202
+ dirname(npxCommand),
203
+ "/opt/homebrew/bin",
204
+ "/usr/local/bin",
205
+ "/usr/bin",
206
+ "/bin",
207
+ "/usr/sbin",
208
+ "/sbin"
209
+ ])].join(":");
210
+ }
211
+
212
+ async function readConfig(configPath, agent) {
213
+ let text;
214
+ try {
215
+ text = await readFile(configPath, "utf8");
216
+ } catch (error) {
217
+ if (error?.code === "ENOENT") return {};
218
+ throw error;
219
+ }
220
+ if (!text.trim()) return {};
221
+ let parsed;
222
+ try {
223
+ parsed = JSON.parse(text);
224
+ } catch {
225
+ throw new Error(`${agentLabel(agent)} MCP config is not valid JSON: ${configPath}`);
226
+ }
227
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
228
+ throw new Error(`${agentLabel(agent)} MCP config must contain a JSON object: ${configPath}`);
229
+ }
230
+ if (parsed.mcpServers !== undefined && (!parsed.mcpServers || typeof parsed.mcpServers !== "object" || Array.isArray(parsed.mcpServers))) {
231
+ throw new Error(`${agentLabel(agent)} mcpServers must be a JSON object: ${configPath}`);
232
+ }
233
+ return parsed;
234
+ }
235
+
236
+ async function writeConfig(configPath, config) {
237
+ await mkdir(dirname(configPath), { recursive: true });
238
+ const temporaryPath = `${configPath}.${process.pid}.${Date.now()}.tmp`;
239
+ await writeFile(temporaryPath, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
240
+ await rename(temporaryPath, configPath);
241
+ }
package/src/cli.mjs CHANGED
@@ -8,7 +8,7 @@ import { deployLocalLegoApp } from "./lego-deploy.mjs";
8
8
  import { applyLegoMigrations, planLegoMigrations } from "./lego-migrations.mjs";
9
9
  import { callMcpTool, listMcpTools } from "./mcp-client.mjs";
10
10
  import { runMcpProxy } from "./mcp-proxy.mjs";
11
- import { installWorkBuddyMcp } from "./workbuddy-install.mjs";
11
+ import { installAgentMcp } from "./agent-install.mjs";
12
12
 
13
13
  export async function runCli(args, options = {}) {
14
14
  const stdout = options.stdout ?? process.stdout;
@@ -114,24 +114,15 @@ export async function runCli(args, options = {}) {
114
114
  }
115
115
  throw new Error("Usage: fargo lego d1 migrate plan|apply --app <appId> [--dir migrations] [--table-prefix app_prefix_] [--allow-data-migrations]");
116
116
  }
117
+ if (args[0] === "agent" && args[1] === "install") {
118
+ await installAgentConnection(args, requiredOption(args, "--agent"), stdout, store, options);
119
+ return;
120
+ }
117
121
  if (args[0] === "workbuddy" && args[1] === "install") {
118
- let credential = await store.load();
119
- if (credential) {
120
- try {
121
- credential = await currentCredential(store, options);
122
- } catch {
123
- await store.clear();
124
- credential = undefined;
125
- }
126
- }
127
- if (credential?.authorization_method !== "authorization_code_pkce") {
128
- credential = undefined;
129
- }
130
- if (!credential?.refresh_token) await authorizeWithPkce(args, stdout, store, { ...options, agentType: "workbuddy" });
131
- stdout.write(`${JSON.stringify(await installWorkBuddyMcp(options))}\n`);
122
+ await installAgentConnection(args, "workbuddy", stdout, store, options);
132
123
  return;
133
124
  }
134
- throw new Error("Usage: fargo auth login|device|status|logout | fargo mcp serve|tools|call | fargo lego snapshot upload|list|download | fargo lego deploy | fargo lego d1 migrate plan|apply | fargo workbuddy install | fargo version");
125
+ throw new Error("Usage: fargo auth login|device|status|logout | fargo agent install --agent <agent> | fargo mcp serve|tools|call | fargo lego snapshot upload|list|download | fargo lego deploy | fargo lego d1 migrate plan|apply | fargo workbuddy install | fargo version");
135
126
  } catch (error) {
136
127
  stderr.write(`fargo: ${error instanceof Error ? error.message : "unknown error"}\n`);
137
128
  if (options.throwOnError) throw error;
@@ -143,6 +134,7 @@ const ROOT_HELP = `Usage: fargo <command> [options]
143
134
 
144
135
  Commands:
145
136
  auth login|device|status|logout
137
+ agent install --agent <codex|claude-code|cursor|workbuddy>
146
138
  mcp serve|tools|call
147
139
  lego snapshot upload|list|download
148
140
  lego deploy
@@ -197,6 +189,23 @@ function isHelpOption(args) {
197
189
  return args.includes("--help") || args.includes("-h");
198
190
  }
199
191
 
192
+ async function installAgentConnection(args, agentType, stdout, store, options) {
193
+ let credential = await store.load();
194
+ if (credential) {
195
+ try {
196
+ credential = await currentCredential(store, options);
197
+ } catch {
198
+ await store.clear();
199
+ credential = undefined;
200
+ }
201
+ }
202
+ if (credential?.authorization_method !== "authorization_code_pkce") credential = undefined;
203
+ const authorizationReused = Boolean(credential?.refresh_token);
204
+ if (!authorizationReused) await authorizeWithPkce(args, stdout, store, { ...options, agentType });
205
+ const installation = await installAgentMcp(agentType, options);
206
+ stdout.write(`${JSON.stringify({ ...installation, authorization_reused: authorizationReused })}\n`);
207
+ }
208
+
200
209
  function assertAssetsDirectoryProvided(args, assetsDirectory) {
201
210
  const assetOptions = [
202
211
  "--assets-binding",
package/src/constants.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const CLI_VERSION = "0.7.2";
1
+ export const CLI_VERSION = "0.7.3";
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,129 +1,12 @@
1
- import { constants as fsConstants } from "node:fs";
2
- import { access, mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
- import { homedir } from "node:os";
4
- import { dirname, isAbsolute, join, resolve } from "node:path";
5
- import { CLI_VERSION } from "./constants.mjs";
1
+ import { defaultAgentConfigPath } from "./agent-install.mjs";
6
2
 
7
- const PACKAGE_NAME = "@fargowealth/fargo-ai-cli";
3
+ export {
4
+ defaultAgentConfigPath,
5
+ installAgentMcp,
6
+ installWorkBuddyMcp,
7
+ resolveNpxCommand
8
+ } from "./agent-install.mjs";
8
9
 
9
- export async function installWorkBuddyMcp(options = {}) {
10
- const platform = options.platform ?? process.platform;
11
- const env = options.env ?? process.env;
12
- const homeDirectory = options.home ?? homedir();
13
- const configPath = options.configPath
14
- ?? env.FARGO_WORKBUDDY_MCP_CONFIG
15
- ?? defaultWorkBuddyConfigPath(homeDirectory);
16
- const packageSpec = `${PACKAGE_NAME}@${options.packageVersion ?? CLI_VERSION}`;
17
- const config = await readConfig(configPath);
18
- const npxCommand = options.npxCommand ?? (platform === "darwin"
19
- ? await resolveNpxCommand({ env, execPath: options.execPath, platform })
20
- : "npx");
21
- const nextServer = {
22
- type: "stdio",
23
- command: npxCommand,
24
- args: ["-y", packageSpec, "mcp", "serve"],
25
- timeout: 60000
26
- };
27
- if (platform === "darwin" || platform === "win32") {
28
- const credentialDirectory = platform === "win32"
29
- ? join(env.APPDATA || homeDirectory, "Fargo AI")
30
- : join(homeDirectory, ".fargo");
31
- nextServer.env = {
32
- FARGO_CREDENTIAL_PATH: join(credentialDirectory, "credentials.json"),
33
- FARGO_CREDENTIAL_STORE: "file",
34
- FARGO_REFRESH_LOCK_PATH: join(credentialDirectory, "oauth-refresh.lock")
35
- };
36
- if (platform === "darwin") nextServer.env.PATH = buildMacPath(npxCommand);
37
- if (platform === "win32") {
38
- const windowsPath = env.PATH ?? env.Path ?? env.path;
39
- if (windowsPath) nextServer.env.PATH = windowsPath;
40
- }
41
- }
42
- const previousServer = config.mcpServers?.["fargo-ai"];
43
- const changed = JSON.stringify(previousServer) !== JSON.stringify(nextServer);
44
-
45
- if (changed) {
46
- config.mcpServers = { ...(config.mcpServers ?? {}), "fargo-ai": nextServer };
47
- await writeConfig(configPath, config);
48
- }
49
-
50
- return {
51
- type: "workbuddy_installation_complete",
52
- installed: true,
53
- changed,
54
- config_path: configPath,
55
- server_name: "fargo-ai",
56
- package: packageSpec,
57
- trust_required: true,
58
- next_action: "Open WorkBuddy MCP management, trust and enable fargo-ai, then reload MCP or start a new task."
59
- };
60
- }
61
-
62
- export async function resolveNpxCommand(options = {}) {
63
- const platform = options.platform ?? process.platform;
64
- const env = options.env ?? process.env;
65
- const execPath = options.execPath ?? process.execPath;
66
- const executableName = platform === "win32" ? "npx.cmd" : "npx";
67
- const pathDelimiter = platform === "win32" ? ";" : ":";
68
- const candidates = [
69
- join(dirname(execPath), executableName),
70
- ...String(env.PATH ?? "").split(pathDelimiter).filter(Boolean).map((entry) => join(entry, executableName))
71
- ];
72
- for (const candidate of candidates) {
73
- const absoluteCandidate = isAbsolute(candidate) ? candidate : resolve(candidate);
74
- try {
75
- await access(absoluteCandidate, platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK);
76
- return absoluteCandidate;
77
- } catch {
78
- // Keep searching for an executable that will also be visible to the GUI host.
79
- }
80
- }
81
- throw new Error("Unable to locate an absolute npx executable for WorkBuddy");
82
- }
83
-
84
- function buildMacPath(npxCommand) {
85
- return [...new Set([
86
- dirname(npxCommand),
87
- "/opt/homebrew/bin",
88
- "/usr/local/bin",
89
- "/usr/bin",
90
- "/bin",
91
- "/usr/sbin",
92
- "/sbin"
93
- ])].join(":");
94
- }
95
-
96
- export function defaultWorkBuddyConfigPath(homeDirectory = homedir()) {
97
- return join(homeDirectory, ".workbuddy-ai", "mcp.json");
98
- }
99
-
100
- async function readConfig(configPath) {
101
- let text;
102
- try {
103
- text = await readFile(configPath, "utf8");
104
- } catch (error) {
105
- if (error?.code === "ENOENT") return {};
106
- throw error;
107
- }
108
- if (!text.trim()) return {};
109
- let parsed;
110
- try {
111
- parsed = JSON.parse(text);
112
- } catch {
113
- throw new Error(`WorkBuddy MCP config is not valid JSON: ${configPath}`);
114
- }
115
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
116
- throw new Error(`WorkBuddy MCP config must contain a JSON object: ${configPath}`);
117
- }
118
- if (parsed.mcpServers !== undefined && (!parsed.mcpServers || typeof parsed.mcpServers !== "object" || Array.isArray(parsed.mcpServers))) {
119
- throw new Error(`WorkBuddy mcpServers must be a JSON object: ${configPath}`);
120
- }
121
- return parsed;
122
- }
123
-
124
- async function writeConfig(configPath, config) {
125
- await mkdir(dirname(configPath), { recursive: true });
126
- const temporaryPath = `${configPath}.${process.pid}.${Date.now()}.tmp`;
127
- await writeFile(temporaryPath, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
128
- await rename(temporaryPath, configPath);
10
+ export function defaultWorkBuddyConfigPath(homeDirectory) {
11
+ return defaultAgentConfigPath("workbuddy", homeDirectory);
129
12
  }