@llmnesia/mcp 0.1.3 → 0.1.6
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-EBVGUZX2.js → chunk-JBCYML3I.js} +1 -1
- package/dist/{cli-JFZO44NP.js → cli-LNUL4QWX.js} +326 -99
- 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
|
@@ -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-JBCYML3I.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
|
}
|
|
@@ -620,7 +629,7 @@ async function installPosix(manifestBase, hostPath, log) {
|
|
|
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
|
-
log(
|
|
632
|
+
log(`\u2713 ${label} connected`);
|
|
624
633
|
installed += 1;
|
|
625
634
|
}
|
|
626
635
|
return installed;
|
|
@@ -639,12 +648,11 @@ async function installWindows(manifestBase, hostPath, installDir, log) {
|
|
|
639
648
|
await writeFile(batPath, `@echo off\r
|
|
640
649
|
"${process.execPath}" "${hostPath}" %*\r
|
|
641
650
|
`, "utf8");
|
|
642
|
-
log(
|
|
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
|
-
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,23 +661,27 @@ async function installWindows(manifestBase, hostPath, installDir, log) {
|
|
|
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
|
-
log(
|
|
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, quiet = false } = {}) {
|
|
669
|
+
async function installNativeHost({ ids = [], corpus, codexHome, force = false, quiet = false } = {}) {
|
|
662
670
|
const log = quiet ? () => {
|
|
663
671
|
} : console.log;
|
|
664
672
|
const isWindows2 = osPlatform === "win32";
|
|
665
673
|
const invalidId = ids.find((id) => !/^[a-p]{32}$/.test(id));
|
|
666
674
|
if (invalidId) {
|
|
667
|
-
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
|
+
);
|
|
668
678
|
}
|
|
669
679
|
const allIds = [.../* @__PURE__ */ new Set([STORE_EXTENSION_ID, ...ids])];
|
|
670
680
|
const allowedOrigins = allIds.map((id) => `chrome-extension://${id}/`);
|
|
671
681
|
if (await supportedBrowserCount(isWindows2) === 0) {
|
|
672
|
-
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
|
+
);
|
|
673
685
|
}
|
|
674
686
|
const installDir = join(home, ".llmnesia");
|
|
675
687
|
const configPath = join(installDir, "config.json");
|
|
@@ -683,38 +695,68 @@ async function installNativeHost({ ids = [], corpus, force = false, quiet = fals
|
|
|
683
695
|
if (!isWindows2) {
|
|
684
696
|
await chmod(hostPath, 493);
|
|
685
697
|
}
|
|
686
|
-
log(
|
|
698
|
+
log("\u2713 Helper installed");
|
|
687
699
|
const manifestBase = {
|
|
688
700
|
name: HOST_NAME,
|
|
689
|
-
description: "LLMnesia \u2014 reads local Claude Code sessions",
|
|
701
|
+
description: "LLMnesia \u2014 reads local Claude Code and Codex sessions",
|
|
690
702
|
type: "stdio",
|
|
691
703
|
allowed_origins: allowedOrigins
|
|
692
704
|
};
|
|
693
705
|
const installedCount = isWindows2 ? await installWindows(manifestBase, hostPath, installDir, log) : await installPosix(manifestBase, hostPath, log);
|
|
694
706
|
const corpusRoot = resolveCorpusDir2(corpus);
|
|
707
|
+
const resolvedCodexHome = resolveCodexHome(codexHome);
|
|
695
708
|
const existingRoot = existing && typeof existing.corpusRoot === "string" ? existing.corpusRoot : null;
|
|
696
709
|
let corpusRootChanged = true;
|
|
697
710
|
if (existingRoot && existingRoot !== corpusRoot && !force) {
|
|
698
711
|
corpusRootChanged = false;
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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}`);
|
|
702
723
|
} else {
|
|
703
|
-
await writeFile(
|
|
704
|
-
|
|
724
|
+
await writeFile(
|
|
725
|
+
configPath,
|
|
726
|
+
`${JSON.stringify({ ...existing ?? {}, corpusRoot, codexHome: resolvedCodexHome }, null, 2)}
|
|
727
|
+
`,
|
|
728
|
+
{ encoding: "utf8", mode: 384 }
|
|
729
|
+
);
|
|
705
730
|
await chmod(configPath, 384);
|
|
706
|
-
log(
|
|
707
|
-
log(`
|
|
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"}`);
|
|
708
749
|
}
|
|
709
|
-
log(
|
|
710
|
-
|
|
711
|
-
log("
|
|
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");
|
|
712
753
|
return {
|
|
713
754
|
installedCount,
|
|
714
755
|
allowedIds: allIds,
|
|
715
756
|
configPath,
|
|
716
757
|
corpusRoot: existingRoot && !corpusRootChanged ? existingRoot : corpusRoot,
|
|
717
|
-
corpusRootChanged
|
|
758
|
+
corpusRootChanged,
|
|
759
|
+
codexHome: resolvedCodexHome
|
|
718
760
|
};
|
|
719
761
|
}
|
|
720
762
|
async function uninstallNativeHost() {
|
|
@@ -2564,12 +2606,13 @@ async function watchInbox(corpus, onDrain) {
|
|
|
2564
2606
|
// src/clientRegistration.ts
|
|
2565
2607
|
var import_cross_spawn = __toESM(require_cross_spawn(), 1);
|
|
2566
2608
|
import { execFile as execFile2 } from "child_process";
|
|
2567
|
-
import { access, readFile as readFile2, stat as stat5, writeFile as writeFile2 } from "fs/promises";
|
|
2609
|
+
import { access, mkdir as mkdir2, readFile as readFile2, stat as stat5, writeFile as writeFile2 } from "fs/promises";
|
|
2568
2610
|
import { homedir as homedir2, platform as platform2 } from "os";
|
|
2569
2611
|
import { dirname as dirname4, join as join4 } from "path";
|
|
2570
2612
|
import { promisify as promisify2 } from "util";
|
|
2571
2613
|
var execFileAsync2 = promisify2(execFile2);
|
|
2572
2614
|
var SERVER_NAME = "llmnesia";
|
|
2615
|
+
var CODEX_APPROVAL_MODE = "writes";
|
|
2573
2616
|
async function commandOnPath(name) {
|
|
2574
2617
|
try {
|
|
2575
2618
|
const { stdout } = await execFileAsync2(process.platform === "win32" ? "where" : "which", [name]);
|
|
@@ -2912,21 +2955,47 @@ claude mcp add --scope user ${SERVER_NAME} -- ${serve.command} ${serve.args.join
|
|
|
2912
2955
|
function sameServeCommand(left, right) {
|
|
2913
2956
|
return left.command === right.command && JSON.stringify(left.args) === JSON.stringify(right.args);
|
|
2914
2957
|
}
|
|
2915
|
-
async function registerCodex(serve, cli, configPath, confirm) {
|
|
2958
|
+
async function registerCodex(serve, cli, configPath, confirm, now, prepareMissing = false) {
|
|
2916
2959
|
if (!await cli.detect()) {
|
|
2917
|
-
|
|
2960
|
+
const configDirExists = await clientDirExists(configPath);
|
|
2961
|
+
if (!configDirExists && !prepareMissing) {
|
|
2918
2962
|
return {
|
|
2919
2963
|
client: "Codex",
|
|
2920
2964
|
action: "skipped",
|
|
2921
2965
|
detail: `Not installed (no Codex CLI and no ${dirname4(configPath)}).`
|
|
2922
2966
|
};
|
|
2923
2967
|
}
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2968
|
+
if (!configDirExists) {
|
|
2969
|
+
await mkdir2(dirname4(configPath), { recursive: true });
|
|
2970
|
+
}
|
|
2971
|
+
try {
|
|
2972
|
+
const raw = await readFile2(configPath, "utf8");
|
|
2973
|
+
if (renderCodexConfig(raw, serve) === raw) {
|
|
2974
|
+
return { client: "Codex", action: "skipped", detail: `Already registered at ${configPath}.` };
|
|
2975
|
+
}
|
|
2976
|
+
} catch (err) {
|
|
2977
|
+
if (!(isErrnoException(err) && err.code === "ENOENT")) {
|
|
2978
|
+
return { client: "Codex", action: "error", detail: `Could not inspect ${configPath}: ${errorMessage(err)}` };
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
const ok2 = await confirm(
|
|
2982
|
+
`Codex: update ${configPath} to register LLMnesia? Existing settings are preserved and backed up.
|
|
2983
|
+
Add:
|
|
2984
|
+
${codexBlock(serve)}`
|
|
2985
|
+
);
|
|
2986
|
+
if (!ok2) {
|
|
2987
|
+
return { client: "Codex", action: "declined", detail: "Skipped by user." };
|
|
2988
|
+
}
|
|
2989
|
+
try {
|
|
2990
|
+
const update = await writeCodexConfig(configPath, serve, now);
|
|
2991
|
+
return {
|
|
2992
|
+
client: "Codex",
|
|
2993
|
+
action: update.changed ? configDirExists ? "registered" : "prepared" : "skipped",
|
|
2994
|
+
detail: update.changed ? configDirExists ? `Wrote ${configPath}${update.backupPath ? ` (backup: ${update.backupPath})` : ""}.` : `Prepared ${configPath} for a future Codex or ChatGPT installation.` : `Already registered at ${configPath}.`
|
|
2995
|
+
};
|
|
2996
|
+
} catch (err) {
|
|
2997
|
+
return { client: "Codex", action: "error", detail: `Could not update ${configPath}: ${errorMessage(err)}` };
|
|
2998
|
+
}
|
|
2930
2999
|
}
|
|
2931
3000
|
let existing;
|
|
2932
3001
|
try {
|
|
@@ -2935,7 +3004,20 @@ ${codexBlock(serve)}Then fully quit and reopen Codex.`
|
|
|
2935
3004
|
return { client: "Codex", action: "error", detail: `Could not inspect Codex MCP settings: ${errorMessage(err)}` };
|
|
2936
3005
|
}
|
|
2937
3006
|
if (existing?.serve && sameServeCommand(existing.serve, serve)) {
|
|
2938
|
-
|
|
3007
|
+
try {
|
|
3008
|
+
const update = await writeCodexConfig(configPath, serve, now);
|
|
3009
|
+
return {
|
|
3010
|
+
client: "Codex",
|
|
3011
|
+
action: update.changed ? "registered" : "skipped",
|
|
3012
|
+
detail: update.changed ? `Already registered; updated tool approval policy in ${configPath}.` : "Already registered."
|
|
3013
|
+
};
|
|
3014
|
+
} catch (err) {
|
|
3015
|
+
return {
|
|
3016
|
+
client: "Codex",
|
|
3017
|
+
action: "error",
|
|
3018
|
+
detail: `Already registered, but could not set its tool approval policy: ${errorMessage(err)}`
|
|
3019
|
+
};
|
|
3020
|
+
}
|
|
2939
3021
|
}
|
|
2940
3022
|
if (existing && !existing.serve) {
|
|
2941
3023
|
return {
|
|
@@ -2975,20 +3057,37 @@ codex mcp add ${SERVER_NAME} -- ${serve.command} ${serve.args.join(" ")}`
|
|
|
2975
3057
|
}
|
|
2976
3058
|
return { client: "Codex", action: "error", detail: `codex mcp add failed: ${errorMessage(err)}` };
|
|
2977
3059
|
}
|
|
3060
|
+
try {
|
|
3061
|
+
await writeCodexConfig(configPath, serve, now);
|
|
3062
|
+
} catch (err) {
|
|
3063
|
+
return {
|
|
3064
|
+
client: "Codex",
|
|
3065
|
+
action: "error",
|
|
3066
|
+
detail: `Registered the server, but could not set its tool approval policy in ${configPath}: ${errorMessage(err)}`
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
2978
3069
|
return {
|
|
2979
3070
|
client: "Codex",
|
|
2980
3071
|
action: "registered",
|
|
2981
3072
|
detail: `Ran: codex mcp add ${SERVER_NAME} -- ${serve.command} ${serve.args.join(" ")}`
|
|
2982
3073
|
};
|
|
2983
3074
|
}
|
|
2984
|
-
async function registerJsonClient(clientLabel, configPath, serve, confirm, now) {
|
|
2985
|
-
|
|
3075
|
+
async function registerJsonClient(clientLabel, configPath, serve, confirm, now, prepareMissing = false) {
|
|
3076
|
+
const configDirExists = await clientDirExists(configPath);
|
|
3077
|
+
if (!configDirExists && !prepareMissing) {
|
|
2986
3078
|
return {
|
|
2987
3079
|
client: clientLabel,
|
|
2988
3080
|
action: "skipped",
|
|
2989
3081
|
detail: `Not installed (no ${dirname4(configPath)}). Re-run install after setting it up.`
|
|
2990
3082
|
};
|
|
2991
3083
|
}
|
|
3084
|
+
if (!configDirExists) {
|
|
3085
|
+
try {
|
|
3086
|
+
await mkdir2(dirname4(configPath), { recursive: true });
|
|
3087
|
+
} catch (err) {
|
|
3088
|
+
return { client: clientLabel, action: "error", detail: `Could not prepare ${configPath}: ${errorMessage(err)}` };
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
2992
3091
|
let data;
|
|
2993
3092
|
let existed;
|
|
2994
3093
|
let raw;
|
|
@@ -3025,7 +3124,11 @@ ${preview}`;
|
|
|
3025
3124
|
const nextData = { ...data, mcpServers: { ...servers, [SERVER_NAME]: nextEntry } };
|
|
3026
3125
|
await writeFile2(configPath, `${JSON.stringify(nextData, null, 2)}
|
|
3027
3126
|
`, "utf8");
|
|
3028
|
-
return {
|
|
3127
|
+
return {
|
|
3128
|
+
client: clientLabel,
|
|
3129
|
+
action: configDirExists ? "registered" : "prepared",
|
|
3130
|
+
detail: configDirExists ? `Wrote ${configPath}.` : `Prepared ${configPath} for a future ${clientLabel} installation.`
|
|
3131
|
+
};
|
|
3029
3132
|
} catch (err) {
|
|
3030
3133
|
return { client: clientLabel, action: "error", detail: `Could not write ${configPath}: ${errorMessage(err)}` };
|
|
3031
3134
|
}
|
|
@@ -3035,8 +3138,82 @@ function codexBlock(serve) {
|
|
|
3035
3138
|
return `[mcp_servers.${SERVER_NAME}]
|
|
3036
3139
|
command = ${JSON.stringify(serve.command)}
|
|
3037
3140
|
args = [${argsToml}]
|
|
3141
|
+
default_tools_approval_mode = ${JSON.stringify(CODEX_APPROVAL_MODE)}
|
|
3038
3142
|
`;
|
|
3039
3143
|
}
|
|
3144
|
+
function codexHeaders(raw) {
|
|
3145
|
+
const headers = [];
|
|
3146
|
+
const pattern = /^[ \t]*\[([^\]\r\n]+)\][ \t]*(?:#.*)?$/gm;
|
|
3147
|
+
for (let match = pattern.exec(raw); match; match = pattern.exec(raw)) {
|
|
3148
|
+
headers.push({ start: match.index, name: match[1].replace(/[ \t]/g, "") });
|
|
3149
|
+
}
|
|
3150
|
+
return headers;
|
|
3151
|
+
}
|
|
3152
|
+
function isCodexRootHeader(name) {
|
|
3153
|
+
return /^mcp_servers\.(?:llmnesia|"llmnesia"|'llmnesia')$/.test(name);
|
|
3154
|
+
}
|
|
3155
|
+
function isCodexChildHeader(name) {
|
|
3156
|
+
return /^mcp_servers\.(?:llmnesia|"llmnesia"|'llmnesia')\./.test(name);
|
|
3157
|
+
}
|
|
3158
|
+
function codexSectionRange(raw) {
|
|
3159
|
+
const headers = codexHeaders(raw);
|
|
3160
|
+
const roots = headers.filter((header) => isCodexRootHeader(header.name));
|
|
3161
|
+
if (roots.length > 1) {
|
|
3162
|
+
throw new Error(`found more than one [mcp_servers.${SERVER_NAME}] table; the file was left unchanged`);
|
|
3163
|
+
}
|
|
3164
|
+
const root = roots[0];
|
|
3165
|
+
if (!root) {
|
|
3166
|
+
if (headers.some((header) => isCodexChildHeader(header.name))) {
|
|
3167
|
+
throw new Error(
|
|
3168
|
+
`found LLMnesia MCP subtables without a [mcp_servers.${SERVER_NAME}] table; the file was left unchanged`
|
|
3169
|
+
);
|
|
3170
|
+
}
|
|
3171
|
+
return null;
|
|
3172
|
+
}
|
|
3173
|
+
const rootIndex = headers.indexOf(root);
|
|
3174
|
+
let end = raw.length;
|
|
3175
|
+
for (let i = rootIndex + 1; i < headers.length; i += 1) {
|
|
3176
|
+
if (!isCodexChildHeader(headers[i].name)) {
|
|
3177
|
+
end = headers[i].start;
|
|
3178
|
+
break;
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
return { start: root.start, end };
|
|
3182
|
+
}
|
|
3183
|
+
function renderCodexConfig(raw, serve) {
|
|
3184
|
+
const newline = raw.includes("\r\n") ? "\r\n" : "\n";
|
|
3185
|
+
const block = codexBlock(serve).replace(/\n/g, newline);
|
|
3186
|
+
const range = codexSectionRange(raw);
|
|
3187
|
+
if (range) {
|
|
3188
|
+
return `${raw.slice(0, range.start)}${block}${raw.slice(range.end)}`;
|
|
3189
|
+
}
|
|
3190
|
+
const base = raw.replace(/[ \t\r\n]+$/g, "");
|
|
3191
|
+
return `${base}${base ? `${newline}${newline}` : ""}${block}`;
|
|
3192
|
+
}
|
|
3193
|
+
async function writeCodexConfig(configPath, serve, now) {
|
|
3194
|
+
let raw = "";
|
|
3195
|
+
let existed = true;
|
|
3196
|
+
try {
|
|
3197
|
+
raw = await readFile2(configPath, "utf8");
|
|
3198
|
+
} catch (err) {
|
|
3199
|
+
if (isErrnoException(err) && err.code === "ENOENT") {
|
|
3200
|
+
existed = false;
|
|
3201
|
+
} else {
|
|
3202
|
+
throw err;
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
const next = renderCodexConfig(raw, serve);
|
|
3206
|
+
if (next === raw) {
|
|
3207
|
+
return { changed: false };
|
|
3208
|
+
}
|
|
3209
|
+
let backupPath;
|
|
3210
|
+
if (existed) {
|
|
3211
|
+
backupPath = `${configPath}.bak-${now()}`;
|
|
3212
|
+
await writeFile2(backupPath, raw, "utf8");
|
|
3213
|
+
}
|
|
3214
|
+
await writeFile2(configPath, next, { encoding: "utf8", mode: 384 });
|
|
3215
|
+
return { changed: true, ...backupPath ? { backupPath } : {} };
|
|
3216
|
+
}
|
|
3040
3217
|
function genericBlock(serve) {
|
|
3041
3218
|
return JSON.stringify({ mcpServers: { [SERVER_NAME]: { command: serve.command, args: serve.args } } }, null, 2);
|
|
3042
3219
|
}
|
|
@@ -3044,6 +3221,7 @@ async function registerClients(options) {
|
|
|
3044
3221
|
const home2 = options.home ?? homedir2();
|
|
3045
3222
|
const osPlatform2 = options.osPlatform ?? platform2();
|
|
3046
3223
|
const now = options.now ?? (() => Date.now());
|
|
3224
|
+
const prepareMissing = options.prepareMissingDesktopApps === true;
|
|
3047
3225
|
const { claudeDesktop, cursor, codex } = paths(home2, osPlatform2);
|
|
3048
3226
|
const results = [];
|
|
3049
3227
|
const safe = async (client, fn) => {
|
|
@@ -3063,13 +3241,18 @@ async function registerClients(options) {
|
|
|
3063
3241
|
results.push(
|
|
3064
3242
|
await safe(
|
|
3065
3243
|
"Claude Desktop",
|
|
3066
|
-
() => registerJsonClient("Claude Desktop", claudeDesktop, options.serve, options.confirm, now)
|
|
3244
|
+
() => registerJsonClient("Claude Desktop", claudeDesktop, options.serve, options.confirm, now, prepareMissing)
|
|
3067
3245
|
)
|
|
3068
3246
|
);
|
|
3069
3247
|
}
|
|
3070
|
-
results.push(await safe("Cursor", () => registerJsonClient("Cursor", cursor, options.serve, options.confirm, now)));
|
|
3071
3248
|
results.push(
|
|
3072
|
-
await safe("
|
|
3249
|
+
await safe("Cursor", () => registerJsonClient("Cursor", cursor, options.serve, options.confirm, now, prepareMissing))
|
|
3250
|
+
);
|
|
3251
|
+
results.push(
|
|
3252
|
+
await safe(
|
|
3253
|
+
"Codex",
|
|
3254
|
+
() => registerCodex(options.serve, options.codexCli ?? realCodexCli, codex, options.confirm, now, prepareMissing)
|
|
3255
|
+
)
|
|
3073
3256
|
);
|
|
3074
3257
|
results.push({
|
|
3075
3258
|
client: "Other MCP clients (Windsurf, Zed, LM Studio, ...)",
|
|
@@ -3111,20 +3294,27 @@ async function unregisterClaudeCode(cli) {
|
|
|
3111
3294
|
};
|
|
3112
3295
|
}
|
|
3113
3296
|
}
|
|
3114
|
-
async function unregisterCodex(cli, configPath) {
|
|
3297
|
+
async function unregisterCodex(cli, configPath, now) {
|
|
3115
3298
|
if (!await cli.detect()) {
|
|
3116
3299
|
try {
|
|
3117
|
-
const
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
action: "error",
|
|
3122
|
-
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.`
|
|
3123
|
-
};
|
|
3300
|
+
const raw = await readFile2(configPath, "utf8");
|
|
3301
|
+
const range = codexSectionRange(raw);
|
|
3302
|
+
if (!range) {
|
|
3303
|
+
return { client: "Codex", action: "skipped", detail: "`codex` not found and no registration was detected." };
|
|
3124
3304
|
}
|
|
3305
|
+
const backupPath = `${configPath}.bak-${now()}`;
|
|
3306
|
+
await writeFile2(backupPath, raw, "utf8");
|
|
3307
|
+
const next = `${raw.slice(0, range.start)}${raw.slice(range.end)}`.replace(/[ \t\r\n]+$/g, "");
|
|
3308
|
+
await writeFile2(configPath, next ? `${next}
|
|
3309
|
+
` : "", "utf8");
|
|
3310
|
+
return {
|
|
3311
|
+
client: "Codex",
|
|
3312
|
+
action: "registered",
|
|
3313
|
+
detail: `Removed from ${configPath} (backup: ${backupPath}).`
|
|
3314
|
+
};
|
|
3125
3315
|
} catch (err) {
|
|
3126
3316
|
if (!(isErrnoException(err) && err.code === "ENOENT")) {
|
|
3127
|
-
return { client: "Codex", action: "error", detail: `Could not
|
|
3317
|
+
return { client: "Codex", action: "error", detail: `Could not update ${configPath}: ${errorMessage(err)}` };
|
|
3128
3318
|
}
|
|
3129
3319
|
}
|
|
3130
3320
|
return { client: "Codex", action: "skipped", detail: "`codex` not found and no registration was detected." };
|
|
@@ -3192,7 +3382,7 @@ async function unregisterClients(options = {}) {
|
|
|
3192
3382
|
results.push(await safe("Claude Desktop", () => unregisterJsonClient("Claude Desktop", claudeDesktop, now)));
|
|
3193
3383
|
}
|
|
3194
3384
|
results.push(await safe("Cursor", () => unregisterJsonClient("Cursor", cursor, now)));
|
|
3195
|
-
results.push(await safe("Codex", () => unregisterCodex(options.codexCli ?? realCodexCli, codex)));
|
|
3385
|
+
results.push(await safe("Codex", () => unregisterCodex(options.codexCli ?? realCodexCli, codex, now)));
|
|
3196
3386
|
results.push({
|
|
3197
3387
|
client: "Other MCP clients",
|
|
3198
3388
|
action: "skipped",
|
|
@@ -3207,7 +3397,7 @@ import { delimiter, dirname as dirname6, isAbsolute as isAbsolute2, join as join
|
|
|
3207
3397
|
import { homedir as homedir4, platform as platform3 } from "os";
|
|
3208
3398
|
|
|
3209
3399
|
// src/runtimeInstall.ts
|
|
3210
|
-
import { cp, mkdir as
|
|
3400
|
+
import { cp, mkdir as mkdir3, readdir as readdir3, rename, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
3211
3401
|
import { existsSync } from "fs";
|
|
3212
3402
|
import { homedir as homedir3 } from "os";
|
|
3213
3403
|
import { dirname as dirname5, join as join5 } from "path";
|
|
@@ -3233,10 +3423,10 @@ async function installRuntime(options = {}) {
|
|
|
3233
3423
|
const nonce = `${process.pid}-${Date.now()}`;
|
|
3234
3424
|
const stagingDir = join5(parentDir, `.bin-install-${nonce}`);
|
|
3235
3425
|
const backupDir = join5(parentDir, `.bin-backup-${nonce}`);
|
|
3236
|
-
await
|
|
3426
|
+
await mkdir3(parentDir, { recursive: true });
|
|
3237
3427
|
await rm2(stagingDir, { recursive: true, force: true });
|
|
3238
3428
|
await rm2(backupDir, { recursive: true, force: true });
|
|
3239
|
-
await
|
|
3429
|
+
await mkdir3(stagingDir, { recursive: true });
|
|
3240
3430
|
try {
|
|
3241
3431
|
for (const name of await readdir3(sourceDir)) {
|
|
3242
3432
|
await cp(join5(sourceDir, name), join5(stagingDir, name), { recursive: true });
|
|
@@ -3622,64 +3812,90 @@ ${message}
|
|
|
3622
3812
|
rl.close();
|
|
3623
3813
|
}
|
|
3624
3814
|
}
|
|
3625
|
-
function
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
if (
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
if (
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3815
|
+
function summarizeClientRegistrations(results) {
|
|
3816
|
+
const connected = [];
|
|
3817
|
+
const prepared = [];
|
|
3818
|
+
const errors = [];
|
|
3819
|
+
for (const result of results) {
|
|
3820
|
+
if (result.client.startsWith("Other MCP clients")) continue;
|
|
3821
|
+
if (result.action === "registered" || result.action === "skipped" && result.detail.startsWith("Already registered")) {
|
|
3822
|
+
connected.push(result.client);
|
|
3823
|
+
} else if (result.action === "prepared") {
|
|
3824
|
+
prepared.push(result.client);
|
|
3825
|
+
} else if (result.action === "error") {
|
|
3826
|
+
errors.push({ client: result.client, detail: result.detail });
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
return { connected, prepared, errors };
|
|
3830
|
+
}
|
|
3831
|
+
function naturalList(items) {
|
|
3832
|
+
if (items.length <= 1) return items[0] ?? "";
|
|
3833
|
+
if (items.length === 2) return `${items[0]} and ${items[1]}`;
|
|
3834
|
+
return `${items.slice(0, -1).join(", ")}, and ${items.at(-1)}`;
|
|
3835
|
+
}
|
|
3836
|
+
function formatInstallSuccess(connectedClients, preparedClients = []) {
|
|
3837
|
+
const apps = naturalList(connectedClients);
|
|
3838
|
+
const restart = apps ? `Fully close and reopen your browser and ${apps}.
|
|
3839
|
+
` : "Fully close and reopen your browser.\n";
|
|
3840
|
+
const future = preparedClients.length > 0 ? `${naturalList(preparedClients)} will also be ready if you install ${preparedClients.length === 1 ? "it" : "them"} later.
|
|
3841
|
+
` : "";
|
|
3842
|
+
return `
|
|
3843
|
+
Setup complete.
|
|
3844
|
+
${restart}${future}LLMnesia Settings will confirm when your history is ready.
|
|
3845
|
+
`;
|
|
3650
3846
|
}
|
|
3651
3847
|
async function runInstall(positionals, options, flags) {
|
|
3652
3848
|
let hadProblems = false;
|
|
3653
|
-
process.stdout.write("
|
|
3849
|
+
process.stdout.write("Connecting your browser...\n");
|
|
3654
3850
|
try {
|
|
3655
|
-
|
|
3851
|
+
await installNativeHost({
|
|
3656
3852
|
ids: positionals,
|
|
3657
3853
|
corpus: options.corpus,
|
|
3658
3854
|
force: flags.has("force"),
|
|
3659
3855
|
quiet: true
|
|
3660
3856
|
});
|
|
3661
|
-
process.stdout.write(
|
|
3662
|
-
`);
|
|
3857
|
+
process.stdout.write("Browser connected.\n");
|
|
3663
3858
|
} catch (err) {
|
|
3664
3859
|
hadProblems = true;
|
|
3665
|
-
process.stderr.write(
|
|
3860
|
+
process.stderr.write(`Could not connect your browser: ${err instanceof Error ? err.message : String(err)}
|
|
3666
3861
|
`);
|
|
3667
3862
|
}
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3863
|
+
process.stdout.write("Connecting your desktop AI apps...\n");
|
|
3864
|
+
let runtime;
|
|
3865
|
+
try {
|
|
3866
|
+
runtime = await installRuntime();
|
|
3867
|
+
} catch (err) {
|
|
3868
|
+
process.stderr.write(`Could not install LLMnesia: ${err instanceof Error ? err.message : String(err)}
|
|
3869
|
+
`);
|
|
3870
|
+
process.stderr.write("Setup needs attention. Fix the message above, then run the same command again.\n");
|
|
3871
|
+
return 1;
|
|
3872
|
+
}
|
|
3873
|
+
const results = await registerClients({
|
|
3874
|
+
serve: runtime.serve,
|
|
3875
|
+
confirm: async () => true,
|
|
3876
|
+
prepareMissingDesktopApps: true
|
|
3877
|
+
});
|
|
3878
|
+
const registrations = summarizeClientRegistrations(results);
|
|
3879
|
+
for (const client of registrations.connected) {
|
|
3880
|
+
process.stdout.write(`${client} connected.
|
|
3881
|
+
`);
|
|
3882
|
+
}
|
|
3883
|
+
for (const error of registrations.errors) {
|
|
3884
|
+
process.stderr.write(`Could not connect ${error.client}: ${error.detail}
|
|
3885
|
+
`);
|
|
3886
|
+
}
|
|
3887
|
+
hadProblems ||= registrations.errors.length > 0;
|
|
3888
|
+
if (registrations.connected.length === 0 && registrations.prepared.length === 0) {
|
|
3889
|
+
hadProblems = true;
|
|
3678
3890
|
process.stderr.write(
|
|
3679
|
-
"
|
|
3891
|
+
"No supported desktop AI app was found. Install or open Codex, Claude Desktop, or Cursor, then run the same command again.\n"
|
|
3680
3892
|
);
|
|
3893
|
+
}
|
|
3894
|
+
if (hadProblems) {
|
|
3895
|
+
process.stderr.write("\nSetup needs attention. Fix the message above, then run the same command again.\n");
|
|
3681
3896
|
return 1;
|
|
3682
3897
|
}
|
|
3898
|
+
process.stdout.write(formatInstallSuccess(registrations.connected, registrations.prepared));
|
|
3683
3899
|
return 0;
|
|
3684
3900
|
}
|
|
3685
3901
|
async function runUninstall(flags) {
|
|
@@ -3694,10 +3910,19 @@ async function runUninstall(flags) {
|
|
|
3694
3910
|
}
|
|
3695
3911
|
}
|
|
3696
3912
|
const results = await unregisterClients();
|
|
3697
|
-
|
|
3913
|
+
for (const result of results) {
|
|
3914
|
+
if (result.client.startsWith("Other MCP clients")) continue;
|
|
3915
|
+
if (result.action === "registered") {
|
|
3916
|
+
process.stdout.write(`${result.client} disconnected.
|
|
3917
|
+
`);
|
|
3918
|
+
} else if (result.action === "error") {
|
|
3919
|
+
process.stderr.write(`Could not disconnect ${result.client}: ${result.detail}
|
|
3920
|
+
`);
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3698
3923
|
if (results.some((result) => result.action === "error")) {
|
|
3699
3924
|
process.stderr.write(
|
|
3700
|
-
"Uninstall stopped before removing the
|
|
3925
|
+
"Uninstall stopped before removing the remaining files. Fix the message above and try again.\n"
|
|
3701
3926
|
);
|
|
3702
3927
|
return 1;
|
|
3703
3928
|
}
|
|
@@ -3889,7 +4114,9 @@ function collapse(text) {
|
|
|
3889
4114
|
return text.replace(/\s+/g, " ").trim();
|
|
3890
4115
|
}
|
|
3891
4116
|
export {
|
|
3892
|
-
|
|
4117
|
+
formatInstallSuccess,
|
|
4118
|
+
main,
|
|
4119
|
+
summarizeClientRegistrations
|
|
3893
4120
|
};
|
|
3894
4121
|
/*! Bundled license information:
|
|
3895
4122
|
|