@love-moon/conductor-cli 0.2.42 → 0.3.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.
@@ -1,29 +1,33 @@
1
- import fs from "node:fs";
1
+ import crypto from "node:crypto";
2
2
  import { promises as fsp } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import readline from "node:readline";
6
- import crypto from "node:crypto";
7
5
 
8
6
  import yaml from "js-yaml";
7
+ import {
8
+ buildResumeArgsForBackend as buildResumeArgsForBackendFromSdk,
9
+ findSessionPath as findSessionPathFromSdk,
10
+ resolveResumeContext as resolveResumeContextFromSdk,
11
+ resolveSessionRunDirectory as resolveSessionRunDirectoryFromSdk,
12
+ resumeProviderForBackend as resumeProviderForBackendFromSdk,
13
+ } from "@love-moon/ai-sdk";
14
+
9
15
  import {
10
16
  filterRuntimeSupportedAllowCliList,
11
17
  getExternalRuntimeBackendDescriptor,
12
18
  isRuntimeSupportedBackend,
13
19
  normalizeRuntimeBackendAlias,
14
- parseCommandParts,
15
20
  resolveConfiguredRuntimeBackend,
16
21
  } from "../runtime-backends.js";
17
22
 
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
-
23
23
  function normalizeBackend(backend) {
24
24
  return String(backend || "").trim().toLowerCase();
25
25
  }
26
26
 
27
+ function normalizeSessionId(sessionId) {
28
+ return typeof sessionId === "string" ? sessionId.trim() : "";
29
+ }
30
+
27
31
  function resolveHomeDir(options) {
28
32
  if (options?.homeDir) {
29
33
  return options.homeDir;
@@ -31,10 +35,6 @@ function resolveHomeDir(options) {
31
35
  return os.homedir();
32
36
  }
33
37
 
34
- function normalizeSessionId(sessionId) {
35
- return typeof sessionId === "string" ? sessionId.trim() : "";
36
- }
37
-
38
38
  function resolveConfigFilePath(options = {}) {
39
39
  const configuredPath =
40
40
  typeof options?.configFilePath === "string" && options.configFilePath.trim()
@@ -47,439 +47,12 @@ function resolveConfigFilePath(options = {}) {
47
47
  : path.join(resolveHomeDir(options), ".conductor", "config.yaml");
48
48
  }
49
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
-
292
- export function buildResumeArgsForBackend(backend, sessionId) {
293
- const resumeSessionId = normalizeSessionId(sessionId);
294
- if (!resumeSessionId) {
295
- return [];
296
- }
297
- const normalizedBackend = normalizeBackend(backend);
298
- if (normalizedBackend === "codex" || normalizedBackend === "code") {
299
- return ["resume", resumeSessionId];
300
- }
301
- if (normalizedBackend === "claude" || normalizedBackend === "claude-code") {
302
- return ["--resume", resumeSessionId];
303
- }
304
- if (normalizedBackend === "copilot") {
305
- return [`--resume=${resumeSessionId}`];
306
- }
307
- if (normalizedBackend === "kimi" || normalizedBackend === "kimi-cli" || normalizedBackend === "kimi-code") {
308
- return ["--session", resumeSessionId];
309
- }
310
- throw new Error(`--resume is not supported for backend "${backend}"`);
311
- }
312
-
313
- export function resumeProviderForBackend(backend) {
314
- const normalizedBackend = normalizeBackend(backend);
315
- if (normalizedBackend === "codex" || normalizedBackend === "code") {
316
- return "codex";
317
- }
318
- if (normalizedBackend === "claude" || normalizedBackend === "claude-code") {
319
- return "claude";
320
- }
321
- if (normalizedBackend === "copilot") {
322
- return "copilot";
323
- }
324
- if (normalizedBackend === "kimi" || normalizedBackend === "kimi-cli" || normalizedBackend === "kimi-code") {
325
- return "kimi";
326
- }
327
- return null;
328
- }
329
-
330
- export async function findSessionPath(provider, sessionId, options = {}) {
331
- const normalizedProvider = String(provider || "").trim().toLowerCase();
332
- if (normalizedProvider === "codex") {
333
- return findCodexSessionPath(sessionId, options);
334
- }
335
- if (normalizedProvider === "claude") {
336
- return findClaudeSessionPath(sessionId, options);
337
- }
338
- if (normalizedProvider === "kimi") {
339
- return findKimiSessionPath(sessionId, options);
340
- }
341
- throw new Error(`Unsupported provider: ${provider}`);
342
- }
343
-
344
- export async function findCodexSessionPath(sessionId, options = {}) {
345
- const normalizedSessionId = normalizeSessionId(sessionId);
346
- if (!normalizedSessionId) {
347
- return null;
348
- }
349
- const homeDir = resolveHomeDir(options);
350
- const sessionsDir = options.codexSessionsDir || path.join(homeDir, ".codex", "sessions");
351
- return findCodexSessionFile(sessionsDir, normalizedSessionId);
352
- }
353
-
354
- export async function findClaudeSessionPath(sessionId, options = {}) {
355
- const normalizedSessionId = normalizeSessionId(sessionId);
356
- if (!normalizedSessionId) {
357
- return null;
358
- }
359
-
360
- const homeDir = resolveHomeDir(options);
361
- const projectsDir = options.claudeProjectsDir || path.join(homeDir, ".claude", "projects");
362
- const sessionEntries = await findClaudeSessionEntries(projectsDir, normalizedSessionId);
363
- if (sessionEntries.length > 0) {
364
- return sessionEntries[0]?.source || null;
365
- }
366
-
367
- const tasksDir = options.claudeTasksDir || path.join(homeDir, ".claude", "tasks");
368
- const directTaskDir = path.join(tasksDir, normalizedSessionId);
369
- if (await pathExists(directTaskDir, "directory")) {
370
- return directTaskDir;
371
- }
372
-
373
- return null;
374
- }
375
-
376
- export async function findKimiSessionPath(sessionId, options = {}) {
377
- const normalizedSessionId = normalizeSessionId(sessionId);
378
- if (!normalizedSessionId) {
379
- return null;
380
- }
381
-
382
- const homeDir = resolveHomeDir(options);
383
- const sessionsDir = options.kimiSessionsDir || path.join(homeDir, ".kimi", "sessions");
384
- return findKimiSessionDirectory(sessionsDir, normalizedSessionId);
385
- }
386
-
387
- export async function resolveSessionRunDirectory(sessionPath) {
388
- const normalizedPath = typeof sessionPath === "string" ? sessionPath.trim() : "";
389
- if (!normalizedPath) {
390
- throw new Error("Invalid session path");
391
- }
392
- let stats;
393
- try {
394
- stats = await fsp.stat(normalizedPath);
395
- } catch {
396
- throw new Error(`Session path does not exist: ${normalizedPath}`);
397
- }
398
- return stats.isDirectory() ? normalizedPath : path.dirname(normalizedPath);
399
- }
400
-
401
- export async function inspectResumeTarget(backend, sessionId, options = {}) {
402
- return resolveResumeContext(backend, sessionId, options);
50
+ function normalizeProjectPathCandidate(value) {
51
+ return typeof value === "string" && value.trim() ? value.trim() : "";
403
52
  }
404
53
 
405
- export async function resolveResumeContext(backend, sessionId, options = {}) {
406
- const normalizedSessionId = normalizeSessionId(sessionId);
407
- if (!normalizedSessionId) {
408
- throw new Error("--resume requires a session id");
409
- }
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);
421
- if (!provider) {
422
- const externalContext = await resolveExternalResumeContext(backend, normalizedSessionId, {
423
- ...options,
424
- configFilePath,
425
- allowCliList,
426
- });
427
- if (externalContext) {
428
- return externalContext;
429
- }
430
- throw new Error(`--resume is not supported for backend "${backend}"`);
431
- }
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
-
447
- const sessionPath = await findSessionPath(provider, normalizedSessionId, options);
448
- if (!sessionPath) {
449
- throw new Error(`Invalid --resume session id for ${provider}: ${normalizedSessionId}`);
450
- }
451
-
452
- const cwdFromSession = await extractResumeCwdFromSession(
453
- provider,
454
- sessionPath,
455
- normalizedSessionId,
456
- options,
457
- );
458
- const fallbackCwd =
459
- provider === "kimi" ? null : await resolveSessionRunDirectory(sessionPath);
460
- const cwd = cwdFromSession || fallbackCwd;
461
- if (!cwd) {
462
- if (provider === "kimi") {
463
- throw new Error(
464
- `Could not resolve workspace for Kimi session ${normalizedSessionId}. Re-run from the original workspace or resume a session previously started by conductor fire.`,
465
- );
466
- }
467
- throw new Error(`Could not resolve workspace for ${provider} session ${normalizedSessionId}`);
468
- }
469
- if (!(await isExistingDirectory(cwd))) {
470
- throw new Error(`Resume workspace path does not exist: ${cwd}`);
471
- }
472
-
473
- return {
474
- provider,
475
- sessionId: normalizedSessionId,
476
- sessionPath,
477
- cwd,
478
- debugMetadata: {
479
- cwdSource: cwdFromSession ? "session" : "session_path",
480
- sessionPath,
481
- },
482
- };
54
+ function normalizeConductorRecordSourcePath(value) {
55
+ return typeof value === "string" && value.trim() ? value.trim() : null;
483
56
  }
484
57
 
485
58
  async function isExistingDirectory(targetPath) {
@@ -495,65 +68,6 @@ async function isExistingDirectory(targetPath) {
495
68
  }
496
69
  }
497
70
 
498
- async function extractCodexResumeCwd(sessionPath) {
499
- if (!sessionPath.endsWith(".jsonl")) {
500
- return null;
501
- }
502
- const rl = readline.createInterface({
503
- input: fs.createReadStream(sessionPath),
504
- crlfDelay: Infinity,
505
- });
506
- for await (const line of rl) {
507
- const trimmed = line.trim();
508
- if (!trimmed) {
509
- continue;
510
- }
511
- let entry;
512
- try {
513
- entry = JSON.parse(trimmed);
514
- } catch {
515
- continue;
516
- }
517
- const maybeCwd = entry?.type === "session_meta" ? entry?.payload?.cwd : null;
518
- if (typeof maybeCwd === "string" && maybeCwd.trim()) {
519
- return maybeCwd.trim();
520
- }
521
- }
522
- return null;
523
- }
524
-
525
- async function extractClaudeResumeCwd(sessionPath, sessionId) {
526
- if (!sessionPath.endsWith(".jsonl")) {
527
- return null;
528
- }
529
- const rl = readline.createInterface({
530
- input: fs.createReadStream(sessionPath),
531
- crlfDelay: Infinity,
532
- });
533
- for await (const line of rl) {
534
- const trimmed = line.trim();
535
- if (!trimmed) {
536
- continue;
537
- }
538
- let entry;
539
- try {
540
- entry = JSON.parse(trimmed);
541
- } catch {
542
- continue;
543
- }
544
- const idMatches = String(entry?.sessionId || "").trim() === sessionId;
545
- const maybeCwd = entry?.cwd;
546
- if (idMatches && typeof maybeCwd === "string" && maybeCwd.trim()) {
547
- return maybeCwd.trim();
548
- }
549
- }
550
- return null;
551
- }
552
-
553
- function md5Hex(value) {
554
- return crypto.createHash("md5").update(String(value ?? "")).digest("hex");
555
- }
556
-
557
71
  function listCandidateWorkingDirectories(options = {}) {
558
72
  const candidates = [];
559
73
  const push = (value) => {
@@ -574,6 +88,10 @@ function listCandidateWorkingDirectories(options = {}) {
574
88
  return candidates;
575
89
  }
576
90
 
91
+ function md5Hex(value) {
92
+ return crypto.createHash("md5").update(String(value ?? "")).digest("hex");
93
+ }
94
+
577
95
  async function loadConductorSessionRecords(options = {}) {
578
96
  const homeDir = resolveHomeDir(options);
579
97
  const defaultPaths = [
@@ -678,24 +196,6 @@ async function loadConfiguredAllowCliList(options = {}) {
678
196
  return filterRuntimeSupportedAllowCliList(parsed.allow_cli_list, { configFilePath });
679
197
  }
680
198
 
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
-
699
199
  async function resolveResumeLookupBackend(backend, options = {}) {
700
200
  const normalizedBackend = normalizeBackend(backend);
701
201
  if (!normalizedBackend) {
@@ -715,204 +215,48 @@ async function resolveResumeLookupBackend(backend, options = {}) {
715
215
  return normalizeRuntimeBackendAlias(normalizedBackend, { configFilePath });
716
216
  }
717
217
 
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
- }
218
+ async function kimiWorkspaceLookupFromConductorRecords(worktreeHash, options, sessionId) {
219
+ const records = await loadConductorSessionRecords(options);
729
220
  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");
221
+ const allowCliList = await loadConfiguredAllowCliList({ ...options, configFilePath });
222
+ const bySessionId = [];
223
+ const byHash = [];
744
224
 
745
- for (const candidate of backendCandidates) {
746
- const configuredBackend = await resolveConfiguredRuntimeBackend(candidate, allowCliList, {
225
+ for (const record of records) {
226
+ const projectPath = normalizeProjectPathCandidate(record?.project_path);
227
+ if (!projectPath) {
228
+ continue;
229
+ }
230
+ const backendType = await resolveResumeLookupBackend(record?.backend_type, {
231
+ ...options,
747
232
  configFilePath,
233
+ allowCliList,
748
234
  });
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
- );
235
+ const recordSessionId = normalizeSessionId(record?.session_id);
236
+ const projectHash = md5Hex(projectPath);
237
+ if (
238
+ recordSessionId === sessionId &&
239
+ (backendType === "kimi" || !backendType) &&
240
+ projectHash === worktreeHash &&
241
+ !bySessionId.includes(projectPath)
242
+ ) {
243
+ bySessionId.push(projectPath);
845
244
  }
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
- }
245
+ if (projectHash === worktreeHash && !byHash.includes(projectPath)) {
246
+ byHash.push(projectPath);
871
247
  }
872
248
  }
873
- }
874
249
 
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}`);
250
+ if (bySessionId.length > 0) {
251
+ return bySessionId[0];
887
252
  }
888
- if (!(await isExistingDirectory(cwd))) {
889
- throw new Error(`Resume workspace path does not exist: ${cwd}`);
253
+ if (byHash.length === 1) {
254
+ return byHash[0];
890
255
  }
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
-
907
- function normalizeProjectPathCandidate(value) {
908
- return typeof value === "string" && value.trim() ? value.trim() : "";
909
- }
910
-
911
- function normalizeConductorRecordSourcePath(value) {
912
- return typeof value === "string" && value.trim() ? value.trim() : null;
256
+ return null;
913
257
  }
914
258
 
915
- async function resolveExternalResumeContext(backend, sessionId, options = {}) {
259
+ async function resolveExternalResumeContextFromConductorRecords(backend, sessionId, options = {}) {
916
260
  const configFilePath = resolveConfigFilePath(options);
917
261
  const allowCliList =
918
262
  options.allowCliList && typeof options.allowCliList === "object"
@@ -923,7 +267,7 @@ async function resolveExternalResumeContext(backend, sessionId, options = {}) {
923
267
  configFilePath,
924
268
  allowCliList,
925
269
  });
926
- if (!normalizedBackend || resumeProviderForBackend(normalizedBackend)) {
270
+ if (!normalizedBackend || resumeProviderForBackendFromSdk(normalizedBackend)) {
927
271
  return null;
928
272
  }
929
273
  if (!(await isRuntimeSupportedBackend(normalizedBackend, { configFilePath }))) {
@@ -1005,185 +349,84 @@ async function resolveExternalResumeContext(backend, sessionId, options = {}) {
1005
349
  throw new Error(`Could not resolve workspace for backend "${normalizedBackend}" session ${sessionId}`);
1006
350
  }
1007
351
 
1008
- async function resolveKimiResumeCwd(sessionPath, sessionId, options = {}) {
1009
- const sessionDirectory = typeof sessionPath === "string" ? sessionPath.trim() : "";
1010
- if (!sessionDirectory) {
1011
- return null;
1012
- }
1013
-
1014
- const worktreeHash = path.basename(path.dirname(sessionDirectory));
1015
- if (!worktreeHash) {
1016
- return null;
1017
- }
1018
-
1019
- for (const candidate of listCandidateWorkingDirectories(options)) {
1020
- if (md5Hex(candidate) === worktreeHash) {
1021
- return candidate;
1022
- }
1023
- }
352
+ // ---------------------------------------------------------------------------
353
+ // Public API thin facade over `@love-moon/ai-sdk` resume.
354
+ // ---------------------------------------------------------------------------
1024
355
 
1025
- const records = await loadConductorSessionRecords(options);
1026
- const configFilePath = resolveConfigFilePath(options);
1027
- const allowCliList = await loadConfiguredAllowCliList({ ...options, configFilePath });
1028
- const bySessionId = [];
1029
- const byHash = [];
1030
-
1031
- for (const record of records) {
1032
- const projectPath = normalizeProjectPathCandidate(record?.project_path);
1033
- if (!projectPath) {
1034
- continue;
1035
- }
1036
- const backendType = await resolveResumeLookupBackend(record?.backend_type, {
1037
- ...options,
1038
- configFilePath,
1039
- allowCliList,
1040
- });
1041
- const recordSessionId = normalizeSessionId(record?.session_id);
1042
- const projectHash = md5Hex(projectPath);
1043
- if (
1044
- recordSessionId === sessionId &&
1045
- (backendType === "kimi" || !backendType) &&
1046
- projectHash === worktreeHash &&
1047
- !bySessionId.includes(projectPath)
1048
- ) {
1049
- bySessionId.push(projectPath);
1050
- }
1051
- if (projectHash === worktreeHash && !byHash.includes(projectPath)) {
1052
- byHash.push(projectPath);
1053
- }
1054
- }
1055
-
1056
- if (bySessionId.length > 0) {
1057
- return bySessionId[0];
1058
- }
1059
- if (byHash.length === 1) {
1060
- return byHash[0];
1061
- }
1062
- return null;
356
+ export function buildResumeArgsForBackend(backend, sessionId) {
357
+ return buildResumeArgsForBackendFromSdk(backend, sessionId);
1063
358
  }
1064
359
 
1065
- async function extractResumeCwdFromSession(provider, sessionPath, sessionId, options = {}) {
1066
- if (provider === "codex") {
1067
- return extractCodexResumeCwd(sessionPath);
1068
- }
1069
- if (provider === "claude") {
1070
- return extractClaudeResumeCwd(sessionPath, sessionId);
1071
- }
1072
- if (provider === "kimi") {
1073
- return resolveKimiResumeCwd(sessionPath, sessionId, options);
1074
- }
1075
- return null;
360
+ export function resumeProviderForBackend(backend) {
361
+ return resumeProviderForBackendFromSdk(backend);
1076
362
  }
1077
363
 
1078
- async function findCodexSessionFile(rootDir, sessionId) {
1079
- const queue = [rootDir];
1080
- while (queue.length) {
1081
- const current = queue.pop();
1082
- let entries = [];
1083
- try {
1084
- entries = await fsp.readdir(current, { withFileTypes: true });
1085
- } catch {
1086
- continue;
1087
- }
1088
- for (const entry of entries) {
1089
- const fullPath = path.join(current, entry.name);
1090
- if (entry.isDirectory()) {
1091
- queue.push(fullPath);
1092
- } else if (entry.isFile() && entry.name.includes(sessionId) && entry.name.endsWith(".jsonl")) {
1093
- return fullPath;
1094
- }
1095
- }
1096
- }
1097
- return null;
364
+ export async function findSessionPath(provider, sessionId, options = {}) {
365
+ return findSessionPathFromSdk(provider, sessionId, options);
1098
366
  }
1099
367
 
1100
- async function findClaudeSessionEntries(projectsDir, sessionId) {
1101
- const entries = [];
1102
- let projectDirs = [];
1103
- try {
1104
- projectDirs = await fsp.readdir(projectsDir, { withFileTypes: true });
1105
- } catch {
1106
- return entries;
1107
- }
1108
-
1109
- for (const projectDir of projectDirs) {
1110
- if (!projectDir.isDirectory()) {
1111
- continue;
1112
- }
1113
- const projectPath = path.join(projectsDir, projectDir.name);
1114
- let files = [];
1115
- try {
1116
- files = await fsp.readdir(projectPath, { withFileTypes: true });
1117
- } catch {
1118
- continue;
1119
- }
368
+ export async function findCodexSessionPath(sessionId, options = {}) {
369
+ return findSessionPathFromSdk("codex", sessionId, options);
370
+ }
1120
371
 
1121
- for (const file of files) {
1122
- if (!file.isFile()) {
1123
- continue;
1124
- }
1125
- if (!file.name.endsWith(".jsonl") || file.name.startsWith("agent-")) {
1126
- continue;
1127
- }
372
+ export async function findClaudeSessionPath(sessionId, options = {}) {
373
+ return findSessionPathFromSdk("claude", sessionId, options);
374
+ }
1128
375
 
1129
- const filePath = path.join(projectPath, file.name);
1130
- const rl = readline.createInterface({
1131
- input: fs.createReadStream(filePath),
1132
- crlfDelay: Infinity,
1133
- });
376
+ export async function findKimiSessionPath(sessionId, options = {}) {
377
+ return findSessionPathFromSdk("kimi", sessionId, options);
378
+ }
1134
379
 
1135
- for await (const line of rl) {
1136
- const trimmed = line.trim();
1137
- if (!trimmed) {
1138
- continue;
1139
- }
1140
- let entry;
1141
- try {
1142
- entry = JSON.parse(trimmed);
1143
- } catch {
1144
- continue;
1145
- }
1146
- if (entry.sessionId === sessionId) {
1147
- entries.push({ ...entry, source: filePath });
1148
- }
1149
- }
1150
- }
1151
- }
380
+ export async function resolveSessionRunDirectory(sessionPath) {
381
+ return resolveSessionRunDirectoryFromSdk(sessionPath);
382
+ }
1152
383
 
1153
- return entries;
384
+ export async function inspectResumeTarget(backend, sessionId, options = {}) {
385
+ return resolveResumeContext(backend, sessionId, options);
1154
386
  }
1155
387
 
1156
- async function findKimiSessionDirectory(rootDir, sessionId) {
1157
- let hashDirs = [];
1158
- try {
1159
- hashDirs = await fsp.readdir(rootDir, { withFileTypes: true });
1160
- } catch {
1161
- return null;
388
+ export async function resolveResumeContext(backend, sessionId, options = {}) {
389
+ const normalizedSessionId = normalizeSessionId(sessionId);
390
+ if (!normalizedSessionId) {
391
+ throw new Error("--resume requires a session id");
1162
392
  }
393
+ const configFilePath = resolveConfigFilePath(options);
394
+ const allowCliList =
395
+ options.allowCliList && typeof options.allowCliList === "object"
396
+ ? options.allowCliList
397
+ : await loadConfiguredAllowCliList({ ...options, configFilePath });
398
+ const lookupBackend = await resolveResumeLookupBackend(backend, {
399
+ ...options,
400
+ configFilePath,
401
+ allowCliList,
402
+ });
403
+ const effectiveBackend = lookupBackend || backend;
404
+ const provider = resumeProviderForBackendFromSdk(effectiveBackend);
1163
405
 
1164
- for (const hashDir of hashDirs) {
1165
- if (!hashDir.isDirectory()) {
1166
- continue;
1167
- }
1168
- const candidateDir = path.join(rootDir, hashDir.name, sessionId);
1169
- if (await pathExists(candidateDir, "directory")) {
1170
- return candidateDir;
406
+ if (!provider) {
407
+ const externalContext = await resolveExternalResumeContextFromConductorRecords(backend, normalizedSessionId, {
408
+ ...options,
409
+ configFilePath,
410
+ allowCliList,
411
+ });
412
+ if (externalContext) {
413
+ return externalContext;
1171
414
  }
415
+ throw new Error(`--resume is not supported for backend "${backend}"`);
1172
416
  }
1173
- return null;
1174
- }
1175
417
 
1176
- async function pathExists(targetPath, expectedType) {
1177
- try {
1178
- const stats = await fsp.stat(targetPath);
1179
- if (expectedType === "file") {
1180
- return stats.isFile();
1181
- }
1182
- if (expectedType === "directory") {
1183
- return stats.isDirectory();
1184
- }
1185
- return true;
1186
- } catch {
1187
- return false;
418
+ const sdkOptions = {
419
+ ...options,
420
+ configFilePath,
421
+ allowCliList,
422
+ backend,
423
+ runtimeBackend: effectiveBackend,
424
+ };
425
+
426
+ if (provider === "kimi" && typeof sdkOptions.lookupWorkspaceByHash !== "function") {
427
+ sdkOptions.lookupWorkspaceByHash = async (worktreeHash, ctx = {}) =>
428
+ kimiWorkspaceLookupFromConductorRecords(worktreeHash, options, ctx.sessionId || normalizedSessionId);
1188
429
  }
430
+
431
+ return resolveResumeContextFromSdk(effectiveBackend, normalizedSessionId, sdkOptions);
1189
432
  }