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.
- overcode/__init__.py +5 -0
- overcode/cli.py +812 -0
- overcode/config.py +72 -0
- overcode/daemon.py +1184 -0
- overcode/daemon_claude_skill.md +180 -0
- overcode/daemon_state.py +113 -0
- overcode/data_export.py +257 -0
- overcode/dependency_check.py +227 -0
- overcode/exceptions.py +219 -0
- overcode/history_reader.py +448 -0
- overcode/implementations.py +214 -0
- overcode/interfaces.py +49 -0
- overcode/launcher.py +434 -0
- overcode/logging_config.py +193 -0
- overcode/mocks.py +152 -0
- overcode/monitor_daemon.py +808 -0
- overcode/monitor_daemon_state.py +358 -0
- overcode/pid_utils.py +225 -0
- overcode/presence_logger.py +454 -0
- overcode/protocols.py +143 -0
- overcode/session_manager.py +606 -0
- overcode/settings.py +412 -0
- overcode/standing_instructions.py +276 -0
- overcode/status_constants.py +190 -0
- overcode/status_detector.py +339 -0
- overcode/status_history.py +164 -0
- overcode/status_patterns.py +264 -0
- overcode/summarizer_client.py +136 -0
- overcode/summarizer_component.py +312 -0
- overcode/supervisor_daemon.py +1000 -0
- overcode/supervisor_layout.sh +50 -0
- overcode/tmux_manager.py +228 -0
- overcode/tui.py +2549 -0
- overcode/tui_helpers.py +495 -0
- overcode/web_api.py +279 -0
- overcode/web_server.py +138 -0
- overcode/web_templates.py +563 -0
- overcode-0.1.0.dist-info/METADATA +87 -0
- overcode-0.1.0.dist-info/RECORD +43 -0
- overcode-0.1.0.dist-info/WHEEL +5 -0
- overcode-0.1.0.dist-info/entry_points.txt +2 -0
- overcode-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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()
|