context-mode 1.0.136 → 1.0.138
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 +2 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +9 -24
- package/build/adapters/codex/index.js +24 -3
- package/build/adapters/jetbrains-copilot/hooks.d.ts +11 -3
- package/build/adapters/jetbrains-copilot/hooks.js +11 -7
- package/build/adapters/opencode/index.d.ts +1 -0
- package/build/adapters/opencode/index.js +25 -0
- package/build/adapters/opencode/plugin.d.ts +22 -0
- package/build/adapters/opencode/plugin.js +52 -0
- package/build/adapters/pi/extension.js +20 -4
- package/build/adapters/pi/mcp-bridge.d.ts +2 -1
- package/build/adapters/pi/mcp-bridge.js +49 -3
- package/build/adapters/vscode-copilot/hooks.d.ts +27 -3
- package/build/adapters/vscode-copilot/hooks.js +27 -12
- package/build/cli.js +199 -32
- package/build/lifecycle.d.ts +2 -51
- package/build/lifecycle.js +3 -67
- package/build/openclaw-plugin.d.ts +130 -0
- package/build/openclaw-plugin.js +626 -0
- package/build/opencode-plugin.d.ts +122 -0
- package/build/opencode-plugin.js +372 -0
- package/build/pi-extension.d.ts +14 -0
- package/build/pi-extension.js +451 -0
- package/build/server.d.ts +19 -0
- package/build/server.js +145 -59
- package/build/session/db.d.ts +6 -0
- package/build/session/db.js +17 -3
- package/build/util/db-lock.d.ts +65 -0
- package/build/util/db-lock.js +166 -0
- package/build/util/sibling-mcp.d.ts +0 -40
- package/build/util/sibling-mcp.js +11 -116
- package/cli.bundle.mjs +181 -166
- package/configs/kilo/kilo.json +0 -11
- package/configs/opencode/opencode.json +0 -11
- package/hooks/normalize-hooks.mjs +101 -19
- package/hooks/session-db.bundle.mjs +3 -3
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/heal-installed-plugins.mjs +115 -1
- package/scripts/postinstall.mjs +16 -18
- package/server.bundle.mjs +112 -110
- package/start.mjs +11 -14
|
@@ -0,0 +1,451 @@
|
|
|
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 { SessionDB } from "./session/db.js";
|
|
19
|
+
import { extractEvents, extractUserEvents } from "./session/extract.js";
|
|
20
|
+
import { buildResumeSnapshot } from "./session/snapshot.js";
|
|
21
|
+
// ── Pi Tool Name Mapping ─────────────────────────────────
|
|
22
|
+
// Pi uses lowercase; shared extractors expect PascalCase (Claude Code convention).
|
|
23
|
+
const PI_TOOL_MAP = {
|
|
24
|
+
bash: "Bash",
|
|
25
|
+
read: "Read",
|
|
26
|
+
write: "Write",
|
|
27
|
+
edit: "Edit",
|
|
28
|
+
grep: "Grep",
|
|
29
|
+
find: "Glob",
|
|
30
|
+
ls: "Glob",
|
|
31
|
+
};
|
|
32
|
+
// ── Routing patterns ─────────────────────────────────────
|
|
33
|
+
// Inline HTTP client patterns to block in bash — self-contained, no routing module needed.
|
|
34
|
+
const BLOCKED_BASH_PATTERNS = [
|
|
35
|
+
/\bcurl\s/,
|
|
36
|
+
/\bwget\s/,
|
|
37
|
+
/\bfetch\s*\(/,
|
|
38
|
+
/\brequests\.get\s*\(/,
|
|
39
|
+
/\brequests\.post\s*\(/,
|
|
40
|
+
/\bhttp\.get\s*\(/,
|
|
41
|
+
/\bhttp\.request\s*\(/,
|
|
42
|
+
/\burllib\.request/,
|
|
43
|
+
/\bInvoke-WebRequest\b/,
|
|
44
|
+
];
|
|
45
|
+
// ── Module-level DB singleton ────────────────────────────
|
|
46
|
+
let _db = null;
|
|
47
|
+
let _sessionId = "";
|
|
48
|
+
// Per-session gate: routing block injected at most once per session_id.
|
|
49
|
+
const _routingInjected = new Set();
|
|
50
|
+
// Cached routing-block string (built once per process from hooks/routing-block.mjs).
|
|
51
|
+
let _routingBlock = null;
|
|
52
|
+
async function getRoutingBlock(pluginRoot) {
|
|
53
|
+
if (_routingBlock !== null)
|
|
54
|
+
return _routingBlock;
|
|
55
|
+
try {
|
|
56
|
+
const routingMod = await import(pathToFileURL(join(pluginRoot, "hooks", "routing-block.mjs")).href);
|
|
57
|
+
const namingMod = await import(pathToFileURL(join(pluginRoot, "hooks", "core", "tool-naming.mjs")).href);
|
|
58
|
+
const t = namingMod.createToolNamer("pi");
|
|
59
|
+
_routingBlock = String(routingMod.createRoutingBlock(t));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
_routingBlock = "";
|
|
63
|
+
}
|
|
64
|
+
return _routingBlock;
|
|
65
|
+
}
|
|
66
|
+
// Cached buildAutoInjection (500-token cap, prioritized).
|
|
67
|
+
let _buildAutoInjection = undefined;
|
|
68
|
+
async function getAutoInjection(pluginRoot) {
|
|
69
|
+
if (_buildAutoInjection !== undefined)
|
|
70
|
+
return _buildAutoInjection;
|
|
71
|
+
try {
|
|
72
|
+
const mod = await import(pathToFileURL(join(pluginRoot, "hooks", "auto-injection.mjs")).href);
|
|
73
|
+
_buildAutoInjection = mod.buildAutoInjection;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
_buildAutoInjection = null;
|
|
77
|
+
}
|
|
78
|
+
return _buildAutoInjection ?? null;
|
|
79
|
+
}
|
|
80
|
+
// ── Helpers ──────────────────────────────────────────────
|
|
81
|
+
function getSessionDir() {
|
|
82
|
+
const dir = join(homedir(), ".pi", "context-mode", "sessions");
|
|
83
|
+
mkdirSync(dir, { recursive: true });
|
|
84
|
+
return dir;
|
|
85
|
+
}
|
|
86
|
+
function getDBPath() {
|
|
87
|
+
return join(getSessionDir(), "context-mode.db");
|
|
88
|
+
}
|
|
89
|
+
function getOrCreateDB() {
|
|
90
|
+
if (!_db) {
|
|
91
|
+
_db = new SessionDB({ dbPath: getDBPath() });
|
|
92
|
+
}
|
|
93
|
+
return _db;
|
|
94
|
+
}
|
|
95
|
+
/** Derive a stable session ID from Pi's session file path (SHA256, 16 hex chars). */
|
|
96
|
+
function deriveSessionId(ctx) {
|
|
97
|
+
try {
|
|
98
|
+
const sessionManager = ctx.sessionManager;
|
|
99
|
+
const sessionFile = sessionManager?.getSessionFile?.();
|
|
100
|
+
if (sessionFile && typeof sessionFile === "string") {
|
|
101
|
+
return createHash("sha256").update(sessionFile).digest("hex").slice(0, 16);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// best effort
|
|
106
|
+
}
|
|
107
|
+
return `pi-${Date.now()}`;
|
|
108
|
+
}
|
|
109
|
+
/** Build stats text for the /ctx-stats command. */
|
|
110
|
+
function buildStatsText(db, sessionId) {
|
|
111
|
+
try {
|
|
112
|
+
const events = db.getEvents(sessionId);
|
|
113
|
+
const stats = db.getSessionStats(sessionId);
|
|
114
|
+
const lines = [
|
|
115
|
+
"## context-mode stats (Pi)",
|
|
116
|
+
"",
|
|
117
|
+
`- Session: \`${sessionId.slice(0, 8)}...\``,
|
|
118
|
+
`- Events captured: ${events.length}`,
|
|
119
|
+
`- Compactions: ${stats?.compact_count ?? 0}`,
|
|
120
|
+
];
|
|
121
|
+
// Event breakdown by category
|
|
122
|
+
const byCategory = {};
|
|
123
|
+
for (const ev of events) {
|
|
124
|
+
const key = ev.category ?? "unknown";
|
|
125
|
+
byCategory[key] = (byCategory[key] ?? 0) + 1;
|
|
126
|
+
}
|
|
127
|
+
if (Object.keys(byCategory).length > 0) {
|
|
128
|
+
lines.push("- Event breakdown:");
|
|
129
|
+
for (const [category, count] of Object.entries(byCategory)) {
|
|
130
|
+
lines.push(` - ${category}: ${count}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Session age
|
|
134
|
+
if (stats?.started_at) {
|
|
135
|
+
const startedMs = new Date(stats.started_at).getTime();
|
|
136
|
+
const ageMinutes = Math.round((Date.now() - startedMs) / 60_000);
|
|
137
|
+
lines.push(`- Session age: ${ageMinutes}m`);
|
|
138
|
+
}
|
|
139
|
+
return lines.join("\n");
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return "context-mode stats unavailable (session DB error)";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function resolveCommandContext(argsOrCtx, ctx) {
|
|
146
|
+
if (ctx !== undefined)
|
|
147
|
+
return ctx;
|
|
148
|
+
if (argsOrCtx && typeof argsOrCtx === "object")
|
|
149
|
+
return argsOrCtx;
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
function handleCommandText(text, ctx) {
|
|
153
|
+
if (ctx?.hasUI) {
|
|
154
|
+
ctx.ui.notify(text, "info");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
return { text };
|
|
158
|
+
}
|
|
159
|
+
// ── Extension entry point ────────────────────────────────
|
|
160
|
+
/** Pi extension default export. Called once by Pi runtime with the extension API. */
|
|
161
|
+
export default function piExtension(pi) {
|
|
162
|
+
const buildDir = dirname(fileURLToPath(import.meta.url));
|
|
163
|
+
const pluginRoot = resolve(buildDir, "..");
|
|
164
|
+
const projectDir = process.env.PI_PROJECT_DIR || process.cwd();
|
|
165
|
+
const db = getOrCreateDB();
|
|
166
|
+
// ── 1. session_start — Initialize session ──────────────
|
|
167
|
+
pi.on("session_start", (ctx) => {
|
|
168
|
+
try {
|
|
169
|
+
_sessionId = deriveSessionId(ctx ?? {});
|
|
170
|
+
db.ensureSession(_sessionId, projectDir);
|
|
171
|
+
db.cleanupOldSessions(7);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// best effort — never break session start
|
|
175
|
+
if (!_sessionId) {
|
|
176
|
+
_sessionId = `pi-${Date.now()}`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
// ── 2. tool_call — PreToolUse routing enforcement ──────
|
|
181
|
+
// Block bash commands that contain curl/wget/fetch/requests patterns.
|
|
182
|
+
pi.on("tool_call", (event) => {
|
|
183
|
+
try {
|
|
184
|
+
const toolName = String(event?.toolName ?? "").toLowerCase();
|
|
185
|
+
if (toolName !== "bash")
|
|
186
|
+
return;
|
|
187
|
+
const command = String(event?.input?.command ?? "");
|
|
188
|
+
if (!command)
|
|
189
|
+
return;
|
|
190
|
+
const isBlocked = BLOCKED_BASH_PATTERNS.some((p) => p.test(command));
|
|
191
|
+
if (isBlocked) {
|
|
192
|
+
return {
|
|
193
|
+
block: true,
|
|
194
|
+
reason: "Use context-mode MCP tools (execute, fetch_and_index) instead of inline HTTP clients. " +
|
|
195
|
+
"Raw curl/wget/fetch output floods the context window.",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// Routing failure — allow passthrough
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
// ── 3. tool_result — PostToolUse event capture ─────────
|
|
204
|
+
pi.on("tool_result", (event) => {
|
|
205
|
+
try {
|
|
206
|
+
if (!_sessionId)
|
|
207
|
+
return;
|
|
208
|
+
const rawToolName = String(event?.toolName ?? event?.tool_name ?? "");
|
|
209
|
+
const mappedToolName = PI_TOOL_MAP[rawToolName.toLowerCase()] ?? rawToolName;
|
|
210
|
+
// Normalize result to string
|
|
211
|
+
const rawResult = event?.result ?? event?.output;
|
|
212
|
+
const resultStr = typeof rawResult === "string"
|
|
213
|
+
? rawResult
|
|
214
|
+
: rawResult != null
|
|
215
|
+
? JSON.stringify(rawResult)
|
|
216
|
+
: undefined;
|
|
217
|
+
// Detect errors
|
|
218
|
+
const hasError = Boolean(event?.error || event?.isError);
|
|
219
|
+
const hookInput = {
|
|
220
|
+
tool_name: mappedToolName,
|
|
221
|
+
tool_input: event?.params ?? event?.input ?? {},
|
|
222
|
+
tool_response: resultStr,
|
|
223
|
+
tool_output: hasError ? { isError: true } : undefined,
|
|
224
|
+
};
|
|
225
|
+
const events = extractEvents(hookInput);
|
|
226
|
+
if (events.length > 0) {
|
|
227
|
+
for (const ev of events) {
|
|
228
|
+
db.insertEvent(_sessionId, ev, "PostToolUse");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else if (rawToolName) {
|
|
232
|
+
// Fallback: record unrecognized tool call as generic event
|
|
233
|
+
const data = JSON.stringify({
|
|
234
|
+
tool: rawToolName,
|
|
235
|
+
params: event?.params ?? event?.input,
|
|
236
|
+
});
|
|
237
|
+
db.insertEvent(_sessionId, {
|
|
238
|
+
type: "tool_call",
|
|
239
|
+
category: "pi",
|
|
240
|
+
data,
|
|
241
|
+
priority: 1,
|
|
242
|
+
data_hash: createHash("sha256")
|
|
243
|
+
.update(data)
|
|
244
|
+
.digest("hex")
|
|
245
|
+
.slice(0, 16),
|
|
246
|
+
}, "PostToolUse");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// Silent — session capture must never break the tool call
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
// ── 4. before_agent_start — Routing + active_memory + resume injection ─
|
|
254
|
+
pi.on("before_agent_start", async (event) => {
|
|
255
|
+
try {
|
|
256
|
+
if (!_sessionId)
|
|
257
|
+
return;
|
|
258
|
+
const prompt = String(event?.prompt ?? "");
|
|
259
|
+
// Extract user events from the prompt text
|
|
260
|
+
if (prompt) {
|
|
261
|
+
const userEvents = extractUserEvents(prompt);
|
|
262
|
+
for (const ev of userEvents) {
|
|
263
|
+
db.insertEvent(_sessionId, ev, "UserPromptSubmit");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const existingPrompt = String(event?.systemPrompt ?? "");
|
|
267
|
+
const parts = [];
|
|
268
|
+
if (existingPrompt)
|
|
269
|
+
parts.push(existingPrompt);
|
|
270
|
+
// Pi-1: Inject routing block once per session (gated by _routingInjected).
|
|
271
|
+
// v1.0.107 — visible marker so Pi users can verify the routing block
|
|
272
|
+
// reached the model (Mickey-class verification path; mirrors OpenCode).
|
|
273
|
+
if (!_routingInjected.has(_sessionId)) {
|
|
274
|
+
const routingBlock = await getRoutingBlock(pluginRoot);
|
|
275
|
+
if (routingBlock) {
|
|
276
|
+
const marker = `<!-- context-mode: routing block injected (sessionID=${String(_sessionId).slice(0, 8)}) -->`;
|
|
277
|
+
parts.push(marker + "\n" + routingBlock);
|
|
278
|
+
_routingInjected.add(_sessionId);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Pi-3 + Pi-4: Always build active_memory (not just post-compact),
|
|
282
|
+
// capped at 500 tokens via buildAutoInjection. Falls back to inline
|
|
283
|
+
// budget loop if the helper is unavailable.
|
|
284
|
+
const activeEvents = db.getEvents(_sessionId, {
|
|
285
|
+
minPriority: 3,
|
|
286
|
+
limit: 50,
|
|
287
|
+
});
|
|
288
|
+
if (activeEvents.length > 0) {
|
|
289
|
+
const buildAuto = await getAutoInjection(pluginRoot);
|
|
290
|
+
let memoryContext = "";
|
|
291
|
+
if (buildAuto) {
|
|
292
|
+
memoryContext = buildAuto(activeEvents.map((e) => ({
|
|
293
|
+
category: String(e.category ?? ""),
|
|
294
|
+
data: String(e.data ?? ""),
|
|
295
|
+
})));
|
|
296
|
+
}
|
|
297
|
+
// Fallback (or if helper produced empty output): inline 500-token cap.
|
|
298
|
+
if (!memoryContext) {
|
|
299
|
+
const memoryLines = ["<active_memory>"];
|
|
300
|
+
let budget = 2000; // ~500 tokens at 4 chars/token
|
|
301
|
+
for (const ev of activeEvents) {
|
|
302
|
+
const line = ` <event type="${ev.type}" category="${ev.category}">${ev.data}</event>`;
|
|
303
|
+
if (line.length > budget)
|
|
304
|
+
break;
|
|
305
|
+
memoryLines.push(line);
|
|
306
|
+
budget -= line.length;
|
|
307
|
+
}
|
|
308
|
+
memoryLines.push("</active_memory>");
|
|
309
|
+
if (memoryLines.length > 2)
|
|
310
|
+
memoryContext = memoryLines.join("\n");
|
|
311
|
+
}
|
|
312
|
+
if (memoryContext)
|
|
313
|
+
parts.push(memoryContext);
|
|
314
|
+
}
|
|
315
|
+
// Resume snapshot (only when present and unconsumed).
|
|
316
|
+
const resume = db.getResume(_sessionId);
|
|
317
|
+
if (resume && !resume.consumed && resume.snapshot) {
|
|
318
|
+
parts.push(resume.snapshot);
|
|
319
|
+
db.markResumeConsumed(_sessionId);
|
|
320
|
+
}
|
|
321
|
+
// Return modified systemPrompt only if we added something beyond existing.
|
|
322
|
+
const baseLen = existingPrompt ? 1 : 0;
|
|
323
|
+
if (parts.length > baseLen) {
|
|
324
|
+
return { systemPrompt: parts.join("\n\n") };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// best effort — never break agent start
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
// ── 4b. before_provider_response — capture response metadata ───
|
|
332
|
+
// Pi-2: Register the missing event so providers can record latency,
|
|
333
|
+
// model, and token usage when Pi exposes them. Best-effort only;
|
|
334
|
+
// the handler must never throw or modify the response.
|
|
335
|
+
pi.on("before_provider_response", (event) => {
|
|
336
|
+
try {
|
|
337
|
+
if (!_sessionId)
|
|
338
|
+
return;
|
|
339
|
+
const meta = {
|
|
340
|
+
model: event?.model ?? event?.providerModel,
|
|
341
|
+
provider: event?.provider,
|
|
342
|
+
latencyMs: event?.latencyMs ?? event?.latency,
|
|
343
|
+
tokens: event?.usage ?? event?.tokens,
|
|
344
|
+
};
|
|
345
|
+
// Skip when Pi gives us nothing useful — avoids noise in the DB.
|
|
346
|
+
if (meta.model == null &&
|
|
347
|
+
meta.provider == null &&
|
|
348
|
+
meta.latencyMs == null &&
|
|
349
|
+
meta.tokens == null) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const data = JSON.stringify(meta);
|
|
353
|
+
db.insertEvent(_sessionId, {
|
|
354
|
+
type: "provider_response",
|
|
355
|
+
category: "pi",
|
|
356
|
+
data,
|
|
357
|
+
priority: 1,
|
|
358
|
+
data_hash: createHash("sha256").update(data).digest("hex").slice(0, 16),
|
|
359
|
+
}, "PostToolUse");
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
// best effort — never break provider response
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
// ── 5. session_before_compact — Build resume snapshot ──
|
|
366
|
+
pi.on("session_before_compact", () => {
|
|
367
|
+
try {
|
|
368
|
+
if (!_sessionId)
|
|
369
|
+
return;
|
|
370
|
+
const allEvents = db.getEvents(_sessionId);
|
|
371
|
+
if (allEvents.length === 0)
|
|
372
|
+
return;
|
|
373
|
+
const stats = db.getSessionStats(_sessionId);
|
|
374
|
+
const snapshot = buildResumeSnapshot(allEvents, {
|
|
375
|
+
compactCount: (stats?.compact_count ?? 0) + 1,
|
|
376
|
+
});
|
|
377
|
+
db.upsertResume(_sessionId, snapshot, allEvents.length);
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
// best effort — never break compaction
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
// ── 6. session_compact — Increment compact counter ─────
|
|
384
|
+
pi.on("session_compact", () => {
|
|
385
|
+
try {
|
|
386
|
+
if (!_sessionId)
|
|
387
|
+
return;
|
|
388
|
+
db.incrementCompactCount(_sessionId);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// best effort
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
// ── 7. session_shutdown — Cleanup old sessions ─────────
|
|
395
|
+
pi.on("session_shutdown", () => {
|
|
396
|
+
try {
|
|
397
|
+
if (_db) {
|
|
398
|
+
_db.cleanupOldSessions(7);
|
|
399
|
+
}
|
|
400
|
+
_db = null;
|
|
401
|
+
_routingInjected.clear();
|
|
402
|
+
_sessionId = "";
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
// best effort — never throw during shutdown
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
// ── 8. Slash commands ──────────────────────────────────
|
|
409
|
+
pi.registerCommand("ctx-stats", {
|
|
410
|
+
description: "Show context-mode session statistics",
|
|
411
|
+
handler: async (argsOrCtx, maybeCtx) => {
|
|
412
|
+
const ctx = resolveCommandContext(argsOrCtx, maybeCtx);
|
|
413
|
+
const text = !_db || !_sessionId
|
|
414
|
+
? "context-mode: no active session"
|
|
415
|
+
: buildStatsText(_db, _sessionId);
|
|
416
|
+
return handleCommandText(text, ctx);
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
pi.registerCommand("ctx-doctor", {
|
|
420
|
+
description: "Run context-mode diagnostics",
|
|
421
|
+
handler: async (argsOrCtx, maybeCtx) => {
|
|
422
|
+
const ctx = resolveCommandContext(argsOrCtx, maybeCtx);
|
|
423
|
+
const dbPath = getDBPath();
|
|
424
|
+
const dbExists = existsSync(dbPath);
|
|
425
|
+
const lines = [
|
|
426
|
+
"## ctx-doctor (Pi)",
|
|
427
|
+
"",
|
|
428
|
+
`- DB path: \`${dbPath}\``,
|
|
429
|
+
`- DB exists: ${dbExists}`,
|
|
430
|
+
`- Session ID: \`${_sessionId ? _sessionId.slice(0, 8) + "..." : "none"}\``,
|
|
431
|
+
`- Plugin root: \`${pluginRoot}\``,
|
|
432
|
+
`- Project dir: \`${projectDir}\``,
|
|
433
|
+
];
|
|
434
|
+
if (_db && _sessionId) {
|
|
435
|
+
try {
|
|
436
|
+
const stats = _db.getSessionStats(_sessionId);
|
|
437
|
+
const eventCount = _db.getEventCount(_sessionId);
|
|
438
|
+
lines.push(`- Events: ${eventCount}`);
|
|
439
|
+
lines.push(`- Compactions: ${stats?.compact_count ?? 0}`);
|
|
440
|
+
const resume = _db.getResume(_sessionId);
|
|
441
|
+
lines.push(`- Resume snapshot: ${resume ? (resume.consumed ? "consumed" : "available") : "none"}`);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
lines.push("- DB query error");
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const text = lines.join("\n");
|
|
448
|
+
return handleCommandText(text, ctx);
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
}
|
package/build/server.d.ts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
3
|
import { type SpawnSyncOptions, type SpawnSyncReturns } from "node:child_process";
|
|
3
4
|
import { ContentStore } from "./store.js";
|
|
5
|
+
import { type PlatformId } from "./adapters/types.js";
|
|
6
|
+
export declare const server: McpServer;
|
|
7
|
+
export interface RegisteredCtxTool {
|
|
8
|
+
name: string;
|
|
9
|
+
config: Record<string, unknown>;
|
|
10
|
+
handler: (args: Record<string, unknown>) => Promise<unknown> | unknown;
|
|
11
|
+
}
|
|
12
|
+
export declare const REGISTERED_CTX_TOOLS: RegisteredCtxTool[];
|
|
13
|
+
export declare function shouldSuppressMcpToolsForNativePluginHost(opts?: {
|
|
14
|
+
embedded?: string;
|
|
15
|
+
platform?: PlatformId;
|
|
16
|
+
settings?: Record<string, unknown> | null;
|
|
17
|
+
}): boolean;
|
|
18
|
+
type ToolContextOverride = {
|
|
19
|
+
projectDir: string;
|
|
20
|
+
sessionId?: string;
|
|
21
|
+
};
|
|
22
|
+
export declare function withProjectDirOverride<T>(projectDir: string | ToolContextOverride, fn: () => Promise<T>): Promise<T>;
|
|
4
23
|
/**
|
|
5
24
|
* Build the FK-attribution object passed to every ContentStore.index*() call
|
|
6
25
|
* in this process. CLAUDE_SESSION_ID is the only MCP-side handle we have on
|