@dev.sail.money/sailor 1.3.0-206 → 1.3.0-209

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.
Files changed (104) hide show
  1. package/package.json +1 -1
  2. package/packages/cli/dist/index.cjs +555 -414
  3. package/packages/cli/dist/server.cjs +1263 -56
  4. package/packages/sdk/dist/intelligence.d.ts +1 -1
  5. package/packages/sdk/dist/intelligence.js +1 -1
  6. package/packages/sdk/dist/keyring.d.ts +0 -6
  7. package/packages/sdk/dist/keyring.d.ts.map +1 -1
  8. package/packages/sdk/dist/keyring.js +1 -8
  9. package/packages/sdk/dist/keyring.js.map +1 -1
  10. package/packages/sdk/dist/signing.d.ts +29 -0
  11. package/packages/sdk/dist/signing.d.ts.map +1 -1
  12. package/packages/ui/dist/assets/{add-Dj4_n773.js → add-CLs0-B0p.js} +1 -1
  13. package/packages/ui/dist/assets/{all-wallets-BaFysN1G.js → all-wallets-CDOk28sj.js} +1 -1
  14. package/packages/ui/dist/assets/{app-store-DOc31zzY.js → app-store-DVzpi1qN.js} +1 -1
  15. package/packages/ui/dist/assets/{apple-BvrvOkPH.js → apple-C9U9iH1l.js} +1 -1
  16. package/packages/ui/dist/assets/{arrow-bottom-E1AfjXOT.js → arrow-bottom-DjQ4jGMe.js} +1 -1
  17. package/packages/ui/dist/assets/{arrow-bottom-circle-sou7Vo1V.js → arrow-bottom-circle-jxial9Si.js} +1 -1
  18. package/packages/ui/dist/assets/{arrow-left-wk3G9Ywf.js → arrow-left-BjAyI8D1.js} +1 -1
  19. package/packages/ui/dist/assets/{arrow-right-_a703_Pw.js → arrow-right-BlFO4Tcx.js} +1 -1
  20. package/packages/ui/dist/assets/{arrow-top-txyg3R3e.js → arrow-top-DJ3oh3DH.js} +1 -1
  21. package/packages/ui/dist/assets/{bank-BB1kd0NJ.js → bank-B98it1qN.js} +1 -1
  22. package/packages/ui/dist/assets/{basic-D0dy7nIZ.js → basic-DsgeyPNu.js} +1 -1
  23. package/packages/ui/dist/assets/{browser-BwHLhsTn.js → browser-j4vCeHwE.js} +1 -1
  24. package/packages/ui/dist/assets/{card-DRC6HmPF.js → card-CzxbMvEd.js} +1 -1
  25. package/packages/ui/dist/assets/{ccip-Dq6KeJQl.js → ccip-U9yiu_EK.js} +1 -1
  26. package/packages/ui/dist/assets/{checkmark-TbAAQGjd.js → checkmark-D5sWBbk3.js} +1 -1
  27. package/packages/ui/dist/assets/{checkmark-bold-4723vJ0-.js → checkmark-bold-BW6w3KCC.js} +1 -1
  28. package/packages/ui/dist/assets/{chevron-bottom-DtLoBRe9.js → chevron-bottom-BbHhmTQq.js} +1 -1
  29. package/packages/ui/dist/assets/{chevron-left-BtydsF9j.js → chevron-left-D8BDH4Wo.js} +1 -1
  30. package/packages/ui/dist/assets/{chevron-right-BbkU_Kf8.js → chevron-right-BQuQk5zc.js} +1 -1
  31. package/packages/ui/dist/assets/{chevron-top-BWf-sN-F.js → chevron-top-1oeSXkx4.js} +1 -1
  32. package/packages/ui/dist/assets/{chrome-store-CVFxc5H2.js → chrome-store-B1OMtzC-.js} +1 -1
  33. package/packages/ui/dist/assets/{clock-B4hJww1X.js → clock-e0HueehS.js} +1 -1
  34. package/packages/ui/dist/assets/{close-Ci93jqGe.js → close-Cy1ytr26.js} +1 -1
  35. package/packages/ui/dist/assets/{coinPlaceholder-cRv_xDgC.js → coinPlaceholder-DqJu3ZPV.js} +1 -1
  36. package/packages/ui/dist/assets/{compass-_sZZPtm6.js → compass-XDz_N2Hg.js} +1 -1
  37. package/packages/ui/dist/assets/{copy-DLaiLK0n.js → copy-DmDo3Nob.js} +1 -1
  38. package/packages/ui/dist/assets/{core-YssZmpJa.js → core-DbkMGSYW.js} +3 -3
  39. package/packages/ui/dist/assets/cursor-DRFqL8nZ.js +3 -0
  40. package/packages/ui/dist/assets/{cursor-transparent-CzXQbB2p.js → cursor-transparent-DF2aA10D.js} +1 -1
  41. package/packages/ui/dist/assets/{desktop-BiqQI1Ss.js → desktop-CVydOWLn.js} +1 -1
  42. package/packages/ui/dist/assets/{disconnect-DYu7YCzu.js → disconnect-BuN2Whnz.js} +1 -1
  43. package/packages/ui/dist/assets/{discord-C-ife-UZ.js → discord-DMNPvTB2.js} +1 -1
  44. package/packages/ui/dist/assets/{etherscan-Dq7ac-p9.js → etherscan-CacW-93f.js} +1 -1
  45. package/packages/ui/dist/assets/{events-BhTGLk8n.js → events-CZkVDFih.js} +1 -1
  46. package/packages/ui/dist/assets/{exclamation-triangle-BcOdRdbp.js → exclamation-triangle-B26hIvqO.js} +1 -1
  47. package/packages/ui/dist/assets/{extension-DmJLIidM.js → extension-B7rI0gv7.js} +1 -1
  48. package/packages/ui/dist/assets/{external-link-HNjp7-Po.js → external-link-CX8skCbH.js} +1 -1
  49. package/packages/ui/dist/assets/{facebook-BrSvHNyw.js → facebook-BDqbE6kl.js} +1 -1
  50. package/packages/ui/dist/assets/{fallback-Ubz80uXP.js → fallback-DWQYKf3l.js} +1 -1
  51. package/packages/ui/dist/assets/{farcaster-BsjltM6k.js → farcaster-zQ3oQzZ8.js} +1 -1
  52. package/packages/ui/dist/assets/{filters-B4yQbYZP.js → filters-DCglZYsM.js} +1 -1
  53. package/packages/ui/dist/assets/{github-CFq82Dds.js → github-BH-BYjss.js} +1 -1
  54. package/packages/ui/dist/assets/{google-DwZutf4a.js → google-DYFWxZbC.js} +1 -1
  55. package/packages/ui/dist/assets/{help-circle-CyA_8KQg.js → help-circle-BXH_DtZ_.js} +1 -1
  56. package/packages/ui/dist/assets/{id-jyZYn4LJ.js → id-Dr2yTeX1.js} +1 -1
  57. package/packages/ui/dist/assets/{image-Zv0yoBcH.js → image-DeJey1Eg.js} +1 -1
  58. package/packages/ui/dist/assets/{index-CeGppB9s.js → index-BP-UuQ8g.js} +1 -1
  59. package/packages/ui/dist/assets/{index-BY8A6EK8.js → index-BksbyFjf.js} +1 -1
  60. package/packages/ui/dist/assets/{index-BdG43zCl.js → index-C55-Ne92.js} +1 -1
  61. package/packages/ui/dist/assets/index-D6_Zei-R.js +1790 -0
  62. package/packages/ui/dist/assets/{index-67NncC_u.js → index-DVjAiPHI.js} +3 -3
  63. package/packages/ui/dist/assets/{index-BxRaYDiX.js → index-uSdXE5b_.js} +1 -1
  64. package/packages/ui/dist/assets/{index.es-C8EklrJY.js → index.es-dZlNPpfR.js} +4 -4
  65. package/packages/ui/dist/assets/{info-BwGEOdmB.js → info-B91pD5Rw.js} +1 -1
  66. package/packages/ui/dist/assets/{info-circle-BPCcX-Iq.js → info-circle-CySl3_on.js} +1 -1
  67. package/packages/ui/dist/assets/{lightbulb-BRwr4IaC.js → lightbulb-wwG8LQLo.js} +1 -1
  68. package/packages/ui/dist/assets/{mail-D-k_Vhe4.js → mail-D617YZ8y.js} +1 -1
  69. package/packages/ui/dist/assets/{metamask-sdk-AcOgwGrj.js → metamask-sdk-RbLbxUgF.js} +1 -1
  70. package/packages/ui/dist/assets/{mobile-B1ok9PfY.js → mobile-CD9KlA0m.js} +1 -1
  71. package/packages/ui/dist/assets/{more-DeMTMLWs.js → more-BQ_rpPXo.js} +1 -1
  72. package/packages/ui/dist/assets/{network-placeholder-J0Rc1I89.js → network-placeholder-BA7nl9e6.js} +1 -1
  73. package/packages/ui/dist/assets/{nftPlaceholder-dXGrzzoa.js → nftPlaceholder-D1EaaeDn.js} +1 -1
  74. package/packages/ui/dist/assets/{off-CS0f77lw.js → off-DnGG35_w.js} +1 -1
  75. package/packages/ui/dist/assets/{parseSignature-DJ16fHC4.js → parseSignature-E29x8poM.js} +1 -1
  76. package/packages/ui/dist/assets/{play-store-DmK8UaNC.js → play-store-C5u2FvYB.js} +1 -1
  77. package/packages/ui/dist/assets/{plus-CSGSnoCh.js → plus-CM4xJkvI.js} +1 -1
  78. package/packages/ui/dist/assets/{qr-code-BJMH0xmM.js → qr-code-kM3FUIdR.js} +1 -1
  79. package/packages/ui/dist/assets/{recycle-horizontal-D5XP5VgJ.js → recycle-horizontal-DWrRWIBM.js} +1 -1
  80. package/packages/ui/dist/assets/{refresh-MLX3Rdp6.js → refresh-CztbjK2d.js} +1 -1
  81. package/packages/ui/dist/assets/{reown-logo-CVoDFQbM.js → reown-logo-DUHEIodF.js} +1 -1
  82. package/packages/ui/dist/assets/{search-BDJDyvjC.js → search-BJtb4mn1.js} +1 -1
  83. package/packages/ui/dist/assets/{secp256k1-BWh3B02k.js → secp256k1-BjrTZpQx.js} +1 -1
  84. package/packages/ui/dist/assets/{send-BOq2svx_.js → send-B3_kavG1.js} +1 -1
  85. package/packages/ui/dist/assets/{swapHorizontal-ByjLi_-_.js → swapHorizontal-BrF9POiv.js} +1 -1
  86. package/packages/ui/dist/assets/{swapHorizontalBold-fmCp_4AE.js → swapHorizontalBold-Dgf8OjXj.js} +1 -1
  87. package/packages/ui/dist/assets/{swapHorizontalMedium-D6dFseYw.js → swapHorizontalMedium-C6wX0JZj.js} +1 -1
  88. package/packages/ui/dist/assets/{swapHorizontalRoundedBold-B2V3m4ha.js → swapHorizontalRoundedBold-DTAVw6oF.js} +1 -1
  89. package/packages/ui/dist/assets/{swapVertical-DVmCsmeT.js → swapVertical-KGlTFNoi.js} +1 -1
  90. package/packages/ui/dist/assets/{telegram-Bv8JRbMM.js → telegram-DnLrK-4y.js} +1 -1
  91. package/packages/ui/dist/assets/{three-dots-YYDJ18z6.js → three-dots-C6_oMJfw.js} +1 -1
  92. package/packages/ui/dist/assets/{twitch-B2_Jy-Ft.js → twitch-DvPqPD4f.js} +1 -1
  93. package/packages/ui/dist/assets/{twitterIcon-pKQzPQRt.js → twitterIcon-BZdk82Am.js} +1 -1
  94. package/packages/ui/dist/assets/{verify-Bty2jIav.js → verify-CbESD5G0.js} +1 -1
  95. package/packages/ui/dist/assets/{verify-filled-CKz2fuKl.js → verify-filled-BRjTrLxw.js} +1 -1
  96. package/packages/ui/dist/assets/{w3m-modal-Wg1mtmVM.js → w3m-modal-D9b2TGHc.js} +1 -1
  97. package/packages/ui/dist/assets/{wallet-DslzC8rD.js → wallet-B87E5UId.js} +1 -1
  98. package/packages/ui/dist/assets/{wallet-placeholder-Dyw8jJ56.js → wallet-placeholder-DmssSbYf.js} +1 -1
  99. package/packages/ui/dist/assets/{walletconnect-DZBfbgp4.js → walletconnect-CZkLKWMZ.js} +1 -1
  100. package/packages/ui/dist/assets/{warning-circle-D7NPDx4k.js → warning-circle-BPTpHB1P.js} +1 -1
  101. package/packages/ui/dist/assets/{x-DRR_9FNs.js → x-D_dEtEDU.js} +1 -1
  102. package/packages/ui/dist/index.html +1 -1
  103. package/packages/ui/dist/assets/cursor-B_xyJnjj.js +0 -3
  104. package/packages/ui/dist/assets/index-_DkPx8RN.js +0 -1790
@@ -22626,7 +22626,7 @@ var require_websocket = __commonJS({
22626
22626
  var EventEmitter = require("events");
22627
22627
  var https = require("https");
22628
22628
  var http2 = require("http");
22629
- var net2 = require("net");
22629
+ var net3 = require("net");
22630
22630
  var tls = require("tls");
22631
22631
  var { randomBytes: randomBytes4, createHash } = require("crypto");
22632
22632
  var { Duplex, Readable } = require("stream");
@@ -23360,12 +23360,12 @@ var require_websocket = __commonJS({
23360
23360
  }
23361
23361
  function netConnect(options) {
23362
23362
  options.path = options.socketPath;
23363
- return net2.connect(options);
23363
+ return net3.connect(options);
23364
23364
  }
23365
23365
  function tlsConnect(options) {
23366
23366
  options.path = void 0;
23367
23367
  if (!options.servername && options.servername !== "") {
23368
- options.servername = net2.isIP(options.host) ? "" : options.host;
23368
+ options.servername = net3.isIP(options.host) ? "" : options.host;
23369
23369
  }
23370
23370
  return tls.connect(options);
23371
23371
  }
@@ -33756,7 +33756,7 @@ var require_websocket2 = __commonJS({
33756
33756
  var EventEmitter = require("events");
33757
33757
  var https = require("https");
33758
33758
  var http2 = require("http");
33759
- var net2 = require("net");
33759
+ var net3 = require("net");
33760
33760
  var tls = require("tls");
33761
33761
  var { randomBytes: randomBytes4, createHash } = require("crypto");
33762
33762
  var { Duplex, Readable } = require("stream");
@@ -34500,12 +34500,12 @@ var require_websocket2 = __commonJS({
34500
34500
  }
34501
34501
  function netConnect(options) {
34502
34502
  options.path = options.socketPath;
34503
- return net2.connect(options);
34503
+ return net3.connect(options);
34504
34504
  }
34505
34505
  function tlsConnect(options) {
34506
34506
  options.path = void 0;
34507
34507
  if (!options.servername && options.servername !== "") {
34508
- options.servername = net2.isIP(options.host) ? "" : options.host;
34508
+ options.servername = net3.isIP(options.host) ? "" : options.host;
34509
34509
  }
34510
34510
  return tls.connect(options);
34511
34511
  }
@@ -37445,18 +37445,12 @@ var LocalKeyring = class _LocalKeyring {
37445
37445
  this.account = account2;
37446
37446
  this.address = account2.address;
37447
37447
  this.privateKey = options.privateKey;
37448
- } else if (options.type === "mnemonic") {
37448
+ } else {
37449
37449
  const account2 = mnemonicToAccount(options.mnemonic, options.derivationPath ? { path: options.derivationPath } : void 0);
37450
37450
  this.account = account2;
37451
37451
  this.address = account2.address;
37452
- } else {
37453
- throw new Error("keystore decryption not implemented \u2014 use type: 'privateKey' or type: 'mnemonic' instead");
37454
37452
  }
37455
37453
  }
37456
- /** Returns a lightweight signer stub for read-only contexts where the key is not available. */
37457
- static fromAddress(address) {
37458
- return { address };
37459
- }
37460
37454
  /** Generates a brand-new keyring backed by a random private key. */
37461
37455
  static generate() {
37462
37456
  return new _LocalKeyring({ type: "privateKey", privateKey: generatePrivateKey() });
@@ -38497,15 +38491,58 @@ function upsertAccountInList(account2, name, baseSailDir = sailDir()) {
38497
38491
  }
38498
38492
 
38499
38493
  // src/signing/client.ts
38500
- var import_node_fs6 = require("node:fs");
38494
+ var import_node_fs7 = require("node:fs");
38501
38495
  var import_node_path5 = require("node:path");
38502
38496
 
38503
38497
  // src/signing/server.ts
38504
38498
  var import_node_crypto2 = require("node:crypto");
38505
- var import_node_fs5 = require("node:fs");
38499
+ var import_node_fs6 = require("node:fs");
38506
38500
  var import_node_http = require("node:http");
38507
- var import_node_net = require("node:net");
38508
38501
  var import_node_path4 = require("node:path");
38502
+
38503
+ // src/lib/process.ts
38504
+ var import_node_fs5 = __toESM(require("node:fs"), 1);
38505
+ var import_node_net = __toESM(require("node:net"), 1);
38506
+ function agentPidPath(chainId) {
38507
+ return chainId != null ? sailPath(`agent-${chainId}.pid`) : sailPath("agent.pid");
38508
+ }
38509
+ function writeAgentPid(chainId) {
38510
+ import_node_fs5.default.mkdirSync(sailPath(), { recursive: true });
38511
+ import_node_fs5.default.writeFileSync(agentPidPath(chainId), `${process.pid}
38512
+ `);
38513
+ }
38514
+ function clearAgentPid(chainId) {
38515
+ try {
38516
+ import_node_fs5.default.rmSync(agentPidPath(chainId));
38517
+ } catch {
38518
+ }
38519
+ }
38520
+ function readAgentPid(chainId) {
38521
+ try {
38522
+ const pid = Number.parseInt(import_node_fs5.default.readFileSync(agentPidPath(chainId), "utf-8").trim(), 10);
38523
+ return Number.isNaN(pid) ? null : pid;
38524
+ } catch {
38525
+ return null;
38526
+ }
38527
+ }
38528
+ function isProcessAlive(pid) {
38529
+ try {
38530
+ process.kill(pid, 0);
38531
+ return true;
38532
+ } catch (err) {
38533
+ return err.code === "EPERM";
38534
+ }
38535
+ }
38536
+ function findFreePort(from14) {
38537
+ return new Promise((resolve3, reject) => {
38538
+ const server = import_node_net.default.createServer();
38539
+ server.unref();
38540
+ server.on("error", () => findFreePort(from14 + 1).then(resolve3, reject));
38541
+ server.listen(from14, "127.0.0.1", () => server.close(() => resolve3(from14)));
38542
+ });
38543
+ }
38544
+
38545
+ // src/signing/server.ts
38509
38546
  init_esm2();
38510
38547
 
38511
38548
  // ../../node_modules/.pnpm/ws@8.21.0_bufferutil@4.1.0_utf-8-validate@5.0.10/node_modules/ws/wrapper.mjs
@@ -38545,7 +38582,7 @@ function findUiDist() {
38545
38582
  (0, import_node_path4.join)(process.cwd(), "..", "ui", "dist")
38546
38583
  ];
38547
38584
  for (const c of candidates) {
38548
- if ((0, import_node_fs5.existsSync)((0, import_node_path4.join)(c, "index.html"))) return c;
38585
+ if ((0, import_node_fs6.existsSync)((0, import_node_path4.join)(c, "index.html"))) return c;
38549
38586
  }
38550
38587
  return null;
38551
38588
  }
@@ -38593,7 +38630,7 @@ var SigningServer = class {
38593
38630
  return this.httpServer?.listening ?? false;
38594
38631
  }
38595
38632
  async start() {
38596
- this.port = await findAvailablePort(this.port);
38633
+ this.port = await findFreePort(this.port);
38597
38634
  this._url = `http://localhost:${this.port}`;
38598
38635
  this.requestSecret = (0, import_node_crypto2.randomBytes)(16).toString("hex");
38599
38636
  const http2 = (0, import_node_http.createServer)((req, res) => this.handleHttp(req, res));
@@ -38773,13 +38810,13 @@ var SigningServer = class {
38773
38810
  const path11 = this.sailFile("config.json");
38774
38811
  let config = {};
38775
38812
  try {
38776
- config = JSON.parse((0, import_node_fs5.readFileSync)(path11, "utf-8"));
38813
+ config = JSON.parse((0, import_node_fs6.readFileSync)(path11, "utf-8"));
38777
38814
  } catch {
38778
38815
  }
38779
38816
  if (Number(config.chainId) === Number(chainId)) return;
38780
38817
  config.chainId = Number(chainId);
38781
- (0, import_node_fs5.mkdirSync)(this.sailFile(), { recursive: true });
38782
- (0, import_node_fs5.writeFileSync)(path11, `${JSON.stringify(config, null, 2)}
38818
+ (0, import_node_fs6.mkdirSync)(this.sailFile(), { recursive: true });
38819
+ (0, import_node_fs6.writeFileSync)(path11, `${JSON.stringify(config, null, 2)}
38783
38820
  `);
38784
38821
  } catch {
38785
38822
  }
@@ -38787,7 +38824,7 @@ var SigningServer = class {
38787
38824
  /** Stream a JSON file back, or a fallback body when it is missing/invalid. */
38788
38825
  sendJsonFile(res, filePath, fallback2) {
38789
38826
  try {
38790
- const raw = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
38827
+ const raw = (0, import_node_fs6.readFileSync)(filePath, "utf-8");
38791
38828
  JSON.parse(raw);
38792
38829
  res.writeHead(200, { "Content-Type": "application/json" });
38793
38830
  res.end(raw);
@@ -38854,8 +38891,8 @@ var SigningServer = class {
38854
38891
  };
38855
38892
  const baseSailDir = this.sailFile();
38856
38893
  upsertAccountInList(record, void 0, baseSailDir);
38857
- (0, import_node_fs5.mkdirSync)(baseSailDir, { recursive: true });
38858
- (0, import_node_fs5.writeFileSync)(this.sailFile("account.json"), `${JSON.stringify(record, null, 2)}
38894
+ (0, import_node_fs6.mkdirSync)(baseSailDir, { recursive: true });
38895
+ (0, import_node_fs6.writeFileSync)(this.sailFile("account.json"), `${JSON.stringify(record, null, 2)}
38859
38896
  `);
38860
38897
  this.syncConfigChainId(chainId);
38861
38898
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -38870,12 +38907,12 @@ var SigningServer = class {
38870
38907
  res.writeHead(200, { "Content-Type": "application/json" });
38871
38908
  let active = null;
38872
38909
  try {
38873
- active = JSON.parse((0, import_node_fs5.readFileSync)(this.sailFile("account.json"), "utf-8")).safe;
38910
+ active = JSON.parse((0, import_node_fs6.readFileSync)(this.sailFile("account.json"), "utf-8")).safe;
38874
38911
  } catch {
38875
38912
  }
38876
38913
  try {
38877
38914
  const accounts = JSON.parse(
38878
- (0, import_node_fs5.readFileSync)(this.sailFile("state", "accounts.json"), "utf-8")
38915
+ (0, import_node_fs6.readFileSync)(this.sailFile("state", "accounts.json"), "utf-8")
38879
38916
  );
38880
38917
  res.end(
38881
38918
  JSON.stringify(
@@ -38888,7 +38925,7 @@ var SigningServer = class {
38888
38925
  } catch {
38889
38926
  try {
38890
38927
  const a = JSON.parse(
38891
- (0, import_node_fs5.readFileSync)(this.sailFile("account.json"), "utf-8")
38928
+ (0, import_node_fs6.readFileSync)(this.sailFile("account.json"), "utf-8")
38892
38929
  );
38893
38930
  res.end(JSON.stringify([{ ...a, name: "My SMA", active: true, addedAt: null }]));
38894
38931
  } catch {
@@ -39011,16 +39048,16 @@ var SigningServer = class {
39011
39048
  res.end();
39012
39049
  return;
39013
39050
  }
39014
- if ((0, import_node_fs5.existsSync)(filePath)) {
39051
+ if ((0, import_node_fs6.existsSync)(filePath)) {
39015
39052
  const mime = MIME[(0, import_node_path4.extname)(filePath)] ?? "application/octet-stream";
39016
39053
  res.writeHead(200, { "Content-Type": mime });
39017
- res.end((0, import_node_fs5.readFileSync)(filePath));
39054
+ res.end((0, import_node_fs6.readFileSync)(filePath));
39018
39055
  return;
39019
39056
  }
39020
39057
  const indexHtml = (0, import_node_path4.join)(this.uiDist, "index.html");
39021
- if ((0, import_node_fs5.existsSync)(indexHtml)) {
39058
+ if ((0, import_node_fs6.existsSync)(indexHtml)) {
39022
39059
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
39023
- res.end((0, import_node_fs5.readFileSync)(indexHtml));
39060
+ res.end((0, import_node_fs6.readFileSync)(indexHtml));
39024
39061
  return;
39025
39062
  }
39026
39063
  }
@@ -39107,15 +39144,15 @@ var SigningServer = class {
39107
39144
  }
39108
39145
  writeRuntimeState() {
39109
39146
  const path11 = (0, import_node_path4.join)(this.runtimeDir, SERVER_STATE_FILE);
39110
- if ((0, import_node_fs5.existsSync)(path11)) {
39147
+ if ((0, import_node_fs6.existsSync)(path11)) {
39111
39148
  try {
39112
- const existing = JSON.parse((0, import_node_fs5.readFileSync)(path11, "utf8"));
39113
- if (existing.pid != null && existing.pid !== process.pid && pidAlive(existing.pid)) return;
39149
+ const existing = JSON.parse((0, import_node_fs6.readFileSync)(path11, "utf8"));
39150
+ if (existing.pid != null && existing.pid !== process.pid && isProcessAlive(existing.pid)) return;
39114
39151
  } catch {
39115
39152
  }
39116
39153
  }
39117
- if (!(0, import_node_fs5.existsSync)(this.runtimeDir)) (0, import_node_fs5.mkdirSync)(this.runtimeDir, { recursive: true });
39118
- (0, import_node_fs5.writeFileSync)(
39154
+ if (!(0, import_node_fs6.existsSync)(this.runtimeDir)) (0, import_node_fs6.mkdirSync)(this.runtimeDir, { recursive: true });
39155
+ (0, import_node_fs6.writeFileSync)(
39119
39156
  (0, import_node_path4.join)(this.runtimeDir, SERVER_STATE_FILE),
39120
39157
  JSON.stringify(
39121
39158
  {
@@ -39134,43 +39171,25 @@ var SigningServer = class {
39134
39171
  removeRuntimeState() {
39135
39172
  const path11 = (0, import_node_path4.join)(this.runtimeDir, SERVER_STATE_FILE);
39136
39173
  try {
39137
- if (!(0, import_node_fs5.existsSync)(path11)) return;
39138
- const state = JSON.parse((0, import_node_fs5.readFileSync)(path11, "utf8"));
39174
+ if (!(0, import_node_fs6.existsSync)(path11)) return;
39175
+ const state = JSON.parse((0, import_node_fs6.readFileSync)(path11, "utf8"));
39139
39176
  if (state.pid != null && state.pid !== process.pid) return;
39140
- (0, import_node_fs5.unlinkSync)(path11);
39177
+ (0, import_node_fs6.unlinkSync)(path11);
39141
39178
  } catch {
39142
39179
  }
39143
39180
  }
39144
39181
  };
39145
- function pidAlive(pid) {
39146
- try {
39147
- process.kill(pid, 0);
39148
- return true;
39149
- } catch (err) {
39150
- return err.code === "EPERM";
39151
- }
39152
- }
39153
39182
  function reapStaleRuntimeState(projectRoot = process.cwd()) {
39154
39183
  const path11 = (0, import_node_path4.join)(projectRoot, RUNTIME_SUBDIR, SERVER_STATE_FILE);
39155
39184
  try {
39156
- if (!(0, import_node_fs5.existsSync)(path11)) return;
39157
- const state = JSON.parse((0, import_node_fs5.readFileSync)(path11, "utf8"));
39158
- if (state.pid != null && state.pid !== process.pid && !pidAlive(state.pid)) {
39159
- (0, import_node_fs5.unlinkSync)(path11);
39185
+ if (!(0, import_node_fs6.existsSync)(path11)) return;
39186
+ const state = JSON.parse((0, import_node_fs6.readFileSync)(path11, "utf8"));
39187
+ if (state.pid != null && state.pid !== process.pid && !isProcessAlive(state.pid)) {
39188
+ (0, import_node_fs6.unlinkSync)(path11);
39160
39189
  }
39161
39190
  } catch {
39162
39191
  }
39163
39192
  }
39164
- async function findAvailablePort(startPort) {
39165
- return new Promise((res) => {
39166
- const probe = (0, import_node_net.createServer)();
39167
- probe.listen(startPort, "127.0.0.1", () => {
39168
- const addr = probe.address();
39169
- probe.close(() => res(addr.port));
39170
- });
39171
- probe.on("error", () => res(findAvailablePort(startPort + 1)));
39172
- });
39173
- }
39174
39193
 
39175
39194
  // src/signing/client.ts
39176
39195
  var RUNTIME_SERVER_FILE = (0, import_node_path5.join)(".sail", "runtime", "server.json");
@@ -39252,14 +39271,14 @@ function toLoopbackIPv4(url) {
39252
39271
  }
39253
39272
  function readRuntimeServerState(projectRoot) {
39254
39273
  const file = (0, import_node_path5.join)(projectRoot, RUNTIME_SERVER_FILE);
39255
- if (!(0, import_node_fs6.existsSync)(file)) return null;
39274
+ if (!(0, import_node_fs7.existsSync)(file)) return null;
39256
39275
  try {
39257
- return JSON.parse((0, import_node_fs6.readFileSync)(file, "utf8"));
39276
+ return JSON.parse((0, import_node_fs7.readFileSync)(file, "utf8"));
39258
39277
  } catch {
39259
39278
  return null;
39260
39279
  }
39261
39280
  }
39262
- function signingPageUrl(_channel, dashboardPort) {
39281
+ function signingPageUrl(dashboardPort) {
39263
39282
  return `http://localhost:${dashboardPort}/#/station`;
39264
39283
  }
39265
39284
  async function discoverDaemon(projectRoot = process.cwd()) {
@@ -39530,7 +39549,7 @@ Deploying SMA on ${getChainById(targetChainId).name} (chain ${targetChainId})\u2
39530
39549
  const channel = await createSigningChannel(process.cwd());
39531
39550
  try {
39532
39551
  await channel.start();
39533
- const stationUrl = signingPageUrl(channel, projectPort(process.cwd()));
39552
+ const stationUrl = signingPageUrl(projectPort(process.cwd()));
39534
39553
  if (json) {
39535
39554
  console.log(
39536
39555
  JSON.stringify(
@@ -39667,100 +39686,6 @@ function emit(json, human, payload) {
39667
39686
 
39668
39687
  // src/lib/project.ts
39669
39688
  init_esm2();
39670
-
39671
- // src/lib/keys.ts
39672
- init_esm2();
39673
- var ROLES = ["manager", "permissionSigner"];
39674
- function normalizeRole(input) {
39675
- const n = input.trim().toLowerCase().replace(/[-_\s]/g, "");
39676
- if (n === "manager" || n === "mgr" || n === "m" || n === "agent" || n === "agentwallet") {
39677
- return "manager";
39678
- }
39679
- if (n === "permissionsigner" || n === "signer" || n === "ps" || n === "permission" || n === "mandatesigner" || n === "mandate") {
39680
- return "permissionSigner";
39681
- }
39682
- return null;
39683
- }
39684
- function roleLabel(role) {
39685
- return role === "manager" ? "agent wallet" : "mandate signer";
39686
- }
39687
- function safeHex(safe) {
39688
- return safe.toLowerCase().replace(/^0x/, "").replace(/[^0-9a-f]/g, "");
39689
- }
39690
- function keyPath(role, safe) {
39691
- if (safe) {
39692
- return sailPath("keys", `${role}-0x${safeHex(safe)}.json`);
39693
- }
39694
- return sailPath("keys", `${role}.json`);
39695
- }
39696
- function legacyKeyPath(role, safe) {
39697
- return sailPath("keys", `${role}-${safeHex(safe)}.json`);
39698
- }
39699
- function resolveKeyPath(role, safe) {
39700
- if (safe) {
39701
- const perSma = keyPath(role, safe);
39702
- if (fileExists(perSma)) return perSma;
39703
- const legacy = legacyKeyPath(role, safe);
39704
- if (fileExists(legacy)) return legacy;
39705
- }
39706
- return keyPath(role);
39707
- }
39708
- function keyExists(role, safe) {
39709
- return fileExists(resolveKeyPath(role, safe));
39710
- }
39711
- async function loadKeyring(role, safe) {
39712
- const keystore = readJsonFile(resolveKeyPath(role, safe));
39713
- if (!keystore) {
39714
- throw new Error(
39715
- `No ${roleLabel(role)} found.
39716
- Run "sailor keys generate" and choose "${roleLabel(role)}" first.`
39717
- );
39718
- }
39719
- const password = await promptHidden(`Password for ${roleLabel(role)} key`);
39720
- try {
39721
- return await LocalKeyring.fromKeystore(keystore, password);
39722
- } catch {
39723
- throw new Error("Invalid password.");
39724
- }
39725
- }
39726
- async function loadManagerSigner(safe) {
39727
- const passphrase = process.env.SAIL_PASSPHRASE;
39728
- if (passphrase) {
39729
- const keystore = readJsonFile(resolveKeyPath("manager", safe));
39730
- if (!keystore) {
39731
- throw new Error(
39732
- 'No agent wallet found.\nRun "sailor keys generate" and choose "agent wallet".'
39733
- );
39734
- }
39735
- try {
39736
- return await LocalKeyring.fromKeystore(keystore, passphrase);
39737
- } catch {
39738
- throw new Error(
39739
- "SAIL_PASSPHRASE does not match this keystore.\nCheck the value in .sail/.env.local (or the SAIL_PASSPHRASE CI secret) \u2014 it must be the passphrase the agent wallet was encrypted with."
39740
- );
39741
- }
39742
- }
39743
- if (process.stdin.isTTY !== true && keyExists("manager", safe)) {
39744
- throw new Error(
39745
- 'Agent keystore found but SAIL_PASSPHRASE is not set.\nIf you created the key in the dashboard, add SAIL_PASSPHRASE to .sail/.env.local, or run "sailor keys generate".\nFor CI, set the SAIL_PASSPHRASE GitHub Actions secret.'
39746
- );
39747
- }
39748
- return loadKeyring("manager", safe);
39749
- }
39750
- function managerKeystorePath(managerAddr) {
39751
- if (!isAddress(managerAddr, { strict: false })) {
39752
- throw new Error(`managerKeystorePath: invalid address "${managerAddr}"`);
39753
- }
39754
- const hex = managerAddr.toLowerCase().replace(/^0x/, "");
39755
- return sailPath("keys", "managers", `${hex}.json`);
39756
- }
39757
- async function loadAnySigner() {
39758
- if (keyExists("permissionSigner")) return loadKeyring("permissionSigner");
39759
- if (keyExists("manager")) return loadKeyring("manager");
39760
- throw new Error('No signing key found.\nRun "sailor keys generate" first.');
39761
- }
39762
-
39763
- // src/lib/project.ts
39764
39689
  function nonEmpty(value) {
39765
39690
  return typeof value === "string" && value.trim().length > 0;
39766
39691
  }
@@ -39819,24 +39744,6 @@ var ProjectContext = class {
39819
39744
  });
39820
39745
  }
39821
39746
  };
39822
- async function loadManagerSigner2() {
39823
- if (!process.env.SAIL_PASSPHRASE) {
39824
- try {
39825
- const env = parseEnvFile(sailPath(".env.local"));
39826
- if (env.SAIL_PASSPHRASE) process.env.SAIL_PASSPHRASE = env.SAIL_PASSPHRASE;
39827
- } catch {
39828
- }
39829
- }
39830
- const passphrase = process.env.SAIL_PASSPHRASE;
39831
- if (passphrase) {
39832
- const keystore = readJsonFile(keyPath("manager"));
39833
- if (!keystore) {
39834
- throw new Error('No manager key found.\nRun "sailor keys generate" and choose "manager".');
39835
- }
39836
- return LocalKeyring.fromKeystore(keystore, passphrase);
39837
- }
39838
- return loadKeyring("manager");
39839
- }
39840
39747
 
39841
39748
  // src/commands/capabilities.ts
39842
39749
  function chainName(chainId) {
@@ -40044,6 +39951,101 @@ function checkSelectorRoutes(calldata, bytecode) {
40044
39951
  return { selector, routes: body.includes(selector) };
40045
39952
  }
40046
39953
 
39954
+ // src/lib/keys.ts
39955
+ init_esm2();
39956
+ var ROLES = ["manager", "permissionSigner"];
39957
+ function normalizeRole(input) {
39958
+ const n = input.trim().toLowerCase().replace(/[-_\s]/g, "");
39959
+ if (n === "manager" || n === "mgr" || n === "m" || n === "agent" || n === "agentwallet") {
39960
+ return "manager";
39961
+ }
39962
+ if (n === "permissionsigner" || n === "signer" || n === "ps" || n === "permission" || n === "mandatesigner" || n === "mandate") {
39963
+ return "permissionSigner";
39964
+ }
39965
+ return null;
39966
+ }
39967
+ function roleLabel(role) {
39968
+ return role === "manager" ? "agent wallet" : "mandate signer";
39969
+ }
39970
+ function safeHex(safe) {
39971
+ return safe.toLowerCase().replace(/^0x/, "").replace(/[^0-9a-f]/g, "");
39972
+ }
39973
+ function keyPath(role, safe) {
39974
+ if (safe) {
39975
+ return sailPath("keys", `${role}-0x${safeHex(safe)}.json`);
39976
+ }
39977
+ return sailPath("keys", `${role}.json`);
39978
+ }
39979
+ function legacyKeyPath(role, safe) {
39980
+ return sailPath("keys", `${role}-${safeHex(safe)}.json`);
39981
+ }
39982
+ function resolveKeyPath(role, safe) {
39983
+ if (safe) {
39984
+ const perSma = keyPath(role, safe);
39985
+ if (fileExists(perSma)) return perSma;
39986
+ const legacy = legacyKeyPath(role, safe);
39987
+ if (fileExists(legacy)) return legacy;
39988
+ }
39989
+ return keyPath(role);
39990
+ }
39991
+ function keyExists(role, safe) {
39992
+ return fileExists(resolveKeyPath(role, safe));
39993
+ }
39994
+ async function loadKeyring(role, safe) {
39995
+ const keystore = readJsonFile(resolveKeyPath(role, safe));
39996
+ if (!keystore) {
39997
+ throw new Error(
39998
+ `No ${roleLabel(role)} found.
39999
+ Run "sailor keys generate" and choose "${roleLabel(role)}" first.`
40000
+ );
40001
+ }
40002
+ const passphrase = process.env.SAIL_PASSPHRASE;
40003
+ if (passphrase) {
40004
+ try {
40005
+ return await LocalKeyring.fromKeystore(keystore, passphrase);
40006
+ } catch {
40007
+ throw new Error(
40008
+ `SAIL_PASSPHRASE does not match the ${roleLabel(role)} keystore.
40009
+ Check the value in .sail/.env.local (or the SAIL_PASSPHRASE CI secret) \u2014 it must be the passphrase this key was encrypted with.`
40010
+ );
40011
+ }
40012
+ }
40013
+ if (process.stdin.isTTY !== true) {
40014
+ throw new Error(
40015
+ `${roleLabel(role)} keystore found but SAIL_PASSPHRASE is not set.
40016
+ Set SAIL_PASSPHRASE in .sail/.env.local (or as a CI secret) to run non-interactively.`
40017
+ );
40018
+ }
40019
+ const password = await promptHidden(`Password for ${roleLabel(role)} key`);
40020
+ try {
40021
+ return await LocalKeyring.fromKeystore(keystore, password);
40022
+ } catch {
40023
+ throw new Error("Invalid password.");
40024
+ }
40025
+ }
40026
+ async function loadManagerSigner(safe) {
40027
+ if (!process.env.SAIL_PASSPHRASE) {
40028
+ try {
40029
+ const env = parseEnvFile(sailPath(".env.local"));
40030
+ if (env.SAIL_PASSPHRASE) process.env.SAIL_PASSPHRASE = env.SAIL_PASSPHRASE;
40031
+ } catch {
40032
+ }
40033
+ }
40034
+ return loadKeyring("manager", safe);
40035
+ }
40036
+ function managerKeystorePath(managerAddr) {
40037
+ if (!isAddress(managerAddr, { strict: false })) {
40038
+ throw new Error(`managerKeystorePath: invalid address "${managerAddr}"`);
40039
+ }
40040
+ const hex = managerAddr.toLowerCase().replace(/^0x/, "");
40041
+ return sailPath("keys", "managers", `${hex}.json`);
40042
+ }
40043
+ async function loadAnySigner() {
40044
+ if (keyExists("permissionSigner")) return loadKeyring("permissionSigner");
40045
+ if (keyExists("manager")) return loadKeyring("manager");
40046
+ throw new Error('No signing key found.\nRun "sailor keys generate" first.');
40047
+ }
40048
+
40047
40049
  // src/lib/permission-resolver.ts
40048
40050
  var IPERMISSION_ABI = [
40049
40051
  {
@@ -40439,11 +40441,11 @@ Probe is heuristic: an unknown selector (${PROBE_SELECTOR}) to a neutral target
40439
40441
  }
40440
40442
 
40441
40443
  // src/commands/init.ts
40442
- var import_node_fs9 = __toESM(require("node:fs"), 1);
40444
+ var import_node_fs10 = __toESM(require("node:fs"), 1);
40443
40445
  var import_node_path8 = __toESM(require("node:path"), 1);
40444
40446
 
40445
40447
  // src/lib/foundry.ts
40446
- var import_node_fs7 = require("node:fs");
40448
+ var import_node_fs8 = require("node:fs");
40447
40449
  var import_node_path6 = require("node:path");
40448
40450
  var FOUNDRY_TOML = `[profile.default]
40449
40451
  src = "mandates"
@@ -40592,7 +40594,7 @@ Compiled artifacts are written to \`out/\` and the deployed address is tracked i
40592
40594
  function scaffoldFoundryWorkspace(root) {
40593
40595
  const dirs = [(0, import_node_path6.join)(root, "mandates"), (0, import_node_path6.join)(root, ".sail", "contracts", "interfaces")];
40594
40596
  for (const d of dirs) {
40595
- if (!(0, import_node_fs7.existsSync)(d)) (0, import_node_fs7.mkdirSync)(d, { recursive: true });
40597
+ if (!(0, import_node_fs8.existsSync)(d)) (0, import_node_fs8.mkdirSync)(d, { recursive: true });
40596
40598
  }
40597
40599
  writeIfMissing((0, import_node_path6.join)(root, "foundry.toml"), FOUNDRY_TOML);
40598
40600
  writeIfMissing(
@@ -40607,11 +40609,11 @@ function scaffoldFoundryWorkspace(root) {
40607
40609
  writeIfMissing((0, import_node_path6.join)(root, "mandates", "README.md"), MANDATES_README);
40608
40610
  }
40609
40611
  function writeIfMissing(path11, content) {
40610
- if (!(0, import_node_fs7.existsSync)(path11)) (0, import_node_fs7.writeFileSync)(path11, content, "utf8");
40612
+ if (!(0, import_node_fs8.existsSync)(path11)) (0, import_node_fs8.writeFileSync)(path11, content, "utf8");
40611
40613
  }
40612
40614
 
40613
40615
  // src/lib/template.ts
40614
- var import_node_fs8 = __toESM(require("node:fs"), 1);
40616
+ var import_node_fs9 = __toESM(require("node:fs"), 1);
40615
40617
  var import_node_path7 = __toESM(require("node:path"), 1);
40616
40618
  var TEMPLATE_COPY_EXCLUDES = /* @__PURE__ */ new Set([
40617
40619
  "node_modules",
@@ -40622,8 +40624,8 @@ var TEMPLATE_COPY_EXCLUDES = /* @__PURE__ */ new Set([
40622
40624
  ".git"
40623
40625
  ]);
40624
40626
  function copyDirSync(src, dest) {
40625
- import_node_fs8.default.mkdirSync(dest, { recursive: true });
40626
- for (const entry of import_node_fs8.default.readdirSync(src, { withFileTypes: true })) {
40627
+ import_node_fs9.default.mkdirSync(dest, { recursive: true });
40628
+ for (const entry of import_node_fs9.default.readdirSync(src, { withFileTypes: true })) {
40627
40629
  if (TEMPLATE_COPY_EXCLUDES.has(entry.name)) continue;
40628
40630
  const srcPath = import_node_path7.default.join(src, entry.name);
40629
40631
  const destName = entry.name.startsWith("_") ? `.${entry.name.slice(1)}` : entry.name;
@@ -40631,24 +40633,24 @@ function copyDirSync(src, dest) {
40631
40633
  if (entry.isDirectory()) {
40632
40634
  copyDirSync(srcPath, destPath);
40633
40635
  } else {
40634
- import_node_fs8.default.copyFileSync(srcPath, destPath);
40636
+ import_node_fs9.default.copyFileSync(srcPath, destPath);
40635
40637
  }
40636
40638
  }
40637
40639
  }
40638
40640
  function writeIfMissing2(file, content) {
40639
- if (!import_node_fs8.default.existsSync(file)) import_node_fs8.default.writeFileSync(file, content, "utf-8");
40641
+ if (!import_node_fs9.default.existsSync(file)) import_node_fs9.default.writeFileSync(file, content, "utf-8");
40640
40642
  }
40641
40643
  function copyDirSyncIfMissing(src, dest, added = [], base2 = dest) {
40642
- import_node_fs8.default.mkdirSync(dest, { recursive: true });
40643
- for (const entry of import_node_fs8.default.readdirSync(src, { withFileTypes: true })) {
40644
+ import_node_fs9.default.mkdirSync(dest, { recursive: true });
40645
+ for (const entry of import_node_fs9.default.readdirSync(src, { withFileTypes: true })) {
40644
40646
  if (TEMPLATE_COPY_EXCLUDES.has(entry.name)) continue;
40645
40647
  const srcPath = import_node_path7.default.join(src, entry.name);
40646
40648
  const destName = entry.name.startsWith("_") ? `.${entry.name.slice(1)}` : entry.name;
40647
40649
  const destPath = import_node_path7.default.join(dest, destName);
40648
40650
  if (entry.isDirectory()) {
40649
40651
  copyDirSyncIfMissing(srcPath, destPath, added, base2);
40650
- } else if (!import_node_fs8.default.existsSync(destPath)) {
40651
- import_node_fs8.default.copyFileSync(srcPath, destPath);
40652
+ } else if (!import_node_fs9.default.existsSync(destPath)) {
40653
+ import_node_fs9.default.copyFileSync(srcPath, destPath);
40652
40654
  added.push(import_node_path7.default.relative(base2, destPath));
40653
40655
  }
40654
40656
  }
@@ -40674,7 +40676,7 @@ var DEV_PKG = "@dev.sail.money/sailor";
40674
40676
  function cliPackageInfo() {
40675
40677
  try {
40676
40678
  const pkg = JSON.parse(
40677
- import_node_fs9.default.readFileSync(import_node_path8.default.join(packageRoot(), "package.json"), "utf-8")
40679
+ import_node_fs10.default.readFileSync(import_node_path8.default.join(packageRoot(), "package.json"), "utf-8")
40678
40680
  );
40679
40681
  return {
40680
40682
  name: pkg.name ?? CANONICAL_PKG,
@@ -40691,13 +40693,13 @@ function scaffoldProjectWorkspace(dest, name, options) {
40691
40693
  return n;
40692
40694
  })() : null;
40693
40695
  const sailDir2 = import_node_path8.default.join(dest, ".sail");
40694
- import_node_fs9.default.mkdirSync(import_node_path8.default.join(sailDir2, "keys"), { recursive: true });
40695
- import_node_fs9.default.mkdirSync(import_node_path8.default.join(sailDir2, "runtime"), { recursive: true });
40696
- import_node_fs9.default.mkdirSync(import_node_path8.default.join(sailDir2, "state"), { recursive: true });
40696
+ import_node_fs10.default.mkdirSync(import_node_path8.default.join(sailDir2, "keys"), { recursive: true });
40697
+ import_node_fs10.default.mkdirSync(import_node_path8.default.join(sailDir2, "runtime"), { recursive: true });
40698
+ import_node_fs10.default.mkdirSync(import_node_path8.default.join(sailDir2, "state"), { recursive: true });
40697
40699
  const installMode = process.env.SAILOR_INSTALL_MODE === "docker" ? "docker" : "local";
40698
40700
  const _rawContainerName = process.env.SAILOR_CONTAINER_NAME ?? "agent";
40699
40701
  const containerName = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(_rawContainerName) ? _rawContainerName : "agent";
40700
- import_node_fs9.default.writeFileSync(
40702
+ import_node_fs10.default.writeFileSync(
40701
40703
  import_node_path8.default.join(sailDir2, "config.json"),
40702
40704
  `${JSON.stringify(
40703
40705
  {
@@ -40724,7 +40726,7 @@ function scaffoldProjectWorkspace(dest, name, options) {
40724
40726
  const chainEntries = Object.values(chains);
40725
40727
  const perChainVarLines = chainEntries.map((c) => `# ${c.rpcEnvVar}=https://your-${c.name.toLowerCase().replace(/\s+/g, "-")}-endpoint`).join("\n");
40726
40728
  const chainIdExample = chainId != null ? `CHAIN_ID=${chainId}` : `# CHAIN_ID=8453 # set after choosing your chain in Stage 1`;
40727
- import_node_fs9.default.writeFileSync(
40729
+ import_node_fs10.default.writeFileSync(
40728
40730
  import_node_path8.default.join(dest, ".env.example"),
40729
40731
  `# Sailor agent environment
40730
40732
  #
@@ -40750,7 +40752,7 @@ ${perChainVarLines}
40750
40752
  const val = isActive && options.rpcUrl ? options.rpcUrl : `https://your-${c.name.toLowerCase().replace(/\s+/g, "-")}-endpoint`;
40751
40753
  return isActive && options.rpcUrl ? `${c.rpcEnvVar}=${val}` : `# ${c.rpcEnvVar}=${val}`;
40752
40754
  }).join("\n");
40753
- import_node_fs9.default.writeFileSync(
40755
+ import_node_fs10.default.writeFileSync(
40754
40756
  import_node_path8.default.join(sailDir2, ".env.local"),
40755
40757
  `# Real values \u2014 never commit this file.
40756
40758
  #
@@ -40777,8 +40779,8 @@ async function initCommand(dir, options = {}) {
40777
40779
  throw new Error(`Invalid template name: "${templateName}"`);
40778
40780
  }
40779
40781
  const templateSrc = import_node_path8.default.join(templatesDir, templateName);
40780
- const availableTemplates = () => import_node_fs9.default.existsSync(templatesDir) ? import_node_fs9.default.readdirSync(templatesDir).filter((e) => import_node_fs9.default.existsSync(import_node_path8.default.join(templatesDir, e, "package.json"))).join(", ") || "none" : "none";
40781
- if (!import_node_fs9.default.existsSync(templateSrc) || !import_node_fs9.default.existsSync(import_node_path8.default.join(templateSrc, "package.json"))) {
40782
+ const availableTemplates = () => import_node_fs10.default.existsSync(templatesDir) ? import_node_fs10.default.readdirSync(templatesDir).filter((e) => import_node_fs10.default.existsSync(import_node_path8.default.join(templatesDir, e, "package.json"))).join(", ") || "none" : "none";
40783
+ if (!import_node_fs10.default.existsSync(templateSrc) || !import_node_fs10.default.existsSync(import_node_path8.default.join(templateSrc, "package.json"))) {
40782
40784
  const available = availableTemplates();
40783
40785
  const hint = available === "none" ? `
40784
40786
  No templates found under ${templatesDir}.
@@ -40791,19 +40793,19 @@ run from the repo root.` : ` Available: ${available}`;
40791
40793
  if (!inPlace && !dest.startsWith(cwd + import_node_path8.default.sep) && dest !== cwd) {
40792
40794
  throw new Error(`Directory must be inside the current working directory`);
40793
40795
  }
40794
- if (!inPlace && import_node_fs9.default.existsSync(dest) && !options.force) {
40796
+ if (!inPlace && import_node_fs10.default.existsSync(dest) && !options.force) {
40795
40797
  throw new Error(`Directory already exists: ${dest}
40796
40798
  Pass --force to scaffold into it anyway (existing files with the same name are overwritten).`);
40797
40799
  }
40798
- if (inPlace && import_node_fs9.default.existsSync(import_node_path8.default.join(dest, ".sail", "config.json")) && !options.force) {
40800
+ if (inPlace && import_node_fs10.default.existsSync(import_node_path8.default.join(dest, ".sail", "config.json")) && !options.force) {
40799
40801
  throw new Error(
40800
40802
  "This project is already initialized.\nRun `sailor update` to re-sync template files, or `sailor init --force` to re-initialize (overwrites scaffold files; your .sail/keys/ and .sail/state/ are left in place)."
40801
40803
  );
40802
40804
  }
40803
40805
  const existingConfigPath = import_node_path8.default.join(dest, ".sail", "config.json");
40804
- const previousConfig = import_node_fs9.default.existsSync(existingConfigPath) ? (() => {
40806
+ const previousConfig = import_node_fs10.default.existsSync(existingConfigPath) ? (() => {
40805
40807
  try {
40806
- return JSON.parse(import_node_fs9.default.readFileSync(existingConfigPath, "utf-8"));
40808
+ return JSON.parse(import_node_fs10.default.readFileSync(existingConfigPath, "utf-8"));
40807
40809
  } catch {
40808
40810
  return null;
40809
40811
  }
@@ -40811,37 +40813,37 @@ Pass --force to scaffold into it anyway (existing files with the same name are o
40811
40813
  copyDirSync(templateSrc, dest);
40812
40814
  const pkgRoot = packageRoot();
40813
40815
  const examplesPermSrc = import_node_path8.default.join(pkgRoot, "examples", "permissions");
40814
- if (import_node_fs9.default.existsSync(examplesPermSrc)) {
40816
+ if (import_node_fs10.default.existsSync(examplesPermSrc)) {
40815
40817
  copyDirSync(examplesPermSrc, import_node_path8.default.join(dest, "examples", "permissions"));
40816
40818
  }
40817
40819
  const customMandateSrc = import_node_path8.default.join(pkgRoot, "examples", "custom-mandate");
40818
- if (import_node_fs9.default.existsSync(customMandateSrc)) {
40820
+ if (import_node_fs10.default.existsSync(customMandateSrc)) {
40819
40821
  copyDirSync(customMandateSrc, import_node_path8.default.join(dest, "examples", "custom-mandate"));
40820
40822
  }
40821
40823
  const permModelSrc = import_node_path8.default.join(pkgRoot, "docs", "PERMISSION_MODEL.md");
40822
- if (import_node_fs9.default.existsSync(permModelSrc)) {
40823
- import_node_fs9.default.mkdirSync(import_node_path8.default.join(dest, "docs"), { recursive: true });
40824
- writeIfMissing2(import_node_path8.default.join(dest, "docs", "PERMISSION_MODEL.md"), import_node_fs9.default.readFileSync(permModelSrc, "utf-8"));
40824
+ if (import_node_fs10.default.existsSync(permModelSrc)) {
40825
+ import_node_fs10.default.mkdirSync(import_node_path8.default.join(dest, "docs"), { recursive: true });
40826
+ writeIfMissing2(import_node_path8.default.join(dest, "docs", "PERMISSION_MODEL.md"), import_node_fs10.default.readFileSync(permModelSrc, "utf-8"));
40825
40827
  }
40826
40828
  const pkgPath = import_node_path8.default.join(dest, "package.json");
40827
- if (import_node_fs9.default.existsSync(pkgPath)) {
40828
- const pkg = JSON.parse(import_node_fs9.default.readFileSync(pkgPath, "utf-8"));
40829
+ if (import_node_fs10.default.existsSync(pkgPath)) {
40830
+ const pkg = JSON.parse(import_node_fs10.default.readFileSync(pkgPath, "utf-8"));
40829
40831
  pkg.name = name;
40830
40832
  const devDeps = pkg.devDependencies ?? {};
40831
40833
  const { name: cliName, version: cliVer } = cliPackageInfo();
40832
40834
  devDeps[CANONICAL_PKG] = cliName === DEV_PKG ? `npm:${DEV_PKG}@^${cliVer}` : `^${cliVer}`;
40833
40835
  pkg.devDependencies = devDeps;
40834
- import_node_fs9.default.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
40836
+ import_node_fs10.default.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
40835
40837
  `);
40836
40838
  }
40837
40839
  const tsconfigPath = import_node_path8.default.join(dest, "tsconfig.json");
40838
- if (import_node_fs9.default.existsSync(tsconfigPath)) {
40839
- const tsconfig = JSON.parse(import_node_fs9.default.readFileSync(tsconfigPath, "utf-8"));
40840
+ if (import_node_fs10.default.existsSync(tsconfigPath)) {
40841
+ const tsconfig = JSON.parse(import_node_fs10.default.readFileSync(tsconfigPath, "utf-8"));
40840
40842
  const co = tsconfig.compilerOptions;
40841
40843
  if (co && "paths" in co) {
40842
40844
  delete co.paths;
40843
40845
  delete co.baseUrl;
40844
- import_node_fs9.default.writeFileSync(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}
40846
+ import_node_fs10.default.writeFileSync(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}
40845
40847
  `);
40846
40848
  }
40847
40849
  }
@@ -40879,20 +40881,20 @@ function chainLabel(chainId) {
40879
40881
  }
40880
40882
  function detectState(dest) {
40881
40883
  try {
40882
- const configRaw = import_node_fs9.default.readFileSync(import_node_path8.default.join(dest, ".sail", "config.json"), "utf-8");
40884
+ const configRaw = import_node_fs10.default.readFileSync(import_node_path8.default.join(dest, ".sail", "config.json"), "utf-8");
40883
40885
  const config = JSON.parse(configRaw);
40884
40886
  const projectName = config.name ?? import_node_path8.default.basename(dest);
40885
40887
  const accountPath = import_node_path8.default.join(dest, ".sail", "account.json");
40886
- if (!import_node_fs9.default.existsSync(accountPath)) {
40888
+ if (!import_node_fs10.default.existsSync(accountPath)) {
40887
40889
  return { kind: "B", projectName, chain: chainLabel(config.chainId ?? 0) };
40888
40890
  }
40889
- const accountRaw = import_node_fs9.default.readFileSync(accountPath, "utf-8");
40891
+ const accountRaw = import_node_fs10.default.readFileSync(accountPath, "utf-8");
40890
40892
  const account2 = JSON.parse(accountRaw);
40891
40893
  const sma = account2.safe ?? "";
40892
40894
  const chain2 = chainLabel(account2.chainId ?? config.chainId ?? 0);
40893
40895
  let permissionCount = 0;
40894
40896
  try {
40895
- const mandatesRaw = import_node_fs9.default.readFileSync(
40897
+ const mandatesRaw = import_node_fs10.default.readFileSync(
40896
40898
  import_node_path8.default.join(dest, ".sail", "state", "mandates.json"),
40897
40899
  "utf-8"
40898
40900
  );
@@ -40981,7 +40983,7 @@ Created ${name}/`);
40981
40983
  }
40982
40984
 
40983
40985
  // src/commands/update.ts
40984
- var import_node_fs10 = __toESM(require("node:fs"), 1);
40986
+ var import_node_fs11 = __toESM(require("node:fs"), 1);
40985
40987
  var import_node_path9 = __toESM(require("node:path"), 1);
40986
40988
  var UPDATE_PATHS = [
40987
40989
  ".agents",
@@ -40997,18 +40999,18 @@ var STALE_PATHS = [
40997
40999
  ];
40998
41000
  async function updateCommand() {
40999
41001
  const dest = process.cwd();
41000
- if (!import_node_fs10.default.existsSync(import_node_path9.default.join(dest, ".sail", "config.json"))) {
41002
+ if (!import_node_fs11.default.existsSync(import_node_path9.default.join(dest, ".sail", "config.json"))) {
41001
41003
  throw new Error("Not a sailor project \u2014 .sail/config.json not found. Run `sailor init` first.");
41002
41004
  }
41003
41005
  const templateSrc = import_node_path9.default.join(packageRoot(), "templates", "default");
41004
- if (!import_node_fs10.default.existsSync(templateSrc)) {
41006
+ if (!import_node_fs11.default.existsSync(templateSrc)) {
41005
41007
  throw new Error(`Template directory not found at ${templateSrc}`);
41006
41008
  }
41007
41009
  const removed = [];
41008
41010
  for (const p of STALE_PATHS) {
41009
41011
  const target = import_node_path9.default.join(dest, p);
41010
- if (import_node_fs10.default.existsSync(target)) {
41011
- import_node_fs10.default.rmSync(target, { recursive: true, force: true });
41012
+ if (import_node_fs11.default.existsSync(target)) {
41013
+ import_node_fs11.default.rmSync(target, { recursive: true, force: true });
41012
41014
  removed.push(p);
41013
41015
  }
41014
41016
  }
@@ -41016,13 +41018,13 @@ async function updateCommand() {
41016
41018
  for (const p of UPDATE_PATHS) {
41017
41019
  const src = import_node_path9.default.join(templateSrc, p);
41018
41020
  const dst = import_node_path9.default.join(dest, p);
41019
- if (!import_node_fs10.default.existsSync(src)) continue;
41020
- const stat = import_node_fs10.default.statSync(src);
41021
+ if (!import_node_fs11.default.existsSync(src)) continue;
41022
+ const stat = import_node_fs11.default.statSync(src);
41021
41023
  if (stat.isDirectory()) {
41022
41024
  copyDirSync(src, dst);
41023
41025
  } else {
41024
- import_node_fs10.default.mkdirSync(import_node_path9.default.dirname(dst), { recursive: true });
41025
- import_node_fs10.default.copyFileSync(src, dst);
41026
+ import_node_fs11.default.mkdirSync(import_node_path9.default.dirname(dst), { recursive: true });
41027
+ import_node_fs11.default.copyFileSync(src, dst);
41026
41028
  }
41027
41029
  updated.push(p);
41028
41030
  }
@@ -41030,7 +41032,7 @@ async function updateCommand() {
41030
41032
  copyDirSyncIfMissing(templateSrc, dest, added);
41031
41033
  const configPath = import_node_path9.default.join(dest, ".sail", "config.json");
41032
41034
  try {
41033
- const config = JSON.parse(import_node_fs10.default.readFileSync(configPath, "utf-8"));
41035
+ const config = JSON.parse(import_node_fs11.default.readFileSync(configPath, "utf-8"));
41034
41036
  const newMode = process.env.SAILOR_INSTALL_MODE === "docker" ? "docker" : "local";
41035
41037
  const _rawContainerName = process.env.SAILOR_CONTAINER_NAME ?? "agent";
41036
41038
  const containerName = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(_rawContainerName) ? _rawContainerName : "agent";
@@ -41043,7 +41045,7 @@ async function updateCommand() {
41043
41045
  } else {
41044
41046
  delete config.containerName;
41045
41047
  }
41046
- import_node_fs10.default.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
41048
+ import_node_fs11.default.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
41047
41049
  `, "utf-8");
41048
41050
  if (previousMode === "docker" && newMode === "local") {
41049
41051
  const prev = previousContainer ?? "agent";
@@ -41057,7 +41059,7 @@ Switched to Docker install (container: ${containerName}).`);
41057
41059
  }
41058
41060
  } else if (newMode === "docker" && config.containerName !== containerName) {
41059
41061
  config.containerName = containerName;
41060
- import_node_fs10.default.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
41062
+ import_node_fs11.default.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
41061
41063
  `, "utf-8");
41062
41064
  }
41063
41065
  } catch {
@@ -41086,7 +41088,7 @@ Added (new in template):`);
41086
41088
  }
41087
41089
 
41088
41090
  // src/commands/keys.ts
41089
- var import_node_fs11 = __toESM(require("node:fs"), 1);
41091
+ var import_node_fs12 = __toESM(require("node:fs"), 1);
41090
41092
  var import_node_path10 = __toESM(require("node:path"), 1);
41091
41093
  function readStdin() {
41092
41094
  return new Promise((resolve3, reject) => {
@@ -41193,14 +41195,14 @@ async function keysExportCi() {
41193
41195
  );
41194
41196
  }
41195
41197
  const dest = import_node_path10.default.resolve(process.cwd(), "ci-keystore.json");
41196
- import_node_fs11.default.copyFileSync(src, dest);
41198
+ import_node_fs12.default.copyFileSync(src, dest);
41197
41199
  console.log(`\u2713 Keystore copied to ci-keystore.json`);
41198
41200
  console.log(` Source: ${src}`);
41199
41201
  const gitignorePath = import_node_path10.default.resolve(process.cwd(), ".gitignore");
41200
- if (import_node_fs11.default.existsSync(gitignorePath)) {
41201
- const content = import_node_fs11.default.readFileSync(gitignorePath, "utf-8");
41202
+ if (import_node_fs12.default.existsSync(gitignorePath)) {
41203
+ const content = import_node_fs12.default.readFileSync(gitignorePath, "utf-8");
41202
41204
  if (!content.includes("ci-keystore.json")) {
41203
- import_node_fs11.default.appendFileSync(
41205
+ import_node_fs12.default.appendFileSync(
41204
41206
  gitignorePath,
41205
41207
  "\n# CI keystore \u2014 encrypted agent wallet, safe to commit\n!ci-keystore.json\n"
41206
41208
  );
@@ -41238,10 +41240,125 @@ async function keysShow() {
41238
41240
 
41239
41241
  // src/commands/mandate-contracts.ts
41240
41242
  var import_node_child_process = require("node:child_process");
41241
- var import_node_fs12 = require("node:fs");
41242
- var import_node_path11 = require("node:path");
41243
+ var import_node_fs14 = require("node:fs");
41244
+ var import_node_path12 = require("node:path");
41243
41245
  init_esm2();
41244
41246
 
41247
+ // src/lib/permission-explainer.ts
41248
+ var import_node_fs13 = require("node:fs");
41249
+ var import_node_path11 = require("node:path");
41250
+ function explainPermission(name, sourcePath) {
41251
+ const resolved = sourcePath ?? (0, import_node_path11.join)(process.cwd(), "mandates", `${name}.sol`);
41252
+ let src;
41253
+ try {
41254
+ src = (0, import_node_fs13.readFileSync)(resolved, "utf8");
41255
+ } catch {
41256
+ return null;
41257
+ }
41258
+ return parseStructured(src) ?? parseNatSpec(src) ?? parseRequires(src);
41259
+ }
41260
+ var ENFORCED_HEADER = /ENFORCE[SD]\s+ON-CHAIN/i;
41261
+ var NOT_ENFORCED_HEADER = /NOT\s+(ENFORCED|BOUNDED)/i;
41262
+ function parseStructured(src) {
41263
+ if (!ENFORCED_HEADER.test(src)) return null;
41264
+ const lines = src.split("\n");
41265
+ const result = {
41266
+ source: "structured",
41267
+ enforced: [],
41268
+ notEnforced: []
41269
+ };
41270
+ const headerLines = [];
41271
+ for (const line of lines) {
41272
+ const t = line.trim();
41273
+ if (t.startsWith("import ") || t.startsWith("contract ") || t.startsWith("abstract contract ")) break;
41274
+ if (t.startsWith("//")) headerLines.push(t.slice(2));
41275
+ }
41276
+ let section = null;
41277
+ for (const raw of headerLines) {
41278
+ const trimmed = raw.trim();
41279
+ if (/^─+$/.test(trimmed)) continue;
41280
+ if (NOT_ENFORCED_HEADER.test(trimmed)) {
41281
+ section = "notEnforced";
41282
+ continue;
41283
+ }
41284
+ if (ENFORCED_HEADER.test(trimmed)) {
41285
+ section = "enforced";
41286
+ continue;
41287
+ }
41288
+ if (trimmed.includes("VERIFY BEFORE USE")) {
41289
+ section = null;
41290
+ continue;
41291
+ }
41292
+ if (section === null) {
41293
+ const kv = trimmed.match(/^(Protocol|Version|Chain|Target)\s*:\s*(.+)$/i);
41294
+ if (kv) {
41295
+ const key = kv[1].toLowerCase();
41296
+ result[key] = kv[2].trim();
41297
+ }
41298
+ }
41299
+ if (section && trimmed.startsWith("\u2022")) {
41300
+ const bullet = trimmed.slice(1).trim();
41301
+ if (bullet) result[section].push(bullet);
41302
+ }
41303
+ }
41304
+ if (result.enforced.length === 0 && !result.protocol) return null;
41305
+ return result;
41306
+ }
41307
+ function parseNatSpec(src) {
41308
+ const enforced = [];
41309
+ const notEnforced = [];
41310
+ let current = null;
41311
+ let buf = [];
41312
+ const flush = () => {
41313
+ if (current && buf.length) {
41314
+ const text = buf.join(" ").replace(/\s+/g, " ").trim();
41315
+ if (text) (current === "enforced" ? enforced : notEnforced).push(text);
41316
+ }
41317
+ buf = [];
41318
+ };
41319
+ for (const line of src.split("\n")) {
41320
+ const t = line.trim();
41321
+ const tagged = t.match(/^\/\/\/\s*@(\w+)\s+(.+)$/);
41322
+ if (tagged) {
41323
+ const [, tag, text] = tagged;
41324
+ if (tag === "notice") {
41325
+ flush();
41326
+ current = "enforced";
41327
+ buf.push(text.trim());
41328
+ } else if (tag === "dev") {
41329
+ flush();
41330
+ current = "notEnforced";
41331
+ buf.push(text.trim());
41332
+ } else {
41333
+ flush();
41334
+ current = null;
41335
+ }
41336
+ continue;
41337
+ }
41338
+ const cont = t.match(/^\/\/\/\s?(.*)$/);
41339
+ if (cont && current) {
41340
+ const text = cont[1].trim();
41341
+ if (text) buf.push(text);
41342
+ continue;
41343
+ }
41344
+ if (!t.startsWith("///")) {
41345
+ flush();
41346
+ current = null;
41347
+ }
41348
+ }
41349
+ flush();
41350
+ if (enforced.length === 0 && notEnforced.length === 0) return null;
41351
+ return { source: "natspec", enforced, notEnforced };
41352
+ }
41353
+ function parseRequires(src) {
41354
+ const enforced = [];
41355
+ const re = /require\s*\([^,)]+,\s*["']([^"']+)["']\)/g;
41356
+ let m;
41357
+ while ((m = re.exec(src)) !== null) enforced.push(m[1]);
41358
+ if (enforced.length === 0) return null;
41359
+ return { source: "require-only", enforced, notEnforced: [] };
41360
+ }
41361
+
41245
41362
  // src/lib/mandates.ts
41246
41363
  var MandateStore = class {
41247
41364
  filePath;
@@ -41370,7 +41487,7 @@ Project: ${project.name}`));
41370
41487
  if (!keyExists("manager")) {
41371
41488
  throw new Error('No agent wallet found. Run "sailor keys generate" and choose "agent wallet" first.');
41372
41489
  }
41373
- const agentSigner = await loadManagerSigner2();
41490
+ const agentSigner = await loadManagerSigner();
41374
41491
  const agentAddress = agentSigner.address;
41375
41492
  say(() => console.log("\u2713", `Agent wallet: ${agentAddress}`));
41376
41493
  const chain2 = getChainById(project.chainId);
@@ -41383,7 +41500,7 @@ Project: ${project.name}`));
41383
41500
  () => console.log(
41384
41501
  `
41385
41502
  \u2192 Open the Sailor dashboard to approve signing requests:
41386
- ${signingPageUrl(channel, projectPort(process.cwd()))}
41503
+ ${signingPageUrl(projectPort(process.cwd()))}
41387
41504
  `
41388
41505
  )
41389
41506
  );
@@ -41706,6 +41823,8 @@ Pushing signing request for "${template.label}" permission\u2026`));
41706
41823
  ` The mandate signer (${permissionSigner}) must sign in the browser \u2014 not the agent wallet.`
41707
41824
  )
41708
41825
  );
41826
+ const permRecord = new MandateStore().find(templateAddress);
41827
+ const permExplanation = (permRecord ? explainPermission(permRecord.name, permRecord.sourcePath) : explainPermission(template.label)) ?? void 0;
41709
41828
  const response = await channel.requestSignature({
41710
41829
  type: "typed-data",
41711
41830
  kind: "register-permission",
@@ -41718,6 +41837,7 @@ Pushing signing request for "${template.label}" permission\u2026`));
41718
41837
  { label: "Permission", value: template.label },
41719
41838
  { label: "Mandate signer", value: permissionSigner }
41720
41839
  ],
41840
+ explanation: permExplanation,
41721
41841
  typedData
41722
41842
  });
41723
41843
  if (response.status === "rejected") {
@@ -41884,7 +42004,7 @@ Mandate command failed: ${msg}`), {
41884
42004
  process.exit(1);
41885
42005
  }
41886
42006
  function announceSigningUrl(json) {
41887
- const url = signingPageUrl(void 0, projectPort(process.cwd()));
42007
+ const url = signingPageUrl(projectPort(process.cwd()));
41888
42008
  if (json) {
41889
42009
  process.stdout.write(`${JSON.stringify({ status: "waiting_for_signature", url })}
41890
42010
  `);
@@ -41932,12 +42052,12 @@ async function runDeploy(project, channel, options) {
41932
42052
  )
41933
42053
  );
41934
42054
  }
41935
- const { abi: abi2, bytecode, contractName, artifactPath } = resolveArtifact(options);
42055
+ const { abi: abi2, bytecode, contractName, artifactPath, sourcePath } = resolveArtifact(options);
41936
42056
  let argsJson;
41937
42057
  if (options.argsFile) {
41938
- const argsFilePath = (0, import_node_path11.resolve)(options.argsFile);
42058
+ const argsFilePath = (0, import_node_path12.resolve)(options.argsFile);
41939
42059
  try {
41940
- argsJson = (0, import_node_fs12.readFileSync)(argsFilePath, "utf8").trim();
42060
+ argsJson = (0, import_node_fs14.readFileSync)(argsFilePath, "utf8").trim();
41941
42061
  } catch {
41942
42062
  throw new Error(`Cannot read --args-file: ${argsFilePath}`);
41943
42063
  }
@@ -41950,6 +42070,7 @@ async function runDeploy(project, channel, options) {
41950
42070
  const publicClient = publicClientFor(project);
41951
42071
  announceSigningUrl(json);
41952
42072
  say(() => console.log(`Pushing deploy request for "${contractName}"\u2026`));
42073
+ const explanation = explainPermission(contractName, sourcePath) ?? void 0;
41953
42074
  const response = await channel.requestSignature({
41954
42075
  type: "transaction",
41955
42076
  kind: "deploy-mandate",
@@ -41964,7 +42085,8 @@ async function runDeploy(project, channel, options) {
41964
42085
  label: options.argsFile ? "Constructor args (from file)" : "Constructor args",
41965
42086
  value: argsJson ? argsJson : "(none)"
41966
42087
  }
41967
- ]
42088
+ ],
42089
+ explanation
41968
42090
  });
41969
42091
  if (response.status === "rejected") {
41970
42092
  throw new Error(`User rejected permission deployment: ${response.reason ?? "no reason given"}`);
@@ -41984,6 +42106,7 @@ async function runDeploy(project, channel, options) {
41984
42106
  address: deployed,
41985
42107
  txHash: response.txHash,
41986
42108
  chainId,
42109
+ sourcePath,
41987
42110
  artifactPath,
41988
42111
  constructorArgs: parseArgsRaw(options.args),
41989
42112
  deployedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -42145,7 +42268,7 @@ async function runDeployClone(project, channel, options) {
42145
42268
  }
42146
42269
  const chain2 = getChainById(project.chainId);
42147
42270
  const publicClient = publicClientFor(project);
42148
- const agentSigner = await loadManagerSigner2();
42271
+ const agentSigner = await loadManagerSigner();
42149
42272
  const registered = await publicClient.readContract({
42150
42273
  address: project.contracts.kernel,
42151
42274
  abi: SailKernelAbi,
@@ -42489,7 +42612,7 @@ Connect the owner wallet (mandate signer) in the browser \u2014 the agent wallet
42489
42612
  } catch (err) {
42490
42613
  if (err.message.startsWith("Security:")) throw err;
42491
42614
  }
42492
- const agentSigner = await loadManagerSigner2();
42615
+ const agentSigner = await loadManagerSigner();
42493
42616
  const walletClient = createWalletClient({
42494
42617
  account: agentSigner.viemAccount,
42495
42618
  chain: getChainById(project.chainId),
@@ -42531,7 +42654,7 @@ async function attachToSma(project, channel, publicClient, sma, mandate2, label,
42531
42654
  const say = (fn) => {
42532
42655
  if (!json) fn();
42533
42656
  };
42534
- const agentSigner = await loadManagerSigner2();
42657
+ const agentSigner = await loadManagerSigner();
42535
42658
  const registered = await publicClient.readContract({
42536
42659
  address: project.contracts.kernel,
42537
42660
  abi: SailKernelAbi,
@@ -42572,7 +42695,7 @@ async function attachBatchToSma(project, channel, publicClient, sma, permissions
42572
42695
  const say = (fn) => {
42573
42696
  if (!json) fn();
42574
42697
  };
42575
- const agentSigner = await loadManagerSigner2();
42698
+ const agentSigner = await loadManagerSigner();
42576
42699
  const registered = await publicClient.readContract({
42577
42700
  address: project.contracts.kernel,
42578
42701
  abi: SailKernelAbi,
@@ -42618,6 +42741,15 @@ async function attachBatchToSma(project, channel, publicClient, sma, permissions
42618
42741
  Attaching ${permissions.length} permissions in one signature \u2014 the mandate signer (${permissionSigner}) signs in the browser\u2026`
42619
42742
  )
42620
42743
  );
42744
+ const permStore = new MandateStore();
42745
+ const permExplanations = permissions.map((addr) => {
42746
+ const rec = permStore.find(addr);
42747
+ return {
42748
+ label: rec?.name ?? addr,
42749
+ address: addr,
42750
+ explanation: (rec ? explainPermission(rec.name, rec.sourcePath) : null) ?? void 0
42751
+ };
42752
+ });
42621
42753
  const response = await channel.requestSignature({
42622
42754
  type: "typed-data",
42623
42755
  kind: "register-permission",
@@ -42629,6 +42761,7 @@ Attaching ${permissions.length} permissions in one signature \u2014 the mandate
42629
42761
  { label: "Permissions", value: String(permissions.length) },
42630
42762
  { label: "Mandate signer", value: permissionSigner }
42631
42763
  ],
42764
+ permissions: permExplanations,
42632
42765
  typedData
42633
42766
  });
42634
42767
  if (response.status === "rejected") {
@@ -42776,11 +42909,11 @@ function resolveArtifact(options) {
42776
42909
  let contractName = options.contract ?? options.name ?? "";
42777
42910
  if (!artifactPath) {
42778
42911
  if (!options.contract) throw new Error("Provide --artifact <path> or --contract <name>");
42779
- artifactPath = (0, import_node_path11.join)(options.out, `${options.contract}.sol`, `${options.contract}.json`);
42912
+ artifactPath = (0, import_node_path12.join)(options.out, `${options.contract}.sol`, `${options.contract}.json`);
42780
42913
  }
42781
- const resolved = (0, import_node_path11.resolve)(artifactPath);
42782
- const projectRoot = (0, import_node_path11.resolve)(process.cwd());
42783
- if (!resolved.startsWith(projectRoot + import_node_path11.sep) && resolved !== projectRoot) {
42914
+ const resolved = (0, import_node_path12.resolve)(artifactPath);
42915
+ const projectRoot = (0, import_node_path12.resolve)(process.cwd());
42916
+ if (!resolved.startsWith(projectRoot + import_node_path12.sep) && resolved !== projectRoot) {
42784
42917
  throw new Error(
42785
42918
  `Artifact path must be inside the project directory.
42786
42919
  Resolved: ${resolved}`
@@ -42788,14 +42921,14 @@ Resolved: ${resolved}`
42788
42921
  }
42789
42922
  artifactPath = resolved;
42790
42923
  if (options.build) runForgeBuild();
42791
- if (!(0, import_node_fs12.existsSync)(artifactPath)) {
42924
+ if (!(0, import_node_fs14.existsSync)(artifactPath)) {
42792
42925
  ensureForgeHint();
42793
42926
  throw new Error(
42794
42927
  `Artifact not found: ${artifactPath}
42795
42928
  Compile your mandate first (e.g. \`forge build\`), or pass --build.`
42796
42929
  );
42797
42930
  }
42798
- const artifact = JSON.parse((0, import_node_fs12.readFileSync)(artifactPath, "utf8"));
42931
+ const artifact = JSON.parse((0, import_node_fs14.readFileSync)(artifactPath, "utf8"));
42799
42932
  const abi2 = artifact.abi;
42800
42933
  const bytecodeRaw = artifact.bytecode?.object ?? artifact.bytecode;
42801
42934
  if (!bytecodeRaw || typeof bytecodeRaw !== "string") {
@@ -42806,7 +42939,16 @@ Compile your mandate first (e.g. \`forge build\`), or pass --build.`
42806
42939
  const m = artifactPath.match(/([^/\\]+)\.json$/);
42807
42940
  contractName = m ? m[1] : "Mandate";
42808
42941
  }
42809
- return { abi: abi2, bytecode, contractName, artifactPath };
42942
+ let sourcePath;
42943
+ const compilationTarget = artifact.metadata?.settings?.compilationTarget;
42944
+ if (compilationTarget && typeof compilationTarget === "object") {
42945
+ sourcePath = Object.keys(compilationTarget)[0];
42946
+ }
42947
+ sourcePath = sourcePath ?? artifact.ast?.absolutePath;
42948
+ if (sourcePath && !sourcePath.startsWith("/")) {
42949
+ sourcePath = (0, import_node_path12.resolve)(projectRoot, sourcePath);
42950
+ }
42951
+ return { abi: abi2, bytecode, contractName, artifactPath, sourcePath };
42810
42952
  }
42811
42953
  function parseArgsRaw(argsJson) {
42812
42954
  if (!argsJson) return void 0;
@@ -42876,88 +43018,6 @@ function runForgeBuild() {
42876
43018
 
42877
43019
  // src/commands/mandate.ts
42878
43020
  init_esm2();
42879
-
42880
- // src/lib/permission-explainer.ts
42881
- var import_node_fs13 = require("node:fs");
42882
- var import_node_path12 = require("node:path");
42883
- function explainPermission(name, sourcePath) {
42884
- const resolved = sourcePath ?? (0, import_node_path12.join)(process.cwd(), "mandates", `${name}.sol`);
42885
- let src;
42886
- try {
42887
- src = (0, import_node_fs13.readFileSync)(resolved, "utf8");
42888
- } catch {
42889
- return null;
42890
- }
42891
- return parseStructured(src) ?? parseNatSpec(src) ?? parseRequires(src);
42892
- }
42893
- function parseStructured(src) {
42894
- if (!src.includes("ENFORCED ON-CHAIN")) return null;
42895
- const lines = src.split("\n");
42896
- const result = {
42897
- source: "structured",
42898
- enforced: [],
42899
- notEnforced: []
42900
- };
42901
- const headerLines = [];
42902
- for (const line of lines) {
42903
- const t = line.trim();
42904
- if (t.startsWith("import ") || t.startsWith("contract ") || t.startsWith("abstract contract ")) break;
42905
- if (t.startsWith("//")) headerLines.push(t.slice(2));
42906
- }
42907
- let section = null;
42908
- for (const raw of headerLines) {
42909
- const trimmed = raw.trim();
42910
- if (/^─+$/.test(trimmed)) continue;
42911
- if (trimmed.includes("ENFORCED ON-CHAIN")) {
42912
- section = "enforced";
42913
- continue;
42914
- }
42915
- if (trimmed.includes("NOT ENFORCED")) {
42916
- section = "notEnforced";
42917
- continue;
42918
- }
42919
- if (trimmed.includes("VERIFY BEFORE USE")) {
42920
- section = null;
42921
- continue;
42922
- }
42923
- if (section === null) {
42924
- const kv = trimmed.match(/^(Protocol|Version|Chain|Target)\s*:\s*(.+)$/i);
42925
- if (kv) {
42926
- const key = kv[1].toLowerCase();
42927
- result[key] = kv[2].trim();
42928
- }
42929
- }
42930
- if (section && trimmed.startsWith("\u2022")) {
42931
- const bullet = trimmed.slice(1).trim();
42932
- if (bullet) result[section].push(bullet);
42933
- }
42934
- }
42935
- if (result.enforced.length === 0 && !result.protocol) return null;
42936
- return result;
42937
- }
42938
- function parseNatSpec(src) {
42939
- const enforced = [];
42940
- const notEnforced = [];
42941
- for (const line of src.split("\n")) {
42942
- const t = line.trim();
42943
- const notice = t.match(/^\/\/\/\s*@notice\s+(.+)$/);
42944
- if (notice) enforced.push(notice[1].trim());
42945
- const dev = t.match(/^\/\/\/\s*@dev\s+(.+)$/);
42946
- if (dev) notEnforced.push(dev[1].trim());
42947
- }
42948
- if (enforced.length === 0 && notEnforced.length === 0) return null;
42949
- return { source: "natspec", enforced, notEnforced };
42950
- }
42951
- function parseRequires(src) {
42952
- const enforced = [];
42953
- const re = /require\s*\([^,)]+,\s*["']([^"']+)["']\)/g;
42954
- let m;
42955
- while ((m = re.exec(src)) !== null) enforced.push(m[1]);
42956
- if (enforced.length === 0) return null;
42957
- return { source: "require-only", enforced, notEnforced: [] };
42958
- }
42959
-
42960
- // src/commands/mandate.ts
42961
43021
  function resolveActiveChain(account2) {
42962
43022
  try {
42963
43023
  const project = new ProjectContext();
@@ -43033,7 +43093,9 @@ ${permissions.length} permission(s) tracked for SMA ${account2.safe}:
43033
43093
  const draft = {
43034
43094
  account: account2.safe,
43035
43095
  chainId,
43036
- permissions: permissions.filter((p) => !p.revokedOnChain).map((p) => {
43096
+ // Only permissions not already registered on this SMA belong in the signing
43097
+ // draft — re-registering an already-registered permission reverts on-chain.
43098
+ permissions: permissions.filter((p) => !p.revokedOnChain && !p.registeredOnSma).map((p) => {
43037
43099
  const mandate2 = store.find(p.address);
43038
43100
  const explanation = explainPermission(p.label, mandate2?.sourcePath) ?? void 0;
43039
43101
  return { address: p.address, label: p.label, explanation };
@@ -43106,7 +43168,7 @@ ${unregistered.length} permission(s) are not yet registered on this SMA. Initiat
43106
43168
  }
43107
43169
 
43108
43170
  // src/commands/mandate-simulate.ts
43109
- var import_node_fs14 = require("node:fs");
43171
+ var import_node_fs15 = require("node:fs");
43110
43172
  init_esm2();
43111
43173
  function parseExpect(raw, where) {
43112
43174
  if (raw === void 0) return void 0;
@@ -43122,7 +43184,7 @@ function resolveSampleCalls(options) {
43122
43184
  if (options.calls) {
43123
43185
  let raw;
43124
43186
  try {
43125
- raw = JSON.parse((0, import_node_fs14.readFileSync)(options.calls, "utf8"));
43187
+ raw = JSON.parse((0, import_node_fs15.readFileSync)(options.calls, "utf8"));
43126
43188
  } catch (err) {
43127
43189
  throw new Error(`Could not read --calls file "${options.calls}": ${err.message}`);
43128
43190
  }
@@ -43338,7 +43400,7 @@ async function mandateSimulate(options) {
43338
43400
  }
43339
43401
 
43340
43402
  // src/commands/rotate-signer.ts
43341
- var import_node_fs15 = require("node:fs");
43403
+ var import_node_fs16 = require("node:fs");
43342
43404
  init_esm2();
43343
43405
  var PENDING_REATTACH_FILE = ["state", "pending-reattach.json"];
43344
43406
  async function rotateSigner(options) {
@@ -43468,7 +43530,7 @@ SMA: ${smaAddress}`);
43468
43530
  console.log(
43469
43531
  `
43470
43532
  \u2192 Open the Sailor dashboard to approve signing requests:
43471
- ${signingPageUrl(channel, projectPort(process.cwd()))}
43533
+ ${signingPageUrl(projectPort(process.cwd()))}
43472
43534
  `
43473
43535
  );
43474
43536
  });
@@ -43726,7 +43788,7 @@ ensure the agent that signs dispatches holds this key.`
43726
43788
  const keystore = await keyring.exportKeystore(password);
43727
43789
  writeJsonFile(target, keystore, 384);
43728
43790
  const perManagerPath = managerKeystorePath(keyring.address);
43729
- (0, import_node_fs15.mkdirSync)(sailPath("keys", "managers"), { recursive: true });
43791
+ (0, import_node_fs16.mkdirSync)(sailPath("keys", "managers"), { recursive: true });
43730
43792
  writeJsonFile(perManagerPath, keystore, 384);
43731
43793
  say(
43732
43794
  () => console.log(
@@ -43738,7 +43800,7 @@ ensure the agent that signs dispatches holds this key.`
43738
43800
  function promoteManagerKeystore(newManager, say) {
43739
43801
  const stored = readJsonFile(managerKeystorePath(newManager));
43740
43802
  if (!stored) return;
43741
- (0, import_node_fs15.mkdirSync)(sailPath("keys", "managers"), { recursive: true });
43803
+ (0, import_node_fs16.mkdirSync)(sailPath("keys", "managers"), { recursive: true });
43742
43804
  const activeTarget = keyPath("manager");
43743
43805
  const displaced = readJsonFile(activeTarget);
43744
43806
  if (displaced?.address) {
@@ -43790,7 +43852,7 @@ function writePending(pending) {
43790
43852
  }
43791
43853
  function clearPending() {
43792
43854
  try {
43793
- (0, import_node_fs15.rmSync)(sailPath(...PENDING_REATTACH_FILE), { force: true });
43855
+ (0, import_node_fs16.rmSync)(sailPath(...PENDING_REATTACH_FILE), { force: true });
43794
43856
  } catch {
43795
43857
  }
43796
43858
  }
@@ -43844,7 +43906,7 @@ async function ownerConnect(options) {
43844
43906
  await channel.start();
43845
43907
  if (!options.json) {
43846
43908
  console.log("\u2192 Open the Sailor dashboard and connect your wallet:");
43847
- console.log(` ${signingPageUrl(channel, projectPort(projectRoot))}`);
43909
+ console.log(` ${signingPageUrl(projectPort(projectRoot))}`);
43848
43910
  if (channel.remote) console.log(" (using the running signing station)");
43849
43911
  console.log("\nWaiting for a wallet connection\u2026");
43850
43912
  }
@@ -43893,41 +43955,6 @@ var import_node_fs17 = __toESM(require("node:fs"), 1);
43893
43955
  var import_node_path13 = __toESM(require("node:path"), 1);
43894
43956
  var import_node_url = require("node:url");
43895
43957
  init_esm2();
43896
-
43897
- // src/lib/process.ts
43898
- var import_node_fs16 = __toESM(require("node:fs"), 1);
43899
- function agentPidPath(chainId) {
43900
- return chainId != null ? sailPath(`agent-${chainId}.pid`) : sailPath("agent.pid");
43901
- }
43902
- function writeAgentPid(chainId) {
43903
- import_node_fs16.default.mkdirSync(sailPath(), { recursive: true });
43904
- import_node_fs16.default.writeFileSync(agentPidPath(chainId), `${process.pid}
43905
- `);
43906
- }
43907
- function clearAgentPid(chainId) {
43908
- try {
43909
- import_node_fs16.default.rmSync(agentPidPath(chainId));
43910
- } catch {
43911
- }
43912
- }
43913
- function readAgentPid(chainId) {
43914
- try {
43915
- const pid = Number.parseInt(import_node_fs16.default.readFileSync(agentPidPath(chainId), "utf-8").trim(), 10);
43916
- return Number.isNaN(pid) ? null : pid;
43917
- } catch {
43918
- return null;
43919
- }
43920
- }
43921
- function isProcessAlive(pid) {
43922
- try {
43923
- process.kill(pid, 0);
43924
- return true;
43925
- } catch {
43926
- return false;
43927
- }
43928
- }
43929
-
43930
- // src/commands/run.ts
43931
43958
  var DEFAULT_INTERVAL_SEC = 60;
43932
43959
  var sleep2 = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
43933
43960
  var ERC20_READ_ABI = [
@@ -44972,7 +44999,7 @@ function requireAccount() {
44972
44999
  }
44973
45000
  return account2;
44974
45001
  }
44975
- async function sessionPause() {
45002
+ async function sessionPause(opts = {}) {
44976
45003
  const account2 = requireAccount();
44977
45004
  const signer = await loadAnySigner();
44978
45005
  const client = makeClient(account2.chainId);
@@ -44987,9 +45014,10 @@ async function sessionPause() {
44987
45014
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
44988
45015
  };
44989
45016
  writeJsonFile(sailPath("session.json"), session2);
44990
- console.log("Session paused \u2014 agent dispatch rights revoked.");
45017
+ if (opts.json) console.log(JSON.stringify(session2));
45018
+ else console.log("Session paused \u2014 agent dispatch rights revoked.");
44991
45019
  }
44992
- async function sessionResume() {
45020
+ async function sessionResume(opts = {}) {
44993
45021
  const account2 = requireAccount();
44994
45022
  const signer = await loadAnySigner();
44995
45023
  const client = makeClient(account2.chainId);
@@ -45004,7 +45032,8 @@ async function sessionResume() {
45004
45032
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
45005
45033
  };
45006
45034
  writeJsonFile(sailPath("session.json"), session2);
45007
- console.log("Session resumed \u2014 agent dispatch rights restored.");
45035
+ if (opts.json) console.log(JSON.stringify(session2));
45036
+ else console.log("Session resumed \u2014 agent dispatch rights restored.");
45008
45037
  }
45009
45038
 
45010
45039
  // src/commands/station.ts
@@ -45122,7 +45151,11 @@ async function status() {
45122
45151
  const hasManager = keyExists("manager");
45123
45152
  const hasPermissionSigner = keyExists("permissionSigner");
45124
45153
  const account2 = readJsonFile(sailPath("account.json"));
45125
- const mandate2 = readJsonFile(sailPath("mandate.json"));
45154
+ const mandateRaw = readJsonFile(sailPath("mandate.json"));
45155
+ const mandates = (Array.isArray(mandateRaw) ? mandateRaw : mandateRaw ? [mandateRaw] : []).filter(
45156
+ Boolean
45157
+ );
45158
+ const hasMandate = mandates.length > 0;
45126
45159
  const session2 = readJsonFile(sailPath("session.json"));
45127
45160
  console.log("Sailor status");
45128
45161
  console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
@@ -45138,9 +45171,12 @@ async function status() {
45138
45171
  console.log(' \u2717 not deployed run "sailor onboard --new-sma"');
45139
45172
  }
45140
45173
  console.log("Mandate:");
45141
- if (mandate2) {
45142
- const n = mandate2.permissions.length;
45143
- console.log(` \u2713 signed ${n} permission${n === 1 ? "" : "s"}`);
45174
+ if (hasMandate) {
45175
+ const n = mandates.reduce((sum, m) => sum + (m?.permissions?.length ?? 0), 0);
45176
+ const permLabel = `${n} permission${n === 1 ? "" : "s"}`;
45177
+ console.log(
45178
+ mandates.length > 1 ? ` \u2713 signed ${mandates.length} mandates, ${permLabel}` : ` \u2713 signed ${permLabel}`
45179
+ );
45144
45180
  } else {
45145
45181
  console.log(' \u2717 not signed run "sailor mandate sign"');
45146
45182
  }
@@ -45150,7 +45186,7 @@ async function status() {
45150
45186
  let agentState;
45151
45187
  if (running) {
45152
45188
  agentState = `running (PID ${pid})`;
45153
- } else if (!mandate2) {
45189
+ } else if (!hasMandate) {
45154
45190
  agentState = "not configured";
45155
45191
  } else if (session2 && session2.active === false) {
45156
45192
  agentState = "stopped (session paused)";
@@ -45161,10 +45197,55 @@ async function status() {
45161
45197
  }
45162
45198
 
45163
45199
  // src/commands/ui.ts
45164
- var import_node_child_process4 = require("node:child_process");
45200
+ var import_node_child_process5 = require("node:child_process");
45165
45201
  var import_node_fs20 = __toESM(require("node:fs"), 1);
45166
45202
  var import_node_net2 = __toESM(require("node:net"), 1);
45167
45203
  var import_node_path16 = __toESM(require("node:path"), 1);
45204
+
45205
+ // src/lib/tailscale.ts
45206
+ var import_node_child_process4 = require("node:child_process");
45207
+ function tailscaleAvailable() {
45208
+ try {
45209
+ return (0, import_node_child_process4.spawnSync)("tailscale", ["version"], { stdio: "ignore" }).status === 0;
45210
+ } catch {
45211
+ return false;
45212
+ }
45213
+ }
45214
+ function tailnetDnsName() {
45215
+ const r = (0, import_node_child_process4.spawnSync)("tailscale", ["status", "--json"], { encoding: "utf-8" });
45216
+ if (r.status !== 0 || !r.stdout) return null;
45217
+ try {
45218
+ const j = JSON.parse(r.stdout);
45219
+ if (j.BackendState && j.BackendState !== "Running") return null;
45220
+ const dns = j.Self?.DNSName;
45221
+ if (typeof dns !== "string" || dns.length === 0) return null;
45222
+ return dns.replace(/\.$/, "");
45223
+ } catch {
45224
+ return null;
45225
+ }
45226
+ }
45227
+ function tailscaleServeUp(port) {
45228
+ const r = (0, import_node_child_process4.spawnSync)(
45229
+ "tailscale",
45230
+ ["serve", "--bg", "--https=443", `http://127.0.0.1:${port}`],
45231
+ { encoding: "utf-8" }
45232
+ );
45233
+ const out = `${r.stdout ?? ""}${r.stderr ?? ""}`;
45234
+ if (r.status !== 0) {
45235
+ throw new Error(out.trim() || `exit ${r.status ?? "?"}`);
45236
+ }
45237
+ if (/serve is not enabled/i.test(out)) {
45238
+ throw new Error(out.trim());
45239
+ }
45240
+ }
45241
+ function tailscaleServeDown() {
45242
+ try {
45243
+ (0, import_node_child_process4.spawnSync)("tailscale", ["serve", "--https=443", "off"], { stdio: "ignore" });
45244
+ } catch {
45245
+ }
45246
+ }
45247
+
45248
+ // src/commands/ui.ts
45168
45249
  var UI_STATE_FILE = import_node_path16.default.join(".sail", "runtime", "ui.json");
45169
45250
  function readState2(projectRoot) {
45170
45251
  const file = import_node_path16.default.join(projectRoot, UI_STATE_FILE);
@@ -45175,21 +45256,33 @@ function readState2(projectRoot) {
45175
45256
  return null;
45176
45257
  }
45177
45258
  }
45178
- function isAlive(pid) {
45259
+ function installedCliVersion() {
45179
45260
  try {
45180
- process.kill(pid, 0);
45181
- return true;
45261
+ const pkg = JSON.parse(
45262
+ import_node_fs20.default.readFileSync(import_node_path16.default.join(packageRoot(), "package.json"), "utf-8")
45263
+ );
45264
+ return pkg.version ?? null;
45182
45265
  } catch {
45183
- return false;
45266
+ return null;
45184
45267
  }
45185
45268
  }
45186
- function findFreePort(from14) {
45187
- return new Promise((resolve3, reject) => {
45188
- const server = import_node_net2.default.createServer();
45189
- server.unref();
45190
- server.on("error", () => findFreePort(from14 + 1).then(resolve3, reject));
45191
- server.listen(from14, "127.0.0.1", () => server.close(() => resolve3(from14)));
45192
- });
45269
+ async function warnIfVersionSkew(port) {
45270
+ const cli = installedCliVersion();
45271
+ if (!cli) return;
45272
+ try {
45273
+ const res = await fetch(`http://127.0.0.1:${port}/api/version`, {
45274
+ signal: AbortSignal.timeout(500)
45275
+ });
45276
+ if (!res.ok) return;
45277
+ const { running } = await res.json();
45278
+ if (running && running !== cli) {
45279
+ console.warn(
45280
+ `\u26A0 Version skew: the running dashboard is v${running}, but the installed CLI is v${cli}.
45281
+ Restart it to load the new version: sailor ui stop && sailor ui start`
45282
+ );
45283
+ }
45284
+ } catch {
45285
+ }
45193
45286
  }
45194
45287
  function waitForPort(port, timeoutMs) {
45195
45288
  const deadline = Date.now() + timeoutMs;
@@ -45209,7 +45302,7 @@ function waitForPort(port, timeoutMs) {
45209
45302
  attempt();
45210
45303
  });
45211
45304
  }
45212
- async function uiCommand() {
45305
+ async function uiCommand(opts = {}) {
45213
45306
  const distDir = cliDistDir();
45214
45307
  const uiDistDir = import_node_path16.default.join(packageRoot(), "packages", "ui", "dist");
45215
45308
  const serverBundle = import_node_path16.default.resolve(distDir, "server.cjs");
@@ -45223,19 +45316,49 @@ async function uiCommand() {
45223
45316
  if (!import_node_fs20.default.existsSync(import_node_path16.default.join(uiDistDir, "index.html"))) {
45224
45317
  throw new Error(`UI dist not found at ${uiDistDir}. Re-run the sailor build.`);
45225
45318
  }
45319
+ let tailnetUrl = null;
45320
+ if (opts.expose) {
45321
+ if (opts.expose !== "tailscale") {
45322
+ throw new Error(`Unknown --expose mode "${opts.expose}". Supported: tailscale.`);
45323
+ }
45324
+ if (!tailscaleAvailable()) {
45325
+ throw new Error(
45326
+ "tailscale CLI not found. Install Tailscale and run `tailscale up`, or start the dashboard without --expose to keep it local-only."
45327
+ );
45328
+ }
45329
+ const dns = tailnetDnsName();
45330
+ if (!dns) {
45331
+ throw new Error(
45332
+ "Could not resolve this node's tailnet name. Is tailscale running and logged in? Run `tailscale status` to check."
45333
+ );
45334
+ }
45335
+ tailnetUrl = `https://${dns}`;
45336
+ }
45226
45337
  const existing = readState2(projectRoot);
45227
- if (existing && isAlive(existing.pid)) {
45338
+ if (existing && isProcessAlive(existing.pid)) {
45228
45339
  console.log(`Sailor UI is already running (pid ${existing.pid}) at http://localhost:${existing.port}`);
45340
+ if (opts.expose) {
45341
+ console.log("To expose it on the tailnet, stop it first: sailor ui stop && sailor ui start --expose tailscale");
45342
+ }
45343
+ await warnIfVersionSkew(existing.port);
45229
45344
  return;
45230
45345
  }
45231
45346
  const runtimeDir = import_node_path16.default.join(projectRoot, ".sail", "runtime");
45232
45347
  import_node_fs20.default.mkdirSync(runtimeDir, { recursive: true });
45233
45348
  const logFile = import_node_path16.default.join(runtimeDir, "ui.log");
45234
45349
  const logFd = import_node_fs20.default.openSync(logFile, "a");
45235
- const child = (0, import_node_child_process4.spawn)(process.execPath, [serverBundle], {
45350
+ const corsOrigins = [process.env.SAILOR_CORS_ORIGINS, tailnetUrl].filter(Boolean).join(",");
45351
+ const child = (0, import_node_child_process5.spawn)(process.execPath, [serverBundle], {
45236
45352
  detached: true,
45237
45353
  stdio: ["ignore", logFd, logFd],
45238
- env: { ...process.env, SAIL_DIR: sailDir2, SERVE_DIST: "1", PORT: String(port), SAILOR_UI_DIST: uiDistDir }
45354
+ env: {
45355
+ ...process.env,
45356
+ SAIL_DIR: sailDir2,
45357
+ SERVE_DIST: "1",
45358
+ PORT: String(port),
45359
+ SAILOR_UI_DIST: uiDistDir,
45360
+ ...corsOrigins ? { SAILOR_CORS_ORIGINS: corsOrigins } : {}
45361
+ }
45239
45362
  });
45240
45363
  child.unref();
45241
45364
  import_node_fs20.default.closeSync(logFd);
@@ -45243,7 +45366,7 @@ async function uiCommand() {
45243
45366
  const deadline = Date.now() + READY_TIMEOUT_MS;
45244
45367
  let ready = false;
45245
45368
  while (Date.now() < deadline) {
45246
- if (!isAlive(child.pid)) break;
45369
+ if (!isProcessAlive(child.pid)) break;
45247
45370
  if (await waitForPort(port, 500)) {
45248
45371
  ready = true;
45249
45372
  break;
@@ -45262,17 +45385,31 @@ Server output:
45262
45385
  ${tail}` : ` See ${import_node_path16.default.relative(projectRoot, logFile)}.`)
45263
45386
  );
45264
45387
  }
45388
+ let exposed = false;
45389
+ if (tailnetUrl) {
45390
+ try {
45391
+ tailscaleServeUp(port);
45392
+ exposed = true;
45393
+ } catch (err) {
45394
+ console.warn(
45395
+ `\u26A0 Could not expose on the tailnet: ${err.message}
45396
+ The dashboard is still running locally.`
45397
+ );
45398
+ }
45399
+ }
45265
45400
  import_node_fs20.default.writeFileSync(
45266
45401
  import_node_path16.default.join(projectRoot, UI_STATE_FILE),
45267
- JSON.stringify({ pid: child.pid, port, startedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
45402
+ JSON.stringify({ pid: child.pid, port, startedAt: (/* @__PURE__ */ new Date()).toISOString(), exposed }, null, 2)
45268
45403
  );
45269
45404
  console.log(`Sailor UI started at http://localhost:${port} (pid ${child.pid})`);
45405
+ if (exposed) console.log(`Exposed on your tailnet at ${tailnetUrl}/`);
45270
45406
  console.log(`Stop it with: sailor ui stop`);
45271
45407
  }
45272
- function uiStatus() {
45408
+ async function uiStatus() {
45273
45409
  const state = readState2(process.cwd());
45274
- if (state && isAlive(state.pid)) {
45410
+ if (state && isProcessAlive(state.pid)) {
45275
45411
  console.log(`\u25CF running http://localhost:${state.port} (pid ${state.pid})`);
45412
+ await warnIfVersionSkew(state.port);
45276
45413
  } else {
45277
45414
  if (state) import_node_fs20.default.rmSync(import_node_path16.default.join(process.cwd(), UI_STATE_FILE), { force: true });
45278
45415
  console.log("\u25CB Sailor UI is not running");
@@ -45285,12 +45422,16 @@ function uiStop() {
45285
45422
  console.log("Sailor UI is not running.");
45286
45423
  return;
45287
45424
  }
45288
- if (!isAlive(state.pid)) {
45425
+ if (!isProcessAlive(state.pid)) {
45289
45426
  import_node_fs20.default.rmSync(import_node_path16.default.join(projectRoot, UI_STATE_FILE), { force: true });
45290
45427
  console.log("Sailor UI is not running (stale state file removed).");
45291
45428
  return;
45292
45429
  }
45293
45430
  process.kill(state.pid, "SIGTERM");
45431
+ if (state.exposed) {
45432
+ tailscaleServeDown();
45433
+ console.log("Tailnet exposure removed.");
45434
+ }
45294
45435
  import_node_fs20.default.rmSync(import_node_path16.default.join(projectRoot, UI_STATE_FILE), { force: true });
45295
45436
  console.log(`Stopped Sailor UI (pid ${state.pid}).`);
45296
45437
  }
@@ -45342,7 +45483,7 @@ program2.command("init [dir]").description("Scaffold a new Sail agent into the c
45342
45483
  );
45343
45484
  program2.command("update").description("Re-sync agent tooling files (skills, AGENTS.md, Dockerfile) from the latest template").action(action(updateCommand));
45344
45485
  var ui = program2.command("ui").description("Manage the local Sailor dashboard");
45345
- ui.command("start").description("Start the dashboard at localhost:3333 (default)").action(action(uiCommand));
45486
+ ui.command("start").description("Start the dashboard at localhost:3333 (default)").option("--expose <mode>", "Expose the dashboard over HTTPS on your tailnet (mode: tailscale)").action(actionWith(uiCommand));
45346
45487
  ui.command("stop").description("Stop the running dashboard").action(() => uiStop());
45347
45488
  ui.command("status").description("Show whether the dashboard is running").action(() => uiStatus());
45348
45489
  ui.action(action(uiCommand));
@@ -45407,8 +45548,8 @@ program2.command("run").description("Run the agent execution loop (use --once fo
45407
45548
  var trigger = program2.command("trigger").description("Wake the agent on demand from an external system");
45408
45549
  trigger.command("github").description("Fire the agent's GitHub Actions workflow_dispatch (the same job the cron runs)").option("--workflow <file>", "Workflow file to dispatch", "agent-tick.yml").option("--ref <branch>", "Git ref to run the workflow on", "main").option("--reason <text>", "Why this run fired \u2014 recorded as the workflow's reason input").option("--repo <owner/repo>", "Override the repository (default: from the git origin remote)").option("--json", "Emit machine-readable JSON").action(actionWith(triggerGithub));
45409
45550
  var session = program2.command("session").description("Control the agent session");
45410
- session.command("pause").description("Pause the agent session (revoke dispatch rights)").action(action(sessionPause));
45411
- session.command("resume").description("Resume a paused session").action(action(sessionResume));
45551
+ session.command("pause").description("Pause the agent session (revoke dispatch rights)").option("--json", "Emit machine-readable JSON").action(actionWith(sessionPause));
45552
+ session.command("resume").description("Resume a paused session").option("--json", "Emit machine-readable JSON").action(actionWith(sessionResume));
45412
45553
  program2.command("doctor").description(
45413
45554
  "Preflight (read-only): kernel model, permission health, RPC + gas balances, before spending gas"
45414
45555
  ).option("--json", "Output machine-readable JSON").option("--account <address>", "SMA to check (defaults to .sail/account.json)").action(actionWith(doctor));