1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Deserializer, Serialize};
3
4fn null_to_empty<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
6 Option::<String>::deserialize(d).map(|o| o.unwrap_or_default())
7}
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12#[serde(rename_all = "snake_case")]
13pub enum PatternType {
14 RepetitiveInstruction,
15 RecurringMistake,
16 WorkflowPattern,
17 StaleContext,
18 RedundantContext,
19}
20
21impl std::fmt::Display for PatternType {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 match self {
24 Self::RepetitiveInstruction => write!(f, "repetitive_instruction"),
25 Self::RecurringMistake => write!(f, "recurring_mistake"),
26 Self::WorkflowPattern => write!(f, "workflow_pattern"),
27 Self::StaleContext => write!(f, "stale_context"),
28 Self::RedundantContext => write!(f, "redundant_context"),
29 }
30 }
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
34#[serde(rename_all = "snake_case")]
35pub enum PatternStatus {
36 Discovered,
37 Active,
38 Archived,
39 Dismissed,
40}
41
42impl std::fmt::Display for PatternStatus {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 Self::Discovered => write!(f, "discovered"),
46 Self::Active => write!(f, "active"),
47 Self::Archived => write!(f, "archived"),
48 Self::Dismissed => write!(f, "dismissed"),
49 }
50 }
51}
52
53impl PatternStatus {
54 pub fn from_str(s: &str) -> Self {
55 match s {
56 "discovered" => Self::Discovered,
57 "active" => Self::Active,
58 "archived" => Self::Archived,
59 "dismissed" => Self::Dismissed,
60 _ => Self::Discovered,
61 }
62 }
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
66#[serde(rename_all = "snake_case")]
67pub enum SuggestedTarget {
68 Skill,
69 ClaudeMd,
70 GlobalAgent,
71 DbOnly,
72}
73
74impl std::fmt::Display for SuggestedTarget {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 match self {
77 Self::Skill => write!(f, "skill"),
78 Self::ClaudeMd => write!(f, "claude_md"),
79 Self::GlobalAgent => write!(f, "global_agent"),
80 Self::DbOnly => write!(f, "db_only"),
81 }
82 }
83}
84
85impl SuggestedTarget {
86 pub fn from_str(s: &str) -> Self {
87 match s {
88 "skill" => Self::Skill,
89 "claude_md" => Self::ClaudeMd,
90 "global_agent" => Self::GlobalAgent,
91 "db_only" => Self::DbOnly,
92 _ => Self::DbOnly,
93 }
94 }
95}
96
97impl PatternType {
98 pub fn from_str(s: &str) -> Self {
99 match s {
100 "repetitive_instruction" => Self::RepetitiveInstruction,
101 "recurring_mistake" => Self::RecurringMistake,
102 "workflow_pattern" => Self::WorkflowPattern,
103 "stale_context" => Self::StaleContext,
104 "redundant_context" => Self::RedundantContext,
105 _ => Self::WorkflowPattern,
106 }
107 }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct Pattern {
112 pub id: String,
113 pub pattern_type: PatternType,
114 pub description: String,
115 pub confidence: f64,
116 pub times_seen: i64,
117 pub first_seen: DateTime<Utc>,
118 pub last_seen: DateTime<Utc>,
119 pub last_projected: Option<DateTime<Utc>>,
120 pub status: PatternStatus,
121 pub source_sessions: Vec<String>,
122 pub related_files: Vec<String>,
123 pub suggested_content: String,
124 pub suggested_target: SuggestedTarget,
125 pub project: Option<String>,
126 pub generation_failed: bool,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
133#[serde(tag = "type")]
134pub enum SessionEntry {
135 #[serde(rename = "user")]
136 User(UserEntry),
137 #[serde(rename = "assistant")]
138 Assistant(AssistantEntry),
139 #[serde(rename = "summary")]
140 Summary(SummaryEntry),
141 #[serde(rename = "file-history-snapshot")]
142 FileHistorySnapshot(serde_json::Value),
143 #[serde(rename = "progress")]
144 Progress(serde_json::Value),
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148#[serde(rename_all = "camelCase")]
149pub struct UserEntry {
150 pub uuid: String,
151 #[serde(default)]
152 pub parent_uuid: Option<String>,
153 #[serde(default)]
154 pub session_id: Option<String>,
155 #[serde(default)]
156 pub cwd: Option<String>,
157 #[serde(default)]
158 pub version: Option<String>,
159 #[serde(default)]
160 pub git_branch: Option<String>,
161 #[serde(default)]
162 pub timestamp: Option<String>,
163 pub message: UserMessage,
164 #[serde(default)]
165 pub is_sidechain: bool,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct UserMessage {
170 pub role: String,
171 pub content: MessageContent,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
175#[serde(untagged)]
176pub enum MessageContent {
177 Text(String),
178 Blocks(Vec<ContentBlock>),
179 Other(serde_json::Value),
181}
182
183impl MessageContent {
184 pub fn as_text(&self) -> String {
186 match self {
187 MessageContent::Text(s) => s.clone(),
188 MessageContent::Blocks(blocks) => {
189 let mut parts = Vec::new();
190 for block in blocks {
191 match block {
192 ContentBlock::Text { text } => parts.push(text.clone()),
193 ContentBlock::ToolResult { content, .. } => {
194 if let Some(c) = content {
195 parts.push(c.as_text());
196 }
197 }
198 _ => {}
199 }
200 }
201 parts.join("\n")
202 }
203 MessageContent::Other(_) => String::new(),
204 }
205 }
206
207 pub fn is_tool_result(&self) -> bool {
209 matches!(self, MessageContent::Blocks(blocks) if blocks.iter().any(|b| matches!(b, ContentBlock::ToolResult { .. })))
210 }
211
212 pub fn is_unknown(&self) -> bool {
214 matches!(self, MessageContent::Other(_))
215 }
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
219#[serde(tag = "type")]
220pub enum ContentBlock {
221 #[serde(rename = "text")]
222 Text { text: String },
223 #[serde(rename = "thinking")]
224 Thinking {
225 thinking: String,
226 #[serde(default)]
227 signature: Option<String>,
228 },
229 #[serde(rename = "tool_use")]
230 ToolUse {
231 id: String,
232 name: String,
233 #[serde(default)]
234 input: serde_json::Value,
235 },
236 #[serde(rename = "tool_result")]
237 ToolResult {
238 tool_use_id: String,
239 #[serde(default)]
240 content: Option<ToolResultContent>,
241 },
242 #[serde(other)]
244 Unknown,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249#[serde(untagged)]
250pub enum ToolResultContent {
251 Text(String),
252 Blocks(Vec<serde_json::Value>),
253}
254
255impl ToolResultContent {
256 pub fn as_text(&self) -> String {
257 match self {
258 Self::Text(s) => s.clone(),
259 Self::Blocks(blocks) => {
260 blocks
261 .iter()
262 .filter_map(|b| b.get("text").and_then(|t| t.as_str()))
263 .collect::<Vec<_>>()
264 .join("\n")
265 }
266 }
267 }
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
271#[serde(rename_all = "camelCase")]
272pub struct AssistantEntry {
273 pub uuid: String,
274 #[serde(default)]
275 pub parent_uuid: Option<String>,
276 #[serde(default)]
277 pub session_id: Option<String>,
278 #[serde(default)]
279 pub cwd: Option<String>,
280 #[serde(default)]
281 pub version: Option<String>,
282 #[serde(default)]
283 pub git_branch: Option<String>,
284 #[serde(default)]
285 pub timestamp: Option<String>,
286 pub message: AssistantMessage,
287 #[serde(default)]
288 pub is_sidechain: bool,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct AssistantMessage {
293 pub role: String,
294 #[serde(default)]
295 pub model: Option<String>,
296 #[serde(default)]
297 pub content: Vec<ContentBlock>,
298 #[serde(default)]
299 pub stop_reason: Option<String>,
300 #[serde(default)]
301 pub usage: Option<Usage>,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct Usage {
306 #[serde(default)]
307 pub input_tokens: u64,
308 #[serde(default)]
309 pub output_tokens: u64,
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
313#[serde(rename_all = "camelCase")]
314pub struct SummaryEntry {
315 #[serde(default)]
316 pub uuid: String,
317 #[serde(default)]
318 pub parent_uuid: Option<String>,
319 #[serde(default)]
320 pub session_id: Option<String>,
321 #[serde(default)]
322 pub timestamp: Option<String>,
323 #[serde(default)]
324 pub summary: Option<String>,
325 #[serde(default)]
327 pub message: Option<serde_json::Value>,
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct Session {
335 pub session_id: String,
336 pub project: String,
337 pub session_path: String,
338 pub user_messages: Vec<ParsedUserMessage>,
339 pub assistant_messages: Vec<ParsedAssistantMessage>,
340 pub summaries: Vec<String>,
341 pub tools_used: Vec<String>,
342 pub errors: Vec<String>,
343 pub metadata: SessionMetadata,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct ParsedUserMessage {
348 pub text: String,
349 pub timestamp: Option<String>,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct ParsedAssistantMessage {
354 pub text: String,
355 pub thinking_summary: Option<String>,
356 pub tools: Vec<String>,
357 pub timestamp: Option<String>,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct SessionMetadata {
362 pub cwd: Option<String>,
363 pub version: Option<String>,
364 pub git_branch: Option<String>,
365 pub model: Option<String>,
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
371#[serde(rename_all = "camelCase")]
372pub struct HistoryEntry {
373 #[serde(default)]
374 pub display: Option<String>,
375 #[serde(default)]
376 pub timestamp: Option<u64>,
377 #[serde(default)]
378 pub project: Option<String>,
379 #[serde(default)]
380 pub session_id: Option<String>,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct PluginSkillSummary {
387 pub plugin_name: String,
388 pub skill_name: String,
389 pub description: String,
390}
391
392#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct ContextSnapshot {
394 pub claude_md: Option<String>,
395 pub skills: Vec<SkillFile>,
396 pub memory_md: Option<String>,
397 pub global_agents: Vec<AgentFile>,
398 #[serde(default)]
399 pub plugin_skills: Vec<PluginSkillSummary>,
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct SkillFile {
404 pub path: String,
405 pub content: String,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct AgentFile {
410 pub path: String,
411 pub content: String,
412}
413
414#[derive(Debug, Clone)]
417pub struct IngestedSession {
418 pub session_id: String,
419 pub project: String,
420 pub session_path: String,
421 pub file_size: u64,
422 pub file_mtime: String,
423 pub ingested_at: DateTime<Utc>,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize)]
430#[serde(tag = "action")]
431pub enum PatternUpdate {
432 #[serde(rename = "new")]
433 New(NewPattern),
434 #[serde(rename = "update")]
435 Update(UpdateExisting),
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct NewPattern {
440 pub pattern_type: PatternType,
441 #[serde(deserialize_with = "null_to_empty")]
442 pub description: String,
443 pub confidence: f64,
444 #[serde(default)]
445 pub source_sessions: Vec<String>,
446 #[serde(default)]
447 pub related_files: Vec<String>,
448 #[serde(default, deserialize_with = "null_to_empty")]
449 pub suggested_content: String,
450 pub suggested_target: SuggestedTarget,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
454pub struct UpdateExisting {
455 #[serde(deserialize_with = "null_to_empty")]
456 pub existing_id: String,
457 #[serde(default)]
458 pub new_sessions: Vec<String>,
459 pub new_confidence: f64,
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize)]
464pub struct AnalysisResponse {
465 #[serde(default)]
466 pub reasoning: String,
467 pub patterns: Vec<PatternUpdate>,
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize)]
472pub struct ClaudeCliOutput {
473 #[serde(default)]
474 pub result: Option<String>,
475 #[serde(default)]
476 pub is_error: bool,
477 #[serde(default)]
478 pub duration_ms: u64,
479 #[serde(default)]
480 pub num_turns: u64,
481 #[serde(default)]
482 pub stop_reason: Option<String>,
483 #[serde(default)]
484 pub session_id: Option<String>,
485 #[serde(default)]
486 pub usage: Option<CliUsage>,
487 #[serde(default)]
490 pub structured_output: Option<serde_json::Value>,
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize)]
495pub struct CliUsage {
496 #[serde(default)]
497 pub input_tokens: u64,
498 #[serde(default)]
499 pub output_tokens: u64,
500 #[serde(default)]
501 pub cache_creation_input_tokens: u64,
502 #[serde(default)]
503 pub cache_read_input_tokens: u64,
504}
505
506impl ClaudeCliOutput {
507 pub fn total_input_tokens(&self) -> u64 {
509 self.usage.as_ref().map_or(0, |u| {
510 u.input_tokens + u.cache_creation_input_tokens + u.cache_read_input_tokens
511 })
512 }
513
514 pub fn total_output_tokens(&self) -> u64 {
516 self.usage.as_ref().map_or(0, |u| u.output_tokens)
517 }
518}
519
520#[derive(Debug, Clone, Serialize, Deserialize)]
522pub struct AuditEntry {
523 pub timestamp: DateTime<Utc>,
524 pub action: String,
525 pub details: serde_json::Value,
526}
527
528#[derive(Debug, Clone)]
530pub struct BatchDetail {
531 pub batch_index: usize,
532 pub session_count: usize,
533 pub session_ids: Vec<String>,
534 pub prompt_chars: usize,
535 pub input_tokens: u64,
536 pub output_tokens: u64,
537 pub new_patterns: usize,
538 pub updated_patterns: usize,
539 pub reasoning: String,
540 pub ai_response_preview: String,
541}
542
543#[derive(Debug, Clone)]
545pub struct AnalyzeResult {
546 pub sessions_analyzed: usize,
547 pub new_patterns: usize,
548 pub updated_patterns: usize,
549 pub total_patterns: usize,
550 pub input_tokens: u64,
551 pub output_tokens: u64,
552 pub batch_details: Vec<BatchDetail>,
553}
554
555#[derive(Debug, Clone, Serialize)]
557pub struct CompactSession {
558 pub session_id: String,
559 pub project: String,
560 pub user_messages: Vec<CompactUserMessage>,
561 pub tools_used: Vec<String>,
562 pub errors: Vec<String>,
563 pub thinking_highlights: Vec<String>,
564 pub summaries: Vec<String>,
565}
566
567#[derive(Debug, Clone, Serialize)]
568pub struct CompactUserMessage {
569 pub text: String,
570 #[serde(skip_serializing_if = "Option::is_none")]
571 pub timestamp: Option<String>,
572}
573
574#[derive(Debug, Clone, Serialize)]
576pub struct CompactPattern {
577 pub id: String,
578 pub pattern_type: String,
579 pub description: String,
580 pub confidence: f64,
581 pub times_seen: i64,
582 pub suggested_target: String,
583}
584
585#[derive(Debug, Clone, Serialize, Deserialize)]
589pub struct Projection {
590 pub id: String,
591 pub pattern_id: String,
592 pub target_type: String,
593 pub target_path: String,
594 pub content: String,
595 pub applied_at: DateTime<Utc>,
596 pub pr_url: Option<String>,
597 pub status: ProjectionStatus,
598}
599
600#[derive(Debug, Clone)]
602pub struct SkillDraft {
603 pub name: String,
604 pub content: String,
605 pub pattern_id: String,
606}
607
608#[derive(Debug, Clone, Serialize, Deserialize)]
610pub struct SkillValidation {
611 pub valid: bool,
612 #[serde(default)]
613 pub feedback: String,
614}
615
616#[derive(Debug, Clone)]
618pub struct AgentDraft {
619 pub name: String,
620 pub content: String,
621 pub pattern_id: String,
622}
623
624#[derive(Debug, Clone)]
626pub struct ApplyAction {
627 pub pattern_id: String,
628 pub pattern_description: String,
629 pub target_type: SuggestedTarget,
630 pub target_path: String,
631 pub content: String,
632 pub track: ApplyTrack,
633}
634
635#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
637#[serde(rename_all = "snake_case")]
638pub enum ProjectionStatus {
639 PendingReview,
640 Applied,
641 Dismissed,
642}
643
644impl std::fmt::Display for ProjectionStatus {
645 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
646 match self {
647 Self::PendingReview => write!(f, "pending_review"),
648 Self::Applied => write!(f, "applied"),
649 Self::Dismissed => write!(f, "dismissed"),
650 }
651 }
652}
653
654impl ProjectionStatus {
655 pub fn from_str(s: &str) -> Option<Self> {
656 match s {
657 "pending_review" => Some(Self::PendingReview),
658 "applied" => Some(Self::Applied),
659 "dismissed" => Some(Self::Dismissed),
660 _ => None,
661 }
662 }
663}
664
665#[derive(Debug, Clone, PartialEq)]
667pub enum ApplyTrack {
668 Personal,
670 Shared,
672}
673
674impl std::fmt::Display for ApplyTrack {
675 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
676 match self {
677 Self::Personal => write!(f, "personal"),
678 Self::Shared => write!(f, "shared"),
679 }
680 }
681}
682
683#[derive(Debug, Clone)]
685pub struct ApplyPlan {
686 pub actions: Vec<ApplyAction>,
687}
688
689impl ApplyPlan {
690 pub fn is_empty(&self) -> bool {
691 self.actions.is_empty()
692 }
693
694 pub fn personal_actions(&self) -> Vec<&ApplyAction> {
695 self.actions.iter().filter(|a| a.track == ApplyTrack::Personal).collect()
696 }
697
698 pub fn shared_actions(&self) -> Vec<&ApplyAction> {
699 self.actions.iter().filter(|a| a.track == ApplyTrack::Shared).collect()
700 }
701}
702
703#[cfg(test)]
704mod tests {
705 use super::*;
706
707 #[test]
708 fn test_projection_status_display() {
709 assert_eq!(ProjectionStatus::PendingReview.to_string(), "pending_review");
710 assert_eq!(ProjectionStatus::Applied.to_string(), "applied");
711 assert_eq!(ProjectionStatus::Dismissed.to_string(), "dismissed");
712 }
713
714 #[test]
715 fn test_projection_status_from_str() {
716 assert_eq!(ProjectionStatus::from_str("pending_review"), Some(ProjectionStatus::PendingReview));
717 assert_eq!(ProjectionStatus::from_str("applied"), Some(ProjectionStatus::Applied));
718 assert_eq!(ProjectionStatus::from_str("dismissed"), Some(ProjectionStatus::Dismissed));
719 assert_eq!(ProjectionStatus::from_str("unknown"), None);
720 }
721}