sneakoscope 0.9.8 → 0.9.10
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 +2 -0
- package/package.json +1 -1
- package/src/cli/install-helpers.mjs +256 -46
- package/src/cli/main.mjs +17 -22
- package/src/core/codex-app.mjs +59 -0
- package/src/core/fsx.mjs +1 -1
package/README.md
CHANGED
|
@@ -210,6 +210,8 @@ Flags:
|
|
|
210
210
|
|
|
211
211
|
`sks codex-lb status` reports whether a ChatGPT OAuth backup is present and shows the `sks codex-lb release` hint when applicable. `sks doctor` surfaces the same hint.
|
|
212
212
|
|
|
213
|
+
If Codex App shows `access token could not be refreshed` after codex-lb setup or status checks, recover the ChatGPT OAuth side without discarding codex-lb: run `sks codex-lb status`, then `sks codex-lb repair`. Repair restores a ChatGPT OAuth backup when one exists while keeping `model_provider = "codex-lb"` selected and the codex-lb key in `CODEX_LB_API_KEY`. If no OAuth backup exists, sign in again in Codex App/CLI, then rerun `sks codex-lb repair`. Use `sks codex-lb release` only when you want to switch fully away from codex-lb.
|
|
214
|
+
|
|
213
215
|
If you only want to stop routing through codex-lb without touching `auth.json`, use the lighter `sks codex-lb unselect` instead:
|
|
214
216
|
|
|
215
217
|
```sh
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.10",
|
|
5
5
|
"description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Goal, AutoResearch, TriWiki, and Honest Mode.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
|
|
@@ -9,6 +9,7 @@ import { formatHarnessConflictReport, llmHarnessCleanupPrompt, scanHarnessConfli
|
|
|
9
9
|
import { initProject, installSkills } from '../core/init.mjs';
|
|
10
10
|
import { context7ConfigToml, DOLLAR_SKILL_NAMES, GETDESIGN_REFERENCE, hasContext7ConfigText, RECOMMENDED_SKILLS } from '../core/routes.mjs';
|
|
11
11
|
import { codexLaunchCommand, platformTmuxInstallHint, tmuxReadiness } from '../core/tmux-ui.mjs';
|
|
12
|
+
import { reconcileCodexAppUpgradeProcesses } from '../core/codex-app.mjs';
|
|
12
13
|
|
|
13
14
|
const DEFAULT_CODEX_APP_PLUGINS = [
|
|
14
15
|
['browser', 'openai-bundled'],
|
|
@@ -46,6 +47,11 @@ export async function postinstall({ bootstrap }) {
|
|
|
46
47
|
else if (fastModeRepair.status === 'present') console.log('Codex App Fast mode: config already compatible.');
|
|
47
48
|
else if (fastModeRepair.status === 'skipped') console.log(`Codex App Fast mode: skipped (${fastModeRepair.reason}).`);
|
|
48
49
|
else if (fastModeRepair.status === 'failed') console.log(`Codex App Fast mode: auto repair failed. Run \`sks setup\`. ${fastModeRepair.error || ''}`.trim());
|
|
50
|
+
const appProcessRepair = await reconcileCodexAppUpgradeProcesses();
|
|
51
|
+
if (appProcessRepair.status === 'repaired') console.log(`Codex App reconnect repair: stopped ${appProcessRepair.killed.length} stale orphan app-server process(es). Restart Codex App to reconnect cleanly.`);
|
|
52
|
+
else if (appProcessRepair.status === 'partial') console.log(`Codex App reconnect repair: stopped ${appProcessRepair.killed.length} stale orphan app-server process(es); ${appProcessRepair.failed.length} could not be stopped. Restart Codex App if reconnecting continues.`);
|
|
53
|
+
else if (appProcessRepair.status === 'skipped' && appProcessRepair.reason !== 'platform') console.log(`Codex App reconnect repair: skipped (${appProcessRepair.reason}).`);
|
|
54
|
+
else if (appProcessRepair.status === 'failed') console.log(`Codex App reconnect repair: skipped (${appProcessRepair.error || appProcessRepair.reason || 'process check failed'}).`);
|
|
49
55
|
const globalSkills = await ensureGlobalCodexSkillsDuringInstall();
|
|
50
56
|
if (globalSkills.status === 'installed') {
|
|
51
57
|
const removed = globalSkills.removed_stale_generated_skills || [];
|
|
@@ -88,8 +94,12 @@ async function reportPostinstallCodexLbAuth() {
|
|
|
88
94
|
else if (codexLbAuth.status === 'missing_base_url') console.log('codex-lb auth: stored key has no recoverable base URL. Run `sks codex-lb reconfigure --host <domain> --api-key <key>` once.');
|
|
89
95
|
else if (codexLbAuth.status && codexLbAuth.status !== 'not_configured') console.log(`codex-lb auth: repair skipped (${codexLbAuth.status}${codexLbAuth.error ? `: ${codexLbAuth.error}` : ''}).`);
|
|
90
96
|
const reconcile = codexLbAuth.auth_reconcile;
|
|
91
|
-
if (reconcile?.status === '
|
|
92
|
-
console.log(`codex-lb auth:
|
|
97
|
+
if (reconcile?.status === 'oauth_preserved') {
|
|
98
|
+
console.log(`codex-lb auth: ChatGPT OAuth preserved for Codex App; codex-lb key stays in env_key (OAuth backup at ${reconcile.backup_path}).`);
|
|
99
|
+
} else if (reconcile?.status === 'oauth_restored') {
|
|
100
|
+
console.log(`codex-lb auth: restored ChatGPT OAuth from ${reconcile.backup_path} while keeping codex-lb selected.`);
|
|
101
|
+
} else if (reconcile?.status === 'apikey_forced') {
|
|
102
|
+
console.log(`codex-lb auth: forced API-key auth.json for CLI-only use (OAuth backup at ${reconcile.backup_path}).`);
|
|
93
103
|
} else if (reconcile?.status === 'backup_only') {
|
|
94
104
|
console.log(`codex-lb auth: detected ChatGPT OAuth tokens in auth.json. OAuth backup written to ${reconcile.backup_path}; auth.json left untouched because SKS_CODEX_LB_NO_AUTH_RECONCILE=1.`);
|
|
95
105
|
} else if (reconcile?.status === 'failed') {
|
|
@@ -263,6 +273,9 @@ export async function configureCodexLb(opts = {}) {
|
|
|
263
273
|
await fsp.chmod(envPath, 0o600).catch(() => {});
|
|
264
274
|
const codexEnvironment = await syncCodexLbProviderEnvironment({ env_path: envPath, base_url: baseUrl }, { ...opts, home });
|
|
265
275
|
const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home, force: true });
|
|
276
|
+
const codexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
|
|
277
|
+
const authReconcile = await reconcileCodexLbAuthConflict({ ...opts, home, status: codexLb }).catch((err) => ({ status: 'failed', reason: 'exception', error: err.message }));
|
|
278
|
+
const finalCodexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
|
|
266
279
|
const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
|
|
267
280
|
return {
|
|
268
281
|
ok,
|
|
@@ -271,9 +284,11 @@ export async function configureCodexLb(opts = {}) {
|
|
|
271
284
|
env_path: envPath,
|
|
272
285
|
base_url: baseUrl,
|
|
273
286
|
env_key: 'CODEX_LB_API_KEY',
|
|
287
|
+
auth_reconcile: authReconcile,
|
|
288
|
+
codex_lb: finalCodexLb,
|
|
274
289
|
codex_environment: codexEnvironment,
|
|
275
290
|
codex_login: codexLogin,
|
|
276
|
-
error: codexEnvironment.error || codexLogin.error || null
|
|
291
|
+
error: authReconcile.error || codexEnvironment.error || codexLogin.error || null
|
|
277
292
|
};
|
|
278
293
|
}
|
|
279
294
|
|
|
@@ -284,6 +299,9 @@ export async function codexLbStatus(opts = {}) {
|
|
|
284
299
|
const config = await readText(configPath, '');
|
|
285
300
|
const envExists = await exists(envPath);
|
|
286
301
|
const envText = envExists ? await readText(envPath, '') : '';
|
|
302
|
+
const authPath = opts.authPath || codexAuthPath(home);
|
|
303
|
+
const authText = await readText(authPath, '');
|
|
304
|
+
const authMode = codexAuthModeSummary(authText);
|
|
287
305
|
const envKeyConfigured = Boolean(parseCodexLbEnvKey(envText));
|
|
288
306
|
const providerConfigured = /\[model_providers\.codex-lb\]/.test(config);
|
|
289
307
|
const selected = hasTopLevelCodexLbSelected(config);
|
|
@@ -299,10 +317,52 @@ export async function codexLbStatus(opts = {}) {
|
|
|
299
317
|
env_file: envExists,
|
|
300
318
|
env_key_configured: envKeyConfigured,
|
|
301
319
|
env_base_url_configured: Boolean(parseCodexLbEnvBaseUrl(envText)),
|
|
302
|
-
base_url: baseUrl
|
|
320
|
+
base_url: baseUrl,
|
|
321
|
+
auth_path: authPath,
|
|
322
|
+
auth_mode: authMode.mode,
|
|
323
|
+
auth_usable_for_codex_app: authMode.codex_app_usable,
|
|
324
|
+
auth_summary: authMode.summary
|
|
303
325
|
};
|
|
304
326
|
}
|
|
305
327
|
|
|
328
|
+
export function formatCodexLbStatusText(status = {}, opts = {}) {
|
|
329
|
+
const backupPresent = Boolean(opts.backupPresent);
|
|
330
|
+
const backupPath = opts.backupPath || '';
|
|
331
|
+
const lines = [
|
|
332
|
+
'SKS codex-lb',
|
|
333
|
+
'',
|
|
334
|
+
`Configured: ${status.ok ? 'yes' : 'no'}`,
|
|
335
|
+
`Selected: ${status.selected ? 'yes' : 'no'}`,
|
|
336
|
+
`Provider: ${status.provider_configured ? 'yes' : 'no'}`,
|
|
337
|
+
`Provider requires OpenAI auth: ${status.provider_requires_openai_auth ? 'yes' : 'missing'}`,
|
|
338
|
+
`Codex App auth: ${status.auth_usable_for_codex_app ? 'ok' : 'needs sign-in/repair'} (${status.auth_mode || 'unknown'})`
|
|
339
|
+
];
|
|
340
|
+
if (status.auth_summary) lines.push(`Auth detail: ${status.auth_summary}`);
|
|
341
|
+
lines.push(`Env file: ${status.env_file ? status.env_path : 'missing'}`);
|
|
342
|
+
if (status.base_url) lines.push(`Base URL: ${status.base_url}`);
|
|
343
|
+
lines.push(`ChatGPT backup: ${backupPresent ? `yes (${backupPath})` : 'no'}`);
|
|
344
|
+
if (status.ok && !status.auth_usable_for_codex_app && backupPresent) lines.push('', 'Run: sks codex-lb repair to restore the ChatGPT OAuth backup while keeping codex-lb selected.');
|
|
345
|
+
else if (status.ok && !status.auth_usable_for_codex_app) lines.push('', 'Sign in to Codex App/CLI again, then run: sks codex-lb repair');
|
|
346
|
+
else if (status.ok && !status.selected) lines.push('', 'Run: sks codex-lb repair to activate codex-lb for Codex App.');
|
|
347
|
+
else if (!status.ok && status.base_url && status.env_key_configured) lines.push('', 'Run: sks codex-lb repair to restore the upstream codex-lb provider block.');
|
|
348
|
+
else if (!status.ok) lines.push('', 'Run: sks codex-lb setup --host <domain> --api-key <key>');
|
|
349
|
+
else lines.push('', 'Repair provider auth: sks codex-lb repair');
|
|
350
|
+
if (backupPresent) lines.push('Switch fully away from codex-lb: sks codex-lb release');
|
|
351
|
+
return `${lines.join('\n')}\n`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function formatCodexLbRepairResultText(result = {}) {
|
|
355
|
+
const lines = [
|
|
356
|
+
'codex-lb provider auth repaired for Codex CLI/App environment.',
|
|
357
|
+
`Config: ${result.config_path}`,
|
|
358
|
+
`Key env: ${result.env_path}`
|
|
359
|
+
];
|
|
360
|
+
if (result.auth_reconcile?.status === 'oauth_restored') lines.push(`Codex App auth: ChatGPT OAuth restored from ${result.auth_reconcile.backup_path}.`);
|
|
361
|
+
else if (result.auth_reconcile?.status === 'oauth_preserved') lines.push('Codex App auth: ChatGPT OAuth preserved; codex-lb will use CODEX_LB_API_KEY from env_key.');
|
|
362
|
+
else if (result.auth_reconcile?.status === 'apikey_auth_active') lines.push('Codex App auth: API-key auth.json is still active. Sign in again if the App asks for ChatGPT OAuth.');
|
|
363
|
+
return `${lines.join('\n')}\n`;
|
|
364
|
+
}
|
|
365
|
+
|
|
306
366
|
function codexLbResponsesEndpoint(baseUrl = '') {
|
|
307
367
|
const base = String(baseUrl || '').trim().replace(/\/+$/, '');
|
|
308
368
|
if (!base) return '';
|
|
@@ -313,6 +373,77 @@ function codexLbChainCheckEnabled(env = process.env) {
|
|
|
313
373
|
return env.SKS_CODEX_LB_CHAIN_CHECK !== '0' && env.SKS_SKIP_CODEX_LB_CHAIN_CHECK !== '1';
|
|
314
374
|
}
|
|
315
375
|
|
|
376
|
+
function codexLbChainCachePath(home = process.env.HOME || os.homedir()) {
|
|
377
|
+
return path.join(home, '.codex', 'sks-codex-lb-chain-health.json');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function codexLbChainCacheTtlMs(status = '', env = process.env) {
|
|
381
|
+
const hardFailure = Boolean(status && !['chain_ok', 'previous_response_not_found'].includes(status));
|
|
382
|
+
const key = hardFailure ? 'SKS_CODEX_LB_CHAIN_CHECK_FAILURE_CACHE_TTL_MS' : 'SKS_CODEX_LB_CHAIN_CHECK_CACHE_TTL_MS';
|
|
383
|
+
const fallback = hardFailure ? 30 * 1000 : 5 * 60 * 1000;
|
|
384
|
+
const raw = env[key] ?? env.SKS_CODEX_LB_CHAIN_CHECK_CACHE_TTL_MS;
|
|
385
|
+
if (raw === undefined || raw === '') return fallback;
|
|
386
|
+
const parsed = Number(raw);
|
|
387
|
+
return Number.isFinite(parsed) ? Math.max(0, parsed) : fallback;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function codexLbChainCacheEnabled(opts = {}, env = process.env) {
|
|
391
|
+
if (opts.force || opts.cache === false) return false;
|
|
392
|
+
if (opts.fetch) return false;
|
|
393
|
+
if (env.SKS_CODEX_LB_CHAIN_CHECK_CACHE === '0') return false;
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function readCodexLbChainCache({ endpoint, home, opts = {}, env = process.env } = {}) {
|
|
398
|
+
if (!endpoint || !codexLbChainCacheEnabled(opts, env)) return null;
|
|
399
|
+
const cachePath = opts.cachePath || codexLbChainCachePath(home || env.HOME || os.homedir());
|
|
400
|
+
const text = await readText(cachePath, '');
|
|
401
|
+
if (!text.trim()) return null;
|
|
402
|
+
try {
|
|
403
|
+
const parsed = JSON.parse(text);
|
|
404
|
+
if (parsed?.schema !== 'sks.codex-lb-chain-health.v1' || parsed.endpoint !== endpoint || !parsed.result?.status) return null;
|
|
405
|
+
const now = typeof opts.now === 'function' ? opts.now() : Date.now();
|
|
406
|
+
const checkedAt = Number(parsed.checked_at_ms || 0);
|
|
407
|
+
const ttlMs = codexLbChainCacheTtlMs(parsed.result.status, env);
|
|
408
|
+
if (!checkedAt || ttlMs <= 0 || now - checkedAt > ttlMs) return null;
|
|
409
|
+
return {
|
|
410
|
+
...parsed.result,
|
|
411
|
+
endpoint,
|
|
412
|
+
cached: true,
|
|
413
|
+
cache_path: cachePath,
|
|
414
|
+
cache_age_ms: Math.max(0, now - checkedAt)
|
|
415
|
+
};
|
|
416
|
+
} catch {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function writeCodexLbChainCache(result = {}, { endpoint, home, opts = {}, env = process.env } = {}) {
|
|
422
|
+
if (!endpoint || !result.status || !codexLbChainCacheEnabled(opts, env)) return result;
|
|
423
|
+
const cachePath = opts.cachePath || codexLbChainCachePath(home || env.HOME || os.homedir());
|
|
424
|
+
const now = typeof opts.now === 'function' ? opts.now() : Date.now();
|
|
425
|
+
const cacheResult = {
|
|
426
|
+
ok: Boolean(result.ok),
|
|
427
|
+
status: result.status,
|
|
428
|
+
chain_unhealthy: result.chain_unhealthy === true,
|
|
429
|
+
http_status: result.http_status || null,
|
|
430
|
+
error: result.error || null
|
|
431
|
+
};
|
|
432
|
+
try {
|
|
433
|
+
await ensureDir(path.dirname(cachePath));
|
|
434
|
+
await writeTextAtomic(cachePath, `${JSON.stringify({
|
|
435
|
+
schema: 'sks.codex-lb-chain-health.v1',
|
|
436
|
+
endpoint,
|
|
437
|
+
checked_at_ms: now,
|
|
438
|
+
result: cacheResult
|
|
439
|
+
}, null, 2)}\n`);
|
|
440
|
+
await fsp.chmod(cachePath, 0o600).catch(() => {});
|
|
441
|
+
} catch {
|
|
442
|
+
// Cache writes are a launch optimization only; never block codex-lb startup.
|
|
443
|
+
}
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
446
|
+
|
|
316
447
|
function isPreviousResponseNotFound(payload = {}) {
|
|
317
448
|
const error = payload?.error || payload?.response?.error || payload;
|
|
318
449
|
const text = typeof error === 'string'
|
|
@@ -382,8 +513,11 @@ export async function checkCodexLbResponseChain(status = {}, opts = {}) {
|
|
|
382
513
|
if (!codexLbChainCheckEnabled(env) && !opts.force) return { ok: true, status: 'skipped', skipped: true, reason: 'SKS_CODEX_LB_CHAIN_CHECK=0' };
|
|
383
514
|
const endpoint = codexLbResponsesEndpoint(opts.baseUrl || status.base_url);
|
|
384
515
|
if (!endpoint) return { ok: false, status: 'missing_base_url', chain_unhealthy: true };
|
|
385
|
-
const
|
|
516
|
+
const home = opts.home || env.HOME || os.homedir();
|
|
517
|
+
const apiKey = opts.apiKey || parseCodexLbEnvKey(await readText(opts.envPath || status.env_path || codexLbEnvPath(home), ''));
|
|
386
518
|
if (!apiKey) return { ok: false, status: 'missing_env_key', chain_unhealthy: true };
|
|
519
|
+
const cached = await readCodexLbChainCache({ endpoint, home, opts, env });
|
|
520
|
+
if (cached) return cached;
|
|
387
521
|
const fetchImpl = opts.fetch || globalThis.fetch;
|
|
388
522
|
if (typeof fetchImpl !== 'function') return { ok: true, status: 'skipped', skipped: true, reason: 'fetch unavailable' };
|
|
389
523
|
const model = opts.model || env.SKS_CODEX_MODEL || 'gpt-5.5';
|
|
@@ -400,19 +534,19 @@ export async function checkCodexLbResponseChain(status = {}, opts = {}) {
|
|
|
400
534
|
};
|
|
401
535
|
const first = await fetchCodexLbResponse(fetchImpl, endpoint, apiKey, baseBody, timeoutMs);
|
|
402
536
|
if (!first.ok || !first.response_id) {
|
|
403
|
-
return {
|
|
537
|
+
return writeCodexLbChainCache({
|
|
404
538
|
ok: false,
|
|
405
539
|
status: first.ok ? 'missing_response_id' : 'first_request_failed',
|
|
406
540
|
chain_unhealthy: true,
|
|
407
541
|
endpoint,
|
|
408
542
|
http_status: first.status,
|
|
409
543
|
error: redactSecretText(first.error_payload?.error?.message || first.error_payload?.response?.error?.message || first.text || 'codex-lb first Responses request failed', [apiKey])
|
|
410
|
-
};
|
|
544
|
+
}, { endpoint, home, opts, env });
|
|
411
545
|
}
|
|
412
546
|
const second = await fetchCodexLbResponse(fetchImpl, endpoint, apiKey, { ...baseBody, previous_response_id: first.response_id }, timeoutMs);
|
|
413
|
-
if (second.ok) return { ok: true, status: 'chain_ok', endpoint, response_id: first.response_id, chained_response_id: second.response_id || null, http_status: second.status };
|
|
547
|
+
if (second.ok) return writeCodexLbChainCache({ ok: true, status: 'chain_ok', endpoint, response_id: first.response_id, chained_response_id: second.response_id || null, http_status: second.status }, { endpoint, home, opts, env });
|
|
414
548
|
const previousMissing = isPreviousResponseNotFound(second.error_payload || second.json || second.text);
|
|
415
|
-
return {
|
|
549
|
+
return writeCodexLbChainCache({
|
|
416
550
|
ok: false,
|
|
417
551
|
status: previousMissing ? 'previous_response_not_found' : 'second_request_failed',
|
|
418
552
|
chain_unhealthy: true,
|
|
@@ -420,7 +554,7 @@ export async function checkCodexLbResponseChain(status = {}, opts = {}) {
|
|
|
420
554
|
response_id: first.response_id,
|
|
421
555
|
http_status: second.status,
|
|
422
556
|
error: redactSecretText(second.error_payload?.error?.message || second.error_payload?.response?.error?.message || second.text || 'codex-lb chained Responses request failed', [apiKey])
|
|
423
|
-
};
|
|
557
|
+
}, { endpoint, home, opts, env });
|
|
424
558
|
}
|
|
425
559
|
|
|
426
560
|
function hasTopLevelCodexLbSelected(text = '') {
|
|
@@ -473,6 +607,7 @@ export async function repairCodexLbAuth(opts = {}) {
|
|
|
473
607
|
const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
|
|
474
608
|
const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir(), force: true });
|
|
475
609
|
const authReconcile = await reconcileCodexLbAuthConflict({ ...opts, status }).catch((err) => ({ status: 'failed', reason: 'exception', error: err.message }));
|
|
610
|
+
const finalStatus = await codexLbStatus(opts);
|
|
476
611
|
const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
|
|
477
612
|
return {
|
|
478
613
|
ok,
|
|
@@ -484,7 +619,7 @@ export async function repairCodexLbAuth(opts = {}) {
|
|
|
484
619
|
legacy_auth_migrated: legacyAuthMigrated,
|
|
485
620
|
legacy_auth_path: legacyAuthPath,
|
|
486
621
|
auth_reconcile: authReconcile,
|
|
487
|
-
codex_lb:
|
|
622
|
+
codex_lb: finalStatus,
|
|
488
623
|
codex_environment: codexEnvironment,
|
|
489
624
|
codex_login: codexLogin
|
|
490
625
|
};
|
|
@@ -504,6 +639,7 @@ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
|
|
|
504
639
|
const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
|
|
505
640
|
const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir(), force: true });
|
|
506
641
|
const authReconcile = await reconcileCodexLbAuthConflict({ ...opts, status }).catch((err) => ({ status: 'failed', reason: 'exception', error: err.message }));
|
|
642
|
+
const finalStatus = await codexLbStatus(opts);
|
|
507
643
|
const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
|
|
508
644
|
return {
|
|
509
645
|
ok,
|
|
@@ -511,7 +647,7 @@ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
|
|
|
511
647
|
config_path: status.config_path,
|
|
512
648
|
env_path: status.env_path,
|
|
513
649
|
base_url: status.base_url,
|
|
514
|
-
codex_lb:
|
|
650
|
+
codex_lb: finalStatus,
|
|
515
651
|
codex_environment: codexEnvironment,
|
|
516
652
|
codex_login: codexLogin,
|
|
517
653
|
auth_reconcile: authReconcile,
|
|
@@ -565,6 +701,19 @@ function parseCodexAuthApiKey(text = '') {
|
|
|
565
701
|
}
|
|
566
702
|
}
|
|
567
703
|
|
|
704
|
+
function codexAuthModeSummary(text = '') {
|
|
705
|
+
const raw = String(text || '').trim();
|
|
706
|
+
if (!raw) return { mode: 'missing', codex_app_usable: false, summary: 'missing auth.json' };
|
|
707
|
+
if (hasChatgptOAuthTokens(raw)) return { mode: 'chatgpt_oauth', codex_app_usable: true, summary: 'ChatGPT OAuth token blob present' };
|
|
708
|
+
const apiKey = parseCodexAuthApiKey(raw);
|
|
709
|
+
if (apiKey) return { mode: 'apikey', codex_app_usable: false, summary: 'API-key auth.json; Codex App may require ChatGPT sign-in for requires_openai_auth providers' };
|
|
710
|
+
try {
|
|
711
|
+
const parsed = JSON.parse(raw);
|
|
712
|
+
if (parsed?.auth_mode === 'browser') return { mode: 'browser_marker', codex_app_usable: false, summary: 'browser auth marker without refresh tokens' };
|
|
713
|
+
} catch {}
|
|
714
|
+
return { mode: 'unknown', codex_app_usable: false, summary: 'unrecognized auth.json shape' };
|
|
715
|
+
}
|
|
716
|
+
|
|
568
717
|
// Migrate auth.json from legacy {"auth_mode":"apikey","key":"..."} to the codex 0.130.0+
|
|
569
718
|
// format {"auth_mode":"apikey","OPENAI_API_KEY":"..."}. Safe: preserves key value, only renames field.
|
|
570
719
|
async function migrateCodexAuthKeyFormat(opts = {}) {
|
|
@@ -587,11 +736,10 @@ async function migrateCodexAuthKeyFormat(opts = {}) {
|
|
|
587
736
|
}
|
|
588
737
|
}
|
|
589
738
|
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
// auth.json
|
|
594
|
-
// Opt out with SKS_CODEX_LB_NO_AUTH_RECONCILE=1 (the backup is still produced so nothing is lost).
|
|
739
|
+
// Codex App needs a refreshable ChatGPT OAuth blob when a provider declares
|
|
740
|
+
// requires_openai_auth=true. For codex-lb, the proxy key belongs in env_key
|
|
741
|
+
// (CODEX_LB_API_KEY), so SKS preserves or restores OAuth by default and only
|
|
742
|
+
// writes apikey auth.json when explicitly requested for CLI-only legacy use.
|
|
595
743
|
export async function reconcileCodexLbAuthConflict(opts = {}) {
|
|
596
744
|
const home = opts.home || process.env.HOME || os.homedir();
|
|
597
745
|
const status = opts.status || await codexLbStatus({ ...opts, home });
|
|
@@ -607,42 +755,77 @@ export async function reconcileCodexLbAuthConflict(opts = {}) {
|
|
|
607
755
|
if (!authText.trim()) {
|
|
608
756
|
return { status: 'skipped', reason: 'auth_empty', auth_path: authPath };
|
|
609
757
|
}
|
|
610
|
-
if (!hasChatgptOAuthTokens(authText)) {
|
|
611
|
-
return { status: 'no_oauth_conflict', auth_path: authPath };
|
|
612
|
-
}
|
|
613
758
|
const envText = await readText(status.env_path, '');
|
|
614
759
|
const apiKey = parseCodexLbEnvKey(envText);
|
|
615
760
|
if (!apiKey) {
|
|
616
761
|
return { status: 'skipped', reason: 'missing_env_key', auth_path: authPath };
|
|
617
762
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
763
|
+
if (hasChatgptOAuthTokens(authText)) {
|
|
764
|
+
try {
|
|
765
|
+
await ensureDir(path.dirname(backupPath));
|
|
766
|
+
await writeTextAtomic(backupPath, authText);
|
|
767
|
+
await fsp.chmod(backupPath, 0o600).catch(() => {});
|
|
768
|
+
} catch (err) {
|
|
769
|
+
return { status: 'failed', reason: 'backup_failed', auth_path: authPath, backup_path: backupPath, error: err.message };
|
|
770
|
+
}
|
|
771
|
+
if (process.env.SKS_CODEX_LB_NO_AUTH_RECONCILE === '1' && !opts.force) {
|
|
772
|
+
return {
|
|
773
|
+
status: 'backup_only',
|
|
774
|
+
reason: 'SKS_CODEX_LB_NO_AUTH_RECONCILE=1',
|
|
775
|
+
auth_path: authPath,
|
|
776
|
+
backup_path: backupPath
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
if (process.env.SKS_CODEX_LB_FORCE_APIKEY_AUTH !== '1') {
|
|
780
|
+
return {
|
|
781
|
+
status: 'oauth_preserved',
|
|
782
|
+
reason: 'codex_app_requires_refreshable_oauth',
|
|
783
|
+
auth_path: authPath,
|
|
784
|
+
backup_path: backupPath
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
const replacement = `${JSON.stringify({ auth_mode: 'apikey', OPENAI_API_KEY: apiKey }, null, 2)}\n`;
|
|
789
|
+
await writeTextAtomic(authPath, replacement);
|
|
790
|
+
await fsp.chmod(authPath, 0o600).catch(() => {});
|
|
791
|
+
} catch (err) {
|
|
792
|
+
return { status: 'failed', reason: 'write_failed', auth_path: authPath, backup_path: backupPath, error: err.message };
|
|
793
|
+
}
|
|
627
794
|
return {
|
|
628
|
-
status: '
|
|
629
|
-
reason: '
|
|
795
|
+
status: 'apikey_forced',
|
|
796
|
+
reason: 'SKS_CODEX_LB_FORCE_APIKEY_AUTH=1',
|
|
630
797
|
auth_path: authPath,
|
|
631
798
|
backup_path: backupPath
|
|
632
799
|
};
|
|
633
800
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
await
|
|
638
|
-
|
|
639
|
-
|
|
801
|
+
|
|
802
|
+
const currentApiKey = parseCodexAuthApiKey(authText);
|
|
803
|
+
if (currentApiKey && currentApiKey === apiKey) {
|
|
804
|
+
const backupText = await readText(backupPath, '');
|
|
805
|
+
if (hasChatgptOAuthTokens(backupText) && process.env.SKS_CODEX_LB_KEEP_APIKEY_AUTH !== '1') {
|
|
806
|
+
try {
|
|
807
|
+
const restored = backupText.endsWith('\n') ? backupText : `${backupText}\n`;
|
|
808
|
+
await writeTextAtomic(authPath, restored);
|
|
809
|
+
await fsp.chmod(authPath, 0o600).catch(() => {});
|
|
810
|
+
return {
|
|
811
|
+
status: 'oauth_restored',
|
|
812
|
+
reason: 'restored_chatgpt_oauth_for_codex_app',
|
|
813
|
+
auth_path: authPath,
|
|
814
|
+
backup_path: backupPath
|
|
815
|
+
};
|
|
816
|
+
} catch (err) {
|
|
817
|
+
return { status: 'failed', reason: 'restore_failed', auth_path: authPath, backup_path: backupPath, error: err.message };
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
status: 'apikey_auth_active',
|
|
822
|
+
reason: hasChatgptOAuthTokens(backupText) ? 'SKS_CODEX_LB_KEEP_APIKEY_AUTH=1' : 'chatgpt_oauth_backup_missing',
|
|
823
|
+
auth_path: authPath,
|
|
824
|
+
backup_path: backupPath
|
|
825
|
+
};
|
|
640
826
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
auth_path: authPath,
|
|
644
|
-
backup_path: backupPath
|
|
645
|
-
};
|
|
827
|
+
|
|
828
|
+
return { status: 'no_oauth_conflict', auth_path: authPath };
|
|
646
829
|
}
|
|
647
830
|
|
|
648
831
|
// Expose the ChatGPT OAuth backup path so the CLI can surface it in status / release output.
|
|
@@ -1589,6 +1772,7 @@ function codexLbPostinstallEnv(baseEnv, overrides = {}) {
|
|
|
1589
1772
|
SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
|
|
1590
1773
|
SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0',
|
|
1591
1774
|
SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1',
|
|
1775
|
+
SKS_SKIP_CODEX_APP_UPGRADE_REPAIR: '1',
|
|
1592
1776
|
...overrides
|
|
1593
1777
|
};
|
|
1594
1778
|
}
|
|
@@ -1661,7 +1845,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
1661
1845
|
const codexLbPostinstallAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
1662
1846
|
const codexLbLoginCallsAfterPostinstall = await codexLbLoginCallCount(codexLbHome);
|
|
1663
1847
|
if (!String(codexLbPostinstall.stdout || '').includes('codex-lb auth: preserved') || !codexLbPostinstallAuth.includes('"auth_mode":"browser"') || codexLbPostinstallAuth.includes('sk-test') || codexLbLoginCallsAfterPostinstall !== codexLbLoginCallsBeforePostinstall) throw new Error('selftest: postinstall auth');
|
|
1664
|
-
const postinstallEnvKeys = ['HOME', 'PATH', 'INIT_CWD', 'SKS_GLOBAL_ROOT', 'SKS_POSTINSTALL_BOOTSTRAP', 'SKS_POSTINSTALL_NO_BOOTSTRAP', 'SKS_SKIP_POSTINSTALL_SHIM', 'SKS_SKIP_POSTINSTALL_CONTEXT7', 'SKS_SKIP_POSTINSTALL_GETDESIGN', 'SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS', 'SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH', 'SKS_SKIP_CODEX_LB_LAUNCH_ENV', 'SKS_CODEX_LB_SYNC_CODEX_LOGIN'];
|
|
1848
|
+
const postinstallEnvKeys = ['HOME', 'PATH', 'INIT_CWD', 'SKS_GLOBAL_ROOT', 'SKS_POSTINSTALL_BOOTSTRAP', 'SKS_POSTINSTALL_NO_BOOTSTRAP', 'SKS_SKIP_POSTINSTALL_SHIM', 'SKS_SKIP_POSTINSTALL_CONTEXT7', 'SKS_SKIP_POSTINSTALL_GETDESIGN', 'SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS', 'SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH', 'SKS_SKIP_CODEX_LB_LAUNCH_ENV', 'SKS_SKIP_CODEX_APP_UPGRADE_REPAIR', 'SKS_CODEX_LB_SYNC_CODEX_LOGIN'];
|
|
1665
1849
|
const postinstallEnvBefore = Object.fromEntries(postinstallEnvKeys.map((key) => [key, process.env[key]]));
|
|
1666
1850
|
const codexLbLoginCallsBeforeBootstrap = await codexLbLoginCallCount(codexLbHome);
|
|
1667
1851
|
try {
|
|
@@ -1677,7 +1861,8 @@ export async function selftestCodexLb(tmp) {
|
|
|
1677
1861
|
SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
|
|
1678
1862
|
SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
|
|
1679
1863
|
SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0',
|
|
1680
|
-
SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1'
|
|
1864
|
+
SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1',
|
|
1865
|
+
SKS_SKIP_CODEX_APP_UPGRADE_REPAIR: '1'
|
|
1681
1866
|
});
|
|
1682
1867
|
await postinstall({
|
|
1683
1868
|
bootstrap: async () => {
|
|
@@ -1730,7 +1915,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
1730
1915
|
const codexLbReconcileJson = JSON.parse(codexLbReconcileRepair.stdout);
|
|
1731
1916
|
const codexLbReconcileAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
1732
1917
|
const codexLbReconcileBackup = await safeReadText(path.join(codexLbHome, '.codex', 'auth.chatgpt-backup.json'));
|
|
1733
|
-
if (codexLbReconcileJson.auth_reconcile?.status !== '
|
|
1918
|
+
if (codexLbReconcileJson.auth_reconcile?.status !== 'oauth_preserved' || !codexLbReconcileAuth.includes('oauth-id') || !codexLbReconcileAuth.includes('oauth-refresh') || codexLbReconcileAuth.includes('sk-test') || !codexLbReconcileBackup.includes('oauth-id') || !codexLbReconcileBackup.includes('oauth-refresh')) throw new Error('selftest: codex-lb oauth reconcile should preserve ChatGPT OAuth and back it up');
|
|
1734
1919
|
// Opt-out path: SKS_CODEX_LB_NO_AUTH_RECONCILE=1 keeps auth.json untouched but still backs up the OAuth blob.
|
|
1735
1920
|
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), `${oauthAuthJson}\n`);
|
|
1736
1921
|
await fsp.rm(path.join(codexLbHome, '.codex', 'auth.chatgpt-backup.json'), { force: true });
|
|
@@ -1740,6 +1925,16 @@ export async function selftestCodexLb(tmp) {
|
|
|
1740
1925
|
const codexLbReconcileOptOutAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
1741
1926
|
const codexLbReconcileOptOutBackup = await safeReadText(path.join(codexLbHome, '.codex', 'auth.chatgpt-backup.json'));
|
|
1742
1927
|
if (codexLbReconcileOptOutJson.auth_reconcile?.status !== 'backup_only' || !codexLbReconcileOptOutAuth.includes('oauth-id') || !codexLbReconcileOptOutBackup.includes('oauth-id')) throw new Error('selftest: codex-lb oauth reconcile opt-out should back up but not rewrite auth.json');
|
|
1928
|
+
// Restore path: older SKS versions could leave the codex-lb API key in auth.json. Repair should
|
|
1929
|
+
// restore the ChatGPT OAuth backup while keeping codex-lb selected for provider routing.
|
|
1930
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"apikey","OPENAI_API_KEY":"sk-test"}\n');
|
|
1931
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.chatgpt-backup.json'), `${oauthAuthJson}\n`);
|
|
1932
|
+
const codexLbReconcileRestoreRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'auth', 'repair', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
1933
|
+
if (codexLbReconcileRestoreRepair.code !== 0) throw new Error(`selftest: codex-lb oauth restore repair exited ${codexLbReconcileRestoreRepair.code}: ${codexLbReconcileRestoreRepair.stderr}`);
|
|
1934
|
+
const codexLbReconcileRestoreJson = JSON.parse(codexLbReconcileRestoreRepair.stdout);
|
|
1935
|
+
const codexLbReconcileRestoreAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
1936
|
+
const codexLbReconcileRestoreConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
|
|
1937
|
+
if (codexLbReconcileRestoreJson.auth_reconcile?.status !== 'oauth_restored' || !codexLbReconcileRestoreAuth.includes('oauth-id') || codexLbReconcileRestoreAuth.includes('sk-test') || !hasTopLevelCodexLbSelected(codexLbReconcileRestoreConfig)) throw new Error('selftest: codex-lb oauth restore should replace apikey auth.json with ChatGPT OAuth backup while keeping codex-lb selected');
|
|
1743
1938
|
// codex-lb auth: release flow — restore ChatGPT OAuth from backup so the user can return to
|
|
1744
1939
|
// the official ChatGPT account login. Default deselects model_provider; flags control whether
|
|
1745
1940
|
// the provider stays selected and whether the backup file is removed after restore.
|
|
@@ -1932,7 +2127,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
1932
2127
|
});
|
|
1933
2128
|
if (codexLbNotConfigured.code !== 0 || String(codexLbNotConfigured.stdout || '').includes('codex-lb auth:')) throw new Error('selftest: postinstall should stay quiet when codex-lb is not configured');
|
|
1934
2129
|
const codexLbStatusText = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'status'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
1935
|
-
if (!String(codexLbStatusText.stdout || '').includes('
|
|
2130
|
+
if (!String(codexLbStatusText.stdout || '').includes('Codex App auth:') || !String(codexLbStatusText.stdout || '').includes('sks codex-lb repair')) throw new Error('selftest: codex-lb status did not advertise App auth state and repair command');
|
|
1936
2131
|
const nonInteractiveLaunchChainCalls = [];
|
|
1937
2132
|
const nonInteractiveLaunch = await maybePromptCodexLbSetupForLaunch([], {
|
|
1938
2133
|
home: codexLbHome,
|
|
@@ -2023,6 +2218,21 @@ export async function selftestCodexLb(tmp) {
|
|
|
2023
2218
|
}
|
|
2024
2219
|
);
|
|
2025
2220
|
if (!okChain.ok || okChain.status !== 'chain_ok' || chainCalls.length !== 2 || !String(chainCalls[0].url).endsWith('/backend-api/codex/responses') || chainCalls[1].body.previous_response_id !== 'resp_selftest_1') throw new Error('selftest: codex-lb response chain health check did not verify previous_response_id continuity');
|
|
2221
|
+
const previousGlobalFetch = globalThis.fetch;
|
|
2222
|
+
const cacheCalls = [];
|
|
2223
|
+
const cachePath = path.join(codexLbHome, '.codex', 'chain-cache-selftest.json');
|
|
2224
|
+
try {
|
|
2225
|
+
globalThis.fetch = async (url, init) => {
|
|
2226
|
+
cacheCalls.push({ url, body: JSON.parse(init.body) });
|
|
2227
|
+
return new Response(JSON.stringify({ id: cacheCalls.length === 1 ? 'resp_cache_1' : 'resp_cache_2' }), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
2228
|
+
};
|
|
2229
|
+
const cacheStatus = { base_url: 'https://cache.example.test/backend-api/codex', env_path: path.join(codexLbHome, '.codex', 'sks-codex-lb.env') };
|
|
2230
|
+
const firstCache = await checkCodexLbResponseChain(cacheStatus, { home: codexLbHome, apiKey: 'sk-test', timeoutMs: 1000, cachePath, now: () => 1000 });
|
|
2231
|
+
const secondCache = await checkCodexLbResponseChain(cacheStatus, { home: codexLbHome, apiKey: 'sk-test', timeoutMs: 1000, cachePath, now: () => 2000 });
|
|
2232
|
+
if (!firstCache.ok || firstCache.status !== 'chain_ok' || secondCache.cached !== true || secondCache.status !== 'chain_ok' || cacheCalls.length !== 2) throw new Error('selftest: codex-lb response chain cache did not avoid repeated launch preflight calls');
|
|
2233
|
+
} finally {
|
|
2234
|
+
globalThis.fetch = previousGlobalFetch;
|
|
2235
|
+
}
|
|
2026
2236
|
const brokenChain = await checkCodexLbResponseChain(
|
|
2027
2237
|
{ base_url: 'https://lb.example.test/backend-api/codex', env_path: path.join(codexLbHome, '.codex', 'sks-codex-lb.env') },
|
|
2028
2238
|
{
|
package/src/cli/main.mjs
CHANGED
|
@@ -73,7 +73,7 @@ import { MISTAKE_RECALL_ARTIFACT, contractConsumesMistakeRecall } from '../core/
|
|
|
73
73
|
import { buildPromptContext } from '../core/prompt-context-builder.mjs';
|
|
74
74
|
import { renderTeamDashboardState, writeTeamDashboardState } from '../core/team-dashboard-renderer.mjs';
|
|
75
75
|
import { GOAL_WORKFLOW_ARTIFACT } from '../core/goal-workflow.mjs';
|
|
76
|
-
import { CODEX_APP_DOCS_URL, codexAccessTokenStatus, codexAppIntegrationStatus, formatCodexAppStatus } from '../core/codex-app.mjs';
|
|
76
|
+
import { CODEX_APP_DOCS_URL, codexAccessTokenStatus, codexAppIntegrationStatus, findCodexAppUpgradeRepairTargets, formatCodexAppStatus, parseProcessRows } from '../core/codex-app.mjs';
|
|
77
77
|
import { buildAllFeaturesSelftest, buildFeatureRegistry, validateFeatureRegistry } from '../core/feature-registry.mjs';
|
|
78
78
|
import { codexAppRemoteControlCommand } from './codex-app-command.mjs';
|
|
79
79
|
import { allFeaturesCommand, featuresCommand, hooksCommand, hooksExplainReport } from './feature-commands.mjs';
|
|
@@ -81,7 +81,7 @@ import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs'
|
|
|
81
81
|
import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, defaultCodexLaunchArgs, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, sksAsciiLogo, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchMadTmuxUi, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, reconcileTmuxTeamCockpit, runTmuxStatus, sanitizeTmuxSessionName, sweepCodexLbTmuxSessions, sweepTmuxTeamSurfaces, teamLaneStyle } from '../core/tmux-ui.mjs';
|
|
82
82
|
import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
|
|
83
83
|
import { context7Command } from './context7-command.mjs';
|
|
84
|
-
import { askPostinstallQuestion, checkCodexLbResponseChain, checkContext7, checkRequiredSkills, codexLbChatgptBackupPath, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexFastModeDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, releaseCodexLbAuthHold, repairCodexLbAuth, selftestCodexLb, shouldAutoApproveInstall, unselectCodexLbProvider } from './install-helpers.mjs';
|
|
84
|
+
import { askPostinstallQuestion, checkCodexLbResponseChain, checkContext7, checkRequiredSkills, codexLbChatgptBackupPath, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexFastModeDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, formatCodexLbRepairResultText, formatCodexLbStatusText, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, releaseCodexLbAuthHold, repairCodexLbAuth, selftestCodexLb, shouldAutoApproveInstall, unselectCodexLbProvider } from './install-helpers.mjs';
|
|
85
85
|
import { buildTeamPlan, codeStructureCommand, dbCommand, defaultBeta, defaultVGraph, evalCommand, gcCommand, goalCommand, gxCommand, harnessCommand, hproofCommand, madHighCommand as runMadHighCommand, memoryCommand, migrateWikiContextPack, parseTeamCreateArgs, perfCommand, profileCommand, projectWikiClaims, proofFieldCommand, qaLoopCommand, quickstartCommand, researchCommand, skillDreamCommand, statsCommand, team, teamWorkflowMarkdown, validateArtifactsCommand, wikiCommand, wikiVoxelRowCount, writeWikiContextPack } from './maintenance-commands.mjs';
|
|
86
86
|
import { openClawCommand } from './openclaw-command.mjs';
|
|
87
87
|
import { recallPulseCommand } from './recallpulse-command.mjs';
|
|
@@ -1150,19 +1150,7 @@ async function codexLbCommand(action = 'status', args = []) {
|
|
|
1150
1150
|
const backupPath = codexLbChatgptBackupPath();
|
|
1151
1151
|
const backupPresent = await exists(backupPath);
|
|
1152
1152
|
if (json) return console.log(JSON.stringify({ ...status, chatgpt_backup_present: backupPresent, chatgpt_backup_path: backupPath }, null, 2));
|
|
1153
|
-
|
|
1154
|
-
console.log(`Configured: ${status.ok ? 'yes' : 'no'}`);
|
|
1155
|
-
console.log(`Selected: ${status.selected ? 'yes' : 'no'}`);
|
|
1156
|
-
console.log(`Provider: ${status.provider_configured ? 'yes' : 'no'}`);
|
|
1157
|
-
console.log(`Codex App auth: ${status.provider_requires_openai_auth ? 'yes' : 'missing'}`);
|
|
1158
|
-
console.log(`Env file: ${status.env_file ? status.env_path : 'missing'}`);
|
|
1159
|
-
if (status.base_url) console.log(`Base URL: ${status.base_url}`);
|
|
1160
|
-
console.log(`ChatGPT backup: ${backupPresent ? `yes (${backupPath})` : 'no'}`);
|
|
1161
|
-
if (status.ok && !status.selected) console.log('\nRun: sks codex-lb repair to activate codex-lb for Codex App.');
|
|
1162
|
-
else if (!status.ok && status.base_url && status.env_key_configured) console.log('\nRun: sks codex-lb repair to restore the upstream codex-lb provider block.');
|
|
1163
|
-
else if (!status.ok) console.log('\nRun: sks codex-lb setup --host <domain> --api-key <key>');
|
|
1164
|
-
else console.log('\nRepair provider auth: sks codex-lb repair');
|
|
1165
|
-
if (backupPresent) console.log('Switch back to ChatGPT OAuth login: sks codex-lb release');
|
|
1153
|
+
process.stdout.write(formatCodexLbStatusText(status, { backupPresent, backupPath }));
|
|
1166
1154
|
return;
|
|
1167
1155
|
}
|
|
1168
1156
|
if (sub === 'release') {
|
|
@@ -1248,9 +1236,7 @@ async function codexLbCommand(action = 'status', args = []) {
|
|
|
1248
1236
|
process.exitCode = 1;
|
|
1249
1237
|
return;
|
|
1250
1238
|
}
|
|
1251
|
-
|
|
1252
|
-
console.log(`Config: ${result.config_path}`);
|
|
1253
|
-
console.log(`Key env: ${result.env_path}`);
|
|
1239
|
+
process.stdout.write(formatCodexLbRepairResultText(result));
|
|
1254
1240
|
return;
|
|
1255
1241
|
}
|
|
1256
1242
|
if (sub === 'setup' || sub === 'reconfigure') {
|
|
@@ -2303,11 +2289,11 @@ async function selftest() {
|
|
|
2303
2289
|
await ensureDir(path.join(conflictTmp, '.omx'));
|
|
2304
2290
|
const conflictScan = await scanHarnessConflicts(conflictTmp, { home: path.join(conflictTmp, 'home') });
|
|
2305
2291
|
if (!conflictScan.hard_block || !formatHarnessConflictReport(conflictScan).includes('GPT-5.5')) throw new Error('selftest: OMX conflict did not block with cleanup prompt');
|
|
2306
|
-
const postinstallConflict = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: conflictTmp, env: { INIT_CWD: conflictTmp, HOME: path.join(conflictTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
2292
|
+
const postinstallConflict = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: conflictTmp, env: { INIT_CWD: conflictTmp, HOME: path.join(conflictTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1', SKS_SKIP_CODEX_APP_UPGRADE_REPAIR: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
2307
2293
|
if (postinstallConflict.code !== 0) throw new Error('selftest: postinstall conflict notice should not make npm install fail');
|
|
2308
2294
|
const postinstallConflictOutput = String(`${postinstallConflict.stdout}\n${postinstallConflict.stderr}`);
|
|
2309
2295
|
if (!postinstallConflictOutput.includes('SKS setup is blocked') || postinstallConflictOutput.includes('Cleanup prompt:')) throw new Error('selftest: postinstall conflict notice did not stay informational');
|
|
2310
|
-
const postinstallConflictPrompt = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: conflictTmp, input: 'y\n', env: { INIT_CWD: conflictTmp, HOME: path.join(conflictTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1', SKS_POSTINSTALL_PROMPT: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
2296
|
+
const postinstallConflictPrompt = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: conflictTmp, input: 'y\n', env: { INIT_CWD: conflictTmp, HOME: path.join(conflictTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1', SKS_SKIP_CODEX_APP_UPGRADE_REPAIR: '1', SKS_POSTINSTALL_PROMPT: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
2311
2297
|
if (postinstallConflictPrompt.code !== 0 || !String(postinstallConflictPrompt.stdout || '').includes('Goal: completely remove the conflicting Codex harnesses')) throw new Error('selftest: conflict prompt');
|
|
2312
2298
|
const postinstallSetupTmp = tmpdir();
|
|
2313
2299
|
await writeJsonAtomic(path.join(postinstallSetupTmp, 'package.json'), { name: 'postinstall-setup-smoke', version: '0.0.0' });
|
|
@@ -2317,7 +2303,7 @@ async function selftest() {
|
|
|
2317
2303
|
await ensureDir(path.join(postinstallSetupHome, '.agents', 'skills', name));
|
|
2318
2304
|
await writeTextAtomic(path.join(postinstallSetupHome, '.agents', 'skills', name, 'SKILL.md'), stalePluginSkillContent(name));
|
|
2319
2305
|
}
|
|
2320
|
-
const postinstallSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: postinstallSetupTmp, env: { INIT_CWD: postinstallSetupTmp, HOME: path.join(postinstallSetupTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1', SKS_SKIP_CLI_TOOLS: '1' }, timeoutMs: 30000, maxOutputBytes: 256 * 1024 });
|
|
2306
|
+
const postinstallSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: postinstallSetupTmp, env: { INIT_CWD: postinstallSetupTmp, HOME: path.join(postinstallSetupTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1', SKS_SKIP_CODEX_APP_UPGRADE_REPAIR: '1', SKS_SKIP_CLI_TOOLS: '1' }, timeoutMs: 30000, maxOutputBytes: 256 * 1024 });
|
|
2321
2307
|
if (postinstallSetup.code !== 0) throw new Error(`selftest: postinstall setup exited ${postinstallSetup.code}: ${postinstallSetup.stderr}`);
|
|
2322
2308
|
if (await exists(path.join(postinstallSetupTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'))) throw new Error('selftest: postinstall installed deprecated agent-team fallback skill');
|
|
2323
2309
|
if (!String(postinstallSetup.stdout || '').includes('SKS bootstrap: auto-running sks setup --bootstrap --install-scope global --force') || !String(postinstallSetup.stdout || '').includes('SKS Ready')) throw new Error('selftest: postinstall bootstrap');
|
|
@@ -2355,7 +2341,7 @@ async function selftest() {
|
|
|
2355
2341
|
const postinstallNoMarkerCwd = path.join(postinstallNoMarkerTmp, 'cwd');
|
|
2356
2342
|
const postinstallNoMarkerGlobalRoot = path.join(postinstallNoMarkerTmp, 'global-root');
|
|
2357
2343
|
await ensureDir(postinstallNoMarkerCwd);
|
|
2358
|
-
const postinstallNoMarker = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: postinstallNoMarkerCwd, env: { INIT_CWD: postinstallNoMarkerCwd, HOME: postinstallNoMarkerHome, SKS_GLOBAL_ROOT: postinstallNoMarkerGlobalRoot, SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1', SKS_SKIP_CLI_TOOLS: '1' }, timeoutMs: 30000, maxOutputBytes: 256 * 1024 });
|
|
2344
|
+
const postinstallNoMarker = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: postinstallNoMarkerCwd, env: { INIT_CWD: postinstallNoMarkerCwd, HOME: postinstallNoMarkerHome, SKS_GLOBAL_ROOT: postinstallNoMarkerGlobalRoot, SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1', SKS_SKIP_CODEX_APP_UPGRADE_REPAIR: '1', SKS_SKIP_CLI_TOOLS: '1' }, timeoutMs: 30000, maxOutputBytes: 256 * 1024 });
|
|
2359
2345
|
if (postinstallNoMarker.code !== 0) throw new Error(`selftest: no-marker postinstall bootstrap exited ${postinstallNoMarker.code}: ${postinstallNoMarker.stderr}`);
|
|
2360
2346
|
if (!String(postinstallNoMarker.stdout || '').includes('no project marker found; auto-running global SKS runtime bootstrap')) throw new Error('selftest: no-marker bootstrap');
|
|
2361
2347
|
if (!(await exists(path.join(postinstallNoMarkerGlobalRoot, '.sneakoscope', 'manifest.json')))) throw new Error('selftest: no-marker postinstall did not bootstrap global runtime root');
|
|
@@ -3196,6 +3182,15 @@ async function selftest() {
|
|
|
3196
3182
|
const codexAppFixtureOpts = { codex: { bin: fakeCodex, version: 'codex-cli 99.0.0' }, home: appFeatureTmp, cwd: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } };
|
|
3197
3183
|
const codexAppFeatureStatus = await codexAppIntegrationStatus(codexAppFixtureOpts);
|
|
3198
3184
|
if (!codexAppFeatureStatus.ok || !codexAppFeatureStatus.features?.required_flags_ok || !codexAppFeatureStatus.features?.codex_git_commit || !codexAppFeatureStatus.features?.remote_control || !codexAppFeatureStatus.features?.git_actions?.ok || !codexAppFeatureStatus.features?.fast_mode_config?.ok) throw new Error('selftest: codex-app check did not accept required app feature flags, git actions, remote_control, and unlocked Fast UI config');
|
|
3185
|
+
const codexAppProcessRows = parseProcessRows([
|
|
3186
|
+
'101 1 /Applications/Codex.app/Contents/Resources/codex app-server --analytics-default-enabled',
|
|
3187
|
+
'200 1 /Applications/Codex.app/Contents/MacOS/Codex',
|
|
3188
|
+
'201 200 /Applications/Codex.app/Contents/Resources/codex app-server --analytics-default-enabled',
|
|
3189
|
+
'202 1 /Applications/Codex.app/Contents/Resources/codex app-server --listen stdio://',
|
|
3190
|
+
'203 1 /Users/me/.nvm/versions/node/bin/codex --model gpt-5.5'
|
|
3191
|
+
].join('\n'));
|
|
3192
|
+
const codexAppRepairTargets = findCodexAppUpgradeRepairTargets(codexAppProcessRows);
|
|
3193
|
+
if (codexAppRepairTargets.length !== 1 || codexAppRepairTargets[0].pid !== 101) throw new Error('selftest: Codex App upgrade repair target selection is not limited to orphan desktop app-server processes');
|
|
3199
3194
|
const codexAppOldCliStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodex, version: 'codex-cli 0.129.0' }, home: appFeatureTmp, cwd: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
|
|
3200
3195
|
if (codexAppOldCliStatus.ok || codexAppOldCliStatus.features?.git_actions?.ok || !codexAppOldCliStatus.guidance.some((line) => line.includes('git commit/push actions are blocked'))) throw new Error('selftest: codex-app check did not block commit/push actions on old Codex CLI remote-control');
|
|
3201
3196
|
const missingDefaultPluginTmp = tmpdir();
|
package/src/core/codex-app.mjs
CHANGED
|
@@ -238,6 +238,65 @@ export function codexSupportsRemoteControl(versionText) {
|
|
|
238
238
|
return Boolean(current && compareVersions(current, CODEX_REMOTE_CONTROL_MIN_VERSION) >= 0);
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
export function parseProcessRows(text = '') {
|
|
242
|
+
return String(text || '')
|
|
243
|
+
.split(/\r?\n/)
|
|
244
|
+
.map((line) => line.trim())
|
|
245
|
+
.filter(Boolean)
|
|
246
|
+
.map((line) => {
|
|
247
|
+
const match = line.match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
248
|
+
if (!match) return null;
|
|
249
|
+
return {
|
|
250
|
+
pid: Number.parseInt(match[1], 10),
|
|
251
|
+
ppid: Number.parseInt(match[2], 10),
|
|
252
|
+
command: match[3]
|
|
253
|
+
};
|
|
254
|
+
})
|
|
255
|
+
.filter((row) => Number.isFinite(row?.pid) && Number.isFinite(row?.ppid) && row.command);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function findCodexAppUpgradeRepairTargets(rows = []) {
|
|
259
|
+
return rows.filter((row) => (
|
|
260
|
+
row?.ppid === 1
|
|
261
|
+
&& /\/Codex\.app\/Contents\/Resources\/codex\s+app-server\s+--analytics-default-enabled(?:\s|$)/.test(String(row.command || ''))
|
|
262
|
+
));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function reconcileCodexAppUpgradeProcesses(opts = {}) {
|
|
266
|
+
const platform = opts.platform || process.platform;
|
|
267
|
+
const env = opts.env || process.env;
|
|
268
|
+
if (platform !== 'darwin') return { status: 'skipped', reason: 'platform', killed: [] };
|
|
269
|
+
if (env.SKS_SKIP_CODEX_APP_UPGRADE_REPAIR === '1') return { status: 'skipped', reason: 'SKS_SKIP_CODEX_APP_UPGRADE_REPAIR=1', killed: [] };
|
|
270
|
+
const run = opts.runProcess || runProcess;
|
|
271
|
+
const ps = await run('ps', ['-axo', 'pid=', '-o', 'ppid=', '-o', 'command='], {
|
|
272
|
+
timeoutMs: opts.timeoutMs || 5000,
|
|
273
|
+
maxOutputBytes: opts.maxOutputBytes || 256 * 1024
|
|
274
|
+
}).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
|
|
275
|
+
if (ps.code !== 0) return { status: 'failed', reason: 'ps_failed', error: ps.stderr || ps.stdout || 'ps exited non-zero', killed: [] };
|
|
276
|
+
const rows = parseProcessRows(ps.stdout);
|
|
277
|
+
const targets = findCodexAppUpgradeRepairTargets(rows);
|
|
278
|
+
const killed = [];
|
|
279
|
+
const failed = [];
|
|
280
|
+
for (const target of targets) {
|
|
281
|
+
if (opts.dryRun) {
|
|
282
|
+
killed.push({ pid: target.pid, command: target.command, dry_run: true });
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const kill = await run('kill', ['-TERM', String(target.pid)], {
|
|
286
|
+
timeoutMs: opts.timeoutMs || 5000,
|
|
287
|
+
maxOutputBytes: 8 * 1024
|
|
288
|
+
}).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
|
|
289
|
+
if (kill.code === 0) killed.push({ pid: target.pid, command: target.command });
|
|
290
|
+
else failed.push({ pid: target.pid, command: target.command, error: kill.stderr || kill.stdout || 'kill exited non-zero' });
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
status: failed.length ? 'partial' : killed.length ? 'repaired' : 'clean',
|
|
294
|
+
killed,
|
|
295
|
+
failed,
|
|
296
|
+
checked: rows.length
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
241
300
|
export function formatCodexRemoteControlStatus(status) {
|
|
242
301
|
const lines = [
|
|
243
302
|
'Codex remote-control',
|
package/src/core/fsx.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
|
|
8
|
-
export const PACKAGE_VERSION = '0.9.
|
|
8
|
+
export const PACKAGE_VERSION = '0.9.10';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
|