@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.
@@ -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
+ }
@@ -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: "exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check",
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
- // opencode: {
39
- // command: "opencode",
40
- // execArgs: "",
41
- // description: "OpenCode CLI"
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
- const fullCommand = info.execArgs
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).slice(0, 3).forEach(([key, info]) => {
182
- const fullCommand = info.execArgs
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;