@larkup/vector-stores 0.1.20 → 0.1.22

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 ADDED
@@ -0,0 +1,13 @@
1
+ # @larkup/vector-stores
2
+
3
+ ## 0.1.22
4
+
5
+ ### Patch Changes
6
+
7
+ - 08e7029: Fix LanceDB native-module loading in packaged Larkup installations, align marketplace catalog versions with published tools, and make clearing a knowledge base discoverable.
8
+
9
+ ## 0.1.21
10
+
11
+ ### Patch Changes
12
+
13
+ - 5caaf2f: feat: support persistent S3-compatible LanceDB storage for serverless deployments
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larkup/vector-stores",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -14,14 +14,14 @@
14
14
  "./chroma": "./src/adapters/chroma.ts"
15
15
  },
16
16
  "dependencies": {
17
- "@lancedb/lancedb": "^0.30.0",
18
- "@pinecone-database/pinecone": "^7.2.0"
17
+ "@lancedb/lancedb": "^0.31.0",
18
+ "@pinecone-database/pinecone": "^8.1.0"
19
19
  },
20
20
  "peerDependencies": {
21
- "@larkup/core": "0.1.20"
21
+ "@larkup/core": ">=0.1.20 <1.0.0"
22
22
  },
23
23
  "devDependencies": {
24
- "chromadb": "^1.10.5",
24
+ "chromadb": "^3.5.0",
25
25
  "typescript": "5.7.3"
26
26
  }
27
27
  }
@@ -7,42 +7,44 @@
7
7
  */
8
8
 
9
9
  export interface VectorRecord {
10
- id: string
11
- vector: number[]
12
- text: string
13
- title: string
14
- url?: string
15
- source: string
16
- documentId: string
17
- chunkIndex: number
18
- metadata?: Record<string, any>
10
+ id: string;
11
+ vector: number[];
12
+ text: string;
13
+ title: string;
14
+ url?: string;
15
+ source: string;
16
+ documentId: string;
17
+ chunkIndex: number;
18
+ metadata?: Record<string, any>;
19
19
  }
20
20
 
21
21
  export interface QueryHit {
22
- id: string
23
- score: number
24
- text: string
25
- title: string
26
- url?: string
27
- documentId: string
28
- metadata?: Record<string, any>
22
+ id: string;
23
+ score: number;
24
+ text: string;
25
+ title: string;
26
+ url?: string;
27
+ documentId: string;
28
+ metadata?: Record<string, any>;
29
29
  }
30
30
 
31
31
  export interface VectorStoreAdapter {
32
32
  /** Create / connect to the index, sized for `dimensions`. */
33
- init(dimensions: number): Promise<void>
33
+ init(dimensions: number): Promise<void>;
34
34
  /** Remove all existing vectors (full re-index). */
35
- reset(): Promise<void>
35
+ reset(): Promise<void>;
36
36
  /** Upsert a batch of records. */
37
- upsert(records: VectorRecord[]): Promise<void>
37
+ upsert(records: VectorRecord[]): Promise<void>;
38
38
  /** Number of vectors currently stored, if cheaply knowable. */
39
- count(): Promise<number | null>
39
+ count(): Promise<number | null>;
40
40
  /**
41
41
  * Nearest-neighbour search for a query vector.
42
42
  * Pass `queryText` for hybrid/lexical stores so they can also generate a
43
43
  * sparse vector and merge dense + sparse results (RRF).
44
44
  */
45
- query(vector: number[], topK: number, queryText?: string): Promise<QueryHit[]>
45
+ query(vector: number[], topK: number, queryText?: string): Promise<QueryHit[]>;
46
46
  /** Test connection to the vector store. Throws if invalid. */
47
- testConnection(dimensions: number): Promise<void>
47
+ testConnection(dimensions: number): Promise<void>;
48
+ /** Delete all chunks associated with the given document IDs. */
49
+ deleteByDocumentIds(documentIds: string[]): Promise<void>;
48
50
  }
@@ -1,4 +1,4 @@
1
- import type { QueryHit, VectorRecord, VectorStoreAdapter } from "./base";
1
+ import type { QueryHit, VectorRecord, VectorStoreAdapter } from './base';
2
2
 
3
3
  /**
4
4
  * Chroma adapter — self-hosted (client-server) or Chroma Cloud.
@@ -47,7 +47,7 @@ export class ChromaAdapter implements VectorStoreAdapter {
47
47
  private readonly collectionName: string;
48
48
 
49
49
  constructor(private readonly config: ChromaConfig) {
50
- this.collectionName = config.collectionName?.trim() || "documents";
50
+ this.collectionName = config.collectionName?.trim() || 'documents';
51
51
  }
52
52
 
53
53
  /**
@@ -60,37 +60,37 @@ export class ChromaAdapter implements VectorStoreAdapter {
60
60
 
61
61
  let chromadb: any;
62
62
  try {
63
- chromadb = await import("chromadb");
63
+ chromadb = await import('chromadb');
64
64
  } catch {
65
65
  throw new Error(
66
66
  'The "chromadb" package is not installed. Please install it first via the Configure page or run: pnpm add chromadb --filter @larkup/vector-stores',
67
67
  );
68
68
  }
69
69
 
70
- if (this.config.mode === "cloud") {
70
+ if (this.config.mode === 'cloud') {
71
71
  if (!this.config.apiKey) {
72
- throw new Error("Chroma Cloud requires an API key.");
72
+ throw new Error('Chroma Cloud requires an API key.');
73
73
  }
74
74
  const CloudClientClass = chromadb.CloudClient ?? chromadb.default?.CloudClient;
75
75
  if (!CloudClientClass) {
76
- throw new Error("Could not find CloudClient in the chromadb package.");
76
+ throw new Error('Could not find CloudClient in the chromadb package.');
77
77
  }
78
78
  this.client = new CloudClientClass({
79
79
  apiKey: this.config.apiKey,
80
- tenant: this.config.tenant || "default_tenant",
81
- database: this.config.database || "default_database",
80
+ tenant: this.config.tenant || 'default_tenant',
81
+ database: this.config.database || 'default_database',
82
82
  });
83
83
  } else {
84
84
  // Server mode
85
- const host = this.config.host?.trim() || "http://localhost:8000";
85
+ const host = this.config.host?.trim() || 'http://localhost:8000';
86
86
  const ClientClass = chromadb.ChromaClient ?? chromadb.default?.ChromaClient;
87
87
  if (!ClientClass) {
88
- throw new Error("Could not find ChromaClient in the chromadb package.");
88
+ throw new Error('Could not find ChromaClient in the chromadb package.');
89
89
  }
90
90
  const opts: any = { path: host };
91
91
  if (this.config.authToken) {
92
92
  opts.auth = {
93
- provider: "token",
93
+ provider: 'token',
94
94
  credentials: this.config.authToken,
95
95
  };
96
96
  }
@@ -105,7 +105,7 @@ export class ChromaAdapter implements VectorStoreAdapter {
105
105
  const client = await this.getClient();
106
106
  this.collection = await client.getOrCreateCollection({
107
107
  name: this.collectionName,
108
- metadata: { "hnsw:space": "cosine" },
108
+ metadata: { 'hnsw:space': 'cosine' },
109
109
  });
110
110
  return this.collection;
111
111
  }
@@ -126,6 +126,16 @@ export class ChromaAdapter implements VectorStoreAdapter {
126
126
  this.collection = null;
127
127
  }
128
128
 
129
+ async deleteByDocumentIds(documentIds: string[]): Promise<void> {
130
+ if (documentIds.length === 0) return;
131
+ try {
132
+ const collection = await this.getCollection();
133
+ await collection.delete({ where: { documentId: { $in: documentIds } } });
134
+ } catch (err) {
135
+ console.error('Failed to delete vectors by documentIds in Chroma:', err);
136
+ }
137
+ }
138
+
129
139
  // ── Upsert ─────────────────────────────────────────────────────────────────
130
140
 
131
141
  async upsert(records: VectorRecord[]): Promise<void> {
@@ -145,7 +155,6 @@ export class ChromaAdapter implements VectorStoreAdapter {
145
155
  metadatas.push(this.buildMetadata(r));
146
156
  }
147
157
 
148
- // Chroma has a max batch size of ~5000 for upsert, process in chunks
149
158
  const BATCH = 4096;
150
159
  for (let i = 0; i < ids.length; i += BATCH) {
151
160
  await collection.upsert({
@@ -169,7 +178,7 @@ export class ChromaAdapter implements VectorStoreAdapter {
169
178
  if (r.metadata) {
170
179
  for (const [k, v] of Object.entries(r.metadata)) {
171
180
  if (v === null || v === undefined) continue;
172
- if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
181
+ if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
173
182
  meta[k] = v;
174
183
  } else {
175
184
  meta[k] = JSON.stringify(v);
@@ -192,14 +201,10 @@ export class ChromaAdapter implements VectorStoreAdapter {
192
201
 
193
202
  // ── Query ──────────────────────────────────────────────────────────────────
194
203
 
195
- async query(
196
- vector: number[],
197
- topK: number,
198
- queryText?: string,
199
- ): Promise<QueryHit[]> {
204
+ async query(vector: number[], topK: number, queryText?: string): Promise<QueryHit[]> {
200
205
  const collection = await this.getCollection();
201
- const isSemantic = this.config.indexType === "semantic";
202
- const isLexical = this.config.indexType === "lexical";
206
+ const isSemantic = this.config.indexType === 'semantic';
207
+ const isLexical = this.config.indexType === 'lexical';
203
208
 
204
209
  // Semantic only
205
210
  if (isSemantic || !queryText?.trim()) {
@@ -226,15 +231,11 @@ export class ChromaAdapter implements VectorStoreAdapter {
226
231
  return denseHits;
227
232
  }
228
233
 
229
- private async denseQuery(
230
- collection: any,
231
- vector: number[],
232
- topK: number,
233
- ): Promise<QueryHit[]> {
234
+ private async denseQuery(collection: any, vector: number[], topK: number): Promise<QueryHit[]> {
234
235
  const result = await collection.query({
235
236
  queryEmbeddings: [vector],
236
237
  nResults: topK,
237
- include: ["documents", "metadatas", "distances"],
238
+ include: ['documents', 'metadatas', 'distances'],
238
239
  });
239
240
 
240
241
  return this.parseQueryResult(result);
@@ -245,11 +246,10 @@ export class ChromaAdapter implements VectorStoreAdapter {
245
246
  queryText: string,
246
247
  topK: number,
247
248
  ): Promise<QueryHit[]> {
248
- // Chroma supports substring filtering natively via get() with whereDocument
249
249
  const result = await collection.get({
250
- whereDocument: { "$contains": queryText },
250
+ whereDocument: { $contains: queryText },
251
251
  limit: topK,
252
- include: ["documents", "metadatas"],
252
+ include: ['documents', 'metadatas'],
253
253
  });
254
254
 
255
255
  return this.parseGetResult(result);
@@ -266,24 +266,16 @@ export class ChromaAdapter implements VectorStoreAdapter {
266
266
  const meta = (metadatas[i] ?? {}) as Record<string, any>;
267
267
  const score = 1.0; // Exact/lexical matches get max base score before RRF
268
268
 
269
- const {
270
- title,
271
- url,
272
- source,
273
- documentId,
274
- chunkIndex,
275
- ...customMetadata
276
- } = meta;
269
+ const { title, url, source, documentId, chunkIndex, ...customMetadata } = meta;
277
270
 
278
271
  return {
279
272
  id,
280
273
  score,
281
- text: (documents[i] as string) ?? "",
282
- title: (title as string) ?? "Untitled",
274
+ text: (documents[i] as string) ?? '',
275
+ title: (title as string) ?? 'Untitled',
283
276
  url: (url as string) || undefined,
284
- documentId: (documentId as string) ?? "",
285
- metadata:
286
- Object.keys(customMetadata).length > 0 ? customMetadata : undefined,
277
+ documentId: (documentId as string) ?? '',
278
+ metadata: Object.keys(customMetadata).length > 0 ? customMetadata : undefined,
287
279
  };
288
280
  });
289
281
  }
@@ -299,28 +291,19 @@ export class ChromaAdapter implements VectorStoreAdapter {
299
291
  return ids.map((id, i) => {
300
292
  const meta = (metadatas[i] ?? {}) as Record<string, any>;
301
293
  const distance = distances[i] ?? 0;
302
- // Chroma cosine distance: 0 = identical, 2 = opposite
303
294
  // Convert to a similarity score
304
295
  const score = 1 / (1 + distance);
305
296
 
306
- const {
307
- title,
308
- url,
309
- source,
310
- documentId,
311
- chunkIndex,
312
- ...customMetadata
313
- } = meta;
297
+ const { title, url, source, documentId, chunkIndex, ...customMetadata } = meta;
314
298
 
315
299
  return {
316
300
  id,
317
301
  score,
318
- text: (documents[i] as string) ?? "",
319
- title: (title as string) ?? "Untitled",
302
+ text: (documents[i] as string) ?? '',
303
+ title: (title as string) ?? 'Untitled',
320
304
  url: (url as string) || undefined,
321
- documentId: (documentId as string) ?? "",
322
- metadata:
323
- Object.keys(customMetadata).length > 0 ? customMetadata : undefined,
305
+ documentId: (documentId as string) ?? '',
306
+ metadata: Object.keys(customMetadata).length > 0 ? customMetadata : undefined,
324
307
  };
325
308
  });
326
309
  }
@@ -339,29 +322,26 @@ export class ChromaAdapter implements VectorStoreAdapter {
339
322
  try {
340
323
  await client.heartbeat();
341
324
  } catch (err: any) {
342
- const msg: string = err.message ?? "";
325
+ const msg: string = err.message ?? '';
343
326
 
344
327
  if (
345
- msg.includes("401") ||
346
- msg.includes("Unauthorized") ||
347
- msg.includes("403") ||
348
- msg.includes("Forbidden")
328
+ msg.includes('401') ||
329
+ msg.includes('Unauthorized') ||
330
+ msg.includes('403') ||
331
+ msg.includes('Forbidden')
349
332
  ) {
350
- throw new Error(
351
- "Invalid Chroma credentials. Please check your auth token or API key.",
352
- );
333
+ throw new Error('Invalid Chroma credentials. Please check your auth token or API key.');
353
334
  }
354
335
 
355
- if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
336
+ if (msg.includes('ECONNREFUSED') || msg.includes('fetch failed')) {
356
337
  throw new Error(
357
- "Could not reach the Chroma server. Is it running and accessible at the configured URL?",
338
+ 'Could not reach the Chroma server. Is it running and accessible at the configured URL?',
358
339
  );
359
340
  }
360
341
 
361
342
  throw new Error(`Failed to reach Chroma server: ${msg}`);
362
343
  }
363
344
 
364
- // Verify we can access the specified tenant/database by listing collections
365
345
  try {
366
346
  await client.listCollections();
367
347
  } catch (err: any) {
@@ -374,11 +354,7 @@ export class ChromaAdapter implements VectorStoreAdapter {
374
354
 
375
355
  // ── Helpers ──────────────────────────────────────────────────────────────────
376
356
 
377
- function rrfMerge(
378
- denseHits: QueryHit[],
379
- sparseHits: QueryHit[],
380
- topK: number,
381
- ): QueryHit[] {
357
+ function rrfMerge(denseHits: QueryHit[], sparseHits: QueryHit[], topK: number): QueryHit[] {
382
358
  const scores = new Map<string, { hit: QueryHit; score: number }>();
383
359
 
384
360
  const addList = (hits: QueryHit[]) =>
@@ -1,10 +1,28 @@
1
- import path from "node:path";
2
- import fs from "node:fs/promises";
3
- import * as lancedb from "@lancedb/lancedb";
4
- import type { QueryHit, VectorRecord, VectorStoreAdapter } from "./base";
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import type { QueryHit, VectorRecord, VectorStoreAdapter } from './base';
4
+
5
+ type LanceDBModule = typeof import('@lancedb/lancedb');
6
+ type LanceConnection = Awaited<ReturnType<LanceDBModule['connect']>>;
7
+ type LanceTable = Awaited<ReturnType<LanceConnection['openTable']>>;
8
+
9
+ let lancedbModule: LanceDBModule | null = null;
5
10
 
6
11
  /**
7
- * LanceDB adapter embedded/on-disk for local dev, or LanceDB Cloud.
12
+ * Keep LanceDB outside Turbopack's server bundle. Its native binding resolves
13
+ * platform-specific optional packages at runtime, and static bundling turns
14
+ * the package name into a non-resolvable hashed external module.
15
+ */
16
+ async function getLanceDB(): Promise<LanceDBModule> {
17
+ if (!lancedbModule) {
18
+ lancedbModule = await import(/* turbopackIgnore: true */ '@lancedb/lancedb');
19
+ }
20
+ return lancedbModule;
21
+ }
22
+
23
+ /**
24
+ * LanceDB adapter — embedded/on-disk for local dev, LanceDB Cloud, or an
25
+ * S3-compatible object store (including Cloudflare R2).
8
26
  *
9
27
  * The table schema is inferred from the first batch of rows we add, so `init`
10
28
  * is a no-op connect; the table is (re)created lazily on `reset`/first upsert.
@@ -15,6 +33,11 @@ interface LanceConfig {
15
33
  dbPath?: string;
16
34
  uri?: string;
17
35
  apiKey?: string;
36
+ s3Uri?: string;
37
+ s3Endpoint?: string;
38
+ s3Region?: string;
39
+ s3AccessKeyId?: string;
40
+ s3SecretAccessKey?: string;
18
41
  tableName?: string;
19
42
  }
20
43
 
@@ -31,39 +54,55 @@ interface LanceRow extends Record<string, unknown> {
31
54
  }
32
55
 
33
56
  export class LanceDBAdapter implements VectorStoreAdapter {
34
- private conn: lancedb.Connection | null = null;
35
- private table: lancedb.Table | null = null;
57
+ private conn: LanceConnection | null = null;
58
+ private table: LanceTable | null = null;
36
59
  private readonly tableName: string;
37
60
 
38
61
  constructor(private readonly config: LanceConfig) {
39
- this.tableName = config.tableName?.trim() || "documents";
62
+ this.tableName = config.tableName?.trim() || 'documents';
40
63
  }
41
64
 
42
- private async connect(): Promise<lancedb.Connection> {
65
+ private async connect(): Promise<LanceConnection> {
43
66
  if (this.conn) return this.conn;
44
- if (this.config.mode === "cloud") {
67
+ const lancedb = await getLanceDB();
68
+ if (this.config.mode === 'cloud') {
45
69
  if (!this.config.uri || !this.config.apiKey) {
46
- throw new Error("LanceDB Cloud requires both a URI and an API key.");
70
+ throw new Error('LanceDB Cloud requires both a URI and an API key.');
47
71
  }
48
- if (!this.config.uri.startsWith("db://")) {
49
- throw new Error(
50
- "LanceDB Cloud URI must start with 'db://' (e.g. db://my-database).",
51
- );
72
+ if (!this.config.uri.startsWith('db://')) {
73
+ throw new Error("LanceDB Cloud URI must start with 'db://' (e.g. db://my-database).");
52
74
  }
53
75
  this.conn = await lancedb.connect(this.config.uri, {
54
76
  apiKey: this.config.apiKey,
55
77
  });
78
+ } else if (this.config.mode === 's3') {
79
+ if (!this.config.s3Uri || !this.config.s3AccessKeyId || !this.config.s3SecretAccessKey) {
80
+ throw new Error(
81
+ 'S3-compatible LanceDB requires a URI, access key ID, and secret access key.',
82
+ );
83
+ }
84
+ if (!this.config.s3Uri.startsWith('s3://')) {
85
+ throw new Error(
86
+ "S3-compatible LanceDB URI must start with 's3://' (e.g. s3://my-bucket/lancedb).",
87
+ );
88
+ }
89
+ this.conn = await lancedb.connect(this.config.s3Uri, {
90
+ storageOptions: {
91
+ aws_access_key_id: this.config.s3AccessKeyId,
92
+ aws_secret_access_key: this.config.s3SecretAccessKey,
93
+ ...(this.config.s3Endpoint ? { aws_endpoint: this.config.s3Endpoint } : {}),
94
+ ...(this.config.s3Region ? { aws_region: this.config.s3Region } : {}),
95
+ },
96
+ });
56
97
  } else {
57
- const dir = this.config.dbPath?.trim() || "./.larkup/lancedb";
98
+ const dir = this.config.dbPath?.trim() || './.larkup/lancedb';
58
99
  let abs = path.isAbsolute(dir) ? dir : path.join(process.cwd(), dir);
59
100
 
60
101
  try {
61
102
  await fs.mkdir(abs, { recursive: true });
62
103
  } catch (err: any) {
63
104
  if (path.isAbsolute(dir)) {
64
- // The user likely forgot the dot (e.g. typed "/lancedb" instead of "./lancedb").
65
- // Automatically retry by prepending "." to make it relative to the workspace.
66
- const fallbackDir = "." + dir;
105
+ const fallbackDir = '.' + dir;
67
106
  abs = path.join(process.cwd(), fallbackDir);
68
107
  try {
69
108
  await fs.mkdir(abs, { recursive: true });
@@ -73,9 +112,7 @@ export class LanceDBAdapter implements VectorStoreAdapter {
73
112
  );
74
113
  }
75
114
  } else {
76
- throw new Error(
77
- `Could not create directory for LanceDB at "${abs}": ${err.message}`,
78
- );
115
+ throw new Error(`Could not create directory for LanceDB at "${abs}": ${err.message}`);
79
116
  }
80
117
  }
81
118
 
@@ -101,13 +138,23 @@ export class LanceDBAdapter implements VectorStoreAdapter {
101
138
  this.table = null;
102
139
  }
103
140
 
141
+ async deleteByDocumentIds(documentIds: string[]): Promise<void> {
142
+ if (documentIds.length === 0) return;
143
+ if (!this.table) {
144
+ await this.init();
145
+ if (!this.table) return;
146
+ }
147
+ const idsString = documentIds.map((id) => `'${id}'`).join(', ');
148
+ await this.table.delete(`documentId IN (${idsString})`);
149
+ }
150
+
104
151
  private toRow(r: VectorRecord): LanceRow {
105
152
  return {
106
153
  id: r.id,
107
154
  vector: r.vector,
108
155
  text: r.text,
109
156
  title: r.title,
110
- url: r.url ?? "",
157
+ url: r.url ?? '',
111
158
  source: r.source,
112
159
  documentId: r.documentId,
113
160
  chunkIndex: r.chunkIndex,
@@ -151,39 +198,35 @@ export class LanceDBAdapter implements VectorStoreAdapter {
151
198
  await this.init();
152
199
  if (!this.table) return [];
153
200
  }
154
- const rows = (await this.table
155
- .search(vector)
156
- .limit(topK)
157
- .toArray()) as Array<LanceRow & { _distance?: number }>;
201
+ const rows = (await this.table.search(vector).limit(topK).toArray()) as Array<
202
+ LanceRow & { _distance?: number }
203
+ >;
158
204
 
159
205
  return rows.map((row) => ({
160
206
  id: row.id as string,
161
- // Convert L2 distance a 0..1 similarity-ish score for display.
162
- score: typeof row._distance === "number" ? 1 / (1 + row._distance) : 0,
207
+ score: typeof row._distance === 'number' ? 1 / (1 + row._distance) : 0,
163
208
  text: row.text as string,
164
209
  title: row.title as string,
165
210
  url: (row.url as string) || undefined,
166
211
  documentId: row.documentId as string,
167
- metadata:
168
- typeof row.metadata === "string" ? JSON.parse(row.metadata) : undefined,
212
+ metadata: typeof row.metadata === 'string' ? JSON.parse(row.metadata) : undefined,
169
213
  }));
170
214
  }
171
215
 
172
216
  async testConnection(dimensions: number): Promise<void> {
173
- // ── Local mode: just verify the directory is reachable ──────────────────
174
- if (this.config.mode !== "cloud") {
217
+ if (this.config.mode !== 'cloud') {
175
218
  try {
176
219
  await this.connect();
177
220
  return;
178
221
  } catch (err: any) {
179
- throw new Error(`Failed to connect to local LanceDB: ${err.message}`);
222
+ const label = this.config.mode === 's3' ? 'S3-compatible LanceDB' : 'local LanceDB';
223
+ throw new Error(`Failed to connect to ${label}: ${err.message}`);
180
224
  }
181
225
  }
182
226
 
183
227
  // ── Cloud mode ───────────────────────────────────────────────────────────
184
- // connect() is lazy (never contacts the server), so we call tableNames()
185
228
  // to force a real authenticated HTTP round-trip.
186
- let conn: lancedb.Connection;
229
+ let conn: LanceConnection;
187
230
  try {
188
231
  conn = await this.connect();
189
232
  } catch (err: any) {
@@ -193,22 +236,21 @@ export class LanceDBAdapter implements VectorStoreAdapter {
193
236
  try {
194
237
  await conn.tableNames();
195
238
  } catch (err: any) {
196
- const msg: string = err.message ?? "";
239
+ const msg: string = err.message ?? '';
197
240
 
198
- // The SDK puts the HTTP status INSIDE the message string (not as a
199
241
  // property). Pattern: "Http error: ... 401 Unauthorized: Unauthorized"
200
242
  if (
201
- msg.includes("401") ||
202
- msg.includes("Unauthorized") ||
203
- msg.includes("403") ||
204
- msg.includes("Forbidden")
243
+ msg.includes('401') ||
244
+ msg.includes('Unauthorized') ||
245
+ msg.includes('403') ||
246
+ msg.includes('Forbidden')
205
247
  ) {
206
248
  throw new Error(
207
- "Invalid LanceDB Cloud API key. Please check your credentials in the LanceDB Cloud dashboard.",
249
+ 'Invalid LanceDB Cloud API key. Please check your credentials in the LanceDB Cloud dashboard.',
208
250
  );
209
251
  }
210
252
 
211
- if (msg.includes("404") || msg.includes("Not Found")) {
253
+ if (msg.includes('404') || msg.includes('Not Found')) {
212
254
  throw new Error(
213
255
  `LanceDB Cloud database not found. Please verify your URI (e.g. "db://my-database").`,
214
256
  );