zozul-cli 0.3.8 → 0.4.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/.claude/settings.local.json +4 -1
- package/DEVELOPMENT.md +16 -9
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +4 -0
- package/dist/cli/commands.js.map +1 -1
- package/dist/dashboard/index.html +133 -35
- 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 +7 -2
- package/dist/storage/repo.d.ts.map +1 -1
- package/dist/storage/repo.js +37 -26
- package/dist/storage/repo.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands.ts +5 -0
- package/src/dashboard/index.html +133 -35
- package/src/storage/db.ts +12 -0
- package/src/storage/repo.ts +44 -32
package/src/storage/repo.ts
CHANGED
|
@@ -36,14 +36,14 @@ export class SessionRepo {
|
|
|
36
36
|
ended_at = COALESCE(@ended_at, ended_at),
|
|
37
37
|
total_turns = @total_turns,
|
|
38
38
|
model = COALESCE(@model, model),
|
|
39
|
-
--
|
|
40
|
-
--
|
|
41
|
-
total_input_tokens =
|
|
42
|
-
total_output_tokens =
|
|
43
|
-
total_cache_read_tokens =
|
|
44
|
-
total_cache_creation_tokens =
|
|
45
|
-
total_cost_usd =
|
|
46
|
-
total_duration_ms =
|
|
39
|
+
-- JSONL values act as initial estimates; recomputeSessionCostsFromOtel
|
|
40
|
+
-- overwrites with authoritative OTEL data when available.
|
|
41
|
+
total_input_tokens = @total_input_tokens,
|
|
42
|
+
total_output_tokens = @total_output_tokens,
|
|
43
|
+
total_cache_read_tokens = @total_cache_read_tokens,
|
|
44
|
+
total_cache_creation_tokens = @total_cache_creation_tokens,
|
|
45
|
+
total_cost_usd = @total_cost_usd,
|
|
46
|
+
total_duration_ms = @total_duration_ms
|
|
47
47
|
`).run({
|
|
48
48
|
...session,
|
|
49
49
|
ended_at: session.ended_at ?? null,
|
|
@@ -180,15 +180,17 @@ export class SessionRepo {
|
|
|
180
180
|
`).run(event);
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
listSessions(limit = 50, offset = 0, from?: string, to?: string): SessionRow[] {
|
|
183
|
+
listSessions(limit = 50, offset = 0, from?: string, to?: string): (SessionRow & { user_turns: number })[] {
|
|
184
184
|
if (from && to) {
|
|
185
185
|
return this.db.prepare(`
|
|
186
|
-
SELECT * FROM
|
|
187
|
-
|
|
186
|
+
SELECT s.*, (SELECT COUNT(*) FROM turns t WHERE t.session_id = s.id AND t.is_real_user = 1) as user_turns
|
|
187
|
+
FROM sessions s WHERE s.parent_session_id IS NULL AND s.started_at >= ? AND s.started_at <= ? ORDER BY s.started_at DESC LIMIT ? OFFSET ?
|
|
188
|
+
`).all(from, to, limit, offset) as (SessionRow & { user_turns: number })[];
|
|
188
189
|
}
|
|
189
190
|
return this.db.prepare(`
|
|
190
|
-
SELECT * FROM
|
|
191
|
-
|
|
191
|
+
SELECT s.*, (SELECT COUNT(*) FROM turns t WHERE t.session_id = s.id AND t.is_real_user = 1) as user_turns
|
|
192
|
+
FROM sessions s WHERE s.parent_session_id IS NULL ORDER BY s.started_at DESC LIMIT ? OFFSET ?
|
|
193
|
+
`).all(limit, offset) as (SessionRow & { user_turns: number })[];
|
|
192
194
|
}
|
|
193
195
|
|
|
194
196
|
countSessions(from?: string, to?: string): number {
|
|
@@ -200,8 +202,11 @@ export class SessionRepo {
|
|
|
200
202
|
return row.n;
|
|
201
203
|
}
|
|
202
204
|
|
|
203
|
-
getSession(id: string): SessionRow | undefined {
|
|
204
|
-
return this.db.prepare(`
|
|
205
|
+
getSession(id: string): (SessionRow & { user_turns: number }) | undefined {
|
|
206
|
+
return this.db.prepare(`
|
|
207
|
+
SELECT s.*, (SELECT COUNT(*) FROM turns t WHERE t.session_id = s.id AND t.is_real_user = 1) as user_turns
|
|
208
|
+
FROM sessions s WHERE s.id = ?
|
|
209
|
+
`).get(id) as (SessionRow & { user_turns: number }) | undefined;
|
|
205
210
|
}
|
|
206
211
|
|
|
207
212
|
getSubSessions(parentSessionId: string): SessionRow[] {
|
|
@@ -270,7 +275,7 @@ export class SessionRepo {
|
|
|
270
275
|
timestamp: string;
|
|
271
276
|
}): void {
|
|
272
277
|
this.db.prepare(`
|
|
273
|
-
INSERT INTO otel_metrics (name, value, attributes, session_id, model, timestamp)
|
|
278
|
+
INSERT OR IGNORE INTO otel_metrics (name, value, attributes, session_id, model, timestamp)
|
|
274
279
|
VALUES (@name, @value, @attributes, @session_id, @model, @timestamp)
|
|
275
280
|
`).run(metric);
|
|
276
281
|
}
|
|
@@ -284,7 +289,7 @@ export class SessionRepo {
|
|
|
284
289
|
timestamp: string;
|
|
285
290
|
}[]): void {
|
|
286
291
|
const stmt = this.db.prepare(`
|
|
287
|
-
INSERT INTO otel_metrics (name, value, attributes, session_id, model, timestamp)
|
|
292
|
+
INSERT OR IGNORE INTO otel_metrics (name, value, attributes, session_id, model, timestamp)
|
|
288
293
|
VALUES (@name, @value, @attributes, @session_id, @model, @timestamp)
|
|
289
294
|
`);
|
|
290
295
|
const tx = this.db.transaction((rows: typeof metrics) => {
|
|
@@ -612,7 +617,7 @@ export class SessionRepo {
|
|
|
612
617
|
* Fixes drift caused by late-start catch-up batches or duplicate processing.
|
|
613
618
|
* Safe to call repeatedly — always produces correct values from append-only source data.
|
|
614
619
|
*/
|
|
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 }[] {
|
|
620
|
+
getTaskGroups(from?: string, to?: string): { tags: string; turn_count: number; human_interventions: number; total_duration_ms: number; total_cost_usd: number; total_tokens: number; last_seen: string }[] {
|
|
616
621
|
const timeFilter = from && to ? " AND t.timestamp >= ? AND t.timestamp <= ?" : "";
|
|
617
622
|
const params: unknown[] = from && to ? [from, to] : [];
|
|
618
623
|
|
|
@@ -628,6 +633,7 @@ export class SessionRepo {
|
|
|
628
633
|
SUM(CASE WHEN t.is_real_user = 1 THEN 1 ELSE 0 END) as human_interventions,
|
|
629
634
|
COALESCE(SUM(t.duration_ms), 0) as total_duration_ms,
|
|
630
635
|
COALESCE(SUM(${PROPORTIONAL_COST_SQL}), 0) as total_cost_usd,
|
|
636
|
+
COALESCE(SUM(t.input_tokens), 0) + COALESCE(SUM(t.output_tokens), 0) + COALESCE(SUM(t.cache_read_tokens), 0) as total_tokens,
|
|
631
637
|
MAX(t.timestamp) as last_seen
|
|
632
638
|
FROM turns t
|
|
633
639
|
LEFT JOIN turn_tag_sets tts ON tts.turn_id = t.id
|
|
@@ -635,11 +641,11 @@ export class SessionRepo {
|
|
|
635
641
|
WHERE s.parent_session_id IS NULL${timeFilter}
|
|
636
642
|
GROUP BY COALESCE(tts.tag_set, 'Untagged')
|
|
637
643
|
ORDER BY last_seen DESC
|
|
638
|
-
`).all(...params) as { tags: string; turn_count: number; human_interventions: number; total_duration_ms: number; total_cost_usd: number; last_seen: string }[];
|
|
644
|
+
`).all(...params) as { tags: string; turn_count: number; human_interventions: number; total_duration_ms: number; total_cost_usd: number; total_tokens: number; last_seen: string }[];
|
|
639
645
|
}
|
|
640
646
|
|
|
641
647
|
recomputeSessionCostsFromOtel(): number {
|
|
642
|
-
// Recompute from raw OTEL for sessions that have metrics
|
|
648
|
+
// Step 1: Recompute from raw OTEL for sessions that have metrics (authoritative)
|
|
643
649
|
const updated = this.db.prepare(`
|
|
644
650
|
UPDATE sessions SET
|
|
645
651
|
total_cost_usd = COALESCE((
|
|
@@ -649,31 +655,37 @@ export class SessionRepo {
|
|
|
649
655
|
total_input_tokens = COALESCE((
|
|
650
656
|
SELECT SUM(value) FROM otel_metrics
|
|
651
657
|
WHERE name = 'claude_code.token.usage' AND json_extract(attributes, '$.type') = 'input' AND session_id = sessions.id
|
|
652
|
-
),
|
|
658
|
+
), 0),
|
|
653
659
|
total_output_tokens = COALESCE((
|
|
654
660
|
SELECT SUM(value) FROM otel_metrics
|
|
655
661
|
WHERE name = 'claude_code.token.usage' AND json_extract(attributes, '$.type') = 'output' AND session_id = sessions.id
|
|
656
|
-
),
|
|
662
|
+
), 0),
|
|
657
663
|
total_cache_read_tokens = COALESCE((
|
|
658
664
|
SELECT SUM(value) FROM otel_metrics
|
|
659
665
|
WHERE name = 'claude_code.token.usage' AND json_extract(attributes, '$.type') = 'cacheRead' AND session_id = sessions.id
|
|
660
|
-
),
|
|
666
|
+
), 0),
|
|
661
667
|
total_cache_creation_tokens = COALESCE((
|
|
662
668
|
SELECT SUM(value) FROM otel_metrics
|
|
663
669
|
WHERE name = 'claude_code.token.usage' AND json_extract(attributes, '$.type') = 'cacheCreation' AND session_id = sessions.id
|
|
664
|
-
),
|
|
665
|
-
total_duration_ms = COALESCE((
|
|
666
|
-
SELECT SUM(value) * 1000 FROM otel_metrics
|
|
667
|
-
WHERE name = 'claude_code.active_time.total' AND session_id = sessions.id
|
|
668
|
-
), total_duration_ms)
|
|
670
|
+
), 0)
|
|
669
671
|
WHERE id IN (SELECT DISTINCT session_id FROM otel_metrics WHERE session_id IS NOT NULL)
|
|
670
672
|
`).run();
|
|
671
673
|
|
|
672
|
-
//
|
|
674
|
+
// Step 2: Recompute duration from turns for all sessions
|
|
673
675
|
this.db.prepare(`
|
|
674
|
-
UPDATE sessions SET
|
|
675
|
-
|
|
676
|
-
|
|
676
|
+
UPDATE sessions SET
|
|
677
|
+
total_duration_ms = COALESCE((
|
|
678
|
+
SELECT SUM(duration_ms) FROM turns WHERE session_id = sessions.id
|
|
679
|
+
), 0)
|
|
680
|
+
`).run();
|
|
681
|
+
|
|
682
|
+
// Step 3: Fallback for sessions without OTEL — use SUM(turns.cost_usd)
|
|
683
|
+
this.db.prepare(`
|
|
684
|
+
UPDATE sessions SET
|
|
685
|
+
total_cost_usd = COALESCE((
|
|
686
|
+
SELECT SUM(cost_usd) FROM turns WHERE session_id = sessions.id
|
|
687
|
+
), 0)
|
|
688
|
+
WHERE id NOT IN (SELECT DISTINCT session_id FROM otel_metrics WHERE session_id IS NOT NULL)
|
|
677
689
|
`).run();
|
|
678
690
|
|
|
679
691
|
return updated.changes;
|