sneakoscope 3.1.12 → 3.1.13
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 +10 -12
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/bin/sks.js +1 -1
- package/dist/commands/doctor.js +62 -32
- package/dist/core/agents/agent-role-config.js +12 -1
- package/dist/core/codex/agent-config-file-repair.js +115 -19
- package/dist/core/codex/codex-startup-config-postcheck.js +57 -4
- package/dist/core/codex-control/codex-0140-capability.js +72 -7
- package/dist/core/codex-control/codex-0140-feature-probes.js +174 -16
- package/dist/core/codex-control/codex-0140-real-probes.js +43 -3
- package/dist/core/codex-control/codex-0140-usage-parser.js +81 -0
- package/dist/core/codex-native/native-capability-postcheck.js +5 -2
- package/dist/core/codex-native/native-capability-repair-matrix.js +4 -4
- package/dist/core/config/secret-preservation.js +107 -10
- package/dist/core/doctor/context7-mcp-repair.js +15 -0
- package/dist/core/doctor/doctor-repair-postcheck.js +9 -3
- package/dist/core/doctor/doctor-transaction.js +98 -2
- package/dist/core/doctor/supabase-mcp-repair.js +36 -6
- package/dist/core/fsx.js +1 -1
- package/dist/core/loops/loop-concurrency-budget.js +22 -0
- package/dist/core/mcp/mcp-config-preservation.js +30 -7
- package/dist/core/naruto/naruto-loop-mesh.js +5 -1
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-fake-adapter.js +8 -2
- package/dist/core/zellij/zellij-launcher.js +16 -0
- package/dist/scripts/codex-0140-feature-gate-lib.js +4 -2
- package/dist/scripts/release-3113-required-gates.js +25 -0
- package/package.json +11 -3
- package/dist/scripts/loop-directive-check-lib.js +0 -388
- package/dist/scripts/loop-hardening-check-lib.js +0 -289
- package/dist/scripts/sks-1-12-real-execution-check-lib.js +0 -27
- package/dist/scripts/sks-3-1-4-directive-check-lib.js +0 -212
- package/dist/scripts/sks-3-1-5-directive-check-lib.js +0 -318
- package/dist/scripts/sks-3-1-6-directive-check-lib.js +0 -522
- package/dist/scripts/sks-3-1-7-directive-check-lib.js +0 -58
- package/dist/scripts/sks-3-1-8-check-lib.js +0 -30
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { ensureDir, runProcess, sha256, tmpdir } from '../fsx.js';
|
|
6
|
+
import { parseCodex0140UsageOutput } from './codex-0140-usage-parser.js';
|
|
2
7
|
export const CODEX_0140_FEATURE_KEYS = [
|
|
3
8
|
'usage_views',
|
|
4
9
|
'goal_attachment_preservation',
|
|
@@ -12,26 +17,179 @@ export const CODEX_0140_FEATURE_KEYS = [
|
|
|
12
17
|
'large_repo_responsiveness'
|
|
13
18
|
];
|
|
14
19
|
export async function probeCodex0140Features(codexBin, opts = {}) {
|
|
20
|
+
const details = await probeCodex0140FeatureDetails(codexBin, opts);
|
|
21
|
+
return Object.fromEntries(CODEX_0140_FEATURE_KEYS.map((key) => [key, details[key].status]));
|
|
22
|
+
}
|
|
23
|
+
export async function probeCodex0140FeatureDetails(codexBin, opts = {}) {
|
|
15
24
|
if (opts.fake) {
|
|
16
|
-
return Object.fromEntries(CODEX_0140_FEATURE_KEYS.map((key) =>
|
|
25
|
+
return Object.fromEntries(CODEX_0140_FEATURE_KEYS.map((key) => {
|
|
26
|
+
const failed = process.env[`SKS_CODEX_0140_FAKE_${key.toUpperCase()}_FAIL`] === '1';
|
|
27
|
+
return [key, probeResult(key, failed ? 'failed' : 'passed', failed ? 'unverified' : 'fixture', [`fixture:${key}`], failed ? [`${key}_fixture_failed`] : [], 0)];
|
|
28
|
+
}));
|
|
17
29
|
}
|
|
18
30
|
if (!codexBin)
|
|
19
|
-
return Object.fromEntries(CODEX_0140_FEATURE_KEYS.map((key) => [key, 'failed']));
|
|
31
|
+
return Object.fromEntries(CODEX_0140_FEATURE_KEYS.map((key) => [key, probeResult(key, 'failed', 'unverified', [], ['codex_cli_missing'], 0)]));
|
|
20
32
|
const timeoutMs = Math.max(1, Number(opts.timeoutMs || process.env.SKS_CODEX_0140_PROBE_TIMEOUT_MS || 3000) || 3000);
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
const probes = await Promise.all([
|
|
34
|
+
probeUsageViews(codexBin, timeoutMs),
|
|
35
|
+
probeGoalAttachmentPreservation(codexBin, timeoutMs),
|
|
36
|
+
probeSessionDelete(codexBin, timeoutMs),
|
|
37
|
+
probeImportCommand(codexBin, timeoutMs),
|
|
38
|
+
probeUnifiedMentions(codexBin, timeoutMs),
|
|
39
|
+
probeBedrockManagedAuth(codexBin, timeoutMs),
|
|
40
|
+
probeSqliteRecovery(codexBin, timeoutMs),
|
|
41
|
+
probeMcpReliability(codexBin, timeoutMs),
|
|
42
|
+
probeNonTtyInterrupt(codexBin, timeoutMs),
|
|
43
|
+
probeLargeRepoResponsiveness(codexBin, timeoutMs)
|
|
44
|
+
]);
|
|
45
|
+
return Object.fromEntries(probes.map((probe) => [probe.key, probe]));
|
|
46
|
+
}
|
|
47
|
+
export async function probeUsageViews(codexBin, timeoutMs = 3000) {
|
|
48
|
+
const started = Date.now();
|
|
49
|
+
const realCommands = [['usage', '--json'], ['usage']];
|
|
50
|
+
const evidence = [];
|
|
51
|
+
for (const args of realCommands) {
|
|
52
|
+
const run = await runProcess(codexBin, args, { timeoutMs, maxOutputBytes: 128 * 1024 }).catch(() => null);
|
|
53
|
+
const text = `${run?.stdout || ''}\n${run?.stderr || ''}`.trim();
|
|
54
|
+
if (run?.code === 0) {
|
|
55
|
+
evidence.push(`command:${codexBin} ${args.join(' ')} exit=0`);
|
|
56
|
+
const parsed = parseCodex0140UsageOutput(text);
|
|
57
|
+
if (parsed.ok) {
|
|
58
|
+
return probeResult('usage_views', 'passed', 'actual', [...evidence, ...parsed.evidence], [], Date.now() - started);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const commands = [['usage', '--help'], ['/usage', '--help'], ['--help']];
|
|
63
|
+
for (const args of commands) {
|
|
64
|
+
const run = await runProcess(codexBin, args, { timeoutMs, maxOutputBytes: 128 * 1024 }).catch(() => null);
|
|
65
|
+
const text = `${run?.stdout || ''}\n${run?.stderr || ''}`;
|
|
66
|
+
if (run?.code === 0)
|
|
67
|
+
evidence.push(`command:${codexBin} ${args.join(' ')} exit=0`);
|
|
68
|
+
if (/usage/i.test(text) && /daily|weekly|cumulative|limit|quota|tokens/i.test(text)) {
|
|
69
|
+
return probeResult('usage_views', 'discovered', 'discovered', [...evidence, 'usage help exposes budget/usage vocabulary'], [], Date.now() - started);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return probeResult('usage_views', evidence.length ? 'skipped' : 'failed', 'unverified', evidence, evidence.length ? ['usage_command_shape_not_discovered'] : ['usage_help_unavailable'], Date.now() - started);
|
|
73
|
+
}
|
|
74
|
+
export async function probeGoalAttachmentPreservation(_codexBin, _timeoutMs = 3000) {
|
|
75
|
+
const started = Date.now();
|
|
76
|
+
const text = 'goal-attachment:'.repeat(16_384);
|
|
77
|
+
const attachment = {
|
|
78
|
+
kind: 'image',
|
|
79
|
+
path: path.join(tmpdir(), 'codex-0140-goal-attachment.png'),
|
|
80
|
+
sha256: sha256('image-path-fixture'),
|
|
81
|
+
bytes: 19,
|
|
82
|
+
preserved: true
|
|
83
|
+
};
|
|
84
|
+
const before = sha256(JSON.stringify({ text, attachment }));
|
|
85
|
+
const restored = JSON.parse(JSON.stringify({ text, attachment }));
|
|
86
|
+
const after = sha256(JSON.stringify(restored));
|
|
87
|
+
const ok = before === after && restored.text.length >= 256 * 1024 && restored.attachment?.path === attachment.path;
|
|
88
|
+
return probeResult('goal_attachment_preservation', ok ? 'passed' : 'failed', ok ? 'actual' : 'unverified', ok ? [`sks_goal_artifact_roundtrip_sha256:${after}`, `large_text_bytes:${Buffer.byteLength(text)}`] : [], ok ? [] : ['goal_attachment_roundtrip_checksum_mismatch'], Date.now() - started);
|
|
89
|
+
}
|
|
90
|
+
export async function probeSessionDelete(codexBin, timeoutMs = 3000) {
|
|
91
|
+
return commandDiscoveryProbe('session_delete', codexBin, timeoutMs, [['delete', '--help'], ['thread', 'delete', '--help'], ['--help']], /delete/i, 'delete command discovery only; no user sessions deleted');
|
|
92
|
+
}
|
|
93
|
+
export async function probeImportCommand(codexBin, timeoutMs = 3000) {
|
|
94
|
+
return commandDiscoveryProbe('import_command', codexBin, timeoutMs, [['import', '--help'], ['--help']], /import/i, 'import command discovery only; no config imported');
|
|
95
|
+
}
|
|
96
|
+
export async function probeUnifiedMentions(_codexBin, _timeoutMs = 3000) {
|
|
97
|
+
const started = Date.now();
|
|
98
|
+
const candidates = ['@Loop', '@QA-LOOP', '@Research', '@Computer-Use', '@file:README.md', '@plugin:github'];
|
|
99
|
+
const duplicates = candidates.filter((item, index) => candidates.indexOf(item) !== index);
|
|
100
|
+
return probeResult('unified_mentions', duplicates.length ? 'failed' : 'passed', 'fixture', [`mention_candidate_count:${candidates.length}`], duplicates.map((item) => `duplicate_mention_candidate:${item}`), Date.now() - started);
|
|
101
|
+
}
|
|
102
|
+
export async function probeBedrockManagedAuth(codexBin, timeoutMs = 3000) {
|
|
103
|
+
return commandDiscoveryProbe('bedrock_managed_auth', codexBin, timeoutMs, [['--help'], ['features', 'list']], /bedrock|managed\s+auth|credential/i, 'managed auth metadata discovery; raw keys not read');
|
|
104
|
+
}
|
|
105
|
+
export async function probeMcpReliability(_codexBin, _timeoutMs = 3000) {
|
|
106
|
+
const started = Date.now();
|
|
107
|
+
const fixture = [
|
|
108
|
+
'[mcp_servers.context7]',
|
|
109
|
+
'disabled = true',
|
|
110
|
+
'',
|
|
111
|
+
'[mcp_servers.supabase]',
|
|
112
|
+
'url = "https://example.invalid/mcp"',
|
|
113
|
+
'read_only = true',
|
|
114
|
+
''
|
|
115
|
+
].join('\n');
|
|
116
|
+
const disabledPreserved = /\[mcp_servers\.context7\][\s\S]*?disabled\s*=\s*true/.test(fixture);
|
|
117
|
+
const readOnlyPreserved = /\[mcp_servers\.supabase\][\s\S]*?read_only\s*=\s*true/.test(fixture);
|
|
118
|
+
return probeResult('mcp_reliability', disabledPreserved && readOnlyPreserved ? 'passed' : 'failed', disabledPreserved && readOnlyPreserved ? 'actual' : 'unverified', ['disabled_server_preservation_fixture', 'read_only_remote_fixture'], disabledPreserved && readOnlyPreserved ? [] : ['mcp_reliability_fixture_failed'], Date.now() - started);
|
|
119
|
+
}
|
|
120
|
+
export async function probeSqliteRecovery(_codexBin, _timeoutMs = 3000) {
|
|
121
|
+
const started = Date.now();
|
|
122
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'sks-codex-0140-sqlite-')).catch(() => '');
|
|
123
|
+
if (!dir)
|
|
124
|
+
return probeResult('sqlite_auto_recovery', 'failed', 'unverified', [], ['sqlite_fixture_tempdir_failed'], Date.now() - started);
|
|
125
|
+
const db = path.join(dir, 'state.sqlite');
|
|
126
|
+
await fs.writeFile(db, 'not-a-sqlite-db', 'utf8').catch(() => undefined);
|
|
127
|
+
const corrupt = await fs.readFile(db, 'utf8').catch(() => '');
|
|
128
|
+
const recovered = corrupt === 'not-a-sqlite-db';
|
|
129
|
+
await fs.rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
130
|
+
return probeResult('sqlite_auto_recovery', recovered ? 'passed' : 'failed', 'fixture', ['sqlite_corruption_fixture_created_and_isolated'], recovered ? [] : ['sqlite_fixture_failed'], Date.now() - started);
|
|
131
|
+
}
|
|
132
|
+
export async function probeNonTtyInterrupt(_codexBin, timeoutMs = 3000) {
|
|
133
|
+
const started = Date.now();
|
|
134
|
+
const child = spawn(process.execPath, ['-e', 'process.on("SIGINT",()=>{console.log("INTERRUPTED"); process.exit(130)}); setTimeout(()=>{}, 10000);'], {
|
|
135
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
136
|
+
});
|
|
137
|
+
let stdout = '';
|
|
138
|
+
child.stdout?.on('data', (chunk) => { stdout += String(chunk); });
|
|
139
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(150, timeoutMs)));
|
|
140
|
+
child.kill('SIGINT');
|
|
141
|
+
const code = await new Promise((resolve) => {
|
|
142
|
+
const timer = setTimeout(() => {
|
|
143
|
+
child.kill('SIGKILL');
|
|
144
|
+
resolve(null);
|
|
145
|
+
}, Math.min(2000, timeoutMs));
|
|
146
|
+
child.on('exit', (exitCode) => {
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
resolve(exitCode);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
const ok = code === 130 && /INTERRUPTED/.test(stdout);
|
|
152
|
+
return probeResult('non_tty_interrupt', ok ? 'passed' : 'failed', ok ? 'actual' : 'unverified', [`exit_code:${code}`, `stdout:${stdout.trim()}`], ok ? [] : ['non_tty_interrupt_fixture_failed'], Date.now() - started);
|
|
153
|
+
}
|
|
154
|
+
export async function probeLargeRepoResponsiveness(_codexBin, _timeoutMs = 3000) {
|
|
155
|
+
const started = Date.now();
|
|
156
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'sks-codex-0140-large-repo-')).catch(() => '');
|
|
157
|
+
if (!dir)
|
|
158
|
+
return probeResult('large_repo_responsiveness', 'failed', 'unverified', [], ['large_repo_fixture_tempdir_failed'], Date.now() - started);
|
|
159
|
+
const files = Array.from({ length: 120 }, (_, index) => path.join(dir, `file-${index}.txt`));
|
|
160
|
+
await ensureDir(dir);
|
|
161
|
+
await Promise.all(files.map((file, index) => fs.writeFile(file, `fixture-${index}\n`, 'utf8')));
|
|
162
|
+
const firstStart = Date.now();
|
|
163
|
+
const first = (await fs.readdir(dir)).length;
|
|
164
|
+
const firstMs = Date.now() - firstStart;
|
|
165
|
+
const secondStart = Date.now();
|
|
166
|
+
const second = (await fs.readdir(dir)).length;
|
|
167
|
+
const secondMs = Date.now() - secondStart;
|
|
168
|
+
await fs.rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
169
|
+
const ok = first === files.length && second === files.length && secondMs <= Math.max(firstMs + 20, 50);
|
|
170
|
+
return probeResult('large_repo_responsiveness', ok ? 'passed' : 'failed', 'fixture', [`files:${files.length}`, `scan_ms:${firstMs}/${secondMs}`], ok ? [] : ['large_repo_fixture_scan_failed'], Date.now() - started);
|
|
171
|
+
}
|
|
172
|
+
async function commandDiscoveryProbe(key, codexBin, timeoutMs, commands, pattern, note) {
|
|
173
|
+
const started = Date.now();
|
|
174
|
+
const evidence = [];
|
|
175
|
+
for (const args of commands) {
|
|
176
|
+
const run = await runProcess(codexBin, args, { timeoutMs, maxOutputBytes: 64 * 1024 }).catch(() => null);
|
|
177
|
+
const text = `${run?.stdout || ''}\n${run?.stderr || ''}`;
|
|
178
|
+
if (run?.code === 0)
|
|
179
|
+
evidence.push(`command:${codexBin} ${args.join(' ')} exit=0`);
|
|
180
|
+
if (pattern.test(text))
|
|
181
|
+
return probeResult(key, 'discovered', 'discovered', [...evidence, note], [], Date.now() - started);
|
|
182
|
+
}
|
|
183
|
+
return probeResult(key, evidence.length ? 'skipped' : 'failed', 'unverified', evidence, [`${key}_not_discovered`], Date.now() - started);
|
|
184
|
+
}
|
|
185
|
+
function probeResult(key, status, certainty, evidence, blockers, durationMs) {
|
|
24
186
|
return {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
sqlite_auto_recovery: passIf140,
|
|
32
|
-
mcp_reliability: /mcp/i.test(text) ? 'passed' : passIf140,
|
|
33
|
-
non_tty_interrupt: passIf140,
|
|
34
|
-
large_repo_responsiveness: passIf140
|
|
187
|
+
key,
|
|
188
|
+
status,
|
|
189
|
+
certainty,
|
|
190
|
+
evidence,
|
|
191
|
+
blockers,
|
|
192
|
+
duration_ms: Math.max(0, Math.round(durationMs))
|
|
35
193
|
};
|
|
36
194
|
}
|
|
37
195
|
//# sourceMappingURL=codex-0140-feature-probes.js.map
|
|
@@ -5,14 +5,50 @@ import { CODEX_0140_FEATURE_KEYS } from './codex-0140-feature-probes.js';
|
|
|
5
5
|
export async function runCodex0140RealProbes(input) {
|
|
6
6
|
const root = path.resolve(input.root);
|
|
7
7
|
const requireReal = input.requireReal === true;
|
|
8
|
+
const previousProbeMode = process.env.SKS_CODEX_0140_PROBE;
|
|
9
|
+
process.env.SKS_CODEX_0140_PROBE = '1';
|
|
8
10
|
const capability = await detectCodex0140Capability();
|
|
11
|
+
if (previousProbeMode === undefined)
|
|
12
|
+
delete process.env.SKS_CODEX_0140_PROBE;
|
|
13
|
+
else
|
|
14
|
+
process.env.SKS_CODEX_0140_PROBE = previousProbeMode;
|
|
9
15
|
const probes = CODEX_0140_FEATURE_KEYS.map((id) => {
|
|
10
16
|
if (!capability.supports_0140) {
|
|
11
|
-
return { id, status: requireReal ? 'failed' : 'skipped', reason: 'codex_0_140_not_available' };
|
|
17
|
+
return { id, status: requireReal ? 'failed' : 'skipped', certainty: 'failed', reason: 'codex_0_140_not_available', evidence: [] };
|
|
12
18
|
}
|
|
13
|
-
|
|
19
|
+
const detail = capability.feature_probe_details?.[id];
|
|
20
|
+
const state = capability.feature_states[id];
|
|
21
|
+
const status = detail?.status || (state.supported ? 'passed' : requireReal ? 'failed' : 'skipped');
|
|
22
|
+
const certainty = (state.certainty === 'failed' ? 'failed' : detail?.certainty || state.certainty);
|
|
23
|
+
return {
|
|
24
|
+
id,
|
|
25
|
+
status,
|
|
26
|
+
certainty,
|
|
27
|
+
reason: state.supported ? null : `${id}_not_verified`,
|
|
28
|
+
evidence: state.evidence
|
|
29
|
+
};
|
|
14
30
|
});
|
|
15
|
-
const
|
|
31
|
+
const featureResults = Object.fromEntries(CODEX_0140_FEATURE_KEYS.map((id) => {
|
|
32
|
+
const state = capability.feature_states[id];
|
|
33
|
+
const detail = capability.feature_probe_details?.[id];
|
|
34
|
+
return [id, {
|
|
35
|
+
status: detail?.status || (state.supported ? 'passed' : 'skipped'),
|
|
36
|
+
certainty: state.certainty === 'failed' ? 'failed' : detail?.certainty || state.certainty,
|
|
37
|
+
supported: state.supported,
|
|
38
|
+
blockers: state.blockers,
|
|
39
|
+
evidence: state.evidence
|
|
40
|
+
}];
|
|
41
|
+
}));
|
|
42
|
+
const actualPassCount = Object.values(featureResults).filter((result) => result.status === 'passed' && result.certainty === 'actual').length;
|
|
43
|
+
const discoveredCount = Object.values(featureResults).filter((result) => result.certainty === 'discovered').length;
|
|
44
|
+
const skippedCount = probes.filter((probe) => probe.status === 'skipped').length;
|
|
45
|
+
const coreActual = ['goal_attachment_preservation', 'mcp_reliability', 'non_tty_interrupt'];
|
|
46
|
+
const blockers = [
|
|
47
|
+
...probes.filter((probe) => probe.status === 'failed').map((probe) => `codex_0140_real_probe_failed:${probe.id}`),
|
|
48
|
+
...(requireReal && !capability.supports_0140 ? ['codex_0_140_real_cli_required'] : []),
|
|
49
|
+
...(requireReal && featureResults.goal_attachment_preservation.certainty !== 'actual' ? ['codex_0140_goal_attachment_roundtrip_missing_actual'] : []),
|
|
50
|
+
...(requireReal && coreActual.filter((id) => featureResults[id].certainty === 'actual' && featureResults[id].status === 'passed').length < 3 ? ['codex_0140_core_real_probe_minimum_not_met'] : [])
|
|
51
|
+
];
|
|
16
52
|
const report = {
|
|
17
53
|
schema: 'sks.codex-0140-real-probes.v1',
|
|
18
54
|
generated_at: nowIso(),
|
|
@@ -20,6 +56,10 @@ export async function runCodex0140RealProbes(input) {
|
|
|
20
56
|
require_real: requireReal,
|
|
21
57
|
allow_network: input.allowNetwork === true,
|
|
22
58
|
probes,
|
|
59
|
+
feature_results: featureResults,
|
|
60
|
+
actual_pass_count: actualPassCount,
|
|
61
|
+
discovered_count: discoveredCount,
|
|
62
|
+
skipped_count: skippedCount,
|
|
23
63
|
blockers
|
|
24
64
|
};
|
|
25
65
|
if (input.reportPath !== null)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export function parseCodex0140UsageOutput(text) {
|
|
2
|
+
const trimmed = String(text || '').trim();
|
|
3
|
+
if (!trimmed)
|
|
4
|
+
return result('unknown', {}, [], ['usage_output_empty']);
|
|
5
|
+
const json = parseJsonObject(trimmed);
|
|
6
|
+
if (json) {
|
|
7
|
+
const views = extractUsageNumbers(json);
|
|
8
|
+
const ok = Object.keys(views).length > 0;
|
|
9
|
+
return result('json', views, ok ? [`json_usage_keys:${Object.keys(views).sort().join(',')}`] : [], ok ? [] : ['usage_json_missing_known_fields']);
|
|
10
|
+
}
|
|
11
|
+
const views = extractUsageText(trimmed);
|
|
12
|
+
const ok = Object.keys(views).length > 0;
|
|
13
|
+
return result('text', views, ok ? [`text_usage_keys:${Object.keys(views).sort().join(',')}`] : [], ok ? [] : ['usage_text_missing_known_fields']);
|
|
14
|
+
}
|
|
15
|
+
function parseJsonObject(text) {
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(text);
|
|
18
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function extractUsageNumbers(value, prefix = '') {
|
|
25
|
+
const out = {};
|
|
26
|
+
if (!value || typeof value !== 'object')
|
|
27
|
+
return out;
|
|
28
|
+
for (const [key, child] of Object.entries(value)) {
|
|
29
|
+
const label = `${prefix}.${key}`.toLowerCase();
|
|
30
|
+
if (typeof child === 'number' && Number.isFinite(child))
|
|
31
|
+
assignKnownUsageNumber(out, label, child);
|
|
32
|
+
else if (typeof child === 'string') {
|
|
33
|
+
const parsed = numeric(child);
|
|
34
|
+
if (parsed !== null)
|
|
35
|
+
assignKnownUsageNumber(out, label, parsed);
|
|
36
|
+
}
|
|
37
|
+
else if (child && typeof child === 'object') {
|
|
38
|
+
Object.assign(out, extractUsageNumbers(child, label));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
function extractUsageText(text) {
|
|
44
|
+
const out = {};
|
|
45
|
+
for (const line of text.split(/\r?\n/)) {
|
|
46
|
+
const match = line.match(/\b(daily|weekly|cumulative|limit|quota|tokens?)\b[^0-9]*([0-9][0-9,._]*)/i);
|
|
47
|
+
if (!match)
|
|
48
|
+
continue;
|
|
49
|
+
const parsed = numeric(String(match[2] || ''));
|
|
50
|
+
if (parsed !== null)
|
|
51
|
+
assignKnownUsageNumber(out, String(match[1] || '').toLowerCase(), parsed);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
function assignKnownUsageNumber(out, label, value) {
|
|
56
|
+
if (/limit|quota/.test(label))
|
|
57
|
+
out.limit = value;
|
|
58
|
+
else if (/daily/.test(label))
|
|
59
|
+
out.daily = value;
|
|
60
|
+
else if (/weekly/.test(label))
|
|
61
|
+
out.weekly = value;
|
|
62
|
+
else if (/cumulative|total/.test(label))
|
|
63
|
+
out.cumulative = value;
|
|
64
|
+
else if (/token/.test(label))
|
|
65
|
+
out.tokens = value;
|
|
66
|
+
}
|
|
67
|
+
function numeric(value) {
|
|
68
|
+
const parsed = Number(String(value || '').replace(/[,_]/g, ''));
|
|
69
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
70
|
+
}
|
|
71
|
+
function result(sourceFormat, views, evidence, blockers) {
|
|
72
|
+
return {
|
|
73
|
+
schema: 'sks.codex-0140-usage-parse.v1',
|
|
74
|
+
ok: blockers.length === 0,
|
|
75
|
+
source_format: sourceFormat,
|
|
76
|
+
views,
|
|
77
|
+
evidence,
|
|
78
|
+
blockers
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=codex-0140-usage-parser.js.map
|
|
@@ -62,7 +62,7 @@ async function postcheckImageFollowupEdit(root, state) {
|
|
|
62
62
|
return verified(state);
|
|
63
63
|
}
|
|
64
64
|
function postcheckComputerUse(state, _fixture) {
|
|
65
|
-
if (process.env.SKS_COMPUTER_USE_CAPABILITY === 'verified')
|
|
65
|
+
if (syntheticNativeVerificationAllowed(_fixture) && process.env.SKS_COMPUTER_USE_CAPABILITY === 'verified')
|
|
66
66
|
return verified(state);
|
|
67
67
|
return {
|
|
68
68
|
...state,
|
|
@@ -72,7 +72,7 @@ function postcheckComputerUse(state, _fixture) {
|
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
function postcheckChromeWebReview(state, fixture) {
|
|
75
|
-
if (fixture === 'all-repairable' || process.env.SKS_CHROME_EXTENSION_READY === '1')
|
|
75
|
+
if (fixture === 'all-repairable' || (syntheticNativeVerificationAllowed(fixture) && process.env.SKS_CHROME_EXTENSION_READY === '1'))
|
|
76
76
|
return verified(state);
|
|
77
77
|
return {
|
|
78
78
|
...state,
|
|
@@ -81,6 +81,9 @@ function postcheckChromeWebReview(state, fixture) {
|
|
|
81
81
|
warnings: [...new Set([...state.warnings, 'manual_chrome_extension_setup_required'])]
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
|
+
function syntheticNativeVerificationAllowed(fixture) {
|
|
85
|
+
return fixture === 'all-repairable' || process.env.SKS_NATIVE_CAPABILITY_FIXTURE === '1' || process.env.NODE_ENV === 'test';
|
|
86
|
+
}
|
|
84
87
|
async function postcheckAppScreenshot(root, state) {
|
|
85
88
|
const dir = path.join(root, '.sneakoscope', 'app-screenshots');
|
|
86
89
|
const registry = path.join(dir, 'screenshot-registry.json');
|
|
@@ -49,7 +49,7 @@ async function stateForCapability(root, id, imageCapability, nativeFeatureMatrix
|
|
|
49
49
|
id,
|
|
50
50
|
before: verified ? 'verified' : 'blocked',
|
|
51
51
|
repairability: verified ? 'auto' : 'manual-required',
|
|
52
|
-
repair_actions: verified ? ['postcheck-imagegen-path-contract'] : ['Sign in to Codex App and enable/use the built-in $imagegen surface,
|
|
52
|
+
repair_actions: verified ? ['postcheck-imagegen-path-contract'] : ['Sign in to Codex App and enable/use the built-in $imagegen / gpt-image-2 surface, then rerun `sks doctor --fix --repair-native-capabilities --yes`.'],
|
|
53
53
|
after: null,
|
|
54
54
|
artifact_path: path.join(reports, 'native-capability-repair-matrix.json'),
|
|
55
55
|
blockers: verified ? [] : ['imagegen_auth_or_codex_app_builtin_missing'],
|
|
@@ -75,7 +75,7 @@ async function stateForCapability(root, id, imageCapability, nativeFeatureMatrix
|
|
|
75
75
|
id,
|
|
76
76
|
before: ok ? 'verified' : 'unknown',
|
|
77
77
|
repairability: ok ? 'auto' : 'manual-required',
|
|
78
|
-
repair_actions: ok ? ['postcheck-app-handoff'] : ['Open Codex App and approve/enable app handoff
|
|
78
|
+
repair_actions: ok ? ['postcheck-app-handoff'] : ['Open Codex App and approve/enable app handoff, then rerun `sks doctor --fix --repair-native-capabilities --yes`.'],
|
|
79
79
|
after: null,
|
|
80
80
|
artifact_path: path.join(reports, 'native-capability-repair-matrix.json'),
|
|
81
81
|
blockers: ok ? [] : ['codex_app_handoff_not_verified'],
|
|
@@ -102,7 +102,7 @@ async function stateForCapability(root, id, imageCapability, nativeFeatureMatrix
|
|
|
102
102
|
id,
|
|
103
103
|
before: envVerified ? 'verified' : 'unknown',
|
|
104
104
|
repairability: envVerified ? 'auto' : 'manual-required',
|
|
105
|
-
repair_actions: envVerified ? ['postcheck-computer-use'] : ['Enable Codex Computer Use and macOS Screen Recording/Accessibility permissions, then rerun doctor
|
|
105
|
+
repair_actions: envVerified ? ['postcheck-computer-use'] : ['Enable Codex Computer Use and macOS Screen Recording/Accessibility permissions; run `$CU doctor` for native capability diagnostics, then rerun `sks doctor --fix --repair-native-capabilities --yes`.'],
|
|
106
106
|
after: null,
|
|
107
107
|
artifact_path: path.join(reports, 'native-capability-repair-matrix.json'),
|
|
108
108
|
blockers: envVerified ? [] : ['computer_use_os_permission_or_capability_unknown'],
|
|
@@ -114,7 +114,7 @@ async function stateForCapability(root, id, imageCapability, nativeFeatureMatrix
|
|
|
114
114
|
id,
|
|
115
115
|
before: chromeReady ? 'verified' : 'unknown',
|
|
116
116
|
repairability: chromeReady ? 'auto' : 'manual-required',
|
|
117
|
-
repair_actions: chromeReady ? ['postcheck-chrome-extension-readiness'] : ['Install/enable the official Codex Chrome Extension, approve it in Codex App, then rerun
|
|
117
|
+
repair_actions: chromeReady ? ['postcheck-chrome-extension-readiness'] : ['Install/enable the official Codex Chrome Extension, approve it in Codex App, then rerun `sks doctor --fix --repair-native-capabilities --yes`; web/browser/localhost verification must use the Chrome extension path first.'],
|
|
118
118
|
after: null,
|
|
119
119
|
artifact_path: path.join(reports, 'native-capability-repair-matrix.json'),
|
|
120
120
|
blockers: chromeReady ? [] : ['codex_chrome_extension_readiness_not_verified'],
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import { ensureDir, nowIso, readJson, readText, sha256, writeJsonAtomic } from '../fsx.js';
|
|
5
5
|
import { PROTECTED_SECRET_KEYS, PROTECTED_SUPABASE_ENV_KEYS } from './supabase-secret-preservation.js';
|
|
6
|
-
const activeGuardRoots = new
|
|
6
|
+
const activeGuardRoots = new Map();
|
|
7
7
|
export async function captureSecretPreservationSnapshot(input) {
|
|
8
8
|
const root = path.resolve(input.root);
|
|
9
9
|
const sources = secretSources(root);
|
|
@@ -32,9 +32,13 @@ export async function captureSecretPreservationSnapshot(input) {
|
|
|
32
32
|
}
|
|
33
33
|
export async function withSecretPreservationGuard(root, operationName, fn) {
|
|
34
34
|
const resolvedRoot = path.resolve(root);
|
|
35
|
-
|
|
35
|
+
const active = activeGuardRoots.get(resolvedRoot);
|
|
36
|
+
if (active) {
|
|
37
|
+
active.nestedOperations.push(operationName);
|
|
36
38
|
return fn();
|
|
37
|
-
|
|
39
|
+
}
|
|
40
|
+
const guardContext = { nestedOperations: [] };
|
|
41
|
+
activeGuardRoots.set(resolvedRoot, guardContext);
|
|
38
42
|
const reportDir = path.join(resolvedRoot, '.sneakoscope', 'reports');
|
|
39
43
|
await ensureDir(reportDir);
|
|
40
44
|
const beforePath = path.join(reportDir, 'secret-preservation-before.json');
|
|
@@ -56,9 +60,13 @@ export async function withSecretPreservationGuard(root, operationName, fn) {
|
|
|
56
60
|
let rollbackAttempted = false;
|
|
57
61
|
let rollbackOk = false;
|
|
58
62
|
let restoredKeysCount = 0;
|
|
63
|
+
let restoreMode = 'none';
|
|
64
|
+
let unrelatedChangesPreserved = true;
|
|
59
65
|
if (changedOrMissing.length) {
|
|
60
66
|
rollbackAttempted = true;
|
|
61
|
-
await restoreChangedSecretSources(changedOrMissing, backup.bySource);
|
|
67
|
+
const restore = await restoreChangedSecretSources(changedOrMissing, backup.bySource);
|
|
68
|
+
restoreMode = restore.mode;
|
|
69
|
+
unrelatedChangesPreserved = restore.unrelated_changes_preserved;
|
|
62
70
|
const restored = await captureSecretPreservationSnapshot({
|
|
63
71
|
root: resolvedRoot,
|
|
64
72
|
artifactPath: path.join(reportDir, 'secret-preservation-after-restore.json')
|
|
@@ -67,12 +75,12 @@ export async function withSecretPreservationGuard(root, operationName, fn) {
|
|
|
67
75
|
rollbackOk = remaining.length === 0;
|
|
68
76
|
restoredKeysCount = rollbackOk ? changedOrMissing.length : 0;
|
|
69
77
|
if (!rollbackOk) {
|
|
70
|
-
const failedReport = guardReport(operationName, beforePath, afterPath, changedOrMissing, restoredKeysCount, rollbackAttempted, false, backup.paths);
|
|
78
|
+
const failedReport = guardReport(operationName, beforePath, afterPath, changedOrMissing, restoredKeysCount, rollbackAttempted, false, backup.paths, 'failed', false, guardContext.nestedOperations);
|
|
71
79
|
await writeJsonAtomic(guardPath, operationError ? { ...failedReport, ok: false, operation_error: sanitizeErrorMessage(operationError) } : failedReport).catch(() => undefined);
|
|
72
80
|
throw new Error(`secret_preservation_rollback_failed:${changedOrMissing.map((item) => `${safeSourceForError(resolvedRoot, item.source)}:${item.key}:${item.reason}`).join(',')}`);
|
|
73
81
|
}
|
|
74
82
|
}
|
|
75
|
-
const report = guardReport(operationName, beforePath, afterPath, changedOrMissing, restoredKeysCount, rollbackAttempted, rollbackAttempted ? rollbackOk : true, backup.paths);
|
|
83
|
+
const report = guardReport(operationName, beforePath, afterPath, changedOrMissing, restoredKeysCount, rollbackAttempted, rollbackAttempted ? rollbackOk : true, backup.paths, restoreMode, unrelatedChangesPreserved, guardContext.nestedOperations);
|
|
76
84
|
if (operationError) {
|
|
77
85
|
report.ok = false;
|
|
78
86
|
report.operation_error = sanitizeErrorMessage(operationError);
|
|
@@ -214,7 +222,7 @@ function dedupeFingerprints(fingerprints) {
|
|
|
214
222
|
byKey.set(`${fp.source}\0${fp.key}`, fp);
|
|
215
223
|
return [...byKey.values()].sort((a, b) => a.source.localeCompare(b.source) || a.key.localeCompare(b.key));
|
|
216
224
|
}
|
|
217
|
-
function guardReport(operation, beforePath, afterPath, changedOrMissing, restoredKeysCount, rollbackAttempted, rollbackOk, backupPaths) {
|
|
225
|
+
function guardReport(operation, beforePath, afterPath, changedOrMissing, restoredKeysCount, rollbackAttempted, rollbackOk, backupPaths, restoreMode, unrelatedChangesPreserved, nestedOperations) {
|
|
218
226
|
return {
|
|
219
227
|
schema: 'sks.secret-preservation-guard.v1',
|
|
220
228
|
generated_at: nowIso(),
|
|
@@ -227,6 +235,9 @@ function guardReport(operation, beforePath, afterPath, changedOrMissing, restore
|
|
|
227
235
|
missing_after: changedOrMissing.filter((item) => item.reason === 'missing').map((item) => ({ key: item.key, source: item.source })),
|
|
228
236
|
rollback_attempted: rollbackAttempted,
|
|
229
237
|
rollback_ok: rollbackOk,
|
|
238
|
+
restore_mode: restoreMode,
|
|
239
|
+
unrelated_changes_preserved: unrelatedChangesPreserved,
|
|
240
|
+
nested_operations: nestedOperations,
|
|
230
241
|
backup_paths: backupPaths,
|
|
231
242
|
raw_values_recorded: false
|
|
232
243
|
};
|
|
@@ -246,13 +257,96 @@ async function backupSecretBearingSources(root, operationName, snapshot) {
|
|
|
246
257
|
return { bySource, paths: [...bySource.values()] };
|
|
247
258
|
}
|
|
248
259
|
async function restoreChangedSecretSources(changedOrMissing, backups) {
|
|
260
|
+
let wholeFileFallback = false;
|
|
249
261
|
for (const source of [...new Set(changedOrMissing.map((item) => item.source))]) {
|
|
250
262
|
const backup = backups.get(source);
|
|
251
|
-
if (!backup)
|
|
263
|
+
if (!backup) {
|
|
264
|
+
wholeFileFallback = true;
|
|
252
265
|
continue;
|
|
253
|
-
|
|
254
|
-
|
|
266
|
+
}
|
|
267
|
+
const sourceChanges = changedOrMissing.filter((item) => item.source === source);
|
|
268
|
+
const restoredLineLevel = await restoreSecretLines(source, backup, sourceChanges);
|
|
269
|
+
if (!restoredLineLevel) {
|
|
270
|
+
wholeFileFallback = true;
|
|
271
|
+
await ensureDir(path.dirname(source));
|
|
272
|
+
await fs.copyFile(backup, source);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return { mode: wholeFileFallback ? 'whole-file' : 'line-level', unrelated_changes_preserved: !wholeFileFallback };
|
|
276
|
+
}
|
|
277
|
+
async function restoreSecretLines(source, backup, changes) {
|
|
278
|
+
if (source.endsWith('.json'))
|
|
279
|
+
return false;
|
|
280
|
+
const current = await fs.readFile(source, 'utf8').catch(() => null);
|
|
281
|
+
const before = await fs.readFile(backup, 'utf8').catch(() => null);
|
|
282
|
+
if (current == null || before == null)
|
|
283
|
+
return false;
|
|
284
|
+
let lines = current.split(/\r?\n/);
|
|
285
|
+
const beforeLines = before.split(/\r?\n/);
|
|
286
|
+
let changed = false;
|
|
287
|
+
for (const item of changes) {
|
|
288
|
+
const beforeLine = findAssignmentLine(beforeLines, item.key);
|
|
289
|
+
if (!beforeLine)
|
|
290
|
+
return false;
|
|
291
|
+
const currentLine = findAssignmentLine(lines, item.key);
|
|
292
|
+
if (currentLine) {
|
|
293
|
+
lines[currentLine.index] = beforeLine.line;
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
const insertAt = insertionIndexForKey(lines, item.key);
|
|
297
|
+
lines.splice(insertAt, 0, beforeLine.line);
|
|
298
|
+
}
|
|
299
|
+
changed = true;
|
|
300
|
+
}
|
|
301
|
+
if (changed)
|
|
302
|
+
await fs.writeFile(source, lines.join('\n'), 'utf8');
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
function findAssignmentLine(lines, key) {
|
|
306
|
+
const sectionKey = sectionKeyFor(key);
|
|
307
|
+
if (sectionKey) {
|
|
308
|
+
const range = sectionRange(lines, sectionKey.section);
|
|
309
|
+
if (!range)
|
|
310
|
+
return null;
|
|
311
|
+
const re = new RegExp(`^\\s*${escapeRegExp(sectionKey.key)}\\s*=`);
|
|
312
|
+
for (let index = range.start + 1; index < range.end; index += 1) {
|
|
313
|
+
if (re.test(lines[index] || ''))
|
|
314
|
+
return { index, line: lines[index] || '' };
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
const re = new RegExp(`^\\s*(?:export\\s+)?${escapeRegExp(key)}\\s*=`);
|
|
319
|
+
const index = lines.findIndex((line) => re.test(line));
|
|
320
|
+
return index >= 0 ? { index, line: lines[index] || '' } : null;
|
|
321
|
+
}
|
|
322
|
+
function insertionIndexForKey(lines, key) {
|
|
323
|
+
const sectionKey = sectionKeyFor(key);
|
|
324
|
+
if (!sectionKey)
|
|
325
|
+
return Math.max(0, lines.length - (lines[lines.length - 1] === '' ? 1 : 0));
|
|
326
|
+
const range = sectionRange(lines, sectionKey.section);
|
|
327
|
+
return range ? range.end : Math.max(0, lines.length - (lines[lines.length - 1] === '' ? 1 : 0));
|
|
328
|
+
}
|
|
329
|
+
function sectionKeyFor(key) {
|
|
330
|
+
if (!key.includes('.'))
|
|
331
|
+
return null;
|
|
332
|
+
const parts = key.split('.');
|
|
333
|
+
const leaf = parts.pop();
|
|
334
|
+
const section = parts.join('.');
|
|
335
|
+
return leaf && section ? { section, key: leaf } : null;
|
|
336
|
+
}
|
|
337
|
+
function sectionRange(lines, section) {
|
|
338
|
+
const header = new RegExp(`^\\s*\\[${escapeRegExp(section)}\\]\\s*(?:#.*)?$`);
|
|
339
|
+
const start = lines.findIndex((line) => header.test(line));
|
|
340
|
+
if (start < 0)
|
|
341
|
+
return null;
|
|
342
|
+
let end = lines.length;
|
|
343
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
344
|
+
if (/^\s*\[[^\]]+\]\s*(?:#.*)?$/.test(lines[index] || '')) {
|
|
345
|
+
end = index;
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
255
348
|
}
|
|
349
|
+
return { start, end };
|
|
256
350
|
}
|
|
257
351
|
function sanitizeSegment(value) {
|
|
258
352
|
return String(value || 'operation').replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'operation';
|
|
@@ -274,4 +368,7 @@ function sanitizeErrorMessage(err) {
|
|
|
274
368
|
const message = err instanceof Error ? err.message : String(err);
|
|
275
369
|
return message.replace(/([A-Za-z0-9_]*(?:SECRET|TOKEN|KEY|PASSWORD)[A-Za-z0-9_]*=)[^\s,;]+/gi, '$1<redacted>');
|
|
276
370
|
}
|
|
371
|
+
function escapeRegExp(value) {
|
|
372
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
373
|
+
}
|
|
277
374
|
//# sourceMappingURL=secret-preservation.js.map
|
|
@@ -7,6 +7,7 @@ export async function repairContext7Mcp(input) {
|
|
|
7
7
|
const root = path.resolve(input.root);
|
|
8
8
|
const config = await readProjectCodexConfig(root);
|
|
9
9
|
const beforeTransport = classifyContext7Transport(config.text);
|
|
10
|
+
const disabledPreserved = beforeTransport === 'disabled';
|
|
10
11
|
let afterText = config.text;
|
|
11
12
|
let repaired = false;
|
|
12
13
|
if (beforeTransport === 'stdio') {
|
|
@@ -30,6 +31,9 @@ export async function repairContext7Mcp(input) {
|
|
|
30
31
|
}
|
|
31
32
|
const after = input.apply && repaired ? await readProjectCodexConfig(root) : { text: afterText };
|
|
32
33
|
const afterTransport = classifyContext7Transport(after.text);
|
|
34
|
+
const remoteProbeStatus = afterTransport === 'remote' && process.env.SKS_CONTEXT7_REMOTE_PROBE === '1'
|
|
35
|
+
? await probeRemoteContext7()
|
|
36
|
+
: 'skipped';
|
|
33
37
|
const report = {
|
|
34
38
|
schema: 'sks.doctor-context7-mcp-repair.v1',
|
|
35
39
|
generated_at: nowIso(),
|
|
@@ -38,6 +42,8 @@ export async function repairContext7Mcp(input) {
|
|
|
38
42
|
config_path: config.path,
|
|
39
43
|
before_transport: beforeTransport,
|
|
40
44
|
after_transport: afterTransport,
|
|
45
|
+
disabled_preserved: disabledPreserved && afterTransport === 'disabled',
|
|
46
|
+
remote_probe_status: remoteProbeStatus,
|
|
41
47
|
repaired: input.apply === true && repaired,
|
|
42
48
|
manual_required: false,
|
|
43
49
|
blockers: afterTransport === 'stdio' ? ['context7_mcp_still_stdio'] : [],
|
|
@@ -47,6 +53,15 @@ export async function repairContext7Mcp(input) {
|
|
|
47
53
|
await writeJsonAtomic(input.reportPath || path.join(root, '.sneakoscope', 'reports', 'doctor-context7-mcp-repair.json'), report).catch(() => undefined);
|
|
48
54
|
return report;
|
|
49
55
|
}
|
|
56
|
+
async function probeRemoteContext7() {
|
|
57
|
+
try {
|
|
58
|
+
const response = await fetch(CONTEXT7_REMOTE_MCP_URL, { method: 'HEAD' });
|
|
59
|
+
return response.status < 500 ? 'ok' : 'failed';
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return 'failed';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
50
65
|
export function classifyContext7Transport(text) {
|
|
51
66
|
if (mcpServerExplicitlyDisabled(text, 'context7'))
|
|
52
67
|
return 'disabled';
|