@hanhnd/agent-kit 1.0.34 → 1.0.36

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 (41) hide show
  1. package/dist/cli/commands/cli-digest-pending.test.js +19 -10
  2. package/dist/cli/commands/memory.js +44 -4
  3. package/dist/cli.js +348 -179
  4. package/dist/core/config/index.d.ts +3 -0
  5. package/dist/core/config/index.js +20 -0
  6. package/dist/entrypoints/server.js +1 -3
  7. package/dist/mcp/memory.d.ts +2 -1
  8. package/dist/mcp/memory.js +45 -18
  9. package/dist/server.js +294 -200
  10. package/dist/services/digest/constants.d.ts +3 -3
  11. package/dist/services/digest/constants.js +3 -3
  12. package/dist/services/digest/digest-background.test.js +19 -8
  13. package/dist/services/digest/digest-init.test.js +13 -2
  14. package/dist/services/digest/digest-pending.test.js +13 -2
  15. package/dist/services/digest/digest-processor.test.js +52 -0
  16. package/dist/services/digest/files.js +3 -3
  17. package/dist/services/digest/model-registry.js +2 -2
  18. package/dist/services/digest/processor.js +43 -13
  19. package/dist/services/digest/providers/llama-local.js +17 -28
  20. package/dist/services/digest/types.d.ts +3 -1
  21. package/dist/services/digest/types.js +1 -1
  22. package/dist/services/memory/chunker.d.ts +5 -2
  23. package/dist/services/memory/chunker.js +3 -1
  24. package/dist/services/memory/chunker.test.js +21 -11
  25. package/dist/services/memory/constants.js +1 -1
  26. package/dist/services/memory/embedder.js +1 -1
  27. package/dist/services/memory/index.d.ts +1 -1
  28. package/dist/services/memory/indexer.d.ts +2 -1
  29. package/dist/services/memory/indexer.js +24 -8
  30. package/dist/services/memory/indexer.test.js +38 -2
  31. package/dist/services/memory/memory.test.js +172 -15
  32. package/dist/services/memory/store.d.ts +9 -3
  33. package/dist/services/memory/store.js +132 -98
  34. package/dist/services/memory/store.test.js +239 -14
  35. package/dist/services/memory/types.d.ts +9 -1
  36. package/dist/services/memory/types.js +2 -1
  37. package/dist/utils/paths.d.ts +2 -0
  38. package/dist/utils/paths.js +2 -1
  39. package/dist/utils/utils.d.ts +1 -0
  40. package/dist/utils/utils.js +3 -3
  41. package/package.json +3 -5
@@ -1,7 +1,9 @@
1
- import Database from 'better-sqlite3';
1
+ import Database from 'libsql';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { eng, removeStopwords } from 'stopword';
5
+ export const SCHEMA_VERSION = 3;
6
+ const LibsqlDatabase = Database;
5
7
  export class StoreError extends Error {
6
8
  constructor(message, cause) {
7
9
  super(message);
@@ -13,7 +15,6 @@ export class StoreError extends Error {
13
15
  export class MemoryStore {
14
16
  config;
15
17
  db;
16
- _vecAvailable = false;
17
18
  constructor(dbPath, config) {
18
19
  this.config = config;
19
20
  fs.mkdirSync(path.dirname(dbPath), { recursive: true });
@@ -21,10 +22,9 @@ export class MemoryStore {
21
22
  }
22
23
  openDatabase(dbPath) {
23
24
  try {
24
- const db = new Database(dbPath);
25
+ const db = new LibsqlDatabase(dbPath);
25
26
  db.pragma('journal_mode = WAL');
26
27
  db.pragma('busy_timeout = 5000');
27
- this.loadVecExtension(db);
28
28
  this.createSchema(db);
29
29
  return db;
30
30
  }
@@ -36,97 +36,107 @@ export class MemoryStore {
36
36
  catch {
37
37
  // ignore
38
38
  }
39
- const db = new Database(dbPath);
40
- db.pragma('journal_mode = WAL');
41
- db.pragma('busy_timeout = 5000');
42
- this.loadVecExtension(db);
43
- this.createSchema(db);
44
- return db;
39
+ try {
40
+ const db = new LibsqlDatabase(dbPath);
41
+ db.pragma('journal_mode = WAL');
42
+ db.pragma('busy_timeout = 5000');
43
+ this.createSchema(db);
44
+ return db;
45
+ }
46
+ catch (retryErr) {
47
+ throw new StoreError('database open failed', retryErr);
48
+ }
45
49
  }
46
50
  }
47
- loadVecExtension(db) {
51
+ createSchema(db) {
48
52
  try {
49
- // eslint-disable-next-line @typescript-eslint/no-require-imports
50
- const sqliteVec = require('sqlite-vec');
51
- sqliteVec.load(db);
52
- this._vecAvailable = true;
53
+ const current = this.readUserVersion(db);
54
+ if (current < SCHEMA_VERSION) {
55
+ db.exec(`
56
+ DROP TRIGGER IF EXISTS memory_chunks_ai;
57
+ DROP TRIGGER IF EXISTS memory_chunks_ad;
58
+ DROP TABLE IF EXISTS memory_fts;
59
+ DROP TABLE IF EXISTS memory_vec;
60
+ DROP INDEX IF EXISTS idx_memory_chunks_embedding;
61
+ DROP TABLE IF EXISTS memory_chunks;
62
+ `);
63
+ db.pragma(`user_version = ${SCHEMA_VERSION}`);
64
+ }
65
+ db.exec(`
66
+ CREATE TABLE IF NOT EXISTS memory_chunks (
67
+ rowid INTEGER PRIMARY KEY AUTOINCREMENT,
68
+ id TEXT NOT NULL UNIQUE,
69
+ source TEXT NOT NULL,
70
+ source_type TEXT NOT NULL DEFAULT 'wiki',
71
+ heading TEXT NOT NULL DEFAULT '',
72
+ heading_level INTEGER NOT NULL DEFAULT 0,
73
+ content TEXT NOT NULL,
74
+ line_start INTEGER NOT NULL,
75
+ line_end INTEGER NOT NULL,
76
+ indexed_at INTEGER NOT NULL,
77
+ file_mtime_at INTEGER NOT NULL DEFAULT 0,
78
+ embedding F32_BLOB(${this.config.vectorDimension}) NOT NULL
79
+ );
80
+
81
+ CREATE INDEX IF NOT EXISTS idx_memory_chunks_type_mtime
82
+ ON memory_chunks (source_type, file_mtime_at DESC);
83
+
84
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
85
+ source,
86
+ heading,
87
+ content,
88
+ content='memory_chunks',
89
+ content_rowid='rowid'
90
+ );
91
+
92
+ CREATE TRIGGER IF NOT EXISTS memory_chunks_ai
93
+ AFTER INSERT ON memory_chunks BEGIN
94
+ INSERT INTO memory_fts(rowid, source, heading, content)
95
+ VALUES (new.rowid, new.source, new.heading, new.content);
96
+ END;
97
+
98
+ CREATE TRIGGER IF NOT EXISTS memory_chunks_ad
99
+ AFTER DELETE ON memory_chunks BEGIN
100
+ INSERT INTO memory_fts(memory_fts, rowid, source, heading, content)
101
+ VALUES ('delete', old.rowid, old.source, old.heading, old.content);
102
+ END;
103
+
104
+ CREATE INDEX IF NOT EXISTS idx_memory_chunks_embedding
105
+ ON memory_chunks (libsql_vector_idx(embedding));
106
+ `);
53
107
  }
54
108
  catch (err) {
55
- console.warn('[memory-store] sqlite-vec unavailable, degrading to FTS5-only:', err);
56
- this._vecAvailable = false;
109
+ throw new StoreError('schema migration failed', err);
57
110
  }
58
111
  }
59
- createSchema(db) {
60
- db.exec(`
61
- CREATE TABLE IF NOT EXISTS memory_chunks (
62
- rowid INTEGER PRIMARY KEY AUTOINCREMENT,
63
- id TEXT NOT NULL UNIQUE,
64
- source TEXT NOT NULL,
65
- heading TEXT NOT NULL DEFAULT '',
66
- heading_level INTEGER NOT NULL DEFAULT 0,
67
- content TEXT NOT NULL,
68
- line_start INTEGER NOT NULL,
69
- line_end INTEGER NOT NULL,
70
- indexed_at INTEGER NOT NULL
71
- );
72
-
73
- CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
74
- source,
75
- heading,
76
- content,
77
- content='memory_chunks',
78
- content_rowid='rowid'
79
- );
80
-
81
- CREATE TRIGGER IF NOT EXISTS memory_chunks_ai
82
- AFTER INSERT ON memory_chunks BEGIN
83
- INSERT INTO memory_fts(rowid, source, heading, content)
84
- VALUES (new.rowid, new.source, new.heading, new.content);
85
- END;
86
-
87
- CREATE TRIGGER IF NOT EXISTS memory_chunks_ad
88
- AFTER DELETE ON memory_chunks BEGIN
89
- INSERT INTO memory_fts(memory_fts, rowid, source, heading, content)
90
- VALUES ('delete', old.rowid, old.source, old.heading, old.content);
91
- END;
92
- `);
93
- if (this._vecAvailable) {
94
- try {
95
- db.exec(`
96
- CREATE VIRTUAL TABLE IF NOT EXISTS memory_vec USING vec0(
97
- embedding FLOAT[${this.config.vectorDimension}]
98
- );
99
- `);
100
- }
101
- catch (err) {
102
- console.warn('[memory-store] Failed to create memory_vec, disabling vector search:', err);
103
- this._vecAvailable = false;
104
- }
112
+ readUserVersion(db) {
113
+ const value = db.pragma('user_version', { simple: true });
114
+ if (typeof value === 'number')
115
+ return value;
116
+ if (Array.isArray(value)) {
117
+ const [row] = value;
118
+ return typeof row?.user_version === 'number' ? row.user_version : 0;
119
+ }
120
+ if (value && typeof value === 'object' && 'user_version' in value) {
121
+ const row = value;
122
+ return typeof row.user_version === 'number' ? row.user_version : 0;
105
123
  }
124
+ return 0;
106
125
  }
107
126
  upsert(chunks, embeddings) {
108
127
  const insertChunk = this.db.prepare(`
109
128
  INSERT OR REPLACE INTO memory_chunks
110
- (id, source, heading, heading_level, content, line_start, line_end, indexed_at)
111
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
129
+ (id, source, source_type, heading, heading_level, content, line_start, line_end, indexed_at, file_mtime_at, embedding)
130
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?))
112
131
  `);
113
- let insertVec = null;
114
- if (this._vecAvailable) {
115
- insertVec = this.db.prepare(`INSERT OR REPLACE INTO memory_vec (rowid, embedding) VALUES (?, ?)`);
116
- }
117
132
  const upsertAll = this.db.transaction(() => {
118
133
  for (let i = 0; i < chunks.length; i++) {
119
134
  const chunk = chunks[i];
120
135
  try {
136
+ const vectorBinding = this.toVectorBinding(embeddings[i]);
121
137
  // Delete existing row first to get correct rowid
122
138
  this.db.prepare(`DELETE FROM memory_chunks WHERE id = ?`).run(chunk.id);
123
- const info = insertChunk.run(chunk.id, chunk.source, chunk.heading, chunk.headingLevel, chunk.content, chunk.lineStart, chunk.lineEnd, Date.now());
124
- if (insertVec && embeddings[i] && info.lastInsertRowid) {
125
- const rowid = Number(info.lastInsertRowid);
126
- const emb = embeddings[i];
127
- const buf = Buffer.from(emb.buffer, emb.byteOffset, emb.byteLength);
128
- insertVec.run(rowid, buf);
129
- }
139
+ insertChunk.run(chunk.id, chunk.source, chunk.sourceType, chunk.heading, chunk.headingLevel, chunk.content, chunk.lineStart, chunk.lineEnd, Date.now(), chunk.fileMtimeAt, vectorBinding);
130
140
  }
131
141
  catch (err) {
132
142
  console.warn(`[memory-store] Failed to upsert chunk ${chunk.id}:`, err);
@@ -140,6 +150,25 @@ export class MemoryStore {
140
150
  throw new StoreError('Full batch upsert failed', err);
141
151
  }
142
152
  }
153
+ toVectorBinding(embedding) {
154
+ if (!embedding)
155
+ throw new Error('missing embedding');
156
+ if (!(embedding instanceof Float32Array))
157
+ throw new Error('missing embedding');
158
+ if (embedding.length !== this.config.vectorDimension)
159
+ throw new Error('embedding dimension mismatch');
160
+ const values = Array.from(embedding, (value) => {
161
+ if (!Number.isFinite(value))
162
+ throw new Error('embedding contains non-finite value');
163
+ return value;
164
+ });
165
+ return JSON.stringify(values);
166
+ }
167
+ normalizeVectorDistance(distance) {
168
+ if (distance <= 0)
169
+ return 1;
170
+ return Math.max(0, Math.min(1, 1 - distance));
171
+ }
143
172
  hashesBySource(source) {
144
173
  const rows = this.db.prepare(`SELECT id FROM memory_chunks WHERE source = ?`).all(source);
145
174
  return new Set(rows.map((r) => r.id));
@@ -148,13 +177,23 @@ export class MemoryStore {
148
177
  const rows = this.db.prepare(`SELECT DISTINCT source FROM memory_chunks`).all();
149
178
  return rows.map((r) => r.source);
150
179
  }
180
+ getRecentSources(options) {
181
+ const limit = Math.min(Math.max(1, options.limit || 5), 50);
182
+ const whereClause = options.sourceType ? ' WHERE source_type = ?' : '';
183
+ const params = options.sourceType ? [options.sourceType, limit] : [limit];
184
+ try {
185
+ const rows = this.db
186
+ .prepare(`SELECT source, MAX(file_mtime_at) AS t FROM memory_chunks${whereClause} GROUP BY source ORDER BY t DESC LIMIT ?`)
187
+ .all(...params);
188
+ return rows.map((row) => ({ source: row.source, fileMtimeAt: row.t }));
189
+ }
190
+ catch (err) {
191
+ console.warn('[memory-store] Recent source lookup failed:', err);
192
+ return [];
193
+ }
194
+ }
151
195
  deleteBySource(source) {
152
- const rows = this.db.prepare(`SELECT rowid FROM memory_chunks WHERE source = ?`).all(source);
153
196
  const deleteAll = this.db.transaction(() => {
154
- if (this._vecAvailable && rows.length > 0) {
155
- const placeholders = rows.map(() => '?').join(', ');
156
- this.db.prepare(`DELETE FROM memory_vec WHERE rowid IN (${placeholders})`).run(...rows.map((r) => r.rowid));
157
- }
158
197
  this.db.prepare(`DELETE FROM memory_chunks WHERE source = ?`).run(source);
159
198
  });
160
199
  deleteAll();
@@ -163,31 +202,24 @@ export class MemoryStore {
163
202
  if (ids.length === 0)
164
203
  return;
165
204
  const placeholders = ids.map(() => '?').join(', ');
166
- const rows = this.db.prepare(`SELECT rowid FROM memory_chunks WHERE id IN (${placeholders})`).all(...ids);
167
205
  const deleteAll = this.db.transaction(() => {
168
- if (this._vecAvailable && rows.length > 0) {
169
- const rPlaceholders = rows.map(() => '?').join(', ');
170
- this.db.prepare(`DELETE FROM memory_vec WHERE rowid IN (${rPlaceholders})`).run(...rows.map((r) => r.rowid));
171
- }
172
206
  this.db.prepare(`DELETE FROM memory_chunks WHERE id IN (${placeholders})`).run(...ids);
173
207
  });
174
208
  deleteAll();
175
209
  }
176
210
  searchDense(embedding, limit) {
177
- if (!this._vecAvailable)
178
- return [];
179
211
  try {
180
- const buf = Buffer.from(embedding.buffer);
212
+ const queryVector = this.toVectorBinding(embedding);
181
213
  const rows = this.db
182
- .prepare(`SELECT mc.id, mv.distance
183
- FROM memory_vec mv
184
- JOIN memory_chunks mc ON mc.rowid = mv.rowid
185
- WHERE mv.embedding MATCH ? AND k = ?
186
- ORDER BY mv.distance`)
187
- .all(buf, limit);
214
+ .prepare(`SELECT mc.id, vector_distance_cos(mc.embedding, vector32(?)) AS distance
215
+ FROM vector_top_k('idx_memory_chunks_embedding', vector32(?), ?) AS vector_matches
216
+ JOIN memory_chunks mc ON mc.rowid = vector_matches.id
217
+ ORDER BY distance
218
+ LIMIT ?`)
219
+ .all(queryVector, queryVector, limit, limit);
188
220
  return rows.map((r) => ({
189
221
  id: r.id,
190
- score: Math.max(0, 1 - r.distance),
222
+ score: this.normalizeVectorDistance(r.distance),
191
223
  }));
192
224
  }
193
225
  catch (err) {
@@ -239,21 +271,23 @@ export class MemoryStore {
239
271
  return [];
240
272
  const placeholders = ids.map(() => '?').join(', ');
241
273
  const rows = this.db
242
- .prepare(`SELECT id, source, heading, heading_level, content, line_start, line_end
274
+ .prepare(`SELECT id, source, source_type, heading, heading_level, content, line_start, line_end, file_mtime_at
243
275
  FROM memory_chunks WHERE id IN (${placeholders})`)
244
276
  .all(...ids);
245
277
  return rows.map((r) => ({
246
278
  id: r.id,
247
279
  source: r.source,
280
+ sourceType: r.source_type,
248
281
  heading: r.heading,
249
282
  headingLevel: r.heading_level,
250
283
  content: r.content,
251
284
  lineStart: r.line_start,
252
285
  lineEnd: r.line_end,
286
+ fileMtimeAt: r.file_mtime_at,
253
287
  }));
254
288
  }
255
289
  get vecAvailable() {
256
- return this._vecAvailable;
290
+ return true;
257
291
  }
258
292
  close() {
259
293
  this.db.close();