@yugabytedb/meko-mcp 2.2.0 → 2.5.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 (67) hide show
  1. package/README.md +168 -61
  2. package/assets/hooks-handlers/lib/capture-cleanup-cli.js +2134 -0
  3. package/assets/hooks-handlers/lib/capture-notices.js +224 -14
  4. package/assets/hooks-handlers/lib/capture-state.js +206 -1
  5. package/assets/hooks-handlers/lib/capture-status-cli.js +6 -2
  6. package/assets/hooks-handlers/lib/capture.js +1364 -139
  7. package/assets/hooks-handlers/lib/checkpoint-timer.js +445 -18
  8. package/assets/hooks-handlers/lib/journal-bridge.js +218 -0
  9. package/assets/hooks-handlers/lib/kiro-handler.js +26 -4
  10. package/assets/hooks-handlers/lib/opencode-handler.js +643 -0
  11. package/assets/hooks-handlers/lib/opencode-normalizer.js +145 -0
  12. package/assets/hooks-handlers/lib/terminal-handoff.js +10 -1
  13. package/assets/opencode-plugin/meko.js +213 -0
  14. package/assets/scripts/meko-statusline.sh +102 -19
  15. package/assets/skills/meko-mcp-tools/SKILL.md +59 -16
  16. package/assets/skills/meko-mcp-tools/references/_sections.md +2 -2
  17. package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +15 -7
  18. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +275 -19
  19. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +7 -4
  20. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +39 -11
  21. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +10 -6
  22. package/assets/skills/meko-mcp-tools/references/tools-overview.md +44 -29
  23. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +79 -14
  24. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +47 -1
  25. package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +6 -39
  26. package/assets/skills/meko-mcp-tools-desktop/SKILL.md +57 -21
  27. package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +4 -4
  28. package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
  29. package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +8 -6
  30. package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +284 -38
  31. package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +32 -10
  32. package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +16 -12
  33. package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +46 -31
  34. package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +79 -14
  35. package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +69 -1
  36. package/assets/skills/meko-select-datapack-desktop/SKILL.md +93 -14
  37. package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +44 -12
  38. package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +14 -10
  39. package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +17 -5
  40. package/bin/create-meko-setup.mjs +186 -30
  41. package/lib/autofix.mjs +100 -0
  42. package/lib/browser-login.mjs +495 -0
  43. package/lib/capture-cleanup.mjs +63 -0
  44. package/lib/clients/claude-code.mjs +1 -0
  45. package/lib/clients/codex.mjs +80 -35
  46. package/lib/clients/common.mjs +7 -0
  47. package/lib/clients/index.mjs +2 -0
  48. package/lib/clients/kiro.mjs +5 -5
  49. package/lib/clients/opencode.mjs +537 -0
  50. package/lib/config.mjs +68 -1
  51. package/lib/installer.mjs +18 -2
  52. package/lib/migrate/adapters/claude-code.mjs +11 -2
  53. package/lib/migrate/adapters/codex.mjs +9 -2
  54. package/lib/migrate/adapters/memory-md.mjs +27 -8
  55. package/lib/migrate/adapters/opencode.mjs +148 -0
  56. package/lib/migrate/claude-parser.mjs +153 -9
  57. package/lib/migrate/datapack.mjs +254 -16
  58. package/lib/migrate/id.mjs +195 -13
  59. package/lib/migrate/mcp-client.mjs +59 -1
  60. package/lib/migrate/opencode-normalizer.mjs +132 -0
  61. package/lib/migrate/orchestrator.mjs +294 -78
  62. package/lib/migrate/progress.mjs +239 -5
  63. package/lib/migrate/registry.mjs +72 -0
  64. package/lib/migrate.mjs +97 -9
  65. package/lib/validate.mjs +111 -22
  66. package/package.json +5 -5
  67. package/scripts/bundle-assets.mjs +4 -0
@@ -0,0 +1,100 @@
1
+ /**
2
+ * autofix.mjs: the installer side of automatic capture repair.
3
+ *
4
+ * The checkpoint timer runs `--capture-cleanup fix --auto` every
5
+ * MEKO_AUTOFIX_INTERVAL seconds. The on/off choice lives in
6
+ * autofix-config.json in the capture directory, which capture-state.js
7
+ * autofixSetting() reads. MEKO_AUTOFIX in the hook environment overrides it.
8
+ *
9
+ * Most clients share one capture directory, but OpenCode hooks keep the one
10
+ * saved in their hook env file at first install, so the file is written to
11
+ * every capture directory the installer knows about.
12
+ */
13
+
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { dirname, isAbsolute, join, resolve } from "node:path";
16
+
17
+ import { getCaptureRoot, getOpenCodeHookEnvPath } from "./config.mjs";
18
+
19
+ const CONFIG_FILE = "autofix-config.json";
20
+
21
+ function savedOpenCodeRoot(scope, projectRoot) {
22
+ try {
23
+ const file = getOpenCodeHookEnvPath(scope, projectRoot);
24
+ if (!existsSync(file)) return null;
25
+ const value = JSON.parse(readFileSync(file, "utf8")).MEKO_WATERMARK_DIR;
26
+ if (typeof value !== "string" || !value) return null;
27
+ return isAbsolute(value) ? value : resolve(value);
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /** Every capture directory a client's timer may read the setting from. */
34
+ export function captureRoots({ projectRoot = process.cwd() } = {}) {
35
+ const roots = [getCaptureRoot()];
36
+ for (const scope of ["user", "project"]) {
37
+ const saved = savedOpenCodeRoot(scope, projectRoot);
38
+ if (saved && !roots.includes(saved)) roots.push(saved);
39
+ }
40
+ return roots;
41
+ }
42
+
43
+ export function autofixConfigPath() {
44
+ return join(getCaptureRoot(), CONFIG_FILE);
45
+ }
46
+
47
+ /** @returns {boolean|null} the saved choice in the main capture directory, or null. */
48
+ export function readAutofixSetting() {
49
+ try {
50
+ const cfg = JSON.parse(readFileSync(autofixConfigPath(), "utf8"));
51
+ return typeof cfg.enabled === "boolean" ? cfg.enabled : null;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Save --autofix / --no-autofix in every capture directory. `enable`
59
+ * undefined keeps an earlier choice, so a plain reinstall doesn't turn
60
+ * repair back on.
61
+ *
62
+ * @param {{ enable?: boolean, dryRun?: boolean, projectRoot?: string }} opts
63
+ * @returns {{ enabled: boolean, changed: boolean, paths: string[] }}
64
+ */
65
+ export function applyAutofixSetting({ enable, dryRun = false, projectRoot } = {}) {
66
+ const saved = readAutofixSetting();
67
+ const paths = captureRoots({ projectRoot }).map((root) => join(root, CONFIG_FILE));
68
+ if (enable === undefined) {
69
+ return { enabled: saved === null ? true : saved, changed: false, paths };
70
+ }
71
+ if (!dryRun) {
72
+ for (const file of paths) {
73
+ mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
74
+ writeFileSync(
75
+ file,
76
+ `${JSON.stringify({ enabled: enable, updated_at: new Date().toISOString() })}\n`,
77
+ { mode: 0o600 },
78
+ );
79
+ }
80
+ }
81
+ return { enabled: enable, changed: saved !== enable, paths };
82
+ }
83
+
84
+ /** Same rule as capture-state.js autofixIntervalSeconds(). */
85
+ function intervalSeconds(env) {
86
+ const n = Number.parseInt(env.MEKO_AUTOFIX_INTERVAL || "", 10);
87
+ return Math.max(60, Number.isFinite(n) ? n : 1800);
88
+ }
89
+
90
+ /** One line for the end of an install. */
91
+ export function describeAutofix(enabled, env = process.env) {
92
+ const envOverride = env.MEKO_AUTOFIX;
93
+ if (envOverride != null && envOverride !== "") {
94
+ return `Automatic capture repair follows MEKO_AUTOFIX=${envOverride} in this environment.`;
95
+ }
96
+ const minutes = Math.max(1, Math.round(intervalSeconds(env) / 60));
97
+ return enabled
98
+ ? `Automatic capture repair is on: every ${minutes} minute${minutes === 1 ? "" : "s"} the capture timer fixes stuck sessions (--capture-cleanup fix). Turn it off with --no-autofix.`
99
+ : "Automatic capture repair is off. Turn it on with --autofix, or run --capture-cleanup fix yourself.";
100
+ }
@@ -0,0 +1,495 @@
1
+ /**
2
+ * Browser sign-in for Cloud Meko: OAuth once, then trade the session for an
3
+ * API key.
4
+ *
5
+ * The flow is the MCP authorization spec, ported from
6
+ * yugabyte/meko-cli lib/oauth.mjs:
7
+ *
8
+ * 1. Probe the MCP endpoint unauthenticated and read WWW-Authenticate.
9
+ * 2. Discover protected-resource and authorization-server metadata.
10
+ * 3. Register a public client (DCR) with a loopback redirect_uri.
11
+ * 4. Open the browser on /authorize with PKCE (S256) and wait for the
12
+ * callback on 127.0.0.1.
13
+ * 5. Exchange the code for an access token.
14
+ * 6. POST it to <server>/api-keys, which creates an `mko_tkn_` key for the
15
+ * signed-in user.
16
+ *
17
+ * Only the API key leaves this module. The OAuth tokens are discarded: the
18
+ * client configs and capture hooks all authenticate with the key, so nothing
19
+ * here needs refreshing.
20
+ */
21
+
22
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
23
+ import http from "node:http";
24
+ import os from "node:os";
25
+ import { spawn } from "node:child_process";
26
+
27
+ import { MEKO_PRODUCTION_CONSOLE_URL } from "./clients/common.mjs";
28
+
29
+ const PROTOCOL_VERSION = "2025-03-26";
30
+ const USER_AGENT = "meko-installer/1.0 (+https://github.com/yugabyte/meko-mcp-server)";
31
+ export const DEFAULT_LOGIN_TIMEOUT_MS = 5 * 60_000;
32
+ const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
33
+ const MAX_KEY_NAME_LEN = 100;
34
+
35
+ export class BrowserLoginError extends Error {
36
+ constructor(message, { code = "login_failed", status = null } = {}) {
37
+ super(message);
38
+ this.name = "BrowserLoginError";
39
+ this.code = code;
40
+ this.status = status;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Sign in through the browser and return a freshly created API key.
46
+ *
47
+ * @param {object} params
48
+ * @param {string} params.url MCP endpoint, e.g. https://mcp.mekodata.ai/mcp
49
+ * @param {string} [params.keyName] Label for the new key (default: installer + hostname + time)
50
+ * @param {boolean} [params.noBrowser] Print the URL instead of opening a browser
51
+ * @param {number} [params.loginTimeoutMs]
52
+ * @param {(msg: string) => void} [params.notify]
53
+ * @param {typeof fetch} [params.fetchImpl]
54
+ * @param {(url: string) => Promise<boolean>} [params.openBrowser]
55
+ * @returns {Promise<{apiKey: string, id: string|null, name: string, expiresAt: string|null}>}
56
+ */
57
+ export async function loginAndCreateApiKey({
58
+ url,
59
+ keyName = defaultKeyName(),
60
+ noBrowser = false,
61
+ loginTimeoutMs = DEFAULT_LOGIN_TIMEOUT_MS,
62
+ notify = () => {},
63
+ fetchImpl = globalThis.fetch,
64
+ openBrowser = openDefaultBrowser,
65
+ } = {}) {
66
+ const accessToken = await loginOAuth({
67
+ url,
68
+ noBrowser,
69
+ loginTimeoutMs,
70
+ notify,
71
+ fetchImpl,
72
+ openBrowser,
73
+ });
74
+ return createApiKey({ url, accessToken, keyName, fetchImpl });
75
+ }
76
+
77
+ export async function loginOAuth({
78
+ url,
79
+ noBrowser = false,
80
+ loginTimeoutMs = DEFAULT_LOGIN_TIMEOUT_MS,
81
+ notify = () => {},
82
+ fetchImpl = globalThis.fetch,
83
+ openBrowser = openDefaultBrowser,
84
+ }) {
85
+ const callback = await createCallbackServer();
86
+ try {
87
+ const discovery = await discoverOAuth(url, fetchImpl);
88
+ const client = await registerClient(discovery, callback.redirectUri, fetchImpl);
89
+ const state = base64url(randomBytes(32));
90
+ const verifier = base64url(randomBytes(48));
91
+ const challenge = base64url(createHash("sha256").update(verifier).digest());
92
+ const authorizationUrl = buildAuthorizationUrl({
93
+ discovery,
94
+ client,
95
+ redirectUri: callback.redirectUri,
96
+ state,
97
+ challenge,
98
+ });
99
+
100
+ if (noBrowser) {
101
+ notify(`Open this URL in a browser to sign in:\n ${authorizationUrl}`);
102
+ } else {
103
+ const opened = await openBrowser(authorizationUrl);
104
+ notify(
105
+ opened
106
+ ? `Opened your browser to sign in. If it didn't open, use this URL:\n ${authorizationUrl}`
107
+ : `Could not open a browser. Open this URL to sign in:\n ${authorizationUrl}`,
108
+ );
109
+ }
110
+
111
+ const returned = await callback.wait(loginTimeoutMs);
112
+ if (returned.error) {
113
+ throw new BrowserLoginError(
114
+ `Sign-in failed: ${returned.error_description ?? returned.error}`,
115
+ { code: "authorization_denied" },
116
+ );
117
+ }
118
+ if (!safeEqual(returned.state, state)) {
119
+ throw new BrowserLoginError("Sign-in callback state did not match. Run the installer again.");
120
+ }
121
+ if (!returned.code) {
122
+ throw new BrowserLoginError("Sign-in callback did not include an authorization code.");
123
+ }
124
+
125
+ const tokens = await exchangeAuthorizationCode({
126
+ discovery,
127
+ client,
128
+ redirectUri: callback.redirectUri,
129
+ code: returned.code,
130
+ verifier,
131
+ fetchImpl,
132
+ });
133
+ return tokens.access_token;
134
+ } finally {
135
+ await callback.close();
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Trade an OAuth access token for an `mko_tkn_` key via POST <server>/api-keys.
141
+ */
142
+ export async function createApiKey({ url, accessToken, keyName = defaultKeyName(), fetchImpl = globalThis.fetch }) {
143
+ const endpoint = apiKeysEndpoint(url);
144
+ const response = await timedFetch(fetchImpl, endpoint, {
145
+ method: "POST",
146
+ headers: {
147
+ Authorization: `Bearer ${accessToken}`,
148
+ "Content-Type": "application/json",
149
+ Accept: "application/json",
150
+ "User-Agent": USER_AGENT,
151
+ },
152
+ body: JSON.stringify({ name: keyName }),
153
+ });
154
+ const body = await response.json().catch(() => null);
155
+ const console = consoleUrlFor(url);
156
+
157
+ if (response.status === 404 || response.status === 405) {
158
+ throw new BrowserLoginError(
159
+ "This Meko server does not support browser sign-in yet. " +
160
+ `Create an API key at ${console} > Settings > API Keys, then rerun with --api-key.`,
161
+ { code: "unsupported", status: response.status },
162
+ );
163
+ }
164
+ if (response.status === 429) {
165
+ throw new BrowserLoginError(
166
+ "You already have the maximum number of active API keys. " +
167
+ `Revoke one at ${console} > Settings > API Keys, then run the installer again.`,
168
+ { code: "token_cap_reached", status: 429 },
169
+ );
170
+ }
171
+ if (response.status === 403 && body?.error?.code === "account_not_provisioned") {
172
+ // The browser signed in as a Cognito identity with no Meko account, e.g.
173
+ // Google in the browser but email and password in the console. The
174
+ // server refuses rather than minting a key for a new, empty account.
175
+ throw new BrowserLoginError(
176
+ `You signed in as a user that has no Meko account. Sign in at ${console} with the ` +
177
+ "method you normally use (for example Google, or email and password), then run the " +
178
+ "installer again and pick that same method. If the browser chose an account " +
179
+ `automatically, sign out at ${console} first. Or create a key at ${console} > ` +
180
+ "Settings > API Keys and rerun with --api-key.",
181
+ { code: "account_not_provisioned", status: 403 },
182
+ );
183
+ }
184
+ if (!response.ok || typeof body?.api_key !== "string" || !body.api_key.startsWith("mko_tkn_")) {
185
+ const detail = body?.error?.message ? `: ${body.error.message}` : "";
186
+ throw new BrowserLoginError(
187
+ `Could not create an API key (HTTP ${response.status})${detail}`,
188
+ { code: body?.error?.code ?? "create_failed", status: response.status },
189
+ );
190
+ }
191
+ return {
192
+ apiKey: body.api_key,
193
+ id: body.id ?? null,
194
+ name: body.name ?? keyName,
195
+ expiresAt: body.expires_at ?? null,
196
+ };
197
+ }
198
+
199
+ /** `https://mcp.mekodata.ai/mcp` → `https://mcp.mekodata.ai/api-keys`. */
200
+ export function apiKeysEndpoint(mcpUrl) {
201
+ const parsed = new URL(mcpUrl);
202
+ const base = parsed.pathname.replace(/\/+$/, "").replace(/\/mcp$/, "");
203
+ return `${parsed.origin}${base}/api-keys`;
204
+ }
205
+
206
+ /** Console that issues keys for this MCP host. Dev keys only work on dev. */
207
+ export function consoleUrlFor(mcpUrl) {
208
+ try {
209
+ const host = new URL(mcpUrl).hostname;
210
+ if (host === "mekodev.com" || host.endsWith(".mekodev.com")) return "https://app.mekodev.com";
211
+ } catch {
212
+ // fall through
213
+ }
214
+ return MEKO_PRODUCTION_CONSOLE_URL;
215
+ }
216
+
217
+ export function defaultKeyName({ hostname = os.hostname(), now = new Date() } = {}) {
218
+ const stamp = now.toISOString().replace(/\.\d{3}Z$/, "Z");
219
+ const host = String(hostname || "unknown-host").replace(/\.local$/, "");
220
+ const name = `Meko installer (${host}) ${stamp}`;
221
+ if (name.length <= MAX_KEY_NAME_LEN) return name;
222
+ const room = MAX_KEY_NAME_LEN - `Meko installer () ${stamp}`.length;
223
+ return `Meko installer (${host.slice(0, Math.max(room, 0))}) ${stamp}`;
224
+ }
225
+
226
+ // ---------------------------------------------------------------------------
227
+ // OAuth plumbing
228
+ // ---------------------------------------------------------------------------
229
+
230
+ export async function discoverOAuth(serverUrl, fetchImpl = globalThis.fetch) {
231
+ const challenge = await probeChallenge(serverUrl, fetchImpl);
232
+ const resourceMetadata = await firstJson(
233
+ protectedResourceUrls(serverUrl, challenge.resourceMetadata),
234
+ fetchImpl,
235
+ );
236
+ const authorizationServer = resourceMetadata?.authorization_servers?.[0] ?? new URL(serverUrl).origin;
237
+ const metadata = await firstJson(authorizationMetadataUrls(authorizationServer, serverUrl), fetchImpl);
238
+ if (!metadata?.authorization_endpoint || !metadata?.token_endpoint) {
239
+ throw new BrowserLoginError(
240
+ "This Meko endpoint does not advertise OAuth sign-in. Use --api-key instead.",
241
+ { code: "unsupported" },
242
+ );
243
+ }
244
+ return {
245
+ authorization_endpoint: metadata.authorization_endpoint,
246
+ token_endpoint: metadata.token_endpoint,
247
+ registration_endpoint: metadata.registration_endpoint ?? new URL("/register", authorizationServer).toString(),
248
+ scope: challenge.scope
249
+ ?? resourceMetadata?.scopes_supported?.join(" ")
250
+ ?? metadata.scopes_supported?.join(" ")
251
+ ?? null,
252
+ resource: resourceMetadata?.resource ?? null,
253
+ };
254
+ }
255
+
256
+ async function probeChallenge(serverUrl, fetchImpl) {
257
+ const response = await timedFetch(fetchImpl, serverUrl, {
258
+ method: "POST",
259
+ headers: {
260
+ "Content-Type": "application/json",
261
+ Accept: "application/json, text/event-stream",
262
+ "MCP-Protocol-Version": PROTOCOL_VERSION,
263
+ "User-Agent": USER_AGENT,
264
+ },
265
+ body: JSON.stringify({
266
+ jsonrpc: "2.0",
267
+ id: "oauth-discovery",
268
+ method: "initialize",
269
+ params: {
270
+ protocolVersion: PROTOCOL_VERSION,
271
+ capabilities: {},
272
+ clientInfo: { name: "meko-installer", version: "1.0.0" },
273
+ },
274
+ }),
275
+ });
276
+ const header = response.headers.get("www-authenticate") ?? "";
277
+ return {
278
+ resourceMetadata: challengeField(header, "resource_metadata"),
279
+ scope: challengeField(header, "scope"),
280
+ };
281
+ }
282
+
283
+ async function registerClient(discovery, redirectUri, fetchImpl) {
284
+ const response = await timedFetch(fetchImpl, discovery.registration_endpoint, {
285
+ method: "POST",
286
+ headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
287
+ body: JSON.stringify({
288
+ redirect_uris: [redirectUri],
289
+ token_endpoint_auth_method: "none",
290
+ grant_types: ["authorization_code", "refresh_token"],
291
+ response_types: ["code"],
292
+ client_name: "Meko installer",
293
+ software_id: "meko-installer",
294
+ software_version: "1.0.0",
295
+ ...(discovery.scope ? { scope: discovery.scope } : {}),
296
+ }),
297
+ });
298
+ const body = await response.json().catch(() => null);
299
+ if (!response.ok || !body?.client_id) {
300
+ throw new BrowserLoginError(
301
+ `OAuth client registration failed (HTTP ${response.status}). Use --api-key instead.`,
302
+ { code: "registration_failed", status: response.status },
303
+ );
304
+ }
305
+ return body;
306
+ }
307
+
308
+ function buildAuthorizationUrl({ discovery, client, redirectUri, state, challenge }) {
309
+ const url = new URL(discovery.authorization_endpoint);
310
+ url.search = new URLSearchParams({
311
+ response_type: "code",
312
+ client_id: client.client_id,
313
+ redirect_uri: redirectUri,
314
+ state,
315
+ code_challenge: challenge,
316
+ code_challenge_method: "S256",
317
+ ...(discovery.scope ? { scope: discovery.scope } : {}),
318
+ ...(discovery.resource ? { resource: discovery.resource } : {}),
319
+ }).toString();
320
+ return url.toString();
321
+ }
322
+
323
+ async function exchangeAuthorizationCode({ discovery, client, redirectUri, code, verifier, fetchImpl }) {
324
+ const form = new URLSearchParams({
325
+ grant_type: "authorization_code",
326
+ code,
327
+ redirect_uri: redirectUri,
328
+ client_id: client.client_id,
329
+ code_verifier: verifier,
330
+ ...(discovery.resource ? { resource: discovery.resource } : {}),
331
+ });
332
+ const headers = { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": USER_AGENT };
333
+ const method = client.token_endpoint_auth_method ?? "none";
334
+ if (method === "client_secret_basic") {
335
+ const id = encodeURIComponent(client.client_id);
336
+ const secret = encodeURIComponent(client.client_secret ?? "");
337
+ headers.Authorization = `Basic ${Buffer.from(`${id}:${secret}`).toString("base64")}`;
338
+ } else if (method === "client_secret_post" && client.client_secret) {
339
+ form.set("client_secret", client.client_secret);
340
+ }
341
+ const response = await timedFetch(fetchImpl, discovery.token_endpoint, { method: "POST", headers, body: form });
342
+ const body = await response.json().catch(() => null);
343
+ if (!response.ok || !body?.access_token) {
344
+ throw new BrowserLoginError(
345
+ `OAuth token exchange failed (HTTP ${response.status}). Run the installer again.`,
346
+ { code: "token_exchange_failed", status: response.status },
347
+ );
348
+ }
349
+ return body;
350
+ }
351
+
352
+ const CALLBACK_PAGE =
353
+ "<!doctype html><meta charset=utf-8><title>Meko sign-in</title>" +
354
+ "<body style=\"font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto\">" +
355
+ "<h1>Signed in to Meko</h1><p>You can close this window and return to the terminal.</p></body>";
356
+
357
+ async function createCallbackServer() {
358
+ let settle;
359
+ let fail;
360
+ const result = new Promise((resolve, reject) => {
361
+ settle = resolve;
362
+ fail = reject;
363
+ });
364
+ const server = http.createServer((req, res) => {
365
+ const requestUrl = new URL(req.url, "http://localhost");
366
+ if (requestUrl.pathname !== "/callback") {
367
+ res.writeHead(404, { "Content-Type": "text/plain" });
368
+ res.end("Not found");
369
+ return;
370
+ }
371
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
372
+ res.end(CALLBACK_PAGE);
373
+ settle(Object.fromEntries(requestUrl.searchParams));
374
+ });
375
+ await new Promise((resolve, reject) => {
376
+ server.once("error", reject);
377
+ server.listen(0, "127.0.0.1", resolve);
378
+ });
379
+ server.on("error", (err) => fail(err));
380
+ const { port } = server.address();
381
+ return {
382
+ redirectUri: `http://localhost:${port}/callback`,
383
+ wait: (timeoutMs) => {
384
+ const timer = setTimeout(
385
+ () =>
386
+ fail(
387
+ new BrowserLoginError(
388
+ `Timed out after ${Math.round(timeoutMs / 1000)}s waiting for browser sign-in. ` +
389
+ "Run the installer again, or use --api-key.",
390
+ { code: "timeout" },
391
+ ),
392
+ ),
393
+ timeoutMs,
394
+ );
395
+ timer.unref?.();
396
+ return result.finally(() => clearTimeout(timer));
397
+ },
398
+ close: () =>
399
+ new Promise((resolve) => {
400
+ server.closeAllConnections?.();
401
+ server.close(() => resolve());
402
+ }),
403
+ };
404
+ }
405
+
406
+ export async function openDefaultBrowser(url, { platform = process.platform, spawnImpl = spawn } = {}) {
407
+ const command = platform === "darwin" ? "open" : platform === "win32" ? "rundll32" : "xdg-open";
408
+ const args = platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
409
+ try {
410
+ const child = spawnImpl(command, args, { detached: true, stdio: "ignore" });
411
+ return await new Promise((resolve) => {
412
+ child.once("error", () => resolve(false));
413
+ child.once("spawn", () => {
414
+ child.unref();
415
+ resolve(true);
416
+ });
417
+ });
418
+ } catch {
419
+ return false;
420
+ }
421
+ }
422
+
423
+ function protectedResourceUrls(serverUrl, advertised) {
424
+ const parsed = new URL(serverUrl);
425
+ const urls = [];
426
+ if (advertised) urls.push(advertised);
427
+ if (parsed.pathname && parsed.pathname !== "/") {
428
+ urls.push(`${parsed.origin}/.well-known/oauth-protected-resource${parsed.pathname}`);
429
+ }
430
+ urls.push(`${parsed.origin}/.well-known/oauth-protected-resource`);
431
+ return [...new Set(urls)];
432
+ }
433
+
434
+ function authorizationMetadataUrls(authServer, serverUrl) {
435
+ const parsed = new URL(authServer || serverUrl);
436
+ if (parsed.pathname && parsed.pathname !== "/") {
437
+ const path = parsed.pathname.replace(/\/$/, "");
438
+ return [
439
+ `${parsed.origin}/.well-known/oauth-authorization-server${path}`,
440
+ `${parsed.origin}/.well-known/openid-configuration${path}`,
441
+ `${parsed.origin}${path}/.well-known/openid-configuration`,
442
+ ];
443
+ }
444
+ return [
445
+ `${parsed.origin}/.well-known/oauth-authorization-server`,
446
+ `${parsed.origin}/.well-known/openid-configuration`,
447
+ ];
448
+ }
449
+
450
+ async function firstJson(urls, fetchImpl) {
451
+ for (const url of urls) {
452
+ const response = await timedFetch(fetchImpl, url, {
453
+ headers: { "MCP-Protocol-Version": PROTOCOL_VERSION, "User-Agent": USER_AGENT },
454
+ });
455
+ if (!response.ok) continue;
456
+ try {
457
+ return await response.json();
458
+ } catch {
459
+ continue;
460
+ }
461
+ }
462
+ return null;
463
+ }
464
+
465
+ async function timedFetch(fetchImpl, url, options) {
466
+ const controller = new AbortController();
467
+ const timer = setTimeout(() => controller.abort(), DEFAULT_REQUEST_TIMEOUT_MS);
468
+ timer.unref?.();
469
+ try {
470
+ return await fetchImpl(url, { ...options, signal: controller.signal });
471
+ } catch (err) {
472
+ if (err?.name === "AbortError") {
473
+ throw new BrowserLoginError(`Request timed out: ${url}`, { code: "network" });
474
+ }
475
+ throw new BrowserLoginError(`Request failed: ${err?.message ?? String(err)}`, { code: "network" });
476
+ } finally {
477
+ clearTimeout(timer);
478
+ }
479
+ }
480
+
481
+ function challengeField(header, field) {
482
+ const match = new RegExp(`${field}=(?:"([^"]+)"|([^\\s,]+))`, "i").exec(header);
483
+ return match?.[1] ?? match?.[2] ?? null;
484
+ }
485
+
486
+ function safeEqual(left, right) {
487
+ if (typeof left !== "string" || typeof right !== "string") return false;
488
+ const a = Buffer.from(left);
489
+ const b = Buffer.from(right);
490
+ return a.length === b.length && timingSafeEqual(a, b);
491
+ }
492
+
493
+ function base64url(value) {
494
+ return Buffer.from(value).toString("base64url");
495
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * capture-cleanup.mjs: installer helper that invokes capture-cleanup-cli.js.
3
+ *
4
+ * Resolves the CLI the same way capture-status.mjs does, so a published
5
+ * package runs the bundled copy next to the capture.js it drains with.
6
+ */
7
+
8
+ import { spawnSync } from "node:child_process";
9
+ import { existsSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+
12
+ import { resolveCaptureStatusCli } from "./capture-status.mjs";
13
+
14
+ /**
15
+ * Absolute path to capture-cleanup-cli.js under the active asset root.
16
+ * @returns {string}
17
+ */
18
+ export function resolveCaptureCleanupCli() {
19
+ return join(dirname(resolveCaptureStatusCli()), "capture-cleanup-cli.js");
20
+ }
21
+
22
+ /**
23
+ * Build the CLI argument list from parsed installer flags.
24
+ *
25
+ * @param {string} action
26
+ * @param {{ session?: string[], all?: boolean, 'dry-run'?: boolean,
27
+ * json?: boolean, yes?: boolean, 'max-exchanges'?: string,
28
+ * 'pace-ms'?: string }} flags
29
+ * @returns {string[]}
30
+ */
31
+ export function captureCleanupArgs(action, flags = {}) {
32
+ const args = [action];
33
+ for (const id of flags.session || []) args.push("--session", id);
34
+ if (flags.all) args.push("--all");
35
+ if (flags["dry-run"]) args.push("--dry-run");
36
+ if (flags.json) args.push("--json");
37
+ if (flags.yes) args.push("--yes");
38
+ if (flags["max-exchanges"] != null) args.push("--max-exchanges", String(flags["max-exchanges"]));
39
+ if (flags["pace-ms"] != null) args.push("--pace-ms", String(flags["pace-ms"]));
40
+ return args;
41
+ }
42
+
43
+ /**
44
+ * Run the capture-cleanup CLI with inherited stdio (drain prints progress)
45
+ * and return its exit code.
46
+ *
47
+ * @param {string[]} args
48
+ * @param {{ env?: NodeJS.ProcessEnv }} [opts]
49
+ * @returns {{ code: number, cliPath: string, error?: string }}
50
+ */
51
+ export function runCaptureCleanup(args, opts = {}) {
52
+ const cliPath = resolveCaptureCleanupCli();
53
+ if (!existsSync(cliPath)) {
54
+ return { code: 1, cliPath, error: `capture-cleanup CLI not found at ${cliPath}` };
55
+ }
56
+ const result = spawnSync(process.execPath, [cliPath, ...args], {
57
+ stdio: "inherit",
58
+ env: opts.env ? { ...process.env, ...opts.env } : process.env,
59
+ });
60
+ const code =
61
+ typeof result.status === "number" && result.status >= 0 ? result.status : 1;
62
+ return { code, cliPath };
63
+ }
@@ -65,6 +65,7 @@ const MEKO_MARKETPLACE_NAME = "meko-agent-skills";
65
65
  const MEKO_ALWAYS_AVAILABLE_TOOLS = [
66
66
  "mcp__meko__memory_add",
67
67
  "mcp__meko__memory_search",
68
+ "mcp__meko__context_search",
68
69
  "mcp__meko__memory_get_all",
69
70
  "mcp__meko__memory_get_by_id",
70
71
  "mcp__meko__memory_update",