@4yi-dev/cli 0.1.12 → 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 +4 -0
- package/package.json +1 -1
- package/src/connect.mjs +67 -8
package/README.md
CHANGED
|
@@ -38,6 +38,10 @@ On macOS, `4yi connect claude` also configures an installed Claude Desktop throu
|
|
|
38
38
|
|
|
39
39
|
`--scope project` changes only the project's Claude Code settings and never changes the global Claude Desktop profile.
|
|
40
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.
|
|
44
|
+
|
|
41
45
|
For local development:
|
|
42
46
|
|
|
43
47
|
```bash
|
package/package.json
CHANGED
package/src/connect.mjs
CHANGED
|
@@ -65,6 +65,7 @@ export function connectionPaths({
|
|
|
65
65
|
claudeProject: path.join(cwd, ".claude", "settings.local.json"),
|
|
66
66
|
codexConfig: path.join(codexDir, "config.toml"),
|
|
67
67
|
codexCatalog: path.join(codexDir, "model-catalogs", "4yi.json"),
|
|
68
|
+
codexCredentialHelper: path.join(home, ".4yi", "helpers", "codex-credential.mjs"),
|
|
68
69
|
};
|
|
69
70
|
if (platform === "darwin") {
|
|
70
71
|
const desktopDir = path.join(home, "Library", "Application Support", "Claude-3p");
|
|
@@ -154,17 +155,37 @@ async function ensureToolCli(target, {
|
|
|
154
155
|
function desktopAppCandidates(target, { home = os.homedir(), platform = process.platform, env = process.env } = {}) {
|
|
155
156
|
const appName = target === "claude" ? "Claude" : "Codex";
|
|
156
157
|
if (platform === "darwin") {
|
|
157
|
-
|
|
158
|
+
const candidates = [
|
|
158
159
|
path.join("/Applications", `${appName}.app`),
|
|
159
160
|
path.join(home, "Applications", `${appName}.app`),
|
|
160
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;
|
|
161
169
|
}
|
|
162
170
|
if (platform === "win32") {
|
|
163
|
-
const localAppData = env.LOCALAPPDATA || path.join(home, "AppData", "Local");
|
|
164
|
-
|
|
165
|
-
path.join(localAppData, "Programs", appName, `${appName}.exe`),
|
|
166
|
-
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`),
|
|
167
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;
|
|
168
189
|
}
|
|
169
190
|
return [];
|
|
170
191
|
}
|
|
@@ -179,6 +200,14 @@ function reportDesktopApp(target, {
|
|
|
179
200
|
const meta = TOOL_METADATA[target];
|
|
180
201
|
const candidates = desktopAppCandidates(target, { home, platform, env });
|
|
181
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
|
+
}
|
|
182
211
|
if (detected) {
|
|
183
212
|
stdout(`${meta.appLabel} detected. Quit and reopen it to use the new connection.`);
|
|
184
213
|
} else if (candidates.length > 0) {
|
|
@@ -268,6 +297,25 @@ printf '%s' "$token"
|
|
|
268
297
|
`;
|
|
269
298
|
}
|
|
270
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
|
+
|
|
271
319
|
function configureClaudeDesktop({
|
|
272
320
|
home,
|
|
273
321
|
claudeBaseUrl,
|
|
@@ -494,7 +542,8 @@ async function checkCodex(session, codexBaseUrl, model) {
|
|
|
494
542
|
}
|
|
495
543
|
|
|
496
544
|
async function connectCodex({ session, home, codexHome, codexBaseUrl, stdout, skipCheck = false, tooling = {} }) {
|
|
497
|
-
const
|
|
545
|
+
const platform = tooling.platform || process.platform;
|
|
546
|
+
const paths = connectionPaths({ home, codexHome, platform });
|
|
498
547
|
const { models, defaultModel } = await loadCodexModels(session);
|
|
499
548
|
if (!skipCheck) await checkCodex(session, codexBaseUrl, defaultModel);
|
|
500
549
|
ensureDir(path.dirname(paths.codexConfig));
|
|
@@ -502,17 +551,26 @@ async function connectCodex({ session, home, codexHome, codexBaseUrl, stdout, sk
|
|
|
502
551
|
const catalog = buildCodexCatalog(models, template);
|
|
503
552
|
atomicWrite(paths.codexCatalog, `${JSON.stringify(catalog, null, 2)}\n`);
|
|
504
553
|
|
|
505
|
-
const
|
|
554
|
+
const backupGroup = timestamp();
|
|
555
|
+
const backup = backupFile("codex", paths.codexConfig, home, backupGroup);
|
|
556
|
+
const helperBackup = backupFile("codex", paths.codexCredentialHelper, home, backupGroup);
|
|
506
557
|
let existing = fs.existsSync(paths.codexConfig) ? fs.readFileSync(paths.codexConfig, "utf8") : "";
|
|
507
558
|
existing = removeManagedBlock(existing, CODEX_ROOT_START, CODEX_ROOT_END);
|
|
508
559
|
existing = removeManagedBlock(existing, CODEX_PROVIDER_START, CODEX_PROVIDER_END);
|
|
509
560
|
existing = removeCodexRootAssignments(existing);
|
|
510
561
|
const root = `${CODEX_ROOT_START}\nmodel = ${JSON.stringify(defaultModel)}\nmodel_provider = "4yi"\nmodel_catalog_json = ${JSON.stringify(paths.codexCatalog)}\n${CODEX_ROOT_END}`;
|
|
511
|
-
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);
|
|
512
568
|
atomicWrite(paths.codexConfig, `${root}\n\n${existing ? `${existing}\n\n` : ""}${provider}\n`);
|
|
513
569
|
stdout(`Connected Codex: ${paths.codexConfig}`);
|
|
570
|
+
stdout(`Codex credential helper: ${paths.codexCredentialHelper}`);
|
|
514
571
|
stdout(`Available Codex models: ${models.map((model) => model.id).join(", ")}`);
|
|
515
572
|
stdout(`Backup: ${backup}`);
|
|
573
|
+
stdout(`Backup: ${helperBackup}`);
|
|
516
574
|
}
|
|
517
575
|
|
|
518
576
|
function resolveUrls(session, options) {
|
|
@@ -641,6 +699,7 @@ export const __testing = {
|
|
|
641
699
|
reportDesktopApp,
|
|
642
700
|
configureClaudeDesktop,
|
|
643
701
|
claudeDesktopCredentialHelper,
|
|
702
|
+
codexCredentialHelper,
|
|
644
703
|
readClaudeDesktopStatus,
|
|
645
704
|
removeManagedBlock,
|
|
646
705
|
removeCodexRootAssignments,
|