@yugabytedb/meko-mcp 1.0.8 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +242 -44
  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 +457 -107
  5. package/assets/hooks-handlers/lib/package.json +1 -0
  6. package/assets/hooks-handlers/test_single_conversation.sh +1 -1
  7. package/assets/scripts/package-desktop-skill.sh +36 -0
  8. package/assets/skills/meko-mcp-tools/SKILL.md +167 -53
  9. package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
  10. package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +84 -22
  11. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +89 -56
  12. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +15 -7
  13. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +17 -9
  14. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +94 -5
  15. package/assets/skills/meko-mcp-tools/references/tools-overview.md +52 -37
  16. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +108 -37
  17. package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +6 -10
  18. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +30 -18
  19. package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +55 -69
  20. package/assets/skills/meko-mcp-tools-desktop/SKILL.md +251 -0
  21. package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +150 -0
  22. package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +24 -0
  23. package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +99 -0
  24. package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +406 -0
  25. package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +69 -0
  26. package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +217 -0
  27. package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +112 -0
  28. package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +134 -0
  29. package/assets/skills/meko-mcp-tools-desktop/references/tools-scope-permissions.md +75 -0
  30. package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +155 -0
  31. package/bin/create-meko-setup.mjs +151 -22
  32. package/lib/clients/claude-code.mjs +378 -68
  33. package/lib/clients/claude-desktop.mjs +45 -8
  34. package/lib/clients/codex.mjs +1209 -0
  35. package/lib/clients/common.mjs +223 -1
  36. package/lib/clients/cursor.mjs +118 -13
  37. package/lib/clients/index.mjs +2 -0
  38. package/lib/config.mjs +56 -0
  39. package/lib/detect.mjs +52 -44
  40. package/lib/installer.mjs +11 -4
  41. package/lib/mcp-transport.mjs +61 -2
  42. package/lib/migrate/adapters/memory-md.mjs +13 -6
  43. package/lib/migrate/datapack.mjs +167 -0
  44. package/lib/migrate/id.mjs +127 -9
  45. package/lib/migrate/orchestrator.mjs +68 -10
  46. package/lib/migrate.mjs +346 -3
  47. package/lib/prompt.mjs +22 -4
  48. package/lib/validate.mjs +313 -9
  49. package/lib/version-check.mjs +178 -0
  50. package/package.json +2 -1
  51. package/scripts/bundle-assets.mjs +8 -0
package/lib/validate.mjs CHANGED
@@ -1,10 +1,15 @@
1
1
  /**
2
2
  * validate.mjs - Post-install validation for Meko MCP Server.
3
3
  *
4
- * Runs three checks in sequence:
4
+ * Runs four checks in sequence:
5
5
  * 1. Config check - Is the server registered in `claude mcp list`?
6
- * 2. Connectivity - Can we complete an MCP `initialize` handshake?
7
- * 3. Tools - Does `tools/list` return the expected core tools?
6
+ * 2. Connectivity - Can we complete an MCP `initialize` handshake?
7
+ * 3. Tools - Does `tools/list` return the expected core tools?
8
+ * 4. Canary - Does a deterministic read-only Meko tool call return
9
+ * a sane response? Proves auth → tenant → datapack
10
+ * routing end-to-end without depending on mem0's
11
+ * LLM fact-extractor (which historically failed the
12
+ * canary non-deterministically).
8
13
  *
9
14
  * HTTP transport lives in ./mcp-transport.mjs and is shared with the
10
15
  * migrate subcommand.
@@ -25,10 +30,18 @@ import {
25
30
  /** Core tools that must appear in tools/list for validation to pass. */
26
31
  const CORE_TOOLS = [
27
32
  "memory_search",
33
+ "memory_get_by_id",
28
34
  "conversation_create",
29
35
  "db_summarize_database",
36
+ "datapack_list",
30
37
  ];
31
38
 
39
+ /** Nil UUID used as the conversation_id placeholder for the canary call. */
40
+ const NIL_UUID = "00000000-0000-0000-0000-000000000000";
41
+
42
+ /** Tool the canary invokes. */
43
+ const CANARY_TOOL = "datapack_list";
44
+
32
45
  // ---------------------------------------------------------------------------
33
46
  // 1. Config validation
34
47
  // ---------------------------------------------------------------------------
@@ -118,21 +131,149 @@ export async function validateTools(url, apiKey) {
118
131
  }
119
132
 
120
133
  // ---------------------------------------------------------------------------
121
- // 4. Combined runner
134
+ // 4. Canary validation (deterministic, read-only)
135
+ // ---------------------------------------------------------------------------
136
+
137
+ /**
138
+ * Call a deterministic, read-only Meko tool and verify the response shape.
139
+ *
140
+ * The canary's job is to prove the part of the stack that connectivity +
141
+ * tools/list don't cover: auth was accepted as a real Meko identity, the
142
+ * tenant resolver mapped it to an account, and the datapack routing wired
143
+ * up the user's actual workspace. We invoke `datapack_list` for this — it
144
+ * is a single read-only call, runs a short SQL query against the system
145
+ * schema, and returns deterministically (every authenticated user has at
146
+ * least one datapack: their default).
147
+ *
148
+ * Why not memory_add? The previous canary did a write/read/delete round-trip
149
+ * via `memory_add`/`memory_get_by_id`/`memory_delete_by_id`. mem0's LLM
150
+ * fact-extractor decides per-call whether the input contains facts worth
151
+ * indexing; for the canary text it returned `results: []` *some of the time*
152
+ * even though the graph extractor did add entities. The installer's id
153
+ * extraction then reported "no memory id" and failed validation despite the
154
+ * server being healthy. A non-LLM read-only tool removes that whole class
155
+ * of false negative.
156
+ *
157
+ * Security note: `datapack_list` returns each datapack's
158
+ * `datapack_ysql_connection_string`, which contains a database password.
159
+ * This function MUST NOT echo the raw response into a user-facing message.
160
+ * Failure messages reference the tool name, the JSON-RPC error code, or a
161
+ * server-emitted `error` discriminator — never the body.
162
+ *
163
+ * @param {string} url - Full MCP endpoint URL.
164
+ * @param {string|null} apiKey - Optional API key.
165
+ * @param {object} [options]
166
+ * @param {string} [options.sessionId] - Existing MCP session id. If omitted, a new
167
+ * initialize handshake is performed.
168
+ * @param {number} [options.timeoutMs] - Override the canary call's transport timeout.
169
+ * Defaults to DEFAULT_REQUEST_TIMEOUT_MS.
170
+ * @returns {Promise<{
171
+ * ok: boolean,
172
+ * message: string,
173
+ * datapack_count: number|null,
174
+ * step?: "initialize"|"datapack_list",
175
+ * }>}
176
+ */
177
+ export async function validateCanary(url, apiKey, options = {}) {
178
+ const timeoutMs = options.timeoutMs;
179
+
180
+ let sessionId = options.sessionId ?? null;
181
+ if (!sessionId) {
182
+ const init = await mcpInitialize(url, apiKey);
183
+ if (!init.ok) {
184
+ return {
185
+ ok: false,
186
+ datapack_count: null,
187
+ step: "initialize",
188
+ message: `Canary: could not initialize MCP session (${init.message}).`,
189
+ };
190
+ }
191
+ sessionId = init.sessionId;
192
+ }
193
+
194
+ const callRes = await mcpCallTool(
195
+ url,
196
+ apiKey,
197
+ sessionId,
198
+ CANARY_TOOL,
199
+ {
200
+ scope: "read",
201
+ conversation_id: NIL_UUID,
202
+ },
203
+ { timeoutMs },
204
+ );
205
+
206
+ if (!callRes.ok) {
207
+ // mcpCallTool already produces a sanitized message (tool name + reason
208
+ // or JSON-RPC error code); it never includes the raw text body when
209
+ // the server emits a structured `{error: ...}` envelope. Pass it
210
+ // through verbatim.
211
+ return {
212
+ ok: false,
213
+ datapack_count: null,
214
+ step: CANARY_TOOL,
215
+ message: `Canary: ${callRes.message}`,
216
+ };
217
+ }
218
+
219
+ const datapacks = parseDatapackList(callRes.text);
220
+ if (!datapacks) {
221
+ // The tool returned a 2xx but the body wasn't a recognizable list.
222
+ // Don't dump it — list_datapacks responses include connection-string
223
+ // secrets when they parse correctly, and we have no way to know whether
224
+ // the unparseable body has the same.
225
+ return {
226
+ ok: false,
227
+ datapack_count: null,
228
+ step: CANARY_TOOL,
229
+ message:
230
+ `Canary: ${CANARY_TOOL} returned an unexpected response shape ` +
231
+ `(expected a JSON array of datapack objects). Check the server logs.`,
232
+ };
233
+ }
234
+
235
+ if (datapacks.length === 0) {
236
+ return {
237
+ ok: false,
238
+ datapack_count: 0,
239
+ step: CANARY_TOOL,
240
+ message:
241
+ `Canary: ${CANARY_TOOL} returned zero datapacks. Every authenticated ` +
242
+ `Meko user should have at least their default datapack — this likely ` +
243
+ `means the API token authenticated but resolved to the wrong tenant. ` +
244
+ `Verify the API key matches the tenant the URL points at.`,
245
+ };
246
+ }
247
+
248
+ return {
249
+ ok: true,
250
+ datapack_count: datapacks.length,
251
+ message: `Canary OK — ${CANARY_TOOL} returned ${datapacks.length} datapack(s).`,
252
+ };
253
+ }
254
+
255
+ // ---------------------------------------------------------------------------
256
+ // 5. Combined runner
122
257
  // ---------------------------------------------------------------------------
123
258
 
124
259
  /**
125
260
  * Run all validation checks in sequence and return a summary.
126
261
  *
127
262
  * When the connectivity check succeeds, the tools check reuses the same
128
- * MCP session rather than performing a redundant second initialize.
263
+ * MCP session rather than performing a redundant second initialize. The
264
+ * canary reuses that same session too.
265
+ *
266
+ * The canary is skipped (not failed) when tools/list didn't pass — the
267
+ * canary depends on `datapack_list` being present, and a tools failure
268
+ * already surfaces that.
129
269
  *
130
270
  * @param {string} url - Full MCP endpoint URL.
131
271
  * @param {string|null} apiKey - Optional API key.
132
272
  * @param {string} serverName - Server name for config check (default "meko").
133
273
  * @param {object} [options]
134
274
  * @param {(params: {serverName: string}) => Promise<{ok: boolean, message: string}>} [options.validateConfig]
135
- * @returns {Promise<{config: object, connectivity: object, tools: object, allPassed: boolean}>}
275
+ * @param {boolean} [options.skipCanary] - Skip the canary entirely.
276
+ * @returns {Promise<{config: object, connectivity: object, tools: object, canary: object|null, allPassed: boolean}>}
136
277
  */
137
278
  export async function runValidation(url, apiKey, serverName = "meko", options = {}) {
138
279
  const configValidator = options.validateConfig ?? (async ({ serverName: value }) => validateConfig(value));
@@ -147,11 +288,34 @@ export async function runValidation(url, apiKey, serverName = "meko", options =
147
288
  tools = { ok: false, tools: [], message: `Skipped: ${connectivity.message}` };
148
289
  }
149
290
 
291
+ let canary = null;
292
+ if (!options.skipCanary) {
293
+ if (tools.ok && connectivity.ok) {
294
+ canary = await validateCanary(url, apiKey, {
295
+ sessionId: connectivity.sessionId,
296
+ });
297
+ } else {
298
+ canary = {
299
+ ok: false,
300
+ datapack_count: null,
301
+ message: `Skipped: tools/list did not pass (${tools.message}).`,
302
+ skipped: true,
303
+ };
304
+ }
305
+ }
306
+
307
+ const allPassed =
308
+ config.ok &&
309
+ connectivity.ok &&
310
+ tools.ok &&
311
+ (!canary || canary.ok);
312
+
150
313
  return {
151
314
  config,
152
315
  connectivity,
153
316
  tools,
154
- allPassed: config.ok && connectivity.ok && tools.ok,
317
+ canary,
318
+ allPassed,
155
319
  };
156
320
  }
157
321
 
@@ -190,7 +354,7 @@ async function sendToolsList(url, apiKey, sessionId) {
190
354
  return {
191
355
  ok: false,
192
356
  tools: [],
193
- message: `tools/list returned HTTP ${res.statusCode}: ${res.body}`,
357
+ message: `tools/list returned HTTP ${res.statusCode}: ${String(res.body ?? "").slice(0, 300)}`,
194
358
  };
195
359
  }
196
360
 
@@ -201,7 +365,7 @@ async function sendToolsList(url, apiKey, sessionId) {
201
365
  return {
202
366
  ok: false,
203
367
  tools: [],
204
- message: `tools/list returned non-JSON: ${res.body.slice(0, 200)}`,
368
+ message: `tools/list returned non-JSON: ${String(res.body ?? "").slice(0, 200)}`,
205
369
  };
206
370
  }
207
371
 
@@ -234,6 +398,146 @@ async function sendToolsList(url, apiKey, sessionId) {
234
398
  };
235
399
  }
236
400
 
401
+ // ---------------------------------------------------------------------------
402
+ // Internal: tools/call for the canary
403
+ // ---------------------------------------------------------------------------
404
+
405
+ /**
406
+ * Send a `tools/call` request and unwrap the MCP content envelope.
407
+ *
408
+ * Returns `{ok, text, message}` where `text` is the first text block (what
409
+ * Meko tools return as a JSON-encoded string) and `message` is only
410
+ * populated on the non-ok path. On the non-ok path the message is sanitized
411
+ * — for tool-level errors emitted as `{"error": "...", "detail": "..."}`,
412
+ * the discriminator and detail are surfaced; the raw body is never
413
+ * echoed because it can contain secrets (e.g. `datapack_list` returns
414
+ * connection strings).
415
+ *
416
+ * @param {string} url
417
+ * @param {string|null} apiKey
418
+ * @param {string|null} sessionId
419
+ * @param {string} name - Tool name, e.g. "datapack_list".
420
+ * @param {object} args - Tool arguments.
421
+ * @param {{timeoutMs?: number}} [opts]
422
+ * @returns {Promise<{ok: boolean, text: string|null, message?: string}>}
423
+ */
424
+ async function mcpCallTool(url, apiKey, sessionId, name, args, opts = {}) {
425
+ const payload = {
426
+ jsonrpc: "2.0",
427
+ id: Date.now() + Math.floor(Math.random() * 1000),
428
+ method: "tools/call",
429
+ params: { name, arguments: args },
430
+ };
431
+
432
+ const timeoutMs = opts.timeoutMs;
433
+ let res;
434
+ try {
435
+ res = await jsonRpcRequest(url, payload, apiKey, sessionId, { timeoutMs });
436
+ } catch (err) {
437
+ return {
438
+ ok: false,
439
+ text: null,
440
+ message: `${name} request failed: ${classifyError(err, timeoutMs)}`,
441
+ };
442
+ }
443
+
444
+ if (res.statusCode < 200 || res.statusCode >= 300) {
445
+ return {
446
+ ok: false,
447
+ text: null,
448
+ message: `${name} returned HTTP ${res.statusCode}: ${String(res.body ?? "").slice(0, 300)}`,
449
+ };
450
+ }
451
+
452
+ let parsed;
453
+ try {
454
+ parsed = JSON.parse(res.body);
455
+ } catch {
456
+ return {
457
+ ok: false,
458
+ text: null,
459
+ message: `${name} returned non-JSON at the JSON-RPC envelope.`,
460
+ };
461
+ }
462
+
463
+ if (parsed.error) {
464
+ return {
465
+ ok: false,
466
+ text: null,
467
+ message: `${name} error: ${parsed.error.message ?? JSON.stringify(parsed.error)}`,
468
+ };
469
+ }
470
+
471
+ const text = extractFirstText(parsed.result);
472
+
473
+ // Meko tools sometimes encode a tool-level failure inside the text body
474
+ // (e.g. memory_add returning `{"error": "memory_add_failed", ...}`,
475
+ // datapack_list returning `{"error": "list_failed", "detail": ...}`).
476
+ // Surface the discriminator and detail — never the full body, since the
477
+ // success body of datapack_list contains connection-string secrets and we
478
+ // can't distinguish before parsing.
479
+ if (text) {
480
+ try {
481
+ const body = JSON.parse(text);
482
+ if (body && typeof body === "object" && !Array.isArray(body) && typeof body.error === "string") {
483
+ const detail = typeof body.detail === "string" ? ` (${body.detail})` : "";
484
+ return {
485
+ ok: false,
486
+ text,
487
+ message: `${name} server error: ${body.error}${detail}`,
488
+ };
489
+ }
490
+ } catch {
491
+ // Non-JSON bodies are fine — many tools return plain text.
492
+ }
493
+ }
494
+
495
+ return { ok: true, text };
496
+ }
497
+
498
+ /**
499
+ * Meko tool results come back as `{content: [{type:"text", text: "..."}]}`.
500
+ */
501
+ function extractFirstText(result) {
502
+ const content = result?.content;
503
+ if (!Array.isArray(content)) return null;
504
+ for (const block of content) {
505
+ if (block && block.type === "text" && typeof block.text === "string") {
506
+ return block.text;
507
+ }
508
+ }
509
+ return null;
510
+ }
511
+
512
+ /**
513
+ * Parse a `datapack_list` response body and return the array of datapacks,
514
+ * or `null` if the body is not a recognizable datapack list.
515
+ *
516
+ * Successful `datapack_list` returns a JSON array; each element has at
517
+ * minimum a `datapack_id` and `datapack_name`. We accept any array whose
518
+ * elements look like datapack records — we deliberately don't enumerate or
519
+ * log the elements since they include credentials.
520
+ *
521
+ * @param {string|null} text
522
+ * @returns {Array|null}
523
+ */
524
+ function parseDatapackList(text) {
525
+ if (!text) return null;
526
+ let body;
527
+ try {
528
+ body = JSON.parse(text);
529
+ } catch {
530
+ return null;
531
+ }
532
+ if (!Array.isArray(body)) return null;
533
+ // Empty array is a meaningful answer (caller treats it as "wrong tenant").
534
+ if (body.length === 0) return body;
535
+ const allLookLikeDatapacks = body.every(
536
+ (entry) => entry && typeof entry === "object" && typeof entry.datapack_id === "string",
537
+ );
538
+ return allLookLikeDatapacks ? body : null;
539
+ }
540
+
237
541
  // ---------------------------------------------------------------------------
238
542
  // Internal: child process helper
239
543
  // ---------------------------------------------------------------------------
@@ -0,0 +1,178 @@
1
+ /**
2
+ * version-check.mjs — Optional npm-registry version probe.
3
+ *
4
+ * Never throws, never rejects, never blocks the install. Used by the installer
5
+ * to surface an upgrade hint when a newer @yugabytedb/meko-mcp is available.
6
+ *
7
+ * Zero external dependencies (node:https + node:url only), matching the
8
+ * Design Principles documented in installer/README.md.
9
+ */
10
+
11
+ import https from "node:https";
12
+ import { URL } from "node:url";
13
+
14
+ const REGISTRY_URL =
15
+ "https://registry.npmjs.org/@yugabytedb/meko-mcp/latest";
16
+
17
+ /**
18
+ * Semver-ish comparator. Returns -1 if a<b, 0 if equal, 1 if a>b, or `null`
19
+ * when the comparison cannot be ordered (e.g. a side contains non-numeric
20
+ * pre-release tags).
21
+ *
22
+ * Deliberately minimal: splits on ".", maps to Number, and compares
23
+ * lexicographically over the resulting tuple. Does NOT order pre-release
24
+ * tags (`1.0.0-alpha.1`) — for the installer we only care about stable
25
+ * releases like 1.0.8 vs 1.0.9. The `null` sentinel lets callers
26
+ * distinguish "equal" (0) from "incomparable" and fall back to strict
27
+ * string equality only when actually needed, so identical-but-formatted-
28
+ * differently versions like `1.0` vs `1.0.0` are NOT flagged as outdated.
29
+ */
30
+ export function compareSemver(a, b) {
31
+ const partsA = String(a).split(".").map((x) => Number(x));
32
+ const partsB = String(b).split(".").map((x) => Number(x));
33
+ const hasNaN = partsA.some((n) => Number.isNaN(n)) || partsB.some((n) => Number.isNaN(n));
34
+ if (hasNaN) {
35
+ return a === b ? 0 : null;
36
+ }
37
+ const len = Math.max(partsA.length, partsB.length);
38
+ for (let i = 0; i < len; i++) {
39
+ const x = partsA[i] ?? 0;
40
+ const y = partsB[i] ?? 0;
41
+ if (x < y) return -1;
42
+ if (x > y) return 1;
43
+ }
44
+ return 0;
45
+ }
46
+
47
+ /**
48
+ * Fetch the latest published version of @yugabytedb/meko-mcp from the npm
49
+ * registry and compare against `currentVersion`.
50
+ *
51
+ * Never throws. On any network error, timeout, or parse failure, returns
52
+ * `{ current, latest: null, isOutdated: false, error: <string> }`.
53
+ *
54
+ * @param {object} opts
55
+ * @param {string} opts.currentVersion — the installed version to compare against.
56
+ * @param {number} [opts.timeoutMs=2000] — hard cap on the registry request.
57
+ * @param {boolean} [opts.background=false] — when true, unref the request's
58
+ * underlying socket so an in-flight probe does not keep the Node process
59
+ * alive after the main install completes. Callers who `await` the result
60
+ * (e.g. the `--check-updates` handler) should leave this false.
61
+ * @returns {Promise<{current: string, latest: string|null, isOutdated: boolean, error?: string}>}
62
+ */
63
+ export async function checkLatest({ currentVersion, timeoutMs = 2000, background = false, _requestImpl } = {}) {
64
+ const requestImpl = _requestImpl ?? https.request;
65
+
66
+ return new Promise((resolve) => {
67
+ let settled = false;
68
+ const done = (result) => {
69
+ if (settled) return;
70
+ settled = true;
71
+ resolve({ current: currentVersion, ...result });
72
+ };
73
+
74
+ let url;
75
+ try {
76
+ url = new URL(REGISTRY_URL);
77
+ } catch (err) {
78
+ return done({ latest: null, isOutdated: false, error: `bad-url: ${err.message}` });
79
+ }
80
+
81
+ let req;
82
+ try {
83
+ req = requestImpl(
84
+ {
85
+ method: "GET",
86
+ hostname: url.hostname,
87
+ port: url.port || 443,
88
+ path: url.pathname + url.search,
89
+ headers: {
90
+ Accept: "application/json",
91
+ "User-Agent": `@yugabytedb/meko-mcp/${currentVersion}`,
92
+ },
93
+ },
94
+ (res) => {
95
+ if (res.statusCode && res.statusCode >= 400) {
96
+ // Drain so the socket can be freed, then report error.
97
+ res.resume();
98
+ return done({
99
+ latest: null,
100
+ isOutdated: false,
101
+ error: `http ${res.statusCode}`,
102
+ });
103
+ }
104
+ let body = "";
105
+ res.setEncoding("utf8");
106
+ res.on("data", (chunk) => (body += chunk));
107
+ res.on("end", () => {
108
+ try {
109
+ const parsed = JSON.parse(body);
110
+ const latest = parsed?.version;
111
+ if (typeof latest !== "string" || latest.length === 0) {
112
+ return done({
113
+ latest: null,
114
+ isOutdated: false,
115
+ error: "registry response missing .version",
116
+ });
117
+ }
118
+ let isOutdated;
119
+ const cmp = compareSemver(currentVersion, latest);
120
+ if (cmp === null) {
121
+ // Incomparable (non-numeric tags). Fall back to strict
122
+ // string inequality; only flag outdated if they actually
123
+ // differ as strings.
124
+ isOutdated = currentVersion !== latest;
125
+ } else {
126
+ isOutdated = cmp < 0;
127
+ }
128
+ done({ latest, isOutdated });
129
+ } catch (err) {
130
+ done({
131
+ latest: null,
132
+ isOutdated: false,
133
+ error: `parse: ${err.message}`,
134
+ });
135
+ }
136
+ });
137
+ res.on("error", (err) => {
138
+ done({ latest: null, isOutdated: false, error: err.message });
139
+ });
140
+ },
141
+ );
142
+ } catch (err) {
143
+ return done({ latest: null, isOutdated: false, error: err.message });
144
+ }
145
+
146
+ const timer = setTimeout(() => {
147
+ try {
148
+ req.destroy(new Error("timeout"));
149
+ } catch {
150
+ /* ignore */
151
+ }
152
+ done({ latest: null, isOutdated: false, error: "timeout" });
153
+ }, timeoutMs);
154
+ if (typeof timer.unref === "function") timer.unref();
155
+
156
+ // In background mode, unref the socket so Node can exit as soon as the
157
+ // main work finishes even if the registry request is still in flight.
158
+ // The socket appears on "socket" — not on the request object itself at
159
+ // construction time — so we subscribe rather than reaching for req.socket.
160
+ if (background) {
161
+ req.on("socket", (socket) => {
162
+ if (socket && typeof socket.unref === "function") socket.unref();
163
+ });
164
+ }
165
+
166
+ req.on("error", (err) => {
167
+ clearTimeout(timer);
168
+ done({ latest: null, isOutdated: false, error: err.message });
169
+ });
170
+
171
+ try {
172
+ req.end();
173
+ } catch (err) {
174
+ clearTimeout(timer);
175
+ done({ latest: null, isOutdated: false, error: err.message });
176
+ }
177
+ });
178
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yugabytedb/meko-mcp",
3
- "version": "1.0.8",
3
+ "version": "1.2.0",
4
4
  "description": "Single-line installer for Meko MCP Server across Claude and Cursor clients",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,6 +19,7 @@
19
19
  "postpack": "node scripts/bundle-assets.mjs --clean",
20
20
  "bundle-assets": "node scripts/bundle-assets.mjs",
21
21
  "test": "node --test \"test/*.mjs\"",
22
+ "smoke": "./scripts/prepublish-smoke.sh",
22
23
  "test:unit": "node --test test/config.test.mjs test/validate.test.mjs test/clients.test.mjs",
23
24
  "test:integration": "RUN_INSTALLER_INTEGRATION=1 node --test test/installer.test.mjs",
24
25
  "test:integration:cloud": "RUN_INSTALLER_INTEGRATION=1 node --test test/installer.test.mjs"
@@ -18,10 +18,18 @@ 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
+ },
21
25
  {
22
26
  from: join(repoRoot, "skills", "hooks-handlers"),
23
27
  to: join(assetsDir, "hooks-handlers"),
24
28
  },
29
+ {
30
+ from: join(repoRoot, "skills", "scripts", "package-desktop-skill.sh"),
31
+ to: join(assetsDir, "scripts", "package-desktop-skill.sh"),
32
+ },
25
33
  ];
26
34
 
27
35
  const cleaning = process.argv.includes("--clean");