@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.
- package/README.md +33 -19
- package/dist/{chunk-7BW63ZTP.js → chunk-3JQ7CGCP.js} +6 -1
- package/dist/{chunk-BQM5Q4XX.js → chunk-YVBAQ5VA.js} +1 -1
- package/dist/{cli-CNIF4XT3.js → cli-TVE63AWC.js} +262 -68
- package/dist/cli.js +104 -9
- package/dist/llmnesia-claude-host.mjs +226 -36
- package/dist/mcp.js +77 -7
- package/package.json +2 -2
- package/server.json +2 -2
|
@@ -9,13 +9,13 @@ import {
|
|
|
9
9
|
ingestFile,
|
|
10
10
|
openSearchIndex,
|
|
11
11
|
resolveCorpusDir
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-3JQ7CGCP.js";
|
|
13
13
|
import {
|
|
14
14
|
VERSION,
|
|
15
15
|
__commonJS,
|
|
16
16
|
__require,
|
|
17
17
|
__toESM
|
|
18
|
-
} from "./chunk-
|
|
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(`
|
|
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
|
-
`
|
|
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(`
|
|
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(`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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(
|
|
702
|
-
|
|
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
|
-
|
|
705
|
-
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
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
|
-
|
|
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: {
|
|
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(
|
|
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
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
3625
|
-
|
|
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
|
|
3824
|
+
process.stdout.write("Installing LLMnesia MCP\u2026\n");
|
|
3634
3825
|
try {
|
|
3635
|
-
await installNativeHost({
|
|
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(
|
|
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
|
-
"
|
|
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
|
-
|
|
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"
|