@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.
- package/bin/conductor-config.js +16 -0
- package/bin/conductor-fire.js +258 -153
- package/bin/conductor-serve-ai.js +145 -0
- package/bin/conductor.js +5 -1
- package/package.json +6 -6
- package/src/ai-manager-handlers.js +51 -47
- package/src/daemon.js +321 -121
- package/src/fire/resume.js +498 -107
- package/src/handoff-log-mask.js +64 -0
- package/src/runtime-backends.js +67 -17
- package/src/serve-ai/adapter.js +383 -0
- package/src/serve-ai/config.js +133 -0
- package/src/serve-ai/errors.js +28 -0
- package/src/serve-ai/image-handler.js +92 -0
- package/src/serve-ai/index.js +529 -0
|
@@ -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.
|
|
4
|
-
"gitCommitId": "
|
|
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-
|
|
21
|
-
"@love-moon/ai-
|
|
22
|
-
"@love-moon/
|
|
23
|
-
"@
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
133
|
-
|
|
134
|
-
.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
149
|
-
if (
|
|
150
|
-
|
|
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
|
-
|
|
157
|
-
return String(err);
|
|
161
|
+
return err?.message ?? String(err);
|
|
158
162
|
}
|