@love-moon/conductor-cli 0.2.16 → 0.2.18

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.
@@ -25,11 +25,11 @@ const DEFAULT_CLIs = {
25
25
  execArgs: "exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check",
26
26
  description: "OpenAI Codex CLI"
27
27
  },
28
- copilot: {
29
- command: "copilot",
30
- execArgs: "--allow-all-paths --allow-all-tools",
31
- description: "GitHub Copilot CLI"
32
- },
28
+ // copilot: {
29
+ // command: "copilot",
30
+ // execArgs: "--allow-all-paths --allow-all-tools",
31
+ // description: "GitHub Copilot CLI"
32
+ // },
33
33
  // gemini: {
34
34
  // command: "gemini",
35
35
  // execArgs: "",
@@ -282,15 +282,15 @@ function checkAlternativeInstallations(command) {
282
282
  );
283
283
  }
284
284
 
285
- // 特殊检查:Copilot CLI 可能是 gh copilot 扩展
286
- if (command === "copilot" || command === "copilot-chat") {
287
- try {
288
- execSync("gh copilot --help", { stdio: "pipe", timeout: 5000 });
289
- return true;
290
- } catch {
291
- // gh copilot 未安装
292
- }
293
- }
285
+ // // 特殊检查:Copilot CLI 可能是 gh copilot 扩展
286
+ // if (command === "copilot" || command === "copilot-chat") {
287
+ // try {
288
+ // execSync("gh copilot --help", { stdio: "pipe", timeout: 5000 });
289
+ // return true;
290
+ // } catch {
291
+ // // gh copilot 未安装
292
+ // }
293
+ // }
294
294
 
295
295
  // 检查文件是否存在
296
296
  for (const checkPath of commonPaths) {
@@ -32,6 +32,7 @@ const __dirname = path.dirname(__filename);
32
32
  const PKG_ROOT = path.join(__dirname, "..");
33
33
  const INITIAL_CLI_PROJECT_PATH = process.cwd();
34
34
  const FIRE_LOG_PATH = path.join(INITIAL_CLI_PROJECT_PATH, "conductor.log");
35
+ const FIRE_TASK_MARKER_PREFIX = "active-fire";
35
36
  const ENABLE_FIRE_LOCAL_LOG = !process.env.CONDUCTOR_CLI_COMMAND;
36
37
 
37
38
  const pkgJson = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8"));
@@ -303,6 +304,13 @@ async function main() {
303
304
  backend: cliArgs.backend,
304
305
  daemonName: configuredDaemonName,
305
306
  });
307
+ injectResolvedTaskId(taskContext.taskId);
308
+ injectResolvedTaskId(taskContext.taskId, env);
309
+ try {
310
+ writeFireTaskMarker(taskContext.taskId, runtimeProjectPath);
311
+ } catch (error) {
312
+ log(`Note: Could not persist local task marker: ${error.message}`);
313
+ }
306
314
 
307
315
  log(
308
316
  `Attached to Conductor task ${taskContext.taskId}${
@@ -711,6 +719,73 @@ export function resolveRequestedTaskTitle({
711
719
  return resolveTaskTitle(explicit, runtimeProjectPath);
712
720
  }
713
721
 
722
+ function normalizeTaskId(value) {
723
+ if (typeof value !== "string") {
724
+ return "";
725
+ }
726
+ return value.trim();
727
+ }
728
+
729
+ function resolveFireStateDir(workingDirectory = process.cwd()) {
730
+ const baseDir =
731
+ typeof workingDirectory === "string" && workingDirectory.trim()
732
+ ? path.resolve(workingDirectory.trim())
733
+ : process.cwd();
734
+ return path.join(baseDir, ".conductor", "state");
735
+ }
736
+
737
+ export function injectResolvedTaskId(taskId, env = process.env) {
738
+ const normalizedTaskId = normalizeTaskId(taskId);
739
+ if (!normalizedTaskId) {
740
+ return "";
741
+ }
742
+ env.CONDUCTOR_TASK_ID = normalizedTaskId;
743
+ return normalizedTaskId;
744
+ }
745
+
746
+ export function writeFireTaskMarker(taskId, workingDirectory = process.cwd()) {
747
+ const normalizedTaskId = normalizeTaskId(taskId);
748
+ if (!normalizedTaskId) {
749
+ return "";
750
+ }
751
+
752
+ const stateDir = resolveFireStateDir(workingDirectory);
753
+ const markerFileName = `${FIRE_TASK_MARKER_PREFIX}.task_${normalizedTaskId}.json`;
754
+ const markerPath = path.join(stateDir, markerFileName);
755
+
756
+ fs.mkdirSync(stateDir, { recursive: true });
757
+ fs.writeFileSync(
758
+ markerPath,
759
+ `${JSON.stringify(
760
+ {
761
+ source: "conductor-fire",
762
+ taskId: normalizedTaskId,
763
+ cwd: path.resolve(workingDirectory),
764
+ updatedAt: new Date().toISOString(),
765
+ },
766
+ null,
767
+ 2,
768
+ )}\n`,
769
+ "utf8",
770
+ );
771
+
772
+ for (const entry of fs.readdirSync(stateDir)) {
773
+ if (entry === markerFileName) {
774
+ continue;
775
+ }
776
+ if (!entry.startsWith(`${FIRE_TASK_MARKER_PREFIX}.task_`) || !entry.endsWith(".json")) {
777
+ continue;
778
+ }
779
+ try {
780
+ fs.unlinkSync(path.join(stateDir, entry));
781
+ } catch {
782
+ // ignore stale marker cleanup failures
783
+ }
784
+ }
785
+
786
+ return markerPath;
787
+ }
788
+
714
789
  function normalizeArray(value) {
715
790
  if (!value) {
716
791
  return [];
@@ -992,6 +1067,7 @@ export class BridgeRunner {
992
1067
  this.needsReconnectRecovery = false;
993
1068
  this.remoteStopInfo = null;
994
1069
  this.sessionAnnouncementSent = false;
1070
+ this.boundSessionId = "";
995
1071
  this.errorLoop = null;
996
1072
  this.errorLoopWindowMs = getBoundedEnvInt(
997
1073
  "CONDUCTOR_ERROR_LOOP_WINDOW_MS",
@@ -1058,16 +1134,11 @@ export class BridgeRunner {
1058
1134
  const message = hasRealSessionId
1059
1135
  ? `${this.backendName} session started: ${sessionId}`
1060
1136
  : `${this.backendName} session started`;
1061
- if (hasRealSessionId && typeof this.conductor?.bindTaskSession === "function") {
1062
- try {
1063
- await this.conductor.bindTaskSession(this.taskId, {
1064
- session_id: sessionId,
1065
- session_file_path: sessionFilePath || undefined,
1066
- backend_type: this.backendName,
1067
- });
1068
- } catch (error) {
1069
- log(`Failed to persist task session binding for ${this.taskId}: ${error?.message || error}`);
1070
- }
1137
+ if (hasRealSessionId) {
1138
+ await this.persistTaskSessionBinding({
1139
+ sessionId,
1140
+ sessionFilePath,
1141
+ });
1071
1142
  }
1072
1143
  try {
1073
1144
  await this.conductor.sendMessage(this.taskId, message, {
@@ -1101,6 +1172,51 @@ export class BridgeRunner {
1101
1172
  }
1102
1173
  }
1103
1174
 
1175
+ async persistTaskSessionBinding({ sessionId, sessionFilePath } = {}) {
1176
+ const normalizedSessionId = typeof sessionId === "string" ? sessionId.trim() : "";
1177
+ if (!normalizedSessionId || normalizedSessionId === this.boundSessionId) {
1178
+ return false;
1179
+ }
1180
+ if (typeof this.conductor?.bindTaskSession !== "function") {
1181
+ return false;
1182
+ }
1183
+ try {
1184
+ await this.conductor.bindTaskSession(this.taskId, {
1185
+ session_id: normalizedSessionId,
1186
+ session_file_path:
1187
+ typeof sessionFilePath === "string" && sessionFilePath.trim() ? sessionFilePath.trim() : undefined,
1188
+ backend_type: this.backendName,
1189
+ });
1190
+ this.boundSessionId = normalizedSessionId;
1191
+ return true;
1192
+ } catch (error) {
1193
+ log(`Failed to persist task session binding for ${this.taskId}: ${error?.message || error}`);
1194
+ return false;
1195
+ }
1196
+ }
1197
+
1198
+ async syncBackendSessionBinding() {
1199
+ if (!this.backendSession) {
1200
+ return false;
1201
+ }
1202
+ let sessionInfo = null;
1203
+ try {
1204
+ if (typeof this.backendSession.getSessionInfo === "function") {
1205
+ sessionInfo = this.backendSession.getSessionInfo();
1206
+ }
1207
+ if (!sessionInfo && typeof this.backendSession.ensureSessionInfo === "function") {
1208
+ sessionInfo = await this.backendSession.ensureSessionInfo();
1209
+ }
1210
+ } catch (error) {
1211
+ this.copilotLog(`session binding sync skipped: ${sanitizeForLog(error?.message || error, 160)}`);
1212
+ return false;
1213
+ }
1214
+ return this.persistTaskSessionBinding({
1215
+ sessionId: sessionInfo?.sessionId,
1216
+ sessionFilePath: sessionInfo?.sessionFilePath,
1217
+ });
1218
+ }
1219
+
1104
1220
  async start(abortSignal) {
1105
1221
  abortSignal?.addEventListener("abort", () => {
1106
1222
  this.stopped = true;
@@ -1505,6 +1621,11 @@ export class BridgeRunner {
1505
1621
  return;
1506
1622
  }
1507
1623
 
1624
+ await this.persistTaskSessionBinding({
1625
+ sessionId,
1626
+ sessionFilePath,
1627
+ });
1628
+
1508
1629
  logBackendReply(this.backendName, normalizedText, {
1509
1630
  usage: null,
1510
1631
  replyTo: replyTo || "latest",
@@ -1748,6 +1869,7 @@ export class BridgeRunner {
1748
1869
  cli_args: this.cliArgs,
1749
1870
  });
1750
1871
  }
1872
+ await this.syncBackendSessionBinding();
1751
1873
  if (replyTo) {
1752
1874
  this.processedMessageIds.add(replyTo);
1753
1875
  }
@@ -1877,6 +1999,7 @@ export class BridgeRunner {
1877
1999
  } else {
1878
2000
  this.copilotLog("synthetic session_file turn settled");
1879
2001
  }
2002
+ await this.syncBackendSessionBinding();
1880
2003
  } catch (error) {
1881
2004
  const errorMessage = error instanceof Error ? error.message : String(error);
1882
2005
  if (this.stopped && (this.remoteStopInfo || isSessionClosedError(error))) {
@@ -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.16",
4
- "gitCommitId": "13b4d6c",
3
+ "version": "0.2.18",
4
+ "gitCommitId": "942418b",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "conductor": "bin/conductor.js"
@@ -14,11 +14,11 @@
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.16",
21
- "@love-moon/conductor-sdk": "0.2.16",
20
+ "@love-moon/ai-sdk": "0.2.18",
21
+ "@love-moon/conductor-sdk": "0.2.18",
22
22
  "dotenv": "^16.4.5",
23
23
  "enquirer": "^2.4.1",
24
24
  "js-yaml": "^4.1.1",