@love-moon/conductor-cli 0.17.2 → 0.18.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # @love-moon/conductor-cli
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 72324fd: Add a local multi-chat runtime with isolated Chromium workers, persistent profiles,
8
+ per-account concurrency, resumable events, request deduplication, and SDK/CLI access.
9
+ Add experimental Kimi and Claude webpage adapters and optional AI SDK socket transport.
10
+
11
+ Support Kimi Connect JSON envelopes and DeepSeek XHR SSE patches, with verified
12
+ parallel real-account text chats, independent cancellation, and restart recovery.
13
+
14
+ Add DeepSeek --deepthink / --no-deepthink controls and SDK options with persisted
15
+ chat defaults, per-turn overrides, verified UI state, and runtime capability checks.
16
+
17
+ Connect Conductor web-kimi and web-deepseek to managed isolated Chromium workers
18
+ by default. Add conductor chat-web login, account credential bootstrap, automatic
19
+ daemon startup and idle shutdown, stable cross-process resume IDs, and browser
20
+ release when tasks close. Preserve the shared-profile queue for other providers.
21
+ Retain cancelled conversation URLs and report confirmed stops as interruptions.
22
+
23
+ ### Patch Changes
24
+
25
+ - 31b61ea: Bound closed-chat and terminal-turn retention, reclaim browser profiles and orphan
26
+ directories, and keep streaming deltas in memory between durable checkpoints.
27
+ Defer saturated or busy chat startup to the turn queue, validate authentication at
28
+ allocation, and expire queued prompts before crash recovery or delayed submission.
29
+ Keep Gemini on its legacy transport with global runtime sockets and consume the
30
+ Conductor wrapper separator before forwarding a packaged CLI command.
31
+ - 924f51c: Time out stalled WebSocket handshakes so a fire can no longer wedge forever
32
+ mid-reconnect, and let an in-place restart replace a task's tmux fire that the
33
+ server sees as disconnected instead of refusing with "task already active".
34
+ - Updated dependencies [bdbda18]
35
+ - Updated dependencies [063b97c]
36
+ - Updated dependencies [31b61ea]
37
+ - Updated dependencies [924f51c]
38
+ - Updated dependencies [72324fd]
39
+ - @love-moon/ai-sdk@0.18.0
40
+ - @love-moon/conductor-sdk@0.18.0
41
+
3
42
  ## 0.17.2
4
43
 
5
44
  ### Patch Changes
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { createRequire } from "node:module";
5
+ import { pathToFileURL } from "node:url";
6
+ import yaml from "js-yaml";
7
+ import { resolveConductorConfigPath } from "../src/conductor-paths.js";
8
+
9
+ export async function runChatWebCommand(args = process.argv.slice(2), deps = {}) {
10
+ const log = deps.log ?? (text => process.stdout.write(text));
11
+ if (!args.length || args[0] === "--help" || args[0] === "-h") {
12
+ log("Usage: conductor chat-web login <kimi|deepseek> [--config-file PATH]\n conductor chat-web <serve|chat|turn|runtime> ...\n\nSign in once on the daemon host. web-kimi and web-deepseek start their runtime automatically.\n");
13
+ return 0;
14
+ }
15
+ const env = deps.env ?? process.env;
16
+ const forwarded = [];
17
+ let configFile;
18
+ for (let i = 0; i < args.length; i++) {
19
+ if (args[i] === "--") { forwarded.push(...args.slice(i + (forwarded.length === 0 ? 1 : 0))); break; }
20
+ if (args[i] === "--config-file") {
21
+ if (!args[i + 1]) throw new Error("--config-file requires a path");
22
+ configFile = args[++i];
23
+ } else if (args[i].startsWith("--config-file=")) {
24
+ configFile = args[i].slice("--config-file=".length);
25
+ if (!configFile) throw new Error("--config-file requires a path");
26
+ } else forwarded.push(args[i]);
27
+ }
28
+ args = forwarded;
29
+ const configPath = resolveConductorConfigPath(configFile, env);
30
+ if (configFile && !fs.existsSync(configPath)) throw new Error("Conductor config not found: " + configPath);
31
+ if (fs.existsSync(configPath)) {
32
+ const config = yaml.load(fs.readFileSync(configPath, "utf8"));
33
+ for (const [key, value] of Object.entries(config?.envs ?? {})) {
34
+ if (/^(CHAT_WEB_|https?_proxy$|all_proxy$|no_proxy$)/i.test(key) && env[key] === undefined && value !== null && value !== undefined) env[key] = String(value);
35
+ }
36
+ }
37
+ const req = createRequire(import.meta.url);
38
+ let mod;
39
+ try { mod = deps.chatWebModule ?? await import("@love-moon/chat-web"); }
40
+ catch { throw new Error("Browser chat is unavailable. Install @love-moon/chat-web on this daemon host."); }
41
+ if (args[0] === "login") {
42
+ if (args.length !== 2 || !["kimi", "deepseek"].includes(args[1])) throw new Error("Usage: conductor chat-web login <kimi|deepseek>");
43
+ if (typeof mod.RuntimeChatSession?.open !== "function") throw new Error("Upgrade @love-moon/chat-web to use Conductor browser chat login");
44
+ mod.registerBuiltinProviders();
45
+ await mod.login(args[1], { autoExit: true });
46
+ log("Browser chat account saved. Select web-" + args[1] + " in Conductor.\n");
47
+ return 0;
48
+ }
49
+ const entry = deps.cliEntry ?? path.join(path.dirname(req.resolve("@love-moon/chat-web/package.json")), "dist/cli.js");
50
+ // The packaged CLI handles socket management and prints structured results.
51
+ process.argv = [process.execPath, entry, ...args];
52
+ await (deps.importCli ?? (url => import(url)))(pathToFileURL(entry).href);
53
+ }
54
+
55
+ const entry = process.argv[1];
56
+ if (entry && fs.existsSync(entry) && fs.realpathSync(entry) === fs.realpathSync(new URL(import.meta.url))) {
57
+ runChatWebCommand().then(code => { if (typeof code === "number") process.exitCode = code; }, error => {
58
+ process.stderr.write(error.message + "\n"); process.exitCode = 1;
59
+ });
60
+ }
@@ -68,6 +68,12 @@ const DEFAULT_CLIs = {
68
68
  description: "Chat web (Kimi) via @love-moon/chat-web",
69
69
  runtimeBackend: "chat-web"
70
70
  },
71
+ "web-deepseek": {
72
+ command: "chat-web",
73
+ execArgs: "--model deepseek",
74
+ description: "Chat web (DeepSeek) via @love-moon/chat-web",
75
+ runtimeBackend: "chat-web"
76
+ },
71
77
  "web-gemini": {
72
78
  command: "chat-web",
73
79
  execArgs: "--model gemini",
@@ -143,10 +149,10 @@ function buildConfigEntryLines(cli, info, { commented = false } = {}) {
143
149
  lines.push(`${commentPrefix}opencode runs via ai-sdk server mode with permission=allow`);
144
150
  }
145
151
  if (cli === "web-chatgpt") {
146
- // web-chatgpt / web-gemini / web-kimi resolve to the chat-web runtime backend
152
+ // web-chatgpt / web-gemini / web-kimi / web-deepseek resolve to the chat-web runtime backend
147
153
  // (an in-process Chromium driver via @love-moon/chat-web). The command
148
154
  // line is not executed; CLI parses --model from it.
149
- lines.push(`${commentPrefix}web-chatgpt / web-gemini / web-kimi drive a real Chromium browser via @love-moon/chat-web`);
155
+ lines.push(`${commentPrefix}web-chatgpt / web-gemini / web-kimi / web-deepseek drive a real Chromium browser via @love-moon/chat-web`);
150
156
  }
151
157
 
152
158
  lines.push(`${entryPrefix}${cli}: ${fullCommand}`);
@@ -266,9 +266,17 @@ export function resolveAiSessionCommandLine(backend, allowCliList, env = process
266
266
  }
267
267
 
268
268
  export function resolveAiSessionOptions(backend, allowCliList, env = process.env, sessionBackend = backend) {
269
- return extractAiSessionOptionsFromCommandLine(
270
- resolveAiSessionCommandLine(backend, allowCliList, env, sessionBackend),
271
- );
269
+ const commandLine = resolveAiSessionCommandLine(backend, allowCliList, env, sessionBackend);
270
+ const options = extractAiSessionOptionsFromCommandLine(commandLine);
271
+ if (normalizeRuntimeBackendName(sessionBackend) === "chat-web") {
272
+ const { parts } = parseCommandParts(commandLine);
273
+ for (const [positive, negative, key] of [["--headless", "--headed", "headless"], ["--deepthink", "--no-deepthink", "chatWebDeepThink"]]) {
274
+ if (parts.includes(positive) && parts.includes(negative)) throw new Error(`Choose ${positive} or ${negative}`);
275
+ if (parts.includes(positive)) options[key] = true;
276
+ else if (parts.includes(negative)) options[key] = false;
277
+ }
278
+ }
279
+ return options;
272
280
  }
273
281
 
274
282
  const DEFAULT_POLL_INTERVAL_MS = parseInt(
package/bin/conductor.js CHANGED
@@ -51,6 +51,7 @@ export function runConductorCli(args = argv, deps = {}) {
51
51
  "send-file",
52
52
  "channel",
53
53
  "serve-ai",
54
+ "chat-web",
54
55
  "project",
55
56
  "issue",
56
57
  "task",
@@ -140,6 +141,7 @@ Subcommands:
140
141
  diagnose Diagnose a task and print likely root cause
141
142
  send-file Upload a local file into a task session
142
143
  channel Connect user-owned chat channel providers
144
+ chat-web Sign in and manage browser chat accounts and conversations
143
145
  serve-ai Start an OpenAI-compatible local AI server
144
146
  project Manage Conductor projects (list/show/create/update/delete/reorder/
145
147
  agents/collab/labels/...)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.17.2",
4
- "gitCommitId": "1671ee1",
3
+ "version": "0.18.0",
4
+ "gitCommitId": "825d90a",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/lovemoon-ai/conductor.git"
@@ -20,12 +20,12 @@
20
20
  "provenance": true
21
21
  },
22
22
  "scripts": {
23
- "pretest": "pnpm --dir ../modules/ai-sdk build && pnpm --dir ../modules/conductor-sdk build",
23
+ "pretest": "pnpm --dir ../modules/chat-web build && pnpm --dir ../modules/ai-sdk build && pnpm --dir ../modules/conductor-sdk build",
24
24
  "test": "node --test test/*.test.js"
25
25
  },
26
26
  "dependencies": {
27
- "@love-moon/ai-sdk": "0.17.2",
28
- "@love-moon/conductor-sdk": "0.17.2",
27
+ "@love-moon/ai-sdk": "0.18.0",
28
+ "@love-moon/conductor-sdk": "0.18.0",
29
29
  "@github/copilot-sdk": "^0.3.0",
30
30
  "chrome-launcher": "^1.2.1",
31
31
  "chrome-remote-interface": "^0.33.0",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "optionalDependencies": {
40
40
  "@roamhq/wrtc": "^0.10.0",
41
- "@love-moon/chat-web": "0.17.2"
41
+ "@love-moon/chat-web": "0.18.0"
42
42
  },
43
43
  "pnpm": {
44
44
  "onlyBuiltDependencies": [
package/src/daemon.js CHANGED
@@ -7081,7 +7081,7 @@ export function startDaemon(config = {}, deps = {}) {
7081
7081
  // left is a still-exiting child or a tmux record the liveness reaper has not
7082
7082
  // swept yet; a fire that is somehow still alive is stopped here. Its terminal
7083
7083
  // status is suppressed because the task row now belongs to the new round.
7084
- async function releaseReplacedFire(taskId) {
7084
+ async function releaseReplacedFire(taskId, reason = "persistent_new_round") {
7085
7085
  const record = activeTaskProcesses.get(taskId);
7086
7086
  if (!recordHasLiveFire(record)) {
7087
7087
  return true;
@@ -7099,12 +7099,12 @@ export function startDaemon(config = {}, deps = {}) {
7099
7099
  forgetFireSessionRecord(record);
7100
7100
  return true;
7101
7101
  }
7102
- stopActiveTaskProcess(taskId, { reason: "persistent_new_round", suppressExitStatusReport: true });
7102
+ stopActiveTaskProcess(taskId, { reason, suppressExitStatusReport: true });
7103
7103
  // The tmux stop path retires the record synchronously and never consumes the flag.
7104
7104
  suppressedExitStatusReports.delete(taskId);
7105
7105
  return !activeTaskProcesses.has(taskId);
7106
7106
  }
7107
- stopActiveTaskProcess(taskId, { reason: "persistent_new_round", suppressExitStatusReport: true });
7107
+ stopActiveTaskProcess(taskId, { reason, suppressExitStatusReport: true });
7108
7108
  const stopped = await waitForTaskToStop(taskId);
7109
7109
  if (!stopped) {
7110
7110
  suppressedExitStatusReports.delete(taskId);
@@ -8278,6 +8278,18 @@ export function startDaemon(config = {}, deps = {}) {
8278
8278
  }
8279
8279
  }
8280
8280
 
8281
+ // The server saw no connected fire for this task, so a surviving session
8282
+ // is a wedged fire (e.g. stuck mid-reconnect): replace it, don't refuse.
8283
+ if (
8284
+ !isRefreshSessionInplace &&
8285
+ payload?.replace_existing_fire === true &&
8286
+ recordHasLiveFire(activeTarget)
8287
+ ) {
8288
+ log(`Replacing disconnected fire for task ${normalizedTargetTaskId} before restart`);
8289
+ await releaseReplacedFire(normalizedTargetTaskId, "restart_replace_disconnected_fire");
8290
+ activeTarget = activeTaskProcesses.get(normalizedTargetTaskId);
8291
+ }
8292
+
8281
8293
  // The probe above already cleared the record if the session was
8282
8294
  // conclusively gone, so a surviving tmuxMode entry means a live session.
8283
8295
  const targetHasLiveFire = recordHasLiveFire(activeTarget);
@@ -19,7 +19,7 @@ const BUILT_IN_RUNTIME_BACKEND_SET = new Set(BUILT_IN_RUNTIME_BACKENDS);
19
19
  //
20
20
  // chat-web is intentionally NOT here even though it's also command-optional
21
21
  // (it drives a Chromium browser, not a CLI). The reason: chat-web has
22
- // configurable sub-providers (chatgpt / gemini / kimi) selected via
22
+ // configurable sub-providers (chatgpt / gemini / kimi / deepseek) selected via
23
23
  // `--model`, and surfacing a bare `chat-web` backend alongside user
24
24
  // aliases like `web-chatgpt` / `web-gemini` / `web-kimi` is just confusing — the alias
25
25
  // IS the sub-provider choice. Users who want chat-web must declare an