@4yi-dev/cli 0.1.11 → 0.1.13

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/README.md CHANGED
@@ -32,7 +32,15 @@ Both packages install the `4yi` command. The dev package points at `https://xcla
32
32
 
33
33
  `4yi code` installs OpenCode under `~/.4yi/vendor/opencode`, writes an OpenCode config under `~/.4yi/opencode/opencode.json`, and launches OpenCode with a 4YI provider.
34
34
 
35
- `4yi connect claude` and `4yi connect codex` first check for the corresponding local CLI. If it is missing, 4YI asks before installing the official npm package. Pass `--yes` to approve that CLI installation non-interactively. Desktop apps are detected and reported, but are never installed automatically.
35
+ `4yi connect claude` and `4yi connect codex` first check for the corresponding local CLI. If it is missing, 4YI asks before installing the official npm package. Pass `--yes` to approve that CLI installation non-interactively. Desktop apps are detected but are never installed automatically.
36
+
37
+ On macOS, `4yi connect claude` also configures an installed Claude Desktop through its official third-party Gateway mode. The Desktop credential helper reads the current token from `~/.4yi/config.json`; the token is not copied into Claude's configuration. Quit and reopen Claude Desktop only after its active tasks finish. Third-party mode has a separate session list from the normal Claude profile. Run `4yi restore claude`, then reopen Claude Desktop, to return to the previous profile and session list.
38
+
39
+ `--scope project` changes only the project's Claude Code settings and never changes the global Claude Desktop profile.
40
+
41
+ Codex CLI and Codex App share the same Codex home on native Windows (`%USERPROFILE%\\.codex`). After `4yi connect codex`, finish active App tasks, fully quit and reopen the App, and start a new chat so its embedded Codex process loads the new provider. Microsoft Store/AppX installations may not expose a conventional executable path, but that detection does not gate the shared configuration update.
42
+
43
+ The Codex provider uses the official command-backed authentication configuration. Its helper reads the current token from `~/.4yi/config.json`; `config.toml` and the model catalog do not contain the bearer token.
36
44
 
37
45
  For local development:
38
46
 
package/bin/4yi.mjs CHANGED
@@ -11,6 +11,7 @@ if (command === "help" || command === "--help" || command === "-h") {
11
11
  console.log(" 4yi code --model X pin model X as the default for future sessions");
12
12
  console.log(" 4yi connect <claude|codex|all> connect existing coding tools to 4YI");
13
13
  console.log(" --yes install a missing CLI without prompting");
14
+ console.log(" Claude on macOS also configures installed Claude Desktop 3P Gateway mode");
14
15
  console.log(" 4yi status [claude|codex|all] inspect the current connection");
15
16
  console.log(" 4yi restore <claude|codex|all> restore the latest protected config");
16
17
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4yi-dev/cli",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "4YI command-line launcher for OAuth login and OpenCode runtime",
5
5
  "type": "module",
6
6
  "bin": {
package/src/connect.mjs CHANGED
@@ -20,6 +20,8 @@ const CODEX_ROOT_START = "# >>> 4yi-cli codex-root";
20
20
  const CODEX_ROOT_END = "# <<< 4yi-cli codex-root";
21
21
  const CODEX_PROVIDER_START = "# >>> 4yi-cli codex-provider";
22
22
  const CODEX_PROVIDER_END = "# <<< 4yi-cli codex-provider";
23
+ const CLAUDE_DESKTOP_PROFILE_ID = "4e8e29f6-e0ba-4a2f-8a5f-4e2dcf95f3f0";
24
+ const CLAUDE_DESKTOP_PROFILE_FILE = `${CLAUDE_DESKTOP_PROFILE_ID}.json`;
23
25
  const TOOL_METADATA = {
24
26
  claude: {
25
27
  label: "Claude Code",
@@ -51,22 +53,40 @@ export async function prepareConnectionTools({ target = "all", autoInstall = fal
51
53
  }
52
54
  }
53
55
 
54
- export function connectionPaths({ home = os.homedir(), cwd = process.cwd(), codexHome = process.env.CODEX_HOME } = {}) {
56
+ export function connectionPaths({
57
+ home = os.homedir(),
58
+ cwd = process.cwd(),
59
+ codexHome = process.env.CODEX_HOME,
60
+ platform = process.platform,
61
+ } = {}) {
55
62
  const codexDir = codexHome || path.join(home, ".codex");
56
- return {
63
+ const paths = {
57
64
  claudeUser: path.join(home, ".claude", "settings.json"),
58
65
  claudeProject: path.join(cwd, ".claude", "settings.local.json"),
59
66
  codexConfig: path.join(codexDir, "config.toml"),
60
67
  codexCatalog: path.join(codexDir, "model-catalogs", "4yi.json"),
68
+ codexCredentialHelper: path.join(home, ".4yi", "helpers", "codex-credential.mjs"),
61
69
  };
70
+ if (platform === "darwin") {
71
+ const desktopDir = path.join(home, "Library", "Application Support", "Claude-3p");
72
+ const libraryDir = path.join(desktopDir, "configLibrary");
73
+ Object.assign(paths, {
74
+ claudeDesktopDir: desktopDir,
75
+ claudeDesktopConfig: path.join(desktopDir, "claude_desktop_config.json"),
76
+ claudeDesktopLibraryMeta: path.join(libraryDir, "_meta.json"),
77
+ claudeDesktopProfile: path.join(libraryDir, CLAUDE_DESKTOP_PROFILE_FILE),
78
+ claudeDesktopCredentialHelper: path.join(home, ".4yi", "helpers", "claude-desktop-credential"),
79
+ });
80
+ }
81
+ return paths;
62
82
  }
63
83
 
64
- function atomicWrite(file, value) {
84
+ function atomicWrite(file, value, mode = 0o600) {
65
85
  ensureDir(path.dirname(file));
66
86
  const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
67
- fs.writeFileSync(temp, value, { mode: 0o600 });
87
+ fs.writeFileSync(temp, value, { mode });
68
88
  fs.renameSync(temp, file);
69
- try { fs.chmodSync(file, 0o600); } catch { /* Windows may ignore POSIX modes. */ }
89
+ try { fs.chmodSync(file, mode); } catch { /* Windows may ignore POSIX modes. */ }
70
90
  }
71
91
 
72
92
  function timestamp() {
@@ -135,17 +155,37 @@ async function ensureToolCli(target, {
135
155
  function desktopAppCandidates(target, { home = os.homedir(), platform = process.platform, env = process.env } = {}) {
136
156
  const appName = target === "claude" ? "Claude" : "Codex";
137
157
  if (platform === "darwin") {
138
- return [
158
+ const candidates = [
139
159
  path.join("/Applications", `${appName}.app`),
140
160
  path.join(home, "Applications", `${appName}.app`),
141
161
  ];
162
+ if (target === "codex") {
163
+ candidates.push(
164
+ path.join("/Applications", "ChatGPT.app"),
165
+ path.join(home, "Applications", "ChatGPT.app"),
166
+ );
167
+ }
168
+ return candidates;
142
169
  }
143
170
  if (platform === "win32") {
144
- const localAppData = env.LOCALAPPDATA || path.join(home, "AppData", "Local");
145
- return [
146
- path.join(localAppData, "Programs", appName, `${appName}.exe`),
147
- path.join(localAppData, appName, `${appName}.exe`),
171
+ const localAppData = env.LOCALAPPDATA || path.win32.join(home, "AppData", "Local");
172
+ const candidates = [
173
+ path.win32.join(localAppData, "Programs", appName, `${appName}.exe`),
174
+ path.win32.join(localAppData, appName, `${appName}.exe`),
148
175
  ];
176
+ if (target === "codex") {
177
+ // Microsoft Store/AppX builds keep their mutable runtime under this
178
+ // directory and commonly expose only an execution alias in WindowsApps.
179
+ // Checking both covers the Store build without traversing the protected
180
+ // C:\Program Files\WindowsApps package directory.
181
+ candidates.push(
182
+ path.win32.join(localAppData, "OpenAI", "Codex"),
183
+ path.win32.join(localAppData, "OpenAI", "ChatGPT"),
184
+ path.win32.join(localAppData, "Microsoft", "WindowsApps", "Codex.exe"),
185
+ path.win32.join(localAppData, "Microsoft", "WindowsApps", "ChatGPT.exe"),
186
+ );
187
+ }
188
+ return candidates;
149
189
  }
150
190
  return [];
151
191
  }
@@ -160,6 +200,14 @@ function reportDesktopApp(target, {
160
200
  const meta = TOOL_METADATA[target];
161
201
  const candidates = desktopAppCandidates(target, { home, platform, env });
162
202
  const detected = candidates.some((candidate) => exists(candidate));
203
+ if (target === "codex" && platform === "win32") {
204
+ if (detected) {
205
+ stdout("Codex App detected. 4YI updated the shared Windows Codex home. Finish active tasks, fully quit and reopen the App, then start a new chat to load the new provider.");
206
+ } else {
207
+ stdout("4YI updated the shared Windows Codex home. A Microsoft Store/AppX install may not expose a standard executable path; finish active tasks, fully quit and reopen any running Codex App, then start a new chat.");
208
+ }
209
+ return detected;
210
+ }
163
211
  if (detected) {
164
212
  stdout(`${meta.appLabel} detected. Quit and reopen it to use the new connection.`);
165
213
  } else if (candidates.length > 0) {
@@ -216,6 +264,122 @@ function chooseModel(models, preferred, keyword) {
216
264
  return match || models[0];
217
265
  }
218
266
 
267
+ function readJsonObject(file) {
268
+ if (!fs.existsSync(file) || fs.statSync(file).size === 0) return {};
269
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
270
+ if (!value || Array.isArray(value) || typeof value !== "object") {
271
+ throw new Error(`${file} must contain a JSON object.`);
272
+ }
273
+ return value;
274
+ }
275
+
276
+ function claudeDesktopCredentialHelper() {
277
+ return `#!/bin/sh
278
+ set -eu
279
+
280
+ config_file="$HOME/.4yi/config.json"
281
+ if [ ! -f "$config_file" ]; then
282
+ echo "4YI session not found. Run: 4yi login" >&2
283
+ exit 1
284
+ fi
285
+
286
+ token=$(/usr/bin/plutil -extract token raw -o - "$config_file" 2>/dev/null) || {
287
+ echo "4YI session is invalid. Run: 4yi login" >&2
288
+ exit 1
289
+ }
290
+
291
+ if [ -z "$token" ] || [ "$token" = "null" ]; then
292
+ echo "4YI session is missing a token. Run: 4yi login" >&2
293
+ exit 1
294
+ fi
295
+
296
+ printf '%s' "$token"
297
+ `;
298
+ }
299
+
300
+ function codexCredentialHelper() {
301
+ return `#!/usr/bin/env node
302
+ import fs from "node:fs";
303
+ import os from "node:os";
304
+ import path from "node:path";
305
+
306
+ try {
307
+ const configFile = path.join(os.homedir(), ".4yi", "config.json");
308
+ const config = JSON.parse(fs.readFileSync(configFile, "utf8"));
309
+ const token = typeof config.token === "string" ? config.token.trim() : "";
310
+ if (!token) throw new Error("4YI session is missing a token.");
311
+ process.stdout.write(token);
312
+ } catch (error) {
313
+ process.stderr.write((error.message || "4YI session is invalid.") + " Run: 4yi login\\n");
314
+ process.exit(1);
315
+ }
316
+ `;
317
+ }
318
+
319
+ function configureClaudeDesktop({
320
+ home,
321
+ claudeBaseUrl,
322
+ backupGroup,
323
+ stdout,
324
+ platform = process.platform,
325
+ env = process.env,
326
+ exists = fs.existsSync,
327
+ } = {}) {
328
+ if (platform !== "darwin") return { configured: false, supported: false };
329
+
330
+ const detected = desktopAppCandidates("claude", { home, platform, env }).some((candidate) => exists(candidate));
331
+ if (!detected) {
332
+ stdout("Claude App was not detected in a standard install location. Claude Code is connected; Claude Desktop was not changed.");
333
+ return { configured: false, supported: true, detected: false };
334
+ }
335
+
336
+ let gatewayUrl;
337
+ try {
338
+ gatewayUrl = new URL(claudeBaseUrl);
339
+ } catch {
340
+ stdout(`Claude Desktop was not configured because its Gateway URL is invalid: ${claudeBaseUrl}`);
341
+ return { configured: false, supported: true, detected: true };
342
+ }
343
+ if (gatewayUrl.protocol !== "https:") {
344
+ stdout("Claude Desktop was not configured because official 3P Gateway mode requires an HTTPS endpoint.");
345
+ return { configured: false, supported: true, detected: true };
346
+ }
347
+
348
+ const paths = connectionPaths({ home, platform });
349
+ const files = [
350
+ paths.claudeDesktopConfig,
351
+ paths.claudeDesktopLibraryMeta,
352
+ paths.claudeDesktopProfile,
353
+ paths.claudeDesktopCredentialHelper,
354
+ ];
355
+ for (const file of files) backupFile("claude", file, home, backupGroup);
356
+
357
+ const gateway = gatewayUrl.toString().replace(/\/+$/, "");
358
+ const inference = {
359
+ inferenceProvider: "gateway",
360
+ inferenceGatewayBaseUrl: gateway,
361
+ inferenceCredentialKind: "helper-script",
362
+ inferenceCredentialHelper: paths.claudeDesktopCredentialHelper,
363
+ inferenceGatewayAuthScheme: "bearer",
364
+ disableEssentialTelemetry: true,
365
+ disableNonessentialTelemetry: true,
366
+ };
367
+ const desktopConfig = { ...readJsonObject(paths.claudeDesktopConfig), deploymentMode: "3p", ...inference };
368
+ const libraryMeta = { ...readJsonObject(paths.claudeDesktopLibraryMeta), appliedId: CLAUDE_DESKTOP_PROFILE_ID };
369
+ const profile = { ...readJsonObject(paths.claudeDesktopProfile), ...inference };
370
+
371
+ // Activate last so Claude never observes a selected profile before its
372
+ // credential helper and config-library entry are durable on disk.
373
+ atomicWrite(paths.claudeDesktopCredentialHelper, claudeDesktopCredentialHelper(), 0o700);
374
+ atomicWrite(paths.claudeDesktopProfile, `${JSON.stringify(profile, null, 2)}\n`);
375
+ atomicWrite(paths.claudeDesktopLibraryMeta, `${JSON.stringify(libraryMeta, null, 2)}\n`);
376
+ atomicWrite(paths.claudeDesktopConfig, `${JSON.stringify(desktopConfig, null, 2)}\n`);
377
+
378
+ stdout(`Connected Claude Desktop (official 3P Gateway): ${paths.claudeDesktopProfile}`);
379
+ stdout("Quit and reopen Claude Desktop when no tasks are running. It will use a separate 3P session list; `4yi restore claude` switches back to the previous profile.");
380
+ return { configured: true, supported: true, detected: true, paths };
381
+ }
382
+
219
383
  async function loadClaudeModels(session, claudeBaseUrl) {
220
384
  const base = claudeBaseUrl.replace(/\/+$/, "");
221
385
  const response = await requestJson(base, "/v1/models", { token: session.token });
@@ -224,8 +388,8 @@ async function loadClaudeModels(session, claudeBaseUrl) {
224
388
  return models;
225
389
  }
226
390
 
227
- async function connectClaude({ session, home, cwd, scope, claudeBaseUrl, stdout }) {
228
- const paths = connectionPaths({ home, cwd });
391
+ async function connectClaude({ session, home, cwd, scope, claudeBaseUrl, stdout, tooling = {} }) {
392
+ const paths = connectionPaths({ home, cwd, platform: tooling.platform });
229
393
  const files = scope === "project" ? [paths.claudeProject] : scope === "both" ? [paths.claudeUser, paths.claudeProject] : [paths.claudeUser];
230
394
  const models = await loadClaudeModels(session, claudeBaseUrl);
231
395
  const defaultModel = chooseModel(models, "claude-sonnet-4-6", "sonnet");
@@ -236,11 +400,7 @@ async function connectClaude({ session, home, cwd, scope, claudeBaseUrl, stdout
236
400
  const backupGroup = timestamp();
237
401
  for (const file of files) {
238
402
  const backup = backupFile("claude", file, home, backupGroup);
239
- let data = {};
240
- if (fs.existsSync(file) && fs.statSync(file).size > 0) {
241
- data = JSON.parse(fs.readFileSync(file, "utf8"));
242
- if (!data || Array.isArray(data) || typeof data !== "object") throw new Error(`${file} must contain a JSON object.`);
243
- }
403
+ const data = readJsonObject(file);
244
404
  const env = data.env && !Array.isArray(data.env) && typeof data.env === "object" ? data.env : {};
245
405
  Object.assign(env, {
246
406
  ANTHROPIC_BASE_URL: claudeBaseUrl.replace(/\/+$/, ""),
@@ -260,6 +420,11 @@ async function connectClaude({ session, home, cwd, scope, claudeBaseUrl, stdout
260
420
  stdout(`Backup: ${backup}`);
261
421
  }
262
422
  stdout(`Available Claude models: ${models.join(", ")}`);
423
+ if (scope === "project") {
424
+ stdout("Claude Desktop was not changed because --scope project only configures this project.");
425
+ } else {
426
+ configureClaudeDesktop({ home, claudeBaseUrl, backupGroup, stdout, ...tooling });
427
+ }
263
428
  }
264
429
 
265
430
  function removeManagedBlock(text, start, end) {
@@ -377,7 +542,8 @@ async function checkCodex(session, codexBaseUrl, model) {
377
542
  }
378
543
 
379
544
  async function connectCodex({ session, home, codexHome, codexBaseUrl, stdout, skipCheck = false, tooling = {} }) {
380
- const paths = connectionPaths({ home, codexHome });
545
+ const platform = tooling.platform || process.platform;
546
+ const paths = connectionPaths({ home, codexHome, platform });
381
547
  const { models, defaultModel } = await loadCodexModels(session);
382
548
  if (!skipCheck) await checkCodex(session, codexBaseUrl, defaultModel);
383
549
  ensureDir(path.dirname(paths.codexConfig));
@@ -385,17 +551,26 @@ async function connectCodex({ session, home, codexHome, codexBaseUrl, stdout, sk
385
551
  const catalog = buildCodexCatalog(models, template);
386
552
  atomicWrite(paths.codexCatalog, `${JSON.stringify(catalog, null, 2)}\n`);
387
553
 
388
- const backup = backupFile("codex", paths.codexConfig, home);
554
+ const backupGroup = timestamp();
555
+ const backup = backupFile("codex", paths.codexConfig, home, backupGroup);
556
+ const helperBackup = backupFile("codex", paths.codexCredentialHelper, home, backupGroup);
389
557
  let existing = fs.existsSync(paths.codexConfig) ? fs.readFileSync(paths.codexConfig, "utf8") : "";
390
558
  existing = removeManagedBlock(existing, CODEX_ROOT_START, CODEX_ROOT_END);
391
559
  existing = removeManagedBlock(existing, CODEX_PROVIDER_START, CODEX_PROVIDER_END);
392
560
  existing = removeCodexRootAssignments(existing);
393
561
  const root = `${CODEX_ROOT_START}\nmodel = ${JSON.stringify(defaultModel)}\nmodel_provider = "4yi"\nmodel_catalog_json = ${JSON.stringify(paths.codexCatalog)}\n${CODEX_ROOT_END}`;
394
- const provider = `${CODEX_PROVIDER_START}\n[model_providers."4yi"]\nname = "4YI Gateway"\nbase_url = ${JSON.stringify(codexBaseUrl.replace(/\/+$/, ""))}\nwire_api = "responses"\nexperimental_bearer_token = ${JSON.stringify(session.token)}\n${CODEX_PROVIDER_END}`;
562
+ const nodeExecutable = tooling.nodeExecutable || process.execPath;
563
+ const provider = `${CODEX_PROVIDER_START}\n[model_providers."4yi"]\nname = "4YI Gateway"\nbase_url = ${JSON.stringify(codexBaseUrl.replace(/\/+$/, ""))}\nwire_api = "responses"\n\n[model_providers."4yi".auth]\ncommand = ${JSON.stringify(nodeExecutable)}\nargs = [${JSON.stringify(paths.codexCredentialHelper)}]\ntimeout_ms = 5000\n${CODEX_PROVIDER_END}`;
564
+ // Codex loads the provider when its process starts. Make the command-backed
565
+ // credential durable before activating the provider in config.toml, and
566
+ // never copy the bearer token into Codex-owned files.
567
+ atomicWrite(paths.codexCredentialHelper, codexCredentialHelper(), 0o700);
395
568
  atomicWrite(paths.codexConfig, `${root}\n\n${existing ? `${existing}\n\n` : ""}${provider}\n`);
396
569
  stdout(`Connected Codex: ${paths.codexConfig}`);
570
+ stdout(`Codex credential helper: ${paths.codexCredentialHelper}`);
397
571
  stdout(`Available Codex models: ${models.map((model) => model.id).join(", ")}`);
398
572
  stdout(`Backup: ${backup}`);
573
+ stdout(`Backup: ${helperBackup}`);
399
574
  }
400
575
 
401
576
  function resolveUrls(session, options) {
@@ -416,8 +591,7 @@ export async function connect({ target = "all", session, home = os.homedir(), cw
416
591
  await prepareConnectionTools({ target: normalized, autoInstall: options.autoInstall, stdout, tooling });
417
592
  }
418
593
  if (normalized === "claude" || normalized === "all") {
419
- await connectClaude({ session, home, cwd, scope, claudeBaseUrl: urls.claudeBaseUrl, stdout });
420
- reportDesktopApp("claude", { ...tooling, home, stdout });
594
+ await connectClaude({ session, home, cwd, scope, claudeBaseUrl: urls.claudeBaseUrl, stdout, tooling });
421
595
  }
422
596
  if (normalized === "codex" || normalized === "all") {
423
597
  await connectCodex({ session, home, codexHome, codexBaseUrl: urls.codexBaseUrl, stdout, skipCheck: options.skipCheck, tooling });
@@ -436,12 +610,52 @@ function readClaudeStatus(file) {
436
610
  }
437
611
  }
438
612
 
439
- export function connectionStatus({ target = "all", home = os.homedir(), cwd = process.cwd(), codexHome = process.env.CODEX_HOME, scope = "user", stdout = console.log } = {}) {
613
+ function readClaudeDesktopStatus(paths) {
614
+ const base = {
615
+ config: paths.claudeDesktopConfig,
616
+ profile: paths.claudeDesktopProfile,
617
+ connected: false,
618
+ };
619
+ try {
620
+ const config = readJsonObject(paths.claudeDesktopConfig);
621
+ const meta = readJsonObject(paths.claudeDesktopLibraryMeta);
622
+ const profile = readJsonObject(paths.claudeDesktopProfile);
623
+ const selected = meta.appliedId === CLAUDE_DESKTOP_PROFILE_ID;
624
+ const connected = config.deploymentMode === "3p"
625
+ && selected
626
+ && profile.inferenceProvider === "gateway"
627
+ && profile.inferenceCredentialKind === "helper-script"
628
+ && profile.inferenceGatewayAuthScheme === "bearer"
629
+ && profile.inferenceCredentialHelper === paths.claudeDesktopCredentialHelper
630
+ && fs.existsSync(paths.claudeDesktopCredentialHelper);
631
+ return {
632
+ ...base,
633
+ connected,
634
+ deploymentMode: config.deploymentMode,
635
+ selected,
636
+ baseUrl: profile.inferenceGatewayBaseUrl,
637
+ credential: profile.inferenceCredentialKind,
638
+ };
639
+ } catch (error) {
640
+ return { ...base, error: error.message };
641
+ }
642
+ }
643
+
644
+ export function connectionStatus({
645
+ target = "all",
646
+ home = os.homedir(),
647
+ cwd = process.cwd(),
648
+ codexHome = process.env.CODEX_HOME,
649
+ scope = "user",
650
+ platform = process.platform,
651
+ stdout = console.log,
652
+ } = {}) {
440
653
  const normalized = normalizeTarget(target);
441
- const paths = connectionPaths({ home, cwd, codexHome });
654
+ const paths = connectionPaths({ home, cwd, codexHome, platform });
442
655
  if (normalized === "claude" || normalized === "all") {
443
656
  const files = scope === "project" ? [paths.claudeProject] : scope === "both" ? [paths.claudeUser, paths.claudeProject] : [paths.claudeUser];
444
657
  for (const file of files) stdout(`Claude Code: ${JSON.stringify(readClaudeStatus(file))}`);
658
+ if (paths.claudeDesktopConfig) stdout(`Claude Desktop: ${JSON.stringify(readClaudeDesktopStatus(paths))}`);
445
659
  }
446
660
  if (normalized === "codex" || normalized === "all") {
447
661
  const text = fs.existsSync(paths.codexConfig) ? fs.readFileSync(paths.codexConfig, "utf8") : "";
@@ -483,7 +697,12 @@ export const __testing = {
483
697
  ensureToolCli,
484
698
  executableName,
485
699
  reportDesktopApp,
700
+ configureClaudeDesktop,
701
+ claudeDesktopCredentialHelper,
702
+ codexCredentialHelper,
703
+ readClaudeDesktopStatus,
486
704
  removeManagedBlock,
487
705
  removeCodexRootAssignments,
488
706
  CLAUDE_MANAGED_ENV_KEYS,
707
+ CLAUDE_DESKTOP_PROFILE_ID,
489
708
  };