cntx-ui 3.1.2 → 3.1.4

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/README.md CHANGED
@@ -40,6 +40,10 @@ Agents interact through MCP tools or the HTTP API:
40
40
  | `agent/query` | Semantic search — "where is auth handled?" |
41
41
  | `agent/investigate` | Find integration points for a new feature |
42
42
  | `agent/organize` | Audit and optimize bundle structure |
43
+ | `artifacts/list` | List normalized project artifacts (OpenAPI + Navigation) |
44
+ | `artifacts/get_openapi` | Return OpenAPI artifact summary and payload |
45
+ | `artifacts/get_navigation` | Return Navigation artifact summary and payload |
46
+ | `artifacts/summarize` | Compact cross-artifact summary for agents |
43
47
  | `list_bundles` | List all bundles with metadata |
44
48
  | `get_bundle` | Get full bundle content as XML |
45
49
  | `get_semantic_chunks` | Get all analyzed code chunks |
@@ -47,6 +51,11 @@ Agents interact through MCP tools or the HTTP API:
47
51
 
48
52
  Full tool reference with parameters is generated in `.cntx/AGENT.md` and `.cntx/TOOLS.md`.
49
53
 
54
+ Artifact HTTP endpoints:
55
+ - `GET /api/artifacts`
56
+ - `GET /api/artifacts/openapi`
57
+ - `GET /api/artifacts/navigation`
58
+
50
59
  ## How it works
51
60
 
52
61
  1. **tree-sitter** parses source files into AST, extracts functions/types/interfaces
@@ -14,7 +14,12 @@ const command = args[0] || 'help';
14
14
  const isVerbose = args.includes('--verbose');
15
15
  // Graceful shutdown
16
16
  process.on('SIGINT', () => {
17
- console.log('\nšŸ‘‹ Shutting down cntx-ui...');
17
+ if (command === 'mcp') {
18
+ process.stderr.write('\nšŸ‘‹ Shutting down cntx-ui...\n');
19
+ }
20
+ else {
21
+ console.log('\nšŸ‘‹ Shutting down cntx-ui...');
22
+ }
18
23
  process.exit(0);
19
24
  });
20
25
  async function main() {
@@ -32,7 +37,7 @@ async function main() {
32
37
  await initConfig();
33
38
  break;
34
39
  case 'mcp':
35
- await startServer({ withMcp: true, skipFileWatcher: true, skipBundleGeneration: true });
40
+ await startServer({ withMcp: true, skipFileWatcher: true, isMcp: true });
36
41
  break;
37
42
  case 'bundle':
38
43
  const bundleName = args[1] || 'master';
@@ -81,6 +81,9 @@ export default class APIRouter {
81
81
  if (pathname === '/api/vector-db/search' && method === 'POST') {
82
82
  return await this.handlePostVectorDbSearch(req, res);
83
83
  }
84
+ if (pathname === '/api/vector-db/projection' && method === 'GET') {
85
+ return await this.handleGetVectorDbProjection(req, res);
86
+ }
84
87
  if (pathname === '/api/vector-db/network' && method === 'GET') {
85
88
  return await this.handleGetVectorDbNetwork(req, res);
86
89
  }
@@ -106,6 +109,16 @@ export default class APIRouter {
106
109
  if (pathname === '/api/mcp-status' && method === 'GET') {
107
110
  return await this.handleGetMcpStatus(req, res);
108
111
  }
112
+ // === Artifact Endpoints ===
113
+ if (pathname === '/api/artifacts' && method === 'GET') {
114
+ return await this.handleGetArtifacts(req, res);
115
+ }
116
+ if (pathname === '/api/artifacts/openapi' && method === 'GET') {
117
+ return await this.handleGetArtifact(req, res, 'openapi');
118
+ }
119
+ if (pathname === '/api/artifacts/navigation' && method === 'GET') {
120
+ return await this.handleGetArtifact(req, res, 'navigation');
121
+ }
109
122
  // === Rule Management ===
110
123
  if (pathname === '/api/cursor-rules' && method === 'GET') {
111
124
  return await this.handleGetCursorRules(req, res);
@@ -301,6 +314,103 @@ export default class APIRouter {
301
314
  const results = await this.vectorStore.search(query, { limit });
302
315
  this.sendResponse(res, 200, results);
303
316
  }
317
+ async handleGetVectorDbProjection(req, res) {
318
+ const dbManager = this.configManager.dbManager;
319
+ const embeddingCountRow = dbManager.db.prepare('SELECT COUNT(*) as count FROM vector_embeddings').get();
320
+ const currentEmbeddingCount = embeddingCountRow.count;
321
+ if (currentEmbeddingCount === 0) {
322
+ return this.sendResponse(res, 200, {
323
+ points: [],
324
+ meta: { totalPoints: 0, embeddingCount: 0, computedAt: null, cached: false }
325
+ });
326
+ }
327
+ // Check cache freshness
328
+ const cachedCount = dbManager.getProjectionEmbeddingCount();
329
+ if (cachedCount === currentEmbeddingCount) {
330
+ const cached = dbManager.getProjections();
331
+ if (cached) {
332
+ // Join with chunk metadata
333
+ const chunkMap = new Map();
334
+ const chunks = dbManager.db.prepare('SELECT * FROM semantic_chunks').all();
335
+ for (const c of chunks) {
336
+ const mapped = dbManager.mapChunkRow(c);
337
+ chunkMap.set(mapped.id, mapped);
338
+ }
339
+ const points = cached.map(p => {
340
+ const chunk = chunkMap.get(p.chunkId);
341
+ return {
342
+ id: p.chunkId,
343
+ x: p.x,
344
+ y: p.y,
345
+ name: chunk?.name || 'unknown',
346
+ filePath: chunk?.filePath || '',
347
+ purpose: chunk?.purpose || 'unknown',
348
+ semanticType: chunk?.subtype || chunk?.type || 'unknown',
349
+ complexity: chunk?.complexity?.score || 0,
350
+ directory: chunk?.filePath ? chunk.filePath.split('/').slice(0, -1).join('/') || '.' : '.'
351
+ };
352
+ });
353
+ return this.sendResponse(res, 200, {
354
+ points,
355
+ meta: { totalPoints: points.length, embeddingCount: currentEmbeddingCount, computedAt: new Date().toISOString(), cached: true }
356
+ });
357
+ }
358
+ }
359
+ // Compute fresh UMAP projection
360
+ const rows = dbManager.db.prepare(`
361
+ SELECT ve.chunk_id, ve.embedding, sc.name, sc.file_path, sc.type, sc.subtype, sc.complexity_score, sc.purpose
362
+ FROM vector_embeddings ve
363
+ JOIN semantic_chunks sc ON ve.chunk_id = sc.id
364
+ `).all();
365
+ if (rows.length < 2) {
366
+ return this.sendResponse(res, 200, {
367
+ points: rows.map((r) => ({
368
+ id: r.chunk_id, x: 0, y: 0,
369
+ name: r.name, filePath: r.file_path, purpose: r.purpose || 'unknown',
370
+ semanticType: r.subtype || r.type || 'unknown', complexity: r.complexity_score || 0,
371
+ directory: r.file_path ? r.file_path.split('/').slice(0, -1).join('/') || '.' : '.'
372
+ })),
373
+ meta: { totalPoints: rows.length, embeddingCount: currentEmbeddingCount, computedAt: new Date().toISOString(), cached: false }
374
+ });
375
+ }
376
+ // Extract embeddings as number[][]
377
+ const embeddings = rows.map((r) => {
378
+ const buf = r.embedding;
379
+ const floats = new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
380
+ return Array.from(floats);
381
+ });
382
+ // Run UMAP
383
+ const { UMAP } = await import('umap-js');
384
+ const umap = new UMAP({
385
+ nNeighbors: Math.min(15, rows.length - 1),
386
+ minDist: 0.1,
387
+ nComponents: 2
388
+ });
389
+ const projected = umap.fit(embeddings);
390
+ // Save to cache
391
+ const projections = rows.map((r, i) => ({
392
+ chunkId: r.chunk_id,
393
+ x: projected[i][0],
394
+ y: projected[i][1]
395
+ }));
396
+ dbManager.saveProjections(projections, currentEmbeddingCount);
397
+ // Build response
398
+ const points = rows.map((r, i) => ({
399
+ id: r.chunk_id,
400
+ x: projected[i][0],
401
+ y: projected[i][1],
402
+ name: r.name,
403
+ filePath: r.file_path,
404
+ purpose: r.purpose || 'unknown',
405
+ semanticType: r.subtype || r.type || 'unknown',
406
+ complexity: r.complexity_score || 0,
407
+ directory: r.file_path ? r.file_path.split('/').slice(0, -1).join('/') || '.' : '.'
408
+ }));
409
+ this.sendResponse(res, 200, {
410
+ points,
411
+ meta: { totalPoints: points.length, embeddingCount: currentEmbeddingCount, computedAt: new Date().toISOString(), cached: false }
412
+ });
413
+ }
304
414
  async handleGetVectorDbNetwork(req, res) {
305
415
  const chunks = this.configManager.dbManager.db.prepare('SELECT * FROM semantic_chunks').all();
306
416
  const embeddings = this.configManager.dbManager.db.prepare('SELECT * FROM vector_embeddings').all();
@@ -350,6 +460,15 @@ export default class APIRouter {
350
460
  message: isRunning ? 'MCP server is running' : 'MCP server integration available'
351
461
  });
352
462
  }
463
+ async handleGetArtifacts(req, res) {
464
+ const artifacts = this.cntxServer.artifactManager.refresh();
465
+ this.sendResponse(res, 200, { artifacts });
466
+ }
467
+ async handleGetArtifact(req, res, type) {
468
+ this.cntxServer.artifactManager.refresh();
469
+ const payload = this.cntxServer.artifactManager.getPayload(type);
470
+ this.sendResponse(res, 200, payload);
471
+ }
353
472
  async handleGetCursorRules(req, res) {
354
473
  const filePath = join(this.configManager.CWD, '.cursorrules');
355
474
  if (existsSync(filePath)) {
@@ -0,0 +1,147 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ export default class ArtifactManager {
4
+ cwd;
5
+ records;
6
+ constructor(cwd) {
7
+ this.cwd = cwd;
8
+ this.records = new Map();
9
+ }
10
+ refresh() {
11
+ const openapi = this.resolveOpenApiArtifact();
12
+ const navigation = this.resolveNavigationArtifact();
13
+ this.records.set('openapi', openapi);
14
+ this.records.set('navigation', navigation);
15
+ return this.list();
16
+ }
17
+ list() {
18
+ if (this.records.size === 0) {
19
+ this.refresh();
20
+ }
21
+ return Array.from(this.records.values());
22
+ }
23
+ get(type) {
24
+ if (!this.records.has(type)) {
25
+ this.refresh();
26
+ }
27
+ return this.records.get(type);
28
+ }
29
+ getPayload(type) {
30
+ const record = this.get(type);
31
+ if (!record) {
32
+ return {
33
+ type,
34
+ filePath: '',
35
+ format: 'json',
36
+ exists: false,
37
+ summary: {}
38
+ };
39
+ }
40
+ if (!record.exists) {
41
+ return { ...record };
42
+ }
43
+ try {
44
+ const absolutePath = join(this.cwd, record.filePath);
45
+ const raw = readFileSync(absolutePath, 'utf8');
46
+ if (record.format === 'json') {
47
+ return { ...record, parsed: JSON.parse(raw), raw };
48
+ }
49
+ return { ...record, raw };
50
+ }
51
+ catch {
52
+ return { ...record };
53
+ }
54
+ }
55
+ resolveOpenApiArtifact() {
56
+ const candidates = [
57
+ { path: 'openapi.json', format: 'json' },
58
+ { path: 'openapi.yaml', format: 'yaml' },
59
+ { path: 'openapi.yml', format: 'yaml' }
60
+ ];
61
+ for (const candidate of candidates) {
62
+ const absolutePath = join(this.cwd, candidate.path);
63
+ if (!existsSync(absolutePath))
64
+ continue;
65
+ const summary = this.summarizeOpenApi(absolutePath, candidate.format);
66
+ return {
67
+ type: 'openapi',
68
+ filePath: candidate.path,
69
+ format: candidate.format,
70
+ exists: true,
71
+ summary
72
+ };
73
+ }
74
+ return {
75
+ type: 'openapi',
76
+ filePath: 'openapi.json',
77
+ format: 'json',
78
+ exists: false,
79
+ summary: {}
80
+ };
81
+ }
82
+ resolveNavigationArtifact() {
83
+ const candidates = [
84
+ { path: 'navigation.manifest.json', format: 'json' },
85
+ { path: 'navigation.json', format: 'json' }
86
+ ];
87
+ for (const candidate of candidates) {
88
+ const absolutePath = join(this.cwd, candidate.path);
89
+ if (!existsSync(absolutePath))
90
+ continue;
91
+ const summary = this.summarizeNavigation(absolutePath);
92
+ return {
93
+ type: 'navigation',
94
+ filePath: candidate.path,
95
+ format: candidate.format,
96
+ exists: true,
97
+ summary
98
+ };
99
+ }
100
+ return {
101
+ type: 'navigation',
102
+ filePath: 'navigation.manifest.json',
103
+ format: 'json',
104
+ exists: false,
105
+ summary: {}
106
+ };
107
+ }
108
+ summarizeOpenApi(absolutePath, format) {
109
+ try {
110
+ const raw = readFileSync(absolutePath, 'utf8');
111
+ if (format === 'json') {
112
+ const parsed = JSON.parse(raw);
113
+ const endpointCount = parsed.paths ? Object.keys(parsed.paths).length : 0;
114
+ return {
115
+ title: parsed.info?.title,
116
+ version: parsed.info?.version,
117
+ endpointCount
118
+ };
119
+ }
120
+ const titleMatch = raw.match(/^\s*title:\s*["']?(.+?)["']?\s*$/m);
121
+ const versionMatch = raw.match(/^\s*version:\s*["']?(.+?)["']?\s*$/m);
122
+ return {
123
+ title: titleMatch?.[1],
124
+ version: versionMatch?.[1]
125
+ };
126
+ }
127
+ catch {
128
+ return {};
129
+ }
130
+ }
131
+ summarizeNavigation(absolutePath) {
132
+ try {
133
+ const parsed = JSON.parse(readFileSync(absolutePath, 'utf8'));
134
+ return {
135
+ title: parsed.project?.name || parsed.name,
136
+ version: parsed.version ? String(parsed.version) : undefined,
137
+ routeCount: Array.isArray(parsed.routes) ? parsed.routes.length : 0,
138
+ stateCount: Array.isArray(parsed.states) ? parsed.states.length : 0,
139
+ flowCount: Array.isArray(parsed.flows) ? parsed.flows.length : 0,
140
+ viewportCount: Array.isArray(parsed.viewports) ? parsed.viewports.length : 0
141
+ };
142
+ }
143
+ catch {
144
+ return {};
145
+ }
146
+ }
147
+ }
@@ -170,6 +170,10 @@ export default class BundleManager {
170
170
  try {
171
171
  const fullPath = this.fileSystemManager.fullPath(relativePath);
172
172
  const content = readFileSync(fullPath, 'utf8');
173
+ // Skip files over 100KB in bundle XML to prevent string length crashes
174
+ if (content.length > 100_000) {
175
+ return ` <file path="${this.escapeXml(relativePath)}" skipped="too-large" size="${content.length}" />\n`;
176
+ }
173
177
  const chunks = this.db.getChunksByFile(relativePath);
174
178
  let xml = ` <file path="${this.escapeXml(relativePath)}">\n`;
175
179
  if (chunks.length > 0) {
@@ -79,6 +79,16 @@ export default class DatabaseManager {
79
79
  FOREIGN KEY(session_id) REFERENCES agent_sessions(id) ON DELETE CASCADE
80
80
  );
81
81
 
82
+ -- UMAP Projection Cache
83
+ CREATE TABLE IF NOT EXISTS umap_projections (
84
+ chunk_id TEXT PRIMARY KEY,
85
+ x REAL NOT NULL,
86
+ y REAL NOT NULL,
87
+ computed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
88
+ embedding_count INTEGER NOT NULL,
89
+ FOREIGN KEY(chunk_id) REFERENCES semantic_chunks(id) ON DELETE CASCADE
90
+ );
91
+
82
92
  CREATE INDEX IF NOT EXISTS idx_bundles_changed ON bundles(changed);
83
93
  CREATE INDEX IF NOT EXISTS idx_chunks_file ON semantic_chunks(file_path);
84
94
  CREATE INDEX IF NOT EXISTS idx_chunks_purpose ON semantic_chunks(purpose);
@@ -265,6 +275,44 @@ export default class DatabaseManager {
265
275
  return [];
266
276
  }
267
277
  }
278
+ // UMAP Projection Cache
279
+ saveProjections(projections, embeddingCount) {
280
+ const transaction = this.db.transaction(() => {
281
+ this.db.prepare('DELETE FROM umap_projections').run();
282
+ const stmt = this.db.prepare('INSERT INTO umap_projections (chunk_id, x, y, embedding_count) VALUES (?, ?, ?, ?)');
283
+ for (const p of projections) {
284
+ stmt.run(p.chunkId, p.x, p.y, embeddingCount);
285
+ }
286
+ });
287
+ try {
288
+ transaction();
289
+ return true;
290
+ }
291
+ catch (error) {
292
+ console.error('Failed to save projections:', error.message);
293
+ return false;
294
+ }
295
+ }
296
+ getProjections() {
297
+ try {
298
+ const rows = this.db.prepare('SELECT chunk_id, x, y, embedding_count FROM umap_projections').all();
299
+ if (rows.length === 0)
300
+ return null;
301
+ return rows.map(r => ({ chunkId: r.chunk_id, x: r.x, y: r.y, embeddingCount: r.embedding_count }));
302
+ }
303
+ catch (error) {
304
+ return null;
305
+ }
306
+ }
307
+ getProjectionEmbeddingCount() {
308
+ try {
309
+ const row = this.db.prepare('SELECT embedding_count FROM umap_projections LIMIT 1').get();
310
+ return row?.embedding_count ?? 0;
311
+ }
312
+ catch (error) {
313
+ return 0;
314
+ }
315
+ }
268
316
  // Close database connection
269
317
  close() {
270
318
  if (this.db) {
@@ -92,6 +92,28 @@ export class MCPServer {
92
92
  }
93
93
  // Legacy tools mapping
94
94
  switch (name) {
95
+ case 'artifacts/list': {
96
+ const artifacts = this.cntxServer.artifactManager.refresh();
97
+ return this.createSuccessResponse(id, { content: [{ type: 'text', text: JSON.stringify({ artifacts }, null, 2) }] });
98
+ }
99
+ case 'artifacts/get_openapi': {
100
+ this.cntxServer.artifactManager.refresh();
101
+ const payload = this.cntxServer.artifactManager.getPayload('openapi');
102
+ return this.createSuccessResponse(id, { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] });
103
+ }
104
+ case 'artifacts/get_navigation': {
105
+ this.cntxServer.artifactManager.refresh();
106
+ const payload = this.cntxServer.artifactManager.getPayload('navigation');
107
+ return this.createSuccessResponse(id, { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] });
108
+ }
109
+ case 'artifacts/summarize': {
110
+ const artifacts = this.cntxServer.artifactManager.refresh();
111
+ const summary = {
112
+ openapi: artifacts.find((a) => a.type === 'openapi')?.summary ?? {},
113
+ navigation: artifacts.find((a) => a.type === 'navigation')?.summary ?? {}
114
+ };
115
+ return this.createSuccessResponse(id, { content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }] });
116
+ }
95
117
  case 'list_bundles':
96
118
  const bundles = this.cntxServer.bundleManager.getAllBundleInfo();
97
119
  return this.createSuccessResponse(id, { content: [{ type: 'text', text: JSON.stringify(bundles, null, 2) }] });
@@ -144,6 +166,26 @@ export class MCPServer {
144
166
  description: 'Investigation Mode: Suggest integration points.',
145
167
  inputSchema: { type: 'object', properties: { feature: { type: 'string' } }, required: ['feature'] }
146
168
  },
169
+ {
170
+ name: 'artifacts/list',
171
+ description: 'List normalized project artifacts (OpenAPI and Navigation manifests).',
172
+ inputSchema: { type: 'object', properties: {} }
173
+ },
174
+ {
175
+ name: 'artifacts/get_openapi',
176
+ description: 'Get OpenAPI artifact payload (summary + parsed content when JSON).',
177
+ inputSchema: { type: 'object', properties: {} }
178
+ },
179
+ {
180
+ name: 'artifacts/get_navigation',
181
+ description: 'Get Navigation artifact payload (summary + parsed manifest).',
182
+ inputSchema: { type: 'object', properties: {} }
183
+ },
184
+ {
185
+ name: 'artifacts/summarize',
186
+ description: 'Get compact summaries for OpenAPI and Navigation artifacts.',
187
+ inputSchema: { type: 'object', properties: {} }
188
+ },
147
189
  {
148
190
  name: 'list_bundles',
149
191
  description: 'List all project bundles.',
@@ -106,6 +106,11 @@ export default class SemanticSplitter {
106
106
  if (!existsSync(fullPath))
107
107
  return [];
108
108
  const content = readFileSync(fullPath, 'utf8');
109
+ // Skip files larger than 200KB — tree-sitter and embeddings can't handle them well
110
+ if (content.length > 200_000) {
111
+ console.warn(`Skipping ${relativePath}: file too large (${Math.round(content.length / 1024)}KB)`);
112
+ return [];
113
+ }
109
114
  const parser = this.getParser(relativePath);
110
115
  const tree = parser.parse(content);
111
116
  const root = tree.rootNode;
@@ -394,7 +399,10 @@ export default class SemanticSplitter {
394
399
  if (!node)
395
400
  continue;
396
401
  if (node.type === 'table' || node.type === 'table_array_element' || node.type === 'pair') {
397
- const keyNode = node.childForFieldName('name') || node.childForFieldName('key') || node.namedChild(0);
402
+ // Legacy parser uses namedChild instead of childForFieldName
403
+ const keyNode = node.childForFieldName?.('name')
404
+ || node.childForFieldName?.('key')
405
+ || node.namedChild(0);
398
406
  const name = keyNode ? content.slice(keyNode.startIndex, keyNode.endIndex) : node.type;
399
407
  const structure = this.mapStructureNode(name, node, content, filePath);
400
408
  if (structure.code.length >= this.options.minStructureSize) {
@@ -9,19 +9,29 @@ export default class SimpleVectorStore {
9
9
  modelName;
10
10
  pipe;
11
11
  initialized;
12
+ isMcp;
12
13
  constructor(databaseManager, options = {}) {
13
14
  this.db = databaseManager;
14
15
  this.modelName = options.modelName || 'Xenova/all-MiniLM-L6-v2';
15
16
  this.pipe = null;
16
17
  this.initialized = false;
18
+ this.isMcp = options.isMcp || false;
19
+ }
20
+ log(message) {
21
+ if (this.isMcp) {
22
+ process.stderr.write(message + '\n');
23
+ }
24
+ else {
25
+ console.log(message);
26
+ }
17
27
  }
18
28
  async init() {
19
29
  if (this.initialized)
20
30
  return;
21
- console.log(`šŸ¤– Initializing local RAG engine (${this.modelName})...`);
31
+ this.log(`šŸ¤– Initializing local RAG engine (${this.modelName})...`);
22
32
  this.pipe = await pipeline('feature-extraction', this.modelName);
23
33
  this.initialized = true;
24
- console.log('āœ… Local RAG engine ready');
34
+ this.log('āœ… Local RAG engine ready');
25
35
  }
26
36
  async generateEmbedding(text) {
27
37
  await this.init();
@@ -37,8 +47,9 @@ export default class SimpleVectorStore {
37
47
  const existing = this.db.getEmbedding(chunkId);
38
48
  if (existing)
39
49
  return existing;
40
- // Generate new embedding
41
- const textToEmbed = `${chunk.name} ${chunk.purpose} ${chunk.code}`;
50
+ // Generate new embedding — truncate to 8KB to stay within model limits
51
+ const rawText = `${chunk.name} ${chunk.purpose} ${chunk.code}`;
52
+ const textToEmbed = rawText.length > 8192 ? rawText.slice(0, 8192) : rawText;
42
53
  const embedding = await this.generateEmbedding(textToEmbed);
43
54
  // Save to SQLite
44
55
  this.db.saveEmbedding(chunkId, embedding, this.modelName);