memax-cli 0.1.0-alpha.5 → 0.1.0-alpha.7

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.
@@ -9,6 +9,8 @@ import {
9
9
  import { join, dirname } from "node:path";
10
10
  import { homedir, platform } from "node:os";
11
11
  import { execSync } from "node:child_process";
12
+ import { apiPost } from "../lib/api.js";
13
+ import { loadConfig } from "../lib/config.js";
12
14
 
13
15
  // --- Agent definitions ---
14
16
 
@@ -25,7 +27,7 @@ interface AgentDef {
25
27
 
26
28
  function getAgents(): AgentDef[] {
27
29
  const home = homedir();
28
- const isWin = platform() === "win32";
30
+ const cwd = process.cwd();
29
31
 
30
32
  return [
31
33
  {
@@ -97,6 +99,26 @@ function getAgents(): AgentDef[] {
97
99
  hasHooks: false,
98
100
  detect: () => existsSync(join(home, ".codex")) || commandExists("codex"),
99
101
  },
102
+ {
103
+ name: "OpenClaw",
104
+ id: "openclaw",
105
+ configPath: join(home, ".openclaw", "openclaw.json"),
106
+ format: "json-mcpServers",
107
+ mcpKey: "mcp.servers",
108
+ hasHooks: false,
109
+ detect: () =>
110
+ existsSync(join(home, ".openclaw")) || commandExists("openclaw"),
111
+ },
112
+ {
113
+ name: "OpenCode",
114
+ id: "opencode",
115
+ configPath: join(cwd, ".opencode", "opencode.jsonc"),
116
+ format: "json-mcpServers",
117
+ mcpKey: "mcp",
118
+ hasHooks: false,
119
+ detect: () =>
120
+ existsSync(join(cwd, ".opencode")) || commandExists("opencode"),
121
+ },
100
122
  ];
101
123
  }
102
124
 
@@ -106,6 +128,8 @@ interface SetupOptions {
106
128
  mcp?: boolean;
107
129
  hooks?: boolean;
108
130
  all?: boolean;
131
+ local?: boolean;
132
+ print?: boolean;
109
133
  only?: string;
110
134
  skip?: string;
111
135
  }
@@ -114,20 +138,46 @@ export async function setupCommand(options: SetupOptions): Promise<void> {
114
138
  const enableMcp = options.all || options.mcp;
115
139
  const enableHooks = options.all || options.hooks;
116
140
 
117
- if (!enableMcp && !enableHooks) {
141
+ if (!enableMcp && !enableHooks && !options.print) {
118
142
  printUsage();
119
143
  return;
120
144
  }
121
145
 
122
- // Resolve memax binary
123
- const memaxBin = resolveMemaxBin();
124
- if (!memaxBin) {
125
- console.error(
126
- chalk.red(
127
- "\n Could not find memax binary.\n Install globally: npm install -g memax-cli@alpha\n",
128
- ),
129
- );
130
- process.exit(1);
146
+ // --print: just output config JSON for manual copy/paste
147
+ if (options.print) {
148
+ await printMcpConfigs(options.local ?? false);
149
+ return;
150
+ }
151
+
152
+ // Remote mode (default): need API key for auth
153
+ const useRemote = !options.local;
154
+ let apiKey: string | undefined;
155
+
156
+ if (useRemote && enableMcp) {
157
+ apiKey = await ensureApiKey();
158
+ if (!apiKey) {
159
+ console.error(
160
+ chalk.red(
161
+ "\n Could not create API key. Log in first: memax login\n" +
162
+ " Or use --local for local MCP server.\n",
163
+ ),
164
+ );
165
+ process.exit(1);
166
+ }
167
+ }
168
+
169
+ // Local mode: need memax binary
170
+ let memaxBin: MemaxBin | null = null;
171
+ if (!useRemote || enableHooks) {
172
+ memaxBin = resolveMemaxBin();
173
+ if (!memaxBin && !useRemote) {
174
+ console.error(
175
+ chalk.red(
176
+ "\n Could not find memax binary.\n Install globally: npm install -g memax-cli@alpha\n",
177
+ ),
178
+ );
179
+ process.exit(1);
180
+ }
131
181
  }
132
182
 
133
183
  // Filter agents
@@ -158,6 +208,11 @@ export async function setupCommand(options: SetupOptions): Promise<void> {
158
208
  }
159
209
 
160
210
  console.log(chalk.bold("\n Memax Setup\n"));
211
+ if (useRemote) {
212
+ console.log(chalk.gray(" Mode: remote server (recommended)\n"));
213
+ } else {
214
+ console.log(chalk.gray(" Mode: local CLI\n"));
215
+ }
161
216
 
162
217
  const results: { agent: string; changes: string[] }[] = [];
163
218
 
@@ -167,8 +222,12 @@ export async function setupCommand(options: SetupOptions): Promise<void> {
167
222
  // MCP setup
168
223
  if (enableMcp) {
169
224
  try {
170
- setupMcp(agent, memaxBin);
171
- changes.push("MCP server");
225
+ if (useRemote) {
226
+ setupMcpRemote(agent, apiKey!);
227
+ } else {
228
+ setupMcp(agent, memaxBin!);
229
+ }
230
+ changes.push(useRemote ? "MCP server (remote)" : "MCP server (local)");
172
231
  } catch (err) {
173
232
  console.log(
174
233
  chalk.red(
@@ -178,8 +237,8 @@ export async function setupCommand(options: SetupOptions): Promise<void> {
178
237
  }
179
238
  }
180
239
 
181
- // Hook setup (only for agents that support it)
182
- if (enableHooks && agent.hasHooks) {
240
+ // Hook setup (only for agents that support it — needs local binary)
241
+ if (enableHooks && agent.hasHooks && memaxBin) {
183
242
  try {
184
243
  setupHooks(agent, memaxBin);
185
244
  changes.push("Context injection hook");
@@ -300,7 +359,182 @@ export async function teardownCommand(options: {
300
359
  );
301
360
  }
302
361
 
303
- // --- MCP setup per agent ---
362
+ // --- Remote MCP setup ---
363
+
364
+ async function ensureApiKey(): Promise<string | undefined> {
365
+ try {
366
+ const result = await apiPost<{ key: string }>("/v1/auth/api-keys", {
367
+ name: "mcp-setup",
368
+ expires_in_days: 0, // no expiry
369
+ });
370
+ return result.key;
371
+ } catch {
372
+ return undefined;
373
+ }
374
+ }
375
+
376
+ function getApiUrl(): string {
377
+ return loadConfig().api_url;
378
+ }
379
+
380
+ function setupMcpRemote(agent: AgentDef, apiKey: string): void {
381
+ const mcpUrl = `${getApiUrl()}/mcp`;
382
+
383
+ // Claude Code uses its own CLI
384
+ if (agent.id === "claude-code") {
385
+ if (!commandExists("claude")) {
386
+ throw new Error("claude CLI not found in PATH");
387
+ }
388
+ try {
389
+ execSync("claude mcp remove memax", { stdio: "pipe" });
390
+ } catch {
391
+ // Not installed yet
392
+ }
393
+ // Claude Code HTTP transport
394
+ execSync(
395
+ `claude mcp add memax --transport http ${mcpUrl} --header "Authorization: Bearer ${apiKey}"`,
396
+ { stdio: "pipe" },
397
+ );
398
+ return;
399
+ }
400
+
401
+ // Codex TOML
402
+ if (agent.format === "toml") {
403
+ mkdirSync(dirname(agent.configPath), { recursive: true });
404
+ let content = "";
405
+ if (existsSync(agent.configPath)) {
406
+ content = readFileSync(agent.configPath, "utf-8");
407
+ }
408
+ content = content.replace(/\[mcp_servers\.memax\][\s\S]*?(?=\n\[|$)/, "");
409
+ content = content.trim();
410
+ if (content) content += "\n\n";
411
+ content += `[mcp_servers.memax]\ntype = "url"\nurl = "${mcpUrl}"\n\n[mcp_servers.memax.headers]\nAuthorization = "Bearer ${apiKey}"\n`;
412
+ writeFileSync(agent.configPath, content);
413
+ return;
414
+ }
415
+
416
+ // JSON-based agents
417
+ mkdirSync(dirname(agent.configPath), { recursive: true });
418
+ let config: Record<string, unknown> = {};
419
+ if (existsSync(agent.configPath)) {
420
+ try {
421
+ config = JSON.parse(readFileSync(agent.configPath, "utf-8"));
422
+ } catch {
423
+ // Start fresh
424
+ }
425
+ }
426
+
427
+ const servers = (getNestedKey(config, agent.mcpKey) ?? {}) as Record<
428
+ string,
429
+ unknown
430
+ >;
431
+ servers.memax = {
432
+ type: "url",
433
+ url: mcpUrl,
434
+ headers: {
435
+ Authorization: `Bearer ${apiKey}`,
436
+ },
437
+ };
438
+ setNestedKey(config, agent.mcpKey, servers);
439
+ writeFileSync(agent.configPath, JSON.stringify(config, null, 2) + "\n");
440
+ }
441
+
442
+ async function printMcpConfigs(local: boolean): Promise<void> {
443
+ const apiUrl = getApiUrl();
444
+
445
+ console.log(chalk.bold("\n Memax MCP Configuration\n"));
446
+
447
+ if (local) {
448
+ const bin = resolveMemaxBin();
449
+ const cmd = bin ? bin.command : "memax";
450
+ const args = bin ? [...bin.args, "mcp", "serve"] : ["mcp", "serve"];
451
+
452
+ console.log(chalk.gray(" Mode: local (stdio)\n"));
453
+ console.log(
454
+ chalk.white(" For most agents (Claude Code, Cursor, Gemini, etc.):\n"),
455
+ );
456
+ console.log(
457
+ JSON.stringify(
458
+ {
459
+ mcpServers: {
460
+ memax: { command: cmd, args },
461
+ },
462
+ },
463
+ null,
464
+ 2,
465
+ )
466
+ .split("\n")
467
+ .map((l) => " " + l)
468
+ .join("\n"),
469
+ );
470
+ } else {
471
+ let apiKey: string | undefined;
472
+ try {
473
+ apiKey = await ensureApiKey();
474
+ } catch {
475
+ // Not logged in
476
+ }
477
+
478
+ const keyDisplay = apiKey ?? "mxk_your_api_key_here";
479
+ const mcpUrl = `${apiUrl}/mcp`;
480
+
481
+ console.log(chalk.gray(" Mode: remote server (recommended)\n"));
482
+
483
+ console.log(chalk.white(" For Claude Code:\n"));
484
+ console.log(
485
+ chalk.gray(
486
+ ` claude mcp add memax --transport http ${mcpUrl} --header "Authorization: Bearer ${keyDisplay}"`,
487
+ ),
488
+ );
489
+
490
+ console.log(chalk.white("\n For Cursor, Copilot, Gemini, Windsurf:\n"));
491
+ console.log(
492
+ JSON.stringify(
493
+ {
494
+ mcpServers: {
495
+ memax: {
496
+ type: "url",
497
+ url: mcpUrl,
498
+ headers: {
499
+ Authorization: `Bearer ${keyDisplay}`,
500
+ },
501
+ },
502
+ },
503
+ },
504
+ null,
505
+ 2,
506
+ )
507
+ .split("\n")
508
+ .map((l) => " " + l)
509
+ .join("\n"),
510
+ );
511
+
512
+ console.log(chalk.white("\n For Codex CLI (~/.codex/config.toml):\n"));
513
+ console.log(chalk.gray(` [mcp_servers.memax]`));
514
+ console.log(chalk.gray(` type = "url"`));
515
+ console.log(chalk.gray(` url = "${mcpUrl}"`));
516
+ console.log(chalk.gray(`\n [mcp_servers.memax.headers]`));
517
+ console.log(chalk.gray(` Authorization = "Bearer ${keyDisplay}"`));
518
+
519
+ if (apiKey) {
520
+ console.log(chalk.yellow("\n API key created: mcp-setup"));
521
+ console.log(chalk.gray(" Manage keys: memax auth list-keys"));
522
+ } else {
523
+ console.log(
524
+ chalk.yellow(
525
+ "\n Not logged in — replace mxk_your_api_key_here with a real key.",
526
+ ),
527
+ );
528
+ console.log(
529
+ chalk.gray(" Run: memax login && memax auth create-key --name mcp"),
530
+ );
531
+ }
532
+ }
533
+
534
+ console.log();
535
+ }
536
+
537
+ // --- Local MCP setup per agent ---
304
538
 
305
539
  function setupMcp(agent: AgentDef, bin: MemaxBin): void {
306
540
  // Claude Code has its own CLI for MCP management
@@ -326,12 +560,15 @@ function setupMcp(agent: AgentDef, bin: MemaxBin): void {
326
560
  }
327
561
  }
328
562
 
329
- const servers = (config[agent.mcpKey] ?? {}) as Record<string, unknown>;
563
+ const servers = (getNestedKey(config, agent.mcpKey) ?? {}) as Record<
564
+ string,
565
+ unknown
566
+ >;
330
567
  servers.memax = {
331
568
  command: bin.command,
332
569
  args: [...bin.args, "mcp", "serve"],
333
570
  };
334
- config[agent.mcpKey] = servers;
571
+ setNestedKey(config, agent.mcpKey, servers);
335
572
 
336
573
  writeFileSync(agent.configPath, JSON.stringify(config, null, 2) + "\n");
337
574
  }
@@ -468,11 +705,12 @@ function removeMcpJson(agent: AgentDef): boolean {
468
705
 
469
706
  try {
470
707
  const config = JSON.parse(readFileSync(agent.configPath, "utf-8"));
471
- const servers = config[agent.mcpKey] as Record<string, unknown> | undefined;
708
+ const servers = getNestedKey(config, agent.mcpKey);
472
709
  if (!servers?.memax) return false;
473
710
 
474
711
  delete servers.memax;
475
- if (Object.keys(servers).length === 0) delete config[agent.mcpKey];
712
+ if (Object.keys(servers).length === 0)
713
+ deleteNestedKey(config, agent.mcpKey);
476
714
 
477
715
  writeFileSync(agent.configPath, JSON.stringify(config, null, 2) + "\n");
478
716
  console.log(chalk.gray(` Removed MCP from ${agent.name}`));
@@ -596,6 +834,46 @@ function commandExists(cmd: string): boolean {
596
834
  }
597
835
  }
598
836
 
837
+ // Nested key helpers for configs like openclaw's "mcp.servers"
838
+ function getNestedKey(
839
+ obj: Record<string, unknown>,
840
+ key: string,
841
+ ): Record<string, unknown> | undefined {
842
+ const parts = key.split(".");
843
+ let current: unknown = obj;
844
+ for (const part of parts) {
845
+ if (current == null || typeof current !== "object") return undefined;
846
+ current = (current as Record<string, unknown>)[part];
847
+ }
848
+ return current as Record<string, unknown> | undefined;
849
+ }
850
+
851
+ function setNestedKey(
852
+ obj: Record<string, unknown>,
853
+ key: string,
854
+ value: unknown,
855
+ ): void {
856
+ const parts = key.split(".");
857
+ let current: Record<string, unknown> = obj;
858
+ for (let i = 0; i < parts.length - 1; i++) {
859
+ if (!(parts[i] in current) || typeof current[parts[i]] !== "object") {
860
+ current[parts[i]] = {};
861
+ }
862
+ current = current[parts[i]] as Record<string, unknown>;
863
+ }
864
+ current[parts[parts.length - 1]] = value;
865
+ }
866
+
867
+ function deleteNestedKey(obj: Record<string, unknown>, key: string): void {
868
+ const parts = key.split(".");
869
+ let current: Record<string, unknown> = obj;
870
+ for (let i = 0; i < parts.length - 1; i++) {
871
+ if (!(parts[i] in current) || typeof current[parts[i]] !== "object") return;
872
+ current = current[parts[i]] as Record<string, unknown>;
873
+ }
874
+ delete current[parts[parts.length - 1]];
875
+ }
876
+
599
877
  function writeHookScript(bin: MemaxBin): string {
600
878
  const hooksDir = join(homedir(), ".memax", "hooks");
601
879
  mkdirSync(hooksDir, { recursive: true });
@@ -634,14 +912,21 @@ function printUsage(): void {
634
912
  console.log(chalk.gray(" Usage:\n"));
635
913
  console.log(
636
914
  chalk.gray(
637
- " memax setup --mcp MCP tools for all detected agents",
915
+ " memax setup --mcp Remote MCP server for all detected agents (default)",
638
916
  ),
639
917
  );
640
918
  console.log(
641
919
  chalk.gray(" memax setup --all MCP + hooks (where supported)"),
642
920
  );
921
+ console.log(
922
+ chalk.gray(
923
+ " memax setup --mcp --local Use local CLI instead of remote server",
924
+ ),
925
+ );
926
+ console.log(
927
+ chalk.gray(" memax setup --print Print MCP config to copy/paste"),
928
+ );
643
929
  console.log(chalk.gray(" memax setup --mcp --only claude-code,cursor"));
644
- console.log(chalk.gray(" memax setup --all --skip codex"));
645
930
  console.log(
646
931
  chalk.gray(" memax teardown Remove all integrations\n"),
647
932
  );
@@ -6,6 +6,7 @@ import {
6
6
  watch,
7
7
  existsSync,
8
8
  } from "node:fs";
9
+ import { createInterface } from "node:readline";
9
10
  import { join, relative, extname, resolve, basename } from "node:path";
10
11
  import { homedir } from "node:os";
11
12
  import { apiPost } from "../lib/api.js";
@@ -17,6 +18,7 @@ interface SyncOptions {
17
18
  watch?: boolean;
18
19
  ignore?: string;
19
20
  agentMemory?: boolean;
21
+ yes?: boolean;
20
22
  }
21
23
 
22
24
  const DEFAULT_IGNORE = new Set([
@@ -85,6 +87,21 @@ export async function syncCommand(
85
87
  }
86
88
 
87
89
  console.log(chalk.gray(`Found ${files.length} files to sync`));
90
+
91
+ // Confirm if many files (>10) unless -y is passed
92
+ if (files.length > 10 && !options.yes) {
93
+ console.log(
94
+ chalk.yellow(
95
+ `\n This will push ${files.length} files. Continue? (y/N) `,
96
+ ),
97
+ );
98
+ const confirmed = await confirmSync();
99
+ if (!confirmed) {
100
+ console.log(chalk.gray(" Cancelled.\n"));
101
+ return;
102
+ }
103
+ }
104
+
88
105
  console.log();
89
106
 
90
107
  let pushed = 0;
@@ -296,6 +313,79 @@ function discoverAgentMemoryFiles(): AgentMemoryLocation[] {
296
313
  label: "./.codex/instructions.md",
297
314
  path: join(cwd, ".codex", "instructions.md"),
298
315
  });
316
+ locations.push({
317
+ label: "~/.codex/AGENTS.md",
318
+ path: join(home, ".codex", "AGENTS.md"),
319
+ });
320
+
321
+ // Gemini CLI
322
+ locations.push({
323
+ label: "~/.gemini/GEMINI.md",
324
+ path: join(home, ".gemini", "GEMINI.md"),
325
+ });
326
+ locations.push({ label: "./GEMINI.md", path: join(cwd, "GEMINI.md") });
327
+
328
+ // GitHub Copilot
329
+ locations.push({
330
+ label: "./.github/copilot-instructions.md",
331
+ path: join(cwd, ".github", "copilot-instructions.md"),
332
+ });
333
+
334
+ // Windsurf
335
+ locations.push({
336
+ label: "./.windsurfrules",
337
+ path: join(cwd, ".windsurfrules"),
338
+ });
339
+ const windsurfRulesDir = join(cwd, ".windsurf", "rules");
340
+ if (existsSync(windsurfRulesDir)) {
341
+ try {
342
+ for (const file of readdirSync(windsurfRulesDir)) {
343
+ if (file.endsWith(".md")) {
344
+ locations.push({
345
+ label: `./.windsurf/rules/${file}`,
346
+ path: join(windsurfRulesDir, file),
347
+ });
348
+ }
349
+ }
350
+ } catch {
351
+ // Skip on error
352
+ }
353
+ }
354
+
355
+ // OpenClaw memory
356
+ const openclawMemoryDir = join(home, ".openclaw", "memory");
357
+ if (existsSync(openclawMemoryDir)) {
358
+ try {
359
+ for (const file of readdirSync(openclawMemoryDir)) {
360
+ if (file.endsWith(".md") || file.endsWith(".json")) {
361
+ locations.push({
362
+ label: `~/.openclaw/memory/${file}`,
363
+ path: join(openclawMemoryDir, file),
364
+ });
365
+ }
366
+ }
367
+ } catch {
368
+ // Skip on error
369
+ }
370
+ }
371
+
372
+ // OpenCode (anomalyco) context
373
+ const opencodePath = join(cwd, ".opencode");
374
+ if (existsSync(opencodePath)) {
375
+ // Check for any markdown context files in .opencode/
376
+ try {
377
+ for (const file of readdirSync(opencodePath)) {
378
+ if (file.endsWith(".md")) {
379
+ locations.push({
380
+ label: `./.opencode/${file}`,
381
+ path: join(opencodePath, file),
382
+ });
383
+ }
384
+ }
385
+ } catch {
386
+ // Skip on error
387
+ }
388
+ }
299
389
 
300
390
  // Generic agent config files in current directory
301
391
  locations.push({ label: "./AGENTS.md", path: join(cwd, "AGENTS.md") });
@@ -401,3 +491,16 @@ async function syncAgentMemory(): Promise<void> {
401
491
  ),
402
492
  );
403
493
  }
494
+
495
+ function confirmSync(): Promise<boolean> {
496
+ return new Promise((resolve) => {
497
+ const rl = createInterface({
498
+ input: process.stdin,
499
+ output: process.stdout,
500
+ });
501
+ rl.question(" ", (answer) => {
502
+ rl.close();
503
+ resolve(answer.trim().toLowerCase() === "y");
504
+ });
505
+ });
506
+ }
package/src/index.ts CHANGED
@@ -39,7 +39,7 @@ program
39
39
  // --- Core commands ---
40
40
 
41
41
  program
42
- .command("push")
42
+ .command("push [content]")
43
43
  .description("Save knowledge to your Memax workspace")
44
44
  .option("-f, --file <path>", "File to push")
45
45
  .option("-c, --category <category>", "Category (auto-detected if omitted)")
@@ -77,7 +77,24 @@ program
77
77
  program
78
78
  .command("delete <id>")
79
79
  .description("Delete a note")
80
- .option("--confirm", "Skip confirmation")
80
+ .option("-y, --yes", "Skip confirmation")
81
+ .action(deleteCommand);
82
+
83
+ // Aliases
84
+ program
85
+ .command("remember [content]")
86
+ .description("Alias for push — save knowledge to your Memax workspace")
87
+ .option("-f, --file <path>", "File to push")
88
+ .option("-c, --category <category>", "Category (auto-detected if omitted)")
89
+ .option("-t, --tags <tags>", "Comma-separated tags")
90
+ .option("--title <title>", "Note title")
91
+ .option("--stdin", "Read content from stdin")
92
+ .action(pushCommand);
93
+
94
+ program
95
+ .command("forget <id>")
96
+ .description("Alias for delete — remove a note from your workspace")
97
+ .option("-y, --yes", "Skip confirmation")
81
98
  .action(deleteCommand);
82
99
 
83
100
  // --- Sync ---
@@ -100,6 +117,7 @@ const syncCmd = program
100
117
  "--agent-memory",
101
118
  "Sync native AI agent memory files (Claude Code, Cursor, Codex)",
102
119
  )
120
+ .option("-y, --yes", "Skip confirmation for large syncs")
103
121
  .action(syncCommand);
104
122
 
105
123
  syncCmd
@@ -115,6 +133,8 @@ program
115
133
  .option("--mcp", "Enable MCP server (agent tools)")
116
134
  .option("--hooks", "Enable context injection hooks")
117
135
  .option("--all", "Enable both MCP and hooks")
136
+ .option("--local", "Use local stdio MCP instead of remote server")
137
+ .option("--print", "Print MCP config JSON to copy/paste (no changes made)")
118
138
  .option("--only <agents>", "Only configure these agents (comma-separated)")
119
139
  .option("--skip <agents>", "Skip these agents (comma-separated)")
120
140
  .action(setupCommand);