@llmnesia/mcp 0.1.2 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,13 +9,13 @@ import {
9
9
  ingestFile,
10
10
  openSearchIndex,
11
11
  resolveCorpusDir
12
- } from "./chunk-7BW63ZTP.js";
12
+ } from "./chunk-3JQ7CGCP.js";
13
13
  import {
14
14
  VERSION,
15
15
  __commonJS,
16
16
  __require,
17
17
  __toESM
18
- } from "./chunk-BQM5Q4XX.js";
18
+ } from "./chunk-YVBAQ5VA.js";
19
19
 
20
20
  // ../node_modules/isexe/windows.js
21
21
  var require_windows = __commonJS({
@@ -533,6 +533,7 @@ var HOST_NAME = "com.llmnesia.claude_code";
533
533
  var STORE_EXTENSION_ID = "leekfgbdojiaabifbjbbgiiclannjdkf";
534
534
  var here = dirname(fileURLToPath(import.meta.url));
535
535
  var home = homedir();
536
+ var CLAUDE_PROJECTS_DIR = join(home, ".claude", "projects");
536
537
  var osPlatform = process.env.LLMNESIA_FAKE_PLATFORM || platform();
537
538
  function expandHome(p) {
538
539
  if (p === "~") {
@@ -547,6 +548,11 @@ function resolveCorpusDir2(explicit) {
547
548
  const raw = explicit ?? process.env.LLMNESIA_CORPUS_DIR ?? "~/.llmnesia/corpus";
548
549
  return expandHome(raw);
549
550
  }
551
+ function resolveCodexHome(explicit) {
552
+ const candidates = [explicit, process.env.CODEX_HOME, "~/.codex"];
553
+ const raw = candidates.find((value) => typeof value === "string" && value.trim() !== "");
554
+ return expandHome(raw.trim());
555
+ }
550
556
  async function exists(path) {
551
557
  try {
552
558
  await stat(path);
@@ -573,21 +579,24 @@ async function readExistingConfig(configPath) {
573
579
  if (err && typeof err === "object" && err.code === "ENOENT") {
574
580
  return null;
575
581
  }
576
- throw new Error(`Could not read existing native-host config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
582
+ throw new Error(`LLMnesia couldn't read its settings at ${configPath}: ${err instanceof Error ? err.message : String(err)}. Nothing was changed.`);
577
583
  }
578
584
  let parsed;
579
585
  try {
580
586
  parsed = JSON.parse(raw);
581
587
  } catch (err) {
582
588
  throw new Error(
583
- `Could not parse existing native-host config at ${configPath}: ${err instanceof Error ? err.message : String(err)}. The file was left unchanged.`
589
+ `LLMnesia found invalid JSON in its settings at ${configPath}: ${err instanceof Error ? err.message : String(err)}. Nothing was changed; repair or move that file aside, then retry.`
584
590
  );
585
591
  }
586
592
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
587
- throw new Error(`Existing native-host config at ${configPath} must be a JSON object. The file was left unchanged.`);
593
+ throw new Error(`LLMnesia settings at ${configPath} must contain a JSON object. Nothing was changed.`);
588
594
  }
589
595
  if ("corpusRoot" in parsed && typeof parsed.corpusRoot !== "string") {
590
- throw new Error(`Existing native-host config at ${configPath} has an invalid corpusRoot. The file was left unchanged.`);
596
+ throw new Error(`LLMnesia settings at ${configPath} contain an invalid conversation-data folder. Nothing was changed.`);
597
+ }
598
+ if ("codexHome" in parsed && typeof parsed.codexHome !== "string") {
599
+ throw new Error(`LLMnesia settings at ${configPath} contain an invalid Codex data folder. Nothing was changed.`);
591
600
  }
592
601
  return parsed;
593
602
  }
@@ -609,7 +618,7 @@ function posixBrowsers(homeDir = home, forPlatform = osPlatform) {
609
618
  ["Chromium", join(base, "chromium")]
610
619
  ];
611
620
  }
612
- async function installPosix(manifestBase, hostPath) {
621
+ async function installPosix(manifestBase, hostPath, log) {
613
622
  const manifest = { ...manifestBase, path: hostPath };
614
623
  let installed = 0;
615
624
  for (const [label, browserDir] of posixBrowsers()) {
@@ -620,7 +629,7 @@ async function installPosix(manifestBase, hostPath) {
620
629
  await mkdir(dir, { recursive: true });
621
630
  await writeFile(join(dir, `${HOST_NAME}.json`), `${JSON.stringify(manifest, null, 2)}
622
631
  `, "utf8");
623
- console.log(`Manifest \u2192 ${label}: ${join(dir, `${HOST_NAME}.json`)}`);
632
+ log(`\u2713 ${label} connected`);
624
633
  installed += 1;
625
634
  }
626
635
  return installed;
@@ -634,17 +643,16 @@ function windowsBrowsers() {
634
643
  ["Chromium", join(localAppData, "Chromium", "User Data"), "Software\\Chromium"]
635
644
  ];
636
645
  }
637
- async function installWindows(manifestBase, hostPath, installDir) {
646
+ async function installWindows(manifestBase, hostPath, installDir, log) {
638
647
  const batPath = join(installDir, "llmnesia-claude-host.bat");
639
648
  await writeFile(batPath, `@echo off\r
640
649
  "${process.execPath}" "${hostPath}" %*\r
641
650
  `, "utf8");
642
- console.log(`Launcher \u2192 ${batPath}`);
651
+ log("\u2713 Windows launcher ready");
643
652
  const manifest = { ...manifestBase, path: batPath };
644
653
  const manifestPath = join(installDir, `${HOST_NAME}.json`);
645
654
  await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
646
655
  `, "utf8");
647
- console.log(`Manifest \u2192 ${manifestPath}`);
648
656
  let installed = 0;
649
657
  for (const [label, userDataDir, vendorKey] of windowsBrowsers()) {
650
658
  if (!await exists(userDataDir)) {
@@ -653,21 +661,27 @@ async function installWindows(manifestBase, hostPath, installDir) {
653
661
  const key = `HKCU\\${vendorKey}\\NativeMessagingHosts\\${HOST_NAME}`;
654
662
  const regExe = join(process.env.SystemRoot || "C:\\Windows", "System32", "reg.exe");
655
663
  await execFileAsync(regExe, ["add", key, "/ve", "/t", "REG_SZ", "/d", manifestPath, "/f"]);
656
- console.log(`Registry \u2192 ${label}: ${key}`);
664
+ log(`\u2713 ${label} connected`);
657
665
  installed += 1;
658
666
  }
659
667
  return installed;
660
668
  }
661
- async function installNativeHost({ ids = [], corpus, force = false } = {}) {
669
+ async function installNativeHost({ ids = [], corpus, codexHome, force = false, quiet = false } = {}) {
670
+ const log = quiet ? () => {
671
+ } : console.log;
662
672
  const isWindows2 = osPlatform === "win32";
663
673
  const invalidId = ids.find((id) => !/^[a-p]{32}$/.test(id));
664
674
  if (invalidId) {
665
- throw new Error(`Invalid Chromium extension id: ${JSON.stringify(invalidId)}. Expected 32 letters from a through p.`);
675
+ throw new Error(
676
+ `${JSON.stringify(invalidId)} is not a Chromium extension ID. Only unpacked/development builds need an ID; it must be 32 letters from a through p. Run with --help for an example.`
677
+ );
666
678
  }
667
679
  const allIds = [.../* @__PURE__ */ new Set([STORE_EXTENSION_ID, ...ids])];
668
680
  const allowedOrigins = allIds.map((id) => `chrome-extension://${id}/`);
669
681
  if (await supportedBrowserCount(isWindows2) === 0) {
670
- throw new Error("No supported Chromium browser found (Chrome, Brave, Edge, or Chromium) \u2014 the browser helper was not installed.");
682
+ throw new Error(
683
+ "LLMnesia couldn't find Chrome, Brave, Edge, or Chromium. Open your browser once so it creates a profile, then retry. Safari and Firefox are not supported yet. Nothing was installed."
684
+ );
671
685
  }
672
686
  const installDir = join(home, ".llmnesia");
673
687
  const configPath = join(installDir, "config.json");
@@ -681,38 +695,68 @@ async function installNativeHost({ ids = [], corpus, force = false } = {}) {
681
695
  if (!isWindows2) {
682
696
  await chmod(hostPath, 493);
683
697
  }
684
- console.log(`Host script \u2192 ${hostPath}`);
698
+ log("\u2713 Helper installed");
685
699
  const manifestBase = {
686
700
  name: HOST_NAME,
687
- description: "LLMnesia \u2014 reads local Claude Code sessions",
701
+ description: "LLMnesia \u2014 reads local Claude Code and Codex sessions",
688
702
  type: "stdio",
689
703
  allowed_origins: allowedOrigins
690
704
  };
691
- const installedCount = isWindows2 ? await installWindows(manifestBase, hostPath, installDir) : await installPosix(manifestBase, hostPath);
705
+ const installedCount = isWindows2 ? await installWindows(manifestBase, hostPath, installDir, log) : await installPosix(manifestBase, hostPath, log);
692
706
  const corpusRoot = resolveCorpusDir2(corpus);
707
+ const resolvedCodexHome = resolveCodexHome(codexHome);
693
708
  const existingRoot = existing && typeof existing.corpusRoot === "string" ? existing.corpusRoot : null;
694
709
  let corpusRootChanged = true;
695
710
  if (existingRoot && existingRoot !== corpusRoot && !force) {
696
711
  corpusRootChanged = false;
697
- console.log(`Corpus config kept \u2192 ${configPath}`);
698
- console.log(` already points at: ${existingRoot}`);
699
- console.log(` to change it: re-run with --corpus <dir> --force`);
712
+ await writeFile(
713
+ configPath,
714
+ `${JSON.stringify({ ...existing, corpusRoot: existingRoot, codexHome: resolvedCodexHome }, null, 2)}
715
+ `,
716
+ { encoding: "utf8", mode: 384 }
717
+ );
718
+ await chmod(configPath, 384);
719
+ log("\u2713 Kept your existing conversation-data folder");
720
+ log(` Conversation data: ${existingRoot}`);
721
+ log(` To change it: re-run with --corpus <dir> --force`);
722
+ log(` Codex data: ${resolvedCodexHome}`);
700
723
  } else {
701
- await writeFile(configPath, `${JSON.stringify({ corpusRoot }, null, 2)}
702
- `, { encoding: "utf8", mode: 384 });
724
+ await writeFile(
725
+ configPath,
726
+ `${JSON.stringify({ ...existing ?? {}, corpusRoot, codexHome: resolvedCodexHome }, null, 2)}
727
+ `,
728
+ { encoding: "utf8", mode: 384 }
729
+ );
703
730
  await chmod(configPath, 384);
704
- console.log(`Corpus config \u2192 ${configPath}`);
705
- console.log(` corpus root: ${corpusRoot}`);
731
+ log("\u2713 Settings saved");
732
+ log(` Conversation data: ${corpusRoot}`);
733
+ log(` Codex data: ${resolvedCodexHome}`);
734
+ }
735
+ const [hasClaudeCode, hasCodex] = await Promise.all([
736
+ exists(CLAUDE_PROJECTS_DIR),
737
+ exists(join(resolvedCodexHome, "sessions"))
738
+ ]);
739
+ log("");
740
+ log(
741
+ hasClaudeCode ? `\u2713 Found Claude Code sessions at ${CLAUDE_PROJECTS_DIR}` : `\u2013 No Claude Code sessions found yet (fine if you don't use Claude Code).`
742
+ );
743
+ log(
744
+ hasCodex ? `\u2713 Found Codex sessions at ${resolvedCodexHome}` : `\u2013 No Codex sessions found yet (fine if you don't use Codex).`
745
+ );
746
+ const extraIdCount = allIds.length - 1;
747
+ if (extraIdCount > 0) {
748
+ log(`\u2713 Also authorized ${extraIdCount} unpacked extension ${extraIdCount === 1 ? "build" : "builds"}`);
706
749
  }
707
- console.log(`
708
- Done. Allowed extension id(s): ${allIds.join(", ")}`);
709
- console.log("Fully quit and reopen the browser, then open LLMnesia Settings.");
750
+ log("\nAll set \u2014 LLMnesia can now read your local coding-agent sessions.");
751
+ log("Next: fully quit your browser (not just close its window), reopen it, then open LLMnesia Settings.");
752
+ log("If it doesn't connect, run: npx -y @llmnesia/local-agent-host@latest doctor");
710
753
  return {
711
754
  installedCount,
712
755
  allowedIds: allIds,
713
756
  configPath,
714
757
  corpusRoot: existingRoot && !corpusRootChanged ? existingRoot : corpusRoot,
715
- corpusRootChanged
758
+ corpusRootChanged,
759
+ codexHome: resolvedCodexHome
716
760
  };
717
761
  }
718
762
  async function uninstallNativeHost() {
@@ -2568,6 +2612,7 @@ import { dirname as dirname4, join as join4 } from "path";
2568
2612
  import { promisify as promisify2 } from "util";
2569
2613
  var execFileAsync2 = promisify2(execFile2);
2570
2614
  var SERVER_NAME = "llmnesia";
2615
+ var CODEX_APPROVAL_MODE = "writes";
2571
2616
  async function commandOnPath(name) {
2572
2617
  try {
2573
2618
  const { stdout } = await execFileAsync2(process.platform === "win32" ? "where" : "which", [name]);
@@ -2910,7 +2955,7 @@ claude mcp add --scope user ${SERVER_NAME} -- ${serve.command} ${serve.args.join
2910
2955
  function sameServeCommand(left, right) {
2911
2956
  return left.command === right.command && JSON.stringify(left.args) === JSON.stringify(right.args);
2912
2957
  }
2913
- async function registerCodex(serve, cli, configPath, confirm) {
2958
+ async function registerCodex(serve, cli, configPath, confirm, now) {
2914
2959
  if (!await cli.detect()) {
2915
2960
  if (!await clientDirExists(configPath)) {
2916
2961
  return {
@@ -2919,12 +2964,34 @@ async function registerCodex(serve, cli, configPath, confirm) {
2919
2964
  detail: `Not installed (no Codex CLI and no ${dirname4(configPath)}).`
2920
2965
  };
2921
2966
  }
2922
- return {
2923
- client: "Codex",
2924
- action: "printed",
2925
- detail: `Codex's CLI was not found. Add this to ${configPath}:
2926
- ${codexBlock(serve)}Then fully quit and reopen Codex.`
2927
- };
2967
+ try {
2968
+ const raw = await readFile2(configPath, "utf8");
2969
+ if (renderCodexConfig(raw, serve) === raw) {
2970
+ return { client: "Codex", action: "skipped", detail: `Already registered at ${configPath}.` };
2971
+ }
2972
+ } catch (err) {
2973
+ if (!(isErrnoException(err) && err.code === "ENOENT")) {
2974
+ return { client: "Codex", action: "error", detail: `Could not inspect ${configPath}: ${errorMessage(err)}` };
2975
+ }
2976
+ }
2977
+ const ok2 = await confirm(
2978
+ `Codex: update ${configPath} to register LLMnesia? Existing settings are preserved and backed up.
2979
+ Add:
2980
+ ${codexBlock(serve)}`
2981
+ );
2982
+ if (!ok2) {
2983
+ return { client: "Codex", action: "declined", detail: "Skipped by user." };
2984
+ }
2985
+ try {
2986
+ const update = await writeCodexConfig(configPath, serve, now);
2987
+ return {
2988
+ client: "Codex",
2989
+ action: update.changed ? "registered" : "skipped",
2990
+ detail: update.changed ? `Wrote ${configPath}${update.backupPath ? ` (backup: ${update.backupPath})` : ""}.` : `Already registered at ${configPath}.`
2991
+ };
2992
+ } catch (err) {
2993
+ return { client: "Codex", action: "error", detail: `Could not update ${configPath}: ${errorMessage(err)}` };
2994
+ }
2928
2995
  }
2929
2996
  let existing;
2930
2997
  try {
@@ -2933,7 +3000,20 @@ ${codexBlock(serve)}Then fully quit and reopen Codex.`
2933
3000
  return { client: "Codex", action: "error", detail: `Could not inspect Codex MCP settings: ${errorMessage(err)}` };
2934
3001
  }
2935
3002
  if (existing?.serve && sameServeCommand(existing.serve, serve)) {
2936
- return { client: "Codex", action: "skipped", detail: "Already registered." };
3003
+ try {
3004
+ const update = await writeCodexConfig(configPath, serve, now);
3005
+ return {
3006
+ client: "Codex",
3007
+ action: update.changed ? "registered" : "skipped",
3008
+ detail: update.changed ? `Already registered; updated tool approval policy in ${configPath}.` : "Already registered."
3009
+ };
3010
+ } catch (err) {
3011
+ return {
3012
+ client: "Codex",
3013
+ action: "error",
3014
+ detail: `Already registered, but could not set its tool approval policy: ${errorMessage(err)}`
3015
+ };
3016
+ }
2937
3017
  }
2938
3018
  if (existing && !existing.serve) {
2939
3019
  return {
@@ -2973,6 +3053,15 @@ codex mcp add ${SERVER_NAME} -- ${serve.command} ${serve.args.join(" ")}`
2973
3053
  }
2974
3054
  return { client: "Codex", action: "error", detail: `codex mcp add failed: ${errorMessage(err)}` };
2975
3055
  }
3056
+ try {
3057
+ await writeCodexConfig(configPath, serve, now);
3058
+ } catch (err) {
3059
+ return {
3060
+ client: "Codex",
3061
+ action: "error",
3062
+ detail: `Registered the server, but could not set its tool approval policy in ${configPath}: ${errorMessage(err)}`
3063
+ };
3064
+ }
2976
3065
  return {
2977
3066
  client: "Codex",
2978
3067
  action: "registered",
@@ -3006,8 +3095,9 @@ async function registerJsonClient(clientLabel, configPath, serve, confirm, now)
3006
3095
  if (already && JSON.stringify(already) === JSON.stringify(nextEntry)) {
3007
3096
  return { client: clientLabel, action: "skipped", detail: `Already registered at ${configPath}.` };
3008
3097
  }
3009
- const preview = JSON.stringify({ mcpServers: { ...servers, [SERVER_NAME]: nextEntry } }, null, 2);
3010
- const message = existed ? `${clientLabel}: update ${configPath} to add the "${SERVER_NAME}" MCP server?
3098
+ const preview = JSON.stringify({ mcpServers: { [SERVER_NAME]: nextEntry } }, null, 2);
3099
+ const message = existed ? `${clientLabel}: update ${configPath} to add the "${SERVER_NAME}" MCP server? Existing servers and settings are preserved.
3100
+ Add:
3011
3101
  ${preview}` : `${clientLabel}: create ${configPath} with the "${SERVER_NAME}" MCP server?
3012
3102
  ${preview}`;
3013
3103
  const ok = await confirm(message);
@@ -3032,8 +3122,82 @@ function codexBlock(serve) {
3032
3122
  return `[mcp_servers.${SERVER_NAME}]
3033
3123
  command = ${JSON.stringify(serve.command)}
3034
3124
  args = [${argsToml}]
3125
+ default_tools_approval_mode = ${JSON.stringify(CODEX_APPROVAL_MODE)}
3035
3126
  `;
3036
3127
  }
3128
+ function codexHeaders(raw) {
3129
+ const headers = [];
3130
+ const pattern = /^[ \t]*\[([^\]\r\n]+)\][ \t]*(?:#.*)?$/gm;
3131
+ for (let match = pattern.exec(raw); match; match = pattern.exec(raw)) {
3132
+ headers.push({ start: match.index, name: match[1].replace(/[ \t]/g, "") });
3133
+ }
3134
+ return headers;
3135
+ }
3136
+ function isCodexRootHeader(name) {
3137
+ return /^mcp_servers\.(?:llmnesia|"llmnesia"|'llmnesia')$/.test(name);
3138
+ }
3139
+ function isCodexChildHeader(name) {
3140
+ return /^mcp_servers\.(?:llmnesia|"llmnesia"|'llmnesia')\./.test(name);
3141
+ }
3142
+ function codexSectionRange(raw) {
3143
+ const headers = codexHeaders(raw);
3144
+ const roots = headers.filter((header) => isCodexRootHeader(header.name));
3145
+ if (roots.length > 1) {
3146
+ throw new Error(`found more than one [mcp_servers.${SERVER_NAME}] table; the file was left unchanged`);
3147
+ }
3148
+ const root = roots[0];
3149
+ if (!root) {
3150
+ if (headers.some((header) => isCodexChildHeader(header.name))) {
3151
+ throw new Error(
3152
+ `found LLMnesia MCP subtables without a [mcp_servers.${SERVER_NAME}] table; the file was left unchanged`
3153
+ );
3154
+ }
3155
+ return null;
3156
+ }
3157
+ const rootIndex = headers.indexOf(root);
3158
+ let end = raw.length;
3159
+ for (let i = rootIndex + 1; i < headers.length; i += 1) {
3160
+ if (!isCodexChildHeader(headers[i].name)) {
3161
+ end = headers[i].start;
3162
+ break;
3163
+ }
3164
+ }
3165
+ return { start: root.start, end };
3166
+ }
3167
+ function renderCodexConfig(raw, serve) {
3168
+ const newline = raw.includes("\r\n") ? "\r\n" : "\n";
3169
+ const block = codexBlock(serve).replace(/\n/g, newline);
3170
+ const range = codexSectionRange(raw);
3171
+ if (range) {
3172
+ return `${raw.slice(0, range.start)}${block}${raw.slice(range.end)}`;
3173
+ }
3174
+ const base = raw.replace(/[ \t\r\n]+$/g, "");
3175
+ return `${base}${base ? `${newline}${newline}` : ""}${block}`;
3176
+ }
3177
+ async function writeCodexConfig(configPath, serve, now) {
3178
+ let raw = "";
3179
+ let existed = true;
3180
+ try {
3181
+ raw = await readFile2(configPath, "utf8");
3182
+ } catch (err) {
3183
+ if (isErrnoException(err) && err.code === "ENOENT") {
3184
+ existed = false;
3185
+ } else {
3186
+ throw err;
3187
+ }
3188
+ }
3189
+ const next = renderCodexConfig(raw, serve);
3190
+ if (next === raw) {
3191
+ return { changed: false };
3192
+ }
3193
+ let backupPath;
3194
+ if (existed) {
3195
+ backupPath = `${configPath}.bak-${now()}`;
3196
+ await writeFile2(backupPath, raw, "utf8");
3197
+ }
3198
+ await writeFile2(configPath, next, { encoding: "utf8", mode: 384 });
3199
+ return { changed: true, ...backupPath ? { backupPath } : {} };
3200
+ }
3037
3201
  function genericBlock(serve) {
3038
3202
  return JSON.stringify({ mcpServers: { [SERVER_NAME]: { command: serve.command, args: serve.args } } }, null, 2);
3039
3203
  }
@@ -3066,7 +3230,10 @@ async function registerClients(options) {
3066
3230
  }
3067
3231
  results.push(await safe("Cursor", () => registerJsonClient("Cursor", cursor, options.serve, options.confirm, now)));
3068
3232
  results.push(
3069
- await safe("Codex", () => registerCodex(options.serve, options.codexCli ?? realCodexCli, codex, options.confirm))
3233
+ await safe(
3234
+ "Codex",
3235
+ () => registerCodex(options.serve, options.codexCli ?? realCodexCli, codex, options.confirm, now)
3236
+ )
3070
3237
  );
3071
3238
  results.push({
3072
3239
  client: "Other MCP clients (Windsurf, Zed, LM Studio, ...)",
@@ -3108,20 +3275,27 @@ async function unregisterClaudeCode(cli) {
3108
3275
  };
3109
3276
  }
3110
3277
  }
3111
- async function unregisterCodex(cli, configPath) {
3278
+ async function unregisterCodex(cli, configPath, now) {
3112
3279
  if (!await cli.detect()) {
3113
3280
  try {
3114
- const text = await readFile2(configPath, "utf8");
3115
- if (new RegExp(`^\\s*\\[mcp_servers\\.${SERVER_NAME}\\]\\s*$`, "m").test(text)) {
3116
- return {
3117
- client: "Codex",
3118
- action: "error",
3119
- detail: `A registration remains in ${configPath}, but the Codex CLI is unavailable to remove it safely. Restore the CLI and retry. The runtime was retained.`
3120
- };
3281
+ const raw = await readFile2(configPath, "utf8");
3282
+ const range = codexSectionRange(raw);
3283
+ if (!range) {
3284
+ return { client: "Codex", action: "skipped", detail: "`codex` not found and no registration was detected." };
3121
3285
  }
3286
+ const backupPath = `${configPath}.bak-${now()}`;
3287
+ await writeFile2(backupPath, raw, "utf8");
3288
+ const next = `${raw.slice(0, range.start)}${raw.slice(range.end)}`.replace(/[ \t\r\n]+$/g, "");
3289
+ await writeFile2(configPath, next ? `${next}
3290
+ ` : "", "utf8");
3291
+ return {
3292
+ client: "Codex",
3293
+ action: "registered",
3294
+ detail: `Removed from ${configPath} (backup: ${backupPath}).`
3295
+ };
3122
3296
  } catch (err) {
3123
3297
  if (!(isErrnoException(err) && err.code === "ENOENT")) {
3124
- return { client: "Codex", action: "error", detail: `Could not inspect ${configPath}: ${errorMessage(err)}` };
3298
+ return { client: "Codex", action: "error", detail: `Could not update ${configPath}: ${errorMessage(err)}` };
3125
3299
  }
3126
3300
  }
3127
3301
  return { client: "Codex", action: "skipped", detail: "`codex` not found and no registration was detected." };
@@ -3189,7 +3363,7 @@ async function unregisterClients(options = {}) {
3189
3363
  results.push(await safe("Claude Desktop", () => unregisterJsonClient("Claude Desktop", claudeDesktop, now)));
3190
3364
  }
3191
3365
  results.push(await safe("Cursor", () => unregisterJsonClient("Cursor", cursor, now)));
3192
- results.push(await safe("Codex", () => unregisterCodex(options.codexCli ?? realCodexCli, codex)));
3366
+ results.push(await safe("Codex", () => unregisterCodex(options.codexCli ?? realCodexCli, codex, now)));
3193
3367
  results.push({
3194
3368
  client: "Other MCP clients",
3195
3369
  action: "skipped",
@@ -3594,7 +3768,7 @@ Options:
3594
3768
  --from <iso|ms> search: only conversations created on/after this date
3595
3769
  --to <iso|ms> search: only conversations created on/before this date
3596
3770
  --force install: overwrite a differing corpus root in config.json
3597
- --yes, -y install/uninstall: skip confirmation prompts
3771
+ --yes, -y uninstall: skip its confirmation prompt
3598
3772
  --version, -v print the version and exit
3599
3773
  `;
3600
3774
  function parseDate(value) {
@@ -3621,38 +3795,58 @@ ${message}
3621
3795
  }
3622
3796
  function printRegistrationResults(results) {
3623
3797
  for (const r of results) {
3624
- const icon = r.action === "registered" ? "\u2713" : r.action === "error" ? "\u2717" : "\u2022";
3625
- process.stdout.write(`${icon} ${r.client}: ${r.detail}
3626
-
3798
+ if (r.client.startsWith("Other MCP clients")) {
3799
+ continue;
3800
+ }
3801
+ if (r.action === "registered") {
3802
+ process.stdout.write(`\u2713 ${r.client}
3803
+ `);
3804
+ continue;
3805
+ }
3806
+ if (r.action === "skipped" && r.detail.startsWith("Already registered")) {
3807
+ process.stdout.write(`\u2713 ${r.client} (already configured)
3627
3808
  `);
3809
+ continue;
3810
+ }
3811
+ if (r.action === "error") {
3812
+ process.stderr.write(`\u2717 ${r.client}: ${r.detail}
3813
+ `);
3814
+ continue;
3815
+ }
3816
+ if (r.action === "printed") {
3817
+ process.stdout.write(`\u2022 ${r.client}: ${r.detail}
3818
+ `);
3819
+ }
3628
3820
  }
3629
3821
  }
3630
3822
  async function runInstall(positionals, options, flags) {
3631
- const skipConfirm = flags.has("yes");
3632
3823
  let hadProblems = false;
3633
- process.stdout.write("Installing native messaging host\u2026\n");
3824
+ process.stdout.write("Installing LLMnesia MCP\u2026\n");
3634
3825
  try {
3635
- await installNativeHost({ ids: positionals, corpus: options.corpus, force: flags.has("force") });
3826
+ const host = await installNativeHost({
3827
+ ids: positionals,
3828
+ corpus: options.corpus,
3829
+ force: flags.has("force"),
3830
+ quiet: true
3831
+ });
3832
+ process.stdout.write(`\u2713 Browser helper (${host.installedCount} browser${host.installedCount === 1 ? "" : "s"})
3833
+ `);
3636
3834
  } catch (err) {
3637
3835
  hadProblems = true;
3638
- process.stderr.write(`
3639
- Native host install failed: ${err instanceof Error ? err.message : String(err)}
3836
+ process.stderr.write(`\u2717 Browser helper: ${err instanceof Error ? err.message : String(err)}
3640
3837
  `);
3641
- process.stderr.write("Continuing to register MCP clients anyway \u2014 the server itself doesn't need a browser.\n");
3642
3838
  }
3643
- process.stdout.write("\nInstalling the MCP server runtime\u2026\n");
3644
3839
  const runtime = await installRuntime();
3645
- process.stdout.write(`Installed to ${runtime.binDir} (running on ${runtime.nodePath})
3646
- `);
3647
- process.stdout.write("\nRegistering MCP clients\u2026\n\n");
3648
- const confirm = skipConfirm ? async () => true : promptConfirm;
3649
- const results = await registerClients({ serve: runtime.serve, confirm });
3840
+ process.stdout.write("\u2713 MCP runtime\n");
3841
+ const results = await registerClients({ serve: runtime.serve, confirm: async () => true });
3650
3842
  printRegistrationResults(results);
3651
3843
  hadProblems ||= results.some((result) => result.action === "error");
3652
3844
  process.stdout.write(
3653
- "Next step: fully quit and reopen your MCP client (and your browser, if you\nuse the extension). Reloading a window is not enough \u2014 the config is read\nonce at startup.\n\nFor automatic browser-history sync, open LLMnesia Settings and click\n\u2018Enable automatic sync\u2019 once. Chrome requires that one permission click;\nthe existing-history backfill and future updates are automatic after it.\n\n"
3845
+ "\nDesktop helper installed. Fully quit and reopen your MCP apps and browser.\nBrowser sync is required or your MCP history will be empty. If you did not start in LLMnesia Settings, open it and click \u2018Enable automatic sync\u2019 once. LLMnesia will finish the first sync automatically.\n"
3654
3846
  );
3655
- process.stdout.write("Then run `npx -y @llmnesia/mcp@latest doctor` to check what's wired up.\n");
3847
+ const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
3848
+ process.stdout.write(`Then run \`${npxCommand} -y @llmnesia/mcp@latest doctor\` to check what's wired up.
3849
+ `);
3656
3850
  if (hadProblems) {
3657
3851
  process.stderr.write(
3658
3852
  "\nInstall completed with problems. Review the error messages above; no failure was silently treated as success.\n"