@love-moon/conductor-cli 0.2.32 → 0.2.33

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.
@@ -7,9 +7,11 @@ import crypto from "node:crypto";
7
7
 
8
8
  import yaml from "js-yaml";
9
9
  import {
10
+ filterRuntimeSupportedAllowCliList,
10
11
  getExternalRuntimeBackendDescriptor,
11
12
  isRuntimeSupportedBackend,
12
13
  normalizeRuntimeBackendAlias,
14
+ resolveConfiguredRuntimeBackend,
13
15
  } from "../runtime-backends.js";
14
16
 
15
17
  function normalizeBackend(backend) {
@@ -27,6 +29,18 @@ function normalizeSessionId(sessionId) {
27
29
  return typeof sessionId === "string" ? sessionId.trim() : "";
28
30
  }
29
31
 
32
+ function resolveConfigFilePath(options = {}) {
33
+ const configuredPath =
34
+ typeof options?.configFilePath === "string" && options.configFilePath.trim()
35
+ ? options.configFilePath.trim()
36
+ : typeof process.env.CONDUCTOR_CONFIG === "string" && process.env.CONDUCTOR_CONFIG.trim()
37
+ ? process.env.CONDUCTOR_CONFIG.trim()
38
+ : "";
39
+ return configuredPath
40
+ ? path.resolve(configuredPath)
41
+ : path.join(resolveHomeDir(options), ".conductor", "config.yaml");
42
+ }
43
+
30
44
  export function buildResumeArgsForBackend(backend, sessionId) {
31
45
  const resumeSessionId = normalizeSessionId(sessionId);
32
46
  if (!resumeSessionId) {
@@ -442,6 +456,40 @@ async function loadConductorSessionRecords(options = {}) {
442
456
  return records;
443
457
  }
444
458
 
459
+ async function loadConfiguredAllowCliList(options = {}) {
460
+ const configFilePath = resolveConfigFilePath(options);
461
+ let parsed = null;
462
+ try {
463
+ const content = await fsp.readFile(configFilePath, "utf8");
464
+ parsed = yaml.load(content);
465
+ } catch {
466
+ return {};
467
+ }
468
+ if (!parsed || typeof parsed !== "object" || !parsed.allow_cli_list || typeof parsed.allow_cli_list !== "object") {
469
+ return {};
470
+ }
471
+ return filterRuntimeSupportedAllowCliList(parsed.allow_cli_list, { configFilePath });
472
+ }
473
+
474
+ async function resolveResumeLookupBackend(backend, options = {}) {
475
+ const normalizedBackend = normalizeBackend(backend);
476
+ if (!normalizedBackend) {
477
+ return "";
478
+ }
479
+ const configFilePath = resolveConfigFilePath(options);
480
+ const allowCliList =
481
+ options.allowCliList && typeof options.allowCliList === "object"
482
+ ? options.allowCliList
483
+ : await loadConfiguredAllowCliList({ ...options, configFilePath });
484
+ const configuredBackend = await resolveConfiguredRuntimeBackend(normalizedBackend, allowCliList, {
485
+ configFilePath,
486
+ });
487
+ if (configuredBackend?.runtimeBackend) {
488
+ return configuredBackend.runtimeBackend;
489
+ }
490
+ return normalizeRuntimeBackendAlias(normalizedBackend, { configFilePath });
491
+ }
492
+
445
493
  function normalizeProjectPathCandidate(value) {
446
494
  return typeof value === "string" && value.trim() ? value.trim() : "";
447
495
  }
@@ -451,11 +499,13 @@ function normalizeConductorRecordSourcePath(value) {
451
499
  }
452
500
 
453
501
  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 });
502
+ const configFilePath = resolveConfigFilePath(options);
503
+ const allowCliList = await loadConfiguredAllowCliList({ ...options, configFilePath });
504
+ const normalizedBackend = await resolveResumeLookupBackend(backend, {
505
+ ...options,
506
+ configFilePath,
507
+ allowCliList,
508
+ });
459
509
  if (!normalizedBackend || resumeProviderForBackend(normalizedBackend)) {
460
510
  return null;
461
511
  }
@@ -495,7 +545,11 @@ async function resolveExternalResumeContext(backend, sessionId, options = {}) {
495
545
  const records = await loadConductorSessionRecords(options);
496
546
  for (const record of records) {
497
547
  const recordSessionId = normalizeSessionId(record?.session_id);
498
- const recordBackend = normalizeBackend(record?.backend_type);
548
+ const recordBackend = await resolveResumeLookupBackend(record?.backend_type, {
549
+ ...options,
550
+ configFilePath,
551
+ allowCliList,
552
+ });
499
553
  const projectPath = normalizeProjectPathCandidate(record?.project_path);
500
554
  if (recordSessionId !== sessionId || recordBackend !== normalizedBackend || !projectPath) {
501
555
  continue;
@@ -552,6 +606,8 @@ async function resolveKimiResumeCwd(sessionPath, sessionId, options = {}) {
552
606
  }
553
607
 
554
608
  const records = await loadConductorSessionRecords(options);
609
+ const configFilePath = resolveConfigFilePath(options);
610
+ const allowCliList = await loadConfiguredAllowCliList({ ...options, configFilePath });
555
611
  const bySessionId = [];
556
612
  const byHash = [];
557
613
 
@@ -560,7 +616,11 @@ async function resolveKimiResumeCwd(sessionPath, sessionId, options = {}) {
560
616
  if (!projectPath) {
561
617
  continue;
562
618
  }
563
- const backendType = normalizeBackend(record?.backend_type);
619
+ const backendType = await resolveResumeLookupBackend(record?.backend_type, {
620
+ ...options,
621
+ configFilePath,
622
+ allowCliList,
623
+ });
564
624
  const recordSessionId = normalizeSessionId(record?.session_id);
565
625
  const projectHash = md5Hex(projectPath);
566
626
  if (
@@ -33,6 +33,181 @@ function normalizeRuntimeBackendName(backend) {
33
33
  return String(backend || "").trim().toLowerCase();
34
34
  }
35
35
 
36
+ function stripExecutableSuffix(name) {
37
+ return String(name || "")
38
+ .trim()
39
+ .toLowerCase()
40
+ .replace(/\.(cmd|bat|exe)$/i, "");
41
+ }
42
+
43
+ function parseCommandParts(commandLine) {
44
+ const input = String(commandLine || "").trim();
45
+ if (!input) {
46
+ return { command: "", args: [], parts: [] };
47
+ }
48
+
49
+ const parts = [];
50
+ let current = "";
51
+ let quote = "";
52
+ let escaping = false;
53
+ let tokenStarted = false;
54
+
55
+ for (const char of input) {
56
+ if (escaping) {
57
+ current += char;
58
+ tokenStarted = true;
59
+ escaping = false;
60
+ continue;
61
+ }
62
+
63
+ if (char === "\\") {
64
+ escaping = true;
65
+ tokenStarted = true;
66
+ continue;
67
+ }
68
+
69
+ if (quote) {
70
+ if (char === quote) {
71
+ quote = "";
72
+ } else {
73
+ current += char;
74
+ }
75
+ tokenStarted = true;
76
+ continue;
77
+ }
78
+
79
+ if (char === "'" || char === "\"") {
80
+ quote = char;
81
+ tokenStarted = true;
82
+ continue;
83
+ }
84
+
85
+ if (/\s/.test(char)) {
86
+ if (tokenStarted) {
87
+ parts.push(current);
88
+ current = "";
89
+ tokenStarted = false;
90
+ }
91
+ continue;
92
+ }
93
+
94
+ current += char;
95
+ tokenStarted = true;
96
+ }
97
+
98
+ if (tokenStarted) {
99
+ parts.push(current);
100
+ }
101
+
102
+ return {
103
+ command: parts[0] || "",
104
+ args: parts.slice(1),
105
+ parts,
106
+ };
107
+ }
108
+
109
+ function isEnvironmentAssignment(token) {
110
+ return /^[A-Za-z_][A-Za-z0-9_]*=/.test(String(token || "").trim());
111
+ }
112
+
113
+ function collectExecutableCandidates(commandLine) {
114
+ const { parts } = parseCommandParts(commandLine);
115
+ if (!parts.length) {
116
+ return [];
117
+ }
118
+
119
+ const candidates = [];
120
+ const pushCandidate = (token) => {
121
+ const executable = stripExecutableSuffix(path.basename(String(token || "").trim()));
122
+ if (!executable || candidates.includes(executable)) {
123
+ return;
124
+ }
125
+ candidates.push(executable);
126
+ };
127
+
128
+ const findFirstCommandTokenIndex = (startIndex, { skipEnvAssignments = false } = {}) => {
129
+ for (let index = startIndex; index < parts.length; index += 1) {
130
+ const token = String(parts[index] || "").trim();
131
+ if (!token || token === "--") {
132
+ continue;
133
+ }
134
+ if (skipEnvAssignments && isEnvironmentAssignment(token)) {
135
+ continue;
136
+ }
137
+ if (token.startsWith("-")) {
138
+ continue;
139
+ }
140
+ return index;
141
+ }
142
+ return -1;
143
+ };
144
+
145
+ const walkCommand = (startIndex, { skipEnvAssignments = false } = {}) => {
146
+ const tokenIndex = findFirstCommandTokenIndex(startIndex, { skipEnvAssignments });
147
+ if (tokenIndex === -1) {
148
+ return;
149
+ }
150
+
151
+ const token = parts[tokenIndex];
152
+ pushCandidate(token);
153
+
154
+ const executable = stripExecutableSuffix(path.basename(token));
155
+ if (executable === "env") {
156
+ walkCommand(tokenIndex + 1, { skipEnvAssignments: true });
157
+ return;
158
+ }
159
+
160
+ if (executable === "npx" || executable === "pnpx" || executable === "bunx") {
161
+ walkCommand(tokenIndex + 1);
162
+ return;
163
+ }
164
+
165
+ if (executable === "pnpm" || executable === "yarn" || executable === "npm") {
166
+ const subcommandIndex = findFirstCommandTokenIndex(tokenIndex + 1);
167
+ if (subcommandIndex === -1) {
168
+ return;
169
+ }
170
+ const normalizedSubcommand = stripExecutableSuffix(path.basename(parts[subcommandIndex]));
171
+ if (normalizedSubcommand === "exec" || normalizedSubcommand === "dlx") {
172
+ walkCommand(subcommandIndex + 1);
173
+ }
174
+ }
175
+ };
176
+
177
+ walkCommand(0);
178
+
179
+ return candidates;
180
+ }
181
+
182
+ export function inferBuiltInRuntimeBackendFromCommand(commandLine) {
183
+ const candidates = collectExecutableCandidates(commandLine);
184
+ for (const executable of candidates) {
185
+ if (BUILT_IN_RUNTIME_BACKEND_SET.has(executable)) {
186
+ return executable;
187
+ }
188
+ }
189
+ return "";
190
+ }
191
+
192
+ async function inferRuntimeBackendFromCommand(commandLine, options = {}) {
193
+ const builtInBackend = inferBuiltInRuntimeBackendFromCommand(commandLine);
194
+ if (builtInBackend) {
195
+ return builtInBackend;
196
+ }
197
+ const candidates = collectExecutableCandidates(commandLine);
198
+ for (const executable of candidates) {
199
+ const resolvedBackend = await normalizeRuntimeBackendAlias(executable, options);
200
+ if (await isRuntimeSupportedBackend(resolvedBackend, options)) {
201
+ return resolvedBackend;
202
+ }
203
+ }
204
+ return "";
205
+ }
206
+
207
+ export function isBuiltInRuntimeBackend(backend) {
208
+ return BUILT_IN_RUNTIME_BACKEND_SET.has(normalizeRuntimeBackendName(backend));
209
+ }
210
+
36
211
  function readConfigEnvValue(configFilePath, key) {
37
212
  const targetPath =
38
213
  typeof configFilePath === "string" && configFilePath.trim()
@@ -227,8 +402,8 @@ export async function filterRuntimeSupportedAllowCliList(allowCliList, options =
227
402
  }
228
403
  const filtered = {};
229
404
  for (const [backend, command] of Object.entries(allowCliList)) {
230
- const normalizedBackend = await normalizeRuntimeBackendAlias(backend, options);
231
- if (!(await isRuntimeSupportedBackend(normalizedBackend, options))) {
405
+ const normalizedBackend = normalizeRuntimeBackendName(backend);
406
+ if (!normalizedBackend || LEGACY_RUNTIME_BACKEND_ALIASES.has(normalizedBackend)) {
232
407
  continue;
233
408
  }
234
409
  if (typeof command !== "string" || !command.trim()) {
@@ -237,11 +412,139 @@ export async function filterRuntimeSupportedAllowCliList(allowCliList, options =
237
412
  if (filtered[normalizedBackend] !== undefined) {
238
413
  continue;
239
414
  }
240
- filtered[normalizedBackend] = command.trim();
415
+ try {
416
+ const inferredRuntimeBackend = await inferRuntimeBackendFromCommand(command, options);
417
+ if (inferredRuntimeBackend) {
418
+ filtered[normalizedBackend] = command.trim();
419
+ continue;
420
+ }
421
+ } catch {
422
+ // Ignore external provider discovery failures here so built-in aliases inferred from
423
+ // the command line can still pass through independently.
424
+ }
425
+ try {
426
+ const resolvedBackend = await normalizeRuntimeBackendAlias(normalizedBackend, options);
427
+ const isSupportedResolvedBackend = await isRuntimeSupportedBackend(resolvedBackend, options);
428
+ if (!isSupportedResolvedBackend) {
429
+ continue;
430
+ }
431
+ filtered[normalizedBackend] = command.trim();
432
+ } catch {
433
+ // Skip broken external entries here; callers that actually need external backends
434
+ // can surface discovery failures from the advertised-backend path.
435
+ }
241
436
  }
242
437
  return filtered;
243
438
  }
244
439
 
440
+ export async function resolveConfiguredRuntimeBackend(backend, allowCliList, options = {}) {
441
+ const normalizedBackend = normalizeRuntimeBackendName(backend);
442
+ if (!normalizedBackend || LEGACY_RUNTIME_BACKEND_ALIASES.has(normalizedBackend)) {
443
+ return null;
444
+ }
445
+
446
+ const configuredCommand =
447
+ allowCliList && typeof allowCliList === "object" && typeof allowCliList[normalizedBackend] === "string"
448
+ ? allowCliList[normalizedBackend].trim()
449
+ : "";
450
+ const hasConfiguredEntry = Boolean(configuredCommand);
451
+ if (hasConfiguredEntry) {
452
+ const inferredRuntimeBackend = await inferRuntimeBackendFromCommand(configuredCommand, options);
453
+ if (inferredRuntimeBackend) {
454
+ return {
455
+ requestedBackend: normalizedBackend,
456
+ runtimeBackend: inferredRuntimeBackend,
457
+ commandLine: configuredCommand,
458
+ };
459
+ }
460
+ }
461
+ const resolvedBackend = await normalizeRuntimeBackendAlias(normalizedBackend, options);
462
+ if (hasConfiguredEntry && await isRuntimeSupportedBackend(resolvedBackend, options)) {
463
+ return {
464
+ requestedBackend: normalizedBackend,
465
+ runtimeBackend: resolvedBackend,
466
+ commandLine: configuredCommand,
467
+ };
468
+ }
469
+
470
+ if (!hasConfiguredEntry && !isBuiltInRuntimeBackend(resolvedBackend) && await isRuntimeSupportedBackend(resolvedBackend, options)) {
471
+ return {
472
+ requestedBackend: normalizedBackend,
473
+ runtimeBackend: resolvedBackend,
474
+ commandLine: "",
475
+ };
476
+ }
477
+ return null;
478
+ }
479
+
480
+ export async function listAdvertisedBackends(allowCliList, options = {}) {
481
+ const filteredAllowCliList =
482
+ allowCliList && typeof allowCliList === "object"
483
+ ? Object.fromEntries(
484
+ Object.entries(allowCliList)
485
+ .map(([backend, command]) => [normalizeRuntimeBackendName(backend), typeof command === "string" ? command.trim() : ""])
486
+ .filter(([backend, command]) => backend && command),
487
+ )
488
+ : {};
489
+ const configuredBackends = Object.keys(filteredAllowCliList);
490
+ let runtimeBackends = [...BUILT_IN_RUNTIME_BACKENDS];
491
+ let discoveryError = null;
492
+ try {
493
+ runtimeBackends = await listRuntimeSupportedBackends(options);
494
+ } catch (error) {
495
+ discoveryError = error;
496
+ }
497
+ const discoveredExternalBackends = runtimeBackends.filter((backend) => !BUILT_IN_RUNTIME_BACKEND_SET.has(backend));
498
+ const advertisedConfiguredBackends = [];
499
+ const runtimeBackendMap = {};
500
+
501
+ for (const backend of configuredBackends) {
502
+ try {
503
+ const configuredBackend = await resolveConfiguredRuntimeBackend(backend, filteredAllowCliList, options);
504
+ if (!configuredBackend?.runtimeBackend) {
505
+ continue;
506
+ }
507
+ advertisedConfiguredBackends.push(backend);
508
+ runtimeBackendMap[backend] = configuredBackend.runtimeBackend;
509
+ } catch (error) {
510
+ discoveryError ||= error;
511
+ }
512
+ }
513
+
514
+ const explicitlyConfiguredBackends = new Set(advertisedConfiguredBackends);
515
+ const shadowedExternalBackends = new Set();
516
+
517
+ for (const backend of advertisedConfiguredBackends) {
518
+ const runtimeBackend = runtimeBackendMap[backend] || "";
519
+ if (
520
+ !runtimeBackend ||
521
+ isBuiltInRuntimeBackend(runtimeBackend) ||
522
+ runtimeBackend === backend ||
523
+ explicitlyConfiguredBackends.has(runtimeBackend)
524
+ ) {
525
+ continue;
526
+ }
527
+ shadowedExternalBackends.add(runtimeBackend);
528
+ }
529
+
530
+ const externalBackends = discoveredExternalBackends.filter(
531
+ (backend) => !shadowedExternalBackends.has(backend) && !explicitlyConfiguredBackends.has(backend),
532
+ );
533
+ const supportedBackends = [...new Set([...advertisedConfiguredBackends, ...externalBackends])];
534
+
535
+ for (const backend of externalBackends) {
536
+ runtimeBackendMap[backend] = backend;
537
+ }
538
+
539
+ return {
540
+ configuredBackends: advertisedConfiguredBackends,
541
+ externalBackends,
542
+ supportedBackends,
543
+ runtimeBackendMap,
544
+ discoveryError,
545
+ };
546
+ }
547
+
245
548
  export { BUILT_IN_RUNTIME_BACKENDS as RUNTIME_SUPPORTED_BACKENDS, normalizeRuntimeBackendName };
246
549
 
247
550
  export function resetRuntimeBackendCacheForTests() {