@hmj-ai/cflow 1.1.0 → 1.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,11 +1,12 @@
1
- import { spawn } from 'node:child_process';
2
- import { isAbsolute, resolve } from 'node:path';
1
+ import { createRequire } from 'node:module';
2
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
3
3
  import { Readable, Writable } from 'node:stream';
4
4
  import { client as acpClient, methods, ndJsonStream, PROTOCOL_VERSION, } from '@agentclientprotocol/sdk';
5
5
  import { isWithinDirectory } from './workspace.js';
6
- import { loadAgentManifestRecords, } from './runtime-manifest.js';
7
- import { canChangeWorkspace, discoverAcpCommands, existingAbsoluteDirectory, needsWindowsShell, parseJsonOutput, resolveExecutable, runtimeIdFromCommand, runtimeNameFromCommand, } from './runtime-process.js';
8
- const ADAPTER_BUILD = 'cf-runtime-adapter/3';
6
+ import { adapterDescriptorForId, adapterDescriptorRecord, externalAdapterDescriptors, loadAgentManifestRecords, } from './runtime-manifest.js';
7
+ import { canChangeWorkspace, describeResolvedCommand, discoverAcpCommands, existingAbsoluteDirectory, existingExecutable, launchProcess, parseJsonOutput, resolveCommandAliases, runtimeIdFromCommand, runtimeNameFromCommand, terminateProcess, } from './runtime-process.js';
8
+ const ADAPTER_BUILD = 'cf-runtime-adapter/4';
9
+ const moduleRequire = createRequire(import.meta.url);
9
10
  export class RuntimeExecutionException extends Error {
10
11
  details;
11
12
  constructor(details) {
@@ -77,8 +78,14 @@ const profileFromManifest = (record) => {
77
78
  export const discoverRuntimeProfiles = (options = {}) => {
78
79
  const loaded = loadAgentManifestRecords(options);
79
80
  const declared = loaded.records.map(profileFromManifest);
80
- const declaredCommands = new Set(declared.map((profile) => profile.command));
81
- const generic = discoverAcpCommands(options.projectRoot)
81
+ const external = externalAdapterDescriptors
82
+ .filter((descriptor) => resolveCommandAliases([descriptor.manifest.command, ...(descriptor.commandAliases ?? [])], options))
83
+ .map((descriptor) => profileFromManifest(adapterDescriptorRecord(descriptor, 'path-acp')));
84
+ const declaredCommands = new Set([...declared, ...external].flatMap((profile) => [
85
+ profile.command,
86
+ ...(adapterDescriptorForId(profile.id)?.commandAliases ?? []),
87
+ ]));
88
+ const generic = discoverAcpCommands(options)
82
89
  .filter((command) => !declaredCommands.has(command))
83
90
  .sort()
84
91
  .map((command) => ({
@@ -105,7 +112,7 @@ export const discoverRuntimeProfiles = (options = {}) => {
105
112
  adapterBuild: ADAPTER_BUILD,
106
113
  createdAt: new Date(0).toISOString(),
107
114
  }));
108
- const selected = new Map(generic.map((profile) => [profile.id, profile]));
115
+ const selected = new Map([...generic, ...external].map((profile) => [profile.id, profile]));
109
116
  for (const profile of declared)
110
117
  selected.set(profile.id, profile);
111
118
  return { profiles: [...selected.values()], warnings: loaded.warnings };
@@ -388,7 +395,7 @@ export class RuntimeManager {
388
395
  status: 'unavailable',
389
396
  checkedAt: new Date().toISOString(),
390
397
  latencyMs: Date.now() - started,
391
- stage: this.commandInstalled(profile, true) ? 'installed' : undefined,
398
+ stage: this.commandInstalled(profile) ? 'installed' : undefined,
392
399
  authentication: 'unknown',
393
400
  error: error instanceof Error ? error.message : String(error),
394
401
  };
@@ -467,32 +474,58 @@ export class RuntimeManager {
467
474
  : await Promise.reject(new Error(`RUNTIME_BACKEND_UNSUPPORTED:${profile.backend}`));
468
475
  return profile.outputMode === 'json' ? parseJsonOutput(text) : { content: text.trim() };
469
476
  }
470
- commandInstalled(profile, includeProjectBin = false) {
477
+ commandResolutionOptions(env = process.env) {
478
+ return {
479
+ bundledRoot: this.discoveryOptions.bundledRoot,
480
+ env,
481
+ platform: this.discoveryOptions.platform,
482
+ projectRoot: this.discoveryOptions.projectRoot,
483
+ };
484
+ }
485
+ resolvedCommand(profile, env) {
486
+ if (!profile.command)
487
+ throw new Error('RUNTIME_COMMAND_REQUIRED');
488
+ const descriptor = adapterDescriptorForId(profile.id);
489
+ const resolved = resolveCommandAliases([profile.command, ...(descriptor?.commandAliases ?? [])], this.commandResolutionOptions(env));
490
+ if (!resolved)
491
+ throw new Error(`${profile.backend === 'acp' ? 'ACP_SERVER_NOT_FOUND' : 'AGENT_CLI_NOT_FOUND'}:${profile.command}`);
492
+ return resolved;
493
+ }
494
+ commandInstalled(profile) {
471
495
  if (!profile.command)
472
496
  return false;
473
- return Boolean(resolveExecutable(profile.command, { ...process.env }, {
474
- includeProjectBin,
475
- }));
497
+ try {
498
+ this.resolvedCommand(profile, this.runtimeEnvironment(profile));
499
+ return true;
500
+ }
501
+ catch {
502
+ return false;
503
+ }
504
+ }
505
+ launchSpec(profile, args, cwd, stdio, effects = []) {
506
+ const env = this.runtimeEnvironment(profile, effects);
507
+ return {
508
+ resolved: this.resolvedCommand(profile, env),
509
+ args,
510
+ cwd,
511
+ env,
512
+ stdio,
513
+ };
476
514
  }
477
515
  async probeAcp(profile) {
478
- const command = this.acpCommand(profile);
479
- const child = spawn(command, profile.args, {
480
- cwd: profile.workingDirectory ?? process.cwd(),
481
- env: this.acpEnvironment(profile),
482
- stdio: ['pipe', 'pipe', 'pipe'],
483
- shell: needsWindowsShell(command),
484
- windowsHide: true,
485
- });
516
+ const spec = this.launchSpec(profile, profile.args, profile.workingDirectory ?? process.cwd(), [
517
+ 'pipe',
518
+ 'pipe',
519
+ 'pipe',
520
+ ]);
521
+ const child = launchProcess(spec);
486
522
  let stderr = Buffer.alloc(0);
487
- child.stderr.on('data', (chunk) => {
523
+ child.stderr?.on('data', (chunk) => {
488
524
  stderr = Buffer.concat([stderr, chunk]);
489
525
  if (stderr.length > 8_192)
490
526
  stderr = stderr.subarray(stderr.length - 8_192);
491
527
  });
492
- const terminate = () => {
493
- child.kill('SIGTERM');
494
- setTimeout(() => child.kill('SIGKILL'), 1_000).unref();
495
- };
528
+ const terminate = () => terminateProcess(child);
496
529
  let timeout;
497
530
  try {
498
531
  if (!child.stdin || !child.stdout)
@@ -504,24 +537,29 @@ export class RuntimeManager {
504
537
  clientCapabilities: { plan: {}, session: {} },
505
538
  }));
506
539
  const childError = new Promise((_, reject) => child.once('error', reject));
540
+ const childExit = new Promise((_, reject) => child.once('exit', (code, signal) => reject(new Error(`ACP_SERVER_EXITED:${code ?? signal ?? 'unknown'}`))));
507
541
  const timedOut = new Promise((_, reject) => {
508
542
  timeout = setTimeout(() => {
509
543
  terminate();
510
544
  reject(new Error('ACP_HANDSHAKE_TIMEOUT'));
511
- }, 5_000);
545
+ }, this.discoveryOptions.healthTimeoutMs ?? 5_000);
512
546
  });
513
- const response = await Promise.race([initialize, childError, timedOut]);
547
+ const response = await Promise.race([initialize, childError, childExit, timedOut]);
514
548
  const agent = response.agentInfo
515
549
  ? [response.agentInfo.name, response.agentInfo.version].filter(Boolean).join(' ')
516
550
  : undefined;
517
- return [`ACP ${response.protocolVersion}`, agent, `via ${command}`]
551
+ return [
552
+ `ACP ${response.protocolVersion}`,
553
+ agent,
554
+ `via ${describeResolvedCommand(spec.resolved)}`,
555
+ ]
518
556
  .filter(Boolean)
519
557
  .join(' · ');
520
558
  }
521
559
  catch (error) {
522
560
  const detail = stderr.toString('utf8').trim().slice(-800);
523
- if (error instanceof Error && detail)
524
- throw new Error(`${error.message}:${detail}`);
561
+ if (error instanceof Error)
562
+ throw new Error(`${error.message}${detail ? `:${detail}` : ''} [launch=${describeResolvedCommand(spec.resolved)}]`);
525
563
  throw error;
526
564
  }
527
565
  finally {
@@ -531,25 +569,16 @@ export class RuntimeManager {
531
569
  }
532
570
  }
533
571
  async probeCli(profile) {
534
- const command = this.cliCommand(profile);
572
+ const spec = this.launchSpec(profile, profile.versionArgs, profile.workingDirectory ?? process.cwd(), ['ignore', 'pipe', 'pipe']);
535
573
  if (!profile.versionArgs.length)
536
- return `CLI via ${command}`;
537
- const child = spawn(command, profile.versionArgs, {
538
- cwd: profile.workingDirectory ?? process.cwd(),
539
- env: this.acpEnvironment(profile),
540
- stdio: ['ignore', 'pipe', 'pipe'],
541
- shell: needsWindowsShell(command),
542
- windowsHide: true,
543
- });
574
+ return `CLI via ${describeResolvedCommand(spec.resolved)}`;
575
+ const child = launchProcess(spec);
544
576
  const output = [];
545
577
  const errors = [];
546
- child.stdout.on('data', (chunk) => output.push(chunk));
547
- child.stderr.on('data', (chunk) => errors.push(chunk));
578
+ child.stdout?.on('data', (chunk) => output.push(chunk));
579
+ child.stderr?.on('data', (chunk) => errors.push(chunk));
548
580
  let timeout;
549
- const terminate = () => {
550
- child.kill('SIGTERM');
551
- setTimeout(() => child.kill('SIGKILL'), 1_000).unref();
552
- };
581
+ const terminate = () => terminateProcess(child);
553
582
  try {
554
583
  const result = await new Promise((resolveExit, reject) => {
555
584
  child.once('error', reject);
@@ -557,13 +586,13 @@ export class RuntimeManager {
557
586
  timeout = setTimeout(() => {
558
587
  terminate();
559
588
  reject(new Error('CLI_HEALTHCHECK_TIMEOUT'));
560
- }, 5_000);
589
+ }, this.discoveryOptions.healthTimeoutMs ?? 5_000);
561
590
  });
562
591
  const detail = Buffer.concat(errors).toString('utf8').trim().slice(-800);
563
592
  if (result.code !== 0)
564
- throw new Error(`CLI_HEALTHCHECK_EXITED:${result.code ?? result.signal ?? 'unknown'}${detail ? `:${detail}` : ''}`);
593
+ throw new Error(`CLI_HEALTHCHECK_EXITED:${result.code ?? result.signal ?? 'unknown'}${detail ? `:${detail}` : ''} [launch=${describeResolvedCommand(spec.resolved)}]`);
565
594
  const version = Buffer.concat(output).toString('utf8').trim().split(/\r?\n/, 1)[0];
566
- return [version || profile.name, `via ${command}`].join(' · ');
595
+ return [version || profile.name, `via ${describeResolvedCommand(spec.resolved)}`].join(' · ');
567
596
  }
568
597
  finally {
569
598
  if (timeout)
@@ -571,59 +600,86 @@ export class RuntimeManager {
571
600
  terminate();
572
601
  }
573
602
  }
574
- acpCommand(profile) {
575
- if (!profile.command)
576
- throw new Error('RUNTIME_COMMAND_REQUIRED');
577
- const env = this.acpEnvironment(profile);
578
- const resolved = resolveExecutable(profile.command, env, { includeProjectBin: true });
579
- if (!resolved)
580
- throw new Error(`ACP_SERVER_NOT_FOUND:${profile.command}`);
581
- return resolved;
582
- }
583
- cliCommand(profile) {
584
- if (!profile.command)
585
- throw new Error('RUNTIME_COMMAND_REQUIRED');
586
- const resolved = resolveExecutable(profile.command, this.acpEnvironment(profile));
587
- if (!resolved)
588
- throw new Error(`AGENT_CLI_NOT_FOUND:${profile.command}`);
589
- return resolved;
590
- }
591
- codexPath() {
592
- const env = { ...process.env };
593
- const command = process.env.CFLOW_CODEX_PATH ?? process.env.CODEX_PATH ?? 'codex';
594
- const resolved = resolveExecutable(command, env);
595
- if (!resolved)
596
- throw new Error(`CODEX_CLI_NOT_FOUND:${command}`);
597
- return resolved;
603
+ platformEnvironment() {
604
+ const env = {};
605
+ const required = process.platform === 'win32'
606
+ ? [
607
+ 'PATH',
608
+ 'PATHEXT',
609
+ 'SystemRoot',
610
+ 'ComSpec',
611
+ 'USERPROFILE',
612
+ 'HOMEDRIVE',
613
+ 'HOMEPATH',
614
+ 'APPDATA',
615
+ 'LOCALAPPDATA',
616
+ 'TEMP',
617
+ 'TMP',
618
+ ]
619
+ : ['PATH', 'HOME', 'TMPDIR'];
620
+ for (const key of required) {
621
+ const actual = process.platform === 'win32'
622
+ ? Object.keys(process.env).find((candidate) => candidate.toLowerCase() === key.toLowerCase())
623
+ : key;
624
+ const value = actual ? process.env[actual] : undefined;
625
+ if (value !== undefined)
626
+ env[key] = value;
627
+ }
628
+ return env;
598
629
  }
599
- claudePath() {
600
- const env = { ...process.env };
601
- const command = process.env.CFLOW_CLAUDE_PATH ?? process.env.CLAUDE_CODE_EXECUTABLE ?? 'claude';
602
- const resolved = resolveExecutable(command, env);
630
+ bundledCodexPath() {
631
+ const targets = {
632
+ 'darwin-x64': ['@openai/codex-darwin-x64', 'x86_64-apple-darwin'],
633
+ 'darwin-arm64': ['@openai/codex-darwin-arm64', 'aarch64-apple-darwin'],
634
+ 'linux-x64': ['@openai/codex-linux-x64', 'x86_64-unknown-linux-musl'],
635
+ 'linux-arm64': ['@openai/codex-linux-arm64', 'aarch64-unknown-linux-musl'],
636
+ 'win32-x64': ['@openai/codex-win32-x64', 'x86_64-pc-windows-msvc'],
637
+ 'win32-arm64': ['@openai/codex-win32-arm64', 'aarch64-pc-windows-msvc'],
638
+ };
639
+ const target = targets[`${process.platform}-${process.arch}`];
640
+ if (!target)
641
+ throw new Error(`CODEX_BUNDLED_PLATFORM_UNSUPPORTED:${process.platform}-${process.arch}`);
642
+ let packageJson;
643
+ try {
644
+ packageJson = moduleRequire.resolve(`${target[0]}/package.json`);
645
+ }
646
+ catch {
647
+ throw new Error(`CODEX_BUNDLED_CLI_NOT_FOUND:${target[0]}`);
648
+ }
649
+ const executable = join(dirname(packageJson), 'vendor', target[1], 'bin', process.platform === 'win32' ? 'codex.exe' : 'codex');
650
+ const resolved = existingExecutable(executable);
603
651
  if (!resolved)
604
- throw new Error(`CLAUDE_CODE_CLI_NOT_FOUND:${command}`);
652
+ throw new Error(`CODEX_BUNDLED_CLI_NOT_FOUND:${executable}`);
605
653
  return resolved;
606
654
  }
607
- acpEnvironment(profile, effects = []) {
608
- const env = {};
655
+ runtimeEnvironment(profile, effects = []) {
656
+ const env = this.platformEnvironment();
609
657
  for (const key of profile.envAllowlist) {
610
658
  const value = process.env[key];
611
659
  if (value !== undefined)
612
660
  env[key] = value;
613
661
  }
614
- if (profile.id === 'codex') {
615
- env.CODEX_PATH = this.codexPath();
616
- env.INITIAL_AGENT_MODE = effects.some((effect) => effect.type === 'file-write')
617
- ? 'workspace-write'
618
- : 'read-only';
619
- env.NO_BROWSER ??= '1';
620
- if (profile.model)
621
- env.CODEX_CONFIG = JSON.stringify({ model: profile.model });
622
- }
623
- if (profile.id === 'claude-code') {
624
- env.CLAUDE_CODE_EXECUTABLE = this.claudePath();
625
- if (profile.model)
626
- env.CLAUDE_MODEL_CONFIG = JSON.stringify({ model: profile.model });
662
+ const descriptor = adapterDescriptorForId(profile.id);
663
+ if (descriptor) {
664
+ Object.assign(env, descriptor.staticEnvironment);
665
+ if (profile.model && descriptor.modelEnvironment)
666
+ env[descriptor.modelEnvironment] = JSON.stringify({ model: profile.model });
667
+ if (descriptor.permissionMode)
668
+ env[descriptor.permissionMode.environment] = canChangeWorkspace(effects)
669
+ ? descriptor.permissionMode.workspaceWrite
670
+ : descriptor.permissionMode.readOnly;
671
+ if (descriptor.nativeCommand) {
672
+ const override = process.env[descriptor.nativeCommand.overrideEnvironment];
673
+ if (override) {
674
+ const resolved = resolveCommandAliases([override], this.commandResolutionOptions(process.env));
675
+ if (!resolved)
676
+ throw new Error(`${profile.id === 'codex' ? 'CODEX_CLI_NOT_FOUND' : 'CLAUDE_CODE_CLI_NOT_FOUND'}:${override}`);
677
+ env[descriptor.nativeCommand.adapterEnvironment] = resolved.executable;
678
+ }
679
+ else if (descriptor.nativeCommand.bundled === 'codex') {
680
+ env[descriptor.nativeCommand.adapterEnvironment] = this.bundledCodexPath();
681
+ }
682
+ }
627
683
  }
628
684
  return env;
629
685
  }
@@ -636,30 +692,21 @@ export class RuntimeManager {
636
692
  }
637
693
  async runCli(profile, prompt, signal, effects = [], context, analysis) {
638
694
  const cwd = analysis?.cwd ?? context?.workspaceRoot ?? profile.workingDirectory ?? process.cwd();
639
- const command = this.cliCommand(profile);
640
695
  const args = [
641
696
  ...profile.args,
642
697
  ...this.cliPermissionArgs(profile, effects, analysis),
643
698
  ...(profile.promptTransport === 'argument' ? [prompt] : []),
644
699
  ];
645
- const child = spawn(command, args, {
646
- cwd,
647
- env: this.acpEnvironment(profile, effects),
648
- stdio: ['pipe', 'pipe', 'pipe'],
649
- shell: needsWindowsShell(command),
650
- windowsHide: true,
651
- });
700
+ const spec = this.launchSpec(profile, args, cwd, ['pipe', 'pipe', 'pipe'], effects);
701
+ const child = launchProcess(spec);
652
702
  const output = [];
653
703
  const errors = [];
654
704
  let outputBytes = 0;
655
705
  let outputLimitExceeded = false;
656
- const terminate = () => {
657
- child.kill('SIGTERM');
658
- setTimeout(() => child.kill('SIGKILL'), 1_000).unref();
659
- };
706
+ const terminate = () => terminateProcess(child);
660
707
  const relayAbort = () => terminate();
661
708
  signal.addEventListener('abort', relayAbort, { once: true });
662
- child.stdout.on('data', (chunk) => {
709
+ child.stdout?.on('data', (chunk) => {
663
710
  outputBytes += chunk.length;
664
711
  if (outputBytes > profile.maxOutputBytes) {
665
712
  outputLimitExceeded = true;
@@ -668,11 +715,13 @@ export class RuntimeManager {
668
715
  }
669
716
  output.push(chunk);
670
717
  });
671
- child.stderr.on('data', (chunk) => {
718
+ child.stderr?.on('data', (chunk) => {
672
719
  errors.push(chunk);
673
720
  if (Buffer.concat(errors).length > 65_536)
674
721
  errors.shift();
675
722
  });
723
+ if (!child.stdin)
724
+ throw new Error('RUNTIME_STDIN_UNAVAILABLE');
676
725
  if (profile.promptTransport === 'stdin')
677
726
  child.stdin.end(prompt);
678
727
  else
@@ -704,26 +753,17 @@ export class RuntimeManager {
704
753
  }
705
754
  async runAcp(profile, prompt, signal, effects = [], context, analysis) {
706
755
  const cwd = analysis?.cwd ?? context?.workspaceRoot ?? profile.workingDirectory ?? process.cwd();
707
- const command = this.acpCommand(profile);
708
- const child = spawn(command, profile.args, {
709
- cwd,
710
- env: this.acpEnvironment(profile, effects),
711
- stdio: ['pipe', 'pipe', 'pipe'],
712
- shell: needsWindowsShell(command),
713
- windowsHide: true,
714
- });
756
+ const spec = this.launchSpec(profile, profile.args, cwd, ['pipe', 'pipe', 'pipe'], effects);
757
+ const child = launchProcess(spec);
715
758
  let stderr = Buffer.alloc(0);
716
759
  let stderrText = '';
717
- child.stderr.on('data', (chunk) => {
760
+ child.stderr?.on('data', (chunk) => {
718
761
  stderr = Buffer.concat([stderr, chunk]);
719
762
  if (stderr.length > 65_536)
720
763
  stderr = stderr.subarray(stderr.length - 65_536);
721
764
  stderrText = stderr.toString('utf8');
722
765
  });
723
- const terminate = () => {
724
- child.kill('SIGTERM');
725
- setTimeout(() => child.kill('SIGKILL'), 1_000).unref();
726
- };
766
+ const terminate = () => terminateProcess(child);
727
767
  const relayAbort = () => terminate();
728
768
  signal.addEventListener('abort', relayAbort, { once: true });
729
769
  const exitPromise = new Promise((resolveExit) => child.once('exit', (code, childSignal) => resolveExit({ code, signal: childSignal })));