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.
Files changed (42) hide show
  1. claude_team_mcp/__init__.py +24 -0
  2. claude_team_mcp/__main__.py +8 -0
  3. claude_team_mcp/cli_backends/__init__.py +44 -0
  4. claude_team_mcp/cli_backends/base.py +132 -0
  5. claude_team_mcp/cli_backends/claude.py +110 -0
  6. claude_team_mcp/cli_backends/codex.py +110 -0
  7. claude_team_mcp/colors.py +108 -0
  8. claude_team_mcp/formatting.py +120 -0
  9. claude_team_mcp/idle_detection.py +488 -0
  10. claude_team_mcp/iterm_utils.py +1119 -0
  11. claude_team_mcp/names.py +427 -0
  12. claude_team_mcp/profile.py +364 -0
  13. claude_team_mcp/registry.py +426 -0
  14. claude_team_mcp/schemas/__init__.py +5 -0
  15. claude_team_mcp/schemas/codex.py +267 -0
  16. claude_team_mcp/server.py +390 -0
  17. claude_team_mcp/session_state.py +1058 -0
  18. claude_team_mcp/subprocess_cache.py +119 -0
  19. claude_team_mcp/tools/__init__.py +52 -0
  20. claude_team_mcp/tools/adopt_worker.py +122 -0
  21. claude_team_mcp/tools/annotate_worker.py +57 -0
  22. claude_team_mcp/tools/bd_help.py +42 -0
  23. claude_team_mcp/tools/check_idle_workers.py +98 -0
  24. claude_team_mcp/tools/close_workers.py +194 -0
  25. claude_team_mcp/tools/discover_workers.py +129 -0
  26. claude_team_mcp/tools/examine_worker.py +56 -0
  27. claude_team_mcp/tools/list_workers.py +76 -0
  28. claude_team_mcp/tools/list_worktrees.py +106 -0
  29. claude_team_mcp/tools/message_workers.py +311 -0
  30. claude_team_mcp/tools/read_worker_logs.py +158 -0
  31. claude_team_mcp/tools/spawn_workers.py +634 -0
  32. claude_team_mcp/tools/wait_idle_workers.py +148 -0
  33. claude_team_mcp/utils/__init__.py +17 -0
  34. claude_team_mcp/utils/constants.py +87 -0
  35. claude_team_mcp/utils/errors.py +87 -0
  36. claude_team_mcp/utils/worktree_detection.py +79 -0
  37. claude_team_mcp/worker_prompt.py +350 -0
  38. claude_team_mcp/worktree.py +532 -0
  39. claude_team_mcp-0.4.0.dist-info/METADATA +414 -0
  40. claude_team_mcp-0.4.0.dist-info/RECORD +42 -0
  41. claude_team_mcp-0.4.0.dist-info/WHEEL +4 -0
  42. 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
+