@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,861 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi coding agent extension for context-mode.
|
|
3
|
+
*
|
|
4
|
+
* Follows the OpenClaw adapter pattern: imports shared session modules,
|
|
5
|
+
* registers Pi-specific hooks. NO copy-paste of session logic.
|
|
6
|
+
* NO external npm dependencies beyond what Pi runtime provides.
|
|
7
|
+
*
|
|
8
|
+
* Entry point: `export default function(pi: ExtensionAPI) { ... }`
|
|
9
|
+
*
|
|
10
|
+
* Lifecycle: session_start, tool_call, tool_result, before_agent_start,
|
|
11
|
+
* session_before_compact, session_compact, session_shutdown.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { join, resolve, dirname } from "node:path";
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
|
+
import { resolveSessionDbPath, SessionDB } from "../../session/db.js";
|
|
19
|
+
import { extractEvents, extractUserEvents, parsePiUsage, buildAgentUsageEvent } from "../../session/extract.js";
|
|
20
|
+
import { buildResumeSnapshot } from "../../session/snapshot.js";
|
|
21
|
+
import { bootstrapMCPTools, makeBridgeDiag, isForegroundSession } from "./mcp-bridge.js";
|
|
22
|
+
import { PiAdapter } from "./index.js";
|
|
23
|
+
// ── Pi Tool Name Mapping ─────────────────────────────────
|
|
24
|
+
// Pi uses lowercase; shared extractors expect PascalCase (Claude Code convention).
|
|
25
|
+
const PI_TOOL_MAP = {
|
|
26
|
+
bash: "Bash",
|
|
27
|
+
read: "Read",
|
|
28
|
+
write: "Write",
|
|
29
|
+
edit: "Edit",
|
|
30
|
+
grep: "Grep",
|
|
31
|
+
find: "Glob",
|
|
32
|
+
ls: "Glob",
|
|
33
|
+
};
|
|
34
|
+
// ── Routing patterns ─────────────────────────────────────
|
|
35
|
+
// Inline HTTP client patterns to block in bash — self-contained, no routing module needed.
|
|
36
|
+
//
|
|
37
|
+
// Issue #625 — split into two classes so we can apply different policies:
|
|
38
|
+
//
|
|
39
|
+
// * BLOCKED_HTTP_PATTERNS — language-level HTTP calls (fetch, requests, http,
|
|
40
|
+
// urllib, Invoke-WebRequest). These always flood context with raw response
|
|
41
|
+
// bodies, so they remain unconditionally blocked.
|
|
42
|
+
//
|
|
43
|
+
// * curl / wget — handled separately by isSafeCurlWget() below. Mirrors the
|
|
44
|
+
// reference logic in hooks/core/routing.mjs:660–722. Silent + file-output
|
|
45
|
+
// forms are allowed as an MCP-down escape hatch (the body never enters
|
|
46
|
+
// context). Unsafe forms (stdout, verbose, missing file output) still
|
|
47
|
+
// block. This prevents an unrecoverable session trap when the bridge dies.
|
|
48
|
+
//
|
|
49
|
+
// Both are evaluated AFTER stripQuotedContent() so quoted CLI arguments
|
|
50
|
+
// (e.g. `gh issue list --search "curl wget"`) no longer false-positive.
|
|
51
|
+
const BLOCKED_HTTP_PATTERNS = [
|
|
52
|
+
/\bfetch\s*\(/,
|
|
53
|
+
/\brequests\.get\s*\(/,
|
|
54
|
+
/\brequests\.post\s*\(/,
|
|
55
|
+
/\bhttp\.get\s*\(/,
|
|
56
|
+
/\bhttp\.request\s*\(/,
|
|
57
|
+
/\burllib\.request/,
|
|
58
|
+
/\bInvoke-WebRequest\b/,
|
|
59
|
+
];
|
|
60
|
+
/**
|
|
61
|
+
* Strip heredoc + single-quoted + double-quoted content from a shell command
|
|
62
|
+
* so the routing regex only sees command tokens, not user-provided strings.
|
|
63
|
+
*
|
|
64
|
+
* Mirrors hooks/core/routing.mjs:196–209. Inlined here because the Pi
|
|
65
|
+
* extension is bundled as a standalone build artifact (.pi/extensions/...)
|
|
66
|
+
* and cannot import hooks/core/* at runtime — they live in a sibling tree
|
|
67
|
+
* and may not be present in every Pi installation.
|
|
68
|
+
*
|
|
69
|
+
* Exported for unit tests.
|
|
70
|
+
*/
|
|
71
|
+
export function stripQuotedContent(cmd) {
|
|
72
|
+
return cmd
|
|
73
|
+
.replace(/<<-?\s*["']?(\w+)["']?[\s\S]*?\n\s*\1/g, "") // heredocs
|
|
74
|
+
.replace(/'[^']*'/g, "''") // single-quoted
|
|
75
|
+
.replace(/"[^"]*"/g, '""'); // double-quoted
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Returns true iff `segment` is a curl/wget invocation that is SAFE to allow
|
|
79
|
+
* through the Pi routing block — i.e. it cannot flood the model's context
|
|
80
|
+
* window because the response body is written to disk (or appended to a file)
|
|
81
|
+
* and no verbose/trace flag is dumping headers to stderr.
|
|
82
|
+
*
|
|
83
|
+
* Mirrors hooks/core/routing.mjs:672–701. Segments that are NOT curl/wget
|
|
84
|
+
* return `true` (nothing to evaluate). The caller is expected to split chained
|
|
85
|
+
* commands on `&&`, `||`, `;` and call this per segment.
|
|
86
|
+
*
|
|
87
|
+
* Issue #625 — without this, the only escape hatch when the MCP bridge dies
|
|
88
|
+
* is `gh` CLI or a full Pi restart. Neither is acceptable as baseline UX.
|
|
89
|
+
*
|
|
90
|
+
* Exported for unit tests.
|
|
91
|
+
*/
|
|
92
|
+
export function isSafeCurlWget(segment) {
|
|
93
|
+
const s = segment.trim();
|
|
94
|
+
const isCurl = /\bcurl\b/i.test(s);
|
|
95
|
+
const isWget = /\bwget\b/i.test(s);
|
|
96
|
+
if (!isCurl && !isWget)
|
|
97
|
+
return true; // not curl/wget — nothing to evaluate
|
|
98
|
+
// Check for file output flags (-o file / --output file for curl,
|
|
99
|
+
// -O file / --output-document file for wget) OR shell redirection (> / >>).
|
|
100
|
+
const hasFileOutput = isCurl
|
|
101
|
+
? /\s(-o|--output)\s/.test(s) || /\s>\s*/.test(s) || /\s>>\s*/.test(s)
|
|
102
|
+
: /\s(-O|--output-document)\s/.test(s) ||
|
|
103
|
+
/\s>\s*/.test(s) ||
|
|
104
|
+
/\s>>\s*/.test(s);
|
|
105
|
+
if (!hasFileOutput)
|
|
106
|
+
return false; // no file output → body flows to stdout
|
|
107
|
+
// Stdout aliases: -o -, -o /dev/stdout, -O -, -O /dev/stdout.
|
|
108
|
+
if (isCurl && /\s(-o|--output)\s+(-|\/dev\/stdout)(\s|$)/.test(s))
|
|
109
|
+
return false;
|
|
110
|
+
if (isWget && /\s(-O|--output-document)\s+(-|\/dev\/stdout)(\s|$)/.test(s))
|
|
111
|
+
return false;
|
|
112
|
+
// Verbose/trace flags dump request+response headers to stderr → context.
|
|
113
|
+
if (/\s(-v|--verbose|--trace)\b/.test(s))
|
|
114
|
+
return false;
|
|
115
|
+
// Must be silent (curl: -s/--silent, wget: -q/--quiet) so the progress bar
|
|
116
|
+
// does not spill into stderr → context.
|
|
117
|
+
const isSilent = isCurl
|
|
118
|
+
? /\s-[a-zA-Z]*s|--silent/.test(s)
|
|
119
|
+
: /\s-[a-zA-Z]*q|--quiet/.test(s);
|
|
120
|
+
return isSilent;
|
|
121
|
+
}
|
|
122
|
+
// ── Module-level DB singleton ────────────────────────────
|
|
123
|
+
let _db = null;
|
|
124
|
+
let _dbPath = "";
|
|
125
|
+
let _sessionId = "";
|
|
126
|
+
// MCP bridge handle. The bridge spawns server.bundle.mjs once and
|
|
127
|
+
// registers each MCP tool through pi.registerTool() so the Pi LLM can
|
|
128
|
+
// actually call ctx_execute / ctx_search / etc. (#426). Pi 0.73.x has
|
|
129
|
+
// no native MCP support, so without this bridge the tools are
|
|
130
|
+
// invisible to the LLM and the routing block is dead weight.
|
|
131
|
+
let _mcpBridge = null;
|
|
132
|
+
/**
|
|
133
|
+
* Settles when the MCP bridge bootstrap has finished — resolves on
|
|
134
|
+
* success AND on failure (the bootstrap is best-effort; failures are
|
|
135
|
+
* logged to stderr but never propagated). Exposed for tests so they
|
|
136
|
+
* can `await` the wiring deterministically without relying on internal
|
|
137
|
+
* timing or `setImmediate` polling.
|
|
138
|
+
*
|
|
139
|
+
* Starts as an already-settled promise because the bridge is now bootstrapped
|
|
140
|
+
* lazily from `before_agent_start`, not during extension discovery.
|
|
141
|
+
*/
|
|
142
|
+
export let _mcpBridgeReady = Promise.resolve();
|
|
143
|
+
// Cached buildAutoInjection (500-token cap, prioritized).
|
|
144
|
+
let _buildAutoInjection = undefined;
|
|
145
|
+
// Pending context to inject via the 'context' hook (avoiding systemPrompt mutation
|
|
146
|
+
// which breaks prefix prompt cache on DeepSeek/Anthropic/OpenAI).
|
|
147
|
+
// See: https://github.com/mksglu/context-mode/issues/598
|
|
148
|
+
let _pendingContext = "";
|
|
149
|
+
async function getAutoInjection(pluginRoot) {
|
|
150
|
+
if (_buildAutoInjection !== undefined)
|
|
151
|
+
return _buildAutoInjection;
|
|
152
|
+
try {
|
|
153
|
+
const mod = await import(pathToFileURL(join(pluginRoot, "hooks", "auto-injection.mjs")).href);
|
|
154
|
+
_buildAutoInjection = mod.buildAutoInjection;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
_buildAutoInjection = null;
|
|
158
|
+
}
|
|
159
|
+
return _buildAutoInjection ?? null;
|
|
160
|
+
}
|
|
161
|
+
// ── Helpers ──────────────────────────────────────────────
|
|
162
|
+
// Single PiAdapter instance — owns the canonical session-dir contract
|
|
163
|
+
// (~/.pi/context-mode/sessions). Routing the extension through it means
|
|
164
|
+
// any future segment change in PiAdapter (or BaseAdapter) propagates
|
|
165
|
+
// here automatically instead of silently desyncing (#473 round-3).
|
|
166
|
+
const _piAdapter = new PiAdapter();
|
|
167
|
+
function getSessionDir() {
|
|
168
|
+
const dir = _piAdapter.getSessionDir();
|
|
169
|
+
mkdirSync(dir, { recursive: true });
|
|
170
|
+
return dir;
|
|
171
|
+
}
|
|
172
|
+
// Issue #645 — the MCP server (src/server.ts ctx_stats / ctx_search
|
|
173
|
+
// timeline) resolves the SessionDB filename via
|
|
174
|
+
// `resolveSessionDbPath({ projectDir, sessionsDir })`, which produces a
|
|
175
|
+
// per-project canonical `<16-hex-hash>.db` (case-folded on darwin/win32,
|
|
176
|
+
// suffixed for non-main worktrees). The Pi extension previously wrote
|
|
177
|
+
// every session to a shared `context-mode.db` literal — a different
|
|
178
|
+
// file the server never reads. The result was silent degradation of
|
|
179
|
+
// `ctx_stats` (zero history) and `ctx_search(sort: "timeline")` (sort
|
|
180
|
+
// dropped) for every Pi user. Routing through the same helper keeps the
|
|
181
|
+
// extension-side writes and the server-side reads aligned across
|
|
182
|
+
// case-fold migrations, worktree suffixes, and any future change to the
|
|
183
|
+
// canonical filename contract.
|
|
184
|
+
function getDBPath(projectDir) {
|
|
185
|
+
return resolveSessionDbPath({ projectDir, sessionsDir: getSessionDir() });
|
|
186
|
+
}
|
|
187
|
+
function getOrCreateDB(projectDir) {
|
|
188
|
+
// Reopen the singleton if the resolved DB path changes. Production code
|
|
189
|
+
// normally loads the extension once per process with a single workspace,
|
|
190
|
+
// but defensive re-keying on path keeps the contract honest if a host
|
|
191
|
+
// ever calls piExtension(pi) twice with different projectDirs, and
|
|
192
|
+
// removes a subtle test-isolation foot-gun where stale singletons
|
|
193
|
+
// pointed at a prior test's `<hash>.db`. (#645)
|
|
194
|
+
const dbPath = getDBPath(projectDir);
|
|
195
|
+
if (!_db || _dbPath !== dbPath) {
|
|
196
|
+
if (_db) {
|
|
197
|
+
try {
|
|
198
|
+
_db.close();
|
|
199
|
+
}
|
|
200
|
+
catch { /* best effort */ }
|
|
201
|
+
}
|
|
202
|
+
_db = new SessionDB({ dbPath });
|
|
203
|
+
_dbPath = dbPath;
|
|
204
|
+
}
|
|
205
|
+
return _db;
|
|
206
|
+
}
|
|
207
|
+
/** Derive a stable session ID from Pi's session file path (SHA256, 16 hex chars). */
|
|
208
|
+
function deriveSessionId(ctx) {
|
|
209
|
+
try {
|
|
210
|
+
const sessionManager = ctx.sessionManager;
|
|
211
|
+
const sessionFile = sessionManager?.getSessionFile?.();
|
|
212
|
+
if (sessionFile && typeof sessionFile === "string") {
|
|
213
|
+
return createHash("sha256").update(sessionFile).digest("hex").slice(0, 16);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// best effort
|
|
218
|
+
}
|
|
219
|
+
return `pi-${Date.now()}`;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Parse SessionDB timestamps as UTC. SQLite datetime('now') returns
|
|
223
|
+
* "YYYY-MM-DD HH:MM:SS" in UTC without a timezone suffix; JavaScript parses
|
|
224
|
+
* that shape as local time, which skews ages by the local UTC offset.
|
|
225
|
+
*/
|
|
226
|
+
function parseSessionTimestampMs(value) {
|
|
227
|
+
const trimmed = value.trim();
|
|
228
|
+
const sqliteUtc = trimmed.match(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})(\.\d+)?$/);
|
|
229
|
+
const normalized = sqliteUtc
|
|
230
|
+
? `${sqliteUtc[1]}T${sqliteUtc[2]}${sqliteUtc[3] ?? ""}Z`
|
|
231
|
+
: trimmed;
|
|
232
|
+
return Date.parse(normalized);
|
|
233
|
+
}
|
|
234
|
+
/** Build stats text for the /ctx-stats command. */
|
|
235
|
+
function buildStatsText(db, sessionId) {
|
|
236
|
+
try {
|
|
237
|
+
const events = db.getEvents(sessionId);
|
|
238
|
+
const stats = db.getSessionStats(sessionId);
|
|
239
|
+
const lines = [
|
|
240
|
+
"## context-mode stats (Pi)",
|
|
241
|
+
"",
|
|
242
|
+
`- Session: \`${sessionId.slice(0, 8)}...\``,
|
|
243
|
+
`- Events captured: ${events.length}`,
|
|
244
|
+
`- Compactions: ${stats?.compact_count ?? 0}`,
|
|
245
|
+
];
|
|
246
|
+
// Event breakdown by category
|
|
247
|
+
const byCategory = {};
|
|
248
|
+
for (const ev of events) {
|
|
249
|
+
const key = ev.category ?? "unknown";
|
|
250
|
+
byCategory[key] = (byCategory[key] ?? 0) + 1;
|
|
251
|
+
}
|
|
252
|
+
if (Object.keys(byCategory).length > 0) {
|
|
253
|
+
lines.push("- Event breakdown:");
|
|
254
|
+
for (const [category, count] of Object.entries(byCategory)) {
|
|
255
|
+
lines.push(` - ${category}: ${count}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Session age
|
|
259
|
+
if (stats?.started_at) {
|
|
260
|
+
const startedMs = parseSessionTimestampMs(stats.started_at);
|
|
261
|
+
if (Number.isFinite(startedMs)) {
|
|
262
|
+
const ageMinutes = Math.round((Date.now() - startedMs) / 60_000);
|
|
263
|
+
lines.push(`- Session age: ${ageMinutes}m`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return lines.join("\n");
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return "context-mode stats unavailable (session DB error)";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function resolveCommandContext(argsOrCtx, ctx) {
|
|
273
|
+
if (ctx !== undefined)
|
|
274
|
+
return ctx;
|
|
275
|
+
if (argsOrCtx && typeof argsOrCtx === "object")
|
|
276
|
+
return argsOrCtx;
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
function handleCommandText(text, ctx) {
|
|
280
|
+
if (ctx?.hasUI) {
|
|
281
|
+
ctx.ui.notify(text, "info");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
return { text };
|
|
285
|
+
}
|
|
286
|
+
// ── Pi MCP bridge lazy bootstrap (#534, #809) ───────────
|
|
287
|
+
//
|
|
288
|
+
// Pi loads extensions in several CLI paths that never dispatch an agent turn:
|
|
289
|
+
// top-level help/version, package management (`install`, `list`, ...), config,
|
|
290
|
+
// metadata commands, and project-trust probing. Bootstrapping the MCP bridge
|
|
291
|
+
// during extension discovery is therefore the wrong signal: there may be no
|
|
292
|
+
// real agent lifecycle and no `session_shutdown` cleanup. That caused both the
|
|
293
|
+
// #534 short-lived help/version orphan class and the #809 package-command hang
|
|
294
|
+
// (the bridge child's stdio handles kept Pi alive after `install`/`list`).
|
|
295
|
+
//
|
|
296
|
+
// The robust signal is Pi's agent lifecycle itself. `before_agent_start` fires
|
|
297
|
+
// only for invocations that are about to dispatch a model call, including
|
|
298
|
+
// print-mode subagents (`pi --mode json -p --no-session`). We start and await
|
|
299
|
+
// the bridge from that hook so ctx_* tools are present before Pi snapshots the
|
|
300
|
+
// tool registry, while CLI-only commands never spawn a bridge at all.
|
|
301
|
+
function startPiMCPBridge(pi, serverBundle, shouldKeepHandle, foreground) {
|
|
302
|
+
if (existsSync(serverBundle)) {
|
|
303
|
+
_mcpBridgeReady = bootstrapMCPTools(pi, serverBundle, { foreground }).then((handle) => {
|
|
304
|
+
if (shouldKeepHandle()) {
|
|
305
|
+
_mcpBridge = handle;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
// Bootstrap completed after this extension registration had already
|
|
309
|
+
// shut down or superseded the attempt. Do not publish a stale handle;
|
|
310
|
+
// immediately reclaim the child that bootstrap just spawned.
|
|
311
|
+
try {
|
|
312
|
+
handle.shutdown();
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
// best effort — never throw from best-effort bridge cleanup
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}, (err) => {
|
|
319
|
+
if (!shouldKeepHandle())
|
|
320
|
+
return;
|
|
321
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
322
|
+
// #868: route to Pi's file logger, never process.stderr (raw-mode TUI).
|
|
323
|
+
makeBridgeDiag(pi)(`[context-mode] WARNING: failed to bridge MCP tools to Pi (${msg}). ` +
|
|
324
|
+
`ctx_* tools will not be callable from this session.`);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
// No bundle on disk → nothing to await. Tests can still rely on
|
|
329
|
+
// _mcpBridgeReady being a settled promise.
|
|
330
|
+
_mcpBridgeReady = Promise.resolve();
|
|
331
|
+
}
|
|
332
|
+
return _mcpBridgeReady;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Issue #545 — Pi workspace resolver.
|
|
336
|
+
*
|
|
337
|
+
* Pi's runtime sets PI_CONFIG_DIR to ~/.pi (its CONFIG dir, not the user's
|
|
338
|
+
* project). The extension previously used this as the project anchor, which
|
|
339
|
+
* meant every Pi session re-rooted under ~/.pi — collapsing all of a user's
|
|
340
|
+
* projects into a single phantom workspace. This helper picks the user's
|
|
341
|
+
* actual project directory while NEVER returning a path equal to or under
|
|
342
|
+
* ~/.pi/.
|
|
343
|
+
*
|
|
344
|
+
* Cascade:
|
|
345
|
+
* 1. PI_WORKSPACE_DIR — set by Pi's bridge (extension-set, freshest)
|
|
346
|
+
* 2. PI_PROJECT_DIR — legacy/user override
|
|
347
|
+
* 3. PWD — shell-set, survives process.chdir
|
|
348
|
+
* 4. cwd — last resort
|
|
349
|
+
*
|
|
350
|
+
* Each candidate is rejected if it equals ~/.pi or lives under ~/.pi/. If
|
|
351
|
+
* every candidate is poisoned, falls back to homedir() as a safe non-config
|
|
352
|
+
* anchor — caller may still render a "no project context" notice but the
|
|
353
|
+
* function stays total.
|
|
354
|
+
*/
|
|
355
|
+
export function resolvePiWorkspaceDir(opts) {
|
|
356
|
+
const home = opts.home ?? homedir();
|
|
357
|
+
const piConfigDir = join(home, ".pi");
|
|
358
|
+
const isUnderPi = (p) => {
|
|
359
|
+
if (!p)
|
|
360
|
+
return true;
|
|
361
|
+
if (p === piConfigDir)
|
|
362
|
+
return true;
|
|
363
|
+
// Match both POSIX (/) and Windows (\) child-of relations.
|
|
364
|
+
return p.startsWith(piConfigDir + "/") || p.startsWith(piConfigDir + "\\");
|
|
365
|
+
};
|
|
366
|
+
const candidates = [
|
|
367
|
+
opts.env.PI_WORKSPACE_DIR,
|
|
368
|
+
opts.env.PI_PROJECT_DIR,
|
|
369
|
+
opts.pwd,
|
|
370
|
+
opts.cwd,
|
|
371
|
+
];
|
|
372
|
+
for (const c of candidates) {
|
|
373
|
+
if (c && !isUnderPi(c))
|
|
374
|
+
return c;
|
|
375
|
+
}
|
|
376
|
+
return home;
|
|
377
|
+
}
|
|
378
|
+
// ── Extension entry point ────────────────────────────────
|
|
379
|
+
/** Pi extension default export. Called once by Pi runtime with the extension API. */
|
|
380
|
+
export default function piExtension(pi) {
|
|
381
|
+
const buildDir = dirname(fileURLToPath(import.meta.url));
|
|
382
|
+
const pluginRoot = resolve(buildDir, "..", "..", "..");
|
|
383
|
+
const serverBundle = resolve(pluginRoot, "server.bundle.mjs");
|
|
384
|
+
let mcpBridgeStarted = false;
|
|
385
|
+
let mcpBridgeGeneration = 0;
|
|
386
|
+
const ensureMCPBridge = (foreground) => {
|
|
387
|
+
if (mcpBridgeStarted)
|
|
388
|
+
return _mcpBridgeReady;
|
|
389
|
+
mcpBridgeStarted = true;
|
|
390
|
+
const generation = ++mcpBridgeGeneration;
|
|
391
|
+
return startPiMCPBridge(pi, serverBundle, () => mcpBridgeStarted && mcpBridgeGeneration === generation, foreground);
|
|
392
|
+
};
|
|
393
|
+
// Issue #545 — Pi workspace resolver. PI_CONFIG_DIR is Pi's CONFIG dir
|
|
394
|
+
// (~/.pi), NOT the user's workspace; using it as the project anchor
|
|
395
|
+
// collapsed every Pi session into a single phantom workspace. The
|
|
396
|
+
// dedicated resolver picks PI_WORKSPACE_DIR > PI_PROJECT_DIR > PWD > cwd
|
|
397
|
+
// and refuses to return any path under ~/.pi/.
|
|
398
|
+
const projectDir = resolvePiWorkspaceDir({
|
|
399
|
+
env: process.env,
|
|
400
|
+
pwd: process.env.PWD,
|
|
401
|
+
cwd: process.cwd(),
|
|
402
|
+
});
|
|
403
|
+
// Attribution object for project isolation — ensures every event recorded
|
|
404
|
+
// by the pi adapter carries the correct project_dir. Without this, all
|
|
405
|
+
// events default to project_dir="" which causes cross-project data leakage
|
|
406
|
+
// in shared SessionDB instances.
|
|
407
|
+
const _attribution = { projectDir, source: "workspace_root", confidence: 0.98 };
|
|
408
|
+
const db = getOrCreateDB(projectDir);
|
|
409
|
+
// ── 1. session_start — Initialize session ──────────────
|
|
410
|
+
pi.on("session_start", (_event, ctx) => {
|
|
411
|
+
try {
|
|
412
|
+
_sessionId = deriveSessionId(ctx ?? {});
|
|
413
|
+
db.ensureSession(_sessionId, projectDir);
|
|
414
|
+
db.cleanupOldSessions(7);
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
// best effort — never break session start
|
|
418
|
+
if (!_sessionId) {
|
|
419
|
+
_sessionId = `pi-${Date.now()}`;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
// ── 2. tool_call — PreToolUse routing enforcement ──────
|
|
424
|
+
// Block bash commands that contain curl/wget/fetch/requests patterns.
|
|
425
|
+
pi.on("tool_call", (event) => {
|
|
426
|
+
try {
|
|
427
|
+
const toolName = String(event?.toolName ?? "").toLowerCase();
|
|
428
|
+
if (toolName !== "bash")
|
|
429
|
+
return;
|
|
430
|
+
const command = String(event?.input?.command ?? "");
|
|
431
|
+
if (!command)
|
|
432
|
+
return;
|
|
433
|
+
// Issue #625 — strip quoted content first so words like `curl` inside
|
|
434
|
+
// a `gh issue list --search "...curl..."` argument do not false-positive.
|
|
435
|
+
const stripped = stripQuotedContent(command);
|
|
436
|
+
// Language-level HTTP calls (fetch, requests, http, urllib,
|
|
437
|
+
// Invoke-WebRequest) always flood context — block unconditionally.
|
|
438
|
+
if (BLOCKED_HTTP_PATTERNS.some((p) => p.test(stripped))) {
|
|
439
|
+
return {
|
|
440
|
+
block: true,
|
|
441
|
+
reason: "Use context-mode MCP tools (execute, fetch_and_index) instead of inline HTTP clients. " +
|
|
442
|
+
"Raw fetch/requests/http output floods the context window.",
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
// curl / wget — split chained command on &&, ||, ; and evaluate each
|
|
446
|
+
// segment with isSafeCurlWget(). Allowed if EVERY curl/wget segment is
|
|
447
|
+
// silent + file-output + no verbose + no stdout alias. This preserves
|
|
448
|
+
// the "do not flood context" intent while leaving a recovery path open
|
|
449
|
+
// when the MCP bridge is unreachable.
|
|
450
|
+
if (/(^|\s|&&|\||\;)(curl|wget)\s/i.test(stripped)) {
|
|
451
|
+
const segments = stripped.split(/\s*(?:&&|\|\||;)\s*/);
|
|
452
|
+
const hasUnsafeSegment = segments.some((seg) => !isSafeCurlWget(seg));
|
|
453
|
+
if (hasUnsafeSegment) {
|
|
454
|
+
return {
|
|
455
|
+
block: true,
|
|
456
|
+
reason: "Use context-mode MCP tools (execute, fetch_and_index) instead of inline HTTP clients. " +
|
|
457
|
+
"Raw curl/wget output floods the context window. " +
|
|
458
|
+
"For an MCP-down escape hatch, use silent + file output: " +
|
|
459
|
+
"`curl -s -o /tmp/x.json URL` or `wget -q -O /tmp/x.json URL`.",
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
// Routing failure — allow passthrough
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
// ── 3. tool_result — PostToolUse event capture ─────────
|
|
469
|
+
pi.on("tool_result", (event) => {
|
|
470
|
+
try {
|
|
471
|
+
if (!_sessionId)
|
|
472
|
+
return;
|
|
473
|
+
const rawToolName = String(event?.toolName ?? event?.tool_name ?? "");
|
|
474
|
+
let mappedToolName = PI_TOOL_MAP[rawToolName.toLowerCase()] ?? rawToolName;
|
|
475
|
+
// Pi namespaces MCP-registered tools with the "context_mode_" prefix;
|
|
476
|
+
// the extract functions expect the "mcp__" prefix for MCP tool calls.
|
|
477
|
+
if (/^context_mode_/.test(rawToolName)) {
|
|
478
|
+
mappedToolName = rawToolName.replace(/^context_mode_/, "mcp__context_mode__");
|
|
479
|
+
}
|
|
480
|
+
// Normalize result to string
|
|
481
|
+
const rawResult = event?.result ?? event?.output;
|
|
482
|
+
const resultStr = typeof rawResult === "string"
|
|
483
|
+
? rawResult
|
|
484
|
+
: rawResult != null
|
|
485
|
+
? JSON.stringify(rawResult)
|
|
486
|
+
: undefined;
|
|
487
|
+
// Detect errors
|
|
488
|
+
const hasError = Boolean(event?.error || event?.isError);
|
|
489
|
+
// Pi sends file tool parameters as "path"; the extract functions
|
|
490
|
+
// expect "file_path" (Claude Code convention). Normalise before
|
|
491
|
+
// passing to extractEvents so file reads/writes/edits are tracked.
|
|
492
|
+
const rawInput = { ...(event?.params ?? event?.input ?? {}) };
|
|
493
|
+
if (rawInput.path !== undefined && rawInput.file_path === undefined) {
|
|
494
|
+
rawInput.file_path = String(rawInput.path);
|
|
495
|
+
}
|
|
496
|
+
const hookInput = {
|
|
497
|
+
tool_name: mappedToolName,
|
|
498
|
+
tool_input: rawInput,
|
|
499
|
+
tool_response: resultStr,
|
|
500
|
+
tool_output: hasError ? { isError: true } : undefined,
|
|
501
|
+
};
|
|
502
|
+
const events = extractEvents(hookInput);
|
|
503
|
+
if (events.length > 0) {
|
|
504
|
+
for (const ev of events) {
|
|
505
|
+
db.insertEvent(_sessionId, ev, "PostToolUse", _attribution);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
else if (rawToolName) {
|
|
509
|
+
// Fallback: record unrecognized tool call as generic event
|
|
510
|
+
const data = JSON.stringify({
|
|
511
|
+
tool: rawToolName,
|
|
512
|
+
params: event?.params ?? event?.input,
|
|
513
|
+
});
|
|
514
|
+
db.insertEvent(_sessionId, {
|
|
515
|
+
type: "tool_call",
|
|
516
|
+
category: "pi",
|
|
517
|
+
data,
|
|
518
|
+
priority: 1,
|
|
519
|
+
data_hash: createHash("sha256")
|
|
520
|
+
.update(data)
|
|
521
|
+
.digest("hex")
|
|
522
|
+
.slice(0, 16),
|
|
523
|
+
}, "PostToolUse", _attribution);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
// Silent — session capture must never break the tool call
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
// ── 4. before_agent_start — Routing + active_memory + resume injection ─
|
|
531
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
532
|
+
try {
|
|
533
|
+
_pendingContext = ""; // Reset — will be filled below if events exist
|
|
534
|
+
// Lazily start and await the MCP bridge only when Pi is about to
|
|
535
|
+
// dispatch a real agent turn. This is the non-brittle #534/#809 guard:
|
|
536
|
+
// help/version/package/config CLI paths may load the extension, but they
|
|
537
|
+
// never fire before_agent_start, so they never spawn server.bundle.mjs.
|
|
538
|
+
// Subagents (`pi --mode json -p --no-session`) do fire this hook; awaiting
|
|
539
|
+
// here ensures ctx_* tools are registered before Pi snapshots the tool
|
|
540
|
+
// registry for the model call. Resolves on bootstrap failure too — the
|
|
541
|
+
// bridge is best-effort.
|
|
542
|
+
//
|
|
543
|
+
// #868: the FOREGROUND interactive session's bridge child is spawned with
|
|
544
|
+
// the #854 idle reaper disabled (via ctx.hasUI), so a multi-minute human
|
|
545
|
+
// pause never drops its ctx_* tools. Subagents (hasUI:false) keep the
|
|
546
|
+
// reaper so abandoned children can't accumulate (#854).
|
|
547
|
+
//
|
|
548
|
+
// INVARIANT — deciding foreground on the FIRST before_agent_start is safe
|
|
549
|
+
// even though the bridge spawns single-flight (first-wins, no sticky
|
|
550
|
+
// latch): Pi wires the interactive uiContext inside `mode.init()`, which
|
|
551
|
+
// main.ts AWAITS before dispatching the first prompt — and
|
|
552
|
+
// before_agent_start is emitted only from the per-turn prompt path. So the
|
|
553
|
+
// foreground session's first hook ALWAYS observes hasUI:true; subagents are
|
|
554
|
+
// provably hasUI:false. There is no early-init window where the foreground
|
|
555
|
+
// transiently reads hasUI:false (that window is scoped to a separate,
|
|
556
|
+
// buffered credential event). Do NOT add a latch here — it would guard an
|
|
557
|
+
// unreachable state. (Verified against oh-my-pi: main.ts init→prompt order,
|
|
558
|
+
// interactive-mode.ts uiContext wiring, executor.ts subagent hasUI:false.)
|
|
559
|
+
await ensureMCPBridge(isForegroundSession(ctx));
|
|
560
|
+
if (!_sessionId)
|
|
561
|
+
return;
|
|
562
|
+
const prompt = String(event?.prompt ?? "");
|
|
563
|
+
// Extract user events from the prompt text
|
|
564
|
+
if (prompt) {
|
|
565
|
+
const userEvents = extractUserEvents(prompt);
|
|
566
|
+
for (const ev of userEvents) {
|
|
567
|
+
db.insertEvent(_sessionId, ev, "UserPromptSubmit", _attribution);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const existingPrompt = String(event?.systemPrompt ?? "");
|
|
571
|
+
const parts = [];
|
|
572
|
+
if (existingPrompt)
|
|
573
|
+
parts.push(existingPrompt);
|
|
574
|
+
// Pi-1: Lightweight routing anchor — 7KB routing block is too heavy
|
|
575
|
+
// for Pi's context budget. Tool descriptions from pi.registerTool()
|
|
576
|
+
// already tell the model what each tool does. This anchor gives the
|
|
577
|
+
// deliberate choice (which tool for which scenario) without the full
|
|
578
|
+
// block/redirect/memory/tool-selection hierarchy.
|
|
579
|
+
parts.push("context-mode active. Hierarchy: ctx_batch_execute > ctx_execute > ctx_execute_file > ctx_search. " +
|
|
580
|
+
"Read/edit files → ctx_execute_file. Multi-command research → ctx_batch_execute. " +
|
|
581
|
+
"Web pages → ctx_fetch_and_index then ctx_search. Index docs → ctx_index. " +
|
|
582
|
+
"Stats → ctx_stats. Doctor → ctx_doctor. Upgrade → ctx_upgrade. Purge → ctx_purge.");
|
|
583
|
+
// Pi-3 + Pi-4: Always build active_memory (not just post-compact),
|
|
584
|
+
// capped at 500 tokens via buildAutoInjection. Falls back to inline
|
|
585
|
+
// budget loop if the helper is unavailable.
|
|
586
|
+
//
|
|
587
|
+
// Issue #856 — do NOT re-inject `role` as a standing behavioral_directive
|
|
588
|
+
// on every turn. A casual past phrase that classified as a role would
|
|
589
|
+
// otherwise be pinned and replayed each turn ("since you said 'that's
|
|
590
|
+
// fine for now', I'll leave it"), producing a do-nothing loop. Defense in
|
|
591
|
+
// depth: even if a stale `role` event exists (from an older build, or a
|
|
592
|
+
// genuine persona the user has since moved past), it must not become an
|
|
593
|
+
// inescapable per-turn standing order. Role events stay in the DB and
|
|
594
|
+
// remain queryable via ctx_search(source: "session-events"); intent,
|
|
595
|
+
// skills, decisions, and the resume snapshot are unaffected.
|
|
596
|
+
const activeEvents = db
|
|
597
|
+
.getEvents(_sessionId, {
|
|
598
|
+
minPriority: 3,
|
|
599
|
+
limit: 50,
|
|
600
|
+
})
|
|
601
|
+
.filter((e) => String(e.category ?? "") !== "role");
|
|
602
|
+
if (activeEvents.length > 0) {
|
|
603
|
+
const buildAuto = await getAutoInjection(pluginRoot);
|
|
604
|
+
let memoryContext = "";
|
|
605
|
+
if (buildAuto) {
|
|
606
|
+
memoryContext = buildAuto(activeEvents.map((e) => ({
|
|
607
|
+
category: String(e.category ?? ""),
|
|
608
|
+
data: String(e.data ?? ""),
|
|
609
|
+
})));
|
|
610
|
+
}
|
|
611
|
+
// Fallback (or if helper produced empty output): inline 500-token cap.
|
|
612
|
+
if (!memoryContext) {
|
|
613
|
+
const memoryLines = ["<active_memory>"];
|
|
614
|
+
let budget = 2000; // ~500 tokens at 4 chars/token
|
|
615
|
+
for (const ev of activeEvents) {
|
|
616
|
+
const line = ` <event type="${ev.type}" category="${ev.category}">${ev.data}</event>`;
|
|
617
|
+
if (line.length > budget)
|
|
618
|
+
break;
|
|
619
|
+
memoryLines.push(line);
|
|
620
|
+
budget -= line.length;
|
|
621
|
+
}
|
|
622
|
+
memoryLines.push("</active_memory>");
|
|
623
|
+
if (memoryLines.length > 2)
|
|
624
|
+
memoryContext = memoryLines.join("\n");
|
|
625
|
+
}
|
|
626
|
+
if (memoryContext)
|
|
627
|
+
parts.push(memoryContext);
|
|
628
|
+
}
|
|
629
|
+
// Resume snapshot (only when present and unconsumed).
|
|
630
|
+
const resume = db.getResume(_sessionId);
|
|
631
|
+
if (resume && !resume.consumed && resume.snapshot) {
|
|
632
|
+
parts.push(resume.snapshot);
|
|
633
|
+
db.markResumeConsumed(_sessionId);
|
|
634
|
+
}
|
|
635
|
+
// Store extra context (routing anchor, active_memory, resume, behavioralDirective)
|
|
636
|
+
// for injection via the 'context' hook as a message, NOT as a systemPrompt
|
|
637
|
+
// modification. Mutating systemPrompt breaks prefix prompt caching on
|
|
638
|
+
// DeepSeek/Anthropic/OpenAI because the system message sits at messages[0]
|
|
639
|
+
// and any change invalidates the entire cache chain.
|
|
640
|
+
const baseLen = existingPrompt ? 1 : 0;
|
|
641
|
+
if (parts.length > baseLen) {
|
|
642
|
+
const extraParts = parts.slice(baseLen);
|
|
643
|
+
_pendingContext = extraParts.join("\n\n");
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
_pendingContext = "";
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
_pendingContext = ""; // Reset — ensure no stale data escapes
|
|
651
|
+
// best effort — never break agent start
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
// ── 4a2. context — Inject active_memory + resume + behavioralDirective as message ──
|
|
655
|
+
// Uses the 'context' hook (like hindsight does) to append context at the END of
|
|
656
|
+
// messages rather than mutating systemPrompt at the beginning. This preserves
|
|
657
|
+
// prefix prompt cache for DeepSeek, Anthropic, and OpenAI.
|
|
658
|
+
pi.on("context", (event) => {
|
|
659
|
+
try {
|
|
660
|
+
if (!_pendingContext)
|
|
661
|
+
return;
|
|
662
|
+
const ctx = _pendingContext;
|
|
663
|
+
_pendingContext = "";
|
|
664
|
+
event.messages.push({
|
|
665
|
+
role: "user",
|
|
666
|
+
content: ctx,
|
|
667
|
+
});
|
|
668
|
+
return { messages: event.messages };
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
// best effort — never break context assembly
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
// ── 4b. before_provider_response — capture response metadata ───
|
|
675
|
+
// Pi-2: Register the missing event so providers can record latency,
|
|
676
|
+
// model, and token usage when Pi exposes them. Best-effort only;
|
|
677
|
+
// the handler must never throw or modify the response.
|
|
678
|
+
pi.on("before_provider_response", (event) => {
|
|
679
|
+
try {
|
|
680
|
+
if (!_sessionId)
|
|
681
|
+
return;
|
|
682
|
+
const meta = {
|
|
683
|
+
model: event?.model ?? event?.providerModel,
|
|
684
|
+
provider: event?.provider,
|
|
685
|
+
latencyMs: event?.latencyMs ?? event?.latency,
|
|
686
|
+
tokens: event?.usage ?? event?.tokens,
|
|
687
|
+
};
|
|
688
|
+
// Skip when Pi gives us nothing useful — avoids noise in the DB.
|
|
689
|
+
if (meta.model == null &&
|
|
690
|
+
meta.provider == null &&
|
|
691
|
+
meta.latencyMs == null &&
|
|
692
|
+
meta.tokens == null) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const data = JSON.stringify(meta);
|
|
696
|
+
db.insertEvent(_sessionId, {
|
|
697
|
+
type: "provider_response",
|
|
698
|
+
category: "pi",
|
|
699
|
+
data,
|
|
700
|
+
priority: 1,
|
|
701
|
+
data_hash: createHash("sha256").update(data).digest("hex").slice(0, 16),
|
|
702
|
+
}, "PostToolUse", _attribution);
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
// best effort — never break provider response
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
// ── 4c. turn_end — per-turn token + native-USD cost capture ───
|
|
709
|
+
//
|
|
710
|
+
// Pi delivers per-turn usage on TurnEndEvent.message (an AssistantMessage):
|
|
711
|
+
// usage.{input,output,cacheRead,cacheWrite} + native usage.cost.total in USD,
|
|
712
|
+
// with model on .model. Usage is per-turn incremental, so each turn_end maps
|
|
713
|
+
// to exactly one structured `agent_usage` (category "cost") event — the same
|
|
714
|
+
// shape the Claude Code Stop path emits via buildAgentUsageEvent. We pass
|
|
715
|
+
// Pi's native cost as native_cost_usd so the builder trusts the source over
|
|
716
|
+
// the local price table (cost_confidence: HIGH — no price-table maintenance).
|
|
717
|
+
//
|
|
718
|
+
// Refs: adapter-matrix/pi.md @320261f — shared-events.ts:204-209 (TurnEndEvent),
|
|
719
|
+
// ai/src/types.ts:510/521 (model/usage), catalog/src/types.ts:100-145 (Usage).
|
|
720
|
+
// Best-effort: parse is null-safe and the handler never throws (a telemetry
|
|
721
|
+
// forwarder must never break the agent turn).
|
|
722
|
+
pi.on("turn_end", (event) => {
|
|
723
|
+
try {
|
|
724
|
+
if (!_sessionId)
|
|
725
|
+
return;
|
|
726
|
+
const counts = parsePiUsage(event);
|
|
727
|
+
if (!counts)
|
|
728
|
+
return; // non-assistant turn or all-zero usage
|
|
729
|
+
const ev = buildAgentUsageEvent(counts);
|
|
730
|
+
if (!ev)
|
|
731
|
+
return;
|
|
732
|
+
// db.insertEvent is the extension-side analog of the .mjs hooks'
|
|
733
|
+
// attributeAndInsertEvents (insert + project attribution). The MCP
|
|
734
|
+
// server forwards persisted agent_usage events to the platform.
|
|
735
|
+
db.insertEvent(_sessionId, ev, "Stop", _attribution);
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
// best effort — never break the agent turn
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
// ── 5. session_before_compact — Build resume snapshot ──
|
|
742
|
+
pi.on("session_before_compact", () => {
|
|
743
|
+
try {
|
|
744
|
+
if (!_sessionId)
|
|
745
|
+
return;
|
|
746
|
+
const allEvents = db.getEvents(_sessionId);
|
|
747
|
+
if (allEvents.length === 0)
|
|
748
|
+
return;
|
|
749
|
+
const stats = db.getSessionStats(_sessionId);
|
|
750
|
+
const snapshot = buildResumeSnapshot(allEvents, {
|
|
751
|
+
compactCount: (stats?.compact_count ?? 0) + 1,
|
|
752
|
+
});
|
|
753
|
+
db.upsertResume(_sessionId, snapshot, allEvents.length);
|
|
754
|
+
}
|
|
755
|
+
catch {
|
|
756
|
+
// best effort — never break compaction
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
// ── 6. session_compact — Increment compact counter ─────
|
|
760
|
+
pi.on("session_compact", () => {
|
|
761
|
+
try {
|
|
762
|
+
if (!_sessionId)
|
|
763
|
+
return;
|
|
764
|
+
db.incrementCompactCount(_sessionId);
|
|
765
|
+
}
|
|
766
|
+
catch {
|
|
767
|
+
// best effort
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
// ── 7. session_shutdown — Cleanup old sessions ─────────
|
|
771
|
+
pi.on("session_shutdown", async () => {
|
|
772
|
+
try {
|
|
773
|
+
if (_db) {
|
|
774
|
+
_db.cleanupOldSessions(7);
|
|
775
|
+
}
|
|
776
|
+
_db = null;
|
|
777
|
+
_dbPath = "";
|
|
778
|
+
_sessionId = "";
|
|
779
|
+
}
|
|
780
|
+
catch {
|
|
781
|
+
// best effort — never throw during shutdown
|
|
782
|
+
}
|
|
783
|
+
// Race fix (#472 round-3 + #809 lazy follow-up): if shutdown fires while
|
|
784
|
+
// bridge bootstrap is still in flight, _mcpBridge may be null at this
|
|
785
|
+
// point. Invalidate this bootstrap generation before waiting so any handle
|
|
786
|
+
// that resolves after the 2s ceiling self-shuts down instead of publishing
|
|
787
|
+
// a stale child handle after session shutdown.
|
|
788
|
+
mcpBridgeGeneration++;
|
|
789
|
+
mcpBridgeStarted = false;
|
|
790
|
+
try {
|
|
791
|
+
await Promise.race([
|
|
792
|
+
_mcpBridgeReady,
|
|
793
|
+
new Promise((r) => setTimeout(r, 2000).unref()),
|
|
794
|
+
]);
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
// _mcpBridgeReady never rejects (best-effort), but defensively
|
|
798
|
+
// swallow anyway so shutdown never throws.
|
|
799
|
+
}
|
|
800
|
+
if (_mcpBridge) {
|
|
801
|
+
try {
|
|
802
|
+
_mcpBridge.shutdown();
|
|
803
|
+
}
|
|
804
|
+
catch {
|
|
805
|
+
// best effort — never throw during shutdown
|
|
806
|
+
}
|
|
807
|
+
_mcpBridge = null;
|
|
808
|
+
}
|
|
809
|
+
_mcpBridgeReady = Promise.resolve();
|
|
810
|
+
});
|
|
811
|
+
// ── 8. Slash commands ──────────────────────────────────
|
|
812
|
+
pi.registerCommand("ctx-stats", {
|
|
813
|
+
description: "Show context-mode session statistics",
|
|
814
|
+
handler: async (argsOrCtx, maybeCtx) => {
|
|
815
|
+
const ctx = resolveCommandContext(argsOrCtx, maybeCtx);
|
|
816
|
+
const text = !_db || !_sessionId
|
|
817
|
+
? "context-mode: no active session"
|
|
818
|
+
: buildStatsText(_db, _sessionId);
|
|
819
|
+
return handleCommandText(text, ctx);
|
|
820
|
+
},
|
|
821
|
+
});
|
|
822
|
+
pi.registerCommand("ctx-doctor", {
|
|
823
|
+
description: "Run context-mode diagnostics",
|
|
824
|
+
handler: async (argsOrCtx, maybeCtx) => {
|
|
825
|
+
const ctx = resolveCommandContext(argsOrCtx, maybeCtx);
|
|
826
|
+
const dbPath = getDBPath(projectDir);
|
|
827
|
+
const dbExists = existsSync(dbPath);
|
|
828
|
+
const lines = [
|
|
829
|
+
"## ctx-doctor (Pi)",
|
|
830
|
+
"",
|
|
831
|
+
`- DB path: \`${dbPath}\``,
|
|
832
|
+
`- DB exists: ${dbExists}`,
|
|
833
|
+
`- Session ID: \`${_sessionId ? _sessionId.slice(0, 8) + "..." : "none"}\``,
|
|
834
|
+
`- Plugin root: \`${pluginRoot}\``,
|
|
835
|
+
`- Project dir: \`${projectDir}\``,
|
|
836
|
+
];
|
|
837
|
+
if (_db && _sessionId) {
|
|
838
|
+
try {
|
|
839
|
+
const stats = _db.getSessionStats(_sessionId);
|
|
840
|
+
const eventCount = _db.getEventCount(_sessionId);
|
|
841
|
+
lines.push(`- Events: ${eventCount}`);
|
|
842
|
+
lines.push(`- Compactions: ${stats?.compact_count ?? 0}`);
|
|
843
|
+
const resume = _db.getResume(_sessionId);
|
|
844
|
+
lines.push(`- Resume snapshot: ${resume ? (resume.consumed ? "consumed" : "available") : "none"}`);
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
lines.push("- DB query error");
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const text = lines.join("\n");
|
|
851
|
+
return handleCommandText(text, ctx);
|
|
852
|
+
},
|
|
853
|
+
});
|
|
854
|
+
// ── 9. MCP tool bridge (#426) ───────────────────────────
|
|
855
|
+
//
|
|
856
|
+
// Intentionally no eager bootstrap here. `before_agent_start` is the first
|
|
857
|
+
// lifecycle signal that proves Pi is about to run a model call; starting the
|
|
858
|
+
// bridge there keeps ctx_* available for real agent turns while package/help
|
|
859
|
+
// commands that only load extensions never spawn a long-lived child (#809).
|
|
860
|
+
_mcpBridgeReady = Promise.resolve();
|
|
861
|
+
}
|