@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 +129 -0
- package/bin/conductor-config.js +133 -8
- package/bin/conductor-daemon.js +53 -29
- package/bin/conductor-diagnose.js +3 -1
- package/bin/conductor-fire.js +11 -0
- package/bin/conductor-project.js +3 -1
- package/bin/conductor-update.js +13 -0
- package/package.json +5 -5
- package/src/daemon-lock.js +240 -0
- package/src/daemon.js +717 -123
- package/src/guest-daemon.js +268 -0
- package/src/runtime-backends.js +63 -2
- package/src/version-check.js +23 -0
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
|
package/bin/conductor-config.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
"#
|
|
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
|
|
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
|
-
"#
|
|
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`);
|
package/bin/conductor-daemon.js
CHANGED
|
@@ -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(
|
|
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
|
|
120
|
-
const lockFile = path.join(workspaceRoot,
|
|
121
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
)
|
|
190
|
-
|
|
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)}`;
|
package/bin/conductor-fire.js
CHANGED
|
@@ -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
|
|
package/bin/conductor-project.js
CHANGED
|
@@ -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) => {
|
package/bin/conductor-update.js
CHANGED
|
@@ -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.
|
|
4
|
-
"gitCommitId": "
|
|
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.
|
|
28
|
-
"@love-moon/conductor-sdk": "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.
|
|
41
|
+
"@love-moon/chat-web": "0.11.0"
|
|
42
42
|
},
|
|
43
43
|
"pnpm": {
|
|
44
44
|
"onlyBuiltDependencies": [
|