context-mode 1.0.110 → 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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/index.ts +3 -2
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +152 -34
- package/bin/statusline.mjs +144 -127
- package/build/adapters/base.d.ts +8 -5
- package/build/adapters/base.js +8 -18
- package/build/adapters/claude-code/index.d.ts +24 -3
- package/build/adapters/claude-code/index.js +44 -11
- package/build/adapters/codex/hooks.d.ts +10 -5
- package/build/adapters/codex/hooks.js +10 -5
- package/build/adapters/codex/index.d.ts +17 -5
- package/build/adapters/codex/index.js +337 -37
- package/build/adapters/codex/paths.d.ts +1 -0
- package/build/adapters/codex/paths.js +12 -0
- package/build/adapters/cursor/index.d.ts +6 -0
- package/build/adapters/cursor/index.js +83 -2
- package/build/adapters/detect.d.ts +1 -1
- package/build/adapters/detect.js +29 -6
- package/build/adapters/omp/index.d.ts +65 -0
- package/build/adapters/omp/index.js +182 -0
- package/build/adapters/omp/plugin.d.ts +75 -0
- package/build/adapters/omp/plugin.js +220 -0
- package/build/adapters/openclaw/mcp-tools.d.ts +54 -0
- package/build/adapters/openclaw/mcp-tools.js +198 -0
- package/build/adapters/openclaw/plugin.d.ts +130 -0
- package/build/adapters/openclaw/plugin.js +629 -0
- package/build/adapters/openclaw/workspace-router.d.ts +29 -0
- package/build/adapters/openclaw/workspace-router.js +64 -0
- package/build/adapters/opencode/plugin.d.ts +145 -0
- package/build/adapters/opencode/plugin.js +457 -0
- package/build/adapters/pi/extension.d.ts +26 -0
- package/build/adapters/pi/extension.js +552 -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 +113 -0
- package/build/adapters/pi/mcp-bridge.js +251 -0
- package/build/adapters/types.d.ts +11 -6
- package/build/cli.js +186 -170
- package/build/db-base.d.ts +15 -2
- package/build/db-base.js +50 -5
- package/build/executor.d.ts +2 -0
- package/build/executor.js +15 -2
- package/build/opencode-plugin.js +1 -1
- package/build/runPool.d.ts +36 -0
- package/build/runPool.js +51 -0
- package/build/runtime.js +64 -5
- package/build/search/auto-memory.js +6 -4
- package/build/security.js +30 -10
- package/build/server.d.ts +23 -1
- package/build/server.js +652 -174
- package/build/session/analytics.d.ts +404 -1
- package/build/session/analytics.js +1347 -42
- package/build/session/db.d.ts +114 -5
- package/build/session/db.js +275 -27
- package/build/session/event-emit.d.ts +48 -0
- package/build/session/event-emit.js +101 -0
- package/build/session/extract.d.ts +1 -0
- package/build/session/extract.js +79 -12
- package/build/session/purge.d.ts +111 -0
- package/build/session/purge.js +138 -0
- package/build/store.d.ts +7 -0
- package/build/store.js +69 -6
- package/build/util/claude-config.d.ts +26 -0
- package/build/util/claude-config.js +91 -0
- package/build/util/hook-config.d.ts +4 -0
- package/build/util/hook-config.js +39 -0
- package/cli.bundle.mjs +411 -208
- package/configs/antigravity/GEMINI.md +0 -3
- package/configs/claude-code/CLAUDE.md +1 -4
- package/configs/codex/AGENTS.md +1 -4
- package/configs/codex/config.toml +3 -0
- package/configs/codex/hooks.json +8 -0
- package/configs/cursor/context-mode.mdc +0 -3
- package/configs/gemini-cli/GEMINI.md +0 -3
- package/configs/jetbrains-copilot/copilot-instructions.md +0 -3
- package/configs/kilo/AGENTS.md +0 -3
- package/configs/kiro/KIRO.md +0 -3
- package/configs/omp/SYSTEM.md +85 -0
- package/configs/omp/mcp.json +7 -0
- package/configs/openclaw/AGENTS.md +0 -3
- package/configs/opencode/AGENTS.md +0 -3
- package/configs/pi/AGENTS.md +0 -3
- package/configs/qwen-code/QWEN.md +1 -4
- package/configs/vscode-copilot/copilot-instructions.md +0 -3
- package/configs/zed/AGENTS.md +0 -3
- package/hooks/codex/posttooluse.mjs +9 -2
- package/hooks/codex/precompact.mjs +69 -0
- package/hooks/codex/sessionstart.mjs +13 -9
- package/hooks/codex/stop.mjs +1 -2
- package/hooks/codex/userpromptsubmit.mjs +1 -2
- package/hooks/core/routing.mjs +237 -18
- package/hooks/cursor/afteragentresponse.mjs +1 -1
- package/hooks/cursor/hooks.json +31 -0
- package/hooks/cursor/posttooluse.mjs +1 -1
- package/hooks/cursor/sessionstart.mjs +5 -5
- package/hooks/cursor/stop.mjs +1 -1
- package/hooks/ensure-deps.mjs +12 -13
- package/hooks/gemini-cli/aftertool.mjs +1 -1
- package/hooks/gemini-cli/beforeagent.mjs +1 -1
- package/hooks/gemini-cli/precompress.mjs +3 -2
- package/hooks/gemini-cli/sessionstart.mjs +9 -9
- package/hooks/jetbrains-copilot/posttooluse.mjs +1 -1
- package/hooks/jetbrains-copilot/precompact.mjs +3 -2
- package/hooks/jetbrains-copilot/sessionstart.mjs +9 -9
- package/hooks/kiro/agentspawn.mjs +5 -5
- package/hooks/kiro/posttooluse.mjs +2 -2
- package/hooks/kiro/userpromptsubmit.mjs +1 -1
- package/hooks/posttooluse.mjs +45 -0
- package/hooks/precompact.mjs +17 -0
- package/hooks/pretooluse.mjs +23 -0
- package/hooks/routing-block.mjs +0 -12
- package/hooks/run-hook.mjs +16 -3
- package/hooks/session-db.bundle.mjs +27 -18
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-helpers.mjs +101 -64
- package/hooks/sessionstart.mjs +51 -2
- package/hooks/vscode-copilot/posttooluse.mjs +1 -1
- package/hooks/vscode-copilot/precompact.mjs +3 -2
- package/hooks/vscode-copilot/sessionstart.mjs +9 -9
- package/openclaw.plugin.json +1 -1
- package/package.json +14 -8
- package/server.bundle.mjs +349 -147
- package/skills/UPSTREAM-CREDITS.md +0 -51
- package/skills/context-mode-ops/SKILL.md +0 -299
- package/skills/context-mode-ops/agent-teams.md +0 -198
- package/skills/context-mode-ops/communication.md +0 -224
- package/skills/context-mode-ops/marketing.md +0 -124
- package/skills/context-mode-ops/release.md +0 -214
- package/skills/context-mode-ops/review-pr.md +0 -269
- package/skills/context-mode-ops/tdd.md +0 -329
- package/skills/context-mode-ops/triage-issue.md +0 -266
- package/skills/context-mode-ops/validation.md +0 -307
- package/skills/diagnose/SKILL.md +0 -122
- package/skills/diagnose/scripts/hitl-loop.template.sh +0 -41
- package/skills/grill-me/SKILL.md +0 -15
- package/skills/grill-with-docs/ADR-FORMAT.md +0 -47
- package/skills/grill-with-docs/CONTEXT-FORMAT.md +0 -77
- package/skills/grill-with-docs/SKILL.md +0 -93
- package/skills/improve-codebase-architecture/DEEPENING.md +0 -37
- package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +0 -44
- package/skills/improve-codebase-architecture/LANGUAGE.md +0 -53
- package/skills/improve-codebase-architecture/SKILL.md +0 -76
- package/skills/tdd/SKILL.md +0 -114
- package/skills/tdd/deep-modules.md +0 -33
- package/skills/tdd/interface-design.md +0 -31
- package/skills/tdd/mocking.md +0 -59
- package/skills/tdd/refactoring.md +0 -10
- 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
|
+
}
|