cc_token_usage/data/
scanner.rs1use std::path::Path;
8
9pub use cc_session_jsonl::scanner::{
10 resolve_agent_parents, scan_sessions as scan_sessions_raw, SessionFile,
11};
12
13pub fn scan_claude_home(claude_home: &Path) -> anyhow::Result<Vec<SessionFile>> {
18 Ok(cc_session_jsonl::scanner::scan_sessions(claude_home)?)
19}
20
21pub fn load_agent_meta(
27 session_id: &str,
28 claude_home: &Path,
29) -> std::collections::HashMap<String, (String, String)> {
30 cc_session_jsonl::scanner::load_agent_meta(session_id, claude_home)
31 .into_iter()
32 .map(|(k, meta)| {
33 let agent_type = meta.agent_type.unwrap_or_else(|| "unknown".to_string());
34 let description = meta.description.unwrap_or_default();
35 (k, (agent_type, description))
36 })
37 .collect()
38}
39
40#[cfg(test)]
41mod tests {
42 use super::*;
43 use std::fs;
44 use tempfile::TempDir;
45
46 fn setup_claude_home() -> TempDir {
48 let tmp = TempDir::new().unwrap();
49 let projects = tmp.path().join("projects");
50 fs::create_dir_all(&projects).unwrap();
51 tmp
52 }
53
54 #[test]
55 fn scan_finds_all_session_types() {
56 let tmp = setup_claude_home();
57 let project_dir = tmp
58 .path()
59 .join("projects")
60 .join("-Users-testuser-myproject");
61 fs::create_dir_all(&project_dir).unwrap();
62
63 let main_uuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
65 fs::write(
66 project_dir.join(format!("{}.jsonl", main_uuid)),
67 r#"{"type":"user","sessionId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"}"#,
68 )
69 .unwrap();
70
71 fs::write(
73 project_dir.join("agent-abc1234.jsonl"),
74 r#"{"type":"user","sessionId":"parent-session-id-here"}"#,
75 )
76 .unwrap();
77
78 let subagents_dir = project_dir.join(main_uuid).join("subagents");
80 fs::create_dir_all(&subagents_dir).unwrap();
81 fs::write(
82 subagents_dir.join("agent-long-id-abcdef1234567890.jsonl"),
83 r#"{"type":"user","sessionId":"sub-session"}"#,
84 )
85 .unwrap();
86
87 let files = scan_claude_home(tmp.path()).unwrap();
88
89 assert_eq!(
90 files.len(),
91 3,
92 "should find 3 session files, found: {files:?}"
93 );
94
95 let main = files.iter().find(|f| f.session_id == main_uuid).unwrap();
96 assert!(!main.is_agent);
97 assert!(main.parent_session_id.is_none());
98
99 let legacy = files
100 .iter()
101 .find(|f| f.session_id == "agent-abc1234")
102 .unwrap();
103 assert!(legacy.is_agent);
104 assert!(legacy.parent_session_id.is_none()); let new_agent = files
107 .iter()
108 .find(|f| f.session_id == "agent-long-id-abcdef1234567890")
109 .unwrap();
110 assert!(new_agent.is_agent);
111 assert_eq!(
112 new_agent.parent_session_id.as_deref(),
113 Some(main_uuid),
114 "new-style agent should have parent_session_id from directory name"
115 );
116 }
117
118 #[test]
119 fn agent_has_parent_session_id() {
120 let tmp = setup_claude_home();
121 let project_dir = tmp
122 .path()
123 .join("projects")
124 .join("-Users-testuser-myproject");
125 let parent_uuid = "11111111-2222-3333-4444-555555555555";
126 let subagents_dir = project_dir.join(parent_uuid).join("subagents");
127 fs::create_dir_all(&subagents_dir).unwrap();
128
129 fs::write(
130 subagents_dir.join("agent-newstyle-001.jsonl"),
131 r#"{"type":"user","sessionId":"agent-newstyle-001"}"#,
132 )
133 .unwrap();
134
135 let files = scan_claude_home(tmp.path()).unwrap();
136
137 assert_eq!(files.len(), 1);
138 let agent = &files[0];
139 assert!(agent.is_agent);
140 assert_eq!(
141 agent.parent_session_id.as_deref(),
142 Some(parent_uuid),
143 "new-style agent parent_session_id must match the UUID directory"
144 );
145 }
146
147 #[test]
148 fn ignores_non_jsonl_files() {
149 let tmp = setup_claude_home();
150 let project_dir = tmp.path().join("projects").join("-Users-testuser-proj");
151 fs::create_dir_all(&project_dir).unwrap();
152
153 fs::write(project_dir.join("something.meta.json"), "{}").unwrap();
155
156 let tool_results = project_dir.join("tool-results");
158 fs::create_dir_all(&tool_results).unwrap();
159 fs::write(tool_results.join("result.jsonl"), "{}").unwrap();
160
161 let memory = project_dir.join("memory");
163 fs::create_dir_all(&memory).unwrap();
164 fs::write(memory.join("notes.jsonl"), "{}").unwrap();
165
166 fs::write(project_dir.join("notes.txt"), "hello").unwrap();
168
169 let files = scan_claude_home(tmp.path()).unwrap();
170 assert!(
171 files.is_empty(),
172 "should not find any session files, but found: {files:?}"
173 );
174 }
175
176 #[test]
177 fn resolve_legacy_agent_parent() {
178 let tmp = setup_claude_home();
179 let project_dir = tmp.path().join("projects").join("-Users-testuser-proj");
180 fs::create_dir_all(&project_dir).unwrap();
181
182 let agent_file = project_dir.join("agent-xyz7890.jsonl");
183 fs::write(
184 &agent_file,
185 r#"{"type":"user","sessionId":"parent-sess-id","uuid":"u1"}
186{"type":"assistant","sessionId":"parent-sess-id","uuid":"u2"}"#,
187 )
188 .unwrap();
189
190 let mut files = scan_claude_home(tmp.path()).unwrap();
191 assert_eq!(files.len(), 1);
192 assert!(files[0].parent_session_id.is_none());
193
194 resolve_agent_parents(&mut files).unwrap();
195 assert_eq!(
196 files[0].parent_session_id.as_deref(),
197 Some("parent-sess-id"),
198 "legacy agent parent_session_id should come from first line's sessionId"
199 );
200 }
201}