@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.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/assets/skill/SKILL.md +3 -0
  3. package/assets/skill/cli-reference.md +5 -0
  4. package/assets/skill/examples.md +2 -0
  5. package/package.json +1 -1
  6. package/src/app/constants.ts +64 -8
  7. package/src/cli/commands/embed.ts +6 -2
  8. package/src/cli/commands/get.ts +15 -5
  9. package/src/cli/commands/index-cmd.ts +4 -0
  10. package/src/cli/commands/multi-get.ts +62 -1
  11. package/src/cli/commands/query.ts +8 -2
  12. package/src/cli/commands/search.ts +8 -2
  13. package/src/cli/commands/shared.ts +18 -1
  14. package/src/cli/commands/status.ts +4 -2
  15. package/src/cli/commands/update.ts +6 -1
  16. package/src/cli/commands/vsearch.ts +8 -2
  17. package/src/cli/format/search-results.ts +1 -1
  18. package/src/cli/program.ts +22 -1
  19. package/src/core/user-dirs.ts +21 -12
  20. package/src/ingestion/chunker.ts +6 -0
  21. package/src/llm/cache.ts +133 -27
  22. package/src/llm/errors.ts +32 -0
  23. package/src/llm/nodeLlamaCpp/embedding.ts +69 -3
  24. package/src/llm/nodeLlamaCpp/lifecycle.ts +60 -4
  25. package/src/mcp/resources/index.ts +13 -4
  26. package/src/mcp/server.ts +2 -0
  27. package/src/mcp/tools/get.ts +7 -2
  28. package/src/mcp/tools/multi-get.ts +2 -2
  29. package/src/mcp/tools/query.ts +2 -1
  30. package/src/mcp/tools/search.ts +2 -1
  31. package/src/mcp/tools/vsearch.ts +2 -1
  32. package/src/pipeline/explain.ts +12 -2
  33. package/src/pipeline/hybrid.ts +9 -1
  34. package/src/pipeline/search.ts +1 -0
  35. package/src/pipeline/types.ts +2 -0
  36. package/src/pipeline/vsearch.ts +14 -8
  37. package/src/sdk/client.ts +83 -28
  38. package/src/store/sqlite/adapter.ts +8 -7
  39. package/src/store/vector/sqlite-vec.ts +10 -4
  40. 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 { buildUri, parseUri, URI_PREFIX } from "../../app/constants";
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 header = `<!-- ${doc.uri}
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 = buildUri(collection, path);
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,
@@ -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: doc.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,
@@ -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),
@@ -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),
@@ -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),
@@ -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(count: number, available: boolean): ExplainLine {
125
+ export function explainVector(
126
+ count: number,
127
+ available: boolean,
128
+ unavailableReason?: string,
129
+ guidance?: string
130
+ ): ExplainLine {
126
131
  if (!available) {
127
- return { stage: "vector", message: "unavailable (sqlite-vec not loaded)" };
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
  }
@@ -528,7 +528,14 @@ export async function searchHybrid(
528
528
  }
529
529
  timings.vectorMs = performance.now() - vectorStartedAt;
530
530
 
531
- explainLines.push(explainVector(vecCount, vectorAvailable));
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,
@@ -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,
@@ -43,6 +43,8 @@ export interface SearchResult {
43
43
  score: number;
44
44
  uri: string;
45
45
  title?: string;
46
+ /** Best source line for editor/agent anchors (1-indexed) */
47
+ line?: number;
46
48
  snippet: string;
47
49
  snippetLanguage?: string;
48
50
  snippetRange?: SnippetRange;
@@ -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 { getIndexDbPath } from "../app/constants";
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 unwrapStore(await searchBm25(this.store, query, options));
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 unwrapStore(
413
- await searchVectorWithEmbedding(
414
- {
415
- store: this.store,
416
- vectorIndex: ports.vectorIndex,
417
- embedPort: ports.embedPort,
418
- config: this.config,
419
- },
420
- query,
421
- new Float32Array(queryEmbedResult.value),
422
- options
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 unwrapStore(
465
- await searchHybrid(
466
- {
467
- store: this.store,
468
- config: this.config,
469
- vectorIndex: ports.vectorIndex,
470
- embedPort: ports.embedPort,
471
- expandPort: ports.expandPort,
472
- rerankPort: ports.rerankPort,
473
- },
474
- query,
475
- options
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
- return getDocumentByRef(this.store, this.config, ref, options);
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
- return multiGetDocuments(this.store, this.config, refs, options);
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
- return listDocuments(this.store, options);
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(uri);
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
- .split("/")
2755
- .pop()
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