sneakoscope 3.1.11 → 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 +8 -7
- 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 +161 -2
- package/dist/core/agents/agent-role-config.js +12 -1
- package/dist/core/codex/agent-config-file-repair.js +157 -0
- package/dist/core/codex/codex-startup-config-postcheck.js +83 -0
- package/dist/core/codex-control/codex-0140-capability.js +136 -0
- package/dist/core/codex-control/codex-0140-feature-probes.js +195 -0
- package/dist/core/codex-control/codex-0140-probe-runner.js +5 -0
- package/dist/core/codex-control/codex-0140-real-probe-summary.js +12 -0
- package/dist/core/codex-control/codex-0140-real-probes.js +69 -0
- package/dist/core/codex-control/codex-0140-usage-parser.js +81 -0
- package/dist/core/codex-native/codex-native-feature-broker.js +15 -1
- 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/config-migration-journal.js +2 -0
- package/dist/core/config/secret-preservation.js +108 -11
- package/dist/core/config/supabase-secret-preservation.js +1 -0
- package/dist/core/doctor/codex-startup-config-repair.js +40 -0
- package/dist/core/doctor/context7-mcp-repair.js +77 -0
- package/dist/core/doctor/doctor-codex-startup-repair.js +127 -15
- package/dist/core/doctor/doctor-context7-repair.js +40 -1
- package/dist/core/doctor/doctor-repair-postcheck.js +17 -0
- package/dist/core/doctor/doctor-transaction.js +126 -0
- package/dist/core/doctor/supabase-mcp-repair.js +66 -0
- 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 +53 -0
- 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/core/zellij/zellij-worker-pane-manager.js +19 -2
- package/dist/scripts/codex-0140-feature-gate-lib.js +14 -0
- package/dist/scripts/release-3112-required-gates.js +30 -0
- package/dist/scripts/release-3113-required-gates.js +25 -0
- package/package.json +38 -2
- package/dist/.sks-build-stamp.json +0 -8
- 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
|
@@ -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);
|
|
@@ -80,7 +88,7 @@ export async function withSecretPreservationGuard(root, operationName, fn) {
|
|
|
80
88
|
await writeJsonAtomic(guardPath, report).catch(() => undefined);
|
|
81
89
|
if (operationError)
|
|
82
90
|
throw operationError;
|
|
83
|
-
if (
|
|
91
|
+
if (rollbackAttempted) {
|
|
84
92
|
throw new Error(`secret_preservation_restored:${changedOrMissing.map((item) => `${safeSourceForError(resolvedRoot, item.source)}:${item.key}:${item.reason}`).join(',')}`);
|
|
85
93
|
}
|
|
86
94
|
return result;
|
|
@@ -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
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { nowIso, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { repairAgentRoleConfigs } from '../agents/agent-role-config.js';
|
|
4
|
+
import { repairAgentConfigFileReferences } from '../codex/agent-config-file-repair.js';
|
|
5
|
+
import { postcheckCodexStartupConfig } from '../codex/codex-startup-config-postcheck.js';
|
|
6
|
+
export async function repairCodexStartupConfig(input) {
|
|
7
|
+
const root = path.resolve(input.root);
|
|
8
|
+
const roleRepair = await repairAgentRoleConfigs({
|
|
9
|
+
root,
|
|
10
|
+
apply: input.apply === true,
|
|
11
|
+
reportPath: path.join(root, '.sneakoscope', 'reports', 'agent-role-config-repair.json')
|
|
12
|
+
});
|
|
13
|
+
const fileRepair = await repairAgentConfigFileReferences({
|
|
14
|
+
root,
|
|
15
|
+
apply: input.apply === true,
|
|
16
|
+
reportPath: path.join(root, '.sneakoscope', 'reports', 'agent-config-file-repair.json')
|
|
17
|
+
});
|
|
18
|
+
const postcheck = await postcheckCodexStartupConfig({
|
|
19
|
+
root,
|
|
20
|
+
reportPath: path.join(root, '.sneakoscope', 'reports', 'codex-startup-config-postcheck.json')
|
|
21
|
+
});
|
|
22
|
+
const report = {
|
|
23
|
+
schema: 'sks.codex-startup-config-repair.v1',
|
|
24
|
+
generated_at: nowIso(),
|
|
25
|
+
ok: roleRepair.ok && fileRepair.ok && postcheck.ok,
|
|
26
|
+
apply: input.apply === true,
|
|
27
|
+
role_repair: roleRepair,
|
|
28
|
+
config_file_repair: fileRepair,
|
|
29
|
+
postcheck,
|
|
30
|
+
blockers: [
|
|
31
|
+
...(roleRepair.blockers || []),
|
|
32
|
+
...fileRepair.blockers,
|
|
33
|
+
...postcheck.blockers
|
|
34
|
+
]
|
|
35
|
+
};
|
|
36
|
+
if (input.reportPath !== null)
|
|
37
|
+
await writeJsonAtomic(input.reportPath || path.join(root, '.sneakoscope', 'reports', 'codex-startup-config-repair.json'), report).catch(() => undefined);
|
|
38
|
+
return report;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=codex-startup-config-repair.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { ensureDir, nowIso, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
|
|
3
|
+
import { CONTEXT7_REMOTE_MCP_URL, mcpServerBlock, mcpServerExplicitlyDisabled, readProjectCodexConfig, replaceOrAppendMcpServerBlock } from '../mcp/mcp-config-preservation.js';
|
|
4
|
+
import { guardedWriteFile, guardContextForRoute } from '../safety/mutation-guard.js';
|
|
5
|
+
import { createRequestedScopeContract } from '../safety/requested-scope-contract.js';
|
|
6
|
+
export async function repairContext7Mcp(input) {
|
|
7
|
+
const root = path.resolve(input.root);
|
|
8
|
+
const config = await readProjectCodexConfig(root);
|
|
9
|
+
const beforeTransport = classifyContext7Transport(config.text);
|
|
10
|
+
const disabledPreserved = beforeTransport === 'disabled';
|
|
11
|
+
let afterText = config.text;
|
|
12
|
+
let repaired = false;
|
|
13
|
+
if (beforeTransport === 'stdio') {
|
|
14
|
+
afterText = replaceOrAppendMcpServerBlock(config.text, 'context7', [
|
|
15
|
+
'[mcp_servers.context7]',
|
|
16
|
+
`url = "${CONTEXT7_REMOTE_MCP_URL}"`,
|
|
17
|
+
''
|
|
18
|
+
].join('\n'));
|
|
19
|
+
repaired = afterText !== config.text;
|
|
20
|
+
}
|
|
21
|
+
if (input.apply && repaired) {
|
|
22
|
+
await ensureDir(path.dirname(config.path));
|
|
23
|
+
const backupPath = `${config.path}.context7-mcp-repair-${Date.now().toString(36)}.bak`;
|
|
24
|
+
const contract = createRequestedScopeContract({
|
|
25
|
+
route: '$Team',
|
|
26
|
+
userRequest: 'Write a scoped project backup before doctor Context7 MCP repair.',
|
|
27
|
+
projectRoot: root
|
|
28
|
+
});
|
|
29
|
+
await guardedWriteFile(guardContextForRoute(root, contract, 'doctor Context7 MCP repair backup'), backupPath, config.text).catch(() => undefined);
|
|
30
|
+
await writeTextAtomic(config.path, afterText);
|
|
31
|
+
}
|
|
32
|
+
const after = input.apply && repaired ? await readProjectCodexConfig(root) : { text: afterText };
|
|
33
|
+
const afterTransport = classifyContext7Transport(after.text);
|
|
34
|
+
const remoteProbeStatus = afterTransport === 'remote' && process.env.SKS_CONTEXT7_REMOTE_PROBE === '1'
|
|
35
|
+
? await probeRemoteContext7()
|
|
36
|
+
: 'skipped';
|
|
37
|
+
const report = {
|
|
38
|
+
schema: 'sks.doctor-context7-mcp-repair.v1',
|
|
39
|
+
generated_at: nowIso(),
|
|
40
|
+
ok: afterTransport === 'remote' || afterTransport === 'disabled' || beforeTransport === 'missing',
|
|
41
|
+
apply: input.apply === true,
|
|
42
|
+
config_path: config.path,
|
|
43
|
+
before_transport: beforeTransport,
|
|
44
|
+
after_transport: afterTransport,
|
|
45
|
+
disabled_preserved: disabledPreserved && afterTransport === 'disabled',
|
|
46
|
+
remote_probe_status: remoteProbeStatus,
|
|
47
|
+
repaired: input.apply === true && repaired,
|
|
48
|
+
manual_required: false,
|
|
49
|
+
blockers: afterTransport === 'stdio' ? ['context7_mcp_still_stdio'] : [],
|
|
50
|
+
warnings: beforeTransport === 'missing' ? ['context7_mcp_not_configured'] : []
|
|
51
|
+
};
|
|
52
|
+
if (input.reportPath !== null)
|
|
53
|
+
await writeJsonAtomic(input.reportPath || path.join(root, '.sneakoscope', 'reports', 'doctor-context7-mcp-repair.json'), report).catch(() => undefined);
|
|
54
|
+
return report;
|
|
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
|
+
}
|
|
65
|
+
export function classifyContext7Transport(text) {
|
|
66
|
+
if (mcpServerExplicitlyDisabled(text, 'context7'))
|
|
67
|
+
return 'disabled';
|
|
68
|
+
const block = mcpServerBlock(text, 'context7');
|
|
69
|
+
if (!block)
|
|
70
|
+
return 'missing';
|
|
71
|
+
if (/^\s*url\s*=/m.test(block))
|
|
72
|
+
return 'remote';
|
|
73
|
+
if (/^\s*command\s*=|stdio|npx|context7/i.test(block))
|
|
74
|
+
return 'stdio';
|
|
75
|
+
return 'unknown';
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=context7-mcp-repair.js.map
|
|
@@ -32,7 +32,7 @@ export async function runDoctorCodexStartupRepair(input) {
|
|
|
32
32
|
{ scope: 'project', path: path.join(root, '.codex', 'config.toml'), agentDir: path.join(root, '.codex', 'agents') },
|
|
33
33
|
{ scope: 'global', path: path.join(codexHome, 'config.toml'), agentDir: path.join(codexHome, 'agents') }
|
|
34
34
|
]) {
|
|
35
|
-
configs.push(await inspectOrRepairConfig(candidate, input.fix));
|
|
35
|
+
configs.push(await inspectOrRepairConfig(candidate, input.fix, input.nodeReplCommandCandidates || [], input.includeDefaultNodeReplCandidates !== false));
|
|
36
36
|
}
|
|
37
37
|
const blockers = [...roleFiles.blockers, ...configs.flatMap((entry) => entry.blockers.map((item) => `${entry.scope}:${item}`))];
|
|
38
38
|
const warnings = configs.flatMap((entry) => entry.warnings.map((item) => `${entry.scope}:${item}`));
|
|
@@ -41,6 +41,7 @@ export async function runDoctorCodexStartupRepair(input) {
|
|
|
41
41
|
...roleFiles.created.map((file) => `created missing SKS agent role config ${file}`),
|
|
42
42
|
...configs.flatMap((entry) => [
|
|
43
43
|
...entry.agent_config_files_repaired.map((file) => `${entry.scope} agent config_file now points at ${file}`),
|
|
44
|
+
...(entry.mcp_blocks_repaired || []).map((server) => `${entry.scope} MCP block repaired: ${server}`),
|
|
44
45
|
...entry.stale_mcp_blocks_removed.map((server) => `${entry.scope} stale MCP block removed: ${server}`)
|
|
45
46
|
])
|
|
46
47
|
];
|
|
@@ -69,7 +70,7 @@ export async function runDoctorCodexStartupRepair(input) {
|
|
|
69
70
|
await writeJsonAtomic(reportPath, report);
|
|
70
71
|
return report;
|
|
71
72
|
}
|
|
72
|
-
async function inspectOrRepairConfig(candidate, fix) {
|
|
73
|
+
async function inspectOrRepairConfig(candidate, fix, nodeReplCommandCandidates, includeDefaultNodeReplCandidates) {
|
|
73
74
|
const text = await readText(candidate.path, null);
|
|
74
75
|
if (text == null) {
|
|
75
76
|
return {
|
|
@@ -80,6 +81,7 @@ async function inspectOrRepairConfig(candidate, fix) {
|
|
|
80
81
|
backup_path: null,
|
|
81
82
|
agent_config_files_repaired: [],
|
|
82
83
|
stale_mcp_blocks_removed: [],
|
|
84
|
+
mcp_blocks_repaired: [],
|
|
83
85
|
optional_mcp_blocks_ignored: [],
|
|
84
86
|
blockers: [],
|
|
85
87
|
warnings: candidate.scope === 'global' ? ['codex_home_config_missing_optional'] : []
|
|
@@ -88,6 +90,7 @@ async function inspectOrRepairConfig(candidate, fix) {
|
|
|
88
90
|
let next = text;
|
|
89
91
|
const agentConfigFilesRepaired = [];
|
|
90
92
|
const staleMcpBlocksRemoved = [];
|
|
93
|
+
const mcpBlocksRepaired = [];
|
|
91
94
|
const optionalMcpBlocksIgnored = [];
|
|
92
95
|
const blockers = [];
|
|
93
96
|
const warnings = [];
|
|
@@ -111,19 +114,11 @@ async function inspectOrRepairConfig(candidate, fix) {
|
|
|
111
114
|
next = replaceOrInsertKey(next, table, 'config_file', `"${escapeToml(target)}"`);
|
|
112
115
|
agentConfigFilesRepaired.push(target);
|
|
113
116
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (!command || await commandExists(command))
|
|
120
|
-
continue;
|
|
121
|
-
warnings.push(`stale_mcp_command_missing:${server}`);
|
|
122
|
-
if (fix) {
|
|
123
|
-
next = removeTomlBlock(next, table);
|
|
124
|
-
staleMcpBlocksRemoved.push(server);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
117
|
+
const nodeReplRepair = await inspectOrRepairNodeRepl(next, fix, nodeReplCommandCandidates, includeDefaultNodeReplCandidates);
|
|
118
|
+
next = nodeReplRepair.text;
|
|
119
|
+
warnings.push(...nodeReplRepair.warnings);
|
|
120
|
+
staleMcpBlocksRemoved.push(...nodeReplRepair.removed);
|
|
121
|
+
mcpBlocksRepaired.push(...nodeReplRepair.repaired);
|
|
127
122
|
for (const server of ['supabase_sauron']) {
|
|
128
123
|
if (tomlBlock(next, `mcp_servers.${server}`))
|
|
129
124
|
optionalMcpBlocksIgnored.push(server);
|
|
@@ -147,11 +142,60 @@ async function inspectOrRepairConfig(candidate, fix) {
|
|
|
147
142
|
backup_path: backupPath,
|
|
148
143
|
agent_config_files_repaired: agentConfigFilesRepaired,
|
|
149
144
|
stale_mcp_blocks_removed: staleMcpBlocksRemoved,
|
|
145
|
+
mcp_blocks_repaired: mcpBlocksRepaired,
|
|
150
146
|
optional_mcp_blocks_ignored: optionalMcpBlocksIgnored,
|
|
151
147
|
blockers,
|
|
152
148
|
warnings
|
|
153
149
|
};
|
|
154
150
|
}
|
|
151
|
+
async function inspectOrRepairNodeRepl(text, fix, extraCandidates, includeDefaultCandidates) {
|
|
152
|
+
const server = 'node_repl';
|
|
153
|
+
const table = tomlBlock(text, `mcp_servers.${server}`);
|
|
154
|
+
const fullTable = tomlBlockWithChildren(text, `mcp_servers.${server}`);
|
|
155
|
+
const childBlocks = tomlChildBlocks(text, `mcp_servers.${server}`);
|
|
156
|
+
if (!table && childBlocks.length === 0)
|
|
157
|
+
return { text, warnings: [], removed: [], repaired: [] };
|
|
158
|
+
const command = table ? stringValue(table.text, 'command') : null;
|
|
159
|
+
if (command && await commandExists(command)) {
|
|
160
|
+
return { text, warnings: [], removed: [], repaired: [] };
|
|
161
|
+
}
|
|
162
|
+
const warnings = [table ? `stale_mcp_command_missing:${server}` : `stale_mcp_orphan_children:${server}`];
|
|
163
|
+
if (!fix)
|
|
164
|
+
return { text, warnings, removed: [], repaired: [] };
|
|
165
|
+
const replacement = await firstExistingNodeReplCommand(text, extraCandidates, includeDefaultCandidates);
|
|
166
|
+
if (replacement) {
|
|
167
|
+
if (table) {
|
|
168
|
+
return {
|
|
169
|
+
text: replaceOrInsertKey(text, table, 'command', `"${escapeToml(replacement)}"`),
|
|
170
|
+
warnings,
|
|
171
|
+
removed: [],
|
|
172
|
+
repaired: [server]
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (childBlocks.length) {
|
|
176
|
+
const firstChild = childBlocks[0];
|
|
177
|
+
if (!firstChild)
|
|
178
|
+
return { text, warnings, removed: [], repaired: [] };
|
|
179
|
+
const mainBlock = `[mcp_servers.${server}]\ncommand = "${escapeToml(replacement)}"\nargs = []\n\n`;
|
|
180
|
+
return {
|
|
181
|
+
text: `${text.slice(0, firstChild.start).trimEnd()}${firstChild.start > 0 ? '\n\n' : ''}${mainBlock}${text.slice(firstChild.start).replace(/^\n+/, '')}`,
|
|
182
|
+
warnings,
|
|
183
|
+
removed: [],
|
|
184
|
+
repaired: [server]
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const removalBlocks = [
|
|
189
|
+
...(fullTable ? [fullTable] : table ? [table] : []),
|
|
190
|
+
...childBlocks.filter((block) => !fullTable || block.start < fullTable.start || block.end > fullTable.end)
|
|
191
|
+
];
|
|
192
|
+
return {
|
|
193
|
+
text: removeBlocks(text, removalBlocks),
|
|
194
|
+
warnings,
|
|
195
|
+
removed: [server],
|
|
196
|
+
repaired: []
|
|
197
|
+
};
|
|
198
|
+
}
|
|
155
199
|
async function inspectAgentRoleFiles(root, codexHome) {
|
|
156
200
|
const dirs = [path.join(root, '.codex', 'agents'), path.join(codexHome, 'agents')];
|
|
157
201
|
const sanitized = [];
|
|
@@ -206,9 +250,38 @@ function tomlBlock(text, table) {
|
|
|
206
250
|
const end = nextHeader >= 0 ? header.lastIndex + nextHeader : text.length;
|
|
207
251
|
return { start, end, text: text.slice(start, end) };
|
|
208
252
|
}
|
|
253
|
+
function tomlBlockWithChildren(text, table) {
|
|
254
|
+
const header = new RegExp(`(^|\\n)\\s*\\[${escapeRegExp(table)}\\]\\s*(?:#.*)?(?:\\n|$)`, 'g');
|
|
255
|
+
const match = header.exec(text);
|
|
256
|
+
if (!match)
|
|
257
|
+
return null;
|
|
258
|
+
const start = match.index + (match[1] ? 1 : 0);
|
|
259
|
+
const rest = text.slice(header.lastIndex);
|
|
260
|
+
const nextHeader = rest.search(new RegExp(`\\n\\s*\\[(?!${escapeRegExp(table)}(?:\\.|\\]))[^\\]]+\\]\\s*(?:#.*)?(?:\\n|$)`));
|
|
261
|
+
const end = nextHeader >= 0 ? header.lastIndex + nextHeader : text.length;
|
|
262
|
+
return { start, end, text: text.slice(start, end) };
|
|
263
|
+
}
|
|
264
|
+
function tomlChildBlocks(text, table) {
|
|
265
|
+
const blocks = [];
|
|
266
|
+
const header = new RegExp(`(^|\\n)\\s*\\[${escapeRegExp(table)}\\.[^\\]]+\\]\\s*(?:#.*)?(?:\\n|$)`, 'g');
|
|
267
|
+
let match;
|
|
268
|
+
while ((match = header.exec(text))) {
|
|
269
|
+
const start = match.index + (match[1] ? 1 : 0);
|
|
270
|
+
const rest = text.slice(header.lastIndex);
|
|
271
|
+
const nextHeader = rest.search(new RegExp(`\\n\\s*\\[(?!${escapeRegExp(table)}\\.)[^\\]]+\\]\\s*(?:#.*)?(?:\\n|$)`));
|
|
272
|
+
const end = nextHeader >= 0 ? header.lastIndex + nextHeader : text.length;
|
|
273
|
+
blocks.push({ start, end, text: text.slice(start, end) });
|
|
274
|
+
}
|
|
275
|
+
return blocks;
|
|
276
|
+
}
|
|
209
277
|
function removeTomlBlock(text, block) {
|
|
210
278
|
return `${text.slice(0, block.start).trimEnd()}${block.start > 0 ? '\n\n' : ''}${text.slice(block.end).replace(/^\n+/, '')}`;
|
|
211
279
|
}
|
|
280
|
+
function removeBlocks(text, blocks) {
|
|
281
|
+
return [...blocks]
|
|
282
|
+
.sort((a, b) => b.start - a.start)
|
|
283
|
+
.reduce((current, block) => removeTomlBlock(current, block), text);
|
|
284
|
+
}
|
|
212
285
|
function replaceOrInsertKey(text, block, key, encodedValue) {
|
|
213
286
|
const lines = block.text.replace(/\s*$/, '').split('\n');
|
|
214
287
|
const re = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
|
|
@@ -233,6 +306,45 @@ async function commandExists(command) {
|
|
|
233
306
|
return true;
|
|
234
307
|
return false;
|
|
235
308
|
}
|
|
309
|
+
async function firstExistingNodeReplCommand(configText, extraCandidates, includeDefaultCandidates) {
|
|
310
|
+
const candidates = [
|
|
311
|
+
...extraCandidates,
|
|
312
|
+
...(includeDefaultCandidates ? [
|
|
313
|
+
process.env.SKS_NODE_REPL_COMMAND,
|
|
314
|
+
process.env.NODE_REPL_COMMAND,
|
|
315
|
+
...nodeReplCandidatesFromNodePaths([
|
|
316
|
+
...stringValues(configText, 'NODE_REPL_NODE_PATH'),
|
|
317
|
+
process.env.NODE_REPL_NODE_PATH
|
|
318
|
+
]),
|
|
319
|
+
'/Applications/Codex.app/Contents/Resources/cua_node/bin/node_repl',
|
|
320
|
+
'/Applications/Codex.app/Contents/Resources/node_repl'
|
|
321
|
+
] : [])
|
|
322
|
+
]
|
|
323
|
+
.map((item) => String(item || '').trim())
|
|
324
|
+
.filter(Boolean);
|
|
325
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
326
|
+
if (await commandExists(candidate))
|
|
327
|
+
return candidate;
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
function nodeReplCandidatesFromNodePaths(values) {
|
|
332
|
+
const out = [];
|
|
333
|
+
for (const value of values) {
|
|
334
|
+
const nodePath = String(value || '').trim();
|
|
335
|
+
if (!nodePath)
|
|
336
|
+
continue;
|
|
337
|
+
const dir = path.dirname(nodePath);
|
|
338
|
+
out.push(path.join(dir, 'node_repl'));
|
|
339
|
+
const resources = path.basename(dir) === 'bin' ? path.dirname(path.dirname(dir)) : dir;
|
|
340
|
+
out.push(path.join(resources, 'cua_node', 'bin', 'node_repl'));
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
function stringValues(text, key) {
|
|
345
|
+
const re = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]*)"`, 'gm');
|
|
346
|
+
return [...text.matchAll(re)].map((match) => String(match[1] || '')).filter(Boolean);
|
|
347
|
+
}
|
|
236
348
|
async function backupConfig(configPath, text, label) {
|
|
237
349
|
try {
|
|
238
350
|
const backupPath = `${configPath}.sks-${label}-${Date.now().toString(36)}.bak`;
|
|
@@ -49,6 +49,26 @@ async function inspectOrRepairContext7Config(candidate, fix) {
|
|
|
49
49
|
if (!block)
|
|
50
50
|
return baseConfig(candidate, { present: true, status: 'missing' });
|
|
51
51
|
if (/\burl\s*=\s*["']https:\/\/mcp\.context7\.com\/mcp["']/.test(block.text)) {
|
|
52
|
+
const childBlocks = context7ChildBlocks(text);
|
|
53
|
+
if (childBlocks.length) {
|
|
54
|
+
if (!fix) {
|
|
55
|
+
return baseConfig(candidate, {
|
|
56
|
+
present: true,
|
|
57
|
+
status: 'remote_child_env_detected',
|
|
58
|
+
warnings: ['remote_context7_child_env_unsupported_by_streamable_http']
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const next = removeBlocks(text, childBlocks).replace(/\s*$/, '\n');
|
|
62
|
+
const backupPath = await backupConfig(candidate.path, text);
|
|
63
|
+
await writeTextAtomic(candidate.path, next);
|
|
64
|
+
return baseConfig(candidate, {
|
|
65
|
+
present: true,
|
|
66
|
+
status: 'repaired_to_remote',
|
|
67
|
+
changed: true,
|
|
68
|
+
backup_path: backupPath,
|
|
69
|
+
warnings: ['remote_context7_child_env_removed']
|
|
70
|
+
});
|
|
71
|
+
}
|
|
52
72
|
return baseConfig(candidate, { present: true, status: 'already_remote' });
|
|
53
73
|
}
|
|
54
74
|
const localStdio = /@upstash\/context7-mcp|context7-mcp|command\s*=\s*["']npx(?:\s|["'])/i.test(block.text);
|
|
@@ -68,7 +88,8 @@ async function inspectOrRepairContext7Config(candidate, fix) {
|
|
|
68
88
|
});
|
|
69
89
|
}
|
|
70
90
|
const remoteBlock = `[mcp_servers.context7]\nurl = "${CONTEXT7_REMOTE_URL}"\n`;
|
|
71
|
-
const
|
|
91
|
+
const withRemote = `${text.slice(0, block.start).trimEnd()}${block.start > 0 ? '\n\n' : ''}${remoteBlock}${text.slice(block.end).replace(/^\n+/, '\n')}`.replace(/\s*$/, '\n');
|
|
92
|
+
const next = removeBlocks(withRemote, context7ChildBlocks(withRemote)).replace(/\s*$/, '\n');
|
|
72
93
|
const backupPath = await backupConfig(candidate.path, text);
|
|
73
94
|
await writeTextAtomic(candidate.path, next);
|
|
74
95
|
return baseConfig(candidate, {
|
|
@@ -79,6 +100,24 @@ async function inspectOrRepairContext7Config(candidate, fix) {
|
|
|
79
100
|
warnings: ['local_stdio_context7_replaced_with_remote_mcp']
|
|
80
101
|
});
|
|
81
102
|
}
|
|
103
|
+
function context7ChildBlocks(text) {
|
|
104
|
+
const blocks = [];
|
|
105
|
+
const header = /(^|\n)\s*\[mcp_servers\.context7\.[^\]]+\]\s*(?:#.*)?(?:\n|$)/g;
|
|
106
|
+
let match;
|
|
107
|
+
while ((match = header.exec(text))) {
|
|
108
|
+
const start = match.index + (match[1] ? 1 : 0);
|
|
109
|
+
const rest = text.slice(header.lastIndex);
|
|
110
|
+
const nextHeader = rest.search(/\n\s*\[[^\]]+\]\s*(?:#.*)?(?:\n|$)/);
|
|
111
|
+
const end = nextHeader >= 0 ? header.lastIndex + nextHeader : text.length;
|
|
112
|
+
blocks.push({ start, end, text: text.slice(start, end) });
|
|
113
|
+
}
|
|
114
|
+
return blocks;
|
|
115
|
+
}
|
|
116
|
+
function removeBlocks(text, blocks) {
|
|
117
|
+
return [...blocks]
|
|
118
|
+
.sort((a, b) => b.start - a.start)
|
|
119
|
+
.reduce((current, block) => `${current.slice(0, block.start).trimEnd()}${block.start > 0 ? '\n\n' : ''}${current.slice(block.end).replace(/^\n+/, '')}`, text);
|
|
120
|
+
}
|
|
82
121
|
function context7Block(text) {
|
|
83
122
|
const header = /(^|\n)\s*\[mcp_servers\.context7\]\s*(?:#.*)?(?:\n|$)/g;
|
|
84
123
|
const match = header.exec(text);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {} from './doctor-transaction.js';
|
|
2
|
+
export function doctorRepairPostcheck(transaction) {
|
|
3
|
+
const phases = transaction?.phases || [];
|
|
4
|
+
const requiredBlockers = phases
|
|
5
|
+
.filter((phase) => phase.required_for_ready !== false && phase.ok !== true)
|
|
6
|
+
.flatMap((phase) => phase.blockers.length ? phase.blockers : [`required_phase_not_ready:${phase.id}`]);
|
|
7
|
+
return {
|
|
8
|
+
schema: 'sks.doctor-repair-postcheck.v1',
|
|
9
|
+
ok: transaction?.postcheck_ok === true && requiredBlockers.length === 0,
|
|
10
|
+
transaction_ok: transaction?.ok === true,
|
|
11
|
+
required_ready: requiredBlockers.length === 0,
|
|
12
|
+
manual_required: phases.filter((phase) => phase.manual_required).map((phase) => phase.id),
|
|
13
|
+
optional_manual_required: phases.filter((phase) => phase.manual_required && phase.required_for_ready === false).map((phase) => phase.id),
|
|
14
|
+
blockers: [...new Set([...requiredBlockers, ...phases.flatMap((phase) => phase.blockers)])]
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=doctor-repair-postcheck.js.map
|