@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,464 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SessionStart hook for context-mode
|
|
4
|
+
*
|
|
5
|
+
* Provides the agent with XML-structured "Rules of Engagement"
|
|
6
|
+
* at the beginning of each session. Injects session knowledge on
|
|
7
|
+
* both startup and compact to maintain continuity.
|
|
8
|
+
*
|
|
9
|
+
* Session Lifecycle Rules:
|
|
10
|
+
* - "startup" → Fresh session. Inject previous session knowledge. Cleanup old data.
|
|
11
|
+
* - "compact" → Auto-compact triggered. Inject resume snapshot + stats.
|
|
12
|
+
* - "resume" → User invoked --continue, --resume, or /resume. CC sends the
|
|
13
|
+
* ACTIVE session_id; for /resume this is typically a *fresh*
|
|
14
|
+
* id, so live events miss → fall back to snapshot (#413).
|
|
15
|
+
* - "clear" → User cleared context. No resume.
|
|
16
|
+
*
|
|
17
|
+
* Crash-resilience: wrapped via runHook (#414) — all module loads happen
|
|
18
|
+
* dynamically inside the wrapper so a missing/poisoned dep can never hard-fail
|
|
19
|
+
* the hook. Errors land in ~/.claude/context-mode/hook-errors.log.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { runHook } from "./run-hook.mjs";
|
|
23
|
+
|
|
24
|
+
await runHook(async () => {
|
|
25
|
+
const { createRoutingBlock } = await import("./routing-block.mjs");
|
|
26
|
+
const { createToolNamer } = await import("./core/tool-naming.mjs");
|
|
27
|
+
const { detectPlatformFromEnv } = await import("./core/platform-detect.mjs");
|
|
28
|
+
const { buildAutoInjection } = await import("./auto-injection.mjs");
|
|
29
|
+
const {
|
|
30
|
+
readStdin,
|
|
31
|
+
parseStdin,
|
|
32
|
+
getSessionId,
|
|
33
|
+
getInputProjectDir,
|
|
34
|
+
getSessionDBPath,
|
|
35
|
+
getSessionEventsPath,
|
|
36
|
+
getCleanupFlagPath,
|
|
37
|
+
resolveConfigDir,
|
|
38
|
+
} = await import("./session-helpers.mjs");
|
|
39
|
+
const { writeSessionEventsFile, buildSessionDirective, getSessionEvents } = await import(
|
|
40
|
+
"./session-directive.mjs"
|
|
41
|
+
);
|
|
42
|
+
const { createSessionLoaders, attributeAndInsertEvents } = await import("./session-loaders.mjs");
|
|
43
|
+
const { join, dirname } = await import("node:path");
|
|
44
|
+
const { fileURLToPath } = await import("node:url");
|
|
45
|
+
const { readFileSync, unlinkSync, readdirSync, rmSync, lstatSync, realpathSync, symlinkSync } = await import("node:fs");
|
|
46
|
+
|
|
47
|
+
const detectedPlatform = detectPlatformFromEnv();
|
|
48
|
+
const toolNamer = createToolNamer(detectedPlatform);
|
|
49
|
+
const ROUTING_BLOCK = createRoutingBlock(toolNamer);
|
|
50
|
+
|
|
51
|
+
// Resolve absolute path for imports (fileURLToPath for Windows compat)
|
|
52
|
+
const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
|
|
53
|
+
const { loadSessionDB, loadProjectAttribution, loadExtract } = createSessionLoaders(HOOK_DIR);
|
|
54
|
+
|
|
55
|
+
// Emit a `session_start` canonical event at the boundary of each session
|
|
56
|
+
// lifecycle transition (startup / resume / compact). The platform's insight
|
|
57
|
+
// engine joins on `category='session_start'` to compute per-session
|
|
58
|
+
// aggregates (~60 of 180 patterns depend on this anchor row). Bridge
|
|
59
|
+
// forwards via attributeAndInsertEvents which also stamps the rollup
|
|
60
|
+
// snapshot — safe for the FIRST event of a fresh session.
|
|
61
|
+
async function emitSessionStartLifecycle(db, sessionId, source, projectDir, input) {
|
|
62
|
+
try {
|
|
63
|
+
const { resolveProjectAttributions } = await loadProjectAttribution();
|
|
64
|
+
const lifecycleEvent = {
|
|
65
|
+
type: "session_start",
|
|
66
|
+
category: "session_start",
|
|
67
|
+
data: JSON.stringify({
|
|
68
|
+
source,
|
|
69
|
+
project_dir: projectDir,
|
|
70
|
+
started_at: Math.floor(Date.now() / 1000),
|
|
71
|
+
}),
|
|
72
|
+
priority: 1,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// PRD #4 — emit session_settings_snapshot alongside lifecycle when
|
|
76
|
+
// the SessionStart envelope carries any of mcp_servers / model /
|
|
77
|
+
// permission_mode. Best-effort: missing fields → no snapshot.
|
|
78
|
+
const eventsToEmit = [lifecycleEvent];
|
|
79
|
+
try {
|
|
80
|
+
const extract = await loadExtract();
|
|
81
|
+
if (typeof extract.extractSessionSettings === "function") {
|
|
82
|
+
eventsToEmit.push(...extract.extractSessionSettings(input));
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// settings snapshot is opportunistic — never block lifecycle on it
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
attributeAndInsertEvents(
|
|
89
|
+
db,
|
|
90
|
+
sessionId,
|
|
91
|
+
eventsToEmit,
|
|
92
|
+
input,
|
|
93
|
+
projectDir,
|
|
94
|
+
"SessionStart",
|
|
95
|
+
resolveProjectAttributions,
|
|
96
|
+
);
|
|
97
|
+
} catch {
|
|
98
|
+
// Best-effort — lifecycle emission failure MUST NOT block session start.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Self-heal a partial plugin cache install before anything else
|
|
103
|
+
// touches the cache dir. The Algo-D4 boot gate and the #604
|
|
104
|
+
// normalize-hooks ratchet both fire from start.mjs, which is one of
|
|
105
|
+
// the files that may be missing in the failure mode; sessionstart.mjs
|
|
106
|
+
// fires from CC's hooks.json wiring regardless of MCP boot status, so
|
|
107
|
+
// it is the reliably-available entry point. See
|
|
108
|
+
// hooks/heal-partial-install.mjs for the full failure-mode description.
|
|
109
|
+
try {
|
|
110
|
+
const { healPartialInstallFromMarketplace } = await import("./heal-partial-install.mjs");
|
|
111
|
+
healPartialInstallFromMarketplace();
|
|
112
|
+
} catch { /* best effort, never block session start */ }
|
|
113
|
+
|
|
114
|
+
// Issue #710 — Layer 2: self-heal Claude Code's per-session shell snapshots.
|
|
115
|
+
//
|
|
116
|
+
// Claude Code `source`s ~/.claude/shell-snapshots/snapshot-*.sh before every
|
|
117
|
+
// Bash tool call (refs/platforms/claude-code/src/utils/bash/ShellSnapshot.ts:269-336,
|
|
118
|
+
// sourced at bashProvider.ts:166). The snapshot bakes an `export PATH='…'`
|
|
119
|
+
// line with the context-mode `bin/` of the version active at session boot.
|
|
120
|
+
// After /ctx-upgrade deletes the old cache dir, the snapshot still points
|
|
121
|
+
// at it — every Bash call fails with "Plugin directory does not exist"
|
|
122
|
+
// until the session restarts.
|
|
123
|
+
//
|
|
124
|
+
// Layer 1 (cli.ts /ctx-upgrade) rewrites the active session's snapshot
|
|
125
|
+
// mid-upgrade so the in-process session never sees the broken state.
|
|
126
|
+
// Layer 2 (this) catches sessions that started after /ctx-upgrade but
|
|
127
|
+
// whose snapshots somehow missed the rewrite (parallel sessions, killed
|
|
128
|
+
// /ctx-upgrade run, manual cache surgery). Resolves currentVersion from
|
|
129
|
+
// the plugin's own manifest — no env-var dependency, immune to PATH bugs.
|
|
130
|
+
// Best-effort, never blocks session start.
|
|
131
|
+
try {
|
|
132
|
+
const { selfHealShellSnapshots } = await import("./cache-heal-utils.mjs");
|
|
133
|
+
const { resolve } = await import("node:path");
|
|
134
|
+
const { resolveConfigDir } = await import("./session-helpers.mjs");
|
|
135
|
+
|
|
136
|
+
// Read the version this MCP boot is running under. PLUGIN_ROOT
|
|
137
|
+
// points at ~/.claude/plugins/cache/context-mode/context-mode/<vX>/.
|
|
138
|
+
let currentVersion = null;
|
|
139
|
+
try {
|
|
140
|
+
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT
|
|
141
|
+
?? resolve(HOOK_DIR, "..");
|
|
142
|
+
const manifestPath = resolve(pluginRoot, ".claude-plugin", "plugin.json");
|
|
143
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
144
|
+
if (typeof manifest?.version === "string" && manifest.version) {
|
|
145
|
+
currentVersion = manifest.version;
|
|
146
|
+
}
|
|
147
|
+
} catch { /* missing/malformed manifest — skip self-heal */ }
|
|
148
|
+
|
|
149
|
+
if (currentVersion) {
|
|
150
|
+
const snapshotsDir = resolve(resolveConfigDir(), "shell-snapshots");
|
|
151
|
+
const pluginCacheRoot = resolve(
|
|
152
|
+
resolveConfigDir(),
|
|
153
|
+
"plugins",
|
|
154
|
+
"cache",
|
|
155
|
+
"context-mode",
|
|
156
|
+
"context-mode",
|
|
157
|
+
);
|
|
158
|
+
selfHealShellSnapshots({
|
|
159
|
+
snapshotsDir,
|
|
160
|
+
pluginCacheRoot,
|
|
161
|
+
currentVersion,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
} catch { /* best effort, never block session start */ }
|
|
165
|
+
|
|
166
|
+
let additionalContext = ROUTING_BLOCK;
|
|
167
|
+
|
|
168
|
+
// ─── #558: surface security init failure as agent-facing context ───
|
|
169
|
+
//
|
|
170
|
+
// Pre-558 the only signal of a fail-open security regression was a
|
|
171
|
+
// stderr WARNING line (suppressed/discarded by most adapters). The
|
|
172
|
+
// SessionStart additionalContext block is the in-band channel — the
|
|
173
|
+
// agent reads it, the user sees it. Idempotent by virtue of
|
|
174
|
+
// SessionStart's once-per-session lifecycle.
|
|
175
|
+
try {
|
|
176
|
+
const { initSecurity, isSecurityInitFailed, buildSecurityWarningContext } =
|
|
177
|
+
await import("./core/routing.mjs");
|
|
178
|
+
const { resolve: _resolve } = await import("node:path");
|
|
179
|
+
await initSecurity(_resolve(HOOK_DIR, "..", "build"));
|
|
180
|
+
if (isSecurityInitFailed()) {
|
|
181
|
+
const warning = buildSecurityWarningContext();
|
|
182
|
+
if (warning) additionalContext = warning + "\n\n" + additionalContext;
|
|
183
|
+
}
|
|
184
|
+
} catch { /* security probe is best-effort — never block session start */ }
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const raw = await readStdin();
|
|
188
|
+
const input = parseStdin(raw);
|
|
189
|
+
const source = input.source ?? "startup";
|
|
190
|
+
|
|
191
|
+
if (source === "compact") {
|
|
192
|
+
// Session was compacted — write events to file for auto-indexing, inject directive only
|
|
193
|
+
const { SessionDB } = await loadSessionDB();
|
|
194
|
+
const dbPath = getSessionDBPath();
|
|
195
|
+
const db = new SessionDB({ dbPath });
|
|
196
|
+
const sessionId = getSessionId(input);
|
|
197
|
+
const resume = db.getResume(sessionId);
|
|
198
|
+
|
|
199
|
+
if (resume && !resume.consumed) {
|
|
200
|
+
db.markResumeConsumed(sessionId);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const events = getSessionEvents(db, sessionId);
|
|
204
|
+
if (events.length > 0) {
|
|
205
|
+
const eventMeta = writeSessionEventsFile(events, getSessionEventsPath());
|
|
206
|
+
additionalContext += buildSessionDirective("compact", eventMeta, toolNamer);
|
|
207
|
+
|
|
208
|
+
// Auto-inject behavioral state on compaction (role, decisions, skills, intent)
|
|
209
|
+
const autoInjection = buildAutoInjection(events);
|
|
210
|
+
if (autoInjection) {
|
|
211
|
+
additionalContext += "\n\n" + autoInjection;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// D2 PRD Phase 6.2: emit snapshot-consumed with bytes_returned=snapshot.length.
|
|
215
|
+
// The resumed snapshot bytes ARE returned to the model — that's the whole
|
|
216
|
+
// point of resume — so account them on bytes_returned, not bytes_avoided.
|
|
217
|
+
// v1.0.160: route through wire — resume metric on the platform reads
|
|
218
|
+
// category='session-resume' rows. Both snapshot-consumed (bytes
|
|
219
|
+
// returned) and resume_completed land here so the dashboard sees
|
|
220
|
+
// every resume boundary.
|
|
221
|
+
try {
|
|
222
|
+
const resumeRow = (resume && resume.snapshot)
|
|
223
|
+
? resume
|
|
224
|
+
: (db.getResume?.(sessionId) ?? null);
|
|
225
|
+
const snapshotBytes = resumeRow?.snapshot?.length ?? 0;
|
|
226
|
+
const { resolveProjectAttributions } = await loadProjectAttribution();
|
|
227
|
+
const projectDirResumeMeta = getInputProjectDir(input);
|
|
228
|
+
|
|
229
|
+
await attributeAndInsertEvents(
|
|
230
|
+
db,
|
|
231
|
+
sessionId,
|
|
232
|
+
[{
|
|
233
|
+
type: "snapshot-consumed",
|
|
234
|
+
category: "session-resume",
|
|
235
|
+
data: `Session resumed from ${source}. Snapshot ${snapshotBytes} bytes injected.`,
|
|
236
|
+
priority: 1,
|
|
237
|
+
bytes_returned: snapshotBytes,
|
|
238
|
+
}, {
|
|
239
|
+
type: "resume_completed",
|
|
240
|
+
category: "session-resume",
|
|
241
|
+
data: `Session resumed from ${source}. Prior events loaded.`,
|
|
242
|
+
priority: 1,
|
|
243
|
+
}],
|
|
244
|
+
input,
|
|
245
|
+
projectDirResumeMeta,
|
|
246
|
+
"SessionStart",
|
|
247
|
+
resolveProjectAttributions,
|
|
248
|
+
);
|
|
249
|
+
} catch { /* best-effort */ }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Emit lifecycle anchor BEFORE close — engine joins on
|
|
253
|
+
// category='session_start' to compute per-session aggregates.
|
|
254
|
+
// Cross-platform projectDir via getInputProjectDir (covers cursor's
|
|
255
|
+
// workspace_roots[], codex/gemini/qwen's *_PROJECT_DIR env vars,
|
|
256
|
+
// CC's CLAUDE_PROJECT_DIR, falls back to input.cwd and process.cwd).
|
|
257
|
+
const projectDirCompact = getInputProjectDir(input);
|
|
258
|
+
await emitSessionStartLifecycle(db, sessionId, "compact", projectDirCompact, input);
|
|
259
|
+
db.close();
|
|
260
|
+
} else if (source === "resume") {
|
|
261
|
+
// User invoked --continue, --resume, or /resume — clear cleanup flag so
|
|
262
|
+
// startup doesn't wipe data on the next fresh boot.
|
|
263
|
+
try { unlinkSync(getCleanupFlagPath()); } catch { /* no flag */ }
|
|
264
|
+
|
|
265
|
+
const { SessionDB } = await loadSessionDB();
|
|
266
|
+
const dbPath = getSessionDBPath();
|
|
267
|
+
const db = new SessionDB({ dbPath });
|
|
268
|
+
|
|
269
|
+
// 1) Try live events for the resumed session. Filter strictly to the
|
|
270
|
+
// incoming session_id — falling back to getLatestSessionEvents(db)
|
|
271
|
+
// leaks events from any other session whose session_meta.started_at
|
|
272
|
+
// is more recent (cross-worktree bleed observed in the wild).
|
|
273
|
+
const sessionId = getSessionId(input);
|
|
274
|
+
const events = sessionId ? getSessionEvents(db, sessionId) : [];
|
|
275
|
+
if (events.length > 0) {
|
|
276
|
+
const eventMeta = writeSessionEventsFile(events, getSessionEventsPath());
|
|
277
|
+
additionalContext += buildSessionDirective("resume", eventMeta, toolNamer);
|
|
278
|
+
} else if (sessionId) {
|
|
279
|
+
// 2) Snapshot fallback (#413). /resume hands us a *new* active session
|
|
280
|
+
// id whose live event table is empty; the prior conversation lives
|
|
281
|
+
// in `session_resume.snapshot`. Mirrors the OpenCode/OpenClaw resume
|
|
282
|
+
// injection path (opencode-plugin.ts:454). claimLatestUnconsumedResume
|
|
283
|
+
// excludes the current id, so we surface the latest unconsumed
|
|
284
|
+
// snapshot from any prior session in this project.
|
|
285
|
+
const row = db.claimLatestUnconsumedResume(sessionId);
|
|
286
|
+
if (row?.snapshot) {
|
|
287
|
+
additionalContext += "\n\n" + row.snapshot;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const projectDirResume = getInputProjectDir(input);
|
|
292
|
+
if (sessionId) {
|
|
293
|
+
await emitSessionStartLifecycle(db, sessionId, "resume", projectDirResume, input);
|
|
294
|
+
}
|
|
295
|
+
db.close();
|
|
296
|
+
} else if (source === "startup") {
|
|
297
|
+
// Fresh session (no --continue) — clean slate, capture CLAUDE.md rules.
|
|
298
|
+
const { SessionDB } = await loadSessionDB();
|
|
299
|
+
const dbPath = getSessionDBPath();
|
|
300
|
+
const db = new SessionDB({ dbPath });
|
|
301
|
+
try { unlinkSync(getSessionEventsPath()); } catch { /* no stale file */ }
|
|
302
|
+
|
|
303
|
+
// Detect true fresh start vs --continue (which fires startup→resume).
|
|
304
|
+
// If cleanup flag exists from a PREVIOUS startup that was never followed by
|
|
305
|
+
// resume, that was a true fresh start — aggressively wipe all data.
|
|
306
|
+
db.cleanupOldSessions(7);
|
|
307
|
+
// Bug fix: the unconditional DELETE below USED to wipe ALL orphan
|
|
308
|
+
// events (any session_id missing from session_meta). On a power-outage
|
|
309
|
+
// restart this destroyed 1000+ events of real Claude Code work whose
|
|
310
|
+
// UUID session_ids hadn't yet had their session_meta row written
|
|
311
|
+
// (timing window between insertEvent and ensureSession). See
|
|
312
|
+
// tests/session/cleanup-preserves-live-uuid-events.test.ts.
|
|
313
|
+
//
|
|
314
|
+
// Now: protect anything that LOOKS like a real session UUID
|
|
315
|
+
// (4 dashes per RFC 4122 8-4-4-4-12), unless it's already older than
|
|
316
|
+
// the 7-day cleanup horizon. Detection-probe orphans like 'pid-12345'
|
|
317
|
+
// (no UUID shape) are still wiped aggressively — they're noise.
|
|
318
|
+
// Loose 4-dash shape `*-*-*-*-*`. Claude Code session_ids are UUIDs
|
|
319
|
+
// (5 dash-separated segments) and match. `pid-XXXXX` probes have one
|
|
320
|
+
// dash and don't match → wiped aggressively. We deliberately keep
|
|
321
|
+
// this loose so adapters that may eventually share this DB (or reuse
|
|
322
|
+
// this hook with hybrid `claude-code-...`-style IDs across 15
|
|
323
|
+
// platforms) aren't accidentally classified as orphans. The 7-day
|
|
324
|
+
// fallback still wipes truly abandoned UUIDs.
|
|
325
|
+
db.db.exec(`
|
|
326
|
+
DELETE FROM session_events
|
|
327
|
+
WHERE session_id NOT IN (SELECT session_id FROM session_meta)
|
|
328
|
+
AND (
|
|
329
|
+
session_id NOT GLOB '*-*-*-*-*' -- pid-XXX probes etc.
|
|
330
|
+
OR created_at < datetime('now', '-7 day') -- truly abandoned UUIDs
|
|
331
|
+
)
|
|
332
|
+
`);
|
|
333
|
+
|
|
334
|
+
// Proactively capture CLAUDE.md files — Claude Code loads them as system
|
|
335
|
+
// context at startup, invisible to PostToolUse hooks. We read them from
|
|
336
|
+
// disk so they survive compact/resume via the session events pipeline.
|
|
337
|
+
const sessionId = getSessionId(input);
|
|
338
|
+
// v1.0.160: cross-adapter projectDir resolution (was hardcoded CC env).
|
|
339
|
+
const projectDir = getInputProjectDir(input);
|
|
340
|
+
db.ensureSession(sessionId, projectDir);
|
|
341
|
+
const claudeMdPaths = [
|
|
342
|
+
join(resolveConfigDir(), "CLAUDE.md"),
|
|
343
|
+
join(projectDir, "CLAUDE.md"),
|
|
344
|
+
join(projectDir, ".claude", "CLAUDE.md"),
|
|
345
|
+
];
|
|
346
|
+
// v1.0.160: collect rule events into a batch and forward through wire.
|
|
347
|
+
// Dashboard's "CLAUDE.md adoption" widget COUNTs category='rule' rows on
|
|
348
|
+
// the platform — without this routing the widget reads 0 no matter how
|
|
349
|
+
// many CLAUDE.md files actually loaded.
|
|
350
|
+
const ruleEvents = [];
|
|
351
|
+
for (const p of claudeMdPaths) {
|
|
352
|
+
try {
|
|
353
|
+
const content = readFileSync(p, "utf-8");
|
|
354
|
+
if (content.trim()) {
|
|
355
|
+
ruleEvents.push({ type: "rule", category: "rule", data: p, priority: 1 });
|
|
356
|
+
ruleEvents.push({ type: "rule_content", category: "rule", data: content, priority: 1 });
|
|
357
|
+
}
|
|
358
|
+
} catch { /* file doesn't exist — skip */ }
|
|
359
|
+
}
|
|
360
|
+
if (ruleEvents.length > 0) {
|
|
361
|
+
try {
|
|
362
|
+
const { resolveProjectAttributions } = await loadProjectAttribution();
|
|
363
|
+
attributeAndInsertEvents(
|
|
364
|
+
db,
|
|
365
|
+
sessionId,
|
|
366
|
+
ruleEvents,
|
|
367
|
+
input,
|
|
368
|
+
projectDir,
|
|
369
|
+
"SessionStart",
|
|
370
|
+
resolveProjectAttributions,
|
|
371
|
+
);
|
|
372
|
+
} catch { /* best-effort — rule capture must never block start */ }
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Lifecycle anchor for a fresh session — emits BEFORE the CLAUDE.md
|
|
376
|
+
// rule events have been forwarded so the `session_start` row lands
|
|
377
|
+
// as the very first row the platform sees for this session.
|
|
378
|
+
await emitSessionStartLifecycle(db, sessionId, "startup", projectDir, input);
|
|
379
|
+
|
|
380
|
+
db.close();
|
|
381
|
+
|
|
382
|
+
// Age-gated lazy cleanup of old plugin cache version dirs (#181).
|
|
383
|
+
// Only delete dirs older than 1 hour to avoid breaking active sessions.
|
|
384
|
+
// Use lstatSync (not statSync) so a fresh symlink whose target happens
|
|
385
|
+
// to be old is evaluated against the symlink's own mtime, not the
|
|
386
|
+
// target's — otherwise self-heal hooks that re-create breadcrumb
|
|
387
|
+
// symlinks for previous cache versions would be wiped out and any
|
|
388
|
+
// session pinned to one of those versions would lose its plugin root
|
|
389
|
+
// mid-flight (#644).
|
|
390
|
+
try {
|
|
391
|
+
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
|
|
392
|
+
if (pluginRoot) {
|
|
393
|
+
const cacheParentMatch = pluginRoot.match(/^(.*[\\/]plugins[\\/]cache[\\/][^\\/]+[\\/][^\\/]+[\\/])/);
|
|
394
|
+
if (cacheParentMatch) {
|
|
395
|
+
const cacheParent = cacheParentMatch[1];
|
|
396
|
+
const myDir = pluginRoot.replace(cacheParent, "").replace(/[\\/]/g, "");
|
|
397
|
+
const ONE_HOUR = 3600000;
|
|
398
|
+
const now = Date.now();
|
|
399
|
+
for (const d of readdirSync(cacheParent)) {
|
|
400
|
+
if (d === myDir) continue;
|
|
401
|
+
const oldDir = join(cacheParent, d);
|
|
402
|
+
try {
|
|
403
|
+
const st = lstatSync(oldDir);
|
|
404
|
+
let danglingBreadcrumb = false;
|
|
405
|
+
if (st.isSymbolicLink()) {
|
|
406
|
+
try {
|
|
407
|
+
realpathSync(oldDir);
|
|
408
|
+
} catch {
|
|
409
|
+
danglingBreadcrumb = true;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (danglingBreadcrumb || now - st.mtimeMs > ONE_HOUR) {
|
|
413
|
+
rmSync(oldDir, { recursive: true, force: true });
|
|
414
|
+
// Leave a breadcrumb symlink (junction on Windows) in the
|
|
415
|
+
// removed version's place so sessions that loaded hooks
|
|
416
|
+
// from it before an auto-update keep resolving their
|
|
417
|
+
// scripts instead of erroring on every hook call until
|
|
418
|
+
// restart (#814, #807). Also fires when the entry was
|
|
419
|
+
// itself a stale breadcrumb, re-pointing it at the live
|
|
420
|
+
// root (a chain of updates would otherwise leave links
|
|
421
|
+
// targeting intermediate versions that no longer exist).
|
|
422
|
+
// The fresh mtime makes #644's lstat age gate protect it
|
|
423
|
+
// for the next hour, and the next sweep refreshes it
|
|
424
|
+
// again. Same pattern as healCacheMidSession (server.ts)
|
|
425
|
+
// and postinstall.mjs.
|
|
426
|
+
try {
|
|
427
|
+
symlinkSync(pluginRoot, oldDir, process.platform === "win32" ? "junction" : undefined);
|
|
428
|
+
} catch { /* best effort — plain delete is the pre-#814 behaviour */ }
|
|
429
|
+
}
|
|
430
|
+
} catch {
|
|
431
|
+
// On Windows, a dangling junction can fail before we can read
|
|
432
|
+
// its own mtime. Treat that as a stale breadcrumb and try to
|
|
433
|
+
// repoint it at the live root; failures remain best-effort.
|
|
434
|
+
try {
|
|
435
|
+
rmSync(oldDir, { recursive: true, force: true });
|
|
436
|
+
symlinkSync(pluginRoot, oldDir, process.platform === "win32" ? "junction" : undefined);
|
|
437
|
+
} catch { /* skip */ }
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
} catch { /* best effort — never block session start */ }
|
|
443
|
+
}
|
|
444
|
+
// "clear" — no reset needed; ctx_purge is the only wipe mechanism
|
|
445
|
+
} catch (err) {
|
|
446
|
+
// Session continuity is best-effort — never block session start
|
|
447
|
+
try {
|
|
448
|
+
const { appendFileSync } = await import("node:fs");
|
|
449
|
+
const { join: pjoin } = await import("node:path");
|
|
450
|
+
const { resolveConfigDir: _resolve } = await import("./session-helpers.mjs");
|
|
451
|
+
appendFileSync(
|
|
452
|
+
pjoin(_resolve(), "context-mode", "sessionstart-debug.log"),
|
|
453
|
+
`[${new Date().toISOString()}] ${err?.message || err}\n${err?.stack || ""}\n`,
|
|
454
|
+
);
|
|
455
|
+
} catch { /* ignore logging failure */ }
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
console.log(JSON.stringify({
|
|
459
|
+
hookSpecificOutput: {
|
|
460
|
+
hookEventName: "SessionStart",
|
|
461
|
+
additionalContext,
|
|
462
|
+
},
|
|
463
|
+
}));
|
|
464
|
+
});
|
package/hooks/stop.mjs
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./suppress-stderr.mjs";
|
|
3
|
+
import "./ensure-deps.mjs";
|
|
4
|
+
/**
|
|
5
|
+
* Claude Code Stop hook — record turn-end state for continuity.
|
|
6
|
+
*
|
|
7
|
+
* Stop fires when Claude is about to finish the current assistant turn. This is
|
|
8
|
+
* not a true session shutdown event, so record a turn_end marker and never ask
|
|
9
|
+
* Claude to continue.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir } from "./session-helpers.mjs";
|
|
13
|
+
import { createSessionLoaders, attributeAndInsertEvents } from "./session-loaders.mjs";
|
|
14
|
+
import { dirname } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
|
|
18
|
+
const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const raw = await readStdin();
|
|
23
|
+
const input = parseStdin(raw);
|
|
24
|
+
const projectDir = getInputProjectDir(input);
|
|
25
|
+
|
|
26
|
+
const { SessionDB } = await loadSessionDB();
|
|
27
|
+
const dbPath = getSessionDBPath(undefined, projectDir);
|
|
28
|
+
const db = new SessionDB({ dbPath });
|
|
29
|
+
const sessionId = getSessionId(input);
|
|
30
|
+
|
|
31
|
+
db.ensureSession(sessionId, projectDir);
|
|
32
|
+
const payload = {
|
|
33
|
+
stop_hook_active: input.stop_hook_active ?? false,
|
|
34
|
+
last_assistant_message: typeof input.last_assistant_message === "string"
|
|
35
|
+
? input.last_assistant_message.slice(0, 2000)
|
|
36
|
+
: null,
|
|
37
|
+
};
|
|
38
|
+
db.insertEvent(sessionId, {
|
|
39
|
+
type: "turn_end",
|
|
40
|
+
category: "session",
|
|
41
|
+
data: JSON.stringify(payload),
|
|
42
|
+
priority: 1,
|
|
43
|
+
}, "Stop");
|
|
44
|
+
|
|
45
|
+
// ─── claude-code MAIN-turn cost capture (cursor-aware, no double-count) ───
|
|
46
|
+
// The transcript grows every turn and the forward loop forwards ALL passed
|
|
47
|
+
// events, so we emit ONLY the turns NEW since the last Stop, keyed by a
|
|
48
|
+
// per-session high-water cursor. Each step is best-effort — a hook must
|
|
49
|
+
// never block the session, so a transcript read or extract failure here is
|
|
50
|
+
// swallowed without aborting the turn_end write above.
|
|
51
|
+
try {
|
|
52
|
+
const transcriptPath = typeof input.transcript_path === "string" ? input.transcript_path : null;
|
|
53
|
+
if (transcriptPath) {
|
|
54
|
+
let transcript = null;
|
|
55
|
+
try {
|
|
56
|
+
transcript = readFileSync(transcriptPath, "utf-8");
|
|
57
|
+
} catch {
|
|
58
|
+
// unreadable/missing transcript — skip capture this turn.
|
|
59
|
+
}
|
|
60
|
+
if (transcript) {
|
|
61
|
+
const { extractTranscriptUsageSince } = await loadExtract();
|
|
62
|
+
const { resolveProjectAttributions } = await loadProjectAttribution();
|
|
63
|
+
const cursor = db.getUsageCursor(sessionId);
|
|
64
|
+
const { events, cursor: next } = extractTranscriptUsageSince(transcript, cursor);
|
|
65
|
+
if (events.length > 0) {
|
|
66
|
+
// attributeAndInsertEvents both INSERTS locally and FORWARDS to the
|
|
67
|
+
// platform (gated on ~/.context-mode/platform.json).
|
|
68
|
+
attributeAndInsertEvents(db, sessionId, events, input, projectDir, "Stop", resolveProjectAttributions);
|
|
69
|
+
}
|
|
70
|
+
if (next) db.setUsageCursor(sessionId, next);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Best-effort cost capture — never block the session on failure.
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
db.close();
|
|
78
|
+
} catch {
|
|
79
|
+
// Claude Code hooks must not block the session.
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
process.stdout.write("{}\n");
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suppress stderr at the OS file descriptor level.
|
|
3
|
+
*
|
|
4
|
+
* Native C++ modules (better-sqlite3) write directly to fd 2 during
|
|
5
|
+
* initialization, bypassing Node.js process.stderr. Platforms like
|
|
6
|
+
* Claude Code interpret ANY stderr output as hook failure.
|
|
7
|
+
*
|
|
8
|
+
* This module MUST be the first import in every hook entry point.
|
|
9
|
+
* ESM evaluates imports depth-first in declaration order, so importing
|
|
10
|
+
* this module first ensures fd 2 is redirected to /dev/null before
|
|
11
|
+
* any native modules are loaded.
|
|
12
|
+
*
|
|
13
|
+
* Cross-platform: os.devNull → /dev/null (Unix) or \\.\NUL (Windows).
|
|
14
|
+
* See: https://github.com/mxalbert1996/context-mode/issues/68
|
|
15
|
+
*/
|
|
16
|
+
import { closeSync, openSync } from "node:fs";
|
|
17
|
+
import { devNull } from "node:os";
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
closeSync(2);
|
|
21
|
+
openSync(devNull, "w"); // Acquires fd 2 (lowest available)
|
|
22
|
+
} catch {
|
|
23
|
+
// Fallback: suppress at Node.js stream level
|
|
24
|
+
process.stderr.write = /** @type {any} */ (() => true);
|
|
25
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* UserPromptSubmit hook for context-mode session continuity.
|
|
4
|
+
*
|
|
5
|
+
* Captures every user prompt so the LLM can continue from the exact
|
|
6
|
+
* point where the user left off after compact or session restart.
|
|
7
|
+
*
|
|
8
|
+
* Must be fast (<10ms). Just a single SQLite write.
|
|
9
|
+
*
|
|
10
|
+
* Crash-resilience: wrapped via runHook (#414) — module loads happen
|
|
11
|
+
* dynamically so missing deps log + exit 0 instead of MODULE_NOT_FOUND.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { runHook } from "./run-hook.mjs";
|
|
15
|
+
|
|
16
|
+
await runHook(async () => {
|
|
17
|
+
const {
|
|
18
|
+
readStdin,
|
|
19
|
+
parseStdin,
|
|
20
|
+
getSessionId,
|
|
21
|
+
getSessionDBPath,
|
|
22
|
+
getInputProjectDir,
|
|
23
|
+
} = await import("./session-helpers.mjs");
|
|
24
|
+
const { createSessionLoaders, attributeAndInsertEvents } = await import("./session-loaders.mjs");
|
|
25
|
+
const { dirname } = await import("node:path");
|
|
26
|
+
const { fileURLToPath } = await import("node:url");
|
|
27
|
+
|
|
28
|
+
const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const raw = await readStdin();
|
|
33
|
+
const input = parseStdin(raw);
|
|
34
|
+
const projectDir = getInputProjectDir(input);
|
|
35
|
+
|
|
36
|
+
const prompt = input.prompt ?? input.message ?? "";
|
|
37
|
+
const trimmed = (prompt || "").trim();
|
|
38
|
+
|
|
39
|
+
// Skip system-generated messages — only capture genuine user prompts
|
|
40
|
+
const isSystemMessage = trimmed.startsWith("<task-notification>")
|
|
41
|
+
|| trimmed.startsWith("<system-reminder>")
|
|
42
|
+
|| trimmed.startsWith("<context_guidance>")
|
|
43
|
+
|| trimmed.startsWith("<tool-result>");
|
|
44
|
+
|
|
45
|
+
if (trimmed.length > 0 && !isSystemMessage) {
|
|
46
|
+
const { SessionDB } = await loadSessionDB();
|
|
47
|
+
const { extractUserEvents, extractUserPromptFeatures } = await loadExtract();
|
|
48
|
+
const { resolveProjectAttributions } = await loadProjectAttribution();
|
|
49
|
+
const dbPath = getSessionDBPath();
|
|
50
|
+
const db = new SessionDB({ dbPath });
|
|
51
|
+
const sessionId = getSessionId(input);
|
|
52
|
+
|
|
53
|
+
db.ensureSession(sessionId, projectDir);
|
|
54
|
+
|
|
55
|
+
// 1. Always save the raw prompt with F1 §2 features attached.
|
|
56
|
+
// Features attach to the existing user_prompt event payload alongside
|
|
57
|
+
// the raw `data` field (do NOT remove `data`). Platform Zod envelope
|
|
58
|
+
// is forward-compatible; new fields persist as typed columns.
|
|
59
|
+
const promptFeatures = typeof extractUserPromptFeatures === "function"
|
|
60
|
+
? extractUserPromptFeatures(trimmed)
|
|
61
|
+
: {};
|
|
62
|
+
const promptEvent = {
|
|
63
|
+
type: "user_prompt",
|
|
64
|
+
category: "user-prompt",
|
|
65
|
+
data: prompt,
|
|
66
|
+
priority: 1,
|
|
67
|
+
...promptFeatures,
|
|
68
|
+
};
|
|
69
|
+
const promptAttributions = attributeAndInsertEvents(
|
|
70
|
+
db, sessionId, [promptEvent], input, projectDir, "UserPromptSubmit", resolveProjectAttributions,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// 2. Extract decision/role/intent/data from user message
|
|
74
|
+
const userEvents = extractUserEvents(trimmed);
|
|
75
|
+
// Feed lastKnownProjectDir from the first attribution into the second batch
|
|
76
|
+
const savedLastKnown = promptAttributions[0]?.projectDir || null;
|
|
77
|
+
const sessionStats = db.getSessionStats(sessionId);
|
|
78
|
+
const lastKnownProjectDir = typeof db.getLatestAttributedProjectDir === "function"
|
|
79
|
+
? db.getLatestAttributedProjectDir(sessionId)
|
|
80
|
+
: null;
|
|
81
|
+
const userAttributions = resolveProjectAttributions(userEvents, {
|
|
82
|
+
sessionOriginDir: sessionStats?.project_dir || projectDir,
|
|
83
|
+
inputProjectDir: projectDir,
|
|
84
|
+
workspaceRoots: Array.isArray(input.workspace_roots) ? input.workspace_roots : [],
|
|
85
|
+
lastKnownProjectDir: savedLastKnown || lastKnownProjectDir,
|
|
86
|
+
});
|
|
87
|
+
// v1.0.160: route through wire so prompt-derived events (decision /
|
|
88
|
+
// role / intent / data extractions) reach the platform. Previously
|
|
89
|
+
// they only landed in local SessionDB → dashboard's prompt-flow
|
|
90
|
+
// insights stayed at 0.
|
|
91
|
+
if (userEvents.length > 0) {
|
|
92
|
+
attributeAndInsertEvents(
|
|
93
|
+
db,
|
|
94
|
+
sessionId,
|
|
95
|
+
userEvents,
|
|
96
|
+
input,
|
|
97
|
+
projectDir,
|
|
98
|
+
"UserPromptSubmit",
|
|
99
|
+
resolveProjectAttributions,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
db.close();
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// UserPromptSubmit must never block the session — silent fallback
|
|
107
|
+
}
|
|
108
|
+
});
|