@gmickel/gno 1.2.1 → 1.3.1
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 +1 -1
- package/assets/skill/SKILL.md +3 -0
- package/assets/skill/cli-reference.md +5 -0
- package/assets/skill/examples.md +2 -0
- package/package.json +1 -1
- package/src/app/constants.ts +64 -8
- package/src/cli/commands/embed.ts +6 -2
- package/src/cli/commands/get.ts +15 -5
- package/src/cli/commands/index-cmd.ts +4 -0
- package/src/cli/commands/multi-get.ts +62 -1
- package/src/cli/commands/query.ts +8 -2
- package/src/cli/commands/search.ts +8 -2
- package/src/cli/commands/shared.ts +18 -1
- package/src/cli/commands/status.ts +4 -2
- package/src/cli/commands/update.ts +6 -1
- package/src/cli/commands/vsearch.ts +8 -2
- package/src/cli/format/search-results.ts +1 -1
- package/src/cli/program.ts +22 -1
- package/src/core/user-dirs.ts +21 -12
- package/src/ingestion/chunker.ts +6 -0
- package/src/llm/cache.ts +133 -27
- package/src/llm/errors.ts +32 -0
- package/src/llm/nodeLlamaCpp/embedding.ts +69 -3
- package/src/llm/nodeLlamaCpp/lifecycle.ts +60 -4
- package/src/mcp/resources/index.ts +13 -4
- package/src/mcp/server.ts +2 -0
- package/src/mcp/tools/get.ts +7 -2
- package/src/mcp/tools/multi-get.ts +2 -2
- package/src/mcp/tools/query.ts +2 -1
- package/src/mcp/tools/search.ts +2 -1
- package/src/mcp/tools/vsearch.ts +2 -1
- package/src/pipeline/explain.ts +12 -2
- package/src/pipeline/hybrid.ts +9 -1
- package/src/pipeline/search.ts +1 -0
- package/src/pipeline/types.ts +2 -0
- package/src/pipeline/vsearch.ts +14 -8
- package/src/sdk/client.ts +83 -28
- package/src/store/sqlite/adapter.ts +8 -7
- package/src/store/vector/sqlite-vec.ts +10 -4
- package/src/store/vector/types.ts +2 -0
|
@@ -13,7 +13,12 @@ import { join as pathJoin } from "node:path";
|
|
|
13
13
|
import type { DocumentRow, TagCount } from "../../store/types";
|
|
14
14
|
import type { ToolContext } from "../server";
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
buildUri,
|
|
18
|
+
decorateUriForIndex,
|
|
19
|
+
parseUri,
|
|
20
|
+
URI_PREFIX,
|
|
21
|
+
} from "../../app/constants";
|
|
17
22
|
import { MCP_ERRORS } from "../../core/errors";
|
|
18
23
|
import { normalizeTag, validateTag } from "../../core/tags";
|
|
19
24
|
import { normalizeCollectionName } from "../../core/validation";
|
|
@@ -64,7 +69,8 @@ function formatResourceContent(
|
|
|
64
69
|
const langLine = doc.languageHint
|
|
65
70
|
? `\n language: ${doc.languageHint}`
|
|
66
71
|
: "";
|
|
67
|
-
const
|
|
72
|
+
const displayUri = decorateUriForIndex(doc.uri, ctx.indexName);
|
|
73
|
+
const header = `<!-- ${displayUri}
|
|
68
74
|
docid: ${doc.docid}
|
|
69
75
|
source: ${absPath}
|
|
70
76
|
mime: ${doc.sourceMime}${langLine}
|
|
@@ -94,7 +100,7 @@ export function registerResources(server: McpServer, ctx: ToolContext): void {
|
|
|
94
100
|
|
|
95
101
|
return {
|
|
96
102
|
resources: listResult.value.map((doc) => ({
|
|
97
|
-
uri: doc.uri,
|
|
103
|
+
uri: decorateUriForIndex(doc.uri, ctx.indexName),
|
|
98
104
|
name: doc.relPath,
|
|
99
105
|
mimeType: doc.sourceMime || "text/markdown",
|
|
100
106
|
description: doc.title ?? undefined,
|
|
@@ -160,7 +166,10 @@ export function registerResources(server: McpServer, ctx: ToolContext): void {
|
|
|
160
166
|
const formattedContent = formatResourceContent(doc, content, ctx);
|
|
161
167
|
|
|
162
168
|
// Build canonical URI
|
|
163
|
-
const canonicalUri =
|
|
169
|
+
const canonicalUri = decorateUriForIndex(
|
|
170
|
+
buildUri(collection, path),
|
|
171
|
+
parsed.indexName ?? ctx.indexName
|
|
172
|
+
);
|
|
164
173
|
|
|
165
174
|
return {
|
|
166
175
|
contents: [
|
package/src/mcp/server.ts
CHANGED
|
@@ -57,6 +57,7 @@ export interface ToolContext {
|
|
|
57
57
|
config: Config;
|
|
58
58
|
collections: Collection[];
|
|
59
59
|
actualConfigPath: string;
|
|
60
|
+
indexName?: string;
|
|
60
61
|
toolMutex: Mutex;
|
|
61
62
|
jobManager: JobManager;
|
|
62
63
|
serverInstanceId: string;
|
|
@@ -164,6 +165,7 @@ export async function startMcpServer(options: McpServerOptions): Promise<void> {
|
|
|
164
165
|
config,
|
|
165
166
|
collections,
|
|
166
167
|
actualConfigPath,
|
|
168
|
+
indexName: options.indexName,
|
|
167
169
|
toolMutex,
|
|
168
170
|
jobManager,
|
|
169
171
|
serverInstanceId,
|
package/src/mcp/tools/get.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { join as pathJoin } from "node:path";
|
|
|
9
9
|
import type { DocumentRow, StorePort } from "../../store/types";
|
|
10
10
|
import type { ToolContext } from "../server";
|
|
11
11
|
|
|
12
|
-
import { parseUri } from "../../app/constants";
|
|
12
|
+
import { decorateUriForIndex, parseUri } from "../../app/constants";
|
|
13
13
|
import { parseRef } from "../../cli/commands/ref-parser";
|
|
14
14
|
import {
|
|
15
15
|
getDocumentCapabilities,
|
|
@@ -196,7 +196,12 @@ export function handleGet(
|
|
|
196
196
|
|
|
197
197
|
const response: GetResponse = {
|
|
198
198
|
docid: doc.docid,
|
|
199
|
-
uri:
|
|
199
|
+
uri: decorateUriForIndex(
|
|
200
|
+
doc.uri,
|
|
201
|
+
parsed.type === "uri"
|
|
202
|
+
? (parseUri(parsed.value)?.indexName ?? ctx.indexName)
|
|
203
|
+
: ctx.indexName
|
|
204
|
+
),
|
|
200
205
|
title: doc.title ?? undefined,
|
|
201
206
|
content,
|
|
202
207
|
totalLines,
|
|
@@ -9,7 +9,7 @@ import { join as pathJoin } from "node:path";
|
|
|
9
9
|
import type { DocumentRow, StorePort } from "../../store/types";
|
|
10
10
|
import type { ToolContext } from "../server";
|
|
11
11
|
|
|
12
|
-
import { parseUri } from "../../app/constants";
|
|
12
|
+
import { decorateUriForIndex, parseUri } from "../../app/constants";
|
|
13
13
|
import { parseRef } from "../../cli/commands/ref-parser";
|
|
14
14
|
import { runTool, type ToolResult } from "./index";
|
|
15
15
|
|
|
@@ -232,7 +232,7 @@ export function handleMultiGet(
|
|
|
232
232
|
|
|
233
233
|
documents.push({
|
|
234
234
|
docid: doc.docid,
|
|
235
|
-
uri: doc.uri,
|
|
235
|
+
uri: decorateUriForIndex(doc.uri, ctx.indexName),
|
|
236
236
|
title: doc.title ?? undefined,
|
|
237
237
|
content,
|
|
238
238
|
totalLines: (contentResult.value ?? "").split("\n").length,
|
package/src/mcp/tools/query.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
} from "../../pipeline/types";
|
|
19
19
|
import type { ToolContext } from "../server";
|
|
20
20
|
|
|
21
|
-
import { parseUri } from "../../app/constants";
|
|
21
|
+
import { decorateUriForIndex, parseUri } from "../../app/constants";
|
|
22
22
|
import { createNonTtyProgressRenderer } from "../../cli/progress";
|
|
23
23
|
import { resolveDepthPolicy } from "../../core/depth-policy";
|
|
24
24
|
import { normalizeStructuredQueryInput } from "../../core/structured-query";
|
|
@@ -76,6 +76,7 @@ function enrichWithAbsPath(
|
|
|
76
76
|
|
|
77
77
|
return {
|
|
78
78
|
...r,
|
|
79
|
+
uri: decorateUriForIndex(r.uri, ctx.indexName),
|
|
79
80
|
source: {
|
|
80
81
|
...r.source,
|
|
81
82
|
absPath: pathJoin(collection.path, r.source.relPath),
|
package/src/mcp/tools/search.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { join as pathJoin } from "node:path";
|
|
|
9
9
|
import type { SearchResult, SearchResults } from "../../pipeline/types";
|
|
10
10
|
import type { ToolContext } from "../server";
|
|
11
11
|
|
|
12
|
-
import { parseUri } from "../../app/constants";
|
|
12
|
+
import { decorateUriForIndex, parseUri } from "../../app/constants";
|
|
13
13
|
import { searchBm25 } from "../../pipeline/search";
|
|
14
14
|
import { normalizeTagFilters, runTool, type ToolResult } from "./index";
|
|
15
15
|
|
|
@@ -51,6 +51,7 @@ function enrichWithAbsPath(
|
|
|
51
51
|
|
|
52
52
|
return {
|
|
53
53
|
...r,
|
|
54
|
+
uri: decorateUriForIndex(r.uri, ctx.indexName),
|
|
54
55
|
source: {
|
|
55
56
|
...r.source,
|
|
56
57
|
absPath: pathJoin(collection.path, r.source.relPath),
|
package/src/mcp/tools/vsearch.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { join as pathJoin } from "node:path";
|
|
|
9
9
|
import type { SearchResult, SearchResults } from "../../pipeline/types";
|
|
10
10
|
import type { ToolContext } from "../server";
|
|
11
11
|
|
|
12
|
-
import { parseUri } from "../../app/constants";
|
|
12
|
+
import { decorateUriForIndex, parseUri } from "../../app/constants";
|
|
13
13
|
import { createNonTtyProgressRenderer } from "../../cli/progress";
|
|
14
14
|
import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
|
|
15
15
|
import { resolveDownloadPolicy } from "../../llm/policy";
|
|
@@ -60,6 +60,7 @@ function enrichWithAbsPath(
|
|
|
60
60
|
|
|
61
61
|
return {
|
|
62
62
|
...r,
|
|
63
|
+
uri: decorateUriForIndex(r.uri, ctx.indexName),
|
|
63
64
|
source: {
|
|
64
65
|
...r.source,
|
|
65
66
|
absPath: pathJoin(collection.path, r.source.relPath),
|
package/src/pipeline/explain.ts
CHANGED
|
@@ -122,9 +122,19 @@ export function explainBm25(count: number): ExplainLine {
|
|
|
122
122
|
return { stage: "bm25", message: `${count} candidates` };
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
export function explainVector(
|
|
125
|
+
export function explainVector(
|
|
126
|
+
count: number,
|
|
127
|
+
available: boolean,
|
|
128
|
+
unavailableReason?: string,
|
|
129
|
+
guidance?: string
|
|
130
|
+
): ExplainLine {
|
|
126
131
|
if (!available) {
|
|
127
|
-
|
|
132
|
+
const detail = unavailableReason ? `: ${unavailableReason}` : "";
|
|
133
|
+
const hint = guidance ? `; ${guidance}` : "";
|
|
134
|
+
return {
|
|
135
|
+
stage: "vector",
|
|
136
|
+
message: `unavailable (sqlite-vec not loaded${detail})${hint}`,
|
|
137
|
+
};
|
|
128
138
|
}
|
|
129
139
|
return { stage: "vector", message: `${count} candidates` };
|
|
130
140
|
}
|
package/src/pipeline/hybrid.ts
CHANGED
|
@@ -528,7 +528,14 @@ export async function searchHybrid(
|
|
|
528
528
|
}
|
|
529
529
|
timings.vectorMs = performance.now() - vectorStartedAt;
|
|
530
530
|
|
|
531
|
-
explainLines.push(
|
|
531
|
+
explainLines.push(
|
|
532
|
+
explainVector(
|
|
533
|
+
vecCount,
|
|
534
|
+
vectorAvailable,
|
|
535
|
+
vectorIndex?.loadError,
|
|
536
|
+
vectorIndex?.guidance
|
|
537
|
+
)
|
|
538
|
+
);
|
|
532
539
|
|
|
533
540
|
// ─────────────────────────────────────────────────────────────────────────
|
|
534
541
|
// 3. RRF Fusion
|
|
@@ -800,6 +807,7 @@ export async function searchHybrid(
|
|
|
800
807
|
score: candidate.blendedScore,
|
|
801
808
|
uri: doc.uri,
|
|
802
809
|
title: doc.title ?? undefined,
|
|
810
|
+
line: snippetChunk.startLine,
|
|
803
811
|
snippet,
|
|
804
812
|
snippetLanguage: chunk.language ?? undefined,
|
|
805
813
|
snippetRange,
|
package/src/pipeline/search.ts
CHANGED
|
@@ -117,6 +117,7 @@ function buildSearchResult(ctx: BuildResultContext): SearchResult {
|
|
|
117
117
|
score: fts.score, // Raw score, normalized later as batch
|
|
118
118
|
uri: fts.uri ?? "",
|
|
119
119
|
title: fts.title,
|
|
120
|
+
line: chunk?.startLine,
|
|
120
121
|
snippet,
|
|
121
122
|
snippetLanguage: chunk?.language ?? undefined,
|
|
122
123
|
snippetRange,
|
package/src/pipeline/types.ts
CHANGED
package/src/pipeline/vsearch.ts
CHANGED
|
@@ -50,6 +50,16 @@ export interface VectorSearchDeps {
|
|
|
50
50
|
config: Config;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function vectorUnavailableMessage(vectorIndex: VectorIndexPort): string {
|
|
54
|
+
const reason = vectorIndex.loadError
|
|
55
|
+
? ` Reason: ${vectorIndex.loadError}`
|
|
56
|
+
: "";
|
|
57
|
+
const guidance = vectorIndex.guidance
|
|
58
|
+
? ` ${vectorIndex.guidance}`
|
|
59
|
+
: " Run: gno doctor";
|
|
60
|
+
return `Vector search unavailable.${reason}${guidance}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
64
|
// Search Function (with pre-computed embedding)
|
|
55
65
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -82,10 +92,7 @@ export async function searchVectorWithEmbedding(
|
|
|
82
92
|
|
|
83
93
|
// Check if vector search is available
|
|
84
94
|
if (!vectorIndex.searchAvailable) {
|
|
85
|
-
return err(
|
|
86
|
-
"VEC_SEARCH_UNAVAILABLE",
|
|
87
|
-
"Vector search requires sqlite-vec. Run: gno embed"
|
|
88
|
-
);
|
|
95
|
+
return err("VEC_SEARCH_UNAVAILABLE", vectorUnavailableMessage(vectorIndex));
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
// Search nearest neighbors
|
|
@@ -220,6 +227,7 @@ export async function searchVectorWithEmbedding(
|
|
|
220
227
|
score,
|
|
221
228
|
uri: doc.uri,
|
|
222
229
|
title: doc.title ?? undefined,
|
|
230
|
+
line: chunk.startLine,
|
|
223
231
|
snippet: chunk.text,
|
|
224
232
|
snippetLanguage: chunk.language ?? undefined,
|
|
225
233
|
snippetRange: {
|
|
@@ -273,6 +281,7 @@ export async function searchVectorWithEmbedding(
|
|
|
273
281
|
score,
|
|
274
282
|
uri: doc.uri,
|
|
275
283
|
title: doc.title ?? undefined,
|
|
284
|
+
line: chunk.startLine,
|
|
276
285
|
snippet: fullContent ?? chunk.text,
|
|
277
286
|
snippetLanguage: chunk.language ?? undefined,
|
|
278
287
|
// --full: no snippetRange (full doc content)
|
|
@@ -354,10 +363,7 @@ export async function searchVector(
|
|
|
354
363
|
|
|
355
364
|
// Check if vector search is available
|
|
356
365
|
if (!vectorIndex.searchAvailable) {
|
|
357
|
-
return err(
|
|
358
|
-
"VEC_SEARCH_UNAVAILABLE",
|
|
359
|
-
"Vector search requires sqlite-vec. Run: gno embed"
|
|
360
|
-
);
|
|
366
|
+
return err("VEC_SEARCH_UNAVAILABLE", vectorUnavailableMessage(vectorIndex));
|
|
361
367
|
}
|
|
362
368
|
|
|
363
369
|
// Embed query with contextual formatting
|
package/src/sdk/client.ts
CHANGED
|
@@ -37,7 +37,11 @@ import type {
|
|
|
37
37
|
GnoVectorSearchOptions,
|
|
38
38
|
} from "./types";
|
|
39
39
|
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
decorateUriForIndex,
|
|
42
|
+
getIndexDbPath,
|
|
43
|
+
parseUri,
|
|
44
|
+
} from "../app/constants";
|
|
41
45
|
import { ConfigSchema, loadConfig } from "../config";
|
|
42
46
|
import {
|
|
43
47
|
atomicWrite,
|
|
@@ -88,6 +92,7 @@ interface OpenedClientState {
|
|
|
88
92
|
store: SqliteAdapter;
|
|
89
93
|
llm: LlmAdapter;
|
|
90
94
|
downloadPolicy: DownloadPolicy;
|
|
95
|
+
indexName?: string;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
interface RuntimePorts {
|
|
@@ -158,6 +163,7 @@ async function resolveClientState(
|
|
|
158
163
|
llm: new LlmAdapter(config, options.cacheDir),
|
|
159
164
|
downloadPolicy:
|
|
160
165
|
options.downloadPolicy ?? resolveDownloadPolicy(process.env, {}),
|
|
166
|
+
indexName: options.indexName,
|
|
161
167
|
};
|
|
162
168
|
}
|
|
163
169
|
|
|
@@ -170,6 +176,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
170
176
|
private readonly store: SqliteAdapter;
|
|
171
177
|
private readonly llm: LlmAdapter;
|
|
172
178
|
private readonly downloadPolicy: DownloadPolicy;
|
|
179
|
+
private readonly indexName?: string;
|
|
173
180
|
private closed = false;
|
|
174
181
|
|
|
175
182
|
constructor(state: OpenedClientState) {
|
|
@@ -180,6 +187,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
180
187
|
this.store = state.store;
|
|
181
188
|
this.llm = state.llm;
|
|
182
189
|
this.downloadPolicy = state.downloadPolicy;
|
|
190
|
+
this.indexName = state.indexName;
|
|
183
191
|
}
|
|
184
192
|
|
|
185
193
|
isOpen(): boolean {
|
|
@@ -371,12 +379,24 @@ class GnoClientImpl implements GnoClient {
|
|
|
371
379
|
}
|
|
372
380
|
}
|
|
373
381
|
|
|
382
|
+
private decorateSearchResults(results: SearchResults): SearchResults {
|
|
383
|
+
return {
|
|
384
|
+
...results,
|
|
385
|
+
results: results.results.map((result) => ({
|
|
386
|
+
...result,
|
|
387
|
+
uri: decorateUriForIndex(result.uri, this.indexName),
|
|
388
|
+
})),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
374
392
|
async search(
|
|
375
393
|
query: string,
|
|
376
394
|
options: import("../pipeline/types").SearchOptions = {}
|
|
377
395
|
): Promise<SearchResults> {
|
|
378
396
|
this.assertOpen();
|
|
379
|
-
return
|
|
397
|
+
return this.decorateSearchResults(
|
|
398
|
+
unwrapStore(await searchBm25(this.store, query, options))
|
|
399
|
+
);
|
|
380
400
|
}
|
|
381
401
|
|
|
382
402
|
async vsearch(
|
|
@@ -409,17 +429,19 @@ class GnoClientImpl implements GnoClient {
|
|
|
409
429
|
});
|
|
410
430
|
}
|
|
411
431
|
|
|
412
|
-
return
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
432
|
+
return this.decorateSearchResults(
|
|
433
|
+
unwrapStore(
|
|
434
|
+
await searchVectorWithEmbedding(
|
|
435
|
+
{
|
|
436
|
+
store: this.store,
|
|
437
|
+
vectorIndex: ports.vectorIndex,
|
|
438
|
+
embedPort: ports.embedPort,
|
|
439
|
+
config: this.config,
|
|
440
|
+
},
|
|
441
|
+
query,
|
|
442
|
+
new Float32Array(queryEmbedResult.value),
|
|
443
|
+
options
|
|
444
|
+
)
|
|
423
445
|
)
|
|
424
446
|
);
|
|
425
447
|
} finally {
|
|
@@ -461,18 +483,20 @@ class GnoClientImpl implements GnoClient {
|
|
|
461
483
|
});
|
|
462
484
|
|
|
463
485
|
try {
|
|
464
|
-
return
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
486
|
+
return this.decorateSearchResults(
|
|
487
|
+
unwrapStore(
|
|
488
|
+
await searchHybrid(
|
|
489
|
+
{
|
|
490
|
+
store: this.store,
|
|
491
|
+
config: this.config,
|
|
492
|
+
vectorIndex: ports.vectorIndex,
|
|
493
|
+
embedPort: ports.embedPort,
|
|
494
|
+
expandPort: ports.expandPort,
|
|
495
|
+
rerankPort: ports.rerankPort,
|
|
496
|
+
},
|
|
497
|
+
query,
|
|
498
|
+
options
|
|
499
|
+
)
|
|
476
500
|
)
|
|
477
501
|
);
|
|
478
502
|
} finally {
|
|
@@ -606,17 +630,48 @@ class GnoClientImpl implements GnoClient {
|
|
|
606
630
|
|
|
607
631
|
async get(ref: string, options: GnoGetOptions = {}) {
|
|
608
632
|
this.assertOpen();
|
|
609
|
-
|
|
633
|
+
const explicitIndex = ref.startsWith("gno://")
|
|
634
|
+
? parseUri(ref)?.indexName
|
|
635
|
+
: undefined;
|
|
636
|
+
const result = await getDocumentByRef(
|
|
637
|
+
this.store,
|
|
638
|
+
this.config,
|
|
639
|
+
ref,
|
|
640
|
+
options
|
|
641
|
+
);
|
|
642
|
+
return {
|
|
643
|
+
...result,
|
|
644
|
+
uri: decorateUriForIndex(result.uri, explicitIndex ?? this.indexName),
|
|
645
|
+
};
|
|
610
646
|
}
|
|
611
647
|
|
|
612
648
|
async multiGet(refs: string[], options: GnoMultiGetOptions = {}) {
|
|
613
649
|
this.assertOpen();
|
|
614
|
-
|
|
650
|
+
const result = await multiGetDocuments(
|
|
651
|
+
this.store,
|
|
652
|
+
this.config,
|
|
653
|
+
refs,
|
|
654
|
+
options
|
|
655
|
+
);
|
|
656
|
+
return {
|
|
657
|
+
...result,
|
|
658
|
+
documents: result.documents.map((doc) => ({
|
|
659
|
+
...doc,
|
|
660
|
+
uri: decorateUriForIndex(doc.uri, this.indexName),
|
|
661
|
+
})),
|
|
662
|
+
};
|
|
615
663
|
}
|
|
616
664
|
|
|
617
665
|
async list(options: GnoListOptions = {}) {
|
|
618
666
|
this.assertOpen();
|
|
619
|
-
|
|
667
|
+
const result = await listDocuments(this.store, options);
|
|
668
|
+
return {
|
|
669
|
+
...result,
|
|
670
|
+
documents: result.documents.map((doc) => ({
|
|
671
|
+
...doc,
|
|
672
|
+
uri: decorateUriForIndex(doc.uri, this.indexName),
|
|
673
|
+
})),
|
|
674
|
+
};
|
|
620
675
|
}
|
|
621
676
|
|
|
622
677
|
async status(): Promise<IndexStatus> {
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// CRITICAL: Import setup FIRST to configure custom SQLite before any Database use
|
|
11
11
|
import "./setup";
|
|
12
12
|
import { Database } from "bun:sqlite";
|
|
13
|
+
// node:path basename: no Bun path utilities.
|
|
14
|
+
import { basename } from "node:path";
|
|
13
15
|
|
|
14
16
|
import type { Collection, Context, FtsTokenizer } from "../../config/types";
|
|
15
17
|
import type {
|
|
@@ -42,7 +44,7 @@ import type {
|
|
|
42
44
|
} from "../types";
|
|
43
45
|
import type { SqliteDbProvider } from "./types";
|
|
44
46
|
|
|
45
|
-
import { buildUri, deriveDocid } from "../../app/constants";
|
|
47
|
+
import { buildUri, deriveDocid, stripUriIndex } from "../../app/constants";
|
|
46
48
|
import { normalizeWikiName, stripWikiMdExt } from "../../core/links";
|
|
47
49
|
import { migrations, runMigrations } from "../migrations";
|
|
48
50
|
import { err, ok } from "../types";
|
|
@@ -675,9 +677,10 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
675
677
|
): Promise<StoreResult<DocumentRow | null>> {
|
|
676
678
|
try {
|
|
677
679
|
const db = this.ensureOpen();
|
|
680
|
+
const canonicalUri = stripUriIndex(uri);
|
|
678
681
|
const row = db
|
|
679
682
|
.query<DbDocumentRow, [string]>("SELECT * FROM documents WHERE uri = ?")
|
|
680
|
-
.get(
|
|
683
|
+
.get(canonicalUri);
|
|
681
684
|
|
|
682
685
|
return ok(row ? mapDocumentRow(row) : null);
|
|
683
686
|
} catch (cause) {
|
|
@@ -2750,11 +2753,9 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2750
2753
|
|
|
2751
2754
|
// Derive indexName from dbPath (basename without extension)
|
|
2752
2755
|
const indexName =
|
|
2753
|
-
this.dbPath
|
|
2754
|
-
.
|
|
2755
|
-
.
|
|
2756
|
-
?.replace(SQLITE_EXT_REGEX, "")
|
|
2757
|
-
?.replace(INDEX_PREFIX_REGEX, "") || "default";
|
|
2756
|
+
basename(this.dbPath)
|
|
2757
|
+
.replace(SQLITE_EXT_REGEX, "")
|
|
2758
|
+
.replace(INDEX_PREFIX_REGEX, "") || "default";
|
|
2758
2759
|
|
|
2759
2760
|
// Get collection stats with chunk counts
|
|
2760
2761
|
interface CollectionStat {
|
|
@@ -66,6 +66,14 @@ export interface VectorIndexOptions {
|
|
|
66
66
|
distanceMetric?: "cosine" | "l2";
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
const SQLITE_VEC_GUIDANCE =
|
|
70
|
+
"Run `gno doctor` for sqlite-vec diagnostics. On macOS, verify Homebrew SQLite/sqlite-vec installation and see TROUBLESHOOTING.md.";
|
|
71
|
+
|
|
72
|
+
function formatUnavailableMessage(loadError?: string): string {
|
|
73
|
+
const reason = loadError ? ` Reason: ${loadError}` : "";
|
|
74
|
+
return `Vector search requires sqlite-vec. Embeddings are stored, but KNN search is disabled.${reason} ${SQLITE_VEC_GUIDANCE}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
/**
|
|
70
78
|
* Create a VectorIndexPort for a specific model.
|
|
71
79
|
* sqlite-vec is optional - storage works without it, search disabled.
|
|
@@ -147,6 +155,7 @@ export async function createVectorIndexPort(
|
|
|
147
155
|
model,
|
|
148
156
|
dimensions,
|
|
149
157
|
loadError,
|
|
158
|
+
guidance: searchAvailable ? undefined : SQLITE_VEC_GUIDANCE,
|
|
150
159
|
get vecDirty() {
|
|
151
160
|
return vecDirty;
|
|
152
161
|
},
|
|
@@ -235,10 +244,7 @@ export async function createVectorIndexPort(
|
|
|
235
244
|
): Promise<StoreResult<VectorSearchResult[]>> {
|
|
236
245
|
if (!(searchAvailable && searchStmt)) {
|
|
237
246
|
return Promise.resolve(
|
|
238
|
-
err(
|
|
239
|
-
"VEC_SEARCH_UNAVAILABLE",
|
|
240
|
-
"Vector search requires sqlite-vec. Embeddings stored but KNN search disabled."
|
|
241
|
-
)
|
|
247
|
+
err("VEC_SEARCH_UNAVAILABLE", formatUnavailableMessage(loadError))
|
|
242
248
|
);
|
|
243
249
|
}
|
|
244
250
|
|
|
@@ -60,6 +60,8 @@ export interface VectorIndexPort {
|
|
|
60
60
|
readonly dimensions: number;
|
|
61
61
|
/** Error message if sqlite-vec failed to load (for diagnostics) */
|
|
62
62
|
readonly loadError?: string;
|
|
63
|
+
/** User-facing recovery guidance when search is unavailable */
|
|
64
|
+
readonly guidance?: string;
|
|
63
65
|
/** True if vec0 inserts failed during this session (needs sync) */
|
|
64
66
|
vecDirty: boolean;
|
|
65
67
|
|