context-mode 1.0.99 → 1.0.101
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 +22 -8
- package/build/adapters/claude-code/hooks.d.ts +4 -2
- package/build/adapters/claude-code/hooks.js +11 -4
- package/build/adapters/codex/index.d.ts +1 -1
- package/build/adapters/codex/index.js +6 -5
- package/build/adapters/gemini-cli/hooks.js +2 -1
- package/build/adapters/jetbrains-copilot/hooks.js +2 -1
- package/build/adapters/kiro/hooks.js +2 -1
- package/build/adapters/qwen-code/index.d.ts +1 -1
- package/build/adapters/qwen-code/index.js +13 -10
- package/build/adapters/types.d.ts +13 -0
- package/build/adapters/types.js +23 -1
- package/build/adapters/vscode-copilot/hooks.js +2 -1
- package/build/openclaw-plugin.js +3 -2
- 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 +73 -24
- 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 +67 -4
- package/build/types.d.ts +1 -0
- package/cli.bundle.mjs +254 -119
- package/configs/claude-code/CLAUDE.md +21 -1
- package/configs/codex/AGENTS.md +22 -1
- 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/userpromptsubmit.mjs +1 -1
- package/hooks/core/mcp-ready.mjs +7 -1
- package/hooks/ensure-deps.mjs +35 -7
- 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/insight/server.mjs +379 -1
- package/insight/src/lib/api.ts +88 -16
- package/insight/src/routes/index.tsx +566 -5
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +222 -87
- package/start.mjs +3 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified multi-source search — merges ContentStore, SessionDB, and
|
|
3
|
+
* auto-memory results into a single ranked or chronological result set.
|
|
4
|
+
*
|
|
5
|
+
* Used by ctx_search when sort="timeline" to search across all sources,
|
|
6
|
+
* or sort="relevance" (default) for ContentStore-only BM25 search.
|
|
7
|
+
*/
|
|
8
|
+
import type { ContentStore } from "../store.js";
|
|
9
|
+
import type { SessionDB } from "../session/db.js";
|
|
10
|
+
export interface UnifiedSearchResult {
|
|
11
|
+
title: string;
|
|
12
|
+
content: string;
|
|
13
|
+
source: string;
|
|
14
|
+
origin: "current-session" | "prior-session" | "auto-memory";
|
|
15
|
+
timestamp?: string;
|
|
16
|
+
rank?: number;
|
|
17
|
+
matchLayer?: string;
|
|
18
|
+
highlighted?: string;
|
|
19
|
+
contentType?: "code" | "prose";
|
|
20
|
+
}
|
|
21
|
+
export interface SearchAllSourcesOpts {
|
|
22
|
+
query: string;
|
|
23
|
+
limit: number;
|
|
24
|
+
store: ContentStore;
|
|
25
|
+
sort?: "relevance" | "timeline";
|
|
26
|
+
source?: string;
|
|
27
|
+
contentType?: "code" | "prose";
|
|
28
|
+
sessionDB?: SessionDB | null;
|
|
29
|
+
projectDir?: string;
|
|
30
|
+
configDir?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Search across all available sources.
|
|
34
|
+
*
|
|
35
|
+
* - sort="relevance" (default): BM25-ranked results from ContentStore only.
|
|
36
|
+
* - sort="timeline": chronological merge of ContentStore + SessionDB + auto-memory.
|
|
37
|
+
*
|
|
38
|
+
* Errors in any single source are caught and logged — partial results
|
|
39
|
+
* are always returned.
|
|
40
|
+
*/
|
|
41
|
+
export declare function searchAllSources(opts: SearchAllSourcesOpts): UnifiedSearchResult[];
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified multi-source search — merges ContentStore, SessionDB, and
|
|
3
|
+
* auto-memory results into a single ranked or chronological result set.
|
|
4
|
+
*
|
|
5
|
+
* Used by ctx_search when sort="timeline" to search across all sources,
|
|
6
|
+
* or sort="relevance" (default) for ContentStore-only BM25 search.
|
|
7
|
+
*/
|
|
8
|
+
import { searchAutoMemory } from "./auto-memory.js";
|
|
9
|
+
const DEBUG = process.env.DEBUG?.includes("context-mode");
|
|
10
|
+
// ─────────────────────────────────────────────────────────
|
|
11
|
+
// Implementation
|
|
12
|
+
// ─────────────────────────────────────────────────────────
|
|
13
|
+
/**
|
|
14
|
+
* Search across all available sources.
|
|
15
|
+
*
|
|
16
|
+
* - sort="relevance" (default): BM25-ranked results from ContentStore only.
|
|
17
|
+
* - sort="timeline": chronological merge of ContentStore + SessionDB + auto-memory.
|
|
18
|
+
*
|
|
19
|
+
* Errors in any single source are caught and logged — partial results
|
|
20
|
+
* are always returned.
|
|
21
|
+
*/
|
|
22
|
+
export function searchAllSources(opts) {
|
|
23
|
+
const { query, limit, store, sort = "relevance", source, contentType, sessionDB, projectDir, configDir, } = opts;
|
|
24
|
+
const results = [];
|
|
25
|
+
// Capture session start time once — used as proxy for ContentStore items
|
|
26
|
+
// (we don't know exact indexing time, but all content is from current session)
|
|
27
|
+
const sessionStartTime = new Date().toISOString();
|
|
28
|
+
// ── Source 1: ContentStore (always, both modes) ──
|
|
29
|
+
try {
|
|
30
|
+
const storeResults = store.searchWithFallback(query, limit, source, contentType);
|
|
31
|
+
results.push(...storeResults.map((r) => ({
|
|
32
|
+
title: r.title,
|
|
33
|
+
content: r.content,
|
|
34
|
+
source: r.source,
|
|
35
|
+
origin: "current-session",
|
|
36
|
+
timestamp: r.timestamp || sessionStartTime,
|
|
37
|
+
rank: r.rank,
|
|
38
|
+
matchLayer: r.matchLayer,
|
|
39
|
+
highlighted: r.highlighted,
|
|
40
|
+
contentType: r.contentType,
|
|
41
|
+
})));
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
if (DEBUG)
|
|
45
|
+
process.stderr.write(`[ctx] ContentStore search failed: ${e}\n`);
|
|
46
|
+
}
|
|
47
|
+
// ── Sources 2+3: timeline mode only ──
|
|
48
|
+
if (sort === "timeline") {
|
|
49
|
+
// Source 2: SessionDB — prior session events
|
|
50
|
+
try {
|
|
51
|
+
if (sessionDB) {
|
|
52
|
+
const dbResults = sessionDB.searchEvents(query, limit, projectDir || "", source);
|
|
53
|
+
results.push(...dbResults.map((r) => ({
|
|
54
|
+
title: `[${r.category}] ${r.type}`,
|
|
55
|
+
content: r.data,
|
|
56
|
+
source: "prior-session",
|
|
57
|
+
origin: "prior-session",
|
|
58
|
+
timestamp: r.created_at,
|
|
59
|
+
})));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
if (DEBUG)
|
|
64
|
+
process.stderr.write(`[ctx] SessionDB search failed: ${e}\n`);
|
|
65
|
+
}
|
|
66
|
+
// Source 3: Auto-memory
|
|
67
|
+
try {
|
|
68
|
+
const memResults = searchAutoMemory([query], limit, projectDir, configDir);
|
|
69
|
+
results.push(...memResults);
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
if (DEBUG)
|
|
73
|
+
process.stderr.write(`[ctx] auto-memory search failed: ${e}\n`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// ── Normalize timestamps for consistent sorting ──
|
|
77
|
+
// SQLite datetime('now') → "YYYY-MM-DD HH:MM:SS" (no T, no Z)
|
|
78
|
+
// ISO → "YYYY-MM-DDTHH:MM:SS.sssZ"
|
|
79
|
+
for (const r of results) {
|
|
80
|
+
if (r.timestamp && !r.timestamp.includes("T")) {
|
|
81
|
+
r.timestamp = r.timestamp.replace(" ", "T") + "Z";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// ── Sort ──
|
|
85
|
+
if (sort === "timeline") {
|
|
86
|
+
results.sort((a, b) => (a.timestamp || "").localeCompare(b.timestamp || ""));
|
|
87
|
+
}
|
|
88
|
+
return results.slice(0, limit);
|
|
89
|
+
}
|
package/build/server.js
CHANGED
|
@@ -16,7 +16,9 @@ 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";
|
|
21
|
+
import { buildNodeCommand } from "./adapters/types.js";
|
|
20
22
|
import { loadDatabase } from "./db-base.js";
|
|
21
23
|
import { AnalyticsEngine, formatReport } from "./session/analytics.js";
|
|
22
24
|
const __pkg_dir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -1081,6 +1083,7 @@ server.registerTool("ctx_search", {
|
|
|
1081
1083
|
"Pass ALL search questions as queries array in ONE call. " +
|
|
1082
1084
|
"File-backed sources are auto-refreshed when the source file changes.\n\n" +
|
|
1083
1085
|
"TIPS: 2-4 specific terms per query. Use 'source' to scope results.\n\n" +
|
|
1086
|
+
"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
1087
|
"When reporting results — terse like caveman. Technical substance exact. Only fluff die. Pattern: [thing] [action] [reason]. [next step].",
|
|
1085
1088
|
inputSchema: z.object({
|
|
1086
1089
|
queries: z.preprocess(coerceJsonArray, z
|
|
@@ -1100,13 +1103,20 @@ server.registerTool("ctx_search", {
|
|
|
1100
1103
|
.enum(["code", "prose"])
|
|
1101
1104
|
.optional()
|
|
1102
1105
|
.describe("Filter results by content type: 'code' or 'prose'."),
|
|
1106
|
+
sort: z
|
|
1107
|
+
.enum(["relevance", "timeline"])
|
|
1108
|
+
.optional()
|
|
1109
|
+
.default("relevance")
|
|
1110
|
+
.describe("Sort mode. 'relevance' (default): BM25 ranked, current session only. " +
|
|
1111
|
+
"'timeline': chronological across current session, prior sessions, and auto-memory."),
|
|
1103
1112
|
}),
|
|
1104
1113
|
}, async (params) => {
|
|
1105
1114
|
try {
|
|
1106
1115
|
const store = getStore();
|
|
1116
|
+
const sort = params.sort || "relevance";
|
|
1107
1117
|
// 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) {
|
|
1118
|
+
// tool that requires prior indexing. Skip for timeline mode (SessionDB/auto-memory may have data).
|
|
1119
|
+
if (sort !== "timeline" && store.getStats().chunks === 0) {
|
|
1110
1120
|
return trackResponse("ctx_search", {
|
|
1111
1121
|
content: [{
|
|
1112
1122
|
type: "text",
|
|
@@ -1163,26 +1173,65 @@ server.registerTool("ctx_search", {
|
|
|
1163
1173
|
const MAX_TOTAL = 40 * 1024; // 40KB total cap
|
|
1164
1174
|
let totalSize = 0;
|
|
1165
1175
|
const sections = [];
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1176
|
+
// Open SessionDB once before the loop (Blocker 4: avoid open/close per query)
|
|
1177
|
+
let timelineDB = null;
|
|
1178
|
+
if (sort === "timeline") {
|
|
1179
|
+
try {
|
|
1180
|
+
const sessionsDir = getSessionDir();
|
|
1181
|
+
const dbFile = join(sessionsDir, `${hashProjectDir()}.db`);
|
|
1182
|
+
if (existsSync(dbFile)) {
|
|
1183
|
+
timelineDB = new SessionDB({ dbPath: dbFile });
|
|
1184
|
+
}
|
|
1170
1185
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1186
|
+
catch { /* SessionDB unavailable — search ContentStore + auto-memory only */ }
|
|
1187
|
+
}
|
|
1188
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
1189
|
+
try {
|
|
1190
|
+
for (const q of queryList) {
|
|
1191
|
+
if (totalSize > MAX_TOTAL) {
|
|
1192
|
+
sections.push(`## ${q}\n(output cap reached)\n`);
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
let results;
|
|
1196
|
+
if (sort === "timeline") {
|
|
1197
|
+
results = searchAllSources({
|
|
1198
|
+
query: q,
|
|
1199
|
+
limit: effectiveLimit,
|
|
1200
|
+
store,
|
|
1201
|
+
sort,
|
|
1202
|
+
source,
|
|
1203
|
+
contentType,
|
|
1204
|
+
sessionDB: timelineDB,
|
|
1205
|
+
projectDir: getProjectDir(),
|
|
1206
|
+
configDir,
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
else {
|
|
1210
|
+
results = store.searchWithFallback(q, effectiveLimit, source, contentType);
|
|
1211
|
+
}
|
|
1212
|
+
if (results.length === 0) {
|
|
1213
|
+
sections.push(`## ${q}\nNo results found.`);
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
const formatted = results
|
|
1217
|
+
.map((r, i) => {
|
|
1218
|
+
const origin = r.origin || "current-session";
|
|
1219
|
+
const ts = r.timestamp ? r.timestamp.slice(0, 16).replace("T", " ") : "";
|
|
1220
|
+
const header = `--- [${origin}${ts ? " | " + ts : ""} | ${r.source}] ---`;
|
|
1221
|
+
const heading = `### ${r.title}`;
|
|
1222
|
+
const snippet = extractSnippet(r.content, q, 1500, r.highlighted);
|
|
1223
|
+
return `${header}\n${heading}\n\n${snippet}`;
|
|
1224
|
+
})
|
|
1225
|
+
.join("\n\n");
|
|
1226
|
+
sections.push(`## ${q}\n\n${formatted}`);
|
|
1227
|
+
totalSize += formatted.length;
|
|
1175
1228
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
})
|
|
1183
|
-
.join("\n\n");
|
|
1184
|
-
sections.push(`## ${q}\n\n${formatted}`);
|
|
1185
|
-
totalSize += formatted.length;
|
|
1229
|
+
}
|
|
1230
|
+
finally {
|
|
1231
|
+
try {
|
|
1232
|
+
timelineDB?.close();
|
|
1233
|
+
}
|
|
1234
|
+
catch { }
|
|
1186
1235
|
}
|
|
1187
1236
|
let output = sections.join("\n\n---\n\n");
|
|
1188
1237
|
// Report auto-refreshed stale sources
|
|
@@ -1767,10 +1816,10 @@ server.registerTool("ctx_upgrade", {
|
|
|
1767
1816
|
catch { /* best effort — don't block upgrade */ }
|
|
1768
1817
|
let cmd;
|
|
1769
1818
|
if (existsSync(bundlePath)) {
|
|
1770
|
-
cmd =
|
|
1819
|
+
cmd = `${buildNodeCommand(bundlePath)} upgrade`;
|
|
1771
1820
|
}
|
|
1772
1821
|
else if (existsSync(fallbackPath)) {
|
|
1773
|
-
cmd =
|
|
1822
|
+
cmd = `${buildNodeCommand(fallbackPath)} upgrade`;
|
|
1774
1823
|
}
|
|
1775
1824
|
else {
|
|
1776
1825
|
// Inline fallback: neither CLI file exists (e.g. marketplace installs).
|
|
@@ -1811,7 +1860,7 @@ server.registerTool("ctx_upgrade", {
|
|
|
1811
1860
|
const tmpScript = resolve(pluginRoot, ".ctx-upgrade-inline.mjs");
|
|
1812
1861
|
const { writeFileSync: writeTmp } = await import("node:fs");
|
|
1813
1862
|
writeTmp(tmpScript, scriptLines);
|
|
1814
|
-
cmd =
|
|
1863
|
+
cmd = buildNodeCommand(tmpScript);
|
|
1815
1864
|
}
|
|
1816
1865
|
const text = [
|
|
1817
1866
|
"## ctx-upgrade",
|
|
@@ -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
|
}
|