@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4yi-dev/cli",
3
- "version": "0.1.12",
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
@@ -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
- return [
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
- return [
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 paths = connectionPaths({ home, codexHome });
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 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);
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 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);
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,