opencode-log 0.3.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.
- opencode_log/__init__.py +3 -0
- opencode_log/cache.py +240 -0
- opencode_log/cli.py +565 -0
- opencode_log/markdown.py +197 -0
- opencode_log/models.py +131 -0
- opencode_log/normalizer.py +146 -0
- opencode_log/render.py +360 -0
- opencode_log/storage.py +271 -0
- opencode_log/templates/combined.html +345 -0
- opencode_log/templates/components/base_styles.css +126 -0
- opencode_log/templates/components/edit_diff_styles.css +76 -0
- opencode_log/templates/components/filter_styles.css +168 -0
- opencode_log/templates/components/global_styles.css +237 -0
- opencode_log/templates/components/message_styles.css +1057 -0
- opencode_log/templates/components/page_nav_styles.css +79 -0
- opencode_log/templates/components/project_card_styles.css +138 -0
- opencode_log/templates/components/pygments_styles.css +218 -0
- opencode_log/templates/components/search.html +774 -0
- opencode_log/templates/components/search_inline.html +29 -0
- opencode_log/templates/components/search_inline_script.html +3 -0
- opencode_log/templates/components/search_results_panel.html +10 -0
- opencode_log/templates/components/search_styles.css +371 -0
- opencode_log/templates/components/session_nav.html +39 -0
- opencode_log/templates/components/session_nav_styles.css +106 -0
- opencode_log/templates/components/timeline.html +493 -0
- opencode_log/templates/components/timeline_styles.css +151 -0
- opencode_log/templates/components/timezone_converter.js +115 -0
- opencode_log/templates/components/todo_styles.css +186 -0
- opencode_log/templates/index.html +308 -0
- opencode_log/templates/transcript.html +372 -0
- opencode_log-0.3.0.dist-info/METADATA +519 -0
- opencode_log-0.3.0.dist-info/RECORD +35 -0
- opencode_log-0.3.0.dist-info/WHEEL +4 -0
- opencode_log-0.3.0.dist-info/entry_points.txt +2 -0
- opencode_log-0.3.0.dist-info/licenses/LICENSE +21 -0
opencode_log/__init__.py
ADDED
opencode_log/cache.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .models import Message, Session, SessionDiffItem, SessionInfo, TodoItem
|
|
8
|
+
|
|
9
|
+
CACHE_VERSION = 2
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _session_to_dict(session: Session) -> dict[str, Any]:
|
|
13
|
+
info = session.info
|
|
14
|
+
return {
|
|
15
|
+
"info": {
|
|
16
|
+
"id": info.id,
|
|
17
|
+
"project_id": info.project_id,
|
|
18
|
+
"directory": info.directory,
|
|
19
|
+
"title": info.title,
|
|
20
|
+
"slug": info.slug,
|
|
21
|
+
"version": info.version,
|
|
22
|
+
"created_ms": info.created_ms,
|
|
23
|
+
"updated_ms": info.updated_ms,
|
|
24
|
+
"additions": info.additions,
|
|
25
|
+
"deletions": info.deletions,
|
|
26
|
+
"files": info.files,
|
|
27
|
+
},
|
|
28
|
+
"messages": [
|
|
29
|
+
{
|
|
30
|
+
"id": m.id,
|
|
31
|
+
"session_id": m.session_id,
|
|
32
|
+
"role": m.role,
|
|
33
|
+
"created_ms": m.created_ms,
|
|
34
|
+
"completed_ms": m.completed_ms,
|
|
35
|
+
"model": m.model,
|
|
36
|
+
"provider": m.provider,
|
|
37
|
+
"mode": m.mode,
|
|
38
|
+
"agent": m.agent,
|
|
39
|
+
"cost": m.cost,
|
|
40
|
+
"tokens_input": m.tokens_input,
|
|
41
|
+
"tokens_output": m.tokens_output,
|
|
42
|
+
"tokens_reasoning": m.tokens_reasoning,
|
|
43
|
+
"tokens_cache_read": m.tokens_cache_read,
|
|
44
|
+
"tokens_cache_write": m.tokens_cache_write,
|
|
45
|
+
"finish": m.finish,
|
|
46
|
+
"error": m.error,
|
|
47
|
+
"parts": m.parts,
|
|
48
|
+
}
|
|
49
|
+
for m in session.messages
|
|
50
|
+
],
|
|
51
|
+
"todos": [
|
|
52
|
+
{
|
|
53
|
+
"id": item.id,
|
|
54
|
+
"content": item.content,
|
|
55
|
+
"status": item.status,
|
|
56
|
+
"priority": item.priority,
|
|
57
|
+
}
|
|
58
|
+
for item in session.todos
|
|
59
|
+
],
|
|
60
|
+
"diffs": [
|
|
61
|
+
{
|
|
62
|
+
"file": item.file,
|
|
63
|
+
"status": item.status,
|
|
64
|
+
"additions": item.additions,
|
|
65
|
+
"deletions": item.deletions,
|
|
66
|
+
}
|
|
67
|
+
for item in session.diffs
|
|
68
|
+
],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _session_from_dict(data: dict[str, Any]) -> Session | None:
|
|
73
|
+
info_data = data.get("info")
|
|
74
|
+
if not isinstance(info_data, dict):
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
info = SessionInfo(
|
|
78
|
+
id=str(info_data.get("id", "")),
|
|
79
|
+
project_id=str(info_data.get("project_id", "")),
|
|
80
|
+
directory=str(info_data.get("directory", "")),
|
|
81
|
+
title=str(info_data.get("title", "Untitled Session")),
|
|
82
|
+
slug=info_data.get("slug"),
|
|
83
|
+
version=info_data.get("version"),
|
|
84
|
+
created_ms=info_data.get("created_ms"),
|
|
85
|
+
updated_ms=info_data.get("updated_ms"),
|
|
86
|
+
additions=int(info_data.get("additions", 0) or 0),
|
|
87
|
+
deletions=int(info_data.get("deletions", 0) or 0),
|
|
88
|
+
files=int(info_data.get("files", 0) or 0),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
raw_messages = data.get("messages")
|
|
92
|
+
if not isinstance(raw_messages, list):
|
|
93
|
+
return Session(info=info, messages=[])
|
|
94
|
+
|
|
95
|
+
messages: list[Message] = []
|
|
96
|
+
for item in raw_messages:
|
|
97
|
+
if not isinstance(item, dict):
|
|
98
|
+
continue
|
|
99
|
+
raw_parts = item.get("parts")
|
|
100
|
+
parts = raw_parts if isinstance(raw_parts, list) else []
|
|
101
|
+
messages.append(
|
|
102
|
+
Message(
|
|
103
|
+
id=str(item.get("id", "")),
|
|
104
|
+
session_id=str(item.get("session_id", info.id)),
|
|
105
|
+
role=str(item.get("role", "unknown")),
|
|
106
|
+
created_ms=item.get("created_ms"),
|
|
107
|
+
completed_ms=item.get("completed_ms"),
|
|
108
|
+
model=item.get("model"),
|
|
109
|
+
provider=item.get("provider"),
|
|
110
|
+
mode=item.get("mode"),
|
|
111
|
+
agent=item.get("agent"),
|
|
112
|
+
cost=float(item.get("cost", 0.0) or 0.0),
|
|
113
|
+
tokens_input=int(item.get("tokens_input", 0) or 0),
|
|
114
|
+
tokens_output=int(item.get("tokens_output", 0) or 0),
|
|
115
|
+
tokens_reasoning=int(item.get("tokens_reasoning", 0) or 0),
|
|
116
|
+
tokens_cache_read=int(item.get("tokens_cache_read", 0) or 0),
|
|
117
|
+
tokens_cache_write=int(item.get("tokens_cache_write", 0) or 0),
|
|
118
|
+
finish=item.get("finish"),
|
|
119
|
+
error=item.get("error"),
|
|
120
|
+
parts=parts,
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
todos: list[TodoItem] = []
|
|
124
|
+
for item in data.get("todos", []):
|
|
125
|
+
if not isinstance(item, dict):
|
|
126
|
+
continue
|
|
127
|
+
todos.append(
|
|
128
|
+
TodoItem(
|
|
129
|
+
id=str(item.get("id", "")),
|
|
130
|
+
content=str(item.get("content", "")),
|
|
131
|
+
status=str(item.get("status", "pending")),
|
|
132
|
+
priority=str(item.get("priority", "medium")),
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
diffs: list[SessionDiffItem] = []
|
|
137
|
+
for item in data.get("diffs", []):
|
|
138
|
+
if not isinstance(item, dict):
|
|
139
|
+
continue
|
|
140
|
+
diffs.append(
|
|
141
|
+
SessionDiffItem(
|
|
142
|
+
file=str(item.get("file", "")),
|
|
143
|
+
status=str(item.get("status", "modified")),
|
|
144
|
+
additions=int(item.get("additions", 0) or 0),
|
|
145
|
+
deletions=int(item.get("deletions", 0) or 0),
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return Session(info=info, messages=messages, todos=todos, diffs=diffs)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class CacheManager:
|
|
153
|
+
def __init__(self, cache_dir: Path):
|
|
154
|
+
self.cache_dir = cache_dir
|
|
155
|
+
self.sessions_dir = cache_dir / "sessions"
|
|
156
|
+
self.state_path = cache_dir / "state.json"
|
|
157
|
+
self.data: dict[str, Any] = {
|
|
158
|
+
"version": CACHE_VERSION,
|
|
159
|
+
"session_cache": {},
|
|
160
|
+
"render_cache": {},
|
|
161
|
+
}
|
|
162
|
+
self._load()
|
|
163
|
+
|
|
164
|
+
def _load(self) -> None:
|
|
165
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
166
|
+
self.sessions_dir.mkdir(parents=True, exist_ok=True)
|
|
167
|
+
if not self.state_path.exists():
|
|
168
|
+
return
|
|
169
|
+
try:
|
|
170
|
+
payload = json.loads(self.state_path.read_text(encoding="utf-8"))
|
|
171
|
+
if isinstance(payload, dict) and payload.get("version") == CACHE_VERSION:
|
|
172
|
+
self.data = payload
|
|
173
|
+
except Exception:
|
|
174
|
+
self.data = {
|
|
175
|
+
"version": CACHE_VERSION,
|
|
176
|
+
"session_cache": {},
|
|
177
|
+
"render_cache": {},
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
def save(self) -> None:
|
|
181
|
+
self.state_path.write_text(
|
|
182
|
+
json.dumps(self.data, ensure_ascii=False, indent=2),
|
|
183
|
+
encoding="utf-8",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def clear(self) -> None:
|
|
187
|
+
self.data = {
|
|
188
|
+
"version": CACHE_VERSION,
|
|
189
|
+
"session_cache": {},
|
|
190
|
+
"render_cache": {},
|
|
191
|
+
}
|
|
192
|
+
self.save()
|
|
193
|
+
|
|
194
|
+
def get_session(self, session_id: str, updated_ms: int | None) -> Session | None:
|
|
195
|
+
entries = self.data.get("session_cache")
|
|
196
|
+
if not isinstance(entries, dict):
|
|
197
|
+
return None
|
|
198
|
+
entry = entries.get(session_id)
|
|
199
|
+
if not isinstance(entry, dict):
|
|
200
|
+
return None
|
|
201
|
+
if entry.get("updated_ms") != updated_ms:
|
|
202
|
+
return None
|
|
203
|
+
path = self.sessions_dir / f"{session_id}.json"
|
|
204
|
+
if not path.exists():
|
|
205
|
+
return None
|
|
206
|
+
try:
|
|
207
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
208
|
+
if not isinstance(raw, dict):
|
|
209
|
+
return None
|
|
210
|
+
return _session_from_dict(raw)
|
|
211
|
+
except Exception:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
def set_session(self, session: Session) -> None:
|
|
215
|
+
path = self.sessions_dir / f"{session.info.id}.json"
|
|
216
|
+
path.write_text(
|
|
217
|
+
json.dumps(_session_to_dict(session), ensure_ascii=False),
|
|
218
|
+
encoding="utf-8",
|
|
219
|
+
)
|
|
220
|
+
entries = self.data.setdefault("session_cache", {})
|
|
221
|
+
if isinstance(entries, dict):
|
|
222
|
+
entries[session.info.id] = {
|
|
223
|
+
"updated_ms": session.info.updated_ms,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
def should_render(self, key: str, signature: str, out_path: Path) -> bool:
|
|
227
|
+
cache = self.data.get("render_cache")
|
|
228
|
+
if not isinstance(cache, dict):
|
|
229
|
+
return True
|
|
230
|
+
existing = cache.get(key)
|
|
231
|
+
if not out_path.exists():
|
|
232
|
+
return True
|
|
233
|
+
if not isinstance(existing, dict):
|
|
234
|
+
return True
|
|
235
|
+
return existing.get("signature") != signature
|
|
236
|
+
|
|
237
|
+
def mark_rendered(self, key: str, signature: str) -> None:
|
|
238
|
+
cache = self.data.setdefault("render_cache", {})
|
|
239
|
+
if isinstance(cache, dict):
|
|
240
|
+
cache[key] = {"signature": signature}
|