zozul-cli 0.3.7 → 0.3.8
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/.claude/settings.local.json +4 -1
- package/dist/dashboard/index.html +32 -8
- package/dist/hooks/server.js +6 -0
- package/dist/hooks/server.js.map +1 -1
- package/dist/parser/ingest.d.ts +2 -0
- package/dist/parser/ingest.d.ts.map +1 -1
- package/dist/parser/ingest.js +8 -3
- package/dist/parser/ingest.js.map +1 -1
- package/dist/parser/jsonl.d.ts +9 -3
- package/dist/parser/jsonl.d.ts.map +1 -1
- package/dist/parser/jsonl.js +35 -11
- package/dist/parser/jsonl.js.map +1 -1
- package/dist/parser/types.d.ts +2 -0
- package/dist/parser/types.d.ts.map +1 -1
- package/dist/parser/watcher.d.ts.map +1 -1
- package/dist/parser/watcher.js +37 -10
- package/dist/parser/watcher.js.map +1 -1
- package/dist/storage/db.d.ts +2 -0
- package/dist/storage/db.d.ts.map +1 -1
- package/dist/storage/db.js +12 -0
- package/dist/storage/db.js.map +1 -1
- package/dist/storage/repo.d.ts +1 -0
- package/dist/storage/repo.d.ts.map +1 -1
- package/dist/storage/repo.js +29 -18
- package/dist/storage/repo.js.map +1 -1
- package/dist/sync/sync.test.js +12 -0
- package/dist/sync/sync.test.js.map +1 -1
- package/dist/sync/transform.d.ts +2 -0
- package/dist/sync/transform.d.ts.map +1 -1
- package/dist/sync/transform.js +2 -0
- package/dist/sync/transform.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +32 -8
- package/src/hooks/server.ts +7 -0
- package/src/parser/ingest.ts +10 -5
- package/src/parser/jsonl.ts +43 -7
- package/src/parser/types.ts +2 -0
- package/src/parser/watcher.ts +34 -9
- package/src/storage/db.ts +12 -0
- package/src/storage/repo.ts +30 -18
- package/src/sync/sync.test.ts +12 -0
- package/src/sync/transform.ts +4 -0
package/src/parser/watcher.ts
CHANGED
|
@@ -30,9 +30,9 @@ export async function watchSessionFiles(opts: WatcherOptions): Promise<() => voi
|
|
|
30
30
|
if (catchUp) {
|
|
31
31
|
const files = discoverSessionFiles();
|
|
32
32
|
let caught = 0;
|
|
33
|
-
for (const { filePath, projectPath } of files) {
|
|
33
|
+
for (const { filePath, projectPath, parentSessionId, agentType } of files) {
|
|
34
34
|
try {
|
|
35
|
-
await ingestSessionFile(repo, filePath, projectPath);
|
|
35
|
+
await ingestSessionFile(repo, filePath, projectPath, { parentSessionId, agentType });
|
|
36
36
|
caught++;
|
|
37
37
|
} catch {
|
|
38
38
|
// Ignore parse errors on individual files
|
|
@@ -61,7 +61,11 @@ export async function watchSessionFiles(opts: WatcherOptions): Promise<() => voi
|
|
|
61
61
|
timers.delete(filePath);
|
|
62
62
|
try {
|
|
63
63
|
const projectPath = decodeProjectPath(filePath);
|
|
64
|
-
|
|
64
|
+
const subagentInfo = extractSubagentInfo(filePath);
|
|
65
|
+
await ingestSessionFile(repo, filePath, projectPath ?? undefined, {
|
|
66
|
+
parentSessionId: subagentInfo?.parentSessionId,
|
|
67
|
+
agentType: subagentInfo?.agentType,
|
|
68
|
+
});
|
|
65
69
|
if (verbose) {
|
|
66
70
|
process.stderr.write(`[watcher] ingested: ${filePath}\n`);
|
|
67
71
|
}
|
|
@@ -103,14 +107,35 @@ export async function watchSessionFiles(opts: WatcherOptions): Promise<() => voi
|
|
|
103
107
|
};
|
|
104
108
|
}
|
|
105
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Extract parent session ID and agent type from a subagent file path.
|
|
112
|
+
* Path format: .../<parent-uuid>/subagents/<agent-id>.jsonl
|
|
113
|
+
*/
|
|
114
|
+
function extractSubagentInfo(filePath: string): { parentSessionId: string; agentType: string | undefined } | null {
|
|
115
|
+
const match = filePath.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\/subagents\/([^/]+)\.jsonl$/i);
|
|
116
|
+
if (!match) return null;
|
|
117
|
+
const parentSessionId = match[1];
|
|
118
|
+
let agentType: string | undefined;
|
|
119
|
+
try {
|
|
120
|
+
const metaPath = filePath.replace(".jsonl", ".meta.json");
|
|
121
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
122
|
+
agentType = meta.agentType;
|
|
123
|
+
} catch { /* missing or invalid meta */ }
|
|
124
|
+
return { parentSessionId, agentType };
|
|
125
|
+
}
|
|
126
|
+
|
|
106
127
|
/**
|
|
107
128
|
* Extract the decoded project path from an absolute JSONL file path.
|
|
108
|
-
*
|
|
109
|
-
*
|
|
129
|
+
* Handles both main sessions and subagent files:
|
|
130
|
+
* ~/.claude/projects/<encoded>/<uuid>.jsonl
|
|
131
|
+
* ~/.claude/projects/<encoded>/<uuid>/subagents/<agent-id>.jsonl
|
|
110
132
|
*/
|
|
111
133
|
function decodeProjectPath(filePath: string): string | null {
|
|
112
|
-
//
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
115
|
-
|
|
134
|
+
// Main session: projects/<encoded>/<uuid>.jsonl
|
|
135
|
+
const parentMatch = filePath.match(/projects\/([^/]+)\/[0-9a-f-]{36}\.jsonl$/i);
|
|
136
|
+
if (parentMatch) return parentMatch[1].replace(/-/g, "/");
|
|
137
|
+
// Subagent: projects/<encoded>/<uuid>/subagents/<agent-id>.jsonl
|
|
138
|
+
const subMatch = filePath.match(/projects\/([^/]+)\/[0-9a-f-]{36}\/subagents\/[^/]+\.jsonl$/i);
|
|
139
|
+
if (subMatch) return subMatch[1].replace(/-/g, "/");
|
|
140
|
+
return null;
|
|
116
141
|
}
|
package/src/storage/db.ts
CHANGED
|
@@ -116,11 +116,23 @@ function migrate(db: Database.Database): void {
|
|
|
116
116
|
last_synced_at TEXT
|
|
117
117
|
);
|
|
118
118
|
`);
|
|
119
|
+
|
|
120
|
+
// Additive migrations (safe to re-run; errors mean column already exists)
|
|
121
|
+
const addColumns = [
|
|
122
|
+
`ALTER TABLE sessions ADD COLUMN parent_session_id TEXT`,
|
|
123
|
+
`ALTER TABLE sessions ADD COLUMN agent_type TEXT`,
|
|
124
|
+
];
|
|
125
|
+
for (const sql of addColumns) {
|
|
126
|
+
try { db.exec(sql); } catch { /* column already exists */ }
|
|
127
|
+
}
|
|
128
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id)`);
|
|
119
129
|
}
|
|
120
130
|
|
|
121
131
|
export type SessionRow = {
|
|
122
132
|
id: string;
|
|
123
133
|
project_path: string | null;
|
|
134
|
+
parent_session_id: string | null;
|
|
135
|
+
agent_type: string | null;
|
|
124
136
|
started_at: string;
|
|
125
137
|
ended_at: string | null;
|
|
126
138
|
total_input_tokens: number;
|
package/src/storage/repo.ts
CHANGED
|
@@ -22,15 +22,17 @@ export class SessionRepo {
|
|
|
22
22
|
|
|
23
23
|
upsertSession(session: Omit<SessionRow, "ended_at"> & { ended_at?: string | null }): void {
|
|
24
24
|
this.db.prepare(`
|
|
25
|
-
INSERT INTO sessions (id, project_path, started_at, ended_at,
|
|
26
|
-
total_output_tokens, total_cache_read_tokens, total_cache_creation_tokens,
|
|
25
|
+
INSERT INTO sessions (id, project_path, parent_session_id, agent_type, started_at, ended_at,
|
|
26
|
+
total_input_tokens, total_output_tokens, total_cache_read_tokens, total_cache_creation_tokens,
|
|
27
27
|
total_cost_usd, total_turns, total_duration_ms, model)
|
|
28
|
-
VALUES (@id, @project_path, @
|
|
29
|
-
@total_output_tokens, @total_cache_read_tokens, @total_cache_creation_tokens,
|
|
28
|
+
VALUES (@id, @project_path, @parent_session_id, @agent_type, @started_at, @ended_at,
|
|
29
|
+
@total_input_tokens, @total_output_tokens, @total_cache_read_tokens, @total_cache_creation_tokens,
|
|
30
30
|
@total_cost_usd, @total_turns, @total_duration_ms, @model)
|
|
31
31
|
ON CONFLICT(id) DO UPDATE SET
|
|
32
32
|
started_at = MIN(started_at, @started_at),
|
|
33
33
|
project_path = COALESCE(@project_path, project_path),
|
|
34
|
+
parent_session_id = COALESCE(@parent_session_id, parent_session_id),
|
|
35
|
+
agent_type = COALESCE(@agent_type, agent_type),
|
|
34
36
|
ended_at = COALESCE(@ended_at, ended_at),
|
|
35
37
|
total_turns = @total_turns,
|
|
36
38
|
model = COALESCE(@model, model),
|
|
@@ -45,6 +47,8 @@ export class SessionRepo {
|
|
|
45
47
|
`).run({
|
|
46
48
|
...session,
|
|
47
49
|
ended_at: session.ended_at ?? null,
|
|
50
|
+
parent_session_id: session.parent_session_id ?? null,
|
|
51
|
+
agent_type: session.agent_type ?? null,
|
|
48
52
|
});
|
|
49
53
|
}
|
|
50
54
|
|
|
@@ -179,20 +183,20 @@ export class SessionRepo {
|
|
|
179
183
|
listSessions(limit = 50, offset = 0, from?: string, to?: string): SessionRow[] {
|
|
180
184
|
if (from && to) {
|
|
181
185
|
return this.db.prepare(`
|
|
182
|
-
SELECT * FROM sessions WHERE started_at >= ? AND started_at <= ? ORDER BY started_at DESC LIMIT ? OFFSET ?
|
|
186
|
+
SELECT * FROM sessions WHERE parent_session_id IS NULL AND started_at >= ? AND started_at <= ? ORDER BY started_at DESC LIMIT ? OFFSET ?
|
|
183
187
|
`).all(from, to, limit, offset) as SessionRow[];
|
|
184
188
|
}
|
|
185
189
|
return this.db.prepare(`
|
|
186
|
-
SELECT * FROM sessions ORDER BY started_at DESC LIMIT ? OFFSET ?
|
|
190
|
+
SELECT * FROM sessions WHERE parent_session_id IS NULL ORDER BY started_at DESC LIMIT ? OFFSET ?
|
|
187
191
|
`).all(limit, offset) as SessionRow[];
|
|
188
192
|
}
|
|
189
193
|
|
|
190
194
|
countSessions(from?: string, to?: string): number {
|
|
191
195
|
if (from && to) {
|
|
192
|
-
const row = this.db.prepare(`SELECT COUNT(*) as n FROM sessions WHERE started_at >= ? AND started_at <= ?`).get(from, to) as { n: number };
|
|
196
|
+
const row = this.db.prepare(`SELECT COUNT(*) as n FROM sessions WHERE parent_session_id IS NULL AND started_at >= ? AND started_at <= ?`).get(from, to) as { n: number };
|
|
193
197
|
return row.n;
|
|
194
198
|
}
|
|
195
|
-
const row = this.db.prepare(`SELECT COUNT(*) as n FROM sessions`).get() as { n: number };
|
|
199
|
+
const row = this.db.prepare(`SELECT COUNT(*) as n FROM sessions WHERE parent_session_id IS NULL`).get() as { n: number };
|
|
196
200
|
return row.n;
|
|
197
201
|
}
|
|
198
202
|
|
|
@@ -200,6 +204,12 @@ export class SessionRepo {
|
|
|
200
204
|
return this.db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(id) as SessionRow | undefined;
|
|
201
205
|
}
|
|
202
206
|
|
|
207
|
+
getSubSessions(parentSessionId: string): SessionRow[] {
|
|
208
|
+
return this.db.prepare(`
|
|
209
|
+
SELECT * FROM sessions WHERE parent_session_id = ? ORDER BY started_at ASC
|
|
210
|
+
`).all(parentSessionId) as SessionRow[];
|
|
211
|
+
}
|
|
212
|
+
|
|
203
213
|
getSessionTurns(sessionId: string): TurnRow[] {
|
|
204
214
|
return this.db.prepare(`
|
|
205
215
|
SELECT * FROM turns WHERE session_id = ? ORDER BY turn_index ASC
|
|
@@ -221,7 +231,7 @@ export class SessionRepo {
|
|
|
221
231
|
getAggregateStats(from?: string, to?: string) {
|
|
222
232
|
if (from && to) {
|
|
223
233
|
return this.db.prepare(`
|
|
224
|
-
WITH fs AS (SELECT * FROM sessions WHERE started_at >= ? AND started_at <= ?),
|
|
234
|
+
WITH fs AS (SELECT * FROM sessions WHERE parent_session_id IS NULL AND started_at >= ? AND started_at <= ?),
|
|
225
235
|
fh AS (SELECT * FROM hook_events WHERE timestamp >= ? AND timestamp <= ?)
|
|
226
236
|
SELECT
|
|
227
237
|
(SELECT COUNT(*) FROM fs) as total_sessions,
|
|
@@ -237,13 +247,13 @@ export class SessionRepo {
|
|
|
237
247
|
}
|
|
238
248
|
return this.db.prepare(`
|
|
239
249
|
SELECT
|
|
240
|
-
(SELECT COUNT(*) FROM sessions) as total_sessions,
|
|
241
|
-
(SELECT SUM(total_input_tokens) FROM sessions) as total_input_tokens,
|
|
242
|
-
(SELECT SUM(total_output_tokens) FROM sessions) as total_output_tokens,
|
|
243
|
-
(SELECT SUM(total_cache_read_tokens) FROM sessions) as total_cache_read_tokens,
|
|
244
|
-
(SELECT SUM(total_cost_usd) FROM sessions) as total_cost_usd,
|
|
245
|
-
(SELECT SUM(total_turns) FROM sessions) as total_turns,
|
|
246
|
-
(SELECT SUM(total_duration_ms) FROM sessions) as total_duration_ms,
|
|
250
|
+
(SELECT COUNT(*) FROM sessions WHERE parent_session_id IS NULL) as total_sessions,
|
|
251
|
+
(SELECT SUM(total_input_tokens) FROM sessions WHERE parent_session_id IS NULL) as total_input_tokens,
|
|
252
|
+
(SELECT SUM(total_output_tokens) FROM sessions WHERE parent_session_id IS NULL) as total_output_tokens,
|
|
253
|
+
(SELECT SUM(total_cache_read_tokens) FROM sessions WHERE parent_session_id IS NULL) as total_cache_read_tokens,
|
|
254
|
+
(SELECT SUM(total_cost_usd) FROM sessions WHERE parent_session_id IS NULL) as total_cost_usd,
|
|
255
|
+
(SELECT SUM(total_turns) FROM sessions WHERE parent_session_id IS NULL) as total_turns,
|
|
256
|
+
(SELECT SUM(total_duration_ms) FROM sessions WHERE parent_session_id IS NULL) as total_duration_ms,
|
|
247
257
|
(SELECT COUNT(*) FROM hook_events WHERE event_name = 'UserPromptSubmit') as total_user_prompts,
|
|
248
258
|
(SELECT COUNT(*) FROM hook_events WHERE event_name = 'Stop') as total_interruptions
|
|
249
259
|
`).get();
|
|
@@ -478,6 +488,7 @@ export class SessionRepo {
|
|
|
478
488
|
|
|
479
489
|
// Only show real user turns (each row = one interaction block)
|
|
480
490
|
conditions.push("t.is_real_user = 1");
|
|
491
|
+
conditions.push("t.session_id IN (SELECT id FROM sessions WHERE parent_session_id IS NULL)");
|
|
481
492
|
|
|
482
493
|
const where = conditions.length > 0 ? "WHERE " + conditions.join(" AND ") : "";
|
|
483
494
|
params.push(limit, offset);
|
|
@@ -602,7 +613,7 @@ export class SessionRepo {
|
|
|
602
613
|
* Safe to call repeatedly — always produces correct values from append-only source data.
|
|
603
614
|
*/
|
|
604
615
|
getTaskGroups(from?: string, to?: string): { tags: string; turn_count: number; human_interventions: number; total_duration_ms: number; total_cost_usd: number; last_seen: string }[] {
|
|
605
|
-
const timeFilter = from && to ? "
|
|
616
|
+
const timeFilter = from && to ? " AND t.timestamp >= ? AND t.timestamp <= ?" : "";
|
|
606
617
|
const params: unknown[] = from && to ? [from, to] : [];
|
|
607
618
|
|
|
608
619
|
return this.db.prepare(`
|
|
@@ -620,7 +631,8 @@ export class SessionRepo {
|
|
|
620
631
|
MAX(t.timestamp) as last_seen
|
|
621
632
|
FROM turns t
|
|
622
633
|
LEFT JOIN turn_tag_sets tts ON tts.turn_id = t.id
|
|
623
|
-
JOIN sessions s ON s.id = t.session_id
|
|
634
|
+
JOIN sessions s ON s.id = t.session_id
|
|
635
|
+
WHERE s.parent_session_id IS NULL${timeFilter}
|
|
624
636
|
GROUP BY COALESCE(tts.tag_set, 'Untagged')
|
|
625
637
|
ORDER BY last_seen DESC
|
|
626
638
|
`).all(...params) as { tags: string; turn_count: number; human_interventions: number; total_duration_ms: number; total_cost_usd: number; last_seen: string }[];
|
package/src/sync/sync.test.ts
CHANGED
|
@@ -104,6 +104,8 @@ describe("zozul sync e2e", () => {
|
|
|
104
104
|
repo.upsertSession({
|
|
105
105
|
id: "sess-001",
|
|
106
106
|
project_path: "/projects/test",
|
|
107
|
+
parent_session_id: null,
|
|
108
|
+
agent_type: null,
|
|
107
109
|
started_at: "2026-03-28T10:00:00Z",
|
|
108
110
|
total_input_tokens: 100,
|
|
109
111
|
total_output_tokens: 50,
|
|
@@ -249,6 +251,8 @@ describe("zozul sync e2e", () => {
|
|
|
249
251
|
repo.upsertSession({
|
|
250
252
|
id: "sess-002",
|
|
251
253
|
project_path: null,
|
|
254
|
+
parent_session_id: null,
|
|
255
|
+
agent_type: null,
|
|
252
256
|
started_at: "2026-03-28T11:00:00Z",
|
|
253
257
|
total_input_tokens: 10,
|
|
254
258
|
total_output_tokens: 10,
|
|
@@ -296,6 +300,8 @@ describe("zozul sync e2e", () => {
|
|
|
296
300
|
repo.upsertSession({
|
|
297
301
|
id: "sess-A",
|
|
298
302
|
project_path: null,
|
|
303
|
+
parent_session_id: null,
|
|
304
|
+
agent_type: null,
|
|
299
305
|
started_at: "2026-03-28T12:00:00Z",
|
|
300
306
|
total_input_tokens: 10,
|
|
301
307
|
total_output_tokens: 10,
|
|
@@ -330,6 +336,8 @@ describe("zozul sync e2e", () => {
|
|
|
330
336
|
repo.upsertSession({
|
|
331
337
|
id: "sess-B",
|
|
332
338
|
project_path: "/new",
|
|
339
|
+
parent_session_id: null,
|
|
340
|
+
agent_type: null,
|
|
333
341
|
started_at: "2026-03-28T13:00:00Z",
|
|
334
342
|
total_input_tokens: 20,
|
|
335
343
|
total_output_tokens: 20,
|
|
@@ -369,6 +377,8 @@ describe("zozul sync e2e", () => {
|
|
|
369
377
|
repo.upsertSession({
|
|
370
378
|
id: "sess-dry",
|
|
371
379
|
project_path: null,
|
|
380
|
+
parent_session_id: null,
|
|
381
|
+
agent_type: null,
|
|
372
382
|
started_at: "2026-03-28T14:00:00Z",
|
|
373
383
|
total_input_tokens: 5,
|
|
374
384
|
total_output_tokens: 5,
|
|
@@ -411,6 +421,8 @@ describe("zozul sync e2e", () => {
|
|
|
411
421
|
repo.upsertSession({
|
|
412
422
|
id: "sess-err",
|
|
413
423
|
project_path: null,
|
|
424
|
+
parent_session_id: null,
|
|
425
|
+
agent_type: null,
|
|
414
426
|
started_at: "2026-03-28T15:00:00Z",
|
|
415
427
|
total_input_tokens: 10,
|
|
416
428
|
total_output_tokens: 10,
|
package/src/sync/transform.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
export type ApiSession = {
|
|
8
8
|
id: string;
|
|
9
9
|
project_path: string | null;
|
|
10
|
+
parent_session_id: string | null;
|
|
11
|
+
agent_type: string | null;
|
|
10
12
|
started_at: string;
|
|
11
13
|
ended_at: string | null;
|
|
12
14
|
total_input_tokens: number;
|
|
@@ -103,6 +105,8 @@ export function transformSession(row: SessionRow): ApiSession {
|
|
|
103
105
|
return {
|
|
104
106
|
id: row.id,
|
|
105
107
|
project_path: row.project_path,
|
|
108
|
+
parent_session_id: row.parent_session_id,
|
|
109
|
+
agent_type: row.agent_type,
|
|
106
110
|
started_at: row.started_at,
|
|
107
111
|
ended_at: row.ended_at,
|
|
108
112
|
total_input_tokens: row.total_input_tokens,
|