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.
- package/.vscode/mcp.json +8 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +5 -2
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +59 -1
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/recall.d.ts.map +1 -1
- package/dist/commands/recall.js +2 -1
- package/dist/commands/recall.js.map +1 -1
- package/dist/commands/setup.d.ts +13 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +531 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/index.js +22 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/commands/login.ts +5 -2
- package/src/commands/mcp.ts +76 -1
- package/src/commands/recall.ts +4 -1
- package/src/commands/setup.ts +682 -0
- package/src/index.ts +27 -2
- package/.turbo/turbo-build.log +0 -4
|
@@ -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
|
+
`;
|