@mxalbert/context-mode 2.0.0

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 (365) hide show
  1. package/.claude-plugin/marketplace.json +33 -0
  2. package/.claude-plugin/plugin.json +31 -0
  3. package/.codex-plugin/hooks.json +65 -0
  4. package/.codex-plugin/mcp.json +13 -0
  5. package/.codex-plugin/plugin.json +31 -0
  6. package/.openclaw-plugin/index.ts +12 -0
  7. package/.openclaw-plugin/openclaw.plugin.json +38 -0
  8. package/.openclaw-plugin/package.json +28 -0
  9. package/LICENSE +94 -0
  10. package/README.md +1619 -0
  11. package/bin/statusline.mjs +378 -0
  12. package/build/adapters/antigravity/index.d.ts +51 -0
  13. package/build/adapters/antigravity/index.js +169 -0
  14. package/build/adapters/antigravity-cli/index.d.ts +51 -0
  15. package/build/adapters/antigravity-cli/index.js +342 -0
  16. package/build/adapters/base.d.ts +92 -0
  17. package/build/adapters/base.js +135 -0
  18. package/build/adapters/claude-code/config.d.ts +8 -0
  19. package/build/adapters/claude-code/config.js +8 -0
  20. package/build/adapters/claude-code/hooks.d.ts +106 -0
  21. package/build/adapters/claude-code/hooks.js +183 -0
  22. package/build/adapters/claude-code/index.d.ts +80 -0
  23. package/build/adapters/claude-code/index.js +569 -0
  24. package/build/adapters/claude-code-base.d.ts +49 -0
  25. package/build/adapters/claude-code-base.js +113 -0
  26. package/build/adapters/client-map.d.ts +10 -0
  27. package/build/adapters/client-map.js +42 -0
  28. package/build/adapters/codex/config.d.ts +8 -0
  29. package/build/adapters/codex/config.js +8 -0
  30. package/build/adapters/codex/hooks.d.ts +55 -0
  31. package/build/adapters/codex/hooks.js +64 -0
  32. package/build/adapters/codex/index.d.ts +87 -0
  33. package/build/adapters/codex/index.js +995 -0
  34. package/build/adapters/codex/paths.d.ts +1 -0
  35. package/build/adapters/codex/paths.js +12 -0
  36. package/build/adapters/codex/usage.d.ts +107 -0
  37. package/build/adapters/codex/usage.js +227 -0
  38. package/build/adapters/copilot-base.d.ts +94 -0
  39. package/build/adapters/copilot-base.js +297 -0
  40. package/build/adapters/copilot-cli/hooks.d.ts +33 -0
  41. package/build/adapters/copilot-cli/hooks.js +64 -0
  42. package/build/adapters/copilot-cli/index.d.ts +48 -0
  43. package/build/adapters/copilot-cli/index.js +341 -0
  44. package/build/adapters/cursor/config.d.ts +4 -0
  45. package/build/adapters/cursor/config.js +4 -0
  46. package/build/adapters/cursor/hooks.d.ts +55 -0
  47. package/build/adapters/cursor/hooks.js +82 -0
  48. package/build/adapters/cursor/index.d.ts +64 -0
  49. package/build/adapters/cursor/index.js +517 -0
  50. package/build/adapters/detect.d.ts +136 -0
  51. package/build/adapters/detect.js +623 -0
  52. package/build/adapters/gemini-cli/config.d.ts +8 -0
  53. package/build/adapters/gemini-cli/config.js +8 -0
  54. package/build/adapters/gemini-cli/hooks.d.ts +83 -0
  55. package/build/adapters/gemini-cli/hooks.js +115 -0
  56. package/build/adapters/gemini-cli/index.d.ts +80 -0
  57. package/build/adapters/gemini-cli/index.js +483 -0
  58. package/build/adapters/jetbrains-copilot/config.d.ts +8 -0
  59. package/build/adapters/jetbrains-copilot/config.js +8 -0
  60. package/build/adapters/jetbrains-copilot/hooks.d.ts +59 -0
  61. package/build/adapters/jetbrains-copilot/hooks.js +87 -0
  62. package/build/adapters/jetbrains-copilot/index.d.ts +31 -0
  63. package/build/adapters/jetbrains-copilot/index.js +131 -0
  64. package/build/adapters/kimi/config.d.ts +8 -0
  65. package/build/adapters/kimi/config.js +8 -0
  66. package/build/adapters/kimi/hooks.d.ts +28 -0
  67. package/build/adapters/kimi/hooks.js +34 -0
  68. package/build/adapters/kimi/index.d.ts +66 -0
  69. package/build/adapters/kimi/index.js +537 -0
  70. package/build/adapters/kimi/paths.d.ts +21 -0
  71. package/build/adapters/kimi/paths.js +52 -0
  72. package/build/adapters/kimi/usage.d.ts +82 -0
  73. package/build/adapters/kimi/usage.js +217 -0
  74. package/build/adapters/kiro/hooks.d.ts +61 -0
  75. package/build/adapters/kiro/hooks.js +94 -0
  76. package/build/adapters/kiro/index.d.ts +61 -0
  77. package/build/adapters/kiro/index.js +301 -0
  78. package/build/adapters/omp/index.d.ts +65 -0
  79. package/build/adapters/omp/index.js +182 -0
  80. package/build/adapters/omp/plugin.d.ts +81 -0
  81. package/build/adapters/omp/plugin.js +331 -0
  82. package/build/adapters/omp/usage.d.ts +49 -0
  83. package/build/adapters/omp/usage.js +110 -0
  84. package/build/adapters/openclaw/config.d.ts +8 -0
  85. package/build/adapters/openclaw/config.js +8 -0
  86. package/build/adapters/openclaw/hooks.d.ts +50 -0
  87. package/build/adapters/openclaw/hooks.js +61 -0
  88. package/build/adapters/openclaw/index.d.ts +74 -0
  89. package/build/adapters/openclaw/index.js +439 -0
  90. package/build/adapters/openclaw/mcp-tools.d.ts +54 -0
  91. package/build/adapters/openclaw/mcp-tools.js +207 -0
  92. package/build/adapters/openclaw/plugin.d.ts +141 -0
  93. package/build/adapters/openclaw/plugin.js +818 -0
  94. package/build/adapters/openclaw/session-db.d.ts +55 -0
  95. package/build/adapters/openclaw/session-db.js +88 -0
  96. package/build/adapters/openclaw/usage.d.ts +34 -0
  97. package/build/adapters/openclaw/usage.js +52 -0
  98. package/build/adapters/openclaw/workspace-router.d.ts +29 -0
  99. package/build/adapters/openclaw/workspace-router.js +64 -0
  100. package/build/adapters/opencode/config.d.ts +8 -0
  101. package/build/adapters/opencode/config.js +8 -0
  102. package/build/adapters/opencode/hooks.d.ts +38 -0
  103. package/build/adapters/opencode/hooks.js +50 -0
  104. package/build/adapters/opencode/index.d.ts +144 -0
  105. package/build/adapters/opencode/index.js +638 -0
  106. package/build/adapters/opencode/plugin.d.ts +249 -0
  107. package/build/adapters/opencode/plugin.js +1536 -0
  108. package/build/adapters/opencode/v2.d.ts +117 -0
  109. package/build/adapters/opencode/v2.js +224 -0
  110. package/build/adapters/opencode/zod3tov4.d.ts +1 -0
  111. package/build/adapters/opencode/zod3tov4.js +132 -0
  112. package/build/adapters/pi/extension.d.ts +81 -0
  113. package/build/adapters/pi/extension.js +861 -0
  114. package/build/adapters/pi/index.d.ts +57 -0
  115. package/build/adapters/pi/index.js +173 -0
  116. package/build/adapters/pi/mcp-bridge.d.ts +268 -0
  117. package/build/adapters/pi/mcp-bridge.js +894 -0
  118. package/build/adapters/qwen-code/hooks.d.ts +26 -0
  119. package/build/adapters/qwen-code/hooks.js +29 -0
  120. package/build/adapters/qwen-code/index.d.ts +40 -0
  121. package/build/adapters/qwen-code/index.js +361 -0
  122. package/build/adapters/qwen-code/usage.d.ts +90 -0
  123. package/build/adapters/qwen-code/usage.js +222 -0
  124. package/build/adapters/types.d.ts +360 -0
  125. package/build/adapters/types.js +118 -0
  126. package/build/adapters/vscode-copilot/config.d.ts +8 -0
  127. package/build/adapters/vscode-copilot/config.js +8 -0
  128. package/build/adapters/vscode-copilot/hooks.d.ts +71 -0
  129. package/build/adapters/vscode-copilot/hooks.js +98 -0
  130. package/build/adapters/vscode-copilot/index.d.ts +32 -0
  131. package/build/adapters/vscode-copilot/index.js +222 -0
  132. package/build/adapters/zed/index.d.ts +40 -0
  133. package/build/adapters/zed/index.js +163 -0
  134. package/build/cli.d.ts +34 -0
  135. package/build/cli.js +1828 -0
  136. package/build/db-base.d.ts +250 -0
  137. package/build/db-base.js +756 -0
  138. package/build/executor.d.ts +72 -0
  139. package/build/executor.js +678 -0
  140. package/build/exit-classify.d.ts +19 -0
  141. package/build/exit-classify.js +12 -0
  142. package/build/fetch/blocks.d.ts +145 -0
  143. package/build/fetch/blocks.js +244 -0
  144. package/build/fetch/extract.d.ts +68 -0
  145. package/build/fetch/extract.js +123 -0
  146. package/build/fetch/page-store.d.ts +66 -0
  147. package/build/fetch/page-store.js +174 -0
  148. package/build/fetch-cache.d.ts +13 -0
  149. package/build/fetch-cache.js +15 -0
  150. package/build/lifecycle.d.ts +124 -0
  151. package/build/lifecycle.js +278 -0
  152. package/build/package-identity.d.ts +26 -0
  153. package/build/package-identity.js +28 -0
  154. package/build/runPool.d.ts +36 -0
  155. package/build/runPool.js +51 -0
  156. package/build/runtime.d.ts +71 -0
  157. package/build/runtime.js +659 -0
  158. package/build/search/auto-memory.d.ts +47 -0
  159. package/build/search/auto-memory.js +170 -0
  160. package/build/search/ctx-search-schema.d.ts +90 -0
  161. package/build/search/ctx-search-schema.js +135 -0
  162. package/build/search/flood-guard.d.ts +57 -0
  163. package/build/search/flood-guard.js +80 -0
  164. package/build/search/unified.d.ts +56 -0
  165. package/build/search/unified.js +104 -0
  166. package/build/security.d.ts +206 -0
  167. package/build/security.js +780 -0
  168. package/build/server.d.ts +240 -0
  169. package/build/server.js +5034 -0
  170. package/build/session/analytics.d.ts +773 -0
  171. package/build/session/analytics.js +2420 -0
  172. package/build/session/db.d.ts +468 -0
  173. package/build/session/db.js +1330 -0
  174. package/build/session/error-classifier.d.ts +87 -0
  175. package/build/session/error-classifier.js +303 -0
  176. package/build/session/event-emit.d.ts +48 -0
  177. package/build/session/event-emit.js +101 -0
  178. package/build/session/extract.d.ts +333 -0
  179. package/build/session/extract.js +2688 -0
  180. package/build/session/model-prices.json +429 -0
  181. package/build/session/persist-tool-calls.d.ts +54 -0
  182. package/build/session/persist-tool-calls.js +105 -0
  183. package/build/session/pricing.d.ts +64 -0
  184. package/build/session/pricing.js +151 -0
  185. package/build/session/project-attribution.d.ts +73 -0
  186. package/build/session/project-attribution.js +245 -0
  187. package/build/session/purge.d.ts +138 -0
  188. package/build/session/purge.js +240 -0
  189. package/build/session/retrieval-marker.d.ts +39 -0
  190. package/build/session/retrieval-marker.js +65 -0
  191. package/build/session/snapshot.d.ts +46 -0
  192. package/build/session/snapshot.js +532 -0
  193. package/build/store-directory.d.ts +56 -0
  194. package/build/store-directory.js +254 -0
  195. package/build/store.d.ts +147 -0
  196. package/build/store.js +1714 -0
  197. package/build/truncate.d.ts +57 -0
  198. package/build/truncate.js +142 -0
  199. package/build/types.d.ts +117 -0
  200. package/build/types.js +20 -0
  201. package/build/util/claude-config.d.ts +32 -0
  202. package/build/util/claude-config.js +84 -0
  203. package/build/util/hook-config.d.ts +27 -0
  204. package/build/util/hook-config.js +76 -0
  205. package/build/util/jsonc.d.ts +14 -0
  206. package/build/util/jsonc.js +104 -0
  207. package/build/util/plugin-cache-integrity.d.ts +51 -0
  208. package/build/util/plugin-cache-integrity.js +146 -0
  209. package/build/util/project-dir.d.ts +149 -0
  210. package/build/util/project-dir.js +340 -0
  211. package/build/util/sibling-mcp.d.ts +79 -0
  212. package/build/util/sibling-mcp.js +181 -0
  213. package/cli.bundle.mjs +1495 -0
  214. package/configs/antigravity/GEMINI.md +68 -0
  215. package/configs/antigravity/mcp_config.json +7 -0
  216. package/configs/antigravity-cli/hooks/hooks.json +37 -0
  217. package/configs/antigravity-cli/hooks.json +37 -0
  218. package/configs/antigravity-cli/mcp_config.json +10 -0
  219. package/configs/antigravity-cli/plugin.json +14 -0
  220. package/configs/antigravity-cli/rules/context-mode.md +77 -0
  221. package/configs/antigravity-cli/skills/context-mode/SKILL.md +77 -0
  222. package/configs/claude-code/CLAUDE.md +91 -0
  223. package/configs/codex/AGENTS.md +99 -0
  224. package/configs/codex/config.toml +8 -0
  225. package/configs/codex/hooks.json +47 -0
  226. package/configs/copilot-cli/.github/plugin/plugin.json +23 -0
  227. package/configs/copilot-cli/.mcp.json +12 -0
  228. package/configs/copilot-cli/README.md +47 -0
  229. package/configs/copilot-cli/hooks.json +41 -0
  230. package/configs/copilot-cli/skills/context-mode/SKILL.md +38 -0
  231. package/configs/cursor/context-mode.mdc +76 -0
  232. package/configs/cursor/hooks.json +21 -0
  233. package/configs/cursor/mcp.json +7 -0
  234. package/configs/gemini-cli/GEMINI.md +88 -0
  235. package/configs/gemini-cli/mcp.json +7 -0
  236. package/configs/gemini-cli/settings.json +60 -0
  237. package/configs/jetbrains-copilot/copilot-instructions.md +80 -0
  238. package/configs/jetbrains-copilot/hooks.json +16 -0
  239. package/configs/jetbrains-copilot/mcp.json +7 -0
  240. package/configs/kilo/AGENTS.md +84 -0
  241. package/configs/kilo/kilo.json +6 -0
  242. package/configs/kimi/hooks.json +54 -0
  243. package/configs/kiro/KIRO.md +84 -0
  244. package/configs/kiro/agent.json +18 -0
  245. package/configs/kiro/mcp.json +7 -0
  246. package/configs/omp/SYSTEM.md +85 -0
  247. package/configs/omp/mcp.json +7 -0
  248. package/configs/openclaw/AGENTS.md +87 -0
  249. package/configs/openclaw/openclaw.json +13 -0
  250. package/configs/opencode/AGENTS.md +84 -0
  251. package/configs/opencode/opencode.json +6 -0
  252. package/configs/pi/AGENTS.md +3 -0
  253. package/configs/qwen-code/QWEN.md +91 -0
  254. package/configs/vscode-copilot/copilot-instructions.md +80 -0
  255. package/configs/vscode-copilot/hooks.json +16 -0
  256. package/configs/vscode-copilot/mcp.json +7 -0
  257. package/configs/zed/AGENTS.md +68 -0
  258. package/hooks/antigravity-cli/payload.mjs +98 -0
  259. package/hooks/antigravity-cli/posttooluse.mjs +138 -0
  260. package/hooks/antigravity-cli/pretooluse.mjs +78 -0
  261. package/hooks/antigravity-cli/stop.mjs +58 -0
  262. package/hooks/auto-injection.mjs +102 -0
  263. package/hooks/cache-heal-utils.mjs +379 -0
  264. package/hooks/codex/platform.mjs +1 -0
  265. package/hooks/codex/posttooluse.mjs +66 -0
  266. package/hooks/codex/precompact.mjs +70 -0
  267. package/hooks/codex/pretooluse.mjs +42 -0
  268. package/hooks/codex/sessionstart.mjs +121 -0
  269. package/hooks/codex/stop.mjs +138 -0
  270. package/hooks/codex/userpromptsubmit.mjs +75 -0
  271. package/hooks/copilot-cli/posttooluse.mjs +79 -0
  272. package/hooks/copilot-cli/precompact.mjs +66 -0
  273. package/hooks/copilot-cli/pretooluse.mjs +41 -0
  274. package/hooks/copilot-cli/sessionstart.mjs +121 -0
  275. package/hooks/copilot-cli/stop.mjs +59 -0
  276. package/hooks/copilot-cli/userpromptsubmit.mjs +77 -0
  277. package/hooks/core/codex-caps.mjs +112 -0
  278. package/hooks/core/formatters.mjs +356 -0
  279. package/hooks/core/mcp-ready.mjs +109 -0
  280. package/hooks/core/platform-detect.mjs +49 -0
  281. package/hooks/core/routing.mjs +1050 -0
  282. package/hooks/core/stdin.mjs +90 -0
  283. package/hooks/core/tool-naming.mjs +57 -0
  284. package/hooks/cursor/afteragentresponse.mjs +74 -0
  285. package/hooks/cursor/hooks.json +31 -0
  286. package/hooks/cursor/posttooluse.mjs +72 -0
  287. package/hooks/cursor/pretooluse.mjs +26 -0
  288. package/hooks/cursor/sessionstart.mjs +97 -0
  289. package/hooks/cursor/stop.mjs +48 -0
  290. package/hooks/ensure-deps.mjs +254 -0
  291. package/hooks/formatters/claude-code.mjs +113 -0
  292. package/hooks/formatters/cursor.mjs +37 -0
  293. package/hooks/formatters/gemini-cli.mjs +55 -0
  294. package/hooks/formatters/vscode-copilot.mjs +55 -0
  295. package/hooks/gemini-cli/aftermodel.mjs +70 -0
  296. package/hooks/gemini-cli/aftertool.mjs +61 -0
  297. package/hooks/gemini-cli/beforeagent.mjs +99 -0
  298. package/hooks/gemini-cli/beforetool.mjs +27 -0
  299. package/hooks/gemini-cli/precompress.mjs +55 -0
  300. package/hooks/gemini-cli/sessionstart.mjs +137 -0
  301. package/hooks/heal-partial-install.mjs +712 -0
  302. package/hooks/hooks.json +143 -0
  303. package/hooks/jetbrains-copilot/posttooluse.mjs +61 -0
  304. package/hooks/jetbrains-copilot/precompact.mjs +55 -0
  305. package/hooks/jetbrains-copilot/pretooluse.mjs +27 -0
  306. package/hooks/jetbrains-copilot/sessionstart.mjs +124 -0
  307. package/hooks/kimi/platform.mjs +1 -0
  308. package/hooks/kimi/posttooluse.mjs +72 -0
  309. package/hooks/kimi/precompact.mjs +80 -0
  310. package/hooks/kimi/pretooluse.mjs +42 -0
  311. package/hooks/kimi/sessionend.mjs +61 -0
  312. package/hooks/kimi/sessionstart.mjs +113 -0
  313. package/hooks/kimi/stop.mjs +132 -0
  314. package/hooks/kimi/userpromptsubmit.mjs +90 -0
  315. package/hooks/kiro/agentspawn.mjs +97 -0
  316. package/hooks/kiro/posttooluse.mjs +51 -0
  317. package/hooks/kiro/pretooluse.mjs +64 -0
  318. package/hooks/kiro/userpromptsubmit.mjs +88 -0
  319. package/hooks/normalize-hooks.mjs +323 -0
  320. package/hooks/platform-bridge.mjs +340 -0
  321. package/hooks/posttooluse.mjs +225 -0
  322. package/hooks/precompact.mjs +100 -0
  323. package/hooks/pretooluse.mjs +227 -0
  324. package/hooks/qwen-code/platform.mjs +1 -0
  325. package/hooks/qwen-code/stop.mjs +168 -0
  326. package/hooks/routing-block.mjs +105 -0
  327. package/hooks/run-hook.mjs +95 -0
  328. package/hooks/security.bundle.mjs +2 -0
  329. package/hooks/session-attribution.bundle.mjs +1 -0
  330. package/hooks/session-db.bundle.mjs +145 -0
  331. package/hooks/session-directive.mjs +520 -0
  332. package/hooks/session-extract.bundle.mjs +3 -0
  333. package/hooks/session-helpers.mjs +428 -0
  334. package/hooks/session-loaders.mjs +448 -0
  335. package/hooks/session-snapshot.bundle.mjs +32 -0
  336. package/hooks/sessionstart.mjs +464 -0
  337. package/hooks/stop.mjs +82 -0
  338. package/hooks/suppress-stderr.mjs +25 -0
  339. package/hooks/userpromptsubmit.mjs +108 -0
  340. package/hooks/vscode-copilot/posttooluse.mjs +61 -0
  341. package/hooks/vscode-copilot/precompact.mjs +55 -0
  342. package/hooks/vscode-copilot/pretooluse.mjs +27 -0
  343. package/hooks/vscode-copilot/sessionstart.mjs +129 -0
  344. package/openclaw.plugin.json +38 -0
  345. package/package.json +132 -0
  346. package/scripts/heal-better-sqlite3.mjs +430 -0
  347. package/scripts/heal-installed-plugins.mjs +635 -0
  348. package/scripts/plugin-cache-integrity.mjs +248 -0
  349. package/scripts/postinstall.mjs +397 -0
  350. package/server.bundle.mjs +1421 -0
  351. package/server.js +5 -0
  352. package/skills/.ignore +7 -0
  353. package/skills/context-mode/SKILL.md +300 -0
  354. package/skills/context-mode/references/anti-patterns.md +283 -0
  355. package/skills/context-mode/references/patterns-javascript.md +298 -0
  356. package/skills/context-mode/references/patterns-python.md +304 -0
  357. package/skills/context-mode/references/patterns-shell.md +277 -0
  358. package/skills/ctx-doctor/SKILL.md +22 -0
  359. package/skills/ctx-index/SKILL.md +46 -0
  360. package/skills/ctx-insight/SKILL.md +24 -0
  361. package/skills/ctx-purge/SKILL.md +49 -0
  362. package/skills/ctx-search/SKILL.md +35 -0
  363. package/skills/ctx-stats/SKILL.md +26 -0
  364. package/skills/ctx-upgrade/SKILL.md +31 -0
  365. package/start.mjs +616 -0
@@ -0,0 +1,659 @@
1
+ import { execFileSync, execSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { JS_RUNTIMES } from "./adapters/types.js";
4
+ /**
5
+ * Allowlist for SHELL env override. Only POSIX shells + Windows shells permit
6
+ * arbitrary command interpretation; anything else (e.g., /usr/bin/python set
7
+ * as SHELL) would let an attacker redirect the executor to a non-shell binary.
8
+ *
9
+ * basename split handles BOTH `/` and `\` separators so a Windows-style path
10
+ * (`C:\Program Files\PowerShell\7\pwsh.exe`) classifies correctly even when
11
+ * the runtime is on POSIX (where node:path.basename only splits on `/`).
12
+ *
13
+ * Match is case-insensitive; `.exe` extension tolerated for Windows binaries.
14
+ */
15
+ const ALLOWED_SHELL_BASENAMES = /^(bash|sh|zsh|dash|pwsh|powershell|cmd)(\.exe)?$/i;
16
+ const BUN_BASENAME = /^bun(\.exe)?$/i;
17
+ function runtimeBasename(runtimePath) {
18
+ const segments = runtimePath.split(/[\\/]/);
19
+ return segments[segments.length - 1] ?? runtimePath;
20
+ }
21
+ export function isAllowlistedShell(shellPath) {
22
+ // Cross-OS basename: split on either separator, take the last segment.
23
+ return ALLOWED_SHELL_BASENAMES.test(runtimeBasename(shellPath));
24
+ }
25
+ function isWindowsWslBash(shellPath) {
26
+ const lower = shellPath.toLowerCase().replace(/\//g, "\\");
27
+ return /\\windows\\(?:system32|sysnative)\\bash\.exe$/.test(lower) ||
28
+ /\\microsoft\\windowsapps\\bash\.exe$/.test(lower);
29
+ }
30
+ function isWindowsSystemCmd(shellPath) {
31
+ const lower = shellPath.toLowerCase().replace(/\//g, "\\");
32
+ return /\\windows\\(?:system32|sysnative)\\cmd\.exe$/.test(lower);
33
+ }
34
+ const isWindows = process.platform === "win32";
35
+ function commandExists(cmd) {
36
+ try {
37
+ const check = isWindows ? `where ${cmd}` : `command -v ${cmd}`;
38
+ execSync(check, { stdio: "pipe" });
39
+ return true;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ /**
46
+ * Stricter probe than commandExists() — also verifies the resolved binary
47
+ * actually runs. On Windows, `where python3` matches the Microsoft Store
48
+ * App Execution Alias stub at C:\Users\<u>\AppData\Local\Microsoft\WindowsApps\
49
+ * even when no real Python is installed; the stub exits non-zero (9009) and
50
+ * pops the Store. Filter those entries out and require `<cmd> --version` to
51
+ * exit 0 before declaring the runtime available (#455).
52
+ */
53
+ function runnableExists(cmd) {
54
+ if (isWindows) {
55
+ // Reject if every `where` hit lives under Microsoft\WindowsApps (Store stubs).
56
+ try {
57
+ const out = execSync(`where ${cmd}`, { encoding: "utf-8", stdio: "pipe" });
58
+ const hits = out.trim().split(/\r?\n/).map(p => p.trim()).filter(Boolean);
59
+ if (hits.length === 0)
60
+ return false;
61
+ const realHits = hits.filter(p => !/\\Microsoft\\WindowsApps\\/i.test(p));
62
+ if (realHits.length === 0)
63
+ return false;
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
69
+ else if (!commandExists(cmd)) {
70
+ return false;
71
+ }
72
+ // Probe with --version. On Windows, allow 5s for cold-start (MS Store stub
73
+ // fallthrough can be slow). On POSIX, 1500ms is plenty for a real binary
74
+ // and keeps cold detection of python3 → python → py under ~5s total (#454).
75
+ try {
76
+ // DEP0190 fix: avoid args array with shell:true on Windows.
77
+ // Use execSync with a command string when shell is required;
78
+ // keep execFileSync (no shell) on POSIX.
79
+ if (isWindows) {
80
+ execSync(`"${cmd}" --version`, { stdio: "pipe", timeout: 5000 });
81
+ }
82
+ else {
83
+ execFileSync(cmd, ["--version"], { stdio: "pipe", timeout: 1500 });
84
+ }
85
+ return true;
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
91
+ function bunExists() {
92
+ if (commandExists("bun"))
93
+ return true;
94
+ for (const p of bunFallbackPaths()) {
95
+ if (existsSync(p))
96
+ return true;
97
+ }
98
+ return false;
99
+ }
100
+ function bunCommand() {
101
+ // Prefer absolute .exe paths so spawn() can run with shell:false on Windows.
102
+ // `where bun` may resolve to a `bun.cmd` npm shim (#506) which CreateProcess
103
+ // cannot execute directly — return the real .exe wherever we can find one.
104
+ for (const p of bunFallbackPaths()) {
105
+ if (existsSync(p))
106
+ return p;
107
+ }
108
+ // Bare name only if PATH resolution confirms it. On Windows this is
109
+ // typically a .cmd shim — the executor's needsShell list (which now
110
+ // includes "bun" — see #506) ensures shell:true so cmd.exe can resolve it.
111
+ if (commandExists("bun"))
112
+ return "bun";
113
+ // Synthetic last-resort path for diagnostics/error messages.
114
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
115
+ return isWindows ? `${home}\\.bun\\bin\\bun.exe` : `${home}/.bun/bin/bun`;
116
+ }
117
+ /** Fallback paths where Bun may be installed but not on PATH. */
118
+ function bunFallbackPaths() {
119
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
120
+ if (isWindows) {
121
+ const localAppData = process.env.LOCALAPPDATA ?? "";
122
+ const appData = process.env.APPDATA ?? "";
123
+ return [
124
+ // Native bun installer locations (irm bun.sh/install.ps1).
125
+ ...(home ? [`${home}\\.bun\\bin\\bun.exe`] : []),
126
+ ...(localAppData ? [`${localAppData}\\bun\\bin\\bun.exe`] : []),
127
+ // npm i -g bun installs bun.exe under the npm prefix (typically
128
+ // %APPDATA%\npm\node_modules\bun\bin\bun.exe). Without this, npm
129
+ // installs were "found" via bun.cmd shim on PATH and the bare "bun"
130
+ // string was returned — spawn() then ENOENT'd because CreateProcess
131
+ // can't execute .cmd files (#506).
132
+ ...(appData ? [`${appData}\\npm\\node_modules\\bun\\bin\\bun.exe`] : []),
133
+ ];
134
+ }
135
+ return home ? [`${home}/.bun/bin/bun`] : [];
136
+ }
137
+ /** Well-known Git-for-Windows bash.exe locations (MSYS bash that performs
138
+ * Windows→POSIX path conversion for native git — #826). */
139
+ const KNOWN_GIT_BASH_PATHS = [
140
+ "C:\\Program Files\\Git\\usr\\bin\\bash.exe",
141
+ "C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe",
142
+ ];
143
+ /**
144
+ * On Windows, resolve the first non-WSL bash that is actually available.
145
+ *
146
+ * Availability is gated by `where bash` (#796): bash must be discoverable on
147
+ * PATH for us to claim it. WSL bash (C:\Windows\System32\bash.exe) cannot
148
+ * handle Windows paths, so we skip it and prefer Git Bash / MSYS2 bash.
149
+ *
150
+ * Routing the gate through `where bash` — rather than probing the known Git
151
+ * Bash paths with existsSync first — is deliberate: when bash is genuinely
152
+ * unavailable, the caller must fall through to pwsh (PR intent). Probing the
153
+ * filesystem first re-detected a real Git Bash on the runner even though the
154
+ * scenario was "bash unavailable", so pwsh was never reached.
155
+ *
156
+ * #826 is preserved: when `where bash` surfaces a Git Bash candidate we
157
+ * canonicalize it to the absolute Git\usr\bin\bash.exe path (so native git
158
+ * keeps MSYS path conversion) by preferring a matching known path that exists.
159
+ */
160
+ function resolveWindowsBash() {
161
+ let candidates;
162
+ try {
163
+ const result = execSync("where bash", { encoding: "utf-8", stdio: "pipe" });
164
+ candidates = result.trim().split(/\r?\n/).map(p => p.trim()).filter(Boolean);
165
+ }
166
+ catch {
167
+ // bash not on PATH → genuinely unavailable. Fall through to pwsh/etc.
168
+ return null;
169
+ }
170
+ for (const p of candidates) {
171
+ const lower = p.toLowerCase();
172
+ if (lower.includes("system32") || lower.includes("windowsapps"))
173
+ continue;
174
+ // Prefer the canonical Git\usr\bin\bash.exe so native git retains MSYS
175
+ // path conversion. `where bash` on a Git-for-Windows install may surface
176
+ // the Git\cmd\bash shim or usr\bin path; upgrade to a known absolute path
177
+ // when one exists on disk.
178
+ for (const known of KNOWN_GIT_BASH_PATHS) {
179
+ if (existsSync(known))
180
+ return known;
181
+ }
182
+ return p;
183
+ }
184
+ return null;
185
+ }
186
+ function resolveWindowsShell(windowsBash = resolveWindowsBash()) {
187
+ // Prefer Git Bash (#826) so native git keeps its MSYS path conversion.
188
+ // The caller passes the already-resolved windowsBash to avoid probing the
189
+ // filesystem twice (it also feeds the cmd.exe shellOverride guard above).
190
+ // Fall back through POSIX sh, then PowerShell Core (pwsh) for proper UTF-8
191
+ // handling, then Windows PowerShell, then cmd.exe as the last resort.
192
+ return windowsBash
193
+ ?? (commandExists("sh")
194
+ ? "sh"
195
+ : commandExists("pwsh")
196
+ ? "pwsh"
197
+ : commandExists("powershell")
198
+ ? "powershell"
199
+ : "cmd.exe");
200
+ }
201
+ function getVersion(cmd, args = ["--version"]) {
202
+ try {
203
+ // DEP0190 fix: avoid args array with shell:true on Windows.
204
+ if (process.platform === "win32") {
205
+ // Hardening (PR #537 review): quote any cmd.exe metacharacter, not just
206
+ // whitespace. Current arg sources are internally controlled, but cheap
207
+ // defense-in-depth for future call sites.
208
+ const cmdStr = [cmd, ...args]
209
+ .map(a => /[\s"&|<>^()%!]/.test(a) ? JSON.stringify(a) : a)
210
+ .join(" ");
211
+ return execSync(cmdStr, {
212
+ encoding: "utf-8",
213
+ stdio: ["pipe", "pipe", "pipe"],
214
+ timeout: 5000,
215
+ })
216
+ .trim()
217
+ .split(/\r?\n/)[0];
218
+ }
219
+ else {
220
+ return execFileSync(cmd, args, {
221
+ encoding: "utf-8",
222
+ stdio: ["pipe", "pipe", "pipe"],
223
+ timeout: 5000,
224
+ })
225
+ .trim()
226
+ .split(/\r?\n/)[0];
227
+ }
228
+ }
229
+ catch {
230
+ return "unknown";
231
+ }
232
+ }
233
+ /**
234
+ * Resolve the JavaScript runtime used by PolyglotExecutor.
235
+ *
236
+ * PR #190 (f69b0d2) made `process.execPath` the default so snap-Node
237
+ * envs would not re-invoke the snap wrapper via PATH. That assumed
238
+ * `process.execPath` always points at a JS runtime — true on Node,
239
+ * tsx, and snap-Node, but FALSE when context-mode runs in-process
240
+ * inside a bun-compiled self-contained binary (OpenCode, Kilo, …).
241
+ * In those hosts, `process.execPath` resolves to `opencode.exe` /
242
+ * `opencode` (NOT node), and spawning that with a `.js` argument
243
+ * triggers the yargs "Failed to change directory" error (#731).
244
+ *
245
+ * Fix: gate `process.execPath` on the existing `JS_RUNTIMES`
246
+ * allowlist (single source of truth — same set used by
247
+ * `buildNodeCommand()` in src/adapters/types.ts since PR #708). When
248
+ * the execPath basename is not a known JS runtime, fall back to a
249
+ * PATH-resolved `node`. If neither is reachable, return `null` and
250
+ * let ctx_doctor surface an actionable error.
251
+ *
252
+ * The cross-OS guard is the allowlist itself — NOT a `win32` check.
253
+ * OpenCode ships self-contained binaries on macOS and Linux too,
254
+ * and the bug reproduces identically there.
255
+ */
256
+ export function resolveJavascriptRuntime(bun, deps = {}) {
257
+ if (bun)
258
+ return bun;
259
+ const execPath = deps.execPath ?? process.execPath;
260
+ const cmdExists = deps.commandExists ?? commandExists;
261
+ // Cross-OS basename: split on either separator, strip optional `.exe`.
262
+ const base = execPath
263
+ .split(/[\\/]/)
264
+ .pop()
265
+ .replace(/\.exe$/i, "");
266
+ if (JS_RUNTIMES.has(base)) {
267
+ // Real JS runtime (node, bun, deno) — preserves #190 snap-Node fix
268
+ // because the snap wrapper's binary is literally named `node`.
269
+ //
270
+ // Issue #800 — liveness guard: on Homebrew, process.execPath points into
271
+ // the versioned Cellar (/opt/homebrew/Cellar/node/26.0.0/bin/node).
272
+ // `brew upgrade` + `brew cleanup` deletes the old Cellar, so the path
273
+ // dangles for the life of the already-running MCP server. If the path
274
+ // doesn't exist on disk, skip it and fall through to PATH node.
275
+ if (existsSync(execPath)) {
276
+ return execPath;
277
+ }
278
+ // Stale execPath (deleted Cellar, corrupted install, uninstall while
279
+ // process alive). Fall through to PATH resolution below.
280
+ }
281
+ // Host binary (opencode/kilo/etc.) — fall back to node on PATH.
282
+ if (cmdExists("node"))
283
+ return "node";
284
+ // No usable runtime — doctor + summary must handle null gracefully.
285
+ return null;
286
+ }
287
+ export function detectRuntimes() {
288
+ const hasBun = bunExists();
289
+ const bun = hasBun ? bunCommand() : null;
290
+ // Honor SHELL env var when it points at a real binary AND the basename is
291
+ // an allowlisted shell. Lets users with non-standard setups (custom bash,
292
+ // msys2, pwsh) pin context-mode to their preferred shell.
293
+ //
294
+ // Allowlist (PR #401 ops review): basename must match
295
+ // /^(bash|sh|zsh|dash|pwsh|powershell|cmd)(\.exe)?$/. Without this guard, an attacker
296
+ // who controls SHELL (e.g., supply-chain compromise of a profile script)
297
+ // could redirect the executor to /usr/bin/python or any arbitrary binary.
298
+ const userShell = process.env.SHELL;
299
+ const isWin = process.platform === "win32";
300
+ const windowsBash = isWin ? resolveWindowsBash() : null;
301
+ const shellOverride = userShell &&
302
+ existsSync(userShell) &&
303
+ isAllowlistedShell(userShell) &&
304
+ !(isWin && isWindowsWslBash(userShell)) &&
305
+ // Windows OpenSSH can inject the system cmd.exe as ambient SHELL. When
306
+ // Git Bash is installed, treating that as an explicit override breaks the
307
+ // POSIX shell executor path restored by #36/#384/#791.
308
+ !(isWin && windowsBash && isWindowsSystemCmd(userShell))
309
+ ? userShell
310
+ : null;
311
+ return {
312
+ javascript: resolveJavascriptRuntime(bun),
313
+ typescript: bun
314
+ ? bun
315
+ : commandExists("tsx")
316
+ ? "tsx"
317
+ : commandExists("ts-node")
318
+ ? "ts-node"
319
+ : null,
320
+ python: runnableExists("python3")
321
+ ? "python3"
322
+ : runnableExists("python")
323
+ ? "python"
324
+ : runnableExists("py")
325
+ ? "py"
326
+ : null,
327
+ shell: shellOverride ?? (isWin
328
+ ? resolveWindowsShell(windowsBash)
329
+ : commandExists("bash") ? "bash" : "sh"),
330
+ ruby: commandExists("ruby") ? "ruby" : null,
331
+ go: commandExists("go") ? "go" : null,
332
+ rust: commandExists("rustc") ? "rustc" : null,
333
+ php: commandExists("php") ? "php" : null,
334
+ perl: commandExists("perl") ? "perl" : null,
335
+ r: commandExists("Rscript")
336
+ ? "Rscript"
337
+ : commandExists("r")
338
+ ? "r"
339
+ : null,
340
+ elixir: commandExists("elixir") ? "elixir" : null,
341
+ csharp: commandExists("dotnet-script") ? "dotnet-script" : null,
342
+ };
343
+ }
344
+ export function hasBunRuntime() {
345
+ return bunExists();
346
+ }
347
+ /**
348
+ * Cached result of {@link resolveHookRuntime}. Populated on first call so the
349
+ * relatively expensive `bun --version` probe runs at most once per process.
350
+ * Reset via {@link resetHookRuntimeCache} (test-only).
351
+ */
352
+ let _hookRuntimeCache = null;
353
+ /**
354
+ * Reset the hook-runtime resolution cache. Test-only — production code
355
+ * should never call this. Vitest mocks `node:child_process`/`node:fs`
356
+ * per-test, so the per-process cache from a previous test would otherwise
357
+ * mask the mock and yield the host's real bun/node detection result.
358
+ */
359
+ export function resetHookRuntimeCache() {
360
+ _hookRuntimeCache = null;
361
+ }
362
+ /**
363
+ * Parse a `bun --version` stdout string and return true when the version is
364
+ * ≥1.0.0. Anything that doesn't match `MAJOR.MINOR.PATCH` (with optional
365
+ * pre-release suffix) returns false — we refuse to trust runtimes whose
366
+ * version we can't read because the failure mode is silent miscompare
367
+ * (e.g. a banner line getting interpreted as "0.0.0").
368
+ */
369
+ function bunVersionAtLeast1(versionOutput) {
370
+ const trimmed = versionOutput.trim();
371
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(trimmed);
372
+ if (!m)
373
+ return false;
374
+ const major = Number(m[1]);
375
+ return Number.isFinite(major) && major >= 1;
376
+ }
377
+ /**
378
+ * Resolve the JS runtime to use for spawning hook scripts (issue #738).
379
+ *
380
+ * Returns Bun when:
381
+ * - a bun binary is located via {@link bunCommand} (already handles the
382
+ * Windows .cmd shim trap from #506 + absolute path fallbacks), AND
383
+ * - `bun --version` exits 0 within the probe timeout, AND
384
+ * - the reported semver major is ≥1.
385
+ *
386
+ * Returns Node (`process.execPath`) on every other path — missing bun,
387
+ * version probe failure, version <1, malformed version banner. Silent
388
+ * fallback: never throws, never logs to stderr (a noisy log would clutter
389
+ * the same MCP boot output that #719 tightened up).
390
+ *
391
+ * Result is cached at module load so the cost is amortised across every
392
+ * hook command emission for the lifetime of the process. The cache also
393
+ * keeps the behaviour deterministic — if the user `brew uninstall bun`
394
+ * mid-session, the cached resolution stays valid for that session and the
395
+ * next MCP boot re-detects.
396
+ *
397
+ * Why bun ≥1.0 instead of "any bun":
398
+ * - Bun 0.x had multiple ESM/module-resolution regressions that broke
399
+ * dynamic `import()` inside hooks (and our hooks do ~7 dynamic imports
400
+ * in `pretooluse.mjs`).
401
+ * - 1.0 ships stable npm-compat that our better-sqlite3-adjacent code
402
+ * relies on indirectly (hooks share `ensure-deps.mjs` which is
403
+ * bun-safe past 1.0 but not 0.x).
404
+ *
405
+ * NOT used by:
406
+ * - `buildNodeCommand` — kept on `process.execPath` for openclaw doctor /
407
+ * upgrade hints which must invoke the better-sqlite3-loading CLI on
408
+ * Node (#543: bun cannot dlopen better-sqlite3's prebuilt .node).
409
+ * - `ensure-deps.mjs` — separate path, must stay on Node for the same
410
+ * reason.
411
+ * - `ctx_upgrade` — separate path, must stay on Node for the same reason.
412
+ */
413
+ /**
414
+ * Liveness-guarded Node path for the hook-runtime fallback (issue #841).
415
+ *
416
+ * `process.execPath` is pinned into every baked hook command because PATH
417
+ * resolution is unreliable for hooks (#190 snap-Node re-invokes the wrapper;
418
+ * #369 Windows Git Bash / MSYS can't resolve a bare `node`). But under a
419
+ * version manager (mise / asdf / nvm) execPath is a *version-pinned* absolute
420
+ * path — e.g. `~/.local/share/mise/installs/node/20.1.0/bin/node`. A routine
421
+ * `mise upgrade node` installs the next patch and DELETES the 20.1.0 dir, so
422
+ * the cached path dangles and every hook spawn fails with ENOENT — silently
423
+ * killing context-mode for that user.
424
+ *
425
+ * Same liveness-guard shape as the #800/#803 fix in
426
+ * {@link resolveJavascriptRuntime}: use the pinned execPath IFF it still
427
+ * exists on disk (preserving the #190/#369 reasons it was pinned), otherwise
428
+ * re-resolve a working `node` from PATH. The version manager's shim dir is on
429
+ * PATH and always points at the current patch, so bare `node` heals the host
430
+ * without a re-install. Falls back to the (stale) execPath only when no PATH
431
+ * node is reachable either — a strictly-better last resort than a dangling
432
+ * versioned path, and the doctor/upgrade flows surface the actionable error.
433
+ */
434
+ function liveNodeRuntime() {
435
+ if (existsSync(process.execPath)) {
436
+ return { path: process.execPath, isBun: false };
437
+ }
438
+ if (commandExists("node")) {
439
+ return { path: "node", isBun: false };
440
+ }
441
+ return { path: process.execPath, isBun: false };
442
+ }
443
+ export function resolveHookRuntime() {
444
+ if (_hookRuntimeCache)
445
+ return _hookRuntimeCache;
446
+ const nodeFallback = liveNodeRuntime();
447
+ try {
448
+ if (!bunExists()) {
449
+ _hookRuntimeCache = nodeFallback;
450
+ return _hookRuntimeCache;
451
+ }
452
+ const bun = bunCommand();
453
+ // Re-use the same probe shape as getVersion (POSIX execFile, Windows
454
+ // execSync quoted string for DEP0190 compliance).
455
+ let versionOutput;
456
+ try {
457
+ if (process.platform === "win32") {
458
+ const out = execSync(`"${bun}" --version`, {
459
+ encoding: "utf-8",
460
+ stdio: ["pipe", "pipe", "pipe"],
461
+ timeout: 5000,
462
+ });
463
+ versionOutput = String(out);
464
+ }
465
+ else {
466
+ const out = execFileSync(bun, ["--version"], {
467
+ encoding: "utf-8",
468
+ stdio: ["pipe", "pipe", "pipe"],
469
+ timeout: 5000,
470
+ });
471
+ versionOutput = String(out);
472
+ }
473
+ }
474
+ catch {
475
+ _hookRuntimeCache = nodeFallback;
476
+ return _hookRuntimeCache;
477
+ }
478
+ if (!bunVersionAtLeast1(versionOutput)) {
479
+ _hookRuntimeCache = nodeFallback;
480
+ return _hookRuntimeCache;
481
+ }
482
+ _hookRuntimeCache = { path: bun, isBun: true };
483
+ return _hookRuntimeCache;
484
+ }
485
+ catch {
486
+ _hookRuntimeCache = nodeFallback;
487
+ return _hookRuntimeCache;
488
+ }
489
+ }
490
+ export function getRuntimeSummary(runtimes) {
491
+ const lines = [];
492
+ const bunPreferred = runtimes.javascript?.endsWith("bun") ?? false;
493
+ if (runtimes.javascript) {
494
+ lines.push(` JavaScript: ${runtimes.javascript} (${getVersion(runtimes.javascript)})${bunPreferred ? " ⚡" : ""}`);
495
+ }
496
+ else {
497
+ // #731: host binary (opencode/kilo) AND no PATH-resolvable node.
498
+ // Surface actionable guidance instead of rendering literal `null`.
499
+ lines.push(` JavaScript: not available (install node or bun — host process is not a JS runtime)`);
500
+ }
501
+ if (runtimes.typescript) {
502
+ lines.push(` TypeScript: ${runtimes.typescript} (${getVersion(runtimes.typescript)})`);
503
+ }
504
+ else {
505
+ lines.push(` TypeScript: not available (install bun, tsx, or ts-node)`);
506
+ }
507
+ if (runtimes.python) {
508
+ lines.push(` Python: ${runtimes.python} (${getVersion(runtimes.python)})`);
509
+ }
510
+ else {
511
+ lines.push(` Python: not available`);
512
+ }
513
+ lines.push(` Shell: ${runtimes.shell} (${getVersion(runtimes.shell)})`);
514
+ // Optional runtimes — only show if available
515
+ if (runtimes.ruby)
516
+ lines.push(` Ruby: ${runtimes.ruby} (${getVersion(runtimes.ruby)})`);
517
+ if (runtimes.go)
518
+ lines.push(` Go: ${runtimes.go} (${getVersion(runtimes.go, ["version"])})`);
519
+ if (runtimes.rust)
520
+ lines.push(` Rust: ${runtimes.rust} (${getVersion(runtimes.rust)})`);
521
+ if (runtimes.php)
522
+ lines.push(` PHP: ${runtimes.php} (${getVersion(runtimes.php)})`);
523
+ if (runtimes.perl)
524
+ lines.push(` Perl: ${runtimes.perl} (${getVersion(runtimes.perl)})`);
525
+ if (runtimes.r)
526
+ lines.push(` R: ${runtimes.r} (${getVersion(runtimes.r)})`);
527
+ if (runtimes.elixir)
528
+ lines.push(` Elixir: ${runtimes.elixir} (${getVersion(runtimes.elixir)})`);
529
+ if (runtimes.csharp)
530
+ lines.push(` C#: ${runtimes.csharp} (${getVersion(runtimes.csharp)})`);
531
+ if (!bunPreferred) {
532
+ lines.push("");
533
+ lines.push(" Tip: Install Bun for 3-5x faster JS/TS execution → https://bun.sh");
534
+ }
535
+ return lines.join("\n");
536
+ }
537
+ export function getAvailableLanguages(runtimes) {
538
+ const langs = ["javascript", "shell"];
539
+ if (runtimes.typescript)
540
+ langs.push("typescript");
541
+ if (runtimes.python)
542
+ langs.push("python");
543
+ if (runtimes.ruby)
544
+ langs.push("ruby");
545
+ if (runtimes.go)
546
+ langs.push("go");
547
+ if (runtimes.rust)
548
+ langs.push("rust");
549
+ if (runtimes.php)
550
+ langs.push("php");
551
+ if (runtimes.perl)
552
+ langs.push("perl");
553
+ if (runtimes.r)
554
+ langs.push("r");
555
+ if (runtimes.elixir)
556
+ langs.push("elixir");
557
+ if (runtimes.csharp)
558
+ langs.push("csharp");
559
+ return langs;
560
+ }
561
+ export function buildCommand(runtimes, language, filePath) {
562
+ switch (language) {
563
+ case "javascript":
564
+ if (!runtimes.javascript) {
565
+ // #731: in-process plugin host (opencode/kilo binary) AND no
566
+ // PATH-resolvable node. Refuse early with an actionable error
567
+ // instead of spawning the host binary (the original bug shape).
568
+ throw new Error("No JavaScript runtime available. Install Node.js or Bun on PATH (the host process is not itself a JS runtime).");
569
+ }
570
+ return BUN_BASENAME.test(runtimeBasename(runtimes.javascript))
571
+ ? [runtimes.javascript, "run", filePath]
572
+ : [runtimes.javascript, filePath];
573
+ case "typescript":
574
+ if (!runtimes.typescript) {
575
+ throw new Error("No TypeScript runtime available. Install one of: bun (recommended), tsx (npm i -g tsx), or ts-node.");
576
+ }
577
+ if (BUN_BASENAME.test(runtimeBasename(runtimes.typescript)))
578
+ return [runtimes.typescript, "run", filePath];
579
+ if (runtimes.typescript === "tsx")
580
+ return ["tsx", filePath];
581
+ return ["ts-node", filePath];
582
+ case "python":
583
+ if (!runtimes.python) {
584
+ throw new Error("No Python runtime available. Install python3 or python.");
585
+ }
586
+ return [runtimes.python, filePath];
587
+ case "shell": {
588
+ // Re-evaluate platform per call so detection-time and command-build-time
589
+ // can be tested independently (and to allow tests to stub process.platform).
590
+ const winNow = process.platform === "win32";
591
+ if (winNow) {
592
+ const shellName = runtimes.shell.toLowerCase();
593
+ if (shellName.includes("bash") || shellName.endsWith("/sh") || shellName.endsWith("\\sh.exe")) {
594
+ // bash -c "source 'path'" — avoids MSYS2 path mangling on non-C:
595
+ // drives. When bash.exe receives a script as a direct argument,
596
+ // MSYS rewrites D:\tmp\script → D:\c\tmp\script and execution
597
+ // breaks. The -c flag prevents MSYS from touching the file arg.
598
+ // Single-quote escape: ' → '\''
599
+ const escaped = filePath.replace(/'/g, "'\\''");
600
+ return [runtimes.shell, "-c", `source '${escaped}'`];
601
+ }
602
+ if (shellName.includes("powershell") || shellName.includes("pwsh")) {
603
+ // Windows PowerShell defaults to Restricted when no execution policy
604
+ // is configured. Use process-scoped Bypass so generated temp scripts
605
+ // run without changing machine/user policy.
606
+ return [runtimes.shell, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", filePath];
607
+ }
608
+ const shellBase = shellName.split(/[\\/]/).pop() ?? shellName;
609
+ if (shellBase === "cmd" || shellBase === "cmd.exe") {
610
+ return [runtimes.shell, "/d", "/s", "/c", filePath];
611
+ }
612
+ // Other Windows shells: direct file.
613
+ }
614
+ return [runtimes.shell, filePath];
615
+ }
616
+ case "ruby":
617
+ if (!runtimes.ruby) {
618
+ throw new Error("Ruby not available. Install ruby.");
619
+ }
620
+ return [runtimes.ruby, filePath];
621
+ case "go":
622
+ if (!runtimes.go) {
623
+ throw new Error("Go not available. Install go.");
624
+ }
625
+ return ["go", "run", filePath];
626
+ case "rust": {
627
+ if (!runtimes.rust) {
628
+ throw new Error("Rust not available. Install rustc via https://rustup.rs");
629
+ }
630
+ // Rust needs compile + run — handled specially in executor
631
+ return ["__rust_compile_run__", filePath];
632
+ }
633
+ case "php":
634
+ if (!runtimes.php) {
635
+ throw new Error("PHP not available. Install php.");
636
+ }
637
+ return ["php", filePath];
638
+ case "perl":
639
+ if (!runtimes.perl) {
640
+ throw new Error("Perl not available. Install perl.");
641
+ }
642
+ return ["perl", filePath];
643
+ case "r":
644
+ if (!runtimes.r) {
645
+ throw new Error("R not available. Install R / Rscript.");
646
+ }
647
+ return [runtimes.r, filePath];
648
+ case "elixir":
649
+ if (!runtimes.elixir) {
650
+ throw new Error("Elixir not available. Install elixir.");
651
+ }
652
+ return ["elixir", filePath];
653
+ case "csharp":
654
+ if (!runtimes.csharp) {
655
+ throw new Error("C# not available. Install dotnet-script via `dotnet tool install -g dotnet-script`.");
656
+ }
657
+ return [runtimes.csharp, filePath];
658
+ }
659
+ }