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.
Files changed (89) hide show
  1. agent_decoder-0.3.2.dist-info/METADATA +387 -0
  2. agent_decoder-0.3.2.dist-info/RECORD +89 -0
  3. agent_decoder-0.3.2.dist-info/WHEEL +4 -0
  4. agent_decoder-0.3.2.dist-info/entry_points.txt +2 -0
  5. agent_decoder-0.3.2.dist-info/licenses/LICENSE +111 -0
  6. clau_decode/__init__.py +8 -0
  7. clau_decode/__main__.py +11 -0
  8. clau_decode/_auth_env.py +118 -0
  9. clau_decode/_pty_preexec.py +42 -0
  10. clau_decode/analytics/__init__.py +0 -0
  11. clau_decode/analytics/aggregator.py +55 -0
  12. clau_decode/analytics/cost.py +76 -0
  13. clau_decode/analytics/extractor.py +32 -0
  14. clau_decode/analytics/fast.py +289 -0
  15. clau_decode/analytics/models.py +49 -0
  16. clau_decode/analytics/pricing.py +162 -0
  17. clau_decode/analytics/prompt.py +34 -0
  18. clau_decode/analytics/service.py +23 -0
  19. clau_decode/analytics/stats.py +117 -0
  20. clau_decode/analytics/tips.py +176 -0
  21. clau_decode/btw_capture.py +290 -0
  22. clau_decode/cli.py +504 -0
  23. clau_decode/config.py +144 -0
  24. clau_decode/db.py +2054 -0
  25. clau_decode/driver_manager.py +387 -0
  26. clau_decode/drivers/__init__.py +27 -0
  27. clau_decode/drivers/base.py +170 -0
  28. clau_decode/drivers/registry.py +113 -0
  29. clau_decode/drivers/tmux_driver.py +488 -0
  30. clau_decode/editor.py +182 -0
  31. clau_decode/events_bus.py +69 -0
  32. clau_decode/locks.py +388 -0
  33. clau_decode/migrate.py +1313 -0
  34. clau_decode/models.py +285 -0
  35. clau_decode/parser.py +466 -0
  36. clau_decode/providers/__init__.py +30 -0
  37. clau_decode/providers/base.py +132 -0
  38. clau_decode/providers/claude.py +81 -0
  39. clau_decode/providers/codex.py +441 -0
  40. clau_decode/providers/registry.py +89 -0
  41. clau_decode/pty_native.py +39 -0
  42. clau_decode/pty_runner.py +2406 -0
  43. clau_decode/pty_screen_state.py +65 -0
  44. clau_decode/recap_runner.py +230 -0
  45. clau_decode/reporter.py +305 -0
  46. clau_decode/scanner.py +125 -0
  47. clau_decode/server.py +2841 -0
  48. clau_decode/static/android-chrome-192x192.png +0 -0
  49. clau_decode/static/android-chrome-512x512.png +0 -0
  50. clau_decode/static/app-icon.png +0 -0
  51. clau_decode/static/apple-touch-icon.png +0 -0
  52. clau_decode/static/assets/ChatView-BBVXuITs.js +79 -0
  53. clau_decode/static/assets/ChatView-BrP-ENHg.css +1 -0
  54. clau_decode/static/assets/Dashboard-3CYO4HQw.js +1 -0
  55. clau_decode/static/assets/FileViewer-BwbDc3ky.js +1 -0
  56. clau_decode/static/assets/HelpPopup-CPtECjYw.js +1 -0
  57. clau_decode/static/assets/ScrollContainer-BT_Dzxah.js +1 -0
  58. clau_decode/static/assets/SearchOverlay-DqI7aZaC.js +1 -0
  59. clau_decode/static/assets/SessionItem-DlyKg80u.js +1 -0
  60. clau_decode/static/assets/SettingsModal-CZBfMqx7.js +1 -0
  61. clau_decode/static/assets/ShortcutsPopup-W7CPdCzW.js +1 -0
  62. clau_decode/static/assets/Sidebar-BBswhEua.js +1 -0
  63. clau_decode/static/assets/client-DTSXvAb8.js +1 -0
  64. clau_decode/static/assets/fira-code-DGosTW8U.woff2 +0 -0
  65. clau_decode/static/assets/formatRelative-j86webkr.js +1 -0
  66. clau_decode/static/assets/hljs-B3O74O7Z.js +35 -0
  67. clau_decode/static/assets/index-BHdCOIkq.css +10 -0
  68. clau_decode/static/assets/index-DfKltZ6z.js +5 -0
  69. clau_decode/static/assets/ioskeley-mono-C7VR2-sL.woff2 +0 -0
  70. clau_decode/static/assets/jetbrains-mono-V6pRDFza.woff2 +0 -0
  71. clau_decode/static/assets/julia-mono-Cbvba6sW.woff2 +0 -0
  72. clau_decode/static/assets/libertinus-mono-Coz17E-O.woff2 +0 -0
  73. clau_decode/static/assets/monaspace-argon-RS-2TGNg.woff2 +0 -0
  74. clau_decode/static/assets/nativePtyFonts-BaXKtUsn.js +1 -0
  75. clau_decode/static/assets/react-Bi_azaFt.js +9 -0
  76. clau_decode/static/assets/renderSnippet-CqpQQugH.js +1 -0
  77. clau_decode/static/assets/rolldown-runtime-S-ySWqyJ.js +1 -0
  78. clau_decode/static/assets/shared-DdVGprvF.js +1 -0
  79. clau_decode/static/assets/source-code-pro-DEXWLoHG.woff2 +0 -0
  80. clau_decode/static/assets/spline-sans-mono-mUpA6Mve.woff2 +0 -0
  81. clau_decode/static/assets/ui-BcGPjpYn.js +1 -0
  82. clau_decode/static/assets/xanh-mono-Dsg19vWS.woff2 +0 -0
  83. clau_decode/static/favicon-16x16.png +0 -0
  84. clau_decode/static/favicon-32x32.png +0 -0
  85. clau_decode/static/favicon.ico +0 -0
  86. clau_decode/static/index.html +24 -0
  87. clau_decode/static/site.webmanifest +1 -0
  88. clau_decode/update_check.py +51 -0
  89. 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]