overcode 0.1.2__py3-none-any.whl → 0.1.3__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 CHANGED
@@ -2,4 +2,4 @@
2
2
  Overcode - A supervisor for managing multiple Claude Code instances.
3
3
  """
4
4
 
5
- __version__ = "0.1.2"
5
+ __version__ = "0.1.3"
overcode/cli.py CHANGED
@@ -39,6 +39,15 @@ supervisor_daemon_app = typer.Typer(
39
39
  )
40
40
  app.add_typer(supervisor_daemon_app, name="supervisor-daemon")
41
41
 
42
+ # Config subcommand group
43
+ config_app = typer.Typer(
44
+ name="config",
45
+ help="Manage configuration",
46
+ no_args_is_help=False,
47
+ invoke_without_command=True,
48
+ )
49
+ app.add_typer(config_app, name="config")
50
+
42
51
  # Console for rich output
43
52
  console = Console()
44
53
 
@@ -194,6 +203,33 @@ def cleanup(session: SessionOption = "agents"):
194
203
  rprint("[dim]No terminated sessions to clean up[/dim]")
195
204
 
196
205
 
206
+ @app.command(name="set-value")
207
+ def set_value(
208
+ name: Annotated[str, typer.Argument(help="Name of agent")],
209
+ value: Annotated[int, typer.Argument(help="Priority value (default 1000, higher = more important)")],
210
+ session: SessionOption = "agents",
211
+ ):
212
+ """Set agent priority value for sorting (#61).
213
+
214
+ Higher values indicate higher priority. Default is 1000.
215
+
216
+ Examples:
217
+ overcode set-value my-agent 2000 # High priority
218
+ overcode set-value my-agent 500 # Low priority
219
+ overcode set-value my-agent 1000 # Reset to default
220
+ """
221
+ from .session_manager import SessionManager
222
+
223
+ manager = SessionManager()
224
+ agent = manager.get_session_by_name(name)
225
+ if not agent:
226
+ rprint(f"[red]Error: Agent '{name}' not found[/red]")
227
+ raise typer.Exit(code=1)
228
+
229
+ manager.set_agent_value(agent.id, value)
230
+ rprint(f"[green]✓ Set {name} value to {value}[/green]")
231
+
232
+
197
233
  @app.command()
198
234
  def send(
199
235
  name: Annotated[str, typer.Argument(help="Name of agent")],
@@ -771,70 +807,132 @@ def supervisor_daemon_watch(session: SessionOption = "agents"):
771
807
 
772
808
 
773
809
  # =============================================================================
774
- # Summarizer Commands
810
+ # Config Commands
775
811
  # =============================================================================
776
812
 
813
+ CONFIG_TEMPLATE = """\
814
+ # Overcode configuration
815
+ # Location: ~/.overcode/config.yaml
816
+
817
+ # Default instructions sent to new agents
818
+ # default_standing_instructions: "Be concise. Ask before making large changes."
819
+
820
+ # AI summarizer settings (for corporate API gateways)
821
+ # summarizer:
822
+ # api_url: https://api.openai.com/v1/chat/completions
823
+ # model: gpt-4o-mini
824
+ # api_key_var: OPENAI_API_KEY # env var containing the API key
825
+
826
+ # Cloud relay for remote monitoring
827
+ # relay:
828
+ # enabled: false
829
+ # url: https://your-worker.workers.dev/update
830
+ # api_key: your-secret-key
831
+ # interval: 30 # seconds between pushes
832
+
833
+ # Web dashboard time presets
834
+ # web:
835
+ # time_presets:
836
+ # - name: "Morning"
837
+ # start: "09:00"
838
+ # end: "12:00"
839
+ # - name: "Full Day"
840
+ # start: "09:00"
841
+ # end: "17:00"
842
+ """
777
843
 
778
- @app.command()
779
- def summarizer(
780
- action: Annotated[
781
- str, typer.Argument(help="Action: on, off, or status")
782
- ] = "status",
783
- session: SessionOption = "agents",
784
- ):
785
- """Control the agent activity summarizer.
786
844
 
787
- The summarizer uses GPT-4o-mini to generate human-readable summaries
788
- of what each agent has been doing. Requires OPENAI_API_KEY env var.
845
+ @config_app.callback(invoke_without_command=True)
846
+ def config_default(ctx: typer.Context):
847
+ """Show current configuration (default when no subcommand given)."""
848
+ if ctx.invoked_subcommand is None:
849
+ _config_show()
789
850
 
790
- Examples:
791
- overcode summarizer status # Check current state
792
- overcode summarizer on # Enable summarizer
793
- overcode summarizer off # Disable summarizer
851
+
852
+ @config_app.command("init")
853
+ def config_init(
854
+ force: Annotated[
855
+ bool, typer.Option("--force", "-f", help="Overwrite existing config file")
856
+ ] = False,
857
+ ):
858
+ """Create a config file with documented defaults.
859
+
860
+ Creates ~/.overcode/config.yaml with all options commented out.
861
+ Use --force to overwrite an existing config file.
794
862
  """
795
- from .summarizer_component import (
796
- set_summarizer_enabled,
797
- is_summarizer_enabled,
798
- SummarizerClient,
799
- )
800
- from .monitor_daemon_state import get_monitor_daemon_state
863
+ from .config import CONFIG_PATH
801
864
 
802
- action = action.lower()
865
+ # Ensure directory exists
866
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
803
867
 
804
- if action == "status":
805
- # Check if API key is available
806
- api_available = SummarizerClient.is_available()
807
- enabled = is_summarizer_enabled(session)
868
+ if CONFIG_PATH.exists() and not force:
869
+ rprint(f"[yellow]Config file already exists:[/yellow] {CONFIG_PATH}")
870
+ rprint("[dim]Use --force to overwrite[/dim]")
871
+ raise typer.Exit(1)
808
872
 
809
- # Get stats from daemon state
810
- state = get_monitor_daemon_state(session)
873
+ CONFIG_PATH.write_text(CONFIG_TEMPLATE)
874
+ rprint(f"[green]✓[/green] Created config file: [bold]{CONFIG_PATH}[/bold]")
875
+ rprint("[dim]Edit to customize your settings[/dim]")
811
876
 
812
- rprint(f"[bold]Summarizer Status ({session}):[/bold]")
813
- rprint(f" API key: {'[green]available[/green]' if api_available else '[red]not set[/red] (export OPENAI_API_KEY=...)'}")
814
- rprint(f" Enabled: {'[green]yes[/green]' if enabled else '[dim]no[/dim]'}")
815
877
 
816
- if state:
817
- rprint(f" API calls: {state.summarizer_calls}")
818
- rprint(f" Est. cost: ${state.summarizer_cost_usd:.4f}")
878
+ @config_app.command("show")
879
+ def config_show():
880
+ """Show current configuration."""
881
+ _config_show()
819
882
 
820
- elif action == "on":
821
- if not SummarizerClient.is_available():
822
- rprint("[red]Error:[/red] OPENAI_API_KEY environment variable not set")
823
- rprint("[dim]Export your API key: export OPENAI_API_KEY='sk-...'[/dim]")
824
- raise typer.Exit(1)
825
883
 
826
- set_summarizer_enabled(session, True)
827
- rprint(f"[green]✓[/green] Summarizer enabled for session '{session}'")
828
- rprint("[dim]Summaries will appear in the web dashboard and TUI[/dim]")
884
+ def _config_show():
885
+ """Internal function to display current config."""
886
+ from .config import CONFIG_PATH, load_config
829
887
 
830
- elif action == "off":
831
- set_summarizer_enabled(session, False)
832
- rprint(f"[green]✓[/green] Summarizer disabled for session '{session}'")
888
+ if not CONFIG_PATH.exists():
889
+ rprint(f"[dim]No config file found at {CONFIG_PATH}[/dim]")
890
+ rprint("[dim]Run 'overcode config init' to create one[/dim]")
891
+ return
833
892
 
834
- else:
835
- rprint(f"[red]Unknown action:[/red] {action}")
836
- rprint("[dim]Use: on, off, or status[/dim]")
837
- raise typer.Exit(1)
893
+ config = load_config()
894
+ if not config:
895
+ rprint(f"[dim]Config file is empty: {CONFIG_PATH}[/dim]")
896
+ return
897
+
898
+ rprint(f"[bold]Configuration[/bold] ({CONFIG_PATH}):\n")
899
+
900
+ # Show each configured section
901
+ if "default_standing_instructions" in config:
902
+ instr = config["default_standing_instructions"]
903
+ display = instr[:60] + "..." if len(instr) > 60 else instr
904
+ rprint(f" default_standing_instructions: \"{display}\"")
905
+
906
+ if "summarizer" in config:
907
+ s = config["summarizer"]
908
+ rprint(" summarizer:")
909
+ if "api_url" in s:
910
+ rprint(f" api_url: {s['api_url']}")
911
+ if "model" in s:
912
+ rprint(f" model: {s['model']}")
913
+ if "api_key_var" in s:
914
+ rprint(f" api_key_var: {s['api_key_var']}")
915
+
916
+ if "relay" in config:
917
+ r = config["relay"]
918
+ rprint(" relay:")
919
+ rprint(f" enabled: {r.get('enabled', False)}")
920
+ if "url" in r:
921
+ rprint(f" url: {r['url']}")
922
+ if "interval" in r:
923
+ rprint(f" interval: {r['interval']}s")
924
+
925
+ if "web" in config:
926
+ w = config["web"]
927
+ if "time_presets" in w:
928
+ rprint(f" web.time_presets: {len(w['time_presets'])} presets")
929
+
930
+
931
+ @config_app.command("path")
932
+ def config_path():
933
+ """Show the config file path."""
934
+ from .config import CONFIG_PATH
935
+ print(CONFIG_PATH)
838
936
 
839
937
 
840
938
  # =============================================================================
overcode/config.py CHANGED
@@ -72,6 +72,72 @@ def get_relay_config() -> Optional[dict]:
72
72
  }
73
73
 
74
74
 
75
+ def get_summarizer_config() -> dict:
76
+ """Get summarizer configuration for AI summaries.
77
+
78
+ Config file takes precedence, environment variables are fallback.
79
+
80
+ Config format in ~/.overcode/config.yaml:
81
+ summarizer:
82
+ api_url: https://api.openai.com/v1/chat/completions
83
+ model: gpt-4o-mini
84
+ api_key_var: OPENAI_API_KEY # env var name containing the key
85
+
86
+ Environment variable fallbacks:
87
+ OVERCODE_SUMMARIZER_API_URL
88
+ OVERCODE_SUMMARIZER_MODEL
89
+ OVERCODE_SUMMARIZER_API_KEY_VAR
90
+
91
+ Returns:
92
+ Dict with api_url, model, and api_key (resolved from env var)
93
+ """
94
+ import os
95
+
96
+ # Defaults
97
+ default_api_url = "https://api.openai.com/v1/chat/completions"
98
+ default_model = "gpt-4o-mini"
99
+ default_api_key_var = "OPENAI_API_KEY"
100
+
101
+ config = load_config()
102
+ summarizer = config.get("summarizer", {})
103
+
104
+ # Config file takes precedence, env vars are fallback
105
+ api_url = summarizer.get("api_url") or os.environ.get("OVERCODE_SUMMARIZER_API_URL") or default_api_url
106
+ model = summarizer.get("model") or os.environ.get("OVERCODE_SUMMARIZER_MODEL") or default_model
107
+ api_key_var = summarizer.get("api_key_var") or os.environ.get("OVERCODE_SUMMARIZER_API_KEY_VAR") or default_api_key_var
108
+
109
+ # Resolve the actual API key from the configured env var
110
+ api_key = os.environ.get(api_key_var)
111
+
112
+ return {
113
+ "api_url": api_url,
114
+ "model": model,
115
+ "api_key": api_key,
116
+ "api_key_var": api_key_var,
117
+ }
118
+
119
+
120
+ def get_timeline_config() -> dict:
121
+ """Get timeline display configuration.
122
+
123
+ Config format in ~/.overcode/config.yaml:
124
+ timeline:
125
+ hours: 3.0 # How many hours of history to show
126
+
127
+ Returns:
128
+ Dict with timeline settings (hours)
129
+ """
130
+ default_hours = 3.0
131
+
132
+ config = load_config()
133
+ timeline = config.get("timeline", {})
134
+ hours = timeline.get("hours", default_hours)
135
+
136
+ return {
137
+ "hours": hours,
138
+ }
139
+
140
+
75
141
  def get_web_time_presets() -> list:
76
142
  """Get time presets for the web analytics dashboard.
77
143
 
@@ -1,17 +1,16 @@
1
1
  # Overcode Supervisor Skill
2
2
 
3
- You are the Overcode supervisor agent. Your mission: **Make all RED sessions GREEN**.
3
+ You are the Overcode supervisor agent. Your mission: **Attempt to unblock each RED session once, then exit**.
4
4
 
5
5
  ## Your Role
6
6
 
7
- You monitor and unblock Claude agent sessions running in tmux. When sessions get stuck (RED status), you help them make progress by:
7
+ You unblock Claude agent sessions running in tmux. When sessions are stuck (RED status), you make ONE attempt to help each by:
8
8
  - Reading their output to understand what they're stuck on
9
- - Making decisions based on their autopilot instructions
9
+ - Making decisions based on their standing instructions
10
10
  - Approving safe permission requests
11
11
  - Sending guidance or clarifying information
12
- - Having multi-turn conversations with agents
13
12
 
14
- **When all sessions are GREEN, your job is done - exit successfully.**
13
+ **IMPORTANT: Make ONE attempt per RED session, then exit. Do not loop or wait to see if your action worked. The supervisor daemon will call you again later if sessions are still RED.**
15
14
 
16
15
  ## How to Control Sessions (Recommended)
17
16
 
@@ -115,35 +114,32 @@ You must follow these rules when deciding to approve operations:
115
114
  # 1. Read current session states
116
115
  cat ~/.overcode/sessions/sessions.json | jq '.[] | {name, tmux_window, standing_instructions, stats}'
117
116
 
118
- # 2. Find RED sessions
119
- # Check TUI or parse status
120
-
121
- # 3. For each RED session, read output
122
- tmux capture-pane -t agents:1 -p -S -100
123
-
124
- # 4. Make decision based on:
125
- # - What they're stuck on
126
- # - Their autopilot instruction
127
- # - Approval rules
117
+ # 2. Find RED sessions (use overcode list)
118
+ overcode list
128
119
 
129
- # 5a. If permission request is safe, approve:
130
- tmux send-keys -t agents:1 "" C-m
120
+ # 3. For EACH RED session, make ONE attempt:
131
121
 
132
- # 5b. If they need guidance, send message:
133
- tmux send-keys -t agents:1 "Focus on the core feature first, implement error handling later." C-m
122
+ # a. Read output to understand what they're stuck on
123
+ overcode show agent-name --lines 100
134
124
 
135
- # 5c. If permission unsafe, reject:
136
- tmux send-keys -t agents:1 Escape
125
+ # b. Make decision based on:
126
+ # - What they're stuck on
127
+ # - Their standing instructions
128
+ # - Approval rules below
137
129
 
138
- # 6. Log your action
139
- echo "$(date): Approved Write permission for recipe-book session (within working dir)" >> ~/.overcode/supervisor.log
130
+ # c. Take action:
131
+ overcode send agent-name enter # Approve permission
132
+ overcode send agent-name escape # Reject permission
133
+ overcode send agent-name "guidance" # Send instructions
140
134
 
141
- # 7. Repeat for other RED sessions
135
+ # d. Move to next RED session immediately (don't wait)
142
136
 
143
- # 8. When all GREEN, exit
137
+ # 4. After attempting ALL RED sessions once, EXIT
144
138
  exit 0
145
139
  ```
146
140
 
141
+ **Key point:** Do NOT loop back to check if sessions turned green. Make one attempt per session and exit. The supervisor daemon will invoke you again if needed.
142
+
147
143
  ## Real Example
148
144
 
149
145
  **Session:** recipe-book
@@ -168,13 +164,20 @@ echo "$(date): recipe-book - Approved write to desserts.md (within working dir,
168
164
 
169
165
  ## Your Process
170
166
 
171
- 1. **Survey** - Read all session states from sessions.json
172
- 2. **Identify** - Find RED sessions (waiting for user)
173
- 3. **Investigate** - Read their tmux output to see what they're stuck on
174
- 4. **Decide** - Apply approval rules and autopilot context
175
- 5. **Act** - Send tmux commands to unblock them
176
- 6. **Log** - Record your decisions
177
- 7. **Repeat** - Check if more sessions need help
178
- 8. **Exit** - When all GREEN, your job is complete
167
+ 1. **Survey** - Run `overcode list` to see all sessions and their status
168
+ 2. **Identify** - Note which sessions are RED (waiting for user)
169
+ 3. **For each RED session:**
170
+ - **Investigate** - Run `overcode show <name>` to see what they're stuck on
171
+ - **Decide** - Apply approval rules and check their standing instructions
172
+ - **Act** - Send ONE command to unblock them
173
+ - **Move on** - Immediately proceed to next RED session
174
+ 4. **Exit** - After attempting each RED session once, run `exit 0`
175
+
176
+ **Do NOT:**
177
+ - Loop back to check if sessions turned green
178
+ - Wait to see if your action worked
179
+ - Make multiple attempts on the same session
180
+
181
+ The supervisor daemon runs continuously and will invoke you again if sessions are still RED.
179
182
 
180
183
  Remember: You're a decision-making agent that helps other agents make progress. Be helpful but safe. When in doubt, err on the side of caution.
@@ -202,6 +202,44 @@ def get_session_ids_for_session(
202
202
  return sorted(session_ids)
203
203
 
204
204
 
205
+ def get_current_session_id_for_directory(
206
+ directory: str,
207
+ since: datetime,
208
+ history_path: Path = CLAUDE_HISTORY_PATH
209
+ ) -> Optional[str]:
210
+ """Get the most recent Claude sessionId for a directory since a given time.
211
+
212
+ This is used to discover new Claude sessionIds that should be tracked
213
+ by an overcode agent running in that directory (#119).
214
+
215
+ Args:
216
+ directory: The project directory path
217
+ since: Only consider entries after this time
218
+ history_path: Path to history file
219
+
220
+ Returns:
221
+ The most recent sessionId, or None if no matching entries
222
+ """
223
+ entries = read_history(history_path)
224
+ session_dir = str(Path(directory).resolve())
225
+ since_ms = int(since.timestamp() * 1000)
226
+
227
+ latest_session_id = None
228
+ latest_timestamp = 0
229
+
230
+ for entry in entries:
231
+ if entry.timestamp_ms < since_ms:
232
+ continue
233
+ if entry.project:
234
+ entry_dir = str(Path(entry.project).resolve())
235
+ if entry_dir == session_dir and entry.session_id:
236
+ if entry.timestamp_ms > latest_timestamp:
237
+ latest_timestamp = entry.timestamp_ms
238
+ latest_session_id = entry.session_id
239
+
240
+ return latest_session_id
241
+
242
+
205
243
  def encode_project_path(path: str) -> str:
206
244
  """Encode a project path to Claude Code's directory naming format.
207
245
 
@@ -397,6 +435,10 @@ def get_session_stats(
397
435
 
398
436
  Combines interaction counting with token usage from session files.
399
437
 
438
+ For context window calculation, only owned sessionIds are used to avoid
439
+ cross-contamination when multiple agents run in the same directory (#119).
440
+ Total token counting still uses all matched sessionIds.
441
+
400
442
  Args:
401
443
  session: The overcode Session
402
444
  history_path: Path to history.jsonl
@@ -418,21 +460,25 @@ def get_session_stats(
418
460
  interactions = get_interactions_for_session(session, history_path)
419
461
  interaction_count = len(interactions)
420
462
 
421
- # Get unique session IDs
422
- session_ids = set()
463
+ # Get unique session IDs from interactions (for total token counting)
464
+ all_session_ids = set()
423
465
  for entry in interactions:
424
466
  if entry.session_id:
425
- session_ids.add(entry.session_id)
467
+ all_session_ids.add(entry.session_id)
468
+
469
+ # Get owned sessionIds for context calculation (#119)
470
+ # Only use explicitly tracked sessionIds to avoid showing wrong agent's context
471
+ owned_session_ids = getattr(session, 'claude_session_ids', None) or []
426
472
 
427
473
  # Sum token usage and work times across all session files
428
474
  total_input = 0
429
475
  total_output = 0
430
476
  total_cache_creation = 0
431
477
  total_cache_read = 0
432
- current_context = 0 # Track most recent context size
478
+ current_context = 0 # Track most recent context size (only from owned sessions)
433
479
  all_work_times: List[float] = []
434
480
 
435
- for sid in session_ids:
481
+ for sid in all_session_ids:
436
482
  session_file = get_session_file_path(
437
483
  session.start_directory, sid, projects_path
438
484
  )
@@ -441,14 +487,29 @@ def get_session_stats(
441
487
  total_output += usage["output_tokens"]
442
488
  total_cache_creation += usage["cache_creation_tokens"]
443
489
  total_cache_read += usage["cache_read_tokens"]
444
- # Keep the largest current context (most recent across all session files)
445
- if usage["current_context_tokens"] > current_context:
446
- current_context = usage["current_context_tokens"]
490
+
491
+ # Only track context from OWNED sessionIds to avoid cross-contamination (#119)
492
+ if sid in owned_session_ids:
493
+ if usage["current_context_tokens"] > current_context:
494
+ current_context = usage["current_context_tokens"]
447
495
 
448
496
  # Collect work times from this session file
449
497
  work_times = read_work_times_from_session_file(session_file, since=session_start)
450
498
  all_work_times.extend(work_times)
451
499
 
500
+ # Check for subagent files in {sessionId}/subagents/
501
+ encoded = encode_project_path(session.start_directory)
502
+ subagents_dir = projects_path / encoded / sid / "subagents"
503
+ if subagents_dir.exists():
504
+ for subagent_file in subagents_dir.glob("agent-*.jsonl"):
505
+ sub_usage = read_token_usage_from_session_file(
506
+ subagent_file, since=session_start
507
+ )
508
+ total_input += sub_usage["input_tokens"]
509
+ total_output += sub_usage["output_tokens"]
510
+ total_cache_creation += sub_usage["cache_creation_tokens"]
511
+ total_cache_read += sub_usage["cache_read_tokens"]
512
+
452
513
  return ClaudeSessionStats(
453
514
  interaction_count=interaction_count,
454
515
  input_tokens=total_input,