sneakoscope 0.7.40 → 0.7.41

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
@@ -170,7 +170,20 @@ Bare `sks` creates or reuses the default named tmux session for Codex CLI and at
170
170
 
171
171
  Before opening tmux, SKS checks the installed Codex CLI against npm `@openai/codex@latest`. If a newer version exists, it asks `Y/n`; answering `y` updates automatically with `npm i -g @openai/codex@latest` and then opens tmux with the updated Codex CLI.
172
172
 
173
- If you use [codex-lb](https://github.com/Soju06/codex-lb), start it first, create an API key in its dashboard, then add this provider to `~/.codex/config.toml`:
173
+ If you use [codex-lb](https://github.com/Soju06/codex-lb), start it first, create an API key in its dashboard, then run:
174
+
175
+ ```sh
176
+ sks codex-lb setup --host https://your-codex-lb.example.com --api-key "sk-clb-..."
177
+ sks
178
+ ```
179
+
180
+ Bare `sks` asks this before opening Codex when codex-lb is not configured:
181
+
182
+ ```text
183
+ Authenticate and route Codex through codex-lb? [y/N]
184
+ ```
185
+
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`, 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. The generated provider config follows the codex-lb README's Codex CLI API-key setup:
174
187
 
175
188
  ```toml
176
189
  model_provider = "codex-lb"
@@ -184,13 +197,6 @@ supports_websockets = true
184
197
  requires_openai_auth = true
185
198
  ```
186
199
 
187
- Then run:
188
-
189
- ```sh
190
- export CODEX_LB_API_KEY="sk-clb-..."
191
- sks
192
- ```
193
-
194
200
  ### MAD tmux Launch
195
201
 
196
202
  ```sh
@@ -431,7 +437,7 @@ sks dollar-commands
431
437
  4. Optional codex-lb key setup for CLI `sks` runs.
432
438
 
433
439
  ```sh
434
- export CODEX_LB_API_KEY="sk-clb-..."
440
+ sks codex-lb setup --host <domain> --api-key <key>
435
441
  sks
436
442
  ```
437
443
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.7.40",
4
+ "version": "0.7.41",
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",
@@ -3,7 +3,7 @@ import os from 'node:os';
3
3
  import fsp from 'node:fs/promises';
4
4
  import readline from 'node:readline/promises';
5
5
  import { stdin as input, stdout as output } from 'node:process';
6
- import { ensureDir, exists, globalSksRoot, packageRoot, runProcess, which, writeTextAtomic } from '../core/fsx.mjs';
6
+ import { ensureDir, exists, globalSksRoot, packageRoot, readText, runProcess, which, writeTextAtomic } from '../core/fsx.mjs';
7
7
  import { getCodexInfo } from '../core/codex-adapter.mjs';
8
8
  import { formatHarnessConflictReport, llmHarnessCleanupPrompt, scanHarnessConflicts } from '../core/harness-conflicts.mjs';
9
9
  import { installSkills } from '../core/init.mjs';
@@ -113,6 +113,132 @@ export async function askPostinstallQuestion(question) {
113
113
  }
114
114
  }
115
115
 
116
+ export function codexLbConfigPath(home = process.env.HOME || os.homedir()) {
117
+ return path.join(home, '.codex', 'config.toml');
118
+ }
119
+
120
+ export function codexLbEnvPath(home = process.env.HOME || os.homedir()) {
121
+ return path.join(home, '.codex', 'sks-codex-lb.env');
122
+ }
123
+
124
+ export function normalizeCodexLbBaseUrl(input = '') {
125
+ let host = String(input || '').trim();
126
+ if (!host) host = 'http://127.0.0.1:2455';
127
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(host)) host = `https://${host}`;
128
+ host = host.replace(/\/+$/, '');
129
+ return /\/backend-api\/codex$/i.test(host) ? host : `${host}/backend-api/codex`;
130
+ }
131
+
132
+ export async function configureCodexLb(opts = {}) {
133
+ const home = opts.home || process.env.HOME || os.homedir();
134
+ const configPath = opts.configPath || codexLbConfigPath(home);
135
+ const envPath = opts.envPath || codexLbEnvPath(home);
136
+ const baseUrl = normalizeCodexLbBaseUrl(opts.host || opts.baseUrl);
137
+ const apiKey = String(opts.apiKey || '').trim();
138
+ if (!apiKey) return { ok: false, status: 'missing_api_key', config_path: configPath, env_path: envPath };
139
+ await ensureDir(path.dirname(configPath));
140
+ const current = await readText(configPath, '');
141
+ const next = upsertCodexLbConfig(current, baseUrl);
142
+ await writeTextAtomic(configPath, next);
143
+ await writeTextAtomic(envPath, `export CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`);
144
+ await fsp.chmod(envPath, 0o600).catch(() => {});
145
+ process.env.CODEX_LB_API_KEY = apiKey;
146
+ return { ok: true, status: 'configured', config_path: configPath, env_path: envPath, base_url: baseUrl, env_key: 'CODEX_LB_API_KEY' };
147
+ }
148
+
149
+ export async function codexLbStatus(opts = {}) {
150
+ const home = opts.home || process.env.HOME || os.homedir();
151
+ const configPath = opts.configPath || codexLbConfigPath(home);
152
+ const envPath = opts.envPath || codexLbEnvPath(home);
153
+ const config = await readText(configPath, '');
154
+ const envExists = await exists(envPath);
155
+ const envText = envExists ? await readText(envPath, '') : '';
156
+ const envKeyConfigured = /^(\s*export\s+)?CODEX_LB_API_KEY\s*=.+$/m.test(envText);
157
+ const providerConfigured = /\[model_providers\.codex-lb\]/.test(config);
158
+ const selected = /model_provider\s*=\s*"codex-lb"/.test(config);
159
+ return {
160
+ ok: selected && providerConfigured && envKeyConfigured,
161
+ config_path: configPath,
162
+ env_path: envPath,
163
+ provider_configured: providerConfigured,
164
+ selected,
165
+ env_file: envExists,
166
+ env_key_configured: envKeyConfigured,
167
+ base_url: config.match(/base_url\s*=\s*"([^"]+)"/)?.[1] || null
168
+ };
169
+ }
170
+
171
+ export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
172
+ if (args.includes('--json') || args.includes('--skip-codex-lb') || process.env.SKS_SKIP_CODEX_LB_PROMPT === '1') return { status: 'skipped' };
173
+ if (!canAskYesNo()) return { status: 'non_interactive' };
174
+ const status = await codexLbStatus(opts);
175
+ if (status.ok) return { status: 'present', ...status };
176
+ const useCodexLb = (await askPostinstallQuestion('\nAuthenticate and route Codex through codex-lb? [y/N] ')).trim();
177
+ if (!/^(y|yes|예|네|응)$/i.test(useCodexLb)) return { status: 'continued_to_codex' };
178
+ const host = (await askPostinstallQuestion('codex-lb host domain [http://127.0.0.1:2455]: ')).trim() || 'http://127.0.0.1:2455';
179
+ const apiKey = (await askPostinstallQuestion('codex-lb API key: ')).trim();
180
+ const configured = await configureCodexLb({ ...opts, host, apiKey });
181
+ if (configured.ok) console.log(`codex-lb configured: ${configured.base_url}`);
182
+ else console.log('codex-lb setup skipped: API key was empty.');
183
+ return configured;
184
+ }
185
+
186
+ function upsertCodexLbConfig(text = '', baseUrl) {
187
+ let next = upsertTopLevelTomlString(text, 'model_provider', 'codex-lb');
188
+ const block = [
189
+ '[model_providers.codex-lb]',
190
+ 'name = "OpenAI"',
191
+ `base_url = "${baseUrl}"`,
192
+ 'wire_api = "responses"',
193
+ 'env_key = "CODEX_LB_API_KEY"',
194
+ 'supports_websockets = true',
195
+ 'requires_openai_auth = true'
196
+ ].join('\n');
197
+ next = upsertTomlTable(next, 'model_providers.codex-lb', block);
198
+ return `${next.trim()}\n`;
199
+ }
200
+
201
+ function upsertTopLevelTomlString(text, key, value) {
202
+ const line = `${key} = "${value}"`;
203
+ const lines = String(text || '').split('\n');
204
+ const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
205
+ const end = firstTable === -1 ? lines.length : firstTable;
206
+ for (let i = 0; i < end; i++) {
207
+ if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(lines[i])) {
208
+ lines[i] = line;
209
+ return lines.join('\n').replace(/\n{3,}/g, '\n\n');
210
+ }
211
+ }
212
+ lines.splice(end, 0, line);
213
+ return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
214
+ }
215
+
216
+ function upsertTomlTable(text, table, block) {
217
+ let lines = String(text || '').trimEnd().split('\n');
218
+ if (lines.length === 1 && lines[0] === '') lines = [];
219
+ const header = `[${table}]`;
220
+ const start = lines.findIndex((x) => x.trim() === header);
221
+ const blockLines = String(block || '').trim().split('\n');
222
+ if (start === -1) return [...lines, ...(lines.length ? [''] : []), ...blockLines].join('\n').replace(/\n{3,}/g, '\n\n');
223
+ let end = lines.length;
224
+ for (let i = start + 1; i < lines.length; i++) {
225
+ if (/^\s*\[.+\]\s*$/.test(lines[i])) {
226
+ end = i;
227
+ break;
228
+ }
229
+ }
230
+ lines.splice(start, end - start, ...blockLines);
231
+ return lines.join('\n').replace(/\n{3,}/g, '\n\n');
232
+ }
233
+
234
+ function shellSingleQuote(value) {
235
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
236
+ }
237
+
238
+ function escapeRegExp(value) {
239
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
240
+ }
241
+
116
242
  export async function ensureSksCommandDuringInstall(opts = {}) {
117
243
  if (process.env.SKS_SKIP_POSTINSTALL_SHIM === '1' && !opts.force) return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_SHIM=1' };
118
244
  const pathEnv = opts.pathEnv ?? process.env.PATH ?? '';
package/src/cli/main.mjs CHANGED
@@ -58,10 +58,10 @@ import { renderTeamDashboardState, writeTeamDashboardState } from '../core/team-
58
58
  import { GOAL_WORKFLOW_ARTIFACT } from '../core/goal-workflow.mjs';
59
59
  import { CODEX_APP_DOCS_URL, codexAppIntegrationStatus, formatCodexAppStatus } from '../core/codex-app.mjs';
60
60
  import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs';
61
- import { buildTmuxLaunchPlan, buildTmuxOpenArgs, createTmuxSession, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, runTmuxStatus, sanitizeTmuxSessionName, teamLaneStyle } from '../core/tmux-ui.mjs';
61
+ import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, runTmuxStatus, sanitizeTmuxSessionName, teamLaneStyle } from '../core/tmux-ui.mjs';
62
62
  import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
63
63
  import { context7Command } from './context7-command.mjs';
64
- import { askPostinstallQuestion, checkContext7, checkRequiredSkills, ensureCodexCliTool, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, shouldAutoApproveInstall } from './install-helpers.mjs';
64
+ import { askPostinstallQuestion, checkContext7, checkRequiredSkills, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, shouldAutoApproveInstall } from './install-helpers.mjs';
65
65
  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';
66
66
  import { openClawCommand } from './openclaw-command.mjs';
67
67
 
@@ -91,7 +91,7 @@ export async function main(args) {
91
91
  if (cmd === 'dollar-commands' || cmd === 'dollars' || cmd === '$') return dollarCommands(tail);
92
92
  if (String(cmd).toLowerCase() === 'dfix') return dfixHelp();
93
93
  const handlers = {
94
- 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), openclaw: () => openClawCommand(tail), bootstrap: () => bootstrap(tail), deps: () => deps(sub, rest),
94
+ 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),
95
95
  '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),
96
96
  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),
97
97
  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)
@@ -118,7 +118,12 @@ async function defaultTmuxCommand(args = []) {
118
118
  process.exitCode = 1;
119
119
  return;
120
120
  }
121
- return launchTmuxUi(args, { conciseBlockers: true });
121
+ const lb = await maybePromptCodexLbSetupForLaunch(args);
122
+ if (lb.status === 'missing_api_key') {
123
+ process.exitCode = 1;
124
+ return;
125
+ }
126
+ return launchTmuxUi(args, codexLbImmediateLaunchOpts(args, lb, { conciseBlockers: true }));
122
127
  }
123
128
 
124
129
  function help(args = []) {
@@ -140,6 +145,7 @@ Usage:
140
145
  sks bootstrap [--install-scope global|project] [--local-only] [--json]
141
146
  sks deps check|install [tmux|codex|context7|all] [--yes] [--json]
142
147
  sks codex-app
148
+ sks codex-lb setup --host <domain> --api-key <key>
143
149
  sks openclaw install|path|print [--dir path] [--force] [--json]
144
150
  sks --mad [--high]
145
151
  sks auto-review status|enable|start [--high]
@@ -906,7 +912,12 @@ async function tmuxCommand(sub = 'start', args = []) {
906
912
  process.exitCode = 1;
907
913
  return;
908
914
  }
909
- const result = await launchTmuxUi(args);
915
+ const lb = await maybePromptCodexLbSetupForLaunch(args);
916
+ if (lb.status === 'missing_api_key') {
917
+ process.exitCode = 1;
918
+ return;
919
+ }
920
+ const result = await launchTmuxUi(args, codexLbImmediateLaunchOpts(args, lb));
910
921
  if (flag(args, '--json')) console.log(JSON.stringify(result, null, 2));
911
922
  return;
912
923
  }
@@ -914,6 +925,56 @@ async function tmuxCommand(sub = 'start', args = []) {
914
925
  process.exitCode = 1;
915
926
  }
916
927
 
928
+ async function codexLbCommand(action = 'status', args = []) {
929
+ const sub = action || 'status';
930
+ const json = flag(args, '--json');
931
+ if (sub === 'status' || sub === 'check') {
932
+ const status = await codexLbStatus();
933
+ if (json) return console.log(JSON.stringify(status, null, 2));
934
+ console.log('SKS codex-lb\n');
935
+ console.log(`Configured: ${status.ok ? 'yes' : 'no'}`);
936
+ console.log(`Selected: ${status.selected ? 'yes' : 'no'}`);
937
+ console.log(`Provider: ${status.provider_configured ? 'yes' : 'no'}`);
938
+ console.log(`Env file: ${status.env_file ? status.env_path : 'missing'}`);
939
+ if (status.base_url) console.log(`Base URL: ${status.base_url}`);
940
+ if (!status.ok) console.log('\nRun: sks codex-lb setup --host <domain> --api-key <key>');
941
+ return;
942
+ }
943
+ if (sub === 'setup') {
944
+ const host = readOption(args, '--host', readOption(args, '--domain', null));
945
+ const apiKey = readOption(args, '--api-key', readOption(args, '--key', null));
946
+ if (!host || !apiKey) {
947
+ if (json) return console.log(JSON.stringify({ ok: false, reason: 'missing_host_or_api_key' }, null, 2));
948
+ console.error('Usage: sks codex-lb setup --host <domain> --api-key <key>');
949
+ process.exitCode = 1;
950
+ return;
951
+ }
952
+ const result = await configureCodexLb({ host, apiKey });
953
+ if (json) return console.log(JSON.stringify(result, null, 2));
954
+ if (!result.ok) {
955
+ console.error(`codex-lb setup failed: ${result.status}`);
956
+ process.exitCode = 1;
957
+ return;
958
+ }
959
+ console.log(`codex-lb configured: ${result.base_url}`);
960
+ console.log(`Config: ${result.config_path}`);
961
+ console.log(`Key env: ${result.env_path}`);
962
+ return;
963
+ }
964
+ console.error('Usage: sks codex-lb status|setup --host <domain> --api-key <key> [--json]');
965
+ process.exitCode = 1;
966
+ }
967
+
968
+ function codexLbImmediateLaunchOpts(args = [], lb = {}, opts = {}) {
969
+ if (!lb?.ok || lb.status !== 'configured') return opts;
970
+ if (readOption(args, '--session', null) || readOption(args, '--workspace', null)) return opts;
971
+ const root = readOption(args, '--root', process.cwd());
972
+ const session = sanitizeTmuxSessionName(`sks-codex-lb-${Date.now().toString(36)}-${defaultTmuxSessionName(root)}`);
973
+ console.log(`codex-lb key loaded for this launch: ${lb.env_path}`);
974
+ console.log(`Using fresh tmux session: ${session}`);
975
+ return { ...opts, session };
976
+ }
977
+
917
978
  async function madHighCommand(args = []) {
918
979
  const cleanArgs = args.filter((arg) => !['--mad', '--MAD', '--mad-sks', '--high', '--no-auto-install-tmux'].includes(arg));
919
980
  if (flag(args, '--json')) {
@@ -1233,7 +1294,7 @@ function usage(args = []) {
1233
1294
  const topic = String(args[0] || 'overview').toLowerCase();
1234
1295
  const blocks = {
1235
1296
  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}`],
1236
- 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:', ' # Add the codex-lb provider to ~/.codex/config.toml, then:', ' export CODEX_LB_API_KEY="sk-clb-..."', ' sks', '', 'Fallback:', ' npx -y -p sneakoscope sks root', '', 'Project:', ' npm i -D sneakoscope', ' npx sks setup --install-scope project'],
1297
+ 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'],
1237
1298
  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.'],
1238
1299
  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.'],
1239
1300
  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.'],
@@ -1935,6 +1996,20 @@ async function selftest() {
1935
1996
  if (tmuxOpenArgs.join(' ') !== 'attach-session -t sks-mad-selftest') throw new Error('selftest failed: MAD tmux attach args are not stable by session name');
1936
1997
  const defaultFastHighPlan = await buildTmuxLaunchPlan({ root: tmp, tmux: { ok: true, bin: 'tmux', version: '3.4' }, codex: { bin: 'codex', version: 'codex-cli 99.0.0' }, app: { ok: true } });
1937
1998
  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');
1999
+ const codexLbHome = path.join(tmp, 'codex-lb-home');
2000
+ const codexLbSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'setup', '--host', 'lb.example.test', '--api-key', 'sk-test', '--json'], {
2001
+ cwd: tmp,
2002
+ env: { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global') },
2003
+ timeoutMs: 15000,
2004
+ maxOutputBytes: 64 * 1024
2005
+ });
2006
+ if (codexLbSetup.code !== 0) throw new Error(`selftest failed: codex-lb setup exited ${codexLbSetup.code}: ${codexLbSetup.stderr}`);
2007
+ const codexLbSetupJson = JSON.parse(codexLbSetup.stdout);
2008
+ const codexLbConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
2009
+ const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
2010
+ 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'")) throw new Error('selftest failed: codex-lb setup did not write provider config and env key');
2011
+ const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
2012
+ if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest failed: tmux launch command does not source codex-lb env file');
1938
2013
  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');
1939
2014
  if (shouldAutoAttachTmux(['--mad', '--json'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest failed: MAD tmux json mode should not auto-attach');
1940
2015
  if (shouldAutoAttachTmux(['--mad', '--no-attach'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest failed: MAD tmux --no-attach should remain print-only');
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.40';
8
+ export const PACKAGE_VERSION = '0.7.41';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
@@ -456,6 +456,7 @@ 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]', description: 'Check Codex App install and first-party MCP/plugin readiness, then show app setup files and examples.' },
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
460
  { 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.' },
460
461
  { 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.' },
461
462
  { name: 'mad', usage: 'sks --mad [--high]', description: 'Open a one-shot tmux Codex CLI workspace with the SKS MAD full-access auto-review profile.' },
@@ -108,6 +108,7 @@ export function codexLaunchCommand(root, codexBin, codexArgs = []) {
108
108
  `printf '\\nProject: %s\\n' ${shellEscape(root)}`,
109
109
  'printf \'Runtime: tmux session for Codex CLI\\n\'',
110
110
  'printf \'Prompt: use canonical $ commands, for example $Team or $QA-LOOP\\n\\n\'',
111
+ '[ -f "$HOME/.codex/sks-codex-lb.env" ] && . "$HOME/.codex/sks-codex-lb.env"',
111
112
  'sleep 1',
112
113
  `exec ${[shellEscape(codexBin), ...extraArgs.map(shellEscape), '--cd', shellEscape(root)].join(' ')}`
113
114
  ].join('; ');