@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.2.35",
4
- "gitCommitId": "686ee4d",
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-sdk": "0.2.35",
22
- "@love-moon/conductor-sdk": "0.2.35",
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()) {