@hivehub/rulebook 4.4.1 → 5.1.1

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 (89) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +68 -8
  3. package/dist/cli/commands.d.ts.map +1 -1
  4. package/dist/cli/commands.js +86 -0
  5. package/dist/cli/commands.js.map +1 -1
  6. package/dist/core/agent-template-engine.d.ts +51 -0
  7. package/dist/core/agent-template-engine.d.ts.map +1 -0
  8. package/dist/core/agent-template-engine.js +285 -0
  9. package/dist/core/agent-template-engine.js.map +1 -0
  10. package/dist/core/complexity-detector.d.ts +36 -0
  11. package/dist/core/complexity-detector.d.ts.map +1 -0
  12. package/dist/core/complexity-detector.js +254 -0
  13. package/dist/core/complexity-detector.js.map +1 -0
  14. package/dist/core/config-manager.d.ts.map +1 -1
  15. package/dist/core/config-manager.js +7 -8
  16. package/dist/core/config-manager.js.map +1 -1
  17. package/dist/core/generator.d.ts +1 -0
  18. package/dist/core/generator.d.ts.map +1 -1
  19. package/dist/core/generator.js +21 -3
  20. package/dist/core/generator.js.map +1 -1
  21. package/dist/core/indexer/background-indexer.d.ts +2 -2
  22. package/dist/core/indexer/background-indexer.d.ts.map +1 -1
  23. package/dist/core/indexer/background-indexer.js +55 -61
  24. package/dist/core/indexer/background-indexer.js.map +1 -1
  25. package/dist/core/rule-engine.d.ts +64 -0
  26. package/dist/core/rule-engine.d.ts.map +1 -0
  27. package/dist/core/rule-engine.js +333 -0
  28. package/dist/core/rule-engine.js.map +1 -0
  29. package/dist/core/task-manager.d.ts +4 -0
  30. package/dist/core/task-manager.d.ts.map +1 -1
  31. package/dist/core/task-manager.js +39 -24
  32. package/dist/core/task-manager.js.map +1 -1
  33. package/dist/index.js +182 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/mcp/rulebook-server.d.ts +6 -0
  36. package/dist/mcp/rulebook-server.d.ts.map +1 -1
  37. package/dist/mcp/rulebook-server.js +394 -49
  38. package/dist/mcp/rulebook-server.js.map +1 -1
  39. package/dist/memory/hnsw-index.d.ts +6 -1
  40. package/dist/memory/hnsw-index.d.ts.map +1 -1
  41. package/dist/memory/hnsw-index.js +133 -18
  42. package/dist/memory/hnsw-index.js.map +1 -1
  43. package/dist/memory/memory-manager.d.ts +2 -0
  44. package/dist/memory/memory-manager.d.ts.map +1 -1
  45. package/dist/memory/memory-manager.js +16 -7
  46. package/dist/memory/memory-manager.js.map +1 -1
  47. package/dist/memory/memory-store.d.ts +15 -3
  48. package/dist/memory/memory-store.d.ts.map +1 -1
  49. package/dist/memory/memory-store.js +327 -274
  50. package/dist/memory/memory-store.js.map +1 -1
  51. package/dist/types.d.ts +17 -0
  52. package/dist/types.d.ts.map +1 -1
  53. package/package.json +8 -3
  54. package/templates/agents/compiler/codegen-debugger.md +34 -0
  55. package/templates/agents/compiler/stdlib-engineer.md +28 -0
  56. package/templates/agents/compiler/test-coverage-guardian.md +31 -0
  57. package/templates/agents/game-engine/cpp-core-expert.md +35 -0
  58. package/templates/agents/game-engine/render-engineer.md +22 -0
  59. package/templates/agents/game-engine/shader-engineer.md +38 -0
  60. package/templates/agents/game-engine/systems-integration.md +43 -0
  61. package/templates/agents/generic/code-reviewer.md +41 -0
  62. package/templates/agents/generic/docs-writer.md +25 -0
  63. package/templates/agents/generic/project-manager.md +36 -0
  64. package/templates/agents/generic/researcher.md +34 -0
  65. package/templates/agents/generic/test-engineer.md +41 -0
  66. package/templates/agents/implementer.md +8 -4
  67. package/templates/agents/mobile/platform-specialist.md +22 -0
  68. package/templates/agents/mobile/ui-engineer.md +22 -0
  69. package/templates/agents/tester.md +7 -4
  70. package/templates/agents/web-app/api-designer.md +22 -0
  71. package/templates/agents/web-app/backend-engineer.md +30 -0
  72. package/templates/agents/web-app/database-engineer.md +22 -0
  73. package/templates/agents/web-app/frontend-engineer.md +29 -0
  74. package/templates/agents/web-app/security-reviewer.md +32 -0
  75. package/templates/core/AGENT_AUTOMATION.md +8 -0
  76. package/templates/core/RULEBOOK.md +12 -0
  77. package/templates/core/TIER1_PROHIBITIONS.md +154 -0
  78. package/templates/core/TOKEN_OPTIMIZATION.md +49 -0
  79. package/templates/git/GIT_WORKFLOW.md +35 -0
  80. package/templates/rules/follow-task-sequence.md +36 -0
  81. package/templates/rules/git-safety.md +29 -0
  82. package/templates/rules/incremental-implementation.md +56 -0
  83. package/templates/rules/incremental-tests.md +29 -0
  84. package/templates/rules/no-deferred.md +31 -0
  85. package/templates/rules/no-shortcuts.md +30 -0
  86. package/templates/rules/research-first.md +30 -0
  87. package/templates/rules/sequential-editing.md +21 -0
  88. package/templates/rules/session-workflow.md +24 -0
  89. package/templates/rules/task-decomposition.md +32 -0
@@ -1,18 +1,22 @@
1
1
  /**
2
- * SQLite Storage Layer using sql.js (WASM)
2
+ * SQLite Storage Layer using better-sqlite3 (native)
3
3
  *
4
4
  * Provides CRUD operations for memories and sessions,
5
5
  * with FTS5 full-text search (BM25 ranking).
6
+ *
7
+ * Replaced sql.js (WASM) in v5.0 to eliminate:
8
+ * - Full DB export() copies on every save (100-500MB allocations)
9
+ * - WASM JIT warmup delay (~300ms on init)
10
+ * - Event loop blocking on synchronous writeFileSync of entire DB
11
+ *
12
+ * better-sqlite3 writes directly to disk via SQLite's WAL journal.
13
+ * No export(), no manual saveToDisk(), no memory copies.
6
14
  */
7
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
15
+ import { existsSync, mkdirSync, statSync, writeFileSync } from 'fs';
8
16
  import { join } from 'path';
9
- // Increase threshold to reduce blocking syncs during heavy write bursts
10
- // (db.export() + writeFileSync blocks the event loop for the full DB size)
11
- const AUTO_SAVE_THRESHOLD = 200;
12
17
  export class MemoryStore {
13
18
  db = null;
14
19
  dbPath;
15
- writeCount = 0;
16
20
  initialized = false;
17
21
  constructor(dbPath) {
18
22
  this.dbPath = dbPath;
@@ -25,223 +29,217 @@ export class MemoryStore {
25
29
  if (!existsSync(dir)) {
26
30
  mkdirSync(dir, { recursive: true });
27
31
  }
28
- // Dynamic import sql.js
29
- const initSqlJs = (await import('sql.js')).default;
30
- const SQL = await initSqlJs();
31
- // Load existing DB or create new
32
- if (existsSync(this.dbPath)) {
33
- const fileData = readFileSync(this.dbPath);
34
- this.db = new SQL.Database(fileData);
32
+ // Try better-sqlite3 (native, fast) first.
33
+ // Falls back to sql.js (WASM, portable) if native addon isn't available
34
+ // (e.g. no C++ build tools on Windows).
35
+ try {
36
+ const Database = (await import('better-sqlite3')).default;
37
+ this.db = new Database(this.dbPath);
38
+ this.db.pragma('journal_mode = WAL');
39
+ this.db.pragma('foreign_keys = ON');
35
40
  }
36
- else {
37
- this.db = new SQL.Database();
41
+ catch {
42
+ // better-sqlite3 not available — fall back to sql.js (WASM)
43
+ console.error('[MemoryStore] better-sqlite3 unavailable, falling back to sql.js (slower but portable)');
44
+ const { default: initSqlJs } = await import('sql.js');
45
+ const SQL = await initSqlJs();
46
+ let rawDb;
47
+ if (existsSync(this.dbPath)) {
48
+ const { readFileSync } = await import('fs');
49
+ rawDb = new SQL.Database(readFileSync(this.dbPath));
50
+ }
51
+ else {
52
+ rawDb = new SQL.Database();
53
+ }
54
+ // Wrap sql.js to match better-sqlite3 API surface used in this file
55
+ this.db = this.wrapSqlJs(rawDb);
38
56
  }
39
57
  this.createSchema();
40
58
  this.initialized = true;
41
- // Force save to disk immediately after initialization to ensure the .db file exists.
42
- // Without this, the DB only persists after AUTO_SAVE_THRESHOLD writes or explicit close(),
43
- // which means sessions that end early never create the file on disk.
59
+ // For sql.js: force initial save so the .db file exists on disk
60
+ // (better-sqlite3 creates the file automatically on open)
44
61
  this.saveToDisk();
45
62
  }
46
63
  createSchema() {
47
64
  if (!this.db)
48
65
  throw new Error('Database not initialized');
49
- this.db.run(`
50
- CREATE TABLE IF NOT EXISTS memories (
51
- id TEXT PRIMARY KEY,
52
- type TEXT NOT NULL,
53
- title TEXT NOT NULL,
54
- content TEXT NOT NULL,
55
- project TEXT NOT NULL DEFAULT '',
56
- tags TEXT NOT NULL DEFAULT '[]',
57
- session_id TEXT,
58
- created_at INTEGER NOT NULL,
59
- updated_at INTEGER NOT NULL,
60
- accessed_at INTEGER NOT NULL
61
- )
66
+ this.db.exec(`
67
+ CREATE TABLE IF NOT EXISTS memories (
68
+ id TEXT PRIMARY KEY,
69
+ type TEXT NOT NULL,
70
+ title TEXT NOT NULL,
71
+ content TEXT NOT NULL,
72
+ project TEXT NOT NULL DEFAULT '',
73
+ tags TEXT NOT NULL DEFAULT '[]',
74
+ session_id TEXT,
75
+ created_at INTEGER NOT NULL,
76
+ updated_at INTEGER NOT NULL,
77
+ accessed_at INTEGER NOT NULL
78
+ )
62
79
  `);
63
- this.db.run(`
64
- CREATE TABLE IF NOT EXISTS sessions (
65
- id TEXT PRIMARY KEY,
66
- project TEXT NOT NULL DEFAULT '',
67
- status TEXT NOT NULL DEFAULT 'active',
68
- started_at INTEGER NOT NULL,
69
- ended_at INTEGER,
70
- summary TEXT,
71
- tool_calls INTEGER NOT NULL DEFAULT 0
72
- )
80
+ this.db.exec(`
81
+ CREATE TABLE IF NOT EXISTS sessions (
82
+ id TEXT PRIMARY KEY,
83
+ project TEXT NOT NULL DEFAULT '',
84
+ status TEXT NOT NULL DEFAULT 'active',
85
+ started_at INTEGER NOT NULL,
86
+ ended_at INTEGER,
87
+ summary TEXT,
88
+ tool_calls INTEGER NOT NULL DEFAULT 0
89
+ )
73
90
  `);
74
91
  // --- Indexer Extensions ---
75
- this.db.run(`
76
- CREATE TABLE IF NOT EXISTS code_nodes (
77
- id TEXT PRIMARY KEY,
78
- type TEXT NOT NULL,
79
- name TEXT NOT NULL,
80
- file_path TEXT NOT NULL,
81
- start_line INTEGER NOT NULL,
82
- end_line INTEGER NOT NULL,
83
- content TEXT NOT NULL,
84
- summary TEXT,
85
- hash TEXT NOT NULL,
86
- updated_at INTEGER NOT NULL
87
- )
92
+ this.db.exec(`
93
+ CREATE TABLE IF NOT EXISTS code_nodes (
94
+ id TEXT PRIMARY KEY,
95
+ type TEXT NOT NULL,
96
+ name TEXT NOT NULL,
97
+ file_path TEXT NOT NULL,
98
+ start_line INTEGER NOT NULL,
99
+ end_line INTEGER NOT NULL,
100
+ content TEXT NOT NULL,
101
+ summary TEXT,
102
+ hash TEXT NOT NULL,
103
+ updated_at INTEGER NOT NULL
104
+ )
88
105
  `);
89
- this.db.run(`
90
- CREATE TABLE IF NOT EXISTS code_edges (
91
- id TEXT PRIMARY KEY,
92
- source_id TEXT NOT NULL,
93
- target_id TEXT NOT NULL,
94
- type TEXT NOT NULL,
95
- weight REAL NOT NULL DEFAULT 1.0,
96
- FOREIGN KEY(source_id) REFERENCES code_nodes(id) ON DELETE CASCADE
97
- )
106
+ this.db.exec(`
107
+ CREATE TABLE IF NOT EXISTS code_edges (
108
+ id TEXT PRIMARY KEY,
109
+ source_id TEXT NOT NULL,
110
+ target_id TEXT NOT NULL,
111
+ type TEXT NOT NULL,
112
+ weight REAL NOT NULL DEFAULT 1.0,
113
+ FOREIGN KEY(source_id) REFERENCES code_nodes(id) ON DELETE CASCADE
114
+ )
98
115
  `);
99
116
  // FTS5 virtual table for BM25 search
100
- // Use external content mode synced with memories table
101
117
  try {
102
- this.db.run(`
103
- CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
104
- title, content, type,
105
- content=memories,
106
- content_rowid=rowid
107
- )
118
+ this.db.exec(`
119
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
120
+ title, content, type,
121
+ content=memories,
122
+ content_rowid=rowid
123
+ )
108
124
  `);
109
125
  // Triggers to sync FTS with memories table
110
- this.db.run(`
111
- CREATE TRIGGER IF NOT EXISTS memory_fts_ai AFTER INSERT ON memories BEGIN
112
- INSERT INTO memory_fts(rowid, title, content, type)
113
- VALUES (new.rowid, new.title, new.content, new.type);
114
- END
126
+ this.db.exec(`
127
+ CREATE TRIGGER IF NOT EXISTS memory_fts_ai AFTER INSERT ON memories BEGIN
128
+ INSERT INTO memory_fts(rowid, title, content, type)
129
+ VALUES (new.rowid, new.title, new.content, new.type);
130
+ END
115
131
  `);
116
- this.db.run(`
117
- CREATE TRIGGER IF NOT EXISTS memory_fts_ad AFTER DELETE ON memories BEGIN
118
- INSERT INTO memory_fts(memory_fts, rowid, title, content, type)
119
- VALUES ('delete', old.rowid, old.title, old.content, old.type);
120
- END
132
+ this.db.exec(`
133
+ CREATE TRIGGER IF NOT EXISTS memory_fts_ad AFTER DELETE ON memories BEGIN
134
+ INSERT INTO memory_fts(memory_fts, rowid, title, content, type)
135
+ VALUES ('delete', old.rowid, old.title, old.content, old.type);
136
+ END
121
137
  `);
122
- this.db.run(`
123
- CREATE TRIGGER IF NOT EXISTS memory_fts_au AFTER UPDATE ON memories BEGIN
124
- INSERT INTO memory_fts(memory_fts, rowid, title, content, type)
125
- VALUES ('delete', old.rowid, old.title, old.content, old.type);
126
- INSERT INTO memory_fts(rowid, title, content, type)
127
- VALUES (new.rowid, new.title, new.content, new.type);
128
- END
138
+ this.db.exec(`
139
+ CREATE TRIGGER IF NOT EXISTS memory_fts_au AFTER UPDATE ON memories BEGIN
140
+ INSERT INTO memory_fts(memory_fts, rowid, title, content, type)
141
+ VALUES ('delete', old.rowid, old.title, old.content, old.type);
142
+ INSERT INTO memory_fts(rowid, title, content, type)
143
+ VALUES (new.rowid, new.title, new.content, new.type);
144
+ END
129
145
  `);
130
146
  }
131
147
  catch {
132
148
  // FTS5 may not be available in some builds; continue without it
133
149
  }
134
150
  // Index for common queries
135
- this.db.run('CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type)');
136
- this.db.run('CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at)');
137
- this.db.run('CREATE INDEX IF NOT EXISTS idx_memories_accessed ON memories(accessed_at)');
138
- this.db.run('CREATE INDEX IF NOT EXISTS idx_memories_session ON memories(session_id)');
139
- this.db.run('CREATE INDEX IF NOT EXISTS idx_code_nodes_type ON code_nodes(type)');
140
- this.db.run('CREATE INDEX IF NOT EXISTS idx_code_nodes_path ON code_nodes(file_path)');
141
- this.db.run('CREATE INDEX IF NOT EXISTS idx_code_edges_source ON code_edges(source_id)');
142
- this.db.run('CREATE INDEX IF NOT EXISTS idx_code_edges_target ON code_edges(target_id)');
143
- }
144
- trackWrite() {
145
- this.writeCount++;
146
- if (this.writeCount >= AUTO_SAVE_THRESHOLD) {
147
- this.saveToDisk();
148
- this.writeCount = 0;
149
- }
151
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type)');
152
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at)');
153
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_memories_accessed ON memories(accessed_at)');
154
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_memories_session ON memories(session_id)');
155
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_code_nodes_type ON code_nodes(type)');
156
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_code_nodes_path ON code_nodes(file_path)');
157
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_code_edges_source ON code_edges(source_id)');
158
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_code_edges_target ON code_edges(target_id)');
150
159
  }
151
160
  // --- Memory CRUD ---
152
161
  saveMemory(memory) {
153
162
  if (!this.db)
154
163
  throw new Error('Database not initialized');
155
- this.db.run(`INSERT OR REPLACE INTO memories (id, type, title, content, project, tags, session_id, created_at, updated_at, accessed_at)
156
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
157
- memory.id,
158
- memory.type,
159
- memory.title,
160
- memory.content,
161
- memory.project,
162
- JSON.stringify(memory.tags),
163
- memory.sessionId ?? null,
164
- memory.createdAt,
165
- memory.updatedAt,
166
- memory.accessedAt,
167
- ]);
168
- this.trackWrite();
164
+ this.db.prepare(`INSERT OR REPLACE INTO memories (id, type, title, content, project, tags, session_id, created_at, updated_at, accessed_at)
165
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(memory.id, memory.type, memory.title, memory.content, memory.project, JSON.stringify(memory.tags), memory.sessionId ?? null, memory.createdAt, memory.updatedAt, memory.accessedAt);
169
166
  }
170
167
  getMemory(id) {
171
168
  if (!this.db)
172
169
  throw new Error('Database not initialized');
173
- const result = this.db.exec(`SELECT id, type, title, content, project, tags, session_id, created_at, updated_at, accessed_at
174
- FROM memories WHERE id = '${id.replace(/'/g, "''")}'`);
175
- if (result.length === 0 || result[0].values.length === 0)
170
+ const row = this.db.prepare(`SELECT id, type, title, content, project, tags, session_id, created_at, updated_at, accessed_at
171
+ FROM memories WHERE id = ?`).get(id);
172
+ if (!row)
176
173
  return null;
177
- const row = result[0].values[0];
178
174
  return this.rowToMemory(row);
179
175
  }
180
176
  deleteMemory(id) {
181
177
  if (!this.db)
182
178
  throw new Error('Database not initialized');
183
- this.db.run(`DELETE FROM memories WHERE id = ?`, [id]);
184
- this.trackWrite();
179
+ this.db.prepare(`DELETE FROM memories WHERE id = ?`).run(id);
185
180
  }
186
181
  listMemories(options) {
187
182
  if (!this.db)
188
183
  throw new Error('Database not initialized');
189
184
  const conditions = [];
190
- if (options?.type)
191
- conditions.push(`type = '${options.type}'`);
192
- if (options?.project)
193
- conditions.push(`project = '${options.project.replace(/'/g, "''")}'`);
185
+ const params = [];
186
+ if (options?.type) {
187
+ conditions.push(`type = ?`);
188
+ params.push(options.type);
189
+ }
190
+ if (options?.project) {
191
+ conditions.push(`project = ?`);
192
+ params.push(options.project);
193
+ }
194
194
  const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
195
- const limit = options?.limit ? `LIMIT ${options.limit}` : 'LIMIT 100';
196
- const offset = options?.offset ? `OFFSET ${options.offset}` : '';
197
- const result = this.db.exec(`SELECT id, type, title, content, project, tags, session_id, created_at, updated_at, accessed_at
198
- FROM memories ${where} ORDER BY created_at DESC ${limit} ${offset}`);
199
- if (result.length === 0)
200
- return [];
201
- return result[0].values.map((row) => this.rowToMemory(row));
195
+ const limit = options?.limit ?? 100;
196
+ const offset = options?.offset ?? 0;
197
+ const rows = this.db.prepare(`SELECT id, type, title, content, project, tags, session_id, created_at, updated_at, accessed_at
198
+ FROM memories ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
199
+ return rows.map((row) => this.rowToMemory(row));
202
200
  }
203
201
  updateAccessedAt(id) {
204
202
  if (!this.db)
205
203
  throw new Error('Database not initialized');
206
- this.db.run(`UPDATE memories SET accessed_at = ? WHERE id = ?`, [Date.now(), id]);
207
- this.trackWrite();
204
+ this.db.prepare(`UPDATE memories SET accessed_at = ? WHERE id = ?`).run(Date.now(), id);
208
205
  }
209
206
  getMemoryCount() {
210
207
  if (!this.db)
211
208
  throw new Error('Database not initialized');
212
- const result = this.db.exec('SELECT COUNT(*) FROM memories');
213
- if (result.length === 0)
214
- return 0;
215
- return Number(result[0].values[0][0]);
209
+ const row = this.db.prepare('SELECT COUNT(*) as count FROM memories').get();
210
+ return row.count;
216
211
  }
217
212
  // --- BM25 Search ---
218
213
  searchBM25(query, limit = 20, filters) {
219
214
  if (!this.db)
220
215
  throw new Error('Database not initialized');
221
216
  try {
222
- // Escape FTS5 special characters
223
- const escaped = query.replace(/['"]/g, ' ').trim();
217
+ // Escape FTS5 special characters — strip quotes and special operators
218
+ const escaped = query.replace(/['"*(){}[\]:^~!]/g, ' ').trim();
224
219
  if (!escaped)
225
220
  return [];
226
- let sql = `
227
- SELECT m.id, bm25(memory_fts) as score
228
- FROM memory_fts f
229
- JOIN memories m ON m.rowid = f.rowid
230
- WHERE memory_fts MATCH '${escaped}'
221
+ // Build FTS5 match query with proper term quoting
222
+ const terms = escaped.split(/\s+/).filter(t => t.length > 1);
223
+ if (terms.length === 0)
224
+ return [];
225
+ const ftsQuery = terms.map(t => `"${t}"`).join(' OR ');
226
+ let sql = `
227
+ SELECT m.id, bm25(memory_fts) as score
228
+ FROM memory_fts f
229
+ JOIN memories m ON m.rowid = f.rowid
230
+ WHERE memory_fts MATCH '${ftsQuery}'
231
231
  `;
232
232
  if (filters?.type) {
233
- sql += ` AND m.type = '${filters.type}'`;
233
+ sql += ` AND m.type = '${filters.type.replace(/'/g, "''")}'`;
234
234
  }
235
235
  if (filters?.project) {
236
236
  sql += ` AND m.project = '${filters.project.replace(/'/g, "''")}'`;
237
237
  }
238
238
  sql += ` ORDER BY score LIMIT ${limit}`;
239
- const result = this.db.exec(sql);
240
- if (result.length === 0)
241
- return [];
242
- return result[0].values.map((row) => ({
243
- id: row[0],
244
- score: Math.abs(row[1]), // BM25 returns negative scores
239
+ const rows = this.db.prepare(sql).all();
240
+ return rows.map((row) => ({
241
+ id: row.id,
242
+ score: Math.abs(row.score), // BM25 returns negative scores
245
243
  }));
246
244
  }
247
245
  catch {
@@ -265,91 +263,72 @@ export class MemoryStore {
265
263
  if (filters?.project)
266
264
  sql += ` AND project = '${filters.project.replace(/'/g, "''")}'`;
267
265
  sql += ` LIMIT ${limit}`;
268
- const result = this.db.exec(sql);
269
- if (result.length === 0)
270
- return [];
271
- return result[0].values.map((row, i) => ({
272
- id: row[0],
273
- score: 1 / (i + 1), // Simple rank-based score
266
+ const rows = this.db.prepare(sql).all();
267
+ return rows.map((row, i) => ({
268
+ id: row.id,
269
+ score: 1 / (i + 1),
274
270
  }));
275
271
  }
276
272
  // --- Sessions ---
277
273
  createSession(session) {
278
274
  if (!this.db)
279
275
  throw new Error('Database not initialized');
280
- this.db.run(`INSERT INTO sessions (id, project, status, started_at, ended_at, summary, tool_calls)
281
- VALUES (?, ?, ?, ?, ?, ?, ?)`, [
282
- session.id,
283
- session.project,
284
- session.status,
285
- session.startedAt,
286
- session.endedAt ?? null,
287
- session.summary ?? null,
288
- session.toolCalls,
289
- ]);
290
- this.trackWrite();
276
+ this.db.prepare(`INSERT INTO sessions (id, project, status, started_at, ended_at, summary, tool_calls)
277
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run(session.id, session.project, session.status, session.startedAt, session.endedAt ?? null, session.summary ?? null, session.toolCalls);
291
278
  }
292
279
  endSession(id, summary) {
293
280
  if (!this.db)
294
281
  throw new Error('Database not initialized');
295
- this.db.run(`UPDATE sessions SET status = 'completed', ended_at = ?, summary = ? WHERE id = ?`, [Date.now(), summary ?? null, id]);
296
- this.trackWrite();
282
+ this.db.prepare(`UPDATE sessions SET status = 'completed', ended_at = ?, summary = ? WHERE id = ?`).run(Date.now(), summary ?? null, id);
297
283
  }
298
284
  getActiveSession() {
299
285
  if (!this.db)
300
286
  throw new Error('Database not initialized');
301
- const result = this.db.exec(`SELECT id, project, status, started_at, ended_at, summary, tool_calls
302
- FROM sessions WHERE status = 'active' ORDER BY started_at DESC LIMIT 1`);
303
- if (result.length === 0 || result[0].values.length === 0)
287
+ const row = this.db.prepare(`SELECT id, project, status, started_at, ended_at, summary, tool_calls
288
+ FROM sessions WHERE status = 'active' ORDER BY started_at DESC LIMIT 1`).get();
289
+ if (!row)
304
290
  return null;
305
- const row = result[0].values[0];
306
291
  return {
307
- id: row[0],
308
- project: row[1],
309
- status: row[2],
310
- startedAt: row[3],
311
- endedAt: row[4],
312
- summary: row[5],
313
- toolCalls: row[6],
292
+ id: row.id,
293
+ project: row.project,
294
+ status: row.status,
295
+ startedAt: row.started_at,
296
+ endedAt: row.ended_at,
297
+ summary: row.summary,
298
+ toolCalls: row.tool_calls,
314
299
  };
315
300
  }
316
301
  getSessionCount() {
317
302
  if (!this.db)
318
303
  throw new Error('Database not initialized');
319
- const result = this.db.exec('SELECT COUNT(*) FROM sessions');
320
- if (result.length === 0)
321
- return 0;
322
- return Number(result[0].values[0][0]);
304
+ const row = this.db.prepare('SELECT COUNT(*) as count FROM sessions').get();
305
+ return row.count;
323
306
  }
324
307
  // --- Queries for cache/stats ---
325
308
  getOldestMemoryTimestamp() {
326
309
  if (!this.db)
327
310
  return undefined;
328
- const result = this.db.exec('SELECT MIN(created_at) FROM memories');
329
- if (result.length === 0 || result[0].values[0][0] === null)
330
- return undefined;
331
- return Number(result[0].values[0][0]);
311
+ const row = this.db.prepare('SELECT MIN(created_at) as ts FROM memories').get();
312
+ return row.ts ?? undefined;
332
313
  }
333
314
  getNewestMemoryTimestamp() {
334
315
  if (!this.db)
335
316
  return undefined;
336
- const result = this.db.exec('SELECT MAX(created_at) FROM memories');
337
- if (result.length === 0 || result[0].values[0][0] === null)
338
- return undefined;
339
- return Number(result[0].values[0][0]);
317
+ const row = this.db.prepare('SELECT MAX(created_at) as ts FROM memories').get();
318
+ return row.ts ?? undefined;
340
319
  }
341
320
  getEvictionCandidates(batchSize, activeSessionId) {
342
321
  if (!this.db)
343
322
  return [];
344
323
  let sql = `SELECT id FROM memories WHERE type != 'decision'`;
324
+ const params = [];
345
325
  if (activeSessionId) {
346
- sql += ` AND (session_id IS NULL OR session_id != '${activeSessionId.replace(/'/g, "''")}')`;
326
+ sql += ` AND (session_id IS NULL OR session_id != ?)`;
327
+ params.push(activeSessionId);
347
328
  }
348
- sql += ` ORDER BY accessed_at ASC, created_at ASC LIMIT ${batchSize}`;
349
- const result = this.db.exec(sql);
350
- if (result.length === 0)
351
- return [];
352
- return result[0].values.map((row) => ({ id: row[0] }));
329
+ sql += ` ORDER BY accessed_at ASC, created_at ASC LIMIT ?`;
330
+ params.push(batchSize);
331
+ return this.db.prepare(sql).all(...params);
353
332
  }
354
333
  /**
355
334
  * Get memories in chronological order around a given memory
@@ -358,126 +337,200 @@ export class MemoryStore {
358
337
  if (!this.db)
359
338
  return [];
360
339
  // Get anchor memory's timestamp
361
- const anchorResult = this.db.exec(`SELECT created_at FROM memories WHERE id = '${memoryId.replace(/'/g, "''")}'`);
362
- if (anchorResult.length === 0 || anchorResult[0].values.length === 0)
340
+ const anchor = this.db.prepare(`SELECT created_at FROM memories WHERE id = ?`).get(memoryId);
341
+ if (!anchor)
363
342
  return [];
364
- const anchorTs = anchorResult[0].values[0][0];
343
+ const anchorTs = anchor.created_at;
365
344
  // Get before + anchor + after
366
- const result = this.db.exec(`SELECT id, title, type, created_at FROM (
367
- SELECT id, title, type, created_at FROM memories
368
- WHERE created_at <= ${anchorTs}
369
- ORDER BY created_at DESC LIMIT ${window + 1}
370
- )
371
- UNION ALL
372
- SELECT id, title, type, created_at FROM (
373
- SELECT id, title, type, created_at FROM memories
374
- WHERE created_at > ${anchorTs}
375
- ORDER BY created_at ASC LIMIT ${window}
376
- )
377
- ORDER BY created_at ASC`);
378
- if (result.length === 0)
379
- return [];
380
- return result[0].values.map((row) => ({
381
- id: row[0],
382
- title: row[1],
383
- type: row[2],
384
- createdAt: row[3],
345
+ const rows = this.db.prepare(`SELECT id, title, type, created_at FROM (
346
+ SELECT id, title, type, created_at FROM memories
347
+ WHERE created_at <= ?
348
+ ORDER BY created_at DESC LIMIT ?
349
+ )
350
+ UNION ALL
351
+ SELECT id, title, type, created_at FROM (
352
+ SELECT id, title, type, created_at FROM memories
353
+ WHERE created_at > ?
354
+ ORDER BY created_at ASC LIMIT ?
355
+ )
356
+ ORDER BY created_at ASC`).all(anchorTs, window + 1, anchorTs, window);
357
+ return rows.map((row) => ({
358
+ id: row.id,
359
+ title: row.title,
360
+ type: row.type,
361
+ createdAt: row.created_at,
385
362
  }));
386
363
  }
387
364
  // --- Persistence ---
388
365
  getDbSizeBytes() {
389
366
  if (!this.db)
390
367
  return 0;
391
- return this.db.export().byteLength;
368
+ // O(1) — reads page count and page size from SQLite internal state
369
+ // No memory allocation, no data copy (unlike sql.js db.export())
370
+ try {
371
+ const stat = statSync(this.dbPath);
372
+ return stat.size;
373
+ }
374
+ catch {
375
+ return 0;
376
+ }
392
377
  }
393
378
  saveToDisk() {
394
379
  if (!this.db)
395
380
  return;
396
- const dir = join(this.dbPath, '..');
397
- if (!existsSync(dir)) {
398
- mkdirSync(dir, { recursive: true });
381
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
382
+ if (this.db._isSqlJs) {
383
+ // sql.js: export + writeFileSync
384
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
385
+ this.db._sqlJsSave();
386
+ }
387
+ else {
388
+ // better-sqlite3: WAL checkpoint
389
+ this.db.pragma('wal_checkpoint(TRUNCATE)');
399
390
  }
400
- const data = this.db.export();
401
- writeFileSync(this.dbPath, data);
402
391
  }
403
392
  close() {
404
393
  if (this.db) {
405
- this.saveToDisk();
406
394
  this.db.close();
407
395
  this.db = null;
408
396
  this.initialized = false;
409
397
  }
410
398
  }
399
+ /**
400
+ * Wraps a sql.js database to expose the same API surface as better-sqlite3.
401
+ * This enables the rest of the class to use db.prepare().run/get/all uniformly.
402
+ */
403
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
404
+ wrapSqlJs(rawDb) {
405
+ const dbPath = this.dbPath;
406
+ let writeCount = 0;
407
+ const saveToDisk = () => {
408
+ const data = rawDb.export();
409
+ const dir = join(dbPath, '..');
410
+ if (!existsSync(dir))
411
+ mkdirSync(dir, { recursive: true });
412
+ writeFileSync(dbPath, data);
413
+ };
414
+ const trackWrite = () => {
415
+ writeCount++;
416
+ if (writeCount >= 200) {
417
+ saveToDisk();
418
+ writeCount = 0;
419
+ }
420
+ };
421
+ return {
422
+ exec: (sql) => rawDb.run(sql),
423
+ pragma: () => { }, // No-op for sql.js
424
+ prepare: (sql) => ({
425
+ run: (...params) => {
426
+ rawDb.run(sql, params.length === 1 && Array.isArray(params[0]) ? params[0] : params);
427
+ trackWrite();
428
+ },
429
+ get: (...params) => {
430
+ const result = rawDb.exec(sql.replace(/\?/g, () => {
431
+ const p = params.shift();
432
+ if (p === null || p === undefined)
433
+ return 'NULL';
434
+ if (typeof p === 'string')
435
+ return `'${p.replace(/'/g, "''")}'`;
436
+ return String(p);
437
+ }));
438
+ if (!result.length || !result[0].values.length)
439
+ return undefined;
440
+ const cols = result[0].columns;
441
+ const row = result[0].values[0];
442
+ const obj = {};
443
+ cols.forEach((c, i) => { obj[c] = row[i]; });
444
+ return obj;
445
+ },
446
+ all: (...params) => {
447
+ let processed = sql;
448
+ for (const p of params) {
449
+ if (p === null || p === undefined) {
450
+ processed = processed.replace('?', 'NULL');
451
+ }
452
+ else if (typeof p === 'string') {
453
+ processed = processed.replace('?', `'${p.replace(/'/g, "''")}'`);
454
+ }
455
+ else {
456
+ processed = processed.replace('?', String(p));
457
+ }
458
+ }
459
+ const result = rawDb.exec(processed);
460
+ if (!result.length)
461
+ return [];
462
+ const cols = result[0].columns;
463
+ return result[0].values.map((row) => {
464
+ const obj = {};
465
+ cols.forEach((c, i) => { obj[c] = row[i]; });
466
+ return obj;
467
+ });
468
+ },
469
+ }),
470
+ close: () => { saveToDisk(); rawDb.close(); },
471
+ // For saveToDisk() compatibility
472
+ _sqlJsSave: saveToDisk,
473
+ _isSqlJs: true,
474
+ };
475
+ }
411
476
  rowToMemory(row) {
412
477
  return {
413
- id: row[0],
414
- type: row[1],
415
- title: row[2],
416
- content: row[3],
417
- project: row[4],
418
- tags: JSON.parse(row[5] || '[]'),
419
- sessionId: row[6] || undefined,
420
- createdAt: row[7],
421
- updatedAt: row[8],
422
- accessedAt: row[9],
478
+ id: row.id,
479
+ type: row.type,
480
+ title: row.title,
481
+ content: row.content,
482
+ project: row.project,
483
+ tags: JSON.parse(row.tags || '[]'),
484
+ sessionId: row.session_id || undefined,
485
+ createdAt: row.created_at,
486
+ updatedAt: row.updated_at,
487
+ accessedAt: row.accessed_at,
423
488
  };
424
489
  }
425
490
  // --- Background Indexer Graph Persistence ---
426
491
  saveCodeNode(node) {
427
492
  if (!this.db)
428
493
  throw new Error('Database not initialized');
429
- this.db.run(`INSERT OR REPLACE INTO code_nodes (id, type, name, file_path, start_line, end_line, content, summary, hash, updated_at)
430
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
431
- node.id,
432
- node.type,
433
- node.name,
434
- node.filePath,
435
- node.startLine,
436
- node.endLine,
437
- node.content,
438
- node.summary ?? null,
439
- node.hash,
440
- node.updatedAt,
441
- ]);
442
- this.trackWrite();
494
+ this.db.prepare(`INSERT OR REPLACE INTO code_nodes (id, type, name, file_path, start_line, end_line, content, summary, hash, updated_at)
495
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(node.id, node.type, node.name, node.filePath, node.startLine, node.endLine, node.content, node.summary ?? null, node.hash, node.updatedAt);
443
496
  }
444
497
  saveCodeEdge(edge) {
445
498
  if (!this.db)
446
499
  throw new Error('Database not initialized');
447
- this.db.run(`INSERT OR IGNORE INTO code_edges (id, source_id, target_id, type, weight)
448
- VALUES (?, ?, ?, ?, ?)`, [edge.id, edge.sourceId, edge.targetId, edge.type, edge.weight]);
449
- this.trackWrite();
500
+ this.db.prepare(`INSERT OR IGNORE INTO code_edges (id, source_id, target_id, type, weight)
501
+ VALUES (?, ?, ?, ?, ?)`).run(edge.id, edge.sourceId, edge.targetId, edge.type, edge.weight);
502
+ }
503
+ getCodeNodeIdsByFile(filePath) {
504
+ if (!this.db)
505
+ return [];
506
+ const rows = this.db.prepare(`SELECT id FROM code_nodes WHERE file_path = ?`).all(filePath);
507
+ return rows.map((row) => row.id);
450
508
  }
451
509
  deleteCodeNodesByFile(filePath) {
452
510
  if (!this.db)
453
511
  throw new Error('Database not initialized');
454
- // Due to ON DELETE CASCADE, deleting the code_nodes will auto delete code_edges where source_id = node.id
455
- this.db.run(`DELETE FROM code_nodes WHERE file_path = ?`, [filePath]);
456
- this.trackWrite();
512
+ this.db.prepare(`DELETE FROM code_nodes WHERE file_path = ?`).run(filePath);
457
513
  }
458
514
  getCodeNodeByHash(id) {
459
515
  if (!this.db)
460
516
  throw new Error('Database not initialized');
461
- const result = this.db.exec(`SELECT hash FROM code_nodes WHERE id = '${id.replace(/'/g, "''")}'`);
462
- if (result.length === 0 || result[0].values.length === 0)
463
- return null;
464
- return result[0].values[0][0];
517
+ const row = this.db.prepare(`SELECT hash FROM code_nodes WHERE id = ?`).get(id);
518
+ return row?.hash ?? null;
465
519
  }
466
520
  getCodeNode(id) {
467
521
  if (!this.db)
468
522
  throw new Error('Database not initialized');
469
- const result = this.db.exec(`SELECT id, type, name, file_path, content, hash, updated_at FROM code_nodes WHERE id = '${id.replace(/'/g, "''")}'`);
470
- if (result.length === 0 || result[0].values.length === 0)
523
+ const row = this.db.prepare(`SELECT id, type, name, file_path, content, hash, updated_at FROM code_nodes WHERE id = ?`).get(id);
524
+ if (!row)
471
525
  return null;
472
- const row = result[0].values[0];
473
526
  return {
474
- id: row[0],
475
- type: row[1],
476
- name: row[2],
477
- filePath: row[3],
478
- content: row[4],
479
- hash: row[5],
480
- updatedAt: row[6],
527
+ id: row.id,
528
+ type: row.type,
529
+ name: row.name,
530
+ filePath: row.file_path,
531
+ content: row.content,
532
+ hash: row.hash,
533
+ updatedAt: row.updated_at,
481
534
  };
482
535
  }
483
536
  }