@love-moon/conductor-cli 0.2.30 → 0.2.32

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.
@@ -6,6 +6,11 @@ import readline from "node:readline";
6
6
  import crypto from "node:crypto";
7
7
 
8
8
  import yaml from "js-yaml";
9
+ import {
10
+ getExternalRuntimeBackendDescriptor,
11
+ isRuntimeSupportedBackend,
12
+ normalizeRuntimeBackendAlias,
13
+ } from "../runtime-backends.js";
9
14
 
10
15
  function normalizeBackend(backend) {
11
16
  return String(backend || "").trim().toLowerCase();
@@ -166,6 +171,10 @@ export async function resolveResumeContext(backend, sessionId, options = {}) {
166
171
  }
167
172
  const provider = resumeProviderForBackend(backend);
168
173
  if (!provider) {
174
+ const externalContext = await resolveExternalResumeContext(backend, normalizedSessionId, options);
175
+ if (externalContext) {
176
+ return externalContext;
177
+ }
169
178
  throw new Error(`--resume is not supported for backend "${backend}"`);
170
179
  }
171
180
 
@@ -424,7 +433,10 @@ async function loadConductorSessionRecords(options = {}) {
424
433
  if (!entry || typeof entry !== "object") {
425
434
  continue;
426
435
  }
427
- records.push(entry);
436
+ records.push({
437
+ ...entry,
438
+ __conductorSourcePath: filePath,
439
+ });
428
440
  }
429
441
  }
430
442
  return records;
@@ -434,6 +446,94 @@ function normalizeProjectPathCandidate(value) {
434
446
  return typeof value === "string" && value.trim() ? value.trim() : "";
435
447
  }
436
448
 
449
+ function normalizeConductorRecordSourcePath(value) {
450
+ return typeof value === "string" && value.trim() ? value.trim() : null;
451
+ }
452
+
453
+ async function resolveExternalResumeContext(backend, sessionId, options = {}) {
454
+ const configFilePath =
455
+ typeof options?.configFilePath === "string" && options.configFilePath.trim()
456
+ ? options.configFilePath.trim()
457
+ : undefined;
458
+ const normalizedBackend = await normalizeRuntimeBackendAlias(backend, { configFilePath });
459
+ if (!normalizedBackend || resumeProviderForBackend(normalizedBackend)) {
460
+ return null;
461
+ }
462
+ if (!(await isRuntimeSupportedBackend(normalizedBackend, { configFilePath }))) {
463
+ return null;
464
+ }
465
+
466
+ const descriptor = await getExternalRuntimeBackendDescriptor(normalizedBackend, { configFilePath });
467
+ if (typeof descriptor?.resolveResumeContext === "function") {
468
+ const resolvedContext = await descriptor.resolveResumeContext(sessionId, options);
469
+ const cwd = normalizeProjectPathCandidate(resolvedContext?.cwd);
470
+ if (!cwd) {
471
+ throw new Error(`Could not resolve workspace for backend "${normalizedBackend}" session ${sessionId}`);
472
+ }
473
+ if (!(await isExistingDirectory(cwd))) {
474
+ throw new Error(`Resume workspace path does not exist: ${cwd}`);
475
+ }
476
+ const sessionPath = normalizeProjectPathCandidate(resolvedContext?.sessionPath);
477
+ return {
478
+ provider: normalizedBackend,
479
+ sessionId,
480
+ sessionPath,
481
+ cwd,
482
+ debugMetadata: {
483
+ cwdSource:
484
+ typeof resolvedContext?.debugMetadata?.cwdSource === "string" && resolvedContext.debugMetadata.cwdSource.trim()
485
+ ? resolvedContext.debugMetadata.cwdSource.trim()
486
+ : "provider",
487
+ sessionPath,
488
+ ...(resolvedContext?.debugMetadata && typeof resolvedContext.debugMetadata === "object"
489
+ ? resolvedContext.debugMetadata
490
+ : {}),
491
+ },
492
+ };
493
+ }
494
+
495
+ const records = await loadConductorSessionRecords(options);
496
+ for (const record of records) {
497
+ const recordSessionId = normalizeSessionId(record?.session_id);
498
+ const recordBackend = normalizeBackend(record?.backend_type);
499
+ const projectPath = normalizeProjectPathCandidate(record?.project_path);
500
+ if (recordSessionId !== sessionId || recordBackend !== normalizedBackend || !projectPath) {
501
+ continue;
502
+ }
503
+ if (!(await isExistingDirectory(projectPath))) {
504
+ continue;
505
+ }
506
+ const sessionPath = normalizeConductorRecordSourcePath(record?.__conductorSourcePath);
507
+ return {
508
+ provider: normalizedBackend,
509
+ sessionId,
510
+ sessionPath,
511
+ cwd: projectPath,
512
+ debugMetadata: {
513
+ cwdSource: "conductor_session_record",
514
+ sessionPath,
515
+ },
516
+ };
517
+ }
518
+
519
+ for (const candidate of listCandidateWorkingDirectories(options)) {
520
+ if (await isExistingDirectory(candidate)) {
521
+ return {
522
+ provider: normalizedBackend,
523
+ sessionId,
524
+ sessionPath: null,
525
+ cwd: candidate,
526
+ debugMetadata: {
527
+ cwdSource: "current_working_directory",
528
+ sessionPath: null,
529
+ },
530
+ };
531
+ }
532
+ }
533
+
534
+ throw new Error(`Could not resolve workspace for backend "${normalizedBackend}" session ${sessionId}`);
535
+ }
536
+
437
537
  async function resolveKimiResumeCwd(sessionPath, sessionId, options = {}) {
438
538
  const sessionDirectory = typeof sessionPath === "string" ? sessionPath.trim() : "";
439
539
  if (!sessionDirectory) {
@@ -1,22 +1,234 @@
1
- export const RUNTIME_SUPPORTED_BACKENDS = ["codex", "claude", "kimi", "opencode"];
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
2
4
 
3
- export function normalizeRuntimeBackendName(backend) {
5
+ import yaml from "js-yaml";
6
+
7
+ const BUILT_IN_RUNTIME_BACKENDS = ["codex", "claude", "kimi", "opencode"];
8
+ const BUILT_IN_RUNTIME_BACKEND_SET = new Set(BUILT_IN_RUNTIME_BACKENDS);
9
+ const LEGACY_RUNTIME_BACKEND_ALIASES = new Set([
10
+ "code",
11
+ "claude-code",
12
+ "open-code",
13
+ "open_code",
14
+ "kimi-cli",
15
+ "kimi-code",
16
+ ]);
17
+ const externalRuntimeCatalogPromises = new Map();
18
+ let externalRuntimeImportNonce = 0;
19
+
20
+ function normalizeProviderPathEnv(value) {
21
+ return String(value || "").trim();
22
+ }
23
+
24
+ function listProviderModulePaths(providerPathEnv) {
25
+ const raw = normalizeProviderPathEnv(providerPathEnv);
26
+ if (!raw) {
27
+ return [];
28
+ }
29
+ return [...new Set(raw.split(process.platform === "win32" ? ";" : ":").map((item) => item.trim()).filter(Boolean))];
30
+ }
31
+
32
+ function normalizeRuntimeBackendName(backend) {
4
33
  return String(backend || "").trim().toLowerCase();
5
34
  }
6
35
 
7
- export function isRuntimeSupportedBackend(backend) {
8
- return RUNTIME_SUPPORTED_BACKENDS.includes(normalizeRuntimeBackendName(backend));
36
+ function readConfigEnvValue(configFilePath, key) {
37
+ const targetPath =
38
+ typeof configFilePath === "string" && configFilePath.trim()
39
+ ? path.resolve(configFilePath.trim())
40
+ : path.join(process.env.HOME || "", ".conductor", "config.yaml");
41
+ try {
42
+ if (!targetPath || !fs.existsSync(targetPath)) {
43
+ return "";
44
+ }
45
+ const parsed = yaml.load(fs.readFileSync(targetPath, "utf8"));
46
+ if (!parsed || typeof parsed !== "object") {
47
+ return "";
48
+ }
49
+ const value = parsed?.envs?.[key];
50
+ return typeof value === "string" ? value.trim() : "";
51
+ } catch {
52
+ return "";
53
+ }
54
+ }
55
+
56
+ function resolveProviderPathEnv(options = {}) {
57
+ return (
58
+ normalizeProviderPathEnv(process.env.AISDK_PROVIDER_PATH) ||
59
+ normalizeProviderPathEnv(readConfigEnvValue(options.configFilePath, "AISDK_PROVIDER_PATH"))
60
+ );
61
+ }
62
+
63
+ function createEmptyExternalCatalog() {
64
+ return {
65
+ backends: [],
66
+ backendSet: new Set(),
67
+ aliasToBackend: new Map(),
68
+ descriptors: [],
69
+ };
70
+ }
71
+
72
+ function validateDescriptor(descriptor, sourcePath) {
73
+ if (!descriptor || typeof descriptor !== "object") {
74
+ throw new Error(`External AI SDK provider module ${sourcePath} contains an invalid provider descriptor.`);
75
+ }
76
+ const backend = normalizeRuntimeBackendName(descriptor.backend);
77
+ if (!backend) {
78
+ throw new Error(`External AI SDK provider module ${sourcePath} is missing provider.backend.`);
79
+ }
80
+ if (BUILT_IN_RUNTIME_BACKEND_SET.has(backend)) {
81
+ throw new Error(`External AI SDK provider backend "${backend}" from ${sourcePath} conflicts with a built-in backend.`);
82
+ }
83
+ if (LEGACY_RUNTIME_BACKEND_ALIASES.has(backend)) {
84
+ throw new Error(`External AI SDK provider backend "${backend}" from ${sourcePath} conflicts with a reserved CLI alias.`);
85
+ }
86
+ const variant = String(descriptor.variant || "").trim();
87
+ if (!variant) {
88
+ throw new Error(`External AI SDK provider "${backend}" from ${sourcePath} is missing provider.variant.`);
89
+ }
90
+ if (typeof descriptor.createSession !== "function") {
91
+ throw new Error(`External AI SDK provider "${backend}" from ${sourcePath} is missing provider.createSession().`);
92
+ }
93
+ const aliases = Array.isArray(descriptor.aliases)
94
+ ? descriptor.aliases.map((item) => normalizeRuntimeBackendName(item)).filter(Boolean)
95
+ : [];
96
+ return {
97
+ backend,
98
+ aliases,
99
+ resolveResumeContext: typeof descriptor.resolveResumeContext === "function" ? descriptor.resolveResumeContext : null,
100
+ };
101
+ }
102
+
103
+ async function importExternalProviderModule(modulePath) {
104
+ const resolvedPath = path.isAbsolute(modulePath) ? modulePath : path.resolve(modulePath);
105
+ const moduleUrl = pathToFileURL(resolvedPath);
106
+ moduleUrl.searchParams.set("conductor-external-provider-attempt", String(++externalRuntimeImportNonce));
107
+ return import(moduleUrl.href);
108
+ }
109
+
110
+ function registerExternalAlias(catalog, alias, backend, sourcePath) {
111
+ if (!alias) {
112
+ return;
113
+ }
114
+ if (BUILT_IN_RUNTIME_BACKEND_SET.has(alias)) {
115
+ throw new Error(`External AI SDK provider alias "${alias}" from ${sourcePath} conflicts with a built-in backend.`);
116
+ }
117
+ if (LEGACY_RUNTIME_BACKEND_ALIASES.has(alias)) {
118
+ throw new Error(`External AI SDK provider alias "${alias}" from ${sourcePath} conflicts with a reserved CLI alias.`);
119
+ }
120
+ const existingBackend = catalog.aliasToBackend.get(alias);
121
+ if (existingBackend && existingBackend !== backend) {
122
+ throw new Error(
123
+ `External AI SDK provider alias "${alias}" from ${sourcePath} conflicts with backend "${existingBackend}".`,
124
+ );
125
+ }
126
+ catalog.aliasToBackend.set(alias, backend);
127
+ }
128
+
129
+ function formatExternalProviderLoadError(modulePath, error) {
130
+ const message = error?.message || String(error);
131
+ return [
132
+ `Failed to load external AI SDK provider module ${modulePath}: ${message}`,
133
+ "Help: if this provider comes from a local repo or workspace, did you forget to run pnpm install?",
134
+ ].join(" ");
9
135
  }
10
136
 
11
- export function filterRuntimeSupportedAllowCliList(allowCliList) {
137
+ async function loadExternalRuntimeCatalog(providerPathEnv) {
138
+ const catalog = createEmptyExternalCatalog();
139
+ for (const modulePath of listProviderModulePaths(providerPathEnv)) {
140
+ let importedModule;
141
+ try {
142
+ importedModule = await importExternalProviderModule(modulePath);
143
+ } catch (error) {
144
+ throw new Error(formatExternalProviderLoadError(modulePath, error));
145
+ }
146
+ const providers = Array.isArray(importedModule?.providers) ? importedModule.providers : [];
147
+ if (providers.length === 0) {
148
+ throw new Error(`External AI SDK provider module ${modulePath} must export a non-empty providers array.`);
149
+ }
150
+ for (const rawDescriptor of providers) {
151
+ const descriptor = validateDescriptor(rawDescriptor, modulePath);
152
+ if (catalog.backendSet.has(descriptor.backend)) {
153
+ throw new Error(
154
+ `External AI SDK provider backend "${descriptor.backend}" is declared more than once (latest: ${modulePath}).`,
155
+ );
156
+ }
157
+ catalog.descriptors.push(descriptor);
158
+ catalog.backends.push(descriptor.backend);
159
+ catalog.backendSet.add(descriptor.backend);
160
+ registerExternalAlias(catalog, descriptor.backend, descriptor.backend, modulePath);
161
+ for (const alias of descriptor.aliases) {
162
+ registerExternalAlias(catalog, alias, descriptor.backend, modulePath);
163
+ }
164
+ }
165
+ }
166
+ return catalog;
167
+ }
168
+
169
+ async function getExternalRuntimeCatalog(options = {}) {
170
+ const providerPathEnv = resolveProviderPathEnv(options);
171
+ if (!externalRuntimeCatalogPromises.has(providerPathEnv)) {
172
+ const loadPromise = loadExternalRuntimeCatalog(providerPathEnv).catch((error) => {
173
+ externalRuntimeCatalogPromises.delete(providerPathEnv);
174
+ throw error;
175
+ });
176
+ externalRuntimeCatalogPromises.set(providerPathEnv, loadPromise);
177
+ }
178
+ return externalRuntimeCatalogPromises.get(providerPathEnv);
179
+ }
180
+
181
+ export async function normalizeRuntimeBackendAlias(backend, options = {}) {
182
+ const normalized = normalizeRuntimeBackendName(backend);
183
+ if (!normalized) {
184
+ return "";
185
+ }
186
+ if (LEGACY_RUNTIME_BACKEND_ALIASES.has(normalized) || BUILT_IN_RUNTIME_BACKEND_SET.has(normalized)) {
187
+ return normalized;
188
+ }
189
+ const catalog = await getExternalRuntimeCatalog(options);
190
+ return catalog.aliasToBackend.get(normalized) || normalized;
191
+ }
192
+
193
+ export async function listRuntimeSupportedBackends(options = {}) {
194
+ const catalog = await getExternalRuntimeCatalog(options);
195
+ return [...BUILT_IN_RUNTIME_BACKENDS, ...catalog.backends];
196
+ }
197
+
198
+ export async function getExternalRuntimeBackendDescriptor(backend, options = {}) {
199
+ const normalized = await normalizeRuntimeBackendAlias(backend, options);
200
+ if (!normalized || BUILT_IN_RUNTIME_BACKEND_SET.has(normalized) || LEGACY_RUNTIME_BACKEND_ALIASES.has(normalized)) {
201
+ return null;
202
+ }
203
+ const catalog = await getExternalRuntimeCatalog(options);
204
+ return catalog.backendSet.has(normalized)
205
+ ? {
206
+ backend: normalized,
207
+ ...(catalog.descriptors.find((descriptor) => descriptor.backend === normalized) || {}),
208
+ }
209
+ : null;
210
+ }
211
+
212
+ export async function isRuntimeSupportedBackend(backend, options = {}) {
213
+ const normalized = await normalizeRuntimeBackendAlias(backend, options);
214
+ if (BUILT_IN_RUNTIME_BACKEND_SET.has(normalized)) {
215
+ return true;
216
+ }
217
+ if (LEGACY_RUNTIME_BACKEND_ALIASES.has(normalized)) {
218
+ return false;
219
+ }
220
+ const catalog = await getExternalRuntimeCatalog(options);
221
+ return catalog.backendSet.has(normalized);
222
+ }
223
+
224
+ export async function filterRuntimeSupportedAllowCliList(allowCliList, options = {}) {
12
225
  if (!allowCliList || typeof allowCliList !== "object") {
13
226
  return {};
14
227
  }
15
-
16
228
  const filtered = {};
17
229
  for (const [backend, command] of Object.entries(allowCliList)) {
18
- const normalizedBackend = normalizeRuntimeBackendName(backend);
19
- if (!RUNTIME_SUPPORTED_BACKENDS.includes(normalizedBackend)) {
230
+ const normalizedBackend = await normalizeRuntimeBackendAlias(backend, options);
231
+ if (!(await isRuntimeSupportedBackend(normalizedBackend, options))) {
20
232
  continue;
21
233
  }
22
234
  if (typeof command !== "string" || !command.trim()) {
@@ -25,7 +237,13 @@ export function filterRuntimeSupportedAllowCliList(allowCliList) {
25
237
  if (filtered[normalizedBackend] !== undefined) {
26
238
  continue;
27
239
  }
28
- filtered[normalizedBackend] = command;
240
+ filtered[normalizedBackend] = command.trim();
29
241
  }
30
242
  return filtered;
31
243
  }
244
+
245
+ export { BUILT_IN_RUNTIME_BACKENDS as RUNTIME_SUPPORTED_BACKENDS, normalizeRuntimeBackendName };
246
+
247
+ export function resetRuntimeBackendCacheForTests() {
248
+ externalRuntimeCatalogPromises.clear();
249
+ }