@love-moon/conductor-cli 0.9.0 → 0.11.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,134 @@
1
1
  # @love-moon/conductor-cli
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c987457: Allow creating a project whose workspace path does not exist yet.
8
+
9
+ The daemon now accepts `create_if_missing` on `validate_project_path` and will
10
+ `mkdir -p` the workspace before snapshotting it, advertising the new
11
+ `project_path_create` capability. In the web Create Project dialog a missing
12
+ path no longer dead-ends: it offers "Create this directory and continue", so a
13
+ typo still fails loudly instead of silently creating the wrong folder.
14
+ `conductor project create` gains `--create-workspace` for the same behavior.
15
+
16
+ - ccf902d: Add daemon sharing so a host can invite other conductor accounts to run tasks on their machine.
17
+
18
+ A host daemon can create share invitations from the web settings page. Each invite is scoped to one guest user and carries a `allow_cli_list` whitelist. When the guest accepts, the host daemon spawns an isolated guest daemon with a dedicated workspace and token; the guest sees only their own tasks and cannot access the host's projects or files outside the shared workspace.
19
+
20
+ Guest daemons are supervised by the host daemon: if the host restarts, guest configs are regenerated from the backend on the next reconcile; revoking a share stops the guest daemon and removes its workspace.
21
+
22
+ This changes the agent-token schema (a new `scope` column is required on the backend) and adds `conductor_guest`, `allow_cli_list`, and `daemon_name` handling to the guest config path.
23
+
24
+ ### Patch Changes
25
+
26
+ - 30afccc: Make the claude backend usable when conductor runs as root.
27
+
28
+ Claude Code refuses `bypassPermissions` / `--dangerously-skip-permissions` under
29
+ root unless `IS_SANDBOX=1` or `CLAUDE_CODE_BUBBLEWRAP` is set, so every claude
30
+ turn died immediately on root installs (docker, CI, bare VPS). Both routes a
31
+ claude command takes are now root-aware, sharing one check
32
+ (`isClaudeRootPermissionRestricted`, which mirrors claude's own gate):
33
+
34
+ - The agent-SDK session downgrades the permission mode to `acceptEdits` (auto
35
+ mode) and stops passing `allowDangerouslySkipPermissions`.
36
+ - The daemon's tool-preset PTY path rewrites the configured `allow_cli_list`
37
+ command via `resolveClaudeCommandForRoot`, using the same env the spawned
38
+ child receives, so a per-task `IS_SANDBOX=1` opt-out is honored.
39
+ - `conductor config` no longer writes `--dangerously-skip-permissions` into a
40
+ config generated on a root machine.
41
+
42
+ The mode is also configurable now: `claude --permission-mode acceptEdits` in
43
+ `allow_cli_list` is lifted out of the command string. An unrecognized mode still
44
+ falls back to the default instead of failing, but logs a warning at session boot.
45
+
46
+ ai-sdk gains three exports: `isClaudeRootPermissionRestricted`,
47
+ `resolveClaudeCommandForRoot`, `resolveClaudePermissionPolicy`.
48
+
49
+ - a3c41dd: Document every supported key in the config file `conductor config` generates.
50
+
51
+ The generated `config.yaml` only ever contained six keys, so nothing told a new
52
+ user that the other fifteen existed. They are now all present as commented-out
53
+ entries carrying their default value and env-var override, grouped into
54
+ Connection / Coding CLIs / Environment / Daemon behaviour / Optional features:
55
+
56
+ - `websocket_url`, previously written only when device authorization returned one
57
+ - `pre_prompt`, `custom_commands`, `cdp_user_data_dir`
58
+ - `remote_exec`, `fire_tmux_mode`, `auto_update`, `auto_update_respawn`,
59
+ `update_window`
60
+ - `ai_manager.codex.auth_json`, `serve_ai.{host,port,backend,api_key}`,
61
+ `channels.feishu.*`
62
+ - `envs.no_proxy`, `envs.DEEPSEEK_API_KEY`, `envs.DEEPSEEK_BASE_URL` alongside
63
+ the proxy keys and `AISDK_PROVIDER_PATH` that were already shown
64
+
65
+ Three notes correct documentation that would otherwise mislead. The commented
66
+ `websocket_url` example is derived from this install's own `backend_url` (via
67
+ the same rule as `ConductorConfig.resolvedWebsocketUrl`) instead of naming the
68
+ official host, so a self-hosted user who uncomments it is not pointed at
69
+ someone else's server. `daemon_name` is flagged as the one key whose precedence
70
+ is inverted — the config value wins over `CONDUCTOR_DAEMON_NAME`. `log_level`
71
+ is marked as validated on load but not yet consumed by any component.
72
+
73
+ Behaviour is unchanged: the same six keys are still written uncommented, and
74
+ the daemon still derives its own WebSocket URL from `backend_url` regardless of
75
+ the `websocket_url` key.
76
+
77
+ - ccf902d: Refuse `--force` daemon restart when it would take over an unrelated daemon instance.
78
+
79
+ Previously `conductor daemon --force` blindly replaced any existing lock, which could kill a daemon belonging to a different conductor home, backend URL, or agent token. The lock now stores an identity fingerprint (home directory, backend URL, agent token prefix, and daemon name) and `--force` only overwrites when the fingerprint matches. A mismatch reports the running daemon's identity and exits with code 7.
80
+
81
+ This makes `--force` safe for "restart my own stuck daemon" while preventing accidental cross-instance takeovers.
82
+
83
+ - 884945c: Keep every task in a multi-agent group in one working directory. Creating a
84
+ task with several agents and `worktree` enabled put the worker in
85
+ `.conductor/worktrees/<branch>/` but pinned each reviewer to the project root,
86
+ so reviewers read the base branch instead of the worker's changes. Reviewers
87
+ now inherit the worker's worktree.
88
+
89
+ Reviewers join as reuse-only members: they carry the full worktree identity —
90
+ so archiving one member no longer deletes a directory its siblings are still
91
+ running in — but never run `git worktree add` themselves. The daemon
92
+ additionally serializes worktree preparation per on-disk root, so concurrent
93
+ group members can no longer race on creating the same branch, and treats a
94
+ worktree as ready only once preparation has fully completed rather than as soon
95
+ as `.git` appears.
96
+
97
+ Two new optional daemon environment variables:
98
+ `CONDUCTOR_WORKTREE_REUSE_WAIT_TIMEOUT_MS` (default `180000`) and
99
+ `CONDUCTOR_WORKTREE_REUSE_POLL_INTERVAL_MS` (default `250`).
100
+
101
+ - Updated dependencies [30afccc]
102
+ - @love-moon/ai-sdk@0.11.0
103
+ - @love-moon/conductor-sdk@0.11.0
104
+
105
+ ## 0.10.0
106
+
107
+ ### Minor Changes
108
+
109
+ - 43c4f87: Runtime health preflight and agent schedule access control. The daemon now
110
+ advertises positive backend runtime health (`x-conductor-runtime-health`) so the
111
+ backend can reject task creation with `503 runtime_unavailable` before any
112
+ timeline activity, and adds `disable_built_in_cli_list` to opt out of built-in
113
+ SDK backends. The SDK attributes agent-originated scheduled-message calls with
114
+ `X-Conductor-Actor: agent` so per-task `agent_schedule_access`
115
+ (full/read_only/blocked) can govern `conductor task schedule` from agents while
116
+ human/UI calls stay unrestricted.
117
+
118
+ ### Patch Changes
119
+
120
+ - 43c4f87: Fix `conductor fire` and `conductor diagnose` with an explicit `--config-file`
121
+ resolving `agent_token`/`backend_url` from daemon-injected `CONDUCTOR_*` env
122
+ instead of the file. Fire could dial the file's websocket with the inherited
123
+ token and loop on `4002 invalid-token`; diagnose queried the wrong backend and
124
+ 404'd. An explicit config file now wins over inherited env, matching the
125
+ daemon's behavior.
126
+ - 43c4f87: Fix `conductor update` installing into the wrong npm prefix.
127
+ - Updated dependencies [43c4f87]
128
+ - Updated dependencies [43c4f87]
129
+ - @love-moon/ai-sdk@0.10.0
130
+ - @love-moon/conductor-sdk@0.10.0
131
+
3
132
  ## 0.9.0
4
133
 
5
134
  ### Minor Changes
@@ -10,6 +10,7 @@ import { createRequire } from "node:module";
10
10
  import yargs from "yargs/yargs";
11
11
  import { hideBin } from "yargs/helpers";
12
12
  import { RUNTIME_SUPPORTED_BACKENDS } from "../src/runtime-backends.js";
13
+ import { isClaudeRootPermissionRestricted } from "@love-moon/ai-sdk";
13
14
  import { resolveConductorConfigPath } from "../src/conductor-paths.js";
14
15
 
15
16
  const CONFIG_FILE = resolveConductorConfigPath();
@@ -21,7 +22,12 @@ const packageJson = JSON.parse(
21
22
  const DEFAULT_CLIs = {
22
23
  claude: {
23
24
  command: "claude",
24
- execArgs: "--dangerously-skip-permissions",
25
+ // claude exits 1 with `--dangerously-skip-permissions` under root, so never
26
+ // write a flag the user's own machine cannot run. Same root check the
27
+ // daemon and the ai-sdk session use.
28
+ execArgs: isClaudeRootPermissionRestricted()
29
+ ? "--permission-mode acceptEdits"
30
+ : "--dangerously-skip-permissions",
25
31
  description: "Anthropic Claude CLI"
26
32
  },
27
33
  codex: {
@@ -293,20 +299,44 @@ async function main() {
293
299
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
294
300
 
295
301
  const lines = [
302
+ "# Conductor CLI configuration.",
303
+ "# Lookup order: --config-file > $CONDUCTOR_CONFIG > $CONDUCTOR_HOME/config.yaml",
304
+ "# > ~/.conductor/config.yaml",
305
+ "# Commented-out keys are optional; the value shown is the default.",
306
+ "",
307
+ "# ---------------------------------------------------------------------------",
308
+ "# Connection",
309
+ "# ---------------------------------------------------------------------------",
310
+ "",
311
+ "# Agent token issued by `conductor config`. Env: CONDUCTOR_AGENT_TOKEN",
296
312
  `agent_token: ${yamlQuote(token)}`,
313
+ "# Conductor server this daemon talks to. Env: CONDUCTOR_BACKEND_URL",
297
314
  `backend_url: ${yamlQuote(resolvedBackendUrl)}`,
315
+ "# Realtime endpoint used by fire and the other CLI commands; derived from",
316
+ "# backend_url when unset. The daemon always derives its own WebSocket URL",
317
+ "# from backend_url and ignores this key. Env: CONDUCTOR_WS_URL",
318
+ resolvedWebsocketUrl
319
+ ? `websocket_url: ${yamlQuote(resolvedWebsocketUrl)}`
320
+ : `# websocket_url: ${deriveWebsocketUrlHint(resolvedBackendUrl)}`,
321
+ "# Name this machine shows up as in the app. Precedence is inverted here:",
322
+ "# --daemon-name > this key > CONDUCTOR_DAEMON_NAME > hostname, so this value",
323
+ "# wins over the env var.",
298
324
  `daemon_name: ${yamlQuote(defaultDaemonName)}`,
325
+ "# debug | info | warning | error | critical (default: info). Validated on",
326
+ "# load but not consumed by any component yet. Env: CONDUCTOR_LOG_LEVEL",
299
327
  "log_level: debug",
328
+ "# Root directory where the daemon creates task workspaces. Env: CONDUCTOR_WS",
300
329
  "workspace: '~/ws/fires'",
301
330
  "",
302
- "# Allowed coding CLIs",
331
+ "# ---------------------------------------------------------------------------",
332
+ "# Coding CLIs",
333
+ "# ---------------------------------------------------------------------------",
334
+ "",
335
+ "# Allowed coding CLIs. Key = backend name shown in the app, value = command",
336
+ "# line used to launch it. Only backends listed here can run tasks.",
303
337
  "allow_cli_list:"
304
338
  ];
305
339
 
306
- if (resolvedWebsocketUrl) {
307
- lines.splice(2, 0, `websocket_url: ${yamlQuote(resolvedWebsocketUrl)}`);
308
- }
309
-
310
340
  if (detectedCLIs.length > 0) {
311
341
  detectedCLIs.forEach((cli) => {
312
342
  const info = DEFAULT_CLIs[cli];
@@ -321,12 +351,93 @@ async function main() {
321
351
 
322
352
  lines.push(
323
353
  "",
324
- "# Uncomment to use custom envs, such as proxy.",
354
+ "# Uncomment to disable built-in SDK backends that are otherwise advertised",
355
+ "# automatically (copilot, dsh). Listing a name here hides it from the",
356
+ "# daemon's supported backends without removing the shipped dependency.",
357
+ "# disable_built_in_cli_list:",
358
+ "# - dsh",
359
+ "# - copilot",
360
+ "",
361
+ "# Text automatically sent as the first message of every new task, per",
362
+ "# backend. ${VAR} / $VAR are expanded from the environment.",
363
+ "# pre_prompt:",
364
+ "# claude: Always answer in English and run the tests before finishing.",
365
+ "# codex: Read AGENTS.md first.",
366
+ "",
367
+ "# ---------------------------------------------------------------------------",
368
+ "# Environment for the coding CLIs",
369
+ "# ---------------------------------------------------------------------------",
370
+ "",
371
+ "# Extra environment variables exported into every backend process. Common",
372
+ "# uses: proxies, API keys, and loading a private AI SDK provider.",
325
373
  "# envs:",
326
374
  "# http_proxy: http://127.0.0.1:7890",
327
375
  "# https_proxy: http://127.0.0.1:7890",
328
376
  "# all_proxy: socks5://127.0.0.1:7890",
329
- "# AISDK_PROVIDER_PATH: path-to-private-aisdk-provider.js"
377
+ "# no_proxy: localhost,127.0.0.1",
378
+ "# DEEPSEEK_API_KEY: sk-xxx # required by the built-in dsh backend",
379
+ "# DEEPSEEK_BASE_URL: https://api.deepseek.com",
380
+ "# AISDK_PROVIDER_PATH: path-to-private-aisdk-provider.js",
381
+ "",
382
+ "# ---------------------------------------------------------------------------",
383
+ "# Daemon behaviour",
384
+ "# ---------------------------------------------------------------------------",
385
+ "",
386
+ "# Accept `conductor remote-exec` requests for this host.",
387
+ "# Env: CONDUCTOR_REMOTE_EXEC",
388
+ "# remote_exec: true",
389
+ "",
390
+ "# Launch each Fire process inside a detached tmux session so it survives a",
391
+ "# daemon restart. Requires tmux on PATH. Env: CONDUCTOR_FIRE_TMUX_MODE",
392
+ "# fire_tmux_mode: false",
393
+ "",
394
+ "# Let the daemon upgrade the CLI in place (npm/pnpm installs only).",
395
+ "# Env: CONDUCTOR_AUTO_UPDATE",
396
+ "# auto_update: true",
397
+ "",
398
+ "# After an auto-update, restart the daemon into the new version.",
399
+ "# Env: CONDUCTOR_AUTO_UPDATE_RESPAWN",
400
+ "# auto_update_respawn: false",
401
+ "",
402
+ "# Local-time window in which auto-update may run. Env: CONDUCTOR_UPDATE_WINDOW",
403
+ "# update_window: '02:00-04:00'",
404
+ "",
405
+ "# Scripts exposed to the app as one-click commands. Key = command name,",
406
+ "# value = executable script path (relative paths resolve against this file).",
407
+ "# custom_commands:",
408
+ "# restart-service: ~/.conductor/scripts/restart-service.sh",
409
+ "",
410
+ "# Chrome profile directory used by `conductor chrome` and the web-* backends.",
411
+ "# Env: CDP_USER_DATA_DIR",
412
+ "# cdp_user_data_dir: ~/.conductor/chrome-profile",
413
+ "",
414
+ "# ---------------------------------------------------------------------------",
415
+ "# Optional features",
416
+ "# ---------------------------------------------------------------------------",
417
+ "",
418
+ "# Extra Codex auth.json files to switch between from the app's account",
419
+ "# picker. The file name (without .json) becomes the account name.",
420
+ "# ai_manager:",
421
+ "# codex:",
422
+ "# auth_json:",
423
+ "# - ~/.codex/auth-work.json",
424
+ "# - ~/.codex/auth-personal.json",
425
+ "",
426
+ "# Defaults for `conductor serve-ai`, the OpenAI-compatible local server.",
427
+ "# serve_ai:",
428
+ "# host: 127.0.0.1",
429
+ "# port: 8787",
430
+ "# backend: codex",
431
+ "# api_key: local-dev-key",
432
+ "",
433
+ "# Feishu (Lark) chat channel, uploaded with `conductor channel connect feishu`.",
434
+ "# channels:",
435
+ "# feishu:",
436
+ "# app_id: cli_xxx",
437
+ "# app_secret: xxx",
438
+ "# verification_token: xxx",
439
+ "# encrypt_key: xxx",
440
+ ""
330
441
  );
331
442
 
332
443
  fs.writeFileSync(CONFIG_FILE, lines.join("\n"), "utf-8");
@@ -681,6 +792,20 @@ function yamlQuote(value) {
681
792
  return JSON.stringify(value);
682
793
  }
683
794
 
795
+ // Mirror ConductorConfig.resolvedWebsocketUrl so the commented-out
796
+ // `websocket_url` example shows THIS install's derived default rather than a
797
+ // hard-coded official host — a self-hosted user who uncomments it would
798
+ // otherwise point their CLI at someone else's server.
799
+ function deriveWebsocketUrlHint(backendUrl) {
800
+ try {
801
+ const url = new URL(backendUrl);
802
+ const scheme = url.protocol === "https:" ? "wss" : "ws";
803
+ return `${scheme}://${url.host}/ws/agent`;
804
+ } catch {
805
+ return "wss://<your-backend-host>/ws/agent";
806
+ }
807
+ }
808
+
684
809
  main()
685
810
  .catch((error) => {
686
811
  process.stderr.write(`Failed to write config: ${error?.message || error}\n`);
@@ -14,6 +14,12 @@ import {
14
14
  resolveConductorConfigPath,
15
15
  resolveConductorHome,
16
16
  } from "../src/conductor-paths.js";
17
+ import {
18
+ DAEMON_LOCK_FILE_NAME,
19
+ buildDaemonInstanceIdentity,
20
+ describeForceRestartRefusal,
21
+ parseDaemonLockState,
22
+ } from "../src/daemon-lock.js";
17
23
 
18
24
  const argv = hideBin(process.argv);
19
25
 
@@ -109,39 +115,33 @@ function expandHomePath(inputPath, homeDir) {
109
115
  return inputPath;
110
116
  }
111
117
 
112
- function resolveWorkspaceRoot(configFilePath) {
113
- const userConfig = loadUserConfig(configFilePath);
118
+ function resolveWorkspaceRoot(userConfig) {
114
119
  const home = process.env.HOME || os.homedir() || "/tmp";
115
120
  const workspaceRoot = process.env.CONDUCTOR_WS || userConfig.workspace || path.join(home, "ws");
116
121
  return expandHomePath(workspaceRoot, home);
117
122
  }
118
123
 
119
- function findRunningDaemonPid(workspaceRoot) {
120
- const lockFile = path.join(workspaceRoot, "daemon.pid");
121
- if (!fs.existsSync(lockFile)) {
124
+ function readDaemonLockState(workspaceRoot) {
125
+ const lockFile = path.join(workspaceRoot, DAEMON_LOCK_FILE_NAME);
126
+ try {
127
+ return parseDaemonLockState(fs.readFileSync(lockFile, "utf8"));
128
+ } catch (_err) {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ function findRunningDaemonPid(lockState) {
134
+ const pid = lockState?.pid;
135
+ if (!pid) {
122
136
  return null;
123
137
  }
124
138
  try {
125
- const pid = parseInt(fs.readFileSync(lockFile, "utf8"), 10);
126
- if (Number.isNaN(pid)) {
127
- return null;
128
- }
129
139
  process.kill(pid, 0);
130
140
  return pid;
131
141
  } catch (err) {
132
- if (err && err.code === "ESRCH") {
133
- return null;
134
- }
135
- return pidFromLockFile(lockFile);
136
- }
137
- }
138
-
139
- function pidFromLockFile(lockFile) {
140
- try {
141
- const pid = parseInt(fs.readFileSync(lockFile, "utf8"), 10);
142
- return Number.isNaN(pid) ? null : pid;
143
- } catch (_err) {
144
- return null;
142
+ // EPERM means the process exists but is owned by another OS user, which is
143
+ // still "running" for our purposes.
144
+ return err && err.code === "ESRCH" ? null : pid;
145
145
  }
146
146
  }
147
147
 
@@ -181,13 +181,37 @@ const effectiveConfigFile = args.configFile
181
181
  : undefined;
182
182
 
183
183
  if (args.nohup) {
184
- const workspaceRoot = resolveWorkspaceRoot(effectiveConfigFile);
185
- const runningPid = findRunningDaemonPid(workspaceRoot);
186
- if (runningPid && !args.force) {
187
- process.stderr.write(
188
- `${CLI_NAME} detected an existing daemon (PID ${runningPid}). Use --force to restart.\n`,
189
- );
190
- process.exit(1);
184
+ const userConfig = loadUserConfig(effectiveConfigFile);
185
+ const workspaceRoot = resolveWorkspaceRoot(userConfig);
186
+ const lockState = readDaemonLockState(workspaceRoot);
187
+ const runningPid = findRunningDaemonPid(lockState);
188
+ if (runningPid) {
189
+ if (!args.force) {
190
+ process.stderr.write(
191
+ `${CLI_NAME} detected an existing daemon (PID ${runningPid}). Use --force to restart.\n`,
192
+ );
193
+ process.exit(1);
194
+ }
195
+ const identity = buildDaemonInstanceIdentity({
196
+ conductorHome: materializedConductorPathEnv.CONDUCTOR_HOME,
197
+ configPath: materializedConductorPathEnv.CONDUCTOR_CONFIG,
198
+ workspaceRoot,
199
+ daemonName:
200
+ (typeof userConfig.daemon_name === "string" && userConfig.daemon_name.trim()) ||
201
+ process.env.CONDUCTOR_DAEMON_NAME ||
202
+ os.hostname(),
203
+ });
204
+ // Mirrors the ownership gate in src/daemon.js so --nohup --force fails in
205
+ // the foreground (where the user sees it) instead of inside a detached log.
206
+ const ownershipError = describeForceRestartRefusal({
207
+ lockState,
208
+ identity,
209
+ lockFile: path.join(workspaceRoot, DAEMON_LOCK_FILE_NAME),
210
+ });
211
+ if (ownershipError) {
212
+ process.stderr.write(`${CLI_NAME} ${ownershipError}\n`);
213
+ process.exit(1);
214
+ }
191
215
  }
192
216
 
193
217
  const logsDir = path.join(resolveConductorHome(), "logs");
@@ -6,6 +6,8 @@ import { hideBin } from "yargs/helpers";
6
6
 
7
7
  import { loadConfig } from "@love-moon/conductor-sdk";
8
8
 
9
+ import { envForExplicitConfigFile } from "../src/config-env.js";
10
+
9
11
  const CLI_NAME = process.env.CONDUCTOR_CLI_NAME || "conductor diagnose";
10
12
  const DEFAULT_TIMEOUT_MS = 8000;
11
13
 
@@ -45,7 +47,7 @@ main().catch((error) => {
45
47
  });
46
48
 
47
49
  async function main() {
48
- const config = loadConfig(args.configFile);
50
+ const config = loadConfig(args.configFile, { env: envForExplicitConfigFile(args.configFile) });
49
51
  const timeoutMs = normalizePositiveInt(args.timeoutMs, DEFAULT_TIMEOUT_MS);
50
52
  const baseUrl = String(config.backendUrl || "").replace(/\/+$/, "");
51
53
  const endpoint = `${baseUrl}/api/diagnostics/tasks/${encodeURIComponent(taskId)}`;
@@ -583,6 +583,17 @@ export function createPendingRemoteInterruptQueue() {
583
583
  async function main() {
584
584
  syncPwdEnvWithProcessCwdForDaemonLaunch();
585
585
  const cliArgs = await parseCliArgs();
586
+ if (cliArgs.configFile) {
587
+ // An explicit --config-file must win over CONDUCTOR_* backend/token env
588
+ // injected by an owning daemon or session (same convention as
589
+ // envForExplicitConfigFile); otherwise fire dials the file's websocket
590
+ // with the inherited token and loops on 4002 invalid-token.
591
+ delete process.env.CONDUCTOR_AGENT_TOKEN;
592
+ delete process.env.CONDUCTOR_BACKEND_URL;
593
+ delete process.env.CONDUCTOR_WS_URL;
594
+ delete process.env.CONDUCTOR_BACKEND_WS_URL;
595
+ process.env.CONDUCTOR_CONFIG = cliArgs.configFile;
596
+ }
586
597
  let runtimeProjectPath = process.cwd();
587
598
  let backendSession = null;
588
599
 
@@ -7,7 +7,7 @@
7
7
  * list [--include-hidden]
8
8
  * show [<id|name>]
9
9
  * current
10
- * create [--name <n>] [--workspace-path <p>] [--daemon-host <h>] [--default] [--client-request-id <key>]
10
+ * create [--name <n>] [--workspace-path <p>] [--daemon-host <h>] [--create-workspace] [--default] [--client-request-id <key>]
11
11
  * set-default <id|name>
12
12
  * hide <id|name>
13
13
  * unhide <id|name>
@@ -204,6 +204,7 @@ async function handleCreate(argv, deps) {
204
204
  ...(defaultName ? { name: defaultName } : {}),
205
205
  ...(workspacePath ? { workspacePath } : {}),
206
206
  ...(daemonHost ? { daemonHost } : {}),
207
+ ...(argv.createWorkspace ? { createWorkspaceIfMissing: true } : {}),
207
208
  metadata,
208
209
  ...(argv.clientRequestId ? { clientRequestId: String(argv.clientRequestId) } : {}),
209
210
  };
@@ -375,6 +376,7 @@ export async function main(argvInput = hideBin(process.argv), deps = {}) {
375
376
  .option("name", { type: "string", describe: "Project name (defaults to basename of workspace path)" })
376
377
  .option("workspace-path", { type: "string", describe: "Workspace path (defaults to cwd)" })
377
378
  .option("daemon-host", { type: "string", describe: "Daemon hostname for binding" })
379
+ .option("create-workspace", { type: "boolean", default: false, describe: "Create the workspace path on the daemon if it does not exist" })
378
380
  .option("default", { type: "boolean", default: false, describe: "Create the user's default project" })
379
381
  .option("client-request-id", { type: "string", describe: "Idempotency key" }),
380
382
  async (argv) => {
@@ -17,6 +17,7 @@ import {
17
17
  fetchLatestVersion,
18
18
  isNewerVersion,
19
19
  detectPackageManager,
20
+ resolveGlobalInstallPrefix,
20
21
  } from "../src/version-check.js";
21
22
  import {
22
23
  buildPnpmAllowBuildArgs,
@@ -163,6 +164,18 @@ async function performUpdate() {
163
164
  console.log(` Using package manager: ${colorize(packageManager, "cyan")}`);
164
165
  console.log("");
165
166
 
167
+ // `npm install -g` targets whichever npm wins the PATH lookup, which is not necessarily the
168
+ // one that installed us -- a Conductor-managed Node, an nvm shim and a system npm all resolve
169
+ // to different prefixes. When they disagree the update lands in a tree nobody runs, so
170
+ // `conductor --version` keeps reporting the old build and the next update repeats the cycle.
171
+ // Pin every child command to the prefix the running package actually lives in.
172
+ const installPrefix = packageManager === "npm" ? resolveGlobalInstallPrefix(PKG_ROOT) : null;
173
+ if (installPrefix) {
174
+ process.env.npm_config_prefix = installPrefix;
175
+ console.log(` Install prefix: ${colorize(installPrefix, "cyan")}`);
176
+ console.log("");
177
+ }
178
+
166
179
  if (packageManager === "pnpm") {
167
180
  console.log(" Preparing pnpm native dependency allowlist...");
168
181
  await ensurePnpmOnlyBuiltDependencies({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.9.0",
4
- "gitCommitId": "606e5a6",
3
+ "version": "0.11.0",
4
+ "gitCommitId": "7d57852",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/lovemoon-ai/conductor.git"
@@ -24,8 +24,8 @@
24
24
  "test": "node --test test/*.test.js"
25
25
  },
26
26
  "dependencies": {
27
- "@love-moon/ai-sdk": "0.9.0",
28
- "@love-moon/conductor-sdk": "0.9.0",
27
+ "@love-moon/ai-sdk": "0.11.0",
28
+ "@love-moon/conductor-sdk": "0.11.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.9.0"
41
+ "@love-moon/chat-web": "0.11.0"
42
42
  },
43
43
  "pnpm": {
44
44
  "onlyBuiltDependencies": [