sneakoscope 6.1.2 → 6.2.0
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 +11 -4
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/cli/install-helpers.js +8 -3
- package/dist/config/skills-manifest.json +58 -58
- package/dist/core/agents/agent-effort-policy.js +28 -17
- package/dist/core/agents/agent-schema.js +1 -1
- package/dist/core/codex/codex-config-guard.js +178 -7
- package/dist/core/codex/codex-config-readability.js +21 -8
- package/dist/core/codex/codex-config-toml.js +14 -11
- package/dist/core/codex-app/mcp-manager.js +679 -0
- package/dist/core/codex-app/sks-menubar.js +405 -6
- package/dist/core/codex-control/codex-lb-launch-recovery.js +15 -0
- package/dist/core/codex-native/core-skill-manifest.js +1 -1
- package/dist/core/commands/mad-sks-command.js +44 -3
- package/dist/core/commands/menubar-command.js +94 -0
- package/dist/core/commands/naruto-command.js +3 -1
- package/dist/core/commands/wiki-command.js +11 -5
- package/dist/core/fsx.js +1 -0
- package/dist/core/hooks-runtime/code-pack-freshness-preflight.js +14 -8
- package/dist/core/hooks-runtime/naruto-decision-gate.js +184 -0
- package/dist/core/hooks-runtime/stop-repeat-guard.js +89 -0
- package/dist/core/hooks-runtime.js +36 -105
- package/dist/core/init/skills.js +4 -4
- package/dist/core/init.js +1 -1
- package/dist/core/managed-assets/managed-assets-manifest.js +178 -29
- package/dist/core/preflight/parallel-preflight-engine.js +16 -3
- package/dist/core/proof/route-adapter.js +1 -1
- package/dist/core/proof/selftest-proof-fixtures.js +3 -5
- package/dist/core/provider/model-router.js +15 -6
- package/dist/core/release/package-size-budget.js +4 -6
- package/dist/core/research/research-plan-markdown.js +123 -0
- package/dist/core/research/research-super-search.js +8 -4
- package/dist/core/research.js +4 -119
- package/dist/core/retention.js +70 -2
- package/dist/core/routes/dollar-manifest-lite.js +1 -1
- package/dist/core/routes.js +75 -22
- package/dist/core/subagents/agent-catalog.js +87 -15
- package/dist/core/subagents/model-policy.js +189 -48
- package/dist/core/subagents/naruto-help-contract.js +11 -4
- package/dist/core/subagents/official-subagent-preparation.js +55 -9
- package/dist/core/subagents/official-subagent-prompt.js +45 -23
- package/dist/core/subagents/thread-budget.js +1 -1
- package/dist/core/subagents/triwiki-attention.js +117 -14
- package/dist/core/triwiki/code-pack-head-freshness.js +291 -0
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-launcher.js +12 -2
- package/dist/core/zellij/zellij-pane-proof.js +9 -1
- package/dist/core/zellij/zellij-update.js +14 -1
- package/dist/scripts/canonical-test-runner.js +7 -1
- package/dist/scripts/codex-lb-fast-mode-truth-check.js +11 -1
- package/dist/scripts/codex-lb-fast-ui-preservation-check.js +9 -2
- package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +13 -2
- package/dist/scripts/codex-native-agent-role-content-check.js +13 -1
- package/dist/scripts/codex-sdk-backend-router-check.js +2 -0
- package/dist/scripts/lib/codex-sdk-gate-lib.js +4 -0
- package/dist/scripts/official-subagent-workflow-check.js +1 -1
- package/dist/scripts/packlist-performance-check.js +1 -18
- package/dist/scripts/python-codex-sdk-all-pipelines-check.js +8 -0
- package/dist/scripts/sks-menubar-install-check.js +6 -1
- package/dist/scripts/super-search-provider-interface-check.js +31 -18
- package/package.json +3 -1
- package/dist/scripts/codex-0139-feature-probes-check.js +0 -30
- package/dist/scripts/codex-0139-marketplace-source-check.js +0 -13
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readText } from '../fsx.js';
|
|
5
|
+
import { writeCodexConfigGuarded } from '../codex/codex-config-guard.js';
|
|
6
|
+
import { parseCodexConfigToml } from '../codex/codex-config-toml.js';
|
|
7
|
+
import { withFileLock } from '../locks/file-lock.js';
|
|
8
|
+
export const CODEX_MCP_LIST_SCHEMA = 'sks.menubar-mcp-list.v1';
|
|
9
|
+
export const CODEX_MCP_MUTATION_SCHEMA = 'sks.menubar-mcp-mutation.v1';
|
|
10
|
+
export function codexMcpConfigPath(homeInput) {
|
|
11
|
+
const home = path.resolve(homeInput || process.env.HOME || os.homedir());
|
|
12
|
+
return path.join(home, '.codex', 'config.toml');
|
|
13
|
+
}
|
|
14
|
+
export async function listCodexMcpServers(opts = {}) {
|
|
15
|
+
const configPath = resolveConfigPath(opts);
|
|
16
|
+
const read = await readConfigText(configPath);
|
|
17
|
+
if (!read.ok) {
|
|
18
|
+
return {
|
|
19
|
+
schema: CODEX_MCP_LIST_SCHEMA,
|
|
20
|
+
ok: false,
|
|
21
|
+
scope: 'global',
|
|
22
|
+
source: 'config_toml_static',
|
|
23
|
+
config_path: configPath,
|
|
24
|
+
servers: [],
|
|
25
|
+
blockers: [configReadBlocker(read.code)],
|
|
26
|
+
warnings: [],
|
|
27
|
+
read_error_code: read.code,
|
|
28
|
+
read_error: read.error
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const text = read.text;
|
|
32
|
+
const parsed = parseConfig(text);
|
|
33
|
+
if (!parsed.ok) {
|
|
34
|
+
return {
|
|
35
|
+
schema: CODEX_MCP_LIST_SCHEMA,
|
|
36
|
+
ok: false,
|
|
37
|
+
scope: 'global',
|
|
38
|
+
source: 'config_toml_static',
|
|
39
|
+
config_path: configPath,
|
|
40
|
+
servers: [],
|
|
41
|
+
blockers: ['codex_mcp_config_toml_parse_failed'],
|
|
42
|
+
warnings: [],
|
|
43
|
+
parse_error: parsed.error
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const rawServers = isRecord(parsed.value.mcp_servers) ? parsed.value.mcp_servers : {};
|
|
47
|
+
const servers = Object.entries(rawServers)
|
|
48
|
+
.filter(([, value]) => isRecord(value))
|
|
49
|
+
.map(([name, value]) => summarizeServer(name, value))
|
|
50
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
51
|
+
return {
|
|
52
|
+
schema: CODEX_MCP_LIST_SCHEMA,
|
|
53
|
+
ok: true,
|
|
54
|
+
scope: 'global',
|
|
55
|
+
source: 'config_toml_static',
|
|
56
|
+
config_path: configPath,
|
|
57
|
+
server_count: servers.length,
|
|
58
|
+
enabled_count: servers.filter((server) => server.enabled).length,
|
|
59
|
+
servers,
|
|
60
|
+
blockers: [],
|
|
61
|
+
warnings: ['changes_apply_to_new_codex_sessions']
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export async function addCodexMcpServer(input, opts = {}) {
|
|
65
|
+
const normalized = normalizeAddInput(input);
|
|
66
|
+
if (!normalized.ok)
|
|
67
|
+
return mutationFailure('add', null, opts, normalized.blockers);
|
|
68
|
+
return mutateCodexMcpConfig('add', normalized.value.name, opts, (before, parsed) => {
|
|
69
|
+
const servers = isRecord(parsed.mcp_servers) ? parsed.mcp_servers : {};
|
|
70
|
+
if (Object.prototype.hasOwnProperty.call(servers, normalized.value.name)) {
|
|
71
|
+
return { ok: false, blockers: ['codex_mcp_server_already_exists'] };
|
|
72
|
+
}
|
|
73
|
+
const newline = before.includes('\r\n') ? '\r\n' : '\n';
|
|
74
|
+
const block = renderServerBlock(normalized.value).replace(/\n/g, newline);
|
|
75
|
+
const separator = !before
|
|
76
|
+
? ''
|
|
77
|
+
: before.endsWith(`${newline}${newline}`)
|
|
78
|
+
? ''
|
|
79
|
+
: before.endsWith(newline)
|
|
80
|
+
? newline
|
|
81
|
+
: `${newline}${newline}`;
|
|
82
|
+
const next = `${before}${separator}${block}${newline}`;
|
|
83
|
+
return { ok: true, next, enabled: true };
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
export async function setCodexMcpServerEnabled(nameInput, enabled, opts = {}) {
|
|
87
|
+
const name = normalizeServerName(nameInput);
|
|
88
|
+
if (!name)
|
|
89
|
+
return mutationFailure(enabled ? 'enable' : 'disable', null, opts, ['invalid_codex_mcp_server_name']);
|
|
90
|
+
return mutateCodexMcpConfig(enabled ? 'enable' : 'disable', name, opts, (before, parsed) => {
|
|
91
|
+
const servers = isRecord(parsed.mcp_servers) ? parsed.mcp_servers : {};
|
|
92
|
+
if (!Object.prototype.hasOwnProperty.call(servers, name)) {
|
|
93
|
+
return { ok: false, blockers: ['codex_mcp_server_not_found'] };
|
|
94
|
+
}
|
|
95
|
+
const next = upsertServerEnabled(before, name, enabled);
|
|
96
|
+
if (next === null)
|
|
97
|
+
return { ok: false, blockers: ['codex_mcp_server_table_not_found'] };
|
|
98
|
+
return { ok: true, next, enabled };
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
export async function removeCodexMcpServer(nameInput, opts = {}) {
|
|
102
|
+
const name = normalizeServerName(nameInput);
|
|
103
|
+
if (!name)
|
|
104
|
+
return mutationFailure('remove', null, opts, ['invalid_codex_mcp_server_name']);
|
|
105
|
+
return mutateCodexMcpConfig('remove', name, opts, (before, parsed) => {
|
|
106
|
+
const servers = isRecord(parsed.mcp_servers) ? parsed.mcp_servers : {};
|
|
107
|
+
if (!Object.prototype.hasOwnProperty.call(servers, name)) {
|
|
108
|
+
return { ok: false, blockers: ['codex_mcp_server_not_found'] };
|
|
109
|
+
}
|
|
110
|
+
const next = removeServerTables(before, name);
|
|
111
|
+
if (next === null)
|
|
112
|
+
return { ok: false, blockers: ['codex_mcp_server_table_not_found'] };
|
|
113
|
+
return { ok: true, next, enabled: null };
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async function mutateCodexMcpConfig(action, name, opts, mutate) {
|
|
117
|
+
const configPath = resolveConfigPath(opts);
|
|
118
|
+
const root = path.resolve(opts.root || opts.home || path.dirname(path.dirname(configPath)));
|
|
119
|
+
const lockPath = `${configPath}.sks-mcp.lock`;
|
|
120
|
+
try {
|
|
121
|
+
return await withFileLock({ lockPath, timeoutMs: 10_000, staleMs: 60_000 }, async () => {
|
|
122
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
123
|
+
const read = await readConfigText(configPath);
|
|
124
|
+
if (!read.ok) {
|
|
125
|
+
return mutationFailure(action, name, opts, [configReadBlocker(read.code)], read.error);
|
|
126
|
+
}
|
|
127
|
+
const before = read.text;
|
|
128
|
+
const parsed = parseConfig(before);
|
|
129
|
+
if (!parsed.ok) {
|
|
130
|
+
return mutationFailure(action, name, opts, ['codex_mcp_config_toml_parse_failed'], parsed.error);
|
|
131
|
+
}
|
|
132
|
+
const change = mutate(before, parsed.value);
|
|
133
|
+
if (!change.ok)
|
|
134
|
+
return mutationFailure(action, name, opts, change.blockers);
|
|
135
|
+
const write = await writeCodexConfigGuarded({
|
|
136
|
+
root,
|
|
137
|
+
configPath,
|
|
138
|
+
before,
|
|
139
|
+
cause: `menubar-mcp-${action}`,
|
|
140
|
+
backupTag: `menubar-mcp-${action}`,
|
|
141
|
+
ownershipVerified: true,
|
|
142
|
+
verifyUnchangedBeforeWrite: true,
|
|
143
|
+
expectedBeforeExists: read.exists,
|
|
144
|
+
preserveFastUiKeys: false,
|
|
145
|
+
preserveTextFormatting: true,
|
|
146
|
+
mutate: () => change.next
|
|
147
|
+
});
|
|
148
|
+
if (!write.ok) {
|
|
149
|
+
if (write.status === 'concurrent_change_detected' && attempt < 3)
|
|
150
|
+
continue;
|
|
151
|
+
return mutationFailure(action, name, opts, [
|
|
152
|
+
write.status === 'concurrent_change_detected'
|
|
153
|
+
? 'codex_mcp_config_busy'
|
|
154
|
+
: `codex_mcp_config_write_${write.status}`
|
|
155
|
+
], undefined, write);
|
|
156
|
+
}
|
|
157
|
+
const list = await listCodexMcpServers({ ...opts, configPath });
|
|
158
|
+
const applied = list.ok === true && mutationApplied(action, name, change.enabled, list.servers);
|
|
159
|
+
if (!applied && attempt < 3)
|
|
160
|
+
continue;
|
|
161
|
+
return {
|
|
162
|
+
schema: CODEX_MCP_MUTATION_SCHEMA,
|
|
163
|
+
ok: applied,
|
|
164
|
+
action,
|
|
165
|
+
name,
|
|
166
|
+
enabled: change.enabled,
|
|
167
|
+
scope: 'global',
|
|
168
|
+
config_path: configPath,
|
|
169
|
+
changed: write.changed,
|
|
170
|
+
backup_path: write.backup_path,
|
|
171
|
+
restart_required: applied,
|
|
172
|
+
servers: list.servers,
|
|
173
|
+
blockers: applied ? [] : list.ok === true ? ['codex_mcp_config_changed_after_write'] : list.blockers,
|
|
174
|
+
warnings: ['changes_apply_to_new_codex_sessions'],
|
|
175
|
+
attempts: attempt,
|
|
176
|
+
write
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return mutationFailure(action, name, opts, ['codex_mcp_config_busy']);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
return mutationFailure(action, name, opts, [messageOf(error).startsWith('file_lock_timeout:') ? 'codex_mcp_config_busy' : 'codex_mcp_config_mutation_failed'], messageOf(error));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function mutationFailure(action, name, opts, blockers, error, write) {
|
|
187
|
+
return {
|
|
188
|
+
schema: CODEX_MCP_MUTATION_SCHEMA,
|
|
189
|
+
ok: false,
|
|
190
|
+
action,
|
|
191
|
+
name,
|
|
192
|
+
enabled: null,
|
|
193
|
+
scope: 'global',
|
|
194
|
+
config_path: resolveConfigPath(opts),
|
|
195
|
+
changed: false,
|
|
196
|
+
restart_required: false,
|
|
197
|
+
servers: [],
|
|
198
|
+
blockers,
|
|
199
|
+
warnings: [],
|
|
200
|
+
...(error ? { error } : {}),
|
|
201
|
+
...(write ? { write } : {})
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function normalizeAddInput(input) {
|
|
205
|
+
if (!isRecord(input))
|
|
206
|
+
return { ok: false, blockers: ['codex_mcp_add_payload_required'] };
|
|
207
|
+
const name = normalizeServerName(input.name);
|
|
208
|
+
const transport = String(input.transport || '').trim().toLowerCase();
|
|
209
|
+
const blockers = [];
|
|
210
|
+
if (!name)
|
|
211
|
+
blockers.push('invalid_codex_mcp_server_name');
|
|
212
|
+
if (transport !== 'stdio' && transport !== 'url')
|
|
213
|
+
blockers.push('invalid_codex_mcp_transport');
|
|
214
|
+
const value = {
|
|
215
|
+
name: name || '',
|
|
216
|
+
transport: transport === 'url' ? 'url' : 'stdio'
|
|
217
|
+
};
|
|
218
|
+
const startupTimeout = optionalPositiveNumber(input.startup_timeout_sec, 'invalid_codex_mcp_startup_timeout', blockers);
|
|
219
|
+
const toolTimeout = optionalPositiveNumber(input.tool_timeout_sec, 'invalid_codex_mcp_tool_timeout', blockers);
|
|
220
|
+
if (startupTimeout !== null)
|
|
221
|
+
value.startup_timeout_sec = startupTimeout;
|
|
222
|
+
if (toolTimeout !== null)
|
|
223
|
+
value.tool_timeout_sec = toolTimeout;
|
|
224
|
+
if (transport === 'url') {
|
|
225
|
+
const url = normalizeHttpUrl(input.url);
|
|
226
|
+
if (!url)
|
|
227
|
+
blockers.push('invalid_codex_mcp_url');
|
|
228
|
+
else
|
|
229
|
+
value.url = url;
|
|
230
|
+
const bearer = normalizeEnvKey(input.bearer_token_env_var, true);
|
|
231
|
+
if (input.bearer_token_env_var && !bearer)
|
|
232
|
+
blockers.push('invalid_codex_mcp_bearer_token_env_var');
|
|
233
|
+
if (bearer)
|
|
234
|
+
value.bearer_token_env_var = bearer;
|
|
235
|
+
}
|
|
236
|
+
if (transport === 'stdio') {
|
|
237
|
+
const command = normalizeSingleLine(input.command, 1024);
|
|
238
|
+
if (!command)
|
|
239
|
+
blockers.push('invalid_codex_mcp_command');
|
|
240
|
+
else
|
|
241
|
+
value.command = command;
|
|
242
|
+
const args = Array.isArray(input.args) ? input.args.map((arg) => normalizeSingleLine(arg, 4096)).filter((arg) => arg !== null) : [];
|
|
243
|
+
if (Array.isArray(input.args) && args.length !== input.args.length)
|
|
244
|
+
blockers.push('invalid_codex_mcp_argument');
|
|
245
|
+
if (args.length > 100)
|
|
246
|
+
blockers.push('too_many_codex_mcp_arguments');
|
|
247
|
+
if (args.length)
|
|
248
|
+
value.args = args;
|
|
249
|
+
if (input.env !== undefined) {
|
|
250
|
+
if (!isRecord(input.env))
|
|
251
|
+
blockers.push('invalid_codex_mcp_env');
|
|
252
|
+
else {
|
|
253
|
+
const env = {};
|
|
254
|
+
for (const [key, rawValue] of Object.entries(input.env)) {
|
|
255
|
+
const normalizedKey = normalizeEnvKey(key, false);
|
|
256
|
+
const normalizedValue = normalizeSingleLine(rawValue, 16 * 1024, true);
|
|
257
|
+
if (!normalizedKey || normalizedValue === null)
|
|
258
|
+
blockers.push('invalid_codex_mcp_env');
|
|
259
|
+
else
|
|
260
|
+
env[normalizedKey] = normalizedValue;
|
|
261
|
+
}
|
|
262
|
+
if (Object.keys(env).length > 64)
|
|
263
|
+
blockers.push('too_many_codex_mcp_env_vars');
|
|
264
|
+
if (Object.keys(env).length)
|
|
265
|
+
value.env = env;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return blockers.length ? { ok: false, blockers: [...new Set(blockers)] } : { ok: true, value };
|
|
270
|
+
}
|
|
271
|
+
function renderServerBlock(input) {
|
|
272
|
+
const lines = [
|
|
273
|
+
`[mcp_servers.${tomlKey(input.name)}]`,
|
|
274
|
+
'enabled = true'
|
|
275
|
+
];
|
|
276
|
+
if (input.transport === 'url') {
|
|
277
|
+
lines.push(`url = ${tomlString(input.url || '')}`);
|
|
278
|
+
if (input.bearer_token_env_var)
|
|
279
|
+
lines.push(`bearer_token_env_var = ${tomlString(input.bearer_token_env_var)}`);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
lines.push(`command = ${tomlString(input.command || '')}`);
|
|
283
|
+
if (input.args?.length)
|
|
284
|
+
lines.push(`args = [${input.args.map(tomlString).join(', ')}]`);
|
|
285
|
+
if (input.env && Object.keys(input.env).length) {
|
|
286
|
+
const entries = Object.entries(input.env).sort(([left], [right]) => left.localeCompare(right));
|
|
287
|
+
lines.push(`env = { ${entries.map(([key, value]) => `${tomlKey(key)} = ${tomlString(value)}`).join(', ')} }`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (input.startup_timeout_sec !== undefined)
|
|
291
|
+
lines.push(`startup_timeout_sec = ${input.startup_timeout_sec}`);
|
|
292
|
+
if (input.tool_timeout_sec !== undefined)
|
|
293
|
+
lines.push(`tool_timeout_sec = ${input.tool_timeout_sec}`);
|
|
294
|
+
return lines.join('\n');
|
|
295
|
+
}
|
|
296
|
+
function summarizeServer(name, value) {
|
|
297
|
+
const command = typeof value.command === 'string' ? displayCommand(value.command) : null;
|
|
298
|
+
const rawUrl = typeof value.url === 'string' ? value.url : null;
|
|
299
|
+
const args = Array.isArray(value.args) ? value.args : [];
|
|
300
|
+
const transport = rawUrl ? 'url' : command ? 'stdio' : 'unknown';
|
|
301
|
+
const url = rawUrl ? redactUrl(rawUrl) : null;
|
|
302
|
+
const envKeys = isRecord(value.env) ? Object.keys(value.env).sort() : [];
|
|
303
|
+
const summary = transport === 'url'
|
|
304
|
+
? `Remote · ${url || 'URL configured'}`
|
|
305
|
+
: transport === 'stdio'
|
|
306
|
+
? `Local · ${command}${args.length ? ` · ${args.length} arg${args.length === 1 ? '' : 's'}` : ''}`
|
|
307
|
+
: 'Configuration requires review';
|
|
308
|
+
return {
|
|
309
|
+
name,
|
|
310
|
+
enabled: value.enabled !== false,
|
|
311
|
+
transport,
|
|
312
|
+
command,
|
|
313
|
+
argument_count: args.length,
|
|
314
|
+
env_keys: envKeys,
|
|
315
|
+
url,
|
|
316
|
+
bearer_token_env_var: typeof value.bearer_token_env_var === 'string' ? value.bearer_token_env_var : null,
|
|
317
|
+
startup_timeout_sec: finiteNumberOrNull(value.startup_timeout_sec),
|
|
318
|
+
tool_timeout_sec: finiteNumberOrNull(value.tool_timeout_sec),
|
|
319
|
+
summary
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function upsertServerEnabled(text, name, enabled) {
|
|
323
|
+
const source = String(text || '');
|
|
324
|
+
const span = tableSpans(source).find((candidate) => !candidate.array && isServerRoot(candidate.path, name));
|
|
325
|
+
if (!span)
|
|
326
|
+
return null;
|
|
327
|
+
let state = 'normal';
|
|
328
|
+
for (const line of sourceLines(source, span.headerEnd, span.end)) {
|
|
329
|
+
if (state === 'normal') {
|
|
330
|
+
const match = line.content.match(/^([ \t]*enabled[ \t]*=[ \t]*)(true|false)([ \t]*(?:#.*)?)$/);
|
|
331
|
+
if (match?.[1] !== undefined && match[2] !== undefined) {
|
|
332
|
+
const valueStart = line.start + match[1].length;
|
|
333
|
+
const valueEnd = valueStart + match[2].length;
|
|
334
|
+
return `${source.slice(0, valueStart)}${enabled ? 'true' : 'false'}${source.slice(valueEnd)}`;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
state = advanceTomlMultilineState(line.content, state);
|
|
338
|
+
}
|
|
339
|
+
const newline = source.includes('\r\n') ? '\r\n' : '\n';
|
|
340
|
+
const needsLeadingNewline = span.headerEnd > 0 && !/[\r\n]/.test(source[span.headerEnd - 1] || '');
|
|
341
|
+
const insertion = `${needsLeadingNewline ? newline : ''}enabled = ${enabled ? 'true' : 'false'}${newline}`;
|
|
342
|
+
return `${source.slice(0, span.headerEnd)}${insertion}${source.slice(span.headerEnd)}`;
|
|
343
|
+
}
|
|
344
|
+
function removeServerTables(text, name) {
|
|
345
|
+
const source = String(text || '');
|
|
346
|
+
const spans = tableSpans(source).filter((span) => isServerPath(span.path, name));
|
|
347
|
+
if (!spans.length)
|
|
348
|
+
return null;
|
|
349
|
+
let next = source;
|
|
350
|
+
for (const span of [...spans].sort((left, right) => right.start - left.start)) {
|
|
351
|
+
const removalEnd = tableRemovalEnd(source, span);
|
|
352
|
+
next = `${next.slice(0, span.start)}${next.slice(removalEnd)}`;
|
|
353
|
+
}
|
|
354
|
+
return next;
|
|
355
|
+
}
|
|
356
|
+
function tableRemovalEnd(source, span) {
|
|
357
|
+
let removalEnd = span.end;
|
|
358
|
+
const lines = sourceLines(source, span.headerEnd, span.end);
|
|
359
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
360
|
+
const line = lines[index];
|
|
361
|
+
if (!line)
|
|
362
|
+
continue;
|
|
363
|
+
const trimmed = line.content.trim();
|
|
364
|
+
if (trimmed === '' || trimmed.startsWith('#')) {
|
|
365
|
+
removalEnd = line.start;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
return removalEnd;
|
|
371
|
+
}
|
|
372
|
+
function tableSpans(text) {
|
|
373
|
+
const source = String(text || '');
|
|
374
|
+
const headers = [];
|
|
375
|
+
let state = 'normal';
|
|
376
|
+
for (const line of sourceLines(source)) {
|
|
377
|
+
if (state === 'normal') {
|
|
378
|
+
const header = parseTableHeader(line.content);
|
|
379
|
+
if (header)
|
|
380
|
+
headers.push({ start: line.start, headerEnd: line.end, path: header.path, array: header.array });
|
|
381
|
+
}
|
|
382
|
+
state = advanceTomlMultilineState(line.content, state);
|
|
383
|
+
}
|
|
384
|
+
return headers.map((header, index) => ({
|
|
385
|
+
start: header.start,
|
|
386
|
+
headerEnd: header.headerEnd,
|
|
387
|
+
end: headers[index + 1]?.start ?? source.length,
|
|
388
|
+
path: header.path,
|
|
389
|
+
array: header.array
|
|
390
|
+
}));
|
|
391
|
+
}
|
|
392
|
+
function sourceLines(text, start = 0, end = text.length) {
|
|
393
|
+
const lines = [];
|
|
394
|
+
let cursor = Math.max(0, start);
|
|
395
|
+
const limit = Math.min(text.length, Math.max(cursor, end));
|
|
396
|
+
while (cursor < limit) {
|
|
397
|
+
const newlineIndex = text.indexOf('\n', cursor);
|
|
398
|
+
const lineEnd = newlineIndex === -1 || newlineIndex >= limit ? limit : newlineIndex + 1;
|
|
399
|
+
let contentEnd = newlineIndex === -1 || newlineIndex >= limit ? lineEnd : newlineIndex;
|
|
400
|
+
if (contentEnd > cursor && text[contentEnd - 1] === '\r')
|
|
401
|
+
contentEnd -= 1;
|
|
402
|
+
lines.push({ start: cursor, end: lineEnd, content: text.slice(cursor, contentEnd) });
|
|
403
|
+
cursor = lineEnd;
|
|
404
|
+
}
|
|
405
|
+
return lines;
|
|
406
|
+
}
|
|
407
|
+
function advanceTomlMultilineState(line, initial) {
|
|
408
|
+
let state = initial;
|
|
409
|
+
let quote = null;
|
|
410
|
+
let escaped = false;
|
|
411
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
412
|
+
if (state === 'multiline-basic') {
|
|
413
|
+
if (line.startsWith('"""', index) && !isEscapedAt(line, index)) {
|
|
414
|
+
state = 'normal';
|
|
415
|
+
index += 2;
|
|
416
|
+
}
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (state === 'multiline-literal') {
|
|
420
|
+
if (line.startsWith("'''", index)) {
|
|
421
|
+
state = 'normal';
|
|
422
|
+
index += 2;
|
|
423
|
+
}
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const char = line[index] || '';
|
|
427
|
+
if (quote === 'basic') {
|
|
428
|
+
if (escaped) {
|
|
429
|
+
escaped = false;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (char === '\\') {
|
|
433
|
+
escaped = true;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (char === '"')
|
|
437
|
+
quote = null;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (quote === 'literal') {
|
|
441
|
+
if (char === "'")
|
|
442
|
+
quote = null;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (char === '#')
|
|
446
|
+
break;
|
|
447
|
+
if (line.startsWith('"""', index)) {
|
|
448
|
+
state = 'multiline-basic';
|
|
449
|
+
index += 2;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (line.startsWith("'''", index)) {
|
|
453
|
+
state = 'multiline-literal';
|
|
454
|
+
index += 2;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (char === '"')
|
|
458
|
+
quote = 'basic';
|
|
459
|
+
else if (char === "'")
|
|
460
|
+
quote = 'literal';
|
|
461
|
+
}
|
|
462
|
+
return state;
|
|
463
|
+
}
|
|
464
|
+
function isEscapedAt(value, index) {
|
|
465
|
+
let slashes = 0;
|
|
466
|
+
for (let cursor = index - 1; cursor >= 0 && value[cursor] === '\\'; cursor -= 1)
|
|
467
|
+
slashes += 1;
|
|
468
|
+
return slashes % 2 === 1;
|
|
469
|
+
}
|
|
470
|
+
function parseTableHeader(line) {
|
|
471
|
+
const arrayMatch = line.match(/^\s*\[\[([^\[\]]+)\]\]\s*(?:#.*)?$/);
|
|
472
|
+
const tableMatch = arrayMatch ? null : line.match(/^\s*\[([^\[\]]+)\]\s*(?:#.*)?$/);
|
|
473
|
+
const source = arrayMatch?.[1] || tableMatch?.[1];
|
|
474
|
+
if (!source)
|
|
475
|
+
return null;
|
|
476
|
+
const segments = [];
|
|
477
|
+
let current = '';
|
|
478
|
+
let quote = null;
|
|
479
|
+
let escaped = false;
|
|
480
|
+
for (const char of source) {
|
|
481
|
+
if (quote) {
|
|
482
|
+
current += char;
|
|
483
|
+
if (quote === '"' && escaped) {
|
|
484
|
+
escaped = false;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (quote === '"' && char === '\\') {
|
|
488
|
+
escaped = true;
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (char === quote)
|
|
492
|
+
quote = null;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (char === '"' || char === "'") {
|
|
496
|
+
quote = char;
|
|
497
|
+
current += char;
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (char === '.') {
|
|
501
|
+
const decoded = decodeTomlKey(current);
|
|
502
|
+
if (decoded === null)
|
|
503
|
+
return null;
|
|
504
|
+
segments.push(decoded);
|
|
505
|
+
current = '';
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
current += char;
|
|
509
|
+
}
|
|
510
|
+
if (quote)
|
|
511
|
+
return null;
|
|
512
|
+
const decoded = decodeTomlKey(current);
|
|
513
|
+
if (decoded === null)
|
|
514
|
+
return null;
|
|
515
|
+
segments.push(decoded);
|
|
516
|
+
return { path: segments, array: Boolean(arrayMatch) };
|
|
517
|
+
}
|
|
518
|
+
function decodeTomlKey(value) {
|
|
519
|
+
const token = value.trim();
|
|
520
|
+
if (!token)
|
|
521
|
+
return null;
|
|
522
|
+
if (token.startsWith('"') && token.endsWith('"')) {
|
|
523
|
+
try {
|
|
524
|
+
return JSON.parse(token);
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (token.startsWith("'") && token.endsWith("'"))
|
|
531
|
+
return token.slice(1, -1);
|
|
532
|
+
return /^[A-Za-z0-9_-]+$/.test(token) ? token : null;
|
|
533
|
+
}
|
|
534
|
+
function isServerRoot(parts, name) {
|
|
535
|
+
return parts.length === 2 && parts[0] === 'mcp_servers' && parts[1] === name;
|
|
536
|
+
}
|
|
537
|
+
function isServerPath(parts, name) {
|
|
538
|
+
return parts.length >= 2 && parts[0] === 'mcp_servers' && parts[1] === name;
|
|
539
|
+
}
|
|
540
|
+
function normalizeServerName(value) {
|
|
541
|
+
const text = normalizeSingleLine(value, 64);
|
|
542
|
+
return text && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/.test(text) ? text : null;
|
|
543
|
+
}
|
|
544
|
+
function normalizeEnvKey(value, optional) {
|
|
545
|
+
const text = normalizeSingleLine(value, 128);
|
|
546
|
+
if (!text)
|
|
547
|
+
return optional ? null : null;
|
|
548
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(text) ? text : null;
|
|
549
|
+
}
|
|
550
|
+
function normalizeSingleLine(value, maxLength, allowEmpty = false) {
|
|
551
|
+
if (typeof value !== 'string')
|
|
552
|
+
return null;
|
|
553
|
+
const text = value.trim();
|
|
554
|
+
if ((!allowEmpty && !text) || text.length > maxLength || /[\r\n\0]/.test(text))
|
|
555
|
+
return null;
|
|
556
|
+
return text;
|
|
557
|
+
}
|
|
558
|
+
function normalizeHttpUrl(value) {
|
|
559
|
+
const text = normalizeSingleLine(value, 4096);
|
|
560
|
+
if (!text)
|
|
561
|
+
return null;
|
|
562
|
+
try {
|
|
563
|
+
const url = new URL(text);
|
|
564
|
+
return url.protocol === 'http:' || url.protocol === 'https:' ? url.toString() : null;
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function optionalPositiveNumber(value, blocker, blockers) {
|
|
571
|
+
if (value === undefined || value === null || value === '')
|
|
572
|
+
return null;
|
|
573
|
+
const number = Number(value);
|
|
574
|
+
if (!Number.isFinite(number) || number <= 0 || number > 3600) {
|
|
575
|
+
blockers.push(blocker);
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
return number;
|
|
579
|
+
}
|
|
580
|
+
function redactUrl(value) {
|
|
581
|
+
try {
|
|
582
|
+
const url = new URL(value);
|
|
583
|
+
if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.origin === 'null')
|
|
584
|
+
return '[configured URL]';
|
|
585
|
+
return url.pathname && url.pathname !== '/' ? `${url.origin}/…` : url.origin;
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
return '[configured URL]';
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function displayCommand(value) {
|
|
592
|
+
const text = String(value || '').trim();
|
|
593
|
+
const base = path.basename(text);
|
|
594
|
+
if (!base || base.length > 128 || !/^[A-Za-z0-9._+-]+$/.test(base))
|
|
595
|
+
return '[configured command]';
|
|
596
|
+
return base;
|
|
597
|
+
}
|
|
598
|
+
function tomlString(value) {
|
|
599
|
+
return JSON.stringify(String(value));
|
|
600
|
+
}
|
|
601
|
+
function tomlKey(value) {
|
|
602
|
+
return tomlString(value);
|
|
603
|
+
}
|
|
604
|
+
function finiteNumberOrNull(value) {
|
|
605
|
+
const number = Number(value);
|
|
606
|
+
return Number.isFinite(number) ? number : null;
|
|
607
|
+
}
|
|
608
|
+
function parseConfig(text) {
|
|
609
|
+
try {
|
|
610
|
+
return { ok: true, value: parseCodexConfigToml(text) };
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
return { ok: false, error: safeTomlParseError(error) };
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function safeTomlParseError(error) {
|
|
617
|
+
const message = messageOf(error);
|
|
618
|
+
const lineColumn = message.match(/\bline\s+(\d+)(?:\s*,?\s*column\s+(\d+))?/i);
|
|
619
|
+
if (lineColumn?.[1])
|
|
620
|
+
return `invalid_toml_at_line_${lineColumn[1]}${lineColumn[2] ? `_column_${lineColumn[2]}` : ''}`;
|
|
621
|
+
const offset = message.match(/\bat\s+(\d+):(\d+)\b/);
|
|
622
|
+
if (offset?.[1] && offset[2])
|
|
623
|
+
return `invalid_toml_at_line_${offset[1]}_column_${offset[2]}`;
|
|
624
|
+
return 'invalid_toml';
|
|
625
|
+
}
|
|
626
|
+
async function readConfigText(configPath) {
|
|
627
|
+
let stat;
|
|
628
|
+
try {
|
|
629
|
+
stat = await fs.lstat(configPath);
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
const code = errorCode(error);
|
|
633
|
+
if (code === 'ENOENT')
|
|
634
|
+
return { ok: true, text: '', exists: false };
|
|
635
|
+
return { ok: false, code: code || 'UNKNOWN', error: messageOf(error) };
|
|
636
|
+
}
|
|
637
|
+
if (stat.isSymbolicLink()) {
|
|
638
|
+
return { ok: false, code: 'SYMLINK', error: `Refusing to replace symbolic-link Codex config: ${configPath}` };
|
|
639
|
+
}
|
|
640
|
+
if (!stat.isFile()) {
|
|
641
|
+
return { ok: false, code: 'NON_REGULAR', error: `Codex config is not a regular file: ${configPath}` };
|
|
642
|
+
}
|
|
643
|
+
try {
|
|
644
|
+
return { ok: true, text: await readText(configPath), exists: true };
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
return { ok: false, code: errorCode(error) || 'UNKNOWN', error: messageOf(error) };
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function mutationApplied(action, name, enabled, servers) {
|
|
651
|
+
const server = servers.find((candidate) => candidate.name === name);
|
|
652
|
+
if (action === 'remove')
|
|
653
|
+
return !server;
|
|
654
|
+
if (!server)
|
|
655
|
+
return false;
|
|
656
|
+
if (action === 'enable' || action === 'disable')
|
|
657
|
+
return server.enabled === enabled;
|
|
658
|
+
return action === 'add';
|
|
659
|
+
}
|
|
660
|
+
function configReadBlocker(code) {
|
|
661
|
+
if (code === 'SYMLINK')
|
|
662
|
+
return 'codex_mcp_config_symlink_unsupported';
|
|
663
|
+
if (code === 'NON_REGULAR')
|
|
664
|
+
return 'codex_mcp_config_not_regular_file';
|
|
665
|
+
return 'codex_mcp_config_read_failed';
|
|
666
|
+
}
|
|
667
|
+
function resolveConfigPath(opts) {
|
|
668
|
+
return path.resolve(opts.configPath || codexMcpConfigPath(opts.home));
|
|
669
|
+
}
|
|
670
|
+
function isRecord(value) {
|
|
671
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
672
|
+
}
|
|
673
|
+
function messageOf(error) {
|
|
674
|
+
return error instanceof Error ? error.message : String(error);
|
|
675
|
+
}
|
|
676
|
+
function errorCode(error) {
|
|
677
|
+
return error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
|
|
678
|
+
}
|
|
679
|
+
//# sourceMappingURL=mcp-manager.js.map
|