overcode 0.1.6__tar.gz → 0.1.7__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.6/src/overcode.egg-info → overcode-0.1.7}/PKG-INFO +1 -1
- {overcode-0.1.6 → overcode-0.1.7}/pyproject.toml +1 -1
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/claude_config.py +49 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/cli.py +238 -132
- overcode-0.1.7/src/overcode/dependency_check.py +111 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/history_reader.py +85 -21
- overcode-0.1.7/src/overcode/hook_handler.py +108 -0
- overcode-0.1.7/src/overcode/hook_status_detector.py +189 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/launcher.py +10 -68
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/monitor_daemon.py +43 -17
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/protocols.py +28 -1
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/session_manager.py +26 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/settings.py +6 -9
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/status_detector.py +42 -45
- overcode-0.1.7/src/overcode/status_detector_factory.py +73 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/status_patterns.py +46 -12
- overcode-0.1.7/src/overcode/summary_columns.py +701 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/summary_groups.py +24 -6
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/supervisor_daemon.py +13 -51
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tui.py +312 -164
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tui.tcss +5 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tui_actions/input.py +1 -1
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tui_actions/navigation.py +5 -6
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tui_actions/session.py +133 -222
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tui_actions/view.py +2 -2
- overcode-0.1.7/src/overcode/tui_formatters.py +235 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tui_helpers.py +18 -227
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tui_widgets/daemon_panel.py +27 -14
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tui_widgets/daemon_status_bar.py +66 -28
- overcode-0.1.7/src/overcode/tui_widgets/preview_pane.py +103 -0
- overcode-0.1.7/src/overcode/tui_widgets/session_summary.py +437 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tui_widgets/status_timeline.py +26 -12
- {overcode-0.1.6 → overcode-0.1.7/src/overcode.egg-info}/PKG-INFO +1 -1
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode.egg-info/SOURCES.txt +5 -0
- overcode-0.1.6/src/overcode/dependency_check.py +0 -227
- overcode-0.1.6/src/overcode/tui_widgets/preview_pane.py +0 -69
- overcode-0.1.6/src/overcode/tui_widgets/session_summary.py +0 -621
- {overcode-0.1.6 → overcode-0.1.7}/LICENSE +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/MANIFEST.in +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/README.md +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/setup.cfg +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/__init__.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/config.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/daemon_claude_skill.md +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/daemon_logging.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/daemon_utils.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/data_export.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/exceptions.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/implementations.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/interfaces.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/logging_config.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/mocks.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/monitor_daemon_core.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/monitor_daemon_state.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/pid_utils.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/presence_logger.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/standing_instructions.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/status_constants.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/status_history.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/summarizer_client.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/summarizer_component.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/supervisor_daemon_core.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/supervisor_layout.sh +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/testing/__init__.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/testing/renderer.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/testing/tmux_driver.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/testing/tui_eye.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/testing/tui_eye_skill.md +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/time_context.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tmux_manager.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tmux_utils.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tui_actions/__init__.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tui_actions/daemon.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tui_logic.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tui_render.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tui_widgets/__init__.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tui_widgets/command_bar.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tui_widgets/help_overlay.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/tui_widgets/summary_config_modal.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/web_api.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/web_chartjs.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/web_server.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/web_server_runner.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode/web_templates.py +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode.egg-info/dependency_links.txt +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode.egg-info/entry_points.txt +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode.egg-info/requires.txt +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/src/overcode.egg-info/top_level.txt +0 -0
- {overcode-0.1.6 → overcode-0.1.7}/tests/test_e2e_multi_agent_jokes.py +0 -0
|
@@ -88,3 +88,52 @@ class ClaudeConfigEditor:
|
|
|
88
88
|
|
|
89
89
|
self.save(updated)
|
|
90
90
|
return True
|
|
91
|
+
|
|
92
|
+
def remove_hook(self, event: str, command: str) -> bool:
|
|
93
|
+
"""Remove a matcher group containing this command.
|
|
94
|
+
|
|
95
|
+
Returns True if found and removed, False if not found.
|
|
96
|
+
Cleans up empty event arrays and empty hooks dict.
|
|
97
|
+
"""
|
|
98
|
+
settings = self.load()
|
|
99
|
+
hooks_dict = settings.get("hooks", {})
|
|
100
|
+
event_list = hooks_dict.get(event, [])
|
|
101
|
+
|
|
102
|
+
# Find the matcher group index containing this command
|
|
103
|
+
index_to_remove = None
|
|
104
|
+
for i, entry in enumerate(event_list):
|
|
105
|
+
for hook in entry.get("hooks", []):
|
|
106
|
+
if hook.get("command") == command:
|
|
107
|
+
index_to_remove = i
|
|
108
|
+
break
|
|
109
|
+
if index_to_remove is not None:
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
if index_to_remove is None:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
updated = copy.deepcopy(settings)
|
|
116
|
+
del updated["hooks"][event][index_to_remove]
|
|
117
|
+
|
|
118
|
+
# Clean up empty event array
|
|
119
|
+
if not updated["hooks"][event]:
|
|
120
|
+
del updated["hooks"][event]
|
|
121
|
+
|
|
122
|
+
# Clean up empty hooks dict
|
|
123
|
+
if not updated["hooks"]:
|
|
124
|
+
del updated["hooks"]
|
|
125
|
+
|
|
126
|
+
self.save(updated)
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
def list_hooks_matching(self, command_prefix: str) -> list[tuple[str, str]]:
|
|
130
|
+
"""Return [(event, command)] for all hooks whose command starts with prefix."""
|
|
131
|
+
settings = self.load()
|
|
132
|
+
results = []
|
|
133
|
+
for event, entries in settings.get("hooks", {}).items():
|
|
134
|
+
for entry in entries:
|
|
135
|
+
for hook in entry.get("hooks", []):
|
|
136
|
+
cmd = hook.get("command", "")
|
|
137
|
+
if cmd.startswith(command_prefix):
|
|
138
|
+
results.append((event, cmd))
|
|
139
|
+
return results
|
|
@@ -40,6 +40,14 @@ supervisor_daemon_app = typer.Typer(
|
|
|
40
40
|
)
|
|
41
41
|
app.add_typer(supervisor_daemon_app, name="supervisor-daemon")
|
|
42
42
|
|
|
43
|
+
# Hooks subcommand group
|
|
44
|
+
hooks_app = typer.Typer(
|
|
45
|
+
name="hooks",
|
|
46
|
+
help="Manage Claude Code hook integration.",
|
|
47
|
+
no_args_is_help=True,
|
|
48
|
+
)
|
|
49
|
+
app.add_typer(hooks_app, name="hooks")
|
|
50
|
+
|
|
43
51
|
# Config subcommand group
|
|
44
52
|
config_app = typer.Typer(
|
|
45
53
|
name="config",
|
|
@@ -128,12 +136,11 @@ def launch(
|
|
|
128
136
|
@app.command("list")
|
|
129
137
|
def list_agents(session: SessionOption = "agents"):
|
|
130
138
|
"""List running agents with status."""
|
|
131
|
-
from .status_detector import StatusDetector
|
|
132
|
-
from .history_reader import get_session_stats
|
|
133
139
|
from .tui_helpers import (
|
|
134
140
|
calculate_uptime, format_duration, format_tokens,
|
|
135
141
|
get_current_state_times, get_status_symbol
|
|
136
142
|
)
|
|
143
|
+
from .monitor_daemon_state import get_monitor_daemon_state
|
|
137
144
|
|
|
138
145
|
launcher = ClaudeLauncher(session)
|
|
139
146
|
sessions = launcher.list_sessions()
|
|
@@ -142,17 +149,39 @@ def list_agents(session: SessionOption = "agents"):
|
|
|
142
149
|
rprint("[dim]No running agents[/dim]")
|
|
143
150
|
return
|
|
144
151
|
|
|
145
|
-
|
|
152
|
+
# Prefer daemon state for status/activity (single source of truth)
|
|
153
|
+
daemon_state = get_monitor_daemon_state(session)
|
|
154
|
+
use_daemon = daemon_state is not None and not daemon_state.is_stale()
|
|
155
|
+
|
|
156
|
+
# Only create detector as fallback when daemon isn't running
|
|
157
|
+
detector = None
|
|
158
|
+
if not use_daemon:
|
|
159
|
+
from .status_detector_factory import StatusDetectorDispatcher
|
|
160
|
+
detector = StatusDetectorDispatcher(session)
|
|
161
|
+
|
|
146
162
|
terminated_count = 0
|
|
147
163
|
|
|
148
164
|
for sess in sessions:
|
|
149
|
-
# For terminated sessions, use stored status; otherwise detect from tmux
|
|
150
165
|
if sess.status == "terminated":
|
|
151
166
|
status = "terminated"
|
|
152
167
|
activity = "(tmux window no longer exists)"
|
|
153
168
|
terminated_count += 1
|
|
169
|
+
elif use_daemon:
|
|
170
|
+
ds = daemon_state.get_session_by_name(sess.name)
|
|
171
|
+
if ds:
|
|
172
|
+
status = ds.current_status
|
|
173
|
+
activity = ds.current_activity
|
|
174
|
+
else:
|
|
175
|
+
# Session not yet in daemon state — detect directly
|
|
176
|
+
if detector is None:
|
|
177
|
+
from .status_detector_factory import StatusDetectorDispatcher
|
|
178
|
+
detector = StatusDetectorDispatcher(session)
|
|
179
|
+
status, activity, _ = detector.detect_status(sess)
|
|
154
180
|
else:
|
|
155
|
-
status, activity, _ =
|
|
181
|
+
status, activity, _ = detector.detect_status(sess)
|
|
182
|
+
|
|
183
|
+
if sess.is_asleep:
|
|
184
|
+
status = "asleep"
|
|
156
185
|
|
|
157
186
|
symbol, _ = get_status_symbol(status)
|
|
158
187
|
|
|
@@ -162,10 +191,10 @@ def list_agents(session: SessionOption = "agents"):
|
|
|
162
191
|
# Get state times using shared helper
|
|
163
192
|
green_time, non_green_time, sleep_time = get_current_state_times(sess.stats, is_asleep=sess.is_asleep)
|
|
164
193
|
|
|
165
|
-
#
|
|
166
|
-
stats =
|
|
167
|
-
if stats:
|
|
168
|
-
stats_display = f"{stats.interaction_count:>2}i {format_tokens(stats.
|
|
194
|
+
# Stats from session manager (already synced by daemon)
|
|
195
|
+
stats = sess.stats
|
|
196
|
+
if stats.interaction_count > 0:
|
|
197
|
+
stats_display = f"{stats.interaction_count:>2}i {format_tokens(stats.input_tokens + stats.output_tokens):>5}"
|
|
169
198
|
else:
|
|
170
199
|
stats_display = " -i -"
|
|
171
200
|
|
|
@@ -279,6 +308,40 @@ def set_budget(
|
|
|
279
308
|
rprint(f"[green]✓ Cleared budget for {name}[/green]")
|
|
280
309
|
|
|
281
310
|
|
|
311
|
+
@app.command()
|
|
312
|
+
def annotate(
|
|
313
|
+
name: Annotated[str, typer.Argument(help="Name of agent")],
|
|
314
|
+
text: Annotated[
|
|
315
|
+
Optional[List[str]], typer.Argument(help="Annotation text (omit to clear)")
|
|
316
|
+
] = None,
|
|
317
|
+
session: SessionOption = "agents",
|
|
318
|
+
):
|
|
319
|
+
"""Set or clear a human annotation on an agent (#223).
|
|
320
|
+
|
|
321
|
+
Allows programmatic annotation of agents so scripts and other tools
|
|
322
|
+
can communicate status to the overcode TUI.
|
|
323
|
+
|
|
324
|
+
Examples:
|
|
325
|
+
overcode annotate my-agent "Working on auth module"
|
|
326
|
+
overcode annotate my-agent Building the API layer
|
|
327
|
+
overcode annotate my-agent # Clear annotation
|
|
328
|
+
"""
|
|
329
|
+
from .session_manager import SessionManager
|
|
330
|
+
|
|
331
|
+
manager = SessionManager()
|
|
332
|
+
agent = manager.get_session_by_name(name)
|
|
333
|
+
if not agent:
|
|
334
|
+
rprint(f"[red]Error: Agent '{name}' not found[/red]")
|
|
335
|
+
raise typer.Exit(code=1)
|
|
336
|
+
|
|
337
|
+
annotation = " ".join(text) if text else ""
|
|
338
|
+
manager.set_human_annotation(agent.id, annotation)
|
|
339
|
+
if annotation:
|
|
340
|
+
rprint(f"[green]✓ Annotation set for {name}:[/green] {annotation}")
|
|
341
|
+
else:
|
|
342
|
+
rprint(f"[green]✓ Annotation cleared for {name}[/green]")
|
|
343
|
+
|
|
344
|
+
|
|
282
345
|
@app.command()
|
|
283
346
|
def send(
|
|
284
347
|
name: Annotated[str, typer.Argument(help="Name of agent")],
|
|
@@ -336,14 +399,10 @@ def show(
|
|
|
336
399
|
session: SessionOption = "agents",
|
|
337
400
|
):
|
|
338
401
|
"""Show agent details and recent output."""
|
|
339
|
-
from .status_detector import StatusDetector
|
|
340
402
|
from .history_reader import get_session_stats
|
|
341
403
|
from .status_patterns import extract_background_bash_count, extract_live_subagent_count, strip_ansi
|
|
342
|
-
from .tui_helpers import
|
|
343
|
-
|
|
344
|
-
format_line_count, get_current_state_times, get_status_symbol,
|
|
345
|
-
get_git_diff_stats,
|
|
346
|
-
)
|
|
404
|
+
from .tui_helpers import get_git_diff_stats
|
|
405
|
+
from .summary_columns import build_cli_context, render_cli_stats
|
|
347
406
|
from .monitor_daemon_state import get_monitor_daemon_state
|
|
348
407
|
|
|
349
408
|
launcher = ClaudeLauncher(session)
|
|
@@ -354,18 +413,39 @@ def show(
|
|
|
354
413
|
rprint(f"[red]✗[/red] Agent '[bold]{name}[/bold]' not found")
|
|
355
414
|
raise typer.Exit(1)
|
|
356
415
|
|
|
357
|
-
#
|
|
416
|
+
# Read daemon state for status/activity (single source of truth)
|
|
417
|
+
daemon_state = get_monitor_daemon_state(session)
|
|
418
|
+
daemon_session = None
|
|
419
|
+
if daemon_state and not daemon_state.is_stale():
|
|
420
|
+
daemon_session = daemon_state.get_session_by_name(name)
|
|
421
|
+
|
|
422
|
+
# Get status/activity from daemon state, falling back to detection
|
|
358
423
|
pane_content_raw = ""
|
|
359
424
|
if sess.status == "terminated":
|
|
360
425
|
status = "terminated"
|
|
361
426
|
activity = "(tmux window no longer exists)"
|
|
427
|
+
elif daemon_session:
|
|
428
|
+
status = daemon_session.current_status
|
|
429
|
+
activity = daemon_session.current_activity
|
|
362
430
|
else:
|
|
363
|
-
|
|
364
|
-
|
|
431
|
+
# Daemon not running — fall back to direct detection
|
|
432
|
+
from .status_detector_factory import create_status_detector
|
|
433
|
+
detector = create_status_detector(
|
|
434
|
+
session,
|
|
435
|
+
strategy="hooks" if sess.hook_status_detection else "polling",
|
|
436
|
+
)
|
|
437
|
+
status, activity, pane_content_raw = detector.detect_status(sess)
|
|
365
438
|
|
|
366
439
|
if sess.is_asleep:
|
|
367
440
|
status = "asleep"
|
|
368
441
|
|
|
442
|
+
# Capture pane content separately if needed for display or stats parsing
|
|
443
|
+
need_pane = (not stats_only and lines > 0) or not no_stats
|
|
444
|
+
if need_pane and not pane_content_raw and sess.status != "terminated":
|
|
445
|
+
from .status_detector_factory import StatusDetectorDispatcher
|
|
446
|
+
dispatcher = StatusDetectorDispatcher(session)
|
|
447
|
+
pane_content_raw = dispatcher.get_pane_content(sess.tmux_window, num_lines=max(lines, 50))
|
|
448
|
+
|
|
369
449
|
if not no_stats:
|
|
370
450
|
# Gather all stats
|
|
371
451
|
bg_bash_count = extract_background_bash_count(pane_content_raw) if pane_content_raw else 0
|
|
@@ -384,97 +464,40 @@ def show(
|
|
|
384
464
|
except Exception:
|
|
385
465
|
pass
|
|
386
466
|
|
|
387
|
-
|
|
388
|
-
green_time, non_green_time, sleep_time = get_current_state_times(
|
|
389
|
-
sess.stats, is_asleep=sess.is_asleep
|
|
390
|
-
)
|
|
391
|
-
active_time = green_time + non_green_time
|
|
392
|
-
active_pct = (green_time / active_time * 100) if active_time > 0 else 0
|
|
393
|
-
|
|
467
|
+
# AI summaries from daemon state
|
|
394
468
|
ai_short = ""
|
|
395
469
|
ai_context = ""
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
470
|
+
if daemon_session:
|
|
471
|
+
ai_short = daemon_session.activity_summary or ""
|
|
472
|
+
ai_context = daemon_session.activity_summary_context or ""
|
|
473
|
+
|
|
474
|
+
# Build context and render via column system
|
|
475
|
+
any_has_budget = sess.cost_budget_usd > 0
|
|
476
|
+
ctx = build_cli_context(
|
|
477
|
+
session=sess,
|
|
478
|
+
stats=sess.stats,
|
|
479
|
+
claude_stats=claude_stats,
|
|
480
|
+
git_diff_stats=git_diff,
|
|
481
|
+
status=status,
|
|
482
|
+
bg_bash_count=bg_bash_count,
|
|
483
|
+
live_sub_count=live_sub_count,
|
|
484
|
+
any_has_budget=any_has_budget,
|
|
485
|
+
)
|
|
405
486
|
|
|
406
|
-
# Status line
|
|
407
|
-
symbol, _ = get_status_symbol(status)
|
|
408
|
-
time_in_state = ""
|
|
409
|
-
if sess.stats.state_since:
|
|
410
|
-
try:
|
|
411
|
-
from datetime import datetime
|
|
412
|
-
elapsed = (datetime.now() - datetime.fromisoformat(sess.stats.state_since)).total_seconds()
|
|
413
|
-
time_in_state = f" ({format_duration(elapsed)})"
|
|
414
|
-
except (ValueError, TypeError):
|
|
415
|
-
pass
|
|
416
|
-
|
|
417
|
-
# Permissiveness emoji
|
|
418
|
-
perm_map = {"bypass": "🔥 bypass", "permissive": "🏃 permissive", "normal": "👮 normal"}
|
|
419
|
-
perm_display = perm_map.get(sess.permissiveness_mode, sess.permissiveness_mode)
|
|
420
|
-
|
|
421
|
-
# Render stats
|
|
422
487
|
print(f"=== {name} ===")
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
print(f"Repo: {repo_info:<28} Mode: {perm_display}")
|
|
427
|
-
print(f"Time ctx: {tc_display}")
|
|
428
|
-
|
|
429
|
-
# Time
|
|
430
|
-
time_str = f"▶ {format_duration(green_time):>5} active ⏸ {format_duration(non_green_time):>5} stalled 💤 {format_duration(sleep_time):>5} sleep ({active_pct:.0f}%)"
|
|
431
|
-
print(f"Time: {time_str}")
|
|
432
|
-
|
|
433
|
-
# Tokens & cost
|
|
434
|
-
if claude_stats:
|
|
435
|
-
token_str = f"Σ {format_tokens(claude_stats.total_tokens)}"
|
|
436
|
-
if claude_stats.current_context_tokens > 0:
|
|
437
|
-
ctx_pct = min(100, claude_stats.current_context_tokens / 200_000 * 100)
|
|
438
|
-
token_str += f" (context {ctx_pct:.0f}%)"
|
|
439
|
-
cost = sess.stats.estimated_cost_usd
|
|
440
|
-
budget = sess.cost_budget_usd
|
|
441
|
-
if budget > 0:
|
|
442
|
-
cost_display = f"{format_cost(cost)}/{format_cost(budget)}"
|
|
443
|
-
else:
|
|
444
|
-
cost_display = format_cost(cost)
|
|
445
|
-
print(f"Tokens: {token_str:<28} Cost: {cost_display}")
|
|
446
|
-
|
|
447
|
-
# Work & interactions
|
|
448
|
-
median_work = claude_stats.median_work_time
|
|
449
|
-
work_str = format_duration(median_work) if median_work > 0 else "-"
|
|
450
|
-
human_count = max(0, claude_stats.interaction_count - sess.stats.steers_count)
|
|
451
|
-
print(f"Work: ⏱ {work_str} median{'':<18} Interactions: 👤 {human_count} human 🤖 {sess.stats.steers_count} robot")
|
|
452
|
-
else:
|
|
453
|
-
print(f"Tokens: -")
|
|
454
|
-
|
|
455
|
-
# Git
|
|
456
|
-
if git_diff:
|
|
457
|
-
files, ins, dels = git_diff
|
|
458
|
-
print(f"Git: Δ{files} files +{format_line_count(ins)} -{format_line_count(dels)}")
|
|
459
|
-
|
|
460
|
-
# Subagents & background bashes (live counts from status bar)
|
|
461
|
-
print(f"Agents: 🤿 {live_sub_count} subagents 🐚 {bg_bash_count} background bashes")
|
|
462
|
-
|
|
463
|
-
# Standing orders
|
|
464
|
-
if sess.standing_instructions:
|
|
465
|
-
prefix = "✓ " if sess.standing_orders_complete else ""
|
|
466
|
-
instr = sess.standing_instructions[:80]
|
|
467
|
-
print(f"Orders: 📋 {prefix}{instr}")
|
|
488
|
+
label_width = max(len(label) for label, _ in render_cli_stats(ctx)) + 1
|
|
489
|
+
for label, value in render_cli_stats(ctx):
|
|
490
|
+
print(f"{label + ':':<{label_width + 1}} {value}")
|
|
468
491
|
|
|
469
|
-
# AI summaries
|
|
492
|
+
# AI summaries (not a column — comes from daemon state)
|
|
470
493
|
if ai_short:
|
|
471
|
-
print(f"AI:
|
|
494
|
+
print(f"{'AI:':<{label_width + 1}} {ai_short}")
|
|
472
495
|
if ai_context:
|
|
473
|
-
print(f"Context:
|
|
496
|
+
print(f"{'Context:':<{label_width + 1}} {ai_context}")
|
|
474
497
|
|
|
475
|
-
# Activity from status detector
|
|
498
|
+
# Activity from status detector (not a column — transient)
|
|
476
499
|
if activity:
|
|
477
|
-
print(f"Activity:
|
|
500
|
+
print(f"{'Activity:':<{label_width + 1}} {activity[:100]}")
|
|
478
501
|
|
|
479
502
|
print()
|
|
480
503
|
|
|
@@ -498,42 +521,25 @@ def show(
|
|
|
498
521
|
rprint(f"[dim]No pane output available[/dim]")
|
|
499
522
|
|
|
500
523
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
Called by a UserPromptSubmit hook on every prompt. Outputs a single
|
|
506
|
-
line with clock, presence, office hours, uptime, and heartbeat info.
|
|
507
|
-
Silently exits when not in an overcode-managed session (env vars missing).
|
|
508
|
-
"""
|
|
509
|
-
from .time_context import get_agent_identity, generate_time_context
|
|
510
|
-
|
|
511
|
-
name, tmux = get_agent_identity()
|
|
512
|
-
if not name or not tmux:
|
|
513
|
-
raise typer.Exit(0)
|
|
514
|
-
|
|
515
|
-
line = generate_time_context(tmux, name)
|
|
516
|
-
if not line:
|
|
517
|
-
raise typer.Exit(0)
|
|
518
|
-
print(line)
|
|
524
|
+
# =============================================================================
|
|
525
|
+
# Hooks Commands
|
|
526
|
+
# =============================================================================
|
|
519
527
|
|
|
520
528
|
|
|
521
|
-
@
|
|
522
|
-
def
|
|
529
|
+
@hooks_app.command("install")
|
|
530
|
+
def hooks_install(
|
|
523
531
|
project: Annotated[
|
|
524
532
|
bool,
|
|
525
533
|
typer.Option("--project", "-p", help="Install to project-level .claude/settings.json instead of user-level"),
|
|
526
534
|
] = False,
|
|
527
535
|
):
|
|
528
|
-
"""Install
|
|
536
|
+
"""Install all overcode hooks into Claude Code settings.
|
|
529
537
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
The hook runs 'overcode time-context' on every prompt, giving Claude
|
|
534
|
-
continuous awareness of clock, presence, office hours, and uptime.
|
|
538
|
+
Installs hooks for: UserPromptSubmit, PostToolUse, Stop,
|
|
539
|
+
PermissionRequest, SessionEnd. All use the unified 'overcode hook-handler'.
|
|
535
540
|
"""
|
|
536
541
|
from .claude_config import ClaudeConfigEditor
|
|
542
|
+
from .hook_handler import OVERCODE_HOOKS
|
|
537
543
|
|
|
538
544
|
if project:
|
|
539
545
|
editor = ClaudeConfigEditor.project_level()
|
|
@@ -543,18 +549,107 @@ def install_hook(
|
|
|
543
549
|
level = "user"
|
|
544
550
|
|
|
545
551
|
try:
|
|
546
|
-
|
|
552
|
+
settings = editor.load()
|
|
547
553
|
except ValueError as e:
|
|
548
554
|
rprint(f"[red]Error:[/red] {e}")
|
|
549
555
|
raise typer.Exit(1)
|
|
550
556
|
|
|
551
|
-
|
|
552
|
-
|
|
557
|
+
# Install all overcode hooks (idempotent)
|
|
558
|
+
installed = 0
|
|
559
|
+
already = 0
|
|
560
|
+
for event, command in OVERCODE_HOOKS:
|
|
561
|
+
if editor.add_hook(event, command):
|
|
562
|
+
installed += 1
|
|
563
|
+
else:
|
|
564
|
+
already += 1
|
|
565
|
+
|
|
566
|
+
if installed > 0:
|
|
567
|
+
events = ", ".join(event for event, _ in OVERCODE_HOOKS)
|
|
568
|
+
rprint(f"[green]\u2713[/green] Installed {installed} hook(s) in {level} settings")
|
|
553
569
|
rprint(f" [dim]{editor.path}[/dim]")
|
|
554
|
-
rprint(f"\n
|
|
555
|
-
rprint(f"
|
|
570
|
+
rprint(f"\n Events: {events}")
|
|
571
|
+
rprint(f" All hooks run 'overcode hook-handler' (reads event from stdin).")
|
|
572
|
+
elif already == len(OVERCODE_HOOKS):
|
|
573
|
+
rprint(f"[green]\u2713[/green] All {already} hooks already installed in {level} settings")
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
@hooks_app.command("uninstall")
|
|
577
|
+
def hooks_uninstall(
|
|
578
|
+
project: Annotated[
|
|
579
|
+
bool,
|
|
580
|
+
typer.Option("--project", "-p", help="Uninstall from project-level .claude/settings.json instead of user-level"),
|
|
581
|
+
] = False,
|
|
582
|
+
):
|
|
583
|
+
"""Remove all overcode hooks from Claude Code settings."""
|
|
584
|
+
from .claude_config import ClaudeConfigEditor
|
|
585
|
+
from .hook_handler import OVERCODE_HOOKS
|
|
586
|
+
|
|
587
|
+
if project:
|
|
588
|
+
editor = ClaudeConfigEditor.project_level()
|
|
589
|
+
level = "project"
|
|
556
590
|
else:
|
|
557
|
-
|
|
591
|
+
editor = ClaudeConfigEditor.user_level()
|
|
592
|
+
level = "user"
|
|
593
|
+
|
|
594
|
+
try:
|
|
595
|
+
editor.load()
|
|
596
|
+
except ValueError as e:
|
|
597
|
+
rprint(f"[red]Error:[/red] {e}")
|
|
598
|
+
raise typer.Exit(1)
|
|
599
|
+
|
|
600
|
+
removed = 0
|
|
601
|
+
for event, command in OVERCODE_HOOKS:
|
|
602
|
+
if editor.remove_hook(event, command):
|
|
603
|
+
removed += 1
|
|
604
|
+
|
|
605
|
+
if removed > 0:
|
|
606
|
+
rprint(f"[green]\u2713[/green] Removed {removed} hook(s) from {level} settings")
|
|
607
|
+
else:
|
|
608
|
+
rprint(f"[dim]No overcode hooks found in {level} settings[/dim]")
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@hooks_app.command("status")
|
|
612
|
+
def hooks_status():
|
|
613
|
+
"""Show which overcode hooks are installed."""
|
|
614
|
+
from .claude_config import ClaudeConfigEditor
|
|
615
|
+
from .hook_handler import OVERCODE_HOOKS
|
|
616
|
+
|
|
617
|
+
for level_name, editor in [
|
|
618
|
+
("User-level", ClaudeConfigEditor.user_level()),
|
|
619
|
+
("Project-level", ClaudeConfigEditor.project_level()),
|
|
620
|
+
]:
|
|
621
|
+
try:
|
|
622
|
+
editor.load()
|
|
623
|
+
except ValueError:
|
|
624
|
+
rprint(f"\n{level_name} ({editor.path}):")
|
|
625
|
+
rprint(f" [red](invalid JSON)[/red]")
|
|
626
|
+
continue
|
|
627
|
+
|
|
628
|
+
if not editor.path.exists():
|
|
629
|
+
rprint(f"\n{level_name} ({editor.path}):")
|
|
630
|
+
rprint(f" [dim](no settings file)[/dim]")
|
|
631
|
+
continue
|
|
632
|
+
|
|
633
|
+
rprint(f"\n{level_name} ({editor.path}):")
|
|
634
|
+
|
|
635
|
+
for event, command in OVERCODE_HOOKS:
|
|
636
|
+
if editor.has_hook(event, command):
|
|
637
|
+
rprint(f" {event:<20} {command} [green]\u2713[/green]")
|
|
638
|
+
else:
|
|
639
|
+
rprint(f" {event:<20} [dim]not installed[/dim]")
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
@app.command("hook-handler", hidden=True)
|
|
643
|
+
def hook_handler_cmd():
|
|
644
|
+
"""Handle Claude Code hook events (internal).
|
|
645
|
+
|
|
646
|
+
Called by Claude Code hooks, not by users directly.
|
|
647
|
+
Reads event JSON from stdin, writes state for status detection,
|
|
648
|
+
and outputs time-context for UserPromptSubmit events.
|
|
649
|
+
"""
|
|
650
|
+
from .hook_handler import handle_hook_event
|
|
651
|
+
|
|
652
|
+
handle_hook_event()
|
|
558
653
|
|
|
559
654
|
|
|
560
655
|
@app.command()
|
|
@@ -634,6 +729,12 @@ def instruct(
|
|
|
634
729
|
rprint(f"[dim]Tip: Use 'overcode presets' to see available presets[/dim]")
|
|
635
730
|
|
|
636
731
|
|
|
732
|
+
def _signal_heartbeat_change(session: str) -> None:
|
|
733
|
+
"""Wake the monitor daemon so heartbeat status updates immediately (#212)."""
|
|
734
|
+
from .settings import signal_activity
|
|
735
|
+
signal_activity(session)
|
|
736
|
+
|
|
737
|
+
|
|
637
738
|
@app.command()
|
|
638
739
|
def heartbeat(
|
|
639
740
|
name: Annotated[str, typer.Argument(help="Name of agent")],
|
|
@@ -726,6 +827,7 @@ def heartbeat(
|
|
|
726
827
|
heartbeat_paused=False,
|
|
727
828
|
heartbeat_instruction="",
|
|
728
829
|
)
|
|
830
|
+
_signal_heartbeat_change(session)
|
|
729
831
|
rprint(f"[green]✓ Heartbeat disabled for {name}[/green]")
|
|
730
832
|
return
|
|
731
833
|
|
|
@@ -735,6 +837,7 @@ def heartbeat(
|
|
|
735
837
|
rprint(f"[yellow]Heartbeat is not enabled for {name}[/yellow]")
|
|
736
838
|
return
|
|
737
839
|
manager.update_session(agent.id, heartbeat_paused=True)
|
|
840
|
+
_signal_heartbeat_change(session)
|
|
738
841
|
rprint(f"[green]✓ Heartbeat paused for {name}[/green]")
|
|
739
842
|
return
|
|
740
843
|
|
|
@@ -744,6 +847,7 @@ def heartbeat(
|
|
|
744
847
|
rprint(f"[yellow]Heartbeat is not enabled for {name}[/yellow]")
|
|
745
848
|
return
|
|
746
849
|
manager.update_session(agent.id, heartbeat_paused=False)
|
|
850
|
+
_signal_heartbeat_change(session)
|
|
747
851
|
rprint(f"[green]✓ Heartbeat resumed for {name}[/green]")
|
|
748
852
|
return
|
|
749
853
|
|
|
@@ -761,6 +865,7 @@ def heartbeat(
|
|
|
761
865
|
heartbeat_frequency_seconds=final_freq,
|
|
762
866
|
heartbeat_instruction=instruction,
|
|
763
867
|
)
|
|
868
|
+
_signal_heartbeat_change(session)
|
|
764
869
|
rprint(f"[green]✓ Heartbeat enabled for {name}[/green]")
|
|
765
870
|
rprint(f" Frequency: {format_duration(final_freq)}")
|
|
766
871
|
rprint(f" Instruction: {instruction}")
|
|
@@ -775,6 +880,7 @@ def heartbeat(
|
|
|
775
880
|
|
|
776
881
|
if updates:
|
|
777
882
|
manager.update_session(agent.id, **updates)
|
|
883
|
+
_signal_heartbeat_change(session)
|
|
778
884
|
rprint(f"[green]✓ Heartbeat config updated for {name}[/green]")
|
|
779
885
|
if freq_seconds:
|
|
780
886
|
rprint(f" Frequency: {format_duration(freq_seconds)}")
|