overcode 0.1.8__tar.gz → 0.2.0__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.
Files changed (95) hide show
  1. {overcode-0.1.8/src/overcode.egg-info → overcode-0.2.0}/PKG-INFO +1 -1
  2. {overcode-0.1.8 → overcode-0.2.0}/pyproject.toml +1 -1
  3. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/claude_config.py +47 -0
  4. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/cli.py +253 -11
  5. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/config.py +17 -0
  6. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/history_reader.py +259 -179
  7. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/launcher.py +44 -2
  8. overcode-0.2.0/src/overcode/notifier.py +145 -0
  9. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/session_manager.py +7 -0
  10. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/settings.py +13 -0
  11. overcode-0.2.0/src/overcode/sister_controller.py +268 -0
  12. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/sister_poller.py +85 -5
  13. overcode-0.2.0/src/overcode/status_history.py +308 -0
  14. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/summary_columns.py +7 -6
  15. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/summary_groups.py +9 -0
  16. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/time_context.py +3 -1
  17. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui.py +342 -68
  18. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_actions/input.py +21 -5
  19. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_actions/session.py +132 -46
  20. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_actions/view.py +19 -3
  21. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_logic.py +82 -0
  22. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_render.py +2 -1
  23. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_widgets/session_summary.py +3 -11
  24. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/web_api.py +78 -2
  25. overcode-0.2.0/src/overcode/web_control_api.py +525 -0
  26. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/web_server.py +226 -3
  27. {overcode-0.1.8 → overcode-0.2.0/src/overcode.egg-info}/PKG-INFO +1 -1
  28. {overcode-0.1.8 → overcode-0.2.0}/src/overcode.egg-info/SOURCES.txt +3 -0
  29. overcode-0.1.8/src/overcode/status_history.py +0 -164
  30. {overcode-0.1.8 → overcode-0.2.0}/LICENSE +0 -0
  31. {overcode-0.1.8 → overcode-0.2.0}/MANIFEST.in +0 -0
  32. {overcode-0.1.8 → overcode-0.2.0}/README.md +0 -0
  33. {overcode-0.1.8 → overcode-0.2.0}/setup.cfg +0 -0
  34. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/__init__.py +0 -0
  35. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/bundled_skills.py +0 -0
  36. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/daemon_claude_skill.md +0 -0
  37. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/daemon_logging.py +0 -0
  38. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/daemon_utils.py +0 -0
  39. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/data_export.py +0 -0
  40. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/dependency_check.py +0 -0
  41. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/exceptions.py +0 -0
  42. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/follow_mode.py +0 -0
  43. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/hook_handler.py +0 -0
  44. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/hook_status_detector.py +0 -0
  45. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/implementations.py +0 -0
  46. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/interfaces.py +0 -0
  47. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/logging_config.py +0 -0
  48. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/mocks.py +0 -0
  49. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/monitor_daemon.py +0 -0
  50. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/monitor_daemon_core.py +0 -0
  51. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/monitor_daemon_state.py +0 -0
  52. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/pid_utils.py +0 -0
  53. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/presence_logger.py +0 -0
  54. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/protocols.py +0 -0
  55. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/standing_instructions.py +0 -0
  56. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/status_constants.py +0 -0
  57. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/status_detector.py +0 -0
  58. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/status_detector_factory.py +0 -0
  59. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/status_patterns.py +0 -0
  60. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/summarizer_client.py +0 -0
  61. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/summarizer_component.py +0 -0
  62. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/supervisor_daemon.py +0 -0
  63. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/supervisor_daemon_core.py +0 -0
  64. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/supervisor_layout.sh +0 -0
  65. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/testing/__init__.py +0 -0
  66. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/testing/renderer.py +0 -0
  67. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/testing/tmux_driver.py +0 -0
  68. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/testing/tui_eye.py +0 -0
  69. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/testing/tui_eye_skill.md +0 -0
  70. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tmux_manager.py +0 -0
  71. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tmux_utils.py +0 -0
  72. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui.tcss +0 -0
  73. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_actions/__init__.py +0 -0
  74. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_actions/daemon.py +0 -0
  75. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_actions/navigation.py +0 -0
  76. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_formatters.py +0 -0
  77. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_helpers.py +0 -0
  78. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_widgets/__init__.py +0 -0
  79. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_widgets/command_bar.py +0 -0
  80. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_widgets/daemon_panel.py +0 -0
  81. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_widgets/daemon_status_bar.py +0 -0
  82. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
  83. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_widgets/help_overlay.py +0 -0
  84. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_widgets/preview_pane.py +0 -0
  85. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_widgets/status_timeline.py +0 -0
  86. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/tui_widgets/summary_config_modal.py +0 -0
  87. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/usage_monitor.py +0 -0
  88. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/web_chartjs.py +0 -0
  89. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/web_server_runner.py +0 -0
  90. {overcode-0.1.8 → overcode-0.2.0}/src/overcode/web_templates.py +0 -0
  91. {overcode-0.1.8 → overcode-0.2.0}/src/overcode.egg-info/dependency_links.txt +0 -0
  92. {overcode-0.1.8 → overcode-0.2.0}/src/overcode.egg-info/entry_points.txt +0 -0
  93. {overcode-0.1.8 → overcode-0.2.0}/src/overcode.egg-info/requires.txt +0 -0
  94. {overcode-0.1.8 → overcode-0.2.0}/src/overcode.egg-info/top_level.txt +0 -0
  95. {overcode-0.1.8 → overcode-0.2.0}/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.8
3
+ Version: 0.2.0
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "overcode"
7
- version = "0.1.8"
7
+ version = "0.2.0"
8
8
  description = "A supervisor for managing multiple Claude Code instances in tmux"
9
9
  authors = [
10
10
  {name = "Mike Bond"}
@@ -137,3 +137,50 @@ class ClaudeConfigEditor:
137
137
  if cmd.startswith(command_prefix):
138
138
  results.append((event, cmd))
139
139
  return results
140
+
141
+ # ----- Permission management -----
142
+
143
+ def add_permission(self, tool_pattern: str) -> bool:
144
+ """Add to permissions.allow. Returns True if newly added."""
145
+ settings = self.load()
146
+ allow_list = settings.get("permissions", {}).get("allow", [])
147
+ if tool_pattern in allow_list:
148
+ return False
149
+
150
+ updated = copy.deepcopy(settings)
151
+ if "permissions" not in updated:
152
+ updated["permissions"] = {}
153
+ if "allow" not in updated["permissions"]:
154
+ updated["permissions"]["allow"] = []
155
+ updated["permissions"]["allow"].append(tool_pattern)
156
+ self.save(updated)
157
+ return True
158
+
159
+ def remove_permission(self, tool_pattern: str) -> bool:
160
+ """Remove from permissions.allow. Returns True if found."""
161
+ settings = self.load()
162
+ allow_list = settings.get("permissions", {}).get("allow", [])
163
+ if tool_pattern not in allow_list:
164
+ return False
165
+
166
+ updated = copy.deepcopy(settings)
167
+ updated["permissions"]["allow"].remove(tool_pattern)
168
+
169
+ # Clean up empty allow list
170
+ if not updated["permissions"]["allow"]:
171
+ del updated["permissions"]["allow"]
172
+
173
+ # Clean up empty permissions dict
174
+ if not updated["permissions"]:
175
+ del updated["permissions"]
176
+
177
+ self.save(updated)
178
+ return True
179
+
180
+ def list_permissions_matching(self, prefix: str) -> list[str]:
181
+ """Return entries from permissions.allow that start with prefix."""
182
+ settings = self.load()
183
+ return [
184
+ p for p in settings.get("permissions", {}).get("allow", [])
185
+ if p.startswith(prefix)
186
+ ]
@@ -56,6 +56,14 @@ skills_app = typer.Typer(
56
56
  )
57
57
  app.add_typer(skills_app, name="skills")
58
58
 
59
+ # Perms subcommand group
60
+ perms_app = typer.Typer(
61
+ name="perms",
62
+ help="Manage Claude Code tool permissions for overcode commands.",
63
+ no_args_is_help=True,
64
+ )
65
+ app.add_typer(perms_app, name="perms")
66
+
59
67
  # Budget subcommand group (#244)
60
68
  budget_app = typer.Typer(
61
69
  name="budget",
@@ -244,6 +252,9 @@ def list_agents(
244
252
  cost: Annotated[
245
253
  bool, typer.Option("--cost", help="Show $ cost instead of token counts")
246
254
  ] = False,
255
+ sisters: Annotated[
256
+ bool, typer.Option("--sisters", help="Include sister (remote) agents")
257
+ ] = False,
247
258
  session: SessionOption = "agents",
248
259
  ):
249
260
  """List running agents with status.
@@ -260,12 +271,21 @@ def list_agents(
260
271
  )
261
272
  from .monitor_daemon_state import get_monitor_daemon_state
262
273
  from .summary_columns import build_cli_context, SUMMARY_COLUMNS
274
+ from .tui_logic import compute_tree_metadata, sort_sessions_by_tree
263
275
  from rich.text import Text
264
276
  from rich.console import Console
265
277
 
266
278
  launcher = ClaudeLauncher(session)
267
279
  sessions = launcher.list_sessions()
268
280
 
281
+ # Merge sister sessions if --sisters flag
282
+ if sisters:
283
+ from .sister_poller import SisterPoller
284
+ poller = SisterPoller()
285
+ if poller.has_sisters:
286
+ remote_sessions = poller.poll_all()
287
+ sessions = sessions + remote_sessions
288
+
269
289
  if not sessions:
270
290
  rprint("[dim]No running agents[/dim]")
271
291
  return
@@ -288,6 +308,10 @@ def list_agents(
288
308
  rprint("[dim]No running agents[/dim]")
289
309
  return
290
310
 
311
+ # Sort in tree order and compute tree metadata for depth/prefix
312
+ sessions = sort_sessions_by_tree(sessions)
313
+ tree_meta = compute_tree_metadata(sessions)
314
+
291
315
  # Columns to render in list mode (subset of TUI columns)
292
316
  list_columns = {
293
317
  "status_symbol", "time_in_state", "agent_name",
@@ -315,7 +339,11 @@ def list_agents(
315
339
  terminated_count = 0
316
340
 
317
341
  for sess in sessions:
318
- if sess.status == "terminated":
342
+ if getattr(sess, 'is_remote', False):
343
+ # Remote sessions carry status in their stats
344
+ status = sess.stats.current_state or "running"
345
+ activity = sess.stats.current_task or ""
346
+ elif sess.status == "terminated":
319
347
  status = "terminated"
320
348
  activity = "(tmux window no longer exists)"
321
349
  terminated_count += 1
@@ -348,14 +376,18 @@ def list_agents(
348
376
 
349
377
  # Get git diff stats
350
378
  git_diff = None
351
- try:
352
- if sess.start_directory:
353
- git_diff = get_git_diff_stats(sess.start_directory)
354
- except Exception:
355
- pass
356
-
357
- # Build column context
358
- child_count = len(launcher.sessions.get_children(sess.id))
379
+ if getattr(sess, 'is_remote', False):
380
+ git_diff = getattr(sess, 'remote_git_diff', None)
381
+ else:
382
+ try:
383
+ if sess.start_directory:
384
+ git_diff = get_git_diff_stats(sess.start_directory)
385
+ except Exception:
386
+ pass
387
+
388
+ # Build column context using tree metadata for child count
389
+ meta = tree_meta.get(sess.id)
390
+ child_count = meta.child_count if meta else 0
359
391
  ctx = build_cli_context(
360
392
  session=sess, stats=sess.stats,
361
393
  claude_stats=claude_stats, git_diff_stats=git_diff,
@@ -370,8 +402,8 @@ def list_agents(
370
402
  ctx.summary_detail = "med"
371
403
  ctx.show_cost = cost
372
404
 
373
- # Handle tree indentation (#244)
374
- depth = launcher.sessions.compute_depth(sess)
405
+ # Handle tree indentation (#244) using compute_tree_metadata
406
+ depth = meta.depth if meta else 0
375
407
  indent = " " * depth
376
408
  available = name_width - len(indent)
377
409
  ctx.display_name = (indent + sess.name[:available]).ljust(name_width)
@@ -1241,6 +1273,148 @@ def skills_status():
1241
1273
  rprint(f" {name:<20} [yellow]modified[/yellow]")
1242
1274
 
1243
1275
 
1276
+ # =============================================================================
1277
+ # Perms Commands
1278
+ # =============================================================================
1279
+
1280
+ OVERCODE_SAFE_PERMS = [
1281
+ "Bash(overcode report *)",
1282
+ "Bash(overcode show *)",
1283
+ "Bash(overcode list *)",
1284
+ "Bash(overcode follow *)",
1285
+ "Bash(overcode kill *)",
1286
+ "Bash(overcode budget *)",
1287
+ ]
1288
+
1289
+ OVERCODE_PUNCHY_PERMS = [
1290
+ "Bash(overcode launch *)",
1291
+ "Bash(overcode send *)",
1292
+ "Bash(overcode instruct *)",
1293
+ ]
1294
+
1295
+
1296
+ @perms_app.command("install")
1297
+ def perms_install(
1298
+ project: Annotated[
1299
+ bool,
1300
+ typer.Option("--project", "-p", help="Install to project-level .claude/settings.json instead of user-level"),
1301
+ ] = False,
1302
+ all_perms: Annotated[
1303
+ bool,
1304
+ typer.Option("--all", help="Include punchy permissions (launch, send, instruct)"),
1305
+ ] = False,
1306
+ ):
1307
+ """Install overcode tool permissions into Claude Code settings.
1308
+
1309
+ By default installs safe (read-only) permissions. Use --all to also
1310
+ include punchy permissions that can spawn or control agents.
1311
+
1312
+ Safe: report, show, list, follow, kill, budget
1313
+ Punchy (--all): launch, send, instruct
1314
+ """
1315
+ from .claude_config import ClaudeConfigEditor
1316
+
1317
+ if project:
1318
+ editor = ClaudeConfigEditor.project_level()
1319
+ level = "project"
1320
+ else:
1321
+ editor = ClaudeConfigEditor.user_level()
1322
+ level = "user"
1323
+
1324
+ try:
1325
+ editor.load()
1326
+ except ValueError as e:
1327
+ rprint(f"[red]Error:[/red] {e}")
1328
+ raise typer.Exit(1)
1329
+
1330
+ perms = OVERCODE_SAFE_PERMS + (OVERCODE_PUNCHY_PERMS if all_perms else [])
1331
+
1332
+ installed = 0
1333
+ already = 0
1334
+ for perm in perms:
1335
+ if editor.add_permission(perm):
1336
+ installed += 1
1337
+ else:
1338
+ already += 1
1339
+
1340
+ if installed > 0:
1341
+ tier = "safe + punchy" if all_perms else "safe"
1342
+ rprint(f"[green]\u2713[/green] Installed {installed} permission(s) in {level} settings ({tier})")
1343
+ rprint(f" [dim]{editor.path}[/dim]")
1344
+ for perm in perms:
1345
+ rprint(f" {perm}")
1346
+ elif already == len(perms):
1347
+ rprint(f"[green]\u2713[/green] All {already} permissions already installed in {level} settings")
1348
+
1349
+
1350
+ @perms_app.command("uninstall")
1351
+ def perms_uninstall(
1352
+ project: Annotated[
1353
+ bool,
1354
+ typer.Option("--project", "-p", help="Uninstall from project-level .claude/settings.json instead of user-level"),
1355
+ ] = False,
1356
+ ):
1357
+ """Remove all overcode permissions from Claude Code settings."""
1358
+ from .claude_config import ClaudeConfigEditor
1359
+
1360
+ if project:
1361
+ editor = ClaudeConfigEditor.project_level()
1362
+ level = "project"
1363
+ else:
1364
+ editor = ClaudeConfigEditor.user_level()
1365
+ level = "user"
1366
+
1367
+ try:
1368
+ editor.load()
1369
+ except ValueError as e:
1370
+ rprint(f"[red]Error:[/red] {e}")
1371
+ raise typer.Exit(1)
1372
+
1373
+ all_perms = OVERCODE_SAFE_PERMS + OVERCODE_PUNCHY_PERMS
1374
+ removed = 0
1375
+ for perm in all_perms:
1376
+ if editor.remove_permission(perm):
1377
+ removed += 1
1378
+
1379
+ if removed > 0:
1380
+ rprint(f"[green]\u2713[/green] Removed {removed} permission(s) from {level} settings")
1381
+ else:
1382
+ rprint(f"[dim]No overcode permissions found in {level} settings[/dim]")
1383
+
1384
+
1385
+ @perms_app.command("status")
1386
+ def perms_status():
1387
+ """Show which overcode permissions are installed."""
1388
+ from .claude_config import ClaudeConfigEditor
1389
+
1390
+ all_perms = OVERCODE_SAFE_PERMS + OVERCODE_PUNCHY_PERMS
1391
+
1392
+ for level_name, editor in [
1393
+ ("User-level", ClaudeConfigEditor.user_level()),
1394
+ ("Project-level", ClaudeConfigEditor.project_level()),
1395
+ ]:
1396
+ try:
1397
+ editor.load()
1398
+ except ValueError:
1399
+ rprint(f"\n{level_name} ({editor.path}):")
1400
+ rprint(f" [red](invalid JSON)[/red]")
1401
+ continue
1402
+
1403
+ if not editor.path.exists():
1404
+ rprint(f"\n{level_name} ({editor.path}):")
1405
+ rprint(f" [dim](no settings file)[/dim]")
1406
+ continue
1407
+
1408
+ rprint(f"\n{level_name} ({editor.path}):")
1409
+
1410
+ installed = editor.list_permissions_matching("Bash(overcode ")
1411
+ for perm in all_perms:
1412
+ if perm in installed:
1413
+ rprint(f" {perm} [green]\u2713[/green]")
1414
+ else:
1415
+ rprint(f" {perm:<30} [dim]not installed[/dim]")
1416
+
1417
+
1244
1418
  @app.command("hook-handler", hidden=True)
1245
1419
  def hook_handler_cmd():
1246
1420
  """Handle Claude Code hook events (internal).
@@ -1508,6 +1682,7 @@ def monitor(
1508
1682
  """Launch the standalone TUI monitor."""
1509
1683
  if restart:
1510
1684
  from .monitor_daemon import stop_monitor_daemon, is_monitor_daemon_running, get_monitor_daemon_pid
1685
+ from .web_server import is_web_server_running, stop_web_server, start_web_server, get_web_server_url
1511
1686
 
1512
1687
  if is_monitor_daemon_running(session):
1513
1688
  pid = get_monitor_daemon_pid(session)
@@ -1517,6 +1692,16 @@ def monitor(
1517
1692
  rprint("[red]Failed to stop monitor daemon[/red]")
1518
1693
  raise typer.Exit(1)
1519
1694
 
1695
+ if is_web_server_running(session):
1696
+ ok, msg = stop_web_server(session)
1697
+ if ok:
1698
+ rprint(f"[green]✓[/green] Web server stopped")
1699
+ started, start_msg = start_web_server(session)
1700
+ if started:
1701
+ rprint(f"[green]✓[/green] Web server restarted ({start_msg})")
1702
+ else:
1703
+ rprint(f"[yellow]Warning: web server failed to restart: {start_msg}[/yellow]")
1704
+
1520
1705
  from .tui import run_tui
1521
1706
 
1522
1707
  run_tui(session, diagnostics=diagnostics)
@@ -2042,6 +2227,63 @@ def sister_remove(
2042
2227
  rprint(f"[green]✓ Removed sister '{name}'[/green]")
2043
2228
 
2044
2229
 
2230
+ @sister_app.command("allow-control")
2231
+ def sister_allow_control(
2232
+ on: Annotated[bool, typer.Option("--on", help="Enable remote control")] = False,
2233
+ off: Annotated[bool, typer.Option("--off", help="Disable remote control")] = False,
2234
+ ):
2235
+ """Show or toggle remote control for this machine's web server.
2236
+
2237
+ When enabled, sister instances can send commands (kill, restart, send
2238
+ instructions, etc.) to agents on this machine via POST endpoints.
2239
+
2240
+ Examples:
2241
+ overcode sister allow-control # Show current status
2242
+ overcode sister allow-control --on # Enable remote control
2243
+ overcode sister allow-control --off # Disable remote control
2244
+ """
2245
+ from .config import load_config, save_config, get_web_api_key
2246
+
2247
+ config = load_config()
2248
+ web = config.setdefault("web", {})
2249
+
2250
+ if on and off:
2251
+ rprint("[red]Error: Cannot use --on and --off together[/red]")
2252
+ raise typer.Exit(code=1)
2253
+
2254
+ if on:
2255
+ api_key = get_web_api_key()
2256
+ web["allow_control"] = True
2257
+ save_config(config)
2258
+ rprint(f"[green]✓ Remote control enabled (web.allow_control = true)[/green]")
2259
+ if api_key:
2260
+ masked_key = api_key[:4] + "..."
2261
+ rprint(f" API key: {masked_key}")
2262
+ else:
2263
+ rprint(f" [yellow]Warning: web.api_key is not set — endpoints are unauthenticated[/yellow]")
2264
+ rprint(f" [dim]This is fine if you're using SSH tunnels. Otherwise set it in ~/.overcode/config.yaml:[/dim]")
2265
+ rprint()
2266
+ rprint(" web:")
2267
+ rprint(' api_key: "your-secret-key"')
2268
+ rprint(f" Restart web server for changes to take effect.")
2269
+ elif off:
2270
+ web["allow_control"] = False
2271
+ save_config(config)
2272
+ rprint(f"[green]✓ Remote control disabled (web.allow_control = false)[/green]")
2273
+ else:
2274
+ # Show current status
2275
+ enabled = web.get("allow_control", False)
2276
+ api_key = get_web_api_key()
2277
+ if enabled:
2278
+ masked_key = (api_key[:4] + "...") if api_key else "(not set)"
2279
+ rprint(f"Remote control: [green]enabled[/green]")
2280
+ rprint(f" API key: {masked_key}")
2281
+ else:
2282
+ rprint(f"Remote control: [red]disabled[/red]")
2283
+ if not api_key:
2284
+ rprint(f" [dim]Note: web.api_key is also not set[/dim]")
2285
+
2286
+
2045
2287
  # =============================================================================
2046
2288
  # Config Commands
2047
2289
  # =============================================================================
@@ -247,6 +247,23 @@ def get_web_api_key() -> Optional[str]:
247
247
  return web.get("api_key") or None
248
248
 
249
249
 
250
+ def get_web_allow_control() -> bool:
251
+ """Check if remote control is enabled for the web server.
252
+
253
+ When False (default), all POST/PUT/DELETE endpoints return 403.
254
+
255
+ Config format in ~/.overcode/config.yaml:
256
+ web:
257
+ allow_control: true
258
+
259
+ Returns:
260
+ True if remote control is enabled, False otherwise
261
+ """
262
+ config = load_config()
263
+ web = config.get("web", {})
264
+ return bool(web.get("allow_control", False))
265
+
266
+
250
267
  def get_sisters_config() -> List[dict]:
251
268
  """Get sister instance configuration for cross-machine monitoring.
252
269