@love-moon/conductor-cli 0.2.39 → 0.2.41

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,145 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import process from "node:process";
5
+
6
+ import yargs from "yargs/yargs";
7
+ import { hideBin } from "yargs/helpers";
8
+
9
+ import { startServeAiServer } from "../src/serve-ai/index.js";
10
+ import {
11
+ resolveServeAiConfigPaths,
12
+ writeServeAiConfigFile,
13
+ } from "../src/serve-ai/config.js";
14
+
15
+ const CLI_NAME = process.env.CONDUCTOR_CLI_NAME || "conductor serve-ai";
16
+
17
+ main().catch((error) => {
18
+ process.stderr.write(`serve-ai failed: ${error?.message || error}\n`);
19
+ process.exit(1);
20
+ });
21
+
22
+ async function main() {
23
+ await yargs(hideBin(process.argv))
24
+ .scriptName(CLI_NAME)
25
+ .command(
26
+ "$0",
27
+ "Start an OpenAI-compatible AI server",
28
+ (cmd) =>
29
+ cmd
30
+ .option("host", {
31
+ type: "string",
32
+ default: process.env.CONDUCTOR_SERVE_AI_HOST || undefined,
33
+ describe: "Host interface to bind",
34
+ })
35
+ .option("port", {
36
+ type: "number",
37
+ default: process.env.CONDUCTOR_SERVE_AI_PORT ? Number(process.env.CONDUCTOR_SERVE_AI_PORT) : undefined,
38
+ describe: "TCP port to bind",
39
+ })
40
+ .option("backend", {
41
+ type: "string",
42
+ describe: "Default backend/model to use when request model is omitted",
43
+ })
44
+ .option("config-file", {
45
+ type: "string",
46
+ describe: "Primary Conductor config path to check before falling back",
47
+ })
48
+ .option("api-key", {
49
+ type: "string",
50
+ describe: "Optional API key to require via Authorization: Bearer <key>",
51
+ })
52
+ .example("$0", "Start an OpenAI-compatible server using config.yaml or config-ai-serve.yaml")
53
+ .example("$0 --backend kimi --port 9000", "Use kimi as default backend and listen on port 9000")
54
+ .example("$0 --api-key local-dev-key", "Require Bearer local-dev-key"),
55
+ async (args) => {
56
+ const server = await startServeAiServer({
57
+ host: args.host,
58
+ port: args.port,
59
+ backend: args.backend,
60
+ configFile: args.configFile,
61
+ apiKey: args.apiKey,
62
+ });
63
+
64
+ process.stdout.write(
65
+ `OpenAI-compatible server listening at ${server.url} (default model: ${args.backend || "auto"})\n`,
66
+ );
67
+ process.stdout.write(
68
+ `Config source: ${server.configSource} (${server.configPath})\n`,
69
+ );
70
+
71
+ const shutdown = async () => {
72
+ process.stdout.write("Shutting down serve-ai server\n");
73
+ await server.close().catch(() => {});
74
+ process.exit(0);
75
+ };
76
+
77
+ process.on("SIGINT", () => {
78
+ void shutdown();
79
+ });
80
+ process.on("SIGTERM", () => {
81
+ void shutdown();
82
+ });
83
+
84
+ await new Promise(() => {});
85
+ },
86
+ )
87
+ .command(
88
+ "init",
89
+ "Create a dedicated config-ai-serve.yaml next to the primary config path",
90
+ (cmd) =>
91
+ cmd
92
+ .option("config-file", {
93
+ type: "string",
94
+ describe: "Primary Conductor config path whose directory will host config-ai-serve.yaml",
95
+ })
96
+ .option("backend", {
97
+ type: "string",
98
+ default: "codex",
99
+ describe: "Default backend to write into serve_ai.backend",
100
+ })
101
+ .option("host", {
102
+ type: "string",
103
+ default: "127.0.0.1",
104
+ describe: "Default host to write into serve_ai.host",
105
+ })
106
+ .option("port", {
107
+ type: "number",
108
+ default: 8787,
109
+ describe: "Default port to write into serve_ai.port",
110
+ })
111
+ .option("api-key", {
112
+ type: "string",
113
+ describe: "Optional default API key to write into serve_ai.api_key",
114
+ })
115
+ .option("force", {
116
+ type: "boolean",
117
+ default: false,
118
+ describe: "Overwrite an existing config-ai-serve.yaml",
119
+ })
120
+ .example("$0 init", "Create ~/.conductor/config-ai-serve.yaml")
121
+ .example("$0 init --config-file /tmp/custom/config.yaml", "Create /tmp/custom/config-ai-serve.yaml"),
122
+ async (args) => {
123
+ const { conductorConfigPath, serveAiConfigPath } = resolveServeAiConfigPaths(args.configFile);
124
+ if (fs.existsSync(serveAiConfigPath) && !args.force) {
125
+ throw new Error(
126
+ `serve-ai config already exists at ${serveAiConfigPath}. Use --force to overwrite it.`,
127
+ );
128
+ }
129
+
130
+ writeServeAiConfigFile(serveAiConfigPath, {
131
+ backend: args.backend,
132
+ host: args.host,
133
+ port: args.port,
134
+ apiKey: args.apiKey,
135
+ });
136
+
137
+ process.stdout.write(`Primary config path: ${conductorConfigPath}\n`);
138
+ process.stdout.write(`Created serve-ai config: ${serveAiConfigPath}\n`);
139
+ },
140
+ )
141
+ .demandCommand(1, "")
142
+ .help()
143
+ .strict()
144
+ .parseAsync();
145
+ }
package/bin/conductor.js CHANGED
@@ -11,6 +11,7 @@
11
11
  * diagnose - Diagnose a task in production/backend
12
12
  * send-file - Upload a local file into a task session
13
13
  * channel - Connect user-owned chat channel providers
14
+ * serve-ai - Start an OpenAI-compatible local AI server
14
15
  */
15
16
 
16
17
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -34,7 +35,7 @@ export function runConductorCli(args = argv, deps = {}) {
34
35
  const processArgv = deps.processArgv || process.argv;
35
36
  const fsExistsSync = deps.existsSync || fs.existsSync;
36
37
  const checkForUpdates = deps.maybeCheckForUpdates || maybeCheckForUpdates;
37
- const validSubcommands = ["fire", "daemon", "config", "update", "diagnose", "send-file", "channel"];
38
+ const validSubcommands = ["fire", "daemon", "config", "update", "diagnose", "send-file", "channel", "serve-ai"];
38
39
 
39
40
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
40
41
  showHelp(consoleImpl);
@@ -115,6 +116,7 @@ Subcommands:
115
116
  diagnose Diagnose a task and print likely root cause
116
117
  send-file Upload a local file into a task session
117
118
  channel Connect user-owned chat channel providers
119
+ serve-ai Start an OpenAI-compatible local AI server
118
120
 
119
121
  Options:
120
122
  -h, --help Show this help message
@@ -127,6 +129,7 @@ Examples:
127
129
  conductor diagnose <task-id>
128
130
  conductor send-file ./screenshot.png
129
131
  conductor channel connect feishu
132
+ conductor serve-ai --port 8787
130
133
  conductor config
131
134
  conductor update
132
135
 
@@ -138,6 +141,7 @@ For subcommand-specific help:
138
141
  conductor diagnose --help
139
142
  conductor send-file --help
140
143
  conductor channel --help
144
+ conductor serve-ai --help
141
145
 
142
146
  Version: ${pkgJson.version}
143
147
  `);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.2.39",
4
- "gitCommitId": "30204c8",
3
+ "version": "0.2.41",
4
+ "gitCommitId": "89ac834",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "conductor": "bin/conductor.js"
@@ -17,10 +17,10 @@
17
17
  "test": "node --test test/*.test.js"
18
18
  },
19
19
  "dependencies": {
20
- "@love-moon/ai-bridge": "0.1.4",
21
- "@love-moon/ai-manager": "0.2.39",
22
- "@love-moon/ai-sdk": "0.2.39",
23
- "@love-moon/conductor-sdk": "0.2.39",
20
+ "@love-moon/ai-manager": "0.2.41",
21
+ "@love-moon/ai-sdk": "0.2.41",
22
+ "@love-moon/conductor-sdk": "0.2.41",
23
+ "@github/copilot-sdk": "^0.2.2",
24
24
  "chrome-launcher": "^1.2.1",
25
25
  "chrome-remote-interface": "^0.33.0",
26
26
  "dotenv": "^16.4.5",
@@ -22,7 +22,7 @@ export function createAiManagerHandlers(opts = {}) {
22
22
  manager.getCurrentCodexAccount().catch(() => null),
23
23
  ]);
24
24
  const network = {};
25
- const tools = ["codex", "claude", "kimi"];
25
+ const tools = ["codex", "claude", "kimi", "copilot"];
26
26
  await Promise.all(
27
27
  tools.map(async (tool) => {
28
28
  if (install[tool]?.installed) {
@@ -42,32 +42,37 @@ export function createAiManagerHandlers(opts = {}) {
42
42
  async function quota(args = {}) {
43
43
  const tools = pickToolFilter(args);
44
44
  const out = {};
45
- if (tools.has("codex")) {
46
- try {
47
- out.codex = await manager.getCodexQuota({
48
- forceRefresh: Boolean(args.forceRefresh),
49
- });
50
- } catch (err) {
51
- out.codex = { tool: "codex", error: errMsg(err), source: "unknown" };
52
- }
53
- }
54
- if (tools.has("claude")) {
55
- try {
56
- out.claude = await manager.getClaudeQuota({
57
- forceRefresh: Boolean(args.forceRefresh),
58
- });
59
- } catch (err) {
60
- out.claude = { tool: "claude", error: errMsg(err), source: "unknown" };
61
- }
62
- }
63
- if (tools.has("kimi")) {
64
- try {
65
- out.kimi = await manager.getKimiQuota({
66
- forceRefresh: Boolean(args.forceRefresh),
67
- });
68
- } catch (err) {
69
- out.kimi = { tool: "kimi", error: errMsg(err), source: "unknown" };
45
+ const forceRefresh = Boolean(args.forceRefresh);
46
+ const jobs = [];
47
+ const addJob = (tool, fetcher) => {
48
+ if (!tools.has(tool)) {
49
+ return;
70
50
  }
51
+ jobs.push((async () => {
52
+ try {
53
+ return [tool, await fetcher()];
54
+ } catch (err) {
55
+ return [tool, { tool, error: errMsg(err), source: "unknown" }];
56
+ }
57
+ })());
58
+ };
59
+
60
+ addJob("codex", () => manager.getCodexQuota({
61
+ forceRefresh,
62
+ }));
63
+ addJob("claude", () => manager.getClaudeQuota({
64
+ forceRefresh,
65
+ }));
66
+ addJob("kimi", () => manager.getKimiQuota({
67
+ forceRefresh,
68
+ }));
69
+ addJob("copilot", () => manager.getCopilotQuota({
70
+ forceRefresh,
71
+ }));
72
+
73
+ const entries = await Promise.all(jobs);
74
+ for (const [tool, result] of entries) {
75
+ out[tool] = result;
71
76
  }
72
77
  return out;
73
78
  }
@@ -129,30 +134,29 @@ export async function handleAiManagerRequest(client, handlers, payload) {
129
134
  return { error: "missing request_id" };
130
135
  }
131
136
 
132
- const out = await handlers.dispatch({ action, args: payload?.args });
133
- await client
134
- .sendJson({
135
- type: "ai_manager_response",
136
- payload: {
137
- request_id: requestId,
138
- action,
139
- result: out.result,
140
- error: out.error,
141
- },
142
- })
143
- .catch(() => {});
144
- return out;
137
+ const response = await handlers.dispatch({
138
+ action,
139
+ args: payload?.args && typeof payload.args === "object" ? payload.args : {},
140
+ });
141
+
142
+ const outgoing = {
143
+ type: "ai_manager_response",
144
+ payload: {
145
+ request_id: requestId,
146
+ action,
147
+ ...(response?.error ? { error: response.error } : { result: response?.result }),
148
+ },
149
+ };
150
+ await client.sendJson(outgoing).catch(() => {});
151
+ return response;
145
152
  }
146
153
 
147
- function pickToolFilter(args) {
148
- const t = args?.tool;
149
- if (t === "codex") return new Set(["codex"]);
150
- if (t === "claude") return new Set(["claude"]);
151
- if (t === "kimi") return new Set(["kimi"]);
152
- return new Set(["codex", "claude", "kimi"]);
154
+ function pickToolFilter(args = {}) {
155
+ const tool = typeof args.tool === "string" ? args.tool.trim().toLowerCase() : "";
156
+ if (tool && ["codex", "claude", "kimi", "copilot"].includes(tool)) return new Set([tool]);
157
+ return new Set(["codex", "claude", "kimi", "copilot"]);
153
158
  }
154
159
 
155
160
  function errMsg(err) {
156
- if (err instanceof Error) return err.message;
157
- return String(err);
161
+ return err?.message ?? String(err);
158
162
  }