@love-moon/conductor-cli 0.11.1 → 0.12.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,69 @@
1
1
  # @love-moon/conductor-cli
2
2
 
3
+ ## 0.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b749180: Add `conductor remote cp` for copying a single file to or from another daemon's
8
+ host, and fold the existing `remote-exec` command into a `conductor remote`
9
+ umbrella alongside it.
10
+
11
+ - `conductor remote exec ...` replaces `conductor remote-exec ...`. Behaviour is
12
+ unchanged; only the command path moved.
13
+ - `conductor remote cp <src> <dst>` follows scp: exactly one side is written as
14
+ `<daemon>:<path>`, and that decides the direction. Contents are verified end
15
+ to end with SHA-256 and written via a temporary file plus rename, so an
16
+ interrupted copy never truncates an existing file.
17
+ - `conductor remote cp -r` copies a directory, matching scp's destination
18
+ rules. It packs with `tar`, moves the one tarball through the ordinary
19
+ transfer and unpacks on the other side, so permissions and symlinks survive
20
+ and the wire protocol is unchanged. Needs `tar` and `remote_exec` on the
21
+ target; the limit applies to the compressed size.
22
+ - New `remote_file` daemon capability, declinable with `remote_file: false` in
23
+ the config or `CONDUCTOR_REMOTE_FILE=0`, kept separate from `remote_exec` so
24
+ version skew fails with a clean 409 instead of a timeout.
25
+ - Transfers are chunked at 32 MiB with `Content-Range` and resume automatically
26
+ after a network blip, from the server's offset going up and from the local
27
+ partial file coming down. Chunking is also what keeps the server safe at the
28
+ raised limit: no single request is large or long-lived.
29
+ - The per-file limit is now 1 GiB, bounded by a global staged-bytes budget
30
+ (8 GiB), a per-user budget (2 GiB), a free-disk floor, and reservation by
31
+ declared size at creation.
32
+ - Bytes travel over HTTP through the backend rather than the agent WebSocket.
33
+ The socket only carries a small control message. See RFC 0037.
34
+
35
+ Deploying this needs the updated `web/nginx_conf`: the file-transfer routes set
36
+ `client_max_body_size` to 64m — a per-chunk cap, not a per-file one — and turn
37
+ off `proxy_request_buffering`.
38
+
39
+ ### Patch Changes
40
+
41
+ - @love-moon/conductor-sdk@0.12.0
42
+ - @love-moon/ai-sdk@0.12.0
43
+
44
+ ## 0.11.2
45
+
46
+ ### Patch Changes
47
+
48
+ - 938efe7: Fix "New task from this" when the successor runs on a different daemon.
49
+
50
+ Picking another daemon in the dialog created the successor row and then killed
51
+ it one second later with `new task failed: Could not resolve resume cwd`. The
52
+ server intentionally drops the source machine's paths for a cross-daemon fork,
53
+ and the target daemon had nothing left to resolve: no launch-config `cwd`, no
54
+ worktree, no local project path (the default project has none, and a project
55
+ bound to another daemon is refused by design), and no source session in its own
56
+ session store.
57
+
58
+ Fork modes now fall back to a fresh `<workspace>/<date>/<run>` directory — the
59
+ same fallback `create_task` already uses — so the successor starts like a
60
+ brand-new task on that machine and picks up the conversation through the
61
+ resume-context URL. In-place restarts still fail loudly, because they must reuse
62
+ the original working directory.
63
+
64
+ - @love-moon/conductor-sdk@0.11.2
65
+ - @love-moon/ai-sdk@0.11.2
66
+
3
67
  ## 0.11.1
4
68
 
5
69
  ### Patch Changes
@@ -387,6 +387,10 @@ async function main() {
387
387
  "# Env: CONDUCTOR_REMOTE_EXEC",
388
388
  "# remote_exec: true",
389
389
  "",
390
+ "# Accept `conductor remote cp` file transfers for this host.",
391
+ "# Env: CONDUCTOR_REMOTE_FILE",
392
+ "# remote_file: true",
393
+ "",
390
394
  "# Launch each Fire process inside a detached tmux session so it survives a",
391
395
  "# daemon restart. Requires tmux on PATH. Env: CONDUCTOR_FIRE_TMUX_MODE",
392
396
  "# fire_tmux_mode: false",
@@ -415,8 +419,10 @@ async function main() {
415
419
  "# Optional features",
416
420
  "# ---------------------------------------------------------------------------",
417
421
  "",
418
- "# Extra Codex auth.json files to switch between from the app's account",
419
- "# picker. The file name (without .json) becomes the account name.",
422
+ "# Optional: extra Codex auth.json files to switch between from the app's",
423
+ "# account picker (shown only when more than one is listed). The file name",
424
+ "# (without .json) becomes the account name. Without this, quota comes from",
425
+ "# $CODEX_HOME/auth.json (default ~/.codex/auth.json).",
420
426
  "# ai_manager:",
421
427
  "# codex:",
422
428
  "# auth_json:",
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * conductor remote — act on another daemon's host.
5
+ *
6
+ * conductor remote exec --target ubuntu -- bash -lc "pnpm build 2>&1 | tail -20"
7
+ * conductor remote cp ./build.tar.gz ubuntu:/srv/app/build.tar.gz
8
+ * conductor remote cp ubuntu:/var/log/conductor.log ./conductor.log
9
+ *
10
+ * Verb dispatch is hand-rolled rather than delegated to yargs on purpose:
11
+ * `exec` has to hand an arbitrary remote argv through untouched, and yargs'
12
+ * `strict()` would try to interpret the remote command's own flags.
13
+ */
14
+
15
+ import path from "node:path";
16
+ import process from "node:process";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ import { EXIT } from "../src/remote/client.js";
20
+ import { runRemoteExec, showHelp as showExecHelp } from "../src/remote/exec.js";
21
+ import { runRemoteCp, showHelp as showCpHelp } from "../src/remote/cp.js";
22
+
23
+ export { EXIT };
24
+
25
+ const VERBS = new Map([
26
+ ["exec", runRemoteExec],
27
+ ["cp", runRemoteCp],
28
+ ]);
29
+
30
+ const isMainModule = (() => {
31
+ const currentFile = fileURLToPath(import.meta.url);
32
+ const entryFile = process.argv[1] ? path.resolve(process.argv[1]) : "";
33
+ return entryFile === currentFile;
34
+ })();
35
+
36
+ export function showHelp(consoleImpl = console) {
37
+ consoleImpl.log(`conductor remote - act on another daemon's host
38
+
39
+ Usage:
40
+ conductor remote <verb> [options]
41
+
42
+ Verbs:
43
+ exec Run a command on another daemon's host
44
+ cp Copy a file to or from another daemon's host
45
+
46
+ Options:
47
+ -h, --help Show this help
48
+
49
+ Examples:
50
+ conductor remote exec --target ubuntu --workspace /srv/app -- git log --oneline -5
51
+ conductor remote cp ./build.tar.gz ubuntu:/srv/app/build.tar.gz
52
+ conductor remote cp ubuntu:/var/log/conductor.log ./conductor.log
53
+
54
+ For verb-specific help:
55
+ conductor remote exec --help
56
+ conductor remote cp --help
57
+ `);
58
+ }
59
+
60
+ export async function runRemote(argv, deps = {}) {
61
+ const consoleImpl = deps.console || console;
62
+
63
+ if (argv.length === 0) {
64
+ showHelp(consoleImpl);
65
+ return EXIT.CLI_ERROR;
66
+ }
67
+
68
+ const verb = argv[0];
69
+ if (verb === "--help" || verb === "-h") {
70
+ showHelp(consoleImpl);
71
+ return EXIT.OK;
72
+ }
73
+
74
+ const handler = VERBS.get(verb);
75
+ if (!handler) {
76
+ // `--help` on a verb belongs to the verb, but a bare flag in the verb slot
77
+ // is a usage mistake worth naming precisely.
78
+ consoleImpl.error(`Error: unknown verb '${verb}'`);
79
+ consoleImpl.error(`Valid verbs: ${[...VERBS.keys()].join(", ")}`);
80
+ showHelp(consoleImpl);
81
+ return EXIT.CLI_ERROR;
82
+ }
83
+
84
+ return handler(argv.slice(1), deps);
85
+ }
86
+
87
+ export { showExecHelp, showCpHelp };
88
+
89
+ if (isMainModule) {
90
+ // `process.exitCode` rather than `process.exit()`: writes to a pipe are async,
91
+ // and exiting outright truncates them. Let the loop drain and end naturally.
92
+ process.exitCode = await runRemote(process.argv.slice(2));
93
+ }
@@ -182,34 +182,56 @@ export async function sendFileToTask(options) {
182
182
  body.set("file", new Blob([fileBuffer], { type: mimeType }), fileName);
183
183
 
184
184
  const content = typeof options.content === "string" ? options.content.trim() : "";
185
- if (content) {
186
- body.set("content", content);
187
- }
188
-
189
185
  const role = typeof options.role === "string" && options.role.trim()
190
186
  ? options.role.trim().toLowerCase()
191
187
  : "sdk";
192
- body.set("role", role);
188
+ const authHeaders = {
189
+ Authorization: `Bearer ${config.agentToken}`,
190
+ Accept: "application/json",
191
+ };
193
192
 
194
- const url = new URL(`/api/tasks/${encodeURIComponent(taskId)}/attachments`, config.backendUrl);
195
- const response = await fetchImpl(String(url), {
193
+ // Phase 1 stages the bytes. This endpoint only stores the file; the row it
194
+ // creates carries no message and expires within the attachment TTL.
195
+ const uploadUrl = new URL(`/api/tasks/${encodeURIComponent(taskId)}/attachments`, config.backendUrl);
196
+ const uploadResponse = await fetchImpl(String(uploadUrl), {
196
197
  method: "POST",
197
- headers: {
198
- Authorization: `Bearer ${config.agentToken}`,
199
- Accept: "application/json",
200
- },
198
+ headers: authHeaders,
201
199
  body,
202
200
  });
203
201
 
204
- const rawText = await response.text();
205
- if (!response.ok) {
206
- const details = formatErrorBody(rawText);
207
- throw new Error(`Upload failed (${response.status})${details ? `: ${details}` : ""}`);
202
+ const uploadText = await uploadResponse.text();
203
+ if (!uploadResponse.ok) {
204
+ const details = formatErrorBody(uploadText);
205
+ throw new Error(`Upload failed (${uploadResponse.status})${details ? `: ${details}` : ""}`);
206
+ }
207
+ const attachment = (uploadText ? JSON.parse(uploadText) : {}).attachment;
208
+ if (!attachment || !attachment.id) {
209
+ throw new Error("Upload succeeded but the server returned no attachment id");
210
+ }
211
+
212
+ // Phase 2 binds the staged file to a message. Without it the attachment
213
+ // never reaches the chat UI and the janitor deletes it when staging expires.
214
+ const messageUrl = new URL(`/api/tasks/${encodeURIComponent(taskId)}/messages`, config.backendUrl);
215
+ const messageResponse = await fetchImpl(String(messageUrl), {
216
+ method: "POST",
217
+ headers: { ...authHeaders, "Content-Type": "application/json" },
218
+ body: JSON.stringify({
219
+ content: content || `Attached file: ${attachment.name}`,
220
+ role,
221
+ attachmentIds: [attachment.id],
222
+ }),
223
+ });
224
+
225
+ const messageText = await messageResponse.text();
226
+ if (!messageResponse.ok) {
227
+ const details = formatErrorBody(messageText);
228
+ throw new Error(`Attach failed (${messageResponse.status})${details ? `: ${details}` : ""}`);
208
229
  }
209
230
 
210
231
  return {
211
232
  taskId,
212
- response: rawText ? JSON.parse(rawText) : {},
233
+ attachment,
234
+ response: messageText ? JSON.parse(messageText) : {},
213
235
  };
214
236
  }
215
237
 
package/bin/conductor.js CHANGED
@@ -15,7 +15,7 @@
15
15
  * project - Manage Conductor projects (list/show/create/...)
16
16
  * issue - Manage issues (list/show/create/update/start/done)
17
17
  * task - Manage tasks (create/list/show/send/messages/schedule)
18
- * remote-exec - Run a command on another daemon's host
18
+ * remote - Run commands and copy files on another daemon's host
19
19
  */
20
20
 
21
21
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -51,7 +51,7 @@ export function runConductorCli(args = argv, deps = {}) {
51
51
  "project",
52
52
  "issue",
53
53
  "task",
54
- "remote-exec",
54
+ "remote",
55
55
  ];
56
56
 
57
57
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
@@ -137,7 +137,7 @@ Subcommands:
137
137
  project Manage Conductor projects (list/show/create/...)
138
138
  issue Manage issues (list/show/create/update/start/done)
139
139
  task Manage tasks (create/list/show/send/messages/schedule)
140
- remote-exec Run a command on another daemon's host
140
+ remote Run commands and copy files on another daemon's host (exec/cp)
141
141
 
142
142
  Options:
143
143
  -h, --help Show this help message
@@ -162,7 +162,8 @@ Examples:
162
162
  conductor task create --title "Refactor module" --prompt "Extract the parser" --backend codex
163
163
  conductor task send <task-id> "please add a unit test"
164
164
  conductor task schedule create <task-id> "follow up" --delay 10m
165
- conductor remote-exec --target ubuntu --workspace /home/duino/ws/holomotion ls .
165
+ conductor remote exec --target ubuntu --workspace /home/duino/ws/holomotion ls .
166
+ conductor remote cp ./build.tar.gz ubuntu:/srv/app/build.tar.gz
166
167
 
167
168
  For subcommand-specific help:
168
169
  conductor fire --help
@@ -176,7 +177,7 @@ For subcommand-specific help:
176
177
  conductor project --help
177
178
  conductor issue --help
178
179
  conductor task --help
179
- conductor remote-exec --help
180
+ conductor remote --help
180
181
 
181
182
  Version: ${pkgJson.version}
182
183
  `);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.11.1",
4
- "gitCommitId": "c174c7e",
3
+ "version": "0.12.0",
4
+ "gitCommitId": "c1047e8",
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.11.1",
28
- "@love-moon/conductor-sdk": "0.11.1",
27
+ "@love-moon/ai-sdk": "0.12.0",
28
+ "@love-moon/conductor-sdk": "0.12.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.11.1"
41
+ "@love-moon/chat-web": "0.12.0"
42
42
  },
43
43
  "pnpm": {
44
44
  "onlyBuiltDependencies": [