claude-team-mcp 0.1.0__py3-none-any.whl → 0.2.1__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.
Files changed (36) hide show
  1. claude_team_mcp/colors.py +4 -4
  2. claude_team_mcp/formatting.py +46 -49
  3. claude_team_mcp/idle_detection.py +208 -0
  4. claude_team_mcp/iterm_utils.py +204 -306
  5. claude_team_mcp/names.py +424 -0
  6. claude_team_mcp/profile.py +44 -505
  7. claude_team_mcp/registry.py +140 -113
  8. claude_team_mcp/server.py +42 -2107
  9. claude_team_mcp/session_state.py +384 -123
  10. claude_team_mcp/tools/__init__.py +52 -0
  11. claude_team_mcp/tools/adopt_worker.py +122 -0
  12. claude_team_mcp/tools/annotate_worker.py +57 -0
  13. claude_team_mcp/tools/bd_help.py +42 -0
  14. claude_team_mcp/tools/check_idle_workers.py +98 -0
  15. claude_team_mcp/tools/close_workers.py +194 -0
  16. claude_team_mcp/tools/discover_workers.py +129 -0
  17. claude_team_mcp/tools/examine_worker.py +56 -0
  18. claude_team_mcp/tools/list_workers.py +76 -0
  19. claude_team_mcp/tools/list_worktrees.py +106 -0
  20. claude_team_mcp/tools/message_workers.py +217 -0
  21. claude_team_mcp/tools/read_worker_logs.py +158 -0
  22. claude_team_mcp/tools/spawn_workers.py +601 -0
  23. claude_team_mcp/tools/wait_idle_workers.py +148 -0
  24. claude_team_mcp/utils/__init__.py +17 -0
  25. claude_team_mcp/utils/constants.py +81 -0
  26. claude_team_mcp/utils/errors.py +87 -0
  27. claude_team_mcp/utils/worktree_detection.py +79 -0
  28. claude_team_mcp/worker_prompt.py +164 -0
  29. claude_team_mcp/worktree.py +532 -0
  30. claude_team_mcp-0.2.1.dist-info/METADATA +377 -0
  31. claude_team_mcp-0.2.1.dist-info/RECORD +36 -0
  32. claude_team_mcp/task_completion.py +0 -681
  33. claude_team_mcp-0.1.0.dist-info/METADATA +0 -235
  34. claude_team_mcp-0.1.0.dist-info/RECORD +0 -15
  35. {claude_team_mcp-0.1.0.dist-info → claude_team_mcp-0.2.1.dist-info}/WHEEL +0 -0
  36. {claude_team_mcp-0.1.0.dist-info → claude_team_mcp-0.2.1.dist-info}/entry_points.txt +0 -0
claude_team_mcp/colors.py CHANGED
@@ -9,7 +9,7 @@ import colorsys
9
9
  from typing import TYPE_CHECKING
10
10
 
11
11
  if TYPE_CHECKING:
12
- import iterm2
12
+ from iterm2.color import Color as ItermColor
13
13
 
14
14
 
15
15
  # Golden ratio conjugate (φ - 1), used for even hue distribution
@@ -24,7 +24,7 @@ def generate_tab_color(
24
24
  index: int,
25
25
  saturation: float = DEFAULT_SATURATION,
26
26
  lightness: float = DEFAULT_LIGHTNESS,
27
- ) -> "iterm2.Color":
27
+ ) -> "ItermColor":
28
28
  """
29
29
  Generate a distinct tab color for a given index.
30
30
 
@@ -51,7 +51,7 @@ def generate_tab_color(
51
51
  # Colors remain visually distinct even for many sessions
52
52
  color10 = generate_tab_color(10)
53
53
  """
54
- import iterm2
54
+ from iterm2.color import Color
55
55
 
56
56
  # Calculate hue using golden ratio distribution
57
57
  # Multiply index by golden ratio conjugate and take fractional part
@@ -62,7 +62,7 @@ def generate_tab_color(
62
62
  r, g, b = colorsys.hls_to_rgb(hue, lightness, saturation)
63
63
 
64
64
  # iterm2.Color expects integer RGB values 0-255
65
- return iterm2.Color(
65
+ return Color(
66
66
  r=int(r * 255),
67
67
  g=int(g * 255),
68
68
  b=int(b * 255),
@@ -11,18 +11,18 @@ from typing import Optional
11
11
  def format_session_title(
12
12
  session_name: str,
13
13
  issue_id: Optional[str] = None,
14
- task_desc: Optional[str] = None,
14
+ annotation: Optional[str] = None,
15
15
  ) -> str:
16
16
  """
17
17
  Format a session title for iTerm2 tab display.
18
18
 
19
19
  Creates a formatted title string combining session name, optional issue ID,
20
- and optional task description.
20
+ and optional annotation.
21
21
 
22
22
  Args:
23
23
  session_name: Session identifier (e.g., "worker-1")
24
24
  issue_id: Optional issue/ticket ID (e.g., "cic-3dj")
25
- task_desc: Optional task description (e.g., "profile module")
25
+ annotation: Optional task annotation (e.g., "profile module")
26
26
 
27
27
  Returns:
28
28
  Formatted title string.
@@ -31,7 +31,7 @@ def format_session_title(
31
31
  >>> format_session_title("worker-1", "cic-3dj", "profile module")
32
32
  '[worker-1] cic-3dj: profile module'
33
33
 
34
- >>> format_session_title("worker-2", task_desc="refactor auth")
34
+ >>> format_session_title("worker-2", annotation="refactor auth")
35
35
  '[worker-2] refactor auth'
36
36
 
37
37
  >>> format_session_title("worker-3")
@@ -40,69 +40,66 @@ def format_session_title(
40
40
  # Build the title in parts
41
41
  title_parts = [f"[{session_name}]"]
42
42
 
43
- if issue_id and task_desc:
44
- # Both issue ID and description: "issue_id: task_desc"
45
- title_parts.append(f"{issue_id}: {task_desc}")
43
+ if issue_id and annotation:
44
+ # Both issue ID and annotation: "issue_id: annotation"
45
+ title_parts.append(f"{issue_id}: {annotation}")
46
46
  elif issue_id:
47
47
  # Only issue ID
48
48
  title_parts.append(issue_id)
49
- elif task_desc:
50
- # Only description
51
- title_parts.append(task_desc)
49
+ elif annotation:
50
+ # Only annotation
51
+ title_parts.append(annotation)
52
52
 
53
53
  return " ".join(title_parts)
54
54
 
55
55
 
56
56
  def format_badge_text(
57
- issue_id: Optional[str] = None,
58
- task_desc: Optional[str] = None,
59
- max_length: int = 25,
57
+ name: str,
58
+ bead: Optional[str] = None,
59
+ annotation: Optional[str] = None,
60
+ max_annotation_length: int = 30,
60
61
  ) -> str:
61
62
  """
62
- Format abbreviated badge text for iTerm2 badge display.
63
+ Format badge text with bead/name on first line, annotation on second.
63
64
 
64
- Creates a compact string suitable for display in an iTerm2 badge,
65
- truncating with ellipsis if necessary.
65
+ Creates a multi-line string suitable for iTerm2 badge display:
66
+ - Line 1: bead ID if provided, otherwise worker name
67
+ - Line 2: annotation (if provided), truncated if too long
66
68
 
67
69
  Args:
68
- issue_id: Optional issue/ticket ID (e.g., "cic-3dj")
69
- task_desc: Optional task description (e.g., "profile module")
70
- max_length: Maximum length of the output string (default 25)
70
+ name: Worker name (used if bead not provided)
71
+ bead: Optional bead/issue ID (e.g., "cic-3dj")
72
+ annotation: Optional task annotation
73
+ max_annotation_length: Maximum length for annotation line (default 30)
71
74
 
72
75
  Returns:
73
- Abbreviated badge text, truncated with "..." if needed.
76
+ Badge text, potentially multi-line.
74
77
 
75
78
  Examples:
76
- >>> format_badge_text("cic-3dj", "profile module", max_length=25)
77
- 'cic-3dj: profile module'
79
+ >>> format_badge_text("Groucho", "cic-3dj", "profile module")
80
+ 'cic-3dj\\nprofile module'
78
81
 
79
- >>> format_badge_text("cic-3dj", "implement user authentication system", max_length=25)
80
- 'cic-3dj: implement us...'
82
+ >>> format_badge_text("Groucho", annotation="quick task")
83
+ 'Groucho\\nquick task'
81
84
 
82
- >>> format_badge_text(task_desc="quick fix", max_length=25)
83
- 'quick fix'
85
+ >>> format_badge_text("Groucho", "cic-3dj")
86
+ 'cic-3dj'
84
87
 
85
- >>> format_badge_text()
86
- ''
87
- """
88
- if max_length < 4:
89
- # Too short to meaningfully truncate
90
- max_length = 4
88
+ >>> format_badge_text("Groucho")
89
+ 'Groucho'
91
90
 
92
- # Build the full text
93
- if issue_id and task_desc:
94
- full_text = f"{issue_id}: {task_desc}"
95
- elif issue_id:
96
- full_text = issue_id
97
- elif task_desc:
98
- full_text = task_desc
99
- else:
100
- return ""
101
-
102
- # Truncate if necessary
103
- if len(full_text) <= max_length:
104
- return full_text
105
-
106
- # Reserve 3 characters for ellipsis
107
- truncated = full_text[: max_length - 3].rstrip()
108
- return f"{truncated}..."
91
+ >>> format_badge_text("Groucho", annotation="a very long annotation here", max_annotation_length=20)
92
+ 'Groucho\\na very long annot...'
93
+ """
94
+ # First line: bead if provided, otherwise name
95
+ first_line = bead if bead else name
96
+
97
+ # Second line: annotation if provided, with truncation
98
+ if annotation:
99
+ if len(annotation) > max_annotation_length:
100
+ # Reserve 3 chars for ellipsis
101
+ truncated = annotation[: max_annotation_length - 3].rstrip()
102
+ annotation = f"{truncated}..."
103
+ return f"{first_line}\n{annotation}"
104
+
105
+ return first_line
@@ -0,0 +1,208 @@
1
+ """
2
+ Idle Detection for Claude Team Workers
3
+
4
+ Detects when workers are idle (finished responding) using Stop hook signals.
5
+ Workers are spawned with a Stop hook that fires when Claude finishes responding.
6
+ The hook embeds a session ID marker in the JSONL - if it fired with no subsequent
7
+ messages, the worker is idle.
8
+
9
+ Binary state model:
10
+ - Idle: Stop hook fired, no messages after it
11
+ - Working: Either no stop hook yet, or messages exist after the last one
12
+ """
13
+
14
+ import asyncio
15
+ import logging
16
+ import time
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+
20
+ from .session_state import is_session_stopped
21
+
22
+ logger = logging.getLogger("claude-team-mcp")
23
+
24
+ # Default timeout for waiting operations (10 minutes)
25
+ DEFAULT_TIMEOUT = 600.0
26
+ DEFAULT_POLL_INTERVAL = 2.0
27
+
28
+
29
+ def is_idle(jsonl_path: Path, session_id: str) -> bool:
30
+ """
31
+ Check if a session is idle (finished responding).
32
+
33
+ A session is idle if its Stop hook has fired and no messages
34
+ have been sent after it.
35
+
36
+ Args:
37
+ jsonl_path: Path to the session JSONL file
38
+ session_id: The session ID (matches marker in Stop hook)
39
+
40
+ Returns:
41
+ True if idle, False if working or file not found
42
+ """
43
+ if not jsonl_path.exists():
44
+ return False
45
+ return is_session_stopped(jsonl_path, session_id)
46
+
47
+
48
+ async def wait_for_idle(
49
+ jsonl_path: Path,
50
+ session_id: str,
51
+ timeout: float = DEFAULT_TIMEOUT,
52
+ poll_interval: float = DEFAULT_POLL_INTERVAL,
53
+ ) -> dict:
54
+ """
55
+ Wait for a session to become idle.
56
+
57
+ Polls until the Stop hook fires or timeout is reached.
58
+
59
+ Args:
60
+ jsonl_path: Path to session JSONL file
61
+ session_id: The session ID to check
62
+ timeout: Maximum seconds to wait (default 600s / 10 min)
63
+ poll_interval: Seconds between checks
64
+
65
+ Returns:
66
+ Dict with {idle: bool, session_id: str, waited_seconds: float, timed_out: bool}
67
+ """
68
+ start = time.time()
69
+
70
+ while time.time() - start < timeout:
71
+ if is_idle(jsonl_path, session_id):
72
+ return {
73
+ "idle": True,
74
+ "session_id": session_id,
75
+ "waited_seconds": time.time() - start,
76
+ "timed_out": False,
77
+ }
78
+ await asyncio.sleep(poll_interval)
79
+
80
+ # Timeout
81
+ return {
82
+ "idle": False,
83
+ "session_id": session_id,
84
+ "waited_seconds": timeout,
85
+ "timed_out": True,
86
+ }
87
+
88
+
89
+ @dataclass
90
+ class SessionInfo:
91
+ """Info needed to check a session's idle state."""
92
+
93
+ jsonl_path: Path
94
+ session_id: str
95
+
96
+
97
+ async def wait_for_any_idle(
98
+ sessions: list[SessionInfo],
99
+ timeout: float = DEFAULT_TIMEOUT,
100
+ poll_interval: float = DEFAULT_POLL_INTERVAL,
101
+ ) -> dict:
102
+ """
103
+ Wait for ANY session to become idle.
104
+
105
+ Returns as soon as the first session becomes idle.
106
+ Useful for pipeline patterns where you want to process results
107
+ as they become available.
108
+
109
+ Args:
110
+ sessions: List of SessionInfo to monitor
111
+ timeout: Maximum seconds to wait
112
+ poll_interval: Seconds between checks
113
+
114
+ Returns:
115
+ Dict with {
116
+ idle_session_id: str | None, # First session to become idle
117
+ idle: bool, # True if any session became idle
118
+ waited_seconds: float,
119
+ timed_out: bool,
120
+ }
121
+ """
122
+ start = time.time()
123
+
124
+ while time.time() - start < timeout:
125
+ for session in sessions:
126
+ if is_idle(session.jsonl_path, session.session_id):
127
+ return {
128
+ "idle_session_id": session.session_id,
129
+ "idle": True,
130
+ "waited_seconds": time.time() - start,
131
+ "timed_out": False,
132
+ }
133
+ await asyncio.sleep(poll_interval)
134
+
135
+ return {
136
+ "idle_session_id": None,
137
+ "idle": False,
138
+ "waited_seconds": timeout,
139
+ "timed_out": True,
140
+ }
141
+
142
+
143
+ async def wait_for_all_idle(
144
+ sessions: list[SessionInfo],
145
+ timeout: float = DEFAULT_TIMEOUT,
146
+ poll_interval: float = DEFAULT_POLL_INTERVAL,
147
+ ) -> dict:
148
+ """
149
+ Wait for ALL sessions to become idle.
150
+
151
+ Returns when every session has become idle, or on timeout.
152
+ Useful for fan-out/fan-in patterns where you need all results
153
+ before proceeding.
154
+
155
+ Args:
156
+ sessions: List of SessionInfo to monitor
157
+ timeout: Maximum seconds to wait
158
+ poll_interval: Seconds between checks
159
+
160
+ Returns:
161
+ Dict with {
162
+ idle_session_ids: list[str], # Sessions that are idle
163
+ all_idle: bool, # True if all sessions became idle
164
+ waiting_on: list[str], # Sessions still working (if timed out)
165
+ waited_seconds: float,
166
+ timed_out: bool,
167
+ }
168
+ """
169
+ start = time.time()
170
+
171
+ while time.time() - start < timeout:
172
+ idle_sessions = []
173
+ working_sessions = []
174
+
175
+ for session in sessions:
176
+ if is_idle(session.jsonl_path, session.session_id):
177
+ idle_sessions.append(session.session_id)
178
+ else:
179
+ working_sessions.append(session.session_id)
180
+
181
+ if not working_sessions:
182
+ # All idle!
183
+ return {
184
+ "idle_session_ids": idle_sessions,
185
+ "all_idle": True,
186
+ "waiting_on": [],
187
+ "waited_seconds": time.time() - start,
188
+ "timed_out": False,
189
+ }
190
+
191
+ await asyncio.sleep(poll_interval)
192
+
193
+ # Timeout - return final state
194
+ idle_sessions = []
195
+ working_sessions = []
196
+ for session in sessions:
197
+ if is_idle(session.jsonl_path, session.session_id):
198
+ idle_sessions.append(session.session_id)
199
+ else:
200
+ working_sessions.append(session.session_id)
201
+
202
+ return {
203
+ "idle_session_ids": idle_sessions,
204
+ "all_idle": False,
205
+ "waiting_on": working_sessions,
206
+ "waited_seconds": timeout,
207
+ "timed_out": True,
208
+ }