@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,248 @@
1
+ /**
2
+ * Plugin cache integrity check (Algo-D4 + Algo-D5).
3
+ *
4
+ * Algorithmic defense against #550: a partial install (interrupted npm
5
+ * install, broken marketplace pull, half-finished /ctx-upgrade) leaves
6
+ * start.mjs spawnable but a critical sibling (server.bundle.mjs,
7
+ * cli.bundle.mjs, hooks/<event>.mjs, …) missing. The MCP child then
8
+ * dies silently downstream and the user sees an opaque "MCP server
9
+ * failed to start" with no actionable signal.
10
+ *
11
+ * The expected sibling tree is DERIVED from `package.json files[]` —
12
+ * the npm publish source of truth. Adding a new entry there auto-
13
+ * extends the integrity check; no parallel hardcoded list to maintain
14
+ * (the trap that bites every project that hand-rolls "list of files
15
+ * that must exist at runtime").
16
+ *
17
+ * Two consumers:
18
+ * 1. start.mjs at boot — calls assertPluginCacheIntegrity, on !ok
19
+ * writes a structured CONTEXT_MODE_PARTIAL_INSTALL stderr block
20
+ * and exits 2. Fail-fast — the alternative is a downstream stack
21
+ * trace from `import("./server.bundle.mjs")` that hides the
22
+ * actual root cause.
23
+ * 2. src/cli.ts ctx doctor (Algo-D5) — same helper, same answer,
24
+ * surfaced as a HealthCheck so users get the diagnostic without
25
+ * restarting the MCP server.
26
+ *
27
+ * Pure JS, Node.js built-ins only. Ships in package.json files[] so
28
+ * users running off the npm tarball get the same code path the
29
+ * developer ran during `pretest`.
30
+ */
31
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
32
+ import { join, relative, sep } from "node:path";
33
+
34
+ /**
35
+ * Walk a directory recursively, returning a flat list of relative file
36
+ * paths (using `/` as separator inside the returned strings). Skips
37
+ * unreadable entries silently — the integrity check operates on what
38
+ * IS readable; missing entries are reported by the caller.
39
+ */
40
+ function listFilesRecursive(absDir, baseAbs) {
41
+ const out = [];
42
+ let entries;
43
+ try {
44
+ entries = readdirSync(absDir);
45
+ } catch {
46
+ return out; // unreadable — caller will report the parent as missing
47
+ }
48
+ for (const name of entries) {
49
+ const full = join(absDir, name);
50
+ let st;
51
+ try {
52
+ st = statSync(full);
53
+ } catch {
54
+ continue;
55
+ }
56
+ if (st.isDirectory()) {
57
+ out.push(...listFilesRecursive(full, baseAbs));
58
+ } else {
59
+ out.push(relative(baseAbs, full));
60
+ }
61
+ }
62
+ return out;
63
+ }
64
+
65
+ /**
66
+ * Compute the expected sibling tree for a given pluginRoot, derived
67
+ * from the supplied `package.json files[]` array.
68
+ *
69
+ * Algorithm:
70
+ * - Each entry in files[] is resolved against pluginRoot.
71
+ * - If it points to a directory → list every file inside recursively.
72
+ * - If it points to a file → kept as-is.
73
+ * - Entries that don't exist at probe-time are EXCLUDED from the
74
+ * manifest (they show up as `missing` in the assert step instead).
75
+ * This avoids the trap of "manifest contains paths that have never
76
+ * existed" — the manifest is a snapshot of WHAT IS, not WHAT WAS
77
+ * PUBLISHED.
78
+ *
79
+ * Returns relative paths (relative to pluginRoot). Used by both
80
+ * assertPluginCacheIntegrity and the doctor surface.
81
+ */
82
+ export function derivePluginManifest({ pkg, pluginRoot }) {
83
+ if (!pkg || !Array.isArray(pkg.files)) return [];
84
+ const manifest = new Set();
85
+ for (const entry of pkg.files) {
86
+ if (typeof entry !== "string" || !entry) continue;
87
+ const absEntry = join(pluginRoot, entry);
88
+ if (!existsSync(absEntry)) continue;
89
+ let st;
90
+ try {
91
+ st = statSync(absEntry);
92
+ } catch {
93
+ continue;
94
+ }
95
+ if (st.isDirectory()) {
96
+ for (const f of listFilesRecursive(absEntry, pluginRoot)) manifest.add(f);
97
+ } else {
98
+ manifest.add(entry);
99
+ }
100
+ }
101
+ return [...manifest];
102
+ }
103
+
104
+ /**
105
+ * LEGACY_FALLBACK — the v1.0.126 hardcoded REQUIRED_RUNTIME_SIBLINGS,
106
+ * preserved verbatim. Forms the union seed for the algorithmic set so
107
+ * the post-558 contract is strictly additive over the pre-558 contract
108
+ * (no required sibling ever silently disappears).
109
+ *
110
+ * Also acts as a safety net when `package.json` is unreadable — the
111
+ * boot gate stays loud even if the publish manifest is corrupted.
112
+ */
113
+ const LEGACY_FALLBACK = Object.freeze([
114
+ "server.bundle.mjs",
115
+ "cli.bundle.mjs",
116
+ join("hooks", "pretooluse.mjs"),
117
+ join("hooks", "posttooluse.mjs"),
118
+ join("hooks", "precompact.mjs"),
119
+ join("hooks", "sessionstart.mjs"),
120
+ join("hooks", "userpromptsubmit.mjs"),
121
+ ]);
122
+
123
+ /**
124
+ * SOFT_FALLBACK_BUNDLES — bundles that already implement
125
+ * bundle-first / build-fallback resolution (via session-loaders.mjs or
126
+ * session-helpers.mjs). Their absence on a published install is
127
+ * gracefully recoverable, so they MUST NOT join the fail-fast boot
128
+ * gate — the gate would refuse to start a working install.
129
+ *
130
+ * The security bundle is intentionally NOT here: its absence creates a
131
+ * silent fail-OPEN regression (#558), so it IS boot-critical.
132
+ */
133
+ const SOFT_FALLBACK_BUNDLES = new Set([
134
+ "hooks/session-extract.bundle.mjs",
135
+ "hooks/session-snapshot.bundle.mjs",
136
+ "hooks/session-db.bundle.mjs",
137
+ "hooks/session-attribution.bundle.mjs",
138
+ ]);
139
+
140
+ /**
141
+ * Algorithmically extract every esbuild output path from
142
+ * `package.json scripts.bundle`. The bundle script is the SINGLE
143
+ * SOURCE OF TRUTH for "what bundles this build produces" — parsing
144
+ * its `--outfile=…` arguments avoids the parallel-list trap that
145
+ * bit Algo-D4 v1.0.126 (the hardcoded REQUIRED list lagged the
146
+ * actual bundle output).
147
+ *
148
+ * Returns POSIX-style relative paths (forward slashes) for stable
149
+ * comparison with SOFT_FALLBACK_BUNDLES. Caller normalizes to
150
+ * `path.join` shape before pluginRoot-relative resolution.
151
+ */
152
+ function extractBundleOutfiles(pkg) {
153
+ const script = pkg?.scripts?.bundle;
154
+ if (typeof script !== "string") return [];
155
+ const out = new Set();
156
+ // Match every `--outfile=<path>` token (path is whitespace-delimited
157
+ // because the script chains commands with `&&`).
158
+ const re = /--outfile=(\S+)/g;
159
+ let m;
160
+ while ((m = re.exec(script)) !== null) {
161
+ out.add(m[1]);
162
+ }
163
+ return [...out];
164
+ }
165
+
166
+ /**
167
+ * Algorithmic — derive the boot-critical sibling set as the union of:
168
+ * 1. LEGACY_FALLBACK (the v1.0.126 contract, preserved verbatim).
169
+ * 2. Every esbuild output path from `package.json scripts.bundle`
170
+ * that is NOT in SOFT_FALLBACK_BUNDLES.
171
+ *
172
+ * Why algorithmic instead of hardcoded:
173
+ *
174
+ * v1.0.126 shipped Algo-D4 with a hardcoded REQUIRED_RUNTIME_SIBLINGS
175
+ * array that omitted `hooks/security.bundle.mjs` (the bundle didn't
176
+ * ship until v1.0.127). The hardcoded list would need manual
177
+ * extension every time a runtime bundle is added — the same trap
178
+ * would re-bite the next bundle. Deriving from `scripts.bundle`
179
+ * closes the trap: any new bundle output is auto-gated unless it
180
+ * joins the soft-fallback whitelist (which is itself an explicit
181
+ * architectural decision, not a maintenance burden). (#558)
182
+ *
183
+ * Returns OS-native-separator relative paths (suitable for
184
+ * `path.join(pluginRoot, …)`).
185
+ *
186
+ * If `package.json` is unreadable, returns LEGACY_FALLBACK as a
187
+ * safety net so the boot gate never goes silent due to a parse
188
+ * error in the publish manifest.
189
+ */
190
+ export function getRequiredRuntimeSiblings(pluginRoot) {
191
+ let pkg;
192
+ try {
193
+ pkg = JSON.parse(readFileSync(join(pluginRoot, "package.json"), "utf-8"));
194
+ } catch {
195
+ return [...LEGACY_FALLBACK];
196
+ }
197
+ const required = new Set(LEGACY_FALLBACK);
198
+ for (const outfile of extractBundleOutfiles(pkg)) {
199
+ // Normalize to POSIX for soft-fallback membership check —
200
+ // scripts.bundle is hand-authored with forward slashes already,
201
+ // but be defensive in case a Windows-authored package.json ever
202
+ // reaches us.
203
+ const posix = outfile.split(sep).join("/");
204
+ if (SOFT_FALLBACK_BUNDLES.has(posix)) continue;
205
+ // Convert back to OS-native sep for downstream filesystem ops.
206
+ required.add(posix.split("/").join(sep));
207
+ }
208
+ return [...required];
209
+ }
210
+
211
+ /**
212
+ * Verify boot-critical siblings exist at pluginRoot.
213
+ *
214
+ * Returns `{ ok, missing }`. Pure — does NOT touch process.exit or
215
+ * stderr. The caller (start.mjs at boot, src/cli.ts at doctor) decides
216
+ * the failure surface (fail-fast exit 2 vs. doctor diagnostic).
217
+ *
218
+ * Required-set is computed by `getRequiredRuntimeSiblings()` —
219
+ * algorithmically derived from `package.json files[]` filtered to the
220
+ * RUNTIME_CRITICAL_PATTERN. Drift between publish manifest and runtime
221
+ * contract becomes architecturally impossible (#558).
222
+ */
223
+ export function assertPluginCacheIntegrity({ pluginRoot }) {
224
+ const missing = [];
225
+ for (const rel of getRequiredRuntimeSiblings(pluginRoot)) {
226
+ const abs = join(pluginRoot, rel);
227
+ if (!existsSync(abs)) missing.push(abs);
228
+ }
229
+ return { ok: missing.length === 0, missing };
230
+ }
231
+
232
+ /**
233
+ * Format the structured stderr block start.mjs emits when integrity
234
+ * fails. Marker line `CONTEXT_MODE_PARTIAL_INSTALL` lets external
235
+ * monitoring grep for the exact failure mode without parsing free-form
236
+ * text. Keep the format stable across versions.
237
+ */
238
+ export function formatPartialInstallReport({ pluginRoot, missing }) {
239
+ const lines = [
240
+ "CONTEXT_MODE_PARTIAL_INSTALL",
241
+ ` pluginRoot: ${pluginRoot}`,
242
+ " missing:",
243
+ ...missing.map((m) => ` - ${m}`),
244
+ " fix: rm -rf the install dir and re-pull (marketplace) or run `npm install -g @mxalbert/context-mode` again.",
245
+ "",
246
+ ];
247
+ return lines.join("\n");
248
+ }
@@ -0,0 +1,397 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * postinstall — cross-platform post-install tasks
4
+ *
5
+ * 1. OpenClaw detection (print helper message)
6
+ * 2. Windows global install: fix broken bin→node_modules path
7
+ * when nvm4w places the shim and node_modules in different directories.
8
+ * Creates a directory junction so npm's %~dp0\node_modules\... resolves.
9
+ */
10
+
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, symlinkSync, lstatSync, unlinkSync } from "node:fs";
12
+ import { execSync } from "node:child_process";
13
+ import { dirname, resolve, join, sep } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ import { homedir } from "node:os";
16
+ import { healBetterSqlite3Binding } from "./heal-better-sqlite3.mjs";
17
+ import { healInstalledPlugins, healSettingsEnabledPlugins, healPluginJsonMcpServers, sweepStaleMcpJson } from "./heal-installed-plugins.mjs";
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const pkgRoot = resolve(__dirname, "..");
21
+
22
+ // npm package identity (v2.0.0 fork rename). The plugin/display id stays
23
+ // "context-mode"; only the npm package name is scoped.
24
+ const PACKAGE_NAME = "@mxalbert/context-mode";
25
+ const PLUGIN_ID = "context-mode";
26
+ // Claude Code registry key: "<pluginId>@<npmPackage>". A scoped package
27
+ // yields a double-@ key ("context-mode@@mxalbert/context-mode").
28
+ const PLUGIN_KEY = `${PLUGIN_ID}@${PACKAGE_NAME}`;
29
+
30
+ // ── -2. Issue #564 — Linux SIGSEGV class hard-fail (v1.0.132) ────────
31
+ // On Linux + Node < 22.5 + no Bun, better-sqlite3's native addon is
32
+ // vulnerable to V8 calling `madvise(MADV_DONTNEED)` on memory ranges
33
+ // that overlap the addon's `.got.plt` section, corrupting resolved
34
+ // symbol addresses and causing sporadic SIGSEGV (1-4/hour) — see
35
+ // https://github.com/nodejs/node/issues/62515 and our internal #564.
36
+ //
37
+ // node:sqlite (built-in, no native addon, no .got.plt to corrupt) ships
38
+ // from Node 22.5 onward — that is the contract `hasModernSqlite()` in
39
+ // src/db-base.ts encodes. Six prior fixes (#228, #331, #461, #540,
40
+ // #551, #556) silently assumed users had Node >= 22.5 on Linux; #564
41
+ // is the second confirmed report (after #556) of the same SIGSEGV
42
+ // class on Node 20.
43
+ //
44
+ // The architect mandate for v1.0.132 is HARD-FAIL, not warn-then-
45
+ // degrade. `engines.node >= 22.5.0` in package.json is cosmetic under
46
+ // the default npm `engine-strict=false`, so the contract has to be
47
+ // enforced HERE — preinstall/postinstall is the only place that can
48
+ // `process.exit(1)` across npm/pnpm/yarn.
49
+ //
50
+ // Linux + Bun is allowed through (bun:sqlite sidesteps better-sqlite3
51
+ // entirely). Non-Linux platforms are unaffected by the madvise bug
52
+ // and pass through unchanged.
53
+ {
54
+ const isLinux = process.platform === "linux";
55
+ const hasBun =
56
+ typeof globalThis.Bun !== "undefined" ||
57
+ typeof process.versions.bun === "string";
58
+ const [majStr, minStr] = (process.versions.node ?? "0.0.0").split(".");
59
+ const major = Number(majStr);
60
+ const minor = Number(minStr);
61
+ const hasModernNode =
62
+ Number.isFinite(major) &&
63
+ Number.isFinite(minor) &&
64
+ (major > 22 || (major === 22 && minor >= 5));
65
+ if (isLinux && !hasBun && !hasModernNode) {
66
+ process.stderr.write(
67
+ "\n" +
68
+ "context-mode: install aborted\n" +
69
+ " Linux + Node " + (process.versions.node ?? "?") + " is unsupported.\n" +
70
+ " context-mode requires Node.js >= 22.5 (or Bun) on Linux to avoid the\n" +
71
+ " V8 madvise(MADV_DONTNEED) SIGSEGV affecting better-sqlite3 (1-4/hour).\n" +
72
+ " Tracking: https://github.com/nodejs/node/issues/62515\n" +
73
+ " https://github.com/mxalbert1996/context-mode/issues/564\n" +
74
+ "\n" +
75
+ " Fix: upgrade Node (recommended)\n" +
76
+ " nvm install 22.5 && nvm use 22.5\n" +
77
+ " npm install -g @mxalbert/context-mode\n" +
78
+ "\n" +
79
+ " Or: run under Bun\n" +
80
+ " curl -fsSL https://bun.sh/install | bash\n" +
81
+ " bun add -g @mxalbert/context-mode\n" +
82
+ "\n",
83
+ );
84
+ process.exit(1);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * True when running as a real `npm install -g @mxalbert/context-mode`. We use this
90
+ * to keep contributors' local `npm install` runs from rewriting their HOME's
91
+ * Claude Code registry (would be very surprising during dev).
92
+ *
93
+ * Heuristic: npm sets `npm_config_global=true` for global installs AND the
94
+ * package directory has no nearby `.git` (a contributor's clone always
95
+ * does). Both signals must agree.
96
+ */
97
+ function isGlobalInstall() {
98
+ if (process.env.npm_config_global !== "true") return false;
99
+ // Walk up a few levels looking for .git — contributors always have one.
100
+ let dir = pkgRoot;
101
+ for (let i = 0; i < 4; i++) {
102
+ if (existsSync(join(dir, ".git"))) return false;
103
+ const parent = dirname(dir);
104
+ if (parent === dir) break;
105
+ dir = parent;
106
+ }
107
+ return true;
108
+ }
109
+
110
+ /**
111
+ * Validate that a path is safe to interpolate into a cmd.exe command.
112
+ * Rejects characters that could enable command injection via cmd.exe.
113
+ */
114
+ function isSafeWindowsPath(p) {
115
+ return !/[&|<>"^%\r\n]/.test(p);
116
+ }
117
+
118
+ // ── -1. v1.0.114 hotfix — installed_plugins.json registry repair ─────
119
+ // /ctx-upgrade in v1.0.113 poisoned the registry (entry.version drifted
120
+ // + enabledPlugins emptied), making Claude Code's plugin loader skip
121
+ // context-mode entirely. start.mjs HEAL 3+4 fix this on every MCP boot,
122
+ // but already-broken users have no MCP to boot — they need the heal to
123
+ // run from npm postinstall. Shared module so both call sites stay in
124
+ // sync. Only runs in real `npm install -g` to avoid surprising
125
+ // contributors. Best effort, never blocks install. (#46915 follow-up.)
126
+ if (isGlobalInstall()) {
127
+ try {
128
+ const registryPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
129
+ const pluginCacheRoot = resolve(homedir(), ".claude", "plugins", "cache");
130
+ const result = healInstalledPlugins({
131
+ registryPath,
132
+ pluginCacheRoot,
133
+ pluginKey: PLUGIN_KEY,
134
+ });
135
+ if (result.skipped === "no-registry") {
136
+ // Standalone npm user (no Claude Code) — silent success.
137
+ process.stderr.write("context-mode: install OK, no Claude Code registry found\n");
138
+ } else if (result.error) {
139
+ process.stderr.write(`context-mode: install OK, registry heal skipped (${result.error})\n`);
140
+ } else if (result.healed && result.healed.length > 0) {
141
+ process.stderr.write(`context-mode: healed installed_plugins.json (${result.healed.join(", ")})\n`);
142
+ } else {
143
+ process.stderr.write("context-mode: install OK, no heal needed\n");
144
+ }
145
+ } catch (err) {
146
+ // Never block install on a heal failure.
147
+ try {
148
+ process.stderr.write(`context-mode: install OK, heal aborted (${(err && err.message) || err})\n`);
149
+ } catch { /* truly best effort */ }
150
+ }
151
+
152
+ // v1.0.116: also heal settings.json.enabledPlugins (the file Claude Code's
153
+ // plugin loader actually reads). v1.0.114 only touched installed_plugins.json.
154
+ try {
155
+ const settingsPath = resolve(homedir(), ".claude", "settings.json");
156
+ const r = healSettingsEnabledPlugins({
157
+ settingsPath,
158
+ pluginKey: PLUGIN_KEY,
159
+ });
160
+ if (r.healed && r.healed.length > 0) {
161
+ process.stderr.write(`context-mode: healed settings.json (${r.healed.join(", ")})\n`);
162
+ }
163
+ // skipped/error: silent — already covered by the prior heal's stderr line.
164
+ } catch { /* never block install */ }
165
+
166
+ // v1.0.119: Layer 5b (Issue #523). Heal .claude-plugin/plugin.json's
167
+ // mcpServers["context-mode"].args[0] when /ctx-upgrade left a tmpdir-prefixed
168
+ // path baked in. Iterates EVERY installed cache entry's installPath so
169
+ // already-broken users self-recover the next time `npm install -g @mxalbert/context-mode`
170
+ // runs. Best effort, never blocks install.
171
+ try {
172
+ const ipPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
173
+ const cacheRoot = resolve(homedir(), ".claude", "plugins", "cache");
174
+ if (existsSync(ipPath)) {
175
+ const ip = JSON.parse(readFileSync(ipPath, "utf-8"));
176
+ const entries = (ip && ip.plugins && ip.plugins[PLUGIN_KEY]) || [];
177
+ let healedAny = false;
178
+ if (Array.isArray(entries)) {
179
+ for (const entry of entries) {
180
+ const installPath = entry && entry.installPath;
181
+ if (typeof installPath !== "string" || !installPath) continue;
182
+ try {
183
+ const r = healPluginJsonMcpServers({
184
+ pluginRoot: installPath,
185
+ pluginCacheRoot: cacheRoot,
186
+ pluginKey: PLUGIN_KEY,
187
+ });
188
+ if (r && Array.isArray(r.healed) && r.healed.length > 0) {
189
+ healedAny = true;
190
+ }
191
+ } catch { /* per-entry best effort */ }
192
+ }
193
+ }
194
+ // Issue #609 — Layer 6: sweep stale `.mcp.json` files from every
195
+ // per-version cache dir. Replaces the previous per-entry healMcpJsonArgs
196
+ // loop (v1.0.122) — `.mcp.json` is no longer written from cli.ts so
197
+ // remaining files in the cache are stale carry-forwards that block
198
+ // future auto-updates from working cleanly. Single sweep per install.
199
+ try {
200
+ const sweepResult = sweepStaleMcpJson({
201
+ pluginCacheRoot: cacheRoot,
202
+ pluginKey: PLUGIN_KEY,
203
+ });
204
+ if (sweepResult && Array.isArray(sweepResult.removed) && sweepResult.removed.length > 0) {
205
+ process.stderr.write(`context-mode: swept ${sweepResult.removed.length} stale .mcp.json file(s) (Issue #609)\n`);
206
+ }
207
+ } catch { /* never block install */ }
208
+ if (healedAny) {
209
+ process.stderr.write("context-mode: healed mcpServers args (Issue #523)\n");
210
+ }
211
+ }
212
+ } catch { /* never block install */ }
213
+ }
214
+
215
+ // ── 0. Self-heal Layer 3: Backward symlink for stale registry (anthropics/claude-code#46915) ──
216
+ // When this install completes, installed_plugins.json may still point to an old
217
+ // non-existent path. Create a symlink from that old path → our new directory.
218
+ try {
219
+ const ipPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
220
+ if (existsSync(ipPath)) {
221
+ const ip = JSON.parse(readFileSync(ipPath, "utf-8"));
222
+ const cacheRoot = resolve(homedir(), ".claude", "plugins", "cache");
223
+ for (const [key, entries] of Object.entries(ip.plugins || {})) {
224
+ if (key !== PLUGIN_KEY) continue;
225
+ for (const entry of entries) {
226
+ const rp = entry.installPath;
227
+ if (!rp || existsSync(rp)) continue;
228
+ // Path traversal guard
229
+ if (!resolve(rp).startsWith(cacheRoot + sep)) continue;
230
+ // Remove dangling symlink
231
+ try { if (lstatSync(rp).isSymbolicLink()) unlinkSync(rp); } catch {}
232
+ const rpParent = dirname(rp);
233
+ if (!existsSync(rpParent)) mkdirSync(rpParent, { recursive: true });
234
+ try {
235
+ symlinkSync(pkgRoot, rp, process.platform === "win32" ? "junction" : undefined);
236
+ } catch { /* may fail if path is locked or permissions */ }
237
+ }
238
+ }
239
+ }
240
+ } catch { /* best effort — don't block install */ }
241
+
242
+ // ── 1. OpenClaw detection ────────────────────────────────────────────
243
+ if (process.env.OPENCLAW_STATE_DIR) {
244
+ console.log("\n OpenClaw detected. Run: npm run install:openclaw\n");
245
+ }
246
+
247
+ // ── 2. Windows global install — nvm4w junction fix ───────────────────
248
+ // npm's .cmd shim resolves modules via %~dp0\node_modules\<pkg>\...
249
+ // On nvm4w the shim lives at C:\nvm4w\nodejs\ but node_modules is at
250
+ // C:\Users\<USER>\AppData\Roaming\npm\node_modules\. The relative path
251
+ // breaks because they're on different prefixes.
252
+ //
253
+ // Fix: detect the mismatch and create a directory junction so the shim
254
+ // can reach us through the expected relative path.
255
+
256
+ if (process.platform === "win32" && process.env.npm_config_global === "true") {
257
+ try {
258
+ // npm prefix is where both the .cmd shims and node_modules live
259
+ // Use npm_config_prefix env (set during install) or fall back to `npm config get prefix`
260
+ // Note: `npm bin -g` was removed in npm v9+, so we use prefix instead
261
+ const prefix = (
262
+ process.env.npm_config_prefix ||
263
+ execSync("npm config get prefix", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim()
264
+ );
265
+
266
+ const actualPkgDir = pkgRoot;
267
+
268
+ // npm's .cmd shim uses %~dp0\node_modules\<pkg>\... to find the entry point.
269
+ // On nvm4w, stale shims at C:\nvm4w\nodejs\ may exist alongside correct ones
270
+ // at the npm prefix. We create junctions at ALL known shim locations.
271
+ const shimDirs = new Set([prefix]);
272
+
273
+ // Detect stale shim locations via `where` command
274
+ try {
275
+ const whereOutput = execSync("where context-mode.cmd", {
276
+ encoding: "utf-8",
277
+ stdio: ["pipe", "pipe", "pipe"],
278
+ }).trim();
279
+ for (const line of whereOutput.split(/\r?\n/)) {
280
+ if (line.endsWith("context-mode.cmd")) {
281
+ shimDirs.add(dirname(line));
282
+ }
283
+ }
284
+ } catch { /* where may fail if not installed yet */ }
285
+
286
+ for (const shimDir of shimDirs) {
287
+ // Scoped package: npm places it at node_modules/@mxalbert/context-mode.
288
+ const expectedPkgDir = join(shimDir, "node_modules", PACKAGE_NAME);
289
+
290
+ if (
291
+ resolve(expectedPkgDir).toLowerCase() !== resolve(actualPkgDir).toLowerCase() &&
292
+ !existsSync(expectedPkgDir)
293
+ ) {
294
+ const expectedNodeModules = join(shimDir, "node_modules");
295
+ if (!existsSync(expectedNodeModules)) {
296
+ mkdirSync(expectedNodeModules, { recursive: true });
297
+ }
298
+ // Ensure the @scope parent directory exists before creating the junction.
299
+ if (!existsSync(dirname(expectedPkgDir))) {
300
+ mkdirSync(dirname(expectedPkgDir), { recursive: true });
301
+ }
302
+
303
+ // Create directory junction (no admin privileges needed on Windows 10+)
304
+ // Validate paths to prevent cmd.exe injection via shell metacharacters
305
+ if (!isSafeWindowsPath(expectedPkgDir) || !isSafeWindowsPath(actualPkgDir)) {
306
+ console.warn(` context-mode: skipping junction — path contains unsafe characters`);
307
+ } else {
308
+ execSync(`mklink /J "${expectedPkgDir}" "${actualPkgDir}"`, {
309
+ shell: "cmd.exe",
310
+ stdio: "pipe",
311
+ });
312
+ console.log(`\n context-mode: created junction for nvm4w compatibility`);
313
+ console.log(` ${expectedPkgDir} → ${actualPkgDir}\n`);
314
+ }
315
+ }
316
+ }
317
+
318
+ // Also fix stale shims that reference old bin entry (build/cli.js → cli.bundle.mjs)
319
+ try {
320
+ const whereOutput = execSync("where context-mode.cmd", {
321
+ encoding: "utf-8",
322
+ stdio: ["pipe", "pipe", "pipe"],
323
+ }).trim();
324
+ for (const line of whereOutput.split(/\r?\n/)) {
325
+ if (line.endsWith("context-mode.cmd")) {
326
+ const content = readFileSync(line, "utf-8");
327
+ if (content.includes("build\\cli.js") || content.includes("build/cli.js")) {
328
+ // Rewrite stale shim to use cli.bundle.mjs
329
+ const fixed = content
330
+ .replace(/build[\\\/]cli\.js/g, "cli.bundle.mjs");
331
+ writeFileSync(line, fixed);
332
+ console.log(` context-mode: fixed stale shim at ${line}`);
333
+ }
334
+ }
335
+ }
336
+ } catch { /* best effort */ }
337
+ } catch {
338
+ // Best effort — don't block install. User can use npx as fallback.
339
+ }
340
+ }
341
+
342
+ // ── 3. Native binding self-heal — better-sqlite3 (#408) ──────────────
343
+ // On Windows, `npm rebuild` falls through to node-gyp without MSVC; bypass
344
+ // that by spawning prebuild-install directly. Cross-platform safety net —
345
+ // the binding can also go missing on macOS/Linux when prebuilds are stale
346
+ // or the install was interrupted.
347
+ //
348
+ // Logic lives in scripts/heal-better-sqlite3.mjs (shared with
349
+ // hooks/ensure-deps.mjs so there's one source of truth).
350
+ try { healBetterSqlite3Binding(pkgRoot); } catch { /* best effort — don't block install */ }
351
+
352
+ // ── 4. Hook normalization at install time (#414) ─────────────────────
353
+ // hooks/hooks.json + .claude-plugin/plugin.json ship with `${CLAUDE_PLUGIN_ROOT}`
354
+ // + bare `node` command. On Windows + Claude Code that combination triggers
355
+ // `cjs/loader:1479 MODULE_NOT_FOUND` (placeholder mangling, MSYS path issues,
356
+ // PATH lookup failure). start.mjs normalizes on every MCP boot, but normalizing
357
+ // here too closes the gap for the very first hook fire after a fresh install
358
+ // (before any MCP server has run).
359
+ //
360
+ // Guard 1: only run on REAL `npm install -g @mxalbert/context-mode`. A contributor's
361
+ // `npm install` from a git clone (or CI checkout) must NOT mutate the
362
+ // source-tracked `.claude-plugin/plugin.json` — doing so substitutes the
363
+ // literal `${CLAUDE_PLUGIN_ROOT}` with an absolute path and trips
364
+ // `scripts/assert-asymmetric-drift.mjs` (Issue #531) in the build chain.
365
+ // Reuses `isGlobalInstall()` (section -1 already gates that way); the
366
+ // `.git` walk inside it is what keeps contributor / CI installs untouched.
367
+ //
368
+ // Guard 2: /ctx-upgrade clones the repo to `<tmpdir>/context-mode-upgrade-<epoch>/`
369
+ // and runs `npm install` there before `cpSync`-ing files into the real pluginRoot
370
+ // (src/cli.ts). The tmpdir has no `.git`, so `isGlobalInstall()` returns
371
+ // true there — we need this second check to skip the staging dir. Without
372
+ // it, pkgRoot is the tmpdir → hooks.json gets the tmpdir's absolute paths
373
+ // baked in → cpSync copies that poisoned hooks.json into the real plugin
374
+ // dir → tmpdir is later cleaned → every hook fires with MODULE_NOT_FOUND.
375
+ // start.mjs normalizes correctly on the next MCP boot from the real
376
+ // pluginRoot anyway.
377
+ const TMPDIR_UPGRADE_RE = /[/\\]context-mode-upgrade-\d+[/\\]?$/;
378
+ if (isGlobalInstall() && !TMPDIR_UPGRADE_RE.test(pkgRoot)) {
379
+ try {
380
+ // #738: probe for Bun ≥1.0 so the post-install hooks.json rewrite picks
381
+ // the faster runtime where available. Probe failures (e.g. build not
382
+ // present yet during `npm install` itself) fall through to nodePath.
383
+ let jsRuntimePath;
384
+ try {
385
+ const { resolveHookRuntime } = await import("../build/runtime.js");
386
+ const r = resolveHookRuntime();
387
+ if (r.isBun) jsRuntimePath = r.path;
388
+ } catch { /* best effort — fall through */ }
389
+ const { normalizeHooksOnStartup } = await import("../hooks/normalize-hooks.mjs");
390
+ normalizeHooksOnStartup({
391
+ pluginRoot: pkgRoot,
392
+ nodePath: process.execPath,
393
+ jsRuntimePath,
394
+ platform: process.platform,
395
+ });
396
+ } catch { /* best effort — never block install */ }
397
+ }