claude-tempo 0.3.0 → 0.4.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/README.md CHANGED
@@ -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:
@@ -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
+ }
@@ -34,12 +34,11 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.runPreflight = runPreflight;
37
- const fs_1 = require("fs");
38
37
  const child_process_1 = require("child_process");
39
- const path_1 = require("path");
40
38
  const config_1 = require("../config");
41
39
  const connection_1 = require("../connection");
42
40
  const spawn_1 = require("../spawn");
41
+ const mcp_1 = require("./mcp");
43
42
  const out = __importStar(require("./output"));
44
43
  function whichSync(cmd) {
45
44
  const bin = process.platform === 'win32' ? 'where' : 'which';
@@ -87,19 +86,11 @@ async function runPreflight(opts) {
87
86
  out.check('claude-tempo-server found', serverOk, serverOk ? serverPath : 'not on PATH');
88
87
  if (!serverOk)
89
88
  errors.push('claude-tempo-server not found on PATH. Run: npm install -g claude-tempo');
90
- // 5. MCP config in project
91
- const mcpPath = (0, path_1.join)(opts.dir, '.mcp.json');
92
- let mcpOk = false;
93
- if ((0, fs_1.existsSync)(mcpPath)) {
94
- try {
95
- const mcp = JSON.parse((0, fs_1.readFileSync)(mcpPath, 'utf8'));
96
- mcpOk = !!mcp?.mcpServers?.['claude-tempo'];
97
- }
98
- catch { /* invalid json */ }
99
- }
100
- out.check('.mcp.json configured', mcpOk, mcpOk ? mcpPath : 'missing or no claude-tempo entry');
89
+ // 5. MCP config (global or project-level)
90
+ const mcpOk = (0, mcp_1.isMcpConfigured)(opts.dir);
91
+ out.check('MCP configured', mcpOk, mcpOk ? 'global or project' : 'not found');
101
92
  if (!mcpOk)
102
- warnings.push(`No claude-tempo MCP config in ${opts.dir}. Run: claude-tempo init`);
93
+ warnings.push('No claude-tempo MCP config found. Run: claude-tempo init');
103
94
  console.log();
104
95
  return { ok: errors.length === 0, errors, warnings };
105
96
  }
package/dist/cli.js CHANGED
@@ -47,6 +47,8 @@ function parseArgs(argv) {
47
47
  skipPreflight: false,
48
48
  background: false,
49
49
  keepMcp: false,
50
+ all: false,
51
+ project: false,
50
52
  };
51
53
  let i = 0;
52
54
  while (i < argv.length) {
@@ -81,6 +83,15 @@ function parseArgs(argv) {
81
83
  else if (arg === '--keep-mcp') {
82
84
  result.keepMcp = true;
83
85
  }
86
+ else if (arg === '--all') {
87
+ result.all = true;
88
+ }
89
+ else if (arg === '--project') {
90
+ result.project = true;
91
+ }
92
+ else if (arg === '--ensemble' && i + 1 < argv.length) {
93
+ result.ensemble = argv[++i];
94
+ }
84
95
  else if (arg === '--agent' && i + 1 < argv.length) {
85
96
  const val = argv[++i];
86
97
  if (val !== 'claude' && val !== 'copilot') {
@@ -124,6 +135,8 @@ async function main() {
124
135
  const args = parseArgs(process.argv.slice(2));
125
136
  const ensemble = args.positional[1] || process.env[config_1.ENV.ENSEMBLE] || 'default';
126
137
  const overrides = cliOverrides(args);
138
+ // Resolve the default agent from config (only needed for commands that use it)
139
+ const resolvedAgent = () => args.agent ?? (0, config_1.getConfig)(overrides).defaultAgent;
127
140
  switch (args.command) {
128
141
  case 'conduct':
129
142
  await (0, commands_1.start)({
@@ -131,7 +144,7 @@ async function main() {
131
144
  conductor: true,
132
145
  name: args.name,
133
146
  skipPreflight: args.skipPreflight,
134
- agent: args.agent ?? 'claude',
147
+ agent: resolvedAgent(),
135
148
  dir: args.dir,
136
149
  ...overrides,
137
150
  });
@@ -142,7 +155,7 @@ async function main() {
142
155
  conductor: false,
143
156
  name: args.name,
144
157
  skipPreflight: args.skipPreflight,
145
- agent: args.agent ?? 'claude',
158
+ agent: resolvedAgent(),
146
159
  dir: args.dir,
147
160
  ...overrides,
148
161
  });
@@ -159,6 +172,14 @@ async function main() {
159
172
  ...overrides,
160
173
  });
161
174
  break;
175
+ case 'stop':
176
+ await (0, commands_1.stop)({
177
+ name: args.name,
178
+ ensemble: args.positional[1],
179
+ all: args.all || undefined,
180
+ ...overrides,
181
+ });
182
+ break;
162
183
  case 'down':
163
184
  await (0, commands_1.down)({
164
185
  removeMcp: !args.keepMcp,
@@ -170,12 +191,12 @@ async function main() {
170
191
  await (0, commands_1.up)({
171
192
  ensemble,
172
193
  name: args.name,
173
- agent: args.agent ?? 'claude',
194
+ agent: resolvedAgent(),
174
195
  ...overrides,
175
196
  });
176
197
  break;
177
198
  case 'init':
178
- await (0, commands_1.init)({ dir: args.dir });
199
+ await (0, commands_1.init)({ dir: args.dir, project: args.project });
179
200
  break;
180
201
  case 'config':
181
202
  await (0, config_command_1.configCommand)(args.positional);