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.
Files changed (42) hide show
  1. agent_trace/__init__.py +3 -0
  2. agent_trace/blame.py +471 -0
  3. agent_trace/blame_git.py +133 -0
  4. agent_trace/blame_meta.py +167 -0
  5. agent_trace/cli.py +2680 -0
  6. agent_trace/commit_link.py +226 -0
  7. agent_trace/config.py +137 -0
  8. agent_trace/context.py +385 -0
  9. agent_trace/conversations.py +358 -0
  10. agent_trace/git_notes.py +491 -0
  11. agent_trace/hooks/__init__.py +163 -0
  12. agent_trace/hooks/base.py +233 -0
  13. agent_trace/hooks/claude.py +483 -0
  14. agent_trace/hooks/codex.py +232 -0
  15. agent_trace/hooks/cursor.py +278 -0
  16. agent_trace/hooks/git.py +144 -0
  17. agent_trace/ledger.py +955 -0
  18. agent_trace/models.py +618 -0
  19. agent_trace/record.py +417 -0
  20. agent_trace/registry.py +230 -0
  21. agent_trace/remote.py +673 -0
  22. agent_trace/rewrite.py +176 -0
  23. agent_trace/rules.py +209 -0
  24. agent_trace/schemas/commit-link.schema.json +34 -0
  25. agent_trace/schemas/git-note.schema.json +88 -0
  26. agent_trace/schemas/ledger.schema.json +97 -0
  27. agent_trace/schemas/remotes.schema.json +30 -0
  28. agent_trace/schemas/sync-state.schema.json +49 -0
  29. agent_trace/schemas/trace-record.schema.json +130 -0
  30. agent_trace/session.py +136 -0
  31. agent_trace/storage.py +229 -0
  32. agent_trace/summary.py +465 -0
  33. agent_trace/summary_presets.py +188 -0
  34. agent_trace/sync.py +1182 -0
  35. agent_trace/telemetry.py +142 -0
  36. agent_trace/trace.py +460 -0
  37. agent_trace_cli-0.1.0.dist-info/METADATA +535 -0
  38. agent_trace_cli-0.1.0.dist-info/RECORD +42 -0
  39. agent_trace_cli-0.1.0.dist-info/WHEEL +5 -0
  40. agent_trace_cli-0.1.0.dist-info/entry_points.txt +2 -0
  41. agent_trace_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
  42. 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