@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,995 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapters/codex — Codex CLI platform adapter.
|
|
3
|
+
*
|
|
4
|
+
* Implements HookAdapter for Codex CLI's JSON stdin/stdout paradigm.
|
|
5
|
+
*
|
|
6
|
+
* Codex CLI hook specifics:
|
|
7
|
+
* - 6 hook events: PreToolUse, PostToolUse, PreCompact, SessionStart, UserPromptSubmit, Stop
|
|
8
|
+
* - Same wire protocol as Claude Code (JSON stdin → stdout)
|
|
9
|
+
* - Config: $CODEX_HOME or ~/.codex (hooks.json + config.toml)
|
|
10
|
+
* - Session dir: $CODEX_HOME/context-mode/sessions/
|
|
11
|
+
*
|
|
12
|
+
* Hook dispatch is stable in Codex CLI. PreToolUse deny decisions work,
|
|
13
|
+
* while input rewriting remains blocked on upstream updatedInput support.
|
|
14
|
+
* Track: https://github.com/openai/codex/issues/18491
|
|
15
|
+
*/
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync, accessSync, copyFileSync, constants, mkdirSync, } from "node:fs";
|
|
18
|
+
import { resolve, dirname, join } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { BaseAdapter, resolveContextModeDataRoot } from "../base.js";
|
|
21
|
+
import { PLUGIN_KEY } from "../../package-identity.js";
|
|
22
|
+
import { hashProjectDirCanonical } from "../../session/db.js";
|
|
23
|
+
import { resolveCodexConfigDir } from "./paths.js";
|
|
24
|
+
// PreToolUse matcher: canonical Codex tool names + context-mode bare MCP tool
|
|
25
|
+
// names + external MCP catch-all literal (#529, #547 hotfix).
|
|
26
|
+
//
|
|
27
|
+
// Codex CLI's Rust `regex` crate does NOT support look-around, and
|
|
28
|
+
// `is_exact_matcher` (refs/platforms/codex/codex-rs/hooks/src/events/common.rs:152)
|
|
29
|
+
// short-circuits the regex engine entirely when the matcher contains only
|
|
30
|
+
// [A-Za-z0-9_|]. v1.0.124 shipped a matcher with `(?!.*context-mode)` AND
|
|
31
|
+
// `mcp__.*__ctx_*` regex syntax — Codex rejected the file at boot with
|
|
32
|
+
// "look-around not supported" → all v1.0.124 Codex users broken (#547).
|
|
33
|
+
//
|
|
34
|
+
// Fix: keep only literal tool names (charset-clean). The hook BODY already
|
|
35
|
+
// filters context-mode's own MCP tools via `isExternalMcpTool()` in
|
|
36
|
+
// hooks/core/routing.mjs, so dropping `mcp__.*__ctx_*` and the lookaround
|
|
37
|
+
// preserves end-to-end semantics. The literal `mcp__` final segment is a
|
|
38
|
+
// no-op under exact-matcher mode but kept for parity with hooks/hooks.json.
|
|
39
|
+
//
|
|
40
|
+
// Keep this as a single string literal — `codex.test.ts` drift-guard parses
|
|
41
|
+
// the source with a `"([^"]+)"` regex.
|
|
42
|
+
const PRE_TOOL_USE_MATCHER_PATTERN = "local_shell|shell|shell_command|exec_command|Bash|Shell|apply_patch|Edit|Write|grep_files|ctx_execute|ctx_execute_file|ctx_batch_execute|ctx_fetch_and_index|ctx_search|ctx_index|mcp__";
|
|
43
|
+
const CODEX_HOOK_COMMANDS = {
|
|
44
|
+
PreToolUse: "context-mode hook codex pretooluse",
|
|
45
|
+
PostToolUse: "context-mode hook codex posttooluse",
|
|
46
|
+
SessionStart: "context-mode hook codex sessionstart",
|
|
47
|
+
PreCompact: "context-mode hook codex precompact",
|
|
48
|
+
UserPromptSubmit: "context-mode hook codex userpromptsubmit",
|
|
49
|
+
Stop: "context-mode hook codex stop",
|
|
50
|
+
};
|
|
51
|
+
const LEGACY_HOOK_PATH_SUFFIXES = {
|
|
52
|
+
PreToolUse: ["hooks/pretooluse.mjs", "hooks/codex/pretooluse.mjs"],
|
|
53
|
+
PostToolUse: ["hooks/posttooluse.mjs", "hooks/codex/posttooluse.mjs"],
|
|
54
|
+
SessionStart: ["hooks/sessionstart.mjs", "hooks/codex/sessionstart.mjs"],
|
|
55
|
+
PreCompact: ["hooks/precompact.mjs", "hooks/codex/precompact.mjs"],
|
|
56
|
+
UserPromptSubmit: ["hooks/userpromptsubmit.mjs", "hooks/codex/userpromptsubmit.mjs"],
|
|
57
|
+
Stop: ["hooks/stop.mjs", "hooks/codex/stop.mjs"],
|
|
58
|
+
};
|
|
59
|
+
export function probeCodexCliVersion(runCommand = execFileSync) {
|
|
60
|
+
try {
|
|
61
|
+
const output = process.platform === "win32"
|
|
62
|
+
? runCommand("cmd.exe", ["/d", "/s", "/c", "codex --version"], {
|
|
63
|
+
encoding: "utf-8",
|
|
64
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
65
|
+
timeout: 5000,
|
|
66
|
+
})
|
|
67
|
+
: runCommand("codex", ["--version"], {
|
|
68
|
+
encoding: "utf-8",
|
|
69
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
70
|
+
timeout: 1500,
|
|
71
|
+
});
|
|
72
|
+
const version = String(output).trim();
|
|
73
|
+
return version.length > 0 ? version : "available (version output empty)";
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function parseCodexContextModePluginRoot(raw) {
|
|
80
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
81
|
+
// PLUGIN_KEY has no regex metacharacters ("context-mode@@mxalbert/context-mode")
|
|
82
|
+
// so it interpolates into the pattern verbatim.
|
|
83
|
+
const match = line.match(new RegExp(`^\\s*${PLUGIN_KEY}\\s+installed,\\s+enabled\\s+\\S+\\s+(.+?)\\s*$`));
|
|
84
|
+
if (match?.[1])
|
|
85
|
+
return match[1].trim();
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
function getTomlSection(raw, sectionName) {
|
|
90
|
+
const lines = raw.split(/\r?\n/);
|
|
91
|
+
let inSection = false;
|
|
92
|
+
const body = [];
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
const section = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
|
|
95
|
+
if (section) {
|
|
96
|
+
if (inSection)
|
|
97
|
+
break;
|
|
98
|
+
inSection = section[1]?.trim() === sectionName;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (inSection)
|
|
102
|
+
body.push(line);
|
|
103
|
+
}
|
|
104
|
+
return inSection ? body.join("\n") : null;
|
|
105
|
+
}
|
|
106
|
+
function hasCodexHooksFeature(raw) {
|
|
107
|
+
const features = getTomlSection(raw, "features");
|
|
108
|
+
return features !== null && /^\s*hooks\s*=\s*true\s*(?:#.*)?$/mi.test(features);
|
|
109
|
+
}
|
|
110
|
+
function hasDeprecatedCodexHooksFeature(raw) {
|
|
111
|
+
const features = getTomlSection(raw, "features");
|
|
112
|
+
return features !== null && /^\s*codex_hooks\s*=\s*true\s*(?:#.*)?$/mi.test(features);
|
|
113
|
+
}
|
|
114
|
+
function hasCodexPluginEnabled(raw) {
|
|
115
|
+
const plugin = getTomlSection(raw, `plugins."${PLUGIN_KEY}"`);
|
|
116
|
+
return plugin !== null && /^\s*enabled\s*=\s*true\s*(?:#.*)?$/mi.test(plugin);
|
|
117
|
+
}
|
|
118
|
+
function hasStandaloneContextModeMcp(raw) {
|
|
119
|
+
return getTomlSection(raw, "mcp_servers.context-mode") !== null;
|
|
120
|
+
}
|
|
121
|
+
function ensureCodexHooksFeature(raw) {
|
|
122
|
+
if (hasCodexHooksFeature(raw))
|
|
123
|
+
return { text: raw, changed: false };
|
|
124
|
+
const newline = raw.includes("\r\n") ? "\r\n" : "\n";
|
|
125
|
+
const lines = raw.split(/\r?\n/);
|
|
126
|
+
const featuresIndex = lines.findIndex((line) => /^\s*\[features\]\s*(?:#.*)?$/.test(line));
|
|
127
|
+
if (featuresIndex === -1) {
|
|
128
|
+
const prefix = raw.length > 0 && !raw.endsWith("\n") ? newline : "";
|
|
129
|
+
return {
|
|
130
|
+
text: `${raw}${prefix}[features]${newline}hooks = true${newline}`,
|
|
131
|
+
changed: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
let endIndex = lines.length;
|
|
135
|
+
for (let i = featuresIndex + 1; i < lines.length; i++) {
|
|
136
|
+
if (/^\s*\[[^\]]+\]\s*(?:#.*)?$/.test(lines[i] ?? "")) {
|
|
137
|
+
endIndex = i;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (let i = featuresIndex + 1; i < endIndex; i++) {
|
|
142
|
+
if (/^\s*hooks\s*=/.test(lines[i] ?? "")) {
|
|
143
|
+
lines[i] = "hooks = true";
|
|
144
|
+
return { text: lines.join(newline), changed: true };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
lines.splice(featuresIndex + 1, 0, "hooks = true");
|
|
148
|
+
return { text: lines.join(newline), changed: true };
|
|
149
|
+
}
|
|
150
|
+
function removeTomlSections(raw, shouldRemove) {
|
|
151
|
+
const newline = raw.includes("\r\n") ? "\r\n" : "\n";
|
|
152
|
+
const lines = raw.split(/\r?\n/);
|
|
153
|
+
const out = [];
|
|
154
|
+
const removed = [];
|
|
155
|
+
let skipping = false;
|
|
156
|
+
for (const line of lines) {
|
|
157
|
+
const section = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
|
|
158
|
+
if (section) {
|
|
159
|
+
const sectionName = section[1]?.trim() ?? "";
|
|
160
|
+
skipping = shouldRemove(sectionName);
|
|
161
|
+
if (skipping)
|
|
162
|
+
removed.push(sectionName);
|
|
163
|
+
}
|
|
164
|
+
if (!skipping)
|
|
165
|
+
out.push(line);
|
|
166
|
+
}
|
|
167
|
+
return { text: out.join(newline), removed };
|
|
168
|
+
}
|
|
169
|
+
function parseTomlQuotedString(raw) {
|
|
170
|
+
const trimmed = raw.trim();
|
|
171
|
+
if (!trimmed.startsWith('"') || !trimmed.endsWith('"'))
|
|
172
|
+
return null;
|
|
173
|
+
try {
|
|
174
|
+
const parsed = JSON.parse(trimmed);
|
|
175
|
+
return typeof parsed === "string" ? parsed : null;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Codex hook-state keys are TOML quoted keys, not guaranteed JSON strings.
|
|
179
|
+
// Preserve Windows backslashes such as C:\Users\... even when they are not
|
|
180
|
+
// valid JSON escapes, while still handling the common escaped quote/slash.
|
|
181
|
+
let out = "";
|
|
182
|
+
let escaping = false;
|
|
183
|
+
for (const ch of trimmed.slice(1, -1)) {
|
|
184
|
+
if (escaping) {
|
|
185
|
+
out += ch === '"' || ch === "\\" ? ch : `\\${ch}`;
|
|
186
|
+
escaping = false;
|
|
187
|
+
}
|
|
188
|
+
else if (ch === "\\") {
|
|
189
|
+
escaping = true;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
out += ch;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (escaping)
|
|
196
|
+
out += "\\";
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// ─────────────────────────────────────────────────────────
|
|
201
|
+
// Adapter implementation
|
|
202
|
+
// ─────────────────────────────────────────────────────────
|
|
203
|
+
export class CodexAdapter extends BaseAdapter {
|
|
204
|
+
codexPluginListRunner;
|
|
205
|
+
constructor(options = {}) {
|
|
206
|
+
super([".codex"]);
|
|
207
|
+
this.codexPluginListRunner = options.codexPluginListRunner ?? execFileSync;
|
|
208
|
+
}
|
|
209
|
+
name = "Codex CLI";
|
|
210
|
+
paradigm = "json-stdio";
|
|
211
|
+
capabilities = {
|
|
212
|
+
preToolUse: true,
|
|
213
|
+
postToolUse: true,
|
|
214
|
+
preCompact: true,
|
|
215
|
+
sessionStart: true,
|
|
216
|
+
canModifyArgs: false,
|
|
217
|
+
canModifyOutput: false,
|
|
218
|
+
canInjectSessionContext: true,
|
|
219
|
+
};
|
|
220
|
+
// ── Input parsing ──────────────────────────────────────
|
|
221
|
+
parsePreToolUseInput(raw) {
|
|
222
|
+
const input = raw;
|
|
223
|
+
return {
|
|
224
|
+
toolName: input.tool_name ?? "",
|
|
225
|
+
toolInput: input.tool_input ?? {},
|
|
226
|
+
sessionId: this.extractSessionId(input),
|
|
227
|
+
projectDir: this.getProjectDir(input),
|
|
228
|
+
raw,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
parsePostToolUseInput(raw) {
|
|
232
|
+
const input = raw;
|
|
233
|
+
return {
|
|
234
|
+
toolName: input.tool_name ?? "",
|
|
235
|
+
toolInput: input.tool_input ?? {},
|
|
236
|
+
toolOutput: input.tool_response,
|
|
237
|
+
sessionId: this.extractSessionId(input),
|
|
238
|
+
projectDir: this.getProjectDir(input),
|
|
239
|
+
raw,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
parsePreCompactInput(raw) {
|
|
243
|
+
const input = raw;
|
|
244
|
+
return {
|
|
245
|
+
sessionId: this.extractSessionId(input),
|
|
246
|
+
projectDir: this.getProjectDir(input),
|
|
247
|
+
raw,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
parseSessionStartInput(raw) {
|
|
251
|
+
const input = raw;
|
|
252
|
+
const rawSource = input.source ?? "startup";
|
|
253
|
+
let source;
|
|
254
|
+
switch (rawSource) {
|
|
255
|
+
case "compact":
|
|
256
|
+
source = "compact";
|
|
257
|
+
break;
|
|
258
|
+
case "resume":
|
|
259
|
+
source = "resume";
|
|
260
|
+
break;
|
|
261
|
+
case "clear":
|
|
262
|
+
source = "clear";
|
|
263
|
+
break;
|
|
264
|
+
default:
|
|
265
|
+
source = "startup";
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
sessionId: this.extractSessionId(input),
|
|
269
|
+
source,
|
|
270
|
+
projectDir: this.getProjectDir(input),
|
|
271
|
+
raw,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
// ── Response formatting ────────────────────────────────
|
|
275
|
+
// Codex CLI uses hookSpecificOutput wrapper for all hook responses.
|
|
276
|
+
// Unlike Claude Code, Codex does NOT support updatedInput or updatedMCPToolOutput.
|
|
277
|
+
formatPreToolUseResponse(response) {
|
|
278
|
+
if (response.decision === "deny") {
|
|
279
|
+
return {
|
|
280
|
+
hookSpecificOutput: {
|
|
281
|
+
hookEventName: "PreToolUse",
|
|
282
|
+
permissionDecision: "deny",
|
|
283
|
+
permissionDecisionReason: response.reason ?? "Blocked by context-mode hook",
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (response.decision === "context" && response.additionalContext) {
|
|
288
|
+
// Codex does not support additionalContext in PreToolUse (fails open).
|
|
289
|
+
// Context injection works via PostToolUse and SessionStart instead.
|
|
290
|
+
return {};
|
|
291
|
+
}
|
|
292
|
+
// "allow" — return empty object for passthrough
|
|
293
|
+
return {};
|
|
294
|
+
}
|
|
295
|
+
formatPostToolUseResponse(response) {
|
|
296
|
+
if (response.additionalContext) {
|
|
297
|
+
return {
|
|
298
|
+
hookSpecificOutput: {
|
|
299
|
+
hookEventName: "PostToolUse",
|
|
300
|
+
additionalContext: response.additionalContext,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return {};
|
|
305
|
+
}
|
|
306
|
+
formatPreCompactResponse(response) {
|
|
307
|
+
// Codex PreCompact currently accepts only universal hook fields.
|
|
308
|
+
// The hook script stores snapshots in context-mode's DB; SessionStart
|
|
309
|
+
// injects them after compaction.
|
|
310
|
+
return {};
|
|
311
|
+
}
|
|
312
|
+
formatSessionStartResponse(response) {
|
|
313
|
+
if (response.context) {
|
|
314
|
+
return {
|
|
315
|
+
hookSpecificOutput: {
|
|
316
|
+
hookEventName: "SessionStart",
|
|
317
|
+
additionalContext: response.context,
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return {};
|
|
322
|
+
}
|
|
323
|
+
// ── Configuration ──────────────────────────────────────
|
|
324
|
+
getConfigDir(_projectDir) {
|
|
325
|
+
return resolveCodexConfigDir();
|
|
326
|
+
}
|
|
327
|
+
getSettingsPath() {
|
|
328
|
+
return join(this.getConfigDir(), "config.toml");
|
|
329
|
+
}
|
|
330
|
+
getSessionDir() {
|
|
331
|
+
// Issue #649: honor CONTEXT_MODE_DATA_DIR universal storage override
|
|
332
|
+
// before falling back to the $CODEX_HOME-rooted default. Settings.toml
|
|
333
|
+
// and hooks.json continue to live under getConfigDir() so the Codex CLI
|
|
334
|
+
// sees its own config in the expected place.
|
|
335
|
+
const override = resolveContextModeDataRoot();
|
|
336
|
+
const dir = override
|
|
337
|
+
? join(override, "context-mode", "sessions")
|
|
338
|
+
: join(this.getConfigDir(), "context-mode", "sessions");
|
|
339
|
+
mkdirSync(dir, { recursive: true });
|
|
340
|
+
return dir;
|
|
341
|
+
}
|
|
342
|
+
// C2 narrowing (2026-05): the historical `getSessionDBPath` /
|
|
343
|
+
// `getSessionEventsPath` overrides were removed. Both delegated to the
|
|
344
|
+
// same canonical helpers (`resolveSessionDbPath` / `hashProjectDirCanonical`
|
|
345
|
+
// + `getWorktreeSuffix`) which already normalize the path internally —
|
|
346
|
+
// the explicit `normalizeWorktreePath` here was a no-op. Callers now reach
|
|
347
|
+
// the helpers directly through `adapter.getSessionDir()`.
|
|
348
|
+
getInstructionFiles() {
|
|
349
|
+
// Codex CLI honors AGENTS.md plus an optional override file.
|
|
350
|
+
return ["AGENTS.md", "AGENTS.override.md"];
|
|
351
|
+
}
|
|
352
|
+
getMemoryDir(projectDir) {
|
|
353
|
+
// Codex uses "memories" (plural), not the default "memory".
|
|
354
|
+
// Issue #649: honor CONTEXT_MODE_DATA_DIR for context-mode-owned
|
|
355
|
+
// persistent memory while preserving the platform-native plural folder
|
|
356
|
+
// name so legacy Codex tooling continues to find it when DATA_DIR is
|
|
357
|
+
// unset. Under the override, layout is `<DATA_DIR>/context-mode/memories`.
|
|
358
|
+
// Issue #663: scope by projectDir hash so parallel projects can't
|
|
359
|
+
// read each other's memory.
|
|
360
|
+
const override = resolveContextModeDataRoot();
|
|
361
|
+
const base = override
|
|
362
|
+
? join(override, "context-mode", "memories")
|
|
363
|
+
: join(this.getConfigDir(), "memories");
|
|
364
|
+
if (!projectDir)
|
|
365
|
+
return base;
|
|
366
|
+
return join(base, hashProjectDirCanonical(projectDir));
|
|
367
|
+
}
|
|
368
|
+
generateHookConfig(_pluginRoot) {
|
|
369
|
+
return {
|
|
370
|
+
PreToolUse: [
|
|
371
|
+
{
|
|
372
|
+
matcher: PRE_TOOL_USE_MATCHER_PATTERN,
|
|
373
|
+
hooks: [
|
|
374
|
+
{
|
|
375
|
+
type: "command",
|
|
376
|
+
command: CODEX_HOOK_COMMANDS.PreToolUse,
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
PostToolUse: [
|
|
382
|
+
{
|
|
383
|
+
matcher: "",
|
|
384
|
+
hooks: [
|
|
385
|
+
{
|
|
386
|
+
type: "command",
|
|
387
|
+
command: CODEX_HOOK_COMMANDS.PostToolUse,
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
SessionStart: [
|
|
393
|
+
{
|
|
394
|
+
matcher: "",
|
|
395
|
+
hooks: [
|
|
396
|
+
{
|
|
397
|
+
type: "command",
|
|
398
|
+
command: CODEX_HOOK_COMMANDS.SessionStart,
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
PreCompact: [
|
|
404
|
+
{
|
|
405
|
+
matcher: "",
|
|
406
|
+
hooks: [
|
|
407
|
+
{
|
|
408
|
+
type: "command",
|
|
409
|
+
command: CODEX_HOOK_COMMANDS.PreCompact,
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
UserPromptSubmit: [
|
|
415
|
+
{
|
|
416
|
+
matcher: "",
|
|
417
|
+
hooks: [
|
|
418
|
+
{
|
|
419
|
+
type: "command",
|
|
420
|
+
command: CODEX_HOOK_COMMANDS.UserPromptSubmit,
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
Stop: [
|
|
426
|
+
{
|
|
427
|
+
matcher: "",
|
|
428
|
+
hooks: [
|
|
429
|
+
{
|
|
430
|
+
type: "command",
|
|
431
|
+
command: CODEX_HOOK_COMMANDS.Stop,
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
readSettings() {
|
|
439
|
+
// Codex CLI uses TOML format. Full TOML parsing is complex;
|
|
440
|
+
// return null for now. MCP configuration should be done manually
|
|
441
|
+
// or via a dedicated TOML library in the upgrade flow.
|
|
442
|
+
try {
|
|
443
|
+
const raw = readFileSync(this.getSettingsPath(), "utf-8");
|
|
444
|
+
// Return raw TOML as a single-key object for inspection
|
|
445
|
+
return { _raw_toml: raw };
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
writeSettings(_settings) {
|
|
452
|
+
// Codex CLI uses TOML format. Writing TOML requires a dedicated
|
|
453
|
+
// serializer. This is a no-op; TOML config should be edited
|
|
454
|
+
// manually or via the `codex` CLI tool.
|
|
455
|
+
}
|
|
456
|
+
// ── Diagnostics (doctor) ─────────────────────────────────
|
|
457
|
+
validateHooks(pluginRoot) {
|
|
458
|
+
const results = [];
|
|
459
|
+
const codexCliVersion = probeCodexCliVersion();
|
|
460
|
+
let settingsRaw = "";
|
|
461
|
+
let settingsReadable = false;
|
|
462
|
+
results.push({
|
|
463
|
+
check: "Codex CLI binary",
|
|
464
|
+
status: codexCliVersion ? "pass" : "warn",
|
|
465
|
+
message: codexCliVersion
|
|
466
|
+
? `codex --version resolved to ${codexCliVersion}`
|
|
467
|
+
: "Could not run codex --version; hooks need the Codex CLI available on PATH",
|
|
468
|
+
...(codexCliVersion ? {} : { fix: "Install Codex CLI or make codex available on PATH" }),
|
|
469
|
+
});
|
|
470
|
+
try {
|
|
471
|
+
settingsRaw = readFileSync(this.getSettingsPath(), "utf-8");
|
|
472
|
+
settingsReadable = true;
|
|
473
|
+
const enabled = hasCodexHooksFeature(settingsRaw);
|
|
474
|
+
const deprecatedOnly = !enabled && hasDeprecatedCodexHooksFeature(settingsRaw);
|
|
475
|
+
results.push({
|
|
476
|
+
check: "Codex hooks feature flag",
|
|
477
|
+
status: enabled ? "pass" : "fail",
|
|
478
|
+
message: enabled
|
|
479
|
+
? `[features].hooks enabled in ${this.getSettingsPath()}`
|
|
480
|
+
: deprecatedOnly
|
|
481
|
+
? `[features].codex_hooks is deprecated; [features].hooks is missing in ${this.getSettingsPath()}`
|
|
482
|
+
: `[features].hooks missing from ${this.getSettingsPath()}`,
|
|
483
|
+
...(enabled ? {} : { fix: "context-mode upgrade" }),
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
results.push({
|
|
488
|
+
check: "Codex hooks feature flag",
|
|
489
|
+
status: "warn",
|
|
490
|
+
message: `Could not read ${this.getSettingsPath()}`,
|
|
491
|
+
fix: "context-mode upgrade",
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
const expected = this.generateHookConfig("");
|
|
495
|
+
const pluginHookStatus = this.getCodexPluginHookStatus(pluginRoot, settingsRaw, settingsReadable);
|
|
496
|
+
const codexPluginEnabled = pluginHookStatus.enabled;
|
|
497
|
+
const codexPluginHooksAvailable = pluginHookStatus.hooksAvailable;
|
|
498
|
+
if (codexPluginEnabled && pluginHookStatus.runtimeRoot) {
|
|
499
|
+
results.push({
|
|
500
|
+
check: "Codex plugin root",
|
|
501
|
+
status: pluginHookStatus.rootMismatch ? "warn" : "pass",
|
|
502
|
+
message: pluginHookStatus.rootMismatch
|
|
503
|
+
? `context-mode doctor is running from ${pluginHookStatus.configuredRoot}, but Codex plugin manager reports ${pluginHookStatus.runtimeRoot}`
|
|
504
|
+
: `Codex plugin manager reports ${pluginHookStatus.runtimeRoot}`,
|
|
505
|
+
...(pluginHookStatus.rootMismatch
|
|
506
|
+
? { fix: "Restart Codex after upgrade; run context-mode upgrade to keep native user-hook fallback until the plugin root converges" }
|
|
507
|
+
: {}),
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
else if (codexPluginEnabled) {
|
|
511
|
+
results.push({
|
|
512
|
+
check: "Codex plugin root",
|
|
513
|
+
status: "warn",
|
|
514
|
+
message: `${PLUGIN_KEY} is enabled, but \`codex plugin list\` did not report its runtime root`,
|
|
515
|
+
fix: `Restart Codex or verify \`codex plugin list\` shows ${PLUGIN_KEY} installed and enabled`,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
if (codexPluginEnabled && !codexPluginHooksAvailable) {
|
|
519
|
+
const expectedRoot = pluginHookStatus.runtimeRoot ?? pluginRoot;
|
|
520
|
+
results.push({
|
|
521
|
+
check: "Codex plugin hooks",
|
|
522
|
+
status: "fail",
|
|
523
|
+
message: `context-mode Codex plugin is enabled, but ${join(expectedRoot, ".codex-plugin", "hooks.json")} is missing`,
|
|
524
|
+
fix: "Reinstall or upgrade the context-mode Codex plugin",
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
if (codexPluginEnabled && hasStandaloneContextModeMcp(settingsRaw)) {
|
|
528
|
+
results.push({
|
|
529
|
+
check: "Standalone MCP duplicate",
|
|
530
|
+
status: "warn",
|
|
531
|
+
message: `[mcp_servers.context-mode] is still registered while ${PLUGIN_KEY} is enabled; Codex may start both plugin and standalone MCP surfaces`,
|
|
532
|
+
fix: "context-mode upgrade (removes the standalone Codex MCP registration when the plugin owns context-mode)",
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
const hookConfig = this.readHooksConfig();
|
|
536
|
+
if (!hookConfig.ok) {
|
|
537
|
+
if (hookConfig.reason === "missing" && codexPluginHooksAvailable) {
|
|
538
|
+
const pluginHookChecks = Object.keys(expected).map((hookName) => ({
|
|
539
|
+
check: `${hookName} hook`,
|
|
540
|
+
status: "pass",
|
|
541
|
+
message: `${hookName} hook provided by ${PLUGIN_KEY} plugin`,
|
|
542
|
+
}));
|
|
543
|
+
return results.concat(pluginHookChecks);
|
|
544
|
+
}
|
|
545
|
+
if (hookConfig.reason === "missing") {
|
|
546
|
+
return results.concat([{
|
|
547
|
+
check: "Hooks config",
|
|
548
|
+
status: "fail",
|
|
549
|
+
message: `No readable ${this.getHooksPath()} found`,
|
|
550
|
+
fix: "Copy configs/codex/hooks.json to hooks.json or run context-mode upgrade",
|
|
551
|
+
}]);
|
|
552
|
+
}
|
|
553
|
+
if (hookConfig.reason === "invalid_json") {
|
|
554
|
+
return results.concat([{
|
|
555
|
+
check: "Hooks config",
|
|
556
|
+
status: "fail",
|
|
557
|
+
message: `${this.getHooksPath()} is not valid JSON: ${hookConfig.error}`,
|
|
558
|
+
fix: "Repair hooks.json so it contains valid JSON, then rerun context-mode upgrade if needed",
|
|
559
|
+
}]);
|
|
560
|
+
}
|
|
561
|
+
return results.concat([{
|
|
562
|
+
check: "Hooks config",
|
|
563
|
+
status: "fail",
|
|
564
|
+
message: `Could not read ${this.getHooksPath()}: ${hookConfig.error}`,
|
|
565
|
+
fix: "Check permissions and file accessibility for hooks.json, then rerun context-mode upgrade if needed",
|
|
566
|
+
}]);
|
|
567
|
+
}
|
|
568
|
+
if (!hookConfig.config.hooks && !codexPluginHooksAvailable) {
|
|
569
|
+
return results.concat([{
|
|
570
|
+
check: "Hooks config",
|
|
571
|
+
status: "fail",
|
|
572
|
+
message: `${this.getHooksPath()} is missing the top-level hooks object`,
|
|
573
|
+
fix: `Update ${this.getHooksPath()} to match configs/codex/hooks.json`,
|
|
574
|
+
}]);
|
|
575
|
+
}
|
|
576
|
+
const hookChecks = codexPluginHooksAvailable
|
|
577
|
+
? Object.keys(expected).map((hookName) => ({
|
|
578
|
+
check: `${hookName} hook`,
|
|
579
|
+
status: "pass",
|
|
580
|
+
message: `${hookName} hook provided by ${PLUGIN_KEY} plugin`,
|
|
581
|
+
}))
|
|
582
|
+
: Object.entries(expected).map(([hookName, entries]) => {
|
|
583
|
+
const actualEntries = hookConfig.config.hooks?.[hookName];
|
|
584
|
+
const expectedEntry = entries[0];
|
|
585
|
+
const ok = Array.isArray(actualEntries)
|
|
586
|
+
&& actualEntries.some((entry) => this.isExpectedHookEntry(hookName, entry, expectedEntry));
|
|
587
|
+
const missingStatus = hookName === "PreCompact" ? "warn" : "fail";
|
|
588
|
+
return {
|
|
589
|
+
check: `${hookName} hook`,
|
|
590
|
+
status: (ok ? "pass" : missingStatus),
|
|
591
|
+
message: ok
|
|
592
|
+
? `${hookName} hook configured in ${this.getHooksPath()}`
|
|
593
|
+
: hookName === "PreCompact"
|
|
594
|
+
? `${hookName} hook missing or not pointing to context-mode; compaction snapshots require a Codex build that emits PreCompact`
|
|
595
|
+
: `${hookName} hook missing or not pointing to context-mode`,
|
|
596
|
+
fix: ok ? undefined : `Update ${this.getHooksPath()} to match configs/codex/hooks.json`,
|
|
597
|
+
};
|
|
598
|
+
});
|
|
599
|
+
// #603: surface duplicate context-mode entries per hook event. Codex fires
|
|
600
|
+
// every matching entry, so duplicates double the work, can saturate the
|
|
601
|
+
// MCP transport (`Transport closed`), and have been observed to inflate
|
|
602
|
+
// codex-tui.log into the multi-GB range. `context-mode upgrade` collapses
|
|
603
|
+
// them via `upsertManagedHookEntry`, so the fix is one command away.
|
|
604
|
+
const duplicateChecks = [];
|
|
605
|
+
for (const hookName of Object.keys(expected)) {
|
|
606
|
+
const actualEntries = hookConfig.config.hooks?.[hookName];
|
|
607
|
+
if (!Array.isArray(actualEntries))
|
|
608
|
+
continue;
|
|
609
|
+
const managedCount = actualEntries.filter((entry) => this.isManagedContextModeEntry(hookName, entry)).length;
|
|
610
|
+
if (managedCount > 1) {
|
|
611
|
+
duplicateChecks.push({
|
|
612
|
+
check: `${hookName} duplicates`,
|
|
613
|
+
status: "warn",
|
|
614
|
+
message: `${managedCount} context-mode entries found for ${hookName} in ${this.getHooksPath()}; Codex will fire all of them`,
|
|
615
|
+
fix: "context-mode upgrade (collapses duplicate context-mode entries; preserves unrelated hooks)",
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
else if (codexPluginHooksAvailable && managedCount === 1) {
|
|
619
|
+
duplicateChecks.push({
|
|
620
|
+
check: `${hookName} plugin duplicate`,
|
|
621
|
+
status: "warn",
|
|
622
|
+
message: `${hookName} is configured in both ${this.getHooksPath()} and the context-mode Codex plugin; Codex will fire both hooks`,
|
|
623
|
+
fix: "context-mode upgrade (removes user config context-mode hooks; preserves unrelated hooks)",
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return results.concat(hookChecks, duplicateChecks);
|
|
628
|
+
}
|
|
629
|
+
checkPluginRegistration() {
|
|
630
|
+
try {
|
|
631
|
+
const raw = readFileSync(this.getSettingsPath(), "utf-8");
|
|
632
|
+
const pluginEnabled = hasCodexPluginEnabled(raw);
|
|
633
|
+
const standaloneMcp = hasStandaloneContextModeMcp(raw);
|
|
634
|
+
const hasMcpSection = raw.includes("[mcp_servers]") || raw.includes("[mcp_servers.");
|
|
635
|
+
if (pluginEnabled && standaloneMcp) {
|
|
636
|
+
return {
|
|
637
|
+
check: "MCP registration",
|
|
638
|
+
status: "warn",
|
|
639
|
+
message: `${PLUGIN_KEY} plugin is enabled, but standalone [mcp_servers.context-mode] is also configured`,
|
|
640
|
+
fix: "context-mode upgrade",
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
if (pluginEnabled) {
|
|
644
|
+
return {
|
|
645
|
+
check: "MCP registration",
|
|
646
|
+
status: "pass",
|
|
647
|
+
message: `${PLUGIN_KEY} plugin enabled`,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
if (standaloneMcp) {
|
|
651
|
+
return {
|
|
652
|
+
check: "MCP registration",
|
|
653
|
+
status: "pass",
|
|
654
|
+
message: "context-mode found in [mcp_servers] config",
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
if (hasMcpSection) {
|
|
658
|
+
return {
|
|
659
|
+
check: "MCP registration",
|
|
660
|
+
status: "fail",
|
|
661
|
+
message: "[mcp_servers] section exists but context-mode not found",
|
|
662
|
+
fix: `Add context-mode to [mcp_servers] in ${this.getSettingsPath()}`,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
check: "MCP registration",
|
|
667
|
+
status: "fail",
|
|
668
|
+
message: "No [mcp_servers] section in config.toml",
|
|
669
|
+
fix: `Add [mcp_servers.context-mode] to ${this.getSettingsPath()}`,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
return {
|
|
674
|
+
check: "MCP registration",
|
|
675
|
+
status: "warn",
|
|
676
|
+
message: `Could not read ${this.getSettingsPath()}`,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
getInstalledVersion() {
|
|
681
|
+
// Codex uses standalone MCP registration; there is no platform-owned
|
|
682
|
+
// plugin version to compare against the context-mode npm package.
|
|
683
|
+
return "standalone";
|
|
684
|
+
}
|
|
685
|
+
// ── Upgrade ────────────────────────────────────────────
|
|
686
|
+
configureAllHooks(pluginRoot) {
|
|
687
|
+
const hookConfig = this.readHooksConfig();
|
|
688
|
+
const changes = [];
|
|
689
|
+
const settingsPath = this.getSettingsPath();
|
|
690
|
+
let settingsRaw = "";
|
|
691
|
+
try {
|
|
692
|
+
settingsRaw = readFileSync(settingsPath, "utf-8");
|
|
693
|
+
}
|
|
694
|
+
catch {
|
|
695
|
+
settingsRaw = "";
|
|
696
|
+
}
|
|
697
|
+
const pluginHookStatus = this.getCodexPluginHookStatus(pluginRoot, settingsRaw, settingsRaw.length > 0);
|
|
698
|
+
const codexPluginOwnsHooks = pluginHookStatus.ownsHooksForUpgrade;
|
|
699
|
+
let hookFile;
|
|
700
|
+
if (hookConfig.ok) {
|
|
701
|
+
hookFile = hookConfig.config;
|
|
702
|
+
}
|
|
703
|
+
else if (hookConfig.reason === "missing") {
|
|
704
|
+
hookFile = { hooks: {} };
|
|
705
|
+
}
|
|
706
|
+
else if (hookConfig.reason === "invalid_json") {
|
|
707
|
+
const backupPath = this.backupFile(this.getHooksPath(), ".broken");
|
|
708
|
+
changes.push(`Backed up malformed Codex hooks to ${backupPath}`);
|
|
709
|
+
hookFile = { hooks: {} };
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
throw new Error(`Failed to update ${this.getHooksPath()}: ${hookConfig.error}`);
|
|
713
|
+
}
|
|
714
|
+
const hooks = hookFile.hooks && typeof hookFile.hooks === "object" && !Array.isArray(hookFile.hooks)
|
|
715
|
+
? hookFile.hooks
|
|
716
|
+
: {};
|
|
717
|
+
const desiredHooks = this.generateHookConfig(pluginRoot);
|
|
718
|
+
const hookChangeStart = changes.length;
|
|
719
|
+
if (codexPluginOwnsHooks) {
|
|
720
|
+
for (const hookName of Object.keys(desiredHooks)) {
|
|
721
|
+
this.removeManagedHookEntries(hooks, hookName, changes);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
for (const [hookName, entries] of Object.entries(desiredHooks)) {
|
|
726
|
+
this.upsertManagedHookEntry(hooks, hookName, entries[0], changes);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (changes.length > hookChangeStart) {
|
|
730
|
+
hookFile.hooks = hooks;
|
|
731
|
+
this.writeHooksConfig(hookFile);
|
|
732
|
+
changes.push(codexPluginOwnsHooks
|
|
733
|
+
? `Removed duplicate context-mode user hooks from ${this.getHooksPath()}`
|
|
734
|
+
: `Wrote native Codex hooks to ${this.getHooksPath()}`);
|
|
735
|
+
}
|
|
736
|
+
let settingsText = ensureCodexHooksFeature(settingsRaw).text;
|
|
737
|
+
const enabledSettingsChanged = settingsText !== settingsRaw;
|
|
738
|
+
if (codexPluginOwnsHooks) {
|
|
739
|
+
const removedMcp = removeTomlSections(settingsText, (sectionName) => sectionName === "mcp_servers.context-mode"
|
|
740
|
+
|| sectionName.startsWith("mcp_servers.context-mode.tools."));
|
|
741
|
+
if (removedMcp.removed.length > 0) {
|
|
742
|
+
settingsText = removedMcp.text;
|
|
743
|
+
changes.push("Removed standalone Codex context-mode MCP registration");
|
|
744
|
+
}
|
|
745
|
+
const prunedTrust = this.pruneStaleUserHookTrustState(settingsText, hooks);
|
|
746
|
+
if (prunedTrust.removed.length > 0) {
|
|
747
|
+
settingsText = prunedTrust.text;
|
|
748
|
+
changes.push(`Removed ${prunedTrust.removed.length} stale Codex hook trust entr${prunedTrust.removed.length === 1 ? "y" : "ies"}`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (settingsText !== settingsRaw) {
|
|
752
|
+
const newline = settingsText.includes("\r\n") ? "\r\n" : "\n";
|
|
753
|
+
const text = settingsText.endsWith("\n")
|
|
754
|
+
? settingsText
|
|
755
|
+
: `${settingsText}${newline}`;
|
|
756
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
757
|
+
writeFileSync(settingsPath, text, "utf-8");
|
|
758
|
+
if (enabledSettingsChanged)
|
|
759
|
+
changes.push("Enabled Codex hooks feature flag");
|
|
760
|
+
}
|
|
761
|
+
return changes;
|
|
762
|
+
}
|
|
763
|
+
backupSettings() {
|
|
764
|
+
let firstBackupPath = null;
|
|
765
|
+
for (const settingsPath of [this.getHooksPath(), this.getSettingsPath()]) {
|
|
766
|
+
try {
|
|
767
|
+
accessSync(settingsPath, constants.R_OK);
|
|
768
|
+
const backupPath = this.backupFile(settingsPath);
|
|
769
|
+
firstBackupPath ??= backupPath;
|
|
770
|
+
}
|
|
771
|
+
catch {
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return firstBackupPath;
|
|
776
|
+
}
|
|
777
|
+
setHookPermissions(_pluginRoot) {
|
|
778
|
+
// Hook permissions are set during plugin install
|
|
779
|
+
return [];
|
|
780
|
+
}
|
|
781
|
+
updatePluginRegistry(_pluginRoot, _version) {
|
|
782
|
+
// Codex CLI has no plugin registry
|
|
783
|
+
}
|
|
784
|
+
getRoutingInstructions() {
|
|
785
|
+
const instructionsPath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "configs", "codex", "AGENTS.md");
|
|
786
|
+
try {
|
|
787
|
+
return readFileSync(instructionsPath, "utf-8");
|
|
788
|
+
}
|
|
789
|
+
catch {
|
|
790
|
+
// Fallback inline instructions
|
|
791
|
+
return "# context-mode\n\nUse context-mode MCP tools (execute, execute_file, batch_execute, fetch_and_index, search) instead of bash/cat/curl for data-heavy operations.";
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// ── Internal helpers ───────────────────────────────────
|
|
795
|
+
/**
|
|
796
|
+
* Resolve the project directory for a Codex hook input.
|
|
797
|
+
* Priority: input.cwd > CODEX_PROJECT_DIR env > process.cwd().
|
|
798
|
+
* Mirrors the cursor / opencode pattern so downstream hooks always
|
|
799
|
+
* receive a defined projectDir even under worktrees or when the
|
|
800
|
+
* platform omits cwd from the wire payload.
|
|
801
|
+
*/
|
|
802
|
+
getProjectDir(input) {
|
|
803
|
+
return input.cwd ?? process.env.CODEX_PROJECT_DIR ?? process.cwd();
|
|
804
|
+
}
|
|
805
|
+
getHooksPath() {
|
|
806
|
+
return join(this.getConfigDir(), "hooks.json");
|
|
807
|
+
}
|
|
808
|
+
backupFile(filePath, suffix = "") {
|
|
809
|
+
const backupPath = suffix
|
|
810
|
+
? `${filePath}${suffix}-${new Date().toISOString().replace(/[:.]/g, "-")}.bak`
|
|
811
|
+
: `${filePath}.bak`;
|
|
812
|
+
copyFileSync(filePath, backupPath);
|
|
813
|
+
return backupPath;
|
|
814
|
+
}
|
|
815
|
+
readHooksConfig() {
|
|
816
|
+
const hooksPath = this.getHooksPath();
|
|
817
|
+
try {
|
|
818
|
+
return { ok: true, config: JSON.parse(readFileSync(hooksPath, "utf-8")) };
|
|
819
|
+
}
|
|
820
|
+
catch (error) {
|
|
821
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
822
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
823
|
+
? String(error.code ?? "")
|
|
824
|
+
: "";
|
|
825
|
+
if (code === "ENOENT") {
|
|
826
|
+
return { ok: false, reason: "missing" };
|
|
827
|
+
}
|
|
828
|
+
if (error instanceof SyntaxError) {
|
|
829
|
+
return { ok: false, reason: "invalid_json", error: message };
|
|
830
|
+
}
|
|
831
|
+
return { ok: false, reason: "read_error", error: message };
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
writeHooksConfig(config) {
|
|
835
|
+
const hooksPath = this.getHooksPath();
|
|
836
|
+
mkdirSync(dirname(hooksPath), { recursive: true });
|
|
837
|
+
writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
838
|
+
}
|
|
839
|
+
upsertManagedHookEntry(hooks, hookName, expectedEntry, changes) {
|
|
840
|
+
const currentEntries = Array.isArray(hooks[hookName]) ? [...hooks[hookName]] : [];
|
|
841
|
+
const managedIndices = currentEntries
|
|
842
|
+
.map((entry, index) => this.isManagedContextModeEntry(hookName, entry) ? index : -1)
|
|
843
|
+
.filter((index) => index >= 0);
|
|
844
|
+
if (managedIndices.length === 0) {
|
|
845
|
+
currentEntries.push(expectedEntry);
|
|
846
|
+
hooks[hookName] = currentEntries;
|
|
847
|
+
changes.push(`Added ${hookName} hook`);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const primaryIndex = managedIndices[0];
|
|
851
|
+
if (JSON.stringify(currentEntries[primaryIndex]) !== JSON.stringify(expectedEntry)) {
|
|
852
|
+
currentEntries[primaryIndex] = expectedEntry;
|
|
853
|
+
changes.push(`Updated ${hookName} hook`);
|
|
854
|
+
}
|
|
855
|
+
for (const duplicateIndex of managedIndices.slice(1).reverse()) {
|
|
856
|
+
currentEntries.splice(duplicateIndex, 1);
|
|
857
|
+
changes.push(`Removed duplicate ${hookName} context-mode hook`);
|
|
858
|
+
}
|
|
859
|
+
hooks[hookName] = currentEntries;
|
|
860
|
+
}
|
|
861
|
+
removeManagedHookEntries(hooks, hookName, changes) {
|
|
862
|
+
const currentEntries = Array.isArray(hooks[hookName]) ? [...hooks[hookName]] : [];
|
|
863
|
+
const filtered = currentEntries.filter((entry) => !this.isManagedContextModeEntry(hookName, entry));
|
|
864
|
+
const removed = currentEntries.length - filtered.length;
|
|
865
|
+
if (removed === 0)
|
|
866
|
+
return;
|
|
867
|
+
if (filtered.length > 0) {
|
|
868
|
+
hooks[hookName] = filtered;
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
delete hooks[hookName];
|
|
872
|
+
}
|
|
873
|
+
changes.push(`Removed ${removed} ${hookName} context-mode user hook${removed === 1 ? "" : "s"}`);
|
|
874
|
+
}
|
|
875
|
+
hasCodexPluginHookManifest(pluginRoot) {
|
|
876
|
+
return existsSync(join(pluginRoot, ".codex-plugin", "hooks.json"));
|
|
877
|
+
}
|
|
878
|
+
getCodexPluginHookStatus(pluginRoot, settingsRaw, settingsReadable) {
|
|
879
|
+
const enabled = settingsReadable && hasCodexPluginEnabled(settingsRaw);
|
|
880
|
+
const configuredRoot = resolve(pluginRoot);
|
|
881
|
+
const configuredManifestAvailable = this.hasCodexPluginHookManifest(configuredRoot);
|
|
882
|
+
const runtimeRoot = enabled ? this.probeCodexContextModePluginRoot() : null;
|
|
883
|
+
const runtimeManifestAvailable = runtimeRoot
|
|
884
|
+
? this.hasCodexPluginHookManifest(runtimeRoot)
|
|
885
|
+
: false;
|
|
886
|
+
const rootMismatch = runtimeRoot
|
|
887
|
+
? !this.samePath(configuredRoot, runtimeRoot)
|
|
888
|
+
: false;
|
|
889
|
+
const hooksAvailable = enabled && (runtimeManifestAvailable
|
|
890
|
+
|| (!runtimeRoot && configuredManifestAvailable));
|
|
891
|
+
return {
|
|
892
|
+
enabled,
|
|
893
|
+
configuredRoot,
|
|
894
|
+
configuredManifestAvailable,
|
|
895
|
+
runtimeRoot,
|
|
896
|
+
runtimeManifestAvailable,
|
|
897
|
+
rootMismatch,
|
|
898
|
+
hooksAvailable,
|
|
899
|
+
ownsHooksForUpgrade: enabled
|
|
900
|
+
&& runtimeRoot !== null
|
|
901
|
+
&& runtimeManifestAvailable
|
|
902
|
+
&& !rootMismatch,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
probeCodexContextModePluginRoot() {
|
|
906
|
+
try {
|
|
907
|
+
const output = process.platform === "win32"
|
|
908
|
+
? this.codexPluginListRunner("cmd.exe", ["/d", "/s", "/c", "codex plugin list"], {
|
|
909
|
+
encoding: "utf-8",
|
|
910
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
911
|
+
timeout: 5000,
|
|
912
|
+
})
|
|
913
|
+
: this.codexPluginListRunner("codex", ["plugin", "list"], {
|
|
914
|
+
encoding: "utf-8",
|
|
915
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
916
|
+
timeout: 5000,
|
|
917
|
+
});
|
|
918
|
+
return parseCodexContextModePluginRoot(String(output));
|
|
919
|
+
}
|
|
920
|
+
catch {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
samePath(left, right) {
|
|
925
|
+
return this.normalizeCommand(resolve(left)) === this.normalizeCommand(resolve(right));
|
|
926
|
+
}
|
|
927
|
+
pruneStaleUserHookTrustState(settingsRaw, hooks) {
|
|
928
|
+
const hooksPath = this.normalizeCommand(this.getHooksPath());
|
|
929
|
+
const eventNames = {
|
|
930
|
+
post_compact: "PostCompact",
|
|
931
|
+
post_tool_use: "PostToolUse",
|
|
932
|
+
pre_compact: "PreCompact",
|
|
933
|
+
pre_tool_use: "PreToolUse",
|
|
934
|
+
session_start: "SessionStart",
|
|
935
|
+
stop: "Stop",
|
|
936
|
+
user_prompt_submit: "UserPromptSubmit",
|
|
937
|
+
};
|
|
938
|
+
return removeTomlSections(settingsRaw, (sectionName) => {
|
|
939
|
+
const prefix = "hooks.state.";
|
|
940
|
+
if (!sectionName.startsWith(prefix))
|
|
941
|
+
return false;
|
|
942
|
+
const key = parseTomlQuotedString(sectionName.slice(prefix.length));
|
|
943
|
+
if (key === null)
|
|
944
|
+
return false;
|
|
945
|
+
const normalized = this.normalizeCommand(key);
|
|
946
|
+
const parts = normalized.split(":");
|
|
947
|
+
const hookIndex = Number(parts.pop());
|
|
948
|
+
const entryIndex = Number(parts.pop());
|
|
949
|
+
const eventName = eventNames[parts.pop() ?? ""];
|
|
950
|
+
const stateHooksPath = parts.join(":");
|
|
951
|
+
if (stateHooksPath !== hooksPath
|
|
952
|
+
|| !eventName
|
|
953
|
+
|| !Number.isInteger(entryIndex)
|
|
954
|
+
|| !Number.isInteger(hookIndex)) {
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
const entry = hooks[eventName]?.[entryIndex];
|
|
958
|
+
return !entry || !Array.isArray(entry.hooks) || !entry.hooks[hookIndex];
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
isExpectedHookEntry(hookName, entry, expectedEntry) {
|
|
962
|
+
if (!entry || typeof entry !== "object")
|
|
963
|
+
return false;
|
|
964
|
+
if (hookName === "PreToolUse" && entry.matcher !== expectedEntry.matcher) {
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
return this.entryContainsManagedCommand(hookName, entry);
|
|
968
|
+
}
|
|
969
|
+
isManagedContextModeEntry(hookName, entry) {
|
|
970
|
+
if (!entry || typeof entry !== "object")
|
|
971
|
+
return false;
|
|
972
|
+
return this.entryContainsManagedCommand(hookName, entry);
|
|
973
|
+
}
|
|
974
|
+
entryContainsManagedCommand(hookName, entry) {
|
|
975
|
+
const normalizedCommands = (Array.isArray(entry.hooks) ? entry.hooks : [])
|
|
976
|
+
.map((hook) => this.normalizeCommand(hook.command))
|
|
977
|
+
.filter((command) => command.length > 0);
|
|
978
|
+
const expectedCliCommand = this.normalizeCommand(CODEX_HOOK_COMMANDS[hookName] ?? "");
|
|
979
|
+
const legacySuffixes = LEGACY_HOOK_PATH_SUFFIXES[hookName] ?? [];
|
|
980
|
+
return normalizedCommands.some((command) => command.includes(expectedCliCommand)
|
|
981
|
+
|| legacySuffixes.some((suffix) => command.includes(suffix)));
|
|
982
|
+
}
|
|
983
|
+
normalizeCommand(command) {
|
|
984
|
+
return (command ?? "").replace(/\\/g, "/");
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Extract session ID from Codex CLI hook input.
|
|
988
|
+
* Priority: session_id field > fallback to ppid.
|
|
989
|
+
*/
|
|
990
|
+
extractSessionId(input) {
|
|
991
|
+
if (input.session_id)
|
|
992
|
+
return input.session_id;
|
|
993
|
+
return `pid-${process.ppid}`;
|
|
994
|
+
}
|
|
995
|
+
}
|