agent-decoder 0.3.2__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_decoder-0.3.2.dist-info/METADATA +387 -0
- agent_decoder-0.3.2.dist-info/RECORD +89 -0
- agent_decoder-0.3.2.dist-info/WHEEL +4 -0
- agent_decoder-0.3.2.dist-info/entry_points.txt +2 -0
- agent_decoder-0.3.2.dist-info/licenses/LICENSE +111 -0
- clau_decode/__init__.py +8 -0
- clau_decode/__main__.py +11 -0
- clau_decode/_auth_env.py +118 -0
- clau_decode/_pty_preexec.py +42 -0
- clau_decode/analytics/__init__.py +0 -0
- clau_decode/analytics/aggregator.py +55 -0
- clau_decode/analytics/cost.py +76 -0
- clau_decode/analytics/extractor.py +32 -0
- clau_decode/analytics/fast.py +289 -0
- clau_decode/analytics/models.py +49 -0
- clau_decode/analytics/pricing.py +162 -0
- clau_decode/analytics/prompt.py +34 -0
- clau_decode/analytics/service.py +23 -0
- clau_decode/analytics/stats.py +117 -0
- clau_decode/analytics/tips.py +176 -0
- clau_decode/btw_capture.py +290 -0
- clau_decode/cli.py +504 -0
- clau_decode/config.py +144 -0
- clau_decode/db.py +2054 -0
- clau_decode/driver_manager.py +387 -0
- clau_decode/drivers/__init__.py +27 -0
- clau_decode/drivers/base.py +170 -0
- clau_decode/drivers/registry.py +113 -0
- clau_decode/drivers/tmux_driver.py +488 -0
- clau_decode/editor.py +182 -0
- clau_decode/events_bus.py +69 -0
- clau_decode/locks.py +388 -0
- clau_decode/migrate.py +1313 -0
- clau_decode/models.py +285 -0
- clau_decode/parser.py +466 -0
- clau_decode/providers/__init__.py +30 -0
- clau_decode/providers/base.py +132 -0
- clau_decode/providers/claude.py +81 -0
- clau_decode/providers/codex.py +441 -0
- clau_decode/providers/registry.py +89 -0
- clau_decode/pty_native.py +39 -0
- clau_decode/pty_runner.py +2406 -0
- clau_decode/pty_screen_state.py +65 -0
- clau_decode/recap_runner.py +230 -0
- clau_decode/reporter.py +305 -0
- clau_decode/scanner.py +125 -0
- clau_decode/server.py +2841 -0
- clau_decode/static/android-chrome-192x192.png +0 -0
- clau_decode/static/android-chrome-512x512.png +0 -0
- clau_decode/static/app-icon.png +0 -0
- clau_decode/static/apple-touch-icon.png +0 -0
- clau_decode/static/assets/ChatView-BBVXuITs.js +79 -0
- clau_decode/static/assets/ChatView-BrP-ENHg.css +1 -0
- clau_decode/static/assets/Dashboard-3CYO4HQw.js +1 -0
- clau_decode/static/assets/FileViewer-BwbDc3ky.js +1 -0
- clau_decode/static/assets/HelpPopup-CPtECjYw.js +1 -0
- clau_decode/static/assets/ScrollContainer-BT_Dzxah.js +1 -0
- clau_decode/static/assets/SearchOverlay-DqI7aZaC.js +1 -0
- clau_decode/static/assets/SessionItem-DlyKg80u.js +1 -0
- clau_decode/static/assets/SettingsModal-CZBfMqx7.js +1 -0
- clau_decode/static/assets/ShortcutsPopup-W7CPdCzW.js +1 -0
- clau_decode/static/assets/Sidebar-BBswhEua.js +1 -0
- clau_decode/static/assets/client-DTSXvAb8.js +1 -0
- clau_decode/static/assets/fira-code-DGosTW8U.woff2 +0 -0
- clau_decode/static/assets/formatRelative-j86webkr.js +1 -0
- clau_decode/static/assets/hljs-B3O74O7Z.js +35 -0
- clau_decode/static/assets/index-BHdCOIkq.css +10 -0
- clau_decode/static/assets/index-DfKltZ6z.js +5 -0
- clau_decode/static/assets/ioskeley-mono-C7VR2-sL.woff2 +0 -0
- clau_decode/static/assets/jetbrains-mono-V6pRDFza.woff2 +0 -0
- clau_decode/static/assets/julia-mono-Cbvba6sW.woff2 +0 -0
- clau_decode/static/assets/libertinus-mono-Coz17E-O.woff2 +0 -0
- clau_decode/static/assets/monaspace-argon-RS-2TGNg.woff2 +0 -0
- clau_decode/static/assets/nativePtyFonts-BaXKtUsn.js +1 -0
- clau_decode/static/assets/react-Bi_azaFt.js +9 -0
- clau_decode/static/assets/renderSnippet-CqpQQugH.js +1 -0
- clau_decode/static/assets/rolldown-runtime-S-ySWqyJ.js +1 -0
- clau_decode/static/assets/shared-DdVGprvF.js +1 -0
- clau_decode/static/assets/source-code-pro-DEXWLoHG.woff2 +0 -0
- clau_decode/static/assets/spline-sans-mono-mUpA6Mve.woff2 +0 -0
- clau_decode/static/assets/ui-BcGPjpYn.js +1 -0
- clau_decode/static/assets/xanh-mono-Dsg19vWS.woff2 +0 -0
- clau_decode/static/favicon-16x16.png +0 -0
- clau_decode/static/favicon-32x32.png +0 -0
- clau_decode/static/favicon.ico +0 -0
- clau_decode/static/index.html +24 -0
- clau_decode/static/site.webmanifest +1 -0
- clau_decode/update_check.py +51 -0
- clau_decode/watcher.py +63 -0
clau_decode/models.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""Shared domain models — the single source of truth for data shapes across the app."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, Literal, Optional, Union
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# Content blocks — mirrors API content block types
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TextBlock(BaseModel):
|
|
17
|
+
type: Literal["text"] = "text"
|
|
18
|
+
text: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ToolUseBlock(BaseModel):
|
|
22
|
+
type: Literal["tool_use"] = "tool_use"
|
|
23
|
+
id: str
|
|
24
|
+
name: str
|
|
25
|
+
input: dict[str, Any] = Field(default_factory=dict)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ToolResultBlock(BaseModel):
|
|
29
|
+
type: Literal["tool_result"] = "tool_result"
|
|
30
|
+
tool_use_id: str
|
|
31
|
+
content: Union[str, list[dict[str, Any]], None] = None
|
|
32
|
+
is_error: bool = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ThinkingBlock(BaseModel):
|
|
36
|
+
type: Literal["thinking"] = "thinking"
|
|
37
|
+
thinking: str
|
|
38
|
+
signature: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ImageBlock(BaseModel):
|
|
42
|
+
type: Literal["image"] = "image"
|
|
43
|
+
source: dict[str, Any] = Field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
ContentBlock = Union[
|
|
47
|
+
TextBlock, ToolUseBlock, ToolResultBlock, ThinkingBlock, ImageBlock
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Token usage — from API assistant messages
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TokenUsage(BaseModel):
|
|
57
|
+
input_tokens: int = 0
|
|
58
|
+
output_tokens: int = 0
|
|
59
|
+
cache_creation_input_tokens: int = 0
|
|
60
|
+
cache_read_input_tokens: int = 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Message — one turn in the conversation
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Message(BaseModel):
|
|
69
|
+
id: str
|
|
70
|
+
session_id: str
|
|
71
|
+
parent_id: Optional[str] = None
|
|
72
|
+
role: Literal["user", "assistant", "system"]
|
|
73
|
+
content_blocks: list[ContentBlock] = Field(default_factory=list)
|
|
74
|
+
timestamp: Optional[datetime] = None
|
|
75
|
+
model: Optional[str] = None
|
|
76
|
+
is_sidechain: bool = False
|
|
77
|
+
is_meta: bool = False
|
|
78
|
+
cwd: Optional[str] = None
|
|
79
|
+
git_branch: Optional[str] = None
|
|
80
|
+
request_id: Optional[str] = None
|
|
81
|
+
# Links sub-agent messages to the tool_use that spawned them
|
|
82
|
+
source_tool_assistant_uuid: Optional[str] = None
|
|
83
|
+
usage: Optional[TokenUsage] = None
|
|
84
|
+
provider: str = "claude"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class MessageTree(BaseModel):
|
|
88
|
+
"""A message with its direct children attached (for sidechain branches)."""
|
|
89
|
+
|
|
90
|
+
message: Message
|
|
91
|
+
children: list["MessageTree"] = Field(default_factory=list)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Session — one JSONL file
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Session(BaseModel):
|
|
100
|
+
id: str
|
|
101
|
+
project_id: str
|
|
102
|
+
file_path: str
|
|
103
|
+
title: Optional[str] = None
|
|
104
|
+
# Server-side rename override (issue #11). Frontend overlays this on top
|
|
105
|
+
# of `title` so the original parsed title stays available for display
|
|
106
|
+
# fallback and search debugging.
|
|
107
|
+
custom_title: Optional[str] = None
|
|
108
|
+
# Server-persisted session flags (previously localStorage-only on the FE).
|
|
109
|
+
# ISO-8601 string when set, None when unset. See Database.set_archived /
|
|
110
|
+
# set_starred / set_viewed_at + db._migrate_add_session_meta_flags.
|
|
111
|
+
archived_at: Optional[str] = None
|
|
112
|
+
starred_at: Optional[str] = None
|
|
113
|
+
viewed_at: Optional[str] = None
|
|
114
|
+
model: Optional[str] = None
|
|
115
|
+
started_at: Optional[datetime] = None
|
|
116
|
+
updated_at: Optional[datetime] = None
|
|
117
|
+
message_count: int = 0
|
|
118
|
+
user_message_count: int = 0
|
|
119
|
+
cwd: Optional[str] = None
|
|
120
|
+
git_branch: Optional[str] = None
|
|
121
|
+
is_worktree: bool = False
|
|
122
|
+
is_fork: bool = False
|
|
123
|
+
permission_mode: Optional[str] = None
|
|
124
|
+
provider: str = "claude"
|
|
125
|
+
last_message_role: Optional[Literal["user", "assistant", "system"]] = None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class SessionDetail(Session):
|
|
129
|
+
"""Session with its full conversation included."""
|
|
130
|
+
|
|
131
|
+
messages: list[Message] = Field(default_factory=list)
|
|
132
|
+
total_message_count: Optional[int] = None # set when messages are truncated
|
|
133
|
+
# Resolved from the session's project ``resolved_path`` in the DB.
|
|
134
|
+
# True if the cwd (working directory) still exists on disk; False if
|
|
135
|
+
# the project directory has been deleted since the session was last
|
|
136
|
+
# scanned. The FE uses this to short-circuit submit with a clear
|
|
137
|
+
# error rather than letting the PTY spawn fail downstream.
|
|
138
|
+
cwd_exists: bool = True
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# Project — one project directory (may contain many sessions)
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class Project(BaseModel):
|
|
147
|
+
id: str # slug derived from directory path
|
|
148
|
+
display_name: str # human-readable name
|
|
149
|
+
raw_path: str # the mangled path string from directory name
|
|
150
|
+
resolved_path: Optional[str] = None # actual filesystem path if it exists
|
|
151
|
+
data_source: str = "" # which configured root path this came from
|
|
152
|
+
session_count: int = 0
|
|
153
|
+
last_activity_at: Optional[datetime] = None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# Search
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class SearchHit(BaseModel):
|
|
162
|
+
session_id: str
|
|
163
|
+
session_title: Optional[str]
|
|
164
|
+
project_id: str
|
|
165
|
+
message_id: str
|
|
166
|
+
role: str
|
|
167
|
+
snippet: str # FTS5 highlighted excerpt
|
|
168
|
+
timestamp: Optional[datetime]
|
|
169
|
+
# Distinguishes regular messages from ephemeral (/btw) exchanges.
|
|
170
|
+
source: Literal["message", "ephemeral"] = "message"
|
|
171
|
+
# Only set for ephemeral hits: the kind tag (e.g. "btw") and the
|
|
172
|
+
# responds_to id linking user/assistant pairs.
|
|
173
|
+
kind: Optional[str] = None
|
|
174
|
+
responds_to: Optional[int] = None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
# Config — user-facing settings
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class Profile(BaseModel):
|
|
183
|
+
id: str = Field(default_factory=lambda: __import__("uuid").uuid4().hex[:12])
|
|
184
|
+
name: str = Field(..., min_length=1)
|
|
185
|
+
data_paths: list[str] = Field(default_factory=lambda: ["~/.claude"])
|
|
186
|
+
color: str = "#b8956a"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class AppConfig(BaseModel):
|
|
190
|
+
data_paths: list[str] = Field(
|
|
191
|
+
default_factory=lambda: ["~/.claude"],
|
|
192
|
+
description="Root directories to scan (legacy, used when profiles is empty)",
|
|
193
|
+
)
|
|
194
|
+
profiles: list[Profile] = Field(default_factory=list)
|
|
195
|
+
active_profile_id: Optional[str] = None
|
|
196
|
+
theme: Literal["light", "dark", "system"] = "system"
|
|
197
|
+
auto_open_browser: bool = True
|
|
198
|
+
port: int = 4242
|
|
199
|
+
host: str = "127.0.0.1"
|
|
200
|
+
edit_enabled: bool = True
|
|
201
|
+
# Show a small per-provider tag on non-Claude sidebar rows. Off by default
|
|
202
|
+
# — opt-in, since a tag on every chat adds clutter as more CLIs integrate.
|
|
203
|
+
show_provider_tag: bool = False
|
|
204
|
+
claude_default_permission_mode: str = "default"
|
|
205
|
+
chat_send_shortcut: Literal["enter", "modEnter"] = "enter"
|
|
206
|
+
native_pty_font_family: Literal[
|
|
207
|
+
"monaspace-argon",
|
|
208
|
+
"source-code-pro",
|
|
209
|
+
"fira-code",
|
|
210
|
+
"jetbrains-mono",
|
|
211
|
+
"ioskeley-mono",
|
|
212
|
+
"libertinus-mono",
|
|
213
|
+
"antithesis",
|
|
214
|
+
"thesansmono-condensed",
|
|
215
|
+
"xanh-mono",
|
|
216
|
+
"julia-mono",
|
|
217
|
+
"spline-sans-mono",
|
|
218
|
+
"system-monospace",
|
|
219
|
+
] = "monaspace-argon"
|
|
220
|
+
native_pty_cols: int = Field(
|
|
221
|
+
default=100,
|
|
222
|
+
ge=20,
|
|
223
|
+
le=400,
|
|
224
|
+
description=(
|
|
225
|
+
"Native PTY width in terminal columns. Single source of truth: the "
|
|
226
|
+
"PTY is spawned at this width (TIOCSWINSZ) and the browser terminal "
|
|
227
|
+
"renders at the same width. The terminal never reflows its width, so "
|
|
228
|
+
"Claude's output is displayed exactly as a native session would."
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
claude_auto_stop_quiet_default_turns: bool = False
|
|
232
|
+
claude_recap_enabled: bool = False
|
|
233
|
+
claude_recap_idle_minutes: int = 5
|
|
234
|
+
codex_data_paths: list[str] = Field(
|
|
235
|
+
default_factory=lambda: ["~/.codex/sessions"],
|
|
236
|
+
description="Root directories to scan for Codex CLI session files (rollout-*.jsonl)",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def get_all_scan_paths(self) -> list[str]:
|
|
240
|
+
"""Collect all data_paths from all profiles (deduplicated, expanded)."""
|
|
241
|
+
from pathlib import Path
|
|
242
|
+
|
|
243
|
+
if self.profiles:
|
|
244
|
+
seen: set[str] = set()
|
|
245
|
+
result: list[str] = []
|
|
246
|
+
for p in self.profiles:
|
|
247
|
+
for dp in p.data_paths:
|
|
248
|
+
expanded = str(Path(dp).expanduser())
|
|
249
|
+
if expanded not in seen:
|
|
250
|
+
result.append(dp)
|
|
251
|
+
seen.add(expanded)
|
|
252
|
+
return result or self.data_paths
|
|
253
|
+
return self.data_paths
|
|
254
|
+
|
|
255
|
+
def get_active_data_sources(self) -> list[str] | None:
|
|
256
|
+
"""Return expanded paths for the active profile, or None for all.
|
|
257
|
+
|
|
258
|
+
Codex data paths are always appended to the active profile's sources:
|
|
259
|
+
profiles partition *Claude* history (e.g. personal vs work), but Codex
|
|
260
|
+
is not part of that partition, so its sessions must stay visible no
|
|
261
|
+
matter which Claude profile is active. Without this, an active profile
|
|
262
|
+
filters out every Codex project (whose ``data_source`` is the ~/.codex
|
|
263
|
+
root) and Codex sessions vanish from the list/search/analytics.
|
|
264
|
+
"""
|
|
265
|
+
from pathlib import Path
|
|
266
|
+
|
|
267
|
+
if not self.profiles or not self.active_profile_id:
|
|
268
|
+
return None
|
|
269
|
+
codex = [str(Path(dp).expanduser()) for dp in self.codex_data_paths]
|
|
270
|
+
for p in self.profiles:
|
|
271
|
+
if p.id == self.active_profile_id:
|
|
272
|
+
return [str(Path(dp).expanduser()) for dp in p.data_paths] + codex
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# ---------------------------------------------------------------------------
|
|
277
|
+
# API response envelopes
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class StatsResponse(BaseModel):
|
|
282
|
+
total_projects: int
|
|
283
|
+
total_sessions: int
|
|
284
|
+
total_messages: int
|
|
285
|
+
data_paths: list[str]
|