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