context-mode 1.0.24 → 1.0.27
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/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +134 -30
- package/build/adapters/client-map.js +4 -0
- package/build/adapters/detect.js +26 -1
- package/build/adapters/types.d.ts +1 -1
- package/build/adapters/zed/index.d.ts +43 -0
- package/build/adapters/zed/index.js +218 -0
- package/build/cli.js +39 -7
- package/build/db-base.d.ts +15 -3
- package/build/db-base.js +96 -4
- package/build/pi-extension.d.ts +14 -0
- package/build/pi-extension.js +340 -0
- package/build/server.js +8 -4
- package/build/store.d.ts +3 -3
- package/build/store.js +218 -61
- package/build/types.d.ts +1 -1
- package/cli.bundle.mjs +167 -109
- package/configs/kiro/KIRO.md +58 -0
- package/configs/kiro/agent.json +18 -0
- package/configs/pi/AGENTS.md +58 -0
- package/configs/zed/AGENTS.md +58 -0
- package/hooks/core/stdin.mjs +1 -1
- package/hooks/kiro/posttooluse.mjs +2 -10
- package/hooks/session-helpers.mjs +1 -1
- package/hooks/session-loaders.mjs +2 -2
- package/native-abi.mjs +73 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -3
- package/server.bundle.mjs +137 -81
- package/start.mjs +8 -1
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
|
|
@@ -252,21 +256,21 @@ async function doctor() {
|
|
|
252
256
|
p.log.warn(color.yellow("Plugin enabled: WARN") +
|
|
253
257
|
` — ${pluginCheck.message}`);
|
|
254
258
|
}
|
|
255
|
-
// FTS5 /
|
|
256
|
-
p.log.step("Checking FTS5 /
|
|
259
|
+
// FTS5 / SQLite
|
|
260
|
+
p.log.step("Checking FTS5 / SQLite...");
|
|
257
261
|
try {
|
|
258
|
-
const Database = (await import("
|
|
262
|
+
const Database = (await import("./db-base.js")).loadDatabase();
|
|
259
263
|
const db = new Database(":memory:");
|
|
260
264
|
db.exec("CREATE VIRTUAL TABLE fts_test USING fts5(content)");
|
|
261
265
|
db.exec("INSERT INTO fts_test(content) VALUES ('hello world')");
|
|
262
266
|
const row = db.prepare("SELECT * FROM fts_test WHERE fts_test MATCH 'hello'").get();
|
|
263
267
|
db.close();
|
|
264
268
|
if (row && row.content === "hello world") {
|
|
265
|
-
p.log.success(color.green("FTS5 /
|
|
269
|
+
p.log.success(color.green("FTS5 / SQLite: PASS") + " — native module works");
|
|
266
270
|
}
|
|
267
271
|
else {
|
|
268
272
|
criticalFails++;
|
|
269
|
-
p.log.error(color.red("FTS5 /
|
|
273
|
+
p.log.error(color.red("FTS5 / SQLite: FAIL") + " — query returned unexpected result");
|
|
270
274
|
}
|
|
271
275
|
}
|
|
272
276
|
catch (err) {
|
|
@@ -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",
|
|
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 {
|
package/build/db-base.d.ts
CHANGED
|
@@ -22,9 +22,21 @@ export interface PreparedStatement {
|
|
|
22
22
|
iterate(...params: unknown[]): IterableIterator<unknown>;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
25
|
+
* Wraps a bun:sqlite Database to provide better-sqlite3-compatible API.
|
|
26
|
+
* Bridges: .pragma(), multi-statement .exec(), .get() null→undefined.
|
|
27
|
+
*/
|
|
28
|
+
export declare class BunSQLiteAdapter {
|
|
29
|
+
#private;
|
|
30
|
+
constructor(rawDb: any);
|
|
31
|
+
pragma(source: string): any;
|
|
32
|
+
exec(sql: string): any;
|
|
33
|
+
prepare(sql: string): any;
|
|
34
|
+
transaction(fn: (...args: any[]) => any): any;
|
|
35
|
+
close(): void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Lazy-load better-sqlite3. Falls back to bun:sqlite via BunSQLiteAdapter
|
|
39
|
+
* when better-sqlite3 is unavailable (Bun runtime, issue #45).
|
|
28
40
|
*/
|
|
29
41
|
export declare function loadDatabase(): typeof DatabaseConstructor;
|
|
30
42
|
/**
|
package/build/db-base.js
CHANGED
|
@@ -10,18 +10,110 @@ import { unlinkSync } from "node:fs";
|
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
// ─────────────────────────────────────────────────────────
|
|
13
|
+
// bun:sqlite adapter (#45)
|
|
14
|
+
// ─────────────────────────────────────────────────────────
|
|
15
|
+
/**
|
|
16
|
+
* Wraps a bun:sqlite Database to provide better-sqlite3-compatible API.
|
|
17
|
+
* Bridges: .pragma(), multi-statement .exec(), .get() null→undefined.
|
|
18
|
+
*/
|
|
19
|
+
export class BunSQLiteAdapter {
|
|
20
|
+
#raw;
|
|
21
|
+
constructor(rawDb) {
|
|
22
|
+
this.#raw = rawDb;
|
|
23
|
+
}
|
|
24
|
+
pragma(source) {
|
|
25
|
+
const stmt = this.#raw.prepare(`PRAGMA ${source}`);
|
|
26
|
+
const rows = stmt.all();
|
|
27
|
+
if (!rows || rows.length === 0)
|
|
28
|
+
return undefined;
|
|
29
|
+
// Multi-row pragmas (table_xinfo, etc.) → return array
|
|
30
|
+
if (rows.length > 1)
|
|
31
|
+
return rows;
|
|
32
|
+
// Single-row: extract scalar value (e.g. journal_mode = "wal")
|
|
33
|
+
const values = Object.values(rows[0]);
|
|
34
|
+
return values.length === 1 ? values[0] : rows[0];
|
|
35
|
+
}
|
|
36
|
+
exec(sql) {
|
|
37
|
+
// bun:sqlite .exec() is single-statement only.
|
|
38
|
+
// Split multi-statement SQL respecting string literals (don't split on ; inside quotes).
|
|
39
|
+
let current = "";
|
|
40
|
+
let inString = null;
|
|
41
|
+
for (let i = 0; i < sql.length; i++) {
|
|
42
|
+
const ch = sql[i];
|
|
43
|
+
if (inString) {
|
|
44
|
+
current += ch;
|
|
45
|
+
if (ch === inString)
|
|
46
|
+
inString = null;
|
|
47
|
+
}
|
|
48
|
+
else if (ch === "'" || ch === '"') {
|
|
49
|
+
current += ch;
|
|
50
|
+
inString = ch;
|
|
51
|
+
}
|
|
52
|
+
else if (ch === ";") {
|
|
53
|
+
const trimmed = current.trim();
|
|
54
|
+
if (trimmed)
|
|
55
|
+
this.#raw.prepare(trimmed).run();
|
|
56
|
+
current = "";
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
current += ch;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const trimmed = current.trim();
|
|
63
|
+
if (trimmed)
|
|
64
|
+
this.#raw.prepare(trimmed).run();
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
prepare(sql) {
|
|
68
|
+
const stmt = this.#raw.prepare(sql);
|
|
69
|
+
return {
|
|
70
|
+
run: (...args) => stmt.run(...args),
|
|
71
|
+
get: (...args) => {
|
|
72
|
+
const r = stmt.get(...args);
|
|
73
|
+
return r === null ? undefined : r;
|
|
74
|
+
},
|
|
75
|
+
all: (...args) => stmt.all(...args),
|
|
76
|
+
iterate: (...args) => stmt.iterate(...args),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
transaction(fn) {
|
|
80
|
+
return this.#raw.transaction(fn);
|
|
81
|
+
}
|
|
82
|
+
close() {
|
|
83
|
+
this.#raw.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// ─────────────────────────────────────────────────────────
|
|
13
87
|
// Lazy loader
|
|
14
88
|
// ─────────────────────────────────────────────────────────
|
|
15
89
|
let _Database = null;
|
|
16
90
|
/**
|
|
17
|
-
* Lazy-load better-sqlite3.
|
|
18
|
-
*
|
|
19
|
-
* is not yet installed (marketplace first-run scenario).
|
|
91
|
+
* Lazy-load better-sqlite3. Falls back to bun:sqlite via BunSQLiteAdapter
|
|
92
|
+
* when better-sqlite3 is unavailable (Bun runtime, issue #45).
|
|
20
93
|
*/
|
|
21
94
|
export function loadDatabase() {
|
|
22
95
|
if (!_Database) {
|
|
23
96
|
const require = createRequire(import.meta.url);
|
|
24
|
-
|
|
97
|
+
try {
|
|
98
|
+
_Database = require("better-sqlite3");
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// better-sqlite3 unavailable (Bun runtime) — wrap bun:sqlite
|
|
102
|
+
if (!globalThis.Bun) {
|
|
103
|
+
throw new Error("better-sqlite3 failed to load and Bun runtime not detected");
|
|
104
|
+
}
|
|
105
|
+
// Compute module name at runtime to prevent esbuild from resolving at bundle time.
|
|
106
|
+
// esbuild constant-folds string concat but NOT Array.join().
|
|
107
|
+
const bunSqliteMod = ["bun", "sqlite"].join(":");
|
|
108
|
+
const BunDB = require(bunSqliteMod).Database;
|
|
109
|
+
_Database = function BunDatabaseFactory(path, opts) {
|
|
110
|
+
const raw = new BunDB(path, {
|
|
111
|
+
readonly: opts?.readonly,
|
|
112
|
+
create: true,
|
|
113
|
+
});
|
|
114
|
+
return new BunSQLiteAdapter(raw);
|
|
115
|
+
};
|
|
116
|
+
}
|
|
25
117
|
}
|
|
26
118
|
return _Database;
|
|
27
119
|
}
|
|
@@ -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,6 +15,7 @@ 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
|
+
import { loadDatabase } from "./db-base.js";
|
|
18
19
|
const __pkg_dir = dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
const VERSION = (() => {
|
|
20
21
|
for (const rel of ["../package.json", "./package.json"]) {
|
|
@@ -789,6 +790,10 @@ server.registerTool("ctx_search", {
|
|
|
789
790
|
.string()
|
|
790
791
|
.optional()
|
|
791
792
|
.describe("Filter to a specific indexed source (partial match)."),
|
|
793
|
+
contentType: z
|
|
794
|
+
.enum(["code", "prose"])
|
|
795
|
+
.optional()
|
|
796
|
+
.describe("Filter results by content type: 'code' or 'prose'."),
|
|
792
797
|
}),
|
|
793
798
|
}, async (params) => {
|
|
794
799
|
try {
|
|
@@ -808,7 +813,7 @@ server.registerTool("ctx_search", {
|
|
|
808
813
|
isError: true,
|
|
809
814
|
});
|
|
810
815
|
}
|
|
811
|
-
const { limit = 3, source } = params;
|
|
816
|
+
const { limit = 3, source, contentType } = params;
|
|
812
817
|
// Progressive throttling: track calls in time window
|
|
813
818
|
const now = Date.now();
|
|
814
819
|
if (now - searchWindowStart > SEARCH_WINDOW_MS) {
|
|
@@ -840,7 +845,7 @@ server.registerTool("ctx_search", {
|
|
|
840
845
|
sections.push(`## ${q}\n(output cap reached)\n`);
|
|
841
846
|
continue;
|
|
842
847
|
}
|
|
843
|
-
const results = store.searchWithFallback(q, effectiveLimit, source);
|
|
848
|
+
const results = store.searchWithFallback(q, effectiveLimit, source, contentType);
|
|
844
849
|
if (results.length === 0) {
|
|
845
850
|
sections.push(`## ${q}\nNo results found.`);
|
|
846
851
|
continue;
|
|
@@ -1314,8 +1319,7 @@ server.registerTool("ctx_stats", {
|
|
|
1314
1319
|
const worktreeSuffix = getWorktreeSuffix();
|
|
1315
1320
|
const sessionDbPath = join(homedir(), ".claude", "context-mode", "sessions", `${dbHash}${worktreeSuffix}.db`);
|
|
1316
1321
|
if (existsSync(sessionDbPath)) {
|
|
1317
|
-
const
|
|
1318
|
-
const Database = require("better-sqlite3");
|
|
1322
|
+
const Database = loadDatabase();
|
|
1319
1323
|
const sdb = new Database(sessionDbPath, { readonly: true });
|
|
1320
1324
|
const eventTotal = sdb.prepare("SELECT COUNT(*) as cnt FROM session_events").get();
|
|
1321
1325
|
const byCategory = sdb.prepare("SELECT category, COUNT(*) as cnt FROM session_events GROUP BY category ORDER BY cnt DESC").all();
|
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;
|