claude-team-mcp 0.7.0__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.
@@ -0,0 +1,263 @@
1
+ """
2
+ CLI helpers for claude-team configuration commands.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import os
9
+ from dataclasses import asdict
10
+ from pathlib import Path
11
+ from collections.abc import Callable, Mapping
12
+
13
+ from . import config as config_module
14
+ from .config import ClaudeTeamConfig, ConfigError, parse_config
15
+
16
+ _ALLOWED_AGENT_TYPES = {"claude", "codex"}
17
+ _ALLOWED_LAYOUTS = {"auto", "new"}
18
+ _ALLOWED_TERMINAL_BACKENDS = {"iterm", "tmux"}
19
+ _ALLOWED_ISSUE_TRACKERS = {"beads", "pebbles"}
20
+
21
+
22
+ def init_config(
23
+ *,
24
+ force: bool = False,
25
+ config_path: Path | None = None,
26
+ ) -> Path:
27
+ """Write the default config file to disk and return the path."""
28
+
29
+ path = _resolve_config_path(config_path)
30
+ if path.exists() and not force:
31
+ raise ConfigError(f"Config file already exists: {path}")
32
+ config = config_module.default_config()
33
+ return config_module.save_config(config, path)
34
+
35
+
36
+ def load_effective_config_data(
37
+ *,
38
+ env: Mapping[str, str] | None = None,
39
+ config_path: Path | None = None,
40
+ ) -> dict:
41
+ """Load config data with environment overrides applied."""
42
+
43
+ config = config_module.load_config(config_path)
44
+ data = asdict(config)
45
+ _apply_env_overrides(data, env or os.environ)
46
+ return data
47
+
48
+
49
+ def render_config_json(
50
+ *,
51
+ env: Mapping[str, str] | None = None,
52
+ config_path: Path | None = None,
53
+ ) -> str:
54
+ """Render the effective config as formatted JSON."""
55
+
56
+ data = load_effective_config_data(env=env, config_path=config_path)
57
+ return json.dumps(data, indent=2, sort_keys=True)
58
+
59
+
60
+ def get_config_value(
61
+ key: str,
62
+ *,
63
+ env: Mapping[str, str] | None = None,
64
+ config_path: Path | None = None,
65
+ ) -> object:
66
+ """Return a single config value by dotted path."""
67
+
68
+ data = load_effective_config_data(env=env, config_path=config_path)
69
+ return _get_nested_value(data, key)
70
+
71
+
72
+ def set_config_value(
73
+ key: str,
74
+ raw_value: str,
75
+ *,
76
+ config_path: Path | None = None,
77
+ ) -> ClaudeTeamConfig:
78
+ """Set a config value by dotted path, validate, and persist."""
79
+
80
+ config = config_module.load_config(config_path)
81
+ data = asdict(config)
82
+ parsed_value = _parse_cli_value(key, raw_value)
83
+ _set_nested_value(data, key, parsed_value)
84
+ updated = parse_config(data)
85
+ config_module.save_config(updated, _resolve_config_path(config_path))
86
+ return updated
87
+
88
+
89
+ def format_value_json(value: object) -> str:
90
+ """Format a single config value as JSON."""
91
+
92
+ return json.dumps(value)
93
+
94
+
95
+ def _resolve_config_path(config_path: Path | None) -> Path:
96
+ # Resolve the config path, defaulting to ~/.claude-team/config.json.
97
+ return (config_path or config_module.CONFIG_PATH).expanduser()
98
+
99
+
100
+ def _apply_env_overrides(data: dict, env: Mapping[str, str]) -> None:
101
+ # Apply env overrides using the same precedence logic as runtime helpers.
102
+ command_override = env.get("CLAUDE_TEAM_COMMAND")
103
+ if command_override:
104
+ data["commands"]["claude"] = command_override
105
+
106
+ codex_override = env.get("CLAUDE_TEAM_CODEX_COMMAND")
107
+ if codex_override:
108
+ data["commands"]["codex"] = codex_override
109
+
110
+ # Terminal backend is a direct override (mirrors select_backend_id).
111
+ backend_override = env.get("CLAUDE_TEAM_TERMINAL_BACKEND")
112
+ if backend_override:
113
+ data["terminal"]["backend"] = backend_override.strip().lower()
114
+
115
+ # Issue tracker override mirrors detect_issue_tracker validation.
116
+ tracker_override = env.get("CLAUDE_TEAM_ISSUE_TRACKER")
117
+ if tracker_override:
118
+ normalized = tracker_override.strip().lower()
119
+ if normalized in _ALLOWED_ISSUE_TRACKERS:
120
+ data["issue_tracker"]["override"] = normalized
121
+
122
+ # Events overrides use integer parsing with graceful fallback.
123
+ max_size_override = env.get("CLAUDE_TEAM_EVENTS_MAX_SIZE_MB")
124
+ if max_size_override:
125
+ parsed = _parse_int_override(max_size_override)
126
+ if parsed is not None:
127
+ data["events"]["max_size_mb"] = parsed
128
+
129
+ recent_hours_override = env.get("CLAUDE_TEAM_EVENTS_RECENT_HOURS")
130
+ if recent_hours_override:
131
+ parsed = _parse_int_override(recent_hours_override)
132
+ if parsed is not None:
133
+ data["events"]["recent_hours"] = parsed
134
+
135
+
136
+ def _parse_int_override(raw_value: str) -> int | None:
137
+ # Parse env overrides as integers; invalid values are ignored.
138
+ try:
139
+ return int(raw_value)
140
+ except ValueError:
141
+ return None
142
+
143
+
144
+ def _parse_cli_value(key: str, raw_value: str) -> object:
145
+ # Parse CLI values according to the config schema.
146
+ parser = _FIELD_PARSERS.get(key)
147
+ if parser is None:
148
+ raise ConfigError(f"Unknown config key: {key}")
149
+ return parser(raw_value, key)
150
+
151
+
152
+ def _parse_optional_string(raw_value: str, field: str) -> str | None:
153
+ # Parse optional string fields (allows null).
154
+ if _is_null(raw_value):
155
+ return None
156
+ if not raw_value.strip():
157
+ raise ConfigError(f"{field} cannot be empty")
158
+ return raw_value
159
+
160
+
161
+ def _parse_bool(raw_value: str, field: str) -> bool:
162
+ # Parse boolean values in JSON-compatible form.
163
+ normalized = raw_value.strip().lower()
164
+ if normalized == "true":
165
+ return True
166
+ if normalized == "false":
167
+ return False
168
+ raise ConfigError(f"{field} must be a boolean")
169
+
170
+
171
+ def _parse_int(raw_value: str, field: str) -> int:
172
+ # Parse integer values with minimum validation.
173
+ try:
174
+ value = int(raw_value.strip())
175
+ except ValueError as exc:
176
+ raise ConfigError(f"{field} must be an integer") from exc
177
+ if value < 1:
178
+ raise ConfigError(f"{field} must be at least 1")
179
+ return value
180
+
181
+
182
+ def _parse_literal(raw_value: str, field: str, allowed: set[str]) -> str:
183
+ # Parse literal string values constrained to allowed sets.
184
+ value = raw_value.strip()
185
+ if value not in allowed:
186
+ joined = ", ".join(sorted(allowed))
187
+ raise ConfigError(f"{field} must be one of: {joined}")
188
+ return value
189
+
190
+
191
+ def _parse_optional_literal(
192
+ raw_value: str,
193
+ field: str,
194
+ allowed: set[str],
195
+ ) -> str | None:
196
+ # Parse nullable literal values constrained to allowed sets.
197
+ if _is_null(raw_value):
198
+ return None
199
+ return _parse_literal(raw_value, field, allowed)
200
+
201
+
202
+ def _is_null(raw_value: str) -> bool:
203
+ # Treat "null" as a request to clear optional fields.
204
+ return raw_value.strip().lower() == "null"
205
+
206
+
207
+ def _get_nested_value(data: dict, key: str) -> object:
208
+ # Retrieve values by dotted path, validating against known keys.
209
+ if key not in _GET_KEYS:
210
+ raise ConfigError(f"Unknown config key: {key}")
211
+ parts = key.split(".")
212
+ current: object = data
213
+ for part in parts:
214
+ if not isinstance(current, dict) or part not in current:
215
+ raise ConfigError(f"Unknown config key: {key}")
216
+ current = current[part]
217
+ return current
218
+
219
+
220
+ def _set_nested_value(data: dict, key: str, value: object) -> None:
221
+ # Assign values by dotted path, ensuring the key exists.
222
+ parts = key.split(".")
223
+ current: dict = data
224
+ for part in parts[:-1]:
225
+ if part not in current or not isinstance(current[part], dict):
226
+ raise ConfigError(f"Unknown config key: {key}")
227
+ current = current[part]
228
+ leaf = parts[-1]
229
+ if leaf not in current:
230
+ raise ConfigError(f"Unknown config key: {key}")
231
+ current[leaf] = value
232
+
233
+
234
+ _FIELD_PARSERS: dict[str, Callable[[str, str], object]] = {
235
+ "commands.claude": _parse_optional_string,
236
+ "commands.codex": _parse_optional_string,
237
+ "defaults.agent_type": lambda value, field: _parse_literal(
238
+ value,
239
+ field,
240
+ _ALLOWED_AGENT_TYPES,
241
+ ),
242
+ "defaults.skip_permissions": _parse_bool,
243
+ "defaults.use_worktree": _parse_bool,
244
+ "defaults.layout": lambda value, field: _parse_literal(
245
+ value,
246
+ field,
247
+ _ALLOWED_LAYOUTS,
248
+ ),
249
+ "terminal.backend": lambda value, field: _parse_optional_literal(
250
+ value,
251
+ field,
252
+ _ALLOWED_TERMINAL_BACKENDS,
253
+ ),
254
+ "events.max_size_mb": _parse_int,
255
+ "events.recent_hours": _parse_int,
256
+ "issue_tracker.override": lambda value, field: _parse_optional_literal(
257
+ value,
258
+ field,
259
+ _ALLOWED_ISSUE_TRACKERS,
260
+ ),
261
+ }
262
+
263
+ _GET_KEYS = set(_FIELD_PARSERS.keys()) | {"version"}
@@ -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
  ]
claude_team_mcp/server.py CHANGED
@@ -398,8 +398,10 @@ def run_server(transport: str = "stdio", port: int = 8766):
398
398
  def main():
399
399
  """CLI entry point with argument parsing."""
400
400
  import argparse
401
+ import sys
401
402
 
402
403
  parser = argparse.ArgumentParser(description="Claude Team MCP Server")
404
+ # Global server options apply when no subcommand is provided.
403
405
  parser.add_argument(
404
406
  "--http",
405
407
  action="store_true",
@@ -411,9 +413,76 @@ def main():
411
413
  default=8766,
412
414
  help="Port for HTTP mode (default: 8766)",
413
415
  )
416
+ # Config subcommands for reading/writing ~/.claude-team/config.json.
417
+ subparsers = parser.add_subparsers(dest="command")
418
+
419
+ config_parser = subparsers.add_parser(
420
+ "config",
421
+ help="Manage claude-team configuration",
422
+ )
423
+ config_subparsers = config_parser.add_subparsers(dest="config_command")
424
+
425
+ init_parser = config_subparsers.add_parser(
426
+ "init",
427
+ help="Write default config to disk",
428
+ )
429
+ init_parser.add_argument(
430
+ "--force",
431
+ action="store_true",
432
+ help="Overwrite existing config file",
433
+ )
434
+
435
+ config_subparsers.add_parser(
436
+ "show",
437
+ help="Show effective config (file + env overrides)",
438
+ )
439
+
440
+ get_parser = config_subparsers.add_parser(
441
+ "get",
442
+ help="Get a single config value by dotted path",
443
+ )
444
+ get_parser.add_argument("key", help="Dotted config key (e.g. defaults.layout)")
445
+
446
+ set_parser = config_subparsers.add_parser(
447
+ "set",
448
+ help="Set a single config value by dotted path",
449
+ )
450
+ set_parser.add_argument("key", help="Dotted config key (e.g. defaults.layout)")
451
+ set_parser.add_argument("value", help="Value to set")
414
452
 
415
453
  args = parser.parse_args()
416
454
 
455
+ # Handle config subcommands early to avoid starting the server.
456
+ if args.command == "config":
457
+ from .config import ConfigError
458
+ from .config_cli import (
459
+ format_value_json,
460
+ get_config_value,
461
+ init_config,
462
+ render_config_json,
463
+ set_config_value,
464
+ )
465
+
466
+ try:
467
+ if args.config_command == "init":
468
+ path = init_config(force=args.force)
469
+ print(path)
470
+ elif args.config_command == "show":
471
+ print(render_config_json())
472
+ elif args.config_command == "get":
473
+ value = get_config_value(args.key)
474
+ print(format_value_json(value))
475
+ elif args.config_command == "set":
476
+ set_config_value(args.key, args.value)
477
+ else:
478
+ config_parser.print_help()
479
+ raise SystemExit(2)
480
+ except ConfigError as exc:
481
+ print(f"Error: {exc}", file=sys.stderr)
482
+ raise SystemExit(1) from exc
483
+ return
484
+
485
+ # Default behavior: run the MCP server.
417
486
  if args.http:
418
487
  run_server(transport="streamable-http", port=args.port)
419
488
  else:
@@ -2,18 +2,36 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
5
6
  import os
6
7
  from typing import Mapping
7
8
 
9
+ from ..config import ClaudeTeamConfig, ConfigError, load_config
8
10
  from .base import TerminalBackend, TerminalSession
9
11
  from .iterm import ItermBackend, MAX_PANES_PER_TAB
10
12
  from .tmux import TmuxBackend
11
13
 
14
+ logger = logging.getLogger("claude-team-mcp")
12
15
 
13
- def select_backend_id(env: Mapping[str, str] | None = None) -> str:
14
- """Select a terminal backend id based on environment configuration."""
15
- environ = env or os.environ
16
+
17
+ def select_backend_id(
18
+ env: Mapping[str, str] | None = None,
19
+ config: ClaudeTeamConfig | None = None,
20
+ ) -> str:
21
+ """Select a terminal backend id based on environment and config."""
22
+ environ = os.environ if env is None else env
16
23
  configured = environ.get("CLAUDE_TEAM_TERMINAL_BACKEND")
24
+ if configured:
25
+ return configured.strip().lower()
26
+ if config is None:
27
+ try:
28
+ config = load_config()
29
+ except ConfigError as exc:
30
+ logger.warning(
31
+ "Invalid config file; ignoring terminal backend override: %s", exc
32
+ )
33
+ config = None
34
+ configured = config.terminal.backend if config else None
17
35
  if configured:
18
36
  return configured.strip().lower()
19
37
  if environ.get("TMUX"):
@@ -7,6 +7,7 @@ Provides a TerminalBackend implementation backed by tmux CLI commands.
7
7
  from __future__ import annotations
8
8
 
9
9
  import asyncio
10
+ import hashlib
10
11
  import re
11
12
  import subprocess
12
13
  import uuid
@@ -44,7 +45,11 @@ ISSUE_ID_PATTERN = re.compile(r"\b[A-Za-z][A-Za-z0-9]*-[A-Za-z0-9]*\d[A-Za-z0-9]
44
45
 
45
46
  SHELL_READY_MARKER = "CLAUDE_TEAM_READY_7f3a9c"
46
47
  CODEX_PRE_ENTER_DELAY = 0.5
47
- TMUX_SESSION_NAME = "claude-team"
48
+ TMUX_SESSION_PREFIX = "claude-team"
49
+ TMUX_SESSION_HASH_LEN = 8
50
+ TMUX_SESSION_SLUG_MAX = 32
51
+ TMUX_SESSION_FALLBACK = "project"
52
+ TMUX_SESSION_PREFIXED = f"{TMUX_SESSION_PREFIX}-"
48
53
 
49
54
  LAYOUT_PANE_NAMES = {
50
55
  "single": ["main"],
@@ -62,6 +67,47 @@ LAYOUT_SELECT = {
62
67
  }
63
68
 
64
69
 
70
+ # Normalize a project name into a tmux-safe slug.
71
+ def _tmux_safe_slug(value: str) -> str:
72
+ slug = re.sub(r"[^A-Za-z0-9_-]+", "-", value.strip())
73
+ slug = slug.strip("-_")
74
+ if not slug:
75
+ return TMUX_SESSION_FALLBACK
76
+ if len(slug) > TMUX_SESSION_SLUG_MAX:
77
+ slug = slug[:TMUX_SESSION_SLUG_MAX].rstrip("-_")
78
+ return slug or TMUX_SESSION_FALLBACK
79
+
80
+
81
+ def project_name_from_path(project_path: str | None) -> str | None:
82
+ """Return a display name for a project path, handling worktree paths."""
83
+ if not project_path:
84
+ return None
85
+ path = Path(project_path)
86
+ parts = path.parts
87
+ if ".worktrees" in parts:
88
+ worktrees_index = parts.index(".worktrees")
89
+ if worktrees_index > 0:
90
+ return parts[worktrees_index - 1]
91
+ return path.name
92
+
93
+
94
+ def tmux_session_name_for_project(project_path: str | None) -> str:
95
+ """Return the per-project tmux session name for a given project path."""
96
+ project_name = project_name_from_path(project_path) or TMUX_SESSION_FALLBACK
97
+ slug = _tmux_safe_slug(project_name)
98
+ if project_path:
99
+ digest_source = project_path
100
+ else:
101
+ digest_source = uuid.uuid4().hex
102
+ digest = hashlib.sha1(digest_source.encode("utf-8")).hexdigest()[:TMUX_SESSION_HASH_LEN]
103
+ return f"{TMUX_SESSION_PREFIXED}{slug}-{digest}"
104
+
105
+
106
+ # Determine whether a tmux session is managed by claude-team.
107
+ def _is_managed_session_name(session_name: str) -> bool:
108
+ return session_name.startswith(TMUX_SESSION_PREFIXED)
109
+
110
+
65
111
  class TmuxBackend(TerminalBackend):
66
112
  """Terminal backend adapter for tmux."""
67
113
 
@@ -94,23 +140,24 @@ class TmuxBackend(TerminalBackend):
94
140
  profile: str | None = None,
95
141
  profile_customizations: Any | None = None,
96
142
  ) -> TerminalSession:
97
- """Create a worker window in the claude-team tmux session."""
143
+ """Create a worker window in a per-project tmux session."""
98
144
  if profile or profile_customizations:
99
145
  raise ValueError("tmux backend does not support profiles")
100
146
 
101
147
  base_name = name or self._generate_window_name()
102
- project_name = self._project_name_from_path(project_path)
148
+ project_name = project_name_from_path(project_path)
103
149
  resolved_issue_id = self._resolve_issue_id(issue_id, coordinator_annotation)
104
150
  window_name = self._format_window_name(base_name, project_name, resolved_issue_id)
151
+ session_name = tmux_session_name_for_project(project_path)
105
152
 
106
153
  # Ensure the dedicated session exists, then create a new window for this worker.
107
154
  try:
108
- await self._run_tmux(["has-session", "-t", TMUX_SESSION_NAME])
155
+ await self._run_tmux(["has-session", "-t", session_name])
109
156
  output = await self._run_tmux(
110
157
  [
111
158
  "new-window",
112
159
  "-t",
113
- TMUX_SESSION_NAME,
160
+ session_name,
114
161
  "-n",
115
162
  window_name,
116
163
  "-P",
@@ -124,7 +171,7 @@ class TmuxBackend(TerminalBackend):
124
171
  "new-session",
125
172
  "-d",
126
173
  "-s",
127
- TMUX_SESSION_NAME,
174
+ session_name,
128
175
  "-n",
129
176
  window_name,
130
177
  "-P",
@@ -138,7 +185,7 @@ class TmuxBackend(TerminalBackend):
138
185
  raise RuntimeError("Failed to determine tmux pane id for new window")
139
186
 
140
187
  metadata = {
141
- "session_name": TMUX_SESSION_NAME,
188
+ "session_name": session_name,
142
189
  "window_id": window_id,
143
190
  "window_index": window_index,
144
191
  "window_name": window_name,
@@ -295,13 +342,12 @@ class TmuxBackend(TerminalBackend):
295
342
  return panes
296
343
 
297
344
  async def list_sessions(self) -> list[TerminalSession]:
298
- """List all tmux panes in the claude-team session."""
345
+ """List all tmux panes in claude-team-managed sessions."""
299
346
  try:
300
347
  output = await self._run_tmux(
301
348
  [
302
349
  "list-panes",
303
- "-t",
304
- TMUX_SESSION_NAME,
350
+ "-a",
305
351
  "-F",
306
352
  "#{session_name}\t#{window_id}\t#{window_name}\t#{window_index}\t#{pane_index}\t#{pane_id}",
307
353
  ]
@@ -320,6 +366,8 @@ class TmuxBackend(TerminalBackend):
320
366
  if len(parts) != 6:
321
367
  continue
322
368
  session_name, window_id, window_name, window_index, pane_index, pane_id = parts
369
+ if not _is_managed_session_name(session_name):
370
+ continue
323
371
  sessions.append(
324
372
  TerminalSession(
325
373
  backend_id=self.backend_id,
@@ -349,8 +397,7 @@ class TmuxBackend(TerminalBackend):
349
397
  output = await self._run_tmux(
350
398
  [
351
399
  "list-panes",
352
- "-t",
353
- TMUX_SESSION_NAME,
400
+ "-a",
354
401
  "-F",
355
402
  "#{session_name}\t#{window_id}\t#{window_index}\t#{pane_index}\t#{pane_active}\t#{pane_id}",
356
403
  ]
@@ -368,6 +415,8 @@ class TmuxBackend(TerminalBackend):
368
415
  if len(parts) != 6:
369
416
  continue
370
417
  session_name, window_id, window_index, pane_index, pane_active, pane_id = parts
418
+ if not _is_managed_session_name(session_name):
419
+ continue
371
420
  panes_by_window.setdefault((session_name, window_id, window_index), []).append(
372
421
  {
373
422
  "pane_id": pane_id,
@@ -576,18 +625,6 @@ class TmuxBackend(TerminalBackend):
576
625
 
577
626
  return False
578
627
 
579
- # Extract a display name for the project from a project path.
580
- def _project_name_from_path(self, project_path: str | None) -> str | None:
581
- if not project_path:
582
- return None
583
- path = Path(project_path)
584
- parts = path.parts
585
- if ".worktrees" in parts:
586
- worktrees_index = parts.index(".worktrees")
587
- if worktrees_index > 0:
588
- return parts[worktrees_index - 1]
589
- return path.name
590
-
591
628
  # Resolve an issue id from explicit input or a coordinator annotation.
592
629
  def _resolve_issue_id(
593
630
  self,
@@ -43,7 +43,7 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
43
43
  by claude-team write their terminal IDs into the JSONL
44
44
  (e.g., <!claude-team-iterm:UUID!> or <!claude-team-tmux:%1!>), enabling
45
45
  reliable detection and recovery after MCP server restarts.
46
- For tmux, only panes in the shared "claude-team" session are scanned.
46
+ For tmux, only panes in claude-team-managed sessions are scanned.
47
47
 
48
48
  Only JSONL files modified within max_age seconds are checked. If a session
49
49
  was started more than max_age seconds ago and hasn't had recent activity,