overcode 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 (43) hide show
  1. overcode/__init__.py +5 -0
  2. overcode/cli.py +812 -0
  3. overcode/config.py +72 -0
  4. overcode/daemon.py +1184 -0
  5. overcode/daemon_claude_skill.md +180 -0
  6. overcode/daemon_state.py +113 -0
  7. overcode/data_export.py +257 -0
  8. overcode/dependency_check.py +227 -0
  9. overcode/exceptions.py +219 -0
  10. overcode/history_reader.py +448 -0
  11. overcode/implementations.py +214 -0
  12. overcode/interfaces.py +49 -0
  13. overcode/launcher.py +434 -0
  14. overcode/logging_config.py +193 -0
  15. overcode/mocks.py +152 -0
  16. overcode/monitor_daemon.py +808 -0
  17. overcode/monitor_daemon_state.py +358 -0
  18. overcode/pid_utils.py +225 -0
  19. overcode/presence_logger.py +454 -0
  20. overcode/protocols.py +143 -0
  21. overcode/session_manager.py +606 -0
  22. overcode/settings.py +412 -0
  23. overcode/standing_instructions.py +276 -0
  24. overcode/status_constants.py +190 -0
  25. overcode/status_detector.py +339 -0
  26. overcode/status_history.py +164 -0
  27. overcode/status_patterns.py +264 -0
  28. overcode/summarizer_client.py +136 -0
  29. overcode/summarizer_component.py +312 -0
  30. overcode/supervisor_daemon.py +1000 -0
  31. overcode/supervisor_layout.sh +50 -0
  32. overcode/tmux_manager.py +228 -0
  33. overcode/tui.py +2549 -0
  34. overcode/tui_helpers.py +495 -0
  35. overcode/web_api.py +279 -0
  36. overcode/web_server.py +138 -0
  37. overcode/web_templates.py +563 -0
  38. overcode-0.1.0.dist-info/METADATA +87 -0
  39. overcode-0.1.0.dist-info/RECORD +43 -0
  40. overcode-0.1.0.dist-info/WHEEL +5 -0
  41. overcode-0.1.0.dist-info/entry_points.txt +2 -0
  42. overcode-0.1.0.dist-info/licenses/LICENSE +21 -0
  43. overcode-0.1.0.dist-info/top_level.txt +1 -0
overcode/web_api.py ADDED
@@ -0,0 +1,279 @@
1
+ """
2
+ API data handlers for web server.
3
+
4
+ Reuses existing helpers from tui_helpers.py and reads from Monitor Daemon state.
5
+ """
6
+
7
+ from datetime import datetime, timedelta
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ from .monitor_daemon_state import (
11
+ get_monitor_daemon_state,
12
+ MonitorDaemonState,
13
+ SessionDaemonState,
14
+ )
15
+ from .status_history import read_agent_status_history
16
+ from .tui_helpers import (
17
+ format_duration,
18
+ format_tokens,
19
+ build_timeline_slots,
20
+ calculate_uptime,
21
+ get_git_diff_stats,
22
+ )
23
+ from .status_constants import (
24
+ get_status_emoji,
25
+ get_status_color,
26
+ AGENT_TIMELINE_CHARS,
27
+ PRESENCE_TIMELINE_CHARS,
28
+ )
29
+
30
+
31
+ # CSS color values for web (Rich/Textual colors -> CSS hex)
32
+ WEB_COLORS = {
33
+ "green": "#22c55e",
34
+ "yellow": "#eab308",
35
+ "orange1": "#f97316",
36
+ "red": "#ef4444",
37
+ "dim": "#6b7280",
38
+ "cyan": "#06b6d4",
39
+ }
40
+
41
+
42
+ def get_web_color(status_color: str) -> str:
43
+ """Convert Rich color name to CSS hex color."""
44
+ return WEB_COLORS.get(status_color, "#6b7280")
45
+
46
+
47
+ def get_status_data(tmux_session: str) -> Dict[str, Any]:
48
+ """Get current status data for all agents.
49
+
50
+ Args:
51
+ tmux_session: tmux session name to monitor
52
+
53
+ Returns:
54
+ Dictionary with daemon info, summary, and per-agent data
55
+ """
56
+ state = get_monitor_daemon_state(tmux_session)
57
+ now = datetime.now()
58
+
59
+ result = {
60
+ "timestamp": now.isoformat(),
61
+ "daemon": _build_daemon_info(state),
62
+ "presence": _build_presence_info(state),
63
+ "summary": _build_summary(state),
64
+ "agents": [],
65
+ }
66
+
67
+ if state:
68
+ for s in state.sessions:
69
+ result["agents"].append(_build_agent_info(s, now))
70
+
71
+ return result
72
+
73
+
74
+ def _build_daemon_info(state: Optional[MonitorDaemonState]) -> Dict[str, Any]:
75
+ """Build daemon status information."""
76
+ if state is None:
77
+ return {
78
+ "running": False,
79
+ "status": "stopped",
80
+ "loop_count": 0,
81
+ "interval": 0,
82
+ "last_loop": None,
83
+ "supervisor_claude_running": False,
84
+ }
85
+
86
+ running = not state.is_stale()
87
+
88
+ return {
89
+ "running": running,
90
+ "status": state.status if running else "stopped",
91
+ "loop_count": state.loop_count,
92
+ "interval": state.current_interval,
93
+ "last_loop": state.last_loop_time,
94
+ "supervisor_claude_running": state.supervisor_claude_running,
95
+ "summarizer_enabled": state.summarizer_enabled,
96
+ "summarizer_available": state.summarizer_available,
97
+ "summarizer_calls": state.summarizer_calls,
98
+ "summarizer_cost_usd": state.summarizer_cost_usd,
99
+ }
100
+
101
+
102
+ def _build_presence_info(state: Optional[MonitorDaemonState]) -> Dict[str, Any]:
103
+ """Build presence information."""
104
+ if not state or not state.presence_available:
105
+ return {"available": False}
106
+
107
+ state_names = {1: "locked", 2: "inactive", 3: "active"}
108
+ return {
109
+ "available": True,
110
+ "state": state.presence_state,
111
+ "state_name": state_names.get(state.presence_state, "unknown"),
112
+ "idle_seconds": state.presence_idle_seconds or 0,
113
+ }
114
+
115
+
116
+ def _build_summary(state: Optional[MonitorDaemonState]) -> Dict[str, Any]:
117
+ """Build summary statistics."""
118
+ if not state:
119
+ return {
120
+ "total_agents": 0,
121
+ "green_agents": 0,
122
+ "total_green_time": 0,
123
+ "total_non_green_time": 0,
124
+ }
125
+
126
+ return {
127
+ "total_agents": len(state.sessions),
128
+ "green_agents": state.green_sessions,
129
+ "total_green_time": state.total_green_time,
130
+ "total_non_green_time": state.total_non_green_time,
131
+ }
132
+
133
+
134
+ def _build_agent_info(s: SessionDaemonState, now: datetime) -> Dict[str, Any]:
135
+ """Build agent info dict from SessionDaemonState."""
136
+ # Calculate time in current state
137
+ time_in_state = 0.0
138
+ if s.status_since:
139
+ try:
140
+ state_start = datetime.fromisoformat(s.status_since)
141
+ time_in_state = (now - state_start).total_seconds()
142
+ except ValueError:
143
+ pass
144
+
145
+ # Calculate current green/non-green time including elapsed
146
+ green_time = s.green_time_seconds
147
+ non_green_time = s.non_green_time_seconds
148
+
149
+ if s.current_status == "running":
150
+ green_time += time_in_state
151
+ elif s.current_status != "terminated":
152
+ non_green_time += time_in_state
153
+
154
+ total_time = green_time + non_green_time
155
+ percent_active = (green_time / total_time * 100) if total_time > 0 else 0
156
+
157
+ # Calculate human interactions (total - robot)
158
+ human_interactions = max(0, s.interaction_count - s.steers_count)
159
+
160
+ status_color = get_status_color(s.current_status)
161
+
162
+ # Calculate uptime from start_time
163
+ uptime = calculate_uptime(s.start_time, now) if s.start_time else "-"
164
+
165
+ # Get git diff stats if start_directory available
166
+ git_diff = None
167
+ if s.start_directory:
168
+ git_diff = get_git_diff_stats(s.start_directory)
169
+
170
+ # Permission mode emoji (matching TUI)
171
+ perm_emoji = "👮" # normal
172
+ if s.permissiveness_mode == "bypass":
173
+ perm_emoji = "🔥"
174
+ elif s.permissiveness_mode == "permissive":
175
+ perm_emoji = "🏃"
176
+
177
+ return {
178
+ "name": s.name,
179
+ "status": s.current_status,
180
+ "status_emoji": get_status_emoji(s.current_status),
181
+ "status_color": status_color,
182
+ "status_color_hex": get_web_color(status_color),
183
+ "activity": s.current_activity[:100] if s.current_activity else "",
184
+ "repo": s.repo_name or "",
185
+ "branch": s.branch or "",
186
+ "green_time": format_duration(green_time),
187
+ "green_time_raw": green_time,
188
+ "non_green_time": format_duration(non_green_time),
189
+ "non_green_time_raw": non_green_time,
190
+ "percent_active": round(percent_active),
191
+ "human_interactions": human_interactions,
192
+ "robot_steers": s.steers_count,
193
+ "tokens": format_tokens(s.input_tokens + s.output_tokens),
194
+ "tokens_raw": s.input_tokens + s.output_tokens,
195
+ "cost_usd": round(s.estimated_cost_usd, 2),
196
+ "standing_orders": bool(s.standing_instructions),
197
+ "standing_orders_complete": s.standing_orders_complete,
198
+ "time_in_state": format_duration(time_in_state),
199
+ "time_in_state_raw": time_in_state,
200
+ "median_work_time": format_duration(s.median_work_time) if s.median_work_time > 0 else "-",
201
+ # New fields for TUI parity
202
+ "uptime": uptime,
203
+ "permissiveness_mode": s.permissiveness_mode,
204
+ "perm_emoji": perm_emoji,
205
+ "git_diff_files": git_diff[0] if git_diff else 0,
206
+ "git_diff_insertions": git_diff[1] if git_diff else 0,
207
+ "git_diff_deletions": git_diff[2] if git_diff else 0,
208
+ # Activity summary (if summarizer enabled)
209
+ "activity_summary": s.activity_summary or "",
210
+ "activity_summary_updated": s.activity_summary_updated,
211
+ }
212
+
213
+
214
+ def get_timeline_data(tmux_session: str, hours: float = 3.0, slots: int = 60) -> Dict[str, Any]:
215
+ """Get timeline history data.
216
+
217
+ Args:
218
+ tmux_session: tmux session name
219
+ hours: How many hours of history (default 3)
220
+ slots: Number of time slots for the timeline (default 60)
221
+
222
+ Returns:
223
+ Dictionary with timeline slot data per agent
224
+ """
225
+ now = datetime.now()
226
+
227
+ result: Dict[str, Any] = {
228
+ "hours": hours,
229
+ "slot_count": slots,
230
+ "agents": {},
231
+ "status_chars": AGENT_TIMELINE_CHARS,
232
+ "status_colors": {k: get_web_color(get_status_color(k)) for k in AGENT_TIMELINE_CHARS},
233
+ }
234
+
235
+ # Get agent history
236
+ all_history = read_agent_status_history(hours=hours)
237
+
238
+ # Group by agent
239
+ agent_histories: Dict[str, List] = {}
240
+ for ts, agent, status, activity in all_history:
241
+ if agent not in agent_histories:
242
+ agent_histories[agent] = []
243
+ agent_histories[agent].append((ts, status))
244
+
245
+ # Build timeline for each agent
246
+ for agent_name, history in agent_histories.items():
247
+ slot_states = build_timeline_slots(history, slots, hours, now)
248
+
249
+ # Count green slots
250
+ green_slots = sum(1 for s in slot_states.values() if s == "running")
251
+ total_slots = len(slot_states) if slot_states else 1
252
+ percent_green = (green_slots / total_slots * 100) if total_slots > 0 else 0
253
+
254
+ # Build slot list with status and color
255
+ slot_list = []
256
+ for i in range(slots):
257
+ if i in slot_states:
258
+ status = slot_states[i]
259
+ slot_list.append({
260
+ "index": i,
261
+ "status": status,
262
+ "char": AGENT_TIMELINE_CHARS.get(status, "─"),
263
+ "color": get_web_color(get_status_color(status)),
264
+ })
265
+
266
+ result["agents"][agent_name] = {
267
+ "slots": slot_list,
268
+ "percent_green": round(percent_green),
269
+ }
270
+
271
+ return result
272
+
273
+
274
+ def get_health_data() -> Dict[str, Any]:
275
+ """Get health check data."""
276
+ return {
277
+ "status": "ok",
278
+ "timestamp": datetime.now().isoformat(),
279
+ }
overcode/web_server.py ADDED
@@ -0,0 +1,138 @@
1
+ """
2
+ Web server for Overcode dashboard.
3
+
4
+ Provides a mobile-optimized read-only dashboard for monitoring agents.
5
+ Uses Python stdlib http.server - no additional dependencies required.
6
+ """
7
+
8
+ import json
9
+ import sys
10
+ from http.server import HTTPServer, BaseHTTPRequestHandler
11
+ from typing import Optional
12
+ from urllib.parse import urlparse, parse_qs
13
+
14
+ from .web_templates import get_dashboard_html
15
+ from .web_api import get_status_data, get_timeline_data, get_health_data
16
+
17
+
18
+ class OvercodeHandler(BaseHTTPRequestHandler):
19
+ """HTTP request handler for overcode dashboard."""
20
+
21
+ # Set by run_server before starting
22
+ tmux_session: str = "agents"
23
+
24
+ def do_GET(self) -> None:
25
+ """Handle GET requests."""
26
+ parsed = urlparse(self.path)
27
+ path = parsed.path
28
+ query = parse_qs(parsed.query)
29
+
30
+ if path == "/" or path == "/index.html":
31
+ self._serve_dashboard()
32
+ elif path == "/api/status":
33
+ self._serve_json(get_status_data(self.tmux_session))
34
+ elif path == "/api/timeline":
35
+ hours = float(query.get("hours", [3.0])[0])
36
+ slots = int(query.get("slots", [60])[0])
37
+ self._serve_json(get_timeline_data(self.tmux_session, hours=hours, slots=slots))
38
+ elif path == "/health":
39
+ self._serve_json(get_health_data())
40
+ else:
41
+ self.send_error(404, "Not Found")
42
+
43
+ def _serve_dashboard(self) -> None:
44
+ """Serve the dashboard HTML page."""
45
+ try:
46
+ html = get_dashboard_html()
47
+ html_bytes = html.encode("utf-8")
48
+ self.send_response(200)
49
+ self.send_header("Content-Type", "text/html; charset=utf-8")
50
+ self.send_header("Content-Length", str(len(html_bytes)))
51
+ self.send_header("Cache-Control", "no-cache")
52
+ self.end_headers()
53
+ self.wfile.write(html_bytes)
54
+ except Exception as e:
55
+ self.send_error(500, f"Internal error: {e}")
56
+
57
+ def _serve_json(self, data: dict) -> None:
58
+ """Serve JSON data."""
59
+ try:
60
+ body = json.dumps(data, indent=2)
61
+ body_bytes = body.encode("utf-8")
62
+ self.send_response(200)
63
+ self.send_header("Content-Type", "application/json")
64
+ self.send_header("Content-Length", str(len(body_bytes)))
65
+ self.send_header("Access-Control-Allow-Origin", "*")
66
+ self.send_header("Cache-Control", "no-cache")
67
+ self.end_headers()
68
+ self.wfile.write(body_bytes)
69
+ except Exception as e:
70
+ self.send_error(500, f"Internal error: {e}")
71
+
72
+ def log_message(self, format: str, *args) -> None:
73
+ """Custom log format - less verbose than default."""
74
+ # Only log errors and important requests, not every poll
75
+ if args and len(args) >= 2:
76
+ status = str(args[1])
77
+ path = str(args[0])
78
+ # Don't log successful API polls
79
+ if status.startswith("2") and "/api/" in path:
80
+ return
81
+ sys.stderr.write(f"[web] {args[0] if args else format}\n")
82
+
83
+
84
+ def run_server(
85
+ host: str = "0.0.0.0",
86
+ port: int = 8080,
87
+ tmux_session: str = "agents"
88
+ ) -> None:
89
+ """Run the web dashboard server.
90
+
91
+ Args:
92
+ host: Host to bind to (default: 0.0.0.0 for all interfaces)
93
+ port: Port to listen on (default: 8080)
94
+ tmux_session: tmux session name to monitor
95
+ """
96
+ # Set the tmux session on the handler class
97
+ OvercodeHandler.tmux_session = tmux_session
98
+
99
+ server_address = (host, port)
100
+
101
+ try:
102
+ server = HTTPServer(server_address, OvercodeHandler)
103
+ except OSError as e:
104
+ if "Address already in use" in str(e):
105
+ print(f"Error: Port {port} is already in use. Try a different port with --port")
106
+ sys.exit(1)
107
+ raise
108
+
109
+ # Get actual bound address for display
110
+ bound_host, bound_port = server.server_address
111
+
112
+ print(f"Overcode Dashboard")
113
+ print(f"====================")
114
+ print(f"Monitoring tmux session: {tmux_session}")
115
+ print(f"")
116
+ print(f"Local: http://localhost:{bound_port}")
117
+
118
+ if host == "0.0.0.0":
119
+ # Try to get the machine's IP for network access
120
+ try:
121
+ import socket
122
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
123
+ s.connect(("8.8.8.8", 80))
124
+ ip = s.getsockname()[0]
125
+ s.close()
126
+ print(f"Network: http://{ip}:{bound_port}")
127
+ except Exception:
128
+ print(f"Network: http://<your-ip>:{bound_port}")
129
+
130
+ print(f"")
131
+ print(f"Press Ctrl+C to stop")
132
+ print(f"")
133
+
134
+ try:
135
+ server.serve_forever()
136
+ except KeyboardInterrupt:
137
+ print("\nShutting down...")
138
+ server.shutdown()