@wei840222/qmd 2026.8.23

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 (94) hide show
  1. package/CHANGELOG.md +1373 -0
  2. package/LICENSE +45 -0
  3. package/README.md +1439 -0
  4. package/THIRD_PARTY_NOTICES.md +31 -0
  5. package/bin/qmd +192 -0
  6. package/dist/ast.d.ts +65 -0
  7. package/dist/ast.js +334 -0
  8. package/dist/bench/bench.d.ts +35 -0
  9. package/dist/bench/bench.js +338 -0
  10. package/dist/bench/cjk-baseline.d.ts +36 -0
  11. package/dist/bench/cjk-baseline.js +111 -0
  12. package/dist/bench/fixture.d.ts +2 -0
  13. package/dist/bench/fixture.js +84 -0
  14. package/dist/bench/score.d.ts +38 -0
  15. package/dist/bench/score.js +107 -0
  16. package/dist/bench/types.d.ts +110 -0
  17. package/dist/bench/types.js +8 -0
  18. package/dist/cli/build-info.json +4 -0
  19. package/dist/cli/embed-lock.d.ts +24 -0
  20. package/dist/cli/embed-lock.js +94 -0
  21. package/dist/cli/embedding-owner.d.ts +10 -0
  22. package/dist/cli/embedding-owner.js +20 -0
  23. package/dist/cli/formatter.d.ts +120 -0
  24. package/dist/cli/formatter.js +355 -0
  25. package/dist/cli/mcp-pid.d.ts +25 -0
  26. package/dist/cli/mcp-pid.js +86 -0
  27. package/dist/cli/qmd.d.ts +72 -0
  28. package/dist/cli/qmd.js +4806 -0
  29. package/dist/cli/version.d.ts +42 -0
  30. package/dist/cli/version.js +80 -0
  31. package/dist/collections.d.ts +200 -0
  32. package/dist/collections.js +433 -0
  33. package/dist/db.d.ts +65 -0
  34. package/dist/db.js +143 -0
  35. package/dist/diagnostics.d.ts +62 -0
  36. package/dist/diagnostics.js +260 -0
  37. package/dist/embedding/config.d.ts +52 -0
  38. package/dist/embedding/config.js +229 -0
  39. package/dist/embedding/identity.d.ts +58 -0
  40. package/dist/embedding/identity.js +321 -0
  41. package/dist/embedding/local-identity.d.ts +1 -0
  42. package/dist/embedding/local-identity.js +15 -0
  43. package/dist/embedding/local.d.ts +34 -0
  44. package/dist/embedding/local.js +290 -0
  45. package/dist/embedding/openai.d.ts +79 -0
  46. package/dist/embedding/openai.js +477 -0
  47. package/dist/embedding/owner.d.ts +13 -0
  48. package/dist/embedding/owner.js +36 -0
  49. package/dist/embedding/provider.d.ts +68 -0
  50. package/dist/embedding/provider.js +16 -0
  51. package/dist/embedding/remote-chunking.d.ts +22 -0
  52. package/dist/embedding/remote-chunking.js +83 -0
  53. package/dist/embedding/remote-embedding.d.ts +15 -0
  54. package/dist/embedding/remote-embedding.js +77 -0
  55. package/dist/hybrid-llm.d.ts +18 -0
  56. package/dist/hybrid-llm.js +53 -0
  57. package/dist/index.d.ts +244 -0
  58. package/dist/index.js +418 -0
  59. package/dist/llm.d.ts +566 -0
  60. package/dist/llm.js +1847 -0
  61. package/dist/maintenance.d.ts +33 -0
  62. package/dist/maintenance.js +52 -0
  63. package/dist/mcp/origin-guard.d.ts +67 -0
  64. package/dist/mcp/origin-guard.js +137 -0
  65. package/dist/mcp/server.d.ts +116 -0
  66. package/dist/mcp/server.js +919 -0
  67. package/dist/paths.d.ts +1 -0
  68. package/dist/paths.js +4 -0
  69. package/dist/remote-llm.d.ts +52 -0
  70. package/dist/remote-llm.js +464 -0
  71. package/dist/search/cjk-analyzer.d.ts +33 -0
  72. package/dist/search/cjk-analyzer.js +158 -0
  73. package/dist/search/cjk-index.d.ts +104 -0
  74. package/dist/search/cjk-index.js +1031 -0
  75. package/dist/search/jieba-loader.d.ts +23 -0
  76. package/dist/search/jieba-loader.js +79 -0
  77. package/dist/search/query-expansion.d.ts +23 -0
  78. package/dist/search/query-expansion.js +43 -0
  79. package/dist/search/zh-dict.txt +624013 -0
  80. package/dist/store.d.ts +1218 -0
  81. package/dist/store.js +6076 -0
  82. package/dist/trust.d.ts +152 -0
  83. package/dist/trust.js +249 -0
  84. package/package.json +139 -0
  85. package/scripts/build.mjs +83 -0
  86. package/scripts/check-package-grammars.mjs +29 -0
  87. package/scripts/package-smoke.mjs +205 -0
  88. package/scripts/sync-zh-dict.mjs +187 -0
  89. package/scripts/test-all.mjs +45 -0
  90. package/skills/qmd/SKILL.md +324 -0
  91. package/skills/qmd/references/mcp-setup.md +119 -0
  92. package/skills/release/SKILL.md +141 -0
  93. package/skills/release/scripts/install-hooks.sh +38 -0
  94. package/skills/release/scripts/release-context.sh +129 -0
package/dist/index.js ADDED
@@ -0,0 +1,418 @@
1
+ /**
2
+ * QMD SDK - Library mode for programmatic access to QMD search and indexing.
3
+ *
4
+ * Usage:
5
+ * import { createStore } from '@wei840222/qmd'
6
+ *
7
+ * const store = await createStore({
8
+ * dbPath: './my-index.sqlite',
9
+ * config: {
10
+ * collections: {
11
+ * docs: { path: '/path/to/docs', pattern: '**\/*.md' }
12
+ * }
13
+ * }
14
+ * })
15
+ *
16
+ * const results = await store.search({ query: "how does auth work?" })
17
+ * await store.close()
18
+ */
19
+ import { existsSync } from "node:fs";
20
+ import { createStore as createStoreInternal, hybridQuery, structuredSearch, extractSnippet, addLineNumbers, DEFAULT_MULTI_GET_MAX_BYTES, reindexCollection, generateEmbeddings, listCollections as storeListCollections, syncConfigToDb, getStoreCollections, getStoreCollection, getStoreGlobalContext, getStoreContexts, upsertStoreCollection, removeCollection as removeCollectionWithDocuments, renameCollection as renameCollectionWithDocuments, updateStoreContext, removeStoreContext, setStoreGlobalContext, vacuumDatabase, cleanupOrphanedContent, cleanupOrphanedVectors, deleteLLMCache, deleteInactiveDocuments, clearAllEmbeddings, getPendingEmbeddingDocsReadOnly, getIndexHealthReadOnly, getStatusReadOnly, } from "./store.js";
21
+ import { DEFAULT_EMBED_MODEL_URI, LlamaCpp, waitForLLMSessionsToDrain, } from "./llm.js";
22
+ import { LocalEmbeddingProviderOwner } from "./embedding/local.js";
23
+ import { OpenAIEmbeddingProvider, UnavailableOpenAIEmbeddingProvider, } from "./embedding/openai.js";
24
+ import { authorizeRemoteEmbeddingRequest, remoteEmbeddingIdentity, } from "./embedding/remote-embedding.js";
25
+ import { readStoredEmbeddingIdentity } from "./embedding/identity.js";
26
+ import { inspectIndexDiagnostics } from "./diagnostics.js";
27
+ import { EmbeddingConfigError, readCanonicalEmbeddingConfig, resolveEmbeddingConfig, writeCanonicalEmbeddingConfig, } from "./embedding/config.js";
28
+ import { rebuildCjkLexicalIndex } from "./search/cjk-index.js";
29
+ import { RemoteLLM } from "./remote-llm.js";
30
+ import { HybridLLM } from "./hybrid-llm.js";
31
+ import { createCollectionConfigSource, loadConfig, addCollection as collectionsAddCollection, removeCollection as collectionsRemoveCollection, renameCollection as collectionsRenameCollection, addContext as collectionsAddContext, removeContext as collectionsRemoveContext, setGlobalContext as collectionsSetGlobalContext, } from "./collections.js";
32
+ // Re-export utility functions and types used by frontends
33
+ export { extractSnippet, addLineNumbers, DEFAULT_MULTI_GET_MAX_BYTES };
34
+ // Re-export getDefaultDbPath for CLI/MCP that need the default database location
35
+ export { getDefaultDbPath } from "./store.js";
36
+ // Re-export Maintenance class for CLI housekeeping operations
37
+ export { Maintenance } from "./maintenance.js";
38
+ /**
39
+ * Create a QMD store for programmatic access to search and indexing.
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * // With a YAML config file
44
+ * const store = await createStore({
45
+ * dbPath: './index.sqlite',
46
+ * configPath: './qmd.yml',
47
+ * })
48
+ *
49
+ * // With inline config (no files needed besides the DB)
50
+ * const store = await createStore({
51
+ * dbPath: './index.sqlite',
52
+ * config: {
53
+ * collections: {
54
+ * docs: { path: '/path/to/docs', pattern: '**\/*.md' }
55
+ * }
56
+ * }
57
+ * })
58
+ *
59
+ * const results = await store.search({ query: "authentication flow" })
60
+ * await store.close()
61
+ * ```
62
+ */
63
+ export async function createStore(options) {
64
+ if (!options.dbPath) {
65
+ throw new Error("dbPath is required");
66
+ }
67
+ if (options.configPath && options.config) {
68
+ throw new Error("Provide either configPath or config, not both");
69
+ }
70
+ // A missing read-only index uses an initialized in-memory store without
71
+ // creating the requested on-disk file.
72
+ const useTemporaryStore = options.readOnly === true && !existsSync(options.dbPath);
73
+ const internal = createStoreInternal(useTemporaryStore ? ":memory:" : options.dbPath, { readOnly: options.readOnly === true && !useTemporaryStore });
74
+ const db = internal.db;
75
+ const externalConfigSource = options.configPath
76
+ ? createCollectionConfigSource({ configPath: options.configPath })
77
+ : options.config
78
+ ? createCollectionConfigSource({ config: options.config })
79
+ : undefined;
80
+ // Sync config into SQLite store_collections
81
+ let config;
82
+ if (externalConfigSource) {
83
+ config = loadConfig(externalConfigSource);
84
+ if (!options.readOnly || useTemporaryStore)
85
+ syncConfigToDb(db, config);
86
+ }
87
+ // else: DB-only mode — no external config, use existing store_collections
88
+ const embedding = resolveEmbeddingConfig({
89
+ config,
90
+ dbConfig: readCanonicalEmbeddingConfig(db),
91
+ defaultLocalModel: DEFAULT_EMBED_MODEL_URI,
92
+ });
93
+ if (!options.readOnly || useTemporaryStore) {
94
+ writeCanonicalEmbeddingConfig(db, embedding.canonical);
95
+ }
96
+ // Create a per-store LlamaCpp instance — lazy-loads models on first use,
97
+ // auto-unloads after 5 min inactivity to free VRAM.
98
+ const localLlm = new LlamaCpp({
99
+ embedModel: embedding.canonical.provider === "local"
100
+ ? embedding.canonical.model
101
+ : DEFAULT_EMBED_MODEL_URI,
102
+ generateModel: config?.models?.generate,
103
+ rerankModel: config?.models?.rerank,
104
+ inactivityTimeoutMs: 5 * 60 * 1000,
105
+ disposeModelsOnInactivity: true,
106
+ });
107
+ const rawGenerateUrl = config?.models?.generate_url ?? config?.models?.generate_base_url ?? config?.models?.generate_api_url;
108
+ const rawRerankUrl = config?.models?.rerank_url ?? config?.models?.rerank_base_url ?? config?.models?.rerank_api_url;
109
+ const hasRemoteLLM = Boolean(rawGenerateUrl || rawRerankUrl);
110
+ const remoteLlm = hasRemoteLLM
111
+ ? new RemoteLLM({
112
+ generateUrl: config?.models?.generate_url,
113
+ generateBaseUrl: config?.models?.generate_base_url,
114
+ generateApiUrl: config?.models?.generate_api_url,
115
+ generateApiModel: config?.models?.generate_api_model,
116
+ generateApiKey: config?.models?.generate_api_key,
117
+ rerankUrl: config?.models?.rerank_url,
118
+ rerankBaseUrl: config?.models?.rerank_base_url,
119
+ rerankApiUrl: config?.models?.rerank_api_url,
120
+ rerankApiModel: config?.models?.rerank_api_model,
121
+ rerankApiKey: config?.models?.rerank_api_key,
122
+ timeoutMs: options.remoteRequestTimeoutMs,
123
+ })
124
+ : undefined;
125
+ const llm = remoteLlm ? new HybridLLM(localLlm, remoteLlm) : localLlm;
126
+ internal.llm = llm;
127
+ let closeEmbeddingResources;
128
+ let remoteKeyConfigured = false;
129
+ if (embedding.canonical.provider === "local") {
130
+ const embeddingOwner = new LocalEmbeddingProviderOwner(localLlm, {
131
+ model: embedding.canonical.model,
132
+ dimension: embedding.canonical.dimension ?? undefined,
133
+ });
134
+ internal.embeddingProvider = embeddingOwner.provider;
135
+ closeEmbeddingResources = () => embeddingOwner.close();
136
+ }
137
+ else {
138
+ const apiKey = config?.models?.embed_api_key?.trim() ||
139
+ process.env.OPENAI_API_KEY?.trim();
140
+ remoteKeyConfigured = apiKey != null && apiKey !== "";
141
+ const configuredModel = embedding.canonical.model;
142
+ const configuredDimension = embedding.canonical.dimension;
143
+ const configuredBaseUrl = embedding.canonical.baseUrl;
144
+ // For non-default endpoints (self-hosted / proxy), API key is optional
145
+ const canConstruct = remoteKeyConfigured || embedding.credentialAvailable;
146
+ const provider = canConstruct
147
+ ? new OpenAIEmbeddingProvider({
148
+ apiKey: apiKey || undefined,
149
+ model: configuredModel,
150
+ dimension: configuredDimension,
151
+ baseUrl: configuredBaseUrl,
152
+ requestTimeoutMs: options.remoteRequestTimeoutMs,
153
+ authorizeRequest: request => {
154
+ const activeProvider = internal.embeddingProvider;
155
+ if (!activeProvider?.remote) {
156
+ throw new EmbeddingConfigError("Remote embedding provider is not active.");
157
+ }
158
+ const storedIdentity = readStoredEmbeddingIdentity(db);
159
+ const requestIdentity = [
160
+ storedIdentity,
161
+ remoteEmbeddingIdentity(activeProvider, "regex"),
162
+ remoteEmbeddingIdentity(activeProvider, "auto"),
163
+ ].find(identity => identity?.fingerprint === request.fingerprint);
164
+ if (!requestIdentity) {
165
+ throw new EmbeddingConfigError("Remote request fingerprint does not match an active embedding identity.");
166
+ }
167
+ authorizeRemoteEmbeddingRequest(db, requestIdentity, request.purpose, {
168
+ lease: request.buildLease,
169
+ requestFingerprint: request.fingerprint,
170
+ });
171
+ },
172
+ })
173
+ : new UnavailableOpenAIEmbeddingProvider({ model: configuredModel, dimension: configuredDimension, baseUrl: configuredBaseUrl });
174
+ internal.embeddingProvider = provider;
175
+ closeEmbeddingResources = async () => {
176
+ try {
177
+ await provider.close();
178
+ }
179
+ finally {
180
+ try {
181
+ await waitForLLMSessionsToDrain(localLlm);
182
+ }
183
+ finally {
184
+ await llm.dispose();
185
+ }
186
+ }
187
+ };
188
+ }
189
+ internal.authorizeRemoteRequest = (purpose, context) => {
190
+ const activeProvider = internal.embeddingProvider;
191
+ if (!activeProvider?.remote) {
192
+ throw new EmbeddingConfigError("Remote embedding provider is not active.");
193
+ }
194
+ authorizeRemoteEmbeddingRequest(db, context.identity ?? remoteEmbeddingIdentity(activeProvider), purpose, { lease: context.lease });
195
+ };
196
+ let closePromise;
197
+ const store = {
198
+ internal,
199
+ dbPath: internal.dbPath,
200
+ // Search
201
+ search: async (opts) => {
202
+ if (!opts.query && !opts.queries) {
203
+ throw new Error("search() requires either 'query' or 'queries'");
204
+ }
205
+ // Normalize collection/collections
206
+ const collections = [
207
+ ...(typeof opts.collection === "string" ? [opts.collection] : (opts.collection ?? [])),
208
+ ...(opts.collections ?? []),
209
+ ];
210
+ const skipRerank = opts.rerank === false;
211
+ if (opts.queries) {
212
+ // Pre-expanded queries — use structuredSearch
213
+ return structuredSearch(internal, opts.queries, {
214
+ collections: collections.length > 0 ? collections : undefined,
215
+ limit: opts.limit,
216
+ minScore: opts.minScore,
217
+ explain: opts.explain,
218
+ rerankContext: opts.rerankContext,
219
+ candidateLimit: opts.candidateLimit,
220
+ skipRerank,
221
+ chunkStrategy: opts.chunkStrategy,
222
+ });
223
+ }
224
+ // Simple query string — use hybridQuery (expand + search + rerank)
225
+ return hybridQuery(internal, opts.query, {
226
+ collections: collections.length > 0 ? collections : undefined,
227
+ collection: collections.length === 1 ? collections[0] : (collections.length > 0 ? collections : undefined),
228
+ limit: opts.limit,
229
+ minScore: opts.minScore,
230
+ explain: opts.explain,
231
+ expansionContext: opts.expansionContext,
232
+ rerankContext: opts.rerankContext,
233
+ expansion: opts.expansion,
234
+ hooks: opts.hooks,
235
+ candidateLimit: opts.candidateLimit,
236
+ skipRerank,
237
+ chunkStrategy: opts.chunkStrategy,
238
+ });
239
+ },
240
+ searchLex: async (q, opts) => internal.searchFTS(q, opts?.limit, opts?.collection),
241
+ searchVector: async (q, opts) => {
242
+ const provider = internal.embeddingProvider;
243
+ return internal.searchVec(q, provider?.model ?? internal.llm?.embedModelName ?? DEFAULT_EMBED_MODEL_URI, opts?.limit, opts?.collection);
244
+ },
245
+ expandQuery: async (q, opts) => internal.expandQuery(q, undefined, opts?.expansionContext),
246
+ get: async (pathOrDocid, opts) => internal.findDocument(pathOrDocid, opts),
247
+ getDocumentBody: async (pathOrDocid, opts) => {
248
+ const result = internal.findDocument(pathOrDocid, { includeBody: false });
249
+ if ("error" in result)
250
+ return null;
251
+ return internal.getDocumentBody(result, opts?.fromLine, opts?.maxLines);
252
+ },
253
+ multiGet: async (pattern, opts) => internal.findDocuments(pattern, opts),
254
+ // Collection Management — external config wins when configured; reconciliation mutates SQLite.
255
+ addCollection: async (name, opts) => {
256
+ if (externalConfigSource) {
257
+ collectionsAddCollection(name, opts.path, opts.pattern, opts.ignore, externalConfigSource);
258
+ syncConfigToDb(db, loadConfig(externalConfigSource));
259
+ }
260
+ else {
261
+ upsertStoreCollection(db, name, { path: opts.path, pattern: opts.pattern, ignore: opts.ignore });
262
+ }
263
+ },
264
+ removeCollection: async (name) => {
265
+ if (externalConfigSource) {
266
+ if (!collectionsRemoveCollection(name, externalConfigSource))
267
+ return false;
268
+ syncConfigToDb(db, loadConfig(externalConfigSource));
269
+ }
270
+ else {
271
+ if (!getStoreCollection(db, name))
272
+ return false;
273
+ removeCollectionWithDocuments(db, name);
274
+ }
275
+ return true;
276
+ },
277
+ renameCollection: async (oldName, newName) => {
278
+ if (externalConfigSource) {
279
+ if (!collectionsRenameCollection(oldName, newName, externalConfigSource))
280
+ return false;
281
+ syncConfigToDb(db, loadConfig(externalConfigSource));
282
+ }
283
+ else {
284
+ if (!getStoreCollection(db, oldName))
285
+ return false;
286
+ renameCollectionWithDocuments(db, oldName, newName);
287
+ }
288
+ return true;
289
+ },
290
+ listCollections: async () => storeListCollections(db),
291
+ getDefaultCollectionNames: async () => {
292
+ const collections = storeListCollections(db);
293
+ return collections.filter(c => c.includeByDefault).map(c => c.name);
294
+ },
295
+ // Context Management follows the same external-config-first reconciliation contract.
296
+ addContext: async (collectionName, pathPrefix, contextText) => {
297
+ if (externalConfigSource) {
298
+ if (!collectionsAddContext(collectionName, pathPrefix, contextText, externalConfigSource))
299
+ return false;
300
+ syncConfigToDb(db, loadConfig(externalConfigSource));
301
+ return true;
302
+ }
303
+ return updateStoreContext(db, collectionName, pathPrefix, contextText);
304
+ },
305
+ removeContext: async (collectionName, pathPrefix) => {
306
+ if (externalConfigSource) {
307
+ if (!collectionsRemoveContext(collectionName, pathPrefix, externalConfigSource))
308
+ return false;
309
+ syncConfigToDb(db, loadConfig(externalConfigSource));
310
+ return true;
311
+ }
312
+ return removeStoreContext(db, collectionName, pathPrefix);
313
+ },
314
+ setGlobalContext: async (context) => {
315
+ if (externalConfigSource) {
316
+ collectionsSetGlobalContext(context, externalConfigSource);
317
+ syncConfigToDb(db, loadConfig(externalConfigSource));
318
+ }
319
+ else {
320
+ setStoreGlobalContext(db, context);
321
+ }
322
+ },
323
+ getGlobalContext: async () => getStoreGlobalContext(db),
324
+ listContexts: async () => getStoreContexts(db),
325
+ // Indexing — reads collections from SQLite
326
+ update: async (updateOpts) => {
327
+ const collections = getStoreCollections(db);
328
+ const filtered = updateOpts?.collections
329
+ ? collections.filter(c => updateOpts.collections.includes(c.name))
330
+ : collections;
331
+ internal.clearCache();
332
+ let totalIndexed = 0, totalUpdated = 0, totalUnchanged = 0, totalRemoved = 0, totalSkipped = 0;
333
+ for (const col of filtered) {
334
+ const result = await reindexCollection(internal, col.path, col.pattern || "**/*.md", col.name, {
335
+ ignorePatterns: col.ignore,
336
+ onProgress: updateOpts?.onProgress
337
+ ? (info) => updateOpts.onProgress({ collection: col.name, ...info })
338
+ : undefined,
339
+ });
340
+ totalIndexed += result.indexed;
341
+ totalUpdated += result.updated;
342
+ totalUnchanged += result.unchanged;
343
+ totalRemoved += result.removed;
344
+ totalSkipped += result.skipped;
345
+ }
346
+ await rebuildCjkLexicalIndex(options.dbPath);
347
+ return {
348
+ collections: filtered.length,
349
+ indexed: totalIndexed,
350
+ updated: totalUpdated,
351
+ unchanged: totalUnchanged,
352
+ removed: totalRemoved,
353
+ skipped: totalSkipped,
354
+ needsEmbedding: internal.getHashesNeedingEmbedding(),
355
+ };
356
+ },
357
+ embed: async (embedOpts) => {
358
+ const provider = internal.embeddingProvider;
359
+ if (provider?.remote) {
360
+ if (embedOpts?.force && embedOpts.collection) {
361
+ throw new EmbeddingConfigError("Remote destructive embedding rebuilds cannot be collection-scoped.");
362
+ }
363
+ const identity = remoteEmbeddingIdentity(provider, embedOpts?.chunkStrategy);
364
+ const pending = getPendingEmbeddingDocsReadOnly(db, embedOpts?.collection, identity.model, identity.fingerprint);
365
+ if (pending.length === 0 && !embedOpts?.force) {
366
+ return { docsProcessed: 0, chunksEmbedded: 0, errors: 0, failures: [], durationMs: 0 };
367
+ }
368
+ if (!remoteKeyConfigured && !embedding.credentialAvailable) {
369
+ throw new EmbeddingConfigError("OpenAI document embedding is authorized, but OPENAI_API_KEY is not configured.");
370
+ }
371
+ }
372
+ return generateEmbeddings(internal, {
373
+ force: embedOpts?.force,
374
+ model: embedOpts?.model,
375
+ collection: embedOpts?.collection,
376
+ maxDocsPerBatch: embedOpts?.maxDocsPerBatch,
377
+ maxBatchBytes: embedOpts?.maxBatchBytes,
378
+ chunkStrategy: embedOpts?.chunkStrategy,
379
+ onProgress: embedOpts?.onProgress,
380
+ });
381
+ },
382
+ // Index Health
383
+ getStatus: async () => {
384
+ const diagnostics = inspectIndexDiagnostics(db, {
385
+ fallbackModel: embedding.canonical.model,
386
+ provider: internal.embeddingProvider,
387
+ keyConfigured: remoteKeyConfigured,
388
+ });
389
+ return {
390
+ ...getStatusReadOnly(db, diagnostics.embedding.chunks.pendingDocuments),
391
+ diagnostics,
392
+ };
393
+ },
394
+ getIndexHealth: async () => {
395
+ const diagnostics = inspectIndexDiagnostics(db, {
396
+ fallbackModel: embedding.canonical.model,
397
+ provider: internal.embeddingProvider,
398
+ keyConfigured: remoteKeyConfigured,
399
+ });
400
+ return getIndexHealthReadOnly(db, diagnostics.embedding.chunks.pendingDocuments);
401
+ },
402
+ // Lifecycle
403
+ close: () => {
404
+ if (!closePromise) {
405
+ closePromise = (async () => {
406
+ try {
407
+ await closeEmbeddingResources();
408
+ }
409
+ finally {
410
+ internal.close();
411
+ }
412
+ })();
413
+ }
414
+ return closePromise;
415
+ },
416
+ };
417
+ return store;
418
+ }