@love-moon/conductor-cli 0.2.4 → 0.2.6
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-fire.js +30 -3
- package/package.json +3 -3
- package/src/daemon.js +90 -22
package/bin/conductor-fire.js
CHANGED
|
@@ -20,7 +20,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
|
20
20
|
import yargs from "yargs/yargs";
|
|
21
21
|
import { hideBin } from "yargs/helpers";
|
|
22
22
|
import yaml from "js-yaml";
|
|
23
|
-
import { TuiDriver, claudeCodeProfile, codexProfile } from "@love-moon/tui-driver";
|
|
23
|
+
import { TuiDriver, claudeCodeProfile, codexProfile, copilotProfile } from "@love-moon/tui-driver";
|
|
24
24
|
import { loadConfig } from "@love-moon/conductor-sdk";
|
|
25
25
|
import {
|
|
26
26
|
loadHistoryFromSpec,
|
|
@@ -145,7 +145,7 @@ async function main() {
|
|
|
145
145
|
|
|
146
146
|
if (cliArgs.listBackends) {
|
|
147
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 kimi: kimi\n`);
|
|
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 copilot: copilot --allow-all-paths --allow-all-tools\n kimi: kimi\n`);
|
|
149
149
|
} else {
|
|
150
150
|
process.stdout.write(`Supported backends (from config):\n`);
|
|
151
151
|
for (const [name, command] of Object.entries(allowCliList)) {
|
|
@@ -256,6 +256,7 @@ async function main() {
|
|
|
256
256
|
|
|
257
257
|
const signals = new AbortController();
|
|
258
258
|
let shutdownSignal = null;
|
|
259
|
+
const launchedByDaemon = Boolean(process.env.CONDUCTOR_CLI_COMMAND);
|
|
259
260
|
const onSigint = () => {
|
|
260
261
|
shutdownSignal = shutdownSignal || "SIGINT";
|
|
261
262
|
signals.abort();
|
|
@@ -272,6 +273,16 @@ async function main() {
|
|
|
272
273
|
} finally {
|
|
273
274
|
process.off("SIGINT", onSigint);
|
|
274
275
|
process.off("SIGTERM", onSigterm);
|
|
276
|
+
if (shutdownSignal && !launchedByDaemon) {
|
|
277
|
+
try {
|
|
278
|
+
await conductor.sendTaskStatus(taskContext.taskId, {
|
|
279
|
+
status: "KILLED",
|
|
280
|
+
summary: `terminated by ${shutdownSignal}`,
|
|
281
|
+
});
|
|
282
|
+
} catch (error) {
|
|
283
|
+
log(`Failed to report task status (KILLED): ${error?.message || error}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
275
286
|
if (typeof backendSession.close === "function") {
|
|
276
287
|
await backendSession.close();
|
|
277
288
|
}
|
|
@@ -392,10 +403,12 @@ Config file format (~/.conductor/config.yaml):
|
|
|
392
403
|
allow_cli_list:
|
|
393
404
|
codex: codex --dangerously-bypass-approvals-and-sandbox
|
|
394
405
|
claude: claude --dangerously-skip-permissions
|
|
406
|
+
copilot: copilot --allow-all-paths --allow-all-tools
|
|
395
407
|
|
|
396
408
|
Examples:
|
|
397
409
|
${CLI_NAME} -- "fix the bug" # Use default backend
|
|
398
410
|
${CLI_NAME} --backend claude -- "fix the bug" # Use Claude CLI backend
|
|
411
|
+
${CLI_NAME} --backend copilot -- "fix the bug" # Use GitHub Copilot CLI backend
|
|
399
412
|
${CLI_NAME} --list-backends # Show configured backends
|
|
400
413
|
${CLI_NAME} --config-file ~/.conductor/config.yaml -- "fix the bug"
|
|
401
414
|
|
|
@@ -481,6 +494,7 @@ async function ensureTaskContext(conductor, opts) {
|
|
|
481
494
|
const payload = {
|
|
482
495
|
project_id: projectId,
|
|
483
496
|
task_title: deriveTaskTitle(opts.initialPrompt, opts.requestedTitle, opts.backend),
|
|
497
|
+
backend_type: opts.backend,
|
|
484
498
|
};
|
|
485
499
|
if (opts.initialPrompt) {
|
|
486
500
|
payload.prefill = opts.initialPrompt;
|
|
@@ -565,6 +579,7 @@ const BACKEND_PROFILE_MAP = {
|
|
|
565
579
|
codex: "codex",
|
|
566
580
|
claude: "claude-code",
|
|
567
581
|
"claude-code": "claude-code",
|
|
582
|
+
copilot: "copilot",
|
|
568
583
|
};
|
|
569
584
|
|
|
570
585
|
function profileNameForBackend(backend) {
|
|
@@ -638,7 +653,12 @@ class TuiDriverSession {
|
|
|
638
653
|
throw new Error(`Backend "${backend}" is not supported by tui-driver`);
|
|
639
654
|
}
|
|
640
655
|
|
|
641
|
-
const
|
|
656
|
+
const profileMap = {
|
|
657
|
+
codex: codexProfile,
|
|
658
|
+
"claude-code": claudeCodeProfile,
|
|
659
|
+
copilot: copilotProfile,
|
|
660
|
+
};
|
|
661
|
+
const baseProfile = profileMap[profileName];
|
|
642
662
|
const envConfig = loadEnvConfig(options.configFile);
|
|
643
663
|
const proxyEnv = proxyToEnv(envConfig);
|
|
644
664
|
const cliEnv = envConfig && typeof envConfig === "object" ? { ...envConfig, ...proxyEnv } : proxyEnv;
|
|
@@ -1029,6 +1049,13 @@ class ConductorClient {
|
|
|
1029
1049
|
});
|
|
1030
1050
|
}
|
|
1031
1051
|
|
|
1052
|
+
async sendTaskStatus(taskId, payload) {
|
|
1053
|
+
return this.callTool("send_task_status", {
|
|
1054
|
+
task_id: taskId,
|
|
1055
|
+
...(payload || {}),
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1032
1059
|
async sendRuntimeStatus(taskId, payload) {
|
|
1033
1060
|
return this.callTool("send_runtime_status", {
|
|
1034
1061
|
task_id: taskId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/conductor-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"conductor": "bin/conductor.js"
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"test": "node --test"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@love-moon/tui-driver": "0.2.
|
|
20
|
-
"@love-moon/conductor-sdk": "0.2.
|
|
19
|
+
"@love-moon/tui-driver": "0.2.6",
|
|
20
|
+
"@love-moon/conductor-sdk": "0.2.6",
|
|
21
21
|
"@modelcontextprotocol/sdk": "^1.20.2",
|
|
22
22
|
"dotenv": "^16.4.5",
|
|
23
23
|
"enquirer": "^2.4.1",
|
package/src/daemon.js
CHANGED
|
@@ -59,6 +59,8 @@ function getAllowCliList(userConfig) {
|
|
|
59
59
|
export function startDaemon(config = {}, deps = {}) {
|
|
60
60
|
const exitFn = deps.exit || process.exit;
|
|
61
61
|
const killFn = deps.kill || process.kill;
|
|
62
|
+
let requestShutdown = async () => {};
|
|
63
|
+
let shutdownSignalHandled = false;
|
|
62
64
|
|
|
63
65
|
const exitAndReturn = (code) => {
|
|
64
66
|
exitFn(code);
|
|
@@ -214,18 +216,31 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
214
216
|
};
|
|
215
217
|
|
|
216
218
|
process.on("exit", cleanupLock);
|
|
219
|
+
const handleSignal = (signal) => {
|
|
220
|
+
if (shutdownSignalHandled) return;
|
|
221
|
+
shutdownSignalHandled = true;
|
|
222
|
+
void (async () => {
|
|
223
|
+
try {
|
|
224
|
+
log(`Received ${signal}, shutting down...`);
|
|
225
|
+
await requestShutdown(`signal ${signal}`);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
logError(`Graceful shutdown failed on ${signal}: ${err?.message || err}`);
|
|
228
|
+
} finally {
|
|
229
|
+
cleanupLock();
|
|
230
|
+
exitFn(0);
|
|
231
|
+
}
|
|
232
|
+
})();
|
|
233
|
+
};
|
|
217
234
|
process.on("SIGINT", () => {
|
|
218
|
-
|
|
219
|
-
process.exit();
|
|
235
|
+
handleSignal("SIGINT");
|
|
220
236
|
});
|
|
221
237
|
process.on("SIGTERM", () => {
|
|
222
|
-
|
|
223
|
-
process.exit();
|
|
238
|
+
handleSignal("SIGTERM");
|
|
224
239
|
});
|
|
225
240
|
process.on("uncaughtException", (err) => {
|
|
226
241
|
logError(`Uncaught exception: ${err}`);
|
|
227
242
|
cleanupLock();
|
|
228
|
-
|
|
243
|
+
exitFn(1);
|
|
229
244
|
});
|
|
230
245
|
|
|
231
246
|
if (config.CLEAN_ALL) {
|
|
@@ -256,6 +271,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
256
271
|
let disconnectedSinceLastConnectedLog = false;
|
|
257
272
|
let didRecoverStaleTasks = false;
|
|
258
273
|
const activeTaskProcesses = new Map();
|
|
274
|
+
const suppressedExitStatusReports = new Set();
|
|
259
275
|
const client = createWebSocketClient(sdkConfig, {
|
|
260
276
|
extraHeaders: {
|
|
261
277
|
"x-conductor-host": AGENT_NAME,
|
|
@@ -652,6 +668,8 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
652
668
|
clearTimeout(active.stopForceKillTimer);
|
|
653
669
|
}
|
|
654
670
|
activeTaskProcesses.delete(taskId);
|
|
671
|
+
const suppressExitStatusReport = suppressedExitStatusReports.has(taskId);
|
|
672
|
+
suppressedExitStatusReports.delete(taskId);
|
|
655
673
|
if (logStream) {
|
|
656
674
|
const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
|
|
657
675
|
if (signal) {
|
|
@@ -679,25 +697,59 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
679
697
|
? "completed"
|
|
680
698
|
: `exited with code ${code}`;
|
|
681
699
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
700
|
+
if (!suppressExitStatusReport) {
|
|
701
|
+
client
|
|
702
|
+
.sendJson({
|
|
703
|
+
type: "task_status_update",
|
|
704
|
+
payload: {
|
|
705
|
+
task_id: taskId,
|
|
706
|
+
project_id: projectId,
|
|
707
|
+
status,
|
|
708
|
+
summary,
|
|
709
|
+
},
|
|
710
|
+
})
|
|
711
|
+
.catch((err) => {
|
|
712
|
+
logError(`Failed to report task status (${status}) for ${taskId}: ${err?.message || err}`);
|
|
713
|
+
});
|
|
714
|
+
}
|
|
695
715
|
});
|
|
696
716
|
}
|
|
697
717
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
718
|
+
let closePromise = null;
|
|
719
|
+
async function shutdownDaemon(reason = "manual close") {
|
|
720
|
+
if (closePromise) {
|
|
721
|
+
return closePromise;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
closePromise = (async () => {
|
|
725
|
+
const activeEntries = [...activeTaskProcesses.entries()];
|
|
726
|
+
if (activeEntries.length > 0) {
|
|
727
|
+
log(`Shutdown requested (${reason}); stopping ${activeEntries.length} active task(s)`);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
await Promise.allSettled(
|
|
731
|
+
activeEntries.map(async ([taskId, record]) => {
|
|
732
|
+
suppressedExitStatusReports.add(taskId);
|
|
733
|
+
try {
|
|
734
|
+
await client.sendJson({
|
|
735
|
+
type: "task_status_update",
|
|
736
|
+
payload: {
|
|
737
|
+
task_id: taskId,
|
|
738
|
+
project_id: record.projectId,
|
|
739
|
+
status: "KILLED",
|
|
740
|
+
summary: `daemon shutdown (${reason})`,
|
|
741
|
+
},
|
|
742
|
+
});
|
|
743
|
+
} catch (err) {
|
|
744
|
+
logError(`Failed to report shutdown status (KILLED) for ${taskId}: ${err?.message || err}`);
|
|
745
|
+
}
|
|
746
|
+
}),
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
for (const [taskId, record] of activeEntries) {
|
|
750
|
+
if (record?.stopForceKillTimer) {
|
|
751
|
+
clearTimeout(record.stopForceKillTimer);
|
|
752
|
+
}
|
|
701
753
|
try {
|
|
702
754
|
if (typeof record.child?.kill === "function") {
|
|
703
755
|
record.child.kill("SIGTERM");
|
|
@@ -706,8 +758,24 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
706
758
|
logError(`Failed to stop task ${taskId} on daemon close: ${error?.message || error}`);
|
|
707
759
|
}
|
|
708
760
|
}
|
|
761
|
+
|
|
709
762
|
activeTaskProcesses.clear();
|
|
710
|
-
|
|
763
|
+
|
|
764
|
+
try {
|
|
765
|
+
await Promise.resolve(client.disconnect());
|
|
766
|
+
} catch (error) {
|
|
767
|
+
logError(`Failed to disconnect client on daemon close: ${error?.message || error}`);
|
|
768
|
+
}
|
|
769
|
+
})();
|
|
770
|
+
|
|
771
|
+
return closePromise;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
requestShutdown = shutdownDaemon;
|
|
775
|
+
|
|
776
|
+
return {
|
|
777
|
+
close: () => {
|
|
778
|
+
void shutdownDaemon();
|
|
711
779
|
},
|
|
712
780
|
};
|
|
713
781
|
}
|