@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.
- package/README.md +81 -23
- package/assets/hooks-handlers/before-submit-prompt.sh +44 -0
- package/assets/hooks-handlers/cursor-session-start.sh +42 -0
- package/assets/hooks-handlers/lib/capture.js +404 -88
- package/assets/hooks-handlers/lib/checkpoint-timer.js +23 -11
- package/assets/hooks-handlers/session-start.sh +6 -2
- package/assets/scripts/meko-statusline.sh +119 -0
- package/assets/scripts/package-desktop-skill.sh +39 -0
- package/assets/skills/meko-mcp-tools/SKILL.md +42 -50
- package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +75 -150
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +30 -36
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +6 -22
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +9 -8
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +35 -42
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +28 -92
- package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +5 -9
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +1 -81
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +244 -0
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +180 -0
- package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +24 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +99 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +327 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +63 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +214 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +105 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +70 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-scope-permissions.md +71 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +75 -0
- package/assets/skills/meko-select-datapack-desktop/SKILL.md +198 -0
- package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +121 -0
- package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +66 -0
- package/bin/create-meko-setup.mjs +33 -4
- package/lib/clients/claude-code.mjs +133 -6
- package/lib/clients/claude-desktop.mjs +48 -20
- package/lib/clients/codex.mjs +538 -5
- package/lib/clients/common.mjs +122 -1
- package/lib/clients/cursor.mjs +120 -27
- package/lib/config.mjs +52 -0
- package/lib/installer.mjs +26 -2
- package/lib/mcp-transport.mjs +5 -0
- package/lib/validate.mjs +0 -1
- package/package.json +1 -1
- package/scripts/bundle-assets.mjs +16 -0
- package/assets/skills/meko-mcp-tools/references/tools-db-query-patterns.md +0 -38
- package/assets/skills/meko-mcp-tools/references/tools-write-guardrails.md +0 -30
package/lib/clients/common.mjs
CHANGED
|
@@ -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
|
package/lib/clients/cursor.mjs
CHANGED
|
@@ -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 =
|
|
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 || "
|
|
112
|
+
const mode = cursorHookMode || "native";
|
|
113
|
+
const installedHooksRoot = getCursorHooksInstallRoot(paths, scope, projectRoot);
|
|
105
114
|
if (dryRun) {
|
|
106
|
-
|
|
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"
|
|
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
|
-
|
|
116
|
-
|
|
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"
|
|
119
|
-
installClaudeCompatHooks({
|
|
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 =
|
|
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 || "
|
|
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
|
-
|
|
210
|
-
|
|
246
|
+
const entry = cfg?.mcpServers?.[serverName];
|
|
247
|
+
if (!entry) {
|
|
248
|
+
return { ok: false, message: `Server "${serverName}" not found in ${mcpPath}.` };
|
|
211
249
|
}
|
|
212
|
-
|
|
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
|
|
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
|
-
|
|
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({
|
|
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 =
|
|
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"
|
|
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 !
|
|
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"
|
|
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 !
|
|
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 = "
|
|
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 = "
|
|
252
|
+
cursorHookMode = "native",
|
|
229
253
|
} = options;
|
|
230
254
|
|
|
231
255
|
const adapters = getClientAdapters();
|
package/lib/mcp-transport.mjs
CHANGED
|
@@ -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
package/package.json
CHANGED
|
@@ -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
|
-
```
|