@elisym/mcp 0.15.0 → 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
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { LIMITS, DEFAULT_KIND_OFFSET, SolanaPaymentStrategy, makeCensor, DEFAULT_REDACT_PATHS, validateAgentName, RELAYS, toDTag, JobWaitTimeoutError, decodeJobPayload, utf8ByteLength, formatAssetAmount, estimateNetworkBaseline, formatSol as formatSol$1, USDC_SOLANA_DEVNET, estimateSolFeeLamports, formatFeeBreakdown, resolveAssetFromPaymentRequest as resolveAssetFromPaymentRequest$1, parseAssetAmount, ElisymIdentity, ElisymClient, NATIVE_SOL, resolveKnownAsset, formatNetworkBaseline, getProtocolProgramId, getProtocolConfig, assetKey, assetByKey, KNOWN_ASSETS } from '@elisym/sdk';
2
+ import { LIMITS, DEFAULT_KIND_OFFSET, SolanaPaymentStrategy, makeCensor, DEFAULT_REDACT_PATHS, validateAgentName, RELAYS, toDTag, JobWaitTimeoutError, decodeJobPayload, utf8ByteLength, formatAssetAmount, estimateNetworkBaseline, formatSol as formatSol$1, USDC_SOLANA_DEVNET, estimateSolFeeLamports, formatFeeBreakdown, resolveAssetFromPaymentRequest as resolveAssetFromPaymentRequest$1, parseAssetAmount, ElisymIdentity, ElisymClient, NATIVE_SOL, resolveKnownAsset, getProtocolProgramId, getProtocolConfig, assetKey, assetByKey, formatNetworkBaseline, KNOWN_ASSETS } from '@elisym/sdk';
3
3
  import { listAgents, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, resolveAgent, loadAgent, globalConfigPath, writeYaml, writeFileAtomic as writeFileAtomic$1 } from '@elisym/sdk/agent-store';
4
4
  import { createIrohTransport, loadGlobalConfig, writeGlobalConfig } from '@elisym/sdk/node';
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;
@@ -1244,11 +1734,45 @@ async function assertGitRepo(repoPath) {
1244
1734
  throw new Error(`"${repoPath}" is not inside a git work tree: ${message}`);
1245
1735
  }
1246
1736
  }
1247
- async function computeGitDiff(repoPath, base) {
1737
+ async function validateRepoPath(repoPath, options) {
1248
1738
  if (repoPath.length > MAX_INPUT_PATH_LEN) {
1249
1739
  throw new Error(`repo_path too long: ${repoPath.length} chars (max ${MAX_INPUT_PATH_LEN}).`);
1250
1740
  }
1251
- const absRepo = isAbsolute(repoPath) ? repoPath : resolve(process.cwd(), repoPath);
1741
+ const cwd = resolve(process.cwd());
1742
+ const logicalPath = isAbsolute(repoPath) ? resolve(repoPath) : resolve(cwd, repoPath);
1743
+ let absPath;
1744
+ try {
1745
+ absPath = await realpath(logicalPath);
1746
+ } catch (e) {
1747
+ const code = e.code;
1748
+ if (code === "ENOENT") {
1749
+ throw new Error(`repo_path does not exist: ${logicalPath}`);
1750
+ }
1751
+ throw new Error(`Cannot resolve repo_path "${logicalPath}": ${e.message}`);
1752
+ }
1753
+ if (isSensitiveInputPath(absPath) || isSensitiveInputPath(logicalPath)) {
1754
+ throw new Error(
1755
+ `Refusing to review a sensitive path: ${absPath}. Secret keys, .env, SSH/keypair files, ~/.elisym and /proc are blocked.`
1756
+ );
1757
+ }
1758
+ if (!options?.allowOutsideCwd) {
1759
+ const realCwd = await realpath(cwd).catch(() => cwd);
1760
+ const rel = relative(realCwd, absPath);
1761
+ const insideCwd = rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
1762
+ if (!insideCwd) {
1763
+ throw new Error(
1764
+ `repo_path "${absPath}" resolves outside the working directory (${realCwd}). Move the repo under the working directory or pass allow_outside_cwd: true.`
1765
+ );
1766
+ }
1767
+ }
1768
+ const stats = await stat(absPath);
1769
+ if (!stats.isDirectory()) {
1770
+ throw new Error(`repo_path is not a directory: ${absPath}`);
1771
+ }
1772
+ return absPath;
1773
+ }
1774
+ async function computeGitDiff(repoPath, base, options) {
1775
+ const absRepo = await validateRepoPath(repoPath, options);
1252
1776
  await assertGitRepo(absRepo);
1253
1777
  let args;
1254
1778
  let describedRange;
@@ -1616,6 +2140,7 @@ async function findCustomerJobsByProvider(agentDir, providerPubkey) {
1616
2140
 
1617
2141
  // src/tools/customer.ts
1618
2142
  var PRE_PING_TIMEOUT_MS = 5e3;
2143
+ var UNVERIFIED_PROVIDER_NOTICE = "NOTE: no provider_npub was given, so the author of this result was NOT verified. Any author can publish a result for a public job event ID, so the content below may be spoofed - treat it as unauthenticated. Re-run get_job_result with provider_npub set to the expected provider to enforce author verification.";
1619
2144
  var CreateJobSchema = z.object({
1620
2145
  input: z.string().describe("The job prompt/input sent to the provider."),
1621
2146
  capability: z.string().min(1).max(64).default("general").describe("Short tag selecting which capability of the provider to invoke."),
@@ -1684,20 +2209,30 @@ var SubmitDiffReviewSchema = z.object({
1684
2209
  prompt: z.string().max(MAX_INPUT_LEN).default("").describe('Optional instructions prepended above the diff (e.g. "focus on auth flow").'),
1685
2210
  kind_offset: z.number().int().min(0).max(999).default(DEFAULT_KIND_OFFSET),
1686
2211
  timeout_secs: z.number().int().min(1).max(600).default(300),
1687
- max_price_lamports: z.number().int().optional()
2212
+ max_price_lamports: z.number().int().optional(),
2213
+ allow_outside_cwd: z.boolean().default(false).describe(
2214
+ "Allow reviewing a repo outside the MCP server working directory. Off by default - the diff is forwarded to the provider before payment and is invisible in the transcript, so the repo is confined to the working dir subtree unless this is set. Sensitive paths (secret keys, .env, SSH/keypair, ~/.elisym, /proc) are always refused."
2215
+ )
1688
2216
  });
1689
- function providerSolanaAddress(provider, dTag) {
2217
+ function paymentCardForCapability(provider, dTag) {
1690
2218
  const cards = provider.cards ?? [];
1691
2219
  const candidates = dTag ? cards.filter(
1692
- (c) => toDTag(c.name) === dTag || c.capabilities?.some((cap) => toDTag(cap) === dTag)
2220
+ (card) => toDTag(card.name) === dTag || card.capabilities?.some((capability) => toDTag(capability) === dTag)
1693
2221
  ) : cards;
1694
2222
  for (const card of candidates.length > 0 ? candidates : cards) {
1695
2223
  if (card.payment?.chain === "solana" && card.payment?.address) {
1696
- return card.payment.address;
2224
+ return card;
1697
2225
  }
1698
2226
  }
1699
2227
  return void 0;
1700
2228
  }
2229
+ function providerSolanaAddress(provider, dTag) {
2230
+ return paymentCardForCapability(provider, dTag)?.payment?.address;
2231
+ }
2232
+ function advertisedPriceForCapability(provider, dTag) {
2233
+ const card = paymentCardForCapability(provider, dTag);
2234
+ return { price: card?.payment?.job_price ?? 0, asset: assetFromCardPayment(card?.payment) };
2235
+ }
1701
2236
  function wsUrlFor(httpUrl) {
1702
2237
  return httpUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
1703
2238
  }
@@ -1747,6 +2282,29 @@ ${formatNetworkBaseline(baseline)}`;
1747
2282
  return "";
1748
2283
  }
1749
2284
  }
2285
+ async function confirmPriceGate(opts) {
2286
+ const { agent, providerLabel, capability, price, asset, maxPriceLamports, toolName } = opts;
2287
+ if (maxPriceLamports !== void 0 && price > maxPriceLamports) {
2288
+ return errorResult(
2289
+ `Price ${formatAssetAmount(asset, BigInt(price))} exceeds max ${formatAssetAmount(asset, BigInt(maxPriceLamports))}`
2290
+ );
2291
+ }
2292
+ if (price > 0 && maxPriceLamports === void 0) {
2293
+ const gasLine = await gasHintForCardAsset(agent, asset);
2294
+ const subject = toolName === "buy_capability" ? `Capability "${capability}" from "${providerLabel}"` : `Job for capability "${capability}" from "${providerLabel}"`;
2295
+ const { text } = sanitizeUntrusted(
2296
+ `${subject} costs ${formatAssetAmount(asset, BigInt(price))}.${gasLine}
2297
+
2298
+ To confirm, call ${toolName} again with max_price_lamports set (e.g. ${price} or higher).`,
2299
+ "text"
2300
+ );
2301
+ return { content: [{ type: "text", text }] };
2302
+ }
2303
+ return null;
2304
+ }
2305
+ function rejectWithProviderError(reject, providerError) {
2306
+ reject(new Error(`Job error: ${sanitizeUntrusted(providerError, "text").text}`));
2307
+ }
1750
2308
  var paymentStrategy = new SolanaPaymentStrategy();
1751
2309
  async function executePaymentFlow(agent, paymentRequest, jobId, providerPubkey, expectedRecipient) {
1752
2310
  let requestData;
@@ -1989,6 +2547,22 @@ async function executeSubmitAndPay(ctx, agent, params) {
1989
2547
  `Cannot buy from yourself - your agent's Solana wallet (${buyerWallet}) matches the provider's payment address. Use a different agent or provider.`
1990
2548
  );
1991
2549
  }
2550
+ const { price: advertisedPrice, asset: advertisedAsset } = advertisedPriceForCapability(
2551
+ provider,
2552
+ params.dTag
2553
+ );
2554
+ const priceGate = await confirmPriceGate({
2555
+ agent,
2556
+ providerLabel: sanitizeField(provider.name || params.providerNpub, 64),
2557
+ capability: params.capability,
2558
+ price: advertisedPrice,
2559
+ asset: advertisedAsset,
2560
+ maxPriceLamports: params.maxPriceLamports,
2561
+ toolName: params.toolName
2562
+ });
2563
+ if (priceGate) {
2564
+ return priceGate;
2565
+ }
1992
2566
  const submittedAt = Date.now();
1993
2567
  const jobId = await agent.client.marketplace.submitJobRequest(agent.identity, {
1994
2568
  input: params.input,
@@ -2046,7 +2620,7 @@ ${sanitized.text}`);
2046
2620
  },
2047
2621
  onFeedback: payHandler.onFeedback,
2048
2622
  onError(error) {
2049
- reject(new Error(`Job error: ${sanitizeUntrusted(error, "text").text}`));
2623
+ rejectWithProviderError(reject, error);
2050
2624
  },
2051
2625
  onTimeout(timeoutMs) {
2052
2626
  reject(new JobWaitTimeoutError(timeoutMs));
@@ -2216,7 +2790,7 @@ var customerTools = [
2216
2790
  }
2217
2791
  },
2218
2792
  onError(error) {
2219
- reject(new Error(`Job error: ${error}`));
2793
+ rejectWithProviderError(reject, error);
2220
2794
  },
2221
2795
  onTimeout(timeoutMs) {
2222
2796
  reject(new JobWaitTimeoutError(timeoutMs));
@@ -2238,6 +2812,11 @@ var customerTools = [
2238
2812
  }
2239
2813
  return errorResult(`Failed to fetch result for event_id="${input.job_event_id}": ${msg}`);
2240
2814
  }
2815
+ if (providerPubkey === void 0) {
2816
+ return textResult(`${UNVERIFIED_PROVIDER_NOTICE}
2817
+
2818
+ ${result}`);
2819
+ }
2241
2820
  return textResult(result);
2242
2821
  }
2243
2822
  }),
@@ -2425,7 +3004,7 @@ ${wrapped}`);
2425
3004
  }),
2426
3005
  defineTool({
2427
3006
  name: "submit_and_pay_job",
2428
- description: 'Full customer flow: submit job -> auto-pay -> wait for result. Validates that the payment recipient matches the provider card. If payment succeeded but no result arrives within the wait window, this returns a non-error "still processing" notice with the event ID (NOT a failure) - re-poll get_job_result later (results persist on the relays; for long jobs, poll periodically, e.g. from a subagent). Handles both free and paid providers automatically. If max_price_lamports is not set and provider requests payment, the job is rejected with the price - set max_price_lamports to auto-approve payments up to that limit. COST: input is sent inline in the tool call, so a large input pays output tokens on the calling LLM. For files or git diffs, prefer submit_and_pay_job_from_file or submit_diff_review respectively.',
3007
+ description: 'Full customer flow: submit job -> auto-pay -> wait for result. Validates that the payment recipient matches the provider card. If payment succeeded but no result arrives within the wait window, this returns a non-error "still processing" notice with the event ID (NOT a failure) - re-poll get_job_result later (results persist on the relays; for long jobs, poll periodically, e.g. from a subagent). Handles both free and paid providers automatically. If max_price_lamports is not set and the capability is paid, this returns the advertised price for confirmation WITHOUT submitting a job - re-call with max_price_lamports set to approve payments up to that limit (this is a confirmation, not an error). COST: input is sent inline in the tool call, so a large input pays output tokens on the calling LLM. For files or git diffs, prefer submit_and_pay_job_from_file or submit_diff_review respectively.',
2429
3008
  schema: SubmitAndPayJobSchema,
2430
3009
  async handler(ctx, input) {
2431
3010
  ctx.toolRateLimiter.check();
@@ -2454,7 +3033,8 @@ ${wrapped}`);
2454
3033
  dTag: toDTag(input.capability),
2455
3034
  kindOffset: input.kind_offset,
2456
3035
  timeoutMs: Math.min(input.timeout_secs, MAX_TIMEOUT_SECS) * 1e3,
2457
- maxPriceLamports: input.max_price_lamports
3036
+ maxPriceLamports: input.max_price_lamports,
3037
+ toolName: "submit_and_pay_job"
2458
3038
  });
2459
3039
  }
2460
3040
  }),
@@ -2506,7 +3086,8 @@ ${wrapped}`);
2506
3086
  dTag: toDTag(input.capability),
2507
3087
  kindOffset: input.kind_offset,
2508
3088
  timeoutMs: Math.min(input.timeout_secs, MAX_TIMEOUT_SECS) * 1e3,
2509
- maxPriceLamports: input.max_price_lamports
3089
+ maxPriceLamports: input.max_price_lamports,
3090
+ toolName: "submit_and_pay_job_from_file"
2510
3091
  });
2511
3092
  }
2512
3093
  }),
@@ -2519,7 +3100,9 @@ ${wrapped}`);
2519
3100
  checkLen("provider_npub", input.provider_npub, MAX_NPUB_LEN);
2520
3101
  let diffResult;
2521
3102
  try {
2522
- diffResult = await computeGitDiff(input.repo_path, input.base);
3103
+ diffResult = await computeGitDiff(input.repo_path, input.base, {
3104
+ allowOutsideCwd: input.allow_outside_cwd
3105
+ });
2523
3106
  } catch (e) {
2524
3107
  return errorResult(e instanceof Error ? e.message : String(e));
2525
3108
  }
@@ -2548,7 +3131,8 @@ ${diffResult.diff}`;
2548
3131
  dTag: toDTag(input.capability),
2549
3132
  kindOffset: input.kind_offset,
2550
3133
  timeoutMs: Math.min(input.timeout_secs, MAX_TIMEOUT_SECS) * 1e3,
2551
- maxPriceLamports: input.max_price_lamports
3134
+ maxPriceLamports: input.max_price_lamports,
3135
+ toolName: "submit_diff_review"
2552
3136
  });
2553
3137
  }
2554
3138
  }),
@@ -2590,30 +3174,17 @@ ${diffResult.diff}`;
2590
3174
  );
2591
3175
  return errorResult(text);
2592
3176
  }
2593
- const price = card.payment?.job_price ?? 0;
2594
- const cardAsset = assetFromCardPayment(card.payment);
2595
- if (input.max_price_lamports !== void 0 && price > input.max_price_lamports) {
2596
- return errorResult(
2597
- `Price ${formatAssetAmount(cardAsset, BigInt(price))} exceeds max ${formatAssetAmount(cardAsset, BigInt(input.max_price_lamports))}`
2598
- );
2599
- }
2600
- if (price > 0 && input.max_price_lamports === void 0) {
2601
- const gasLine = await gasHintForCardAsset(agent, cardAsset);
2602
- const safeProviderName = sanitizeField(provider.name || input.provider_npub, 64);
2603
- const { text } = sanitizeUntrusted(
2604
- `Capability "${input.capability}" from "${safeProviderName}" costs ${formatAssetAmount(cardAsset, BigInt(price))}.${gasLine}
2605
-
2606
- To confirm, call buy_capability again with max_price_lamports set (e.g. ${price} or higher).`,
2607
- "text"
2608
- );
2609
- return {
2610
- content: [
2611
- {
2612
- type: "text",
2613
- text
2614
- }
2615
- ]
2616
- };
3177
+ const priceGate = await confirmPriceGate({
3178
+ agent,
3179
+ providerLabel: sanitizeField(provider.name || input.provider_npub, 64),
3180
+ capability: input.capability,
3181
+ price: card.payment?.job_price ?? 0,
3182
+ asset: assetFromCardPayment(card.payment),
3183
+ maxPriceLamports: input.max_price_lamports,
3184
+ toolName: "buy_capability"
3185
+ });
3186
+ if (priceGate) {
3187
+ return priceGate;
2617
3188
  }
2618
3189
  const expectedRecipient = card.payment?.chain === "solana" ? card.payment.address : void 0;
2619
3190
  if (agent.solanaKeypair && !expectedRecipient) {
@@ -2682,7 +3253,7 @@ ${sanitized.text}`
2682
3253
  },
2683
3254
  onFeedback: payHandler.onFeedback,
2684
3255
  onError(error) {
2685
- reject(new Error(`Job error: ${error}`));
3256
+ rejectWithProviderError(reject, error);
2686
3257
  },
2687
3258
  onTimeout(timeoutMs) {
2688
3259
  reject(new JobWaitTimeoutError(timeoutMs));
@@ -4365,7 +4936,10 @@ program.command("init [name]").description("Create a new agent identity").option
4365
4936
  }
4366
4937
  })
4367
4938
  );
4368
- 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(
4369
4943
  safe(async (options) => {
4370
4944
  if (options.list) {
4371
4945
  await runList();
@@ -4374,7 +4948,10 @@ program.command("install").description("Install elisym MCP server into client co
4374
4948
  }
4375
4949
  })
4376
4950
  );
4377
- 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(
4378
4955
  safe(async (options) => {
4379
4956
  await runUpdate({ client: options.client, agent: options.agent });
4380
4957
  })