@memoket-ai/cli 2.0.1

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/src/cli.js ADDED
@@ -0,0 +1,475 @@
1
+ // Command definitions.
2
+
3
+ import { Command } from "commander";
4
+ import { connect, listTools, callTool } from "./mcp-client.js";
5
+ import { login, ensureToken, loadCreds, fetchWellKnown } from "./oauth.js";
6
+ import { loginBasic } from "./auth-basic.js";
7
+ import { listGatewayTools, callGatewayTool, getGatewayTool } from "./gateway.js";
8
+ import { registerClaude, registerVSCode } from "./register.js";
9
+ import { defaultName, version, credentialsPath, mcpURLFromBase } from "./config.js";
10
+ import { tierOf, stripTier, firstLine, short, or, toolParams, requiredParamNames } from "./utils.js";
11
+ import {
12
+ createAPIToken,
13
+ listAPITokens,
14
+ revokeAPIToken,
15
+ rotateAPIToken,
16
+ } from "./http-integration.js";
17
+ import {
18
+ listEndpoints,
19
+ useEndpoint,
20
+ activeMcpURL,
21
+ activeName,
22
+ activeBase,
23
+ } from "./endpoints.js";
24
+
25
+ // resolveURL 优先级:--url 一次性覆盖 > 当前激活的命名环境(补 /mcp)> 默认(prod)。
26
+ // 激活环境读 ~/.memoket/config.json(同步),activeMcpURL 内部已回退默认 prod。
27
+ function resolveURL(opts) {
28
+ if (opts.url) return opts.url;
29
+ return activeMcpURL();
30
+ }
31
+
32
+ // printToolParams 打印一个工具的参数清单(对齐 name / (必填) / 类型 / 描述)。
33
+ // log 允许写到 stdout(describe) 或 stderr(报错提示);full=true 时不截断描述。
34
+ function printToolParams(tool, { log = console.log, full = false } = {}) {
35
+ const params = toolParams(tool);
36
+ if (params.length === 0) {
37
+ log(" (无参数)");
38
+ return;
39
+ }
40
+ const w = Math.max(...params.map((p) => p.name.length));
41
+ for (const p of params) {
42
+ const tag = p.required ? "(必填)" : "(可选)";
43
+ const type = (p.type || "").padEnd(9);
44
+ const desc = full ? p.description : firstLine(p.description);
45
+ log(` ${p.name.padEnd(w)} ${tag} ${type} ${desc}`.replace(/\s+$/, ""));
46
+ }
47
+ }
48
+
49
+ // exampleFor 造一条 key=value 示例:优先拿必填参数,否则第一个参数;值用样例或占位。
50
+ function exampleFor(tool, sampleValue) {
51
+ const params = toolParams(tool);
52
+ const pick = params.find((p) => p.required) || params[0];
53
+ const key = pick ? pick.name : "key";
54
+ const val = sampleValue !== undefined ? sampleValue : "value";
55
+ return `memoket call ${tool.name} ${key}=${val}`;
56
+ }
57
+
58
+ export function buildProgram() {
59
+ const program = new Command();
60
+ program
61
+ .name("memoket")
62
+ .description("Memoket MCP onboarding & passthrough CLI")
63
+ .version(version)
64
+ .option("--url <url>", "one-off MCP endpoint override (see `memoket endpoints`)")
65
+ .option("--name <name>", "service name when registering into a client", defaultName);
66
+
67
+ program
68
+ .command("setup")
69
+ .description("register Memoket MCP into local AI clients and log in (most common)")
70
+ .action(async (opts, cmd) => {
71
+ const root = cmd.parent;
72
+ const mcpURL = resolveURL(root.opts());
73
+ const name = root.opts().name;
74
+ console.log("== Memoket setup wizard ==");
75
+ console.log("MCP endpoint:", mcpURL);
76
+ console.log();
77
+
78
+ console.log("1) Register MCP into local AI clients");
79
+ const claude = await registerClaude(name, mcpURL);
80
+ if (claude.ok) {
81
+ console.log(` - Claude Code: registered ✓\n ${claude.cmd}`);
82
+ } else {
83
+ console.log(` - Claude Code: skipped/failed — run manually:\n ${claude.cmd}\n (${claude.err.message})`);
84
+ }
85
+ const vscode = await registerVSCode(name, mcpURL);
86
+ if (vscode.ok) {
87
+ console.log(" - VS Code: registered ✓");
88
+ } else {
89
+ console.log(` - VS Code: skipped/failed — run manually:\n ${vscode.cmd}`);
90
+ }
91
+ console.log(` - ChatGPT / Claude.ai: in Settings → Connectors, paste this URL: ${mcpURL}`);
92
+ console.log();
93
+
94
+ console.log("2) Log in (browser authorization)");
95
+ try {
96
+ await login(mcpURL);
97
+ } catch (e) {
98
+ throw new Error(`login failed: ${e.message}`);
99
+ }
100
+ console.log(" ✓ CLI obtained an access token (for `memoket tools` / `call`)");
101
+ console.log();
102
+
103
+ console.log("3) Done");
104
+ console.log(" - Claude Code needs its own login: in a session run /mcp, choose memoket, authorize in browser");
105
+ console.log(" (the CLI's token is not shared with Claude Code)");
106
+ console.log(" - Verify: memoket tools or memoket call get_memoket_overview");
107
+ process.exit(0);
108
+ });
109
+
110
+ program
111
+ .command("login")
112
+ .description("login (browser OAuth by default; --email/--password/--basic for email+password → gateway) and save token")
113
+ .option("--browser", "use the browser OAuth flow (default; kept for compatibility)")
114
+ .option("--basic", "use email/password login → gateway instead of the browser")
115
+ .option("--email <email>", "account email for email/password login (or set MEMOKET_EMAIL)")
116
+ .option("--password <password>", "account password for email/password login (or set MEMOKET_PASSWORD)")
117
+ .action(async (opts, cmd) => {
118
+ const mcpURL = resolveURL(cmd.parent.opts());
119
+ // 默认走浏览器 OAuth;仅当用户显式选择邮箱密码(--basic 或传了 --email/--password)时走 loginBasic。
120
+ const wantBasic = opts.basic || opts.email !== undefined || opts.password !== undefined;
121
+ if (wantBasic) {
122
+ // 邮箱密码登录 → gateway /auth/v1/login,存 userToken。
123
+ const p = await loginBasic(mcpURL, { email: opts.email, password: opts.password });
124
+ console.log(`✓ Logged in; userToken saved to ${p}`);
125
+ console.log(" Next: memoket call list_recordings (mtok 会用 userToken 自动签发)");
126
+ } else {
127
+ // 默认:浏览器 OAuth(--browser 为显式/兼容用,效果同默认)。
128
+ console.log("Logging in (browser OAuth):", mcpURL);
129
+ await login(mcpURL);
130
+ const p = await credentialsPath();
131
+ console.log(`✓ Logged in; credentials saved to ${p}`);
132
+ }
133
+ // Force exit — OAuth callback server / undici pool / open 子进程可能吊着事件循环。
134
+ process.exit(0);
135
+ });
136
+
137
+ // 隐藏别名:`status` 的信息已并进 `doctor`,但旧脚本仍可跑等价逻辑。
138
+ program
139
+ .command("status", { hidden: true })
140
+ .description("show endpoint, token status, and run an initialize handshake (legacy; see `memoket doctor`)")
141
+ .action(async (opts, cmd) => {
142
+ const mcpURL = resolveURL(cmd.parent.opts());
143
+ console.log("endpoint:", mcpURL);
144
+ const c = await loadCreds();
145
+ if (c) {
146
+ const exp = c.expires_at
147
+ ? new Date(c.expires_at * 1000).toISOString()
148
+ : "no expiry recorded";
149
+ console.log(`token: logged in (client_id=${c.client_id}, expires=${exp})`);
150
+ } else {
151
+ console.log("token: not logged in — run `memoket login`");
152
+ }
153
+ const token = await ensureToken(mcpURL);
154
+ const s = await connect(mcpURL, token);
155
+ const info = s.info;
156
+ console.log(
157
+ `server: ${info.serverInfo?.name} v${info.serverInfo?.version} (protocol ${info.protocolVersion}, session ${short(s.id)})`,
158
+ );
159
+ });
160
+
161
+ program
162
+ .command("tools")
163
+ .description("list tools exposed by the gateway (GET /v1/tools)")
164
+ .action(async (opts, cmd) => {
165
+ const mcpURL = resolveURL(cmd.parent.opts());
166
+ const tools = await listGatewayTools(mcpURL);
167
+
168
+ const groups = new Map();
169
+ for (const t of tools) {
170
+ const g = tierOf(t.description);
171
+ if (!groups.has(g)) groups.set(g, []);
172
+ groups.get(g).push(t);
173
+ }
174
+ const order = [...groups.keys()].sort();
175
+
176
+ console.log(`gateway · ${tools.length} tools`);
177
+ for (const g of order) {
178
+ console.log(`\n[${g}]`);
179
+ for (const t of groups.get(g)) {
180
+ const name = (t.name || "").padEnd(28);
181
+ const scopes = (t.required_scopes || []).join(",");
182
+ // 行尾带上必填参数名(如 [query]),让用户一眼看到关键入参;无必填则不加。
183
+ const req = requiredParamNames(t);
184
+ const reqHint = req.length ? ` [${req.join(",")}]` : "";
185
+ console.log(` ${name} ${firstLine(stripTier(t.description))}${scopes ? ` (scope: ${scopes})` : ""}${reqHint}`);
186
+ }
187
+ }
188
+ });
189
+
190
+ program
191
+ .command("call <tool> [args...]")
192
+ .description("call a tool: memoket call <tool> [key=value ...] [--json '{...}']")
193
+ .option("--json <payload>", "JSON object merged into the call args (mutually composable with key=value)")
194
+ .action(async (tool, rest, options, cmd) => {
195
+ const mcpURL = resolveURL(cmd.parent.opts());
196
+ const args = {};
197
+
198
+ // commander 已显式声明 --json;这里负责把它 parse 进 args。
199
+ // 任意拼写错(--jsno / --Json / 等等)都会在 commander 层被拒,不会走到这里。
200
+ if (options.json !== undefined) {
201
+ try {
202
+ Object.assign(args, JSON.parse(options.json));
203
+ } catch (e) {
204
+ throw new Error(`--json parse failed: ${e.message}`);
205
+ }
206
+ }
207
+
208
+ const positionals = []; // 不含 "=" 的裸参数 —— 后端解析不了,需拦下来给提示。
209
+ for (const a of rest) {
210
+ const eq = a.indexOf("=");
211
+ if (eq > 0) {
212
+ const { coerce } = await import("./utils.js");
213
+ args[a.slice(0, eq)] = coerce(a.slice(eq + 1));
214
+ } else {
215
+ positionals.push(a);
216
+ }
217
+ }
218
+
219
+ // 用户最常见的困惑:传了位置参数(如 `call search_recordings Elisa`),
220
+ // 后端只认 JSON body 里的 key,位置参数被静默丢弃 → 报 "query is required"。
221
+ // 这里拦下来,用该工具的真实参数给出 key=value 示例,而不是干巴巴报错。
222
+ if (positionals.length > 0) {
223
+ let toolDef = null;
224
+ try {
225
+ toolDef = await getGatewayTool(mcpURL, tool);
226
+ } catch {
227
+ /* schema 取不到就退化成通用提示 */
228
+ }
229
+ console.error(`参数要用 key=value 形式,位置参数(${positionals.join(" ")})已被忽略。`);
230
+ if (toolDef) {
231
+ console.error(`\n${tool} 的参数:`);
232
+ printToolParams(toolDef, { log: console.error });
233
+ console.error(`\n例: ${exampleFor(toolDef, positionals[0])}`);
234
+ console.error(`详情: memoket describe ${tool}`);
235
+ } else {
236
+ console.error(`\n例: memoket call ${tool} key=${positionals[0]} (参数名见: memoket describe ${tool})`);
237
+ }
238
+ process.exit(1);
239
+ }
240
+
241
+ // 无参调用且该工具有必填参数:先打印参数说明,别把用户直接推给后端报错。
242
+ // (whoami / list_recordings 这类无必填参数的工具不受影响,照常调用。)
243
+ if (Object.keys(args).length === 0) {
244
+ let toolDef = null;
245
+ try {
246
+ toolDef = await getGatewayTool(mcpURL, tool);
247
+ } catch {
248
+ /* 取不到 schema 就照常调用,让后端决定 */
249
+ }
250
+ if (toolDef && requiredParamNames(toolDef).length > 0) {
251
+ console.error(`${tool} 需要参数(key=value 形式):`);
252
+ printToolParams(toolDef, { log: console.error });
253
+ console.error(`\n例: ${exampleFor(toolDef)}`);
254
+ console.error(`详情: memoket describe ${tool}`);
255
+ process.exit(1);
256
+ }
257
+ }
258
+
259
+ const res = await callGatewayTool(mcpURL, tool, args);
260
+ console.log(JSON.stringify(res, null, 2));
261
+ });
262
+
263
+ program
264
+ .command("describe <tool>")
265
+ .description("show a tool's input parameters (name / required / type / description)")
266
+ .action(async (tool, opts, cmd) => {
267
+ const mcpURL = resolveURL(cmd.parent.opts());
268
+ const toolDef = await getGatewayTool(mcpURL, tool);
269
+ if (!toolDef) {
270
+ console.error(`unknown tool: ${tool} (列表: memoket tools)`);
271
+ process.exit(1);
272
+ }
273
+ const tier = tierOf(toolDef.description);
274
+ console.log(`${toolDef.name} [${tier}]`);
275
+ console.log(stripTier(toolDef.description));
276
+ const scopes = (toolDef.required_scopes || []).join(",");
277
+ if (scopes) console.log(`scope: ${scopes}`);
278
+ console.log("\n参数:");
279
+ printToolParams(toolDef, { full: true });
280
+ if (toolParams(toolDef).length > 0) {
281
+ console.log(`\n例: ${exampleFor(toolDef)}`);
282
+ } else {
283
+ console.log(`\n例: memoket call ${tool}`);
284
+ }
285
+ });
286
+
287
+
288
+ // `endpoints` 管理本地预置的命名环境(持久化到 ~/.memoket/config.json)。
289
+ // 无子命令 → 列出所有环境并标记 ACTIVE。
290
+ const endpointsCmd = program
291
+ .command("endpoints")
292
+ .description("list named endpoints (use `endpoints use <name>` to switch the active one)")
293
+ .action(() => {
294
+ // 先打印当前激活环境的清爽块:endpoint(补 /mcp) / environment(激活名) / api-gateway(激活 base)。
295
+ const envName = activeName();
296
+ const base = activeBase();
297
+ const endpoint = mcpURLFromBase(base);
298
+ const lw = "api/gateway".length; // 标签列宽(最长标签)
299
+ console.log("Memoket endpoints");
300
+ console.log();
301
+ console.log(` ${"endpoint".padEnd(lw)} ${endpoint}`);
302
+ console.log(` ${"environment".padEnd(lw)} ${envName}`);
303
+ console.log(` ${"api/gateway".padEnd(lw)} ${base}`);
304
+ console.log();
305
+
306
+ // 下面再列可切换的别名(NAME / BASE URL / ACTIVE)。
307
+ const rows = listEndpoints();
308
+ const w = Math.max(4, ...rows.map((r) => r.name.length));
309
+ const bw = Math.max(8, ...rows.map((r) => r.base.length));
310
+ console.log(`${"NAME".padEnd(w)} ${"BASE URL".padEnd(bw)} ACTIVE`);
311
+ for (const r of rows) {
312
+ console.log(`${r.name.padEnd(w)} ${r.base.padEnd(bw)} ${r.active ? "*" : ""}`.replace(/\s+$/, ""));
313
+ }
314
+ });
315
+
316
+ endpointsCmd
317
+ .command("use <name>")
318
+ .description("switch the active endpoint (persisted)")
319
+ .action((name) => {
320
+ try {
321
+ useEndpoint(name);
322
+ console.log(`✓ active -> ${name}`);
323
+ } catch (e) {
324
+ console.error(`unknown endpoint: ${name}`);
325
+ if (e.known) console.error(`available: ${e.known.join(", ")}`);
326
+ process.exit(1);
327
+ }
328
+ });
329
+
330
+ endpointsCmd
331
+ .command("add <name> <baseURL>")
332
+ .description("add or override a custom named endpoint (base host root, no /mcp)")
333
+ .action(async (name, baseURL) => {
334
+ const { addEndpoint } = await import("./endpoints.js");
335
+ const saved = addEndpoint(name, baseURL);
336
+ console.log(`✓ endpoint ${name} -> ${saved}`);
337
+ });
338
+
339
+ endpointsCmd
340
+ .command("remove <name>")
341
+ .description("remove a custom named endpoint (built-ins cannot be removed)")
342
+ .action(async (name) => {
343
+ const { removeEndpoint } = await import("./endpoints.js");
344
+ removeEndpoint(name);
345
+ console.log(`✓ removed endpoint ${name}`);
346
+ });
347
+
348
+ // `token` 拍平成 1 层:无子命令时默认「签发」,靠 flags 路由到列/撤。
349
+ // 旧的 create/list/revoke 子命令仍保留(隐藏,功能不变)。
350
+ const apiTokens = program
351
+ .command("token")
352
+ .description("issue / list / revoke Personal API Tokens (default action = issue)")
353
+ .option("--name <name>", "token name", "HTTP integration")
354
+ .option("--scope <scope...>", "API scopes (repeat or comma-separated)")
355
+ .option("--expires-in <days>", "token lifetime in days", "90")
356
+ .option("--list", "list API tokens instead of issuing")
357
+ .option("--revoke <id>", "revoke the API token with this id instead of issuing")
358
+ .option("--rotate <id>", "rotate (旧换新) the API token with this id: sign a new mtok, revoke the old")
359
+ .action(async (opts, cmd) => {
360
+ const rootCmd = cmd.parent;
361
+ const mcpURL = resolveURL(rootCmd.opts());
362
+ // 全局 --name(服务注册名)与本命令 --name 同名,commander 会把命令行里的
363
+ // --name 归给 root,token 自己的 --name 收不到。若用户在命令行显式传了 --name,
364
+ // 就把它当作 token 名;没传时用本命令默认 "HTTP integration"。
365
+ if (rootCmd.getOptionValueSource("name") === "cli") {
366
+ opts.name = rootCmd.opts().name;
367
+ }
368
+ // 互斥路由:--rotate 优先 → 旧换新;--revoke → 撤;--list → 列;都没传 → 默认签发。
369
+ if (opts.rotate !== undefined) {
370
+ await rotateAPIToken(mcpURL, opts.rotate);
371
+ } else if (opts.revoke !== undefined) {
372
+ await revokeAPIToken(mcpURL, opts.revoke);
373
+ } else if (opts.list) {
374
+ await listAPITokens(mcpURL);
375
+ } else {
376
+ await createAPIToken(mcpURL, opts);
377
+ }
378
+ });
379
+
380
+ // 隐藏的旧子命令:向后兼容,功能与拍平前完全一致。
381
+ apiTokens
382
+ .command("create", { hidden: true })
383
+ .description("create a scoped Personal API Token (legacy; use `memoket token`)")
384
+ .option("--name <name>", "token name", "HTTP integration")
385
+ .option("--scope <scope...>", "API scopes (repeat or comma-separated)")
386
+ .option("--expires-in <days>", "token lifetime in days", "90")
387
+ .action(async (opts, cmd) => {
388
+ const mcpURL = resolveURL(cmd.parent.parent.opts());
389
+ await createAPIToken(mcpURL, opts);
390
+ });
391
+
392
+ apiTokens
393
+ .command("list", { hidden: true })
394
+ .description("list API tokens (legacy; use `memoket token --list`)")
395
+ .action(async (opts, cmd) => {
396
+ const mcpURL = resolveURL(cmd.parent.parent.opts());
397
+ await listAPITokens(mcpURL);
398
+ });
399
+
400
+ apiTokens
401
+ .command("revoke <id>", { hidden: true })
402
+ .description("revoke an API token (legacy; use `memoket token --revoke <id>`)")
403
+ .action(async (id, opts, cmd) => {
404
+ const mcpURL = resolveURL(cmd.parent.parent.opts());
405
+ await revokeAPIToken(mcpURL, id);
406
+ });
407
+
408
+ // `token rotate <id>`:旧换新。等价于 `token --rotate <id>`,作为子命令更直观。
409
+ apiTokens
410
+ .command("rotate <id>")
411
+ .description("rotate a token (旧换新): sign a new mtok from the old token's user+scopes, revoke the old")
412
+ .action(async (id, opts, cmd) => {
413
+ const mcpURL = resolveURL(cmd.parent.parent.opts());
414
+ await rotateAPIToken(mcpURL, id);
415
+ });
416
+
417
+ program
418
+ .command("doctor")
419
+ .description("check connectivity and OAuth discovery")
420
+ .action(async (opts, cmd) => {
421
+ const mcpURL = resolveURL(cmd.parent.opts());
422
+ console.log("endpoint:", mcpURL);
423
+
424
+ // 并进原 `status` 的登录态信息(client_id / expiry)。
425
+ const c = await loadCreds();
426
+ if (c) {
427
+ const exp = c.expires_at
428
+ ? new Date(c.expires_at * 1000).toISOString()
429
+ : "no expiry recorded";
430
+ console.log(`token: logged in (client_id=${c.client_id}, expires=${exp})`);
431
+ } else {
432
+ console.log("token: not logged in — run `memoket login`");
433
+ }
434
+
435
+ let ok = true;
436
+ try {
437
+ const wk = await fetchWellKnown(mcpURL);
438
+ console.log("✓ OAuth discovery succeeded");
439
+ console.log(" authorize:", wk.authorization_endpoint);
440
+ console.log(" token: ", wk.token_endpoint);
441
+ console.log(" register:", or(wk.registration_endpoint, "(no DCR)"));
442
+ } catch (e) {
443
+ ok = false;
444
+ console.log("✗ OAuth discovery failed:", e.message);
445
+ }
446
+
447
+ const token = await ensureToken(mcpURL);
448
+ try {
449
+ const s = await connect(mcpURL, token);
450
+ const info = s.info;
451
+ console.log(
452
+ `✓ initialize: ${info.serverInfo?.name} v${info.serverInfo?.version} (protocol ${info.protocolVersion})`,
453
+ );
454
+ const tools = await listTools(s);
455
+ console.log(`✓ tools/list: ${tools.length} tools`);
456
+ } catch (e) {
457
+ console.log("✗ initialize/tools/list failed:", e.message);
458
+ if (!ok) throw new Error("server unreachable or misconfigured");
459
+ throw e;
460
+ }
461
+ });
462
+
463
+ // 无参 → 打印帮助(exit 0);带了不认识的子命令(如已删的 `connect`)→ 报未知命令并 exit 1。
464
+ program.action((options, cmd) => {
465
+ const extra = cmd.args || [];
466
+ if (extra.length) {
467
+ console.error(`error: unknown command '${extra[0]}'`);
468
+ console.error("run `memoket --help` for the command list");
469
+ process.exit(1);
470
+ }
471
+ program.help();
472
+ });
473
+
474
+ return program;
475
+ }
package/src/config.js ADDED
@@ -0,0 +1,77 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+
7
+ // version is overridable at build time if needed; keep in sync with package.json.
8
+ export const version = "2.0.1";
9
+
10
+ // Build-time endpoints / active default / api base: stamped into
11
+ // src/config.local.js by scripts/build.js (npm run build:<env>). When the
12
+ // file is missing (fresh checkout, build not yet run), we fall back to
13
+ // the prod endpoint so the CLI still works out of the box.
14
+ //
15
+ // Note: internal dev/test endpoints live ONLY in config/dev.json and
16
+ // config/test.json, which are NOT shipped in the public npm tarball (see
17
+ // package.json#files). This file falls back to prod so a fresh install
18
+ // from npm has a working default; team members running the CLI from a
19
+ // clone should `npm run build:dev` first.
20
+ const FALLBACK_DEFAULT_ENDPOINTS = {
21
+ prod: "https://mcp.memoket.ai",
22
+ };
23
+ const FALLBACK_DEFAULT_ACTIVE = "prod";
24
+ const FALLBACK_API_BASE = "https://api.memoket.ai";
25
+
26
+ let _defaultEndpoints = FALLBACK_DEFAULT_ENDPOINTS;
27
+ let _defaultActiveEndpoint = FALLBACK_DEFAULT_ACTIVE;
28
+ let _apiBase = FALLBACK_API_BASE;
29
+ const localPath = join(__dirname, "config.local.js");
30
+ if (existsSync(localPath)) {
31
+ const local = await import("./config.local.js");
32
+ if (local.defaultEndpoints && typeof local.defaultEndpoints === "object") {
33
+ _defaultEndpoints = local.defaultEndpoints;
34
+ }
35
+ if (typeof local.defaultActiveEndpoint === "string") {
36
+ _defaultActiveEndpoint = local.defaultActiveEndpoint;
37
+ }
38
+ if (typeof local.apiBase === "string") {
39
+ _apiBase = local.apiBase;
40
+ }
41
+ }
42
+
43
+ export const defaultEndpoints = _defaultEndpoints;
44
+ export const defaultActiveEndpoint = _defaultActiveEndpoint;
45
+ export const apiBase = _apiBase;
46
+
47
+ // mcpURLFromBase 把主机根补成 mcp endpoint(下游沿用 /mcp 后缀的历史约定)。
48
+ export function mcpURLFromBase(base) {
49
+ return `${String(base).replace(/\/$/, "")}/mcp`;
50
+ }
51
+
52
+ export const defaultName = "memoket";
53
+ export const defaultScope = "read write"; // matches the existing backend client; server grants what it grants
54
+ export const callbackPort = 8765;
55
+ export const clientName = "memoket-cli";
56
+ export const protocolVersion = "2025-03-26";
57
+
58
+ // memoketDir 解析 ~/.memoket 目录(同步);credentials.json / auth.json / config.json 都在这里。
59
+ export function memoketDir() {
60
+ const home = process.env.HOME || process.env.USERPROFILE || "";
61
+ if (!home) throw new Error("cannot determine home directory");
62
+ const sep = process.platform === "win32" ? "\\" : "/";
63
+ return `${home}${sep}.memoket`;
64
+ }
65
+
66
+ // credentialsPath is where the CLI stores its own OAuth token (used by
67
+ // `memoket tools` / `memoket call`). It does NOT feed any external client.
68
+ export async function credentialsPath() {
69
+ const sep = process.platform === "win32" ? "\\" : "/";
70
+ return `${memoketDir()}${sep}credentials.json`;
71
+ }
72
+
73
+ // endpointsConfigPath 是命名环境的持久化文件(激活环境 + 自定义环境)。
74
+ export function endpointsConfigPath() {
75
+ const sep = process.platform === "win32" ? "\\" : "/";
76
+ return `${memoketDir()}${sep}config.json`;
77
+ }
@@ -0,0 +1,11 @@
1
+ // AUTO-GENERATED by scripts/build.js — do not edit.
2
+ // Source: config/prod.json
3
+ // Regenerate with: npm run build:prod
4
+
5
+ export const defaultEndpoints = {
6
+ "prod": "https://mcp.memoket.ai"
7
+ };
8
+
9
+ export const defaultActiveEndpoint = "prod";
10
+
11
+ export const apiBase = "https://api.memoket.ai";
@@ -0,0 +1,112 @@
1
+ // 命名环境(endpoints):本地预置 + 用户可持久化覆盖/扩展,存到 ~/.memoket/config.json。
2
+ // 只保存「主机根」(不带 /mcp);resolveURL 用 mcpURLFromBase 补 /mcp。
3
+ //
4
+ // config.json 格式:
5
+ // {
6
+ // "active": "dev",
7
+ // "endpoints": { "myenv": "https://api.example.com" } // 可选:自定义/覆盖
8
+ // }
9
+ // endpoints 与内置 defaultEndpoints 合并(用户项覆盖同名内置项)。
10
+
11
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
12
+ import {
13
+ defaultEndpoints,
14
+ defaultActiveEndpoint,
15
+ endpointsConfigPath,
16
+ memoketDir,
17
+ mcpURLFromBase,
18
+ } from "./config.js";
19
+
20
+ // loadConfig 读回持久化配置(同步);文件不存在/损坏时回退空配置。
21
+ function loadConfig() {
22
+ try {
23
+ const raw = readFileSync(endpointsConfigPath(), "utf8");
24
+ const obj = JSON.parse(raw);
25
+ return {
26
+ active: typeof obj.active === "string" ? obj.active : undefined,
27
+ endpoints: obj.endpoints && typeof obj.endpoints === "object" ? obj.endpoints : {},
28
+ };
29
+ } catch {
30
+ return { active: undefined, endpoints: {} };
31
+ }
32
+ }
33
+
34
+ function saveConfig(cfg) {
35
+ const dir = memoketDir();
36
+ mkdirSync(dir, { recursive: true });
37
+ writeFileSync(endpointsConfigPath(), JSON.stringify(cfg, null, 2), { mode: 0o600 });
38
+ }
39
+
40
+ // mergedEndpoints 返回「内置 + 自定义」的完整环境表(自定义覆盖同名内置)。
41
+ function mergedEndpoints(cfg = loadConfig()) {
42
+ return { ...defaultEndpoints, ...cfg.endpoints };
43
+ }
44
+
45
+ // activeName 返回当前激活环境名:持久化的 active(若仍有效)→ 否则默认 prod。
46
+ export function activeName() {
47
+ const cfg = loadConfig();
48
+ const all = mergedEndpoints(cfg);
49
+ if (cfg.active && all[cfg.active]) return cfg.active;
50
+ return defaultActiveEndpoint;
51
+ }
52
+
53
+ // activeBase 返回当前激活环境的主机根(不带 /mcp)。
54
+ export function activeBase() {
55
+ const all = mergedEndpoints();
56
+ const name = activeName();
57
+ return all[name];
58
+ }
59
+
60
+ // activeMcpURL 返回当前激活环境的 mcp endpoint(补 /mcp),供 resolveURL 使用。
61
+ export function activeMcpURL() {
62
+ return mcpURLFromBase(activeBase());
63
+ }
64
+
65
+ // listEndpoints 返回 [{ name, base, active }],顺序:内置(dev/test/prod) 优先,其后自定义。
66
+ export function listEndpoints() {
67
+ const cfg = loadConfig();
68
+ const all = mergedEndpoints(cfg);
69
+ const active = activeName();
70
+ const names = [
71
+ ...Object.keys(defaultEndpoints),
72
+ ...Object.keys(cfg.endpoints || {}).filter((n) => !(n in defaultEndpoints)),
73
+ ];
74
+ return names.map((name) => ({ name, base: all[name], active: name === active }));
75
+ }
76
+
77
+ // useEndpoint 切换激活环境;未知名字抛错(由调用方列出可选项)。
78
+ export function useEndpoint(name) {
79
+ const all = mergedEndpoints();
80
+ if (!all[name]) {
81
+ const err = new Error(`unknown endpoint: ${name}`);
82
+ err.known = Object.keys(all);
83
+ throw err;
84
+ }
85
+ const cfg = loadConfig();
86
+ cfg.active = name;
87
+ saveConfig(cfg);
88
+ return name;
89
+ }
90
+
91
+ // addEndpoint 新增/覆盖一个自定义环境(存主机根,去掉尾斜杠)。
92
+ export function addEndpoint(name, baseURL) {
93
+ if (!name) throw new Error("endpoint name is required");
94
+ if (!baseURL) throw new Error("base URL is required");
95
+ const cfg = loadConfig();
96
+ cfg.endpoints = cfg.endpoints || {};
97
+ cfg.endpoints[name] = String(baseURL).replace(/\/$/, "");
98
+ saveConfig(cfg);
99
+ return cfg.endpoints[name];
100
+ }
101
+
102
+ // removeEndpoint 删除一个自定义环境;内置环境不可删。删掉的若是激活项则回退默认。
103
+ export function removeEndpoint(name) {
104
+ if (name in defaultEndpoints) throw new Error(`cannot remove built-in endpoint: ${name}`);
105
+ const cfg = loadConfig();
106
+ if (!cfg.endpoints || !(name in cfg.endpoints)) {
107
+ throw new Error(`unknown custom endpoint: ${name}`);
108
+ }
109
+ delete cfg.endpoints[name];
110
+ if (cfg.active === name) cfg.active = defaultActiveEndpoint;
111
+ saveConfig(cfg);
112
+ }