context-mode 1.0.23 → 1.0.26

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/build/cli.js CHANGED
@@ -14,7 +14,7 @@
14
14
  import * as p from "@clack/prompts";
15
15
  import color from "picocolors";
16
16
  import { execSync } from "node:child_process";
17
- import { readFileSync, cpSync, accessSync, existsSync, readdirSync, rmSync, closeSync, openSync, constants } from "node:fs";
17
+ import { readFileSync, writeFileSync, cpSync, accessSync, existsSync, readdirSync, rmSync, closeSync, openSync, constants } from "node:fs";
18
18
  import { request as httpsRequest } from "node:https";
19
19
  import { resolve, dirname, join } from "node:path";
20
20
  import { tmpdir, devNull } from "node:os";
@@ -50,6 +50,10 @@ const HOOK_MAP = {
50
50
  posttooluse: "hooks/cursor/posttooluse.mjs",
51
51
  sessionstart: "hooks/cursor/sessionstart.mjs",
52
52
  },
53
+ "kiro": {
54
+ pretooluse: "hooks/kiro/pretooluse.mjs",
55
+ posttooluse: "hooks/kiro/posttooluse.mjs",
56
+ },
53
57
  };
54
58
  async function hookDispatch(platform, event) {
55
59
  // Suppress stderr at OS fd level — native C++ modules (better-sqlite3) write
@@ -393,7 +397,7 @@ async function upgrade() {
393
397
  }
394
398
  const items = [
395
399
  "build", "src", "hooks", "skills", ".claude-plugin",
396
- "start.mjs", "server.bundle.mjs", "cli.bundle.mjs", "package.json", ".mcp.json",
400
+ "start.mjs", "server.bundle.mjs", "cli.bundle.mjs", "package.json",
397
401
  ];
398
402
  for (const item of items) {
399
403
  try {
@@ -402,6 +406,16 @@ async function upgrade() {
402
406
  }
403
407
  catch { /* some files may not exist in source */ }
404
408
  }
409
+ // Write .mcp.json with resolved absolute path (fixes #132)
410
+ const mcpConfig = {
411
+ mcpServers: {
412
+ "context-mode": {
413
+ command: "node",
414
+ args: [resolve(pluginRoot, "start.mjs")],
415
+ },
416
+ },
417
+ };
418
+ writeFileSync(resolve(pluginRoot, ".mcp.json"), JSON.stringify(mcpConfig, null, 2) + "\n");
405
419
  s.stop(color.green(`Updated in-place to v${newVersion}`));
406
420
  // Fix registry — adapter-aware
407
421
  adapter.updatePluginRegistry(pluginRoot, newVersion);
@@ -414,6 +428,24 @@ async function upgrade() {
414
428
  timeout: 60000,
415
429
  });
416
430
  s.stop("Dependencies ready");
431
+ // Rebuild native addons for current Node.js ABI (fixes #131)
432
+ s.start("Rebuilding native addons");
433
+ try {
434
+ execSync("npm rebuild better-sqlite3", {
435
+ cwd: pluginRoot,
436
+ stdio: "pipe",
437
+ timeout: 60000,
438
+ });
439
+ s.stop(color.green("Native addons rebuilt"));
440
+ changes.push("Rebuilt better-sqlite3 for current Node.js");
441
+ }
442
+ catch (err) {
443
+ const message = err instanceof Error ? err.message : String(err);
444
+ s.stop(color.yellow("Native addon rebuild warning"));
445
+ p.log.warn(color.yellow("better-sqlite3 rebuild issue") +
446
+ ` — ${message}` +
447
+ color.dim(`\n Try manually: cd "${pluginRoot}" && npm rebuild better-sqlite3`));
448
+ }
417
449
  // Update global npm
418
450
  s.start("Updating npm global package");
419
451
  try {
@@ -0,0 +1,14 @@
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
+ /** Pi extension default export. Called once by Pi runtime with the extension API. */
14
+ export default function piExtension(pi: any): void;
@@ -0,0 +1,340 @@
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 } 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
+ // ── Helpers ──────────────────────────────────────────────
49
+ function getSessionDir() {
50
+ const dir = join(homedir(), ".pi", "context-mode", "sessions");
51
+ mkdirSync(dir, { recursive: true });
52
+ return dir;
53
+ }
54
+ function getDBPath() {
55
+ return join(getSessionDir(), "context-mode.db");
56
+ }
57
+ function getOrCreateDB() {
58
+ if (!_db) {
59
+ _db = new SessionDB({ dbPath: getDBPath() });
60
+ }
61
+ return _db;
62
+ }
63
+ /** Derive a stable session ID from Pi's session file path (SHA256, 16 hex chars). */
64
+ function deriveSessionId(ctx) {
65
+ try {
66
+ const sessionManager = ctx.sessionManager;
67
+ const sessionFile = sessionManager?.getSessionFile?.();
68
+ if (sessionFile && typeof sessionFile === "string") {
69
+ return createHash("sha256").update(sessionFile).digest("hex").slice(0, 16);
70
+ }
71
+ }
72
+ catch {
73
+ // best effort
74
+ }
75
+ return `pi-${Date.now()}`;
76
+ }
77
+ /** Build stats text for the /ctx-stats command. */
78
+ function buildStatsText(db, sessionId) {
79
+ try {
80
+ const events = db.getEvents(sessionId);
81
+ const stats = db.getSessionStats(sessionId);
82
+ const lines = [
83
+ "## context-mode stats (Pi)",
84
+ "",
85
+ `- Session: \`${sessionId.slice(0, 8)}...\``,
86
+ `- Events captured: ${events.length}`,
87
+ `- Compactions: ${stats?.compact_count ?? 0}`,
88
+ ];
89
+ // Event breakdown by category
90
+ const byCategory = {};
91
+ for (const ev of events) {
92
+ const key = ev.category ?? "unknown";
93
+ byCategory[key] = (byCategory[key] ?? 0) + 1;
94
+ }
95
+ if (Object.keys(byCategory).length > 0) {
96
+ lines.push("- Event breakdown:");
97
+ for (const [category, count] of Object.entries(byCategory)) {
98
+ lines.push(` - ${category}: ${count}`);
99
+ }
100
+ }
101
+ // Session age
102
+ if (stats?.started_at) {
103
+ const startedMs = new Date(stats.started_at).getTime();
104
+ const ageMinutes = Math.round((Date.now() - startedMs) / 60_000);
105
+ lines.push(`- Session age: ${ageMinutes}m`);
106
+ }
107
+ return lines.join("\n");
108
+ }
109
+ catch {
110
+ return "context-mode stats unavailable (session DB error)";
111
+ }
112
+ }
113
+ // ── Extension entry point ────────────────────────────────
114
+ /** Pi extension default export. Called once by Pi runtime with the extension API. */
115
+ export default function piExtension(pi) {
116
+ const buildDir = dirname(fileURLToPath(import.meta.url));
117
+ const pluginRoot = resolve(buildDir, "..");
118
+ const projectDir = process.env.PI_PROJECT_DIR || process.cwd();
119
+ const db = getOrCreateDB();
120
+ // ── 1. session_start — Initialize session ──────────────
121
+ pi.on("session_start", (ctx) => {
122
+ try {
123
+ _sessionId = deriveSessionId(ctx ?? {});
124
+ db.ensureSession(_sessionId, projectDir);
125
+ db.cleanupOldSessions(7);
126
+ }
127
+ catch {
128
+ // best effort — never break session start
129
+ if (!_sessionId) {
130
+ _sessionId = `pi-${Date.now()}`;
131
+ }
132
+ }
133
+ });
134
+ // ── 2. tool_call — PreToolUse routing enforcement ──────
135
+ // Block bash commands that contain curl/wget/fetch/requests patterns.
136
+ pi.on("tool_call", (event) => {
137
+ try {
138
+ const toolName = String(event?.toolName ?? "").toLowerCase();
139
+ if (toolName !== "bash")
140
+ return;
141
+ const command = String(event?.input?.command ?? "");
142
+ if (!command)
143
+ return;
144
+ const isBlocked = BLOCKED_BASH_PATTERNS.some((p) => p.test(command));
145
+ if (isBlocked) {
146
+ return {
147
+ block: true,
148
+ reason: "Use context-mode MCP tools (execute, fetch_and_index) instead of inline HTTP clients. " +
149
+ "Raw curl/wget/fetch output floods the context window.",
150
+ };
151
+ }
152
+ }
153
+ catch {
154
+ // Routing failure — allow passthrough
155
+ }
156
+ });
157
+ // ── 3. tool_result — PostToolUse event capture ─────────
158
+ pi.on("tool_result", (event) => {
159
+ try {
160
+ if (!_sessionId)
161
+ return;
162
+ const rawToolName = String(event?.toolName ?? event?.tool_name ?? "");
163
+ const mappedToolName = PI_TOOL_MAP[rawToolName.toLowerCase()] ?? rawToolName;
164
+ // Normalize result to string
165
+ const rawResult = event?.result ?? event?.output;
166
+ const resultStr = typeof rawResult === "string"
167
+ ? rawResult
168
+ : rawResult != null
169
+ ? JSON.stringify(rawResult)
170
+ : undefined;
171
+ // Detect errors
172
+ const hasError = Boolean(event?.error || event?.isError);
173
+ const hookInput = {
174
+ tool_name: mappedToolName,
175
+ tool_input: event?.params ?? event?.input ?? {},
176
+ tool_response: resultStr,
177
+ tool_output: hasError ? { isError: true } : undefined,
178
+ };
179
+ const events = extractEvents(hookInput);
180
+ if (events.length > 0) {
181
+ for (const ev of events) {
182
+ db.insertEvent(_sessionId, ev, "PostToolUse");
183
+ }
184
+ }
185
+ else if (rawToolName) {
186
+ // Fallback: record unrecognized tool call as generic event
187
+ const data = JSON.stringify({
188
+ tool: rawToolName,
189
+ params: event?.params ?? event?.input,
190
+ });
191
+ db.insertEvent(_sessionId, {
192
+ type: "tool_call",
193
+ category: "pi",
194
+ data,
195
+ priority: 1,
196
+ data_hash: createHash("sha256")
197
+ .update(data)
198
+ .digest("hex")
199
+ .slice(0, 16),
200
+ }, "PostToolUse");
201
+ }
202
+ }
203
+ catch {
204
+ // Silent — session capture must never break the tool call
205
+ }
206
+ });
207
+ // ── 4. before_agent_start — Resume injection + user events ─
208
+ pi.on("before_agent_start", (event) => {
209
+ try {
210
+ if (!_sessionId)
211
+ return;
212
+ const prompt = String(event?.prompt ?? "");
213
+ // Extract user events from the prompt text
214
+ if (prompt) {
215
+ const userEvents = extractUserEvents(prompt);
216
+ for (const ev of userEvents) {
217
+ db.insertEvent(_sessionId, ev, "UserPromptSubmit");
218
+ }
219
+ }
220
+ // Check for unconsumed resume snapshot
221
+ const resume = db.getResume(_sessionId);
222
+ if (!resume || resume.consumed)
223
+ return;
224
+ // Build FTS5 active memory from the current prompt
225
+ const stats = db.getSessionStats(_sessionId);
226
+ if ((stats?.compact_count ?? 0) === 0)
227
+ return;
228
+ // Mark resume as consumed so it is not re-injected
229
+ db.markResumeConsumed(_sessionId);
230
+ // Build memory context from recent high-priority events
231
+ const allEvents = db.getEvents(_sessionId, { minPriority: 2, limit: 50 });
232
+ let memoryContext = "";
233
+ if (allEvents.length > 0) {
234
+ const memoryLines = ["<active_memory>"];
235
+ for (const ev of allEvents) {
236
+ memoryLines.push(` <event type="${ev.type}" category="${ev.category}">${ev.data}</event>`);
237
+ }
238
+ memoryLines.push("</active_memory>");
239
+ memoryContext = memoryLines.join("\n");
240
+ }
241
+ // Compose the augmented system prompt
242
+ const existingPrompt = String(event?.systemPrompt ?? "");
243
+ const parts = [];
244
+ if (existingPrompt)
245
+ parts.push(existingPrompt);
246
+ if (resume.snapshot)
247
+ parts.push(resume.snapshot);
248
+ if (memoryContext)
249
+ parts.push(memoryContext);
250
+ if (parts.length > (existingPrompt ? 1 : 0)) {
251
+ return { systemPrompt: parts.join("\n\n") };
252
+ }
253
+ }
254
+ catch {
255
+ // best effort — never break agent start
256
+ }
257
+ });
258
+ // ── 5. session_before_compact — Build resume snapshot ──
259
+ pi.on("session_before_compact", () => {
260
+ try {
261
+ if (!_sessionId)
262
+ return;
263
+ const allEvents = db.getEvents(_sessionId);
264
+ if (allEvents.length === 0)
265
+ return;
266
+ const stats = db.getSessionStats(_sessionId);
267
+ const snapshot = buildResumeSnapshot(allEvents, {
268
+ compactCount: (stats?.compact_count ?? 0) + 1,
269
+ });
270
+ db.upsertResume(_sessionId, snapshot, allEvents.length);
271
+ }
272
+ catch {
273
+ // best effort — never break compaction
274
+ }
275
+ });
276
+ // ── 6. session_compact — Increment compact counter ─────
277
+ pi.on("session_compact", () => {
278
+ try {
279
+ if (!_sessionId)
280
+ return;
281
+ db.incrementCompactCount(_sessionId);
282
+ }
283
+ catch {
284
+ // best effort
285
+ }
286
+ });
287
+ // ── 7. session_shutdown — Cleanup old sessions ─────────
288
+ pi.on("session_shutdown", () => {
289
+ try {
290
+ if (_db) {
291
+ _db.cleanupOldSessions(7);
292
+ }
293
+ _db = null;
294
+ _sessionId = "";
295
+ }
296
+ catch {
297
+ // best effort — never throw during shutdown
298
+ }
299
+ });
300
+ // ── 8. Slash commands ──────────────────────────────────
301
+ pi.registerCommand("ctx-stats", {
302
+ description: "Show context-mode session statistics",
303
+ handler: () => {
304
+ if (!_db || !_sessionId) {
305
+ return { text: "context-mode: no active session" };
306
+ }
307
+ return { text: buildStatsText(_db, _sessionId) };
308
+ },
309
+ });
310
+ pi.registerCommand("ctx-doctor", {
311
+ description: "Run context-mode diagnostics",
312
+ handler: () => {
313
+ const dbPath = getDBPath();
314
+ const dbExists = existsSync(dbPath);
315
+ const lines = [
316
+ "## ctx-doctor (Pi)",
317
+ "",
318
+ `- DB path: \`${dbPath}\``,
319
+ `- DB exists: ${dbExists}`,
320
+ `- Session ID: \`${_sessionId ? _sessionId.slice(0, 8) + "..." : "none"}\``,
321
+ `- Plugin root: \`${pluginRoot}\``,
322
+ `- Project dir: \`${projectDir}\``,
323
+ ];
324
+ if (_db && _sessionId) {
325
+ try {
326
+ const stats = _db.getSessionStats(_sessionId);
327
+ const eventCount = _db.getEventCount(_sessionId);
328
+ lines.push(`- Events: ${eventCount}`);
329
+ lines.push(`- Compactions: ${stats?.compact_count ?? 0}`);
330
+ const resume = _db.getResume(_sessionId);
331
+ lines.push(`- Resume snapshot: ${resume ? (resume.consumed ? "consumed" : "available") : "none"}`);
332
+ }
333
+ catch {
334
+ lines.push("- DB query error");
335
+ }
336
+ }
337
+ return { text: lines.join("\n") };
338
+ },
339
+ });
340
+ }
package/build/server.js CHANGED
@@ -15,8 +15,19 @@ import { detectRuntimes, getRuntimeSummary, getAvailableLanguages, hasBunRuntime
15
15
  import { classifyNonZeroExit } from "./exit-classify.js";
16
16
  import { startLifecycleGuard } from "./lifecycle.js";
17
17
  import { getWorktreeSuffix } from "./session/db.js";
18
- const require = createRequire(import.meta.url);
19
- const VERSION = require("../package.json").version;
18
+ const __pkg_dir = dirname(fileURLToPath(import.meta.url));
19
+ const VERSION = (() => {
20
+ for (const rel of ["../package.json", "./package.json"]) {
21
+ const p = resolve(__pkg_dir, rel);
22
+ if (existsSync(p)) {
23
+ try {
24
+ return JSON.parse(readFileSync(p, "utf8")).version;
25
+ }
26
+ catch { }
27
+ }
28
+ }
29
+ return "unknown";
30
+ })();
20
31
  // Prevent silent server death from unhandled async errors
21
32
  process.on("unhandledRejection", (err) => {
22
33
  process.stderr.write(`[context-mode] unhandledRejection: ${err}\n`);
@@ -778,6 +789,10 @@ server.registerTool("ctx_search", {
778
789
  .string()
779
790
  .optional()
780
791
  .describe("Filter to a specific indexed source (partial match)."),
792
+ contentType: z
793
+ .enum(["code", "prose"])
794
+ .optional()
795
+ .describe("Filter results by content type: 'code' or 'prose'."),
781
796
  }),
782
797
  }, async (params) => {
783
798
  try {
@@ -797,7 +812,7 @@ server.registerTool("ctx_search", {
797
812
  isError: true,
798
813
  });
799
814
  }
800
- const { limit = 3, source } = params;
815
+ const { limit = 3, source, contentType } = params;
801
816
  // Progressive throttling: track calls in time window
802
817
  const now = Date.now();
803
818
  if (now - searchWindowStart > SEARCH_WINDOW_MS) {
@@ -829,7 +844,7 @@ server.registerTool("ctx_search", {
829
844
  sections.push(`## ${q}\n(output cap reached)\n`);
830
845
  continue;
831
846
  }
832
- const results = store.searchWithFallback(q, effectiveLimit, source);
847
+ const results = store.searchWithFallback(q, effectiveLimit, source, contentType);
833
848
  if (results.length === 0) {
834
849
  sections.push(`## ${q}\nNo results found.`);
835
850
  continue;
package/build/store.d.ts CHANGED
@@ -37,10 +37,10 @@ export declare class ContentStore {
37
37
  * Falls back to `indexPlainText` if the content is not valid JSON.
38
38
  */
39
39
  indexJSON(content: string, source: string, maxChunkBytes?: number): IndexResult;
40
- search(query: string, limit?: number, source?: string, mode?: "AND" | "OR"): SearchResult[];
41
- searchTrigram(query: string, limit?: number, source?: string, mode?: "AND" | "OR"): SearchResult[];
40
+ search(query: string, limit?: number, source?: string, mode?: "AND" | "OR", contentType?: "code" | "prose"): SearchResult[];
41
+ searchTrigram(query: string, limit?: number, source?: string, mode?: "AND" | "OR", contentType?: "code" | "prose"): SearchResult[];
42
42
  fuzzyCorrect(query: string): string | null;
43
- searchWithFallback(query: string, limit?: number, source?: string): SearchResult[];
43
+ searchWithFallback(query: string, limit?: number, source?: string, contentType?: "code" | "prose"): SearchResult[];
44
44
  listSources(): Array<{
45
45
  label: string;
46
46
  chunkCount: number;