@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,569 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapters/claude-code — Claude Code platform adapter.
|
|
3
|
+
*
|
|
4
|
+
* Extends ClaudeCodeBaseAdapter (shared wire-protocol parse/format methods)
|
|
5
|
+
* with Claude Code-specific configuration, diagnostics, and upgrade logic.
|
|
6
|
+
*
|
|
7
|
+
* Claude Code hook specifics:
|
|
8
|
+
* - Session ID: transcript_path UUID > session_id > CLAUDE_SESSION_ID > ppid
|
|
9
|
+
* - Config root: $CLAUDE_CONFIG_DIR (when set) or ~/.claude
|
|
10
|
+
* - Settings: <configDir>/settings.json
|
|
11
|
+
* - Session dir: <configDir>/context-mode/sessions/
|
|
12
|
+
* - Plugin registry: <configDir>/plugins/installed_plugins.json
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, chmodSync, accessSync, mkdirSync, constants, } from "node:fs";
|
|
15
|
+
import { resolve, join } from "node:path";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { ClaudeCodeBaseAdapter } from "../claude-code-base.js";
|
|
18
|
+
import { resolveContextModeDataRoot } from "../base.js";
|
|
19
|
+
import { resolveClaudeConfigDir } from "../../util/claude-config.js";
|
|
20
|
+
import { checkPluginCacheIntegritySync } from "../../util/plugin-cache-integrity.js";
|
|
21
|
+
import { buildHookRuntimeCommand, } from "../types.js";
|
|
22
|
+
import { HOOK_TYPES, HOOK_SCRIPTS, REQUIRED_HOOKS, PRE_TOOL_USE_MATCHERS, PRE_TOOL_USE_MATCHER_PATTERN, isContextModeHook, isAnyContextModeHook, extractHookScriptPath, buildHookCommand, } from "./hooks.js";
|
|
23
|
+
// ─────────────────────────────────────────────────────────
|
|
24
|
+
// Adapter implementation
|
|
25
|
+
// ─────────────────────────────────────────────────────────
|
|
26
|
+
export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
27
|
+
constructor() {
|
|
28
|
+
super([".claude"]);
|
|
29
|
+
}
|
|
30
|
+
name = "Claude Code";
|
|
31
|
+
paradigm = "json-stdio";
|
|
32
|
+
projectDirEnvVar = "CLAUDE_PROJECT_DIR";
|
|
33
|
+
capabilities = {
|
|
34
|
+
preToolUse: true,
|
|
35
|
+
postToolUse: true,
|
|
36
|
+
preCompact: true,
|
|
37
|
+
sessionStart: true,
|
|
38
|
+
canModifyArgs: true,
|
|
39
|
+
canModifyOutput: true,
|
|
40
|
+
canInjectSessionContext: true,
|
|
41
|
+
};
|
|
42
|
+
// ── Configuration ──────────────────────────────────────
|
|
43
|
+
/**
|
|
44
|
+
* Honor `CLAUDE_CONFIG_DIR` (the canonical Claude Code config root) before
|
|
45
|
+
* falling back to `~/.claude`. Mirrors the contract that
|
|
46
|
+
* `hooks/session-helpers.mjs::resolveConfigDir` already follows — including
|
|
47
|
+
* tilde expansion for shells that pass `~/foo` through unchanged — so server
|
|
48
|
+
* and hooks agree on where session-scoped state lives. See issue #453.
|
|
49
|
+
*
|
|
50
|
+
* Tilde regex `/^~[/\\]?/` only handles the current-user form (`~`, `~/`,
|
|
51
|
+
* `~\`); `~user/` is NOT expanded to a per-user homedir (matches
|
|
52
|
+
* `resolveConfigDir`). Non-tilde values are run through `resolve()` to
|
|
53
|
+
* normalize relative paths to absolute against cwd; the hook helper
|
|
54
|
+
* intentionally leaves them raw, but the adapter contract guarantees an
|
|
55
|
+
* absolute path (BaseAdapter.getConfigDir docstring).
|
|
56
|
+
*
|
|
57
|
+
* Issue #460 round-3: routed through the canonical
|
|
58
|
+
* `resolveClaudeConfigDir` util so server, CLI, security, and adapter
|
|
59
|
+
* agree byte-for-byte (incl. empty/whitespace-only env fallback).
|
|
60
|
+
*/
|
|
61
|
+
getConfigDir(_projectDir) {
|
|
62
|
+
return resolveClaudeConfigDir();
|
|
63
|
+
}
|
|
64
|
+
getSessionDir() {
|
|
65
|
+
// Issue #649: honor CONTEXT_MODE_DATA_DIR universal storage override
|
|
66
|
+
// before falling back to the Claude-rooted default. The override moves
|
|
67
|
+
// ONLY context-mode-owned state; settings.json + CLAUDE_CONFIG_DIR stay
|
|
68
|
+
// intact below.
|
|
69
|
+
const override = resolveContextModeDataRoot();
|
|
70
|
+
const dir = override
|
|
71
|
+
? join(override, "context-mode", "sessions")
|
|
72
|
+
: join(this.getConfigDir(), "context-mode", "sessions");
|
|
73
|
+
mkdirSync(dir, { recursive: true });
|
|
74
|
+
return dir;
|
|
75
|
+
}
|
|
76
|
+
getSettingsPath() {
|
|
77
|
+
return join(this.getConfigDir(), "settings.json");
|
|
78
|
+
}
|
|
79
|
+
generateHookConfig(pluginRoot) {
|
|
80
|
+
// Algo-D3: every command flows through `buildHookRuntimeCommand`
|
|
81
|
+
// (defined in src/adapters/types.ts), which:
|
|
82
|
+
// - quotes both runtime path and scriptPath (#548 — Windows
|
|
83
|
+
// pluginRoots with spaces no longer fall through
|
|
84
|
+
// extractHookScriptPath's ambiguous-tail fallback),
|
|
85
|
+
// - swaps backslashes for forward slashes (#372 MSYS path mangling),
|
|
86
|
+
// - resolves the JS runtime via `resolveHookRuntime`: Bun ≥1.0 when
|
|
87
|
+
// available, else `process.execPath` (#369 PATH resolution on Git
|
|
88
|
+
// Bash, #738 bun cold-start win).
|
|
89
|
+
// Pre-D3 we hand-rolled `node "${pluginRoot}/hooks/X.mjs"` for all
|
|
90
|
+
// six events; bare `node` made claude-code the lone outlier and
|
|
91
|
+
// dropping the execPath swap re-opened the Windows class. Algo-D3.5
|
|
92
|
+
// (CI invariant in tests/adapters/claude-code.test.ts) locks this in
|
|
93
|
+
// for adapter #16.
|
|
94
|
+
const preToolUseCommand = buildHookRuntimeCommand(`${pluginRoot}/hooks/pretooluse.mjs`);
|
|
95
|
+
const preToolUseMatchers = [...PRE_TOOL_USE_MATCHERS];
|
|
96
|
+
return {
|
|
97
|
+
PreToolUse: preToolUseMatchers.map((matcher) => ({
|
|
98
|
+
matcher,
|
|
99
|
+
hooks: [{ type: "command", command: preToolUseCommand }],
|
|
100
|
+
})),
|
|
101
|
+
PostToolUse: [
|
|
102
|
+
{
|
|
103
|
+
matcher: "",
|
|
104
|
+
hooks: [
|
|
105
|
+
{
|
|
106
|
+
type: "command",
|
|
107
|
+
command: buildHookRuntimeCommand(`${pluginRoot}/hooks/posttooluse.mjs`),
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
PreCompact: [
|
|
113
|
+
{
|
|
114
|
+
matcher: "",
|
|
115
|
+
hooks: [
|
|
116
|
+
{
|
|
117
|
+
type: "command",
|
|
118
|
+
command: buildHookRuntimeCommand(`${pluginRoot}/hooks/precompact.mjs`),
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
UserPromptSubmit: [
|
|
124
|
+
{
|
|
125
|
+
matcher: "",
|
|
126
|
+
hooks: [
|
|
127
|
+
{
|
|
128
|
+
type: "command",
|
|
129
|
+
command: buildHookRuntimeCommand(`${pluginRoot}/hooks/userpromptsubmit.mjs`),
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
SessionStart: [
|
|
135
|
+
{
|
|
136
|
+
matcher: "",
|
|
137
|
+
hooks: [
|
|
138
|
+
{
|
|
139
|
+
type: "command",
|
|
140
|
+
command: buildHookRuntimeCommand(`${pluginRoot}/hooks/sessionstart.mjs`),
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
Stop: [
|
|
146
|
+
{
|
|
147
|
+
matcher: "",
|
|
148
|
+
hooks: [
|
|
149
|
+
{
|
|
150
|
+
type: "command",
|
|
151
|
+
command: buildHookRuntimeCommand(`${pluginRoot}/hooks/stop.mjs`),
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
readSettings() {
|
|
159
|
+
try {
|
|
160
|
+
const raw = readFileSync(this.getSettingsPath(), "utf-8");
|
|
161
|
+
return JSON.parse(raw);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
writeSettings(settings) {
|
|
168
|
+
writeFileSync(this.getSettingsPath(), JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
169
|
+
}
|
|
170
|
+
// ── Diagnostics (doctor) ─────────────────────────────────
|
|
171
|
+
validateHooks(pluginRoot) {
|
|
172
|
+
const results = [];
|
|
173
|
+
const settings = this.readSettings();
|
|
174
|
+
if (!settings) {
|
|
175
|
+
results.push({
|
|
176
|
+
check: "PreToolUse hook",
|
|
177
|
+
status: "fail",
|
|
178
|
+
message: `Could not read ${this.getSettingsPath()}`,
|
|
179
|
+
fix: "context-mode upgrade",
|
|
180
|
+
});
|
|
181
|
+
return results;
|
|
182
|
+
}
|
|
183
|
+
const hooks = settings.hooks;
|
|
184
|
+
// Read plugin hooks.json as fallback (Issue #94: plugin installs
|
|
185
|
+
// register hooks in hooks/hooks.json, not in settings.json)
|
|
186
|
+
const pluginHooks = this.readPluginHooks(pluginRoot);
|
|
187
|
+
// Check PreToolUse (settings.json first, then plugin hooks.json fallback)
|
|
188
|
+
const hasPreToolUse = this.checkHookType(hooks, pluginHooks, HOOK_TYPES.PRE_TOOL_USE);
|
|
189
|
+
results.push({
|
|
190
|
+
check: "PreToolUse hook",
|
|
191
|
+
status: hasPreToolUse ? "pass" : "fail",
|
|
192
|
+
message: hasPreToolUse
|
|
193
|
+
? "PreToolUse hook configured"
|
|
194
|
+
: "No PreToolUse hooks found",
|
|
195
|
+
fix: hasPreToolUse ? undefined : "context-mode upgrade",
|
|
196
|
+
});
|
|
197
|
+
// Check SessionStart (settings.json first, then plugin hooks.json fallback)
|
|
198
|
+
const hasSessionStart = this.checkHookType(hooks, pluginHooks, HOOK_TYPES.SESSION_START);
|
|
199
|
+
results.push({
|
|
200
|
+
check: "SessionStart hook",
|
|
201
|
+
status: hasSessionStart ? "pass" : "fail",
|
|
202
|
+
message: hasSessionStart
|
|
203
|
+
? "SessionStart hook configured"
|
|
204
|
+
: "No SessionStart hooks found",
|
|
205
|
+
fix: hasSessionStart ? undefined : "context-mode upgrade",
|
|
206
|
+
});
|
|
207
|
+
return results;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Adapter-defined health checks (Algo-D1 + Algo-D5).
|
|
211
|
+
*
|
|
212
|
+
* For each entry in HOOK_SCRIPTS (the canonical hookType → scriptName
|
|
213
|
+
* map), emit a HealthCheck that joins `pluginRoot + "hooks" +
|
|
214
|
+
* scriptName` and probes via `existsSync`. Crucially, this NEVER
|
|
215
|
+
* parses a hook command — pluginRoot and scriptName are both in our
|
|
216
|
+
* hand, so the regex round-trip that produced the #548 doubled-path
|
|
217
|
+
* FAIL is bypassed entirely.
|
|
218
|
+
*
|
|
219
|
+
* The hook check derives from HOOK_SCRIPTS (single source of truth in
|
|
220
|
+
* src/adapters/claude-code/hooks.ts), so adding a new hook event in
|
|
221
|
+
* that map auto-extends doctor coverage — no parallel hardcoded list
|
|
222
|
+
* to maintain.
|
|
223
|
+
*
|
|
224
|
+
* Algo-D5: appends a single "Plugin cache integrity" check that
|
|
225
|
+
* delegates to the same helper start.mjs uses at boot
|
|
226
|
+
* (scripts/plugin-cache-integrity.mjs::assertPluginCacheIntegrity).
|
|
227
|
+
* Same code, two callsites — boot fail-fast and doctor diagnostic
|
|
228
|
+
* agree byte-for-byte. Users hitting #550 get the actionable signal
|
|
229
|
+
* without restarting the MCP server.
|
|
230
|
+
*/
|
|
231
|
+
getHealthChecks(pluginRoot) {
|
|
232
|
+
const hookChecks = Object.entries(HOOK_SCRIPTS).map(([hookType, scriptName]) => {
|
|
233
|
+
const absolutePath = join(pluginRoot, "hooks", scriptName);
|
|
234
|
+
return {
|
|
235
|
+
name: `Hook script: ${hookType} (${scriptName})`,
|
|
236
|
+
check: () => {
|
|
237
|
+
// Direct existsSync — no hook-command parsing, no regex.
|
|
238
|
+
// pluginRoot is the value the doctor was invoked with;
|
|
239
|
+
// scriptName comes from the canonical HOOK_SCRIPTS map.
|
|
240
|
+
if (existsSync(absolutePath)) {
|
|
241
|
+
return { status: "OK", detail: absolutePath };
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
status: "FAIL",
|
|
245
|
+
detail: `not found at ${absolutePath}`,
|
|
246
|
+
};
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
const integrityCheck = {
|
|
251
|
+
name: "Plugin cache integrity",
|
|
252
|
+
check: () => checkPluginCacheIntegritySync(pluginRoot),
|
|
253
|
+
};
|
|
254
|
+
return [...hookChecks, integrityCheck];
|
|
255
|
+
}
|
|
256
|
+
/** Read plugin hooks from hooks/hooks.json or .claude-plugin/hooks/hooks.json */
|
|
257
|
+
readPluginHooks(pluginRoot) {
|
|
258
|
+
const candidates = [
|
|
259
|
+
join(pluginRoot, "hooks", "hooks.json"),
|
|
260
|
+
join(pluginRoot, ".claude-plugin", "hooks", "hooks.json"),
|
|
261
|
+
];
|
|
262
|
+
for (const candidate of candidates) {
|
|
263
|
+
try {
|
|
264
|
+
const raw = readFileSync(candidate, "utf-8");
|
|
265
|
+
const parsed = JSON.parse(raw);
|
|
266
|
+
if (parsed.hooks)
|
|
267
|
+
return parsed.hooks;
|
|
268
|
+
}
|
|
269
|
+
catch { /* not available */ }
|
|
270
|
+
}
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
/** Check if a hook type is configured in either settings.json or plugin hooks */
|
|
274
|
+
checkHookType(settingsHooks, pluginHooks, hookType) {
|
|
275
|
+
// Check settings.json
|
|
276
|
+
const fromSettings = settingsHooks?.[hookType];
|
|
277
|
+
if (fromSettings && fromSettings.length > 0) {
|
|
278
|
+
if (fromSettings.some((entry) => isContextModeHook(entry, hookType))) {
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Fallback: check plugin hooks.json
|
|
283
|
+
const fromPlugin = pluginHooks?.[hookType];
|
|
284
|
+
if (fromPlugin && fromPlugin.length > 0) {
|
|
285
|
+
if (fromPlugin.some((entry) => isContextModeHook(entry, hookType))) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
checkPluginRegistration() {
|
|
292
|
+
const settings = this.readSettings();
|
|
293
|
+
if (!settings) {
|
|
294
|
+
return {
|
|
295
|
+
check: "Plugin registration",
|
|
296
|
+
status: "warn",
|
|
297
|
+
message: "Could not read settings.json",
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const enabledPlugins = settings.enabledPlugins;
|
|
301
|
+
if (!enabledPlugins) {
|
|
302
|
+
return {
|
|
303
|
+
check: "Plugin registration",
|
|
304
|
+
status: "warn",
|
|
305
|
+
message: "No enabledPlugins section found (might be using standalone MCP mode)",
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const pluginKey = Object.keys(enabledPlugins).find((k) => k.startsWith("context-mode"));
|
|
309
|
+
if (pluginKey && enabledPlugins[pluginKey]) {
|
|
310
|
+
return {
|
|
311
|
+
check: "Plugin registration",
|
|
312
|
+
status: "pass",
|
|
313
|
+
message: `Plugin enabled: ${pluginKey}`,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
check: "Plugin registration",
|
|
318
|
+
status: "warn",
|
|
319
|
+
message: "context-mode not in enabledPlugins (might be using standalone MCP mode)",
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
getInstalledVersion() {
|
|
323
|
+
// Primary: read from installed_plugins.json
|
|
324
|
+
try {
|
|
325
|
+
const ipPath = join(this.getConfigDir(), "plugins", "installed_plugins.json");
|
|
326
|
+
const ipRaw = JSON.parse(readFileSync(ipPath, "utf-8"));
|
|
327
|
+
const plugins = ipRaw.plugins ?? {};
|
|
328
|
+
for (const [key, entries] of Object.entries(plugins)) {
|
|
329
|
+
if (!key.toLowerCase().includes("context-mode"))
|
|
330
|
+
continue;
|
|
331
|
+
const arr = entries;
|
|
332
|
+
if (arr.length > 0 && typeof arr[0].version === "string") {
|
|
333
|
+
return arr[0].version;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
/* fallback below */
|
|
339
|
+
}
|
|
340
|
+
// Fallback: scan common plugin cache locations.
|
|
341
|
+
// `resolveClaudeConfigDir` honors $CLAUDE_CONFIG_DIR; the literal
|
|
342
|
+
// `~/.claude` is also retained as a hard floor so environments that
|
|
343
|
+
// misconfigure the env still find the canonical dir if it exists.
|
|
344
|
+
const bases = Array.from(new Set([
|
|
345
|
+
this.getConfigDir(),
|
|
346
|
+
resolveClaudeConfigDir(),
|
|
347
|
+
resolve(homedir(), ".claude"),
|
|
348
|
+
resolve(homedir(), ".config", "claude"),
|
|
349
|
+
]));
|
|
350
|
+
for (const base of bases) {
|
|
351
|
+
const cacheDir = resolve(base, "plugins", "cache", "context-mode", "context-mode");
|
|
352
|
+
try {
|
|
353
|
+
const entries = readdirSync(cacheDir);
|
|
354
|
+
const versions = entries
|
|
355
|
+
.filter((e) => /^\d+\.\d+\.\d+/.test(e))
|
|
356
|
+
.sort((a, b) => {
|
|
357
|
+
const pa = a.split(".").map(Number);
|
|
358
|
+
const pb = b.split(".").map(Number);
|
|
359
|
+
for (let i = 0; i < 3; i++) {
|
|
360
|
+
if ((pa[i] ?? 0) !== (pb[i] ?? 0))
|
|
361
|
+
return (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
362
|
+
}
|
|
363
|
+
return 0;
|
|
364
|
+
});
|
|
365
|
+
if (versions.length > 0)
|
|
366
|
+
return versions[versions.length - 1];
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
/* continue */
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return "not installed";
|
|
373
|
+
}
|
|
374
|
+
// ── Upgrade ────────────────────────────────────────────
|
|
375
|
+
configureAllHooks(pluginRoot) {
|
|
376
|
+
const settings = this.readSettings() ?? {};
|
|
377
|
+
const hooks = (settings.hooks ?? {});
|
|
378
|
+
const changes = [];
|
|
379
|
+
// Remove stale context-mode hook entries across ALL hook types (fixes #187).
|
|
380
|
+
// After a marketplace auto-update or version change, settings.json may contain
|
|
381
|
+
// hardcoded paths pointing to deleted version directories (e.g., .../0.9.17/hooks/...).
|
|
382
|
+
// Clean these before registering fresh entries to prevent SessionStart errors.
|
|
383
|
+
for (const hookType of Object.keys(hooks)) {
|
|
384
|
+
const entries = hooks[hookType];
|
|
385
|
+
if (!Array.isArray(entries))
|
|
386
|
+
continue;
|
|
387
|
+
const filtered = entries.filter((entry) => {
|
|
388
|
+
const typedEntry = entry;
|
|
389
|
+
if (!isAnyContextModeHook(typedEntry))
|
|
390
|
+
return true; // preserve non-context-mode hooks
|
|
391
|
+
// Keep CLI dispatcher entries (path-independent, never stale)
|
|
392
|
+
const commands = typedEntry.hooks ?? [];
|
|
393
|
+
const hasOnlyDispatcherCommands = commands.every((h) => !h.command || !extractHookScriptPath(h.command));
|
|
394
|
+
if (hasOnlyDispatcherCommands)
|
|
395
|
+
return true;
|
|
396
|
+
// For node path commands, check if the referenced script file exists
|
|
397
|
+
return commands.every((h) => {
|
|
398
|
+
const scriptPath = h.command ? extractHookScriptPath(h.command) : null;
|
|
399
|
+
if (!scriptPath)
|
|
400
|
+
return true; // not a path-based command
|
|
401
|
+
return existsSync(scriptPath);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
const removed = entries.length - filtered.length;
|
|
405
|
+
if (removed > 0) {
|
|
406
|
+
hooks[hookType] = filtered;
|
|
407
|
+
changes.push(`Removed ${removed} stale ${hookType} hook(s)`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// If plugin hooks.json already covers all required hooks AND context-mode is
|
|
411
|
+
// actually installed as a Claude Code plugin (present in enabledPlugins), skip
|
|
412
|
+
// settings.json registration — hooks.json with ${CLAUDE_PLUGIN_ROOT} is the
|
|
413
|
+
// source of truth for plugin installs (Issue #198).
|
|
414
|
+
//
|
|
415
|
+
// Standalone / MacPorts installs are NOT in enabledPlugins. For those, the
|
|
416
|
+
// hooks/hooks.json shipped in the npm package is never consulted by Claude Code
|
|
417
|
+
// (it uses ${CLAUDE_PLUGIN_ROOT} which is only set in plugin mode). We must
|
|
418
|
+
// always write absolute-path hook commands to settings.json in that case.
|
|
419
|
+
const pluginRegistration = this.checkPluginRegistration();
|
|
420
|
+
const isPluginInstall = pluginRegistration.status === "pass";
|
|
421
|
+
const pluginHooks = isPluginInstall ? this.readPluginHooks(pluginRoot) : undefined;
|
|
422
|
+
if (pluginHooks) {
|
|
423
|
+
const allCovered = REQUIRED_HOOKS.every((ht) => this.checkHookType(undefined, pluginHooks, ht));
|
|
424
|
+
if (allCovered) {
|
|
425
|
+
// Strip ONLY the inner context-mode hook commands from each matcher entry —
|
|
426
|
+
// hooks.json is the source of truth for ctx-mode. User hooks co-located in
|
|
427
|
+
// the same matcher entry MUST be preserved (#415: entry-level filter wiped
|
|
428
|
+
// every co-located user hook). After stripping, prune entries whose `hooks`
|
|
429
|
+
// array becomes empty.
|
|
430
|
+
const ctxScriptNames = Object.values(HOOK_SCRIPTS);
|
|
431
|
+
const isCtxModeCommand = (cmd) => cmd != null &&
|
|
432
|
+
(ctxScriptNames.some((s) => cmd.includes(s)) ||
|
|
433
|
+
cmd.includes("context-mode hook"));
|
|
434
|
+
for (const hookType of Object.keys(hooks)) {
|
|
435
|
+
const entries = hooks[hookType];
|
|
436
|
+
if (!Array.isArray(entries))
|
|
437
|
+
continue;
|
|
438
|
+
let totalRemoved = 0;
|
|
439
|
+
for (const entry of entries) {
|
|
440
|
+
const typedEntry = entry;
|
|
441
|
+
const innerHooks = typedEntry.hooks ?? [];
|
|
442
|
+
const before = innerHooks.length;
|
|
443
|
+
typedEntry.hooks = innerHooks.filter((h) => !isCtxModeCommand(h.command));
|
|
444
|
+
totalRemoved += before - typedEntry.hooks.length;
|
|
445
|
+
}
|
|
446
|
+
const pruned = entries.filter((e) => {
|
|
447
|
+
const ih = e.hooks;
|
|
448
|
+
return Array.isArray(ih) && ih.length > 0;
|
|
449
|
+
});
|
|
450
|
+
if (totalRemoved > 0 || pruned.length !== entries.length) {
|
|
451
|
+
hooks[hookType] = pruned;
|
|
452
|
+
if (totalRemoved > 0) {
|
|
453
|
+
changes.push(`Removed ${totalRemoved} duplicate ${hookType} hook(s) — covered by plugin hooks.json`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
settings.hooks = hooks;
|
|
458
|
+
this.writeSettings(settings);
|
|
459
|
+
changes.push("Skipped settings.json registration — plugin hooks.json is sufficient");
|
|
460
|
+
return changes;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// Register fresh hooks for required hook types
|
|
464
|
+
const hookTypes = [
|
|
465
|
+
HOOK_TYPES.PRE_TOOL_USE,
|
|
466
|
+
HOOK_TYPES.SESSION_START,
|
|
467
|
+
];
|
|
468
|
+
for (const hookType of hookTypes) {
|
|
469
|
+
const command = buildHookCommand(hookType, pluginRoot);
|
|
470
|
+
if (hookType === HOOK_TYPES.PRE_TOOL_USE) {
|
|
471
|
+
const entry = {
|
|
472
|
+
matcher: PRE_TOOL_USE_MATCHER_PATTERN,
|
|
473
|
+
hooks: [{ type: "command", command }],
|
|
474
|
+
};
|
|
475
|
+
const existing = hooks.PreToolUse;
|
|
476
|
+
if (existing && Array.isArray(existing)) {
|
|
477
|
+
const idx = existing.findIndex((e) => isContextModeHook(e, hookType));
|
|
478
|
+
if (idx >= 0) {
|
|
479
|
+
existing[idx] = entry;
|
|
480
|
+
changes.push(`Updated existing ${hookType} hook entry`);
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
existing.push(entry);
|
|
484
|
+
changes.push(`Added ${hookType} hook entry`);
|
|
485
|
+
}
|
|
486
|
+
hooks.PreToolUse = existing;
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
hooks.PreToolUse = [entry];
|
|
490
|
+
changes.push(`Created ${hookType} hooks section`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
const entry = {
|
|
495
|
+
matcher: "",
|
|
496
|
+
hooks: [{ type: "command", command }],
|
|
497
|
+
};
|
|
498
|
+
const existing = hooks[hookType];
|
|
499
|
+
if (existing && Array.isArray(existing)) {
|
|
500
|
+
const idx = existing.findIndex((e) => isContextModeHook(e, hookType));
|
|
501
|
+
if (idx >= 0) {
|
|
502
|
+
existing[idx] = entry;
|
|
503
|
+
changes.push(`Updated existing ${hookType} hook entry`);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
existing.push(entry);
|
|
507
|
+
changes.push(`Added ${hookType} hook entry`);
|
|
508
|
+
}
|
|
509
|
+
hooks[hookType] = existing;
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
hooks[hookType] = [entry];
|
|
513
|
+
changes.push(`Created ${hookType} hooks section`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
settings.hooks = hooks;
|
|
518
|
+
this.writeSettings(settings);
|
|
519
|
+
return changes;
|
|
520
|
+
}
|
|
521
|
+
setHookPermissions(pluginRoot) {
|
|
522
|
+
const set = [];
|
|
523
|
+
for (const [, scriptName] of Object.entries(HOOK_SCRIPTS)) {
|
|
524
|
+
const scriptPath = resolve(pluginRoot, "hooks", scriptName);
|
|
525
|
+
try {
|
|
526
|
+
accessSync(scriptPath, constants.R_OK);
|
|
527
|
+
chmodSync(scriptPath, 0o755);
|
|
528
|
+
set.push(scriptPath);
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
/* skip missing scripts */
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return set;
|
|
535
|
+
}
|
|
536
|
+
updatePluginRegistry(pluginRoot, version) {
|
|
537
|
+
try {
|
|
538
|
+
const ipPath = join(this.getConfigDir(), "plugins", "installed_plugins.json");
|
|
539
|
+
const ipRaw = JSON.parse(readFileSync(ipPath, "utf-8"));
|
|
540
|
+
for (const [key, entries] of Object.entries(ipRaw.plugins || {})) {
|
|
541
|
+
if (!key.toLowerCase().includes("context-mode"))
|
|
542
|
+
continue;
|
|
543
|
+
for (const entry of entries) {
|
|
544
|
+
entry.installPath = pluginRoot;
|
|
545
|
+
entry.version = version;
|
|
546
|
+
entry.lastUpdated = new Date().toISOString();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
writeFileSync(ipPath, JSON.stringify(ipRaw, null, 2) + "\n", "utf-8");
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
/* best effort */
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// ── Session ID extraction ───────────────────────────────
|
|
556
|
+
// Claude Code priority: transcript_path UUID > session_id > CLAUDE_SESSION_ID > ppid
|
|
557
|
+
extractSessionId(input) {
|
|
558
|
+
if (input.transcript_path) {
|
|
559
|
+
const match = input.transcript_path.match(/([a-f0-9-]{36})\.jsonl$/);
|
|
560
|
+
if (match)
|
|
561
|
+
return match[1];
|
|
562
|
+
}
|
|
563
|
+
if (input.session_id)
|
|
564
|
+
return input.session_id;
|
|
565
|
+
if (process.env.CLAUDE_SESSION_ID)
|
|
566
|
+
return process.env.CLAUDE_SESSION_ID;
|
|
567
|
+
return `pid-${process.ppid}`;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapters/claude-code-base — Shared base for Claude Code wire-protocol adapters.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code and Qwen Code use the identical JSON stdin/stdout hook protocol:
|
|
5
|
+
* - Input fields: tool_name, tool_input, tool_output, is_error, session_id,
|
|
6
|
+
* transcript_path, source
|
|
7
|
+
* - Blocking: `permissionDecision: "deny"` in response
|
|
8
|
+
* - Arg modification: `updatedInput` field in response
|
|
9
|
+
* - Output modification: `updatedMCPToolOutput` field in response
|
|
10
|
+
* - Context injection: `additionalContext` at response root (not wrapped)
|
|
11
|
+
* - PreCompact/SessionStart: stdout on exit 0
|
|
12
|
+
*
|
|
13
|
+
* This base class implements the 8 shared parse/format methods.
|
|
14
|
+
* Subclasses provide platform-specific config (env vars, settings path,
|
|
15
|
+
* session ID priority, hook config, diagnostics, upgrade).
|
|
16
|
+
*/
|
|
17
|
+
import { BaseAdapter } from "./base.js";
|
|
18
|
+
import type { PreToolUseEvent, PostToolUseEvent, PreCompactEvent, SessionStartEvent, PreToolUseResponse, PostToolUseResponse, PreCompactResponse, SessionStartResponse } from "./types.js";
|
|
19
|
+
export interface ClaudeCodeWireInput {
|
|
20
|
+
tool_name?: string;
|
|
21
|
+
tool_input?: Record<string, unknown>;
|
|
22
|
+
tool_output?: string;
|
|
23
|
+
is_error?: boolean;
|
|
24
|
+
session_id?: string;
|
|
25
|
+
transcript_path?: string;
|
|
26
|
+
source?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare abstract class ClaudeCodeBaseAdapter extends BaseAdapter {
|
|
29
|
+
/**
|
|
30
|
+
* Environment variable name for the project directory.
|
|
31
|
+
* Claude Code: "CLAUDE_PROJECT_DIR", Qwen Code: "QWEN_PROJECT_DIR"
|
|
32
|
+
*/
|
|
33
|
+
protected abstract readonly projectDirEnvVar: string;
|
|
34
|
+
parsePreToolUseInput(raw: unknown): PreToolUseEvent;
|
|
35
|
+
parsePostToolUseInput(raw: unknown): PostToolUseEvent;
|
|
36
|
+
parsePreCompactInput(raw: unknown): PreCompactEvent;
|
|
37
|
+
parseSessionStartInput(raw: unknown): SessionStartEvent;
|
|
38
|
+
formatPreToolUseResponse(response: PreToolUseResponse): unknown;
|
|
39
|
+
formatPostToolUseResponse(response: PostToolUseResponse): unknown;
|
|
40
|
+
formatPreCompactResponse(response: PreCompactResponse): unknown;
|
|
41
|
+
formatSessionStartResponse(response: SessionStartResponse): unknown;
|
|
42
|
+
/**
|
|
43
|
+
* Extract session ID from wire input. Default priority (Claude Code):
|
|
44
|
+
* transcript_path UUID > session_id > env var > ppid fallback
|
|
45
|
+
*
|
|
46
|
+
* Override in subclasses for different priority (e.g., Qwen: session_id first).
|
|
47
|
+
*/
|
|
48
|
+
protected abstract extractSessionId(input: ClaudeCodeWireInput): string;
|
|
49
|
+
}
|