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 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. Create `.mcp.json` in your project
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
- # In your project directory, create .mcp.json
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
- | `init` | Create `.mcp.json` config in the current directory |
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
- Creates `.mcp.json` in the current directory (or merges into an existing one):
184
-
185
- ```json
186
- {
187
- "mcpServers": {
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 remain running until the 24-hour timeout
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 Claude conductor or custom Temporal client to receive work via `cue`.
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
- npm install @github/copilot-sdk # optional dependency (~243MB)
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
- Also requires [GitHub Copilot CLI](https://docs.github.com/en/copilot/github-copilot-in-the-cli) installed, authenticated, with an active subscription. Node 20+ required for Copilot features.
393
+ Or recruit from within any active session:
385
394
 
386
- ### Starting a Copilot player
395
+ > "Recruit a copilot session named 'copilot-dev' in /repos/my-project with agent copilot"
387
396
 
388
- The easiest way is via `recruit` from any active session:
397
+ ### Setting a default agent
389
398
 
390
- > "Recruit a copilot session named 'copilot-dev' in /repos/my-project with agent copilot"
399
+ To avoid passing `--agent copilot` every time:
391
400
 
392
- Or start the bridge directly:
401
+ ```bash
402
+ claude-tempo config set default-agent copilot
403
+ ```
404
+
405
+ Or via environment variable:
393
406
 
394
407
  ```bash
395
- CLAUDE_TEMPO_ENSEMBLE=default COPILOT_BRIDGE_NAME=copilot-dev npx ts-node src/copilot-bridge.ts
408
+ export CLAUDE_TEMPO_DEFAULT_AGENT=copilot
396
409
  ```
397
410
 
398
- ### How it works
411
+ Resolution order: `--agent` flag → `CLAUDE_TEMPO_DEFAULT_AGENT` env → config file → `claude`.
399
412
 
400
- 1. Bridge spawns a Copilot CLI session via the SDK with claude-tempo as MCP server
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
- ### Copilot environment variables
415
+ Set `COPILOT_BRIDGE_MODEL` to use a specific model for Copilot sessions:
407
416
 
408
- | Variable | Default | Description |
409
- |----------|---------|-------------|
410
- | `COPILOT_BRIDGE_NAME` | *(none)* | Player name |
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
- - No interactive access — bridge sessions only respond to cues
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
 
@@ -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 {};
@@ -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
- const mcpPath = (0, path_1.join)(opts.dir, '.mcp.json');
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: Init .mcp.json if needed
430
- const mcpPath = (0, path_1.join)(process.cwd(), '.mcp.json');
431
- let mcpExists = false;
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('.mcp.json created', true);
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: Stop Temporal server
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 3: Remove .mcp.json entry
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
- const { unlinkSync } = require('fs');
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')} Create .mcp.json config in the current directory
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: claude; start/conduct)
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
- --keep-mcp Don't remove .mcp.json entry (down only)
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;
@@ -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
+ }