sneakoscope 0.7.48 → 0.7.49

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
@@ -174,6 +174,7 @@ If you use [codex-lb](https://github.com/Soju06/codex-lb), start it first, creat
174
174
 
175
175
  ```sh
176
176
  sks codex-lb setup --host https://your-codex-lb.example.com --api-key "sk-clb-..."
177
+ sks codex-lb repair
177
178
  sks
178
179
  ```
179
180
 
@@ -183,7 +184,11 @@ Bare `sks` asks this before opening Codex when codex-lb is not configured:
183
184
  Authenticate and route Codex through codex-lb? [y/N]
184
185
  ```
185
186
 
186
- Answering `y` asks for the hosted domain and API key, writes `~/.codex/config.toml`, stores the key in `~/.codex/sks-codex-lb.env` with mode `0600`, syncs Codex CLI API-key auth through `codex login --with-api-key`, and sources that env file before launching Codex in tmux. When codex-lb is configured from this prompt, SKS opens a fresh tmux session for that launch so the new key is loaded by the Codex process immediately. SKS keeps Codex App Fast mode visible and defaulted by writing `service_tier = "fast"`, `[features].fast_mode = true`, and the `sks-fast-high` profile while removing only legacy top-level `model` and `model_reasoning_effort` locks; route-specific reasoning stays in named profiles or explicit tmux launch args. The generated provider config follows the codex-lb README's Codex CLI API-key setup:
187
+ Answering `y` asks for the hosted domain and API key, writes `~/.codex/config.toml`, stores the key in `~/.codex/sks-codex-lb.env` with mode `0600`, syncs Codex CLI API-key auth through `codex login --with-api-key`, and sources that env file before launching Codex in tmux. When codex-lb is configured from this prompt, SKS opens a fresh tmux session for that launch so the new key is loaded by the Codex process immediately. SKS keeps Codex App Fast mode visible and defaulted by writing `service_tier = "fast"`, `[features].fast_mode = true`, and the `sks-fast-high` profile while removing only legacy top-level `model` and `model_reasoning_effort` locks; route-specific reasoning stays in named profiles or explicit tmux launch args.
188
+
189
+ If Codex CLI auth drifts after a tmux/MAD launch, run `sks codex-lb repair` or `sks auth repair`. This reuses the stored `~/.codex/sks-codex-lb.env` key and re-syncs Codex CLI API-key auth without asking for the key again. To replace the key or host, run `sks codex-lb reconfigure --host <domain> --api-key <key>`.
190
+
191
+ The generated provider config follows the codex-lb README's Codex CLI API-key setup:
187
192
 
188
193
  ```toml
189
194
  model_provider = "codex-lb"
@@ -205,7 +210,7 @@ sks --mad
205
210
  sks --mad --yes
206
211
  ```
207
212
 
208
- This creates/uses the `sks-mad-high` Codex profile for a one-shot full-access, high-reasoning tmux session with `sandbox_mode = "danger-full-access"` and `approval_policy = "never"`, opens an active MAD-SKS permission gate for that tmux run, then launches Codex with `--sandbox danger-full-access --ask-for-approval never` and attaches to the session in an interactive terminal. While the gate is active, live server work, Supabase MCP database writes, direct SQL, targeted DML, schema cleanup, and needed migrations are allowed. Catastrophic database wipe/all-row/project-management safeguards remain active. Repeat launches reuse the same named SKS MAD tmux session.
213
+ This syncs existing codex-lb/Codex CLI auth before launch, creates/uses the `sks-mad-high` Codex profile for a one-shot full-access, high-reasoning tmux session with `sandbox_mode = "danger-full-access"` and `approval_policy = "never"`, opens an active MAD-SKS permission gate for that tmux run, then launches Codex with `--sandbox danger-full-access --ask-for-approval never` and attaches to the session in an interactive terminal. If codex-lb is configured and no explicit `--workspace`/`--session` was passed, SKS opens a fresh tmux session so the repaired key is loaded by the Codex process immediately. While the gate is active, live server work, Supabase MCP database writes, direct SQL, targeted DML, schema cleanup, and needed migrations are allowed. Catastrophic database wipe/all-row/project-management safeguards remain active. Repeat launches reuse the same named SKS MAD tmux session unless auth repair requires a fresh codex-lb session.
209
214
 
210
215
  MAD does not disable the pipeline contract: stages, executors, reviewers, and auto-review policy still must not invent unrequested fallback implementation code. If the requested path cannot be implemented, SKS should block with evidence rather than add substitute behavior.
211
216
 
@@ -448,6 +453,7 @@ sks dollar-commands
448
453
 
449
454
  ```sh
450
455
  sks codex-lb setup --host <domain> --api-key <key>
456
+ sks codex-lb repair
451
457
  sks
452
458
  ```
453
459
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.7.48",
4
+ "version": "0.7.49",
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",
@@ -148,7 +148,7 @@ export async function configureCodexLb(opts = {}) {
148
148
  await writeTextAtomic(envPath, `export CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`);
149
149
  await fsp.chmod(envPath, 0o600).catch(() => {});
150
150
  process.env.CODEX_LB_API_KEY = apiKey;
151
- const codexLogin = await syncCodexApiKeyLogin(apiKey, { home });
151
+ const codexLogin = await syncCodexApiKeyLogin(apiKey, { home, force: true });
152
152
  return { ok: true, status: 'configured', config_path: configPath, env_path: envPath, base_url: baseUrl, env_key: 'CODEX_LB_API_KEY', codex_login: codexLogin };
153
153
  }
154
154
 
@@ -174,6 +174,29 @@ export async function codexLbStatus(opts = {}) {
174
174
  };
175
175
  }
176
176
 
177
+ export async function repairCodexLbAuth(opts = {}) {
178
+ const status = await codexLbStatus(opts);
179
+ if (!status.ok) {
180
+ return {
181
+ ok: false,
182
+ status: 'not_configured',
183
+ config_path: status.config_path,
184
+ env_path: status.env_path,
185
+ codex_lb: status
186
+ };
187
+ }
188
+ const codexLogin = await ensureCodexLbLoginFromEnv(status, opts);
189
+ return {
190
+ ok: Boolean(codexLogin.ok),
191
+ status: codexLogin.ok ? 'repaired' : codexLogin.status,
192
+ config_path: status.config_path,
193
+ env_path: status.env_path,
194
+ base_url: status.base_url,
195
+ codex_lb: status,
196
+ codex_login: codexLogin
197
+ };
198
+ }
199
+
177
200
  export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
178
201
  if (args.includes('--json') || args.includes('--skip-codex-lb') || process.env.SKS_SKIP_CODEX_LB_PROMPT === '1') return { status: 'skipped' };
179
202
  if (!canAskYesNo()) return { status: 'non_interactive' };
@@ -198,7 +221,7 @@ async function ensureCodexLbLoginFromEnv(status = {}, opts = {}) {
198
221
  const envPath = opts.envPath || status.env_path || codexLbEnvPath(home);
199
222
  const apiKey = parseCodexLbEnvKey(await readText(envPath, ''));
200
223
  if (!apiKey) return { ok: false, status: 'missing_env_key' };
201
- return syncCodexApiKeyLogin(apiKey, { ...opts, home });
224
+ return syncCodexApiKeyLogin(apiKey, { ...opts, home, force: true });
202
225
  }
203
226
 
204
227
  async function syncCodexApiKeyLogin(apiKey, opts = {}) {
@@ -208,8 +231,10 @@ async function syncCodexApiKeyLogin(apiKey, opts = {}) {
208
231
  if (!codexBin) return { ok: false, status: 'codex_missing' };
209
232
  await ensureDir(codexHome);
210
233
  const env = { HOME: home, CODEX_HOME: codexHome, CODEX_LB_API_KEY: apiKey };
211
- const current = await runProcess(codexBin, ['login', 'status'], { env, timeoutMs: 10000, maxOutputBytes: 8192 });
212
- if (current.code === 0 && !/not logged in/i.test(`${current.stdout}\n${current.stderr}`)) return { ok: true, status: 'present' };
234
+ if (!opts.force) {
235
+ const current = await runProcess(codexBin, ['login', 'status'], { env, timeoutMs: 10000, maxOutputBytes: 8192 });
236
+ if (current.code === 0 && !/not logged in/i.test(`${current.stdout}\n${current.stderr}`)) return { ok: true, status: 'present' };
237
+ }
213
238
  const login = await runProcess(codexBin, ['login', '--with-api-key'], { input: `${apiKey}\n`, env, timeoutMs: 15000, maxOutputBytes: 8192 });
214
239
  if (login.code === 0) return { ok: true, status: 'synced' };
215
240
  return { ok: false, status: 'login_failed', error: (login.stderr || login.stdout || 'codex login failed').trim() };
package/src/cli/main.mjs CHANGED
@@ -14,7 +14,7 @@ import { containsUserQuestion, noQuestionContinuationReason } from '../core/no-q
14
14
  import { evaluateDoneGate, defaultDoneGate } from '../core/hproof.mjs';
15
15
  import { emitHook } from '../core/hooks-runtime.mjs';
16
16
  import { storageReport, enforceRetention, pruneWikiArtifacts } from '../core/retention.mjs';
17
- import { classifySql, classifyCommand, checkDbOperation, handleMadSksUserConfirmation, loadDbSafetyPolicy, scanDbSafety } from '../core/db-safety.mjs';
17
+ import { classifySql, classifyCommand, classifyToolPayload, checkDbOperation, handleMadSksUserConfirmation, loadDbSafetyPolicy, scanDbSafety } from '../core/db-safety.mjs';
18
18
  import { checkHarnessModification, harnessGuardStatus, isHarnessSourceProject } from '../core/harness-guard.mjs';
19
19
  import { formatHarnessConflictReport, llmHarnessCleanupPrompt, scanHarnessConflicts } from '../core/harness-conflicts.mjs';
20
20
  import { context7Docs, context7Resolve, context7Text, context7Tools } from '../core/context7-client.mjs';
@@ -68,7 +68,7 @@ import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs'
68
68
  import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, runTmuxStatus, sanitizeTmuxSessionName, teamLaneStyle } from '../core/tmux-ui.mjs';
69
69
  import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
70
70
  import { context7Command } from './context7-command.mjs';
71
- import { askPostinstallQuestion, checkContext7, checkRequiredSkills, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, shouldAutoApproveInstall } from './install-helpers.mjs';
71
+ import { askPostinstallQuestion, checkContext7, checkRequiredSkills, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, repairCodexLbAuth, shouldAutoApproveInstall } from './install-helpers.mjs';
72
72
  import { buildTeamPlan, codeStructureCommand, dbCommand, defaultBeta, defaultVGraph, evalCommand, gcCommand, goalCommand, gxCommand, harnessCommand, hproofCommand, memoryCommand, migrateWikiContextPack, parseTeamCreateArgs, perfCommand, profileCommand, projectWikiClaims, proofFieldCommand, qaLoopCommand, quickstartCommand, researchCommand, skillDreamCommand, statsCommand, team, teamWorkflowMarkdown, validateArtifactsCommand, wikiCommand, wikiVoxelRowCount, writeWikiContextPack } from './maintenance-commands.mjs';
73
73
  import { openClawCommand } from './openclaw-command.mjs';
74
74
 
@@ -98,7 +98,7 @@ export async function main(args) {
98
98
  if (cmd === 'dollar-commands' || cmd === 'dollars' || cmd === '$') return dollarCommands(tail);
99
99
  if (String(cmd).toLowerCase() === 'dfix') return dfixHelp();
100
100
  const handlers = {
101
- postinstall: () => postinstall({ bootstrap }), wizard: () => wizard(tail), ui: () => wizard(tail), 'update-check': () => updateCheck(tail), help: () => help(tail), commands: () => commands(tail), usage: () => usage(tail), root: () => rootCommand(tail), quickstart: () => quickstartCommand(), 'codex-app': () => codexAppHelp(tail), 'codex-lb': () => codexLbCommand(sub, rest), openclaw: () => openClawCommand(tail), bootstrap: () => bootstrap(tail), deps: () => deps(sub, rest),
101
+ postinstall: () => postinstall({ bootstrap }), wizard: () => wizard(tail), ui: () => wizard(tail), 'update-check': () => updateCheck(tail), help: () => help(tail), commands: () => commands(tail), usage: () => usage(tail), root: () => rootCommand(tail), quickstart: () => quickstartCommand(), 'codex-app': () => codexAppHelp(tail), 'codex-lb': () => codexLbCommand(sub, rest), auth: () => codexLbCommand(sub, rest), openclaw: () => openClawCommand(tail), bootstrap: () => bootstrap(tail), deps: () => deps(sub, rest),
102
102
  'qa-loop': () => qaLoopCommand(sub, rest), ppt: () => pptCommand(sub, rest), context7: () => context7Command(sub, rest), pipeline: () => pipeline(sub, rest), guard: () => guard(sub, rest), conflicts: () => conflicts(sub, rest), versioning: () => versioning(sub, rest), reasoning: () => reasoningCommand(tail), aliases: () => aliases(), setup: () => setup(tail), 'fix-path': () => fixPath(tail), doctor: () => doctor(tail), init: () => init(tail), selftest: () => selftest(tail),
103
103
  goal: () => goalCommand(sub, rest), research: () => researchCommand(sub, rest), hook: () => emitHook(sub), profile: () => profileCommand(sub, rest), hproof: () => hproofCommand(sub, rest), 'validate-artifacts': () => validateArtifactsCommand(tail), perf: () => perfCommand(sub, rest), 'proof-field': () => proofFieldCommand(sub, rest), 'skill-dream': () => skillDreamCommand(sub, rest), 'code-structure': () => codeStructureCommand(sub, rest), memory: () => memoryCommand(sub, rest), gx: () => gxCommand(sub, rest),
104
104
  team: () => team(tail), db: () => dbCommand(sub, rest), eval: () => evalCommand(sub, rest), harness: () => harnessCommand(sub, rest), wiki: () => wikiCommand(sub, rest), gc: () => gcCommand(tail), stats: () => statsCommand(tail)
@@ -152,7 +152,8 @@ Usage:
152
152
  sks bootstrap [--install-scope global|project] [--local-only] [--json]
153
153
  sks deps check|install [tmux|codex|context7|all] [--yes] [--json]
154
154
  sks codex-app
155
- sks codex-lb setup --host <domain> --api-key <key>
155
+ sks codex-lb status|repair|setup --host <domain> --api-key <key>
156
+ sks auth status|repair|setup --host <domain> --api-key <key>
156
157
  sks openclaw install|path|print [--dir path] [--force] [--json]
157
158
  sks --mad [--high]
158
159
  sks auto-review status|enable|start [--high]
@@ -1019,14 +1020,29 @@ async function codexLbCommand(action = 'status', args = []) {
1019
1020
  console.log(`Env file: ${status.env_file ? status.env_path : 'missing'}`);
1020
1021
  if (status.base_url) console.log(`Base URL: ${status.base_url}`);
1021
1022
  if (!status.ok) console.log('\nRun: sks codex-lb setup --host <domain> --api-key <key>');
1023
+ else console.log('\nRepair auth: sks codex-lb repair');
1022
1024
  return;
1023
1025
  }
1024
- if (sub === 'setup') {
1026
+ if (sub === 'repair' || sub === 'resync' || sub === 'login') {
1027
+ const result = await repairCodexLbAuth();
1028
+ if (json) return console.log(JSON.stringify(result, null, 2));
1029
+ if (!result.ok) {
1030
+ if (result.status === 'not_configured') console.error('codex-lb auth repair failed: codex-lb is not fully configured. Run: sks codex-lb setup --host <domain> --api-key <key>');
1031
+ else console.error(`codex-lb auth repair failed: ${result.status}${result.codex_login?.error ? `: ${result.codex_login.error}` : ''}`);
1032
+ process.exitCode = 1;
1033
+ return;
1034
+ }
1035
+ console.log('codex-lb auth repaired for Codex CLI.');
1036
+ console.log(`Config: ${result.config_path}`);
1037
+ console.log(`Key env: ${result.env_path}`);
1038
+ return;
1039
+ }
1040
+ if (sub === 'setup' || sub === 'reconfigure') {
1025
1041
  const host = readOption(args, '--host', readOption(args, '--domain', null));
1026
1042
  const apiKey = readOption(args, '--api-key', readOption(args, '--key', null));
1027
1043
  if (!host || !apiKey) {
1028
1044
  if (json) return console.log(JSON.stringify({ ok: false, reason: 'missing_host_or_api_key' }, null, 2));
1029
- console.error('Usage: sks codex-lb setup --host <domain> --api-key <key>');
1045
+ console.error('Usage: sks codex-lb setup|reconfigure --host <domain> --api-key <key>');
1030
1046
  process.exitCode = 1;
1031
1047
  return;
1032
1048
  }
@@ -1042,7 +1058,7 @@ async function codexLbCommand(action = 'status', args = []) {
1042
1058
  console.log(`Key env: ${result.env_path}`);
1043
1059
  return;
1044
1060
  }
1045
- console.error('Usage: sks codex-lb status|setup --host <domain> --api-key <key> [--json]');
1061
+ console.error('Usage: sks codex-lb status|repair|setup --host <domain> --api-key <key> [--json]');
1046
1062
  process.exitCode = 1;
1047
1063
  }
1048
1064
 
@@ -1085,12 +1101,24 @@ async function madHighCommand(args = []) {
1085
1101
  process.exitCode = 1;
1086
1102
  return;
1087
1103
  }
1104
+ const lb = await maybePromptCodexLbSetupForLaunch(args);
1105
+ if (lb.status === 'missing_api_key') {
1106
+ process.exitCode = 1;
1107
+ return;
1108
+ }
1088
1109
  const profile = await enableMadHighProfile();
1089
1110
  const madLaunch = await activateMadTmuxPermissionState(process.cwd());
1090
1111
  console.log(`SKS MAD ready: ${madHighProfileName()} | gate ${madLaunch.mission_id}`);
1091
1112
  console.log('Live full-access active; catastrophic DB wipe/all-row/project-management guards remain.');
1092
- const workspace = readOption(cleanArgs, '--workspace', readOption(cleanArgs, '--session', `sks-mad-${defaultTmuxSessionName(process.cwd())}`));
1113
+ const launchLb = lb.status === 'present' ? { ...lb, status: 'configured' } : lb;
1114
+ const launchOpts = codexLbImmediateLaunchOpts(cleanArgs, launchLb, {
1115
+ codexArgs: profile.launch_args,
1116
+ autoInstallTmux: !flag(args, '--no-auto-install-tmux'),
1117
+ conciseBlockers: true
1118
+ });
1119
+ const workspace = readOption(cleanArgs, '--workspace', readOption(cleanArgs, '--session', launchOpts.session || `sks-mad-${defaultTmuxSessionName(process.cwd())}`));
1093
1120
  return launchTmuxUi([...cleanArgs, '--workspace', workspace], {
1121
+ ...launchOpts,
1094
1122
  codexArgs: profile.launch_args,
1095
1123
  autoInstallTmux: !flag(args, '--no-auto-install-tmux'),
1096
1124
  conciseBlockers: true
@@ -1438,7 +1466,7 @@ function usage(args = []) {
1438
1466
  const topic = String(args[0] || 'overview').toLowerCase();
1439
1467
  const blocks = {
1440
1468
  overview: ['ㅅㅋㅅ Usage', '', 'Discover:', ' sks commands', ' sks quickstart', ' sks root', ' sks bootstrap', ' sks deps check', ' sks codex-app check', ' sks tmux check', ' sks dollar-commands', '', `Topics: ${USAGE_TOPICS}`],
1441
- install: ['Install', '', '1. Global install:', ' npm i -g sneakoscope', '', '2. Bootstrap and check dependencies:', ' sks bootstrap', ' sks deps check', '', '3. Confirm Codex App commands:', ' sks codex-app check', ' sks dollar-commands', '', '4. Optional codex-lb key setup for CLI sks runs:', ' sks codex-lb setup --host <domain> --api-key <key>', ' sks', '', 'Fallback:', ' npx -y -p sneakoscope sks root', '', 'Project:', ' npm i -D sneakoscope', ' npx sks setup --install-scope project'],
1469
+ install: ['Install', '', '1. Global install:', ' npm i -g sneakoscope', '', '2. Bootstrap and check dependencies:', ' sks bootstrap', ' sks deps check', '', '3. Confirm Codex App commands:', ' sks codex-app check', ' sks dollar-commands', '', '4. Optional codex-lb key setup for CLI sks runs:', ' sks codex-lb setup --host <domain> --api-key <key>', ' sks codex-lb repair', ' sks', '', 'Fallback:', ' npx -y -p sneakoscope sks root', '', 'Project:', ' npm i -D sneakoscope', ' npx sks setup --install-scope project'],
1442
1470
  bootstrap: ['Bootstrap', '', ' sks bootstrap', ' sks setup --bootstrap', '', 'Creates project SKS files, Codex App skills/hooks/config, state/guard files, then checks Codex App, Context7, and tmux.'],
1443
1471
  root: ['Root', '', ' sks root [--json]', '', 'Inside a project, SKS uses that project root. Outside any project marker, runtime commands use the per-user global SKS root instead of writing .sneakoscope into the current random folder.'],
1444
1472
  deps: ['Dependencies', '', ' sks deps check [--json]', ' sks deps install [tmux|codex|context7|all] [--yes]', '', 'tmux on macOS uses Homebrew after Y/n approval for missing installs or Homebrew-managed upgrades. If PATH resolves an npm-managed tmux, SKS prompts for npm i -g tmux@latest instead. Unknown non-Homebrew tmux paths are reported as conflicts.'],
@@ -2151,10 +2179,16 @@ async function selftest() {
2151
2179
  if (defaultFastHighPlan.codexArgs.join(' ') !== '--model gpt-5.5 -c model_reasoning_effort="high"') throw new Error('selftest failed: default sks tmux launch is not fast-high');
2152
2180
  const codexLbHome = path.join(tmp, 'codex-lb-home');
2153
2181
  await ensureDir(path.join(codexLbHome, '.codex'));
2182
+ const codexLbFakeBin = path.join(tmp, 'codex-lb-fake-bin');
2183
+ await ensureDir(codexLbFakeBin);
2184
+ const codexLbFakeCodex = path.join(codexLbFakeBin, 'codex');
2185
+ 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\\\",\\\"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");
2186
+ await fsp.chmod(codexLbFakeCodex, 0o755);
2154
2187
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[notice]\nfast_default_opt_out = true\n');
2188
+ const codexLbEnvForSelftest = { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global'), PATH: `${codexLbFakeBin}${path.delimiter}${process.env.PATH || ''}` };
2155
2189
  const codexLbSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'setup', '--host', 'lb.example.test', '--api-key', 'sk-test', '--json'], {
2156
2190
  cwd: tmp,
2157
- env: { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global') },
2191
+ env: codexLbEnvForSelftest,
2158
2192
  timeoutMs: 15000,
2159
2193
  maxOutputBytes: 64 * 1024
2160
2194
  });
@@ -2163,11 +2197,21 @@ async function selftest() {
2163
2197
  const codexLbConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
2164
2198
  const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
2165
2199
  const codexLbAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
2166
- if (!codexLbSetupJson.ok || codexLbSetupJson.base_url !== 'https://lb.example.test/backend-api/codex' || !codexLbConfig.includes('model_provider = "codex-lb"') || !codexLbConfig.includes('[model_providers.codex-lb]') || !codexLbEnv.includes("CODEX_LB_API_KEY='sk-test'") || !codexLbAuth.includes('"auth_mode": "apikey"')) throw new Error('selftest failed: codex-lb setup did not write provider config, env key, and Codex API-key auth');
2200
+ if (!codexLbSetupJson.ok || codexLbSetupJson.base_url !== 'https://lb.example.test/backend-api/codex' || !codexLbConfig.includes('model_provider = "codex-lb"') || !codexLbConfig.includes('[model_providers.codex-lb]') || !codexLbEnv.includes("CODEX_LB_API_KEY='sk-test'") || !/(\"auth_mode\"\s*:\s*\"apikey\")/.test(codexLbAuth)) throw new Error('selftest failed: codex-lb setup did not write provider config, env key, and Codex API-key auth');
2201
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
2202
+ const codexLbRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'auth', 'repair', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2203
+ if (codexLbRepair.code !== 0) throw new Error(`selftest failed: codex-lb repair exited ${codexLbRepair.code}: ${codexLbRepair.stderr}`);
2204
+ const codexLbRepairJson = JSON.parse(codexLbRepair.stdout);
2205
+ const codexLbRepairedAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
2206
+ if (!codexLbRepairJson.ok || codexLbRepairJson.status !== 'repaired' || !codexLbRepairedAuth.includes('"auth_mode":"apikey"') || !codexLbRepairedAuth.includes('sk-test')) throw new Error('selftest failed: codex-lb repair did not force API-key auth from stored env key');
2207
+ const codexLbStatusText = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'status'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2208
+ if (!String(codexLbStatusText.stdout || '').includes('Repair auth: sks codex-lb repair')) throw new Error('selftest failed: codex-lb status did not advertise repair command');
2167
2209
  if (!codexLbConfig.includes('service_tier = "fast"') || !codexLbConfig.includes('fast_mode = true') || !codexLbConfig.includes('fast_mode_ui = true') || !codexLbConfig.includes('[user.fast_mode]') || !codexLbConfig.includes('visible = true') || !codexLbConfig.includes('enabled = true') || !codexLbConfig.includes('default_profile = "sks-fast-high"') || !/\[profiles\.sks-fast-high\][\s\S]*?service_tier = "fast"/.test(codexLbConfig) || codexLbConfig.includes('fast_default_opt_out = true') || hasTopLevelCodexModeLock(codexLbConfig)) throw new Error('selftest failed: codex-lb setup did not preserve Codex App Fast mode defaults');
2168
2210
  const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
2169
2211
  if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest failed: tmux launch command does not source codex-lb env file');
2170
2212
  if (!codexLbLaunch.includes('SKS_TMUX_LOGO_ANIMATION') || !codexLbLaunch.includes('SNEAKOSCOPE CODEX')) throw new Error('selftest failed: tmux launch command does not include the animated SKS logo intro');
2213
+ const madLaunchSource = await safeReadText(path.join(packageRoot(), 'src', 'cli', 'main.mjs'));
2214
+ if (!madLaunchSource.includes('const lb = await maybePromptCodexLbSetupForLaunch(args)') || !madLaunchSource.includes("const launchLb = lb.status === 'present'") || !madLaunchSource.includes('codexLbImmediateLaunchOpts(cleanArgs, launchLb')) throw new Error('selftest failed: MAD launch does not sync codex-lb auth and fresh-session launch options');
2171
2215
  if (!shouldAutoAttachTmux(['--mad'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest failed: MAD tmux launch does not auto-attach in an interactive terminal');
2172
2216
  if (shouldAutoAttachTmux(['--mad', '--json'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest failed: MAD tmux json mode should not auto-attach');
2173
2217
  if (shouldAutoAttachTmux(['--mad', '--no-attach'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest failed: MAD tmux --no-attach should remain print-only');
@@ -3310,6 +3354,10 @@ async function selftest() {
3310
3354
  await setCurrent(tmp, { mission_id: id, mode: 'QALOOP', phase: 'QALOOP_RUNNING_NO_QUESTIONS' });
3311
3355
  if (!containsUserQuestion('확인해 주세요?')) throw new Error('selftest failed: question guard');
3312
3356
  if (classifySql('drop table users;').level !== 'destructive') throw new Error('selftest failed: destructive sql not detected');
3357
+ const patchPayloadClass = classifyToolPayload({ tool_name: 'apply_patch', command: '*** Update File: src/example.mjs\n+ok\n' });
3358
+ if (patchPayloadClass.level !== 'none') throw new Error('selftest failed: apply_patch file edits should not be classified as DB writes');
3359
+ const supabaseWritePayloadClass = classifyToolPayload({ tool_name: 'mcp__supabase__execute_sql', sql: "update users set name = 'x' where id = '1';" });
3360
+ if (supabaseWritePayloadClass.level !== 'write' || !supabaseWritePayloadClass.toolReasons.includes('database_tool')) throw new Error('selftest failed: Supabase execute_sql write classification was weakened');
3313
3361
  if (classifyCommand('supabase db reset').level !== 'destructive') throw new Error('selftest failed: supabase db reset not detected');
3314
3362
  const dbDecision = await checkDbOperation(tmp, { mission_id: id }, { tool_name: 'mcp__supabase__execute_sql', sql: 'drop table users;' }, { duringNoQuestion: true });
3315
3363
  if (dbDecision.action !== 'block') throw new Error('selftest failed: destructive MCP SQL allowed');
@@ -199,6 +199,9 @@ export function classifyToolPayload(payload = {}) {
199
199
  const sqlClass = classifySql(combined);
200
200
  const commandClass = classifyCommand(strings.find((s) => /\b(supabase|psql|prisma|drizzle|knex|sequelize)\b/i.test(s)) || '');
201
201
  const toolReasons = [];
202
+ if (/\b(apply_patch|edit|write|create|remove|rename|str_replace|file_write|fs_write)\b/i.test(toolName) && !/supabase|postgres|database|execute_sql|apply_migration|sql_query|db_|_db\b|migration/.test(toolName)) {
203
+ return { level: 'none', toolName, toolReasons, sql: sqlClass, command: commandClass, stringsExamined: strings.length };
204
+ }
202
205
  if (/supabase|postgres|database|execute_sql|apply_migration|sql_query|db_|_db\b|migration/.test(toolName)) toolReasons.push('database_tool');
203
206
  if (/delete_project|pause_project|restore_project|delete_branch|reset_branch|merge_branch/.test(toolName)) toolReasons.push('dangerous_supabase_management_tool');
204
207
  let level = 'none';
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.7.48';
8
+ export const PACKAGE_VERSION = '0.7.49';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
@@ -456,7 +456,8 @@ export const COMMAND_CATALOG = [
456
456
  { name: 'root', usage: 'sks root [--json]', description: 'Show whether SKS is using a project root or the per-user global SKS runtime root.' },
457
457
  { name: 'deps', usage: 'sks deps check|install [tmux|codex|context7|all] [--yes]', description: 'Check or guided-install Node/npm PATH, Codex CLI/App, Context7, Browser Use, Computer Use, tmux, and Homebrew on macOS.' },
458
458
  { name: 'codex-app', usage: 'sks codex-app [check|open|remote-control]', description: 'Check Codex App install and first-party MCP/plugin readiness, then show app setup files, examples, and Codex CLI 0.130.0+ remote-control availability.' },
459
- { name: 'codex-lb', usage: 'sks codex-lb status|setup --host <domain> --api-key <key>', description: 'Configure codex-lb as the Codex CLI provider by writing ~/.codex/config.toml and the CODEX_LB_API_KEY env file.' },
459
+ { name: 'codex-lb', usage: 'sks codex-lb status|repair|setup --host <domain> --api-key <key>', description: 'Configure or repair codex-lb Codex CLI auth by writing ~/.codex/config.toml, syncing auth.json, and loading the CODEX_LB_API_KEY env file.' },
460
+ { name: 'auth', usage: 'sks auth status|repair|setup --host <domain> --api-key <key>', description: 'Shortcut for codex-lb auth status, repair, and setup commands.' },
460
461
  { name: 'openclaw', usage: 'sks openclaw install|path|print [--dir path] [--force] [--json]', description: 'Generate an OpenClaw skill package so OpenClaw agents can discover and use local SKS workflows.' },
461
462
  { name: 'tmux', usage: 'sks | sks tmux open|check|status [--workspace name]', description: 'Open the default SKS tmux runtime with bare sks, or use tmux subcommands for explicit launch/check/status.' },
462
463
  { name: 'mad', usage: 'sks --mad [--high]', description: 'Open a one-shot tmux Codex CLI workspace with the SKS MAD full-access auto-review profile.' },