@elisym/mcp 0.15.1 → 0.15.2

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/dist/index.js CHANGED
@@ -5,8 +5,8 @@ import { createIrohTransport, loadGlobalConfig, writeGlobalConfig } from '@elisy
5
5
  import { getBase58Encoder, getBase58Decoder, generateKeyPairSigner, createSolanaRpc, address, createSolanaRpcSubscriptions, sendAndConfirmTransactionFactory, getSignatureFromTransaction, pipe, createTransactionMessage, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, signTransactionMessageWithSigners, createKeyPairSignerFromBytes, isAddress } from '@solana/kit';
6
6
  import { Command } from 'commander';
7
7
  import { generateSecretKey, nip19, getPublicKey } from 'nostr-tools';
8
- import { realpath, readFile, stat, rm, writeFile, rename, unlink } from 'node:fs/promises';
9
- import { tmpdir, homedir, platform } from 'node:os';
8
+ import { realpath, readFile, stat, mkdir, rm, writeFile, rename, unlink } from 'node:fs/promises';
9
+ import { tmpdir, platform, homedir } from 'node:os';
10
10
  import { dirname, join, resolve, isAbsolute, basename, relative } from 'node:path';
11
11
  import { readFileSync, mkdtempSync } from 'node:fs';
12
12
  import { fileURLToPath } from 'node:url';
@@ -370,6 +370,9 @@ function decodeNpub(npub) {
370
370
  function elisymPackageArgs() {
371
371
  return ["-y", `@elisym/mcp@~${PACKAGE_VERSION}`];
372
372
  }
373
+ function userHome() {
374
+ return process.env.HOME ?? homedir();
375
+ }
373
376
  function validateClientName(name) {
374
377
  if (name === void 0) {
375
378
  return;
@@ -399,8 +402,9 @@ async function safeRewriteJson(path, expectedRaw, newConfig) {
399
402
  var CLIENTS = [
400
403
  {
401
404
  name: "claude-desktop",
405
+ format: "json",
402
406
  configPath() {
403
- const home = homedir();
407
+ const home = userHome();
404
408
  switch (platform()) {
405
409
  case "darwin":
406
410
  return join(home, "Library/Application Support/Claude/claude_desktop_config.json");
@@ -413,21 +417,29 @@ var CLIENTS = [
413
417
  },
414
418
  {
415
419
  name: "claude-code",
420
+ format: "json",
416
421
  // Claude Code CLI keeps user-scope MCP servers under `mcpServers` at the top
417
422
  // level of `~/.claude.json`. Project-scope (`.mcp.json` in cwd) and local-scope
418
423
  // (`projects.<path>.mcpServers` inside the same file) are deliberately not
419
424
  // touched here - this installer only writes user scope so the server is
420
425
  // available across all projects.
421
- configPath: () => join(homedir(), ".claude.json")
426
+ configPath: () => join(userHome(), ".claude.json")
422
427
  },
423
428
  {
424
429
  name: "cursor",
425
- configPath: () => join(homedir(), ".cursor/mcp.json")
430
+ format: "json",
431
+ configPath: () => join(userHome(), ".cursor/mcp.json")
432
+ },
433
+ {
434
+ name: "codex",
435
+ format: "codex-toml",
436
+ configPath: () => join(userHome(), ".codex/config.toml")
426
437
  },
427
438
  {
428
439
  name: "windsurf",
440
+ format: "json",
429
441
  configPath() {
430
- const home = homedir();
442
+ const home = userHome();
431
443
  if (platform() === "darwin") {
432
444
  return join(home, "Library/Application Support/Windsurf/mcp.json");
433
445
  }
@@ -480,7 +492,7 @@ async function runInstall(options) {
480
492
  continue;
481
493
  }
482
494
  try {
483
- const result = await installToConfig(path, entry, effectiveAgent);
495
+ const result = client.format === "codex-toml" ? await installToCodexConfig(path, entry, options.agent) : await installToConfig(path, entry, options.agent);
484
496
  if (result === "installed") {
485
497
  console.log(`Installed to ${client.name}: ${path}`);
486
498
  installed++;
@@ -499,7 +511,7 @@ async function runInstall(options) {
499
511
  }
500
512
  }
501
513
  async function resolveDefaultAgent() {
502
- const agents = await listAgents(process.cwd());
514
+ const agents = await listAgents(userHome());
503
515
  const [first, second] = agents;
504
516
  if (!first) {
505
517
  return { kind: "none" };
@@ -523,6 +535,20 @@ async function runUpdate(options) {
523
535
  if (!path) {
524
536
  continue;
525
537
  }
538
+ if (client.format === "codex-toml") {
539
+ try {
540
+ const result = await updateCodexConfig(path, options.agent);
541
+ if (result === "updated") {
542
+ console.log(`Updated ${client.name}: ${path} -> @elisym/mcp@~${PACKAGE_VERSION}`);
543
+ updated++;
544
+ }
545
+ } catch (err) {
546
+ if (err.code !== "ENOENT") {
547
+ console.log(`Skipped ${client.name}: ${err.message}`);
548
+ }
549
+ }
550
+ continue;
551
+ }
526
552
  let raw;
527
553
  try {
528
554
  raw = await readFile(path, "utf-8");
@@ -561,12 +587,16 @@ async function runUpdate(options) {
561
587
  }
562
588
  const entry = existing;
563
589
  const newArgs = elisymPackageArgs();
590
+ const packageArg = newArgs[1];
591
+ if (packageArg === void 0) {
592
+ throw new Error("Internal error: missing package argument for elisym MCP install.");
593
+ }
564
594
  if (Array.isArray(entry.args)) {
565
595
  const idx = entry.args.findIndex(
566
596
  (a) => typeof a === "string" && a.startsWith("@elisym/mcp@")
567
597
  );
568
598
  if (idx >= 0) {
569
- entry.args[idx] = newArgs[1];
599
+ entry.args[idx] = packageArg;
570
600
  } else {
571
601
  entry.args = newArgs;
572
602
  }
@@ -601,6 +631,16 @@ async function runUninstall(options) {
601
631
  if (!path) {
602
632
  continue;
603
633
  }
634
+ if (client.format === "codex-toml") {
635
+ try {
636
+ const result = await uninstallFromCodexConfig(path);
637
+ if (result === "removed") {
638
+ console.log(`Removed from ${client.name}: ${path}`);
639
+ }
640
+ } catch {
641
+ }
642
+ continue;
643
+ }
604
644
  let raw;
605
645
  try {
606
646
  raw = await readFile(path, "utf-8");
@@ -634,8 +674,7 @@ async function runList() {
634
674
  }
635
675
  try {
636
676
  const raw = await readFile(path, "utf-8");
637
- const config = JSON.parse(raw);
638
- const installed = !!config.mcpServers?.elisym;
677
+ const installed = client.format === "codex-toml" ? findCodexElisymBlock(raw) !== null : !!JSON.parse(raw).mcpServers?.elisym;
639
678
  console.log(`${client.name}: ${installed ? "installed" : "available"} (${path})`);
640
679
  } catch {
641
680
  console.log(`${client.name}: not found`);
@@ -687,6 +726,457 @@ async function installToConfig(path, entry, agentRebind) {
687
726
  await safeRewriteJson(path, raw, config);
688
727
  return "installed";
689
728
  }
729
+ function findCodexElisymBlock(raw) {
730
+ const table = findTomlTableRange(raw, "mcp_servers.elisym");
731
+ if (!table) {
732
+ return null;
733
+ }
734
+ return { start: table.start, end: table.end, body: raw.slice(table.start, table.end) };
735
+ }
736
+ function findTomlTableRange(raw, path) {
737
+ return findTomlTableRanges(raw).find((table) => table.path === path) ?? null;
738
+ }
739
+ function findTomlTableRanges(raw) {
740
+ const lines = raw.match(/^.*(?:\n|$)/gm) ?? [];
741
+ const offsets = [];
742
+ let offset = 0;
743
+ for (const line of lines) {
744
+ offsets.push(offset);
745
+ offset += line.length;
746
+ }
747
+ const ranges = [];
748
+ for (const [lineIndex, line] of lines.entries()) {
749
+ const path = parseTomlTableHeader(line);
750
+ if (!path) {
751
+ continue;
752
+ }
753
+ let end = raw.length;
754
+ for (let nextLineIndex = lineIndex + 1; nextLineIndex < lines.length; nextLineIndex++) {
755
+ if (parseTomlTableHeader(lines[nextLineIndex] ?? "")) {
756
+ end = offsets[nextLineIndex] ?? raw.length;
757
+ break;
758
+ }
759
+ }
760
+ ranges.push({ start: offsets[lineIndex] ?? 0, end, path });
761
+ }
762
+ return ranges;
763
+ }
764
+ function parseTomlTableHeader(line) {
765
+ const match = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/.exec(line.trimEnd());
766
+ return match ? match[1] : null;
767
+ }
768
+ function parseCodexEnv(block) {
769
+ const env = {};
770
+ let section = "other";
771
+ for (const line of block.split(/\r?\n/)) {
772
+ const trimmed = line.trim();
773
+ if (trimmed.startsWith("[")) {
774
+ if (/^\[mcp_servers\.elisym\]\s*(?:#.*)?$/.test(trimmed)) {
775
+ section = "elisym";
776
+ } else if (/^\[mcp_servers\.elisym\.env\]\s*(?:#.*)?$/.test(trimmed)) {
777
+ section = "env";
778
+ } else {
779
+ section = "other";
780
+ }
781
+ continue;
782
+ }
783
+ if (trimmed === "" || trimmed.startsWith("#")) {
784
+ continue;
785
+ }
786
+ if (section === "elisym") {
787
+ Object.assign(env, parseCodexInlineEnv(trimmed));
788
+ } else if (section === "env") {
789
+ const match = /^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+?)\s*(?:#.*)?$/.exec(trimmed);
790
+ if (!match) {
791
+ continue;
792
+ }
793
+ const [, key, rawValue] = match;
794
+ const value = parseTomlString(rawValue);
795
+ if (value !== void 0) {
796
+ env[key] = value;
797
+ }
798
+ }
799
+ }
800
+ return env;
801
+ }
802
+ function parseCodexInlineEnv(line) {
803
+ const env = {};
804
+ const match = /^env\s*=\s*\{(.*)\}\s*(?:#.*)?$/.exec(line);
805
+ if (!match) {
806
+ return env;
807
+ }
808
+ const [, body] = match;
809
+ for (const entry of splitInlineTableEntries(body)) {
810
+ const entryMatch = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+?)\s*$/.exec(entry);
811
+ if (!entryMatch) {
812
+ continue;
813
+ }
814
+ const [, key, rawValue] = entryMatch;
815
+ const value = parseTomlString(rawValue);
816
+ if (value !== void 0) {
817
+ env[key] = value;
818
+ }
819
+ }
820
+ return env;
821
+ }
822
+ function splitInlineTableEntries(body) {
823
+ const entries = [];
824
+ let current = "";
825
+ let inString = false;
826
+ let escaped = false;
827
+ for (const char of body) {
828
+ if (escaped) {
829
+ current += char;
830
+ escaped = false;
831
+ continue;
832
+ }
833
+ if (char === "\\" && inString) {
834
+ current += char;
835
+ escaped = true;
836
+ continue;
837
+ }
838
+ if (char === '"') {
839
+ current += char;
840
+ inString = !inString;
841
+ continue;
842
+ }
843
+ if (char === "," && !inString) {
844
+ entries.push(current);
845
+ current = "";
846
+ continue;
847
+ }
848
+ current += char;
849
+ }
850
+ if (current.trim() !== "") {
851
+ entries.push(current);
852
+ }
853
+ return entries;
854
+ }
855
+ function parseTomlString(rawValue) {
856
+ if (rawValue.startsWith("'") && rawValue.endsWith("'")) {
857
+ return rawValue.slice(1, -1);
858
+ }
859
+ try {
860
+ return JSON.parse(rawValue);
861
+ } catch {
862
+ return void 0;
863
+ }
864
+ }
865
+ function quoteTomlString(value) {
866
+ return JSON.stringify(value);
867
+ }
868
+ function renderTomlStringArray(values) {
869
+ return `[${values.map((value) => quoteTomlString(value)).join(", ")}]`;
870
+ }
871
+ function renderCodexTomlBlock(entry) {
872
+ const lines = [
873
+ "[mcp_servers.elisym]",
874
+ `command = ${quoteTomlString(String(entry.command ?? "npx"))}`,
875
+ `args = ${renderTomlStringArray(Array.isArray(entry.args) ? entry.args.map(String) : [])}`
876
+ ];
877
+ const env = entry.env && typeof entry.env === "object" && !Array.isArray(entry.env) ? entry.env : {};
878
+ const envEntries = Object.entries(env);
879
+ if (envEntries.length > 0) {
880
+ lines.push("", "[mcp_servers.elisym.env]");
881
+ for (const [key, value] of envEntries) {
882
+ lines.push(`${key} = ${quoteTomlString(String(value))}`);
883
+ }
884
+ }
885
+ return `${lines.join("\n")}
886
+ `;
887
+ }
888
+ function updateCodexTomlBlock(body, env, rewriteEnv) {
889
+ const withFreshPackagePin = updateCodexPackagePin(body);
890
+ if (!rewriteEnv) {
891
+ return withFreshPackagePin;
892
+ }
893
+ const agent = env.ELISYM_AGENT;
894
+ if (agent === void 0) {
895
+ return withFreshPackagePin;
896
+ }
897
+ return replaceCodexAgentEnv(withFreshPackagePin, agent);
898
+ }
899
+ function updateCodexPackagePin(body) {
900
+ const lines = body.match(/^.*(?:\n|$)/gm) ?? [];
901
+ let section = "other";
902
+ for (const [lineIndex, line] of lines.entries()) {
903
+ const trimmed = line.trim();
904
+ if (trimmed.startsWith("[")) {
905
+ if (/^\[mcp_servers\.elisym\]\s*(?:#.*)?$/.test(trimmed)) {
906
+ section = "elisym";
907
+ } else if (/^\[mcp_servers\.elisym\.env\]\s*(?:#.*)?$/.test(trimmed)) {
908
+ section = "env";
909
+ } else {
910
+ section = "other";
911
+ }
912
+ continue;
913
+ }
914
+ if (section !== "elisym" || !/^\s*args\s*=/.test(line)) {
915
+ continue;
916
+ }
917
+ const packageSpec = `@elisym/mcp@~${PACKAGE_VERSION}`;
918
+ const assignmentEndIndex = findTomlAssignmentEnd(lines, lineIndex);
919
+ const assignmentLines = lines.slice(lineIndex, assignmentEndIndex + 1);
920
+ const packageLineOffset = assignmentLines.findIndex(
921
+ (assignmentLine) => assignmentLine.includes("@elisym/mcp@")
922
+ );
923
+ if (packageLineOffset >= 0) {
924
+ const packageLineIndex = lineIndex + packageLineOffset;
925
+ const packageLine = lines[packageLineIndex] ?? "";
926
+ lines[packageLineIndex] = packageLine.replace(/@elisym\/mcp@[^"\],\s]+/, packageSpec);
927
+ } else {
928
+ lines.splice(
929
+ lineIndex,
930
+ assignmentEndIndex - lineIndex + 1,
931
+ replaceTomlAssignmentValue(line, renderTomlStringArray(elisymPackageArgs()))
932
+ );
933
+ }
934
+ break;
935
+ }
936
+ return lines.join("");
937
+ }
938
+ function findTomlAssignmentEnd(lines, startIndex) {
939
+ const firstLine = lines[startIndex] ?? "";
940
+ const assignmentStart = firstLine.indexOf("=");
941
+ if (assignmentStart < 0) {
942
+ return startIndex;
943
+ }
944
+ let depth = 0;
945
+ let sawArray = false;
946
+ for (let lineIndex = startIndex; lineIndex < lines.length; lineIndex++) {
947
+ const line = lines[lineIndex] ?? "";
948
+ if (lineIndex > startIndex && /^\s*\[/.test(line) && depth > 0) {
949
+ return lineIndex - 1;
950
+ }
951
+ const scanFrom = lineIndex === startIndex ? assignmentStart + 1 : 0;
952
+ const scan = scanTomlArrayLine(line, scanFrom, depth);
953
+ depth = scan.depth;
954
+ sawArray = sawArray || scan.sawArray;
955
+ if (!sawArray) {
956
+ return startIndex;
957
+ }
958
+ if (depth <= 0) {
959
+ return lineIndex;
960
+ }
961
+ }
962
+ return lines.length - 1;
963
+ }
964
+ function scanTomlArrayLine(line, startIndex, initialDepth) {
965
+ let depth = initialDepth;
966
+ let sawArray = false;
967
+ let inString = false;
968
+ let escaped = false;
969
+ for (let charIndex = startIndex; charIndex < line.length; charIndex++) {
970
+ const char = line[charIndex];
971
+ if (escaped) {
972
+ escaped = false;
973
+ continue;
974
+ }
975
+ if (char === "\\" && inString) {
976
+ escaped = true;
977
+ continue;
978
+ }
979
+ if (char === '"') {
980
+ inString = !inString;
981
+ continue;
982
+ }
983
+ if (char === "#" && !inString) {
984
+ break;
985
+ }
986
+ if (char === "[" && !inString) {
987
+ depth++;
988
+ sawArray = true;
989
+ } else if (char === "]" && !inString) {
990
+ depth--;
991
+ }
992
+ }
993
+ return { depth, sawArray };
994
+ }
995
+ function replaceTomlAssignmentValue(line, value) {
996
+ const match = /^(\s*[A-Za-z_][A-Za-z0-9_]*\s*=\s*).*(\r?\n)?$/.exec(line);
997
+ if (!match) {
998
+ return line;
999
+ }
1000
+ const [, prefix, newline = ""] = match;
1001
+ return `${prefix}${value}${newline}`;
1002
+ }
1003
+ function replaceCodexAgentEnv(body, agent) {
1004
+ const lines = body.match(/^.*(?:\n|$)/gm) ?? [];
1005
+ let section = "other";
1006
+ let envInsertionIndex = -1;
1007
+ let elisymInsertionIndex = -1;
1008
+ for (const [lineIndex, line] of lines.entries()) {
1009
+ const trimmed = line.trim();
1010
+ if (trimmed.startsWith("[")) {
1011
+ if (section === "env" && envInsertionIndex < 0) {
1012
+ envInsertionIndex = lineIndex;
1013
+ }
1014
+ if (section === "elisym") {
1015
+ elisymInsertionIndex = lineIndex;
1016
+ }
1017
+ const header = parseTomlTableHeader(trimmed);
1018
+ if (header === "mcp_servers.elisym") {
1019
+ section = "elisym";
1020
+ } else if (header === "mcp_servers.elisym.env") {
1021
+ section = "env";
1022
+ continue;
1023
+ } else {
1024
+ section = "other";
1025
+ }
1026
+ }
1027
+ if (section === "env") {
1028
+ if (/^\s*ELISYM_AGENT\s*=/.test(line)) {
1029
+ lines[lineIndex] = replaceTomlAssignmentValue(line, quoteTomlString(agent));
1030
+ return lines.join("");
1031
+ }
1032
+ continue;
1033
+ }
1034
+ if (section === "elisym") {
1035
+ elisymInsertionIndex = lineIndex + 1;
1036
+ if (/^\s*env\s*=/.test(line)) {
1037
+ const nextLine = replaceCodexInlineEnvAgent(line, agent);
1038
+ if (nextLine !== null) {
1039
+ lines[lineIndex] = nextLine;
1040
+ return lines.join("");
1041
+ }
1042
+ }
1043
+ }
1044
+ }
1045
+ if (section === "env") {
1046
+ envInsertionIndex = lines.length;
1047
+ } else if (section === "elisym") {
1048
+ elisymInsertionIndex = lines.length;
1049
+ }
1050
+ const agentLine = `ELISYM_AGENT = ${quoteTomlString(agent)}
1051
+ `;
1052
+ if (envInsertionIndex >= 0) {
1053
+ lines.splice(envInsertionIndex, 0, agentLine);
1054
+ return lines.join("");
1055
+ }
1056
+ const insertionIndex = elisymInsertionIndex >= 0 ? elisymInsertionIndex : lines.length;
1057
+ lines.splice(insertionIndex, 0, "\n", "[mcp_servers.elisym.env]\n", agentLine);
1058
+ return lines.join("");
1059
+ }
1060
+ function replaceCodexInlineEnvAgent(line, agent) {
1061
+ const match = /^(\s*env\s*=\s*\{)(.*)(\}\s*(?:#.*)?(?:\r?\n)?)$/.exec(line);
1062
+ if (!match) {
1063
+ return null;
1064
+ }
1065
+ const [, prefix, body, suffix] = match;
1066
+ const entries = splitInlineTableEntries(body);
1067
+ const nextEntries = [];
1068
+ let replaced = false;
1069
+ for (const entry of entries) {
1070
+ if (/^\s*ELISYM_AGENT\s*=/.test(entry)) {
1071
+ const entryMatch = /^(\s*ELISYM_AGENT\s*=\s*).*$/.exec(entry);
1072
+ if (!entryMatch) {
1073
+ return null;
1074
+ }
1075
+ nextEntries.push(`${entryMatch[1]}${quoteTomlString(agent)}`);
1076
+ replaced = true;
1077
+ } else {
1078
+ nextEntries.push(entry);
1079
+ }
1080
+ }
1081
+ if (!replaced) {
1082
+ nextEntries.push(` ELISYM_AGENT = ${quoteTomlString(agent)} `);
1083
+ }
1084
+ return `${prefix}${nextEntries.join(",")}${suffix}`;
1085
+ }
1086
+ function removeCodexElisymTables(raw) {
1087
+ const ranges = findTomlTableRanges(raw).filter((table) => isCodexElisymPath(table.path)).sort((left, right) => right.start - left.start);
1088
+ let nextRaw = raw;
1089
+ for (const range of ranges) {
1090
+ nextRaw = `${nextRaw.slice(0, range.start)}${nextRaw.slice(range.end)}`;
1091
+ }
1092
+ return nextRaw;
1093
+ }
1094
+ function isCodexElisymPath(path) {
1095
+ return path === "mcp_servers.elisym" || path.startsWith("mcp_servers.elisym.");
1096
+ }
1097
+ function replaceCodexBlock(raw, block, replacement) {
1098
+ {
1099
+ let separator = "\n\n";
1100
+ if (raw.length === 0 || raw.endsWith("\n\n")) {
1101
+ separator = "";
1102
+ } else if (raw.endsWith("\n")) {
1103
+ separator = "\n";
1104
+ }
1105
+ return `${raw}${separator}${replacement}`;
1106
+ }
1107
+ }
1108
+ async function installToCodexConfig(path, entry, agentRebind) {
1109
+ let raw;
1110
+ try {
1111
+ raw = await readFile(path, "utf-8");
1112
+ } catch (err) {
1113
+ if (err.code !== "ENOENT") {
1114
+ throw err;
1115
+ }
1116
+ await mkdir(dirname(path), { recursive: true });
1117
+ await writeFileAtomic(path, renderCodexTomlBlock(entry), 384);
1118
+ return "installed";
1119
+ }
1120
+ const block = findCodexElisymBlock(raw);
1121
+ if (block) {
1122
+ if (agentRebind === void 0) {
1123
+ return "unchanged";
1124
+ }
1125
+ const env = parseCodexEnv(raw);
1126
+ if (env.ELISYM_AGENT === agentRebind) {
1127
+ return "unchanged";
1128
+ }
1129
+ env.ELISYM_AGENT = agentRebind;
1130
+ const replacement = updateCodexTomlBlock(raw, env, true);
1131
+ await safeRewriteRaw(path, raw, replacement);
1132
+ return "rebound";
1133
+ }
1134
+ await safeRewriteRaw(path, raw, replaceCodexBlock(raw, null, renderCodexTomlBlock(entry)));
1135
+ return "installed";
1136
+ }
1137
+ async function updateCodexConfig(path, agentOverride) {
1138
+ const raw = await readFile(path, "utf-8");
1139
+ const block = findCodexElisymBlock(raw);
1140
+ if (!block) {
1141
+ return "unchanged";
1142
+ }
1143
+ const env = parseCodexEnv(raw);
1144
+ const existingAgentRaw = typeof env.ELISYM_AGENT === "string" ? env.ELISYM_AGENT : void 0;
1145
+ if (existingAgentRaw !== void 0 && agentOverride === void 0) {
1146
+ validateAgentName(existingAgentRaw);
1147
+ }
1148
+ if (agentOverride !== void 0) {
1149
+ env.ELISYM_AGENT = agentOverride;
1150
+ }
1151
+ const replacement = updateCodexTomlBlock(raw, env, agentOverride !== void 0);
1152
+ await safeRewriteRaw(path, raw, replacement);
1153
+ return "updated";
1154
+ }
1155
+ async function uninstallFromCodexConfig(path) {
1156
+ const raw = await readFile(path, "utf-8");
1157
+ const replacement = removeCodexElisymTables(raw);
1158
+ if (replacement === raw) {
1159
+ return "unchanged";
1160
+ }
1161
+ await safeRewriteRaw(path, raw, replacement);
1162
+ return "removed";
1163
+ }
1164
+ async function safeRewriteRaw(path, expectedRaw, nextRaw) {
1165
+ let recheck;
1166
+ try {
1167
+ recheck = await readFile(path, "utf-8");
1168
+ } catch (err) {
1169
+ throw new Error(
1170
+ `${path} disappeared between read and write: ${err.message}. Re-run after the file is restored.`
1171
+ );
1172
+ }
1173
+ if (recheck !== expectedRaw) {
1174
+ throw new Error(
1175
+ `${path} was modified by another process during update. Close the MCP client and re-run.`
1176
+ );
1177
+ }
1178
+ await writeFileAtomic(path, nextRaw, 384);
1179
+ }
690
1180
  function ensureIrohTransport(agent) {
691
1181
  if (agent.irohTransport) {
692
1182
  return agent.irohTransport;
@@ -4446,7 +4936,10 @@ program.command("init [name]").description("Create a new agent identity").option
4446
4936
  }
4447
4937
  })
4448
4938
  );
4449
- program.command("install").description("Install elisym MCP server into client configs").option("--client <name>", "Specific client (claude-desktop, claude-code, cursor, windsurf)").option("--agent <name>", "Bind to specific agent").option("--list", "List detected clients").action(
4939
+ program.command("install").description("Install elisym MCP server into client configs").option(
4940
+ "--client <name>",
4941
+ "Specific client (claude-desktop, claude-code, cursor, codex, windsurf)"
4942
+ ).option("--agent <name>", "Bind to specific agent").option("--list", "List detected clients").action(
4450
4943
  safe(async (options) => {
4451
4944
  if (options.list) {
4452
4945
  await runList();
@@ -4455,7 +4948,10 @@ program.command("install").description("Install elisym MCP server into client co
4455
4948
  }
4456
4949
  })
4457
4950
  );
4458
- program.command("update").description("Refresh the elisym MCP entry in installed client configs").option("--client <name>", "Specific client (claude-desktop, claude-code, cursor, windsurf)").option("--agent <name>", "Override the agent binding").action(
4951
+ program.command("update").description("Refresh the elisym MCP entry in installed client configs").option(
4952
+ "--client <name>",
4953
+ "Specific client (claude-desktop, claude-code, cursor, codex, windsurf)"
4954
+ ).option("--agent <name>", "Override the agent binding").action(
4459
4955
  safe(async (options) => {
4460
4956
  await runUpdate({ client: options.client, agent: options.agent });
4461
4957
  })