compass-agent 2.0.4

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/src/db.ts ADDED
@@ -0,0 +1,398 @@
1
+ import { Database } from "bun:sqlite";
2
+ import path from "path";
3
+ import type {
4
+ SessionRow, CwdHistoryRow, ChannelDefaultRow, TeachingRow,
5
+ ReminderRow, UsageLogRow, WorktreeRow, TeachingCountRow,
6
+ } from "./types.ts";
7
+
8
+ // ── Factory ─────────────────────────────────────────────────
9
+
10
+ export function createDatabase(dbPath: string) {
11
+ const db = new Database(dbPath);
12
+ db.exec("PRAGMA journal_mode = WAL");
13
+
14
+ // ── Core tables ───────────────────────────────────────────
15
+ db.exec(`
16
+ CREATE TABLE IF NOT EXISTS sessions (
17
+ channel_id TEXT PRIMARY KEY,
18
+ session_id TEXT NOT NULL,
19
+ persisted INTEGER DEFAULT 0,
20
+ created_at TEXT DEFAULT (datetime('now')),
21
+ updated_at TEXT DEFAULT (datetime('now'))
22
+ )
23
+ `);
24
+
25
+ // Migrations for sessions table
26
+ try { db.exec("ALTER TABLE sessions ADD COLUMN persisted INTEGER DEFAULT 0"); } catch {}
27
+ try { db.exec("ALTER TABLE sessions ADD COLUMN cwd TEXT DEFAULT NULL"); } catch {}
28
+ try { db.exec("ALTER TABLE sessions ADD COLUMN user_id TEXT DEFAULT NULL"); } catch {}
29
+
30
+ db.exec(`
31
+ CREATE TABLE IF NOT EXISTS cwd_history (
32
+ path TEXT PRIMARY KEY,
33
+ last_used TEXT DEFAULT (datetime('now'))
34
+ )
35
+ `);
36
+
37
+ db.exec(`
38
+ CREATE TABLE IF NOT EXISTS channel_defaults (
39
+ channel_id TEXT PRIMARY KEY,
40
+ cwd TEXT NOT NULL,
41
+ set_by TEXT,
42
+ updated_at TEXT DEFAULT (datetime('now'))
43
+ )
44
+ `);
45
+
46
+ // ── Phase 1 tables ────────────────────────────────────────
47
+ db.exec(`
48
+ CREATE TABLE IF NOT EXISTS team_knowledge (
49
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
50
+ instruction TEXT NOT NULL,
51
+ added_by TEXT NOT NULL,
52
+ workspace_id TEXT DEFAULT 'default',
53
+ created_at TEXT DEFAULT (datetime('now')),
54
+ active INTEGER DEFAULT 1
55
+ )
56
+ `);
57
+
58
+ db.exec(`
59
+ CREATE TABLE IF NOT EXISTS annotations (
60
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
61
+ file_path TEXT NOT NULL,
62
+ line_start INTEGER,
63
+ line_end INTEGER,
64
+ content TEXT NOT NULL,
65
+ category TEXT DEFAULT 'general',
66
+ commit_hash TEXT,
67
+ added_by TEXT NOT NULL,
68
+ session_key TEXT,
69
+ created_at TEXT DEFAULT (datetime('now')),
70
+ active INTEGER DEFAULT 1
71
+ )
72
+ `);
73
+
74
+ db.exec(`
75
+ CREATE TABLE IF NOT EXISTS usage_logs (
76
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
77
+ session_key TEXT NOT NULL,
78
+ user_id TEXT NOT NULL,
79
+ model TEXT,
80
+ input_tokens INTEGER DEFAULT 0,
81
+ output_tokens INTEGER DEFAULT 0,
82
+ total_cost_usd REAL DEFAULT 0,
83
+ duration_ms INTEGER DEFAULT 0,
84
+ num_turns INTEGER DEFAULT 0,
85
+ created_at TEXT DEFAULT (datetime('now'))
86
+ )
87
+ `);
88
+
89
+ db.exec(`
90
+ CREATE TABLE IF NOT EXISTS shared_sessions (
91
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
92
+ share_code TEXT UNIQUE NOT NULL,
93
+ session_key TEXT NOT NULL,
94
+ channel_id TEXT NOT NULL,
95
+ shared_by TEXT NOT NULL,
96
+ summary TEXT,
97
+ created_at TEXT DEFAULT (datetime('now')),
98
+ expires_at TEXT
99
+ )
100
+ `);
101
+
102
+ db.exec(`
103
+ CREATE TABLE IF NOT EXISTS watched_channels (
104
+ channel_id TEXT PRIMARY KEY,
105
+ added_by TEXT NOT NULL,
106
+ watch_mode TEXT DEFAULT 'errors',
107
+ rate_limit_minutes INTEGER DEFAULT 30,
108
+ last_triggered_at TEXT,
109
+ created_at TEXT DEFAULT (datetime('now')),
110
+ active INTEGER DEFAULT 1
111
+ )
112
+ `);
113
+
114
+ db.exec(`
115
+ CREATE TABLE IF NOT EXISTS snapshots (
116
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
117
+ session_key TEXT NOT NULL,
118
+ snapshot_name TEXT,
119
+ git_stash_ref TEXT,
120
+ git_branch TEXT,
121
+ worktree_path TEXT,
122
+ files_changed TEXT,
123
+ created_by TEXT NOT NULL,
124
+ created_at TEXT DEFAULT (datetime('now'))
125
+ )
126
+ `);
127
+
128
+ db.exec(`
129
+ CREATE TABLE IF NOT EXISTS mcp_configs (
130
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
131
+ server_name TEXT NOT NULL,
132
+ config_json TEXT NOT NULL,
133
+ workspace_id TEXT DEFAULT 'default',
134
+ added_by TEXT NOT NULL,
135
+ enabled INTEGER DEFAULT 1,
136
+ created_at TEXT DEFAULT (datetime('now')),
137
+ updated_at TEXT DEFAULT (datetime('now')),
138
+ UNIQUE(server_name, workspace_id)
139
+ )
140
+ `);
141
+
142
+ db.exec(`
143
+ CREATE TABLE IF NOT EXISTS feedback (
144
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
145
+ session_key TEXT NOT NULL,
146
+ user_id TEXT NOT NULL,
147
+ sentiment TEXT NOT NULL,
148
+ message_ts TEXT,
149
+ created_at TEXT DEFAULT (datetime('now'))
150
+ )
151
+ `);
152
+
153
+ db.exec(`
154
+ CREATE TABLE IF NOT EXISTS worktrees (
155
+ session_key TEXT PRIMARY KEY,
156
+ repo_path TEXT NOT NULL,
157
+ worktree_path TEXT NOT NULL,
158
+ branch_name TEXT NOT NULL,
159
+ locked INTEGER DEFAULT 0,
160
+ created_at TEXT DEFAULT (datetime('now')),
161
+ last_active_at TEXT DEFAULT (datetime('now')),
162
+ cleaned_up INTEGER DEFAULT 0
163
+ )
164
+ `);
165
+
166
+ db.exec(`
167
+ CREATE TABLE IF NOT EXISTS reminders (
168
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
169
+ channel_id TEXT NOT NULL,
170
+ user_id TEXT NOT NULL,
171
+ bot_id TEXT NOT NULL,
172
+ content TEXT NOT NULL,
173
+ original_input TEXT NOT NULL,
174
+ cron_expression TEXT,
175
+ one_time INTEGER DEFAULT 0,
176
+ next_trigger_at TEXT NOT NULL,
177
+ active INTEGER DEFAULT 1,
178
+ created_at TEXT DEFAULT (datetime('now'))
179
+ )
180
+ `);
181
+
182
+ // ── Prepared statements ───────────────────────────────────
183
+ const _getSession = db.prepare("SELECT * FROM sessions WHERE channel_id = ?");
184
+ const _upsertSession = db.prepare(`
185
+ INSERT INTO sessions (channel_id, session_id, persisted)
186
+ VALUES (?, ?, 0)
187
+ ON CONFLICT(channel_id) DO UPDATE SET
188
+ session_id = excluded.session_id,
189
+ persisted = 0,
190
+ updated_at = datetime('now')
191
+ `);
192
+ const _markPersisted = db.prepare(
193
+ "UPDATE sessions SET persisted = 1, updated_at = datetime('now') WHERE channel_id = ?"
194
+ );
195
+ const _deleteSession = db.prepare("DELETE FROM sessions WHERE channel_id = ?");
196
+ const _setCwd = db.prepare(
197
+ "UPDATE sessions SET cwd = ?, updated_at = datetime('now') WHERE channel_id = ?"
198
+ );
199
+ const _getCwdHistory = db.prepare(
200
+ "SELECT path, last_used FROM cwd_history ORDER BY last_used DESC"
201
+ );
202
+ const _addCwdHistory = db.prepare(`
203
+ INSERT INTO cwd_history (path, last_used) VALUES (?, datetime('now'))
204
+ ON CONFLICT(path) DO UPDATE SET last_used = datetime('now')
205
+ `);
206
+ const _getAllActiveSessions = db.prepare(
207
+ "SELECT * FROM sessions ORDER BY updated_at DESC LIMIT 20"
208
+ );
209
+ const _getChannelDefault = db.prepare(
210
+ "SELECT * FROM channel_defaults WHERE channel_id = ?"
211
+ );
212
+ const _setChannelDefault = db.prepare(`
213
+ INSERT INTO channel_defaults (channel_id, cwd, set_by)
214
+ VALUES (?, ?, ?)
215
+ ON CONFLICT(channel_id) DO UPDATE SET
216
+ cwd = excluded.cwd,
217
+ set_by = excluded.set_by,
218
+ updated_at = datetime('now')
219
+ `);
220
+ const _addReminder = db.prepare(`
221
+ INSERT INTO reminders (channel_id, user_id, bot_id, content, original_input, cron_expression, one_time, next_trigger_at)
222
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
223
+ `);
224
+ const _getDueReminders = db.prepare(
225
+ "SELECT * FROM reminders WHERE active = 1 AND next_trigger_at <= datetime('now')"
226
+ );
227
+ const _updateNextTrigger = db.prepare(
228
+ "UPDATE reminders SET next_trigger_at = ? WHERE id = ?"
229
+ );
230
+ const _deactivateReminder = db.prepare(
231
+ "UPDATE reminders SET active = 0 WHERE id = ?"
232
+ );
233
+ const _getActiveReminders = db.prepare(
234
+ "SELECT * FROM reminders WHERE active = 1 AND user_id = ? ORDER BY next_trigger_at"
235
+ );
236
+ const _addTeaching = db.prepare(`
237
+ INSERT INTO team_knowledge (instruction, added_by, workspace_id)
238
+ VALUES (?, ?, ?)
239
+ `);
240
+ const _getTeachings = db.prepare(
241
+ "SELECT id, instruction, added_by, created_at FROM team_knowledge WHERE workspace_id = ? AND active = 1 ORDER BY id"
242
+ );
243
+ const _removeTeaching = db.prepare(
244
+ "UPDATE team_knowledge SET active = 0 WHERE id = ?"
245
+ );
246
+ const _getTeachingCount = db.prepare(
247
+ "SELECT COUNT(*) as count FROM team_knowledge WHERE workspace_id = ? AND active = 1"
248
+ );
249
+ const _addUsageLog = db.prepare(`
250
+ INSERT INTO usage_logs (session_key, user_id, model, input_tokens, output_tokens, total_cost_usd, duration_ms, num_turns)
251
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
252
+ `);
253
+ const _getRecentUsage = db.prepare(
254
+ "SELECT * FROM usage_logs ORDER BY created_at DESC LIMIT ?"
255
+ );
256
+ const _upsertWorktree = db.prepare(`
257
+ INSERT INTO worktrees (session_key, repo_path, worktree_path, branch_name)
258
+ VALUES (?, ?, ?, ?)
259
+ ON CONFLICT(session_key) DO UPDATE SET
260
+ repo_path = excluded.repo_path,
261
+ worktree_path = excluded.worktree_path,
262
+ branch_name = excluded.branch_name,
263
+ last_active_at = datetime('now'),
264
+ cleaned_up = 0
265
+ `);
266
+ const _getWorktree = db.prepare(
267
+ "SELECT * FROM worktrees WHERE session_key = ?"
268
+ );
269
+ const _touchWorktree = db.prepare(
270
+ "UPDATE worktrees SET last_active_at = datetime('now') WHERE session_key = ?"
271
+ );
272
+ const _markWorktreeCleaned = db.prepare(
273
+ "UPDATE worktrees SET cleaned_up = 1 WHERE session_key = ?"
274
+ );
275
+ const _getStaleWorktrees = db.prepare(
276
+ "SELECT * FROM worktrees WHERE cleaned_up = 0 AND last_active_at < datetime('now', '-' || ? || ' minutes')"
277
+ );
278
+ const _getActiveWorktrees = db.prepare(
279
+ "SELECT * FROM worktrees WHERE cleaned_up = 0"
280
+ );
281
+ const _addFeedback = db.prepare(`
282
+ INSERT INTO feedback (session_key, user_id, sentiment, message_ts)
283
+ VALUES (?, ?, ?, ?)
284
+ `);
285
+
286
+ // ── Return all accessors ──────────────────────────────────
287
+ return {
288
+ db,
289
+ getSession(channelId: string): SessionRow | null {
290
+ return _getSession.get(channelId) as SessionRow | null;
291
+ },
292
+ upsertSession(channelId: string, sessionId: string): void {
293
+ _upsertSession.run(channelId, sessionId);
294
+ },
295
+ markPersisted(channelId: string): void {
296
+ _markPersisted.run(channelId);
297
+ },
298
+ deleteSession(channelId: string): void {
299
+ _deleteSession.run(channelId);
300
+ },
301
+ setCwd(channelId: string, cwd: string): void {
302
+ _setCwd.run(cwd, channelId);
303
+ },
304
+ getCwdHistory(): CwdHistoryRow[] {
305
+ return _getCwdHistory.all() as CwdHistoryRow[];
306
+ },
307
+ addCwdHistory(p: string): void {
308
+ _addCwdHistory.run(p);
309
+ },
310
+ getAllActiveSessions(): SessionRow[] {
311
+ return _getAllActiveSessions.all() as SessionRow[];
312
+ },
313
+ getChannelDefault(channelId: string): ChannelDefaultRow | null {
314
+ return _getChannelDefault.get(channelId) as ChannelDefaultRow | null;
315
+ },
316
+ setChannelDefault(channelId: string, cwd: string, setBy: string): void {
317
+ _setChannelDefault.run(channelId, cwd, setBy);
318
+ },
319
+ addTeaching(instruction: string, addedBy: string, workspaceId: string = "default"): void {
320
+ _addTeaching.run(instruction, addedBy, workspaceId);
321
+ },
322
+ getTeachings(workspaceId: string = "default"): TeachingRow[] {
323
+ return _getTeachings.all(workspaceId) as TeachingRow[];
324
+ },
325
+ removeTeaching(id: number): void {
326
+ _removeTeaching.run(id);
327
+ },
328
+ getTeachingCount(workspaceId: string = "default"): TeachingCountRow {
329
+ return _getTeachingCount.get(workspaceId) as TeachingCountRow;
330
+ },
331
+ addUsageLog(
332
+ sessionKey: string, userId: string, model: string | null,
333
+ inputTokens: number, outputTokens: number, cost: number,
334
+ durationMs: number, numTurns: number
335
+ ): void {
336
+ _addUsageLog.run(sessionKey, userId, model, inputTokens, outputTokens, cost, durationMs, numTurns);
337
+ },
338
+ getRecentUsage(limit: number = 10): UsageLogRow[] {
339
+ return _getRecentUsage.all(limit) as UsageLogRow[];
340
+ },
341
+ upsertWorktree(sessionKey: string, repoPath: string, worktreePath: string, branchName: string): void {
342
+ _upsertWorktree.run(sessionKey, repoPath, worktreePath, branchName);
343
+ },
344
+ getWorktree(sessionKey: string): WorktreeRow | null {
345
+ return _getWorktree.get(sessionKey) as WorktreeRow | null;
346
+ },
347
+ touchWorktree(sessionKey: string): void {
348
+ _touchWorktree.run(sessionKey);
349
+ },
350
+ markWorktreeCleaned(sessionKey: string): void {
351
+ _markWorktreeCleaned.run(sessionKey);
352
+ },
353
+ getStaleWorktrees(idleMinutes: number): WorktreeRow[] {
354
+ return _getStaleWorktrees.all(idleMinutes) as WorktreeRow[];
355
+ },
356
+ getActiveWorktrees(): WorktreeRow[] {
357
+ return _getActiveWorktrees.all() as WorktreeRow[];
358
+ },
359
+ addFeedback(sessionKey: string, userId: string, sentiment: string, messageTs: string): void {
360
+ _addFeedback.run(sessionKey, userId, sentiment, messageTs);
361
+ },
362
+ addReminder(
363
+ channelId: string, userId: string, botId: string, content: string,
364
+ originalInput: string, cronExpression: string | null, oneTime: number, nextTriggerAt: string
365
+ ): void {
366
+ _addReminder.run(channelId, userId, botId, content, originalInput, cronExpression, oneTime, nextTriggerAt);
367
+ },
368
+ getDueReminders(): ReminderRow[] {
369
+ return _getDueReminders.all() as ReminderRow[];
370
+ },
371
+ updateNextTrigger(nextTriggerAt: string, id: number): void {
372
+ _updateNextTrigger.run(nextTriggerAt, id);
373
+ },
374
+ deactivateReminder(id: number): void {
375
+ _deactivateReminder.run(id);
376
+ },
377
+ getActiveReminders(userId: string): ReminderRow[] {
378
+ return _getActiveReminders.all(userId) as ReminderRow[];
379
+ },
380
+ };
381
+ }
382
+
383
+ // ── Default instance (production) ───────────────────────────
384
+
385
+ const defaultDb = createDatabase(path.join(import.meta.dir, "..", "sessions.db"));
386
+
387
+ export const {
388
+ db,
389
+ getSession, upsertSession, markPersisted, deleteSession,
390
+ setCwd, getCwdHistory, addCwdHistory, getAllActiveSessions,
391
+ getChannelDefault, setChannelDefault,
392
+ addTeaching, getTeachings, removeTeaching, getTeachingCount,
393
+ addUsageLog, getRecentUsage,
394
+ upsertWorktree, getWorktree, touchWorktree, markWorktreeCleaned,
395
+ getStaleWorktrees, getActiveWorktrees,
396
+ addFeedback,
397
+ addReminder, getDueReminders, updateNextTrigger, deactivateReminder, getActiveReminders,
398
+ } = defaultDb;