context-mode 1.0.111 → 1.0.112

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.
Files changed (150) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/index.ts +3 -2
  4. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  5. package/.openclaw-plugin/package.json +1 -1
  6. package/README.md +152 -34
  7. package/bin/statusline.mjs +144 -127
  8. package/build/adapters/base.d.ts +8 -5
  9. package/build/adapters/base.js +8 -18
  10. package/build/adapters/claude-code/index.d.ts +24 -3
  11. package/build/adapters/claude-code/index.js +44 -11
  12. package/build/adapters/codex/hooks.d.ts +10 -5
  13. package/build/adapters/codex/hooks.js +10 -5
  14. package/build/adapters/codex/index.d.ts +17 -5
  15. package/build/adapters/codex/index.js +337 -37
  16. package/build/adapters/codex/paths.d.ts +1 -0
  17. package/build/adapters/codex/paths.js +12 -0
  18. package/build/adapters/cursor/index.d.ts +6 -0
  19. package/build/adapters/cursor/index.js +83 -2
  20. package/build/adapters/detect.d.ts +1 -1
  21. package/build/adapters/detect.js +29 -6
  22. package/build/adapters/omp/index.d.ts +65 -0
  23. package/build/adapters/omp/index.js +182 -0
  24. package/build/adapters/omp/plugin.d.ts +75 -0
  25. package/build/adapters/omp/plugin.js +220 -0
  26. package/build/adapters/openclaw/mcp-tools.d.ts +54 -0
  27. package/build/adapters/openclaw/mcp-tools.js +198 -0
  28. package/build/adapters/openclaw/plugin.d.ts +130 -0
  29. package/build/adapters/openclaw/plugin.js +629 -0
  30. package/build/adapters/openclaw/workspace-router.d.ts +29 -0
  31. package/build/adapters/openclaw/workspace-router.js +64 -0
  32. package/build/adapters/opencode/plugin.d.ts +145 -0
  33. package/build/adapters/opencode/plugin.js +457 -0
  34. package/build/adapters/pi/extension.d.ts +26 -0
  35. package/build/adapters/pi/extension.js +552 -0
  36. package/build/adapters/pi/index.d.ts +57 -0
  37. package/build/adapters/pi/index.js +173 -0
  38. package/build/adapters/pi/mcp-bridge.d.ts +113 -0
  39. package/build/adapters/pi/mcp-bridge.js +251 -0
  40. package/build/adapters/types.d.ts +11 -6
  41. package/build/cli.js +186 -170
  42. package/build/db-base.d.ts +15 -2
  43. package/build/db-base.js +50 -5
  44. package/build/executor.d.ts +2 -0
  45. package/build/executor.js +15 -2
  46. package/build/runPool.d.ts +36 -0
  47. package/build/runPool.js +51 -0
  48. package/build/runtime.js +64 -5
  49. package/build/search/auto-memory.js +6 -4
  50. package/build/security.js +30 -10
  51. package/build/server.d.ts +23 -1
  52. package/build/server.js +652 -174
  53. package/build/session/analytics.d.ts +404 -1
  54. package/build/session/analytics.js +1347 -42
  55. package/build/session/db.d.ts +114 -5
  56. package/build/session/db.js +275 -27
  57. package/build/session/event-emit.d.ts +48 -0
  58. package/build/session/event-emit.js +101 -0
  59. package/build/session/extract.d.ts +1 -0
  60. package/build/session/extract.js +79 -12
  61. package/build/session/purge.d.ts +111 -0
  62. package/build/session/purge.js +138 -0
  63. package/build/store.d.ts +7 -0
  64. package/build/store.js +69 -6
  65. package/build/util/claude-config.d.ts +26 -0
  66. package/build/util/claude-config.js +91 -0
  67. package/build/util/hook-config.d.ts +4 -0
  68. package/build/util/hook-config.js +39 -0
  69. package/cli.bundle.mjs +411 -208
  70. package/configs/antigravity/GEMINI.md +0 -3
  71. package/configs/claude-code/CLAUDE.md +1 -4
  72. package/configs/codex/AGENTS.md +1 -4
  73. package/configs/codex/config.toml +3 -0
  74. package/configs/codex/hooks.json +8 -0
  75. package/configs/cursor/context-mode.mdc +0 -3
  76. package/configs/gemini-cli/GEMINI.md +0 -3
  77. package/configs/jetbrains-copilot/copilot-instructions.md +0 -3
  78. package/configs/kilo/AGENTS.md +0 -3
  79. package/configs/kiro/KIRO.md +0 -3
  80. package/configs/omp/SYSTEM.md +85 -0
  81. package/configs/omp/mcp.json +7 -0
  82. package/configs/openclaw/AGENTS.md +0 -3
  83. package/configs/opencode/AGENTS.md +0 -3
  84. package/configs/pi/AGENTS.md +0 -3
  85. package/configs/qwen-code/QWEN.md +1 -4
  86. package/configs/vscode-copilot/copilot-instructions.md +0 -3
  87. package/configs/zed/AGENTS.md +0 -3
  88. package/hooks/codex/posttooluse.mjs +9 -2
  89. package/hooks/codex/precompact.mjs +69 -0
  90. package/hooks/codex/sessionstart.mjs +13 -9
  91. package/hooks/codex/stop.mjs +1 -2
  92. package/hooks/codex/userpromptsubmit.mjs +1 -2
  93. package/hooks/core/routing.mjs +237 -18
  94. package/hooks/cursor/afteragentresponse.mjs +1 -1
  95. package/hooks/cursor/hooks.json +31 -0
  96. package/hooks/cursor/posttooluse.mjs +1 -1
  97. package/hooks/cursor/sessionstart.mjs +5 -5
  98. package/hooks/cursor/stop.mjs +1 -1
  99. package/hooks/ensure-deps.mjs +12 -13
  100. package/hooks/gemini-cli/aftertool.mjs +1 -1
  101. package/hooks/gemini-cli/beforeagent.mjs +1 -1
  102. package/hooks/gemini-cli/precompress.mjs +3 -2
  103. package/hooks/gemini-cli/sessionstart.mjs +9 -9
  104. package/hooks/jetbrains-copilot/posttooluse.mjs +1 -1
  105. package/hooks/jetbrains-copilot/precompact.mjs +3 -2
  106. package/hooks/jetbrains-copilot/sessionstart.mjs +9 -9
  107. package/hooks/kiro/agentspawn.mjs +5 -5
  108. package/hooks/kiro/posttooluse.mjs +2 -2
  109. package/hooks/kiro/userpromptsubmit.mjs +1 -1
  110. package/hooks/posttooluse.mjs +45 -0
  111. package/hooks/precompact.mjs +17 -0
  112. package/hooks/pretooluse.mjs +23 -0
  113. package/hooks/routing-block.mjs +0 -12
  114. package/hooks/run-hook.mjs +16 -3
  115. package/hooks/session-db.bundle.mjs +27 -18
  116. package/hooks/session-extract.bundle.mjs +2 -2
  117. package/hooks/session-helpers.mjs +101 -64
  118. package/hooks/sessionstart.mjs +51 -2
  119. package/hooks/vscode-copilot/posttooluse.mjs +1 -1
  120. package/hooks/vscode-copilot/precompact.mjs +3 -2
  121. package/hooks/vscode-copilot/sessionstart.mjs +9 -9
  122. package/openclaw.plugin.json +1 -1
  123. package/package.json +14 -8
  124. package/server.bundle.mjs +349 -147
  125. package/skills/UPSTREAM-CREDITS.md +0 -51
  126. package/skills/context-mode-ops/SKILL.md +0 -299
  127. package/skills/context-mode-ops/agent-teams.md +0 -198
  128. package/skills/context-mode-ops/communication.md +0 -224
  129. package/skills/context-mode-ops/marketing.md +0 -124
  130. package/skills/context-mode-ops/release.md +0 -214
  131. package/skills/context-mode-ops/review-pr.md +0 -269
  132. package/skills/context-mode-ops/tdd.md +0 -329
  133. package/skills/context-mode-ops/triage-issue.md +0 -266
  134. package/skills/context-mode-ops/validation.md +0 -307
  135. package/skills/diagnose/SKILL.md +0 -122
  136. package/skills/diagnose/scripts/hitl-loop.template.sh +0 -41
  137. package/skills/grill-me/SKILL.md +0 -15
  138. package/skills/grill-with-docs/ADR-FORMAT.md +0 -47
  139. package/skills/grill-with-docs/CONTEXT-FORMAT.md +0 -77
  140. package/skills/grill-with-docs/SKILL.md +0 -93
  141. package/skills/improve-codebase-architecture/DEEPENING.md +0 -37
  142. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +0 -44
  143. package/skills/improve-codebase-architecture/LANGUAGE.md +0 -53
  144. package/skills/improve-codebase-architecture/SKILL.md +0 -76
  145. package/skills/tdd/SKILL.md +0 -114
  146. package/skills/tdd/deep-modules.md +0 -33
  147. package/skills/tdd/interface-design.md +0 -31
  148. package/skills/tdd/mocking.md +0 -59
  149. package/skills/tdd/refactoring.md +0 -10
  150. package/skills/tdd/tests.md +0 -61
@@ -0,0 +1,26 @@
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
+ /**
14
+ * Settles when the MCP bridge bootstrap has finished — resolves on
15
+ * success AND on failure (the bootstrap is best-effort; failures are
16
+ * logged to stderr but never propagated). Exposed for tests so they
17
+ * can `await` the wiring deterministically without relying on internal
18
+ * timing or `setImmediate` polling.
19
+ *
20
+ * Reset to a fresh promise on every `piExtension(pi)` call so repeated
21
+ * registrations in one test process don't see a stale resolution from
22
+ * a prior load.
23
+ */
24
+ export declare let _mcpBridgeReady: Promise<void>;
25
+ /** Pi extension default export. Called once by Pi runtime with the extension API. */
26
+ export default function piExtension(pi: any): void;
@@ -0,0 +1,552 @@
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 { join, resolve, dirname } from "node:path";
16
+ import { fileURLToPath, pathToFileURL } from "node:url";
17
+ import { SessionDB } from "../../session/db.js";
18
+ import { extractEvents, extractUserEvents } from "../../session/extract.js";
19
+ import { buildResumeSnapshot } from "../../session/snapshot.js";
20
+ import { bootstrapMCPTools } from "./mcp-bridge.js";
21
+ import { PiAdapter } from "./index.js";
22
+ // ── Pi Tool Name Mapping ─────────────────────────────────
23
+ // Pi uses lowercase; shared extractors expect PascalCase (Claude Code convention).
24
+ const PI_TOOL_MAP = {
25
+ bash: "Bash",
26
+ read: "Read",
27
+ write: "Write",
28
+ edit: "Edit",
29
+ grep: "Grep",
30
+ find: "Glob",
31
+ ls: "Glob",
32
+ };
33
+ // ── Routing patterns ─────────────────────────────────────
34
+ // Inline HTTP client patterns to block in bash — self-contained, no routing module needed.
35
+ const BLOCKED_BASH_PATTERNS = [
36
+ /\bcurl\s/,
37
+ /\bwget\s/,
38
+ /\bfetch\s*\(/,
39
+ /\brequests\.get\s*\(/,
40
+ /\brequests\.post\s*\(/,
41
+ /\bhttp\.get\s*\(/,
42
+ /\bhttp\.request\s*\(/,
43
+ /\burllib\.request/,
44
+ /\bInvoke-WebRequest\b/,
45
+ ];
46
+ // ── Module-level DB singleton ────────────────────────────
47
+ let _db = null;
48
+ let _sessionId = "";
49
+ // MCP bridge handle. The bridge spawns server.bundle.mjs once and
50
+ // registers each MCP tool through pi.registerTool() so the Pi LLM can
51
+ // actually call ctx_execute / ctx_search / etc. (#426). Pi 0.73.x has
52
+ // no native MCP support, so without this bridge the tools are
53
+ // invisible to the LLM and the routing block is dead weight.
54
+ let _mcpBridge = null;
55
+ /**
56
+ * Settles when the MCP bridge bootstrap has finished — resolves on
57
+ * success AND on failure (the bootstrap is best-effort; failures are
58
+ * logged to stderr but never propagated). Exposed for tests so they
59
+ * can `await` the wiring deterministically without relying on internal
60
+ * timing or `setImmediate` polling.
61
+ *
62
+ * Reset to a fresh promise on every `piExtension(pi)` call so repeated
63
+ * registrations in one test process don't see a stale resolution from
64
+ * a prior load.
65
+ */
66
+ export let _mcpBridgeReady = Promise.resolve();
67
+ // Cached routing-block string (built once per process from hooks/routing-block.mjs).
68
+ let _routingBlock = null;
69
+ async function getRoutingBlock(pluginRoot) {
70
+ if (_routingBlock !== null)
71
+ return _routingBlock;
72
+ try {
73
+ const routingMod = await import(pathToFileURL(join(pluginRoot, "hooks", "routing-block.mjs")).href);
74
+ const namingMod = await import(pathToFileURL(join(pluginRoot, "hooks", "core", "tool-naming.mjs")).href);
75
+ const t = namingMod.createToolNamer("pi");
76
+ _routingBlock = String(routingMod.createRoutingBlock(t));
77
+ }
78
+ catch {
79
+ _routingBlock = "";
80
+ }
81
+ return _routingBlock;
82
+ }
83
+ // Cached buildAutoInjection (500-token cap, prioritized).
84
+ let _buildAutoInjection = undefined;
85
+ async function getAutoInjection(pluginRoot) {
86
+ if (_buildAutoInjection !== undefined)
87
+ return _buildAutoInjection;
88
+ try {
89
+ const mod = await import(pathToFileURL(join(pluginRoot, "hooks", "auto-injection.mjs")).href);
90
+ _buildAutoInjection = mod.buildAutoInjection;
91
+ }
92
+ catch {
93
+ _buildAutoInjection = null;
94
+ }
95
+ return _buildAutoInjection ?? null;
96
+ }
97
+ // ── Helpers ──────────────────────────────────────────────
98
+ // Single PiAdapter instance — owns the canonical session-dir contract
99
+ // (~/.pi/context-mode/sessions). Routing the extension through it means
100
+ // any future segment change in PiAdapter (or BaseAdapter) propagates
101
+ // here automatically instead of silently desyncing (#473 round-3).
102
+ const _piAdapter = new PiAdapter();
103
+ function getSessionDir() {
104
+ const dir = _piAdapter.getSessionDir();
105
+ mkdirSync(dir, { recursive: true });
106
+ return dir;
107
+ }
108
+ function getDBPath() {
109
+ return join(getSessionDir(), "context-mode.db");
110
+ }
111
+ function getOrCreateDB() {
112
+ if (!_db) {
113
+ _db = new SessionDB({ dbPath: getDBPath() });
114
+ }
115
+ return _db;
116
+ }
117
+ /** Derive a stable session ID from Pi's session file path (SHA256, 16 hex chars). */
118
+ function deriveSessionId(ctx) {
119
+ try {
120
+ const sessionManager = ctx.sessionManager;
121
+ const sessionFile = sessionManager?.getSessionFile?.();
122
+ if (sessionFile && typeof sessionFile === "string") {
123
+ return createHash("sha256").update(sessionFile).digest("hex").slice(0, 16);
124
+ }
125
+ }
126
+ catch {
127
+ // best effort
128
+ }
129
+ return `pi-${Date.now()}`;
130
+ }
131
+ /**
132
+ * Parse SessionDB timestamps as UTC. SQLite datetime('now') returns
133
+ * "YYYY-MM-DD HH:MM:SS" in UTC without a timezone suffix; JavaScript parses
134
+ * that shape as local time, which skews ages by the local UTC offset.
135
+ */
136
+ function parseSessionTimestampMs(value) {
137
+ const trimmed = value.trim();
138
+ const sqliteUtc = trimmed.match(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})(\.\d+)?$/);
139
+ const normalized = sqliteUtc
140
+ ? `${sqliteUtc[1]}T${sqliteUtc[2]}${sqliteUtc[3] ?? ""}Z`
141
+ : trimmed;
142
+ return Date.parse(normalized);
143
+ }
144
+ /** Build stats text for the /ctx-stats command. */
145
+ function buildStatsText(db, sessionId) {
146
+ try {
147
+ const events = db.getEvents(sessionId);
148
+ const stats = db.getSessionStats(sessionId);
149
+ const lines = [
150
+ "## context-mode stats (Pi)",
151
+ "",
152
+ `- Session: \`${sessionId.slice(0, 8)}...\``,
153
+ `- Events captured: ${events.length}`,
154
+ `- Compactions: ${stats?.compact_count ?? 0}`,
155
+ ];
156
+ // Event breakdown by category
157
+ const byCategory = {};
158
+ for (const ev of events) {
159
+ const key = ev.category ?? "unknown";
160
+ byCategory[key] = (byCategory[key] ?? 0) + 1;
161
+ }
162
+ if (Object.keys(byCategory).length > 0) {
163
+ lines.push("- Event breakdown:");
164
+ for (const [category, count] of Object.entries(byCategory)) {
165
+ lines.push(` - ${category}: ${count}`);
166
+ }
167
+ }
168
+ // Session age
169
+ if (stats?.started_at) {
170
+ const startedMs = parseSessionTimestampMs(stats.started_at);
171
+ if (Number.isFinite(startedMs)) {
172
+ const ageMinutes = Math.round((Date.now() - startedMs) / 60_000);
173
+ lines.push(`- Session age: ${ageMinutes}m`);
174
+ }
175
+ }
176
+ return lines.join("\n");
177
+ }
178
+ catch {
179
+ return "context-mode stats unavailable (session DB error)";
180
+ }
181
+ }
182
+ function resolveCommandContext(argsOrCtx, ctx) {
183
+ if (ctx !== undefined)
184
+ return ctx;
185
+ if (argsOrCtx && typeof argsOrCtx === "object")
186
+ return argsOrCtx;
187
+ return undefined;
188
+ }
189
+ function handleCommandText(text, ctx) {
190
+ if (ctx?.hasUI) {
191
+ ctx.ui.notify(text, "info");
192
+ return;
193
+ }
194
+ return { text };
195
+ }
196
+ // ── Extension entry point ────────────────────────────────
197
+ /** Pi extension default export. Called once by Pi runtime with the extension API. */
198
+ export default function piExtension(pi) {
199
+ const buildDir = dirname(fileURLToPath(import.meta.url));
200
+ const pluginRoot = resolve(buildDir, "..", "..", "..");
201
+ const projectDir = process.env.PI_PROJECT_DIR || process.cwd();
202
+ const db = getOrCreateDB();
203
+ // ── 1. session_start — Initialize session ──────────────
204
+ pi.on("session_start", (_event, ctx) => {
205
+ try {
206
+ _sessionId = deriveSessionId(ctx ?? {});
207
+ db.ensureSession(_sessionId, projectDir);
208
+ db.cleanupOldSessions(7);
209
+ }
210
+ catch {
211
+ // best effort — never break session start
212
+ if (!_sessionId) {
213
+ _sessionId = `pi-${Date.now()}`;
214
+ }
215
+ }
216
+ });
217
+ // ── 2. tool_call — PreToolUse routing enforcement ──────
218
+ // Block bash commands that contain curl/wget/fetch/requests patterns.
219
+ pi.on("tool_call", (event) => {
220
+ try {
221
+ const toolName = String(event?.toolName ?? "").toLowerCase();
222
+ if (toolName !== "bash")
223
+ return;
224
+ const command = String(event?.input?.command ?? "");
225
+ if (!command)
226
+ return;
227
+ const isBlocked = BLOCKED_BASH_PATTERNS.some((p) => p.test(command));
228
+ if (isBlocked) {
229
+ return {
230
+ block: true,
231
+ reason: "Use context-mode MCP tools (execute, fetch_and_index) instead of inline HTTP clients. " +
232
+ "Raw curl/wget/fetch output floods the context window.",
233
+ };
234
+ }
235
+ }
236
+ catch {
237
+ // Routing failure — allow passthrough
238
+ }
239
+ });
240
+ // ── 3. tool_result — PostToolUse event capture ─────────
241
+ pi.on("tool_result", (event) => {
242
+ try {
243
+ if (!_sessionId)
244
+ return;
245
+ const rawToolName = String(event?.toolName ?? event?.tool_name ?? "");
246
+ const mappedToolName = PI_TOOL_MAP[rawToolName.toLowerCase()] ?? rawToolName;
247
+ // Normalize result to string
248
+ const rawResult = event?.result ?? event?.output;
249
+ const resultStr = typeof rawResult === "string"
250
+ ? rawResult
251
+ : rawResult != null
252
+ ? JSON.stringify(rawResult)
253
+ : undefined;
254
+ // Detect errors
255
+ const hasError = Boolean(event?.error || event?.isError);
256
+ const hookInput = {
257
+ tool_name: mappedToolName,
258
+ tool_input: event?.params ?? event?.input ?? {},
259
+ tool_response: resultStr,
260
+ tool_output: hasError ? { isError: true } : undefined,
261
+ };
262
+ const events = extractEvents(hookInput);
263
+ if (events.length > 0) {
264
+ for (const ev of events) {
265
+ db.insertEvent(_sessionId, ev, "PostToolUse");
266
+ }
267
+ }
268
+ else if (rawToolName) {
269
+ // Fallback: record unrecognized tool call as generic event
270
+ const data = JSON.stringify({
271
+ tool: rawToolName,
272
+ params: event?.params ?? event?.input,
273
+ });
274
+ db.insertEvent(_sessionId, {
275
+ type: "tool_call",
276
+ category: "pi",
277
+ data,
278
+ priority: 1,
279
+ data_hash: createHash("sha256")
280
+ .update(data)
281
+ .digest("hex")
282
+ .slice(0, 16),
283
+ }, "PostToolUse");
284
+ }
285
+ }
286
+ catch {
287
+ // Silent — session capture must never break the tool call
288
+ }
289
+ });
290
+ // ── 4. before_agent_start — Routing + active_memory + resume injection ─
291
+ pi.on("before_agent_start", async (event) => {
292
+ try {
293
+ // Block first agent start until the MCP bridge bootstrap has
294
+ // settled so the LLM call dispatched right after this handler
295
+ // sees the ctx_* tools in Pi's registry. Each subagent starts
296
+ // a fresh `pi --mode json -p --no-session` process whose only
297
+ // window to register tools is the gap between piExtension(pi)
298
+ // returning and the first before_agent_start firing — that gap
299
+ // is too small for the spawn → initialize → tools/list →
300
+ // pi.registerTool round-trip, so without this await the first
301
+ // (and often only) prompt of a subagent goes out with an empty
302
+ // ctx_* registry and the routing block becomes dead weight.
303
+ // Resolves on bootstrap failure too — the bridge is best-effort.
304
+ await _mcpBridgeReady;
305
+ if (!_sessionId)
306
+ return;
307
+ const prompt = String(event?.prompt ?? "");
308
+ // Extract user events from the prompt text
309
+ if (prompt) {
310
+ const userEvents = extractUserEvents(prompt);
311
+ for (const ev of userEvents) {
312
+ db.insertEvent(_sessionId, ev, "UserPromptSubmit");
313
+ }
314
+ }
315
+ const existingPrompt = String(event?.systemPrompt ?? "");
316
+ const parts = [];
317
+ if (existingPrompt)
318
+ parts.push(existingPrompt);
319
+ // Pi-1: Inject routing block every turn.
320
+ // Unlike Claude Code where the SessionStart hook injects once into a persistent
321
+ // context, Pi rebuilds the system prompt fresh on every before_agent_start call.
322
+ // The routing block must be re-injected each turn or it disappears after turn 1.
323
+ const routingBlock = await getRoutingBlock(pluginRoot);
324
+ if (routingBlock) {
325
+ parts.push(routingBlock);
326
+ }
327
+ // Pi-3 + Pi-4: Always build active_memory (not just post-compact),
328
+ // capped at 500 tokens via buildAutoInjection. Falls back to inline
329
+ // budget loop if the helper is unavailable.
330
+ const activeEvents = db.getEvents(_sessionId, {
331
+ minPriority: 3,
332
+ limit: 50,
333
+ });
334
+ if (activeEvents.length > 0) {
335
+ const buildAuto = await getAutoInjection(pluginRoot);
336
+ let memoryContext = "";
337
+ if (buildAuto) {
338
+ memoryContext = buildAuto(activeEvents.map((e) => ({
339
+ category: String(e.category ?? ""),
340
+ data: String(e.data ?? ""),
341
+ })));
342
+ }
343
+ // Fallback (or if helper produced empty output): inline 500-token cap.
344
+ if (!memoryContext) {
345
+ const memoryLines = ["<active_memory>"];
346
+ let budget = 2000; // ~500 tokens at 4 chars/token
347
+ for (const ev of activeEvents) {
348
+ const line = ` <event type="${ev.type}" category="${ev.category}">${ev.data}</event>`;
349
+ if (line.length > budget)
350
+ break;
351
+ memoryLines.push(line);
352
+ budget -= line.length;
353
+ }
354
+ memoryLines.push("</active_memory>");
355
+ if (memoryLines.length > 2)
356
+ memoryContext = memoryLines.join("\n");
357
+ }
358
+ if (memoryContext)
359
+ parts.push(memoryContext);
360
+ }
361
+ // Resume snapshot (only when present and unconsumed).
362
+ const resume = db.getResume(_sessionId);
363
+ if (resume && !resume.consumed && resume.snapshot) {
364
+ parts.push(resume.snapshot);
365
+ db.markResumeConsumed(_sessionId);
366
+ }
367
+ // Return modified systemPrompt only if we added something beyond existing.
368
+ const baseLen = existingPrompt ? 1 : 0;
369
+ if (parts.length > baseLen) {
370
+ return { systemPrompt: parts.join("\n\n") };
371
+ }
372
+ }
373
+ catch {
374
+ // best effort — never break agent start
375
+ }
376
+ });
377
+ // ── 4b. before_provider_response — capture response metadata ───
378
+ // Pi-2: Register the missing event so providers can record latency,
379
+ // model, and token usage when Pi exposes them. Best-effort only;
380
+ // the handler must never throw or modify the response.
381
+ pi.on("before_provider_response", (event) => {
382
+ try {
383
+ if (!_sessionId)
384
+ return;
385
+ const meta = {
386
+ model: event?.model ?? event?.providerModel,
387
+ provider: event?.provider,
388
+ latencyMs: event?.latencyMs ?? event?.latency,
389
+ tokens: event?.usage ?? event?.tokens,
390
+ };
391
+ // Skip when Pi gives us nothing useful — avoids noise in the DB.
392
+ if (meta.model == null &&
393
+ meta.provider == null &&
394
+ meta.latencyMs == null &&
395
+ meta.tokens == null) {
396
+ return;
397
+ }
398
+ const data = JSON.stringify(meta);
399
+ db.insertEvent(_sessionId, {
400
+ type: "provider_response",
401
+ category: "pi",
402
+ data,
403
+ priority: 1,
404
+ data_hash: createHash("sha256").update(data).digest("hex").slice(0, 16),
405
+ }, "PostToolUse");
406
+ }
407
+ catch {
408
+ // best effort — never break provider response
409
+ }
410
+ });
411
+ // ── 5. session_before_compact — Build resume snapshot ──
412
+ pi.on("session_before_compact", () => {
413
+ try {
414
+ if (!_sessionId)
415
+ return;
416
+ const allEvents = db.getEvents(_sessionId);
417
+ if (allEvents.length === 0)
418
+ return;
419
+ const stats = db.getSessionStats(_sessionId);
420
+ const snapshot = buildResumeSnapshot(allEvents, {
421
+ compactCount: (stats?.compact_count ?? 0) + 1,
422
+ });
423
+ db.upsertResume(_sessionId, snapshot, allEvents.length);
424
+ }
425
+ catch {
426
+ // best effort — never break compaction
427
+ }
428
+ });
429
+ // ── 6. session_compact — Increment compact counter ─────
430
+ pi.on("session_compact", () => {
431
+ try {
432
+ if (!_sessionId)
433
+ return;
434
+ db.incrementCompactCount(_sessionId);
435
+ }
436
+ catch {
437
+ // best effort
438
+ }
439
+ });
440
+ // ── 7. session_shutdown — Cleanup old sessions ─────────
441
+ pi.on("session_shutdown", async () => {
442
+ try {
443
+ if (_db) {
444
+ _db.cleanupOldSessions(7);
445
+ }
446
+ _db = null;
447
+ _sessionId = "";
448
+ }
449
+ catch {
450
+ // best effort — never throw during shutdown
451
+ }
452
+ // Race fix (#472 round-3): if shutdown fires while bridge bootstrap
453
+ // is still in flight, _mcpBridge is null at this point and the
454
+ // freshly-spawned MCP child gets orphaned once bootstrap eventually
455
+ // resolves. Await the bootstrap up to a 2s ceiling so we see the
456
+ // real handle, then call shutdown() on it. The ceiling prevents a
457
+ // hung bootstrap (e.g. broken bundle) from blocking session exit.
458
+ try {
459
+ await Promise.race([
460
+ _mcpBridgeReady,
461
+ new Promise((r) => setTimeout(r, 2000).unref()),
462
+ ]);
463
+ }
464
+ catch {
465
+ // _mcpBridgeReady never rejects (best-effort), but defensively
466
+ // swallow anyway so shutdown never throws.
467
+ }
468
+ if (_mcpBridge) {
469
+ try {
470
+ _mcpBridge.shutdown();
471
+ }
472
+ catch {
473
+ // best effort — never throw during shutdown
474
+ }
475
+ _mcpBridge = null;
476
+ }
477
+ });
478
+ // ── 8. Slash commands ──────────────────────────────────
479
+ pi.registerCommand("ctx-stats", {
480
+ description: "Show context-mode session statistics",
481
+ handler: async (argsOrCtx, maybeCtx) => {
482
+ const ctx = resolveCommandContext(argsOrCtx, maybeCtx);
483
+ const text = !_db || !_sessionId
484
+ ? "context-mode: no active session"
485
+ : buildStatsText(_db, _sessionId);
486
+ return handleCommandText(text, ctx);
487
+ },
488
+ });
489
+ pi.registerCommand("ctx-doctor", {
490
+ description: "Run context-mode diagnostics",
491
+ handler: async (argsOrCtx, maybeCtx) => {
492
+ const ctx = resolveCommandContext(argsOrCtx, maybeCtx);
493
+ const dbPath = getDBPath();
494
+ const dbExists = existsSync(dbPath);
495
+ const lines = [
496
+ "## ctx-doctor (Pi)",
497
+ "",
498
+ `- DB path: \`${dbPath}\``,
499
+ `- DB exists: ${dbExists}`,
500
+ `- Session ID: \`${_sessionId ? _sessionId.slice(0, 8) + "..." : "none"}\``,
501
+ `- Plugin root: \`${pluginRoot}\``,
502
+ `- Project dir: \`${projectDir}\``,
503
+ ];
504
+ if (_db && _sessionId) {
505
+ try {
506
+ const stats = _db.getSessionStats(_sessionId);
507
+ const eventCount = _db.getEventCount(_sessionId);
508
+ lines.push(`- Events: ${eventCount}`);
509
+ lines.push(`- Compactions: ${stats?.compact_count ?? 0}`);
510
+ const resume = _db.getResume(_sessionId);
511
+ lines.push(`- Resume snapshot: ${resume ? (resume.consumed ? "consumed" : "available") : "none"}`);
512
+ }
513
+ catch {
514
+ lines.push("- DB query error");
515
+ }
516
+ }
517
+ const text = lines.join("\n");
518
+ return handleCommandText(text, ctx);
519
+ },
520
+ });
521
+ // ── 9. MCP tool bridge (#426) ───────────────────────────
522
+ //
523
+ // Pi 0.73.x has no native MCP support. Without bridging here, the
524
+ // routing block tells the LLM to call ctx_execute / ctx_search / etc.
525
+ // but those tools never appear in Pi's tool list and the LLM cannot
526
+ // reach them — context-mode becomes a pure cost (~2.5K tokens of
527
+ // system-prompt overhead, 0 actual ctx_* calls).
528
+ //
529
+ // Spawn server.bundle.mjs as a long-lived MCP child and register
530
+ // each of its tools via pi.registerTool() so they enter the Pi
531
+ // tool list under their bare names — same names the routing block
532
+ // emits for the Pi platform (per hooks/core/tool-naming.mjs).
533
+ //
534
+ // Best-effort: a missing bundle or a spawn failure must NOT prevent
535
+ // the rest of the extension (session capture, hooks, slash commands)
536
+ // from initializing. We log to stderr and continue.
537
+ const serverBundle = resolve(pluginRoot, "server.bundle.mjs");
538
+ if (existsSync(serverBundle)) {
539
+ _mcpBridgeReady = bootstrapMCPTools(pi, serverBundle).then((handle) => {
540
+ _mcpBridge = handle;
541
+ }, (err) => {
542
+ const msg = err instanceof Error ? err.message : String(err);
543
+ process.stderr.write(`[context-mode] WARNING: failed to bridge MCP tools to Pi (${msg}). ` +
544
+ `ctx_* tools will not be callable from this session.\n`);
545
+ });
546
+ }
547
+ else {
548
+ // No bundle on disk → nothing to await. Tests can still rely on
549
+ // _mcpBridgeReady being a settled promise.
550
+ _mcpBridgeReady = Promise.resolve();
551
+ }
552
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * adapters/pi — Pi coding agent platform adapter.
3
+ *
4
+ * Implements HookAdapter for Pi's MCP-only paradigm at the adapter layer.
5
+ *
6
+ * Pi hook specifics:
7
+ * - NO JSON-stdio hooks. Pi exposes a JS-callback runtime API
8
+ * (`pi.on("session_start", fn)`, `pi.on("tool_call", fn)`, …) which is
9
+ * wired DIRECTLY by `src/adapters/pi/extension.ts`. The HookAdapter
10
+ * contract here intentionally reports `mcp-only` and all-false
11
+ * capabilities so harness paths that walk the JSON-stdio matrix do not
12
+ * try to register stdio hooks for Pi.
13
+ * - Config root: ~/.pi/
14
+ * - Settings: ~/.pi/settings.json (kept lightweight — Pi does not
15
+ * prescribe a canonical settings file, but several internal tools
16
+ * write one; using settings.json keeps parity with Claude Code).
17
+ * - Session dir: ~/.pi/context-mode/sessions/ (parallel to ~/.claude/,
18
+ * ~/.omp/) — this is the data-isolation contract from issue #473.
19
+ * - Instruction file: AGENTS.md (per configs/pi/AGENTS.md).
20
+ *
21
+ * Why a dedicated adapter is mandatory:
22
+ * Before this adapter existed, `getAdapter("pi")` fell through to the
23
+ * `default` arm of the switch in `src/adapters/detect.ts` and returned a
24
+ * ClaudeCodeAdapter. Pi sessions therefore wrote DBs and event logs into
25
+ * `~/.claude/context-mode/sessions/`, contaminating Claude Code state and
26
+ * silently leaking Pi user data into the wrong storage root (issue #473
27
+ * follow-up). The OMP adapter fixed the same class of bug for OMP; this
28
+ * adapter closes the gap for Pi.
29
+ */
30
+ import { BaseAdapter } from "../base.js";
31
+ import type { HookAdapter, HookParadigm, PlatformCapabilities, DiagnosticResult, PreToolUseEvent, PostToolUseEvent, PreCompactEvent, SessionStartEvent, PreToolUseResponse, PostToolUseResponse, PreCompactResponse, SessionStartResponse, HookRegistration } from "../types.js";
32
+ export declare class PiAdapter extends BaseAdapter implements HookAdapter {
33
+ constructor();
34
+ readonly name = "Pi";
35
+ readonly paradigm: HookParadigm;
36
+ readonly capabilities: PlatformCapabilities;
37
+ parsePreToolUseInput(_raw: unknown): PreToolUseEvent;
38
+ parsePostToolUseInput(_raw: unknown): PostToolUseEvent;
39
+ parsePreCompactInput(_raw: unknown): PreCompactEvent;
40
+ parseSessionStartInput(_raw: unknown): SessionStartEvent;
41
+ formatPreToolUseResponse(_response: PreToolUseResponse): unknown;
42
+ formatPostToolUseResponse(_response: PostToolUseResponse): unknown;
43
+ formatPreCompactResponse(_response: PreCompactResponse): unknown;
44
+ formatSessionStartResponse(_response: SessionStartResponse): unknown;
45
+ getSettingsPath(): string;
46
+ getInstructionFiles(): string[];
47
+ generateHookConfig(_pluginRoot: string): HookRegistration;
48
+ readSettings(): Record<string, unknown> | null;
49
+ writeSettings(settings: Record<string, unknown>): void;
50
+ validateHooks(_pluginRoot: string): DiagnosticResult[];
51
+ checkPluginRegistration(): DiagnosticResult;
52
+ getInstalledVersion(): string;
53
+ configureAllHooks(_pluginRoot: string): string[];
54
+ setHookPermissions(_pluginRoot: string): string[];
55
+ updatePluginRegistry(_pluginRoot: string, _version: string): void;
56
+ getRoutingInstructions(): string;
57
+ }