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.
- agent_recorder-0.1.0/PKG-INFO +237 -0
- agent_recorder-0.1.0/README.md +211 -0
- agent_recorder-0.1.0/agent_flight_recorder/__init__.py +0 -0
- agent_recorder-0.1.0/agent_flight_recorder/adapters/__init__.py +0 -0
- agent_recorder-0.1.0/agent_flight_recorder/adapters/claude.py +139 -0
- agent_recorder-0.1.0/agent_flight_recorder/adapters/codex.py +139 -0
- agent_recorder-0.1.0/agent_flight_recorder/analyzers/__init__.py +0 -0
- agent_recorder-0.1.0/agent_flight_recorder/analyzers/skill_extractor.py +120 -0
- agent_recorder-0.1.0/agent_flight_recorder/analyzers/stats.py +45 -0
- agent_recorder-0.1.0/agent_flight_recorder/cli.py +117 -0
- agent_recorder-0.1.0/agent_flight_recorder/db.py +163 -0
- agent_recorder-0.1.0/agent_flight_recorder/ingester.py +42 -0
- agent_recorder-0.1.0/agent_flight_recorder/models.py +65 -0
- agent_recorder-0.1.0/agent_flight_recorder/redactor.py +20 -0
- agent_recorder-0.1.0/agent_flight_recorder/render/__init__.py +0 -0
- agent_recorder-0.1.0/agent_flight_recorder/render/terminal.py +88 -0
- agent_recorder-0.1.0/pyproject.toml +47 -0
- agent_recorder-0.1.0/tests/__init__.py +0 -0
- agent_recorder-0.1.0/tests/conftest.py +6 -0
- agent_recorder-0.1.0/tests/fixtures/claude_sample.jsonl +6 -0
- agent_recorder-0.1.0/tests/fixtures/codex_sample.jsonl +7 -0
- agent_recorder-0.1.0/tests/test_claude_adapter.py +57 -0
- agent_recorder-0.1.0/tests/test_codex_adapter.py +53 -0
- agent_recorder-0.1.0/tests/test_db.py +63 -0
- agent_recorder-0.1.0/tests/test_ingester.py +47 -0
- agent_recorder-0.1.0/tests/test_redactor.py +24 -0
- agent_recorder-0.1.0/tests/test_skill_extractor.py +44 -0
- 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
|
|
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")
|