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,364 @@
|
|
|
1
|
+
"""
|
|
2
|
+
iTerm2 Profile Management for Claude Team MCP
|
|
3
|
+
|
|
4
|
+
Handles creation and customization of iTerm2 profiles for managed Claude sessions.
|
|
5
|
+
Includes automatic dark/light mode detection and consistent visual styling.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from iterm2.color import Color as ItermColor
|
|
14
|
+
from iterm2.connection import Connection as ItermConnection
|
|
15
|
+
from iterm2.profile import LocalWriteOnlyProfile
|
|
16
|
+
|
|
17
|
+
from .subprocess_cache import cached_system_profiler
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("claude-team-mcp.profile")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# =============================================================================
|
|
23
|
+
# Constants
|
|
24
|
+
# =============================================================================
|
|
25
|
+
|
|
26
|
+
# Profile identifier - used to find or create our managed profile
|
|
27
|
+
PROFILE_NAME = "claude-team"
|
|
28
|
+
|
|
29
|
+
# Font configuration - Source Code Pro preferred, Menlo as fallback
|
|
30
|
+
FONT_PRIMARY = "Source Code Pro"
|
|
31
|
+
FONT_FALLBACK = "Menlo"
|
|
32
|
+
FONT_SIZE = 12
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# =============================================================================
|
|
36
|
+
# Color Schemes
|
|
37
|
+
# =============================================================================
|
|
38
|
+
|
|
39
|
+
# Light mode color scheme - optimized for readability
|
|
40
|
+
COLORS_LIGHT = {
|
|
41
|
+
"foreground": (30, 30, 30), # Near-black text
|
|
42
|
+
"background": (255, 255, 255), # White background
|
|
43
|
+
"cursor": (0, 122, 255), # Blue cursor
|
|
44
|
+
"selection": (179, 215, 255), # Light blue selection
|
|
45
|
+
"bold": (0, 0, 0), # Black for bold text
|
|
46
|
+
# ANSI colors (normal)
|
|
47
|
+
"ansi_black": (0, 0, 0),
|
|
48
|
+
"ansi_red": (194, 54, 33),
|
|
49
|
+
"ansi_green": (37, 137, 58),
|
|
50
|
+
"ansi_yellow": (173, 124, 36),
|
|
51
|
+
"ansi_blue": (66, 133, 244),
|
|
52
|
+
"ansi_magenta": (162, 73, 162),
|
|
53
|
+
"ansi_cyan": (23, 162, 184),
|
|
54
|
+
"ansi_white": (255, 255, 255),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Dark mode color scheme - optimized for low-light environments
|
|
58
|
+
COLORS_DARK = {
|
|
59
|
+
"foreground": (229, 229, 229), # Light gray text
|
|
60
|
+
"background": (30, 30, 30), # Near-black background
|
|
61
|
+
"cursor": (66, 133, 244), # Blue cursor
|
|
62
|
+
"selection": (62, 68, 81), # Dark gray selection
|
|
63
|
+
"bold": (255, 255, 255), # White for bold text
|
|
64
|
+
# ANSI colors (normal)
|
|
65
|
+
"ansi_black": (0, 0, 0),
|
|
66
|
+
"ansi_red": (255, 85, 85),
|
|
67
|
+
"ansi_green": (80, 200, 120),
|
|
68
|
+
"ansi_yellow": (255, 204, 0),
|
|
69
|
+
"ansi_blue": (100, 149, 237),
|
|
70
|
+
"ansi_magenta": (218, 112, 214),
|
|
71
|
+
"ansi_cyan": (0, 206, 209),
|
|
72
|
+
"ansi_white": (255, 255, 255),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# =============================================================================
|
|
77
|
+
# Screen Dimension Calculation
|
|
78
|
+
# =============================================================================
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def calculate_screen_dimensions() -> tuple[int, int]:
|
|
82
|
+
"""
|
|
83
|
+
Calculate terminal columns/rows to fill the screen.
|
|
84
|
+
|
|
85
|
+
Uses system_profiler to get screen resolution and calculates appropriate
|
|
86
|
+
terminal dimensions based on Menlo 12pt font cell size.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Tuple of (columns, rows) for a screen-filling terminal window
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
# Use cached system_profiler to avoid repeated slow calls
|
|
93
|
+
stdout = cached_system_profiler("SPDisplaysDataType")
|
|
94
|
+
if stdout is None:
|
|
95
|
+
logger.warning("system_profiler failed, using default dimensions")
|
|
96
|
+
return (200, 60)
|
|
97
|
+
|
|
98
|
+
# Parse resolution from output like "Resolution: 3024 x 1964"
|
|
99
|
+
match = re.search(r"Resolution: (\d+) x (\d+)", stdout)
|
|
100
|
+
if not match:
|
|
101
|
+
logger.warning("Could not parse screen resolution, using defaults")
|
|
102
|
+
return (200, 60)
|
|
103
|
+
|
|
104
|
+
screen_w, screen_h = int(match.group(1)), int(match.group(2))
|
|
105
|
+
|
|
106
|
+
# Detect Retina display (2x scale factor)
|
|
107
|
+
scale = 2 if "Retina" in stdout else 1
|
|
108
|
+
logical_w = screen_w // scale
|
|
109
|
+
logical_h = screen_h // scale
|
|
110
|
+
|
|
111
|
+
# Subtract margins for window chrome:
|
|
112
|
+
# - ~20px for window borders
|
|
113
|
+
# - ~100px for menu bar + dock + title bar
|
|
114
|
+
usable_w = logical_w - 20
|
|
115
|
+
usable_h = logical_h - 100
|
|
116
|
+
|
|
117
|
+
# Menlo 12pt cell size (approximately)
|
|
118
|
+
cell_w, cell_h = 7.2, 14.0
|
|
119
|
+
|
|
120
|
+
cols = int(usable_w / cell_w)
|
|
121
|
+
rows = int(usable_h / cell_h)
|
|
122
|
+
|
|
123
|
+
logger.debug(
|
|
124
|
+
f"Screen {screen_w}x{screen_h} (scale {scale}) -> "
|
|
125
|
+
f"terminal {cols}x{rows}"
|
|
126
|
+
)
|
|
127
|
+
return (cols, rows)
|
|
128
|
+
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.warning(f"Failed to calculate screen dimensions: {e}")
|
|
131
|
+
return (200, 60)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# =============================================================================
|
|
135
|
+
# Appearance Mode Detection
|
|
136
|
+
# =============================================================================
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def detect_appearance_mode(connection: "ItermConnection") -> str:
|
|
140
|
+
"""
|
|
141
|
+
Detect the current macOS appearance mode (light or dark).
|
|
142
|
+
|
|
143
|
+
Uses iTerm2's effective theme to determine the system appearance.
|
|
144
|
+
Falls back to 'dark' if detection fails.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
connection: Active iTerm2 connection
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
'light' or 'dark' based on system appearance
|
|
151
|
+
"""
|
|
152
|
+
try:
|
|
153
|
+
from iterm2.app import async_get_app
|
|
154
|
+
|
|
155
|
+
# Get the app object to query effective theme
|
|
156
|
+
app = await async_get_app(connection)
|
|
157
|
+
if app is None:
|
|
158
|
+
logger.warning("Could not get iTerm2 app, defaulting to dark")
|
|
159
|
+
return "dark"
|
|
160
|
+
|
|
161
|
+
# iTerm2's effective_theme returns a list of theme components
|
|
162
|
+
# Common values include 'dark', 'light', 'automatic'
|
|
163
|
+
theme = await app.async_get_variable("effectiveTheme")
|
|
164
|
+
|
|
165
|
+
if theme and isinstance(theme, str):
|
|
166
|
+
# effectiveTheme is a string like "dark" or "light"
|
|
167
|
+
theme_lower = theme.lower()
|
|
168
|
+
if "light" in theme_lower:
|
|
169
|
+
return "light"
|
|
170
|
+
elif "dark" in theme_lower:
|
|
171
|
+
return "dark"
|
|
172
|
+
|
|
173
|
+
# If theme is a list (some iTerm2 versions), check for dark indicators
|
|
174
|
+
if theme and isinstance(theme, list):
|
|
175
|
+
for component in theme:
|
|
176
|
+
if isinstance(component, str) and "dark" in component.lower():
|
|
177
|
+
return "dark"
|
|
178
|
+
return "light"
|
|
179
|
+
|
|
180
|
+
logger.warning(f"Could not parse theme '{theme}', defaulting to dark")
|
|
181
|
+
return "dark"
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.warning(f"Failed to detect appearance mode: {e}, defaulting to dark")
|
|
185
|
+
return "dark"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def get_colors_for_mode(mode: str) -> dict:
|
|
189
|
+
"""
|
|
190
|
+
Get the color scheme dictionary for the specified appearance mode.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
mode: Either 'light' or 'dark'
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Dictionary of color names to RGB tuples
|
|
197
|
+
"""
|
|
198
|
+
if mode == "light":
|
|
199
|
+
return COLORS_LIGHT.copy()
|
|
200
|
+
return COLORS_DARK.copy()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# =============================================================================
|
|
204
|
+
# Profile Management
|
|
205
|
+
# =============================================================================
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
async def get_or_create_profile(connection: "ItermConnection") -> str:
|
|
209
|
+
"""
|
|
210
|
+
Get or create the claude-team iTerm2 profile.
|
|
211
|
+
|
|
212
|
+
Checks if a profile named 'claude-team' exists. If not, creates it
|
|
213
|
+
with sensible defaults including font configuration and color scheme
|
|
214
|
+
based on the current system appearance mode.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
connection: Active iTerm2 connection
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
The profile name (PROFILE_NAME constant)
|
|
221
|
+
|
|
222
|
+
Note:
|
|
223
|
+
This function creates a partial profile. The caller should
|
|
224
|
+
use create_session_customizations() to apply per-session
|
|
225
|
+
customizations like tab color and title.
|
|
226
|
+
"""
|
|
227
|
+
from iterm2.profile import LocalWriteOnlyProfile as LWOProfile
|
|
228
|
+
from iterm2.profile import PartialProfile
|
|
229
|
+
|
|
230
|
+
# Get all existing profiles
|
|
231
|
+
all_profiles = await PartialProfile.async_query(connection)
|
|
232
|
+
profile_names = [p.name for p in all_profiles if p.name]
|
|
233
|
+
|
|
234
|
+
# Check if our profile already exists
|
|
235
|
+
if PROFILE_NAME in profile_names:
|
|
236
|
+
logger.debug(f"Profile '{PROFILE_NAME}' already exists")
|
|
237
|
+
return PROFILE_NAME
|
|
238
|
+
|
|
239
|
+
logger.info(f"Creating new profile '{PROFILE_NAME}'")
|
|
240
|
+
|
|
241
|
+
# Find a suitable source profile (prefer Default, then first available)
|
|
242
|
+
source_profile = None
|
|
243
|
+
for profile in all_profiles:
|
|
244
|
+
if profile.name == "Default":
|
|
245
|
+
source_profile = profile
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
if not source_profile and all_profiles:
|
|
249
|
+
source_profile = all_profiles[0]
|
|
250
|
+
|
|
251
|
+
if not source_profile:
|
|
252
|
+
raise RuntimeError("No profiles found to use as template")
|
|
253
|
+
|
|
254
|
+
# Create a new profile with our name
|
|
255
|
+
# iTerm2 doesn't have a direct "create profile" API, so we use
|
|
256
|
+
# LocalWriteOnlyProfile to define settings and create a session with it
|
|
257
|
+
|
|
258
|
+
# Detect appearance mode for initial colors
|
|
259
|
+
mode = await detect_appearance_mode(connection)
|
|
260
|
+
colors = get_colors_for_mode(mode)
|
|
261
|
+
|
|
262
|
+
# Create the profile settings
|
|
263
|
+
profile = LWOProfile()
|
|
264
|
+
profile.set_name(PROFILE_NAME)
|
|
265
|
+
|
|
266
|
+
# Font configuration - use async_set methods for font
|
|
267
|
+
# Try Source Code Pro first, fall back to Menlo
|
|
268
|
+
try:
|
|
269
|
+
profile.set_normal_font(f"{FONT_PRIMARY} {FONT_SIZE}")
|
|
270
|
+
except Exception:
|
|
271
|
+
logger.warning(f"Font '{FONT_PRIMARY}' not available, using '{FONT_FALLBACK}'")
|
|
272
|
+
profile.set_normal_font(f"{FONT_FALLBACK} {FONT_SIZE}")
|
|
273
|
+
|
|
274
|
+
# Apply color scheme
|
|
275
|
+
_apply_colors_to_profile(profile, colors)
|
|
276
|
+
|
|
277
|
+
# Window settings - use tabs, not fullscreen
|
|
278
|
+
profile.set_use_tab_color(True)
|
|
279
|
+
profile.set_smart_cursor_color(True)
|
|
280
|
+
|
|
281
|
+
# The profile will be created implicitly when a session uses it.
|
|
282
|
+
# For now, we need to ensure it exists by using create_profile_with_api
|
|
283
|
+
# or by having a session use it.
|
|
284
|
+
|
|
285
|
+
# Note: iTerm2's Python API doesn't have a direct "create profile from scratch" method.
|
|
286
|
+
# The profile will be created when first used. For persistence, users should
|
|
287
|
+
# save the profile via iTerm2's UI or use the JSON profile import feature.
|
|
288
|
+
|
|
289
|
+
logger.info(
|
|
290
|
+
f"Profile '{PROFILE_NAME}' configured with {FONT_PRIMARY} {FONT_SIZE}pt, "
|
|
291
|
+
f"{mode} mode colors"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
return PROFILE_NAME
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
async def apply_appearance_colors(
|
|
298
|
+
profile: "LocalWriteOnlyProfile",
|
|
299
|
+
connection: "ItermConnection",
|
|
300
|
+
) -> None:
|
|
301
|
+
"""
|
|
302
|
+
Apply current appearance mode colors to a session profile.
|
|
303
|
+
|
|
304
|
+
Detects the current macOS light/dark mode and applies the appropriate
|
|
305
|
+
color scheme to the given profile. Call this when creating per-session
|
|
306
|
+
customizations to ensure workers match the current system appearance.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
profile: The LocalWriteOnlyProfile to modify
|
|
310
|
+
connection: Active iTerm2 connection (needed for appearance detection)
|
|
311
|
+
"""
|
|
312
|
+
mode = await detect_appearance_mode(connection)
|
|
313
|
+
colors = get_colors_for_mode(mode)
|
|
314
|
+
_apply_colors_to_profile(profile, colors)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _apply_colors_to_profile(
|
|
318
|
+
profile: "LocalWriteOnlyProfile",
|
|
319
|
+
colors: dict,
|
|
320
|
+
) -> None:
|
|
321
|
+
"""
|
|
322
|
+
Apply a color scheme to a profile.
|
|
323
|
+
|
|
324
|
+
Helper function that sets all color-related profile properties
|
|
325
|
+
from a color scheme dictionary.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
profile: The profile to modify
|
|
329
|
+
colors: Dictionary of color names to RGB tuples
|
|
330
|
+
"""
|
|
331
|
+
from iterm2.color import Color
|
|
332
|
+
|
|
333
|
+
def rgb_to_color(rgb: tuple[int, int, int]) -> "ItermColor":
|
|
334
|
+
return Color(rgb[0], rgb[1], rgb[2])
|
|
335
|
+
|
|
336
|
+
# Basic colors
|
|
337
|
+
if "foreground" in colors:
|
|
338
|
+
profile.set_foreground_color(rgb_to_color(colors["foreground"]))
|
|
339
|
+
if "background" in colors:
|
|
340
|
+
profile.set_background_color(rgb_to_color(colors["background"]))
|
|
341
|
+
if "cursor" in colors:
|
|
342
|
+
profile.set_cursor_color(rgb_to_color(colors["cursor"]))
|
|
343
|
+
if "selection" in colors:
|
|
344
|
+
profile.set_selection_color(rgb_to_color(colors["selection"]))
|
|
345
|
+
if "bold" in colors:
|
|
346
|
+
profile.set_bold_color(rgb_to_color(colors["bold"]))
|
|
347
|
+
|
|
348
|
+
# ANSI colors
|
|
349
|
+
ansi_color_setters = [
|
|
350
|
+
("ansi_black", profile.set_ansi_0_color),
|
|
351
|
+
("ansi_red", profile.set_ansi_1_color),
|
|
352
|
+
("ansi_green", profile.set_ansi_2_color),
|
|
353
|
+
("ansi_yellow", profile.set_ansi_3_color),
|
|
354
|
+
("ansi_blue", profile.set_ansi_4_color),
|
|
355
|
+
("ansi_magenta", profile.set_ansi_5_color),
|
|
356
|
+
("ansi_cyan", profile.set_ansi_6_color),
|
|
357
|
+
("ansi_white", profile.set_ansi_7_color),
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
for color_name, setter in ansi_color_setters:
|
|
361
|
+
if color_name in colors:
|
|
362
|
+
setter(rgb_to_color(colors[color_name]))
|
|
363
|
+
|
|
364
|
+
|