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
@@ -0,0 +1,448 @@
1
+ """
2
+ Read Claude Code's history and session files for interaction/token counting.
3
+
4
+ Claude Code stores data in:
5
+ - ~/.claude/history.jsonl - interaction history (prompts sent)
6
+ - ~/.claude/projects/{encoded-path}/{sessionId}.jsonl - full conversation with token usage
7
+
8
+ Each assistant message in session files has usage data:
9
+ {
10
+ "usage": {
11
+ "input_tokens": 1003,
12
+ "cache_creation_input_tokens": 2884,
13
+ "cache_read_input_tokens": 25944,
14
+ "output_tokens": 278
15
+ }
16
+ }
17
+ """
18
+
19
+ import json
20
+ from pathlib import Path
21
+ from datetime import datetime
22
+ from typing import List, Optional, TYPE_CHECKING
23
+ from dataclasses import dataclass
24
+
25
+ if TYPE_CHECKING:
26
+ from .session_manager import Session
27
+
28
+
29
+ CLAUDE_HISTORY_PATH = Path.home() / ".claude" / "history.jsonl"
30
+ CLAUDE_PROJECTS_PATH = Path.home() / ".claude" / "projects"
31
+
32
+
33
+ @dataclass
34
+ class ClaudeSessionStats:
35
+ """Statistics for a Claude Code session."""
36
+ interaction_count: int
37
+ input_tokens: int
38
+ output_tokens: int
39
+ cache_creation_tokens: int
40
+ cache_read_tokens: int
41
+ work_times: List[float] # seconds per work cycle (prompt to next prompt)
42
+
43
+ @property
44
+ def total_tokens(self) -> int:
45
+ """Total tokens (input + output, not counting cache)."""
46
+ return self.input_tokens + self.output_tokens
47
+
48
+ @property
49
+ def total_tokens_with_cache(self) -> int:
50
+ """Total tokens including cache operations."""
51
+ return (self.input_tokens + self.output_tokens +
52
+ self.cache_creation_tokens + self.cache_read_tokens)
53
+
54
+ @property
55
+ def median_work_time(self) -> float:
56
+ """Median work time in seconds (50th percentile)."""
57
+ if not self.work_times:
58
+ return 0.0
59
+ sorted_times = sorted(self.work_times)
60
+ n = len(sorted_times)
61
+ if n % 2 == 0:
62
+ return (sorted_times[n // 2 - 1] + sorted_times[n // 2]) / 2
63
+ return sorted_times[n // 2]
64
+
65
+
66
+ @dataclass
67
+ class HistoryEntry:
68
+ """A single interaction from Claude Code history."""
69
+ display: str
70
+ timestamp_ms: int
71
+ project: Optional[str]
72
+ session_id: Optional[str]
73
+
74
+ @property
75
+ def timestamp(self) -> datetime:
76
+ """Convert millisecond timestamp to datetime."""
77
+ return datetime.fromtimestamp(self.timestamp_ms / 1000)
78
+
79
+
80
+ def read_history(history_path: Path = CLAUDE_HISTORY_PATH) -> List[HistoryEntry]:
81
+ """Read all entries from history.jsonl.
82
+
83
+ Args:
84
+ history_path: Path to history file (defaults to ~/.claude/history.jsonl)
85
+
86
+ Returns:
87
+ List of HistoryEntry objects, oldest first
88
+ """
89
+ if not history_path.exists():
90
+ return []
91
+
92
+ entries = []
93
+ try:
94
+ with open(history_path, 'r') as f:
95
+ for line in f:
96
+ line = line.strip()
97
+ if not line:
98
+ continue
99
+ try:
100
+ data = json.loads(line)
101
+ entry = HistoryEntry(
102
+ display=data.get("display", ""),
103
+ timestamp_ms=data.get("timestamp", 0),
104
+ project=data.get("project"),
105
+ session_id=data.get("sessionId"),
106
+ )
107
+ entries.append(entry)
108
+ except (json.JSONDecodeError, KeyError):
109
+ # Skip malformed entries
110
+ continue
111
+ except IOError:
112
+ return []
113
+
114
+ return entries
115
+
116
+
117
+ def get_interactions_for_session(
118
+ session: "Session",
119
+ history_path: Path = CLAUDE_HISTORY_PATH
120
+ ) -> List[HistoryEntry]:
121
+ """Get history entries matching a session.
122
+
123
+ Matches by:
124
+ 1. Project path == session.start_directory
125
+ 2. Timestamp >= session.start_time
126
+
127
+ Args:
128
+ session: The overcode Session to match
129
+ history_path: Path to history file
130
+
131
+ Returns:
132
+ List of matching HistoryEntry objects
133
+ """
134
+ if not session.start_directory:
135
+ return []
136
+
137
+ # Parse session start time
138
+ try:
139
+ session_start = datetime.fromisoformat(session.start_time)
140
+ session_start_ms = int(session_start.timestamp() * 1000)
141
+ except (ValueError, TypeError):
142
+ return []
143
+
144
+ # Normalize the project path for comparison
145
+ session_dir = str(Path(session.start_directory).resolve())
146
+
147
+ entries = read_history(history_path)
148
+ matching = []
149
+
150
+ for entry in entries:
151
+ # Must be after session started
152
+ if entry.timestamp_ms < session_start_ms:
153
+ continue
154
+
155
+ # Must match project directory
156
+ if entry.project:
157
+ entry_dir = str(Path(entry.project).resolve())
158
+ if entry_dir == session_dir:
159
+ matching.append(entry)
160
+
161
+ return matching
162
+
163
+
164
+ def count_interactions(
165
+ session: "Session",
166
+ history_path: Path = CLAUDE_HISTORY_PATH
167
+ ) -> int:
168
+ """Count interactions for a session.
169
+
170
+ Args:
171
+ session: The overcode Session to count for
172
+ history_path: Path to history file
173
+
174
+ Returns:
175
+ Number of interactions (user prompts) for this session
176
+ """
177
+ return len(get_interactions_for_session(session, history_path))
178
+
179
+
180
+ def get_session_ids_for_session(
181
+ session: "Session",
182
+ history_path: Path = CLAUDE_HISTORY_PATH
183
+ ) -> List[str]:
184
+ """Get unique Claude Code sessionIds for an overcode session.
185
+
186
+ One overcode session may span multiple Claude Code sessions
187
+ (if Claude is restarted in the same tmux window).
188
+
189
+ Args:
190
+ session: The overcode Session
191
+ history_path: Path to history file
192
+
193
+ Returns:
194
+ List of unique sessionId strings
195
+ """
196
+ entries = get_interactions_for_session(session, history_path)
197
+ session_ids = set()
198
+ for entry in entries:
199
+ if entry.session_id:
200
+ session_ids.add(entry.session_id)
201
+ return sorted(session_ids)
202
+
203
+
204
+ def encode_project_path(path: str) -> str:
205
+ """Encode a project path to Claude Code's directory naming format.
206
+
207
+ Claude Code stores project data in directories named like:
208
+ /home/user/myproject -> -home-user-myproject
209
+
210
+ Args:
211
+ path: The project path to encode
212
+
213
+ Returns:
214
+ Encoded directory name
215
+ """
216
+ # Resolve to absolute path and replace / with -
217
+ resolved = str(Path(path).resolve())
218
+ # Replace path separators with dashes, prepend dash
219
+ return resolved.replace("/", "-")
220
+
221
+
222
+ def get_session_file_path(
223
+ project_path: str,
224
+ session_id: str,
225
+ projects_path: Path = CLAUDE_PROJECTS_PATH
226
+ ) -> Path:
227
+ """Get the path to a Claude Code session JSONL file.
228
+
229
+ Args:
230
+ project_path: The project directory path
231
+ session_id: The Claude Code sessionId
232
+ projects_path: Base path for Claude projects
233
+
234
+ Returns:
235
+ Path to the session JSONL file
236
+ """
237
+ encoded = encode_project_path(project_path)
238
+ return projects_path / encoded / f"{session_id}.jsonl"
239
+
240
+
241
+ def read_token_usage_from_session_file(
242
+ session_file: Path,
243
+ since: Optional[datetime] = None
244
+ ) -> dict:
245
+ """Read token usage from a Claude Code session JSONL file.
246
+
247
+ Args:
248
+ session_file: Path to the session JSONL file
249
+ since: Only count tokens from messages after this time
250
+
251
+ Returns:
252
+ Dict with input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens
253
+ """
254
+ totals = {
255
+ "input_tokens": 0,
256
+ "output_tokens": 0,
257
+ "cache_creation_tokens": 0,
258
+ "cache_read_tokens": 0,
259
+ }
260
+
261
+ if not session_file.exists():
262
+ return totals
263
+
264
+ try:
265
+ with open(session_file, 'r') as f:
266
+ for line in f:
267
+ line = line.strip()
268
+ if not line:
269
+ continue
270
+ try:
271
+ data = json.loads(line)
272
+ # Only assistant messages have usage data
273
+ if data.get("type") == "assistant":
274
+ # Check timestamp if filtering by time
275
+ if since:
276
+ ts_str = data.get("timestamp")
277
+ if ts_str:
278
+ try:
279
+ # Parse ISO timestamp (e.g., "2026-01-02T06:56:01.975Z")
280
+ msg_time = datetime.fromisoformat(
281
+ ts_str.replace("Z", "+00:00")
282
+ ).replace(tzinfo=None)
283
+ if msg_time < since:
284
+ continue
285
+ except (ValueError, TypeError):
286
+ pass
287
+
288
+ message = data.get("message", {})
289
+ usage = message.get("usage", {})
290
+ if usage:
291
+ totals["input_tokens"] += usage.get("input_tokens", 0)
292
+ totals["output_tokens"] += usage.get("output_tokens", 0)
293
+ totals["cache_creation_tokens"] += usage.get(
294
+ "cache_creation_input_tokens", 0
295
+ )
296
+ totals["cache_read_tokens"] += usage.get(
297
+ "cache_read_input_tokens", 0
298
+ )
299
+ except (json.JSONDecodeError, KeyError, TypeError):
300
+ continue
301
+ except IOError:
302
+ pass
303
+
304
+ return totals
305
+
306
+
307
+ def read_work_times_from_session_file(
308
+ session_file: Path,
309
+ since: Optional[datetime] = None
310
+ ) -> List[float]:
311
+ """Calculate work times from a Claude Code session file.
312
+
313
+ Work time = time from one user prompt to the next user prompt.
314
+ This represents how long the agent worked autonomously.
315
+
316
+ Only counts actual user prompts (not tool results which are automatic).
317
+
318
+ Args:
319
+ session_file: Path to the session JSONL file
320
+ since: Only count work times from messages after this time
321
+
322
+ Returns:
323
+ List of work times in seconds
324
+ """
325
+ if not session_file.exists():
326
+ return []
327
+
328
+ user_prompt_times: List[datetime] = []
329
+
330
+ try:
331
+ with open(session_file, 'r') as f:
332
+ for line in f:
333
+ line = line.strip()
334
+ if not line:
335
+ continue
336
+ try:
337
+ data = json.loads(line)
338
+ if data.get("type") != "user":
339
+ continue
340
+
341
+ # Check if this is an actual user prompt (not a tool result)
342
+ message = data.get("message", {})
343
+ content = message.get("content", "")
344
+
345
+ # Tool results have content as a list with tool_result type
346
+ if isinstance(content, list):
347
+ # Check if it's a tool result
348
+ if content and content[0].get("type") == "tool_result":
349
+ continue
350
+
351
+ # Parse timestamp
352
+ ts_str = data.get("timestamp")
353
+ if not ts_str:
354
+ continue
355
+
356
+ try:
357
+ msg_time = datetime.fromisoformat(
358
+ ts_str.replace("Z", "+00:00")
359
+ ).replace(tzinfo=None)
360
+
361
+ # Filter by since time
362
+ if since and msg_time < since:
363
+ continue
364
+
365
+ user_prompt_times.append(msg_time)
366
+ except (ValueError, TypeError):
367
+ continue
368
+
369
+ except (json.JSONDecodeError, KeyError, TypeError):
370
+ continue
371
+ except IOError:
372
+ return []
373
+
374
+ # Calculate durations between consecutive prompts
375
+ work_times = []
376
+ for i in range(1, len(user_prompt_times)):
377
+ duration = (user_prompt_times[i] - user_prompt_times[i - 1]).total_seconds()
378
+ if duration > 0:
379
+ work_times.append(duration)
380
+
381
+ return work_times
382
+
383
+
384
+ def get_session_stats(
385
+ session: "Session",
386
+ history_path: Path = CLAUDE_HISTORY_PATH,
387
+ projects_path: Path = CLAUDE_PROJECTS_PATH
388
+ ) -> Optional[ClaudeSessionStats]:
389
+ """Get comprehensive stats for an overcode session.
390
+
391
+ Combines interaction counting with token usage from session files.
392
+
393
+ Args:
394
+ session: The overcode Session
395
+ history_path: Path to history.jsonl
396
+ projects_path: Path to Claude projects directory
397
+
398
+ Returns:
399
+ ClaudeSessionStats if session has start_directory, None otherwise
400
+ """
401
+ if not session.start_directory:
402
+ return None
403
+
404
+ # Parse session start time for filtering
405
+ try:
406
+ session_start = datetime.fromisoformat(session.start_time)
407
+ except (ValueError, TypeError):
408
+ return None
409
+
410
+ # Get interaction count and session IDs
411
+ interactions = get_interactions_for_session(session, history_path)
412
+ interaction_count = len(interactions)
413
+
414
+ # Get unique session IDs
415
+ session_ids = set()
416
+ for entry in interactions:
417
+ if entry.session_id:
418
+ session_ids.add(entry.session_id)
419
+
420
+ # Sum token usage and work times across all session files
421
+ total_input = 0
422
+ total_output = 0
423
+ total_cache_creation = 0
424
+ total_cache_read = 0
425
+ all_work_times: List[float] = []
426
+
427
+ for sid in session_ids:
428
+ session_file = get_session_file_path(
429
+ session.start_directory, sid, projects_path
430
+ )
431
+ usage = read_token_usage_from_session_file(session_file, since=session_start)
432
+ total_input += usage["input_tokens"]
433
+ total_output += usage["output_tokens"]
434
+ total_cache_creation += usage["cache_creation_tokens"]
435
+ total_cache_read += usage["cache_read_tokens"]
436
+
437
+ # Collect work times from this session file
438
+ work_times = read_work_times_from_session_file(session_file, since=session_start)
439
+ all_work_times.extend(work_times)
440
+
441
+ return ClaudeSessionStats(
442
+ interaction_count=interaction_count,
443
+ input_tokens=total_input,
444
+ output_tokens=total_output,
445
+ cache_creation_tokens=total_cache_creation,
446
+ cache_read_tokens=total_cache_read,
447
+ work_times=all_work_times,
448
+ )
@@ -0,0 +1,214 @@
1
+ """
2
+ Real implementations of protocol interfaces.
3
+
4
+ These are production implementations that make actual subprocess calls
5
+ to tmux, perform real file I/O, etc.
6
+ """
7
+
8
+ import json
9
+ import subprocess
10
+ import os
11
+ import time
12
+ from pathlib import Path
13
+ from typing import Optional, List, Dict, Any
14
+
15
+
16
+ class RealTmux:
17
+ """Production implementation of TmuxInterface using subprocess"""
18
+
19
+ def capture_pane(self, session: str, window: int, lines: int = 100) -> Optional[str]:
20
+ try:
21
+ result = subprocess.run(
22
+ ["tmux", "capture-pane", "-t", f"{session}:{window}",
23
+ "-p", "-S", f"-{lines}"],
24
+ capture_output=True, text=True, timeout=5
25
+ )
26
+ if result.returncode == 0:
27
+ return result.stdout
28
+ return None
29
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError):
30
+ return None
31
+
32
+ def send_keys(self, session: str, window: int, keys: str, enter: bool = True) -> bool:
33
+ try:
34
+ # For Claude Code: text and Enter must be sent as SEPARATE commands
35
+ # with a small delay, otherwise Claude Code doesn't process the Enter.
36
+ target = f"{session}:{window}"
37
+
38
+ # Send text first (if any)
39
+ if keys:
40
+ result = subprocess.run(
41
+ ["tmux", "send-keys", "-t", target, keys],
42
+ timeout=5, capture_output=True
43
+ )
44
+ if result.returncode != 0:
45
+ return False
46
+ # Small delay for Claude Code to process text
47
+ time.sleep(0.1)
48
+
49
+ # Send Enter separately
50
+ if enter:
51
+ result = subprocess.run(
52
+ ["tmux", "send-keys", "-t", target, "Enter"],
53
+ timeout=5, capture_output=True
54
+ )
55
+ if result.returncode != 0:
56
+ return False
57
+
58
+ return True
59
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError):
60
+ return False
61
+
62
+ def has_session(self, session: str) -> bool:
63
+ try:
64
+ result = subprocess.run(
65
+ ["tmux", "has-session", "-t", session],
66
+ capture_output=True, timeout=5
67
+ )
68
+ return result.returncode == 0
69
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError):
70
+ return False
71
+
72
+ def new_session(self, session: str) -> bool:
73
+ try:
74
+ result = subprocess.run(
75
+ ["tmux", "new-session", "-d", "-s", session],
76
+ capture_output=True, timeout=5
77
+ )
78
+ return result.returncode == 0
79
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError):
80
+ return False
81
+
82
+ def new_window(self, session: str, name: str, command: Optional[List[str]] = None,
83
+ cwd: Optional[str] = None) -> Optional[int]:
84
+ try:
85
+ cmd = ["tmux", "new-window", "-t", session, "-n", name, "-P", "-F", "#{window_index}"]
86
+ if cwd:
87
+ cmd.extend(["-c", cwd])
88
+ if command:
89
+ cmd.append(" ".join(command))
90
+
91
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
92
+ if result.returncode == 0:
93
+ return int(result.stdout.strip())
94
+ return None
95
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError, ValueError):
96
+ return None
97
+
98
+ def kill_window(self, session: str, window: int) -> bool:
99
+ try:
100
+ result = subprocess.run(
101
+ ["tmux", "kill-window", "-t", f"{session}:{window}"],
102
+ capture_output=True, timeout=5
103
+ )
104
+ return result.returncode == 0
105
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError):
106
+ return False
107
+
108
+ def kill_session(self, session: str) -> bool:
109
+ try:
110
+ result = subprocess.run(
111
+ ["tmux", "kill-session", "-t", session],
112
+ capture_output=True, timeout=5
113
+ )
114
+ return result.returncode == 0
115
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError):
116
+ return False
117
+
118
+ def list_windows(self, session: str) -> List[Dict[str, Any]]:
119
+ try:
120
+ result = subprocess.run(
121
+ ["tmux", "list-windows", "-t", session, "-F",
122
+ "#{window_index}:#{window_name}:#{window_active}"],
123
+ capture_output=True, text=True, timeout=5
124
+ )
125
+ if result.returncode != 0:
126
+ return []
127
+
128
+ windows = []
129
+ for line in result.stdout.strip().split('\n'):
130
+ if line:
131
+ parts = line.split(':')
132
+ if len(parts) >= 3:
133
+ windows.append({
134
+ 'index': int(parts[0]),
135
+ 'name': parts[1],
136
+ 'active': parts[2] == '1'
137
+ })
138
+ return windows
139
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError):
140
+ return []
141
+
142
+ def attach(self, session: str) -> None:
143
+ os.execlp("tmux", "tmux", "attach-session", "-t", session)
144
+
145
+
146
+ class RealFileSystem:
147
+ """Production implementation of FileSystemInterface"""
148
+
149
+ def read_json(self, path: Path) -> Optional[Dict[str, Any]]:
150
+ try:
151
+ if not path.exists():
152
+ return None
153
+ with open(path, 'r') as f:
154
+ return json.load(f)
155
+ except (json.JSONDecodeError, IOError):
156
+ return None
157
+
158
+ def write_json(self, path: Path, data: Dict[str, Any]) -> bool:
159
+ try:
160
+ # Write atomically via temp file
161
+ temp_path = path.with_suffix('.tmp')
162
+ with open(temp_path, 'w') as f:
163
+ json.dump(data, f, indent=2)
164
+ temp_path.replace(path)
165
+ return True
166
+ except IOError:
167
+ return False
168
+
169
+ def exists(self, path: Path) -> bool:
170
+ return path.exists()
171
+
172
+ def mkdir(self, path: Path, parents: bool = True) -> bool:
173
+ try:
174
+ path.mkdir(parents=parents, exist_ok=True)
175
+ return True
176
+ except IOError:
177
+ return False
178
+
179
+ def read_text(self, path: Path) -> Optional[str]:
180
+ try:
181
+ return path.read_text()
182
+ except IOError:
183
+ return None
184
+
185
+ def write_text(self, path: Path, content: str) -> bool:
186
+ try:
187
+ path.write_text(content)
188
+ return True
189
+ except IOError:
190
+ return False
191
+
192
+
193
+ class RealSubprocess:
194
+ """Production implementation of SubprocessInterface"""
195
+
196
+ def run(self, cmd: List[str], timeout: Optional[int] = None,
197
+ capture_output: bool = True) -> Optional[Dict[str, Any]]:
198
+ try:
199
+ result = subprocess.run(
200
+ cmd, timeout=timeout, capture_output=capture_output, text=True
201
+ )
202
+ return {
203
+ 'returncode': result.returncode,
204
+ 'stdout': result.stdout if capture_output else '',
205
+ 'stderr': result.stderr if capture_output else ''
206
+ }
207
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError):
208
+ return None
209
+
210
+ def popen(self, cmd: List[str], cwd: Optional[str] = None) -> Any:
211
+ try:
212
+ return subprocess.Popen(cmd, cwd=cwd)
213
+ except subprocess.SubprocessError:
214
+ return None
overcode/interfaces.py ADDED
@@ -0,0 +1,49 @@
1
+ """
2
+ Protocol definitions and implementations for external dependencies.
3
+
4
+ This module re-exports from the split modules for backward compatibility:
5
+ - protocols.py: Protocol (interface) definitions
6
+ - implementations.py: Real (production) implementations
7
+ - mocks.py: Mock implementations for testing
8
+
9
+ New code should import directly from the specific modules:
10
+ from overcode.protocols import TmuxInterface
11
+ from overcode.implementations import RealTmux
12
+ from overcode.mocks import MockTmux
13
+ """
14
+
15
+ # Re-export protocols
16
+ from .protocols import (
17
+ TmuxInterface,
18
+ FileSystemInterface,
19
+ SubprocessInterface,
20
+ )
21
+
22
+ # Re-export real implementations
23
+ from .implementations import (
24
+ RealTmux,
25
+ RealFileSystem,
26
+ RealSubprocess,
27
+ )
28
+
29
+ # Re-export mocks
30
+ from .mocks import (
31
+ MockTmux,
32
+ MockFileSystem,
33
+ MockSubprocess,
34
+ )
35
+
36
+ __all__ = [
37
+ # Protocols
38
+ "TmuxInterface",
39
+ "FileSystemInterface",
40
+ "SubprocessInterface",
41
+ # Real implementations
42
+ "RealTmux",
43
+ "RealFileSystem",
44
+ "RealSubprocess",
45
+ # Mocks
46
+ "MockTmux",
47
+ "MockFileSystem",
48
+ "MockSubprocess",
49
+ ]