@love-moon/conductor-cli 0.2.29 → 0.2.31

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) {
@@ -315,8 +315,16 @@ export async function repairAndVerifyGlobalNodePty({
315
315
  await ensurePnpmOnlyBuiltDependencies({ runCommand, dependencies, global: true });
316
316
  }
317
317
 
318
+ const packageDirectory = await resolveGlobalPackageDirectory({
319
+ packageManager,
320
+ packageName,
321
+ runCommand,
322
+ });
323
+
318
324
  if (packageManager === "pnpm") {
319
- const rebuildResult = await runCommand("pnpm", ["rebuild", "-g", ...dependencies]);
325
+ const rebuildResult = await runCommand("pnpm", ["rebuild", ...dependencies], {
326
+ cwd: packageDirectory,
327
+ });
320
328
  if (!rebuildResult.success) {
321
329
  throw new Error(
322
330
  `pnpm rebuild failed: ${String(rebuildResult.stderr || rebuildResult.stdout || "unknown error").trim()}`,
@@ -336,12 +344,6 @@ export async function repairAndVerifyGlobalNodePty({
336
344
  );
337
345
  }
338
346
  }
339
-
340
- const packageDirectory = await resolveGlobalPackageDirectory({
341
- packageManager,
342
- packageName,
343
- runCommand,
344
- });
345
347
  await verifyNodePtyForPackageDirectory({
346
348
  packageDirectory,
347
349
  runCommand,
@@ -1,22 +1,226 @@
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
+ async function loadExternalRuntimeCatalog(providerPathEnv) {
130
+ const catalog = createEmptyExternalCatalog();
131
+ for (const modulePath of listProviderModulePaths(providerPathEnv)) {
132
+ let importedModule;
133
+ try {
134
+ importedModule = await importExternalProviderModule(modulePath);
135
+ } catch (error) {
136
+ throw new Error(`Failed to load external AI SDK provider module ${modulePath}: ${error?.message || error}`);
137
+ }
138
+ const providers = Array.isArray(importedModule?.providers) ? importedModule.providers : [];
139
+ if (providers.length === 0) {
140
+ throw new Error(`External AI SDK provider module ${modulePath} must export a non-empty providers array.`);
141
+ }
142
+ for (const rawDescriptor of providers) {
143
+ const descriptor = validateDescriptor(rawDescriptor, modulePath);
144
+ if (catalog.backendSet.has(descriptor.backend)) {
145
+ throw new Error(
146
+ `External AI SDK provider backend "${descriptor.backend}" is declared more than once (latest: ${modulePath}).`,
147
+ );
148
+ }
149
+ catalog.descriptors.push(descriptor);
150
+ catalog.backends.push(descriptor.backend);
151
+ catalog.backendSet.add(descriptor.backend);
152
+ registerExternalAlias(catalog, descriptor.backend, descriptor.backend, modulePath);
153
+ for (const alias of descriptor.aliases) {
154
+ registerExternalAlias(catalog, alias, descriptor.backend, modulePath);
155
+ }
156
+ }
157
+ }
158
+ return catalog;
159
+ }
160
+
161
+ async function getExternalRuntimeCatalog(options = {}) {
162
+ const providerPathEnv = resolveProviderPathEnv(options);
163
+ if (!externalRuntimeCatalogPromises.has(providerPathEnv)) {
164
+ const loadPromise = loadExternalRuntimeCatalog(providerPathEnv).catch((error) => {
165
+ externalRuntimeCatalogPromises.delete(providerPathEnv);
166
+ throw error;
167
+ });
168
+ externalRuntimeCatalogPromises.set(providerPathEnv, loadPromise);
169
+ }
170
+ return externalRuntimeCatalogPromises.get(providerPathEnv);
171
+ }
172
+
173
+ export async function normalizeRuntimeBackendAlias(backend, options = {}) {
174
+ const normalized = normalizeRuntimeBackendName(backend);
175
+ if (!normalized) {
176
+ return "";
177
+ }
178
+ if (LEGACY_RUNTIME_BACKEND_ALIASES.has(normalized) || BUILT_IN_RUNTIME_BACKEND_SET.has(normalized)) {
179
+ return normalized;
180
+ }
181
+ const catalog = await getExternalRuntimeCatalog(options);
182
+ return catalog.aliasToBackend.get(normalized) || normalized;
183
+ }
184
+
185
+ export async function listRuntimeSupportedBackends(options = {}) {
186
+ const catalog = await getExternalRuntimeCatalog(options);
187
+ return [...BUILT_IN_RUNTIME_BACKENDS, ...catalog.backends];
188
+ }
189
+
190
+ export async function getExternalRuntimeBackendDescriptor(backend, options = {}) {
191
+ const normalized = await normalizeRuntimeBackendAlias(backend, options);
192
+ if (!normalized || BUILT_IN_RUNTIME_BACKEND_SET.has(normalized) || LEGACY_RUNTIME_BACKEND_ALIASES.has(normalized)) {
193
+ return null;
194
+ }
195
+ const catalog = await getExternalRuntimeCatalog(options);
196
+ return catalog.backendSet.has(normalized)
197
+ ? {
198
+ backend: normalized,
199
+ ...(catalog.descriptors.find((descriptor) => descriptor.backend === normalized) || {}),
200
+ }
201
+ : null;
202
+ }
203
+
204
+ export async function isRuntimeSupportedBackend(backend, options = {}) {
205
+ const normalized = await normalizeRuntimeBackendAlias(backend, options);
206
+ if (BUILT_IN_RUNTIME_BACKEND_SET.has(normalized)) {
207
+ return true;
208
+ }
209
+ if (LEGACY_RUNTIME_BACKEND_ALIASES.has(normalized)) {
210
+ return false;
211
+ }
212
+ const catalog = await getExternalRuntimeCatalog(options);
213
+ return catalog.backendSet.has(normalized);
9
214
  }
10
215
 
11
- export function filterRuntimeSupportedAllowCliList(allowCliList) {
216
+ export async function filterRuntimeSupportedAllowCliList(allowCliList, options = {}) {
12
217
  if (!allowCliList || typeof allowCliList !== "object") {
13
218
  return {};
14
219
  }
15
-
16
220
  const filtered = {};
17
221
  for (const [backend, command] of Object.entries(allowCliList)) {
18
- const normalizedBackend = normalizeRuntimeBackendName(backend);
19
- if (!RUNTIME_SUPPORTED_BACKENDS.includes(normalizedBackend)) {
222
+ const normalizedBackend = await normalizeRuntimeBackendAlias(backend, options);
223
+ if (!(await isRuntimeSupportedBackend(normalizedBackend, options))) {
20
224
  continue;
21
225
  }
22
226
  if (typeof command !== "string" || !command.trim()) {
@@ -25,7 +229,13 @@ export function filterRuntimeSupportedAllowCliList(allowCliList) {
25
229
  if (filtered[normalizedBackend] !== undefined) {
26
230
  continue;
27
231
  }
28
- filtered[normalizedBackend] = command;
232
+ filtered[normalizedBackend] = command.trim();
29
233
  }
30
234
  return filtered;
31
235
  }
236
+
237
+ export { BUILT_IN_RUNTIME_BACKENDS as RUNTIME_SUPPORTED_BACKENDS, normalizeRuntimeBackendName };
238
+
239
+ export function resetRuntimeBackendCacheForTests() {
240
+ externalRuntimeCatalogPromises.clear();
241
+ }