context-mode 1.0.169 → 2.0.0-next.6

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 (382) hide show
  1. package/.claude-plugin/marketplace.json +4 -22
  2. package/.claude-plugin/plugin.json +9 -24
  3. package/.codex-plugin/hooks.json +18 -33
  4. package/.codex-plugin/mcp.json +5 -4
  5. package/.codex-plugin/plugin.json +8 -27
  6. package/SHA256SUMS +37 -0
  7. package/claude-code/skills/ctx-doctor/SKILL.md +6 -0
  8. package/claude-code/skills/ctx-stats/SKILL.md +6 -0
  9. package/claude-code/skills/ctx-upgrade/SKILL.md +6 -0
  10. package/cli.mjs +6859 -0
  11. package/codex/skills/ctx-doctor/SKILL.md +6 -0
  12. package/codex/skills/ctx-stats/SKILL.md +6 -0
  13. package/codex/skills/ctx-upgrade/SKILL.md +6 -0
  14. package/copilot/plugin.json +14 -0
  15. package/fixtures/hook-roundtrip.json +22 -0
  16. package/gemini-extension.json +14 -0
  17. package/hook.mjs +202 -0
  18. package/hooks/claude-code.json +50 -0
  19. package/hooks/codex.json +50 -0
  20. package/hooks/copilot-cli.json +33 -0
  21. package/hooks/hooks.json +16 -111
  22. package/hooks/kiro.json +69 -0
  23. package/hooks/qwen.json +50 -0
  24. package/hooks/vscode.json +40 -0
  25. package/hooks/zcode.json +53 -0
  26. package/opencode/plugin.js +22491 -0
  27. package/package.json +32 -114
  28. package/pi/index.js +50876 -0
  29. package/pi/skills/ctx-doctor/SKILL.md +6 -0
  30. package/pi/skills/ctx-stats/SKILL.md +6 -0
  31. package/pi/skills/ctx-upgrade/SKILL.md +6 -0
  32. package/qwen-extension.json +15 -0
  33. package/run.mjs +5639 -0
  34. package/search.mjs +5639 -0
  35. package/server.mjs +48214 -0
  36. package/skills/ctx-doctor/SKILL.md +3 -19
  37. package/skills/ctx-stats/SKILL.md +3 -23
  38. package/skills/ctx-upgrade/SKILL.md +3 -28
  39. package/.openclaw-plugin/index.ts +0 -12
  40. package/.openclaw-plugin/openclaw.plugin.json +0 -38
  41. package/.openclaw-plugin/package.json +0 -28
  42. package/LICENSE +0 -94
  43. package/README.md +0 -1619
  44. package/bin/statusline.mjs +0 -378
  45. package/build/adapters/antigravity/index.d.ts +0 -51
  46. package/build/adapters/antigravity/index.js +0 -169
  47. package/build/adapters/antigravity-cli/index.d.ts +0 -51
  48. package/build/adapters/antigravity-cli/index.js +0 -342
  49. package/build/adapters/base.d.ts +0 -92
  50. package/build/adapters/base.js +0 -135
  51. package/build/adapters/claude-code/config.d.ts +0 -8
  52. package/build/adapters/claude-code/config.js +0 -8
  53. package/build/adapters/claude-code/hooks.d.ts +0 -106
  54. package/build/adapters/claude-code/hooks.js +0 -183
  55. package/build/adapters/claude-code/index.d.ts +0 -80
  56. package/build/adapters/claude-code/index.js +0 -569
  57. package/build/adapters/claude-code-base.d.ts +0 -49
  58. package/build/adapters/claude-code-base.js +0 -113
  59. package/build/adapters/client-map.d.ts +0 -10
  60. package/build/adapters/client-map.js +0 -42
  61. package/build/adapters/codex/config.d.ts +0 -8
  62. package/build/adapters/codex/config.js +0 -8
  63. package/build/adapters/codex/hooks.d.ts +0 -55
  64. package/build/adapters/codex/hooks.js +0 -64
  65. package/build/adapters/codex/index.d.ts +0 -87
  66. package/build/adapters/codex/index.js +0 -992
  67. package/build/adapters/codex/paths.d.ts +0 -1
  68. package/build/adapters/codex/paths.js +0 -12
  69. package/build/adapters/codex/usage.d.ts +0 -107
  70. package/build/adapters/codex/usage.js +0 -227
  71. package/build/adapters/copilot-base.d.ts +0 -94
  72. package/build/adapters/copilot-base.js +0 -297
  73. package/build/adapters/copilot-cli/hooks.d.ts +0 -33
  74. package/build/adapters/copilot-cli/hooks.js +0 -64
  75. package/build/adapters/copilot-cli/index.d.ts +0 -48
  76. package/build/adapters/copilot-cli/index.js +0 -341
  77. package/build/adapters/cursor/config.d.ts +0 -4
  78. package/build/adapters/cursor/config.js +0 -4
  79. package/build/adapters/cursor/hooks.d.ts +0 -55
  80. package/build/adapters/cursor/hooks.js +0 -82
  81. package/build/adapters/cursor/index.d.ts +0 -64
  82. package/build/adapters/cursor/index.js +0 -517
  83. package/build/adapters/detect.d.ts +0 -136
  84. package/build/adapters/detect.js +0 -623
  85. package/build/adapters/gemini-cli/config.d.ts +0 -8
  86. package/build/adapters/gemini-cli/config.js +0 -8
  87. package/build/adapters/gemini-cli/hooks.d.ts +0 -83
  88. package/build/adapters/gemini-cli/hooks.js +0 -115
  89. package/build/adapters/gemini-cli/index.d.ts +0 -80
  90. package/build/adapters/gemini-cli/index.js +0 -483
  91. package/build/adapters/jetbrains-copilot/config.d.ts +0 -8
  92. package/build/adapters/jetbrains-copilot/config.js +0 -8
  93. package/build/adapters/jetbrains-copilot/hooks.d.ts +0 -59
  94. package/build/adapters/jetbrains-copilot/hooks.js +0 -87
  95. package/build/adapters/jetbrains-copilot/index.d.ts +0 -31
  96. package/build/adapters/jetbrains-copilot/index.js +0 -131
  97. package/build/adapters/kimi/config.d.ts +0 -8
  98. package/build/adapters/kimi/config.js +0 -8
  99. package/build/adapters/kimi/hooks.d.ts +0 -28
  100. package/build/adapters/kimi/hooks.js +0 -34
  101. package/build/adapters/kimi/index.d.ts +0 -66
  102. package/build/adapters/kimi/index.js +0 -537
  103. package/build/adapters/kimi/paths.d.ts +0 -21
  104. package/build/adapters/kimi/paths.js +0 -52
  105. package/build/adapters/kimi/usage.d.ts +0 -82
  106. package/build/adapters/kimi/usage.js +0 -217
  107. package/build/adapters/kiro/hooks.d.ts +0 -61
  108. package/build/adapters/kiro/hooks.js +0 -94
  109. package/build/adapters/kiro/index.d.ts +0 -61
  110. package/build/adapters/kiro/index.js +0 -301
  111. package/build/adapters/omp/index.d.ts +0 -65
  112. package/build/adapters/omp/index.js +0 -182
  113. package/build/adapters/omp/plugin.d.ts +0 -81
  114. package/build/adapters/omp/plugin.js +0 -331
  115. package/build/adapters/omp/usage.d.ts +0 -49
  116. package/build/adapters/omp/usage.js +0 -110
  117. package/build/adapters/openclaw/config.d.ts +0 -8
  118. package/build/adapters/openclaw/config.js +0 -8
  119. package/build/adapters/openclaw/hooks.d.ts +0 -50
  120. package/build/adapters/openclaw/hooks.js +0 -61
  121. package/build/adapters/openclaw/index.d.ts +0 -74
  122. package/build/adapters/openclaw/index.js +0 -439
  123. package/build/adapters/openclaw/mcp-tools.d.ts +0 -54
  124. package/build/adapters/openclaw/mcp-tools.js +0 -207
  125. package/build/adapters/openclaw/plugin.d.ts +0 -141
  126. package/build/adapters/openclaw/plugin.js +0 -818
  127. package/build/adapters/openclaw/session-db.d.ts +0 -55
  128. package/build/adapters/openclaw/session-db.js +0 -88
  129. package/build/adapters/openclaw/usage.d.ts +0 -34
  130. package/build/adapters/openclaw/usage.js +0 -52
  131. package/build/adapters/openclaw/workspace-router.d.ts +0 -29
  132. package/build/adapters/openclaw/workspace-router.js +0 -64
  133. package/build/adapters/opencode/config.d.ts +0 -8
  134. package/build/adapters/opencode/config.js +0 -8
  135. package/build/adapters/opencode/hooks.d.ts +0 -38
  136. package/build/adapters/opencode/hooks.js +0 -50
  137. package/build/adapters/opencode/index.d.ts +0 -67
  138. package/build/adapters/opencode/index.js +0 -463
  139. package/build/adapters/opencode/plugin.d.ts +0 -184
  140. package/build/adapters/opencode/plugin.js +0 -558
  141. package/build/adapters/opencode/zod3tov4.d.ts +0 -1
  142. package/build/adapters/opencode/zod3tov4.js +0 -132
  143. package/build/adapters/pi/extension.d.ts +0 -81
  144. package/build/adapters/pi/extension.js +0 -861
  145. package/build/adapters/pi/index.d.ts +0 -57
  146. package/build/adapters/pi/index.js +0 -173
  147. package/build/adapters/pi/mcp-bridge.d.ts +0 -268
  148. package/build/adapters/pi/mcp-bridge.js +0 -894
  149. package/build/adapters/qwen-code/hooks.d.ts +0 -26
  150. package/build/adapters/qwen-code/hooks.js +0 -29
  151. package/build/adapters/qwen-code/index.d.ts +0 -40
  152. package/build/adapters/qwen-code/index.js +0 -361
  153. package/build/adapters/qwen-code/usage.d.ts +0 -90
  154. package/build/adapters/qwen-code/usage.js +0 -222
  155. package/build/adapters/types.d.ts +0 -360
  156. package/build/adapters/types.js +0 -118
  157. package/build/adapters/vscode-copilot/config.d.ts +0 -8
  158. package/build/adapters/vscode-copilot/config.js +0 -8
  159. package/build/adapters/vscode-copilot/hooks.d.ts +0 -71
  160. package/build/adapters/vscode-copilot/hooks.js +0 -98
  161. package/build/adapters/vscode-copilot/index.d.ts +0 -32
  162. package/build/adapters/vscode-copilot/index.js +0 -222
  163. package/build/adapters/zed/index.d.ts +0 -40
  164. package/build/adapters/zed/index.js +0 -163
  165. package/build/cli.d.ts +0 -34
  166. package/build/cli.js +0 -1827
  167. package/build/db-base.d.ts +0 -179
  168. package/build/db-base.js +0 -569
  169. package/build/executor.d.ts +0 -72
  170. package/build/executor.js +0 -678
  171. package/build/exit-classify.d.ts +0 -19
  172. package/build/exit-classify.js +0 -12
  173. package/build/fetch-cache.d.ts +0 -13
  174. package/build/fetch-cache.js +0 -15
  175. package/build/lifecycle.d.ts +0 -124
  176. package/build/lifecycle.js +0 -278
  177. package/build/runPool.d.ts +0 -36
  178. package/build/runPool.js +0 -51
  179. package/build/runtime.d.ts +0 -71
  180. package/build/runtime.js +0 -659
  181. package/build/search/auto-memory.d.ts +0 -47
  182. package/build/search/auto-memory.js +0 -170
  183. package/build/search/ctx-search-schema.d.ts +0 -90
  184. package/build/search/ctx-search-schema.js +0 -135
  185. package/build/search/flood-guard.d.ts +0 -57
  186. package/build/search/flood-guard.js +0 -80
  187. package/build/search/unified.d.ts +0 -56
  188. package/build/search/unified.js +0 -104
  189. package/build/security.d.ts +0 -206
  190. package/build/security.js +0 -780
  191. package/build/server.d.ts +0 -184
  192. package/build/server.js +0 -4563
  193. package/build/session/analytics.d.ts +0 -773
  194. package/build/session/analytics.js +0 -2420
  195. package/build/session/db.d.ts +0 -468
  196. package/build/session/db.js +0 -1308
  197. package/build/session/error-classifier.d.ts +0 -87
  198. package/build/session/error-classifier.js +0 -303
  199. package/build/session/event-emit.d.ts +0 -48
  200. package/build/session/event-emit.js +0 -101
  201. package/build/session/extract.d.ts +0 -333
  202. package/build/session/extract.js +0 -2688
  203. package/build/session/model-prices.json +0 -429
  204. package/build/session/persist-tool-calls.d.ts +0 -54
  205. package/build/session/persist-tool-calls.js +0 -105
  206. package/build/session/pricing.d.ts +0 -64
  207. package/build/session/pricing.js +0 -151
  208. package/build/session/project-attribution.d.ts +0 -73
  209. package/build/session/project-attribution.js +0 -245
  210. package/build/session/purge.d.ts +0 -138
  211. package/build/session/purge.js +0 -240
  212. package/build/session/retrieval-marker.d.ts +0 -39
  213. package/build/session/retrieval-marker.js +0 -65
  214. package/build/session/snapshot.d.ts +0 -46
  215. package/build/session/snapshot.js +0 -532
  216. package/build/store-directory.d.ts +0 -56
  217. package/build/store-directory.js +0 -254
  218. package/build/store.d.ts +0 -147
  219. package/build/store.js +0 -1714
  220. package/build/truncate.d.ts +0 -57
  221. package/build/truncate.js +0 -142
  222. package/build/types.d.ts +0 -117
  223. package/build/types.js +0 -20
  224. package/build/util/claude-config.d.ts +0 -32
  225. package/build/util/claude-config.js +0 -84
  226. package/build/util/hook-config.d.ts +0 -27
  227. package/build/util/hook-config.js +0 -76
  228. package/build/util/jsonc.d.ts +0 -14
  229. package/build/util/jsonc.js +0 -104
  230. package/build/util/plugin-cache-integrity.d.ts +0 -51
  231. package/build/util/plugin-cache-integrity.js +0 -146
  232. package/build/util/project-dir.d.ts +0 -149
  233. package/build/util/project-dir.js +0 -340
  234. package/build/util/sibling-mcp.d.ts +0 -79
  235. package/build/util/sibling-mcp.js +0 -181
  236. package/cli.bundle.mjs +0 -1236
  237. package/configs/antigravity/GEMINI.md +0 -68
  238. package/configs/antigravity/mcp_config.json +0 -7
  239. package/configs/antigravity-cli/hooks/hooks.json +0 -37
  240. package/configs/antigravity-cli/hooks.json +0 -37
  241. package/configs/antigravity-cli/mcp_config.json +0 -10
  242. package/configs/antigravity-cli/plugin.json +0 -14
  243. package/configs/antigravity-cli/rules/context-mode.md +0 -77
  244. package/configs/antigravity-cli/skills/context-mode/SKILL.md +0 -77
  245. package/configs/claude-code/CLAUDE.md +0 -91
  246. package/configs/codex/AGENTS.md +0 -99
  247. package/configs/codex/config.toml +0 -8
  248. package/configs/codex/hooks.json +0 -47
  249. package/configs/copilot-cli/.github/plugin/plugin.json +0 -23
  250. package/configs/copilot-cli/.mcp.json +0 -12
  251. package/configs/copilot-cli/README.md +0 -47
  252. package/configs/copilot-cli/hooks.json +0 -41
  253. package/configs/copilot-cli/skills/context-mode/SKILL.md +0 -38
  254. package/configs/cursor/context-mode.mdc +0 -76
  255. package/configs/cursor/hooks.json +0 -21
  256. package/configs/cursor/mcp.json +0 -7
  257. package/configs/gemini-cli/GEMINI.md +0 -88
  258. package/configs/gemini-cli/mcp.json +0 -7
  259. package/configs/gemini-cli/settings.json +0 -60
  260. package/configs/jetbrains-copilot/copilot-instructions.md +0 -80
  261. package/configs/jetbrains-copilot/hooks.json +0 -16
  262. package/configs/jetbrains-copilot/mcp.json +0 -7
  263. package/configs/kilo/AGENTS.md +0 -84
  264. package/configs/kilo/kilo.json +0 -6
  265. package/configs/kimi/hooks.json +0 -54
  266. package/configs/kiro/KIRO.md +0 -84
  267. package/configs/kiro/agent.json +0 -18
  268. package/configs/kiro/mcp.json +0 -7
  269. package/configs/omp/SYSTEM.md +0 -85
  270. package/configs/omp/mcp.json +0 -7
  271. package/configs/openclaw/AGENTS.md +0 -87
  272. package/configs/openclaw/openclaw.json +0 -13
  273. package/configs/opencode/AGENTS.md +0 -84
  274. package/configs/opencode/opencode.json +0 -6
  275. package/configs/pi/AGENTS.md +0 -3
  276. package/configs/qwen-code/QWEN.md +0 -91
  277. package/configs/vscode-copilot/copilot-instructions.md +0 -80
  278. package/configs/vscode-copilot/hooks.json +0 -16
  279. package/configs/vscode-copilot/mcp.json +0 -7
  280. package/configs/zed/AGENTS.md +0 -68
  281. package/hooks/antigravity-cli/payload.mjs +0 -98
  282. package/hooks/antigravity-cli/posttooluse.mjs +0 -138
  283. package/hooks/antigravity-cli/pretooluse.mjs +0 -78
  284. package/hooks/antigravity-cli/stop.mjs +0 -58
  285. package/hooks/auto-injection.mjs +0 -102
  286. package/hooks/cache-heal-utils.mjs +0 -379
  287. package/hooks/codex/platform.mjs +0 -1
  288. package/hooks/codex/posttooluse.mjs +0 -66
  289. package/hooks/codex/precompact.mjs +0 -70
  290. package/hooks/codex/pretooluse.mjs +0 -42
  291. package/hooks/codex/sessionstart.mjs +0 -121
  292. package/hooks/codex/stop.mjs +0 -138
  293. package/hooks/codex/userpromptsubmit.mjs +0 -75
  294. package/hooks/copilot-cli/posttooluse.mjs +0 -79
  295. package/hooks/copilot-cli/precompact.mjs +0 -66
  296. package/hooks/copilot-cli/pretooluse.mjs +0 -41
  297. package/hooks/copilot-cli/sessionstart.mjs +0 -121
  298. package/hooks/copilot-cli/stop.mjs +0 -59
  299. package/hooks/copilot-cli/userpromptsubmit.mjs +0 -77
  300. package/hooks/core/codex-caps.mjs +0 -112
  301. package/hooks/core/formatters.mjs +0 -356
  302. package/hooks/core/mcp-ready.mjs +0 -109
  303. package/hooks/core/platform-detect.mjs +0 -49
  304. package/hooks/core/routing.mjs +0 -1017
  305. package/hooks/core/stdin.mjs +0 -90
  306. package/hooks/core/tool-naming.mjs +0 -57
  307. package/hooks/cursor/afteragentresponse.mjs +0 -74
  308. package/hooks/cursor/hooks.json +0 -31
  309. package/hooks/cursor/posttooluse.mjs +0 -72
  310. package/hooks/cursor/pretooluse.mjs +0 -26
  311. package/hooks/cursor/sessionstart.mjs +0 -97
  312. package/hooks/cursor/stop.mjs +0 -48
  313. package/hooks/ensure-deps.mjs +0 -254
  314. package/hooks/formatters/claude-code.mjs +0 -113
  315. package/hooks/formatters/cursor.mjs +0 -37
  316. package/hooks/formatters/gemini-cli.mjs +0 -55
  317. package/hooks/formatters/vscode-copilot.mjs +0 -55
  318. package/hooks/gemini-cli/aftermodel.mjs +0 -70
  319. package/hooks/gemini-cli/aftertool.mjs +0 -61
  320. package/hooks/gemini-cli/beforeagent.mjs +0 -99
  321. package/hooks/gemini-cli/beforetool.mjs +0 -27
  322. package/hooks/gemini-cli/precompress.mjs +0 -55
  323. package/hooks/gemini-cli/sessionstart.mjs +0 -137
  324. package/hooks/heal-partial-install.mjs +0 -712
  325. package/hooks/jetbrains-copilot/posttooluse.mjs +0 -61
  326. package/hooks/jetbrains-copilot/precompact.mjs +0 -55
  327. package/hooks/jetbrains-copilot/pretooluse.mjs +0 -27
  328. package/hooks/jetbrains-copilot/sessionstart.mjs +0 -124
  329. package/hooks/kimi/platform.mjs +0 -1
  330. package/hooks/kimi/posttooluse.mjs +0 -72
  331. package/hooks/kimi/precompact.mjs +0 -80
  332. package/hooks/kimi/pretooluse.mjs +0 -42
  333. package/hooks/kimi/sessionend.mjs +0 -61
  334. package/hooks/kimi/sessionstart.mjs +0 -113
  335. package/hooks/kimi/stop.mjs +0 -132
  336. package/hooks/kimi/userpromptsubmit.mjs +0 -90
  337. package/hooks/kiro/agentspawn.mjs +0 -97
  338. package/hooks/kiro/posttooluse.mjs +0 -51
  339. package/hooks/kiro/pretooluse.mjs +0 -64
  340. package/hooks/kiro/userpromptsubmit.mjs +0 -88
  341. package/hooks/normalize-hooks.mjs +0 -323
  342. package/hooks/platform-bridge.mjs +0 -340
  343. package/hooks/posttooluse.mjs +0 -225
  344. package/hooks/precompact.mjs +0 -100
  345. package/hooks/pretooluse.mjs +0 -227
  346. package/hooks/qwen-code/platform.mjs +0 -1
  347. package/hooks/qwen-code/stop.mjs +0 -168
  348. package/hooks/routing-block.mjs +0 -105
  349. package/hooks/run-hook.mjs +0 -95
  350. package/hooks/security.bundle.mjs +0 -2
  351. package/hooks/session-attribution.bundle.mjs +0 -1
  352. package/hooks/session-db.bundle.mjs +0 -144
  353. package/hooks/session-directive.mjs +0 -520
  354. package/hooks/session-extract.bundle.mjs +0 -3
  355. package/hooks/session-helpers.mjs +0 -428
  356. package/hooks/session-loaders.mjs +0 -448
  357. package/hooks/session-snapshot.bundle.mjs +0 -32
  358. package/hooks/sessionstart.mjs +0 -464
  359. package/hooks/stop.mjs +0 -82
  360. package/hooks/suppress-stderr.mjs +0 -25
  361. package/hooks/userpromptsubmit.mjs +0 -108
  362. package/hooks/vscode-copilot/posttooluse.mjs +0 -61
  363. package/hooks/vscode-copilot/precompact.mjs +0 -55
  364. package/hooks/vscode-copilot/pretooluse.mjs +0 -27
  365. package/hooks/vscode-copilot/sessionstart.mjs +0 -129
  366. package/openclaw.plugin.json +0 -38
  367. package/scripts/heal-better-sqlite3.mjs +0 -430
  368. package/scripts/heal-installed-plugins.mjs +0 -598
  369. package/scripts/plugin-cache-integrity.mjs +0 -248
  370. package/scripts/postinstall.mjs +0 -384
  371. package/server.bundle.mjs +0 -1162
  372. package/skills/.ignore +0 -7
  373. package/skills/context-mode/SKILL.md +0 -300
  374. package/skills/context-mode/references/anti-patterns.md +0 -283
  375. package/skills/context-mode/references/patterns-javascript.md +0 -298
  376. package/skills/context-mode/references/patterns-python.md +0 -304
  377. package/skills/context-mode/references/patterns-shell.md +0 -277
  378. package/skills/ctx-index/SKILL.md +0 -46
  379. package/skills/ctx-insight/SKILL.md +0 -24
  380. package/skills/ctx-purge/SKILL.md +0 -49
  381. package/skills/ctx-search/SKILL.md +0 -35
  382. package/start.mjs +0 -616
package/build/cli.js DELETED
@@ -1,1827 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * context-mode CLI
4
- *
5
- * Usage:
6
- * context-mode → Start MCP server (stdio)
7
- * context-mode doctor → Diagnose runtime issues, hooks, FTS5, version
8
- * context-mode upgrade → Fix hooks, permissions, and settings
9
- * context-mode hook <platform> <event> → Dispatch a hook script (used by platform hook configs)
10
- * CONTEXT_MODE_DIR=/abs/path context-mode → Override sessions/content storage root
11
- * Empty/whitespace is ignored; non-empty values must be absolute.
12
- *
13
- * Platform auto-detection: CLI detects which platform is running
14
- * (Claude Code, Gemini CLI, OpenCode, etc.) and uses the appropriate adapter.
15
- */
16
- import * as p from "@clack/prompts";
17
- import color from "picocolors";
18
- import { execFileSync, execSync, execFile as nodeExecFile } from "node:child_process";
19
- import { readFileSync, cpSync, accessSync, existsSync, readdirSync, rmSync, closeSync, openSync, chmodSync, lstatSync, realpathSync, statSync, constants } from "node:fs";
20
- import { request as httpsRequest } from "node:https";
21
- import { resolve, dirname, join, sep, basename, isAbsolute } from "node:path";
22
- import { tmpdir, devNull, homedir } from "node:os";
23
- import { fileURLToPath, pathToFileURL } from "node:url";
24
- import { detectRuntimes, getRuntimeSummary, hasBunRuntime, getAvailableLanguages, } from "./runtime.js";
25
- import { getHookScriptPaths } from "./util/hook-config.js";
26
- import { resolveClaudeConfigDir } from "./util/claude-config.js";
27
- import { ensureWritableStorageDir, formatStorageDirectoryError, resolveContentStorageDir, resolveSessionStorageDir, resolveStatsStorageDir, StorageDirectoryError, } from "./session/db.js";
28
- import { ContentStore } from "./store.js";
29
- import { readToolDenyPatterns, evaluateFilePath } from "./security.js";
30
- // v1.0.128 — Issue #559 sibling MCP kill helpers (see PR-559-560-FIX-DESIGN.md).
31
- import { discoverSiblingMcpPids, killSiblingMcpServers } from "./util/sibling-mcp.js";
32
- // v1.0.119 — Issue #523 Layer 5 heal: post-bump assertion on .claude-plugin/plugin.json
33
- // mcpServers args. Single source of truth shared with start.mjs HEAL block + postinstall.
34
- // @ts-expect-error — JS module, no TS declarations
35
- import { healPluginJsonMcpServers, sweepStaleMcpJson } from "../scripts/heal-installed-plugins.mjs";
36
- // @ts-expect-error — JS module, no TS declarations
37
- import { detectWindowsVsYear } from "../scripts/heal-better-sqlite3.mjs";
38
- // Private 16-LOC copy of browserOpenArgv. Canonical version lives in src/server.ts;
39
- // duplicated here so the cli bundle does not pull server.ts top-level boot side effects.
40
- // Keep in sync — pure data, no I/O.
41
- function browserOpenArgv(url, platform) {
42
- if (platform === "darwin")
43
- return [{ cmd: "open", args: [url] }];
44
- if (platform === "win32") {
45
- return [{ cmd: "cmd", args: ["/c", "start", "", url] }];
46
- }
47
- return [
48
- { cmd: "xdg-open", args: [url] },
49
- { cmd: "sensible-browser", args: [url] },
50
- ];
51
- }
52
- // ── Adapter imports ──────────────────────────────────────
53
- import { detectPlatform, getAdapter } from "./adapters/detect.js";
54
- import { isInProcessPluginPlatform } from "./adapters/types.js";
55
- /* -------------------------------------------------------
56
- * Hook dispatcher — `context-mode hook <platform> <event>`
57
- * ------------------------------------------------------- */
58
- const HOOK_MAP = {
59
- "claude-code": {
60
- pretooluse: "hooks/pretooluse.mjs",
61
- posttooluse: "hooks/posttooluse.mjs",
62
- precompact: "hooks/precompact.mjs",
63
- sessionstart: "hooks/sessionstart.mjs",
64
- userpromptsubmit: "hooks/userpromptsubmit.mjs",
65
- stop: "hooks/stop.mjs",
66
- },
67
- "gemini-cli": {
68
- beforeagent: "hooks/gemini-cli/beforeagent.mjs",
69
- beforetool: "hooks/gemini-cli/beforetool.mjs",
70
- aftertool: "hooks/gemini-cli/aftertool.mjs",
71
- precompress: "hooks/gemini-cli/precompress.mjs",
72
- sessionstart: "hooks/gemini-cli/sessionstart.mjs",
73
- },
74
- "vscode-copilot": {
75
- pretooluse: "hooks/vscode-copilot/pretooluse.mjs",
76
- posttooluse: "hooks/vscode-copilot/posttooluse.mjs",
77
- precompact: "hooks/vscode-copilot/precompact.mjs",
78
- sessionstart: "hooks/vscode-copilot/sessionstart.mjs",
79
- },
80
- "cursor": {
81
- pretooluse: "hooks/cursor/pretooluse.mjs",
82
- posttooluse: "hooks/cursor/posttooluse.mjs",
83
- sessionstart: "hooks/cursor/sessionstart.mjs",
84
- stop: "hooks/cursor/stop.mjs",
85
- afteragentresponse: "hooks/cursor/afteragentresponse.mjs",
86
- },
87
- "codex": {
88
- pretooluse: "hooks/codex/pretooluse.mjs",
89
- posttooluse: "hooks/codex/posttooluse.mjs",
90
- precompact: "hooks/codex/precompact.mjs",
91
- sessionstart: "hooks/codex/sessionstart.mjs",
92
- userpromptsubmit: "hooks/codex/userpromptsubmit.mjs",
93
- stop: "hooks/codex/stop.mjs",
94
- },
95
- "kiro": {
96
- pretooluse: "hooks/kiro/pretooluse.mjs",
97
- posttooluse: "hooks/kiro/posttooluse.mjs",
98
- },
99
- "jetbrains-copilot": {
100
- pretooluse: "hooks/jetbrains-copilot/pretooluse.mjs",
101
- posttooluse: "hooks/jetbrains-copilot/posttooluse.mjs",
102
- precompact: "hooks/jetbrains-copilot/precompact.mjs",
103
- sessionstart: "hooks/jetbrains-copilot/sessionstart.mjs",
104
- },
105
- "copilot-cli": {
106
- pretooluse: "hooks/copilot-cli/pretooluse.mjs",
107
- posttooluse: "hooks/copilot-cli/posttooluse.mjs",
108
- precompact: "hooks/copilot-cli/precompact.mjs",
109
- sessionstart: "hooks/copilot-cli/sessionstart.mjs",
110
- userpromptsubmit: "hooks/copilot-cli/userpromptsubmit.mjs",
111
- stop: "hooks/copilot-cli/stop.mjs",
112
- },
113
- // Antigravity CLI (`agy`) — bounded PreToolUse enforcement plus capture-only
114
- // PostToolUse/Stop hooks. Configured via an installed agy plugin's
115
- // hooks/hooks.json or ~/.gemini/config/hooks.json.
116
- "antigravity-cli": {
117
- pretooluse: "hooks/antigravity-cli/pretooluse.mjs",
118
- posttooluse: "hooks/antigravity-cli/posttooluse.mjs",
119
- stop: "hooks/antigravity-cli/stop.mjs",
120
- },
121
- "kimi": {
122
- pretooluse: "hooks/kimi/pretooluse.mjs",
123
- posttooluse: "hooks/kimi/posttooluse.mjs",
124
- precompact: "hooks/kimi/precompact.mjs",
125
- sessionstart: "hooks/kimi/sessionstart.mjs",
126
- sessionend: "hooks/kimi/sessionend.mjs",
127
- userpromptsubmit: "hooks/kimi/userpromptsubmit.mjs",
128
- stop: "hooks/kimi/stop.mjs",
129
- },
130
- "qwen-code": {
131
- pretooluse: "hooks/pretooluse.mjs",
132
- posttooluse: "hooks/posttooluse.mjs",
133
- precompact: "hooks/precompact.mjs",
134
- sessionstart: "hooks/sessionstart.mjs",
135
- userpromptsubmit: "hooks/userpromptsubmit.mjs",
136
- },
137
- };
138
- async function hookDispatch(platform, event) {
139
- // Suppress stderr at OS fd level — native C++ modules (better-sqlite3) write
140
- // directly to fd 2 during initialization, bypassing Node.js process.stderr.
141
- // Platforms like Claude Code interpret ANY stderr output as hook failure.
142
- // Cross-platform: os.devNull → /dev/null (Unix) or \\.\NUL (Windows). See: #68
143
- try {
144
- closeSync(2);
145
- openSync(devNull, "w"); // Acquires fd 2 (lowest available)
146
- }
147
- catch {
148
- process.stderr.write = (() => true);
149
- }
150
- const scriptPath = HOOK_MAP[platform]?.[event];
151
- if (!scriptPath) {
152
- // Fail OPEN. context-mode has no hook for this platform/event — most often
153
- // because a newer adapter's hook command (`context-mode hook copilot-cli …`)
154
- // is running against an OLDER global binary that predates that adapter
155
- // (version skew). Exit 0 (no decision) so the host ALLOWS the tool. Exiting
156
- // non-zero here makes some hosts treat it as a hook ERROR and DENY the tool:
157
- // verified against GitHub Copilot CLI 1.0.59, where an exit-1 + empty-stdout
158
- // PreToolUse hook blocks EVERY tool ("Denied by preToolUse hook (hook
159
- // errored)") — bricking the agent during a skew instead of just disabling
160
- // context-mode's instrumentation.
161
- process.exit(0);
162
- }
163
- const pluginRoot = getPluginRoot();
164
- await import(pathToFileURL(join(pluginRoot, scriptPath)).href);
165
- }
166
- /* -------------------------------------------------------
167
- * Entry point
168
- * ------------------------------------------------------- */
169
- const args = process.argv.slice(2);
170
- function printHelp() {
171
- console.log([
172
- "Usage:",
173
- " context-mode Start MCP server (stdio)",
174
- " context-mode index <path> Index a file or directory into the FTS5 knowledge base",
175
- " context-mode search <query...> Search the current project's FTS5 knowledge base",
176
- " context-mode doctor Diagnose runtime issues, hooks, FTS5, version",
177
- " context-mode upgrade Fix hooks, permissions, and settings",
178
- " context-mode hook <platform> <event> Dispatch a configured hook script",
179
- " context-mode statusline Print Claude Code status line",
180
- "",
181
- "Index options:",
182
- " --source <label> Source label (default: project:<directory-name> or path)",
183
- " --project <path> Project identity for the content DB (default: indexed dir or cwd)",
184
- " --max-depth <n> Directory recursion depth (default: 5)",
185
- " --max-files <n> Directory file cap (default: 200)",
186
- " --ext <.ts,.md> Comma-separated extension allowlist",
187
- " --include <glob> Directory include pattern (repeatable)",
188
- " --exclude <glob> Directory exclude pattern (repeatable)",
189
- " --no-gitignore Do not apply .gitignore during directory walks",
190
- " --follow-symlinks Follow directory symlinks inside the root",
191
- "",
192
- "Search options:",
193
- " --project <path> Project identity for the content DB (default: cwd)",
194
- " --source <label> Filter to a source label (partial match)",
195
- " --limit <n> Results to show (default: 3)",
196
- " --type <code|prose> Filter by content type",
197
- "",
198
- "Environment:",
199
- " CONTEXT_MODE_DIR=/absolute/path Override sessions/content storage root; empty is ignored, non-empty must be absolute",
200
- ].join("\n"));
201
- }
202
- if (args[0] === "--help" || args[0] === "-h" || args[0] === "help") {
203
- printHelp();
204
- }
205
- else if (args[0] === "index") {
206
- indexCommand(args.slice(1)).then((code) => process.exit(code));
207
- }
208
- else if (args[0] === "search") {
209
- searchCommand(args.slice(1)).then((code) => process.exit(code));
210
- }
211
- else if (args[0] === "doctor") {
212
- doctor().then((code) => process.exit(code));
213
- }
214
- else if (args[0] === "upgrade") {
215
- // Issue #542 — accept --platform <id> from the ctx_upgrade MCP handler,
216
- // which forwards the live MCP clientInfo's resolved PlatformId. The flag
217
- // wins over upgrade()'s own detectPlatform() heuristic chain so an
218
- // ambiguous config-dir collision (e.g. ~/.cursor + ~/.pi both present)
219
- // can never misroute the upgrade.
220
- const platformFlagIdx = args.indexOf("--platform");
221
- const platformArg = platformFlagIdx >= 0 && args[platformFlagIdx + 1]
222
- ? args[platformFlagIdx + 1]
223
- : undefined;
224
- upgrade(platformArg ? { platform: platformArg } : undefined).catch((err) => {
225
- const message = err instanceof Error ? err.message : String(err);
226
- p.log.error(color.red(message));
227
- process.exit(1);
228
- });
229
- }
230
- else if (args[0] === "hook") {
231
- hookDispatch(args[1], args[2]);
232
- }
233
- else if (args[0] === "insight") {
234
- insight();
235
- }
236
- else if (args[0] === "statusline") {
237
- // Status line implementation lives in bin/statusline.mjs to keep it
238
- // dependency-free and fast. Forward stdin and exit with its result.
239
- statuslineForward();
240
- }
241
- else {
242
- // Default: start MCP server
243
- import("./server.js");
244
- }
245
- /* -------------------------------------------------------
246
- * Shared helpers
247
- * ------------------------------------------------------- */
248
- /** Normalize Windows backslash paths to forward slashes for Bash (MSYS2) compatibility. */
249
- export function toUnixPath(p) {
250
- return p.replace(/\\/g, "/");
251
- }
252
- /**
253
- * Windows-safe npm execution. On Windows:
254
- * - "npm" → "npm.cmd" (Node won't resolve via PATHEXT in execFile)
255
- * - shell: true required (Node v20+ CVE-2024-27980 mitigation)
256
- * See: https://github.com/mksglu/context-mode/issues/344
257
- */
258
- const isWin = process.platform === "win32";
259
- export function npmExecFile(args, opts = {}) {
260
- execFileSync(isWin ? "npm.cmd" : "npm", args, {
261
- ...opts,
262
- ...(isWin ? { shell: true } : {}),
263
- });
264
- }
265
- export function npmExec(command, opts = {}) {
266
- // Issue #511: use top-level static import (line 17) — never inline `require("node:...")`
267
- // in ESM-bundled sources. esbuild rewrites them to a `__require` shim that throws
268
- // `Dynamic require of "node:child_process" is not supported` under Node ESM/Bun.
269
- // Cast preserves the prior `require()`-as-`any` shape; `shell: true` is the documented
270
- // Node behavior even though @types/node typed `shell` as `string | undefined`.
271
- const execOpts = {
272
- ...opts,
273
- ...(isWin ? { shell: true } : {}),
274
- };
275
- execSync(isWin ? command.replace(/^npm /, "npm.cmd ") : command, execOpts);
276
- }
277
- export function openInBrowser(url, platform = process.platform, runner = nodeExecFile) {
278
- const opts = { stdio: "ignore" };
279
- const hint = () => console.error(`\nCould not auto-open browser. Open manually: ${url}`);
280
- // Platform→argv mapping is canonical in src/server.ts; mirrored privately
281
- // above to avoid pulling server boot side effects into the cli bundle.
282
- const attempts = browserOpenArgv(url, platform);
283
- let opened = false;
284
- for (const { cmd, args } of attempts) {
285
- try {
286
- runner(cmd, args, opts);
287
- opened = true;
288
- break;
289
- }
290
- catch { /* try next fallback */ }
291
- }
292
- if (!opened)
293
- hint();
294
- }
295
- function defaultPluginRoot() {
296
- const __filename = fileURLToPath(import.meta.url);
297
- const __dirname = dirname(__filename);
298
- // build/cli.js or src/cli.ts → go up one level; cli.bundle.mjs at project root → stay here
299
- if (__dirname.endsWith("/build") || __dirname.endsWith("\\build") ||
300
- __dirname.endsWith("/src") || __dirname.endsWith("\\src")) {
301
- return resolve(__dirname, "..");
302
- }
303
- return __dirname;
304
- }
305
- // Opencode/Kilocode install plugins from npm into a per-package cache folder.
306
- // Layout (changed silently in late 2024 — see PR #376 / KiloCode#9503):
307
- // POSIX : ~/.cache/<platform>/packages/context-mode@latest/node_modules/context-mode
308
- // Windows: %LOCALAPPDATA%\<platform>\packages\context-mode@latest\node_modules\context-mode
309
- function cachePluginRoot(platform) {
310
- const subPath = ["packages", "context-mode@latest", "node_modules", "context-mode"];
311
- if (process.platform === "win32") {
312
- const localApp = process.env.LOCALAPPDATA;
313
- if (localApp)
314
- return resolve(localApp, platform, ...subPath);
315
- return resolve(homedir(), "AppData", "Local", platform, ...subPath);
316
- }
317
- return resolve(homedir(), ".cache", platform, ...subPath);
318
- }
319
- function getPluginRoot() {
320
- const platform = detectPlatform().platform;
321
- if (isInProcessPluginPlatform(platform)) {
322
- return cachePluginRoot(platform);
323
- }
324
- return defaultPluginRoot();
325
- }
326
- function getLocalVersion() {
327
- try {
328
- const pkg = JSON.parse(readFileSync(resolve(getPluginRoot(), "package.json"), "utf-8"));
329
- return pkg.version ?? "unknown";
330
- }
331
- catch {
332
- return "unknown";
333
- }
334
- }
335
- async function fetchLatestVersion() {
336
- // Use node:https instead of global fetch to avoid a Windows libuv assertion
337
- // (UV_HANDLE_CLOSING) caused by undici's connection-pool background threads
338
- // racing with process.exit() teardown on Node.js v24+.
339
- return new Promise((resolve) => {
340
- const req = httpsRequest("https://registry.npmjs.org/context-mode/latest", { headers: { Connection: "close" } }, (res) => {
341
- let raw = "";
342
- res.on("data", (chunk) => { raw += chunk; });
343
- res.on("end", () => {
344
- try {
345
- const data = JSON.parse(raw);
346
- resolve(data.version ?? "unknown");
347
- }
348
- catch {
349
- resolve("unknown");
350
- }
351
- });
352
- });
353
- req.on("error", () => resolve("unknown"));
354
- req.setTimeout(5000, () => { req.destroy(); resolve("unknown"); });
355
- req.end();
356
- });
357
- }
358
- /* -------------------------------------------------------
359
- * Doctor — adapter-aware diagnostics
360
- * ------------------------------------------------------- */
361
- function describeStorageSource(dir) {
362
- return dir.envVar ? dir.envVar : "adapter default";
363
- }
364
- function parseFlags(argv) {
365
- const positional = [];
366
- const flags = {};
367
- for (let i = 0; i < argv.length; i++) {
368
- const arg = argv[i];
369
- if (!arg.startsWith("--") || arg === "--") {
370
- positional.push(arg);
371
- continue;
372
- }
373
- const raw = arg.slice(2);
374
- const eq = raw.indexOf("=");
375
- const key = eq >= 0 ? raw.slice(0, eq) : raw;
376
- const inlineValue = eq >= 0 ? raw.slice(eq + 1) : undefined;
377
- const next = argv[i + 1];
378
- const value = inlineValue !== undefined
379
- ? inlineValue
380
- : next && !next.startsWith("--")
381
- ? (i++, next)
382
- : true;
383
- if (key === "include" || key === "exclude") {
384
- const prev = flags[key];
385
- flags[key] = Array.isArray(prev) ? [...prev, String(value)] : [String(value)];
386
- }
387
- else {
388
- flags[key] = value;
389
- }
390
- }
391
- return { positional, flags };
392
- }
393
- function stringFlag(flags, key) {
394
- const v = flags[key];
395
- if (typeof v === "string" && v.length > 0)
396
- return v;
397
- return undefined;
398
- }
399
- function boolFlag(flags, key) {
400
- return flags[key] === true || flags[key] === "true";
401
- }
402
- function stringListFlag(flags, key) {
403
- const v = flags[key];
404
- if (Array.isArray(v))
405
- return v.filter(Boolean);
406
- if (typeof v === "string" && v.length > 0)
407
- return [v];
408
- return undefined;
409
- }
410
- function numberFlag(flags, key, opts = {}) {
411
- const raw = stringFlag(flags, key);
412
- if (!raw)
413
- return undefined;
414
- const n = Number(raw);
415
- const min = opts.min ?? 1;
416
- if (!Number.isInteger(n) || n < min)
417
- throw new Error(`--${key} must be an integer >= ${min}`);
418
- return n;
419
- }
420
- function extFlag(flags) {
421
- const raw = stringFlag(flags, "ext") ?? stringFlag(flags, "extensions");
422
- if (!raw)
423
- return undefined;
424
- const exts = raw
425
- .split(",")
426
- .map((x) => x.trim())
427
- .filter(Boolean)
428
- .map((x) => (x.startsWith(".") ? x : `.${x}`));
429
- return exts.length > 0 ? exts : undefined;
430
- }
431
- function resolveCliProjectDir(projectFlag, fallback) {
432
- if (projectFlag)
433
- return resolve(projectFlag);
434
- return resolve(fallback);
435
- }
436
- async function openCliContentStore(projectDir) {
437
- const adapter = await getAdapter(detectPlatform().platform);
438
- const contentStorage = resolveContentStorageDir(() => adapter.getSessionDir());
439
- const contentDir = ensureWritableStorageDir(contentStorage);
440
- const { resolveContentStorePath } = await import("./session/db.js");
441
- const dbPath = resolveContentStorePath({ projectDir, contentDir });
442
- return { store: new ContentStore(dbPath), dbPath, contentDir };
443
- }
444
- function defaultSourceForPath(absPath) {
445
- try {
446
- if (statSync(absPath).isDirectory())
447
- return `project:${basename(absPath) || absPath}`;
448
- }
449
- catch { /* path errors are reported by the index command */ }
450
- return absPath;
451
- }
452
- function assertReadAllowed(path, projectDir) {
453
- const denyGlobs = readToolDenyPatterns("Read", projectDir);
454
- const denied = evaluateFilePath(path, denyGlobs, process.platform === "win32", projectDir);
455
- if (denied.denied) {
456
- throw new Error(`Read denied by policy: ${path}`);
457
- }
458
- }
459
- async function indexCommand(argv) {
460
- try {
461
- const parsed = parseFlags(argv);
462
- const target = parsed.positional[0];
463
- if (!target || target === "-h" || target === "--help") {
464
- console.log("Usage: context-mode index <path> [--source label] [--project path] [--max-files n] [--max-depth n] [--ext .ts,.md]");
465
- return target ? 0 : 1;
466
- }
467
- const absPath = isAbsolute(target) ? resolve(target) : resolve(process.cwd(), target);
468
- if (!existsSync(absPath))
469
- throw new Error(`Path does not exist: ${absPath}`);
470
- const st = statSync(absPath);
471
- const projectDir = resolveCliProjectDir(stringFlag(parsed.flags, "project"), st.isDirectory() ? absPath : dirname(absPath));
472
- const source = stringFlag(parsed.flags, "source") ?? defaultSourceForPath(absPath);
473
- const { store, dbPath } = await openCliContentStore(projectDir);
474
- try {
475
- assertReadAllowed(absPath, projectDir);
476
- if (st.isDirectory()) {
477
- const denyGlobs = readToolDenyPatterns("Read", projectDir);
478
- const result = store.indexDirectory({
479
- path: absPath,
480
- source,
481
- include: stringListFlag(parsed.flags, "include"),
482
- exclude: stringListFlag(parsed.flags, "exclude"),
483
- maxDepth: numberFlag(parsed.flags, "max-depth", { min: 0 }),
484
- maxFiles: numberFlag(parsed.flags, "max-files"),
485
- extensions: extFlag(parsed.flags),
486
- respectGitignore: !boolFlag(parsed.flags, "no-gitignore"),
487
- followSymlinks: boolFlag(parsed.flags, "follow-symlinks"),
488
- perFileDeny: (filePath) => {
489
- try {
490
- return evaluateFilePath(filePath, denyGlobs, process.platform === "win32", projectDir).denied;
491
- }
492
- catch {
493
- return false;
494
- }
495
- },
496
- });
497
- const cap = result.capped ? ` (cap reached at ${result.filesIndexed} files)` : "";
498
- const denied = result.denied > 0 ? `; ${result.denied} denied` : "";
499
- const failed = result.failed > 0 ? `; ${result.failed} failed` : "";
500
- console.log(`Indexed ${result.filesIndexed} files (${result.totalChunks} sections) from ${absPath}${cap}${denied}${failed}`);
501
- }
502
- else {
503
- const result = store.index({ path: absPath, source });
504
- console.log(`Indexed ${result.totalChunks} sections (${result.codeChunks} with code) from ${absPath}`);
505
- }
506
- console.log(`Source: ${source}`);
507
- console.log(`Project: ${projectDir}`);
508
- console.log(`DB: ${dbPath}`);
509
- return 0;
510
- }
511
- finally {
512
- store.close();
513
- }
514
- }
515
- catch (err) {
516
- const message = err instanceof Error ? err.message : String(err);
517
- console.error(`context-mode index: ${message}`);
518
- return 1;
519
- }
520
- }
521
- async function searchCommand(argv) {
522
- try {
523
- const parsed = parseFlags(argv);
524
- const query = parsed.positional.join(" ").trim();
525
- if (!query || query === "-h" || query === "--help") {
526
- console.log("Usage: context-mode search <query...> [--source label] [--project path] [--limit n] [--type code|prose]");
527
- return query ? 0 : 1;
528
- }
529
- const projectDir = resolveCliProjectDir(stringFlag(parsed.flags, "project"), process.cwd());
530
- const { store, dbPath } = await openCliContentStore(projectDir);
531
- try {
532
- const limit = numberFlag(parsed.flags, "limit") ?? 3;
533
- const type = stringFlag(parsed.flags, "type");
534
- if (type && type !== "code" && type !== "prose")
535
- throw new Error("--type must be code or prose");
536
- const results = store.searchWithFallback(query, limit, stringFlag(parsed.flags, "source"), type);
537
- if (results.length === 0) {
538
- console.log(`No matches for: ${query}`);
539
- console.log(`Project: ${projectDir}`);
540
- console.log(`DB: ${dbPath}`);
541
- return 0;
542
- }
543
- for (const [i, r] of results.entries()) {
544
- const content = r.content.replace(/\s+/g, " ").trim();
545
- const snippet = content.length > 500 ? `${content.slice(0, 500)}...` : content;
546
- console.log(`## ${i + 1}. ${r.title}`);
547
- console.log(`Source: ${r.source}`);
548
- console.log(`Type: ${r.contentType}`);
549
- console.log(snippet);
550
- console.log("");
551
- }
552
- return 0;
553
- }
554
- finally {
555
- store.close();
556
- }
557
- }
558
- catch (err) {
559
- const message = err instanceof Error ? err.message : String(err);
560
- console.error(`context-mode search: ${message}`);
561
- return 1;
562
- }
563
- }
564
- function logStorageDir(dir) {
565
- try {
566
- ensureWritableStorageDir(dir);
567
- p.log.success(color.green(`Storage ${dir.kind}: PASS`) +
568
- color.dim(` — ${dir.path} (${describeStorageSource(dir)})`));
569
- return 0;
570
- }
571
- catch (err) {
572
- if (err instanceof StorageDirectoryError) {
573
- p.log.error(color.red(`Storage ${dir.kind}: FAIL`) +
574
- color.dim(` — ${formatStorageDirectoryError(err)}`));
575
- return 1;
576
- }
577
- throw err;
578
- }
579
- }
580
- async function doctor() {
581
- if (process.stdout.isTTY)
582
- console.clear();
583
- // Detect platform
584
- const detection = detectPlatform();
585
- const adapter = await getAdapter(detection.platform);
586
- p.intro(color.bgMagenta(color.white(" context-mode doctor ")));
587
- p.log.info(`Platform: ${color.cyan(adapter.name)}` +
588
- color.dim(` (${detection.confidence} confidence — ${detection.reason})`));
589
- let criticalFails = 0;
590
- try {
591
- const sessionDir = resolveSessionStorageDir(() => adapter.getSessionDir());
592
- const contentDir = resolveContentStorageDir(() => sessionDir.path);
593
- const statsDir = resolveStatsStorageDir(() => sessionDir.path);
594
- p.note([
595
- `sessions: ${sessionDir.path} (${describeStorageSource(sessionDir)})`,
596
- `content: ${contentDir.path} (${describeStorageSource(contentDir)})`,
597
- `stats: ${statsDir.path} (${describeStorageSource(statsDir)})`,
598
- ].join("\n"), "Storage paths");
599
- criticalFails += logStorageDir(sessionDir);
600
- criticalFails += logStorageDir(contentDir);
601
- criticalFails += logStorageDir(statsDir);
602
- }
603
- catch (err) {
604
- if (err instanceof StorageDirectoryError) {
605
- criticalFails++;
606
- p.log.error(color.red(`Storage ${err.kind}: FAIL`) +
607
- color.dim(` — ${formatStorageDirectoryError(err)}`));
608
- }
609
- else {
610
- throw err;
611
- }
612
- }
613
- const s = p.spinner();
614
- s.start("Running diagnostics");
615
- let runtimes;
616
- let available;
617
- try {
618
- runtimes = detectRuntimes();
619
- available = getAvailableLanguages(runtimes);
620
- }
621
- catch {
622
- s.stop("Diagnostics partial");
623
- p.log.warn(color.yellow("Could not detect runtimes") + color.dim(" — module may be missing, restart session after upgrade"));
624
- p.outro(color.yellow("Doctor could not fully run — try again after restarting"));
625
- return 1;
626
- }
627
- s.stop("Diagnostics complete");
628
- // Runtime check
629
- p.note(getRuntimeSummary(runtimes), "Runtimes");
630
- // ── Issue #564 — Linux + Node < 22.5 + no Bun is unsafe ────────────
631
- // V8's madvise(MADV_DONTNEED) can corrupt better-sqlite3's native addon
632
- // `.got.plt` on Linux, causing sporadic SIGSEGV (1-4/hour). The 22.5
633
- // gate (`hasModernSqlite()` in src/db-base.ts:226-244) is the contract:
634
- // at or above it we use node:sqlite (built-in, no native addon, no
635
- // .got.plt to corrupt); below it we fall through to better-sqlite3
636
- // which WILL crash. engines.node + a hard-fail postinstall guard this
637
- // at install time, but doctor() surfaces it for already-installed users
638
- // (and for adapters whose MCP host swallows stderr during install).
639
- // Refs:
640
- // - https://github.com/nodejs/node/issues/62515
641
- // - https://github.com/mksglu/context-mode/issues/564
642
- {
643
- const { hasModernSqlite } = await import("./db-base.js");
644
- if (process.platform === "linux" &&
645
- !hasModernSqlite() &&
646
- !hasBunRuntime()) {
647
- criticalFails++;
648
- p.log.error(color.red("Node version: FAIL") +
649
- ` — Linux + Node ${process.versions.node} is unsafe (SIGSEGV)` +
650
- color.dim("\n context-mode requires Node.js >= 22.5 (or Bun) on Linux to avoid the" +
651
- "\n V8 madvise(MADV_DONTNEED) SIGSEGV in better-sqlite3 (1-4/hour)." +
652
- "\n Refs: https://github.com/nodejs/node/issues/62515" +
653
- "\n https://github.com/mksglu/context-mode/issues/564" +
654
- "\n Fix: nvm install 22.5 && nvm use 22.5 && npm install -g context-mode" +
655
- "\n Or: curl -fsSL https://bun.sh/install | bash && bun add -g context-mode"));
656
- }
657
- }
658
- // Speed tier
659
- if (hasBunRuntime()) {
660
- p.log.success(color.green("Performance: FAST") +
661
- " — Bun detected for JS/TS execution");
662
- }
663
- else {
664
- p.log.warn(color.yellow("Performance: NORMAL") +
665
- " — Using Node.js (install Bun for 3-5x speed boost)");
666
- }
667
- // Language coverage
668
- const total = 11;
669
- const pct = ((available.length / total) * 100).toFixed(0);
670
- if (available.length < 2) {
671
- criticalFails++;
672
- p.log.error(color.red(`Language coverage: ${available.length}/${total} (${pct}%)`) +
673
- " — too few runtimes detected" +
674
- color.dim(` — ${available.join(", ") || "none"}`));
675
- }
676
- else {
677
- p.log.info(`Language coverage: ${available.length}/${total} (${pct}%)` +
678
- color.dim(` — ${available.join(", ")}`));
679
- }
680
- // Server test
681
- p.log.step("Testing server initialization...");
682
- try {
683
- const { PolyglotExecutor } = await import("./executor.js");
684
- const executor = new PolyglotExecutor({ runtimes });
685
- const result = await executor.execute({
686
- language: "javascript",
687
- code: 'console.log("ok");',
688
- timeout: 5000,
689
- });
690
- if (result.exitCode === 0 && result.stdout.trim() === "ok") {
691
- p.log.success(color.green("Server test: PASS"));
692
- }
693
- else {
694
- criticalFails++;
695
- const detail = result.stderr?.trim() ? ` (${result.stderr.trim().slice(0, 200)})` : "";
696
- p.log.error(color.red("Server test: FAIL") + ` — exit ${result.exitCode}${detail}`);
697
- }
698
- }
699
- catch (err) {
700
- const message = err instanceof Error ? err.message : String(err);
701
- if (message.includes("Cannot find module") || message.includes("MODULE_NOT_FOUND")) {
702
- p.log.warn(color.yellow("Server test: SKIP") + color.dim(" — module not available (restart session after upgrade)"));
703
- }
704
- else {
705
- criticalFails++;
706
- p.log.error(color.red("Server test: FAIL") + ` — ${message}`);
707
- }
708
- }
709
- // Hooks — adapter-aware validation
710
- p.log.step(`Checking ${adapter.name} hooks configuration...`);
711
- const pluginRoot = getPluginRoot();
712
- const hookResults = adapter.validateHooks(pluginRoot);
713
- for (const result of hookResults) {
714
- if (result.status === "pass") {
715
- p.log.success(color.green(`${result.check}: PASS`) + ` — ${result.message}`);
716
- }
717
- else if (result.status === "warn") {
718
- p.log.warn(color.yellow(`${result.check}: WARN`) +
719
- ` — ${result.message}` +
720
- (result.fix ? color.dim(`\n Run: ${result.fix}`) : ""));
721
- }
722
- else {
723
- p.log.error(color.red(`${result.check}: FAIL`) +
724
- ` — ${result.message}` +
725
- (result.fix ? color.dim(`\n Run: ${result.fix}`) : ""));
726
- }
727
- }
728
- // Hook scripts exist — Algo-D1 protocol path takes precedence.
729
- // Adapters that override `getHealthChecks` (claude-code today) get a
730
- // direct `existsSync(join(pluginRoot, "hooks", scriptName))` per
731
- // HOOK_SCRIPTS entry — no regex round-trip on a hook command, so the
732
- // #548 doubled-path FAIL class can't surface. Adapters that don't
733
- // override fall through to the legacy `getHookScriptPaths` flow which
734
- // generates the hook config and parses each command via
735
- // `extractHookScriptPath`. Post-D3 every adapter emits buildNodeCommand-
736
- // shape, so the legacy flow is also safe — but the direct existsSync
737
- // path is strictly preferable when the adapter offers it.
738
- p.log.step("Checking hook scripts...");
739
- const adapterHealthChecks = adapter.getHealthChecks?.(pluginRoot) ?? [];
740
- if (adapterHealthChecks.length > 0) {
741
- for (const hc of adapterHealthChecks) {
742
- const result = hc.check();
743
- if (result.status === "OK") {
744
- p.log.success(color.green(`${hc.name}: PASS`) +
745
- (result.detail ? color.dim(` — ${result.detail}`) : ""));
746
- }
747
- else {
748
- p.log.error(color.red(`${hc.name}: FAIL`) +
749
- (result.detail ? color.dim(` — ${result.detail}`) : ""));
750
- }
751
- }
752
- }
753
- else {
754
- const hookScriptPaths = getHookScriptPaths(adapter, pluginRoot);
755
- if (hookScriptPaths.length === 0) {
756
- p.log.success(color.green("Hook scripts: PASS") + color.dim(" — no direct .mjs script paths to verify"));
757
- }
758
- else {
759
- for (const scriptPath of hookScriptPaths) {
760
- const absolutePath = resolve(pluginRoot, scriptPath);
761
- try {
762
- accessSync(absolutePath, constants.R_OK);
763
- p.log.success(color.green("Hook script exists: PASS") + color.dim(` — ${absolutePath}`));
764
- }
765
- catch {
766
- p.log.error(color.red("Hook script exists: FAIL") +
767
- color.dim(` — not found at ${absolutePath}`));
768
- }
769
- }
770
- }
771
- }
772
- // Plugin registration — adapter-aware
773
- p.log.step(`Checking ${adapter.name} plugin registration...`);
774
- const pluginCheck = adapter.checkPluginRegistration();
775
- if (pluginCheck.status === "pass") {
776
- p.log.success(color.green("Plugin enabled: PASS") + color.dim(` — ${pluginCheck.message}`));
777
- }
778
- else {
779
- p.log.warn(color.yellow("Plugin enabled: WARN") +
780
- ` — ${pluginCheck.message}`);
781
- }
782
- // ── Issue #613 — proactive Tier C absolute-path detection ───────────
783
- // PR #620 fixed `buildHookCommand` for vscode-copilot + jetbrains-copilot
784
- // so future writes are CLI-dispatcher-shape. But users who ran
785
- // /ctx-upgrade on v1.0.136 or earlier are still carrying poisoned
786
- // committable files in their workspace:
787
- // - `.github/hooks/context-mode.json` (vscode-copilot, team-shared)
788
- // - `.jetbrains/copilot/hooks.json` (jetbrains-copilot, team-shared)
789
- // - `.cursor/hooks.json` (cursor, team-shared)
790
- // Per ISSUE-613-VERDICT §6.1 these are Tier C — workspace-committed
791
- // cross-machine config. Doctor scans them for absolute paths and
792
- // fnm_multishells shims; if found, FAIL with `ctx_upgrade` remediation.
793
- // Per ISSUE-604-VERDICT §11 ("silent-green doctor while hooks are dead
794
- // is itself a P0 trust bug") — surface poison BEFORE the user hits a
795
- // runtime failure.
796
- p.log.step("Checking team-shared hook configs in your workspace...");
797
- {
798
- const projectDir = process.cwd();
799
- const tierCFiles = [
800
- ".github/hooks/context-mode.json",
801
- ".cursor/hooks.json",
802
- ".jetbrains/copilot/hooks.json",
803
- ];
804
- let tierCFails = 0;
805
- let tierCChecked = 0;
806
- // Detect absolute-path patterns that should never appear in a
807
- // workspace-committed config. Per Mert's standing Windows-safety rule:
808
- // handle both `/` and `\\` separators.
809
- function isAbsoluteOrShimPath(s) {
810
- // unix absolute
811
- if (s.startsWith("/"))
812
- return true;
813
- // Windows drive-letter absolute (e.g. C:/, C:\)
814
- if (/^[A-Za-z]:[/\\]/.test(s))
815
- return true;
816
- // Windows UNC or escaped-backslash absolute fragments
817
- if (s.includes("\\\\"))
818
- return true;
819
- // fnm shim hint — issue #613 reporter's exact stderr shape
820
- if (s.includes("fnm_multishells"))
821
- return true;
822
- // process.execPath literal baked into JSON
823
- if (s.includes("process.execPath"))
824
- return true;
825
- return false;
826
- }
827
- function recurseStrings(node, hit) {
828
- if (typeof node === "string") {
829
- hit(node);
830
- }
831
- else if (Array.isArray(node)) {
832
- for (const item of node)
833
- recurseStrings(item, hit);
834
- }
835
- else if (node && typeof node === "object") {
836
- for (const v of Object.values(node))
837
- recurseStrings(v, hit);
838
- }
839
- }
840
- for (const rel of tierCFiles) {
841
- const abs = resolve(projectDir, rel);
842
- if (!existsSync(abs))
843
- continue; // missing config → SKIP, no false fail
844
- tierCChecked++;
845
- try {
846
- const parsed = JSON.parse(readFileSync(abs, "utf-8"));
847
- const offenders = [];
848
- recurseStrings(parsed, (s) => {
849
- if (isAbsoluteOrShimPath(s))
850
- offenders.push(s);
851
- });
852
- if (offenders.length > 0) {
853
- criticalFails++;
854
- tierCFails++;
855
- // Truncate to one example to keep output readable; show count.
856
- const example = offenders[0].length > 100
857
- ? offenders[0].slice(0, 97) + "..."
858
- : offenders[0];
859
- p.log.error(color.red(`Hook config: FAIL`) +
860
- ` — ${rel} has your machine's local paths baked in` +
861
- color.dim("\n This file is committed to git, so teammates and CI will get your path and the hooks will break for them." +
862
- `\n Found ${offenders.length} hard-coded path(s), e.g.: ${example}` +
863
- "\n Fix: run /context-mode:ctx-upgrade — it rewrites the file to a portable form that works on every machine." +
864
- "\n Details: https://github.com/mksglu/context-mode/issues/613"));
865
- }
866
- else {
867
- p.log.success(color.green("Hook config: PASS") +
868
- color.dim(` — ${rel} is portable (no hard-coded paths)`));
869
- }
870
- }
871
- catch (err) {
872
- // Malformed JSON should not crash doctor; warn and move on.
873
- const msg = err instanceof Error ? err.message : String(err);
874
- p.log.warn(color.yellow(`Hook config: WARN`) +
875
- ` — ${rel} is not valid JSON` +
876
- color.dim("\n Doctor cannot scan it for portability issues until the file parses." +
877
- "\n Fix: open the file and check it in a JSON validator, or delete it and run /context-mode:ctx-upgrade to regenerate." +
878
- `\n Parser said: ${msg.slice(0, 160)}`));
879
- }
880
- }
881
- if (tierCChecked === 0) {
882
- p.log.info(color.dim("Hook config: SKIP — no team-shared hook configs found in this workspace"));
883
- }
884
- else if (tierCFails === 0) {
885
- // already individual PASS messages above; no need for a summary
886
- }
887
- }
888
- // ── Issue #609 — proactive stale `.mcp.json` detection ──────────────
889
- // PR #620 deleted the per-version cache `.mcp.json` write from cli.ts
890
- // and shipped `sweepStaleMcpJson` to clean up any pre-existing copies.
891
- // But users on the field may still have stale `.mcp.json` files left
892
- // by /ctx-upgrade flows that ran before PR #620 (or by Claude Code's
893
- // native auto-update copying a poisoned file forward). Surface those
894
- // as WARN (recoverable — next ctx_upgrade sweeps them) so the user
895
- // knows what to do instead of being told everything is green while
896
- // the file lingers on disk.
897
- // Per ISSUE-604-VERDICT §11 same trust contract as Tier C check above.
898
- p.log.step("Checking for leftover .mcp.json files from older versions...");
899
- {
900
- const cacheRoot = join(homedir(), ".claude", "plugins", "cache", "context-mode", "context-mode");
901
- if (!existsSync(cacheRoot)) {
902
- p.log.info(color.dim("Leftover .mcp.json check: SKIP — no plugin cache exists yet (Claude Code has not installed context-mode here)"));
903
- }
904
- else {
905
- let staleCount = 0;
906
- const staleVersions = [];
907
- try {
908
- const versionDirs = readdirSync(cacheRoot);
909
- for (const v of versionDirs) {
910
- const candidate = join(cacheRoot, v, ".mcp.json");
911
- if (existsSync(candidate)) {
912
- staleCount++;
913
- if (staleVersions.length < 5)
914
- staleVersions.push(v);
915
- }
916
- }
917
- }
918
- catch (err) {
919
- const msg = err instanceof Error ? err.message : String(err);
920
- p.log.warn(color.yellow("Leftover .mcp.json check: WARN") +
921
- ` — could not read the plugin cache directory` +
922
- color.dim(`\n Path: ${cacheRoot}` +
923
- `\n Reason: ${msg.slice(0, 160)}` +
924
- "\n Fix: check that the directory is readable, then re-run doctor. If the issue persists, run /context-mode:ctx-upgrade."));
925
- staleCount = 0;
926
- }
927
- if (staleCount === 0) {
928
- p.log.success(color.green("Leftover .mcp.json check: PASS") +
929
- color.dim(" — no old .mcp.json files in the plugin cache"));
930
- }
931
- else {
932
- // WARN, not FAIL — per architect spec this is recoverable.
933
- p.log.warn(color.yellow("Leftover .mcp.json check: WARN") +
934
- ` — found ${staleCount} old .mcp.json file(s) left over from previous context-mode versions` +
935
- color.dim("\n These are harmless but should be cleaned up so they cannot confuse Claude Code after an auto-update." +
936
- `\n Versions affected: ${staleVersions.join(", ")}${staleCount > staleVersions.length ? ", ..." : ""}` +
937
- "\n Fix: run /context-mode:ctx-upgrade — it sweeps these files automatically on the next run." +
938
- "\n Details: https://github.com/mksglu/context-mode/issues/609"));
939
- }
940
- }
941
- }
942
- // FTS5 / SQLite
943
- p.log.step("Checking FTS5 / SQLite...");
944
- try {
945
- const Database = (await import("./db-base.js")).loadDatabase();
946
- const db = new Database(":memory:");
947
- db.exec("CREATE VIRTUAL TABLE fts_test USING fts5(content)");
948
- db.exec("INSERT INTO fts_test(content) VALUES ('hello world')");
949
- const row = db.prepare("SELECT * FROM fts_test WHERE fts_test MATCH 'hello'").get();
950
- db.close();
951
- if (row && row.content === "hello world") {
952
- p.log.success(color.green("FTS5 / SQLite: PASS") + " — native module works");
953
- }
954
- else {
955
- criticalFails++;
956
- p.log.error(color.red("FTS5 / SQLite: FAIL") + " — query returned unexpected result");
957
- }
958
- }
959
- catch (err) {
960
- const message = err instanceof Error ? err.message : String(err);
961
- // Distinguish package-missing from binding-missing (#514). Both
962
- // throw with similar shapes from `import("better-sqlite3")` but the
963
- // recovery commands differ:
964
- // - package-missing → `npm install better-sqlite3 --no-optional`
965
- // (npm@7+ silently drops optionalDependencies on engine
966
- // mismatch, e.g. Node 26 vs better-sqlite3@12.x — we name the
967
- // package explicitly + flip the optional filter to recover)
968
- // - binding-missing → `npm rebuild better-sqlite3` (#408 flow,
969
- // Windows + missing prebuild-install shim)
970
- const pluginRootForDoctor = getPluginRoot();
971
- const bsqPackageDir = resolve(pluginRootForDoctor, "node_modules", "better-sqlite3");
972
- const packageMissing = !existsSync(bsqPackageDir);
973
- if (packageMissing) {
974
- criticalFails++;
975
- p.log.error(color.red("FTS5 / better-sqlite3: FAIL") +
976
- color.dim(" — package-missing") +
977
- color.dim(`\n Path: ${bsqPackageDir}` +
978
- "\n Root cause: npm silently skipped better-sqlite3 because the package's `engines` field excluded the running Node (issue #514, e.g. Node 26 vs better-sqlite3@12.x)." +
979
- `\n Try (primary): cd "${pluginRootForDoctor}" && npm install better-sqlite3 --no-optional` +
980
- "\n Try (fallback): /context-mode:ctx-upgrade"));
981
- }
982
- else if (message.includes("Cannot find module") || message.includes("MODULE_NOT_FOUND")) {
983
- p.log.warn(color.yellow("FTS5 / better-sqlite3: SKIP") + color.dim(" — module not available (restart session after upgrade)"));
984
- }
985
- else {
986
- criticalFails++;
987
- // Detect better-sqlite3 native bindings-missing pattern (issue #408).
988
- // The `bindings` package throws "Could not locate the bindings file"
989
- // when better_sqlite3.node failed to install — typical on Windows
990
- // when prebuild-install was not on PATH so install fell through to
991
- // node-gyp without an MSVC toolchain.
992
- const isBindingsMissing = /Could not locate the bindings file/i.test(message) ||
993
- /bindings\.node/i.test(message) ||
994
- /\bbindings\b/i.test(message);
995
- if (isBindingsMissing && process.platform === "win32") {
996
- p.log.error(color.red("FTS5 / better-sqlite3: FAIL") +
997
- ` — ${message}` +
998
- color.dim("\n Root cause: prebuild-install was likely not on PATH, so install fell through to node-gyp without an MSVC toolchain (Windows)." +
999
- "\n Try (primary): npm install better-sqlite3 # re-resolves the dep tree and re-links the prebuild-install bin shim to fetch a prebuilt binary" +
1000
- "\n Try (fallback): npm rebuild better-sqlite3"));
1001
- }
1002
- else {
1003
- p.log.error(color.red("FTS5 / better-sqlite3: FAIL") +
1004
- ` — ${message}` +
1005
- color.dim("\n Try: npm rebuild better-sqlite3"));
1006
- }
1007
- }
1008
- }
1009
- // Version check — adapter-aware
1010
- p.log.step("Checking versions...");
1011
- const localVersion = getLocalVersion();
1012
- const latestVersion = await fetchLatestVersion();
1013
- const installedVersion = adapter.getInstalledVersion();
1014
- if (latestVersion === "unknown") {
1015
- p.log.warn(color.yellow("npm (MCP): WARN") +
1016
- ` — local v${localVersion}, could not reach npm registry`);
1017
- }
1018
- else if (localVersion === latestVersion) {
1019
- p.log.success(color.green("npm (MCP): PASS") +
1020
- ` — v${localVersion}`);
1021
- }
1022
- else {
1023
- p.log.warn(color.yellow("npm (MCP): WARN") +
1024
- ` — local v${localVersion}, latest v${latestVersion}` +
1025
- color.dim("\n Run: /context-mode:ctx-upgrade"));
1026
- }
1027
- if (installedVersion === "standalone") {
1028
- p.log.info(color.dim(`${adapter.name}: standalone MCP mode`) +
1029
- " — no platform plugin version to compare");
1030
- }
1031
- else if (installedVersion === "not installed") {
1032
- p.log.info(color.dim(`${adapter.name}: not installed`) +
1033
- " — using standalone MCP mode");
1034
- }
1035
- else if (latestVersion !== "unknown" && installedVersion === latestVersion) {
1036
- p.log.success(color.green(`${adapter.name}: PASS`) +
1037
- ` — v${installedVersion}`);
1038
- }
1039
- else if (latestVersion !== "unknown") {
1040
- p.log.warn(color.yellow(`${adapter.name}: WARN`) +
1041
- ` — v${installedVersion}, latest v${latestVersion}` +
1042
- color.dim("\n Run: /context-mode:ctx-upgrade"));
1043
- }
1044
- else {
1045
- p.log.info(`${adapter.name}: v${installedVersion}` +
1046
- color.dim(" — could not verify against npm registry"));
1047
- }
1048
- // Summary
1049
- if (criticalFails > 0) {
1050
- p.outro(color.red(`Diagnostics failed — ${criticalFails} critical issue(s) found`));
1051
- return 1;
1052
- }
1053
- p.outro(available.length >= 4
1054
- ? color.green("Diagnostics complete!")
1055
- : color.yellow("Some checks need attention — see above for details"));
1056
- return 0;
1057
- }
1058
- /* -------------------------------------------------------
1059
- * Insight — hosted analytics dashboard
1060
- * ------------------------------------------------------- */
1061
- // Insight pivoted from a locally-built dashboard to the hosted product at
1062
- // context-mode.com/insight (the landing page is the single source of truth).
1063
- // The command now just opens that URL in the default browser.
1064
- async function insight() {
1065
- const url = "https://context-mode.com/insight";
1066
- console.log(`\n context-mode Insight\n ${url}\n`);
1067
- // Open browser — execFile with arg array, no shell interpolation.
1068
- openInBrowser(url);
1069
- }
1070
- /* -------------------------------------------------------
1071
- * Upgrade — adapter-aware hook configuration
1072
- * ------------------------------------------------------- */
1073
- async function upgrade(opts) {
1074
- if (process.stdout.isTTY)
1075
- console.clear();
1076
- // Issue #542 — when the MCP ctx_upgrade handler threads through an
1077
- // explicit --platform <id> (resolved from live clientInfo), trust it
1078
- // over the local heuristic chain. detectPlatform() with no args cannot
1079
- // see the MCP handshake and falls through to the config-dir tier,
1080
- // which misdetects Pi/OMP installs as Cursor on systems where both
1081
- // ~/.cursor/ and ~/.pi/ exist.
1082
- const detection = opts?.platform
1083
- ? { platform: opts.platform, confidence: "high", reason: `--platform ${opts.platform} from ctx_upgrade handler` }
1084
- : detectPlatform();
1085
- const adapter = await getAdapter(detection.platform);
1086
- p.intro(color.bgCyan(color.black(" context-mode upgrade ")));
1087
- p.log.info(`Platform: ${color.cyan(adapter.name)}` +
1088
- color.dim(` (${detection.confidence} confidence)`));
1089
- let pluginRoot = getPluginRoot();
1090
- const changes = [];
1091
- const s = p.spinner();
1092
- // Step 0: Sync the marketplace clone (#418).
1093
- // Claude Code reads plugin metadata from ~/.claude/plugins/marketplaces/context-mode/.
1094
- // Without a git pull there, the marketplace stays pinned at the install-time
1095
- // commit and CC keeps reporting the old version even after our cache dir is
1096
- // updated — users then see "ctx-upgrade succeeded" but nothing actually
1097
- // changed at the plugin-system level.
1098
- // Issue #460 round-3: route through resolveClaudeConfigDir so users who
1099
- // relocate their CC config root keep the marketplace clone in the same tree.
1100
- const marketplaceDir = resolve(resolveClaudeConfigDir(), "plugins", "marketplaces", "context-mode");
1101
- if (existsSync(join(marketplaceDir, ".git"))) {
1102
- s.start("Syncing marketplace clone");
1103
- try {
1104
- // Preserve user dev edits (Mert-class users symlink the clone to a worktree).
1105
- const statusOut = execFileSync("git", ["-C", marketplaceDir, "status", "--porcelain"], { stdio: "pipe", encoding: "utf-8", timeout: 5000 });
1106
- if (statusOut.trim()) {
1107
- s.stop(color.yellow("Marketplace clone has local edits — skipping git pull"));
1108
- p.log.info(color.dim(` Run manually: git -C "${marketplaceDir}" stash && git pull --ff-only`));
1109
- }
1110
- else {
1111
- execFileSync("git", ["-C", marketplaceDir, "fetch", "--tags", "origin"], { stdio: "pipe", timeout: 30000 });
1112
- execFileSync("git", ["-C", marketplaceDir, "reset", "--hard", "origin/HEAD"], { stdio: "pipe", timeout: 10000 });
1113
- s.stop(color.green("Marketplace clone synced"));
1114
- changes.push("Marketplace clone updated to upstream");
1115
- }
1116
- }
1117
- catch (err) {
1118
- const message = err instanceof Error ? err.message : String(err);
1119
- s.stop(color.yellow("Marketplace sync skipped"));
1120
- p.log.warn(color.yellow("git refresh on marketplace failed") + ` — ${message}`);
1121
- p.log.info(color.dim(" Continuing — cache dir update will still happen."));
1122
- }
1123
- }
1124
- // Step 1: Pull latest from GitHub
1125
- p.log.step("Pulling latest from GitHub...");
1126
- const localVersion = getLocalVersion();
1127
- const tmpDir = join(tmpdir(), `context-mode-upgrade-${Date.now()}`);
1128
- s.start("Cloning mksglu/context-mode");
1129
- try {
1130
- execFileSync("git", ["clone", "--depth", "1", "https://github.com/mksglu/context-mode.git", tmpDir], { stdio: "pipe", timeout: 30000 });
1131
- s.stop("Downloaded");
1132
- const srcDir = tmpDir;
1133
- const newPkg = JSON.parse(readFileSync(resolve(srcDir, "package.json"), "utf-8"));
1134
- const newVersion = newPkg.version ?? "unknown";
1135
- if (newVersion === localVersion) {
1136
- p.log.success(color.green("Already on latest") + ` — v${localVersion}`);
1137
- rmSync(tmpDir, { recursive: true, force: true });
1138
- }
1139
- else {
1140
- p.log.info(`Update available: ${color.yellow("v" + localVersion)} → ${color.green("v" + newVersion)}`);
1141
- // v1.0.128 — Issue #559: terminate sibling MCP servers BEFORE installing
1142
- // new files. Historically /ctx-upgrade rsynced new code over the old
1143
- // tree but never signalled the running MCP server, so the previous
1144
- // version stayed alive holding stdio + DB handles. Across enough
1145
- // upgrades users observed 5+ context-mode start.mjs processes pinned
1146
- // to RAM. Discovery + kill must happen before npm install to avoid
1147
- // racing against the EXCLUSIVE lock the new server claims on first
1148
- // ctx_search (see #560 fix). Wrapped in try/catch so a missing pgrep
1149
- // (stripped Linux distro) or unavailable PowerShell (weird Windows)
1150
- // can never block the upgrade itself.
1151
- try {
1152
- const siblingPids = discoverSiblingMcpPids({
1153
- ownPid: process.pid,
1154
- ownPpid: process.ppid,
1155
- });
1156
- if (siblingPids.length > 0) {
1157
- const killReport = await killSiblingMcpServers({ pids: siblingPids });
1158
- if (killReport.totalKilled > 0) {
1159
- // Concise summary only — no PIDs in the user-facing log to keep
1160
- // the line readable. Plural-aware so "1 sibling MCP server" reads
1161
- // naturally alongside "3 sibling MCP servers".
1162
- const noun = killReport.totalKilled === 1
1163
- ? "sibling MCP server"
1164
- : "sibling MCP servers";
1165
- p.log.info(color.dim(`Stopped ${killReport.totalKilled} ${noun} (SIGTERM: ${killReport.terminatedBySigterm}, SIGKILL: ${killReport.terminatedBySigkill})`));
1166
- }
1167
- }
1168
- }
1169
- catch { /* never block upgrade on discovery/kill failure */ }
1170
- // Step 2: Install dependencies + build
1171
- s.start("Installing dependencies & building");
1172
- const vsYear = detectWindowsVsYear();
1173
- npmExecFile(["install", "--no-audit", "--no-fund"], {
1174
- cwd: srcDir,
1175
- stdio: "pipe",
1176
- timeout: 120000,
1177
- ...(vsYear ? { env: { ...process.env, npm_config_msvs_version: vsYear } } : {}),
1178
- });
1179
- npmExecFile(["run", "build"], {
1180
- cwd: srcDir,
1181
- stdio: "pipe",
1182
- timeout: 60000,
1183
- });
1184
- s.stop("Built successfully");
1185
- // Step 3: Update in-place
1186
- s.start("Updating files in-place");
1187
- // Old version dirs are cleaned lazily by sessionstart.mjs (age-gated >1h)
1188
- // to avoid breaking active sessions that still reference them (#181).
1189
- // Read files list from cloned repo's package.json so new directories
1190
- // (like insight/) are automatically included without chicken-and-egg issues
1191
- // where the old CLI doesn't know about new directories.
1192
- const clonedPkg = JSON.parse(readFileSync(resolve(srcDir, "package.json"), "utf-8"));
1193
- const items = [
1194
- ...(clonedPkg.files || []),
1195
- "src", "package.json",
1196
- ];
1197
- // Supply-chain containment on items[]. A compromised upstream tag
1198
- // shipping files: ["../../.ssh/authorized_keys"] or an absolute
1199
- // path would, without a guard, hand rmSync+cpSync an arbitrary
1200
- // destination under the user's UID. resolve(P, "/abs") discards P,
1201
- // so the absolute-path variant escapes too. Reject items whose
1202
- // resolved path escapes either srcDir or pluginRoot. Mirrors the
1203
- // pattern hooks/heal-partial-install.mjs already uses for its own
1204
- // files[] expansion (PR #699).
1205
- //
1206
- // Also refuse to copy any symlink encountered anywhere under a
1207
- // source item. cpSync's default is to preserve source symlinks as
1208
- // destination symlinks; a compromised upstream tag committing a
1209
- // symlink to /etc inside src/ would plant that link in pluginRoot,
1210
- // and the next Claude Code session that loads pluginRoot/src/*
1211
- // would dereference through to the attacker target. Filtering at
1212
- // copy time keeps pluginRoot symlink-free regardless of what the
1213
- // clone shipped.
1214
- const pluginRootWithSep = resolve(pluginRoot) + sep;
1215
- const srcDirWithSep = resolve(srcDir) + sep;
1216
- const refuseSymlinks = (src) => {
1217
- try {
1218
- return !lstatSync(src).isSymbolicLink();
1219
- }
1220
- catch {
1221
- return false;
1222
- }
1223
- };
1224
- for (const item of items) {
1225
- const from = resolve(srcDir, item);
1226
- const to = resolve(pluginRoot, item);
1227
- if (!(to + sep).startsWith(pluginRootWithSep))
1228
- continue;
1229
- if (!(from + sep).startsWith(srcDirWithSep))
1230
- continue;
1231
- if (!refuseSymlinks(from))
1232
- continue;
1233
- // Existence-check the source BEFORE the rm so a `files[]` entry that
1234
- // doesn't exist in srcDir can never delete-without-replace at
1235
- // pluginRoot. The catch-all below swallows cpSync failures too, and
1236
- // a swallowed cp after a successful rm is exactly how a partial
1237
- // install lands silently. Mirrors the safe pattern in
1238
- // server.ts's inline-fallback upgrade path (PR #699).
1239
- if (!existsSync(from))
1240
- continue;
1241
- try {
1242
- rmSync(to, { recursive: true, force: true });
1243
- cpSync(from, to, { recursive: true, filter: refuseSymlinks });
1244
- }
1245
- catch { /* best effort, next /ctx-upgrade retries */ }
1246
- }
1247
- // Issue #609 — DO NOT write `.mcp.json` into the plugin cache dir.
1248
- //
1249
- // Historical context: #411 fixed an absolute-path bake by writing the
1250
- // ${CLAUDE_PLUGIN_ROOT} placeholder form here. #531 (commit 9261377)
1251
- // removed `.mcp.json` from `package.json files[]` so the npm tarball
1252
- // stopped shipping it. But the cli-side write persisted, so every
1253
- // /ctx-upgrade re-baked one. When Claude Code's native plugin manager
1254
- // auto-update later carries a previous version's `.mcp.json` forward
1255
- // into a fresh version dir, the stale start.mjs absolute path goes
1256
- // with it → MODULE_NOT_FOUND on every MCP boot.
1257
- //
1258
- // Architectural fix: Claude Code reads `.claude-plugin/plugin.json`
1259
- // .mcpServers as the canonical source (upstream:
1260
- // refs/platforms/claude-code/src/utils/plugins/mcpPluginIntegration.ts:131-212).
1261
- // `.mcp.json` is a redundant per-version artifact whose only role
1262
- // historically was to be a write-time poison vector. Don't write it.
1263
- // The post-bump cache-sweep below removes any pre-existing copies so
1264
- // the previous-version-carry vector cannot replay.
1265
- // Issue #711 + #414 split: normalize hooks.json (only) here.
1266
- //
1267
- // - plugin.json must NOT be normalized during /ctx-upgrade — Claude
1268
- // Code carries it forward into new versioned cache dirs on
1269
- // auto-update, so baked absolute paths go stale (#711).
1270
- // - hooks/hooks.json MUST be normalized during /ctx-upgrade on
1271
- // Windows + Git Bash — Claude Code fires SessionStart / PreToolUse
1272
- // BEFORE the MCP server boots, so the unresolved
1273
- // `${CLAUDE_PLUGIN_ROOT}` placeholder yields MODULE_NOT_FOUND for
1274
- // the first hook fire after upgrade (#414, originally wired in
1275
- // 13d1342 / #528).
1276
- //
1277
- // The narrow `normalizeHooksJsonOnly` helper preserves both invariants.
1278
- // start.mjs continues to call the full `normalizeHooksOnStartup` at the
1279
- // next MCP boot to re-heal plugin.json against the live __dirname.
1280
- try {
1281
- // #738: pass the resolved Bun ≥1.0 path so /ctx-upgrade's hooks.json
1282
- // rewrite gains the same cold-start win as the boot-time rewrite.
1283
- // Probe failures fall through to nodePath default.
1284
- let jsRuntimePath;
1285
- try {
1286
- const { resolveHookRuntime } = await import("./runtime.js");
1287
- const r = resolveHookRuntime();
1288
- if (r.isBun)
1289
- jsRuntimePath = r.path;
1290
- }
1291
- catch { /* best effort */ }
1292
- const mod =
1293
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1294
- (await import("../hooks/normalize-hooks.mjs"));
1295
- mod.normalizeHooksJsonOnly({
1296
- pluginRoot,
1297
- nodePath: process.execPath,
1298
- jsRuntimePath,
1299
- platform: process.platform,
1300
- });
1301
- }
1302
- catch { /* best effort — never block upgrade */ }
1303
- // Issue #710 — Layer 1: rewrite stale shell-snapshot PATH entries.
1304
- //
1305
- // Claude Code's per-session shell snapshot
1306
- // (~/.claude/shell-snapshots/snapshot-*.sh, baked at session boot —
1307
- // refs/platforms/claude-code/src/utils/bash/ShellSnapshot.ts:269-336)
1308
- // is `source`d before every Bash tool call. It contains an
1309
- // `export PATH='…'` line including the context-mode `bin/` for the
1310
- // version active at session start. /ctx-upgrade deletes the old
1311
- // cache dir mid-session — the snapshot still points at it, so every
1312
- // Bash call fails with "Plugin directory does not exist" until the
1313
- // session restarts. Layer 1 fixes the active session immediately;
1314
- // Layer 2 (sessionstart.mjs) heals any session that started before
1315
- // /ctx-upgrade ran.
1316
- //
1317
- // claude-code only — no other adapter uses shell-snapshots. Skip
1318
- // when running under a non-claude-code adapter (Codex/Cursor/Gemini
1319
- // etc. spawn Bash differently and have no `~/.claude/shell-snapshots`
1320
- // tree). Best-effort, idempotent, never throws.
1321
- try {
1322
- if (detection.platform === "claude-code") {
1323
- const { rewriteShellSnapshots } = await import(
1324
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1325
- "../hooks/cache-heal-utils.mjs");
1326
- const snapshotsDir = resolve(resolveClaudeConfigDir(), "shell-snapshots");
1327
- const result = rewriteShellSnapshots({
1328
- snapshotsDir,
1329
- currentVersion: newVersion,
1330
- });
1331
- if (result.rewritten.length > 0) {
1332
- p.log.info(color.dim(` Healed ${result.rewritten.length} stale shell snapshot(s) — Bash tool calls in the active session will pick up v${newVersion} immediately`));
1333
- }
1334
- }
1335
- }
1336
- catch { /* best effort — never block upgrade */ }
1337
- s.stop(color.green(`Updated in-place to v${newVersion}`));
1338
- // v1.0.114 hotfix — pre-flight: verify the in-place copy actually
1339
- // wrote a plugin.json carrying newVersion BEFORE we tell the
1340
- // registry that's the install path. If the manifest still reports
1341
- // the old version (rsync race, partial write, files-array drift),
1342
- // updating the registry would create the silent v1.0.113-class
1343
- // drift Mert hit. Bail out — the next /ctx-upgrade gets to retry.
1344
- const pluginManifest = resolve(pluginRoot, ".claude-plugin", "plugin.json");
1345
- let onDiskVersion = null;
1346
- try {
1347
- const pj = JSON.parse(readFileSync(pluginManifest, "utf-8"));
1348
- if (pj && typeof pj.version === "string")
1349
- onDiskVersion = pj.version;
1350
- }
1351
- catch { /* parse error → onDiskVersion stays null */ }
1352
- if (onDiskVersion !== newVersion) {
1353
- throw new Error(`pluginRoot manifest version mismatch — disk says "${onDiskVersion ?? "<missing>"}" but newVersion is "${newVersion}". Refusing to bump registry.`);
1354
- }
1355
- // Fix registry — adapter-aware
1356
- adapter.updatePluginRegistry(pluginRoot, newVersion);
1357
- p.log.info(color.dim(" Registry synced to " + pluginRoot));
1358
- // v1.0.114 hotfix — post-write assertion: re-read installed_plugins.json
1359
- // and verify installPath/.claude-plugin/plugin.json's version matches
1360
- // the registry entry. Throws on mismatch — fails loudly so a future
1361
- // adapter regression surfaces here, not weeks later in user reports.
1362
- try {
1363
- const ipPath = resolve(resolveClaudeConfigDir(), "plugins", "installed_plugins.json");
1364
- if (existsSync(ipPath)) {
1365
- const ip = JSON.parse(readFileSync(ipPath, "utf-8"));
1366
- const entries = ip?.plugins?.["context-mode@context-mode"];
1367
- if (Array.isArray(entries)) {
1368
- for (const entry of entries) {
1369
- const ip2 = entry?.installPath;
1370
- if (typeof ip2 !== "string" || !ip2)
1371
- continue;
1372
- if (!existsSync(ip2)) {
1373
- throw new Error(`installPath does not exist on disk: ${ip2}`);
1374
- }
1375
- const pjPath = resolve(ip2, ".claude-plugin", "plugin.json");
1376
- if (!existsSync(pjPath)) {
1377
- throw new Error(`missing plugin.json manifest at ${pjPath}`);
1378
- }
1379
- const pj = JSON.parse(readFileSync(pjPath, "utf-8"));
1380
- if (pj?.version !== entry.version) {
1381
- throw new Error(`version mismatch — registry says "${entry.version}" but ${pjPath} says "${pj?.version}"`);
1382
- }
1383
- }
1384
- }
1385
- }
1386
- }
1387
- catch (err) {
1388
- const message = err instanceof Error ? err.message : String(err);
1389
- throw new Error(`Registry consistency check failed: ${message}`);
1390
- }
1391
- // v1.0.119 — Issue #523 — Layer 5 heal: assert .claude-plugin/plugin.json's
1392
- // mcpServers["context-mode"].args[0] is the literal ${CLAUDE_PLUGIN_ROOT}/start.mjs
1393
- // placeholder, not a tmpdir-prefixed absolute path. cli.ts already wrote .mcp.json
1394
- // with the placeholder (#411 fix), but plugin.json was never touched here — and
1395
- // start.mjs's normalize-hooks (Windows + #378) can bake in absolute paths that
1396
- // become stale across upgrades. We call the shared heal twice: first call cleans
1397
- // any drift; second call MUST return healed:[] or we throw. Single source of
1398
- // truth shared with start.mjs HEAL block + postinstall.
1399
- try {
1400
- const pluginCacheRoot = resolve(resolveClaudeConfigDir(), "plugins", "cache");
1401
- const pluginKey = "context-mode@context-mode";
1402
- const firstPass = healPluginJsonMcpServers({ pluginRoot, pluginCacheRoot, pluginKey });
1403
- if (firstPass && firstPass.error) {
1404
- throw new Error(firstPass.error);
1405
- }
1406
- const secondPass = healPluginJsonMcpServers({ pluginRoot, pluginCacheRoot, pluginKey });
1407
- if (secondPass && Array.isArray(secondPass.healed) && secondPass.healed.length > 0) {
1408
- throw new Error(`Plugin manifest drift: plugin.json mcpServers.args still poisoned after first heal pass (healed=${secondPass.healed.join(",")})`);
1409
- }
1410
- }
1411
- catch (err) {
1412
- const message = err instanceof Error ? err.message : String(err);
1413
- throw new Error(`plugin.json drift check failed: ${message}`);
1414
- }
1415
- // Issue #609 — Layer 6 replacement: sweep stale `.mcp.json` files from
1416
- // every per-version cache dir. Supersedes the previous healMcpJsonArgs
1417
- // drift-check block (v1.0.122) — that block existed because cli.ts
1418
- // itself wrote `.mcp.json`. With the write gone (above), the only
1419
- // remaining `.mcp.json` files are stale carry-forwards from earlier
1420
- // versions. Sweep them so Claude Code's auto-update can't replay them
1421
- // into a fresh version dir.
1422
- //
1423
- // Belt-and-braces: a second sweep call MUST report removed:[] or we
1424
- // throw — same architectural-lock pattern as the plugin.json drift
1425
- // check above. Single source of truth shared with start.mjs HEAL
1426
- // block + postinstall.
1427
- try {
1428
- const pluginCacheRoot = resolve(resolveClaudeConfigDir(), "plugins", "cache");
1429
- const pluginKey = "context-mode@context-mode";
1430
- const firstSweep = sweepStaleMcpJson({ pluginCacheRoot, pluginKey });
1431
- if (firstSweep && firstSweep.removed && firstSweep.removed.length > 0) {
1432
- p.log.info(color.dim(` Swept ${firstSweep.removed.length} stale .mcp.json file(s) from cache`));
1433
- }
1434
- const secondSweep = sweepStaleMcpJson({ pluginCacheRoot, pluginKey });
1435
- if (secondSweep && Array.isArray(secondSweep.removed) && secondSweep.removed.length > 0) {
1436
- throw new Error(`.mcp.json sweep drift: ${secondSweep.removed.length} file(s) still present after first pass`);
1437
- }
1438
- }
1439
- catch (err) {
1440
- const message = err instanceof Error ? err.message : String(err);
1441
- throw new Error(`.mcp.json sweep check failed: ${message}`);
1442
- }
1443
- // v1.0.X — Layer 7 heal: update user-level ~/.claude.json MCP server
1444
- // registrations that point to old context-mode version dirs.
1445
- // (anthropics/claude-code#59310 workaround — see heal-installed-plugins.mjs)
1446
- try {
1447
- // @ts-expect-error — JS module, no TS declarations
1448
- const { healClaudeJsonMcpArgs } = await import("../scripts/heal-installed-plugins.mjs");
1449
- const dotClaudeJson = resolve(homedir(), ".claude.json");
1450
- const pluginCacheParent = resolve(resolveClaudeConfigDir(), "plugins", "cache", "context-mode", "context-mode");
1451
- const result = healClaudeJsonMcpArgs({ dotClaudeJsonPath: dotClaudeJson, pluginCacheParent, newPluginRoot: pluginRoot });
1452
- if (result.healed && result.healed.length > 0) {
1453
- p.log.info(color.dim(" ~/.claude.json user MCP registrations updated → " + newVersion));
1454
- }
1455
- }
1456
- catch {
1457
- /* best effort — never block upgrade */
1458
- }
1459
- // v1.0.114 hotfix — marketplace post-pull assertion: clone (if
1460
- // present) MUST be on newVersion. Mert's case showed marketplace
1461
- // stuck at v1.0.89 — the sync block above swallowed that silently.
1462
- // Warn (don't throw) — npm-only users have no marketplace clone.
1463
- try {
1464
- const marketplaceManifest = resolve(marketplaceDir, ".claude-plugin", "plugin.json");
1465
- if (existsSync(marketplaceManifest)) {
1466
- const mpj = JSON.parse(readFileSync(marketplaceManifest, "utf-8"));
1467
- if (mpj?.version !== newVersion) {
1468
- p.log.warn(color.yellow("Marketplace clone version mismatch") +
1469
- ` — ${marketplaceDir} reports "${mpj?.version}" but expected "${newVersion}"`);
1470
- p.log.info(color.dim(` Run manually: git -C "${marketplaceDir}" fetch --tags origin && git -C "${marketplaceDir}" reset --hard origin/HEAD`));
1471
- }
1472
- }
1473
- }
1474
- catch { /* best effort */ }
1475
- // Install production deps
1476
- s.start("Installing production dependencies");
1477
- npmExecFile(["install", "--production", "--no-audit", "--no-fund"], {
1478
- cwd: pluginRoot,
1479
- stdio: "pipe",
1480
- timeout: 60000,
1481
- });
1482
- s.stop("Dependencies ready");
1483
- if (!isInProcessPluginPlatform(detection.platform)) {
1484
- // Verify native addons through the same bootstrap start.mjs imports.
1485
- // On modern Node, the ABI-specific cache file is the compatibility marker;
1486
- // the active binding alone may be stale from a previous Node ABI.
1487
- s.start("Verifying native addon ABI");
1488
- const bsqAbiCachePath = resolve(pluginRoot, "node_modules", "better-sqlite3", "build", "Release", `better_sqlite3.abi${process.versions.modules}.node`);
1489
- try {
1490
- const ensureDepsPath = resolve(pluginRoot, "hooks", "ensure-deps.mjs");
1491
- if (!existsSync(ensureDepsPath)) {
1492
- throw new Error(`missing ${ensureDepsPath}`);
1493
- }
1494
- await import(`${pathToFileURL(ensureDepsPath).href}?upgrade=${Date.now()}`);
1495
- if (existsSync(bsqAbiCachePath)) {
1496
- s.stop(color.green("Native addons OK") + color.dim(" — ABI cache present"));
1497
- changes.push(`better-sqlite3 ABI ${process.versions.modules} cache ready`);
1498
- }
1499
- else {
1500
- s.stop(color.yellow("Native addon ABI cache missing"));
1501
- p.log.warn(color.dim(` Try manually: cd "${pluginRoot}" && npm rebuild better-sqlite3`));
1502
- }
1503
- }
1504
- catch (err) {
1505
- const message = err instanceof Error ? err.message : String(err);
1506
- s.stop(color.yellow("Native addon ABI bootstrap unavailable"));
1507
- p.log.warn(color.yellow("better-sqlite3 ABI repair did not run") +
1508
- ` — ${message}` +
1509
- color.dim(`\n Try manually: cd "${pluginRoot}" && npm rebuild better-sqlite3`));
1510
- }
1511
- // ── Post-install binding verifier (#514) ────────────────────
1512
- // npm@7+ silently drops optionalDependencies whose engines
1513
- // field excludes the running Node (e.g. Node 26 vs
1514
- // better-sqlite3@12.x). On a silent skip the package directory
1515
- // is missing entirely and ensure-deps cannot recover. Fail
1516
- // loud so /ctx-upgrade no longer reports success while the
1517
- // knowledge base is unusable.
1518
- const bsqBindingPath = resolve(pluginRoot, "node_modules", "better-sqlite3", "build", "Release", "better_sqlite3.node");
1519
- if (!existsSync(bsqBindingPath)) {
1520
- // Try one last self-heal — explicit, named install bypasses
1521
- // the optionalDependency silent-skip path even if the dep
1522
- // somehow regressed back to optional.
1523
- try {
1524
- const healPath = resolve(pluginRoot, "scripts", "heal-better-sqlite3.mjs");
1525
- if (existsSync(healPath)) {
1526
- const mod = await import(`${pathToFileURL(healPath).href}?upgrade=${Date.now()}`);
1527
- if (typeof mod.healBetterSqlite3Binding === "function") {
1528
- mod.healBetterSqlite3Binding(pluginRoot);
1529
- }
1530
- }
1531
- }
1532
- catch { /* best effort — verifier below will fail loud */ }
1533
- }
1534
- if (!existsSync(bsqBindingPath)) {
1535
- // Mark the upgrade process for a non-zero exit at completion.
1536
- // Stays in scope only for the rest of upgrade(); the actual
1537
- // exit-code wiring sits below the top-level changes report.
1538
- process.exitCode = 1;
1539
- p.log.error(color.red("better-sqlite3 native binding: MISSING") +
1540
- color.dim(`\n Path: ${bsqBindingPath}`) +
1541
- color.dim("\n Cause: npm silently skipped the package (Node engine mismatch, issue #514)") +
1542
- color.dim(`\n Try (primary): cd "${pluginRoot}" && npm install better-sqlite3 --no-optional`) +
1543
- color.dim("\n Try (fallback): /context-mode:ctx-doctor"));
1544
- }
1545
- // Update global npm
1546
- s.start("Updating npm global package");
1547
- try {
1548
- npmExecFile(["install", "-g", pluginRoot, "--no-audit", "--no-fund"], {
1549
- stdio: "pipe",
1550
- timeout: 30000,
1551
- });
1552
- s.stop(color.green("npm global updated"));
1553
- changes.push("Updated npm global package");
1554
- }
1555
- catch {
1556
- s.stop(color.yellow("npm global update skipped"));
1557
- p.log.info(color.dim(" Could not update global npm — may need sudo or standalone install"));
1558
- }
1559
- }
1560
- // Cleanup
1561
- rmSync(tmpDir, { recursive: true, force: true });
1562
- // Sync skills to the active install path from installed_plugins.json (#228).
1563
- // Only targets the ACTUAL directory Claude Code reads from — not spraying everywhere.
1564
- // Issue #460 round-3: honor $CLAUDE_CONFIG_DIR so the registry lookup
1565
- // tracks relocated CC config trees.
1566
- try {
1567
- const claudeRoot = resolveClaudeConfigDir();
1568
- const registryPath = resolve(claudeRoot, "plugins", "installed_plugins.json");
1569
- if (existsSync(registryPath)) {
1570
- // The registry's installPath fields are written by Claude Code under
1571
- // <claudeRoot>/plugins/cache/<marketplace>/<plugin>/<version>. Any other
1572
- // shape means the registry has been tampered with by a co-resident
1573
- // plugin, a malicious postinstall script, or another local actor.
1574
- // Without containment, cpSync would happily recursive-write the in-repo
1575
- // skills/ tree to /etc/skills, ~/.ssh/skills, or wherever the attacker
1576
- // pointed. server.ts:790 (healCacheMidSession) already gates the same
1577
- // field this way; the symmetric guard belongs here too.
1578
- //
1579
- // The lexical resolve+startsWith check rejects ".."-escapes and
1580
- // absolute paths outside cacheRoot, but path.resolve doesn't
1581
- // dereference symlinks. A same-uid actor who can plant a symlink
1582
- // AT <cacheRoot>/<owner>/<plugin>/<version> targeting an attacker
1583
- // dir gets past the lexical guard, then cpSync follows the link at
1584
- // FS-write time. Re-check via realpathSync so a planted symlink
1585
- // anchor fails the gate.
1586
- const cacheRoot = resolve(claudeRoot, "plugins", "cache");
1587
- let cacheRootCanon;
1588
- try {
1589
- cacheRootCanon = realpathSync(cacheRoot);
1590
- }
1591
- catch {
1592
- cacheRootCanon = cacheRoot;
1593
- }
1594
- const cacheRootWithSep = cacheRootCanon + sep;
1595
- const registry = JSON.parse(readFileSync(registryPath, "utf-8"));
1596
- const entries = registry?.plugins?.["context-mode@context-mode"];
1597
- if (Array.isArray(entries)) {
1598
- for (const entry of entries) {
1599
- const installPath = entry?.installPath;
1600
- if (typeof installPath !== "string" || !installPath)
1601
- continue;
1602
- if (installPath === pluginRoot)
1603
- continue;
1604
- const resolvedInstallPath = resolve(installPath);
1605
- if (!(resolvedInstallPath + sep).startsWith(cacheRootWithSep))
1606
- continue;
1607
- if (!existsSync(resolvedInstallPath))
1608
- continue;
1609
- let realInstallPath;
1610
- try {
1611
- realInstallPath = realpathSync(resolvedInstallPath);
1612
- }
1613
- catch {
1614
- continue;
1615
- }
1616
- if (!(realInstallPath + sep).startsWith(cacheRootWithSep))
1617
- continue;
1618
- const srcSkills = resolve(srcDir, "skills");
1619
- if (existsSync(srcSkills)) {
1620
- cpSync(srcSkills, resolve(realInstallPath, "skills"), { recursive: true });
1621
- changes.push(`Synced skills to active install path`);
1622
- }
1623
- }
1624
- }
1625
- }
1626
- }
1627
- catch { /* best effort — registry may not exist or be malformed */ }
1628
- changes.push(`Updated v${localVersion} → v${newVersion}`);
1629
- p.log.success(color.green("Plugin reinstalled from GitHub!") +
1630
- color.dim(` — v${newVersion}`));
1631
- }
1632
- }
1633
- catch (err) {
1634
- const message = err instanceof Error ? err.message : String(err);
1635
- s.stop(color.red("Update failed"));
1636
- p.log.error(color.red("GitHub pull failed") + ` — ${message}`);
1637
- // Issue #628 — Windows `spawnSync cmd.exe ETIMEDOUT` (and any
1638
- // other Step 1/2 throw — network, npm, manifest mismatch) used
1639
- // to fall through to Steps 3-7 (backup, hooks, perms, doctor),
1640
- // all of which succeed against the OLD on-disk install. The
1641
- // process then exited 0 and the upgrade-checklist renderer
1642
- // marked `[x] Built and installed vNEW` while in-place files,
1643
- // installed_plugins.json registry, and per-version cache dirs
1644
- // stayed at vOLD. Worse: the marketplace clone synced earlier
1645
- // in this same run is now AHEAD of cache+registry — Claude
1646
- // Code's plugin manager keeps offering the same upgrade
1647
- // forever (drift trap; reporter had to hand-edit
1648
- // installed_plugins.json to escape).
1649
- //
1650
- // Algo defense: mark the process for non-zero exit and surface
1651
- // an actionable recovery hint. Steps 3-7 still run because the
1652
- // user's hooks may be broken regardless — but the overall
1653
- // upgrade no longer reports success.
1654
- process.exitCode = 1;
1655
- p.log.warn(color.yellow("In-place files were NOT updated") +
1656
- color.dim(" — old version is still on disk; hooks/settings will still be refreshed."));
1657
- p.log.info(color.dim(" Recovery: re-run /ctx-upgrade once network is stable, or run /context-mode:ctx-doctor for a full health check."));
1658
- try {
1659
- rmSync(tmpDir, { recursive: true, force: true });
1660
- }
1661
- catch { /* ignore */ }
1662
- }
1663
- // Step 3: Backup settings — adapter-aware
1664
- p.log.step(`Backing up ${adapter.name} settings...`);
1665
- const backupPath = adapter.backupSettings();
1666
- if (backupPath?.endsWith(".bak")) {
1667
- p.log.success(color.green("Backup created") + color.dim(" -> " + backupPath));
1668
- changes.push("Backed up settings");
1669
- }
1670
- else if (backupPath) {
1671
- p.log.success(color.green("Backup skipped") + color.dim(" — no changes needed"));
1672
- }
1673
- else {
1674
- p.log.warn(color.yellow("No existing settings to backup") +
1675
- " — a new one will be created");
1676
- }
1677
- // Step 4: Configure hooks — adapter-aware
1678
- p.log.step(`Configuring ${adapter.name} hooks...`);
1679
- try {
1680
- const hookChanges = adapter.configureAllHooks(pluginRoot);
1681
- for (const change of hookChanges) {
1682
- p.log.info(color.dim(` ${change}`));
1683
- changes.push(change);
1684
- }
1685
- p.log.success(color.green("Hooks configured") + color.dim(` — ${adapter.name}`));
1686
- }
1687
- catch (err) {
1688
- const message = err instanceof Error ? err.message : String(err);
1689
- throw new Error(`Hook configuration failed: ${message}`);
1690
- }
1691
- // Step 5: Set hook script permissions — adapter-aware
1692
- p.log.step("Setting hook script permissions...");
1693
- const permSet = adapter.setHookPermissions(pluginRoot);
1694
- // Also ensure CLI binaries are executable (tsc doesn't set +x)
1695
- // chmod is POSIX-only — skip on Windows where execute bits are irrelevant
1696
- if (process.platform !== "win32") {
1697
- for (const bin of ["build/cli.js", "cli.bundle.mjs"]) {
1698
- const binPath = resolve(pluginRoot, bin);
1699
- try {
1700
- accessSync(binPath, constants.F_OK);
1701
- chmodSync(binPath, 0o755);
1702
- permSet.push(binPath);
1703
- }
1704
- catch { /* not found — skip */ }
1705
- }
1706
- }
1707
- if (permSet.length > 0) {
1708
- p.log.success(color.green("Permissions set") + color.dim(` — ${permSet.length} hook script(s)`));
1709
- changes.push(`Set ${permSet.length} hook scripts as executable`);
1710
- }
1711
- else {
1712
- p.log.error(color.red("No hook scripts found") +
1713
- color.dim(" — expected in " + resolve(pluginRoot, "hooks")));
1714
- }
1715
- // Step 6: Report
1716
- if (changes.length > 0) {
1717
- p.note(changes.map((c) => color.green(" + ") + c).join("\n"), "Changes Applied");
1718
- }
1719
- else {
1720
- p.log.info(color.dim("No changes were needed."));
1721
- }
1722
- // Restart notice — new MCP tools require MCP server restart
1723
- const restartHint = adapter.name === "Claude Code"
1724
- ? "/reload-plugins, new terminal, or restart session"
1725
- : "new terminal or restart session";
1726
- p.log.warn(color.yellow("Restart for new MCP tools to take effect.") +
1727
- color.dim(` (${restartHint})`));
1728
- // Step 7: Run doctor
1729
- p.log.step("Running doctor to verify...");
1730
- console.log();
1731
- try {
1732
- const cliBundlePath = resolve(pluginRoot, "cli.bundle.mjs");
1733
- const cliBuildPath = resolve(pluginRoot, "build", "cli.js");
1734
- const cliPath = existsSync(cliBundlePath) ? cliBundlePath : cliBuildPath;
1735
- execFileSync("node", [cliPath, "doctor"], {
1736
- stdio: "inherit",
1737
- timeout: 30000,
1738
- cwd: pluginRoot,
1739
- env: { ...process.env, CONTEXT_MODE_PLATFORM: detection.platform },
1740
- });
1741
- }
1742
- catch {
1743
- p.log.warn(color.yellow("Doctor had warnings") +
1744
- color.dim(` — restart your ${adapter.name} session to pick up the new version`));
1745
- }
1746
- }
1747
- /* -------------------------------------------------------
1748
- * statusline — forward to bin/statusline.mjs
1749
- * ------------------------------------------------------- */
1750
- function statuslineForward() {
1751
- // Try multiple plugin-root candidates in priority order. After ctx-upgrade,
1752
- // getPluginRoot() can resolve to a cache dir that sessionstart.mjs (#181)
1753
- // already cleaned, leaving bin/statusline.mjs missing. Falling back to the
1754
- // marketplace clone (#418-synced, stable across upgrades) and to the path
1755
- // Claude Code itself loads from (installed_plugins.json) keeps the bar
1756
- // alive instead of silently going blank.
1757
- // Issue #460 round-3: marketplace + registry paths must follow
1758
- // $CLAUDE_CONFIG_DIR so relocated CC trees still find the statusline binary.
1759
- const claudeRoot = resolveClaudeConfigDir();
1760
- const candidates = [
1761
- resolve(getPluginRoot(), "bin", "statusline.mjs"),
1762
- resolve(claudeRoot, "plugins", "marketplaces", "context-mode", "bin", "statusline.mjs"),
1763
- ];
1764
- // installed_plugins.json may list one or more install paths CC actually
1765
- // loads from. Prefer those if they exist.
1766
- try {
1767
- const registryPath = resolve(claudeRoot, "plugins", "installed_plugins.json");
1768
- if (existsSync(registryPath)) {
1769
- // Same trust boundary as the cpSync site in upgrade() and as
1770
- // server.ts:790's healCacheMidSession: only honor installPath values
1771
- // that resolve under <claudeRoot>/plugins/cache. A stray /etc or
1772
- // ~/.ssh entry written by another local actor must not become the
1773
- // script the statusline forwarder imports, since statusline re-fires
1774
- // several times per second and would hand the attacker durable RCE
1775
- // on the user's behalf.
1776
- //
1777
- // path.resolve is purely lexical, so a same-uid actor who can plant
1778
- // a symlink at <cacheRoot>/<owner>/<plugin>/<version> targeting an
1779
- // attacker dir would pass the lexical gate. Re-check via
1780
- // realpathSync so the dynamic-import target's actual on-disk
1781
- // location also stays under cacheRoot.
1782
- const cacheRoot = resolve(claudeRoot, "plugins", "cache");
1783
- let cacheRootCanon;
1784
- try {
1785
- cacheRootCanon = realpathSync(cacheRoot);
1786
- }
1787
- catch {
1788
- cacheRootCanon = cacheRoot;
1789
- }
1790
- const cacheRootWithSep = cacheRootCanon + sep;
1791
- const registry = JSON.parse(readFileSync(registryPath, "utf-8"));
1792
- const entries = registry?.plugins?.["context-mode@context-mode"];
1793
- if (Array.isArray(entries)) {
1794
- for (const entry of entries) {
1795
- const installPath = entry?.installPath;
1796
- if (typeof installPath !== "string" || !installPath)
1797
- continue;
1798
- const resolvedInstallPath = resolve(installPath);
1799
- if (!(resolvedInstallPath + sep).startsWith(cacheRootWithSep))
1800
- continue;
1801
- let realInstallPath;
1802
- try {
1803
- realInstallPath = realpathSync(resolvedInstallPath);
1804
- }
1805
- catch {
1806
- continue;
1807
- }
1808
- if (!(realInstallPath + sep).startsWith(cacheRootWithSep))
1809
- continue;
1810
- candidates.push(resolve(realInstallPath, "bin", "statusline.mjs"));
1811
- }
1812
- }
1813
- }
1814
- }
1815
- catch { /* registry malformed — fall through to other candidates */ }
1816
- const scriptPath = candidates.find((c) => existsSync(c));
1817
- if (!scriptPath) {
1818
- // Statusline output is the user-facing status bar; stderr surfaces visibly
1819
- // in some terminals. Exit silently — the bar simply stays empty until the
1820
- // next /ctx-upgrade or restart resolves the path.
1821
- process.exit(0);
1822
- }
1823
- // Re-exec via dynamic import so stdin/stdout are inherited cleanly.
1824
- import(pathToFileURL(scriptPath).href).catch(() => {
1825
- process.exit(0);
1826
- });
1827
- }