cc-tracer 0.1.3__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.
- cc_tracer-0.1.3/LICENSE +21 -0
- cc_tracer-0.1.3/PKG-INFO +148 -0
- cc_tracer-0.1.3/README.md +101 -0
- cc_tracer-0.1.3/pyproject.toml +38 -0
- cc_tracer-0.1.3/setup.cfg +4 -0
- cc_tracer-0.1.3/src/cc_tracer/__init__.py +3 -0
- cc_tracer-0.1.3/src/cc_tracer/__main__.py +4 -0
- cc_tracer-0.1.3/src/cc_tracer/cli.py +194 -0
- cc_tracer-0.1.3/src/cc_tracer/config.py +17 -0
- cc_tracer-0.1.3/src/cc_tracer/hooks.py +70 -0
- cc_tracer-0.1.3/src/cc_tracer/server.py +322 -0
- cc_tracer-0.1.3/src/cc_tracer/settings_example.json +37 -0
- cc_tracer-0.1.3/src/cc_tracer/static/index.html +636 -0
- cc_tracer-0.1.3/src/cc_tracer.egg-info/PKG-INFO +148 -0
- cc_tracer-0.1.3/src/cc_tracer.egg-info/SOURCES.txt +17 -0
- cc_tracer-0.1.3/src/cc_tracer.egg-info/dependency_links.txt +1 -0
- cc_tracer-0.1.3/src/cc_tracer.egg-info/entry_points.txt +2 -0
- cc_tracer-0.1.3/src/cc_tracer.egg-info/requires.txt +3 -0
- cc_tracer-0.1.3/src/cc_tracer.egg-info/top_level.txt +1 -0
cc_tracer-0.1.3/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The Claude Code Tracer Authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cc_tracer-0.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cc-tracer
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: A local, infra-free, raw-fidelity inspector for a single Claude Code session — a DevTools Network tab for Claude Code.
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 The Claude Code Tracer Authors
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Project-URL: Homepage, https://github.com/zhluo/claude-code-tracer
|
|
28
|
+
Keywords: claude,claude-code,tracing,debugging,observability
|
|
29
|
+
Classifier: Development Status :: 4 - Beta
|
|
30
|
+
Classifier: Intended Audience :: Developers
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Environment :: Console
|
|
33
|
+
Classifier: Operating System :: OS Independent
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
40
|
+
Requires-Python: >=3.9
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
License-File: LICENSE
|
|
43
|
+
Requires-Dist: fastapi
|
|
44
|
+
Requires-Dist: uvicorn
|
|
45
|
+
Requires-Dist: httpx
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
# cc-tracer (For Claude Code)
|
|
49
|
+
|
|
50
|
+
A local, infra-free, raw-fidelity inspector for a **single** Claude Code session — a
|
|
51
|
+
*DevTools Network tab for Claude Code*. Install it, point Claude Code at it, and watch
|
|
52
|
+
one session's hook events and raw API turns on a live timeline.
|
|
53
|
+
|
|
54
|
+
## How it captures
|
|
55
|
+
|
|
56
|
+
A local FastAPI server (127.0.0.1:7355) with Two capture tiers:
|
|
57
|
+
|
|
58
|
+
- **Hook tier** — Claude Code hooks POST to the tracer server. Captures *what Claude
|
|
59
|
+
did*: prompts, Pre/PostToolUse, results, stop reasons.
|
|
60
|
+
- **API-proxy tier** — point `ANTHROPIC_BASE_URL` at the tracer; it streams `/v1/*` to
|
|
61
|
+
the real API (plain HTTP in, real HTTPS out — no cert trust needed) and reassembles
|
|
62
|
+
the streaming response into full API turns: system prompt, message context, reasoning
|
|
63
|
+
text, token usage — *what Claude saw and thought*.
|
|
64
|
+
|
|
65
|
+
## Getting started
|
|
66
|
+
|
|
67
|
+
Prerequisites: `python3` (3.9+), `pip`, and the `claude` CLI on your `PATH`.
|
|
68
|
+
|
|
69
|
+
Install the package, then let one command do everything — configure the Claude Code
|
|
70
|
+
hooks, start the tracer server, and launch Claude Code routed through it:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install cc-tracer # or: pip install -e . (from a clone)
|
|
74
|
+
cc-tracer start
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Then open the tracer UI at <http://127.0.0.1:7355> and use Claude Code as usual. The
|
|
78
|
+
tracer **keeps running after you quit Claude Code** so you can keep browsing the
|
|
79
|
+
capture — run `cc-tracer stop` when you're done.
|
|
80
|
+
|
|
81
|
+
What `cc-tracer start` does:
|
|
82
|
+
|
|
83
|
+
1. Merges the tracer hooks into the **project-local** `.claude/settings.json` (in the
|
|
84
|
+
current directory, so they apply only to this project — not every Claude session;
|
|
85
|
+
idempotent, backs the original up to `.bak` once). Use `--settings PATH` to target a
|
|
86
|
+
different file, e.g. `~/.claude/settings.json` to trace globally.
|
|
87
|
+
2. Starts a detached tracer server on `:7355` (hook sink + UI + API proxy), or reuses
|
|
88
|
+
one already running there. The server is left running when Claude exits.
|
|
89
|
+
3. Exports ANTHROPIC_BASE_URL=http://127.0.0.1:7355 and runs `claude` (you can pass claude args after `--`).
|
|
90
|
+
|
|
91
|
+
Session JSONL is written to `~/.cc-tracer/logs/` (override with `TRACER_LOG_DIR` or
|
|
92
|
+
`--log-dir`).
|
|
93
|
+
|
|
94
|
+
For instance:
|
|
95
|
+
```bash
|
|
96
|
+
cc-tracer start \
|
|
97
|
+
--log-dir ./logs/ \
|
|
98
|
+
-- -c # -c to continue recent claude session
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
To run **just the server** without launching Claude — e.g. to browse past captures in
|
|
102
|
+
the UI — use
|
|
103
|
+
```bash
|
|
104
|
+
cc-tracer start --server-only
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Stopping
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
cc-tracer stop # stop the server AND remove the tracer's hooks
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Hooks follow the server's lifecycle: `start` adds them and `stop` removes only the
|
|
114
|
+
tracer's own entries (your other hooks are left alone). With the server running, you
|
|
115
|
+
can also point a Claude session you launch yourself at it with
|
|
116
|
+
`export ANTHROPIC_BASE_URL=http://127.0.0.1:7355`.
|
|
117
|
+
|
|
118
|
+
## When to use this vs. OpenTelemetry
|
|
119
|
+
|
|
120
|
+
Claude Code emits OpenTelemetry natively, and tools like SigNoz, Grafana, and
|
|
121
|
+
LangSmith build on it. They are **aggregate observability** — dashboards, cost/latency
|
|
122
|
+
trends, alerting, cross-session correlation across a fleet. If that's your goal, use
|
|
123
|
+
them; this tracer doesn't try to.
|
|
124
|
+
|
|
125
|
+
This tracer targets the thing those tools explicitly aren't: a **local, infra-free,
|
|
126
|
+
raw-fidelity inspector for a single session** — think "DevTools Network tab for Claude
|
|
127
|
+
Code."
|
|
128
|
+
|
|
129
|
+
| | This tracer | Native OTel (SigNoz / Grafana / LangSmith) |
|
|
130
|
+
| --- | --- | --- |
|
|
131
|
+
| Raw API request/response body | ✅ | ❌ (spans/metrics; content redacted by default) |
|
|
132
|
+
| Reasoning / thinking text | ✅ | ❌ |
|
|
133
|
+
| Tool layer (Pre/Post, Edit diffs) | ✅ | ✅ |
|
|
134
|
+
| Backend infra required | none (JSONL + one HTML file) | collector + storage + UI |
|
|
135
|
+
| Data stays fully local | ✅ | configurable / often SaaS |
|
|
136
|
+
| Cross-session stats & alerting | ❌ | ✅ |
|
|
137
|
+
|
|
138
|
+
The closest off-the-shelf analog is a manual mitmproxy setup; the base-URL forwarder
|
|
139
|
+
avoids that approach's CA forging and `NODE_EXTRA_CA_CERTS` fuss.
|
|
140
|
+
|
|
141
|
+
### Caveats
|
|
142
|
+
|
|
143
|
+
- **Proxy ≠ full coverage.** The API proxy only sees HTTP traffic. Transport that
|
|
144
|
+
isn't plain HTTP (e.g. the Agent SDK's IPC/WebSocket) is captured only at the hook
|
|
145
|
+
tier. Native OTel emits regardless of transport.
|
|
146
|
+
- **SSE reassembly is schema-coupled.** Rebuilding turns depends on the current
|
|
147
|
+
Anthropic event shape, so it needs upkeep when the API evolves — a maintenance cost
|
|
148
|
+
the OTel-based tools don't carry.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# cc-tracer (For Claude Code)
|
|
2
|
+
|
|
3
|
+
A local, infra-free, raw-fidelity inspector for a **single** Claude Code session — a
|
|
4
|
+
*DevTools Network tab for Claude Code*. Install it, point Claude Code at it, and watch
|
|
5
|
+
one session's hook events and raw API turns on a live timeline.
|
|
6
|
+
|
|
7
|
+
## How it captures
|
|
8
|
+
|
|
9
|
+
A local FastAPI server (127.0.0.1:7355) with Two capture tiers:
|
|
10
|
+
|
|
11
|
+
- **Hook tier** — Claude Code hooks POST to the tracer server. Captures *what Claude
|
|
12
|
+
did*: prompts, Pre/PostToolUse, results, stop reasons.
|
|
13
|
+
- **API-proxy tier** — point `ANTHROPIC_BASE_URL` at the tracer; it streams `/v1/*` to
|
|
14
|
+
the real API (plain HTTP in, real HTTPS out — no cert trust needed) and reassembles
|
|
15
|
+
the streaming response into full API turns: system prompt, message context, reasoning
|
|
16
|
+
text, token usage — *what Claude saw and thought*.
|
|
17
|
+
|
|
18
|
+
## Getting started
|
|
19
|
+
|
|
20
|
+
Prerequisites: `python3` (3.9+), `pip`, and the `claude` CLI on your `PATH`.
|
|
21
|
+
|
|
22
|
+
Install the package, then let one command do everything — configure the Claude Code
|
|
23
|
+
hooks, start the tracer server, and launch Claude Code routed through it:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install cc-tracer # or: pip install -e . (from a clone)
|
|
27
|
+
cc-tracer start
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then open the tracer UI at <http://127.0.0.1:7355> and use Claude Code as usual. The
|
|
31
|
+
tracer **keeps running after you quit Claude Code** so you can keep browsing the
|
|
32
|
+
capture — run `cc-tracer stop` when you're done.
|
|
33
|
+
|
|
34
|
+
What `cc-tracer start` does:
|
|
35
|
+
|
|
36
|
+
1. Merges the tracer hooks into the **project-local** `.claude/settings.json` (in the
|
|
37
|
+
current directory, so they apply only to this project — not every Claude session;
|
|
38
|
+
idempotent, backs the original up to `.bak` once). Use `--settings PATH` to target a
|
|
39
|
+
different file, e.g. `~/.claude/settings.json` to trace globally.
|
|
40
|
+
2. Starts a detached tracer server on `:7355` (hook sink + UI + API proxy), or reuses
|
|
41
|
+
one already running there. The server is left running when Claude exits.
|
|
42
|
+
3. Exports ANTHROPIC_BASE_URL=http://127.0.0.1:7355 and runs `claude` (you can pass claude args after `--`).
|
|
43
|
+
|
|
44
|
+
Session JSONL is written to `~/.cc-tracer/logs/` (override with `TRACER_LOG_DIR` or
|
|
45
|
+
`--log-dir`).
|
|
46
|
+
|
|
47
|
+
For instance:
|
|
48
|
+
```bash
|
|
49
|
+
cc-tracer start \
|
|
50
|
+
--log-dir ./logs/ \
|
|
51
|
+
-- -c # -c to continue recent claude session
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
To run **just the server** without launching Claude — e.g. to browse past captures in
|
|
55
|
+
the UI — use
|
|
56
|
+
```bash
|
|
57
|
+
cc-tracer start --server-only
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Stopping
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
cc-tracer stop # stop the server AND remove the tracer's hooks
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Hooks follow the server's lifecycle: `start` adds them and `stop` removes only the
|
|
67
|
+
tracer's own entries (your other hooks are left alone). With the server running, you
|
|
68
|
+
can also point a Claude session you launch yourself at it with
|
|
69
|
+
`export ANTHROPIC_BASE_URL=http://127.0.0.1:7355`.
|
|
70
|
+
|
|
71
|
+
## When to use this vs. OpenTelemetry
|
|
72
|
+
|
|
73
|
+
Claude Code emits OpenTelemetry natively, and tools like SigNoz, Grafana, and
|
|
74
|
+
LangSmith build on it. They are **aggregate observability** — dashboards, cost/latency
|
|
75
|
+
trends, alerting, cross-session correlation across a fleet. If that's your goal, use
|
|
76
|
+
them; this tracer doesn't try to.
|
|
77
|
+
|
|
78
|
+
This tracer targets the thing those tools explicitly aren't: a **local, infra-free,
|
|
79
|
+
raw-fidelity inspector for a single session** — think "DevTools Network tab for Claude
|
|
80
|
+
Code."
|
|
81
|
+
|
|
82
|
+
| | This tracer | Native OTel (SigNoz / Grafana / LangSmith) |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| Raw API request/response body | ✅ | ❌ (spans/metrics; content redacted by default) |
|
|
85
|
+
| Reasoning / thinking text | ✅ | ❌ |
|
|
86
|
+
| Tool layer (Pre/Post, Edit diffs) | ✅ | ✅ |
|
|
87
|
+
| Backend infra required | none (JSONL + one HTML file) | collector + storage + UI |
|
|
88
|
+
| Data stays fully local | ✅ | configurable / often SaaS |
|
|
89
|
+
| Cross-session stats & alerting | ❌ | ✅ |
|
|
90
|
+
|
|
91
|
+
The closest off-the-shelf analog is a manual mitmproxy setup; the base-URL forwarder
|
|
92
|
+
avoids that approach's CA forging and `NODE_EXTRA_CA_CERTS` fuss.
|
|
93
|
+
|
|
94
|
+
### Caveats
|
|
95
|
+
|
|
96
|
+
- **Proxy ≠ full coverage.** The API proxy only sees HTTP traffic. Transport that
|
|
97
|
+
isn't plain HTTP (e.g. the Agent SDK's IPC/WebSocket) is captured only at the hook
|
|
98
|
+
tier. Native OTel emits regardless of transport.
|
|
99
|
+
- **SSE reassembly is schema-coupled.** Rebuilding turns depends on the current
|
|
100
|
+
Anthropic event shape, so it needs upkeep when the API evolves — a maintenance cost
|
|
101
|
+
the OTel-based tools don't carry.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cc-tracer"
|
|
7
|
+
version = "0.1.3"
|
|
8
|
+
description = "A local, infra-free, raw-fidelity inspector for a single Claude Code session — a DevTools Network tab for Claude Code."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
keywords = ["claude", "claude-code", "tracing", "debugging", "observability"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Software Development :: Debuggers",
|
|
25
|
+
]
|
|
26
|
+
dependencies = ["fastapi", "uvicorn", "httpx"]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
cc-tracer = "cc_tracer.cli:main"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/zhluo/claude-code-tracer"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.package-data]
|
|
38
|
+
cc_tracer = ["static/*", "settings_example.json"]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""cc-tracer command-line interface.
|
|
2
|
+
|
|
3
|
+
start install hooks (if needed), start a detached server, run Claude through it
|
|
4
|
+
stop stop the server and remove its hooks
|
|
5
|
+
|
|
6
|
+
(`_serve` is an internal, hidden command: the bare server process that `start`
|
|
7
|
+
spawns detached. Use `start` instead.)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import os
|
|
12
|
+
import signal
|
|
13
|
+
import socket
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from . import config, hooks as hooks_mod
|
|
20
|
+
|
|
21
|
+
# Project-local by default so hooks only apply to Claude Code run in this project
|
|
22
|
+
# (not every session on the machine). Override with --settings ~/.claude/settings.json.
|
|
23
|
+
DEFAULT_SETTINGS = ".claude/settings.json"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _wait_port(port, timeout=10.0):
|
|
27
|
+
deadline = time.time() + timeout
|
|
28
|
+
while time.time() < deadline:
|
|
29
|
+
if _port_open(port):
|
|
30
|
+
return True
|
|
31
|
+
time.sleep(0.1)
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _port_open(port):
|
|
36
|
+
with socket.socket() as s:
|
|
37
|
+
s.settimeout(0.2)
|
|
38
|
+
try:
|
|
39
|
+
s.connect(("127.0.0.1", port))
|
|
40
|
+
return True
|
|
41
|
+
except OSError:
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _hook_url(port):
|
|
46
|
+
return f"http://127.0.0.1:{port}/event"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def cmd_serve(a):
|
|
50
|
+
"""Internal `_serve`: the bare server process (writes a pidfile so `stop`
|
|
51
|
+
can find it). Spawned detached by `start`; not meant to be run directly."""
|
|
52
|
+
if a.log_dir:
|
|
53
|
+
os.environ["TRACER_LOG_DIR"] = str(a.log_dir)
|
|
54
|
+
import uvicorn
|
|
55
|
+
from .server import app # imported after env is set so LOG_DIR picks it up
|
|
56
|
+
pid_file = config.pid_file(a.port)
|
|
57
|
+
pid_file.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
pid_file.write_text(str(os.getpid()))
|
|
59
|
+
try:
|
|
60
|
+
uvicorn.run(app, host=a.host, port=a.port)
|
|
61
|
+
finally:
|
|
62
|
+
try:
|
|
63
|
+
pid_file.unlink()
|
|
64
|
+
except OSError:
|
|
65
|
+
pass
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def cmd_start(a):
|
|
70
|
+
base = f"http://127.0.0.1:{a.port}"
|
|
71
|
+
settings = Path(a.settings).expanduser()
|
|
72
|
+
log_dir = Path(a.log_dir) if a.log_dir else config.log_dir()
|
|
73
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
|
|
75
|
+
# 1. Ensure the tracer hooks are installed (idempotent).
|
|
76
|
+
added = hooks_mod.install(settings, _hook_url(a.port))
|
|
77
|
+
print(f"Hooks {'installed in' if added else 'already in'} {settings}")
|
|
78
|
+
|
|
79
|
+
# 2. Start a detached server (or reuse one already on the port). Detached +
|
|
80
|
+
# own session so quitting Claude leaves the tracer running.
|
|
81
|
+
server = None
|
|
82
|
+
if _port_open(a.port):
|
|
83
|
+
print(f"Reusing tracer already running at {base}")
|
|
84
|
+
else:
|
|
85
|
+
env = os.environ.copy()
|
|
86
|
+
env["TRACER_LOG_DIR"] = str(log_dir)
|
|
87
|
+
server_log = open(log_dir / "server.log", "a")
|
|
88
|
+
server = subprocess.Popen(
|
|
89
|
+
[sys.executable, "-m", "cc_tracer", "_serve", "--host", "127.0.0.1", "--port", str(a.port)],
|
|
90
|
+
env=env, stdout=server_log, stderr=server_log, start_new_session=True,
|
|
91
|
+
)
|
|
92
|
+
if not _wait_port(a.port):
|
|
93
|
+
print(f"cc-tracer server failed to start; see {log_dir / 'server.log'}", file=sys.stderr)
|
|
94
|
+
server.terminate()
|
|
95
|
+
return 1
|
|
96
|
+
print(f"Started tracer at {base} (log: {log_dir / 'server.log'})")
|
|
97
|
+
|
|
98
|
+
print(f"Tracer UI: {base}")
|
|
99
|
+
stop = "cc-tracer stop" + ("" if a.port == config.DEFAULT_PORT else f" --port {a.port}")
|
|
100
|
+
|
|
101
|
+
# 3. With --server-only, leave the detached server running and exit.
|
|
102
|
+
if a.server_only:
|
|
103
|
+
print(f" point Claude at it: export ANTHROPIC_BASE_URL={base}")
|
|
104
|
+
print(f" stop it (and remove hooks) with: {stop}")
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
# Otherwise run Claude Code routed through the tracer.
|
|
108
|
+
run_env = os.environ.copy()
|
|
109
|
+
run_env["ANTHROPIC_BASE_URL"] = base
|
|
110
|
+
claude_args = [x for x in a.claude_args if x != "--"]
|
|
111
|
+
try:
|
|
112
|
+
rc = subprocess.call(["claude", *claude_args], env=run_env)
|
|
113
|
+
except FileNotFoundError:
|
|
114
|
+
print("claude not found on PATH. The tracer is running; start Claude yourself "
|
|
115
|
+
f"with: export ANTHROPIC_BASE_URL={base}", file=sys.stderr)
|
|
116
|
+
rc = 127
|
|
117
|
+
except KeyboardInterrupt:
|
|
118
|
+
rc = 130
|
|
119
|
+
|
|
120
|
+
# Leave the server running after Claude exits; `stop` tears it down.
|
|
121
|
+
print(f"\nTracer still running at {base}")
|
|
122
|
+
print(f" stop it (and remove hooks) with: {stop}")
|
|
123
|
+
return rc
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def cmd_stop(a):
|
|
127
|
+
if a.log_dir:
|
|
128
|
+
os.environ["TRACER_LOG_DIR"] = str(a.log_dir)
|
|
129
|
+
|
|
130
|
+
# 1. Stop the server (via its pidfile), if one is running.
|
|
131
|
+
pid_file = config.pid_file(a.port)
|
|
132
|
+
if pid_file.exists():
|
|
133
|
+
try:
|
|
134
|
+
pid = int(pid_file.read_text().strip())
|
|
135
|
+
except ValueError:
|
|
136
|
+
print(f"Corrupt pidfile; removing {pid_file}")
|
|
137
|
+
pid_file.unlink()
|
|
138
|
+
pid = None
|
|
139
|
+
if pid is not None:
|
|
140
|
+
try:
|
|
141
|
+
os.kill(pid, signal.SIGTERM)
|
|
142
|
+
for _ in range(50): # wait for shutdown to free the port
|
|
143
|
+
if not _port_open(a.port):
|
|
144
|
+
break
|
|
145
|
+
time.sleep(0.1)
|
|
146
|
+
print(f"Stopped cc-tracer (pid {pid}) on port {a.port}.")
|
|
147
|
+
except ProcessLookupError:
|
|
148
|
+
print(f"Tracer (pid {pid}) wasn't running.")
|
|
149
|
+
try:
|
|
150
|
+
pid_file.unlink()
|
|
151
|
+
except OSError:
|
|
152
|
+
pass
|
|
153
|
+
else:
|
|
154
|
+
msg = f"No running tracer for port {a.port}."
|
|
155
|
+
if _port_open(a.port):
|
|
156
|
+
msg += f" (Something else is listening on {a.port}.)"
|
|
157
|
+
print(msg)
|
|
158
|
+
|
|
159
|
+
# 2. Remove the tracer hooks (only the entries pointing at our URL).
|
|
160
|
+
settings = Path(a.settings).expanduser()
|
|
161
|
+
url = _hook_url(a.port)
|
|
162
|
+
removed = hooks_mod.uninstall(settings, url)
|
|
163
|
+
print(f"{'Removed' if removed else 'No'} tracer hooks ({url}) in {settings}")
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def main(argv=None):
|
|
168
|
+
p = argparse.ArgumentParser(prog="cc-tracer", description="A local inspector for a single Claude Code session.")
|
|
169
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
170
|
+
|
|
171
|
+
s = sub.add_parser("start", help="install hooks, start a detached server, run Claude through it")
|
|
172
|
+
s.add_argument("--port", type=int, default=config.DEFAULT_PORT)
|
|
173
|
+
s.add_argument("--settings", default=DEFAULT_SETTINGS, help="settings.json for hooks (default: project-local .claude/settings.json)")
|
|
174
|
+
s.add_argument("--log-dir", type=Path, help="override $TRACER_LOG_DIR")
|
|
175
|
+
s.add_argument("--server-only", action="store_true",
|
|
176
|
+
help="just start the server (don't launch Claude)")
|
|
177
|
+
s.add_argument("claude_args", nargs=argparse.REMAINDER, help="args passed through to `claude`")
|
|
178
|
+
s.set_defaults(func=cmd_start)
|
|
179
|
+
|
|
180
|
+
st = sub.add_parser("stop", help="stop the tracer server and remove its hooks")
|
|
181
|
+
st.add_argument("--port", type=int, default=config.DEFAULT_PORT)
|
|
182
|
+
st.add_argument("--settings", default=DEFAULT_SETTINGS, help="settings.json to remove hooks from (default: project-local)")
|
|
183
|
+
st.add_argument("--log-dir", type=Path, help="where start recorded its pidfile")
|
|
184
|
+
st.set_defaults(func=cmd_stop)
|
|
185
|
+
|
|
186
|
+
# Internal: the bare server process that `start` spawns detached.
|
|
187
|
+
sv = sub.add_parser("_serve", help=argparse.SUPPRESS)
|
|
188
|
+
sv.add_argument("--port", type=int, default=config.DEFAULT_PORT)
|
|
189
|
+
sv.add_argument("--host", default="127.0.0.1")
|
|
190
|
+
sv.add_argument("--log-dir", type=Path)
|
|
191
|
+
sv.set_defaults(func=cmd_serve)
|
|
192
|
+
|
|
193
|
+
a = p.parse_args(argv)
|
|
194
|
+
return a.func(a) or 0
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Shared configuration: where session JSONL lives, default port."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
DEFAULT_PORT = 7355
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def log_dir() -> Path:
|
|
10
|
+
"""Directory holding per-session JSONL. Override with $TRACER_LOG_DIR;
|
|
11
|
+
defaults to ~/.cc-tracer/logs so sessions land in one predictable place."""
|
|
12
|
+
return Path(os.environ.get("TRACER_LOG_DIR") or Path.home() / ".cc-tracer" / "logs")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def pid_file(port: int) -> Path:
|
|
16
|
+
"""Where `serve` records its PID so `stop` can find it (per port, in log_dir)."""
|
|
17
|
+
return log_dir() / f"cc-tracer-{port}.pid"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Install / remove the tracer's HTTP hooks in a Claude Code settings.json."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
# The Claude Code events we capture. The server records any event it receives,
|
|
8
|
+
# so this list is purely which hooks we register.
|
|
9
|
+
EVENTS = [
|
|
10
|
+
"SessionStart", "SessionEnd", "UserPromptSubmit",
|
|
11
|
+
"PreToolUse", "PostToolUse", "PostToolUseFailure",
|
|
12
|
+
"SubagentStart", "SubagentStop", "PreCompact", "PostCompact", "Stop",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def tracer_hooks(url):
|
|
17
|
+
"""The hooks block that POSTs every captured event to `url`."""
|
|
18
|
+
return {ev: [{"hooks": [{"type": "http", "url": url}]}] for ev in EVENTS}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def install(settings_path, url):
|
|
22
|
+
"""Merge the tracer hooks into settings.json (idempotent). Backs the original
|
|
23
|
+
up to <name>.json.bak once. Returns True if anything was added."""
|
|
24
|
+
settings_path = Path(settings_path)
|
|
25
|
+
current = {}
|
|
26
|
+
if settings_path.exists():
|
|
27
|
+
current = json.loads(settings_path.read_text() or "{}")
|
|
28
|
+
bak = settings_path.with_suffix(".json.bak")
|
|
29
|
+
if not bak.exists():
|
|
30
|
+
shutil.copy(settings_path, bak)
|
|
31
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
hooks = current.setdefault("hooks", {})
|
|
34
|
+
added = False
|
|
35
|
+
for event, entries in tracer_hooks(url).items():
|
|
36
|
+
bucket = hooks.setdefault(event, [])
|
|
37
|
+
serialized = json.dumps(bucket)
|
|
38
|
+
for entry in entries:
|
|
39
|
+
if json.dumps(entry) not in serialized:
|
|
40
|
+
bucket.append(entry)
|
|
41
|
+
added = True
|
|
42
|
+
settings_path.write_text(json.dumps(current, indent=2) + "\n")
|
|
43
|
+
return added
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def uninstall(settings_path, url):
|
|
47
|
+
"""Remove only the HTTP hook entries pointing at `url`. Returns True if any
|
|
48
|
+
were removed. Leaves the user's other hooks untouched."""
|
|
49
|
+
settings_path = Path(settings_path)
|
|
50
|
+
if not settings_path.exists():
|
|
51
|
+
return False
|
|
52
|
+
current = json.loads(settings_path.read_text() or "{}")
|
|
53
|
+
hooks = current.get("hooks", {})
|
|
54
|
+
removed = False
|
|
55
|
+
for event in list(hooks):
|
|
56
|
+
kept_groups = []
|
|
57
|
+
for group in hooks[event]:
|
|
58
|
+
inner = [h for h in group.get("hooks", [])
|
|
59
|
+
if not (h.get("type") == "http" and h.get("url") == url)]
|
|
60
|
+
if len(inner) != len(group.get("hooks", [])):
|
|
61
|
+
removed = True
|
|
62
|
+
if inner:
|
|
63
|
+
kept_groups.append({**group, "hooks": inner})
|
|
64
|
+
if kept_groups:
|
|
65
|
+
hooks[event] = kept_groups
|
|
66
|
+
else:
|
|
67
|
+
del hooks[event]
|
|
68
|
+
current["hooks"] = hooks
|
|
69
|
+
settings_path.write_text(json.dumps(current, indent=2) + "\n")
|
|
70
|
+
return removed
|