@spark-agents/engram 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,524 @@
1
+ import { homedir } from "node:os";
2
+ import path from "node:path";
3
+ import { resolveConfig } from "./config.js";
4
+ import { createEmbeddingClient } from "./embedding.js";
5
+ import { createEngramManager } from "./manager.js";
6
+ import { createReranker } from "./reranker.js";
7
+ import { createIndexManager } from "./store.js";
8
+ import { createSyncManager } from "./sync.js";
9
+ const managers = new Map();
10
+ function defaultAgentDir(agentId) {
11
+ return path.join(homedir(), ".openclaw", "agents", agentId);
12
+ }
13
+ async function resolveApiKey(api, config) {
14
+ if (config.geminiApiKey) {
15
+ const envMatch = config.geminiApiKey.match(/^\$\{(\w+)\}$/);
16
+ if (envMatch) {
17
+ const envValue = process.env[envMatch[1]];
18
+ if (envValue) {
19
+ return envValue;
20
+ }
21
+ }
22
+ else {
23
+ return config.geminiApiKey;
24
+ }
25
+ }
26
+ try {
27
+ const auth = await api.runtime.modelAuth.resolveApiKeyForProvider({
28
+ provider: "google",
29
+ cfg: api.config,
30
+ });
31
+ if (auth?.apiKey) {
32
+ return auth.apiKey;
33
+ }
34
+ }
35
+ catch {
36
+ // Provider auth may be unavailable depending on host runtime.
37
+ }
38
+ if (process.env.GEMINI_API_KEY) {
39
+ return process.env.GEMINI_API_KEY;
40
+ }
41
+ if (process.env.GOOGLE_API_KEY) {
42
+ return process.env.GOOGLE_API_KEY;
43
+ }
44
+ throw new Error("Engram: No Gemini API key found. Either configure Google as a provider (openclaw onboard), " +
45
+ "set GEMINI_API_KEY environment variable, or add geminiApiKey to plugin config.");
46
+ }
47
+ async function getOrCreateEngramManager(api, config, ctx) {
48
+ const agentId = String(ctx?.agentId ?? "default");
49
+ const existing = managers.get(agentId);
50
+ if (existing) {
51
+ return existing;
52
+ }
53
+ const apiKey = await resolveApiKey(api, config);
54
+ const baseUrl = api?.config?.models?.providers?.google?.baseUrl;
55
+ const workspaceDir = ctx?.workspaceDir ?? process.cwd();
56
+ const embedding = await createEmbeddingClient({
57
+ apiKey,
58
+ dimensions: config.dimensions,
59
+ baseUrl,
60
+ });
61
+ const dbPath = path.join(ctx?.agentDir ?? defaultAgentDir(agentId), "engram", "index.sqlite");
62
+ const sessionsDir = path.join(ctx?.agentDir ?? defaultAgentDir(agentId), "sessions");
63
+ const index = createIndexManager({
64
+ dbPath,
65
+ dimensions: config.dimensions,
66
+ workspaceDir,
67
+ });
68
+ const syncManager = createSyncManager({
69
+ workspaceDir,
70
+ index,
71
+ embedding,
72
+ chunkTokens: config.chunkTokens,
73
+ chunkOverlap: config.chunkOverlap,
74
+ sessionsDir,
75
+ multimodal: config.multimodal.enabled
76
+ ? {
77
+ enabled: true,
78
+ modalities: config.multimodal.modalities,
79
+ maxFileBytes: config.multimodal.maxFileBytes,
80
+ }
81
+ : undefined,
82
+ });
83
+ const reranker = config.reranking ? await createReranker() : null;
84
+ if (reranker) {
85
+ api.logger?.info?.("engram: cross-encoder reranker loaded");
86
+ }
87
+ else if (config.reranking) {
88
+ api.logger?.warn?.("engram: reranker requested but unavailable, continuing without");
89
+ }
90
+ const manager = createEngramManager({
91
+ index,
92
+ embedding,
93
+ syncManager,
94
+ reranker,
95
+ config,
96
+ workspaceDir,
97
+ });
98
+ syncManager.startWatching({
99
+ debounceMs: 1000,
100
+ intervalMinutes: 5,
101
+ });
102
+ managers.set(agentId, manager);
103
+ api.logger?.info?.(`engram: initialized manager for agent "${agentId}"`);
104
+ return manager;
105
+ }
106
+ export function resolveAgentsFromConfig(clawConfig, agentFilter) {
107
+ const agentList = Array.isArray(clawConfig?.agents?.list) ? clawConfig.agents.list : [];
108
+ const defaultWorkspace = clawConfig?.agents?.defaults?.workspace ?? path.join(homedir(), ".openclaw", "workspace");
109
+ const agents = agentList
110
+ .filter((agent) => typeof agent?.id === "string" && agent.id.trim().length > 0)
111
+ .map((agent) => ({
112
+ id: String(agent.id),
113
+ workspace: agent.workspace ?? defaultWorkspace,
114
+ agentDir: agent.agentDir ?? defaultAgentDir(agent.id),
115
+ }));
116
+ if (agentFilter) {
117
+ const match = agents.find((agent) => agent.id === agentFilter);
118
+ if (!match) {
119
+ throw new Error(`Agent "${agentFilter}" not found in config`);
120
+ }
121
+ return [match];
122
+ }
123
+ return agents;
124
+ }
125
+ function parsePositiveInteger(value, fallback) {
126
+ const parsed = Number.parseInt(String(value ?? ""), 10);
127
+ if (Number.isFinite(parsed) && parsed > 0) {
128
+ return parsed;
129
+ }
130
+ return fallback;
131
+ }
132
+ function parseNonNegativeNumber(value, fallback) {
133
+ const parsed = Number.parseFloat(String(value ?? ""));
134
+ if (Number.isFinite(parsed) && parsed >= 0) {
135
+ return parsed;
136
+ }
137
+ return fallback;
138
+ }
139
+ function truncateSnippet(text, maxLength) {
140
+ if (text.length <= maxLength) {
141
+ return text;
142
+ }
143
+ return `${text.slice(0, maxLength)}...`;
144
+ }
145
+ async function closeCliManagers(logger, agentIds) {
146
+ for (const agentId of agentIds) {
147
+ const manager = managers.get(agentId);
148
+ if (!manager) {
149
+ continue;
150
+ }
151
+ try {
152
+ await manager.close();
153
+ }
154
+ catch (error) {
155
+ const message = error instanceof Error ? error.message : String(error);
156
+ logger?.warn?.(`engram: failed to close manager for "${agentId}": ${message}`);
157
+ }
158
+ finally {
159
+ managers.delete(agentId);
160
+ }
161
+ }
162
+ }
163
+ function formatSources(status) {
164
+ if (!Array.isArray(status.sources) || status.sources.length === 0) {
165
+ return "none";
166
+ }
167
+ return status.sources.map((source) => source.source).join(", ");
168
+ }
169
+ function createCliContext(agent) {
170
+ return {
171
+ agentId: agent.id,
172
+ agentDir: agent.agentDir,
173
+ workspaceDir: agent.workspace,
174
+ };
175
+ }
176
+ export function registerEngramCli(program, api, config, clawConfig) {
177
+ const memory = program.command("engram").description("Engram memory plugin — status, search, and reindex");
178
+ memory
179
+ .command("status")
180
+ .description("Show memory search index status")
181
+ .option("--agent <id>", "Agent id")
182
+ .option("--deep", "Probe embedding and vector availability", false)
183
+ .option("--json", "Print JSON", false)
184
+ .option("--verbose", "Verbose logging", false)
185
+ .action(async (opts) => {
186
+ let agents;
187
+ try {
188
+ agents = resolveAgentsFromConfig(clawConfig, opts.agent);
189
+ }
190
+ catch (error) {
191
+ const message = error instanceof Error ? error.message : String(error);
192
+ console.error(message);
193
+ process.exitCode = 1;
194
+ return;
195
+ }
196
+ if (agents.length === 0) {
197
+ console.error("No agents found in OpenClaw config (agents.list).");
198
+ process.exitCode = 1;
199
+ return;
200
+ }
201
+ const rows = [];
202
+ const usedManagers = new Set();
203
+ try {
204
+ for (const agent of agents) {
205
+ if (opts.verbose) {
206
+ api.logger?.info?.(`engram: status for agent "${agent.id}"`);
207
+ }
208
+ try {
209
+ const manager = await getOrCreateEngramManager(api, config, createCliContext(agent));
210
+ usedManagers.add(agent.id);
211
+ const status = manager.status();
212
+ let embeddingProbe;
213
+ let vectorProbe;
214
+ if (opts.deep) {
215
+ embeddingProbe = await manager.probeEmbeddingAvailability();
216
+ vectorProbe = await manager.probeVectorAvailability();
217
+ }
218
+ const output = {
219
+ agentId: agent.id,
220
+ provider: status.provider,
221
+ model: status.model,
222
+ files: status.files,
223
+ chunks: status.chunks,
224
+ dirty: status.dirty,
225
+ dbPath: status.dbPath,
226
+ workspaceDir: agent.workspace,
227
+ sources: status.sources,
228
+ vector: status.vector,
229
+ fts: status.fts,
230
+ embeddingProbe,
231
+ vectorProbe,
232
+ };
233
+ rows.push(output);
234
+ if (!opts.json) {
235
+ console.log(`Memory Search (${agent.id})`);
236
+ console.log(`Provider: ${status.provider} (Engram)`);
237
+ console.log(`Model: ${status.model}`);
238
+ console.log(`Sources: ${formatSources(status)}`);
239
+ console.log(`Indexed: ${status.files} files · ${status.chunks} chunks`);
240
+ console.log(`Dirty: ${status.dirty ? "yes" : "no"}`);
241
+ console.log(`Store: ${status.dbPath}`);
242
+ console.log(`Workspace: ${agent.workspace}`);
243
+ console.log("By source:");
244
+ for (const source of status.sources) {
245
+ console.log(` ${source.source} · ${source.files} files · ${source.chunks} chunks`);
246
+ }
247
+ console.log(`Vector: ${status.vector.enabled ? "enabled" : "disabled"} (dims=${status.vector.dims})`);
248
+ console.log(`FTS: ${status.fts.enabled ? "enabled" : "disabled"}`);
249
+ if (opts.deep) {
250
+ const embeddingLine = embeddingProbe?.ok
251
+ ? "ok"
252
+ : `failed (${embeddingProbe?.error ?? "unknown error"})`;
253
+ console.log(`Embedding: ${embeddingLine}`);
254
+ console.log(`Vector probe: ${vectorProbe ? "ok" : "unavailable"}`);
255
+ }
256
+ console.log("");
257
+ }
258
+ }
259
+ catch (error) {
260
+ const message = error instanceof Error ? error.message : String(error);
261
+ rows.push({ agentId: agent.id, error: message });
262
+ if (opts.json) {
263
+ continue;
264
+ }
265
+ console.error(`Memory status failed (${agent.id}): ${message}`);
266
+ console.log("");
267
+ process.exitCode = 1;
268
+ }
269
+ }
270
+ if (opts.json) {
271
+ console.log(JSON.stringify(rows, null, 2));
272
+ }
273
+ }
274
+ finally {
275
+ await closeCliManagers(api.logger, usedManagers);
276
+ }
277
+ });
278
+ memory
279
+ .command("index")
280
+ .description("Reindex memory files")
281
+ .option("--agent <id>", "Agent id")
282
+ .option("--force", "Force full reindex", false)
283
+ .option("--verbose", "Verbose logging", false)
284
+ .action(async (opts) => {
285
+ let agents;
286
+ try {
287
+ agents = resolveAgentsFromConfig(clawConfig, opts.agent);
288
+ }
289
+ catch (error) {
290
+ const message = error instanceof Error ? error.message : String(error);
291
+ console.error(message);
292
+ process.exitCode = 1;
293
+ return;
294
+ }
295
+ if (agents.length === 0) {
296
+ console.error("No agents found in OpenClaw config (agents.list).");
297
+ process.exitCode = 1;
298
+ return;
299
+ }
300
+ let completed = 0;
301
+ const usedManagers = new Set();
302
+ try {
303
+ for (const agent of agents) {
304
+ if (opts.verbose) {
305
+ api.logger?.info?.(`engram: indexing agent "${agent.id}"`);
306
+ }
307
+ process.stdout.write(`Indexing ${agent.id}... `);
308
+ try {
309
+ const manager = await getOrCreateEngramManager(api, config, createCliContext(agent));
310
+ usedManagers.add(agent.id);
311
+ await manager.sync({ force: Boolean(opts.force) });
312
+ const status = manager.status();
313
+ console.log(`done (${status.files} files, ${status.chunks} chunks)`);
314
+ completed += 1;
315
+ }
316
+ catch (error) {
317
+ const message = error instanceof Error ? error.message : String(error);
318
+ console.log(`failed (${message})`);
319
+ process.exitCode = 1;
320
+ }
321
+ }
322
+ }
323
+ finally {
324
+ await closeCliManagers(api.logger, usedManagers);
325
+ }
326
+ console.log(`Indexed ${completed} agents`);
327
+ });
328
+ memory
329
+ .command("search")
330
+ .description("Search memory files")
331
+ .argument("[query]", "Search query")
332
+ .option("--agent <id>", "Agent id")
333
+ .option("--query <text>", "Search query (alternative to positional argument)")
334
+ .option("--max-results <n>", "Max results", "10")
335
+ .option("--min-score <n>", "Minimum score", "0")
336
+ .option("--json", "Print JSON", false)
337
+ .action(async (queryArg, opts) => {
338
+ const query = (opts.query ?? queryArg ?? "").trim();
339
+ if (!query) {
340
+ console.error("Missing search query. Provide [query] or --query <text>.");
341
+ process.exitCode = 1;
342
+ return;
343
+ }
344
+ let agent;
345
+ try {
346
+ if (opts.agent) {
347
+ agent = resolveAgentsFromConfig(clawConfig, opts.agent)[0];
348
+ }
349
+ else {
350
+ const agents = resolveAgentsFromConfig(clawConfig);
351
+ agent = agents[0];
352
+ }
353
+ }
354
+ catch (error) {
355
+ const message = error instanceof Error ? error.message : String(error);
356
+ console.error(message);
357
+ process.exitCode = 1;
358
+ return;
359
+ }
360
+ if (!agent) {
361
+ console.error("No agents found in OpenClaw config (agents.list).");
362
+ process.exitCode = 1;
363
+ return;
364
+ }
365
+ const maxResults = parsePositiveInteger(opts.maxResults, 10);
366
+ const minScore = parseNonNegativeNumber(opts.minScore, 0);
367
+ const usedManagers = new Set();
368
+ try {
369
+ const manager = await getOrCreateEngramManager(api, config, createCliContext(agent));
370
+ usedManagers.add(agent.id);
371
+ const results = await manager.search(query, {
372
+ maxResults,
373
+ minScore,
374
+ });
375
+ if (opts.json) {
376
+ console.log(JSON.stringify(results, null, 2));
377
+ return;
378
+ }
379
+ if (results.length === 0) {
380
+ console.log("No matches.");
381
+ return;
382
+ }
383
+ for (const result of results) {
384
+ const citation = `${result.path}#L${result.startLine}-L${result.endLine}`;
385
+ console.log(`[${result.score.toFixed(2)}] ${citation}`);
386
+ console.log(` ${truncateSnippet(result.snippet, 200)}`);
387
+ console.log("");
388
+ }
389
+ }
390
+ catch (error) {
391
+ const message = error instanceof Error ? error.message : String(error);
392
+ console.error(`Memory search failed: ${message}`);
393
+ process.exitCode = 1;
394
+ }
395
+ finally {
396
+ await closeCliManagers(api.logger, usedManagers);
397
+ }
398
+ });
399
+ }
400
+ function stringifyToolOutput(value) {
401
+ return {
402
+ content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
403
+ details: value,
404
+ };
405
+ }
406
+ function buildMemorySearchTool(api, getManager, ctx) {
407
+ return {
408
+ name: "memory_search",
409
+ label: "Memory Search",
410
+ description: "Semantically search memory files and session transcripts. Returns top snippets with path and lines.",
411
+ parameters: {
412
+ type: "object",
413
+ properties: {
414
+ query: { type: "string", description: "Search query" },
415
+ maxResults: { type: "number", description: "Max results (default: 10)" },
416
+ minScore: { type: "number", description: "Min relevance score 0-1 (default: 0)" },
417
+ },
418
+ required: ["query"],
419
+ },
420
+ async execute(_toolCallId, params) {
421
+ try {
422
+ const manager = await getManager();
423
+ manager.syncIfDirty();
424
+ const results = await manager.search(params.query, {
425
+ maxResults: params.maxResults,
426
+ minScore: params.minScore,
427
+ sessionKey: ctx?.sessionKey,
428
+ });
429
+ const status = manager.status();
430
+ return stringifyToolOutput({
431
+ results,
432
+ provider: status.provider,
433
+ model: status.model,
434
+ citations: "auto",
435
+ });
436
+ }
437
+ catch (error) {
438
+ const message = error instanceof Error ? error.message : String(error);
439
+ api.logger?.error?.(`engram: memory_search failed: ${message}`);
440
+ return stringifyToolOutput({
441
+ results: [],
442
+ disabled: true,
443
+ unavailable: true,
444
+ error: message,
445
+ warning: `Engram search failed: ${message}`,
446
+ action: "Check Gemini API key configuration",
447
+ });
448
+ }
449
+ },
450
+ };
451
+ }
452
+ function buildMemoryGetTool(api, getManager) {
453
+ return {
454
+ name: "memory_get",
455
+ label: "Memory Get",
456
+ description: "Read a snippet from a memory file by path and optional line range.",
457
+ parameters: {
458
+ type: "object",
459
+ properties: {
460
+ path: { type: "string", description: "Relative file path" },
461
+ from: { type: "number", description: "Start line (1-indexed)" },
462
+ lines: { type: "number", description: "Number of lines to return" },
463
+ },
464
+ required: ["path"],
465
+ },
466
+ async execute(_toolCallId, params) {
467
+ try {
468
+ const manager = await getManager();
469
+ const result = await manager.readFile({
470
+ relPath: params.path,
471
+ from: params.from,
472
+ lines: params.lines,
473
+ });
474
+ return stringifyToolOutput({
475
+ text: result.text,
476
+ path: result.path,
477
+ });
478
+ }
479
+ catch (error) {
480
+ const message = error instanceof Error ? error.message : String(error);
481
+ api.logger?.error?.(`engram: memory_get failed for "${params.path}": ${message}`);
482
+ return stringifyToolOutput({
483
+ path: params.path,
484
+ text: "",
485
+ disabled: true,
486
+ unavailable: true,
487
+ error: message,
488
+ });
489
+ }
490
+ },
491
+ };
492
+ }
493
+ const engramPlugin = {
494
+ id: "engram",
495
+ name: "Engram",
496
+ description: "Multimodal memory powered by Gemini Embedding-2 with hybrid search and ONNX reranking",
497
+ kind: "memory",
498
+ register(api) {
499
+ const config = resolveConfig(api.pluginConfig);
500
+ const logger = api.logger;
501
+ logger?.info?.(`engram: registered (dims: ${config.dimensions}, chunks: ${config.chunkTokens})`);
502
+ if (typeof api.registerTool === "function") {
503
+ api.registerTool((ctx) => {
504
+ const manager = getOrCreateEngramManager(api, config, ctx);
505
+ return [
506
+ buildMemorySearchTool(api, () => manager, ctx),
507
+ buildMemoryGetTool(api, () => manager),
508
+ ];
509
+ }, { names: ["memory_search", "memory_get"] });
510
+ }
511
+ else {
512
+ logger?.warn?.("engram: registerTool is not available on plugin API");
513
+ }
514
+ if (typeof api.registerCli === "function") {
515
+ api.registerCli(({ program, config: clawConfig }) => {
516
+ registerEngramCli(program, api, config, clawConfig);
517
+ }, { commands: ["engram"] });
518
+ }
519
+ else {
520
+ logger?.warn?.("engram: registerCli is not available on plugin API");
521
+ }
522
+ },
523
+ };
524
+ export default engramPlugin;
@@ -0,0 +1,76 @@
1
+ import type { ResolvedConfig } from "./config.js";
2
+ import type { Reranker } from "./reranker.js";
3
+ import type { SyncManager } from "./sync.js";
4
+ import type { EmbeddingClient, IndexManager, SearchResult, SyncOptions } from "./types.js";
5
+ export interface EngramManager {
6
+ /** Search memory (delegates to search.ts) */
7
+ search(query: string, opts?: {
8
+ maxResults?: number;
9
+ minScore?: number;
10
+ sessionKey?: string;
11
+ }): Promise<SearchResult[]>;
12
+ /** Read a file by relative path with optional line range */
13
+ readFile(params: {
14
+ relPath: string;
15
+ from?: number;
16
+ lines?: number;
17
+ }): Promise<{
18
+ text: string;
19
+ path: string;
20
+ }>;
21
+ /** Get provider status */
22
+ status(): EngramStatus;
23
+ /** Sync the index */
24
+ sync(opts?: SyncOptions): Promise<void>;
25
+ /** Lazy sync before search if dirty */
26
+ syncIfDirty(): void;
27
+ /** Sync once for a session key */
28
+ warmSession(sessionKey?: string): Promise<void>;
29
+ /** Test if embedding API is reachable */
30
+ probeEmbeddingAvailability(): Promise<{
31
+ ok: boolean;
32
+ error?: string;
33
+ }>;
34
+ /** Test if sqlite-vec is loaded */
35
+ probeVectorAvailability(): Promise<boolean>;
36
+ /** Clean up resources */
37
+ close(): Promise<void>;
38
+ }
39
+ export interface EngramStatus {
40
+ backend: string;
41
+ provider: string;
42
+ model: string;
43
+ files: number;
44
+ chunks: number;
45
+ dirty: boolean;
46
+ workspaceDir: string;
47
+ dbPath: string;
48
+ sources: Array<{
49
+ source: "memory" | "sessions";
50
+ files: number;
51
+ chunks: number;
52
+ }>;
53
+ vector: {
54
+ enabled: boolean;
55
+ dims: number;
56
+ };
57
+ fts: {
58
+ enabled: boolean;
59
+ available: boolean;
60
+ };
61
+ custom: {
62
+ rrfK: number;
63
+ vectorWeight: number;
64
+ bm25Weight: number;
65
+ chunkTokens: number;
66
+ chunkOverlap: number;
67
+ };
68
+ }
69
+ export declare function createEngramManager(params: {
70
+ index: IndexManager;
71
+ embedding: EmbeddingClient;
72
+ syncManager: SyncManager;
73
+ reranker?: Reranker | null;
74
+ config: ResolvedConfig;
75
+ workspaceDir: string;
76
+ }): EngramManager;
@@ -0,0 +1,103 @@
1
+ import { search } from "./search.js";
2
+ const DEFAULT_RRF_K = 60;
3
+ const DEFAULT_VECTOR_WEIGHT = 0.7;
4
+ const DEFAULT_BM25_WEIGHT = 0.3;
5
+ function inferProvider(model) {
6
+ const normalized = model.toLowerCase();
7
+ if (normalized.includes("gemini")) {
8
+ return "gemini";
9
+ }
10
+ if (normalized.includes("text-embedding")) {
11
+ return "openai";
12
+ }
13
+ return "gemini";
14
+ }
15
+ export function createEngramManager(params) {
16
+ const { index, embedding, syncManager, reranker, config, workspaceDir } = params;
17
+ return {
18
+ async search(query, opts) {
19
+ return search(query, embedding, index, {
20
+ maxResults: opts?.maxResults,
21
+ minScore: opts?.minScore,
22
+ sessionKey: opts?.sessionKey,
23
+ vectorWeight: DEFAULT_VECTOR_WEIGHT,
24
+ bm25Weight: DEFAULT_BM25_WEIGHT,
25
+ rrfK: DEFAULT_RRF_K,
26
+ timeDecay: config.timeDecay,
27
+ maxSessionShare: config.maxSessionShare,
28
+ reranker: reranker ?? null,
29
+ });
30
+ },
31
+ async readFile(params) {
32
+ const result = index.readFileContent(params.relPath, params.from, params.lines);
33
+ if (result) {
34
+ return result;
35
+ }
36
+ return { text: "", path: params.relPath };
37
+ },
38
+ status() {
39
+ const stats = index.stats();
40
+ const vectorEnabled = stats.vectorDims > 0;
41
+ return {
42
+ backend: "engram",
43
+ provider: inferProvider(embedding.model),
44
+ model: embedding.model,
45
+ files: stats.files,
46
+ chunks: stats.chunks,
47
+ dirty: syncManager.isDirty(),
48
+ workspaceDir,
49
+ dbPath: stats.dbPath,
50
+ sources: stats.sources,
51
+ vector: {
52
+ enabled: vectorEnabled,
53
+ dims: stats.vectorDims,
54
+ },
55
+ fts: {
56
+ enabled: true,
57
+ available: true,
58
+ },
59
+ custom: {
60
+ rrfK: DEFAULT_RRF_K,
61
+ vectorWeight: DEFAULT_VECTOR_WEIGHT,
62
+ bm25Weight: DEFAULT_BM25_WEIGHT,
63
+ chunkTokens: config.chunkTokens,
64
+ chunkOverlap: config.chunkOverlap,
65
+ },
66
+ };
67
+ },
68
+ async sync(opts) {
69
+ await syncManager.sync(opts);
70
+ },
71
+ syncIfDirty() {
72
+ syncManager.syncIfDirty();
73
+ },
74
+ async warmSession(sessionKey) {
75
+ await syncManager.warmSession(sessionKey);
76
+ },
77
+ async probeEmbeddingAvailability() {
78
+ try {
79
+ await embedding.embedText("test", "RETRIEVAL_QUERY");
80
+ return { ok: true };
81
+ }
82
+ catch (error) {
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ return { ok: false, error: message };
85
+ }
86
+ },
87
+ async probeVectorAvailability() {
88
+ return index.stats().vectorDims > 0;
89
+ },
90
+ async close() {
91
+ if (reranker) {
92
+ try {
93
+ await reranker.close();
94
+ }
95
+ catch {
96
+ // Keep shutdown best-effort.
97
+ }
98
+ }
99
+ index.close();
100
+ syncManager.close();
101
+ },
102
+ };
103
+ }