claudeck 1.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 (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/cli.js +2 -0
  4. package/config/agent-chains.json +16 -0
  5. package/config/agent-dags.json +16 -0
  6. package/config/agents.json +46 -0
  7. package/config/bot-prompt.json +3 -0
  8. package/config/folders.json +66 -0
  9. package/config/prompts.json +92 -0
  10. package/config/repos.json +86 -0
  11. package/config/telegram-config.json +17 -0
  12. package/config/workflows.json +90 -0
  13. package/db.js +1198 -0
  14. package/package.json +55 -0
  15. package/plugins/claude-editor/client.css +171 -0
  16. package/plugins/claude-editor/client.js +183 -0
  17. package/plugins/event-stream/client.css +207 -0
  18. package/plugins/event-stream/client.js +271 -0
  19. package/plugins/linear/client.css +345 -0
  20. package/plugins/linear/client.js +380 -0
  21. package/plugins/linear/config.json +5 -0
  22. package/plugins/linear/server.js +312 -0
  23. package/plugins/repos/client.css +549 -0
  24. package/plugins/repos/client.js +663 -0
  25. package/plugins/repos/server.js +232 -0
  26. package/plugins/sudoku/client.css +196 -0
  27. package/plugins/sudoku/client.js +329 -0
  28. package/plugins/tasks/client.css +414 -0
  29. package/plugins/tasks/client.js +394 -0
  30. package/plugins/tasks/server.js +116 -0
  31. package/plugins/tic-tac-toe/client.css +167 -0
  32. package/plugins/tic-tac-toe/client.js +241 -0
  33. package/public/css/core/components.css +232 -0
  34. package/public/css/core/layout.css +330 -0
  35. package/public/css/core/print.css +18 -0
  36. package/public/css/core/reset.css +36 -0
  37. package/public/css/core/responsive.css +378 -0
  38. package/public/css/core/theme.css +116 -0
  39. package/public/css/core/variables.css +93 -0
  40. package/public/css/features/agent-monitor.css +297 -0
  41. package/public/css/features/agent-sidebar.css +525 -0
  42. package/public/css/features/agents.css +996 -0
  43. package/public/css/features/analytics.css +181 -0
  44. package/public/css/features/background-sessions.css +321 -0
  45. package/public/css/features/cost-dashboard.css +168 -0
  46. package/public/css/features/home.css +313 -0
  47. package/public/css/features/retro-terminal.css +88 -0
  48. package/public/css/features/telegram.css +127 -0
  49. package/public/css/features/tour.css +148 -0
  50. package/public/css/features/voice-input.css +60 -0
  51. package/public/css/features/welcome.css +241 -0
  52. package/public/css/panels/assistant-bot.css +442 -0
  53. package/public/css/panels/dev-docs.css +292 -0
  54. package/public/css/panels/file-explorer.css +322 -0
  55. package/public/css/panels/git-panel.css +221 -0
  56. package/public/css/panels/mcp-manager.css +199 -0
  57. package/public/css/panels/tips-feed.css +353 -0
  58. package/public/css/ui/commands.css +273 -0
  59. package/public/css/ui/context-gauge.css +76 -0
  60. package/public/css/ui/file-picker.css +69 -0
  61. package/public/css/ui/image-attachments.css +106 -0
  62. package/public/css/ui/messages.css +884 -0
  63. package/public/css/ui/modals.css +122 -0
  64. package/public/css/ui/parallel.css +217 -0
  65. package/public/css/ui/permissions.css +110 -0
  66. package/public/css/ui/right-panel.css +481 -0
  67. package/public/css/ui/sessions.css +689 -0
  68. package/public/css/ui/status-bar.css +425 -0
  69. package/public/css/ui/toolbox.css +206 -0
  70. package/public/data/tips.json +218 -0
  71. package/public/icons/favicon.png +0 -0
  72. package/public/icons/icon-192.png +0 -0
  73. package/public/icons/icon-512.png +0 -0
  74. package/public/icons/whaly.png +0 -0
  75. package/public/index.html +1140 -0
  76. package/public/js/core/api.js +591 -0
  77. package/public/js/core/constants.js +3 -0
  78. package/public/js/core/dom.js +270 -0
  79. package/public/js/core/events.js +10 -0
  80. package/public/js/core/plugin-loader.js +153 -0
  81. package/public/js/core/store.js +39 -0
  82. package/public/js/core/utils.js +25 -0
  83. package/public/js/core/ws.js +64 -0
  84. package/public/js/features/agent-monitor.js +222 -0
  85. package/public/js/features/agents.js +1209 -0
  86. package/public/js/features/analytics.js +397 -0
  87. package/public/js/features/attachments.js +251 -0
  88. package/public/js/features/background-sessions.js +475 -0
  89. package/public/js/features/chat.js +589 -0
  90. package/public/js/features/cost-dashboard.js +152 -0
  91. package/public/js/features/dag-editor.js +399 -0
  92. package/public/js/features/easter-egg.js +46 -0
  93. package/public/js/features/home.js +270 -0
  94. package/public/js/features/projects.js +372 -0
  95. package/public/js/features/prompts.js +228 -0
  96. package/public/js/features/sessions.js +332 -0
  97. package/public/js/features/telegram.js +131 -0
  98. package/public/js/features/tour.js +210 -0
  99. package/public/js/features/voice-input.js +185 -0
  100. package/public/js/features/welcome.js +43 -0
  101. package/public/js/features/workflows.js +277 -0
  102. package/public/js/main.js +51 -0
  103. package/public/js/panels/assistant-bot.js +445 -0
  104. package/public/js/panels/dev-docs.js +380 -0
  105. package/public/js/panels/file-explorer.js +486 -0
  106. package/public/js/panels/git-panel.js +285 -0
  107. package/public/js/panels/mcp-manager.js +311 -0
  108. package/public/js/panels/tips-feed.js +303 -0
  109. package/public/js/ui/commands.js +114 -0
  110. package/public/js/ui/context-gauge.js +100 -0
  111. package/public/js/ui/diff.js +124 -0
  112. package/public/js/ui/disabled-tools.js +36 -0
  113. package/public/js/ui/export.js +74 -0
  114. package/public/js/ui/formatting.js +206 -0
  115. package/public/js/ui/header-dropdowns.js +72 -0
  116. package/public/js/ui/input-meta.js +71 -0
  117. package/public/js/ui/max-turns.js +21 -0
  118. package/public/js/ui/messages.js +387 -0
  119. package/public/js/ui/model-selector.js +20 -0
  120. package/public/js/ui/notifications.js +232 -0
  121. package/public/js/ui/parallel.js +176 -0
  122. package/public/js/ui/permissions.js +168 -0
  123. package/public/js/ui/right-panel.js +173 -0
  124. package/public/js/ui/shortcuts.js +143 -0
  125. package/public/js/ui/sidebar-toggle.js +29 -0
  126. package/public/js/ui/status-bar.js +172 -0
  127. package/public/js/ui/tab-sdk.js +623 -0
  128. package/public/js/ui/theme.js +38 -0
  129. package/public/manifest.json +13 -0
  130. package/public/offline.html +190 -0
  131. package/public/style.css +42 -0
  132. package/public/sw.js +91 -0
  133. package/server/agent-loop.js +385 -0
  134. package/server/dag-executor.js +265 -0
  135. package/server/orchestrator.js +514 -0
  136. package/server/paths.js +61 -0
  137. package/server/plugin-mount.js +56 -0
  138. package/server/push-sender.js +31 -0
  139. package/server/routes/agents.js +294 -0
  140. package/server/routes/bot.js +45 -0
  141. package/server/routes/exec.js +35 -0
  142. package/server/routes/files.js +218 -0
  143. package/server/routes/mcp.js +82 -0
  144. package/server/routes/messages.js +36 -0
  145. package/server/routes/notifications.js +37 -0
  146. package/server/routes/projects.js +207 -0
  147. package/server/routes/prompts.js +53 -0
  148. package/server/routes/sessions.js +103 -0
  149. package/server/routes/stats.js +143 -0
  150. package/server/routes/telegram.js +71 -0
  151. package/server/routes/tips.js +135 -0
  152. package/server/routes/workflows.js +81 -0
  153. package/server/summarizer.js +55 -0
  154. package/server/telegram-poller.js +205 -0
  155. package/server/telegram-sender.js +304 -0
  156. package/server/ws-handler.js +926 -0
  157. package/server.js +179 -0
package/db.js ADDED
@@ -0,0 +1,1198 @@
1
+ import Database from "better-sqlite3";
2
+ import { dbPath } from "./server/paths.js";
3
+
4
+ const db = new Database(dbPath);
5
+
6
+ // Enable WAL mode for better concurrent performance
7
+ db.pragma("journal_mode = WAL");
8
+
9
+ // Create tables
10
+ db.exec(`
11
+ CREATE TABLE IF NOT EXISTS sessions (
12
+ id TEXT PRIMARY KEY,
13
+ claude_session_id TEXT,
14
+ project_name TEXT,
15
+ project_path TEXT,
16
+ created_at INTEGER DEFAULT (unixepoch()),
17
+ last_used_at INTEGER DEFAULT (unixepoch())
18
+ );
19
+
20
+ CREATE TABLE IF NOT EXISTS costs (
21
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
22
+ session_id TEXT REFERENCES sessions(id),
23
+ cost_usd REAL,
24
+ duration_ms INTEGER,
25
+ num_turns INTEGER,
26
+ created_at INTEGER DEFAULT (unixepoch())
27
+ );
28
+
29
+ CREATE TABLE IF NOT EXISTS messages (
30
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
31
+ session_id TEXT REFERENCES sessions(id),
32
+ role TEXT NOT NULL,
33
+ content TEXT NOT NULL,
34
+ created_at INTEGER DEFAULT (unixepoch())
35
+ );
36
+
37
+ CREATE TABLE IF NOT EXISTS claude_sessions (
38
+ session_id TEXT NOT NULL,
39
+ chat_id TEXT NOT NULL DEFAULT '',
40
+ claude_session_id TEXT NOT NULL,
41
+ PRIMARY KEY (session_id, chat_id)
42
+ );
43
+
44
+ CREATE TABLE IF NOT EXISTS push_subscriptions (
45
+ endpoint TEXT PRIMARY KEY,
46
+ keys_p256dh TEXT NOT NULL,
47
+ keys_auth TEXT NOT NULL,
48
+ created_at INTEGER DEFAULT (unixepoch())
49
+ );
50
+
51
+ CREATE TABLE IF NOT EXISTS todos (
52
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
53
+ text TEXT NOT NULL,
54
+ done INTEGER DEFAULT 0,
55
+ position INTEGER DEFAULT 0,
56
+ created_at INTEGER DEFAULT (unixepoch()),
57
+ updated_at INTEGER DEFAULT (unixepoch())
58
+ );
59
+ `);
60
+
61
+ // Migrations
62
+ try { db.exec(`ALTER TABLE messages ADD COLUMN chat_id TEXT DEFAULT NULL`); } catch { /* exists */ }
63
+ try { db.exec(`ALTER TABLE sessions ADD COLUMN title TEXT DEFAULT NULL`); } catch { /* exists */ }
64
+ try { db.exec(`ALTER TABLE sessions ADD COLUMN pinned INTEGER DEFAULT 0`); } catch { /* exists */ }
65
+ try { db.exec(`ALTER TABLE costs ADD COLUMN input_tokens INTEGER DEFAULT 0`); } catch { /* exists */ }
66
+ try { db.exec(`ALTER TABLE costs ADD COLUMN output_tokens INTEGER DEFAULT 0`); } catch { /* exists */ }
67
+ // New columns for costs table
68
+ try { db.exec(`ALTER TABLE costs ADD COLUMN model TEXT DEFAULT NULL`); } catch { /* exists */ }
69
+ try { db.exec(`ALTER TABLE costs ADD COLUMN stop_reason TEXT DEFAULT NULL`); } catch { /* exists */ }
70
+ try { db.exec(`ALTER TABLE costs ADD COLUMN is_error INTEGER DEFAULT 0`); } catch { /* exists */ }
71
+ try { db.exec(`ALTER TABLE costs ADD COLUMN cache_read_tokens INTEGER DEFAULT 0`); } catch { /* exists */ }
72
+ try { db.exec(`ALTER TABLE costs ADD COLUMN cache_creation_tokens INTEGER DEFAULT 0`); } catch { /* exists */ }
73
+ // New columns for messages table (workflow metadata)
74
+ try { db.exec(`ALTER TABLE messages ADD COLUMN workflow_id TEXT DEFAULT NULL`); } catch { /* exists */ }
75
+ try { db.exec(`ALTER TABLE messages ADD COLUMN workflow_step_index INTEGER DEFAULT NULL`); } catch { /* exists */ }
76
+ try { db.exec(`ALTER TABLE messages ADD COLUMN workflow_step_label TEXT DEFAULT NULL`); } catch { /* exists */ }
77
+ // AI-generated session summary
78
+ try { db.exec(`ALTER TABLE sessions ADD COLUMN summary TEXT DEFAULT NULL`); } catch { /* exists */ }
79
+ // Todo archive
80
+ try { db.exec(`ALTER TABLE todos ADD COLUMN archived INTEGER DEFAULT 0`); } catch { /* exists */ }
81
+ // Todo priority (0=none, 1=low, 2=medium, 3=high)
82
+ try { db.exec(`ALTER TABLE todos ADD COLUMN priority INTEGER DEFAULT 0`); } catch { /* exists */ }
83
+
84
+ // Agent context (shared memory between agents in a chain/orchestration run)
85
+ db.exec(`
86
+ CREATE TABLE IF NOT EXISTS agent_context (
87
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
88
+ run_id TEXT NOT NULL,
89
+ agent_id TEXT NOT NULL,
90
+ key TEXT NOT NULL,
91
+ value TEXT NOT NULL,
92
+ created_at INTEGER DEFAULT (unixepoch()),
93
+ UNIQUE(run_id, agent_id, key)
94
+ );
95
+ CREATE INDEX IF NOT EXISTS idx_agent_context_run ON agent_context(run_id);
96
+ `);
97
+
98
+ // Agent runs table (monitoring dashboard)
99
+ db.exec(`
100
+ CREATE TABLE IF NOT EXISTS agent_runs (
101
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
102
+ run_id TEXT NOT NULL,
103
+ agent_id TEXT NOT NULL,
104
+ agent_title TEXT NOT NULL,
105
+ run_type TEXT NOT NULL DEFAULT 'single',
106
+ parent_id TEXT,
107
+ status TEXT NOT NULL DEFAULT 'running',
108
+ turns INTEGER DEFAULT 0,
109
+ cost_usd REAL DEFAULT 0,
110
+ duration_ms INTEGER DEFAULT 0,
111
+ input_tokens INTEGER DEFAULT 0,
112
+ output_tokens INTEGER DEFAULT 0,
113
+ error TEXT,
114
+ started_at INTEGER DEFAULT (unixepoch()),
115
+ completed_at INTEGER
116
+ );
117
+ CREATE INDEX IF NOT EXISTS idx_agent_runs_agent ON agent_runs(agent_id);
118
+ CREATE INDEX IF NOT EXISTS idx_agent_runs_started ON agent_runs(started_at);
119
+ CREATE INDEX IF NOT EXISTS idx_agent_runs_run_id ON agent_runs(run_id);
120
+ `);
121
+
122
+ // Brags table
123
+ db.exec(`
124
+ CREATE TABLE IF NOT EXISTS brags (
125
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
126
+ todo_id INTEGER REFERENCES todos(id),
127
+ text TEXT NOT NULL,
128
+ summary TEXT NOT NULL,
129
+ created_at INTEGER DEFAULT (unixepoch())
130
+ );
131
+ `);
132
+
133
+ // Indexes for query performance
134
+ db.exec(`
135
+ CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id);
136
+ CREATE INDEX IF NOT EXISTS idx_messages_session_chat ON messages(session_id, chat_id);
137
+ CREATE INDEX IF NOT EXISTS idx_costs_session_id ON costs(session_id);
138
+ CREATE INDEX IF NOT EXISTS idx_costs_created_at ON costs(created_at);
139
+ CREATE INDEX IF NOT EXISTS idx_sessions_project_path ON sessions(project_path);
140
+ CREATE INDEX IF NOT EXISTS idx_sessions_pinned_last_used ON sessions(pinned DESC, last_used_at DESC);
141
+ `);
142
+
143
+ // Deduplicated mode CASE subquery — used in 4 session listing queries
144
+ const MODE_CASE = `
145
+ CASE
146
+ WHEN EXISTS (SELECT 1 FROM messages m WHERE m.session_id = s.id AND m.chat_id IS NOT NULL)
147
+ AND EXISTS (SELECT 1 FROM messages m WHERE m.session_id = s.id AND m.chat_id IS NULL)
148
+ THEN 'both'
149
+ WHEN EXISTS (SELECT 1 FROM messages m WHERE m.session_id = s.id AND m.chat_id IS NOT NULL)
150
+ THEN 'parallel'
151
+ ELSE 'single'
152
+ END AS mode`;
153
+
154
+ // Prepared statements
155
+ const stmts = {
156
+ createSession: db.prepare(
157
+ `INSERT OR IGNORE INTO sessions (id, claude_session_id, project_name, project_path)
158
+ VALUES (?, ?, ?, ?)`
159
+ ),
160
+ updateClaudeSessionId: db.prepare(
161
+ `UPDATE sessions SET claude_session_id = ? WHERE id = ?`
162
+ ),
163
+ getSession: db.prepare(`SELECT * FROM sessions WHERE id = ?`),
164
+ listSessions: db.prepare(
165
+ `SELECT s.*, ${MODE_CASE}
166
+ FROM sessions s ORDER BY s.pinned DESC, s.last_used_at DESC LIMIT ?`
167
+ ),
168
+ listSessionsByProject: db.prepare(
169
+ `SELECT s.*, ${MODE_CASE}
170
+ FROM sessions s WHERE s.project_path = ? ORDER BY s.pinned DESC, s.last_used_at DESC LIMIT ?`
171
+ ),
172
+ touchSession: db.prepare(
173
+ `UPDATE sessions SET last_used_at = unixepoch() WHERE id = ?`
174
+ ),
175
+ addCost: db.prepare(
176
+ `INSERT INTO costs (session_id, cost_usd, duration_ms, num_turns, input_tokens, output_tokens, model, stop_reason, is_error, cache_read_tokens, cache_creation_tokens) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
177
+ ),
178
+ addMessage: db.prepare(
179
+ `INSERT INTO messages (session_id, role, content, chat_id, workflow_id, workflow_step_index, workflow_step_label) VALUES (?, ?, ?, ?, ?, ?, ?)`
180
+ ),
181
+ getMessages: db.prepare(
182
+ `SELECT * FROM messages WHERE session_id = ? ORDER BY id ASC`
183
+ ),
184
+ getMessagesByChatId: db.prepare(
185
+ `SELECT * FROM messages WHERE session_id = ? AND chat_id = ? ORDER BY id ASC`
186
+ ),
187
+ getMessagesNoChatId: db.prepare(
188
+ `SELECT * FROM messages WHERE session_id = ? AND chat_id IS NULL ORDER BY id ASC`
189
+ ),
190
+ getTotalCost: db.prepare(`SELECT COALESCE(SUM(cost_usd), 0) AS total FROM costs`),
191
+ getProjectCost: db.prepare(
192
+ `SELECT COALESCE(SUM(c.cost_usd), 0) AS total
193
+ FROM costs c JOIN sessions s ON c.session_id = s.id
194
+ WHERE s.project_path = ?`
195
+ ),
196
+ setClaudeSession: db.prepare(
197
+ `INSERT OR REPLACE INTO claude_sessions (session_id, chat_id, claude_session_id) VALUES (?, ?, ?)`
198
+ ),
199
+ getClaudeSessionId: db.prepare(
200
+ `SELECT claude_session_id FROM claude_sessions WHERE session_id = ? AND chat_id = ?`
201
+ ),
202
+ allClaudeSessions: db.prepare(
203
+ `SELECT * FROM claude_sessions`
204
+ ),
205
+ updateSessionTitle: db.prepare(
206
+ `UPDATE sessions SET title = ? WHERE id = ?`
207
+ ),
208
+ toggleSessionPin: db.prepare(
209
+ `UPDATE sessions SET pinned = CASE WHEN pinned = 1 THEN 0 ELSE 1 END WHERE id = ?`
210
+ ),
211
+ updateSessionSummary: db.prepare(
212
+ `UPDATE sessions SET summary = ? WHERE id = ?`
213
+ ),
214
+ searchSessions: db.prepare(
215
+ `SELECT s.*, ${MODE_CASE}
216
+ FROM sessions s WHERE s.project_path = ? AND (s.title LIKE ? OR s.project_name LIKE ?) ORDER BY s.pinned DESC, s.last_used_at DESC LIMIT ?`
217
+ ),
218
+ searchSessionsAll: db.prepare(
219
+ `SELECT s.*, ${MODE_CASE}
220
+ FROM sessions s WHERE (s.title LIKE ? OR s.project_name LIKE ?) ORDER BY s.pinned DESC, s.last_used_at DESC LIMIT ?`
221
+ ),
222
+ getSessionCosts: db.prepare(
223
+ `SELECT s.id, s.title, s.project_name, s.last_used_at,
224
+ COALESCE(SUM(c.cost_usd), 0) AS total_cost,
225
+ COALESCE(SUM(c.num_turns), 0) AS turns,
226
+ COALESCE(SUM(c.input_tokens), 0) AS input_tokens,
227
+ COALESCE(SUM(c.output_tokens), 0) AS output_tokens
228
+ FROM sessions s
229
+ LEFT JOIN costs c ON c.session_id = s.id
230
+ WHERE s.project_path = ?
231
+ GROUP BY s.id
232
+ ORDER BY total_cost DESC`
233
+ ),
234
+ getSessionCostsAll: db.prepare(
235
+ `SELECT s.id, s.title, s.project_name, s.last_used_at,
236
+ COALESCE(SUM(c.cost_usd), 0) AS total_cost,
237
+ COALESCE(SUM(c.num_turns), 0) AS turns,
238
+ COALESCE(SUM(c.input_tokens), 0) AS input_tokens,
239
+ COALESCE(SUM(c.output_tokens), 0) AS output_tokens
240
+ FROM sessions s
241
+ LEFT JOIN costs c ON c.session_id = s.id
242
+ GROUP BY s.id
243
+ ORDER BY total_cost DESC`
244
+ ),
245
+ getCostTimeline: db.prepare(
246
+ `SELECT date(c.created_at, 'unixepoch') AS date,
247
+ SUM(c.cost_usd) AS cost
248
+ FROM costs c
249
+ WHERE c.created_at >= unixepoch() - 30 * 86400
250
+ GROUP BY date(c.created_at, 'unixepoch')
251
+ ORDER BY date ASC`
252
+ ),
253
+ // Todo CRUD
254
+ listTodos: db.prepare(`SELECT * FROM todos WHERE archived = 0 ORDER BY position ASC, id ASC`),
255
+ listArchivedTodos: db.prepare(`SELECT * FROM todos WHERE archived = 1 ORDER BY updated_at DESC`),
256
+ createTodo: db.prepare(`INSERT INTO todos (text, position) VALUES (?, (SELECT COALESCE(MAX(position),0)+1 FROM todos))`),
257
+ updateTodo: db.prepare(`UPDATE todos SET text = COALESCE(?, text), done = COALESCE(?, done), priority = COALESCE(?, priority), updated_at = unixepoch() WHERE id = ?`),
258
+ archiveTodo: db.prepare(`UPDATE todos SET archived = ?, updated_at = unixepoch() WHERE id = ?`),
259
+ deleteTodo: db.prepare(`DELETE FROM todos WHERE id = ?`),
260
+ todoCounts: db.prepare(`
261
+ SELECT
262
+ (SELECT COUNT(*) FROM todos WHERE archived = 0) AS active,
263
+ (SELECT COUNT(*) FROM todos WHERE archived = 1) AS archived,
264
+ (SELECT COUNT(*) FROM brags) AS brags
265
+ `),
266
+
267
+ // Brag CRUD
268
+ createBrag: db.prepare(`INSERT INTO brags (todo_id, text, summary) VALUES (?, ?, ?)`),
269
+ listBrags: db.prepare(`SELECT * FROM brags ORDER BY created_at DESC`),
270
+ deleteBrag: db.prepare(`DELETE FROM brags WHERE id = ?`),
271
+
272
+ yearlyActivity: db.prepare(
273
+ `SELECT
274
+ date(c.created_at, 'unixepoch') AS date,
275
+ COUNT(DISTINCT c.session_id) AS sessions,
276
+ COUNT(*) AS queries,
277
+ COALESCE(SUM(c.cost_usd), 0) AS cost,
278
+ COALESCE(SUM(c.input_tokens), 0) AS input_tokens,
279
+ COALESCE(SUM(c.output_tokens), 0) AS output_tokens,
280
+ COALESCE(SUM(c.num_turns), 0) AS turns
281
+ FROM costs c
282
+ WHERE c.created_at >= unixepoch() - 365 * 86400
283
+ GROUP BY date(c.created_at, 'unixepoch')
284
+ ORDER BY date ASC`
285
+ ),
286
+ getCostTimelineByProject: db.prepare(
287
+ `SELECT date(c.created_at, 'unixepoch') AS date,
288
+ SUM(c.cost_usd) AS cost
289
+ FROM costs c
290
+ JOIN sessions s ON c.session_id = s.id
291
+ WHERE s.project_path = ? AND c.created_at >= unixepoch() - 30 * 86400
292
+ GROUP BY date(c.created_at, 'unixepoch')
293
+ ORDER BY date ASC`
294
+ ),
295
+ getTotalTokens: db.prepare(
296
+ `SELECT COALESCE(SUM(input_tokens), 0) AS input_tokens,
297
+ COALESCE(SUM(output_tokens), 0) AS output_tokens
298
+ FROM costs`
299
+ ),
300
+ getProjectTokens: db.prepare(
301
+ `SELECT COALESCE(SUM(c.input_tokens), 0) AS input_tokens,
302
+ COALESCE(SUM(c.output_tokens), 0) AS output_tokens
303
+ FROM costs c JOIN sessions s ON c.session_id = s.id
304
+ WHERE s.project_path = ?`
305
+ ),
306
+ };
307
+
308
+ export function createSession(id, claudeSessionId, projectName, projectPath) {
309
+ stmts.createSession.run(id, claudeSessionId, projectName, projectPath);
310
+ }
311
+
312
+ export function updateClaudeSessionId(id, claudeSessionId) {
313
+ stmts.updateClaudeSessionId.run(claudeSessionId, id);
314
+ }
315
+
316
+ export function getSession(id) {
317
+ return stmts.getSession.get(id);
318
+ }
319
+
320
+ export function listSessions(limit = 20, projectPath) {
321
+ if (projectPath) {
322
+ return stmts.listSessionsByProject.all(projectPath, limit);
323
+ }
324
+ return stmts.listSessions.all(limit);
325
+ }
326
+
327
+ export function touchSession(id) {
328
+ stmts.touchSession.run(id);
329
+ }
330
+
331
+ export function addCost(sessionId, costUsd, durationMs, numTurns, inputTokens = 0, outputTokens = 0, { model = null, stopReason = null, isError = 0, cacheReadTokens = 0, cacheCreationTokens = 0 } = {}) {
332
+ stmts.addCost.run(sessionId, costUsd, durationMs, numTurns, inputTokens, outputTokens, model, stopReason, isError, cacheReadTokens, cacheCreationTokens);
333
+ }
334
+
335
+ export function getTotalCost() {
336
+ return stmts.getTotalCost.get().total;
337
+ }
338
+
339
+ export function getProjectCost(projectPath) {
340
+ return stmts.getProjectCost.get(projectPath).total;
341
+ }
342
+
343
+ export function addMessage(sessionId, role, content, chatId = null, workflowMeta = null) {
344
+ stmts.addMessage.run(sessionId, role, content, chatId, workflowMeta?.workflowId ?? null, workflowMeta?.stepIndex ?? null, workflowMeta?.stepLabel ?? null);
345
+ }
346
+
347
+ export function getMessages(sessionId) {
348
+ return stmts.getMessages.all(sessionId);
349
+ }
350
+
351
+ export function getMessagesByChatId(sessionId, chatId) {
352
+ return stmts.getMessagesByChatId.all(sessionId, chatId);
353
+ }
354
+
355
+ export function getMessagesNoChatId(sessionId) {
356
+ return stmts.getMessagesNoChatId.all(sessionId);
357
+ }
358
+
359
+ export function setClaudeSession(sessionId, chatId, claudeSessionId) {
360
+ stmts.setClaudeSession.run(sessionId, chatId, claudeSessionId);
361
+ }
362
+
363
+ export function getClaudeSessionId(sessionId, chatId) {
364
+ const row = stmts.getClaudeSessionId.get(sessionId, chatId);
365
+ return row ? row.claude_session_id : null;
366
+ }
367
+
368
+ export function allClaudeSessions() {
369
+ return stmts.allClaudeSessions.all();
370
+ }
371
+
372
+ export function updateSessionTitle(id, title) {
373
+ stmts.updateSessionTitle.run(title, id);
374
+ }
375
+
376
+ export function toggleSessionPin(id) {
377
+ stmts.toggleSessionPin.run(id);
378
+ }
379
+
380
+ export function updateSessionSummary(id, summary) {
381
+ stmts.updateSessionSummary.run(summary, id);
382
+ }
383
+
384
+ export function searchSessions(query, limit = 20, projectPath) {
385
+ const pattern = `%${query}%`;
386
+ if (projectPath) {
387
+ return stmts.searchSessions.all(projectPath, pattern, pattern, limit);
388
+ }
389
+ return stmts.searchSessionsAll.all(pattern, pattern, limit);
390
+ }
391
+
392
+ export const deleteSession = db.transaction((id) => {
393
+ db.prepare("DELETE FROM claude_sessions WHERE session_id = ?").run(id);
394
+ db.prepare("DELETE FROM costs WHERE session_id = ?").run(id);
395
+ db.prepare("DELETE FROM messages WHERE session_id = ?").run(id);
396
+ db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
397
+ });
398
+
399
+ export function getSessionCosts(projectPath) {
400
+ if (projectPath) {
401
+ return stmts.getSessionCosts.all(projectPath);
402
+ }
403
+ return stmts.getSessionCostsAll.all();
404
+ }
405
+
406
+ export function getCostTimeline(projectPath) {
407
+ if (projectPath) {
408
+ return stmts.getCostTimelineByProject.all(projectPath);
409
+ }
410
+ return stmts.getCostTimeline.all();
411
+ }
412
+
413
+ export function getTotalTokens() {
414
+ return stmts.getTotalTokens.get();
415
+ }
416
+
417
+ export function getProjectTokens(projectPath) {
418
+ return stmts.getProjectTokens.get(projectPath);
419
+ }
420
+
421
+ // ── Error categorization CASE (reused in multiple queries) ────
422
+ const ERROR_CATEGORY_CASE = `
423
+ CASE
424
+ WHEN json_extract(tr.content, '$.content') LIKE '%ENOENT%'
425
+ OR json_extract(tr.content, '$.content') LIKE '%does not exist%'
426
+ OR json_extract(tr.content, '$.content') LIKE '%No such file%'
427
+ THEN 'File Not Found'
428
+ WHEN json_extract(tr.content, '$.content') LIKE '%Denied by user%'
429
+ OR json_extract(tr.content, '$.content') LIKE '%Aborted by user%'
430
+ THEN 'User Denied'
431
+ WHEN json_extract(tr.content, '$.content') LIKE '%timed out%'
432
+ THEN 'Timeout'
433
+ WHEN json_extract(tr.content, '$.content') LIKE '%File has not been read%'
434
+ OR json_extract(tr.content, '$.content') LIKE '%File has been modified%'
435
+ THEN 'File State Error'
436
+ WHEN json_extract(tr.content, '$.content') LIKE '%EISDIR%'
437
+ OR json_extract(tr.content, '$.content') LIKE '%illegal operation on a directory%'
438
+ THEN 'Directory Error'
439
+ WHEN json_extract(tr.content, '$.content') LIKE '%Found % matches%'
440
+ THEN 'Multiple Matches'
441
+ WHEN json_extract(tr.content, '$.content') LIKE '%command not found%'
442
+ THEN 'Command Not Found'
443
+ WHEN json_extract(tr.content, '$.content') LIKE '%npm error%'
444
+ OR json_extract(tr.content, '$.content') LIKE '%SyntaxError%'
445
+ OR json_extract(tr.content, '$.content') LIKE '%error TS%'
446
+ THEN 'Build/Runtime Error'
447
+ ELSE 'Other'
448
+ END`;
449
+
450
+ // ── Analytics queries ──────────────────────────────────────────
451
+
452
+ const analyticsStmts = {
453
+ overviewAll: db.prepare(`
454
+ SELECT
455
+ (SELECT COUNT(*) FROM sessions) AS sessions,
456
+ COUNT(*) AS queries,
457
+ COALESCE(SUM(cost_usd), 0) AS totalCost,
458
+ COALESCE(SUM(num_turns), 0) AS totalTurns,
459
+ COALESCE(SUM(output_tokens), 0) AS totalOutputTokens
460
+ FROM costs
461
+ `),
462
+ overviewByProject: db.prepare(`
463
+ SELECT
464
+ COUNT(DISTINCT s.id) AS sessions,
465
+ COUNT(c.id) AS queries,
466
+ COALESCE(SUM(c.cost_usd), 0) AS totalCost,
467
+ COALESCE(SUM(c.num_turns), 0) AS totalTurns,
468
+ COALESCE(SUM(c.output_tokens), 0) AS totalOutputTokens
469
+ FROM sessions s
470
+ LEFT JOIN costs c ON c.session_id = s.id
471
+ WHERE s.project_path = ?
472
+ `),
473
+ errorRateAll: db.prepare(`
474
+ SELECT
475
+ COUNT(CASE WHEN json_extract(content, '$.isError') = 1 THEN 1 END) AS errors,
476
+ COUNT(*) AS total
477
+ FROM messages WHERE role = 'tool_result'
478
+ `),
479
+ errorRateByProject: db.prepare(`
480
+ SELECT
481
+ COUNT(CASE WHEN json_extract(m.content, '$.isError') = 1 THEN 1 END) AS errors,
482
+ COUNT(*) AS total
483
+ FROM messages m
484
+ JOIN sessions s ON m.session_id = s.id
485
+ WHERE m.role = 'tool_result' AND s.project_path = ?
486
+ `),
487
+ dailyBreakdownAll: db.prepare(`
488
+ SELECT
489
+ date(c.created_at, 'unixepoch') AS date,
490
+ COUNT(*) AS queries,
491
+ SUM(c.cost_usd) AS cost,
492
+ SUM(c.num_turns) AS turns,
493
+ SUM(c.output_tokens) AS output_tok
494
+ FROM costs c
495
+ WHERE c.created_at >= unixepoch() - 30 * 86400
496
+ GROUP BY date(c.created_at, 'unixepoch')
497
+ ORDER BY date ASC
498
+ `),
499
+ dailyBreakdownByProject: db.prepare(`
500
+ SELECT
501
+ date(c.created_at, 'unixepoch') AS date,
502
+ COUNT(*) AS queries,
503
+ SUM(c.cost_usd) AS cost,
504
+ SUM(c.num_turns) AS turns,
505
+ SUM(c.output_tokens) AS output_tok
506
+ FROM costs c
507
+ JOIN sessions s ON c.session_id = s.id
508
+ WHERE s.project_path = ? AND c.created_at >= unixepoch() - 30 * 86400
509
+ GROUP BY date(c.created_at, 'unixepoch')
510
+ ORDER BY date ASC
511
+ `),
512
+ hourlyActivityAll: db.prepare(`
513
+ SELECT
514
+ CAST(strftime('%H', c.created_at, 'unixepoch', 'localtime') AS INTEGER) AS hour,
515
+ COUNT(*) AS queries,
516
+ SUM(c.cost_usd) AS cost
517
+ FROM costs c
518
+ GROUP BY strftime('%H', c.created_at, 'unixepoch', 'localtime')
519
+ ORDER BY hour ASC
520
+ `),
521
+ hourlyActivityByProject: db.prepare(`
522
+ SELECT
523
+ CAST(strftime('%H', c.created_at, 'unixepoch', 'localtime') AS INTEGER) AS hour,
524
+ COUNT(*) AS queries,
525
+ SUM(c.cost_usd) AS cost
526
+ FROM costs c
527
+ JOIN sessions s ON c.session_id = s.id
528
+ WHERE s.project_path = ?
529
+ GROUP BY strftime('%H', c.created_at, 'unixepoch', 'localtime')
530
+ ORDER BY hour ASC
531
+ `),
532
+ projectBreakdown: db.prepare(`
533
+ SELECT
534
+ s.project_name AS name,
535
+ s.project_path AS path,
536
+ COUNT(DISTINCT s.id) AS sessions,
537
+ COUNT(c.id) AS queries,
538
+ COALESCE(SUM(c.cost_usd), 0) AS totalCost,
539
+ CASE WHEN COUNT(DISTINCT s.id) > 0
540
+ THEN COALESCE(SUM(c.cost_usd), 0) / COUNT(DISTINCT s.id)
541
+ ELSE 0 END AS avgCost,
542
+ CASE WHEN COUNT(DISTINCT s.id) > 0
543
+ THEN COALESCE(SUM(c.num_turns), 0) / COUNT(DISTINCT s.id)
544
+ ELSE 0 END AS avgTurns
545
+ FROM sessions s
546
+ LEFT JOIN costs c ON c.session_id = s.id
547
+ GROUP BY s.project_path
548
+ ORDER BY totalCost DESC
549
+ `),
550
+ topSessionsAll: db.prepare(`
551
+ SELECT
552
+ s.title,
553
+ s.project_name AS project,
554
+ COALESCE(SUM(c.cost_usd), 0) AS cost,
555
+ COALESCE(SUM(c.num_turns), 0) AS turns,
556
+ COUNT(c.id) AS queries,
557
+ COALESCE(SUM(c.duration_ms), 0) / 60000.0 AS duration_min
558
+ FROM sessions s
559
+ LEFT JOIN costs c ON c.session_id = s.id
560
+ GROUP BY s.id
561
+ HAVING cost > 0
562
+ ORDER BY cost DESC
563
+ LIMIT 10
564
+ `),
565
+ topSessionsByProject: db.prepare(`
566
+ SELECT
567
+ s.title,
568
+ s.project_name AS project,
569
+ COALESCE(SUM(c.cost_usd), 0) AS cost,
570
+ COALESCE(SUM(c.num_turns), 0) AS turns,
571
+ COUNT(c.id) AS queries,
572
+ COALESCE(SUM(c.duration_ms), 0) / 60000.0 AS duration_min
573
+ FROM sessions s
574
+ LEFT JOIN costs c ON c.session_id = s.id
575
+ WHERE s.project_path = ?
576
+ GROUP BY s.id
577
+ HAVING cost > 0
578
+ ORDER BY cost DESC
579
+ LIMIT 10
580
+ `),
581
+ toolUsageAll: db.prepare(`
582
+ SELECT
583
+ json_extract(content, '$.name') AS name,
584
+ COUNT(*) AS count
585
+ FROM messages
586
+ WHERE role = 'tool' AND json_extract(content, '$.name') IS NOT NULL
587
+ GROUP BY json_extract(content, '$.name')
588
+ ORDER BY count DESC
589
+ `),
590
+ toolUsageByProject: db.prepare(`
591
+ SELECT
592
+ json_extract(m.content, '$.name') AS name,
593
+ COUNT(*) AS count
594
+ FROM messages m
595
+ JOIN sessions s ON m.session_id = s.id
596
+ WHERE m.role = 'tool' AND s.project_path = ? AND json_extract(m.content, '$.name') IS NOT NULL
597
+ GROUP BY json_extract(m.content, '$.name')
598
+ ORDER BY count DESC
599
+ `),
600
+ toolErrorsAll: db.prepare(`
601
+ SELECT
602
+ json_extract(t.content, '$.name') AS name,
603
+ COUNT(CASE WHEN json_extract(tr.content, '$.isError') = 1 THEN 1 END) AS errors,
604
+ COUNT(*) AS total,
605
+ CAST(COUNT(CASE WHEN json_extract(tr.content, '$.isError') = 1 THEN 1 END) AS REAL) / NULLIF(COUNT(*), 0) * 100 AS errorRate
606
+ FROM messages t
607
+ JOIN messages tr ON tr.session_id = t.session_id
608
+ AND tr.role = 'tool_result'
609
+ AND json_extract(tr.content, '$.toolUseId') = json_extract(t.content, '$.id')
610
+ WHERE t.role = 'tool'
611
+ GROUP BY json_extract(t.content, '$.name')
612
+ HAVING errors > 0
613
+ ORDER BY errors DESC
614
+ `),
615
+ toolErrorsByProject: db.prepare(`
616
+ SELECT
617
+ json_extract(t.content, '$.name') AS name,
618
+ COUNT(CASE WHEN json_extract(tr.content, '$.isError') = 1 THEN 1 END) AS errors,
619
+ COUNT(*) AS total,
620
+ CAST(COUNT(CASE WHEN json_extract(tr.content, '$.isError') = 1 THEN 1 END) AS REAL) / NULLIF(COUNT(*), 0) * 100 AS errorRate
621
+ FROM messages t
622
+ JOIN messages tr ON tr.session_id = t.session_id
623
+ AND tr.role = 'tool_result'
624
+ AND json_extract(tr.content, '$.toolUseId') = json_extract(t.content, '$.id')
625
+ JOIN sessions s ON t.session_id = s.id
626
+ WHERE t.role = 'tool' AND s.project_path = ?
627
+ GROUP BY json_extract(t.content, '$.name')
628
+ HAVING errors > 0
629
+ ORDER BY errors DESC
630
+ `),
631
+ sessionDepthAll: db.prepare(`
632
+ SELECT
633
+ CASE
634
+ WHEN cnt = 1 THEN '1 query'
635
+ WHEN cnt BETWEEN 2 AND 3 THEN '2-3'
636
+ WHEN cnt BETWEEN 4 AND 6 THEN '4-6'
637
+ WHEN cnt BETWEEN 7 AND 10 THEN '7-10'
638
+ ELSE '10+'
639
+ END AS bucket,
640
+ COUNT(*) AS count,
641
+ AVG(total_cost) AS avgCost
642
+ FROM (
643
+ SELECT s.id, COUNT(c.id) AS cnt, COALESCE(SUM(c.cost_usd), 0) AS total_cost
644
+ FROM sessions s
645
+ LEFT JOIN costs c ON c.session_id = s.id
646
+ GROUP BY s.id
647
+ HAVING cnt > 0
648
+ )
649
+ GROUP BY bucket
650
+ ORDER BY MIN(cnt)
651
+ `),
652
+ sessionDepthByProject: db.prepare(`
653
+ SELECT
654
+ CASE
655
+ WHEN cnt = 1 THEN '1 query'
656
+ WHEN cnt BETWEEN 2 AND 3 THEN '2-3'
657
+ WHEN cnt BETWEEN 4 AND 6 THEN '4-6'
658
+ WHEN cnt BETWEEN 7 AND 10 THEN '7-10'
659
+ ELSE '10+'
660
+ END AS bucket,
661
+ COUNT(*) AS count,
662
+ AVG(total_cost) AS avgCost
663
+ FROM (
664
+ SELECT s.id, COUNT(c.id) AS cnt, COALESCE(SUM(c.cost_usd), 0) AS total_cost
665
+ FROM sessions s
666
+ LEFT JOIN costs c ON c.session_id = s.id
667
+ WHERE s.project_path = ?
668
+ GROUP BY s.id
669
+ HAVING cnt > 0
670
+ )
671
+ GROUP BY bucket
672
+ ORDER BY MIN(cnt)
673
+ `),
674
+ msgLengthAll: db.prepare(`
675
+ SELECT
676
+ CASE
677
+ WHEN len < 100 THEN '<100'
678
+ WHEN len BETWEEN 100 AND 499 THEN '100-499'
679
+ WHEN len BETWEEN 500 AND 999 THEN '500-999'
680
+ WHEN len BETWEEN 1000 AND 4999 THEN '1k-5k'
681
+ ELSE '5k+'
682
+ END AS bucket,
683
+ COUNT(*) AS count,
684
+ CAST(AVG(len) AS INTEGER) AS avgChars
685
+ FROM (
686
+ SELECT LENGTH(json_extract(content, '$.text')) AS len
687
+ FROM messages
688
+ WHERE role = 'user' AND json_extract(content, '$.text') IS NOT NULL
689
+ )
690
+ WHERE len > 0
691
+ GROUP BY bucket
692
+ ORDER BY MIN(len)
693
+ `),
694
+ msgLengthByProject: db.prepare(`
695
+ SELECT
696
+ CASE
697
+ WHEN len < 100 THEN '<100'
698
+ WHEN len BETWEEN 100 AND 499 THEN '100-499'
699
+ WHEN len BETWEEN 500 AND 999 THEN '500-999'
700
+ WHEN len BETWEEN 1000 AND 4999 THEN '1k-5k'
701
+ ELSE '5k+'
702
+ END AS bucket,
703
+ COUNT(*) AS count,
704
+ CAST(AVG(len) AS INTEGER) AS avgChars
705
+ FROM (
706
+ SELECT LENGTH(json_extract(m.content, '$.text')) AS len
707
+ FROM messages m
708
+ JOIN sessions s ON m.session_id = s.id
709
+ WHERE m.role = 'user' AND s.project_path = ? AND json_extract(m.content, '$.text') IS NOT NULL
710
+ )
711
+ WHERE len > 0
712
+ GROUP BY bucket
713
+ ORDER BY MIN(len)
714
+ `),
715
+ topBashCommandsAll: db.prepare(`
716
+ SELECT
717
+ SUBSTR(json_extract(content, '$.input.command'), 1, 80) AS command,
718
+ COUNT(*) AS count
719
+ FROM messages
720
+ WHERE role = 'tool' AND json_extract(content, '$.name') = 'Bash'
721
+ AND json_extract(content, '$.input.command') IS NOT NULL
722
+ GROUP BY SUBSTR(json_extract(content, '$.input.command'), 1, 80)
723
+ ORDER BY count DESC
724
+ LIMIT 10
725
+ `),
726
+ topBashCommandsByProject: db.prepare(`
727
+ SELECT
728
+ SUBSTR(json_extract(m.content, '$.input.command'), 1, 80) AS command,
729
+ COUNT(*) AS count
730
+ FROM messages m
731
+ JOIN sessions s ON m.session_id = s.id
732
+ WHERE m.role = 'tool' AND s.project_path = ? AND json_extract(m.content, '$.name') = 'Bash'
733
+ AND json_extract(m.content, '$.input.command') IS NOT NULL
734
+ GROUP BY SUBSTR(json_extract(m.content, '$.input.command'), 1, 80)
735
+ ORDER BY count DESC
736
+ LIMIT 10
737
+ `),
738
+ topFilesAll: db.prepare(`
739
+ SELECT
740
+ json_extract(content, '$.input.file_path') AS path,
741
+ COUNT(*) AS count,
742
+ json_extract(content, '$.name') AS tool
743
+ FROM messages
744
+ WHERE role = 'tool'
745
+ AND json_extract(content, '$.name') IN ('Read', 'Write', 'Edit')
746
+ AND json_extract(content, '$.input.file_path') IS NOT NULL
747
+ GROUP BY json_extract(content, '$.input.file_path'), json_extract(content, '$.name')
748
+ ORDER BY count DESC
749
+ LIMIT 15
750
+ `),
751
+ topFilesByProject: db.prepare(`
752
+ SELECT
753
+ json_extract(m.content, '$.input.file_path') AS path,
754
+ COUNT(*) AS count,
755
+ json_extract(m.content, '$.name') AS tool
756
+ FROM messages m
757
+ JOIN sessions s ON m.session_id = s.id
758
+ WHERE m.role = 'tool' AND s.project_path = ?
759
+ AND json_extract(m.content, '$.name') IN ('Read', 'Write', 'Edit')
760
+ AND json_extract(m.content, '$.input.file_path') IS NOT NULL
761
+ GROUP BY json_extract(m.content, '$.input.file_path'), json_extract(m.content, '$.name')
762
+ ORDER BY count DESC
763
+ LIMIT 15
764
+ `),
765
+
766
+ // ── Error pattern analytics ──────────────────────────────────
767
+ errorCategoriesAll: db.prepare(`
768
+ SELECT ${ERROR_CATEGORY_CASE} AS category, COUNT(*) AS count
769
+ FROM messages tr
770
+ WHERE tr.role = 'tool_result' AND json_extract(tr.content, '$.isError') = 1
771
+ GROUP BY category
772
+ ORDER BY count DESC
773
+ `),
774
+ errorCategoriesByProject: db.prepare(`
775
+ SELECT ${ERROR_CATEGORY_CASE} AS category, COUNT(*) AS count
776
+ FROM messages tr
777
+ JOIN sessions s ON tr.session_id = s.id
778
+ WHERE tr.role = 'tool_result' AND json_extract(tr.content, '$.isError') = 1
779
+ AND s.project_path = ?
780
+ GROUP BY category
781
+ ORDER BY count DESC
782
+ `),
783
+ errorTimelineAll: db.prepare(`
784
+ SELECT date(tr.created_at, 'unixepoch') AS date, COUNT(*) AS errors
785
+ FROM messages tr
786
+ WHERE tr.role = 'tool_result' AND json_extract(tr.content, '$.isError') = 1
787
+ AND tr.created_at >= unixepoch() - 30 * 86400
788
+ GROUP BY date(tr.created_at, 'unixepoch')
789
+ ORDER BY date ASC
790
+ `),
791
+ errorTimelineByProject: db.prepare(`
792
+ SELECT date(tr.created_at, 'unixepoch') AS date, COUNT(*) AS errors
793
+ FROM messages tr
794
+ JOIN sessions s ON tr.session_id = s.id
795
+ WHERE tr.role = 'tool_result' AND json_extract(tr.content, '$.isError') = 1
796
+ AND s.project_path = ? AND tr.created_at >= unixepoch() - 30 * 86400
797
+ GROUP BY date(tr.created_at, 'unixepoch')
798
+ ORDER BY date ASC
799
+ `),
800
+ errorsByToolAll: db.prepare(`
801
+ SELECT
802
+ COALESCE(json_extract(t.content, '$.name'), 'Unknown') AS tool,
803
+ ${ERROR_CATEGORY_CASE} AS category,
804
+ COUNT(*) AS errors
805
+ FROM messages tr
806
+ LEFT JOIN messages t ON t.session_id = tr.session_id
807
+ AND t.role = 'tool'
808
+ AND json_extract(t.content, '$.id') = json_extract(tr.content, '$.toolUseId')
809
+ WHERE tr.role = 'tool_result' AND json_extract(tr.content, '$.isError') = 1
810
+ GROUP BY tool, category
811
+ ORDER BY errors DESC
812
+ `),
813
+ errorsByToolByProject: db.prepare(`
814
+ SELECT
815
+ COALESCE(json_extract(t.content, '$.name'), 'Unknown') AS tool,
816
+ ${ERROR_CATEGORY_CASE} AS category,
817
+ COUNT(*) AS errors
818
+ FROM messages tr
819
+ JOIN sessions s ON tr.session_id = s.id
820
+ LEFT JOIN messages t ON t.session_id = tr.session_id
821
+ AND t.role = 'tool'
822
+ AND json_extract(t.content, '$.id') = json_extract(tr.content, '$.toolUseId')
823
+ WHERE tr.role = 'tool_result' AND json_extract(tr.content, '$.isError') = 1
824
+ AND s.project_path = ?
825
+ GROUP BY tool, category
826
+ ORDER BY errors DESC
827
+ `),
828
+ recentErrorsAll: db.prepare(`
829
+ SELECT
830
+ COALESCE(json_extract(t.content, '$.name'), 'Unknown') AS tool,
831
+ SUBSTR(json_extract(tr.content, '$.content'), 1, 200) AS preview,
832
+ json_extract(tr.content, '$.content') AS full_content,
833
+ s.title AS session_title,
834
+ tr.created_at AS timestamp
835
+ FROM messages tr
836
+ JOIN sessions s ON tr.session_id = s.id
837
+ LEFT JOIN messages t ON t.session_id = tr.session_id
838
+ AND t.role = 'tool'
839
+ AND json_extract(t.content, '$.id') = json_extract(tr.content, '$.toolUseId')
840
+ WHERE tr.role = 'tool_result' AND json_extract(tr.content, '$.isError') = 1
841
+ ORDER BY tr.created_at DESC
842
+ LIMIT 20
843
+ `),
844
+ recentErrorsByProject: db.prepare(`
845
+ SELECT
846
+ COALESCE(json_extract(t.content, '$.name'), 'Unknown') AS tool,
847
+ SUBSTR(json_extract(tr.content, '$.content'), 1, 200) AS preview,
848
+ json_extract(tr.content, '$.content') AS full_content,
849
+ s.title AS session_title,
850
+ tr.created_at AS timestamp
851
+ FROM messages tr
852
+ JOIN sessions s ON tr.session_id = s.id
853
+ LEFT JOIN messages t ON t.session_id = tr.session_id
854
+ AND t.role = 'tool'
855
+ AND json_extract(t.content, '$.id') = json_extract(tr.content, '$.toolUseId')
856
+ WHERE tr.role = 'tool_result' AND json_extract(tr.content, '$.isError') = 1
857
+ AND s.project_path = ?
858
+ ORDER BY tr.created_at DESC
859
+ LIMIT 20
860
+ `),
861
+
862
+ // ── Model usage & cache efficiency ─────────────────────────
863
+ modelUsageAll: db.prepare(`
864
+ SELECT
865
+ COALESCE(model, 'unknown') AS model,
866
+ COUNT(*) AS count,
867
+ COALESCE(SUM(cost_usd), 0) AS cost,
868
+ COALESCE(SUM(input_tokens + output_tokens), 0) AS tokens
869
+ FROM costs
870
+ GROUP BY COALESCE(model, 'unknown')
871
+ ORDER BY cost DESC
872
+ `),
873
+ modelUsageByProject: db.prepare(`
874
+ SELECT
875
+ COALESCE(c.model, 'unknown') AS model,
876
+ COUNT(*) AS count,
877
+ COALESCE(SUM(c.cost_usd), 0) AS cost,
878
+ COALESCE(SUM(c.input_tokens + c.output_tokens), 0) AS tokens
879
+ FROM costs c
880
+ JOIN sessions s ON c.session_id = s.id
881
+ WHERE s.project_path = ?
882
+ GROUP BY COALESCE(c.model, 'unknown')
883
+ ORDER BY cost DESC
884
+ `),
885
+ cacheEfficiencyAll: db.prepare(`
886
+ SELECT
887
+ date(c.created_at, 'unixepoch') AS date,
888
+ COALESCE(SUM(c.cache_read_tokens), 0) AS cache_read,
889
+ COALESCE(SUM(c.cache_creation_tokens), 0) AS cache_creation,
890
+ COALESCE(SUM(c.input_tokens), 0) AS total_input
891
+ FROM costs c
892
+ WHERE c.created_at >= unixepoch() - 30 * 86400
893
+ GROUP BY date(c.created_at, 'unixepoch')
894
+ ORDER BY date ASC
895
+ `),
896
+ cacheEfficiencyByProject: db.prepare(`
897
+ SELECT
898
+ date(c.created_at, 'unixepoch') AS date,
899
+ COALESCE(SUM(c.cache_read_tokens), 0) AS cache_read,
900
+ COALESCE(SUM(c.cache_creation_tokens), 0) AS cache_creation,
901
+ COALESCE(SUM(c.input_tokens), 0) AS total_input
902
+ FROM costs c
903
+ JOIN sessions s ON c.session_id = s.id
904
+ WHERE s.project_path = ? AND c.created_at >= unixepoch() - 30 * 86400
905
+ GROUP BY date(c.created_at, 'unixepoch')
906
+ ORDER BY date ASC
907
+ `),
908
+ };
909
+
910
+ export function getAnalyticsOverview(projectPath) {
911
+ const overview = projectPath
912
+ ? analyticsStmts.overviewByProject.get(projectPath)
913
+ : analyticsStmts.overviewAll.get();
914
+ const errors = projectPath
915
+ ? analyticsStmts.errorRateByProject.get(projectPath)
916
+ : analyticsStmts.errorRateAll.get();
917
+ return {
918
+ ...overview,
919
+ errorRate: errors.total > 0 ? (errors.errors / errors.total * 100) : 0,
920
+ };
921
+ }
922
+
923
+ export function getDailyBreakdown(projectPath) {
924
+ return projectPath
925
+ ? analyticsStmts.dailyBreakdownByProject.all(projectPath)
926
+ : analyticsStmts.dailyBreakdownAll.all();
927
+ }
928
+
929
+ export function getHourlyActivity(projectPath) {
930
+ return projectPath
931
+ ? analyticsStmts.hourlyActivityByProject.all(projectPath)
932
+ : analyticsStmts.hourlyActivityAll.all();
933
+ }
934
+
935
+ export function getProjectBreakdown() {
936
+ return analyticsStmts.projectBreakdown.all();
937
+ }
938
+
939
+ export function getTopSessionsByCost(projectPath) {
940
+ return projectPath
941
+ ? analyticsStmts.topSessionsByProject.all(projectPath)
942
+ : analyticsStmts.topSessionsAll.all();
943
+ }
944
+
945
+ export function getToolUsage(projectPath) {
946
+ return projectPath
947
+ ? analyticsStmts.toolUsageByProject.all(projectPath)
948
+ : analyticsStmts.toolUsageAll.all();
949
+ }
950
+
951
+ export function getToolErrors(projectPath) {
952
+ return projectPath
953
+ ? analyticsStmts.toolErrorsByProject.all(projectPath)
954
+ : analyticsStmts.toolErrorsAll.all();
955
+ }
956
+
957
+ export function getSessionDepth(projectPath) {
958
+ return projectPath
959
+ ? analyticsStmts.sessionDepthByProject.all(projectPath)
960
+ : analyticsStmts.sessionDepthAll.all();
961
+ }
962
+
963
+ export function getMsgLengthDistribution(projectPath) {
964
+ return projectPath
965
+ ? analyticsStmts.msgLengthByProject.all(projectPath)
966
+ : analyticsStmts.msgLengthAll.all();
967
+ }
968
+
969
+ export function getTopBashCommands(projectPath) {
970
+ return projectPath
971
+ ? analyticsStmts.topBashCommandsByProject.all(projectPath)
972
+ : analyticsStmts.topBashCommandsAll.all();
973
+ }
974
+
975
+ export function getTopFiles(projectPath) {
976
+ return projectPath
977
+ ? analyticsStmts.topFilesByProject.all(projectPath)
978
+ : analyticsStmts.topFilesAll.all();
979
+ }
980
+
981
+ export function getErrorCategories(projectPath) {
982
+ return projectPath
983
+ ? analyticsStmts.errorCategoriesByProject.all(projectPath)
984
+ : analyticsStmts.errorCategoriesAll.all();
985
+ }
986
+
987
+ export function getErrorTimeline(projectPath) {
988
+ return projectPath
989
+ ? analyticsStmts.errorTimelineByProject.all(projectPath)
990
+ : analyticsStmts.errorTimelineAll.all();
991
+ }
992
+
993
+ export function getErrorsByTool(projectPath) {
994
+ return projectPath
995
+ ? analyticsStmts.errorsByToolByProject.all(projectPath)
996
+ : analyticsStmts.errorsByToolAll.all();
997
+ }
998
+
999
+ export function getRecentErrors(projectPath) {
1000
+ return projectPath
1001
+ ? analyticsStmts.recentErrorsByProject.all(projectPath)
1002
+ : analyticsStmts.recentErrorsAll.all();
1003
+ }
1004
+
1005
+ export function getModelUsage(projectPath) {
1006
+ return projectPath
1007
+ ? analyticsStmts.modelUsageByProject.all(projectPath)
1008
+ : analyticsStmts.modelUsageAll.all();
1009
+ }
1010
+
1011
+ export function getYearlyActivity() {
1012
+ return stmts.yearlyActivity.all();
1013
+ }
1014
+
1015
+ export function getCacheEfficiency(projectPath) {
1016
+ return projectPath
1017
+ ? analyticsStmts.cacheEfficiencyByProject.all(projectPath)
1018
+ : analyticsStmts.cacheEfficiencyAll.all();
1019
+ }
1020
+
1021
+ // ── Todo CRUD ────────────────────────────────────────────────
1022
+ export function listTodos(archived = false) {
1023
+ return archived ? stmts.listArchivedTodos.all() : stmts.listTodos.all();
1024
+ }
1025
+ export function createTodo(text) { return stmts.createTodo.run(text); }
1026
+ export function updateTodo(id, text, done, priority) { return stmts.updateTodo.run(text, done, priority, id); }
1027
+ export function archiveTodo(id, archived) { return stmts.archiveTodo.run(archived ? 1 : 0, id); }
1028
+ export function deleteTodo(id) { return stmts.deleteTodo.run(id); }
1029
+
1030
+ export function getTodoCounts() { return stmts.todoCounts.get(); }
1031
+
1032
+ // ── Brag CRUD ─────────────────────────────────────────────────
1033
+ export function createBrag(todoId, text, summary) { return stmts.createBrag.run(todoId, text, summary); }
1034
+ export function listBrags() { return stmts.listBrags.all(); }
1035
+ export function deleteBrag(id) { return stmts.deleteBrag.run(id); }
1036
+
1037
+ // ── Push subscription queries ────────────────────────────────
1038
+ const pushStmts = {
1039
+ upsert: db.prepare(
1040
+ `INSERT INTO push_subscriptions (endpoint, keys_p256dh, keys_auth)
1041
+ VALUES (?, ?, ?)
1042
+ ON CONFLICT(endpoint) DO UPDATE SET keys_p256dh = excluded.keys_p256dh, keys_auth = excluded.keys_auth`
1043
+ ),
1044
+ delete: db.prepare(`DELETE FROM push_subscriptions WHERE endpoint = ?`),
1045
+ getAll: db.prepare(`SELECT * FROM push_subscriptions`),
1046
+ };
1047
+
1048
+ export function upsertPushSubscription(endpoint, p256dh, auth) {
1049
+ pushStmts.upsert.run(endpoint, p256dh, auth);
1050
+ }
1051
+
1052
+ export function deletePushSubscription(endpoint) {
1053
+ pushStmts.delete.run(endpoint);
1054
+ }
1055
+
1056
+ export function getAllPushSubscriptions() {
1057
+ return pushStmts.getAll.all();
1058
+ }
1059
+
1060
+ // ── Agent context (shared memory) ─────────────────────────
1061
+ const ctxStmts = {
1062
+ set: db.prepare(
1063
+ `INSERT INTO agent_context (run_id, agent_id, key, value)
1064
+ VALUES (?, ?, ?, ?)
1065
+ ON CONFLICT(run_id, agent_id, key) DO UPDATE SET value = excluded.value`
1066
+ ),
1067
+ get: db.prepare(
1068
+ `SELECT value FROM agent_context WHERE run_id = ? AND agent_id = ? AND key = ?`
1069
+ ),
1070
+ getAllForRun: db.prepare(
1071
+ `SELECT agent_id, key, value, created_at FROM agent_context WHERE run_id = ? ORDER BY created_at ASC`
1072
+ ),
1073
+ getByKey: db.prepare(
1074
+ `SELECT agent_id, value FROM agent_context WHERE run_id = ? AND key = ?`
1075
+ ),
1076
+ deleteRun: db.prepare(
1077
+ `DELETE FROM agent_context WHERE run_id = ?`
1078
+ ),
1079
+ };
1080
+
1081
+ export function setAgentContext(runId, agentId, key, value) {
1082
+ ctxStmts.set.run(runId, agentId, key, typeof value === "string" ? value : JSON.stringify(value));
1083
+ }
1084
+
1085
+ export function getAgentContext(runId, agentId, key) {
1086
+ const row = ctxStmts.get.get(runId, agentId, key);
1087
+ return row ? row.value : null;
1088
+ }
1089
+
1090
+ export function getAllAgentContext(runId) {
1091
+ return ctxStmts.getAllForRun.all(runId);
1092
+ }
1093
+
1094
+ export function getAgentContextByKey(runId, key) {
1095
+ return ctxStmts.getByKey.all(runId, key);
1096
+ }
1097
+
1098
+ export function deleteAgentContext(runId) {
1099
+ ctxStmts.deleteRun.run(runId);
1100
+ }
1101
+
1102
+ // ── Agent runs (monitoring) ────────────────────────────
1103
+ const runStmts = {
1104
+ insert: db.prepare(
1105
+ `INSERT INTO agent_runs (run_id, agent_id, agent_title, run_type, parent_id, status)
1106
+ VALUES (?, ?, ?, ?, ?, 'running')`
1107
+ ),
1108
+ complete: db.prepare(
1109
+ `UPDATE agent_runs SET status = ?, turns = ?, cost_usd = ?, duration_ms = ?,
1110
+ input_tokens = ?, output_tokens = ?, error = ?, completed_at = unixepoch()
1111
+ WHERE run_id = ? AND agent_id = ?`
1112
+ ),
1113
+ listRecent: db.prepare(
1114
+ `SELECT * FROM agent_runs ORDER BY started_at DESC LIMIT ?`
1115
+ ),
1116
+ agentSummary: db.prepare(
1117
+ `SELECT
1118
+ agent_id, agent_title,
1119
+ COUNT(*) AS runs,
1120
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS successes,
1121
+ SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) AS errors,
1122
+ COALESCE(SUM(cost_usd), 0) AS total_cost,
1123
+ COALESCE(AVG(CASE WHEN status = 'completed' THEN cost_usd END), 0) AS avg_cost,
1124
+ COALESCE(AVG(CASE WHEN status = 'completed' THEN duration_ms END), 0) AS avg_duration,
1125
+ COALESCE(AVG(CASE WHEN status = 'completed' THEN turns END), 0) AS avg_turns,
1126
+ COALESCE(SUM(input_tokens), 0) AS total_input_tokens,
1127
+ COALESCE(SUM(output_tokens), 0) AS total_output_tokens
1128
+ FROM agent_runs
1129
+ GROUP BY agent_id
1130
+ ORDER BY total_cost DESC`
1131
+ ),
1132
+ overview: db.prepare(
1133
+ `SELECT
1134
+ COUNT(*) AS total_runs,
1135
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
1136
+ SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) AS errored,
1137
+ COALESCE(SUM(cost_usd), 0) AS total_cost,
1138
+ COALESCE(AVG(CASE WHEN status = 'completed' THEN duration_ms END), 0) AS avg_duration,
1139
+ COALESCE(AVG(CASE WHEN status = 'completed' THEN turns END), 0) AS avg_turns,
1140
+ COALESCE(SUM(input_tokens), 0) AS total_input_tokens,
1141
+ COALESCE(SUM(output_tokens), 0) AS total_output_tokens
1142
+ FROM agent_runs`
1143
+ ),
1144
+ byType: db.prepare(
1145
+ `SELECT
1146
+ run_type,
1147
+ COUNT(*) AS runs,
1148
+ COALESCE(SUM(cost_usd), 0) AS cost,
1149
+ COALESCE(AVG(duration_ms), 0) AS avg_duration
1150
+ FROM agent_runs
1151
+ GROUP BY run_type
1152
+ ORDER BY runs DESC`
1153
+ ),
1154
+ dailyRuns: db.prepare(
1155
+ `SELECT
1156
+ date(started_at, 'unixepoch') AS date,
1157
+ COUNT(*) AS runs,
1158
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
1159
+ SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) AS errored,
1160
+ COALESCE(SUM(cost_usd), 0) AS cost
1161
+ FROM agent_runs
1162
+ WHERE started_at >= unixepoch() - 30 * 86400
1163
+ GROUP BY date(started_at, 'unixepoch')
1164
+ ORDER BY date ASC`
1165
+ ),
1166
+ };
1167
+
1168
+ export function recordAgentRunStart(runId, agentId, agentTitle, runType = 'single', parentId = null) {
1169
+ runStmts.insert.run(runId, agentId, agentTitle, runType, parentId);
1170
+ }
1171
+
1172
+ export function recordAgentRunComplete(runId, agentId, status, turns, costUsd, durationMs, inputTokens, outputTokens, error = null) {
1173
+ runStmts.complete.run(status, turns, costUsd, durationMs, inputTokens, outputTokens, error, runId, agentId);
1174
+ }
1175
+
1176
+ export function getAgentRunsRecent(limit = 50) {
1177
+ return runStmts.listRecent.all(limit);
1178
+ }
1179
+
1180
+ export function getAgentRunsSummary() {
1181
+ return runStmts.agentSummary.all();
1182
+ }
1183
+
1184
+ export function getAgentRunsOverview() {
1185
+ return runStmts.overview.get();
1186
+ }
1187
+
1188
+ export function getAgentRunsByType() {
1189
+ return runStmts.byType.all();
1190
+ }
1191
+
1192
+ export function getAgentRunsDaily() {
1193
+ return runStmts.dailyRuns.all();
1194
+ }
1195
+
1196
+ export function getDb() {
1197
+ return db;
1198
+ }