@love-moon/conductor-cli 0.1.2 → 0.1.3

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.
@@ -12,12 +12,10 @@ const CONFIG_FILE = path.join(CONFIG_DIR, "config.yaml");
12
12
  const backendUrl =
13
13
  process.env.CONDUCTOR_BACKEND_URL ||
14
14
  process.env.BACKEND_URL ||
15
- "http://localhost:6152";
15
+ "https://conductor-ai.top";
16
16
 
17
17
  const websocketUrl =
18
- process.env.CONDUCTOR_WS_URL ||
19
- process.env.PUBLIC_WS_URL ||
20
- deriveWebsocketUrl(backendUrl);
18
+ process.env.CONDUCTOR_WS_URL || process.env.PUBLIC_WS_URL || "";
21
19
 
22
20
  async function main() {
23
21
  if (fs.existsSync(CONFIG_FILE)) {
@@ -40,7 +38,11 @@ async function main() {
40
38
  if (websocketUrl) {
41
39
  lines.push(`websocket_url: ${yamlQuote(websocketUrl)}`);
42
40
  }
43
- lines.push("log_level: info", "");
41
+ lines.push("log_level: debug", "");
42
+ lines.push("allow_cli_list:", "");
43
+ lines.push("codex: codex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check", "");
44
+ lines.push("claude: claude --dangerously-skip-permissions", "");
45
+ lines.push("copilot: copilot --allow-all-paths --allow-all-tools -i", "");
44
46
 
45
47
  fs.writeFileSync(CONFIG_FILE, lines.join("\n"), "utf-8");
46
48
  process.stdout.write(`Wrote Conductor config to ${CONFIG_FILE}\n`);
@@ -62,16 +64,6 @@ async function promptForToken() {
62
64
  }
63
65
  }
64
66
 
65
- function deriveWebsocketUrl(url) {
66
- try {
67
- const parsed = new URL(url);
68
- const scheme = parsed.protocol === "https:" ? "wss" : "ws";
69
- return `${scheme}://${parsed.host}/ws/agent`;
70
- } catch {
71
- return "";
72
- }
73
- }
74
-
75
67
  function yamlQuote(value) {
76
68
  return JSON.stringify(value);
77
69
  }
@@ -3,12 +3,13 @@
3
3
  /**
4
4
  * conductor-fire - Conductor-aware AI coding agent runner.
5
5
  *
6
- * Supports multiple backends: codex, claude, copilot, tmates, cursor, gemini.
6
+ * Supports configurable backends via allow_cli_list in config file.
7
7
  * This CLI bridges various AI coding agents with Conductor via MCP.
8
8
  */
9
9
 
10
10
  import fs from "node:fs";
11
11
  import { createRequire } from "node:module";
12
+ import os from "node:os";
12
13
  import path from "node:path";
13
14
  import process from "node:process";
14
15
  import { setTimeout as delay } from "node:timers/promises";
@@ -18,6 +19,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
18
19
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
19
20
  import yargs from "yargs/yargs";
20
21
  import { hideBin } from "yargs/helpers";
22
+ import yaml from "js-yaml";
21
23
  import { cli2sdk } from "@love-moon/cli2sdk";
22
24
  import { loadConfig } from "@love-moon/conductor-sdk";
23
25
  import {
@@ -51,9 +53,69 @@ const CLI_NAME = (process.env.CONDUCTOR_CLI_NAME || path.basename(process.argv[1
51
53
  "",
52
54
  );
53
55
 
54
- // Supported backends
55
- const SUPPORTED_BACKENDS = ["codex", "claude", "copilot", "tmates", "cursor", "gemini"];
56
- const DEFAULT_BACKEND = process.env.CONDUCTOR_BACKEND || "codex";
56
+ // Load allow_cli_list from config file (no defaults - must be configured)
57
+ function loadAllowCliList(configFilePath) {
58
+ try {
59
+ const home = os.homedir();
60
+ const configPath = configFilePath || process.env.CONDUCTOR_CONFIG || path.join(home, ".conductor", "config.yaml");
61
+ if (fs.existsSync(configPath)) {
62
+ const content = fs.readFileSync(configPath, "utf8");
63
+ const parsed = yaml.load(content);
64
+ if (parsed && typeof parsed === "object" && parsed.allow_cli_list) {
65
+ return parsed.allow_cli_list;
66
+ }
67
+ }
68
+ } catch (error) {
69
+ // ignore error
70
+ }
71
+ return {};
72
+ }
73
+
74
+ // Load envs config from config file
75
+ function loadEnvConfig(configFilePath) {
76
+ try {
77
+ const home = os.homedir();
78
+ const configPath = configFilePath || process.env.CONDUCTOR_CONFIG || path.join(home, ".conductor", "config.yaml");
79
+ if (fs.existsSync(configPath)) {
80
+ const content = fs.readFileSync(configPath, "utf8");
81
+ const parsed = yaml.load(content);
82
+ if (parsed && typeof parsed === "object" && parsed.envs) {
83
+ return parsed.envs;
84
+ }
85
+ }
86
+ } catch (error) {
87
+ // ignore error
88
+ }
89
+ return null;
90
+ }
91
+
92
+ // Convert proxy env values to standard proxy environment variables
93
+ function proxyToEnv(envConfig) {
94
+ if (!envConfig || typeof envConfig !== "object") {
95
+ return {};
96
+ }
97
+ const env = {};
98
+ // Support both snake_case and lowercase keys
99
+ const mappings = {
100
+ http_proxy: ["HTTP_PROXY", "http_proxy"],
101
+ https_proxy: ["HTTPS_PROXY", "https_proxy"],
102
+ all_proxy: ["ALL_PROXY", "all_proxy"],
103
+ no_proxy: ["NO_PROXY", "no_proxy"],
104
+ };
105
+ for (const [key, envKeys] of Object.entries(mappings)) {
106
+ const value = envConfig[key] || envConfig[key.toUpperCase()];
107
+ if (value) {
108
+ for (const envKey of envKeys) {
109
+ env[envKey] = value;
110
+ }
111
+ }
112
+ }
113
+ return env;
114
+ }
115
+
116
+ // Get CLI command from environment variable (set by daemon) or from config
117
+ const CUSTOM_CLI_COMMAND = process.env.CONDUCTOR_CLI_COMMAND;
118
+ const DEFAULT_ALLOW_CLI_LIST = loadAllowCliList();
57
119
 
58
120
  const DEFAULT_POLL_INTERVAL_MS = parseInt(
59
121
  process.env.CONDUCTOR_CLI_POLL_INTERVAL_MS || process.env.CCODEX_POLL_INTERVAL_MS || "2000",
@@ -78,9 +140,19 @@ async function main() {
78
140
  return;
79
141
  }
80
142
 
143
+ const allowCliList = loadAllowCliList(cliArgs.configFile);
144
+ const supportedBackends = Object.keys(allowCliList);
145
+
81
146
  if (cliArgs.listBackends) {
82
- process.stdout.write(`Supported backends: ${SUPPORTED_BACKENDS.join(", ")}\n`);
83
- process.stdout.write(`Default: ${DEFAULT_BACKEND}\n`);
147
+ if (supportedBackends.length === 0) {
148
+ process.stdout.write(`No 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`);
149
+ } else {
150
+ process.stdout.write(`Supported backends (from config):\n`);
151
+ for (const [name, command] of Object.entries(allowCliList)) {
152
+ process.stdout.write(` ${name}: ${command}\n`);
153
+ }
154
+ process.stdout.write(`\nDefault: ${supportedBackends[0]}\n`);
155
+ }
84
156
  return;
85
157
  }
86
158
 
@@ -120,13 +192,13 @@ async function main() {
120
192
  }
121
193
  }
122
194
 
123
- // Create backend session based on selected backend
124
- const backendSession = createBackendSession(cliArgs.backend, {
125
- threadOptions: cliArgs.threadOptions,
195
+ // Create backend session using cli2sdk with generic provider
196
+ const backendSession = new Cli2SdkSession(cliArgs.backend, {
126
197
  initialImages: cliArgs.initialImages,
127
198
  cwd: CLI_PROJECT_PATH,
128
199
  initialHistory: fromHistory.history,
129
200
  resumeSessionId: fromHistory.sessionId,
201
+ configFile: cliArgs.configFile,
130
202
  });
131
203
 
132
204
  log(`Using backend: ${cliArgs.backend}`);
@@ -190,16 +262,22 @@ async function main() {
190
262
  }
191
263
  }
192
264
 
193
- /**
194
- * Create a backend session based on the selected backend type.
195
- */
196
- function createBackendSession(backend, options) {
197
- // Use cli2sdk for all backends including codex
198
- return new Cli2SdkSession(backend, options);
265
+ function extractConfigFileFromArgv(argv) {
266
+ for (let i = 0; i < argv.length; i += 1) {
267
+ const arg = argv[i];
268
+ if (arg === "--config-file") {
269
+ return argv[i + 1];
270
+ }
271
+ if (arg.startsWith("--config-file=")) {
272
+ return arg.slice("--config-file=".length);
273
+ }
274
+ }
275
+ return undefined;
199
276
  }
200
277
 
201
- function parseCliArgs() {
202
- const argv = hideBin(process.argv);
278
+ export function parseCliArgs(argvInput = process.argv) {
279
+ const rawArgv = Array.isArray(argvInput) ? argvInput : process.argv;
280
+ const argv = hideBin(rawArgv);
203
281
  const separatorIndex = argv.indexOf("--");
204
282
 
205
283
  // When no separator, parse all args first to check for conductor-specific options
@@ -211,6 +289,10 @@ function parseCliArgs() {
211
289
  const listBackendsWithoutSeparator = separatorIndex === -1 && argv.includes("--list-backends");
212
290
  const helpWithoutSeparator = separatorIndex === -1 && (argv.includes("--help") || argv.includes("-h"));
213
291
 
292
+ const configFileFromArgs = extractConfigFileFromArgv(argv);
293
+ const allowCliList = loadAllowCliList(configFileFromArgs);
294
+ const supportedBackends = Object.keys(allowCliList);
295
+
214
296
  const conductorArgs = yargs(conductorArgv)
215
297
  .scriptName(CLI_NAME)
216
298
  .usage("Usage: $0 [--backend <name>] -- [backend options and prompt]")
@@ -218,9 +300,8 @@ function parseCliArgs() {
218
300
  .option("backend", {
219
301
  alias: "b",
220
302
  type: "string",
221
- describe: `Backend to use (${SUPPORTED_BACKENDS.join(", ")})`,
222
- default: DEFAULT_BACKEND,
223
- choices: SUPPORTED_BACKENDS,
303
+ describe: `Backend to use (loaded from config: ${supportedBackends.join(", ") || "none configured"})`,
304
+ ...(supportedBackends.length > 0 ? { choices: supportedBackends } : {}),
224
305
  })
225
306
  .option("list-backends", {
226
307
  type: "boolean",
@@ -269,13 +350,14 @@ function parseCliArgs() {
269
350
 
270
351
  // Handle help early
271
352
  if (helpWithoutSeparator) {
353
+ const defaultBackend = supportedBackends[0] || "none";
272
354
  process.stdout.write(`${CLI_NAME} - Conductor-aware AI coding agent runner
273
355
 
274
356
  Usage: ${CLI_NAME} [options] -- [backend options and prompt]
275
357
 
276
358
  Options:
277
- -b, --backend <name> Backend to use (${SUPPORTED_BACKENDS.join(", ")}) [default: ${DEFAULT_BACKEND}]
278
- --list-backends List available backends and exit
359
+ -b, --backend <name> Backend to use (from config: ${supportedBackends.join(", ") || "none configured"}) [default: ${defaultBackend}]
360
+ --list-backends List available backends from config and exit
279
361
  --config-file <path> Path to Conductor config file
280
362
  --poll-interval <ms> Polling interval when waiting for Conductor messages
281
363
  -t, --title <text> Optional task title shown in the app task list
@@ -284,17 +366,19 @@ Options:
284
366
  -v, --version Show Conductor CLI version and exit
285
367
  -h, --help Show this help message
286
368
 
369
+ Config file format (~/.conductor/config.yaml):
370
+ allow_cli_list:
371
+ codex: codex --dangerously-bypass-approvals-and-sandbox
372
+ claude: claude --dangerously-skip-permissions
373
+
287
374
  Examples:
288
- ${CLI_NAME} -- "fix the bug" # Use default backend (codex)
375
+ ${CLI_NAME} -- "fix the bug" # Use default backend
289
376
  ${CLI_NAME} --backend claude -- "fix the bug" # Use Claude CLI backend
290
- ${CLI_NAME} --backend tmates -- "fix the bug" # Use TMates CLI backend
291
- ${CLI_NAME} --backend copilot -- "fix the bug" # Use Copilot CLI backend
377
+ ${CLI_NAME} --list-backends # Show configured backends
292
378
  ${CLI_NAME} --config-file ~/.conductor/config.yaml -- "fix the bug"
293
- ${CLI_NAME} --backend codex --from -- "continue"
294
- ${CLI_NAME} --backend codex --from codex-session-id -- "continue"
295
379
 
296
380
  Environment:
297
- CONDUCTOR_BACKEND Default backend (overrides codex)
381
+ CONDUCTOR_BACKEND Default backend
298
382
  CONDUCTOR_PROJECT_ID Project ID to attach to
299
383
  CONDUCTOR_TASK_ID Attach to existing task instead of creating new one
300
384
  `);
@@ -311,13 +395,7 @@ Environment:
311
395
  })
312
396
  .parse();
313
397
 
314
- // Apply codex-specific defaults when using codex backend
315
- const backend = conductorArgs.backend || DEFAULT_BACKEND;
316
- if (backend === "codex") {
317
- backendArgs.sandboxMode ||= "danger-full-access";
318
- backendArgs.approvalPolicy ||= "never";
319
- backendArgs.skipGitRepoCheck ||= true;
320
- }
398
+ const backend = conductorArgs.backend || supportedBackends[0];
321
399
 
322
400
  const prompt = (backendArgs._ || []).map((part) => String(part)).join(" ").trim();
323
401
  const initialImages = normalizeArray(backendArgs.image || backendArgs.i).map((img) => String(img));
@@ -336,7 +414,6 @@ Environment:
336
414
  backend,
337
415
  initialPrompt: prompt || conductorArgs.prefill || "",
338
416
  initialImages,
339
- threadOptions: deriveThreadOptions(backendArgs, CLI_PROJECT_PATH),
340
417
  pollIntervalMs,
341
418
  rawBackendArgs: backendArgv,
342
419
  taskTitle: resolveTaskTitle(conductorArgs.title),
@@ -355,73 +432,6 @@ function resolveTaskTitle(titleFlag) {
355
432
  return cwdName || "";
356
433
  }
357
434
 
358
- function deriveThreadOptions(codexArgs, defaultWorkingDirectory = CLI_PROJECT_PATH) {
359
- const options = {};
360
- const allowedKeys = new Set([
361
- "model",
362
- "sandboxMode",
363
- "workingDirectory",
364
- "skipGitRepoCheck",
365
- "modelReasoningEffort",
366
- "networkAccessEnabled",
367
- "webSearchEnabled",
368
- "approvalPolicy",
369
- ]);
370
-
371
- Object.entries(codexArgs || {}).forEach(([key, value]) => {
372
- if (key === "_" || key === "$0") {
373
- return;
374
- }
375
- const optionKey = mapCodexFlagToThreadOption(key);
376
- if (allowedKeys.has(optionKey)) {
377
- options[optionKey] = value;
378
- }
379
- });
380
-
381
- if (codexArgs.fullAuto) {
382
- options.sandboxMode ||= "workspace-write";
383
- options.approvalPolicy ||= "on-request";
384
- }
385
-
386
- if (codexArgs.dangerouslyBypassApprovalsAndSandbox) {
387
- options.sandboxMode = "danger-full-access";
388
- options.approvalPolicy = "never";
389
- }
390
-
391
- if (!options.workingDirectory && defaultWorkingDirectory) {
392
- options.workingDirectory = defaultWorkingDirectory;
393
- }
394
-
395
- return options;
396
- }
397
-
398
- function mapCodexFlagToThreadOption(flag) {
399
- const normalized = toCamelCase(flag);
400
- if (normalized === "sandbox") {
401
- return "sandboxMode";
402
- }
403
- if (normalized === "askForApproval" || normalized === "approval") {
404
- return "approvalPolicy";
405
- }
406
- if (normalized === "cd" || normalized === "c" || normalized === "workingDirectory") {
407
- return "workingDirectory";
408
- }
409
- if (normalized === "search" || normalized === "webSearch") {
410
- return "webSearchEnabled";
411
- }
412
- if (normalized === "networkAccess" || normalized === "network") {
413
- return "networkAccessEnabled";
414
- }
415
- return normalized;
416
- }
417
-
418
- function toCamelCase(value) {
419
- return String(value || "")
420
- .replace(/^[\s-_]+|[\s-_]+$/g, "")
421
- .replace(/[-_]+(.)/g, (_, chr) => (chr ? chr.toUpperCase() : ""))
422
- .replace(/^[A-Z]/, (chr) => chr.toLowerCase());
423
- }
424
-
425
435
  function normalizeArray(value) {
426
436
  if (!value) {
427
437
  return [];
@@ -455,6 +465,16 @@ async function ensureTaskContext(conductor, opts) {
455
465
  }
456
466
 
457
467
  const session = await conductor.createTaskSession(payload);
468
+
469
+ // Auto-bind current path to project if not already bound
470
+ try {
471
+ await conductor.bindProjectPath(projectId);
472
+ log(`Bound current path to project ${projectId}`);
473
+ } catch (error) {
474
+ // Ignore binding errors - it's not critical
475
+ log(`Note: Could not bind path to project: ${error.message}`);
476
+ }
477
+
458
478
  return {
459
479
  taskId: session.task_id,
460
480
  appUrl: session.app_url || null,
@@ -467,6 +487,17 @@ async function resolveProjectId(conductor, explicit) {
467
487
  return explicit;
468
488
  }
469
489
 
490
+ // First, try to match project by current path
491
+ try {
492
+ const matchResult = await conductor.matchProjectByPath();
493
+ if (matchResult?.project_id) {
494
+ log(`Matched project ${matchResult.project_name || matchResult.project_id} by path ${matchResult.matched_path}`);
495
+ return matchResult.project_id;
496
+ }
497
+ } catch (error) {
498
+ log(`Unable to match project by path: ${error.message}`);
499
+ }
500
+
470
501
  try {
471
502
  const record = await conductor.getLocalProjectRecord();
472
503
  if (record?.project_id) {
@@ -509,19 +540,36 @@ function deriveTaskTitle(prompt, explicit, backend = "codex") {
509
540
  }
510
541
 
511
542
  /**
512
- * Cli2SdkSession - Backend session using cli2sdk providers (all backends including codex).
543
+ * Cli2SdkSession - Backend session using cli2sdk with generic provider.
544
+ * All CLI commands come from config - no provider-specific argument transformation.
513
545
  */
514
546
  class Cli2SdkSession {
515
547
  constructor(backend, options = {}) {
516
548
  this.backend = backend;
517
549
  this.options = options;
518
550
 
519
- // For codex, pass threadOptions as config
520
- const config = backend === "codex"
521
- ? { provider: backend, codex: options.threadOptions }
522
- : { provider: backend };
551
+ // Get CLI command: first from env var (set by daemon), then from config file
552
+ const allowCliList = options.configFile ? loadAllowCliList(options.configFile) : DEFAULT_ALLOW_CLI_LIST;
553
+ const cliCommand = CUSTOM_CLI_COMMAND || allowCliList[backend] || backend;
554
+ const parts = cliCommand.split(/\s+/);
555
+ const command = parts[0];
556
+ const args = parts.slice(1);
557
+
558
+ // Load envs config and normalize proxy envs
559
+ const envConfig = loadEnvConfig(options.configFile);
560
+ const proxyEnv = proxyToEnv(envConfig);
561
+ const cliEnv = envConfig && typeof envConfig === "object" ? { ...envConfig, ...proxyEnv } : proxyEnv;
562
+ if (Object.keys(proxyEnv).length > 0) {
563
+ log(`Using proxy: ${proxyEnv.http_proxy || proxyEnv.https_proxy || proxyEnv.all_proxy}`);
564
+ }
565
+
566
+ log(`Using CLI command for ${backend}: ${cliCommand}`);
567
+
568
+ this.sdk = cli2sdk({
569
+ provider: "generic",
570
+ generic: { command, args, inputFormat: "text", env: cliEnv },
571
+ });
523
572
 
524
- this.sdk = cli2sdk(config);
525
573
  this.sessionId = options.resumeSessionId || `${backend}-${Date.now()}`;
526
574
  this.history = Array.isArray(options.initialHistory) ? [...options.initialHistory] : [];
527
575
  }
@@ -531,7 +579,7 @@ class Cli2SdkSession {
531
579
  }
532
580
 
533
581
  get threadOptions() {
534
- return this.backend === "codex" ? this.options.threadOptions : { model: this.backend };
582
+ return { model: this.backend };
535
583
  }
536
584
 
537
585
  async runTurn(promptText, { useInitialImages = false } = {}) {
@@ -547,19 +595,9 @@ class Cli2SdkSession {
547
595
  log(`[${this.backend}] Running prompt: ${promptText.slice(0, 100)}...`);
548
596
 
549
597
  try {
550
- if (this.backend === "codex" && useInitialImages && this.options.initialImages?.length > 0) {
551
- log("[codex] Initial images are not supported via codex CLI; ignoring images.");
552
- }
553
-
554
- const cwd =
555
- this.backend === "codex" && this.options.threadOptions?.workingDirectory
556
- ? this.options.threadOptions.workingDirectory
557
- : this.options.cwd;
558
-
559
598
  const result = await this.sdk.run(promptText, {
560
- provider: this.backend,
561
599
  history: this.history,
562
- cwd,
600
+ cwd: this.options.cwd,
563
601
  });
564
602
 
565
603
  // Update history for multi-turn conversations
@@ -689,6 +727,14 @@ class ConductorClient {
689
727
  return this.callTool("get_local_project_id", this.injectProjectPath({}));
690
728
  }
691
729
 
730
+ async matchProjectByPath() {
731
+ return this.callTool("match_project_by_path", this.injectProjectPath({}));
732
+ }
733
+
734
+ async bindProjectPath(projectId) {
735
+ return this.callTool("bind_project_path", this.injectProjectPath({ project_id: projectId }));
736
+ }
737
+
692
738
  async callTool(name, args) {
693
739
  const result = await this.client.callTool({
694
740
  name,
@@ -896,8 +942,23 @@ function logBackendReply(backend, text, { usage, replyTo }) {
896
942
  log(`${backend} reply (${replyTo}): ${text}${usageSuffix}`);
897
943
  }
898
944
 
899
- main().catch((error) => {
900
- const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
901
- process.stderr.write(`[${CLI_NAME} ${ts}] failed: ${error.stack || error.message}\n`);
902
- process.exitCode = 1;
903
- });
945
+ function isDirectRun() {
946
+ try {
947
+ if (!process.argv[1]) {
948
+ return false;
949
+ }
950
+ const argvPath = fs.realpathSync(process.argv[1]);
951
+ const scriptPath = fs.realpathSync(__filename);
952
+ return argvPath === scriptPath;
953
+ } catch {
954
+ return false;
955
+ }
956
+ }
957
+
958
+ if (isDirectRun()) {
959
+ main().catch((error) => {
960
+ const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
961
+ process.stderr.write(`[${CLI_NAME} ${ts}] failed: ${error.stack || error.message}\n`);
962
+ process.exitCode = 1;
963
+ });
964
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "conductor": "bin/conductor.js"
@@ -19,7 +19,6 @@
19
19
  "@love-moon/cli2sdk": "0.1.0",
20
20
  "@love-moon/conductor-sdk": "0.1.0",
21
21
  "@modelcontextprotocol/sdk": "^1.20.2",
22
- "@openai/codex-sdk": "^0.58.0",
23
22
  "dotenv": "^16.4.5",
24
23
  "enquirer": "^2.4.1",
25
24
  "js-yaml": "^4.1.1",
package/src/daemon.js CHANGED
@@ -42,6 +42,20 @@ function getUserConfig(configFilePath) {
42
42
  return {};
43
43
  }
44
44
 
45
+ // Default CLI commands for supported backends
46
+ const DEFAULT_CLI_LIST = {
47
+ codex: "codex --dangerously-bypass-approvals-and-sandbox",
48
+ claude: "claude --dangerously-skip-permissions",
49
+ };
50
+
51
+ function getAllowCliList(userConfig) {
52
+ // If user has configured allow_cli_list, use it; otherwise use defaults
53
+ if (userConfig.allow_cli_list && typeof userConfig.allow_cli_list === "object") {
54
+ return userConfig.allow_cli_list;
55
+ }
56
+ return DEFAULT_CLI_LIST;
57
+ }
58
+
45
59
  export function startDaemon(config = {}, deps = {}) {
46
60
  let fileConfig;
47
61
  try {
@@ -83,6 +97,10 @@ export function startDaemon(config = {}, deps = {}) {
83
97
  path.join(process.env.HOME || "/tmp", "ws");
84
98
  const CLI_PATH_VAL = config.CLI_PATH || CLI_PATH;
85
99
 
100
+ // Get allow_cli_list from config
101
+ const ALLOW_CLI_LIST = getAllowCliList(userConfig);
102
+ const SUPPORTED_BACKENDS = Object.keys(ALLOW_CLI_LIST);
103
+
86
104
  const spawnFn = deps.spawn || spawn;
87
105
  const mkdirSyncFn = deps.mkdirSync || fs.mkdirSync;
88
106
  const writeFileSyncFn = deps.writeFileSync || fs.writeFileSync;
@@ -171,6 +189,7 @@ export function startDaemon(config = {}, deps = {}) {
171
189
  log(`Workspace: ${WORKSPACE_ROOT}`);
172
190
  log(`CLI Path: ${CLI_PATH_VAL}`);
173
191
  log(`Daemon Name: ${AGENT_NAME}`);
192
+ log(`Supported Backends: ${SUPPORTED_BACKENDS.join(", ")}`);
174
193
 
175
194
  const sdkConfig = new ConductorConfig({
176
195
  agentToken: AGENT_TOKEN,
@@ -181,6 +200,7 @@ export function startDaemon(config = {}, deps = {}) {
181
200
  const client = new ConductorWebSocketClient(sdkConfig, {
182
201
  extraHeaders: {
183
202
  "x-conductor-host": AGENT_NAME,
203
+ "x-conductor-backends": SUPPORTED_BACKENDS.join(","),
184
204
  },
185
205
  });
186
206
 
@@ -203,7 +223,35 @@ export function startDaemon(config = {}, deps = {}) {
203
223
  }
204
224
  }
205
225
 
206
- function handleCreateTask(payload) {
226
+ async function getProjectLocalPath(projectId) {
227
+ try {
228
+ const response = await fetchFn(`${BACKEND_HTTP}/api/projects/${projectId}`, {
229
+ method: "GET",
230
+ headers: {
231
+ Authorization: `Bearer ${AGENT_TOKEN}`,
232
+ Accept: "application/json",
233
+ },
234
+ });
235
+ if (!response.ok) {
236
+ return null;
237
+ }
238
+ const project = await response.json();
239
+ if (!project.metadata) {
240
+ return null;
241
+ }
242
+ const metadata = typeof project.metadata === "string" ? JSON.parse(project.metadata) : project.metadata;
243
+ const localPaths = metadata.localPaths;
244
+ if (!localPaths || typeof localPaths !== "object") {
245
+ return null;
246
+ }
247
+ return localPaths[AGENT_NAME] || null;
248
+ } catch (error) {
249
+ log(`Failed to get project local path: ${error.message}`);
250
+ return null;
251
+ }
252
+ }
253
+
254
+ async function handleCreateTask(payload) {
207
255
  const { task_id: taskId, project_id: projectId, backend_type: backendType, initial_content: initialContent } =
208
256
  payload || {};
209
257
 
@@ -212,8 +260,29 @@ export function startDaemon(config = {}, deps = {}) {
212
260
  return;
213
261
  }
214
262
 
263
+ // Validate and get CLI command for the backend
264
+ const effectiveBackend = backendType || SUPPORTED_BACKENDS[0];
265
+ if (!SUPPORTED_BACKENDS.includes(effectiveBackend)) {
266
+ logError(`Unsupported backend: ${effectiveBackend}. Supported: ${SUPPORTED_BACKENDS.join(", ")}`);
267
+ client
268
+ .sendJson({
269
+ type: "task_status_update",
270
+ payload: {
271
+ task_id: taskId,
272
+ project_id: projectId,
273
+ status: "FAILED",
274
+ summary: `Unsupported backend: ${effectiveBackend}`,
275
+ },
276
+ })
277
+ .catch(() => {});
278
+ return;
279
+ }
280
+
281
+ const cliCommand = ALLOW_CLI_LIST[effectiveBackend];
282
+
215
283
  log("");
216
- log(`Creating task ${taskId} for project ${projectId} (${backendType})`);
284
+ log(`Creating task ${taskId} for project ${projectId} (${effectiveBackend})`);
285
+ log(`CLI command: ${cliCommand}`);
217
286
  client
218
287
  .sendJson({
219
288
  type: "task_status_update",
@@ -227,12 +296,27 @@ export function startDaemon(config = {}, deps = {}) {
227
296
  logError(`Failed to report task status (CREATED) for ${taskId}: ${err?.message || err}`);
228
297
  });
229
298
 
230
- const taskDir = path.join(WORKSPACE_ROOT, projectId, taskId);
231
- mkdirSyncFn(taskDir, { recursive: true });
299
+ // Check if project has a bound local path for this daemon
300
+ const boundPath = await getProjectLocalPath(projectId);
301
+ let taskDir;
302
+ let logPath;
303
+
304
+ if (boundPath) {
305
+ // Use the bound path directly (don't create subdirectory)
306
+ taskDir = boundPath;
307
+ log(`Using project bound path: ${taskDir}`);
308
+ // Create log file in the bound path
309
+ logPath = path.join(taskDir, `.conductor-${taskId}.log`);
310
+ } else {
311
+ // Use default workspace structure
312
+ taskDir = path.join(WORKSPACE_ROOT, projectId, taskId);
313
+ mkdirSyncFn(taskDir, { recursive: true });
314
+ logPath = path.join(taskDir, ".conductor.log");
315
+ }
232
316
 
233
317
  const args = [];
234
- if (backendType) {
235
- args.push("--backend", backendType);
318
+ if (effectiveBackend) {
319
+ args.push("--backend", effectiveBackend);
236
320
  }
237
321
  if (initialContent) {
238
322
  args.push("--prefill", initialContent);
@@ -240,7 +324,6 @@ export function startDaemon(config = {}, deps = {}) {
240
324
  // Explicitly separate conductor flags from backend args so they don't leak into messages
241
325
  args.push("--");
242
326
 
243
- const logPath = path.join(taskDir, ".conductor.log");
244
327
  let logStream;
245
328
  try {
246
329
  logStream = createWriteStreamFn(logPath, { flags: "a" });
@@ -255,6 +338,7 @@ export function startDaemon(config = {}, deps = {}) {
255
338
  ...process.env,
256
339
  CONDUCTOR_PROJECT_ID: projectId,
257
340
  CONDUCTOR_TASK_ID: taskId,
341
+ CONDUCTOR_CLI_COMMAND: cliCommand,
258
342
  };
259
343
  if (config.CONFIG_FILE) {
260
344
  env.CONDUCTOR_CONFIG = config.CONFIG_FILE;