@love-moon/conductor-cli 0.2.17 → 0.2.19
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-config.js +28 -29
- package/bin/conductor-fire.js +210 -18
- package/bin/conductor-send-file.js +290 -0
- package/bin/conductor.js +5 -1
- package/package.json +6 -5
- package/src/daemon.js +975 -29
- package/src/runtime-backends.js +31 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import fsp from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import process from "node:process";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
import yargs from "yargs/yargs";
|
|
11
|
+
import { hideBin } from "yargs/helpers";
|
|
12
|
+
import { ConductorConfig, loadConfig } from "@love-moon/conductor-sdk";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_MIME_TYPE = "application/octet-stream";
|
|
15
|
+
const DEFAULT_CONFIG_PATH = path.join(os.homedir(), ".conductor", "config.yaml");
|
|
16
|
+
const FIRE_TASK_MARKER_PREFIX = "active-fire";
|
|
17
|
+
|
|
18
|
+
const EXTENSION_TO_MIME = {
|
|
19
|
+
".gif": "image/gif",
|
|
20
|
+
".heic": "image/heic",
|
|
21
|
+
".jpeg": "image/jpeg",
|
|
22
|
+
".jpg": "image/jpeg",
|
|
23
|
+
".json": "application/json",
|
|
24
|
+
".mov": "video/quicktime",
|
|
25
|
+
".mp3": "audio/mpeg",
|
|
26
|
+
".mp4": "video/mp4",
|
|
27
|
+
".pdf": "application/pdf",
|
|
28
|
+
".png": "image/png",
|
|
29
|
+
".svg": "image/svg+xml",
|
|
30
|
+
".txt": "text/plain",
|
|
31
|
+
".wav": "audio/wav",
|
|
32
|
+
".webm": "video/webm",
|
|
33
|
+
".webp": "image/webp",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const isMainModule = (() => {
|
|
37
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
38
|
+
const entryFile = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
39
|
+
return entryFile === currentFile;
|
|
40
|
+
})();
|
|
41
|
+
|
|
42
|
+
function walkUpDirectories(startDir) {
|
|
43
|
+
const visited = [];
|
|
44
|
+
let currentDir = path.resolve(startDir);
|
|
45
|
+
while (true) {
|
|
46
|
+
visited.push(currentDir);
|
|
47
|
+
const parentDir = path.dirname(currentDir);
|
|
48
|
+
if (parentDir === currentDir) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
currentDir = parentDir;
|
|
52
|
+
}
|
|
53
|
+
return visited;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeTaskId(value) {
|
|
57
|
+
if (typeof value !== "string") {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
return value.trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function pickLatestTaskIdFromStateDir(stateDir) {
|
|
64
|
+
if (!fs.existsSync(stateDir)) {
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const matches = [];
|
|
69
|
+
for (const entry of fs.readdirSync(stateDir)) {
|
|
70
|
+
const match = entry.match(new RegExp(`^${FIRE_TASK_MARKER_PREFIX}\\.task_([0-9a-f-]+)\\.json$`, "i"));
|
|
71
|
+
if (!match) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const filePath = path.join(stateDir, entry);
|
|
75
|
+
let mtimeMs = 0;
|
|
76
|
+
try {
|
|
77
|
+
mtimeMs = fs.statSync(filePath).mtimeMs;
|
|
78
|
+
} catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
matches.push({ taskId: match[1], mtimeMs });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (matches.length === 0) {
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
matches.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
89
|
+
return matches[0].taskId;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function detectTaskId(options = {}) {
|
|
93
|
+
const env = options.env || process.env;
|
|
94
|
+
const cwd = options.cwd || process.cwd();
|
|
95
|
+
|
|
96
|
+
const envTaskId = normalizeTaskId(env.CONDUCTOR_TASK_ID);
|
|
97
|
+
if (envTaskId) {
|
|
98
|
+
return envTaskId;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const directory of walkUpDirectories(cwd)) {
|
|
102
|
+
const stateTaskId = pickLatestTaskIdFromStateDir(path.join(directory, ".conductor", "state"));
|
|
103
|
+
if (stateTaskId) {
|
|
104
|
+
return stateTaskId;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return "";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function loadCliConfig(configFile, env = process.env) {
|
|
112
|
+
const configPath = configFile ? path.resolve(configFile) : DEFAULT_CONFIG_PATH;
|
|
113
|
+
if (fs.existsSync(configPath)) {
|
|
114
|
+
return loadConfig(configPath, { env });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const agentToken = typeof env.CONDUCTOR_AGENT_TOKEN === "string" ? env.CONDUCTOR_AGENT_TOKEN.trim() : "";
|
|
118
|
+
const backendUrl = typeof env.CONDUCTOR_BACKEND_URL === "string" ? env.CONDUCTOR_BACKEND_URL.trim() : "";
|
|
119
|
+
if (agentToken && backendUrl) {
|
|
120
|
+
return new ConductorConfig({
|
|
121
|
+
agentToken,
|
|
122
|
+
backendUrl,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return loadConfig(configPath, { env });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function guessMimeType(fileName, preferredMimeType = "") {
|
|
130
|
+
const preferred = typeof preferredMimeType === "string" ? preferredMimeType.trim() : "";
|
|
131
|
+
if (preferred) {
|
|
132
|
+
return preferred;
|
|
133
|
+
}
|
|
134
|
+
const extension = path.extname(fileName).toLowerCase();
|
|
135
|
+
return EXTENSION_TO_MIME[extension] || DEFAULT_MIME_TYPE;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function formatErrorBody(text) {
|
|
139
|
+
const normalized = text.trim();
|
|
140
|
+
if (!normalized) {
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(normalized);
|
|
145
|
+
if (parsed && typeof parsed === "object" && typeof parsed.error === "string") {
|
|
146
|
+
return parsed.error;
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
// ignore parse failures
|
|
150
|
+
}
|
|
151
|
+
return normalized;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function sendFileToTask(options) {
|
|
155
|
+
const env = options.env || process.env;
|
|
156
|
+
const cwd = options.cwd || process.cwd();
|
|
157
|
+
const fetchImpl = options.fetchImpl || global.fetch;
|
|
158
|
+
if (typeof fetchImpl !== "function") {
|
|
159
|
+
throw new Error("fetch is not available");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const taskId = normalizeTaskId(options.taskId) || detectTaskId({ env, cwd });
|
|
163
|
+
if (!taskId) {
|
|
164
|
+
throw new Error("Unable to resolve task ID. Pass --task-id or run inside an active Conductor fire workspace.");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const config = loadCliConfig(options.configFile, env);
|
|
168
|
+
const filePath = path.resolve(cwd, String(options.filePath || ""));
|
|
169
|
+
const stats = await fsp.stat(filePath).catch(() => null);
|
|
170
|
+
if (!stats || !stats.isFile()) {
|
|
171
|
+
throw new Error(`File not found: ${filePath}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const fileBuffer = await fsp.readFile(filePath);
|
|
175
|
+
const fileName =
|
|
176
|
+
typeof options.name === "string" && options.name.trim()
|
|
177
|
+
? path.basename(options.name.trim())
|
|
178
|
+
: path.basename(filePath);
|
|
179
|
+
const mimeType = guessMimeType(fileName, options.mimeType);
|
|
180
|
+
const body = new FormData();
|
|
181
|
+
body.set("file", new Blob([fileBuffer], { type: mimeType }), fileName);
|
|
182
|
+
|
|
183
|
+
const content = typeof options.content === "string" ? options.content.trim() : "";
|
|
184
|
+
if (content) {
|
|
185
|
+
body.set("content", content);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const role = typeof options.role === "string" && options.role.trim()
|
|
189
|
+
? options.role.trim().toLowerCase()
|
|
190
|
+
: "sdk";
|
|
191
|
+
body.set("role", role);
|
|
192
|
+
|
|
193
|
+
const url = new URL(`/api/tasks/${encodeURIComponent(taskId)}/attachments`, config.backendUrl);
|
|
194
|
+
const response = await fetchImpl(String(url), {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: {
|
|
197
|
+
Authorization: `Bearer ${config.agentToken}`,
|
|
198
|
+
Accept: "application/json",
|
|
199
|
+
},
|
|
200
|
+
body,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const rawText = await response.text();
|
|
204
|
+
if (!response.ok) {
|
|
205
|
+
const details = formatErrorBody(rawText);
|
|
206
|
+
throw new Error(`Upload failed (${response.status})${details ? `: ${details}` : ""}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
taskId,
|
|
211
|
+
response: rawText ? JSON.parse(rawText) : {},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function main(argvInput = hideBin(process.argv)) {
|
|
216
|
+
await yargs(argvInput)
|
|
217
|
+
.scriptName("conductor send-file")
|
|
218
|
+
.command(
|
|
219
|
+
"$0 <file>",
|
|
220
|
+
"Upload a local file into the task session",
|
|
221
|
+
(command) => command
|
|
222
|
+
.positional("file", {
|
|
223
|
+
describe: "Path to the local file to upload into the task session",
|
|
224
|
+
type: "string",
|
|
225
|
+
demandOption: true,
|
|
226
|
+
})
|
|
227
|
+
.option("task-id", {
|
|
228
|
+
type: "string",
|
|
229
|
+
describe: "Explicit Conductor task ID. Defaults to auto-detecting the current task.",
|
|
230
|
+
})
|
|
231
|
+
.option("config-file", {
|
|
232
|
+
type: "string",
|
|
233
|
+
describe: "Path to Conductor config file",
|
|
234
|
+
})
|
|
235
|
+
.option("content", {
|
|
236
|
+
alias: "m",
|
|
237
|
+
type: "string",
|
|
238
|
+
describe: "Optional message text to accompany the uploaded file",
|
|
239
|
+
})
|
|
240
|
+
.option("role", {
|
|
241
|
+
choices: ["sdk", "assistant", "user"],
|
|
242
|
+
default: "sdk",
|
|
243
|
+
describe: "Message role to write into the task session",
|
|
244
|
+
})
|
|
245
|
+
.option("mime-type", {
|
|
246
|
+
type: "string",
|
|
247
|
+
describe: "Override MIME type detection",
|
|
248
|
+
})
|
|
249
|
+
.option("name", {
|
|
250
|
+
type: "string",
|
|
251
|
+
describe: "Override the filename shown in Conductor",
|
|
252
|
+
})
|
|
253
|
+
.option("json", {
|
|
254
|
+
type: "boolean",
|
|
255
|
+
default: false,
|
|
256
|
+
describe: "Print the raw JSON response",
|
|
257
|
+
}),
|
|
258
|
+
async (argv) => {
|
|
259
|
+
const result = await sendFileToTask({
|
|
260
|
+
filePath: argv.file,
|
|
261
|
+
taskId: argv.taskId,
|
|
262
|
+
configFile: argv.configFile,
|
|
263
|
+
content: argv.content,
|
|
264
|
+
role: argv.role,
|
|
265
|
+
mimeType: argv.mimeType,
|
|
266
|
+
name: argv.name,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
if (argv.json) {
|
|
270
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const attachments = Array.isArray(result.response?.attachments) ? result.response.attachments : [];
|
|
275
|
+
const attachmentNames = attachments.map((attachment) => attachment?.name).filter(Boolean);
|
|
276
|
+
const summary = attachmentNames.length > 0 ? attachmentNames.join(", ") : path.basename(String(argv.file));
|
|
277
|
+
process.stdout.write(`Uploaded ${summary} to task ${result.taskId}\n`);
|
|
278
|
+
},
|
|
279
|
+
)
|
|
280
|
+
.help()
|
|
281
|
+
.strict()
|
|
282
|
+
.parse();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (isMainModule) {
|
|
286
|
+
main().catch((error) => {
|
|
287
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
});
|
|
290
|
+
}
|
package/bin/conductor.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
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
|
+
* send-file - Upload a local file into a task session
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
import { fileURLToPath } from "node:url";
|
|
@@ -45,7 +46,7 @@ if (argv[0] === "--version" || argv[0] === "-v") {
|
|
|
45
46
|
const subcommand = argv[0];
|
|
46
47
|
|
|
47
48
|
// Valid subcommands
|
|
48
|
-
const validSubcommands = ["fire", "daemon", "config", "update", "diagnose"];
|
|
49
|
+
const validSubcommands = ["fire", "daemon", "config", "update", "diagnose", "send-file"];
|
|
49
50
|
|
|
50
51
|
if (!validSubcommands.includes(subcommand)) {
|
|
51
52
|
console.error(`Error: Unknown subcommand '${subcommand}'`);
|
|
@@ -88,6 +89,7 @@ Subcommands:
|
|
|
88
89
|
config Interactive configuration setup
|
|
89
90
|
update Update the CLI to the latest version
|
|
90
91
|
diagnose Diagnose a task and print likely root cause
|
|
92
|
+
send-file Upload a local file into a task session
|
|
91
93
|
|
|
92
94
|
Options:
|
|
93
95
|
-h, --help Show this help message
|
|
@@ -98,6 +100,7 @@ Examples:
|
|
|
98
100
|
conductor fire --backend claude -- "add feature"
|
|
99
101
|
conductor daemon --config-file ~/.conductor/config.yaml
|
|
100
102
|
conductor diagnose <task-id>
|
|
103
|
+
conductor send-file ./screenshot.png
|
|
101
104
|
conductor config
|
|
102
105
|
conductor update
|
|
103
106
|
|
|
@@ -107,6 +110,7 @@ For subcommand-specific help:
|
|
|
107
110
|
conductor config --help
|
|
108
111
|
conductor update --help
|
|
109
112
|
conductor diagnose --help
|
|
113
|
+
conductor send-file --help
|
|
110
114
|
|
|
111
115
|
Version: ${pkgJson.version}
|
|
112
116
|
`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/conductor-cli",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"gitCommitId": "
|
|
3
|
+
"version": "0.2.19",
|
|
4
|
+
"gitCommitId": "346e048",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"conductor": "bin/conductor.js"
|
|
@@ -14,14 +14,15 @@
|
|
|
14
14
|
"access": "public"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
|
-
"test": "node --test"
|
|
17
|
+
"test": "node --test test/*.test.js"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@love-moon/ai-sdk": "0.2.
|
|
21
|
-
"@love-moon/conductor-sdk": "0.2.
|
|
20
|
+
"@love-moon/ai-sdk": "0.2.19",
|
|
21
|
+
"@love-moon/conductor-sdk": "0.2.19",
|
|
22
22
|
"dotenv": "^16.4.5",
|
|
23
23
|
"enquirer": "^2.4.1",
|
|
24
24
|
"js-yaml": "^4.1.1",
|
|
25
|
+
"node-pty": "^1.0.0",
|
|
25
26
|
"ws": "^8.18.0",
|
|
26
27
|
"yargs": "^17.7.2",
|
|
27
28
|
"chrome-launcher": "^1.2.1",
|