@love-moon/conductor-cli 0.17.1 → 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,70 @@
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
+
42
+ ## 0.17.2
43
+
44
+ ### Patch Changes
45
+
46
+ - 0a9488e: Add Kimi website text conversations with persistent login, multi-turn context,
47
+ reply progress, new chats and browser diagnostics. Expose the provider through
48
+ ai-sdk's chat-web backend and the web-kimi Conductor configuration alias.
49
+
50
+ Explicit unknown chat-web providers now fail with a clear error instead of
51
+ silently opening ChatGPT. The existing kimi CLI backend is unchanged.
52
+
53
+ - b16c1ae: Allow a shared daemon to run the AI side of a Global AI Backend task while code
54
+ stays on one of the user's own daemons. Advertise
55
+ `shared_ai_global_backend_v1` and forward the task id on remote exec, file, and
56
+ MCP requests so the backend can authorize only that task's remote binding.
57
+
58
+ Keep remote grants tied to active tasks: shared credentials cannot mint remote
59
+ bindings, renew them through restarts or persistent rounds, or reactivate ended
60
+ tasks through status updates. Check shared AI compatibility when restarting or
61
+ starting later rounds of an existing remote task too.
62
+
63
+ - Updated dependencies [b124565]
64
+ - Updated dependencies [0a9488e]
65
+ - @love-moon/ai-sdk@0.17.2
66
+ - @love-moon/conductor-sdk@0.17.2
67
+
3
68
  ## 0.17.1
4
69
 
5
70
  ### 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
+ }
@@ -55,13 +55,25 @@ const DEFAULT_CLIs = {
55
55
  // chat-web is the runtime backend (an in-process Chromium driver, not a CLI
56
56
  // binary). It has multiple sub-providers selected via --model. Each user-
57
57
  // facing alias below resolves to the chat-web runtime; advertising the bare
58
- // `chat-web` backend would be ambiguous, so we emit two explicit aliases.
58
+ // `chat-web` backend would be ambiguous, so we emit explicit aliases.
59
59
  "web-chatgpt": {
60
60
  command: "chat-web",
61
61
  execArgs: "--model chatgpt",
62
62
  description: "Chat web (ChatGPT) via @love-moon/chat-web",
63
63
  runtimeBackend: "chat-web"
64
64
  },
65
+ "web-kimi": {
66
+ command: "chat-web",
67
+ execArgs: "--model kimi",
68
+ description: "Chat web (Kimi) via @love-moon/chat-web",
69
+ runtimeBackend: "chat-web"
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
+ },
65
77
  "web-gemini": {
66
78
  command: "chat-web",
67
79
  execArgs: "--model gemini",
@@ -137,10 +149,10 @@ function buildConfigEntryLines(cli, info, { commented = false } = {}) {
137
149
  lines.push(`${commentPrefix}opencode runs via ai-sdk server mode with permission=allow`);
138
150
  }
139
151
  if (cli === "web-chatgpt") {
140
- // web-chatgpt / web-gemini both resolve to the chat-web runtime backend
152
+ // web-chatgpt / web-gemini / web-kimi / web-deepseek resolve to the chat-web runtime backend
141
153
  // (an in-process Chromium driver via @love-moon/chat-web). The command
142
154
  // line is not executed; CLI parses --model from it.
143
- lines.push(`${commentPrefix}web-chatgpt / web-gemini 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`);
144
156
  }
145
157
 
146
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(
@@ -940,10 +948,10 @@ async function main() {
940
948
  env: process.env,
941
949
  });
942
950
 
943
- // chat-web tasks drive a real Chromium browser that holds a per-profile
944
- // singleton lock for as long as the task lives. A task that never ends
945
- // pins that browser indefinitely (and blocks other chats for the same
946
- // provider), so we cap its active lifetime and auto-stop it when exceeded.
951
+ // chat-web tasks retain a tab in the shared profile browser while alive.
952
+ // New runtimes schedule turns across tasks; older runtimes exclusively
953
+ // own the profile. Cap task lifetime so abandoned sessions release their
954
+ // browser resources in either runtime.
947
955
  // The conversation itself is preserved (it lives in the provider account
948
956
  // and the persisted profile; closing the browser does not delete it).
949
957
  const runtimeBackendToken = String(sessionCommandLine || "")
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.1",
4
- "gitCommitId": "e3dbbad",
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.1",
28
- "@love-moon/conductor-sdk": "0.17.1",
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.1"
41
+ "@love-moon/chat-web": "0.18.0"
42
42
  },
43
43
  "pnpm": {
44
44
  "onlyBuiltDependencies": [
package/src/daemon.js CHANGED
@@ -3928,6 +3928,9 @@ export function startDaemon(config = {}, deps = {}) {
3928
3928
  // another daemon over `conductor remote` and binds the remote_* MCP tools
3929
3929
  // to a remoteWorkspace as well as a remoteWorktree.
3930
3930
  "global_backend_v1",
3931
+ // A shared AI also sends CONDUCTOR_TASK_ID on every remote request so the
3932
+ // backend can grant only that task's A → B remote permission.
3933
+ "shared_ai_global_backend_v1",
3931
3934
  "backend_session_list",
3932
3935
  CUSTOM_COMMANDS_CAPABILITY,
3933
3936
  UPDATE_DAEMON_CAPABILITY,
@@ -7078,7 +7081,7 @@ export function startDaemon(config = {}, deps = {}) {
7078
7081
  // left is a still-exiting child or a tmux record the liveness reaper has not
7079
7082
  // swept yet; a fire that is somehow still alive is stopped here. Its terminal
7080
7083
  // status is suppressed because the task row now belongs to the new round.
7081
- async function releaseReplacedFire(taskId) {
7084
+ async function releaseReplacedFire(taskId, reason = "persistent_new_round") {
7082
7085
  const record = activeTaskProcesses.get(taskId);
7083
7086
  if (!recordHasLiveFire(record)) {
7084
7087
  return true;
@@ -7096,12 +7099,12 @@ export function startDaemon(config = {}, deps = {}) {
7096
7099
  forgetFireSessionRecord(record);
7097
7100
  return true;
7098
7101
  }
7099
- stopActiveTaskProcess(taskId, { reason: "persistent_new_round", suppressExitStatusReport: true });
7102
+ stopActiveTaskProcess(taskId, { reason, suppressExitStatusReport: true });
7100
7103
  // The tmux stop path retires the record synchronously and never consumes the flag.
7101
7104
  suppressedExitStatusReports.delete(taskId);
7102
7105
  return !activeTaskProcesses.has(taskId);
7103
7106
  }
7104
- stopActiveTaskProcess(taskId, { reason: "persistent_new_round", suppressExitStatusReport: true });
7107
+ stopActiveTaskProcess(taskId, { reason, suppressExitStatusReport: true });
7105
7108
  const stopped = await waitForTaskToStop(taskId);
7106
7109
  if (!stopped) {
7107
7110
  suppressedExitStatusReports.delete(taskId);
@@ -8275,6 +8278,18 @@ export function startDaemon(config = {}, deps = {}) {
8275
8278
  }
8276
8279
  }
8277
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
+
8278
8293
  // The probe above already cleared the record if the session was
8279
8294
  // conclusively gone, so a surviving tmuxMode entry means a live session.
8280
8295
  const targetHasLiveFire = recordHasLiveFire(activeTarget);
@@ -62,7 +62,11 @@ export function loadCliConfig(configFile, env = process.env) {
62
62
  }
63
63
 
64
64
  function authHeaders(config) {
65
- return { Authorization: `Bearer ${config.agentToken}` };
65
+ const taskId = process.env.CONDUCTOR_TASK_ID?.trim();
66
+ return {
67
+ Authorization: `Bearer ${config.agentToken}`,
68
+ ...(taskId ? { "X-Conductor-Task-Id": taskId } : {}),
69
+ };
66
70
  }
67
71
 
68
72
  async function toApiError(response) {
@@ -19,6 +19,7 @@ export const REMOTE_WORKTREE_ENV = "CONDUCTOR_REMOTE_WORKTREE";
19
19
  const FORWARDED_ENV = [
20
20
  "CONDUCTOR_AGENT_TOKEN",
21
21
  "CONDUCTOR_BACKEND_URL",
22
+ "CONDUCTOR_TASK_ID",
22
23
  "CONDUCTOR_CONFIG",
23
24
  "CONDUCTOR_HOME",
24
25
  "CONDUCTOR_REMOTE_CHUNK_BYTES",
@@ -19,9 +19,9 @@ 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 / deepseek) 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
- // aliases like `web-chatgpt` / `web-gemini` is just confusing — the alias
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
26
26
  // explicit allow_cli_list entry (which is also where `--model` lives).
27
27
  const COMMAND_OPTIONAL_BUILT_IN_RUNTIME_BACKENDS = ["copilot", "dsh"];