claude-tempo 0.20.0 → 0.21.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.
@@ -178,6 +178,8 @@ async function start(opts) {
178
178
  temporalEnvVars[config_1.ENV.TEMPORAL_TLS_CERT_PATH] = config.temporalTlsCertPath;
179
179
  if (config.temporalTlsKeyPath)
180
180
  temporalEnvVars[config_1.ENV.TEMPORAL_TLS_KEY_PATH] = config.temporalTlsKeyPath;
181
+ if (config.claudeBin)
182
+ temporalEnvVars[config_1.ENV.CLAUDE_BIN] = config.claudeBin;
181
183
  if (opts.agent === 'copilot') {
182
184
  const { pid } = (0, spawn_1.spawnCopilotBridge)({
183
185
  name: opts.name || `copilot-${Date.now()}`,
@@ -212,7 +214,7 @@ async function start(opts) {
212
214
  [config_1.ENV.CONDUCTOR]: opts.conductor ? 'true' : '',
213
215
  [config_1.ENV.PLAYER_NAME]: sessionName || '',
214
216
  };
215
- const { pid } = (0, spawn_1.spawnInTerminal)(claudeArgs, workDir, envVars);
217
+ const { pid } = (0, spawn_1.spawnInTerminal)(claudeArgs, workDir, envVars, { claudeBin: config.claudeBin });
216
218
  out.success(`Launched ${role} session${sessionName ? ` "${sessionName}"` : ''} (pid ${pid ?? 'unknown'})`);
217
219
  }
218
220
  out.log(` Ensemble: ${opts.ensemble}`);
@@ -668,13 +670,93 @@ async function up(opts) {
668
670
  }
669
671
  // Resolve conductor agent from lineup or CLI flags
670
672
  const conductorAgent = lineup?.conductor?.agent === 'copilot' ? 'copilot' : opts.agent;
671
- // Step 5: Connect to Temporal and pre-create conductor workflow before spawning
673
+ // Step 5: Connect to Temporal and check for existing conductor
672
674
  console.log();
673
- out.log(`Launching conductor in ensemble ${out.cyan(opts.ensemble)}${conductorAgent === 'copilot' ? out.dim(' (copilot)') : ''}...`);
674
675
  const connection = await (0, connection_1.createTemporalConnection)(config);
675
676
  const client = new client_1.Client({ connection, namespace: config.temporalNamespace });
676
- const sessionName = opts.name || lineup?.conductor?.name || (conductorAgent === 'copilot' ? `${opts.ensemble}-conductor` : 'conductor');
677
677
  const conductorWfId = (0, config_1.conductorWorkflowId)(opts.ensemble);
678
+ // Check if a conductor is already running
679
+ try {
680
+ const existingHandle = client.workflow.getHandle(conductorWfId);
681
+ const desc = await existingHandle.describe();
682
+ if (desc.status.name === 'RUNNING') {
683
+ if (!process.stdin.isTTY) {
684
+ out.error(`A conductor is already running for ensemble "${opts.ensemble}".`);
685
+ out.log(` Use ${out.dim('--resume')} to reconnect, or ${out.dim('claude-tempo start')} to join as a player.`);
686
+ process.exit(1);
687
+ }
688
+ out.warn(`A conductor is already running for ensemble "${opts.ensemble}".`);
689
+ console.log();
690
+ out.log(` 1) Join as a new player session`);
691
+ out.log(` 2) Reconnect to the existing conductor (--resume)`);
692
+ out.log(` 3) Tear down and start fresh`);
693
+ out.log(` 4) Cancel`);
694
+ console.log();
695
+ const choice = await new Promise((res) => {
696
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
697
+ rl.question(` ${out.cyan('?')} Choose an option [1-4]: `, (answer) => {
698
+ rl.close();
699
+ res(answer.trim());
700
+ });
701
+ });
702
+ switch (choice) {
703
+ case '1':
704
+ // Join as a player — delegate to start()
705
+ console.log();
706
+ out.log('Joining as a player session...');
707
+ await start({
708
+ ensemble: opts.ensemble,
709
+ conductor: false,
710
+ name: opts.name,
711
+ skipPreflight: true, // infrastructure already verified above
712
+ agent: opts.agent,
713
+ dir: process.cwd(),
714
+ });
715
+ return;
716
+ case '2':
717
+ // Reconnect to existing conductor
718
+ console.log();
719
+ out.log('Reconnecting to existing conductor...');
720
+ await start({
721
+ ensemble: opts.ensemble,
722
+ conductor: true,
723
+ resume: true,
724
+ name: opts.name,
725
+ skipPreflight: true,
726
+ agent: opts.agent,
727
+ dir: process.cwd(),
728
+ });
729
+ return;
730
+ case '3':
731
+ // Terminate existing workflows, then fall through to normal up flow
732
+ console.log();
733
+ try {
734
+ await client.workflow.getHandle(conductorWfId).terminate('up: fresh start');
735
+ }
736
+ catch { /* may not exist */ }
737
+ try {
738
+ await client.workflow.getHandle((0, config_1.schedulerWorkflowId)(opts.ensemble)).terminate('up: fresh start');
739
+ }
740
+ catch { /* may not exist */ }
741
+ try {
742
+ await client.workflow.getHandle((0, config_1.maestroWorkflowId)(opts.ensemble)).terminate('up: fresh start');
743
+ }
744
+ catch { /* may not exist */ }
745
+ out.success('Existing ensemble torn down');
746
+ // Fall through to normal up flow below
747
+ break;
748
+ case '4':
749
+ default:
750
+ out.log('Cancelled.');
751
+ process.exit(0);
752
+ }
753
+ }
754
+ }
755
+ catch {
756
+ // No existing conductor — proceed normally
757
+ }
758
+ out.log(`Launching conductor in ensemble ${out.cyan(opts.ensemble)}${conductorAgent === 'copilot' ? out.dim(' (copilot)') : ''}...`);
759
+ const sessionName = opts.name || lineup?.conductor?.name || (conductorAgent === 'copilot' ? `${opts.ensemble}-conductor` : 'conductor');
678
760
  // Resolve conductor agent type from lineup
679
761
  const conductorType = lineup?.conductor?.agent && lineup.conductor.agent !== 'default' && lineup.conductor.agent !== 'copilot'
680
762
  ? lineup.conductor.agent
@@ -761,7 +843,7 @@ async function up(opts) {
761
843
  if (resolvedConductorType || conductorTypeName) {
762
844
  conductorEnvVars[config_1.ENV.PLAYER_TYPE] = resolvedConductorType?.name || conductorTypeName || '';
763
845
  }
764
- ({ pid } = (0, spawn_1.spawnInTerminal)(claudeArgs, process.cwd(), conductorEnvVars));
846
+ ({ pid } = (0, spawn_1.spawnInTerminal)(claudeArgs, process.cwd(), conductorEnvVars, { claudeBin: config.claudeBin }));
765
847
  }
766
848
  out.success(`Conductor launched (pid ${pid ?? 'unknown'})`);
767
849
  // Step 6: If lineup provided, recruit players and create schedules
@@ -866,7 +948,7 @@ async function up(opts) {
866
948
  if (resolvedPlayerType) {
867
949
  playerEnvVars[config_1.ENV.PLAYER_TYPE] = resolvedPlayerType.name;
868
950
  }
869
- (0, spawn_1.spawnInTerminal)(claudeArgs, playerWorkDir, playerEnvVars);
951
+ (0, spawn_1.spawnInTerminal)(claudeArgs, playerWorkDir, playerEnvVars, { claudeBin: config.claudeBin });
870
952
  }
871
953
  out.log(` ${out.green('ok')} ${out.bold(player.name)} in ${playerWorkDir}`);
872
954
  }
@@ -1685,7 +1767,9 @@ async function encore(opts) {
1685
1767
  envVars[config_1.ENV.TEMPORAL_TLS_CERT_PATH] = config.temporalTlsCertPath;
1686
1768
  if (config.temporalTlsKeyPath)
1687
1769
  envVars[config_1.ENV.TEMPORAL_TLS_KEY_PATH] = config.temporalTlsKeyPath;
1688
- const { pid } = (0, spawn_1.spawnInTerminal)(spawnArgs, targetMeta.workDir, envVars);
1770
+ if (config.claudeBin)
1771
+ envVars[config_1.ENV.CLAUDE_BIN] = config.claudeBin;
1772
+ const { pid } = (0, spawn_1.spawnInTerminal)(spawnArgs, targetMeta.workDir, envVars, { claudeBin: config.claudeBin });
1689
1773
  out.success(`Encore! "${opts.name}" revived (pid ${pid})`);
1690
1774
  await connection.close();
1691
1775
  }
@@ -173,6 +173,8 @@ function configSet(key, value) {
173
173
  'temporal-tls-key-path': 'temporalTlsKeyPath',
174
174
  defaultAgent: 'defaultAgent',
175
175
  'default-agent': 'defaultAgent',
176
+ claudeBin: 'claudeBin',
177
+ 'claude-bin': 'claudeBin',
176
178
  };
177
179
  const configKey = keyMap[key];
178
180
  if (!configKey) {
@@ -200,6 +202,7 @@ function configShow() {
200
202
  { key: 'temporalTlsCertPath', configKey: 'temporalTlsCertPath' },
201
203
  { key: 'temporalTlsKeyPath', configKey: 'temporalTlsKeyPath' },
202
204
  { key: 'defaultAgent', configKey: 'defaultAgent' },
205
+ { key: 'claudeBin', configKey: 'claudeBin' },
203
206
  ];
204
207
  out.log(` Config file: ${out.dim(config_1.CONFIG_FILE_PATH)}`);
205
208
  console.log();
package/dist/config.d.ts CHANGED
@@ -15,6 +15,7 @@ export declare const ENV: {
15
15
  readonly TEMPORAL_TLS_KEY_PATH: "TEMPORAL_TLS_KEY_PATH";
16
16
  readonly DEFAULT_AGENT: "CLAUDE_TEMPO_DEFAULT_AGENT";
17
17
  readonly PLAYER_TYPE: "CLAUDE_TEMPO_PLAYER_TYPE";
18
+ readonly CLAUDE_BIN: "CLAUDE_TEMPO_CLAUDE_BIN";
18
19
  };
19
20
  export interface Config {
20
21
  temporalAddress: string;
@@ -23,6 +24,7 @@ export interface Config {
23
24
  temporalTlsCertPath?: string;
24
25
  temporalTlsKeyPath?: string;
25
26
  defaultAgent: AgentType;
27
+ claudeBin?: string;
26
28
  taskQueue: string;
27
29
  ensemble: string;
28
30
  }
@@ -34,6 +36,7 @@ export interface PersistedConfig {
34
36
  temporalTlsCertPath?: string;
35
37
  temporalTlsKeyPath?: string;
36
38
  defaultAgent?: AgentType;
39
+ claudeBin?: string;
37
40
  }
38
41
  export declare const CLAUDE_TEMPO_HOME: string;
39
42
  export declare const CONFIG_FILE_PATH: string;
package/dist/config.js CHANGED
@@ -36,6 +36,7 @@ exports.ENV = {
36
36
  TEMPORAL_TLS_KEY_PATH: 'TEMPORAL_TLS_KEY_PATH',
37
37
  DEFAULT_AGENT: 'CLAUDE_TEMPO_DEFAULT_AGENT',
38
38
  PLAYER_TYPE: 'CLAUDE_TEMPO_PLAYER_TYPE',
39
+ CLAUDE_BIN: 'CLAUDE_TEMPO_CLAUDE_BIN',
39
40
  };
40
41
  exports.CLAUDE_TEMPO_HOME = (0, path_1.join)((0, os_1.homedir)(), '.claude-tempo');
41
42
  exports.CONFIG_FILE_PATH = (0, path_1.join)(exports.CLAUDE_TEMPO_HOME, 'config.json');
@@ -192,6 +193,7 @@ function getConfig(overrides = {}) {
192
193
  defaultAgent: validAgent(overrides.defaultAgent
193
194
  || process.env[exports.ENV.DEFAULT_AGENT]
194
195
  || configFile.defaultAgent),
196
+ claudeBin: process.env[exports.ENV.CLAUDE_BIN] || configFile.claudeBin || undefined,
195
197
  taskQueue: process.env[exports.ENV.TASK_QUEUE] ?? 'claude-tempo',
196
198
  ensemble: process.env[exports.ENV.ENSEMBLE] ?? 'default',
197
199
  };
@@ -227,6 +229,7 @@ function getConfigWithSources(overrides = {}) {
227
229
  const tlsCert = resolveWithSource('temporalTlsCertPath', overrides.temporalTlsCertPath, exports.ENV.TEMPORAL_TLS_CERT_PATH, configFile.temporalTlsCertPath, temporalCli.temporalTlsCertPath);
228
230
  const tlsKey = resolveWithSource('temporalTlsKeyPath', overrides.temporalTlsKeyPath, exports.ENV.TEMPORAL_TLS_KEY_PATH, configFile.temporalTlsKeyPath, temporalCli.temporalTlsKeyPath);
229
231
  const defaultAgent = resolveWithSource('defaultAgent', overrides.defaultAgent, exports.ENV.DEFAULT_AGENT, configFile.defaultAgent, undefined, 'claude');
232
+ const claudeBin = resolveWithSource('claudeBin', undefined, exports.ENV.CLAUDE_BIN, configFile.claudeBin, undefined);
230
233
  return {
231
234
  config: {
232
235
  temporalAddress: address.value,
@@ -235,6 +238,7 @@ function getConfigWithSources(overrides = {}) {
235
238
  temporalTlsCertPath: tlsCert.value,
236
239
  temporalTlsKeyPath: tlsKey.value,
237
240
  defaultAgent: validAgent(defaultAgent.value),
241
+ claudeBin: claudeBin.value,
238
242
  taskQueue: process.env[exports.ENV.TASK_QUEUE] ?? 'claude-tempo',
239
243
  ensemble: process.env[exports.ENV.ENSEMBLE] ?? 'default',
240
244
  },
@@ -245,6 +249,7 @@ function getConfigWithSources(overrides = {}) {
245
249
  temporalTlsCertPath: tlsCert.source,
246
250
  temporalTlsKeyPath: tlsKey.source,
247
251
  defaultAgent: defaultAgent.source,
252
+ claudeBin: claudeBin.source,
248
253
  },
249
254
  };
250
255
  }
package/dist/spawn.d.ts CHANGED
@@ -10,8 +10,17 @@ export declare function resolveIconPath(): string;
10
10
  export declare function ensureWindowsTerminalProfile(): boolean;
11
11
  /** POSIX shell-safe single-quoting (works in bash, zsh, and fish) */
12
12
  export declare function shellQuote(s: string): string;
13
- /** Resolve the absolute path to the `claude` binary */
14
- export declare function resolveClaudePath(): string;
13
+ /**
14
+ * Resolve the path to the `claude` binary.
15
+ *
16
+ * Resolution order:
17
+ * 1. `configBin` parameter (from Config.claudeBin — env var or config file)
18
+ * 2. `CLAUDE_TEMPO_CLAUDE_BIN` env var (checked directly for spawned processes that
19
+ * may not have full config resolution, e.g., activities)
20
+ * 3. `which claude` / `where claude` lookup
21
+ * 4. Bare `claude` fallback
22
+ */
23
+ export declare function resolveClaudePath(configBin?: string): string;
15
24
  /**
16
25
  * Detect the macOS terminal the user is actually running in.
17
26
  *
@@ -38,7 +47,9 @@ export declare function buildClaudeCommand(claudeBin: string, claudeArgs: string
38
47
  * - Windows: shell:true with env vars
39
48
  * - Linux: terminal emulator with -e flag
40
49
  */
41
- export declare function spawnInTerminal(claudeArgs: string[], workDir: string, envVars: Record<string, string>): {
50
+ export declare function spawnInTerminal(claudeArgs: string[], workDir: string, envVars: Record<string, string>, options?: {
51
+ claudeBin?: string;
52
+ }): {
42
53
  pid: number | undefined;
43
54
  };
44
55
  export interface CopilotBridgeOpts {
package/dist/spawn.js CHANGED
@@ -130,8 +130,25 @@ function ensureWindowsTerminalProfile() {
130
130
  function shellQuote(s) {
131
131
  return `'${s.replace(/'/g, "'\\''")}'`;
132
132
  }
133
- /** Resolve the absolute path to the `claude` binary */
134
- function resolveClaudePath() {
133
+ /**
134
+ * Resolve the path to the `claude` binary.
135
+ *
136
+ * Resolution order:
137
+ * 1. `configBin` parameter (from Config.claudeBin — env var or config file)
138
+ * 2. `CLAUDE_TEMPO_CLAUDE_BIN` env var (checked directly for spawned processes that
139
+ * may not have full config resolution, e.g., activities)
140
+ * 3. `which claude` / `where claude` lookup
141
+ * 4. Bare `claude` fallback
142
+ */
143
+ function resolveClaudePath(configBin) {
144
+ // Priority 1: explicit config value
145
+ if (configBin)
146
+ return configBin;
147
+ // Priority 2: env var (may be set by parent process)
148
+ const envBin = process.env.CLAUDE_TEMPO_CLAUDE_BIN;
149
+ if (envBin)
150
+ return envBin;
151
+ // Priority 3: which/where lookup
135
152
  const cmd = process.platform === 'win32' ? 'where' : 'which';
136
153
  try {
137
154
  return (0, child_process_1.execFileSync)(cmd, ['claude'], { encoding: 'utf8' }).trim().split('\n')[0];
@@ -205,8 +222,10 @@ function buildClaudeCommand(claudeBin, claudeArgs, envVars) {
205
222
  const envInline = Object.entries(envVars)
206
223
  .map(([k, v]) => `${k}=${shellQuote(v)}`)
207
224
  .join(' ');
225
+ // Quote the binary path if it contains spaces (e.g., "C:\Program Files\...")
226
+ const quotedBin = claudeBin.includes(' ') ? shellQuote(claudeBin) : claudeBin;
208
227
  const args = claudeArgs.map(a => shellQuote(a)).join(' ');
209
- return envInline ? `${envInline} ${claudeBin} ${args}` : `${claudeBin} ${args}`;
228
+ return envInline ? `${envInline} ${quotedBin} ${args}` : `${quotedBin} ${args}`;
210
229
  }
211
230
  /**
212
231
  * Spawn a Claude Code session in a visible terminal window.
@@ -218,8 +237,8 @@ function buildClaudeCommand(claudeBin, claudeArgs, envVars) {
218
237
  * - Windows: shell:true with env vars
219
238
  * - Linux: terminal emulator with -e flag
220
239
  */
221
- function spawnInTerminal(claudeArgs, workDir, envVars) {
222
- const claudeBin = resolveClaudePath();
240
+ function spawnInTerminal(claudeArgs, workDir, envVars, options) {
241
+ const claudeBin = resolveClaudePath(options?.claudeBin);
223
242
  const claudeInvocation = buildClaudeCommand(claudeBin, claudeArgs, envVars);
224
243
  if (process.platform === 'darwin') {
225
244
  const detected = detectMacTerminal();
@@ -308,7 +327,9 @@ function spawnInTerminal(claudeArgs, workDir, envVars) {
308
327
  const setCmds = Object.entries(envVars)
309
328
  .map(([k, v]) => `set "${k}=${cmdEscape(v)}"`)
310
329
  .join(' && ');
311
- const claudeCmd = `${cmdEscape(claudeBin)} ${claudeArgs.map(a => `"${cmdEscape(a)}"`).join(' ')}`;
330
+ // Quote the binary path if it contains spaces (e.g., "C:\Program Files\...")
331
+ const quotedWinBin = claudeBin.includes(' ') ? `"${cmdEscape(claudeBin)}"` : cmdEscape(claudeBin);
332
+ const claudeCmd = `${quotedWinBin} ${claudeArgs.map(a => `"${cmdEscape(a)}"`).join(' ')}`;
312
333
  const innerCmd = setCmds
313
334
  ? `${setCmds} && ${claudeCmd}`
314
335
  : claudeCmd;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-tempo",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "MCP server for multi-session Claude Code coordination via Temporal",
5
5
  "keywords": [
6
6
  "mcp",