memax-cli 0.0.1 → 0.1.0-alpha.10

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 (91) hide show
  1. package/.vscode/mcp.json +8 -0
  2. package/dist/commands/auth.d.ts +6 -0
  3. package/dist/commands/auth.d.ts.map +1 -0
  4. package/dist/commands/auth.js +62 -0
  5. package/dist/commands/auth.js.map +1 -0
  6. package/dist/commands/capture.d.ts +17 -0
  7. package/dist/commands/capture.d.ts.map +1 -0
  8. package/dist/commands/capture.js +61 -0
  9. package/dist/commands/capture.js.map +1 -0
  10. package/dist/commands/config.d.ts +3 -0
  11. package/dist/commands/config.d.ts.map +1 -0
  12. package/dist/commands/config.js +24 -0
  13. package/dist/commands/config.js.map +1 -0
  14. package/dist/commands/delete.d.ts +4 -0
  15. package/dist/commands/delete.d.ts.map +1 -0
  16. package/dist/commands/delete.js +45 -0
  17. package/dist/commands/delete.js.map +1 -0
  18. package/dist/commands/hook.d.ts +2 -0
  19. package/dist/commands/hook.d.ts.map +1 -0
  20. package/dist/commands/hook.js +189 -0
  21. package/dist/commands/hook.js.map +1 -0
  22. package/dist/commands/list.d.ts +8 -0
  23. package/dist/commands/list.d.ts.map +1 -0
  24. package/dist/commands/list.js +23 -0
  25. package/dist/commands/list.js.map +1 -0
  26. package/dist/commands/login.d.ts +4 -0
  27. package/dist/commands/login.d.ts.map +1 -0
  28. package/dist/commands/login.js +131 -0
  29. package/dist/commands/login.js.map +1 -0
  30. package/dist/commands/mcp.d.ts +3 -0
  31. package/dist/commands/mcp.d.ts.map +1 -0
  32. package/dist/commands/mcp.js +384 -0
  33. package/dist/commands/mcp.js.map +1 -0
  34. package/dist/commands/push.d.ts +11 -0
  35. package/dist/commands/push.d.ts.map +1 -0
  36. package/dist/commands/push.js +98 -0
  37. package/dist/commands/push.js.map +1 -0
  38. package/dist/commands/recall.d.ts +12 -0
  39. package/dist/commands/recall.d.ts.map +1 -0
  40. package/dist/commands/recall.js +107 -0
  41. package/dist/commands/recall.js.map +1 -0
  42. package/dist/commands/setup.d.ts +16 -0
  43. package/dist/commands/setup.d.ts.map +1 -0
  44. package/dist/commands/setup.js +869 -0
  45. package/dist/commands/setup.js.map +1 -0
  46. package/dist/commands/show.d.ts +2 -0
  47. package/dist/commands/show.d.ts.map +1 -0
  48. package/dist/commands/show.js +29 -0
  49. package/dist/commands/show.js.map +1 -0
  50. package/dist/commands/sync.d.ts +12 -0
  51. package/dist/commands/sync.d.ts.map +1 -0
  52. package/dist/commands/sync.js +414 -0
  53. package/dist/commands/sync.js.map +1 -0
  54. package/dist/index.d.ts +3 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +168 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/lib/api.d.ts +4 -0
  59. package/dist/lib/api.d.ts.map +1 -0
  60. package/dist/lib/api.js +95 -0
  61. package/dist/lib/api.js.map +1 -0
  62. package/dist/lib/config.d.ts +10 -0
  63. package/dist/lib/config.d.ts.map +1 -0
  64. package/dist/lib/config.js +49 -0
  65. package/dist/lib/config.js.map +1 -0
  66. package/dist/lib/credentials.d.ts +11 -0
  67. package/dist/lib/credentials.d.ts.map +1 -0
  68. package/dist/lib/credentials.js +36 -0
  69. package/dist/lib/credentials.js.map +1 -0
  70. package/package.json +39 -4
  71. package/src/commands/auth.ts +92 -0
  72. package/src/commands/capture.ts +86 -0
  73. package/src/commands/config.ts +27 -0
  74. package/src/commands/delete.ts +58 -0
  75. package/src/commands/hook.ts +243 -0
  76. package/src/commands/list.ts +38 -0
  77. package/src/commands/login.ts +164 -0
  78. package/src/commands/mcp.ts +490 -0
  79. package/src/commands/push.ts +137 -0
  80. package/src/commands/recall.ts +163 -0
  81. package/src/commands/setup.ts +1129 -0
  82. package/src/commands/show.ts +35 -0
  83. package/src/commands/sync.ts +506 -0
  84. package/src/index.ts +223 -0
  85. package/src/lib/api.ts +110 -0
  86. package/src/lib/config.ts +61 -0
  87. package/src/lib/credentials.ts +42 -0
  88. package/tsconfig.json +9 -0
  89. package/LICENSE +0 -24
  90. package/README.md +0 -2
  91. package/bin/memax.js +0 -13
@@ -0,0 +1,1129 @@
1
+ import chalk from "chalk";
2
+ import {
3
+ readFileSync,
4
+ writeFileSync,
5
+ mkdirSync,
6
+ existsSync,
7
+ chmodSync,
8
+ } from "node:fs";
9
+ import { join, dirname } from "node:path";
10
+ import { homedir, platform } from "node:os";
11
+ import { execSync } from "node:child_process";
12
+ import { apiPost } from "../lib/api.js";
13
+ import { loadConfig } from "../lib/config.js";
14
+
15
+ // --- Agent definitions ---
16
+
17
+ interface AgentDef {
18
+ name: string;
19
+ id: string;
20
+ configPath: string; // global MCP config file
21
+ format: "json-mcpServers" | "json-servers" | "toml";
22
+ /** Key under which MCP servers live */
23
+ mcpKey: string;
24
+ hasHooks: boolean;
25
+ /** Global instruction file path (e.g. ~/.claude/CLAUDE.md) — null if none */
26
+ globalInstructionFile: string | null;
27
+ detect: () => boolean; // is this agent likely installed?
28
+ }
29
+
30
+ function getAgents(): AgentDef[] {
31
+ const home = homedir();
32
+ const cwd = process.cwd();
33
+
34
+ return [
35
+ {
36
+ name: "Claude Code",
37
+ id: "claude-code",
38
+ configPath: join(home, ".claude", "settings.json"),
39
+ format: "json-mcpServers",
40
+ mcpKey: "mcpServers",
41
+ hasHooks: true,
42
+ globalInstructionFile: join(home, ".claude", "CLAUDE.md"),
43
+ detect: () =>
44
+ existsSync(join(home, ".claude")) || commandExists("claude"),
45
+ },
46
+ {
47
+ name: "Cursor",
48
+ id: "cursor",
49
+ configPath: join(home, ".cursor", "mcp.json"),
50
+ format: "json-mcpServers",
51
+ mcpKey: "mcpServers",
52
+ hasHooks: false,
53
+ globalInstructionFile: null, // project-level .cursorrules only
54
+ detect: () =>
55
+ existsSync(join(home, ".cursor")) || commandExists("cursor"),
56
+ },
57
+ {
58
+ name: "Windsurf",
59
+ id: "windsurf",
60
+ configPath: join(home, ".codeium", "windsurf", "mcp_config.json"),
61
+ format: "json-mcpServers",
62
+ mcpKey: "mcpServers",
63
+ hasHooks: false,
64
+ globalInstructionFile: null, // project-level .windsurfrules only
65
+ detect: () =>
66
+ existsSync(join(home, ".codeium", "windsurf")) ||
67
+ commandExists("windsurf"),
68
+ },
69
+ {
70
+ name: "Gemini CLI",
71
+ id: "gemini",
72
+ configPath: join(home, ".gemini", "settings.json"),
73
+ format: "json-mcpServers",
74
+ mcpKey: "mcpServers",
75
+ hasHooks: true,
76
+ globalInstructionFile: join(home, ".gemini", "GEMINI.md"),
77
+ detect: () =>
78
+ existsSync(join(home, ".gemini")) || commandExists("gemini"),
79
+ },
80
+ {
81
+ name: "GitHub Copilot CLI",
82
+ id: "copilot",
83
+ configPath: join(home, ".copilot", "mcp-config.json"),
84
+ format: "json-mcpServers",
85
+ mcpKey: "mcpServers",
86
+ hasHooks: false,
87
+ globalInstructionFile: null, // uses .github/copilot-instructions.md (project-level)
88
+ detect: () =>
89
+ existsSync(join(home, ".copilot")) || commandExists("gh copilot"),
90
+ },
91
+ {
92
+ name: "Copilot (VS Code)",
93
+ id: "vscode",
94
+ configPath: join(".vscode", "mcp.json"),
95
+ format: "json-servers",
96
+ mcpKey: "servers",
97
+ hasHooks: false,
98
+ globalInstructionFile: null,
99
+ detect: () => existsSync(".vscode") || commandExists("code"),
100
+ },
101
+ {
102
+ name: "Codex CLI",
103
+ id: "codex",
104
+ configPath: join(home, ".codex", "config.toml"),
105
+ format: "toml",
106
+ mcpKey: "mcp_servers",
107
+ hasHooks: false,
108
+ globalInstructionFile: join(home, ".codex", "AGENTS.md"),
109
+ detect: () => existsSync(join(home, ".codex")) || commandExists("codex"),
110
+ },
111
+ {
112
+ name: "OpenClaw",
113
+ id: "openclaw",
114
+ configPath: join(home, ".openclaw", "openclaw.json"),
115
+ format: "json-mcpServers",
116
+ mcpKey: "mcp.servers",
117
+ hasHooks: false,
118
+ globalInstructionFile: null, // OpenClaw has its own memory system
119
+ detect: () =>
120
+ existsSync(join(home, ".openclaw")) || commandExists("openclaw"),
121
+ },
122
+ {
123
+ name: "OpenCode",
124
+ id: "opencode",
125
+ configPath: join(cwd, ".opencode", "opencode.jsonc"),
126
+ format: "json-mcpServers",
127
+ mcpKey: "mcp",
128
+ hasHooks: false,
129
+ globalInstructionFile: null, // project-level only
130
+ detect: () =>
131
+ existsSync(join(cwd, ".opencode")) || commandExists("opencode"),
132
+ },
133
+ ];
134
+ }
135
+
136
+ // --- Setup command ---
137
+
138
+ interface SetupOptions {
139
+ mcp?: boolean;
140
+ hooks?: boolean;
141
+ instructions?: boolean;
142
+ all?: boolean;
143
+ local?: boolean;
144
+ print?: boolean;
145
+ only?: string;
146
+ skip?: string;
147
+ }
148
+
149
+ export async function setupCommand(options: SetupOptions): Promise<void> {
150
+ const enableMcp = options.all || options.mcp;
151
+ const enableHooks = options.all || options.hooks;
152
+ const enableInstructions = options.all || options.instructions;
153
+
154
+ if (!enableMcp && !enableHooks && !enableInstructions && !options.print) {
155
+ printUsage();
156
+ return;
157
+ }
158
+
159
+ // --print: just output config JSON for manual copy/paste
160
+ if (options.print) {
161
+ await printMcpConfigs(options.local ?? false);
162
+ return;
163
+ }
164
+
165
+ // Remote mode (default): need API key for auth
166
+ const useRemote = !options.local;
167
+ let apiKey: string | undefined;
168
+
169
+ if (useRemote && enableMcp) {
170
+ apiKey = await ensureApiKey();
171
+ if (!apiKey) {
172
+ console.error(
173
+ chalk.red(
174
+ "\n Could not create API key. Log in first: memax login\n" +
175
+ " Or use --local for local MCP server.\n",
176
+ ),
177
+ );
178
+ process.exit(1);
179
+ }
180
+ }
181
+
182
+ // Local mode: need memax binary
183
+ let memaxBin: MemaxBin | null = null;
184
+ if (!useRemote || enableHooks) {
185
+ memaxBin = resolveMemaxBin();
186
+ if (!memaxBin && !useRemote) {
187
+ console.error(
188
+ chalk.red(
189
+ "\n Could not find memax binary.\n Install globally: npm install -g memax-cli@alpha\n",
190
+ ),
191
+ );
192
+ process.exit(1);
193
+ }
194
+ }
195
+
196
+ // Filter agents
197
+ const allAgents = getAgents();
198
+ const onlySet = options.only
199
+ ? new Set(options.only.split(",").map((s) => s.trim().toLowerCase()))
200
+ : null;
201
+ const skipSet = options.skip
202
+ ? new Set(options.skip.split(",").map((s) => s.trim().toLowerCase()))
203
+ : new Set<string>();
204
+
205
+ const agents = allAgents.filter((a) => {
206
+ if (skipSet.has(a.id)) return false;
207
+ if (onlySet) return onlySet.has(a.id);
208
+ return a.detect();
209
+ });
210
+
211
+ if (agents.length === 0) {
212
+ console.log(chalk.yellow("\n No supported AI agents detected.\n"));
213
+ console.log(chalk.gray(" Supported agents:"));
214
+ for (const a of allAgents) {
215
+ console.log(chalk.gray(` • ${a.name} (--only ${a.id})`));
216
+ }
217
+ console.log(
218
+ chalk.gray("\n Use --only to force setup for a specific agent.\n"),
219
+ );
220
+ return;
221
+ }
222
+
223
+ console.log(chalk.bold("\n Memax Setup\n"));
224
+ if (useRemote) {
225
+ console.log(chalk.gray(" Mode: remote server (recommended)\n"));
226
+ } else {
227
+ console.log(chalk.gray(" Mode: local CLI\n"));
228
+ }
229
+
230
+ const results: { agent: string; changes: string[] }[] = [];
231
+
232
+ for (const agent of agents) {
233
+ const changes: string[] = [];
234
+
235
+ // MCP setup
236
+ if (enableMcp) {
237
+ try {
238
+ if (useRemote) {
239
+ setupMcpRemote(agent, apiKey!);
240
+ } else {
241
+ setupMcp(agent, memaxBin!);
242
+ }
243
+ changes.push(useRemote ? "MCP server (remote)" : "MCP server (local)");
244
+ } catch (err) {
245
+ console.log(
246
+ chalk.red(
247
+ ` ✗ ${agent.name}: MCP setup failed — ${(err as Error).message}`,
248
+ ),
249
+ );
250
+ }
251
+ }
252
+
253
+ // Hook setup (only for agents that support it — needs local binary)
254
+ if (enableHooks && agent.hasHooks && memaxBin) {
255
+ try {
256
+ setupHooks(agent, memaxBin);
257
+ changes.push("Context injection hook");
258
+ } catch (err) {
259
+ console.log(
260
+ chalk.red(
261
+ ` ✗ ${agent.name}: Hook setup failed — ${(err as Error).message}`,
262
+ ),
263
+ );
264
+ }
265
+ }
266
+
267
+ // Inject memax instructions into agent's global instruction file
268
+ if (enableInstructions && agent.globalInstructionFile) {
269
+ try {
270
+ injectInstructions(agent.globalInstructionFile);
271
+ changes.push("Instructions injected");
272
+ } catch (err) {
273
+ console.log(
274
+ chalk.red(
275
+ ` ✗ ${agent.name}: Instruction injection failed — ${(err as Error).message}`,
276
+ ),
277
+ );
278
+ }
279
+ }
280
+
281
+ if (changes.length > 0) {
282
+ results.push({ agent: agent.name, changes });
283
+ }
284
+ }
285
+
286
+ // Print summary
287
+ if (results.length === 0) {
288
+ console.log(chalk.yellow(" No changes made.\n"));
289
+ return;
290
+ }
291
+
292
+ console.log(chalk.green(" Configured:\n"));
293
+ for (const r of results) {
294
+ console.log(chalk.white(` ${r.agent}`));
295
+ for (const c of r.changes) {
296
+ console.log(chalk.gray(` ✓ ${c}`));
297
+ }
298
+ }
299
+
300
+ console.log(chalk.gray("\n MCP tools available to all configured agents:"));
301
+ console.log(
302
+ chalk.gray(" • memax_recall — semantic search your knowledge"),
303
+ );
304
+ console.log(chalk.gray(" • memax_push — save knowledge from sessions"));
305
+ console.log(chalk.gray(" • memax_get — read full note by ID"));
306
+ console.log(chalk.gray(" • memax_search — browse notes by category"));
307
+
308
+ if (enableHooks) {
309
+ const hookAgents = results.filter((r) =>
310
+ r.changes.includes("Context injection hook"),
311
+ );
312
+ if (hookAgents.length > 0) {
313
+ console.log(
314
+ chalk.gray(
315
+ `\n Hooks installed for: ${hookAgents.map((r) => r.agent).join(", ")}`,
316
+ ),
317
+ );
318
+ console.log(
319
+ chalk.gray(
320
+ " Every prompt gets relevant context injected automatically.",
321
+ ),
322
+ );
323
+ }
324
+ }
325
+
326
+ console.log(
327
+ chalk.gray("\n Restart your agents for changes to take effect.\n"),
328
+ );
329
+ }
330
+
331
+ export async function teardownCommand(options: {
332
+ only?: string;
333
+ }): Promise<void> {
334
+ const allAgents = getAgents();
335
+ const onlySet = options.only
336
+ ? new Set(options.only.split(",").map((s) => s.trim().toLowerCase()))
337
+ : null;
338
+
339
+ const agents = onlySet
340
+ ? allAgents.filter((a) => onlySet.has(a.id))
341
+ : allAgents;
342
+
343
+ let removed = false;
344
+
345
+ for (const agent of agents) {
346
+ try {
347
+ // Claude Code uses its own CLI
348
+ if (agent.id === "claude-code") {
349
+ if (commandExists("claude")) {
350
+ try {
351
+ execSync("claude mcp remove memax", { stdio: "pipe" });
352
+ console.log(chalk.gray(` Removed MCP from ${agent.name}`));
353
+ removed = true;
354
+ } catch {
355
+ // Not installed
356
+ }
357
+ }
358
+ if (agent.hasHooks && existsSync(agent.configPath)) {
359
+ if (removeHooks(agent)) removed = true;
360
+ }
361
+ if (
362
+ agent.globalInstructionFile &&
363
+ removeInstructions(agent.globalInstructionFile)
364
+ ) {
365
+ console.log(chalk.gray(` Removed instructions from ${agent.name}`));
366
+ removed = true;
367
+ }
368
+ continue;
369
+ }
370
+
371
+ if (!existsSync(agent.configPath)) continue;
372
+
373
+ if (agent.format === "toml") {
374
+ if (removeMcpToml(agent)) removed = true;
375
+ } else {
376
+ if (removeMcpJson(agent)) removed = true;
377
+ }
378
+ if (agent.hasHooks && removeHooks(agent)) removed = true;
379
+ if (
380
+ agent.globalInstructionFile &&
381
+ removeInstructions(agent.globalInstructionFile)
382
+ ) {
383
+ console.log(chalk.gray(` Removed instructions from ${agent.name}`));
384
+ removed = true;
385
+ }
386
+ } catch {
387
+ // Skip agents we can't clean up
388
+ }
389
+ }
390
+
391
+ if (!removed) {
392
+ console.log(chalk.yellow("\n No Memax integrations found to remove.\n"));
393
+ return;
394
+ }
395
+
396
+ console.log(
397
+ chalk.green(
398
+ "\n Memax integrations removed.\n Restart your agents for changes to take effect.\n",
399
+ ),
400
+ );
401
+ }
402
+
403
+ // --- Remote MCP setup ---
404
+
405
+ async function ensureApiKey(): Promise<string | undefined> {
406
+ try {
407
+ const result = await apiPost<{ key: string }>("/v1/auth/api-keys", {
408
+ name: "mcp-setup",
409
+ expires_in_days: 0, // no expiry
410
+ });
411
+ return result.key;
412
+ } catch {
413
+ return undefined;
414
+ }
415
+ }
416
+
417
+ function getApiUrl(): string {
418
+ return loadConfig().api_url;
419
+ }
420
+
421
+ function setupMcpRemote(agent: AgentDef, apiKey: string): void {
422
+ const mcpUrl = `${getApiUrl()}/mcp`;
423
+
424
+ // Claude Code uses its own CLI
425
+ if (agent.id === "claude-code") {
426
+ if (!commandExists("claude")) {
427
+ throw new Error("claude CLI not found in PATH");
428
+ }
429
+ try {
430
+ execSync("claude mcp remove memax", { stdio: "pipe" });
431
+ } catch {
432
+ // Not installed yet
433
+ }
434
+ // Claude Code HTTP transport
435
+ execSync(
436
+ `claude mcp add memax --transport http ${mcpUrl} --header "Authorization: Bearer ${apiKey}"`,
437
+ { stdio: "pipe" },
438
+ );
439
+ return;
440
+ }
441
+
442
+ // Codex TOML
443
+ if (agent.format === "toml") {
444
+ mkdirSync(dirname(agent.configPath), { recursive: true });
445
+ let content = "";
446
+ if (existsSync(agent.configPath)) {
447
+ content = readFileSync(agent.configPath, "utf-8");
448
+ }
449
+ content = content.replace(/\[mcp_servers\.memax\][\s\S]*?(?=\n\[|$)/, "");
450
+ content = content.trim();
451
+ if (content) content += "\n\n";
452
+ content += `[mcp_servers.memax]\ntype = "url"\nurl = "${mcpUrl}"\n\n[mcp_servers.memax.headers]\nAuthorization = "Bearer ${apiKey}"\n`;
453
+ writeFileSync(agent.configPath, content);
454
+ return;
455
+ }
456
+
457
+ // JSON-based agents
458
+ mkdirSync(dirname(agent.configPath), { recursive: true });
459
+ let config: Record<string, unknown> = {};
460
+ if (existsSync(agent.configPath)) {
461
+ try {
462
+ config = JSON.parse(readFileSync(agent.configPath, "utf-8"));
463
+ } catch {
464
+ // Start fresh
465
+ }
466
+ }
467
+
468
+ const servers = (getNestedKey(config, agent.mcpKey) ?? {}) as Record<
469
+ string,
470
+ unknown
471
+ >;
472
+ // Copilot CLI uses "http", others use "url" or no type field
473
+ const mcpType = agent.id === "copilot" ? "http" : "url";
474
+ servers.memax = {
475
+ type: mcpType,
476
+ url: mcpUrl,
477
+ headers: {
478
+ Authorization: `Bearer ${apiKey}`,
479
+ },
480
+ };
481
+ setNestedKey(config, agent.mcpKey, servers);
482
+ writeFileSync(agent.configPath, JSON.stringify(config, null, 2) + "\n");
483
+ }
484
+
485
+ async function printMcpConfigs(local: boolean): Promise<void> {
486
+ const apiUrl = getApiUrl();
487
+
488
+ console.log(chalk.bold("\n Memax MCP Configuration\n"));
489
+
490
+ if (local) {
491
+ const bin = resolveMemaxBin();
492
+ const cmd = bin ? bin.command : "memax";
493
+ const args = bin ? [...bin.args, "mcp", "serve"] : ["mcp", "serve"];
494
+
495
+ console.log(chalk.gray(" Mode: local (stdio)\n"));
496
+ console.log(
497
+ chalk.white(" For most agents (Claude Code, Cursor, Gemini, etc.):\n"),
498
+ );
499
+ console.log(
500
+ JSON.stringify(
501
+ {
502
+ mcpServers: {
503
+ memax: { command: cmd, args },
504
+ },
505
+ },
506
+ null,
507
+ 2,
508
+ )
509
+ .split("\n")
510
+ .map((l) => " " + l)
511
+ .join("\n"),
512
+ );
513
+ } else {
514
+ let apiKey: string | undefined;
515
+ try {
516
+ apiKey = await ensureApiKey();
517
+ } catch {
518
+ // Not logged in
519
+ }
520
+
521
+ const keyDisplay = apiKey ?? "mxk_your_api_key_here";
522
+ const mcpUrl = `${apiUrl}/mcp`;
523
+
524
+ console.log(chalk.gray(" Mode: remote server (recommended)\n"));
525
+
526
+ console.log(chalk.white(" For Claude Code:\n"));
527
+ console.log(
528
+ chalk.gray(
529
+ ` claude mcp add memax --transport http ${mcpUrl} --header "Authorization: Bearer ${keyDisplay}"`,
530
+ ),
531
+ );
532
+
533
+ console.log(chalk.white("\n For Cursor, Copilot, Gemini, Windsurf:\n"));
534
+ console.log(
535
+ JSON.stringify(
536
+ {
537
+ mcpServers: {
538
+ memax: {
539
+ type: "url",
540
+ url: mcpUrl,
541
+ headers: {
542
+ Authorization: `Bearer ${keyDisplay}`,
543
+ },
544
+ },
545
+ },
546
+ },
547
+ null,
548
+ 2,
549
+ )
550
+ .split("\n")
551
+ .map((l) => " " + l)
552
+ .join("\n"),
553
+ );
554
+
555
+ console.log(chalk.white("\n For Codex CLI (~/.codex/config.toml):\n"));
556
+ console.log(chalk.gray(` [mcp_servers.memax]`));
557
+ console.log(chalk.gray(` type = "url"`));
558
+ console.log(chalk.gray(` url = "${mcpUrl}"`));
559
+ console.log(chalk.gray(`\n [mcp_servers.memax.headers]`));
560
+ console.log(chalk.gray(` Authorization = "Bearer ${keyDisplay}"`));
561
+
562
+ if (apiKey) {
563
+ console.log(chalk.yellow("\n API key created: mcp-setup"));
564
+ console.log(chalk.gray(" Manage keys: memax auth list-keys"));
565
+ } else {
566
+ console.log(
567
+ chalk.yellow(
568
+ "\n Not logged in — replace mxk_your_api_key_here with a real key.",
569
+ ),
570
+ );
571
+ console.log(
572
+ chalk.gray(" Run: memax login && memax auth create-key --name mcp"),
573
+ );
574
+ }
575
+ }
576
+
577
+ console.log();
578
+ }
579
+
580
+ // --- Local MCP setup per agent ---
581
+
582
+ function setupMcp(agent: AgentDef, bin: MemaxBin): void {
583
+ // Claude Code has its own CLI for MCP management
584
+ if (agent.id === "claude-code") {
585
+ setupMcpClaudeCode(bin);
586
+ return;
587
+ }
588
+
589
+ mkdirSync(dirname(agent.configPath), { recursive: true });
590
+
591
+ if (agent.format === "toml") {
592
+ setupMcpToml(agent, bin);
593
+ return;
594
+ }
595
+
596
+ // JSON-based agents
597
+ let config: Record<string, unknown> = {};
598
+ if (existsSync(agent.configPath)) {
599
+ try {
600
+ config = JSON.parse(readFileSync(agent.configPath, "utf-8"));
601
+ } catch {
602
+ // Start fresh
603
+ }
604
+ }
605
+
606
+ const servers = (getNestedKey(config, agent.mcpKey) ?? {}) as Record<
607
+ string,
608
+ unknown
609
+ >;
610
+ servers.memax = {
611
+ command: bin.command,
612
+ args: [...bin.args, "mcp", "serve"],
613
+ };
614
+ setNestedKey(config, agent.mcpKey, servers);
615
+
616
+ writeFileSync(agent.configPath, JSON.stringify(config, null, 2) + "\n");
617
+ }
618
+
619
+ function setupMcpClaudeCode(bin: MemaxBin): void {
620
+ // Claude Code uses its own CLI for MCP — settings.json mcpServers is ignored
621
+ if (!commandExists("claude")) {
622
+ throw new Error("claude CLI not found in PATH");
623
+ }
624
+
625
+ // Remove existing first (idempotent)
626
+ try {
627
+ execSync("claude mcp remove memax", { stdio: "pipe" });
628
+ } catch {
629
+ // Not installed yet — fine
630
+ }
631
+
632
+ // claude mcp add <name> -- <command> [args...]
633
+ const allArgs = [...bin.args, "mcp", "serve"];
634
+ const cmd = `claude mcp add memax -- ${bin.command} ${allArgs.join(" ")}`;
635
+
636
+ try {
637
+ execSync(cmd, { stdio: "pipe" });
638
+ } catch (err) {
639
+ throw new Error(`claude mcp add failed: ${(err as Error).message}`);
640
+ }
641
+ }
642
+
643
+ function setupMcpToml(agent: AgentDef, bin: MemaxBin): void {
644
+ // Codex uses TOML — append or update the memax section
645
+ let content = "";
646
+ if (existsSync(agent.configPath)) {
647
+ content = readFileSync(agent.configPath, "utf-8");
648
+ }
649
+
650
+ // Remove existing memax section if present
651
+ content = content.replace(/\[mcp_servers\.memax\][\s\S]*?(?=\n\[|$)/, "");
652
+
653
+ const args = [...bin.args, "mcp", "serve"].map((a) => `"${a}"`).join(", ");
654
+
655
+ content = content.trim();
656
+ if (content) content += "\n\n";
657
+ content += `[mcp_servers.memax]\ncommand = "${bin.command}"\nargs = [${args}]\n`;
658
+
659
+ writeFileSync(agent.configPath, content);
660
+ }
661
+
662
+ // --- Hook setup ---
663
+
664
+ function setupHooks(agent: AgentDef, bin: MemaxBin): void {
665
+ if (agent.id === "claude-code") {
666
+ setupClaudeCodeHooks(agent, bin);
667
+ } else if (agent.id === "gemini") {
668
+ setupGeminiHooks(agent, bin);
669
+ }
670
+ }
671
+
672
+ function setupClaudeCodeHooks(agent: AgentDef, bin: MemaxBin): void {
673
+ const hookScript = writeHookScript(bin);
674
+
675
+ let config: Record<string, unknown> = {};
676
+ if (existsSync(agent.configPath)) {
677
+ try {
678
+ config = JSON.parse(readFileSync(agent.configPath, "utf-8"));
679
+ } catch {
680
+ // Start fresh
681
+ }
682
+ }
683
+
684
+ const hooks = (config.hooks ?? {}) as Record<string, unknown[]>;
685
+
686
+ // Remove existing memax hooks
687
+ if (hooks["UserPromptSubmit"]) {
688
+ hooks["UserPromptSubmit"] = (
689
+ hooks["UserPromptSubmit"] as Array<{
690
+ hooks?: Array<{ command?: string }>;
691
+ }>
692
+ ).filter((h) => !h.hooks?.some((hh) => hh.command?.includes("memax")));
693
+ }
694
+
695
+ hooks["UserPromptSubmit"] = [
696
+ ...((hooks["UserPromptSubmit"] as unknown[]) ?? []),
697
+ {
698
+ matcher: "",
699
+ hooks: [{ type: "command", command: hookScript, timeout: 30 }],
700
+ },
701
+ ];
702
+
703
+ // Stop hook: auto-capture session learnings on session end
704
+ const captureScript = writeCaptureHookScript(bin);
705
+ if (hooks["Stop"]) {
706
+ hooks["Stop"] = (
707
+ hooks["Stop"] as Array<{ hooks?: Array<{ command?: string }> }>
708
+ ).filter((h) => !h.hooks?.some((hh) => hh.command?.includes("memax")));
709
+ }
710
+ hooks["Stop"] = [
711
+ ...((hooks["Stop"] as unknown[]) ?? []),
712
+ {
713
+ matcher: "",
714
+ hooks: [{ type: "command", command: captureScript, timeout: 60 }],
715
+ },
716
+ ];
717
+
718
+ config.hooks = hooks;
719
+ writeFileSync(agent.configPath, JSON.stringify(config, null, 2) + "\n");
720
+ }
721
+
722
+ function setupGeminiHooks(agent: AgentDef, bin: MemaxBin): void {
723
+ const hookScript = writeHookScript(bin);
724
+
725
+ let config: Record<string, unknown> = {};
726
+ if (existsSync(agent.configPath)) {
727
+ try {
728
+ config = JSON.parse(readFileSync(agent.configPath, "utf-8"));
729
+ } catch {
730
+ // Start fresh
731
+ }
732
+ }
733
+
734
+ const hooks = (config.hooks ?? {}) as Record<string, unknown[]>;
735
+
736
+ // Remove existing memax hooks from both old ("Startup") and correct event
737
+ for (const event of ["Startup", "BeforeAgent"]) {
738
+ if (hooks[event]) {
739
+ hooks[event] = (
740
+ hooks[event] as Array<{ hooks?: Array<{ command?: string }> }>
741
+ ).filter((h) => !h.hooks?.some((hh) => hh.command?.includes("memax")));
742
+ if ((hooks[event] as unknown[]).length === 0) delete hooks[event];
743
+ }
744
+ }
745
+
746
+ // BeforeAgent fires after user submits a prompt — equivalent to Claude Code's PrePromptSubmit
747
+ hooks["BeforeAgent"] = [
748
+ ...((hooks["BeforeAgent"] as unknown[]) ?? []),
749
+ {
750
+ matcher: "",
751
+ hooks: [{ type: "command", command: hookScript, timeout: 30 }],
752
+ },
753
+ ];
754
+
755
+ config.hooks = hooks;
756
+ writeFileSync(agent.configPath, JSON.stringify(config, null, 2) + "\n");
757
+ }
758
+
759
+ // --- Teardown helpers ---
760
+
761
+ function removeMcpJson(agent: AgentDef): boolean {
762
+ if (!existsSync(agent.configPath)) return false;
763
+
764
+ try {
765
+ const config = JSON.parse(readFileSync(agent.configPath, "utf-8"));
766
+ const servers = getNestedKey(config, agent.mcpKey);
767
+ if (!servers?.memax) return false;
768
+
769
+ delete servers.memax;
770
+ if (Object.keys(servers).length === 0)
771
+ deleteNestedKey(config, agent.mcpKey);
772
+
773
+ writeFileSync(agent.configPath, JSON.stringify(config, null, 2) + "\n");
774
+ console.log(chalk.gray(` Removed MCP from ${agent.name}`));
775
+ return true;
776
+ } catch {
777
+ return false;
778
+ }
779
+ }
780
+
781
+ function removeMcpToml(agent: AgentDef): boolean {
782
+ if (!existsSync(agent.configPath)) return false;
783
+
784
+ let content = readFileSync(agent.configPath, "utf-8");
785
+ const before = content;
786
+ content = content.replace(/\[mcp_servers\.memax\][\s\S]*?(?=\n\[|$)/, "");
787
+
788
+ if (content === before) return false;
789
+
790
+ writeFileSync(agent.configPath, content.trim() + "\n");
791
+ console.log(chalk.gray(` Removed MCP from ${agent.name}`));
792
+ return true;
793
+ }
794
+
795
+ function removeHooks(agent: AgentDef): boolean {
796
+ if (!existsSync(agent.configPath)) return false;
797
+
798
+ try {
799
+ const config = JSON.parse(readFileSync(agent.configPath, "utf-8"));
800
+ const hooks = config.hooks as Record<string, unknown[]> | undefined;
801
+ if (!hooks) return false;
802
+
803
+ let removed = false;
804
+ for (const event of Object.keys(hooks)) {
805
+ const before = (hooks[event] as unknown[]).length;
806
+ hooks[event] = (
807
+ hooks[event] as Array<{
808
+ hooks?: Array<{ command?: string }>;
809
+ command?: string;
810
+ }>
811
+ ).filter(
812
+ (h) =>
813
+ !h.command?.includes("memax") &&
814
+ !h.hooks?.some((hh) => hh.command?.includes("memax")),
815
+ );
816
+ if ((hooks[event] as unknown[]).length < before) removed = true;
817
+ if ((hooks[event] as unknown[]).length === 0) delete hooks[event];
818
+ }
819
+
820
+ if (Object.keys(hooks).length === 0) delete config.hooks;
821
+ if (removed) {
822
+ writeFileSync(agent.configPath, JSON.stringify(config, null, 2) + "\n");
823
+ console.log(chalk.gray(` Removed hooks from ${agent.name}`));
824
+ }
825
+ return removed;
826
+ } catch {
827
+ return false;
828
+ }
829
+ }
830
+
831
+ // --- Shared helpers ---
832
+
833
+ interface MemaxBin {
834
+ command: string;
835
+ args: string[];
836
+ shell: string;
837
+ }
838
+
839
+ function resolveMemaxBin(): MemaxBin | null {
840
+ // 1. Global install — use absolute path so agents find it without shell PATH
841
+ if (commandExists("memax")) {
842
+ try {
843
+ const which = platform() === "win32" ? "where memax" : "which memax";
844
+ const absPath = execSync(which, { encoding: "utf-8", stdio: "pipe" })
845
+ .trim()
846
+ .split("\n")[0];
847
+ if (absPath) {
848
+ return { command: absPath, args: [], shell: absPath };
849
+ }
850
+ } catch {
851
+ // fall through
852
+ }
853
+ return { command: "memax", args: [], shell: "memax" };
854
+ }
855
+
856
+ // 2. Local repo build (faster than npx, always up-to-date during dev)
857
+ const localBuild = join(process.cwd(), "packages", "cli", "dist", "index.js");
858
+ if (existsSync(localBuild)) {
859
+ return {
860
+ command: "node",
861
+ args: [localBuild],
862
+ shell: `node ${localBuild}`,
863
+ };
864
+ }
865
+
866
+ // 3. npx as last resort (slow startup — agents may timeout on first run)
867
+ try {
868
+ execSync("npx --yes memax-cli --version", {
869
+ encoding: "utf-8",
870
+ timeout: 15000,
871
+ stdio: "pipe",
872
+ });
873
+ return {
874
+ command: "npx",
875
+ args: ["-y", "memax-cli"],
876
+ shell: "npx -y memax-cli",
877
+ };
878
+ } catch {
879
+ // npx failed
880
+ }
881
+
882
+ return null;
883
+ }
884
+
885
+ function commandExists(cmd: string): boolean {
886
+ try {
887
+ const which = platform() === "win32" ? "where" : "which";
888
+ execSync(`${which} ${cmd}`, { stdio: "pipe" });
889
+ return true;
890
+ } catch {
891
+ return false;
892
+ }
893
+ }
894
+
895
+ // Nested key helpers for configs like openclaw's "mcp.servers"
896
+ function getNestedKey(
897
+ obj: Record<string, unknown>,
898
+ key: string,
899
+ ): Record<string, unknown> | undefined {
900
+ const parts = key.split(".");
901
+ let current: unknown = obj;
902
+ for (const part of parts) {
903
+ if (current == null || typeof current !== "object") return undefined;
904
+ current = (current as Record<string, unknown>)[part];
905
+ }
906
+ return current as Record<string, unknown> | undefined;
907
+ }
908
+
909
+ function setNestedKey(
910
+ obj: Record<string, unknown>,
911
+ key: string,
912
+ value: unknown,
913
+ ): void {
914
+ const parts = key.split(".");
915
+ let current: Record<string, unknown> = obj;
916
+ for (let i = 0; i < parts.length - 1; i++) {
917
+ if (!(parts[i] in current) || typeof current[parts[i]] !== "object") {
918
+ current[parts[i]] = {};
919
+ }
920
+ current = current[parts[i]] as Record<string, unknown>;
921
+ }
922
+ current[parts[parts.length - 1]] = value;
923
+ }
924
+
925
+ function deleteNestedKey(obj: Record<string, unknown>, key: string): void {
926
+ const parts = key.split(".");
927
+ let current: Record<string, unknown> = obj;
928
+ for (let i = 0; i < parts.length - 1; i++) {
929
+ if (!(parts[i] in current) || typeof current[parts[i]] !== "object") return;
930
+ current = current[parts[i]] as Record<string, unknown>;
931
+ }
932
+ delete current[parts[parts.length - 1]];
933
+ }
934
+
935
+ // --- Instruction injection ---
936
+
937
+ const MEMAX_INSTRUCTION_BLOCK = `
938
+ <!-- memax:start -->
939
+ ## Memax — Persistent Memory
940
+
941
+ You have access to Memax, a persistent cloud knowledge hub shared across all your AI agents.
942
+ Use it proactively — don't wait for the user to ask.
943
+
944
+ **At session start:** Use memax_recall to check for relevant context about the current project or task.
945
+ **During work:** When you discover important decisions, architecture details, debugging solutions,
946
+ or useful context — use memax_push to save them for future sessions.
947
+ **At session end:** Summarize key decisions, learnings, or context worth remembering and push them.
948
+
949
+ **What to remember:** Architecture decisions, API conventions, deployment processes, debugging
950
+ solutions, team preferences, project-specific knowledge. If you'd want to know it in a future
951
+ session, push it now.
952
+
953
+ **What NOT to remember:** Ephemeral task details, file contents (they're in git), obvious things.
954
+
955
+ Available tools: memax_recall (search), memax_push (save), memax_get (read full note),
956
+ memax_search (browse), memax_forget (delete outdated memories).
957
+ <!-- memax:end -->
958
+ `.trim();
959
+
960
+ function injectInstructions(filePath: string): void {
961
+ mkdirSync(dirname(filePath), { recursive: true });
962
+
963
+ let content = "";
964
+ if (existsSync(filePath)) {
965
+ content = readFileSync(filePath, "utf-8");
966
+ }
967
+
968
+ // Remove existing memax block (idempotent)
969
+ content = content.replace(
970
+ /\n?<!-- memax:start -->[\s\S]*?<!-- memax:end -->\n?/,
971
+ "",
972
+ );
973
+
974
+ // Append the block
975
+ content = content.trimEnd() + "\n\n" + MEMAX_INSTRUCTION_BLOCK + "\n";
976
+
977
+ writeFileSync(filePath, content);
978
+ }
979
+
980
+ function removeInstructions(filePath: string): boolean {
981
+ if (!existsSync(filePath)) return false;
982
+
983
+ const content = readFileSync(filePath, "utf-8");
984
+ const cleaned = content.replace(
985
+ /\n?<!-- memax:start -->[\s\S]*?<!-- memax:end -->\n?/,
986
+ "",
987
+ );
988
+
989
+ if (cleaned === content) return false;
990
+
991
+ writeFileSync(filePath, cleaned.trimEnd() + "\n");
992
+ return true;
993
+ }
994
+
995
+ function writeHookScript(bin: MemaxBin): string {
996
+ const hooksDir = join(homedir(), ".memax", "hooks");
997
+ mkdirSync(hooksDir, { recursive: true });
998
+
999
+ const isWindows = platform() === "win32";
1000
+ const scriptName = isWindows ? "context-inject.cmd" : "context-inject.sh";
1001
+ const scriptPath = join(hooksDir, scriptName);
1002
+
1003
+ if (isWindows) {
1004
+ writeFileSync(scriptPath, WIN_HOOK.replace(/\$MEMAX/g, bin.shell));
1005
+ } else {
1006
+ writeFileSync(scriptPath, UNIX_HOOK.replace(/\$MEMAX/g, bin.shell));
1007
+ chmodSync(scriptPath, 0o755);
1008
+ }
1009
+
1010
+ return scriptPath;
1011
+ }
1012
+
1013
+ function writeCaptureHookScript(bin: MemaxBin): string {
1014
+ const hooksDir = join(homedir(), ".memax", "hooks");
1015
+ mkdirSync(hooksDir, { recursive: true });
1016
+
1017
+ const isWindows = platform() === "win32";
1018
+ const scriptName = isWindows ? "session-capture.cmd" : "session-capture.sh";
1019
+ const scriptPath = join(hooksDir, scriptName);
1020
+
1021
+ if (isWindows) {
1022
+ writeFileSync(scriptPath, WIN_CAPTURE_HOOK.replace(/\$MEMAX/g, bin.shell));
1023
+ } else {
1024
+ writeFileSync(scriptPath, UNIX_CAPTURE_HOOK.replace(/\$MEMAX/g, bin.shell));
1025
+ chmodSync(scriptPath, 0o755);
1026
+ }
1027
+
1028
+ return scriptPath;
1029
+ }
1030
+
1031
+ function printUsage(): void {
1032
+ const agents = getAgents();
1033
+ const detected = agents.filter((a) => a.detect());
1034
+
1035
+ console.log(
1036
+ chalk.bold("\n Memax Setup — Configure AI Agent Integrations\n"),
1037
+ );
1038
+
1039
+ if (detected.length > 0) {
1040
+ console.log(chalk.gray(" Detected agents:"));
1041
+ for (const a of detected) {
1042
+ const hookNote = a.hasHooks ? " (MCP + hooks)" : " (MCP)";
1043
+ console.log(chalk.white(` • ${a.name}${hookNote}`));
1044
+ }
1045
+ console.log();
1046
+ }
1047
+
1048
+ console.log(chalk.gray(" Usage:\n"));
1049
+ console.log(
1050
+ chalk.gray(
1051
+ " memax setup --mcp Remote MCP server for all detected agents",
1052
+ ),
1053
+ );
1054
+ console.log(
1055
+ chalk.gray(
1056
+ " memax setup --instructions Inject memax usage instructions into agent configs",
1057
+ ),
1058
+ );
1059
+ console.log(
1060
+ chalk.gray(
1061
+ " memax setup --all MCP + hooks + instructions",
1062
+ ),
1063
+ );
1064
+ console.log(
1065
+ chalk.gray(
1066
+ " memax setup --mcp --local Use local CLI instead of remote server",
1067
+ ),
1068
+ );
1069
+ console.log(
1070
+ chalk.gray(" memax setup --print Print MCP config to copy/paste"),
1071
+ );
1072
+ console.log(chalk.gray(" memax setup --mcp --only claude-code,cursor"));
1073
+ console.log(
1074
+ chalk.gray(" memax teardown Remove all integrations\n"),
1075
+ );
1076
+
1077
+ console.log(chalk.gray(" Supported agents:"));
1078
+ for (const a of agents) {
1079
+ const status = a.detect()
1080
+ ? chalk.green("detected")
1081
+ : chalk.gray("not found");
1082
+ console.log(
1083
+ chalk.gray(` ${a.id.padEnd(14)} ${a.name.padEnd(20)} ${status}`),
1084
+ );
1085
+ }
1086
+ console.log();
1087
+ }
1088
+
1089
+ const UNIX_HOOK = `#!/bin/bash
1090
+ # Memax context injection — installed by: memax setup --hooks
1091
+ set -e
1092
+ INPUT=$(cat)
1093
+ PROMPT=$(echo "$INPUT" | jq -r '.prompt // empty')
1094
+ CWD=$(echo "$INPUT" | jq -r '.cwd // empty')
1095
+ if [ -z "$PROMPT" ] || [ \${#PROMPT} -lt 10 ]; then exit 0; fi
1096
+ case "$PROMPT" in [Yy]|[Yy]es|[Nn]|[Nn]o|ok|OK|sure|Sure|thanks|Thanks|y|n) exit 0 ;; esac
1097
+ if [ -n "$CWD" ]; then cd "$CWD" 2>/dev/null || true; fi
1098
+ CONTEXT=$($MEMAX recall "$PROMPT" --hook --limit 5 --max-tokens 3000 2>/dev/null) || exit 0
1099
+ if [ -n "$CONTEXT" ] && [ "$CONTEXT" != "<memax-context>" ]; then echo "$CONTEXT"; fi
1100
+ exit 0
1101
+ `;
1102
+
1103
+ const WIN_HOOK = `@echo off
1104
+ REM Memax context injection — installed by: memax setup --hooks
1105
+ set /p INPUT=
1106
+ for /f "tokens=*" %%a in ('echo %INPUT% ^| jq -r ".prompt // empty"') do set PROMPT=%%a
1107
+ if "%PROMPT%"=="" exit /b 0
1108
+ $MEMAX recall "%PROMPT%" --hook --limit 5 --max-tokens 3000 2>nul
1109
+ exit /b 0
1110
+ `;
1111
+
1112
+ const UNIX_CAPTURE_HOOK = `#!/bin/bash
1113
+ # Memax session capture — installed by: memax setup --hooks
1114
+ # Fires on session end (Stop hook). Pipes session data to memax capture-session.
1115
+ set -e
1116
+ INPUT=$(cat)
1117
+ SUMMARY=$(echo "$INPUT" | jq -r '.transcript // .summary // empty' 2>/dev/null)
1118
+ if [ -z "$SUMMARY" ]; then exit 0; fi
1119
+ if [ \${#SUMMARY} -lt 50 ]; then exit 0; fi
1120
+ echo "$SUMMARY" | $MEMAX capture-session --agent claude-code 2>/dev/null || true
1121
+ exit 0
1122
+ `;
1123
+
1124
+ const WIN_CAPTURE_HOOK = `@echo off
1125
+ REM Memax session capture — installed by: memax setup --hooks
1126
+ set /p INPUT=
1127
+ $MEMAX capture-session --agent claude-code --summary "%INPUT%" 2>nul
1128
+ exit /b 0
1129
+ `;