agent-trace-cli 0.1.0__py3-none-any.whl
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_trace/__init__.py +3 -0
- agent_trace/blame.py +471 -0
- agent_trace/blame_git.py +133 -0
- agent_trace/blame_meta.py +167 -0
- agent_trace/cli.py +2680 -0
- agent_trace/commit_link.py +226 -0
- agent_trace/config.py +137 -0
- agent_trace/context.py +385 -0
- agent_trace/conversations.py +358 -0
- agent_trace/git_notes.py +491 -0
- agent_trace/hooks/__init__.py +163 -0
- agent_trace/hooks/base.py +233 -0
- agent_trace/hooks/claude.py +483 -0
- agent_trace/hooks/codex.py +232 -0
- agent_trace/hooks/cursor.py +278 -0
- agent_trace/hooks/git.py +144 -0
- agent_trace/ledger.py +955 -0
- agent_trace/models.py +618 -0
- agent_trace/record.py +417 -0
- agent_trace/registry.py +230 -0
- agent_trace/remote.py +673 -0
- agent_trace/rewrite.py +176 -0
- agent_trace/rules.py +209 -0
- agent_trace/schemas/commit-link.schema.json +34 -0
- agent_trace/schemas/git-note.schema.json +88 -0
- agent_trace/schemas/ledger.schema.json +97 -0
- agent_trace/schemas/remotes.schema.json +30 -0
- agent_trace/schemas/sync-state.schema.json +49 -0
- agent_trace/schemas/trace-record.schema.json +130 -0
- agent_trace/session.py +136 -0
- agent_trace/storage.py +229 -0
- agent_trace/summary.py +465 -0
- agent_trace/summary_presets.py +188 -0
- agent_trace/sync.py +1182 -0
- agent_trace/telemetry.py +142 -0
- agent_trace/trace.py +460 -0
- agent_trace_cli-0.1.0.dist-info/METADATA +535 -0
- agent_trace_cli-0.1.0.dist-info/RECORD +42 -0
- agent_trace_cli-0.1.0.dist-info/WHEEL +5 -0
- agent_trace_cli-0.1.0.dist-info/entry_points.txt +2 -0
- agent_trace_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- agent_trace_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base interface for coding-agent adapters (Cursor, Claude Code, Codex, ...).
|
|
3
|
+
|
|
4
|
+
An ``Adapter`` is the single place where everything about one coding agent
|
|
5
|
+
lives:
|
|
6
|
+
|
|
7
|
+
- ``inject`` / ``remove`` / ``is_installed`` — manage the agent's hook
|
|
8
|
+
config (project- or global-level).
|
|
9
|
+
- ``EVENTS`` — map of hook event names this agent emits to translator
|
|
10
|
+
functions that build canonical trace records.
|
|
11
|
+
- ``rules_dir`` / ``rule_extension`` — where prebuilt rules go for this
|
|
12
|
+
agent.
|
|
13
|
+
- ``detect_tool_info`` — recognise the agent at runtime via env/files
|
|
14
|
+
so traces carry the right ``tool.name`` / ``tool.version``.
|
|
15
|
+
|
|
16
|
+
To add support for a new coding agent, subclass ``CodingAgentAdapter`` in
|
|
17
|
+
a new file under ``agent_trace/hooks/<agent>.py`` and register it via
|
|
18
|
+
``register_adapter`` in ``agent_trace/hooks/__init__.py``. Nothing else
|
|
19
|
+
in the codebase should hardcode the agent's name.
|
|
20
|
+
|
|
21
|
+
Stdlib only — no external dependencies.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from abc import ABC, abstractmethod
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Callable, Protocol
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
AGENT_TRACE_CMD = "agent-trace record"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Trace handler: takes raw hook JSON, returns (trace_dict_or_None, event_name).
|
|
35
|
+
EventHandler = Callable[[dict], "tuple[dict | None, str]"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Adapter(Protocol):
|
|
39
|
+
"""Structural protocol for back-compat with earlier callers."""
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
|
|
43
|
+
def global_config_path(self) -> Path: ...
|
|
44
|
+
|
|
45
|
+
def inject(
|
|
46
|
+
self,
|
|
47
|
+
record_invocation: str,
|
|
48
|
+
project_dir: str | None = None,
|
|
49
|
+
*,
|
|
50
|
+
global_install: bool = False,
|
|
51
|
+
) -> bool: ...
|
|
52
|
+
|
|
53
|
+
def remove(self) -> bool: ...
|
|
54
|
+
|
|
55
|
+
def is_installed(self) -> bool: ...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class CodingAgentAdapter(ABC):
|
|
59
|
+
"""Abstract base for a coding-agent integration.
|
|
60
|
+
|
|
61
|
+
Subclasses must set ``name`` (the registry key) and implement the
|
|
62
|
+
hook-injection methods. The remaining pieces (event handlers, rules
|
|
63
|
+
dir, tool detection) are *optional* but encouraged so that every
|
|
64
|
+
surface — ``record``, ``doctor``, ``rule``, ``status`` — can pick
|
|
65
|
+
them up dynamically.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
# Required: short slug used by CLI flags (--tool <name>) and registry.
|
|
69
|
+
name: str = ""
|
|
70
|
+
|
|
71
|
+
# Optional: human label printed in doctor / status output.
|
|
72
|
+
display_name: str = ""
|
|
73
|
+
|
|
74
|
+
# Optional: hook event names this adapter emits → translator functions.
|
|
75
|
+
# Translators return ``(trace_dict_or_None, event_name)``. If empty,
|
|
76
|
+
# the dispatcher in ``record.py`` will not route stdin events to this
|
|
77
|
+
# adapter (e.g. for an adapter that only manages config, no recording).
|
|
78
|
+
EVENTS: dict[str, EventHandler] = {}
|
|
79
|
+
|
|
80
|
+
# Optional: where prebuilt rules live for this agent (relative to repo).
|
|
81
|
+
rules_dir: str = ""
|
|
82
|
+
|
|
83
|
+
# Optional: file extension for rule files (".md", ".mdc", ...).
|
|
84
|
+
rule_extension: str = ".md"
|
|
85
|
+
|
|
86
|
+
# Optional: hook event names that signal "the agent's turn ended" — the
|
|
87
|
+
# dispatcher fires the summary command for these and does NOT route the
|
|
88
|
+
# event to a trace handler. (e.g. Claude's ``Stop``, Cursor's
|
|
89
|
+
# ``afterAgentResponse``.)
|
|
90
|
+
summary_only_events: tuple[str, ...] = ()
|
|
91
|
+
|
|
92
|
+
# Optional: hook event names that should fire summary AND ALSO be
|
|
93
|
+
# dispatched as a regular trace event (e.g. Cursor's ``sessionEnd``,
|
|
94
|
+
# which both ends the session and emits a session-end trace).
|
|
95
|
+
summary_then_trace_events: tuple[str, ...] = ()
|
|
96
|
+
|
|
97
|
+
# Optional: env var names where this agent puts the transcript path.
|
|
98
|
+
# ``transcript_path_from_hook`` walks the registry's union of these.
|
|
99
|
+
# (e.g. Cursor's ``CURSOR_TRANSCRIPT_PATH``.)
|
|
100
|
+
transcript_env_vars: tuple[str, ...] = ()
|
|
101
|
+
|
|
102
|
+
# Optional: env var names where this agent puts the workspace dir.
|
|
103
|
+
# ``project_dir_from_hook`` walks the registry's union of these.
|
|
104
|
+
# (e.g. ``CURSOR_PROJECT_DIR``, ``CLAUDE_PROJECT_DIR``.)
|
|
105
|
+
project_dir_env_vars: tuple[str, ...] = ()
|
|
106
|
+
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
# Hooks: install / remove / inspect
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
@abstractmethod
|
|
112
|
+
def global_config_path(self) -> Path:
|
|
113
|
+
"""Where the global hook configuration for this agent lives."""
|
|
114
|
+
|
|
115
|
+
@abstractmethod
|
|
116
|
+
def inject(
|
|
117
|
+
self,
|
|
118
|
+
record_invocation: str,
|
|
119
|
+
project_dir: str | None = None,
|
|
120
|
+
*,
|
|
121
|
+
global_install: bool = False,
|
|
122
|
+
) -> bool:
|
|
123
|
+
"""Idempotently install hooks that pipe events to ``record_invocation``.
|
|
124
|
+
|
|
125
|
+
``global_install=True`` writes the user-level config; otherwise
|
|
126
|
+
writes a project-scoped config under ``project_dir`` (defaults to
|
|
127
|
+
the current working directory).
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
@abstractmethod
|
|
131
|
+
def remove(self) -> bool:
|
|
132
|
+
"""Remove the agent-trace entries from the *global* config.
|
|
133
|
+
|
|
134
|
+
Returns ``True`` if any entries were removed.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def project_config_paths(self) -> tuple[str, ...]:
|
|
138
|
+
"""Project-scoped config paths (relative to repo root).
|
|
139
|
+
|
|
140
|
+
Used by ``doctor`` / ``status`` to detect project-level installs
|
|
141
|
+
without hardcoding any agent's path. Default: empty.
|
|
142
|
+
"""
|
|
143
|
+
return ()
|
|
144
|
+
|
|
145
|
+
def is_installed(self, *, global_only: bool = True) -> bool:
|
|
146
|
+
"""Whether agent-trace hooks are present in this adapter's config.
|
|
147
|
+
|
|
148
|
+
Default implementation: read the global config file and look for
|
|
149
|
+
``agent-trace record``. Override for adapters with non-text
|
|
150
|
+
config (e.g. binary plist).
|
|
151
|
+
"""
|
|
152
|
+
path = self.global_config_path()
|
|
153
|
+
try:
|
|
154
|
+
raw = path.read_text()
|
|
155
|
+
except (OSError, FileNotFoundError):
|
|
156
|
+
return False
|
|
157
|
+
return AGENT_TRACE_CMD in raw
|
|
158
|
+
|
|
159
|
+
# ------------------------------------------------------------------
|
|
160
|
+
# Recording: hook event → canonical trace record
|
|
161
|
+
# ------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
def handle_event(self, data: dict) -> tuple[dict | None, str | None]:
|
|
164
|
+
"""Translate a hook event payload into a trace record.
|
|
165
|
+
|
|
166
|
+
Looks up ``data['hook_event_name']`` in ``self.EVENTS``. Returns
|
|
167
|
+
``(None, None)`` if the event is not owned by this adapter.
|
|
168
|
+
"""
|
|
169
|
+
event = data.get("hook_event_name", "") or ""
|
|
170
|
+
handler = self.EVENTS.get(event)
|
|
171
|
+
if handler is None:
|
|
172
|
+
return None, None
|
|
173
|
+
return handler(data)
|
|
174
|
+
|
|
175
|
+
def owns_event(self, event_name: str) -> bool:
|
|
176
|
+
return event_name in self.EVENTS
|
|
177
|
+
|
|
178
|
+
# ------------------------------------------------------------------
|
|
179
|
+
# Tool identity
|
|
180
|
+
# ------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
def detect_tool_info(self) -> dict | None:
|
|
183
|
+
"""Return ``{'name': ..., 'version': ...}`` if this agent is
|
|
184
|
+
currently running (env vars, marker files, etc.); else ``None``.
|
|
185
|
+
|
|
186
|
+
Default: not detectable. Override for adapters that expose a
|
|
187
|
+
runtime marker.
|
|
188
|
+
"""
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
# ------------------------------------------------------------------
|
|
192
|
+
# Rule files (prebuilt rules dropped under the project tree)
|
|
193
|
+
# ------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
def rule_path(self, rule_name: str, project_dir: str | None = None) -> Path:
|
|
196
|
+
"""Return the file path for a rule under this agent's rules dir."""
|
|
197
|
+
import os
|
|
198
|
+
|
|
199
|
+
if not self.rules_dir:
|
|
200
|
+
raise NotImplementedError(
|
|
201
|
+
f"Adapter {self.name!r} does not declare a rules_dir; "
|
|
202
|
+
"set ``rules_dir`` on the subclass to enable rules."
|
|
203
|
+
)
|
|
204
|
+
if project_dir is None:
|
|
205
|
+
project_dir = os.getcwd()
|
|
206
|
+
return Path(project_dir) / self.rules_dir / f"agent-trace-{rule_name}{self.rule_extension}"
|
|
207
|
+
|
|
208
|
+
def supports_rules(self) -> bool:
|
|
209
|
+
return bool(self.rules_dir)
|
|
210
|
+
|
|
211
|
+
# ------------------------------------------------------------------
|
|
212
|
+
# Session lifecycle / summary integration
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
def pre_summary_hook(self, data: dict) -> None:
|
|
216
|
+
"""Adapter-specific work to run *before* a summary fires.
|
|
217
|
+
|
|
218
|
+
Default: no-op. Claude overrides this to refresh its session
|
|
219
|
+
model cache from the transcript, since ``PostToolUse`` payloads
|
|
220
|
+
do not include a model id.
|
|
221
|
+
"""
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
def is_session_end(self, event_name: str) -> tuple[bool, bool]:
|
|
225
|
+
"""Classify an event for the session-end / summary pipeline.
|
|
226
|
+
|
|
227
|
+
Returns ``(triggers_summary, also_dispatch_handler)``.
|
|
228
|
+
"""
|
|
229
|
+
if event_name in self.summary_only_events:
|
|
230
|
+
return True, False
|
|
231
|
+
if event_name in self.summary_then_trace_events:
|
|
232
|
+
return True, True
|
|
233
|
+
return False, False
|