overcode 0.1.0__tar.gz → 0.1.2__tar.gz
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-0.1.0/src/overcode.egg-info → overcode-0.1.2}/PKG-INFO +13 -1
- {overcode-0.1.0 → overcode-0.1.2}/README.md +12 -0
- {overcode-0.1.0 → overcode-0.1.2}/pyproject.toml +1 -1
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/__init__.py +1 -1
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/cli.py +42 -3
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/config.py +49 -0
- overcode-0.1.2/src/overcode/daemon_logging.py +144 -0
- overcode-0.1.2/src/overcode/daemon_utils.py +84 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/history_reader.py +17 -5
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/implementations.py +11 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/launcher.py +3 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/mocks.py +4 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/monitor_daemon.py +25 -126
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/pid_utils.py +10 -3
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/protocols.py +12 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/session_manager.py +3 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/settings.py +20 -1
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/standing_instructions.py +15 -6
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/status_constants.py +11 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/status_detector.py +38 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/status_patterns.py +12 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/supervisor_daemon.py +40 -171
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/tui.py +326 -39
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/tui_helpers.py +18 -0
- overcode-0.1.2/src/overcode/web_api.py +763 -0
- overcode-0.1.2/src/overcode/web_chartjs.py +32 -0
- overcode-0.1.2/src/overcode/web_server.py +490 -0
- overcode-0.1.2/src/overcode/web_server_runner.py +104 -0
- overcode-0.1.2/src/overcode/web_templates.py +1656 -0
- {overcode-0.1.0 → overcode-0.1.2/src/overcode.egg-info}/PKG-INFO +13 -1
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode.egg-info/SOURCES.txt +4 -2
- overcode-0.1.0/src/overcode/daemon.py +0 -1184
- overcode-0.1.0/src/overcode/daemon_state.py +0 -113
- overcode-0.1.0/src/overcode/web_api.py +0 -279
- overcode-0.1.0/src/overcode/web_server.py +0 -138
- overcode-0.1.0/src/overcode/web_templates.py +0 -563
- {overcode-0.1.0 → overcode-0.1.2}/LICENSE +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/MANIFEST.in +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/setup.cfg +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/daemon_claude_skill.md +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/data_export.py +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/dependency_check.py +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/exceptions.py +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/interfaces.py +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/logging_config.py +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/monitor_daemon_state.py +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/presence_logger.py +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/status_history.py +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/summarizer_client.py +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/summarizer_component.py +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/supervisor_layout.sh +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode/tmux_manager.py +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode.egg-info/dependency_links.txt +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode.egg-info/entry_points.txt +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode.egg-info/requires.txt +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/src/overcode.egg-info/top_level.txt +0 -0
- {overcode-0.1.0 → overcode-0.1.2}/tests/test_e2e_multi_agent_jokes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: overcode
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: A supervisor for managing multiple Claude Code instances in tmux
|
|
5
5
|
Author: Mike Bond
|
|
6
6
|
Project-URL: Homepage, https://github.com/mkb23/overcode
|
|
@@ -43,6 +43,18 @@ A TUI supervisor for managing multiple Claude Code agents in tmux.
|
|
|
43
43
|
|
|
44
44
|
Monitor status, costs, and activity across all your agents from a single dashboard.
|
|
45
45
|
|
|
46
|
+
## Screenshots
|
|
47
|
+
|
|
48
|
+
**Split-screen with tmux sync** - Monitor agents in the top pane while viewing live agent output below. Press `p` to enable pane sync, then navigate with `j/k` to switch the bottom pane to the selected agent's window.
|
|
49
|
+
|
|
50
|
+

|
|
51
|
+
|
|
52
|
+
> **iTerm2 setup**: Use `Cmd+Shift+D` to split horizontally. Run `overcode monitor` in the top pane and `tmux attach -t agents` in the bottom pane.
|
|
53
|
+
|
|
54
|
+
**Preview mode** - Press `m` to toggle List+Preview mode. Shows collapsed agent list with detailed terminal output preview for the selected agent.
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
46
58
|
## Installation
|
|
47
59
|
|
|
48
60
|
```bash
|
|
@@ -4,6 +4,18 @@ A TUI supervisor for managing multiple Claude Code agents in tmux.
|
|
|
4
4
|
|
|
5
5
|
Monitor status, costs, and activity across all your agents from a single dashboard.
|
|
6
6
|
|
|
7
|
+
## Screenshots
|
|
8
|
+
|
|
9
|
+
**Split-screen with tmux sync** - Monitor agents in the top pane while viewing live agent output below. Press `p` to enable pane sync, then navigate with `j/k` to switch the bottom pane to the selected agent's window.
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
> **iTerm2 setup**: Use `Cmd+Shift+D` to split horizontally. Run `overcode monitor` in the top pane and `tmux attach -t agents` in the bottom pane.
|
|
14
|
+
|
|
15
|
+
**Preview mode** - Press `m` to toggle List+Preview mode. Shows collapsed agent list with detailed terminal output preview for the selected agent.
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
7
19
|
## Installation
|
|
8
20
|
|
|
9
21
|
```bash
|
|
@@ -264,7 +264,7 @@ def instruct(
|
|
|
264
264
|
] = None,
|
|
265
265
|
instructions: Annotated[
|
|
266
266
|
Optional[List[str]],
|
|
267
|
-
typer.Argument(help="Instructions or preset name (e.g.,
|
|
267
|
+
typer.Argument(help="Instructions or preset name (e.g., DO_NOTHING, STANDARD, CODING)"),
|
|
268
268
|
] = None,
|
|
269
269
|
clear: Annotated[
|
|
270
270
|
bool, typer.Option("--clear", "-c", help="Clear standing instructions")
|
|
@@ -276,7 +276,7 @@ def instruct(
|
|
|
276
276
|
):
|
|
277
277
|
"""Set standing instructions for an agent.
|
|
278
278
|
|
|
279
|
-
Use a preset name (
|
|
279
|
+
Use a preset name (DO_NOTHING, STANDARD, CODING, etc.) or provide custom instructions.
|
|
280
280
|
Use --list to see all available presets.
|
|
281
281
|
"""
|
|
282
282
|
from .session_manager import SessionManager
|
|
@@ -285,7 +285,7 @@ def instruct(
|
|
|
285
285
|
if list_presets:
|
|
286
286
|
presets_dict = load_presets()
|
|
287
287
|
rprint("\n[bold]Standing Instruction Presets:[/bold]\n")
|
|
288
|
-
for preset_name in sorted(presets_dict.keys(), key=lambda x: (x != "
|
|
288
|
+
for preset_name in sorted(presets_dict.keys(), key=lambda x: (x != "DO_NOTHING", x)):
|
|
289
289
|
preset = presets_dict[preset_name]
|
|
290
290
|
rprint(f" [cyan]{preset_name:12}[/cyan] {preset.description}")
|
|
291
291
|
rprint("\n[dim]Usage: overcode instruct <agent> <PRESET>[/dim]")
|
|
@@ -405,6 +405,45 @@ def serve(
|
|
|
405
405
|
run_server(host=host, port=port, tmux_session=session)
|
|
406
406
|
|
|
407
407
|
|
|
408
|
+
@app.command()
|
|
409
|
+
def web(
|
|
410
|
+
host: Annotated[
|
|
411
|
+
str, typer.Option("--host", "-h", help="Host to bind to")
|
|
412
|
+
] = "127.0.0.1",
|
|
413
|
+
port: Annotated[
|
|
414
|
+
int, typer.Option("--port", "-p", help="Port to listen on")
|
|
415
|
+
] = 8080,
|
|
416
|
+
):
|
|
417
|
+
"""Launch analytics web dashboard for browsing historical data.
|
|
418
|
+
|
|
419
|
+
A lightweight web app for exploring session history, timeline
|
|
420
|
+
visualization, and efficiency metrics. Uses Chart.js for
|
|
421
|
+
interactive charts with dark theme matching the TUI.
|
|
422
|
+
|
|
423
|
+
Features:
|
|
424
|
+
- Dashboard with summary stats and daily activity charts
|
|
425
|
+
- Session browser with sortable table
|
|
426
|
+
- Timeline view with agent status and user presence
|
|
427
|
+
- Efficiency metrics with cost analysis
|
|
428
|
+
|
|
429
|
+
Time range presets can be configured in ~/.overcode/config.yaml:
|
|
430
|
+
|
|
431
|
+
web:
|
|
432
|
+
time_presets:
|
|
433
|
+
- name: "Morning"
|
|
434
|
+
start: "09:00"
|
|
435
|
+
end: "12:00"
|
|
436
|
+
|
|
437
|
+
Examples:
|
|
438
|
+
overcode web # Start on localhost:8080
|
|
439
|
+
overcode web --port 3000 # Custom port
|
|
440
|
+
overcode web --host 0.0.0.0 # Listen on all interfaces
|
|
441
|
+
"""
|
|
442
|
+
from .web_server import run_analytics_server
|
|
443
|
+
|
|
444
|
+
run_analytics_server(host=host, port=port)
|
|
445
|
+
|
|
446
|
+
|
|
408
447
|
|
|
409
448
|
|
|
410
449
|
@app.command()
|
|
@@ -70,3 +70,52 @@ def get_relay_config() -> Optional[dict]:
|
|
|
70
70
|
"api_key": api_key,
|
|
71
71
|
"interval": relay.get("interval", 30),
|
|
72
72
|
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_web_time_presets() -> list:
|
|
76
|
+
"""Get time presets for the web analytics dashboard.
|
|
77
|
+
|
|
78
|
+
Returns list of preset dictionaries with name, start, end times.
|
|
79
|
+
Falls back to defaults if not configured.
|
|
80
|
+
|
|
81
|
+
Config format in ~/.overcode/config.yaml:
|
|
82
|
+
web:
|
|
83
|
+
time_presets:
|
|
84
|
+
- name: "Morning"
|
|
85
|
+
start: "09:00"
|
|
86
|
+
end: "12:00"
|
|
87
|
+
- name: "Full Day"
|
|
88
|
+
start: "09:00"
|
|
89
|
+
end: "17:00"
|
|
90
|
+
- name: "Night Owl"
|
|
91
|
+
start: "22:00"
|
|
92
|
+
end: "02:00"
|
|
93
|
+
"""
|
|
94
|
+
config = load_config()
|
|
95
|
+
web_config = config.get("web", {})
|
|
96
|
+
presets = web_config.get("time_presets", None)
|
|
97
|
+
|
|
98
|
+
if presets and isinstance(presets, list):
|
|
99
|
+
# Validate and normalize presets
|
|
100
|
+
valid_presets = []
|
|
101
|
+
for p in presets:
|
|
102
|
+
if isinstance(p, dict) and "name" in p:
|
|
103
|
+
valid_presets.append({
|
|
104
|
+
"name": p.get("name", ""),
|
|
105
|
+
"start": p.get("start"),
|
|
106
|
+
"end": p.get("end"),
|
|
107
|
+
})
|
|
108
|
+
if valid_presets:
|
|
109
|
+
# Always add "All Time" at the end
|
|
110
|
+
if not any(p["name"] == "All Time" for p in valid_presets):
|
|
111
|
+
valid_presets.append({"name": "All Time", "start": None, "end": None})
|
|
112
|
+
return valid_presets
|
|
113
|
+
|
|
114
|
+
# Default presets
|
|
115
|
+
return [
|
|
116
|
+
{"name": "Morning", "start": "09:00", "end": "12:00"},
|
|
117
|
+
{"name": "Afternoon", "start": "13:00", "end": "17:00"},
|
|
118
|
+
{"name": "Full Day", "start": "09:00", "end": "17:00"},
|
|
119
|
+
{"name": "Evening", "start": "18:00", "end": "22:00"},
|
|
120
|
+
{"name": "All Time", "start": None, "end": None},
|
|
121
|
+
]
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared logging utilities for Overcode daemons.
|
|
3
|
+
|
|
4
|
+
Provides base logger class with common functionality for both
|
|
5
|
+
monitor_daemon and supervisor_daemon.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
from rich.theme import Theme
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Shared theme for daemon logs
|
|
18
|
+
DAEMON_THEME = Theme({
|
|
19
|
+
"info": "cyan",
|
|
20
|
+
"warn": "yellow",
|
|
21
|
+
"error": "bold red",
|
|
22
|
+
"success": "bold green",
|
|
23
|
+
"daemon_claude": "magenta",
|
|
24
|
+
"dim": "dim white",
|
|
25
|
+
"highlight": "bold white",
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BaseDaemonLogger:
|
|
30
|
+
"""Base logger for daemons with common logging methods."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, log_file: Path, theme: Theme = None):
|
|
33
|
+
"""Initialize the logger.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
log_file: Path to the log file
|
|
37
|
+
theme: Optional Rich theme (defaults to DAEMON_THEME)
|
|
38
|
+
"""
|
|
39
|
+
self.log_file = log_file
|
|
40
|
+
self.log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
self.console = Console(theme=theme or DAEMON_THEME, force_terminal=True)
|
|
42
|
+
|
|
43
|
+
def _write_to_file(self, message: str, level: str = "INFO"):
|
|
44
|
+
"""Write plain text to log file."""
|
|
45
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
46
|
+
line = f"[{timestamp}] [{level}] {message}"
|
|
47
|
+
try:
|
|
48
|
+
with open(self.log_file, 'a') as f:
|
|
49
|
+
f.write(line + '\n')
|
|
50
|
+
except OSError:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def _log(self, style: str, prefix: str, message: str, level: str = "INFO"):
|
|
54
|
+
"""Log a message with style to both console and file."""
|
|
55
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
56
|
+
text = Text()
|
|
57
|
+
text.append(f"[{timestamp}] ", style="dim")
|
|
58
|
+
text.append(f"{prefix} ", style=style)
|
|
59
|
+
text.append(message)
|
|
60
|
+
self.console.print(text)
|
|
61
|
+
self._write_to_file(message, level)
|
|
62
|
+
|
|
63
|
+
def info(self, message: str):
|
|
64
|
+
"""Log info message."""
|
|
65
|
+
self._log("info", "●", message, "INFO")
|
|
66
|
+
|
|
67
|
+
def warn(self, message: str):
|
|
68
|
+
"""Log warning message."""
|
|
69
|
+
self._log("warn", "⚠", message, "WARN")
|
|
70
|
+
|
|
71
|
+
def error(self, message: str):
|
|
72
|
+
"""Log error message."""
|
|
73
|
+
self._log("error", "✗", message, "ERROR")
|
|
74
|
+
|
|
75
|
+
def success(self, message: str):
|
|
76
|
+
"""Log success message."""
|
|
77
|
+
self._log("success", "✓", message, "INFO")
|
|
78
|
+
|
|
79
|
+
def debug(self, message: str):
|
|
80
|
+
"""Log a debug message (only to file, not console)."""
|
|
81
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
82
|
+
try:
|
|
83
|
+
with open(self.log_file, 'a') as f:
|
|
84
|
+
f.write(f"[{timestamp}] DEBUG {message}\n")
|
|
85
|
+
except OSError:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
def section(self, title: str):
|
|
89
|
+
"""Print a section header."""
|
|
90
|
+
self._write_to_file(f"=== {title} ===", "INFO")
|
|
91
|
+
self.console.print()
|
|
92
|
+
self.console.rule(f"[bold cyan]{title}[/]")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class SupervisorDaemonLogger(BaseDaemonLogger):
|
|
96
|
+
"""Logger for supervisor daemon with additional methods."""
|
|
97
|
+
|
|
98
|
+
def __init__(self, log_file: Path):
|
|
99
|
+
super().__init__(log_file)
|
|
100
|
+
self._seen_daemon_claude_lines: set = set()
|
|
101
|
+
|
|
102
|
+
def daemon_claude_output(self, lines: List[str]):
|
|
103
|
+
"""Log daemon claude output, showing only new lines."""
|
|
104
|
+
new_lines = []
|
|
105
|
+
|
|
106
|
+
for line in lines:
|
|
107
|
+
stripped = line.strip()
|
|
108
|
+
if not stripped:
|
|
109
|
+
continue
|
|
110
|
+
if stripped not in self._seen_daemon_claude_lines:
|
|
111
|
+
new_lines.append(stripped)
|
|
112
|
+
self._seen_daemon_claude_lines.add(stripped)
|
|
113
|
+
|
|
114
|
+
# Limit set size
|
|
115
|
+
if len(self._seen_daemon_claude_lines) > 500:
|
|
116
|
+
current_lines = {line.strip() for line in lines if line.strip()}
|
|
117
|
+
self._seen_daemon_claude_lines = current_lines
|
|
118
|
+
|
|
119
|
+
if new_lines:
|
|
120
|
+
for line in new_lines:
|
|
121
|
+
self._write_to_file(f"[DAEMON_CLAUDE] {line}", "INFO")
|
|
122
|
+
if line.startswith('✓') or 'success' in line.lower():
|
|
123
|
+
self.console.print(f" [success]│[/success] {line}")
|
|
124
|
+
elif line.startswith('✗') or 'error' in line.lower() or 'fail' in line.lower():
|
|
125
|
+
self.console.print(f" [error]│[/error] {line}")
|
|
126
|
+
elif line.startswith('>') or line.startswith('$'):
|
|
127
|
+
self.console.print(f" [highlight]│[/highlight] {line}")
|
|
128
|
+
else:
|
|
129
|
+
self.console.print(f" [daemon_claude]│[/daemon_claude] {line}")
|
|
130
|
+
|
|
131
|
+
def status_summary(self, total: int, green: int, non_green: int, loop: int):
|
|
132
|
+
"""Print a status summary line."""
|
|
133
|
+
status_text = Text()
|
|
134
|
+
status_text.append(f"Loop #{loop}: ", style="dim")
|
|
135
|
+
status_text.append(f"{total} agents ", style="highlight")
|
|
136
|
+
status_text.append("(", style="dim")
|
|
137
|
+
status_text.append(f"{green} green", style="success")
|
|
138
|
+
status_text.append(", ", style="dim")
|
|
139
|
+
status_text.append(f"{non_green} non-green", style="warn" if non_green else "dim")
|
|
140
|
+
status_text.append(")", style="dim")
|
|
141
|
+
|
|
142
|
+
self._write_to_file(f"Loop #{loop}: {total} agents ({green} green, {non_green} non-green)", "INFO")
|
|
143
|
+
self.console.print(f"[dim]{datetime.now().strftime('%H:%M:%S')}[/dim] ", end="")
|
|
144
|
+
self.console.print(status_text)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared utilities for Overcode daemons.
|
|
3
|
+
|
|
4
|
+
Provides factory functions for creating daemon PID management helpers,
|
|
5
|
+
avoiding code duplication between monitor_daemon and supervisor_daemon.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import signal
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Callable, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
from .pid_utils import (
|
|
14
|
+
get_process_pid,
|
|
15
|
+
is_process_running,
|
|
16
|
+
remove_pid_file,
|
|
17
|
+
)
|
|
18
|
+
from .settings import DAEMON
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def create_daemon_helpers(
|
|
22
|
+
get_pid_path: Callable[[str], Path],
|
|
23
|
+
daemon_name: str,
|
|
24
|
+
) -> Tuple[
|
|
25
|
+
Callable[[Optional[str]], bool],
|
|
26
|
+
Callable[[Optional[str]], Optional[int]],
|
|
27
|
+
Callable[[Optional[str]], bool],
|
|
28
|
+
]:
|
|
29
|
+
"""Factory to create is_*_running, get_*_pid, stop_* functions for a daemon.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
get_pid_path: Function that takes session name and returns PID file path
|
|
33
|
+
daemon_name: Human-readable name for error messages
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Tuple of (is_running_fn, get_pid_fn, stop_fn)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def is_running(session: str = None) -> bool:
|
|
40
|
+
"""Check if the daemon process is currently running for a session.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
session: tmux session name (default: from config)
|
|
44
|
+
"""
|
|
45
|
+
if session is None:
|
|
46
|
+
session = DAEMON.default_tmux_session
|
|
47
|
+
return is_process_running(get_pid_path(session))
|
|
48
|
+
|
|
49
|
+
def get_pid(session: str = None) -> Optional[int]:
|
|
50
|
+
"""Get the daemon PID if running, None otherwise.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
session: tmux session name (default: from config)
|
|
54
|
+
"""
|
|
55
|
+
if session is None:
|
|
56
|
+
session = DAEMON.default_tmux_session
|
|
57
|
+
return get_process_pid(get_pid_path(session))
|
|
58
|
+
|
|
59
|
+
def stop(session: str = None) -> bool:
|
|
60
|
+
"""Stop the daemon process if running.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
session: tmux session name (default: from config)
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
True if daemon was stopped, False if it wasn't running.
|
|
67
|
+
"""
|
|
68
|
+
if session is None:
|
|
69
|
+
session = DAEMON.default_tmux_session
|
|
70
|
+
pid_path = get_pid_path(session)
|
|
71
|
+
pid = get_process_pid(pid_path)
|
|
72
|
+
if pid is None:
|
|
73
|
+
remove_pid_file(pid_path)
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
os.kill(pid, signal.SIGTERM)
|
|
78
|
+
remove_pid_file(pid_path)
|
|
79
|
+
return True
|
|
80
|
+
except (OSError, ProcessLookupError):
|
|
81
|
+
remove_pid_file(pid_path)
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
return is_running, get_pid, stop
|
|
@@ -39,6 +39,7 @@ class ClaudeSessionStats:
|
|
|
39
39
|
cache_creation_tokens: int
|
|
40
40
|
cache_read_tokens: int
|
|
41
41
|
work_times: List[float] # seconds per work cycle (prompt to next prompt)
|
|
42
|
+
current_context_tokens: int = 0 # Most recent input_tokens (current context size)
|
|
42
43
|
|
|
43
44
|
@property
|
|
44
45
|
def total_tokens(self) -> int:
|
|
@@ -249,13 +250,15 @@ def read_token_usage_from_session_file(
|
|
|
249
250
|
since: Only count tokens from messages after this time
|
|
250
251
|
|
|
251
252
|
Returns:
|
|
252
|
-
Dict with input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens
|
|
253
|
+
Dict with input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
|
|
254
|
+
and current_context_tokens (most recent input_tokens value)
|
|
253
255
|
"""
|
|
254
256
|
totals = {
|
|
255
257
|
"input_tokens": 0,
|
|
256
258
|
"output_tokens": 0,
|
|
257
259
|
"cache_creation_tokens": 0,
|
|
258
260
|
"cache_read_tokens": 0,
|
|
261
|
+
"current_context_tokens": 0, # Most recent input_tokens
|
|
259
262
|
}
|
|
260
263
|
|
|
261
264
|
if not session_file.exists():
|
|
@@ -288,14 +291,18 @@ def read_token_usage_from_session_file(
|
|
|
288
291
|
message = data.get("message", {})
|
|
289
292
|
usage = message.get("usage", {})
|
|
290
293
|
if usage:
|
|
291
|
-
|
|
294
|
+
input_tokens = usage.get("input_tokens", 0)
|
|
295
|
+
cache_read = usage.get("cache_read_input_tokens", 0)
|
|
296
|
+
totals["input_tokens"] += input_tokens
|
|
292
297
|
totals["output_tokens"] += usage.get("output_tokens", 0)
|
|
293
298
|
totals["cache_creation_tokens"] += usage.get(
|
|
294
299
|
"cache_creation_input_tokens", 0
|
|
295
300
|
)
|
|
296
|
-
totals["cache_read_tokens"] +=
|
|
297
|
-
|
|
298
|
-
|
|
301
|
+
totals["cache_read_tokens"] += cache_read
|
|
302
|
+
# Track most recent context size (input + cached context)
|
|
303
|
+
context_size = input_tokens + cache_read
|
|
304
|
+
if context_size > 0:
|
|
305
|
+
totals["current_context_tokens"] = context_size
|
|
299
306
|
except (json.JSONDecodeError, KeyError, TypeError):
|
|
300
307
|
continue
|
|
301
308
|
except IOError:
|
|
@@ -422,6 +429,7 @@ def get_session_stats(
|
|
|
422
429
|
total_output = 0
|
|
423
430
|
total_cache_creation = 0
|
|
424
431
|
total_cache_read = 0
|
|
432
|
+
current_context = 0 # Track most recent context size
|
|
425
433
|
all_work_times: List[float] = []
|
|
426
434
|
|
|
427
435
|
for sid in session_ids:
|
|
@@ -433,6 +441,9 @@ def get_session_stats(
|
|
|
433
441
|
total_output += usage["output_tokens"]
|
|
434
442
|
total_cache_creation += usage["cache_creation_tokens"]
|
|
435
443
|
total_cache_read += usage["cache_read_tokens"]
|
|
444
|
+
# Keep the largest current context (most recent across all session files)
|
|
445
|
+
if usage["current_context_tokens"] > current_context:
|
|
446
|
+
current_context = usage["current_context_tokens"]
|
|
436
447
|
|
|
437
448
|
# Collect work times from this session file
|
|
438
449
|
work_times = read_work_times_from_session_file(session_file, since=session_start)
|
|
@@ -445,4 +456,5 @@ def get_session_stats(
|
|
|
445
456
|
cache_creation_tokens=total_cache_creation,
|
|
446
457
|
cache_read_tokens=total_cache_read,
|
|
447
458
|
work_times=all_work_times,
|
|
459
|
+
current_context_tokens=current_context,
|
|
448
460
|
)
|
|
@@ -142,6 +142,17 @@ class RealTmux:
|
|
|
142
142
|
def attach(self, session: str) -> None:
|
|
143
143
|
os.execlp("tmux", "tmux", "attach-session", "-t", session)
|
|
144
144
|
|
|
145
|
+
def select_window(self, session: str, window: int) -> bool:
|
|
146
|
+
"""Select a window in a tmux session (for external pane sync)."""
|
|
147
|
+
try:
|
|
148
|
+
result = subprocess.run(
|
|
149
|
+
["tmux", "select-window", "-t", f"{session}:{window}"],
|
|
150
|
+
capture_output=True, timeout=5
|
|
151
|
+
)
|
|
152
|
+
return result.returncode == 0
|
|
153
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError):
|
|
154
|
+
return False
|
|
155
|
+
|
|
145
156
|
|
|
146
157
|
class RealFileSystem:
|
|
147
158
|
"""Production implementation of FileSystemInterface"""
|
|
@@ -4,6 +4,9 @@ Launcher for interactive Claude Code sessions in tmux windows.
|
|
|
4
4
|
All Claude sessions launched by overcode are interactive - users can
|
|
5
5
|
take over at any time. Initial prompts are sent as keystrokes after
|
|
6
6
|
Claude starts, not as CLI arguments.
|
|
7
|
+
|
|
8
|
+
TODO: Extract _send_prompt_to_window to a shared tmux utilities module
|
|
9
|
+
(duplicated in supervisor_daemon.py)
|
|
7
10
|
"""
|
|
8
11
|
|
|
9
12
|
import time
|
|
@@ -77,6 +77,10 @@ class MockTmux:
|
|
|
77
77
|
def attach(self, session: str) -> None:
|
|
78
78
|
pass # No-op in tests
|
|
79
79
|
|
|
80
|
+
def select_window(self, session: str, window: int) -> bool:
|
|
81
|
+
"""Select a window - no-op in tests, just return True."""
|
|
82
|
+
return session in self.sessions
|
|
83
|
+
|
|
80
84
|
|
|
81
85
|
class MockFileSystem:
|
|
82
86
|
"""Mock implementation of FileSystemInterface for testing"""
|