sneakoscope 0.7.4 → 0.7.11

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
@@ -44,16 +44,18 @@ sks selftest --mock
44
44
  | Area | What it does |
45
45
  | --- | --- |
46
46
  | CLI runtime | `sks warp open` and `sks --mad` explicitly launch Codex CLI with Warp; bare `sks` only prints help/readiness surfaces. |
47
- | Codex App commands | Installs generated skills so `$Team`, `$From-Chat-IMG`, `$DFix`, `$QA-LOOP`, `$Goal`, `$DB`, `$Wiki`, `$Help`, and related routes are visible in prompt workflows. |
47
+ | 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. |
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 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. |
50
50
  | Skill dreaming | Records cheap generated-skill usage counters in JSON and only periodically scans `.agents/skills` for keep, merge, prune, and improvement candidates. Reports are recommendation-only and never delete skills automatically. |
51
51
  | From-Chat-IMG | Turns chat screenshots plus original attachments into source-bound work orders, then requires scoped QA evidence before completion. |
52
52
  | QA loop | Dogfoods UI/API behavior with safety gates, Codex Computer Use-only UI evidence, safe fixes, and rechecks. |
53
+ | PPT pipeline | Uses `$PPT` for simple, restrained, information-first HTML/PDF presentation artifacts, first asking delivery context, audience profile, STP strategy, decision context, and 3+ pain-point to solution/aha mappings before source research, design-system work, HTML/PDF export, and render QA. Editable source HTML is preserved under `source-html/`, PPT-only temporary build files are cleaned after completion, and generated image assets must prefer Codex App built-in image generation through `$imagegen`. |
53
54
  | Computer Use fast lane | Uses `$Computer-Use` / `$CU` for UI/browser/visual work that needs maximum speed: skip Team debate and upfront TriWiki loops, use Codex Computer Use directly, then refresh/validate TriWiki and run Honest Mode at final closeout. |
54
55
  | Goal | Provides a fast SKS bridge overlay for Codex native persisted `/goal` create, pause, resume, and clear controls; implementation continues through the selected SKS execution route. |
55
56
  | TriWiki voxels | Maintains `.sneakoscope/wiki/context-pack.json` as the context SSOT with coordinate anchors, voxel metadata, `attention.use_first`, and `attention.hydrate_first`. |
56
57
  | Context7 | Requires current docs for external packages, APIs, MCPs, SDKs, and framework/runtime behavior when correctness depends on current guidance. |
58
+ | getdesign.md | Grounds `design.md`, UI/UX design systems, and presentation-like HTML/PDF artifacts in the official getdesign.md reference and Codex skill path. |
57
59
  | DB safety | Treats SQL, migrations, Supabase, RLS, and destructive operations as high risk. |
58
60
  | Release hygiene | Checks versioning, changelog, package contents, tarball size, syntax, selftests, and dry-run publishing. |
59
61
 
@@ -95,6 +97,8 @@ sks bootstrap
95
97
 
96
98
  Project setup writes shared `.gitignore` entries for generated SKS files: `.sneakoscope/`, `.codex/`, `.agents/`, and managed `AGENTS.md`. Use `sks setup --local-only` when you want those excludes kept only in `.git/info/exclude`.
97
99
 
100
+ During npm postinstall, SKS also installs generated Codex App skills and tries the official getdesign Codex skill command, `skills add MohtashamMurshid/getdesign`, when the `skills` CLI is available. If that CLI is missing, setup still installs the generated `getdesign-reference` skill so UI/UX and presentation design-system work keeps using [getdesign.md](https://getdesign.md/) as a required reference.
101
+
98
102
  ### One-Shot Install
99
103
 
100
104
  Use this when you do not want to keep a global install:
@@ -230,7 +234,7 @@ sks code-structure scan --json
230
234
 
231
235
  `sks pipeline plan` is the 0.7 runtime map. It reads or refreshes `.sneakoscope/missions/<id>/pipeline-plan.json`, then shows which lane is active, which stages are kept or skipped, which verification commands are required, and whether the no-unrequested-fallback invariant is present.
232
236
 
233
- `sks proof-field scan` is SKS's lightweight outcome rubric: it maps the goal to proof cones, records unrelated work that can be skipped with evidence, reports a simplicity score, and names escalation triggers for when the route must return to the full Team/Honest proof path. When `execution_lane.lane` is `proof_field_fast_lane`, SKS can keep the parent-owned minimal patch plus listed verification and skip Team debate, fresh executor teams, broad route rework, and unrelated checks. Database, security, visual-forensic, unknown, broad, failed, or unsupported-claim signals fail closed to the normal Team/Honest path. Use `sks pipeline plan --proof-field` after changed files are known to bind that Proof Field decision to the mission plan.
237
+ `sks proof-field scan` is SKS's lightweight outcome rubric: it maps the goal to proof cones, records unrelated work that can be skipped with evidence, reports a simplicity score, and names escalation triggers for when the route must return to the full Team/Honest proof path. The rubric embeds Hyperplan-style adversarial pressure as compact lenses instead of a new command: challenge framing, subtract surface, demand evidence, test integration risk, and consider one simpler alternative. When `execution_lane.lane` is `proof_field_fast_lane`, SKS can keep the parent-owned minimal patch plus listed verification and skip Team debate, fresh executor teams, broad route rework, and unrelated checks. Database, security, visual-forensic, unknown, broad, failed, or unsupported-claim signals fail closed to the normal Team/Honest path. Use `sks pipeline plan --proof-field` after changed files are known to bind that Proof Field decision to the mission plan.
234
238
 
235
239
  `sks skill-dream` keeps generated skill complexity bounded without doing a heavy evaluation on every prompt. Route use writes compact counters to `.sneakoscope/skills/dream-state.json`; after the configured count/cooldown threshold, or when you run `sks skill-dream run`, SKS scans `.agents/skills` and writes `.sneakoscope/reports/skill-dream-latest.json` with keep, merge, prune, and improvement candidates. The report is intentionally advisory: deleting or merging skills requires explicit approval.
236
240
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.7.4",
4
+ "version": "0.7.11",
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,163 @@
1
+ import { projectRoot, readJson, runProcess, sksRoot } from '../core/fsx.mjs';
2
+ import { getCodexInfo } from '../core/codex-adapter.mjs';
3
+ import { context7Docs, context7Resolve, context7Text, context7Tools } from '../core/context7-client.mjs';
4
+ import { context7Evidence, recordContext7Evidence } from '../core/pipeline.mjs';
5
+ import { stateFile } from '../core/mission.mjs';
6
+ import { checkContext7, ensureProjectContext7Config } from './install-helpers.mjs';
7
+
8
+ const flag = (args, name) => args.includes(name);
9
+
10
+ export async function context7Command(sub = 'check', args = []) {
11
+ const action = sub || 'check';
12
+ const setupScope = action === 'setup' ? readOption(args, '--scope', flag(args, '--global') ? 'global' : 'project') : null;
13
+ const root = action === 'setup' && setupScope === 'project' ? await projectRoot() : await sksRoot();
14
+ if (action === 'check') {
15
+ const result = await checkContext7(root);
16
+ if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
17
+ console.log('SKS Context7 MCP\n');
18
+ console.log(`Project config: ${result.project.ok ? 'ok' : 'missing'} ${result.project.path}`);
19
+ console.log(`Global config: ${result.global.ok ? 'ok' : 'missing'} ${result.global.path}`);
20
+ console.log(`Codex mcp list: ${result.codex_mcp_list.ok ? 'ok' : result.codex_mcp_list.checked ? 'missing' : 'not checked'}`);
21
+ console.log(`Ready: ${result.ok ? 'yes' : 'no'}`);
22
+ if (!result.ok) console.log('\nRun: sks context7 setup --scope project');
23
+ return;
24
+ }
25
+ if (action === 'tools') {
26
+ const result = await context7Tools({ timeoutMs: readNumberOption(args, '--timeout-ms', 30000) });
27
+ if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
28
+ console.log('SKS Context7 Local MCP Tools\n');
29
+ console.log(`Server: ${result.server.info?.name || 'context7'} ${result.server.info?.version || ''}`.trim());
30
+ console.log(`Command: ${result.server.command} ${result.server.args.join(' ')}`);
31
+ console.log(`Tools: ${result.tool_names.join(', ') || 'none'}`);
32
+ if (!result.tool_names.includes('resolve-library-id') || !result.tool_names.some((name) => name === 'query-docs' || name === 'get-library-docs')) {
33
+ process.exitCode = 1;
34
+ console.log('\nContext7 local MCP is missing the required resolve/docs tools.');
35
+ }
36
+ return;
37
+ }
38
+ if (action === 'resolve') {
39
+ const positional = positionalArgs(args);
40
+ const libraryName = positional.join(' ').trim();
41
+ if (!libraryName) throw new Error('Usage: sks context7 resolve <library-name> [--query "..."] [--json]');
42
+ const result = await context7Resolve(libraryName, {
43
+ query: readOption(args, '--query', libraryName),
44
+ timeoutMs: readNumberOption(args, '--timeout-ms', 30000)
45
+ });
46
+ if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
47
+ console.log('SKS Context7 Resolve\n');
48
+ console.log(`Library: ${libraryName}`);
49
+ console.log(`ID: ${result.library_id || 'not resolved'}`);
50
+ console.log(`Server: ${result.server.info?.name || 'context7'} ${result.server.info?.version || ''}`.trim());
51
+ const text = context7Text(result.result).split(/\n/).slice(0, 24).join('\n').trim();
52
+ if (text) console.log(`\n${text}`);
53
+ if (!result.ok || !result.library_id) process.exitCode = 1;
54
+ return;
55
+ }
56
+ if (action === 'docs') {
57
+ const positional = positionalArgs(args);
58
+ const libraryNameOrId = positional.join(' ').trim();
59
+ if (!libraryNameOrId) throw new Error('Usage: sks context7 docs <library-name|/org/project> [--query "..."] [--topic "..."] [--tokens N] [--json]');
60
+ const result = await context7Docs(libraryNameOrId, {
61
+ query: readOption(args, '--query', readOption(args, '--topic', libraryNameOrId)),
62
+ topic: readOption(args, '--topic', libraryNameOrId),
63
+ tokens: readNumberOption(args, '--tokens', 2000),
64
+ timeoutMs: readNumberOption(args, '--timeout-ms', 30000)
65
+ });
66
+ if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
67
+ printContext7DocsResult(result, { title: 'SKS Context7 Docs' });
68
+ if (!result.ok) process.exitCode = 1;
69
+ return;
70
+ }
71
+ if (action === 'evidence') {
72
+ const positional = positionalArgs(args);
73
+ const missionArg = positional.shift();
74
+ const libraryNameOrId = positional.join(' ').trim();
75
+ if (!missionArg || !libraryNameOrId) throw new Error('Usage: sks context7 evidence <mission-id|latest> <library-name|/org/project> [--query "..."] [--topic "..."] [--tokens N] [--json]');
76
+ const missionId = await resolveMissionId(root, missionArg);
77
+ if (!missionId) throw new Error('No mission found for Context7 evidence.');
78
+ const result = await context7Docs(libraryNameOrId, {
79
+ query: readOption(args, '--query', readOption(args, '--topic', libraryNameOrId)),
80
+ topic: readOption(args, '--topic', libraryNameOrId),
81
+ tokens: readNumberOption(args, '--tokens', 2000),
82
+ timeoutMs: readNumberOption(args, '--timeout-ms', 30000)
83
+ });
84
+ const state = { ...(await readJson(stateFile(root), {})), mission_id: missionId };
85
+ await recordContext7Evidence(root, state, { tool_name: 'resolve-library-id', library: libraryNameOrId, library_id: result.library_id, source: result.resolve ? 'sks context7 evidence' : 'sks context7 evidence explicit-library-id' });
86
+ if (result.docs_tool) {
87
+ await recordContext7Evidence(root, state, { tool_name: result.docs_tool, library_id: result.library_id, source: 'sks context7 evidence' });
88
+ }
89
+ const evidence = await context7Evidence(root, state);
90
+ const out = { ...result, mission_id: missionId, evidence };
91
+ if (flag(args, '--json')) return console.log(JSON.stringify(out, null, 2));
92
+ printContext7DocsResult(result, { title: 'SKS Context7 Evidence' });
93
+ console.log(`\nMission: ${missionId}`);
94
+ console.log(`Evidence: ${evidence.ok ? 'ok' : 'missing'} resolve=${evidence.resolve ? 'yes' : 'no'} docs=${evidence.docs ? 'yes' : 'no'} events=${evidence.count}`);
95
+ if (!result.ok || !evidence.ok) process.exitCode = 1;
96
+ return;
97
+ }
98
+ if (action === 'setup') {
99
+ const scope = setupScope;
100
+ const transport = readOption(args, '--transport', flag(args, '--remote') ? 'remote' : 'local');
101
+ if (!['project', 'global'].includes(scope)) throw new Error('Invalid Context7 scope. Use project or global.');
102
+ if (!['local', 'remote'].includes(transport)) throw new Error('Invalid Context7 transport. Use local or remote.');
103
+ if (scope === 'project') {
104
+ const changed = await ensureProjectContext7Config(root, transport);
105
+ const result = await checkContext7(root);
106
+ if (flag(args, '--json')) return console.log(JSON.stringify({ changed, ...result }, null, 2));
107
+ console.log(`Context7 project MCP ${changed ? 'configured' : 'already configured'} in .codex/config.toml`);
108
+ console.log(`Ready: ${result.ok ? 'yes' : 'no'}`);
109
+ return;
110
+ }
111
+ const codex = await getCodexInfo();
112
+ if (!codex.bin) throw new Error('Codex CLI missing. Install separately: npm i -g @openai/codex, or set SKS_CODEX_BIN.');
113
+ const cmdArgs = transport === 'remote'
114
+ ? ['mcp', 'add', 'context7', '--url', 'https://mcp.context7.com/mcp']
115
+ : ['mcp', 'add', 'context7', '--', 'npx', '-y', '@upstash/context7-mcp@latest'];
116
+ const result = await runProcess(codex.bin, cmdArgs, { timeoutMs: 30000, maxOutputBytes: 64 * 1024 });
117
+ if (flag(args, '--json')) return console.log(JSON.stringify({ command: `${codex.bin} ${cmdArgs.join(' ')}`, result }, null, 2));
118
+ if (result.code !== 0) throw new Error(result.stderr || result.stdout || 'codex mcp add failed');
119
+ console.log('Context7 global MCP configured.');
120
+ return;
121
+ }
122
+ throw new Error(`Unknown context7 command: ${action}`);
123
+ }
124
+
125
+ function printContext7DocsResult(result, opts = {}) {
126
+ console.log(`${opts.title || 'SKS Context7 Docs'}\n`);
127
+ console.log(`Library ID: ${result.library_id || 'not resolved'}`);
128
+ console.log(`Docs tool: ${result.docs_tool || 'missing'}`);
129
+ console.log(`Server: ${result.server?.info?.name || 'context7'} ${result.server?.info?.version || ''}`.trim());
130
+ const text = context7Text(result.docs).split(/\n/).slice(0, 48).join('\n').trim();
131
+ if (text) console.log(`\n${text}`);
132
+ if (result.error) console.log(`\nError: ${result.error}`);
133
+ }
134
+
135
+ function readOption(args, name, fallback) {
136
+ const i = args.indexOf(name);
137
+ return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
138
+ }
139
+
140
+ function readNumberOption(args, name, fallback) {
141
+ const raw = readOption(args, name, null);
142
+ if (raw == null) return fallback;
143
+ const n = Number(raw);
144
+ return Number.isFinite(n) ? n : fallback;
145
+ }
146
+
147
+ function positionalArgs(args = []) {
148
+ const out = [];
149
+ for (let i = 0; i < args.length; i++) {
150
+ const arg = args[i];
151
+ if (String(arg).startsWith('--')) {
152
+ if (args[i + 1] && !String(args[i + 1]).startsWith('--')) i++;
153
+ continue;
154
+ }
155
+ out.push(arg);
156
+ }
157
+ return out;
158
+ }
159
+
160
+ async function resolveMissionId(root, arg) {
161
+ const { findLatestMission } = await import('../core/mission.mjs');
162
+ return (!arg || arg === 'latest') ? findLatestMission(root) : arg;
163
+ }
@@ -0,0 +1,326 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import fsp from 'node:fs/promises';
4
+ import readline from 'node:readline/promises';
5
+ import { stdin as input, stdout as output } from 'node:process';
6
+ import { ensureDir, exists, packageRoot, runProcess, which, writeTextAtomic } from '../core/fsx.mjs';
7
+ import { getCodexInfo } from '../core/codex-adapter.mjs';
8
+ import { formatHarnessConflictReport, llmHarnessCleanupPrompt, scanHarnessConflicts } from '../core/harness-conflicts.mjs';
9
+ import { installSkills } from '../core/init.mjs';
10
+ import { context7ConfigToml, DOLLAR_SKILL_NAMES, GETDESIGN_REFERENCE, hasContext7ConfigText, RECOMMENDED_SKILLS } from '../core/routes.mjs';
11
+ import { platformWarpInstallHint, warpReadiness } from '../core/warp-ui.mjs';
12
+
13
+ export async function postinstall({ bootstrap }) {
14
+ const installRoot = path.resolve(process.env.INIT_CWD || process.cwd());
15
+ const conflictScan = await scanHarnessConflicts(installRoot);
16
+ if (conflictScan.hard_block) {
17
+ await postinstallHarnessConflictNotice(conflictScan);
18
+ return;
19
+ }
20
+ console.log('\nSKS installed.');
21
+ const shim = await ensureSksCommandDuringInstall();
22
+ if (shim.status === 'present') console.log(`SKS command: available (${shim.command}).`);
23
+ else if (shim.status === 'created') console.log(`SKS command: shim created at ${shim.command}.`);
24
+ else if (shim.status === 'created_not_on_path') console.log(`SKS command: shim created at ${shim.command}. Add ${path.dirname(shim.command)} to PATH, or run npx -y -p sneakoscope sks.`);
25
+ else if (shim.status === 'skipped') console.log(`SKS command: skipped (${shim.reason}).`);
26
+ else console.log(`SKS command: shim unavailable. Use npx -y -p sneakoscope sks. ${shim.error || ''}`.trim());
27
+ const context7Install = await ensureGlobalContext7DuringInstall();
28
+ if (context7Install.status === 'present') console.log('Context7 MCP: already configured for Codex.');
29
+ else if (context7Install.status === 'installed') console.log('Context7 MCP: configured for Codex.');
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
+ else if (context7Install.status === 'skipped') console.log(`Context7 MCP: skipped (${context7Install.reason}).`);
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 globalSkills = await ensureGlobalCodexSkillsDuringInstall();
34
+ if (globalSkills.status === 'installed') console.log(`Codex App global $ skills: installed in ${globalSkills.root} (${globalSkills.installed_count} skills).`);
35
+ else if (globalSkills.status === 'partial') console.log(`Codex App global $ skills: partial in ${globalSkills.root}; missing ${globalSkills.missing_skills.join(', ')}. Run \`sks doctor --fix\`.`);
36
+ else if (globalSkills.status === 'skipped') console.log(`Codex App global $ skills: skipped (${globalSkills.reason}).`);
37
+ else if (globalSkills.status === 'failed') console.log(`Codex App global $ skills: auto setup failed. Run \`sks doctor --fix\`. ${globalSkills.error || ''}`.trim());
38
+ const getdesignSkill = await ensureGlobalGetdesignSkillDuringInstall();
39
+ if (getdesignSkill.status === 'installed') console.log('getdesign Codex skill: installed.');
40
+ else if (getdesignSkill.status === 'present') console.log('getdesign Codex skill: already available.');
41
+ else if (getdesignSkill.status === 'skills_cli_missing') console.log(`getdesign Codex skill: skills CLI missing; generated getdesign-reference skill is installed. Later run \`${getdesignSkill.install}\` if the skills CLI is available.`);
42
+ else if (getdesignSkill.status === 'skipped') console.log(`getdesign Codex skill: skipped (${getdesignSkill.reason}).`);
43
+ else if (getdesignSkill.status === 'failed') console.log(`getdesign Codex skill: auto setup failed; generated getdesign-reference skill remains available. ${getdesignSkill.error || ''}`.trim());
44
+ const bootstrapDecision = await postinstallBootstrapDecision(installRoot);
45
+ if (bootstrapDecision.run) {
46
+ console.log(`SKS bootstrap: ${bootstrapDecision.reason}.`);
47
+ await runPostinstallBootstrap(installRoot, bootstrap);
48
+ return;
49
+ }
50
+ console.log('\nNext:');
51
+ console.log(' sks bootstrap');
52
+ console.log(`\nSKS bootstrap was not run automatically: ${bootstrapDecision.reason}.`);
53
+ console.log('This initializes the current project, installs SKS Codex App skills, verifies Codex App/Context7 readiness, and checks warp/runtime dependencies.');
54
+ console.log('Dependency repair: sks deps check; sks deps install warp');
55
+ console.log('Open runtime after readiness is green: sks\n');
56
+ }
57
+
58
+ async function postinstallHarnessConflictNotice(conflictScan) {
59
+ console.log('\nSneakoscope Codex package installed, but SKS setup is blocked.');
60
+ console.log(formatHarnessConflictReport(conflictScan, { includePrompt: false }));
61
+ console.log('\nWhat this means: npm can finish installing the package, but `sks setup` and `sks doctor --fix` will refuse to activate SKS until the conflicting harness is removed with human approval.');
62
+ console.log('No files were removed by postinstall.');
63
+ console.log('Cleanup requires a human-approved Codex App session. Recommended model: GPT-5.5, reasoning: high.');
64
+ if (shouldAskPostinstallQuestion()) {
65
+ const answer = await askPostinstallQuestion('Show the cleanup prompt now? [y/N] ');
66
+ if (/^(y|yes|예|네|응)$/i.test(answer.trim())) {
67
+ console.log('\nCleanup prompt:\n');
68
+ console.log(llmHarnessCleanupPrompt(conflictScan));
69
+ } else {
70
+ console.log('Cleanup prompt skipped. You can print it later with: sks conflicts prompt');
71
+ }
72
+ } else {
73
+ console.log('Print the cleanup prompt later with: sks conflicts prompt');
74
+ }
75
+ console.log('After approved cleanup, rerun: sks setup && sks doctor --fix && sks selftest --mock\n');
76
+ }
77
+
78
+ function shouldAskPostinstallQuestion() {
79
+ if (process.env.SKS_POSTINSTALL_PROMPT === '1') return true;
80
+ return Boolean(input.isTTY && output.isTTY && process.env.CI !== 'true' && process.env.SKS_POSTINSTALL_NO_PROMPT !== '1');
81
+ }
82
+
83
+ export async function postinstallBootstrapDecision(root) {
84
+ if (process.env.SKS_POSTINSTALL_NO_BOOTSTRAP === '1') return { run: false, reason: 'SKS_POSTINSTALL_NO_BOOTSTRAP=1' };
85
+ if (process.env.SKS_POSTINSTALL_BOOTSTRAP === '0') return { run: false, reason: 'SKS_POSTINSTALL_BOOTSTRAP=0' };
86
+ const candidate = await isProjectSetupCandidate(path.resolve(root || process.cwd()));
87
+ if (!candidate && process.env.SKS_POSTINSTALL_BOOTSTRAP !== '1') return { run: false, reason: 'no project marker found in install cwd' };
88
+ if (process.env.SKS_POSTINSTALL_BOOTSTRAP === '1') return { run: true, reason: 'forced by SKS_POSTINSTALL_BOOTSTRAP=1' };
89
+ return { run: true, reason: 'auto-running sks setup --bootstrap --install-scope global --force' };
90
+ }
91
+
92
+ async function runPostinstallBootstrap(root, bootstrap) {
93
+ const previousCwd = process.cwd();
94
+ process.chdir(path.resolve(root || previousCwd));
95
+ try {
96
+ await bootstrap(['--from-postinstall', '--install-scope', 'global', '--force']);
97
+ } finally {
98
+ process.chdir(previousCwd);
99
+ }
100
+ }
101
+
102
+ export async function askPostinstallQuestion(question) {
103
+ const rl = readline.createInterface({ input, output });
104
+ try {
105
+ return await rl.question(question);
106
+ } finally {
107
+ rl.close();
108
+ }
109
+ }
110
+
111
+ export async function ensureSksCommandDuringInstall(opts = {}) {
112
+ if (process.env.SKS_SKIP_POSTINSTALL_SHIM === '1' && !opts.force) return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_SHIM=1' };
113
+ const pathEnv = opts.pathEnv ?? process.env.PATH ?? '';
114
+ const existing = await findCommandOnPath('sks', pathEnv);
115
+ if (isStableSksBin(existing)) return { status: 'present', command: existing };
116
+ const nodeBin = opts.nodeBin || process.execPath;
117
+ const target = opts.target || path.join(packageRoot(), 'bin', 'sks.mjs');
118
+ const dirs = candidateShimDirs(pathEnv, opts.home || process.env.HOME);
119
+ const script = process.platform === 'win32'
120
+ ? `@echo off\r\n"${nodeBin}" "${target}" %*\r\n`
121
+ : `#!/bin/sh\nexec "${nodeBin}" "${target}" "$@"\n`;
122
+ const suffix = process.platform === 'win32' ? '.cmd' : '';
123
+ let createdFallback = null;
124
+ let lastError = '';
125
+ for (const entry of dirs) {
126
+ const dest = path.join(entry.dir, `sks${suffix}`);
127
+ try {
128
+ await ensureDir(entry.dir);
129
+ await writeTextAtomic(dest, script);
130
+ if (process.platform !== 'win32') await fsp.chmod(dest, 0o755).catch(() => {});
131
+ if (entry.onPath) return { status: 'created', command: dest };
132
+ createdFallback ||= dest;
133
+ } catch (err) {
134
+ lastError = err.message;
135
+ }
136
+ }
137
+ if (createdFallback) return { status: 'created_not_on_path', command: createdFallback };
138
+ return { status: 'failed', error: lastError };
139
+ }
140
+
141
+ function candidateShimDirs(pathEnv, home) {
142
+ const seen = new Set();
143
+ const out = [];
144
+ for (const raw of String(pathEnv || '').split(path.delimiter).filter(Boolean)) {
145
+ const dir = path.resolve(raw);
146
+ if (seen.has(dir) || isTransientNpmBinPath(dir)) continue;
147
+ seen.add(dir);
148
+ out.push({ dir, onPath: true });
149
+ }
150
+ for (const raw of [home && path.join(home, '.local', 'bin'), home && path.join(home, 'bin')].filter(Boolean)) {
151
+ const dir = path.resolve(raw);
152
+ if (seen.has(dir)) continue;
153
+ seen.add(dir);
154
+ out.push({ dir, onPath: false });
155
+ }
156
+ return out;
157
+ }
158
+
159
+ async function findCommandOnPath(name, pathEnv) {
160
+ const suffixes = process.platform === 'win32' ? ['.cmd', '.exe', ''] : [''];
161
+ for (const dir of String(pathEnv || '').split(path.delimiter).filter(Boolean)) {
162
+ for (const suffix of suffixes) {
163
+ const candidate = path.join(dir, `${name}${suffix}`);
164
+ if (await exists(candidate)) return candidate;
165
+ }
166
+ }
167
+ return null;
168
+ }
169
+
170
+ async function ensureGlobalContext7DuringInstall() {
171
+ if (process.env.SKS_SKIP_POSTINSTALL_CONTEXT7 === '1') return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_CONTEXT7=1' };
172
+ const codex = await getCodexInfo().catch(() => ({}));
173
+ if (!codex.bin) return { status: 'codex_missing' };
174
+ const list = await runProcess(codex.bin, ['mcp', 'list'], { timeoutMs: 8000, maxOutputBytes: 32 * 1024 }).catch((err) => ({ code: 1, stderr: err.message, stdout: '' }));
175
+ if (list.code === 0 && /context7/i.test(`${list.stdout}\n${list.stderr}`)) return { status: 'present' };
176
+ const add = await runProcess(codex.bin, ['mcp', 'add', 'context7', '--', 'npx', '-y', '@upstash/context7-mcp@latest'], { timeoutMs: 30000, maxOutputBytes: 64 * 1024 }).catch((err) => ({ code: 1, stderr: err.message, stdout: '' }));
177
+ if (add.code === 0) return { status: 'installed' };
178
+ return { status: 'failed', error: `${add.stderr || add.stdout || 'codex mcp add failed'}`.trim() };
179
+ }
180
+
181
+ export async function ensureGlobalCodexSkillsDuringInstall(opts = {}) {
182
+ if (process.env.SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS === '1' && !opts.force) return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS=1' };
183
+ const home = opts.home || process.env.HOME || os.homedir();
184
+ if (!home) return { status: 'skipped', reason: 'home directory unavailable' };
185
+ const root = globalCodexSkillsRoot(home);
186
+ try {
187
+ const install = await installSkills(home);
188
+ const skills = await checkRequiredSkills(home, root);
189
+ return { status: skills.ok ? 'installed' : 'partial', root, installed_count: install.installed_skills.length, removed_aliases: install.removed_agent_skill_aliases, missing_skills: skills.missing };
190
+ } catch (err) {
191
+ return { status: 'failed', root, error: err.message };
192
+ }
193
+ }
194
+
195
+ async function ensureGlobalGetdesignSkillDuringInstall() {
196
+ if (process.env.SKS_SKIP_POSTINSTALL_GETDESIGN === '1') return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_GETDESIGN=1' };
197
+ const pathEnv = process.env.PATH || '';
198
+ const skillsBin = await findCommandOnPath('skills', pathEnv);
199
+ if (!skillsBin) return { status: 'skills_cli_missing', install: GETDESIGN_REFERENCE.codex_skill_install };
200
+ const add = await runProcess(skillsBin, ['add', GETDESIGN_REFERENCE.codex_skill], {
201
+ timeoutMs: 30000,
202
+ maxOutputBytes: 64 * 1024
203
+ }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
204
+ const out = `${add.stdout || ''}\n${add.stderr || ''}`;
205
+ if (add.code === 0) return { status: /already|exists|present/i.test(out) ? 'present' : 'installed', command: skillsBin };
206
+ if (/already|exists|present/i.test(out)) return { status: 'present', command: skillsBin };
207
+ return { status: 'failed', command: skillsBin, error: out.trim() || 'skills add failed' };
208
+ }
209
+
210
+ export async function ensureRelatedCliTools(args = []) {
211
+ const skip = args.includes('--skip-cli-tools') || process.env.SKS_SKIP_CLI_TOOLS === '1';
212
+ const codex = await ensureCodexCliTool({ skip });
213
+ const warp = await warpReadiness().catch((err) => ({ ok: false, version: null, error: err.message }));
214
+ return {
215
+ codex,
216
+ warp: {
217
+ ok: Boolean(warp.ok),
218
+ app: warp.app || null,
219
+ cli: warp.cli || null,
220
+ version: warp.version || null,
221
+ launch_config_dir: warp.launch_config_dir || null,
222
+ uri_scheme: warp.uri_scheme || null,
223
+ install_hint: warp.ok ? null : platformWarpInstallHint(),
224
+ error: warp.error || null
225
+ }
226
+ };
227
+ }
228
+
229
+ export async function ensureCodexCliTool({ skip = false } = {}) {
230
+ if (skip) return { status: 'skipped', reason: 'SKS_SKIP_CLI_TOOLS=1 or --skip-cli-tools' };
231
+ const before = await getCodexInfo().catch(() => ({}));
232
+ if (before.bin) return { status: 'present', bin: before.bin, version: before.version || null };
233
+ const npmBin = await which('npm');
234
+ if (!npmBin) return { status: 'failed', error: 'npm not found on PATH; install Codex CLI manually with npm i -g @openai/codex@latest.' };
235
+ const install = await runProcess(npmBin, ['i', '-g', '@openai/codex@latest'], {
236
+ timeoutMs: 120000,
237
+ maxOutputBytes: 128 * 1024
238
+ }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
239
+ if (install.code !== 0) {
240
+ return { status: 'failed', error: `${install.stderr || install.stdout || 'npm i -g @openai/codex@latest failed'}`.trim() };
241
+ }
242
+ const after = await getCodexInfo().catch(() => ({}));
243
+ return {
244
+ status: after.bin ? 'installed' : 'installed_not_on_path',
245
+ bin: after.bin || null,
246
+ version: after.version || null,
247
+ hint: after.bin ? null : 'npm completed, but codex is not on PATH. Restart the shell or set SKS_CODEX_BIN.'
248
+ };
249
+ }
250
+
251
+ async function isProjectSetupCandidate(root) {
252
+ const markers = ['package.json', '.git', 'AGENTS.md', '.codex', '.sneakoscope'];
253
+ for (const marker of markers) {
254
+ if (await exists(path.join(root, marker))) return true;
255
+ }
256
+ return false;
257
+ }
258
+
259
+ export async function checkContext7(root) {
260
+ const projectPath = path.join(root, '.codex', 'config.toml');
261
+ const globalPath = path.join(process.env.HOME || '', '.codex', 'config.toml');
262
+ const projectText = await safeReadText(projectPath);
263
+ const globalText = await safeReadText(globalPath);
264
+ const codex = await getCodexInfo().catch(() => ({}));
265
+ let list = { checked: false, ok: false, stdout: '', stderr: '' };
266
+ if (codex.bin) {
267
+ const out = await runProcess(codex.bin, ['mcp', 'list'], { timeoutMs: 8000, maxOutputBytes: 32 * 1024 }).catch((err) => ({ code: 1, stderr: err.message, stdout: '' }));
268
+ list = { checked: true, ok: out.code === 0 && /context7/i.test(`${out.stdout}\n${out.stderr}`), stdout: out.stdout || '', stderr: out.stderr || '' };
269
+ }
270
+ const result = {
271
+ project: { path: projectPath, ok: hasContext7ConfigText(projectText) },
272
+ global: { path: globalPath, ok: hasContext7ConfigText(globalText) },
273
+ codex_mcp_list: list
274
+ };
275
+ result.ok = result.project.ok || result.codex_mcp_list.ok || (result.global.ok && !list.checked);
276
+ return result;
277
+ }
278
+
279
+ export async function ensureProjectContext7Config(root, transport = 'local') {
280
+ const configPath = path.join(root, '.codex', 'config.toml');
281
+ await ensureDir(path.dirname(configPath));
282
+ const current = await safeReadText(configPath);
283
+ const block = context7ConfigToml(transport).trim();
284
+ const existingBlock = /(^|\n)\[mcp_servers\.context7\]\n[\s\S]*?(?=\n\[[^\]]+\]|\s*$)/;
285
+ if (existingBlock.test(current)) {
286
+ const next = current.replace(existingBlock, `$1${block}\n`);
287
+ if (next === current) return false;
288
+ await writeTextAtomic(configPath, next.endsWith('\n') ? next : `${next}\n`);
289
+ return true;
290
+ }
291
+ if (hasContext7ConfigText(current)) return false;
292
+ await writeTextAtomic(configPath, `${current.trimEnd()}${current.trim() ? '\n\n' : ''}${block}\n`);
293
+ return true;
294
+ }
295
+
296
+ export async function checkRequiredSkills(root, skillRoot = root ? path.join(root, '.agents', 'skills') : globalCodexSkillsRoot()) {
297
+ const missing = [];
298
+ for (const name of [...DOLLAR_SKILL_NAMES, ...RECOMMENDED_SKILLS]) {
299
+ if (!(await exists(path.join(skillRoot, name, 'SKILL.md')))) missing.push(name);
300
+ }
301
+ return { ok: missing.length === 0, root: skillRoot, missing };
302
+ }
303
+
304
+ export function globalCodexSkillsRoot(home = process.env.HOME || os.homedir()) {
305
+ return path.join(home, '.agents', 'skills');
306
+ }
307
+
308
+ function isStableSksBin(candidate) {
309
+ return Boolean(candidate) && !isTransientNpmBinPath(candidate);
310
+ }
311
+
312
+ function isTransientNpmBinPath(candidate) {
313
+ const normalized = String(candidate || '').split(path.sep).join('/');
314
+ return normalized.includes('/_npx/')
315
+ || normalized.includes('/_cacache/tmp/')
316
+ || /\/npm-cache\/_npx\//.test(normalized)
317
+ || (/\/node_modules\/\.bin\/sks$/.test(normalized) && normalized.includes('/.npm-cache/'));
318
+ }
319
+
320
+ async function safeReadText(file, fallback = '') {
321
+ try {
322
+ return await fsp.readFile(file, 'utf8');
323
+ } catch {
324
+ return fallback;
325
+ }
326
+ }