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,167 @@
|
|
|
1
|
+
"""Trace metadata for ledger enrichment (internal)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _load_local_traces(project_dir: str) -> list[dict[str, Any]]:
|
|
11
|
+
from .storage import get_traces_path, resolve_project_id
|
|
12
|
+
|
|
13
|
+
pid = resolve_project_id(project_dir, create=False)
|
|
14
|
+
if not pid:
|
|
15
|
+
return []
|
|
16
|
+
traces_path = get_traces_path(pid)
|
|
17
|
+
if not traces_path.exists():
|
|
18
|
+
return []
|
|
19
|
+
traces: list[dict[str, Any]] = []
|
|
20
|
+
try:
|
|
21
|
+
for line in traces_path.read_text().splitlines():
|
|
22
|
+
line = line.strip()
|
|
23
|
+
if line:
|
|
24
|
+
try:
|
|
25
|
+
traces.append(json.loads(line))
|
|
26
|
+
except json.JSONDecodeError:
|
|
27
|
+
continue
|
|
28
|
+
except OSError:
|
|
29
|
+
pass
|
|
30
|
+
return traces
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _load_conversation_preview(
|
|
34
|
+
project_id: str | None,
|
|
35
|
+
content_sha256: str | None,
|
|
36
|
+
max_chars: int = 200,
|
|
37
|
+
) -> str | None:
|
|
38
|
+
"""First ``max_chars`` of the cached transcript bytes referenced by
|
|
39
|
+
``content_sha256``. Distinct from a generated summary — this is just a
|
|
40
|
+
raw head-of-blob preview used as a fallback when no summary is
|
|
41
|
+
configured/available.
|
|
42
|
+
"""
|
|
43
|
+
if not project_id or not content_sha256:
|
|
44
|
+
return None
|
|
45
|
+
from .conversations import cache_path_for_sha
|
|
46
|
+
|
|
47
|
+
p = cache_path_for_sha(project_id, content_sha256)
|
|
48
|
+
if not p.is_file():
|
|
49
|
+
return None
|
|
50
|
+
try:
|
|
51
|
+
with open(p, "r", encoding="utf-8", errors="replace") as f:
|
|
52
|
+
content = f.read(max_chars + 100)
|
|
53
|
+
except OSError:
|
|
54
|
+
return None
|
|
55
|
+
content = content.strip()
|
|
56
|
+
if not content:
|
|
57
|
+
return None
|
|
58
|
+
if len(content) > max_chars:
|
|
59
|
+
content = content[:max_chars] + "..."
|
|
60
|
+
return content
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _find_matching_file(files: list[dict[str, Any]], file_path: str) -> dict[str, Any] | None:
|
|
64
|
+
for f in files:
|
|
65
|
+
if not isinstance(f, dict):
|
|
66
|
+
continue
|
|
67
|
+
trace_path = f.get("path", "")
|
|
68
|
+
if trace_path == file_path:
|
|
69
|
+
return f
|
|
70
|
+
if trace_path.endswith(file_path) or file_path.endswith(trace_path):
|
|
71
|
+
return f
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _collect_ranges(file_entry: dict[str, Any]) -> list[tuple[int, int]]:
|
|
76
|
+
ranges: list[tuple[int, int]] = []
|
|
77
|
+
if "start_line" in file_entry and "end_line" in file_entry:
|
|
78
|
+
try:
|
|
79
|
+
ranges.append((int(file_entry["start_line"]), int(file_entry["end_line"])))
|
|
80
|
+
except (ValueError, TypeError):
|
|
81
|
+
pass
|
|
82
|
+
for conv in file_entry.get("conversations", []):
|
|
83
|
+
if not isinstance(conv, dict):
|
|
84
|
+
continue
|
|
85
|
+
if "start_line" in conv and "end_line" in conv:
|
|
86
|
+
try:
|
|
87
|
+
ranges.append((int(conv["start_line"]), int(conv["end_line"])))
|
|
88
|
+
except (ValueError, TypeError):
|
|
89
|
+
pass
|
|
90
|
+
for r in conv.get("ranges", []):
|
|
91
|
+
if isinstance(r, dict) and "start_line" in r and "end_line" in r:
|
|
92
|
+
try:
|
|
93
|
+
ranges.append((int(r["start_line"]), int(r["end_line"])))
|
|
94
|
+
except (ValueError, TypeError):
|
|
95
|
+
pass
|
|
96
|
+
for change in file_entry.get("changes", []):
|
|
97
|
+
if not isinstance(change, dict):
|
|
98
|
+
continue
|
|
99
|
+
if "start_line" in change and "end_line" in change:
|
|
100
|
+
try:
|
|
101
|
+
ranges.append((int(change["start_line"]), int(change["end_line"])))
|
|
102
|
+
except (ValueError, TypeError):
|
|
103
|
+
pass
|
|
104
|
+
return ranges
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _extract_trace_meta(
|
|
108
|
+
trace: dict[str, Any],
|
|
109
|
+
file_path: str,
|
|
110
|
+
line_number: int,
|
|
111
|
+
) -> dict[str, Any]:
|
|
112
|
+
meta: dict[str, Any] = {"trace_id": trace.get("id")}
|
|
113
|
+
if trace.get("timestamp"):
|
|
114
|
+
meta["timestamp"] = trace["timestamp"]
|
|
115
|
+
tool = trace.get("tool")
|
|
116
|
+
if isinstance(tool, dict):
|
|
117
|
+
meta["tool"] = tool
|
|
118
|
+
files_data = trace.get("files") or []
|
|
119
|
+
matched_file = _find_matching_file(files_data, file_path)
|
|
120
|
+
if matched_file:
|
|
121
|
+
for conv in matched_file.get("conversations", []):
|
|
122
|
+
if not isinstance(conv, dict):
|
|
123
|
+
continue
|
|
124
|
+
contributor = conv.get("contributor") or {}
|
|
125
|
+
if contributor.get("model_id") and not meta.get("model_id"):
|
|
126
|
+
meta["model_id"] = contributor["model_id"]
|
|
127
|
+
if contributor.get("type") and not meta.get("contributor_type"):
|
|
128
|
+
meta["contributor_type"] = contributor["type"]
|
|
129
|
+
if conv.get("id") and not meta.get("conversation_id"):
|
|
130
|
+
meta["conversation_id"] = conv["id"]
|
|
131
|
+
if conv.get("content_sha256") and not meta.get("content_sha256"):
|
|
132
|
+
meta["content_sha256"] = conv["content_sha256"]
|
|
133
|
+
if meta.get("model_id") and meta.get("conversation_id"):
|
|
134
|
+
break
|
|
135
|
+
ranges = _collect_ranges(matched_file)
|
|
136
|
+
best = None
|
|
137
|
+
best_dist = float("inf")
|
|
138
|
+
for start, end in ranges:
|
|
139
|
+
if start <= line_number <= end:
|
|
140
|
+
span = end - start
|
|
141
|
+
if best is None or span < (best[1] - best[0]):
|
|
142
|
+
best = (start, end)
|
|
143
|
+
best_dist = 0
|
|
144
|
+
else:
|
|
145
|
+
dist = min(abs(line_number - start), abs(line_number - end))
|
|
146
|
+
if dist < best_dist:
|
|
147
|
+
best = (start, end)
|
|
148
|
+
best_dist = dist
|
|
149
|
+
if best:
|
|
150
|
+
meta["matched_range"] = {"start_line": best[0], "end_line": best[1]}
|
|
151
|
+
if not meta.get("model_id") or not meta.get("conversation_id"):
|
|
152
|
+
for fe in files_data:
|
|
153
|
+
if not isinstance(fe, dict) or fe is matched_file:
|
|
154
|
+
continue
|
|
155
|
+
for conv in fe.get("conversations", []):
|
|
156
|
+
if not isinstance(conv, dict):
|
|
157
|
+
continue
|
|
158
|
+
contributor = conv.get("contributor") or {}
|
|
159
|
+
if contributor.get("model_id") and not meta.get("model_id"):
|
|
160
|
+
meta["model_id"] = contributor["model_id"]
|
|
161
|
+
if conv.get("id") and not meta.get("conversation_id"):
|
|
162
|
+
meta["conversation_id"] = conv["id"]
|
|
163
|
+
if conv.get("content_sha256") and not meta.get("content_sha256"):
|
|
164
|
+
meta["content_sha256"] = conv["content_sha256"]
|
|
165
|
+
if meta.get("model_id") and meta.get("conversation_id"):
|
|
166
|
+
break
|
|
167
|
+
return meta
|