@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,464 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SessionStart hook for context-mode
4
+ *
5
+ * Provides the agent with XML-structured "Rules of Engagement"
6
+ * at the beginning of each session. Injects session knowledge on
7
+ * both startup and compact to maintain continuity.
8
+ *
9
+ * Session Lifecycle Rules:
10
+ * - "startup" → Fresh session. Inject previous session knowledge. Cleanup old data.
11
+ * - "compact" → Auto-compact triggered. Inject resume snapshot + stats.
12
+ * - "resume" → User invoked --continue, --resume, or /resume. CC sends the
13
+ * ACTIVE session_id; for /resume this is typically a *fresh*
14
+ * id, so live events miss → fall back to snapshot (#413).
15
+ * - "clear" → User cleared context. No resume.
16
+ *
17
+ * Crash-resilience: wrapped via runHook (#414) — all module loads happen
18
+ * dynamically inside the wrapper so a missing/poisoned dep can never hard-fail
19
+ * the hook. Errors land in ~/.claude/context-mode/hook-errors.log.
20
+ */
21
+
22
+ import { runHook } from "./run-hook.mjs";
23
+
24
+ await runHook(async () => {
25
+ const { createRoutingBlock } = await import("./routing-block.mjs");
26
+ const { createToolNamer } = await import("./core/tool-naming.mjs");
27
+ const { detectPlatformFromEnv } = await import("./core/platform-detect.mjs");
28
+ const { buildAutoInjection } = await import("./auto-injection.mjs");
29
+ const {
30
+ readStdin,
31
+ parseStdin,
32
+ getSessionId,
33
+ getInputProjectDir,
34
+ getSessionDBPath,
35
+ getSessionEventsPath,
36
+ getCleanupFlagPath,
37
+ resolveConfigDir,
38
+ } = await import("./session-helpers.mjs");
39
+ const { writeSessionEventsFile, buildSessionDirective, getSessionEvents } = await import(
40
+ "./session-directive.mjs"
41
+ );
42
+ const { createSessionLoaders, attributeAndInsertEvents } = await import("./session-loaders.mjs");
43
+ const { join, dirname } = await import("node:path");
44
+ const { fileURLToPath } = await import("node:url");
45
+ const { readFileSync, unlinkSync, readdirSync, rmSync, lstatSync, realpathSync, symlinkSync } = await import("node:fs");
46
+
47
+ const detectedPlatform = detectPlatformFromEnv();
48
+ const toolNamer = createToolNamer(detectedPlatform);
49
+ const ROUTING_BLOCK = createRoutingBlock(toolNamer);
50
+
51
+ // Resolve absolute path for imports (fileURLToPath for Windows compat)
52
+ const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
53
+ const { loadSessionDB, loadProjectAttribution, loadExtract } = createSessionLoaders(HOOK_DIR);
54
+
55
+ // Emit a `session_start` canonical event at the boundary of each session
56
+ // lifecycle transition (startup / resume / compact). The platform's insight
57
+ // engine joins on `category='session_start'` to compute per-session
58
+ // aggregates (~60 of 180 patterns depend on this anchor row). Bridge
59
+ // forwards via attributeAndInsertEvents which also stamps the rollup
60
+ // snapshot — safe for the FIRST event of a fresh session.
61
+ async function emitSessionStartLifecycle(db, sessionId, source, projectDir, input) {
62
+ try {
63
+ const { resolveProjectAttributions } = await loadProjectAttribution();
64
+ const lifecycleEvent = {
65
+ type: "session_start",
66
+ category: "session_start",
67
+ data: JSON.stringify({
68
+ source,
69
+ project_dir: projectDir,
70
+ started_at: Math.floor(Date.now() / 1000),
71
+ }),
72
+ priority: 1,
73
+ };
74
+
75
+ // PRD #4 — emit session_settings_snapshot alongside lifecycle when
76
+ // the SessionStart envelope carries any of mcp_servers / model /
77
+ // permission_mode. Best-effort: missing fields → no snapshot.
78
+ const eventsToEmit = [lifecycleEvent];
79
+ try {
80
+ const extract = await loadExtract();
81
+ if (typeof extract.extractSessionSettings === "function") {
82
+ eventsToEmit.push(...extract.extractSessionSettings(input));
83
+ }
84
+ } catch {
85
+ // settings snapshot is opportunistic — never block lifecycle on it
86
+ }
87
+
88
+ attributeAndInsertEvents(
89
+ db,
90
+ sessionId,
91
+ eventsToEmit,
92
+ input,
93
+ projectDir,
94
+ "SessionStart",
95
+ resolveProjectAttributions,
96
+ );
97
+ } catch {
98
+ // Best-effort — lifecycle emission failure MUST NOT block session start.
99
+ }
100
+ }
101
+
102
+ // Self-heal a partial plugin cache install before anything else
103
+ // touches the cache dir. The Algo-D4 boot gate and the #604
104
+ // normalize-hooks ratchet both fire from start.mjs, which is one of
105
+ // the files that may be missing in the failure mode; sessionstart.mjs
106
+ // fires from CC's hooks.json wiring regardless of MCP boot status, so
107
+ // it is the reliably-available entry point. See
108
+ // hooks/heal-partial-install.mjs for the full failure-mode description.
109
+ try {
110
+ const { healPartialInstallFromMarketplace } = await import("./heal-partial-install.mjs");
111
+ healPartialInstallFromMarketplace();
112
+ } catch { /* best effort, never block session start */ }
113
+
114
+ // Issue #710 — Layer 2: self-heal Claude Code's per-session shell snapshots.
115
+ //
116
+ // Claude Code `source`s ~/.claude/shell-snapshots/snapshot-*.sh before every
117
+ // Bash tool call (refs/platforms/claude-code/src/utils/bash/ShellSnapshot.ts:269-336,
118
+ // sourced at bashProvider.ts:166). The snapshot bakes an `export PATH='…'`
119
+ // line with the context-mode `bin/` of the version active at session boot.
120
+ // After /ctx-upgrade deletes the old cache dir, the snapshot still points
121
+ // at it — every Bash call fails with "Plugin directory does not exist"
122
+ // until the session restarts.
123
+ //
124
+ // Layer 1 (cli.ts /ctx-upgrade) rewrites the active session's snapshot
125
+ // mid-upgrade so the in-process session never sees the broken state.
126
+ // Layer 2 (this) catches sessions that started after /ctx-upgrade but
127
+ // whose snapshots somehow missed the rewrite (parallel sessions, killed
128
+ // /ctx-upgrade run, manual cache surgery). Resolves currentVersion from
129
+ // the plugin's own manifest — no env-var dependency, immune to PATH bugs.
130
+ // Best-effort, never blocks session start.
131
+ try {
132
+ const { selfHealShellSnapshots } = await import("./cache-heal-utils.mjs");
133
+ const { resolve } = await import("node:path");
134
+ const { resolveConfigDir } = await import("./session-helpers.mjs");
135
+
136
+ // Read the version this MCP boot is running under. PLUGIN_ROOT
137
+ // points at ~/.claude/plugins/cache/context-mode/context-mode/<vX>/.
138
+ let currentVersion = null;
139
+ try {
140
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT
141
+ ?? resolve(HOOK_DIR, "..");
142
+ const manifestPath = resolve(pluginRoot, ".claude-plugin", "plugin.json");
143
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
144
+ if (typeof manifest?.version === "string" && manifest.version) {
145
+ currentVersion = manifest.version;
146
+ }
147
+ } catch { /* missing/malformed manifest — skip self-heal */ }
148
+
149
+ if (currentVersion) {
150
+ const snapshotsDir = resolve(resolveConfigDir(), "shell-snapshots");
151
+ const pluginCacheRoot = resolve(
152
+ resolveConfigDir(),
153
+ "plugins",
154
+ "cache",
155
+ "context-mode",
156
+ "context-mode",
157
+ );
158
+ selfHealShellSnapshots({
159
+ snapshotsDir,
160
+ pluginCacheRoot,
161
+ currentVersion,
162
+ });
163
+ }
164
+ } catch { /* best effort, never block session start */ }
165
+
166
+ let additionalContext = ROUTING_BLOCK;
167
+
168
+ // ─── #558: surface security init failure as agent-facing context ───
169
+ //
170
+ // Pre-558 the only signal of a fail-open security regression was a
171
+ // stderr WARNING line (suppressed/discarded by most adapters). The
172
+ // SessionStart additionalContext block is the in-band channel — the
173
+ // agent reads it, the user sees it. Idempotent by virtue of
174
+ // SessionStart's once-per-session lifecycle.
175
+ try {
176
+ const { initSecurity, isSecurityInitFailed, buildSecurityWarningContext } =
177
+ await import("./core/routing.mjs");
178
+ const { resolve: _resolve } = await import("node:path");
179
+ await initSecurity(_resolve(HOOK_DIR, "..", "build"));
180
+ if (isSecurityInitFailed()) {
181
+ const warning = buildSecurityWarningContext();
182
+ if (warning) additionalContext = warning + "\n\n" + additionalContext;
183
+ }
184
+ } catch { /* security probe is best-effort — never block session start */ }
185
+
186
+ try {
187
+ const raw = await readStdin();
188
+ const input = parseStdin(raw);
189
+ const source = input.source ?? "startup";
190
+
191
+ if (source === "compact") {
192
+ // Session was compacted — write events to file for auto-indexing, inject directive only
193
+ const { SessionDB } = await loadSessionDB();
194
+ const dbPath = getSessionDBPath();
195
+ const db = new SessionDB({ dbPath });
196
+ const sessionId = getSessionId(input);
197
+ const resume = db.getResume(sessionId);
198
+
199
+ if (resume && !resume.consumed) {
200
+ db.markResumeConsumed(sessionId);
201
+ }
202
+
203
+ const events = getSessionEvents(db, sessionId);
204
+ if (events.length > 0) {
205
+ const eventMeta = writeSessionEventsFile(events, getSessionEventsPath());
206
+ additionalContext += buildSessionDirective("compact", eventMeta, toolNamer);
207
+
208
+ // Auto-inject behavioral state on compaction (role, decisions, skills, intent)
209
+ const autoInjection = buildAutoInjection(events);
210
+ if (autoInjection) {
211
+ additionalContext += "\n\n" + autoInjection;
212
+ }
213
+
214
+ // D2 PRD Phase 6.2: emit snapshot-consumed with bytes_returned=snapshot.length.
215
+ // The resumed snapshot bytes ARE returned to the model — that's the whole
216
+ // point of resume — so account them on bytes_returned, not bytes_avoided.
217
+ // v1.0.160: route through wire — resume metric on the platform reads
218
+ // category='session-resume' rows. Both snapshot-consumed (bytes
219
+ // returned) and resume_completed land here so the dashboard sees
220
+ // every resume boundary.
221
+ try {
222
+ const resumeRow = (resume && resume.snapshot)
223
+ ? resume
224
+ : (db.getResume?.(sessionId) ?? null);
225
+ const snapshotBytes = resumeRow?.snapshot?.length ?? 0;
226
+ const { resolveProjectAttributions } = await loadProjectAttribution();
227
+ const projectDirResumeMeta = getInputProjectDir(input);
228
+
229
+ await attributeAndInsertEvents(
230
+ db,
231
+ sessionId,
232
+ [{
233
+ type: "snapshot-consumed",
234
+ category: "session-resume",
235
+ data: `Session resumed from ${source}. Snapshot ${snapshotBytes} bytes injected.`,
236
+ priority: 1,
237
+ bytes_returned: snapshotBytes,
238
+ }, {
239
+ type: "resume_completed",
240
+ category: "session-resume",
241
+ data: `Session resumed from ${source}. Prior events loaded.`,
242
+ priority: 1,
243
+ }],
244
+ input,
245
+ projectDirResumeMeta,
246
+ "SessionStart",
247
+ resolveProjectAttributions,
248
+ );
249
+ } catch { /* best-effort */ }
250
+ }
251
+
252
+ // Emit lifecycle anchor BEFORE close — engine joins on
253
+ // category='session_start' to compute per-session aggregates.
254
+ // Cross-platform projectDir via getInputProjectDir (covers cursor's
255
+ // workspace_roots[], codex/gemini/qwen's *_PROJECT_DIR env vars,
256
+ // CC's CLAUDE_PROJECT_DIR, falls back to input.cwd and process.cwd).
257
+ const projectDirCompact = getInputProjectDir(input);
258
+ await emitSessionStartLifecycle(db, sessionId, "compact", projectDirCompact, input);
259
+ db.close();
260
+ } else if (source === "resume") {
261
+ // User invoked --continue, --resume, or /resume — clear cleanup flag so
262
+ // startup doesn't wipe data on the next fresh boot.
263
+ try { unlinkSync(getCleanupFlagPath()); } catch { /* no flag */ }
264
+
265
+ const { SessionDB } = await loadSessionDB();
266
+ const dbPath = getSessionDBPath();
267
+ const db = new SessionDB({ dbPath });
268
+
269
+ // 1) Try live events for the resumed session. Filter strictly to the
270
+ // incoming session_id — falling back to getLatestSessionEvents(db)
271
+ // leaks events from any other session whose session_meta.started_at
272
+ // is more recent (cross-worktree bleed observed in the wild).
273
+ const sessionId = getSessionId(input);
274
+ const events = sessionId ? getSessionEvents(db, sessionId) : [];
275
+ if (events.length > 0) {
276
+ const eventMeta = writeSessionEventsFile(events, getSessionEventsPath());
277
+ additionalContext += buildSessionDirective("resume", eventMeta, toolNamer);
278
+ } else if (sessionId) {
279
+ // 2) Snapshot fallback (#413). /resume hands us a *new* active session
280
+ // id whose live event table is empty; the prior conversation lives
281
+ // in `session_resume.snapshot`. Mirrors the OpenCode/OpenClaw resume
282
+ // injection path (opencode-plugin.ts:454). claimLatestUnconsumedResume
283
+ // excludes the current id, so we surface the latest unconsumed
284
+ // snapshot from any prior session in this project.
285
+ const row = db.claimLatestUnconsumedResume(sessionId);
286
+ if (row?.snapshot) {
287
+ additionalContext += "\n\n" + row.snapshot;
288
+ }
289
+ }
290
+
291
+ const projectDirResume = getInputProjectDir(input);
292
+ if (sessionId) {
293
+ await emitSessionStartLifecycle(db, sessionId, "resume", projectDirResume, input);
294
+ }
295
+ db.close();
296
+ } else if (source === "startup") {
297
+ // Fresh session (no --continue) — clean slate, capture CLAUDE.md rules.
298
+ const { SessionDB } = await loadSessionDB();
299
+ const dbPath = getSessionDBPath();
300
+ const db = new SessionDB({ dbPath });
301
+ try { unlinkSync(getSessionEventsPath()); } catch { /* no stale file */ }
302
+
303
+ // Detect true fresh start vs --continue (which fires startup→resume).
304
+ // If cleanup flag exists from a PREVIOUS startup that was never followed by
305
+ // resume, that was a true fresh start — aggressively wipe all data.
306
+ db.cleanupOldSessions(7);
307
+ // Bug fix: the unconditional DELETE below USED to wipe ALL orphan
308
+ // events (any session_id missing from session_meta). On a power-outage
309
+ // restart this destroyed 1000+ events of real Claude Code work whose
310
+ // UUID session_ids hadn't yet had their session_meta row written
311
+ // (timing window between insertEvent and ensureSession). See
312
+ // tests/session/cleanup-preserves-live-uuid-events.test.ts.
313
+ //
314
+ // Now: protect anything that LOOKS like a real session UUID
315
+ // (4 dashes per RFC 4122 8-4-4-4-12), unless it's already older than
316
+ // the 7-day cleanup horizon. Detection-probe orphans like 'pid-12345'
317
+ // (no UUID shape) are still wiped aggressively — they're noise.
318
+ // Loose 4-dash shape `*-*-*-*-*`. Claude Code session_ids are UUIDs
319
+ // (5 dash-separated segments) and match. `pid-XXXXX` probes have one
320
+ // dash and don't match → wiped aggressively. We deliberately keep
321
+ // this loose so adapters that may eventually share this DB (or reuse
322
+ // this hook with hybrid `claude-code-...`-style IDs across 15
323
+ // platforms) aren't accidentally classified as orphans. The 7-day
324
+ // fallback still wipes truly abandoned UUIDs.
325
+ db.db.exec(`
326
+ DELETE FROM session_events
327
+ WHERE session_id NOT IN (SELECT session_id FROM session_meta)
328
+ AND (
329
+ session_id NOT GLOB '*-*-*-*-*' -- pid-XXX probes etc.
330
+ OR created_at < datetime('now', '-7 day') -- truly abandoned UUIDs
331
+ )
332
+ `);
333
+
334
+ // Proactively capture CLAUDE.md files — Claude Code loads them as system
335
+ // context at startup, invisible to PostToolUse hooks. We read them from
336
+ // disk so they survive compact/resume via the session events pipeline.
337
+ const sessionId = getSessionId(input);
338
+ // v1.0.160: cross-adapter projectDir resolution (was hardcoded CC env).
339
+ const projectDir = getInputProjectDir(input);
340
+ db.ensureSession(sessionId, projectDir);
341
+ const claudeMdPaths = [
342
+ join(resolveConfigDir(), "CLAUDE.md"),
343
+ join(projectDir, "CLAUDE.md"),
344
+ join(projectDir, ".claude", "CLAUDE.md"),
345
+ ];
346
+ // v1.0.160: collect rule events into a batch and forward through wire.
347
+ // Dashboard's "CLAUDE.md adoption" widget COUNTs category='rule' rows on
348
+ // the platform — without this routing the widget reads 0 no matter how
349
+ // many CLAUDE.md files actually loaded.
350
+ const ruleEvents = [];
351
+ for (const p of claudeMdPaths) {
352
+ try {
353
+ const content = readFileSync(p, "utf-8");
354
+ if (content.trim()) {
355
+ ruleEvents.push({ type: "rule", category: "rule", data: p, priority: 1 });
356
+ ruleEvents.push({ type: "rule_content", category: "rule", data: content, priority: 1 });
357
+ }
358
+ } catch { /* file doesn't exist — skip */ }
359
+ }
360
+ if (ruleEvents.length > 0) {
361
+ try {
362
+ const { resolveProjectAttributions } = await loadProjectAttribution();
363
+ attributeAndInsertEvents(
364
+ db,
365
+ sessionId,
366
+ ruleEvents,
367
+ input,
368
+ projectDir,
369
+ "SessionStart",
370
+ resolveProjectAttributions,
371
+ );
372
+ } catch { /* best-effort — rule capture must never block start */ }
373
+ }
374
+
375
+ // Lifecycle anchor for a fresh session — emits BEFORE the CLAUDE.md
376
+ // rule events have been forwarded so the `session_start` row lands
377
+ // as the very first row the platform sees for this session.
378
+ await emitSessionStartLifecycle(db, sessionId, "startup", projectDir, input);
379
+
380
+ db.close();
381
+
382
+ // Age-gated lazy cleanup of old plugin cache version dirs (#181).
383
+ // Only delete dirs older than 1 hour to avoid breaking active sessions.
384
+ // Use lstatSync (not statSync) so a fresh symlink whose target happens
385
+ // to be old is evaluated against the symlink's own mtime, not the
386
+ // target's — otherwise self-heal hooks that re-create breadcrumb
387
+ // symlinks for previous cache versions would be wiped out and any
388
+ // session pinned to one of those versions would lose its plugin root
389
+ // mid-flight (#644).
390
+ try {
391
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
392
+ if (pluginRoot) {
393
+ const cacheParentMatch = pluginRoot.match(/^(.*[\\/]plugins[\\/]cache[\\/][^\\/]+[\\/][^\\/]+[\\/])/);
394
+ if (cacheParentMatch) {
395
+ const cacheParent = cacheParentMatch[1];
396
+ const myDir = pluginRoot.replace(cacheParent, "").replace(/[\\/]/g, "");
397
+ const ONE_HOUR = 3600000;
398
+ const now = Date.now();
399
+ for (const d of readdirSync(cacheParent)) {
400
+ if (d === myDir) continue;
401
+ const oldDir = join(cacheParent, d);
402
+ try {
403
+ const st = lstatSync(oldDir);
404
+ let danglingBreadcrumb = false;
405
+ if (st.isSymbolicLink()) {
406
+ try {
407
+ realpathSync(oldDir);
408
+ } catch {
409
+ danglingBreadcrumb = true;
410
+ }
411
+ }
412
+ if (danglingBreadcrumb || now - st.mtimeMs > ONE_HOUR) {
413
+ rmSync(oldDir, { recursive: true, force: true });
414
+ // Leave a breadcrumb symlink (junction on Windows) in the
415
+ // removed version's place so sessions that loaded hooks
416
+ // from it before an auto-update keep resolving their
417
+ // scripts instead of erroring on every hook call until
418
+ // restart (#814, #807). Also fires when the entry was
419
+ // itself a stale breadcrumb, re-pointing it at the live
420
+ // root (a chain of updates would otherwise leave links
421
+ // targeting intermediate versions that no longer exist).
422
+ // The fresh mtime makes #644's lstat age gate protect it
423
+ // for the next hour, and the next sweep refreshes it
424
+ // again. Same pattern as healCacheMidSession (server.ts)
425
+ // and postinstall.mjs.
426
+ try {
427
+ symlinkSync(pluginRoot, oldDir, process.platform === "win32" ? "junction" : undefined);
428
+ } catch { /* best effort — plain delete is the pre-#814 behaviour */ }
429
+ }
430
+ } catch {
431
+ // On Windows, a dangling junction can fail before we can read
432
+ // its own mtime. Treat that as a stale breadcrumb and try to
433
+ // repoint it at the live root; failures remain best-effort.
434
+ try {
435
+ rmSync(oldDir, { recursive: true, force: true });
436
+ symlinkSync(pluginRoot, oldDir, process.platform === "win32" ? "junction" : undefined);
437
+ } catch { /* skip */ }
438
+ }
439
+ }
440
+ }
441
+ }
442
+ } catch { /* best effort — never block session start */ }
443
+ }
444
+ // "clear" — no reset needed; ctx_purge is the only wipe mechanism
445
+ } catch (err) {
446
+ // Session continuity is best-effort — never block session start
447
+ try {
448
+ const { appendFileSync } = await import("node:fs");
449
+ const { join: pjoin } = await import("node:path");
450
+ const { resolveConfigDir: _resolve } = await import("./session-helpers.mjs");
451
+ appendFileSync(
452
+ pjoin(_resolve(), "context-mode", "sessionstart-debug.log"),
453
+ `[${new Date().toISOString()}] ${err?.message || err}\n${err?.stack || ""}\n`,
454
+ );
455
+ } catch { /* ignore logging failure */ }
456
+ }
457
+
458
+ console.log(JSON.stringify({
459
+ hookSpecificOutput: {
460
+ hookEventName: "SessionStart",
461
+ additionalContext,
462
+ },
463
+ }));
464
+ });
package/hooks/stop.mjs ADDED
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ import "./suppress-stderr.mjs";
3
+ import "./ensure-deps.mjs";
4
+ /**
5
+ * Claude Code Stop hook — record turn-end state for continuity.
6
+ *
7
+ * Stop fires when Claude is about to finish the current assistant turn. This is
8
+ * not a true session shutdown event, so record a turn_end marker and never ask
9
+ * Claude to continue.
10
+ */
11
+
12
+ import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir } from "./session-helpers.mjs";
13
+ import { createSessionLoaders, attributeAndInsertEvents } from "./session-loaders.mjs";
14
+ import { dirname } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { readFileSync } from "node:fs";
17
+
18
+ const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
19
+ const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
20
+
21
+ try {
22
+ const raw = await readStdin();
23
+ const input = parseStdin(raw);
24
+ const projectDir = getInputProjectDir(input);
25
+
26
+ const { SessionDB } = await loadSessionDB();
27
+ const dbPath = getSessionDBPath(undefined, projectDir);
28
+ const db = new SessionDB({ dbPath });
29
+ const sessionId = getSessionId(input);
30
+
31
+ db.ensureSession(sessionId, projectDir);
32
+ const payload = {
33
+ stop_hook_active: input.stop_hook_active ?? false,
34
+ last_assistant_message: typeof input.last_assistant_message === "string"
35
+ ? input.last_assistant_message.slice(0, 2000)
36
+ : null,
37
+ };
38
+ db.insertEvent(sessionId, {
39
+ type: "turn_end",
40
+ category: "session",
41
+ data: JSON.stringify(payload),
42
+ priority: 1,
43
+ }, "Stop");
44
+
45
+ // ─── claude-code MAIN-turn cost capture (cursor-aware, no double-count) ───
46
+ // The transcript grows every turn and the forward loop forwards ALL passed
47
+ // events, so we emit ONLY the turns NEW since the last Stop, keyed by a
48
+ // per-session high-water cursor. Each step is best-effort — a hook must
49
+ // never block the session, so a transcript read or extract failure here is
50
+ // swallowed without aborting the turn_end write above.
51
+ try {
52
+ const transcriptPath = typeof input.transcript_path === "string" ? input.transcript_path : null;
53
+ if (transcriptPath) {
54
+ let transcript = null;
55
+ try {
56
+ transcript = readFileSync(transcriptPath, "utf-8");
57
+ } catch {
58
+ // unreadable/missing transcript — skip capture this turn.
59
+ }
60
+ if (transcript) {
61
+ const { extractTranscriptUsageSince } = await loadExtract();
62
+ const { resolveProjectAttributions } = await loadProjectAttribution();
63
+ const cursor = db.getUsageCursor(sessionId);
64
+ const { events, cursor: next } = extractTranscriptUsageSince(transcript, cursor);
65
+ if (events.length > 0) {
66
+ // attributeAndInsertEvents both INSERTS locally and FORWARDS to the
67
+ // platform (gated on ~/.context-mode/platform.json).
68
+ attributeAndInsertEvents(db, sessionId, events, input, projectDir, "Stop", resolveProjectAttributions);
69
+ }
70
+ if (next) db.setUsageCursor(sessionId, next);
71
+ }
72
+ }
73
+ } catch {
74
+ // Best-effort cost capture — never block the session on failure.
75
+ }
76
+
77
+ db.close();
78
+ } catch {
79
+ // Claude Code hooks must not block the session.
80
+ }
81
+
82
+ process.stdout.write("{}\n");
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Suppress stderr at the OS file descriptor level.
3
+ *
4
+ * Native C++ modules (better-sqlite3) write directly to fd 2 during
5
+ * initialization, bypassing Node.js process.stderr. Platforms like
6
+ * Claude Code interpret ANY stderr output as hook failure.
7
+ *
8
+ * This module MUST be the first import in every hook entry point.
9
+ * ESM evaluates imports depth-first in declaration order, so importing
10
+ * this module first ensures fd 2 is redirected to /dev/null before
11
+ * any native modules are loaded.
12
+ *
13
+ * Cross-platform: os.devNull → /dev/null (Unix) or \\.\NUL (Windows).
14
+ * See: https://github.com/mxalbert1996/context-mode/issues/68
15
+ */
16
+ import { closeSync, openSync } from "node:fs";
17
+ import { devNull } from "node:os";
18
+
19
+ try {
20
+ closeSync(2);
21
+ openSync(devNull, "w"); // Acquires fd 2 (lowest available)
22
+ } catch {
23
+ // Fallback: suppress at Node.js stream level
24
+ process.stderr.write = /** @type {any} */ (() => true);
25
+ }
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * UserPromptSubmit hook for context-mode session continuity.
4
+ *
5
+ * Captures every user prompt so the LLM can continue from the exact
6
+ * point where the user left off after compact or session restart.
7
+ *
8
+ * Must be fast (<10ms). Just a single SQLite write.
9
+ *
10
+ * Crash-resilience: wrapped via runHook (#414) — module loads happen
11
+ * dynamically so missing deps log + exit 0 instead of MODULE_NOT_FOUND.
12
+ */
13
+
14
+ import { runHook } from "./run-hook.mjs";
15
+
16
+ await runHook(async () => {
17
+ const {
18
+ readStdin,
19
+ parseStdin,
20
+ getSessionId,
21
+ getSessionDBPath,
22
+ getInputProjectDir,
23
+ } = await import("./session-helpers.mjs");
24
+ const { createSessionLoaders, attributeAndInsertEvents } = await import("./session-loaders.mjs");
25
+ const { dirname } = await import("node:path");
26
+ const { fileURLToPath } = await import("node:url");
27
+
28
+ const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
29
+ const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
30
+
31
+ try {
32
+ const raw = await readStdin();
33
+ const input = parseStdin(raw);
34
+ const projectDir = getInputProjectDir(input);
35
+
36
+ const prompt = input.prompt ?? input.message ?? "";
37
+ const trimmed = (prompt || "").trim();
38
+
39
+ // Skip system-generated messages — only capture genuine user prompts
40
+ const isSystemMessage = trimmed.startsWith("<task-notification>")
41
+ || trimmed.startsWith("<system-reminder>")
42
+ || trimmed.startsWith("<context_guidance>")
43
+ || trimmed.startsWith("<tool-result>");
44
+
45
+ if (trimmed.length > 0 && !isSystemMessage) {
46
+ const { SessionDB } = await loadSessionDB();
47
+ const { extractUserEvents, extractUserPromptFeatures } = await loadExtract();
48
+ const { resolveProjectAttributions } = await loadProjectAttribution();
49
+ const dbPath = getSessionDBPath();
50
+ const db = new SessionDB({ dbPath });
51
+ const sessionId = getSessionId(input);
52
+
53
+ db.ensureSession(sessionId, projectDir);
54
+
55
+ // 1. Always save the raw prompt with F1 §2 features attached.
56
+ // Features attach to the existing user_prompt event payload alongside
57
+ // the raw `data` field (do NOT remove `data`). Platform Zod envelope
58
+ // is forward-compatible; new fields persist as typed columns.
59
+ const promptFeatures = typeof extractUserPromptFeatures === "function"
60
+ ? extractUserPromptFeatures(trimmed)
61
+ : {};
62
+ const promptEvent = {
63
+ type: "user_prompt",
64
+ category: "user-prompt",
65
+ data: prompt,
66
+ priority: 1,
67
+ ...promptFeatures,
68
+ };
69
+ const promptAttributions = attributeAndInsertEvents(
70
+ db, sessionId, [promptEvent], input, projectDir, "UserPromptSubmit", resolveProjectAttributions,
71
+ );
72
+
73
+ // 2. Extract decision/role/intent/data from user message
74
+ const userEvents = extractUserEvents(trimmed);
75
+ // Feed lastKnownProjectDir from the first attribution into the second batch
76
+ const savedLastKnown = promptAttributions[0]?.projectDir || null;
77
+ const sessionStats = db.getSessionStats(sessionId);
78
+ const lastKnownProjectDir = typeof db.getLatestAttributedProjectDir === "function"
79
+ ? db.getLatestAttributedProjectDir(sessionId)
80
+ : null;
81
+ const userAttributions = resolveProjectAttributions(userEvents, {
82
+ sessionOriginDir: sessionStats?.project_dir || projectDir,
83
+ inputProjectDir: projectDir,
84
+ workspaceRoots: Array.isArray(input.workspace_roots) ? input.workspace_roots : [],
85
+ lastKnownProjectDir: savedLastKnown || lastKnownProjectDir,
86
+ });
87
+ // v1.0.160: route through wire so prompt-derived events (decision /
88
+ // role / intent / data extractions) reach the platform. Previously
89
+ // they only landed in local SessionDB → dashboard's prompt-flow
90
+ // insights stayed at 0.
91
+ if (userEvents.length > 0) {
92
+ attributeAndInsertEvents(
93
+ db,
94
+ sessionId,
95
+ userEvents,
96
+ input,
97
+ projectDir,
98
+ "UserPromptSubmit",
99
+ resolveProjectAttributions,
100
+ );
101
+ }
102
+
103
+ db.close();
104
+ }
105
+ } catch {
106
+ // UserPromptSubmit must never block the session — silent fallback
107
+ }
108
+ });