@love-moon/conductor-cli 0.1.1 → 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.
- package/bin/conductor-config.js +7 -15
- package/bin/conductor-daemon.js +4 -2
- package/bin/conductor-fire.js +187 -126
- package/bin/conductor.js +104 -0
- package/package.json +2 -6
- package/src/daemon.js +91 -7
package/bin/conductor-config.js
CHANGED
|
@@ -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
|
-
"
|
|
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:
|
|
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
|
}
|
package/bin/conductor-daemon.js
CHANGED
|
@@ -11,8 +11,10 @@ import { startDaemon } from "../src/daemon.js";
|
|
|
11
11
|
|
|
12
12
|
const argv = hideBin(process.argv);
|
|
13
13
|
|
|
14
|
+
const CLI_NAME = process.env.CONDUCTOR_CLI_NAME || "conductor-daemon";
|
|
15
|
+
|
|
14
16
|
const args = yargs(argv)
|
|
15
|
-
.scriptName(
|
|
17
|
+
.scriptName(CLI_NAME)
|
|
16
18
|
.usage("Usage: $0 [--name <daemon-name>] [--clean-all] [--config-file <path>] [--nohup]")
|
|
17
19
|
.option("name", {
|
|
18
20
|
alias: "n",
|
|
@@ -56,7 +58,7 @@ if (args.nohup) {
|
|
|
56
58
|
env: { ...process.env },
|
|
57
59
|
});
|
|
58
60
|
child.unref();
|
|
59
|
-
process.stdout.write(
|
|
61
|
+
process.stdout.write(`${CLI_NAME} running in background. Logs: ${logPath}\n`);
|
|
60
62
|
process.exit(0);
|
|
61
63
|
}
|
|
62
64
|
|
package/bin/conductor-fire.js
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* conductor-fire - Conductor-aware AI coding agent runner.
|
|
5
5
|
*
|
|
6
|
-
* Supports
|
|
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
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
|
124
|
-
const backendSession =
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
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 (${
|
|
222
|
-
|
|
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 (${
|
|
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
|
|
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} --
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
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
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
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/bin/conductor.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* conductor - Main CLI entry point with subcommand routing.
|
|
5
|
+
*
|
|
6
|
+
* Subcommands:
|
|
7
|
+
* fire - Run AI coding agents with Conductor integration
|
|
8
|
+
* daemon - Start long-running daemon for task orchestration
|
|
9
|
+
* config - Interactive configuration setup
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import yargs from "yargs/yargs";
|
|
17
|
+
import { hideBin } from "yargs/helpers";
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = path.dirname(__filename);
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const PKG_ROOT = path.join(__dirname, "..");
|
|
23
|
+
|
|
24
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8"));
|
|
25
|
+
|
|
26
|
+
// Parse command line arguments
|
|
27
|
+
const argv = process.argv.slice(2);
|
|
28
|
+
|
|
29
|
+
// If no arguments or help flag, show help
|
|
30
|
+
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
31
|
+
showHelp();
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// If version flag, show version
|
|
36
|
+
if (argv[0] === "--version" || argv[0] === "-v") {
|
|
37
|
+
console.log(`conductor version ${pkgJson.version}`);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get the subcommand
|
|
42
|
+
const subcommand = argv[0];
|
|
43
|
+
|
|
44
|
+
// Valid subcommands
|
|
45
|
+
const validSubcommands = ["fire", "daemon", "config"];
|
|
46
|
+
|
|
47
|
+
if (!validSubcommands.includes(subcommand)) {
|
|
48
|
+
console.error(`Error: Unknown subcommand '${subcommand}'`);
|
|
49
|
+
console.error(`Valid subcommands: ${validSubcommands.join(", ")}`);
|
|
50
|
+
console.error(`Run 'conductor --help' for usage information.`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Route to the appropriate subcommand
|
|
55
|
+
const subcommandArgs = argv.slice(1);
|
|
56
|
+
|
|
57
|
+
// Set environment variable to track the CLI name for logging
|
|
58
|
+
process.env.CONDUCTOR_CLI_NAME = `conductor ${subcommand}`;
|
|
59
|
+
|
|
60
|
+
// Import and execute the subcommand
|
|
61
|
+
const subcommandPath = path.join(__dirname, `conductor-${subcommand}.js`);
|
|
62
|
+
|
|
63
|
+
if (!fs.existsSync(subcommandPath)) {
|
|
64
|
+
console.error(`Error: Subcommand implementation not found: ${subcommandPath}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Replace process.argv to pass the correct arguments to the subcommand
|
|
69
|
+
process.argv = [process.argv[0], subcommandPath, ...subcommandArgs];
|
|
70
|
+
|
|
71
|
+
// Dynamically import and execute the subcommand
|
|
72
|
+
import(subcommandPath).catch((error) => {
|
|
73
|
+
console.error(`Error loading subcommand '${subcommand}': ${error.message}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
function showHelp() {
|
|
78
|
+
console.log(`conductor - Conductor CLI tool
|
|
79
|
+
|
|
80
|
+
Usage: conductor <subcommand> [options]
|
|
81
|
+
|
|
82
|
+
Subcommands:
|
|
83
|
+
fire Run AI coding agents with Conductor integration
|
|
84
|
+
daemon Start long-running daemon for task orchestration
|
|
85
|
+
config Interactive configuration setup
|
|
86
|
+
|
|
87
|
+
Options:
|
|
88
|
+
-h, --help Show this help message
|
|
89
|
+
-v, --version Show version information
|
|
90
|
+
|
|
91
|
+
Examples:
|
|
92
|
+
conductor fire -- "fix the bug"
|
|
93
|
+
conductor fire --backend claude -- "add feature"
|
|
94
|
+
conductor daemon --config-file ~/.conductor/config.yaml
|
|
95
|
+
conductor config
|
|
96
|
+
|
|
97
|
+
For subcommand-specific help:
|
|
98
|
+
conductor fire --help
|
|
99
|
+
conductor daemon --help
|
|
100
|
+
conductor config --help
|
|
101
|
+
|
|
102
|
+
Version: ${pkgJson.version}
|
|
103
|
+
`);
|
|
104
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/conductor-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
|
-
"conductor
|
|
7
|
-
"conductor-fire": "bin/conductor-fire.js",
|
|
8
|
-
"conductor-daemon": "bin/conductor-daemon.js",
|
|
9
|
-
"conductor-config": "bin/conductor-config.js"
|
|
6
|
+
"conductor": "bin/conductor.js"
|
|
10
7
|
},
|
|
11
8
|
"files": [
|
|
12
9
|
"bin",
|
|
@@ -22,7 +19,6 @@
|
|
|
22
19
|
"@love-moon/cli2sdk": "0.1.0",
|
|
23
20
|
"@love-moon/conductor-sdk": "0.1.0",
|
|
24
21
|
"@modelcontextprotocol/sdk": "^1.20.2",
|
|
25
|
-
"@openai/codex-sdk": "^0.58.0",
|
|
26
22
|
"dotenv": "^16.4.5",
|
|
27
23
|
"enquirer": "^2.4.1",
|
|
28
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
|
|
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} (${
|
|
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
|
-
|
|
231
|
-
|
|
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 (
|
|
235
|
-
args.push("--backend",
|
|
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;
|