@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.
- package/CHANGELOG.md +1373 -0
- package/LICENSE +45 -0
- package/README.md +1439 -0
- package/THIRD_PARTY_NOTICES.md +31 -0
- package/bin/qmd +192 -0
- package/dist/ast.d.ts +65 -0
- package/dist/ast.js +334 -0
- package/dist/bench/bench.d.ts +35 -0
- package/dist/bench/bench.js +338 -0
- package/dist/bench/cjk-baseline.d.ts +36 -0
- package/dist/bench/cjk-baseline.js +111 -0
- package/dist/bench/fixture.d.ts +2 -0
- package/dist/bench/fixture.js +84 -0
- package/dist/bench/score.d.ts +38 -0
- package/dist/bench/score.js +107 -0
- package/dist/bench/types.d.ts +110 -0
- package/dist/bench/types.js +8 -0
- package/dist/cli/build-info.json +4 -0
- package/dist/cli/embed-lock.d.ts +24 -0
- package/dist/cli/embed-lock.js +94 -0
- package/dist/cli/embedding-owner.d.ts +10 -0
- package/dist/cli/embedding-owner.js +20 -0
- package/dist/cli/formatter.d.ts +120 -0
- package/dist/cli/formatter.js +355 -0
- package/dist/cli/mcp-pid.d.ts +25 -0
- package/dist/cli/mcp-pid.js +86 -0
- package/dist/cli/qmd.d.ts +72 -0
- package/dist/cli/qmd.js +4806 -0
- package/dist/cli/version.d.ts +42 -0
- package/dist/cli/version.js +80 -0
- package/dist/collections.d.ts +200 -0
- package/dist/collections.js +433 -0
- package/dist/db.d.ts +65 -0
- package/dist/db.js +143 -0
- package/dist/diagnostics.d.ts +62 -0
- package/dist/diagnostics.js +260 -0
- package/dist/embedding/config.d.ts +52 -0
- package/dist/embedding/config.js +229 -0
- package/dist/embedding/identity.d.ts +58 -0
- package/dist/embedding/identity.js +321 -0
- package/dist/embedding/local-identity.d.ts +1 -0
- package/dist/embedding/local-identity.js +15 -0
- package/dist/embedding/local.d.ts +34 -0
- package/dist/embedding/local.js +290 -0
- package/dist/embedding/openai.d.ts +79 -0
- package/dist/embedding/openai.js +477 -0
- package/dist/embedding/owner.d.ts +13 -0
- package/dist/embedding/owner.js +36 -0
- package/dist/embedding/provider.d.ts +68 -0
- package/dist/embedding/provider.js +16 -0
- package/dist/embedding/remote-chunking.d.ts +22 -0
- package/dist/embedding/remote-chunking.js +83 -0
- package/dist/embedding/remote-embedding.d.ts +15 -0
- package/dist/embedding/remote-embedding.js +77 -0
- package/dist/hybrid-llm.d.ts +18 -0
- package/dist/hybrid-llm.js +53 -0
- package/dist/index.d.ts +244 -0
- package/dist/index.js +418 -0
- package/dist/llm.d.ts +566 -0
- package/dist/llm.js +1847 -0
- package/dist/maintenance.d.ts +33 -0
- package/dist/maintenance.js +52 -0
- package/dist/mcp/origin-guard.d.ts +67 -0
- package/dist/mcp/origin-guard.js +137 -0
- package/dist/mcp/server.d.ts +116 -0
- package/dist/mcp/server.js +919 -0
- package/dist/paths.d.ts +1 -0
- package/dist/paths.js +4 -0
- package/dist/remote-llm.d.ts +52 -0
- package/dist/remote-llm.js +464 -0
- package/dist/search/cjk-analyzer.d.ts +33 -0
- package/dist/search/cjk-analyzer.js +158 -0
- package/dist/search/cjk-index.d.ts +104 -0
- package/dist/search/cjk-index.js +1031 -0
- package/dist/search/jieba-loader.d.ts +23 -0
- package/dist/search/jieba-loader.js +79 -0
- package/dist/search/query-expansion.d.ts +23 -0
- package/dist/search/query-expansion.js +43 -0
- package/dist/search/zh-dict.txt +624013 -0
- package/dist/store.d.ts +1218 -0
- package/dist/store.js +6076 -0
- package/dist/trust.d.ts +152 -0
- package/dist/trust.js +249 -0
- package/package.json +139 -0
- package/scripts/build.mjs +83 -0
- package/scripts/check-package-grammars.mjs +29 -0
- package/scripts/package-smoke.mjs +205 -0
- package/scripts/sync-zh-dict.mjs +187 -0
- package/scripts/test-all.mjs +45 -0
- package/skills/qmd/SKILL.md +324 -0
- package/skills/qmd/references/mcp-setup.md +119 -0
- package/skills/release/SKILL.md +141 -0
- package/skills/release/scripts/install-hooks.sh +38 -0
- 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
|
+
}
|