@love-moon/conductor-cli 0.15.2 → 0.17.0

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.
@@ -16,6 +16,27 @@
16
16
  * schedule list <id>
17
17
  * schedule create <id> [<message>] (--delay 10m | --at ISO | --every 1h)
18
18
  * schedule delete <id> <schedule-id>
19
+ * schedule update <id> <schedule-id> [<message>] [--delay|--at|--every ...]
20
+ * stop <id> | interrupt <id> [--target-reply-to <msg-id>]
21
+ * restart <id> [--strategy inplace|new_task] [--backend] [--refresh-session]
22
+ * [--daemon-host <h>] [--code-host <h>] [--first-message <text>]
23
+ * delete <id> --yes [--permanent] | archive <id> | unarchive <id> [--daemon-host <h>]
24
+ * rename <id> <title> | pin <id> | unpin <id> | labels <id> [<label-id>...] [--clear]
25
+ * move <id> [<project>] [--back] | share <id> | unshare <id> | shared <token|link>
26
+ * preview <id> <path>
27
+ * transcribe <audio.wav|mp3> [--language <tag>]
28
+ * speak <text> [-o FILE|-]
29
+ * persistent <id> [--enable|--disable] [--instructions ...] [--summary ...]
30
+ * round end <id> | round start <id> [<message>] [--backend] [--daemon-host] [--worktree inherit|new|none]
31
+ * cleanup-worktree <id> | terminal open|close|show <id>
32
+ * attachment download <id> <attachment-id> [-o FILE|DIR|-]
33
+ * resume --daemon-host <h> --backend <b> --session <id> [--session-file <p>] [--title] [--prompt]
34
+ *
35
+ * create also takes: --daemon-host, --agent <name>[:backend] (repeatable; first
36
+ * is the worker), --global-backend <backend>@<host>, --worktree, --persistent
37
+ * send also takes: --attach FILE (repeatable)
38
+ * messages also takes: --follow [--interval <sec>] [--until-idle]
39
+ * list also takes (without --archived): --all-projects, --project-ids a,b
19
40
  *
20
41
  * Global flags supported on every write subcommand:
21
42
  * --json, --dry-run, --project, --config-file
@@ -42,6 +63,7 @@ import {
42
63
  reportError,
43
64
  resolveProject,
44
65
  } from "../src/entity-helpers.js";
66
+ import * as extra from "../src/task-commands.js";
45
67
 
46
68
  const isMainModule = (() => {
47
69
  const currentFile = fileURLToPath(import.meta.url);
@@ -281,8 +303,14 @@ async function handleList(argv, deps) {
281
303
  // Validated here rather than with yargs implies/conflicts: those count the
282
304
  // boolean defaults as "set", and the parser's fail() does not stop the handler.
283
305
  let argsProblem = null;
284
- if (!argv.archived && (argv.search !== undefined || argv.allProjects || argv.page !== undefined)) {
285
- argsProblem = "--search, --all-projects and --page require --archived";
306
+ if (!argv.archived && (argv.search !== undefined || argv.page !== undefined)) {
307
+ argsProblem = "--search and --page require --archived";
308
+ } else if (!argv.archived && (argv.allProjects || argv.projectIds) && (argv.project || argv.issue || argv.includeMoved)) {
309
+ argsProblem = "--all-projects / --project-ids cannot be combined with --project, --issue, or --include-moved";
310
+ } else if (argv.allProjects && argv.projectIds) {
311
+ argsProblem = "Pass either --all-projects or --project-ids, not both";
312
+ } else if (argv.archived && argv.projectIds) {
313
+ argsProblem = "--project-ids cannot be combined with --archived (use --all-projects)";
286
314
  } else if (argv.archived && (argv.issue || argv.status || argv.includeMoved || (argv.allProjects && argv.project))) {
287
315
  argsProblem = "--archived cannot be combined with --issue, --status, --include-moved, or --project with --all-projects";
288
316
  }
@@ -291,6 +319,7 @@ async function handleList(argv, deps) {
291
319
  err.code = "ARGS";
292
320
  throw err;
293
321
  }
322
+ if (!argv.archived && (argv.allProjects || argv.projectIds)) return extra.handleMultiProjectList(argv, deps);
294
323
  const apis = await buildApis(deps);
295
324
  if (argv.archived) return handleArchivedList(argv, deps, apis);
296
325
  const project = await resolveProject(apis, { env: deps.env, cwd: deps.cwd, project: argv.project });
@@ -323,6 +352,16 @@ async function handleList(argv, deps) {
323
352
  }
324
353
 
325
354
  async function handleCreate(argv, deps) {
355
+ // Removed flag: an unknown argument only warns, and would start a plain local task.
356
+ if (argv.remoteWorktree !== undefined) {
357
+ const err = new Error(
358
+ "--remote-worktree was removed. Create the task in the project on the daemon that holds the code, "
359
+ + "with --global-backend <backend>@<ai-host> --worktree: the host now names the daemon that runs the AI, "
360
+ + "not the one that holds the code.",
361
+ );
362
+ err.code = "ARGS";
363
+ throw err;
364
+ }
326
365
  const apis = await buildApis(deps);
327
366
  const project = await resolveProject(apis, {
328
367
  env: deps.env,
@@ -335,6 +374,18 @@ async function handleCreate(argv, deps) {
335
374
  err.code = "ARGS";
336
375
  throw err;
337
376
  }
377
+ if (project.hidden) {
378
+ printPretty(
379
+ deps.stderr,
380
+ `Warning: project ${project.name || project.id} is hidden; the task will not show in the web task list until you unhide it.`,
381
+ );
382
+ }
383
+ if (extra.hasExtendedCreateOptions(argv)) {
384
+ // The SDK's createTask only forwards the basic fields; the extended options
385
+ // (daemon, agent group, global backend, worktree, persistent) go straight to
386
+ // the route with the same payload the web Create Task dialog sends.
387
+ return extra.handleExtendedCreate(argv, deps, project.id, title);
388
+ }
338
389
 
339
390
  const body = {
340
391
  projectId: project.id,
@@ -446,6 +497,9 @@ async function handleSend(argv, deps) {
446
497
  // `--metadata-json '{"actor":"system"}'` cannot spoof CLI audit fields
447
498
  // (review H1). The CLI's `actor: "cli"` always wins inside `audit`.
448
499
  const metadata = buildAuditMetadata(deps.env, extraMetadata || {});
500
+ if ([].concat(argv.attach || []).filter(Boolean).length > 0) {
501
+ return extra.handleSendWithAttachments(argv, deps, content, metadata);
502
+ }
449
503
  const body = {
450
504
  role: "user",
451
505
  content,
@@ -466,10 +520,10 @@ async function handleSend(argv, deps) {
466
520
  // SDK signature is `sendTaskMessage(taskId, content, options?)`. Earlier we
467
521
  // were passing the body object as the second arg, which caused the SDK's
468
522
  // `typeof content === 'string'` guard to throw at runtime (review B1).
469
- const result = await apis.tasks.sendTaskMessage(argv.id, content, {
523
+ const result = await extra.retryWhileFireOwnerMissing(() => apis.tasks.sendTaskMessage(argv.id, content, {
470
524
  role: body.role,
471
525
  metadata: body.metadata,
472
- });
526
+ }), deps);
473
527
  if (argv.json) {
474
528
  printJson(deps.stdout, result ?? { sent: true });
475
529
  return EXIT.OK;
@@ -527,6 +581,9 @@ async function handleMessages(argv, deps) {
527
581
  limit: argv.limit ? Number(argv.limit) : undefined,
528
582
  before: argv.before ? String(argv.before) : undefined,
529
583
  });
584
+ if (argv.follow) {
585
+ return extra.followMessages(argv, deps, apis, Array.isArray(list) ? list : []);
586
+ }
530
587
  if (argv.json) {
531
588
  printJson(deps.stdout, Array.isArray(list) ? list : []);
532
589
  return EXIT.OK;
@@ -623,6 +680,43 @@ async function handleScheduleDelete(argv, deps) {
623
680
  return EXIT.OK;
624
681
  }
625
682
 
683
+ async function handleScheduleUpdate(argv, deps) {
684
+ const hasMessage = (argv.message !== undefined && argv.message !== "") || argv.fromFile || argv.stdin;
685
+ const content = hasMessage
686
+ ? readMessageInput({
687
+ positional: argv.message,
688
+ fromFile: argv.fromFile,
689
+ useStdin: Boolean(argv.stdin),
690
+ stdin: deps.stdin,
691
+ })
692
+ : undefined;
693
+ const hasSchedule = argv.delay !== undefined || argv.at !== undefined || argv.every !== undefined;
694
+ const schedule = hasSchedule ? parseScheduleOptions(argv) : undefined;
695
+ return extra.handleScheduleUpdate(argv, deps, content, schedule);
696
+ }
697
+
698
+ function withMessageInput(cmd) {
699
+ return cmd
700
+ .option("stdin", { type: "boolean", default: false })
701
+ .option("from-file", { type: "string" });
702
+ }
703
+
704
+ function scheduleModeOptions(cmd) {
705
+ return cmd
706
+ .option("delay", { type: "string", describe: "Send once after a duration, e.g. 10m or 2h" })
707
+ .option("at", { type: "string", describe: "Send once at an ISO/local date-time string" })
708
+ .option("every", { type: "string", describe: "Repeat every duration, e.g. 30m or 1h" })
709
+ .option("if-idle", { type: "boolean", default: false, describe: "For repeats, only send when the AI is idle" })
710
+ .option("max-runs", { type: "number", describe: "For repeats, stop after N sends" })
711
+ .option("max-skips", { type: "number", describe: "For repeats, stop after N skips" })
712
+ .option("stop-at", { type: "string", describe: "For repeats, stop after this date-time" })
713
+ .option("keep-when-task-stopped", {
714
+ type: "boolean",
715
+ default: false,
716
+ describe: "For repeats, skip instead of completing when the task is not running",
717
+ });
718
+ }
719
+
626
720
  export async function main(argvInput = hideBin(process.argv), deps = {}) {
627
721
  const stdout = deps.stdout || process.stdout;
628
722
  const stderr = deps.stderr || process.stderr;
@@ -630,6 +724,9 @@ export async function main(argvInput = hideBin(process.argv), deps = {}) {
630
724
  const cwd = deps.cwd || process.cwd();
631
725
  const consoleErr = { error: (msg) => stderr.write(`${msg}\n`) };
632
726
  const handlerDeps = { ...deps, stdout, stderr, env, cwd };
727
+ const run = (handler) => async (argv) => {
728
+ exitCode = await handler(argv, { ...handlerDeps, configFile: argv.configFile });
729
+ };
633
730
 
634
731
  let exitCode = EXIT.OK;
635
732
  try {
@@ -654,7 +751,8 @@ export async function main(argvInput = hideBin(process.argv), deps = {}) {
654
751
  })
655
752
  .option("archived", { type: "boolean", describe: "List archived tasks instead of active ones" })
656
753
  .option("search", { type: "string", describe: "With --archived: match title or transcript text" })
657
- .option("all-projects", { type: "boolean", describe: "With --archived: search every project" })
754
+ .option("all-projects", { type: "boolean", describe: "List tasks across every project (with --archived: search every project)" })
755
+ .option("project-ids", { type: "string", describe: "Comma-separated project ids to list together (merged cross-daemon view)" })
658
756
  .option("page", { type: "number", describe: "With --archived: result page (10 per page)" }),
659
757
  async (argv) => {
660
758
  exitCode = await handleList(argv, { ...handlerDeps, configFile: argv.configFile });
@@ -680,7 +778,17 @@ export async function main(argvInput = hideBin(process.argv), deps = {}) {
680
778
  .option("parent-task-id", {
681
779
  type: "string",
682
780
  describe: "Display the new task in the same task-card group as this task",
683
- }),
781
+ })
782
+ .option("daemon-host", { type: "string", describe: "Run the AI on this daemon (defaults to the project's daemon)" })
783
+ .option("agent", {
784
+ type: "string",
785
+ array: true,
786
+ describe: "Multi-agent group member <name>[:backend]; repeat it. The first is the worker, the rest reviewers",
787
+ })
788
+ .option("global-backend", { type: "string", describe: "Run the AI on a global AI backend from settings, as <backend>@<host>" })
789
+ .option("worktree", { type: "boolean", describe: "Run in a new git worktree on the project's daemon" })
790
+ .option("remote-worktree", { type: "string", hidden: true })
791
+ .option("persistent", { type: "boolean", describe: "Create a persistent task (RFC 0039)" }),
684
792
  async (argv) => {
685
793
  exitCode = await handleCreate(argv, { ...handlerDeps, configFile: argv.configFile });
686
794
  },
@@ -709,7 +817,8 @@ export async function main(argvInput = hideBin(process.argv), deps = {}) {
709
817
  .positional("message", { type: "string" })
710
818
  .option("stdin", { type: "boolean", default: false })
711
819
  .option("from-file", { type: "string" })
712
- .option("metadata-json", { type: "string", describe: "Extra JSON metadata to merge into the message" }),
820
+ .option("metadata-json", { type: "string", describe: "Extra JSON metadata to merge into the message" })
821
+ .option("attach", { type: "string", array: true, describe: "Attach a local file; repeat for several" }),
713
822
  async (argv) => {
714
823
  exitCode = await handleSend(argv, { ...handlerDeps, configFile: argv.configFile });
715
824
  },
@@ -730,15 +839,230 @@ export async function main(argvInput = hideBin(process.argv), deps = {}) {
730
839
  )
731
840
  .command(
732
841
  "messages <id>",
733
- "Pull a slice of task messages and exit (no --follow in this RFC)",
842
+ "Pull a slice of task messages (or --follow to stream new ones)",
734
843
  (cmd) => cmd
735
844
  .positional("id", { type: "string", demandOption: true })
736
845
  .option("limit", { type: "number" })
737
- .option("before", { type: "string", describe: "Cursor: message id to paginate before" }),
846
+ .option("before", { type: "string", describe: "Cursor: message id to paginate before" })
847
+ .option("follow", { alias: "f", type: "boolean", default: false, describe: "Keep polling and print new messages" })
848
+ .option("interval", { type: "number", default: 2, describe: "With --follow: poll interval in seconds" })
849
+ .option("until-idle", { type: "boolean", default: false, describe: "With --follow: stop once the task is no longer running" }),
738
850
  async (argv) => {
739
851
  exitCode = await handleMessages(argv, { ...handlerDeps, configFile: argv.configFile });
740
852
  },
741
853
  )
854
+ .command(
855
+ "stop <id>",
856
+ "Stop (kill) a running task",
857
+ (cmd) => cmd.positional("id", { type: "string", demandOption: true }),
858
+ run(extra.handleStop),
859
+ )
860
+ .command(
861
+ "interrupt <id>",
862
+ "Interrupt the current AI turn (the Stop button in the chat)",
863
+ (cmd) => cmd
864
+ .positional("id", { type: "string", demandOption: true })
865
+ .option("target-reply-to", { type: "string", describe: "Reply target of the turn to interrupt (defaults to the latest user message)" }),
866
+ run(extra.handleInterrupt),
867
+ )
868
+ .command(
869
+ "restart <id>",
870
+ "Restart a task's AI session (in place, or as a new task)",
871
+ (cmd) => cmd
872
+ .positional("id", { type: "string", demandOption: true })
873
+ .option("strategy", { choices: ["inplace", "new_task"], describe: "inplace resumes the session; new_task forks a successor" })
874
+ .option("backend", { type: "string", describe: "Switch to this AI backend" })
875
+ .option("refresh-session", { type: "boolean", default: false, describe: "Start a fresh AI session in place" })
876
+ .option("daemon-host", { type: "string", describe: "Run the restarted AI on this daemon; with --strategy new_task it keeps working on the source task's code remotely when it can (add --code-host <same> to move the work there instead)" })
877
+ .option("code-host", { type: "string", describe: "With --strategy new_task: daemon holding the code (default: the source task's); another daemon moves the work there and needs --daemon-host set to it" })
878
+ .option("first-message", { type: "string", describe: "With --strategy new_task: first message instead of the transcript handoff" }),
879
+ run(extra.handleRestart),
880
+ )
881
+ .command(
882
+ "delete <id>",
883
+ "Delete a task (--permanent deletes an archived task for good)",
884
+ (cmd) => cmd
885
+ .positional("id", { type: "string", demandOption: true })
886
+ .option("permanent", { type: "boolean", default: false, describe: "Permanently delete an archived task" })
887
+ .option("yes", { alias: "y", type: "boolean", default: false, describe: "Confirm the deletion" }),
888
+ run(extra.handleDelete),
889
+ )
890
+ .command(
891
+ "archive <id>",
892
+ "Archive (pack) a task: stop it and keep its transcript searchable",
893
+ (cmd) => cmd.positional("id", { type: "string", demandOption: true }),
894
+ run(extra.handleArchive),
895
+ )
896
+ .command(
897
+ "unarchive <id>",
898
+ "Restore an archived task",
899
+ (cmd) => cmd
900
+ .positional("id", { type: "string", demandOption: true })
901
+ .option("daemon-host", { type: "string", describe: "Restore on this daemon (required when the original is offline)" })
902
+ .option("backend", { type: "string", describe: "Restore with this AI backend" }),
903
+ run(extra.handleUnarchive),
904
+ )
905
+ .command(
906
+ "rename <id> <title>",
907
+ "Rename a task",
908
+ (cmd) => cmd.positional("id", { type: "string", demandOption: true }).positional("title", { type: "string", demandOption: true }),
909
+ run(extra.handleRename),
910
+ )
911
+ .command(
912
+ "pin <id>",
913
+ "Pin a task to the top of the list",
914
+ (cmd) => cmd.positional("id", { type: "string", demandOption: true }),
915
+ run((argv, deps) => extra.handlePin(argv, deps, true)),
916
+ )
917
+ .command(
918
+ "unpin <id>",
919
+ "Unpin a task",
920
+ (cmd) => cmd.positional("id", { type: "string", demandOption: true }),
921
+ run((argv, deps) => extra.handlePin(argv, deps, false)),
922
+ )
923
+ .command(
924
+ "move <id> [targetProject]",
925
+ "Show a task under another project (display only; --back files it under its own project)",
926
+ (cmd) => cmd
927
+ .positional("id", { type: "string", demandOption: true })
928
+ .positional("targetProject", { type: "string", describe: "Target project id or name" })
929
+ .option("back", { type: "boolean", default: false }),
930
+ run(extra.handleMove),
931
+ )
932
+ .command(
933
+ "labels <id> [labelIds..]",
934
+ "Replace the labels attached to a task",
935
+ (cmd) => cmd
936
+ .positional("id", { type: "string", demandOption: true })
937
+ .positional("labelIds", { type: "string", array: true })
938
+ .option("clear", { type: "boolean", default: false, describe: "Remove all labels" }),
939
+ run(extra.handleLabels),
940
+ )
941
+ .command(
942
+ "share <id>",
943
+ "Create (or print) the read-only share link of a task",
944
+ (cmd) => cmd.positional("id", { type: "string", demandOption: true }),
945
+ run(extra.handleShare),
946
+ )
947
+ .command(
948
+ "preview <id> <path>",
949
+ "Open a temporary link to a file on the task's daemon (HTML, Markdown, images…)",
950
+ (cmd) => cmd
951
+ .positional("id", { type: "string", demandOption: true })
952
+ .positional("path", { type: "string", demandOption: true, describe: "Absolute, ~/ or relative to the task's working directory" }),
953
+ run(extra.handlePreview),
954
+ )
955
+ .command(
956
+ "unshare <id>",
957
+ "Revoke a task's share link",
958
+ (cmd) => cmd.positional("id", { type: "string", demandOption: true }),
959
+ run(extra.handleUnshare),
960
+ )
961
+ .command(
962
+ "shared <token>",
963
+ "Read a shared task (token or share link, as opened by the /share page)",
964
+ (cmd) => cmd.positional("token", { type: "string", demandOption: true, describe: "Share token or https://…/share/<token> link" }),
965
+ run(extra.handleShared),
966
+ )
967
+ .command(
968
+ "transcribe <file>",
969
+ "Speech to text for a .wav/.mp3 file (the web composer's voice input)",
970
+ (cmd) => cmd
971
+ .positional("file", { type: "string", demandOption: true })
972
+ .option("language", { type: "string", describe: "Language hint, e.g. zh or en" }),
973
+ run(extra.handleTranscribe),
974
+ )
975
+ .command(
976
+ "speak <text>",
977
+ "Text to speech as .mp3 (the web voice mode's reply voice)",
978
+ (cmd) => cmd
979
+ .positional("text", { type: "string", demandOption: true })
980
+ .option("output", { alias: "o", type: "string", describe: "Output file (default speech.mp3), or - for stdout" }),
981
+ run(extra.handleSpeak),
982
+ )
983
+ .command(
984
+ "persistent <id>",
985
+ "Change persistent-task settings (RFC 0039)",
986
+ (cmd) => cmd
987
+ .positional("id", { type: "string", demandOption: true })
988
+ .option("enable", { type: "boolean" })
989
+ .option("disable", { type: "boolean" })
990
+ .option("auto-end-round", { type: "boolean", describe: "End the round after 1h without a reply (default on; --no-auto-end-round to turn off)" })
991
+ .option("instructions", { type: "string", describe: "Standing instructions carried into every round" })
992
+ .option("summary", { type: "string", describe: "Carried-over summary" }),
993
+ run(extra.handlePersistent),
994
+ )
995
+ .command(
996
+ "round",
997
+ "End the current round or start a new one on a persistent task",
998
+ (cmd) => cmd
999
+ .command(
1000
+ "end <id>",
1001
+ "End the current round",
1002
+ (sub) => sub.positional("id", { type: "string", demandOption: true }),
1003
+ run(extra.handleRoundEnd),
1004
+ )
1005
+ .command(
1006
+ "start <id> [message]",
1007
+ "Start a new round (fresh AI session) with a first message",
1008
+ (sub) => withMessageInput(sub
1009
+ .positional("id", { type: "string", demandOption: true })
1010
+ .positional("message", { type: "string" }))
1011
+ .option("backend", { type: "string" })
1012
+ .option("daemon-host", { type: "string" })
1013
+ .option("worktree", { choices: ["inherit", "new", "none"] })
1014
+ .option("expected-round", { type: "number", describe: "Refuse if another client already moved past this round" }),
1015
+ run((argv, deps) => extra.handleRoundStart(argv, deps, () => readMessageInput({
1016
+ positional: argv.message,
1017
+ fromFile: argv.fromFile,
1018
+ useStdin: Boolean(argv.stdin),
1019
+ stdin: deps.stdin,
1020
+ }))),
1021
+ )
1022
+ .demandCommand(1),
1023
+ )
1024
+ .command(
1025
+ "cleanup-worktree <id>",
1026
+ "Remove a stopped task's isolated git worktree",
1027
+ (cmd) => cmd.positional("id", { type: "string", demandOption: true }),
1028
+ run(extra.handleCleanupWorktree),
1029
+ )
1030
+ .command(
1031
+ "terminal",
1032
+ "Open, show, or close the PTY terminal attached to a task",
1033
+ (cmd) => cmd
1034
+ .command("open <id>", "Attach a PTY terminal to the task", (sub) => sub.positional("id", { type: "string", demandOption: true }), run((argv, deps) => extra.handleTerminal(argv, deps, "open")))
1035
+ .command("show <id>", "Show the attached terminal", (sub) => sub.positional("id", { type: "string", demandOption: true }), run((argv, deps) => extra.handleTerminal(argv, deps, "show")))
1036
+ .command("close <id>", "Close and delete the attached terminal", (sub) => sub.positional("id", { type: "string", demandOption: true }), run((argv, deps) => extra.handleTerminal(argv, deps, "close")))
1037
+ .demandCommand(1),
1038
+ )
1039
+ .command(
1040
+ "attachment",
1041
+ "Download a message attachment",
1042
+ (cmd) => cmd
1043
+ .command(
1044
+ "download <id> <attachmentId>",
1045
+ "Save an attachment (ids are in `task messages --json`)",
1046
+ (sub) => sub
1047
+ .positional("id", { type: "string", demandOption: true })
1048
+ .positional("attachmentId", { type: "string", demandOption: true })
1049
+ .option("output", { alias: "o", type: "string", describe: "File or directory to write to; - for stdout" }),
1050
+ run(extra.handleAttachmentDownload),
1051
+ )
1052
+ .demandCommand(1),
1053
+ )
1054
+ .command(
1055
+ "resume",
1056
+ "Resume an existing AI CLI session as a new app task (see `conductor daemon sessions <host>`)",
1057
+ (cmd) => cmd
1058
+ .option("daemon-host", { type: "string", demandOption: true })
1059
+ .option("backend", { type: "string", demandOption: true })
1060
+ .option("session", { type: "string", demandOption: true, describe: "Session id" })
1061
+ .option("session-file", { type: "string", describe: "Session file path on the daemon" })
1062
+ .option("title", { type: "string" })
1063
+ .option("prompt", { type: "string", describe: "First instruction after resuming" }),
1064
+ run(extra.handleResume),
1065
+ )
742
1066
  .command(
743
1067
  "schedule",
744
1068
  "Create, list, and delete scheduled messages for a task",
@@ -776,6 +1100,17 @@ export async function main(argvInput = hideBin(process.argv), deps = {}) {
776
1100
  exitCode = await handleScheduleCreate(argv, { ...handlerDeps, configFile: argv.configFile });
777
1101
  },
778
1102
  )
1103
+ .command(
1104
+ "update <id> <scheduleId> [message]",
1105
+ "Edit an active scheduled message's text and/or schedule",
1106
+ (sub) => scheduleModeOptions(withMessageInput(sub
1107
+ .positional("id", { type: "string", demandOption: true })
1108
+ .positional("scheduleId", { type: "string", demandOption: true })
1109
+ .positional("message", { type: "string" }))),
1110
+ async (argv) => {
1111
+ exitCode = await handleScheduleUpdate(argv, { ...handlerDeps, configFile: argv.configFile });
1112
+ },
1113
+ )
779
1114
  .command(
780
1115
  "delete <id> <scheduleId>",
781
1116
  "Delete an active scheduled message",
@@ -17,6 +17,7 @@ import {
17
17
  fetchLatestVersion,
18
18
  isNewerVersion,
19
19
  detectPackageManager,
20
+ resolveBundledNodeDir,
20
21
  resolveGlobalInstallPrefix,
21
22
  } from "../src/version-check.js";
22
23
  import {
@@ -31,6 +32,7 @@ const PKG_ROOT = path.join(__dirname, "..");
31
32
 
32
33
  const pkgJson = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8"));
33
34
  const CURRENT_VERSION = pkgJson.version;
35
+ const INSTALL_TIMEOUT_MS = 15 * 60_000;
34
36
  const INSTALL_METHOD = resolveInstallMethod({
35
37
  env: process.env,
36
38
  packageRoot: PKG_ROOT,
@@ -176,6 +178,11 @@ async function performUpdate() {
176
178
  console.log("");
177
179
  }
178
180
 
181
+ const nodeDir = resolveBundledNodeDir();
182
+ if (nodeDir && !process.env.npm_config_nodedir) {
183
+ process.env.npm_config_nodedir = nodeDir;
184
+ }
185
+
179
186
  if (packageManager === "pnpm") {
180
187
  console.log(" Preparing pnpm native dependency allowlist...");
181
188
  await ensurePnpmOnlyBuiltDependencies({
@@ -212,16 +219,26 @@ async function performUpdate() {
212
219
  stdio: "inherit",
213
220
  shell: true
214
221
  });
222
+ // npm never gives up on a registry connection that stalls mid-handshake.
223
+ let timedOut = false;
224
+ const timer = setTimeout(() => {
225
+ timedOut = true;
226
+ child.kill("SIGTERM");
227
+ }, INSTALL_TIMEOUT_MS);
215
228
 
216
229
  child.on("close", (code) => {
230
+ clearTimeout(timer);
217
231
  if (code === 0) {
218
232
  resolve();
233
+ } else if (timedOut) {
234
+ reject(new Error(`${cmd} did not finish within ${INSTALL_TIMEOUT_MS / 60_000} minutes`));
219
235
  } else {
220
236
  reject(new Error(`Exit code ${code}`));
221
237
  }
222
238
  });
223
239
 
224
240
  child.on("error", (error) => {
241
+ clearTimeout(timer);
225
242
  reject(error);
226
243
  });
227
244
  });
package/bin/conductor.js CHANGED
@@ -5,17 +5,20 @@
5
5
  *
6
6
  * Subcommands:
7
7
  * fire - Run AI coding agents with Conductor integration
8
- * daemon - Start long-running daemon for task orchestration
8
+ * daemon - Start the daemon, or query/control daemons (list/restart/upgrade/share/...)
9
9
  * config - Interactive configuration setup
10
10
  * update - Update the CLI to the latest version
11
11
  * diagnose - Diagnose a task in production/backend
12
12
  * send-file - Upload a local file into a task session
13
13
  * channel - Connect user-owned chat channel providers
14
14
  * serve-ai - Start an OpenAI-compatible local AI server
15
- * project - Manage Conductor projects (list/show/create/...)
16
- * issue - Manage issues (list/show/create/update/start/done)
17
- * task - Manage tasks (create/list/show/send/messages/schedule)
15
+ * project - Manage Conductor projects (list/create/update/delete/collab/labels/...)
16
+ * issue - Manage issues (list/show/create/update/start/done/delete)
17
+ * task - Manage tasks (create/send/stop/restart/archive/share/schedule/...)
18
18
  * remote - Run commands and copy files on another daemon's host
19
+ * search - Search message history across all tasks
20
+ * settings - Manage user preferences (global backends, catchphrases, daily report, ...)
21
+ * auth - Show the signed-in user and manage API tokens
19
22
  */
20
23
 
21
24
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -52,6 +55,9 @@ export function runConductorCli(args = argv, deps = {}) {
52
55
  "issue",
53
56
  "task",
54
57
  "remote",
58
+ "search",
59
+ "settings",
60
+ "auth",
55
61
  ];
56
62
 
57
63
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
@@ -127,17 +133,23 @@ Usage: conductor <subcommand> [options]
127
133
 
128
134
  Subcommands:
129
135
  fire Run AI coding agents with Conductor integration
130
- daemon Start long-running daemon (or: daemon list/tools/quota to query daemons)
136
+ daemon Start long-running daemon (or: daemon list/tools/quota/sessions/restart/
137
+ upgrade/accounts/commands/share to query and control daemons)
131
138
  config Interactive configuration setup
132
139
  update Update the CLI to the latest version
133
140
  diagnose Diagnose a task and print likely root cause
134
141
  send-file Upload a local file into a task session
135
142
  channel Connect user-owned chat channel providers
136
143
  serve-ai Start an OpenAI-compatible local AI server
137
- project Manage Conductor projects (list/show/create/...)
138
- issue Manage issues (list/show/create/update/start/done)
139
- task Manage tasks (create/list/show/send/messages/schedule)
144
+ project Manage Conductor projects (list/show/create/update/delete/reorder/
145
+ agents/collab/labels/...)
146
+ issue Manage issues (list/show/create/update/start/done/delete)
147
+ task Manage tasks (create/send/insert/interrupt/stop/restart/delete/
148
+ archive/unarchive/rename/pin/move/labels/share/round/resume/...)
140
149
  remote Run commands and copy files on another daemon's host (exec/cp/wait/mcp)
150
+ search Search message history across all tasks
151
+ settings Manage user preferences (global-backends/catchphrases/daily-report/reports/...)
152
+ auth Show the signed-in user and manage API tokens (whoami/tokens)
141
153
 
142
154
  Options:
143
155
  -h, --help Show this help message
@@ -163,8 +175,14 @@ Examples:
163
175
  conductor task create --title "Refactor module" --prompt "Extract the parser" --backend codex
164
176
  conductor task send <task-id> "please add a unit test"
165
177
  conductor task schedule create <task-id> "follow up" --delay 10m
178
+ conductor task create --title "Refactor" --agent feature-dev --agent code-reviewer --worktree
179
+ conductor task restart <task-id> --strategy new_task --backend codex
180
+ conductor daemon upgrade macmini --wait
166
181
  conductor remote exec --target ubuntu --workspace /home/duino/ws/holomotion ls .
167
182
  conductor remote cp ./build.tar.gz ubuntu:/srv/app/build.tar.gz
183
+ conductor search "flaky test" --limit 20
184
+ conductor settings catchphrases add "please add a unit test"
185
+ conductor auth tokens create --name ci
168
186
 
169
187
  For subcommand-specific help:
170
188
  conductor fire --help
@@ -179,6 +197,9 @@ For subcommand-specific help:
179
197
  conductor issue --help
180
198
  conductor task --help
181
199
  conductor remote --help
200
+ conductor search --help
201
+ conductor settings --help
202
+ conductor auth --help
182
203
 
183
204
  Version: ${pkgJson.version}
184
205
  `);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.15.2",
4
- "gitCommitId": "f795a61",
3
+ "version": "0.17.0",
4
+ "gitCommitId": "2799bc0",
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.15.2",
28
- "@love-moon/conductor-sdk": "0.15.2",
27
+ "@love-moon/ai-sdk": "0.17.0",
28
+ "@love-moon/conductor-sdk": "0.17.0",
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.15.2"
41
+ "@love-moon/chat-web": "0.17.0"
42
42
  },
43
43
  "pnpm": {
44
44
  "onlyBuiltDependencies": [