@hasna/mementos 0.14.22 → 0.14.23

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/README.md CHANGED
@@ -18,8 +18,9 @@ mementos auth signup
18
18
  mementos setup agents
19
19
  ```
20
20
 
21
- `mementos setup agents` writes remote MCP and hook snippets for Claude Code,
22
- Codex, OpenCode, and Antigravity under the user's config directory.
21
+ `mementos setup agents` registers the hosted `mementos-mcp` server in Claude
22
+ Code, Codex, OpenCode, and Antigravity config files. It also writes managed
23
+ diagnostic snippets under `~/.config/mementos/agent-integrations`.
23
24
 
24
25
  ## MCP
25
26
 
@@ -36,3 +37,9 @@ MEMENTOS_API_TOKEN=<token> mementos inject "project context"
36
37
  ```
37
38
 
38
39
  The command prints a prompt-ready memory block from the hosted API.
40
+
41
+ Claude Code also receives `UserPromptSubmit`, `SessionStart`, and `Stop` hook
42
+ entries that call `mementos hook ...` and return hosted memory as hook context.
43
+ Codex does not expose a native hook file in `~/.codex/config.toml`, so the
44
+ installer registers MCP there and keeps the injection command in the managed
45
+ integration snippets.
@@ -1 +1 @@
1
- {"version":3,"file":"agent-setup.d.ts","sourceRoot":"","sources":["../src/agent-setup.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,qBAAqB,8DAA+D,CAAC;AAElG,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAExE,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,WAAW,CAAC;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,kBAAkB,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;CAChF;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,WAAW,EAAE,oBAAoB,EAAE,CAAC;IACpC,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,iBAAsB,GAAG,cAAc,CA0CnF;AAED,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,iBAAsB,GAAG,cAAc,CAmCjF;AAED,wBAAgB,wBAAwB,CAAC,MAAM,SAAwB;;;EAStE;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAW9D"}
1
+ {"version":3,"file":"agent-setup.d.ts","sourceRoot":"","sources":["../src/agent-setup.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,qBAAqB,8DAA+D,CAAC;AAElG,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAExE,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,WAAW,CAAC;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,kBAAkB,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;CAChF;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,WAAW,EAAE,oBAAoB,EAAE,CAAC;IACpC,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,iBAAsB,GAAG,cAAc,CA0CnF;AAED,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,iBAAsB,GAAG,cAAc,CAqCjF;AAED,wBAAgB,wBAAwB,CAAC,MAAM,SAAwB;;;EAStE;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAW9D"}
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // @bun
3
3
 
4
4
  // src/agent-setup.ts
5
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
5
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
6
6
  import { homedir } from "os";
7
7
  import { dirname, join } from "path";
8
8
  var SUPPORTED_AGENT_HOSTS = ["claude-code", "codex", "opencode", "antigravity"];
@@ -45,7 +45,7 @@ function buildAgentSetupPlan(options = {}) {
45
45
  return {
46
46
  baseDir,
47
47
  files,
48
- hostResults: hosts.map((host) => ({ host, path: hostManagedConfigPath(home, host), status: "planned" })),
48
+ hostResults: hosts.map((host) => ({ host, path: hostNativeConfigPath(home, host), status: "planned" })),
49
49
  hosts,
50
50
  mcpCommand
51
51
  };
@@ -57,7 +57,7 @@ function installAgentSetup(options = {}) {
57
57
  ...plan,
58
58
  hostResults: plan.hostResults.map((result) => ({
59
59
  ...result,
60
- status: existsSync(result.path) ? "present" : "missing"
60
+ status: hasNativeHostInstall(result.path, result.host) ? "present" : "missing"
61
61
  }))
62
62
  };
63
63
  }
@@ -69,7 +69,9 @@ function installAgentSetup(options = {}) {
69
69
  };
70
70
  }
71
71
  for (const result of plan.hostResults)
72
- rmSync(result.path, { force: true });
72
+ uninstallNativeHost(result.path, result.host);
73
+ for (const host of plan.hosts)
74
+ rmSync(hostManagedConfigPath(options.homeDir ?? homedir(), host), { force: true });
73
75
  return {
74
76
  ...plan,
75
77
  hostResults: plan.hostResults.map((result) => ({ ...result, status: "removed" }))
@@ -81,6 +83,8 @@ function installAgentSetup(options = {}) {
81
83
  mkdirSync(dirname(file.path), { recursive: true });
82
84
  writeFileSync(file.path, file.content, { mode: file.executable ? 493 : 420 });
83
85
  }
86
+ for (const result of plan.hostResults)
87
+ installNativeHost(result.path, result.host, plan);
84
88
  return {
85
89
  ...plan,
86
90
  hostResults: plan.hostResults.map((result) => ({ ...result, status: "installed" }))
@@ -155,7 +159,9 @@ function renderManagedHostConfig(host, commands) {
155
159
  return {
156
160
  ...base,
157
161
  hooks: {
158
- Stop: [{ hooks: [{ type: "command", command: commands.hookCommand }] }]
162
+ UserPromptSubmit: [{ matcher: "", hooks: [{ type: "command", command: "mementos hook user-prompt-submit" }] }],
163
+ SessionStart: [{ matcher: "", hooks: [{ type: "command", command: "mementos hook session-start" }] }],
164
+ Stop: [{ matcher: "", hooks: [{ type: "command", command: commands.hookCommand }] }]
159
165
  }
160
166
  };
161
167
  }
@@ -184,6 +190,235 @@ function hostManagedConfigPath(home, host) {
184
190
  return join(home, ".config", "antigravity", "mementos.managed.json");
185
191
  }
186
192
  }
193
+ function hostNativeConfigPath(home, host) {
194
+ switch (host) {
195
+ case "claude-code":
196
+ return join(home, ".claude", "settings.json");
197
+ case "codex":
198
+ return join(home, ".codex", "config.toml");
199
+ case "opencode":
200
+ return join(home, ".config", "opencode", "opencode.json");
201
+ case "antigravity":
202
+ return join(home, ".antigravity", "mcp.json");
203
+ }
204
+ }
205
+ function installNativeHost(path, host, plan) {
206
+ mkdirSync(dirname(path), { recursive: true });
207
+ if (host === "codex") {
208
+ writeFileSync(path, installCodexToml(readTextFile(path), plan), { mode: 420 });
209
+ return;
210
+ }
211
+ const config = readJsonObject(path);
212
+ if (host === "claude-code") {
213
+ writeJsonObject(path, installClaudeConfig(config, plan));
214
+ return;
215
+ }
216
+ if (host === "opencode") {
217
+ writeJsonObject(path, installOpenCodeConfig(config, plan));
218
+ return;
219
+ }
220
+ writeJsonObject(path, installAntigravityConfig(config, plan));
221
+ }
222
+ function uninstallNativeHost(path, host) {
223
+ if (!existsSync(path))
224
+ return;
225
+ if (host === "codex") {
226
+ writeFileSync(path, uninstallCodexToml(readTextFile(path)), { mode: 420 });
227
+ return;
228
+ }
229
+ const config = readJsonObject(path);
230
+ if (host === "claude-code")
231
+ writeJsonObject(path, uninstallClaudeConfig(config));
232
+ if (host === "opencode")
233
+ writeJsonObject(path, uninstallOpenCodeConfig(config));
234
+ if (host === "antigravity")
235
+ writeJsonObject(path, uninstallMcpServersConfig(config));
236
+ }
237
+ function hasNativeHostInstall(path, host) {
238
+ if (!existsSync(path))
239
+ return false;
240
+ if (host === "codex")
241
+ return /\[mcp_servers\.mementos\]/.test(readTextFile(path));
242
+ const config = readJsonObject(path);
243
+ if (host === "opencode")
244
+ return isRecord(config.mcp) && isRecord(config.mcp.mementos);
245
+ return isRecord(config.mcpServers) && isRecord(config.mcpServers.mementos);
246
+ }
247
+ function installClaudeConfig(config, plan) {
248
+ return {
249
+ ...config,
250
+ mcpServers: {
251
+ ...recordValue(config.mcpServers),
252
+ mementos: stdioMcpServer(plan, true)
253
+ },
254
+ hooks: mergeAgentHooks(recordValue(config.hooks), {
255
+ UserPromptSubmit: "mementos hook user-prompt-submit",
256
+ SessionStart: "mementos hook session-start",
257
+ Stop: "mementos hook stop"
258
+ })
259
+ };
260
+ }
261
+ function uninstallClaudeConfig(config) {
262
+ return {
263
+ ...config,
264
+ mcpServers: omitKey(recordValue(config.mcpServers), "mementos"),
265
+ hooks: removeAgentHooks(recordValue(config.hooks), [
266
+ "mementos hook user-prompt-submit",
267
+ "mementos hook session-start",
268
+ "mementos hook stop"
269
+ ])
270
+ };
271
+ }
272
+ function installOpenCodeConfig(config, plan) {
273
+ return {
274
+ ...config,
275
+ mcp: {
276
+ ...recordValue(config.mcp),
277
+ mementos: {
278
+ type: "local",
279
+ command: [plan.mcpCommand],
280
+ enabled: true,
281
+ environment: remoteEnv(baseApiUrlFromPlan(plan))
282
+ }
283
+ }
284
+ };
285
+ }
286
+ function uninstallOpenCodeConfig(config) {
287
+ return {
288
+ ...config,
289
+ mcp: omitKey(recordValue(config.mcp), "mementos")
290
+ };
291
+ }
292
+ function installAntigravityConfig(config, plan) {
293
+ return {
294
+ ...config,
295
+ mcpServers: {
296
+ ...recordValue(config.mcpServers),
297
+ mementos: stdioMcpServer(plan, false)
298
+ }
299
+ };
300
+ }
301
+ function uninstallMcpServersConfig(config) {
302
+ return {
303
+ ...config,
304
+ mcpServers: omitKey(recordValue(config.mcpServers), "mementos")
305
+ };
306
+ }
307
+ function installCodexToml(current, plan) {
308
+ const withoutExisting = removeTomlTable(removeManagedCodexBlock(current), "mcp_servers.mementos");
309
+ const block = [
310
+ "# BEGIN @hasna/mementos",
311
+ "[mcp_servers.mementos]",
312
+ `command = ${JSON.stringify(plan.mcpCommand)}`,
313
+ "[mcp_servers.mementos.env]",
314
+ `MEMENTOS_API_URL = ${JSON.stringify(baseApiUrlFromPlan(plan))}`,
315
+ 'MEMENTOS_API_TOKEN = "$MEMENTOS_API_TOKEN"',
316
+ "# END @hasna/mementos",
317
+ ""
318
+ ].join(`
319
+ `);
320
+ return `${withoutExisting.trimEnd()}
321
+
322
+ ${block}`;
323
+ }
324
+ function uninstallCodexToml(current) {
325
+ return `${removeTomlTable(removeManagedCodexBlock(current), "mcp_servers.mementos").trimEnd()}
326
+ `;
327
+ }
328
+ function mergeAgentHooks(hooks, commands) {
329
+ const next = { ...hooks };
330
+ for (const [event, command] of Object.entries(commands)) {
331
+ const current = next[event];
332
+ const entries = Array.isArray(current) ? [...current] : [];
333
+ if (!entries.some((entry) => JSON.stringify(entry).includes(command))) {
334
+ entries.push({ matcher: "", hooks: [{ type: "command", command }] });
335
+ }
336
+ next[event] = entries;
337
+ }
338
+ return next;
339
+ }
340
+ function removeAgentHooks(hooks, commands) {
341
+ const next = { ...hooks };
342
+ for (const [event, entries] of Object.entries(next)) {
343
+ if (!Array.isArray(entries))
344
+ continue;
345
+ next[event] = entries.filter((entry) => !commands.some((command) => JSON.stringify(entry).includes(command)));
346
+ }
347
+ return next;
348
+ }
349
+ function stdioMcpServer(plan, includeType) {
350
+ return {
351
+ ...includeType ? { type: "stdio" } : {},
352
+ command: plan.mcpCommand,
353
+ env: remoteEnv(baseApiUrlFromPlan(plan))
354
+ };
355
+ }
356
+ function remoteEnv(apiUrl) {
357
+ return {
358
+ MEMENTOS_API_URL: apiUrl,
359
+ MEMENTOS_API_TOKEN: "$MEMENTOS_API_TOKEN"
360
+ };
361
+ }
362
+ function baseApiUrlFromPlan(plan) {
363
+ const mcpFile = plan.files.find((file) => file.path.endsWith("mcp-server.json"));
364
+ if (!mcpFile)
365
+ return "https://mementos.md";
366
+ try {
367
+ const parsed = JSON.parse(mcpFile.content);
368
+ return parsed.mcpServers?.mementos?.env?.MEMENTOS_API_URL ?? "https://mementos.md";
369
+ } catch {
370
+ return "https://mementos.md";
371
+ }
372
+ }
373
+ function removeManagedCodexBlock(value) {
374
+ return value.replace(/\n?# BEGIN @hasna\/mementos\n[\s\S]*?# END @hasna\/mementos\n?/g, `
375
+ `);
376
+ }
377
+ function removeTomlTable(value, table) {
378
+ const tablePattern = table.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
379
+ const lines = value.split(`
380
+ `);
381
+ const output = [];
382
+ let skipping = false;
383
+ for (const line of lines) {
384
+ if (new RegExp(`^\\[${tablePattern}(?:\\.|\\])`).test(line.trim())) {
385
+ skipping = true;
386
+ continue;
387
+ }
388
+ if (skipping && /^\[[^\]]+\]/.test(line.trim()))
389
+ skipping = false;
390
+ if (!skipping)
391
+ output.push(line);
392
+ }
393
+ return output.join(`
394
+ `);
395
+ }
396
+ function readTextFile(path) {
397
+ return existsSync(path) ? readFileSync(path, "utf8") : "";
398
+ }
399
+ function readJsonObject(path) {
400
+ if (!existsSync(path))
401
+ return {};
402
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
403
+ if (!isRecord(parsed))
404
+ throw new Error(`${path} must contain a JSON object`);
405
+ return parsed;
406
+ }
407
+ function writeJsonObject(path, value) {
408
+ mkdirSync(dirname(path), { recursive: true });
409
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
410
+ `, { mode: 420 });
411
+ }
412
+ function recordValue(value) {
413
+ return isRecord(value) ? value : {};
414
+ }
415
+ function isRecord(value) {
416
+ return typeof value === "object" && value !== null && !Array.isArray(value);
417
+ }
418
+ function omitKey(record, key) {
419
+ const { [key]: _removed, ...rest } = record;
420
+ return rest;
421
+ }
187
422
  function renderInjectionInstructions({ apiUrl, injectCommand }) {
188
423
  return `# mementos.md Memory Injection
189
424
 
@@ -453,12 +688,52 @@ async function injectMemory(args) {
453
688
  }
454
689
  async function runHook(args) {
455
690
  const subcommand = args[0] ?? "help";
691
+ const hookEvents = {
692
+ "user-prompt-submit": "UserPromptSubmit",
693
+ "session-start": "SessionStart",
694
+ "subagent-start": "SubagentStart",
695
+ setup: "Setup"
696
+ };
697
+ if (subcommand in hookEvents) {
698
+ const query2 = await readHookQuery();
699
+ const payload = await client.injectionContext({ query: query2, limit: 12 });
700
+ const block = readTextPayload(payload) ?? formatMemoryInjection(payload);
701
+ return {
702
+ suppressOutput: true,
703
+ hookSpecificOutput: {
704
+ hookEventName: hookEvents[subcommand],
705
+ additionalContext: block
706
+ }
707
+ };
708
+ }
456
709
  if (subcommand !== "stop") {
457
- throw new Error("usage: mementos hook stop");
710
+ throw new Error("usage: mementos hook <stop|user-prompt-submit|session-start|subagent-start|setup>");
458
711
  }
459
712
  const query = process.env.MEMENTOS_INJECTION_QUERY ?? process.cwd();
460
713
  return injectMemory([query]);
461
714
  }
715
+ async function readHookQuery() {
716
+ if (process.env.MEMENTOS_INJECTION_QUERY)
717
+ return process.env.MEMENTOS_INJECTION_QUERY;
718
+ const stdin = await readOptionalStdin();
719
+ if (!stdin)
720
+ return process.cwd();
721
+ try {
722
+ const parsed = JSON.parse(stdin);
723
+ const prompt = parsed.prompt ?? parsed.userPrompt ?? parsed.message ?? parsed.cwd;
724
+ if (typeof prompt === "string" && prompt.trim())
725
+ return prompt.trim();
726
+ } catch {
727
+ if (stdin.trim())
728
+ return stdin.trim();
729
+ }
730
+ return process.cwd();
731
+ }
732
+ async function readOptionalStdin() {
733
+ if (process.stdin.isTTY)
734
+ return "";
735
+ return new Response(Bun.stdin.stream()).text();
736
+ }
462
737
  function parseSetupOptions(args) {
463
738
  const options = {};
464
739
  for (let index = 0;index < args.length; index += 1) {
@@ -502,7 +777,7 @@ Usage:
502
777
  mementos auth signup
503
778
  mementos setup agents [--dry-run|--verify|--uninstall]
504
779
  mementos inject <query>
505
- mementos hook stop
780
+ mementos hook <stop|user-prompt-submit|session-start|subagent-start|setup>
506
781
  mementos capabilities
507
782
  mementos agents
508
783
  mementos usage
package/dist/index.js CHANGED
@@ -291,7 +291,7 @@ function requiredText(input, message) {
291
291
  return input.trim();
292
292
  }
293
293
  // src/agent-setup.ts
294
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
294
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
295
295
  import { homedir } from "os";
296
296
  import { dirname, join } from "path";
297
297
  var SUPPORTED_AGENT_HOSTS = ["claude-code", "codex", "opencode", "antigravity"];
@@ -334,7 +334,7 @@ function buildAgentSetupPlan(options = {}) {
334
334
  return {
335
335
  baseDir,
336
336
  files,
337
- hostResults: hosts.map((host) => ({ host, path: hostManagedConfigPath(home, host), status: "planned" })),
337
+ hostResults: hosts.map((host) => ({ host, path: hostNativeConfigPath(home, host), status: "planned" })),
338
338
  hosts,
339
339
  mcpCommand
340
340
  };
@@ -346,7 +346,7 @@ function installAgentSetup(options = {}) {
346
346
  ...plan,
347
347
  hostResults: plan.hostResults.map((result) => ({
348
348
  ...result,
349
- status: existsSync(result.path) ? "present" : "missing"
349
+ status: hasNativeHostInstall(result.path, result.host) ? "present" : "missing"
350
350
  }))
351
351
  };
352
352
  }
@@ -358,7 +358,9 @@ function installAgentSetup(options = {}) {
358
358
  };
359
359
  }
360
360
  for (const result of plan.hostResults)
361
- rmSync(result.path, { force: true });
361
+ uninstallNativeHost(result.path, result.host);
362
+ for (const host of plan.hosts)
363
+ rmSync(hostManagedConfigPath(options.homeDir ?? homedir(), host), { force: true });
362
364
  return {
363
365
  ...plan,
364
366
  hostResults: plan.hostResults.map((result) => ({ ...result, status: "removed" }))
@@ -370,6 +372,8 @@ function installAgentSetup(options = {}) {
370
372
  mkdirSync(dirname(file.path), { recursive: true });
371
373
  writeFileSync(file.path, file.content, { mode: file.executable ? 493 : 420 });
372
374
  }
375
+ for (const result of plan.hostResults)
376
+ installNativeHost(result.path, result.host, plan);
373
377
  return {
374
378
  ...plan,
375
379
  hostResults: plan.hostResults.map((result) => ({ ...result, status: "installed" }))
@@ -444,7 +448,9 @@ function renderManagedHostConfig(host, commands) {
444
448
  return {
445
449
  ...base,
446
450
  hooks: {
447
- Stop: [{ hooks: [{ type: "command", command: commands.hookCommand }] }]
451
+ UserPromptSubmit: [{ matcher: "", hooks: [{ type: "command", command: "mementos hook user-prompt-submit" }] }],
452
+ SessionStart: [{ matcher: "", hooks: [{ type: "command", command: "mementos hook session-start" }] }],
453
+ Stop: [{ matcher: "", hooks: [{ type: "command", command: commands.hookCommand }] }]
448
454
  }
449
455
  };
450
456
  }
@@ -473,6 +479,235 @@ function hostManagedConfigPath(home, host) {
473
479
  return join(home, ".config", "antigravity", "mementos.managed.json");
474
480
  }
475
481
  }
482
+ function hostNativeConfigPath(home, host) {
483
+ switch (host) {
484
+ case "claude-code":
485
+ return join(home, ".claude", "settings.json");
486
+ case "codex":
487
+ return join(home, ".codex", "config.toml");
488
+ case "opencode":
489
+ return join(home, ".config", "opencode", "opencode.json");
490
+ case "antigravity":
491
+ return join(home, ".antigravity", "mcp.json");
492
+ }
493
+ }
494
+ function installNativeHost(path, host, plan) {
495
+ mkdirSync(dirname(path), { recursive: true });
496
+ if (host === "codex") {
497
+ writeFileSync(path, installCodexToml(readTextFile(path), plan), { mode: 420 });
498
+ return;
499
+ }
500
+ const config = readJsonObject(path);
501
+ if (host === "claude-code") {
502
+ writeJsonObject(path, installClaudeConfig(config, plan));
503
+ return;
504
+ }
505
+ if (host === "opencode") {
506
+ writeJsonObject(path, installOpenCodeConfig(config, plan));
507
+ return;
508
+ }
509
+ writeJsonObject(path, installAntigravityConfig(config, plan));
510
+ }
511
+ function uninstallNativeHost(path, host) {
512
+ if (!existsSync(path))
513
+ return;
514
+ if (host === "codex") {
515
+ writeFileSync(path, uninstallCodexToml(readTextFile(path)), { mode: 420 });
516
+ return;
517
+ }
518
+ const config = readJsonObject(path);
519
+ if (host === "claude-code")
520
+ writeJsonObject(path, uninstallClaudeConfig(config));
521
+ if (host === "opencode")
522
+ writeJsonObject(path, uninstallOpenCodeConfig(config));
523
+ if (host === "antigravity")
524
+ writeJsonObject(path, uninstallMcpServersConfig(config));
525
+ }
526
+ function hasNativeHostInstall(path, host) {
527
+ if (!existsSync(path))
528
+ return false;
529
+ if (host === "codex")
530
+ return /\[mcp_servers\.mementos\]/.test(readTextFile(path));
531
+ const config = readJsonObject(path);
532
+ if (host === "opencode")
533
+ return isRecord(config.mcp) && isRecord(config.mcp.mementos);
534
+ return isRecord(config.mcpServers) && isRecord(config.mcpServers.mementos);
535
+ }
536
+ function installClaudeConfig(config, plan) {
537
+ return {
538
+ ...config,
539
+ mcpServers: {
540
+ ...recordValue(config.mcpServers),
541
+ mementos: stdioMcpServer(plan, true)
542
+ },
543
+ hooks: mergeAgentHooks(recordValue(config.hooks), {
544
+ UserPromptSubmit: "mementos hook user-prompt-submit",
545
+ SessionStart: "mementos hook session-start",
546
+ Stop: "mementos hook stop"
547
+ })
548
+ };
549
+ }
550
+ function uninstallClaudeConfig(config) {
551
+ return {
552
+ ...config,
553
+ mcpServers: omitKey(recordValue(config.mcpServers), "mementos"),
554
+ hooks: removeAgentHooks(recordValue(config.hooks), [
555
+ "mementos hook user-prompt-submit",
556
+ "mementos hook session-start",
557
+ "mementos hook stop"
558
+ ])
559
+ };
560
+ }
561
+ function installOpenCodeConfig(config, plan) {
562
+ return {
563
+ ...config,
564
+ mcp: {
565
+ ...recordValue(config.mcp),
566
+ mementos: {
567
+ type: "local",
568
+ command: [plan.mcpCommand],
569
+ enabled: true,
570
+ environment: remoteEnv(baseApiUrlFromPlan(plan))
571
+ }
572
+ }
573
+ };
574
+ }
575
+ function uninstallOpenCodeConfig(config) {
576
+ return {
577
+ ...config,
578
+ mcp: omitKey(recordValue(config.mcp), "mementos")
579
+ };
580
+ }
581
+ function installAntigravityConfig(config, plan) {
582
+ return {
583
+ ...config,
584
+ mcpServers: {
585
+ ...recordValue(config.mcpServers),
586
+ mementos: stdioMcpServer(plan, false)
587
+ }
588
+ };
589
+ }
590
+ function uninstallMcpServersConfig(config) {
591
+ return {
592
+ ...config,
593
+ mcpServers: omitKey(recordValue(config.mcpServers), "mementos")
594
+ };
595
+ }
596
+ function installCodexToml(current, plan) {
597
+ const withoutExisting = removeTomlTable(removeManagedCodexBlock(current), "mcp_servers.mementos");
598
+ const block = [
599
+ "# BEGIN @hasna/mementos",
600
+ "[mcp_servers.mementos]",
601
+ `command = ${JSON.stringify(plan.mcpCommand)}`,
602
+ "[mcp_servers.mementos.env]",
603
+ `MEMENTOS_API_URL = ${JSON.stringify(baseApiUrlFromPlan(plan))}`,
604
+ 'MEMENTOS_API_TOKEN = "$MEMENTOS_API_TOKEN"',
605
+ "# END @hasna/mementos",
606
+ ""
607
+ ].join(`
608
+ `);
609
+ return `${withoutExisting.trimEnd()}
610
+
611
+ ${block}`;
612
+ }
613
+ function uninstallCodexToml(current) {
614
+ return `${removeTomlTable(removeManagedCodexBlock(current), "mcp_servers.mementos").trimEnd()}
615
+ `;
616
+ }
617
+ function mergeAgentHooks(hooks, commands) {
618
+ const next = { ...hooks };
619
+ for (const [event, command] of Object.entries(commands)) {
620
+ const current = next[event];
621
+ const entries = Array.isArray(current) ? [...current] : [];
622
+ if (!entries.some((entry) => JSON.stringify(entry).includes(command))) {
623
+ entries.push({ matcher: "", hooks: [{ type: "command", command }] });
624
+ }
625
+ next[event] = entries;
626
+ }
627
+ return next;
628
+ }
629
+ function removeAgentHooks(hooks, commands) {
630
+ const next = { ...hooks };
631
+ for (const [event, entries] of Object.entries(next)) {
632
+ if (!Array.isArray(entries))
633
+ continue;
634
+ next[event] = entries.filter((entry) => !commands.some((command) => JSON.stringify(entry).includes(command)));
635
+ }
636
+ return next;
637
+ }
638
+ function stdioMcpServer(plan, includeType) {
639
+ return {
640
+ ...includeType ? { type: "stdio" } : {},
641
+ command: plan.mcpCommand,
642
+ env: remoteEnv(baseApiUrlFromPlan(plan))
643
+ };
644
+ }
645
+ function remoteEnv(apiUrl) {
646
+ return {
647
+ MEMENTOS_API_URL: apiUrl,
648
+ MEMENTOS_API_TOKEN: "$MEMENTOS_API_TOKEN"
649
+ };
650
+ }
651
+ function baseApiUrlFromPlan(plan) {
652
+ const mcpFile = plan.files.find((file) => file.path.endsWith("mcp-server.json"));
653
+ if (!mcpFile)
654
+ return "https://mementos.md";
655
+ try {
656
+ const parsed = JSON.parse(mcpFile.content);
657
+ return parsed.mcpServers?.mementos?.env?.MEMENTOS_API_URL ?? "https://mementos.md";
658
+ } catch {
659
+ return "https://mementos.md";
660
+ }
661
+ }
662
+ function removeManagedCodexBlock(value) {
663
+ return value.replace(/\n?# BEGIN @hasna\/mementos\n[\s\S]*?# END @hasna\/mementos\n?/g, `
664
+ `);
665
+ }
666
+ function removeTomlTable(value, table) {
667
+ const tablePattern = table.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
668
+ const lines = value.split(`
669
+ `);
670
+ const output = [];
671
+ let skipping = false;
672
+ for (const line of lines) {
673
+ if (new RegExp(`^\\[${tablePattern}(?:\\.|\\])`).test(line.trim())) {
674
+ skipping = true;
675
+ continue;
676
+ }
677
+ if (skipping && /^\[[^\]]+\]/.test(line.trim()))
678
+ skipping = false;
679
+ if (!skipping)
680
+ output.push(line);
681
+ }
682
+ return output.join(`
683
+ `);
684
+ }
685
+ function readTextFile(path) {
686
+ return existsSync(path) ? readFileSync(path, "utf8") : "";
687
+ }
688
+ function readJsonObject(path) {
689
+ if (!existsSync(path))
690
+ return {};
691
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
692
+ if (!isRecord(parsed))
693
+ throw new Error(`${path} must contain a JSON object`);
694
+ return parsed;
695
+ }
696
+ function writeJsonObject(path, value) {
697
+ mkdirSync(dirname(path), { recursive: true });
698
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
699
+ `, { mode: 420 });
700
+ }
701
+ function recordValue(value) {
702
+ return isRecord(value) ? value : {};
703
+ }
704
+ function isRecord(value) {
705
+ return typeof value === "object" && value !== null && !Array.isArray(value);
706
+ }
707
+ function omitKey(record, key) {
708
+ const { [key]: _removed, ...rest } = record;
709
+ return rest;
710
+ }
476
711
  function renderInjectionInstructions({ apiUrl, injectCommand }) {
477
712
  return `# mementos.md Memory Injection
478
713
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/mementos",
3
- "version": "0.14.22",
3
+ "version": "0.14.23",
4
4
  "description": "Remote-only mementos.md SaaS CLI and MCP client",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",