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