@hanhnd/agent-kit 1.0.35 → 1.0.37

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.
@@ -1,8 +1,8 @@
1
+ import Database from 'libsql';
1
2
  import * as assert from 'node:assert/strict';
2
3
  import * as fs from 'node:fs';
3
4
  import * as os from 'node:os';
4
5
  import * as path from 'node:path';
5
- import Database from 'better-sqlite3';
6
6
  import { after, before, describe, test } from 'node:test';
7
7
  import { MemoryStore, SCHEMA_VERSION } from './store.js';
8
8
  import { EmbeddingModelName } from './types.js';
@@ -29,6 +29,25 @@ function makeChunk(overrides = {}) {
29
29
  ...overrides,
30
30
  };
31
31
  }
32
+ function makeEmbedding(activeIndex, dimension = TEST_CONFIG.vectorDimension) {
33
+ const embedding = new Float32Array(dimension);
34
+ embedding[activeIndex] = 1;
35
+ return embedding;
36
+ }
37
+ function readUserVersion(db) {
38
+ const value = db.pragma('user_version', { simple: true });
39
+ if (typeof value === 'number')
40
+ return value;
41
+ if (Array.isArray(value)) {
42
+ const [row] = value;
43
+ return typeof row?.user_version === 'number' ? row.user_version : 0;
44
+ }
45
+ if (value && typeof value === 'object' && 'user_version' in value) {
46
+ const row = value;
47
+ return typeof row.user_version === 'number' ? row.user_version : 0;
48
+ }
49
+ return 0;
50
+ }
32
51
  describe('MemoryStore', () => {
33
52
  let tmpDir;
34
53
  let dbPath;
@@ -42,8 +61,50 @@ describe('MemoryStore', () => {
42
61
  store.close();
43
62
  fs.rmSync(tmpDir, { recursive: true, force: true });
44
63
  });
45
- test('vecAvailable is a boolean', () => {
46
- assert.equal(typeof store.vecAvailable, 'boolean');
64
+ test('libsql supports required local driver and vector features', () => {
65
+ const libsqlDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memory-libsql-compat-'));
66
+ const libsqlDb = new Database(path.join(libsqlDir, 'index.db'));
67
+ try {
68
+ assert.equal(readUserVersion(libsqlDb), 0);
69
+ libsqlDb.pragma('journal_mode = WAL');
70
+ libsqlDb.pragma('busy_timeout = 5000');
71
+ libsqlDb.pragma(`user_version = ${SCHEMA_VERSION}`);
72
+ assert.equal(readUserVersion(libsqlDb), SCHEMA_VERSION);
73
+ libsqlDb.exec(`
74
+ CREATE TABLE vector_items (
75
+ rowid INTEGER PRIMARY KEY AUTOINCREMENT,
76
+ name TEXT NOT NULL,
77
+ embedding F32_BLOB(3) NOT NULL
78
+ );
79
+ CREATE INDEX idx_vector_items_embedding
80
+ ON vector_items (libsql_vector_idx(embedding));
81
+ `);
82
+ const insert = libsqlDb.prepare(`INSERT INTO vector_items (name, embedding) VALUES (?, vector32(?))`);
83
+ const info = insert.run('alpha', '[1,0,0]');
84
+ assert.equal(info.lastInsertRowid, 1);
85
+ insert.run('beta', '[0,1,0]');
86
+ const insertGamma = libsqlDb.transaction(() => insert.run('gamma', '[0,0,1]'));
87
+ insertGamma();
88
+ const distance = libsqlDb
89
+ .prepare(`SELECT vector_distance_cos(embedding, vector32(?)) AS distance FROM vector_items WHERE name = ?`)
90
+ .get('[1,0,0]', 'alpha');
91
+ assert.equal(distance.distance, 0);
92
+ const rows = libsqlDb
93
+ .prepare(`SELECT vector_items.name
94
+ FROM vector_top_k('idx_vector_items_embedding', vector32(?), ?) AS vector_matches
95
+ JOIN vector_items ON vector_items.rowid = vector_matches.id
96
+ ORDER BY vector_distance_cos(vector_items.embedding, vector32(?))`)
97
+ .all('[1,0,0]', 2, '[1,0,0]');
98
+ assert.equal(rows[0].name, 'alpha');
99
+ assert.equal(rows.length, 2);
100
+ }
101
+ finally {
102
+ libsqlDb.close();
103
+ fs.rmSync(libsqlDir, { recursive: true, force: true });
104
+ }
105
+ });
106
+ test('vecAvailable is true after schema creation', () => {
107
+ assert.equal(store.vecAvailable, true);
47
108
  });
48
109
  test('hashesBySource returns empty set for unknown source', () => {
49
110
  const hashes = store.hashesBySource('nonexistent.md');
@@ -52,7 +113,7 @@ describe('MemoryStore', () => {
52
113
  });
53
114
  test('upsert stores chunks and hashesBySource returns their ids', () => {
54
115
  const chunk = makeChunk({ id: 'upsert-test-0001', source: 'upsert.md', sourceType: 'entity', fileMtimeAt: 2000 });
55
- const embedding = new Float32Array(384).fill(0.1);
116
+ const embedding = makeEmbedding(0);
56
117
  store.upsert([chunk], [embedding]);
57
118
  const hashes = store.hashesBySource('upsert.md');
58
119
  assert.ok(hashes.has('upsert-test-0001'));
@@ -71,7 +132,7 @@ describe('MemoryStore', () => {
71
132
  source: 'deleteme.md',
72
133
  content: 'delete source chunk 2',
73
134
  });
74
- store.upsert([c1, c2], [new Float32Array(384), new Float32Array(384)]);
135
+ store.upsert([c1, c2], [makeEmbedding(1), makeEmbedding(2)]);
75
136
  store.deleteBySource('deleteme.md');
76
137
  const hashes = store.hashesBySource('deleteme.md');
77
138
  assert.equal(hashes.size, 0);
@@ -82,7 +143,7 @@ describe('MemoryStore', () => {
82
143
  source: 'bm25.md',
83
144
  content: 'uniqueKeywordXYZ for BM25 testing',
84
145
  });
85
- store.upsert([chunk], [new Float32Array(384)]);
146
+ store.upsert([chunk], [makeEmbedding(3)]);
86
147
  const results = store.searchBm25('uniqueKeywordXYZ', 5);
87
148
  assert.ok(results.length > 0, 'Expected at least one BM25 result');
88
149
  assert.ok(results.some((r) => r.id === 'bm25-search-0001'), `Expected chunk id in results, got: ${results.map((r) => r.id).join(', ')}`);
@@ -101,7 +162,7 @@ describe('MemoryStore', () => {
101
162
  heading: 'Open Questions',
102
163
  content: 'How should git manage worktree lifecycle decisions?',
103
164
  });
104
- store.upsert([preference, unrelated], [new Float32Array(384), new Float32Array(384)]);
165
+ store.upsert([preference, unrelated], [makeEmbedding(4), makeEmbedding(5)]);
105
166
  const results = store.searchBm25('personal likes and preferences of the user', 5);
106
167
  assert.ok(results.length > 0, 'Expected preference query to return results');
107
168
  assert.equal(results[0].id, 'preference-search-0001');
@@ -122,7 +183,7 @@ describe('MemoryStore', () => {
122
183
  lineEnd: 20,
123
184
  fileMtimeAt: 3000,
124
185
  });
125
- store.upsert([chunk], [new Float32Array(384)]);
186
+ store.upsert([chunk], [makeEmbedding(6)]);
126
187
  const results = store.getChunksByIds(['get-by-ids-0001']);
127
188
  assert.equal(results.length, 1);
128
189
  const [r] = results;
@@ -137,7 +198,7 @@ describe('MemoryStore', () => {
137
198
  });
138
199
  test('getChunksByIds returns only found rows for mixed ids', () => {
139
200
  const chunk = makeChunk({ id: 'partial-found-0001', source: 'partial.md', content: 'partial' });
140
- store.upsert([chunk], [new Float32Array(384)]);
201
+ store.upsert([chunk], [makeEmbedding(7)]);
141
202
  const results = store.getChunksByIds(['partial-found-0001', 'does-not-exist-999']);
142
203
  assert.equal(results.length, 1);
143
204
  assert.equal(results[0].id, 'partial-found-0001');
@@ -148,14 +209,14 @@ describe('MemoryStore', () => {
148
209
  source: 'indexed-source.md',
149
210
  content: 'indexed',
150
211
  });
151
- store.upsert([chunk], [new Float32Array(384)]);
212
+ store.upsert([chunk], [makeEmbedding(8)]);
152
213
  const sources = store.indexedSources();
153
214
  assert.ok(sources.includes('indexed-source.md'), `Expected 'indexed-source.md' in ${sources.join(', ')}`);
154
215
  });
155
216
  test('deleteByIds removes specific chunks', () => {
156
217
  const c1 = makeChunk({ id: 'del-ids-0001', source: 'del-ids.md', content: 'delete by id 1' });
157
218
  const c2 = makeChunk({ id: 'del-ids-0002', source: 'del-ids.md', content: 'delete by id 2' });
158
- store.upsert([c1, c2], [new Float32Array(384), new Float32Array(384)]);
219
+ store.upsert([c1, c2], [makeEmbedding(9), makeEmbedding(10)]);
159
220
  store.deleteByIds(['del-ids-0001']);
160
221
  const hashes = store.hashesBySource('del-ids.md');
161
222
  assert.ok(!hashes.has('del-ids-0001'), 'Deleted chunk id must be gone');
@@ -168,7 +229,7 @@ describe('MemoryStore', () => {
168
229
  source: 'fts5.md',
169
230
  content: 'degradedModeTest keyword',
170
231
  });
171
- store.upsert([chunk], [new Float32Array(384)]);
232
+ store.upsert([chunk], [makeEmbedding(11)]);
172
233
  const results = store.searchBm25('degradedModeTest', 5);
173
234
  assert.ok(results.length > 0, 'FTS5 search must work regardless of vecAvailable');
174
235
  });
@@ -205,7 +266,7 @@ describe('MemoryStore', () => {
205
266
  fileMtimeAt: 500,
206
267
  content: 'entity',
207
268
  }),
208
- ], [new Float32Array(384), new Float32Array(384), new Float32Array(384), new Float32Array(384)]);
269
+ ], [makeEmbedding(12), makeEmbedding(13), makeEmbedding(14), makeEmbedding(15)]);
209
270
  const allRows = recentStore.getRecentSources({ limit: 10 });
210
271
  assert.deepEqual(allRows.map((row) => row.source), [
211
272
  'compiled/entities/entity.md',
@@ -221,7 +282,41 @@ describe('MemoryStore', () => {
221
282
  fs.rmSync(recentDir, { recursive: true, force: true });
222
283
  }
223
284
  });
224
- test('schema migration recreates versioned schema with new columns and recency index', () => {
285
+ test('searchDense returns nearest vector with bounded scores', () => {
286
+ const denseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memory-dense-store-'));
287
+ const denseStore = new MemoryStore(path.join(denseDir, 'index.db'), { ...TEST_CONFIG, wikiDir: denseDir });
288
+ try {
289
+ denseStore.upsert([
290
+ makeChunk({ id: 'dense-alpha', source: 'dense.md', content: 'alpha vector' }),
291
+ makeChunk({ id: 'dense-beta', source: 'dense.md', content: 'beta vector' }),
292
+ makeChunk({ id: 'dense-gamma', source: 'dense.md', content: 'gamma vector' }),
293
+ ], [makeEmbedding(21), makeEmbedding(22), makeEmbedding(23)]);
294
+ const results = denseStore.searchDense(makeEmbedding(22), 3);
295
+ assert.ok(results.length > 0, 'Expected dense search results');
296
+ assert.equal(results[0].id, 'dense-beta');
297
+ assert.ok(results.every((result) => result.score >= 0 && result.score <= 1));
298
+ }
299
+ finally {
300
+ denseStore.close();
301
+ fs.rmSync(denseDir, { recursive: true, force: true });
302
+ }
303
+ });
304
+ test('searchDense returns empty array for invalid query embedding', () => {
305
+ assert.deepEqual(store.searchDense(new Float32Array(3), 5), []);
306
+ });
307
+ test('upsert skips invalid embeddings while persisting valid chunks', () => {
308
+ const validChunk = makeChunk({ id: 'valid-embedding-0001', source: 'embedding-validation.md' });
309
+ const invalidChunk = makeChunk({ id: 'invalid-embedding-0001', source: 'embedding-validation.md' });
310
+ const nonFiniteChunk = makeChunk({ id: 'non-finite-embedding-0001', source: 'embedding-validation.md' });
311
+ const nonFiniteEmbedding = makeEmbedding(24);
312
+ nonFiniteEmbedding[25] = Number.NaN;
313
+ store.upsert([validChunk, invalidChunk, nonFiniteChunk], [makeEmbedding(24), new Float32Array(3), nonFiniteEmbedding]);
314
+ const hashes = store.hashesBySource('embedding-validation.md');
315
+ assert.ok(hashes.has('valid-embedding-0001'));
316
+ assert.ok(!hashes.has('invalid-embedding-0001'));
317
+ assert.ok(!hashes.has('non-finite-embedding-0001'));
318
+ });
319
+ test('schema migration recreates versioned schema with new columns, vector index, and working BM25', () => {
225
320
  const migrationDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memory-migration-'));
226
321
  const migrationDbPath = path.join(migrationDir, 'index.db');
227
322
  const db = new Database(migrationDbPath);
@@ -264,22 +359,75 @@ describe('MemoryStore', () => {
264
359
  try {
265
360
  const verifyDb = new Database(migrationDbPath, { readonly: true });
266
361
  try {
267
- assert.equal(verifyDb.pragma('user_version', { simple: true }), SCHEMA_VERSION);
362
+ assert.equal(readUserVersion(verifyDb), SCHEMA_VERSION);
268
363
  const columns = verifyDb.prepare(`PRAGMA table_info(memory_chunks)`).all();
269
364
  assert.ok(columns.some((column) => column.name === 'source_type'));
270
365
  assert.ok(columns.some((column) => column.name === 'file_mtime_at'));
366
+ assert.ok(columns.some((column) => column.name === 'embedding'));
271
367
  const indexes = verifyDb.prepare(`PRAGMA index_list(memory_chunks)`).all();
272
368
  assert.ok(indexes.some((index) => index.name === 'idx_memory_chunks_type_mtime'));
369
+ assert.ok(indexes.some((index) => index.name === 'idx_memory_chunks_embedding'));
273
370
  const rowCount = verifyDb.prepare(`SELECT COUNT(*) AS count FROM memory_chunks`).get();
274
371
  assert.equal(rowCount.count, 0);
275
372
  }
276
373
  finally {
277
374
  verifyDb.close();
278
375
  }
376
+ const migratedChunk = makeChunk({
377
+ id: 'migrated-bm25-0001',
378
+ source: 'migrated.md',
379
+ content: 'postMigrationKeyword is searchable after migration',
380
+ });
381
+ migratedStore.upsert([migratedChunk], [makeEmbedding(16)]);
382
+ const results = migratedStore.searchBm25('postMigrationKeyword', 5);
383
+ assert.ok(results.some((result) => result.id === 'migrated-bm25-0001'));
279
384
  }
280
385
  finally {
281
386
  migratedStore.close();
282
387
  fs.rmSync(migrationDir, { recursive: true, force: true });
283
388
  }
284
389
  });
390
+ test('recreates schema when existing vector dimension differs from config', () => {
391
+ const dimensionDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memory-dimension-migration-'));
392
+ const dimensionDbPath = path.join(dimensionDir, 'index.db');
393
+ const baseStore = new MemoryStore(dimensionDbPath, { ...TEST_CONFIG, wikiDir: dimensionDir });
394
+ try {
395
+ baseStore.upsert([makeChunk({ id: 'old-dimension-row', source: 'dimension.md' })], [makeEmbedding(17)]);
396
+ }
397
+ finally {
398
+ baseStore.close();
399
+ }
400
+ const largeConfig = {
401
+ ...TEST_CONFIG,
402
+ wikiDir: dimensionDir,
403
+ embeddingModel: EmbeddingModelName.LARGE,
404
+ vectorDimension: 768,
405
+ };
406
+ const largeStore = new MemoryStore(dimensionDbPath, largeConfig);
407
+ try {
408
+ const verifyDb = new Database(dimensionDbPath, { readonly: true });
409
+ try {
410
+ const columns = verifyDb.prepare(`PRAGMA table_info(memory_chunks)`).all();
411
+ const embeddingColumn = columns.find((column) => column.name === 'embedding');
412
+ assert.equal(embeddingColumn?.type, 'F32_BLOB(768)');
413
+ const rowCount = verifyDb.prepare(`SELECT COUNT(*) AS count FROM memory_chunks`).get();
414
+ assert.equal(rowCount.count, 0);
415
+ }
416
+ finally {
417
+ verifyDb.close();
418
+ }
419
+ const chunk = makeChunk({
420
+ id: 'new-dimension-row',
421
+ source: 'dimension.md',
422
+ content: 'large dimension vector can be indexed',
423
+ });
424
+ largeStore.upsert([chunk], [makeEmbedding(18, 768)]);
425
+ const results = largeStore.searchBm25('large dimension vector', 5);
426
+ assert.ok(results.some((result) => result.id === 'new-dimension-row'));
427
+ }
428
+ finally {
429
+ largeStore.close();
430
+ fs.rmSync(dimensionDir, { recursive: true, force: true });
431
+ }
432
+ });
285
433
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanhnd/agent-kit",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
4
4
  "description": "Agent Kit MCP server for software development agent workflows",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "scripts": {
24
24
  "build": "rm -rf dist && tsc && npm run bundle",
25
- "bundle": "esbuild src/entrypoints/server.ts src/entrypoints/cli.ts --bundle --platform=node --format=esm --outdir=dist --external:node:* --external:better-sqlite3 --external:sqlite-vec --external:fastembed --external:node-llama-cpp",
25
+ "bundle": "esbuild src/entrypoints/server.ts src/entrypoints/cli.ts --bundle --platform=node --format=esm --outdir=dist --external:node:* --external:libsql --external:fastembed --external:node-llama-cpp",
26
26
  "dev": "tsc --watch",
27
27
  "prepublishOnly": "npm run build",
28
28
  "test": "tsc && node --test $(find dist -name '*.test.js' | tr '\\n' ' ')"
@@ -30,15 +30,13 @@
30
30
  "dependencies": {
31
31
  "node-llama-cpp": "^3.0.0",
32
32
  "@modelcontextprotocol/sdk": "^1.13.1",
33
- "better-sqlite3": "^11.0.0",
34
33
  "dedent": "^1.5.3",
35
34
  "fastembed": "^1.14.0",
36
- "sqlite-vec": "^0.1.0",
35
+ "libsql": "^0.5.29",
37
36
  "stopword": "^3.1.5",
38
37
  "zod": "^3.24.4"
39
38
  },
40
39
  "devDependencies": {
41
- "@types/better-sqlite3": "^7.6.0",
42
40
  "@types/node": "^22.15.21",
43
41
  "esbuild": "^0.25.4",
44
42
  "typescript": "^5.8.3"