@relayflows/sdk 2.0.31 → 2.0.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.
Files changed (95) hide show
  1. package/dist/authored-preflight.d.ts +4 -0
  2. package/dist/authored-preflight.d.ts.map +1 -0
  3. package/dist/authored-preflight.js +50 -0
  4. package/dist/authored-preflight.js.map +1 -0
  5. package/dist/authored-worker-step.d.ts.map +1 -1
  6. package/dist/authored-worker-step.js +3 -2
  7. package/dist/authored-worker-step.js.map +1 -1
  8. package/dist/cli/check.d.ts +10 -2
  9. package/dist/cli/check.d.ts.map +1 -1
  10. package/dist/cli/check.js +16 -137
  11. package/dist/cli/check.js.map +1 -1
  12. package/dist/cli/cli-probe.d.ts +20 -0
  13. package/dist/cli/cli-probe.d.ts.map +1 -0
  14. package/dist/cli/cli-probe.js +166 -0
  15. package/dist/cli/cli-probe.js.map +1 -0
  16. package/dist/cli/cloud-mirror-session.d.ts +97 -0
  17. package/dist/cli/cloud-mirror-session.d.ts.map +1 -0
  18. package/dist/cli/cloud-mirror-session.js +264 -0
  19. package/dist/cli/cloud-mirror-session.js.map +1 -0
  20. package/dist/cli-commands.d.ts +6 -0
  21. package/dist/cli-commands.d.ts.map +1 -1
  22. package/dist/cli-commands.js +1 -0
  23. package/dist/cli-commands.js.map +1 -1
  24. package/dist/cli.d.ts +7 -0
  25. package/dist/cli.d.ts.map +1 -1
  26. package/dist/cli.js +84 -18
  27. package/dist/cli.js.map +1 -1
  28. package/dist/cloud-deploy.d.ts +1 -1
  29. package/dist/cloud-deploy.d.ts.map +1 -1
  30. package/dist/cloud-deploy.js +24 -8
  31. package/dist/cloud-deploy.js.map +1 -1
  32. package/dist/cloud-mirror-ledger.d.ts +40 -0
  33. package/dist/cloud-mirror-ledger.d.ts.map +1 -0
  34. package/dist/cloud-mirror-ledger.js +142 -0
  35. package/dist/cloud-mirror-ledger.js.map +1 -0
  36. package/dist/cloud-mirror-step.d.ts +180 -0
  37. package/dist/cloud-mirror-step.d.ts.map +1 -0
  38. package/dist/cloud-mirror-step.js +523 -0
  39. package/dist/cloud-mirror-step.js.map +1 -0
  40. package/dist/cloud-mirror-transport.d.ts +94 -0
  41. package/dist/cloud-mirror-transport.d.ts.map +1 -0
  42. package/dist/cloud-mirror-transport.js +163 -0
  43. package/dist/cloud-mirror-transport.js.map +1 -0
  44. package/dist/cloud-mirror.d.ts +132 -0
  45. package/dist/cloud-mirror.d.ts.map +1 -0
  46. package/dist/cloud-mirror.js +556 -0
  47. package/dist/cloud-mirror.js.map +1 -0
  48. package/dist/cloud-transcript-codex.d.ts.map +1 -1
  49. package/dist/cloud-transcript-codex.js +90 -4
  50. package/dist/cloud-transcript-codex.js.map +1 -1
  51. package/dist/cloud-transcript-types.d.ts +19 -1
  52. package/dist/cloud-transcript-types.d.ts.map +1 -1
  53. package/dist/cloud-transcript-types.js.map +1 -1
  54. package/dist/cloud-transcript.d.ts +1 -1
  55. package/dist/cloud-transcript.d.ts.map +1 -1
  56. package/dist/cloud-transcript.js +10 -0
  57. package/dist/cloud-transcript.js.map +1 -1
  58. package/dist/helper-reference.d.ts.map +1 -1
  59. package/dist/helper-reference.js +134 -6
  60. package/dist/helper-reference.js.map +1 -1
  61. package/dist/hosted-extension-runtime.js +1 -1
  62. package/dist/hosted-extension-sandbox.js +1 -1
  63. package/dist/package-version.d.ts +10 -0
  64. package/dist/package-version.d.ts.map +1 -0
  65. package/dist/package-version.js +22 -0
  66. package/dist/package-version.js.map +1 -0
  67. package/dist/preflight.d.ts +14 -0
  68. package/dist/preflight.d.ts.map +1 -1
  69. package/dist/preflight.js +18 -3
  70. package/dist/preflight.js.map +1 -1
  71. package/dist/relay-cli.d.ts.map +1 -1
  72. package/dist/relay-cli.js +1 -21
  73. package/dist/relay-cli.js.map +1 -1
  74. package/package.json +2 -2
  75. package/src/authored-preflight.ts +46 -0
  76. package/src/authored-worker-step.ts +4 -3
  77. package/src/cli/check.ts +25 -163
  78. package/src/cli/cli-probe.ts +194 -0
  79. package/src/cli/cloud-mirror-session.ts +329 -0
  80. package/src/cli-commands.ts +1 -0
  81. package/src/cli.ts +94 -19
  82. package/src/cloud-deploy.ts +26 -8
  83. package/src/cloud-mirror-ledger.ts +161 -0
  84. package/src/cloud-mirror-step.ts +650 -0
  85. package/src/cloud-mirror-transport.ts +261 -0
  86. package/src/cloud-mirror.ts +633 -0
  87. package/src/cloud-transcript-codex.ts +96 -4
  88. package/src/cloud-transcript-types.ts +18 -1
  89. package/src/cloud-transcript.ts +9 -1
  90. package/src/helper-reference.ts +131 -6
  91. package/src/hosted-extension-runtime.ts +1 -1
  92. package/src/hosted-extension-sandbox.ts +1 -1
  93. package/src/package-version.ts +23 -0
  94. package/src/preflight.ts +31 -7
  95. package/src/relay-cli.ts +1 -24
package/src/cli/check.ts CHANGED
@@ -1,23 +1,13 @@
1
1
  import { communicationInstruction } from '../communication/spec.js';
2
2
  import { checkCommunicationEnvironment } from '../communication/preflight.js';
3
- import { agentEnvironment, brokerEnvironment } from '../communication/environment.js';
4
3
  import { accessSync, constants, readFileSync } from 'node:fs';
5
4
  import { dirname, isAbsolute, join, parse as parsePath, resolve } from 'node:path';
6
- import { spawnSync } from 'node:child_process';
7
5
  import { parse as parseYaml } from 'yaml';
8
6
  import { CompileError, compileSpec, kernelToAuthoring } from '../compile.js';
9
7
  import { agentWorkerDiagnostics } from './check-worker-surface.js';
10
8
  import { helperReady } from '../yaml-helper-effect.js';
11
9
  import { flowRequirements, type FlowRequirements } from '../flow-requirements.js';
12
- import {
13
- adapterIdentification,
14
- authenticationProbe,
15
- cliAdapterKind,
16
- displayInvocation,
17
- modelReadinessProbe,
18
- type CliInvocation,
19
- } from '../cli-adapter.js';
20
- import { MODEL_ENV } from '../worker-cli.js';
10
+ import { probeCli, resolveExecutable } from './cli-probe.js';
21
11
  import { modelNameError } from '../model-name.js';
22
12
  import type { FlowSpec } from '../spec.js';
23
13
  import type { McpServerConfig } from '../spec.js';
@@ -26,9 +16,9 @@ import type { StepGateInspection } from '../gate-contract.js';
26
16
  import type { CheckFailureKind, CheckWarningKind } from '../failure-kinds.js';
27
17
  import {
28
18
  preflight,
29
- CliProbeError,
30
19
  type CliResolution,
31
20
  type CliProbeResult,
21
+ type CliProbeOutcome,
32
22
  type PreflightDiagnostic,
33
23
  type PreflightProbes,
34
24
  } from '../preflight.js';
@@ -118,6 +108,15 @@ export interface CheckExecution {
118
108
  flow?: FlowSpec;
119
109
  }
120
110
 
111
+ /** Compatibility options accepted by older authored callers. */
112
+ export interface AuthoredCheckOptions {
113
+ projectConfig?: ProjectConfig;
114
+ probeCache?: CliProbeOutcomeMap;
115
+ communicationChecked?: boolean;
116
+ }
117
+
118
+ type CliProbeOutcomeMap = Map<string, CliProbeOutcome>;
119
+
121
120
  /**
122
121
  * Facts about the *caller*, not about the spec, that change which diagnostics
123
122
  * apply. Preflight stays a pure function of the spec plus environment probes;
@@ -186,14 +185,26 @@ function safeRequirements(authoring: FlowSpec, projectCli: string | undefined):
186
185
  export function checkAuthoredFlow(
187
186
  authoring: FlowSpec,
188
187
  path: string,
189
- projectConfig?: ProjectConfig,
188
+ projectConfigOrOptions?: ProjectConfig | AuthoredCheckOptions,
190
189
  invocation: CheckInvocation = {},
190
+ cliProbeCache?: Map<string, CliProbeOutcome>,
191
191
  ): CheckExecution {
192
192
  const absolutePath = resolve(path);
193
193
  try {
194
+ const options = projectConfigOrOptions !== undefined
195
+ && ('projectConfig' in projectConfigOrOptions
196
+ || 'probeCache' in projectConfigOrOptions
197
+ || 'communicationChecked' in projectConfigOrOptions)
198
+ ? projectConfigOrOptions
199
+ : undefined;
200
+ const projectConfig = options !== undefined
201
+ ? options.projectConfig
202
+ : projectConfigOrOptions as ProjectConfig | undefined;
203
+ const effectiveProbeCache = cliProbeCache ?? options?.probeCache;
194
204
  const config = projectConfig ?? readProjectConfig(dirname(absolutePath));
195
205
  const probes = systemProbes(dirname(absolutePath), config);
196
206
  const result = preflight(authoring, {
207
+ ...(effectiveProbeCache === undefined ? {} : { cliProbeCache: effectiveProbeCache }),
197
208
  projectCli: config.cli,
198
209
  projectConfigPath: config.path,
199
210
  projectSearchStart: dirname(absolutePath),
@@ -210,7 +221,7 @@ export function checkAuthoredFlow(
210
221
  )
211
222
  : undefined;
212
223
  if (flow?.steps.some(step => step.type === 'agent' && communicationInstruction(step.instruction))) {
213
- try { checkCommunicationEnvironment(flow); }
224
+ try { if (options?.communicationChecked !== true) checkCommunicationEnvironment(flow); }
214
225
  catch (error) {
215
226
  result.ok = false;
216
227
  result.diagnostics.push({ severity: 'refusal', kind: 'probe_failed',
@@ -479,155 +490,6 @@ function canonicalCli(cli: string, directory: string): string {
479
490
  return resolve(directory, cli);
480
491
  }
481
492
 
482
- function probeCli(
483
- cli: string,
484
- directory: string,
485
- model?: string,
486
- execution?: 'managed',
487
- ): CliProbeResult {
488
- const executable = resolveExecutable(cli, directory);
489
- if (executable === undefined) return { exists: false, authenticated: false };
490
- const kind = cliAdapterKind(executable);
491
- // Relay owns interactive CLI launch/injection. Its generic PTY path is not
492
- // the headless wrapper protocol; do not demand that protocol from Gemini,
493
- // Cursor, OpenCode, or other interactive tools. Never invent an auth pass.
494
- if (execution === 'managed' && kind === 'relayflows-wrapper-v1') {
495
- return { exists: true, supported: true, authenticated: 'unverified' };
496
- }
497
- const environment = execution === 'managed'
498
- ? { ...brokerEnvironment(process.env), ...agentEnvironment(executable) } : process.env;
499
- const probe = (invocation: CliInvocation) => runProbe(executable, directory, invocation, environment);
500
- const identification = adapterIdentification(kind);
501
- const identified = probe(identification.invocation);
502
- if (
503
- identified.status !== 0
504
- || (identification.expectedStdout !== undefined
505
- && identified.stdout.trim() !== identification.expectedStdout)
506
- ) {
507
- return { exists: true, supported: false, authenticated: false };
508
- }
509
- const auth = authenticationProbe(kind);
510
- const authCommand = displayInvocation(cli, auth);
511
- if (model === undefined) {
512
- return {
513
- exists: true,
514
- supported: true,
515
- authenticated: probe(auth).status === 0,
516
- authCommand,
517
- };
518
- }
519
-
520
- const scoped = modelReadinessProbe(kind, model);
521
- const modelCommand = displayInvocation(cli, scoped);
522
- // A successful real provider round trip (or identified wrapper probe)
523
- // proves both auth and exact-model access. On failure, run the adapter's
524
- // actual auth command solely to classify auth vs model access truthfully.
525
- if (probe(scoped).status === 0) {
526
- return {
527
- exists: true,
528
- supported: true,
529
- authenticated: true,
530
- modelAvailable: true,
531
- authCommand,
532
- modelCommand,
533
- };
534
- }
535
- const authProbe = probe(auth);
536
- const authenticated = authProbe.status === 0;
537
- return {
538
- exists: true,
539
- supported: true,
540
- authenticated,
541
- modelAvailable: false,
542
- authCommand,
543
- modelCommand,
544
- // Only on failure: on success there is nothing to explain, and the output
545
- // is the most identity-bearing thing this function touches.
546
- ...(authenticated
547
- ? {}
548
- : {
549
- authExitCode: authProbe.status,
550
- authFailureDetail: redactProbeOutput(
551
- `${authProbe.stderr}${authProbe.stdout}`,
552
- ).trim().slice(0, 500),
553
- }),
554
- };
555
- }
556
-
557
- /**
558
- * Redact anything that looks like a credential or an account identifier.
559
- *
560
- * `auth status` output is diagnostic, but it is also the one place an account
561
- * email, org id or token fragment can appear. The point of surfacing it is to
562
- * say WHY a probe failed, which survives redaction; leaking an identity into a
563
- * refusal message that gets pasted into issues does not.
564
- */
565
- function redactProbeOutput(text: string): string {
566
- return text
567
- .replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, '<redacted-email>')
568
- .replace(/\b(sk|pk|oat|rt)[-_][A-Za-z0-9._-]{8,}/gi, '<redacted-token>')
569
- .replace(/\b[A-Fa-f0-9]{32,}\b/g, '<redacted-hex>');
570
- }
571
-
572
- function runProbe(
573
- executable: string,
574
- directory: string,
575
- invocation: CliInvocation,
576
- environment: NodeJS.ProcessEnv = process.env,
577
- ): { status: number | null; stdout: string; stderr: string } {
578
- const env = { ...environment };
579
- delete env[MODEL_ENV];
580
- if (invocation.modelEnv !== undefined) env[MODEL_ENV] = invocation.modelEnv;
581
- const result = spawnSync(executable, invocation.args, {
582
- cwd: directory,
583
- encoding: 'utf8',
584
- // stderr was 'ignore'. A failing `auth status` writes its reason there, so
585
- // discarding it made every authentication refusal structurally
586
- // undiagnosable: the refusal could say a probe exited non-zero and never
587
- // what it said. Captured, then redacted at the point of use.
588
- stdio: ['ignore', 'pipe', 'pipe'],
589
- timeout: invocation.timeoutMs,
590
- env,
591
- });
592
- const failure = classifySpawnFailure(result.error, result.signal, invocation.timeoutMs);
593
- if (failure !== undefined) throw failure;
594
- return {
595
- status: result.status,
596
- stdout: result.stdout,
597
- stderr: result.stderr ?? '',
598
- };
599
- }
600
-
601
- function resolveExecutable(command: string, directory: string): string | undefined {
602
- if (command.includes('/') || isAbsolute(command)) {
603
- const path = isAbsolute(command) ? command : resolve(directory, command);
604
- try {
605
- accessSync(path, constants.X_OK);
606
- return path;
607
- } catch {
608
- return undefined;
609
- }
610
- }
611
- const result = spawnSync('which', [command], { encoding: 'utf8', timeout: 5_000 });
612
- const failure = classifySpawnFailure(result.error, result.signal, 5_000);
613
- if (failure !== undefined) throw failure;
614
- return result.status === 0 ? result.stdout.trim() : undefined;
615
- }
616
-
617
- function classifySpawnFailure(
618
- error: Error | undefined,
619
- signal: NodeJS.Signals | null,
620
- timeoutMs: number,
621
- ): CliProbeError | undefined {
622
- if (error !== undefined) {
623
- const detail = (error as NodeJS.ErrnoException).code === 'ETIMEDOUT'
624
- ? `timeout:${timeoutMs}ms` as const
625
- : 'spawn_failed' as const;
626
- return new CliProbeError(detail);
627
- }
628
- return signal === null ? undefined : new CliProbeError(`signal:${signal}`);
629
- }
630
-
631
493
  function executableExists(command: string, directory: string): boolean {
632
494
  return resolveExecutable(command, directory) !== undefined;
633
495
  }
@@ -0,0 +1,194 @@
1
+ import { accessSync, constants } from 'node:fs';
2
+ import { isAbsolute, resolve } from 'node:path';
3
+ import { execFile, spawnSync } from 'node:child_process';
4
+ import { agentEnvironment, brokerEnvironment } from '../communication/environment.js';
5
+ import { adapterIdentification, authenticationProbe, cliAdapterKind, displayInvocation,
6
+ modelReadinessProbe, type CliInvocation } from '../cli-adapter.js';
7
+ import { MODEL_ENV } from '../worker-cli.js';
8
+ import { CliProbeError, type CliProbeResult } from '../preflight.js';
9
+
10
+ interface ProbeRequest {
11
+ executable: string;
12
+ directory: string;
13
+ invocation: CliInvocation;
14
+ environment: NodeJS.ProcessEnv;
15
+ }
16
+ interface ProbeOutput { status: number | null; stdout: string; stderr: string }
17
+
18
+ /** One decision sequence; checking uses synchronous I/O, live flows yield the loop. */
19
+ export function probeCli(...args: Parameters<typeof probeSequence>): CliProbeResult {
20
+ return driveSync(probeSequence(...args));
21
+ }
22
+
23
+ export async function probeCliAsync(...args: Parameters<typeof probeSequence>): Promise<CliProbeResult> {
24
+ const sequence = probeSequence(...args);
25
+ let next = sequence.next();
26
+ while (!next.done) next = sequence.next(await runProbeAsync(next.value));
27
+ return next.value;
28
+ }
29
+
30
+ export function resolveExecutable(command: string, directory: string): string | undefined {
31
+ return driveSync(executableSequence(command, directory));
32
+ }
33
+
34
+ function driveSync<T>(sequence: Generator<ProbeRequest, T, ProbeOutput>): T {
35
+ let next = sequence.next();
36
+ while (!next.done) {
37
+ const request = next.value;
38
+ const result = spawnSync(request.executable, request.invocation.args, {
39
+ ...probeOptions(request),
40
+ // Preserve the old synchronous probe contract: provider CLIs must not
41
+ // inherit a readable stdin that can block auth/identify probes.
42
+ stdio: ['ignore', 'pipe', 'pipe'],
43
+ });
44
+ const failure = classifySpawnFailure(result.error, result.signal, request.invocation.timeoutMs);
45
+ if (failure !== undefined) throw failure;
46
+ next = sequence.next({ status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' });
47
+ }
48
+ return next.value;
49
+ }
50
+
51
+ function probeOptions({ directory, invocation, environment }: ProbeRequest) {
52
+ const env = { ...environment };
53
+ delete env[MODEL_ENV];
54
+ if (invocation.modelEnv !== undefined) env[MODEL_ENV] = invocation.modelEnv;
55
+ return { cwd: directory, encoding: 'utf8' as const, timeout: invocation.timeoutMs,
56
+ maxBuffer: 1024 * 1024, env };
57
+ }
58
+
59
+ function runProbeAsync(request: ProbeRequest): Promise<ProbeOutput> {
60
+ return new Promise((resolve, reject) => {
61
+ const child = execFile(request.executable, request.invocation.args, probeOptions(request),
62
+ (error, stdout, stderr) => {
63
+ // Numeric exit codes are probe results; launch, timeout, signal and buffer
64
+ // errors are failures to collect a fact, just as in the synchronous driver.
65
+ if (error?.killed && typeof error.code !== 'string') return reject(new CliProbeError(`timeout:${request.invocation.timeoutMs}ms`));
66
+ const failure = classifySpawnFailure(
67
+ error !== null && typeof error.code !== 'number' && error.signal == null ? error : undefined,
68
+ error?.signal ?? null, request.invocation.timeoutMs);
69
+ if (failure !== undefined) return reject(failure);
70
+ resolve({ status: error === null ? 0 : typeof error.code === 'number' ? error.code : null,
71
+ stdout, stderr });
72
+ });
73
+ child.stdin?.end();
74
+ });
75
+ }
76
+
77
+ function* probeSequence(
78
+ cli: string,
79
+ directory: string,
80
+ model?: string,
81
+ execution?: 'managed',
82
+ ): Generator<ProbeRequest, CliProbeResult, ProbeOutput> {
83
+ const executable = yield* executableSequence(cli, directory);
84
+ if (executable === undefined) return { exists: false, authenticated: false };
85
+ const kind = cliAdapterKind(executable);
86
+ // Relay owns interactive CLI launch/injection. Its generic PTY path is not
87
+ // the headless wrapper protocol; do not demand that protocol from Gemini,
88
+ // Cursor, OpenCode, or other interactive tools. Never invent an auth pass.
89
+ if (execution === 'managed' && kind === 'relayflows-wrapper-v1') {
90
+ return { exists: true, supported: true, authenticated: 'unverified' };
91
+ }
92
+ const environment = execution === 'managed'
93
+ ? { ...brokerEnvironment(process.env), ...agentEnvironment(executable) } : process.env;
94
+ const probe = (invocation: CliInvocation): ProbeRequest => ({ executable, directory, invocation, environment });
95
+ const identification = adapterIdentification(kind);
96
+ const identified = yield probe(identification.invocation);
97
+ if (
98
+ identified.status !== 0
99
+ || (identification.expectedStdout !== undefined
100
+ && identified.stdout.trim() !== identification.expectedStdout)
101
+ ) {
102
+ return { exists: true, supported: false, authenticated: false };
103
+ }
104
+ const auth = authenticationProbe(kind);
105
+ const authCommand = displayInvocation(cli, auth);
106
+ if (model === undefined) {
107
+ return {
108
+ exists: true,
109
+ supported: true,
110
+ authenticated: (yield probe(auth)).status === 0,
111
+ authCommand,
112
+ };
113
+ }
114
+
115
+ const scoped = modelReadinessProbe(kind, model);
116
+ const modelCommand = displayInvocation(cli, scoped);
117
+ // A successful real provider round trip (or identified wrapper probe)
118
+ // proves both auth and exact-model access. On failure, run the adapter's
119
+ // actual auth command solely to classify auth vs model access truthfully.
120
+ if ((yield probe(scoped)).status === 0) {
121
+ return {
122
+ exists: true,
123
+ supported: true,
124
+ authenticated: true,
125
+ modelAvailable: true,
126
+ authCommand,
127
+ modelCommand,
128
+ };
129
+ }
130
+ const authProbe = yield probe(auth);
131
+ const authenticated = authProbe.status === 0;
132
+ return {
133
+ exists: true,
134
+ supported: true,
135
+ authenticated,
136
+ modelAvailable: false,
137
+ authCommand,
138
+ modelCommand,
139
+ // Only on failure: on success there is nothing to explain, and the output
140
+ // is the most identity-bearing thing this function touches.
141
+ ...(authenticated
142
+ ? {}
143
+ : {
144
+ authExitCode: authProbe.status,
145
+ authFailureDetail: redactProbeOutput(
146
+ `${authProbe.stderr}${authProbe.stdout}`,
147
+ ).trim().slice(0, 500),
148
+ }),
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Redact anything that looks like a credential or an account identifier.
154
+ *
155
+ * `auth status` output is diagnostic, but it is also the one place an account
156
+ * email, org id or token fragment can appear. The point of surfacing it is to
157
+ * say WHY a probe failed, which survives redaction; leaking an identity into a
158
+ * refusal message that gets pasted into issues does not.
159
+ */
160
+ function redactProbeOutput(text: string): string {
161
+ return text
162
+ .replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, '<redacted-email>')
163
+ .replace(/\b(sk|pk|oat|rt)[-_][A-Za-z0-9._-]{8,}/gi, '<redacted-token>')
164
+ .replace(/\b[A-Fa-f0-9]{32,}\b/g, '<redacted-hex>');
165
+ }
166
+
167
+ function* executableSequence(command: string, directory: string): Generator<ProbeRequest, string | undefined, ProbeOutput> {
168
+ if (command.includes('/') || isAbsolute(command)) {
169
+ const path = isAbsolute(command) ? command : resolve(directory, command);
170
+ try {
171
+ accessSync(path, constants.X_OK);
172
+ return path;
173
+ } catch {
174
+ return undefined;
175
+ }
176
+ }
177
+ const result = yield { executable: 'which', directory: process.cwd(),
178
+ invocation: { args: [command], timeoutMs: 5_000 }, environment: process.env };
179
+ return result.status === 0 ? result.stdout.trim() : undefined;
180
+ }
181
+
182
+ function classifySpawnFailure(
183
+ error: Error | undefined,
184
+ signal: NodeJS.Signals | null,
185
+ timeoutMs: number,
186
+ ): CliProbeError | undefined {
187
+ if (error !== undefined) {
188
+ const detail = (error as NodeJS.ErrnoException).code === 'ETIMEDOUT'
189
+ ? `timeout:${timeoutMs}ms` as const
190
+ : 'spawn_failed' as const;
191
+ return new CliProbeError(detail);
192
+ }
193
+ return signal === null ? undefined : new CliProbeError(`signal:${signal}`);
194
+ }