@4yi-dev/cli 0.1.11 → 0.1.12

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,11 @@ 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.
36
40
 
37
41
  For local development:
38
42
 
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.12",
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,39 @@ 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"),
61
68
  };
69
+ if (platform === "darwin") {
70
+ const desktopDir = path.join(home, "Library", "Application Support", "Claude-3p");
71
+ const libraryDir = path.join(desktopDir, "configLibrary");
72
+ Object.assign(paths, {
73
+ claudeDesktopDir: desktopDir,
74
+ claudeDesktopConfig: path.join(desktopDir, "claude_desktop_config.json"),
75
+ claudeDesktopLibraryMeta: path.join(libraryDir, "_meta.json"),
76
+ claudeDesktopProfile: path.join(libraryDir, CLAUDE_DESKTOP_PROFILE_FILE),
77
+ claudeDesktopCredentialHelper: path.join(home, ".4yi", "helpers", "claude-desktop-credential"),
78
+ });
79
+ }
80
+ return paths;
62
81
  }
63
82
 
64
- function atomicWrite(file, value) {
83
+ function atomicWrite(file, value, mode = 0o600) {
65
84
  ensureDir(path.dirname(file));
66
85
  const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
67
- fs.writeFileSync(temp, value, { mode: 0o600 });
86
+ fs.writeFileSync(temp, value, { mode });
68
87
  fs.renameSync(temp, file);
69
- try { fs.chmodSync(file, 0o600); } catch { /* Windows may ignore POSIX modes. */ }
88
+ try { fs.chmodSync(file, mode); } catch { /* Windows may ignore POSIX modes. */ }
70
89
  }
71
90
 
72
91
  function timestamp() {
@@ -216,6 +235,103 @@ function chooseModel(models, preferred, keyword) {
216
235
  return match || models[0];
217
236
  }
218
237
 
238
+ function readJsonObject(file) {
239
+ if (!fs.existsSync(file) || fs.statSync(file).size === 0) return {};
240
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
241
+ if (!value || Array.isArray(value) || typeof value !== "object") {
242
+ throw new Error(`${file} must contain a JSON object.`);
243
+ }
244
+ return value;
245
+ }
246
+
247
+ function claudeDesktopCredentialHelper() {
248
+ return `#!/bin/sh
249
+ set -eu
250
+
251
+ config_file="$HOME/.4yi/config.json"
252
+ if [ ! -f "$config_file" ]; then
253
+ echo "4YI session not found. Run: 4yi login" >&2
254
+ exit 1
255
+ fi
256
+
257
+ token=$(/usr/bin/plutil -extract token raw -o - "$config_file" 2>/dev/null) || {
258
+ echo "4YI session is invalid. Run: 4yi login" >&2
259
+ exit 1
260
+ }
261
+
262
+ if [ -z "$token" ] || [ "$token" = "null" ]; then
263
+ echo "4YI session is missing a token. Run: 4yi login" >&2
264
+ exit 1
265
+ fi
266
+
267
+ printf '%s' "$token"
268
+ `;
269
+ }
270
+
271
+ function configureClaudeDesktop({
272
+ home,
273
+ claudeBaseUrl,
274
+ backupGroup,
275
+ stdout,
276
+ platform = process.platform,
277
+ env = process.env,
278
+ exists = fs.existsSync,
279
+ } = {}) {
280
+ if (platform !== "darwin") return { configured: false, supported: false };
281
+
282
+ const detected = desktopAppCandidates("claude", { home, platform, env }).some((candidate) => exists(candidate));
283
+ if (!detected) {
284
+ stdout("Claude App was not detected in a standard install location. Claude Code is connected; Claude Desktop was not changed.");
285
+ return { configured: false, supported: true, detected: false };
286
+ }
287
+
288
+ let gatewayUrl;
289
+ try {
290
+ gatewayUrl = new URL(claudeBaseUrl);
291
+ } catch {
292
+ stdout(`Claude Desktop was not configured because its Gateway URL is invalid: ${claudeBaseUrl}`);
293
+ return { configured: false, supported: true, detected: true };
294
+ }
295
+ if (gatewayUrl.protocol !== "https:") {
296
+ stdout("Claude Desktop was not configured because official 3P Gateway mode requires an HTTPS endpoint.");
297
+ return { configured: false, supported: true, detected: true };
298
+ }
299
+
300
+ const paths = connectionPaths({ home, platform });
301
+ const files = [
302
+ paths.claudeDesktopConfig,
303
+ paths.claudeDesktopLibraryMeta,
304
+ paths.claudeDesktopProfile,
305
+ paths.claudeDesktopCredentialHelper,
306
+ ];
307
+ for (const file of files) backupFile("claude", file, home, backupGroup);
308
+
309
+ const gateway = gatewayUrl.toString().replace(/\/+$/, "");
310
+ const inference = {
311
+ inferenceProvider: "gateway",
312
+ inferenceGatewayBaseUrl: gateway,
313
+ inferenceCredentialKind: "helper-script",
314
+ inferenceCredentialHelper: paths.claudeDesktopCredentialHelper,
315
+ inferenceGatewayAuthScheme: "bearer",
316
+ disableEssentialTelemetry: true,
317
+ disableNonessentialTelemetry: true,
318
+ };
319
+ const desktopConfig = { ...readJsonObject(paths.claudeDesktopConfig), deploymentMode: "3p", ...inference };
320
+ const libraryMeta = { ...readJsonObject(paths.claudeDesktopLibraryMeta), appliedId: CLAUDE_DESKTOP_PROFILE_ID };
321
+ const profile = { ...readJsonObject(paths.claudeDesktopProfile), ...inference };
322
+
323
+ // Activate last so Claude never observes a selected profile before its
324
+ // credential helper and config-library entry are durable on disk.
325
+ atomicWrite(paths.claudeDesktopCredentialHelper, claudeDesktopCredentialHelper(), 0o700);
326
+ atomicWrite(paths.claudeDesktopProfile, `${JSON.stringify(profile, null, 2)}\n`);
327
+ atomicWrite(paths.claudeDesktopLibraryMeta, `${JSON.stringify(libraryMeta, null, 2)}\n`);
328
+ atomicWrite(paths.claudeDesktopConfig, `${JSON.stringify(desktopConfig, null, 2)}\n`);
329
+
330
+ stdout(`Connected Claude Desktop (official 3P Gateway): ${paths.claudeDesktopProfile}`);
331
+ 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.");
332
+ return { configured: true, supported: true, detected: true, paths };
333
+ }
334
+
219
335
  async function loadClaudeModels(session, claudeBaseUrl) {
220
336
  const base = claudeBaseUrl.replace(/\/+$/, "");
221
337
  const response = await requestJson(base, "/v1/models", { token: session.token });
@@ -224,8 +340,8 @@ async function loadClaudeModels(session, claudeBaseUrl) {
224
340
  return models;
225
341
  }
226
342
 
227
- async function connectClaude({ session, home, cwd, scope, claudeBaseUrl, stdout }) {
228
- const paths = connectionPaths({ home, cwd });
343
+ async function connectClaude({ session, home, cwd, scope, claudeBaseUrl, stdout, tooling = {} }) {
344
+ const paths = connectionPaths({ home, cwd, platform: tooling.platform });
229
345
  const files = scope === "project" ? [paths.claudeProject] : scope === "both" ? [paths.claudeUser, paths.claudeProject] : [paths.claudeUser];
230
346
  const models = await loadClaudeModels(session, claudeBaseUrl);
231
347
  const defaultModel = chooseModel(models, "claude-sonnet-4-6", "sonnet");
@@ -236,11 +352,7 @@ async function connectClaude({ session, home, cwd, scope, claudeBaseUrl, stdout
236
352
  const backupGroup = timestamp();
237
353
  for (const file of files) {
238
354
  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
- }
355
+ const data = readJsonObject(file);
244
356
  const env = data.env && !Array.isArray(data.env) && typeof data.env === "object" ? data.env : {};
245
357
  Object.assign(env, {
246
358
  ANTHROPIC_BASE_URL: claudeBaseUrl.replace(/\/+$/, ""),
@@ -260,6 +372,11 @@ async function connectClaude({ session, home, cwd, scope, claudeBaseUrl, stdout
260
372
  stdout(`Backup: ${backup}`);
261
373
  }
262
374
  stdout(`Available Claude models: ${models.join(", ")}`);
375
+ if (scope === "project") {
376
+ stdout("Claude Desktop was not changed because --scope project only configures this project.");
377
+ } else {
378
+ configureClaudeDesktop({ home, claudeBaseUrl, backupGroup, stdout, ...tooling });
379
+ }
263
380
  }
264
381
 
265
382
  function removeManagedBlock(text, start, end) {
@@ -416,8 +533,7 @@ export async function connect({ target = "all", session, home = os.homedir(), cw
416
533
  await prepareConnectionTools({ target: normalized, autoInstall: options.autoInstall, stdout, tooling });
417
534
  }
418
535
  if (normalized === "claude" || normalized === "all") {
419
- await connectClaude({ session, home, cwd, scope, claudeBaseUrl: urls.claudeBaseUrl, stdout });
420
- reportDesktopApp("claude", { ...tooling, home, stdout });
536
+ await connectClaude({ session, home, cwd, scope, claudeBaseUrl: urls.claudeBaseUrl, stdout, tooling });
421
537
  }
422
538
  if (normalized === "codex" || normalized === "all") {
423
539
  await connectCodex({ session, home, codexHome, codexBaseUrl: urls.codexBaseUrl, stdout, skipCheck: options.skipCheck, tooling });
@@ -436,12 +552,52 @@ function readClaudeStatus(file) {
436
552
  }
437
553
  }
438
554
 
439
- export function connectionStatus({ target = "all", home = os.homedir(), cwd = process.cwd(), codexHome = process.env.CODEX_HOME, scope = "user", stdout = console.log } = {}) {
555
+ function readClaudeDesktopStatus(paths) {
556
+ const base = {
557
+ config: paths.claudeDesktopConfig,
558
+ profile: paths.claudeDesktopProfile,
559
+ connected: false,
560
+ };
561
+ try {
562
+ const config = readJsonObject(paths.claudeDesktopConfig);
563
+ const meta = readJsonObject(paths.claudeDesktopLibraryMeta);
564
+ const profile = readJsonObject(paths.claudeDesktopProfile);
565
+ const selected = meta.appliedId === CLAUDE_DESKTOP_PROFILE_ID;
566
+ const connected = config.deploymentMode === "3p"
567
+ && selected
568
+ && profile.inferenceProvider === "gateway"
569
+ && profile.inferenceCredentialKind === "helper-script"
570
+ && profile.inferenceGatewayAuthScheme === "bearer"
571
+ && profile.inferenceCredentialHelper === paths.claudeDesktopCredentialHelper
572
+ && fs.existsSync(paths.claudeDesktopCredentialHelper);
573
+ return {
574
+ ...base,
575
+ connected,
576
+ deploymentMode: config.deploymentMode,
577
+ selected,
578
+ baseUrl: profile.inferenceGatewayBaseUrl,
579
+ credential: profile.inferenceCredentialKind,
580
+ };
581
+ } catch (error) {
582
+ return { ...base, error: error.message };
583
+ }
584
+ }
585
+
586
+ export function connectionStatus({
587
+ target = "all",
588
+ home = os.homedir(),
589
+ cwd = process.cwd(),
590
+ codexHome = process.env.CODEX_HOME,
591
+ scope = "user",
592
+ platform = process.platform,
593
+ stdout = console.log,
594
+ } = {}) {
440
595
  const normalized = normalizeTarget(target);
441
- const paths = connectionPaths({ home, cwd, codexHome });
596
+ const paths = connectionPaths({ home, cwd, codexHome, platform });
442
597
  if (normalized === "claude" || normalized === "all") {
443
598
  const files = scope === "project" ? [paths.claudeProject] : scope === "both" ? [paths.claudeUser, paths.claudeProject] : [paths.claudeUser];
444
599
  for (const file of files) stdout(`Claude Code: ${JSON.stringify(readClaudeStatus(file))}`);
600
+ if (paths.claudeDesktopConfig) stdout(`Claude Desktop: ${JSON.stringify(readClaudeDesktopStatus(paths))}`);
445
601
  }
446
602
  if (normalized === "codex" || normalized === "all") {
447
603
  const text = fs.existsSync(paths.codexConfig) ? fs.readFileSync(paths.codexConfig, "utf8") : "";
@@ -483,7 +639,11 @@ export const __testing = {
483
639
  ensureToolCli,
484
640
  executableName,
485
641
  reportDesktopApp,
642
+ configureClaudeDesktop,
643
+ claudeDesktopCredentialHelper,
644
+ readClaudeDesktopStatus,
486
645
  removeManagedBlock,
487
646
  removeCodexRootAssignments,
488
647
  CLAUDE_MANAGED_ENV_KEYS,
648
+ CLAUDE_DESKTOP_PROFILE_ID,
489
649
  };