@love-moon/conductor-cli 0.2.32 → 0.2.33

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.
@@ -19,7 +19,7 @@ import yargs from "yargs/yargs";
19
19
  import { hideBin } from "yargs/helpers";
20
20
  import yaml from "js-yaml";
21
21
  import { createAiSession } from "@love-moon/ai-sdk";
22
- import { ConductorClient, loadConfig } from "@love-moon/conductor-sdk";
22
+ import { ConductorClient, ProjectContext, loadConfig } from "@love-moon/conductor-sdk";
23
23
  import {
24
24
  buildResumeArgsForBackend as buildCliResumeArgsForBackend,
25
25
  resolveResumeContext as resolveCliResumeContext,
@@ -28,8 +28,9 @@ import {
28
28
  } from "../src/fire/resume.js";
29
29
  import {
30
30
  filterRuntimeSupportedAllowCliList,
31
- RUNTIME_SUPPORTED_BACKENDS,
32
- listRuntimeSupportedBackends,
31
+ isBuiltInRuntimeBackend,
32
+ listAdvertisedBackends,
33
+ resolveConfiguredRuntimeBackend,
33
34
  normalizeRuntimeBackendAlias,
34
35
  normalizeRuntimeBackendName,
35
36
  } from "../src/runtime-backends.js";
@@ -99,37 +100,46 @@ async function loadAllowCliList(configFilePath) {
99
100
  return {};
100
101
  }
101
102
 
102
- export function resolveAiSessionCommandLine(backend, allowCliList, env = process.env) {
103
+ export function resolveAiSessionCommandLine(backend, allowCliList, env = process.env, sessionBackend = backend) {
103
104
  const normalizedBackend = normalizeRuntimeBackendName(backend);
105
+ const normalizedSessionBackend = normalizeRuntimeBackendName(sessionBackend);
104
106
  const envKeyByBackend = {
107
+ codex: "CONDUCTOR_CODEX_APP_SERVER_COMMAND",
105
108
  opencode: "CONDUCTOR_OPENCODE_COMMAND",
106
109
  kimi: "CONDUCTOR_KIMI_COMMAND",
107
110
  };
108
- const envKey = envKeyByBackend[normalizedBackend];
109
- if (!envKey) {
110
- return "";
111
- }
111
+ const envKey = envKeyByBackend[normalizedSessionBackend];
112
112
 
113
- const preferredEnvCommand = typeof env?.[envKey] === "string" ? env[envKey].trim() : "";
113
+ const preferredEnvCommand = envKey && typeof env?.[envKey] === "string" ? env[envKey].trim() : "";
114
114
  if (preferredEnvCommand) {
115
115
  return preferredEnvCommand;
116
116
  }
117
117
 
118
118
  const configuredCommand =
119
- allowCliList && typeof allowCliList === "object" && typeof allowCliList[normalizedBackend] === "string"
120
- ? allowCliList[normalizedBackend].trim()
119
+ allowCliList && typeof allowCliList === "object"
120
+ ? typeof allowCliList[normalizedBackend] === "string"
121
+ ? allowCliList[normalizedBackend].trim()
122
+ : typeof allowCliList[normalizedSessionBackend] === "string"
123
+ ? allowCliList[normalizedSessionBackend].trim()
124
+ : ""
121
125
  : "";
122
- if (configuredCommand) {
123
- return configuredCommand;
124
- }
125
126
 
126
127
  const daemonCommand =
127
128
  typeof env?.CONDUCTOR_CLI_COMMAND === "string" ? env.CONDUCTOR_CLI_COMMAND.trim() : "";
128
- if (daemonCommand) {
129
- return daemonCommand;
129
+
130
+ const resolvedCommand = configuredCommand || daemonCommand;
131
+ if (!resolvedCommand) {
132
+ return "";
130
133
  }
131
134
 
132
- return "";
135
+ if (normalizedSessionBackend === "codex") {
136
+ if (/\bapp-server\b/.test(resolvedCommand)) {
137
+ return resolvedCommand;
138
+ }
139
+ return `${resolvedCommand} app-server --listen stdio://`;
140
+ }
141
+
142
+ return resolvedCommand;
133
143
  }
134
144
 
135
145
  const DEFAULT_POLL_INTERVAL_MS = parseInt(
@@ -443,12 +453,15 @@ async function main() {
443
453
  }
444
454
 
445
455
  const allowCliList = await loadAllowCliList(cliArgs.configFile);
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));
456
+ const { supportedBackends, externalBackends, discoveryError } = await listAdvertisedBackends(allowCliList, {
457
+ configFilePath: cliArgs.configFile,
458
+ });
449
459
 
450
460
  if (cliArgs.listBackends) {
451
461
  if (supportedBackends.length === 0 && externalBackends.length === 0) {
462
+ if (discoveryError) {
463
+ throw discoveryError;
464
+ }
452
465
  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`);
453
466
  } else {
454
467
  if (supportedBackends.length > 0) {
@@ -471,7 +484,7 @@ async function main() {
471
484
  let resumeContext = null;
472
485
  if (cliArgs.resumeSessionId) {
473
486
  const bootstrap = await bootstrapResumeContextForFire({
474
- backend: cliArgs.backend,
487
+ backend: cliArgs.sessionBackend || cliArgs.backend,
475
488
  configFile: cliArgs.configFile,
476
489
  resumeSessionId: cliArgs.resumeSessionId,
477
490
  });
@@ -611,6 +624,7 @@ async function main() {
611
624
  requestedTitle: requestedTaskTitle,
612
625
  backend: cliArgs.backend,
613
626
  daemonName: configuredDaemonName,
627
+ projectPath: runtimeProjectPath,
614
628
  });
615
629
  injectResolvedTaskId(taskContext.taskId);
616
630
  injectResolvedTaskId(taskContext.taskId, env);
@@ -641,9 +655,14 @@ async function main() {
641
655
 
642
656
  const resolvedResumeSessionId = cliArgs.resumeSessionId;
643
657
 
644
- const sessionCommandLine = resolveAiSessionCommandLine(cliArgs.backend, allowCliList, process.env);
658
+ const sessionCommandLine = resolveAiSessionCommandLine(
659
+ cliArgs.backend,
660
+ allowCliList,
661
+ process.env,
662
+ cliArgs.sessionBackend,
663
+ );
645
664
 
646
- backendSession = createAiSession(cliArgs.backend, {
665
+ backendSession = createAiSession(cliArgs.sessionBackend || cliArgs.backend, {
647
666
  initialImages: cliArgs.initialImages,
648
667
  cwd: runtimeProjectPath,
649
668
  resumeSessionId: resolvedResumeSessionId,
@@ -728,8 +747,8 @@ async function main() {
728
747
 
729
748
  let runnerError = null;
730
749
  try {
731
- if (!resolvedResumeSessionId && String(cliArgs.backend).trim().toLowerCase() === "codex") {
732
- await withFreshSessionBootstrapLock(cliArgs.backend, runtimeProjectPath, async () => {
750
+ if (!resolvedResumeSessionId && String(cliArgs.sessionBackend || cliArgs.backend).trim().toLowerCase() === "codex") {
751
+ await withFreshSessionBootstrapLock(cliArgs.sessionBackend || cliArgs.backend, runtimeProjectPath, async () => {
733
752
  await runner.announceBackendSession();
734
753
  });
735
754
  }
@@ -890,9 +909,9 @@ export async function parseCliArgs(argvInput = process.argv) {
890
909
 
891
910
  const configFileFromArgs = extractConfigFileFromArgv(argv);
892
911
  const allowCliList = await loadAllowCliList(configFileFromArgs);
893
- const supportedBackends = Object.keys(allowCliList);
894
- const discoveredBackends = await listRuntimeSupportedBackends({ configFilePath: configFileFromArgs });
895
- const externalBackends = discoveredBackends.filter((backend) => !RUNTIME_SUPPORTED_BACKENDS.includes(backend));
912
+ const { supportedBackends, externalBackends, discoveryError } = await listAdvertisedBackends(allowCliList, {
913
+ configFilePath: configFileFromArgs,
914
+ });
896
915
 
897
916
  const conductorArgs = yargs(conductorArgv)
898
917
  .scriptName(CLI_NAME)
@@ -994,21 +1013,36 @@ Environment:
994
1013
  })
995
1014
  .parse();
996
1015
 
997
- const backend = conductorArgs.backend
998
- ? await normalizeRuntimeBackendAlias(conductorArgs.backend, { configFilePath: configFileFromArgs })
1016
+ const requestedBackend = conductorArgs.backend
1017
+ ? normalizeRuntimeBackendName(conductorArgs.backend)
999
1018
  : supportedBackends[0] || externalBackends[0];
1019
+ const configuredBackend = await resolveConfiguredRuntimeBackend(requestedBackend, allowCliList, {
1020
+ configFilePath: configFileFromArgs,
1021
+ });
1022
+ const backend = configuredBackend?.requestedBackend || requestedBackend;
1023
+ const sessionBackend =
1024
+ configuredBackend?.runtimeBackend ||
1025
+ (backend ? await normalizeRuntimeBackendAlias(backend, { configFilePath: configFileFromArgs }) : "");
1000
1026
  const shouldRequireBackend =
1001
1027
  !Boolean(conductorArgs.listBackends) &&
1002
1028
  !listBackendsWithoutSeparator &&
1003
1029
  !Boolean(conductorArgs.version) &&
1004
1030
  !versionWithoutSeparator;
1005
- const runtimeSupportedBackends = new Set(discoveredBackends);
1006
- if (backend && !runtimeSupportedBackends.has(backend) && shouldRequireBackend) {
1031
+ const runtimeSupportedBackends = new Set(supportedBackends);
1032
+ const advertisedExternalBackends = new Set(externalBackends);
1033
+ const hasConfiguredEntry = Boolean(configuredBackend?.commandLine);
1034
+ const isAllowedExternalBackend =
1035
+ !isBuiltInRuntimeBackend(sessionBackend) &&
1036
+ advertisedExternalBackends.has(sessionBackend);
1037
+ if (backend && shouldRequireBackend && !hasConfiguredEntry && !isAllowedExternalBackend) {
1007
1038
  throw new Error(
1008
1039
  `Unsupported backend "${backend}". Supported backends: ${[...runtimeSupportedBackends].join(", ") || "none configured"}.`,
1009
1040
  );
1010
1041
  }
1011
1042
  if (!backend && shouldRequireBackend) {
1043
+ if (discoveryError) {
1044
+ throw discoveryError;
1045
+ }
1012
1046
  throw new Error("No supported backends configured. Add allow_cli_list entries or set AISDK_PROVIDER_PATH for external providers.");
1013
1047
  }
1014
1048
 
@@ -1036,6 +1070,7 @@ Environment:
1036
1070
  taskTitle: typeof conductorArgs.title === "string" ? conductorArgs.title.trim() : "",
1037
1071
  hasExplicitTaskTitle: typeof conductorArgs.title === "string" && Boolean(conductorArgs.title.trim()),
1038
1072
  configFile: conductorArgs.configFile,
1073
+ sessionBackend,
1039
1074
  resumeSessionId,
1040
1075
  showVersion: Boolean(conductorArgs.version) || versionWithoutSeparator,
1041
1076
  listBackends: Boolean(conductorArgs.listBackends) || listBackendsWithoutSeparator,
@@ -1152,7 +1187,10 @@ async function ensureTaskContext(conductor, opts) {
1152
1187
  };
1153
1188
  }
1154
1189
 
1155
- const projectId = await resolveProjectId(conductor, opts.requestedProjectId);
1190
+ const projectId = await resolveProjectId(conductor, opts.requestedProjectId, {
1191
+ daemonName: opts.daemonName,
1192
+ projectPath: opts.projectPath,
1193
+ });
1156
1194
  const payload = {
1157
1195
  project_id: projectId,
1158
1196
  task_title: deriveTaskTitle(opts.initialPrompt, opts.requestedTitle, opts.backend),
@@ -1167,15 +1205,6 @@ async function ensureTaskContext(conductor, opts) {
1167
1205
 
1168
1206
  const session = await conductor.createTaskSession(payload);
1169
1207
 
1170
- // Auto-bind current path to project if not already bound
1171
- try {
1172
- await conductor.bindProjectPath(projectId);
1173
- log(`Bound current path to project ${projectId}`);
1174
- } catch (error) {
1175
- // Ignore binding errors - it's not critical
1176
- log(`Note: Could not bind path to project: ${error.message}`);
1177
- }
1178
-
1179
1208
  return {
1180
1209
  taskId: session.task_id,
1181
1210
  appUrl: session.app_url || null,
@@ -1184,51 +1213,155 @@ async function ensureTaskContext(conductor, opts) {
1184
1213
  };
1185
1214
  }
1186
1215
 
1187
- async function resolveProjectId(conductor, explicit) {
1216
+ export async function resolveProjectId(conductor, explicit, opts = {}) {
1188
1217
  if (explicit) {
1189
1218
  return explicit;
1190
1219
  }
1191
1220
 
1192
- // First, try to match project by current path
1221
+ const daemonHost = resolveDaemonHost(opts.daemonName);
1222
+ const projectPath = typeof opts.projectPath === "string" && opts.projectPath.trim() ? opts.projectPath.trim() : process.cwd();
1223
+
1224
+ if (!daemonHost) {
1225
+ return resolveDefaultProjectId(conductor);
1226
+ }
1227
+
1228
+ const exists = await isExistingDirectory(projectPath);
1229
+ if (!exists) {
1230
+ throw new Error(`Workspace path does not exist: ${projectPath}`);
1231
+ }
1232
+
1233
+ const snapshot = resolveWorkspaceSnapshot(projectPath);
1234
+ const projectName = deriveProjectName(snapshot);
1235
+
1193
1236
  try {
1194
- const matchResult = await conductor.matchProjectByPath();
1237
+ const matchResult = await conductor.matchProjectByPath({
1238
+ daemon_host: daemonHost,
1239
+ project_path: snapshot.projectRoot,
1240
+ });
1195
1241
  if (matchResult?.project_id) {
1196
1242
  log(`Matched project ${matchResult.project_name || matchResult.project_id} by path ${matchResult.matched_path}`);
1197
- return matchResult.project_id;
1243
+ let resolvedProjectId = matchResult.project_id;
1244
+ try {
1245
+ const bindResult = await conductor.bindProjectPath(matchResult.project_id, {
1246
+ daemon_host: daemonHost,
1247
+ project_path: snapshot.projectRoot,
1248
+ });
1249
+ if (typeof bindResult?.project_id === "string" && bindResult.project_id.trim()) {
1250
+ resolvedProjectId = bindResult.project_id.trim();
1251
+ }
1252
+ } catch (error) {
1253
+ log(`Unable to backfill bound workspace path: ${error.message}`);
1254
+ try {
1255
+ const rebound = await conductor.matchProjectByPath({
1256
+ daemon_host: daemonHost,
1257
+ project_path: snapshot.projectRoot,
1258
+ });
1259
+ if (rebound?.project_id) {
1260
+ resolvedProjectId = rebound.project_id;
1261
+ }
1262
+ } catch {
1263
+ // ignore retry match failures
1264
+ }
1265
+ }
1266
+ return resolvedProjectId;
1198
1267
  }
1199
1268
  } catch (error) {
1200
1269
  log(`Unable to match project by path: ${error.message}`);
1201
1270
  }
1202
1271
 
1203
1272
  try {
1204
- const record = await conductor.getLocalProjectRecord();
1205
- if (record?.project_id) {
1206
- try {
1207
- const listing = await conductor.listProjects();
1208
- const exists = Array.isArray(listing?.projects)
1209
- ? listing.projects.some((project) => String(project?.id || "") === String(record.project_id))
1210
- : false;
1211
- if (exists) {
1212
- return record.project_id;
1213
- }
1214
- log(`Local session project ${record.project_id} no longer exists; falling back to server project list`);
1215
- } catch (verifyError) {
1216
- log(`Unable to verify local project record; using cached project id: ${verifyError.message}`);
1217
- return record.project_id;
1218
- }
1273
+ const created = await conductor.createProject({
1274
+ name: projectName,
1275
+ bindingConfirmed: true,
1276
+ daemonHost,
1277
+ workspacePath: snapshot.projectRoot,
1278
+ repoRoot: snapshot.repoRoot,
1279
+ worktreeBranch: snapshot.worktreeBranch,
1280
+ lastCommit: snapshot.lastCommit,
1281
+ fileCount: snapshot.fileCount,
1282
+ });
1283
+ if (created?.id) {
1284
+ log(`Created bound project ${created.name || created.id} for ${daemonHost}:${snapshot.projectRoot}`);
1285
+ return created.id;
1219
1286
  }
1287
+ throw new Error("create_project returned no id");
1220
1288
  } catch (error) {
1221
- log(`Unable to resolve project via local session: ${error.message}`);
1289
+ log(`Unable to create bound project: ${error.message}`);
1290
+ }
1291
+
1292
+ try {
1293
+ const retryMatch = await conductor.matchProjectByPath({
1294
+ daemon_host: daemonHost,
1295
+ project_path: snapshot.projectRoot,
1296
+ });
1297
+ if (retryMatch?.project_id) {
1298
+ return retryMatch.project_id;
1299
+ }
1300
+ } catch {
1301
+ // ignore retry match failures
1302
+ }
1303
+
1304
+ log(`Unable to resolve bound project for ${daemonHost}:${snapshot.projectRoot}, falling back to default`);
1305
+ return resolveDefaultProjectId(conductor);
1306
+ }
1307
+
1308
+ function resolveDaemonHost(daemonName) {
1309
+ if (typeof daemonName === "string" && daemonName.trim()) {
1310
+ return daemonName.trim();
1311
+ }
1312
+ const fromEnv = typeof process.env.CONDUCTOR_DAEMON_NAME === "string" ? process.env.CONDUCTOR_DAEMON_NAME.trim() : "";
1313
+ if (fromEnv) {
1314
+ return fromEnv;
1315
+ }
1316
+ const fromAgent = typeof process.env.CONDUCTOR_AGENT_NAME === "string" ? process.env.CONDUCTOR_AGENT_NAME.trim() : "";
1317
+ if (fromAgent) {
1318
+ return fromAgent;
1222
1319
  }
1320
+ try {
1321
+ return os.hostname();
1322
+ } catch {
1323
+ return "";
1324
+ }
1325
+ }
1223
1326
 
1224
- const listing = await conductor.listProjects();
1225
- const first = listing?.projects?.[0];
1226
- if (first?.id) {
1227
- return first.id;
1327
+ function resolveWorkspaceSnapshot(projectPath) {
1328
+ try {
1329
+ const context = new ProjectContext(projectPath);
1330
+ return context.snapshot();
1331
+ } catch {
1332
+ return {
1333
+ projectRoot: path.resolve(projectPath),
1334
+ };
1228
1335
  }
1229
- log("No projects available; creating default project...");
1336
+ }
1337
+
1338
+ function deriveProjectName(snapshot) {
1339
+ const basePath = snapshot.repoRoot || snapshot.projectRoot;
1340
+ const name = basePath ? path.basename(basePath) : "";
1341
+ const baseName = name || "New Project";
1342
+ const digest = createHash("sha1").update(basePath || baseName).digest("hex").slice(0, 8);
1343
+ return `${baseName}-${digest}`;
1344
+ }
1345
+
1346
+ async function resolveDefaultProjectId(conductor) {
1230
1347
  try {
1231
- const created = await conductor.createProject("default", "Auto-created by conductor-fire");
1348
+ const listing = await conductor.listProjects();
1349
+ const defaultProject = Array.isArray(listing?.projects)
1350
+ ? listing.projects.find((project) => Boolean(project?.isDefault))
1351
+ : null;
1352
+ if (defaultProject?.id) {
1353
+ return defaultProject.id;
1354
+ }
1355
+ } catch {
1356
+ // ignore list failures
1357
+ }
1358
+
1359
+ log("No bound daemon available; creating default project...");
1360
+ try {
1361
+ const created = await conductor.createProject({
1362
+ name: "Default Project",
1363
+ isDefault: true,
1364
+ });
1232
1365
  if (created?.id) {
1233
1366
  log(`Created default project ${created.id}`);
1234
1367
  return created.id;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.2.32",
4
- "gitCommitId": "c749d4b",
3
+ "version": "0.2.33",
4
+ "gitCommitId": "db7f9bf",
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.32",
22
- "@love-moon/conductor-sdk": "0.2.32",
21
+ "@love-moon/ai-sdk": "0.2.33",
22
+ "@love-moon/conductor-sdk": "0.2.33",
23
23
  "chrome-launcher": "^1.2.1",
24
24
  "chrome-remote-interface": "^0.33.0",
25
25
  "dotenv": "^16.4.5",