@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,379 @@
|
|
|
1
|
+
// cache-heal-utils.mjs — fixes Brew-node-upgrade stale path bug
|
|
2
|
+
//
|
|
3
|
+
// Problem: start.mjs writes process.execPath into ~/.claude/settings.json
|
|
4
|
+
// when registering the cache-heal hook. On Brew, process.execPath returns
|
|
5
|
+
// the *versioned* Cellar snapshot:
|
|
6
|
+
//
|
|
7
|
+
// /opt/homebrew/Cellar/node/25.9.0_2/bin/node
|
|
8
|
+
//
|
|
9
|
+
// When Brew upgrades Node, that path disappears and Claude fails to spawn
|
|
10
|
+
// the hook ("session start" error). The stable symlink is:
|
|
11
|
+
//
|
|
12
|
+
// /opt/homebrew/bin/node
|
|
13
|
+
//
|
|
14
|
+
// Fix is two layered:
|
|
15
|
+
// A) New installs on Unix: write hook script with `#!/usr/bin/env node`
|
|
16
|
+
// shebang + chmod +x, register hook command as the bare script path.
|
|
17
|
+
// `env` resolves node from PATH at runtime — survives any Node upgrade.
|
|
18
|
+
// Windows keeps the explicit-execPath form (no shebang support).
|
|
19
|
+
// B) Self-heal: every MCP boot, scan ~/.claude/settings.json for an
|
|
20
|
+
// existing cache-heal hook command whose leading node path no longer
|
|
21
|
+
// exists. If stale, rewrite using pattern (A).
|
|
22
|
+
//
|
|
23
|
+
// This module is pure (no global state) and side-effect free except for
|
|
24
|
+
// the explicit selfHealCacheHealHook() entry point that touches disk.
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
existsSync,
|
|
28
|
+
readFileSync,
|
|
29
|
+
writeFileSync,
|
|
30
|
+
chmodSync,
|
|
31
|
+
statSync,
|
|
32
|
+
readdirSync,
|
|
33
|
+
renameSync,
|
|
34
|
+
unlinkSync,
|
|
35
|
+
} from "node:fs";
|
|
36
|
+
import { join } from "node:path";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Convert any path string to forward slashes (matches normalize-hooks style,
|
|
40
|
+
* keeps round-trips on Windows safe).
|
|
41
|
+
*/
|
|
42
|
+
function fwd(p) {
|
|
43
|
+
return String(p).replace(/\\/g, "/");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extract the leading executable path from a hook command string IF it
|
|
48
|
+
* looks like a node binary. Returns null when the command is shebang-style
|
|
49
|
+
* (bare script path) or when the leading executable isn't node.
|
|
50
|
+
*
|
|
51
|
+
* Accepted shapes:
|
|
52
|
+
* '"/abs/path/to/node" "/abs/path/script.mjs"'
|
|
53
|
+
* '/abs/path/to/node "/abs/path/script.mjs"' (unquoted node)
|
|
54
|
+
*
|
|
55
|
+
* Returns null for:
|
|
56
|
+
* '"/abs/path/script.mjs"' (shebang form)
|
|
57
|
+
* '"/usr/bin/python3" "/abs/path/script.py"' (not node)
|
|
58
|
+
*/
|
|
59
|
+
export function extractNodePath(cmd) {
|
|
60
|
+
if (!cmd || typeof cmd !== "string") return null;
|
|
61
|
+
const trimmed = cmd.trim();
|
|
62
|
+
if (!trimmed) return null;
|
|
63
|
+
|
|
64
|
+
// Match: optional quote, capture path until matching quote or whitespace.
|
|
65
|
+
let leading;
|
|
66
|
+
if (trimmed.startsWith('"')) {
|
|
67
|
+
const end = trimmed.indexOf('"', 1);
|
|
68
|
+
if (end === -1) return null;
|
|
69
|
+
leading = trimmed.slice(1, end);
|
|
70
|
+
} else {
|
|
71
|
+
const end = trimmed.search(/\s/);
|
|
72
|
+
leading = end === -1 ? trimmed : trimmed.slice(0, end);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!leading) return null;
|
|
76
|
+
|
|
77
|
+
// Only treat as a node path if the basename is a node binary.
|
|
78
|
+
// Match: "node", "node.exe" (case-insensitive on Windows-style names).
|
|
79
|
+
const base = leading.split(/[\\/]/).pop() ?? "";
|
|
80
|
+
if (!/^node(\.exe)?$/i.test(base)) return null;
|
|
81
|
+
|
|
82
|
+
return leading;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* True when the hook command's leading node path no longer exists on disk.
|
|
87
|
+
* Returns false for shebang-style commands (no node prefix to validate).
|
|
88
|
+
*/
|
|
89
|
+
export function isStaleNodePath(cmd) {
|
|
90
|
+
const nodePath = extractNodePath(cmd);
|
|
91
|
+
if (!nodePath) return false;
|
|
92
|
+
try {
|
|
93
|
+
return !existsSync(nodePath);
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build a cross-platform hook command for the cache-heal script.
|
|
101
|
+
*
|
|
102
|
+
* On Unix (anything except win32):
|
|
103
|
+
* - Returns just the script path (double-quoted), e.g. '"/path/to/script.mjs"'
|
|
104
|
+
* - Caller MUST ensure the script has `#!/usr/bin/env node` shebang and
|
|
105
|
+
* chmod 0o755.
|
|
106
|
+
* - `env` resolves node from PATH at runtime → survives Brew/asdf/nvm
|
|
107
|
+
* upgrades.
|
|
108
|
+
*
|
|
109
|
+
* On Windows:
|
|
110
|
+
* - Returns '"<nodePath>" "<scriptPath>"' (forward slashes, both quoted).
|
|
111
|
+
* - Windows has no shebang support; we must invoke node explicitly.
|
|
112
|
+
*/
|
|
113
|
+
export function buildHookCommand({ scriptPath, platform, nodePath }) {
|
|
114
|
+
if (!scriptPath || typeof scriptPath !== "string") {
|
|
115
|
+
throw new TypeError("buildHookCommand: scriptPath is required");
|
|
116
|
+
}
|
|
117
|
+
const safeScript = fwd(scriptPath);
|
|
118
|
+
if (platform === "win32") {
|
|
119
|
+
if (!nodePath || typeof nodePath !== "string") {
|
|
120
|
+
throw new TypeError(
|
|
121
|
+
"buildHookCommand: nodePath is required on win32",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const safeNode = fwd(nodePath);
|
|
125
|
+
return `"${safeNode}" "${safeScript}"`;
|
|
126
|
+
}
|
|
127
|
+
return `"${safeScript}"`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Self-heal step for ~/.claude/settings.json.
|
|
132
|
+
*
|
|
133
|
+
* - Looks at SessionStart hooks for any registered cache-heal hook.
|
|
134
|
+
* - If its command has a stale node path (Brew upgrade scenario),
|
|
135
|
+
* rewrites the command using buildHookCommand() — Unix gets shebang
|
|
136
|
+
* form, Windows gets explicit nodePath form.
|
|
137
|
+
* - No-op when:
|
|
138
|
+
* * settings.json doesn't exist
|
|
139
|
+
* * no cache-heal hook is registered
|
|
140
|
+
* * the hook command is already valid (path exists or shebang form)
|
|
141
|
+
* - On Unix, also re-asserts the script's shebang + chmod +x so a healed
|
|
142
|
+
* command actually works.
|
|
143
|
+
*
|
|
144
|
+
* Returns: one of "noop" | "healed" | "missing-settings" — useful for
|
|
145
|
+
* tests and telemetry.
|
|
146
|
+
*
|
|
147
|
+
* Best-effort — all I/O is wrapped; never throws.
|
|
148
|
+
*/
|
|
149
|
+
export function selfHealCacheHealHook({
|
|
150
|
+
settingsPath,
|
|
151
|
+
scriptPath,
|
|
152
|
+
platform,
|
|
153
|
+
nodePath,
|
|
154
|
+
}) {
|
|
155
|
+
if (!settingsPath || !existsSync(settingsPath)) return "missing-settings";
|
|
156
|
+
|
|
157
|
+
let raw;
|
|
158
|
+
try {
|
|
159
|
+
raw = readFileSync(settingsPath, "utf-8");
|
|
160
|
+
} catch {
|
|
161
|
+
return "noop";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse(raw);
|
|
167
|
+
} catch {
|
|
168
|
+
return "noop";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const hooks = parsed?.hooks;
|
|
172
|
+
if (!hooks || typeof hooks !== "object") return "noop";
|
|
173
|
+
const sessionStart = Array.isArray(hooks.SessionStart)
|
|
174
|
+
? hooks.SessionStart
|
|
175
|
+
: null;
|
|
176
|
+
if (!sessionStart) return "noop";
|
|
177
|
+
|
|
178
|
+
let healed = false;
|
|
179
|
+
for (const matcher of sessionStart) {
|
|
180
|
+
const inner = matcher?.hooks;
|
|
181
|
+
if (!Array.isArray(inner)) continue;
|
|
182
|
+
for (const h of inner) {
|
|
183
|
+
if (typeof h?.command !== "string") continue;
|
|
184
|
+
if (!h.command.includes("context-mode-cache-heal")) continue;
|
|
185
|
+
if (!isStaleNodePath(h.command)) continue;
|
|
186
|
+
|
|
187
|
+
// Stale → rewrite.
|
|
188
|
+
h.command = buildHookCommand({ scriptPath, platform, nodePath });
|
|
189
|
+
healed = true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!healed) return "noop";
|
|
194
|
+
|
|
195
|
+
// Unix: re-assert shebang + chmod so the bare-script command works.
|
|
196
|
+
if (platform !== "win32" && scriptPath && existsSync(scriptPath)) {
|
|
197
|
+
try {
|
|
198
|
+
ensureShebangAndExecBit(scriptPath);
|
|
199
|
+
} catch {
|
|
200
|
+
/* best effort */
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
writeFileSync(
|
|
206
|
+
settingsPath,
|
|
207
|
+
JSON.stringify(parsed, null, 2) + "\n",
|
|
208
|
+
"utf-8",
|
|
209
|
+
);
|
|
210
|
+
} catch {
|
|
211
|
+
return "noop";
|
|
212
|
+
}
|
|
213
|
+
return "healed";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Issue #710 — heal Claude Code's per-session shell snapshots.
|
|
218
|
+
*
|
|
219
|
+
* Claude Code writes a per-session snapshot at boot:
|
|
220
|
+
* ~/.claude/shell-snapshots/snapshot-<shell>-<ts>-<rand>.sh
|
|
221
|
+
* Every Bash tool call `source`s that snapshot to reproduce the user env
|
|
222
|
+
* (refs/platforms/claude-code/src/utils/bash/ShellSnapshot.ts:269-336;
|
|
223
|
+
* sourced before every Bash tool call at bashProvider.ts:166). The snapshot
|
|
224
|
+
* bakes an `export PATH='…'` line containing the active context-mode
|
|
225
|
+
* `bin/` for the then-current cache version, e.g.
|
|
226
|
+
* …/.claude/plugins/cache/context-mode/context-mode/1.0.146/bin
|
|
227
|
+
*
|
|
228
|
+
* /ctx-upgrade installs the new version and deletes the old cache dir
|
|
229
|
+
* mid-session, but it never touches the snapshot — so every subsequent
|
|
230
|
+
* Bash tool call fails with "Plugin directory does not exist: …/1.0.146"
|
|
231
|
+
* until the session restarts.
|
|
232
|
+
*
|
|
233
|
+
* This helper rewrites the version segment of every context-mode PATH
|
|
234
|
+
* entry in every snapshot under `snapshotsDir` to `currentVersion`.
|
|
235
|
+
* Anchored on the doubled `context-mode/context-mode/` segment so sibling
|
|
236
|
+
* plugins (`pm-skills/pm-toolkit`, `claude-adhd/claude-adhd`, …) and
|
|
237
|
+
* shape-spoofing entries (`evil-owner/context-mode/1.0.146`) are
|
|
238
|
+
* untouched.
|
|
239
|
+
*
|
|
240
|
+
* Layered like cache-heal-utils' brew-node fix:
|
|
241
|
+
* Layer 1 — /ctx-upgrade calls this after install (cli.ts) so the
|
|
242
|
+
* session that just upgraded sees the new bin on the next
|
|
243
|
+
* Bash call.
|
|
244
|
+
* Layer 2 — SessionStart hook calls this on every boot so a session
|
|
245
|
+
* that started before /ctx-upgrade ran still self-heals.
|
|
246
|
+
*
|
|
247
|
+
* Write contract:
|
|
248
|
+
* - Atomic: write to `<file>.tmp-<pid>-<ts>` then rename. Snapshots
|
|
249
|
+
* are `source`d concurrently; a half-written file would crash the
|
|
250
|
+
* bash subprocess mid-call.
|
|
251
|
+
* - Idempotent: a snapshot already on `currentVersion` is not
|
|
252
|
+
* re-written (mtime preserved). A snapshot with no context-mode
|
|
253
|
+
* entry is not re-written.
|
|
254
|
+
* - Best-effort: every I/O is wrapped; never throws. Telemetry shape
|
|
255
|
+
* is `{ rewritten: string[] }` for caller logging.
|
|
256
|
+
* - Cross-platform: handles both unix (`/Users/x/.claude/…`),
|
|
257
|
+
* Cygwin/Git Bash (`/c/Users/x/.claude/…`), and Windows native
|
|
258
|
+
* (`C:\Users\x\.claude\…`) path variants. ShellSnapshot.ts
|
|
259
|
+
* writes paths using whatever shell wrote them, so all three
|
|
260
|
+
* shapes can appear depending on the user's shell environment.
|
|
261
|
+
*/
|
|
262
|
+
export function rewriteShellSnapshots({ snapshotsDir, currentVersion }) {
|
|
263
|
+
const out = { rewritten: [] };
|
|
264
|
+
if (
|
|
265
|
+
!snapshotsDir ||
|
|
266
|
+
typeof snapshotsDir !== "string" ||
|
|
267
|
+
!currentVersion ||
|
|
268
|
+
typeof currentVersion !== "string"
|
|
269
|
+
) {
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
let entries;
|
|
273
|
+
try {
|
|
274
|
+
if (!existsSync(snapshotsDir)) return out;
|
|
275
|
+
entries = readdirSync(snapshotsDir);
|
|
276
|
+
} catch {
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Match the version segment of any PATH entry of the form
|
|
281
|
+
// …/plugins/cache/context-mode/context-mode/<VERSION>/bin
|
|
282
|
+
// across all three path shapes (`/`, `\`, mixed). The doubled
|
|
283
|
+
// `context-mode/context-mode/` is the trust anchor — it prevents
|
|
284
|
+
// shape-spoofing from another owner.
|
|
285
|
+
//
|
|
286
|
+
// Captures:
|
|
287
|
+
// $1 — separator-tolerant prefix up to and including the second
|
|
288
|
+
// `context-mode` segment + its trailing separator
|
|
289
|
+
// $2 — version segment (no separators)
|
|
290
|
+
// $3 — trailing separator + `bin`
|
|
291
|
+
const versionSegmentRe =
|
|
292
|
+
/(context-mode[/\\]context-mode[/\\])([^/\\]+)([/\\]bin)/g;
|
|
293
|
+
|
|
294
|
+
for (const name of entries) {
|
|
295
|
+
if (!name.endsWith(".sh")) continue;
|
|
296
|
+
const file = join(snapshotsDir, name);
|
|
297
|
+
let content;
|
|
298
|
+
try {
|
|
299
|
+
const st = statSync(file);
|
|
300
|
+
if (!st.isFile()) continue;
|
|
301
|
+
content = readFileSync(file, "utf-8");
|
|
302
|
+
} catch {
|
|
303
|
+
// Binary, unreadable, or vanished — skip.
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let touched = false;
|
|
308
|
+
const next = content.replace(
|
|
309
|
+
versionSegmentRe,
|
|
310
|
+
(whole, prefix, version, suffix) => {
|
|
311
|
+
if (version === currentVersion) return whole;
|
|
312
|
+
touched = true;
|
|
313
|
+
return `${prefix}${currentVersion}${suffix}`;
|
|
314
|
+
},
|
|
315
|
+
);
|
|
316
|
+
if (!touched) continue;
|
|
317
|
+
|
|
318
|
+
// Atomic rename — never write directly to `file` because the
|
|
319
|
+
// snapshot may be sourced by a concurrent Bash subprocess.
|
|
320
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
321
|
+
try {
|
|
322
|
+
writeFileSync(tmp, next, "utf-8");
|
|
323
|
+
renameSync(tmp, file);
|
|
324
|
+
out.rewritten.push(file);
|
|
325
|
+
} catch {
|
|
326
|
+
// Best-effort cleanup of the tmp file; never throw.
|
|
327
|
+
try {
|
|
328
|
+
unlinkSync(tmp);
|
|
329
|
+
} catch {
|
|
330
|
+
/* best effort */
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Issue #710 Layer 2 — self-heal entry point for SessionStart.
|
|
340
|
+
*
|
|
341
|
+
* Resolves the snapshots directory + current version from the live
|
|
342
|
+
* environment (or accepts explicit overrides for tests) and delegates to
|
|
343
|
+
* `rewriteShellSnapshots`. Wrap-and-swallow; never throws.
|
|
344
|
+
*
|
|
345
|
+
* `pluginCacheRoot` is accepted to match the cache-heal-utils precedent
|
|
346
|
+
* surface but not yet used (the version segment alone is sufficient for
|
|
347
|
+
* the regex match — we don't need to walk the cache to know the right
|
|
348
|
+
* answer; the cli passes the version it just installed). Kept in the
|
|
349
|
+
* shape for forward-compat if a future heal pass needs to cross-check
|
|
350
|
+
* the on-disk symlink target.
|
|
351
|
+
*/
|
|
352
|
+
export function selfHealShellSnapshots({
|
|
353
|
+
snapshotsDir,
|
|
354
|
+
pluginCacheRoot: _pluginCacheRoot,
|
|
355
|
+
currentVersion,
|
|
356
|
+
}) {
|
|
357
|
+
return rewriteShellSnapshots({ snapshotsDir, currentVersion });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Ensure a script starts with `#!/usr/bin/env node` and has 0o755 mode.
|
|
362
|
+
* Idempotent — leaves correctly-shebanged scripts unchanged.
|
|
363
|
+
*/
|
|
364
|
+
export function ensureShebangAndExecBit(scriptPath) {
|
|
365
|
+
if (!scriptPath || !existsSync(scriptPath)) return;
|
|
366
|
+
try {
|
|
367
|
+
const content = readFileSync(scriptPath, "utf-8");
|
|
368
|
+
if (!content.startsWith("#!")) {
|
|
369
|
+
writeFileSync(scriptPath, `#!/usr/bin/env node\n${content}`, "utf-8");
|
|
370
|
+
}
|
|
371
|
+
// statSync().mode lower 9 bits = perms.
|
|
372
|
+
const mode = statSync(scriptPath).mode & 0o777;
|
|
373
|
+
if (mode !== 0o755) {
|
|
374
|
+
chmodSync(scriptPath, 0o755);
|
|
375
|
+
}
|
|
376
|
+
} catch {
|
|
377
|
+
/* best effort */
|
|
378
|
+
}
|
|
379
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
process.env.CONTEXT_MODE_PLATFORM = "codex";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./platform.mjs";
|
|
3
|
+
import "../suppress-stderr.mjs";
|
|
4
|
+
import "../ensure-deps.mjs";
|
|
5
|
+
/**
|
|
6
|
+
* Codex CLI postToolUse hook — session event capture.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir, CODEX_OPTS } from "../session-helpers.mjs";
|
|
10
|
+
import { createSessionLoaders, attributeAndInsertEvents } from "../session-loaders.mjs";
|
|
11
|
+
import { dirname } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
|
|
16
|
+
const OPTS = CODEX_OPTS;
|
|
17
|
+
|
|
18
|
+
function normalizeToolName(toolName) {
|
|
19
|
+
// Keep Codex-native tool names like apply_patch intact; only normalize
|
|
20
|
+
// legacy shell aliases that should route through the Bash extractors.
|
|
21
|
+
if (toolName === "Shell") return "Bash";
|
|
22
|
+
return toolName;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const raw = await readStdin();
|
|
27
|
+
const input = parseStdin(raw);
|
|
28
|
+
const projectDir = getInputProjectDir(input, OPTS);
|
|
29
|
+
|
|
30
|
+
const { extractEvents } = await loadExtract();
|
|
31
|
+
const { resolveProjectAttributions } = await loadProjectAttribution();
|
|
32
|
+
const { SessionDB } = await loadSessionDB();
|
|
33
|
+
|
|
34
|
+
const dbPath = getSessionDBPath(OPTS, projectDir);
|
|
35
|
+
const db = new SessionDB({ dbPath });
|
|
36
|
+
const sessionId = getSessionId(input, OPTS);
|
|
37
|
+
|
|
38
|
+
db.ensureSession(sessionId, projectDir);
|
|
39
|
+
|
|
40
|
+
const normalizedInput = {
|
|
41
|
+
tool_name: normalizeToolName(input.tool_name ?? ""),
|
|
42
|
+
tool_input: input.tool_input ?? {},
|
|
43
|
+
tool_response: typeof input.tool_response === "string"
|
|
44
|
+
? input.tool_response
|
|
45
|
+
: JSON.stringify(input.tool_response ?? ""),
|
|
46
|
+
tool_output: input.tool_output
|
|
47
|
+
? {
|
|
48
|
+
...input.tool_output,
|
|
49
|
+
isError: input.tool_output.isError === true || input.tool_output.is_error === true,
|
|
50
|
+
}
|
|
51
|
+
: undefined,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const events = extractEvents(normalizedInput);
|
|
55
|
+
|
|
56
|
+
attributeAndInsertEvents(db, sessionId, events, input, projectDir, "PostToolUse", resolveProjectAttributions);
|
|
57
|
+
|
|
58
|
+
db.close();
|
|
59
|
+
} catch {
|
|
60
|
+
// Swallow errors — hook must not fail
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Codex PostToolUse requires hookEventName in hookSpecificOutput
|
|
64
|
+
process.stdout.write(JSON.stringify({
|
|
65
|
+
hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: "" },
|
|
66
|
+
}) + "\n");
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./platform.mjs";
|
|
3
|
+
import "../suppress-stderr.mjs";
|
|
4
|
+
import "../ensure-deps.mjs";
|
|
5
|
+
/**
|
|
6
|
+
* Codex CLI PreCompact hook - snapshot generation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
readStdin,
|
|
11
|
+
parseStdin,
|
|
12
|
+
getSessionId,
|
|
13
|
+
getSessionDBPath,
|
|
14
|
+
getInputProjectDir,
|
|
15
|
+
resolveConfigDir,
|
|
16
|
+
CODEX_OPTS,
|
|
17
|
+
} from "../session-helpers.mjs";
|
|
18
|
+
import { createSessionLoaders } from "../session-loaders.mjs";
|
|
19
|
+
import { appendFileSync } from "node:fs";
|
|
20
|
+
import { join, dirname } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const { loadSessionDB, loadSnapshot } = createSessionLoaders(HOOK_DIR);
|
|
25
|
+
const OPTS = CODEX_OPTS;
|
|
26
|
+
const DEBUG_LOG = join(resolveConfigDir(OPTS), "context-mode", "precompact-debug.log");
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const raw = await readStdin();
|
|
30
|
+
const input = parseStdin(raw);
|
|
31
|
+
const projectDir = getInputProjectDir(input, OPTS);
|
|
32
|
+
|
|
33
|
+
const { buildResumeSnapshot } = await loadSnapshot();
|
|
34
|
+
const { SessionDB } = await loadSessionDB();
|
|
35
|
+
|
|
36
|
+
const dbPath = getSessionDBPath(OPTS, projectDir);
|
|
37
|
+
const db = new SessionDB({ dbPath });
|
|
38
|
+
const sessionId = getSessionId(input, OPTS);
|
|
39
|
+
|
|
40
|
+
const events = db.getEvents(sessionId);
|
|
41
|
+
|
|
42
|
+
if (events.length > 0) {
|
|
43
|
+
const stats = db.getSessionStats(sessionId);
|
|
44
|
+
const snapshot = buildResumeSnapshot(events, {
|
|
45
|
+
compactCount: (stats?.compact_count ?? 0) + 1,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
db.upsertResume(sessionId, snapshot, events.length);
|
|
49
|
+
db.incrementCompactCount(sessionId);
|
|
50
|
+
|
|
51
|
+
const fileEvents = events.filter((event) => event.category === "file");
|
|
52
|
+
db.insertEvent(sessionId, {
|
|
53
|
+
type: "compaction_summary",
|
|
54
|
+
category: "compaction",
|
|
55
|
+
data: `Session compacted. ${events.length} events, ${fileEvents.length} files touched.`,
|
|
56
|
+
priority: 1,
|
|
57
|
+
}, "PreCompact");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
db.close();
|
|
61
|
+
} catch (err) {
|
|
62
|
+
try {
|
|
63
|
+
appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] ${err?.message || err}\n`);
|
|
64
|
+
} catch {
|
|
65
|
+
// Hook errors must not break Codex compaction.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Codex PreCompact accepts universal hook fields only; no hookSpecificOutput.
|
|
70
|
+
process.stdout.write(JSON.stringify({}) + "\n");
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./platform.mjs";
|
|
3
|
+
import "../suppress-stderr.mjs";
|
|
4
|
+
/**
|
|
5
|
+
* Codex CLI preToolUse hook for context-mode.
|
|
6
|
+
*
|
|
7
|
+
* Codex PreToolUse honors `permissionDecision:"deny"` on all builds, and
|
|
8
|
+
* `permissionDecision:"allow" + updatedInput` / `additionalContext` on
|
|
9
|
+
* codex-cli >= 0.141.0 (#845). Capability is detected at runtime by
|
|
10
|
+
* codex-caps.mjs; older builds fail closed (redirect → deny). `ask` is still
|
|
11
|
+
* unsupported. Source: codex-rs/hooks/src/engine/output_parser.rs
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { dirname, resolve } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { readStdin, parseStdin, getInputProjectDir, getSessionId, CODEX_OPTS } from "../session-helpers.mjs";
|
|
17
|
+
import { routePreToolUse, initSecurity } from "../core/routing.mjs";
|
|
18
|
+
import { formatDecision } from "../core/formatters.mjs";
|
|
19
|
+
import { codexSupportsUpdatedInput } from "../core/codex-caps.mjs";
|
|
20
|
+
|
|
21
|
+
const __hookDir = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
await initSecurity(resolve(__hookDir, "..", "..", "build"));
|
|
23
|
+
|
|
24
|
+
const raw = await readStdin();
|
|
25
|
+
const input = parseStdin(raw);
|
|
26
|
+
const tool = input.tool_name ?? "";
|
|
27
|
+
const toolInput = input.tool_input ?? {};
|
|
28
|
+
const projectDir = getInputProjectDir(input, CODEX_OPTS);
|
|
29
|
+
|
|
30
|
+
const decision = routePreToolUse(tool, toolInput, projectDir, "codex", getSessionId(input, CODEX_OPTS));
|
|
31
|
+
// #845: only modify/context depend on Codex's rewrite capability. Detection is
|
|
32
|
+
// cached, but skip the probe entirely for deny / ask / passthrough decisions.
|
|
33
|
+
const needsCaps = decision && (decision.action === "modify" || decision.action === "context");
|
|
34
|
+
const response = formatDecision(
|
|
35
|
+
"codex",
|
|
36
|
+
decision,
|
|
37
|
+
needsCaps ? { codexSupportsRewrite: codexSupportsUpdatedInput() } : {},
|
|
38
|
+
);
|
|
39
|
+
const output = response ?? {
|
|
40
|
+
hookSpecificOutput: { hookEventName: "PreToolUse" },
|
|
41
|
+
};
|
|
42
|
+
process.stdout.write(JSON.stringify(output) + "\n");
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./platform.mjs";
|
|
3
|
+
import "../suppress-stderr.mjs";
|
|
4
|
+
import "../ensure-deps.mjs";
|
|
5
|
+
/**
|
|
6
|
+
* Codex CLI sessionStart hook for context-mode.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createRoutingBlock } from "../routing-block.mjs";
|
|
10
|
+
import { createToolNamer } from "../core/tool-naming.mjs";
|
|
11
|
+
|
|
12
|
+
const toolNamer = createToolNamer("codex");
|
|
13
|
+
const ROUTING_BLOCK = createRoutingBlock(toolNamer);
|
|
14
|
+
import {
|
|
15
|
+
writeSessionEventsFile,
|
|
16
|
+
buildSessionDirective,
|
|
17
|
+
getSessionEvents,
|
|
18
|
+
} from "../session-directive.mjs";
|
|
19
|
+
import {
|
|
20
|
+
readStdin,
|
|
21
|
+
parseStdin,
|
|
22
|
+
getSessionId,
|
|
23
|
+
getSessionDBPath,
|
|
24
|
+
getSessionEventsPath,
|
|
25
|
+
getCleanupFlagPath,
|
|
26
|
+
getInputProjectDir,
|
|
27
|
+
resolveConfigDir,
|
|
28
|
+
CODEX_OPTS,
|
|
29
|
+
} from "../session-helpers.mjs";
|
|
30
|
+
import { createSessionLoaders } from "../session-loaders.mjs";
|
|
31
|
+
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
import { fileURLToPath } from "node:url";
|
|
34
|
+
|
|
35
|
+
const HOOK_DIR = fileURLToPath(new URL(".", import.meta.url));
|
|
36
|
+
const { loadSessionDB } = createSessionLoaders(HOOK_DIR);
|
|
37
|
+
const OPTS = CODEX_OPTS;
|
|
38
|
+
|
|
39
|
+
let additionalContext = ROUTING_BLOCK;
|
|
40
|
+
|
|
41
|
+
function captureCodexInstructionRules(db, sessionId, projectDir) {
|
|
42
|
+
const paths = [];
|
|
43
|
+
for (const baseDir of [resolveConfigDir(OPTS), projectDir]) {
|
|
44
|
+
paths.push(join(baseDir, "AGENTS.md"));
|
|
45
|
+
paths.push(join(baseDir, "AGENTS.override.md"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const p of [...new Set(paths)]) {
|
|
49
|
+
try {
|
|
50
|
+
if (!existsSync(p)) continue;
|
|
51
|
+
const content = readFileSync(p, "utf8");
|
|
52
|
+
db.insertEvent(sessionId, { type: "rule", category: "rule", data: p, priority: 1 });
|
|
53
|
+
db.insertEvent(sessionId, { type: "rule_content", category: "rule", data: content, priority: 1 });
|
|
54
|
+
} catch {
|
|
55
|
+
// Missing or unreadable rule files should never break SessionStart.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const raw = await readStdin();
|
|
62
|
+
const input = parseStdin(raw);
|
|
63
|
+
const source = input.source ?? "startup";
|
|
64
|
+
const projectDir = getInputProjectDir(input, CODEX_OPTS);
|
|
65
|
+
|
|
66
|
+
if (source === "compact" || source === "resume") {
|
|
67
|
+
const { SessionDB } = await loadSessionDB();
|
|
68
|
+
const dbPath = getSessionDBPath(OPTS, projectDir);
|
|
69
|
+
const db = new SessionDB({ dbPath });
|
|
70
|
+
const sessionId = getSessionId(input, OPTS);
|
|
71
|
+
let resumeSnapshot = null;
|
|
72
|
+
|
|
73
|
+
if (source === "compact") {
|
|
74
|
+
const resume = sessionId ? db.getResume(sessionId) : null;
|
|
75
|
+
if (resume && !resume.consumed) {
|
|
76
|
+
resumeSnapshot = resume.snapshot;
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
try { unlinkSync(getCleanupFlagPath(OPTS, projectDir)); } catch { /* no flag */ }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Filter events to the session being resumed/compacted. Falling back to
|
|
83
|
+
// getLatestSessionEvents(db) for resume leaks events from any other
|
|
84
|
+
// session whose session_meta.started_at is more recent — observed
|
|
85
|
+
// cross-session bleed when a different session started after this one
|
|
86
|
+
// and before the resume.
|
|
87
|
+
const events = sessionId ? getSessionEvents(db, sessionId) : [];
|
|
88
|
+
if (events.length > 0) {
|
|
89
|
+
const eventMeta = writeSessionEventsFile(events, getSessionEventsPath(OPTS, projectDir));
|
|
90
|
+
additionalContext += buildSessionDirective(source, eventMeta, toolNamer);
|
|
91
|
+
}
|
|
92
|
+
if (resumeSnapshot) {
|
|
93
|
+
additionalContext += `\n\n${resumeSnapshot}`;
|
|
94
|
+
db.markResumeConsumed(sessionId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
db.close();
|
|
98
|
+
} else if (source === "startup") {
|
|
99
|
+
const { SessionDB } = await loadSessionDB();
|
|
100
|
+
const dbPath = getSessionDBPath(OPTS, projectDir);
|
|
101
|
+
const db = new SessionDB({ dbPath });
|
|
102
|
+
try { unlinkSync(getSessionEventsPath(OPTS, projectDir)); } catch { /* no stale file */ }
|
|
103
|
+
|
|
104
|
+
db.cleanupOldSessions(7);
|
|
105
|
+
db.db.exec(`DELETE FROM session_events WHERE session_id NOT IN (SELECT session_id FROM session_meta)`);
|
|
106
|
+
|
|
107
|
+
const sessionId = getSessionId(input, OPTS);
|
|
108
|
+
db.ensureSession(sessionId, projectDir);
|
|
109
|
+
captureCodexInstructionRules(db, sessionId, projectDir);
|
|
110
|
+
|
|
111
|
+
db.close();
|
|
112
|
+
}
|
|
113
|
+
// clear => routing block only
|
|
114
|
+
} catch {
|
|
115
|
+
// Swallow errors — hook must not fail
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Codex SessionStart requires hookEventName in hookSpecificOutput
|
|
119
|
+
process.stdout.write(JSON.stringify({
|
|
120
|
+
hookSpecificOutput: { hookEventName: "SessionStart", additionalContext },
|
|
121
|
+
}) + "\n");
|