claude-team-mcp 0.4.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_mcp/__init__.py +24 -0
- claude_team_mcp/__main__.py +8 -0
- claude_team_mcp/cli_backends/__init__.py +44 -0
- claude_team_mcp/cli_backends/base.py +132 -0
- claude_team_mcp/cli_backends/claude.py +110 -0
- claude_team_mcp/cli_backends/codex.py +110 -0
- claude_team_mcp/colors.py +108 -0
- claude_team_mcp/formatting.py +120 -0
- claude_team_mcp/idle_detection.py +488 -0
- claude_team_mcp/iterm_utils.py +1119 -0
- claude_team_mcp/names.py +427 -0
- claude_team_mcp/profile.py +364 -0
- claude_team_mcp/registry.py +426 -0
- claude_team_mcp/schemas/__init__.py +5 -0
- claude_team_mcp/schemas/codex.py +267 -0
- claude_team_mcp/server.py +390 -0
- claude_team_mcp/session_state.py +1058 -0
- claude_team_mcp/subprocess_cache.py +119 -0
- claude_team_mcp/tools/__init__.py +52 -0
- claude_team_mcp/tools/adopt_worker.py +122 -0
- claude_team_mcp/tools/annotate_worker.py +57 -0
- claude_team_mcp/tools/bd_help.py +42 -0
- claude_team_mcp/tools/check_idle_workers.py +98 -0
- claude_team_mcp/tools/close_workers.py +194 -0
- claude_team_mcp/tools/discover_workers.py +129 -0
- claude_team_mcp/tools/examine_worker.py +56 -0
- claude_team_mcp/tools/list_workers.py +76 -0
- claude_team_mcp/tools/list_worktrees.py +106 -0
- claude_team_mcp/tools/message_workers.py +311 -0
- claude_team_mcp/tools/read_worker_logs.py +158 -0
- claude_team_mcp/tools/spawn_workers.py +634 -0
- claude_team_mcp/tools/wait_idle_workers.py +148 -0
- claude_team_mcp/utils/__init__.py +17 -0
- claude_team_mcp/utils/constants.py +87 -0
- claude_team_mcp/utils/errors.py +87 -0
- claude_team_mcp/utils/worktree_detection.py +79 -0
- claude_team_mcp/worker_prompt.py +350 -0
- claude_team_mcp/worktree.py +532 -0
- claude_team_mcp-0.4.0.dist-info/METADATA +414 -0
- claude_team_mcp-0.4.0.dist-info/RECORD +42 -0
- claude_team_mcp-0.4.0.dist-info/WHEEL +4 -0
- claude_team_mcp-0.4.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Claude Team MCP Server
|
|
3
|
+
|
|
4
|
+
An MCP server that allows one Claude Code session to spawn and manage
|
|
5
|
+
a team of other Claude Code sessions via iTerm2.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
from .colors import generate_tab_color, get_hue_for_index, hsl_to_rgb_tuple
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
"""Entry point for the claude-team command."""
|
|
15
|
+
from .server import main as server_main
|
|
16
|
+
server_main()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"main",
|
|
21
|
+
"generate_tab_color",
|
|
22
|
+
"get_hue_for_index",
|
|
23
|
+
"hsl_to_rgb_tuple",
|
|
24
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI Backends Module.
|
|
3
|
+
|
|
4
|
+
Provides abstraction layer for different agent CLI tools (Claude Code, Codex, etc.)
|
|
5
|
+
This allows claude-team to orchestrate multiple agent types through a unified interface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .base import AgentCLI
|
|
9
|
+
from .claude import ClaudeCLI, claude_cli
|
|
10
|
+
from .codex import CodexCLI, codex_cli
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AgentCLI",
|
|
14
|
+
"ClaudeCLI",
|
|
15
|
+
"claude_cli",
|
|
16
|
+
"CodexCLI",
|
|
17
|
+
"codex_cli",
|
|
18
|
+
"get_cli_backend",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_cli_backend(agent_type: str = "claude") -> AgentCLI:
|
|
23
|
+
"""
|
|
24
|
+
Get a CLI backend instance by agent type.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
agent_type: The agent type ("claude" or "codex")
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
An AgentCLI implementation instance
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ValueError: If the agent type is not supported
|
|
34
|
+
"""
|
|
35
|
+
backends = {
|
|
36
|
+
"claude": claude_cli,
|
|
37
|
+
"codex": codex_cli,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if agent_type not in backends:
|
|
41
|
+
valid = ", ".join(sorted(backends.keys()))
|
|
42
|
+
raise ValueError(f"Unknown agent type: {agent_type}. Valid types: {valid}")
|
|
43
|
+
|
|
44
|
+
return backends[agent_type]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base protocol for CLI agent backends.
|
|
3
|
+
|
|
4
|
+
Defines the interface that all CLI backends (Claude, Codex, etc.) must implement.
|
|
5
|
+
This abstraction allows claude-team to orchestrate different agent CLIs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import abstractmethod
|
|
9
|
+
from typing import Literal, Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class AgentCLI(Protocol):
|
|
14
|
+
"""
|
|
15
|
+
Protocol defining the interface for agent CLI backends.
|
|
16
|
+
|
|
17
|
+
Each implementation encapsulates the CLI-specific details:
|
|
18
|
+
- Command and arguments
|
|
19
|
+
- Ready detection patterns
|
|
20
|
+
- Idle/completion detection method
|
|
21
|
+
- Settings/hook injection support
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def engine_id(self) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Unique identifier for this CLI engine (e.g., "claude", "codex").
|
|
29
|
+
|
|
30
|
+
Used for configuration, logging, and distinguishing between backends.
|
|
31
|
+
"""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def command(self) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Return the CLI executable name or path.
|
|
38
|
+
|
|
39
|
+
Examples: "claude", "codex", "/usr/local/bin/custom-agent"
|
|
40
|
+
"""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def build_args(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
dangerously_skip_permissions: bool = False,
|
|
48
|
+
settings_file: str | None = None,
|
|
49
|
+
) -> list[str]:
|
|
50
|
+
"""
|
|
51
|
+
Build the argument list for the CLI command.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
dangerously_skip_permissions: If True, add flag to skip permission prompts
|
|
55
|
+
settings_file: Optional path to settings file for hook injection
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
List of command-line arguments (not including the command itself)
|
|
59
|
+
"""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def ready_patterns(self) -> list[str]:
|
|
64
|
+
"""
|
|
65
|
+
Return patterns that indicate the CLI is ready for input.
|
|
66
|
+
|
|
67
|
+
These patterns are searched for in terminal output to detect when
|
|
68
|
+
the agent has started and is ready to receive prompts.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List of strings to search for in terminal output
|
|
72
|
+
"""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def idle_detection_method(self) -> Literal["stop_hook", "jsonl_stream", "none"]:
|
|
77
|
+
"""
|
|
78
|
+
Return the method used to detect when the agent finishes responding.
|
|
79
|
+
|
|
80
|
+
- "stop_hook": Uses a Stop hook that fires when the agent completes
|
|
81
|
+
- "jsonl_stream": Monitors JSONL output for completion markers
|
|
82
|
+
- "none": No idle detection available (must use timeouts)
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
The detection method identifier
|
|
86
|
+
"""
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def supports_settings_file(self) -> bool:
|
|
91
|
+
"""
|
|
92
|
+
Return whether this CLI supports --settings flag for hook injection.
|
|
93
|
+
|
|
94
|
+
If False, build_args() should ignore the settings_file parameter.
|
|
95
|
+
"""
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
def build_full_command(
|
|
99
|
+
self,
|
|
100
|
+
*,
|
|
101
|
+
dangerously_skip_permissions: bool = False,
|
|
102
|
+
settings_file: str | None = None,
|
|
103
|
+
env_vars: dict[str, str] | None = None,
|
|
104
|
+
) -> str:
|
|
105
|
+
"""
|
|
106
|
+
Build the complete command string including env vars.
|
|
107
|
+
|
|
108
|
+
This is a convenience method that combines command(), build_args(),
|
|
109
|
+
and optional environment variables into a single shell command string.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
dangerously_skip_permissions: Skip permission prompts
|
|
113
|
+
settings_file: Settings file for hook injection
|
|
114
|
+
env_vars: Environment variables to prepend
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Complete command string ready for shell execution
|
|
118
|
+
"""
|
|
119
|
+
cmd = self.command()
|
|
120
|
+
args = self.build_args(
|
|
121
|
+
dangerously_skip_permissions=dangerously_skip_permissions,
|
|
122
|
+
settings_file=settings_file if self.supports_settings_file() else None,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if args:
|
|
126
|
+
cmd = f"{cmd} {' '.join(args)}"
|
|
127
|
+
|
|
128
|
+
if env_vars:
|
|
129
|
+
env_exports = " ".join(f"{k}={v}" for k, v in env_vars.items())
|
|
130
|
+
cmd = f"{env_exports} {cmd}"
|
|
131
|
+
|
|
132
|
+
return cmd
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Claude Code CLI backend.
|
|
3
|
+
|
|
4
|
+
Implements the AgentCLI protocol for Claude Code CLI.
|
|
5
|
+
This preserves the existing behavior from iterm_utils.py.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from .base import AgentCLI
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ClaudeCLI(AgentCLI):
|
|
15
|
+
"""
|
|
16
|
+
Claude Code CLI implementation.
|
|
17
|
+
|
|
18
|
+
Supports:
|
|
19
|
+
- --dangerously-skip-permissions flag
|
|
20
|
+
- --settings flag for Stop hook injection
|
|
21
|
+
- Ready detection via TUI patterns (robot banner, '>' prompt, 'tokens' status)
|
|
22
|
+
- Idle detection via Stop hook markers in JSONL
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def engine_id(self) -> str:
|
|
27
|
+
"""Return 'claude' as the engine identifier."""
|
|
28
|
+
return "claude"
|
|
29
|
+
|
|
30
|
+
def command(self) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Return the Claude CLI command.
|
|
33
|
+
|
|
34
|
+
Respects CLAUDE_TEAM_COMMAND environment variable for overrides
|
|
35
|
+
(e.g., "happy" wrapper).
|
|
36
|
+
"""
|
|
37
|
+
return os.environ.get("CLAUDE_TEAM_COMMAND", "claude")
|
|
38
|
+
|
|
39
|
+
def build_args(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
dangerously_skip_permissions: bool = False,
|
|
43
|
+
settings_file: str | None = None,
|
|
44
|
+
) -> list[str]:
|
|
45
|
+
"""
|
|
46
|
+
Build Claude CLI arguments.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
dangerously_skip_permissions: Add --dangerously-skip-permissions
|
|
50
|
+
settings_file: Path to settings JSON for Stop hook injection
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List of CLI arguments
|
|
54
|
+
"""
|
|
55
|
+
args: list[str] = []
|
|
56
|
+
|
|
57
|
+
if dangerously_skip_permissions:
|
|
58
|
+
args.append("--dangerously-skip-permissions")
|
|
59
|
+
|
|
60
|
+
# Only add --settings for the default 'claude' command.
|
|
61
|
+
# Custom commands like 'happy' have their own session tracking mechanisms.
|
|
62
|
+
# See HAPPY_INTEGRATION_RESEARCH.md for full analysis.
|
|
63
|
+
if settings_file and self._is_default_command():
|
|
64
|
+
args.append("--settings")
|
|
65
|
+
args.append(settings_file)
|
|
66
|
+
|
|
67
|
+
return args
|
|
68
|
+
|
|
69
|
+
def ready_patterns(self) -> list[str]:
|
|
70
|
+
"""
|
|
71
|
+
Return patterns indicating Claude TUI is ready.
|
|
72
|
+
|
|
73
|
+
These patterns appear in Claude's startup:
|
|
74
|
+
- '>' prompt indicates input ready
|
|
75
|
+
- 'tokens' in status bar
|
|
76
|
+
- Parts of the robot ASCII art banner
|
|
77
|
+
"""
|
|
78
|
+
return [
|
|
79
|
+
">", # Input prompt
|
|
80
|
+
"tokens", # Status bar
|
|
81
|
+
"Claude Code v", # Version line in banner
|
|
82
|
+
"▐▛███▜▌", # Top of robot head
|
|
83
|
+
"▝▜█████▛▘", # Middle of robot
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
def idle_detection_method(self) -> Literal["stop_hook", "jsonl_stream", "none"]:
|
|
87
|
+
"""
|
|
88
|
+
Claude uses Stop hook for idle detection.
|
|
89
|
+
|
|
90
|
+
A Stop hook writes a marker to the JSONL when Claude finishes responding.
|
|
91
|
+
"""
|
|
92
|
+
return "stop_hook"
|
|
93
|
+
|
|
94
|
+
def supports_settings_file(self) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
Claude supports --settings for hook injection.
|
|
97
|
+
|
|
98
|
+
Only returns True for the default 'claude' command.
|
|
99
|
+
Custom wrappers may have their own settings mechanisms.
|
|
100
|
+
"""
|
|
101
|
+
return self._is_default_command()
|
|
102
|
+
|
|
103
|
+
def _is_default_command(self) -> bool:
|
|
104
|
+
"""Check if using the default 'claude' command (not a custom wrapper)."""
|
|
105
|
+
cmd = os.environ.get("CLAUDE_TEAM_COMMAND", "claude")
|
|
106
|
+
return cmd == "claude"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# Singleton instance for convenience
|
|
110
|
+
claude_cli = ClaudeCLI()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAI Codex CLI backend.
|
|
3
|
+
|
|
4
|
+
Implements the AgentCLI protocol for OpenAI's Codex CLI.
|
|
5
|
+
This is a basic implementation - full integration will be done in later tasks.
|
|
6
|
+
|
|
7
|
+
Codex CLI reference: https://github.com/openai/codex
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from .base import AgentCLI
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CodexCLI(AgentCLI):
|
|
17
|
+
"""
|
|
18
|
+
OpenAI Codex CLI implementation.
|
|
19
|
+
|
|
20
|
+
Note: This is a basic structure. Full Codex integration (ready detection,
|
|
21
|
+
idle detection, etc.) will be implemented in later tasks (cic-f7w.3+).
|
|
22
|
+
|
|
23
|
+
Codex CLI characteristics:
|
|
24
|
+
- Uses `codex` command
|
|
25
|
+
- Has --full-auto flag for non-interactive mode
|
|
26
|
+
- No known Stop hook equivalent (may need JSONL streaming or timeouts)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def engine_id(self) -> str:
|
|
31
|
+
"""Return 'codex' as the engine identifier."""
|
|
32
|
+
return "codex"
|
|
33
|
+
|
|
34
|
+
def command(self) -> str:
|
|
35
|
+
"""
|
|
36
|
+
Return the Codex CLI command.
|
|
37
|
+
|
|
38
|
+
Respects CLAUDE_TEAM_CODEX_COMMAND environment variable for overrides
|
|
39
|
+
(e.g., "happy codex" wrapper).
|
|
40
|
+
"""
|
|
41
|
+
return os.environ.get("CLAUDE_TEAM_CODEX_COMMAND", "codex")
|
|
42
|
+
|
|
43
|
+
def build_args(
|
|
44
|
+
self,
|
|
45
|
+
*,
|
|
46
|
+
dangerously_skip_permissions: bool = False,
|
|
47
|
+
settings_file: str | None = None,
|
|
48
|
+
) -> list[str]:
|
|
49
|
+
"""
|
|
50
|
+
Build Codex CLI arguments for interactive mode.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
dangerously_skip_permissions: Maps to --full-auto for Codex
|
|
54
|
+
settings_file: Ignored - Codex doesn't support settings injection
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
List of CLI arguments for interactive mode
|
|
58
|
+
"""
|
|
59
|
+
args: list[str] = []
|
|
60
|
+
|
|
61
|
+
# Codex uses --dangerously-bypass-approvals-and-sandbox for autonomous operation
|
|
62
|
+
# (--full-auto doesn't work through happy wrapper)
|
|
63
|
+
if dangerously_skip_permissions:
|
|
64
|
+
args.append("--dangerously-bypass-approvals-and-sandbox")
|
|
65
|
+
|
|
66
|
+
# Note: settings_file is ignored - Codex doesn't support this
|
|
67
|
+
# Idle detection uses session file polling instead
|
|
68
|
+
|
|
69
|
+
return args
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def ready_patterns(self) -> list[str]:
|
|
73
|
+
"""
|
|
74
|
+
Return patterns indicating Codex CLI is ready for input.
|
|
75
|
+
|
|
76
|
+
Codex in interactive mode shows status bar when ready.
|
|
77
|
+
Updated for Codex CLI v0.80.0 behavior.
|
|
78
|
+
"""
|
|
79
|
+
return [
|
|
80
|
+
"context left", # Status bar shows "100% context left"
|
|
81
|
+
"for shortcuts", # Status bar shows "? for shortcuts"
|
|
82
|
+
"What can I help you with?", # Legacy prompt (older versions)
|
|
83
|
+
"codex>", # Alternative prompt pattern
|
|
84
|
+
"»", # Codex uses this prompt symbol
|
|
85
|
+
"Waiting for messages", # Happy codex wrapper
|
|
86
|
+
"Codex Agent Running", # Happy codex status bar
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
def idle_detection_method(self) -> Literal["stop_hook", "jsonl_stream", "none"]:
|
|
90
|
+
"""
|
|
91
|
+
Codex idle detection method.
|
|
92
|
+
|
|
93
|
+
Codex writes session files to ~/.codex/sessions/YYYY/MM/DD/.
|
|
94
|
+
The idle_detection module polls these files for agent_message
|
|
95
|
+
events which indicate the agent has finished responding.
|
|
96
|
+
"""
|
|
97
|
+
return "jsonl_stream"
|
|
98
|
+
|
|
99
|
+
def supports_settings_file(self) -> bool:
|
|
100
|
+
"""
|
|
101
|
+
Codex doesn't support --settings for hook injection.
|
|
102
|
+
|
|
103
|
+
Alternative completion detection methods will be needed.
|
|
104
|
+
"""
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# Singleton instance for convenience
|
|
110
|
+
codex_cli = CodexCLI()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dynamic tab color generation for iTerm2 sessions.
|
|
3
|
+
|
|
4
|
+
Generates visually distinct colors using the golden ratio for hue distribution,
|
|
5
|
+
ensuring each session gets a unique, easily distinguishable tab color.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import colorsys
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from iterm2.color import Color as ItermColor
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Golden ratio conjugate (φ - 1), used for even hue distribution
|
|
16
|
+
GOLDEN_RATIO_CONJUGATE = 0.618033988749895
|
|
17
|
+
|
|
18
|
+
# Default saturation and lightness values for tab colors (HSL)
|
|
19
|
+
DEFAULT_SATURATION = 0.65 # 65%
|
|
20
|
+
DEFAULT_LIGHTNESS = 0.55 # 55%
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def generate_tab_color(
|
|
24
|
+
index: int,
|
|
25
|
+
saturation: float = DEFAULT_SATURATION,
|
|
26
|
+
lightness: float = DEFAULT_LIGHTNESS,
|
|
27
|
+
) -> "ItermColor":
|
|
28
|
+
"""
|
|
29
|
+
Generate a distinct tab color for a given index.
|
|
30
|
+
|
|
31
|
+
Uses the golden ratio to distribute hues evenly across the color wheel,
|
|
32
|
+
ensuring that consecutively spawned sessions have visually distinct colors.
|
|
33
|
+
The golden ratio approach avoids clustering that can occur with linear
|
|
34
|
+
hue increments.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
index: The session index (0-based). Each index produces a unique hue.
|
|
38
|
+
saturation: HSL saturation value (0.0-1.0). Default 0.65 (65%).
|
|
39
|
+
lightness: HSL lightness value (0.0-1.0). Default 0.55 (55%).
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
iterm2.Color object ready to use with tab color APIs.
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
# First session gets a warm orange-red
|
|
46
|
+
color0 = generate_tab_color(0)
|
|
47
|
+
|
|
48
|
+
# Second session gets a contrasting blue-green
|
|
49
|
+
color1 = generate_tab_color(1)
|
|
50
|
+
|
|
51
|
+
# Colors remain visually distinct even for many sessions
|
|
52
|
+
color10 = generate_tab_color(10)
|
|
53
|
+
"""
|
|
54
|
+
from iterm2.color import Color
|
|
55
|
+
|
|
56
|
+
# Calculate hue using golden ratio distribution
|
|
57
|
+
# Multiply index by golden ratio conjugate and take fractional part
|
|
58
|
+
# This distributes hues evenly across the color wheel
|
|
59
|
+
hue = (index * GOLDEN_RATIO_CONJUGATE) % 1.0
|
|
60
|
+
|
|
61
|
+
# Convert HSL to RGB (colorsys uses HLS ordering: hue, lightness, saturation)
|
|
62
|
+
r, g, b = colorsys.hls_to_rgb(hue, lightness, saturation)
|
|
63
|
+
|
|
64
|
+
# iterm2.Color expects integer RGB values 0-255
|
|
65
|
+
return Color(
|
|
66
|
+
r=int(r * 255),
|
|
67
|
+
g=int(g * 255),
|
|
68
|
+
b=int(b * 255),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def hsl_to_rgb_tuple(
|
|
73
|
+
hue: float,
|
|
74
|
+
saturation: float = DEFAULT_SATURATION,
|
|
75
|
+
lightness: float = DEFAULT_LIGHTNESS,
|
|
76
|
+
) -> tuple[int, int, int]:
|
|
77
|
+
"""
|
|
78
|
+
Convert HSL values to RGB tuple (0-255 range).
|
|
79
|
+
|
|
80
|
+
Utility function for cases where raw RGB values are needed
|
|
81
|
+
instead of an iterm2.Color object.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
hue: HSL hue value (0.0-1.0)
|
|
85
|
+
saturation: HSL saturation value (0.0-1.0)
|
|
86
|
+
lightness: HSL lightness value (0.0-1.0)
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Tuple of (red, green, blue) integers in 0-255 range.
|
|
90
|
+
"""
|
|
91
|
+
# colorsys uses HLS (hue, lightness, saturation) ordering
|
|
92
|
+
r, g, b = colorsys.hls_to_rgb(hue, lightness, saturation)
|
|
93
|
+
return (int(r * 255), int(g * 255), int(b * 255))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_hue_for_index(index: int) -> float:
|
|
97
|
+
"""
|
|
98
|
+
Get the hue value (0.0-1.0) for a given index.
|
|
99
|
+
|
|
100
|
+
Useful when you need just the hue for custom color manipulation.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
index: The session index (0-based)
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Hue value in range 0.0 to 1.0
|
|
107
|
+
"""
|
|
108
|
+
return (index * GOLDEN_RATIO_CONJUGATE) % 1.0
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Formatting utilities for Claude Team MCP.
|
|
3
|
+
|
|
4
|
+
Provides functions for formatting session titles, badge text, and other
|
|
5
|
+
display strings used in iTerm2 tabs and UI badges.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def format_session_title(
|
|
12
|
+
session_name: str,
|
|
13
|
+
issue_id: Optional[str] = None,
|
|
14
|
+
annotation: Optional[str] = None,
|
|
15
|
+
) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Format a session title for iTerm2 tab display.
|
|
18
|
+
|
|
19
|
+
Creates a formatted title string combining session name, optional issue ID,
|
|
20
|
+
and optional annotation.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
session_name: Session identifier (e.g., "worker-1")
|
|
24
|
+
issue_id: Optional issue/ticket ID (e.g., "cic-3dj")
|
|
25
|
+
annotation: Optional task annotation (e.g., "profile module")
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Formatted title string.
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
>>> format_session_title("worker-1", "cic-3dj", "profile module")
|
|
32
|
+
'[worker-1] cic-3dj: profile module'
|
|
33
|
+
|
|
34
|
+
>>> format_session_title("worker-2", annotation="refactor auth")
|
|
35
|
+
'[worker-2] refactor auth'
|
|
36
|
+
|
|
37
|
+
>>> format_session_title("worker-3")
|
|
38
|
+
'[worker-3]'
|
|
39
|
+
"""
|
|
40
|
+
# Build the title in parts
|
|
41
|
+
title_parts = [f"[{session_name}]"]
|
|
42
|
+
|
|
43
|
+
if issue_id and annotation:
|
|
44
|
+
# Both issue ID and annotation: "issue_id: annotation"
|
|
45
|
+
title_parts.append(f"{issue_id}: {annotation}")
|
|
46
|
+
elif issue_id:
|
|
47
|
+
# Only issue ID
|
|
48
|
+
title_parts.append(issue_id)
|
|
49
|
+
elif annotation:
|
|
50
|
+
# Only annotation
|
|
51
|
+
title_parts.append(annotation)
|
|
52
|
+
|
|
53
|
+
return " ".join(title_parts)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def format_badge_text(
|
|
57
|
+
name: str,
|
|
58
|
+
bead: Optional[str] = None,
|
|
59
|
+
annotation: Optional[str] = None,
|
|
60
|
+
agent_type: Optional[str] = None,
|
|
61
|
+
max_annotation_length: int = 30,
|
|
62
|
+
) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Format badge text with bead/name on first line, annotation on second.
|
|
65
|
+
|
|
66
|
+
Creates a multi-line string suitable for iTerm2 badge display:
|
|
67
|
+
- Line 1: Agent type prefix (if not "claude") + bead ID (if provided) or worker name
|
|
68
|
+
- Line 2: annotation (if provided), truncated if too long
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
name: Worker name (used if bead not provided)
|
|
72
|
+
bead: Optional bead/issue ID (e.g., "cic-3dj")
|
|
73
|
+
annotation: Optional task annotation
|
|
74
|
+
agent_type: Optional agent type ("claude" or "codex"). If "codex",
|
|
75
|
+
adds a prefix to the first line.
|
|
76
|
+
max_annotation_length: Maximum length for annotation line (default 30)
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Badge text, potentially multi-line.
|
|
80
|
+
|
|
81
|
+
Examples:
|
|
82
|
+
>>> format_badge_text("Groucho", "cic-3dj", "profile module")
|
|
83
|
+
'cic-3dj\\nprofile module'
|
|
84
|
+
|
|
85
|
+
>>> format_badge_text("Groucho", annotation="quick task")
|
|
86
|
+
'Groucho\\nquick task'
|
|
87
|
+
|
|
88
|
+
>>> format_badge_text("Groucho", "cic-3dj")
|
|
89
|
+
'cic-3dj'
|
|
90
|
+
|
|
91
|
+
>>> format_badge_text("Groucho")
|
|
92
|
+
'Groucho'
|
|
93
|
+
|
|
94
|
+
>>> format_badge_text("Groucho", annotation="a very long annotation here", max_annotation_length=20)
|
|
95
|
+
'Groucho\\na very long annot...'
|
|
96
|
+
|
|
97
|
+
>>> format_badge_text("Groucho", agent_type="codex")
|
|
98
|
+
'[Codex] Groucho'
|
|
99
|
+
|
|
100
|
+
>>> format_badge_text("Groucho", "cic-3dj", agent_type="codex")
|
|
101
|
+
'[Codex] cic-3dj'
|
|
102
|
+
"""
|
|
103
|
+
# First line: bead if provided, otherwise name
|
|
104
|
+
first_line = bead if bead else name
|
|
105
|
+
|
|
106
|
+
# Add agent type prefix for non-Claude agents
|
|
107
|
+
if agent_type and agent_type != "claude":
|
|
108
|
+
# Capitalize the agent type for display (e.g., "codex" -> "Codex")
|
|
109
|
+
type_display = agent_type.capitalize()
|
|
110
|
+
first_line = f"[{type_display}] {first_line}"
|
|
111
|
+
|
|
112
|
+
# Second line: annotation if provided, with truncation
|
|
113
|
+
if annotation:
|
|
114
|
+
if len(annotation) > max_annotation_length:
|
|
115
|
+
# Reserve 3 chars for ellipsis
|
|
116
|
+
truncated = annotation[: max_annotation_length - 3].rstrip()
|
|
117
|
+
annotation = f"{truncated}..."
|
|
118
|
+
return f"{first_line}\n{annotation}"
|
|
119
|
+
|
|
120
|
+
return first_line
|