@love-moon/conductor-cli 0.2.9 → 0.2.11
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 +17 -26
- package/bin/conductor-fire.js +92 -3
- package/bin/conductor-update.js +307 -0
- package/bin/conductor.js +8 -4
- package/package.json +3 -3
- package/src/daemon.js +31 -2
package/bin/conductor-config.js
CHANGED
|
@@ -27,34 +27,24 @@ const DEFAULT_CLIs = {
|
|
|
27
27
|
},
|
|
28
28
|
copilot: {
|
|
29
29
|
command: "copilot",
|
|
30
|
-
execArgs: "--allow-all-paths --allow-all-tools
|
|
30
|
+
execArgs: "--allow-all-paths --allow-all-tools",
|
|
31
31
|
description: "GitHub Copilot CLI"
|
|
32
32
|
},
|
|
33
|
-
gemini: {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
},
|
|
38
|
-
opencode: {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
},
|
|
43
|
-
kimi: {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
},
|
|
48
|
-
aider: {
|
|
49
|
-
command: "aider",
|
|
50
|
-
execArgs: "",
|
|
51
|
-
description: "Aider AI Pair Programming"
|
|
52
|
-
},
|
|
53
|
-
goose: {
|
|
54
|
-
command: "goose",
|
|
55
|
-
execArgs: "",
|
|
56
|
-
description: "Goose AI Agent CLI"
|
|
57
|
-
},
|
|
33
|
+
// gemini: {
|
|
34
|
+
// command: "gemini",
|
|
35
|
+
// execArgs: "",
|
|
36
|
+
// description: "Google Gemini CLI"
|
|
37
|
+
// },
|
|
38
|
+
// opencode: {
|
|
39
|
+
// command: "opencode",
|
|
40
|
+
// execArgs: "",
|
|
41
|
+
// description: "OpenCode CLI"
|
|
42
|
+
// },
|
|
43
|
+
// kimi: {
|
|
44
|
+
// command: "kimi",
|
|
45
|
+
// execArgs: "--yolo --print --prompt",
|
|
46
|
+
// description: "Kimi CLI"
|
|
47
|
+
// },
|
|
58
48
|
};
|
|
59
49
|
|
|
60
50
|
const backendUrl =
|
|
@@ -168,6 +158,7 @@ async function main() {
|
|
|
168
158
|
`agent_token: ${yamlQuote(token)}`,
|
|
169
159
|
`backend_url: ${yamlQuote(backendUrl)}`,
|
|
170
160
|
"log_level: debug",
|
|
161
|
+
"workspace: '~/ws/fires'",
|
|
171
162
|
"",
|
|
172
163
|
"# Allowed coding CLIs",
|
|
173
164
|
"allow_cli_list:"
|
package/bin/conductor-fire.js
CHANGED
|
@@ -30,6 +30,8 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
30
30
|
const __dirname = path.dirname(__filename);
|
|
31
31
|
const PKG_ROOT = path.join(__dirname, "..");
|
|
32
32
|
const CLI_PROJECT_PATH = process.cwd();
|
|
33
|
+
const FIRE_LOG_PATH = path.join(CLI_PROJECT_PATH, "conductor.log");
|
|
34
|
+
const ENABLE_FIRE_LOCAL_LOG = !process.env.CONDUCTOR_CLI_COMMAND;
|
|
33
35
|
|
|
34
36
|
const pkgJson = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8"));
|
|
35
37
|
const CLI_NAME = (process.env.CONDUCTOR_CLI_NAME || path.basename(process.argv[1] || "conductor-fire")).replace(
|
|
@@ -106,6 +108,17 @@ const DEFAULT_POLL_INTERVAL_MS = parseInt(
|
|
|
106
108
|
10,
|
|
107
109
|
);
|
|
108
110
|
|
|
111
|
+
function appendFireLocalLog(line) {
|
|
112
|
+
if (!ENABLE_FIRE_LOCAL_LOG) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
fs.appendFileSync(FIRE_LOG_PATH, line);
|
|
117
|
+
} catch {
|
|
118
|
+
// ignore file log errors
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
109
122
|
async function main() {
|
|
110
123
|
const cliArgs = parseCliArgs();
|
|
111
124
|
|
|
@@ -1057,6 +1070,7 @@ class BridgeRunner {
|
|
|
1057
1070
|
this.includeInitialImages = includeInitialImages;
|
|
1058
1071
|
this.cliArgs = cliArgs;
|
|
1059
1072
|
this.backendName = backendName || "codex";
|
|
1073
|
+
this.isCopilotBackend = String(this.backendName).toLowerCase() === "copilot";
|
|
1060
1074
|
this.stopped = false;
|
|
1061
1075
|
this.runningTurn = false;
|
|
1062
1076
|
this.processedMessageIds = new Set();
|
|
@@ -1065,16 +1079,25 @@ class BridgeRunner {
|
|
|
1065
1079
|
this.needsReconnectRecovery = false;
|
|
1066
1080
|
}
|
|
1067
1081
|
|
|
1082
|
+
copilotLog(message) {
|
|
1083
|
+
if (!this.isCopilotBackend) {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
log(`[copilot-debug] task=${this.taskId} ${message}`);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1068
1089
|
async start(abortSignal) {
|
|
1069
1090
|
abortSignal?.addEventListener("abort", () => {
|
|
1070
1091
|
this.stopped = true;
|
|
1071
1092
|
});
|
|
1072
1093
|
|
|
1073
1094
|
if (this.initialPrompt) {
|
|
1095
|
+
this.copilotLog("processing initial prompt");
|
|
1074
1096
|
await this.handleSyntheticMessage(this.initialPrompt, {
|
|
1075
1097
|
includeImages: this.includeInitialImages,
|
|
1076
1098
|
});
|
|
1077
1099
|
}
|
|
1100
|
+
this.copilotLog("running startup backfill");
|
|
1078
1101
|
await this.backfillPendingUserMessages();
|
|
1079
1102
|
|
|
1080
1103
|
while (!this.stopped) {
|
|
@@ -1120,17 +1143,24 @@ class BridgeRunner {
|
|
|
1120
1143
|
if (messages.length === 0) {
|
|
1121
1144
|
return false;
|
|
1122
1145
|
}
|
|
1146
|
+
const ackToken = result.next_ack_token || result.nextAckToken;
|
|
1147
|
+
this.copilotLog(
|
|
1148
|
+
`received batch size=${messages.length} hasMore=${Boolean(result?.has_more)} ackToken=${ackToken ? "yes" : "no"} roles=${messages
|
|
1149
|
+
.map((item) => String(item?.role || "unknown").toLowerCase())
|
|
1150
|
+
.join(",")}`,
|
|
1151
|
+
);
|
|
1123
1152
|
|
|
1124
1153
|
for (const message of messages) {
|
|
1125
1154
|
if (!this.shouldRespond(message)) {
|
|
1155
|
+
this.copilotLog(`skip message role=${String(message?.role || "unknown").toLowerCase()}`);
|
|
1126
1156
|
continue;
|
|
1127
1157
|
}
|
|
1128
1158
|
await this.respondToMessage(message);
|
|
1129
1159
|
}
|
|
1130
1160
|
|
|
1131
|
-
const ackToken = result.next_ack_token || result.nextAckToken;
|
|
1132
1161
|
if (ackToken) {
|
|
1133
1162
|
await this.conductor.ackMessages(this.taskId, ackToken);
|
|
1163
|
+
this.copilotLog(`acked batch ackToken=${truncateText(String(ackToken), 40)}`);
|
|
1134
1164
|
}
|
|
1135
1165
|
const hasMore = Boolean(result?.has_more);
|
|
1136
1166
|
return hasMore;
|
|
@@ -1148,6 +1178,7 @@ class BridgeRunner {
|
|
|
1148
1178
|
const backendUrl = process.env.CONDUCTOR_BACKEND_URL;
|
|
1149
1179
|
const token = process.env.CONDUCTOR_AGENT_TOKEN;
|
|
1150
1180
|
if (!backendUrl || !token) {
|
|
1181
|
+
this.copilotLog("skip backfill: missing backend url or token");
|
|
1151
1182
|
return;
|
|
1152
1183
|
}
|
|
1153
1184
|
|
|
@@ -1163,10 +1194,12 @@ class BridgeRunner {
|
|
|
1163
1194
|
},
|
|
1164
1195
|
);
|
|
1165
1196
|
if (!response.ok) {
|
|
1197
|
+
this.copilotLog(`backfill request failed status=${response.status}`);
|
|
1166
1198
|
return;
|
|
1167
1199
|
}
|
|
1168
1200
|
const history = await response.json();
|
|
1169
1201
|
if (!Array.isArray(history) || history.length === 0) {
|
|
1202
|
+
this.copilotLog("backfill: no history messages");
|
|
1170
1203
|
return;
|
|
1171
1204
|
}
|
|
1172
1205
|
|
|
@@ -1182,8 +1215,12 @@ class BridgeRunner {
|
|
|
1182
1215
|
.slice(lastSdkIndex + 1)
|
|
1183
1216
|
.filter((item) => String(item?.role || "").toLowerCase() === "user")
|
|
1184
1217
|
.filter((item) => typeof item?.content === "string" && item.content.trim());
|
|
1218
|
+
this.copilotLog(
|
|
1219
|
+
`backfill loaded history=${history.length} pendingUsers=${pendingUserMessages.length} lastSdkIndex=${lastSdkIndex}`,
|
|
1220
|
+
);
|
|
1185
1221
|
|
|
1186
1222
|
for (const pending of pendingUserMessages) {
|
|
1223
|
+
this.copilotLog(`backfill replay message id=${pending.id ? String(pending.id) : "unknown"}`);
|
|
1187
1224
|
await this.respondToMessage({
|
|
1188
1225
|
message_id: pending.id ? String(pending.id) : undefined,
|
|
1189
1226
|
role: "user",
|
|
@@ -1238,6 +1275,11 @@ class BridgeRunner {
|
|
|
1238
1275
|
this.lastRuntimeStatusPayload = {
|
|
1239
1276
|
...runtime,
|
|
1240
1277
|
};
|
|
1278
|
+
this.copilotLog(
|
|
1279
|
+
`runtime replyTo=${replyTo || "latest"} state=${runtime.state || ""} phase=${runtime.phase || ""} inProgress=${Boolean(
|
|
1280
|
+
runtime.reply_in_progress,
|
|
1281
|
+
)} status="${sanitizeForLog(runtime.status_line || "", 120)}" done="${sanitizeForLog(runtime.status_done_line || "", 120)}" preview="${sanitizeForLog(runtime.reply_preview || "", 120)}"`,
|
|
1282
|
+
);
|
|
1241
1283
|
|
|
1242
1284
|
try {
|
|
1243
1285
|
await this.conductor.sendRuntimeStatus(this.taskId, {
|
|
@@ -1266,14 +1308,32 @@ class BridgeRunner {
|
|
|
1266
1308
|
async respondToMessage(message) {
|
|
1267
1309
|
const content = String(message.content || "").trim();
|
|
1268
1310
|
if (!content) {
|
|
1311
|
+
this.copilotLog(`skip empty message replyTo=${message?.message_id || "latest"}`);
|
|
1269
1312
|
return;
|
|
1270
1313
|
}
|
|
1271
1314
|
const replyTo = message.message_id;
|
|
1272
1315
|
if (replyTo && this.processedMessageIds.has(replyTo)) {
|
|
1316
|
+
this.copilotLog(`skip duplicated message replyTo=${replyTo}`);
|
|
1273
1317
|
return;
|
|
1274
1318
|
}
|
|
1275
1319
|
this.lastRuntimeStatusSignature = null;
|
|
1276
1320
|
this.runningTurn = true;
|
|
1321
|
+
const turnStartedAt = Date.now();
|
|
1322
|
+
let turnWatchdog = null;
|
|
1323
|
+
if (this.isCopilotBackend) {
|
|
1324
|
+
turnWatchdog = setInterval(() => {
|
|
1325
|
+
const runtime = this.lastRuntimeStatusPayload || {};
|
|
1326
|
+
this.copilotLog(
|
|
1327
|
+
`turn waiting replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt} state=${runtime.state || ""} phase=${runtime.phase || ""} status="${sanitizeForLog(runtime.status_line || runtime.status_done_line || "", 120)}"`,
|
|
1328
|
+
);
|
|
1329
|
+
}, 30000);
|
|
1330
|
+
if (typeof turnWatchdog.unref === "function") {
|
|
1331
|
+
turnWatchdog.unref();
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
this.copilotLog(
|
|
1335
|
+
`turn start replyTo=${replyTo || "latest"} role=${String(message.role || "").toLowerCase()} contentLen=${content.length} preview="${sanitizeForLog(content, 120)}"`,
|
|
1336
|
+
);
|
|
1277
1337
|
log(`Processing message ${replyTo} (${message.role})`);
|
|
1278
1338
|
try {
|
|
1279
1339
|
await this.reportRuntimeStatus(
|
|
@@ -1291,6 +1351,11 @@ class BridgeRunner {
|
|
|
1291
1351
|
void this.reportRuntimeStatus(payload, replyTo);
|
|
1292
1352
|
},
|
|
1293
1353
|
});
|
|
1354
|
+
this.copilotLog(
|
|
1355
|
+
`runTurn completed replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt} answerLen=${String(
|
|
1356
|
+
result.text || "",
|
|
1357
|
+
).trim().length} items=${Array.isArray(result.items) ? result.items.length : 0}`,
|
|
1358
|
+
);
|
|
1294
1359
|
|
|
1295
1360
|
await this.reportRuntimeStatus(
|
|
1296
1361
|
{
|
|
@@ -1320,8 +1385,12 @@ class BridgeRunner {
|
|
|
1320
1385
|
if (replyTo) {
|
|
1321
1386
|
this.processedMessageIds.add(replyTo);
|
|
1322
1387
|
}
|
|
1388
|
+
this.copilotLog(`sdk_message sent replyTo=${replyTo || "latest"} responseLen=${responseText.length}`);
|
|
1323
1389
|
} catch (error) {
|
|
1324
1390
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1391
|
+
this.copilotLog(
|
|
1392
|
+
`turn failed replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt} error="${sanitizeForLog(errorMessage, 200)}"`,
|
|
1393
|
+
);
|
|
1325
1394
|
await this.reportRuntimeStatus(
|
|
1326
1395
|
{
|
|
1327
1396
|
state: "ERROR",
|
|
@@ -1333,6 +1402,12 @@ class BridgeRunner {
|
|
|
1333
1402
|
);
|
|
1334
1403
|
await this.reportError(`${this.backendName} 处理失败: ${errorMessage}`, replyTo);
|
|
1335
1404
|
} finally {
|
|
1405
|
+
if (turnWatchdog) {
|
|
1406
|
+
clearInterval(turnWatchdog);
|
|
1407
|
+
}
|
|
1408
|
+
this.copilotLog(
|
|
1409
|
+
`turn end replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt} processedIds=${this.processedMessageIds.size}`,
|
|
1410
|
+
);
|
|
1336
1411
|
this.runningTurn = false;
|
|
1337
1412
|
}
|
|
1338
1413
|
}
|
|
@@ -1340,6 +1415,8 @@ class BridgeRunner {
|
|
|
1340
1415
|
async handleSyntheticMessage(content, { includeImages }) {
|
|
1341
1416
|
this.lastRuntimeStatusSignature = null;
|
|
1342
1417
|
this.runningTurn = true;
|
|
1418
|
+
const startedAt = Date.now();
|
|
1419
|
+
this.copilotLog(`synthetic turn start includeImages=${Boolean(includeImages)} contentLen=${String(content || "").length}`);
|
|
1343
1420
|
try {
|
|
1344
1421
|
const result = await this.backendSession.runTurn(content, {
|
|
1345
1422
|
useInitialImages: includeImages,
|
|
@@ -1347,6 +1424,9 @@ class BridgeRunner {
|
|
|
1347
1424
|
void this.reportRuntimeStatus(payload, "initial");
|
|
1348
1425
|
},
|
|
1349
1426
|
});
|
|
1427
|
+
this.copilotLog(
|
|
1428
|
+
`synthetic runTurn completed elapsedMs=${Date.now() - startedAt} answerLen=${String(result.text || "").trim().length}`,
|
|
1429
|
+
);
|
|
1350
1430
|
const backendLabel = this.backendName.charAt(0).toUpperCase() + this.backendName.slice(1);
|
|
1351
1431
|
const intro = `${backendLabel} 已根据初始提示给出回复:`;
|
|
1352
1432
|
const replyText =
|
|
@@ -1364,9 +1444,14 @@ class BridgeRunner {
|
|
|
1364
1444
|
cli_args: this.cliArgs,
|
|
1365
1445
|
synthetic: true,
|
|
1366
1446
|
});
|
|
1447
|
+
this.copilotLog(`synthetic sdk_message sent responseLen=${text.length}`);
|
|
1367
1448
|
} catch (error) {
|
|
1449
|
+
this.copilotLog(
|
|
1450
|
+
`synthetic turn failed elapsedMs=${Date.now() - startedAt} error="${sanitizeForLog(error?.message || error, 200)}"`,
|
|
1451
|
+
);
|
|
1368
1452
|
await this.reportError(`初始提示执行失败: ${error.message}`);
|
|
1369
1453
|
} finally {
|
|
1454
|
+
this.copilotLog(`synthetic turn end elapsedMs=${Date.now() - startedAt}`);
|
|
1370
1455
|
this.runningTurn = false;
|
|
1371
1456
|
}
|
|
1372
1457
|
}
|
|
@@ -1424,7 +1509,9 @@ function extractAgentTextFromMetadata(metadata) {
|
|
|
1424
1509
|
|
|
1425
1510
|
function log(message) {
|
|
1426
1511
|
const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
|
|
1427
|
-
|
|
1512
|
+
const line = `[${CLI_NAME} ${ts}] ${message}\n`;
|
|
1513
|
+
process.stdout.write(line);
|
|
1514
|
+
appendFireLocalLog(line);
|
|
1428
1515
|
}
|
|
1429
1516
|
|
|
1430
1517
|
function logBackendReply(backend, text, { usage, replyTo }) {
|
|
@@ -1448,7 +1535,9 @@ function isDirectRun() {
|
|
|
1448
1535
|
if (isDirectRun()) {
|
|
1449
1536
|
main().catch((error) => {
|
|
1450
1537
|
const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
|
|
1451
|
-
|
|
1538
|
+
const line = `[${CLI_NAME} ${ts}] failed: ${error.stack || error.message}\n`;
|
|
1539
|
+
process.stderr.write(line);
|
|
1540
|
+
appendFireLocalLog(line);
|
|
1452
1541
|
process.exitCode = 1;
|
|
1453
1542
|
});
|
|
1454
1543
|
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* conductor update - Check for and install updates to the CLI.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import { execSync, spawn } from "node:child_process";
|
|
12
|
+
import process from "node:process";
|
|
13
|
+
import readline from "node:readline/promises";
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const PKG_ROOT = path.join(__dirname, "..");
|
|
19
|
+
|
|
20
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8"));
|
|
21
|
+
const PACKAGE_NAME = pkgJson.name;
|
|
22
|
+
const CURRENT_VERSION = pkgJson.version;
|
|
23
|
+
|
|
24
|
+
// ANSI 颜色代码
|
|
25
|
+
const COLORS = {
|
|
26
|
+
yellow: "\x1b[33m",
|
|
27
|
+
green: "\x1b[32m",
|
|
28
|
+
cyan: "\x1b[36m",
|
|
29
|
+
red: "\x1b[31m",
|
|
30
|
+
reset: "\x1b[0m",
|
|
31
|
+
bold: "\x1b[1m"
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function colorize(text, color) {
|
|
35
|
+
return `${COLORS[color] || ""}${text}${COLORS.reset}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
// 解析命令行参数
|
|
40
|
+
const args = process.argv.slice(2);
|
|
41
|
+
const forceUpdate = args.includes("--force") || args.includes("-f");
|
|
42
|
+
const skipConfirm = args.includes("--yes") || args.includes("-y");
|
|
43
|
+
const showHelp = args.includes("--help") || args.includes("-h");
|
|
44
|
+
|
|
45
|
+
if (showHelp) {
|
|
46
|
+
showHelpMessage();
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(colorize(`📦 ${PACKAGE_NAME}`, "cyan"));
|
|
51
|
+
console.log(` Current version: ${CURRENT_VERSION}`);
|
|
52
|
+
console.log("");
|
|
53
|
+
|
|
54
|
+
// 检查远程最新版本
|
|
55
|
+
console.log("🔍 Checking for updates...");
|
|
56
|
+
|
|
57
|
+
let latestVersion;
|
|
58
|
+
try {
|
|
59
|
+
latestVersion = await getLatestVersion();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error(colorize(`❌ Failed to check for updates: ${error.message}`, "red"));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(` Latest version: ${latestVersion}`);
|
|
66
|
+
console.log("");
|
|
67
|
+
|
|
68
|
+
// 比较版本
|
|
69
|
+
if (!forceUpdate && !isNewerVersion(latestVersion, CURRENT_VERSION)) {
|
|
70
|
+
console.log(colorize("✅ You are already on the latest version!", "green"));
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (forceUpdate) {
|
|
75
|
+
console.log(colorize("⚡ Force update requested", "yellow"));
|
|
76
|
+
} else {
|
|
77
|
+
console.log(colorize(`⬆️ Update available: ${CURRENT_VERSION} → ${latestVersion}`, "green"));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log("");
|
|
81
|
+
|
|
82
|
+
// 确认更新
|
|
83
|
+
if (!skipConfirm) {
|
|
84
|
+
const shouldUpdate = await confirmUpdate(latestVersion);
|
|
85
|
+
if (!shouldUpdate) {
|
|
86
|
+
console.log("Update cancelled.");
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 执行更新
|
|
92
|
+
console.log("");
|
|
93
|
+
console.log(colorize("🚀 Installing update...", "cyan"));
|
|
94
|
+
console.log("");
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await performUpdate();
|
|
98
|
+
console.log("");
|
|
99
|
+
console.log(colorize("✅ Update completed successfully!", "green"));
|
|
100
|
+
console.log("");
|
|
101
|
+
console.log(" Run 'conductor --version' to verify the new version.");
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error("");
|
|
104
|
+
console.error(colorize(`❌ Update failed: ${error.message}`, "red"));
|
|
105
|
+
console.error("");
|
|
106
|
+
console.error(" You can try updating manually with:");
|
|
107
|
+
console.error(` npm install -g ${PACKAGE_NAME}@latest`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function getLatestVersion() {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
// 使用 npm view 获取最新版本
|
|
115
|
+
try {
|
|
116
|
+
const result = execSync(`npm view ${PACKAGE_NAME} version --json`, {
|
|
117
|
+
encoding: "utf-8",
|
|
118
|
+
timeout: 10000,
|
|
119
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// npm view 返回的是带引号的字符串 JSON
|
|
123
|
+
const version = JSON.parse(result.trim());
|
|
124
|
+
resolve(version);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// 如果失败,尝试从 registry API 获取
|
|
127
|
+
fetchLatestFromRegistry()
|
|
128
|
+
.then(resolve)
|
|
129
|
+
.catch(reject);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function fetchLatestFromRegistry() {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const https = require("https");
|
|
137
|
+
const url = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
138
|
+
|
|
139
|
+
https.get(url, { timeout: 10000 }, (res) => {
|
|
140
|
+
let data = "";
|
|
141
|
+
|
|
142
|
+
res.on("data", (chunk) => {
|
|
143
|
+
data += chunk;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
res.on("end", () => {
|
|
147
|
+
try {
|
|
148
|
+
const json = JSON.parse(data);
|
|
149
|
+
if (json.version) {
|
|
150
|
+
resolve(json.version);
|
|
151
|
+
} else {
|
|
152
|
+
reject(new Error("Invalid response from registry"));
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
reject(new Error(`Failed to parse registry response: ${error.message}`));
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}).on("error", (error) => {
|
|
159
|
+
reject(new Error(`Network error: ${error.message}`));
|
|
160
|
+
}).on("timeout", () => {
|
|
161
|
+
reject(new Error("Request timed out"));
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isNewerVersion(latest, current) {
|
|
167
|
+
// 简单的版本比较
|
|
168
|
+
const parseVersion = (v) => v.replace(/^v/, "").split(".").map(Number);
|
|
169
|
+
|
|
170
|
+
const latestParts = parseVersion(latest);
|
|
171
|
+
const currentParts = parseVersion(current);
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < Math.max(latestParts.length, currentParts.length); i++) {
|
|
174
|
+
const l = latestParts[i] || 0;
|
|
175
|
+
const c = currentParts[i] || 0;
|
|
176
|
+
|
|
177
|
+
if (l > c) return true;
|
|
178
|
+
if (l < c) return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return false; // 版本相同
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function confirmUpdate(version) {
|
|
185
|
+
const rl = readline.createInterface({
|
|
186
|
+
input: process.stdin,
|
|
187
|
+
output: process.stdout,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const answer = await rl.question(
|
|
192
|
+
colorize(`Do you want to update to version ${version}? (Y/n): `, "yellow")
|
|
193
|
+
);
|
|
194
|
+
const normalized = answer.trim().toLowerCase();
|
|
195
|
+
return normalized === "" || normalized === "y" || normalized === "yes";
|
|
196
|
+
} finally {
|
|
197
|
+
rl.close();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function performUpdate() {
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
// 检测使用的包管理器
|
|
204
|
+
const packageManager = detectPackageManager();
|
|
205
|
+
console.log(` Using package manager: ${colorize(packageManager, "cyan")}`);
|
|
206
|
+
console.log("");
|
|
207
|
+
|
|
208
|
+
let cmd, args;
|
|
209
|
+
|
|
210
|
+
switch (packageManager) {
|
|
211
|
+
case "pnpm":
|
|
212
|
+
cmd = "pnpm";
|
|
213
|
+
args = ["add", "-g", `${PACKAGE_NAME}@latest`];
|
|
214
|
+
break;
|
|
215
|
+
case "yarn":
|
|
216
|
+
cmd = "yarn";
|
|
217
|
+
args = ["global", "add", `${PACKAGE_NAME}@latest`];
|
|
218
|
+
break;
|
|
219
|
+
case "npm":
|
|
220
|
+
default:
|
|
221
|
+
cmd = "npm";
|
|
222
|
+
args = ["install", "-g", `${PACKAGE_NAME}@latest`];
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log(` Running: ${colorize(`${cmd} ${args.join(" ")}`, "cyan")}`);
|
|
227
|
+
console.log("");
|
|
228
|
+
|
|
229
|
+
const child = spawn(cmd, args, {
|
|
230
|
+
stdio: "inherit",
|
|
231
|
+
shell: true
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
child.on("close", (code) => {
|
|
235
|
+
if (code === 0) {
|
|
236
|
+
resolve();
|
|
237
|
+
} else {
|
|
238
|
+
reject(new Error(`Exit code ${code}`));
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
child.on("error", (error) => {
|
|
243
|
+
reject(error);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function detectPackageManager() {
|
|
249
|
+
// 通过分析 conductor 命令的路径来推断包管理器
|
|
250
|
+
try {
|
|
251
|
+
const conductorPath = execSync("which conductor || where conductor", {
|
|
252
|
+
encoding: "utf-8",
|
|
253
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
254
|
+
}).trim();
|
|
255
|
+
|
|
256
|
+
if (conductorPath.includes("pnpm")) {
|
|
257
|
+
return "pnpm";
|
|
258
|
+
}
|
|
259
|
+
if (conductorPath.includes("yarn")) {
|
|
260
|
+
return "yarn";
|
|
261
|
+
}
|
|
262
|
+
if (conductorPath.includes(".npm") || conductorPath.includes("npm")) {
|
|
263
|
+
return "npm";
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
// 忽略错误,使用默认检测
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 检查哪个包管理器可用
|
|
270
|
+
try {
|
|
271
|
+
execSync("pnpm --version", { stdio: "pipe" });
|
|
272
|
+
return "pnpm";
|
|
273
|
+
} catch {
|
|
274
|
+
// pnpm 不可用
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
execSync("yarn --version", { stdio: "pipe" });
|
|
279
|
+
return "yarn";
|
|
280
|
+
} catch {
|
|
281
|
+
// yarn 不可用
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return "npm"; // 默认使用 npm
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function showHelpMessage() {
|
|
288
|
+
console.log(`conductor update - Update the CLI to the latest version
|
|
289
|
+
|
|
290
|
+
Usage: conductor update [options]
|
|
291
|
+
|
|
292
|
+
Options:
|
|
293
|
+
-f, --force Force update even if already on latest version
|
|
294
|
+
-y, --yes Skip confirmation prompt
|
|
295
|
+
-h, --help Show this help message
|
|
296
|
+
|
|
297
|
+
Examples:
|
|
298
|
+
conductor update Check for updates and prompt to install
|
|
299
|
+
conductor update --yes Update without prompting
|
|
300
|
+
conductor update --force Force reinstall the latest version
|
|
301
|
+
`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
main().catch((error) => {
|
|
305
|
+
console.error(colorize(`Unexpected error: ${error?.message || error}`, "red"));
|
|
306
|
+
process.exit(1);
|
|
307
|
+
});
|
package/bin/conductor.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* conductor - Main CLI entry point with subcommand routing.
|
|
5
5
|
*
|
|
6
6
|
* Subcommands:
|
|
7
|
-
* fire
|
|
8
|
-
* daemon
|
|
9
|
-
* config
|
|
7
|
+
* fire - Run AI coding agents with Conductor integration
|
|
8
|
+
* daemon - Start long-running daemon for task orchestration
|
|
9
|
+
* config - Interactive configuration setup
|
|
10
|
+
* update - Update the CLI to the latest version
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
@@ -42,7 +43,7 @@ if (argv[0] === "--version" || argv[0] === "-v") {
|
|
|
42
43
|
const subcommand = argv[0];
|
|
43
44
|
|
|
44
45
|
// Valid subcommands
|
|
45
|
-
const validSubcommands = ["fire", "daemon", "config"];
|
|
46
|
+
const validSubcommands = ["fire", "daemon", "config", "update"];
|
|
46
47
|
|
|
47
48
|
if (!validSubcommands.includes(subcommand)) {
|
|
48
49
|
console.error(`Error: Unknown subcommand '${subcommand}'`);
|
|
@@ -83,6 +84,7 @@ Subcommands:
|
|
|
83
84
|
fire Run AI coding agents with Conductor integration
|
|
84
85
|
daemon Start long-running daemon for task orchestration
|
|
85
86
|
config Interactive configuration setup
|
|
87
|
+
update Update the CLI to the latest version
|
|
86
88
|
|
|
87
89
|
Options:
|
|
88
90
|
-h, --help Show this help message
|
|
@@ -93,11 +95,13 @@ Examples:
|
|
|
93
95
|
conductor fire --backend claude -- "add feature"
|
|
94
96
|
conductor daemon --config-file ~/.conductor/config.yaml
|
|
95
97
|
conductor config
|
|
98
|
+
conductor update
|
|
96
99
|
|
|
97
100
|
For subcommand-specific help:
|
|
98
101
|
conductor fire --help
|
|
99
102
|
conductor daemon --help
|
|
100
103
|
conductor config --help
|
|
104
|
+
conductor update --help
|
|
101
105
|
|
|
102
106
|
Version: ${pkgJson.version}
|
|
103
107
|
`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/conductor-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"conductor": "bin/conductor.js"
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"test": "node --test"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@love-moon/tui-driver": "0.2.
|
|
20
|
-
"@love-moon/conductor-sdk": "0.2.
|
|
19
|
+
"@love-moon/tui-driver": "0.2.11",
|
|
20
|
+
"@love-moon/conductor-sdk": "0.2.11",
|
|
21
21
|
"dotenv": "^16.4.5",
|
|
22
22
|
"enquirer": "^2.4.1",
|
|
23
23
|
"js-yaml": "^4.1.1",
|
package/src/daemon.js
CHANGED
|
@@ -14,15 +14,30 @@ dotenv.config();
|
|
|
14
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
15
|
const __dirname = path.dirname(__filename);
|
|
16
16
|
const CLI_PATH = path.resolve(__dirname, "..", "bin", "conductor-fire.js");
|
|
17
|
+
const DAEMON_LOG_DIR = path.join(os.homedir(), ".conductor", "logs");
|
|
18
|
+
const DAEMON_LOG_PATH = path.join(DAEMON_LOG_DIR, "conductor-daemon.log");
|
|
19
|
+
|
|
20
|
+
function appendDaemonLog(line) {
|
|
21
|
+
try {
|
|
22
|
+
fs.mkdirSync(DAEMON_LOG_DIR, { recursive: true });
|
|
23
|
+
fs.appendFileSync(DAEMON_LOG_PATH, line);
|
|
24
|
+
} catch {
|
|
25
|
+
// ignore file log errors
|
|
26
|
+
}
|
|
27
|
+
}
|
|
17
28
|
|
|
18
29
|
function log(message) {
|
|
19
30
|
const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
|
|
20
|
-
|
|
31
|
+
const line = `[conductor-daemon ${ts}] ${message}\n`;
|
|
32
|
+
process.stdout.write(line);
|
|
33
|
+
appendDaemonLog(line);
|
|
21
34
|
}
|
|
22
35
|
|
|
23
36
|
function logError(message) {
|
|
24
37
|
const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
|
|
25
|
-
|
|
38
|
+
const line = `[conductor-daemon ${ts}] ${message}\n`;
|
|
39
|
+
process.stderr.write(line);
|
|
40
|
+
appendDaemonLog(line);
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
function getUserConfig(configFilePath) {
|
|
@@ -656,6 +671,20 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
656
671
|
return;
|
|
657
672
|
}
|
|
658
673
|
|
|
674
|
+
const existingTaskRecord = activeTaskProcesses.get(taskId);
|
|
675
|
+
if (existingTaskRecord?.child) {
|
|
676
|
+
log(
|
|
677
|
+
`Duplicate create_task ignored for ${taskId}: task already active (pid=${existingTaskRecord.child.pid ?? "unknown"})`,
|
|
678
|
+
);
|
|
679
|
+
sendAgentCommandAck({
|
|
680
|
+
requestId,
|
|
681
|
+
taskId,
|
|
682
|
+
eventType: "create_task",
|
|
683
|
+
accepted: true,
|
|
684
|
+
}).catch(() => {});
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
|
|
659
688
|
// Validate and get CLI command for the backend
|
|
660
689
|
const effectiveBackend = backendType || SUPPORTED_BACKENDS[0];
|
|
661
690
|
if (!SUPPORTED_BACKENDS.includes(effectiveBackend)) {
|