@yugabytedb/meko-mcp 1.1.0 → 2.0.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.
Files changed (46) hide show
  1. package/README.md +81 -23
  2. package/assets/hooks-handlers/before-submit-prompt.sh +44 -0
  3. package/assets/hooks-handlers/cursor-session-start.sh +42 -0
  4. package/assets/hooks-handlers/lib/capture.js +404 -88
  5. package/assets/hooks-handlers/lib/checkpoint-timer.js +23 -11
  6. package/assets/hooks-handlers/session-start.sh +6 -2
  7. package/assets/scripts/meko-statusline.sh +119 -0
  8. package/assets/scripts/package-desktop-skill.sh +39 -0
  9. package/assets/skills/meko-mcp-tools/SKILL.md +42 -50
  10. package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
  11. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +75 -150
  12. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +30 -36
  13. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +6 -22
  14. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +9 -8
  15. package/assets/skills/meko-mcp-tools/references/tools-overview.md +35 -42
  16. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +28 -92
  17. package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +5 -9
  18. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +1 -81
  19. package/assets/skills/meko-mcp-tools-desktop/SKILL.md +244 -0
  20. package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +180 -0
  21. package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +24 -0
  22. package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +99 -0
  23. package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +327 -0
  24. package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +63 -0
  25. package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +214 -0
  26. package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +105 -0
  27. package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +70 -0
  28. package/assets/skills/meko-mcp-tools-desktop/references/tools-scope-permissions.md +71 -0
  29. package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +75 -0
  30. package/assets/skills/meko-select-datapack-desktop/SKILL.md +198 -0
  31. package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +121 -0
  32. package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +66 -0
  33. package/bin/create-meko-setup.mjs +33 -4
  34. package/lib/clients/claude-code.mjs +133 -6
  35. package/lib/clients/claude-desktop.mjs +48 -20
  36. package/lib/clients/codex.mjs +538 -5
  37. package/lib/clients/common.mjs +122 -1
  38. package/lib/clients/cursor.mjs +120 -27
  39. package/lib/config.mjs +52 -0
  40. package/lib/installer.mjs +26 -2
  41. package/lib/mcp-transport.mjs +5 -0
  42. package/lib/validate.mjs +0 -1
  43. package/package.json +1 -1
  44. package/scripts/bundle-assets.mjs +16 -0
  45. package/assets/skills/meko-mcp-tools/references/tools-db-query-patterns.md +0 -38
  46. package/assets/skills/meko-mcp-tools/references/tools-write-guardrails.md +0 -30
@@ -5,14 +5,18 @@ import {
5
5
  symlinkSync,
6
6
  cpSync,
7
7
  lstatSync,
8
+ statSync,
8
9
  unlinkSync,
9
10
  rmSync,
10
11
  readFileSync,
12
+ renameSync,
11
13
  writeFileSync,
12
14
  } from "node:fs";
13
15
  import { dirname, join, resolve } from "node:path";
14
16
  import { fileURLToPath } from "node:url";
15
17
 
18
+ export const MEKO_MCP_USER_AGENT = "meko-mcp-installer/1.0";
19
+
16
20
  /**
17
21
  * @returns {string} Absolute repository root path.
18
22
  */
@@ -82,6 +86,16 @@ export function resolveHooksRoot() {
82
86
  return join(getAssetRoot(), "hooks-handlers");
83
87
  }
84
88
 
89
+ /**
90
+ * @returns {string} absolute path to the statusline script source.
91
+ * Lives under skills/scripts/ in source, mirrored under assets/scripts/ in
92
+ * the npm-packed install — both roots are siblings of skills/hooks-handlers/
93
+ * and skills/skills/.
94
+ */
95
+ export function resolveStatuslineSource() {
96
+ return join(getAssetRoot(), "scripts", "meko-statusline.sh");
97
+ }
98
+
85
99
  /**
86
100
  * Run a shell command and return stdout.
87
101
  * @param {string} cmd
@@ -108,14 +122,98 @@ export function shellQuote(value) {
108
122
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
109
123
  }
110
124
 
125
+ // statSync error codes that mean a symlink is broken — its target cannot be
126
+ // resolved to a real inode. We deliberately do NOT include EACCES / EPERM:
127
+ // those mean "target may exist but I can't see it", and silently removing
128
+ // someone else's link based on a permission failure is unsafe.
129
+ const BROKEN_SYMLINK_STAT_CODES = new Set([
130
+ "ENOENT", // target does not exist (the textbook "dangling symlink")
131
+ "ENOTDIR", // a non-directory component appears mid-path (e.g. link -> file/child)
132
+ "ELOOP", // too many redirections (cyclic symlinks)
133
+ ]);
134
+
135
+ /**
136
+ * True when `targetPath` is a symlink whose target cannot be resolved. A
137
+ * regular file/dir or a working symlink returns false; a missing path also
138
+ * returns false. See `BROKEN_SYMLINK_STAT_CODES` for which resolution
139
+ * failures count.
140
+ *
141
+ * Why this matters: `existsSync` returns false for a broken symlink, so a
142
+ * naive `if (existsSync(dest)) {...}` branch falls through and tries to write
143
+ * over the symlink. On macOS, `cpSync` then aborts the entire process via an
144
+ * uncaught libc++ `filesystem_error` ("Operation not supported" inside
145
+ * `std::filesystem::equivalent`) whenever one operand is a broken symlink —
146
+ * not just the ENOENT case but also ENOTDIR (`link -> file/child`) and ELOOP.
147
+ * Detecting any of these lets callers either treat the path as an existing
148
+ * entry or clear it before writing.
149
+ *
150
+ * @param {string} targetPath
151
+ * @returns {boolean}
152
+ */
153
+ export function isDanglingSymlink(targetPath) {
154
+ let stat;
155
+ try {
156
+ stat = lstatSync(targetPath);
157
+ } catch {
158
+ return false;
159
+ }
160
+ if (!stat.isSymbolicLink()) return false;
161
+ try {
162
+ statSync(targetPath);
163
+ return false;
164
+ } catch (err) {
165
+ return BROKEN_SYMLINK_STAT_CODES.has(err.code);
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Unlink that tolerates the narrow race window where another process
171
+ * already removed `targetPath` between our check and our write (ENOENT)
172
+ * but propagates every other failure (EACCES, EPERM, EBUSY, etc.) as a
173
+ * normal JS error.
174
+ *
175
+ * Why not `removePath`: that helper swallows ALL errors, which is fine
176
+ * for cleanup paths but dangerous in front of `cpSync`. If unlink truly
177
+ * fails (e.g. read-only parent dir), the broken symlink is still on
178
+ * disk; cpSync would then dive into the macOS libc++ `filesystem_error`
179
+ * abort path described in `isDanglingSymlink`. Surfacing the unlink
180
+ * error here lets the surrounding installer surface a clean error
181
+ * instead of aborting the process.
182
+ *
183
+ * @param {string} targetPath
184
+ */
185
+ function unlinkAllowingMissing(targetPath) {
186
+ try {
187
+ unlinkSync(targetPath);
188
+ } catch (err) {
189
+ if (err && err.code === "ENOENT") return;
190
+ throw err;
191
+ }
192
+ }
193
+
111
194
  /**
112
195
  * Create symlink when possible; fallback to copy.
196
+ *
197
+ * Pre-clears a broken symlink at `destination` so the copy fallback path
198
+ * doesn't crash. See `isDanglingSymlink` above for the macOS libc++ trap.
199
+ * Uses `unlinkAllowingMissing` rather than `removePath` so an unlink
200
+ * failure (e.g. read-only parent dir) surfaces as a normal JS error
201
+ * instead of silently leaving the broken link in place — otherwise
202
+ * `cpSync` below would still abort the process.
203
+ *
113
204
  * @param {string} source
114
205
  * @param {string} destination
115
206
  * @returns {"symlink"|"copy"}
116
207
  */
117
- export function symlinkOrCopy(source, destination) {
208
+ export function symlinkOrCopy(source, destination, { preferCopy = false } = {}) {
118
209
  mkdirSync(dirname(destination), { recursive: true });
210
+ if (isDanglingSymlink(destination)) {
211
+ unlinkAllowingMissing(destination);
212
+ }
213
+ if (preferCopy) {
214
+ cpSync(source, destination, { recursive: true });
215
+ return "copy";
216
+ }
119
217
  try {
120
218
  symlinkSync(source, destination, "junction");
121
219
  return "symlink";
@@ -125,6 +223,29 @@ export function symlinkOrCopy(source, destination) {
125
223
  }
126
224
  }
127
225
 
226
+ /** Atomically refresh a runtime-owned directory at a stable path. */
227
+ export function installStableTree(source, destination) {
228
+ if (resolve(source) === resolve(destination)) return;
229
+ const suffix = `${process.pid}-${Date.now()}`;
230
+ const staging = `${destination}.tmp-${suffix}`;
231
+ const backup = `${destination}.bak-${suffix}`;
232
+
233
+ mkdirSync(dirname(destination), { recursive: true });
234
+ try {
235
+ cpSync(source, staging, { recursive: true });
236
+ if (existsSync(destination)) renameSync(destination, backup);
237
+ try {
238
+ renameSync(staging, destination);
239
+ } catch (err) {
240
+ if (existsSync(backup)) renameSync(backup, destination);
241
+ throw err;
242
+ }
243
+ rmSync(backup, { recursive: true, force: true });
244
+ } finally {
245
+ rmSync(staging, { recursive: true, force: true });
246
+ }
247
+ }
248
+
128
249
  /**
129
250
  * Remove path if it is a symlink or directory/file.
130
251
  * @param {string} targetPath
@@ -16,7 +16,11 @@ import {
16
16
  writeJsonFile,
17
17
  } from "../config.mjs";
18
18
  import {
19
+ MEKO_MCP_USER_AGENT,
19
20
  getInstallerVersion,
21
+ installStableTree,
22
+ isDanglingSymlink,
23
+ isPackagedInstall,
20
24
  markerIsStale,
21
25
  readInstallerMarker,
22
26
  resolveHooksRoot,
@@ -29,7 +33,7 @@ import {
29
33
  const SKILLS = [{ name: "meko-mcp-tools" }];
30
34
 
31
35
  const HOOKS = [
32
- { event: "sessionStart", script: "session-start.sh", timeout: 15000 },
36
+ { event: "sessionStart", script: "cursor-session-start.sh", timeout: 15000 },
33
37
  { event: "preCompact", script: "pre-compact.sh" },
34
38
  { event: "sessionEnd", script: "session-end.sh", timeout: 30000 },
35
39
  ];
@@ -38,6 +42,9 @@ const HOOKS = [
38
42
  * @returns {object}
39
43
  */
40
44
  export function createCursorAdapter(customPaths = {}) {
45
+ const resolveHooksRootPath = customPaths.resolveHooksRoot ?? resolveHooksRoot;
46
+ const resolveSkillSourcePath = customPaths.resolveSkillSource ?? resolveSkillSource;
47
+ const packagedInstallDetector = customPaths.isPackagedInstall ?? isPackagedInstall;
41
48
  const paths = {
42
49
  getMcpPath: customPaths.getMcpPath ?? getCursorMcpPath,
43
50
  getHooksPath: customPaths.getHooksPath ?? getCursorHooksPath,
@@ -83,6 +90,7 @@ export function createCursorAdapter(customPaths = {}) {
83
90
  if (dryRun) {
84
91
  log.dim(` Would update ${mcpPath}:`);
85
92
  log.dim(` mcpServers.${name}.url = ${url}`);
93
+ log.dim(` mcpServers.${name}.headers.User-Agent = ${MEKO_MCP_USER_AGENT}`);
86
94
  if (apiKey) {
87
95
  log.dim(" mcpServers.<name>.headers.Authorization = <set>");
88
96
  }
@@ -93,30 +101,48 @@ export function createCursorAdapter(customPaths = {}) {
93
101
  if (!cfg.mcpServers || typeof cfg.mcpServers !== "object") {
94
102
  cfg.mcpServers = {};
95
103
  }
96
- const server = { url };
104
+ const server = { url, headers: { "User-Agent": MEKO_MCP_USER_AGENT } };
97
105
  if (apiKey) {
98
- server.headers = { Authorization: `Bearer ${apiKey}` };
106
+ server.headers.Authorization = `Bearer ${apiKey}`;
99
107
  }
100
108
  cfg.mcpServers[name] = server;
101
109
  writeJsonFile(mcpPath, cfg);
102
110
  },
103
111
  setHookEnvironment({ url, apiKey, dryRun, log, scope, projectRoot, cursorHookMode }) {
104
- const mode = cursorHookMode || "hybrid";
112
+ const mode = cursorHookMode || "native";
113
+ const installedHooksRoot = getCursorHooksInstallRoot(paths, scope, projectRoot);
105
114
  if (dryRun) {
106
- if (mode === "native" || mode === "hybrid") {
115
+ log.dim(` Would copy Cursor hook scripts to ${installedHooksRoot}`);
116
+ if (mode === "native") {
107
117
  log.dim(` Would configure Cursor hooks at ${paths.getHooksPath(scope, projectRoot)}`);
108
118
  }
109
- if (mode === "claude-compat" || mode === "hybrid") {
119
+ if (mode === "claude-compat") {
110
120
  log.dim(` Would configure Claude-compatible hooks at ${paths.getClaudeCompatSettingsPath(scope, projectRoot)}`);
111
121
  }
112
122
  return;
113
123
  }
114
124
 
115
- if (mode === "native" || mode === "hybrid") {
116
- installNativeCursorHooks({ scope, projectRoot, url, apiKey, paths });
125
+ installStableTree(resolveHooksRootPath(), installedHooksRoot);
126
+
127
+ if (mode === "native") {
128
+ installNativeCursorHooks({
129
+ scope,
130
+ projectRoot,
131
+ url,
132
+ apiKey,
133
+ paths,
134
+ handlersRoot: installedHooksRoot,
135
+ });
117
136
  }
118
- if (mode === "claude-compat" || mode === "hybrid") {
119
- installClaudeCompatHooks({ scope, projectRoot, url, apiKey, paths });
137
+ if (mode === "claude-compat") {
138
+ installClaudeCompatHooks({
139
+ scope,
140
+ projectRoot,
141
+ url,
142
+ apiKey,
143
+ paths,
144
+ handlersRoot: installedHooksRoot,
145
+ });
120
146
  }
121
147
  },
122
148
  installSkills({ dryRun, log, scope, projectRoot }) {
@@ -130,7 +156,7 @@ export function createCursorAdapter(customPaths = {}) {
130
156
 
131
157
  mkdirSync(skillDir, { recursive: true });
132
158
  for (const skill of SKILLS) {
133
- const source = resolveSkillSource(skill.name);
159
+ const source = resolveSkillSourcePath(skill.name);
134
160
  const destination = join(skillDir, skill.name);
135
161
 
136
162
  if (existsSync(destination)) {
@@ -159,9 +185,20 @@ export function createCursorAdapter(customPaths = {}) {
159
185
  // Same version already deployed — nothing to do.
160
186
  continue;
161
187
  }
188
+ } else if (isDanglingSymlink(destination)) {
189
+ // Stale symlink left behind by an earlier install whose target
190
+ // (e.g. a deleted worktree) no longer exists. existsSync is false
191
+ // for these, so the branch above misses them. Clear it so the
192
+ // fresh-install path below can replace it cleanly.
193
+ log.dim?.(
194
+ ` Replacing dangling symlink at ${destination} (target missing).`,
195
+ );
196
+ removePath(destination);
162
197
  }
163
198
 
164
- const strategy = symlinkOrCopy(source, destination);
199
+ const strategy = symlinkOrCopy(source, destination, {
200
+ preferCopy: packagedInstallDetector(),
201
+ });
165
202
  // Only stamp a marker when we materialized a real copy. Writing into
166
203
  // a symlinked source dir would mutate the shipped asset tree, which
167
204
  // is confusing at best and a permission error at worst. Symlinks
@@ -186,7 +223,7 @@ export function createCursorAdapter(customPaths = {}) {
186
223
  uninstallCursorHooks({
187
224
  scope,
188
225
  projectRoot,
189
- mode: cursorHookMode || "hybrid",
226
+ mode: cursorHookMode || "native",
190
227
  paths,
191
228
  });
192
229
  results.push({ key: "hooks", ok: true, message: "Cursor hook configuration cleaned up." });
@@ -206,10 +243,20 @@ export function createCursorAdapter(customPaths = {}) {
206
243
  async validateConfig({ serverName, scope, projectRoot }) {
207
244
  const mcpPath = paths.getMcpPath(scope, projectRoot);
208
245
  const cfg = readJsonFile(mcpPath);
209
- if (cfg?.mcpServers?.[serverName]) {
210
- return { ok: true, message: `Server "${serverName}" is registered in ${mcpPath}.` };
246
+ const entry = cfg?.mcpServers?.[serverName];
247
+ if (!entry) {
248
+ return { ok: false, message: `Server "${serverName}" not found in ${mcpPath}.` };
211
249
  }
212
- return { ok: false, message: `Server "${serverName}" not found in ${mcpPath}.` };
250
+ const hasUserAgent = Object.keys(entry.headers ?? {}).some(
251
+ (key) => key.toLowerCase() === "user-agent",
252
+ );
253
+ if (!hasUserAgent) {
254
+ return {
255
+ ok: false,
256
+ message: `Server "${serverName}" in ${mcpPath} is missing the User-Agent header required by the Cloud WAF.`,
257
+ };
258
+ }
259
+ return { ok: true, message: `Server "${serverName}" is registered in ${mcpPath}.` };
213
260
  },
214
261
  };
215
262
  }
@@ -234,7 +281,22 @@ function ensureEnvBlock(cfg, url, apiKey) {
234
281
  }
235
282
  }
236
283
 
237
- function installNativeCursorHooks({ scope, projectRoot, url, apiKey, paths }) {
284
+ function getCursorHooksInstallRoot(paths, scope, projectRoot) {
285
+ return join(
286
+ dirname(paths.getHooksPath(scope, projectRoot)),
287
+ "meko-hooks",
288
+ "hooks-handlers",
289
+ );
290
+ }
291
+
292
+ function installNativeCursorHooks({
293
+ scope,
294
+ projectRoot,
295
+ url,
296
+ apiKey,
297
+ paths,
298
+ handlersRoot,
299
+ }) {
238
300
  const hooksPath = paths.getHooksPath(scope, projectRoot);
239
301
  const cfg = readJsonFile(hooksPath);
240
302
  if (!cfg.version) cfg.version = 1;
@@ -242,11 +304,12 @@ function installNativeCursorHooks({ scope, projectRoot, url, apiKey, paths }) {
242
304
  cfg.hooks = {};
243
305
  }
244
306
 
245
- const handlersRoot = resolveHooksRoot();
307
+ pruneNativeCursorMekoHooks(cfg.hooks);
246
308
  for (const hook of HOOKS) {
247
309
  const scriptPath = join(handlersRoot, hook.script);
248
310
  const entry = { command: buildScriptCommand({ scriptPath }) };
249
311
  if (hook.timeout) entry.timeout = hook.timeout;
312
+ if (hook.matcher) entry.matcher = hook.matcher;
250
313
  upsertHookEntry(cfg.hooks, hook.event, entry);
251
314
  }
252
315
 
@@ -268,14 +331,20 @@ function installNativeCursorHooks({ scope, projectRoot, url, apiKey, paths }) {
268
331
  // will be pruned; the user must re-run the Cursor installer to restore
269
332
  // them. pruneMekoHookEntries emits a warning on any removal so the user
270
333
  // has a chance to notice before hooks silently disappear.
271
- function installClaudeCompatHooks({ scope, projectRoot, url, apiKey, paths }) {
334
+ function installClaudeCompatHooks({
335
+ scope,
336
+ projectRoot,
337
+ url,
338
+ apiKey,
339
+ paths,
340
+ handlersRoot,
341
+ }) {
272
342
  const settingsPath = paths.getClaudeCompatSettingsPath(scope, projectRoot);
273
343
  const cfg = readJsonFile(settingsPath);
274
344
  if (!cfg.hooks || typeof cfg.hooks !== "object") {
275
345
  cfg.hooks = {};
276
346
  }
277
347
 
278
- const handlersRoot = resolveHooksRoot();
279
348
  const compatMap = {
280
349
  SessionStart: { script: "session-start.sh", timeout: 15000 },
281
350
  PreCompact: { script: "pre-compact.sh" },
@@ -305,6 +374,30 @@ function upsertHookEntry(hooksObj, event, entry) {
305
374
  if (!exists) hooksObj[event].push(entry);
306
375
  }
307
376
 
377
+ function pruneNativeCursorMekoHooks(hooksObj) {
378
+ if (!hooksObj || typeof hooksObj !== "object") return;
379
+ for (const event of Object.keys(hooksObj)) {
380
+ if (!Array.isArray(hooksObj[event])) continue;
381
+ hooksObj[event] = hooksObj[event].filter((item) => {
382
+ if (!item?.command) return true;
383
+ return !isMekoHookCommand(item.command);
384
+ });
385
+ }
386
+ }
387
+
388
+ function isMekoHookCommand(command) {
389
+ if (typeof command !== "string") return false;
390
+ if (!command.includes("hooks-handlers/")) return false;
391
+ return [
392
+ "session-start.sh",
393
+ "cursor-session-start.sh",
394
+ "before-submit-prompt.sh",
395
+ "pre-compact.sh",
396
+ "session-end.sh",
397
+ "post-tool-context.sh",
398
+ ].some((script) => command.includes(script));
399
+ }
400
+
308
401
  function upsertClaudeCompatHook(hooksObj, event, entry) {
309
402
  if (!Array.isArray(hooksObj[event])) hooksObj[event] = [];
310
403
  let bucket = hooksObj[event].find((item) => Array.isArray(item.hooks));
@@ -317,11 +410,9 @@ function upsertClaudeCompatHook(hooksObj, event, entry) {
317
410
  }
318
411
 
319
412
  function uninstallCursorHooks({ scope, projectRoot, mode, paths }) {
320
- const handlersRoot = resolveHooksRoot();
321
- const scripts = ["session-start.sh", "pre-compact.sh", "session-end.sh"]
322
- .map((name) => join(handlersRoot, name));
413
+ const handlersRoot = getCursorHooksInstallRoot(paths, scope, projectRoot);
323
414
 
324
- if (mode === "native" || mode === "hybrid") {
415
+ if (mode === "native") {
325
416
  const hooksPath = paths.getHooksPath(scope, projectRoot);
326
417
  const cfg = readJsonFile(hooksPath);
327
418
  if (cfg?.hooks && typeof cfg.hooks === "object") {
@@ -329,14 +420,14 @@ function uninstallCursorHooks({ scope, projectRoot, mode, paths }) {
329
420
  if (!Array.isArray(cfg.hooks[event])) continue;
330
421
  cfg.hooks[event] = cfg.hooks[event].filter((item) => {
331
422
  if (!item?.command) return true;
332
- return !scripts.some((script) => item.command.includes(script));
423
+ return !isMekoHookCommand(item.command);
333
424
  });
334
425
  }
335
426
  writeJsonFile(hooksPath, cfg);
336
427
  }
337
428
  }
338
429
 
339
- if (mode === "claude-compat" || mode === "hybrid") {
430
+ if (mode === "claude-compat") {
340
431
  // NOTE: we deliberately do NOT strip env.MEKO_MCP_URL / env.MEKO_API_KEY here.
341
432
  // On scope=user, this file is the same ~/.claude/settings.json the Claude Code
342
433
  // adapter owns; the Claude Code uninstall (claude-code.mjs removeKeys call) is
@@ -352,7 +443,7 @@ function uninstallCursorHooks({ scope, projectRoot, mode, paths }) {
352
443
  if (!Array.isArray(group?.hooks)) return group;
353
444
  const filtered = group.hooks.filter((item) => {
354
445
  if (!item?.command) return true;
355
- return !scripts.some((script) => item.command.includes(script));
446
+ return !isMekoHookCommand(item.command);
356
447
  });
357
448
  return { ...group, hooks: filtered };
358
449
  }).filter((group) => {
@@ -363,6 +454,8 @@ function uninstallCursorHooks({ scope, projectRoot, mode, paths }) {
363
454
  writeJsonFile(settingsPath, cfg);
364
455
  }
365
456
  }
457
+
458
+ removePath(dirname(handlersRoot));
366
459
  }
367
460
 
368
461
  /**
package/lib/config.mjs CHANGED
@@ -117,6 +117,58 @@ export function getCodexAgentsMdPath() {
117
117
  return join(codexHome, "AGENTS.md");
118
118
  }
119
119
 
120
+ /**
121
+ * Return path to Codex's user-level hooks config.
122
+ * @returns {string}
123
+ */
124
+ export function getCodexHooksPath() {
125
+ const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
126
+ return join(codexHome, "hooks.json");
127
+ }
128
+
129
+ /**
130
+ * Return path to the Meko env file the Codex hooks source on launch.
131
+ * Lives next to hooks.json under $CODEX_HOME (default ~/.codex/) and is
132
+ * written at mode 0600. Used because Codex's hooks.json schema has no
133
+ * `env` block, and inlining the API key into each hook's `command`
134
+ * field would expose it via `ps -ef` while the hook runs.
135
+ *
136
+ * The hook command becomes `. ~/.codex/meko-env.sh && bash /path/to/hook.sh`,
137
+ * so the secret never appears in process arguments.
138
+ *
139
+ * @returns {string}
140
+ */
141
+ export function getCodexMekoEnvFilePath() {
142
+ const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
143
+ return join(codexHome, "meko-env.sh");
144
+ }
145
+
146
+ /**
147
+ * Return the stable directory the Codex hook scripts are copied into at
148
+ * install time.
149
+ *
150
+ * Why a dedicated copy instead of referencing the shipped asset root
151
+ * directly: when the installer runs via `npx @yugabytedb/meko-mcp`, the
152
+ * asset root resolves to an ephemeral npm cache dir
153
+ * (`~/.npm/_npx/<hash>/node_modules/@yugabytedb/meko-mcp/assets/...`).
154
+ * npx garbage-collects those hashed dirs, so a hooks.json that pins the
155
+ * npx path becomes a dangling pointer on the next npx run — Codex then
156
+ * spawns `bash '<missing script>'`, which exits 127 ("command not found")
157
+ * and surfaces as a failed SessionStart hook + the misleading "could not
158
+ * reach Meko" fallback. Copying the scripts under $CODEX_HOME gives the
159
+ * hook a home-dir-stable target that outlives the npx cache.
160
+ *
161
+ * The `hooks-handlers` leaf is preserved so the command string still
162
+ * matches `isMekoHookCommand`'s prune regex — that lets a reinstall
163
+ * auto-heal any stale npx-pinned entry left by an older installer.
164
+ *
165
+ * @returns {string}
166
+ */
167
+ export function getCodexHooksInstallRoot() {
168
+ const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
169
+ return join(codexHome, "meko-hooks", "hooks-handlers");
170
+ }
171
+
120
172
  /**
121
173
  * Return path to the shared agent-skills directory used by Codex
122
174
  * (and other agent runtimes that follow the convention).
package/lib/installer.mjs CHANGED
@@ -33,11 +33,12 @@ export async function install(options) {
33
33
  skipSkills = false,
34
34
  skipValidation = false,
35
35
  skipCanary = false,
36
+ installStatusline = false,
36
37
  dryRun = false,
37
38
  verbose = false,
38
39
  yes = false,
39
40
  projectRoot = process.cwd(),
40
- cursorHookMode = "hybrid",
41
+ cursorHookMode = "native",
41
42
  } = options;
42
43
 
43
44
  const adapters = getClientAdapters();
@@ -50,6 +51,7 @@ export async function install(options) {
50
51
 
51
52
  let totalSteps = 3; // register + env/hooks + validate
52
53
  if (!skipSkills) totalSteps += 1;
54
+ if (installStatusline && typeof adapter.installStatusline === "function") totalSteps += 1;
53
55
  if (skipValidation) totalSteps -= 1;
54
56
 
55
57
  banner(`Meko Setup for ${adapter.displayName}`);
@@ -171,6 +173,28 @@ export async function install(options) {
171
173
  currentStep++;
172
174
  }
173
175
 
176
+ if (installStatusline && typeof adapter.installStatusline === "function") {
177
+ try {
178
+ adapter.installStatusline({
179
+ scope,
180
+ dryRun,
181
+ verbose,
182
+ log: LOG,
183
+ settingsPath,
184
+ projectRoot,
185
+ });
186
+ stepOk(
187
+ currentStep,
188
+ totalSteps,
189
+ dryRun ? "Wiring datapack statusline (dry-run)." : "Datapack statusline wired.",
190
+ );
191
+ } catch (err) {
192
+ stepFail(currentStep, totalSteps, "Failed to wire datapack statusline.");
193
+ warn(`Non-critical: ${err.message}`);
194
+ }
195
+ currentStep++;
196
+ }
197
+
174
198
  if (!skipValidation) {
175
199
  if (dryRun) {
176
200
  dim("Would validate config, MCP connectivity, and tool availability.");
@@ -225,7 +249,7 @@ export async function uninstall(options = {}) {
225
249
  scope = "user",
226
250
  verbose = false,
227
251
  projectRoot = process.cwd(),
228
- cursorHookMode = "hybrid",
252
+ cursorHookMode = "native",
229
253
  } = options;
230
254
 
231
255
  const adapters = getClientAdapters();
@@ -39,6 +39,11 @@ export function jsonRpcRequest(url, payload, apiKey, sessionId, opts = {}) {
39
39
  const headers = {
40
40
  "Content-Type": "application/json",
41
41
  Accept: "application/json, text/event-stream",
42
+ // Cloud Meko sits behind a WAF that 403s requests with no User-Agent
43
+ // (Node's http/https send none by default). This transport powers install
44
+ // validation and migration against cloud URLs, so it hits the same WAF as
45
+ // the capture hooks — send an explicit UA so those calls aren't rejected.
46
+ "User-Agent": "meko-installer/1.0 (+https://github.com/yugabyte/meko-mcp-server)",
42
47
  };
43
48
  if (apiKey) {
44
49
  headers["Authorization"] = `Bearer ${apiKey}`;
package/lib/validate.mjs CHANGED
@@ -32,7 +32,6 @@ const CORE_TOOLS = [
32
32
  "memory_search",
33
33
  "memory_get_by_id",
34
34
  "conversation_create",
35
- "db_summarize_database",
36
35
  "datapack_list",
37
36
  ];
38
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yugabytedb/meko-mcp",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "Single-line installer for Meko MCP Server across Claude and Cursor clients",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,10 +18,26 @@ const PAYLOAD = [
18
18
  from: join(repoRoot, "skills", "skills", "meko-mcp-tools"),
19
19
  to: join(assetsDir, "skills", "meko-mcp-tools"),
20
20
  },
21
+ {
22
+ from: join(repoRoot, "skills", "skills", "meko-mcp-tools-desktop"),
23
+ to: join(assetsDir, "skills", "meko-mcp-tools-desktop"),
24
+ },
25
+ {
26
+ from: join(repoRoot, "skills", "skills", "meko-select-datapack-desktop"),
27
+ to: join(assetsDir, "skills", "meko-select-datapack-desktop"),
28
+ },
21
29
  {
22
30
  from: join(repoRoot, "skills", "hooks-handlers"),
23
31
  to: join(assetsDir, "hooks-handlers"),
24
32
  },
33
+ {
34
+ from: join(repoRoot, "skills", "scripts", "package-desktop-skill.sh"),
35
+ to: join(assetsDir, "scripts", "package-desktop-skill.sh"),
36
+ },
37
+ {
38
+ from: join(repoRoot, "skills", "scripts", "meko-statusline.sh"),
39
+ to: join(assetsDir, "scripts", "meko-statusline.sh"),
40
+ },
25
41
  ];
26
42
 
27
43
  const cleaning = process.argv.includes("--clean");
@@ -1,38 +0,0 @@
1
- # Read-Only vs Write Query Execution Patterns
2
-
3
- **WRONG:** `db_run_write_query(scope="write", query="SELECT * FROM users")` — requires write scope unnecessarily, triggers guardrail validation, returns `{"rows_affected": -1}` instead of data.
4
- **WRONG:** `db_run_read_only_query(scope="read", query="INSERT INTO ...")` — fails: "cannot execute INSERT in a read-only transaction."
5
-
6
- ## How Each Tool Works
7
-
8
- **`db_run_read_only_query`:** `BEGIN READ ONLY` → execute → `ROLLBACK`. Returns JSON array of row objects.
9
-
10
- **`db_run_write_query`:** Guardrail validation → execute → `COMMIT`. Returns `{"rows_affected": N}`.
11
-
12
- ## When to Use db_summarize_database
13
-
14
- Use instead of writing your own `information_schema` query when you need a table overview:
15
-
16
- ```
17
- db_summarize_database(scope="read", schema="public")
18
- -- Returns table names, column schemas, AND row counts in one call
19
- ```
20
-
21
- ## SQL Best Practices
22
-
23
- **Schema qualification:** For non-public schemas, use `analytics."monthly_revenue"` and pass `schema="analytics"` to `db_summarize_database`.
24
-
25
- **String escaping:** Queries are passed directly to `cur.execute()` — no parameter placeholders. Escape values in SQL: `WHERE name = 'O''Brien'`.
26
-
27
- **LIMIT on exploratory queries:**
28
- ```
29
- db_run_read_only_query(scope="read", query="SELECT * FROM large_table LIMIT 25")
30
- ```
31
-
32
- **Batch large writes:** Due to the 1000-row INSERT cap, split large inserts. Or use `INSERT ... SELECT` for unlimited rows from an existing table.
33
-
34
- **One statement per call:**
35
- ```
36
- WRONG: "CREATE TABLE a (id INT); CREATE TABLE b (id INT);"
37
- RIGHT: Two separate db_run_write_query calls
38
- ```