@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,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex capability detection for the PreToolUse formatter (#845).
|
|
3
|
+
*
|
|
4
|
+
* Recent Codex builds honor PreToolUse `permissionDecision:"allow" + updatedInput`
|
|
5
|
+
* (command rewrite) and `additionalContext`. Older builds reject/ignore those
|
|
6
|
+
* fields. context-mode must emit the rewrite shape ONLY when the running Codex
|
|
7
|
+
* supports it and otherwise fail closed (deny) — it must never silently pass a
|
|
8
|
+
* redirect through.
|
|
9
|
+
*
|
|
10
|
+
* Detection parses `codex --version` and compares it against the first version
|
|
11
|
+
* verified to honor the contract. The result is cached to a temp file with a
|
|
12
|
+
* short TTL so the hot PreToolUse path does not spawn a process on every tool
|
|
13
|
+
* call. Any failure (no codex on PATH, parse error) → false → fail closed.
|
|
14
|
+
*
|
|
15
|
+
* There is intentionally NO opt-in env flag — those rot into dead code because
|
|
16
|
+
* nobody exercises the off-by-default path. The correct behavior is detected at
|
|
17
|
+
* runtime so it is always the default.
|
|
18
|
+
*/
|
|
19
|
+
import { execFileSync } from "node:child_process";
|
|
20
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { tmpdir } from "node:os";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* First Codex release verified to honor PreToolUse allow+updatedInput and
|
|
26
|
+
* additionalContext: codex-cli 0.141.0 (#845, validated against the shipped
|
|
27
|
+
* binary's output_parser). Below this we fail closed.
|
|
28
|
+
*/
|
|
29
|
+
export const MIN_REWRITE_VERSION = [0, 141, 0];
|
|
30
|
+
|
|
31
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // re-probe at most hourly
|
|
32
|
+
const CACHE_FILE = "context-mode-codex-caps.json";
|
|
33
|
+
|
|
34
|
+
/** Parse a `codex --version` line ("codex-cli 0.141.0") → [major, minor, patch]. */
|
|
35
|
+
export function parseCodexVersion(raw) {
|
|
36
|
+
const s = String(raw ?? "");
|
|
37
|
+
const isDigit = (c) => c >= "0" && c <= "9";
|
|
38
|
+
for (let i = 0; i < s.length; i++) {
|
|
39
|
+
let j = i;
|
|
40
|
+
const parts = [];
|
|
41
|
+
while (parts.length < 3) {
|
|
42
|
+
const start = j;
|
|
43
|
+
while (j < s.length && isDigit(s[j])) j++;
|
|
44
|
+
if (j === start) break; // no digits where a number was expected
|
|
45
|
+
parts.push(Number(s.slice(start, j)));
|
|
46
|
+
if (parts.length < 3) {
|
|
47
|
+
if (s[j] !== ".") break; // separator must be a dot
|
|
48
|
+
j++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (parts.length === 3) return parts;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Semantic ">=" over [major, minor, patch] tuples. */
|
|
57
|
+
export function versionGte(a, b) {
|
|
58
|
+
for (let i = 0; i < 3; i++) {
|
|
59
|
+
const x = a[i] ?? 0;
|
|
60
|
+
const y = b[i] ?? 0;
|
|
61
|
+
if (x > y) return true;
|
|
62
|
+
if (x < y) return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function defaultRunVersion() {
|
|
68
|
+
const opts = { encoding: "utf8", timeout: 2000, stdio: ["ignore", "pipe", "ignore"] };
|
|
69
|
+
// Mirror the adapter's cross-platform probe (src/adapters/codex/index.ts):
|
|
70
|
+
// on Windows `codex` resolves to a .cmd shim that execFile cannot launch
|
|
71
|
+
// directly, so route through cmd.exe.
|
|
72
|
+
return process.platform === "win32"
|
|
73
|
+
? execFileSync("cmd.exe", ["/d", "/s", "/c", "codex --version"], opts)
|
|
74
|
+
: execFileSync("codex", ["--version"], opts);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Whether the running Codex honors PreToolUse allow+updatedInput /
|
|
79
|
+
* additionalContext. Fails closed (false) on any error. Cached to a temp file
|
|
80
|
+
* with a TTL so the hot path avoids per-call process spawns.
|
|
81
|
+
*
|
|
82
|
+
* @param {object} [io] test seams
|
|
83
|
+
* @param {() => string} [io.runVersion] returns `codex --version` stdout
|
|
84
|
+
* @param {() => number} [io.now] clock in ms
|
|
85
|
+
* @param {string} [io.cachePath] cache file path
|
|
86
|
+
* @returns {boolean}
|
|
87
|
+
*/
|
|
88
|
+
export function codexSupportsUpdatedInput(io = {}) {
|
|
89
|
+
const now = io.now ?? Date.now;
|
|
90
|
+
const cachePath = io.cachePath ?? join(tmpdir(), CACHE_FILE);
|
|
91
|
+
const runVersion = io.runVersion ?? defaultRunVersion;
|
|
92
|
+
|
|
93
|
+
// Fast path: a non-expired cache entry.
|
|
94
|
+
try {
|
|
95
|
+
const cached = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
96
|
+
if (cached && typeof cached.at === "number" && now() - cached.at < CACHE_TTL_MS) {
|
|
97
|
+
return cached.supported === true;
|
|
98
|
+
}
|
|
99
|
+
} catch { /* cache miss / corrupt — re-detect below */ }
|
|
100
|
+
|
|
101
|
+
let supported = false;
|
|
102
|
+
try {
|
|
103
|
+
const version = parseCodexVersion(runVersion());
|
|
104
|
+
supported = version ? versionGte(version, MIN_REWRITE_VERSION) : false;
|
|
105
|
+
} catch {
|
|
106
|
+
supported = false; // no codex on PATH / probe failed → fail closed
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try { writeFileSync(cachePath, JSON.stringify({ at: now(), supported })); } catch { /* best effort */ }
|
|
110
|
+
|
|
111
|
+
return supported;
|
|
112
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-specific response formatters.
|
|
3
|
+
* Takes normalized decision from routing.mjs -> platform-specific JSON output.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const formatters = {
|
|
7
|
+
"claude-code": {
|
|
8
|
+
deny: (reason) => ({
|
|
9
|
+
hookSpecificOutput: {
|
|
10
|
+
hookEventName: "PreToolUse",
|
|
11
|
+
permissionDecision: "deny",
|
|
12
|
+
permissionDecisionReason: reason,
|
|
13
|
+
},
|
|
14
|
+
}),
|
|
15
|
+
ask: () => ({
|
|
16
|
+
hookSpecificOutput: {
|
|
17
|
+
hookEventName: "PreToolUse",
|
|
18
|
+
permissionDecision: "ask",
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
// Tool-aware modify handling for claude-code:
|
|
22
|
+
//
|
|
23
|
+
// - Bash redirect (updatedInput.command): CC v2.1.x ignores
|
|
24
|
+
// `updatedInput.command` substitution under `permissionDecision: "allow"`
|
|
25
|
+
// — original command runs unchanged. Verified via /diagnose Phase 4
|
|
26
|
+
// forced-deny probe: only `permissionDecision: "deny"` is honored for
|
|
27
|
+
// Bash blocking. Emit deny + extract echo payload into
|
|
28
|
+
// `permissionDecisionReason`.
|
|
29
|
+
//
|
|
30
|
+
// - Agent prompt injection (updatedInput.prompt): CC honors
|
|
31
|
+
// allow+updatedInput for Agent tool — modified prompt reaches the
|
|
32
|
+
// subagent. Keep modify shape so subagent routing-block injection works.
|
|
33
|
+
//
|
|
34
|
+
// - Any other shape: pass through as modify and let CC decide.
|
|
35
|
+
//
|
|
36
|
+
// Other adapters (gemini-cli, vscode-copilot, etc.) keep their own modify
|
|
37
|
+
// semantics — their hosts implement updatedInput differently or not at all.
|
|
38
|
+
modify: (updatedInput) => {
|
|
39
|
+
const ui = updatedInput ?? {};
|
|
40
|
+
const isBashCommandRedirect = "command" in ui;
|
|
41
|
+
if (!isBashCommandRedirect) {
|
|
42
|
+
return {
|
|
43
|
+
hookSpecificOutput: {
|
|
44
|
+
hookEventName: "PreToolUse",
|
|
45
|
+
updatedInput: ui,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// routing.mjs wraps the redirect guidance in `echo "..."` form.
|
|
50
|
+
// Extract the quoted payload as the deny reason. Fall back to a generic
|
|
51
|
+
// ADR-0003 CASE A message if the shape doesn't match.
|
|
52
|
+
const cmd = ui.command ?? "";
|
|
53
|
+
const m = cmd.match(/^echo\s+"(.+)"$/s);
|
|
54
|
+
const reason = m
|
|
55
|
+
? m[1]
|
|
56
|
+
: "Redirected to ctx_execute / ctx_fetch_and_index. Call ctx_execute(language, code) to fetch and derive your answer in one round trip, or call ctx_fetch_and_index(url, source) when you want to query the response later via ctx_search. Both have full network access. Retry the same call on a transient DNS error (EAI_AGAIN, ETIMEDOUT, ENETUNREACH).";
|
|
57
|
+
return {
|
|
58
|
+
hookSpecificOutput: {
|
|
59
|
+
hookEventName: "PreToolUse",
|
|
60
|
+
permissionDecision: "deny",
|
|
61
|
+
permissionDecisionReason: reason,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
context: (additionalContext) => ({
|
|
66
|
+
hookSpecificOutput: {
|
|
67
|
+
hookEventName: "PreToolUse",
|
|
68
|
+
additionalContext,
|
|
69
|
+
},
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
"gemini-cli": {
|
|
74
|
+
deny: (reason) => ({ decision: "deny", reason }),
|
|
75
|
+
ask: () => null, // Gemini CLI has no "ask" concept
|
|
76
|
+
modify: (updatedInput) => ({
|
|
77
|
+
hookSpecificOutput: { tool_input: updatedInput },
|
|
78
|
+
}),
|
|
79
|
+
context: (additionalContext) => ({
|
|
80
|
+
hookSpecificOutput: { additionalContext },
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
"vscode-copilot": {
|
|
85
|
+
deny: (reason) => ({
|
|
86
|
+
permissionDecision: "deny",
|
|
87
|
+
permissionDecisionReason: reason,
|
|
88
|
+
}),
|
|
89
|
+
ask: () => ({
|
|
90
|
+
permissionDecision: "ask",
|
|
91
|
+
}),
|
|
92
|
+
modify: (updatedInput) => ({
|
|
93
|
+
hookSpecificOutput: {
|
|
94
|
+
hookEventName: "PreToolUse",
|
|
95
|
+
permissionDecision: "allow",
|
|
96
|
+
permissionDecisionReason: "Routed to context-mode sandbox",
|
|
97
|
+
updatedInput,
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
100
|
+
context: (additionalContext) => ({
|
|
101
|
+
hookSpecificOutput: {
|
|
102
|
+
hookEventName: "PreToolUse",
|
|
103
|
+
additionalContext,
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// GitHub Copilot CLI uses top-level decision fields (NOT the VS Code
|
|
109
|
+
// hookSpecificOutput wrapper) — matches CopilotCliAdapter.format*Response.
|
|
110
|
+
"copilot-cli": {
|
|
111
|
+
deny: (reason) => ({
|
|
112
|
+
permissionDecision: "deny",
|
|
113
|
+
permissionDecisionReason: reason,
|
|
114
|
+
}),
|
|
115
|
+
// Carry the reason on `ask` too, so the user sees WHY confirmation is
|
|
116
|
+
// requested (Copilot CLI honors permissionDecisionReason; matches the
|
|
117
|
+
// adapter's formatPreToolUseResponse ask branch). Fall back when the
|
|
118
|
+
// routing decision carries no reason, so the prompt is never bare.
|
|
119
|
+
ask: (reason) => ({
|
|
120
|
+
permissionDecision: "ask",
|
|
121
|
+
permissionDecisionReason: reason ?? "Action requires user confirmation",
|
|
122
|
+
}),
|
|
123
|
+
modify: (updatedInput) => ({
|
|
124
|
+
modifiedArgs: updatedInput,
|
|
125
|
+
}),
|
|
126
|
+
context: (additionalContext) => ({
|
|
127
|
+
additionalContext,
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
"jetbrains-copilot": {
|
|
132
|
+
deny: (reason) => ({
|
|
133
|
+
permissionDecision: "deny",
|
|
134
|
+
permissionDecisionReason: reason,
|
|
135
|
+
}),
|
|
136
|
+
ask: () => ({
|
|
137
|
+
permissionDecision: "ask",
|
|
138
|
+
}),
|
|
139
|
+
modify: (updatedInput) => ({
|
|
140
|
+
hookSpecificOutput: {
|
|
141
|
+
hookEventName: "PreToolUse",
|
|
142
|
+
permissionDecision: "allow",
|
|
143
|
+
permissionDecisionReason: "Routed to context-mode sandbox",
|
|
144
|
+
updatedInput,
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
context: (additionalContext) => ({
|
|
148
|
+
hookSpecificOutput: {
|
|
149
|
+
hookEventName: "PreToolUse",
|
|
150
|
+
additionalContext,
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
"codex": {
|
|
156
|
+
deny: (reason) => ({
|
|
157
|
+
hookSpecificOutput: {
|
|
158
|
+
hookEventName: "PreToolUse",
|
|
159
|
+
permissionDecision: "deny",
|
|
160
|
+
permissionDecisionReason: reason,
|
|
161
|
+
},
|
|
162
|
+
}),
|
|
163
|
+
// Codex still rejects permissionDecision:"ask" in PreToolUse (verified
|
|
164
|
+
// against codex-cli 0.141.0 output_parser.rs). Keep dropping it.
|
|
165
|
+
ask: () => null,
|
|
166
|
+
// #845: modern Codex (>= 0.141.0) honors permissionDecision:"allow" +
|
|
167
|
+
// updatedInput (command rewrite). Emit it when the running Codex supports
|
|
168
|
+
// it; otherwise FAIL CLOSED — turn the redirect into an enforceable deny
|
|
169
|
+
// carrying the same guidance, so the bytes-flood guard never silently
|
|
170
|
+
// passes through. `codexSupportsRewrite` is detected at runtime by the
|
|
171
|
+
// codex hook (hooks/core/codex-caps.mjs) and threaded in via formatDecision.
|
|
172
|
+
modify: (updatedInput, { codexSupportsRewrite } = {}) => {
|
|
173
|
+
if (codexSupportsRewrite) {
|
|
174
|
+
return {
|
|
175
|
+
hookSpecificOutput: {
|
|
176
|
+
hookEventName: "PreToolUse",
|
|
177
|
+
permissionDecision: "allow",
|
|
178
|
+
updatedInput,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const ui = updatedInput ?? {};
|
|
183
|
+
// Only command redirects must fail closed. Non-command rewrites (e.g.
|
|
184
|
+
// Agent prompt injection) are advisory — drop rather than block the tool.
|
|
185
|
+
if (!("command" in ui)) return null;
|
|
186
|
+
return {
|
|
187
|
+
hookSpecificOutput: {
|
|
188
|
+
hookEventName: "PreToolUse",
|
|
189
|
+
permissionDecision: "deny",
|
|
190
|
+
permissionDecisionReason: codexRedirectReason(ui.command),
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
// #845: surface additionalContext on Codex builds that support it; older
|
|
195
|
+
// builds ignore the field, so drop the advisory nudge rather than emit a
|
|
196
|
+
// shape they reject.
|
|
197
|
+
context: (additionalContext, { codexSupportsRewrite } = {}) =>
|
|
198
|
+
codexSupportsRewrite
|
|
199
|
+
? { hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext } }
|
|
200
|
+
: null,
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
"kimi": {
|
|
204
|
+
// Kimi Code / Kimi CLI hook runners parse ONLY `permissionDecision === "deny"`
|
|
205
|
+
// for structured PreToolUse output. Anything else (ask / allow+updatedInput /
|
|
206
|
+
// additionalContext) is silently dropped, and the host's HookResult type has
|
|
207
|
+
// no `additionalContext` field at all.
|
|
208
|
+
// Evidence: refs/platforms/kimi-code/packages/agent-core/src/session/hooks/
|
|
209
|
+
// runner.ts:36-39,162-178 (HookSpecificOutputSchema + structuredOutput())
|
|
210
|
+
// Evidence: refs/platforms/kimi-code/packages/agent-core/src/session/hooks/
|
|
211
|
+
// types.ts:28-37 (HookResult has no additionalContext)
|
|
212
|
+
// Evidence: refs/platforms/kimi-cli/src/kimi_cli/hooks/runner.py:62-89
|
|
213
|
+
// (Python runtime behaves identically)
|
|
214
|
+
// This mirrors the codex precedent established at commit 607dc70 (#225),
|
|
215
|
+
// where the same upstream "deny-only" parser forced ask/modify/context to
|
|
216
|
+
// return null in the formatter rather than emit fields the host ignores.
|
|
217
|
+
deny: (reason) => ({
|
|
218
|
+
hookSpecificOutput: {
|
|
219
|
+
hookEventName: "PreToolUse",
|
|
220
|
+
permissionDecision: "deny",
|
|
221
|
+
permissionDecisionReason: reason,
|
|
222
|
+
},
|
|
223
|
+
}),
|
|
224
|
+
ask: () => null, // Kimi runner ignores permissionDecision !== "deny"
|
|
225
|
+
modify: () => null, // Kimi runner has no updatedInput channel
|
|
226
|
+
context: () => null, // Kimi HookResult has no additionalContext field
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
"antigravity-cli": {
|
|
230
|
+
// agy PreToolUse accepts the Claude-compatible top-level decision shape.
|
|
231
|
+
// agy 1.0.6 does NOT honor PreToolUse additionalContext (verified by
|
|
232
|
+
// transcript probe), so context guidance must become an enforceable deny
|
|
233
|
+
// or it disappears and the native tool runs unchanged.
|
|
234
|
+
deny: (reason) => ({ decision: "deny", reason }),
|
|
235
|
+
// Carry a fallback reason on `ask` so a security-policy ask (routing emits
|
|
236
|
+
// {action:"ask"} with no reason) never shows a bare, unexplained prompt.
|
|
237
|
+
ask: (reason) => ({ decision: "ask", reason: reason ?? "Action requires user confirmation" }),
|
|
238
|
+
// agy cannot modify tool args, so a routing `modify` becomes a deny. Surface
|
|
239
|
+
// the per-tool redirect guidance routing carried in `updatedInput.command`
|
|
240
|
+
// (an `echo "<guidance>"` payload that already uses agy's context-mode/<tool>
|
|
241
|
+
// surface) instead of a generic line; fall back to the generic redirect.
|
|
242
|
+
modify: (updatedInput) => {
|
|
243
|
+
const cmd = updatedInput?.command ?? updatedInput?.CommandLine ?? "";
|
|
244
|
+
const m = String(cmd).match(/^echo\s+"([\s\S]*)"\s*$/);
|
|
245
|
+
const guidance = m ? m[1].replace(/\\(["\\])/g, "$1") : "";
|
|
246
|
+
return {
|
|
247
|
+
decision: "deny",
|
|
248
|
+
reason:
|
|
249
|
+
guidance ||
|
|
250
|
+
"context-mode: redirected. Use the context-mode MCP tools (ctx_execute / ctx_fetch_and_index / ctx_search) so raw bytes stay out of the conversation.",
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
context: (additionalContext) => ({
|
|
254
|
+
decision: "deny",
|
|
255
|
+
reason: agyContextReason(additionalContext),
|
|
256
|
+
}),
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
"cursor": {
|
|
260
|
+
deny: (reason) => ({
|
|
261
|
+
permission: "deny",
|
|
262
|
+
user_message: reason,
|
|
263
|
+
}),
|
|
264
|
+
ask: () => ({
|
|
265
|
+
permission: "ask",
|
|
266
|
+
}),
|
|
267
|
+
modify: (updatedInput) => ({
|
|
268
|
+
updated_input: updatedInput,
|
|
269
|
+
}),
|
|
270
|
+
context: (additionalContext) => ({
|
|
271
|
+
agent_message: additionalContext,
|
|
272
|
+
}),
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// Keep in sync with the identical agyContextReason in
|
|
277
|
+
// src/adapters/antigravity-cli/index.ts: this bundled .mjs formatter (runtime
|
|
278
|
+
// hook path) and the TS adapter are separate layers; the text must not drift.
|
|
279
|
+
function agyContextReason(additionalContext) {
|
|
280
|
+
const text = String(additionalContext ?? "")
|
|
281
|
+
.replace(/<\/?context_guidance>/g, " ")
|
|
282
|
+
.replace(/<\/?tip>/g, " ")
|
|
283
|
+
.replace(/\s+/g, " ")
|
|
284
|
+
.trim();
|
|
285
|
+
return text
|
|
286
|
+
? `context-mode: use the context-mode MCP tools instead of this native tool. ${text}`
|
|
287
|
+
: "context-mode: use the context-mode MCP tools instead of this native tool so raw bytes stay out of the conversation.";
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// #845: routing wraps redirect guidance as `echo "<guidance>"`. Unwrap a command
|
|
291
|
+
// that is exactly `echo "<inner>"` (with optional surrounding whitespace) and
|
|
292
|
+
// return the inner string, or null when the shape doesn't match. Greedy: inner
|
|
293
|
+
// runs from the first `"` after `echo` to the last `"` before trailing space.
|
|
294
|
+
function unwrapEcho(command) {
|
|
295
|
+
const s = String(command ?? "");
|
|
296
|
+
// Match the regex `\s` class exactly: space, tab, newline, carriage return,
|
|
297
|
+
// form feed, vertical tab (so behavior is identical to /^echo\s+"…"\s*$/).
|
|
298
|
+
const isWs = (c) =>
|
|
299
|
+
c === " " || c === "\t" || c === "\n" || c === "\r" || c === "\f" || c === "\v";
|
|
300
|
+
if (!s.startsWith("echo")) return null;
|
|
301
|
+
let i = 4;
|
|
302
|
+
if (i >= s.length || !isWs(s[i])) return null; // `echo` must be followed by whitespace
|
|
303
|
+
while (i < s.length && isWs(s[i])) i++;
|
|
304
|
+
if (s[i] !== "\"") return null; // payload must open with a quote
|
|
305
|
+
let end = s.length;
|
|
306
|
+
while (end > 0 && isWs(s[end - 1])) end--; // drop trailing whitespace
|
|
307
|
+
if (end <= i + 1 || s[end - 1] !== "\"") return null; // must close with a quote
|
|
308
|
+
return s.slice(i + 1, end - 1);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Reverse the shell double-quote escaping routing applied: `\"` → `"`, `\\` → `\`.
|
|
312
|
+
function unescapeDquote(s) {
|
|
313
|
+
let out = "";
|
|
314
|
+
for (let i = 0; i < s.length; i++) {
|
|
315
|
+
if (s[i] === "\\" && (s[i + 1] === "\"" || s[i + 1] === "\\")) {
|
|
316
|
+
out += s[i + 1];
|
|
317
|
+
i++;
|
|
318
|
+
} else {
|
|
319
|
+
out += s[i];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// When Codex cannot rewrite the command we surface that guidance as the deny
|
|
326
|
+
// reason instead (mirrors the claude-code / antigravity-cli echo extraction).
|
|
327
|
+
function codexRedirectReason(command) {
|
|
328
|
+
const inner = unwrapEcho(command);
|
|
329
|
+
if (inner !== null) return unescapeDquote(inner);
|
|
330
|
+
return "context-mode: command redirected. Use the context-mode MCP tools (ctx_execute / ctx_fetch_and_index / ctx_search) so raw output stays out of the conversation.";
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Apply a formatter to a normalized routing decision.
|
|
335
|
+
* Returns the platform-specific JSON response, or null for passthrough.
|
|
336
|
+
*
|
|
337
|
+
* `opts` carries optional per-platform capability hints (e.g. codex
|
|
338
|
+
* `codexSupportsRewrite`). Formatters that ignore the extra argument are
|
|
339
|
+
* unaffected.
|
|
340
|
+
*/
|
|
341
|
+
export function formatDecision(platform, decision, opts = {}) {
|
|
342
|
+
if (!decision) return null;
|
|
343
|
+
|
|
344
|
+
const fmt = formatters[platform];
|
|
345
|
+
if (!fmt) return null;
|
|
346
|
+
|
|
347
|
+
switch (decision.action) {
|
|
348
|
+
case "deny": return fmt.deny(decision.reason);
|
|
349
|
+
// Pass the reason to ask() too — platforms whose ask formatter ignores it
|
|
350
|
+
// (legacy `ask: () => …`) are unaffected; copilot-cli surfaces it.
|
|
351
|
+
case "ask": return fmt.ask(decision.reason);
|
|
352
|
+
case "modify": return fmt.modify(decision.updatedInput, opts);
|
|
353
|
+
case "context": return fmt.context(decision.additionalContext, opts);
|
|
354
|
+
default: return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP readiness sentinel — checks if MCP server has started.
|
|
3
|
+
*
|
|
4
|
+
* Server writes sentinel (containing its PID) after connect().
|
|
5
|
+
* Hooks scan for any live sentinel to detect MCP readiness.
|
|
6
|
+
*
|
|
7
|
+
* Fix for #347: Claude Code spawns hooks via `bash -c "node ..."` on Linux/WSL2.
|
|
8
|
+
* The intermediate shell makes process.ppid point to a transient bash PID, not
|
|
9
|
+
* Claude Code. Directory-scan + PID liveness probe works regardless of spawn topology.
|
|
10
|
+
*
|
|
11
|
+
* Sentinel path: <tmpRoot>/context-mode-mcp-ready-<MCP_PID>
|
|
12
|
+
* Scan: glob all context-mode-mcp-ready-* files, probe each PID.
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
18
|
+
const SENTINEL_PREFIX = "context-mode-mcp-ready-";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Sentinel freshness window (#844). The MCP server refreshes its sentinel's
|
|
22
|
+
* mtime every 30s while alive (see `main()` in src/server.ts). A sentinel
|
|
23
|
+
* touched within this window is treated as a live server even when
|
|
24
|
+
* `process.kill(pid, 0)` cannot see the PID — e.g. a sandbox sharing /tmp
|
|
25
|
+
* across an isolated PID namespace, where the live host PID is invisible.
|
|
26
|
+
* 90s = 3x the server refresh interval, tolerant of scheduler jitter / load.
|
|
27
|
+
*/
|
|
28
|
+
const SENTINEL_FRESH_MS = 90_000;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the temp root — hardcoded /tmp on Unix to avoid TMPDIR mismatch.
|
|
32
|
+
* Tests may override via CONTEXT_MODE_MCP_SENTINEL_DIR to isolate scan from
|
|
33
|
+
* leftover sentinels in the real /tmp.
|
|
34
|
+
*/
|
|
35
|
+
export function sentinelDir() {
|
|
36
|
+
const override = process.env.CONTEXT_MODE_MCP_SENTINEL_DIR;
|
|
37
|
+
if (override && override.length > 0) return override;
|
|
38
|
+
return process.platform === "win32" ? tmpdir() : "/tmp";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build sentinel path for a given PID.
|
|
43
|
+
* Used by server.ts to write its own sentinel.
|
|
44
|
+
*/
|
|
45
|
+
export function sentinelPathForPid(pid) {
|
|
46
|
+
return join(sentinelDir(), `${SENTINEL_PREFIX}${pid}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @deprecated Use sentinelPathForPid(process.pid) from server.ts.
|
|
51
|
+
* Kept for backward compat during migration — tests that still
|
|
52
|
+
* write sentinels with process.ppid will work for one release cycle.
|
|
53
|
+
*/
|
|
54
|
+
export function sentinelPath() {
|
|
55
|
+
return join(sentinelDir(), `${SENTINEL_PREFIX}${process.ppid}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if any MCP server is alive by scanning sentinel files.
|
|
60
|
+
*
|
|
61
|
+
* Scans sentinelDir() for context-mode-mcp-ready-* files, reads the PID
|
|
62
|
+
* from each, and probes with kill(pid, 0). Cleans up stale sentinels
|
|
63
|
+
* from crashed servers.
|
|
64
|
+
*
|
|
65
|
+
* Handles:
|
|
66
|
+
* - PPID mismatch (WSL2 shell wrappers) — no ppid dependency
|
|
67
|
+
* - Stale sentinels (SIGKILL, OOM) — PID liveness check + age threshold
|
|
68
|
+
* - TMPDIR mismatch — hardcoded /tmp on Unix
|
|
69
|
+
* - Shared /tmp across isolated PID namespaces (#844) — a live host PID is
|
|
70
|
+
* invisible to `kill(pid, 0)` from a sandbox, so a recently-refreshed
|
|
71
|
+
* sentinel is trusted instead of being deleted.
|
|
72
|
+
*/
|
|
73
|
+
export function isMCPReady() {
|
|
74
|
+
try {
|
|
75
|
+
const dir = sentinelDir();
|
|
76
|
+
const files = readdirSync(dir).filter(f => f.startsWith(SENTINEL_PREFIX));
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
for (const f of files) {
|
|
79
|
+
const fullPath = join(dir, f);
|
|
80
|
+
let pid;
|
|
81
|
+
try {
|
|
82
|
+
pid = parseInt(readFileSync(fullPath, "utf8"), 10);
|
|
83
|
+
} catch {
|
|
84
|
+
// Unreadable (torn mid-write) — leave it for the owner / a later scan.
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (isNaN(pid)) continue;
|
|
88
|
+
try {
|
|
89
|
+
process.kill(pid, 0); // throws if the PID is not signalable from here
|
|
90
|
+
return true; // same-namespace liveness confirmed
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// EPERM: the process exists but is owned by another user → alive.
|
|
93
|
+
if (err && err.code === "EPERM") return true;
|
|
94
|
+
// ESRCH (or anything else): the PID is invisible from THIS namespace.
|
|
95
|
+
// That is NOT proof the server is dead — a shared /tmp across isolated
|
|
96
|
+
// PID namespaces (#844) hides a live host PID. Trust a recently
|
|
97
|
+
// refreshed sentinel rather than delete a live server's marker.
|
|
98
|
+
let ageMs = Infinity;
|
|
99
|
+
try { ageMs = now - statSync(fullPath).mtimeMs; } catch { /* stat failed → treat as stale */ }
|
|
100
|
+
if (ageMs < SENTINEL_FRESH_MS) return true;
|
|
101
|
+
// Old AND unprobeable → genuinely stale (crash / OOM / SIGKILL) → clean up.
|
|
102
|
+
try { unlinkSync(fullPath); } catch { /* best effort */ }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform detection from process env vars.
|
|
3
|
+
*
|
|
4
|
+
* Each supported platform sets a distinctive env var when invoking
|
|
5
|
+
* hook scripts; we use those to pick the correct tool-namer prefix
|
|
6
|
+
* and routing block. Falls back to "claude-code" when nothing matches
|
|
7
|
+
* so existing CC behavior is preserved.
|
|
8
|
+
*
|
|
9
|
+
* SINGLE SOURCE OF TRUTH: this table mirrors `PLATFORM_ENV_VARS` in
|
|
10
|
+
* `src/adapters/detect.ts:33-77`. Every entry has been verified against
|
|
11
|
+
* the platform's own runtime source code (full audit May 2026, see
|
|
12
|
+
* git blame). DO NOT add platform env vars here that aren't also in
|
|
13
|
+
* detect.ts — the two MUST stay in lock-step or detection will diverge
|
|
14
|
+
* between MCP-server-side and hook-script-side.
|
|
15
|
+
*
|
|
16
|
+
* Order matters — same as detect.ts. Forks listed BEFORE the fork's
|
|
17
|
+
* parent so collision detection works (e.g. cursor BEFORE vscode-copilot
|
|
18
|
+
* because Cursor inherits VSCODE_PID as a fork; antigravity BEFORE
|
|
19
|
+
* vscode-copilot for the same reason).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// Mirror of `PLATFORM_ENV_VARS` in src/adapters/detect.ts:33-77.
|
|
23
|
+
// Keep in lock-step. If you change one, change the other.
|
|
24
|
+
const PLATFORM_ENV_VARS_MIRROR = [
|
|
25
|
+
["claude-code", ["CLAUDE_PROJECT_DIR", "CLAUDE_SESSION_ID"]],
|
|
26
|
+
["antigravity", ["ANTIGRAVITY_CLI_ALIAS"]],
|
|
27
|
+
["cursor", ["CURSOR_TRACE_ID", "CURSOR_CLI"]],
|
|
28
|
+
["kilo", ["KILO_PID"]],
|
|
29
|
+
["opencode", ["OPENCODE_CLIENT", "OPENCODE_TERMINAL", "OPENCODE", "OPENCODE_PID"]],
|
|
30
|
+
["zed", ["ZED_SESSION_ID", "ZED_TERM"]],
|
|
31
|
+
["codex", ["CODEX_THREAD_ID", "CODEX_CI"]],
|
|
32
|
+
["gemini-cli", ["GEMINI_PROJECT_DIR", "GEMINI_CLI"]],
|
|
33
|
+
["vscode-copilot", ["VSCODE_PID", "VSCODE_CWD"]],
|
|
34
|
+
["jetbrains-copilot", ["IDEA_INITIAL_DIRECTORY"]],
|
|
35
|
+
["qwen-code", ["QWEN_PROJECT_DIR"]],
|
|
36
|
+
["pi", ["PI_PROJECT_DIR"]],
|
|
37
|
+
// openclaw — no auto-set process env vars; falls through to default
|
|
38
|
+
// kiro — no auto-set process env vars; falls through to default
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export function detectPlatformFromEnv(env = process.env) {
|
|
42
|
+
for (const [platform, vars] of PLATFORM_ENV_VARS_MIRROR) {
|
|
43
|
+
if (vars.some((v) => env[v])) return platform;
|
|
44
|
+
}
|
|
45
|
+
return "claude-code";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Re-exported for tests so they can assert against the same canonical table.
|
|
49
|
+
export { PLATFORM_ENV_VARS_MIRROR };
|