@love-moon/conductor-cli 0.7.5 → 0.7.7
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/CHANGELOG.md +38 -0
- package/bin/conductor-channel.js +3 -4
- package/bin/conductor-chrome.js +3 -2
- package/bin/conductor-config.js +55 -5
- package/bin/conductor-daemon.js +18 -7
- package/bin/conductor-fire.js +23 -19
- package/bin/conductor-send-file.js +2 -3
- package/bin/conductor-serve-ai.js +1 -1
- package/bin/conductor.js +4 -0
- package/package.json +5 -5
- package/src/ai-manager-handlers.js +1 -1
- package/src/cli-update-notifier.js +11 -4
- package/src/conductor-paths.js +61 -0
- package/src/custom-command-handlers.js +4 -2
- package/src/daemon.js +961 -84
- package/src/entity-helpers.js +2 -8
- package/src/fire/resume.js +17 -17
- package/src/handoff-log-mask.js +43 -0
- package/src/runtime-backends.js +2 -4
- package/src/serve-ai/config.js +7 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# @love-moon/conductor-cli
|
|
2
2
|
|
|
3
|
+
## 0.7.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 400f3a7: Report a terminal task status when a stop request finds no active process, so a
|
|
8
|
+
task whose Fire already died converges instead of sitting in `killing` forever.
|
|
9
|
+
|
|
10
|
+
Drop queued terminal status events before an in-place restart reuses a working
|
|
11
|
+
directory. The durable upstream outbox lives inside that directory, so an
|
|
12
|
+
undelivered `KILLED` from the previous run was flushed on startup and killed the
|
|
13
|
+
task that had just finished resuming.
|
|
14
|
+
|
|
15
|
+
- 67498dc: Report a Fire that dies inside its tmux session instead of leaving the task
|
|
16
|
+
hanging. In tmux mode the daemon's child is the short-lived `tmux new-session`
|
|
17
|
+
client, not the Fire, so an abnormal death (crash, OOM, SIGKILL) went unreported
|
|
18
|
+
and the task sat at `running` until reconcile relabelled it as a user stop. The
|
|
19
|
+
Fire now records its own exit code into its log under a per-launch nonce, and the
|
|
20
|
+
liveness reaper classifies the death from that marker and publishes a terminal
|
|
21
|
+
status with the real cause.
|
|
22
|
+
- Updated dependencies [400f3a7]
|
|
23
|
+
- @love-moon/conductor-sdk@0.7.7
|
|
24
|
+
- @love-moon/ai-sdk@0.7.7
|
|
25
|
+
|
|
26
|
+
## 0.7.6
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- 7bbb412: Add `CONDUCTOR_HOME` support for relocating user-level configuration, logs,
|
|
31
|
+
Fire locks, sessions, update metadata, and AI manager caches while leaving
|
|
32
|
+
project-scoped `.conductor` directories and Fire task markers in place.
|
|
33
|
+
|
|
34
|
+
Migrate device authorization to `conductor.conductor-ai.top` while preserving
|
|
35
|
+
compatibility with the legacy official endpoint and self-hosted backends.
|
|
36
|
+
|
|
37
|
+
- Updated dependencies [7bbb412]
|
|
38
|
+
- @love-moon/conductor-sdk@0.7.6
|
|
39
|
+
- @love-moon/ai-sdk@0.7.6
|
|
40
|
+
|
|
3
41
|
## 0.7.5
|
|
4
42
|
|
|
5
43
|
### Patch Changes
|
package/bin/conductor-channel.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
|
-
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import process from "node:process";
|
|
7
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -10,6 +9,7 @@ import yargs from "yargs/yargs";
|
|
|
10
9
|
import { hideBin } from "yargs/helpers";
|
|
11
10
|
import { loadConfig } from "@love-moon/conductor-sdk";
|
|
12
11
|
import { envForExplicitConfigFile } from "../src/config-env.js";
|
|
12
|
+
import { resolveConductorConfigPath } from "../src/conductor-paths.js";
|
|
13
13
|
|
|
14
14
|
const isMainModule = (() => {
|
|
15
15
|
const currentFile = fileURLToPath(import.meta.url);
|
|
@@ -18,8 +18,7 @@ const isMainModule = (() => {
|
|
|
18
18
|
})();
|
|
19
19
|
|
|
20
20
|
function resolveDefaultConfigPath(env = process.env) {
|
|
21
|
-
|
|
22
|
-
return path.join(home, ".conductor", "config.yaml");
|
|
21
|
+
return resolveConductorConfigPath(undefined, env);
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
function readTextFile(filePath) {
|
|
@@ -55,7 +54,7 @@ export async function connectFeishuChannel(options = {}) {
|
|
|
55
54
|
throw new Error("fetch is not available");
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
const resolvedConfigPath =
|
|
57
|
+
const resolvedConfigPath = resolveConductorConfigPath(options.configFile, env);
|
|
59
58
|
const rawYaml = readTextFile(resolvedConfigPath);
|
|
60
59
|
const config = loadConfig(resolvedConfigPath, {
|
|
61
60
|
env: envForExplicitConfigFile(options.configFile, env),
|
package/bin/conductor-chrome.js
CHANGED
|
@@ -9,6 +9,7 @@ import yargs from 'yargs';
|
|
|
9
9
|
import { hideBin } from 'yargs/helpers';
|
|
10
10
|
import { launch as launchChrome } from 'chrome-launcher';
|
|
11
11
|
import CDP from 'chrome-remote-interface';
|
|
12
|
+
import { resolveConductorConfigPath } from '../src/conductor-paths.js';
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
const BACKEND_URLS = {
|
|
@@ -52,7 +53,7 @@ const PAGE_AUTOMATION_PATH = new URL('pageAutomation.js', AUTOMATION_SRC_ROOT);
|
|
|
52
53
|
const PROVIDERS_DIR = new URL('providers/', AUTOMATION_SRC_ROOT);
|
|
53
54
|
let automationScriptCache;
|
|
54
55
|
|
|
55
|
-
const CONFIG_PATH =
|
|
56
|
+
const CONFIG_PATH = resolveConductorConfigPath();
|
|
56
57
|
|
|
57
58
|
function expandHomeDir(maybePath) {
|
|
58
59
|
if (!maybePath) {
|
|
@@ -80,7 +81,7 @@ async function loadUserConfig() {
|
|
|
80
81
|
}
|
|
81
82
|
} catch (error) {
|
|
82
83
|
if (error.code !== 'ENOENT') {
|
|
83
|
-
console.warn(
|
|
84
|
+
console.warn(`Failed to read ${CONFIG_PATH}:`, error);
|
|
84
85
|
}
|
|
85
86
|
cachedUserConfig = {};
|
|
86
87
|
}
|
package/bin/conductor-config.js
CHANGED
|
@@ -9,9 +9,10 @@ import { execFileSync, execSync } from "node:child_process";
|
|
|
9
9
|
import yargs from "yargs/yargs";
|
|
10
10
|
import { hideBin } from "yargs/helpers";
|
|
11
11
|
import { RUNTIME_SUPPORTED_BACKENDS } from "../src/runtime-backends.js";
|
|
12
|
+
import { resolveConductorConfigPath } from "../src/conductor-paths.js";
|
|
12
13
|
|
|
13
|
-
const
|
|
14
|
-
const
|
|
14
|
+
const CONFIG_FILE = resolveConductorConfigPath();
|
|
15
|
+
const CONFIG_DIR = path.dirname(CONFIG_FILE);
|
|
15
16
|
const packageJson = JSON.parse(
|
|
16
17
|
fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"),
|
|
17
18
|
);
|
|
@@ -68,6 +69,9 @@ const defaultDaemonName = os.hostname() || "my-daemon";
|
|
|
68
69
|
const cliVersion = packageJson.version || "unknown";
|
|
69
70
|
const OPENCODE_INSTALL_URL = "https://opencode.ai/install";
|
|
70
71
|
const OPENCODE_NPM_PACKAGE = "opencode-ai";
|
|
72
|
+
const LEGACY_CONDUCTOR_PUBLIC_HOST = "conductor-ai.top";
|
|
73
|
+
const CONDUCTOR_PUBLIC_HOST = "conductor.conductor-ai.top";
|
|
74
|
+
const CONDUCTOR_PUBLIC_ORIGIN = "https://conductor.conductor-ai.top";
|
|
71
75
|
|
|
72
76
|
const COLORS = {
|
|
73
77
|
yellow: "\x1b[33m",
|
|
@@ -518,7 +522,9 @@ async function authorizeDeviceAndGetToken() {
|
|
|
518
522
|
console.log(colorize("Open this link in your browser to authorize this device:", "cyan"));
|
|
519
523
|
console.log("");
|
|
520
524
|
console.log(`Device code: ${colorize(startData.user_code, "bold")}`);
|
|
521
|
-
console.log(
|
|
525
|
+
console.log(
|
|
526
|
+
`Direct link: ${normalizeOfficialConductorUrl(startData.verification_uri_complete, backendUrl)}`,
|
|
527
|
+
);
|
|
522
528
|
console.log("");
|
|
523
529
|
console.log("Only approve the request if the web page shows the same device code.");
|
|
524
530
|
console.log("");
|
|
@@ -551,10 +557,17 @@ async function authorizeDeviceAndGetToken() {
|
|
|
551
557
|
continue;
|
|
552
558
|
}
|
|
553
559
|
if (pollData.status === "approved") {
|
|
560
|
+
const resolvedBackendUrl = normalizeOfficialConductorUrl(
|
|
561
|
+
pollData.backend_url || backendUrl,
|
|
562
|
+
backendUrl,
|
|
563
|
+
);
|
|
554
564
|
const result = {
|
|
555
565
|
agentToken: pollData.agent_token,
|
|
556
|
-
backendUrl:
|
|
557
|
-
websocketUrl:
|
|
566
|
+
backendUrl: resolvedBackendUrl,
|
|
567
|
+
websocketUrl: normalizeOfficialConductorUrl(
|
|
568
|
+
pollData.websocket_url || null,
|
|
569
|
+
backendUrl,
|
|
570
|
+
),
|
|
558
571
|
};
|
|
559
572
|
lastDeviceAuthConfig = result;
|
|
560
573
|
console.log(colorize("✓ Device authorized", "green"));
|
|
@@ -571,6 +584,43 @@ async function authorizeDeviceAndGetToken() {
|
|
|
571
584
|
throw new Error("Device authorization timed out");
|
|
572
585
|
}
|
|
573
586
|
|
|
587
|
+
function isOfficialConductorBackend(value) {
|
|
588
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
try {
|
|
592
|
+
const hostname = new URL(value).hostname;
|
|
593
|
+
return hostname === LEGACY_CONDUCTOR_PUBLIC_HOST || hostname === CONDUCTOR_PUBLIC_HOST;
|
|
594
|
+
} catch {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function normalizeOfficialConductorUrl(value, requestBackendUrl) {
|
|
600
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
601
|
+
return value;
|
|
602
|
+
}
|
|
603
|
+
if (!isOfficialConductorBackend(requestBackendUrl)) {
|
|
604
|
+
return value;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
try {
|
|
608
|
+
const parsed = new URL(value);
|
|
609
|
+
if (parsed.hostname !== LEGACY_CONDUCTOR_PUBLIC_HOST) {
|
|
610
|
+
return value;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const canonicalOrigin = new URL(CONDUCTOR_PUBLIC_ORIGIN);
|
|
614
|
+
parsed.protocol = parsed.protocol === "ws:" || parsed.protocol === "wss:"
|
|
615
|
+
? "wss:"
|
|
616
|
+
: canonicalOrigin.protocol;
|
|
617
|
+
parsed.host = canonicalOrigin.host;
|
|
618
|
+
return parsed.toString();
|
|
619
|
+
} catch {
|
|
620
|
+
return value;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
574
624
|
async function parseJsonResponse(response) {
|
|
575
625
|
try {
|
|
576
626
|
return await response.json();
|
package/bin/conductor-daemon.js
CHANGED
|
@@ -9,10 +9,16 @@ import { hideBin } from "yargs/helpers";
|
|
|
9
9
|
import yaml from "js-yaml";
|
|
10
10
|
|
|
11
11
|
import { startDaemon } from "../src/daemon.js";
|
|
12
|
+
import {
|
|
13
|
+
materializeConductorPathEnv,
|
|
14
|
+
resolveConductorConfigPath,
|
|
15
|
+
resolveConductorHome,
|
|
16
|
+
} from "../src/conductor-paths.js";
|
|
12
17
|
|
|
13
18
|
const argv = hideBin(process.argv);
|
|
14
19
|
|
|
15
20
|
const CLI_NAME = process.env.CONDUCTOR_CLI_NAME || "conductor-daemon";
|
|
21
|
+
const CONDUCTOR_HOME = resolveConductorHome();
|
|
16
22
|
|
|
17
23
|
function parseJsonArrayEnv(value) {
|
|
18
24
|
if (typeof value !== "string" || !value.trim()) {
|
|
@@ -75,8 +81,7 @@ function formatBeijingTimestampForFile(date = new Date()) {
|
|
|
75
81
|
|
|
76
82
|
function loadUserConfig(configFilePath) {
|
|
77
83
|
try {
|
|
78
|
-
const
|
|
79
|
-
const configPath = configFilePath || path.join(home, ".conductor", "config.yaml");
|
|
84
|
+
const configPath = resolveConductorConfigPath(configFilePath);
|
|
80
85
|
if (!fs.existsSync(configPath)) {
|
|
81
86
|
return {};
|
|
82
87
|
}
|
|
@@ -146,7 +151,7 @@ const args = yargs(argv)
|
|
|
146
151
|
.option("nohup", {
|
|
147
152
|
type: "boolean",
|
|
148
153
|
default: false,
|
|
149
|
-
describe:
|
|
154
|
+
describe: `Run in background and write logs to ${path.join(CONDUCTOR_HOME, "logs", "<timestamp>.log")}`,
|
|
150
155
|
})
|
|
151
156
|
.option("force", {
|
|
152
157
|
type: "boolean",
|
|
@@ -162,15 +167,21 @@ const args = yargs(argv)
|
|
|
162
167
|
type: "string",
|
|
163
168
|
describe: "Path to Conductor config file",
|
|
164
169
|
})
|
|
165
|
-
.example(
|
|
170
|
+
.example(`$0 --config-file ${resolveConductorConfigPath()}`, "Run with daemon_name from config")
|
|
166
171
|
.example("$0 --nohup", "Run daemon in background with logfile")
|
|
167
172
|
.example("$0 --nohup --force", "Restart daemon in background by stopping the existing one")
|
|
168
173
|
.help()
|
|
169
174
|
.strict()
|
|
170
175
|
.parse();
|
|
171
176
|
|
|
177
|
+
const materializedConductorPathEnv = materializeConductorPathEnv(args.configFile);
|
|
178
|
+
Object.assign(process.env, materializedConductorPathEnv);
|
|
179
|
+
const effectiveConfigFile = args.configFile
|
|
180
|
+
? materializedConductorPathEnv.CONDUCTOR_CONFIG
|
|
181
|
+
: undefined;
|
|
182
|
+
|
|
172
183
|
if (args.nohup) {
|
|
173
|
-
const workspaceRoot = resolveWorkspaceRoot(
|
|
184
|
+
const workspaceRoot = resolveWorkspaceRoot(effectiveConfigFile);
|
|
174
185
|
const runningPid = findRunningDaemonPid(workspaceRoot);
|
|
175
186
|
if (runningPid && !args.force) {
|
|
176
187
|
process.stderr.write(
|
|
@@ -179,7 +190,7 @@ if (args.nohup) {
|
|
|
179
190
|
process.exit(1);
|
|
180
191
|
}
|
|
181
192
|
|
|
182
|
-
const logsDir = path.join(
|
|
193
|
+
const logsDir = path.join(resolveConductorHome(), "logs");
|
|
183
194
|
fs.mkdirSync(logsDir, { recursive: true });
|
|
184
195
|
const timestamp = formatBeijingTimestampForFile(new Date());
|
|
185
196
|
const logPath = path.join(logsDir, `${timestamp}.log`);
|
|
@@ -197,7 +208,7 @@ if (args.nohup) {
|
|
|
197
208
|
|
|
198
209
|
startDaemon({
|
|
199
210
|
CLEAN_ALL: args.cleanAll,
|
|
200
|
-
CONFIG_FILE:
|
|
211
|
+
CONFIG_FILE: effectiveConfigFile,
|
|
201
212
|
FORCE: args.force,
|
|
202
213
|
...resolveLauncherConfig(),
|
|
203
214
|
});
|
package/bin/conductor-fire.js
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
normalizeRuntimeBackendAlias,
|
|
37
37
|
normalizeRuntimeBackendName,
|
|
38
38
|
} from "../src/runtime-backends.js";
|
|
39
|
+
import { resolveConductorConfigPath, resolveConductorHome } from "../src/conductor-paths.js";
|
|
39
40
|
|
|
40
41
|
const __filename = fileURLToPath(import.meta.url);
|
|
41
42
|
const __dirname = path.dirname(__filename);
|
|
@@ -111,8 +112,7 @@ export function shouldFireReportTaskStatus({ launchedByDaemon = false, phase } =
|
|
|
111
112
|
|
|
112
113
|
// Load allow_cli_list from config file (no defaults - must be configured)
|
|
113
114
|
function loadFireConfigYaml(configFilePath) {
|
|
114
|
-
const
|
|
115
|
-
const configPath = configFilePath || process.env.CONDUCTOR_CONFIG || path.join(home, ".conductor", "config.yaml");
|
|
115
|
+
const configPath = resolveConductorConfigPath(configFilePath);
|
|
116
116
|
try {
|
|
117
117
|
if (fs.existsSync(configPath)) {
|
|
118
118
|
const content = fs.readFileSync(configPath, "utf8");
|
|
@@ -313,14 +313,14 @@ function resolveLockWorkingDirectory(workingDirectory) {
|
|
|
313
313
|
}
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
export function resolveFreshSessionBootstrapLockPath(backendName, workingDirectory) {
|
|
316
|
+
export function resolveFreshSessionBootstrapLockPath(backendName, workingDirectory, env = process.env) {
|
|
317
317
|
const normalizedBackend = String(backendName || "").trim().toLowerCase();
|
|
318
318
|
if (normalizedBackend !== "codex") {
|
|
319
319
|
return null;
|
|
320
320
|
}
|
|
321
321
|
const lockKey = `${normalizedBackend}:${resolveLockWorkingDirectory(workingDirectory)}`;
|
|
322
322
|
const digest = createHash("sha1").update(lockKey).digest("hex");
|
|
323
|
-
return path.join(
|
|
323
|
+
return path.join(resolveConductorHome(env), "locks", `session-bootstrap-${digest}.lock`);
|
|
324
324
|
}
|
|
325
325
|
|
|
326
326
|
function acquireFileLock(lockPath) {
|
|
@@ -605,7 +605,7 @@ async function main() {
|
|
|
605
605
|
if (discoveryError) {
|
|
606
606
|
throw discoveryError;
|
|
607
607
|
}
|
|
608
|
-
process.stdout.write(`No supported backends configured.\n\nAdd allow_cli_list to your config file (
|
|
608
|
+
process.stdout.write(`No supported backends configured.\n\nAdd allow_cli_list to your config file (${resolveConductorConfigPath(cliArgs.configFile)}):\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`);
|
|
609
609
|
} else {
|
|
610
610
|
if (supportedBackends.length > 0) {
|
|
611
611
|
process.stdout.write(`Supported backends (from config):\n`);
|
|
@@ -1306,6 +1306,7 @@ export async function parseCliArgs(argvInput = process.argv) {
|
|
|
1306
1306
|
// Handle help early
|
|
1307
1307
|
if (helpWithoutSeparator) {
|
|
1308
1308
|
const defaultBackend = supportedBackends[0] || externalBackends[0] || "none";
|
|
1309
|
+
const defaultConfigPath = resolveConductorConfigPath(configFileFromArgs);
|
|
1309
1310
|
process.stdout.write(`${CLI_NAME} - Conductor-aware AI coding agent runner
|
|
1310
1311
|
|
|
1311
1312
|
Usage: ${CLI_NAME} [options] -- [backend options and prompt]
|
|
@@ -1320,7 +1321,7 @@ Options:
|
|
|
1320
1321
|
-v, --version Show Conductor CLI version and exit
|
|
1321
1322
|
-h, --help Show this help message
|
|
1322
1323
|
|
|
1323
|
-
Config file format (
|
|
1324
|
+
Config file format (${defaultConfigPath}):
|
|
1324
1325
|
allow_cli_list:
|
|
1325
1326
|
codex: codex --dangerously-bypass-approvals-and-sandbox
|
|
1326
1327
|
claude: claude --dangerously-skip-permissions
|
|
@@ -1335,9 +1336,11 @@ Examples:
|
|
|
1335
1336
|
${CLI_NAME} --backend codex --resume <id> # Resume Codex session
|
|
1336
1337
|
${CLI_NAME} --backend kimi --resume <id> # Resume Kimi session
|
|
1337
1338
|
${CLI_NAME} --list-backends # Show configured backends
|
|
1338
|
-
${CLI_NAME} --config-file
|
|
1339
|
+
${CLI_NAME} --config-file ${defaultConfigPath} -- "fix the bug"
|
|
1339
1340
|
|
|
1340
1341
|
Environment:
|
|
1342
|
+
CONDUCTOR_HOME User data directory [default: ~/.conductor]
|
|
1343
|
+
CONDUCTOR_CONFIG Config file override (takes precedence over CONDUCTOR_HOME)
|
|
1341
1344
|
CONDUCTOR_BACKEND Default backend
|
|
1342
1345
|
CONDUCTOR_PROJECT_ID Project ID to attach to
|
|
1343
1346
|
CONDUCTOR_TASK_ID Attach to existing task instead of creating new one
|
|
@@ -1460,30 +1463,31 @@ function normalizeTaskId(value) {
|
|
|
1460
1463
|
return value.trim();
|
|
1461
1464
|
}
|
|
1462
1465
|
|
|
1463
|
-
function resolveFireStateDir(workingDirectory) {
|
|
1466
|
+
export function resolveFireStateDir(workingDirectory, env = process.env) {
|
|
1464
1467
|
// Priority:
|
|
1465
1468
|
// 1. CONDUCTOR_FIRE_STATE_DIR env override (used by tests to isolate the
|
|
1466
1469
|
// marker dir into a tmpdir; also handy for ops to relocate state).
|
|
1467
|
-
// 2. Explicit `workingDirectory` argument
|
|
1468
|
-
//
|
|
1469
|
-
//
|
|
1470
|
+
// 2. Explicit `workingDirectory` argument. Production marker writes use
|
|
1471
|
+
// this project-scoped path so commands such as send-file can discover
|
|
1472
|
+
// the active task while walking up from the current directory.
|
|
1473
|
+
// 3. $CONDUCTOR_HOME/state (defaults to ~/.conductor/state) only when no
|
|
1474
|
+
// project working directory is available.
|
|
1470
1475
|
//
|
|
1471
1476
|
// We deliberately do NOT default to process.cwd(), which would pollute
|
|
1472
1477
|
// every project directory a user runs `conductor fire` from and cause tests
|
|
1473
1478
|
// to leak marker files into the repo.
|
|
1474
1479
|
const envOverride =
|
|
1475
|
-
typeof
|
|
1476
|
-
|
|
1477
|
-
?
|
|
1480
|
+
typeof env.CONDUCTOR_FIRE_STATE_DIR === "string" &&
|
|
1481
|
+
env.CONDUCTOR_FIRE_STATE_DIR.trim()
|
|
1482
|
+
? env.CONDUCTOR_FIRE_STATE_DIR.trim()
|
|
1478
1483
|
: "";
|
|
1479
1484
|
if (envOverride) {
|
|
1480
1485
|
return path.resolve(envOverride);
|
|
1481
1486
|
}
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
return path.join(baseDir, ".conductor", "state");
|
|
1487
|
+
if (typeof workingDirectory === "string" && workingDirectory.trim()) {
|
|
1488
|
+
return path.join(path.resolve(workingDirectory.trim()), ".conductor", "state");
|
|
1489
|
+
}
|
|
1490
|
+
return path.join(resolveConductorHome(env), "state");
|
|
1487
1491
|
}
|
|
1488
1492
|
|
|
1489
1493
|
/**
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import fsp from "node:fs/promises";
|
|
5
|
-
import os from "node:os";
|
|
6
5
|
import path from "node:path";
|
|
7
6
|
import process from "node:process";
|
|
8
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -11,9 +10,9 @@ import yargs from "yargs/yargs";
|
|
|
11
10
|
import { hideBin } from "yargs/helpers";
|
|
12
11
|
import { ConductorConfig, loadConfig } from "@love-moon/conductor-sdk";
|
|
13
12
|
import { envForExplicitConfigFile } from "../src/config-env.js";
|
|
13
|
+
import { resolveConductorConfigPath } from "../src/conductor-paths.js";
|
|
14
14
|
|
|
15
15
|
const DEFAULT_MIME_TYPE = "application/octet-stream";
|
|
16
|
-
const DEFAULT_CONFIG_PATH = path.join(os.homedir(), ".conductor", "config.yaml");
|
|
17
16
|
const FIRE_TASK_MARKER_PREFIX = "active-fire";
|
|
18
17
|
|
|
19
18
|
const EXTENSION_TO_MIME = {
|
|
@@ -110,7 +109,7 @@ export function detectTaskId(options = {}) {
|
|
|
110
109
|
}
|
|
111
110
|
|
|
112
111
|
function loadCliConfig(configFile, env = process.env) {
|
|
113
|
-
const configPath = configFile
|
|
112
|
+
const configPath = resolveConductorConfigPath(configFile, env);
|
|
114
113
|
const configEnv = envForExplicitConfigFile(configFile, env);
|
|
115
114
|
if (fs.existsSync(configPath)) {
|
|
116
115
|
return loadConfig(configPath, { env: configEnv });
|
|
@@ -117,7 +117,7 @@ async function main() {
|
|
|
117
117
|
default: false,
|
|
118
118
|
describe: "Overwrite an existing config-ai-serve.yaml",
|
|
119
119
|
})
|
|
120
|
-
.example("$0 init", "Create
|
|
120
|
+
.example("$0 init", "Create config-ai-serve.yaml under CONDUCTOR_HOME")
|
|
121
121
|
.example("$0 init --config-file /tmp/custom/config.yaml", "Create /tmp/custom/config-ai-serve.yaml"),
|
|
122
122
|
async (args) => {
|
|
123
123
|
const { conductorConfigPath, serveAiConfigPath } = resolveServeAiConfigPaths(args.configFile);
|
package/bin/conductor.js
CHANGED
|
@@ -140,6 +140,10 @@ Options:
|
|
|
140
140
|
-h, --help Show this help message
|
|
141
141
|
-v, --version Show version information
|
|
142
142
|
|
|
143
|
+
Environment:
|
|
144
|
+
CONDUCTOR_HOME User data directory (default: ~/.conductor)
|
|
145
|
+
CONDUCTOR_CONFIG Config file override (takes precedence over CONDUCTOR_HOME)
|
|
146
|
+
|
|
143
147
|
Examples:
|
|
144
148
|
conductor fire -- "fix the bug"
|
|
145
149
|
conductor fire --backend claude -- "add feature"
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/conductor-cli",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"gitCommitId": "
|
|
3
|
+
"version": "0.7.7",
|
|
4
|
+
"gitCommitId": "26b2dbb",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/lovemoon-ai/conductor.git"
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"test": "node --test test/*.test.js"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@love-moon/ai-sdk": "0.7.
|
|
28
|
-
"@love-moon/conductor-sdk": "0.7.
|
|
27
|
+
"@love-moon/ai-sdk": "0.7.7",
|
|
28
|
+
"@love-moon/conductor-sdk": "0.7.7",
|
|
29
29
|
"@github/copilot-sdk": "^0.3.0",
|
|
30
30
|
"chrome-launcher": "^1.2.1",
|
|
31
31
|
"chrome-remote-interface": "^0.33.0",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"optionalDependencies": {
|
|
40
40
|
"@roamhq/wrtc": "^0.10.0",
|
|
41
|
-
"@love-moon/chat-web": "0.7.
|
|
41
|
+
"@love-moon/chat-web": "0.7.7"
|
|
42
42
|
},
|
|
43
43
|
"pnpm": {
|
|
44
44
|
"onlyBuiltDependencies": [
|
|
@@ -9,7 +9,7 @@ const BASE_QUOTA_TOOLS = ["codex", "claude", "kimi", "copilot"];
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* @param {object} opts
|
|
12
|
-
* @param {string} [opts.configPath] Path to
|
|
12
|
+
* @param {string} [opts.configPath] Path to config.yaml. Defaults to $CONDUCTOR_HOME/config.yaml.
|
|
13
13
|
*/
|
|
14
14
|
export function createAiManagerHandlers(opts = {}) {
|
|
15
15
|
const manager = new AiManager(opts.configPath ? { configPath: opts.configPath } : undefined);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
|
|
5
4
|
import { buildUpgradeCommand, fetchLatestVersion, isNewerVersion } from "./version-check.js";
|
|
5
|
+
import { resolveConductorHome } from "./conductor-paths.js";
|
|
6
6
|
|
|
7
7
|
export const DEFAULT_VERSION_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
|
|
8
8
|
export const DEFAULT_VERSION_NOTIFY_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
@@ -44,8 +44,13 @@ function isTimestampOlderThan(value, ageMs, nowMs) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export function resolveVersionCheckCachePath(options = {}) {
|
|
47
|
-
const
|
|
48
|
-
|
|
47
|
+
const env = options.env || process.env;
|
|
48
|
+
const conductorHome = options.conductorHome
|
|
49
|
+
? path.resolve(options.conductorHome)
|
|
50
|
+
: options.homeDir
|
|
51
|
+
? resolveConductorHome({}, { userHome: options.homeDir })
|
|
52
|
+
: resolveConductorHome(env);
|
|
53
|
+
return path.join(conductorHome, DEFAULT_CACHE_FILE);
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
export function normalizeVersionCheckCache(value) {
|
|
@@ -171,7 +176,9 @@ export async function maybeCheckForUpdates(options = {}) {
|
|
|
171
176
|
const fetchLatestVersionFn = options.fetchLatestVersion || fetchLatestVersion;
|
|
172
177
|
const cacheOptions = {
|
|
173
178
|
cachePath: options.cachePath,
|
|
174
|
-
|
|
179
|
+
conductorHome: options.conductorHome,
|
|
180
|
+
homeDir: options.homeDir,
|
|
181
|
+
env,
|
|
175
182
|
readFile: options.readFile,
|
|
176
183
|
writeFile: options.writeFile,
|
|
177
184
|
mkdir: options.mkdir,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const CONDUCTOR_HOME_ENV_VAR = "CONDUCTOR_HOME";
|
|
5
|
+
export const CONDUCTOR_CONFIG_ENV_VAR = "CONDUCTOR_CONFIG";
|
|
6
|
+
|
|
7
|
+
function normalizeOptionalString(value) {
|
|
8
|
+
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveUserHome(env = process.env, fallbackHome = os.homedir()) {
|
|
12
|
+
return (
|
|
13
|
+
normalizeOptionalString(env?.HOME) ||
|
|
14
|
+
normalizeOptionalString(env?.USERPROFILE) ||
|
|
15
|
+
normalizeOptionalString(fallbackHome) ||
|
|
16
|
+
"/tmp"
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function resolveHomeRelativePath(value, env = process.env, fallbackHome = os.homedir()) {
|
|
21
|
+
const normalized = normalizeOptionalString(value);
|
|
22
|
+
if (!normalized) {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
const userHome = resolveUserHome(env, fallbackHome);
|
|
26
|
+
if (normalized === "~") {
|
|
27
|
+
return path.resolve(userHome);
|
|
28
|
+
}
|
|
29
|
+
if (normalized.startsWith("~/") || normalized.startsWith("~\\")) {
|
|
30
|
+
return path.resolve(userHome, normalized.slice(2));
|
|
31
|
+
}
|
|
32
|
+
return path.resolve(normalized);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveConductorHome(env = process.env, options = {}) {
|
|
36
|
+
const configuredHome = normalizeOptionalString(env?.[CONDUCTOR_HOME_ENV_VAR]);
|
|
37
|
+
if (configuredHome) {
|
|
38
|
+
return resolveHomeRelativePath(configuredHome, env, options.fallbackHome);
|
|
39
|
+
}
|
|
40
|
+
const userHome = normalizeOptionalString(options.userHome) || resolveUserHome(env, options.fallbackHome);
|
|
41
|
+
return path.join(path.resolve(userHome), ".conductor");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolveConductorConfigPath(configFile, env = process.env, options = {}) {
|
|
45
|
+
const explicitPath = normalizeOptionalString(configFile);
|
|
46
|
+
if (explicitPath) {
|
|
47
|
+
return resolveHomeRelativePath(explicitPath, env, options.fallbackHome);
|
|
48
|
+
}
|
|
49
|
+
const configuredPath = normalizeOptionalString(env?.[CONDUCTOR_CONFIG_ENV_VAR]);
|
|
50
|
+
if (configuredPath) {
|
|
51
|
+
return resolveHomeRelativePath(configuredPath, env, options.fallbackHome);
|
|
52
|
+
}
|
|
53
|
+
return path.join(resolveConductorHome(env, options), "config.yaml");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function materializeConductorPathEnv(configFile, env = process.env, options = {}) {
|
|
57
|
+
return {
|
|
58
|
+
CONDUCTOR_HOME: resolveConductorHome(env, options),
|
|
59
|
+
CONDUCTOR_CONFIG: resolveConductorConfigPath(configFile, env, options),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -7,6 +7,8 @@ import { randomUUID } from "node:crypto";
|
|
|
7
7
|
|
|
8
8
|
import yaml from "js-yaml";
|
|
9
9
|
|
|
10
|
+
import { resolveConductorConfigPath } from "./conductor-paths.js";
|
|
11
|
+
|
|
10
12
|
const VALID_ACTIONS = new Set(["list", "run", "status"]);
|
|
11
13
|
const MAX_TAIL_CHARS = 12_000;
|
|
12
14
|
const MAX_RUNS = 200;
|
|
@@ -25,7 +27,7 @@ export const CUSTOM_COMMANDS_CAPABILITY = "custom_commands";
|
|
|
25
27
|
* @param {typeof spawn} [opts.spawnFn]
|
|
26
28
|
*/
|
|
27
29
|
export function createCustomCommandHandlers(opts = {}) {
|
|
28
|
-
const configPath = opts.configPath
|
|
30
|
+
const configPath = resolveConductorConfigPath(opts.configPath);
|
|
29
31
|
const spawnFn = opts.spawnFn || spawn;
|
|
30
32
|
const runs = new Map();
|
|
31
33
|
const runningByKey = new Map();
|
|
@@ -202,7 +204,7 @@ export async function loadCustomCommands(configPath) {
|
|
|
202
204
|
* @param {string} configPath
|
|
203
205
|
* @returns {CustomCommand[]}
|
|
204
206
|
*/
|
|
205
|
-
export function parseCustomCommandsConfig(config, configPath =
|
|
207
|
+
export function parseCustomCommandsConfig(config, configPath = resolveConductorConfigPath()) {
|
|
206
208
|
const root = config && typeof config === "object" ? config : {};
|
|
207
209
|
const rawCommands = root.custom_commands;
|
|
208
210
|
if (rawCommands === undefined || rawCommands === null) {
|