agent-recorder 0.1.0__tar.gz

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.
Files changed (28) hide show
  1. agent_recorder-0.1.0/PKG-INFO +237 -0
  2. agent_recorder-0.1.0/README.md +211 -0
  3. agent_recorder-0.1.0/agent_flight_recorder/__init__.py +0 -0
  4. agent_recorder-0.1.0/agent_flight_recorder/adapters/__init__.py +0 -0
  5. agent_recorder-0.1.0/agent_flight_recorder/adapters/claude.py +139 -0
  6. agent_recorder-0.1.0/agent_flight_recorder/adapters/codex.py +139 -0
  7. agent_recorder-0.1.0/agent_flight_recorder/analyzers/__init__.py +0 -0
  8. agent_recorder-0.1.0/agent_flight_recorder/analyzers/skill_extractor.py +120 -0
  9. agent_recorder-0.1.0/agent_flight_recorder/analyzers/stats.py +45 -0
  10. agent_recorder-0.1.0/agent_flight_recorder/cli.py +117 -0
  11. agent_recorder-0.1.0/agent_flight_recorder/db.py +163 -0
  12. agent_recorder-0.1.0/agent_flight_recorder/ingester.py +42 -0
  13. agent_recorder-0.1.0/agent_flight_recorder/models.py +65 -0
  14. agent_recorder-0.1.0/agent_flight_recorder/redactor.py +20 -0
  15. agent_recorder-0.1.0/agent_flight_recorder/render/__init__.py +0 -0
  16. agent_recorder-0.1.0/agent_flight_recorder/render/terminal.py +88 -0
  17. agent_recorder-0.1.0/pyproject.toml +47 -0
  18. agent_recorder-0.1.0/tests/__init__.py +0 -0
  19. agent_recorder-0.1.0/tests/conftest.py +6 -0
  20. agent_recorder-0.1.0/tests/fixtures/claude_sample.jsonl +6 -0
  21. agent_recorder-0.1.0/tests/fixtures/codex_sample.jsonl +7 -0
  22. agent_recorder-0.1.0/tests/test_claude_adapter.py +57 -0
  23. agent_recorder-0.1.0/tests/test_codex_adapter.py +53 -0
  24. agent_recorder-0.1.0/tests/test_db.py +63 -0
  25. agent_recorder-0.1.0/tests/test_ingester.py +47 -0
  26. agent_recorder-0.1.0/tests/test_redactor.py +24 -0
  27. agent_recorder-0.1.0/tests/test_skill_extractor.py +44 -0
  28. agent_recorder-0.1.0/tests/test_stats.py +45 -0
@@ -0,0 +1,237 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-recorder
3
+ Version: 0.1.0
4
+ Summary: Local-first CLI that records every Claude Code and Codex session into a searchable SQLite database
5
+ Project-URL: Homepage, https://github.com/AravindKurapati/agent-flight-recorder
6
+ Project-URL: Repository, https://github.com/AravindKurapati/agent-flight-recorder
7
+ Project-URL: Issues, https://github.com/AravindKurapati/agent-flight-recorder/issues
8
+ Author-email: Aravind Kurapati <arvind.kurapati@gmail.com>
9
+ License: MIT
10
+ Keywords: agents,ai,claude,cli,codex,devtools,llm,observability,session-recording
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: pydantic>=2
23
+ Requires-Dist: rapidfuzz>=3
24
+ Requires-Dist: typer[all]>=0.12
25
+ Description-Content-Type: text/markdown
26
+
27
+ # agent-flight-recorder
28
+
29
+ > **Every agent session you run disappears the moment it ends. This one doesn't.**
30
+
31
+ A local-first CLI that records every Claude Code and Codex session — prompts, tool calls, shell commands, file changes, errors, and token costs — into a searchable SQLite database. Find what you worked on, see what failed, and extract reusable workflow patterns.
32
+
33
+ ---
34
+
35
+ ## The problem this solves
36
+
37
+ AI coding sessions are opaque and ephemeral. When a session ends, all you have is changed files and a vague memory of what the agent tried. There's no way to answer:
38
+
39
+ - What tools did the agent call, and in what order?
40
+ - Which shell commands failed, and what was the error?
41
+ - How much did that session actually cost in tokens?
42
+ - Why does this same class of problem keep taking 3 sessions to fix?
43
+
44
+ `afr` records all of that and keeps it queryable — locally, permanently, without sending anything to a server.
45
+
46
+ ---
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ git clone https://github.com/AravindKurapati/agent-flight-recorder
52
+ cd agent-flight-recorder
53
+ pip install -e .
54
+ ```
55
+
56
+ **Requirements:** Python 3.11+ — no API keys, no accounts, everything stays on your machine.
57
+
58
+ ---
59
+
60
+ ## Usage
61
+
62
+ ### Ingest your sessions
63
+
64
+ ```bash
65
+ afr ingest claude # reads ~/.claude/projects/**/*.jsonl
66
+ afr ingest codex # reads ~/.codex/sessions/**/*.jsonl
67
+ ```
68
+
69
+ Or wire a Claude Code hook to ingest automatically after every session (add to `~/.claude/settings.json`):
70
+
71
+ ```json
72
+ {
73
+ "hooks": {
74
+ "Stop": [{ "matcher": "", "hooks": [{ "type": "command", "command": "afr ingest claude" }] }]
75
+ }
76
+ }
77
+ ```
78
+
79
+ ### Browse recent runs
80
+
81
+ ```bash
82
+ afr list --days 7
83
+ ```
84
+
85
+ ```
86
+ ID Source Goal Outcome Tokens↑ Date
87
+ 50c3f2a1 claude Fix the Modal deployment error untagged 1,842 2026-05-08
88
+ 903b12cd claude Add React component for dashboard untagged 311 2026-05-08
89
+ ad7e9f21 claude Debug the authentication middleware untagged 250 2026-05-07
90
+ ```
91
+
92
+ ### Inspect a session
93
+
94
+ ```bash
95
+ afr show 50c3f2a1
96
+ ```
97
+
98
+ ```
99
+ ┌─────────────────────────────────────────────────────────────────┐
100
+ │ Fix the Modal deployment error │
101
+ │ claude | 2026-05-08 10:00 → 10:14 | untagged │
102
+ └─────────────────────────────────────────────────────────────────┘
103
+
104
+ Tool Calls
105
+ ✓ Read {"file_path": "finsight.py"}
106
+ ✗ Bash {"command": "modal run finsight.py"}
107
+ ✓ Edit {"file_path": "finsight.py"}
108
+ ✓ Bash {"command": "modal deploy finsight.py"}
109
+
110
+ Shell Commands
111
+ [1] modal run finsight.py
112
+ [0] modal deploy finsight.py
113
+
114
+ Errors
115
+ • Error: missing secret huggingface-secret
116
+
117
+ Tokens: 1,842 in / 4,201 out | Cache read: 920 | Cost: $0.0000
118
+ ```
119
+
120
+ ### Search across all sessions
121
+
122
+ ```bash
123
+ afr search "authentication"
124
+ afr search "modal" --days 30
125
+ ```
126
+
127
+ ### See patterns across sessions
128
+
129
+ ```bash
130
+ afr stats --days 30
131
+ ```
132
+
133
+ ```
134
+ Total runs: 74
135
+
136
+ Outcomes
137
+ shipped: 12
138
+ blocked: 8
139
+ untagged: 54
140
+
141
+ Tokens 126,450 in / 4,091,200 out
142
+ Errors 187 tool | 62 shell
143
+
144
+ Top Tools
145
+ Bash: 891
146
+ Read: 412
147
+ Edit: 308
148
+ Write: 201
149
+ Glob: 94
150
+ mcp__exa__web_search_exa: 87
151
+ ```
152
+
153
+ ### Tag a run with its outcome
154
+
155
+ ```bash
156
+ afr tag 50c3f2a1 shipped
157
+ afr tag 903b12cd blocked
158
+ ```
159
+
160
+ Valid outcomes: `shipped`, `blocked`, `abandoned`, `exploratory`
161
+
162
+ ### Extract reusable workflow skills
163
+
164
+ After a few sessions solving the same class of problem, run:
165
+
166
+ ```bash
167
+ afr extract-skills --min-runs 3
168
+ ```
169
+
170
+ ```
171
+ Candidate: deployment-modal-debug
172
+ 4 sessions | tools: Bash, Read | errors: 6
173
+ Generate SKILL.md? [y/n]: y
174
+ Written → generated_skills/deployment-modal-debug/SKILL.md
175
+ ```
176
+
177
+ It clusters sessions by keyword similarity, finds ones that ended in `shipped`, and drafts a `SKILL.md` from the successful tool sequence. You approve before anything is written.
178
+
179
+ ---
180
+
181
+ ## All commands
182
+
183
+ | Command | What it does |
184
+ |---------|-------------|
185
+ | `afr ingest claude` | Parse `~/.claude/` sessions into the database |
186
+ | `afr ingest codex` | Parse `~/.codex/` sessions into the database |
187
+ | `afr list [--days N]` | Table of recent runs with outcome and token count |
188
+ | `afr show <id>` | Full detail: tool calls, shell commands, errors, cost |
189
+ | `afr search <query>` | Full-text search across run goals and summaries |
190
+ | `afr stats [--days N]` | Outcome distribution, top tools, error counts |
191
+ | `afr tag <id> <outcome>` | Label a run: shipped / blocked / abandoned / exploratory |
192
+ | `afr extract-skills` | Cluster sessions, propose SKILL.md candidates |
193
+
194
+ `<id>` accepts the first 8 characters from `afr list` output.
195
+
196
+ ---
197
+
198
+ ## What gets recorded
199
+
200
+ For each session:
201
+
202
+ - **Goal** — first user message
203
+ - **Tool calls** — every tool fired, input summary, success or error
204
+ - **Shell commands** — command, exit code, stdout/stderr excerpt
205
+ - **File events** — every read, write, patch, or delete
206
+ - **Errors** — failed tool calls and non-zero shell exits
207
+ - **Token counts** — input, output, cache read, cache write
208
+ - **Outcome** — you tag this: `shipped`, `blocked`, `abandoned`, `exploratory`
209
+
210
+ Secrets are redacted before anything is written to the database (API keys, bearer tokens, private keys, `.env` contents).
211
+
212
+ ---
213
+
214
+ ## How data is stored
215
+
216
+ Everything lives at `~/.afr/afr.db` — a single SQLite file on your machine. No data leaves your machine. No telemetry. No accounts.
217
+
218
+ You can query it directly with any SQLite client:
219
+
220
+ ```bash
221
+ sqlite3 ~/.afr/afr.db "SELECT user_goal, outcome, tokens_in FROM runs ORDER BY started_at DESC LIMIT 10"
222
+ ```
223
+
224
+ ---
225
+
226
+ ## Supported agents
227
+
228
+ | Agent | Source | Adapter |
229
+ |-------|--------|---------|
230
+ | Claude Code | `~/.claude/projects/**/*.jsonl` | Full — tool calls, tokens, errors |
231
+ | Codex (OpenAI) | `~/.codex/sessions/**/*.jsonl` | Full — tool name mapping, tokens, errors |
232
+
233
+ ---
234
+
235
+ ## License
236
+
237
+ MIT
@@ -0,0 +1,211 @@
1
+ # agent-flight-recorder
2
+
3
+ > **Every agent session you run disappears the moment it ends. This one doesn't.**
4
+
5
+ A local-first CLI that records every Claude Code and Codex session — prompts, tool calls, shell commands, file changes, errors, and token costs — into a searchable SQLite database. Find what you worked on, see what failed, and extract reusable workflow patterns.
6
+
7
+ ---
8
+
9
+ ## The problem this solves
10
+
11
+ AI coding sessions are opaque and ephemeral. When a session ends, all you have is changed files and a vague memory of what the agent tried. There's no way to answer:
12
+
13
+ - What tools did the agent call, and in what order?
14
+ - Which shell commands failed, and what was the error?
15
+ - How much did that session actually cost in tokens?
16
+ - Why does this same class of problem keep taking 3 sessions to fix?
17
+
18
+ `afr` records all of that and keeps it queryable — locally, permanently, without sending anything to a server.
19
+
20
+ ---
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ git clone https://github.com/AravindKurapati/agent-flight-recorder
26
+ cd agent-flight-recorder
27
+ pip install -e .
28
+ ```
29
+
30
+ **Requirements:** Python 3.11+ — no API keys, no accounts, everything stays on your machine.
31
+
32
+ ---
33
+
34
+ ## Usage
35
+
36
+ ### Ingest your sessions
37
+
38
+ ```bash
39
+ afr ingest claude # reads ~/.claude/projects/**/*.jsonl
40
+ afr ingest codex # reads ~/.codex/sessions/**/*.jsonl
41
+ ```
42
+
43
+ Or wire a Claude Code hook to ingest automatically after every session (add to `~/.claude/settings.json`):
44
+
45
+ ```json
46
+ {
47
+ "hooks": {
48
+ "Stop": [{ "matcher": "", "hooks": [{ "type": "command", "command": "afr ingest claude" }] }]
49
+ }
50
+ }
51
+ ```
52
+
53
+ ### Browse recent runs
54
+
55
+ ```bash
56
+ afr list --days 7
57
+ ```
58
+
59
+ ```
60
+ ID Source Goal Outcome Tokens↑ Date
61
+ 50c3f2a1 claude Fix the Modal deployment error untagged 1,842 2026-05-08
62
+ 903b12cd claude Add React component for dashboard untagged 311 2026-05-08
63
+ ad7e9f21 claude Debug the authentication middleware untagged 250 2026-05-07
64
+ ```
65
+
66
+ ### Inspect a session
67
+
68
+ ```bash
69
+ afr show 50c3f2a1
70
+ ```
71
+
72
+ ```
73
+ ┌─────────────────────────────────────────────────────────────────┐
74
+ │ Fix the Modal deployment error │
75
+ │ claude | 2026-05-08 10:00 → 10:14 | untagged │
76
+ └─────────────────────────────────────────────────────────────────┘
77
+
78
+ Tool Calls
79
+ ✓ Read {"file_path": "finsight.py"}
80
+ ✗ Bash {"command": "modal run finsight.py"}
81
+ ✓ Edit {"file_path": "finsight.py"}
82
+ ✓ Bash {"command": "modal deploy finsight.py"}
83
+
84
+ Shell Commands
85
+ [1] modal run finsight.py
86
+ [0] modal deploy finsight.py
87
+
88
+ Errors
89
+ • Error: missing secret huggingface-secret
90
+
91
+ Tokens: 1,842 in / 4,201 out | Cache read: 920 | Cost: $0.0000
92
+ ```
93
+
94
+ ### Search across all sessions
95
+
96
+ ```bash
97
+ afr search "authentication"
98
+ afr search "modal" --days 30
99
+ ```
100
+
101
+ ### See patterns across sessions
102
+
103
+ ```bash
104
+ afr stats --days 30
105
+ ```
106
+
107
+ ```
108
+ Total runs: 74
109
+
110
+ Outcomes
111
+ shipped: 12
112
+ blocked: 8
113
+ untagged: 54
114
+
115
+ Tokens 126,450 in / 4,091,200 out
116
+ Errors 187 tool | 62 shell
117
+
118
+ Top Tools
119
+ Bash: 891
120
+ Read: 412
121
+ Edit: 308
122
+ Write: 201
123
+ Glob: 94
124
+ mcp__exa__web_search_exa: 87
125
+ ```
126
+
127
+ ### Tag a run with its outcome
128
+
129
+ ```bash
130
+ afr tag 50c3f2a1 shipped
131
+ afr tag 903b12cd blocked
132
+ ```
133
+
134
+ Valid outcomes: `shipped`, `blocked`, `abandoned`, `exploratory`
135
+
136
+ ### Extract reusable workflow skills
137
+
138
+ After a few sessions solving the same class of problem, run:
139
+
140
+ ```bash
141
+ afr extract-skills --min-runs 3
142
+ ```
143
+
144
+ ```
145
+ Candidate: deployment-modal-debug
146
+ 4 sessions | tools: Bash, Read | errors: 6
147
+ Generate SKILL.md? [y/n]: y
148
+ Written → generated_skills/deployment-modal-debug/SKILL.md
149
+ ```
150
+
151
+ It clusters sessions by keyword similarity, finds ones that ended in `shipped`, and drafts a `SKILL.md` from the successful tool sequence. You approve before anything is written.
152
+
153
+ ---
154
+
155
+ ## All commands
156
+
157
+ | Command | What it does |
158
+ |---------|-------------|
159
+ | `afr ingest claude` | Parse `~/.claude/` sessions into the database |
160
+ | `afr ingest codex` | Parse `~/.codex/` sessions into the database |
161
+ | `afr list [--days N]` | Table of recent runs with outcome and token count |
162
+ | `afr show <id>` | Full detail: tool calls, shell commands, errors, cost |
163
+ | `afr search <query>` | Full-text search across run goals and summaries |
164
+ | `afr stats [--days N]` | Outcome distribution, top tools, error counts |
165
+ | `afr tag <id> <outcome>` | Label a run: shipped / blocked / abandoned / exploratory |
166
+ | `afr extract-skills` | Cluster sessions, propose SKILL.md candidates |
167
+
168
+ `<id>` accepts the first 8 characters from `afr list` output.
169
+
170
+ ---
171
+
172
+ ## What gets recorded
173
+
174
+ For each session:
175
+
176
+ - **Goal** — first user message
177
+ - **Tool calls** — every tool fired, input summary, success or error
178
+ - **Shell commands** — command, exit code, stdout/stderr excerpt
179
+ - **File events** — every read, write, patch, or delete
180
+ - **Errors** — failed tool calls and non-zero shell exits
181
+ - **Token counts** — input, output, cache read, cache write
182
+ - **Outcome** — you tag this: `shipped`, `blocked`, `abandoned`, `exploratory`
183
+
184
+ Secrets are redacted before anything is written to the database (API keys, bearer tokens, private keys, `.env` contents).
185
+
186
+ ---
187
+
188
+ ## How data is stored
189
+
190
+ Everything lives at `~/.afr/afr.db` — a single SQLite file on your machine. No data leaves your machine. No telemetry. No accounts.
191
+
192
+ You can query it directly with any SQLite client:
193
+
194
+ ```bash
195
+ sqlite3 ~/.afr/afr.db "SELECT user_goal, outcome, tokens_in FROM runs ORDER BY started_at DESC LIMIT 10"
196
+ ```
197
+
198
+ ---
199
+
200
+ ## Supported agents
201
+
202
+ | Agent | Source | Adapter |
203
+ |-------|--------|---------|
204
+ | Claude Code | `~/.claude/projects/**/*.jsonl` | Full — tool calls, tokens, errors |
205
+ | Codex (OpenAI) | `~/.codex/sessions/**/*.jsonl` | Full — tool name mapping, tokens, errors |
206
+
207
+ ---
208
+
209
+ ## License
210
+
211
+ MIT
File without changes
@@ -0,0 +1,139 @@
1
+ import json
2
+ import uuid
3
+ from pathlib import Path
4
+ from typing import Iterator
5
+ from ..models import ParsedSession, Run, ToolCall, ShellCommand, FileEvent, Error
6
+ from ..redactor import redact, redact_json
7
+
8
+ CLAUDE_DIR = Path.home() / ".claude" / "projects"
9
+
10
+ _FILE_TOOL_MAP = {
11
+ "Read": "read", "Write": "write", "Edit": "patch",
12
+ "MultiEdit": "patch", "Glob": "read", "Grep": "read", "Delete": "delete",
13
+ }
14
+
15
+
16
+ def parse_session_file(path: Path) -> ParsedSession:
17
+ path = Path(path)
18
+ run_id = path.stem
19
+ lines = [json.loads(l) for l in path.read_text(encoding="utf-8").splitlines() if l.strip()]
20
+
21
+ project_path = path.parent.name
22
+ user_goal = final_summary = started_at = ended_at = ""
23
+ tokens_in = tokens_out = cache_read = cache_write = 0
24
+ tool_calls: list[ToolCall] = []
25
+ shell_commands: list[ShellCommand] = []
26
+ files: list[FileEvent] = []
27
+ errors: list[Error] = []
28
+ pending: dict[str, dict] = {} # tool_use_id -> tc_data
29
+ pending_bash: dict[str, ShellCommand] = {} # tool_use_id -> ShellCommand
30
+
31
+ for line in lines:
32
+ ts = line.get("timestamp", "")
33
+ if not started_at and ts:
34
+ started_at = ts
35
+ if ts:
36
+ ended_at = ts
37
+
38
+ msg_type = line.get("type", "")
39
+ message = line.get("message", {})
40
+ content = message.get("content", [])
41
+ if not isinstance(content, list):
42
+ content = []
43
+
44
+ if msg_type == "user":
45
+ for block in content:
46
+ if not isinstance(block, dict):
47
+ continue
48
+ if block.get("type") == "text" and not user_goal:
49
+ user_goal = redact(block.get("text", "")[:500])
50
+ elif block.get("type") == "tool_result":
51
+ tu_id = block.get("tool_use_id", "")
52
+ is_error = block.get("is_error", False)
53
+ raw_output = block.get("content", "")
54
+ if isinstance(raw_output, list):
55
+ raw_output = " ".join(b.get("text", "") for b in raw_output if isinstance(b, dict))
56
+ output = redact(str(raw_output)[:500])
57
+
58
+ if tu_id in pending:
59
+ tc_data = pending.pop(tu_id)
60
+ tc_data["output_summary"] = output
61
+ tc_data["status"] = "error" if is_error else "success"
62
+ tool_calls.append(ToolCall(**tc_data))
63
+
64
+ if tu_id in pending_bash:
65
+ sc = pending_bash.pop(tu_id)
66
+ sc.exit_code = 1 if is_error else 0
67
+ if is_error:
68
+ sc.stderr_excerpt = output
69
+ else:
70
+ sc.stdout_excerpt = output
71
+ shell_commands.append(sc)
72
+
73
+ if is_error:
74
+ errors.append(Error(
75
+ id=str(uuid.uuid4()), run_id=run_id, source="tool",
76
+ message=output[:300], raw_json=redact_json(block), timestamp=ts,
77
+ ))
78
+
79
+ elif msg_type == "assistant":
80
+ usage = message.get("usage", {})
81
+ tokens_in += usage.get("input_tokens", 0)
82
+ tokens_out += usage.get("output_tokens", 0)
83
+ cache_read += usage.get("cache_read_input_tokens", 0)
84
+ cache_write += usage.get("cache_creation_input_tokens", 0)
85
+
86
+ for block in content:
87
+ if not isinstance(block, dict):
88
+ continue
89
+ if block.get("type") == "text":
90
+ txt = block.get("text", "").strip()
91
+ if txt:
92
+ final_summary = redact(txt[:500])
93
+ elif block.get("type") == "tool_use":
94
+ tu_id = block.get("id", str(uuid.uuid4()))
95
+ tool_name = block.get("name", "Unknown")
96
+ inp = block.get("input", {})
97
+ tc_data = {
98
+ "id": str(uuid.uuid4()), "run_id": run_id, "tool_name": tool_name,
99
+ "input_summary": redact(json.dumps(inp)[:300]),
100
+ "output_summary": "", "status": "success",
101
+ "duration_ms": None, "timestamp": ts, "raw_json": redact_json(block),
102
+ }
103
+ pending[tu_id] = tc_data
104
+
105
+ if tool_name == "Bash":
106
+ pending_bash[tu_id] = ShellCommand(
107
+ id=str(uuid.uuid4()), run_id=run_id,
108
+ command=redact(inp.get("command", "")[:500]), timestamp=ts,
109
+ )
110
+
111
+ if tool_name in _FILE_TOOL_MAP:
112
+ fpath = inp.get("file_path") or inp.get("path") or inp.get("pattern") or ""
113
+ if fpath:
114
+ files.append(FileEvent(
115
+ id=str(uuid.uuid4()), run_id=run_id,
116
+ path=str(fpath), action=_FILE_TOOL_MAP[tool_name],
117
+ ))
118
+
119
+ for tu_id, tc_data in pending.items():
120
+ tool_calls.append(ToolCall(**tc_data))
121
+ if tu_id in pending_bash:
122
+ shell_commands.append(pending_bash[tu_id])
123
+
124
+ return ParsedSession(
125
+ run=Run(id=run_id, source="claude", project_path=project_path,
126
+ started_at=started_at, ended_at=ended_at, user_goal=user_goal,
127
+ final_summary=final_summary, tokens_in=tokens_in, tokens_out=tokens_out,
128
+ cache_read=cache_read, cache_write=cache_write),
129
+ tool_calls=tool_calls, shell_commands=shell_commands, files=files, errors=errors,
130
+ )
131
+
132
+
133
+ def iter_sessions(claude_dir: Path = CLAUDE_DIR) -> Iterator[Path]:
134
+ claude_dir = Path(claude_dir)
135
+ if not claude_dir.exists():
136
+ return
137
+ for project_dir in claude_dir.iterdir():
138
+ if project_dir.is_dir():
139
+ yield from project_dir.glob("*.jsonl")