memax-cli 0.1.0-alpha.1 → 0.1.0-alpha.3

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.
@@ -0,0 +1,682 @@
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
+
13
+ // --- Agent definitions ---
14
+
15
+ interface AgentDef {
16
+ name: string;
17
+ id: string;
18
+ configPath: string; // global MCP config file
19
+ format: "json-mcpServers" | "json-servers" | "toml";
20
+ /** Key under which MCP servers live */
21
+ mcpKey: string;
22
+ hasHooks: boolean;
23
+ detect: () => boolean; // is this agent likely installed?
24
+ }
25
+
26
+ function getAgents(): AgentDef[] {
27
+ const home = homedir();
28
+ const isWin = platform() === "win32";
29
+
30
+ return [
31
+ {
32
+ name: "Claude Code",
33
+ id: "claude-code",
34
+ configPath: join(home, ".claude", "settings.json"),
35
+ format: "json-mcpServers",
36
+ mcpKey: "mcpServers",
37
+ hasHooks: true,
38
+ detect: () =>
39
+ existsSync(join(home, ".claude")) || commandExists("claude"),
40
+ },
41
+ {
42
+ name: "Cursor",
43
+ id: "cursor",
44
+ configPath: join(home, ".cursor", "mcp.json"),
45
+ format: "json-mcpServers",
46
+ mcpKey: "mcpServers",
47
+ hasHooks: false,
48
+ detect: () =>
49
+ existsSync(join(home, ".cursor")) || commandExists("cursor"),
50
+ },
51
+ {
52
+ name: "Windsurf",
53
+ id: "windsurf",
54
+ configPath: join(home, ".codeium", "windsurf", "mcp_config.json"),
55
+ format: "json-mcpServers",
56
+ mcpKey: "mcpServers",
57
+ hasHooks: false,
58
+ detect: () =>
59
+ existsSync(join(home, ".codeium", "windsurf")) ||
60
+ commandExists("windsurf"),
61
+ },
62
+ {
63
+ name: "Gemini CLI",
64
+ id: "gemini",
65
+ configPath: join(home, ".gemini", "settings.json"),
66
+ format: "json-mcpServers",
67
+ mcpKey: "mcpServers",
68
+ hasHooks: true,
69
+ detect: () =>
70
+ existsSync(join(home, ".gemini")) || commandExists("gemini"),
71
+ },
72
+ {
73
+ name: "GitHub Copilot CLI",
74
+ id: "copilot",
75
+ configPath: join(home, ".copilot", "mcp-config.json"),
76
+ format: "json-mcpServers",
77
+ mcpKey: "mcpServers",
78
+ hasHooks: false,
79
+ detect: () =>
80
+ existsSync(join(home, ".copilot")) || commandExists("gh copilot"),
81
+ },
82
+ {
83
+ name: "Copilot (VS Code)",
84
+ id: "vscode",
85
+ configPath: join(".vscode", "mcp.json"), // project-level
86
+ format: "json-servers",
87
+ mcpKey: "servers",
88
+ hasHooks: false,
89
+ detect: () => existsSync(".vscode") || commandExists("code"),
90
+ },
91
+ {
92
+ name: "Codex CLI",
93
+ id: "codex",
94
+ configPath: join(home, ".codex", "config.toml"),
95
+ format: "toml",
96
+ mcpKey: "mcp_servers",
97
+ hasHooks: false,
98
+ detect: () => existsSync(join(home, ".codex")) || commandExists("codex"),
99
+ },
100
+ ];
101
+ }
102
+
103
+ // --- Setup command ---
104
+
105
+ interface SetupOptions {
106
+ mcp?: boolean;
107
+ hooks?: boolean;
108
+ all?: boolean;
109
+ only?: string;
110
+ skip?: string;
111
+ }
112
+
113
+ export async function setupCommand(options: SetupOptions): Promise<void> {
114
+ const enableMcp = options.all || options.mcp;
115
+ const enableHooks = options.all || options.hooks;
116
+
117
+ if (!enableMcp && !enableHooks) {
118
+ printUsage();
119
+ return;
120
+ }
121
+
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);
131
+ }
132
+
133
+ // Filter agents
134
+ const allAgents = getAgents();
135
+ const onlySet = options.only
136
+ ? new Set(options.only.split(",").map((s) => s.trim().toLowerCase()))
137
+ : null;
138
+ const skipSet = options.skip
139
+ ? new Set(options.skip.split(",").map((s) => s.trim().toLowerCase()))
140
+ : new Set<string>();
141
+
142
+ const agents = allAgents.filter((a) => {
143
+ if (skipSet.has(a.id)) return false;
144
+ if (onlySet) return onlySet.has(a.id);
145
+ return a.detect();
146
+ });
147
+
148
+ if (agents.length === 0) {
149
+ console.log(chalk.yellow("\n No supported AI agents detected.\n"));
150
+ console.log(chalk.gray(" Supported agents:"));
151
+ for (const a of allAgents) {
152
+ console.log(chalk.gray(` • ${a.name} (--only ${a.id})`));
153
+ }
154
+ console.log(
155
+ chalk.gray("\n Use --only to force setup for a specific agent.\n"),
156
+ );
157
+ return;
158
+ }
159
+
160
+ console.log(chalk.bold("\n Memax Setup\n"));
161
+
162
+ const results: { agent: string; changes: string[] }[] = [];
163
+
164
+ for (const agent of agents) {
165
+ const changes: string[] = [];
166
+
167
+ // MCP setup
168
+ if (enableMcp) {
169
+ try {
170
+ setupMcp(agent, memaxBin);
171
+ changes.push("MCP server");
172
+ } catch (err) {
173
+ console.log(
174
+ chalk.red(
175
+ ` ✗ ${agent.name}: MCP setup failed — ${(err as Error).message}`,
176
+ ),
177
+ );
178
+ }
179
+ }
180
+
181
+ // Hook setup (only for agents that support it)
182
+ if (enableHooks && agent.hasHooks) {
183
+ try {
184
+ setupHooks(agent, memaxBin);
185
+ changes.push("Context injection hook");
186
+ } catch (err) {
187
+ console.log(
188
+ chalk.red(
189
+ ` ✗ ${agent.name}: Hook setup failed — ${(err as Error).message}`,
190
+ ),
191
+ );
192
+ }
193
+ }
194
+
195
+ if (changes.length > 0) {
196
+ results.push({ agent: agent.name, changes });
197
+ }
198
+ }
199
+
200
+ // Print summary
201
+ if (results.length === 0) {
202
+ console.log(chalk.yellow(" No changes made.\n"));
203
+ return;
204
+ }
205
+
206
+ console.log(chalk.green(" Configured:\n"));
207
+ for (const r of results) {
208
+ console.log(chalk.white(` ${r.agent}`));
209
+ for (const c of r.changes) {
210
+ console.log(chalk.gray(` ✓ ${c}`));
211
+ }
212
+ }
213
+
214
+ console.log(chalk.gray("\n MCP tools available to all configured agents:"));
215
+ console.log(
216
+ chalk.gray(" • memax_recall — semantic search your knowledge"),
217
+ );
218
+ console.log(chalk.gray(" • memax_push — save knowledge from sessions"));
219
+ console.log(chalk.gray(" • memax_get — read full note by ID"));
220
+ console.log(chalk.gray(" • memax_search — browse notes by category"));
221
+
222
+ if (enableHooks) {
223
+ const hookAgents = results.filter((r) =>
224
+ r.changes.includes("Context injection hook"),
225
+ );
226
+ if (hookAgents.length > 0) {
227
+ console.log(
228
+ chalk.gray(
229
+ `\n Hooks installed for: ${hookAgents.map((r) => r.agent).join(", ")}`,
230
+ ),
231
+ );
232
+ console.log(
233
+ chalk.gray(
234
+ " Every prompt gets relevant context injected automatically.",
235
+ ),
236
+ );
237
+ }
238
+ }
239
+
240
+ console.log(
241
+ chalk.gray("\n Restart your agents for changes to take effect.\n"),
242
+ );
243
+ }
244
+
245
+ export async function teardownCommand(options: {
246
+ only?: string;
247
+ }): Promise<void> {
248
+ const allAgents = getAgents();
249
+ const onlySet = options.only
250
+ ? new Set(options.only.split(",").map((s) => s.trim().toLowerCase()))
251
+ : null;
252
+
253
+ const agents = onlySet
254
+ ? allAgents.filter((a) => onlySet.has(a.id))
255
+ : allAgents;
256
+
257
+ let removed = false;
258
+
259
+ for (const agent of agents) {
260
+ try {
261
+ // Claude Code uses its own CLI
262
+ if (agent.id === "claude-code") {
263
+ if (commandExists("claude")) {
264
+ try {
265
+ execSync("claude mcp remove memax", { stdio: "pipe" });
266
+ console.log(chalk.gray(` Removed MCP from ${agent.name}`));
267
+ removed = true;
268
+ } catch {
269
+ // Not installed
270
+ }
271
+ }
272
+ if (agent.hasHooks && existsSync(agent.configPath)) {
273
+ if (removeHooks(agent)) removed = true;
274
+ }
275
+ continue;
276
+ }
277
+
278
+ if (!existsSync(agent.configPath)) continue;
279
+
280
+ if (agent.format === "toml") {
281
+ if (removeMcpToml(agent)) removed = true;
282
+ } else {
283
+ if (removeMcpJson(agent)) removed = true;
284
+ }
285
+ if (agent.hasHooks && removeHooks(agent)) removed = true;
286
+ } catch {
287
+ // Skip agents we can't clean up
288
+ }
289
+ }
290
+
291
+ if (!removed) {
292
+ console.log(chalk.yellow("\n No Memax integrations found to remove.\n"));
293
+ return;
294
+ }
295
+
296
+ console.log(
297
+ chalk.green(
298
+ "\n Memax integrations removed.\n Restart your agents for changes to take effect.\n",
299
+ ),
300
+ );
301
+ }
302
+
303
+ // --- MCP setup per agent ---
304
+
305
+ function setupMcp(agent: AgentDef, bin: MemaxBin): void {
306
+ // Claude Code has its own CLI for MCP management
307
+ if (agent.id === "claude-code") {
308
+ setupMcpClaudeCode(bin);
309
+ return;
310
+ }
311
+
312
+ mkdirSync(dirname(agent.configPath), { recursive: true });
313
+
314
+ if (agent.format === "toml") {
315
+ setupMcpToml(agent, bin);
316
+ return;
317
+ }
318
+
319
+ // JSON-based agents
320
+ let config: Record<string, unknown> = {};
321
+ if (existsSync(agent.configPath)) {
322
+ try {
323
+ config = JSON.parse(readFileSync(agent.configPath, "utf-8"));
324
+ } catch {
325
+ // Start fresh
326
+ }
327
+ }
328
+
329
+ const servers = (config[agent.mcpKey] ?? {}) as Record<string, unknown>;
330
+ servers.memax = {
331
+ command: bin.command,
332
+ args: [...bin.args, "mcp", "serve"],
333
+ };
334
+ config[agent.mcpKey] = servers;
335
+
336
+ writeFileSync(agent.configPath, JSON.stringify(config, null, 2) + "\n");
337
+ }
338
+
339
+ function setupMcpClaudeCode(bin: MemaxBin): void {
340
+ // Claude Code uses its own CLI for MCP — settings.json mcpServers is ignored
341
+ if (!commandExists("claude")) {
342
+ throw new Error("claude CLI not found in PATH");
343
+ }
344
+
345
+ // Remove existing first (idempotent)
346
+ try {
347
+ execSync("claude mcp remove memax", { stdio: "pipe" });
348
+ } catch {
349
+ // Not installed yet — fine
350
+ }
351
+
352
+ // claude mcp add <name> -- <command> [args...]
353
+ const allArgs = [...bin.args, "mcp", "serve"];
354
+ const cmd = `claude mcp add memax -- ${bin.command} ${allArgs.join(" ")}`;
355
+
356
+ try {
357
+ execSync(cmd, { stdio: "pipe" });
358
+ } catch (err) {
359
+ throw new Error(`claude mcp add failed: ${(err as Error).message}`);
360
+ }
361
+ }
362
+
363
+ function setupMcpToml(agent: AgentDef, bin: MemaxBin): void {
364
+ // Codex uses TOML — append or update the memax section
365
+ let content = "";
366
+ if (existsSync(agent.configPath)) {
367
+ content = readFileSync(agent.configPath, "utf-8");
368
+ }
369
+
370
+ // Remove existing memax section if present
371
+ content = content.replace(/\[mcp_servers\.memax\][\s\S]*?(?=\n\[|$)/, "");
372
+
373
+ const args = [...bin.args, "mcp", "serve"].map((a) => `"${a}"`).join(", ");
374
+
375
+ content = content.trim();
376
+ if (content) content += "\n\n";
377
+ content += `[mcp_servers.memax]\ncommand = "${bin.command}"\nargs = [${args}]\n`;
378
+
379
+ writeFileSync(agent.configPath, content);
380
+ }
381
+
382
+ // --- Hook setup ---
383
+
384
+ function setupHooks(agent: AgentDef, bin: MemaxBin): void {
385
+ if (agent.id === "claude-code") {
386
+ setupClaudeCodeHooks(agent, bin);
387
+ } else if (agent.id === "gemini") {
388
+ setupGeminiHooks(agent, bin);
389
+ }
390
+ }
391
+
392
+ function setupClaudeCodeHooks(agent: AgentDef, bin: MemaxBin): void {
393
+ const hookScript = writeHookScript(bin);
394
+
395
+ let config: Record<string, unknown> = {};
396
+ if (existsSync(agent.configPath)) {
397
+ try {
398
+ config = JSON.parse(readFileSync(agent.configPath, "utf-8"));
399
+ } catch {
400
+ // Start fresh
401
+ }
402
+ }
403
+
404
+ const hooks = (config.hooks ?? {}) as Record<string, unknown[]>;
405
+
406
+ // Remove existing memax hooks
407
+ if (hooks["UserPromptSubmit"]) {
408
+ hooks["UserPromptSubmit"] = (
409
+ hooks["UserPromptSubmit"] as Array<{
410
+ hooks?: Array<{ command?: string }>;
411
+ }>
412
+ ).filter((h) => !h.hooks?.some((hh) => hh.command?.includes("memax")));
413
+ }
414
+
415
+ hooks["UserPromptSubmit"] = [
416
+ ...((hooks["UserPromptSubmit"] as unknown[]) ?? []),
417
+ {
418
+ matcher: "",
419
+ hooks: [{ type: "command", command: hookScript, timeout: 30 }],
420
+ },
421
+ ];
422
+
423
+ config.hooks = hooks;
424
+ writeFileSync(agent.configPath, JSON.stringify(config, null, 2) + "\n");
425
+ }
426
+
427
+ function setupGeminiHooks(agent: AgentDef, bin: MemaxBin): void {
428
+ const hookScript = writeHookScript(bin);
429
+
430
+ let config: Record<string, unknown> = {};
431
+ if (existsSync(agent.configPath)) {
432
+ try {
433
+ config = JSON.parse(readFileSync(agent.configPath, "utf-8"));
434
+ } catch {
435
+ // Start fresh
436
+ }
437
+ }
438
+
439
+ const hooks = (config.hooks ?? {}) as Record<string, unknown[]>;
440
+
441
+ // Remove existing memax hooks from both old ("Startup") and correct event
442
+ for (const event of ["Startup", "BeforeAgent"]) {
443
+ if (hooks[event]) {
444
+ hooks[event] = (
445
+ hooks[event] as Array<{ hooks?: Array<{ command?: string }> }>
446
+ ).filter((h) => !h.hooks?.some((hh) => hh.command?.includes("memax")));
447
+ if ((hooks[event] as unknown[]).length === 0) delete hooks[event];
448
+ }
449
+ }
450
+
451
+ // BeforeAgent fires after user submits a prompt — equivalent to Claude Code's PrePromptSubmit
452
+ hooks["BeforeAgent"] = [
453
+ ...((hooks["BeforeAgent"] as unknown[]) ?? []),
454
+ {
455
+ matcher: "",
456
+ hooks: [{ type: "command", command: hookScript, timeout: 30 }],
457
+ },
458
+ ];
459
+
460
+ config.hooks = hooks;
461
+ writeFileSync(agent.configPath, JSON.stringify(config, null, 2) + "\n");
462
+ }
463
+
464
+ // --- Teardown helpers ---
465
+
466
+ function removeMcpJson(agent: AgentDef): boolean {
467
+ if (!existsSync(agent.configPath)) return false;
468
+
469
+ try {
470
+ const config = JSON.parse(readFileSync(agent.configPath, "utf-8"));
471
+ const servers = config[agent.mcpKey] as Record<string, unknown> | undefined;
472
+ if (!servers?.memax) return false;
473
+
474
+ delete servers.memax;
475
+ if (Object.keys(servers).length === 0) delete config[agent.mcpKey];
476
+
477
+ writeFileSync(agent.configPath, JSON.stringify(config, null, 2) + "\n");
478
+ console.log(chalk.gray(` Removed MCP from ${agent.name}`));
479
+ return true;
480
+ } catch {
481
+ return false;
482
+ }
483
+ }
484
+
485
+ function removeMcpToml(agent: AgentDef): boolean {
486
+ if (!existsSync(agent.configPath)) return false;
487
+
488
+ let content = readFileSync(agent.configPath, "utf-8");
489
+ const before = content;
490
+ content = content.replace(/\[mcp_servers\.memax\][\s\S]*?(?=\n\[|$)/, "");
491
+
492
+ if (content === before) return false;
493
+
494
+ writeFileSync(agent.configPath, content.trim() + "\n");
495
+ console.log(chalk.gray(` Removed MCP from ${agent.name}`));
496
+ return true;
497
+ }
498
+
499
+ function removeHooks(agent: AgentDef): boolean {
500
+ if (!existsSync(agent.configPath)) return false;
501
+
502
+ try {
503
+ const config = JSON.parse(readFileSync(agent.configPath, "utf-8"));
504
+ const hooks = config.hooks as Record<string, unknown[]> | undefined;
505
+ if (!hooks) return false;
506
+
507
+ let removed = false;
508
+ for (const event of Object.keys(hooks)) {
509
+ const before = (hooks[event] as unknown[]).length;
510
+ hooks[event] = (
511
+ hooks[event] as Array<{
512
+ hooks?: Array<{ command?: string }>;
513
+ command?: string;
514
+ }>
515
+ ).filter(
516
+ (h) =>
517
+ !h.command?.includes("memax") &&
518
+ !h.hooks?.some((hh) => hh.command?.includes("memax")),
519
+ );
520
+ if ((hooks[event] as unknown[]).length < before) removed = true;
521
+ if ((hooks[event] as unknown[]).length === 0) delete hooks[event];
522
+ }
523
+
524
+ if (Object.keys(hooks).length === 0) delete config.hooks;
525
+ if (removed) {
526
+ writeFileSync(agent.configPath, JSON.stringify(config, null, 2) + "\n");
527
+ console.log(chalk.gray(` Removed hooks from ${agent.name}`));
528
+ }
529
+ return removed;
530
+ } catch {
531
+ return false;
532
+ }
533
+ }
534
+
535
+ // --- Shared helpers ---
536
+
537
+ interface MemaxBin {
538
+ command: string;
539
+ args: string[];
540
+ shell: string;
541
+ }
542
+
543
+ function resolveMemaxBin(): MemaxBin | null {
544
+ // 1. Global install — use absolute path so agents find it without shell PATH
545
+ if (commandExists("memax")) {
546
+ try {
547
+ const which = platform() === "win32" ? "where memax" : "which memax";
548
+ const absPath = execSync(which, { encoding: "utf-8", stdio: "pipe" })
549
+ .trim()
550
+ .split("\n")[0];
551
+ if (absPath) {
552
+ return { command: absPath, args: [], shell: absPath };
553
+ }
554
+ } catch {
555
+ // fall through
556
+ }
557
+ return { command: "memax", args: [], shell: "memax" };
558
+ }
559
+
560
+ // 2. Local repo build (faster than npx, always up-to-date during dev)
561
+ const localBuild = join(process.cwd(), "packages", "cli", "dist", "index.js");
562
+ if (existsSync(localBuild)) {
563
+ return {
564
+ command: "node",
565
+ args: [localBuild],
566
+ shell: `node ${localBuild}`,
567
+ };
568
+ }
569
+
570
+ // 3. npx as last resort (slow startup — agents may timeout on first run)
571
+ try {
572
+ execSync("npx --yes memax-cli --version", {
573
+ encoding: "utf-8",
574
+ timeout: 15000,
575
+ stdio: "pipe",
576
+ });
577
+ return {
578
+ command: "npx",
579
+ args: ["-y", "memax-cli"],
580
+ shell: "npx -y memax-cli",
581
+ };
582
+ } catch {
583
+ // npx failed
584
+ }
585
+
586
+ return null;
587
+ }
588
+
589
+ function commandExists(cmd: string): boolean {
590
+ try {
591
+ const which = platform() === "win32" ? "where" : "which";
592
+ execSync(`${which} ${cmd}`, { stdio: "pipe" });
593
+ return true;
594
+ } catch {
595
+ return false;
596
+ }
597
+ }
598
+
599
+ function writeHookScript(bin: MemaxBin): string {
600
+ const hooksDir = join(homedir(), ".memax", "hooks");
601
+ mkdirSync(hooksDir, { recursive: true });
602
+
603
+ const isWindows = platform() === "win32";
604
+ const scriptName = isWindows ? "context-inject.cmd" : "context-inject.sh";
605
+ const scriptPath = join(hooksDir, scriptName);
606
+
607
+ if (isWindows) {
608
+ writeFileSync(scriptPath, WIN_HOOK.replace(/\$MEMAX/g, bin.shell));
609
+ } else {
610
+ writeFileSync(scriptPath, UNIX_HOOK.replace(/\$MEMAX/g, bin.shell));
611
+ chmodSync(scriptPath, 0o755);
612
+ }
613
+
614
+ return scriptPath;
615
+ }
616
+
617
+ function printUsage(): void {
618
+ const agents = getAgents();
619
+ const detected = agents.filter((a) => a.detect());
620
+
621
+ console.log(
622
+ chalk.bold("\n Memax Setup — Configure AI Agent Integrations\n"),
623
+ );
624
+
625
+ if (detected.length > 0) {
626
+ console.log(chalk.gray(" Detected agents:"));
627
+ for (const a of detected) {
628
+ const hookNote = a.hasHooks ? " (MCP + hooks)" : " (MCP)";
629
+ console.log(chalk.white(` • ${a.name}${hookNote}`));
630
+ }
631
+ console.log();
632
+ }
633
+
634
+ console.log(chalk.gray(" Usage:\n"));
635
+ console.log(
636
+ chalk.gray(
637
+ " memax setup --mcp MCP tools for all detected agents",
638
+ ),
639
+ );
640
+ console.log(
641
+ chalk.gray(" memax setup --all MCP + hooks (where supported)"),
642
+ );
643
+ console.log(chalk.gray(" memax setup --mcp --only claude-code,cursor"));
644
+ console.log(chalk.gray(" memax setup --all --skip codex"));
645
+ console.log(
646
+ chalk.gray(" memax teardown Remove all integrations\n"),
647
+ );
648
+
649
+ console.log(chalk.gray(" Supported agents:"));
650
+ for (const a of agents) {
651
+ const status = a.detect()
652
+ ? chalk.green("detected")
653
+ : chalk.gray("not found");
654
+ console.log(
655
+ chalk.gray(` ${a.id.padEnd(14)} ${a.name.padEnd(20)} ${status}`),
656
+ );
657
+ }
658
+ console.log();
659
+ }
660
+
661
+ const UNIX_HOOK = `#!/bin/bash
662
+ # Memax context injection — installed by: memax setup --hooks
663
+ set -e
664
+ INPUT=$(cat)
665
+ PROMPT=$(echo "$INPUT" | jq -r '.prompt // empty')
666
+ CWD=$(echo "$INPUT" | jq -r '.cwd // empty')
667
+ if [ -z "$PROMPT" ] || [ \${#PROMPT} -lt 10 ]; then exit 0; fi
668
+ case "$PROMPT" in [Yy]|[Yy]es|[Nn]|[Nn]o|ok|OK|sure|Sure|thanks|Thanks|y|n) exit 0 ;; esac
669
+ if [ -n "$CWD" ]; then cd "$CWD" 2>/dev/null || true; fi
670
+ CONTEXT=$($MEMAX recall "$PROMPT" --hook --limit 5 --max-tokens 3000 2>/dev/null) || exit 0
671
+ if [ -n "$CONTEXT" ] && [ "$CONTEXT" != "<memax-context>" ]; then echo "$CONTEXT"; fi
672
+ exit 0
673
+ `;
674
+
675
+ const WIN_HOOK = `@echo off
676
+ REM Memax context injection — installed by: memax setup --hooks
677
+ set /p INPUT=
678
+ for /f "tokens=*" %%a in ('echo %INPUT% ^| jq -r ".prompt // empty"') do set PROMPT=%%a
679
+ if "%PROMPT%"=="" exit /b 0
680
+ $MEMAX recall "%PROMPT%" --hook --limit 5 --max-tokens 3000 2>nul
681
+ exit /b 0
682
+ `;