claudy 0.6.0

Modern multi-provider launcher for Claude CLI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
use serde::{Deserialize, Serialize};

// ── Input types for store operations ──

#[derive(Debug, Clone)]
pub struct NewSession {
    pub session_uuid: String,
    pub project_id: i64,
    pub source_file: String,
    pub cwd: Option<String>,
    pub model: Option<String>,
    pub first_message: Option<String>,
    pub started_at: Option<String>,
    /// Neutral source label (e.g. "live", "archive") for R2 archive fallback.
    /// NULL for pre-R2 rows.
    pub source_kind: Option<String>,
}

#[derive(Debug, Clone)]
pub struct NewTurn {
    pub session_id: i64,
    pub turn_number: i32,
    pub prompt_text: Option<String>,
    pub response_text: Option<String>,
    pub model: Option<String>,
    pub duration_ms: Option<i64>,
    pub started_at: Option<String>,
    /// Neutral "author" flag — true when this turn's prompt was a genuine
    /// human-typed message (not a meta/command injection). Code-authorship-level
    /// human-vs-AI (which needs git) is deferred to the downstream consumer.
    pub human_authored: bool,
}

#[derive(Debug, Clone)]
pub struct NewTokenUsage {
    pub turn_id: i64,
    pub model: String,
    pub input_tokens: i64,
    pub output_tokens: i64,
    pub cache_creation_input_tokens: i64,
    pub cache_read_input_tokens: i64,
    pub estimated_cost_usd: f64,
}

#[derive(Debug, Clone)]
pub struct NewToolCall {
    pub turn_id: i64,
    pub tool_use_id: String,
    pub tool_name: String,
    pub input_summary: Option<String>,
    pub is_error: bool,
    pub result_summary: Option<String>,
    pub duration_ms: Option<i64>,
}

/// Session-level outcome counters, written into `session_outcomes`.
///
/// These are observed outcomes only — what the session actually did, not how
/// much it cost. Token and cost figures live in `token_usage`/`sessions` and
/// are deliberately absent here.
///
/// `repo` is the raw session cwd, stored verbatim. claudy does no repo
/// curation: every session gets a row, and any canonicalization, grouping, or
/// filtering is left to whatever reads the table.
#[derive(Debug, Clone)]
pub struct NewSessionOutcome {
    pub session_uuid: String,
    pub repo: String,
    pub started_at: Option<String>,
    pub ended_at: Option<String>,
    /// Every tool invocation observed, whatever the tool. This is the
    /// denominator the failure count is read against, so it is deliberately
    /// not restricted to the tools the other counters look at.
    pub n_tool_calls: i64,
    /// Tool invocations whose result was reported as an error.
    pub n_tool_fail: i64,
    /// Commit-shaped git invocations that were not observed to fail.
    pub commits_made: i64,
    /// Revert-shaped git invocations that were not observed to fail.
    pub reverts_made: i64,
}

/// How a batch of [`NewSessionOutcome`] counters relates to what is already stored.
///
/// A parse that started at byte 0 saw the whole transcript, so its counters
/// describe the entire session and supersede any stored row. A parse that
/// resumed from a checkpoint saw only the appended tail, so its counters are a
/// delta to add on top.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutcomeWriteMode {
    /// Authoritative whole-session counts: insert, or replace what is stored.
    Replace,
    /// Tail-only counts: add to an existing row. Never creates a row, so a
    /// partial tail can't masquerade as a complete session.
    Accumulate,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectRecord {
    pub id: i64,
    pub encoded_dir: String,
    pub display_name: String,
    pub resolved_path: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionRecord {
    pub id: i64,
    pub session_uuid: String,
    pub project_id: i64,
    pub cwd: Option<String>,
    pub model: Option<String>,
    pub started_at: Option<String>,
    pub ended_at: Option<String>,
    pub total_turns: i32,
    pub total_cost_usd: f64,
    pub total_duration_ms: i64,
    pub first_message: Option<String>,
    pub source_file: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TurnRecord {
    pub id: i64,
    pub session_id: i64,
    pub turn_number: i32,
    pub prompt_text: Option<String>,
    pub response_text: Option<String>,
    pub model: Option<String>,
    pub duration_ms: Option<i64>,
    pub started_at: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenUsageRecord {
    pub id: i64,
    pub turn_id: i64,
    pub model: String,
    pub input_tokens: i64,
    pub output_tokens: i64,
    pub cache_creation_input_tokens: i64,
    pub cache_read_input_tokens: i64,
    pub estimated_cost_usd: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallRecord {
    pub id: i64,
    pub turn_id: i64,
    pub tool_use_id: String,
    pub tool_name: String,
    pub input_summary: Option<String>,
    pub is_error: bool,
    pub result_summary: Option<String>,
    pub duration_ms: Option<i64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelMetricRecord {
    pub id: i64,
    pub session_id: Option<i64>,
    pub platform: String,
    pub channel_id: String,
    pub user_id: String,
    pub profile: Option<String>,
    pub stream_duration_ms: Option<i64>,
    pub first_byte_ms: Option<i64>,
    pub stream_timeout: bool,
    pub error_type: Option<String>,
}

// ── Analysis result types ──

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenTrendPoint {
    pub date: String,
    pub model: String,
    pub input_tokens: i64,
    pub output_tokens: i64,
    pub total_cost_usd: f64,
    pub session_count: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDistribution {
    pub tool_name: String,
    pub call_count: i64,
    pub error_count: i64,
    pub avg_duration_ms: Option<f64>,
    pub percentage: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CostMetrics {
    pub total_cost_usd: f64,
    pub avg_cost_per_session: f64,
    pub avg_cost_per_turn: f64,
    pub weekly_avg_cost: f64,
    pub total_sessions: i64,
    pub total_turns: i64,
    pub cache_savings_usd: f64,
    pub estimated_cost_portion: f64,
    pub by_model: Vec<ModelCostBreakdown>,
    pub most_expensive_session: Option<SessionCostHighlight>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelCostBreakdown {
    pub model: String,
    pub total_cost_usd: f64,
    pub total_input_tokens: i64,
    pub total_output_tokens: i64,
    pub total_cache_read_tokens: i64,
    pub session_count: i64,
    pub avg_cost_per_session: f64,
    pub percentage: f64,
    pub pricing_source: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionCostHighlight {
    pub session_uuid: String,
    pub project_name: String,
    pub cost_usd: f64,
    pub turns: i32,
    pub model: Option<String>,
    pub started_at: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DashboardStats {
    pub total_sessions: i64,
    pub total_cost_usd: f64,
    pub total_turns: i64,
    pub total_duration_ms: i64,
    pub avg_tokens_per_session: f64,
    pub cache_hit_ratio: f64,
    pub most_used_model: Option<String>,
    pub alert_count: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptEfficiency {
    pub session_uuid: String,
    pub project_name: String,
    pub total_turns: i32,
    pub total_input_tokens: i64,
    pub total_output_tokens: i64,
    pub tool_call_count: i64,
    pub tool_overhead_ratio: f64,
    pub cache_hit_ratio: f64,
    pub cost_usd: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolPattern {
    pub sequence: Vec<String>,
    pub frequency: i64,
    pub error_rate: f64,
    pub is_anti_pattern: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelPerformance {
    pub model: String,
    pub avg_duration_ms: f64,
    pub avg_input_tokens: f64,
    pub avg_output_tokens: f64,
    pub avg_cost_per_session: f64,
    pub total_sessions: i64,
    pub cache_hit_ratio: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionComparison {
    pub session_uuid: String,
    pub project_name: String,
    pub started_at: Option<String>,
    pub duration_ms: i64,
    pub total_cost_usd: f64,
    pub total_turns: i32,
    pub model: Option<String>,
}

// ── Insights summary types (LLM-consumed) ──

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InsightsSummary {
    pub period: InsightsPeriod,
    pub overview: InsightsOverview,
    pub daily_costs: Vec<InsightsDailyCost>,
    pub model_distribution: Vec<ModelCostBreakdown>,
    pub tool_usage: Vec<ToolDistribution>,
    pub notable_sessions: Vec<SessionCostHighlight>,
    pub cost_analysis: InsightsCostAnalysis,
    pub cache_efficiency: InsightsCacheEfficiency,
    pub prompt_efficiency: Vec<PromptEfficiency>,
    pub tool_patterns: Vec<ToolPattern>,
    pub model_performance: Vec<ModelPerformance>,
    pub session_comparisons: Vec<SessionComparison>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InsightsPeriod {
    pub from: String,
    pub to: String,
    pub days: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InsightsOverview {
    pub total_sessions: i64,
    pub total_cost_usd: f64,
    pub total_turns: i64,
    pub avg_tokens_per_session: f64,
    pub most_used_model: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InsightsDailyCost {
    pub date: String,
    pub cost_usd: f64,
    pub sessions: i64,
    pub model: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InsightsCostAnalysis {
    pub total_cost_usd: f64,
    pub avg_cost_per_session: f64,
    pub avg_cost_per_turn: f64,
    pub weekly_avg_cost: f64,
    pub cache_savings_usd: f64,
    pub estimated_cost_portion: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InsightsCacheEfficiency {
    pub hit_ratio: f64,
    pub savings_usd: f64,
}

// ── Recommendation types ──

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum RecommendationCategory {
    CostOptimization,
    PromptEfficiency,
    ModelSelection,
    ToolUsage,
    AntiPattern,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Severity {
    Info,
    Warning,
    Critical,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Recommendation {
    pub category: RecommendationCategory,
    pub severity: Severity,
    pub title: String,
    pub description: String,
    pub action: Option<String>,
}

// ── Ingestion types ──

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IngestionCheckpoint {
    pub file_path: String,
    pub file_modified: String,
    pub byte_offset: i64,
    pub line_count: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IngestionResult {
    pub files_scanned: u32,
    pub files_ingested: u32,
    pub sessions_created: u32,
    pub turns_created: u32,
    pub token_records_created: u32,
    pub tool_calls_created: u32,
    /// Turns skipped due to a per-turn insert failure (the rest of the file still
    /// ingested). Surfaced so silent skips aren't hidden behind a success line.
    pub turns_skipped: u32,
    pub elapsed_ms: u64,
}

/// Freshness of ingested data, for staleness reporting (`analytics status`).
/// Domain-neutral: only timestamps + counts + per-source last-seen.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FreshnessReport {
    /// Latest `turns.started_at` across all turns (RFC3339), or None if no turns.
    pub latest_turn_at: Option<String>,
    pub total_turns: i64,
    /// Per-source last-seen, keyed by the neutral `sessions.source_kind` label.
    /// Empty before R2 sources are tagged.
    pub per_source: Vec<FreshnessSource>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FreshnessSource {
    pub label: String,
    pub last_seen: Option<String>,
}

// ── Pricing types ──

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelPricing {
    pub model_id: String,
    pub input: f64,
    pub output: f64,
    pub cache_write: f64,
    pub cache_read: f64,
    pub source: String,
    pub synced_at: String,
}

// ── Parsed JSONL event (intermediate) ──

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonlEvent {
    #[serde(rename = "type")]
    pub event_type: String,
    pub session_id: Option<String>,
    pub timestamp: Option<String>,
    pub cwd: Option<String>,
    pub uuid: Option<String>,
    pub parent_uuid: Option<String>,
    pub model: Option<String>,
    pub version: Option<String>,
    pub is_meta: Option<bool>,
    pub message: Option<serde_json::Value>,
    pub subtype: Option<String>,
    pub duration_ms: Option<i64>,
    pub cost_usd: Option<f64>,
    pub num_turns: Option<i64>,
}