@love-moon/conductor-cli 0.2.25 → 0.2.27
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/bin/conductor-config.js +207 -38
- package/bin/conductor-diagnose.js +11 -1
- package/bin/conductor-fire.js +27 -10
- package/package.json +4 -4
- package/src/daemon.js +48 -1
- package/src/fire/resume.js +221 -3
- package/src/runtime-backends.js +1 -1
package/bin/conductor-config.js
CHANGED
|
@@ -5,7 +5,7 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import process from "node:process";
|
|
7
7
|
import readline from "node:readline/promises";
|
|
8
|
-
import { execSync } from "node:child_process";
|
|
8
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
9
9
|
import yargs from "yargs/yargs";
|
|
10
10
|
import { hideBin } from "yargs/helpers";
|
|
11
11
|
import { RUNTIME_SUPPORTED_BACKENDS } from "../src/runtime-backends.js";
|
|
@@ -27,6 +27,11 @@ const DEFAULT_CLIs = {
|
|
|
27
27
|
execArgs: "--dangerously-bypass-approvals-and-sandbox --ask-for-approval never",
|
|
28
28
|
description: "OpenAI Codex CLI"
|
|
29
29
|
},
|
|
30
|
+
kimi: {
|
|
31
|
+
command: "kimi",
|
|
32
|
+
execArgs: "",
|
|
33
|
+
description: "Moonshot Kimi CLI"
|
|
34
|
+
},
|
|
30
35
|
opencode: {
|
|
31
36
|
command: "opencode",
|
|
32
37
|
execArgs: "",
|
|
@@ -40,6 +45,8 @@ const backendUrl =
|
|
|
40
45
|
"https://conductor-ai.top";
|
|
41
46
|
const defaultDaemonName = os.hostname() || "my-daemon";
|
|
42
47
|
const cliVersion = packageJson.version || "unknown";
|
|
48
|
+
const OPENCODE_INSTALL_URL = "https://opencode.ai/install";
|
|
49
|
+
const OPENCODE_NPM_PACKAGE = "opencode-ai";
|
|
43
50
|
|
|
44
51
|
const COLORS = {
|
|
45
52
|
yellow: "\x1b[33m",
|
|
@@ -50,6 +57,7 @@ const COLORS = {
|
|
|
50
57
|
};
|
|
51
58
|
|
|
52
59
|
let lastDeviceAuthConfig = null;
|
|
60
|
+
let promptInterface = null;
|
|
53
61
|
|
|
54
62
|
function colorize(text, color) {
|
|
55
63
|
return `${COLORS[color] || ""}${text}${COLORS.reset}`;
|
|
@@ -71,6 +79,29 @@ function buildConfigEntryLines(cli, info, { commented = false } = {}) {
|
|
|
71
79
|
return lines;
|
|
72
80
|
}
|
|
73
81
|
|
|
82
|
+
function getPromptInterface() {
|
|
83
|
+
if (!promptInterface) {
|
|
84
|
+
promptInterface = readline.createInterface({
|
|
85
|
+
input: process.stdin,
|
|
86
|
+
output: process.stdout,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return promptInterface;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function closePromptInterface() {
|
|
93
|
+
if (!promptInterface) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
promptInterface.close();
|
|
97
|
+
promptInterface = null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function exitWithCode(code) {
|
|
101
|
+
closePromptInterface();
|
|
102
|
+
process.exit(code);
|
|
103
|
+
}
|
|
104
|
+
|
|
74
105
|
async function main() {
|
|
75
106
|
const argv = yargs(hideBin(process.argv))
|
|
76
107
|
.option("token", {
|
|
@@ -104,12 +135,12 @@ async function main() {
|
|
|
104
135
|
process.stderr.write(
|
|
105
136
|
colorize(`Config already exists at ${CONFIG_FILE}. Use --force to overwrite.\n`, "yellow")
|
|
106
137
|
);
|
|
107
|
-
|
|
138
|
+
exitWithCode(1);
|
|
108
139
|
}
|
|
109
140
|
|
|
110
141
|
let resolvedBackendUrl = backendUrl;
|
|
111
142
|
let resolvedWebsocketUrl = null;
|
|
112
|
-
|
|
143
|
+
let detectedCLIs = detectInstalledCLIs();
|
|
113
144
|
|
|
114
145
|
if (detectedCLIs.length === 0) {
|
|
115
146
|
console.log("");
|
|
@@ -127,16 +158,43 @@ async function main() {
|
|
|
127
158
|
});
|
|
128
159
|
|
|
129
160
|
console.log("");
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const shouldContinue = await promptYesNo(
|
|
135
|
-
"Do you want to continue creating the config anyway? (y/N): "
|
|
161
|
+
const shouldInstallOpencode = await promptYesNo(
|
|
162
|
+
"Do you want to install opencode now? (Y/n): ",
|
|
163
|
+
{ defaultValue: true }
|
|
136
164
|
);
|
|
137
165
|
|
|
138
|
-
if (
|
|
139
|
-
|
|
166
|
+
if (shouldInstallOpencode) {
|
|
167
|
+
const installResult = installOpencode();
|
|
168
|
+
if (installResult.installed) {
|
|
169
|
+
detectedCLIs = detectInstalledCLIs();
|
|
170
|
+
if (!detectedCLIs.includes("opencode")) {
|
|
171
|
+
detectedCLIs = ["opencode", ...detectedCLIs];
|
|
172
|
+
}
|
|
173
|
+
console.log("");
|
|
174
|
+
if (installResult.message) {
|
|
175
|
+
console.log(colorize(installResult.message, "green"));
|
|
176
|
+
}
|
|
177
|
+
console.log(colorize("✓ OpenCode installed. Continuing with conductor config.", "green"));
|
|
178
|
+
console.log("");
|
|
179
|
+
} else {
|
|
180
|
+
console.log("");
|
|
181
|
+
console.log(colorize(installResult.message, "yellow"));
|
|
182
|
+
console.log("");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (detectedCLIs.length === 0) {
|
|
187
|
+
console.log(colorize("After installing a CLI, run 'conductor config' again.", "yellow"));
|
|
188
|
+
console.log(colorize("=".repeat(70), "yellow"));
|
|
189
|
+
console.log("");
|
|
190
|
+
|
|
191
|
+
const shouldContinue = await promptYesNo(
|
|
192
|
+
"Do you want to continue creating the config anyway? (y/N): "
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (!shouldContinue) {
|
|
196
|
+
exitWithCode(1);
|
|
197
|
+
}
|
|
140
198
|
}
|
|
141
199
|
} else {
|
|
142
200
|
console.log("");
|
|
@@ -154,7 +212,7 @@ async function main() {
|
|
|
154
212
|
token = await promptForToken();
|
|
155
213
|
if (!token) {
|
|
156
214
|
process.stderr.write(colorize("No token provided. Aborting.\n", "yellow"));
|
|
157
|
-
|
|
215
|
+
exitWithCode(1);
|
|
158
216
|
}
|
|
159
217
|
} else {
|
|
160
218
|
const authResult = await authorizeDeviceAndGetToken();
|
|
@@ -227,6 +285,22 @@ function detectInstalledCLIs() {
|
|
|
227
285
|
return detected;
|
|
228
286
|
}
|
|
229
287
|
|
|
288
|
+
function resolveBundledCommandPath(command) {
|
|
289
|
+
const binDir = path.dirname(process.execPath);
|
|
290
|
+
const candidates = os.platform() === "win32"
|
|
291
|
+
? [`${command}.cmd`, `${command}.exe`, command]
|
|
292
|
+
: [command];
|
|
293
|
+
|
|
294
|
+
for (const candidate of candidates) {
|
|
295
|
+
const fullPath = path.join(binDir, candidate);
|
|
296
|
+
if (fs.existsSync(fullPath)) {
|
|
297
|
+
return fullPath;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
230
304
|
function isCommandAvailable(command) {
|
|
231
305
|
try {
|
|
232
306
|
const platform = os.platform();
|
|
@@ -268,6 +342,108 @@ function checkAlternativeInstallations(command) {
|
|
|
268
342
|
return commonPaths.some((checkPath) => fs.existsSync(checkPath));
|
|
269
343
|
}
|
|
270
344
|
|
|
345
|
+
function resolveInstallCommand(command) {
|
|
346
|
+
const bundledPath = resolveBundledCommandPath(command);
|
|
347
|
+
if (bundledPath) {
|
|
348
|
+
return bundledPath;
|
|
349
|
+
}
|
|
350
|
+
return isCommandAvailable(command) ? command : null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function resolveOpencodeInstaller() {
|
|
354
|
+
const overrideCommand = process.env.CONDUCTOR_OPENCODE_INSTALL_COMMAND?.trim();
|
|
355
|
+
if (overrideCommand) {
|
|
356
|
+
return {
|
|
357
|
+
command: overrideCommand,
|
|
358
|
+
args: [],
|
|
359
|
+
display: overrideCommand,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const npmCommand = resolveInstallCommand("npm");
|
|
364
|
+
if (npmCommand) {
|
|
365
|
+
return {
|
|
366
|
+
command: npmCommand,
|
|
367
|
+
args: ["install", "-g", OPENCODE_NPM_PACKAGE],
|
|
368
|
+
display: `${path.basename(npmCommand)} install -g ${OPENCODE_NPM_PACKAGE}`,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const pnpmCommand = resolveInstallCommand("pnpm");
|
|
373
|
+
if (pnpmCommand) {
|
|
374
|
+
return {
|
|
375
|
+
command: pnpmCommand,
|
|
376
|
+
args: ["install", "-g", OPENCODE_NPM_PACKAGE],
|
|
377
|
+
display: `${path.basename(pnpmCommand)} install -g ${OPENCODE_NPM_PACKAGE}`,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const bunCommand = resolveInstallCommand("bun");
|
|
382
|
+
if (bunCommand) {
|
|
383
|
+
return {
|
|
384
|
+
command: bunCommand,
|
|
385
|
+
args: ["install", "-g", OPENCODE_NPM_PACKAGE],
|
|
386
|
+
display: `${path.basename(bunCommand)} install -g ${OPENCODE_NPM_PACKAGE}`,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (os.platform() !== "win32" && isCommandAvailable("bash") && isCommandAvailable("curl")) {
|
|
391
|
+
return {
|
|
392
|
+
command: "bash",
|
|
393
|
+
args: ["-lc", `curl -fsSL ${OPENCODE_INSTALL_URL} | bash`],
|
|
394
|
+
display: `curl -fsSL ${OPENCODE_INSTALL_URL} | bash`,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function printOpencodeInstallInstructions() {
|
|
402
|
+
console.log(colorize("You can install OpenCode manually with one of these commands:", "yellow"));
|
|
403
|
+
console.log(` curl -fsSL ${OPENCODE_INSTALL_URL} | bash`);
|
|
404
|
+
console.log(` npm install -g ${OPENCODE_NPM_PACKAGE}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function installOpencode() {
|
|
408
|
+
const installer = resolveOpencodeInstaller();
|
|
409
|
+
if (!installer) {
|
|
410
|
+
printOpencodeInstallInstructions();
|
|
411
|
+
return {
|
|
412
|
+
installed: false,
|
|
413
|
+
message: "Could not find an automatic installer for opencode in the current environment.",
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
console.log("");
|
|
418
|
+
console.log(colorize(`Installing opencode with: ${installer.display}`, "cyan"));
|
|
419
|
+
console.log("");
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
execFileSync(installer.command, installer.args, {
|
|
423
|
+
stdio: "inherit",
|
|
424
|
+
env: process.env,
|
|
425
|
+
});
|
|
426
|
+
} catch (error) {
|
|
427
|
+
printOpencodeInstallInstructions();
|
|
428
|
+
return {
|
|
429
|
+
installed: false,
|
|
430
|
+
message: `opencode installation failed: ${error?.message || error}`,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (isCommandAvailable("opencode")) {
|
|
435
|
+
return {
|
|
436
|
+
installed: true,
|
|
437
|
+
message: "OpenCode installed successfully.",
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
installed: true,
|
|
443
|
+
message: "OpenCode installed, but the current shell may need a refreshed PATH before detection works.",
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
271
447
|
async function authorizeDeviceAndGetToken() {
|
|
272
448
|
const startResponse = await fetch(new URL("/api/auth/device/start", backendUrl), {
|
|
273
449
|
method: "POST",
|
|
@@ -350,32 +526,21 @@ async function parseJsonResponse(response) {
|
|
|
350
526
|
}
|
|
351
527
|
|
|
352
528
|
async function promptForToken() {
|
|
353
|
-
const rl =
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
try {
|
|
358
|
-
let token = "";
|
|
359
|
-
while (!token) {
|
|
360
|
-
token = (await rl.question("Enter Conductor token: ")).trim();
|
|
361
|
-
}
|
|
362
|
-
return token;
|
|
363
|
-
} finally {
|
|
364
|
-
rl.close();
|
|
529
|
+
const rl = getPromptInterface();
|
|
530
|
+
let token = "";
|
|
531
|
+
while (!token) {
|
|
532
|
+
token = (await rl.question("Enter Conductor token: ")).trim();
|
|
365
533
|
}
|
|
534
|
+
return token;
|
|
366
535
|
}
|
|
367
536
|
|
|
368
|
-
async function promptYesNo(question) {
|
|
369
|
-
const rl =
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
try {
|
|
374
|
-
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
375
|
-
return answer === "y" || answer === "yes";
|
|
376
|
-
} finally {
|
|
377
|
-
rl.close();
|
|
537
|
+
async function promptYesNo(question, { defaultValue = false } = {}) {
|
|
538
|
+
const rl = getPromptInterface();
|
|
539
|
+
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
540
|
+
if (!answer) {
|
|
541
|
+
return defaultValue;
|
|
378
542
|
}
|
|
543
|
+
return answer === "y" || answer === "yes";
|
|
379
544
|
}
|
|
380
545
|
|
|
381
546
|
function sleep(ms) {
|
|
@@ -388,7 +553,11 @@ function yamlQuote(value) {
|
|
|
388
553
|
return JSON.stringify(value);
|
|
389
554
|
}
|
|
390
555
|
|
|
391
|
-
main()
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
556
|
+
main()
|
|
557
|
+
.catch((error) => {
|
|
558
|
+
process.stderr.write(`Failed to write config: ${error?.message || error}\n`);
|
|
559
|
+
exitWithCode(1);
|
|
560
|
+
})
|
|
561
|
+
.finally(() => {
|
|
562
|
+
closePromptInterface();
|
|
563
|
+
});
|
|
@@ -101,7 +101,7 @@ async function runFallbackDiagnosis(baseUrl, token, taskId, timeoutMs) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
const task = taskResp.body || {};
|
|
104
|
-
const messages =
|
|
104
|
+
const messages = normalizeMessageHistoryPayload(msgResp.body);
|
|
105
105
|
const agents = Array.isArray(agentsResp.body) ? agentsResp.body : [];
|
|
106
106
|
|
|
107
107
|
const latestUser = messages
|
|
@@ -159,6 +159,16 @@ async function runFallbackDiagnosis(baseUrl, token, taskId, timeoutMs) {
|
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
function normalizeMessageHistoryPayload(payload) {
|
|
163
|
+
if (Array.isArray(payload)) {
|
|
164
|
+
return payload;
|
|
165
|
+
}
|
|
166
|
+
if (payload && typeof payload === "object" && Array.isArray(payload.messages)) {
|
|
167
|
+
return payload.messages;
|
|
168
|
+
}
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
|
|
162
172
|
function classifyFallback(input) {
|
|
163
173
|
if (input.taskStatus === "completed" || input.taskStatus === "killed") {
|
|
164
174
|
return {
|
package/bin/conductor-fire.js
CHANGED
|
@@ -68,19 +68,23 @@ function loadAllowCliList(configFilePath) {
|
|
|
68
68
|
|
|
69
69
|
export function resolveAiSessionCommandLine(backend, allowCliList, env = process.env) {
|
|
70
70
|
const normalizedBackend = normalizeRuntimeBackendName(backend);
|
|
71
|
-
|
|
71
|
+
const envKeyByBackend = {
|
|
72
|
+
opencode: "CONDUCTOR_OPENCODE_COMMAND",
|
|
73
|
+
kimi: "CONDUCTOR_KIMI_COMMAND",
|
|
74
|
+
};
|
|
75
|
+
const envKey = envKeyByBackend[normalizedBackend];
|
|
76
|
+
if (!envKey) {
|
|
72
77
|
return "";
|
|
73
78
|
}
|
|
74
79
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return opencodeEnvCommand;
|
|
80
|
+
const preferredEnvCommand = typeof env?.[envKey] === "string" ? env[envKey].trim() : "";
|
|
81
|
+
if (preferredEnvCommand) {
|
|
82
|
+
return preferredEnvCommand;
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
const configuredCommand =
|
|
82
|
-
allowCliList && typeof allowCliList === "object" && typeof allowCliList
|
|
83
|
-
? allowCliList.
|
|
86
|
+
allowCliList && typeof allowCliList === "object" && typeof allowCliList[normalizedBackend] === "string"
|
|
87
|
+
? allowCliList[normalizedBackend].trim()
|
|
84
88
|
: "";
|
|
85
89
|
if (configuredCommand) {
|
|
86
90
|
return configuredCommand;
|
|
@@ -410,7 +414,7 @@ async function main() {
|
|
|
410
414
|
|
|
411
415
|
if (cliArgs.listBackends) {
|
|
412
416
|
if (supportedBackends.length === 0) {
|
|
413
|
-
process.stdout.write(`No supported backends configured.\n\nAdd allow_cli_list to your config file (~/.conductor/config.yaml):\n allow_cli_list:\n codex: codex --dangerously-bypass-approvals-and-sandbox\n claude: claude --dangerously-skip-permissions\n opencode: opencode\n`);
|
|
417
|
+
process.stdout.write(`No supported backends configured.\n\nAdd allow_cli_list to your config file (~/.conductor/config.yaml):\n allow_cli_list:\n codex: codex --dangerously-bypass-approvals-and-sandbox\n claude: claude --dangerously-skip-permissions\n kimi: kimi\n opencode: opencode\n`);
|
|
414
418
|
} else {
|
|
415
419
|
process.stdout.write(`Supported backends (from config):\n`);
|
|
416
420
|
for (const [name, command] of Object.entries(allowCliList)) {
|
|
@@ -902,13 +906,16 @@ Config file format (~/.conductor/config.yaml):
|
|
|
902
906
|
allow_cli_list:
|
|
903
907
|
codex: codex --dangerously-bypass-approvals-and-sandbox
|
|
904
908
|
claude: claude --dangerously-skip-permissions
|
|
909
|
+
kimi: kimi
|
|
905
910
|
opencode: opencode
|
|
906
911
|
|
|
907
912
|
Examples:
|
|
908
913
|
${CLI_NAME} -- "fix the bug" # Use default backend
|
|
909
914
|
${CLI_NAME} --backend claude -- "fix the bug" # Use Claude CLI backend
|
|
915
|
+
${CLI_NAME} --backend kimi -- "fix the bug" # Use Kimi CLI backend
|
|
910
916
|
${CLI_NAME} --backend opencode -- "fix the bug" # Use OpenCode backend
|
|
911
917
|
${CLI_NAME} --backend codex --resume <id> # Resume Codex session
|
|
918
|
+
${CLI_NAME} --backend kimi --resume <id> # Resume Kimi session
|
|
912
919
|
${CLI_NAME} --list-backends # Show configured backends
|
|
913
920
|
${CLI_NAME} --config-file ~/.conductor/config.yaml -- "fix the bug"
|
|
914
921
|
|
|
@@ -944,7 +951,7 @@ Environment:
|
|
|
944
951
|
);
|
|
945
952
|
}
|
|
946
953
|
if (!backend && shouldRequireBackend) {
|
|
947
|
-
throw new Error("No supported backends configured. Add codex, claude, or opencode to allow_cli_list.");
|
|
954
|
+
throw new Error("No supported backends configured. Add codex, claude, kimi, or opencode to allow_cli_list.");
|
|
948
955
|
}
|
|
949
956
|
|
|
950
957
|
const prompt = (backendArgs._ || []).map((part) => String(part)).join(" ").trim();
|
|
@@ -1643,6 +1650,16 @@ export class BridgeRunner {
|
|
|
1643
1650
|
return role === "user" || role === "action";
|
|
1644
1651
|
}
|
|
1645
1652
|
|
|
1653
|
+
normalizeMessageHistoryPayload(payload) {
|
|
1654
|
+
if (Array.isArray(payload)) {
|
|
1655
|
+
return payload;
|
|
1656
|
+
}
|
|
1657
|
+
if (payload && typeof payload === "object" && Array.isArray(payload.messages)) {
|
|
1658
|
+
return payload.messages;
|
|
1659
|
+
}
|
|
1660
|
+
return [];
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1646
1663
|
async backfillPendingUserMessages() {
|
|
1647
1664
|
const backendUrl = process.env.CONDUCTOR_BACKEND_URL;
|
|
1648
1665
|
const token = process.env.CONDUCTOR_AGENT_TOKEN;
|
|
@@ -1666,7 +1683,7 @@ export class BridgeRunner {
|
|
|
1666
1683
|
this.copilotLog(`backfill request failed status=${response.status}`);
|
|
1667
1684
|
return;
|
|
1668
1685
|
}
|
|
1669
|
-
const history = await response.json();
|
|
1686
|
+
const history = this.normalizeMessageHistoryPayload(await response.json());
|
|
1670
1687
|
if (!Array.isArray(history) || history.length === 0) {
|
|
1671
1688
|
this.copilotLog("backfill: no history messages");
|
|
1672
1689
|
return;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/conductor-cli",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"gitCommitId": "
|
|
3
|
+
"version": "0.2.27",
|
|
4
|
+
"gitCommitId": "76b37c9",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"conductor": "bin/conductor.js"
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"test": "node --test test/*.test.js"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@love-moon/ai-sdk": "0.2.
|
|
21
|
-
"@love-moon/conductor-sdk": "0.2.
|
|
20
|
+
"@love-moon/ai-sdk": "0.2.27",
|
|
21
|
+
"@love-moon/conductor-sdk": "0.2.27",
|
|
22
22
|
"dotenv": "^16.4.5",
|
|
23
23
|
"enquirer": "^2.4.1",
|
|
24
24
|
"js-yaml": "^4.1.1",
|
package/src/daemon.js
CHANGED
|
@@ -49,6 +49,7 @@ const PLAN_LIMIT_MESSAGES = {
|
|
|
49
49
|
const DEFAULT_TERMINAL_COLS = 120;
|
|
50
50
|
const DEFAULT_TERMINAL_ROWS = 40;
|
|
51
51
|
const DEFAULT_TERMINAL_RING_BUFFER_MAX_BYTES = 2 * 1024 * 1024;
|
|
52
|
+
const DEFAULT_TERMINAL_RESUME_SNAPSHOT_MAX_BYTES = 128 * 1024;
|
|
52
53
|
const DEFAULT_RTC_MODULE_CANDIDATES = ["@roamhq/wrtc", "wrtc"];
|
|
53
54
|
let nodePtySpawnPromise = null;
|
|
54
55
|
|
|
@@ -282,6 +283,14 @@ function normalizeOptionalString(value) {
|
|
|
282
283
|
return normalized || null;
|
|
283
284
|
}
|
|
284
285
|
|
|
286
|
+
function normalizeTerminalResumeStrategy(value) {
|
|
287
|
+
const normalized = normalizeOptionalString(value);
|
|
288
|
+
if (!normalized) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
return normalized.toLowerCase() === "snapshot" ? "snapshot" : null;
|
|
292
|
+
}
|
|
293
|
+
|
|
285
294
|
function normalizeStringArray(value) {
|
|
286
295
|
if (!Array.isArray(value)) {
|
|
287
296
|
return [];
|
|
@@ -529,6 +538,10 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
529
538
|
config.TERMINAL_RING_BUFFER_MAX_BYTES || process.env.CONDUCTOR_TERMINAL_RING_BUFFER_MAX_BYTES,
|
|
530
539
|
DEFAULT_TERMINAL_RING_BUFFER_MAX_BYTES,
|
|
531
540
|
);
|
|
541
|
+
const TERMINAL_RESUME_SNAPSHOT_MAX_BYTES = parsePositiveInt(
|
|
542
|
+
config.TERMINAL_RESUME_SNAPSHOT_MAX_BYTES || process.env.CONDUCTOR_TERMINAL_RESUME_SNAPSHOT_MAX_BYTES,
|
|
543
|
+
DEFAULT_TERMINAL_RESUME_SNAPSHOT_MAX_BYTES,
|
|
544
|
+
);
|
|
532
545
|
|
|
533
546
|
const readLockState = () => {
|
|
534
547
|
const raw = String(readFileSyncFn(LOCK_FILE, "utf-8") || "").trim();
|
|
@@ -814,7 +827,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
814
827
|
"x-conductor-version": cliVersion,
|
|
815
828
|
};
|
|
816
829
|
if (ptyTaskCapabilityEnabled) {
|
|
817
|
-
extraHeaders["x-conductor-capabilities"] = "pty_task";
|
|
830
|
+
extraHeaders["x-conductor-capabilities"] = "pty_task,terminal_snapshot";
|
|
818
831
|
}
|
|
819
832
|
const client = createWebSocketClient(sdkConfig, {
|
|
820
833
|
extraHeaders,
|
|
@@ -1914,6 +1927,23 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1914
1927
|
return record.outputSeq;
|
|
1915
1928
|
}
|
|
1916
1929
|
|
|
1930
|
+
function buildTerminalResumeSnapshot(record) {
|
|
1931
|
+
if (!record || !Array.isArray(record.ringBuffer) || record.ringBuffer.length === 0) {
|
|
1932
|
+
return {
|
|
1933
|
+
lastSeq: normalizeNonNegativeInt(record?.outputSeq, 0),
|
|
1934
|
+
data: "",
|
|
1935
|
+
truncated: false,
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
const joinedData = record.ringBuffer.map((chunk) => chunk?.data || "").join("");
|
|
1939
|
+
const trimmedData = trimTerminalChunkToTailBytes(joinedData, TERMINAL_RESUME_SNAPSHOT_MAX_BYTES);
|
|
1940
|
+
return {
|
|
1941
|
+
lastSeq: normalizeNonNegativeInt(record.outputSeq, 0),
|
|
1942
|
+
data: trimmedData,
|
|
1943
|
+
truncated: getTerminalChunkByteLength(trimmedData) < getTerminalChunkByteLength(joinedData),
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1917
1947
|
function sendDirectPtyPayload(taskId, payload) {
|
|
1918
1948
|
const transport = activePtyRtcTransports.get(taskId);
|
|
1919
1949
|
const channel = transport?.channel;
|
|
@@ -2284,6 +2314,23 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
2284
2314
|
});
|
|
2285
2315
|
|
|
2286
2316
|
const lastSeq = normalizePositiveInt(payload?.last_seq ?? payload?.lastSeq, 0);
|
|
2317
|
+
const connectionId = normalizeOptionalString(payload?.connection_id ?? payload?.connectionId);
|
|
2318
|
+
const resumeStrategy = normalizeTerminalResumeStrategy(payload?.resume_strategy ?? payload?.resumeStrategy);
|
|
2319
|
+
if (lastSeq === 0 && resumeStrategy === "snapshot" && connectionId) {
|
|
2320
|
+
const snapshot = buildTerminalResumeSnapshot(record);
|
|
2321
|
+
await sendTerminalEvent("terminal_snapshot", {
|
|
2322
|
+
task_id: taskId,
|
|
2323
|
+
project_id: record.projectId,
|
|
2324
|
+
pty_session_id: record.ptySessionId,
|
|
2325
|
+
connection_id: connectionId,
|
|
2326
|
+
last_seq: snapshot.lastSeq,
|
|
2327
|
+
data: snapshot.data,
|
|
2328
|
+
truncated: snapshot.truncated,
|
|
2329
|
+
}).catch((err) => {
|
|
2330
|
+
logError(`Failed to report terminal_snapshot for ${taskId}: ${err?.message || err}`);
|
|
2331
|
+
});
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2287
2334
|
for (const chunk of record.ringBuffer) {
|
|
2288
2335
|
if (chunk.seq <= lastSeq) continue;
|
|
2289
2336
|
await sendTerminalEvent("terminal_output", {
|
package/src/fire/resume.js
CHANGED
|
@@ -3,6 +3,7 @@ import { promises as fsp } from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import readline from "node:readline";
|
|
6
|
+
import crypto from "node:crypto";
|
|
6
7
|
|
|
7
8
|
import yaml from "js-yaml";
|
|
8
9
|
|
|
@@ -36,6 +37,9 @@ export function buildResumeArgsForBackend(backend, sessionId) {
|
|
|
36
37
|
if (normalizedBackend === "copilot") {
|
|
37
38
|
return [`--resume=${resumeSessionId}`];
|
|
38
39
|
}
|
|
40
|
+
if (normalizedBackend === "kimi" || normalizedBackend === "kimi-cli" || normalizedBackend === "kimi-code") {
|
|
41
|
+
return ["--session", resumeSessionId];
|
|
42
|
+
}
|
|
39
43
|
throw new Error(`--resume is not supported for backend "${backend}"`);
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -50,6 +54,9 @@ export function resumeProviderForBackend(backend) {
|
|
|
50
54
|
if (normalizedBackend === "copilot") {
|
|
51
55
|
return "copilot";
|
|
52
56
|
}
|
|
57
|
+
if (normalizedBackend === "kimi" || normalizedBackend === "kimi-cli" || normalizedBackend === "kimi-code") {
|
|
58
|
+
return "kimi";
|
|
59
|
+
}
|
|
53
60
|
return null;
|
|
54
61
|
}
|
|
55
62
|
|
|
@@ -64,6 +71,9 @@ export async function findSessionPath(provider, sessionId, options = {}) {
|
|
|
64
71
|
if (normalizedProvider === "copilot") {
|
|
65
72
|
return findCopilotSessionPath(sessionId, options);
|
|
66
73
|
}
|
|
74
|
+
if (normalizedProvider === "kimi") {
|
|
75
|
+
return findKimiSessionPath(sessionId, options);
|
|
76
|
+
}
|
|
67
77
|
throw new Error(`Unsupported provider: ${provider}`);
|
|
68
78
|
}
|
|
69
79
|
|
|
@@ -120,6 +130,17 @@ export async function findCopilotSessionPath(sessionId, options = {}) {
|
|
|
120
130
|
return findPathByName(sessionStateDir, normalizedSessionId);
|
|
121
131
|
}
|
|
122
132
|
|
|
133
|
+
export async function findKimiSessionPath(sessionId, options = {}) {
|
|
134
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
135
|
+
if (!normalizedSessionId) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const homeDir = resolveHomeDir(options);
|
|
140
|
+
const sessionsDir = options.kimiSessionsDir || path.join(homeDir, ".kimi", "sessions");
|
|
141
|
+
return findKimiSessionDirectory(sessionsDir, normalizedSessionId);
|
|
142
|
+
}
|
|
143
|
+
|
|
123
144
|
export async function resolveSessionRunDirectory(sessionPath) {
|
|
124
145
|
const normalizedPath = typeof sessionPath === "string" ? sessionPath.trim() : "";
|
|
125
146
|
if (!normalizedPath) {
|
|
@@ -153,9 +174,23 @@ export async function resolveResumeContext(backend, sessionId, options = {}) {
|
|
|
153
174
|
throw new Error(`Invalid --resume session id for ${provider}: ${normalizedSessionId}`);
|
|
154
175
|
}
|
|
155
176
|
|
|
156
|
-
const cwdFromSession = await extractResumeCwdFromSession(
|
|
157
|
-
|
|
177
|
+
const cwdFromSession = await extractResumeCwdFromSession(
|
|
178
|
+
provider,
|
|
179
|
+
sessionPath,
|
|
180
|
+
normalizedSessionId,
|
|
181
|
+
options,
|
|
182
|
+
);
|
|
183
|
+
const fallbackCwd =
|
|
184
|
+
provider === "kimi" ? null : await resolveSessionRunDirectory(sessionPath);
|
|
158
185
|
const cwd = cwdFromSession || fallbackCwd;
|
|
186
|
+
if (!cwd) {
|
|
187
|
+
if (provider === "kimi") {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Could not resolve workspace for Kimi session ${normalizedSessionId}. Re-run from the original workspace or resume a session previously started by conductor fire.`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
throw new Error(`Could not resolve workspace for ${provider} session ${normalizedSessionId}`);
|
|
193
|
+
}
|
|
159
194
|
if (!(await isExistingDirectory(cwd))) {
|
|
160
195
|
throw new Error(`Resume workspace path does not exist: ${cwd}`);
|
|
161
196
|
}
|
|
@@ -290,7 +325,167 @@ async function extractCopilotResumeCwd(sessionPath) {
|
|
|
290
325
|
return null;
|
|
291
326
|
}
|
|
292
327
|
|
|
293
|
-
|
|
328
|
+
function md5Hex(value) {
|
|
329
|
+
return crypto.createHash("md5").update(String(value ?? "")).digest("hex");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function listCandidateWorkingDirectories(options = {}) {
|
|
333
|
+
const candidates = [];
|
|
334
|
+
const push = (value) => {
|
|
335
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
336
|
+
if (!normalized) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (!candidates.includes(normalized)) {
|
|
340
|
+
candidates.push(normalized);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
push(options.cwd);
|
|
345
|
+
push(options.currentWorkingDirectory);
|
|
346
|
+
push(process.env.PWD);
|
|
347
|
+
push(process.cwd());
|
|
348
|
+
|
|
349
|
+
return candidates;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function loadConductorSessionRecords(options = {}) {
|
|
353
|
+
const homeDir = resolveHomeDir(options);
|
|
354
|
+
const defaultPaths = [
|
|
355
|
+
path.join(homeDir, ".conductor", "session.yaml"),
|
|
356
|
+
path.join(homeDir, ".conductor", "sessions"),
|
|
357
|
+
];
|
|
358
|
+
const recordFiles = [];
|
|
359
|
+
const pushFile = (filePath) => {
|
|
360
|
+
const normalized = typeof filePath === "string" ? filePath.trim() : "";
|
|
361
|
+
if (!normalized || recordFiles.includes(normalized)) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
recordFiles.push(normalized);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
if (Array.isArray(options.conductorSessionFiles)) {
|
|
368
|
+
for (const entry of options.conductorSessionFiles) {
|
|
369
|
+
pushFile(entry);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (Array.isArray(options.conductorSessionDirs)) {
|
|
374
|
+
for (const entry of options.conductorSessionDirs) {
|
|
375
|
+
const normalizedDir = typeof entry === "string" ? entry.trim() : "";
|
|
376
|
+
if (!normalizedDir) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
let files = [];
|
|
380
|
+
try {
|
|
381
|
+
files = await fsp.readdir(normalizedDir, { withFileTypes: true });
|
|
382
|
+
} catch {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
for (const file of files) {
|
|
386
|
+
if (!file.isFile() || !file.name.endsWith(".yaml")) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
pushFile(path.join(normalizedDir, file.name));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
pushFile(defaultPaths[0]);
|
|
394
|
+
let files = [];
|
|
395
|
+
try {
|
|
396
|
+
files = await fsp.readdir(defaultPaths[1], { withFileTypes: true });
|
|
397
|
+
} catch {
|
|
398
|
+
files = [];
|
|
399
|
+
}
|
|
400
|
+
for (const file of files) {
|
|
401
|
+
if (!file.isFile() || !file.name.endsWith(".yaml")) {
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
pushFile(path.join(defaultPaths[1], file.name));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const records = [];
|
|
409
|
+
for (const filePath of recordFiles) {
|
|
410
|
+
let content = "";
|
|
411
|
+
try {
|
|
412
|
+
content = await fsp.readFile(filePath, "utf8");
|
|
413
|
+
} catch {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
let parsed;
|
|
417
|
+
try {
|
|
418
|
+
parsed = yaml.load(content);
|
|
419
|
+
} catch {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const entries = Array.isArray(parsed?.sessions) ? parsed.sessions : Array.isArray(parsed) ? parsed : [];
|
|
423
|
+
for (const entry of entries) {
|
|
424
|
+
if (!entry || typeof entry !== "object") {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
records.push(entry);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return records;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function normalizeProjectPathCandidate(value) {
|
|
434
|
+
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function resolveKimiResumeCwd(sessionPath, sessionId, options = {}) {
|
|
438
|
+
const sessionDirectory = typeof sessionPath === "string" ? sessionPath.trim() : "";
|
|
439
|
+
if (!sessionDirectory) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const worktreeHash = path.basename(path.dirname(sessionDirectory));
|
|
444
|
+
if (!worktreeHash) {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
for (const candidate of listCandidateWorkingDirectories(options)) {
|
|
449
|
+
if (md5Hex(candidate) === worktreeHash) {
|
|
450
|
+
return candidate;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const records = await loadConductorSessionRecords(options);
|
|
455
|
+
const bySessionId = [];
|
|
456
|
+
const byHash = [];
|
|
457
|
+
|
|
458
|
+
for (const record of records) {
|
|
459
|
+
const projectPath = normalizeProjectPathCandidate(record?.project_path);
|
|
460
|
+
if (!projectPath) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const backendType = normalizeBackend(record?.backend_type);
|
|
464
|
+
const recordSessionId = normalizeSessionId(record?.session_id);
|
|
465
|
+
const projectHash = md5Hex(projectPath);
|
|
466
|
+
if (
|
|
467
|
+
recordSessionId === sessionId &&
|
|
468
|
+
(backendType === "kimi" || !backendType) &&
|
|
469
|
+
projectHash === worktreeHash &&
|
|
470
|
+
!bySessionId.includes(projectPath)
|
|
471
|
+
) {
|
|
472
|
+
bySessionId.push(projectPath);
|
|
473
|
+
}
|
|
474
|
+
if (projectHash === worktreeHash && !byHash.includes(projectPath)) {
|
|
475
|
+
byHash.push(projectPath);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (bySessionId.length > 0) {
|
|
480
|
+
return bySessionId[0];
|
|
481
|
+
}
|
|
482
|
+
if (byHash.length === 1) {
|
|
483
|
+
return byHash[0];
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function extractResumeCwdFromSession(provider, sessionPath, sessionId, options = {}) {
|
|
294
489
|
if (provider === "codex") {
|
|
295
490
|
return extractCodexResumeCwd(sessionPath);
|
|
296
491
|
}
|
|
@@ -300,6 +495,9 @@ async function extractResumeCwdFromSession(provider, sessionPath, sessionId) {
|
|
|
300
495
|
if (provider === "copilot") {
|
|
301
496
|
return extractCopilotResumeCwd(sessionPath);
|
|
302
497
|
}
|
|
498
|
+
if (provider === "kimi") {
|
|
499
|
+
return resolveKimiResumeCwd(sessionPath, sessionId, options);
|
|
500
|
+
}
|
|
303
501
|
return null;
|
|
304
502
|
}
|
|
305
503
|
|
|
@@ -404,6 +602,26 @@ async function findPathByName(rootDir, sessionId) {
|
|
|
404
602
|
return null;
|
|
405
603
|
}
|
|
406
604
|
|
|
605
|
+
async function findKimiSessionDirectory(rootDir, sessionId) {
|
|
606
|
+
let hashDirs = [];
|
|
607
|
+
try {
|
|
608
|
+
hashDirs = await fsp.readdir(rootDir, { withFileTypes: true });
|
|
609
|
+
} catch {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
for (const hashDir of hashDirs) {
|
|
614
|
+
if (!hashDir.isDirectory()) {
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
const candidateDir = path.join(rootDir, hashDir.name, sessionId);
|
|
618
|
+
if (await pathExists(candidateDir, "directory")) {
|
|
619
|
+
return candidateDir;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
|
|
407
625
|
async function pathExists(targetPath, expectedType) {
|
|
408
626
|
try {
|
|
409
627
|
const stats = await fsp.stat(targetPath);
|
package/src/runtime-backends.js
CHANGED