A Python CLI for turning raw coding-agent session logs (Claude Code, Cline, Codex, OpenCode, Qwen Code) into clean, redacted, quality-scored JSONL that loads directly into a HuggingFace SFT pipeline.
The pipeline is a chain of JSONL → JSONL stages. Each stage has its own
subcommand and writes a file you can inspect; run chains them all together.
raw logs ──parse──▶ *_raw.jsonl
│
redact (scrub secrets, anonymize paths)
▼
*_redacted.jsonl
│
clean (drop bad tool calls, empty turns, orphans)
▼
*_cleaned.jsonl
│
evaluate (attach quality `score`)
▼
*_scored.jsonl
│
filter (keep score ≥ threshold, format for training)
▼
final.jsonl
pip install -e . # core CLI (stdlib only)
pip install -e '.[huggingface]' # optional: enable Hugging Face dataset uploads
Only validate pulls in transformers — the rest of the pipeline runs on the
standard library alone, so it works in constrained sandboxes with no network.
End-to-end on Claude Code logs (auto-discovers ~/.claude/projects):
python -m logminer run --source claude --output training.jsonl
This writes the final dataset plus four intermediate files
(training_raw.jsonl, _redacted.jsonl, _cleaned.jsonl,
_scored.jsonl) so you can diff stages when debugging.
Higher-quality cut, all supported providers:
python -m logminer run --source all --output data/out.jsonl --min-score 0.7
Upload the final dataset to a Hugging Face dataset repo when a hub token is in
HF_TOKEN, HUGGINGFACE_HUB_TOKEN, or HUGGING_FACE_HUB_TOKEN:
export HF_TOKEN=hf_xxx
python -m logminer run --source claude --output training.jsonl --hf-repo your-name/logminer-data
The dataset always lands at data/train.jsonl in the repo regardless of your
local --output name, so repeated uploads replace it rather than piling up
extra files that load_dataset() would glob into one duplicated split.
On first upload logminer seeds a dataset-card README.md with a consistent
logminer tag and a link back to this GitHub repo, so those datasets are
easier to find on Hugging Face. If the repo already has a README.md it is
left alone — a hand-written card survives re-uploads.
Pass --hf-private to make the dataset repo private; it is applied to
existing repos too, not just newly created ones. If you pass --hf-repo
without a token in the environment, the command fails rather than exiting 0
with nothing published.
If you already ran the earlier stages yourself, filter can upload the final
JSONL too:
python -m logminer filter --input data/scored.jsonl --output data/training.jsonl --hf-repo your-name/logminer-data --hf-private
Parse only, then sanity-check records against a real tokenizer:
python -m logminer parse --source claude --output data/raw.jsonl
python -m logminer validate --input data/raw.jsonl --model Qwen/Qwen3.5-4B
When you omit --input, the parser falls back to each provider's standard
directory:
~/.claude/projects~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/tasks~/.codex/sessions~/.local/share/opencode~/.qwen/projectsPass --input <path> to point at a specific export instead.
--min-score — 0.5 is the default and is forgiving. Raise to 0.7+ when
you have plenty of source logs and want a tighter dataset; lower it when
you're data-starved or still tuning.--max-tokens — 131072 by default. Longer conversations are truncated
to a prefix (still a valid trajectory) rather than dropped, cutting at a
boundary that keeps every tool call paired with its result. Lower it to
match your training context window.--min-turns / --min-token-count — what counts as a "real"
conversation. The evaluator hard-floors anything below these to score 0,
regardless of other signals. Drop them for sparse logs; raise them when you
only want substantive sessions.--source all — convenient, but it parses every provider's default log
dir. Pair --source <name> with --input <path> to target one export.A parsed record:
{
"id": "<session id>",
"source": "claude",
"messages": [
{"role": "user", "content": "..."},
{"role": "assistant", "content": "...", "tool_calls": [...]},
{"role": "tool", "tool_call_id": "...", "content": "..."}
]
}
After evaluate, records gain score, label, reasons, and a metrics
block. After filter, the first message also carries a tools array (the
OpenAI-style tool schemas inferred from observed tool calls), and messages
is serialized to list[str] (one JSON-encoded turn per element) so PyArrow
can unify the schema across rows.
The final JSONL loads cleanly via vanilla datasets.load_dataset:
import json
from datasets import load_dataset
ds = load_dataset("json", data_files="data/training.jsonl", split="train")
for rec in ds:
msgs = [json.loads(m) for m in rec["messages"]] # list[str] → list[dict]
tools = msgs[0].pop("tools", None) if msgs else None
# tokenizer.apply_chat_template(msgs, tools=tools, ...)
Adding a new agent (e.g. Codex) usually means one new file in
logminer/parsers/ plus a one-line entry in parsers/__init__.py. The
pipeline stages are agent-agnostic and don't need changes.
Codex is supported directly: its Responses API-style function-call and
function-output items are normalized to paired assistant tool_calls and
tool turns. Public reasoning summaries and older recoverable reasoning
events are emitted in the same <think>…</think> convention as the other
parsers; reasoning that Codex does not persist cannot be recovered.
Cline task transcripts are supported directly. Its XML-like tool calls and
bracketed tool results are normalized to paired assistant tool_calls and
tool turns; <think> and <thinking> content is retained. To keep SFT
examples tractable, extraction ends after the second attempt_completion or
task_complete call in a task.
See SKILL.md for the parser contract (BaseParser), step-by-step
instructions, and notes on extending the redaction, scoring, and cleaning
stages.
pytest # full suite
pytest tests/test_parsers.py -k claude # one parser at a time
Install pre-commit hooks once per clone so lint and formatting run on every commit:
pip install pre-commit
pre-commit install
pre-commit run --all-files # optional: run against the whole repo right now
Ruff handles both linting and formatting; config lives in pyproject.toml under
[tool.ruff]. CI runs ruff check, ruff format --check, and pytest against
Python 3.10, 3.11, and 3.12 on every push to main and every pull request
(.github/workflows/ci.yml).
Publishing uses Trusted Publishing (OIDC) — no API tokens stored as
secrets. The workflow at .github/workflows/publish.yml builds an sdist + wheel
with python -m build and uploads via pypa/gh-action-pypi-publish.
logminerpublish.ymlpypipypi. Optionally add required reviewers for an extra approval step before
any release runs.version in pyproject.toml (follow semver).main:
git commit -am "Release v0.1.1"
git tag v0.1.1
git push origin main --tags
Alternatively, trigger the workflow manually from the Actions tab
(Publish to PyPI → Run workflow). Manual runs publish whatever is on the
selected branch — make sure version in pyproject.toml is already bumped, or
PyPI will reject the upload as a duplicate.
https://pypi.org/project/logminer/.pip install --upgrade logminer
logminer --help
13 commits
4 commits
Python
100.0%
A Python CLI for turning raw coding-agent session logs (Claude Code, Cline, Codex, OpenCode, Qwen Code) into clean, redacted, quality-scored JSONL that loads directly into a HuggingFace SFT pipeline.
The pipeline is a chain of JSONL → JSONL stages. Each stage has its own
subcommand and writes a file you can inspect; run chains them all together.
raw logs ──parse──▶ *_raw.jsonl
│
redact (scrub secrets, anonymize paths)
▼
*_redacted.jsonl
│
clean (drop bad tool calls, empty turns, orphans)
▼
*_cleaned.jsonl
│
evaluate (attach quality `score`)
▼
*_scored.jsonl
│
filter (keep score ≥ threshold, format for training)
▼
final.jsonl
pip install -e . # core CLI (stdlib only)
pip install -e '.[huggingface]' # optional: enable Hugging Face dataset uploads
Only validate pulls in transformers — the rest of the pipeline runs on the
standard library alone, so it works in constrained sandboxes with no network.
End-to-end on Claude Code logs (auto-discovers ~/.claude/projects):
python -m logminer run --source claude --output training.jsonl
This writes the final dataset plus four intermediate files
(training_raw.jsonl, _redacted.jsonl, _cleaned.jsonl,
_scored.jsonl) so you can diff stages when debugging.
Higher-quality cut, all supported providers:
python -m logminer run --source all --output data/out.jsonl --min-score 0.7
Upload the final dataset to a Hugging Face dataset repo when a hub token is in
HF_TOKEN, HUGGINGFACE_HUB_TOKEN, or HUGGING_FACE_HUB_TOKEN:
export HF_TOKEN=hf_xxx
python -m logminer run --source claude --output training.jsonl --hf-repo your-name/logminer-data
The dataset always lands at data/train.jsonl in the repo regardless of your
local --output name, so repeated uploads replace it rather than piling up
extra files that load_dataset() would glob into one duplicated split.
On first upload logminer seeds a dataset-card README.md with a consistent
logminer tag and a link back to this GitHub repo, so those datasets are
easier to find on Hugging Face. If the repo already has a README.md it is
left alone — a hand-written card survives re-uploads.
Pass --hf-private to make the dataset repo private; it is applied to
existing repos too, not just newly created ones. If you pass --hf-repo
without a token in the environment, the command fails rather than exiting 0
with nothing published.
If you already ran the earlier stages yourself, filter can upload the final
JSONL too:
python -m logminer filter --input data/scored.jsonl --output data/training.jsonl --hf-repo your-name/logminer-data --hf-private
Parse only, then sanity-check records against a real tokenizer:
python -m logminer parse --source claude --output data/raw.jsonl
python -m logminer validate --input data/raw.jsonl --model Qwen/Qwen3.5-4B
When you omit --input, the parser falls back to each provider's standard
directory:
~/.claude/projects~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/tasks~/.codex/sessions~/.local/share/opencode~/.qwen/projectsPass --input <path> to point at a specific export instead.
--min-score — 0.5 is the default and is forgiving. Raise to 0.7+ when
you have plenty of source logs and want a tighter dataset; lower it when
you're data-starved or still tuning.--max-tokens — 131072 by default. Longer conversations are truncated
to a prefix (still a valid trajectory) rather than dropped, cutting at a
boundary that keeps every tool call paired with its result. Lower it to
match your training context window.--min-turns / --min-token-count — what counts as a "real"
conversation. The evaluator hard-floors anything below these to score 0,
regardless of other signals. Drop them for sparse logs; raise them when you
only want substantive sessions.--source all — convenient, but it parses every provider's default log
dir. Pair --source <name> with --input <path> to target one export.A parsed record:
{
"id": "<session id>",
"source": "claude",
"messages": [
{"role": "user", "content": "..."},
{"role": "assistant", "content": "...", "tool_calls": [...]},
{"role": "tool", "tool_call_id": "...", "content": "..."}
]
}
After evaluate, records gain score, label, reasons, and a metrics
block. After filter, the first message also carries a tools array (the
OpenAI-style tool schemas inferred from observed tool calls), and messages
is serialized to list[str] (one JSON-encoded turn per element) so PyArrow
can unify the schema across rows.
The final JSONL loads cleanly via vanilla datasets.load_dataset:
import json
from datasets import load_dataset
ds = load_dataset("json", data_files="data/training.jsonl", split="train")
for rec in ds:
msgs = [json.loads(m) for m in rec["messages"]] # list[str] → list[dict]
tools = msgs[0].pop("tools", None) if msgs else None
# tokenizer.apply_chat_template(msgs, tools=tools, ...)
Adding a new agent (e.g. Codex) usually means one new file in
logminer/parsers/ plus a one-line entry in parsers/__init__.py. The
pipeline stages are agent-agnostic and don't need changes.
Codex is supported directly: its Responses API-style function-call and
function-output items are normalized to paired assistant tool_calls and
tool turns. Public reasoning summaries and older recoverable reasoning
events are emitted in the same <think>…</think> convention as the other
parsers; reasoning that Codex does not persist cannot be recovered.
Cline task transcripts are supported directly. Its XML-like tool calls and
bracketed tool results are normalized to paired assistant tool_calls and
tool turns; <think> and <thinking> content is retained. To keep SFT
examples tractable, extraction ends after the second attempt_completion or
task_complete call in a task.
See SKILL.md for the parser contract (BaseParser), step-by-step
instructions, and notes on extending the redaction, scoring, and cleaning
stages.
pytest # full suite
pytest tests/test_parsers.py -k claude # one parser at a time
Install pre-commit hooks once per clone so lint and formatting run on every commit:
pip install pre-commit
pre-commit install
pre-commit run --all-files # optional: run against the whole repo right now
Ruff handles both linting and formatting; config lives in pyproject.toml under
[tool.ruff]. CI runs ruff check, ruff format --check, and pytest against
Python 3.10, 3.11, and 3.12 on every push to main and every pull request
(.github/workflows/ci.yml).
Publishing uses Trusted Publishing (OIDC) — no API tokens stored as
secrets. The workflow at .github/workflows/publish.yml builds an sdist + wheel
with python -m build and uploads via pypa/gh-action-pypi-publish.
logminerpublish.ymlpypipypi. Optionally add required reviewers for an extra approval step before
any release runs.version in pyproject.toml (follow semver).main:
git commit -am "Release v0.1.1"
git tag v0.1.1
git push origin main --tags
Alternatively, trigger the workflow manually from the Actions tab
(Publish to PyPI → Run workflow). Manual runs publish whatever is on the
selected branch — make sure version in pyproject.toml is already bumped, or
PyPI will reject the upload as a duplicate.
https://pypi.org/project/logminer/.pip install --upgrade logminer
logminer --help
13 commits
4 commits
Python
100.0%