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