@yugabytedb/meko-mcp 1.2.0 → 2.1.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 +59 -25
- package/assets/hooks-handlers/before-submit-prompt.sh +2 -2
- package/assets/hooks-handlers/codex-session-end.sh +78 -0
- package/assets/hooks-handlers/lib/capture.js +366 -152
- package/assets/hooks-handlers/lib/checkpoint-timer.js +67 -13
- package/assets/hooks-handlers/session-end.sh +10 -0
- package/assets/hooks-handlers/session-start.sh +14 -3
- package/assets/hooks-handlers/test_checkpoint_timer.sh +162 -0
- package/assets/scripts/meko-statusline.sh +119 -0
- package/assets/scripts/package-desktop-skill.sh +64 -30
- package/assets/skills/meko-mcp-tools/SKILL.md +84 -86
- package/assets/skills/meko-mcp-tools/references/_sections.md +2 -4
- package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +29 -25
- package/assets/skills/meko-mcp-tools/references/tools-conversation-dedup.md +5 -7
- package/assets/skills/meko-mcp-tools/references/tools-conversation-reasoning.md +50 -0
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +126 -184
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +31 -37
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +10 -26
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +26 -30
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +60 -66
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +28 -93
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +5 -99
- package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +9 -8
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +138 -138
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +46 -16
- package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +2 -2
- package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +28 -24
- package/assets/skills/meko-mcp-tools-desktop/references/tools-conversation-reasoning.md +50 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +120 -174
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +16 -22
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +28 -34
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +60 -66
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +28 -93
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +4 -98
- package/assets/skills/meko-select-datapack-desktop/SKILL.md +208 -0
- package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +121 -0
- package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +41 -0
- package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +66 -0
- package/bin/create-meko-setup.mjs +74 -6
- package/lib/clients/claude-code.mjs +137 -6
- package/lib/clients/claude-desktop.mjs +51 -12
- package/lib/clients/codex.mjs +329 -13
- package/lib/clients/common.mjs +43 -1
- package/lib/clients/cursor.mjs +82 -21
- package/lib/config.mjs +26 -0
- package/lib/detect.mjs +7 -2
- package/lib/installer.mjs +24 -0
- package/lib/mcp-transport.mjs +5 -0
- package/lib/prompt.mjs +1 -1
- package/lib/url-selection.mjs +55 -0
- package/lib/validate.mjs +0 -1
- package/package.json +1 -1
- package/scripts/bundle-assets.mjs +8 -0
- package/assets/skills/meko-mcp-tools/references/tools-db-query-patterns.md +0 -38
- package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +0 -75
- package/assets/skills/meko-mcp-tools/references/tools-write-guardrails.md +0 -30
- package/assets/skills/meko-mcp-tools-desktop/references/tools-scope-permissions.md +0 -75
package/lib/clients/codex.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import { askYesNo } from "../prompt.mjs";
|
|
|
28
28
|
import {
|
|
29
29
|
getCodexAgentsMdPath,
|
|
30
30
|
getCodexConfigPath,
|
|
31
|
+
getCodexHooksInstallRoot,
|
|
31
32
|
getCodexHooksPath,
|
|
32
33
|
getCodexMekoEnvFilePath,
|
|
33
34
|
getCodexSkillsRoot,
|
|
@@ -36,6 +37,8 @@ import {
|
|
|
36
37
|
} from "../config.mjs";
|
|
37
38
|
import {
|
|
38
39
|
getInstallerVersion,
|
|
40
|
+
installStableTree,
|
|
41
|
+
isPackagedInstall,
|
|
39
42
|
isDanglingSymlink,
|
|
40
43
|
markerIsStale,
|
|
41
44
|
readInstallerMarker,
|
|
@@ -50,6 +53,16 @@ import {
|
|
|
50
53
|
const CODEX_INSTALL_URL = "https://github.com/openai/codex";
|
|
51
54
|
const CODEX_APP_INSTALL_URL = "https://openai.com/index/codex";
|
|
52
55
|
|
|
56
|
+
// Cloud Meko (mcp.mekodata.ai) sits behind a WAF that returns an HTML 403 for
|
|
57
|
+
// any request with no User-Agent. Codex's native MCP client (codex_rmcp_client)
|
|
58
|
+
// sends none by default, so its `initialize` handshake 403s and the server
|
|
59
|
+
// fails to start. `codex mcp add` exposes no header flag, so we set a per-server
|
|
60
|
+
// `http_headers` entry in config.toml. Scoped to [mcp_servers.<name>] only — it
|
|
61
|
+
// affects requests to the Meko MCP server and nothing else. Local installs skip
|
|
62
|
+
// this (no WAF in front of localhost). See GH #174 and the capture.js UA fix.
|
|
63
|
+
const CODEX_MCP_USER_AGENT =
|
|
64
|
+
"codex-mcp/1.0 (+https://github.com/yugabyte/meko-mcp-server)";
|
|
65
|
+
|
|
53
66
|
// AGENTS.md sentinel markers. Idempotent upsert/strip relies on these being
|
|
54
67
|
// stable across versions — never rename without a migration story.
|
|
55
68
|
const AGENTS_MD_START = "<!-- meko:start -->";
|
|
@@ -64,14 +77,19 @@ const RC_FENCE_END = "# <<< meko mcp <<<";
|
|
|
64
77
|
const SKILLS = [{ name: "meko-mcp-tools" }];
|
|
65
78
|
// Codex hook timeouts are in SECONDS (Duration::from_secs in
|
|
66
79
|
// codex-rs/hooks/src/engine/command_runner.rs), not milliseconds like Claude
|
|
67
|
-
// Code / Cursor.
|
|
68
|
-
//
|
|
80
|
+
// Code / Cursor. Codex hard-caps SessionEnd at three seconds, so its dedicated
|
|
81
|
+
// handler only spools input and launches the actual capture in the background.
|
|
69
82
|
const MEKO_HOOKS = [
|
|
70
83
|
{ event: "SessionStart", script: "session-start.sh", timeout: 15 },
|
|
71
84
|
{ event: "PreCompact", script: "pre-compact.sh" },
|
|
72
|
-
{ event: "SessionEnd", script: "session-end.sh", timeout:
|
|
85
|
+
{ event: "SessionEnd", script: "codex-session-end.sh", timeout: 3 },
|
|
86
|
+
];
|
|
87
|
+
// Include the former synchronous Codex SessionEnd handler so reinstall prunes
|
|
88
|
+
// it instead of leaving both old and new handlers active.
|
|
89
|
+
const MEKO_HOOK_SCRIPTS = [
|
|
90
|
+
...MEKO_HOOKS.map((hook) => hook.script),
|
|
91
|
+
"session-end.sh",
|
|
73
92
|
];
|
|
74
|
-
const MEKO_HOOK_SCRIPTS = MEKO_HOOKS.map((hook) => hook.script);
|
|
75
93
|
|
|
76
94
|
/**
|
|
77
95
|
* @param {{
|
|
@@ -80,6 +98,7 @@ const MEKO_HOOK_SCRIPTS = MEKO_HOOKS.map((hook) => hook.script);
|
|
|
80
98
|
* getHooksPath?: () => string,
|
|
81
99
|
* getSkillsRoot?: () => string,
|
|
82
100
|
* resolveHooksRoot?: () => string,
|
|
101
|
+
* getHooksInstallRoot?: () => string,
|
|
83
102
|
* runCommand?: (cmd: string) => string,
|
|
84
103
|
* probeCodex?: () => {found: boolean, version: string, path: string},
|
|
85
104
|
* probeCodexApp?: () => {found: boolean, path: string, platform: string, version: string} | null,
|
|
@@ -95,7 +114,10 @@ export function createCodexAdapter(options = {}) {
|
|
|
95
114
|
const resolveMekoEnvFilePath =
|
|
96
115
|
options.getMekoEnvFilePath ?? getCodexMekoEnvFilePath;
|
|
97
116
|
const resolveHooksRootPath = options.resolveHooksRoot ?? resolveHooksRoot;
|
|
117
|
+
const resolveHooksInstallRoot =
|
|
118
|
+
options.getHooksInstallRoot ?? getCodexHooksInstallRoot;
|
|
98
119
|
const resolveSkillsRoot = options.getSkillsRoot ?? getCodexSkillsRoot;
|
|
120
|
+
const packagedInstallDetector = options.isPackagedInstall ?? isPackagedInstall;
|
|
99
121
|
const runCommand = options.runCommand ?? defaultRunCommand;
|
|
100
122
|
const probeCodex = options.probeCodex ?? defaultProbeCodex;
|
|
101
123
|
const probeCodexApp = options.probeCodexApp ?? defaultProbeCodexApp;
|
|
@@ -107,6 +129,16 @@ export function createCodexAdapter(options = {}) {
|
|
|
107
129
|
|
|
108
130
|
let scopeWarned = false;
|
|
109
131
|
|
|
132
|
+
// A reinstall's remove→register sequence deletes the whole
|
|
133
|
+
// [mcp_servers.<name>] block (via `codex mcp remove` or removeCodexConfigBlock),
|
|
134
|
+
// including any http_headers in either inline or table form, before
|
|
135
|
+
// registerServer rewrites it — which would silently drop a user's custom
|
|
136
|
+
// headers. removeRegistration stashes the pre-existing headers (as a key→value
|
|
137
|
+
// map, form-agnostic) here so the later ensureCodexHttpHeaderUserAgent step
|
|
138
|
+
// can merge them back while guaranteeing a User-Agent. null = nothing to
|
|
139
|
+
// preserve.
|
|
140
|
+
let preservedHttpHeaders = null;
|
|
141
|
+
|
|
110
142
|
/** Returns the resolved config.toml path; warns once on project scope. */
|
|
111
143
|
function settingsPathFor(scope, log) {
|
|
112
144
|
if (scope === "project" && !scopeWarned) {
|
|
@@ -169,10 +201,11 @@ export function createCodexAdapter(options = {}) {
|
|
|
169
201
|
surfaces.app = app?.found ? app : null;
|
|
170
202
|
}
|
|
171
203
|
|
|
204
|
+
let cliFound = false;
|
|
172
205
|
if (surfaces.cli) {
|
|
173
206
|
try {
|
|
174
207
|
runCommand(`codex mcp get ${shellQuote(name)}`);
|
|
175
|
-
|
|
208
|
+
cliFound = true;
|
|
176
209
|
} catch {
|
|
177
210
|
// fall through to TOML grep — `codex mcp get` exits non-zero when
|
|
178
211
|
// the entry isn't present, but might also fail for unrelated
|
|
@@ -182,13 +215,27 @@ export function createCodexAdapter(options = {}) {
|
|
|
182
215
|
}
|
|
183
216
|
const path = settingsPathFor(scope, null);
|
|
184
217
|
const raw = safeReadFile(path);
|
|
185
|
-
if (raw
|
|
186
|
-
|
|
218
|
+
if (raw !== null) {
|
|
219
|
+
const parsed = parseCodexConfigForServer(raw, name);
|
|
220
|
+
if (parsed.found) {
|
|
221
|
+
return {
|
|
222
|
+
found: true,
|
|
223
|
+
status: "Configured",
|
|
224
|
+
...(parsed.url ? { url: parsed.url } : {}),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return cliFound
|
|
187
229
|
? { found: true, status: "Configured" }
|
|
188
230
|
: { found: false, status: "" };
|
|
189
231
|
},
|
|
190
232
|
removeRegistration(name, { scope, log } = {}) {
|
|
191
233
|
const path = settingsPathFor(scope, log);
|
|
234
|
+
// Capture any existing http_headers (inline OR table form) BEFORE we
|
|
235
|
+
// delete the block, so a reinstall can restore the user's custom values
|
|
236
|
+
// (see preservedHttpHeaders). Done on both the CLI and TOML paths since
|
|
237
|
+
// `codex mcp remove` also drops the whole block and any subtable.
|
|
238
|
+
preservedHttpHeaders = readCodexHttpHeaders(safeReadFile(path), name);
|
|
192
239
|
if (surfaces.cli) {
|
|
193
240
|
try {
|
|
194
241
|
runCommand(`codex mcp remove ${shellQuote(name)}`);
|
|
@@ -209,6 +256,8 @@ export function createCodexAdapter(options = {}) {
|
|
|
209
256
|
const useApiKey = apiKey ? "MEKO_API_KEY" : null;
|
|
210
257
|
const cliCmd = buildCliRegisterCommand(name, url, useApiKey);
|
|
211
258
|
|
|
259
|
+
const cloud = !isLocalUrl(url);
|
|
260
|
+
|
|
212
261
|
if (dryRun) {
|
|
213
262
|
if (surfaces.cli) {
|
|
214
263
|
log.dim?.(` Would run: ${cliCmd}`);
|
|
@@ -220,6 +269,11 @@ export function createCodexAdapter(options = {}) {
|
|
|
220
269
|
" Codex resolves the bearer token from $MEKO_API_KEY at runtime; the key is not written to config.toml.",
|
|
221
270
|
);
|
|
222
271
|
}
|
|
272
|
+
if (cloud) {
|
|
273
|
+
log.dim?.(
|
|
274
|
+
` Would ensure [mcp_servers.${name}] has an http_headers User-Agent (Cloud WAF needs one; backs up ${path} first, preserves any existing header).`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
223
277
|
return;
|
|
224
278
|
}
|
|
225
279
|
|
|
@@ -257,6 +311,42 @@ export function createCodexAdapter(options = {}) {
|
|
|
257
311
|
if (verbose) log.dim?.(` Wrote [mcp_servers.${name}] to ${path}`);
|
|
258
312
|
}
|
|
259
313
|
|
|
314
|
+
// Cloud Meko's WAF 403s Codex's native MCP client when it sends no
|
|
315
|
+
// User-Agent on the initialize handshake. `codex mcp add` has no header
|
|
316
|
+
// flag, so patch the written block with an http_headers User-Agent.
|
|
317
|
+
// Reads the file first, preserves any existing http_headers, and backs
|
|
318
|
+
// up config.toml before writing. Local installs have no WAF, so skip.
|
|
319
|
+
if (cloud) {
|
|
320
|
+
// Merge back any custom http_headers captured by removeRegistration
|
|
321
|
+
// before the reinstall wiped the block, while guaranteeing a User-Agent.
|
|
322
|
+
const preserved = preservedHttpHeaders;
|
|
323
|
+
preservedHttpHeaders = null; // consume — don't leak into a later call
|
|
324
|
+
const outcome = ensureCodexHttpHeaderUserAgent(path, name, {
|
|
325
|
+
preserved,
|
|
326
|
+
});
|
|
327
|
+
if (outcome === "restored") {
|
|
328
|
+
log.dim?.(
|
|
329
|
+
` Restored your existing http_headers on [mcp_servers.${name}] (with a User-Agent) after re-registration.`,
|
|
330
|
+
);
|
|
331
|
+
} else if (outcome === "merged") {
|
|
332
|
+
log.dim?.(
|
|
333
|
+
` Added a User-Agent to your existing http_headers on [mcp_servers.${name}] so Codex's MCP client clears the Cloud WAF (backup written to ${path}.bak).`,
|
|
334
|
+
);
|
|
335
|
+
} else if (outcome === "added") {
|
|
336
|
+
log.dim?.(
|
|
337
|
+
` Set http_headers User-Agent on [mcp_servers.${name}] so Codex's MCP client clears the Cloud WAF (backup written to ${path}.bak).`,
|
|
338
|
+
);
|
|
339
|
+
} else if (outcome === "present" && verbose) {
|
|
340
|
+
log.dim?.(
|
|
341
|
+
` [mcp_servers.${name}] already has an http_headers User-Agent — left as-is.`,
|
|
342
|
+
);
|
|
343
|
+
} else if (outcome === "absent") {
|
|
344
|
+
log.warn?.(
|
|
345
|
+
` Could not find [mcp_servers.${name}] in ${path} to set the Cloud WAF User-Agent header; if Codex reports an HTTP 403 on startup, add http_headers = { "User-Agent" = "${CODEX_MCP_USER_AGENT}" } manually.`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
260
350
|
// Cloud auth UX: env-var-only model. The key is never written to
|
|
261
351
|
// config.toml; the user's runtime env supplies it. Print actionable
|
|
262
352
|
// instructions and (when interactive) offer to wire things up.
|
|
@@ -300,9 +390,11 @@ export function createCodexAdapter(options = {}) {
|
|
|
300
390
|
},
|
|
301
391
|
setHookEnvironment({ url, apiKey, log, dryRun, verbose }) {
|
|
302
392
|
const hooksPath = resolveHooksPath();
|
|
303
|
-
const
|
|
393
|
+
const assetHooksRoot = resolveHooksRootPath();
|
|
394
|
+
const installedHooksRoot = resolveHooksInstallRoot();
|
|
304
395
|
const envFilePath = resolveMekoEnvFilePath();
|
|
305
396
|
if (dryRun) {
|
|
397
|
+
log?.dim?.(` Would copy Codex hook scripts to ${installedHooksRoot}`);
|
|
306
398
|
log?.dim?.(` Would write env file at ${envFilePath} (mode 0600)`);
|
|
307
399
|
log?.dim?.(` Would configure Codex hooks at ${hooksPath}`);
|
|
308
400
|
if (apiKey) {
|
|
@@ -313,8 +405,16 @@ export function createCodexAdapter(options = {}) {
|
|
|
313
405
|
return;
|
|
314
406
|
}
|
|
315
407
|
|
|
408
|
+
// Copy the hook scripts to a home-dir-stable location before wiring
|
|
409
|
+
// hooks.json at that path. Referencing the shipped asset root directly
|
|
410
|
+
// would pin an ephemeral npx cache dir that npx later GCs, leaving the
|
|
411
|
+
// hook a dangling `bash '<missing>'` that exits 127. See
|
|
412
|
+
// getCodexHooksInstallRoot for the full rationale.
|
|
413
|
+
installStableTree(assetHooksRoot, installedHooksRoot);
|
|
316
414
|
writeCodexMekoEnvFile(envFilePath, { url, apiKey });
|
|
317
|
-
const changed = upsertCodexMekoHooks(hooksPath,
|
|
415
|
+
const changed = upsertCodexMekoHooks(hooksPath, installedHooksRoot, {
|
|
416
|
+
envFilePath,
|
|
417
|
+
});
|
|
318
418
|
if (verbose || changed) {
|
|
319
419
|
log?.dim?.(` Updated Codex hooks at ${hooksPath}`);
|
|
320
420
|
}
|
|
@@ -374,7 +474,9 @@ export function createCodexAdapter(options = {}) {
|
|
|
374
474
|
removePath(destination);
|
|
375
475
|
}
|
|
376
476
|
|
|
377
|
-
const strategy = symlinkOrCopy(source, destination
|
|
477
|
+
const strategy = symlinkOrCopy(source, destination, {
|
|
478
|
+
preferCopy: packagedInstallDetector(),
|
|
479
|
+
});
|
|
378
480
|
if (strategy === "copy") writeInstallerMarker(destination);
|
|
379
481
|
}
|
|
380
482
|
|
|
@@ -434,7 +536,10 @@ export function createCodexAdapter(options = {}) {
|
|
|
434
536
|
try {
|
|
435
537
|
const hooksPath = resolveHooksPath();
|
|
436
538
|
const envFilePath = resolveMekoEnvFilePath();
|
|
437
|
-
const changed = removeCodexMekoHooks(hooksPath, {
|
|
539
|
+
const changed = removeCodexMekoHooks(hooksPath, {
|
|
540
|
+
envFilePath,
|
|
541
|
+
hooksInstallRoot: resolveHooksInstallRoot(),
|
|
542
|
+
});
|
|
438
543
|
if (changed) {
|
|
439
544
|
results.push({
|
|
440
545
|
key: "hooks",
|
|
@@ -550,7 +655,7 @@ function upsertCodexMekoHooks(hooksPath, hooksRoot, { envFilePath } = {}) {
|
|
|
550
655
|
function buildCodexHookCommand(scriptPath, { envFilePath } = {}) {
|
|
551
656
|
const parts = [];
|
|
552
657
|
if (envFilePath) parts.push(`. ${shellQuote(envFilePath)}`);
|
|
553
|
-
parts.push(`bash ${shellQuote(scriptPath)}`);
|
|
658
|
+
parts.push(`MEKO_HOOK_CLIENT=codex bash ${shellQuote(scriptPath)}`);
|
|
554
659
|
return parts.join(" && ");
|
|
555
660
|
}
|
|
556
661
|
|
|
@@ -576,7 +681,7 @@ function writeCodexMekoEnvFile(path, { url, apiKey }) {
|
|
|
576
681
|
chmodSync(path, 0o600);
|
|
577
682
|
}
|
|
578
683
|
|
|
579
|
-
function removeCodexMekoHooks(hooksPath, { envFilePath } = {}) {
|
|
684
|
+
function removeCodexMekoHooks(hooksPath, { envFilePath, hooksInstallRoot } = {}) {
|
|
580
685
|
const cfg = readJsonFile(hooksPath);
|
|
581
686
|
let changed = false;
|
|
582
687
|
if (cfg.hooks && typeof cfg.hooks === "object") {
|
|
@@ -610,6 +715,17 @@ function removeCodexMekoHooks(hooksPath, { envFilePath } = {}) {
|
|
|
610
715
|
// best-effort
|
|
611
716
|
}
|
|
612
717
|
}
|
|
718
|
+
|
|
719
|
+
// Remove the copied hook scripts (getCodexHooksInstallRoot). We remove the
|
|
720
|
+
// whole meko-hooks/ wrapper, not just the hooks-handlers leaf, so no empty
|
|
721
|
+
// parent is left behind. best-effort — never let cleanup fail uninstall.
|
|
722
|
+
if (hooksInstallRoot) {
|
|
723
|
+
try {
|
|
724
|
+
rmSync(dirname(hooksInstallRoot), { recursive: true, force: true });
|
|
725
|
+
} catch {
|
|
726
|
+
// best-effort
|
|
727
|
+
}
|
|
728
|
+
}
|
|
613
729
|
return changed;
|
|
614
730
|
}
|
|
615
731
|
|
|
@@ -764,6 +880,200 @@ function buildCliRegisterCommand(name, url, bearerEnv) {
|
|
|
764
880
|
return cmd;
|
|
765
881
|
}
|
|
766
882
|
|
|
883
|
+
/**
|
|
884
|
+
* True for localhost / loopback MCP URLs (no cloud WAF in front). Parses the
|
|
885
|
+
* URL and compares the HOSTNAME exactly — a substring test would wrongly
|
|
886
|
+
* classify `https://localhost.example.com` (a real remote host) as local and
|
|
887
|
+
* skip the WAF User-Agent header. Unparseable input is treated as non-local
|
|
888
|
+
* (cloud), i.e. we err toward setting the header.
|
|
889
|
+
*/
|
|
890
|
+
function isLocalUrl(url) {
|
|
891
|
+
let host;
|
|
892
|
+
try {
|
|
893
|
+
host = new URL(String(url ?? "")).hostname;
|
|
894
|
+
} catch {
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
// `new URL` strips the brackets from IPv6 literals, so `[::1]` → `::1`.
|
|
898
|
+
host = host.replace(/^\[|\]$/g, "").toLowerCase();
|
|
899
|
+
return (
|
|
900
|
+
host === "localhost" ||
|
|
901
|
+
host === "::1" ||
|
|
902
|
+
// Entire 127.0.0.0/8 block is loopback, not just 127.0.0.1.
|
|
903
|
+
/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host)
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const HTTP_HEADER_USER_AGENT_KEY = "User-Agent";
|
|
908
|
+
|
|
909
|
+
// Matches one `KEY = "VALUE"` (or single-quoted) pair. KEY may be
|
|
910
|
+
// double-quoted, single-quoted, or a bare TOML key (letters/digits/_/-).
|
|
911
|
+
// Used for both the inline `{ ... }` body and the table-form body.
|
|
912
|
+
const TOML_KV_PAIR =
|
|
913
|
+
/(?:"([^"]*)"|'([^']*)'|([A-Za-z0-9_-]+))\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
914
|
+
|
|
915
|
+
/** True if `map` has a `User-Agent` key, case-insensitively (HTTP names are). */
|
|
916
|
+
function hasUserAgentKey(map) {
|
|
917
|
+
return Object.keys(map).some((k) => k.toLowerCase() === "user-agent");
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** Parse `KEY = "VALUE"` pairs from a chunk of TOML into an ordered object. */
|
|
921
|
+
function parseTomlKvPairs(text) {
|
|
922
|
+
const out = {};
|
|
923
|
+
TOML_KV_PAIR.lastIndex = 0;
|
|
924
|
+
let m;
|
|
925
|
+
while ((m = TOML_KV_PAIR.exec(text)) !== null) {
|
|
926
|
+
const key = m[1] ?? m[2] ?? m[3];
|
|
927
|
+
const value = m[4] ?? m[5] ?? "";
|
|
928
|
+
if (key !== undefined) out[key] = value;
|
|
929
|
+
}
|
|
930
|
+
return out;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Locate a table-form `[mcp_servers.<name>.http_headers]` section, if present.
|
|
935
|
+
* `parseCodexConfigForServer`'s block ends at the first subtable, so this form
|
|
936
|
+
* lives OUTSIDE that block and needs its own lookup.
|
|
937
|
+
*
|
|
938
|
+
* @returns {{start: number, end: number, body: string}|null}
|
|
939
|
+
*/
|
|
940
|
+
function findHttpHeadersSubtable(raw, name) {
|
|
941
|
+
const header = `[mcp_servers.${name}.http_headers]`;
|
|
942
|
+
const idx = findHeader(raw, header);
|
|
943
|
+
if (idx === -1) return null;
|
|
944
|
+
const end = findNextHeader(raw, idx + header.length);
|
|
945
|
+
return { start: idx, end, body: raw.slice(idx + header.length, end) };
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Read the effective `http_headers` for a server from EITHER form (inline
|
|
950
|
+
* `http_headers = { ... }` inside the block, or a `[….http_headers]` subtable),
|
|
951
|
+
* returning an ordered key→value map, or null when neither is present. Used to
|
|
952
|
+
* snapshot a user's headers before a reinstall wipes them.
|
|
953
|
+
*
|
|
954
|
+
* @param {string|null} raw
|
|
955
|
+
* @param {string} name
|
|
956
|
+
* @returns {Record<string,string>|null}
|
|
957
|
+
*/
|
|
958
|
+
function readCodexHttpHeaders(raw, name) {
|
|
959
|
+
if (raw === null || raw === undefined) return null;
|
|
960
|
+
const existing = parseCodexConfigForServer(raw, name);
|
|
961
|
+
if (!existing.found) return null;
|
|
962
|
+
|
|
963
|
+
const block = raw.slice(existing.start, existing.end);
|
|
964
|
+
const inline = block.match(/^[ \t]*http_headers[ \t]*=[ \t]*\{([^}]*)\}/m);
|
|
965
|
+
if (inline) return parseTomlKvPairs(inline[1]);
|
|
966
|
+
|
|
967
|
+
const subtable = findHttpHeadersSubtable(raw, name);
|
|
968
|
+
if (subtable) return parseTomlKvPairs(subtable.body);
|
|
969
|
+
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/** Render a headers map as a single canonical inline `http_headers` line. */
|
|
974
|
+
function renderInlineHttpHeaders(map) {
|
|
975
|
+
const parts = Object.entries(map).map(
|
|
976
|
+
([k, v]) => `"${escapeTomlString(k)}" = "${escapeTomlString(v)}"`,
|
|
977
|
+
);
|
|
978
|
+
return `http_headers = { ${parts.join(", ")} }`;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Ensure the `[mcp_servers.<name>]` block carries an `http_headers` User-Agent
|
|
983
|
+
* so Codex's native MCP client clears the Cloud WAF (see CODEX_MCP_USER_AGENT).
|
|
984
|
+
*
|
|
985
|
+
* This MERGES rather than all-or-nothing: it reads the effective headers from
|
|
986
|
+
* whichever form is present — inline `http_headers = { ... }` or a table-form
|
|
987
|
+
* `[mcp_servers.<name>.http_headers]` — plus any `preserved` map captured
|
|
988
|
+
* before a reinstall wiped the block, then guarantees a `User-Agent` entry
|
|
989
|
+
* exists while keeping every other header the user set. The result is written
|
|
990
|
+
* as a single canonical inline line, and any table-form section is removed so
|
|
991
|
+
* we never leave two conflicting `http_headers` definitions (invalid TOML).
|
|
992
|
+
*
|
|
993
|
+
* Contract — check, back up, then change:
|
|
994
|
+
* 1. Read config.toml (returns "absent" if the file or server block is gone).
|
|
995
|
+
* 2. Merge preserved + existing headers; if a User-Agent is already present
|
|
996
|
+
* AND no reshaping is needed (no table form to collapse), report "present"
|
|
997
|
+
* and write nothing.
|
|
998
|
+
* 3. Otherwise write the merged inline header via writeWithBackup (`.bak`).
|
|
999
|
+
*
|
|
1000
|
+
* @param {string} configPath
|
|
1001
|
+
* @param {string} name
|
|
1002
|
+
* @param {{userAgent?: string, preserved?: Record<string,string>|null}} [opts]
|
|
1003
|
+
* @returns {"added"|"restored"|"merged"|"present"|"absent"} what happened
|
|
1004
|
+
*/
|
|
1005
|
+
function ensureCodexHttpHeaderUserAgent(
|
|
1006
|
+
configPath,
|
|
1007
|
+
name,
|
|
1008
|
+
{ userAgent, preserved = null } = {},
|
|
1009
|
+
) {
|
|
1010
|
+
const ua = userAgent ?? CODEX_MCP_USER_AGENT;
|
|
1011
|
+
const raw = safeReadFile(configPath);
|
|
1012
|
+
if (raw === null) return "absent";
|
|
1013
|
+
const existing = parseCodexConfigForServer(raw, name);
|
|
1014
|
+
if (!existing.found) return "absent";
|
|
1015
|
+
|
|
1016
|
+
const block = raw.slice(existing.start, existing.end);
|
|
1017
|
+
const inlineMatch = block.match(
|
|
1018
|
+
/^[ \t]*http_headers[ \t]*=[ \t]*\{([^}]*)\}[ \t]*$/m,
|
|
1019
|
+
);
|
|
1020
|
+
const subtable = findHttpHeadersSubtable(raw, name);
|
|
1021
|
+
|
|
1022
|
+
// Merge order: existing on-disk headers first (inline or table), then any
|
|
1023
|
+
// reinstall-preserved map layered on top. Preserved wins on key collisions
|
|
1024
|
+
// because it reflects the user's most recent intent before the wipe.
|
|
1025
|
+
const onDisk = inlineMatch
|
|
1026
|
+
? parseTomlKvPairs(inlineMatch[1])
|
|
1027
|
+
: subtable
|
|
1028
|
+
? parseTomlKvPairs(subtable.body)
|
|
1029
|
+
: {};
|
|
1030
|
+
const merged = { ...onDisk, ...(preserved ?? {}) };
|
|
1031
|
+
|
|
1032
|
+
const hadUserAgent = hasUserAgentKey(merged);
|
|
1033
|
+
const restoring = Boolean(preserved);
|
|
1034
|
+
|
|
1035
|
+
// Nothing to do only when a User-Agent is already present AND the on-disk
|
|
1036
|
+
// form is a single inline line (nothing to collapse or restore).
|
|
1037
|
+
if (hadUserAgent && inlineMatch && !subtable && !restoring) return "present";
|
|
1038
|
+
|
|
1039
|
+
if (!hadUserAgent) merged[HTTP_HEADER_USER_AGENT_KEY] = ua;
|
|
1040
|
+
|
|
1041
|
+
const line = renderInlineHttpHeaders(merged);
|
|
1042
|
+
|
|
1043
|
+
// Build the new server block: drop any existing inline line, then insert the
|
|
1044
|
+
// canonical inline line right under the table header.
|
|
1045
|
+
let newBlock = block;
|
|
1046
|
+
if (inlineMatch) {
|
|
1047
|
+
newBlock = newBlock.replace(
|
|
1048
|
+
/^[ \t]*http_headers[ \t]*=[ \t]*\{[^}]*\}[ \t]*\n?/m,
|
|
1049
|
+
"",
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
const headerLine = `[mcp_servers.${name}]`;
|
|
1053
|
+
const headerIdx = newBlock.indexOf(headerLine);
|
|
1054
|
+
const insertAt = headerIdx + headerLine.length;
|
|
1055
|
+
newBlock =
|
|
1056
|
+
newBlock.slice(0, insertAt) + `\n${line}` + newBlock.slice(insertAt);
|
|
1057
|
+
|
|
1058
|
+
// Reassemble the file, splicing the block back and excising any table-form
|
|
1059
|
+
// subtable (now folded into the inline line) to avoid a duplicate key.
|
|
1060
|
+
let next = raw.slice(0, existing.start) + newBlock + raw.slice(existing.end);
|
|
1061
|
+
if (subtable) {
|
|
1062
|
+
// Recompute the subtable bounds against the rewritten string — the block
|
|
1063
|
+
// edit shifted offsets. The subtable always sits after our block.
|
|
1064
|
+
const fresh = findHttpHeadersSubtable(next, name);
|
|
1065
|
+
if (fresh) {
|
|
1066
|
+
next = next.slice(0, fresh.start) + next.slice(fresh.end);
|
|
1067
|
+
next = next.replace(/\n{3,}/g, "\n\n");
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
writeWithBackup(configPath, next);
|
|
1072
|
+
if (restoring) return "restored";
|
|
1073
|
+
if (subtable || (inlineMatch && !hadUserAgent)) return "merged";
|
|
1074
|
+
return "added";
|
|
1075
|
+
}
|
|
1076
|
+
|
|
767
1077
|
// ---------------------------------------------------------------------------
|
|
768
1078
|
// TOML helpers (App-only fallback path)
|
|
769
1079
|
// ---------------------------------------------------------------------------
|
|
@@ -1206,4 +1516,10 @@ export const __TEST__ = {
|
|
|
1206
1516
|
AGENTS_MD_END,
|
|
1207
1517
|
defaultDetectShellRc,
|
|
1208
1518
|
buildCliRegisterCommand,
|
|
1519
|
+
ensureCodexHttpHeaderUserAgent,
|
|
1520
|
+
readCodexHttpHeaders,
|
|
1521
|
+
findHttpHeadersSubtable,
|
|
1522
|
+
parseTomlKvPairs,
|
|
1523
|
+
isLocalUrl,
|
|
1524
|
+
CODEX_MCP_USER_AGENT,
|
|
1209
1525
|
};
|
package/lib/clients/common.mjs
CHANGED
|
@@ -9,11 +9,16 @@ import {
|
|
|
9
9
|
unlinkSync,
|
|
10
10
|
rmSync,
|
|
11
11
|
readFileSync,
|
|
12
|
+
renameSync,
|
|
12
13
|
writeFileSync,
|
|
13
14
|
} from "node:fs";
|
|
14
15
|
import { dirname, join, resolve } from "node:path";
|
|
15
16
|
import { fileURLToPath } from "node:url";
|
|
16
17
|
|
|
18
|
+
export const MEKO_MCP_USER_AGENT = "meko-mcp-installer/1.0";
|
|
19
|
+
export const MEKO_PRODUCTION_MCP_URL = "https://mcp.mekodata.ai/mcp";
|
|
20
|
+
export const MEKO_PRODUCTION_CONSOLE_URL = "https://cloud.mekodata.ai";
|
|
21
|
+
|
|
17
22
|
/**
|
|
18
23
|
* @returns {string} Absolute repository root path.
|
|
19
24
|
*/
|
|
@@ -83,6 +88,16 @@ export function resolveHooksRoot() {
|
|
|
83
88
|
return join(getAssetRoot(), "hooks-handlers");
|
|
84
89
|
}
|
|
85
90
|
|
|
91
|
+
/**
|
|
92
|
+
* @returns {string} absolute path to the statusline script source.
|
|
93
|
+
* Lives under skills/scripts/ in source, mirrored under assets/scripts/ in
|
|
94
|
+
* the npm-packed install — both roots are siblings of skills/hooks-handlers/
|
|
95
|
+
* and skills/skills/.
|
|
96
|
+
*/
|
|
97
|
+
export function resolveStatuslineSource() {
|
|
98
|
+
return join(getAssetRoot(), "scripts", "meko-statusline.sh");
|
|
99
|
+
}
|
|
100
|
+
|
|
86
101
|
/**
|
|
87
102
|
* Run a shell command and return stdout.
|
|
88
103
|
* @param {string} cmd
|
|
@@ -192,11 +207,15 @@ function unlinkAllowingMissing(targetPath) {
|
|
|
192
207
|
* @param {string} destination
|
|
193
208
|
* @returns {"symlink"|"copy"}
|
|
194
209
|
*/
|
|
195
|
-
export function symlinkOrCopy(source, destination) {
|
|
210
|
+
export function symlinkOrCopy(source, destination, { preferCopy = false } = {}) {
|
|
196
211
|
mkdirSync(dirname(destination), { recursive: true });
|
|
197
212
|
if (isDanglingSymlink(destination)) {
|
|
198
213
|
unlinkAllowingMissing(destination);
|
|
199
214
|
}
|
|
215
|
+
if (preferCopy) {
|
|
216
|
+
cpSync(source, destination, { recursive: true });
|
|
217
|
+
return "copy";
|
|
218
|
+
}
|
|
200
219
|
try {
|
|
201
220
|
symlinkSync(source, destination, "junction");
|
|
202
221
|
return "symlink";
|
|
@@ -206,6 +225,29 @@ export function symlinkOrCopy(source, destination) {
|
|
|
206
225
|
}
|
|
207
226
|
}
|
|
208
227
|
|
|
228
|
+
/** Atomically refresh a runtime-owned directory at a stable path. */
|
|
229
|
+
export function installStableTree(source, destination) {
|
|
230
|
+
if (resolve(source) === resolve(destination)) return;
|
|
231
|
+
const suffix = `${process.pid}-${Date.now()}`;
|
|
232
|
+
const staging = `${destination}.tmp-${suffix}`;
|
|
233
|
+
const backup = `${destination}.bak-${suffix}`;
|
|
234
|
+
|
|
235
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
236
|
+
try {
|
|
237
|
+
cpSync(source, staging, { recursive: true });
|
|
238
|
+
if (existsSync(destination)) renameSync(destination, backup);
|
|
239
|
+
try {
|
|
240
|
+
renameSync(staging, destination);
|
|
241
|
+
} catch (err) {
|
|
242
|
+
if (existsSync(backup)) renameSync(backup, destination);
|
|
243
|
+
throw err;
|
|
244
|
+
}
|
|
245
|
+
rmSync(backup, { recursive: true, force: true });
|
|
246
|
+
} finally {
|
|
247
|
+
rmSync(staging, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
209
251
|
/**
|
|
210
252
|
* Remove path if it is a symlink or directory/file.
|
|
211
253
|
* @param {string} targetPath
|