context-mode 1.0.99 → 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/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 +69 -21
- 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/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 +227 -92
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Claude Code plugins by Mert Koseoğlu",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.100"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "context-mode",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
16
|
-
"version": "1.0.
|
|
16
|
+
"version": "1.0.100",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Mert Koseoğlu"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.100",
|
|
4
4
|
"description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Context Mode",
|
|
4
4
|
"kind": "tool",
|
|
5
5
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.100",
|
|
7
7
|
"sandbox": {
|
|
8
8
|
"mode": "permissive",
|
|
9
9
|
"filesystem_access": "full",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.100",
|
|
4
4
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
package/build/pi-extension.js
CHANGED
|
@@ -242,7 +242,7 @@ export default function piExtension(pi) {
|
|
|
242
242
|
// Mark resume as consumed so it is not re-injected
|
|
243
243
|
db.markResumeConsumed(_sessionId);
|
|
244
244
|
// Build memory context from recent high-priority events
|
|
245
|
-
const allEvents = db.getEvents(_sessionId, { minPriority:
|
|
245
|
+
const allEvents = db.getEvents(_sessionId, { minPriority: 3, limit: 50 });
|
|
246
246
|
let memoryContext = "";
|
|
247
247
|
if (allEvents.length > 0) {
|
|
248
248
|
const memoryLines = ["<active_memory>"];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-memory search — searches CLAUDE.md and MEMORY.md files for
|
|
3
|
+
* persisted decisions, preferences, and context from prior sessions.
|
|
4
|
+
*
|
|
5
|
+
* Returns results in a format compatible with the unified search pipeline.
|
|
6
|
+
*/
|
|
7
|
+
export interface AutoMemoryResult {
|
|
8
|
+
title: string;
|
|
9
|
+
content: string;
|
|
10
|
+
source: string;
|
|
11
|
+
origin: "auto-memory";
|
|
12
|
+
timestamp?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Search auto-memory files (CLAUDE.md, MEMORY.md, user identity files)
|
|
16
|
+
* for content matching any of the given queries.
|
|
17
|
+
*
|
|
18
|
+
* Scans:
|
|
19
|
+
* 1. Project-level: <projectDir>/CLAUDE.md
|
|
20
|
+
* 2. User-level: <configDir>/CLAUDE.md
|
|
21
|
+
* 3. User memory: <configDir>/memory/*.md
|
|
22
|
+
*
|
|
23
|
+
* @param queries Array of search terms
|
|
24
|
+
* @param limit Max results to return
|
|
25
|
+
* @param projectDir Project directory path
|
|
26
|
+
* @param configDir Config directory (e.g. ~/.claude)
|
|
27
|
+
* @returns Matching auto-memory results
|
|
28
|
+
*/
|
|
29
|
+
export declare function searchAutoMemory(queries: string[], limit?: number, projectDir?: string, configDir?: string): AutoMemoryResult[];
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-memory search — searches CLAUDE.md and MEMORY.md files for
|
|
3
|
+
* persisted decisions, preferences, and context from prior sessions.
|
|
4
|
+
*
|
|
5
|
+
* Returns results in a format compatible with the unified search pipeline.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
const DEBUG = process.env.DEBUG?.includes("context-mode");
|
|
11
|
+
/**
|
|
12
|
+
* Search auto-memory files (CLAUDE.md, MEMORY.md, user identity files)
|
|
13
|
+
* for content matching any of the given queries.
|
|
14
|
+
*
|
|
15
|
+
* Scans:
|
|
16
|
+
* 1. Project-level: <projectDir>/CLAUDE.md
|
|
17
|
+
* 2. User-level: <configDir>/CLAUDE.md
|
|
18
|
+
* 3. User memory: <configDir>/memory/*.md
|
|
19
|
+
*
|
|
20
|
+
* @param queries Array of search terms
|
|
21
|
+
* @param limit Max results to return
|
|
22
|
+
* @param projectDir Project directory path
|
|
23
|
+
* @param configDir Config directory (e.g. ~/.claude)
|
|
24
|
+
* @returns Matching auto-memory results
|
|
25
|
+
*/
|
|
26
|
+
export function searchAutoMemory(queries, limit = 5, projectDir, configDir) {
|
|
27
|
+
const results = [];
|
|
28
|
+
const effectiveConfigDir = configDir || join(homedir(), ".claude");
|
|
29
|
+
// Collect candidate files
|
|
30
|
+
const candidates = [];
|
|
31
|
+
// 1. Project-level CLAUDE.md
|
|
32
|
+
if (projectDir) {
|
|
33
|
+
const projectClaude = join(projectDir, "CLAUDE.md");
|
|
34
|
+
if (existsSync(projectClaude)) {
|
|
35
|
+
candidates.push({ path: projectClaude, label: "project/CLAUDE.md" });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// 2. User-level CLAUDE.md
|
|
39
|
+
const userClaude = join(effectiveConfigDir, "CLAUDE.md");
|
|
40
|
+
if (existsSync(userClaude)) {
|
|
41
|
+
candidates.push({ path: userClaude, label: "user/CLAUDE.md" });
|
|
42
|
+
}
|
|
43
|
+
// 3. User memory directory
|
|
44
|
+
const memoryDir = join(effectiveConfigDir, "memory");
|
|
45
|
+
if (existsSync(memoryDir)) {
|
|
46
|
+
try {
|
|
47
|
+
const files = readdirSync(memoryDir).filter(f => f.endsWith(".md"));
|
|
48
|
+
for (const file of files) {
|
|
49
|
+
candidates.push({
|
|
50
|
+
path: join(memoryDir, file),
|
|
51
|
+
label: `memory/${file}`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
if (DEBUG)
|
|
57
|
+
process.stderr.write(`[ctx] auto-memory dir scan failed: ${e}\n`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Search each candidate file for matching queries
|
|
61
|
+
for (const candidate of candidates) {
|
|
62
|
+
if (results.length >= limit)
|
|
63
|
+
break;
|
|
64
|
+
try {
|
|
65
|
+
// Skip files larger than 1MB to avoid memory issues
|
|
66
|
+
try {
|
|
67
|
+
if (statSync(candidate.path).size > 1_000_000)
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const content = readFileSync(candidate.path, "utf-8");
|
|
74
|
+
const contentLower = content.toLowerCase();
|
|
75
|
+
for (const query of queries) {
|
|
76
|
+
if (results.length >= limit)
|
|
77
|
+
break;
|
|
78
|
+
const queryLower = query.toLowerCase();
|
|
79
|
+
// Split query into terms, match if any term is found
|
|
80
|
+
const terms = queryLower.split(/\s+/).filter(t => t.length >= 3);
|
|
81
|
+
const matched = terms.some(term => {
|
|
82
|
+
try {
|
|
83
|
+
return new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, "i").test(content);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return contentLower.includes(term); // fallback for invalid regex
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
if (matched) {
|
|
90
|
+
// Extract a relevant section around the first match
|
|
91
|
+
const firstTermIdx = terms.reduce((best, term) => {
|
|
92
|
+
const idx = contentLower.indexOf(term);
|
|
93
|
+
return idx >= 0 && (best < 0 || idx < best) ? idx : best;
|
|
94
|
+
}, -1);
|
|
95
|
+
let start = Math.max(0, firstTermIdx - 200);
|
|
96
|
+
let end = Math.min(content.length, firstTermIdx + 500);
|
|
97
|
+
const prevBlank = content.lastIndexOf("\n\n", start);
|
|
98
|
+
const nextBlank = content.indexOf("\n\n", end);
|
|
99
|
+
if (prevBlank >= 0)
|
|
100
|
+
start = prevBlank + 2;
|
|
101
|
+
if (nextBlank >= 0)
|
|
102
|
+
end = nextBlank;
|
|
103
|
+
const snippet = content.slice(start, end).trim();
|
|
104
|
+
results.push({
|
|
105
|
+
title: `[auto-memory] ${candidate.label}`,
|
|
106
|
+
content: snippet,
|
|
107
|
+
source: candidate.label,
|
|
108
|
+
origin: "auto-memory",
|
|
109
|
+
timestamp: statSync(candidate.path).mtime.toISOString(),
|
|
110
|
+
});
|
|
111
|
+
break; // one result per file per query batch
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
if (DEBUG)
|
|
117
|
+
process.stderr.write(`[ctx] auto-memory file read failed: ${e}\n`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return results.slice(0, limit);
|
|
121
|
+
}
|
|
@@ -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,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));
|
|
@@ -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",
|
|
@@ -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
|
|
@@ -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
|
*
|