@fargowealth/fargo-ai-cli 0.3.0 → 0.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 +16 -0
- package/README.md +21 -11
- package/package.json +3 -2
- package/src/auth.mjs +14 -2
- package/src/constants.mjs +1 -1
- package/src/credential-store.mjs +63 -72
- package/src/workbuddy-install.mjs +57 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
- **Windows behavior change:** WorkBuddy credentials now use `%APPDATA%\Fargo AI\credentials.json` instead of DPAPI, because sandboxed WorkBuddy sessions may be unable to read or update DPAPI-protected data.
|
|
6
|
+
- `workbuddy install` writes `FARGO_CREDENTIAL_STORE=file` plus absolute credential and refresh-lock paths into the Windows MCP configuration.
|
|
7
|
+
- Existing DPAPI credentials are not migrated. Windows users must reconnect once with 0.4.1.
|
|
8
|
+
|
|
9
|
+
## 0.4.0
|
|
10
|
+
|
|
11
|
+
- **Behavior change:** Fargo CLI is explicitly WorkBuddy-only. On macOS, credentials use the sandbox-compatible private file at `~/.fargo/credentials.json`; the unsupported Keychain mode is rejected. Windows remains protected by DPAPI.
|
|
12
|
+
- `workbuddy install` writes the macOS credential mode and absolute credential/refresh-lock paths into WorkBuddy's MCP configuration.
|
|
13
|
+
- On macOS, `workbuddy install` resolves an absolute `npx` executable and supplies a stable GUI `PATH`.
|
|
14
|
+
- Windows opens the complete OAuth authorization URL directly through Explorer instead of `cmd.exe start`, preventing `&` query separators from truncating `redirect_uri`.
|
|
15
|
+
- Private-file writes are atomic and `fsync`-backed; directory/file modes are repaired to `0700`/`0600`, and symbolic-link credential reads are rejected.
|
|
16
|
+
- Existing macOS 0.3.x Keychain credentials are not copied into the file store. Users must reconnect once; the previous Keychain item should be removed separately if it exists.
|
package/README.md
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
|
-
# Fargo CLI
|
|
1
|
+
# Fargo CLI for WorkBuddy
|
|
2
2
|
|
|
3
|
-
`fargo-cli`
|
|
3
|
+
`fargo-cli` is the compatibility bridge that connects WorkBuddy to the hosted Fargo AI MCP server. It uses OAuth Authorization Code + PKCE over an ephemeral loopback callback and exposes the remote MCP over local stdio.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
|
|
7
|
-
fargo
|
|
8
|
-
fargo auth
|
|
9
|
-
fargo auth device --open-browser=always --output=jsonl
|
|
10
|
-
fargo auth status --output=json
|
|
11
|
-
fargo mcp serve
|
|
12
|
-
fargo auth logout
|
|
6
|
+
npx -y @fargowealth/fargo-ai-cli@latest workbuddy install --open-browser=always --output=jsonl
|
|
7
|
+
npx -y @fargowealth/fargo-ai-cli@latest auth status --output=json
|
|
8
|
+
npx -y @fargowealth/fargo-ai-cli@latest auth logout --output=json
|
|
13
9
|
```
|
|
14
10
|
|
|
15
|
-
The
|
|
11
|
+
The login opens the shared Fargo AI consent page and returns to `http://127.0.0.1:<random-port>/oauth/callback/...`. The CLI binds only to IPv4 loopback, validates OAuth `state`, uses PKCE S256, closes the listener after the callback, and never prints private OAuth credentials.
|
|
16
12
|
|
|
17
|
-
|
|
13
|
+
## Credential storage
|
|
14
|
+
|
|
15
|
+
- **WorkBuddy on macOS:** `~/.fargo/credentials.json`. WorkBuddy's sandbox blocks both the `security` CLI and native Keychain Services. The CLI therefore uses a private file by default and never asks for Full Disk Access.
|
|
16
|
+
- **WorkBuddy on Windows:** `%APPDATA%\Fargo AI\credentials.json`. WorkBuddy may also restrict DPAPI access, so the CLI uses a user-profile credential file and does not depend on DPAPI.
|
|
17
|
+
- **WorkBuddy on Linux:** `~/.fargo/credentials.json`.
|
|
18
|
+
|
|
19
|
+
On macOS and Linux, the credential directory is forced to `0700` and the credential file to `0600`. Windows inherits the current user's NTFS ACL from `%APPDATA%`. Writes use an `fsync`-backed temporary file followed by atomic replacement, symbolic-link reads are rejected, and refresh-token rotation is serialized across local CLI processes.
|
|
20
|
+
|
|
21
|
+
The local file contains an OAuth refresh credential. Do not copy, upload, commit, print, or include it in logs. Use `npx -y @fargowealth/fargo-ai-cli@latest auth logout` to remove it when a device is no longer trusted, and revoke the connection from Fargo AI.
|
|
22
|
+
|
|
23
|
+
Device Authorization Grant remains available only as an explicit fallback:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npx -y @fargowealth/fargo-ai-cli@latest auth device --open-browser=always --output=jsonl
|
|
27
|
+
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fargowealth/fargo-ai-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Fargo AI
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Fargo AI OAuth and stdio MCP bridge for WorkBuddy",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"fargo": "bin/fargo.mjs",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"bin",
|
|
12
12
|
"src",
|
|
13
|
+
"CHANGELOG.md",
|
|
13
14
|
"README.md"
|
|
14
15
|
],
|
|
15
16
|
"engines": {
|
package/src/auth.mjs
CHANGED
|
@@ -176,12 +176,24 @@ export async function refreshCurrentCredential(store, observedCredential, option
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
export function openBrowser(url, platform = process.platform) {
|
|
179
|
-
const
|
|
180
|
-
const args = platform === "win32" ? ["/d", "/s", "/c", "start", "", url] : [url];
|
|
179
|
+
const { args, command } = browserLaunchCommand(url, platform);
|
|
181
180
|
const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
181
|
+
child.on("error", () => {
|
|
182
|
+
// The authorization URL is emitted before this best-effort launch, so the
|
|
183
|
+
// host can still open it when no platform browser launcher is available.
|
|
184
|
+
});
|
|
182
185
|
child.unref();
|
|
183
186
|
}
|
|
184
187
|
|
|
188
|
+
export function browserLaunchCommand(url, platform = process.platform) {
|
|
189
|
+
if (platform === "win32") {
|
|
190
|
+
// Do not route OAuth URLs through `cmd.exe start`: unescaped `&` query
|
|
191
|
+
// separators are parsed by CMD and truncate the URL after client_id.
|
|
192
|
+
return { command: "explorer.exe", args: [url] };
|
|
193
|
+
}
|
|
194
|
+
return { command: platform === "darwin" ? "open" : "xdg-open", args: [url] };
|
|
195
|
+
}
|
|
196
|
+
|
|
185
197
|
function credentialFromToken(payload, authUrl, metadata = {}) {
|
|
186
198
|
if (typeof payload.access_token !== "string" || typeof payload.refresh_token !== "string") {
|
|
187
199
|
throw new Error("OAuth token response did not contain rotating credentials");
|
package/src/constants.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.
|
|
1
|
+
export const CLI_VERSION = "0.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_MCP_URL = "https://mcp.fargowealth.app/mcp";
|
package/src/credential-store.mjs
CHANGED
|
@@ -1,72 +1,36 @@
|
|
|
1
|
-
import { mkdir, open, readFile, rm, stat
|
|
1
|
+
import { chmod, lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
4
|
import { randomUUID } from "node:crypto";
|
|
6
5
|
import { setTimeout as delay } from "node:timers/promises";
|
|
7
6
|
|
|
8
|
-
const KEYCHAIN_SERVICE = "com.fargowealth.fargo-cli.oauth";
|
|
9
|
-
|
|
10
7
|
export function createCredentialStore(options = {}) {
|
|
11
8
|
const platform = options.platform ?? process.platform;
|
|
12
9
|
const env = options.env ?? process.env;
|
|
13
10
|
const home = options.home ?? homedir();
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
? join(env.APPDATA || home, "Fargo AI", "oauth-refresh.lock")
|
|
18
|
-
: join(home, ".fargo", "oauth-refresh.lock"));
|
|
19
|
-
const forceFile = env.FARGO_CREDENTIAL_STORE === "file";
|
|
20
|
-
const withRefreshLock = (callback) => withFileLock(refreshLockPath, callback);
|
|
21
|
-
|
|
22
|
-
if (!forceFile && platform === "darwin") {
|
|
23
|
-
const account = env.USER || env.LOGNAME || "fargo-user";
|
|
24
|
-
return {
|
|
25
|
-
kind: "macos-keychain",
|
|
26
|
-
withRefreshLock,
|
|
27
|
-
async load() {
|
|
28
|
-
const result = await run("security", ["find-generic-password", "-a", account, "-s", KEYCHAIN_SERVICE, "-w"], { allowFailure: true });
|
|
29
|
-
if (result.code !== 0 || !result.stdout.trim()) return undefined;
|
|
30
|
-
return parseCredential(result.stdout.trim());
|
|
31
|
-
},
|
|
32
|
-
async save(value) {
|
|
33
|
-
const serialized = JSON.stringify(value);
|
|
34
|
-
const result = await run("security", ["add-generic-password", "-a", account, "-s", KEYCHAIN_SERVICE, "-w", serialized, "-U"], { allowFailure: true });
|
|
35
|
-
if (result.code !== 0) throw new Error("Unable to save Fargo credentials in macOS Keychain");
|
|
36
|
-
},
|
|
37
|
-
async clear() {
|
|
38
|
-
await run("security", ["delete-generic-password", "-a", account, "-s", KEYCHAIN_SERVICE], { allowFailure: true });
|
|
39
|
-
}
|
|
40
|
-
};
|
|
11
|
+
const storeKind = env.FARGO_CREDENTIAL_STORE;
|
|
12
|
+
if (storeKind === "keychain") {
|
|
13
|
+
throw new Error("FARGO_CREDENTIAL_STORE=keychain is not supported by the WorkBuddy CLI; use file on macOS");
|
|
41
14
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const protectedPath = env.FARGO_CREDENTIAL_PATH ?? join(env.APPDATA || home, "Fargo AI", "credentials.bin");
|
|
45
|
-
return {
|
|
46
|
-
kind: "windows-dpapi",
|
|
47
|
-
withRefreshLock,
|
|
48
|
-
async load() {
|
|
49
|
-
const script = "$p=$env:FARGO_CREDENTIAL_PATH;if(!(Test-Path -LiteralPath $p)){exit 3};$e=[IO.File]::ReadAllBytes($p);$b=[Security.Cryptography.ProtectedData]::Unprotect($e,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Console]::Out.Write([Text.Encoding]::UTF8.GetString($b))";
|
|
50
|
-
const result = await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { allowFailure: true, env: { ...env, FARGO_CREDENTIAL_PATH: protectedPath } });
|
|
51
|
-
if (result.code !== 0 || !result.stdout.trim()) return undefined;
|
|
52
|
-
return parseCredential(result.stdout.trim());
|
|
53
|
-
},
|
|
54
|
-
async save(value) {
|
|
55
|
-
const script = "$p=$env:FARGO_CREDENTIAL_PATH;$d=Split-Path -Parent $p;[IO.Directory]::CreateDirectory($d)|Out-Null;$s=[Console]::In.ReadToEnd();$b=[Text.Encoding]::UTF8.GetBytes($s);$e=[Security.Cryptography.ProtectedData]::Protect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[IO.File]::WriteAllBytes($p,$e)";
|
|
56
|
-
const result = await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { allowFailure: true, env: { ...env, FARGO_CREDENTIAL_PATH: protectedPath }, stdin: JSON.stringify(value) });
|
|
57
|
-
if (result.code !== 0) throw new Error("Unable to save Fargo credentials with Windows DPAPI");
|
|
58
|
-
},
|
|
59
|
-
async clear() {
|
|
60
|
-
await rm(protectedPath, { force: true });
|
|
61
|
-
}
|
|
62
|
-
};
|
|
15
|
+
if (storeKind === "dpapi") {
|
|
16
|
+
throw new Error("FARGO_CREDENTIAL_STORE=dpapi is not supported by the WorkBuddy CLI; use file on Windows");
|
|
63
17
|
}
|
|
18
|
+
if (storeKind && storeKind !== "file") {
|
|
19
|
+
throw new Error(`Unsupported FARGO_CREDENTIAL_STORE: ${storeKind}`);
|
|
20
|
+
}
|
|
21
|
+
const credentialDirectory = platform === "win32"
|
|
22
|
+
? join(env.APPDATA || home, "Fargo AI")
|
|
23
|
+
: join(home, ".fargo");
|
|
24
|
+
const filePath = env.FARGO_CREDENTIAL_PATH ?? join(credentialDirectory, "credentials.json");
|
|
25
|
+
const refreshLockPath = env.FARGO_REFRESH_LOCK_PATH ?? join(credentialDirectory, "oauth-refresh.lock");
|
|
26
|
+
const withRefreshLock = (callback) => withFileLock(refreshLockPath, callback);
|
|
64
27
|
|
|
65
28
|
return {
|
|
66
29
|
kind: "private-file",
|
|
67
30
|
withRefreshLock,
|
|
68
31
|
async load() {
|
|
69
32
|
try {
|
|
33
|
+
await secureExistingFile(filePath);
|
|
70
34
|
return parseCredential(await readFile(filePath, "utf8"));
|
|
71
35
|
} catch (error) {
|
|
72
36
|
if (error?.code === "ENOENT") return undefined;
|
|
@@ -74,17 +38,35 @@ export function createCredentialStore(options = {}) {
|
|
|
74
38
|
}
|
|
75
39
|
},
|
|
76
40
|
async save(value) {
|
|
77
|
-
|
|
78
|
-
await
|
|
41
|
+
const directory = dirname(filePath);
|
|
42
|
+
await ensurePrivateDirectory(directory);
|
|
43
|
+
const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
44
|
+
let handle;
|
|
45
|
+
try {
|
|
46
|
+
handle = await open(temporaryPath, "wx", 0o600);
|
|
47
|
+
await handle.writeFile(`${JSON.stringify(value)}\n`, "utf8");
|
|
48
|
+
await handle.sync();
|
|
49
|
+
await handle.close();
|
|
50
|
+
handle = undefined;
|
|
51
|
+
await chmod(temporaryPath, 0o600);
|
|
52
|
+
await rename(temporaryPath, filePath);
|
|
53
|
+
await secureExistingFile(filePath);
|
|
54
|
+
} finally {
|
|
55
|
+
await handle?.close().catch(() => {});
|
|
56
|
+
await rm(temporaryPath, { force: true });
|
|
57
|
+
}
|
|
79
58
|
},
|
|
80
59
|
async clear() {
|
|
60
|
+
await secureExistingFile(filePath).catch((error) => {
|
|
61
|
+
if (error?.code !== "ENOENT") throw error;
|
|
62
|
+
});
|
|
81
63
|
await rm(filePath, { force: true });
|
|
82
64
|
}
|
|
83
65
|
};
|
|
84
66
|
}
|
|
85
67
|
|
|
86
68
|
async function withFileLock(lockPath, callback) {
|
|
87
|
-
await
|
|
69
|
+
await ensurePrivateDirectory(dirname(lockPath));
|
|
88
70
|
const deadline = Date.now() + 15_000;
|
|
89
71
|
const owner = `${process.pid}:${randomUUID()}`;
|
|
90
72
|
let handle;
|
|
@@ -120,6 +102,32 @@ async function withFileLock(lockPath, callback) {
|
|
|
120
102
|
}
|
|
121
103
|
}
|
|
122
104
|
|
|
105
|
+
async function ensurePrivateDirectory(directoryPath) {
|
|
106
|
+
await mkdir(directoryPath, { recursive: true, mode: 0o700 });
|
|
107
|
+
const details = await lstat(directoryPath);
|
|
108
|
+
if (!details.isDirectory() || details.isSymbolicLink()) {
|
|
109
|
+
throw new Error(`Fargo credential directory is not a private directory: ${directoryPath}`);
|
|
110
|
+
}
|
|
111
|
+
assertCurrentUserOwns(details, "credential directory");
|
|
112
|
+
await chmod(directoryPath, 0o700);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function secureExistingFile(filePath) {
|
|
116
|
+
const details = await lstat(filePath);
|
|
117
|
+
if (!details.isFile() || details.isSymbolicLink()) {
|
|
118
|
+
throw new Error(`Fargo credential path is not a regular file: ${filePath}`);
|
|
119
|
+
}
|
|
120
|
+
assertCurrentUserOwns(details, "credential file");
|
|
121
|
+
await chmod(filePath, 0o600);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function assertCurrentUserOwns(details, label) {
|
|
125
|
+
if (typeof process.getuid !== "function") return;
|
|
126
|
+
if (details.uid !== process.getuid()) {
|
|
127
|
+
throw new Error(`Fargo ${label} is not owned by the current user`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
123
131
|
function parseCredential(value) {
|
|
124
132
|
const parsed = JSON.parse(value);
|
|
125
133
|
if (!parsed || typeof parsed !== "object" || typeof parsed.refresh_token !== "string") {
|
|
@@ -127,20 +135,3 @@ function parseCredential(value) {
|
|
|
127
135
|
}
|
|
128
136
|
return parsed;
|
|
129
137
|
}
|
|
130
|
-
|
|
131
|
-
function runCommand(command, args, options = {}) {
|
|
132
|
-
return new Promise((resolve, reject) => {
|
|
133
|
-
const child = spawn(command, args, { env: options.env ?? process.env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
134
|
-
let stdout = "";
|
|
135
|
-
let stderr = "";
|
|
136
|
-
child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout += chunk; });
|
|
137
|
-
child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr += chunk; });
|
|
138
|
-
child.on("error", reject);
|
|
139
|
-
child.on("close", (code) => {
|
|
140
|
-
const result = { code: code ?? 1, stderr, stdout };
|
|
141
|
-
if (!options.allowFailure && result.code !== 0) reject(new Error(stderr.trim() || `${command} failed`));
|
|
142
|
-
else resolve(result);
|
|
143
|
-
});
|
|
144
|
-
child.stdin.end(options.stdin ?? "");
|
|
145
|
-
});
|
|
146
|
-
}
|
|
@@ -1,22 +1,40 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import { access, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
5
|
import { CLI_VERSION } from "./constants.mjs";
|
|
5
6
|
|
|
6
7
|
const PACKAGE_NAME = "@fargowealth/fargo-ai-cli";
|
|
7
8
|
|
|
8
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();
|
|
9
13
|
const configPath = options.configPath
|
|
10
|
-
??
|
|
11
|
-
?? defaultWorkBuddyConfigPath();
|
|
14
|
+
?? env.FARGO_WORKBUDDY_MCP_CONFIG
|
|
15
|
+
?? defaultWorkBuddyConfigPath(homeDirectory);
|
|
12
16
|
const packageSpec = `${PACKAGE_NAME}@${options.packageVersion ?? CLI_VERSION}`;
|
|
13
17
|
const config = await readConfig(configPath);
|
|
18
|
+
const npxCommand = options.npxCommand ?? (platform === "darwin"
|
|
19
|
+
? await resolveNpxCommand({ env, execPath: options.execPath, platform })
|
|
20
|
+
: "npx");
|
|
14
21
|
const nextServer = {
|
|
15
22
|
type: "stdio",
|
|
16
|
-
command:
|
|
23
|
+
command: npxCommand,
|
|
17
24
|
args: ["-y", packageSpec, "mcp", "serve"],
|
|
18
25
|
timeout: 60000
|
|
19
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
|
+
}
|
|
20
38
|
const previousServer = config.mcpServers?.["fargo-ai"];
|
|
21
39
|
const changed = JSON.stringify(previousServer) !== JSON.stringify(nextServer);
|
|
22
40
|
|
|
@@ -37,6 +55,40 @@ export async function installWorkBuddyMcp(options = {}) {
|
|
|
37
55
|
};
|
|
38
56
|
}
|
|
39
57
|
|
|
58
|
+
export async function resolveNpxCommand(options = {}) {
|
|
59
|
+
const platform = options.platform ?? process.platform;
|
|
60
|
+
const env = options.env ?? process.env;
|
|
61
|
+
const execPath = options.execPath ?? process.execPath;
|
|
62
|
+
const executableName = platform === "win32" ? "npx.cmd" : "npx";
|
|
63
|
+
const pathDelimiter = platform === "win32" ? ";" : ":";
|
|
64
|
+
const candidates = [
|
|
65
|
+
join(dirname(execPath), executableName),
|
|
66
|
+
...String(env.PATH ?? "").split(pathDelimiter).filter(Boolean).map((entry) => join(entry, executableName))
|
|
67
|
+
];
|
|
68
|
+
for (const candidate of candidates) {
|
|
69
|
+
const absoluteCandidate = isAbsolute(candidate) ? candidate : resolve(candidate);
|
|
70
|
+
try {
|
|
71
|
+
await access(absoluteCandidate, platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK);
|
|
72
|
+
return absoluteCandidate;
|
|
73
|
+
} catch {
|
|
74
|
+
// Keep searching for an executable that will also be visible to the GUI host.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
throw new Error("Unable to locate an absolute npx executable for WorkBuddy");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildMacPath(npxCommand) {
|
|
81
|
+
return [...new Set([
|
|
82
|
+
dirname(npxCommand),
|
|
83
|
+
"/opt/homebrew/bin",
|
|
84
|
+
"/usr/local/bin",
|
|
85
|
+
"/usr/bin",
|
|
86
|
+
"/bin",
|
|
87
|
+
"/usr/sbin",
|
|
88
|
+
"/sbin"
|
|
89
|
+
])].join(":");
|
|
90
|
+
}
|
|
91
|
+
|
|
40
92
|
export function defaultWorkBuddyConfigPath(homeDirectory = homedir()) {
|
|
41
93
|
return join(homeDirectory, ".workbuddy-ai", "mcp.json");
|
|
42
94
|
}
|