@love-moon/conductor-cli 0.2.39 → 0.2.40
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 +16 -0
- package/bin/conductor-fire.js +258 -153
- package/bin/conductor-serve-ai.js +145 -0
- package/bin/conductor.js +5 -1
- package/package.json +6 -6
- package/src/ai-manager-handlers.js +51 -47
- package/src/daemon.js +321 -121
- package/src/fire/resume.js +498 -107
- package/src/handoff-log-mask.js +64 -0
- package/src/runtime-backends.js +67 -17
- package/src/serve-ai/adapter.js +383 -0
- package/src/serve-ai/config.js +133 -0
- package/src/serve-ai/errors.js +28 -0
- package/src/serve-ai/image-handler.js +92 -0
- package/src/serve-ai/index.js +529 -0
package/src/fire/resume.js
CHANGED
|
@@ -11,9 +11,15 @@ import {
|
|
|
11
11
|
getExternalRuntimeBackendDescriptor,
|
|
12
12
|
isRuntimeSupportedBackend,
|
|
13
13
|
normalizeRuntimeBackendAlias,
|
|
14
|
+
parseCommandParts,
|
|
14
15
|
resolveConfiguredRuntimeBackend,
|
|
15
16
|
} from "../runtime-backends.js";
|
|
16
17
|
|
|
18
|
+
const LEGACY_COPILOT_CLI_ARGS = new Set(["--allow-all-paths", "--allow-all-tools"]);
|
|
19
|
+
const DEFAULT_COPILOT_RESUME_TIMEOUT_MS = 20_000;
|
|
20
|
+
const DEFAULT_COPILOT_RESUME_STOP_TIMEOUT_MS = 5_000;
|
|
21
|
+
const COPILOT_GITHUB_TOKEN_ENV_KEYS = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"];
|
|
22
|
+
|
|
17
23
|
function normalizeBackend(backend) {
|
|
18
24
|
return String(backend || "").trim().toLowerCase();
|
|
19
25
|
}
|
|
@@ -41,6 +47,248 @@ function resolveConfigFilePath(options = {}) {
|
|
|
41
47
|
: path.join(resolveHomeDir(options), ".conductor", "config.yaml");
|
|
42
48
|
}
|
|
43
49
|
|
|
50
|
+
function normalizeCopilotCliArgs(args) {
|
|
51
|
+
if (!Array.isArray(args)) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
return args.filter((item) => {
|
|
55
|
+
const normalized = typeof item === "string" ? item.trim().toLowerCase() : "";
|
|
56
|
+
return normalized && !LEGACY_COPILOT_CLI_ARGS.has(normalized);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function stripExecutableSuffix(name) {
|
|
61
|
+
return String(name || "")
|
|
62
|
+
.trim()
|
|
63
|
+
.toLowerCase()
|
|
64
|
+
.replace(/\.(cmd|bat|exe)$/i, "");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isDefaultCopilotCommand(command) {
|
|
68
|
+
const normalized = String(command || "").trim();
|
|
69
|
+
if (!normalized || /[\\/]/.test(normalized)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return stripExecutableSuffix(normalized) === "copilot";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isEnvironmentAssignment(token) {
|
|
76
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(String(token || "").trim());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseEnvironmentAssignment(token) {
|
|
80
|
+
const normalized = String(token || "");
|
|
81
|
+
const index = normalized.indexOf("=");
|
|
82
|
+
if (index <= 0) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
key: normalized.slice(0, index),
|
|
87
|
+
value: normalized.slice(index + 1),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isEnvCommand(command) {
|
|
92
|
+
return stripExecutableSuffix(path.basename(String(command || ""))) === "env";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isPathLikeCommand(command) {
|
|
96
|
+
const normalized = String(command || "").trim();
|
|
97
|
+
return (
|
|
98
|
+
normalized.startsWith(".") ||
|
|
99
|
+
normalized.startsWith("/") ||
|
|
100
|
+
normalized.includes("/") ||
|
|
101
|
+
normalized.includes("\\") ||
|
|
102
|
+
/^[A-Za-z]:[\\/]/.test(normalized)
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveExecutablePath(command, env = process.env) {
|
|
107
|
+
const normalized = String(command || "").trim();
|
|
108
|
+
if (!normalized) {
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
if (isPathLikeCommand(normalized)) {
|
|
112
|
+
return normalized;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const pathEnv = typeof env?.PATH === "string" ? env.PATH : process.env.PATH || "";
|
|
116
|
+
const pathExt =
|
|
117
|
+
process.platform === "win32" && !path.extname(normalized)
|
|
118
|
+
? String(env?.PATHEXT || process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD")
|
|
119
|
+
.split(";")
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
: [""];
|
|
122
|
+
for (const dir of pathEnv.split(path.delimiter)) {
|
|
123
|
+
if (!dir) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
for (const ext of pathExt) {
|
|
127
|
+
const candidate = path.join(dir, `${normalized}${ext}`);
|
|
128
|
+
if (fs.existsSync(candidate)) {
|
|
129
|
+
return candidate;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return "";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function unwrapEnvironmentCommand(command, args) {
|
|
137
|
+
const parts = [command, ...args].filter((item) => typeof item === "string" && item.length > 0);
|
|
138
|
+
const extraEnv = {};
|
|
139
|
+
let index = 0;
|
|
140
|
+
|
|
141
|
+
while (index < parts.length && isEnvironmentAssignment(parts[index])) {
|
|
142
|
+
const assignment = parseEnvironmentAssignment(parts[index]);
|
|
143
|
+
if (assignment) {
|
|
144
|
+
extraEnv[assignment.key] = assignment.value;
|
|
145
|
+
}
|
|
146
|
+
index += 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (index > 0) {
|
|
150
|
+
return {
|
|
151
|
+
command: parts[index] || "",
|
|
152
|
+
args: parts.slice(index + 1),
|
|
153
|
+
env: extraEnv,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!isEnvCommand(command)) {
|
|
158
|
+
return { command, args, env: extraEnv };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
index = 0;
|
|
162
|
+
while (index < args.length) {
|
|
163
|
+
const token = args[index];
|
|
164
|
+
if (token === "--") {
|
|
165
|
+
index += 1;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
if (isEnvironmentAssignment(token)) {
|
|
169
|
+
const assignment = parseEnvironmentAssignment(token);
|
|
170
|
+
if (assignment) {
|
|
171
|
+
extraEnv[assignment.key] = assignment.value;
|
|
172
|
+
}
|
|
173
|
+
index += 1;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (String(token || "").startsWith("-")) {
|
|
177
|
+
return { command, args, env: extraEnv };
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
command: args[index] || "",
|
|
184
|
+
args: args.slice(index + 1),
|
|
185
|
+
env: extraEnv,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function hasOwnEnumerableKeys(value) {
|
|
190
|
+
return value && typeof value === "object" && Object.keys(value).length > 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function withoutCopilotGithubTokenEnv(env) {
|
|
194
|
+
const next = env && typeof env === "object" ? { ...env } : {};
|
|
195
|
+
for (const key of COPILOT_GITHUB_TOKEN_ENV_KEYS) {
|
|
196
|
+
delete next[key];
|
|
197
|
+
}
|
|
198
|
+
return next;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function resolvePositiveTimeoutMs(value, fallback) {
|
|
202
|
+
const n = Number(value);
|
|
203
|
+
return Number.isFinite(n) && n > 0 ? Math.round(n) : fallback;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function remainingTimeoutMs(startedAtMs, timeoutMs, message) {
|
|
207
|
+
const remaining = timeoutMs - (Date.now() - startedAtMs);
|
|
208
|
+
if (remaining <= 0) {
|
|
209
|
+
throw new Error(message);
|
|
210
|
+
}
|
|
211
|
+
return remaining;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function withTimeout(promise, timeoutMs, message) {
|
|
215
|
+
let timer = null;
|
|
216
|
+
try {
|
|
217
|
+
return await Promise.race([
|
|
218
|
+
promise,
|
|
219
|
+
new Promise((_, reject) => {
|
|
220
|
+
timer = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
221
|
+
}),
|
|
222
|
+
]);
|
|
223
|
+
} finally {
|
|
224
|
+
if (timer) {
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function resolveCopilotCliLaunch(commandLine, env = process.env) {
|
|
231
|
+
const normalized = typeof commandLine === "string" ? commandLine.trim() : "";
|
|
232
|
+
if (!normalized) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
const parsed = parseCommandParts(normalized);
|
|
236
|
+
const unwrapped = unwrapEnvironmentCommand(parsed.command, parsed.args);
|
|
237
|
+
const command = unwrapped.command;
|
|
238
|
+
const args = unwrapped.args;
|
|
239
|
+
if (!command) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const cliArgs = normalizeCopilotCliArgs(args);
|
|
243
|
+
if (isDefaultCopilotCommand(command)) {
|
|
244
|
+
if (cliArgs.length === 0 && !hasOwnEnumerableKeys(unwrapped.env)) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
cliArgs,
|
|
249
|
+
env: unwrapped.env,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const launchEnv = {
|
|
253
|
+
...process.env,
|
|
254
|
+
...env,
|
|
255
|
+
...unwrapped.env,
|
|
256
|
+
};
|
|
257
|
+
const resolvedPath = resolveExecutablePath(command, launchEnv);
|
|
258
|
+
return {
|
|
259
|
+
cliPath: resolvedPath || command,
|
|
260
|
+
cliArgs,
|
|
261
|
+
env: unwrapped.env,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function normalizeEnvConfigValue(value) {
|
|
266
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function proxyToEnv(envConfig) {
|
|
270
|
+
if (!envConfig || typeof envConfig !== "object") {
|
|
271
|
+
return {};
|
|
272
|
+
}
|
|
273
|
+
const env = {};
|
|
274
|
+
const mappings = {
|
|
275
|
+
http_proxy: ["HTTP_PROXY", "http_proxy"],
|
|
276
|
+
https_proxy: ["HTTPS_PROXY", "https_proxy"],
|
|
277
|
+
all_proxy: ["ALL_PROXY", "all_proxy"],
|
|
278
|
+
no_proxy: ["NO_PROXY", "no_proxy"],
|
|
279
|
+
};
|
|
280
|
+
for (const [key, envKeys] of Object.entries(mappings)) {
|
|
281
|
+
const value = envConfig[key] || envConfig[key.toUpperCase()];
|
|
282
|
+
if (!value) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
for (const envKey of envKeys) {
|
|
286
|
+
env[envKey] = value;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return env;
|
|
290
|
+
}
|
|
291
|
+
|
|
44
292
|
export function buildResumeArgsForBackend(backend, sessionId) {
|
|
45
293
|
const resumeSessionId = normalizeSessionId(sessionId);
|
|
46
294
|
if (!resumeSessionId) {
|
|
@@ -87,9 +335,6 @@ export async function findSessionPath(provider, sessionId, options = {}) {
|
|
|
87
335
|
if (normalizedProvider === "claude") {
|
|
88
336
|
return findClaudeSessionPath(sessionId, options);
|
|
89
337
|
}
|
|
90
|
-
if (normalizedProvider === "copilot") {
|
|
91
|
-
return findCopilotSessionPath(sessionId, options);
|
|
92
|
-
}
|
|
93
338
|
if (normalizedProvider === "kimi") {
|
|
94
339
|
return findKimiSessionPath(sessionId, options);
|
|
95
340
|
}
|
|
@@ -128,27 +373,6 @@ export async function findClaudeSessionPath(sessionId, options = {}) {
|
|
|
128
373
|
return null;
|
|
129
374
|
}
|
|
130
375
|
|
|
131
|
-
export async function findCopilotSessionPath(sessionId, options = {}) {
|
|
132
|
-
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
133
|
-
if (!normalizedSessionId) {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const homeDir = resolveHomeDir(options);
|
|
138
|
-
const sessionStateDir = options.copilotSessionStateDir || path.join(homeDir, ".copilot", "session-state");
|
|
139
|
-
const directJsonlPath = path.join(sessionStateDir, `${normalizedSessionId}.jsonl`);
|
|
140
|
-
if (await pathExists(directJsonlPath, "file")) {
|
|
141
|
-
return directJsonlPath;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const directSessionDir = path.join(sessionStateDir, normalizedSessionId);
|
|
145
|
-
if (await pathExists(directSessionDir, "directory")) {
|
|
146
|
-
return directSessionDir;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return findPathByName(sessionStateDir, normalizedSessionId);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
376
|
export async function findKimiSessionPath(sessionId, options = {}) {
|
|
153
377
|
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
154
378
|
if (!normalizedSessionId) {
|
|
@@ -183,15 +407,43 @@ export async function resolveResumeContext(backend, sessionId, options = {}) {
|
|
|
183
407
|
if (!normalizedSessionId) {
|
|
184
408
|
throw new Error("--resume requires a session id");
|
|
185
409
|
}
|
|
186
|
-
const
|
|
410
|
+
const configFilePath = resolveConfigFilePath(options);
|
|
411
|
+
const allowCliList =
|
|
412
|
+
options.allowCliList && typeof options.allowCliList === "object"
|
|
413
|
+
? options.allowCliList
|
|
414
|
+
: await loadConfiguredAllowCliList({ ...options, configFilePath });
|
|
415
|
+
const lookupBackend = await resolveResumeLookupBackend(backend, {
|
|
416
|
+
...options,
|
|
417
|
+
configFilePath,
|
|
418
|
+
allowCliList,
|
|
419
|
+
});
|
|
420
|
+
const provider = resumeProviderForBackend(lookupBackend || backend);
|
|
187
421
|
if (!provider) {
|
|
188
|
-
const externalContext = await resolveExternalResumeContext(backend, normalizedSessionId,
|
|
422
|
+
const externalContext = await resolveExternalResumeContext(backend, normalizedSessionId, {
|
|
423
|
+
...options,
|
|
424
|
+
configFilePath,
|
|
425
|
+
allowCliList,
|
|
426
|
+
});
|
|
189
427
|
if (externalContext) {
|
|
190
428
|
return externalContext;
|
|
191
429
|
}
|
|
192
430
|
throw new Error(`--resume is not supported for backend "${backend}"`);
|
|
193
431
|
}
|
|
194
432
|
|
|
433
|
+
if (provider === "copilot") {
|
|
434
|
+
const copilotContext = await resolveCopilotResumeContext(normalizedSessionId, {
|
|
435
|
+
...options,
|
|
436
|
+
configFilePath,
|
|
437
|
+
allowCliList,
|
|
438
|
+
backend,
|
|
439
|
+
runtimeBackend: lookupBackend || provider,
|
|
440
|
+
});
|
|
441
|
+
if (!copilotContext) {
|
|
442
|
+
throw new Error(`Invalid --resume session id for copilot: ${normalizedSessionId}`);
|
|
443
|
+
}
|
|
444
|
+
return copilotContext;
|
|
445
|
+
}
|
|
446
|
+
|
|
195
447
|
const sessionPath = await findSessionPath(provider, normalizedSessionId, options);
|
|
196
448
|
if (!sessionPath) {
|
|
197
449
|
throw new Error(`Invalid --resume session id for ${provider}: ${normalizedSessionId}`);
|
|
@@ -298,56 +550,6 @@ async function extractClaudeResumeCwd(sessionPath, sessionId) {
|
|
|
298
550
|
return null;
|
|
299
551
|
}
|
|
300
552
|
|
|
301
|
-
async function extractCopilotResumeCwd(sessionPath) {
|
|
302
|
-
let stats;
|
|
303
|
-
try {
|
|
304
|
-
stats = await fsp.stat(sessionPath);
|
|
305
|
-
} catch {
|
|
306
|
-
return null;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (stats.isDirectory()) {
|
|
310
|
-
const workspaceYamlPath = path.join(sessionPath, "workspace.yaml");
|
|
311
|
-
try {
|
|
312
|
-
const yamlContent = await fsp.readFile(workspaceYamlPath, "utf8");
|
|
313
|
-
const parsed = yaml.load(yamlContent);
|
|
314
|
-
const maybeCwd = parsed && typeof parsed === "object" ? parsed.cwd : null;
|
|
315
|
-
if (typeof maybeCwd === "string" && maybeCwd.trim()) {
|
|
316
|
-
return maybeCwd.trim();
|
|
317
|
-
}
|
|
318
|
-
} catch {
|
|
319
|
-
return null;
|
|
320
|
-
}
|
|
321
|
-
return null;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (!sessionPath.endsWith(".jsonl")) {
|
|
325
|
-
return null;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const rl = readline.createInterface({
|
|
329
|
-
input: fs.createReadStream(sessionPath),
|
|
330
|
-
crlfDelay: Infinity,
|
|
331
|
-
});
|
|
332
|
-
for await (const line of rl) {
|
|
333
|
-
const trimmed = line.trim();
|
|
334
|
-
if (!trimmed) {
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
let entry;
|
|
338
|
-
try {
|
|
339
|
-
entry = JSON.parse(trimmed);
|
|
340
|
-
} catch {
|
|
341
|
-
continue;
|
|
342
|
-
}
|
|
343
|
-
const maybeCwd = entry?.data?.context?.cwd || entry?.data?.cwd;
|
|
344
|
-
if (typeof maybeCwd === "string" && maybeCwd.trim()) {
|
|
345
|
-
return maybeCwd.trim();
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return null;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
553
|
function md5Hex(value) {
|
|
352
554
|
return crypto.createHash("md5").update(String(value ?? "")).digest("hex");
|
|
353
555
|
}
|
|
@@ -456,21 +658,44 @@ async function loadConductorSessionRecords(options = {}) {
|
|
|
456
658
|
return records;
|
|
457
659
|
}
|
|
458
660
|
|
|
459
|
-
async function
|
|
661
|
+
async function loadParsedConfigFile(options = {}) {
|
|
460
662
|
const configFilePath = resolveConfigFilePath(options);
|
|
461
|
-
let parsed = null;
|
|
462
663
|
try {
|
|
463
664
|
const content = await fsp.readFile(configFilePath, "utf8");
|
|
464
|
-
parsed = yaml.load(content);
|
|
665
|
+
const parsed = yaml.load(content);
|
|
666
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
465
667
|
} catch {
|
|
466
|
-
return
|
|
668
|
+
return null;
|
|
467
669
|
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
async function loadConfiguredAllowCliList(options = {}) {
|
|
673
|
+
const configFilePath = resolveConfigFilePath(options);
|
|
674
|
+
const parsed = await loadParsedConfigFile({ ...options, configFilePath });
|
|
468
675
|
if (!parsed || typeof parsed !== "object" || !parsed.allow_cli_list || typeof parsed.allow_cli_list !== "object") {
|
|
469
676
|
return {};
|
|
470
677
|
}
|
|
471
678
|
return filterRuntimeSupportedAllowCliList(parsed.allow_cli_list, { configFilePath });
|
|
472
679
|
}
|
|
473
680
|
|
|
681
|
+
async function loadConfiguredEnvMap(options = {}) {
|
|
682
|
+
const parsed = await loadParsedConfigFile(options);
|
|
683
|
+
if (!parsed || typeof parsed !== "object" || !parsed.envs || typeof parsed.envs !== "object") {
|
|
684
|
+
return {};
|
|
685
|
+
}
|
|
686
|
+
const proxyEnv = proxyToEnv(parsed.envs);
|
|
687
|
+
const normalizedEnv = {
|
|
688
|
+
...proxyEnv,
|
|
689
|
+
};
|
|
690
|
+
for (const [key, value] of Object.entries(parsed.envs)) {
|
|
691
|
+
const normalizedValue = normalizeEnvConfigValue(value);
|
|
692
|
+
if (normalizedValue !== undefined) {
|
|
693
|
+
normalizedEnv[key] = normalizedValue;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return normalizedEnv;
|
|
697
|
+
}
|
|
698
|
+
|
|
474
699
|
async function resolveResumeLookupBackend(backend, options = {}) {
|
|
475
700
|
const normalizedBackend = normalizeBackend(backend);
|
|
476
701
|
if (!normalizedBackend) {
|
|
@@ -490,6 +715,195 @@ async function resolveResumeLookupBackend(backend, options = {}) {
|
|
|
490
715
|
return normalizeRuntimeBackendAlias(normalizedBackend, { configFilePath });
|
|
491
716
|
}
|
|
492
717
|
|
|
718
|
+
async function getCopilotSdkModule(options = {}) {
|
|
719
|
+
if (options.copilotSdkModule && typeof options.copilotSdkModule === "object") {
|
|
720
|
+
return options.copilotSdkModule;
|
|
721
|
+
}
|
|
722
|
+
return import("@github/copilot-sdk");
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function resolveCopilotCommandLine(options = {}) {
|
|
726
|
+
if (typeof options.commandLine === "string" && options.commandLine.trim()) {
|
|
727
|
+
return options.commandLine.trim();
|
|
728
|
+
}
|
|
729
|
+
const configFilePath = resolveConfigFilePath(options);
|
|
730
|
+
const allowCliList =
|
|
731
|
+
options.allowCliList && typeof options.allowCliList === "object"
|
|
732
|
+
? options.allowCliList
|
|
733
|
+
: await loadConfiguredAllowCliList({ ...options, configFilePath });
|
|
734
|
+
const backendCandidates = [];
|
|
735
|
+
const pushCandidate = (backend) => {
|
|
736
|
+
const normalized = normalizeBackend(backend);
|
|
737
|
+
if (normalized && !backendCandidates.includes(normalized)) {
|
|
738
|
+
backendCandidates.push(normalized);
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
pushCandidate(options.backend);
|
|
742
|
+
pushCandidate(options.runtimeBackend);
|
|
743
|
+
pushCandidate("copilot");
|
|
744
|
+
|
|
745
|
+
for (const candidate of backendCandidates) {
|
|
746
|
+
const configuredBackend = await resolveConfiguredRuntimeBackend(candidate, allowCliList, {
|
|
747
|
+
configFilePath,
|
|
748
|
+
});
|
|
749
|
+
const commandLine =
|
|
750
|
+
typeof configuredBackend?.commandLine === "string" && configuredBackend.commandLine.trim()
|
|
751
|
+
? configuredBackend.commandLine.trim()
|
|
752
|
+
: "";
|
|
753
|
+
if (commandLine) {
|
|
754
|
+
return commandLine;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return "";
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async function buildCopilotClientOptions(options = {}) {
|
|
761
|
+
const clientOptions = options.copilotClientOptions && typeof options.copilotClientOptions === "object"
|
|
762
|
+
? { ...options.copilotClientOptions }
|
|
763
|
+
: {};
|
|
764
|
+
const configFilePath = resolveConfigFilePath(options);
|
|
765
|
+
const configEnv = await loadConfiguredEnvMap({ ...options, configFilePath });
|
|
766
|
+
const commandLine = await resolveCopilotCommandLine(options);
|
|
767
|
+
const cliLaunch = resolveCopilotCliLaunch(commandLine, {
|
|
768
|
+
...process.env,
|
|
769
|
+
...configEnv,
|
|
770
|
+
...options.env,
|
|
771
|
+
});
|
|
772
|
+
if (cliLaunch && clientOptions.cliPath === undefined && clientOptions.cliArgs === undefined && clientOptions.cliUrl === undefined) {
|
|
773
|
+
if (cliLaunch.cliPath !== undefined) {
|
|
774
|
+
clientOptions.cliPath = cliLaunch.cliPath;
|
|
775
|
+
}
|
|
776
|
+
if (cliLaunch.cliArgs !== undefined) {
|
|
777
|
+
clientOptions.cliArgs = cliLaunch.cliArgs;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const explicitGithubToken =
|
|
782
|
+
typeof clientOptions.githubToken === "string" && clientOptions.githubToken.trim()
|
|
783
|
+
? clientOptions.githubToken.trim()
|
|
784
|
+
: typeof options.githubToken === "string" && options.githubToken.trim()
|
|
785
|
+
? options.githubToken.trim()
|
|
786
|
+
: "";
|
|
787
|
+
if (clientOptions.githubToken === undefined && explicitGithubToken) {
|
|
788
|
+
clientOptions.githubToken = explicitGithubToken;
|
|
789
|
+
}
|
|
790
|
+
if (clientOptions.useLoggedInUser === undefined && typeof options.useLoggedInUser === "boolean") {
|
|
791
|
+
clientOptions.useLoggedInUser = options.useLoggedInUser;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
let resolvedEnv;
|
|
795
|
+
if (clientOptions.env === undefined) {
|
|
796
|
+
resolvedEnv = {
|
|
797
|
+
...process.env,
|
|
798
|
+
...configEnv,
|
|
799
|
+
...(options.env && typeof options.env === "object" ? options.env : {}),
|
|
800
|
+
...(hasOwnEnumerableKeys(cliLaunch?.env) ? cliLaunch.env : {}),
|
|
801
|
+
};
|
|
802
|
+
} else if (hasOwnEnumerableKeys(cliLaunch?.env)) {
|
|
803
|
+
resolvedEnv = {
|
|
804
|
+
...clientOptions.env,
|
|
805
|
+
...cliLaunch.env,
|
|
806
|
+
};
|
|
807
|
+
} else {
|
|
808
|
+
resolvedEnv = { ...clientOptions.env };
|
|
809
|
+
}
|
|
810
|
+
clientOptions.env = explicitGithubToken
|
|
811
|
+
? resolvedEnv
|
|
812
|
+
: withoutCopilotGithubTokenEnv(resolvedEnv);
|
|
813
|
+
if (!explicitGithubToken && clientOptions.useLoggedInUser === undefined) {
|
|
814
|
+
clientOptions.useLoggedInUser = true;
|
|
815
|
+
}
|
|
816
|
+
if (clientOptions.cwd === undefined) {
|
|
817
|
+
const cwd =
|
|
818
|
+
typeof options.cwd === "string" && options.cwd.trim()
|
|
819
|
+
? options.cwd.trim()
|
|
820
|
+
: process.cwd();
|
|
821
|
+
clientOptions.cwd = cwd;
|
|
822
|
+
}
|
|
823
|
+
return clientOptions;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
async function withCopilotClient(options, fn) {
|
|
827
|
+
const sdkModule = await getCopilotSdkModule(options);
|
|
828
|
+
if (!sdkModule || typeof sdkModule.CopilotClient !== "function") {
|
|
829
|
+
throw new Error("GitHub Copilot SDK client is unavailable");
|
|
830
|
+
}
|
|
831
|
+
const timeoutMs = resolvePositiveTimeoutMs(
|
|
832
|
+
options.copilotResumeTimeoutMs ?? options.timeoutMs,
|
|
833
|
+
DEFAULT_COPILOT_RESUME_TIMEOUT_MS,
|
|
834
|
+
);
|
|
835
|
+
const startedAtMs = Date.now();
|
|
836
|
+
const client = new sdkModule.CopilotClient(await buildCopilotClientOptions(options));
|
|
837
|
+
try {
|
|
838
|
+
if (typeof client.start === "function") {
|
|
839
|
+
const startTimeoutMs = remainingTimeoutMs(startedAtMs, timeoutMs, "copilot resume lookup timed out");
|
|
840
|
+
await withTimeout(
|
|
841
|
+
client.start(),
|
|
842
|
+
startTimeoutMs,
|
|
843
|
+
"copilot resume SDK start timed out",
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
const lookupTimeoutMs = remainingTimeoutMs(startedAtMs, timeoutMs, "copilot resume lookup timed out");
|
|
847
|
+
return await withTimeout(
|
|
848
|
+
fn(client),
|
|
849
|
+
lookupTimeoutMs,
|
|
850
|
+
"copilot resume lookup timed out",
|
|
851
|
+
);
|
|
852
|
+
} finally {
|
|
853
|
+
try {
|
|
854
|
+
if (typeof client.stop === "function") {
|
|
855
|
+
const stopTimeoutMs = resolvePositiveTimeoutMs(
|
|
856
|
+
options.copilotResumeStopTimeoutMs,
|
|
857
|
+
DEFAULT_COPILOT_RESUME_STOP_TIMEOUT_MS,
|
|
858
|
+
);
|
|
859
|
+
await withTimeout(
|
|
860
|
+
client.stop(),
|
|
861
|
+
stopTimeoutMs,
|
|
862
|
+
"copilot resume SDK stop timed out",
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
} catch {
|
|
866
|
+
try {
|
|
867
|
+
await client.forceStop?.();
|
|
868
|
+
} catch {
|
|
869
|
+
// best effort
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async function resolveCopilotResumeContext(sessionId, options = {}) {
|
|
876
|
+
const sessionMetadata = await withCopilotClient(options, async (client) => {
|
|
877
|
+
const sessions = await client.listSessions();
|
|
878
|
+
return sessions.find((entry) => normalizeSessionId(entry?.sessionId) === sessionId) || null;
|
|
879
|
+
});
|
|
880
|
+
if (!sessionMetadata) {
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const cwd = normalizeProjectPathCandidate(sessionMetadata?.context?.cwd);
|
|
885
|
+
if (!cwd) {
|
|
886
|
+
throw new Error(`Could not resolve workspace for copilot session ${sessionId}`);
|
|
887
|
+
}
|
|
888
|
+
if (!(await isExistingDirectory(cwd))) {
|
|
889
|
+
throw new Error(`Resume workspace path does not exist: ${cwd}`);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return {
|
|
893
|
+
provider: "copilot",
|
|
894
|
+
sessionId,
|
|
895
|
+
sessionPath: null,
|
|
896
|
+
cwd,
|
|
897
|
+
debugMetadata: {
|
|
898
|
+
cwdSource: "sdk_list_sessions",
|
|
899
|
+
sessionPath: null,
|
|
900
|
+
context: sessionMetadata?.context && typeof sessionMetadata.context === "object"
|
|
901
|
+
? { ...sessionMetadata.context }
|
|
902
|
+
: undefined,
|
|
903
|
+
},
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
493
907
|
function normalizeProjectPathCandidate(value) {
|
|
494
908
|
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
495
909
|
}
|
|
@@ -500,7 +914,10 @@ function normalizeConductorRecordSourcePath(value) {
|
|
|
500
914
|
|
|
501
915
|
async function resolveExternalResumeContext(backend, sessionId, options = {}) {
|
|
502
916
|
const configFilePath = resolveConfigFilePath(options);
|
|
503
|
-
const allowCliList =
|
|
917
|
+
const allowCliList =
|
|
918
|
+
options.allowCliList && typeof options.allowCliList === "object"
|
|
919
|
+
? options.allowCliList
|
|
920
|
+
: await loadConfiguredAllowCliList({ ...options, configFilePath });
|
|
504
921
|
const normalizedBackend = await resolveResumeLookupBackend(backend, {
|
|
505
922
|
...options,
|
|
506
923
|
configFilePath,
|
|
@@ -652,9 +1069,6 @@ async function extractResumeCwdFromSession(provider, sessionPath, sessionId, opt
|
|
|
652
1069
|
if (provider === "claude") {
|
|
653
1070
|
return extractClaudeResumeCwd(sessionPath, sessionId);
|
|
654
1071
|
}
|
|
655
|
-
if (provider === "copilot") {
|
|
656
|
-
return extractCopilotResumeCwd(sessionPath);
|
|
657
|
-
}
|
|
658
1072
|
if (provider === "kimi") {
|
|
659
1073
|
return resolveKimiResumeCwd(sessionPath, sessionId, options);
|
|
660
1074
|
}
|
|
@@ -739,29 +1153,6 @@ async function findClaudeSessionEntries(projectsDir, sessionId) {
|
|
|
739
1153
|
return entries;
|
|
740
1154
|
}
|
|
741
1155
|
|
|
742
|
-
async function findPathByName(rootDir, sessionId) {
|
|
743
|
-
const queue = [rootDir];
|
|
744
|
-
while (queue.length) {
|
|
745
|
-
const current = queue.pop();
|
|
746
|
-
let entries = [];
|
|
747
|
-
try {
|
|
748
|
-
entries = await fsp.readdir(current, { withFileTypes: true });
|
|
749
|
-
} catch {
|
|
750
|
-
continue;
|
|
751
|
-
}
|
|
752
|
-
for (const entry of entries) {
|
|
753
|
-
const fullPath = path.join(current, entry.name);
|
|
754
|
-
if (entry.name.includes(sessionId)) {
|
|
755
|
-
return fullPath;
|
|
756
|
-
}
|
|
757
|
-
if (entry.isDirectory()) {
|
|
758
|
-
queue.push(fullPath);
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
return null;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
1156
|
async function findKimiSessionDirectory(rootDir, sessionId) {
|
|
766
1157
|
let hashDirs = [];
|
|
767
1158
|
try {
|