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.
- package/.vscode/mcp.json +8 -0
- package/dist/commands/auth.d.ts +6 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +62 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/capture.d.ts +17 -0
- package/dist/commands/capture.d.ts.map +1 -0
- package/dist/commands/capture.js +61 -0
- package/dist/commands/capture.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +24 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/delete.d.ts +4 -0
- package/dist/commands/delete.d.ts.map +1 -0
- package/dist/commands/delete.js +45 -0
- package/dist/commands/delete.js.map +1 -0
- package/dist/commands/hook.d.ts +2 -0
- package/dist/commands/hook.d.ts.map +1 -0
- package/dist/commands/hook.js +189 -0
- package/dist/commands/hook.js.map +1 -0
- package/dist/commands/list.d.ts +8 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +23 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +131 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/mcp.d.ts +3 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +384 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/push.d.ts +11 -0
- package/dist/commands/push.d.ts.map +1 -0
- package/dist/commands/push.js +98 -0
- package/dist/commands/push.js.map +1 -0
- package/dist/commands/recall.d.ts +12 -0
- package/dist/commands/recall.d.ts.map +1 -0
- package/dist/commands/recall.js +107 -0
- package/dist/commands/recall.js.map +1 -0
- package/dist/commands/setup.d.ts +16 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +869 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/show.d.ts +2 -0
- package/dist/commands/show.d.ts.map +1 -0
- package/dist/commands/show.js +29 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/commands/sync.d.ts +12 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +414 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +168 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api.d.ts +4 -0
- package/dist/lib/api.d.ts.map +1 -0
- package/dist/lib/api.js +95 -0
- package/dist/lib/api.js.map +1 -0
- package/dist/lib/config.d.ts +10 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +49 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/credentials.d.ts +11 -0
- package/dist/lib/credentials.d.ts.map +1 -0
- package/dist/lib/credentials.js +36 -0
- package/dist/lib/credentials.js.map +1 -0
- package/package.json +39 -4
- package/src/commands/auth.ts +92 -0
- package/src/commands/capture.ts +86 -0
- package/src/commands/config.ts +27 -0
- package/src/commands/delete.ts +58 -0
- package/src/commands/hook.ts +243 -0
- package/src/commands/list.ts +38 -0
- package/src/commands/login.ts +164 -0
- package/src/commands/mcp.ts +490 -0
- package/src/commands/push.ts +137 -0
- package/src/commands/recall.ts +163 -0
- package/src/commands/setup.ts +1129 -0
- package/src/commands/show.ts +35 -0
- package/src/commands/sync.ts +506 -0
- package/src/index.ts +223 -0
- package/src/lib/api.ts +110 -0
- package/src/lib/config.ts +61 -0
- package/src/lib/credentials.ts +42 -0
- package/tsconfig.json +9 -0
- package/LICENSE +0 -24
- package/README.md +0 -2
- 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
|
+
`;
|