claude-team-mcp 0.7.0__py3-none-any.whl → 0.8.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.
- claude_team/events.py +30 -6
- claude_team_mcp/cli_backends/__init__.py +4 -2
- claude_team_mcp/cli_backends/claude.py +45 -5
- claude_team_mcp/cli_backends/codex.py +44 -3
- claude_team_mcp/config.py +350 -0
- claude_team_mcp/config_cli.py +263 -0
- claude_team_mcp/issue_tracker/__init__.py +68 -3
- claude_team_mcp/server.py +69 -0
- claude_team_mcp/terminal_backends/__init__.py +21 -3
- claude_team_mcp/terminal_backends/tmux.py +61 -24
- claude_team_mcp/tools/discover_workers.py +1 -1
- claude_team_mcp/tools/spawn_workers.py +36 -14
- claude_team_mcp/worktree.py +14 -0
- {claude_team_mcp-0.7.0.dist-info → claude_team_mcp-0.8.0.dist-info}/METADATA +1 -1
- {claude_team_mcp-0.7.0.dist-info → claude_team_mcp-0.8.0.dist-info}/RECORD +17 -15
- {claude_team_mcp-0.7.0.dist-info → claude_team_mcp-0.8.0.dist-info}/WHEEL +0 -0
- {claude_team_mcp-0.7.0.dist-info → claude_team_mcp-0.8.0.dist-info}/entry_points.txt +0 -0
claude_team/events.py
CHANGED
|
@@ -4,11 +4,14 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from dataclasses import asdict, dataclass
|
|
6
6
|
from datetime import datetime, timedelta, timezone
|
|
7
|
+
import logging
|
|
7
8
|
import json
|
|
8
9
|
import os
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Literal
|
|
11
12
|
|
|
13
|
+
from claude_team_mcp.config import ConfigError, EventsConfig, load_config
|
|
14
|
+
|
|
12
15
|
try:
|
|
13
16
|
import fcntl
|
|
14
17
|
except ImportError: # pragma: no cover - platform-specific
|
|
@@ -19,6 +22,8 @@ try:
|
|
|
19
22
|
except ImportError: # pragma: no cover - platform-specific
|
|
20
23
|
msvcrt = None
|
|
21
24
|
|
|
25
|
+
logger = logging.getLogger("claude-team-mcp")
|
|
26
|
+
|
|
22
27
|
|
|
23
28
|
EventType = Literal[
|
|
24
29
|
"snapshot",
|
|
@@ -40,8 +45,20 @@ def _int_env(name: str, default: int) -> int:
|
|
|
40
45
|
return default
|
|
41
46
|
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
def _load_rotation_config() -> EventsConfig:
|
|
49
|
+
# Resolve rotation defaults from config, applying env overrides.
|
|
50
|
+
try:
|
|
51
|
+
config = load_config()
|
|
52
|
+
events_config = config.events
|
|
53
|
+
except ConfigError as exc:
|
|
54
|
+
logger.warning(
|
|
55
|
+
"Invalid config file; using default event rotation config: %s", exc
|
|
56
|
+
)
|
|
57
|
+
events_config = EventsConfig()
|
|
58
|
+
return EventsConfig(
|
|
59
|
+
max_size_mb=_int_env("CLAUDE_TEAM_EVENTS_MAX_SIZE_MB", events_config.max_size_mb),
|
|
60
|
+
recent_hours=_int_env("CLAUDE_TEAM_EVENTS_RECENT_HOURS", events_config.recent_hours),
|
|
61
|
+
)
|
|
45
62
|
|
|
46
63
|
|
|
47
64
|
@dataclass
|
|
@@ -89,6 +106,7 @@ def append_events(events: list[WorkerEvent]) -> None:
|
|
|
89
106
|
payloads = [json.dumps(_event_to_dict(event), ensure_ascii=False) for event in events]
|
|
90
107
|
block = "\n".join(payloads) + "\n"
|
|
91
108
|
event_ts = _latest_event_timestamp(events)
|
|
109
|
+
rotation_config = _load_rotation_config()
|
|
92
110
|
|
|
93
111
|
with path.open("r+", encoding="utf-8") as handle:
|
|
94
112
|
_lock_file(handle)
|
|
@@ -97,8 +115,8 @@ def append_events(events: list[WorkerEvent]) -> None:
|
|
|
97
115
|
handle,
|
|
98
116
|
path,
|
|
99
117
|
current_ts=event_ts,
|
|
100
|
-
max_size_mb=
|
|
101
|
-
recent_hours=
|
|
118
|
+
max_size_mb=rotation_config.max_size_mb,
|
|
119
|
+
recent_hours=rotation_config.recent_hours,
|
|
102
120
|
)
|
|
103
121
|
# Hold the lock across the entire write and flush cycle.
|
|
104
122
|
handle.seek(0, os.SEEK_END)
|
|
@@ -169,8 +187,8 @@ def get_latest_snapshot() -> dict | None:
|
|
|
169
187
|
|
|
170
188
|
|
|
171
189
|
def rotate_events_log(
|
|
172
|
-
max_size_mb: int =
|
|
173
|
-
recent_hours: int =
|
|
190
|
+
max_size_mb: int | None = None,
|
|
191
|
+
recent_hours: int | None = None,
|
|
174
192
|
now: datetime | None = None,
|
|
175
193
|
) -> None:
|
|
176
194
|
"""Rotate the log daily or by size, retaining active/recent workers."""
|
|
@@ -179,6 +197,12 @@ def rotate_events_log(
|
|
|
179
197
|
return
|
|
180
198
|
|
|
181
199
|
current_ts = now or datetime.now(timezone.utc)
|
|
200
|
+
if max_size_mb is None or recent_hours is None:
|
|
201
|
+
rotation_config = _load_rotation_config()
|
|
202
|
+
if max_size_mb is None:
|
|
203
|
+
max_size_mb = rotation_config.max_size_mb
|
|
204
|
+
if recent_hours is None:
|
|
205
|
+
recent_hours = rotation_config.recent_hours
|
|
182
206
|
|
|
183
207
|
with path.open("r+", encoding="utf-8") as handle:
|
|
184
208
|
_lock_file(handle)
|
|
@@ -6,15 +6,17 @@ This allows claude-team to orchestrate multiple agent types through a unified in
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from .base import AgentCLI
|
|
9
|
-
from .claude import ClaudeCLI, claude_cli
|
|
10
|
-
from .codex import CodexCLI, codex_cli
|
|
9
|
+
from .claude import ClaudeCLI, claude_cli, get_claude_command
|
|
10
|
+
from .codex import CodexCLI, codex_cli, get_codex_command
|
|
11
11
|
|
|
12
12
|
__all__ = [
|
|
13
13
|
"AgentCLI",
|
|
14
14
|
"ClaudeCLI",
|
|
15
15
|
"claude_cli",
|
|
16
|
+
"get_claude_command",
|
|
16
17
|
"CodexCLI",
|
|
17
18
|
"codex_cli",
|
|
19
|
+
"get_codex_command",
|
|
18
20
|
"get_cli_backend",
|
|
19
21
|
]
|
|
20
22
|
|
|
@@ -10,6 +10,45 @@ from typing import Literal
|
|
|
10
10
|
|
|
11
11
|
from .base import AgentCLI
|
|
12
12
|
|
|
13
|
+
# Built-in default command.
|
|
14
|
+
_DEFAULT_COMMAND = "claude"
|
|
15
|
+
|
|
16
|
+
# Environment variable for command override (takes highest precedence).
|
|
17
|
+
_ENV_VAR = "CLAUDE_TEAM_COMMAND"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_claude_command() -> str:
|
|
21
|
+
"""
|
|
22
|
+
Get the Claude CLI command with precedence: env var > config > default.
|
|
23
|
+
|
|
24
|
+
Resolution order:
|
|
25
|
+
1. CLAUDE_TEAM_COMMAND environment variable (for override)
|
|
26
|
+
2. Config file commands.claude setting
|
|
27
|
+
3. Built-in default "claude"
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The command to use for Claude CLI
|
|
31
|
+
"""
|
|
32
|
+
# Environment variable takes highest precedence (for override).
|
|
33
|
+
env_val = os.environ.get(_ENV_VAR)
|
|
34
|
+
if env_val:
|
|
35
|
+
return env_val
|
|
36
|
+
|
|
37
|
+
# Try config file next.
|
|
38
|
+
# Import here to avoid circular imports and lazy-load config.
|
|
39
|
+
try:
|
|
40
|
+
from ..config import ConfigError, load_config
|
|
41
|
+
|
|
42
|
+
config = load_config()
|
|
43
|
+
except ConfigError:
|
|
44
|
+
return _DEFAULT_COMMAND
|
|
45
|
+
|
|
46
|
+
if config.commands.claude:
|
|
47
|
+
return config.commands.claude
|
|
48
|
+
|
|
49
|
+
# Fall back to built-in default.
|
|
50
|
+
return _DEFAULT_COMMAND
|
|
51
|
+
|
|
13
52
|
|
|
14
53
|
class ClaudeCLI(AgentCLI):
|
|
15
54
|
"""
|
|
@@ -31,10 +70,12 @@ class ClaudeCLI(AgentCLI):
|
|
|
31
70
|
"""
|
|
32
71
|
Return the Claude CLI command.
|
|
33
72
|
|
|
34
|
-
|
|
35
|
-
|
|
73
|
+
Resolution order:
|
|
74
|
+
1. CLAUDE_TEAM_COMMAND environment variable (for override)
|
|
75
|
+
2. Config file commands.claude setting
|
|
76
|
+
3. Built-in default "claude"
|
|
36
77
|
"""
|
|
37
|
-
return
|
|
78
|
+
return get_claude_command()
|
|
38
79
|
|
|
39
80
|
def build_args(
|
|
40
81
|
self,
|
|
@@ -102,8 +143,7 @@ class ClaudeCLI(AgentCLI):
|
|
|
102
143
|
|
|
103
144
|
def _is_default_command(self) -> bool:
|
|
104
145
|
"""Check if using the default 'claude' command (not a custom wrapper)."""
|
|
105
|
-
|
|
106
|
-
return cmd == "claude"
|
|
146
|
+
return get_claude_command() == _DEFAULT_COMMAND
|
|
107
147
|
|
|
108
148
|
|
|
109
149
|
# Singleton instance for convenience
|
|
@@ -12,6 +12,45 @@ from typing import Literal
|
|
|
12
12
|
|
|
13
13
|
from .base import AgentCLI
|
|
14
14
|
|
|
15
|
+
# Built-in default command.
|
|
16
|
+
_DEFAULT_COMMAND = "codex"
|
|
17
|
+
|
|
18
|
+
# Environment variable for command override (takes highest precedence).
|
|
19
|
+
_ENV_VAR = "CLAUDE_TEAM_CODEX_COMMAND"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_codex_command() -> str:
|
|
23
|
+
"""
|
|
24
|
+
Get the Codex CLI command with precedence: env var > config > default.
|
|
25
|
+
|
|
26
|
+
Resolution order:
|
|
27
|
+
1. CLAUDE_TEAM_CODEX_COMMAND environment variable (for override)
|
|
28
|
+
2. Config file commands.codex setting
|
|
29
|
+
3. Built-in default "codex"
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The command to use for Codex CLI
|
|
33
|
+
"""
|
|
34
|
+
# Environment variable takes highest precedence (for override).
|
|
35
|
+
env_val = os.environ.get(_ENV_VAR)
|
|
36
|
+
if env_val:
|
|
37
|
+
return env_val
|
|
38
|
+
|
|
39
|
+
# Try config file next.
|
|
40
|
+
# Import here to avoid circular imports and lazy-load config.
|
|
41
|
+
try:
|
|
42
|
+
from ..config import ConfigError, load_config
|
|
43
|
+
|
|
44
|
+
config = load_config()
|
|
45
|
+
except ConfigError:
|
|
46
|
+
return _DEFAULT_COMMAND
|
|
47
|
+
|
|
48
|
+
if config.commands.codex:
|
|
49
|
+
return config.commands.codex
|
|
50
|
+
|
|
51
|
+
# Fall back to built-in default.
|
|
52
|
+
return _DEFAULT_COMMAND
|
|
53
|
+
|
|
15
54
|
|
|
16
55
|
class CodexCLI(AgentCLI):
|
|
17
56
|
"""
|
|
@@ -35,10 +74,12 @@ class CodexCLI(AgentCLI):
|
|
|
35
74
|
"""
|
|
36
75
|
Return the Codex CLI command.
|
|
37
76
|
|
|
38
|
-
|
|
39
|
-
|
|
77
|
+
Resolution order:
|
|
78
|
+
1. CLAUDE_TEAM_CODEX_COMMAND environment variable (for override)
|
|
79
|
+
2. Config file commands.codex setting
|
|
80
|
+
3. Built-in default "codex"
|
|
40
81
|
"""
|
|
41
|
-
return
|
|
82
|
+
return get_codex_command()
|
|
42
83
|
|
|
43
84
|
def build_args(
|
|
44
85
|
self,
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration loading for Claude Team MCP.
|
|
3
|
+
|
|
4
|
+
Defines dataclasses for the config schema and utilities for loading
|
|
5
|
+
and validating JSON config files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from dataclasses import asdict, dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Literal
|
|
14
|
+
|
|
15
|
+
CONFIG_VERSION = 1
|
|
16
|
+
CONFIG_DIR = Path.home() / ".claude-team"
|
|
17
|
+
CONFIG_PATH = CONFIG_DIR / "config.json"
|
|
18
|
+
|
|
19
|
+
AgentType = Literal["claude", "codex"]
|
|
20
|
+
LayoutMode = Literal["auto", "new"]
|
|
21
|
+
TerminalBackend = Literal["iterm", "tmux"]
|
|
22
|
+
IssueTrackerName = Literal["beads", "pebbles"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConfigError(ValueError):
|
|
26
|
+
"""Raised when the configuration file is invalid."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class CommandsConfig:
|
|
31
|
+
"""CLI command overrides for supported agent backends."""
|
|
32
|
+
|
|
33
|
+
claude: str | None = None
|
|
34
|
+
codex: str | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class DefaultsConfig:
|
|
39
|
+
"""Default values applied when spawn_workers fields are omitted."""
|
|
40
|
+
|
|
41
|
+
agent_type: AgentType = "claude"
|
|
42
|
+
skip_permissions: bool = False
|
|
43
|
+
use_worktree: bool = True
|
|
44
|
+
layout: LayoutMode = "auto"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class TerminalConfig:
|
|
49
|
+
"""Terminal backend configuration."""
|
|
50
|
+
|
|
51
|
+
backend: TerminalBackend | None = None # None = auto-detect
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class EventsConfig:
|
|
56
|
+
"""Event log rotation configuration."""
|
|
57
|
+
|
|
58
|
+
max_size_mb: int = 1
|
|
59
|
+
recent_hours: int = 24
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class IssueTrackerConfig:
|
|
64
|
+
"""Issue tracker configuration overrides."""
|
|
65
|
+
|
|
66
|
+
override: IssueTrackerName | None = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class ClaudeTeamConfig:
|
|
71
|
+
"""Top-level configuration container for claude-team."""
|
|
72
|
+
|
|
73
|
+
version: int = CONFIG_VERSION
|
|
74
|
+
commands: CommandsConfig = field(default_factory=CommandsConfig)
|
|
75
|
+
defaults: DefaultsConfig = field(default_factory=DefaultsConfig)
|
|
76
|
+
terminal: TerminalConfig = field(default_factory=TerminalConfig)
|
|
77
|
+
events: EventsConfig = field(default_factory=EventsConfig)
|
|
78
|
+
issue_tracker: IssueTrackerConfig = field(default_factory=IssueTrackerConfig)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def default_config() -> ClaudeTeamConfig:
|
|
82
|
+
"""Return a new config instance with default values."""
|
|
83
|
+
|
|
84
|
+
return ClaudeTeamConfig()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def load_config(config_path: Path | None = None) -> ClaudeTeamConfig:
|
|
88
|
+
"""Load config from disk, creating defaults if missing."""
|
|
89
|
+
|
|
90
|
+
path = _resolve_config_path(config_path)
|
|
91
|
+
if not path.exists():
|
|
92
|
+
return default_config()
|
|
93
|
+
|
|
94
|
+
data = _read_json(path)
|
|
95
|
+
return _parse_config(data)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def parse_config(data: dict) -> ClaudeTeamConfig:
|
|
99
|
+
"""Parse and validate a config dictionary."""
|
|
100
|
+
|
|
101
|
+
return _parse_config(data)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def save_config(config: ClaudeTeamConfig, config_path: Path | None = None) -> Path:
|
|
105
|
+
"""Persist config to disk and return the path written."""
|
|
106
|
+
|
|
107
|
+
path = _resolve_config_path(config_path)
|
|
108
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
payload = json.dumps(asdict(config), indent=2, sort_keys=True)
|
|
110
|
+
path.write_text(payload + "\n")
|
|
111
|
+
return path
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _resolve_config_path(config_path: Path | None) -> Path:
|
|
115
|
+
# Resolve the config path, using the default location if needed.
|
|
116
|
+
return (config_path or CONFIG_PATH).expanduser()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _read_json(path: Path) -> dict:
|
|
120
|
+
# Read the file contents first so we can surface IO errors cleanly.
|
|
121
|
+
try:
|
|
122
|
+
raw = path.read_text()
|
|
123
|
+
except OSError as exc:
|
|
124
|
+
raise ConfigError(f"Unable to read config file: {path}") from exc
|
|
125
|
+
|
|
126
|
+
# Decode JSON and enforce an object payload.
|
|
127
|
+
try:
|
|
128
|
+
data = json.loads(raw)
|
|
129
|
+
except json.JSONDecodeError as exc:
|
|
130
|
+
raise ConfigError(f"Invalid JSON in config file: {path}") from exc
|
|
131
|
+
|
|
132
|
+
if not isinstance(data, dict):
|
|
133
|
+
raise ConfigError("Config file must contain a JSON object")
|
|
134
|
+
|
|
135
|
+
return data
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _parse_config(data: dict) -> ClaudeTeamConfig:
|
|
139
|
+
# Validate expected top-level keys before parsing sections.
|
|
140
|
+
_validate_keys(
|
|
141
|
+
data,
|
|
142
|
+
{"version", "commands", "defaults", "terminal", "events", "issue_tracker"},
|
|
143
|
+
"config",
|
|
144
|
+
)
|
|
145
|
+
version = _read_version(data.get("version"))
|
|
146
|
+
commands = _parse_commands(data.get("commands"))
|
|
147
|
+
defaults = _parse_defaults(data.get("defaults"))
|
|
148
|
+
terminal = _parse_terminal(data.get("terminal"))
|
|
149
|
+
events = _parse_events(data.get("events"))
|
|
150
|
+
issue_tracker = _parse_issue_tracker(data.get("issue_tracker"))
|
|
151
|
+
return ClaudeTeamConfig(
|
|
152
|
+
version=version,
|
|
153
|
+
commands=commands,
|
|
154
|
+
defaults=defaults,
|
|
155
|
+
terminal=terminal,
|
|
156
|
+
events=events,
|
|
157
|
+
issue_tracker=issue_tracker,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _read_version(value: object) -> int:
|
|
162
|
+
# Allow missing versions for backward compatibility with early configs.
|
|
163
|
+
if value is None:
|
|
164
|
+
return CONFIG_VERSION
|
|
165
|
+
if not isinstance(value, int):
|
|
166
|
+
raise ConfigError("config.version must be an integer")
|
|
167
|
+
if value != CONFIG_VERSION:
|
|
168
|
+
raise ConfigError(
|
|
169
|
+
f"Unsupported config version {value}; expected {CONFIG_VERSION}"
|
|
170
|
+
)
|
|
171
|
+
return value
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _parse_commands(value: object) -> CommandsConfig:
|
|
175
|
+
# Parse CLI command overrides for each backend.
|
|
176
|
+
data = _ensure_dict(value, "commands")
|
|
177
|
+
_validate_keys(data, {"claude", "codex"}, "commands")
|
|
178
|
+
return CommandsConfig(
|
|
179
|
+
claude=_optional_str(data.get("claude"), "commands.claude"),
|
|
180
|
+
codex=_optional_str(data.get("codex"), "commands.codex"),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _parse_defaults(value: object) -> DefaultsConfig:
|
|
185
|
+
# Parse default spawn_workers fields with explicit validation.
|
|
186
|
+
data = _ensure_dict(value, "defaults")
|
|
187
|
+
_validate_keys(
|
|
188
|
+
data,
|
|
189
|
+
{"agent_type", "skip_permissions", "use_worktree", "layout"},
|
|
190
|
+
"defaults",
|
|
191
|
+
)
|
|
192
|
+
return DefaultsConfig(
|
|
193
|
+
agent_type=_optional_literal(
|
|
194
|
+
data.get("agent_type"),
|
|
195
|
+
{"claude", "codex"},
|
|
196
|
+
"defaults.agent_type",
|
|
197
|
+
DefaultsConfig.agent_type,
|
|
198
|
+
),
|
|
199
|
+
skip_permissions=_optional_bool(
|
|
200
|
+
data.get("skip_permissions"),
|
|
201
|
+
"defaults.skip_permissions",
|
|
202
|
+
DefaultsConfig.skip_permissions,
|
|
203
|
+
),
|
|
204
|
+
use_worktree=_optional_bool(
|
|
205
|
+
data.get("use_worktree"),
|
|
206
|
+
"defaults.use_worktree",
|
|
207
|
+
DefaultsConfig.use_worktree,
|
|
208
|
+
),
|
|
209
|
+
layout=_optional_literal(
|
|
210
|
+
data.get("layout"),
|
|
211
|
+
{"auto", "new"},
|
|
212
|
+
"defaults.layout",
|
|
213
|
+
DefaultsConfig.layout,
|
|
214
|
+
),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _parse_terminal(value: object) -> TerminalConfig:
|
|
219
|
+
# Parse terminal backend configuration.
|
|
220
|
+
data = _ensure_dict(value, "terminal")
|
|
221
|
+
_validate_keys(data, {"backend"}, "terminal")
|
|
222
|
+
return TerminalConfig(
|
|
223
|
+
backend=_optional_literal(
|
|
224
|
+
data.get("backend"),
|
|
225
|
+
{"iterm", "tmux"},
|
|
226
|
+
"terminal.backend",
|
|
227
|
+
None,
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _parse_events(value: object) -> EventsConfig:
|
|
233
|
+
# Parse event log rotation configuration.
|
|
234
|
+
data = _ensure_dict(value, "events")
|
|
235
|
+
_validate_keys(data, {"max_size_mb", "recent_hours"}, "events")
|
|
236
|
+
return EventsConfig(
|
|
237
|
+
max_size_mb=_optional_int(
|
|
238
|
+
data.get("max_size_mb"),
|
|
239
|
+
"events.max_size_mb",
|
|
240
|
+
EventsConfig.max_size_mb,
|
|
241
|
+
min_value=1,
|
|
242
|
+
),
|
|
243
|
+
recent_hours=_optional_int(
|
|
244
|
+
data.get("recent_hours"),
|
|
245
|
+
"events.recent_hours",
|
|
246
|
+
EventsConfig.recent_hours,
|
|
247
|
+
min_value=0,
|
|
248
|
+
),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _parse_issue_tracker(value: object) -> IssueTrackerConfig:
|
|
253
|
+
# Parse issue tracker overrides.
|
|
254
|
+
data = _ensure_dict(value, "issue_tracker")
|
|
255
|
+
_validate_keys(data, {"override"}, "issue_tracker")
|
|
256
|
+
return IssueTrackerConfig(
|
|
257
|
+
override=_optional_literal(
|
|
258
|
+
data.get("override"),
|
|
259
|
+
{"beads", "pebbles"},
|
|
260
|
+
"issue_tracker.override",
|
|
261
|
+
None,
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _ensure_dict(value: object, path: str) -> dict:
|
|
267
|
+
# Ensure sections are JSON objects, defaulting to empty dicts.
|
|
268
|
+
if value is None:
|
|
269
|
+
return {}
|
|
270
|
+
if not isinstance(value, dict):
|
|
271
|
+
raise ConfigError(f"{path} must be a JSON object")
|
|
272
|
+
return value
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _validate_keys(data: dict, allowed: set[str], path: str) -> None:
|
|
276
|
+
# Reject unexpected keys for a config section.
|
|
277
|
+
unknown = set(data.keys()) - allowed
|
|
278
|
+
if unknown:
|
|
279
|
+
joined = ", ".join(sorted(unknown))
|
|
280
|
+
raise ConfigError(f"Unknown keys in {path}: {joined}")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _optional_str(value: object, path: str) -> str | None:
|
|
284
|
+
# Validate optional string fields.
|
|
285
|
+
if value is None:
|
|
286
|
+
return None
|
|
287
|
+
if not isinstance(value, str):
|
|
288
|
+
raise ConfigError(f"{path} must be a string")
|
|
289
|
+
if not value.strip():
|
|
290
|
+
raise ConfigError(f"{path} cannot be empty")
|
|
291
|
+
return value
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _optional_int(value: object, path: str, default: int, min_value: int = 1) -> int:
|
|
295
|
+
# Validate optional integer fields.
|
|
296
|
+
if value is None:
|
|
297
|
+
return default
|
|
298
|
+
if not isinstance(value, int) or isinstance(value, bool):
|
|
299
|
+
raise ConfigError(f"{path} must be an integer")
|
|
300
|
+
if value < min_value:
|
|
301
|
+
raise ConfigError(f"{path} must be at least {min_value}")
|
|
302
|
+
return value
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _optional_bool(value: object, path: str, default: bool) -> bool:
|
|
306
|
+
# Validate optional boolean fields.
|
|
307
|
+
if value is None:
|
|
308
|
+
return default
|
|
309
|
+
if not isinstance(value, bool):
|
|
310
|
+
raise ConfigError(f"{path} must be a boolean")
|
|
311
|
+
return value
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _optional_literal(
|
|
315
|
+
value: object,
|
|
316
|
+
allowed: set[str],
|
|
317
|
+
path: str,
|
|
318
|
+
default: str | None,
|
|
319
|
+
) -> str | None:
|
|
320
|
+
# Validate optional string fields constrained to allowed values.
|
|
321
|
+
if value is None:
|
|
322
|
+
return default
|
|
323
|
+
if not isinstance(value, str):
|
|
324
|
+
raise ConfigError(f"{path} must be a string")
|
|
325
|
+
if value not in allowed:
|
|
326
|
+
joined = ", ".join(sorted(allowed))
|
|
327
|
+
raise ConfigError(f"{path} must be one of: {joined}")
|
|
328
|
+
return value
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
__all__ = [
|
|
332
|
+
"AgentType",
|
|
333
|
+
"ClaudeTeamConfig",
|
|
334
|
+
"CommandsConfig",
|
|
335
|
+
"ConfigError",
|
|
336
|
+
"DefaultsConfig",
|
|
337
|
+
"EventsConfig",
|
|
338
|
+
"IssueTrackerConfig",
|
|
339
|
+
"LayoutMode",
|
|
340
|
+
"TerminalBackend",
|
|
341
|
+
"TerminalConfig",
|
|
342
|
+
"IssueTrackerName",
|
|
343
|
+
"CONFIG_DIR",
|
|
344
|
+
"CONFIG_PATH",
|
|
345
|
+
"CONFIG_VERSION",
|
|
346
|
+
"default_config",
|
|
347
|
+
"load_config",
|
|
348
|
+
"parse_config",
|
|
349
|
+
"save_config",
|
|
350
|
+
]
|