sneakoscope 0.7.44 → 0.7.46

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
@@ -43,7 +43,7 @@ sks selftest --mock
43
43
  | Area | What it does |
44
44
  | --- | --- |
45
45
  | CLI runtime | Bare `sks` opens or reuses the default tmux Codex CLI workspace. `sks tmux open` remains the explicit form for session/workspace flags, and `sks --mad` launches the explicit full-access high-reasoning profile. |
46
- | Codex App commands | Installs generated skills so `$Team`, `$From-Chat-IMG`, `$DFix`, `$QA-LOOP`, `$PPT`, `$Goal`, `$DB`, `$Wiki`, `$Help`, and related routes are visible in prompt workflows. |
46
+ | Codex App commands | Installs generated skills so `$Team`, `$From-Chat-IMG`, `$DFix`, `$QA-LOOP`, `$PPT`, `$Goal`, `$DB`, `$Wiki`, `$Help`, and related routes are visible in prompt workflows. `sks codex-app remote-control` wraps Codex CLI 0.130.0+ headless remote control without falling back to older app-server internals. |
47
47
  | OpenClaw agents | Generates an OpenClaw skill package so OpenClaw agents can attach `sneakoscope-codex`, enable the `shell` tool, and discover/use SKS commands from the target repo root. |
48
48
  | Pipeline plans | Writes `pipeline-plan.json` for stateful routes so the runtime lane, kept stages, skipped stages, verification commands, and no-unrequested-fallback invariant are visible with `sks pipeline plan`. |
49
49
  | Team orchestration | Runs substantial work through score-based ambiguity handling, scouts, TriWiki refresh, debate, runtime task graphs, worker inboxes, implementation, review, cleanup, reflection, and Honest Mode; narrow work should use Proof Field evidence to skip unrelated pipeline work instead of expanding Team. |
@@ -183,10 +183,11 @@ Bare `sks` asks this before opening Codex when codex-lb is not configured:
183
183
  Authenticate and route Codex through codex-lb? [y/N]
184
184
  ```
185
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`, 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. The generated provider config follows the codex-lb README's Codex CLI API-key setup:
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
187
 
188
188
  ```toml
189
189
  model_provider = "codex-lb"
190
+ service_tier = "fast"
190
191
 
191
192
  [model_providers.codex-lb]
192
193
  name = "OpenAI"
@@ -299,9 +300,18 @@ After installing, run:
299
300
  ```sh
300
301
  sks bootstrap
301
302
  sks codex-app check
303
+ sks codex-app remote-control --status
302
304
  sks dollar-commands
303
305
  ```
304
306
 
307
+ For headless remotely controllable Codex App/server sessions on Codex CLI 0.130.0 or newer, run:
308
+
309
+ ```sh
310
+ sks codex-app remote-control -- --help
311
+ ```
312
+
313
+ `sks codex-app check` reports whether the installed Codex CLI is new enough. Codex CLI 0.130.0+ app-server/remote-control threads can pick up config changes live; older CLI/TUI sessions should still be restarted after `.codex/config.toml` or MCP/plugin changes.
314
+
305
315
  Then open Codex App and use prompt commands directly in the chat. Examples:
306
316
 
307
317
  ```text
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.7.44",
4
+ "version": "0.7.46",
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",
@@ -0,0 +1,67 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { codexRemoteControlStatus, formatCodexRemoteControlStatus } from '../core/codex-app.mjs';
3
+
4
+ export async function codexAppRemoteControlCommand(args = [], opts = {}) {
5
+ const controlArgs = argsBeforeSeparator(args);
6
+ if (controlArgs.includes('--help') || controlArgs.includes('-h')) {
7
+ console.log(remoteControlHelp());
8
+ return;
9
+ }
10
+
11
+ const status = await codexRemoteControlStatus();
12
+ if (controlArgs.includes('--json')) {
13
+ console.log(JSON.stringify(status, null, 2));
14
+ if (!status.ok) process.exitCode = 1;
15
+ return;
16
+ }
17
+
18
+ if (controlArgs.includes('--status') || controlArgs.includes('--check') || controlArgs.includes('--dry-run')) {
19
+ console.log(formatCodexRemoteControlStatus(status));
20
+ if (!status.ok) process.exitCode = 1;
21
+ return;
22
+ }
23
+
24
+ if (!status.ok) {
25
+ console.error(formatCodexRemoteControlStatus(status));
26
+ process.exitCode = 1;
27
+ return;
28
+ }
29
+
30
+ const passthrough = stripSeparator(args);
31
+ const spawnFn = opts.spawn || spawn;
32
+ const code = await spawnInherited(spawnFn, status.codex_cli.bin, ['remote-control', ...passthrough], {
33
+ cwd: process.cwd(),
34
+ env: process.env
35
+ });
36
+ if (code) process.exitCode = code;
37
+ }
38
+
39
+ function remoteControlHelp() {
40
+ return [
41
+ 'Usage: sks codex-app remote-control [--status|--check|--dry-run|--json] [-- <codex remote-control args>]',
42
+ '',
43
+ 'Starts Codex CLI 0.130.0+ remote-control, the headless remotely controllable app-server entrypoint.',
44
+ 'SKS only wraps the first-party command and refuses older Codex CLI versions instead of falling back to app-server internals.'
45
+ ].join('\n');
46
+ }
47
+
48
+ function stripSeparator(args = []) {
49
+ const index = args.indexOf('--');
50
+ return index >= 0 ? args.slice(index + 1) : args;
51
+ }
52
+
53
+ function argsBeforeSeparator(args = []) {
54
+ const index = args.indexOf('--');
55
+ return index >= 0 ? args.slice(0, index) : args;
56
+ }
57
+
58
+ function spawnInherited(spawnFn, command, args, opts) {
59
+ return new Promise((resolve) => {
60
+ const child = spawnFn(command, args, { ...opts, stdio: 'inherit' });
61
+ child.on('error', (err) => {
62
+ console.error(`codex remote-control failed to start: ${err.message}`);
63
+ resolve(1);
64
+ });
65
+ child.on('close', (code) => resolve(code || 0));
66
+ });
67
+ }
@@ -30,6 +30,11 @@ export async function postinstall({ bootstrap }) {
30
30
  else if (context7Install.status === 'codex_missing') console.log('Context7 MCP: Codex CLI missing. Install @openai/codex or set SKS_CODEX_BIN, then run `sks context7 setup --scope global` or `sks setup` in a project.');
31
31
  else if (context7Install.status === 'skipped') console.log(`Context7 MCP: skipped (${context7Install.reason}).`);
32
32
  else if (context7Install.status === 'failed') console.log(`Context7 MCP: auto setup failed. Run \`sks context7 setup --scope global\` or \`sks setup\`. ${context7Install.error || ''}`.trim());
33
+ const fastModeRepair = await ensureGlobalCodexFastModeDuringInstall();
34
+ if (fastModeRepair.status === 'updated') console.log(`Codex App Fast mode: restored in ${fastModeRepair.config_path}.`);
35
+ else if (fastModeRepair.status === 'present') console.log('Codex App Fast mode: config already compatible.');
36
+ else if (fastModeRepair.status === 'skipped') console.log(`Codex App Fast mode: skipped (${fastModeRepair.reason}).`);
37
+ else if (fastModeRepair.status === 'failed') console.log(`Codex App Fast mode: auto repair failed. Run \`sks setup\`. ${fastModeRepair.error || ''}`.trim());
33
38
  const globalSkills = await ensureGlobalCodexSkillsDuringInstall();
34
39
  if (globalSkills.status === 'installed') console.log(`Codex App global $ skills: installed in ${globalSkills.root} (${globalSkills.installed_count} skills).`);
35
40
  else if (globalSkills.status === 'partial') console.log(`Codex App global $ skills: partial in ${globalSkills.root}; missing ${globalSkills.missing_skills.join(', ')}. Run \`sks doctor --fix\`.`);
@@ -138,7 +143,7 @@ export async function configureCodexLb(opts = {}) {
138
143
  if (!apiKey) return { ok: false, status: 'missing_api_key', config_path: configPath, env_path: envPath };
139
144
  await ensureDir(path.dirname(configPath));
140
145
  const current = await readText(configPath, '');
141
- const next = upsertCodexLbConfig(current, baseUrl);
146
+ const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, baseUrl));
142
147
  await writeTextAtomic(configPath, next);
143
148
  await writeTextAtomic(envPath, `export CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`);
144
149
  await fsp.chmod(envPath, 0o600).catch(() => {});
@@ -225,6 +230,102 @@ function upsertCodexLbConfig(text = '', baseUrl) {
225
230
  return `${next.trim()}\n`;
226
231
  }
227
232
 
233
+ export async function ensureGlobalCodexFastModeDuringInstall(opts = {}) {
234
+ if (process.env.SKS_SKIP_CODEX_FAST_MODE_REPAIR === '1') return { status: 'skipped', reason: 'SKS_SKIP_CODEX_FAST_MODE_REPAIR=1' };
235
+ const home = opts.home || process.env.HOME || os.homedir();
236
+ const configPath = opts.configPath || codexLbConfigPath(home);
237
+ try {
238
+ await ensureDir(path.dirname(configPath));
239
+ const current = await readText(configPath, '');
240
+ const next = normalizeCodexFastModeUiConfig(current);
241
+ if (next === ensureTrailingNewline(current)) return { status: 'present', config_path: configPath };
242
+ await writeTextAtomic(configPath, next);
243
+ return { status: 'updated', config_path: configPath };
244
+ } catch (err) {
245
+ return { status: 'failed', config_path: configPath, error: err.message };
246
+ }
247
+ }
248
+
249
+ export function normalizeCodexFastModeUiConfig(text = '') {
250
+ let next = removeLegacyTopLevelCodexModeLocks(text);
251
+ next = removeTomlTableKey(next, 'notice', 'fast_default_opt_out');
252
+ next = upsertTopLevelTomlString(next, 'service_tier', 'fast');
253
+ next = upsertTomlTableKey(next, 'features', 'fast_mode = true');
254
+ next = upsertTomlTableKey(next, 'features', 'fast_mode_ui = true');
255
+ next = upsertTomlTableKey(next, 'user.fast_mode', 'visible = true');
256
+ next = upsertTomlTableKey(next, 'user.fast_mode', 'enabled = true');
257
+ next = upsertTomlTableKey(next, 'user.fast_mode', 'default_profile = "sks-fast-high"');
258
+ next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'model = "gpt-5.5"');
259
+ next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'service_tier = "fast"');
260
+ next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'approval_policy = "on-request"');
261
+ next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'sandbox_mode = "workspace-write"');
262
+ next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'model_reasoning_effort = "high"');
263
+ return ensureTrailingNewline(next);
264
+ }
265
+
266
+ function removeLegacyTopLevelCodexModeLocks(text = '') {
267
+ const legacy = {
268
+ model: new Set(['gpt-5.5']),
269
+ model_reasoning_effort: new Set(['high'])
270
+ };
271
+ const lines = String(text || '').split('\n');
272
+ const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
273
+ const end = firstTable === -1 ? lines.length : firstTable;
274
+ return lines.filter((line, index) => {
275
+ if (index >= end) return true;
276
+ const match = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=\s*"([^"]*)"\s*(?:#.*)?$/);
277
+ if (!match) return true;
278
+ return !legacy[match[1]]?.has(match[2]);
279
+ }).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
280
+ }
281
+
282
+ function removeTomlTableKey(text, table, key) {
283
+ const lines = String(text || '').trimEnd().split('\n');
284
+ if (lines.length === 1 && lines[0] === '') return '';
285
+ const header = `[${table}]`;
286
+ const start = lines.findIndex((x) => x.trim() === header);
287
+ if (start === -1) return String(text || '');
288
+ let end = lines.length;
289
+ for (let i = start + 1; i < lines.length; i += 1) {
290
+ if (/^\s*\[.+\]\s*$/.test(lines[i])) {
291
+ end = i;
292
+ break;
293
+ }
294
+ }
295
+ const keyPattern = new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=`);
296
+ return lines.filter((line, index) => index <= start || index >= end || !keyPattern.test(line)).join('\n').replace(/\n{3,}/g, '\n\n');
297
+ }
298
+
299
+ function upsertTomlTableKey(text, table, line) {
300
+ const key = String(line).split('=')[0].trim();
301
+ const lines = String(text || '').trimEnd().split('\n');
302
+ if (lines.length === 1 && lines[0] === '') lines.length = 0;
303
+ const header = `[${table}]`;
304
+ const start = lines.findIndex((x) => x.trim() === header);
305
+ if (start === -1) return [...lines, ...(lines.length ? [''] : []), header, line].join('\n').replace(/\n{3,}/g, '\n\n');
306
+ let end = lines.length;
307
+ for (let i = start + 1; i < lines.length; i++) {
308
+ if (/^\s*\[.+\]\s*$/.test(lines[i])) {
309
+ end = i;
310
+ break;
311
+ }
312
+ }
313
+ const keyRe = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
314
+ for (let i = start + 1; i < end; i++) {
315
+ if (keyRe.test(lines[i])) {
316
+ lines[i] = line;
317
+ return lines.join('\n').replace(/\n{3,}/g, '\n\n');
318
+ }
319
+ }
320
+ lines.splice(end, 0, line);
321
+ return lines.join('\n').replace(/\n{3,}/g, '\n\n');
322
+ }
323
+
324
+ function ensureTrailingNewline(text = '') {
325
+ const value = String(text || '').trimEnd();
326
+ return value ? `${value}\n` : '';
327
+ }
328
+
228
329
  function upsertTopLevelTomlString(text, key, value) {
229
330
  const line = `${key} = "${value}"`;
230
331
  const lines = String(text || '').split('\n');