claude-team-mcp 0.6.1__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/__init__.py +11 -0
- claude_team/events.py +501 -0
- claude_team/idle_detection.py +173 -0
- claude_team/poller.py +245 -0
- 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/idle_detection.py +16 -3
- claude_team_mcp/issue_tracker/__init__.py +68 -3
- claude_team_mcp/iterm_utils.py +5 -73
- claude_team_mcp/registry.py +43 -26
- claude_team_mcp/server.py +164 -61
- claude_team_mcp/session_state.py +364 -2
- claude_team_mcp/terminal_backends/__init__.py +49 -0
- claude_team_mcp/terminal_backends/base.py +106 -0
- claude_team_mcp/terminal_backends/iterm.py +251 -0
- claude_team_mcp/terminal_backends/tmux.py +683 -0
- claude_team_mcp/tools/__init__.py +4 -2
- claude_team_mcp/tools/adopt_worker.py +89 -32
- claude_team_mcp/tools/close_workers.py +39 -10
- claude_team_mcp/tools/discover_workers.py +176 -32
- claude_team_mcp/tools/list_workers.py +29 -0
- claude_team_mcp/tools/message_workers.py +35 -5
- claude_team_mcp/tools/poll_worker_changes.py +227 -0
- claude_team_mcp/tools/spawn_workers.py +254 -153
- claude_team_mcp/tools/wait_idle_workers.py +1 -0
- claude_team_mcp/utils/errors.py +7 -3
- claude_team_mcp/worktree.py +73 -12
- {claude_team_mcp-0.6.1.dist-info → claude_team_mcp-0.8.0.dist-info}/METADATA +1 -1
- claude_team_mcp-0.8.0.dist-info/RECORD +54 -0
- claude_team_mcp-0.6.1.dist-info/RECORD +0 -43
- {claude_team_mcp-0.6.1.dist-info → claude_team_mcp-0.8.0.dist-info}/WHEEL +0 -0
- {claude_team_mcp-0.6.1.dist-info → claude_team_mcp-0.8.0.dist-info}/entry_points.txt +0 -0
|
@@ -9,10 +9,16 @@ from __future__ import annotations
|
|
|
9
9
|
import logging
|
|
10
10
|
import os
|
|
11
11
|
from dataclasses import dataclass, field
|
|
12
|
-
from typing import Protocol, runtime_checkable
|
|
12
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from claude_team_mcp.config import ClaudeTeamConfig
|
|
13
16
|
|
|
14
17
|
logger = logging.getLogger("claude-team-mcp")
|
|
15
18
|
|
|
19
|
+
# Environment variable for explicit tracker override (highest priority).
|
|
20
|
+
ISSUE_TRACKER_ENV_VAR = "CLAUDE_TEAM_ISSUE_TRACKER"
|
|
21
|
+
|
|
16
22
|
|
|
17
23
|
@runtime_checkable
|
|
18
24
|
class IssueTrackerBackend(Protocol):
|
|
@@ -86,16 +92,74 @@ BACKEND_REGISTRY: dict[str, IssueTrackerBackend] = {
|
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
|
|
89
|
-
def detect_issue_tracker(
|
|
95
|
+
def detect_issue_tracker(
|
|
96
|
+
project_path: str,
|
|
97
|
+
config: ClaudeTeamConfig | None = None,
|
|
98
|
+
) -> IssueTrackerBackend | None:
|
|
90
99
|
"""
|
|
91
100
|
Detect the issue tracker backend for the given project path.
|
|
92
101
|
|
|
102
|
+
Resolution order (highest to lowest priority):
|
|
103
|
+
1. CLAUDE_TEAM_ISSUE_TRACKER environment variable
|
|
104
|
+
2. config.issue_tracker.override setting
|
|
105
|
+
3. Marker directory detection (.pebbles, .beads)
|
|
106
|
+
|
|
93
107
|
Args:
|
|
94
108
|
project_path: Absolute or relative path to the project root.
|
|
109
|
+
config: Optional config object. If None, config is loaded from disk.
|
|
95
110
|
|
|
96
111
|
Returns:
|
|
97
|
-
The detected IssueTrackerBackend, or None if no
|
|
112
|
+
The detected IssueTrackerBackend, or None if no tracker is configured
|
|
113
|
+
or detected.
|
|
98
114
|
"""
|
|
115
|
+
# Priority 1: Environment variable override.
|
|
116
|
+
env_override = os.environ.get(ISSUE_TRACKER_ENV_VAR)
|
|
117
|
+
if env_override:
|
|
118
|
+
backend = BACKEND_REGISTRY.get(env_override.lower())
|
|
119
|
+
if backend:
|
|
120
|
+
logger.debug(
|
|
121
|
+
"Using issue tracker '%s' from %s env var",
|
|
122
|
+
backend.name,
|
|
123
|
+
ISSUE_TRACKER_ENV_VAR,
|
|
124
|
+
)
|
|
125
|
+
return backend
|
|
126
|
+
logger.warning(
|
|
127
|
+
"Unknown issue tracker '%s' in %s; ignoring",
|
|
128
|
+
env_override,
|
|
129
|
+
ISSUE_TRACKER_ENV_VAR,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Priority 2: Config file override.
|
|
133
|
+
if config is None:
|
|
134
|
+
# Lazy import to avoid circular dependency at module load time.
|
|
135
|
+
try:
|
|
136
|
+
from claude_team_mcp.config import ConfigError, load_config
|
|
137
|
+
|
|
138
|
+
config = load_config()
|
|
139
|
+
except ConfigError as exc:
|
|
140
|
+
logger.warning("Invalid config file; ignoring overrides: %s", exc)
|
|
141
|
+
config = None
|
|
142
|
+
|
|
143
|
+
if config and config.issue_tracker.override:
|
|
144
|
+
backend = BACKEND_REGISTRY.get(config.issue_tracker.override)
|
|
145
|
+
if backend:
|
|
146
|
+
logger.debug(
|
|
147
|
+
"Using issue tracker '%s' from config override",
|
|
148
|
+
backend.name,
|
|
149
|
+
)
|
|
150
|
+
return backend
|
|
151
|
+
# Config validation should prevent this, but handle gracefully.
|
|
152
|
+
logger.warning(
|
|
153
|
+
"Unknown issue tracker '%s' in config; ignoring",
|
|
154
|
+
config.issue_tracker.override,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Priority 3: Marker directory detection.
|
|
158
|
+
return _detect_from_markers(project_path)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _detect_from_markers(project_path: str) -> IssueTrackerBackend | None:
|
|
162
|
+
"""Detect issue tracker by checking for marker directories."""
|
|
99
163
|
beads_marker = os.path.join(project_path, BEADS_BACKEND.marker_dir)
|
|
100
164
|
pebbles_marker = os.path.join(project_path, PEBBLES_BACKEND.marker_dir)
|
|
101
165
|
|
|
@@ -128,5 +192,6 @@ __all__ = [
|
|
|
128
192
|
"BEADS_BACKEND",
|
|
129
193
|
"PEBBLES_BACKEND",
|
|
130
194
|
"BACKEND_REGISTRY",
|
|
195
|
+
"ISSUE_TRACKER_ENV_VAR",
|
|
131
196
|
"detect_issue_tracker",
|
|
132
197
|
]
|
claude_team_mcp/iterm_utils.py
CHANGED
|
@@ -705,75 +705,6 @@ async def start_agent_in_session(
|
|
|
705
705
|
)
|
|
706
706
|
|
|
707
707
|
|
|
708
|
-
async def start_claude_in_session(
|
|
709
|
-
session: "ItermSession",
|
|
710
|
-
project_path: str,
|
|
711
|
-
dangerously_skip_permissions: bool = False,
|
|
712
|
-
env: Optional[dict[str, str]] = None,
|
|
713
|
-
shell_ready_timeout: float = 10.0,
|
|
714
|
-
claude_ready_timeout: float = 30.0,
|
|
715
|
-
stop_hook_marker_id: Optional[str] = None,
|
|
716
|
-
) -> None:
|
|
717
|
-
"""
|
|
718
|
-
Start Claude Code in an existing iTerm2 session.
|
|
719
|
-
|
|
720
|
-
Convenience wrapper around start_agent_in_session() for Claude.
|
|
721
|
-
Changes to the project directory and launches Claude Code in a single
|
|
722
|
-
atomic command (cd && claude). Waits for shell readiness before sending
|
|
723
|
-
the command, then waits for Claude's startup banner to appear.
|
|
724
|
-
|
|
725
|
-
The command used to launch Claude Code can be overridden by setting
|
|
726
|
-
the CLAUDE_TEAM_COMMAND environment variable (defaults to "claude").
|
|
727
|
-
This is useful for running alternative Claude CLI implementations
|
|
728
|
-
like "happy" or for testing purposes.
|
|
729
|
-
|
|
730
|
-
Args:
|
|
731
|
-
session: iTerm2 session to use
|
|
732
|
-
project_path: Directory to run Claude in
|
|
733
|
-
dangerously_skip_permissions: If True, start with --dangerously-skip-permissions
|
|
734
|
-
env: Optional dict of environment variables to set before running claude
|
|
735
|
-
shell_ready_timeout: Max seconds to wait for shell prompt
|
|
736
|
-
claude_ready_timeout: Max seconds to wait for Claude to start and show banner
|
|
737
|
-
stop_hook_marker_id: If provided, inject a Stop hook that logs this marker
|
|
738
|
-
to the JSONL for completion detection
|
|
739
|
-
|
|
740
|
-
Raises:
|
|
741
|
-
RuntimeError: If shell not ready or Claude fails to start within timeout
|
|
742
|
-
"""
|
|
743
|
-
from .cli_backends import claude_cli
|
|
744
|
-
|
|
745
|
-
await start_agent_in_session(
|
|
746
|
-
session=session,
|
|
747
|
-
cli=claude_cli,
|
|
748
|
-
project_path=project_path,
|
|
749
|
-
dangerously_skip_permissions=dangerously_skip_permissions,
|
|
750
|
-
env=env,
|
|
751
|
-
shell_ready_timeout=shell_ready_timeout,
|
|
752
|
-
agent_ready_timeout=claude_ready_timeout,
|
|
753
|
-
stop_hook_marker_id=stop_hook_marker_id,
|
|
754
|
-
)
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
# Legacy alias for backward compatibility with Claude-specific code
|
|
758
|
-
# that checks for banner patterns. Uses wait_for_agent_ready with claude_cli.
|
|
759
|
-
async def _wait_for_claude_ready_via_agent(
|
|
760
|
-
session: "ItermSession",
|
|
761
|
-
timeout_seconds: float = 15.0,
|
|
762
|
-
poll_interval: float = 0.2,
|
|
763
|
-
stable_count: int = 2,
|
|
764
|
-
) -> bool:
|
|
765
|
-
"""Internal helper - uses wait_for_agent_ready with Claude CLI."""
|
|
766
|
-
from .cli_backends import claude_cli
|
|
767
|
-
|
|
768
|
-
return await wait_for_agent_ready(
|
|
769
|
-
session=session,
|
|
770
|
-
cli=claude_cli,
|
|
771
|
-
timeout_seconds=timeout_seconds,
|
|
772
|
-
poll_interval=poll_interval,
|
|
773
|
-
stable_count=stable_count,
|
|
774
|
-
)
|
|
775
|
-
|
|
776
|
-
|
|
777
708
|
# =============================================================================
|
|
778
709
|
# Multi-Pane Layouts
|
|
779
710
|
# =============================================================================
|
|
@@ -983,15 +914,17 @@ async def create_multi_claude_layout(
|
|
|
983
914
|
profile_customizations=profile_customizations,
|
|
984
915
|
)
|
|
985
916
|
|
|
917
|
+
from .cli_backends import claude_cli
|
|
918
|
+
|
|
986
919
|
# Start Claude in all panes in parallel.
|
|
987
|
-
#
|
|
988
|
-
# which provides proper readiness detection, so no sleeps between starts needed.
|
|
920
|
+
# start_agent_in_session uses wait_for_shell_ready() internally, so no sleeps needed.
|
|
989
921
|
async def start_claude_for_pane(pane_name: str, project_path: str) -> None:
|
|
990
922
|
session = panes[pane_name]
|
|
991
923
|
pane_env = project_envs.get(pane_name) if project_envs else None
|
|
992
924
|
marker_id = pane_marker_ids.get(pane_name) if pane_marker_ids else None
|
|
993
|
-
await
|
|
925
|
+
await start_agent_in_session(
|
|
994
926
|
session=session,
|
|
927
|
+
cli=claude_cli,
|
|
995
928
|
project_path=project_path,
|
|
996
929
|
dangerously_skip_permissions=skip_permissions,
|
|
997
930
|
env=pane_env,
|
|
@@ -1116,4 +1049,3 @@ async def get_window_for_session(
|
|
|
1116
1049
|
return window
|
|
1117
1050
|
return None
|
|
1118
1051
|
|
|
1119
|
-
|
claude_team_mcp/registry.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Session Registry for Claude Team MCP
|
|
3
3
|
|
|
4
4
|
Tracks all spawned Claude Code sessions, maintaining the mapping between
|
|
5
|
-
our session IDs,
|
|
5
|
+
our session IDs, terminal session handles, and Claude JSONL session IDs.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import uuid
|
|
@@ -10,12 +10,14 @@ from dataclasses import dataclass, field
|
|
|
10
10
|
from datetime import datetime
|
|
11
11
|
from enum import Enum
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import
|
|
13
|
+
from typing import Literal, Optional
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
from .session_state import (
|
|
16
|
+
find_codex_session_by_internal_id,
|
|
17
|
+
get_project_dir,
|
|
18
|
+
parse_session,
|
|
19
|
+
)
|
|
20
|
+
from .terminal_backends import TerminalSession
|
|
19
21
|
|
|
20
22
|
# Type alias for supported agent types
|
|
21
23
|
AgentType = Literal["claude", "codex"]
|
|
@@ -31,16 +33,16 @@ class TerminalId:
|
|
|
31
33
|
tools to accept terminal IDs directly for recovery scenarios.
|
|
32
34
|
|
|
33
35
|
Attributes:
|
|
34
|
-
|
|
36
|
+
backend_id: Terminal backend identifier ("iterm", "tmux", "zed", etc.)
|
|
35
37
|
native_id: Terminal's native session ID (e.g., iTerm's UUID)
|
|
36
38
|
"""
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
backend_id: str
|
|
39
41
|
native_id: str
|
|
40
42
|
|
|
41
43
|
def __str__(self) -> str:
|
|
42
44
|
"""For display: 'iterm:DB29DB03-...'"""
|
|
43
|
-
return f"{self.
|
|
45
|
+
return f"{self.backend_id}:{self.native_id}"
|
|
44
46
|
|
|
45
47
|
@classmethod
|
|
46
48
|
def from_string(cls, s: str) -> "TerminalId":
|
|
@@ -50,9 +52,8 @@ class TerminalId:
|
|
|
50
52
|
Falls back to treating bare IDs as iTerm for backwards compatibility.
|
|
51
53
|
"""
|
|
52
54
|
if ":" in s:
|
|
53
|
-
|
|
54
|
-
return cls(
|
|
55
|
-
# Assume bare ID is iTerm for backwards compatibility
|
|
55
|
+
backend_id, native_id = s.split(":", 1)
|
|
56
|
+
return cls(backend_id, native_id)
|
|
56
57
|
return cls("iterm", s)
|
|
57
58
|
|
|
58
59
|
|
|
@@ -69,12 +70,12 @@ class ManagedSession:
|
|
|
69
70
|
"""
|
|
70
71
|
Represents a spawned Claude Code session.
|
|
71
72
|
|
|
72
|
-
Tracks
|
|
73
|
+
Tracks terminal session metadata, project path, and Claude session ID
|
|
73
74
|
discovered from the JSONL file.
|
|
74
75
|
"""
|
|
75
76
|
|
|
76
77
|
session_id: str # Our assigned ID (e.g., "worker-1")
|
|
77
|
-
|
|
78
|
+
terminal_session: TerminalSession
|
|
78
79
|
project_path: str
|
|
79
80
|
claude_session_id: Optional[str] = None # Discovered from JSONL
|
|
80
81
|
name: Optional[str] = None # Optional friendly name
|
|
@@ -87,16 +88,19 @@ class ManagedSession:
|
|
|
87
88
|
worktree_path: Optional[Path] = None # Path to worker's git worktree if any
|
|
88
89
|
main_repo_path: Optional[Path] = None # Path to main git repo (for worktree cleanup)
|
|
89
90
|
|
|
90
|
-
# Terminal-agnostic identifier (auto-populated from
|
|
91
|
+
# Terminal-agnostic identifier (auto-populated from terminal_session if not set)
|
|
91
92
|
terminal_id: Optional[TerminalId] = None
|
|
92
93
|
|
|
93
94
|
# Agent type: "claude" (default) or "codex"
|
|
94
95
|
agent_type: AgentType = "claude"
|
|
95
96
|
|
|
96
97
|
def __post_init__(self):
|
|
97
|
-
"""Auto-populate terminal_id from
|
|
98
|
-
if self.terminal_id is None
|
|
99
|
-
self.terminal_id = TerminalId(
|
|
98
|
+
"""Auto-populate terminal_id from terminal_session if not set."""
|
|
99
|
+
if self.terminal_id is None:
|
|
100
|
+
self.terminal_id = TerminalId(
|
|
101
|
+
self.terminal_session.backend_id,
|
|
102
|
+
self.terminal_session.native_id,
|
|
103
|
+
)
|
|
100
104
|
|
|
101
105
|
def to_dict(self) -> dict:
|
|
102
106
|
"""Convert to dictionary for MCP tool responses."""
|
|
@@ -148,15 +152,21 @@ class ManagedSession:
|
|
|
148
152
|
Get the path to this session's JSONL file.
|
|
149
153
|
|
|
150
154
|
For Claude workers: uses marker-based discovery in ~/.claude/projects/.
|
|
151
|
-
For Codex workers:
|
|
155
|
+
For Codex workers: uses marker-based discovery in ~/.codex/sessions/.
|
|
152
156
|
|
|
153
157
|
Returns:
|
|
154
158
|
Path object, or None if session cannot be discovered
|
|
155
159
|
"""
|
|
156
160
|
if self.agent_type == "codex":
|
|
157
|
-
# For Codex, search the sessions directory
|
|
158
161
|
from .idle_detection import find_codex_session_file
|
|
159
162
|
|
|
163
|
+
# Prefer marker-based match, fall back to most recent for legacy sessions.
|
|
164
|
+
match = find_codex_session_by_internal_id(
|
|
165
|
+
self.session_id,
|
|
166
|
+
max_age_seconds=600,
|
|
167
|
+
)
|
|
168
|
+
if match:
|
|
169
|
+
return match.jsonl_path
|
|
160
170
|
return find_codex_session_file(max_age_seconds=600)
|
|
161
171
|
else:
|
|
162
172
|
# For Claude, use marker-based discovery
|
|
@@ -205,8 +215,14 @@ class ManagedSession:
|
|
|
205
215
|
if self.agent_type == "codex":
|
|
206
216
|
from .idle_detection import find_codex_session_file, is_codex_idle
|
|
207
217
|
|
|
208
|
-
#
|
|
209
|
-
|
|
218
|
+
# Prefer marker-based match, fall back to most recent for legacy sessions.
|
|
219
|
+
match = find_codex_session_by_internal_id(
|
|
220
|
+
self.session_id,
|
|
221
|
+
max_age_seconds=600,
|
|
222
|
+
)
|
|
223
|
+
session_file = match.jsonl_path if match else None
|
|
224
|
+
if not session_file:
|
|
225
|
+
session_file = find_codex_session_file(max_age_seconds=600)
|
|
210
226
|
if not session_file:
|
|
211
227
|
return False
|
|
212
228
|
return is_codex_idle(session_file)
|
|
@@ -269,7 +285,7 @@ class SessionRegistry:
|
|
|
269
285
|
|
|
270
286
|
def add(
|
|
271
287
|
self,
|
|
272
|
-
|
|
288
|
+
terminal_session: TerminalSession,
|
|
273
289
|
project_path: str,
|
|
274
290
|
name: Optional[str] = None,
|
|
275
291
|
session_id: Optional[str] = None,
|
|
@@ -278,7 +294,7 @@ class SessionRegistry:
|
|
|
278
294
|
Add a new session to the registry.
|
|
279
295
|
|
|
280
296
|
Args:
|
|
281
|
-
|
|
297
|
+
terminal_session: Backend-agnostic terminal session handle
|
|
282
298
|
project_path: Directory where Claude is running
|
|
283
299
|
name: Optional friendly name
|
|
284
300
|
session_id: Optional specific ID (auto-generated if not provided)
|
|
@@ -291,7 +307,7 @@ class SessionRegistry:
|
|
|
291
307
|
|
|
292
308
|
session = ManagedSession(
|
|
293
309
|
session_id=session_id,
|
|
294
|
-
|
|
310
|
+
terminal_session=terminal_session,
|
|
295
311
|
project_path=project_path,
|
|
296
312
|
name=name,
|
|
297
313
|
)
|
|
@@ -331,7 +347,8 @@ class SessionRegistry:
|
|
|
331
347
|
|
|
332
348
|
Lookup order (most specific first):
|
|
333
349
|
1. Internal session_id (e.g., "d875b833")
|
|
334
|
-
2. Terminal
|
|
350
|
+
2. Terminal ID with backend prefix (e.g., "iterm:DB29DB03-..."),
|
|
351
|
+
or a bare iTerm ID for backwards compatibility
|
|
335
352
|
3. Session name
|
|
336
353
|
|
|
337
354
|
After MCP restart, internal IDs are lost until import. This method
|