@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 +64 -0
- package/bin/conductor-config.js +8 -2
- package/bin/conductor-remote.js +93 -0
- package/bin/conductor-send-file.js +38 -16
- package/bin/conductor.js +6 -5
- package/package.json +5 -5
- package/src/daemon.js +307 -24
- package/src/remote/archive.js +129 -0
- package/src/remote/client.js +198 -0
- package/src/remote/cp.js +796 -0
- package/{bin/conductor-remote-exec.js → src/remote/exec.js} +126 -154
- package/src/remote-file-handlers.js +703 -0
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
|
package/bin/conductor-config.js
CHANGED
|
@@ -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
|
-
"#
|
|
419
|
-
"# picker
|
|
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
|
-
|
|
188
|
+
const authHeaders = {
|
|
189
|
+
Authorization: `Bearer ${config.agentToken}`,
|
|
190
|
+
Accept: "application/json",
|
|
191
|
+
};
|
|
193
192
|
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
205
|
-
if (!
|
|
206
|
-
const details = formatErrorBody(
|
|
207
|
-
throw new Error(`Upload failed (${
|
|
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
|
-
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
4
|
-
"gitCommitId": "
|
|
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.
|
|
28
|
-
"@love-moon/conductor-sdk": "0.
|
|
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.
|
|
41
|
+
"@love-moon/chat-web": "0.12.0"
|
|
42
42
|
},
|
|
43
43
|
"pnpm": {
|
|
44
44
|
"onlyBuiltDependencies": [
|