atif-make 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.
- atif_make-0.1.0/.github/workflows/publish.yml +35 -0
- atif_make-0.1.0/.github/workflows/test.yml +28 -0
- atif_make-0.1.0/.gitignore +7 -0
- atif_make-0.1.0/PKG-INFO +206 -0
- atif_make-0.1.0/README.md +196 -0
- atif_make-0.1.0/pyproject.toml +32 -0
- atif_make-0.1.0/src/atif_make/__init__.py +15 -0
- atif_make-0.1.0/src/atif_make/archive.py +109 -0
- atif_make-0.1.0/src/atif_make/atif.py +353 -0
- atif_make-0.1.0/src/atif_make/cli.py +279 -0
- atif_make-0.1.0/src/atif_make/convert.py +43 -0
- atif_make-0.1.0/src/atif_make/corpus.py +149 -0
- atif_make-0.1.0/src/atif_make/detect.py +126 -0
- atif_make-0.1.0/src/atif_make/parsers/__init__.py +0 -0
- atif_make-0.1.0/src/atif_make/parsers/atif_json.py +60 -0
- atif_make-0.1.0/src/atif_make/parsers/claude_code.py +575 -0
- atif_make-0.1.0/src/atif_make/parsers/codex.py +385 -0
- atif_make-0.1.0/src/atif_make/parsers/copilot.py +130 -0
- atif_make-0.1.0/src/atif_make/parsers/har.py +403 -0
- atif_make-0.1.0/tests/conftest.py +13 -0
- atif_make-0.1.0/tests/fixtures/claude-code-stream.jsonl +15 -0
- atif_make-0.1.0/tests/fixtures/claude-code-transcript/subagents/agent-abc123.jsonl +4 -0
- atif_make-0.1.0/tests/fixtures/claude-code-transcript/subagents/agent-abc123.meta.json +1 -0
- atif_make-0.1.0/tests/fixtures/claude-code-transcript.jsonl +8 -0
- atif_make-0.1.0/tests/fixtures/codex-exec.jsonl +5 -0
- atif_make-0.1.0/tests/fixtures/codex-rollout.jsonl +8 -0
- atif_make-0.1.0/tests/fixtures/copilot-cli.jsonl +14 -0
- atif_make-0.1.0/tests/fixtures/har-anthropic-multiturn.har +47 -0
- atif_make-0.1.0/tests/fixtures/har-anthropic-stream.har +29 -0
- atif_make-0.1.0/tests/fixtures/har-anthropic-whole.har +29 -0
- atif_make-0.1.0/tests/fixtures/har-openai-chat.har +29 -0
- atif_make-0.1.0/tests/fixtures/har-openai-responses.har +29 -0
- atif_make-0.1.0/tests/test_archive.py +120 -0
- atif_make-0.1.0/tests/test_claude_code.py +80 -0
- atif_make-0.1.0/tests/test_codex.py +56 -0
- atif_make-0.1.0/tests/test_copilot.py +31 -0
- atif_make-0.1.0/tests/test_detect.py +54 -0
- atif_make-0.1.0/tests/test_har.py +53 -0
- atif_make-0.1.0/tests/test_multimodal.py +147 -0
- atif_make-0.1.0/tests/test_spec_compliance.py +84 -0
- atif_make-0.1.0/uv.lock +2204 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI when a version tag is pushed:
|
|
4
|
+
#
|
|
5
|
+
# git tag v0.1.0 && git push origin v0.1.0
|
|
6
|
+
#
|
|
7
|
+
# Authentication uses PyPI Trusted Publishing (OIDC), so no API token is stored
|
|
8
|
+
# anywhere. GitHub mints a short-lived identity for this workflow and PyPI
|
|
9
|
+
# verifies it against the publisher configured for the project.
|
|
10
|
+
on:
|
|
11
|
+
push:
|
|
12
|
+
tags: ["v*"]
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
publish:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
environment: pypi
|
|
18
|
+
permissions:
|
|
19
|
+
# Required for trusted publishing; nothing else needs write access.
|
|
20
|
+
id-token: write
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
- uses: astral-sh/setup-uv@v5
|
|
24
|
+
|
|
25
|
+
- name: Refuse to publish a tag that disagrees with the version
|
|
26
|
+
run: |
|
|
27
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
28
|
+
pkg=$(uv version --short)
|
|
29
|
+
if [ "$tag" != "$pkg" ]; then
|
|
30
|
+
echo "tag $tag does not match project version $pkg" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
- run: uv build
|
|
35
|
+
- run: uv publish --trusted-publishing always
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.12", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v5
|
|
18
|
+
with:
|
|
19
|
+
enable-cache: true
|
|
20
|
+
- run: uv python install ${{ matrix.python-version }}
|
|
21
|
+
- name: Run tests
|
|
22
|
+
run: uv run --python ${{ matrix.python-version }} pytest -q
|
|
23
|
+
|
|
24
|
+
# The spec suite validates output against harbor's reference ATIF models.
|
|
25
|
+
# It pulls a large dependency tree, so it runs on one version only.
|
|
26
|
+
- name: Validate against the ATIF reference models
|
|
27
|
+
if: matrix.python-version == '3.12'
|
|
28
|
+
run: uv run --python 3.12 --extra spec pytest -q tests/test_spec_compliance.py
|
atif_make-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: atif-make
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convert agent logs (Claude Code, Codex, Copilot, HAR) into ATIF v1.7 trajectories.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Provides-Extra: spec
|
|
8
|
+
Requires-Dist: harbor>=0.21.0; extra == 'spec'
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# atif-make
|
|
12
|
+
|
|
13
|
+
Make [ATIF v1.7](https://github.com/harbor-framework/harbor/blob/main/rfcs/0001-trajectory-format.md)
|
|
14
|
+
trajectories from agent logs — Claude Code, Codex, Copilot CLI, and HAR captures.
|
|
15
|
+
|
|
16
|
+
Zero runtime dependencies. Python 3.12+.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
uv tool install atif-make # puts `atif-make` on your PATH
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or as a library in a project:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
uv add atif-make
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
For a browser view of what you convert, see the companion
|
|
31
|
+
[`atif-view`](https://github.com/jammastergirish/atif-view), which depends on this package.
|
|
32
|
+
|
|
33
|
+
`uv tool install` builds an isolated environment, so nothing lands in your
|
|
34
|
+
project or system Python. To follow local edits instead, use
|
|
35
|
+
`uv tool install --editable .`; to remove it, `uv tool uninstall atif-make`.
|
|
36
|
+
|
|
37
|
+
Running from a checkout without installing works too: `uv run atif-make ...`.
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
atif-make ~/.claude/projects/my-project/session.jsonl # convert one log
|
|
41
|
+
atif-make index --stats # what do I have, across agents?
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Commands
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
atif-make <file> convert (shorthand for `atif-make convert`)
|
|
48
|
+
atif-make convert <file> convert one log
|
|
49
|
+
atif-make convert <dir|archive> convert every log inside
|
|
50
|
+
atif-make index [roots...] scan for sessions across agents
|
|
51
|
+
atif-make formats list supported input formats
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Flag | Command | Meaning |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `-o, --output` | convert | output path (default `<input>.trajectory.json`) |
|
|
57
|
+
| `-f, --format` | convert | force the input format instead of detecting it |
|
|
58
|
+
| `--json` | convert | write one self-contained document to stdout |
|
|
59
|
+
| `--bundle OUT.zip` | convert | zip the trajectory with its images and subagents |
|
|
60
|
+
| `--split-subagents` | convert | write subagents as sibling files, not embedded |
|
|
61
|
+
| `--indent N` | convert | JSON indent (default 2) |
|
|
62
|
+
| `-q, --quiet` | convert | suppress progress output |
|
|
63
|
+
| `--stats` | index | print a per-agent summary |
|
|
64
|
+
| `--add` | index | merge into the existing index instead of replacing it |
|
|
65
|
+
|
|
66
|
+
## Supported inputs
|
|
67
|
+
|
|
68
|
+
Most agents write *two* unrelated log shapes — what the CLI streams, and what it
|
|
69
|
+
persists on disk — and they are not interchangeable. atif-make reads both.
|
|
70
|
+
|
|
71
|
+
| Format | Source |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `claude-code-transcript` | `~/.claude/projects/<project>/<session>.jsonl` |
|
|
74
|
+
| `claude-code-stream` | `claude -p --output-format stream-json` |
|
|
75
|
+
| `codex-rollout` | `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` |
|
|
76
|
+
| `codex-exec` | `codex exec --json` |
|
|
77
|
+
| `copilot-cli` | Copilot CLI session logs |
|
|
78
|
+
| `har` | Anthropic Messages, OpenAI Chat Completions, OpenAI Responses |
|
|
79
|
+
| `atif` | An ATIF trajectory that is already converted |
|
|
80
|
+
|
|
81
|
+
Directories, `.zip` and `.tar.gz` (also `.tgz`, `.tar.bz2`, `.tar.xz`) are read
|
|
82
|
+
as containers: every log inside is found and converted. That closes the loop on
|
|
83
|
+
`--bundle` — the zip atif-make hands you to send someone opens again in atif-make,
|
|
84
|
+
images and all.
|
|
85
|
+
|
|
86
|
+
Archives are extracted to a temporary directory, once per run. Members naming
|
|
87
|
+
absolute paths or climbing out with `..` are refused rather than quietly
|
|
88
|
+
sanitised, and an archive that expands past 8 GB or 20,000 entries is rejected
|
|
89
|
+
outright.
|
|
90
|
+
|
|
91
|
+
Format is detected from content, never from the extension. `atif` exists so a
|
|
92
|
+
trajectory someone sends you opens like anything else — it is loaded, not
|
|
93
|
+
reparsed, and unknown fields from a newer ATIF minor version are dropped rather
|
|
94
|
+
than rejected.
|
|
95
|
+
|
|
96
|
+
## What it gets right
|
|
97
|
+
|
|
98
|
+
These are the things that are easy to get wrong, and that silently corrupt a
|
|
99
|
+
trajectory when you do:
|
|
100
|
+
|
|
101
|
+
**Split messages.** Claude Code writes one API response as several JSONL lines
|
|
102
|
+
that share a `message.id` — thinking, text, and each parallel `tool_use` arrive
|
|
103
|
+
separately, with the *same* `usage` object repeated on every line. Treating those
|
|
104
|
+
as separate turns inflates step counts and multiplies token totals. atif-make
|
|
105
|
+
coalesces them and counts usage once.
|
|
106
|
+
|
|
107
|
+
**Out-of-order tool results.** Parallel calls come back interleaved, and a slow
|
|
108
|
+
call can return several turns after it was issued. Pairing results to calls by
|
|
109
|
+
*position* drops some and misattributes others. atif-make pairs by `tool_use_id`.
|
|
110
|
+
|
|
111
|
+
**Byte-capped detection.** A JSONL preamble (hook events, rate-limit notices) can
|
|
112
|
+
push the identifying line kilobytes into a file. atif-make scans whole lines.
|
|
113
|
+
|
|
114
|
+
**Subagent structure.** Claude Code links a delegated agent through a `.meta.json`
|
|
115
|
+
sidecar (`toolUseId`) and an `agentId` field on the result line — not through
|
|
116
|
+
anything in the result *text*. atif-make links by call id, so refs actually resolve
|
|
117
|
+
instead of leaving orphaned subagents.
|
|
118
|
+
|
|
119
|
+
**Images.** Codex embeds screenshots as base64 data URLs and Claude Code as
|
|
120
|
+
base64 content blocks — 24 of the sessions on one test machine carried them, and
|
|
121
|
+
a single Codex session held 65 images totalling 14 MB. Dropping them loses the
|
|
122
|
+
thing the agent was actually looking at, and inlining them makes an unreadable
|
|
123
|
+
document. atif-make writes them to `images/` and references them by relative path,
|
|
124
|
+
which is what the spec asks for.
|
|
125
|
+
|
|
126
|
+
**Malformed timestamps.** ATIF requires ISO 8601. A truncated or hand-edited log
|
|
127
|
+
can carry something else, and passing it through would make the whole trajectory
|
|
128
|
+
fail validation, so an unparseable timestamp is dropped rather than emitted.
|
|
129
|
+
|
|
130
|
+
**HAR tool results.** In a HAR capture a tool's output is not in the response that
|
|
131
|
+
called it — it appears in the *next* request's message history. atif-make harvests
|
|
132
|
+
results across entries and pairs them back by id, while emitting the shared
|
|
133
|
+
conversation prefix only once.
|
|
134
|
+
|
|
135
|
+
## Output — a trajectory is a directory, not a file
|
|
136
|
+
|
|
137
|
+
ATIF references images and split subagents by path *relative to the trajectory
|
|
138
|
+
file*, so anything with attachments is inherently multi-file:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
session.trajectory.json the document
|
|
142
|
+
session.trajectory.<agent-id>.json subagents, with --split-subagents
|
|
143
|
+
images/<sha>.png images, referenced as "images/<sha>.png"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Images are de-duplicated by content hash, so the same screenshot pasted five
|
|
147
|
+
times is stored once.
|
|
148
|
+
|
|
149
|
+
Three ways out, depending on where it's going:
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
atif-make session.jsonl -o out/t.json # directory form: t.json + images/
|
|
153
|
+
atif-make session.jsonl --json # one self-contained doc; images inlined as data: URIs
|
|
154
|
+
atif-make session.jsonl --bundle send.zip # zip of the whole directory — for sending someone
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`--json` is the exception that proves the rule: stdout has no directory to put
|
|
158
|
+
siblings in, so images become data URIs to keep the document standalone.
|
|
159
|
+
|
|
160
|
+
Output carries `timestamp`, `reasoning_content`, per-step `metrics`, multimodal
|
|
161
|
+
`ContentPart` message content, and subagents either embedded
|
|
162
|
+
(`subagent_trajectories`) or split into sibling files with resolvable
|
|
163
|
+
`trajectory_path` refs.
|
|
164
|
+
|
|
165
|
+
## Viewing
|
|
166
|
+
|
|
167
|
+
Conversion is all this package does. To browse trajectories in a browser,
|
|
168
|
+
install the companion [`atif-view`](https://github.com/jammastergirish/atif-view), which depends on this
|
|
169
|
+
package and reads the index below.
|
|
170
|
+
|
|
171
|
+
## Where sessions come from
|
|
172
|
+
|
|
173
|
+
`atif-make index` scans your own machine — nothing is uploaded, and no network call
|
|
174
|
+
is ever made. By default it looks in:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
~/.claude/projects/ Claude Code sessions
|
|
178
|
+
~/.codex/sessions/ Codex sessions
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The result is cached at `~/.atif-make/index.json`. Indexing reads only each file's
|
|
182
|
+
leading lines, so a 143 MB rollout costs the same as a 4 KB one; full conversion
|
|
183
|
+
happens lazily, when you actually open a session.
|
|
184
|
+
|
|
185
|
+
Scan somewhere else, or add a single file someone sent you:
|
|
186
|
+
|
|
187
|
+
```sh
|
|
188
|
+
atif-make index ~/work/logs # replace the index with this scan
|
|
189
|
+
atif-make index --add received.trajectory.json # merge one file into the existing index
|
|
190
|
+
atif-make index --add received-bundle.zip # or a whole bundle
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`--add` merges; without it, `index` replaces. Both raw logs and already-converted
|
|
194
|
+
ATIF trajectories can be added.
|
|
195
|
+
|
|
196
|
+
## Tests
|
|
197
|
+
|
|
198
|
+
```sh
|
|
199
|
+
uv run pytest # unit tests, all synthetic fixtures
|
|
200
|
+
uv sync --extra spec # pulls harbor (large)
|
|
201
|
+
uv run --extra spec pytest # + validate against the reference ATIF models
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The `spec` suite validates every fixture against harbor's own pydantic models —
|
|
205
|
+
ground truth for whether the output is really ATIF, rather than what atif-make
|
|
206
|
+
believes ATIF to be.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# atif-make
|
|
2
|
+
|
|
3
|
+
Make [ATIF v1.7](https://github.com/harbor-framework/harbor/blob/main/rfcs/0001-trajectory-format.md)
|
|
4
|
+
trajectories from agent logs — Claude Code, Codex, Copilot CLI, and HAR captures.
|
|
5
|
+
|
|
6
|
+
Zero runtime dependencies. Python 3.12+.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
uv tool install atif-make # puts `atif-make` on your PATH
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or as a library in a project:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
uv add atif-make
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
For a browser view of what you convert, see the companion
|
|
21
|
+
[`atif-view`](https://github.com/jammastergirish/atif-view), which depends on this package.
|
|
22
|
+
|
|
23
|
+
`uv tool install` builds an isolated environment, so nothing lands in your
|
|
24
|
+
project or system Python. To follow local edits instead, use
|
|
25
|
+
`uv tool install --editable .`; to remove it, `uv tool uninstall atif-make`.
|
|
26
|
+
|
|
27
|
+
Running from a checkout without installing works too: `uv run atif-make ...`.
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
atif-make ~/.claude/projects/my-project/session.jsonl # convert one log
|
|
31
|
+
atif-make index --stats # what do I have, across agents?
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
atif-make <file> convert (shorthand for `atif-make convert`)
|
|
38
|
+
atif-make convert <file> convert one log
|
|
39
|
+
atif-make convert <dir|archive> convert every log inside
|
|
40
|
+
atif-make index [roots...] scan for sessions across agents
|
|
41
|
+
atif-make formats list supported input formats
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
| Flag | Command | Meaning |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `-o, --output` | convert | output path (default `<input>.trajectory.json`) |
|
|
47
|
+
| `-f, --format` | convert | force the input format instead of detecting it |
|
|
48
|
+
| `--json` | convert | write one self-contained document to stdout |
|
|
49
|
+
| `--bundle OUT.zip` | convert | zip the trajectory with its images and subagents |
|
|
50
|
+
| `--split-subagents` | convert | write subagents as sibling files, not embedded |
|
|
51
|
+
| `--indent N` | convert | JSON indent (default 2) |
|
|
52
|
+
| `-q, --quiet` | convert | suppress progress output |
|
|
53
|
+
| `--stats` | index | print a per-agent summary |
|
|
54
|
+
| `--add` | index | merge into the existing index instead of replacing it |
|
|
55
|
+
|
|
56
|
+
## Supported inputs
|
|
57
|
+
|
|
58
|
+
Most agents write *two* unrelated log shapes — what the CLI streams, and what it
|
|
59
|
+
persists on disk — and they are not interchangeable. atif-make reads both.
|
|
60
|
+
|
|
61
|
+
| Format | Source |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `claude-code-transcript` | `~/.claude/projects/<project>/<session>.jsonl` |
|
|
64
|
+
| `claude-code-stream` | `claude -p --output-format stream-json` |
|
|
65
|
+
| `codex-rollout` | `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` |
|
|
66
|
+
| `codex-exec` | `codex exec --json` |
|
|
67
|
+
| `copilot-cli` | Copilot CLI session logs |
|
|
68
|
+
| `har` | Anthropic Messages, OpenAI Chat Completions, OpenAI Responses |
|
|
69
|
+
| `atif` | An ATIF trajectory that is already converted |
|
|
70
|
+
|
|
71
|
+
Directories, `.zip` and `.tar.gz` (also `.tgz`, `.tar.bz2`, `.tar.xz`) are read
|
|
72
|
+
as containers: every log inside is found and converted. That closes the loop on
|
|
73
|
+
`--bundle` — the zip atif-make hands you to send someone opens again in atif-make,
|
|
74
|
+
images and all.
|
|
75
|
+
|
|
76
|
+
Archives are extracted to a temporary directory, once per run. Members naming
|
|
77
|
+
absolute paths or climbing out with `..` are refused rather than quietly
|
|
78
|
+
sanitised, and an archive that expands past 8 GB or 20,000 entries is rejected
|
|
79
|
+
outright.
|
|
80
|
+
|
|
81
|
+
Format is detected from content, never from the extension. `atif` exists so a
|
|
82
|
+
trajectory someone sends you opens like anything else — it is loaded, not
|
|
83
|
+
reparsed, and unknown fields from a newer ATIF minor version are dropped rather
|
|
84
|
+
than rejected.
|
|
85
|
+
|
|
86
|
+
## What it gets right
|
|
87
|
+
|
|
88
|
+
These are the things that are easy to get wrong, and that silently corrupt a
|
|
89
|
+
trajectory when you do:
|
|
90
|
+
|
|
91
|
+
**Split messages.** Claude Code writes one API response as several JSONL lines
|
|
92
|
+
that share a `message.id` — thinking, text, and each parallel `tool_use` arrive
|
|
93
|
+
separately, with the *same* `usage` object repeated on every line. Treating those
|
|
94
|
+
as separate turns inflates step counts and multiplies token totals. atif-make
|
|
95
|
+
coalesces them and counts usage once.
|
|
96
|
+
|
|
97
|
+
**Out-of-order tool results.** Parallel calls come back interleaved, and a slow
|
|
98
|
+
call can return several turns after it was issued. Pairing results to calls by
|
|
99
|
+
*position* drops some and misattributes others. atif-make pairs by `tool_use_id`.
|
|
100
|
+
|
|
101
|
+
**Byte-capped detection.** A JSONL preamble (hook events, rate-limit notices) can
|
|
102
|
+
push the identifying line kilobytes into a file. atif-make scans whole lines.
|
|
103
|
+
|
|
104
|
+
**Subagent structure.** Claude Code links a delegated agent through a `.meta.json`
|
|
105
|
+
sidecar (`toolUseId`) and an `agentId` field on the result line — not through
|
|
106
|
+
anything in the result *text*. atif-make links by call id, so refs actually resolve
|
|
107
|
+
instead of leaving orphaned subagents.
|
|
108
|
+
|
|
109
|
+
**Images.** Codex embeds screenshots as base64 data URLs and Claude Code as
|
|
110
|
+
base64 content blocks — 24 of the sessions on one test machine carried them, and
|
|
111
|
+
a single Codex session held 65 images totalling 14 MB. Dropping them loses the
|
|
112
|
+
thing the agent was actually looking at, and inlining them makes an unreadable
|
|
113
|
+
document. atif-make writes them to `images/` and references them by relative path,
|
|
114
|
+
which is what the spec asks for.
|
|
115
|
+
|
|
116
|
+
**Malformed timestamps.** ATIF requires ISO 8601. A truncated or hand-edited log
|
|
117
|
+
can carry something else, and passing it through would make the whole trajectory
|
|
118
|
+
fail validation, so an unparseable timestamp is dropped rather than emitted.
|
|
119
|
+
|
|
120
|
+
**HAR tool results.** In a HAR capture a tool's output is not in the response that
|
|
121
|
+
called it — it appears in the *next* request's message history. atif-make harvests
|
|
122
|
+
results across entries and pairs them back by id, while emitting the shared
|
|
123
|
+
conversation prefix only once.
|
|
124
|
+
|
|
125
|
+
## Output — a trajectory is a directory, not a file
|
|
126
|
+
|
|
127
|
+
ATIF references images and split subagents by path *relative to the trajectory
|
|
128
|
+
file*, so anything with attachments is inherently multi-file:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
session.trajectory.json the document
|
|
132
|
+
session.trajectory.<agent-id>.json subagents, with --split-subagents
|
|
133
|
+
images/<sha>.png images, referenced as "images/<sha>.png"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Images are de-duplicated by content hash, so the same screenshot pasted five
|
|
137
|
+
times is stored once.
|
|
138
|
+
|
|
139
|
+
Three ways out, depending on where it's going:
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
atif-make session.jsonl -o out/t.json # directory form: t.json + images/
|
|
143
|
+
atif-make session.jsonl --json # one self-contained doc; images inlined as data: URIs
|
|
144
|
+
atif-make session.jsonl --bundle send.zip # zip of the whole directory — for sending someone
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`--json` is the exception that proves the rule: stdout has no directory to put
|
|
148
|
+
siblings in, so images become data URIs to keep the document standalone.
|
|
149
|
+
|
|
150
|
+
Output carries `timestamp`, `reasoning_content`, per-step `metrics`, multimodal
|
|
151
|
+
`ContentPart` message content, and subagents either embedded
|
|
152
|
+
(`subagent_trajectories`) or split into sibling files with resolvable
|
|
153
|
+
`trajectory_path` refs.
|
|
154
|
+
|
|
155
|
+
## Viewing
|
|
156
|
+
|
|
157
|
+
Conversion is all this package does. To browse trajectories in a browser,
|
|
158
|
+
install the companion [`atif-view`](https://github.com/jammastergirish/atif-view), which depends on this
|
|
159
|
+
package and reads the index below.
|
|
160
|
+
|
|
161
|
+
## Where sessions come from
|
|
162
|
+
|
|
163
|
+
`atif-make index` scans your own machine — nothing is uploaded, and no network call
|
|
164
|
+
is ever made. By default it looks in:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
~/.claude/projects/ Claude Code sessions
|
|
168
|
+
~/.codex/sessions/ Codex sessions
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The result is cached at `~/.atif-make/index.json`. Indexing reads only each file's
|
|
172
|
+
leading lines, so a 143 MB rollout costs the same as a 4 KB one; full conversion
|
|
173
|
+
happens lazily, when you actually open a session.
|
|
174
|
+
|
|
175
|
+
Scan somewhere else, or add a single file someone sent you:
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
atif-make index ~/work/logs # replace the index with this scan
|
|
179
|
+
atif-make index --add received.trajectory.json # merge one file into the existing index
|
|
180
|
+
atif-make index --add received-bundle.zip # or a whole bundle
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`--add` merges; without it, `index` replaces. Both raw logs and already-converted
|
|
184
|
+
ATIF trajectories can be added.
|
|
185
|
+
|
|
186
|
+
## Tests
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
uv run pytest # unit tests, all synthetic fixtures
|
|
190
|
+
uv sync --extra spec # pulls harbor (large)
|
|
191
|
+
uv run --extra spec pytest # + validate against the reference ATIF models
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The `spec` suite validates every fixture against harbor's own pydantic models —
|
|
195
|
+
ground truth for whether the output is really ATIF, rather than what atif-make
|
|
196
|
+
believes ATIF to be.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "atif-make"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Convert agent logs (Claude Code, Codex, Copilot, HAR) into ATIF v1.7 trajectories."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
# Deliberately zero runtime dependencies: this runs against local session logs
|
|
9
|
+
# and should stay trivially installable anywhere.
|
|
10
|
+
dependencies = []
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
atif-make = "atif_make.cli:main"
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
# `harbor` carries the reference ATIF pydantic models. It pulls a large tree
|
|
17
|
+
# (litellm, fastapi, supabase), so it is kept out of the default test path and
|
|
18
|
+
# used only by the spec-compliance suite.
|
|
19
|
+
spec = ["harbor>=0.21.0"]
|
|
20
|
+
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
dev = ["pytest>=8.0"]
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["hatchling"]
|
|
26
|
+
build-backend = "hatchling.build"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/atif_make"]
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""atif-make — convert agent logs into ATIF v1.7 trajectories."""
|
|
2
|
+
|
|
3
|
+
from .atif import SCHEMA_VERSION, Trajectory
|
|
4
|
+
from .convert import AGENTS, PARSERS, convert
|
|
5
|
+
from .detect import FORMATS, detect_format
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"SCHEMA_VERSION",
|
|
9
|
+
"Trajectory",
|
|
10
|
+
"convert",
|
|
11
|
+
"detect_format",
|
|
12
|
+
"FORMATS",
|
|
13
|
+
"PARSERS",
|
|
14
|
+
"AGENTS",
|
|
15
|
+
]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Reading logs out of archives and directories.
|
|
2
|
+
|
|
3
|
+
mkatif produces zip bundles, so it has to be able to open them again — and a
|
|
4
|
+
colleague is as likely to send a `.tar.gz` of a session directory as a bare
|
|
5
|
+
file. Archives are extracted to a temporary directory once per process and
|
|
6
|
+
reused, since a viewer may open several trajectories from the same bundle.
|
|
7
|
+
|
|
8
|
+
Extraction is the dangerous part of this file. Archive members can name absolute
|
|
9
|
+
paths or climb out with `..` (the "zip slip" / CVE-2007-4559 family), and a
|
|
10
|
+
small archive can expand to fill a disk. Both are refused rather than sanitised
|
|
11
|
+
quietly, so a malformed bundle fails loudly instead of writing somewhere odd.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import shutil
|
|
17
|
+
import tarfile
|
|
18
|
+
import tempfile
|
|
19
|
+
import zipfile
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
# A session log can legitimately be very large — a 143 MB rollout is real — so
|
|
23
|
+
# the ceiling is high enough for a genuine bundle and low enough to stop a bomb.
|
|
24
|
+
MAX_TOTAL_BYTES = 8 * 1024 * 1024 * 1024
|
|
25
|
+
MAX_MEMBERS = 20_000
|
|
26
|
+
|
|
27
|
+
TAR_SUFFIXES = (".tar", ".tar.gz", ".tgz", ".tar.bz2", ".tbz2", ".tar.xz", ".txz")
|
|
28
|
+
|
|
29
|
+
_cache: dict[Path, Path] = {}
|
|
30
|
+
_temp_dirs: list[tempfile.TemporaryDirectory] = []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_archive(path: Path) -> bool:
|
|
34
|
+
name = path.name.lower()
|
|
35
|
+
if name.endswith(".zip"):
|
|
36
|
+
return zipfile.is_zipfile(path)
|
|
37
|
+
if any(name.endswith(s) for s in TAR_SUFFIXES):
|
|
38
|
+
return tarfile.is_tarfile(path)
|
|
39
|
+
# Fall back to content sniffing for archives with an unhelpful name.
|
|
40
|
+
try:
|
|
41
|
+
if zipfile.is_zipfile(path):
|
|
42
|
+
return True
|
|
43
|
+
return tarfile.is_tarfile(path)
|
|
44
|
+
except (OSError, tarfile.TarError):
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _safe_target(root: Path, member: str) -> Path:
|
|
49
|
+
"""Resolve an archive member under root, refusing anything that escapes."""
|
|
50
|
+
if member.startswith("/") or ".." in Path(member).parts:
|
|
51
|
+
raise ValueError(f"unsafe path in archive: {member}")
|
|
52
|
+
target = (root / member).resolve()
|
|
53
|
+
if not str(target).startswith(str(root.resolve())):
|
|
54
|
+
raise ValueError(f"archive member escapes destination: {member}")
|
|
55
|
+
return target
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def extract(path: Path) -> Path:
|
|
59
|
+
"""Extract an archive to a temporary directory and return its root."""
|
|
60
|
+
resolved = path.resolve()
|
|
61
|
+
cached = _cache.get(resolved)
|
|
62
|
+
if cached is not None and cached.exists():
|
|
63
|
+
return cached
|
|
64
|
+
|
|
65
|
+
holder = tempfile.TemporaryDirectory(prefix="mkatif-")
|
|
66
|
+
_temp_dirs.append(holder) # keep alive for the life of the process
|
|
67
|
+
root = Path(holder.name)
|
|
68
|
+
|
|
69
|
+
if zipfile.is_zipfile(path):
|
|
70
|
+
with zipfile.ZipFile(path) as archive:
|
|
71
|
+
members = archive.infolist()
|
|
72
|
+
if len(members) > MAX_MEMBERS:
|
|
73
|
+
raise ValueError(f"archive has too many entries ({len(members)})")
|
|
74
|
+
total = sum(m.file_size for m in members)
|
|
75
|
+
if total > MAX_TOTAL_BYTES:
|
|
76
|
+
raise ValueError(f"archive expands to {total} bytes, refusing")
|
|
77
|
+
for member in members:
|
|
78
|
+
if member.is_dir():
|
|
79
|
+
continue
|
|
80
|
+
target = _safe_target(root, member.filename)
|
|
81
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
with archive.open(member) as src, target.open("wb") as dst:
|
|
83
|
+
shutil.copyfileobj(src, dst)
|
|
84
|
+
else:
|
|
85
|
+
with tarfile.open(path) as archive:
|
|
86
|
+
members = archive.getmembers()
|
|
87
|
+
if len(members) > MAX_MEMBERS:
|
|
88
|
+
raise ValueError(f"archive has too many entries ({len(members)})")
|
|
89
|
+
total = sum(m.size for m in members if m.isreg())
|
|
90
|
+
if total > MAX_TOTAL_BYTES:
|
|
91
|
+
raise ValueError(f"archive expands to {total} bytes, refusing")
|
|
92
|
+
for member in members:
|
|
93
|
+
if not (member.isreg() or member.isdir()):
|
|
94
|
+
# Skip links and devices outright; nothing here needs them.
|
|
95
|
+
continue
|
|
96
|
+
_safe_target(root, member.name)
|
|
97
|
+
# `filter="data"` is Python's own hardening for the same class of bug.
|
|
98
|
+
archive.extractall(root, filter="data")
|
|
99
|
+
|
|
100
|
+
_cache[resolved] = root
|
|
101
|
+
return root
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def cleanup() -> None:
|
|
105
|
+
"""Drop every extracted directory. Mainly for tests."""
|
|
106
|
+
for holder in _temp_dirs:
|
|
107
|
+
holder.cleanup()
|
|
108
|
+
_temp_dirs.clear()
|
|
109
|
+
_cache.clear()
|