@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.
@@ -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 baseProfile = profileName === "codex" ? codexProfile : claudeCodeProfile;
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.4",
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.4",
20
- "@love-moon/conductor-sdk": "0.2.4",
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
- cleanupLock();
219
- process.exit();
235
+ handleSignal("SIGINT");
220
236
  });
221
237
  process.on("SIGTERM", () => {
222
- cleanupLock();
223
- process.exit();
238
+ handleSignal("SIGTERM");
224
239
  });
225
240
  process.on("uncaughtException", (err) => {
226
241
  logError(`Uncaught exception: ${err}`);
227
242
  cleanupLock();
228
- process.exit(1);
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
- client
683
- .sendJson({
684
- type: "task_status_update",
685
- payload: {
686
- task_id: taskId,
687
- project_id: projectId,
688
- status,
689
- summary,
690
- },
691
- })
692
- .catch((err) => {
693
- logError(`Failed to report task status (${status}) for ${taskId}: ${err?.message || err}`);
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
- return {
699
- close: () => {
700
- for (const [taskId, record] of activeTaskProcesses.entries()) {
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
- client.disconnect();
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
  }