gitmem-mcp 1.4.4 → 1.6.0
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/CHANGELOG.md +27 -0
- package/README.md +21 -4
- package/bin/gitmem.js +10 -0
- package/dist/commands/activate.d.ts +20 -0
- package/dist/commands/activate.js +562 -0
- package/dist/commands/deactivate.d.ts +10 -0
- package/dist/commands/deactivate.js +95 -0
- package/dist/commands/migrate-local.d.ts +53 -0
- package/dist/commands/migrate-local.js +177 -0
- package/dist/hooks/format-utils.js +4 -0
- package/dist/schemas/log.d.ts +2 -2
- package/dist/schemas/search.d.ts +2 -2
- package/dist/schemas/session-close.d.ts +12 -12
- package/dist/server.js +33 -2
- package/dist/services/analytics.d.ts +22 -0
- package/dist/services/analytics.js +68 -0
- package/dist/services/doc-chunker.d.ts +45 -0
- package/dist/services/doc-chunker.js +208 -0
- package/dist/services/doc-index.d.ts +88 -0
- package/dist/services/doc-index.js +328 -0
- package/dist/services/license.d.ts +57 -0
- package/dist/services/license.js +200 -0
- package/dist/services/supabase-client.d.ts +6 -0
- package/dist/services/supabase-client.js +75 -22
- package/dist/services/tier.d.ts +13 -3
- package/dist/services/tier.js +38 -7
- package/dist/tools/definitions.d.ts +688 -0
- package/dist/tools/definitions.js +87 -0
- package/dist/tools/index-docs.d.ts +30 -0
- package/dist/tools/index-docs.js +163 -0
- package/dist/tools/prepare-context.js +7 -0
- package/dist/tools/recall.js +25 -4
- package/dist/tools/search-docs.d.ts +38 -0
- package/dist/tools/search-docs.js +94 -0
- package/dist/tools/search.js +11 -1
- package/dist/tools/session-close.js +76 -7
- package/dist/tools/session-start.js +57 -5
- package/package.json +1 -1
- package/schema/setup.sql +489 -25
|
@@ -2297,6 +2297,92 @@ export const TOOLS = [
|
|
|
2297
2297
|
required: ["type", "tool", "description", "severity"],
|
|
2298
2298
|
},
|
|
2299
2299
|
},
|
|
2300
|
+
{
|
|
2301
|
+
name: "index_docs",
|
|
2302
|
+
description: "Scan a directory of markdown files, chunk them, embed them, and store them in a local doc index for semantic search. Supports incremental indexing: only re-processes changed files. Use search_docs to query the indexed docs.",
|
|
2303
|
+
inputSchema: {
|
|
2304
|
+
type: "object",
|
|
2305
|
+
properties: {
|
|
2306
|
+
directory: {
|
|
2307
|
+
type: "string",
|
|
2308
|
+
description: "Absolute path to directory containing .md files to index",
|
|
2309
|
+
},
|
|
2310
|
+
project: {
|
|
2311
|
+
type: "string",
|
|
2312
|
+
description: "Project namespace (e.g., 'my-project'). Scopes sessions and searches.",
|
|
2313
|
+
},
|
|
2314
|
+
exclude: {
|
|
2315
|
+
type: "array",
|
|
2316
|
+
items: { type: "string" },
|
|
2317
|
+
description: "Directory names to exclude (default: ['_archive', 'node_modules', '.git'])",
|
|
2318
|
+
},
|
|
2319
|
+
force: {
|
|
2320
|
+
type: "boolean",
|
|
2321
|
+
description: "Force re-index all files even if unchanged (default: false)",
|
|
2322
|
+
},
|
|
2323
|
+
clear: {
|
|
2324
|
+
type: "boolean",
|
|
2325
|
+
description: "Clear the doc index for this project before indexing (default: false)",
|
|
2326
|
+
},
|
|
2327
|
+
},
|
|
2328
|
+
required: ["directory"],
|
|
2329
|
+
},
|
|
2330
|
+
},
|
|
2331
|
+
{
|
|
2332
|
+
name: "search_docs",
|
|
2333
|
+
description: "Search indexed repository documentation using semantic similarity (pro/dev tier) or BM25 keyword search (free tier). Returns relevant chunks with file paths for targeted reading. Index docs first with index_docs.",
|
|
2334
|
+
inputSchema: {
|
|
2335
|
+
type: "object",
|
|
2336
|
+
properties: {
|
|
2337
|
+
query: {
|
|
2338
|
+
type: "string",
|
|
2339
|
+
description: "Natural language search query (e.g., 'how does authentication work', 'database schema')",
|
|
2340
|
+
},
|
|
2341
|
+
project: {
|
|
2342
|
+
type: "string",
|
|
2343
|
+
description: "Project namespace (e.g., 'my-project'). Scopes sessions and searches.",
|
|
2344
|
+
},
|
|
2345
|
+
category: {
|
|
2346
|
+
type: "string",
|
|
2347
|
+
description: "Filter results to a specific category (directory name, e.g., 'architecture', 'research')",
|
|
2348
|
+
},
|
|
2349
|
+
match_count: {
|
|
2350
|
+
type: "number",
|
|
2351
|
+
description: "Maximum number of results to return (default: 5)",
|
|
2352
|
+
},
|
|
2353
|
+
},
|
|
2354
|
+
required: ["query"],
|
|
2355
|
+
},
|
|
2356
|
+
},
|
|
2357
|
+
{
|
|
2358
|
+
name: "gitmem-idx",
|
|
2359
|
+
description: "Alias for index_docs. Scan a directory of markdown files and index them for semantic search.",
|
|
2360
|
+
inputSchema: {
|
|
2361
|
+
type: "object",
|
|
2362
|
+
properties: {
|
|
2363
|
+
directory: { type: "string", description: "Absolute path to directory containing .md files" },
|
|
2364
|
+
project: { type: "string", description: "Project namespace" },
|
|
2365
|
+
exclude: { type: "array", items: { type: "string" }, description: "Directory names to exclude" },
|
|
2366
|
+
force: { type: "boolean", description: "Force re-index all files" },
|
|
2367
|
+
clear: { type: "boolean", description: "Clear index before indexing" },
|
|
2368
|
+
},
|
|
2369
|
+
required: ["directory"],
|
|
2370
|
+
},
|
|
2371
|
+
},
|
|
2372
|
+
{
|
|
2373
|
+
name: "gitmem-sd",
|
|
2374
|
+
description: "Alias for search_docs. Search indexed repository documentation.",
|
|
2375
|
+
inputSchema: {
|
|
2376
|
+
type: "object",
|
|
2377
|
+
properties: {
|
|
2378
|
+
query: { type: "string", description: "Natural language search query" },
|
|
2379
|
+
project: { type: "string", description: "Project namespace" },
|
|
2380
|
+
category: { type: "string", description: "Filter by category" },
|
|
2381
|
+
match_count: { type: "number", description: "Max results (default: 5)" },
|
|
2382
|
+
},
|
|
2383
|
+
required: ["query"],
|
|
2384
|
+
},
|
|
2385
|
+
},
|
|
2300
2386
|
];
|
|
2301
2387
|
/**
|
|
2302
2388
|
* Alias tool names — filtered out by default to reduce context window cost.
|
|
@@ -2312,6 +2398,7 @@ export const ALIAS_TOOL_NAMES = new Set([
|
|
|
2312
2398
|
"gitmem-pc", "gitmem-ao",
|
|
2313
2399
|
"gitmem-lt", "gitmem-rt", "gitmem-ct", "gitmem-ps", "gitmem-ds",
|
|
2314
2400
|
"gitmem-cleanup", "gitmem-health", "gitmem-al", "gitmem-graph", "gitmem-fb",
|
|
2401
|
+
"gitmem-idx", "gitmem-sd",
|
|
2315
2402
|
// gm-* aliases
|
|
2316
2403
|
"gm-open", "gm-confirm", "gm-reflect", "gm-refresh", "gm-close",
|
|
2317
2404
|
"gm-scar", "gm-search", "gm-log", "gm-analyze",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index_docs Tool
|
|
3
|
+
*
|
|
4
|
+
* Scans a directory of markdown files, chunks them, embeds them,
|
|
5
|
+
* and stores them in the doc index for semantic search.
|
|
6
|
+
*
|
|
7
|
+
* Supports incremental indexing: only re-processes changed files.
|
|
8
|
+
*/
|
|
9
|
+
import type { Project } from "../types/index.js";
|
|
10
|
+
export interface IndexDocsParams {
|
|
11
|
+
directory: string;
|
|
12
|
+
project?: Project;
|
|
13
|
+
exclude?: string[];
|
|
14
|
+
force?: boolean;
|
|
15
|
+
clear?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface IndexDocsResult {
|
|
18
|
+
directory: string;
|
|
19
|
+
project: string;
|
|
20
|
+
files_scanned: number;
|
|
21
|
+
files_changed: number;
|
|
22
|
+
files_unchanged: number;
|
|
23
|
+
chunks_indexed: number;
|
|
24
|
+
chunks_embedded: number;
|
|
25
|
+
errors: number;
|
|
26
|
+
categories: Record<string, number>;
|
|
27
|
+
display: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function indexDocs(params: IndexDocsParams): Promise<IndexDocsResult>;
|
|
30
|
+
//# sourceMappingURL=index-docs.d.ts.map
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index_docs Tool
|
|
3
|
+
*
|
|
4
|
+
* Scans a directory of markdown files, chunks them, embeds them,
|
|
5
|
+
* and stores them in the doc index for semantic search.
|
|
6
|
+
*
|
|
7
|
+
* Supports incremental indexing: only re-processes changed files.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import { scanDirectory, chunkDocument } from "../services/doc-chunker.js";
|
|
11
|
+
import { indexChunks, getChangedFiles, getIndexStats, clearDocIndex, } from "../services/doc-index.js";
|
|
12
|
+
import { getProject } from "../services/session-state.js";
|
|
13
|
+
import { wrapDisplay, productLine, dimText } from "../services/display-protocol.js";
|
|
14
|
+
export async function indexDocs(params) {
|
|
15
|
+
const directory = params.directory;
|
|
16
|
+
const project = params.project || getProject() || "default";
|
|
17
|
+
const exclude = params.exclude || ["_archive", "node_modules", ".git"];
|
|
18
|
+
const force = params.force || false;
|
|
19
|
+
// Validate directory exists
|
|
20
|
+
if (!fs.existsSync(directory)) {
|
|
21
|
+
const display = wrapDisplay([
|
|
22
|
+
productLine("index_docs", "error"),
|
|
23
|
+
"",
|
|
24
|
+
`Directory not found: ${directory}`,
|
|
25
|
+
"",
|
|
26
|
+
"Provide an absolute path to a directory containing .md files.",
|
|
27
|
+
].join("\n"));
|
|
28
|
+
return {
|
|
29
|
+
directory,
|
|
30
|
+
project,
|
|
31
|
+
files_scanned: 0,
|
|
32
|
+
files_changed: 0,
|
|
33
|
+
files_unchanged: 0,
|
|
34
|
+
chunks_indexed: 0,
|
|
35
|
+
chunks_embedded: 0,
|
|
36
|
+
errors: 1,
|
|
37
|
+
categories: {},
|
|
38
|
+
display,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// Handle clear request
|
|
42
|
+
if (params.clear) {
|
|
43
|
+
const removed = clearDocIndex(project);
|
|
44
|
+
const display = wrapDisplay([
|
|
45
|
+
productLine("index_docs", `cleared ${removed} chunks for project="${project}"`),
|
|
46
|
+
].join("\n"));
|
|
47
|
+
return {
|
|
48
|
+
directory,
|
|
49
|
+
project,
|
|
50
|
+
files_scanned: 0,
|
|
51
|
+
files_changed: 0,
|
|
52
|
+
files_unchanged: 0,
|
|
53
|
+
chunks_indexed: 0,
|
|
54
|
+
chunks_embedded: 0,
|
|
55
|
+
errors: 0,
|
|
56
|
+
categories: {},
|
|
57
|
+
display,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// Scan directory for .md files
|
|
61
|
+
const files = scanDirectory(directory, { exclude });
|
|
62
|
+
if (files.length === 0) {
|
|
63
|
+
const display = wrapDisplay([
|
|
64
|
+
productLine("index_docs", "no markdown files found"),
|
|
65
|
+
"",
|
|
66
|
+
`Scanned: ${directory}`,
|
|
67
|
+
`Excluded: ${exclude.join(", ")}`,
|
|
68
|
+
].join("\n"));
|
|
69
|
+
return {
|
|
70
|
+
directory,
|
|
71
|
+
project,
|
|
72
|
+
files_scanned: 0,
|
|
73
|
+
files_changed: 0,
|
|
74
|
+
files_unchanged: 0,
|
|
75
|
+
chunks_indexed: 0,
|
|
76
|
+
chunks_embedded: 0,
|
|
77
|
+
errors: 0,
|
|
78
|
+
categories: {},
|
|
79
|
+
display,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// Incremental indexing: check which files changed
|
|
83
|
+
const fileHashes = new Map(files.map((f) => [f.relative_path, f.hash]));
|
|
84
|
+
let filesToProcess = files;
|
|
85
|
+
let filesUnchanged = 0;
|
|
86
|
+
if (!force) {
|
|
87
|
+
const changes = getChangedFiles(fileHashes, project);
|
|
88
|
+
const changedSet = new Set([...changes.changed, ...changes.new_files]);
|
|
89
|
+
if (changedSet.size === 0) {
|
|
90
|
+
const stats = getIndexStats(project);
|
|
91
|
+
const display = wrapDisplay([
|
|
92
|
+
productLine("index_docs", "up to date — no changes detected"),
|
|
93
|
+
"",
|
|
94
|
+
`${files.length} files scanned, all unchanged`,
|
|
95
|
+
`${stats.total_chunks} chunks in index`,
|
|
96
|
+
"",
|
|
97
|
+
dimText("Use force=true to re-index all files"),
|
|
98
|
+
].join("\n"));
|
|
99
|
+
return {
|
|
100
|
+
directory,
|
|
101
|
+
project,
|
|
102
|
+
files_scanned: files.length,
|
|
103
|
+
files_changed: 0,
|
|
104
|
+
files_unchanged: files.length,
|
|
105
|
+
chunks_indexed: 0,
|
|
106
|
+
chunks_embedded: 0,
|
|
107
|
+
errors: 0,
|
|
108
|
+
categories: stats.categories,
|
|
109
|
+
display,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
filesToProcess = files.filter((f) => changedSet.has(f.relative_path));
|
|
113
|
+
filesUnchanged = files.length - filesToProcess.length;
|
|
114
|
+
}
|
|
115
|
+
// Chunk the changed files
|
|
116
|
+
const allChunks = [];
|
|
117
|
+
for (const file of filesToProcess) {
|
|
118
|
+
allChunks.push(...chunkDocument(file));
|
|
119
|
+
}
|
|
120
|
+
// Index chunks (embed + store)
|
|
121
|
+
const result = await indexChunks(allChunks, project);
|
|
122
|
+
// Get updated stats
|
|
123
|
+
const stats = getIndexStats(project);
|
|
124
|
+
// Build display
|
|
125
|
+
const lines = [];
|
|
126
|
+
lines.push(productLine("index_docs", `${filesToProcess.length} files → ${result.indexed} chunks`));
|
|
127
|
+
lines.push("");
|
|
128
|
+
lines.push(`Directory: ${directory}`);
|
|
129
|
+
lines.push(`Project: ${project}`);
|
|
130
|
+
lines.push("");
|
|
131
|
+
lines.push(`Files scanned: ${files.length}`);
|
|
132
|
+
lines.push(`Files changed: ${filesToProcess.length}`);
|
|
133
|
+
lines.push(`Files unchanged: ${filesUnchanged}`);
|
|
134
|
+
lines.push(`Chunks indexed: ${result.indexed}`);
|
|
135
|
+
lines.push(`Chunks embedded: ${result.embedded}`);
|
|
136
|
+
if (result.errors > 0) {
|
|
137
|
+
lines.push(`Errors: ${result.errors}`);
|
|
138
|
+
}
|
|
139
|
+
lines.push("");
|
|
140
|
+
// Category breakdown
|
|
141
|
+
if (Object.keys(stats.categories).length > 0) {
|
|
142
|
+
lines.push("Categories:");
|
|
143
|
+
for (const [cat, count] of Object.entries(stats.categories).sort((a, b) => b[1] - a[1])) {
|
|
144
|
+
lines.push(` ${cat.padEnd(20)} ${count} chunks`);
|
|
145
|
+
}
|
|
146
|
+
lines.push("");
|
|
147
|
+
}
|
|
148
|
+
lines.push(`Total index: ${stats.total_chunks} chunks across ${stats.total_files} files`);
|
|
149
|
+
const display = wrapDisplay(lines.join("\n"));
|
|
150
|
+
return {
|
|
151
|
+
directory,
|
|
152
|
+
project,
|
|
153
|
+
files_scanned: files.length,
|
|
154
|
+
files_changed: filesToProcess.length,
|
|
155
|
+
files_unchanged: filesUnchanged,
|
|
156
|
+
chunks_indexed: result.indexed,
|
|
157
|
+
chunks_embedded: result.embedded,
|
|
158
|
+
errors: result.errors,
|
|
159
|
+
categories: stats.categories,
|
|
160
|
+
display,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=index-docs.js.map
|
|
@@ -40,6 +40,13 @@ Proceed with caution — this may be new territory without documented lessons.`;
|
|
|
40
40
|
formatNudgeHeader(scars.length),
|
|
41
41
|
"",
|
|
42
42
|
];
|
|
43
|
+
// Citation protocol — provenance enforcement for any downstream claims
|
|
44
|
+
// Placed BEFORE results so agents see it before processing scars
|
|
45
|
+
lines.push("───────────────────────────────────────────────────");
|
|
46
|
+
lines.push("CITATION RULE: When referencing facts from these scars, cite the record ID.");
|
|
47
|
+
lines.push("Example: \"Edge improved to 3.07 [id:48ebca14]\" — not paraphrased numbers.");
|
|
48
|
+
lines.push("If you cannot cite a specific record for a claim, say \"not in institutional memory.\"");
|
|
49
|
+
lines.push("");
|
|
43
50
|
// Blocking verification requirements first
|
|
44
51
|
const blockingScars = scars.filter((s) => s.required_verification?.blocking);
|
|
45
52
|
if (blockingScars.length > 0) {
|
package/dist/tools/recall.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import * as supabase from "../services/supabase-client.js";
|
|
16
16
|
import { localScarSearch, isLocalSearchReady } from "../services/local-vector-search.js";
|
|
17
|
-
import { hasSupabase, hasVariants, hasMetrics, getTableName } from "../services/tier.js";
|
|
17
|
+
import { hasSupabase, hasVariants, hasMetrics, hasProInsights, getTableName } from "../services/tier.js";
|
|
18
18
|
import { getProject } from "../services/session-state.js";
|
|
19
19
|
import { getStorage } from "../services/storage.js";
|
|
20
20
|
import { Timer, recordMetrics, buildPerformanceData, buildComponentPerformance, calculateContextBytes, } from "../services/metrics.js";
|
|
@@ -48,6 +48,13 @@ No past lessons match this plan closely enough. Scars accumulate as you work —
|
|
|
48
48
|
lines.push(`${dimText("No project-specific lessons yet. Use create_learning to capture your first.")}`);
|
|
49
49
|
lines.push("");
|
|
50
50
|
}
|
|
51
|
+
// Citation protocol — provenance enforcement for any downstream claims
|
|
52
|
+
// Placed BEFORE results so agents see it before processing scars
|
|
53
|
+
lines.push("───────────────────────────────────────────────────");
|
|
54
|
+
lines.push("CITATION RULE: When referencing facts from these scars, cite the record ID.");
|
|
55
|
+
lines.push("Example: \"Edge improved to 3.07 [id:48ebca14]\" — not paraphrased numbers.");
|
|
56
|
+
lines.push("If you cannot cite a specific record for a claim, say \"not in institutional memory.\"");
|
|
57
|
+
lines.push("");
|
|
51
58
|
// Display blocking verification requirements FIRST and prominently
|
|
52
59
|
if (scarsWithVerification.length > 0) {
|
|
53
60
|
lines.push(`${ANSI.yellow}VERIFICATION REQUIRED${ANSI.reset}`);
|
|
@@ -73,12 +80,18 @@ No past lessons match this plan closely enough. Scars accumulate as you work —
|
|
|
73
80
|
for (const scar of scars) {
|
|
74
81
|
const sev = SEV[scar.severity] || "[?]";
|
|
75
82
|
const starterTag = scar.is_starter ? ` ${dimText("[starter]")}` : "";
|
|
76
|
-
|
|
83
|
+
// Confidence tier: marginal matches (< 0.55) get flagged — 66% N/A rate in this range
|
|
84
|
+
const confidenceTag = scar.similarity < 0.55 ? ` ${dimText("[low confidence]")}` : "";
|
|
85
|
+
// Pro: decay tag for scars with reduced behavioral relevance
|
|
86
|
+
const decayTag = hasProInsights() && scar.decay_multiplier !== undefined && scar.decay_multiplier < 0.8
|
|
87
|
+
? ` ${dimText(`[decay: ${Math.round(scar.decay_multiplier * 100)}%]`)}`
|
|
88
|
+
: "";
|
|
89
|
+
lines.push(`${sev} **${scar.title}** (${scar.severity}, ${scar.similarity.toFixed(2)}) ${dimText(`id:${scar.id.slice(0, 8)}`)}${starterTag}${confidenceTag}${decayTag}`);
|
|
77
90
|
// Inline archival hint: scars with high dismiss rates get annotated
|
|
78
91
|
if (dismissals) {
|
|
79
92
|
const counts = dismissals.get(scar.id);
|
|
80
|
-
if (counts && counts.surfaced >=
|
|
81
|
-
lines.push(` _[${counts.dismissed}
|
|
93
|
+
if (counts && counts.surfaced >= 3 && (counts.dismissed / counts.surfaced) >= 0.6) {
|
|
94
|
+
lines.push(` _[dismissed ${counts.dismissed}/${counts.surfaced} times — re-evaluate whether this still applies]_`);
|
|
82
95
|
}
|
|
83
96
|
}
|
|
84
97
|
// Use variant enforcement text if available (blind to variant name)
|
|
@@ -136,6 +149,14 @@ No past lessons match this plan closely enough. Scars accumulate as you work —
|
|
|
136
149
|
lines.push("");
|
|
137
150
|
}
|
|
138
151
|
lines.push("**Acknowledge these lessons before proceeding.**");
|
|
152
|
+
// Pro: graph nudge when triples exist on any scar
|
|
153
|
+
if (hasProInsights() && scars.some(s => s.related_triples && s.related_triples.length > 0)) {
|
|
154
|
+
const firstScarWithTriples = scars.find(s => s.related_triples && s.related_triples.length > 0);
|
|
155
|
+
if (firstScarWithTriples) {
|
|
156
|
+
lines.push("");
|
|
157
|
+
lines.push(dimText(`Pro: Use graph_traverse(lens: 'connected_to', node: '${firstScarWithTriples.title}') to explore deeper connections.`));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
139
160
|
return lines.join("\n");
|
|
140
161
|
}
|
|
141
162
|
/**
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* search_docs Tool
|
|
3
|
+
*
|
|
4
|
+
* Semantic search over indexed repository documentation.
|
|
5
|
+
* Returns relevant chunks with file paths for targeted reading.
|
|
6
|
+
*
|
|
7
|
+
* Like search (for scars), but for docs.
|
|
8
|
+
*/
|
|
9
|
+
import type { Project } from "../types/index.js";
|
|
10
|
+
export interface SearchDocsParams {
|
|
11
|
+
query: string;
|
|
12
|
+
project?: Project;
|
|
13
|
+
category?: string;
|
|
14
|
+
match_count?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface SearchDocsResultEntry {
|
|
17
|
+
id: string;
|
|
18
|
+
file_path: string;
|
|
19
|
+
chunk_index: number;
|
|
20
|
+
title: string;
|
|
21
|
+
section_title: string;
|
|
22
|
+
category: string;
|
|
23
|
+
content: string;
|
|
24
|
+
similarity: number;
|
|
25
|
+
}
|
|
26
|
+
export interface SearchDocsResult {
|
|
27
|
+
query: string;
|
|
28
|
+
project: string;
|
|
29
|
+
results: SearchDocsResultEntry[];
|
|
30
|
+
total_found: number;
|
|
31
|
+
index_stats: {
|
|
32
|
+
total_chunks: number;
|
|
33
|
+
total_files: number;
|
|
34
|
+
};
|
|
35
|
+
display: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function searchDocsHandler(params: SearchDocsParams): Promise<SearchDocsResult>;
|
|
38
|
+
//# sourceMappingURL=search-docs.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* search_docs Tool
|
|
3
|
+
*
|
|
4
|
+
* Semantic search over indexed repository documentation.
|
|
5
|
+
* Returns relevant chunks with file paths for targeted reading.
|
|
6
|
+
*
|
|
7
|
+
* Like search (for scars), but for docs.
|
|
8
|
+
*/
|
|
9
|
+
import { searchDocs as doSearch, getIndexStats } from "../services/doc-index.js";
|
|
10
|
+
import { getProject } from "../services/session-state.js";
|
|
11
|
+
import { wrapDisplay, productLine, dimText, truncate } from "../services/display-protocol.js";
|
|
12
|
+
export async function searchDocsHandler(params) {
|
|
13
|
+
const query = params.query;
|
|
14
|
+
const project = params.project || getProject() || "default";
|
|
15
|
+
const category = params.category;
|
|
16
|
+
const matchCount = params.match_count || 5;
|
|
17
|
+
// Check if index exists
|
|
18
|
+
const stats = getIndexStats(project);
|
|
19
|
+
if (stats.total_chunks === 0) {
|
|
20
|
+
const display = wrapDisplay([
|
|
21
|
+
productLine("search_docs", "no docs indexed"),
|
|
22
|
+
"",
|
|
23
|
+
`No documents indexed for project="${project}".`,
|
|
24
|
+
"",
|
|
25
|
+
"Index docs first:",
|
|
26
|
+
' index_docs({ directory: "/path/to/docs", project: "my-project" })',
|
|
27
|
+
].join("\n"));
|
|
28
|
+
return {
|
|
29
|
+
query,
|
|
30
|
+
project,
|
|
31
|
+
results: [],
|
|
32
|
+
total_found: 0,
|
|
33
|
+
index_stats: {
|
|
34
|
+
total_chunks: stats.total_chunks,
|
|
35
|
+
total_files: stats.total_files,
|
|
36
|
+
},
|
|
37
|
+
display,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Search
|
|
41
|
+
const results = await doSearch(query, {
|
|
42
|
+
project,
|
|
43
|
+
category,
|
|
44
|
+
match_count: matchCount,
|
|
45
|
+
});
|
|
46
|
+
// Build display
|
|
47
|
+
const lines = [];
|
|
48
|
+
lines.push(productLine("search_docs", `${results.length} results · "${truncate(query, 60)}"`));
|
|
49
|
+
if (category) {
|
|
50
|
+
lines.push(`Category filter: ${category}`);
|
|
51
|
+
}
|
|
52
|
+
lines.push("");
|
|
53
|
+
if (results.length === 0) {
|
|
54
|
+
lines.push("No matching docs found.");
|
|
55
|
+
lines.push("");
|
|
56
|
+
lines.push(`Index contains ${stats.total_chunks} chunks across ${stats.total_files} files.`);
|
|
57
|
+
lines.push("Available categories: " + Object.keys(stats.categories).join(", "));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// Show results with file paths prominently
|
|
61
|
+
for (const r of results) {
|
|
62
|
+
const sim = `(${r.similarity.toFixed(2)})`;
|
|
63
|
+
const conf = r.similarity < 0.55 ? ` ${dimText("[low confidence]")}` : "";
|
|
64
|
+
const section = r.section_title ? ` > ${r.section_title}` : "";
|
|
65
|
+
lines.push(`${r.file_path}${section} ${sim}${conf}`);
|
|
66
|
+
lines.push(` ${truncate(r.title, 60)} [${r.category}]`);
|
|
67
|
+
lines.push(` ${truncate(r.content, 120)}`);
|
|
68
|
+
lines.push("");
|
|
69
|
+
}
|
|
70
|
+
lines.push(dimText(`Searched ${stats.total_chunks} chunks across ${stats.total_files} files`));
|
|
71
|
+
}
|
|
72
|
+
const display = wrapDisplay(lines.join("\n"));
|
|
73
|
+
return {
|
|
74
|
+
query,
|
|
75
|
+
project,
|
|
76
|
+
results: results.map((r) => ({
|
|
77
|
+
id: r.id,
|
|
78
|
+
file_path: r.file_path,
|
|
79
|
+
chunk_index: r.chunk_index,
|
|
80
|
+
title: r.title,
|
|
81
|
+
section_title: r.section_title,
|
|
82
|
+
category: r.category,
|
|
83
|
+
content: r.content,
|
|
84
|
+
similarity: r.similarity,
|
|
85
|
+
})),
|
|
86
|
+
total_found: results.length,
|
|
87
|
+
index_stats: {
|
|
88
|
+
total_chunks: stats.total_chunks,
|
|
89
|
+
total_files: stats.total_files,
|
|
90
|
+
},
|
|
91
|
+
display,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=search-docs.js.map
|
package/dist/tools/search.js
CHANGED
|
@@ -30,6 +30,14 @@ function buildSearchDisplay(results, total_found, query, filters) {
|
|
|
30
30
|
if (fp.length > 0)
|
|
31
31
|
lines.push(`Filters: ${fp.join(", ")}`);
|
|
32
32
|
lines.push("");
|
|
33
|
+
// Citation protocol — provenance enforcement for any downstream claims
|
|
34
|
+
// Placed BEFORE results so agents see it before processing results
|
|
35
|
+
if (results.length > 0) {
|
|
36
|
+
lines.push("───────────────────────────────────────────────────");
|
|
37
|
+
lines.push("CITATION RULE: When referencing facts from these results, cite the record ID.");
|
|
38
|
+
lines.push("If you cannot cite a specific record for a claim, say \"not in institutional memory.\"");
|
|
39
|
+
lines.push("");
|
|
40
|
+
}
|
|
33
41
|
if (results.length === 0) {
|
|
34
42
|
lines.push("No results found.");
|
|
35
43
|
return wrapDisplay(lines.join("\n"));
|
|
@@ -40,7 +48,9 @@ function buildSearchDisplay(results, total_found, query, filters) {
|
|
|
40
48
|
const t = truncate(r.title, 50);
|
|
41
49
|
const sim = `(${r.similarity.toFixed(2)})`;
|
|
42
50
|
const issue = r.source_linear_issue ? ` ${r.source_linear_issue}` : "";
|
|
43
|
-
|
|
51
|
+
// Confidence tier: marginal matches (< 0.55) get flagged
|
|
52
|
+
const conf = r.similarity < 0.55 ? ` ${dimText("[low confidence]")}` : "";
|
|
53
|
+
lines.push(`${te} ${se} ${t.padEnd(52)} ${sim}${issue}${conf}`);
|
|
44
54
|
const starterTag = r.is_starter ? ` ${dimText("[starter]")}` : "";
|
|
45
55
|
lines.push(` ${truncate(r.description, 72)} id:${r.id.slice(0, 8)}${starterTag}`);
|
|
46
56
|
}
|