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.
- package/dist/cli/commands.js +91 -7
- package/dist/cli/config-command.js +3 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +5 -0
- package/dist/spawn.d.ts +14 -3
- package/dist/spawn.js +27 -6
- package/package.json +1 -1
package/dist/cli/commands.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
/**
|
|
14
|
-
|
|
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
|
-
/**
|
|
134
|
-
|
|
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} ${
|
|
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
|
-
|
|
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;
|