claude-tempo 0.1.0
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.
- package/.mcp.json +9 -0
- package/CLAUDE.md +84 -0
- package/README.md +400 -0
- package/dist/cli.js +169 -0
- package/dist/server.js +234 -0
- package/package.json +34 -0
- package/src/channel.ts +35 -0
- package/src/cli/commands.ts +579 -0
- package/src/cli/output.ts +36 -0
- package/src/cli/preflight.ts +77 -0
- package/src/cli.ts +151 -0
- package/src/config.ts +25 -0
- package/src/server.ts +213 -0
- package/src/spawn.ts +213 -0
- package/src/tools/cue.ts +60 -0
- package/src/tools/ensemble.ts +102 -0
- package/src/tools/helpers.ts +16 -0
- package/src/tools/listen.ts +43 -0
- package/src/tools/recruit.ts +129 -0
- package/src/tools/report.ts +55 -0
- package/src/tools/resolve.ts +39 -0
- package/src/tools/set-name.ts +57 -0
- package/src/tools/set-part.ts +32 -0
- package/src/tools/terminate.ts +61 -0
- package/src/types.ts +64 -0
- package/src/worker.ts +34 -0
- package/src/workflows/session.ts +204 -0
- package/src/workflows/signals.ts +44 -0
- package/tests/recruit-terminal-test-plan.md +201 -0
- package/tsconfig.json +18 -0
package/.mcp.json
ADDED
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
## What is this?
|
|
4
|
+
|
|
5
|
+
claude-tempo is an MCP server that enables multiple Claude Code sessions to coordinate via Temporal.
|
|
6
|
+
|
|
7
|
+
## Tech Stack
|
|
8
|
+
|
|
9
|
+
- **Runtime**: Node.js 18+ with TypeScript
|
|
10
|
+
- **MCP**: `@modelcontextprotocol/sdk` (stdio transport)
|
|
11
|
+
- **Temporal**: `@temporalio/client`, `@temporalio/worker`, `@temporalio/workflow`, `@temporalio/activity`
|
|
12
|
+
- **No other dependencies** — no database, no custom broker
|
|
13
|
+
|
|
14
|
+
## Project Structure
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
src/
|
|
18
|
+
├── server.ts # MCP server entry point
|
|
19
|
+
├── worker.ts # Temporal worker setup
|
|
20
|
+
├── workflows/
|
|
21
|
+
│ ├── session.ts # claude-session workflow
|
|
22
|
+
│ └── signals.ts # Signal/query type definitions
|
|
23
|
+
├── tools/
|
|
24
|
+
│ ├── ensemble.ts # Discover active sessions
|
|
25
|
+
│ ├── cue.ts # Send message to peer
|
|
26
|
+
│ ├── set-name.ts # Set session name
|
|
27
|
+
│ ├── set-part.ts # Update own summary
|
|
28
|
+
│ ├── resolve.ts # Search-attribute session lookup
|
|
29
|
+
│ ├── listen.ts # Manual message check
|
|
30
|
+
│ ├── recruit.ts # Spawn new session
|
|
31
|
+
│ ├── report.ts # Report to conductor
|
|
32
|
+
│ ├── terminate.ts # Terminate a session
|
|
33
|
+
│ └── helpers.ts # Zod/MCP tool registration wrapper
|
|
34
|
+
├── types.ts # Shared type definitions
|
|
35
|
+
├── channel.ts # Claude channel notification helper
|
|
36
|
+
└── config.ts # Env var handling
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Development
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Install dependencies
|
|
43
|
+
npm install
|
|
44
|
+
|
|
45
|
+
# Start Temporal dev server (separate terminal)
|
|
46
|
+
temporal server start-dev
|
|
47
|
+
|
|
48
|
+
# Run in development
|
|
49
|
+
npx ts-node src/server.ts
|
|
50
|
+
|
|
51
|
+
# Build (compiles TS and pre-bundles workflow code)
|
|
52
|
+
npm run build
|
|
53
|
+
|
|
54
|
+
# Test
|
|
55
|
+
npm test
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
> **Important**: Always run `npm run build` after changing workflow code (`src/workflows/`).
|
|
59
|
+
> The build pre-bundles workflows into `workflow-bundle.js` so all workers use identical code.
|
|
60
|
+
|
|
61
|
+
## Key Concepts
|
|
62
|
+
|
|
63
|
+
- **Player**: A Claude Code session registered as a Temporal workflow
|
|
64
|
+
- **Conductor**: A special player that acts as orchestration hub, connected to external interfaces (one per ensemble)
|
|
65
|
+
- **Ensemble**: The set of all active players, namespaced by `CLAUDE_TEMPO_ENSEMBLE`
|
|
66
|
+
- **Cue**: A message sent to a player by name via Temporal signal
|
|
67
|
+
- **Part**: A player's description of what it's working on
|
|
68
|
+
- **Recruit**: Spawning a new Claude Code session as a player
|
|
69
|
+
- **set_name**: Players start with a random hex ID; `set_name` updates the `ClaudeTempoPlayerId` search attribute to a human-readable name
|
|
70
|
+
|
|
71
|
+
## Dashboard
|
|
72
|
+
|
|
73
|
+
The ensemble dashboard (Maestro) lives in a separate repository: [vinceblank/maestro](https://github.com/vinceblank/maestro)
|
|
74
|
+
|
|
75
|
+
It provides a web UI for managing ensembles, communicating with conductors, and monitoring player activity.
|
|
76
|
+
|
|
77
|
+
## Commit Convention
|
|
78
|
+
|
|
79
|
+
Use conventional commits: `type(scope): message`
|
|
80
|
+
|
|
81
|
+
Examples:
|
|
82
|
+
- `feat(tools): add ensemble discovery tool`
|
|
83
|
+
- `fix(workflow): handle signal delivery edge case`
|
|
84
|
+
- `docs: update getting started guide`
|
package/README.md
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# claude-tempo
|
|
2
|
+
|
|
3
|
+
MCP server for multi-session Claude Code coordination via [Temporal](https://temporal.io).
|
|
4
|
+
|
|
5
|
+
Multiple Claude Code sessions discover each other, exchange messages in real time, and coordinate work — across machines, not just localhost.
|
|
6
|
+
|
|
7
|
+
Inspired by [claude-peers](https://github.com/louislva/claude-peers-mcp) and seeing how it interacted with Claude Code's experimental channel capability. claude-tempo takes the concept further with Temporal as the coordination backbone — adding durable state, cross-machine messaging, structured orchestration, and automatic stale session cleanup.
|
|
8
|
+
|
|
9
|
+
## How it works
|
|
10
|
+
|
|
11
|
+
**claude-tempo** uses Temporal workflows as the coordination layer:
|
|
12
|
+
|
|
13
|
+
- Each Claude Code session registers as a **player** (a Temporal workflow)
|
|
14
|
+
- Players belong to an **ensemble** — a named group of sessions that can see and message each other
|
|
15
|
+
- Players discover each other via `ensemble`, message via `cue`, and spawn new sessions via `recruit`
|
|
16
|
+
- Players can interact directly (peer-to-peer) — no central hub required
|
|
17
|
+
- An optional **conductor** player acts as an orchestration hub for the ensemble, connected to external interfaces like Discord or Telegram
|
|
18
|
+
|
|
19
|
+
### Ensembles
|
|
20
|
+
|
|
21
|
+
An **ensemble** is an isolated group of players identified by name (e.g., `frontend`, `backend`, `default`). Players in one ensemble cannot see or message players in another — they are completely independent.
|
|
22
|
+
|
|
23
|
+
Each ensemble can have:
|
|
24
|
+
- Any number of **players** working on tasks
|
|
25
|
+
- One optional **conductor** coordinating work and connected to external interfaces
|
|
26
|
+
|
|
27
|
+
By default, all sessions join the `default` ensemble. Pass an ensemble name when starting a session to create or join a different one:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
claude-tempo conduct frontend # conduct the "frontend" ensemble
|
|
31
|
+
claude-tempo start backend # join the "backend" ensemble
|
|
32
|
+
claude-tempo conduct # conduct the "default" ensemble
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This lets you run separate groups of sessions for different projects or concerns without interference.
|
|
36
|
+
|
|
37
|
+
```mermaid
|
|
38
|
+
graph TD
|
|
39
|
+
You["You (Discord / Telegram / CLI / Claude Code)"]
|
|
40
|
+
You -->|signal / query| Conductor
|
|
41
|
+
|
|
42
|
+
subgraph Temporal["Temporal Server"]
|
|
43
|
+
Conductor["Conductor Workflow"]
|
|
44
|
+
PA["Player A Workflow"]
|
|
45
|
+
PB["Player B Workflow"]
|
|
46
|
+
PC["Player C Workflow"]
|
|
47
|
+
Conductor -->|cue| PA
|
|
48
|
+
Conductor -->|cue| PB
|
|
49
|
+
Conductor -->|cue| PC
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
subgraph Host1["Host 1"]
|
|
53
|
+
S1["Claude Session A"]
|
|
54
|
+
S2["Claude Session B"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
subgraph Host2["Host 2"]
|
|
58
|
+
S3["Claude Session C"]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
PA -.-> S1
|
|
62
|
+
PB -.-> S2
|
|
63
|
+
PC -.-> S3
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Tools
|
|
67
|
+
|
|
68
|
+
| Tool | Description |
|
|
69
|
+
|------|-------------|
|
|
70
|
+
| `ensemble` | Discover active sessions in your ensemble. Scope: `machine`, `repo`, `all`. |
|
|
71
|
+
| `cue` | Send a message to any session by player name. Instant via Temporal signal. |
|
|
72
|
+
| `set_name` | Set a human-readable name for this session. Used by other players to message you. |
|
|
73
|
+
| `set_part` | Describe what you're working on. Visible to others via `ensemble`. |
|
|
74
|
+
| `listen` | Manual fallback for checking pending messages. |
|
|
75
|
+
| `recruit` | Start a named Claude Code session in a directory. Opens a new terminal window automatically. |
|
|
76
|
+
| `report` | Send updates to the conductor (surfaces to Discord/Telegram). No-op if no conductor. |
|
|
77
|
+
| `terminate` | Terminate a player session by name. Use to clean up orphaned sessions. |
|
|
78
|
+
|
|
79
|
+
## Prerequisites
|
|
80
|
+
|
|
81
|
+
- [Node.js](https://nodejs.org/) 18+
|
|
82
|
+
- [Temporal CLI](https://docs.temporal.io/cli) (for local dev server)
|
|
83
|
+
- [Claude Code](https://claude.ai/code)
|
|
84
|
+
|
|
85
|
+
## Quick start
|
|
86
|
+
|
|
87
|
+
The fastest way to get going — one command handles everything:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# Install
|
|
91
|
+
npm install -g claude-tempo
|
|
92
|
+
|
|
93
|
+
# Go to your project and run `up`
|
|
94
|
+
cd your-project
|
|
95
|
+
claude-tempo up
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`claude-tempo up` will:
|
|
99
|
+
1. Check that the Temporal CLI is installed
|
|
100
|
+
2. Start the Temporal dev server if it's not already running (data persists in `~/.claude-tempo/`)
|
|
101
|
+
3. Register the required search attributes automatically
|
|
102
|
+
4. Create `.mcp.json` in your project if it doesn't exist
|
|
103
|
+
5. Launch a conductor session in a new terminal window
|
|
104
|
+
|
|
105
|
+
After `up` completes, you're ready to add players:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
claude-tempo start # open a player session
|
|
109
|
+
claude-tempo status # see who's active
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Or ask the conductor to `recruit` players for you from inside Claude Code.
|
|
113
|
+
|
|
114
|
+
### Manual setup
|
|
115
|
+
|
|
116
|
+
If you prefer more control, you can run each step individually:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# Start Temporal dev server (keep this running)
|
|
120
|
+
claude-tempo server
|
|
121
|
+
|
|
122
|
+
# In your project directory, create .mcp.json
|
|
123
|
+
cd your-project
|
|
124
|
+
claude-tempo init
|
|
125
|
+
|
|
126
|
+
# Verify everything is ready
|
|
127
|
+
claude-tempo preflight
|
|
128
|
+
|
|
129
|
+
# Start a conductor
|
|
130
|
+
claude-tempo conduct
|
|
131
|
+
|
|
132
|
+
# Add players
|
|
133
|
+
claude-tempo start
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## CLI
|
|
137
|
+
|
|
138
|
+
The `claude-tempo` CLI handles setup, session management, and diagnostics.
|
|
139
|
+
|
|
140
|
+
### Commands
|
|
141
|
+
|
|
142
|
+
| Command | Description |
|
|
143
|
+
|---------|-------------|
|
|
144
|
+
| `up [ensemble]` | First-time setup: start Temporal, configure MCP, launch conductor |
|
|
145
|
+
| `server` | Start the Temporal dev server and register search attributes |
|
|
146
|
+
| `conduct [ensemble]` | Start a conductor session (one per ensemble) |
|
|
147
|
+
| `start [ensemble]` | Start a player session |
|
|
148
|
+
| `status [ensemble]` | Show active sessions and Temporal health |
|
|
149
|
+
| `init` | Create `.mcp.json` config in the current directory |
|
|
150
|
+
| `preflight` | Run environment checks only |
|
|
151
|
+
| `help` | Show usage info |
|
|
152
|
+
|
|
153
|
+
### Options
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
--temporal-address <addr> Temporal server address (default: localhost:7233)
|
|
157
|
+
-n, --name <name> Set the session window name (start/conduct/up)
|
|
158
|
+
--skip-preflight Skip preflight checks (start/conduct)
|
|
159
|
+
--background, -d Run Temporal in background (server only)
|
|
160
|
+
--dir <path> Target directory for init (default: cwd)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### `up` — first-time setup
|
|
164
|
+
|
|
165
|
+
`claude-tempo up` is the recommended way to get started. It handles everything in order:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
$ claude-tempo up myband
|
|
169
|
+
|
|
170
|
+
claude-tempo setup
|
|
171
|
+
pass temporal CLI installed
|
|
172
|
+
... Starting Temporal dev server...
|
|
173
|
+
pass Temporal started (pid 12345, data in ~/.claude-tempo/)
|
|
174
|
+
ok Registered search attribute: ClaudeTempoHostname
|
|
175
|
+
ok Registered search attribute: ClaudeTempoGitRoot
|
|
176
|
+
ok Registered search attribute: ClaudeTempoEnsemble
|
|
177
|
+
ok Registered search attribute: ClaudeTempoPlayerId
|
|
178
|
+
pass .mcp.json created
|
|
179
|
+
|
|
180
|
+
Launching conductor in ensemble myband...
|
|
181
|
+
|
|
182
|
+
ok You're all set!
|
|
183
|
+
Conductor launched (pid 12346)
|
|
184
|
+
Ensemble: myband
|
|
185
|
+
|
|
186
|
+
What next?
|
|
187
|
+
claude-tempo start myband Add a player session
|
|
188
|
+
claude-tempo status myband See who's active
|
|
189
|
+
Or ask the conductor to recruit players for you
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### `server` — Temporal management
|
|
193
|
+
|
|
194
|
+
`claude-tempo server` starts the Temporal dev server with automatic search attribute registration:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
claude-tempo server # foreground (Ctrl+C to stop)
|
|
198
|
+
claude-tempo server --background # daemonize
|
|
199
|
+
claude-tempo server -d # shorthand
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
- Stores data in `~/.claude-tempo/temporal-data.db` (persists across restarts)
|
|
203
|
+
- Registers all required search attributes automatically
|
|
204
|
+
- If Temporal is already running, just registers attributes and exits
|
|
205
|
+
|
|
206
|
+
### `init` — MCP configuration
|
|
207
|
+
|
|
208
|
+
`claude-tempo init` creates a `.mcp.json` in the current directory (or merges into an existing one):
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"mcpServers": {
|
|
213
|
+
"claude-tempo": {
|
|
214
|
+
"command": "claude-tempo-server",
|
|
215
|
+
"args": []
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
No source code or absolute paths needed — `claude-tempo-server` is installed on PATH via the npm package.
|
|
222
|
+
|
|
223
|
+
### `status` — ensemble overview
|
|
224
|
+
|
|
225
|
+
`claude-tempo status` shows all active sessions:
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
Ensemble: myband
|
|
229
|
+
3 active sessions
|
|
230
|
+
|
|
231
|
+
conductor (conductor)
|
|
232
|
+
Orchestrating the team
|
|
233
|
+
/Users/me/projects/app main my-machine.local
|
|
234
|
+
|
|
235
|
+
alice
|
|
236
|
+
Building the REST endpoints
|
|
237
|
+
/Users/me/projects/app feat/api my-machine.local
|
|
238
|
+
|
|
239
|
+
bob
|
|
240
|
+
Working on the dashboard
|
|
241
|
+
/Users/me/projects/app feat/ui my-machine.local
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### `preflight` — environment checks
|
|
245
|
+
|
|
246
|
+
`claude-tempo preflight` verifies your environment:
|
|
247
|
+
|
|
248
|
+
- Node.js >= 18
|
|
249
|
+
- Temporal server reachable
|
|
250
|
+
- `claude` binary on PATH
|
|
251
|
+
- `claude-tempo-server` binary on PATH
|
|
252
|
+
- `.mcp.json` configured in the current directory
|
|
253
|
+
|
|
254
|
+
## Starting a conductor
|
|
255
|
+
|
|
256
|
+
A **conductor** is an optional special player that acts as an orchestration hub for the ensemble. Use a conductor when you want:
|
|
257
|
+
|
|
258
|
+
- A single session coordinating work across multiple players
|
|
259
|
+
- External access to the ensemble via Discord, Telegram, or any Temporal client
|
|
260
|
+
- A central point for players to `report` progress, blockers, and questions
|
|
261
|
+
|
|
262
|
+
Without a conductor, players still work fine — they discover each other via `ensemble` and communicate directly via `cue`. The conductor is a hub, not a gatekeeper.
|
|
263
|
+
|
|
264
|
+
There is one conductor per ensemble. Start one with:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
claude-tempo conduct # conductor in "default" ensemble
|
|
268
|
+
claude-tempo conduct my-project # conductor in "my-project" ensemble
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### External access
|
|
272
|
+
|
|
273
|
+
The conductor's Temporal workflow exposes a signal/query API that anyone can use — no Claude Code session needed:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { Client } from '@temporalio/client';
|
|
277
|
+
|
|
278
|
+
const client = new Client();
|
|
279
|
+
// Conductor workflow ID: claude-session-{ensemble}-conductor
|
|
280
|
+
const conductor = client.workflow.getHandle('claude-session-default-conductor');
|
|
281
|
+
|
|
282
|
+
// Send a command
|
|
283
|
+
await conductor.signal('command', {
|
|
284
|
+
text: 'recruit /repos/api and run tests',
|
|
285
|
+
source: 'cli',
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Check history of commands and reports
|
|
289
|
+
const history = await conductor.query('history');
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
You can also connect external channel plugins (e.g., Discord):
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
CLAUDE_TEMPO_CONDUCTOR=true claude \
|
|
296
|
+
--channels plugin:discord@claude-plugins-official \
|
|
297
|
+
--dangerously-skip-permissions --dangerously-load-development-channels server:claude-tempo
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Starting players
|
|
301
|
+
|
|
302
|
+
The recommended way to build an ensemble is to use the CLI to start sessions. Each session opens in a new terminal window with the full shell environment preserved.
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
# Terminal 1 — conductor
|
|
306
|
+
claude-tempo conduct my-project
|
|
307
|
+
|
|
308
|
+
# Terminal 2 — frontend player
|
|
309
|
+
claude-tempo start my-project -n frontend
|
|
310
|
+
|
|
311
|
+
# Terminal 3 — backend player
|
|
312
|
+
claude-tempo start my-project -n backend
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Or let the conductor `recruit` players directly — this spawns new terminal windows automatically.
|
|
316
|
+
|
|
317
|
+
Once sessions are running, try:
|
|
318
|
+
- "Show me the ensemble" — discovers other sessions
|
|
319
|
+
- "Set your name to 'frontend'" — gives your session a human-readable name
|
|
320
|
+
- "Cue frontend: what are you working on?" — sends a message by name
|
|
321
|
+
|
|
322
|
+
### Terminal support
|
|
323
|
+
|
|
324
|
+
The `recruit` tool and CLI automatically detect and open sessions in your terminal:
|
|
325
|
+
|
|
326
|
+
| Terminal | macOS | Linux | Windows |
|
|
327
|
+
|----------|-------|-------|---------|
|
|
328
|
+
| Ghostty | `initial input` via AppleScript | — | — |
|
|
329
|
+
| iTerm2 | `write text` via AppleScript | — | — |
|
|
330
|
+
| Terminal.app | `.command` file | — | — |
|
|
331
|
+
| gnome-terminal | — | `--` flag | — |
|
|
332
|
+
| konsole / xterm | — | `-e` flag | — |
|
|
333
|
+
| cmd.exe / PowerShell | — | — | `shell:true` |
|
|
334
|
+
|
|
335
|
+
All macOS terminals use approaches that preserve the user's full shell environment (fish, zsh, bash) including node version managers (fnm, nvm).
|
|
336
|
+
|
|
337
|
+
### Session naming
|
|
338
|
+
|
|
339
|
+
Sessions start with a random 8-character hex ID. Use `set_name` to give a session a human-readable name:
|
|
340
|
+
|
|
341
|
+
- Names are stored as Temporal search attributes (`ClaudeTempoPlayerId`) and updated in-place — no workflow restart needed
|
|
342
|
+
- Other players use the name to send messages via `cue` and discover sessions via `ensemble`
|
|
343
|
+
- `recruit` automatically tells the new session to set its name
|
|
344
|
+
- Names must be unique within an ensemble — `set_name` rejects duplicates
|
|
345
|
+
- Names must contain only letters, numbers, hyphens, and underscores
|
|
346
|
+
|
|
347
|
+
## Configuration
|
|
348
|
+
|
|
349
|
+
| Environment Variable | Default | Description |
|
|
350
|
+
|---------------------|---------|-------------|
|
|
351
|
+
| `TEMPORAL_ADDRESS` | `localhost:7233` | Temporal server address |
|
|
352
|
+
| `TEMPORAL_NAMESPACE` | `default` | Temporal namespace |
|
|
353
|
+
| `CLAUDE_TEMPO_TASK_QUEUE` | `claude-tempo` | Task queue name |
|
|
354
|
+
| `CLAUDE_TEMPO_ENSEMBLE` | `default` | Ensemble name (isolates groups of players) |
|
|
355
|
+
| `CLAUDE_TEMPO_CONDUCTOR` | `false` | Set to `true` to enable conductor mode |
|
|
356
|
+
|
|
357
|
+
## Development
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
# Clone and install
|
|
361
|
+
git clone https://github.com/vinceblank/claude-tempo.git
|
|
362
|
+
cd claude-tempo && npm install
|
|
363
|
+
|
|
364
|
+
# Build (compiles TypeScript and pre-bundles workflow code)
|
|
365
|
+
npm run build
|
|
366
|
+
|
|
367
|
+
# Run MCP server in development
|
|
368
|
+
npx ts-node src/server.ts
|
|
369
|
+
|
|
370
|
+
# Link CLI for local testing
|
|
371
|
+
npm link
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
> **Important**: Run `npm run build` after changing workflow code (`src/workflows/`). The build pre-bundles workflows into `workflow-bundle.js` so all workers use identical code.
|
|
375
|
+
|
|
376
|
+
## Why Temporal?
|
|
377
|
+
|
|
378
|
+
- **Cross-machine**: Any session that can reach the Temporal server can join the ensemble
|
|
379
|
+
- **Instant signaling**: Temporal signals deliver messages between sessions with no broker polling
|
|
380
|
+
- **Durable history**: Full audit trail of every message in Temporal's event history
|
|
381
|
+
- **No custom infrastructure**: No broker daemon, no database — just Temporal
|
|
382
|
+
- **Extensible**: The conductor's signal/query contract is a public API anyone can build on
|
|
383
|
+
|
|
384
|
+
## Stale session cleanup
|
|
385
|
+
|
|
386
|
+
When a Claude Code session crashes or is closed without graceful shutdown, its Temporal workflow detects the problem automatically:
|
|
387
|
+
|
|
388
|
+
- If a message is sent to a dead session and remains undelivered for **3 minutes**, the workflow self-completes
|
|
389
|
+
- Before exiting, it notifies the conductor with the undelivered message content so work can be reassigned
|
|
390
|
+
- Idle sessions with no pending messages remain running (they aren't hurting anyone) until the 24-hour execution timeout
|
|
391
|
+
|
|
392
|
+
This means you don't need to manually clean up crashed sessions — just `cue` the dead player and the system handles the rest.
|
|
393
|
+
|
|
394
|
+
## Known limitations
|
|
395
|
+
|
|
396
|
+
- **`recruit` requires manual acknowledgment**: Recruited sessions use `--dangerously-load-development-channels` to enable channel-based message delivery. Claude Code shows an interactive confirmation prompt that must be manually acknowledged (press Enter) in the spawned terminal window. This will be resolved once claude-tempo is published as an approved channel plugin.
|
|
397
|
+
|
|
398
|
+
## License
|
|
399
|
+
|
|
400
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const commands_1 = require("./cli/commands");
|
|
38
|
+
const preflight_1 = require("./cli/preflight");
|
|
39
|
+
const out = __importStar(require("./cli/output"));
|
|
40
|
+
function parseArgs(argv) {
|
|
41
|
+
const result = {
|
|
42
|
+
command: 'help',
|
|
43
|
+
positional: [],
|
|
44
|
+
temporalAddress: process.env.TEMPORAL_ADDRESS || 'localhost:7233',
|
|
45
|
+
dir: process.cwd(),
|
|
46
|
+
skipPreflight: false,
|
|
47
|
+
background: false,
|
|
48
|
+
keepMcp: false,
|
|
49
|
+
};
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < argv.length) {
|
|
52
|
+
const arg = argv[i];
|
|
53
|
+
if (arg === '--temporal-address' && i + 1 < argv.length) {
|
|
54
|
+
result.temporalAddress = argv[++i];
|
|
55
|
+
}
|
|
56
|
+
else if ((arg === '-n' || arg === '--name') && i + 1 < argv.length) {
|
|
57
|
+
result.name = argv[++i];
|
|
58
|
+
}
|
|
59
|
+
else if (arg === '--dir' && i + 1 < argv.length) {
|
|
60
|
+
result.dir = argv[++i];
|
|
61
|
+
}
|
|
62
|
+
else if (arg === '--skip-preflight') {
|
|
63
|
+
result.skipPreflight = true;
|
|
64
|
+
}
|
|
65
|
+
else if (arg === '--background' || arg === '-d') {
|
|
66
|
+
result.background = true;
|
|
67
|
+
}
|
|
68
|
+
else if (arg === '--keep-mcp') {
|
|
69
|
+
result.keepMcp = true;
|
|
70
|
+
}
|
|
71
|
+
else if (arg === '--help' || arg === '-h') {
|
|
72
|
+
result.command = 'help';
|
|
73
|
+
}
|
|
74
|
+
else if (arg === '--version' || arg === '-v') {
|
|
75
|
+
result.command = 'version';
|
|
76
|
+
}
|
|
77
|
+
else if (!arg.startsWith('-')) {
|
|
78
|
+
result.positional.push(arg);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
out.error(`Unknown option: ${arg}`);
|
|
82
|
+
out.log(`Run ${out.dim('claude-tempo help')} for usage.`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
i++;
|
|
86
|
+
}
|
|
87
|
+
if (result.positional.length > 0) {
|
|
88
|
+
result.command = result.positional[0];
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
async function main() {
|
|
93
|
+
const args = parseArgs(process.argv.slice(2));
|
|
94
|
+
const ensemble = args.positional[1] || process.env.CLAUDE_TEMPO_ENSEMBLE || 'default';
|
|
95
|
+
switch (args.command) {
|
|
96
|
+
case 'conduct':
|
|
97
|
+
await (0, commands_1.start)({
|
|
98
|
+
ensemble,
|
|
99
|
+
conductor: true,
|
|
100
|
+
temporalAddress: args.temporalAddress,
|
|
101
|
+
name: args.name,
|
|
102
|
+
skipPreflight: args.skipPreflight,
|
|
103
|
+
});
|
|
104
|
+
break;
|
|
105
|
+
case 'start':
|
|
106
|
+
await (0, commands_1.start)({
|
|
107
|
+
ensemble,
|
|
108
|
+
conductor: false,
|
|
109
|
+
temporalAddress: args.temporalAddress,
|
|
110
|
+
name: args.name,
|
|
111
|
+
skipPreflight: args.skipPreflight,
|
|
112
|
+
});
|
|
113
|
+
break;
|
|
114
|
+
case 'status':
|
|
115
|
+
await (0, commands_1.status)({
|
|
116
|
+
ensemble: args.positional[1], // undefined = show all
|
|
117
|
+
temporalAddress: args.temporalAddress,
|
|
118
|
+
});
|
|
119
|
+
break;
|
|
120
|
+
case 'server':
|
|
121
|
+
await (0, commands_1.server)({
|
|
122
|
+
temporalAddress: args.temporalAddress,
|
|
123
|
+
background: args.background,
|
|
124
|
+
});
|
|
125
|
+
break;
|
|
126
|
+
case 'down':
|
|
127
|
+
await (0, commands_1.down)({
|
|
128
|
+
temporalAddress: args.temporalAddress,
|
|
129
|
+
removeMcp: !args.keepMcp,
|
|
130
|
+
dir: args.dir,
|
|
131
|
+
});
|
|
132
|
+
break;
|
|
133
|
+
case 'up':
|
|
134
|
+
await (0, commands_1.up)({
|
|
135
|
+
ensemble,
|
|
136
|
+
temporalAddress: args.temporalAddress,
|
|
137
|
+
name: args.name,
|
|
138
|
+
});
|
|
139
|
+
break;
|
|
140
|
+
case 'init':
|
|
141
|
+
await (0, commands_1.init)({ dir: args.dir });
|
|
142
|
+
break;
|
|
143
|
+
case 'preflight':
|
|
144
|
+
const result = await (0, preflight_1.runPreflight)({
|
|
145
|
+
temporalAddress: args.temporalAddress,
|
|
146
|
+
projectDir: args.dir,
|
|
147
|
+
});
|
|
148
|
+
for (const w of result.warnings)
|
|
149
|
+
out.warn(w);
|
|
150
|
+
if (!result.ok) {
|
|
151
|
+
for (const e of result.errors)
|
|
152
|
+
out.error(e);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
out.success('All checks passed');
|
|
156
|
+
break;
|
|
157
|
+
case 'version':
|
|
158
|
+
(0, commands_1.version)();
|
|
159
|
+
break;
|
|
160
|
+
case 'help':
|
|
161
|
+
default:
|
|
162
|
+
(0, commands_1.help)();
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
main().catch((err) => {
|
|
167
|
+
out.error(err.message || String(err));
|
|
168
|
+
process.exit(1);
|
|
169
|
+
});
|