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
40pub fn load_workflow_agent_meta(
48 session_id: &str,
49 claude_home: &Path,
50) -> std::collections::HashMap<String, (String, String)> {
51 cc_session_jsonl::scanner::load_workflow_agent_meta(session_id, claude_home)
52 .into_iter()
53 .map(|(k, meta)| {
54 let agent_type = meta.agent_type.unwrap_or_else(|| "unknown".to_string());
55 let description = meta.description.unwrap_or_default();
56 (k, (agent_type, description))
57 })
58 .collect()
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64 use std::fs;
65 use tempfile::TempDir;
66
67 fn setup_claude_home() -> TempDir {
69 let tmp = TempDir::new().unwrap();
70 let projects = tmp.path().join("projects");
71 fs::create_dir_all(&projects).unwrap();
72 tmp
73 }
74
75 #[test]
76 fn scan_finds_all_session_types() {
77 let tmp = setup_claude_home();
78 let project_dir = tmp
79 .path()
80 .join("projects")
81 .join("-Users-testuser-myproject");
82 fs::create_dir_all(&project_dir).unwrap();
83
84 let main_uuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
86 fs::write(
87 project_dir.join(format!("{}.jsonl", main_uuid)),
88 r#"{"type":"user","sessionId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"}"#,
89 )
90 .unwrap();
91
92 fs::write(
94 project_dir.join("agent-abc1234.jsonl"),
95 r#"{"type":"user","sessionId":"parent-session-id-here"}"#,
96 )
97 .unwrap();
98
99 let subagents_dir = project_dir.join(main_uuid).join("subagents");
101 fs::create_dir_all(&subagents_dir).unwrap();
102 fs::write(
103 subagents_dir.join("agent-long-id-abcdef1234567890.jsonl"),
104 r#"{"type":"user","sessionId":"sub-session"}"#,
105 )
106 .unwrap();
107
108 let files = scan_claude_home(tmp.path()).unwrap();
109
110 assert_eq!(
111 files.len(),
112 3,
113 "should find 3 session files, found: {files:?}"
114 );
115
116 let main = files.iter().find(|f| f.session_id == main_uuid).unwrap();
117 assert!(!main.is_agent);
118 assert!(main.parent_session_id.is_none());
119
120 let legacy = files
121 .iter()
122 .find(|f| f.session_id == "agent-abc1234")
123 .unwrap();
124 assert!(legacy.is_agent);
125 assert!(legacy.parent_session_id.is_none()); let new_agent = files
128 .iter()
129 .find(|f| f.session_id == "agent-long-id-abcdef1234567890")
130 .unwrap();
131 assert!(new_agent.is_agent);
132 assert_eq!(
133 new_agent.parent_session_id.as_deref(),
134 Some(main_uuid),
135 "new-style agent should have parent_session_id from directory name"
136 );
137 }
138
139 #[test]
140 fn agent_has_parent_session_id() {
141 let tmp = setup_claude_home();
142 let project_dir = tmp
143 .path()
144 .join("projects")
145 .join("-Users-testuser-myproject");
146 let parent_uuid = "11111111-2222-3333-4444-555555555555";
147 let subagents_dir = project_dir.join(parent_uuid).join("subagents");
148 fs::create_dir_all(&subagents_dir).unwrap();
149
150 fs::write(
151 subagents_dir.join("agent-newstyle-001.jsonl"),
152 r#"{"type":"user","sessionId":"agent-newstyle-001"}"#,
153 )
154 .unwrap();
155
156 let files = scan_claude_home(tmp.path()).unwrap();
157
158 assert_eq!(files.len(), 1);
159 let agent = &files[0];
160 assert!(agent.is_agent);
161 assert_eq!(
162 agent.parent_session_id.as_deref(),
163 Some(parent_uuid),
164 "new-style agent parent_session_id must match the UUID directory"
165 );
166 }
167
168 #[test]
169 fn ignores_non_jsonl_files() {
170 let tmp = setup_claude_home();
171 let project_dir = tmp.path().join("projects").join("-Users-testuser-proj");
172 fs::create_dir_all(&project_dir).unwrap();
173
174 fs::write(project_dir.join("something.meta.json"), "{}").unwrap();
176
177 let tool_results = project_dir.join("tool-results");
179 fs::create_dir_all(&tool_results).unwrap();
180 fs::write(tool_results.join("result.jsonl"), "{}").unwrap();
181
182 let memory = project_dir.join("memory");
184 fs::create_dir_all(&memory).unwrap();
185 fs::write(memory.join("notes.jsonl"), "{}").unwrap();
186
187 fs::write(project_dir.join("notes.txt"), "hello").unwrap();
189
190 let files = scan_claude_home(tmp.path()).unwrap();
191 assert!(
192 files.is_empty(),
193 "should not find any session files, but found: {files:?}"
194 );
195 }
196
197 #[test]
198 fn resolve_legacy_agent_parent() {
199 let tmp = setup_claude_home();
200 let project_dir = tmp.path().join("projects").join("-Users-testuser-proj");
201 fs::create_dir_all(&project_dir).unwrap();
202
203 let agent_file = project_dir.join("agent-xyz7890.jsonl");
204 fs::write(
205 &agent_file,
206 r#"{"type":"user","sessionId":"parent-sess-id","uuid":"u1"}
207{"type":"assistant","sessionId":"parent-sess-id","uuid":"u2"}"#,
208 )
209 .unwrap();
210
211 let mut files = scan_claude_home(tmp.path()).unwrap();
212 assert_eq!(files.len(), 1);
213 assert!(files[0].parent_session_id.is_none());
214
215 resolve_agent_parents(&mut files).unwrap();
216 assert_eq!(
217 files[0].parent_session_id.as_deref(),
218 Some("parent-sess-id"),
219 "legacy agent parent_session_id should come from first line's sessionId"
220 );
221 }
222}