context-mode 1.0.98 → 1.0.100
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 +9 -7
- package/build/adapters/claude-code-base.js +4 -4
- package/build/adapters/codex/index.js +23 -1
- package/build/adapters/qwen-code/index.d.ts +1 -1
- package/build/adapters/qwen-code/index.js +110 -4
- package/build/cli.js +2 -0
- package/build/opencode-plugin.js +1 -1
- package/build/pi-extension.js +1 -1
- package/build/search/auto-memory.d.ts +29 -0
- package/build/search/auto-memory.js +121 -0
- package/build/search/unified.d.ts +41 -0
- package/build/search/unified.js +89 -0
- package/build/server.js +88 -40
- package/build/session/analytics.js +1 -1
- package/build/session/db.d.ts +17 -0
- package/build/session/db.js +28 -0
- package/build/session/extract.d.ts +4 -0
- package/build/session/extract.js +232 -1
- package/build/session/snapshot.js +31 -0
- package/build/store.js +118 -8
- package/build/types.d.ts +1 -0
- package/cli.bundle.mjs +260 -125
- package/configs/claude-code/CLAUDE.md +21 -1
- package/configs/codex/AGENTS.md +23 -2
- package/configs/codex/hooks.json +14 -0
- package/configs/cursor/context-mode.mdc +18 -1
- package/configs/gemini-cli/GEMINI.md +22 -1
- package/configs/jetbrains-copilot/copilot-instructions.md +22 -1
- package/configs/kilo/AGENTS.md +19 -2
- package/configs/kiro/KIRO.md +18 -1
- package/configs/openclaw/AGENTS.md +22 -2
- package/configs/opencode/AGENTS.md +18 -1
- package/configs/pi/AGENTS.md +18 -1
- package/configs/qwen-code/QWEN.md +38 -18
- package/configs/vscode-copilot/copilot-instructions.md +22 -1
- package/hooks/auto-injection.mjs +76 -0
- package/hooks/codex/stop.mjs +43 -0
- package/hooks/codex/userpromptsubmit.mjs +75 -0
- package/hooks/core/mcp-ready.mjs +7 -1
- package/hooks/posttooluse.mjs +50 -1
- package/hooks/precompact.mjs +9 -0
- package/hooks/pretooluse.mjs +27 -0
- package/hooks/routing-block.mjs +7 -1
- package/hooks/session-db.bundle.mjs +19 -13
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-snapshot.bundle.mjs +18 -17
- package/hooks/sessionstart.mjs +17 -0
- package/hooks/userpromptsubmit.mjs +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +228 -93
- package/skills/context-mode-ops/agent-teams.md +1 -1
package/build/server.js
CHANGED
|
@@ -16,7 +16,8 @@ import { readBashPolicies, evaluateCommandDenyOnly, extractShellCommands, readTo
|
|
|
16
16
|
import { detectRuntimes, getRuntimeSummary, getAvailableLanguages, hasBunRuntime, } from "./runtime.js";
|
|
17
17
|
import { classifyNonZeroExit } from "./exit-classify.js";
|
|
18
18
|
import { startLifecycleGuard } from "./lifecycle.js";
|
|
19
|
-
import { getWorktreeSuffix } from "./session/db.js";
|
|
19
|
+
import { getWorktreeSuffix, SessionDB } from "./session/db.js";
|
|
20
|
+
import { searchAllSources } from "./search/unified.js";
|
|
20
21
|
import { loadDatabase } from "./db-base.js";
|
|
21
22
|
import { AnalyticsEngine, formatReport } from "./session/analytics.js";
|
|
22
23
|
const __pkg_dir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -58,7 +59,7 @@ const executor = new PolyglotExecutor({
|
|
|
58
59
|
projectRoot: process.env.CLAUDE_PROJECT_DIR,
|
|
59
60
|
});
|
|
60
61
|
// ─────────────────────────────────────────────────────────
|
|
61
|
-
// FS read tracking preload for
|
|
62
|
+
// FS read tracking preload for ctx_batch_execute
|
|
62
63
|
// ─────────────────────────────────────────────────────────
|
|
63
64
|
// NODE_OPTIONS is denied by the executor's #buildSafeEnv (security).
|
|
64
65
|
// Instead, we inject it as an inline shell env prefix in each batch command.
|
|
@@ -515,7 +516,7 @@ export function formatBatchQueryResults(store, queries, source, maxOutput = 80 *
|
|
|
515
516
|
let outputSize = 0;
|
|
516
517
|
for (const query of queries) {
|
|
517
518
|
if (outputSize > maxOutput) {
|
|
518
|
-
sections.push(`## ${query}\n(output cap reached — use
|
|
519
|
+
sections.push(`## ${query}\n(output cap reached — use ctx_search(queries: ["${query}"]) for details)\n`);
|
|
519
520
|
continue;
|
|
520
521
|
}
|
|
521
522
|
const results = store.searchWithFallback(query, 3, source, undefined, "exact");
|
|
@@ -577,7 +578,7 @@ server.registerTool("ctx_execute", {
|
|
|
577
578
|
.optional()
|
|
578
579
|
.describe("What you're looking for in the output. When provided and output is large (>5KB), " +
|
|
579
580
|
"indexes output into knowledge base and returns section titles + previews — not full content. " +
|
|
580
|
-
"Use
|
|
581
|
+
"Use ctx_search(queries: [...]) to retrieve specific sections. Example: 'failing tests', 'HTTP 500 errors'." +
|
|
581
582
|
"\n\nTIP: Use specific technical terms, not just concepts. Check 'Searchable terms' in the response for available vocabulary."),
|
|
582
583
|
}),
|
|
583
584
|
}, async ({ language, code, timeout, background, intent }) => {
|
|
@@ -779,7 +780,7 @@ function indexStdout(stdout, source) {
|
|
|
779
780
|
content: [
|
|
780
781
|
{
|
|
781
782
|
type: "text",
|
|
782
|
-
text: `Indexed ${indexed.totalChunks} sections (${indexed.codeChunks} with code) from: ${indexed.label}\nUse
|
|
783
|
+
text: `Indexed ${indexed.totalChunks} sections (${indexed.codeChunks} with code) from: ${indexed.label}\nUse ctx_search(queries: ["..."]) to query this content. Use source: "${indexed.label}" to scope results.`,
|
|
783
784
|
},
|
|
784
785
|
],
|
|
785
786
|
};
|
|
@@ -792,7 +793,7 @@ const LARGE_OUTPUT_THRESHOLD = 102_400; // 100KB — auto-index into FTS5, retur
|
|
|
792
793
|
function intentSearch(stdout, intent, source, maxResults = 5) {
|
|
793
794
|
const totalLines = stdout.split("\n").length;
|
|
794
795
|
const totalBytes = Buffer.byteLength(stdout);
|
|
795
|
-
// Index into the PERSISTENT store so user can
|
|
796
|
+
// Index into the PERSISTENT store so user can ctx_search() later
|
|
796
797
|
const persistent = getStore();
|
|
797
798
|
const indexed = persistent.indexPlainText(stdout, source);
|
|
798
799
|
// Search the persistent store directly (porter → trigram → fuzzy)
|
|
@@ -809,7 +810,7 @@ function intentSearch(stdout, intent, source, maxResults = 5) {
|
|
|
809
810
|
lines.push(`Searchable terms: ${distinctiveTerms.join(", ")}`);
|
|
810
811
|
}
|
|
811
812
|
lines.push("");
|
|
812
|
-
lines.push("Use
|
|
813
|
+
lines.push("Use ctx_search(queries: [...]) to explore the indexed content.");
|
|
813
814
|
return lines.join("\n");
|
|
814
815
|
}
|
|
815
816
|
// Return ONLY titles + first-line previews — not full content
|
|
@@ -827,7 +828,7 @@ function intentSearch(stdout, intent, source, maxResults = 5) {
|
|
|
827
828
|
lines.push(`Searchable terms: ${distinctiveTerms.join(", ")}`);
|
|
828
829
|
}
|
|
829
830
|
lines.push("");
|
|
830
|
-
lines.push("Use
|
|
831
|
+
lines.push("Use ctx_search(queries: [...]) to retrieve full content of any section.");
|
|
831
832
|
return lines.join("\n");
|
|
832
833
|
}
|
|
833
834
|
// ─────────────────────────────────────────────────────────
|
|
@@ -977,9 +978,9 @@ server.registerTool("ctx_index", {
|
|
|
977
978
|
"- Skill prompts and instructions that are too large for context\n" +
|
|
978
979
|
"- README files, migration guides, changelog entries\n" +
|
|
979
980
|
"- Any content with code examples you may need to reference precisely\n\n" +
|
|
980
|
-
"After indexing, use '
|
|
981
|
+
"After indexing, use 'ctx_search' to retrieve specific sections on-demand.\n" +
|
|
981
982
|
"When `path` is provided, a content hash is stored for automatic stale detection in search results.\n" +
|
|
982
|
-
"Do NOT use for: log files, test output, CSV, build output — use '
|
|
983
|
+
"Do NOT use for: log files, test output, CSV, build output — use 'ctx_execute_file' for those.",
|
|
983
984
|
inputSchema: z.object({
|
|
984
985
|
content: z
|
|
985
986
|
.string()
|
|
@@ -1023,7 +1024,7 @@ server.registerTool("ctx_index", {
|
|
|
1023
1024
|
content: [
|
|
1024
1025
|
{
|
|
1025
1026
|
type: "text",
|
|
1026
|
-
text: `Indexed ${result.totalChunks} sections (${result.codeChunks} with code) from: ${result.label}\nUse
|
|
1027
|
+
text: `Indexed ${result.totalChunks} sections (${result.codeChunks} with code) from: ${result.label}\nUse ctx_search(queries: ["..."]) to query this content. Use source: "${result.label}" to scope results.`,
|
|
1027
1028
|
},
|
|
1028
1029
|
],
|
|
1029
1030
|
});
|
|
@@ -1081,6 +1082,7 @@ server.registerTool("ctx_search", {
|
|
|
1081
1082
|
"Pass ALL search questions as queries array in ONE call. " +
|
|
1082
1083
|
"File-backed sources are auto-refreshed when the source file changes.\n\n" +
|
|
1083
1084
|
"TIPS: 2-4 specific terms per query. Use 'source' to scope results.\n\n" +
|
|
1085
|
+
"SESSION STATE: If skills, roles, or decisions were set earlier in this conversation, they are still active. Do not discard or contradict them.\n\n" +
|
|
1084
1086
|
"When reporting results — terse like caveman. Technical substance exact. Only fluff die. Pattern: [thing] [action] [reason]. [next step].",
|
|
1085
1087
|
inputSchema: z.object({
|
|
1086
1088
|
queries: z.preprocess(coerceJsonArray, z
|
|
@@ -1100,13 +1102,20 @@ server.registerTool("ctx_search", {
|
|
|
1100
1102
|
.enum(["code", "prose"])
|
|
1101
1103
|
.optional()
|
|
1102
1104
|
.describe("Filter results by content type: 'code' or 'prose'."),
|
|
1105
|
+
sort: z
|
|
1106
|
+
.enum(["relevance", "timeline"])
|
|
1107
|
+
.optional()
|
|
1108
|
+
.default("relevance")
|
|
1109
|
+
.describe("Sort mode. 'relevance' (default): BM25 ranked, current session only. " +
|
|
1110
|
+
"'timeline': chronological across current session, prior sessions, and auto-memory."),
|
|
1103
1111
|
}),
|
|
1104
1112
|
}, async (params) => {
|
|
1105
1113
|
try {
|
|
1106
1114
|
const store = getStore();
|
|
1115
|
+
const sort = params.sort || "relevance";
|
|
1107
1116
|
// Guard: redirect when the index is empty — ctx_search is a follow-up
|
|
1108
|
-
// tool that requires prior indexing.
|
|
1109
|
-
if (store.getStats().chunks === 0) {
|
|
1117
|
+
// tool that requires prior indexing. Skip for timeline mode (SessionDB/auto-memory may have data).
|
|
1118
|
+
if (sort !== "timeline" && store.getStats().chunks === 0) {
|
|
1110
1119
|
return trackResponse("ctx_search", {
|
|
1111
1120
|
content: [{
|
|
1112
1121
|
type: "text",
|
|
@@ -1151,7 +1160,7 @@ server.registerTool("ctx_search", {
|
|
|
1151
1160
|
type: "text",
|
|
1152
1161
|
text: `BLOCKED: ${searchCallCount} search calls in ${Math.round((now - searchWindowStart) / 1000)}s. ` +
|
|
1153
1162
|
"You're flooding context. STOP making individual search calls. " +
|
|
1154
|
-
"Use
|
|
1163
|
+
"Use ctx_batch_execute(commands, queries) for your next research step.",
|
|
1155
1164
|
}],
|
|
1156
1165
|
isError: true,
|
|
1157
1166
|
});
|
|
@@ -1163,26 +1172,65 @@ server.registerTool("ctx_search", {
|
|
|
1163
1172
|
const MAX_TOTAL = 40 * 1024; // 40KB total cap
|
|
1164
1173
|
let totalSize = 0;
|
|
1165
1174
|
const sections = [];
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1175
|
+
// Open SessionDB once before the loop (Blocker 4: avoid open/close per query)
|
|
1176
|
+
let timelineDB = null;
|
|
1177
|
+
if (sort === "timeline") {
|
|
1178
|
+
try {
|
|
1179
|
+
const sessionsDir = getSessionDir();
|
|
1180
|
+
const dbFile = join(sessionsDir, `${hashProjectDir()}.db`);
|
|
1181
|
+
if (existsSync(dbFile)) {
|
|
1182
|
+
timelineDB = new SessionDB({ dbPath: dbFile });
|
|
1183
|
+
}
|
|
1170
1184
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1185
|
+
catch { /* SessionDB unavailable — search ContentStore + auto-memory only */ }
|
|
1186
|
+
}
|
|
1187
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
1188
|
+
try {
|
|
1189
|
+
for (const q of queryList) {
|
|
1190
|
+
if (totalSize > MAX_TOTAL) {
|
|
1191
|
+
sections.push(`## ${q}\n(output cap reached)\n`);
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
let results;
|
|
1195
|
+
if (sort === "timeline") {
|
|
1196
|
+
results = searchAllSources({
|
|
1197
|
+
query: q,
|
|
1198
|
+
limit: effectiveLimit,
|
|
1199
|
+
store,
|
|
1200
|
+
sort,
|
|
1201
|
+
source,
|
|
1202
|
+
contentType,
|
|
1203
|
+
sessionDB: timelineDB,
|
|
1204
|
+
projectDir: getProjectDir(),
|
|
1205
|
+
configDir,
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
else {
|
|
1209
|
+
results = store.searchWithFallback(q, effectiveLimit, source, contentType);
|
|
1210
|
+
}
|
|
1211
|
+
if (results.length === 0) {
|
|
1212
|
+
sections.push(`## ${q}\nNo results found.`);
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
const formatted = results
|
|
1216
|
+
.map((r, i) => {
|
|
1217
|
+
const origin = r.origin || "current-session";
|
|
1218
|
+
const ts = r.timestamp ? r.timestamp.slice(0, 16).replace("T", " ") : "";
|
|
1219
|
+
const header = `--- [${origin}${ts ? " | " + ts : ""} | ${r.source}] ---`;
|
|
1220
|
+
const heading = `### ${r.title}`;
|
|
1221
|
+
const snippet = extractSnippet(r.content, q, 1500, r.highlighted);
|
|
1222
|
+
return `${header}\n${heading}\n\n${snippet}`;
|
|
1223
|
+
})
|
|
1224
|
+
.join("\n\n");
|
|
1225
|
+
sections.push(`## ${q}\n\n${formatted}`);
|
|
1226
|
+
totalSize += formatted.length;
|
|
1175
1227
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
})
|
|
1183
|
-
.join("\n\n");
|
|
1184
|
-
sections.push(`## ${q}\n\n${formatted}`);
|
|
1185
|
-
totalSize += formatted.length;
|
|
1228
|
+
}
|
|
1229
|
+
finally {
|
|
1230
|
+
try {
|
|
1231
|
+
timelineDB?.close();
|
|
1232
|
+
}
|
|
1233
|
+
catch { }
|
|
1186
1234
|
}
|
|
1187
1235
|
let output = sections.join("\n\n---\n\n");
|
|
1188
1236
|
// Report auto-refreshed stale sources
|
|
@@ -1193,7 +1241,7 @@ server.registerTool("ctx_search", {
|
|
|
1193
1241
|
if (searchCallCount >= SEARCH_MAX_RESULTS_AFTER) {
|
|
1194
1242
|
output += `\n\n⚠ search call #${searchCallCount}/${SEARCH_BLOCK_AFTER} in this window. ` +
|
|
1195
1243
|
`Results limited to ${effectiveLimit}/query. ` +
|
|
1196
|
-
`Batch queries:
|
|
1244
|
+
`Batch queries: ctx_search(queries: ["q1","q2","q3"]) or use ctx_batch_execute.`;
|
|
1197
1245
|
}
|
|
1198
1246
|
if (output.trim().length === 0) {
|
|
1199
1247
|
const sources = store.listSources();
|
|
@@ -1296,7 +1344,7 @@ main();
|
|
|
1296
1344
|
server.registerTool("ctx_fetch_and_index", {
|
|
1297
1345
|
title: "Fetch & Index URL",
|
|
1298
1346
|
description: "Fetches URL content, converts HTML to markdown, indexes into searchable knowledge base, " +
|
|
1299
|
-
"and returns a ~3KB preview. Full content stays in sandbox — use
|
|
1347
|
+
"and returns a ~3KB preview. Full content stays in sandbox — use ctx_search() for deeper lookups.\n\n" +
|
|
1300
1348
|
"Better than WebFetch: preview is immediate, full content is searchable, raw HTML never enters context.\n\n" +
|
|
1301
1349
|
"Content-type aware: HTML is converted to markdown, JSON is chunked by key paths, plain text is indexed directly.\n\n" +
|
|
1302
1350
|
"When reporting results — terse like caveman. Technical substance exact. Only fluff die. Pattern: [thing] [action] [reason]. [next step].",
|
|
@@ -1332,7 +1380,7 @@ server.registerTool("ctx_fetch_and_index", {
|
|
|
1332
1380
|
return trackResponse("ctx_fetch_and_index", {
|
|
1333
1381
|
content: [{
|
|
1334
1382
|
type: "text",
|
|
1335
|
-
text: `Cached: **${meta.label}** — ${meta.chunkCount} sections, indexed ${ageStr} (fresh, TTL: 24h).\nTo refresh: call ctx_fetch_and_index again with \`force: true\`.\n\nYou MUST call
|
|
1383
|
+
text: `Cached: **${meta.label}** — ${meta.chunkCount} sections, indexed ${ageStr} (fresh, TTL: 24h).\nTo refresh: call ctx_fetch_and_index again with \`force: true\`.\n\nYou MUST call ctx_search() to answer questions about this content — this cached response contains no content.\nUse: ctx_search(queries: [...], source: "${meta.label}")`,
|
|
1336
1384
|
}],
|
|
1337
1385
|
});
|
|
1338
1386
|
}
|
|
@@ -1406,12 +1454,12 @@ server.registerTool("ctx_fetch_and_index", {
|
|
|
1406
1454
|
// Build preview — first ~3KB of markdown for immediate use
|
|
1407
1455
|
const PREVIEW_LIMIT = 3072;
|
|
1408
1456
|
const preview = markdown.length > PREVIEW_LIMIT
|
|
1409
|
-
? markdown.slice(0, PREVIEW_LIMIT) + "\n\n…[truncated — use
|
|
1457
|
+
? markdown.slice(0, PREVIEW_LIMIT) + "\n\n…[truncated — use ctx_search() for full content]"
|
|
1410
1458
|
: markdown;
|
|
1411
1459
|
const totalKB = (Buffer.byteLength(markdown) / 1024).toFixed(1);
|
|
1412
1460
|
const text = [
|
|
1413
1461
|
`Fetched and indexed **${indexed.totalChunks} sections** (${totalKB}KB) from: ${indexed.label}`,
|
|
1414
|
-
`Full content indexed in sandbox — use
|
|
1462
|
+
`Full content indexed in sandbox — use ctx_search(queries: [...], source: "${indexed.label}") for specific lookups.`,
|
|
1415
1463
|
"",
|
|
1416
1464
|
"---",
|
|
1417
1465
|
"",
|
|
@@ -1445,8 +1493,8 @@ server.registerTool("ctx_batch_execute", {
|
|
|
1445
1493
|
title: "Batch Execute & Search",
|
|
1446
1494
|
description: "Execute multiple commands in ONE call, auto-index all output, and search with multiple queries. " +
|
|
1447
1495
|
"Returns search results directly — no follow-up calls needed.\n\n" +
|
|
1448
|
-
"THIS IS THE PRIMARY TOOL. Use this instead of multiple
|
|
1449
|
-
"One
|
|
1496
|
+
"THIS IS THE PRIMARY TOOL. Use this instead of multiple ctx_execute() calls.\n\n" +
|
|
1497
|
+
"One ctx_batch_execute call replaces 30+ ctx_execute calls + 10+ ctx_search calls.\n" +
|
|
1450
1498
|
"Provide all commands to run and all queries to search — everything happens in one round trip.\n\n" +
|
|
1451
1499
|
"THINK IN CODE: When commands produce data you need to analyze, add processing commands that filter and summarize. Don't pull raw output into context — let the sandbox do the work.\n\n" +
|
|
1452
1500
|
"When reporting results — terse like caveman. Technical substance exact. Only fluff die. Pattern: [thing] [action] [reason]. [next step].",
|
|
@@ -1560,7 +1608,7 @@ server.registerTool("ctx_batch_execute", {
|
|
|
1560
1608
|
sectionTitles.push(s.title);
|
|
1561
1609
|
}
|
|
1562
1610
|
// Run all search queries — source scoped only.
|
|
1563
|
-
// Cross-source search remains available via explicit
|
|
1611
|
+
// Cross-source search remains available via explicit ctx_search().
|
|
1564
1612
|
const queryResults = formatBatchQueryResults(store, queries, source);
|
|
1565
1613
|
// Get searchable terms for edge cases where follow-up is needed
|
|
1566
1614
|
const distinctiveTerms = store.getDistinctiveTerms
|
|
@@ -182,7 +182,7 @@ export class AnalyticsEngine {
|
|
|
182
182
|
if (row.category === "file") {
|
|
183
183
|
display = row.data.split("/").pop() || row.data;
|
|
184
184
|
}
|
|
185
|
-
else if (row.category === "prompt") {
|
|
185
|
+
else if (row.category === "prompt" || row.category === "user-prompt") {
|
|
186
186
|
display = display.length > 50 ? display.slice(0, 47) + "..." : display;
|
|
187
187
|
}
|
|
188
188
|
if (display.length > 40)
|
package/build/session/db.d.ts
CHANGED
|
@@ -92,6 +92,23 @@ export declare class SessionDB extends SQLiteBase {
|
|
|
92
92
|
* Return the most recently attributed project dir for a session.
|
|
93
93
|
*/
|
|
94
94
|
getLatestAttributedProjectDir(sessionId: string): string | null;
|
|
95
|
+
/**
|
|
96
|
+
* Search events by text query scoped to a project directory.
|
|
97
|
+
*
|
|
98
|
+
* Performs a case-insensitive LIKE search across the `data` and `category`
|
|
99
|
+
* columns. An optional `source` parameter filters by exact category match.
|
|
100
|
+
* Returns results ordered by monotonic id (chronological).
|
|
101
|
+
*
|
|
102
|
+
* Best-effort: returns empty array on any error.
|
|
103
|
+
*/
|
|
104
|
+
searchEvents(query: string, limit: number, projectDir: string, source?: string): Array<{
|
|
105
|
+
id: number;
|
|
106
|
+
session_id: string;
|
|
107
|
+
category: string;
|
|
108
|
+
type: string;
|
|
109
|
+
data: string;
|
|
110
|
+
created_at: string;
|
|
111
|
+
}>;
|
|
95
112
|
/**
|
|
96
113
|
* Ensure a session metadata entry exists. Idempotent (INSERT OR IGNORE).
|
|
97
114
|
* `projectDir` is the session origin directory, not per-event attribution.
|
package/build/session/db.js
CHANGED
|
@@ -76,6 +76,7 @@ const S = {
|
|
|
76
76
|
deleteMeta: "deleteMeta",
|
|
77
77
|
deleteResume: "deleteResume",
|
|
78
78
|
getOldSessions: "getOldSessions",
|
|
79
|
+
searchEvents: "searchEvents",
|
|
79
80
|
};
|
|
80
81
|
// ─────────────────────────────────────────────────────────
|
|
81
82
|
// SessionDB
|
|
@@ -225,6 +226,14 @@ export class SessionDB extends SQLiteBase {
|
|
|
225
226
|
p(S.deleteEvents, `DELETE FROM session_events WHERE session_id = ?`);
|
|
226
227
|
p(S.deleteMeta, `DELETE FROM session_meta WHERE session_id = ?`);
|
|
227
228
|
p(S.deleteResume, `DELETE FROM session_resume WHERE session_id = ?`);
|
|
229
|
+
// ── Search ──
|
|
230
|
+
p(S.searchEvents, `SELECT id, session_id, category, type, data, created_at
|
|
231
|
+
FROM session_events
|
|
232
|
+
WHERE project_dir = ?
|
|
233
|
+
AND (data LIKE '%' || ? || '%' ESCAPE '\\' OR category LIKE '%' || ? || '%' ESCAPE '\\')
|
|
234
|
+
AND (? IS NULL OR category = ?)
|
|
235
|
+
ORDER BY id ASC
|
|
236
|
+
LIMIT ?`);
|
|
228
237
|
// ── Cleanup ──
|
|
229
238
|
p(S.getOldSessions, `SELECT session_id FROM session_meta WHERE started_at < datetime('now', ? || ' days')`);
|
|
230
239
|
}
|
|
@@ -310,6 +319,25 @@ export class SessionDB extends SQLiteBase {
|
|
|
310
319
|
const row = this.stmt(S.getLatestAttributedProject).get(sessionId);
|
|
311
320
|
return row?.project_dir || null;
|
|
312
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Search events by text query scoped to a project directory.
|
|
324
|
+
*
|
|
325
|
+
* Performs a case-insensitive LIKE search across the `data` and `category`
|
|
326
|
+
* columns. An optional `source` parameter filters by exact category match.
|
|
327
|
+
* Returns results ordered by monotonic id (chronological).
|
|
328
|
+
*
|
|
329
|
+
* Best-effort: returns empty array on any error.
|
|
330
|
+
*/
|
|
331
|
+
searchEvents(query, limit, projectDir, source) {
|
|
332
|
+
try {
|
|
333
|
+
const escapedQuery = query.replace(/[%_]/g, (char) => "\\" + char);
|
|
334
|
+
const sourceParam = source ?? null;
|
|
335
|
+
return this.stmt(S.searchEvents).all(projectDir, escapedQuery, escapedQuery, sourceParam, sourceParam, limit);
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
}
|
|
313
341
|
// ═══════════════════════════════════════════
|
|
314
342
|
// Meta
|
|
315
343
|
// ═══════════════════════════════════════════
|
|
@@ -35,6 +35,10 @@ export interface HookInput {
|
|
|
35
35
|
isError?: boolean;
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
+
/** Reset error-resolution state (for testing). */
|
|
39
|
+
export declare function resetErrorResolutionState(): void;
|
|
40
|
+
/** Reset iteration-loop state (for testing). */
|
|
41
|
+
export declare function resetIterationLoopState(): void;
|
|
38
42
|
/**
|
|
39
43
|
* Extract session events from a PostToolUse hook input.
|
|
40
44
|
*
|
package/build/session/extract.js
CHANGED
|
@@ -335,9 +335,36 @@ function extractSkill(input) {
|
|
|
335
335
|
type: "skill",
|
|
336
336
|
category: "skill",
|
|
337
337
|
data: safeString(skillName),
|
|
338
|
-
priority:
|
|
338
|
+
priority: 2,
|
|
339
339
|
}];
|
|
340
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Category 16: constraint
|
|
343
|
+
* Constraints discovered through error events — tool failures reveal
|
|
344
|
+
* platform/environment limitations worth remembering.
|
|
345
|
+
*/
|
|
346
|
+
function extractConstraint(input) {
|
|
347
|
+
// Only fire on error events — constraints are discovered through failures
|
|
348
|
+
if (!input.tool_response?.includes("Error") && !input.tool_output?.isError)
|
|
349
|
+
return [];
|
|
350
|
+
const response = String(input.tool_response || "");
|
|
351
|
+
const patterns = [/not supported/i, /cannot/i, /does not support/i, /FAIL/i, /refused/i, /permission denied/i, /incompatible/i];
|
|
352
|
+
for (const pattern of patterns) {
|
|
353
|
+
const match = response.match(pattern);
|
|
354
|
+
if (match) {
|
|
355
|
+
// Extract context around the match
|
|
356
|
+
const idx = response.toLowerCase().indexOf(match[0].toLowerCase());
|
|
357
|
+
const context = response.slice(Math.max(0, idx - 50), Math.min(response.length, idx + 200)).trim();
|
|
358
|
+
return [{
|
|
359
|
+
type: "constraint_discovered",
|
|
360
|
+
category: "constraint",
|
|
361
|
+
data: safeString(context),
|
|
362
|
+
priority: 2,
|
|
363
|
+
}];
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
341
368
|
/**
|
|
342
369
|
* Category 9: subagent
|
|
343
370
|
* Agent tool calls — tracks both launch and completion.
|
|
@@ -410,6 +437,67 @@ function extractDecision(input) {
|
|
|
410
437
|
priority: 2,
|
|
411
438
|
}];
|
|
412
439
|
}
|
|
440
|
+
/**
|
|
441
|
+
* Category 22: agent-finding
|
|
442
|
+
* When the Agent tool completes (subagent returns), capture a structured
|
|
443
|
+
* summary of its findings (first 500 chars of tool_response).
|
|
444
|
+
*/
|
|
445
|
+
function extractAgentFinding(input) {
|
|
446
|
+
if (input.tool_name !== "Agent")
|
|
447
|
+
return [];
|
|
448
|
+
if (!input.tool_response || input.tool_response.length === 0)
|
|
449
|
+
return [];
|
|
450
|
+
const summary = input.tool_response.length > 500
|
|
451
|
+
? input.tool_response.slice(0, 500)
|
|
452
|
+
: input.tool_response;
|
|
453
|
+
return [{
|
|
454
|
+
type: "agent_finding",
|
|
455
|
+
category: "agent-finding",
|
|
456
|
+
data: safeString(summary),
|
|
457
|
+
priority: 2,
|
|
458
|
+
}];
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Category 24: external-ref
|
|
462
|
+
* Scan tool_input and tool_response for external URLs, GitHub issues, and PRs.
|
|
463
|
+
* Deduplicates found refs and skips internal URLs (localhost, 127.0.0.1).
|
|
464
|
+
*/
|
|
465
|
+
function extractExternalRef(input) {
|
|
466
|
+
const haystack = [
|
|
467
|
+
safeStringAny(input.tool_input),
|
|
468
|
+
safeString(input.tool_response),
|
|
469
|
+
].join(" ");
|
|
470
|
+
if (haystack.length === 0)
|
|
471
|
+
return [];
|
|
472
|
+
const refs = new Set();
|
|
473
|
+
// URLs — skip localhost / 127.0.0.1
|
|
474
|
+
const urlMatches = haystack.match(/https?:\/\/[^\s)]+/g);
|
|
475
|
+
if (urlMatches) {
|
|
476
|
+
for (let url of urlMatches) {
|
|
477
|
+
// Strip trailing punctuation that gets captured from JSON/prose
|
|
478
|
+
url = url.replace(/["'})\],;.]+$/, "");
|
|
479
|
+
if (!/localhost|127\.0\.0\.1/i.test(url)) {
|
|
480
|
+
refs.add(url);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// Full GitHub issue/PR URLs are already captured above.
|
|
485
|
+
// Shorthand GitHub issue refs: #123 (only bare, not inside a URL)
|
|
486
|
+
const issueMatches = haystack.match(/(?<!\w)#(\d+)/g);
|
|
487
|
+
if (issueMatches) {
|
|
488
|
+
for (const m of issueMatches) {
|
|
489
|
+
refs.add(m);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (refs.size === 0)
|
|
493
|
+
return [];
|
|
494
|
+
return [{
|
|
495
|
+
type: "external_ref",
|
|
496
|
+
category: "external-ref",
|
|
497
|
+
data: safeString(Array.from(refs).join(", ")),
|
|
498
|
+
priority: 3,
|
|
499
|
+
}];
|
|
500
|
+
}
|
|
413
501
|
/**
|
|
414
502
|
* Category 8: env (worktree)
|
|
415
503
|
* EnterWorktree tool — tracks worktree creation.
|
|
@@ -490,6 +578,52 @@ function extractIntent(message) {
|
|
|
490
578
|
priority: 4,
|
|
491
579
|
}];
|
|
492
580
|
}
|
|
581
|
+
/**
|
|
582
|
+
* Category 25: blocked-on
|
|
583
|
+
* Detect when work is blocked on something, or when a blocker is resolved.
|
|
584
|
+
*/
|
|
585
|
+
const BLOCKER_PATTERNS = [
|
|
586
|
+
/\bblocked on\b/i,
|
|
587
|
+
/\bwaiting for\b/i,
|
|
588
|
+
/\bneed\s+\S+\s+before\b/i,
|
|
589
|
+
/\bcan'?t proceed until\b/i,
|
|
590
|
+
/\bdepends on\b/i,
|
|
591
|
+
/\bblocked\b/i,
|
|
592
|
+
// Turkish patterns
|
|
593
|
+
/\bbekliyor\b/i,
|
|
594
|
+
/\bbekliyorum\b/i,
|
|
595
|
+
];
|
|
596
|
+
const BLOCKER_RESOLVED_PATTERNS = [
|
|
597
|
+
/\bunblocked\b/i,
|
|
598
|
+
/\bresolved\b/i,
|
|
599
|
+
/\bgot the\s+\S+/i,
|
|
600
|
+
/\bis ready now\b/i,
|
|
601
|
+
/\bcan proceed\b/i,
|
|
602
|
+
];
|
|
603
|
+
function extractBlocker(message) {
|
|
604
|
+
const events = [];
|
|
605
|
+
// Check resolution first — if both match, resolution takes priority
|
|
606
|
+
const isResolved = BLOCKER_RESOLVED_PATTERNS.some(p => p.test(message));
|
|
607
|
+
if (isResolved) {
|
|
608
|
+
events.push({
|
|
609
|
+
type: "blocker_resolved",
|
|
610
|
+
category: "blocked-on",
|
|
611
|
+
data: safeString(message),
|
|
612
|
+
priority: 2,
|
|
613
|
+
});
|
|
614
|
+
return events;
|
|
615
|
+
}
|
|
616
|
+
const isBlocked = BLOCKER_PATTERNS.some(p => p.test(message));
|
|
617
|
+
if (isBlocked) {
|
|
618
|
+
events.push({
|
|
619
|
+
type: "blocker",
|
|
620
|
+
category: "blocked-on",
|
|
621
|
+
data: safeString(message),
|
|
622
|
+
priority: 2,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
return events;
|
|
626
|
+
}
|
|
493
627
|
/**
|
|
494
628
|
* Category 12: data
|
|
495
629
|
* Large user-pasted data references (message > 1KB).
|
|
@@ -504,6 +638,96 @@ function extractData(message) {
|
|
|
504
638
|
priority: 4,
|
|
505
639
|
}];
|
|
506
640
|
}
|
|
641
|
+
// ── Cross-event stateful extractors ───────────────────────────────────────
|
|
642
|
+
/**
|
|
643
|
+
* Category 23: error-resolution
|
|
644
|
+
* Detects when an error is followed by a successful fix (cross-event state).
|
|
645
|
+
*/
|
|
646
|
+
let lastError = null;
|
|
647
|
+
function extractErrorResolution(input) {
|
|
648
|
+
const { tool_name, tool_response, tool_output } = input;
|
|
649
|
+
const response = String(tool_response ?? "");
|
|
650
|
+
const isErrorFlag = tool_output?.isError === true;
|
|
651
|
+
const isBashError = tool_name === "Bash" &&
|
|
652
|
+
/exit code [1-9]|error:|Error:|FAIL|failed/i.test(response);
|
|
653
|
+
// If this call is an error, store it and return
|
|
654
|
+
if (isBashError || isErrorFlag) {
|
|
655
|
+
lastError = { tool: tool_name, error: response.slice(0, 200), callsSince: 0 };
|
|
656
|
+
return [];
|
|
657
|
+
}
|
|
658
|
+
// No pending error → nothing to resolve
|
|
659
|
+
if (!lastError)
|
|
660
|
+
return [];
|
|
661
|
+
// Increment staleness counter
|
|
662
|
+
lastError.callsSince++;
|
|
663
|
+
// Timeout: clear after 10 calls without resolution
|
|
664
|
+
if (lastError.callsSince > 10) {
|
|
665
|
+
lastError = null;
|
|
666
|
+
return [];
|
|
667
|
+
}
|
|
668
|
+
// Check if this is a resolution: same tool, or Edit/Write after a Read error
|
|
669
|
+
const sameTool = tool_name === lastError.tool;
|
|
670
|
+
const editAfterReadError = lastError.tool === "Read" && (tool_name === "Edit" || tool_name === "Write");
|
|
671
|
+
if (sameTool || editAfterReadError) {
|
|
672
|
+
const event = {
|
|
673
|
+
type: "error_resolved",
|
|
674
|
+
category: "error-resolution",
|
|
675
|
+
data: safeString(`Error in ${lastError.tool}: ${lastError.error} → Fixed`),
|
|
676
|
+
priority: 2,
|
|
677
|
+
};
|
|
678
|
+
lastError = null;
|
|
679
|
+
return [event];
|
|
680
|
+
}
|
|
681
|
+
return [];
|
|
682
|
+
}
|
|
683
|
+
/** Reset error-resolution state (for testing). */
|
|
684
|
+
export function resetErrorResolutionState() {
|
|
685
|
+
lastError = null;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Category 26: iteration-loop
|
|
689
|
+
* Detects when the same tool is called repeatedly with similar input (stuck loop).
|
|
690
|
+
*/
|
|
691
|
+
const callHistory = [];
|
|
692
|
+
function simpleHash(str) {
|
|
693
|
+
return `${str.length}:${str.slice(0, 20)}`;
|
|
694
|
+
}
|
|
695
|
+
function extractIterationLoop(input) {
|
|
696
|
+
const { tool_name, tool_input } = input;
|
|
697
|
+
const inputHash = simpleHash(JSON.stringify(tool_input).slice(0, 200));
|
|
698
|
+
callHistory.push({ tool: tool_name, inputHash });
|
|
699
|
+
// Keep history bounded
|
|
700
|
+
if (callHistory.length > 50) {
|
|
701
|
+
callHistory.splice(0, callHistory.length - 50);
|
|
702
|
+
}
|
|
703
|
+
// Check last N entries for repeated pattern (minimum 3)
|
|
704
|
+
if (callHistory.length < 3)
|
|
705
|
+
return [];
|
|
706
|
+
let count = 0;
|
|
707
|
+
for (let i = callHistory.length - 1; i >= 0; i--) {
|
|
708
|
+
if (callHistory[i].tool === tool_name && callHistory[i].inputHash === inputHash) {
|
|
709
|
+
count++;
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (count >= 3) {
|
|
716
|
+
// Reset the matching tail to avoid duplicate emissions
|
|
717
|
+
callHistory.splice(callHistory.length - count);
|
|
718
|
+
return [{
|
|
719
|
+
type: "retry_detected",
|
|
720
|
+
category: "iteration-loop",
|
|
721
|
+
data: safeString(`${tool_name} called ${count} times with similar input`),
|
|
722
|
+
priority: 2,
|
|
723
|
+
}];
|
|
724
|
+
}
|
|
725
|
+
return [];
|
|
726
|
+
}
|
|
727
|
+
/** Reset iteration-loop state (for testing). */
|
|
728
|
+
export function resetIterationLoopState() {
|
|
729
|
+
callHistory.length = 0;
|
|
730
|
+
}
|
|
507
731
|
// ── Public API ─────────────────────────────────────────────────────────────
|
|
508
732
|
/**
|
|
509
733
|
* Extract session events from a PostToolUse hook input.
|
|
@@ -528,7 +752,13 @@ export function extractEvents(input) {
|
|
|
528
752
|
events.push(...extractSubagent(input));
|
|
529
753
|
events.push(...extractMcp(input));
|
|
530
754
|
events.push(...extractDecision(input));
|
|
755
|
+
events.push(...extractConstraint(input));
|
|
531
756
|
events.push(...extractWorktree(input));
|
|
757
|
+
events.push(...extractAgentFinding(input));
|
|
758
|
+
events.push(...extractExternalRef(input));
|
|
759
|
+
// Cross-event stateful extractors
|
|
760
|
+
events.push(...extractErrorResolution(input));
|
|
761
|
+
events.push(...extractIterationLoop(input));
|
|
532
762
|
return events;
|
|
533
763
|
}
|
|
534
764
|
catch {
|
|
@@ -548,6 +778,7 @@ export function extractUserEvents(message) {
|
|
|
548
778
|
events.push(...extractUserDecision(message));
|
|
549
779
|
events.push(...extractRole(message));
|
|
550
780
|
events.push(...extractIntent(message));
|
|
781
|
+
events.push(...extractBlocker(message));
|
|
551
782
|
events.push(...extractData(message));
|
|
552
783
|
return events;
|
|
553
784
|
}
|