sneakoscope 0.9.4 → 0.9.7
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/package.json +1 -1
- package/src/cli/install-helpers.mjs +102 -6
- package/src/cli/main.mjs +17 -5
- package/src/core/fsx.mjs +1 -1
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.7",
|
|
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",
|
|
@@ -95,6 +95,17 @@ async function reportPostinstallCodexLbAuth() {
|
|
|
95
95
|
} else if (reconcile?.status === 'failed') {
|
|
96
96
|
console.log(`codex-lb auth: ChatGPT OAuth reconciliation could not complete (${reconcile.reason || 'unknown'}${reconcile.error ? `: ${reconcile.error}` : ''}). Run \`sks codex-lb repair\` to retry.`);
|
|
97
97
|
}
|
|
98
|
+
if (codexLbAuth.base_url && codexLbAuth.codex_lb?.env_key_configured && canAskYesNo() && process.env.SKS_SKIP_CODEX_LB_KEY_PROMPT !== '1') {
|
|
99
|
+
const changeKey = (await askPostinstallQuestion('codex-lb key changed? Update now? [y/N] ')).trim();
|
|
100
|
+
if (/^(y|yes|예|네|응)$/i.test(changeKey)) {
|
|
101
|
+
const newKey = (await askPostinstallQuestion('New codex-lb API key (sk-clb-...): ')).trim();
|
|
102
|
+
if (newKey) {
|
|
103
|
+
const result = await configureCodexLb({ host: codexLbAuth.base_url, apiKey: newKey });
|
|
104
|
+
if (result.ok) console.log(`codex-lb key updated: ${result.base_url}`);
|
|
105
|
+
else console.log(`codex-lb key update failed: ${result.status}${result.error ? `: ${result.error}` : ''}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
98
109
|
return codexLbAuth;
|
|
99
110
|
}
|
|
100
111
|
|
|
@@ -457,6 +468,7 @@ export async function repairCodexLbAuth(opts = {}) {
|
|
|
457
468
|
codex_lb: status
|
|
458
469
|
};
|
|
459
470
|
}
|
|
471
|
+
await migrateCodexAuthKeyFormat({ home: opts.home });
|
|
460
472
|
const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
|
|
461
473
|
const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
|
|
462
474
|
const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir(), force: true });
|
|
@@ -482,6 +494,7 @@ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
|
|
|
482
494
|
if (process.env.SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH === '1' && !opts.force) return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH=1' };
|
|
483
495
|
const status = await codexLbStatus(opts);
|
|
484
496
|
if (!status.selected && !status.provider_configured && !status.env_file) return { status: 'not_configured', codex_lb: status };
|
|
497
|
+
await migrateCodexAuthKeyFormat({ home: opts.home });
|
|
485
498
|
if (status.ok && (!status.selected || !status.provider_requires_openai_auth)) return repairCodexLbAuth(opts);
|
|
486
499
|
if (!status.ok) {
|
|
487
500
|
if (status.base_url && (status.env_key_configured || status.provider_configured || status.selected || status.env_base_url_configured)) return repairCodexLbAuth(opts);
|
|
@@ -552,6 +565,28 @@ function parseCodexAuthApiKey(text = '') {
|
|
|
552
565
|
}
|
|
553
566
|
}
|
|
554
567
|
|
|
568
|
+
// Migrate auth.json from legacy {"auth_mode":"apikey","key":"..."} to the codex 0.130.0+
|
|
569
|
+
// format {"auth_mode":"apikey","OPENAI_API_KEY":"..."}. Safe: preserves key value, only renames field.
|
|
570
|
+
async function migrateCodexAuthKeyFormat(opts = {}) {
|
|
571
|
+
const home = opts.home || process.env.HOME || os.homedir();
|
|
572
|
+
const authPath = opts.authPath || codexAuthPath(home);
|
|
573
|
+
const text = await readText(authPath, '');
|
|
574
|
+
if (!text.trim()) return { status: 'skipped', reason: 'empty' };
|
|
575
|
+
try {
|
|
576
|
+
const parsed = JSON.parse(text);
|
|
577
|
+
if (parsed?.auth_mode !== 'apikey') return { status: 'skipped', reason: 'not_apikey' };
|
|
578
|
+
if (parsed.OPENAI_API_KEY) return { status: 'skipped', reason: 'already_migrated' };
|
|
579
|
+
const legacyKey = parsed.key || parsed.api_key || parsed.apiKey || parsed.openai_api_key;
|
|
580
|
+
if (!legacyKey) return { status: 'skipped', reason: 'no_key_found' };
|
|
581
|
+
const replacement = `${JSON.stringify({ auth_mode: 'apikey', OPENAI_API_KEY: legacyKey })}\n`;
|
|
582
|
+
await writeTextAtomic(authPath, replacement);
|
|
583
|
+
await fsp.chmod(authPath, 0o600).catch(() => {});
|
|
584
|
+
return { status: 'migrated', auth_path: authPath };
|
|
585
|
+
} catch {
|
|
586
|
+
return { status: 'skipped', reason: 'parse_error' };
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
555
590
|
// When codex-lb is selected with env_key auth AND auth.json also carries a real ChatGPT OAuth
|
|
556
591
|
// token blob, Codex CLI/App can pick the OAuth bearer over the env_key bearer and fail against
|
|
557
592
|
// the load balancer. We back the OAuth blob up to ~/.codex/auth.chatgpt-backup.json and replace
|
|
@@ -597,7 +632,7 @@ export async function reconcileCodexLbAuthConflict(opts = {}) {
|
|
|
597
632
|
};
|
|
598
633
|
}
|
|
599
634
|
try {
|
|
600
|
-
const replacement = `${JSON.stringify({ auth_mode: 'apikey',
|
|
635
|
+
const replacement = `${JSON.stringify({ auth_mode: 'apikey', OPENAI_API_KEY: apiKey }, null, 2)}\n`;
|
|
601
636
|
await writeTextAtomic(authPath, replacement);
|
|
602
637
|
await fsp.chmod(authPath, 0o600).catch(() => {});
|
|
603
638
|
} catch (err) {
|
|
@@ -793,8 +828,34 @@ export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
|
|
|
793
828
|
if (codexLogin.status === 'synced') console.log('codex-lb auth synced with Codex CLI login cache.');
|
|
794
829
|
const chainHealth = await checkCodexLbResponseChain(status, opts);
|
|
795
830
|
if (!chainHealth.ok && chainHealth.chain_unhealthy) {
|
|
796
|
-
|
|
797
|
-
|
|
831
|
+
// `previous_response_not_found` is normal for stateless LB deployments that don't persist
|
|
832
|
+
// Responses across requests. The codex-lb provider still works fine — only the chained
|
|
833
|
+
// health probe fails. Keep codex-lb active and just warn.
|
|
834
|
+
if (chainHealth.status === 'previous_response_not_found') {
|
|
835
|
+
console.log('codex-lb response chain check: previous_response_id not persisted by the load balancer (this is normal for stateless deployments). Keeping codex-lb active.');
|
|
836
|
+
return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth };
|
|
837
|
+
}
|
|
838
|
+
// Hard chain failure (auth rejected, timeout, missing base URL, etc.). Don't silently
|
|
839
|
+
// demote a configured codex-lb to ChatGPT OAuth — surface the failure and let the user
|
|
840
|
+
// decide. Default keeps codex-lb (just press Enter).
|
|
841
|
+
console.log(`codex-lb response chain check failed (${chainHealth.status}${chainHealth.error ? `: ${chainHealth.error}` : ''}).`);
|
|
842
|
+
if (process.env.SKS_CODEX_LB_AUTOBYPASS === '1') {
|
|
843
|
+
console.log('SKS_CODEX_LB_AUTOBYPASS=1 set; bypassing codex-lb to ChatGPT OAuth for this launch.');
|
|
844
|
+
return { status: 'chain_unhealthy', ...status, ok: false, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth, bypass_codex_lb: true };
|
|
845
|
+
}
|
|
846
|
+
if (canAskYesNo()) {
|
|
847
|
+
const answer = (await askPostinstallQuestion('Use codex-lb anyway, or fall back to ChatGPT OAuth? [LB/oauth] ')).trim().toLowerCase();
|
|
848
|
+
if (/^(oauth|o|chatgpt|fall ?back|n|no|아니|아니요|ㄴ)$/.test(answer)) {
|
|
849
|
+
console.log('Falling back to ChatGPT OAuth for this launch. Re-enable codex-lb anytime with `sks codex-lb repair`.');
|
|
850
|
+
return { status: 'chain_unhealthy', ...status, ok: false, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth, bypass_codex_lb: true };
|
|
851
|
+
}
|
|
852
|
+
console.log('Keeping codex-lb active. To switch back to ChatGPT OAuth: `sks codex-lb release`.');
|
|
853
|
+
return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth };
|
|
854
|
+
}
|
|
855
|
+
// Non-interactive context with no opt-out env var. The user explicitly configured codex-lb,
|
|
856
|
+
// so default to keeping it active rather than silently swapping providers.
|
|
857
|
+
console.log('Non-interactive launch + chain check failure. Keeping codex-lb active. Set SKS_CODEX_LB_AUTOBYPASS=1 to auto-bypass to ChatGPT OAuth.');
|
|
858
|
+
return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth };
|
|
798
859
|
}
|
|
799
860
|
return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth };
|
|
800
861
|
}
|
|
@@ -1541,7 +1602,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
1541
1602
|
// NOTE: printf format uses literal double-quotes inside single-quoted shell strings so the
|
|
1542
1603
|
// fake login writes proper JSON in both bash and dash (where `\"` is a non-standard printf
|
|
1543
1604
|
// escape that dash emits literally and bash collapses to `"`).
|
|
1544
|
-
await writeTextAtomic(codexLbFakeCodex, "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo \"codex-cli 99.0.0\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"status\" ]; then echo \"logged in with browser auth\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"--with-api-key\" ]; then read key; mkdir -p \"$HOME/.codex\"; printf '{\"auth_mode\":\"apikey\",\"
|
|
1605
|
+
await writeTextAtomic(codexLbFakeCodex, "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo \"codex-cli 99.0.0\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"status\" ]; then echo \"logged in with browser auth\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"--with-api-key\" ]; then read key; mkdir -p \"$HOME/.codex\"; printf '{\"auth_mode\":\"apikey\",\"OPENAI_API_KEY\":\"%s\"}\\n' \"$key\" > \"$HOME/.codex/auth.json\"; printf '%s\\n' \"$key\" >> \"$HOME/.codex/login-calls.log\"; exit 0; fi\necho \"fake codex unsupported\" >&2\nexit 1\n");
|
|
1545
1606
|
await fsp.chmod(codexLbFakeCodex, 0o755);
|
|
1546
1607
|
await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "low"\nservice_tier = "fast"\n\n[profiles.custom]\nmodel_reasoning_effort = "low"\n\n[notice]\nfast_default_opt_out = true\n\n[features]\ncodex_hooks = true\n');
|
|
1547
1608
|
const codexLbEnvForSelftest = { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global'), PATH: `${codexLbFakeBin}${path.delimiter}${process.env.PATH || ''}`, SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1' };
|
|
@@ -1684,7 +1745,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
1684
1745
|
// the provider stays selected and whether the backup file is removed after restore.
|
|
1685
1746
|
const codexLbReleaseConfig = 'model_provider = "codex-lb"\n\n[model_providers.codex-lb]\nname = "OpenAI"\nbase_url = "https://lb.example.test/backend-api/codex"\nwire_api = "responses"\nenv_key = "CODEX_LB_API_KEY"\nsupports_websockets = true\nrequires_openai_auth = true\n';
|
|
1686
1747
|
const codexLbReleaseEnv = "export CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'\nexport CODEX_LB_API_KEY='sk-test'\n";
|
|
1687
|
-
const codexLbReleaseApikeyAuth = '{"auth_mode":"apikey","
|
|
1748
|
+
const codexLbReleaseApikeyAuth = '{"auth_mode":"apikey","OPENAI_API_KEY":"sk-test"}\n';
|
|
1688
1749
|
const codexLbReleaseOauthBackup = `${oauthAuthJson}\n`;
|
|
1689
1750
|
// Happy path: deselect model_provider and preserve backup file.
|
|
1690
1751
|
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), codexLbReleaseApikeyAuth);
|
|
@@ -1897,7 +1958,42 @@ export async function selftestCodexLb(tmp) {
|
|
|
1897
1958
|
return new Response(JSON.stringify({ error: { type: 'invalid_request_error', code: 'previous_response_not_found', message: 'Previous response not found.', param: 'previous_response_id' } }), { status: 400, headers: { 'content-type': 'application/json' } });
|
|
1898
1959
|
}
|
|
1899
1960
|
});
|
|
1900
|
-
if (nonInteractiveBrokenLaunch.status !== '
|
|
1961
|
+
if (nonInteractiveBrokenLaunch.status !== 'present' || nonInteractiveBrokenLaunch.bypass_codex_lb === true || nonInteractiveBrokenLaunch.chain_health?.status !== 'previous_response_not_found') throw new Error('selftest: previous_response_not_found should keep codex-lb active (stateless LB is normal), not silently bypass to ChatGPT OAuth');
|
|
1962
|
+
// Hard chain failure (e.g. 500) in non-interactive context should still keep codex-lb by default — the user explicitly configured it, so don't silently swap providers.
|
|
1963
|
+
const hardBrokenLaunchCalls = [];
|
|
1964
|
+
const hardBrokenLaunch = await maybePromptCodexLbSetupForLaunch([], {
|
|
1965
|
+
home: codexLbHome,
|
|
1966
|
+
apiKey: 'sk-test',
|
|
1967
|
+
codexBin: path.join(codexLbFakeBin, 'codex'),
|
|
1968
|
+
syncLaunchEnv: false,
|
|
1969
|
+
timeoutMs: 1000,
|
|
1970
|
+
fetch: async (_url, init) => {
|
|
1971
|
+
hardBrokenLaunchCalls.push({ body: JSON.parse(init.body) });
|
|
1972
|
+
if (!hardBrokenLaunchCalls[hardBrokenLaunchCalls.length - 1].body.previous_response_id) return new Response(JSON.stringify({ id: 'resp_hardbroken_first' }), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
1973
|
+
return new Response(JSON.stringify({ error: { type: 'server_error', code: 'internal_error', message: 'simulated upstream failure' } }), { status: 500, headers: { 'content-type': 'application/json' } });
|
|
1974
|
+
}
|
|
1975
|
+
});
|
|
1976
|
+
if (hardBrokenLaunch.status !== 'present' || hardBrokenLaunch.bypass_codex_lb === true || hardBrokenLaunch.chain_health?.status !== 'second_request_failed') throw new Error('selftest: hard codex-lb chain failure in non-interactive launch should default to keeping codex-lb active, not silently bypass');
|
|
1977
|
+
// SKS_CODEX_LB_AUTOBYPASS=1 restores the old silent-bypass behavior for CI/automation.
|
|
1978
|
+
process.env.SKS_CODEX_LB_AUTOBYPASS = '1';
|
|
1979
|
+
let autobypassLaunch;
|
|
1980
|
+
try {
|
|
1981
|
+
autobypassLaunch = await maybePromptCodexLbSetupForLaunch([], {
|
|
1982
|
+
home: codexLbHome,
|
|
1983
|
+
apiKey: 'sk-test',
|
|
1984
|
+
codexBin: path.join(codexLbFakeBin, 'codex'),
|
|
1985
|
+
syncLaunchEnv: false,
|
|
1986
|
+
timeoutMs: 1000,
|
|
1987
|
+
fetch: async (_url, init) => {
|
|
1988
|
+
const body = JSON.parse(init.body);
|
|
1989
|
+
if (!body.previous_response_id) return new Response(JSON.stringify({ id: 'resp_autobypass_first' }), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
1990
|
+
return new Response(JSON.stringify({ error: { type: 'server_error', code: 'internal_error', message: 'simulated upstream failure' } }), { status: 500, headers: { 'content-type': 'application/json' } });
|
|
1991
|
+
}
|
|
1992
|
+
});
|
|
1993
|
+
} finally {
|
|
1994
|
+
delete process.env.SKS_CODEX_LB_AUTOBYPASS;
|
|
1995
|
+
}
|
|
1996
|
+
if (autobypassLaunch.status !== 'chain_unhealthy' || autobypassLaunch.bypass_codex_lb !== true || autobypassLaunch.chain_health?.status !== 'second_request_failed') throw new Error('selftest: SKS_CODEX_LB_AUTOBYPASS=1 should bypass codex-lb on hard chain failure');
|
|
1901
1997
|
await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nservice_tier = "fast"\n');
|
|
1902
1998
|
await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'\nexport CODEX_LB_API_KEY='sk-test'\n");
|
|
1903
1999
|
const missingProviderLaunchCalls = [];
|
package/src/cli/main.mjs
CHANGED
|
@@ -1248,13 +1248,20 @@ async function codexLbCommand(action = 'status', args = []) {
|
|
|
1248
1248
|
return;
|
|
1249
1249
|
}
|
|
1250
1250
|
if (sub === 'setup' || sub === 'reconfigure') {
|
|
1251
|
-
|
|
1252
|
-
|
|
1251
|
+
let host = readOption(args, '--host', readOption(args, '--domain', null));
|
|
1252
|
+
let apiKey = readOption(args, '--api-key', readOption(args, '--key', null));
|
|
1253
1253
|
if (!host || !apiKey) {
|
|
1254
1254
|
if (json) return console.log(JSON.stringify({ ok: false, reason: 'missing_host_or_api_key' }, null, 2));
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1255
|
+
if (!canAskYesNo()) {
|
|
1256
|
+
console.error('Usage: sks codex-lb setup|reconfigure --host <domain> --api-key <key>');
|
|
1257
|
+
process.exitCode = 1;
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
console.log('codex-lb setup — configure your Codex load balancer connection.\n');
|
|
1261
|
+
if (!host) host = (await askPostinstallQuestion('Your codex-lb domain (e.g. https://codex.example.com/backend-api/codex): ')).trim();
|
|
1262
|
+
if (!host) { console.error('Setup cancelled: no domain provided.'); process.exitCode = 1; return; }
|
|
1263
|
+
if (!apiKey) apiKey = (await askPostinstallQuestion('Your codex-lb API key (sk-clb-...): ')).trim();
|
|
1264
|
+
if (!apiKey) { console.error('Setup cancelled: no API key provided.'); process.exitCode = 1; return; }
|
|
1258
1265
|
}
|
|
1259
1266
|
const result = await configureCodexLb({ host, apiKey });
|
|
1260
1267
|
if (json) return console.log(JSON.stringify(result, null, 2));
|
|
@@ -2070,6 +2077,11 @@ function readFlagValue(args, name, fallback) {
|
|
|
2070
2077
|
}
|
|
2071
2078
|
|
|
2072
2079
|
async function selftest() {
|
|
2080
|
+
// Force non-interactive mode for the entire selftest so any in-process call that hits
|
|
2081
|
+
// canAskYesNo() (codex-lb provider-restore prompt, chain-failure prompt, etc.) takes the
|
|
2082
|
+
// non-interactive fallback path instead of bubbling a live readline prompt up to the
|
|
2083
|
+
// user's terminal (e.g. during `npm publish` -> prepublishOnly -> release:check -> selftest).
|
|
2084
|
+
process.env.CI = 'true';
|
|
2073
2085
|
const tmp = tmpdir();
|
|
2074
2086
|
process.chdir(tmp);
|
|
2075
2087
|
await initProject(tmp, {});
|
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.7';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
|