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
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dependency checking and graceful degradation utilities.
|
|
3
|
+
|
|
4
|
+
Provides functions to check for required external dependencies (tmux, claude)
|
|
5
|
+
and handle graceful degradation when they're missing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
from typing import Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from .exceptions import TmuxNotFoundError, ClaudeNotFoundError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def find_executable(name: str) -> Optional[str]:
|
|
16
|
+
"""Find the path to an executable.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
name: Name of the executable
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Full path to executable, or None if not found
|
|
23
|
+
"""
|
|
24
|
+
return shutil.which(name)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def check_tmux() -> Tuple[bool, Optional[str], Optional[str]]:
|
|
28
|
+
"""Check if tmux is available and get its version.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Tuple of (is_available, path, version)
|
|
32
|
+
"""
|
|
33
|
+
path = find_executable("tmux")
|
|
34
|
+
if not path:
|
|
35
|
+
return False, None, None
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
result = subprocess.run(
|
|
39
|
+
["tmux", "-V"],
|
|
40
|
+
capture_output=True,
|
|
41
|
+
text=True,
|
|
42
|
+
timeout=5
|
|
43
|
+
)
|
|
44
|
+
version = result.stdout.strip() if result.returncode == 0 else None
|
|
45
|
+
return True, path, version
|
|
46
|
+
except (subprocess.SubprocessError, OSError):
|
|
47
|
+
return True, path, None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check_claude() -> Tuple[bool, Optional[str], Optional[str]]:
|
|
51
|
+
"""Check if Claude Code CLI is available and get its version.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Tuple of (is_available, path, version)
|
|
55
|
+
"""
|
|
56
|
+
path = find_executable("claude")
|
|
57
|
+
if not path:
|
|
58
|
+
return False, None, None
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
result = subprocess.run(
|
|
62
|
+
["claude", "--version"],
|
|
63
|
+
capture_output=True,
|
|
64
|
+
text=True,
|
|
65
|
+
timeout=10
|
|
66
|
+
)
|
|
67
|
+
if result.returncode == 0:
|
|
68
|
+
# Parse version from output like "Claude Code v2.0.75"
|
|
69
|
+
version = result.stdout.strip()
|
|
70
|
+
return True, path, version
|
|
71
|
+
return True, path, None
|
|
72
|
+
except (subprocess.SubprocessError, OSError):
|
|
73
|
+
return True, path, None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def require_tmux() -> str:
|
|
77
|
+
"""Ensure tmux is available, raise if not.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Path to tmux executable
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
TmuxNotFoundError: If tmux is not found
|
|
84
|
+
"""
|
|
85
|
+
available, path, _ = check_tmux()
|
|
86
|
+
if not available:
|
|
87
|
+
raise TmuxNotFoundError(
|
|
88
|
+
"tmux is required but not found. "
|
|
89
|
+
"Install it with: brew install tmux (macOS) or apt install tmux (Linux)"
|
|
90
|
+
)
|
|
91
|
+
return path
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def require_claude() -> str:
|
|
95
|
+
"""Ensure Claude Code CLI is available, raise if not.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Path to claude executable
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
ClaudeNotFoundError: If claude is not found
|
|
102
|
+
"""
|
|
103
|
+
available, path, _ = check_claude()
|
|
104
|
+
if not available:
|
|
105
|
+
raise ClaudeNotFoundError(
|
|
106
|
+
"Claude Code CLI is required but not found. "
|
|
107
|
+
"Install it from: https://claude.ai/claude-code"
|
|
108
|
+
)
|
|
109
|
+
return path
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_dependency_status() -> dict:
|
|
113
|
+
"""Get status of all dependencies.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Dict with dependency info:
|
|
117
|
+
{
|
|
118
|
+
"tmux": {"available": bool, "path": str, "version": str},
|
|
119
|
+
"claude": {"available": bool, "path": str, "version": str},
|
|
120
|
+
}
|
|
121
|
+
"""
|
|
122
|
+
tmux_ok, tmux_path, tmux_ver = check_tmux()
|
|
123
|
+
claude_ok, claude_path, claude_ver = check_claude()
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
"tmux": {
|
|
127
|
+
"available": tmux_ok,
|
|
128
|
+
"path": tmux_path,
|
|
129
|
+
"version": tmux_ver,
|
|
130
|
+
},
|
|
131
|
+
"claude": {
|
|
132
|
+
"available": claude_ok,
|
|
133
|
+
"path": claude_path,
|
|
134
|
+
"version": claude_ver,
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def print_dependency_status():
|
|
140
|
+
"""Print dependency status to console."""
|
|
141
|
+
status = get_dependency_status()
|
|
142
|
+
|
|
143
|
+
print("Dependency Status:")
|
|
144
|
+
print("-" * 40)
|
|
145
|
+
|
|
146
|
+
for name, info in status.items():
|
|
147
|
+
if info["available"]:
|
|
148
|
+
version = info["version"] or "unknown version"
|
|
149
|
+
print(f" {name}: ✓ {version}")
|
|
150
|
+
print(f" Path: {info['path']}")
|
|
151
|
+
else:
|
|
152
|
+
print(f" {name}: ✗ Not found")
|
|
153
|
+
|
|
154
|
+
print("-" * 40)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class DependencyContext:
|
|
158
|
+
"""Context manager that checks dependencies before use.
|
|
159
|
+
|
|
160
|
+
Example:
|
|
161
|
+
with DependencyContext(require_tmux=True, require_claude=True):
|
|
162
|
+
# Code that needs both tmux and claude
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
# With graceful handling:
|
|
166
|
+
with DependencyContext(require_tmux=True, on_missing="warn"):
|
|
167
|
+
# Will warn but continue if tmux missing
|
|
168
|
+
pass
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
require_tmux: bool = False,
|
|
174
|
+
require_claude: bool = False,
|
|
175
|
+
on_missing: str = "raise"
|
|
176
|
+
):
|
|
177
|
+
"""Initialize the dependency context.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
require_tmux: Whether tmux is required
|
|
181
|
+
require_claude: Whether claude is required
|
|
182
|
+
on_missing: What to do if dependency missing:
|
|
183
|
+
"raise" (default), "warn", "ignore"
|
|
184
|
+
"""
|
|
185
|
+
self.require_tmux = require_tmux
|
|
186
|
+
self.require_claude = require_claude
|
|
187
|
+
self.on_missing = on_missing
|
|
188
|
+
self._missing = []
|
|
189
|
+
|
|
190
|
+
def __enter__(self):
|
|
191
|
+
if self.require_tmux:
|
|
192
|
+
try:
|
|
193
|
+
require_tmux()
|
|
194
|
+
except TmuxNotFoundError as e:
|
|
195
|
+
self._handle_missing("tmux", e)
|
|
196
|
+
|
|
197
|
+
if self.require_claude:
|
|
198
|
+
try:
|
|
199
|
+
require_claude()
|
|
200
|
+
except ClaudeNotFoundError as e:
|
|
201
|
+
self._handle_missing("claude", e)
|
|
202
|
+
|
|
203
|
+
return self
|
|
204
|
+
|
|
205
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
def _handle_missing(self, name: str, error: Exception):
|
|
209
|
+
"""Handle a missing dependency based on on_missing setting."""
|
|
210
|
+
self._missing.append(name)
|
|
211
|
+
|
|
212
|
+
if self.on_missing == "raise":
|
|
213
|
+
raise error
|
|
214
|
+
elif self.on_missing == "warn":
|
|
215
|
+
import warnings
|
|
216
|
+
warnings.warn(str(error), UserWarning)
|
|
217
|
+
# "ignore" does nothing
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def missing_dependencies(self) -> list:
|
|
221
|
+
"""List of missing dependencies."""
|
|
222
|
+
return self._missing.copy()
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def all_available(self) -> bool:
|
|
226
|
+
"""Whether all required dependencies are available."""
|
|
227
|
+
return len(self._missing) == 0
|
overcode/exceptions.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exception hierarchy for Overcode.
|
|
3
|
+
|
|
4
|
+
Provides domain-specific exceptions for better error handling and debugging.
|
|
5
|
+
All exceptions inherit from OvercodeError for easy catching of any
|
|
6
|
+
overcode-related error.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OvercodeError(Exception):
|
|
11
|
+
"""Base exception for all Overcode errors."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# =============================================================================
|
|
17
|
+
# State Management Errors
|
|
18
|
+
# =============================================================================
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class StateError(OvercodeError):
|
|
22
|
+
"""Error related to state file operations."""
|
|
23
|
+
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StateReadError(StateError):
|
|
28
|
+
"""Error reading state from file."""
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class StateWriteError(StateError):
|
|
34
|
+
"""Error writing state to file."""
|
|
35
|
+
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class StateCorruptedError(StateError):
|
|
40
|
+
"""State file is corrupted or invalid."""
|
|
41
|
+
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# =============================================================================
|
|
46
|
+
# Tmux Errors
|
|
47
|
+
# =============================================================================
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TmuxError(OvercodeError):
|
|
51
|
+
"""Error related to tmux operations."""
|
|
52
|
+
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TmuxSessionError(TmuxError):
|
|
57
|
+
"""Error with tmux session operations."""
|
|
58
|
+
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TmuxWindowError(TmuxError):
|
|
63
|
+
"""Error with tmux window operations."""
|
|
64
|
+
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TmuxPaneError(TmuxError):
|
|
69
|
+
"""Error with tmux pane operations."""
|
|
70
|
+
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TmuxNotFoundError(TmuxError):
|
|
75
|
+
"""Tmux is not installed or not found."""
|
|
76
|
+
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# =============================================================================
|
|
81
|
+
# Session/Agent Errors
|
|
82
|
+
# =============================================================================
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class SessionError(OvercodeError):
|
|
86
|
+
"""Error related to agent session operations."""
|
|
87
|
+
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class InvalidSessionNameError(SessionError):
|
|
92
|
+
"""Session name is invalid."""
|
|
93
|
+
|
|
94
|
+
# Valid session name pattern: alphanumeric, underscore, hyphen, 1-64 chars
|
|
95
|
+
VALID_PATTERN = r"^[a-zA-Z0-9_-]{1,64}$"
|
|
96
|
+
|
|
97
|
+
def __init__(self, name: str, reason: str = None):
|
|
98
|
+
self.name = name
|
|
99
|
+
if reason:
|
|
100
|
+
msg = f"Invalid session name '{name}': {reason}"
|
|
101
|
+
else:
|
|
102
|
+
msg = f"Invalid session name '{name}'. Use only letters, numbers, underscore, hyphen (1-64 chars)"
|
|
103
|
+
super().__init__(msg)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class SessionNotFoundError(SessionError):
|
|
107
|
+
"""Agent session was not found."""
|
|
108
|
+
|
|
109
|
+
def __init__(self, name: str):
|
|
110
|
+
self.name = name
|
|
111
|
+
super().__init__(f"Session '{name}' not found")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class SessionAlreadyExistsError(SessionError):
|
|
115
|
+
"""Agent session already exists."""
|
|
116
|
+
|
|
117
|
+
def __init__(self, name: str):
|
|
118
|
+
self.name = name
|
|
119
|
+
super().__init__(f"Session '{name}' already exists")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class SessionLaunchError(SessionError):
|
|
123
|
+
"""Error launching an agent session."""
|
|
124
|
+
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class SessionKillError(SessionError):
|
|
129
|
+
"""Error killing an agent session."""
|
|
130
|
+
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# =============================================================================
|
|
135
|
+
# Claude Errors
|
|
136
|
+
# =============================================================================
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class ClaudeError(OvercodeError):
|
|
140
|
+
"""Error related to Claude Code operations."""
|
|
141
|
+
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ClaudeNotFoundError(ClaudeError):
|
|
146
|
+
"""Claude Code is not installed or not found."""
|
|
147
|
+
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class ClaudeStartupError(ClaudeError):
|
|
152
|
+
"""Error starting Claude Code process."""
|
|
153
|
+
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# =============================================================================
|
|
158
|
+
# Configuration Errors
|
|
159
|
+
# =============================================================================
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ConfigError(OvercodeError):
|
|
163
|
+
"""Error related to configuration."""
|
|
164
|
+
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class ConfigReadError(ConfigError):
|
|
169
|
+
"""Error reading configuration file."""
|
|
170
|
+
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class ConfigValidationError(ConfigError):
|
|
175
|
+
"""Configuration validation failed."""
|
|
176
|
+
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# =============================================================================
|
|
181
|
+
# Daemon Errors
|
|
182
|
+
# =============================================================================
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class DaemonError(OvercodeError):
|
|
186
|
+
"""Error related to daemon operations."""
|
|
187
|
+
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class DaemonAlreadyRunningError(DaemonError):
|
|
192
|
+
"""Daemon is already running."""
|
|
193
|
+
|
|
194
|
+
def __init__(self, pid: int):
|
|
195
|
+
self.pid = pid
|
|
196
|
+
super().__init__(f"Daemon already running (PID {pid})")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class DaemonNotRunningError(DaemonError):
|
|
200
|
+
"""Daemon is not running."""
|
|
201
|
+
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# =============================================================================
|
|
206
|
+
# Presence Logger Errors
|
|
207
|
+
# =============================================================================
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class PresenceError(OvercodeError):
|
|
211
|
+
"""Error related to presence logging."""
|
|
212
|
+
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class PresenceApiUnavailableError(PresenceError):
|
|
217
|
+
"""macOS presence APIs are not available."""
|
|
218
|
+
|
|
219
|
+
pass
|