@gmickel/gno 0.3.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/README.md +256 -0
- package/assets/skill/SKILL.md +112 -0
- package/assets/skill/cli-reference.md +327 -0
- package/assets/skill/examples.md +234 -0
- package/assets/skill/mcp-reference.md +159 -0
- package/package.json +90 -0
- package/src/app/constants.ts +313 -0
- package/src/cli/colors.ts +65 -0
- package/src/cli/commands/ask.ts +545 -0
- package/src/cli/commands/cleanup.ts +105 -0
- package/src/cli/commands/collection/add.ts +120 -0
- package/src/cli/commands/collection/index.ts +10 -0
- package/src/cli/commands/collection/list.ts +108 -0
- package/src/cli/commands/collection/remove.ts +64 -0
- package/src/cli/commands/collection/rename.ts +95 -0
- package/src/cli/commands/context/add.ts +67 -0
- package/src/cli/commands/context/check.ts +153 -0
- package/src/cli/commands/context/index.ts +10 -0
- package/src/cli/commands/context/list.ts +109 -0
- package/src/cli/commands/context/rm.ts +52 -0
- package/src/cli/commands/doctor.ts +393 -0
- package/src/cli/commands/embed.ts +462 -0
- package/src/cli/commands/get.ts +356 -0
- package/src/cli/commands/index-cmd.ts +119 -0
- package/src/cli/commands/index.ts +102 -0
- package/src/cli/commands/init.ts +328 -0
- package/src/cli/commands/ls.ts +217 -0
- package/src/cli/commands/mcp/config.ts +300 -0
- package/src/cli/commands/mcp/index.ts +24 -0
- package/src/cli/commands/mcp/install.ts +203 -0
- package/src/cli/commands/mcp/paths.ts +470 -0
- package/src/cli/commands/mcp/status.ts +222 -0
- package/src/cli/commands/mcp/uninstall.ts +158 -0
- package/src/cli/commands/mcp.ts +20 -0
- package/src/cli/commands/models/clear.ts +103 -0
- package/src/cli/commands/models/index.ts +32 -0
- package/src/cli/commands/models/list.ts +214 -0
- package/src/cli/commands/models/path.ts +51 -0
- package/src/cli/commands/models/pull.ts +199 -0
- package/src/cli/commands/models/use.ts +85 -0
- package/src/cli/commands/multi-get.ts +400 -0
- package/src/cli/commands/query.ts +220 -0
- package/src/cli/commands/ref-parser.ts +108 -0
- package/src/cli/commands/reset.ts +191 -0
- package/src/cli/commands/search.ts +136 -0
- package/src/cli/commands/shared.ts +156 -0
- package/src/cli/commands/skill/index.ts +19 -0
- package/src/cli/commands/skill/install.ts +197 -0
- package/src/cli/commands/skill/paths-cmd.ts +81 -0
- package/src/cli/commands/skill/paths.ts +191 -0
- package/src/cli/commands/skill/show.ts +73 -0
- package/src/cli/commands/skill/uninstall.ts +141 -0
- package/src/cli/commands/status.ts +205 -0
- package/src/cli/commands/update.ts +68 -0
- package/src/cli/commands/vsearch.ts +188 -0
- package/src/cli/context.ts +64 -0
- package/src/cli/errors.ts +64 -0
- package/src/cli/format/search-results.ts +211 -0
- package/src/cli/options.ts +183 -0
- package/src/cli/program.ts +1330 -0
- package/src/cli/run.ts +213 -0
- package/src/cli/ui.ts +92 -0
- package/src/config/defaults.ts +20 -0
- package/src/config/index.ts +55 -0
- package/src/config/loader.ts +161 -0
- package/src/config/paths.ts +87 -0
- package/src/config/saver.ts +153 -0
- package/src/config/types.ts +280 -0
- package/src/converters/adapters/markitdownTs/adapter.ts +140 -0
- package/src/converters/adapters/officeparser/adapter.ts +126 -0
- package/src/converters/canonicalize.ts +89 -0
- package/src/converters/errors.ts +218 -0
- package/src/converters/index.ts +51 -0
- package/src/converters/mime.ts +163 -0
- package/src/converters/native/markdown.ts +115 -0
- package/src/converters/native/plaintext.ts +56 -0
- package/src/converters/path.ts +48 -0
- package/src/converters/pipeline.ts +159 -0
- package/src/converters/registry.ts +74 -0
- package/src/converters/types.ts +123 -0
- package/src/converters/versions.ts +24 -0
- package/src/index.ts +27 -0
- package/src/ingestion/chunker.ts +238 -0
- package/src/ingestion/index.ts +32 -0
- package/src/ingestion/language.ts +276 -0
- package/src/ingestion/sync.ts +671 -0
- package/src/ingestion/types.ts +219 -0
- package/src/ingestion/walker.ts +235 -0
- package/src/llm/cache.ts +467 -0
- package/src/llm/errors.ts +191 -0
- package/src/llm/index.ts +58 -0
- package/src/llm/nodeLlamaCpp/adapter.ts +133 -0
- package/src/llm/nodeLlamaCpp/embedding.ts +165 -0
- package/src/llm/nodeLlamaCpp/generation.ts +88 -0
- package/src/llm/nodeLlamaCpp/lifecycle.ts +317 -0
- package/src/llm/nodeLlamaCpp/rerank.ts +94 -0
- package/src/llm/registry.ts +86 -0
- package/src/llm/types.ts +129 -0
- package/src/mcp/resources/index.ts +151 -0
- package/src/mcp/server.ts +229 -0
- package/src/mcp/tools/get.ts +220 -0
- package/src/mcp/tools/index.ts +160 -0
- package/src/mcp/tools/multi-get.ts +263 -0
- package/src/mcp/tools/query.ts +226 -0
- package/src/mcp/tools/search.ts +119 -0
- package/src/mcp/tools/status.ts +81 -0
- package/src/mcp/tools/vsearch.ts +198 -0
- package/src/pipeline/chunk-lookup.ts +44 -0
- package/src/pipeline/expansion.ts +256 -0
- package/src/pipeline/explain.ts +115 -0
- package/src/pipeline/fusion.ts +185 -0
- package/src/pipeline/hybrid.ts +535 -0
- package/src/pipeline/index.ts +64 -0
- package/src/pipeline/query-language.ts +118 -0
- package/src/pipeline/rerank.ts +223 -0
- package/src/pipeline/search.ts +261 -0
- package/src/pipeline/types.ts +328 -0
- package/src/pipeline/vsearch.ts +348 -0
- package/src/store/index.ts +41 -0
- package/src/store/migrations/001-initial.ts +196 -0
- package/src/store/migrations/index.ts +20 -0
- package/src/store/migrations/runner.ts +187 -0
- package/src/store/sqlite/adapter.ts +1242 -0
- package/src/store/sqlite/index.ts +7 -0
- package/src/store/sqlite/setup.ts +129 -0
- package/src/store/sqlite/types.ts +28 -0
- package/src/store/types.ts +506 -0
- package/src/store/vector/index.ts +13 -0
- package/src/store/vector/sqlite-vec.ts +373 -0
- package/src/store/vector/stats.ts +152 -0
- package/src/store/vector/types.ts +115 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP gno_query tool - Hybrid search with expansion and reranking.
|
|
3
|
+
*
|
|
4
|
+
* @module src/mcp/tools/query
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join as pathJoin } from 'node:path';
|
|
8
|
+
import { parseUri } from '../../app/constants';
|
|
9
|
+
import { LlmAdapter } from '../../llm/nodeLlamaCpp/adapter';
|
|
10
|
+
import { getActivePreset } from '../../llm/registry';
|
|
11
|
+
import type {
|
|
12
|
+
EmbeddingPort,
|
|
13
|
+
GenerationPort,
|
|
14
|
+
RerankPort,
|
|
15
|
+
} from '../../llm/types';
|
|
16
|
+
import { type HybridSearchDeps, searchHybrid } from '../../pipeline/hybrid';
|
|
17
|
+
import type { SearchResult, SearchResults } from '../../pipeline/types';
|
|
18
|
+
import {
|
|
19
|
+
createVectorIndexPort,
|
|
20
|
+
type VectorIndexPort,
|
|
21
|
+
} from '../../store/vector';
|
|
22
|
+
import type { ToolContext } from '../server';
|
|
23
|
+
import { runTool, type ToolResult } from './index';
|
|
24
|
+
|
|
25
|
+
interface QueryInput {
|
|
26
|
+
query: string;
|
|
27
|
+
collection?: string;
|
|
28
|
+
limit?: number;
|
|
29
|
+
minScore?: number;
|
|
30
|
+
lang?: string;
|
|
31
|
+
expand?: boolean;
|
|
32
|
+
rerank?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Enrich results with absPath derived from each result's URI.
|
|
37
|
+
*/
|
|
38
|
+
function enrichWithAbsPath(
|
|
39
|
+
results: SearchResult[],
|
|
40
|
+
ctx: ToolContext
|
|
41
|
+
): SearchResult[] {
|
|
42
|
+
return results.map((r) => {
|
|
43
|
+
const parsed = parseUri(r.uri);
|
|
44
|
+
if (!parsed) {
|
|
45
|
+
return r;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const collection = ctx.collections.find(
|
|
49
|
+
(c) => c.name === parsed.collection
|
|
50
|
+
);
|
|
51
|
+
if (!collection) {
|
|
52
|
+
return r;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
...r,
|
|
57
|
+
source: {
|
|
58
|
+
...r.source,
|
|
59
|
+
absPath: pathJoin(collection.path, r.source.relPath),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Format search results as text for MCP content.
|
|
67
|
+
*/
|
|
68
|
+
function formatSearchResults(data: SearchResults): string {
|
|
69
|
+
if (data.results.length === 0) {
|
|
70
|
+
return `No results found for "${data.meta.query}"`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const lines: string[] = [];
|
|
74
|
+
const mode = data.meta.mode === 'bm25_only' ? 'BM25 only' : 'hybrid';
|
|
75
|
+
const flags: string[] = [];
|
|
76
|
+
if (data.meta.expanded) {
|
|
77
|
+
flags.push('expanded');
|
|
78
|
+
}
|
|
79
|
+
if (data.meta.reranked) {
|
|
80
|
+
flags.push('reranked');
|
|
81
|
+
}
|
|
82
|
+
if (data.meta.vectorsUsed) {
|
|
83
|
+
flags.push('vectors');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
lines.push(
|
|
87
|
+
`Found ${data.results.length} results for "${data.meta.query}" (${mode}${flags.length > 0 ? `, ${flags.join(', ')}` : ''}):`
|
|
88
|
+
);
|
|
89
|
+
lines.push('');
|
|
90
|
+
|
|
91
|
+
for (const r of data.results) {
|
|
92
|
+
lines.push(`[${r.docid}] ${r.uri} (score: ${r.score.toFixed(3)})`);
|
|
93
|
+
if (r.title) {
|
|
94
|
+
lines.push(` Title: ${r.title}`);
|
|
95
|
+
}
|
|
96
|
+
if (r.snippet) {
|
|
97
|
+
const snippetPreview = r.snippet.slice(0, 200).replace(/\n/g, ' ');
|
|
98
|
+
lines.push(` ${snippetPreview}${r.snippet.length > 200 ? '...' : ''}`);
|
|
99
|
+
}
|
|
100
|
+
lines.push('');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return lines.join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Handle gno_query tool call.
|
|
108
|
+
*/
|
|
109
|
+
export function handleQuery(
|
|
110
|
+
args: QueryInput,
|
|
111
|
+
ctx: ToolContext
|
|
112
|
+
): Promise<ToolResult> {
|
|
113
|
+
return runTool(
|
|
114
|
+
ctx,
|
|
115
|
+
'gno_query',
|
|
116
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: query with validation, hybrid search, and result formatting
|
|
117
|
+
async () => {
|
|
118
|
+
// Validate collection exists if specified
|
|
119
|
+
if (args.collection) {
|
|
120
|
+
const exists = ctx.collections.some((c) => c.name === args.collection);
|
|
121
|
+
if (!exists) {
|
|
122
|
+
throw new Error(`Collection not found: ${args.collection}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const preset = getActivePreset(ctx.config);
|
|
127
|
+
const llm = new LlmAdapter(ctx.config);
|
|
128
|
+
|
|
129
|
+
let embedPort: EmbeddingPort | null = null;
|
|
130
|
+
let genPort: GenerationPort | null = null;
|
|
131
|
+
let rerankPort: RerankPort | null = null;
|
|
132
|
+
let vectorIndex: VectorIndexPort | null = null;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
// Create embedding port (for vector search) - optional
|
|
136
|
+
const embedResult = await llm.createEmbeddingPort(preset.embed);
|
|
137
|
+
if (embedResult.ok) {
|
|
138
|
+
embedPort = embedResult.value;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Create generation port (for expansion) - optional
|
|
142
|
+
// expand defaults to true per spec
|
|
143
|
+
if (args.expand !== false) {
|
|
144
|
+
const genResult = await llm.createGenerationPort(preset.gen);
|
|
145
|
+
if (genResult.ok) {
|
|
146
|
+
genPort = genResult.value;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Create rerank port - optional
|
|
151
|
+
// rerank defaults to true per spec
|
|
152
|
+
if (args.rerank !== false) {
|
|
153
|
+
const rerankResult = await llm.createRerankPort(preset.rerank);
|
|
154
|
+
if (rerankResult.ok) {
|
|
155
|
+
rerankPort = rerankResult.value;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Create vector index (optional)
|
|
160
|
+
if (embedPort) {
|
|
161
|
+
const embedInitResult = await embedPort.init();
|
|
162
|
+
if (embedInitResult.ok) {
|
|
163
|
+
const dimensions = embedPort.dimensions();
|
|
164
|
+
const db = ctx.store.getRawDb();
|
|
165
|
+
const vectorResult = await createVectorIndexPort(db, {
|
|
166
|
+
model: preset.embed,
|
|
167
|
+
dimensions,
|
|
168
|
+
});
|
|
169
|
+
if (vectorResult.ok) {
|
|
170
|
+
vectorIndex = vectorResult.value;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const deps: HybridSearchDeps = {
|
|
176
|
+
store: ctx.store,
|
|
177
|
+
config: ctx.config,
|
|
178
|
+
vectorIndex,
|
|
179
|
+
embedPort,
|
|
180
|
+
genPort,
|
|
181
|
+
rerankPort,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Note: per spec, lang is a "hint" for query, not a filter
|
|
185
|
+
// Pass as queryLanguageHint to affect expansion prompt selection
|
|
186
|
+
// but NOT retrieval filtering (that would be options.lang)
|
|
187
|
+
const result = await searchHybrid(deps, args.query, {
|
|
188
|
+
limit: args.limit ?? 5,
|
|
189
|
+
minScore: args.minScore,
|
|
190
|
+
collection: args.collection,
|
|
191
|
+
queryLanguageHint: args.lang, // Affects expansion prompt, not retrieval
|
|
192
|
+
noExpand: args.expand === false,
|
|
193
|
+
noRerank: args.rerank === false,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (!result.ok) {
|
|
197
|
+
throw new Error(result.error.message);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Enrich with absPath
|
|
201
|
+
const enrichedResults = enrichWithAbsPath(result.value.results, ctx);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
...result.value,
|
|
205
|
+
results: enrichedResults,
|
|
206
|
+
meta: {
|
|
207
|
+
...result.value.meta,
|
|
208
|
+
// Add queryLanguage hint if provided
|
|
209
|
+
...(args.lang ? { queryLanguage: args.lang } : {}),
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
} finally {
|
|
213
|
+
if (embedPort) {
|
|
214
|
+
await embedPort.dispose();
|
|
215
|
+
}
|
|
216
|
+
if (genPort) {
|
|
217
|
+
await genPort.dispose();
|
|
218
|
+
}
|
|
219
|
+
if (rerankPort) {
|
|
220
|
+
await rerankPort.dispose();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
formatSearchResults
|
|
225
|
+
);
|
|
226
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP gno_search tool - BM25 full-text search.
|
|
3
|
+
*
|
|
4
|
+
* @module src/mcp/tools/search
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join as pathJoin } from 'node:path';
|
|
8
|
+
import { parseUri } from '../../app/constants';
|
|
9
|
+
import { searchBm25 } from '../../pipeline/search';
|
|
10
|
+
import type { SearchResult, SearchResults } from '../../pipeline/types';
|
|
11
|
+
import type { ToolContext } from '../server';
|
|
12
|
+
import { runTool, type ToolResult } from './index';
|
|
13
|
+
|
|
14
|
+
interface SearchInput {
|
|
15
|
+
query: string;
|
|
16
|
+
collection?: string;
|
|
17
|
+
limit?: number;
|
|
18
|
+
minScore?: number;
|
|
19
|
+
lang?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Enrich results with absPath derived from each result's URI.
|
|
24
|
+
*/
|
|
25
|
+
function enrichWithAbsPath(
|
|
26
|
+
results: SearchResult[],
|
|
27
|
+
ctx: ToolContext
|
|
28
|
+
): SearchResult[] {
|
|
29
|
+
return results.map((r) => {
|
|
30
|
+
const parsed = parseUri(r.uri);
|
|
31
|
+
if (!parsed) {
|
|
32
|
+
return r;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const collection = ctx.collections.find(
|
|
36
|
+
(c) => c.name === parsed.collection
|
|
37
|
+
);
|
|
38
|
+
if (!collection) {
|
|
39
|
+
return r;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
...r,
|
|
44
|
+
source: {
|
|
45
|
+
...r.source,
|
|
46
|
+
absPath: pathJoin(collection.path, r.source.relPath),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Format search results as text for MCP content.
|
|
54
|
+
*/
|
|
55
|
+
function formatSearchResults(data: SearchResults): string {
|
|
56
|
+
if (data.results.length === 0) {
|
|
57
|
+
return `No results found for "${data.meta.query}"`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const lines: string[] = [];
|
|
61
|
+
lines.push(`Found ${data.results.length} results for "${data.meta.query}":`);
|
|
62
|
+
lines.push('');
|
|
63
|
+
|
|
64
|
+
for (const r of data.results) {
|
|
65
|
+
lines.push(`[${r.docid}] ${r.uri} (score: ${r.score.toFixed(3)})`);
|
|
66
|
+
if (r.title) {
|
|
67
|
+
lines.push(` Title: ${r.title}`);
|
|
68
|
+
}
|
|
69
|
+
if (r.snippet) {
|
|
70
|
+
const snippetPreview = r.snippet.slice(0, 200).replace(/\n/g, ' ');
|
|
71
|
+
lines.push(` ${snippetPreview}${r.snippet.length > 200 ? '...' : ''}`);
|
|
72
|
+
}
|
|
73
|
+
lines.push('');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return lines.join('\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Handle gno_search tool call.
|
|
81
|
+
*/
|
|
82
|
+
export function handleSearch(
|
|
83
|
+
args: SearchInput,
|
|
84
|
+
ctx: ToolContext
|
|
85
|
+
): Promise<ToolResult> {
|
|
86
|
+
return runTool(
|
|
87
|
+
ctx,
|
|
88
|
+
'gno_search',
|
|
89
|
+
async () => {
|
|
90
|
+
// Validate collection exists if specified
|
|
91
|
+
if (args.collection) {
|
|
92
|
+
const exists = ctx.collections.some((c) => c.name === args.collection);
|
|
93
|
+
if (!exists) {
|
|
94
|
+
throw new Error(`Collection not found: ${args.collection}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const result = await searchBm25(ctx.store, args.query, {
|
|
99
|
+
limit: args.limit ?? 5,
|
|
100
|
+
minScore: args.minScore,
|
|
101
|
+
collection: args.collection,
|
|
102
|
+
lang: args.lang,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!result.ok) {
|
|
106
|
+
throw new Error(result.error.message);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Enrich with absPath
|
|
110
|
+
const enrichedResults = enrichWithAbsPath(result.value.results, ctx);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
...result.value,
|
|
114
|
+
results: enrichedResults,
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
formatSearchResults
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP gno_status tool - Index status and health.
|
|
3
|
+
*
|
|
4
|
+
* @module src/mcp/tools/status
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { IndexStatus } from '../../store/types';
|
|
8
|
+
import type { ToolContext } from '../server';
|
|
9
|
+
import { runTool, type ToolResult } from './index';
|
|
10
|
+
|
|
11
|
+
type StatusInput = Record<string, never>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Format status as text for MCP content.
|
|
15
|
+
*/
|
|
16
|
+
function formatStatus(status: IndexStatus): string {
|
|
17
|
+
const lines: string[] = [];
|
|
18
|
+
|
|
19
|
+
lines.push(`Index: ${status.indexName}`);
|
|
20
|
+
lines.push(`Config: ${status.configPath}`);
|
|
21
|
+
lines.push(`Database: ${status.dbPath}`);
|
|
22
|
+
lines.push(`Health: ${status.healthy ? 'OK' : 'DEGRADED'}`);
|
|
23
|
+
lines.push('');
|
|
24
|
+
|
|
25
|
+
if (status.collections.length === 0) {
|
|
26
|
+
lines.push('No collections configured.');
|
|
27
|
+
} else {
|
|
28
|
+
lines.push('Collections:');
|
|
29
|
+
for (const c of status.collections) {
|
|
30
|
+
lines.push(
|
|
31
|
+
` ${c.name}: ${c.activeDocuments} docs, ${c.totalChunks} chunks` +
|
|
32
|
+
(c.embeddedChunks > 0 ? `, ${c.embeddedChunks} embedded` : '')
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
lines.push('');
|
|
38
|
+
lines.push(
|
|
39
|
+
`Total: ${status.activeDocuments} documents, ${status.totalChunks} chunks`
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (status.embeddingBacklog > 0) {
|
|
43
|
+
lines.push(`Embedding backlog: ${status.embeddingBacklog} chunks`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (status.recentErrors > 0) {
|
|
47
|
+
lines.push(`Recent errors: ${status.recentErrors} (last 24h)`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (status.lastUpdatedAt) {
|
|
51
|
+
lines.push(`Last updated: ${status.lastUpdatedAt}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return lines.join('\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Handle gno_status tool call.
|
|
59
|
+
*/
|
|
60
|
+
export function handleStatus(
|
|
61
|
+
_args: StatusInput,
|
|
62
|
+
ctx: ToolContext
|
|
63
|
+
): Promise<ToolResult> {
|
|
64
|
+
return runTool(
|
|
65
|
+
ctx,
|
|
66
|
+
'gno_status',
|
|
67
|
+
async () => {
|
|
68
|
+
const result = await ctx.store.getStatus();
|
|
69
|
+
if (!result.ok) {
|
|
70
|
+
throw new Error(result.error.message);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Override configPath with actual path from context
|
|
74
|
+
return {
|
|
75
|
+
...result.value,
|
|
76
|
+
configPath: ctx.actualConfigPath,
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
formatStatus
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP gno_vsearch tool - Vector/semantic similarity search.
|
|
3
|
+
*
|
|
4
|
+
* @module src/mcp/tools/vsearch
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join as pathJoin } from 'node:path';
|
|
8
|
+
import { parseUri } from '../../app/constants';
|
|
9
|
+
import { LlmAdapter } from '../../llm/nodeLlamaCpp/adapter';
|
|
10
|
+
import { getActivePreset } from '../../llm/registry';
|
|
11
|
+
import type { SearchResult, SearchResults } from '../../pipeline/types';
|
|
12
|
+
import {
|
|
13
|
+
searchVectorWithEmbedding,
|
|
14
|
+
type VectorSearchDeps,
|
|
15
|
+
} from '../../pipeline/vsearch';
|
|
16
|
+
import { createVectorIndexPort } from '../../store/vector';
|
|
17
|
+
import type { ToolContext } from '../server';
|
|
18
|
+
import { runTool, type ToolResult } from './index';
|
|
19
|
+
|
|
20
|
+
interface VsearchInput {
|
|
21
|
+
query: string;
|
|
22
|
+
collection?: string;
|
|
23
|
+
limit?: number;
|
|
24
|
+
minScore?: number;
|
|
25
|
+
lang?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Enrich results with absPath derived from each result's URI.
|
|
30
|
+
*/
|
|
31
|
+
function enrichWithAbsPath(
|
|
32
|
+
results: SearchResult[],
|
|
33
|
+
ctx: ToolContext
|
|
34
|
+
): SearchResult[] {
|
|
35
|
+
return results.map((r) => {
|
|
36
|
+
const parsed = parseUri(r.uri);
|
|
37
|
+
if (!parsed) {
|
|
38
|
+
return r;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const collection = ctx.collections.find(
|
|
42
|
+
(c) => c.name === parsed.collection
|
|
43
|
+
);
|
|
44
|
+
if (!collection) {
|
|
45
|
+
return r;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
...r,
|
|
50
|
+
source: {
|
|
51
|
+
...r.source,
|
|
52
|
+
absPath: pathJoin(collection.path, r.source.relPath),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Format search results as text for MCP content.
|
|
60
|
+
*/
|
|
61
|
+
function formatSearchResults(data: SearchResults): string {
|
|
62
|
+
if (data.results.length === 0) {
|
|
63
|
+
return `No results found for "${data.meta.query}"`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const lines: string[] = [];
|
|
67
|
+
lines.push(
|
|
68
|
+
`Found ${data.results.length} results for "${data.meta.query}" (vector search):`
|
|
69
|
+
);
|
|
70
|
+
lines.push('');
|
|
71
|
+
|
|
72
|
+
for (const r of data.results) {
|
|
73
|
+
lines.push(`[${r.docid}] ${r.uri} (score: ${r.score.toFixed(3)})`);
|
|
74
|
+
if (r.title) {
|
|
75
|
+
lines.push(` Title: ${r.title}`);
|
|
76
|
+
}
|
|
77
|
+
if (r.snippet) {
|
|
78
|
+
const snippetPreview = r.snippet.slice(0, 200).replace(/\n/g, ' ');
|
|
79
|
+
lines.push(` ${snippetPreview}${r.snippet.length > 200 ? '...' : ''}`);
|
|
80
|
+
}
|
|
81
|
+
lines.push('');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return lines.join('\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Handle gno_vsearch tool call.
|
|
89
|
+
*/
|
|
90
|
+
export function handleVsearch(
|
|
91
|
+
args: VsearchInput,
|
|
92
|
+
ctx: ToolContext
|
|
93
|
+
): Promise<ToolResult> {
|
|
94
|
+
return runTool(
|
|
95
|
+
ctx,
|
|
96
|
+
'gno_vsearch',
|
|
97
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: vector search with validation and result formatting
|
|
98
|
+
async () => {
|
|
99
|
+
// Validate collection exists if specified
|
|
100
|
+
if (args.collection) {
|
|
101
|
+
const exists = ctx.collections.some((c) => c.name === args.collection);
|
|
102
|
+
if (!exists) {
|
|
103
|
+
throw new Error(`Collection not found: ${args.collection}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Get model from active preset
|
|
108
|
+
const preset = getActivePreset(ctx.config);
|
|
109
|
+
const modelUri = preset.embed;
|
|
110
|
+
|
|
111
|
+
// Create LLM adapter for embeddings
|
|
112
|
+
const llm = new LlmAdapter(ctx.config);
|
|
113
|
+
const embedResult = await llm.createEmbeddingPort(modelUri);
|
|
114
|
+
if (!embedResult.ok) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Failed to load embedding model: ${embedResult.error.message}. ` +
|
|
117
|
+
'Ensure models are downloaded with: gno models pull'
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const embedPort = embedResult.value;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
// Embed query
|
|
125
|
+
const queryEmbedResult = await embedPort.embed(args.query);
|
|
126
|
+
if (!queryEmbedResult.ok) {
|
|
127
|
+
throw new Error(queryEmbedResult.error.message);
|
|
128
|
+
}
|
|
129
|
+
const queryEmbedding = new Float32Array(queryEmbedResult.value);
|
|
130
|
+
const dimensions = queryEmbedding.length;
|
|
131
|
+
|
|
132
|
+
// Create vector index port
|
|
133
|
+
const db = ctx.store.getRawDb();
|
|
134
|
+
const vectorResult = await createVectorIndexPort(db, {
|
|
135
|
+
model: modelUri,
|
|
136
|
+
dimensions,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!vectorResult.ok) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Vector index not available: ${vectorResult.error.message}. ` +
|
|
142
|
+
'Run: gno embed'
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const vectorIndex = vectorResult.value;
|
|
147
|
+
|
|
148
|
+
if (!vectorIndex.searchAvailable) {
|
|
149
|
+
const reason = vectorIndex.loadError
|
|
150
|
+
? `sqlite-vec not loaded: ${vectorIndex.loadError}`
|
|
151
|
+
: 'sqlite-vec not available';
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Vector search unavailable (${reason}). ` +
|
|
154
|
+
'Ensure sqlite-vec is installed for your platform.'
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const deps: VectorSearchDeps = {
|
|
159
|
+
store: ctx.store,
|
|
160
|
+
vectorIndex,
|
|
161
|
+
embedPort,
|
|
162
|
+
config: ctx.config,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const result = await searchVectorWithEmbedding(
|
|
166
|
+
deps,
|
|
167
|
+
args.query,
|
|
168
|
+
queryEmbedding,
|
|
169
|
+
{
|
|
170
|
+
limit: args.limit ?? 5,
|
|
171
|
+
minScore: args.minScore,
|
|
172
|
+
collection: args.collection,
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (!result.ok) {
|
|
177
|
+
throw new Error(result.error.message);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Enrich with absPath
|
|
181
|
+
const enrichedResults = enrichWithAbsPath(result.value.results, ctx);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
...result.value,
|
|
185
|
+
results: enrichedResults,
|
|
186
|
+
meta: {
|
|
187
|
+
...result.value.meta,
|
|
188
|
+
// Add queryLanguage hint if provided (per spec, lang is a hint for vsearch)
|
|
189
|
+
...(args.lang ? { queryLanguage: args.lang } : {}),
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
} finally {
|
|
193
|
+
await embedPort.dispose();
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
formatSearchResults
|
|
197
|
+
);
|
|
198
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chunk lookup optimization.
|
|
3
|
+
* Provides O(1) chunk lookup by (hash, seq) instead of O(n) linear scan.
|
|
4
|
+
*
|
|
5
|
+
* Performance tradeoff: Pays O(k) Map build cost per hash on first access
|
|
6
|
+
* to guarantee O(1) lookups thereafter. For one-off lookups, .find() might
|
|
7
|
+
* be marginally faster, but we prefer consistent O(1) access across
|
|
8
|
+
* pipelines where multiple lookups per hash are common.
|
|
9
|
+
*
|
|
10
|
+
* @module src/pipeline/chunk-lookup
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ChunkRow } from '../store/types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create O(1) chunk lookup function from batch-fetched chunks.
|
|
17
|
+
* Lazily indexes chunks by seq on first access per hash.
|
|
18
|
+
* First-wins semantics preserves original .find() behavior for duplicate seq.
|
|
19
|
+
*
|
|
20
|
+
* @param chunksMap - Map from getChunksBatch()
|
|
21
|
+
* @returns Lookup function (hash, seq) => ChunkRow | undefined
|
|
22
|
+
*/
|
|
23
|
+
export function createChunkLookup(
|
|
24
|
+
chunksMap: Map<string, ChunkRow[]>
|
|
25
|
+
): (hash: string, seq: number) => ChunkRow | undefined {
|
|
26
|
+
// Lazy cache: hash -> (seq -> chunk)
|
|
27
|
+
const indexCache = new Map<string, Map<number, ChunkRow>>();
|
|
28
|
+
|
|
29
|
+
return (hash: string, seq: number): ChunkRow | undefined => {
|
|
30
|
+
let bySeq = indexCache.get(hash);
|
|
31
|
+
if (!bySeq) {
|
|
32
|
+
const chunks = chunksMap.get(hash) ?? [];
|
|
33
|
+
bySeq = new Map<number, ChunkRow>();
|
|
34
|
+
// First-wins: preserve .find() behavior for degenerate duplicate seq
|
|
35
|
+
for (const ch of chunks) {
|
|
36
|
+
if (!bySeq.has(ch.seq)) {
|
|
37
|
+
bySeq.set(ch.seq, ch);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
indexCache.set(hash, bySeq);
|
|
41
|
+
}
|
|
42
|
+
return bySeq.get(seq);
|
|
43
|
+
};
|
|
44
|
+
}
|