@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.
- package/README.md +242 -44
- 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 +457 -107
- package/assets/hooks-handlers/lib/package.json +1 -0
- package/assets/hooks-handlers/test_single_conversation.sh +1 -1
- package/assets/scripts/package-desktop-skill.sh +36 -0
- package/assets/skills/meko-mcp-tools/SKILL.md +167 -53
- package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +84 -22
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +89 -56
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +15 -7
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +17 -9
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +94 -5
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +52 -37
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +108 -37
- package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +6 -10
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +30 -18
- package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +55 -69
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +251 -0
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +150 -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 +406 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +69 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +217 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +112 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +134 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-scope-permissions.md +75 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +155 -0
- package/bin/create-meko-setup.mjs +151 -22
- package/lib/clients/claude-code.mjs +378 -68
- package/lib/clients/claude-desktop.mjs +45 -8
- package/lib/clients/codex.mjs +1209 -0
- package/lib/clients/common.mjs +223 -1
- package/lib/clients/cursor.mjs +118 -13
- package/lib/clients/index.mjs +2 -0
- package/lib/config.mjs +56 -0
- package/lib/detect.mjs +52 -44
- package/lib/installer.mjs +11 -4
- package/lib/mcp-transport.mjs +61 -2
- package/lib/migrate/adapters/memory-md.mjs +13 -6
- package/lib/migrate/datapack.mjs +167 -0
- package/lib/migrate/id.mjs +127 -9
- package/lib/migrate/orchestrator.mjs +68 -10
- package/lib/migrate.mjs +346 -3
- package/lib/prompt.mjs +22 -4
- package/lib/validate.mjs +313 -9
- package/lib/version-check.mjs +178 -0
- package/package.json +2 -1
- package/scripts/bundle-assets.mjs +8 -0
package/lib/installer.mjs
CHANGED
|
@@ -32,11 +32,12 @@ export async function install(options) {
|
|
|
32
32
|
name = "meko",
|
|
33
33
|
skipSkills = false,
|
|
34
34
|
skipValidation = false,
|
|
35
|
+
skipCanary = false,
|
|
35
36
|
dryRun = false,
|
|
36
37
|
verbose = false,
|
|
37
38
|
yes = false,
|
|
38
39
|
projectRoot = process.cwd(),
|
|
39
|
-
cursorHookMode = "
|
|
40
|
+
cursorHookMode = "native",
|
|
40
41
|
} = options;
|
|
41
42
|
|
|
42
43
|
const adapters = getClientAdapters();
|
|
@@ -107,7 +108,7 @@ export async function install(options) {
|
|
|
107
108
|
|
|
108
109
|
if (!skipMcpRegistration) {
|
|
109
110
|
try {
|
|
110
|
-
adapter.registerServer({
|
|
111
|
+
await adapter.registerServer({
|
|
111
112
|
name,
|
|
112
113
|
url,
|
|
113
114
|
scope,
|
|
@@ -116,6 +117,7 @@ export async function install(options) {
|
|
|
116
117
|
verbose,
|
|
117
118
|
log: LOG,
|
|
118
119
|
projectRoot,
|
|
120
|
+
yes,
|
|
119
121
|
});
|
|
120
122
|
stepOk(currentStep, totalSteps, dryRun ? "Registering MCP server (dry-run)." : "MCP server registered.");
|
|
121
123
|
} catch (err) {
|
|
@@ -151,6 +153,7 @@ export async function install(options) {
|
|
|
151
153
|
if (!skipSkills) {
|
|
152
154
|
try {
|
|
153
155
|
const result = adapter.installSkills({
|
|
156
|
+
name,
|
|
154
157
|
scope,
|
|
155
158
|
dryRun,
|
|
156
159
|
verbose,
|
|
@@ -176,7 +179,8 @@ export async function install(options) {
|
|
|
176
179
|
try {
|
|
177
180
|
const results = await runValidation(url, apiKey, name, {
|
|
178
181
|
validateConfig: async ({ serverName }) =>
|
|
179
|
-
adapter.validateConfig({ serverName, scope, projectRoot }),
|
|
182
|
+
adapter.validateConfig({ serverName, scope, projectRoot, url, apiKey }),
|
|
183
|
+
skipCanary,
|
|
180
184
|
});
|
|
181
185
|
|
|
182
186
|
const checks = [
|
|
@@ -184,6 +188,9 @@ export async function install(options) {
|
|
|
184
188
|
{ label: "Connectivity", ...results.connectivity },
|
|
185
189
|
{ label: "Tools", ...results.tools },
|
|
186
190
|
];
|
|
191
|
+
if (results.canary) {
|
|
192
|
+
checks.push({ label: "Canary", ...results.canary });
|
|
193
|
+
}
|
|
187
194
|
for (const check of checks) {
|
|
188
195
|
if (check.ok) success(`${check.label}: ${check.message}`);
|
|
189
196
|
else warn(`${check.label}: ${check.message}`);
|
|
@@ -218,7 +225,7 @@ export async function uninstall(options = {}) {
|
|
|
218
225
|
scope = "user",
|
|
219
226
|
verbose = false,
|
|
220
227
|
projectRoot = process.cwd(),
|
|
221
|
-
cursorHookMode = "
|
|
228
|
+
cursorHookMode = "native",
|
|
222
229
|
} = options;
|
|
223
230
|
|
|
224
231
|
const adapters = getClientAdapters();
|
package/lib/mcp-transport.mjs
CHANGED
|
@@ -29,9 +29,16 @@ export function jsonRpcRequest(url, payload, apiKey, sessionId, opts = {}) {
|
|
|
29
29
|
const parsed = new URL(url);
|
|
30
30
|
const transport = parsed.protocol === "https:" ? https : http;
|
|
31
31
|
|
|
32
|
+
// MCP Streamable HTTP requires the client to advertise both content
|
|
33
|
+
// types: a non-streaming JSON response uses application/json, a
|
|
34
|
+
// streaming response (which is what FastMCP returns for `initialize`
|
|
35
|
+
// and any other call when stateless_http isn't `json_response=True`)
|
|
36
|
+
// uses text/event-stream. Sending only `application/json` produces a
|
|
37
|
+
// 406 Not Acceptable from spec-compliant servers (see meko-mcp-server
|
|
38
|
+
// PR #97 / MEKO-137 which turned this on for prod).
|
|
32
39
|
const headers = {
|
|
33
40
|
"Content-Type": "application/json",
|
|
34
|
-
Accept: "application/json",
|
|
41
|
+
Accept: "application/json, text/event-stream",
|
|
35
42
|
};
|
|
36
43
|
if (apiKey) {
|
|
37
44
|
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
@@ -56,10 +63,19 @@ export function jsonRpcRequest(url, payload, apiKey, sessionId, opts = {}) {
|
|
|
56
63
|
const chunks = [];
|
|
57
64
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
58
65
|
res.on("end", () => {
|
|
66
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
67
|
+
// If the server answered with text/event-stream (the MCP spec's
|
|
68
|
+
// streaming response shape), extract the JSON-RPC payload from
|
|
69
|
+
// the first SSE `data:` field. Callers downstream want a plain
|
|
70
|
+
// JSON-RPC body — they don't care which framing the wire used.
|
|
71
|
+
const contentType = String(res.headers["content-type"] ?? "").toLowerCase();
|
|
72
|
+
const body = contentType.includes("text/event-stream")
|
|
73
|
+
? extractSseData(raw)
|
|
74
|
+
: raw;
|
|
59
75
|
resolve({
|
|
60
76
|
statusCode: res.statusCode,
|
|
61
77
|
headers: res.headers,
|
|
62
|
-
body
|
|
78
|
+
body,
|
|
63
79
|
});
|
|
64
80
|
});
|
|
65
81
|
},
|
|
@@ -75,6 +91,49 @@ export function jsonRpcRequest(url, payload, apiKey, sessionId, opts = {}) {
|
|
|
75
91
|
});
|
|
76
92
|
}
|
|
77
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Extract the JSON-RPC payload from an SSE-framed response body.
|
|
96
|
+
*
|
|
97
|
+
* MCP servers running Streamable HTTP wrap each JSON-RPC response in an
|
|
98
|
+
* SSE event of the form:
|
|
99
|
+
*
|
|
100
|
+
* event: message
|
|
101
|
+
* data: {"jsonrpc":"2.0","id":1,"result":{...}}
|
|
102
|
+
*
|
|
103
|
+
* For a single non-streaming JSON-RPC response (initialize, tools/list,
|
|
104
|
+
* tools/call) there's exactly one event in the body. SSE allows the
|
|
105
|
+
* payload to span multiple `data:` lines; per the spec they are joined
|
|
106
|
+
* with `\n`. We concatenate every `data:` line we see in the first event
|
|
107
|
+
* and return that. Returns the raw body unchanged if no `data:` line is
|
|
108
|
+
* found (defensive fallback so a misbehaving server still produces a
|
|
109
|
+
* useful "non-JSON response" error downstream rather than an empty
|
|
110
|
+
* string).
|
|
111
|
+
*
|
|
112
|
+
* @param {string} raw - Raw response body (already utf8-decoded).
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
function extractSseData(raw) {
|
|
116
|
+
const dataLines = [];
|
|
117
|
+
// Split on all three SSE-permitted line terminators per the WHATWG SSE
|
|
118
|
+
// spec: LF, CRLF, and bare CR. Most servers (including FastMCP) emit
|
|
119
|
+
// LF or CRLF, but the spec is explicit about CR-only being valid and
|
|
120
|
+
// we shouldn't break on a producer that uses it.
|
|
121
|
+
for (const rawLine of raw.split(/\r\n|\r|\n/)) {
|
|
122
|
+
if (rawLine === "") {
|
|
123
|
+
// Blank line terminates the event. We only need the first event for
|
|
124
|
+
// a JSON-RPC response.
|
|
125
|
+
if (dataLines.length > 0) break;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
// `data:` field — capture the value, stripping the optional single
|
|
129
|
+
// leading space the spec allows after the colon. Anything else
|
|
130
|
+
// (`event:`, `id:`, `retry:`, comments starting with `:`) we ignore.
|
|
131
|
+
const match = /^data: ?(.*)$/.exec(rawLine);
|
|
132
|
+
if (match) dataLines.push(match[1]);
|
|
133
|
+
}
|
|
134
|
+
return dataLines.length > 0 ? dataLines.join("\n") : raw;
|
|
135
|
+
}
|
|
136
|
+
|
|
78
137
|
/**
|
|
79
138
|
* Produce a human-friendly message from a network error.
|
|
80
139
|
*
|
|
@@ -64,9 +64,9 @@ export async function enumerateMemoryItems(opts) {
|
|
|
64
64
|
if (!cwd) continue;
|
|
65
65
|
const agentId = agentIdForClaude(cwd);
|
|
66
66
|
const projMd = path.join(cwd, ".claude", "CLAUDE.md");
|
|
67
|
-
items.push(...readClaudeMd(projMd, agentId, "project"));
|
|
67
|
+
items.push(...readClaudeMd(projMd, agentId, "project", cwd));
|
|
68
68
|
const memDir = path.join(dir, "memory");
|
|
69
|
-
items.push(...readMemoryDir(memDir, agentId, "project"));
|
|
69
|
+
items.push(...readMemoryDir(memDir, agentId, "project", cwd));
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
} else {
|
|
@@ -74,10 +74,10 @@ export async function enumerateMemoryItems(opts) {
|
|
|
74
74
|
if (!cwd) return [];
|
|
75
75
|
const agentId = agentIdForClaude(cwd);
|
|
76
76
|
const projMd = path.join(cwd, ".claude", "CLAUDE.md");
|
|
77
|
-
items.push(...readClaudeMd(projMd, agentId, "project"));
|
|
77
|
+
items.push(...readClaudeMd(projMd, agentId, "project", cwd));
|
|
78
78
|
const slug = slugifyCwd(cwd);
|
|
79
79
|
const memDir = path.join(home, ".claude", "projects", slug, "memory");
|
|
80
|
-
items.push(...readMemoryDir(memDir, agentId, "project"));
|
|
80
|
+
items.push(...readMemoryDir(memDir, agentId, "project", cwd));
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
return items;
|
|
@@ -90,9 +90,13 @@ export async function enumerateMemoryItems(opts) {
|
|
|
90
90
|
* @param {string} filePath
|
|
91
91
|
* @param {string} agentId
|
|
92
92
|
* @param {"user"|"project"} scope
|
|
93
|
+
* @param {string|null} [cwd] - Absolute project path. Attached to metadata
|
|
94
|
+
* so downstream consumers can correlate even after the agent_id is
|
|
95
|
+
* sanitized down to a basename. Null for user-scope items (which use the
|
|
96
|
+
* `meko_agent` common bucket).
|
|
93
97
|
* @returns {MemoryItem[]}
|
|
94
98
|
*/
|
|
95
|
-
export function readClaudeMd(filePath, agentId, scope) {
|
|
99
|
+
export function readClaudeMd(filePath, agentId, scope, cwd = null) {
|
|
96
100
|
let raw;
|
|
97
101
|
try {
|
|
98
102
|
raw = fs.readFileSync(filePath, "utf8");
|
|
@@ -115,6 +119,7 @@ export function readClaudeMd(filePath, agentId, scope) {
|
|
|
115
119
|
source: path.basename(filePath),
|
|
116
120
|
scope,
|
|
117
121
|
sourcePath: filePath,
|
|
122
|
+
...(cwd ? { cwd } : {}),
|
|
118
123
|
...(heading ? { heading } : {}),
|
|
119
124
|
},
|
|
120
125
|
};
|
|
@@ -160,9 +165,10 @@ export function splitTopLevelSections(md) {
|
|
|
160
165
|
* @param {string} memDir
|
|
161
166
|
* @param {string} agentId
|
|
162
167
|
* @param {"user"|"project"} scope
|
|
168
|
+
* @param {string|null} [cwd] - Absolute project path; attached to metadata.
|
|
163
169
|
* @returns {MemoryItem[]}
|
|
164
170
|
*/
|
|
165
|
-
export function readMemoryDir(memDir, agentId, scope) {
|
|
171
|
+
export function readMemoryDir(memDir, agentId, scope, cwd = null) {
|
|
166
172
|
let entries;
|
|
167
173
|
try {
|
|
168
174
|
entries = fs.readdirSync(memDir);
|
|
@@ -193,6 +199,7 @@ export function readMemoryDir(memDir, agentId, scope) {
|
|
|
193
199
|
source: `memory/${entry}`,
|
|
194
200
|
scope,
|
|
195
201
|
sourcePath: full,
|
|
202
|
+
...(cwd ? { cwd } : {}),
|
|
196
203
|
...(frontmatter.name ? { name: frontmatter.name } : {}),
|
|
197
204
|
...(frontmatter.description ? { description: frontmatter.description } : {}),
|
|
198
205
|
...(frontmatter.type ? { type: frontmatter.type } : {}),
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* datapack.mjs — Resolve a migration's destination datapack.
|
|
3
|
+
*
|
|
4
|
+
* The migrator was historically silent about destination: tools were called
|
|
5
|
+
* without `datapack_id`, the server filled in `tenant_ctx.default_datapack_id`,
|
|
6
|
+
* and the user never saw which pack their data actually landed in. With
|
|
7
|
+
* `--datapack <name-or-id>` we want both an explicit override and a printed
|
|
8
|
+
* confirmation of the default-fallback case. This module owns the lookup.
|
|
9
|
+
*
|
|
10
|
+
* Wire model: the MCP server routes by `datapack_id` parameter, not URL host.
|
|
11
|
+
* `datapack_list` returns the user's visible packs; we resolve here, then the
|
|
12
|
+
* orchestrator threads `datapack_id` into every subsequent `tools/call`.
|
|
13
|
+
*/
|
|
14
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
15
|
+
const DEFAULT_DATAPACK_NAME = "meko_default_datapack";
|
|
16
|
+
|
|
17
|
+
export function isUuid(value) {
|
|
18
|
+
return typeof value === "string" && UUID_RE.test(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse a `datapack_list` response body. Mirrors the shape-validation in
|
|
23
|
+
* validate.mjs: accepts a JSON array whose elements look like datapack
|
|
24
|
+
* records (`datapack_id` string present). Returns null on any other shape so
|
|
25
|
+
* callers can produce a precise error rather than a TypeError on .find().
|
|
26
|
+
*
|
|
27
|
+
* Note: response bodies can include connection-string secrets — never log
|
|
28
|
+
* `text` directly; only the resolved `{id, name}` is safe to surface.
|
|
29
|
+
*
|
|
30
|
+
* @param {string|null} text
|
|
31
|
+
* @returns {Array|null}
|
|
32
|
+
*/
|
|
33
|
+
export function parseDatapackList(text) {
|
|
34
|
+
if (!text) return null;
|
|
35
|
+
let body;
|
|
36
|
+
try {
|
|
37
|
+
body = JSON.parse(text);
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (!Array.isArray(body)) return null;
|
|
42
|
+
if (body.length === 0) return body;
|
|
43
|
+
const allLook = body.every(
|
|
44
|
+
(e) => e && typeof e === "object" && typeof e.datapack_id === "string",
|
|
45
|
+
);
|
|
46
|
+
return allLook ? body : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the destination datapack for a migration.
|
|
51
|
+
*
|
|
52
|
+
* @param {object} opts
|
|
53
|
+
* @param {{callTool: Function}} opts.client Connected McpClient.
|
|
54
|
+
* @param {string|null} opts.requested Raw `--datapack` value (UUID or
|
|
55
|
+
* name) or null for default.
|
|
56
|
+
* @returns {Promise<{
|
|
57
|
+
* ok: boolean,
|
|
58
|
+
* id?: string|null,
|
|
59
|
+
* name?: string|null,
|
|
60
|
+
* source?: "default"|"flag-id"|"flag-name"|"server-fallback",
|
|
61
|
+
* defaultId?: string|null,
|
|
62
|
+
* message?: string,
|
|
63
|
+
* }>}
|
|
64
|
+
*
|
|
65
|
+
* `source` describes how the destination was picked:
|
|
66
|
+
* - `default`: requested=null and a `meko_default_datapack` row exists.
|
|
67
|
+
* - `flag-id` / `flag-name`: explicit override resolved.
|
|
68
|
+
* - `server-fallback`: requested=null and no default row found in the list;
|
|
69
|
+
* we let the server's own fallback chain handle it (id stays null).
|
|
70
|
+
*
|
|
71
|
+
* `defaultId` is the id of the `meko_default_datapack` row if visible — surfaced
|
|
72
|
+
* even when `requested` is non-null so the caller can detect "user named the
|
|
73
|
+
* default explicitly" and reuse the same on-disk state file.
|
|
74
|
+
*
|
|
75
|
+
* Failure cases (`ok: false`):
|
|
76
|
+
* - `datapack_list` call failed (auth, network, server error).
|
|
77
|
+
* - response body wasn't a recognizable datapack list.
|
|
78
|
+
* - explicit override didn't match any row.
|
|
79
|
+
*/
|
|
80
|
+
export async function resolveDestinationDatapack(opts) {
|
|
81
|
+
const { client, requested } = opts;
|
|
82
|
+
const res = await client.callTool("datapack_list", {
|
|
83
|
+
scope: "read",
|
|
84
|
+
conversation_id: "00000000-0000-0000-0000-000000000000",
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
message:
|
|
90
|
+
`datapack_list failed: ${res.error?.message ?? "unknown"}. ` +
|
|
91
|
+
`Cannot resolve --datapack.`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const list = parseDatapackList(res.text);
|
|
95
|
+
if (!list) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
message:
|
|
99
|
+
"datapack_list returned an unrecognized response shape. " +
|
|
100
|
+
"Cannot resolve --datapack.",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const defaultRow = list.find((d) => d.datapack_name === DEFAULT_DATAPACK_NAME);
|
|
105
|
+
const defaultId = defaultRow?.datapack_id ?? null;
|
|
106
|
+
|
|
107
|
+
if (!requested) {
|
|
108
|
+
if (defaultRow) {
|
|
109
|
+
return {
|
|
110
|
+
ok: true,
|
|
111
|
+
id: defaultRow.datapack_id,
|
|
112
|
+
name: defaultRow.datapack_name,
|
|
113
|
+
source: "default",
|
|
114
|
+
defaultId,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return { ok: true, id: null, name: null, source: "server-fallback", defaultId };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (isUuid(requested)) {
|
|
121
|
+
const hit = list.find((d) => d.datapack_id?.toLowerCase() === requested.toLowerCase());
|
|
122
|
+
if (!hit) {
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
message:
|
|
126
|
+
`--datapack ${requested}: no datapack with that id is visible to ` +
|
|
127
|
+
`this API key. ${formatAvailable(list)}`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
ok: true,
|
|
132
|
+
id: hit.datapack_id,
|
|
133
|
+
name: hit.datapack_name ?? null,
|
|
134
|
+
source: "flag-id",
|
|
135
|
+
defaultId,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const hit = list.find((d) => d.datapack_name === requested);
|
|
140
|
+
if (!hit) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
message:
|
|
144
|
+
`--datapack ${JSON.stringify(requested)}: no datapack with that name. ` +
|
|
145
|
+
formatAvailable(list),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
ok: true,
|
|
150
|
+
id: hit.datapack_id,
|
|
151
|
+
name: hit.datapack_name,
|
|
152
|
+
source: "flag-name",
|
|
153
|
+
defaultId,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Format a one-line "Available: a, b, c" hint. Names only — never ids,
|
|
159
|
+
* because the underlying response can carry credentials.
|
|
160
|
+
*/
|
|
161
|
+
function formatAvailable(list) {
|
|
162
|
+
const names = list
|
|
163
|
+
.map((d) => d.datapack_name)
|
|
164
|
+
.filter((n) => typeof n === "string" && n.length > 0);
|
|
165
|
+
if (names.length === 0) return "(no datapacks visible to this API key.)";
|
|
166
|
+
return `Available: ${names.join(", ")}.`;
|
|
167
|
+
}
|
package/lib/migrate/id.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
+
import path from "node:path";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Slugify an absolute path the way Claude Code stores project sessions:
|
|
@@ -34,25 +35,102 @@ export function unslugifyCwd(slug) {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
38
|
+
* Server-side fallback for empty/missing agent_id. Anything written without
|
|
39
|
+
* an explicit value lands here. Used as the cross-project common bucket for
|
|
40
|
+
* genuinely cross-project facts (user identity, global preferences) that any
|
|
41
|
+
* agent should see without knowing the writer's project.
|
|
42
|
+
*/
|
|
43
|
+
export const COMMON_BUCKET_AGENT_ID = "meko_agent";
|
|
44
|
+
|
|
45
|
+
const AGENT_ID_MAX_LEN = 64;
|
|
46
|
+
const KNOWN_LOOSE_CLIENTS = new Set(["claude-desktop", "claude_desktop"]);
|
|
47
|
+
const KNOWN_CODING_CLIENTS = new Set(["claude_code", "cursor"]);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Sanitize a project segment: lowercase, collapse non-`[a-z0-9-]` to `-`,
|
|
51
|
+
* strip leading/trailing `-`, cap at 64 chars. Returns "" if the input
|
|
52
|
+
* yields nothing usable (caller falls back to the bare client name).
|
|
53
|
+
*
|
|
54
|
+
* @param {string} segment
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
function sanitizeProjectSegment(segment) {
|
|
58
|
+
if (!segment) return "";
|
|
59
|
+
const lower = String(segment).toLowerCase();
|
|
60
|
+
const collapsed = lower.replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
61
|
+
return collapsed.slice(0, AGENT_ID_MAX_LEN);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Derive an `agent_id` for a Meko write.
|
|
66
|
+
*
|
|
67
|
+
* Contract:
|
|
68
|
+
* - `envOverride` (typically `MEKO_AGENT_ID`) wins if non-empty.
|
|
69
|
+
* - Loose clients (Claude Desktop) → bare `claude_desktop`; no schema.
|
|
70
|
+
* - Coding clients (Claude Code, Cursor) → `<client>:<repo-basename>`.
|
|
71
|
+
* Outside a repo (basename is empty/`.`/`/`) → bare client name to
|
|
72
|
+
* keep coding-agent traffic out of the common bucket.
|
|
73
|
+
* - Anything else → `meko_agent` (the common cross-project bucket).
|
|
74
|
+
*
|
|
75
|
+
* @param {object} opts
|
|
76
|
+
* @param {string} [opts.client] e.g. "claude_code", "cursor", "claude-desktop"
|
|
77
|
+
* @param {string} [opts.cwd] Absolute working directory, for coding clients
|
|
78
|
+
* @param {string} [opts.envOverride] Raw value of `MEKO_AGENT_ID`, if set
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
export function deriveAgentId(opts = {}) {
|
|
82
|
+
const override = (opts.envOverride ?? "").trim();
|
|
83
|
+
if (override) return override;
|
|
84
|
+
|
|
85
|
+
const client = (opts.client ?? "").trim();
|
|
86
|
+
if (KNOWN_LOOSE_CLIENTS.has(client)) return "claude_desktop";
|
|
87
|
+
if (!KNOWN_CODING_CLIENTS.has(client)) return COMMON_BUCKET_AGENT_ID;
|
|
88
|
+
|
|
89
|
+
const rawBase = opts.cwd ? path.basename(opts.cwd) : "";
|
|
90
|
+
if (!rawBase || rawBase === "." || rawBase === "/") return client;
|
|
91
|
+
const project = sanitizeProjectSegment(rawBase);
|
|
92
|
+
if (!project) return client;
|
|
93
|
+
return `${client}:${project}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Back-compat alias. New code should call `deriveAgentId(...)`.
|
|
98
|
+
*/
|
|
99
|
+
export const AGENT_ID = COMMON_BUCKET_AGENT_ID;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {string} [cwd] Absolute project cwd. Required for project-scope
|
|
103
|
+
* writes; omit for user-scope writes (returns the common bucket).
|
|
104
|
+
* @returns {string}
|
|
39
105
|
*/
|
|
40
106
|
export function agentIdForClaude(cwd) {
|
|
41
|
-
|
|
107
|
+
if (!cwd) return COMMON_BUCKET_AGENT_ID;
|
|
108
|
+
return deriveAgentId({
|
|
109
|
+
client: "claude_code",
|
|
110
|
+
cwd,
|
|
111
|
+
envOverride: process.env.MEKO_AGENT_ID,
|
|
112
|
+
});
|
|
42
113
|
}
|
|
43
114
|
|
|
44
115
|
/**
|
|
45
|
-
* User-scope agent id for
|
|
46
|
-
*
|
|
116
|
+
* User-scope agent id for content like `~/.claude/CLAUDE.md` that is genuinely
|
|
117
|
+
* cross-project. Goes into the `meko_agent` common bucket so any agent can
|
|
118
|
+
* read it regardless of the writer's project.
|
|
47
119
|
*/
|
|
48
|
-
export const CLAUDE_CODE_USER_AGENT_ID =
|
|
120
|
+
export const CLAUDE_CODE_USER_AGENT_ID = COMMON_BUCKET_AGENT_ID;
|
|
49
121
|
|
|
50
122
|
/**
|
|
51
|
-
* @param {string}
|
|
123
|
+
* @param {string} [cwd] Absolute project cwd (or basename — Cursor's projects
|
|
124
|
+
* live under a flat `~/.cursor/projects/<name>/`). Omit for user-scope.
|
|
52
125
|
* @returns {string}
|
|
53
126
|
*/
|
|
54
|
-
export function agentIdForCursor(
|
|
55
|
-
return
|
|
127
|
+
export function agentIdForCursor(cwd) {
|
|
128
|
+
if (!cwd) return COMMON_BUCKET_AGENT_ID;
|
|
129
|
+
return deriveAgentId({
|
|
130
|
+
client: "cursor",
|
|
131
|
+
cwd,
|
|
132
|
+
envOverride: process.env.MEKO_AGENT_ID,
|
|
133
|
+
});
|
|
56
134
|
}
|
|
57
135
|
|
|
58
136
|
/**
|
|
@@ -81,3 +159,43 @@ export function seedFor(agentId, sessionUuid, messageUuid) {
|
|
|
81
159
|
export function memoryHash(text, name = "") {
|
|
82
160
|
return createHash("sha256").update(`${name}\0${text}`).digest("hex");
|
|
83
161
|
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Derive a stable, filesystem-safe key for the destination datapack so the
|
|
165
|
+
* migrator can scope its resume state per datapack.
|
|
166
|
+
*
|
|
167
|
+
* Strategy:
|
|
168
|
+
* - For the canonical cloud URL shape `https://<uuid>.mcp.mekodev.com/mcp`,
|
|
169
|
+
* the leading host label is a UUID — use it verbatim. This keeps the
|
|
170
|
+
* on-disk file name recognizable at a glance (it matches the datapack
|
|
171
|
+
* UUID you'd see in the cloud UI).
|
|
172
|
+
* - For anything else (localhost stacks, custom deployments), fall back
|
|
173
|
+
* to `sha256(normalizedUrl).slice(0, 16)` — still stable, still short,
|
|
174
|
+
* but unambiguously derived from the URL. We strip trailing slashes
|
|
175
|
+
* before hashing so `http://localhost:8000/mcp` and
|
|
176
|
+
* `http://localhost:8000/mcp/` resolve to the same key; otherwise a
|
|
177
|
+
* user copy-pasting the endpoint with or without a trailing slash
|
|
178
|
+
* would land on different state files and break resume.
|
|
179
|
+
*
|
|
180
|
+
* A bare `null`/empty string yields `"unknown"` so callers don't need a
|
|
181
|
+
* null-check just to pick a path. Callers that truly want to error on a
|
|
182
|
+
* missing URL should do so upstream.
|
|
183
|
+
*
|
|
184
|
+
* @param {string|null|undefined} url
|
|
185
|
+
* @returns {string}
|
|
186
|
+
*/
|
|
187
|
+
export function datapackKeyFromUrl(url) {
|
|
188
|
+
if (!url) return "unknown";
|
|
189
|
+
const trimmed = String(url).trim();
|
|
190
|
+
if (!trimmed) return "unknown";
|
|
191
|
+
// UUID host label, e.g. https://352ded8e-...mcp.mekodev.com/mcp
|
|
192
|
+
const m = trimmed.match(
|
|
193
|
+
/^https?:\/\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\./i,
|
|
194
|
+
);
|
|
195
|
+
if (m) return m[1].toLowerCase();
|
|
196
|
+
// Normalize trailing slashes so `.../mcp` and `.../mcp/` (and `.../mcp//`)
|
|
197
|
+
// all hash to the same key. We keep everything else as-is — paths are
|
|
198
|
+
// case-sensitive on most targets, so we deliberately don't touch case.
|
|
199
|
+
const normalized = trimmed.replace(/\/+$/, "");
|
|
200
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
201
|
+
}
|