claude-team-mcp 0.6.1__py3-none-any.whl → 0.7.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 +477 -0
- claude_team/idle_detection.py +173 -0
- claude_team/poller.py +245 -0
- claude_team_mcp/idle_detection.py +16 -3
- claude_team_mcp/iterm_utils.py +5 -73
- claude_team_mcp/registry.py +43 -26
- claude_team_mcp/server.py +95 -61
- claude_team_mcp/session_state.py +364 -2
- claude_team_mcp/terminal_backends/__init__.py +31 -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 +646 -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 +221 -142
- claude_team_mcp/tools/wait_idle_workers.py +1 -0
- claude_team_mcp/utils/errors.py +7 -3
- claude_team_mcp/worktree.py +59 -12
- {claude_team_mcp-0.6.1.dist-info → claude_team_mcp-0.7.0.dist-info}/METADATA +1 -1
- claude_team_mcp-0.7.0.dist-info/RECORD +52 -0
- claude_team_mcp-0.6.1.dist-info/RECORD +0 -43
- {claude_team_mcp-0.6.1.dist-info → claude_team_mcp-0.7.0.dist-info}/WHEEL +0 -0
- {claude_team_mcp-0.6.1.dist-info → claude_team_mcp-0.7.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""
|
|
2
|
+
iTerm2 terminal backend adapter.
|
|
3
|
+
|
|
4
|
+
Wraps iTerm2 session objects in a backend-agnostic interface.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
10
|
+
|
|
11
|
+
from .base import TerminalBackend, TerminalSession
|
|
12
|
+
from .. import iterm_utils
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from iterm2.app import App as ItermApp
|
|
16
|
+
from iterm2.connection import Connection as ItermConnection
|
|
17
|
+
from iterm2.profile import LocalWriteOnlyProfile as ItermLocalWriteOnlyProfile
|
|
18
|
+
from iterm2.session import Session as ItermSession
|
|
19
|
+
from iterm2.tab import Tab as ItermTab
|
|
20
|
+
from iterm2.window import Window as ItermWindow
|
|
21
|
+
|
|
22
|
+
from ..cli_backends import AgentCLI
|
|
23
|
+
|
|
24
|
+
# Re-export iTerm-specific layout limit via the backend layer.
|
|
25
|
+
MAX_PANES_PER_TAB = iterm_utils.MAX_PANES_PER_TAB
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ItermBackend(TerminalBackend):
|
|
29
|
+
"""Terminal backend adapter for iTerm2."""
|
|
30
|
+
|
|
31
|
+
backend_id = "iterm"
|
|
32
|
+
|
|
33
|
+
def __init__(self, connection: "ItermConnection", app: "ItermApp") -> None:
|
|
34
|
+
"""Initialize the backend with an active iTerm2 connection and app."""
|
|
35
|
+
self._connection = connection
|
|
36
|
+
self._app = app
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def connection(self) -> "ItermConnection":
|
|
40
|
+
"""Return the active iTerm2 connection."""
|
|
41
|
+
return self._connection
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def app(self) -> "ItermApp":
|
|
45
|
+
"""Return the active iTerm2 app handle."""
|
|
46
|
+
return self._app
|
|
47
|
+
|
|
48
|
+
def wrap_session(self, handle: "ItermSession") -> TerminalSession:
|
|
49
|
+
"""Wrap an iTerm2 session handle in a TerminalSession."""
|
|
50
|
+
return TerminalSession(
|
|
51
|
+
backend_id=self.backend_id,
|
|
52
|
+
native_id=handle.session_id,
|
|
53
|
+
handle=handle,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def unwrap_session(self, session: TerminalSession) -> "ItermSession":
|
|
57
|
+
"""Extract the iTerm2 session handle from a TerminalSession."""
|
|
58
|
+
return session.handle
|
|
59
|
+
|
|
60
|
+
def handle_from_session(self, session: "ItermSession") -> TerminalSession:
|
|
61
|
+
"""Wrap a native iTerm2 session in a TerminalSession (alias for wrap_session)."""
|
|
62
|
+
return self.wrap_session(session)
|
|
63
|
+
|
|
64
|
+
async def find_handle_by_native_id(self, native_id: str) -> Optional[TerminalSession]:
|
|
65
|
+
"""
|
|
66
|
+
Find a TerminalSession for a native iTerm2 session ID.
|
|
67
|
+
|
|
68
|
+
Returns None if the session cannot be found.
|
|
69
|
+
"""
|
|
70
|
+
for window in self._app.terminal_windows:
|
|
71
|
+
for tab in window.tabs:
|
|
72
|
+
for session in tab.sessions:
|
|
73
|
+
if session.session_id == native_id:
|
|
74
|
+
return self.wrap_session(session)
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def list_handles(self) -> list[TerminalSession]:
|
|
78
|
+
"""Return TerminalSessions for all iTerm2 sessions in all windows."""
|
|
79
|
+
handles: list[TerminalSession] = []
|
|
80
|
+
for window in self._app.terminal_windows:
|
|
81
|
+
for tab in window.tabs:
|
|
82
|
+
for session in tab.sessions:
|
|
83
|
+
handles.append(self.wrap_session(session))
|
|
84
|
+
return handles
|
|
85
|
+
|
|
86
|
+
async def create_session(
|
|
87
|
+
self,
|
|
88
|
+
name: str | None = None,
|
|
89
|
+
*,
|
|
90
|
+
project_path: str | None = None,
|
|
91
|
+
issue_id: str | None = None,
|
|
92
|
+
coordinator_annotation: str | None = None,
|
|
93
|
+
profile: str | None = None,
|
|
94
|
+
profile_customizations: "ItermLocalWriteOnlyProfile" | None = None,
|
|
95
|
+
) -> TerminalSession:
|
|
96
|
+
"""Create a new iTerm2 window/session and return its initial pane."""
|
|
97
|
+
window = await iterm_utils.create_window(
|
|
98
|
+
self._connection,
|
|
99
|
+
profile=profile,
|
|
100
|
+
profile_customizations=profile_customizations,
|
|
101
|
+
)
|
|
102
|
+
tab = window.current_tab
|
|
103
|
+
if tab is None or tab.current_session is None:
|
|
104
|
+
raise RuntimeError("Failed to get initial iTerm2 session from window")
|
|
105
|
+
if name:
|
|
106
|
+
try:
|
|
107
|
+
await tab.async_set_title(name)
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
return self.wrap_session(tab.current_session)
|
|
111
|
+
|
|
112
|
+
async def send_text(self, session: TerminalSession, text: str) -> None:
|
|
113
|
+
"""Send raw text to an iTerm2 session."""
|
|
114
|
+
await iterm_utils.send_text(self.unwrap_session(session), text)
|
|
115
|
+
|
|
116
|
+
async def send_key(self, session: TerminalSession, key: str) -> None:
|
|
117
|
+
"""Send a special key to an iTerm2 session."""
|
|
118
|
+
await iterm_utils.send_key(self.unwrap_session(session), key)
|
|
119
|
+
|
|
120
|
+
async def send_prompt(
|
|
121
|
+
self, session: TerminalSession, text: str, submit: bool = True
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Send a prompt to a terminal session, optionally submitting it."""
|
|
124
|
+
await iterm_utils.send_prompt(self.unwrap_session(session), text, submit=submit)
|
|
125
|
+
|
|
126
|
+
async def send_prompt_for_agent(
|
|
127
|
+
self,
|
|
128
|
+
session: TerminalSession,
|
|
129
|
+
text: str,
|
|
130
|
+
agent_type: str = "claude",
|
|
131
|
+
submit: bool = True,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Send a prompt with agent-specific handling (Claude vs Codex)."""
|
|
134
|
+
await iterm_utils.send_prompt_for_agent(
|
|
135
|
+
self.unwrap_session(session),
|
|
136
|
+
text,
|
|
137
|
+
agent_type=agent_type,
|
|
138
|
+
submit=submit,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
async def read_screen_text(self, session: TerminalSession) -> str:
|
|
142
|
+
"""Read visible screen content from an iTerm2 session."""
|
|
143
|
+
return await iterm_utils.read_screen_text(self.unwrap_session(session))
|
|
144
|
+
|
|
145
|
+
async def split_pane(
|
|
146
|
+
self,
|
|
147
|
+
session: TerminalSession,
|
|
148
|
+
*,
|
|
149
|
+
vertical: bool = True,
|
|
150
|
+
before: bool = False,
|
|
151
|
+
profile: str | None = None,
|
|
152
|
+
profile_customizations: "ItermLocalWriteOnlyProfile" | None = None,
|
|
153
|
+
) -> TerminalSession:
|
|
154
|
+
"""Split an iTerm2 session pane and return the new pane."""
|
|
155
|
+
new_session = await iterm_utils.split_pane(
|
|
156
|
+
self.unwrap_session(session),
|
|
157
|
+
vertical=vertical,
|
|
158
|
+
before=before,
|
|
159
|
+
profile=profile,
|
|
160
|
+
profile_customizations=profile_customizations,
|
|
161
|
+
)
|
|
162
|
+
return self.wrap_session(new_session)
|
|
163
|
+
|
|
164
|
+
async def close_session(self, session: TerminalSession, force: bool = False) -> None:
|
|
165
|
+
"""Close an iTerm2 session pane."""
|
|
166
|
+
await iterm_utils.close_pane(self.unwrap_session(session), force=force)
|
|
167
|
+
|
|
168
|
+
async def create_multi_pane_layout(
|
|
169
|
+
self,
|
|
170
|
+
layout: str,
|
|
171
|
+
*,
|
|
172
|
+
profile: str | None = None,
|
|
173
|
+
profile_customizations: dict[str, Any] | None = None,
|
|
174
|
+
) -> dict[str, TerminalSession]:
|
|
175
|
+
"""Create an iTerm2 multi-pane layout and wrap panes as TerminalSessions."""
|
|
176
|
+
panes = await iterm_utils.create_multi_pane_layout(
|
|
177
|
+
self._connection,
|
|
178
|
+
layout,
|
|
179
|
+
profile=profile,
|
|
180
|
+
profile_customizations=profile_customizations,
|
|
181
|
+
)
|
|
182
|
+
return {name: self.wrap_session(session) for name, session in panes.items()}
|
|
183
|
+
|
|
184
|
+
async def list_sessions(self) -> list[TerminalSession]:
|
|
185
|
+
"""List all iTerm2 sessions across all windows and tabs."""
|
|
186
|
+
return self.list_handles()
|
|
187
|
+
|
|
188
|
+
async def start_agent_in_session(
|
|
189
|
+
self,
|
|
190
|
+
handle: TerminalSession,
|
|
191
|
+
cli: "AgentCLI",
|
|
192
|
+
project_path: str,
|
|
193
|
+
dangerously_skip_permissions: bool = False,
|
|
194
|
+
env: Optional[dict[str, str]] = None,
|
|
195
|
+
shell_ready_timeout: float = 10.0,
|
|
196
|
+
agent_ready_timeout: float = 30.0,
|
|
197
|
+
stop_hook_marker_id: Optional[str] = None,
|
|
198
|
+
output_capture_path: Optional[str] = None,
|
|
199
|
+
) -> None:
|
|
200
|
+
"""Start a CLI agent in an existing terminal session."""
|
|
201
|
+
await iterm_utils.start_agent_in_session(
|
|
202
|
+
session=self.unwrap_session(handle),
|
|
203
|
+
cli=cli,
|
|
204
|
+
project_path=project_path,
|
|
205
|
+
dangerously_skip_permissions=dangerously_skip_permissions,
|
|
206
|
+
env=env,
|
|
207
|
+
shell_ready_timeout=shell_ready_timeout,
|
|
208
|
+
agent_ready_timeout=agent_ready_timeout,
|
|
209
|
+
stop_hook_marker_id=stop_hook_marker_id,
|
|
210
|
+
output_capture_path=output_capture_path,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
async def find_available_window(
|
|
214
|
+
self,
|
|
215
|
+
max_panes: int = MAX_PANES_PER_TAB,
|
|
216
|
+
managed_session_ids: Optional[set[str]] = None,
|
|
217
|
+
) -> Optional[tuple["ItermWindow", "ItermTab", TerminalSession]]:
|
|
218
|
+
"""Find a window/tab with space for more panes."""
|
|
219
|
+
result = await iterm_utils.find_available_window(
|
|
220
|
+
self._app,
|
|
221
|
+
max_panes=max_panes,
|
|
222
|
+
managed_session_ids=managed_session_ids,
|
|
223
|
+
)
|
|
224
|
+
if not result:
|
|
225
|
+
return None
|
|
226
|
+
window, tab, session = result
|
|
227
|
+
return window, tab, self.wrap_session(session)
|
|
228
|
+
|
|
229
|
+
async def get_window_for_handle(
|
|
230
|
+
self,
|
|
231
|
+
handle: TerminalSession,
|
|
232
|
+
) -> Optional["ItermWindow"]:
|
|
233
|
+
"""Return the window containing the given terminal session."""
|
|
234
|
+
return await iterm_utils.get_window_for_session(
|
|
235
|
+
self._app, self.unwrap_session(handle)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
async def activate_app(self) -> None:
|
|
239
|
+
"""Bring the terminal application to the foreground."""
|
|
240
|
+
await self._app.async_activate()
|
|
241
|
+
|
|
242
|
+
async def activate_window_for_handle(self, handle: TerminalSession) -> None:
|
|
243
|
+
"""Activate the window containing the given terminal session."""
|
|
244
|
+
native_session = self.unwrap_session(handle)
|
|
245
|
+
tab = native_session.tab
|
|
246
|
+
if tab is None:
|
|
247
|
+
return
|
|
248
|
+
window = tab.window
|
|
249
|
+
if window is None:
|
|
250
|
+
return
|
|
251
|
+
await window.async_activate()
|