@love-moon/conductor-cli 0.2.18 → 0.2.20
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-channel.js +130 -0
- package/bin/conductor-config.js +29 -30
- package/bin/conductor-diagnose.js +25 -0
- package/bin/conductor-fire.js +300 -9
- package/bin/conductor.js +5 -1
- package/package.json +12 -4
- package/src/daemon.js +1112 -27
- package/src/runtime-backends.js +31 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
import yargs from "yargs/yargs";
|
|
10
|
+
import { hideBin } from "yargs/helpers";
|
|
11
|
+
import { loadConfig } from "@love-moon/conductor-sdk";
|
|
12
|
+
|
|
13
|
+
const isMainModule = (() => {
|
|
14
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
15
|
+
const entryFile = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
16
|
+
return entryFile === currentFile;
|
|
17
|
+
})();
|
|
18
|
+
|
|
19
|
+
function resolveDefaultConfigPath(env = process.env) {
|
|
20
|
+
const home = env.HOME || env.USERPROFILE || os.homedir();
|
|
21
|
+
return path.join(home, ".conductor", "config.yaml");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readTextFile(filePath) {
|
|
25
|
+
return fs.readFileSync(filePath, "utf8");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ensureFeishuChannelConfig(config) {
|
|
29
|
+
const feishu = config?.channels?.feishu;
|
|
30
|
+
if (!feishu?.appId || !feishu?.appSecret || !feishu?.verificationToken) {
|
|
31
|
+
throw new Error("config.yaml is missing channels.feishu.app_id/app_secret/verification_token");
|
|
32
|
+
}
|
|
33
|
+
return feishu;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatErrorBody(text) {
|
|
37
|
+
const normalized = typeof text === "string" ? text.trim() : "";
|
|
38
|
+
if (!normalized) return "";
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(normalized);
|
|
41
|
+
if (parsed && typeof parsed === "object" && typeof parsed.error === "string") {
|
|
42
|
+
return parsed.error;
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// ignore
|
|
46
|
+
}
|
|
47
|
+
return normalized;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function connectFeishuChannel(options = {}) {
|
|
51
|
+
const env = options.env ?? process.env;
|
|
52
|
+
const fetchImpl = options.fetchImpl ?? global.fetch;
|
|
53
|
+
if (typeof fetchImpl !== "function") {
|
|
54
|
+
throw new Error("fetch is not available");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const resolvedConfigPath = path.resolve(options.configFile || resolveDefaultConfigPath(env));
|
|
58
|
+
const rawYaml = readTextFile(resolvedConfigPath);
|
|
59
|
+
const config = loadConfig(resolvedConfigPath, { env });
|
|
60
|
+
const feishu = ensureFeishuChannelConfig(config);
|
|
61
|
+
|
|
62
|
+
const url = new URL("/api/channel/feishu/config", config.backendUrl);
|
|
63
|
+
const response = await fetchImpl(String(url), {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: {
|
|
66
|
+
Authorization: `Bearer ${config.agentToken}`,
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
Accept: "application/json",
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify({ yaml: rawYaml }),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const rawText = await response.text();
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
const detail = formatErrorBody(rawText);
|
|
76
|
+
throw new Error(`Failed to connect Feishu channel (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const parsed = rawText ? JSON.parse(rawText) : {};
|
|
80
|
+
return {
|
|
81
|
+
configPath: resolvedConfigPath,
|
|
82
|
+
feishu,
|
|
83
|
+
config: parsed.config ?? null,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function main(argvInput = hideBin(process.argv), dependencies = {}) {
|
|
88
|
+
const log = dependencies.log ?? console.log;
|
|
89
|
+
const logError = dependencies.logError ?? console.error;
|
|
90
|
+
|
|
91
|
+
await yargs(argvInput)
|
|
92
|
+
.scriptName("conductor channel")
|
|
93
|
+
.command(
|
|
94
|
+
"connect feishu",
|
|
95
|
+
"Upload channels.feishu from config.yaml to Conductor backend",
|
|
96
|
+
(command) => command.option("config-file", {
|
|
97
|
+
type: "string",
|
|
98
|
+
describe: "Path to Conductor config file",
|
|
99
|
+
}),
|
|
100
|
+
async (argv) => {
|
|
101
|
+
try {
|
|
102
|
+
const result = await connectFeishuChannel({
|
|
103
|
+
configFile: argv.configFile,
|
|
104
|
+
});
|
|
105
|
+
const effective = result.config ?? {
|
|
106
|
+
provider: "FEISHU",
|
|
107
|
+
appId: result.feishu.appId,
|
|
108
|
+
verificationToken: result.feishu.verificationToken,
|
|
109
|
+
};
|
|
110
|
+
log(`Connected Feishu channel from ${result.configPath}`);
|
|
111
|
+
log(`app_id: ${effective.appId}`);
|
|
112
|
+
log(`verification_token: ${effective.verificationToken}`);
|
|
113
|
+
log("Configure the same verification_token in Feishu Open Platform webhook settings.");
|
|
114
|
+
} catch (error) {
|
|
115
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
.demandCommand(1)
|
|
121
|
+
.help()
|
|
122
|
+
.parseAsync();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (isMainModule) {
|
|
126
|
+
main().catch((error) => {
|
|
127
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
});
|
|
130
|
+
}
|
package/bin/conductor-config.js
CHANGED
|
@@ -8,6 +8,7 @@ import readline from "node:readline/promises";
|
|
|
8
8
|
import { execSync } from "node:child_process";
|
|
9
9
|
import yargs from "yargs/yargs";
|
|
10
10
|
import { hideBin } from "yargs/helpers";
|
|
11
|
+
import { RUNTIME_SUPPORTED_BACKENDS } from "../src/runtime-backends.js";
|
|
11
12
|
|
|
12
13
|
const CONFIG_DIR = path.join(os.homedir(), ".conductor");
|
|
13
14
|
const CONFIG_FILE = path.join(CONFIG_DIR, "config.yaml");
|
|
@@ -22,24 +23,19 @@ const DEFAULT_CLIs = {
|
|
|
22
23
|
},
|
|
23
24
|
codex: {
|
|
24
25
|
command: "codex",
|
|
25
|
-
execArgs: "
|
|
26
|
+
execArgs: "--dangerously-bypass-approvals-and-sandbox --ask-for-approval never",
|
|
26
27
|
description: "OpenAI Codex CLI"
|
|
27
28
|
},
|
|
28
|
-
// copilot: {
|
|
29
|
-
// command: "copilot",
|
|
30
|
-
// execArgs: "--allow-all-paths --allow-all-tools",
|
|
31
|
-
// description: "GitHub Copilot CLI"
|
|
32
|
-
// },
|
|
33
29
|
// gemini: {
|
|
34
30
|
// command: "gemini",
|
|
35
31
|
// execArgs: "",
|
|
36
32
|
// description: "Google Gemini CLI"
|
|
37
33
|
// },
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
opencode: {
|
|
35
|
+
command: "opencode",
|
|
36
|
+
execArgs: "",
|
|
37
|
+
description: "OpenCode CLI (Conductor runs opencode serve with permission=allow)"
|
|
38
|
+
},
|
|
43
39
|
// kimi: {
|
|
44
40
|
// command: "kimi",
|
|
45
41
|
// execArgs: "--yolo --print --prompt",
|
|
@@ -66,6 +62,22 @@ function colorize(text, color) {
|
|
|
66
62
|
return `${COLORS[color] || ""}${text}${COLORS.reset}`;
|
|
67
63
|
}
|
|
68
64
|
|
|
65
|
+
function buildConfigEntryLines(cli, info, { commented = false } = {}) {
|
|
66
|
+
const fullCommand = info.execArgs
|
|
67
|
+
? `${info.command} ${info.execArgs}`
|
|
68
|
+
: info.command;
|
|
69
|
+
const entryPrefix = commented ? " # " : " ";
|
|
70
|
+
const commentPrefix = commented ? " # " : " # ";
|
|
71
|
+
const lines = [];
|
|
72
|
+
|
|
73
|
+
if (cli === "opencode") {
|
|
74
|
+
lines.push(`${commentPrefix}opencode runs via ai-sdk server mode with permission=allow`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
lines.push(`${entryPrefix}${cli}: ${fullCommand}`);
|
|
78
|
+
return lines;
|
|
79
|
+
}
|
|
80
|
+
|
|
69
81
|
async function main() {
|
|
70
82
|
// 解析命令行参数
|
|
71
83
|
const argv = yargs(hideBin(process.argv))
|
|
@@ -170,19 +182,13 @@ async function main() {
|
|
|
170
182
|
if (detectedCLIs.length > 0) {
|
|
171
183
|
detectedCLIs.forEach(cli => {
|
|
172
184
|
const info = DEFAULT_CLIs[cli];
|
|
173
|
-
|
|
174
|
-
? `${info.command} ${info.execArgs}`
|
|
175
|
-
: info.command;
|
|
176
|
-
lines.push(` ${cli}: ${fullCommand}`);
|
|
185
|
+
lines.push(...buildConfigEntryLines(cli, info));
|
|
177
186
|
});
|
|
178
187
|
} else {
|
|
179
188
|
// 如果没有检测到任何 CLI,添加示例注释
|
|
180
189
|
lines.push(" # No CLI detected. Add your installed CLI here:");
|
|
181
|
-
Object.entries(DEFAULT_CLIs).
|
|
182
|
-
|
|
183
|
-
? `${info.command} ${info.execArgs}`
|
|
184
|
-
: info.command;
|
|
185
|
-
lines.push(` # ${key}: ${fullCommand}`);
|
|
190
|
+
Object.entries(DEFAULT_CLIs).forEach(([key, info]) => {
|
|
191
|
+
lines.push(...buildConfigEntryLines(key, info, { commented: true }));
|
|
186
192
|
});
|
|
187
193
|
}
|
|
188
194
|
|
|
@@ -213,6 +219,9 @@ function detectInstalledCLIs() {
|
|
|
213
219
|
const detected = [];
|
|
214
220
|
|
|
215
221
|
for (const [key, info] of Object.entries(DEFAULT_CLIs)) {
|
|
222
|
+
if (!RUNTIME_SUPPORTED_BACKENDS.includes(key)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
216
225
|
if (isCommandAvailable(info.command)) {
|
|
217
226
|
detected.push(key);
|
|
218
227
|
}
|
|
@@ -282,16 +291,6 @@ function checkAlternativeInstallations(command) {
|
|
|
282
291
|
);
|
|
283
292
|
}
|
|
284
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
|
-
|
|
295
294
|
// 检查文件是否存在
|
|
296
295
|
for (const checkPath of commonPaths) {
|
|
297
296
|
if (fs.existsSync(checkPath)) {
|
|
@@ -244,6 +244,7 @@ function printReport(taskId, report) {
|
|
|
244
244
|
const diagnosis = payload?.diagnosis || {};
|
|
245
245
|
const task = payload?.task || {};
|
|
246
246
|
const realtime = payload?.realtime || {};
|
|
247
|
+
const ptyTransport = payload?.pty_transport || payload?.ptyTransport || {};
|
|
247
248
|
const messages = payload?.messages || {};
|
|
248
249
|
|
|
249
250
|
process.stdout.write(`Task: ${task?.id || taskId}\n`);
|
|
@@ -273,6 +274,25 @@ function printReport(taskId, report) {
|
|
|
273
274
|
);
|
|
274
275
|
}
|
|
275
276
|
}
|
|
277
|
+
const latestLatency = ptyTransport?.latest_latency_sample || ptyTransport?.latestLatencySample;
|
|
278
|
+
if (latestLatency) {
|
|
279
|
+
process.stdout.write(
|
|
280
|
+
`- pty.latest_latency: client->server=${formatLatencyMs(
|
|
281
|
+
latestLatency.client_input_to_server_received_ms ?? latestLatency.clientInputToServerReceivedMs,
|
|
282
|
+
)}, server->daemon=${formatLatencyMs(
|
|
283
|
+
latestLatency.server_received_to_daemon_received_ms ?? latestLatency.serverReceivedToDaemonReceivedMs,
|
|
284
|
+
)}, daemon->first_output=${formatLatencyMs(
|
|
285
|
+
latestLatency.daemon_input_to_first_output_ms ?? latestLatency.daemonInputToFirstOutputMs,
|
|
286
|
+
)}, client->first_output=${formatLatencyMs(
|
|
287
|
+
latestLatency.client_input_to_first_output_ms ?? latestLatency.clientInputToFirstOutputMs,
|
|
288
|
+
)}\n`,
|
|
289
|
+
);
|
|
290
|
+
process.stdout.write(
|
|
291
|
+
`- pty.primary_bottleneck: ${String(
|
|
292
|
+
ptyTransport?.primary_bottleneck || ptyTransport?.primaryBottleneck || "unknown",
|
|
293
|
+
)}\n`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
276
296
|
|
|
277
297
|
const reasons = Array.isArray(diagnosis.reasons) ? diagnosis.reasons : [];
|
|
278
298
|
if (reasons.length > 0) {
|
|
@@ -364,6 +384,11 @@ function detectExecutionFailureLoopKey(value) {
|
|
|
364
384
|
return null;
|
|
365
385
|
}
|
|
366
386
|
|
|
387
|
+
function formatLatencyMs(value) {
|
|
388
|
+
const num = typeof value === "number" ? value : Number(value);
|
|
389
|
+
return Number.isFinite(num) && num >= 0 ? `${Math.round(num)}ms` : "n/a";
|
|
390
|
+
}
|
|
391
|
+
|
|
367
392
|
function normalizePositiveInt(value, fallback) {
|
|
368
393
|
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
369
394
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|