@hivehub/rulebook 4.4.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +45 -4
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +86 -0
- package/dist/cli/commands.js.map +1 -1
- package/dist/core/agent-template-engine.d.ts +51 -0
- package/dist/core/agent-template-engine.d.ts.map +1 -0
- package/dist/core/agent-template-engine.js +273 -0
- package/dist/core/agent-template-engine.js.map +1 -0
- package/dist/core/complexity-detector.d.ts +36 -0
- package/dist/core/complexity-detector.d.ts.map +1 -0
- package/dist/core/complexity-detector.js +254 -0
- package/dist/core/complexity-detector.js.map +1 -0
- package/dist/core/generator.d.ts +1 -0
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +21 -3
- package/dist/core/generator.js.map +1 -1
- package/dist/core/indexer/background-indexer.d.ts +2 -2
- package/dist/core/indexer/background-indexer.d.ts.map +1 -1
- package/dist/core/indexer/background-indexer.js +55 -61
- package/dist/core/indexer/background-indexer.js.map +1 -1
- package/dist/core/rule-engine.d.ts +64 -0
- package/dist/core/rule-engine.d.ts.map +1 -0
- package/dist/core/rule-engine.js +333 -0
- package/dist/core/rule-engine.js.map +1 -0
- package/dist/core/task-manager.d.ts +4 -0
- package/dist/core/task-manager.d.ts.map +1 -1
- package/dist/core/task-manager.js +39 -24
- package/dist/core/task-manager.js.map +1 -1
- package/dist/index.js +182 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/rulebook-server.d.ts.map +1 -1
- package/dist/mcp/rulebook-server.js +278 -26
- package/dist/mcp/rulebook-server.js.map +1 -1
- package/dist/memory/hnsw-index.d.ts +3 -1
- package/dist/memory/hnsw-index.d.ts.map +1 -1
- package/dist/memory/hnsw-index.js +121 -18
- package/dist/memory/hnsw-index.js.map +1 -1
- package/dist/memory/memory-manager.d.ts +2 -0
- package/dist/memory/memory-manager.d.ts.map +1 -1
- package/dist/memory/memory-manager.js +16 -7
- package/dist/memory/memory-manager.js.map +1 -1
- package/dist/memory/memory-store.d.ts +15 -3
- package/dist/memory/memory-store.d.ts.map +1 -1
- package/dist/memory/memory-store.js +327 -274
- package/dist/memory/memory-store.js.map +1 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -3
- package/templates/agents/compiler/codegen-debugger.md +34 -0
- package/templates/agents/compiler/stdlib-engineer.md +28 -0
- package/templates/agents/compiler/test-coverage-guardian.md +31 -0
- package/templates/agents/game-engine/cpp-core-expert.md +35 -0
- package/templates/agents/game-engine/render-engineer.md +22 -0
- package/templates/agents/game-engine/shader-engineer.md +38 -0
- package/templates/agents/game-engine/systems-integration.md +43 -0
- package/templates/agents/generic/code-reviewer.md +39 -0
- package/templates/agents/generic/docs-writer.md +25 -0
- package/templates/agents/generic/project-manager.md +34 -0
- package/templates/agents/generic/researcher.md +34 -0
- package/templates/agents/generic/test-engineer.md +40 -0
- package/templates/agents/mobile/platform-specialist.md +22 -0
- package/templates/agents/mobile/ui-engineer.md +22 -0
- package/templates/agents/web-app/api-designer.md +22 -0
- package/templates/agents/web-app/backend-engineer.md +30 -0
- package/templates/agents/web-app/database-engineer.md +22 -0
- package/templates/agents/web-app/frontend-engineer.md +29 -0
- package/templates/agents/web-app/security-reviewer.md +32 -0
- package/templates/core/AGENT_AUTOMATION.md +8 -0
- package/templates/core/RULEBOOK.md +12 -0
- package/templates/core/TIER1_PROHIBITIONS.md +154 -0
- package/templates/core/TOKEN_OPTIMIZATION.md +49 -0
- package/templates/git/GIT_WORKFLOW.md +35 -0
- package/templates/rules/follow-task-sequence.md +36 -0
- package/templates/rules/git-safety.md +29 -0
- package/templates/rules/incremental-tests.md +29 -0
- package/templates/rules/no-deferred.md +31 -0
- package/templates/rules/no-shortcuts.md +30 -0
- package/templates/rules/research-first.md +30 -0
- package/templates/rules/sequential-editing.md +21 -0
- package/templates/rules/session-workflow.md +24 -0
- package/templates/rules/task-decomposition.md +32 -0
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SQLite Storage Layer using
|
|
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,
|
|
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
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
this.db =
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
//
|
|
42
|
-
//
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
136
|
-
this.db.
|
|
137
|
-
this.db.
|
|
138
|
-
this.db.
|
|
139
|
-
this.db.
|
|
140
|
-
this.db.
|
|
141
|
-
this.db.
|
|
142
|
-
this.db.
|
|
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.
|
|
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
|
|
174
|
-
FROM memories WHERE id =
|
|
175
|
-
if (
|
|
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.
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
196
|
-
const offset = options?.offset
|
|
197
|
-
const
|
|
198
|
-
FROM memories ${where} ORDER BY created_at DESC
|
|
199
|
-
|
|
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.
|
|
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
|
|
213
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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.
|
|
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.
|
|
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
|
|
302
|
-
FROM sessions WHERE status = 'active' ORDER BY started_at DESC LIMIT 1`);
|
|
303
|
-
if (
|
|
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
|
|
308
|
-
project: row
|
|
309
|
-
status: row
|
|
310
|
-
startedAt: row
|
|
311
|
-
endedAt: row
|
|
312
|
-
summary: row
|
|
313
|
-
toolCalls: row
|
|
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
|
|
320
|
-
|
|
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
|
|
329
|
-
|
|
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
|
|
337
|
-
|
|
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 !=
|
|
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
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
362
|
-
if (
|
|
340
|
+
const anchor = this.db.prepare(`SELECT created_at FROM memories WHERE id = ?`).get(memoryId);
|
|
341
|
+
if (!anchor)
|
|
363
342
|
return [];
|
|
364
|
-
const anchorTs =
|
|
343
|
+
const anchorTs = anchor.created_at;
|
|
365
344
|
// Get before + anchor + after
|
|
366
|
-
const
|
|
367
|
-
SELECT id, title, type, created_at FROM memories
|
|
368
|
-
WHERE created_at <=
|
|
369
|
-
ORDER BY created_at DESC LIMIT
|
|
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 >
|
|
375
|
-
ORDER BY created_at ASC LIMIT
|
|
376
|
-
)
|
|
377
|
-
ORDER BY created_at ASC`);
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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
|
-
|
|
397
|
-
if (
|
|
398
|
-
|
|
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
|
|
414
|
-
type: row
|
|
415
|
-
title: row
|
|
416
|
-
content: row
|
|
417
|
-
project: row
|
|
418
|
-
tags: JSON.parse(row
|
|
419
|
-
sessionId: row
|
|
420
|
-
createdAt: row
|
|
421
|
-
updatedAt: row
|
|
422
|
-
accessedAt: row
|
|
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.
|
|
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.
|
|
448
|
-
VALUES (?, ?, ?, ?, ?)
|
|
449
|
-
|
|
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
|
-
|
|
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
|
|
462
|
-
|
|
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
|
|
470
|
-
if (
|
|
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
|
|
475
|
-
type: row
|
|
476
|
-
name: row
|
|
477
|
-
filePath: row
|
|
478
|
-
content: row
|
|
479
|
-
hash: row
|
|
480
|
-
updatedAt: row
|
|
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
|
}
|