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 +12 -2
- package/package.json +1 -1
- package/src/cli/codex-app-command.mjs +67 -0
- package/src/cli/install-helpers.mjs +102 -1
- package/src/cli/main.mjs +189 -16
- package/src/core/codex-app.mjs +86 -3
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +1 -1
- package/src/core/init.mjs +57 -5
- package/src/core/pipeline.mjs +92 -28
- package/src/core/ppt.mjs +571 -13
- package/src/core/questions.mjs +6 -5
- package/src/core/routes.mjs +2 -2
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.
|
|
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');
|