@fargowealth/fargo-ai-cli 0.4.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
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
+
3
9
  ## 0.4.0
4
10
 
5
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.
package/README.md CHANGED
@@ -13,10 +13,10 @@ The login opens the shared Fargo AI consent page and returns to `http://127.0.0.
13
13
  ## Credential storage
14
14
 
15
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:** DPAPI `CurrentUser`, stored as encrypted bytes under `%APPDATA%\Fargo AI\credentials.bin`.
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
17
  - **WorkBuddy on Linux:** `~/.fargo/credentials.json`.
18
18
 
19
- On macOS and Linux, the credential directory is forced to `0700` and the credential file to `0600`. 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.
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
20
 
21
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
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fargowealth/fargo-ai-cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Fargo AI OAuth and stdio MCP bridge for WorkBuddy",
5
5
  "type": "module",
6
6
  "bin": {
package/src/constants.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const CLI_VERSION = "0.4.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";
@@ -1,7 +1,6 @@
1
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
 
@@ -9,45 +8,23 @@ export function createCredentialStore(options = {}) {
9
8
  const platform = options.platform ?? process.platform;
10
9
  const env = options.env ?? process.env;
11
10
  const home = options.home ?? homedir();
12
- const run = options.run ?? runCommand;
13
11
  const storeKind = env.FARGO_CREDENTIAL_STORE;
14
12
  if (storeKind === "keychain") {
15
13
  throw new Error("FARGO_CREDENTIAL_STORE=keychain is not supported by the WorkBuddy CLI; use file on macOS");
16
14
  }
17
- if (storeKind && !["dpapi", "file"].includes(storeKind)) {
18
- throw new Error(`Unsupported FARGO_CREDENTIAL_STORE: ${storeKind}`);
15
+ if (storeKind === "dpapi") {
16
+ throw new Error("FARGO_CREDENTIAL_STORE=dpapi is not supported by the WorkBuddy CLI; use file on Windows");
19
17
  }
20
- if (storeKind === "dpapi" && platform !== "win32") {
21
- throw new Error("FARGO_CREDENTIAL_STORE=dpapi is only supported on Windows");
18
+ if (storeKind && storeKind !== "file") {
19
+ throw new Error(`Unsupported FARGO_CREDENTIAL_STORE: ${storeKind}`);
22
20
  }
23
- const filePath = env.FARGO_CREDENTIAL_PATH ?? join(home, ".fargo", "credentials.json");
24
- const refreshLockPath = env.FARGO_REFRESH_LOCK_PATH ?? (platform === "win32"
25
- ? join(env.APPDATA || home, "Fargo AI", "oauth-refresh.lock")
26
- : join(home, ".fargo", "oauth-refresh.lock"));
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");
27
26
  const withRefreshLock = (callback) => withFileLock(refreshLockPath, callback);
28
27
 
29
- if (platform === "win32" && storeKind !== "file") {
30
- const protectedPath = env.FARGO_CREDENTIAL_PATH ?? join(env.APPDATA || home, "Fargo AI", "credentials.bin");
31
- return {
32
- kind: "windows-dpapi",
33
- withRefreshLock,
34
- async load() {
35
- 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))";
36
- const result = await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { allowFailure: true, env: { ...env, FARGO_CREDENTIAL_PATH: protectedPath } });
37
- if (result.code !== 0 || !result.stdout.trim()) return undefined;
38
- return parseCredential(result.stdout.trim());
39
- },
40
- async save(value) {
41
- 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)";
42
- const result = await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { allowFailure: true, env: { ...env, FARGO_CREDENTIAL_PATH: protectedPath }, stdin: JSON.stringify(value) });
43
- if (result.code !== 0) throw new Error("Unable to save Fargo credentials with Windows DPAPI");
44
- },
45
- async clear() {
46
- await rm(protectedPath, { force: true });
47
- }
48
- };
49
- }
50
-
51
28
  return {
52
29
  kind: "private-file",
53
30
  withRefreshLock,
@@ -158,20 +135,3 @@ function parseCredential(value) {
158
135
  }
159
136
  return parsed;
160
137
  }
161
-
162
- function runCommand(command, args, options = {}) {
163
- return new Promise((resolve, reject) => {
164
- const child = spawn(command, args, { env: options.env ?? process.env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
165
- let stdout = "";
166
- let stderr = "";
167
- child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout += chunk; });
168
- child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr += chunk; });
169
- child.on("error", reject);
170
- child.on("close", (code) => {
171
- const result = { code: code ?? 1, stderr, stdout };
172
- if (!options.allowFailure && result.code !== 0) reject(new Error(stderr.trim() || `${command} failed`));
173
- else resolve(result);
174
- });
175
- child.stdin.end(options.stdin ?? "");
176
- });
177
- }
@@ -24,14 +24,16 @@ export async function installWorkBuddyMcp(options = {}) {
24
24
  args: ["-y", packageSpec, "mcp", "serve"],
25
25
  timeout: 60000
26
26
  };
27
- if (platform === "darwin") {
28
- const credentialDirectory = join(homeDirectory, ".fargo");
27
+ if (platform === "darwin" || platform === "win32") {
28
+ const credentialDirectory = platform === "win32"
29
+ ? join(env.APPDATA || homeDirectory, "Fargo AI")
30
+ : join(homeDirectory, ".fargo");
29
31
  nextServer.env = {
30
32
  FARGO_CREDENTIAL_PATH: join(credentialDirectory, "credentials.json"),
31
33
  FARGO_CREDENTIAL_STORE: "file",
32
- FARGO_REFRESH_LOCK_PATH: join(credentialDirectory, "oauth-refresh.lock"),
33
- PATH: buildMacPath(npxCommand)
34
+ FARGO_REFRESH_LOCK_PATH: join(credentialDirectory, "oauth-refresh.lock")
34
35
  };
36
+ if (platform === "darwin") nextServer.env.PATH = buildMacPath(npxCommand);
35
37
  }
36
38
  const previousServer = config.mcpServers?.["fargo-ai"];
37
39
  const changed = JSON.stringify(previousServer) !== JSON.stringify(nextServer);