cloviz 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/README.md +257 -0
- package/dist/client/assets/index-BlQ10IYF.js +168 -0
- package/dist/client/assets/index-ChwCt4EE.css +1 -0
- package/dist/client/index.html +14 -0
- package/dist/client/logo.png +0 -0
- package/dist/server/-ZSFJZLUJ.js +3025 -0
- package/dist/server/chunk-E2OJZHOR.js +8 -0
- package/dist/server/cli.js +31 -0
- package/package.json +57 -0
|
@@ -0,0 +1,3025 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IS_BUN
|
|
3
|
+
} from "./chunk-E2OJZHOR.js";
|
|
4
|
+
|
|
5
|
+
// src/server/index.ts
|
|
6
|
+
import { Hono as Hono14 } from "hono";
|
|
7
|
+
import { cors } from "hono/cors";
|
|
8
|
+
import { join as join11, dirname } from "path";
|
|
9
|
+
import { homedir as homedir5 } from "os";
|
|
10
|
+
import { existsSync as existsSync7 } from "fs";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
|
|
13
|
+
// src/server/runtime/database.ts
|
|
14
|
+
import { createRequire } from "module";
|
|
15
|
+
function openDatabaseSync(path) {
|
|
16
|
+
if (IS_BUN) {
|
|
17
|
+
const require2 = createRequire(import.meta.url);
|
|
18
|
+
const { Database } = require2("bun:sqlite");
|
|
19
|
+
return new Database(path);
|
|
20
|
+
} else {
|
|
21
|
+
const require2 = createRequire(import.meta.url);
|
|
22
|
+
const Database = require2("better-sqlite3");
|
|
23
|
+
return new Database(path);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/server/db.ts
|
|
28
|
+
import { mkdirSync } from "fs";
|
|
29
|
+
import { join } from "path";
|
|
30
|
+
import { homedir } from "os";
|
|
31
|
+
var DB_DIR = join(homedir(), ".cache", "cloviz");
|
|
32
|
+
var DB_PATH = join(DB_DIR, "cloviz.db");
|
|
33
|
+
var db;
|
|
34
|
+
function getDb() {
|
|
35
|
+
if (!db) {
|
|
36
|
+
mkdirSync(DB_DIR, { recursive: true });
|
|
37
|
+
db = openDatabaseSync(DB_PATH);
|
|
38
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
39
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
40
|
+
db.exec("PRAGMA cache_size = -64000");
|
|
41
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
42
|
+
migrate(db);
|
|
43
|
+
}
|
|
44
|
+
return db;
|
|
45
|
+
}
|
|
46
|
+
function migrate(db3) {
|
|
47
|
+
db3.exec(`
|
|
48
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
49
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
50
|
+
path TEXT UNIQUE NOT NULL,
|
|
51
|
+
display_name TEXT NOT NULL,
|
|
52
|
+
session_count INTEGER DEFAULT 0,
|
|
53
|
+
message_count INTEGER DEFAULT 0,
|
|
54
|
+
created_at TEXT,
|
|
55
|
+
updated_at TEXT
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
59
|
+
id TEXT PRIMARY KEY,
|
|
60
|
+
project_id INTEGER REFERENCES projects(id),
|
|
61
|
+
jsonl_path TEXT,
|
|
62
|
+
summary TEXT,
|
|
63
|
+
first_prompt TEXT,
|
|
64
|
+
message_count INTEGER DEFAULT 0,
|
|
65
|
+
created_at TEXT,
|
|
66
|
+
modified_at TEXT,
|
|
67
|
+
git_branch TEXT,
|
|
68
|
+
is_sidechain INTEGER DEFAULT 0,
|
|
69
|
+
slug TEXT,
|
|
70
|
+
indexed_bytes INTEGER DEFAULT 0
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
74
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
75
|
+
session_id TEXT REFERENCES sessions(id),
|
|
76
|
+
uuid TEXT,
|
|
77
|
+
parent_uuid TEXT,
|
|
78
|
+
type TEXT NOT NULL,
|
|
79
|
+
role TEXT,
|
|
80
|
+
model TEXT,
|
|
81
|
+
content_text TEXT,
|
|
82
|
+
content_json TEXT,
|
|
83
|
+
input_tokens INTEGER DEFAULT 0,
|
|
84
|
+
output_tokens INTEGER DEFAULT 0,
|
|
85
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
86
|
+
cache_creation_tokens INTEGER DEFAULT 0,
|
|
87
|
+
timestamp TEXT,
|
|
88
|
+
byte_offset INTEGER DEFAULT 0
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
CREATE TABLE IF NOT EXISTS tool_uses (
|
|
92
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
93
|
+
message_id INTEGER REFERENCES messages(id),
|
|
94
|
+
session_id TEXT REFERENCES sessions(id),
|
|
95
|
+
tool_name TEXT NOT NULL,
|
|
96
|
+
tool_use_id TEXT,
|
|
97
|
+
input_json TEXT,
|
|
98
|
+
timestamp TEXT
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
CREATE TABLE IF NOT EXISTS plans (
|
|
102
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
103
|
+
filename TEXT UNIQUE NOT NULL,
|
|
104
|
+
content TEXT,
|
|
105
|
+
mtime INTEGER
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
CREATE TABLE IF NOT EXISTS todos (
|
|
109
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
110
|
+
source_file TEXT NOT NULL,
|
|
111
|
+
session_id TEXT,
|
|
112
|
+
agent_id TEXT,
|
|
113
|
+
content TEXT,
|
|
114
|
+
status TEXT,
|
|
115
|
+
active_form TEXT
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
CREATE TABLE IF NOT EXISTS daily_stats (
|
|
119
|
+
date TEXT PRIMARY KEY,
|
|
120
|
+
message_count INTEGER DEFAULT 0,
|
|
121
|
+
session_count INTEGER DEFAULT 0,
|
|
122
|
+
tool_call_count INTEGER DEFAULT 0,
|
|
123
|
+
tokens_by_model TEXT
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
CREATE TABLE IF NOT EXISTS file_history (
|
|
127
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
128
|
+
session_id TEXT,
|
|
129
|
+
file_path TEXT,
|
|
130
|
+
backup_filename TEXT,
|
|
131
|
+
version INTEGER
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
CREATE TABLE IF NOT EXISTS history_entries (
|
|
135
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
136
|
+
display TEXT,
|
|
137
|
+
timestamp INTEGER,
|
|
138
|
+
project TEXT,
|
|
139
|
+
session_id TEXT,
|
|
140
|
+
byte_offset INTEGER DEFAULT 0
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
CREATE TABLE IF NOT EXISTS stats_cache (
|
|
144
|
+
key TEXT PRIMARY KEY,
|
|
145
|
+
value TEXT
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
CREATE TABLE IF NOT EXISTS index_state (
|
|
149
|
+
file_path TEXT PRIMARY KEY,
|
|
150
|
+
indexed_bytes INTEGER DEFAULT 0,
|
|
151
|
+
mtime INTEGER DEFAULT 0
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
CREATE TABLE IF NOT EXISTS commits (
|
|
155
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
156
|
+
project_id INTEGER REFERENCES projects(id),
|
|
157
|
+
hash TEXT NOT NULL,
|
|
158
|
+
short_hash TEXT NOT NULL,
|
|
159
|
+
subject TEXT,
|
|
160
|
+
body TEXT,
|
|
161
|
+
author TEXT,
|
|
162
|
+
author_email TEXT,
|
|
163
|
+
timestamp TEXT NOT NULL,
|
|
164
|
+
files_changed INTEGER DEFAULT 0,
|
|
165
|
+
insertions INTEGER DEFAULT 0,
|
|
166
|
+
deletions INTEGER DEFAULT 0,
|
|
167
|
+
is_claude_authored INTEGER DEFAULT 0,
|
|
168
|
+
UNIQUE(project_id, hash)
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
CREATE TABLE IF NOT EXISTS session_commits (
|
|
172
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
173
|
+
session_id TEXT REFERENCES sessions(id),
|
|
174
|
+
commit_id INTEGER REFERENCES commits(id),
|
|
175
|
+
match_type TEXT NOT NULL,
|
|
176
|
+
UNIQUE(session_id, commit_id)
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
|
|
180
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
|
181
|
+
CREATE INDEX IF NOT EXISTS idx_messages_type ON messages(type);
|
|
182
|
+
CREATE INDEX IF NOT EXISTS idx_tool_uses_session ON tool_uses(session_id);
|
|
183
|
+
CREATE INDEX IF NOT EXISTS idx_tool_uses_name ON tool_uses(tool_name);
|
|
184
|
+
CREATE INDEX IF NOT EXISTS idx_todos_status ON todos(status);
|
|
185
|
+
CREATE INDEX IF NOT EXISTS idx_history_timestamp ON history_entries(timestamp);
|
|
186
|
+
CREATE INDEX IF NOT EXISTS idx_messages_uuid ON messages(uuid);
|
|
187
|
+
CREATE INDEX IF NOT EXISTS idx_commits_project ON commits(project_id);
|
|
188
|
+
CREATE INDEX IF NOT EXISTS idx_commits_timestamp ON commits(timestamp);
|
|
189
|
+
CREATE INDEX IF NOT EXISTS idx_session_commits_session ON session_commits(session_id);
|
|
190
|
+
CREATE INDEX IF NOT EXISTS idx_session_commits_commit ON session_commits(commit_id);
|
|
191
|
+
`);
|
|
192
|
+
try {
|
|
193
|
+
db3.exec("ALTER TABLE projects ADD COLUMN last_indexed_commit TEXT");
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
db3.exec("ALTER TABLE projects ADD COLUMN logo_path TEXT");
|
|
198
|
+
} catch {
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
db3.exec("ALTER TABLE projects ADD COLUMN remote_url TEXT");
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
db3.exec(`
|
|
205
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
|
206
|
+
content_text,
|
|
207
|
+
content='messages',
|
|
208
|
+
content_rowid='id'
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS sessions_fts USING fts5(
|
|
212
|
+
summary,
|
|
213
|
+
first_prompt,
|
|
214
|
+
content='sessions',
|
|
215
|
+
content_rowid='rowid'
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS plans_fts USING fts5(
|
|
219
|
+
filename,
|
|
220
|
+
content,
|
|
221
|
+
content='plans',
|
|
222
|
+
content_rowid='id'
|
|
223
|
+
);
|
|
224
|
+
`);
|
|
225
|
+
db3.exec(`
|
|
226
|
+
CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN
|
|
227
|
+
INSERT INTO messages_fts(rowid, content_text) VALUES (new.id, new.content_text);
|
|
228
|
+
END;
|
|
229
|
+
|
|
230
|
+
CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN
|
|
231
|
+
INSERT INTO messages_fts(messages_fts, rowid, content_text) VALUES('delete', old.id, old.content_text);
|
|
232
|
+
END;
|
|
233
|
+
|
|
234
|
+
CREATE TRIGGER IF NOT EXISTS sessions_ai AFTER INSERT ON sessions BEGIN
|
|
235
|
+
INSERT INTO sessions_fts(rowid, summary, first_prompt) VALUES (new.rowid, new.summary, new.first_prompt);
|
|
236
|
+
END;
|
|
237
|
+
|
|
238
|
+
CREATE TRIGGER IF NOT EXISTS sessions_au AFTER UPDATE ON sessions BEGIN
|
|
239
|
+
INSERT INTO sessions_fts(sessions_fts, rowid, summary, first_prompt) VALUES('delete', old.rowid, old.summary, old.first_prompt);
|
|
240
|
+
INSERT INTO sessions_fts(rowid, summary, first_prompt) VALUES (new.rowid, new.summary, new.first_prompt);
|
|
241
|
+
END;
|
|
242
|
+
|
|
243
|
+
CREATE TRIGGER IF NOT EXISTS plans_ai AFTER INSERT ON plans BEGIN
|
|
244
|
+
INSERT INTO plans_fts(rowid, filename, content) VALUES (new.id, new.filename, new.content);
|
|
245
|
+
END;
|
|
246
|
+
|
|
247
|
+
CREATE TRIGGER IF NOT EXISTS plans_au AFTER UPDATE ON plans BEGIN
|
|
248
|
+
INSERT INTO plans_fts(plans_fts, rowid, filename, content) VALUES('delete', old.id, old.filename, old.content);
|
|
249
|
+
INSERT INTO plans_fts(rowid, filename, content) VALUES (new.id, new.filename, new.content);
|
|
250
|
+
END;
|
|
251
|
+
`);
|
|
252
|
+
}
|
|
253
|
+
function closeDb() {
|
|
254
|
+
if (db) {
|
|
255
|
+
db.close();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/server/pipeline/index.ts
|
|
260
|
+
import { join as join6 } from "path";
|
|
261
|
+
import { readdirSync as readdirSync5, existsSync as existsSync2, statSync as statSync4 } from "fs";
|
|
262
|
+
|
|
263
|
+
// src/server/pipeline/stats-parser.ts
|
|
264
|
+
import { readFileSync } from "fs";
|
|
265
|
+
function parseStatsCache(db3, filePath) {
|
|
266
|
+
let raw;
|
|
267
|
+
try {
|
|
268
|
+
raw = readFileSync(filePath, "utf-8");
|
|
269
|
+
} catch {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const data = JSON.parse(raw);
|
|
273
|
+
const upsert = db3.prepare(
|
|
274
|
+
"INSERT OR REPLACE INTO stats_cache (key, value) VALUES (?, ?)"
|
|
275
|
+
);
|
|
276
|
+
const tx = db3.transaction(() => {
|
|
277
|
+
upsert.run("raw", raw);
|
|
278
|
+
upsert.run("totalSessions", String(data.totalSessions ?? 0));
|
|
279
|
+
upsert.run("totalMessages", String(data.totalMessages ?? 0));
|
|
280
|
+
upsert.run("firstSessionDate", data.firstSessionDate ?? "");
|
|
281
|
+
upsert.run("lastComputedDate", data.lastComputedDate ?? "");
|
|
282
|
+
upsert.run("longestSession", JSON.stringify(data.longestSession ?? {}));
|
|
283
|
+
upsert.run("hourCounts", JSON.stringify(data.hourCounts ?? {}));
|
|
284
|
+
upsert.run("modelUsage", JSON.stringify(data.modelUsage ?? {}));
|
|
285
|
+
upsert.run(
|
|
286
|
+
"totalSpeculationTimeSavedMs",
|
|
287
|
+
String(data.totalSpeculationTimeSavedMs ?? 0)
|
|
288
|
+
);
|
|
289
|
+
db3.exec("DELETE FROM daily_stats");
|
|
290
|
+
const dailyInsert = db3.prepare(
|
|
291
|
+
"INSERT INTO daily_stats (date, message_count, session_count, tool_call_count, tokens_by_model) VALUES (?, ?, ?, ?, ?)"
|
|
292
|
+
);
|
|
293
|
+
const tokensByDate = {};
|
|
294
|
+
if (data.dailyModelTokens) {
|
|
295
|
+
for (const entry of data.dailyModelTokens) {
|
|
296
|
+
tokensByDate[entry.date] = entry.tokensByModel;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (data.dailyActivity) {
|
|
300
|
+
for (const day of data.dailyActivity) {
|
|
301
|
+
dailyInsert.run(
|
|
302
|
+
day.date,
|
|
303
|
+
day.messageCount ?? 0,
|
|
304
|
+
day.sessionCount ?? 0,
|
|
305
|
+
day.toolCallCount ?? 0,
|
|
306
|
+
JSON.stringify(tokensByDate[day.date] ?? {})
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
tx();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/server/pipeline/history-parser.ts
|
|
315
|
+
import { readFileSync as readFileSync2, statSync } from "fs";
|
|
316
|
+
function parseHistory(db3, filePath) {
|
|
317
|
+
let fileSize;
|
|
318
|
+
try {
|
|
319
|
+
fileSize = statSync(filePath).size;
|
|
320
|
+
} catch {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const state = db3.prepare("SELECT indexed_bytes FROM index_state WHERE file_path = ?").get(filePath);
|
|
324
|
+
const indexedBytes = state?.indexed_bytes ?? 0;
|
|
325
|
+
if (indexedBytes >= fileSize) return;
|
|
326
|
+
const buffer = readFileSync2(filePath);
|
|
327
|
+
const newData = buffer.subarray(indexedBytes).toString("utf-8");
|
|
328
|
+
const lines = newData.split("\n").filter((l) => l.trim());
|
|
329
|
+
const insert = db3.prepare(
|
|
330
|
+
"INSERT INTO history_entries (display, timestamp, project, session_id, byte_offset) VALUES (?, ?, ?, ?, ?)"
|
|
331
|
+
);
|
|
332
|
+
const tx = db3.transaction(() => {
|
|
333
|
+
let offset = indexedBytes;
|
|
334
|
+
for (const line of lines) {
|
|
335
|
+
try {
|
|
336
|
+
const entry = JSON.parse(line);
|
|
337
|
+
insert.run(
|
|
338
|
+
entry.display ?? "",
|
|
339
|
+
entry.timestamp ?? 0,
|
|
340
|
+
entry.project ?? "",
|
|
341
|
+
entry.sessionId ?? "",
|
|
342
|
+
offset
|
|
343
|
+
);
|
|
344
|
+
} catch {
|
|
345
|
+
}
|
|
346
|
+
offset += Buffer.byteLength(line, "utf-8") + 1;
|
|
347
|
+
}
|
|
348
|
+
db3.prepare(
|
|
349
|
+
"INSERT OR REPLACE INTO index_state (file_path, indexed_bytes, mtime) VALUES (?, ?, ?)"
|
|
350
|
+
).run(filePath, fileSize, Date.now());
|
|
351
|
+
});
|
|
352
|
+
tx();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/server/pipeline/session-index-parser.ts
|
|
356
|
+
import { readFileSync as readFileSync3, readdirSync } from "fs";
|
|
357
|
+
import { join as join2 } from "path";
|
|
358
|
+
function parseSessionIndex(db3, indexPath, claudeDir) {
|
|
359
|
+
let raw;
|
|
360
|
+
try {
|
|
361
|
+
raw = readFileSync3(indexPath, "utf-8");
|
|
362
|
+
} catch {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const data = JSON.parse(raw);
|
|
366
|
+
const projectPath = data.originalPath ?? "";
|
|
367
|
+
const displayName = projectPath.split("/").filter(Boolean).pop() ?? projectPath;
|
|
368
|
+
db3.prepare(
|
|
369
|
+
`INSERT INTO projects (path, display_name, created_at, updated_at)
|
|
370
|
+
VALUES (?, ?, datetime('now'), datetime('now'))
|
|
371
|
+
ON CONFLICT(path) DO UPDATE SET updated_at = datetime('now')`
|
|
372
|
+
).run(projectPath, displayName);
|
|
373
|
+
const project = db3.prepare("SELECT id FROM projects WHERE path = ?").get(projectPath);
|
|
374
|
+
if (!project) return;
|
|
375
|
+
const entries = data.entries ?? [];
|
|
376
|
+
const upsertSession = db3.prepare(
|
|
377
|
+
`INSERT INTO sessions (id, project_id, jsonl_path, summary, first_prompt, message_count, created_at, modified_at, git_branch, is_sidechain, slug)
|
|
378
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
379
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
380
|
+
project_id = excluded.project_id,
|
|
381
|
+
summary = excluded.summary,
|
|
382
|
+
first_prompt = excluded.first_prompt,
|
|
383
|
+
message_count = excluded.message_count,
|
|
384
|
+
created_at = COALESCE(NULLIF(excluded.created_at, ''), sessions.created_at),
|
|
385
|
+
modified_at = excluded.modified_at,
|
|
386
|
+
git_branch = excluded.git_branch,
|
|
387
|
+
is_sidechain = excluded.is_sidechain`
|
|
388
|
+
);
|
|
389
|
+
const tx = db3.transaction(() => {
|
|
390
|
+
for (const entry of entries) {
|
|
391
|
+
upsertSession.run(
|
|
392
|
+
entry.sessionId,
|
|
393
|
+
project.id,
|
|
394
|
+
entry.fullPath ?? "",
|
|
395
|
+
entry.summary ?? "",
|
|
396
|
+
entry.firstPrompt ?? "",
|
|
397
|
+
entry.messageCount ?? 0,
|
|
398
|
+
entry.created ?? "",
|
|
399
|
+
entry.modified ?? "",
|
|
400
|
+
entry.gitBranch ?? "",
|
|
401
|
+
entry.isSidechain ? 1 : 0,
|
|
402
|
+
""
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const counts = db3.prepare(
|
|
406
|
+
"SELECT COUNT(*) as cnt, COALESCE(SUM(message_count), 0) as msgs FROM sessions WHERE project_id = ?"
|
|
407
|
+
).get(project.id);
|
|
408
|
+
db3.prepare(
|
|
409
|
+
"UPDATE projects SET session_count = ?, message_count = ? WHERE id = ?"
|
|
410
|
+
).run(counts.cnt, counts.msgs, project.id);
|
|
411
|
+
});
|
|
412
|
+
tx();
|
|
413
|
+
}
|
|
414
|
+
function scanAllSessionIndexes(db3, claudeDir) {
|
|
415
|
+
const projectsDir = join2(claudeDir, "projects");
|
|
416
|
+
let dirs;
|
|
417
|
+
try {
|
|
418
|
+
dirs = readdirSync(projectsDir);
|
|
419
|
+
} catch {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
for (const dir of dirs) {
|
|
423
|
+
const indexPath = join2(projectsDir, dir, "sessions-index.json");
|
|
424
|
+
try {
|
|
425
|
+
parseSessionIndex(db3, indexPath, claudeDir);
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/server/pipeline/session-parser.ts
|
|
432
|
+
import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
433
|
+
function parseSessionJsonl(db3, sessionId, jsonlPath) {
|
|
434
|
+
let fileSize;
|
|
435
|
+
try {
|
|
436
|
+
fileSize = statSync2(jsonlPath).size;
|
|
437
|
+
} catch {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const session = db3.prepare("SELECT indexed_bytes FROM sessions WHERE id = ?").get(sessionId);
|
|
441
|
+
const indexedBytes = session?.indexed_bytes ?? 0;
|
|
442
|
+
if (indexedBytes >= fileSize) return;
|
|
443
|
+
const buffer = readFileSync4(jsonlPath);
|
|
444
|
+
const newData = buffer.subarray(indexedBytes).toString("utf-8");
|
|
445
|
+
const lines = newData.split("\n").filter((l) => l.trim());
|
|
446
|
+
const insertMsg = db3.prepare(
|
|
447
|
+
`INSERT INTO messages (session_id, uuid, parent_uuid, type, role, model, content_text, content_json, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, timestamp, byte_offset)
|
|
448
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
449
|
+
);
|
|
450
|
+
const insertTool = db3.prepare(
|
|
451
|
+
`INSERT INTO tool_uses (message_id, session_id, tool_name, tool_use_id, input_json, timestamp)
|
|
452
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
453
|
+
);
|
|
454
|
+
const tx = db3.transaction(() => {
|
|
455
|
+
let offset = indexedBytes;
|
|
456
|
+
let lastSlug = "";
|
|
457
|
+
let lastBranch = "";
|
|
458
|
+
for (const line of lines) {
|
|
459
|
+
try {
|
|
460
|
+
const entry = JSON.parse(line);
|
|
461
|
+
if (entry.slug) lastSlug = entry.slug;
|
|
462
|
+
if (entry.gitBranch) lastBranch = entry.gitBranch;
|
|
463
|
+
if (entry.type === "user" || entry.type === "assistant") {
|
|
464
|
+
const msg = entry.message;
|
|
465
|
+
let contentText = "";
|
|
466
|
+
let contentJson = "";
|
|
467
|
+
let inputTokens = 0;
|
|
468
|
+
let outputTokens = 0;
|
|
469
|
+
let cacheRead = 0;
|
|
470
|
+
let cacheCreation = 0;
|
|
471
|
+
if (msg) {
|
|
472
|
+
if (typeof msg.content === "string") {
|
|
473
|
+
contentText = msg.content;
|
|
474
|
+
} else if (Array.isArray(msg.content)) {
|
|
475
|
+
const textParts = [];
|
|
476
|
+
for (const block of msg.content) {
|
|
477
|
+
if (block.type === "text" && block.text) {
|
|
478
|
+
textParts.push(block.text);
|
|
479
|
+
} else if (block.type === "thinking" && block.thinking) {
|
|
480
|
+
textParts.push(block.thinking);
|
|
481
|
+
} else if (block.type === "tool_result" && block.content) {
|
|
482
|
+
textParts.push(
|
|
483
|
+
typeof block.content === "string" ? block.content : JSON.stringify(block.content)
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
contentText = textParts.join("\n");
|
|
488
|
+
contentJson = JSON.stringify(msg.content);
|
|
489
|
+
}
|
|
490
|
+
if (msg.usage) {
|
|
491
|
+
inputTokens = msg.usage.input_tokens ?? 0;
|
|
492
|
+
outputTokens = msg.usage.output_tokens ?? 0;
|
|
493
|
+
cacheRead = msg.usage.cache_read_input_tokens ?? 0;
|
|
494
|
+
cacheCreation = msg.usage.cache_creation_input_tokens ?? 0;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const result = insertMsg.run(
|
|
498
|
+
sessionId,
|
|
499
|
+
entry.uuid ?? null,
|
|
500
|
+
entry.parentUuid ?? null,
|
|
501
|
+
entry.type,
|
|
502
|
+
msg?.role ?? entry.type,
|
|
503
|
+
msg?.model ?? null,
|
|
504
|
+
contentText,
|
|
505
|
+
contentJson || null,
|
|
506
|
+
inputTokens,
|
|
507
|
+
outputTokens,
|
|
508
|
+
cacheRead,
|
|
509
|
+
cacheCreation,
|
|
510
|
+
entry.timestamp ?? null,
|
|
511
|
+
offset
|
|
512
|
+
);
|
|
513
|
+
if (entry.type === "assistant" && msg && Array.isArray(msg.content)) {
|
|
514
|
+
const msgId = Number(result.lastInsertRowid);
|
|
515
|
+
for (const block of msg.content) {
|
|
516
|
+
if (block.type === "tool_use" && block.name) {
|
|
517
|
+
insertTool.run(
|
|
518
|
+
msgId,
|
|
519
|
+
sessionId,
|
|
520
|
+
block.name,
|
|
521
|
+
block.id ?? null,
|
|
522
|
+
block.input ? JSON.stringify(block.input) : null,
|
|
523
|
+
entry.timestamp ?? null
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
} else if (entry.type === "summary" && entry.summary) {
|
|
529
|
+
db3.prepare("UPDATE sessions SET summary = ? WHERE id = ?").run(
|
|
530
|
+
entry.summary,
|
|
531
|
+
sessionId
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
} catch {
|
|
535
|
+
}
|
|
536
|
+
offset += Buffer.byteLength(line, "utf-8") + 1;
|
|
537
|
+
}
|
|
538
|
+
db3.prepare(
|
|
539
|
+
"UPDATE sessions SET indexed_bytes = ?, slug = COALESCE(NULLIF(?, ''), slug), git_branch = COALESCE(NULLIF(?, ''), git_branch) WHERE id = ?"
|
|
540
|
+
).run(fileSize, lastSlug, lastBranch, sessionId);
|
|
541
|
+
});
|
|
542
|
+
tx();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/server/pipeline/plan-parser.ts
|
|
546
|
+
import { readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
547
|
+
import { join as join3, basename as basename2 } from "path";
|
|
548
|
+
function parsePlan(db3, filePath) {
|
|
549
|
+
let content;
|
|
550
|
+
let mtime;
|
|
551
|
+
try {
|
|
552
|
+
content = readFileSync5(filePath, "utf-8");
|
|
553
|
+
mtime = statSync3(filePath).mtimeMs;
|
|
554
|
+
} catch {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const filename = basename2(filePath);
|
|
558
|
+
db3.prepare(
|
|
559
|
+
`INSERT INTO plans (filename, content, mtime)
|
|
560
|
+
VALUES (?, ?, ?)
|
|
561
|
+
ON CONFLICT(filename) DO UPDATE SET content = excluded.content, mtime = excluded.mtime`
|
|
562
|
+
).run(filename, content, Math.floor(mtime));
|
|
563
|
+
}
|
|
564
|
+
function scanAllPlans(db3, claudeDir) {
|
|
565
|
+
const plansDir = join3(claudeDir, "plans");
|
|
566
|
+
let files;
|
|
567
|
+
try {
|
|
568
|
+
files = readdirSync2(plansDir).filter((f) => f.endsWith(".md"));
|
|
569
|
+
} catch {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const tx = db3.transaction(() => {
|
|
573
|
+
for (const file of files) {
|
|
574
|
+
parsePlan(db3, join3(plansDir, file));
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
tx();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/server/pipeline/todo-parser.ts
|
|
581
|
+
import { readFileSync as readFileSync6, readdirSync as readdirSync3 } from "fs";
|
|
582
|
+
import { join as join4, basename as basename3 } from "path";
|
|
583
|
+
function parseTodo(db3, filePath) {
|
|
584
|
+
let raw;
|
|
585
|
+
try {
|
|
586
|
+
raw = readFileSync6(filePath, "utf-8");
|
|
587
|
+
} catch {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const items = JSON.parse(raw);
|
|
591
|
+
if (!Array.isArray(items) || items.length === 0) return;
|
|
592
|
+
const filename = basename3(filePath, ".json");
|
|
593
|
+
const agentMatch = filename.match(
|
|
594
|
+
/^(.+?)-agent-(.+)$/
|
|
595
|
+
);
|
|
596
|
+
const sessionId = agentMatch ? agentMatch[1] : filename;
|
|
597
|
+
const agentId = agentMatch ? agentMatch[2] : "";
|
|
598
|
+
db3.prepare("DELETE FROM todos WHERE source_file = ?").run(
|
|
599
|
+
basename3(filePath)
|
|
600
|
+
);
|
|
601
|
+
const insert = db3.prepare(
|
|
602
|
+
"INSERT INTO todos (source_file, session_id, agent_id, content, status, active_form) VALUES (?, ?, ?, ?, ?, ?)"
|
|
603
|
+
);
|
|
604
|
+
const tx = db3.transaction(() => {
|
|
605
|
+
for (const item of items) {
|
|
606
|
+
insert.run(
|
|
607
|
+
basename3(filePath),
|
|
608
|
+
sessionId,
|
|
609
|
+
agentId,
|
|
610
|
+
item.content ?? "",
|
|
611
|
+
item.status ?? "pending",
|
|
612
|
+
item.activeForm ?? ""
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
tx();
|
|
617
|
+
}
|
|
618
|
+
function scanAllTodos(db3, claudeDir) {
|
|
619
|
+
const todosDir = join4(claudeDir, "todos");
|
|
620
|
+
let files;
|
|
621
|
+
try {
|
|
622
|
+
files = readdirSync3(todosDir).filter((f) => f.endsWith(".json"));
|
|
623
|
+
} catch {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const tx = db3.transaction(() => {
|
|
627
|
+
for (const file of files) {
|
|
628
|
+
try {
|
|
629
|
+
parseTodo(db3, join4(todosDir, file));
|
|
630
|
+
} catch {
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
tx();
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/server/pipeline/file-history-parser.ts
|
|
638
|
+
import { readdirSync as readdirSync4 } from "fs";
|
|
639
|
+
import { join as join5 } from "path";
|
|
640
|
+
function scanFileHistory(db3, claudeDir) {
|
|
641
|
+
const histDir = join5(claudeDir, "file-history");
|
|
642
|
+
let sessionDirs;
|
|
643
|
+
try {
|
|
644
|
+
sessionDirs = readdirSync4(histDir);
|
|
645
|
+
} catch {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const insert = db3.prepare(
|
|
649
|
+
`INSERT OR IGNORE INTO file_history (session_id, file_path, backup_filename, version)
|
|
650
|
+
VALUES (?, ?, ?, ?)`
|
|
651
|
+
);
|
|
652
|
+
const existing = db3.prepare("SELECT COUNT(*) as cnt FROM file_history").get();
|
|
653
|
+
const tx = db3.transaction(() => {
|
|
654
|
+
for (const sessionId of sessionDirs) {
|
|
655
|
+
const sessionDir = join5(histDir, sessionId);
|
|
656
|
+
let files;
|
|
657
|
+
try {
|
|
658
|
+
files = readdirSync4(sessionDir);
|
|
659
|
+
} catch {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
for (const file of files) {
|
|
663
|
+
const match = file.match(/^(.+)@v(\d+)$/);
|
|
664
|
+
if (!match) continue;
|
|
665
|
+
const hash = match[1];
|
|
666
|
+
const version = parseInt(match[2], 10);
|
|
667
|
+
insert.run(sessionId, hash, file, version);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
tx();
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/server/runtime/process.ts
|
|
675
|
+
async function spawnCommand(cmd, args, options = {}) {
|
|
676
|
+
if (IS_BUN) {
|
|
677
|
+
const proc = Bun.spawn([cmd, ...args], {
|
|
678
|
+
cwd: options.cwd,
|
|
679
|
+
stdout: "pipe",
|
|
680
|
+
stderr: "pipe"
|
|
681
|
+
});
|
|
682
|
+
const stdout = await new Response(proc.stdout).text();
|
|
683
|
+
const exitCode = await proc.exited;
|
|
684
|
+
return { stdout, exitCode };
|
|
685
|
+
} else {
|
|
686
|
+
const { execFile } = await import("child_process");
|
|
687
|
+
const { promisify } = await import("util");
|
|
688
|
+
const execFileAsync = promisify(execFile);
|
|
689
|
+
try {
|
|
690
|
+
const result = await execFileAsync(cmd, args, { cwd: options.cwd, maxBuffer: 10 * 1024 * 1024 });
|
|
691
|
+
return { stdout: result.stdout, exitCode: 0 };
|
|
692
|
+
} catch (err) {
|
|
693
|
+
return { stdout: err.stdout ?? "", exitCode: err.code ?? 1 };
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/server/pipeline/git-parser.ts
|
|
699
|
+
import { existsSync } from "fs";
|
|
700
|
+
var GIT_FIELD_SEP = "%x00";
|
|
701
|
+
var GIT_RECORD_SEP = "%x01";
|
|
702
|
+
var FIELD_SEP = "\0";
|
|
703
|
+
var RECORD_SEP = "";
|
|
704
|
+
async function gitExec(cwd, args) {
|
|
705
|
+
try {
|
|
706
|
+
const result = await spawnCommand("git", args, { cwd });
|
|
707
|
+
if (result.exitCode !== 0) return null;
|
|
708
|
+
return result.stdout;
|
|
709
|
+
} catch {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
async function isGitRepo(path) {
|
|
714
|
+
if (!existsSync(path)) return false;
|
|
715
|
+
const result = await gitExec(path, ["rev-parse", "--git-dir"]);
|
|
716
|
+
return result !== null;
|
|
717
|
+
}
|
|
718
|
+
function isClaude(subject, body) {
|
|
719
|
+
const text = `${subject}
|
|
720
|
+
${body}`.toLowerCase();
|
|
721
|
+
return text.includes("co-authored-by") && (text.includes("claude") || text.includes("noreply@anthropic.com"));
|
|
722
|
+
}
|
|
723
|
+
function parseMetadata(output) {
|
|
724
|
+
const commits = /* @__PURE__ */ new Map();
|
|
725
|
+
const records = output.split(RECORD_SEP).filter((r) => r.trim());
|
|
726
|
+
for (const record of records) {
|
|
727
|
+
const fields = record.split(FIELD_SEP);
|
|
728
|
+
if (fields.length < 6) continue;
|
|
729
|
+
const hash = fields[0].trim();
|
|
730
|
+
const shortHash = fields[1];
|
|
731
|
+
const author = fields[2];
|
|
732
|
+
const authorEmail = fields[3];
|
|
733
|
+
const timestamp = fields[4];
|
|
734
|
+
const subject = fields[5];
|
|
735
|
+
const body = fields.slice(6).join(FIELD_SEP).trim();
|
|
736
|
+
if (!hash || hash.length < 7) continue;
|
|
737
|
+
commits.set(hash, {
|
|
738
|
+
hash,
|
|
739
|
+
shortHash,
|
|
740
|
+
subject,
|
|
741
|
+
body,
|
|
742
|
+
author,
|
|
743
|
+
authorEmail,
|
|
744
|
+
timestamp,
|
|
745
|
+
isClaude: isClaude(subject, body)
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
return commits;
|
|
749
|
+
}
|
|
750
|
+
function parseShortstat(output) {
|
|
751
|
+
const stats = /* @__PURE__ */ new Map();
|
|
752
|
+
const lines = output.split("\n");
|
|
753
|
+
let currentHash = "";
|
|
754
|
+
for (const line of lines) {
|
|
755
|
+
const trimmed = line.trim();
|
|
756
|
+
if (!trimmed) continue;
|
|
757
|
+
if (/^[a-f0-9]{40}$/.test(trimmed)) {
|
|
758
|
+
currentHash = trimmed;
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (currentHash && trimmed.includes("file")) {
|
|
762
|
+
const filesMatch = trimmed.match(/(\d+) files? changed/);
|
|
763
|
+
const insMatch = trimmed.match(/(\d+) insertions?\(\+\)/);
|
|
764
|
+
const delMatch = trimmed.match(/(\d+) deletions?\(-\)/);
|
|
765
|
+
stats.set(currentHash, {
|
|
766
|
+
filesChanged: filesMatch ? parseInt(filesMatch[1]) : 0,
|
|
767
|
+
insertions: insMatch ? parseInt(insMatch[1]) : 0,
|
|
768
|
+
deletions: delMatch ? parseInt(delMatch[1]) : 0
|
|
769
|
+
});
|
|
770
|
+
currentHash = "";
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return stats;
|
|
774
|
+
}
|
|
775
|
+
async function parseGitLog(projectPath, sinceHash, afterDate) {
|
|
776
|
+
const formatStr = `%H${GIT_FIELD_SEP}%h${GIT_FIELD_SEP}%an${GIT_FIELD_SEP}%ae${GIT_FIELD_SEP}%aI${GIT_FIELD_SEP}%s${GIT_FIELD_SEP}%b${GIT_RECORD_SEP}`;
|
|
777
|
+
const metaArgs = ["log", `--format=${formatStr}`, "--no-merges"];
|
|
778
|
+
const statArgs = ["log", "--format=%H", "--shortstat", "--no-merges"];
|
|
779
|
+
if (sinceHash) {
|
|
780
|
+
metaArgs.push(`${sinceHash}..HEAD`);
|
|
781
|
+
statArgs.push(`${sinceHash}..HEAD`);
|
|
782
|
+
} else if (afterDate) {
|
|
783
|
+
metaArgs.push(`--after=${afterDate}`);
|
|
784
|
+
statArgs.push(`--after=${afterDate}`);
|
|
785
|
+
}
|
|
786
|
+
const [metaOutput, statOutput] = await Promise.all([
|
|
787
|
+
gitExec(projectPath, metaArgs),
|
|
788
|
+
gitExec(projectPath, statArgs)
|
|
789
|
+
]);
|
|
790
|
+
if (!metaOutput) return [];
|
|
791
|
+
const metaMap = parseMetadata(metaOutput);
|
|
792
|
+
const statMap = statOutput ? parseShortstat(statOutput) : /* @__PURE__ */ new Map();
|
|
793
|
+
const commits = [];
|
|
794
|
+
for (const [hash, meta] of metaMap) {
|
|
795
|
+
const stat = statMap.get(hash);
|
|
796
|
+
commits.push({
|
|
797
|
+
...meta,
|
|
798
|
+
filesChanged: stat?.filesChanged ?? 0,
|
|
799
|
+
insertions: stat?.insertions ?? 0,
|
|
800
|
+
deletions: stat?.deletions ?? 0
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
return commits;
|
|
804
|
+
}
|
|
805
|
+
function loadSessionWindows(db3, projectId) {
|
|
806
|
+
const sessions = db3.prepare(
|
|
807
|
+
`SELECT s.id, s.created_at, s.modified_at,
|
|
808
|
+
COALESCE(writes.cnt, 0) > 0 as has_writes
|
|
809
|
+
FROM sessions s
|
|
810
|
+
LEFT JOIN (
|
|
811
|
+
SELECT session_id, COUNT(*) as cnt
|
|
812
|
+
FROM tool_uses WHERE tool_name IN ('Write', 'Edit', 'MultiEdit')
|
|
813
|
+
GROUP BY session_id
|
|
814
|
+
) writes ON writes.session_id = s.id
|
|
815
|
+
WHERE s.project_id = ? AND s.created_at IS NOT NULL AND s.modified_at IS NOT NULL`
|
|
816
|
+
).all(projectId);
|
|
817
|
+
return sessions.map((s) => ({
|
|
818
|
+
id: s.id,
|
|
819
|
+
createdAt: s.created_at,
|
|
820
|
+
modifiedAt: s.modified_at,
|
|
821
|
+
hasWrites: !!s.has_writes
|
|
822
|
+
}));
|
|
823
|
+
}
|
|
824
|
+
function matchCommitsToSessions(db3, projectId, commits) {
|
|
825
|
+
const windows = loadSessionWindows(db3, projectId);
|
|
826
|
+
if (windows.length === 0 || commits.length === 0) return;
|
|
827
|
+
const existingLinks = db3.prepare(
|
|
828
|
+
`SELECT DISTINCT sc.session_id
|
|
829
|
+
FROM session_commits sc
|
|
830
|
+
JOIN sessions s ON sc.session_id = s.id
|
|
831
|
+
WHERE s.project_id = ?`
|
|
832
|
+
).all(projectId);
|
|
833
|
+
const linkedSessions = new Set(existingLinks.map((r) => r.session_id));
|
|
834
|
+
const insertLink = db3.prepare(
|
|
835
|
+
`INSERT OR IGNORE INTO session_commits (session_id, commit_id, match_type)
|
|
836
|
+
VALUES (?, ?, ?)`
|
|
837
|
+
);
|
|
838
|
+
const getCommitId = db3.prepare(
|
|
839
|
+
"SELECT id FROM commits WHERE project_id = ? AND hash = ?"
|
|
840
|
+
);
|
|
841
|
+
const sorted = [...commits].sort(
|
|
842
|
+
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
843
|
+
);
|
|
844
|
+
const tx = db3.transaction(() => {
|
|
845
|
+
for (const commit of sorted) {
|
|
846
|
+
const commitRow = getCommitId.get(projectId, commit.hash);
|
|
847
|
+
if (!commitRow) continue;
|
|
848
|
+
for (const session of windows) {
|
|
849
|
+
if (!session.hasWrites) continue;
|
|
850
|
+
if (linkedSessions.has(session.id)) continue;
|
|
851
|
+
if (commit.timestamp >= session.createdAt) {
|
|
852
|
+
insertLink.run(session.id, commitRow.id, "inferred");
|
|
853
|
+
linkedSessions.add(session.id);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
tx();
|
|
859
|
+
}
|
|
860
|
+
async function scanProjectCommits(db3, projectId, projectPath) {
|
|
861
|
+
if (!await isGitRepo(projectPath)) return;
|
|
862
|
+
const project = db3.prepare("SELECT last_indexed_commit FROM projects WHERE id = ?").get(projectId);
|
|
863
|
+
const sinceHash = project?.last_indexed_commit ?? null;
|
|
864
|
+
let afterDate = null;
|
|
865
|
+
if (!sinceHash) {
|
|
866
|
+
const earliest = db3.prepare(
|
|
867
|
+
"SELECT MIN(created_at) as earliest FROM sessions WHERE project_id = ? AND created_at IS NOT NULL AND created_at != ''"
|
|
868
|
+
).get(projectId);
|
|
869
|
+
afterDate = earliest?.earliest ?? null;
|
|
870
|
+
if (!afterDate) return;
|
|
871
|
+
}
|
|
872
|
+
const commits = await parseGitLog(projectPath, sinceHash, afterDate);
|
|
873
|
+
if (commits.length === 0) return;
|
|
874
|
+
const insertCommit = db3.prepare(
|
|
875
|
+
`INSERT OR IGNORE INTO commits (project_id, hash, short_hash, subject, body, author, author_email, timestamp, files_changed, insertions, deletions, is_claude_authored)
|
|
876
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
877
|
+
);
|
|
878
|
+
const tx = db3.transaction(() => {
|
|
879
|
+
for (const c of commits) {
|
|
880
|
+
insertCommit.run(
|
|
881
|
+
projectId,
|
|
882
|
+
c.hash,
|
|
883
|
+
c.shortHash,
|
|
884
|
+
c.subject,
|
|
885
|
+
c.body,
|
|
886
|
+
c.author,
|
|
887
|
+
c.authorEmail,
|
|
888
|
+
c.timestamp,
|
|
889
|
+
c.filesChanged,
|
|
890
|
+
c.insertions,
|
|
891
|
+
c.deletions,
|
|
892
|
+
c.isClaude ? 1 : 0
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
tx();
|
|
897
|
+
matchCommitsToSessions(db3, projectId, commits);
|
|
898
|
+
const newestHash = commits[0].hash;
|
|
899
|
+
db3.prepare("UPDATE projects SET last_indexed_commit = ? WHERE id = ?").run(
|
|
900
|
+
newestHash,
|
|
901
|
+
projectId
|
|
902
|
+
);
|
|
903
|
+
console.log(
|
|
904
|
+
`[git-parser] ${projectPath}: indexed ${commits.length} commits`
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
async function getRemoteUrl(projectPath) {
|
|
908
|
+
const output = await gitExec(projectPath, ["remote", "get-url", "origin"]);
|
|
909
|
+
return output ? output.trim() : null;
|
|
910
|
+
}
|
|
911
|
+
function parseRemoteToWebUrl(remoteUrl) {
|
|
912
|
+
let url = remoteUrl.trim();
|
|
913
|
+
const sshMatch = url.match(/^[\w-]+@([^:]+):(.+?)(?:\.git)?$/);
|
|
914
|
+
if (sshMatch) {
|
|
915
|
+
return `https://${sshMatch[1]}/${sshMatch[2]}`;
|
|
916
|
+
}
|
|
917
|
+
const sshProtoMatch = url.match(/^ssh:\/\/[\w-]+@([^/]+)\/(.+?)(?:\.git)?$/);
|
|
918
|
+
if (sshProtoMatch) {
|
|
919
|
+
return `https://${sshProtoMatch[1]}/${sshProtoMatch[2]}`;
|
|
920
|
+
}
|
|
921
|
+
const httpsMatch = url.match(/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/);
|
|
922
|
+
if (httpsMatch) {
|
|
923
|
+
return `https://${httpsMatch[1]}/${httpsMatch[2]}`;
|
|
924
|
+
}
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
async function scanAllProjectCommits(db3) {
|
|
928
|
+
const projects = db3.prepare("SELECT id, path FROM projects WHERE path IS NOT NULL AND path != ''").all();
|
|
929
|
+
let count = 0;
|
|
930
|
+
for (const project of projects) {
|
|
931
|
+
try {
|
|
932
|
+
await scanProjectCommits(db3, project.id, project.path);
|
|
933
|
+
count++;
|
|
934
|
+
} catch (e) {
|
|
935
|
+
console.error(`[git-parser] Error scanning ${project.path}:`, e);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (count > 0) {
|
|
939
|
+
console.log(`[git-parser] Scanned ${count} projects for commits`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// src/server/pipeline/index.ts
|
|
944
|
+
var STARTUP_MAX_JSONL_SIZE = 2 * 1024 * 1024;
|
|
945
|
+
function runQuickIndex(ctx2) {
|
|
946
|
+
const { db: db3, claudeDir } = ctx2;
|
|
947
|
+
console.log("[pipeline] Starting quick index...");
|
|
948
|
+
const start = Date.now();
|
|
949
|
+
const statsPath = join6(claudeDir, "stats-cache.json");
|
|
950
|
+
if (existsSync2(statsPath)) {
|
|
951
|
+
parseStatsCache(db3, statsPath);
|
|
952
|
+
}
|
|
953
|
+
scanAllSessionIndexes(db3, claudeDir);
|
|
954
|
+
const historyPath = join6(claudeDir, "history.jsonl");
|
|
955
|
+
if (existsSync2(historyPath)) {
|
|
956
|
+
parseHistory(db3, historyPath);
|
|
957
|
+
}
|
|
958
|
+
scanAllPlans(db3, claudeDir);
|
|
959
|
+
scanAllTodos(db3, claudeDir);
|
|
960
|
+
scanFileHistory(db3, claudeDir);
|
|
961
|
+
registerSessionJsonlPaths(ctx2);
|
|
962
|
+
scanProjectLogos(db3);
|
|
963
|
+
scanProjectRemotes(db3).catch(
|
|
964
|
+
(e) => console.error("[pipeline] Error scanning git remotes:", e)
|
|
965
|
+
);
|
|
966
|
+
const elapsed = Date.now() - start;
|
|
967
|
+
console.log(`[pipeline] Quick index completed in ${elapsed}ms`);
|
|
968
|
+
}
|
|
969
|
+
function runBackgroundIndex(ctx2) {
|
|
970
|
+
const { db: db3, claudeDir } = ctx2;
|
|
971
|
+
const projectsDir = join6(claudeDir, "projects");
|
|
972
|
+
let projectDirs;
|
|
973
|
+
try {
|
|
974
|
+
projectDirs = readdirSync5(projectsDir);
|
|
975
|
+
} catch {
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
let count = 0;
|
|
979
|
+
for (const dir of projectDirs) {
|
|
980
|
+
const dirPath = join6(projectsDir, dir);
|
|
981
|
+
let files;
|
|
982
|
+
try {
|
|
983
|
+
files = readdirSync5(dirPath).filter((f) => f.endsWith(".jsonl"));
|
|
984
|
+
} catch {
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
for (const file of files) {
|
|
988
|
+
const sessionId = file.replace(".jsonl", "");
|
|
989
|
+
const jsonlPath = join6(dirPath, file);
|
|
990
|
+
let fileSize;
|
|
991
|
+
try {
|
|
992
|
+
fileSize = statSync4(jsonlPath).size;
|
|
993
|
+
} catch {
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
if (fileSize > STARTUP_MAX_JSONL_SIZE) {
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
const session = db3.prepare("SELECT indexed_bytes FROM sessions WHERE id = ?").get(sessionId);
|
|
1000
|
+
if (session && session.indexed_bytes >= fileSize) {
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
try {
|
|
1004
|
+
parseSessionJsonl(db3, sessionId, jsonlPath);
|
|
1005
|
+
count++;
|
|
1006
|
+
} catch {
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
if (count > 0) {
|
|
1011
|
+
console.log(`[pipeline] Background: ${count} session JSONL files indexed`);
|
|
1012
|
+
}
|
|
1013
|
+
scanAllProjectCommits(db3).catch((e) => {
|
|
1014
|
+
console.error("[pipeline] Error scanning git commits:", e);
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
function registerSessionJsonlPaths(ctx2) {
|
|
1018
|
+
const { db: db3, claudeDir } = ctx2;
|
|
1019
|
+
const projectsDir = join6(claudeDir, "projects");
|
|
1020
|
+
let projectDirs;
|
|
1021
|
+
try {
|
|
1022
|
+
projectDirs = readdirSync5(projectsDir);
|
|
1023
|
+
} catch {
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
const insertStmt = db3.prepare(
|
|
1027
|
+
`INSERT OR IGNORE INTO sessions (id, jsonl_path, message_count, indexed_bytes)
|
|
1028
|
+
VALUES (?, ?, 0, 0)`
|
|
1029
|
+
);
|
|
1030
|
+
const updateStmt = db3.prepare(
|
|
1031
|
+
"UPDATE sessions SET jsonl_path = ? WHERE id = ? AND (jsonl_path IS NULL OR jsonl_path = '')"
|
|
1032
|
+
);
|
|
1033
|
+
const tx = db3.transaction(() => {
|
|
1034
|
+
for (const dir of projectDirs) {
|
|
1035
|
+
const dirPath = join6(projectsDir, dir);
|
|
1036
|
+
let files;
|
|
1037
|
+
try {
|
|
1038
|
+
files = readdirSync5(dirPath).filter((f) => f.endsWith(".jsonl"));
|
|
1039
|
+
} catch {
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
for (const file of files) {
|
|
1043
|
+
const sessionId = file.replace(".jsonl", "");
|
|
1044
|
+
const jsonlPath = join6(dirPath, file);
|
|
1045
|
+
insertStmt.run(sessionId, jsonlPath);
|
|
1046
|
+
updateStmt.run(jsonlPath, sessionId);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
tx();
|
|
1051
|
+
}
|
|
1052
|
+
var LOGO_CANDIDATES = [
|
|
1053
|
+
"logo.svg",
|
|
1054
|
+
"logo.png",
|
|
1055
|
+
"logo.jpg",
|
|
1056
|
+
"logo.webp",
|
|
1057
|
+
"icon.svg",
|
|
1058
|
+
"icon.png",
|
|
1059
|
+
"favicon.svg",
|
|
1060
|
+
"favicon.png",
|
|
1061
|
+
"favicon.ico",
|
|
1062
|
+
"public/logo.svg",
|
|
1063
|
+
"public/logo.png",
|
|
1064
|
+
"public/favicon.svg",
|
|
1065
|
+
"public/favicon.png",
|
|
1066
|
+
"public/favicon.ico",
|
|
1067
|
+
"public/favicon-32x32.png",
|
|
1068
|
+
"static/logo.svg",
|
|
1069
|
+
"static/logo.png",
|
|
1070
|
+
"static/favicon.svg",
|
|
1071
|
+
"static/favicon.png",
|
|
1072
|
+
"static/favicon.ico",
|
|
1073
|
+
"static/favicon-32x32.png",
|
|
1074
|
+
"app/static/logo.svg",
|
|
1075
|
+
"app/static/logo.png",
|
|
1076
|
+
"app/static/favicon.svg",
|
|
1077
|
+
"app/static/favicon.png",
|
|
1078
|
+
"app/static/favicon.ico",
|
|
1079
|
+
"app/static/favicon-32x32.png",
|
|
1080
|
+
"src/logo.svg",
|
|
1081
|
+
"src/logo.png",
|
|
1082
|
+
"src/assets/logo.svg",
|
|
1083
|
+
"src/assets/logo.png",
|
|
1084
|
+
"assets/logo.svg",
|
|
1085
|
+
"assets/logo.png",
|
|
1086
|
+
".github/logo.svg",
|
|
1087
|
+
".github/logo.png"
|
|
1088
|
+
];
|
|
1089
|
+
function scanProjectLogos(db3) {
|
|
1090
|
+
const projects = db3.prepare("SELECT id, path FROM projects WHERE path IS NOT NULL AND path != ''").all();
|
|
1091
|
+
const updateStmt = db3.prepare(
|
|
1092
|
+
"UPDATE projects SET logo_path = ? WHERE id = ?"
|
|
1093
|
+
);
|
|
1094
|
+
for (const project of projects) {
|
|
1095
|
+
if (!existsSync2(project.path)) continue;
|
|
1096
|
+
let found = null;
|
|
1097
|
+
for (const candidate of LOGO_CANDIDATES) {
|
|
1098
|
+
const fullPath = join6(project.path, candidate);
|
|
1099
|
+
if (existsSync2(fullPath)) {
|
|
1100
|
+
found = fullPath;
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
updateStmt.run(found, project.id);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
async function scanProjectRemotes(db3) {
|
|
1108
|
+
const projects = db3.prepare("SELECT id, path FROM projects WHERE path IS NOT NULL AND path != ''").all();
|
|
1109
|
+
const updateStmt = db3.prepare(
|
|
1110
|
+
"UPDATE projects SET remote_url = ? WHERE id = ?"
|
|
1111
|
+
);
|
|
1112
|
+
for (const project of projects) {
|
|
1113
|
+
if (!existsSync2(project.path)) continue;
|
|
1114
|
+
const rawUrl = await getRemoteUrl(project.path);
|
|
1115
|
+
const webUrl = rawUrl ? parseRemoteToWebUrl(rawUrl) : null;
|
|
1116
|
+
updateStmt.run(webUrl, project.id);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
function handleFileChange(ctx2, filePath) {
|
|
1120
|
+
const { db: db3, claudeDir } = ctx2;
|
|
1121
|
+
const relative = filePath.startsWith(claudeDir) ? filePath.slice(claudeDir.length + 1) : filePath;
|
|
1122
|
+
if (relative === "stats-cache.json") {
|
|
1123
|
+
parseStatsCache(db3, filePath);
|
|
1124
|
+
return "stats:updated";
|
|
1125
|
+
}
|
|
1126
|
+
if (relative === "history.jsonl") {
|
|
1127
|
+
parseHistory(db3, filePath);
|
|
1128
|
+
return "history:appended";
|
|
1129
|
+
}
|
|
1130
|
+
if (relative.startsWith("plans/") && relative.endsWith(".md")) {
|
|
1131
|
+
parsePlan(db3, filePath);
|
|
1132
|
+
return "plan:changed";
|
|
1133
|
+
}
|
|
1134
|
+
if (relative.startsWith("todos/") && relative.endsWith(".json")) {
|
|
1135
|
+
parseTodo(db3, filePath);
|
|
1136
|
+
return "todo:changed";
|
|
1137
|
+
}
|
|
1138
|
+
if (relative.startsWith("projects/") && relative.endsWith("sessions-index.json")) {
|
|
1139
|
+
parseSessionIndex(db3, filePath, claudeDir);
|
|
1140
|
+
return "session:updated";
|
|
1141
|
+
}
|
|
1142
|
+
if (relative.startsWith("projects/") && relative.endsWith(".jsonl")) {
|
|
1143
|
+
const parts = relative.split("/");
|
|
1144
|
+
const sessionId = parts[parts.length - 1].replace(".jsonl", "");
|
|
1145
|
+
db3.prepare(
|
|
1146
|
+
`INSERT OR IGNORE INTO sessions (id, jsonl_path, message_count, indexed_bytes)
|
|
1147
|
+
VALUES (?, ?, 0, 0)`
|
|
1148
|
+
).run(sessionId, filePath);
|
|
1149
|
+
parseSessionJsonl(db3, sessionId, filePath);
|
|
1150
|
+
const session = db3.prepare("SELECT project_id FROM sessions WHERE id = ?").get(sessionId);
|
|
1151
|
+
if (session?.project_id) {
|
|
1152
|
+
const project = db3.prepare("SELECT id, path FROM projects WHERE id = ?").get(session.project_id);
|
|
1153
|
+
if (project?.path) {
|
|
1154
|
+
scanProjectCommits(db3, project.id, project.path).catch(() => {
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return "session:updated";
|
|
1159
|
+
}
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// src/server/watcher.ts
|
|
1164
|
+
import chokidar from "chokidar";
|
|
1165
|
+
|
|
1166
|
+
// src/server/ws.ts
|
|
1167
|
+
var clients = /* @__PURE__ */ new Set();
|
|
1168
|
+
function addClient(ws) {
|
|
1169
|
+
clients.add(ws);
|
|
1170
|
+
}
|
|
1171
|
+
function removeClient(ws) {
|
|
1172
|
+
clients.delete(ws);
|
|
1173
|
+
}
|
|
1174
|
+
function broadcast(event, data) {
|
|
1175
|
+
const message = JSON.stringify({ event, data, timestamp: Date.now() });
|
|
1176
|
+
for (const client of clients) {
|
|
1177
|
+
try {
|
|
1178
|
+
client.send(message);
|
|
1179
|
+
} catch {
|
|
1180
|
+
clients.delete(client);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// src/server/watcher.ts
|
|
1186
|
+
var BLACKLIST = [
|
|
1187
|
+
".credentials.json",
|
|
1188
|
+
"statsig",
|
|
1189
|
+
"session-env",
|
|
1190
|
+
"cache",
|
|
1191
|
+
"telemetry",
|
|
1192
|
+
"paste-cache",
|
|
1193
|
+
"shell-snapshots",
|
|
1194
|
+
"skills"
|
|
1195
|
+
];
|
|
1196
|
+
function isBlacklisted(path) {
|
|
1197
|
+
return BLACKLIST.some(
|
|
1198
|
+
(bl) => path.includes(`/${bl}/`) || path.endsWith(`/${bl}`) || path.includes(`/${bl}`)
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
var watcher = null;
|
|
1202
|
+
var watcherRunning = false;
|
|
1203
|
+
function startWatcher(ctx2) {
|
|
1204
|
+
if (watcher) return;
|
|
1205
|
+
const { claudeDir } = ctx2;
|
|
1206
|
+
watcher = chokidar.watch(claudeDir, {
|
|
1207
|
+
ignored: (path) => isBlacklisted(path),
|
|
1208
|
+
persistent: true,
|
|
1209
|
+
ignoreInitial: true,
|
|
1210
|
+
awaitWriteFinish: {
|
|
1211
|
+
stabilityThreshold: 200,
|
|
1212
|
+
pollInterval: 50
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
const handleChange = (filePath) => {
|
|
1216
|
+
if (isBlacklisted(filePath)) return;
|
|
1217
|
+
try {
|
|
1218
|
+
const event = handleFileChange(ctx2, filePath);
|
|
1219
|
+
if (event) {
|
|
1220
|
+
broadcast(event, { path: filePath });
|
|
1221
|
+
}
|
|
1222
|
+
} catch (e) {
|
|
1223
|
+
console.error(`[watcher] Error processing ${filePath}:`, e);
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
watcher.on("add", handleChange);
|
|
1227
|
+
watcher.on("change", handleChange);
|
|
1228
|
+
watcher.on("ready", () => {
|
|
1229
|
+
console.log("[watcher] Ready and watching for changes");
|
|
1230
|
+
});
|
|
1231
|
+
watcherRunning = true;
|
|
1232
|
+
broadcast("watcher:status", { running: true });
|
|
1233
|
+
console.log(`[watcher] Started watching ${claudeDir}`);
|
|
1234
|
+
}
|
|
1235
|
+
function stopWatcher() {
|
|
1236
|
+
if (watcher) {
|
|
1237
|
+
watcher.close();
|
|
1238
|
+
watcher = null;
|
|
1239
|
+
watcherRunning = false;
|
|
1240
|
+
broadcast("watcher:status", { running: false });
|
|
1241
|
+
console.log("[watcher] Stopped");
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
function isWatcherRunning() {
|
|
1245
|
+
return watcherRunning;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// src/server/runtime/file.ts
|
|
1249
|
+
import { existsSync as existsSync3, readFileSync as readFileSync7, createReadStream } from "fs";
|
|
1250
|
+
function fileResponse(path, contentType) {
|
|
1251
|
+
if (IS_BUN) {
|
|
1252
|
+
const file = Bun.file(path);
|
|
1253
|
+
if (contentType) {
|
|
1254
|
+
return new Response(file, {
|
|
1255
|
+
headers: { "Content-Type": contentType }
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
return new Response(file);
|
|
1259
|
+
}
|
|
1260
|
+
const content = readFileSync7(path);
|
|
1261
|
+
const headers = {};
|
|
1262
|
+
if (contentType) {
|
|
1263
|
+
headers["Content-Type"] = contentType;
|
|
1264
|
+
} else {
|
|
1265
|
+
const ext = path.split(".").pop()?.toLowerCase();
|
|
1266
|
+
const mimeMap = {
|
|
1267
|
+
html: "text/html",
|
|
1268
|
+
js: "application/javascript",
|
|
1269
|
+
css: "text/css",
|
|
1270
|
+
json: "application/json",
|
|
1271
|
+
png: "image/png",
|
|
1272
|
+
jpg: "image/jpeg",
|
|
1273
|
+
jpeg: "image/jpeg",
|
|
1274
|
+
svg: "image/svg+xml",
|
|
1275
|
+
ico: "image/x-icon",
|
|
1276
|
+
webp: "image/webp",
|
|
1277
|
+
gif: "image/gif",
|
|
1278
|
+
txt: "text/plain",
|
|
1279
|
+
woff: "font/woff",
|
|
1280
|
+
woff2: "font/woff2",
|
|
1281
|
+
ttf: "font/ttf",
|
|
1282
|
+
eot: "application/vnd.ms-fontobject"
|
|
1283
|
+
};
|
|
1284
|
+
if (ext && mimeMap[ext]) {
|
|
1285
|
+
headers["Content-Type"] = mimeMap[ext];
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return new Response(content, { headers });
|
|
1289
|
+
}
|
|
1290
|
+
function fileStreamResponse(path, contentType) {
|
|
1291
|
+
if (IS_BUN) {
|
|
1292
|
+
const file = Bun.file(path);
|
|
1293
|
+
return new Response(file.stream(), {
|
|
1294
|
+
headers: contentType ? { "Content-Type": contentType } : {}
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
const stream = createReadStream(path);
|
|
1298
|
+
const readable = new ReadableStream({
|
|
1299
|
+
start(controller) {
|
|
1300
|
+
stream.on("data", (chunk) => controller.enqueue(chunk));
|
|
1301
|
+
stream.on("end", () => controller.close());
|
|
1302
|
+
stream.on("error", (err) => controller.error(err));
|
|
1303
|
+
},
|
|
1304
|
+
cancel() {
|
|
1305
|
+
stream.destroy();
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
return new Response(readable, {
|
|
1309
|
+
headers: contentType ? { "Content-Type": contentType } : {}
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// src/server/api/dashboard.ts
|
|
1314
|
+
import { Hono } from "hono";
|
|
1315
|
+
|
|
1316
|
+
// src/server/pricing.ts
|
|
1317
|
+
var PRICING = {
|
|
1318
|
+
"opus-4-5": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },
|
|
1319
|
+
"sonnet-4-5": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
|
|
1320
|
+
"sonnet-4": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
|
|
1321
|
+
"haiku-4-5": { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1 },
|
|
1322
|
+
"haiku-3-5": { input: 0.8, output: 4, cacheWrite: 1, cacheRead: 0.08 }
|
|
1323
|
+
};
|
|
1324
|
+
function matchModel(model) {
|
|
1325
|
+
const normalized = model.toLowerCase().replace("claude-", "").replace(/-\d{8}$/, "");
|
|
1326
|
+
for (const [key, pricing] of Object.entries(PRICING)) {
|
|
1327
|
+
if (normalized.includes(key)) return pricing;
|
|
1328
|
+
}
|
|
1329
|
+
return PRICING["sonnet-4"];
|
|
1330
|
+
}
|
|
1331
|
+
function calculateCost(model, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens) {
|
|
1332
|
+
const pricing = matchModel(model);
|
|
1333
|
+
const inputCost = inputTokens / 1e6 * pricing.input;
|
|
1334
|
+
const outputCost = outputTokens / 1e6 * pricing.output;
|
|
1335
|
+
const cacheWriteCost = cacheCreationTokens / 1e6 * pricing.cacheWrite;
|
|
1336
|
+
const cacheReadCost = cacheReadTokens / 1e6 * pricing.cacheRead;
|
|
1337
|
+
const totalCost = inputCost + outputCost + cacheWriteCost + cacheReadCost;
|
|
1338
|
+
const costWithoutCache = inputCost + outputCost + cacheCreationTokens / 1e6 * pricing.input + cacheReadTokens / 1e6 * pricing.input;
|
|
1339
|
+
const cacheSavings = costWithoutCache - totalCost;
|
|
1340
|
+
return {
|
|
1341
|
+
inputCost,
|
|
1342
|
+
outputCost,
|
|
1343
|
+
cacheWriteCost,
|
|
1344
|
+
cacheReadCost,
|
|
1345
|
+
totalCost,
|
|
1346
|
+
costWithoutCache,
|
|
1347
|
+
cacheSavings
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
function aggregateCosts(costs) {
|
|
1351
|
+
const result = {
|
|
1352
|
+
inputCost: 0,
|
|
1353
|
+
outputCost: 0,
|
|
1354
|
+
cacheWriteCost: 0,
|
|
1355
|
+
cacheReadCost: 0,
|
|
1356
|
+
totalCost: 0,
|
|
1357
|
+
costWithoutCache: 0,
|
|
1358
|
+
cacheSavings: 0
|
|
1359
|
+
};
|
|
1360
|
+
for (const c of costs) {
|
|
1361
|
+
result.inputCost += c.inputCost;
|
|
1362
|
+
result.outputCost += c.outputCost;
|
|
1363
|
+
result.cacheWriteCost += c.cacheWriteCost;
|
|
1364
|
+
result.cacheReadCost += c.cacheReadCost;
|
|
1365
|
+
result.totalCost += c.totalCost;
|
|
1366
|
+
result.costWithoutCache += c.costWithoutCache;
|
|
1367
|
+
result.cacheSavings += c.cacheSavings;
|
|
1368
|
+
}
|
|
1369
|
+
return result;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// src/server/api/dashboard.ts
|
|
1373
|
+
var app = new Hono();
|
|
1374
|
+
app.get("/", (c) => {
|
|
1375
|
+
const db3 = getDb();
|
|
1376
|
+
const projectId = c.req.query("project_id");
|
|
1377
|
+
const stats = {};
|
|
1378
|
+
const rows = db3.prepare("SELECT key, value FROM stats_cache").all();
|
|
1379
|
+
for (const row of rows) {
|
|
1380
|
+
if (row.key !== "raw") {
|
|
1381
|
+
stats[row.key] = row.value;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
const recentSessions = projectId ? db3.prepare(
|
|
1385
|
+
`SELECT s.id, s.summary, s.first_prompt, s.message_count, s.created_at, s.modified_at, s.git_branch, s.slug, s.is_sidechain,
|
|
1386
|
+
p.display_name as project_name, p.path as project_path
|
|
1387
|
+
FROM sessions s
|
|
1388
|
+
LEFT JOIN projects p ON s.project_id = p.id
|
|
1389
|
+
WHERE s.project_id = ?
|
|
1390
|
+
ORDER BY s.modified_at DESC
|
|
1391
|
+
LIMIT 20`
|
|
1392
|
+
).all(projectId) : db3.prepare(
|
|
1393
|
+
`SELECT s.id, s.summary, s.first_prompt, s.message_count, s.created_at, s.modified_at, s.git_branch, s.slug, s.is_sidechain,
|
|
1394
|
+
p.display_name as project_name, p.path as project_path
|
|
1395
|
+
FROM sessions s
|
|
1396
|
+
LEFT JOIN projects p ON s.project_id = p.id
|
|
1397
|
+
ORDER BY s.modified_at DESC
|
|
1398
|
+
LIMIT 20`
|
|
1399
|
+
).all();
|
|
1400
|
+
const projectCount = db3.prepare("SELECT COUNT(*) as cnt FROM projects").get();
|
|
1401
|
+
const sessionCount = projectId ? db3.prepare("SELECT COUNT(*) as cnt FROM sessions WHERE project_id = ?").get(projectId) : db3.prepare("SELECT COUNT(*) as cnt FROM sessions").get();
|
|
1402
|
+
const messageCount = projectId ? db3.prepare(
|
|
1403
|
+
"SELECT COUNT(*) as cnt FROM messages m JOIN sessions s ON m.session_id = s.id WHERE s.project_id = ?"
|
|
1404
|
+
).get(projectId) : db3.prepare("SELECT COUNT(*) as cnt FROM messages").get();
|
|
1405
|
+
const toolUseCount = projectId ? db3.prepare(
|
|
1406
|
+
"SELECT COUNT(*) as cnt FROM tool_uses t JOIN sessions s ON t.session_id = s.id WHERE s.project_id = ?"
|
|
1407
|
+
).get(projectId) : db3.prepare("SELECT COUNT(*) as cnt FROM tool_uses").get();
|
|
1408
|
+
let dailyActivity;
|
|
1409
|
+
if (projectId) {
|
|
1410
|
+
dailyActivity = db3.prepare(
|
|
1411
|
+
`SELECT date,
|
|
1412
|
+
SUM(message_count) as message_count,
|
|
1413
|
+
COUNT(*) as session_count,
|
|
1414
|
+
SUM(tool_count) as tool_call_count
|
|
1415
|
+
FROM (
|
|
1416
|
+
SELECT DATE(s.created_at) as date,
|
|
1417
|
+
(SELECT COUNT(*) FROM messages m WHERE m.session_id = s.id) as message_count,
|
|
1418
|
+
(SELECT COUNT(*) FROM tool_uses t WHERE t.session_id = s.id) as tool_count
|
|
1419
|
+
FROM sessions s
|
|
1420
|
+
WHERE s.project_id = ?
|
|
1421
|
+
)
|
|
1422
|
+
GROUP BY date
|
|
1423
|
+
ORDER BY date`
|
|
1424
|
+
).all(projectId);
|
|
1425
|
+
} else {
|
|
1426
|
+
dailyActivity = db3.prepare(
|
|
1427
|
+
"SELECT date, message_count, session_count, tool_call_count FROM daily_stats ORDER BY date"
|
|
1428
|
+
).all();
|
|
1429
|
+
}
|
|
1430
|
+
let costSummary = null;
|
|
1431
|
+
if (projectId) {
|
|
1432
|
+
const projectModelRows = db3.prepare(
|
|
1433
|
+
`SELECT m.model,
|
|
1434
|
+
SUM(m.input_tokens) as input_tokens,
|
|
1435
|
+
SUM(m.output_tokens) as output_tokens,
|
|
1436
|
+
SUM(m.cache_creation_tokens) as cache_creation_tokens,
|
|
1437
|
+
SUM(m.cache_read_tokens) as cache_read_tokens
|
|
1438
|
+
FROM messages m
|
|
1439
|
+
JOIN sessions s ON m.session_id = s.id
|
|
1440
|
+
WHERE s.project_id = ? AND m.model IS NOT NULL AND m.model != ''
|
|
1441
|
+
GROUP BY m.model`
|
|
1442
|
+
).all(projectId);
|
|
1443
|
+
const costList = [];
|
|
1444
|
+
for (const row of projectModelRows) {
|
|
1445
|
+
costList.push(
|
|
1446
|
+
calculateCost(
|
|
1447
|
+
row.model,
|
|
1448
|
+
row.input_tokens ?? 0,
|
|
1449
|
+
row.output_tokens ?? 0,
|
|
1450
|
+
row.cache_creation_tokens ?? 0,
|
|
1451
|
+
row.cache_read_tokens ?? 0
|
|
1452
|
+
)
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
if (costList.length > 0) {
|
|
1456
|
+
costSummary = aggregateCosts(costList);
|
|
1457
|
+
}
|
|
1458
|
+
} else {
|
|
1459
|
+
const modelUsage = stats.modelUsage ? JSON.parse(stats.modelUsage) : {};
|
|
1460
|
+
const costList = [];
|
|
1461
|
+
for (const [model, usage] of Object.entries(modelUsage)) {
|
|
1462
|
+
costList.push(
|
|
1463
|
+
calculateCost(
|
|
1464
|
+
model,
|
|
1465
|
+
usage.inputTokens || 0,
|
|
1466
|
+
usage.outputTokens || 0,
|
|
1467
|
+
usage.cacheCreationInputTokens || 0,
|
|
1468
|
+
usage.cacheReadInputTokens || 0
|
|
1469
|
+
)
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
if (costList.length > 0) {
|
|
1473
|
+
costSummary = aggregateCosts(costList);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
const projectCostJoin = projectId ? "JOIN sessions s ON m.session_id = s.id" : "";
|
|
1477
|
+
const projectCostWhere = projectId ? "AND s.project_id = ?" : "";
|
|
1478
|
+
const projectCostParams = projectId ? [projectId] : [];
|
|
1479
|
+
const dailyCostRows = db3.prepare(
|
|
1480
|
+
`SELECT DATE(m.timestamp) as date, m.model,
|
|
1481
|
+
SUM(m.input_tokens) as input_tokens,
|
|
1482
|
+
SUM(m.output_tokens) as output_tokens,
|
|
1483
|
+
SUM(m.cache_creation_tokens) as cache_creation_tokens,
|
|
1484
|
+
SUM(m.cache_read_tokens) as cache_read_tokens
|
|
1485
|
+
FROM messages m
|
|
1486
|
+
${projectCostJoin}
|
|
1487
|
+
WHERE m.timestamp IS NOT NULL AND m.model IS NOT NULL AND m.model != ''
|
|
1488
|
+
AND DATE(m.timestamp) >= DATE('now', '-30 days')
|
|
1489
|
+
${projectCostWhere}
|
|
1490
|
+
GROUP BY date, m.model
|
|
1491
|
+
ORDER BY date`
|
|
1492
|
+
).all(...projectCostParams);
|
|
1493
|
+
const dailyCostMap = /* @__PURE__ */ new Map();
|
|
1494
|
+
for (const row of dailyCostRows) {
|
|
1495
|
+
const cost = calculateCost(
|
|
1496
|
+
row.model,
|
|
1497
|
+
row.input_tokens ?? 0,
|
|
1498
|
+
row.output_tokens ?? 0,
|
|
1499
|
+
row.cache_creation_tokens ?? 0,
|
|
1500
|
+
row.cache_read_tokens ?? 0
|
|
1501
|
+
);
|
|
1502
|
+
dailyCostMap.set(
|
|
1503
|
+
row.date,
|
|
1504
|
+
(dailyCostMap.get(row.date) || 0) + cost.totalCost
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
const dailyCostSparkline = [...dailyCostMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([, cost]) => cost);
|
|
1508
|
+
return c.json({
|
|
1509
|
+
stats: {
|
|
1510
|
+
totalSessions: parseInt(stats.totalSessions || "0"),
|
|
1511
|
+
totalMessages: parseInt(stats.totalMessages || "0"),
|
|
1512
|
+
firstSessionDate: stats.firstSessionDate || null,
|
|
1513
|
+
lastComputedDate: stats.lastComputedDate || null,
|
|
1514
|
+
longestSession: stats.longestSession ? JSON.parse(stats.longestSession) : null,
|
|
1515
|
+
hourCounts: stats.hourCounts ? JSON.parse(stats.hourCounts) : {},
|
|
1516
|
+
modelUsage: stats.modelUsage ? JSON.parse(stats.modelUsage) : {}
|
|
1517
|
+
},
|
|
1518
|
+
counts: {
|
|
1519
|
+
projects: projectCount.cnt,
|
|
1520
|
+
sessions: sessionCount.cnt,
|
|
1521
|
+
indexedMessages: messageCount.cnt,
|
|
1522
|
+
toolUses: toolUseCount.cnt
|
|
1523
|
+
},
|
|
1524
|
+
recentSessions,
|
|
1525
|
+
dailyActivity,
|
|
1526
|
+
costSummary,
|
|
1527
|
+
dailyCostSparkline
|
|
1528
|
+
});
|
|
1529
|
+
});
|
|
1530
|
+
var dashboard_default = app;
|
|
1531
|
+
|
|
1532
|
+
// src/server/api/sessions.ts
|
|
1533
|
+
import { Hono as Hono2 } from "hono";
|
|
1534
|
+
var app2 = new Hono2();
|
|
1535
|
+
app2.get("/", (c) => {
|
|
1536
|
+
const db3 = getDb();
|
|
1537
|
+
const project = c.req.query("project");
|
|
1538
|
+
const branch = c.req.query("branch");
|
|
1539
|
+
const limit = parseInt(c.req.query("limit") || "50");
|
|
1540
|
+
const offset = parseInt(c.req.query("offset") || "0");
|
|
1541
|
+
const sort = c.req.query("sort") || "modified_at";
|
|
1542
|
+
const order = c.req.query("order") === "asc" ? "ASC" : "DESC";
|
|
1543
|
+
let where = "1=1";
|
|
1544
|
+
const params = [];
|
|
1545
|
+
if (project) {
|
|
1546
|
+
where += " AND p.path = ?";
|
|
1547
|
+
params.push(project);
|
|
1548
|
+
}
|
|
1549
|
+
if (branch) {
|
|
1550
|
+
where += " AND s.git_branch = ?";
|
|
1551
|
+
params.push(branch);
|
|
1552
|
+
}
|
|
1553
|
+
const allowedSorts = [
|
|
1554
|
+
"created_at",
|
|
1555
|
+
"modified_at",
|
|
1556
|
+
"message_count",
|
|
1557
|
+
"first_prompt"
|
|
1558
|
+
];
|
|
1559
|
+
const sortCol = allowedSorts.includes(sort) ? sort : "modified_at";
|
|
1560
|
+
const sessions = db3.prepare(
|
|
1561
|
+
`SELECT s.id, s.summary, s.first_prompt, s.message_count, s.created_at, s.modified_at,
|
|
1562
|
+
s.git_branch, s.slug, s.is_sidechain, s.indexed_bytes,
|
|
1563
|
+
p.display_name as project_name, p.path as project_path
|
|
1564
|
+
FROM sessions s
|
|
1565
|
+
LEFT JOIN projects p ON s.project_id = p.id
|
|
1566
|
+
WHERE ${where}
|
|
1567
|
+
ORDER BY s.${sortCol} ${order}
|
|
1568
|
+
LIMIT ? OFFSET ?`
|
|
1569
|
+
).all(...params, limit, offset);
|
|
1570
|
+
const total = db3.prepare(
|
|
1571
|
+
`SELECT COUNT(*) as cnt FROM sessions s LEFT JOIN projects p ON s.project_id = p.id WHERE ${where}`
|
|
1572
|
+
).get(...params);
|
|
1573
|
+
return c.json({ sessions, total: total.cnt, limit, offset });
|
|
1574
|
+
});
|
|
1575
|
+
app2.get("/:id", (c) => {
|
|
1576
|
+
const db3 = getDb();
|
|
1577
|
+
const id = c.req.param("id");
|
|
1578
|
+
const session = db3.prepare(
|
|
1579
|
+
`SELECT s.*, p.display_name as project_name, p.path as project_path
|
|
1580
|
+
FROM sessions s
|
|
1581
|
+
LEFT JOIN projects p ON s.project_id = p.id
|
|
1582
|
+
WHERE s.id = ?`
|
|
1583
|
+
).get(id);
|
|
1584
|
+
if (!session) {
|
|
1585
|
+
return c.json({ error: "Session not found" }, 404);
|
|
1586
|
+
}
|
|
1587
|
+
const costRows = db3.prepare(
|
|
1588
|
+
`SELECT model,
|
|
1589
|
+
SUM(input_tokens) as input_tokens,
|
|
1590
|
+
SUM(output_tokens) as output_tokens,
|
|
1591
|
+
SUM(cache_creation_tokens) as cache_creation_tokens,
|
|
1592
|
+
SUM(cache_read_tokens) as cache_read_tokens
|
|
1593
|
+
FROM messages
|
|
1594
|
+
WHERE session_id = ? AND model IS NOT NULL AND model != ''
|
|
1595
|
+
GROUP BY model`
|
|
1596
|
+
).all(id);
|
|
1597
|
+
let sessionCost = null;
|
|
1598
|
+
if (costRows.length > 0) {
|
|
1599
|
+
const costs = costRows.map(
|
|
1600
|
+
(row) => calculateCost(
|
|
1601
|
+
row.model,
|
|
1602
|
+
row.input_tokens ?? 0,
|
|
1603
|
+
row.output_tokens ?? 0,
|
|
1604
|
+
row.cache_creation_tokens ?? 0,
|
|
1605
|
+
row.cache_read_tokens ?? 0
|
|
1606
|
+
)
|
|
1607
|
+
);
|
|
1608
|
+
sessionCost = aggregateCosts(costs);
|
|
1609
|
+
}
|
|
1610
|
+
return c.json({ ...session, sessionCost });
|
|
1611
|
+
});
|
|
1612
|
+
app2.get("/:id/messages", (c) => {
|
|
1613
|
+
const db3 = getDb();
|
|
1614
|
+
const id = c.req.param("id");
|
|
1615
|
+
const limit = parseInt(c.req.query("limit") || "100");
|
|
1616
|
+
const offset = parseInt(c.req.query("offset") || "0");
|
|
1617
|
+
const session = db3.prepare("SELECT jsonl_path, indexed_bytes FROM sessions WHERE id = ?").get(id);
|
|
1618
|
+
if (session?.jsonl_path) {
|
|
1619
|
+
try {
|
|
1620
|
+
parseSessionJsonl(db3, id, session.jsonl_path);
|
|
1621
|
+
} catch {
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
const messages = db3.prepare(
|
|
1625
|
+
`SELECT id, uuid, parent_uuid, type, role, model, content_text, content_json,
|
|
1626
|
+
input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, timestamp
|
|
1627
|
+
FROM messages
|
|
1628
|
+
WHERE session_id = ?
|
|
1629
|
+
ORDER BY id ASC
|
|
1630
|
+
LIMIT ? OFFSET ?`
|
|
1631
|
+
).all(id, limit, offset);
|
|
1632
|
+
const total = db3.prepare("SELECT COUNT(*) as cnt FROM messages WHERE session_id = ?").get(id);
|
|
1633
|
+
const messageIds = messages.map((m) => m.id);
|
|
1634
|
+
let toolUses = [];
|
|
1635
|
+
if (messageIds.length > 0) {
|
|
1636
|
+
toolUses = db3.prepare(
|
|
1637
|
+
`SELECT * FROM tool_uses WHERE session_id = ? ORDER BY id ASC`
|
|
1638
|
+
).all(id);
|
|
1639
|
+
}
|
|
1640
|
+
return c.json({ messages, toolUses, total: total.cnt, limit, offset });
|
|
1641
|
+
});
|
|
1642
|
+
app2.get("/:id/files", (c) => {
|
|
1643
|
+
const db3 = getDb();
|
|
1644
|
+
const id = c.req.param("id");
|
|
1645
|
+
const session = db3.prepare("SELECT id FROM sessions WHERE id = ?").get(id);
|
|
1646
|
+
if (!session) {
|
|
1647
|
+
return c.json({ error: "Session not found" }, 404);
|
|
1648
|
+
}
|
|
1649
|
+
const toolUses = db3.prepare(
|
|
1650
|
+
`SELECT tool_name, input_json, message_id, timestamp
|
|
1651
|
+
FROM tool_uses
|
|
1652
|
+
WHERE session_id = ? AND tool_name IN ('Read', 'Write', 'Edit', 'MultiEdit')
|
|
1653
|
+
ORDER BY timestamp ASC`
|
|
1654
|
+
).all(id);
|
|
1655
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
1656
|
+
for (const tu of toolUses) {
|
|
1657
|
+
let filePath = null;
|
|
1658
|
+
try {
|
|
1659
|
+
const input = JSON.parse(tu.input_json || "{}");
|
|
1660
|
+
filePath = input.file_path || input.path || null;
|
|
1661
|
+
} catch {
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
if (!filePath) continue;
|
|
1665
|
+
if (!fileMap.has(filePath)) {
|
|
1666
|
+
fileMap.set(filePath, []);
|
|
1667
|
+
}
|
|
1668
|
+
fileMap.get(filePath).push({
|
|
1669
|
+
tool: tu.tool_name,
|
|
1670
|
+
message_id: tu.message_id,
|
|
1671
|
+
timestamp: tu.timestamp
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
const files = [...fileMap.entries()].map(([path, operations]) => ({ path, operations })).sort((a, b) => a.path.localeCompare(b.path));
|
|
1675
|
+
return c.json({ files });
|
|
1676
|
+
});
|
|
1677
|
+
app2.get("/:id/tool-timeline", (c) => {
|
|
1678
|
+
const db3 = getDb();
|
|
1679
|
+
const id = c.req.param("id");
|
|
1680
|
+
const messages = db3.prepare(`SELECT id FROM messages WHERE session_id = ? ORDER BY id ASC`).all(id);
|
|
1681
|
+
if (messages.length === 0) {
|
|
1682
|
+
return c.json({
|
|
1683
|
+
buckets: [],
|
|
1684
|
+
totals: { read: 0, write: 0, bash: 0, search: 0, other: 0 }
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
const msgSeq = /* @__PURE__ */ new Map();
|
|
1688
|
+
messages.forEach((m, i) => msgSeq.set(m.id, i));
|
|
1689
|
+
const toolUses = db3.prepare(
|
|
1690
|
+
`SELECT tool_name, message_id FROM tool_uses WHERE session_id = ? ORDER BY id ASC`
|
|
1691
|
+
).all(id);
|
|
1692
|
+
const totalMessages = messages.length;
|
|
1693
|
+
const bucketCount = Math.min(30, totalMessages);
|
|
1694
|
+
const bucketSize = totalMessages / bucketCount;
|
|
1695
|
+
const categorize = (name) => {
|
|
1696
|
+
switch (name) {
|
|
1697
|
+
case "Read":
|
|
1698
|
+
return "read";
|
|
1699
|
+
case "Write":
|
|
1700
|
+
case "Edit":
|
|
1701
|
+
case "MultiEdit":
|
|
1702
|
+
return "write";
|
|
1703
|
+
case "Bash":
|
|
1704
|
+
return "bash";
|
|
1705
|
+
case "Glob":
|
|
1706
|
+
case "Grep":
|
|
1707
|
+
case "WebSearch":
|
|
1708
|
+
case "WebFetch":
|
|
1709
|
+
return "search";
|
|
1710
|
+
default:
|
|
1711
|
+
return "other";
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
const buckets = Array.from({ length: bucketCount }, (_, i) => ({
|
|
1715
|
+
index: i,
|
|
1716
|
+
read: 0,
|
|
1717
|
+
write: 0,
|
|
1718
|
+
bash: 0,
|
|
1719
|
+
search: 0,
|
|
1720
|
+
other: 0
|
|
1721
|
+
}));
|
|
1722
|
+
const totals = { read: 0, write: 0, bash: 0, search: 0, other: 0 };
|
|
1723
|
+
for (const tu of toolUses) {
|
|
1724
|
+
const seq = msgSeq.get(tu.message_id);
|
|
1725
|
+
if (seq === void 0) continue;
|
|
1726
|
+
const bucketIdx = Math.min(
|
|
1727
|
+
Math.floor(seq / bucketSize),
|
|
1728
|
+
bucketCount - 1
|
|
1729
|
+
);
|
|
1730
|
+
const category = categorize(tu.tool_name);
|
|
1731
|
+
buckets[bucketIdx][category]++;
|
|
1732
|
+
totals[category]++;
|
|
1733
|
+
}
|
|
1734
|
+
return c.json({ buckets, totals });
|
|
1735
|
+
});
|
|
1736
|
+
app2.get("/:id/plans", (c) => {
|
|
1737
|
+
const db3 = getDb();
|
|
1738
|
+
const id = c.req.param("id");
|
|
1739
|
+
const session = db3.prepare("SELECT id FROM sessions WHERE id = ?").get(id);
|
|
1740
|
+
if (!session) {
|
|
1741
|
+
return c.json({ error: "Session not found" }, 404);
|
|
1742
|
+
}
|
|
1743
|
+
const toolUseRows = db3.prepare(
|
|
1744
|
+
`SELECT DISTINCT input_json FROM tool_uses
|
|
1745
|
+
WHERE session_id = ? AND tool_name IN ('Write', 'Edit', 'MultiEdit', 'Read')
|
|
1746
|
+
AND input_json LIKE '%/.claude/plans/%.md%'`
|
|
1747
|
+
).all(id);
|
|
1748
|
+
const planFilenames = /* @__PURE__ */ new Set();
|
|
1749
|
+
const planPathRegex = /\.claude\/plans\/([^/]+\.md)/;
|
|
1750
|
+
for (const row of toolUseRows) {
|
|
1751
|
+
try {
|
|
1752
|
+
const input = JSON.parse(row.input_json || "{}");
|
|
1753
|
+
const pathValue = input.file_path || input.path || "";
|
|
1754
|
+
const match = pathValue.match(planPathRegex);
|
|
1755
|
+
if (match) {
|
|
1756
|
+
planFilenames.add(match[1]);
|
|
1757
|
+
}
|
|
1758
|
+
} catch {
|
|
1759
|
+
continue;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
if (planFilenames.size === 0) {
|
|
1763
|
+
return c.json({ plans: [] });
|
|
1764
|
+
}
|
|
1765
|
+
const placeholders = [...planFilenames].map(() => "?").join(",");
|
|
1766
|
+
const plans = db3.prepare(
|
|
1767
|
+
`SELECT id, filename, content, mtime FROM plans
|
|
1768
|
+
WHERE filename IN (${placeholders}) ORDER BY mtime DESC`
|
|
1769
|
+
).all(...planFilenames);
|
|
1770
|
+
return c.json({ plans });
|
|
1771
|
+
});
|
|
1772
|
+
var sessions_default = app2;
|
|
1773
|
+
|
|
1774
|
+
// src/server/api/projects.ts
|
|
1775
|
+
import { Hono as Hono3 } from "hono";
|
|
1776
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1777
|
+
import { extname } from "path";
|
|
1778
|
+
var MIME_TYPES = {
|
|
1779
|
+
".ico": "image/x-icon",
|
|
1780
|
+
".png": "image/png",
|
|
1781
|
+
".jpg": "image/jpeg",
|
|
1782
|
+
".jpeg": "image/jpeg",
|
|
1783
|
+
".svg": "image/svg+xml",
|
|
1784
|
+
".webp": "image/webp",
|
|
1785
|
+
".gif": "image/gif"
|
|
1786
|
+
};
|
|
1787
|
+
var app3 = new Hono3();
|
|
1788
|
+
app3.get("/", (c) => {
|
|
1789
|
+
const db3 = getDb();
|
|
1790
|
+
const projects = db3.prepare(
|
|
1791
|
+
`SELECT p.*,
|
|
1792
|
+
MIN(s.created_at) as first_session,
|
|
1793
|
+
MAX(s.modified_at) as last_session,
|
|
1794
|
+
GROUP_CONCAT(DISTINCT s.git_branch) as branches
|
|
1795
|
+
FROM projects p
|
|
1796
|
+
LEFT JOIN sessions s ON s.project_id = p.id
|
|
1797
|
+
GROUP BY p.id
|
|
1798
|
+
ORDER BY last_session DESC`
|
|
1799
|
+
).all();
|
|
1800
|
+
return c.json({ projects });
|
|
1801
|
+
});
|
|
1802
|
+
app3.get("/logo/:id", async (c) => {
|
|
1803
|
+
const db3 = getDb();
|
|
1804
|
+
const id = c.req.param("id");
|
|
1805
|
+
const project = db3.prepare("SELECT logo_path FROM projects WHERE id = ?").get(id);
|
|
1806
|
+
if (!project?.logo_path || !existsSync4(project.logo_path)) {
|
|
1807
|
+
return c.json({ error: "No logo" }, 404);
|
|
1808
|
+
}
|
|
1809
|
+
const ext = extname(project.logo_path).toLowerCase();
|
|
1810
|
+
const mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
1811
|
+
const response = fileResponse(project.logo_path, mime);
|
|
1812
|
+
return new Response(response.body, {
|
|
1813
|
+
headers: {
|
|
1814
|
+
"Content-Type": mime,
|
|
1815
|
+
"Cache-Control": "public, max-age=86400"
|
|
1816
|
+
}
|
|
1817
|
+
});
|
|
1818
|
+
});
|
|
1819
|
+
app3.get("/costs", (c) => {
|
|
1820
|
+
const db3 = getDb();
|
|
1821
|
+
const rows = db3.prepare(
|
|
1822
|
+
`SELECT p.id as project_id, m.model,
|
|
1823
|
+
SUM(m.input_tokens) as input_tokens,
|
|
1824
|
+
SUM(m.output_tokens) as output_tokens,
|
|
1825
|
+
SUM(m.cache_creation_tokens) as cache_creation_tokens,
|
|
1826
|
+
SUM(m.cache_read_tokens) as cache_read_tokens
|
|
1827
|
+
FROM messages m
|
|
1828
|
+
JOIN sessions s ON m.session_id = s.id
|
|
1829
|
+
JOIN projects p ON s.project_id = p.id
|
|
1830
|
+
WHERE m.model IS NOT NULL AND m.model != ''
|
|
1831
|
+
GROUP BY p.id, m.model`
|
|
1832
|
+
).all();
|
|
1833
|
+
const projectCosts = {};
|
|
1834
|
+
const projectCostLists = {};
|
|
1835
|
+
for (const row of rows) {
|
|
1836
|
+
const cost = calculateCost(
|
|
1837
|
+
row.model,
|
|
1838
|
+
row.input_tokens ?? 0,
|
|
1839
|
+
row.output_tokens ?? 0,
|
|
1840
|
+
row.cache_creation_tokens ?? 0,
|
|
1841
|
+
row.cache_read_tokens ?? 0
|
|
1842
|
+
);
|
|
1843
|
+
if (!projectCostLists[row.project_id]) {
|
|
1844
|
+
projectCostLists[row.project_id] = [];
|
|
1845
|
+
}
|
|
1846
|
+
projectCostLists[row.project_id].push(cost);
|
|
1847
|
+
}
|
|
1848
|
+
for (const [pid, costs] of Object.entries(projectCostLists)) {
|
|
1849
|
+
projectCosts[Number(pid)] = aggregateCosts(costs);
|
|
1850
|
+
}
|
|
1851
|
+
return c.json(projectCosts);
|
|
1852
|
+
});
|
|
1853
|
+
app3.get("/:id", (c) => {
|
|
1854
|
+
const db3 = getDb();
|
|
1855
|
+
const id = c.req.param("id");
|
|
1856
|
+
const project = db3.prepare("SELECT * FROM projects WHERE id = ?").get(id);
|
|
1857
|
+
if (!project) {
|
|
1858
|
+
return c.json({ error: "Project not found" }, 404);
|
|
1859
|
+
}
|
|
1860
|
+
const sessions = db3.prepare(
|
|
1861
|
+
`SELECT id, summary, first_prompt, message_count, created_at, modified_at, git_branch, slug, is_sidechain
|
|
1862
|
+
FROM sessions WHERE project_id = ?
|
|
1863
|
+
ORDER BY modified_at DESC`
|
|
1864
|
+
).all(id);
|
|
1865
|
+
return c.json({ project, sessions });
|
|
1866
|
+
});
|
|
1867
|
+
app3.get("/:id/analytics", (c) => {
|
|
1868
|
+
const db3 = getDb();
|
|
1869
|
+
const id = c.req.param("id");
|
|
1870
|
+
const project = db3.prepare("SELECT * FROM projects WHERE id = ?").get(id);
|
|
1871
|
+
if (!project) {
|
|
1872
|
+
return c.json({ error: "Project not found" }, 404);
|
|
1873
|
+
}
|
|
1874
|
+
const modelRows = db3.prepare(
|
|
1875
|
+
`SELECT m.model,
|
|
1876
|
+
SUM(m.input_tokens) as input_tokens,
|
|
1877
|
+
SUM(m.output_tokens) as output_tokens,
|
|
1878
|
+
SUM(m.cache_creation_tokens) as cache_creation_tokens,
|
|
1879
|
+
SUM(m.cache_read_tokens) as cache_read_tokens
|
|
1880
|
+
FROM messages m
|
|
1881
|
+
JOIN sessions s ON m.session_id = s.id
|
|
1882
|
+
WHERE s.project_id = ? AND m.model IS NOT NULL AND m.model != ''
|
|
1883
|
+
GROUP BY m.model`
|
|
1884
|
+
).all(id);
|
|
1885
|
+
const modelBreakdown = {};
|
|
1886
|
+
const allCosts = [];
|
|
1887
|
+
for (const row of modelRows) {
|
|
1888
|
+
const cost = calculateCost(
|
|
1889
|
+
row.model,
|
|
1890
|
+
row.input_tokens ?? 0,
|
|
1891
|
+
row.output_tokens ?? 0,
|
|
1892
|
+
row.cache_creation_tokens ?? 0,
|
|
1893
|
+
row.cache_read_tokens ?? 0
|
|
1894
|
+
);
|
|
1895
|
+
modelBreakdown[row.model] = cost;
|
|
1896
|
+
allCosts.push(cost);
|
|
1897
|
+
}
|
|
1898
|
+
const costs = aggregateCosts(allCosts);
|
|
1899
|
+
const dailyRows = db3.prepare(
|
|
1900
|
+
`SELECT DATE(m.timestamp) as date,
|
|
1901
|
+
SUM(m.input_tokens) as input_tokens,
|
|
1902
|
+
SUM(m.output_tokens) as output_tokens,
|
|
1903
|
+
SUM(m.cache_creation_tokens) as cache_creation_tokens,
|
|
1904
|
+
SUM(m.cache_read_tokens) as cache_read_tokens,
|
|
1905
|
+
m.model
|
|
1906
|
+
FROM messages m
|
|
1907
|
+
JOIN sessions s ON m.session_id = s.id
|
|
1908
|
+
WHERE s.project_id = ? AND m.timestamp IS NOT NULL AND m.model IS NOT NULL AND m.model != ''
|
|
1909
|
+
GROUP BY date, m.model
|
|
1910
|
+
ORDER BY date`
|
|
1911
|
+
).all(id);
|
|
1912
|
+
const dailyMap = /* @__PURE__ */ new Map();
|
|
1913
|
+
for (const row of dailyRows) {
|
|
1914
|
+
const cost = calculateCost(
|
|
1915
|
+
row.model,
|
|
1916
|
+
row.input_tokens ?? 0,
|
|
1917
|
+
row.output_tokens ?? 0,
|
|
1918
|
+
row.cache_creation_tokens ?? 0,
|
|
1919
|
+
row.cache_read_tokens ?? 0
|
|
1920
|
+
);
|
|
1921
|
+
const existing = dailyMap.get(row.date);
|
|
1922
|
+
if (existing) {
|
|
1923
|
+
existing.totalCost += cost.totalCost;
|
|
1924
|
+
existing.inputCost += cost.inputCost;
|
|
1925
|
+
existing.outputCost += cost.outputCost;
|
|
1926
|
+
existing.cacheWriteCost += cost.cacheWriteCost;
|
|
1927
|
+
existing.cacheReadCost += cost.cacheReadCost;
|
|
1928
|
+
} else {
|
|
1929
|
+
dailyMap.set(row.date, {
|
|
1930
|
+
date: row.date,
|
|
1931
|
+
totalCost: cost.totalCost,
|
|
1932
|
+
inputCost: cost.inputCost,
|
|
1933
|
+
outputCost: cost.outputCost,
|
|
1934
|
+
cacheWriteCost: cost.cacheWriteCost,
|
|
1935
|
+
cacheReadCost: cost.cacheReadCost
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
const dailyCosts = [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
1940
|
+
const sessionRows = db3.prepare(
|
|
1941
|
+
`SELECT s.id, s.summary, s.first_prompt, s.slug, s.message_count, s.created_at,
|
|
1942
|
+
m.model,
|
|
1943
|
+
SUM(m.input_tokens) as input_tokens,
|
|
1944
|
+
SUM(m.output_tokens) as output_tokens,
|
|
1945
|
+
SUM(m.cache_creation_tokens) as cache_creation_tokens,
|
|
1946
|
+
SUM(m.cache_read_tokens) as cache_read_tokens
|
|
1947
|
+
FROM sessions s
|
|
1948
|
+
JOIN messages m ON m.session_id = s.id
|
|
1949
|
+
WHERE s.project_id = ? AND m.model IS NOT NULL AND m.model != ''
|
|
1950
|
+
GROUP BY s.id, m.model`
|
|
1951
|
+
).all(id);
|
|
1952
|
+
const sessionCostMap = /* @__PURE__ */ new Map();
|
|
1953
|
+
for (const row of sessionRows) {
|
|
1954
|
+
const cost = calculateCost(
|
|
1955
|
+
row.model,
|
|
1956
|
+
row.input_tokens ?? 0,
|
|
1957
|
+
row.output_tokens ?? 0,
|
|
1958
|
+
row.cache_creation_tokens ?? 0,
|
|
1959
|
+
row.cache_read_tokens ?? 0
|
|
1960
|
+
);
|
|
1961
|
+
const existing = sessionCostMap.get(row.id);
|
|
1962
|
+
if (existing) {
|
|
1963
|
+
existing.costs.push(cost);
|
|
1964
|
+
} else {
|
|
1965
|
+
sessionCostMap.set(row.id, {
|
|
1966
|
+
id: row.id,
|
|
1967
|
+
summary: row.summary,
|
|
1968
|
+
first_prompt: row.first_prompt,
|
|
1969
|
+
slug: row.slug,
|
|
1970
|
+
message_count: row.message_count,
|
|
1971
|
+
created_at: row.created_at,
|
|
1972
|
+
costs: [cost]
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
const topSessionsByCost = [...sessionCostMap.values()].map((s) => ({
|
|
1977
|
+
id: s.id,
|
|
1978
|
+
summary: s.summary,
|
|
1979
|
+
first_prompt: s.first_prompt,
|
|
1980
|
+
slug: s.slug,
|
|
1981
|
+
message_count: s.message_count,
|
|
1982
|
+
created_at: s.created_at,
|
|
1983
|
+
cost: aggregateCosts(s.costs)
|
|
1984
|
+
})).sort((a, b) => b.cost.totalCost - a.cost.totalCost).slice(0, 10);
|
|
1985
|
+
const toolDistribution = db3.prepare(
|
|
1986
|
+
`SELECT t.tool_name, COUNT(*) as count
|
|
1987
|
+
FROM tool_uses t
|
|
1988
|
+
JOIN sessions s ON t.session_id = s.id
|
|
1989
|
+
WHERE s.project_id = ?
|
|
1990
|
+
GROUP BY t.tool_name
|
|
1991
|
+
ORDER BY count DESC
|
|
1992
|
+
LIMIT 15`
|
|
1993
|
+
).all(id);
|
|
1994
|
+
const sessionCount = db3.prepare("SELECT COUNT(*) as cnt FROM sessions WHERE project_id = ?").get(id);
|
|
1995
|
+
const messageCount = db3.prepare(
|
|
1996
|
+
"SELECT COUNT(*) as cnt FROM messages m JOIN sessions s ON m.session_id = s.id WHERE s.project_id = ?"
|
|
1997
|
+
).get(id);
|
|
1998
|
+
return c.json({
|
|
1999
|
+
project,
|
|
2000
|
+
costs,
|
|
2001
|
+
modelBreakdown,
|
|
2002
|
+
dailyCosts,
|
|
2003
|
+
topSessionsByCost,
|
|
2004
|
+
toolDistribution,
|
|
2005
|
+
sessionCount: sessionCount.cnt,
|
|
2006
|
+
messageCount: messageCount.cnt
|
|
2007
|
+
});
|
|
2008
|
+
});
|
|
2009
|
+
app3.get("/:id/plans", (c) => {
|
|
2010
|
+
const db3 = getDb();
|
|
2011
|
+
const id = c.req.param("id");
|
|
2012
|
+
const project = db3.prepare("SELECT id FROM projects WHERE id = ?").get(id);
|
|
2013
|
+
if (!project) {
|
|
2014
|
+
return c.json({ error: "Project not found" }, 404);
|
|
2015
|
+
}
|
|
2016
|
+
const toolUseRows = db3.prepare(
|
|
2017
|
+
`SELECT DISTINCT tu.input_json, tu.session_id
|
|
2018
|
+
FROM tool_uses tu JOIN sessions s ON tu.session_id = s.id
|
|
2019
|
+
WHERE s.project_id = ? AND tu.tool_name IN ('Write','Edit','MultiEdit','Read')
|
|
2020
|
+
AND tu.input_json LIKE '%/.claude/plans/%.md%'`
|
|
2021
|
+
).all(id);
|
|
2022
|
+
const planSessionMap = /* @__PURE__ */ new Map();
|
|
2023
|
+
const planPathRegex = /\.claude\/plans\/([^/]+\.md)/;
|
|
2024
|
+
for (const row of toolUseRows) {
|
|
2025
|
+
try {
|
|
2026
|
+
const input = JSON.parse(row.input_json || "{}");
|
|
2027
|
+
const pathValue = input.file_path || input.path || "";
|
|
2028
|
+
const match = pathValue.match(planPathRegex);
|
|
2029
|
+
if (match) {
|
|
2030
|
+
const filename = match[1];
|
|
2031
|
+
if (!planSessionMap.has(filename)) {
|
|
2032
|
+
planSessionMap.set(filename, /* @__PURE__ */ new Set());
|
|
2033
|
+
}
|
|
2034
|
+
planSessionMap.get(filename).add(row.session_id);
|
|
2035
|
+
}
|
|
2036
|
+
} catch {
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
if (planSessionMap.size === 0) {
|
|
2041
|
+
return c.json({ plans: [] });
|
|
2042
|
+
}
|
|
2043
|
+
const filenames = [...planSessionMap.keys()];
|
|
2044
|
+
const placeholders = filenames.map(() => "?").join(",");
|
|
2045
|
+
const planRows = db3.prepare(
|
|
2046
|
+
`SELECT id, filename, content, mtime FROM plans
|
|
2047
|
+
WHERE filename IN (${placeholders}) ORDER BY mtime DESC`
|
|
2048
|
+
).all(...filenames);
|
|
2049
|
+
const plans = planRows.map((p) => ({
|
|
2050
|
+
...p,
|
|
2051
|
+
session_ids: [...planSessionMap.get(p.filename) ?? []]
|
|
2052
|
+
}));
|
|
2053
|
+
return c.json({ plans });
|
|
2054
|
+
});
|
|
2055
|
+
app3.get("/:id/todos", (c) => {
|
|
2056
|
+
const db3 = getDb();
|
|
2057
|
+
const id = c.req.param("id");
|
|
2058
|
+
const project = db3.prepare("SELECT id FROM projects WHERE id = ?").get(id);
|
|
2059
|
+
if (!project) {
|
|
2060
|
+
return c.json({ error: "Project not found" }, 404);
|
|
2061
|
+
}
|
|
2062
|
+
const todos = db3.prepare(
|
|
2063
|
+
`SELECT t.*, s.summary as session_summary, s.slug as session_slug
|
|
2064
|
+
FROM todos t JOIN sessions s ON t.session_id = s.id
|
|
2065
|
+
WHERE s.project_id = ? ORDER BY t.id DESC`
|
|
2066
|
+
).all(id);
|
|
2067
|
+
const statusCounts = db3.prepare(
|
|
2068
|
+
`SELECT t.status, COUNT(*) as count FROM todos t
|
|
2069
|
+
JOIN sessions s ON t.session_id = s.id WHERE s.project_id = ?
|
|
2070
|
+
GROUP BY t.status`
|
|
2071
|
+
).all(id);
|
|
2072
|
+
return c.json({ todos, statusCounts });
|
|
2073
|
+
});
|
|
2074
|
+
app3.get("/:id/sessions-enriched", (c) => {
|
|
2075
|
+
const db3 = getDb();
|
|
2076
|
+
const id = c.req.param("id");
|
|
2077
|
+
const project = db3.prepare("SELECT id FROM projects WHERE id = ?").get(id);
|
|
2078
|
+
if (!project) {
|
|
2079
|
+
return c.json({ error: "Project not found" }, 404);
|
|
2080
|
+
}
|
|
2081
|
+
const limit = Math.max(1, Math.min(500, parseInt(c.req.query("limit") || "50", 10) || 50));
|
|
2082
|
+
const offset = Math.max(0, parseInt(c.req.query("offset") || "0", 10) || 0);
|
|
2083
|
+
const since = c.req.query("since") || null;
|
|
2084
|
+
const until = c.req.query("until") || null;
|
|
2085
|
+
const whereClauses = ["s.project_id = ?"];
|
|
2086
|
+
const whereParams = [id];
|
|
2087
|
+
if (since) {
|
|
2088
|
+
whereClauses.push("s.modified_at >= ?");
|
|
2089
|
+
whereParams.push(since);
|
|
2090
|
+
}
|
|
2091
|
+
if (until) {
|
|
2092
|
+
whereClauses.push("s.created_at <= ?");
|
|
2093
|
+
whereParams.push(until);
|
|
2094
|
+
}
|
|
2095
|
+
const whereSQL = whereClauses.join(" AND ");
|
|
2096
|
+
const totalRow = db3.prepare(`SELECT COUNT(*) as total FROM sessions s WHERE ${whereSQL}`).get(...whereParams);
|
|
2097
|
+
const total = totalRow.total;
|
|
2098
|
+
const sessions = db3.prepare(
|
|
2099
|
+
`SELECT s.id, s.summary, s.first_prompt, s.message_count,
|
|
2100
|
+
s.created_at, s.modified_at, s.git_branch, s.slug, s.is_sidechain,
|
|
2101
|
+
COALESCE(reads.cnt, 0) as files_read_count,
|
|
2102
|
+
COALESCE(writes.cnt, 0) as files_written_count,
|
|
2103
|
+
COALESCE(plan_check.cnt, 0) > 0 as has_plan,
|
|
2104
|
+
COALESCE(todo_check.cnt, 0) as todo_count,
|
|
2105
|
+
COALESCE(commit_check.cnt, 0) as commit_count
|
|
2106
|
+
FROM sessions s
|
|
2107
|
+
LEFT JOIN (
|
|
2108
|
+
SELECT session_id, COUNT(DISTINCT
|
|
2109
|
+
json_extract(input_json, '$.file_path')
|
|
2110
|
+
) as cnt
|
|
2111
|
+
FROM tool_uses WHERE tool_name = 'Read'
|
|
2112
|
+
GROUP BY session_id
|
|
2113
|
+
) reads ON reads.session_id = s.id
|
|
2114
|
+
LEFT JOIN (
|
|
2115
|
+
SELECT session_id, COUNT(DISTINCT
|
|
2116
|
+
json_extract(input_json, '$.file_path')
|
|
2117
|
+
) as cnt
|
|
2118
|
+
FROM tool_uses WHERE tool_name IN ('Write', 'Edit', 'MultiEdit')
|
|
2119
|
+
GROUP BY session_id
|
|
2120
|
+
) writes ON writes.session_id = s.id
|
|
2121
|
+
LEFT JOIN (
|
|
2122
|
+
SELECT session_id, COUNT(*) as cnt
|
|
2123
|
+
FROM tool_uses
|
|
2124
|
+
WHERE tool_name IN ('Write', 'Edit', 'MultiEdit', 'Read')
|
|
2125
|
+
AND input_json LIKE '%/.claude/plans/%.md%'
|
|
2126
|
+
GROUP BY session_id
|
|
2127
|
+
) plan_check ON plan_check.session_id = s.id
|
|
2128
|
+
LEFT JOIN (
|
|
2129
|
+
SELECT session_id, COUNT(*) as cnt
|
|
2130
|
+
FROM todos GROUP BY session_id
|
|
2131
|
+
) todo_check ON todo_check.session_id = s.id
|
|
2132
|
+
LEFT JOIN (
|
|
2133
|
+
SELECT session_id, COUNT(*) as cnt
|
|
2134
|
+
FROM session_commits GROUP BY session_id
|
|
2135
|
+
) commit_check ON commit_check.session_id = s.id
|
|
2136
|
+
WHERE ${whereSQL}
|
|
2137
|
+
ORDER BY s.modified_at DESC
|
|
2138
|
+
LIMIT ? OFFSET ?`
|
|
2139
|
+
).all(...whereParams, limit, offset);
|
|
2140
|
+
const sessionIds = sessions.map((s) => s.id);
|
|
2141
|
+
const sessionCostMap = /* @__PURE__ */ new Map();
|
|
2142
|
+
if (sessionIds.length > 0) {
|
|
2143
|
+
const placeholders = sessionIds.map(() => "?").join(",");
|
|
2144
|
+
const costRows = db3.prepare(
|
|
2145
|
+
`SELECT m.session_id, m.model,
|
|
2146
|
+
SUM(m.input_tokens) as input_tokens,
|
|
2147
|
+
SUM(m.output_tokens) as output_tokens,
|
|
2148
|
+
SUM(m.cache_creation_tokens) as cache_creation_tokens,
|
|
2149
|
+
SUM(m.cache_read_tokens) as cache_read_tokens
|
|
2150
|
+
FROM messages m
|
|
2151
|
+
WHERE m.session_id IN (${placeholders}) AND m.model IS NOT NULL AND m.model != ''
|
|
2152
|
+
GROUP BY m.session_id, m.model`
|
|
2153
|
+
).all(...sessionIds);
|
|
2154
|
+
for (const row of costRows) {
|
|
2155
|
+
const cost = calculateCost(
|
|
2156
|
+
row.model,
|
|
2157
|
+
row.input_tokens ?? 0,
|
|
2158
|
+
row.output_tokens ?? 0,
|
|
2159
|
+
row.cache_creation_tokens ?? 0,
|
|
2160
|
+
row.cache_read_tokens ?? 0
|
|
2161
|
+
);
|
|
2162
|
+
if (!sessionCostMap.has(row.session_id)) {
|
|
2163
|
+
sessionCostMap.set(row.session_id, []);
|
|
2164
|
+
}
|
|
2165
|
+
sessionCostMap.get(row.session_id).push(cost);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
const planToolRows = db3.prepare(
|
|
2169
|
+
`SELECT DISTINCT tu.input_json, tu.session_id
|
|
2170
|
+
FROM tool_uses tu JOIN sessions s ON tu.session_id = s.id
|
|
2171
|
+
WHERE s.project_id = ? AND tu.tool_name IN ('Write','Edit','MultiEdit','Read')
|
|
2172
|
+
AND tu.input_json LIKE '%/.claude/plans/%.md%'`
|
|
2173
|
+
).all(id);
|
|
2174
|
+
const planSessionMap = {};
|
|
2175
|
+
const planPathRegex = /\.claude\/plans\/([^/]+\.md)/;
|
|
2176
|
+
for (const row of planToolRows) {
|
|
2177
|
+
try {
|
|
2178
|
+
const input = JSON.parse(row.input_json || "{}");
|
|
2179
|
+
const pathValue = input.file_path || input.path || "";
|
|
2180
|
+
const match = pathValue.match(planPathRegex);
|
|
2181
|
+
if (match) {
|
|
2182
|
+
const filename = match[1];
|
|
2183
|
+
if (!planSessionMap[filename]) {
|
|
2184
|
+
planSessionMap[filename] = [];
|
|
2185
|
+
}
|
|
2186
|
+
if (!planSessionMap[filename].includes(row.session_id)) {
|
|
2187
|
+
planSessionMap[filename].push(row.session_id);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
} catch {
|
|
2191
|
+
continue;
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
const enriched = sessions.map((s) => {
|
|
2195
|
+
const costs = sessionCostMap.get(s.id);
|
|
2196
|
+
return {
|
|
2197
|
+
...s,
|
|
2198
|
+
has_plan: !!s.has_plan,
|
|
2199
|
+
total_cost: costs ? aggregateCosts(costs).totalCost : 0
|
|
2200
|
+
};
|
|
2201
|
+
});
|
|
2202
|
+
return c.json({ sessions: enriched, planSessionMap, total, limit, offset });
|
|
2203
|
+
});
|
|
2204
|
+
var projects_default = app3;
|
|
2205
|
+
|
|
2206
|
+
// src/server/api/analytics.ts
|
|
2207
|
+
import { Hono as Hono4 } from "hono";
|
|
2208
|
+
var app4 = new Hono4();
|
|
2209
|
+
app4.get("/usage", (c) => {
|
|
2210
|
+
const db3 = getDb();
|
|
2211
|
+
const projectId = c.req.query("project_id");
|
|
2212
|
+
let modelUsage;
|
|
2213
|
+
let dailyTokens;
|
|
2214
|
+
if (projectId) {
|
|
2215
|
+
const modelRows = db3.prepare(
|
|
2216
|
+
`SELECT m.model,
|
|
2217
|
+
SUM(m.input_tokens) as input_tokens,
|
|
2218
|
+
SUM(m.output_tokens) as output_tokens,
|
|
2219
|
+
SUM(m.cache_read_tokens) as cache_read_tokens,
|
|
2220
|
+
SUM(m.cache_creation_tokens) as cache_creation_tokens
|
|
2221
|
+
FROM messages m
|
|
2222
|
+
JOIN sessions s ON m.session_id = s.id
|
|
2223
|
+
WHERE s.project_id = ? AND m.model IS NOT NULL AND m.model != ''
|
|
2224
|
+
GROUP BY m.model`
|
|
2225
|
+
).all(projectId);
|
|
2226
|
+
modelUsage = {};
|
|
2227
|
+
for (const row of modelRows) {
|
|
2228
|
+
modelUsage[row.model] = {
|
|
2229
|
+
inputTokens: row.input_tokens ?? 0,
|
|
2230
|
+
outputTokens: row.output_tokens ?? 0,
|
|
2231
|
+
cacheReadInputTokens: row.cache_read_tokens ?? 0,
|
|
2232
|
+
cacheCreationInputTokens: row.cache_creation_tokens ?? 0
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
const dailyRows = db3.prepare(
|
|
2236
|
+
`SELECT DATE(m.timestamp) as date, m.model,
|
|
2237
|
+
SUM(m.input_tokens + m.output_tokens + m.cache_read_tokens + m.cache_creation_tokens) as total_tokens
|
|
2238
|
+
FROM messages m
|
|
2239
|
+
JOIN sessions s ON m.session_id = s.id
|
|
2240
|
+
WHERE s.project_id = ? AND m.timestamp IS NOT NULL AND m.model IS NOT NULL AND m.model != ''
|
|
2241
|
+
GROUP BY date, m.model
|
|
2242
|
+
ORDER BY date`
|
|
2243
|
+
).all(projectId);
|
|
2244
|
+
const dailyMap = /* @__PURE__ */ new Map();
|
|
2245
|
+
for (const row of dailyRows) {
|
|
2246
|
+
if (!dailyMap.has(row.date)) dailyMap.set(row.date, {});
|
|
2247
|
+
dailyMap.get(row.date)[row.model] = row.total_tokens ?? 0;
|
|
2248
|
+
}
|
|
2249
|
+
dailyTokens = [...dailyMap.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([date, tokensByModel]) => ({ date, tokensByModel }));
|
|
2250
|
+
} else {
|
|
2251
|
+
const modelUsageRaw = db3.prepare("SELECT value FROM stats_cache WHERE key = 'modelUsage'").get();
|
|
2252
|
+
modelUsage = modelUsageRaw ? JSON.parse(modelUsageRaw.value) : {};
|
|
2253
|
+
dailyTokens = db3.prepare(
|
|
2254
|
+
"SELECT date, tokens_by_model FROM daily_stats WHERE tokens_by_model != '{}' ORDER BY date"
|
|
2255
|
+
).all().map((row) => ({
|
|
2256
|
+
date: row.date,
|
|
2257
|
+
tokensByModel: JSON.parse(row.tokens_by_model)
|
|
2258
|
+
}));
|
|
2259
|
+
}
|
|
2260
|
+
let totalInput = 0;
|
|
2261
|
+
let totalOutput = 0;
|
|
2262
|
+
let totalCacheRead = 0;
|
|
2263
|
+
let totalCacheCreation = 0;
|
|
2264
|
+
for (const [model, usage] of Object.entries(modelUsage)) {
|
|
2265
|
+
totalInput += usage.inputTokens || 0;
|
|
2266
|
+
totalOutput += usage.outputTokens || 0;
|
|
2267
|
+
totalCacheRead += usage.cacheReadInputTokens || 0;
|
|
2268
|
+
totalCacheCreation += usage.cacheCreationInputTokens || 0;
|
|
2269
|
+
}
|
|
2270
|
+
const cacheHitRate = totalCacheRead + totalCacheCreation > 0 ? totalCacheRead / (totalCacheRead + totalCacheCreation + totalInput) : 0;
|
|
2271
|
+
const costList = [];
|
|
2272
|
+
const perModelCosts = {};
|
|
2273
|
+
for (const [model, usage] of Object.entries(modelUsage)) {
|
|
2274
|
+
const cost = calculateCost(
|
|
2275
|
+
model,
|
|
2276
|
+
usage.inputTokens || 0,
|
|
2277
|
+
usage.outputTokens || 0,
|
|
2278
|
+
usage.cacheCreationInputTokens || 0,
|
|
2279
|
+
usage.cacheReadInputTokens || 0
|
|
2280
|
+
);
|
|
2281
|
+
perModelCosts[model] = cost;
|
|
2282
|
+
costList.push(cost);
|
|
2283
|
+
}
|
|
2284
|
+
const costTotals = costList.length > 0 ? aggregateCosts(costList) : null;
|
|
2285
|
+
return c.json({
|
|
2286
|
+
modelUsage,
|
|
2287
|
+
dailyTokens,
|
|
2288
|
+
totals: {
|
|
2289
|
+
inputTokens: totalInput,
|
|
2290
|
+
outputTokens: totalOutput,
|
|
2291
|
+
cacheReadTokens: totalCacheRead,
|
|
2292
|
+
cacheCreationTokens: totalCacheCreation,
|
|
2293
|
+
cacheHitRate
|
|
2294
|
+
},
|
|
2295
|
+
costTotals,
|
|
2296
|
+
perModelCosts
|
|
2297
|
+
});
|
|
2298
|
+
});
|
|
2299
|
+
app4.get("/tools", (c) => {
|
|
2300
|
+
const db3 = getDb();
|
|
2301
|
+
const projectId = c.req.query("project_id");
|
|
2302
|
+
const projectJoin = projectId ? "JOIN sessions s ON t.session_id = s.id" : "";
|
|
2303
|
+
const projectWhere = projectId ? "AND s.project_id = ?" : "";
|
|
2304
|
+
const projectParams = projectId ? [projectId] : [];
|
|
2305
|
+
const topTools = db3.prepare(
|
|
2306
|
+
`SELECT t.tool_name, COUNT(*) as count
|
|
2307
|
+
FROM tool_uses t
|
|
2308
|
+
${projectJoin}
|
|
2309
|
+
WHERE 1=1 ${projectWhere}
|
|
2310
|
+
GROUP BY t.tool_name
|
|
2311
|
+
ORDER BY count DESC
|
|
2312
|
+
LIMIT 20`
|
|
2313
|
+
).all(...projectParams);
|
|
2314
|
+
const sessionProjectWhere = projectId ? "WHERE s.project_id = ?" : "";
|
|
2315
|
+
const toolsBySession = db3.prepare(
|
|
2316
|
+
`SELECT t.session_id, s.slug, s.first_prompt, COUNT(*) as tool_count
|
|
2317
|
+
FROM tool_uses t
|
|
2318
|
+
LEFT JOIN sessions s ON t.session_id = s.id
|
|
2319
|
+
${sessionProjectWhere}
|
|
2320
|
+
GROUP BY t.session_id
|
|
2321
|
+
ORDER BY tool_count DESC
|
|
2322
|
+
LIMIT 10`
|
|
2323
|
+
).all(...projectParams);
|
|
2324
|
+
const dailyTools = db3.prepare(
|
|
2325
|
+
`SELECT DATE(t.timestamp) as date, t.tool_name, COUNT(*) as count
|
|
2326
|
+
FROM tool_uses t
|
|
2327
|
+
${projectJoin}
|
|
2328
|
+
WHERE t.timestamp IS NOT NULL ${projectWhere}
|
|
2329
|
+
GROUP BY date, t.tool_name
|
|
2330
|
+
ORDER BY date`
|
|
2331
|
+
).all(...projectParams);
|
|
2332
|
+
return c.json({ topTools, toolsBySession, dailyTools });
|
|
2333
|
+
});
|
|
2334
|
+
app4.get("/hourly", (c) => {
|
|
2335
|
+
const db3 = getDb();
|
|
2336
|
+
const projectId = c.req.query("project_id");
|
|
2337
|
+
let hourCounts;
|
|
2338
|
+
let dayOfWeek;
|
|
2339
|
+
let heatmap;
|
|
2340
|
+
if (projectId) {
|
|
2341
|
+
const hourRows = db3.prepare(
|
|
2342
|
+
`SELECT
|
|
2343
|
+
CAST(strftime('%H', datetime(h.timestamp/1000, 'unixepoch')) AS INTEGER) as hour,
|
|
2344
|
+
COUNT(*) as count
|
|
2345
|
+
FROM history_entries h
|
|
2346
|
+
JOIN sessions s ON h.session_id = s.id
|
|
2347
|
+
WHERE h.timestamp > 0 AND s.project_id = ?
|
|
2348
|
+
GROUP BY hour`
|
|
2349
|
+
).all(projectId);
|
|
2350
|
+
hourCounts = {};
|
|
2351
|
+
for (const row of hourRows) {
|
|
2352
|
+
hourCounts[String(row.hour)] = row.count;
|
|
2353
|
+
}
|
|
2354
|
+
dayOfWeek = db3.prepare(
|
|
2355
|
+
`SELECT
|
|
2356
|
+
CAST(strftime('%w', datetime(h.timestamp/1000, 'unixepoch')) AS INTEGER) as dow,
|
|
2357
|
+
COUNT(*) as count
|
|
2358
|
+
FROM history_entries h
|
|
2359
|
+
JOIN sessions s ON h.session_id = s.id
|
|
2360
|
+
WHERE h.timestamp > 0 AND s.project_id = ?
|
|
2361
|
+
GROUP BY dow
|
|
2362
|
+
ORDER BY dow`
|
|
2363
|
+
).all(projectId);
|
|
2364
|
+
heatmap = db3.prepare(
|
|
2365
|
+
`SELECT
|
|
2366
|
+
CAST(strftime('%w', datetime(h.timestamp/1000, 'unixepoch')) AS INTEGER) as dow,
|
|
2367
|
+
CAST(strftime('%H', datetime(h.timestamp/1000, 'unixepoch')) AS INTEGER) as hour,
|
|
2368
|
+
COUNT(*) as count
|
|
2369
|
+
FROM history_entries h
|
|
2370
|
+
JOIN sessions s ON h.session_id = s.id
|
|
2371
|
+
WHERE h.timestamp > 0 AND s.project_id = ?
|
|
2372
|
+
GROUP BY dow, hour`
|
|
2373
|
+
).all(projectId);
|
|
2374
|
+
} else {
|
|
2375
|
+
const hourCountsRaw = db3.prepare("SELECT value FROM stats_cache WHERE key = 'hourCounts'").get();
|
|
2376
|
+
hourCounts = hourCountsRaw ? JSON.parse(hourCountsRaw.value) : {};
|
|
2377
|
+
dayOfWeek = db3.prepare(
|
|
2378
|
+
`SELECT
|
|
2379
|
+
CAST(strftime('%w', datetime(timestamp/1000, 'unixepoch')) AS INTEGER) as dow,
|
|
2380
|
+
COUNT(*) as count
|
|
2381
|
+
FROM history_entries
|
|
2382
|
+
WHERE timestamp > 0
|
|
2383
|
+
GROUP BY dow
|
|
2384
|
+
ORDER BY dow`
|
|
2385
|
+
).all();
|
|
2386
|
+
heatmap = db3.prepare(
|
|
2387
|
+
`SELECT
|
|
2388
|
+
CAST(strftime('%w', datetime(timestamp/1000, 'unixepoch')) AS INTEGER) as dow,
|
|
2389
|
+
CAST(strftime('%H', datetime(timestamp/1000, 'unixepoch')) AS INTEGER) as hour,
|
|
2390
|
+
COUNT(*) as count
|
|
2391
|
+
FROM history_entries
|
|
2392
|
+
WHERE timestamp > 0
|
|
2393
|
+
GROUP BY dow, hour`
|
|
2394
|
+
).all();
|
|
2395
|
+
}
|
|
2396
|
+
return c.json({ hourCounts, dayOfWeek, heatmap });
|
|
2397
|
+
});
|
|
2398
|
+
var analytics_default = app4;
|
|
2399
|
+
|
|
2400
|
+
// src/server/api/search.ts
|
|
2401
|
+
import { Hono as Hono5 } from "hono";
|
|
2402
|
+
var app5 = new Hono5();
|
|
2403
|
+
app5.get("/", (c) => {
|
|
2404
|
+
const db3 = getDb();
|
|
2405
|
+
const q = c.req.query("q");
|
|
2406
|
+
const limit = parseInt(c.req.query("limit") || "30");
|
|
2407
|
+
const projectId = c.req.query("project_id");
|
|
2408
|
+
if (!q || q.trim().length === 0) {
|
|
2409
|
+
return c.json({ results: [], query: "" });
|
|
2410
|
+
}
|
|
2411
|
+
const ftsQuery = q.replace(/['"*()]/g, " ").trim();
|
|
2412
|
+
if (!ftsQuery) {
|
|
2413
|
+
return c.json({ results: [], query: q });
|
|
2414
|
+
}
|
|
2415
|
+
const projectFilter = projectId ? " AND s.project_id = ?" : "";
|
|
2416
|
+
const results = [];
|
|
2417
|
+
try {
|
|
2418
|
+
const params = [ftsQuery];
|
|
2419
|
+
if (projectId) params.push(projectId);
|
|
2420
|
+
params.push(limit);
|
|
2421
|
+
const messageResults = db3.prepare(
|
|
2422
|
+
`SELECT m.id, m.session_id, m.type, m.role, m.timestamp,
|
|
2423
|
+
snippet(messages_fts, 0, '<mark>', '</mark>', '...', 40) as snippet,
|
|
2424
|
+
s.slug, s.first_prompt, p.display_name as project_name
|
|
2425
|
+
FROM messages_fts
|
|
2426
|
+
JOIN messages m ON messages_fts.rowid = m.id
|
|
2427
|
+
LEFT JOIN sessions s ON m.session_id = s.id
|
|
2428
|
+
LEFT JOIN projects p ON s.project_id = p.id
|
|
2429
|
+
WHERE messages_fts MATCH ?${projectFilter}
|
|
2430
|
+
ORDER BY rank
|
|
2431
|
+
LIMIT ?`
|
|
2432
|
+
).all(...params);
|
|
2433
|
+
for (const r of messageResults) {
|
|
2434
|
+
results.push({ ...r, resultType: "message" });
|
|
2435
|
+
}
|
|
2436
|
+
} catch {
|
|
2437
|
+
}
|
|
2438
|
+
try {
|
|
2439
|
+
const params = [ftsQuery];
|
|
2440
|
+
if (projectId) params.push(projectId);
|
|
2441
|
+
params.push(limit);
|
|
2442
|
+
const sessionResults = db3.prepare(
|
|
2443
|
+
`SELECT s.id as session_id, s.slug, s.created_at, s.modified_at, s.message_count,
|
|
2444
|
+
snippet(sessions_fts, 0, '<mark>', '</mark>', '...', 40) as summary_snippet,
|
|
2445
|
+
snippet(sessions_fts, 1, '<mark>', '</mark>', '...', 40) as prompt_snippet,
|
|
2446
|
+
p.display_name as project_name
|
|
2447
|
+
FROM sessions_fts
|
|
2448
|
+
JOIN sessions s ON sessions_fts.rowid = s.rowid
|
|
2449
|
+
LEFT JOIN projects p ON s.project_id = p.id
|
|
2450
|
+
WHERE sessions_fts MATCH ?${projectFilter}
|
|
2451
|
+
ORDER BY rank
|
|
2452
|
+
LIMIT ?`
|
|
2453
|
+
).all(...params);
|
|
2454
|
+
for (const r of sessionResults) {
|
|
2455
|
+
results.push({ ...r, resultType: "session" });
|
|
2456
|
+
}
|
|
2457
|
+
} catch {
|
|
2458
|
+
}
|
|
2459
|
+
try {
|
|
2460
|
+
const planResults = db3.prepare(
|
|
2461
|
+
`SELECT p.filename, p.mtime,
|
|
2462
|
+
snippet(plans_fts, 0, '<mark>', '</mark>', '...', 40) as filename_snippet,
|
|
2463
|
+
snippet(plans_fts, 1, '<mark>', '</mark>', '...', 40) as content_snippet
|
|
2464
|
+
FROM plans_fts
|
|
2465
|
+
JOIN plans p ON plans_fts.rowid = p.id
|
|
2466
|
+
WHERE plans_fts MATCH ?
|
|
2467
|
+
ORDER BY rank
|
|
2468
|
+
LIMIT ?`
|
|
2469
|
+
).all(ftsQuery, limit);
|
|
2470
|
+
for (const r of planResults) {
|
|
2471
|
+
results.push({ ...r, resultType: "plan" });
|
|
2472
|
+
}
|
|
2473
|
+
} catch {
|
|
2474
|
+
}
|
|
2475
|
+
return c.json({ results, query: q });
|
|
2476
|
+
});
|
|
2477
|
+
var search_default = app5;
|
|
2478
|
+
|
|
2479
|
+
// src/server/api/plans.ts
|
|
2480
|
+
import { Hono as Hono6 } from "hono";
|
|
2481
|
+
var app6 = new Hono6();
|
|
2482
|
+
app6.get("/", (c) => {
|
|
2483
|
+
const db3 = getDb();
|
|
2484
|
+
const plans = db3.prepare(
|
|
2485
|
+
"SELECT id, filename, mtime FROM plans ORDER BY mtime DESC"
|
|
2486
|
+
).all();
|
|
2487
|
+
return c.json({ plans });
|
|
2488
|
+
});
|
|
2489
|
+
app6.get("/:name", (c) => {
|
|
2490
|
+
const db3 = getDb();
|
|
2491
|
+
const name = c.req.param("name");
|
|
2492
|
+
const filename = name.endsWith(".md") ? name : `${name}.md`;
|
|
2493
|
+
const plan = db3.prepare("SELECT * FROM plans WHERE filename = ?").get(filename);
|
|
2494
|
+
if (!plan) {
|
|
2495
|
+
return c.json({ error: "Plan not found" }, 404);
|
|
2496
|
+
}
|
|
2497
|
+
return c.json(plan);
|
|
2498
|
+
});
|
|
2499
|
+
var plans_default = app6;
|
|
2500
|
+
|
|
2501
|
+
// src/server/api/todos.ts
|
|
2502
|
+
import { Hono as Hono7 } from "hono";
|
|
2503
|
+
var app7 = new Hono7();
|
|
2504
|
+
app7.get("/", (c) => {
|
|
2505
|
+
const db3 = getDb();
|
|
2506
|
+
const status = c.req.query("status");
|
|
2507
|
+
const sessionId = c.req.query("session");
|
|
2508
|
+
let where = "1=1";
|
|
2509
|
+
const params = [];
|
|
2510
|
+
if (status) {
|
|
2511
|
+
where += " AND status = ?";
|
|
2512
|
+
params.push(status);
|
|
2513
|
+
}
|
|
2514
|
+
if (sessionId) {
|
|
2515
|
+
where += " AND session_id = ?";
|
|
2516
|
+
params.push(sessionId);
|
|
2517
|
+
}
|
|
2518
|
+
const todos = db3.prepare(
|
|
2519
|
+
`SELECT * FROM todos WHERE ${where} ORDER BY id DESC`
|
|
2520
|
+
).all(...params);
|
|
2521
|
+
let countWhere = "1=1";
|
|
2522
|
+
const countParams = [];
|
|
2523
|
+
if (sessionId) {
|
|
2524
|
+
countWhere += " AND session_id = ?";
|
|
2525
|
+
countParams.push(sessionId);
|
|
2526
|
+
}
|
|
2527
|
+
const statusCounts = db3.prepare(
|
|
2528
|
+
`SELECT status, COUNT(*) as count FROM todos WHERE ${countWhere} GROUP BY status`
|
|
2529
|
+
).all(...countParams);
|
|
2530
|
+
return c.json({ todos, statusCounts });
|
|
2531
|
+
});
|
|
2532
|
+
var todos_default = app7;
|
|
2533
|
+
|
|
2534
|
+
// src/server/api/file-history.ts
|
|
2535
|
+
import { Hono as Hono8 } from "hono";
|
|
2536
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
2537
|
+
import { join as join8 } from "path";
|
|
2538
|
+
import { homedir as homedir2 } from "os";
|
|
2539
|
+
var app8 = new Hono8();
|
|
2540
|
+
var CLAUDE_DIR = join8(homedir2(), ".claude");
|
|
2541
|
+
app8.get("/:session", (c) => {
|
|
2542
|
+
const db3 = getDb();
|
|
2543
|
+
const sessionId = c.req.param("session");
|
|
2544
|
+
const files = db3.prepare(
|
|
2545
|
+
`SELECT * FROM file_history WHERE session_id = ? ORDER BY file_path, version`
|
|
2546
|
+
).all(sessionId);
|
|
2547
|
+
return c.json({ files, sessionId });
|
|
2548
|
+
});
|
|
2549
|
+
app8.get("/:session/:filename", (c) => {
|
|
2550
|
+
const sessionId = c.req.param("session");
|
|
2551
|
+
const filename = c.req.param("filename");
|
|
2552
|
+
if (filename.includes("..") || filename.includes("/")) {
|
|
2553
|
+
return c.json({ error: "Invalid filename" }, 400);
|
|
2554
|
+
}
|
|
2555
|
+
const filePath = join8(
|
|
2556
|
+
CLAUDE_DIR,
|
|
2557
|
+
"file-history",
|
|
2558
|
+
sessionId,
|
|
2559
|
+
filename
|
|
2560
|
+
);
|
|
2561
|
+
try {
|
|
2562
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
2563
|
+
return c.json({ content, filename, sessionId });
|
|
2564
|
+
} catch {
|
|
2565
|
+
return c.json({ error: "File not found" }, 404);
|
|
2566
|
+
}
|
|
2567
|
+
});
|
|
2568
|
+
var file_history_default = app8;
|
|
2569
|
+
|
|
2570
|
+
// src/server/api/debug.ts
|
|
2571
|
+
import { Hono as Hono9 } from "hono";
|
|
2572
|
+
import { join as join9 } from "path";
|
|
2573
|
+
import { homedir as homedir3 } from "os";
|
|
2574
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2575
|
+
var app9 = new Hono9();
|
|
2576
|
+
var CLAUDE_DIR2 = join9(homedir3(), ".claude");
|
|
2577
|
+
app9.get("/:session", (c) => {
|
|
2578
|
+
const sessionId = c.req.param("session");
|
|
2579
|
+
if (sessionId.includes("..") || sessionId.includes("/")) {
|
|
2580
|
+
return c.json({ error: "Invalid session ID" }, 400);
|
|
2581
|
+
}
|
|
2582
|
+
const filePath = join9(CLAUDE_DIR2, "debug", `${sessionId}.txt`);
|
|
2583
|
+
if (!existsSync5(filePath)) {
|
|
2584
|
+
return c.json({ error: "Debug log not found" }, 404);
|
|
2585
|
+
}
|
|
2586
|
+
const response = fileStreamResponse(filePath, "text/plain");
|
|
2587
|
+
return new Response(response.body, {
|
|
2588
|
+
headers: {
|
|
2589
|
+
"Content-Type": "text/plain",
|
|
2590
|
+
"Cache-Control": "no-store"
|
|
2591
|
+
}
|
|
2592
|
+
});
|
|
2593
|
+
});
|
|
2594
|
+
var debug_default = app9;
|
|
2595
|
+
|
|
2596
|
+
// src/server/api/costs.ts
|
|
2597
|
+
import { Hono as Hono10 } from "hono";
|
|
2598
|
+
var app10 = new Hono10();
|
|
2599
|
+
app10.get("/", (c) => {
|
|
2600
|
+
const db3 = getDb();
|
|
2601
|
+
const projectId = c.req.query("project_id");
|
|
2602
|
+
const projectJoin = projectId ? "JOIN sessions s ON m.session_id = s.id" : "";
|
|
2603
|
+
const projectWhere = projectId ? "AND s.project_id = ?" : "";
|
|
2604
|
+
const projectParams = projectId ? [projectId] : [];
|
|
2605
|
+
const modelRows = db3.prepare(
|
|
2606
|
+
`SELECT m.model,
|
|
2607
|
+
SUM(m.input_tokens) as input_tokens,
|
|
2608
|
+
SUM(m.output_tokens) as output_tokens,
|
|
2609
|
+
SUM(m.cache_creation_tokens) as cache_creation_tokens,
|
|
2610
|
+
SUM(m.cache_read_tokens) as cache_read_tokens
|
|
2611
|
+
FROM messages m
|
|
2612
|
+
${projectJoin}
|
|
2613
|
+
WHERE m.model IS NOT NULL AND m.model != ''
|
|
2614
|
+
${projectWhere}
|
|
2615
|
+
GROUP BY m.model`
|
|
2616
|
+
).all(...projectParams);
|
|
2617
|
+
const perModelCosts = {};
|
|
2618
|
+
const allCosts = [];
|
|
2619
|
+
for (const row of modelRows) {
|
|
2620
|
+
const cost = calculateCost(
|
|
2621
|
+
row.model,
|
|
2622
|
+
row.input_tokens ?? 0,
|
|
2623
|
+
row.output_tokens ?? 0,
|
|
2624
|
+
row.cache_creation_tokens ?? 0,
|
|
2625
|
+
row.cache_read_tokens ?? 0
|
|
2626
|
+
);
|
|
2627
|
+
perModelCosts[row.model] = { ...cost, model: row.model };
|
|
2628
|
+
allCosts.push(cost);
|
|
2629
|
+
}
|
|
2630
|
+
const totals = aggregateCosts(allCosts);
|
|
2631
|
+
const dailyRows = db3.prepare(
|
|
2632
|
+
`SELECT DATE(m.timestamp) as date, m.model,
|
|
2633
|
+
SUM(m.input_tokens) as input_tokens,
|
|
2634
|
+
SUM(m.output_tokens) as output_tokens,
|
|
2635
|
+
SUM(m.cache_creation_tokens) as cache_creation_tokens,
|
|
2636
|
+
SUM(m.cache_read_tokens) as cache_read_tokens
|
|
2637
|
+
FROM messages m
|
|
2638
|
+
${projectJoin}
|
|
2639
|
+
WHERE m.timestamp IS NOT NULL AND m.model IS NOT NULL AND m.model != ''
|
|
2640
|
+
${projectWhere}
|
|
2641
|
+
GROUP BY date, m.model
|
|
2642
|
+
ORDER BY date`
|
|
2643
|
+
).all(...projectParams);
|
|
2644
|
+
const dailyMap = /* @__PURE__ */ new Map();
|
|
2645
|
+
for (const row of dailyRows) {
|
|
2646
|
+
const cost = calculateCost(
|
|
2647
|
+
row.model,
|
|
2648
|
+
row.input_tokens ?? 0,
|
|
2649
|
+
row.output_tokens ?? 0,
|
|
2650
|
+
row.cache_creation_tokens ?? 0,
|
|
2651
|
+
row.cache_read_tokens ?? 0
|
|
2652
|
+
);
|
|
2653
|
+
const existing = dailyMap.get(row.date);
|
|
2654
|
+
if (existing) {
|
|
2655
|
+
existing.inputCost += cost.inputCost;
|
|
2656
|
+
existing.outputCost += cost.outputCost;
|
|
2657
|
+
existing.cacheWriteCost += cost.cacheWriteCost;
|
|
2658
|
+
existing.cacheReadCost += cost.cacheReadCost;
|
|
2659
|
+
existing.totalCost += cost.totalCost;
|
|
2660
|
+
existing.cacheSavings += cost.cacheSavings;
|
|
2661
|
+
} else {
|
|
2662
|
+
dailyMap.set(row.date, {
|
|
2663
|
+
date: row.date,
|
|
2664
|
+
inputCost: cost.inputCost,
|
|
2665
|
+
outputCost: cost.outputCost,
|
|
2666
|
+
cacheWriteCost: cost.cacheWriteCost,
|
|
2667
|
+
cacheReadCost: cost.cacheReadCost,
|
|
2668
|
+
totalCost: cost.totalCost,
|
|
2669
|
+
cacheSavings: cost.cacheSavings
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
const dailyCosts = [...dailyMap.values()].sort(
|
|
2674
|
+
(a, b) => a.date.localeCompare(b.date)
|
|
2675
|
+
);
|
|
2676
|
+
const now = /* @__PURE__ */ new Date();
|
|
2677
|
+
const day7 = new Date(now);
|
|
2678
|
+
day7.setDate(day7.getDate() - 7);
|
|
2679
|
+
const day30 = new Date(now);
|
|
2680
|
+
day30.setDate(day30.getDate() - 30);
|
|
2681
|
+
const day7Str = day7.toISOString().slice(0, 10);
|
|
2682
|
+
const day30Str = day30.toISOString().slice(0, 10);
|
|
2683
|
+
let cost7d = 0;
|
|
2684
|
+
let cost30d = 0;
|
|
2685
|
+
for (const d of dailyCosts) {
|
|
2686
|
+
if (d.date >= day7Str) cost7d += d.totalCost;
|
|
2687
|
+
if (d.date >= day30Str) cost30d += d.totalCost;
|
|
2688
|
+
}
|
|
2689
|
+
const rates = {
|
|
2690
|
+
perDay7: cost7d / 7,
|
|
2691
|
+
perWeek7: cost7d,
|
|
2692
|
+
perDay30: cost30d / 30,
|
|
2693
|
+
perWeek30: cost30d / 30 * 7
|
|
2694
|
+
};
|
|
2695
|
+
return c.json({
|
|
2696
|
+
totals,
|
|
2697
|
+
perModelCosts,
|
|
2698
|
+
dailyCosts,
|
|
2699
|
+
rates
|
|
2700
|
+
});
|
|
2701
|
+
});
|
|
2702
|
+
var costs_default = app10;
|
|
2703
|
+
|
|
2704
|
+
// src/server/api/commits.ts
|
|
2705
|
+
import { Hono as Hono11 } from "hono";
|
|
2706
|
+
var app11 = new Hono11();
|
|
2707
|
+
app11.get("/session/:id", (c) => {
|
|
2708
|
+
const db3 = getDb();
|
|
2709
|
+
const sessionId = c.req.param("id");
|
|
2710
|
+
const commits = db3.prepare(
|
|
2711
|
+
`SELECT c.*, sc.match_type
|
|
2712
|
+
FROM commits c
|
|
2713
|
+
JOIN session_commits sc ON sc.commit_id = c.id
|
|
2714
|
+
WHERE sc.session_id = ?
|
|
2715
|
+
ORDER BY c.timestamp DESC`
|
|
2716
|
+
).all(sessionId);
|
|
2717
|
+
return c.json({ commits });
|
|
2718
|
+
});
|
|
2719
|
+
app11.get("/project/:id", (c) => {
|
|
2720
|
+
const db3 = getDb();
|
|
2721
|
+
const projectId = c.req.param("id");
|
|
2722
|
+
const limit = parseInt(c.req.query("limit") || "100");
|
|
2723
|
+
const offset = parseInt(c.req.query("offset") || "0");
|
|
2724
|
+
const commits = db3.prepare(
|
|
2725
|
+
`SELECT c.*,
|
|
2726
|
+
GROUP_CONCAT(DISTINCT sc.session_id) as session_ids,
|
|
2727
|
+
GROUP_CONCAT(DISTINCT sc.match_type) as match_types
|
|
2728
|
+
FROM commits c
|
|
2729
|
+
LEFT JOIN session_commits sc ON sc.commit_id = c.id
|
|
2730
|
+
WHERE c.project_id = ?
|
|
2731
|
+
GROUP BY c.id
|
|
2732
|
+
ORDER BY c.timestamp DESC
|
|
2733
|
+
LIMIT ? OFFSET ?`
|
|
2734
|
+
).all(projectId, limit, offset);
|
|
2735
|
+
const total = db3.prepare("SELECT COUNT(*) as cnt FROM commits WHERE project_id = ?").get(projectId);
|
|
2736
|
+
const enriched = commits.map((c2) => ({
|
|
2737
|
+
...c2,
|
|
2738
|
+
session_ids: c2.session_ids ? c2.session_ids.split(",") : [],
|
|
2739
|
+
match_types: c2.match_types ? c2.match_types.split(",") : []
|
|
2740
|
+
}));
|
|
2741
|
+
return c.json({ commits: enriched, total: total.cnt, limit, offset });
|
|
2742
|
+
});
|
|
2743
|
+
var commits_default = app11;
|
|
2744
|
+
|
|
2745
|
+
// src/server/api/watcher-api.ts
|
|
2746
|
+
import { Hono as Hono12 } from "hono";
|
|
2747
|
+
function createWatcherApi(ctx2) {
|
|
2748
|
+
const app13 = new Hono12();
|
|
2749
|
+
app13.post("/start", (c) => {
|
|
2750
|
+
startWatcher(ctx2);
|
|
2751
|
+
return c.json({ running: true });
|
|
2752
|
+
});
|
|
2753
|
+
app13.post("/stop", (c) => {
|
|
2754
|
+
stopWatcher();
|
|
2755
|
+
return c.json({ running: false });
|
|
2756
|
+
});
|
|
2757
|
+
app13.get("/status", (c) => {
|
|
2758
|
+
return c.json({ running: isWatcherRunning() });
|
|
2759
|
+
});
|
|
2760
|
+
return app13;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
// src/server/api/hooks-api.ts
|
|
2764
|
+
import { Hono as Hono13 } from "hono";
|
|
2765
|
+
import { join as join10 } from "path";
|
|
2766
|
+
import { homedir as homedir4 } from "os";
|
|
2767
|
+
import { readFileSync as readFileSync9, writeFileSync, existsSync as existsSync6, mkdirSync as mkdirSync2 } from "fs";
|
|
2768
|
+
var SETTINGS_PATH = join10(homedir4(), ".claude", "settings.json");
|
|
2769
|
+
var HOOK_MARKER = "#cloviz-hook";
|
|
2770
|
+
var PORT = parseInt(process.env.PORT || "3456");
|
|
2771
|
+
function makeHookCommand() {
|
|
2772
|
+
return `curl -s -X POST http://localhost:${PORT}/api/hooks/notify -H 'Content-Type: application/json' -d @- > /dev/null 2>&1 || true ${HOOK_MARKER}`;
|
|
2773
|
+
}
|
|
2774
|
+
function makeHookEntry() {
|
|
2775
|
+
return {
|
|
2776
|
+
matcher: "",
|
|
2777
|
+
hooks: [
|
|
2778
|
+
{
|
|
2779
|
+
type: "command",
|
|
2780
|
+
command: makeHookCommand(),
|
|
2781
|
+
timeout: 600,
|
|
2782
|
+
async: true
|
|
2783
|
+
}
|
|
2784
|
+
]
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2787
|
+
function readSettings() {
|
|
2788
|
+
try {
|
|
2789
|
+
if (!existsSync6(SETTINGS_PATH)) return {};
|
|
2790
|
+
const raw = readFileSync9(SETTINGS_PATH, "utf-8");
|
|
2791
|
+
return JSON.parse(raw);
|
|
2792
|
+
} catch {
|
|
2793
|
+
return {};
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
function writeSettings(settings) {
|
|
2797
|
+
const dir = join10(homedir4(), ".claude");
|
|
2798
|
+
if (!existsSync6(dir)) mkdirSync2(dir, { recursive: true });
|
|
2799
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2800
|
+
}
|
|
2801
|
+
function hasClovizHook(eventHooks) {
|
|
2802
|
+
if (!Array.isArray(eventHooks)) return false;
|
|
2803
|
+
return eventHooks.some((group) => {
|
|
2804
|
+
if (!Array.isArray(group?.hooks)) return false;
|
|
2805
|
+
return group.hooks.some(
|
|
2806
|
+
(h) => typeof h?.command === "string" && h.command.includes(HOOK_MARKER)
|
|
2807
|
+
);
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
function isInstalled() {
|
|
2811
|
+
const settings = readSettings();
|
|
2812
|
+
const hooks = settings.hooks;
|
|
2813
|
+
if (!hooks) return false;
|
|
2814
|
+
return hasClovizHook(hooks.PostToolUse) && hasClovizHook(hooks.Stop);
|
|
2815
|
+
}
|
|
2816
|
+
function createHooksApi(ctx2) {
|
|
2817
|
+
const app13 = new Hono13();
|
|
2818
|
+
app13.get("/status", (c) => {
|
|
2819
|
+
return c.json({ installed: isInstalled() });
|
|
2820
|
+
});
|
|
2821
|
+
app13.post("/install", (c) => {
|
|
2822
|
+
const settings = readSettings();
|
|
2823
|
+
if (!settings.hooks) settings.hooks = {};
|
|
2824
|
+
const hooks = settings.hooks;
|
|
2825
|
+
for (const event of ["PostToolUse", "Stop"]) {
|
|
2826
|
+
if (!Array.isArray(hooks[event])) {
|
|
2827
|
+
hooks[event] = [];
|
|
2828
|
+
}
|
|
2829
|
+
if (!hasClovizHook(hooks[event])) {
|
|
2830
|
+
hooks[event].push(makeHookEntry());
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
writeSettings(settings);
|
|
2834
|
+
const installed = isInstalled();
|
|
2835
|
+
broadcast("hooks:status", { installed });
|
|
2836
|
+
return c.json({ installed });
|
|
2837
|
+
});
|
|
2838
|
+
app13.post("/uninstall", (c) => {
|
|
2839
|
+
const settings = readSettings();
|
|
2840
|
+
const hooks = settings.hooks;
|
|
2841
|
+
if (hooks) {
|
|
2842
|
+
for (const event of ["PostToolUse", "Stop"]) {
|
|
2843
|
+
if (!Array.isArray(hooks[event])) continue;
|
|
2844
|
+
hooks[event] = hooks[event].filter((group) => {
|
|
2845
|
+
if (!Array.isArray(group?.hooks)) return true;
|
|
2846
|
+
const filtered = group.hooks.filter(
|
|
2847
|
+
(h) => !(typeof h?.command === "string" && h.command.includes(HOOK_MARKER))
|
|
2848
|
+
);
|
|
2849
|
+
if (filtered.length === 0) return false;
|
|
2850
|
+
group.hooks = filtered;
|
|
2851
|
+
return true;
|
|
2852
|
+
});
|
|
2853
|
+
if (hooks[event].length === 0) delete hooks[event];
|
|
2854
|
+
}
|
|
2855
|
+
if (Object.keys(hooks).length === 0) delete settings.hooks;
|
|
2856
|
+
writeSettings(settings);
|
|
2857
|
+
}
|
|
2858
|
+
const installed = isInstalled();
|
|
2859
|
+
broadcast("hooks:status", { installed });
|
|
2860
|
+
return c.json({ installed });
|
|
2861
|
+
});
|
|
2862
|
+
app13.post("/notify", async (c) => {
|
|
2863
|
+
try {
|
|
2864
|
+
const body = await c.req.json().catch(() => ({}));
|
|
2865
|
+
const { transcript_path, session_id, hook_event_name } = body;
|
|
2866
|
+
if (transcript_path) {
|
|
2867
|
+
const event = handleFileChange(ctx2, transcript_path);
|
|
2868
|
+
if (event) {
|
|
2869
|
+
broadcast(event, { path: transcript_path, source: "hook" });
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
if (hook_event_name === "Stop") {
|
|
2873
|
+
const historyPath = join10(ctx2.claudeDir, "history.jsonl");
|
|
2874
|
+
if (existsSync6(historyPath)) {
|
|
2875
|
+
const histEvent = handleFileChange(ctx2, historyPath);
|
|
2876
|
+
if (histEvent) broadcast(histEvent, { source: "hook" });
|
|
2877
|
+
}
|
|
2878
|
+
const statsPath = join10(ctx2.claudeDir, "stats-cache.json");
|
|
2879
|
+
if (existsSync6(statsPath)) {
|
|
2880
|
+
const statsEvent = handleFileChange(ctx2, statsPath);
|
|
2881
|
+
if (statsEvent) broadcast(statsEvent, { source: "hook" });
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
} catch (e) {
|
|
2885
|
+
console.error("[hooks] Error processing notify:", e);
|
|
2886
|
+
}
|
|
2887
|
+
return c.json({ ok: true });
|
|
2888
|
+
});
|
|
2889
|
+
return app13;
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
// src/server/index.ts
|
|
2893
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
2894
|
+
var CLAUDE_DIR3 = join11(homedir5(), ".claude");
|
|
2895
|
+
var PORT2 = parseInt(process.env.PORT || "3456");
|
|
2896
|
+
var IS_PROD = process.env.NODE_ENV === "production";
|
|
2897
|
+
var db2 = getDb();
|
|
2898
|
+
var ctx = { db: db2, claudeDir: CLAUDE_DIR3 };
|
|
2899
|
+
runQuickIndex(ctx);
|
|
2900
|
+
var app12 = new Hono14();
|
|
2901
|
+
app12.use("/api/*", cors());
|
|
2902
|
+
app12.route("/api/dashboard", dashboard_default);
|
|
2903
|
+
app12.route("/api/sessions", sessions_default);
|
|
2904
|
+
app12.route("/api/projects", projects_default);
|
|
2905
|
+
app12.route("/api/analytics/costs", costs_default);
|
|
2906
|
+
app12.route("/api/analytics", analytics_default);
|
|
2907
|
+
app12.route("/api/search", search_default);
|
|
2908
|
+
app12.route("/api/plans", plans_default);
|
|
2909
|
+
app12.route("/api/todos", todos_default);
|
|
2910
|
+
app12.route("/api/file-history", file_history_default);
|
|
2911
|
+
app12.route("/api/debug", debug_default);
|
|
2912
|
+
app12.route("/api/commits", commits_default);
|
|
2913
|
+
app12.route("/api/watcher", createWatcherApi(ctx));
|
|
2914
|
+
app12.route("/api/hooks", createHooksApi(ctx));
|
|
2915
|
+
if (IS_PROD) {
|
|
2916
|
+
const clientDir = join11(__dirname, "../../dist/client");
|
|
2917
|
+
if (existsSync7(clientDir)) {
|
|
2918
|
+
app12.get("/*", async (c, next) => {
|
|
2919
|
+
if (c.req.path.startsWith("/api/") || c.req.path === "/ws") {
|
|
2920
|
+
return next();
|
|
2921
|
+
}
|
|
2922
|
+
const filePath = join11(clientDir, c.req.path === "/" ? "index.html" : c.req.path);
|
|
2923
|
+
if (existsSync7(filePath)) {
|
|
2924
|
+
return fileResponse(filePath);
|
|
2925
|
+
}
|
|
2926
|
+
return fileResponse(join11(clientDir, "index.html"));
|
|
2927
|
+
});
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
startWatcher(ctx);
|
|
2931
|
+
function getDashboardSummary() {
|
|
2932
|
+
try {
|
|
2933
|
+
const stats = {};
|
|
2934
|
+
const rows = db2.prepare("SELECT key, value FROM stats_cache").all();
|
|
2935
|
+
for (const row of rows) {
|
|
2936
|
+
if (row.key !== "raw") stats[row.key] = row.value;
|
|
2937
|
+
}
|
|
2938
|
+
return {
|
|
2939
|
+
totalSessions: parseInt(stats.totalSessions || "0"),
|
|
2940
|
+
totalMessages: parseInt(stats.totalMessages || "0")
|
|
2941
|
+
};
|
|
2942
|
+
} catch {
|
|
2943
|
+
return {};
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
if (IS_BUN) {
|
|
2947
|
+
const server = Bun.serve({
|
|
2948
|
+
port: PORT2,
|
|
2949
|
+
fetch(req, server2) {
|
|
2950
|
+
const url = new URL(req.url);
|
|
2951
|
+
if (url.pathname === "/ws") {
|
|
2952
|
+
const upgraded = server2.upgrade(req);
|
|
2953
|
+
if (upgraded) return void 0;
|
|
2954
|
+
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
2955
|
+
}
|
|
2956
|
+
return app12.fetch(req, server2);
|
|
2957
|
+
},
|
|
2958
|
+
websocket: {
|
|
2959
|
+
open(ws) {
|
|
2960
|
+
addClient(ws);
|
|
2961
|
+
const dashboardData = getDashboardSummary();
|
|
2962
|
+
ws.send(
|
|
2963
|
+
JSON.stringify({
|
|
2964
|
+
event: "initial:sync",
|
|
2965
|
+
data: dashboardData,
|
|
2966
|
+
timestamp: Date.now()
|
|
2967
|
+
})
|
|
2968
|
+
);
|
|
2969
|
+
},
|
|
2970
|
+
close(ws) {
|
|
2971
|
+
removeClient(ws);
|
|
2972
|
+
},
|
|
2973
|
+
message(_ws, _msg) {
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
});
|
|
2977
|
+
} else {
|
|
2978
|
+
const { serve } = await import("@hono/node-server");
|
|
2979
|
+
const { createNodeWebSocket } = await import("@hono/node-ws");
|
|
2980
|
+
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app: app12 });
|
|
2981
|
+
const wsClientMap = /* @__PURE__ */ new WeakMap();
|
|
2982
|
+
app12.get("/ws", upgradeWebSocket(() => ({
|
|
2983
|
+
onOpen(_event, ws) {
|
|
2984
|
+
const client = { send: (data) => ws.send(data) };
|
|
2985
|
+
wsClientMap.set(ws, client);
|
|
2986
|
+
addClient(client);
|
|
2987
|
+
const dashboardData = getDashboardSummary();
|
|
2988
|
+
ws.send(
|
|
2989
|
+
JSON.stringify({
|
|
2990
|
+
event: "initial:sync",
|
|
2991
|
+
data: dashboardData,
|
|
2992
|
+
timestamp: Date.now()
|
|
2993
|
+
})
|
|
2994
|
+
);
|
|
2995
|
+
},
|
|
2996
|
+
onClose(_event, ws) {
|
|
2997
|
+
const client = wsClientMap.get(ws);
|
|
2998
|
+
if (client) {
|
|
2999
|
+
removeClient(client);
|
|
3000
|
+
wsClientMap.delete(ws);
|
|
3001
|
+
}
|
|
3002
|
+
},
|
|
3003
|
+
onMessage(_event, _ws) {
|
|
3004
|
+
}
|
|
3005
|
+
})));
|
|
3006
|
+
const server = serve({ fetch: app12.fetch, port: PORT2 }, () => {
|
|
3007
|
+
});
|
|
3008
|
+
injectWebSocket(server);
|
|
3009
|
+
}
|
|
3010
|
+
console.log(`
|
|
3011
|
+
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
3012
|
+
\u2551 Cloviz Dashboard \u2551
|
|
3013
|
+
\u2551 http://localhost:${PORT2} \u2551
|
|
3014
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
3015
|
+
`);
|
|
3016
|
+
setTimeout(() => runBackgroundIndex(ctx), 100);
|
|
3017
|
+
process.on("SIGINT", () => {
|
|
3018
|
+
console.log("\nShutting down...");
|
|
3019
|
+
closeDb();
|
|
3020
|
+
process.exit(0);
|
|
3021
|
+
});
|
|
3022
|
+
var index_default = app12;
|
|
3023
|
+
export {
|
|
3024
|
+
index_default as default
|
|
3025
|
+
};
|