mulmoterminal 1.7.1 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/bin/mulmoterminal.js +22 -34
  2. package/bin/npx-cache-hint.d.ts +2 -0
  3. package/bin/npx-cache-hint.js +69 -0
  4. package/bin/wait-ready.d.ts +26 -0
  5. package/bin/wait-ready.js +61 -0
  6. package/common/modelPresets.ts +11 -6
  7. package/dist/assets/{abnfDiagram-VRR7QNED-RjiYivmv-0ZB3Q7Mv.js → abnfDiagram-VRR7QNED-RjiYivmv-CCLjG9E9.js} +1 -1
  8. package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-Dc62_ljl.js +1 -0
  9. package/dist/assets/{architectureDiagram-ZJ3FMSHR-3nWA91tG-BGEQ26kW.js → architectureDiagram-ZJ3FMSHR-3nWA91tG-BcngES-G.js} +1 -1
  10. package/dist/assets/{blockDiagram-677ZJIJ3-BPuAJQRW-CfG6VjIt.js → blockDiagram-677ZJIJ3-BPuAJQRW-K8X4YdbW.js} +1 -1
  11. package/dist/assets/{c4Diagram-LMCZKHZV-C0LAqQso-5Y3QAEOO.js → c4Diagram-LMCZKHZV-C0LAqQso-DPvBKT_o.js} +1 -1
  12. package/dist/assets/channel-7wUqSdoX-NCtPilPL.js +1 -0
  13. package/dist/assets/{chunk-32BRIVSS-BRrYpgtb-LKWk21XQ.js → chunk-32BRIVSS-BRrYpgtb-Sgw4YiwO.js} +1 -1
  14. package/dist/assets/{chunk-52WLFC77-BJ-ss3Xr-DQ0c_tel.js → chunk-52WLFC77-BJ-ss3Xr-DXTsqQmw.js} +1 -1
  15. package/dist/assets/{chunk-C7G6YPKG-BZEucKEL-D1ScG-A9.js → chunk-C7G6YPKG-BZEucKEL-CzDWUuQD.js} +1 -1
  16. package/dist/assets/{chunk-EX3LRPZG-DLS6FBN1-CM3AIHWh.js → chunk-EX3LRPZG-DLS6FBN1-BZ8245YL.js} +1 -1
  17. package/dist/assets/{chunk-FWX5IMBZ-DHLSFw1H-C7tdhIeD.js → chunk-FWX5IMBZ-DHLSFw1H-rWT4wsYF.js} +2 -2
  18. package/dist/assets/{chunk-HOUHSVGY-Bhlt8hXJ-Cz_lCkPW.js → chunk-HOUHSVGY-Bhlt8hXJ-DHHJ39yY.js} +1 -1
  19. package/dist/assets/{chunk-ICXQ74PX-DwgHBX_g-BiaXcLCM.js → chunk-ICXQ74PX-DwgHBX_g-CVO4XUCO.js} +1 -1
  20. package/dist/assets/{chunk-MOJQB5TN-CMZRaeqt-DynKnij8.js → chunk-MOJQB5TN-CMZRaeqt-CfjRj6ge.js} +1 -1
  21. package/dist/assets/{chunk-OGEWGWER-8Qy4a8b5-BHp0IyoO.js → chunk-OGEWGWER-8Qy4a8b5-Cs1ErBxS.js} +1 -1
  22. package/dist/assets/{chunk-PUDLZKDR-DcrWQRYh-CgXHo6E0.js → chunk-PUDLZKDR-DcrWQRYh-DDlhQIXq.js} +1 -1
  23. package/dist/assets/{chunk-Q4XR5HBZ-ZXVGkG8Z-BEB0ANC3.js → chunk-Q4XR5HBZ-ZXVGkG8Z-DtPCWcx0.js} +1 -1
  24. package/dist/assets/{chunk-V7JOEXUC-DmGdheTX-Ct5XDuM1.js → chunk-V7JOEXUC-DmGdheTX-FwuThvxD.js} +1 -1
  25. package/dist/assets/{chunk-VAUOI2AC-CS9QJ4yz-BkjFKKFo.js → chunk-VAUOI2AC-CS9QJ4yz-rBHFym4n.js} +1 -1
  26. package/dist/assets/{chunk-VR4S4FIN-C6a91eNY-Cvguj9hE.js → chunk-VR4S4FIN-C6a91eNY-6Y9hr75K.js} +1 -1
  27. package/dist/assets/{chunk-WYO6CB5R-BlzOfotS-Dm5-lJPj.js → chunk-WYO6CB5R-BlzOfotS-Cg8JP_k-.js} +1 -1
  28. package/dist/assets/{chunk-ZGVPDNZ5-BYwxNFTK-hzHMTH8r.js → chunk-ZGVPDNZ5-BYwxNFTK-BvTVX4I4.js} +1 -1
  29. package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-D1ky24Ds.js +1 -0
  30. package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-D1ky24Ds.js +1 -0
  31. package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-ORm6k8ze.js +1 -0
  32. package/dist/assets/{cynefinDiagram-TSTJHNR4-DHA9iPo--9Vfx1RL5.js → cynefinDiagram-TSTJHNR4-DHA9iPo--Coj5CuZV.js} +1 -1
  33. package/dist/assets/{dagre-VKFMJZFB--oJKqXBZ-DVMiDNUa.js → dagre-VKFMJZFB--oJKqXBZ-iQ8NhR-E.js} +1 -1
  34. package/dist/assets/{diagram-FQU43EPY-CcJCB9bG-Dw8uXi5J.js → diagram-FQU43EPY-CcJCB9bG-CZBlndUC.js} +1 -1
  35. package/dist/assets/{diagram-G47NLZAW-C_o-WGG1-BiirKYwU.js → diagram-G47NLZAW-C_o-WGG1-CezpEy0i.js} +1 -1
  36. package/dist/assets/{diagram-NH7WQ7WH-CXJCYvY--CxJ-W_AV.js → diagram-NH7WQ7WH-CXJCYvY--DmwB4xPU.js} +1 -1
  37. package/dist/assets/{diagram-OA4YK3LP-BMzeJ87A-C66U-WpA.js → diagram-OA4YK3LP-BMzeJ87A-KO3buG9g.js} +1 -1
  38. package/dist/assets/{diagram-WEI45ONY-D_93NKqo-C5wgOP3m.js → diagram-WEI45ONY-D_93NKqo-epWpVF6L.js} +1 -1
  39. package/dist/assets/{ebnfDiagram-CCIWWBDH-CVai1Ii9-XxRZOagG.js → ebnfDiagram-CCIWWBDH-CVai1Ii9-CluKhaw-.js} +1 -1
  40. package/dist/assets/{erDiagram-Q63AITRT-CiABnA0s-C3SFUgS_.js → erDiagram-Q63AITRT-CiABnA0s-DuCAxim-.js} +1 -1
  41. package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a--puF0Mfa.js +1 -0
  42. package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-Cr4R3boa.js +1 -0
  43. package/dist/assets/{ganttDiagram-NO4QXBWP-DQZvdFo1-CZAkzl2_.js → ganttDiagram-NO4QXBWP-DQZvdFo1-9pklplap.js} +1 -1
  44. package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-Catx3rAM.js +1 -0
  45. package/dist/assets/{gitGraphDiagram-IHSO6WYX-C2ovBouh-CXNXoPSF.js → gitGraphDiagram-IHSO6WYX-C2ovBouh-CWEyMeWe.js} +1 -1
  46. package/dist/assets/index-CxXEEdyv.css +1 -0
  47. package/dist/assets/{index-C-I-KZAf.js → index-Dh4imzyE.js} +184 -184
  48. package/dist/assets/info-DKCQHKI2-Dplx5kMp-C6Q2N8lR.js +1 -0
  49. package/dist/assets/{infoDiagram-FWYZ7A6U-7UnoB5AP-GVMnr0TB.js → infoDiagram-FWYZ7A6U-7UnoB5AP-MGxGOrtx.js} +1 -1
  50. package/dist/assets/{ishikawaDiagram-FXEZZL3T-ByUDM_N2-BmKgPmF9.js → ishikawaDiagram-FXEZZL3T-ByUDM_N2-Bx_aHmxj.js} +1 -1
  51. package/dist/assets/{journeyDiagram-5HDEW3XC-c5xIah9o-BOoOb2Up.js → journeyDiagram-5HDEW3XC-c5xIah9o-DoZ1k7c2.js} +1 -1
  52. package/dist/assets/{kanban-definition-HUTT4EX6-Cnt6loYD-CV1DfLpD.js → kanban-definition-HUTT4EX6-Cnt6loYD-D_jZUsQX.js} +1 -1
  53. package/dist/assets/{lib-BfLKLNAW.js → lib-BuOUsCeD.js} +1 -1
  54. package/dist/assets/{line-D7ziSjKi-qjBgm7Wg.js → line-D7ziSjKi-BHXiLBOo.js} +1 -1
  55. package/dist/assets/{marp-BPQ2jYa4.js → marp-gvb94kjy.js} +1 -1
  56. package/dist/assets/{mermaid-parser.core-DxEa8E3F-bFNCoAMV.js → mermaid-parser.core-DxEa8E3F-kLn0LOBf.js} +2 -2
  57. package/dist/assets/{mermaid.core-V0OYwIz3-bDvzm251.js → mermaid.core-V0OYwIz3-C-sR2rUO.js} +3 -3
  58. package/dist/assets/{mindmap-definition-LN4V7U3C-CexN3O6L-BRp2Mb--.js → mindmap-definition-LN4V7U3C-CexN3O6L-C2mzLUhV.js} +1 -1
  59. package/dist/assets/packet-7NZHBO7P-CQI3flND-C5aIMsHg.js +1 -0
  60. package/dist/assets/{pegDiagram-2B236MQR-BiM4G0cW--kWTOoR2.js → pegDiagram-2B236MQR-BiM4G0cW-ilTx6ElC.js} +1 -1
  61. package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-DLGaOWQD.js +1 -0
  62. package/dist/assets/{pieDiagram-ENE6RG2P-B_fBBS-2-BOFV4ngY.js → pieDiagram-ENE6RG2P-B_fBBS-2-WTDOFCHf.js} +1 -1
  63. package/dist/assets/{quadrantDiagram-ABIIQ3AL-BigGVxCR-jyaO71dB.js → quadrantDiagram-ABIIQ3AL-BigGVxCR-2XE8FR0c.js} +1 -1
  64. package/dist/assets/radar-I7S5WNFK-us6x-Z9R-XEiHFJv1.js +1 -0
  65. package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-CHJoHZk-.js +1 -0
  66. package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-93zoonP0.js +1 -0
  67. package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-Op85BMOm.js +1 -0
  68. package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-CX7O2hYb.js +1 -0
  69. package/dist/assets/{railroadDiagram-RFXS5EU6--lNIlG64-D8DkHqgs.js → railroadDiagram-RFXS5EU6--lNIlG64-BUzUIavz.js} +1 -1
  70. package/dist/assets/{requirementDiagram-TGXJPOKE-D-B0Y5Wu-d5Jr6_2i.js → requirementDiagram-TGXJPOKE-D-B0Y5Wu-rbwe3PPS.js} +1 -1
  71. package/dist/assets/{sankeyDiagram-HTMAVEWB-DlutaXDv-VGEBaos9.js → sankeyDiagram-HTMAVEWB-DlutaXDv-DygoObNU.js} +1 -1
  72. package/dist/assets/{sequenceDiagram-DBY2YBRQ-BOLgtggH-hpEall7s.js → sequenceDiagram-DBY2YBRQ-BOLgtggH-B0ncAj6o.js} +1 -1
  73. package/dist/assets/{stateDiagram-2N3HPSRC-jf_dXUEU-CDm7zgbz.js → stateDiagram-2N3HPSRC-jf_dXUEU-RKHr1CUK.js} +1 -1
  74. package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-CvRqyCfT.js +1 -0
  75. package/dist/assets/{swimlanes-5IMT3BWC-CYjtALQH-Bn3Yx5s4.js → swimlanes-5IMT3BWC-CYjtALQH-xIOTAMQY.js} +1 -1
  76. package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-JHgNIgBl.js +8 -0
  77. package/dist/assets/{timeline-definition-FHXFAJF6-DYJ4oUm8-CtWavdRU.js → timeline-definition-FHXFAJF6-DYJ4oUm8-CGt2mnFM.js} +1 -1
  78. package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-8FYcTJ8G.js +1 -0
  79. package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-DLUudxda.js +1 -0
  80. package/dist/assets/{vennDiagram-L72KCM5P-CdKAHoek-D-tozz5H.js → vennDiagram-L72KCM5P-CdKAHoek-DbQdfDPm.js} +1 -1
  81. package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-BnWGyxAv.js +1 -0
  82. package/dist/assets/{wardleyDiagram-EHGQE667-BF6c4_CW-Bd3huzRu.js → wardleyDiagram-EHGQE667-BF6c4_CW-CE26h6BQ.js} +1 -1
  83. package/dist/assets/{xychartDiagram-FW5EYKEG-CGiKngj7-PiOfLnci.js → xychartDiagram-FW5EYKEG-CGiKngj7-DqMbN7gq.js} +1 -1
  84. package/dist/index.html +2 -2
  85. package/package.json +3 -3
  86. package/server/backends/byte-range.ts +18 -8
  87. package/server/backends/collections.ts +7 -1
  88. package/server/backends/files.ts +14 -9
  89. package/server/backends/html.ts +2 -2
  90. package/server/backends/image-gen.ts +13 -2
  91. package/server/backends/mutateStatus.ts +9 -0
  92. package/server/backends/remoteHost/attachment-path.ts +4 -1
  93. package/server/backends/remoteHost/attachmentStore.spec.ts +14 -1
  94. package/server/backends/remoteHost/attachmentStore.ts +3 -1
  95. package/server/backends/remoteHost/getFeed.spec.ts +1 -1
  96. package/server/backends/remoteHost/handlers.spec.ts +69 -4
  97. package/server/backends/remoteHost/handlers.ts +18 -5
  98. package/server/backends/remoteHost/ingestAttachments.spec.ts +11 -7
  99. package/server/backends/remoteHost/ingestAttachments.ts +29 -19
  100. package/server/backends/remoteHost/skills.ts +4 -2
  101. package/server/backends/remoteHost/stagedStorageIds.ts +5 -2
  102. package/server/backends/streamFile.ts +26 -0
  103. package/server/backends/worklog.ts +3 -3
  104. package/server/config/config-schema.ts +6 -1
  105. package/server/config/header-context.ts +10 -3
  106. package/server/git/pr-for-branch.ts +10 -5
  107. package/server/git/spawn-collect.ts +18 -9
  108. package/server/git/worktree-diff.ts +16 -3
  109. package/server/git/worktree-pr.ts +17 -1
  110. package/server/git/worktrees.ts +21 -5
  111. package/server/infra/plugins-registry.ts +6 -2
  112. package/server/infra/tmux-routes.ts +17 -1
  113. package/server/routes/app-routes.ts +2 -1
  114. package/server/routes/plugin-narration.ts +4 -2
  115. package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-9X3_cRqd.js +0 -1
  116. package/dist/assets/channel-7wUqSdoX-vfZkO_uL.js +0 -1
  117. package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-CeGPKz7I.js +0 -1
  118. package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-CeGPKz7I.js +0 -1
  119. package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-0kc9OM8m.js +0 -1
  120. package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-CjeZJqe8.js +0 -1
  121. package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-B6tkM4yH.js +0 -1
  122. package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-BnYaIjat.js +0 -1
  123. package/dist/assets/index-Cue6y3NP.css +0 -1
  124. package/dist/assets/info-DKCQHKI2-Dplx5kMp-DYTQnnkg.js +0 -1
  125. package/dist/assets/packet-7NZHBO7P-CQI3flND-TB9IxewR.js +0 -1
  126. package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-DfFUYw37.js +0 -1
  127. package/dist/assets/radar-I7S5WNFK-us6x-Z9R-CfuJM-7B.js +0 -1
  128. package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-d15bc0qE.js +0 -1
  129. package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-CyjJaPJJ.js +0 -1
  130. package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-CWODrAAC.js +0 -1
  131. package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-uM99LygW.js +0 -1
  132. package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-BJkHeYpz.js +0 -1
  133. package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-Dq42c4u7.js +0 -8
  134. package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-DVQpaIF5.js +0 -1
  135. package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-DpXrJ_Vl.js +0 -1
  136. package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-DS8lznjT.js +0 -1
@@ -7,13 +7,14 @@
7
7
 
8
8
  import { execSync, spawn } from "node:child_process";
9
9
  import { existsSync, statSync } from "node:fs";
10
- import { get as httpGet } from "node:http";
11
10
  import { createRequire } from "node:module";
12
11
  import { createServer } from "node:net";
13
12
  import { dirname, join, resolve } from "node:path";
14
13
  import { createInterface } from "node:readline";
15
14
  import { fileURLToPath } from "node:url";
16
15
  import { computeUpdateNotice, isUpdateCheckDisabled } from "./update-check.js";
16
+ import { detectNpxCacheDir, npxCacheHintLines } from "./npx-cache-hint.js";
17
+ import { waitUntilReady } from "./wait-ready.js";
17
18
  import {
18
19
  chooseCwd,
19
20
  parsePortArg,
@@ -30,10 +31,12 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
30
31
  const PKG_DIR = join(__dirname, "..");
31
32
  const SERVER_ENTRY = join(PKG_DIR, "server", "index.ts");
32
33
  const DEFAULT_PORT = 34567;
33
- const READY_TIMEOUT_MS = 15_000;
34
34
  // Server exit code meaning "port taken at bind time" — keep in sync with
35
35
  // server/index.ts (PORT_IN_USE_EXIT_CODE).
36
36
  const PORT_IN_USE_EXIT_CODE = 75;
37
+ // Only the end of stderr matters for the crash diagnosis; a long-lived server can log
38
+ // arbitrarily much before dying, so the tail is bounded.
39
+ const STDERR_TAIL_MAX_BYTES = 64 * 1024;
37
40
 
38
41
  // Single source of truth: read the version from the shipped package.json so
39
42
  // `--version` never drifts from the published version.
@@ -170,36 +173,6 @@ function isPortFree(port) {
170
173
  });
171
174
  }
172
175
 
173
- // Poll the server until it answers, then call onReady; give up after the timeout
174
- // so the launcher never hangs on a crash loop. Returns a cancel function — a
175
- // raced/abandoned attempt stops polling so it can't fire a stale banner.
176
- function waitUntilReady(port, onReady) {
177
- const startedAt = Date.now();
178
- let timer = null;
179
- let cancelled = false;
180
- const attempt = () => {
181
- if (cancelled) return;
182
- const req = httpGet({ host: "127.0.0.1", port, path: "/", timeout: 1000 }, (res) => {
183
- res.resume();
184
- if (!cancelled) onReady();
185
- });
186
- req.on("error", retry);
187
- req.on("timeout", () => {
188
- req.destroy();
189
- retry();
190
- });
191
- };
192
- const retry = () => {
193
- if (cancelled || Date.now() - startedAt > READY_TIMEOUT_MS) return;
194
- timer = setTimeout(attempt, 300);
195
- };
196
- attempt();
197
- return () => {
198
- cancelled = true;
199
- if (timer) clearTimeout(timer);
200
- };
201
- }
202
-
203
176
  function printReadyBanner(url) {
204
177
  const bar = "\x1b[32m" + "─".repeat(48) + "\x1b[0m";
205
178
  console.log(`\n${bar}`);
@@ -272,10 +245,18 @@ async function choosePort(requested, explicit) {
272
245
  function runServer(port, noOpen, cwd, onChild) {
273
246
  return new Promise((resolveExit) => {
274
247
  log(`Starting MulmoTerminal on port ${port}...`);
248
+ // stderr is piped (and passed through) so a fatal boot error can be inspected once the
249
+ // child closes — a half-unpacked npx cache entry crashes here with ERR_MODULE_NOT_FOUND,
250
+ // and without reading stderr the launcher cannot tell that from a real bug.
275
251
  const server = spawn(process.execPath, ["--import", "tsx", SERVER_ENTRY], {
276
252
  cwd: PKG_DIR,
277
253
  env: { ...process.env, NODE_ENV: "production", PORT: String(port), CLAUDE_CWD: cwd },
278
- stdio: "inherit",
254
+ stdio: ["inherit", "inherit", "pipe"],
255
+ });
256
+ let stderrTail = "";
257
+ server.stderr.on("data", (chunk) => {
258
+ process.stderr.write(chunk);
259
+ stderrTail = (stderrTail + chunk.toString()).slice(-STDERR_TAIL_MAX_BYTES);
279
260
  });
280
261
  onChild(server);
281
262
 
@@ -292,7 +273,10 @@ function runServer(port, noOpen, cwd, onChild) {
292
273
  }
293
274
  });
294
275
 
295
- server.on("exit", (code) => {
276
+ // `close`, not `exit`: it fires only once the piped stderr has fully drained, so the
277
+ // whole crash output — including a trailing `_npx/<hash>` line that can arrive after
278
+ // `exit` — is in `stderrTail` before we inspect it.
279
+ server.on("close", (code) => {
296
280
  cancelReady();
297
281
  // Exit code 75 means this child failed to bind (EADDRINUSE) and never
298
282
  // served — always retriable, regardless of what a probe to the port saw
@@ -301,6 +285,10 @@ function runServer(port, noOpen, cwd, onChild) {
301
285
  resolveExit();
302
286
  return;
303
287
  }
288
+ if (code !== 0) {
289
+ const cacheDir = detectNpxCacheDir(stderrTail);
290
+ if (cacheDir) npxCacheHintLines(cacheDir, process.platform).forEach((line) => error(line));
291
+ }
304
292
  process.exit(code ?? 1);
305
293
  });
306
294
  });
@@ -0,0 +1,2 @@
1
+ export declare function detectNpxCacheDir(stderrText: string): string | null;
2
+ export declare function npxCacheHintLines(cacheDir: string, platform: NodeJS.Platform): string[];
@@ -0,0 +1,69 @@
1
+ // Turning a confusing crash into a one-line fix. When an `npx mulmoterminal` install is
2
+ // aborted mid-unpack (npm's npx lock race — `ECOMPROMISED: Lock compromised`), the next run
3
+ // reuses the half-unpacked `~/.npm/_npx/<hash>/node_modules` and dies at boot with
4
+ // ERR_MODULE_NOT_FOUND for some transitive dependency. That reads as a broken package, but
5
+ // the published tarball is fine — the local npx cache entry is corrupted, and removing it
6
+ // is the whole fix. Nothing here reads the filesystem; the caller feeds it stderr text.
7
+
8
+ const MISSING_MODULE = /ERR_MODULE_NOT_FOUND|Cannot find (?:package|module)/;
9
+
10
+ // The cache entry itself: `<sep>_npx<sep><hash>` followed by another separator. Anchored on
11
+ // the literal `_npx` rather than on a leading `[^\s'"]*` wildcard — that prefix can also match
12
+ // separators, which makes the match ambiguous and the regex backtrack super-linearly on a long
13
+ // stack trace. The directory part before it is recovered by scanning, not by matching.
14
+ const NPX_ENTRY = /[/\\]_npx[/\\][0-9a-f]+(?=[/\\])/;
15
+
16
+ // What ends the path token a stack trace embeds: whitespace, or the quotes Node wraps
17
+ // specifiers in.
18
+ const PATH_BOUNDARIES = [" ", "\t", "\n", "\r", "'", '"'];
19
+
20
+ // Where the path token containing the entry begins: just past the last boundary before it.
21
+ const pathStartIndex = (textBefore) => Math.max(-1, ...PATH_BOUNDARIES.map((ch) => textBefore.lastIndexOf(ch))) + 1;
22
+
23
+ /**
24
+ * The corrupted npx cache entry a failed boot points at, or null.
25
+ * Fires only when BOTH signals are present — a module-resolution error and a path inside
26
+ * an `_npx/<hash>` cache entry — so an ordinary crash, or a resolution error in a normal
27
+ * install, never triggers the hint.
28
+ */
29
+ export function detectNpxCacheDir(stderrText) {
30
+ if (typeof stderrText !== "string" || !MISSING_MODULE.test(stderrText)) return null;
31
+ const match = NPX_ENTRY.exec(stderrText);
32
+ if (!match) return null;
33
+ return stderrText.slice(pathStartIndex(stderrText.slice(0, match.index)), match.index + match[0].length);
34
+ }
35
+
36
+ // Shell-quote the cache dir so a path containing quote characters (a home dir with an
37
+ // apostrophe on POSIX is the realistic case) produces a command that is still valid — and
38
+ // safe — to copy-paste, rather than one that breaks or runs something unintended.
39
+
40
+ // POSIX single-quoting: wrap in '…' and turn every embedded ' into '\'' (close, escaped
41
+ // quote, reopen). Nothing inside single quotes is special, so this is injection-proof.
42
+ const posixQuote = (s) => `'${s.replace(/'/g, "'\\''")}'`;
43
+
44
+ // PowerShell single-quoting: '…' with each ' doubled. Single quotes suppress $-interpolation
45
+ // (a double-quoted path containing $ would otherwise expand a variable), so this is safe.
46
+ const powershellQuote = (s) => `'${s.replace(/'/g, "''")}'`;
47
+
48
+ // cmd has no way to escape a " inside "…", but " is an illegal character in a Windows path,
49
+ // so double-quoting a real cache dir is safe; it protects the spaces that do occur.
50
+ const cmdQuote = (s) => `"${s}"`;
51
+
52
+ // The delete command differs per platform, and a wrong one is worse than none: `rm -rf` is not
53
+ // a command on Windows, and cmd's `rmdir /s /q` is a syntax error in PowerShell (where `rmdir`
54
+ // aliases Remove-Item), so Windows gets both of its shells spelled out.
55
+ const removalCommands = (cacheDir, platform) =>
56
+ platform === "win32"
57
+ ? [` cmd: rmdir /s /q ${cmdQuote(cacheDir)}`, ` PowerShell: Remove-Item -Recurse -Force ${powershellQuote(cacheDir)}`]
58
+ : [` rm -rf ${posixQuote(cacheDir)}`];
59
+
60
+ /** The recovery instructions shown under the crash, one string per line. */
61
+ export function npxCacheHintLines(cacheDir, platform) {
62
+ return [
63
+ "This looks like a corrupted npx cache, not a MulmoTerminal bug.",
64
+ 'An interrupted `npx` install (npm\'s "Lock compromised" race) can leave this entry half-unpacked:',
65
+ ` ${cacheDir}`,
66
+ "Remove it and run the same command again:",
67
+ ...removalCommands(cacheDir, platform),
68
+ ];
69
+ }
@@ -0,0 +1,26 @@
1
+ // Only the surface probeOnce actually touches — narrower than http.ClientRequest /
2
+ // IncomingMessage on purpose, so a test fake and the real node:http both satisfy it.
3
+ export interface ProbeRequest {
4
+ on(event: "error" | "timeout", listener: (...args: unknown[]) => void): unknown;
5
+ destroy(): void;
6
+ }
7
+
8
+ export interface ProbeResponse {
9
+ resume(): void;
10
+ }
11
+
12
+ export type ProbeGet = (opts: { host: string; port: number; path: string; timeout: number }, cb: (res: ProbeResponse) => void) => ProbeRequest;
13
+
14
+ export declare function probeOnce(get: ProbeGet, port: number, timeoutMs?: number): Promise<"ready" | "retry">;
15
+
16
+ export declare function waitUntilReady(
17
+ port: number,
18
+ onReady: () => void,
19
+ deps?: {
20
+ get?: ProbeGet;
21
+ now?: () => number;
22
+ timeoutMs?: number;
23
+ intervalMs?: number;
24
+ readyTimeoutMs?: number;
25
+ },
26
+ ): () => void;
@@ -0,0 +1,61 @@
1
+ // Polling the launched server until it answers `/`, then firing onReady exactly once.
2
+ // Split out of mulmoterminal.js so the retry state machine is testable without a socket.
3
+
4
+ import { get as httpGet } from "node:http";
5
+
6
+ const PROBE_TIMEOUT_MS = 1000;
7
+ const RETRY_INTERVAL_MS = 300;
8
+ const DEFAULT_READY_TIMEOUT_MS = 15_000;
9
+
10
+ // One probe of the server: resolves "ready" if it answers, else "retry". It NEVER
11
+ // rejects, and yields exactly ONE outcome per call — the crucial guarantee. Destroying a
12
+ // timed-out request itself emits an 'error', so a single timeout would otherwise trigger
13
+ // both the timeout handler AND the error handler; each schedules a retry, the poll loop
14
+ // forks in two, and every fork fires onReady when the server finally answers — printing
15
+ // the banner and opening the browser several times. The `settled` latch collapses each
16
+ // request to one outcome.
17
+ export function probeOnce(get, port, timeoutMs = PROBE_TIMEOUT_MS) {
18
+ return new Promise((resolve) => {
19
+ let settled = false;
20
+ const done = (outcome) => {
21
+ if (settled) return;
22
+ settled = true;
23
+ resolve(outcome);
24
+ };
25
+ const req = get({ host: "127.0.0.1", port, path: "/", timeout: timeoutMs }, (res) => {
26
+ res.resume();
27
+ done("ready");
28
+ });
29
+ req.on("error", () => done("retry"));
30
+ req.on("timeout", () => {
31
+ req.destroy();
32
+ done("retry");
33
+ });
34
+ });
35
+ }
36
+
37
+ // Poll until the server answers, then call onReady once; give up after readyTimeoutMs so
38
+ // the launcher never hangs on a crash loop. Returns a cancel function — a raced/abandoned
39
+ // attempt stops polling so it can't fire a stale banner. `deps` is injectable for tests.
40
+ export function waitUntilReady(port, onReady, deps = {}) {
41
+ const { get = httpGet, now = Date.now, timeoutMs = PROBE_TIMEOUT_MS, intervalMs = RETRY_INTERVAL_MS, readyTimeoutMs = DEFAULT_READY_TIMEOUT_MS } = deps;
42
+ const startedAt = now();
43
+ let cancelled = false;
44
+ let timer = null;
45
+ const loop = async () => {
46
+ if (cancelled) return;
47
+ const outcome = await probeOnce(get, port, timeoutMs);
48
+ if (cancelled) return;
49
+ if (outcome === "ready") {
50
+ onReady();
51
+ return;
52
+ }
53
+ if (now() - startedAt > readyTimeoutMs) return;
54
+ timer = setTimeout(loop, intervalMs);
55
+ };
56
+ loop();
57
+ return () => {
58
+ cancelled = true;
59
+ if (timer) clearTimeout(timer);
60
+ };
61
+ }
@@ -197,7 +197,7 @@ export const MODEL_PRESETS: readonly ModelPreset[] = [
197
197
  provider: "openrouter",
198
198
  id: "nvidia/nemotron-3-ultra-550b-a55b",
199
199
  label: "Nemotron 3 Ultra 550B",
200
- contextLength: 512_288,
200
+ contextLength: 524_288, // 512 KiB = 512 * 1024; was 512_288 (a typo)
201
201
  pricePerMTok: { input: 0.6, output: 3.6 },
202
202
  trials: measured(3, 3, 13),
203
203
  },
@@ -300,14 +300,19 @@ export const MODEL_PRESETS: readonly ModelPreset[] = [
300
300
 
301
301
  // Presets for one provider, plus whatever the user added to that provider's `models`.
302
302
  // A user entry with the same id as a preset keeps the preset's measured numbers rather
303
- // than appearing twice.
303
+ // than appearing twice — and two user entries for the same id (even differing only in
304
+ // case) collapse to one, so a dropdown never shows the same model twice.
304
305
  export function presetsForProvider(providerId: string, userModels: readonly string[] = []): ModelPreset[] {
305
306
  const presets = MODEL_PRESETS.filter((preset) => preset.provider === providerId);
306
307
  // Match `presetFor` (modelOption.ts), which compares ids case-insensitively: a user
307
308
  // entry that only differs from a preset in case is the same model, not a new one.
308
- const known = new Set(presets.map((preset) => preset.id.toLowerCase()));
309
- const added: ModelPreset[] = userModels
310
- .filter((id) => !known.has(id.toLowerCase()))
311
- .map((id) => ({ provider: providerId, id, label: id, contextLength: 0, pricePerMTok: { input: 0, output: 0 }, trials: { status: "unmeasured" } }));
309
+ const seen = new Set(presets.map((preset) => preset.id.toLowerCase()));
310
+ const added: ModelPreset[] = [];
311
+ for (const id of userModels) {
312
+ const key = id.toLowerCase();
313
+ if (seen.has(key)) continue; // already a preset OR an earlier user entry
314
+ seen.add(key);
315
+ added.push({ provider: providerId, id, label: id, contextLength: 0, pricePerMTok: { input: 0, output: 0 }, trials: { status: "unmeasured" } });
316
+ }
312
317
  return [...presets, ...added];
313
318
  }
@@ -1 +1 @@
1
- import{n as e}from"./chunk-Y2CYZVJY-Bdt8pFDJ-DsF7k-Jl.js";import{f as t}from"./src-Brzfja-q-ch1VcTiG.js";import{_ as n,g as r}from"./mermaid-parser.core-DxEa8E3F-bFNCoAMV.js";import"./chunk-WYO6CB5R-BlzOfotS-Dm5-lJPj.js";import"./chunk-VAUOI2AC-CS9QJ4yz-BkjFKKFo.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-DynKnij8.js";import{t as s}from"./chunk-JWPE2WC7-BQ3zXr2k-DSd3Ct95.js";var c=r().RailroadAbnf.parser.LangiumParser,l=e(e=>{let t=e.alternatives.map(u);return t.length===1?t[0]:{type:`choice`,alternatives:t}},`transformAlternation`),u=e(e=>{let t=e.elements.map(f);return t.length===1?t[0]:{type:`sequence`,elements:t}},`transformConcatenation`),d=e(e=>{if(e.includes(`*`)){let[t,n]=e.split(`*`);return{min:t?parseInt(t,10):0,max:n?parseInt(n,10):1/0}}let t=parseInt(e,10);return{min:t,max:t}},`parseRepeat`),f=e(e=>{let t=p(e.primary);if(!e.repeat)return t;let{min:n,max:r}=d(e.repeat);return n===0&&r===1?{type:`optional`,element:t}:{type:`repetition`,element:t,min:n,max:r}},`transformElement`),p=e(e=>{switch(e.$type){case`AbnfStringLiteral`:return{type:`terminal`,value:e.value};case`AbnfNumVal`:return{type:`terminal`,value:e.value};case`AbnfRuleName`:return{type:`nonterminal`,name:e.name};case`AbnfGroup`:return l(e.element);case`AbnfOptionalGroup`:return{type:`optional`,element:l(e.element)};default:throw Error(`Unsupported ABNF primary node: ${e.$type}`)}},`transformPrimary`),m=e(e=>({name:e.name,definition:l(e.definition)}),`transformRule`),h=e(e=>{s(e,a),e.title&&a.setTitle(e.title),e.rules.map(e=>a.addRule(m(e)))},`populateDb`),g={parser:{parse:e(e=>{a.clear(),t.debug(`[ABNF Parser] Starting Langium parse`);let r=c.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new n(r);let i=r.value;t.debug(`[ABNF Parser] Parsed rules:`,i.rules.length),h(i),t.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:a}},db:a,renderer:o,styles:i};export{g as diagram};
1
+ import{n as e}from"./chunk-Y2CYZVJY-Bdt8pFDJ-DsF7k-Jl.js";import{f as t}from"./src-Brzfja-q-ch1VcTiG.js";import{_ as n,g as r}from"./mermaid-parser.core-DxEa8E3F-kLn0LOBf.js";import"./chunk-WYO6CB5R-BlzOfotS-Cg8JP_k-.js";import"./chunk-VAUOI2AC-CS9QJ4yz-rBHFym4n.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-CfjRj6ge.js";import{t as s}from"./chunk-JWPE2WC7-BQ3zXr2k-DSd3Ct95.js";var c=r().RailroadAbnf.parser.LangiumParser,l=e(e=>{let t=e.alternatives.map(u);return t.length===1?t[0]:{type:`choice`,alternatives:t}},`transformAlternation`),u=e(e=>{let t=e.elements.map(f);return t.length===1?t[0]:{type:`sequence`,elements:t}},`transformConcatenation`),d=e(e=>{if(e.includes(`*`)){let[t,n]=e.split(`*`);return{min:t?parseInt(t,10):0,max:n?parseInt(n,10):1/0}}let t=parseInt(e,10);return{min:t,max:t}},`parseRepeat`),f=e(e=>{let t=p(e.primary);if(!e.repeat)return t;let{min:n,max:r}=d(e.repeat);return n===0&&r===1?{type:`optional`,element:t}:{type:`repetition`,element:t,min:n,max:r}},`transformElement`),p=e(e=>{switch(e.$type){case`AbnfStringLiteral`:return{type:`terminal`,value:e.value};case`AbnfNumVal`:return{type:`terminal`,value:e.value};case`AbnfRuleName`:return{type:`nonterminal`,name:e.name};case`AbnfGroup`:return l(e.element);case`AbnfOptionalGroup`:return{type:`optional`,element:l(e.element)};default:throw Error(`Unsupported ABNF primary node: ${e.$type}`)}},`transformPrimary`),m=e(e=>({name:e.name,definition:l(e.definition)}),`transformRule`),h=e(e=>{s(e,a),e.title&&a.setTitle(e.title),e.rules.map(e=>a.addRule(m(e)))},`populateDb`),g={parser:{parse:e(e=>{a.clear(),t.debug(`[ABNF Parser] Starting Langium parse`);let r=c.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new n(r);let i=r.value;t.debug(`[ABNF Parser] Parsed rules:`,i.rules.length),h(i),t.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:a}},db:a,renderer:o,styles:i};export{g as diagram};
@@ -0,0 +1 @@
1
+ import{f as e}from"./mermaid-parser.core-DxEa8E3F-kLn0LOBf.js";export{e as createArchitectureServices};