@teamlens/core 0.1.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/dist/analytics/analytics-engine.d.ts +45 -0
- package/dist/analytics/analytics-engine.d.ts.map +1 -0
- package/dist/analytics/analytics-engine.js +176 -0
- package/dist/analytics/analytics-engine.js.map +1 -0
- package/dist/distribution/distributor.d.ts +19 -0
- package/dist/distribution/distributor.d.ts.map +1 -0
- package/dist/distribution/distributor.js +220 -0
- package/dist/distribution/distributor.js.map +1 -0
- package/dist/extractor/git-extractor.d.ts +36 -0
- package/dist/extractor/git-extractor.d.ts.map +1 -0
- package/dist/extractor/git-extractor.js +198 -0
- package/dist/extractor/git-extractor.js.map +1 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +313 -0
- package/dist/index.js.map +1 -0
- package/dist/retrieval/retriever.d.ts +33 -0
- package/dist/retrieval/retriever.d.ts.map +1 -0
- package/dist/retrieval/retriever.js +126 -0
- package/dist/retrieval/retriever.js.map +1 -0
- package/dist/session/insight-detector.d.ts +10 -0
- package/dist/session/insight-detector.d.ts.map +1 -0
- package/dist/session/insight-detector.js +87 -0
- package/dist/session/insight-detector.js.map +1 -0
- package/dist/session/session-manager.d.ts +49 -0
- package/dist/session/session-manager.d.ts.map +1 -0
- package/dist/session/session-manager.js +228 -0
- package/dist/session/session-manager.js.map +1 -0
- package/dist/staleness/staleness-engine.d.ts +36 -0
- package/dist/staleness/staleness-engine.d.ts.map +1 -0
- package/dist/staleness/staleness-engine.js +141 -0
- package/dist/staleness/staleness-engine.js.map +1 -0
- package/dist/store/database.d.ts +121 -0
- package/dist/store/database.d.ts.map +1 -0
- package/dist/store/database.js +677 -0
- package/dist/store/database.js.map +1 -0
- package/dist/store/embeddings.d.ts +21 -0
- package/dist/store/embeddings.d.ts.map +1 -0
- package/dist/store/embeddings.js +70 -0
- package/dist/store/embeddings.js.map +1 -0
- package/dist/sync/team-sync.d.ts +77 -0
- package/dist/sync/team-sync.d.ts.map +1 -0
- package/dist/sync/team-sync.js +230 -0
- package/dist/sync/team-sync.js.map +1 -0
- package/dist/types.d.ts +223 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +24 -0
- package/dist/types.js.map +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
import initSqlJs from 'sql.js';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import picomatch from 'picomatch';
|
|
6
|
+
export class MemoryDatabase {
|
|
7
|
+
storageDir;
|
|
8
|
+
db;
|
|
9
|
+
dbPath;
|
|
10
|
+
sqlModule;
|
|
11
|
+
saveTimer = null;
|
|
12
|
+
dirty = false;
|
|
13
|
+
constructor(storageDir) {
|
|
14
|
+
this.storageDir = storageDir;
|
|
15
|
+
this.dbPath = path.join(storageDir, 'memory.db');
|
|
16
|
+
}
|
|
17
|
+
/** Async factory — sql.js requires async initialization. */
|
|
18
|
+
static async create(storageDir) {
|
|
19
|
+
const instance = new MemoryDatabase(storageDir);
|
|
20
|
+
fs.mkdirSync(storageDir, { recursive: true });
|
|
21
|
+
const SQL = await initSqlJs();
|
|
22
|
+
instance.sqlModule = SQL;
|
|
23
|
+
if (fs.existsSync(instance.dbPath)) {
|
|
24
|
+
const buffer = fs.readFileSync(instance.dbPath);
|
|
25
|
+
instance.db = new SQL.Database(buffer);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
instance.db = new SQL.Database();
|
|
29
|
+
}
|
|
30
|
+
instance.migrate();
|
|
31
|
+
instance.save();
|
|
32
|
+
return instance;
|
|
33
|
+
}
|
|
34
|
+
/** Reload the database from disk. Use when another process may have written changes. */
|
|
35
|
+
reload() {
|
|
36
|
+
if (fs.existsSync(this.dbPath)) {
|
|
37
|
+
const buffer = fs.readFileSync(this.dbPath);
|
|
38
|
+
this.db.close();
|
|
39
|
+
this.db = new this.sqlModule.Database(buffer);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// ── Compatibility Helpers ──
|
|
43
|
+
queryOne(sql, ...params) {
|
|
44
|
+
const stmt = this.db.prepare(sql);
|
|
45
|
+
if (params.length)
|
|
46
|
+
stmt.bind(params);
|
|
47
|
+
const row = stmt.step() ? stmt.getAsObject() : undefined;
|
|
48
|
+
stmt.free();
|
|
49
|
+
return row;
|
|
50
|
+
}
|
|
51
|
+
queryAll(sql, ...params) {
|
|
52
|
+
const stmt = this.db.prepare(sql);
|
|
53
|
+
if (params.length)
|
|
54
|
+
stmt.bind(params);
|
|
55
|
+
const rows = [];
|
|
56
|
+
while (stmt.step())
|
|
57
|
+
rows.push(stmt.getAsObject());
|
|
58
|
+
stmt.free();
|
|
59
|
+
return rows;
|
|
60
|
+
}
|
|
61
|
+
execute(sql, ...params) {
|
|
62
|
+
this.db.run(sql, params);
|
|
63
|
+
this.scheduleSave();
|
|
64
|
+
}
|
|
65
|
+
scheduleSave() {
|
|
66
|
+
this.dirty = true;
|
|
67
|
+
if (this.saveTimer)
|
|
68
|
+
return; // already scheduled
|
|
69
|
+
this.saveTimer = setTimeout(() => {
|
|
70
|
+
this.saveTimer = null;
|
|
71
|
+
if (this.dirty) {
|
|
72
|
+
this.save();
|
|
73
|
+
this.dirty = false;
|
|
74
|
+
}
|
|
75
|
+
}, 100); // 100ms debounce
|
|
76
|
+
}
|
|
77
|
+
save() {
|
|
78
|
+
fs.writeFileSync(this.dbPath, Buffer.from(this.db.export()));
|
|
79
|
+
}
|
|
80
|
+
/** Force an immediate save (call before close). */
|
|
81
|
+
flush() {
|
|
82
|
+
if (this.saveTimer) {
|
|
83
|
+
clearTimeout(this.saveTimer);
|
|
84
|
+
this.saveTimer = null;
|
|
85
|
+
}
|
|
86
|
+
if (this.dirty) {
|
|
87
|
+
this.save();
|
|
88
|
+
this.dirty = false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// ── Schema ──
|
|
92
|
+
migrate() {
|
|
93
|
+
this.db.run(`
|
|
94
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
95
|
+
id TEXT PRIMARY KEY,
|
|
96
|
+
content TEXT NOT NULL,
|
|
97
|
+
category TEXT NOT NULL,
|
|
98
|
+
source TEXT NOT NULL,
|
|
99
|
+
tier TEXT NOT NULL DEFAULT 'personal',
|
|
100
|
+
author TEXT NOT NULL DEFAULT 'unknown',
|
|
101
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
102
|
+
related_files TEXT NOT NULL DEFAULT '[]',
|
|
103
|
+
commit_sha TEXT,
|
|
104
|
+
staleness REAL NOT NULL DEFAULT 0.0,
|
|
105
|
+
confidence REAL NOT NULL DEFAULT 0.8,
|
|
106
|
+
embedding BLOB,
|
|
107
|
+
created_at TEXT NOT NULL,
|
|
108
|
+
updated_at TEXT NOT NULL,
|
|
109
|
+
validated_at TEXT NOT NULL
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
CREATE TABLE IF NOT EXISTS tracked_files (
|
|
113
|
+
path TEXT PRIMARY KEY,
|
|
114
|
+
hash TEXT NOT NULL,
|
|
115
|
+
last_modified TEXT NOT NULL
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
CREATE TABLE IF NOT EXISTS tracked_commits (
|
|
119
|
+
sha TEXT PRIMARY KEY,
|
|
120
|
+
message TEXT NOT NULL,
|
|
121
|
+
author TEXT NOT NULL,
|
|
122
|
+
date TEXT NOT NULL,
|
|
123
|
+
files TEXT NOT NULL DEFAULT '[]',
|
|
124
|
+
processed INTEGER NOT NULL DEFAULT 0
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
CREATE INDEX IF NOT EXISTS idx_memories_category ON memories(category);
|
|
128
|
+
CREATE INDEX IF NOT EXISTS idx_memories_staleness ON memories(staleness);
|
|
129
|
+
CREATE INDEX IF NOT EXISTS idx_memories_commit ON memories(commit_sha);
|
|
130
|
+
`);
|
|
131
|
+
// Migration: add tier/author columns if upgrading from v0.1
|
|
132
|
+
try {
|
|
133
|
+
this.db.run(`ALTER TABLE memories ADD COLUMN tier TEXT NOT NULL DEFAULT 'personal'`);
|
|
134
|
+
}
|
|
135
|
+
catch { /* column already exists */ }
|
|
136
|
+
try {
|
|
137
|
+
this.db.run(`ALTER TABLE memories ADD COLUMN author TEXT NOT NULL DEFAULT 'unknown'`);
|
|
138
|
+
}
|
|
139
|
+
catch { /* column already exists */ }
|
|
140
|
+
// Create indexes for tier/author (after migration ensures columns exist)
|
|
141
|
+
this.db.run(`
|
|
142
|
+
CREATE INDEX IF NOT EXISTS idx_memories_tier ON memories(tier);
|
|
143
|
+
CREATE INDEX IF NOT EXISTS idx_memories_author ON memories(author);
|
|
144
|
+
`);
|
|
145
|
+
// Migration: add governance columns
|
|
146
|
+
try {
|
|
147
|
+
this.db.run(`ALTER TABLE memories ADD COLUMN scope TEXT DEFAULT NULL`);
|
|
148
|
+
}
|
|
149
|
+
catch { /* column already exists */ }
|
|
150
|
+
try {
|
|
151
|
+
this.db.run(`ALTER TABLE memories ADD COLUMN priority TEXT DEFAULT NULL`);
|
|
152
|
+
}
|
|
153
|
+
catch { /* column already exists */ }
|
|
154
|
+
try {
|
|
155
|
+
this.db.run(`ALTER TABLE memories ADD COLUMN examples TEXT DEFAULT NULL`);
|
|
156
|
+
}
|
|
157
|
+
catch { /* column already exists */ }
|
|
158
|
+
try {
|
|
159
|
+
this.db.run(`ALTER TABLE memories ADD COLUMN active INTEGER NOT NULL DEFAULT 1`);
|
|
160
|
+
}
|
|
161
|
+
catch { /* column already exists */ }
|
|
162
|
+
// Indexes for governance queries
|
|
163
|
+
this.db.run(`
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_memories_source ON memories(source);
|
|
165
|
+
CREATE INDEX IF NOT EXISTS idx_memories_active ON memories(active);
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_memories_priority ON memories(priority);
|
|
167
|
+
`);
|
|
168
|
+
// Migration: add session tracking columns
|
|
169
|
+
try {
|
|
170
|
+
this.db.run(`ALTER TABLE memories ADD COLUMN session_id TEXT DEFAULT NULL`);
|
|
171
|
+
}
|
|
172
|
+
catch { /* column already exists */ }
|
|
173
|
+
try {
|
|
174
|
+
this.db.run(`ALTER TABLE memories ADD COLUMN reuse_count INTEGER NOT NULL DEFAULT 0`);
|
|
175
|
+
}
|
|
176
|
+
catch { /* column already exists */ }
|
|
177
|
+
// Sessions table
|
|
178
|
+
this.db.run(`
|
|
179
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
180
|
+
id TEXT PRIMARY KEY,
|
|
181
|
+
developer TEXT NOT NULL,
|
|
182
|
+
task TEXT DEFAULT '',
|
|
183
|
+
status TEXT DEFAULT 'active',
|
|
184
|
+
tool_name TEXT DEFAULT 'unknown',
|
|
185
|
+
started_at TEXT NOT NULL,
|
|
186
|
+
ended_at TEXT,
|
|
187
|
+
duration_seconds INTEGER,
|
|
188
|
+
files_touched TEXT DEFAULT '[]',
|
|
189
|
+
summary TEXT,
|
|
190
|
+
insight_count INTEGER DEFAULT 0,
|
|
191
|
+
activity_count INTEGER DEFAULT 0,
|
|
192
|
+
duplicates_prevented INTEGER DEFAULT 0
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_developer ON sessions(developer);
|
|
196
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
197
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
|
|
198
|
+
`);
|
|
199
|
+
// Activity events table
|
|
200
|
+
this.db.run(`
|
|
201
|
+
CREATE TABLE IF NOT EXISTS activity_events (
|
|
202
|
+
id TEXT PRIMARY KEY,
|
|
203
|
+
session_id TEXT NOT NULL,
|
|
204
|
+
type TEXT NOT NULL,
|
|
205
|
+
description TEXT DEFAULT '',
|
|
206
|
+
files TEXT DEFAULT '[]',
|
|
207
|
+
timestamp TEXT NOT NULL
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
CREATE INDEX IF NOT EXISTS idx_activity_session ON activity_events(session_id);
|
|
211
|
+
`);
|
|
212
|
+
}
|
|
213
|
+
// ── Memories ──
|
|
214
|
+
insertMemory(extracted, author, tier = 'personal', sessionId) {
|
|
215
|
+
const now = new Date().toISOString();
|
|
216
|
+
const memory = {
|
|
217
|
+
id: randomUUID(),
|
|
218
|
+
content: extracted.content,
|
|
219
|
+
category: extracted.category,
|
|
220
|
+
source: extracted.commitSha ? 'git' : 'agent',
|
|
221
|
+
tier,
|
|
222
|
+
author,
|
|
223
|
+
tags: extracted.tags,
|
|
224
|
+
relatedFiles: extracted.relatedFiles,
|
|
225
|
+
commitSha: extracted.commitSha,
|
|
226
|
+
staleness: 0,
|
|
227
|
+
confidence: extracted.confidence,
|
|
228
|
+
embedding: null,
|
|
229
|
+
scope: null,
|
|
230
|
+
priority: null,
|
|
231
|
+
examples: null,
|
|
232
|
+
active: true,
|
|
233
|
+
sessionId: sessionId ?? null,
|
|
234
|
+
reuseCount: 0,
|
|
235
|
+
createdAt: now,
|
|
236
|
+
updatedAt: now,
|
|
237
|
+
validatedAt: now,
|
|
238
|
+
};
|
|
239
|
+
this.execute(`INSERT INTO memories (id, content, category, source, tier, author, tags, related_files, commit_sha, staleness, confidence, embedding, scope, priority, examples, active, session_id, reuse_count, created_at, updated_at, validated_at)
|
|
240
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, memory.id, memory.content, memory.category, memory.source, memory.tier, memory.author, JSON.stringify(memory.tags), JSON.stringify(memory.relatedFiles), memory.commitSha, memory.staleness, memory.confidence, null, null, null, null, 1, memory.sessionId, 0, memory.createdAt, memory.updatedAt, memory.validatedAt);
|
|
241
|
+
return memory;
|
|
242
|
+
}
|
|
243
|
+
/** Insert a team memory with a known id (for import from team.jsonl). */
|
|
244
|
+
insertTeamMemory(memory) {
|
|
245
|
+
const now = new Date().toISOString();
|
|
246
|
+
this.execute(`INSERT OR IGNORE INTO memories (id, content, category, source, tier, author, tags, related_files, commit_sha, staleness, confidence, embedding, scope, priority, examples, active, session_id, reuse_count, created_at, updated_at, validated_at)
|
|
247
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, memory.id, memory.content, memory.category, memory.source, 'team', memory.author, JSON.stringify(memory.tags), JSON.stringify(memory.relatedFiles), memory.commitSha, 0, memory.confidence, null, memory.scope ? JSON.stringify(memory.scope) : null, memory.priority ?? null, memory.examples ? JSON.stringify(memory.examples) : null, memory.active ? 1 : 0, null, 0, memory.createdAt, now, now);
|
|
248
|
+
}
|
|
249
|
+
getMemory(id) {
|
|
250
|
+
const row = this.queryOne('SELECT * FROM memories WHERE id = ?', id);
|
|
251
|
+
return row ? this.rowToMemory(row) : null;
|
|
252
|
+
}
|
|
253
|
+
getAllMemories(includeStale = false) {
|
|
254
|
+
const query = includeStale
|
|
255
|
+
? 'SELECT * FROM memories ORDER BY created_at DESC'
|
|
256
|
+
: 'SELECT * FROM memories WHERE staleness < 1.0 ORDER BY created_at DESC';
|
|
257
|
+
const rows = this.queryAll(query);
|
|
258
|
+
return rows.map((r) => this.rowToMemory(r));
|
|
259
|
+
}
|
|
260
|
+
getMemoriesByTier(tier, includeStale = false) {
|
|
261
|
+
const query = includeStale
|
|
262
|
+
? 'SELECT * FROM memories WHERE tier = ? ORDER BY created_at DESC'
|
|
263
|
+
: 'SELECT * FROM memories WHERE tier = ? AND staleness < 1.0 ORDER BY created_at DESC';
|
|
264
|
+
const rows = this.queryAll(query, tier);
|
|
265
|
+
return rows.map((r) => this.rowToMemory(r));
|
|
266
|
+
}
|
|
267
|
+
getMemoriesByCategory(category) {
|
|
268
|
+
const rows = this.queryAll('SELECT * FROM memories WHERE category = ? AND staleness < 1.0 ORDER BY created_at DESC', category);
|
|
269
|
+
return rows.map((r) => this.rowToMemory(r));
|
|
270
|
+
}
|
|
271
|
+
getMemoriesByFile(filePath) {
|
|
272
|
+
const rows = this.queryAll('SELECT * FROM memories WHERE related_files LIKE ? ORDER BY created_at DESC', `%${filePath}%`);
|
|
273
|
+
return rows.map((r) => this.rowToMemory(r));
|
|
274
|
+
}
|
|
275
|
+
getMemoriesByAuthor(author) {
|
|
276
|
+
const rows = this.queryAll('SELECT * FROM memories WHERE author = ? AND staleness < 1.0 ORDER BY created_at DESC', author);
|
|
277
|
+
return rows.map((r) => this.rowToMemory(r));
|
|
278
|
+
}
|
|
279
|
+
getTeamAuthors() {
|
|
280
|
+
const rows = this.queryAll(`SELECT author, COUNT(*) as count FROM memories WHERE tier = 'team' AND staleness < 1.0 GROUP BY author ORDER BY count DESC`);
|
|
281
|
+
return rows.map((r) => ({ author: r.author, count: r.count }));
|
|
282
|
+
}
|
|
283
|
+
getMemoriesBySessionId(sessionId) {
|
|
284
|
+
const rows = this.queryAll('SELECT * FROM memories WHERE session_id = ? ORDER BY created_at DESC', sessionId);
|
|
285
|
+
return rows.map((r) => this.rowToMemory(r));
|
|
286
|
+
}
|
|
287
|
+
updateTier(id, tier) {
|
|
288
|
+
this.execute('UPDATE memories SET tier = ?, updated_at = ? WHERE id = ?', tier, new Date().toISOString(), id);
|
|
289
|
+
}
|
|
290
|
+
updateStaleness(id, staleness) {
|
|
291
|
+
this.execute('UPDATE memories SET staleness = ?, updated_at = ? WHERE id = ?', Math.min(staleness, 1.0), new Date().toISOString(), id);
|
|
292
|
+
}
|
|
293
|
+
updateEmbedding(id, embedding) {
|
|
294
|
+
const buffer = Buffer.from(new Float32Array(embedding).buffer);
|
|
295
|
+
this.execute('UPDATE memories SET embedding = ?, updated_at = ? WHERE id = ?', buffer, new Date().toISOString(), id);
|
|
296
|
+
}
|
|
297
|
+
validateMemory(id) {
|
|
298
|
+
const now = new Date().toISOString();
|
|
299
|
+
this.execute('UPDATE memories SET staleness = 0.0, validated_at = ?, updated_at = ? WHERE id = ?', now, now, id);
|
|
300
|
+
}
|
|
301
|
+
deleteMemory(id) {
|
|
302
|
+
this.execute('DELETE FROM memories WHERE id = ?', id);
|
|
303
|
+
}
|
|
304
|
+
hasMemory(id) {
|
|
305
|
+
const row = this.queryOne('SELECT 1 FROM memories WHERE id = ?', id);
|
|
306
|
+
return !!row;
|
|
307
|
+
}
|
|
308
|
+
incrementReuseCount(id) {
|
|
309
|
+
this.execute('UPDATE memories SET reuse_count = reuse_count + 1, updated_at = ? WHERE id = ?', new Date().toISOString(), id);
|
|
310
|
+
}
|
|
311
|
+
getTotalReuseCount() {
|
|
312
|
+
const row = this.queryOne('SELECT COALESCE(SUM(reuse_count), 0) as total FROM memories');
|
|
313
|
+
return row?.total ?? 0;
|
|
314
|
+
}
|
|
315
|
+
getMemoryCount() {
|
|
316
|
+
const total = this.queryOne('SELECT COUNT(*) as count FROM memories').count;
|
|
317
|
+
const stale = this.queryOne('SELECT COUNT(*) as count FROM memories WHERE staleness >= 0.6').count;
|
|
318
|
+
const team = this.queryOne("SELECT COUNT(*) as count FROM memories WHERE tier = 'team'").count;
|
|
319
|
+
return { total, stale, fresh: total - stale, team, personal: total - team };
|
|
320
|
+
}
|
|
321
|
+
// ── Rule Methods ──
|
|
322
|
+
/** Get all rules, ordered by priority. Optionally include inactive. */
|
|
323
|
+
getRules(includeInactive = false) {
|
|
324
|
+
const query = includeInactive
|
|
325
|
+
? `SELECT * FROM memories WHERE source = 'rule' ORDER BY
|
|
326
|
+
CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'normal' THEN 2 WHEN 'low' THEN 3 ELSE 2 END,
|
|
327
|
+
created_at DESC`
|
|
328
|
+
: `SELECT * FROM memories WHERE source = 'rule' AND active = 1 ORDER BY
|
|
329
|
+
CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'normal' THEN 2 WHEN 'low' THEN 3 ELSE 2 END,
|
|
330
|
+
created_at DESC`;
|
|
331
|
+
const rows = this.queryAll(query);
|
|
332
|
+
return rows.map((r) => this.rowToMemory(r));
|
|
333
|
+
}
|
|
334
|
+
/** Get rules that apply to a specific file path (by scope glob matching). */
|
|
335
|
+
getRulesForFile(filePath) {
|
|
336
|
+
const allRules = this.getRules(false);
|
|
337
|
+
return allRules.filter((rule) => {
|
|
338
|
+
if (!rule.scope || rule.scope.length === 0)
|
|
339
|
+
return true; // global rule
|
|
340
|
+
return picomatch.isMatch(filePath, rule.scope);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
/** Toggle a rule active/inactive. */
|
|
344
|
+
setRuleActive(id, active) {
|
|
345
|
+
this.execute('UPDATE memories SET active = ?, updated_at = ? WHERE id = ?', active ? 1 : 0, new Date().toISOString(), id);
|
|
346
|
+
}
|
|
347
|
+
/** Update governance-specific fields on a memory (scope, priority, examples, source). */
|
|
348
|
+
updateRuleFields(id, fields) {
|
|
349
|
+
const updates = [];
|
|
350
|
+
const params = [];
|
|
351
|
+
if (fields.source !== undefined) {
|
|
352
|
+
updates.push('source = ?');
|
|
353
|
+
params.push(fields.source);
|
|
354
|
+
}
|
|
355
|
+
if (fields.scope !== undefined) {
|
|
356
|
+
updates.push('scope = ?');
|
|
357
|
+
params.push(fields.scope ? JSON.stringify(fields.scope) : null);
|
|
358
|
+
}
|
|
359
|
+
if (fields.priority !== undefined) {
|
|
360
|
+
updates.push('priority = ?');
|
|
361
|
+
params.push(fields.priority);
|
|
362
|
+
}
|
|
363
|
+
if (fields.examples !== undefined) {
|
|
364
|
+
updates.push('examples = ?');
|
|
365
|
+
params.push(fields.examples ? JSON.stringify(fields.examples) : null);
|
|
366
|
+
}
|
|
367
|
+
if (updates.length === 0)
|
|
368
|
+
return;
|
|
369
|
+
updates.push('updated_at = ?');
|
|
370
|
+
params.push(new Date().toISOString());
|
|
371
|
+
params.push(id);
|
|
372
|
+
this.execute(`UPDATE memories SET ${updates.join(', ')} WHERE id = ?`, ...params);
|
|
373
|
+
}
|
|
374
|
+
// ── Sessions ──
|
|
375
|
+
insertSession(session) {
|
|
376
|
+
this.execute(`INSERT INTO sessions (id, developer, task, status, tool_name, started_at, ended_at, duration_seconds, files_touched, summary, insight_count, activity_count, duplicates_prevented)
|
|
377
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, session.id, session.developer, session.task, session.status, session.toolName, session.startedAt, session.endedAt, session.durationSeconds, JSON.stringify(session.filesTouched), session.summary, session.insightCount, session.activityCount, session.duplicatesPrevented);
|
|
378
|
+
}
|
|
379
|
+
updateSession(id, fields) {
|
|
380
|
+
const updates = [];
|
|
381
|
+
const params = [];
|
|
382
|
+
if (fields.status !== undefined) {
|
|
383
|
+
updates.push('status = ?');
|
|
384
|
+
params.push(fields.status);
|
|
385
|
+
}
|
|
386
|
+
if (fields.task !== undefined) {
|
|
387
|
+
updates.push('task = ?');
|
|
388
|
+
params.push(fields.task);
|
|
389
|
+
}
|
|
390
|
+
if (fields.endedAt !== undefined) {
|
|
391
|
+
updates.push('ended_at = ?');
|
|
392
|
+
params.push(fields.endedAt);
|
|
393
|
+
}
|
|
394
|
+
if (fields.durationSeconds !== undefined) {
|
|
395
|
+
updates.push('duration_seconds = ?');
|
|
396
|
+
params.push(fields.durationSeconds);
|
|
397
|
+
}
|
|
398
|
+
if (fields.filesTouched !== undefined) {
|
|
399
|
+
updates.push('files_touched = ?');
|
|
400
|
+
params.push(JSON.stringify(fields.filesTouched));
|
|
401
|
+
}
|
|
402
|
+
if (fields.summary !== undefined) {
|
|
403
|
+
updates.push('summary = ?');
|
|
404
|
+
params.push(fields.summary);
|
|
405
|
+
}
|
|
406
|
+
if (fields.insightCount !== undefined) {
|
|
407
|
+
updates.push('insight_count = ?');
|
|
408
|
+
params.push(fields.insightCount);
|
|
409
|
+
}
|
|
410
|
+
if (fields.activityCount !== undefined) {
|
|
411
|
+
updates.push('activity_count = ?');
|
|
412
|
+
params.push(fields.activityCount);
|
|
413
|
+
}
|
|
414
|
+
if (fields.duplicatesPrevented !== undefined) {
|
|
415
|
+
updates.push('duplicates_prevented = ?');
|
|
416
|
+
params.push(fields.duplicatesPrevented);
|
|
417
|
+
}
|
|
418
|
+
if (updates.length === 0)
|
|
419
|
+
return;
|
|
420
|
+
params.push(id);
|
|
421
|
+
this.execute(`UPDATE sessions SET ${updates.join(', ')} WHERE id = ?`, ...params);
|
|
422
|
+
}
|
|
423
|
+
getSession(id) {
|
|
424
|
+
const row = this.queryOne('SELECT * FROM sessions WHERE id = ?', id);
|
|
425
|
+
return row ? this.rowToSession(row) : null;
|
|
426
|
+
}
|
|
427
|
+
getActiveSession(developer) {
|
|
428
|
+
const row = this.queryOne(`SELECT * FROM sessions WHERE developer = ? AND status = 'active' ORDER BY started_at DESC LIMIT 1`, developer);
|
|
429
|
+
return row ? this.rowToSession(row) : null;
|
|
430
|
+
}
|
|
431
|
+
getSessionsInRange(startDate, endDate, limit = 100) {
|
|
432
|
+
const rows = this.queryAll('SELECT * FROM sessions WHERE started_at >= ? AND started_at <= ? ORDER BY started_at DESC LIMIT ?', startDate, endDate, limit);
|
|
433
|
+
return rows.map((r) => this.rowToSession(r));
|
|
434
|
+
}
|
|
435
|
+
getAllSessions(limit = 100, offset = 0) {
|
|
436
|
+
const rows = this.queryAll('SELECT * FROM sessions ORDER BY started_at DESC LIMIT ? OFFSET ?', limit, offset);
|
|
437
|
+
return rows.map((r) => this.rowToSession(r));
|
|
438
|
+
}
|
|
439
|
+
closeSession(id, summary) {
|
|
440
|
+
const session = this.getSession(id);
|
|
441
|
+
if (!session)
|
|
442
|
+
return;
|
|
443
|
+
const endedAt = new Date().toISOString();
|
|
444
|
+
const durationSeconds = Math.round((new Date(endedAt).getTime() - new Date(session.startedAt).getTime()) / 1000);
|
|
445
|
+
this.updateSession(id, {
|
|
446
|
+
status: 'completed',
|
|
447
|
+
endedAt,
|
|
448
|
+
durationSeconds,
|
|
449
|
+
summary: summary ?? session.summary,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
cleanupStaleSessions(timeoutMinutes) {
|
|
453
|
+
const cutoff = new Date(Date.now() - timeoutMinutes * 60 * 1000).toISOString();
|
|
454
|
+
const staleSessions = this.queryAll(`SELECT * FROM sessions WHERE status = 'active' AND started_at < ?`, cutoff);
|
|
455
|
+
for (const row of staleSessions) {
|
|
456
|
+
const session = this.rowToSession(row);
|
|
457
|
+
this.updateSession(session.id, {
|
|
458
|
+
status: 'abandoned',
|
|
459
|
+
endedAt: new Date().toISOString(),
|
|
460
|
+
durationSeconds: Math.round((Date.now() - new Date(session.startedAt).getTime()) / 1000),
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return staleSessions.length;
|
|
464
|
+
}
|
|
465
|
+
// ── Activity Events ──
|
|
466
|
+
insertActivityEvent(event) {
|
|
467
|
+
this.execute(`INSERT INTO activity_events (id, session_id, type, description, files, timestamp)
|
|
468
|
+
VALUES (?, ?, ?, ?, ?, ?)`, event.id, event.sessionId, event.type, event.description, JSON.stringify(event.files), event.timestamp);
|
|
469
|
+
// Increment activity count on session
|
|
470
|
+
this.execute('UPDATE sessions SET activity_count = activity_count + 1 WHERE id = ?', event.sessionId);
|
|
471
|
+
}
|
|
472
|
+
getActivitiesBySession(sessionId) {
|
|
473
|
+
const rows = this.queryAll('SELECT * FROM activity_events WHERE session_id = ? ORDER BY timestamp ASC', sessionId);
|
|
474
|
+
return rows.map((r) => this.rowToActivity(r));
|
|
475
|
+
}
|
|
476
|
+
// ── Analytics Queries ──
|
|
477
|
+
getInsightCountsByDeveloper() {
|
|
478
|
+
const rows = this.queryAll(`SELECT author as developer, COUNT(*) as count FROM memories
|
|
479
|
+
WHERE tier = 'team' AND staleness < 1.0
|
|
480
|
+
GROUP BY author ORDER BY count DESC`);
|
|
481
|
+
return rows.map((r) => ({ developer: r.developer, count: r.count }));
|
|
482
|
+
}
|
|
483
|
+
getSessionCountsByDate(days) {
|
|
484
|
+
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
485
|
+
const rows = this.queryAll(`SELECT DATE(started_at) as date, COUNT(*) as count FROM sessions
|
|
486
|
+
WHERE DATE(started_at) >= ? GROUP BY DATE(started_at) ORDER BY date ASC`, since);
|
|
487
|
+
return rows.map((r) => ({ date: r.date, count: r.count }));
|
|
488
|
+
}
|
|
489
|
+
getInsightCountsByDate(days) {
|
|
490
|
+
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
491
|
+
const rows = this.queryAll(`SELECT DATE(created_at) as date, COUNT(*) as count FROM memories
|
|
492
|
+
WHERE tier = 'team' AND DATE(created_at) >= ? GROUP BY DATE(created_at) ORDER BY date ASC`, since);
|
|
493
|
+
return rows.map((r) => ({ date: r.date, count: r.count }));
|
|
494
|
+
}
|
|
495
|
+
getActiveDevelopersByDate(days) {
|
|
496
|
+
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
497
|
+
const rows = this.queryAll(`SELECT DATE(started_at) as date, COUNT(DISTINCT developer) as count FROM sessions
|
|
498
|
+
WHERE DATE(started_at) >= ? GROUP BY DATE(started_at) ORDER BY date ASC`, since);
|
|
499
|
+
return rows.map((r) => ({ date: r.date, count: r.count }));
|
|
500
|
+
}
|
|
501
|
+
getHotFiles(limit = 10) {
|
|
502
|
+
// Only fetch the two columns we need instead of full Memory objects
|
|
503
|
+
const memories = this.queryAll(`SELECT related_files, created_at FROM memories
|
|
504
|
+
WHERE tier = 'team' AND staleness < 1.0 AND related_files != '[]'
|
|
505
|
+
ORDER BY created_at DESC`);
|
|
506
|
+
const fileMap = new Map();
|
|
507
|
+
for (const row of memories) {
|
|
508
|
+
try {
|
|
509
|
+
const files = JSON.parse(row.related_files);
|
|
510
|
+
for (const file of files) {
|
|
511
|
+
const existing = fileMap.get(file);
|
|
512
|
+
if (!existing) {
|
|
513
|
+
fileMap.set(file, { count: 1, lastInsight: row.created_at });
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
existing.count++;
|
|
517
|
+
if (row.created_at > existing.lastInsight) {
|
|
518
|
+
existing.lastInsight = row.created_at;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
catch { /* skip malformed */ }
|
|
524
|
+
}
|
|
525
|
+
return Array.from(fileMap.entries())
|
|
526
|
+
.map(([filePath, data]) => ({ filePath, count: data.count, lastInsight: data.lastInsight }))
|
|
527
|
+
.sort((a, b) => b.count - a.count)
|
|
528
|
+
.slice(0, limit);
|
|
529
|
+
}
|
|
530
|
+
getTotalSessionCount() {
|
|
531
|
+
const row = this.queryOne('SELECT COUNT(*) as count FROM sessions');
|
|
532
|
+
return row?.count ?? 0;
|
|
533
|
+
}
|
|
534
|
+
getTotalSessionDuration() {
|
|
535
|
+
const row = this.queryOne('SELECT COALESCE(SUM(duration_seconds), 0) as total FROM sessions WHERE duration_seconds IS NOT NULL');
|
|
536
|
+
return row?.total ?? 0;
|
|
537
|
+
}
|
|
538
|
+
getDistinctDevelopers() {
|
|
539
|
+
const rows = this.queryAll('SELECT DISTINCT developer FROM sessions ORDER BY developer ASC');
|
|
540
|
+
return rows.map((r) => r.developer);
|
|
541
|
+
}
|
|
542
|
+
getDuplicatesPrevented() {
|
|
543
|
+
const row = this.queryOne('SELECT COALESCE(SUM(duplicates_prevented), 0) as total FROM sessions');
|
|
544
|
+
return row?.total ?? 0;
|
|
545
|
+
}
|
|
546
|
+
getSessionsByDeveloper(developer) {
|
|
547
|
+
const rows = this.queryAll('SELECT * FROM sessions WHERE developer = ? ORDER BY started_at DESC', developer);
|
|
548
|
+
return rows.map((r) => this.rowToSession(r));
|
|
549
|
+
}
|
|
550
|
+
getInsightsByCategory() {
|
|
551
|
+
const rows = this.queryAll(`SELECT category, COUNT(*) as count FROM memories
|
|
552
|
+
WHERE tier = 'team' AND staleness < 1.0
|
|
553
|
+
GROUP BY category ORDER BY count DESC`);
|
|
554
|
+
const result = {};
|
|
555
|
+
for (const r of rows) {
|
|
556
|
+
result[r.category] = r.count;
|
|
557
|
+
}
|
|
558
|
+
return result;
|
|
559
|
+
}
|
|
560
|
+
getRecentInsights(limit = 20) {
|
|
561
|
+
const rows = this.queryAll(`SELECT * FROM memories WHERE tier = 'team' AND staleness < 1.0
|
|
562
|
+
ORDER BY created_at DESC LIMIT ?`, limit);
|
|
563
|
+
return rows.map((r) => this.rowToMemory(r));
|
|
564
|
+
}
|
|
565
|
+
// ── Optimized SQL Counts (for analytics) ──
|
|
566
|
+
getTeamInsightCount() {
|
|
567
|
+
const row = this.queryOne(`SELECT COUNT(*) as count FROM memories WHERE tier = 'team' AND staleness < 1.0`);
|
|
568
|
+
return row?.count ?? 0;
|
|
569
|
+
}
|
|
570
|
+
getTeamInsightCountSince(since) {
|
|
571
|
+
const row = this.queryOne(`SELECT COUNT(*) as count FROM memories WHERE tier = 'team' AND staleness < 1.0 AND created_at >= ?`, since);
|
|
572
|
+
return row?.count ?? 0;
|
|
573
|
+
}
|
|
574
|
+
getActiveSessionCount() {
|
|
575
|
+
const row = this.queryOne(`SELECT COUNT(*) as count FROM sessions WHERE status = 'active'`);
|
|
576
|
+
return row?.count ?? 0;
|
|
577
|
+
}
|
|
578
|
+
getTotalActivityCount() {
|
|
579
|
+
const row = this.queryOne(`SELECT COUNT(*) as count FROM activity_events`);
|
|
580
|
+
return row?.count ?? 0;
|
|
581
|
+
}
|
|
582
|
+
getAvgSessionDuration() {
|
|
583
|
+
const row = this.queryOne(`SELECT COALESCE(AVG(duration_seconds), 0) as avg FROM sessions WHERE duration_seconds IS NOT NULL`);
|
|
584
|
+
return Math.round(row?.avg ?? 0);
|
|
585
|
+
}
|
|
586
|
+
// ── Tracked Files ──
|
|
587
|
+
upsertTrackedFile(file) {
|
|
588
|
+
this.execute(`INSERT INTO tracked_files (path, hash, last_modified) VALUES (?, ?, ?)
|
|
589
|
+
ON CONFLICT(path) DO UPDATE SET hash = excluded.hash, last_modified = excluded.last_modified`, file.path, file.hash, file.lastModified);
|
|
590
|
+
}
|
|
591
|
+
getTrackedFile(filePath) {
|
|
592
|
+
const row = this.queryOne('SELECT * FROM tracked_files WHERE path = ?', filePath);
|
|
593
|
+
if (!row)
|
|
594
|
+
return null;
|
|
595
|
+
return { path: row.path, hash: row.hash, lastModified: row.last_modified };
|
|
596
|
+
}
|
|
597
|
+
removeTrackedFile(filePath) {
|
|
598
|
+
this.execute('DELETE FROM tracked_files WHERE path = ?', filePath);
|
|
599
|
+
}
|
|
600
|
+
// ── Tracked Commits ──
|
|
601
|
+
upsertCommit(commit) {
|
|
602
|
+
this.execute(`INSERT INTO tracked_commits (sha, message, author, date, files, processed) VALUES (?, ?, ?, ?, ?, ?)
|
|
603
|
+
ON CONFLICT(sha) DO UPDATE SET processed = excluded.processed`, commit.sha, commit.message, commit.author, commit.date, JSON.stringify(commit.files), commit.processed ? 1 : 0);
|
|
604
|
+
}
|
|
605
|
+
getUnprocessedCommits() {
|
|
606
|
+
const rows = this.queryAll('SELECT * FROM tracked_commits WHERE processed = 0 ORDER BY date ASC');
|
|
607
|
+
return rows.map((row) => ({
|
|
608
|
+
sha: row.sha,
|
|
609
|
+
message: row.message,
|
|
610
|
+
author: row.author,
|
|
611
|
+
date: row.date,
|
|
612
|
+
files: JSON.parse(row.files),
|
|
613
|
+
processed: Boolean(row.processed),
|
|
614
|
+
}));
|
|
615
|
+
}
|
|
616
|
+
markCommitProcessed(sha) {
|
|
617
|
+
this.execute('UPDATE tracked_commits SET processed = 1 WHERE sha = ?', sha);
|
|
618
|
+
}
|
|
619
|
+
// ── Helpers ──
|
|
620
|
+
rowToMemory(row) {
|
|
621
|
+
return {
|
|
622
|
+
id: row.id,
|
|
623
|
+
content: row.content,
|
|
624
|
+
category: row.category,
|
|
625
|
+
source: row.source,
|
|
626
|
+
tier: (row.tier ?? 'personal'),
|
|
627
|
+
author: row.author ?? 'unknown',
|
|
628
|
+
tags: JSON.parse(row.tags),
|
|
629
|
+
relatedFiles: JSON.parse(row.related_files),
|
|
630
|
+
commitSha: row.commit_sha,
|
|
631
|
+
staleness: row.staleness,
|
|
632
|
+
confidence: row.confidence,
|
|
633
|
+
embedding: row.embedding ? Array.from(new Float32Array(row.embedding.buffer)) : null,
|
|
634
|
+
scope: row.scope ? JSON.parse(row.scope) : null,
|
|
635
|
+
priority: row.priority ?? null,
|
|
636
|
+
examples: row.examples ? JSON.parse(row.examples) : null,
|
|
637
|
+
active: row.active === undefined ? true : Boolean(row.active),
|
|
638
|
+
sessionId: row.session_id ?? null,
|
|
639
|
+
reuseCount: row.reuse_count ?? 0,
|
|
640
|
+
createdAt: row.created_at,
|
|
641
|
+
updatedAt: row.updated_at,
|
|
642
|
+
validatedAt: row.validated_at,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
rowToSession(row) {
|
|
646
|
+
return {
|
|
647
|
+
id: row.id,
|
|
648
|
+
developer: row.developer,
|
|
649
|
+
task: row.task ?? '',
|
|
650
|
+
status: row.status ?? 'active',
|
|
651
|
+
toolName: row.tool_name ?? 'unknown',
|
|
652
|
+
startedAt: row.started_at,
|
|
653
|
+
endedAt: row.ended_at ?? null,
|
|
654
|
+
durationSeconds: row.duration_seconds ?? null,
|
|
655
|
+
filesTouched: JSON.parse(row.files_touched ?? '[]'),
|
|
656
|
+
summary: row.summary ?? null,
|
|
657
|
+
insightCount: row.insight_count ?? 0,
|
|
658
|
+
activityCount: row.activity_count ?? 0,
|
|
659
|
+
duplicatesPrevented: row.duplicates_prevented ?? 0,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
rowToActivity(row) {
|
|
663
|
+
return {
|
|
664
|
+
id: row.id,
|
|
665
|
+
sessionId: row.session_id,
|
|
666
|
+
type: row.type,
|
|
667
|
+
description: row.description ?? '',
|
|
668
|
+
files: JSON.parse(row.files ?? '[]'),
|
|
669
|
+
timestamp: row.timestamp,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
close() {
|
|
673
|
+
this.flush();
|
|
674
|
+
this.db.close();
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
//# sourceMappingURL=database.js.map
|