@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 +5 -1
- package/bin/4yi.mjs +1 -0
- package/package.json +1 -1
- package/src/connect.mjs +176 -16
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
|
|
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
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({
|
|
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
|
-
|
|
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
|
|
86
|
+
fs.writeFileSync(temp, value, { mode });
|
|
68
87
|
fs.renameSync(temp, file);
|
|
69
|
-
try { fs.chmodSync(file,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|