claude-team-mcp 0.3.0__tar.gz → 0.3.2__tar.gz

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 (81) hide show
  1. claude_team_mcp-0.3.2/.beads/.local_version +1 -0
  2. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/.beads/beads.db +0 -0
  3. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/.beads/beads.db-shm +0 -0
  4. claude_team_mcp-0.3.2/.beads/beads.db-wal +0 -0
  5. claude_team_mcp-0.3.2/.beads/daemon.lock +7 -0
  6. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/.beads/daemon.log +30 -0
  7. claude_team_mcp-0.3.2/.beads/daemon.pid +1 -0
  8. claude_team_mcp-0.3.2/HAPPY_INTEGRATION_RESEARCH.md +187 -0
  9. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/PKG-INFO +14 -1
  10. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/README.md +13 -0
  11. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/pyproject.toml +1 -1
  12. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/iterm_utils.py +18 -2
  13. claude_team_mcp-0.3.2/tests/test_iterm_utils.py +136 -0
  14. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/uv.lock +1 -1
  15. claude_team_mcp-0.3.0/.beads/.local_version +0 -1
  16. claude_team_mcp-0.3.0/.beads/beads.db-wal +0 -0
  17. claude_team_mcp-0.3.0/.beads/daemon.lock +0 -7
  18. claude_team_mcp-0.3.0/.beads/daemon.pid +0 -1
  19. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/.beads/.gitignore +0 -0
  20. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/.beads/README.md +0 -0
  21. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/.beads/config.yaml +0 -0
  22. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/.beads/deletions.jsonl +0 -0
  23. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/.beads/deletions.jsonl.migrated +0 -0
  24. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/.beads/issues.jsonl +0 -0
  25. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/.beads/metadata.json +0 -0
  26. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/.claude/settings.local.json +0 -0
  27. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/.claude-plugin/marketplace.json +0 -0
  28. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/.claude-plugin/plugin.json +0 -0
  29. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/.gitattributes +0 -0
  30. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/.gitignore +0 -0
  31. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/.mcp.json +0 -0
  32. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/CLAUDE.md +0 -0
  33. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/Makefile +0 -0
  34. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/NOTES.md +0 -0
  35. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/commands/check-workers.md +0 -0
  36. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/commands/cleanup-worktrees.md +0 -0
  37. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/commands/merge-worker.md +0 -0
  38. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/commands/pr-worker.md +0 -0
  39. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/commands/spawn-workers.md +0 -0
  40. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/commands/team-summary.md +0 -0
  41. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/scripts/install-commands.py +0 -0
  42. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/settings.json +0 -0
  43. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/__init__.py +0 -0
  44. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/__main__.py +0 -0
  45. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/colors.py +0 -0
  46. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/formatting.py +0 -0
  47. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/idle_detection.py +0 -0
  48. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/names.py +0 -0
  49. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/profile.py +0 -0
  50. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/registry.py +0 -0
  51. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/server.py +0 -0
  52. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/session_state.py +0 -0
  53. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/subprocess_cache.py +0 -0
  54. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/tools/__init__.py +0 -0
  55. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/tools/adopt_worker.py +0 -0
  56. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/tools/annotate_worker.py +0 -0
  57. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/tools/bd_help.py +0 -0
  58. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/tools/check_idle_workers.py +0 -0
  59. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/tools/close_workers.py +0 -0
  60. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/tools/discover_workers.py +0 -0
  61. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/tools/examine_worker.py +0 -0
  62. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/tools/list_workers.py +0 -0
  63. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/tools/list_worktrees.py +0 -0
  64. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/tools/message_workers.py +0 -0
  65. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/tools/read_worker_logs.py +0 -0
  66. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/tools/spawn_workers.py +0 -0
  67. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/tools/wait_idle_workers.py +0 -0
  68. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/utils/__init__.py +0 -0
  69. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/utils/constants.py +0 -0
  70. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/utils/errors.py +0 -0
  71. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/utils/worktree_detection.py +0 -0
  72. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/worker_prompt.py +0 -0
  73. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/src/claude_team_mcp/worktree.py +0 -0
  74. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/tests/__init__.py +0 -0
  75. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/tests/test_colors.py +0 -0
  76. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/tests/test_formatting.py +0 -0
  77. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/tests/test_idle_detection.py +0 -0
  78. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/tests/test_names.py +0 -0
  79. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/tests/test_registry.py +0 -0
  80. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/tests/test_session_state.py +0 -0
  81. {claude_team_mcp-0.3.0 → claude_team_mcp-0.3.2}/tests/test_worker_prompt.py +0 -0
@@ -0,0 +1 @@
1
+ 0.44.0
@@ -0,0 +1,7 @@
1
+ {
2
+ "pid": 48335,
3
+ "parent_pid": 48323,
4
+ "database": "/Users/phaedrus/Projects/claude-team/.beads/beads.db",
5
+ "version": "0.44.0",
6
+ "started_at": "2026-01-07T00:04:00.114512Z"
7
+ }
@@ -10610,3 +10610,33 @@ time=2026-01-05T13:59:42.898-08:00 level=INFO msg="Git HEAD change detected: %s"
10610
10610
  time=2026-01-05T13:59:43.901-08:00 level=INFO msg="Import triggered by file change"
10611
10611
  time=2026-01-05T13:59:43.901-08:00 level=INFO msg="Starting %s..." !BADKEY=auto-import
10612
10612
  time=2026-01-05T13:59:43.903-08:00 level=INFO msg="Skipping %s: JSONL content unchanged" !BADKEY=auto-import
10613
+ time=2026-01-05T14:16:19.112-08:00 level=INFO msg="Received signal %v, shutting down..." !BADKEY=terminated
10614
+ time=2026-01-06T16:04:00.127-08:00 level=INFO msg="Daemon started (interval: %v, auto-commit: %v, auto-push: %v)" !BADKEY=5s !BADKEY=false !BADKEY=false
10615
+ time=2026-01-06T16:04:00.128-08:00 level=INFO msg="using database" path=/Users/phaedrus/Projects/claude-team/.beads/beads.db
10616
+ time=2026-01-06T16:04:00.133-08:00 level=INFO msg="database opened" path=/Users/phaedrus/Projects/claude-team/.beads/beads.db freshness_checking=true
10617
+ time=2026-01-06T16:04:00.133-08:00 level=INFO msg="upgrading .beads/.gitignore"
10618
+ time=2026-01-06T16:04:00.133-08:00 level=WARN msg="failed to upgrade .gitignore" error="open .beads/.gitignore: no such file or directory"
10619
+ time=2026-01-06T16:04:00.137-08:00 level=INFO msg="Repository fingerprint validated: %s" !BADKEY=d0d96a12
10620
+ time=2026-01-06T16:04:00.137-08:00 level=WARN msg="database schema version mismatch" db_version=0.41.0 daemon_version=0.44.0
10621
+ time=2026-01-06T16:04:00.137-08:00 level=INFO msg="auto-upgrading database to daemon version"
10622
+ time=2026-01-06T16:04:00.137-08:00 level=INFO msg="database version updated" version=0.44.0
10623
+ time=2026-01-06T16:04:00.138-08:00 level=INFO msg="starting RPC server" socket=/Users/phaedrus/Projects/claude-team/.beads/bd.sock
10624
+ time=2026-01-06T16:04:00.138-08:00 level=INFO msg="RPC server ready (socket listening)"
10625
+ time=2026-01-06T16:04:00.143-08:00 level=INFO msg="registered in global registry"
10626
+ time=2026-01-06T16:04:00.143-08:00 level=INFO msg="Starting %s..." !BADKEY="sync cycle"
10627
+ time=2026-01-06T16:04:00.151-08:00 level=INFO msg="Exported to JSONL"
10628
+ time=2026-01-06T16:04:01.700-08:00 level=INFO msg="Pulled sync branch %s" !BADKEY=beads-sync
10629
+ time=2026-01-06T16:04:01.701-08:00 level=INFO msg="Synced JSONL from sync branch to main repo"
10630
+ time=2026-01-06T16:04:01.731-08:00 level=INFO msg="Imported from JSONL"
10631
+ time=2026-01-06T16:04:01.742-08:00 level=INFO msg="Sync cycle complete"
10632
+ time=2026-01-06T16:04:01.743-08:00 level=INFO msg="monitoring parent process" pid=0
10633
+ time=2026-01-06T16:04:01.743-08:00 level=INFO msg="using event-driven mode"
10634
+ time=2026-01-06T16:04:01.743-08:00 level=INFO msg="Auto-pull disabled: use 'git pull' manually to sync remote changes"
10635
+ time=2026-01-06T16:04:01.812-08:00 level=INFO msg="File change detected: %s" !BADKEY=/Users/phaedrus/Projects/claude-team/.beads/issues.jsonl
10636
+ time=2026-01-06T16:04:02.973-08:00 level=INFO msg="Import triggered by file change"
10637
+ time=2026-01-06T16:04:02.974-08:00 level=INFO msg="Starting %s..." !BADKEY=auto-import
10638
+ time=2026-01-06T16:04:02.974-08:00 level=INFO msg="Skipping %s: JSONL content unchanged" !BADKEY=auto-import
10639
+ time=2026-01-06T16:04:06.349-08:00 level=INFO msg="File change detected: %s" !BADKEY=/Users/phaedrus/Projects/claude-team/.beads/issues.jsonl
10640
+ time=2026-01-06T16:04:07.351-08:00 level=INFO msg="Import triggered by file change"
10641
+ time=2026-01-06T16:04:07.352-08:00 level=INFO msg="Starting %s..." !BADKEY=auto-import
10642
+ time=2026-01-06T16:04:07.352-08:00 level=INFO msg="Skipping %s: JSONL content unchanged" !BADKEY=auto-import
@@ -0,0 +1 @@
1
+ 48335
@@ -0,0 +1,187 @@
1
+ # Happy Integration Research
2
+
3
+ ## Problem Statement
4
+
5
+ When `CLAUDE_TEAM_COMMAND=happy` is set, sessions spawn and work in iTerm2 but Happy mobile shows them as empty. Running `happy` directly from terminal works fine.
6
+
7
+ ## Root Cause: `--settings` Flag Conflict
8
+
9
+ ### How claude-team spawns sessions
10
+
11
+ In `src/claude_team_mcp/iterm_utils.py:545-552`:
12
+
13
+ ```python
14
+ claude_cmd = os.environ.get("CLAUDE_TEAM_COMMAND", "claude")
15
+ if dangerously_skip_permissions:
16
+ claude_cmd += " --dangerously-skip-permissions"
17
+ if stop_hook_marker_id:
18
+ settings_file = build_stop_hook_settings_file(stop_hook_marker_id)
19
+ claude_cmd += f" --settings {settings_file}"
20
+ ```
21
+
22
+ This creates a command like:
23
+ ```
24
+ happy --dangerously-skip-permissions --settings ~/.claude/claude-team-settings/worker-xxx.json
25
+ ```
26
+
27
+ **Claude-team's settings file** (`~/.claude/claude-team-settings/worker-xxx.json`):
28
+ ```json
29
+ {
30
+ "hooks": {
31
+ "Stop": [{
32
+ "hooks": [{
33
+ "type": "command",
34
+ "command": "echo [worker-done:xxx]"
35
+ }]
36
+ }]
37
+ }
38
+ }
39
+ ```
40
+
41
+ ### How Happy processes the command
42
+
43
+ When Happy receives `--settings <file>`, it treats it as an unknown arg and passes it through to Claude via `options.claudeArgs`. Then in `dist/index-B3gQr6vs.mjs:311-314`:
44
+
45
+ ```javascript
46
+ if (opts.claudeArgs) {
47
+ args.push(...opts.claudeArgs); // Includes claude-team's --settings
48
+ }
49
+ args.push("--settings", opts.hookSettingsPath); // Happy's own --settings
50
+ ```
51
+
52
+ **Happy's settings file** (`~/.happy/tmp/hooks/session-hook-<pid>.json`):
53
+ ```json
54
+ {
55
+ "hooks": {
56
+ "SessionStart": [{
57
+ "matcher": "*",
58
+ "hooks": [{
59
+ "type": "command",
60
+ "command": "node \".../session_hook_forwarder.cjs\" <port>"
61
+ }]
62
+ }]
63
+ }
64
+ }
65
+ ```
66
+
67
+ ### The Conflict
68
+
69
+ Claude receives TWO `--settings` flags:
70
+ ```
71
+ claude ... --settings <claude-team-file> --settings <happy-file>
72
+ ```
73
+
74
+ Claude likely only uses the **first** `--settings` file (standard CLI behavior), meaning:
75
+ - Claude has the **Stop hook** (claude-team's) - for idle detection
76
+ - Claude does **NOT** have the **SessionStart hook** (Happy's) - for session tracking
77
+
78
+ ### Why Happy mobile shows empty sessions
79
+
80
+ Happy's session tracking relies on the `SessionStart` hook:
81
+ 1. Hook fires when Claude starts
82
+ 2. Hook script POSTs session ID to Happy's local hook server (port varies per session)
83
+ 3. Happy daemon receives session ID and updates session metadata
84
+ 4. Mobile app displays session
85
+
86
+ Without the SessionStart hook firing, Happy never learns the Claude session ID, so the session appears empty in the mobile app.
87
+
88
+ ## Key Files
89
+
90
+ | File | Purpose |
91
+ |------|---------|
92
+ | `src/claude_team_mcp/iterm_utils.py:459-500` | `build_stop_hook_settings_file()` creates Stop hook settings |
93
+ | `src/claude_team_mcp/iterm_utils.py:545-552` | Command building that adds `--settings` |
94
+ | Happy `dist/index-B3gQr6vs.mjs:311-314` | Args processing that adds Happy's `--settings` |
95
+ | Happy `dist/index-B3gQr6vs.mjs:4928-4953` | `generateHookSettingsFile()` creates SessionStart hook settings |
96
+ | Happy `dist/index-B3gQr6vs.mjs:4861-4927` | Hook server that receives session notifications |
97
+ | Happy `scripts/session_hook_forwarder.cjs` | Script executed by SessionStart hook |
98
+
99
+ ## Solution Options
100
+
101
+ ### Option A: Don't add `--settings` when using Happy
102
+
103
+ When `CLAUDE_TEAM_COMMAND=happy`, skip adding the `--settings` flag entirely:
104
+
105
+ ```python
106
+ claude_cmd = os.environ.get("CLAUDE_TEAM_COMMAND", "claude")
107
+ if dangerously_skip_permissions:
108
+ claude_cmd += " --dangerously-skip-permissions"
109
+
110
+ # Only add stop hook settings when NOT using happy
111
+ if stop_hook_marker_id and claude_cmd == "claude":
112
+ settings_file = build_stop_hook_settings_file(stop_hook_marker_id)
113
+ claude_cmd += f" --settings {settings_file}"
114
+ ```
115
+
116
+ **Tradeoff:** Loses idle detection for Happy-spawned workers. Claude-team would need an alternative mechanism.
117
+
118
+ ### Option B: Merge hooks into one settings file
119
+
120
+ Create a combined settings file that includes BOTH hooks:
121
+
122
+ ```python
123
+ def build_merged_settings_file(marker_id: str, happy_port: int) -> str:
124
+ settings = {
125
+ "hooks": {
126
+ "Stop": [{
127
+ "hooks": [{
128
+ "type": "command",
129
+ "command": f"echo [worker-done:{marker_id}]"
130
+ }]
131
+ }],
132
+ "SessionStart": [{
133
+ "matcher": "*",
134
+ "hooks": [{
135
+ "type": "command",
136
+ "command": f"node /path/to/session_hook_forwarder.cjs {happy_port}"
137
+ }]
138
+ }]
139
+ }
140
+ }
141
+ # ...
142
+ ```
143
+
144
+ **Tradeoff:** Requires knowing Happy's hook server port, which is dynamic and only known after Happy starts its hook server.
145
+
146
+ ### Option C: Coordinate with Happy's hook mechanism
147
+
148
+ Have claude-team discover Happy's hook port and integrate with it, or use Happy's daemon API directly for session tracking.
149
+
150
+ **Tradeoff:** Complex integration, tight coupling with Happy internals.
151
+
152
+ ### Option D: Request Happy feature - external settings support
153
+
154
+ Ask Happy to support inheriting/merging external `--settings` files instead of overwriting:
155
+
156
+ ```javascript
157
+ // In Happy's args processing
158
+ if (arg === "--settings") {
159
+ options.externalSettings = args[++i]; // Store for later merging
160
+ }
161
+
162
+ // When building Claude args
163
+ const mergedSettings = mergeSettings(opts.externalSettings, opts.hookSettingsPath);
164
+ args.push("--settings", mergedSettings);
165
+ ```
166
+
167
+ **Tradeoff:** Requires Happy changes, but cleanest solution.
168
+
169
+ ## Recommended Approach
170
+
171
+ **Short-term (Option A):** Detect when using Happy and skip the `--settings` flag. Accept the loss of Stop-hook-based idle detection. Use an alternative like polling the JSONL for assistant message patterns.
172
+
173
+ **Long-term (Option D):** Request Happy add support for merging external settings files, or document how to integrate with Happy's session tracking API directly.
174
+
175
+ ## Testing Verification
176
+
177
+ To verify the root cause:
178
+ 1. Run `happy` directly from terminal - check that SessionStart hook fires (session shows in mobile)
179
+ 2. Run via claude-team with `CLAUDE_TEAM_COMMAND=happy` - SessionStart hook should NOT fire
180
+ 3. Temporarily remove claude-team's `--settings` flag and respawn - session should appear in mobile
181
+
182
+ ## Additional Notes
183
+
184
+ - Happy's daemon runs in background and must be started for session tracking
185
+ - Happy auto-starts daemon if not running (line 6469-6478)
186
+ - Session metadata includes `startedBy: "terminal"` vs `"daemon"` - may affect visibility
187
+ - Happy's hook server port is ephemeral (assigned at runtime)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-team-mcp
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: MCP server for managing multiple Claude Code sessions via iTerm2
5
5
  Project-URL: Homepage, https://github.com/Martian-Engineering/claude-team
6
6
  Project-URL: Repository, https://github.com/Martian-Engineering/claude-team
@@ -140,6 +140,19 @@ Add to your Claude Code MCP settings. You can configure this at:
140
140
 
141
141
  After adding the configuration, restart Claude Code for it to take effect.
142
142
 
143
+ ## Environment Variables
144
+
145
+ | Variable | Default | Description |
146
+ |----------|---------|-------------|
147
+ | `CLAUDE_TEAM_COMMAND` | `claude` | Override the command used to start Claude Code in worker sessions. Useful for running alternative CLI implementations like `happy`. |
148
+ | `CLAUDE_TEAM_PROJECT_DIR` | (none) | When set, allows using `"project_path": "auto"` in worker configs to automatically use this path. |
149
+
150
+ Example using an alternative CLI:
151
+
152
+ ```bash
153
+ export CLAUDE_TEAM_COMMAND=happy
154
+ ```
155
+
143
156
  ## MCP Tools
144
157
 
145
158
  ### Worker Management
@@ -110,6 +110,19 @@ Add to your Claude Code MCP settings. You can configure this at:
110
110
 
111
111
  After adding the configuration, restart Claude Code for it to take effect.
112
112
 
113
+ ## Environment Variables
114
+
115
+ | Variable | Default | Description |
116
+ |----------|---------|-------------|
117
+ | `CLAUDE_TEAM_COMMAND` | `claude` | Override the command used to start Claude Code in worker sessions. Useful for running alternative CLI implementations like `happy`. |
118
+ | `CLAUDE_TEAM_PROJECT_DIR` | (none) | When set, allows using `"project_path": "auto"` in worker configs to automatically use this path. |
119
+
120
+ Example using an alternative CLI:
121
+
122
+ ```bash
123
+ export CLAUDE_TEAM_COMMAND=happy
124
+ ```
125
+
113
126
  ## MCP Tools
114
127
 
115
128
  ### Worker Management
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-team-mcp"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "MCP server for managing multiple Claude Code sessions via iTerm2"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -6,6 +6,7 @@ from the original primitives.py for use in the MCP server.
6
6
  """
7
7
 
8
8
  import logging
9
+ import os
9
10
  import re
10
11
  from typing import TYPE_CHECKING, Optional
11
12
 
@@ -515,6 +516,11 @@ async def start_claude_in_session(
515
516
  atomic command (cd && claude). Waits for shell readiness before sending
516
517
  the command, then waits for Claude's startup banner to appear.
517
518
 
519
+ The command used to launch Claude Code can be overridden by setting
520
+ the CLAUDE_TEAM_COMMAND environment variable (defaults to "claude").
521
+ This is useful for running alternative Claude CLI implementations
522
+ like "happy" or for testing purposes.
523
+
518
524
  Args:
519
525
  session: iTerm2 session to use
520
526
  project_path: Directory to run Claude in
@@ -537,10 +543,20 @@ async def start_claude_in_session(
537
543
  )
538
544
 
539
545
  # Build claude command with flags
540
- claude_cmd = "claude"
546
+ # Allow overriding the claude command via environment variable (e.g., "happy")
547
+ claude_cmd = os.environ.get("CLAUDE_TEAM_COMMAND", "claude")
548
+ is_default_claude_command = claude_cmd == "claude"
549
+
541
550
  if dangerously_skip_permissions:
542
551
  claude_cmd += " --dangerously-skip-permissions"
543
- if stop_hook_marker_id:
552
+
553
+ # Only add --settings for the default 'claude' command.
554
+ # Custom commands like 'happy' have their own session tracking mechanisms
555
+ # (e.g., Happy uses a SessionStart hook for mobile app integration).
556
+ # Adding our --settings flag conflicts with theirs because Claude only
557
+ # uses the first --settings file, breaking their session tracking.
558
+ # See HAPPY_INTEGRATION_RESEARCH.md for full analysis.
559
+ if stop_hook_marker_id and is_default_claude_command:
544
560
  settings_file = build_stop_hook_settings_file(stop_hook_marker_id)
545
561
  claude_cmd += f" --settings {settings_file}"
546
562
 
@@ -0,0 +1,136 @@
1
+ """Tests for iterm_utils module."""
2
+
3
+ import os
4
+ from unittest.mock import patch
5
+
6
+ import pytest
7
+
8
+ from claude_team_mcp.iterm_utils import build_stop_hook_settings_file
9
+
10
+
11
+ class TestClaudeCommandBuilding:
12
+ """Tests for Claude command building logic in start_claude_in_session.
13
+
14
+ These tests verify the --settings flag behavior based on CLAUDE_TEAM_COMMAND.
15
+ The actual start_claude_in_session function is async and requires iTerm2,
16
+ so we test the command building logic by examining the key conditions.
17
+ """
18
+
19
+ def test_default_claude_command_gets_settings(self):
20
+ """Default 'claude' command should get --settings flag for idle detection."""
21
+ # Simulate the logic from start_claude_in_session
22
+ with patch.dict(os.environ, {}, clear=False):
23
+ # Remove CLAUDE_TEAM_COMMAND if present
24
+ os.environ.pop("CLAUDE_TEAM_COMMAND", None)
25
+
26
+ claude_cmd = os.environ.get("CLAUDE_TEAM_COMMAND", "claude")
27
+ is_default_claude_command = claude_cmd == "claude"
28
+
29
+ assert claude_cmd == "claude"
30
+ assert is_default_claude_command is True
31
+
32
+ # With a stop_hook_marker_id, --settings should be added
33
+ stop_hook_marker_id = "test-marker-123"
34
+ if stop_hook_marker_id and is_default_claude_command:
35
+ settings_file = build_stop_hook_settings_file(stop_hook_marker_id)
36
+ claude_cmd += f" --settings {settings_file}"
37
+
38
+ assert "--settings" in claude_cmd
39
+ assert "test-marker-123" in settings_file
40
+
41
+ def test_custom_command_skips_settings(self):
42
+ """Custom commands like 'happy' should NOT get --settings flag.
43
+
44
+ Custom commands have their own session tracking mechanisms.
45
+ Adding --settings conflicts with them (e.g., Happy's SessionStart hook).
46
+ """
47
+ with patch.dict(os.environ, {"CLAUDE_TEAM_COMMAND": "happy"}):
48
+ claude_cmd = os.environ.get("CLAUDE_TEAM_COMMAND", "claude")
49
+ is_default_claude_command = claude_cmd == "claude"
50
+
51
+ assert claude_cmd == "happy"
52
+ assert is_default_claude_command is False
53
+
54
+ # With a stop_hook_marker_id, --settings should NOT be added
55
+ stop_hook_marker_id = "test-marker-123"
56
+ if stop_hook_marker_id and is_default_claude_command:
57
+ settings_file = build_stop_hook_settings_file(stop_hook_marker_id)
58
+ claude_cmd += f" --settings {settings_file}"
59
+
60
+ assert "--settings" not in claude_cmd
61
+
62
+ def test_custom_command_with_path_skips_settings(self):
63
+ """Custom commands specified as paths should also skip --settings."""
64
+ with patch.dict(os.environ, {"CLAUDE_TEAM_COMMAND": "/usr/local/bin/happy"}):
65
+ claude_cmd = os.environ.get("CLAUDE_TEAM_COMMAND", "claude")
66
+ is_default_claude_command = claude_cmd == "claude"
67
+
68
+ assert claude_cmd == "/usr/local/bin/happy"
69
+ assert is_default_claude_command is False
70
+
71
+ stop_hook_marker_id = "test-marker-123"
72
+ if stop_hook_marker_id and is_default_claude_command:
73
+ settings_file = build_stop_hook_settings_file(stop_hook_marker_id)
74
+ claude_cmd += f" --settings {settings_file}"
75
+
76
+ assert "--settings" not in claude_cmd
77
+
78
+ def test_dangerously_skip_permissions_still_added(self):
79
+ """--dangerously-skip-permissions should be added regardless of command."""
80
+ # Test with default claude
81
+ with patch.dict(os.environ, {}, clear=False):
82
+ os.environ.pop("CLAUDE_TEAM_COMMAND", None)
83
+ claude_cmd = os.environ.get("CLAUDE_TEAM_COMMAND", "claude")
84
+
85
+ dangerously_skip_permissions = True
86
+ if dangerously_skip_permissions:
87
+ claude_cmd += " --dangerously-skip-permissions"
88
+
89
+ assert "--dangerously-skip-permissions" in claude_cmd
90
+
91
+ # Test with custom command
92
+ with patch.dict(os.environ, {"CLAUDE_TEAM_COMMAND": "happy"}):
93
+ claude_cmd = os.environ.get("CLAUDE_TEAM_COMMAND", "claude")
94
+
95
+ dangerously_skip_permissions = True
96
+ if dangerously_skip_permissions:
97
+ claude_cmd += " --dangerously-skip-permissions"
98
+
99
+ assert "--dangerously-skip-permissions" in claude_cmd
100
+ assert claude_cmd == "happy --dangerously-skip-permissions"
101
+
102
+
103
+ class TestBuildStopHookSettingsFile:
104
+ """Tests for the stop hook settings file builder."""
105
+
106
+ def test_creates_valid_settings_file(self):
107
+ """Settings file should contain Stop hook with marker."""
108
+ import json
109
+ from pathlib import Path
110
+
111
+ marker_id = "test-abc123"
112
+ settings_path = build_stop_hook_settings_file(marker_id)
113
+
114
+ # Verify file exists and is valid JSON
115
+ settings_file = Path(settings_path)
116
+ assert settings_file.exists()
117
+
118
+ content = json.loads(settings_file.read_text())
119
+ assert "hooks" in content
120
+ assert "Stop" in content["hooks"]
121
+
122
+ # Verify marker is in the command
123
+ stop_hooks = content["hooks"]["Stop"]
124
+ assert len(stop_hooks) > 0
125
+ command = stop_hooks[0]["hooks"][0]["command"]
126
+ assert marker_id in command
127
+
128
+ def test_settings_file_in_expected_location(self):
129
+ """Settings file should be in ~/.claude/claude-team-settings/."""
130
+ from pathlib import Path
131
+
132
+ marker_id = "location-test-456"
133
+ settings_path = build_stop_hook_settings_file(marker_id)
134
+
135
+ expected_dir = Path.home() / ".claude" / "claude-team-settings"
136
+ assert settings_path.startswith(str(expected_dir))
@@ -114,7 +114,7 @@ wheels = [
114
114
 
115
115
  [[package]]
116
116
  name = "claude-team-mcp"
117
- version = "0.2.1"
117
+ version = "0.3.1"
118
118
  source = { editable = "." }
119
119
  dependencies = [
120
120
  { name = "iterm2" },
@@ -1 +0,0 @@
1
- 0.42.0
File without changes
@@ -1,7 +0,0 @@
1
- {
2
- "pid": 6716,
3
- "parent_pid": 6710,
4
- "database": "/Users/phaedrus/Projects/claude-team/.beads/beads.db",
5
- "version": "0.41.0",
6
- "started_at": "2025-12-30T17:49:19.75181Z"
7
- }
@@ -1 +0,0 @@
1
- 6716