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.
Files changed (35) hide show
  1. claude_team/__init__.py +11 -0
  2. claude_team/events.py +501 -0
  3. claude_team/idle_detection.py +173 -0
  4. claude_team/poller.py +245 -0
  5. claude_team_mcp/cli_backends/__init__.py +4 -2
  6. claude_team_mcp/cli_backends/claude.py +45 -5
  7. claude_team_mcp/cli_backends/codex.py +44 -3
  8. claude_team_mcp/config.py +350 -0
  9. claude_team_mcp/config_cli.py +263 -0
  10. claude_team_mcp/idle_detection.py +16 -3
  11. claude_team_mcp/issue_tracker/__init__.py +68 -3
  12. claude_team_mcp/iterm_utils.py +5 -73
  13. claude_team_mcp/registry.py +43 -26
  14. claude_team_mcp/server.py +164 -61
  15. claude_team_mcp/session_state.py +364 -2
  16. claude_team_mcp/terminal_backends/__init__.py +49 -0
  17. claude_team_mcp/terminal_backends/base.py +106 -0
  18. claude_team_mcp/terminal_backends/iterm.py +251 -0
  19. claude_team_mcp/terminal_backends/tmux.py +683 -0
  20. claude_team_mcp/tools/__init__.py +4 -2
  21. claude_team_mcp/tools/adopt_worker.py +89 -32
  22. claude_team_mcp/tools/close_workers.py +39 -10
  23. claude_team_mcp/tools/discover_workers.py +176 -32
  24. claude_team_mcp/tools/list_workers.py +29 -0
  25. claude_team_mcp/tools/message_workers.py +35 -5
  26. claude_team_mcp/tools/poll_worker_changes.py +227 -0
  27. claude_team_mcp/tools/spawn_workers.py +254 -153
  28. claude_team_mcp/tools/wait_idle_workers.py +1 -0
  29. claude_team_mcp/utils/errors.py +7 -3
  30. claude_team_mcp/worktree.py +73 -12
  31. {claude_team_mcp-0.6.1.dist-info → claude_team_mcp-0.8.0.dist-info}/METADATA +1 -1
  32. claude_team_mcp-0.8.0.dist-info/RECORD +54 -0
  33. claude_team_mcp-0.6.1.dist-info/RECORD +0 -43
  34. {claude_team_mcp-0.6.1.dist-info → claude_team_mcp-0.8.0.dist-info}/WHEEL +0 -0
  35. {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(project_path: str) -> IssueTrackerBackend | None:
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 markers are present.
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
  ]
@@ -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
- # Each start_claude_in_session call uses wait_for_shell_ready() internally
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 start_claude_in_session(
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
-
@@ -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, iTerm2 session objects, and Claude JSONL 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 TYPE_CHECKING, Literal, Optional
13
+ from typing import Literal, Optional
14
14
 
15
- if TYPE_CHECKING:
16
- from iterm2.session import Session as ItermSession
17
-
18
- from .session_state import get_project_dir, parse_session
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
- terminal_type: Terminal emulator type ("iterm", "zed", "vscode", etc.)
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
- terminal_type: str
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.terminal_type}:{self.native_id}"
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
- terminal_type, native_id = s.split(":", 1)
54
- return cls(terminal_type, native_id)
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 the iTerm2 session object, project path, and Claude session ID
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
- iterm_session: "ItermSession"
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 iterm_session if not set)
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 iterm_session if not set."""
98
- if self.terminal_id is None and self.iterm_session is not None:
99
- self.terminal_id = TerminalId("iterm", self.iterm_session.session_id)
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: searches ~/.codex/sessions/ for matching session files.
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
- # Find the session file (will be discovered from ~/.codex/sessions/)
209
- session_file = find_codex_session_file(max_age_seconds=600)
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
- iterm_session: "ItermSession",
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
- iterm_session: iTerm2 session object
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
- iterm_session=iterm_session,
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 native ID (e.g., "DB29DB03-AA52-4FBF-879A-4DA2C5F9F823")
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