claude-tempo 0.3.0 → 0.4.1
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/README.md +44 -38
- package/dist/cli/commands.d.ts +10 -0
- package/dist/cli/commands.js +229 -29
- package/dist/cli/config-command.js +14 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +61 -0
- package/dist/cli/preflight.js +5 -14
- package/dist/cli.js +25 -4
- package/dist/config.d.ts +5 -0
- package/dist/config.js +8 -0
- package/dist/server.js +1 -1
- package/dist/workflows/session.js +21 -2
- package/package.json +1 -1
- package/workflow-bundle.js +22 -3
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ This will:
|
|
|
32
32
|
1. Check that Temporal CLI is installed
|
|
33
33
|
2. Start the Temporal dev server (data persists in `~/.claude-tempo/`)
|
|
34
34
|
3. Register required search attributes
|
|
35
|
-
4.
|
|
35
|
+
4. Register the claude-tempo MCP server (globally by default)
|
|
36
36
|
5. Launch a conductor session in a new terminal window
|
|
37
37
|
|
|
38
38
|
Then add players:
|
|
@@ -52,7 +52,7 @@ For more control, run each step individually:
|
|
|
52
52
|
# Start Temporal dev server (keep running)
|
|
53
53
|
claude-tempo server
|
|
54
54
|
|
|
55
|
-
#
|
|
55
|
+
# Register claude-tempo MCP server (globally by default)
|
|
56
56
|
cd your-project
|
|
57
57
|
claude-tempo init
|
|
58
58
|
|
|
@@ -98,7 +98,8 @@ claude-tempo <command> [options]
|
|
|
98
98
|
| `start [ensemble]` | Start a player session |
|
|
99
99
|
| `status [ensemble]` | Show active sessions and Temporal health |
|
|
100
100
|
| `config` | Configure Temporal connection settings (interactive or `set`/`show`) |
|
|
101
|
-
| `
|
|
101
|
+
| `stop [ensemble]` | Stop sessions (`-n <name>` for one, `--all` for everything) |
|
|
102
|
+
| `init` | Register claude-tempo MCP server globally (`--project` for per-directory) |
|
|
102
103
|
| `preflight` | Run environment checks |
|
|
103
104
|
| `help` | Show usage info |
|
|
104
105
|
|
|
@@ -180,19 +181,15 @@ Verifies your environment: Node.js >= 18, Temporal reachable, `claude` on PATH,
|
|
|
180
181
|
|
|
181
182
|
### `claude-tempo init`
|
|
182
183
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
"claude-tempo": {
|
|
189
|
-
"command": "npx",
|
|
190
|
-
"args": ["claude-tempo-server"]
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
184
|
+
Registers the claude-tempo MCP server globally so it's available in every Claude Code session:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
claude-tempo init # global install (recommended)
|
|
188
|
+
claude-tempo init --project # per-directory .mcp.json instead
|
|
194
189
|
```
|
|
195
190
|
|
|
191
|
+
If the `claude` CLI is not available, falls back to creating `.mcp.json` in the current directory.
|
|
192
|
+
|
|
196
193
|
## MCP tools
|
|
197
194
|
|
|
198
195
|
These tools are available inside Claude Code sessions connected to claude-tempo:
|
|
@@ -355,6 +352,7 @@ export TEMPORAL_API_KEY=tcl_...
|
|
|
355
352
|
| `CLAUDE_TEMPO_ENSEMBLE` | `default` | Ensemble name |
|
|
356
353
|
| `CLAUDE_TEMPO_CONDUCTOR` | `false` | Enable conductor mode |
|
|
357
354
|
| `CLAUDE_TEMPO_PLAYER_NAME` | *(random hex)* | Player name on startup |
|
|
355
|
+
| `CLAUDE_TEMPO_DEFAULT_AGENT` | `claude` | Default agent type (`claude` or `copilot`) |
|
|
358
356
|
|
|
359
357
|
## Stale session cleanup
|
|
360
358
|
|
|
@@ -362,7 +360,7 @@ When a session crashes or closes without graceful shutdown, Temporal detects it
|
|
|
362
360
|
|
|
363
361
|
- If a message to a dead session remains undelivered for **3 minutes**, the workflow self-completes
|
|
364
362
|
- Before exiting, it notifies the conductor with the undelivered message so work can be reassigned
|
|
365
|
-
- Idle sessions with no pending messages
|
|
363
|
+
- Idle sessions with no pending messages are probed after 1 hour of inactivity via a heartbeat ping; if the ping goes undelivered, the session self-completes
|
|
366
364
|
|
|
367
365
|
No manual cleanup needed — `cue` a dead player and the system handles the rest.
|
|
368
366
|
|
|
@@ -370,52 +368,60 @@ No manual cleanup needed — `cue` a dead player and the system handles the rest
|
|
|
370
368
|
|
|
371
369
|
> **Warning:** Copilot bridge support is experimental and subject to breaking changes.
|
|
372
370
|
|
|
373
|
-
GitHub Copilot CLI sessions can join an ensemble via the Copilot bridge. Bridge sessions are headless — they require a
|
|
371
|
+
GitHub Copilot CLI sessions can join an ensemble via the Copilot bridge. Bridge sessions are headless — they require a conductor or another player to receive work via `cue`.
|
|
374
372
|
|
|
375
373
|
<details>
|
|
376
374
|
<summary>Setup and usage</summary>
|
|
377
375
|
|
|
378
376
|
### Prerequisites
|
|
379
377
|
|
|
378
|
+
- [GitHub Copilot CLI](https://docs.github.com/en/copilot/github-copilot-in-the-cli) installed and authenticated
|
|
379
|
+
- An active GitHub Copilot subscription
|
|
380
|
+
- Node.js 20+
|
|
381
|
+
- Install the Copilot SDK: `npm install @github/copilot-sdk`
|
|
382
|
+
|
|
383
|
+
### Starting Copilot sessions
|
|
384
|
+
|
|
385
|
+
Use `--agent copilot` with any session-launching command:
|
|
386
|
+
|
|
380
387
|
```bash
|
|
381
|
-
|
|
388
|
+
claude-tempo start myband --agent copilot -n copilot-1 # start a player
|
|
389
|
+
claude-tempo conduct myband --agent copilot # start a conductor
|
|
390
|
+
claude-tempo up myband --agent copilot # full setup
|
|
382
391
|
```
|
|
383
392
|
|
|
384
|
-
|
|
393
|
+
Or recruit from within any active session:
|
|
385
394
|
|
|
386
|
-
|
|
395
|
+
> "Recruit a copilot session named 'copilot-dev' in /repos/my-project with agent copilot"
|
|
387
396
|
|
|
388
|
-
|
|
397
|
+
### Setting a default agent
|
|
389
398
|
|
|
390
|
-
|
|
399
|
+
To avoid passing `--agent copilot` every time:
|
|
391
400
|
|
|
392
|
-
|
|
401
|
+
```bash
|
|
402
|
+
claude-tempo config set default-agent copilot
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Or via environment variable:
|
|
393
406
|
|
|
394
407
|
```bash
|
|
395
|
-
|
|
408
|
+
export CLAUDE_TEMPO_DEFAULT_AGENT=copilot
|
|
396
409
|
```
|
|
397
410
|
|
|
398
|
-
|
|
411
|
+
Resolution order: `--agent` flag → `CLAUDE_TEMPO_DEFAULT_AGENT` env → config file → `claude`.
|
|
399
412
|
|
|
400
|
-
|
|
401
|
-
2. MCP server registers the session as a Temporal workflow
|
|
402
|
-
3. Bridge polls for pending messages every 2 seconds
|
|
403
|
-
4. Messages are injected as prompts via `session.sendAndWait()`
|
|
404
|
-
5. The Copilot session can use all claude-tempo tools
|
|
413
|
+
### Model override
|
|
405
414
|
|
|
406
|
-
|
|
415
|
+
Set `COPILOT_BRIDGE_MODEL` to use a specific model for Copilot sessions:
|
|
407
416
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
| `COPILOT_BRIDGE_MODEL` | *(Copilot default)* | Model override |
|
|
412
|
-
| `GITHUB_TOKEN` | *(logged-in user)* | GitHub auth token |
|
|
417
|
+
```bash
|
|
418
|
+
COPILOT_BRIDGE_MODEL=gpt-4o claude-tempo start myband --agent copilot
|
|
419
|
+
```
|
|
413
420
|
|
|
414
421
|
### Limitations
|
|
415
422
|
|
|
416
|
-
-
|
|
417
|
-
- 2-second polling latency (vs instant for Claude Code sessions)
|
|
418
|
-
- Must be spawned via the bridge to participate
|
|
423
|
+
- Headless only — bridge sessions respond to cues, no interactive terminal
|
|
424
|
+
- ~2-second polling latency (vs instant for Claude Code sessions)
|
|
419
425
|
- `@github/copilot-sdk` adds ~243MB to node_modules
|
|
420
426
|
- Node 20+ required (rest of claude-tempo works on Node 18+)
|
|
421
427
|
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ interface StatusOpts extends CliOverrides {
|
|
|
15
15
|
export declare function status(opts: StatusOpts): Promise<void>;
|
|
16
16
|
interface InitOpts {
|
|
17
17
|
dir: string;
|
|
18
|
+
project?: boolean;
|
|
18
19
|
}
|
|
19
20
|
export declare function init(opts: InitOpts): Promise<void>;
|
|
20
21
|
interface ServerOpts extends CliOverrides {
|
|
@@ -32,6 +33,15 @@ interface DownOpts extends CliOverrides {
|
|
|
32
33
|
dir: string;
|
|
33
34
|
}
|
|
34
35
|
export declare function down(opts: DownOpts): Promise<void>;
|
|
36
|
+
interface StopOpts extends CliOverrides {
|
|
37
|
+
/** Stop a specific player by name. */
|
|
38
|
+
name?: string;
|
|
39
|
+
/** Stop all sessions in this ensemble. */
|
|
40
|
+
ensemble?: string;
|
|
41
|
+
/** Stop every session across all ensembles. */
|
|
42
|
+
all?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export declare function stop(opts: StopOpts): Promise<void>;
|
|
35
45
|
export declare function help(): void;
|
|
36
46
|
export declare function version(): void;
|
|
37
47
|
export {};
|
package/dist/cli/commands.js
CHANGED
|
@@ -39,6 +39,7 @@ exports.init = init;
|
|
|
39
39
|
exports.server = server;
|
|
40
40
|
exports.up = up;
|
|
41
41
|
exports.down = down;
|
|
42
|
+
exports.stop = stop;
|
|
42
43
|
exports.help = help;
|
|
43
44
|
exports.version = version;
|
|
44
45
|
const fs_1 = require("fs");
|
|
@@ -48,7 +49,9 @@ const client_1 = require("@temporalio/client");
|
|
|
48
49
|
const spawn_1 = require("../spawn");
|
|
49
50
|
const config_1 = require("../config");
|
|
50
51
|
const connection_1 = require("../connection");
|
|
52
|
+
const signals_1 = require("../workflows/signals");
|
|
51
53
|
const preflight_1 = require("./preflight");
|
|
54
|
+
const mcp_1 = require("./mcp");
|
|
52
55
|
const out = __importStar(require("./output"));
|
|
53
56
|
/** Package root is two levels up from dist/cli/ */
|
|
54
57
|
const PACKAGE_ROOT = (0, path_1.resolve)(__dirname, '..', '..');
|
|
@@ -223,7 +226,36 @@ async function status(opts) {
|
|
|
223
226
|
console.log();
|
|
224
227
|
}
|
|
225
228
|
async function init(opts) {
|
|
226
|
-
|
|
229
|
+
if (opts.project) {
|
|
230
|
+
// Per-project .mcp.json mode
|
|
231
|
+
return initProject(opts.dir);
|
|
232
|
+
}
|
|
233
|
+
// Default: global install via `claude mcp add`
|
|
234
|
+
if ((0, mcp_1.isGlobalMcpRegistered)()) {
|
|
235
|
+
out.success('claude-tempo already registered globally');
|
|
236
|
+
out.log(` ${out.dim('claude mcp list -s user')}`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const claudePath = (0, spawn_1.resolveClaudePath)();
|
|
240
|
+
if (claudePath === 'claude') {
|
|
241
|
+
out.warn('claude binary not found — falling back to project-level .mcp.json');
|
|
242
|
+
return initProject(opts.dir);
|
|
243
|
+
}
|
|
244
|
+
if ((0, mcp_1.addGlobalMcp)()) {
|
|
245
|
+
out.success('Registered claude-tempo globally (user scope)');
|
|
246
|
+
out.log(` ${out.dim('Available in all Claude Code sessions')}`);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
out.warn('Failed to register globally — falling back to project-level .mcp.json');
|
|
250
|
+
return initProject(opts.dir);
|
|
251
|
+
}
|
|
252
|
+
out.log(`\nNext steps:`);
|
|
253
|
+
out.log(` 1. Start Temporal: ${out.dim('temporal server start-dev')}`);
|
|
254
|
+
out.log(` 2. Start conductor: ${out.dim('claude-tempo conduct')}`);
|
|
255
|
+
}
|
|
256
|
+
/** Per-project .mcp.json install (legacy, used with --project flag). */
|
|
257
|
+
function initProject(dir) {
|
|
258
|
+
const mcpPath = (0, path_1.join)(dir, '.mcp.json');
|
|
227
259
|
const entry = {
|
|
228
260
|
command: 'npx',
|
|
229
261
|
args: ['claude-tempo-server'],
|
|
@@ -236,7 +268,6 @@ async function init(opts) {
|
|
|
236
268
|
out.log(` ${out.dim(mcpPath)}`);
|
|
237
269
|
return;
|
|
238
270
|
}
|
|
239
|
-
// Merge into existing config
|
|
240
271
|
existing.mcpServers = existing.mcpServers || {};
|
|
241
272
|
existing.mcpServers['claude-tempo'] = entry;
|
|
242
273
|
(0, fs_1.writeFileSync)(mcpPath, JSON.stringify(existing, null, 2) + '\n');
|
|
@@ -426,22 +457,13 @@ async function up(opts) {
|
|
|
426
457
|
}
|
|
427
458
|
// Step 3: Register search attributes
|
|
428
459
|
registerSearchAttributes(config.temporalAddress, config.temporalNamespace);
|
|
429
|
-
// Step 4:
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if ((0, fs_1.existsSync)(mcpPath)) {
|
|
433
|
-
try {
|
|
434
|
-
const mcp = JSON.parse((0, fs_1.readFileSync)(mcpPath, 'utf8'));
|
|
435
|
-
mcpExists = !!mcp?.mcpServers?.['claude-tempo'];
|
|
436
|
-
}
|
|
437
|
-
catch { /* invalid */ }
|
|
438
|
-
}
|
|
439
|
-
if (mcpExists) {
|
|
440
|
-
out.check('.mcp.json configured', true);
|
|
460
|
+
// Step 4: Register MCP server if needed
|
|
461
|
+
if ((0, mcp_1.isMcpConfigured)(process.cwd())) {
|
|
462
|
+
out.check('MCP configured', true);
|
|
441
463
|
}
|
|
442
464
|
else {
|
|
443
465
|
await init({ dir: process.cwd() });
|
|
444
|
-
out.check('
|
|
466
|
+
out.check('MCP configured', true);
|
|
445
467
|
}
|
|
446
468
|
// Always forward all resolved Temporal settings to child processes.
|
|
447
469
|
const temporalEnvVars = {
|
|
@@ -525,7 +547,9 @@ async function down(opts) {
|
|
|
525
547
|
out.warn('Could not terminate active sessions');
|
|
526
548
|
}
|
|
527
549
|
}
|
|
528
|
-
// Step 2:
|
|
550
|
+
// Step 2: Kill bridge processes via PID files
|
|
551
|
+
killBridgeProcesses();
|
|
552
|
+
// Step 3: Stop Temporal server
|
|
529
553
|
if (temporalUp) {
|
|
530
554
|
// Find and kill the temporal dev server process
|
|
531
555
|
try {
|
|
@@ -545,18 +569,26 @@ async function down(opts) {
|
|
|
545
569
|
else {
|
|
546
570
|
out.log(` ${out.dim('Temporal not running')}`);
|
|
547
571
|
}
|
|
548
|
-
// Step
|
|
572
|
+
// Step 4: Remove MCP config (global + project-level)
|
|
549
573
|
if (opts.removeMcp) {
|
|
574
|
+
// Remove global registration
|
|
575
|
+
if ((0, mcp_1.isGlobalMcpRegistered)()) {
|
|
576
|
+
if ((0, mcp_1.removeGlobalMcp)()) {
|
|
577
|
+
out.success('Removed claude-tempo from global MCP config');
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
out.warn('Could not remove global MCP entry');
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// Also remove project-level .mcp.json entry if present
|
|
550
584
|
const mcpPath = (0, path_1.join)(opts.dir, '.mcp.json');
|
|
551
585
|
if ((0, fs_1.existsSync)(mcpPath)) {
|
|
552
586
|
try {
|
|
553
587
|
const existing = JSON.parse((0, fs_1.readFileSync)(mcpPath, 'utf8'));
|
|
554
588
|
if (existing?.mcpServers?.['claude-tempo']) {
|
|
555
589
|
delete existing.mcpServers['claude-tempo'];
|
|
556
|
-
// If no other MCP servers remain, remove the file entirely
|
|
557
590
|
if (Object.keys(existing.mcpServers).length === 0) {
|
|
558
|
-
|
|
559
|
-
unlinkSync(mcpPath);
|
|
591
|
+
(0, fs_1.unlinkSync)(mcpPath);
|
|
560
592
|
out.success('Removed .mcp.json (no other servers configured)');
|
|
561
593
|
}
|
|
562
594
|
else {
|
|
@@ -564,23 +596,186 @@ async function down(opts) {
|
|
|
564
596
|
out.success('Removed claude-tempo from .mcp.json');
|
|
565
597
|
}
|
|
566
598
|
}
|
|
567
|
-
else {
|
|
568
|
-
out.log(` ${out.dim('.mcp.json has no claude-tempo entry')}`);
|
|
569
|
-
}
|
|
570
599
|
}
|
|
571
600
|
catch {
|
|
572
601
|
out.warn(`Could not update ${mcpPath}`);
|
|
573
602
|
}
|
|
574
603
|
}
|
|
575
|
-
else {
|
|
576
|
-
out.log(` ${out.dim('No .mcp.json found')}`);
|
|
577
|
-
}
|
|
578
604
|
}
|
|
579
605
|
console.log();
|
|
580
606
|
out.success('claude-tempo is shut down');
|
|
581
607
|
out.log(` ${out.dim('Temporal data preserved in ~/.claude-tempo/ (delete manually to reset)')}`);
|
|
582
608
|
console.log();
|
|
583
609
|
}
|
|
610
|
+
async function stop(opts) {
|
|
611
|
+
const config = (0, config_1.getConfig)(opts);
|
|
612
|
+
if (!opts.name && !opts.ensemble && !opts.all) {
|
|
613
|
+
out.error('Specify what to stop:');
|
|
614
|
+
out.log(` ${out.dim('claude-tempo stop <ensemble>')} Stop all sessions in an ensemble`);
|
|
615
|
+
out.log(` ${out.dim('claude-tempo stop <ensemble> -n <name>')} Stop a specific session`);
|
|
616
|
+
out.log(` ${out.dim('claude-tempo stop --all')} Stop everything`);
|
|
617
|
+
process.exit(1);
|
|
618
|
+
}
|
|
619
|
+
let connection;
|
|
620
|
+
try {
|
|
621
|
+
connection = await Promise.race([
|
|
622
|
+
(0, connection_1.createTemporalConnection)(config),
|
|
623
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
|
|
624
|
+
]);
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
out.error(`Cannot connect to Temporal at ${config.temporalAddress}`);
|
|
628
|
+
process.exit(1);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const client = new client_1.Client({ connection, namespace: config.temporalNamespace });
|
|
632
|
+
if (opts.name) {
|
|
633
|
+
// Stop a specific player by name (optionally scoped to ensemble)
|
|
634
|
+
await stopByName(client, opts.name, config, opts.ensemble);
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
// Stop multiple sessions (--ensemble or --all)
|
|
638
|
+
let query = 'WorkflowType = "claudeSessionWorkflow" AND ExecutionStatus = "Running"';
|
|
639
|
+
if (opts.ensemble) {
|
|
640
|
+
query += ` AND ClaudeTempoEnsemble = "${opts.ensemble}"`;
|
|
641
|
+
}
|
|
642
|
+
let stopped = 0;
|
|
643
|
+
for await (const wf of client.workflow.list({ query })) {
|
|
644
|
+
try {
|
|
645
|
+
const handle = client.workflow.getHandle(wf.workflowId);
|
|
646
|
+
await handle.signal(signals_1.shutdownSignal);
|
|
647
|
+
stopped++;
|
|
648
|
+
out.log(` ${out.dim('stopped')} ${wf.workflowId}`);
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
// already closed
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// Clean up PID files
|
|
655
|
+
if (opts.ensemble || opts.all) {
|
|
656
|
+
killBridgeProcesses();
|
|
657
|
+
}
|
|
658
|
+
if (stopped > 0) {
|
|
659
|
+
out.success(`Stopped ${stopped} session${stopped !== 1 ? 's' : ''}`);
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
out.log(opts.ensemble
|
|
663
|
+
? `No active sessions in ensemble "${opts.ensemble}".`
|
|
664
|
+
: 'No active sessions found.');
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
await connection.close();
|
|
668
|
+
}
|
|
669
|
+
async function stopByName(client, name, config, ensemble) {
|
|
670
|
+
// Find the workflow by player name via search attribute
|
|
671
|
+
let query = `WorkflowType = "claudeSessionWorkflow" AND ExecutionStatus = "Running" AND ClaudeTempoPlayerId = "${name}"`;
|
|
672
|
+
if (ensemble) {
|
|
673
|
+
query += ` AND ClaudeTempoEnsemble = "${ensemble}"`;
|
|
674
|
+
}
|
|
675
|
+
let found = false;
|
|
676
|
+
for await (const wf of client.workflow.list({ query })) {
|
|
677
|
+
found = true;
|
|
678
|
+
const handle = client.workflow.getHandle(wf.workflowId);
|
|
679
|
+
// Check if this is a conductor — warn about it
|
|
680
|
+
try {
|
|
681
|
+
const metadata = (await handle.query('getMetadata'));
|
|
682
|
+
if (metadata.isConductor) {
|
|
683
|
+
out.warn(`"${name}" is a conductor session`);
|
|
684
|
+
}
|
|
685
|
+
// Notify the conductor that this session was stopped (if it's not the conductor itself)
|
|
686
|
+
if (!metadata.isConductor && metadata.ensemble) {
|
|
687
|
+
try {
|
|
688
|
+
const conductorWfId = (0, config_1.conductorWorkflowId)(metadata.ensemble);
|
|
689
|
+
const conductorHandle = client.workflow.getHandle(conductorWfId);
|
|
690
|
+
await conductorHandle.signal(signals_1.playerReportSignal, {
|
|
691
|
+
playerId: name,
|
|
692
|
+
text: 'Session stopped by CLI',
|
|
693
|
+
type: 'result',
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
// No conductor or conductor not running — fine
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
// Query failed — proceed with shutdown anyway
|
|
703
|
+
}
|
|
704
|
+
// Send shutdown signal (graceful)
|
|
705
|
+
try {
|
|
706
|
+
await handle.signal(signals_1.shutdownSignal);
|
|
707
|
+
out.success(`Stopped "${name}"`);
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
out.warn(`Could not signal "${name}" — it may have already exited`);
|
|
711
|
+
}
|
|
712
|
+
// Try to kill bridge process via PID file
|
|
713
|
+
killBridgePid(name);
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
if (!found) {
|
|
717
|
+
out.error(`No active session found with name "${name}"`);
|
|
718
|
+
process.exit(1);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Kill a bridge process by reading its PID file from logs/.
|
|
723
|
+
* Cleans up the PID file after.
|
|
724
|
+
*/
|
|
725
|
+
function killBridgePid(name) {
|
|
726
|
+
const pidPath = (0, path_1.join)(process.cwd(), 'logs', `${name}.pid`);
|
|
727
|
+
if (!(0, fs_1.existsSync)(pidPath))
|
|
728
|
+
return;
|
|
729
|
+
try {
|
|
730
|
+
const pid = parseInt((0, fs_1.readFileSync)(pidPath, 'utf8').trim(), 10);
|
|
731
|
+
if (!isNaN(pid)) {
|
|
732
|
+
try {
|
|
733
|
+
process.kill(pid);
|
|
734
|
+
out.log(` ${out.dim(`Killed bridge process (pid ${pid})`)}`);
|
|
735
|
+
}
|
|
736
|
+
catch {
|
|
737
|
+
// Process already dead
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
(0, fs_1.unlinkSync)(pidPath);
|
|
741
|
+
}
|
|
742
|
+
catch {
|
|
743
|
+
// PID file unreadable — ignore
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Kill all bridge processes found in logs/*.pid and clean up PID files.
|
|
748
|
+
*/
|
|
749
|
+
function killBridgeProcesses() {
|
|
750
|
+
const logsDir = (0, path_1.join)(process.cwd(), 'logs');
|
|
751
|
+
if (!(0, fs_1.existsSync)(logsDir))
|
|
752
|
+
return;
|
|
753
|
+
try {
|
|
754
|
+
const pidFiles = (0, fs_1.readdirSync)(logsDir).filter(f => f.endsWith('.pid'));
|
|
755
|
+
for (const pidFile of pidFiles) {
|
|
756
|
+
const pidPath = (0, path_1.join)(logsDir, pidFile);
|
|
757
|
+
try {
|
|
758
|
+
const pid = parseInt((0, fs_1.readFileSync)(pidPath, 'utf8').trim(), 10);
|
|
759
|
+
if (!isNaN(pid)) {
|
|
760
|
+
try {
|
|
761
|
+
process.kill(pid);
|
|
762
|
+
out.log(` ${out.dim(`Killed bridge process ${pidFile.replace('.pid', '')} (pid ${pid})`)}`);
|
|
763
|
+
}
|
|
764
|
+
catch {
|
|
765
|
+
// already dead
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
(0, fs_1.unlinkSync)(pidPath);
|
|
769
|
+
}
|
|
770
|
+
catch {
|
|
771
|
+
// unreadable — skip
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
// logs dir unreadable
|
|
777
|
+
}
|
|
778
|
+
}
|
|
584
779
|
function help() {
|
|
585
780
|
console.log(`
|
|
586
781
|
${out.bold('claude-tempo')} — Multi-session Claude Code coordination via Temporal
|
|
@@ -597,9 +792,10 @@ ${out.bold('Commands:')}
|
|
|
597
792
|
${out.cyan('server')} Start the Temporal dev server and register search attributes
|
|
598
793
|
${out.cyan('conduct')} [ensemble] Start a conductor session (one per ensemble)
|
|
599
794
|
${out.cyan('start')} [ensemble] Start a player session
|
|
795
|
+
${out.cyan('stop')} [ensemble] Stop sessions (-n <name> for one, or --all)
|
|
600
796
|
${out.cyan('status')} [ensemble] Show active sessions and Temporal health
|
|
601
797
|
${out.cyan('config')} Configure Temporal connection settings
|
|
602
|
-
${out.cyan('init')}
|
|
798
|
+
${out.cyan('init')} Register MCP server globally (or --project for .mcp.json)
|
|
603
799
|
${out.cyan('preflight')} Run preflight checks only
|
|
604
800
|
${out.cyan('help')} Show this help message
|
|
605
801
|
|
|
@@ -612,10 +808,13 @@ ${out.bold('Connection options (all commands):')}
|
|
|
612
808
|
|
|
613
809
|
${out.bold('Other options:')}
|
|
614
810
|
-n, --name <name> Set the session window name (start/conduct/up only)
|
|
615
|
-
--agent <claude|copilot> Agent type to spawn (default:
|
|
811
|
+
--agent <claude|copilot> Agent type to spawn (default: from config; start/conduct)
|
|
616
812
|
--skip-preflight Skip preflight checks (start/conduct only)
|
|
617
813
|
--background Run Temporal in background (server only)
|
|
618
|
-
--
|
|
814
|
+
--project Use per-project .mcp.json instead of global (init only)
|
|
815
|
+
--keep-mcp Don't remove MCP config (down only)
|
|
816
|
+
--all Stop all sessions (stop only)
|
|
817
|
+
--ensemble <name> Target a specific ensemble (stop only)
|
|
619
818
|
-d, --dir <path> Target directory (default: cwd)
|
|
620
819
|
|
|
621
820
|
${out.bold('Config command:')}
|
|
@@ -646,6 +845,7 @@ ${out.bold('Environment:')}
|
|
|
646
845
|
TEMPORAL_API_KEY Temporal API key
|
|
647
846
|
TEMPORAL_TLS_CERT_PATH Path to TLS client certificate
|
|
648
847
|
TEMPORAL_TLS_KEY_PATH Path to TLS client key
|
|
848
|
+
CLAUDE_TEMPO_DEFAULT_AGENT Default agent type: claude or copilot (fallback: claude)
|
|
649
849
|
`);
|
|
650
850
|
}
|
|
651
851
|
function version() {
|
|
@@ -126,6 +126,12 @@ async function configInteractive() {
|
|
|
126
126
|
config.temporalTlsCertPath = await ask('TLS cert path', existing.temporalTlsCertPath);
|
|
127
127
|
config.temporalTlsKeyPath = await ask('TLS key path', existing.temporalTlsKeyPath);
|
|
128
128
|
}
|
|
129
|
+
// Default agent type
|
|
130
|
+
const agentChoice = await choose('Default agent', ['claude', 'copilot']);
|
|
131
|
+
if (agentChoice === 'copilot') {
|
|
132
|
+
config.defaultAgent = 'copilot';
|
|
133
|
+
}
|
|
134
|
+
// Don't set defaultAgent if claude — it's the default, keeps config clean
|
|
129
135
|
(0, config_1.saveConfigFile)(config);
|
|
130
136
|
out.success(`Saved to ${config_1.CONFIG_FILE_PATH}`);
|
|
131
137
|
// Test connection
|
|
@@ -165,6 +171,8 @@ function configSet(key, value) {
|
|
|
165
171
|
temporalTlsKeyPath: 'temporalTlsKeyPath',
|
|
166
172
|
'temporal-tls-key': 'temporalTlsKeyPath',
|
|
167
173
|
'temporal-tls-key-path': 'temporalTlsKeyPath',
|
|
174
|
+
defaultAgent: 'defaultAgent',
|
|
175
|
+
'default-agent': 'defaultAgent',
|
|
168
176
|
};
|
|
169
177
|
const configKey = keyMap[key];
|
|
170
178
|
if (!configKey) {
|
|
@@ -172,6 +180,11 @@ function configSet(key, value) {
|
|
|
172
180
|
out.log(` Valid keys: ${Object.keys(keyMap).join(', ')}`);
|
|
173
181
|
process.exit(1);
|
|
174
182
|
}
|
|
183
|
+
// Validate agent type
|
|
184
|
+
if (configKey === 'defaultAgent' && value !== 'claude' && value !== 'copilot') {
|
|
185
|
+
out.error(`Invalid agent type: "${value}". Must be "claude" or "copilot".`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
175
188
|
config[configKey] = value;
|
|
176
189
|
(0, config_1.saveConfigFile)(config);
|
|
177
190
|
out.success(`Set ${configKey} = ${configKey.includes('Key') ? '****' : value}`);
|
|
@@ -186,6 +199,7 @@ function configShow() {
|
|
|
186
199
|
{ key: 'temporalApiKey', configKey: 'temporalApiKey' },
|
|
187
200
|
{ key: 'temporalTlsCertPath', configKey: 'temporalTlsCertPath' },
|
|
188
201
|
{ key: 'temporalTlsKeyPath', configKey: 'temporalTlsKeyPath' },
|
|
202
|
+
{ key: 'defaultAgent', configKey: 'defaultAgent' },
|
|
189
203
|
];
|
|
190
204
|
out.log(` Config file: ${out.dim(config_1.CONFIG_FILE_PATH)}`);
|
|
191
205
|
console.log();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Check if claude-tempo is registered in `claude mcp list` (global user scope). */
|
|
2
|
+
export declare function isGlobalMcpRegistered(): boolean;
|
|
3
|
+
/** Register claude-tempo globally via `claude mcp add`. */
|
|
4
|
+
export declare function addGlobalMcp(): boolean;
|
|
5
|
+
/** Remove claude-tempo from global MCP config via `claude mcp remove`. */
|
|
6
|
+
export declare function removeGlobalMcp(): boolean;
|
|
7
|
+
/** Check if claude-tempo MCP is configured (global or project-level). */
|
|
8
|
+
export declare function isMcpConfigured(projectDir: string): boolean;
|
package/dist/cli/mcp.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isGlobalMcpRegistered = isGlobalMcpRegistered;
|
|
4
|
+
exports.addGlobalMcp = addGlobalMcp;
|
|
5
|
+
exports.removeGlobalMcp = removeGlobalMcp;
|
|
6
|
+
exports.isMcpConfigured = isMcpConfigured;
|
|
7
|
+
const fs_1 = require("fs");
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const path_1 = require("path");
|
|
10
|
+
/** Check if claude-tempo is registered in `claude mcp list` (global user scope). */
|
|
11
|
+
function isGlobalMcpRegistered() {
|
|
12
|
+
try {
|
|
13
|
+
const output = (0, child_process_1.execFileSync)('claude', ['mcp', 'list', '-s', 'user'], {
|
|
14
|
+
encoding: 'utf8',
|
|
15
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
16
|
+
});
|
|
17
|
+
return output.includes('claude-tempo');
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Register claude-tempo globally via `claude mcp add`. */
|
|
24
|
+
function addGlobalMcp() {
|
|
25
|
+
try {
|
|
26
|
+
(0, child_process_1.execFileSync)('claude', [
|
|
27
|
+
'mcp', 'add', 'claude-tempo', '-s', 'user',
|
|
28
|
+
'--', 'npx', 'claude-tempo-server',
|
|
29
|
+
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Remove claude-tempo from global MCP config via `claude mcp remove`. */
|
|
37
|
+
function removeGlobalMcp() {
|
|
38
|
+
try {
|
|
39
|
+
(0, child_process_1.execFileSync)('claude', [
|
|
40
|
+
'mcp', 'remove', 'claude-tempo', '-s', 'user',
|
|
41
|
+
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Check if claude-tempo MCP is configured (global or project-level). */
|
|
49
|
+
function isMcpConfigured(projectDir) {
|
|
50
|
+
if (isGlobalMcpRegistered())
|
|
51
|
+
return true;
|
|
52
|
+
const mcpPath = (0, path_1.join)(projectDir, '.mcp.json');
|
|
53
|
+
if ((0, fs_1.existsSync)(mcpPath)) {
|
|
54
|
+
try {
|
|
55
|
+
const mcp = JSON.parse((0, fs_1.readFileSync)(mcpPath, 'utf8'));
|
|
56
|
+
return !!mcp?.mcpServers?.['claude-tempo'];
|
|
57
|
+
}
|
|
58
|
+
catch { /* invalid json */ }
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|