@love-moon/conductor-cli 0.7.7 → 0.9.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 +36 -0
- package/bin/conductor-config.js +24 -0
- package/bin/conductor-fire.js +30 -2
- package/bin/conductor-remote-exec.js +365 -0
- package/bin/conductor-task.js +124 -0
- package/bin/conductor.js +8 -2
- package/package.json +5 -5
- package/src/ai-manager-handlers.js +5 -2
- package/src/daemon.js +296 -5
- package/src/entity-helpers.js +27 -2
- package/src/remote-exec-handlers.js +421 -0
- package/src/runtime-backends.js +6 -5
package/bin/conductor.js
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* serve-ai - Start an OpenAI-compatible local AI server
|
|
15
15
|
* project - Manage Conductor projects (list/show/create/...)
|
|
16
16
|
* issue - Manage issues (list/show/create/update/start/done)
|
|
17
|
-
* task - Manage tasks (list/show/send/messages/schedule)
|
|
17
|
+
* task - Manage tasks (create/list/show/send/messages/schedule)
|
|
18
|
+
* remote-exec - Run a command on another daemon's host
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -50,6 +51,7 @@ export function runConductorCli(args = argv, deps = {}) {
|
|
|
50
51
|
"project",
|
|
51
52
|
"issue",
|
|
52
53
|
"task",
|
|
54
|
+
"remote-exec",
|
|
53
55
|
];
|
|
54
56
|
|
|
55
57
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
@@ -134,7 +136,8 @@ Subcommands:
|
|
|
134
136
|
serve-ai Start an OpenAI-compatible local AI server
|
|
135
137
|
project Manage Conductor projects (list/show/create/...)
|
|
136
138
|
issue Manage issues (list/show/create/update/start/done)
|
|
137
|
-
task Manage tasks (list/show/send/messages/schedule)
|
|
139
|
+
task Manage tasks (create/list/show/send/messages/schedule)
|
|
140
|
+
remote-exec Run a command on another daemon's host
|
|
138
141
|
|
|
139
142
|
Options:
|
|
140
143
|
-h, --help Show this help message
|
|
@@ -156,8 +159,10 @@ Examples:
|
|
|
156
159
|
conductor update
|
|
157
160
|
conductor project list
|
|
158
161
|
conductor issue create --title "Refactor module" --priority P2
|
|
162
|
+
conductor task create --title "Refactor module" --prompt "Extract the parser" --backend codex
|
|
159
163
|
conductor task send <task-id> "please add a unit test"
|
|
160
164
|
conductor task schedule create <task-id> "follow up" --delay 10m
|
|
165
|
+
conductor remote-exec --target ubuntu --workspace /home/duino/ws/holomotion ls .
|
|
161
166
|
|
|
162
167
|
For subcommand-specific help:
|
|
163
168
|
conductor fire --help
|
|
@@ -171,6 +176,7 @@ For subcommand-specific help:
|
|
|
171
176
|
conductor project --help
|
|
172
177
|
conductor issue --help
|
|
173
178
|
conductor task --help
|
|
179
|
+
conductor remote-exec --help
|
|
174
180
|
|
|
175
181
|
Version: ${pkgJson.version}
|
|
176
182
|
`);
|
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.9.0",
|
|
4
|
+
"gitCommitId": "606e5a6",
|
|
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.9.0",
|
|
28
|
+
"@love-moon/conductor-sdk": "0.9.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.9.0"
|
|
42
42
|
},
|
|
43
43
|
"pnpm": {
|
|
44
44
|
"onlyBuiltDependencies": [
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { AiManager } from "@love-moon/ai-sdk";
|
|
6
6
|
|
|
7
7
|
const VALID_ACTIONS = new Set(["status", "quota", "list_accounts", "switch_account"]);
|
|
8
|
-
const BASE_QUOTA_TOOLS = ["codex", "claude", "kimi", "copilot"];
|
|
8
|
+
const BASE_QUOTA_TOOLS = ["codex", "claude", "kimi", "copilot", "dsh"];
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* @param {object} opts
|
|
@@ -23,7 +23,7 @@ export function createAiManagerHandlers(opts = {}) {
|
|
|
23
23
|
manager.getCurrentCodexAccount().catch(() => null),
|
|
24
24
|
]);
|
|
25
25
|
const network = {};
|
|
26
|
-
const tools = [
|
|
26
|
+
const tools = [...BASE_QUOTA_TOOLS];
|
|
27
27
|
await Promise.all(
|
|
28
28
|
tools.map(async (tool) => {
|
|
29
29
|
if (install[tool]?.installed) {
|
|
@@ -70,6 +70,9 @@ export function createAiManagerHandlers(opts = {}) {
|
|
|
70
70
|
addJob("copilot", () => manager.getCopilotQuota({
|
|
71
71
|
forceRefresh,
|
|
72
72
|
}));
|
|
73
|
+
addJob("dsh", () => manager.getDshQuota({
|
|
74
|
+
forceRefresh,
|
|
75
|
+
}));
|
|
73
76
|
const externalBackends = pickExternalQuotaBackends(args);
|
|
74
77
|
for (const backend of externalBackends) {
|
|
75
78
|
if (!tools.has(backend)) {
|
package/src/daemon.js
CHANGED
|
@@ -30,6 +30,11 @@ import {
|
|
|
30
30
|
createCustomCommandHandlers,
|
|
31
31
|
handleCustomCommandsRequest,
|
|
32
32
|
} from "./custom-command-handlers.js";
|
|
33
|
+
import {
|
|
34
|
+
REMOTE_EXEC_CAPABILITY,
|
|
35
|
+
createRemoteExecHandlers,
|
|
36
|
+
handleRemoteExecRequest,
|
|
37
|
+
} from "./remote-exec-handlers.js";
|
|
33
38
|
import { resolveResumeContext } from "./fire/resume.js";
|
|
34
39
|
import {
|
|
35
40
|
filterRuntimeSupportedAllowCliList,
|
|
@@ -213,6 +218,32 @@ function getFireTmuxModeEnabled(userConfig) {
|
|
|
213
218
|
return false;
|
|
214
219
|
}
|
|
215
220
|
|
|
221
|
+
// Whether this host will accept `remote-exec`. Unlike `pty_task` (gated on a
|
|
222
|
+
// node-pty probe) and `custom_commands` (opt-in per script), remote exec would
|
|
223
|
+
// otherwise be unconditional, leaving a shared CI box or a root-owned daemon no
|
|
224
|
+
// way to decline. Defaults to enabled to match the other daemon capabilities.
|
|
225
|
+
//
|
|
226
|
+
// Resolution order:
|
|
227
|
+
// 1. CONDUCTOR_REMOTE_EXEC env var ("1"/"true"/"on" enable, "0"/"false"/"off" disable)
|
|
228
|
+
// 2. remote_exec boolean in the resolved Conductor config.yaml
|
|
229
|
+
// 3. Default: true
|
|
230
|
+
function getRemoteExecEnabled(userConfig) {
|
|
231
|
+
const rawEnv = process.env.CONDUCTOR_REMOTE_EXEC;
|
|
232
|
+
if (typeof rawEnv === "string" && rawEnv.trim()) {
|
|
233
|
+
const normalized = rawEnv.trim().toLowerCase();
|
|
234
|
+
if (normalized === "1" || normalized === "true" || normalized === "on" || normalized === "yes") {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
if (normalized === "0" || normalized === "false" || normalized === "off" || normalized === "no") {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (userConfig && typeof userConfig === "object" && userConfig.remote_exec === false) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
216
247
|
function normalizePlanLimitType(limitType) {
|
|
217
248
|
if (typeof limitType !== "string") {
|
|
218
249
|
return null;
|
|
@@ -799,6 +830,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
799
830
|
// warning and silently fall back to direct spawn rather than failing every
|
|
800
831
|
// create_task with ENOENT.
|
|
801
832
|
const FIRE_TMUX_MODE_ENABLED = getFireTmuxModeEnabled(userConfig);
|
|
833
|
+
const remoteExecEnabled = getRemoteExecEnabled(userConfig);
|
|
802
834
|
|
|
803
835
|
// Get allow_cli_list from config
|
|
804
836
|
const RAW_ALLOW_CLI_LIST = getRawAllowCliList(userConfig);
|
|
@@ -1843,6 +1875,14 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1843
1875
|
"# (svg/png/jpg/gif/webp/ico/avif) relative to this .conductor/ directory.",
|
|
1844
1876
|
"# Remove this line to use the default folder icon.",
|
|
1845
1877
|
"",
|
|
1878
|
+
"# Optional agent registry used by the task-composer worker/reviewer picker.",
|
|
1879
|
+
"# Agent docs are workspace-relative paths; backend is optional.",
|
|
1880
|
+
"# agents:",
|
|
1881
|
+
"# feature-dev:",
|
|
1882
|
+
"# doc: claw/agents/feature-dev.md",
|
|
1883
|
+
"# description: Implements features end to end.",
|
|
1884
|
+
"# backend: codex",
|
|
1885
|
+
"",
|
|
1846
1886
|
"worktree:",
|
|
1847
1887
|
" sync_branch: false",
|
|
1848
1888
|
" sync_submodules: true",
|
|
@@ -1856,6 +1896,13 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1856
1896
|
].join("\n");
|
|
1857
1897
|
|
|
1858
1898
|
const MAX_PROJECT_ICON_IMAGE_BYTES = 128 * 1024;
|
|
1899
|
+
const MAX_PROJECT_AGENT_ENTRIES = 64;
|
|
1900
|
+
const MAX_PROJECT_AGENT_NAME_LENGTH = 64;
|
|
1901
|
+
const MAX_PROJECT_AGENT_DOC_LENGTH = 512;
|
|
1902
|
+
const MAX_PROJECT_AGENT_DESCRIPTION_LENGTH = 512;
|
|
1903
|
+
const MAX_PROJECT_AGENT_BACKEND_LENGTH = 64;
|
|
1904
|
+
const PROJECT_AGENT_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
1905
|
+
const PROJECT_AGENT_BACKEND_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
1859
1906
|
const PROJECT_ICON_MIME_BY_EXTENSION = {
|
|
1860
1907
|
".svg": "image/svg+xml",
|
|
1861
1908
|
".png": "image/png",
|
|
@@ -1954,6 +2001,118 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1954
2001
|
return null;
|
|
1955
2002
|
}
|
|
1956
2003
|
|
|
2004
|
+
function normalizeProjectAgentSetting(name, value) {
|
|
2005
|
+
const normalizedName = normalizeOptionalString(name);
|
|
2006
|
+
if (
|
|
2007
|
+
!normalizedName ||
|
|
2008
|
+
normalizedName.length > MAX_PROJECT_AGENT_NAME_LENGTH ||
|
|
2009
|
+
!PROJECT_AGENT_NAME_RE.test(normalizedName)
|
|
2010
|
+
) {
|
|
2011
|
+
return null;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
let docValue = null;
|
|
2015
|
+
let descriptionValue = null;
|
|
2016
|
+
let backendValue = null;
|
|
2017
|
+
if (typeof value === "string") {
|
|
2018
|
+
docValue = value;
|
|
2019
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
2020
|
+
docValue = value.doc ?? value.path;
|
|
2021
|
+
descriptionValue = value.description;
|
|
2022
|
+
backendValue = value.backend;
|
|
2023
|
+
} else {
|
|
2024
|
+
return null;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
const doc = normalizeOptionalString(docValue);
|
|
2028
|
+
if (
|
|
2029
|
+
!doc ||
|
|
2030
|
+
doc.length > MAX_PROJECT_AGENT_DOC_LENGTH ||
|
|
2031
|
+
path.isAbsolute(doc) ||
|
|
2032
|
+
/^[A-Za-z]:[\\/]/.test(doc) ||
|
|
2033
|
+
/^[/\\]{2}/.test(doc)
|
|
2034
|
+
) {
|
|
2035
|
+
return null;
|
|
2036
|
+
}
|
|
2037
|
+
const normalizedDoc = path.posix.normalize(doc.split("\\").join("/"));
|
|
2038
|
+
if (normalizedDoc === ".." || normalizedDoc.startsWith("../")) {
|
|
2039
|
+
return null;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
const description = normalizeOptionalString(descriptionValue);
|
|
2043
|
+
const backend = normalizeOptionalString(backendValue)?.toLowerCase() || null;
|
|
2044
|
+
return {
|
|
2045
|
+
name: normalizedName,
|
|
2046
|
+
doc,
|
|
2047
|
+
description:
|
|
2048
|
+
description && description.length <= MAX_PROJECT_AGENT_DESCRIPTION_LENGTH
|
|
2049
|
+
? description
|
|
2050
|
+
: null,
|
|
2051
|
+
backend:
|
|
2052
|
+
backend &&
|
|
2053
|
+
backend.length <= MAX_PROJECT_AGENT_BACKEND_LENGTH &&
|
|
2054
|
+
PROJECT_AGENT_BACKEND_RE.test(backend)
|
|
2055
|
+
? backend
|
|
2056
|
+
: null,
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function normalizeProjectAgentsSetting(agentsNode) {
|
|
2061
|
+
if (!agentsNode || typeof agentsNode !== "object") {
|
|
2062
|
+
return [];
|
|
2063
|
+
}
|
|
2064
|
+
const result = [];
|
|
2065
|
+
const seen = new Set();
|
|
2066
|
+
const push = (entry) => {
|
|
2067
|
+
if (
|
|
2068
|
+
entry &&
|
|
2069
|
+
result.length < MAX_PROJECT_AGENT_ENTRIES &&
|
|
2070
|
+
!seen.has(entry.name)
|
|
2071
|
+
) {
|
|
2072
|
+
seen.add(entry.name);
|
|
2073
|
+
result.push(entry);
|
|
2074
|
+
}
|
|
2075
|
+
};
|
|
2076
|
+
|
|
2077
|
+
if (Array.isArray(agentsNode)) {
|
|
2078
|
+
for (const item of agentsNode) {
|
|
2079
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
2080
|
+
continue;
|
|
2081
|
+
}
|
|
2082
|
+
const keys = Object.keys(item);
|
|
2083
|
+
if (keys.length === 1) {
|
|
2084
|
+
push(normalizeProjectAgentSetting(keys[0], item[keys[0]]));
|
|
2085
|
+
} else if (Object.prototype.hasOwnProperty.call(item, "name")) {
|
|
2086
|
+
push(normalizeProjectAgentSetting(item.name, item));
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
return result;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
for (const [name, value] of Object.entries(agentsNode)) {
|
|
2093
|
+
push(normalizeProjectAgentSetting(name, value));
|
|
2094
|
+
}
|
|
2095
|
+
return result;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
function readProjectAgentsSetting(projectWorkspacePath) {
|
|
2099
|
+
for (const settingsPath of getProjectSettingsCandidates(projectWorkspacePath)) {
|
|
2100
|
+
if (!existsSyncFn(settingsPath)) {
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
try {
|
|
2104
|
+
const parsed = yaml.load(readFileSyncFn(settingsPath, "utf8"));
|
|
2105
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2106
|
+
return [];
|
|
2107
|
+
}
|
|
2108
|
+
return normalizeProjectAgentsSetting(parsed.agents);
|
|
2109
|
+
} catch {
|
|
2110
|
+
return [];
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
return [];
|
|
2114
|
+
}
|
|
2115
|
+
|
|
1957
2116
|
function readProjectWorktreeSettings(projectWorkspacePath) {
|
|
1958
2117
|
for (const settingsPath of getProjectSettingsCandidates(projectWorkspacePath)) {
|
|
1959
2118
|
if (!existsSyncFn(settingsPath)) {
|
|
@@ -2069,11 +2228,19 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
2069
2228
|
|
|
2070
2229
|
if (linkStat) {
|
|
2071
2230
|
if (!linkStat.isSymbolicLink()) {
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2231
|
+
// A real file/dir at the destination means something materialised
|
|
2232
|
+
// data inside the worktree (e.g. `pnpm install` replaced the
|
|
2233
|
+
// node_modules link with a real directory). It may hold real data,
|
|
2234
|
+
// so never clobber it — but throwing here made every task in this
|
|
2235
|
+
// worktree permanently un-restartable. Keep the local copy and skip
|
|
2236
|
+
// the link; remove the path manually (or drop the entry from
|
|
2237
|
+
// worktree.symlink) to restore sharing with the project workspace.
|
|
2238
|
+
logError(
|
|
2239
|
+
`[worktree] skipping symlink for ${configuredPath}: destination already exists and ` +
|
|
2240
|
+
`is not a symlink: ${linkPath}. Keeping the local copy — remove it manually to ` +
|
|
2241
|
+
`restore sharing via worktree.symlink in .conductor/settings.yaml.`,
|
|
2076
2242
|
);
|
|
2243
|
+
continue;
|
|
2077
2244
|
}
|
|
2078
2245
|
// Compare the link's TARGET, not whether that target resolves. A link
|
|
2079
2246
|
// that already points at the right place is correct even when the
|
|
@@ -2804,10 +2971,17 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
2804
2971
|
};
|
|
2805
2972
|
const advertisedCapabilities = [
|
|
2806
2973
|
"project_path_validation",
|
|
2974
|
+
"project_agents_registry",
|
|
2807
2975
|
"restart_daemon",
|
|
2808
2976
|
"refresh_session_inplace",
|
|
2977
|
+
"task_attachments_v1",
|
|
2809
2978
|
CUSTOM_COMMANDS_CAPABILITY,
|
|
2810
2979
|
];
|
|
2980
|
+
if (remoteExecEnabled) {
|
|
2981
|
+
advertisedCapabilities.push(REMOTE_EXEC_CAPABILITY);
|
|
2982
|
+
} else {
|
|
2983
|
+
log("[remote-exec] Disabled by config (remote_exec: false); capability not advertised");
|
|
2984
|
+
}
|
|
2811
2985
|
if (ptyTaskCapabilityEnabled) {
|
|
2812
2986
|
advertisedCapabilities.push("pty_task", "terminal_snapshot");
|
|
2813
2987
|
}
|
|
@@ -2816,6 +2990,9 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
2816
2990
|
}
|
|
2817
2991
|
const aiManagerHandlers = createAiManagerHandlers({ configPath: effectiveConfigPath });
|
|
2818
2992
|
const customCommandHandlers = createCustomCommandHandlers({ configPath: effectiveConfigPath });
|
|
2993
|
+
const remoteExecHandlers = remoteExecEnabled
|
|
2994
|
+
? createRemoteExecHandlers({ defaultWorkspace: homeDir })
|
|
2995
|
+
: null;
|
|
2819
2996
|
|
|
2820
2997
|
const client = createWebSocketClient(sdkConfig, {
|
|
2821
2998
|
extraHeaders,
|
|
@@ -5027,6 +5204,11 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
5027
5204
|
}
|
|
5028
5205
|
if (event.type === "validate_project_path") {
|
|
5029
5206
|
void handleValidateProjectPath(event.payload);
|
|
5207
|
+
return;
|
|
5208
|
+
}
|
|
5209
|
+
if (event.type === "get_project_agents") {
|
|
5210
|
+
void handleGetProjectAgents(event.payload);
|
|
5211
|
+
return;
|
|
5030
5212
|
}
|
|
5031
5213
|
if (event.type === "ai_manager_request") {
|
|
5032
5214
|
handleAiManagerRequest(client, aiManagerHandlers, event.payload).catch((error) => {
|
|
@@ -5038,6 +5220,40 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
5038
5220
|
logError(`Unhandled custom_commands_request failure: ${error?.message || error}`);
|
|
5039
5221
|
});
|
|
5040
5222
|
}
|
|
5223
|
+
if (event.type === "remote_exec_request") {
|
|
5224
|
+
// Remote exec is the only execution path that leaves no Task row and no
|
|
5225
|
+
// per-run log file, so record it here — otherwise a run is invisible
|
|
5226
|
+
// once the daemon restarts. argv is deliberately omitted: it routinely
|
|
5227
|
+
// carries secrets and this log is collected by `collect_logs`.
|
|
5228
|
+
const execArgs = event?.payload?.args && typeof event.payload.args === "object" ? event.payload.args : {};
|
|
5229
|
+
log(
|
|
5230
|
+
`[remote-exec] ${event?.payload?.action || "?"} command=${execArgs.command || ""} ` +
|
|
5231
|
+
`argc=${Array.isArray(execArgs.args) ? execArgs.args.length : 0} cwd=${execArgs.workspace || "<default>"}`,
|
|
5232
|
+
);
|
|
5233
|
+
// Fail fast rather than letting the caller wait out its full timeout —
|
|
5234
|
+
// it has no other way to learn that this daemon will never answer.
|
|
5235
|
+
const rejectReason = !remoteExecHandlers
|
|
5236
|
+
? "remote exec is disabled on this daemon (remote_exec: false)"
|
|
5237
|
+
: daemonShuttingDown
|
|
5238
|
+
? "daemon is shutting down"
|
|
5239
|
+
: "";
|
|
5240
|
+
if (rejectReason) {
|
|
5241
|
+
void client
|
|
5242
|
+
.sendJson({
|
|
5243
|
+
type: "remote_exec_response",
|
|
5244
|
+
payload: {
|
|
5245
|
+
request_id: event?.payload?.request_id ? String(event.payload.request_id) : "",
|
|
5246
|
+
action: event?.payload?.action ? String(event.payload.action) : "",
|
|
5247
|
+
error: rejectReason,
|
|
5248
|
+
},
|
|
5249
|
+
})
|
|
5250
|
+
.catch(() => {});
|
|
5251
|
+
return;
|
|
5252
|
+
}
|
|
5253
|
+
handleRemoteExecRequest(client, remoteExecHandlers, event.payload).catch((error) => {
|
|
5254
|
+
logError(`Unhandled remote_exec_request failure: ${error?.message || error}`);
|
|
5255
|
+
});
|
|
5256
|
+
}
|
|
5041
5257
|
if (event.type === "restart_daemon") {
|
|
5042
5258
|
void handleRestartDaemon(event.payload).catch((error) => {
|
|
5043
5259
|
logError(`Unhandled restart_daemon failure: ${error?.message || error}`);
|
|
@@ -5132,6 +5348,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
5132
5348
|
lastCommitAt: null,
|
|
5133
5349
|
fileCount: null,
|
|
5134
5350
|
icon: null,
|
|
5351
|
+
agents: [],
|
|
5135
5352
|
error: null,
|
|
5136
5353
|
errorCode: null,
|
|
5137
5354
|
validatedAt,
|
|
@@ -5186,6 +5403,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
5186
5403
|
? snapshot.fileCount
|
|
5187
5404
|
: null,
|
|
5188
5405
|
icon: readProjectIconSetting(effectiveWorkspace),
|
|
5406
|
+
agents: readProjectAgentsSetting(effectiveWorkspace),
|
|
5189
5407
|
};
|
|
5190
5408
|
}
|
|
5191
5409
|
} catch (error) {
|
|
@@ -5210,6 +5428,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
5210
5428
|
git_remote_url: result.gitRemoteUrl,
|
|
5211
5429
|
file_count: result.fileCount,
|
|
5212
5430
|
icon: result.icon,
|
|
5431
|
+
agents: result.agents,
|
|
5213
5432
|
error: result.error,
|
|
5214
5433
|
error_code: result.errorCode,
|
|
5215
5434
|
validated_at: result.validatedAt,
|
|
@@ -5220,6 +5439,69 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
5220
5439
|
}
|
|
5221
5440
|
}
|
|
5222
5441
|
|
|
5442
|
+
async function handleGetProjectAgents(payload) {
|
|
5443
|
+
const requestId = payload?.request_id ? String(payload.request_id).trim() : "";
|
|
5444
|
+
const rawWorkspacePath = payload?.workspace_path ? String(payload.workspace_path).trim() : "";
|
|
5445
|
+
const resolvedAt = new Date().toISOString();
|
|
5446
|
+
|
|
5447
|
+
if (!requestId || !rawWorkspacePath) {
|
|
5448
|
+
logError(`Invalid get_project_agents payload: ${JSON.stringify(payload)}`);
|
|
5449
|
+
return;
|
|
5450
|
+
}
|
|
5451
|
+
|
|
5452
|
+
let result = {
|
|
5453
|
+
workspacePath: null,
|
|
5454
|
+
agents: [],
|
|
5455
|
+
error: null,
|
|
5456
|
+
errorCode: null,
|
|
5457
|
+
};
|
|
5458
|
+
try {
|
|
5459
|
+
const resolvedPath = path.resolve(rawWorkspacePath);
|
|
5460
|
+
if (!existsSyncFn(resolvedPath)) {
|
|
5461
|
+
result = {
|
|
5462
|
+
...result,
|
|
5463
|
+
error: `Workspace path does not exist on daemon ${AGENT_NAME}: ${rawWorkspacePath}`,
|
|
5464
|
+
errorCode: "workspace_not_found",
|
|
5465
|
+
};
|
|
5466
|
+
} else if (!statSyncFn(resolvedPath).isDirectory()) {
|
|
5467
|
+
result = {
|
|
5468
|
+
...result,
|
|
5469
|
+
error: `Workspace path is not a directory on daemon ${AGENT_NAME}: ${rawWorkspacePath}`,
|
|
5470
|
+
errorCode: "workspace_not_directory",
|
|
5471
|
+
};
|
|
5472
|
+
} else {
|
|
5473
|
+
result = {
|
|
5474
|
+
...result,
|
|
5475
|
+
workspacePath: resolvedPath,
|
|
5476
|
+
agents: readProjectAgentsSetting(resolvedPath),
|
|
5477
|
+
};
|
|
5478
|
+
}
|
|
5479
|
+
} catch (error) {
|
|
5480
|
+
result = {
|
|
5481
|
+
...result,
|
|
5482
|
+
error: `Failed to read project agents on daemon ${AGENT_NAME}: ${error?.message || error}`,
|
|
5483
|
+
errorCode: "project_agents_read_failed",
|
|
5484
|
+
};
|
|
5485
|
+
}
|
|
5486
|
+
|
|
5487
|
+
try {
|
|
5488
|
+
await client.sendJson({
|
|
5489
|
+
type: "project_agents_resolved",
|
|
5490
|
+
payload: {
|
|
5491
|
+
request_id: requestId,
|
|
5492
|
+
daemon_host: AGENT_NAME,
|
|
5493
|
+
workspace_path: result.workspacePath,
|
|
5494
|
+
agents: result.agents,
|
|
5495
|
+
error: result.error,
|
|
5496
|
+
error_code: result.errorCode,
|
|
5497
|
+
resolved_at: resolvedAt,
|
|
5498
|
+
},
|
|
5499
|
+
});
|
|
5500
|
+
} catch (error) {
|
|
5501
|
+
logError(`Failed to report project_agents_resolved for ${rawWorkspacePath}: ${error?.message || error}`);
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
|
|
5223
5505
|
function stopActiveTaskProcess(
|
|
5224
5506
|
taskId,
|
|
5225
5507
|
{
|
|
@@ -6019,6 +6301,10 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
6019
6301
|
PWD: taskDir,
|
|
6020
6302
|
CONDUCTOR_PROJECT_ID: projectId,
|
|
6021
6303
|
CONDUCTOR_TASK_ID: taskId,
|
|
6304
|
+
// Fire derives its own stable host identity from the owning daemon plus
|
|
6305
|
+
// the task. Passing the resolved name keeps that identity meaningful
|
|
6306
|
+
// even when the daemon was named through the config file.
|
|
6307
|
+
CONDUCTOR_DAEMON_NAME: AGENT_NAME,
|
|
6022
6308
|
CONDUCTOR_LAUNCHED_BY_DAEMON: "1",
|
|
6023
6309
|
...(cliCommand ? { CONDUCTOR_CLI_COMMAND: cliCommand } : {}),
|
|
6024
6310
|
};
|
|
@@ -6533,7 +6819,11 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
6533
6819
|
resolvedResumeCwd = await resolveRestartCwd({
|
|
6534
6820
|
taskId: normalizedTargetTaskId,
|
|
6535
6821
|
projectId: normalizedProjectId,
|
|
6536
|
-
|
|
6822
|
+
// A fork starts a fresh target-backend session, but its workspace
|
|
6823
|
+
// still belongs to the source task. Resolve the source session in
|
|
6824
|
+
// its own provider namespace; target + source session id is not a
|
|
6825
|
+
// meaningful pair for cross-backend handoff.
|
|
6826
|
+
backendType: sourceBackendType,
|
|
6537
6827
|
launchConfig: targetLaunchConfig,
|
|
6538
6828
|
sessionId: normalizedSourceSessionId,
|
|
6539
6829
|
sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
|
|
@@ -6721,6 +7011,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
6721
7011
|
PWD: taskDir,
|
|
6722
7012
|
CONDUCTOR_PROJECT_ID: normalizedProjectId,
|
|
6723
7013
|
CONDUCTOR_TASK_ID: normalizedTargetTaskId,
|
|
7014
|
+
CONDUCTOR_DAEMON_NAME: AGENT_NAME,
|
|
6724
7015
|
CONDUCTOR_LAUNCHED_BY_DAEMON: "1",
|
|
6725
7016
|
...(cliCommand ? { CONDUCTOR_CLI_COMMAND: cliCommand } : {}),
|
|
6726
7017
|
};
|
package/src/entity-helpers.js
CHANGED
|
@@ -192,8 +192,14 @@ export async function resolveProject(apis, options = {}) {
|
|
|
192
192
|
if (!apis.projects || typeof apis.projects.resolveProject !== "function") {
|
|
193
193
|
throw new Error("ProjectsApi.resolveProject is not available in conductor-sdk");
|
|
194
194
|
}
|
|
195
|
+
const explicitProject = String(options.project ?? "").trim();
|
|
196
|
+
if (explicitProject) {
|
|
197
|
+
if (typeof apis.projects.getProject !== "function") {
|
|
198
|
+
throw new Error("ProjectsApi.getProject is not available in conductor-sdk");
|
|
199
|
+
}
|
|
200
|
+
return apis.projects.getProject(explicitProject);
|
|
201
|
+
}
|
|
195
202
|
return apis.projects.resolveProject({
|
|
196
|
-
project: options.project,
|
|
197
203
|
env: options.env || process.env,
|
|
198
204
|
cwd: options.cwd || process.cwd(),
|
|
199
205
|
});
|
|
@@ -332,11 +338,30 @@ export function emitDryRun(stream, json, payload) {
|
|
|
332
338
|
}
|
|
333
339
|
}
|
|
334
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Pull a human-readable reason out of a backend error payload. Backend routes
|
|
343
|
+
* respond with `{ error }` (occasionally `{ message }`), which the SDK attaches
|
|
344
|
+
* to BackendApiError.details. Surfacing it turns an opaque "Backend responded
|
|
345
|
+
* with 409" into the actual cause (e.g. "Project daemon X is offline").
|
|
346
|
+
*/
|
|
347
|
+
function backendErrorDetail(error) {
|
|
348
|
+
const details = error && typeof error === "object" ? error.details : null;
|
|
349
|
+
if (!details) return null;
|
|
350
|
+
if (typeof details === "string") return details.trim() || null;
|
|
351
|
+
if (typeof details === "object") {
|
|
352
|
+
const reason = details.error ?? details.message;
|
|
353
|
+
if (typeof reason === "string" && reason.trim()) return reason.trim();
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
335
358
|
/**
|
|
336
359
|
* Translate an unknown error into a printable + exit-coded form.
|
|
337
360
|
*/
|
|
338
361
|
export function reportError(consoleErr, error) {
|
|
339
362
|
const message = error instanceof Error ? error.message : String(error);
|
|
340
|
-
|
|
363
|
+
const detail = backendErrorDetail(error);
|
|
364
|
+
const line = detail && detail !== message ? `${message}: ${detail}` : message;
|
|
365
|
+
consoleErr.error(`Error: ${line}`);
|
|
341
366
|
return exitCodeForError(error);
|
|
342
367
|
}
|