ccrecall 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +265 -0
- package/package.json +43 -0
- package/src/cli.ts +113 -0
- package/src/db.ts +450 -0
- package/src/index.ts +6 -0
- package/src/parser.ts +208 -0
- package/src/sync-teams.ts +172 -0
- package/src/sync.ts +191 -0
package/src/db.ts
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { Database as BunDB, Statement } from 'bun:sqlite';
|
|
2
|
+
import { existsSync, renameSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_DB_PATH = join(Bun.env.HOME!, '.claude', 'ccrecall.db');
|
|
6
|
+
const LEGACY_DB_PATH = join(Bun.env.HOME!, '.claude', 'cclog.db');
|
|
7
|
+
|
|
8
|
+
function migrate_legacy_db(target_path: string) {
|
|
9
|
+
if (target_path !== DEFAULT_DB_PATH) return;
|
|
10
|
+
if (existsSync(target_path)) return;
|
|
11
|
+
if (!existsSync(LEGACY_DB_PATH)) return;
|
|
12
|
+
|
|
13
|
+
renameSync(LEGACY_DB_PATH, target_path);
|
|
14
|
+
console.log('Migrated database: cclog.db → ccrecall.db');
|
|
15
|
+
}
|
|
16
|
+
const SCHEMA = `
|
|
17
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
project_path TEXT NOT NULL,
|
|
20
|
+
git_branch TEXT,
|
|
21
|
+
cwd TEXT,
|
|
22
|
+
first_timestamp INTEGER,
|
|
23
|
+
last_timestamp INTEGER,
|
|
24
|
+
summary TEXT
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
28
|
+
uuid TEXT PRIMARY KEY,
|
|
29
|
+
session_id TEXT NOT NULL,
|
|
30
|
+
parent_uuid TEXT,
|
|
31
|
+
type TEXT NOT NULL,
|
|
32
|
+
model TEXT,
|
|
33
|
+
content_text TEXT,
|
|
34
|
+
content_json TEXT,
|
|
35
|
+
thinking TEXT,
|
|
36
|
+
timestamp INTEGER NOT NULL,
|
|
37
|
+
input_tokens INTEGER DEFAULT 0,
|
|
38
|
+
output_tokens INTEGER DEFAULT 0,
|
|
39
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
40
|
+
cache_creation_tokens INTEGER DEFAULT 0,
|
|
41
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE TABLE IF NOT EXISTS sync_state (
|
|
45
|
+
file_path TEXT PRIMARY KEY,
|
|
46
|
+
last_modified INTEGER NOT NULL,
|
|
47
|
+
last_byte_offset INTEGER NOT NULL
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
CREATE TABLE IF NOT EXISTS tool_calls (
|
|
51
|
+
id TEXT PRIMARY KEY,
|
|
52
|
+
message_uuid TEXT NOT NULL,
|
|
53
|
+
session_id TEXT NOT NULL,
|
|
54
|
+
tool_name TEXT NOT NULL,
|
|
55
|
+
tool_input TEXT,
|
|
56
|
+
timestamp INTEGER NOT NULL,
|
|
57
|
+
FOREIGN KEY (message_uuid) REFERENCES messages(uuid),
|
|
58
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
CREATE TABLE IF NOT EXISTS tool_results (
|
|
62
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
63
|
+
tool_call_id TEXT NOT NULL,
|
|
64
|
+
message_uuid TEXT NOT NULL,
|
|
65
|
+
session_id TEXT NOT NULL,
|
|
66
|
+
content TEXT,
|
|
67
|
+
is_error INTEGER DEFAULT 0,
|
|
68
|
+
timestamp INTEGER NOT NULL,
|
|
69
|
+
FOREIGN KEY (tool_call_id) REFERENCES tool_calls(id),
|
|
70
|
+
FOREIGN KEY (message_uuid) REFERENCES messages(uuid),
|
|
71
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id);
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_name ON tool_calls(tool_name);
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_tool_results_session ON tool_results(session_id);
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_tool_results_call ON tool_results(tool_call_id);
|
|
81
|
+
|
|
82
|
+
-- Team/Swarm tracking tables
|
|
83
|
+
CREATE TABLE IF NOT EXISTS teams (
|
|
84
|
+
id TEXT PRIMARY KEY,
|
|
85
|
+
name TEXT NOT NULL,
|
|
86
|
+
description TEXT,
|
|
87
|
+
lead_session_id TEXT,
|
|
88
|
+
created_at INTEGER NOT NULL,
|
|
89
|
+
FOREIGN KEY (lead_session_id) REFERENCES sessions(id)
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
CREATE TABLE IF NOT EXISTS team_members (
|
|
93
|
+
id TEXT PRIMARY KEY,
|
|
94
|
+
team_id TEXT NOT NULL,
|
|
95
|
+
name TEXT NOT NULL,
|
|
96
|
+
agent_type TEXT,
|
|
97
|
+
model TEXT,
|
|
98
|
+
prompt TEXT,
|
|
99
|
+
color TEXT,
|
|
100
|
+
cwd TEXT,
|
|
101
|
+
joined_at INTEGER NOT NULL,
|
|
102
|
+
FOREIGN KEY (team_id) REFERENCES teams(id)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
CREATE TABLE IF NOT EXISTS team_tasks (
|
|
106
|
+
id TEXT PRIMARY KEY,
|
|
107
|
+
team_id TEXT NOT NULL,
|
|
108
|
+
owner_name TEXT,
|
|
109
|
+
subject TEXT NOT NULL,
|
|
110
|
+
description TEXT,
|
|
111
|
+
status TEXT DEFAULT 'pending',
|
|
112
|
+
created_at INTEGER,
|
|
113
|
+
completed_at INTEGER,
|
|
114
|
+
FOREIGN KEY (team_id) REFERENCES teams(id)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_teams_name ON teams(name);
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_teams_lead_session ON teams(lead_session_id);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_team_members_team ON team_members(team_id);
|
|
120
|
+
CREATE INDEX IF NOT EXISTS idx_team_tasks_team ON team_tasks(team_id);
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_team_tasks_status ON team_tasks(status);
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
export class Database {
|
|
125
|
+
private db: BunDB;
|
|
126
|
+
private stmt_upsert_session: Statement;
|
|
127
|
+
private stmt_insert_message: Statement;
|
|
128
|
+
private stmt_insert_tool_call: Statement;
|
|
129
|
+
private stmt_insert_tool_result: Statement;
|
|
130
|
+
private stmt_get_sync_state: Statement;
|
|
131
|
+
private stmt_set_sync_state: Statement;
|
|
132
|
+
private stmt_upsert_team: Statement;
|
|
133
|
+
private stmt_upsert_team_member: Statement;
|
|
134
|
+
private stmt_upsert_team_task: Statement;
|
|
135
|
+
|
|
136
|
+
constructor(db_path = DEFAULT_DB_PATH) {
|
|
137
|
+
migrate_legacy_db(db_path);
|
|
138
|
+
this.db = new BunDB(db_path);
|
|
139
|
+
this.db.run('PRAGMA foreign_keys = ON');
|
|
140
|
+
this.db.run(SCHEMA);
|
|
141
|
+
|
|
142
|
+
this.stmt_upsert_session = this.db.prepare(`
|
|
143
|
+
INSERT INTO sessions (id, project_path, git_branch, cwd, first_timestamp, last_timestamp, summary)
|
|
144
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
145
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
146
|
+
last_timestamp = MAX(last_timestamp, excluded.last_timestamp),
|
|
147
|
+
summary = COALESCE(excluded.summary, summary)
|
|
148
|
+
`);
|
|
149
|
+
|
|
150
|
+
this.stmt_insert_message = this.db.prepare(`
|
|
151
|
+
INSERT OR IGNORE INTO messages (
|
|
152
|
+
uuid, session_id, parent_uuid, type, model,
|
|
153
|
+
content_text, content_json, thinking, timestamp,
|
|
154
|
+
input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens
|
|
155
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
156
|
+
`);
|
|
157
|
+
|
|
158
|
+
this.stmt_insert_tool_call = this.db.prepare(`
|
|
159
|
+
INSERT OR IGNORE INTO tool_calls (id, message_uuid, session_id, tool_name, tool_input, timestamp)
|
|
160
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
161
|
+
`);
|
|
162
|
+
|
|
163
|
+
this.stmt_insert_tool_result = this.db.prepare(`
|
|
164
|
+
INSERT OR IGNORE INTO tool_results (tool_call_id, message_uuid, session_id, content, is_error, timestamp)
|
|
165
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
166
|
+
`);
|
|
167
|
+
|
|
168
|
+
this.stmt_get_sync_state = this.db.prepare(
|
|
169
|
+
'SELECT last_modified, last_byte_offset FROM sync_state WHERE file_path = ?',
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
this.stmt_set_sync_state = this.db.prepare(`
|
|
173
|
+
INSERT INTO sync_state (file_path, last_modified, last_byte_offset)
|
|
174
|
+
VALUES (?, ?, ?)
|
|
175
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
176
|
+
last_modified = excluded.last_modified,
|
|
177
|
+
last_byte_offset = excluded.last_byte_offset
|
|
178
|
+
`);
|
|
179
|
+
|
|
180
|
+
this.stmt_upsert_team = this.db.prepare(`
|
|
181
|
+
INSERT INTO teams (id, name, description, lead_session_id, created_at)
|
|
182
|
+
VALUES (?, ?, ?, ?, ?)
|
|
183
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
184
|
+
description = COALESCE(excluded.description, description),
|
|
185
|
+
lead_session_id = COALESCE(excluded.lead_session_id, lead_session_id)
|
|
186
|
+
`);
|
|
187
|
+
|
|
188
|
+
this.stmt_upsert_team_member = this.db.prepare(`
|
|
189
|
+
INSERT INTO team_members (id, team_id, name, agent_type, model, prompt, color, cwd, joined_at)
|
|
190
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
191
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
192
|
+
prompt = COALESCE(excluded.prompt, prompt),
|
|
193
|
+
model = COALESCE(excluded.model, model)
|
|
194
|
+
`);
|
|
195
|
+
|
|
196
|
+
this.stmt_upsert_team_task = this.db.prepare(`
|
|
197
|
+
INSERT INTO team_tasks (id, team_id, owner_name, subject, description, status, created_at, completed_at)
|
|
198
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
199
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
200
|
+
status = excluded.status,
|
|
201
|
+
owner_name = COALESCE(excluded.owner_name, owner_name),
|
|
202
|
+
completed_at = COALESCE(excluded.completed_at, completed_at)
|
|
203
|
+
`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
begin() {
|
|
207
|
+
this.db.run('BEGIN TRANSACTION');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
commit() {
|
|
211
|
+
this.db.run('COMMIT');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
disable_foreign_keys() {
|
|
215
|
+
this.db.run('PRAGMA foreign_keys = OFF');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
enable_foreign_keys() {
|
|
219
|
+
this.db.run('PRAGMA foreign_keys = ON');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
upsert_session(session: {
|
|
223
|
+
id: string;
|
|
224
|
+
project_path: string;
|
|
225
|
+
git_branch?: string;
|
|
226
|
+
cwd?: string;
|
|
227
|
+
timestamp: number;
|
|
228
|
+
summary?: string;
|
|
229
|
+
}) {
|
|
230
|
+
this.stmt_upsert_session.run(
|
|
231
|
+
session.id,
|
|
232
|
+
session.project_path,
|
|
233
|
+
session.git_branch ?? null,
|
|
234
|
+
session.cwd ?? null,
|
|
235
|
+
session.timestamp,
|
|
236
|
+
session.timestamp,
|
|
237
|
+
session.summary ?? null,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
insert_message(msg: {
|
|
242
|
+
uuid: string;
|
|
243
|
+
session_id: string;
|
|
244
|
+
parent_uuid?: string;
|
|
245
|
+
type: string;
|
|
246
|
+
model?: string;
|
|
247
|
+
content_text?: string;
|
|
248
|
+
content_json?: string;
|
|
249
|
+
thinking?: string;
|
|
250
|
+
timestamp: number;
|
|
251
|
+
input_tokens?: number;
|
|
252
|
+
output_tokens?: number;
|
|
253
|
+
cache_read_tokens?: number;
|
|
254
|
+
cache_creation_tokens?: number;
|
|
255
|
+
}) {
|
|
256
|
+
this.stmt_insert_message.run(
|
|
257
|
+
msg.uuid,
|
|
258
|
+
msg.session_id,
|
|
259
|
+
msg.parent_uuid ?? null,
|
|
260
|
+
msg.type,
|
|
261
|
+
msg.model ?? null,
|
|
262
|
+
msg.content_text ?? null,
|
|
263
|
+
msg.content_json ?? null,
|
|
264
|
+
msg.thinking ?? null,
|
|
265
|
+
msg.timestamp,
|
|
266
|
+
msg.input_tokens ?? 0,
|
|
267
|
+
msg.output_tokens ?? 0,
|
|
268
|
+
msg.cache_read_tokens ?? 0,
|
|
269
|
+
msg.cache_creation_tokens ?? 0,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
insert_tool_call(call: {
|
|
274
|
+
id: string;
|
|
275
|
+
message_uuid: string;
|
|
276
|
+
session_id: string;
|
|
277
|
+
tool_name: string;
|
|
278
|
+
tool_input: string;
|
|
279
|
+
timestamp: number;
|
|
280
|
+
}) {
|
|
281
|
+
this.stmt_insert_tool_call.run(
|
|
282
|
+
call.id,
|
|
283
|
+
call.message_uuid,
|
|
284
|
+
call.session_id,
|
|
285
|
+
call.tool_name,
|
|
286
|
+
call.tool_input,
|
|
287
|
+
call.timestamp,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
insert_tool_result(result: {
|
|
292
|
+
tool_call_id: string;
|
|
293
|
+
message_uuid: string;
|
|
294
|
+
session_id: string;
|
|
295
|
+
content: string;
|
|
296
|
+
is_error: boolean;
|
|
297
|
+
timestamp: number;
|
|
298
|
+
}) {
|
|
299
|
+
this.stmt_insert_tool_result.run(
|
|
300
|
+
result.tool_call_id,
|
|
301
|
+
result.message_uuid,
|
|
302
|
+
result.session_id,
|
|
303
|
+
result.content,
|
|
304
|
+
result.is_error ? 1 : 0,
|
|
305
|
+
result.timestamp,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
get_sync_state(
|
|
310
|
+
file_path: string,
|
|
311
|
+
): { last_modified: number; last_byte_offset: number } | undefined {
|
|
312
|
+
return this.stmt_get_sync_state.get(file_path) as
|
|
313
|
+
| { last_modified: number; last_byte_offset: number }
|
|
314
|
+
| undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
set_sync_state(
|
|
318
|
+
file_path: string,
|
|
319
|
+
last_modified: number,
|
|
320
|
+
last_byte_offset: number,
|
|
321
|
+
) {
|
|
322
|
+
this.stmt_set_sync_state.run(
|
|
323
|
+
file_path,
|
|
324
|
+
last_modified,
|
|
325
|
+
last_byte_offset,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
upsert_team(team: {
|
|
330
|
+
id: string;
|
|
331
|
+
name: string;
|
|
332
|
+
description?: string;
|
|
333
|
+
lead_session_id?: string;
|
|
334
|
+
created_at: number;
|
|
335
|
+
}) {
|
|
336
|
+
this.stmt_upsert_team.run(
|
|
337
|
+
team.id,
|
|
338
|
+
team.name,
|
|
339
|
+
team.description ?? null,
|
|
340
|
+
team.lead_session_id ?? null,
|
|
341
|
+
team.created_at,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
upsert_team_member(member: {
|
|
346
|
+
id: string;
|
|
347
|
+
team_id: string;
|
|
348
|
+
name: string;
|
|
349
|
+
agent_type?: string;
|
|
350
|
+
model?: string;
|
|
351
|
+
prompt?: string;
|
|
352
|
+
color?: string;
|
|
353
|
+
cwd?: string;
|
|
354
|
+
joined_at: number;
|
|
355
|
+
}) {
|
|
356
|
+
this.stmt_upsert_team_member.run(
|
|
357
|
+
member.id,
|
|
358
|
+
member.team_id,
|
|
359
|
+
member.name,
|
|
360
|
+
member.agent_type ?? null,
|
|
361
|
+
member.model ?? null,
|
|
362
|
+
member.prompt ?? null,
|
|
363
|
+
member.color ?? null,
|
|
364
|
+
member.cwd ?? null,
|
|
365
|
+
member.joined_at,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
upsert_team_task(task: {
|
|
370
|
+
id: string;
|
|
371
|
+
team_id: string;
|
|
372
|
+
owner_name?: string;
|
|
373
|
+
subject: string;
|
|
374
|
+
description?: string;
|
|
375
|
+
status?: string;
|
|
376
|
+
created_at?: number;
|
|
377
|
+
completed_at?: number;
|
|
378
|
+
}) {
|
|
379
|
+
this.stmt_upsert_team_task.run(
|
|
380
|
+
task.id,
|
|
381
|
+
task.team_id,
|
|
382
|
+
task.owner_name ?? null,
|
|
383
|
+
task.subject,
|
|
384
|
+
task.description ?? null,
|
|
385
|
+
task.status ?? 'pending',
|
|
386
|
+
task.created_at ?? null,
|
|
387
|
+
task.completed_at ?? null,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
get_stats() {
|
|
392
|
+
const sessions = this.db
|
|
393
|
+
.prepare('SELECT COUNT(*) as count FROM sessions')
|
|
394
|
+
.get() as { count: number };
|
|
395
|
+
const messages = this.db
|
|
396
|
+
.prepare('SELECT COUNT(*) as count FROM messages')
|
|
397
|
+
.get() as { count: number };
|
|
398
|
+
const tool_calls = this.db
|
|
399
|
+
.prepare('SELECT COUNT(*) as count FROM tool_calls')
|
|
400
|
+
.get() as { count: number };
|
|
401
|
+
const tool_results = this.db
|
|
402
|
+
.prepare('SELECT COUNT(*) as count FROM tool_results')
|
|
403
|
+
.get() as { count: number };
|
|
404
|
+
const teams = this.db
|
|
405
|
+
.prepare('SELECT COUNT(*) as count FROM teams')
|
|
406
|
+
.get() as { count: number };
|
|
407
|
+
const team_members = this.db
|
|
408
|
+
.prepare('SELECT COUNT(*) as count FROM team_members')
|
|
409
|
+
.get() as { count: number };
|
|
410
|
+
const team_tasks = this.db
|
|
411
|
+
.prepare('SELECT COUNT(*) as count FROM team_tasks')
|
|
412
|
+
.get() as { count: number };
|
|
413
|
+
const tokens = this.db
|
|
414
|
+
.prepare(
|
|
415
|
+
`
|
|
416
|
+
SELECT
|
|
417
|
+
SUM(input_tokens) as input,
|
|
418
|
+
SUM(output_tokens) as output,
|
|
419
|
+
SUM(cache_read_tokens) as cache_read,
|
|
420
|
+
SUM(cache_creation_tokens) as cache_creation
|
|
421
|
+
FROM messages
|
|
422
|
+
`,
|
|
423
|
+
)
|
|
424
|
+
.get() as {
|
|
425
|
+
input: number;
|
|
426
|
+
output: number;
|
|
427
|
+
cache_read: number;
|
|
428
|
+
cache_creation: number;
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
sessions: sessions.count,
|
|
433
|
+
messages: messages.count,
|
|
434
|
+
tool_calls: tool_calls.count,
|
|
435
|
+
tool_results: tool_results.count,
|
|
436
|
+
teams: teams.count,
|
|
437
|
+
team_members: team_members.count,
|
|
438
|
+
team_tasks: team_tasks.count,
|
|
439
|
+
tokens,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
reset_sync_state() {
|
|
444
|
+
this.db.run('DELETE FROM sync_state');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
close() {
|
|
448
|
+
this.db.close();
|
|
449
|
+
}
|
|
450
|
+
}
|
package/src/index.ts
ADDED
package/src/parser.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
export interface TranscriptMessage {
|
|
2
|
+
uuid: string;
|
|
3
|
+
parentUuid?: string;
|
|
4
|
+
sessionId: string;
|
|
5
|
+
type: 'user' | 'assistant' | 'summary';
|
|
6
|
+
timestamp: string;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
gitBranch?: string;
|
|
9
|
+
message?: {
|
|
10
|
+
role?: string;
|
|
11
|
+
content?: string | ContentBlock[];
|
|
12
|
+
model?: string;
|
|
13
|
+
usage?: {
|
|
14
|
+
input_tokens?: number;
|
|
15
|
+
output_tokens?: number;
|
|
16
|
+
cache_read_input_tokens?: number;
|
|
17
|
+
cache_creation_input_tokens?: number;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
summary?: string;
|
|
21
|
+
leafUuid?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ContentBlock {
|
|
25
|
+
type: string;
|
|
26
|
+
text?: string;
|
|
27
|
+
thinking?: string;
|
|
28
|
+
// tool_use fields
|
|
29
|
+
id?: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
input?: unknown;
|
|
32
|
+
// tool_result fields
|
|
33
|
+
tool_use_id?: string;
|
|
34
|
+
content?: string | ContentBlock[];
|
|
35
|
+
is_error?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ToolCall {
|
|
39
|
+
id: string;
|
|
40
|
+
tool_name: string;
|
|
41
|
+
tool_input: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ToolResult {
|
|
45
|
+
tool_call_id: string;
|
|
46
|
+
content: string;
|
|
47
|
+
is_error: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ParsedMessage {
|
|
51
|
+
uuid: string;
|
|
52
|
+
session_id: string;
|
|
53
|
+
parent_uuid?: string;
|
|
54
|
+
type: string;
|
|
55
|
+
model?: string;
|
|
56
|
+
content_text?: string;
|
|
57
|
+
content_json?: string;
|
|
58
|
+
thinking?: string;
|
|
59
|
+
timestamp: number;
|
|
60
|
+
input_tokens: number;
|
|
61
|
+
output_tokens: number;
|
|
62
|
+
cache_read_tokens: number;
|
|
63
|
+
cache_creation_tokens: number;
|
|
64
|
+
cwd?: string;
|
|
65
|
+
git_branch?: string;
|
|
66
|
+
summary?: string;
|
|
67
|
+
tool_calls: ToolCall[];
|
|
68
|
+
tool_results: ToolResult[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function extract_text(
|
|
72
|
+
content: string | ContentBlock[] | undefined,
|
|
73
|
+
): string | undefined {
|
|
74
|
+
if (!content) return undefined;
|
|
75
|
+
if (typeof content === 'string') return content;
|
|
76
|
+
|
|
77
|
+
const text_parts = content
|
|
78
|
+
.filter(
|
|
79
|
+
(b): b is ContentBlock & { text: string } =>
|
|
80
|
+
b.type === 'text' && typeof b.text === 'string',
|
|
81
|
+
)
|
|
82
|
+
.map((b) => b.text);
|
|
83
|
+
|
|
84
|
+
return text_parts.length > 0 ? text_parts.join('\n') : undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function extract_thinking(
|
|
88
|
+
content: string | ContentBlock[] | undefined,
|
|
89
|
+
): string | undefined {
|
|
90
|
+
if (!content || typeof content === 'string') return undefined;
|
|
91
|
+
|
|
92
|
+
const thinking = content.find(
|
|
93
|
+
(b) => b.type === 'thinking' && b.thinking,
|
|
94
|
+
);
|
|
95
|
+
return thinking?.thinking;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function extract_tool_calls(
|
|
99
|
+
content: string | ContentBlock[] | undefined,
|
|
100
|
+
): ToolCall[] {
|
|
101
|
+
if (!content || typeof content === 'string') return [];
|
|
102
|
+
|
|
103
|
+
return content
|
|
104
|
+
.filter((b) => b.type === 'tool_use' && b.id && b.name)
|
|
105
|
+
.map((b) => ({
|
|
106
|
+
id: b.id!,
|
|
107
|
+
tool_name: b.name!,
|
|
108
|
+
tool_input: b.input ? JSON.stringify(b.input) : '{}',
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function extract_tool_results(
|
|
113
|
+
content: string | ContentBlock[] | undefined,
|
|
114
|
+
): ToolResult[] {
|
|
115
|
+
if (!content || typeof content === 'string') return [];
|
|
116
|
+
|
|
117
|
+
return content
|
|
118
|
+
.filter((b) => b.type === 'tool_result' && b.tool_use_id)
|
|
119
|
+
.map((b) => {
|
|
120
|
+
let result_content = '';
|
|
121
|
+
if (typeof b.content === 'string') {
|
|
122
|
+
result_content = b.content;
|
|
123
|
+
} else if (Array.isArray(b.content)) {
|
|
124
|
+
result_content = b.content
|
|
125
|
+
.filter((c) => c.type === 'text' && c.text)
|
|
126
|
+
.map((c) => c.text)
|
|
127
|
+
.join('\n');
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
tool_call_id: b.tool_use_id!,
|
|
131
|
+
content: result_content,
|
|
132
|
+
is_error: b.is_error ?? false,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function parse_message(line: string): ParsedMessage | null {
|
|
138
|
+
try {
|
|
139
|
+
const data = JSON.parse(line) as TranscriptMessage;
|
|
140
|
+
|
|
141
|
+
if (!data.uuid || !data.sessionId || !data.type) return null;
|
|
142
|
+
|
|
143
|
+
const timestamp = new Date(data.timestamp).getTime();
|
|
144
|
+
if (isNaN(timestamp)) return null;
|
|
145
|
+
|
|
146
|
+
const usage = data.message?.usage;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
uuid: data.uuid,
|
|
150
|
+
session_id: data.sessionId,
|
|
151
|
+
parent_uuid: data.parentUuid,
|
|
152
|
+
type: data.type,
|
|
153
|
+
model: data.message?.model,
|
|
154
|
+
content_text:
|
|
155
|
+
data.type === 'summary'
|
|
156
|
+
? data.summary
|
|
157
|
+
: extract_text(data.message?.content),
|
|
158
|
+
content_json: data.message?.content
|
|
159
|
+
? JSON.stringify(data.message.content)
|
|
160
|
+
: undefined,
|
|
161
|
+
thinking: extract_thinking(data.message?.content),
|
|
162
|
+
timestamp,
|
|
163
|
+
input_tokens: usage?.input_tokens ?? 0,
|
|
164
|
+
output_tokens: usage?.output_tokens ?? 0,
|
|
165
|
+
cache_read_tokens: usage?.cache_read_input_tokens ?? 0,
|
|
166
|
+
cache_creation_tokens: usage?.cache_creation_input_tokens ?? 0,
|
|
167
|
+
cwd: data.cwd,
|
|
168
|
+
git_branch: data.gitBranch,
|
|
169
|
+
summary: data.summary,
|
|
170
|
+
tool_calls: extract_tool_calls(data.message?.content),
|
|
171
|
+
tool_results: extract_tool_results(data.message?.content),
|
|
172
|
+
};
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function* parse_file(
|
|
179
|
+
file_path: string,
|
|
180
|
+
start_offset = 0,
|
|
181
|
+
): AsyncGenerator<{ message: ParsedMessage; byte_offset: number }> {
|
|
182
|
+
const file = Bun.file(file_path);
|
|
183
|
+
const text = await file.text();
|
|
184
|
+
|
|
185
|
+
// If starting from offset, slice the content
|
|
186
|
+
const content =
|
|
187
|
+
start_offset > 0
|
|
188
|
+
? new TextDecoder().decode(
|
|
189
|
+
new TextEncoder().encode(text).slice(start_offset),
|
|
190
|
+
)
|
|
191
|
+
: text;
|
|
192
|
+
|
|
193
|
+
const lines = content.split('\n');
|
|
194
|
+
let byte_offset = start_offset;
|
|
195
|
+
|
|
196
|
+
for (const line of lines) {
|
|
197
|
+
const line_bytes = new TextEncoder().encode(line).length + 1; // +1 for newline
|
|
198
|
+
|
|
199
|
+
if (line.trim()) {
|
|
200
|
+
const message = parse_message(line);
|
|
201
|
+
if (message) {
|
|
202
|
+
yield { message, byte_offset: byte_offset + line_bytes };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
byte_offset += line_bytes;
|
|
207
|
+
}
|
|
208
|
+
}
|