@love-moon/conductor-cli 0.2.30 → 0.2.32

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.
@@ -257,7 +257,8 @@ async function main() {
257
257
  "# envs:",
258
258
  "# http_proxy: http://127.0.0.1:7890",
259
259
  "# https_proxy: http://127.0.0.1:7890",
260
- "# all_proxy: socks5://127.0.0.1:7890"
260
+ "# all_proxy: socks5://127.0.0.1:7890",
261
+ "# AISDK_PROVIDER_PATH: path-to-private-aisdk-provider.js"
261
262
  );
262
263
 
263
264
  fs.writeFileSync(CONFIG_FILE, lines.join("\n"), "utf-8");
@@ -26,7 +26,13 @@ import {
26
26
  resolveSessionRunDirectory as resolveCliSessionRunDirectory,
27
27
  resumeProviderForBackend as resumeProviderForCliBackend,
28
28
  } from "../src/fire/resume.js";
29
- import { filterRuntimeSupportedAllowCliList, normalizeRuntimeBackendName } from "../src/runtime-backends.js";
29
+ import {
30
+ filterRuntimeSupportedAllowCliList,
31
+ RUNTIME_SUPPORTED_BACKENDS,
32
+ listRuntimeSupportedBackends,
33
+ normalizeRuntimeBackendAlias,
34
+ normalizeRuntimeBackendName,
35
+ } from "../src/runtime-backends.js";
30
36
 
31
37
  const __filename = fileURLToPath(import.meta.url);
32
38
  const __dirname = path.dirname(__filename);
@@ -34,7 +40,19 @@ const PKG_ROOT = path.join(__dirname, "..");
34
40
  const INITIAL_CLI_PROJECT_PATH = process.cwd();
35
41
  const FIRE_LOG_PATH = path.join(INITIAL_CLI_PROJECT_PATH, "conductor.log");
36
42
  const FIRE_TASK_MARKER_PREFIX = "active-fire";
37
- const ENABLE_FIRE_LOCAL_LOG = !process.env.CONDUCTOR_CLI_COMMAND;
43
+
44
+ export function isLaunchedByDaemon(env = process.env) {
45
+ const daemonHostedValue =
46
+ typeof env?.CONDUCTOR_LAUNCHED_BY_DAEMON === "string" ? env.CONDUCTOR_LAUNCHED_BY_DAEMON.trim().toLowerCase() : "";
47
+ if (daemonHostedValue && daemonHostedValue !== "0" && daemonHostedValue !== "false" && daemonHostedValue !== "no") {
48
+ return true;
49
+ }
50
+ return Boolean(
51
+ typeof env?.CONDUCTOR_CLI_COMMAND === "string" && env.CONDUCTOR_CLI_COMMAND.trim(),
52
+ );
53
+ }
54
+
55
+ const ENABLE_FIRE_LOCAL_LOG = !isLaunchedByDaemon(process.env);
38
56
 
39
57
  const pkgJson = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8"));
40
58
  const CLI_NAME = (process.env.CONDUCTOR_CLI_NAME || path.basename(process.argv[1] || "conductor-fire")).replace(
@@ -48,21 +66,36 @@ export function buildConductorConnectHeaders(version = pkgJson.version) {
48
66
  };
49
67
  }
50
68
 
69
+ export function shouldRunReconnectRecovery({
70
+ isReconnect,
71
+ fireShuttingDown = false,
72
+ runner = null,
73
+ } = {}) {
74
+ if (!isReconnect || fireShuttingDown) {
75
+ return false;
76
+ }
77
+ if (!runner || typeof runner.shouldSuppressReconnectRecovery !== "function") {
78
+ return true;
79
+ }
80
+ return !runner.shouldSuppressReconnectRecovery();
81
+ }
82
+
51
83
  // Load allow_cli_list from config file (no defaults - must be configured)
52
- function loadAllowCliList(configFilePath) {
84
+ async function loadAllowCliList(configFilePath) {
85
+ const home = os.homedir();
86
+ const configPath = configFilePath || process.env.CONDUCTOR_CONFIG || path.join(home, ".conductor", "config.yaml");
87
+ let parsed = null;
53
88
  try {
54
- const home = os.homedir();
55
- const configPath = configFilePath || process.env.CONDUCTOR_CONFIG || path.join(home, ".conductor", "config.yaml");
56
89
  if (fs.existsSync(configPath)) {
57
90
  const content = fs.readFileSync(configPath, "utf8");
58
- const parsed = yaml.load(content);
59
- if (parsed && typeof parsed === "object" && parsed.allow_cli_list) {
60
- return filterRuntimeSupportedAllowCliList(parsed.allow_cli_list);
61
- }
91
+ parsed = yaml.load(content);
62
92
  }
63
93
  } catch (error) {
64
94
  // ignore error
65
95
  }
96
+ if (parsed && typeof parsed === "object" && parsed.allow_cli_list) {
97
+ return await filterRuntimeSupportedAllowCliList(parsed.allow_cli_list, { configFilePath: configPath });
98
+ }
66
99
  return {};
67
100
  }
68
101
 
@@ -396,7 +429,7 @@ export class FireWatchdog {
396
429
  }
397
430
 
398
431
  async function main() {
399
- const cliArgs = parseCliArgs();
432
+ const cliArgs = await parseCliArgs();
400
433
  let runtimeProjectPath = process.cwd();
401
434
  let backendSession = null;
402
435
 
@@ -409,18 +442,28 @@ async function main() {
409
442
  return;
410
443
  }
411
444
 
412
- const allowCliList = loadAllowCliList(cliArgs.configFile);
445
+ const allowCliList = await loadAllowCliList(cliArgs.configFile);
413
446
  const supportedBackends = Object.keys(allowCliList);
447
+ const discoveredBackends = await listRuntimeSupportedBackends({ configFilePath: cliArgs.configFile });
448
+ const externalBackends = discoveredBackends.filter((backend) => !RUNTIME_SUPPORTED_BACKENDS.includes(backend));
414
449
 
415
450
  if (cliArgs.listBackends) {
416
- if (supportedBackends.length === 0) {
451
+ if (supportedBackends.length === 0 && externalBackends.length === 0) {
417
452
  process.stdout.write(`No supported backends configured.\n\nAdd allow_cli_list to your config file (~/.conductor/config.yaml):\n allow_cli_list:\n codex: codex --dangerously-bypass-approvals-and-sandbox\n claude: claude --dangerously-skip-permissions\n kimi: kimi\n opencode: opencode\n`);
418
453
  } else {
419
- process.stdout.write(`Supported backends (from config):\n`);
420
- for (const [name, command] of Object.entries(allowCliList)) {
421
- process.stdout.write(` ${name}: ${command}\n`);
454
+ if (supportedBackends.length > 0) {
455
+ process.stdout.write(`Supported backends (from config):\n`);
456
+ for (const [name, command] of Object.entries(allowCliList)) {
457
+ process.stdout.write(` ${name}: ${command}\n`);
458
+ }
459
+ }
460
+ if (externalBackends.length > 0) {
461
+ process.stdout.write(`${supportedBackends.length > 0 ? "\n" : ""}External backends (from AISDK_PROVIDER_PATH):\n`);
462
+ for (const name of externalBackends) {
463
+ process.stdout.write(` ${name}\n`);
464
+ }
422
465
  }
423
- process.stdout.write(`\nDefault: ${supportedBackends[0]}\n`);
466
+ process.stdout.write(`\nDefault: ${supportedBackends[0] || externalBackends[0]}\n`);
424
467
  }
425
468
  return;
426
469
  }
@@ -429,6 +472,7 @@ async function main() {
429
472
  if (cliArgs.resumeSessionId) {
430
473
  const bootstrap = await bootstrapResumeContextForFire({
431
474
  backend: cliArgs.backend,
475
+ configFile: cliArgs.configFile,
432
476
  resumeSessionId: cliArgs.resumeSessionId,
433
477
  });
434
478
  resumeContext = bootstrap.resumeContext;
@@ -436,7 +480,7 @@ async function main() {
436
480
  }
437
481
 
438
482
  const env = buildEnv();
439
- const launchedByDaemon = Boolean(process.env.CONDUCTOR_CLI_COMMAND);
483
+ const launchedByDaemon = isLaunchedByDaemon(process.env);
440
484
  let reconnectRunner = null;
441
485
  let reconnectTaskId = null;
442
486
  let pendingRemoteStopEvent = null;
@@ -455,7 +499,13 @@ async function main() {
455
499
  fireWatchdog.start();
456
500
 
457
501
  const scheduleReconnectRecovery = ({ isReconnect }) => {
458
- if (!isReconnect) {
502
+ if (
503
+ !shouldRunReconnectRecovery({
504
+ isReconnect,
505
+ fireShuttingDown,
506
+ runner: reconnectRunner,
507
+ })
508
+ ) {
459
509
  return;
460
510
  }
461
511
  log("Conductor connection restored");
@@ -712,7 +762,13 @@ async function main() {
712
762
  summary: "conductor fire exited",
713
763
  };
714
764
  try {
715
- await conductor.sendTaskStatus(taskContext.taskId, finalStatus);
765
+ const statusResult = await conductor.sendTaskStatus(taskContext.taskId, finalStatus);
766
+ if (statusResult?.pending && typeof conductor.flushPendingUpstreamEvents === "function") {
767
+ await conductor.flushPendingUpstreamEvents({
768
+ timeoutMs: 5_000,
769
+ retryIntervalMs: 250,
770
+ });
771
+ }
716
772
  } catch (error) {
717
773
  log(`Failed to report task status (${finalStatus.status}): ${error?.message || error}`);
718
774
  }
@@ -815,7 +871,7 @@ function stripConductorArgsFromArgv(argv = []) {
815
871
  return backendArgs;
816
872
  }
817
873
 
818
- export function parseCliArgs(argvInput = process.argv) {
874
+ export async function parseCliArgs(argvInput = process.argv) {
819
875
  const rawArgv = Array.isArray(argvInput) ? argvInput : process.argv;
820
876
  const argv = hideBin(rawArgv);
821
877
  const separatorIndex = argv.indexOf("--");
@@ -833,8 +889,10 @@ export function parseCliArgs(argvInput = process.argv) {
833
889
  }
834
890
 
835
891
  const configFileFromArgs = extractConfigFileFromArgv(argv);
836
- const allowCliList = loadAllowCliList(configFileFromArgs);
892
+ const allowCliList = await loadAllowCliList(configFileFromArgs);
837
893
  const supportedBackends = Object.keys(allowCliList);
894
+ const discoveredBackends = await listRuntimeSupportedBackends({ configFilePath: configFileFromArgs });
895
+ const externalBackends = discoveredBackends.filter((backend) => !RUNTIME_SUPPORTED_BACKENDS.includes(backend));
838
896
 
839
897
  const conductorArgs = yargs(conductorArgv)
840
898
  .scriptName(CLI_NAME)
@@ -886,14 +944,14 @@ export function parseCliArgs(argvInput = process.argv) {
886
944
 
887
945
  // Handle help early
888
946
  if (helpWithoutSeparator) {
889
- const defaultBackend = supportedBackends[0] || "none";
947
+ const defaultBackend = supportedBackends[0] || externalBackends[0] || "none";
890
948
  process.stdout.write(`${CLI_NAME} - Conductor-aware AI coding agent runner
891
949
 
892
950
  Usage: ${CLI_NAME} [options] -- [backend options and prompt]
893
951
 
894
952
  Options:
895
- -b, --backend <name> Backend to use (from config: ${supportedBackends.join(", ") || "none configured"}) [default: ${defaultBackend}]
896
- --list-backends List available backends from config and exit
953
+ -b, --backend <name> Backend to use (from config or external providers: ${supportedBackends.join(", ") || externalBackends.join(", ") || "none configured"}) [default: ${defaultBackend}]
954
+ --list-backends List available configured and external backends
897
955
  --config-file <path> Path to Conductor config file
898
956
  --poll-interval <ms> Polling interval when waiting for Conductor messages
899
957
  -t, --title <text> Optional task title shown in the app task list
@@ -937,20 +995,21 @@ Environment:
937
995
  .parse();
938
996
 
939
997
  const backend = conductorArgs.backend
940
- ? normalizeRuntimeBackendName(conductorArgs.backend)
941
- : supportedBackends[0];
998
+ ? await normalizeRuntimeBackendAlias(conductorArgs.backend, { configFilePath: configFileFromArgs })
999
+ : supportedBackends[0] || externalBackends[0];
942
1000
  const shouldRequireBackend =
943
1001
  !Boolean(conductorArgs.listBackends) &&
944
1002
  !listBackendsWithoutSeparator &&
945
1003
  !Boolean(conductorArgs.version) &&
946
1004
  !versionWithoutSeparator;
947
- if (backend && !supportedBackends.includes(backend) && shouldRequireBackend) {
1005
+ const runtimeSupportedBackends = new Set(discoveredBackends);
1006
+ if (backend && !runtimeSupportedBackends.has(backend) && shouldRequireBackend) {
948
1007
  throw new Error(
949
- `Unsupported backend "${backend}". Supported backends: ${supportedBackends.join(", ") || "none configured"}.`,
1008
+ `Unsupported backend "${backend}". Supported backends: ${[...runtimeSupportedBackends].join(", ") || "none configured"}.`,
950
1009
  );
951
1010
  }
952
1011
  if (!backend && shouldRequireBackend) {
953
- throw new Error("No supported backends configured. Add codex, claude, kimi, or opencode to allow_cli_list.");
1012
+ throw new Error("No supported backends configured. Add allow_cli_list entries or set AISDK_PROVIDER_PATH for external providers.");
954
1013
  }
955
1014
 
956
1015
  const prompt = (backendArgs._ || []).map((part) => String(part)).join(" ").trim();
@@ -1225,6 +1284,7 @@ export async function resolveResumeContext(backend, sessionId, options = {}) {
1225
1284
 
1226
1285
  export async function bootstrapResumeContextForFire({
1227
1286
  backend,
1287
+ configFile,
1228
1288
  resumeSessionId,
1229
1289
  env = process.env,
1230
1290
  resolveResumeContextFn = resolveResumeContext,
@@ -1247,10 +1307,11 @@ export async function bootstrapResumeContextForFire({
1247
1307
  return { resumeContext, runtimeProjectPath };
1248
1308
  }
1249
1309
 
1250
- resumeContext = await resolveResumeContextFn(backend, resumeSessionId);
1251
- logger(
1252
- `Validated --resume ${resumeContext.sessionId} (${resumeContext.provider}) at ${resumeContext.sessionPath}`,
1253
- );
1310
+ resumeContext = await resolveResumeContextFn(backend, resumeSessionId, {
1311
+ configFilePath: configFile,
1312
+ });
1313
+ const sessionLocation = resumeContext.sessionPath ? ` at ${resumeContext.sessionPath}` : "";
1314
+ logger(`Validated --resume ${resumeContext.sessionId} (${resumeContext.provider})${sessionLocation}`);
1254
1315
  logger(`Resume will run backend from ${resumeContext.cwd}`);
1255
1316
  runtimeProjectPath = await applyWorkingDirectoryFn(resumeContext.cwd);
1256
1317
  logger(`Switched working directory to ${runtimeProjectPath} before Conductor connect`);
@@ -1459,10 +1520,24 @@ export class BridgeRunner {
1459
1520
  const fallbackSessionId = this.resumeSessionId;
1460
1521
  const sessionId = discoveredSessionId || fallbackSessionId;
1461
1522
  const sessionFilePath = sessionInfo?.sessionFilePath ? String(sessionInfo.sessionFilePath).trim() : "";
1523
+ const sessionModel =
1524
+ sessionInfo?.model && String(sessionInfo.model).trim()
1525
+ ? String(sessionInfo.model).trim()
1526
+ : this.backendSession.threadOptions?.model || this.backendName;
1527
+ const sessionModelProvider =
1528
+ sessionInfo?.modelProvider && String(sessionInfo.modelProvider).trim()
1529
+ ? String(sessionInfo.modelProvider).trim()
1530
+ : this.backendSession.threadOptions?.modelProvider || undefined;
1462
1531
  const hasRealSessionId = Boolean(sessionId);
1532
+ const messageSuffix = [
1533
+ sessionModel ? `model=${sessionModel}` : "",
1534
+ sessionModelProvider ? `provider=${sessionModelProvider}` : "",
1535
+ ]
1536
+ .filter(Boolean)
1537
+ .join(" ");
1463
1538
  const message = hasRealSessionId
1464
- ? `${this.backendName} session started: ${sessionId}`
1465
- : `${this.backendName} session started`;
1539
+ ? `${this.backendName} session started: ${sessionId}${messageSuffix ? ` (${messageSuffix})` : ""}`
1540
+ : `${this.backendName} session started${messageSuffix ? ` (${messageSuffix})` : ""}`;
1466
1541
  if (hasRealSessionId) {
1467
1542
  await this.persistTaskSessionBinding({
1468
1543
  sessionId,
@@ -1472,6 +1547,8 @@ export class BridgeRunner {
1472
1547
  try {
1473
1548
  await this.conductor.sendMessage(this.taskId, message, {
1474
1549
  backend: this.backendName,
1550
+ model: sessionModel || undefined,
1551
+ model_provider: sessionModelProvider || undefined,
1475
1552
  thread_id: hasRealSessionId ? sessionId : undefined,
1476
1553
  session_id: hasRealSessionId ? sessionId : undefined,
1477
1554
  session_file_path: sessionFilePath || undefined,
@@ -1593,6 +1670,10 @@ export class BridgeRunner {
1593
1670
  this.needsReconnectRecovery = true;
1594
1671
  }
1595
1672
 
1673
+ shouldSuppressReconnectRecovery() {
1674
+ return this.stopped || Boolean(this.remoteStopInfo);
1675
+ }
1676
+
1596
1677
  getRemoteStopSummary() {
1597
1678
  if (!this.remoteStopInfo) {
1598
1679
  return null;
@@ -1897,7 +1978,7 @@ export class BridgeRunner {
1897
1978
  this.copilotLog(
1898
1979
  `runtime replyTo=${replyTo || "latest"} state=${runtime.state || ""} phase=${runtime.phase || ""} inProgress=${Boolean(
1899
1980
  runtime.reply_in_progress,
1900
- )} status="${sanitizeForLog(runtime.status_line || "", 120)}" done="${sanitizeForLog(runtime.status_done_line || "", 120)}" preview="${sanitizeForLog(runtime.reply_preview || "", 120)}"`,
1981
+ )} status="${sanitizeForLog(runtime.status_line || "", 120)}" done="${sanitizeForLog(runtime.status_done_line || "", 120)}" preview="${sanitizeForLog(runtime.reply_preview || "", 120)}" model="${sanitizeForLog(this.backendSession.threadOptions?.model || this.backendName, 80)}" provider="${sanitizeForLog(this.backendSession.threadOptions?.modelProvider || this.backendName, 80)}"`,
1901
1982
  );
1902
1983
 
1903
1984
  try {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.2.30",
4
- "gitCommitId": "e6a71ad",
3
+ "version": "0.2.32",
4
+ "gitCommitId": "c749d4b",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "conductor": "bin/conductor.js"
@@ -18,8 +18,8 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@love-moon/ai-bridge": "0.1.4",
21
- "@love-moon/ai-sdk": "0.2.30",
22
- "@love-moon/conductor-sdk": "0.2.30",
21
+ "@love-moon/ai-sdk": "0.2.32",
22
+ "@love-moon/conductor-sdk": "0.2.32",
23
23
  "chrome-launcher": "^1.2.1",
24
24
  "chrome-remote-interface": "^0.33.0",
25
25
  "dotenv": "^16.4.5",