@love-moon/conductor-cli 0.2.35 → 0.2.36
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/package.json +6 -4
- package/src/ai-manager-handlers.js +158 -0
- package/src/daemon.js +8 -0
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.36",
|
|
4
|
+
"gitCommitId": "54d9de4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"conductor": "bin/conductor.js"
|
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@love-moon/ai-bridge": "0.1.4",
|
|
21
|
-
"@love-moon/ai-
|
|
22
|
-
"@love-moon/
|
|
21
|
+
"@love-moon/ai-manager": "0.2.36",
|
|
22
|
+
"@love-moon/ai-sdk": "0.2.36",
|
|
23
|
+
"@love-moon/conductor-sdk": "0.2.36",
|
|
23
24
|
"chrome-launcher": "^1.2.1",
|
|
24
25
|
"chrome-remote-interface": "^0.33.0",
|
|
25
26
|
"dotenv": "^16.4.5",
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
],
|
|
40
41
|
"overrides": {
|
|
41
42
|
"@love-moon/ai-sdk": "file:../modules/ai-sdk",
|
|
43
|
+
"@love-moon/ai-manager": "file:../modules/ai-manager",
|
|
42
44
|
"@love-moon/conductor-sdk": "file:../modules/conductor-sdk"
|
|
43
45
|
}
|
|
44
46
|
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Daemon-side glue between the realtime WebSocket and the @love-moon/ai-manager module.
|
|
2
|
+
// The web backend sends `ai_manager_request`; we dispatch by `action`, run it, and
|
|
3
|
+
// reply with `ai_manager_response` carrying the same `request_id`.
|
|
4
|
+
|
|
5
|
+
import { AiManager } from "@love-moon/ai-manager";
|
|
6
|
+
|
|
7
|
+
const VALID_ACTIONS = new Set(["status", "quota", "list_accounts", "switch_account"]);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {object} opts
|
|
11
|
+
* @param {string} [opts.configPath] Path to ~/.conductor/config.yaml. Defaults to ai-manager's default.
|
|
12
|
+
*/
|
|
13
|
+
export function createAiManagerHandlers(opts = {}) {
|
|
14
|
+
const manager = new AiManager(opts.configPath ? { configPath: opts.configPath } : undefined);
|
|
15
|
+
|
|
16
|
+
async function status() {
|
|
17
|
+
// Probe install first; only check network for tools that are actually
|
|
18
|
+
// present so we don't pay an outbound HTTP timeout for a CLI the user
|
|
19
|
+
// never installed.
|
|
20
|
+
const [install, current] = await Promise.all([
|
|
21
|
+
manager.checkInstallAll(),
|
|
22
|
+
manager.getCurrentCodexAccount().catch(() => null),
|
|
23
|
+
]);
|
|
24
|
+
const network = {};
|
|
25
|
+
const tools = ["codex", "claude", "kimi"];
|
|
26
|
+
await Promise.all(
|
|
27
|
+
tools.map(async (tool) => {
|
|
28
|
+
if (install[tool]?.installed) {
|
|
29
|
+
network[tool] = await manager.checkNetwork(tool);
|
|
30
|
+
} else {
|
|
31
|
+
network[tool] = {
|
|
32
|
+
reachable: false,
|
|
33
|
+
endpoint: "",
|
|
34
|
+
error: "not installed",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
return { install, network, currentCodexAccount: current };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function quota(args = {}) {
|
|
43
|
+
const tools = pickToolFilter(args);
|
|
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" };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function listAccounts() {
|
|
76
|
+
return { accounts: await manager.listCodexAccounts() };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function switchAccount(args = {}) {
|
|
80
|
+
if (!args.name || typeof args.name !== "string") {
|
|
81
|
+
throw new Error("switch_account requires a `name` string");
|
|
82
|
+
}
|
|
83
|
+
return await manager.switchCodexAccount(args.name);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Run a single action and return a `result` object, never throwing.
|
|
88
|
+
* @param {{action:string,args?:object}} payload
|
|
89
|
+
*/
|
|
90
|
+
async function dispatch(payload) {
|
|
91
|
+
const action = payload?.action;
|
|
92
|
+
if (!VALID_ACTIONS.has(action)) {
|
|
93
|
+
return { error: `unknown action: ${action}` };
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
switch (action) {
|
|
97
|
+
case "status":
|
|
98
|
+
return { result: await status() };
|
|
99
|
+
case "quota":
|
|
100
|
+
return { result: await quota(payload?.args ?? {}) };
|
|
101
|
+
case "list_accounts":
|
|
102
|
+
return { result: await listAccounts() };
|
|
103
|
+
case "switch_account":
|
|
104
|
+
return { result: await switchAccount(payload?.args ?? {}) };
|
|
105
|
+
default:
|
|
106
|
+
return { error: `unhandled action: ${action}` };
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
return { error: errMsg(err) };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { dispatch, manager };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Wire a handler against an event payload from the web backend, sending the
|
|
118
|
+
* response back through `client.sendJson` once the action completes.
|
|
119
|
+
*
|
|
120
|
+
* @param {object} client - conductor websocket client (must have sendJson)
|
|
121
|
+
* @param {ReturnType<typeof createAiManagerHandlers>} handlers
|
|
122
|
+
* @param {object} payload - event.payload with shape { request_id, action, args }
|
|
123
|
+
*/
|
|
124
|
+
export async function handleAiManagerRequest(client, handlers, payload) {
|
|
125
|
+
const requestId = payload?.request_id ? String(payload.request_id) : "";
|
|
126
|
+
const action = payload?.action ? String(payload.action) : "";
|
|
127
|
+
if (!requestId) {
|
|
128
|
+
// Without a request_id we cannot route the response anywhere; drop and log upstream.
|
|
129
|
+
return { error: "missing request_id" };
|
|
130
|
+
}
|
|
131
|
+
|
|
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;
|
|
145
|
+
}
|
|
146
|
+
|
|
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"]);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function errMsg(err) {
|
|
156
|
+
if (err instanceof Error) return err.message;
|
|
157
|
+
return String(err);
|
|
158
|
+
}
|
package/src/daemon.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
ProjectContext,
|
|
17
17
|
} from "@love-moon/conductor-sdk";
|
|
18
18
|
import { DaemonLogCollector } from "./log-collector.js";
|
|
19
|
+
import { createAiManagerHandlers, handleAiManagerRequest } from "./ai-manager-handlers.js";
|
|
19
20
|
import { resolveResumeContext } from "./fire/resume.js";
|
|
20
21
|
import {
|
|
21
22
|
filterRuntimeSupportedAllowCliList,
|
|
@@ -1456,6 +1457,8 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1456
1457
|
if (advertisedCapabilities.length > 0) {
|
|
1457
1458
|
extraHeaders["x-conductor-capabilities"] = advertisedCapabilities.join(",");
|
|
1458
1459
|
}
|
|
1460
|
+
const aiManagerHandlers = createAiManagerHandlers({ configPath: config.CONFIG_FILE });
|
|
1461
|
+
|
|
1459
1462
|
const client = createWebSocketClient(sdkConfig, {
|
|
1460
1463
|
extraHeaders,
|
|
1461
1464
|
onConnected: ({ isReconnect, connectedAt } = { isReconnect: false, connectedAt: Date.now() }) => {
|
|
@@ -3431,6 +3434,11 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
3431
3434
|
if (event.type === "validate_project_path") {
|
|
3432
3435
|
void handleValidateProjectPath(event.payload);
|
|
3433
3436
|
}
|
|
3437
|
+
if (event.type === "ai_manager_request") {
|
|
3438
|
+
handleAiManagerRequest(client, aiManagerHandlers, event.payload).catch((error) => {
|
|
3439
|
+
logError(`Unhandled ai_manager_request failure: ${error?.message || error}`);
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3434
3442
|
}
|
|
3435
3443
|
|
|
3436
3444
|
function markWatchdogHealthy(signal, at = Date.now()) {
|