@contentful/experience-design-system-generation 2.26.1

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Contentful GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@contentful/experience-design-system-generation",
3
+ "version": "2.26.1",
4
+ "description": "Agent-invocation and skill-prompt engine for the Contentful Experience Design System SDK",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/src/index.js",
8
+ "types": "./dist/src/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/src/index.d.ts",
12
+ "import": "./dist/src/index.js",
13
+ "node": "./dist/src/index.js"
14
+ }
15
+ },
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "registry": "https://npm.pkg.github.com/"
19
+ },
20
+ "files": [
21
+ "dist/",
22
+ "skills/"
23
+ ],
24
+ "scripts": {
25
+ "build": "nx build experience-design-system-generation",
26
+ "typecheck": "nx typecheck experience-design-system-generation",
27
+ "clean": "nx clean experience-design-system-generation",
28
+ "test": "nx test experience-design-system-generation",
29
+ "lint": "nx lint experience-design-system-generation",
30
+ "lint:fix": "nx lint:fix experience-design-system-generation"
31
+ },
32
+ "dependencies": {
33
+ "@contentful/experience-design-system-types": "workspace:*"
34
+ },
35
+ "devDependencies": {
36
+ "@tsconfig/node24": "^24.0.4",
37
+ "@types/node": "^24.0.3",
38
+ "eslint": "^9.39.5",
39
+ "eslint-config-prettier": "^10.1.8",
40
+ "eslint-plugin-prettier": "^5.5.6",
41
+ "typescript-eslint": "^8.67.0",
42
+ "vitest": "^4.0.16"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/contentful/experience-design-system-sdk-public.git",
47
+ "directory": "packages/experience-design-system-generation"
48
+ },
49
+ "homepage": "https://github.com/contentful/experience-design-system-sdk-public#readme",
50
+ "bugs": {
51
+ "url": "https://github.com/contentful/experience-design-system-sdk-public/issues"
52
+ },
53
+ "engines": {
54
+ "node": ">=24"
55
+ },
56
+ "module": "./src/index.js"
57
+ }
@@ -0,0 +1,24 @@
1
+ import type { AgentDebugEvent, AgentAuthStatus, AgentName, AgentRunResult } from './agent-runner.js';
2
+ export interface InvokeAgentOptions {
3
+ agent: AgentName;
4
+ model?: string;
5
+ prompt: string;
6
+ timeoutMs: number;
7
+ onOutput?: (chunk: string) => void;
8
+ }
9
+ /**
10
+ * Abstracts "invoke an agent and get a result back" from the local-subprocess
11
+ * mechanism `runAgent` uses today. A remote implementation (e.g. against an
12
+ * internal agents service) implements the same contract without importing
13
+ * this package's subprocess transport.
14
+ */
15
+ export interface AgentInvoker {
16
+ invoke(options: InvokeAgentOptions): Promise<AgentRunResult>;
17
+ checkAuth(agent: AgentName): Promise<AgentAuthStatus>;
18
+ }
19
+ export interface CreateLocalCliAgentInvokerOptions {
20
+ /** Wire in a debug-event sink (e.g. the CLI's own debug logger). No-op by default. */
21
+ onDebugEvent?: AgentDebugEvent;
22
+ }
23
+ /** Default `AgentInvoker`: spawns the agent CLI binary as a local subprocess. */
24
+ export declare function createLocalCliAgentInvoker(options?: CreateLocalCliAgentInvokerOptions): AgentInvoker;
@@ -0,0 +1,13 @@
1
+ import { checkAgentAuth, runAgent } from './agent-runner.js';
2
+ /** Default `AgentInvoker`: spawns the agent CLI binary as a local subprocess. */
3
+ export function createLocalCliAgentInvoker(options = {}) {
4
+ const { onDebugEvent } = options;
5
+ return {
6
+ invoke(invokeOptions) {
7
+ return runAgent({ ...invokeOptions, onDebugEvent });
8
+ },
9
+ checkAuth(agent) {
10
+ return checkAgentAuth(agent);
11
+ },
12
+ };
13
+ }
@@ -0,0 +1,4 @@
1
+ export declare const AGENT_NAMES: readonly ["claude", "codex", "opencode", "cursor"];
2
+ export type AgentName = (typeof AGENT_NAMES)[number];
3
+ export declare const DEFAULT_AGENT_NAME: AgentName;
4
+ export declare function isAgentName(value: string): value is AgentName;
@@ -0,0 +1,5 @@
1
+ export const AGENT_NAMES = ['claude', 'codex', 'opencode', 'cursor'];
2
+ export const DEFAULT_AGENT_NAME = 'claude';
3
+ export function isAgentName(value) {
4
+ return AGENT_NAMES.includes(value);
5
+ }
@@ -0,0 +1,127 @@
1
+ import type { AgentName } from './agent-names.js';
2
+ export { AGENT_NAMES, DEFAULT_AGENT_NAME, isAgentName, type AgentName } from './agent-names.js';
3
+ export interface AgentRunResult {
4
+ exitCode: number;
5
+ stdout: string;
6
+ stderr: string;
7
+ timedOut: boolean;
8
+ }
9
+ export interface ClassifyPropCall {
10
+ tool: 'classify_prop';
11
+ prop: string;
12
+ cdf_type: string;
13
+ cdf_category: 'content' | 'design' | 'state';
14
+ values?: string[];
15
+ token_kind?: string;
16
+ required?: boolean;
17
+ description?: string;
18
+ default?: string | boolean;
19
+ /** Internal LLM rationale; not customer-facing. Persisted to raw_props.rationale. */
20
+ reason?: string;
21
+ }
22
+ export interface ExcludePropCall {
23
+ tool: 'exclude_prop';
24
+ prop: string;
25
+ reason: string;
26
+ }
27
+ export interface ClassifyComponentCall {
28
+ tool: 'classify_component';
29
+ description?: string;
30
+ /**
31
+ * Component-level rationale strings. Surfaced by the `I` ComponentRationalePanel.
32
+ * Each field is optional; missing fields leave existing DB values untouched
33
+ * (sparse update semantics in applyToolCalls).
34
+ */
35
+ rationale?: {
36
+ description?: string;
37
+ props?: string;
38
+ slots?: string;
39
+ };
40
+ }
41
+ export interface ClassifySlotCall {
42
+ tool: 'classify_slot';
43
+ slot: string;
44
+ required?: boolean;
45
+ allowed_components?: string[];
46
+ description?: string;
47
+ /** Per-slot rationale; persisted to raw_slots.rationale. */
48
+ rationale?: string;
49
+ }
50
+ export type ToolCall = ClassifyPropCall | ExcludePropCall | ClassifyComponentCall | ClassifySlotCall;
51
+ export interface SelectComponentCall {
52
+ tool: 'select_component';
53
+ name: string;
54
+ reason?: string;
55
+ confidence?: number;
56
+ }
57
+ export interface RejectComponentCall {
58
+ tool: 'reject_component';
59
+ name: string;
60
+ reason?: string;
61
+ confidence?: number;
62
+ }
63
+ export type SelectToolCall = SelectComponentCall | RejectComponentCall;
64
+ export interface ParsedSelectToolCalls {
65
+ calls: SelectToolCall[];
66
+ warnings: string[];
67
+ }
68
+ export declare function parseSelectToolCallLines(stdout: string): ParsedSelectToolCalls;
69
+ export interface SetTokenCall {
70
+ tool: 'set_token';
71
+ path: string;
72
+ type: string;
73
+ value: unknown;
74
+ description?: string;
75
+ }
76
+ export interface SetGroupCall {
77
+ tool: 'set_group';
78
+ path: string;
79
+ description?: string;
80
+ }
81
+ export type TokenToolCall = SetTokenCall | SetGroupCall;
82
+ export interface ParsedTokenToolCalls {
83
+ calls: TokenToolCall[];
84
+ warnings: string[];
85
+ }
86
+ export interface ParsedToolCalls {
87
+ calls: ToolCall[];
88
+ warnings: string[];
89
+ }
90
+ export declare function parseToolCallLines(stdout: string): ParsedToolCalls;
91
+ export declare function parseTokenToolCallLines(stdout: string): ParsedTokenToolCalls;
92
+ export declare function resolveBinary(agent: AgentName): string;
93
+ /**
94
+ * Resolve the model for an agent. Explicit flag/creds value wins, then a
95
+ * per-agent `EDS_AGENT_MODEL_<AGENT>` env override (mirrors the
96
+ * `EDS_AGENT_BINARY_<AGENT>` pattern), otherwise the lightweight default for
97
+ * that agent.
98
+ */
99
+ export declare function resolveAgentModel(agent: AgentName, explicit?: string): string;
100
+ export type AgentDebugEvent = (name: string, payload?: Record<string, unknown>) => void;
101
+ export declare function buildArgs(agent: AgentName, prompt: string, model?: string, promptViaStdin?: boolean): string[];
102
+ export declare function runAgent(options: {
103
+ agent: AgentName;
104
+ prompt: string;
105
+ timeoutMs: number;
106
+ model?: string;
107
+ onOutput?: (chunk: string) => void;
108
+ /**
109
+ * Deliver the prompt on stdin instead of as an argv positional. Required for
110
+ * large prompts (e.g. the composition resolver inlining candidate files),
111
+ * which overflow ARG_MAX when passed as an argument.
112
+ */
113
+ promptViaStdin?: boolean;
114
+ /** Optional debug-event sink; callers own how/where events get logged. */
115
+ onDebugEvent?: AgentDebugEvent;
116
+ }): Promise<AgentRunResult>;
117
+ export type AgentAuthStatus = 'ok' | 'unauthenticated' | 'not-found';
118
+ export declare function checkAgentAuth(agent: AgentName): Promise<AgentAuthStatus>;
119
+ /**
120
+ * Build a diagnostic string from a failed agent run, surfacing the agent's own
121
+ * stderr (or stdout, when stderr is empty) so callers never emit a context-free
122
+ * "agent failed". Callers only invoke this on a failed run: either a non-zero
123
+ * exit, or a zero exit that produced no tool calls. A non-zero exit is reported
124
+ * as such; a zero exit is therefore the "produced no tool calls" case.
125
+ */
126
+ export declare function describeAgentFailure(result: AgentRunResult, maxDetail?: number): string;
127
+ export declare function extractSentinelOutput(stdout: string): string | null | 'multiple';
@@ -0,0 +1,420 @@
1
+ import { spawn } from 'node:child_process';
2
+ export { AGENT_NAMES, DEFAULT_AGENT_NAME, isAgentName } from './agent-names.js';
3
+ const VALID_SELECT_TOOL_NAMES = new Set(['select_component', 'reject_component']);
4
+ export function parseSelectToolCallLines(stdout) {
5
+ const calls = [];
6
+ const warnings = [];
7
+ for (const raw of stdout.split('\n')) {
8
+ const line = raw.trim();
9
+ if (!line.startsWith('{'))
10
+ continue;
11
+ let obj;
12
+ try {
13
+ obj = JSON.parse(line);
14
+ }
15
+ catch {
16
+ warnings.push(`unparseable line: ${line.slice(0, 120)}`);
17
+ continue;
18
+ }
19
+ if (typeof obj !== 'object' || obj === null || !('tool' in obj))
20
+ continue;
21
+ const rec = obj;
22
+ if (!VALID_SELECT_TOOL_NAMES.has(rec.tool))
23
+ continue;
24
+ if (typeof rec.name !== 'string' || !rec.name) {
25
+ warnings.push(`${String(rec.tool)} missing name — skipped`);
26
+ continue;
27
+ }
28
+ const call = {
29
+ tool: rec.tool,
30
+ name: rec.name,
31
+ };
32
+ if (typeof rec.reason === 'string')
33
+ call.reason = rec.reason;
34
+ if (typeof rec.confidence === 'number' && rec.confidence >= 1 && rec.confidence <= 5) {
35
+ if (call.tool === 'select_component') {
36
+ call.confidence = rec.confidence;
37
+ }
38
+ else {
39
+ call.confidence = rec.confidence;
40
+ }
41
+ }
42
+ calls.push(call);
43
+ }
44
+ return { calls, warnings };
45
+ }
46
+ const VALID_TOOL_NAMES = new Set(['classify_prop', 'exclude_prop', 'classify_component', 'classify_slot']);
47
+ const VALID_TOKEN_TOOL_NAMES = new Set(['set_token', 'set_group']);
48
+ const VALID_CDF_TYPES = new Set(['string', 'richtext', 'media', 'enum', 'token', 'boolean']);
49
+ const VALID_CATEGORIES = new Set(['content', 'design', 'state']);
50
+ export function parseToolCallLines(stdout) {
51
+ const calls = [];
52
+ const warnings = [];
53
+ for (const raw of stdout.split('\n')) {
54
+ const line = raw.trim();
55
+ if (!line.startsWith('{'))
56
+ continue;
57
+ let obj;
58
+ try {
59
+ obj = JSON.parse(line);
60
+ }
61
+ catch {
62
+ warnings.push(`unparseable line: ${line.slice(0, 120)}`);
63
+ continue;
64
+ }
65
+ if (typeof obj !== 'object' || obj === null || !('tool' in obj))
66
+ continue;
67
+ const rec = obj;
68
+ if (!VALID_TOOL_NAMES.has(rec.tool)) {
69
+ warnings.push(`unknown tool: ${String(rec.tool)}`);
70
+ continue;
71
+ }
72
+ const tool = rec.tool;
73
+ if (tool === 'classify_prop') {
74
+ if (typeof rec.prop !== 'string' || !rec.prop) {
75
+ warnings.push('classify_prop missing prop name — skipped');
76
+ continue;
77
+ }
78
+ if (typeof rec.cdf_type !== 'string' || !VALID_CDF_TYPES.has(rec.cdf_type)) {
79
+ warnings.push(`classify_prop '${rec.prop}': invalid cdf_type '${String(rec.cdf_type)}' — skipped`);
80
+ continue;
81
+ }
82
+ if (typeof rec.cdf_category !== 'string' || !VALID_CATEGORIES.has(rec.cdf_category)) {
83
+ warnings.push(`classify_prop '${rec.prop}': invalid cdf_category '${String(rec.cdf_category)}' — skipped`);
84
+ continue;
85
+ }
86
+ const call = {
87
+ tool: 'classify_prop',
88
+ prop: rec.prop,
89
+ cdf_type: rec.cdf_type,
90
+ cdf_category: rec.cdf_category,
91
+ };
92
+ if (Array.isArray(rec.values) && rec.values.every((v) => typeof v === 'string')) {
93
+ call.values = rec.values;
94
+ }
95
+ if (typeof rec.token_kind === 'string')
96
+ call.token_kind = rec.token_kind;
97
+ if (typeof rec.required === 'boolean')
98
+ call.required = rec.required;
99
+ if (typeof rec.description === 'string')
100
+ call.description = rec.description;
101
+ if (typeof rec.default === 'string' || typeof rec.default === 'boolean')
102
+ call.default = rec.default;
103
+ if (typeof rec.reason === 'string')
104
+ call.reason = rec.reason;
105
+ calls.push(call);
106
+ }
107
+ else if (tool === 'exclude_prop') {
108
+ if (typeof rec.prop !== 'string' || !rec.prop) {
109
+ warnings.push('exclude_prop missing prop name — skipped');
110
+ continue;
111
+ }
112
+ calls.push({
113
+ tool: 'exclude_prop',
114
+ prop: rec.prop,
115
+ reason: typeof rec.reason === 'string' ? rec.reason : '',
116
+ });
117
+ }
118
+ else if (tool === 'classify_component') {
119
+ const call = { tool: 'classify_component' };
120
+ if (typeof rec.description === 'string')
121
+ call.description = rec.description;
122
+ if (typeof rec.rationale === 'object' && rec.rationale !== null) {
123
+ const r = rec.rationale;
124
+ const rationale = {};
125
+ if (typeof r.description === 'string')
126
+ rationale.description = r.description;
127
+ if (typeof r.props === 'string')
128
+ rationale.props = r.props;
129
+ if (typeof r.slots === 'string')
130
+ rationale.slots = r.slots;
131
+ if (Object.keys(rationale).length > 0)
132
+ call.rationale = rationale;
133
+ }
134
+ calls.push(call);
135
+ }
136
+ else if (tool === 'classify_slot') {
137
+ if (typeof rec.slot !== 'string' || !rec.slot) {
138
+ warnings.push('classify_slot missing slot name — skipped');
139
+ continue;
140
+ }
141
+ const call = { tool: 'classify_slot', slot: rec.slot };
142
+ if (typeof rec.required === 'boolean')
143
+ call.required = rec.required;
144
+ if (Array.isArray(rec.allowed_components) && rec.allowed_components.every((v) => typeof v === 'string')) {
145
+ call.allowed_components = rec.allowed_components;
146
+ }
147
+ if (typeof rec.description === 'string')
148
+ call.description = rec.description;
149
+ if (typeof rec.rationale === 'string')
150
+ call.rationale = rec.rationale;
151
+ calls.push(call);
152
+ }
153
+ }
154
+ return { calls, warnings };
155
+ }
156
+ export function parseTokenToolCallLines(stdout) {
157
+ const calls = [];
158
+ const warnings = [];
159
+ for (const raw of stdout.split('\n')) {
160
+ const line = raw.trim();
161
+ if (!line.startsWith('{'))
162
+ continue;
163
+ let obj;
164
+ try {
165
+ obj = JSON.parse(line);
166
+ }
167
+ catch {
168
+ warnings.push(`unparseable line: ${line.slice(0, 120)}`);
169
+ continue;
170
+ }
171
+ if (typeof obj !== 'object' || obj === null || !('tool' in obj))
172
+ continue;
173
+ const rec = obj;
174
+ if (!VALID_TOKEN_TOOL_NAMES.has(rec.tool))
175
+ continue; // not a token call — skip silently
176
+ if (rec.tool === 'set_token') {
177
+ if (typeof rec.path !== 'string' || !rec.path) {
178
+ warnings.push('set_token missing path — skipped');
179
+ continue;
180
+ }
181
+ if (typeof rec.type !== 'string' || !rec.type) {
182
+ warnings.push(`set_token '${rec.path}': missing type — skipped`);
183
+ continue;
184
+ }
185
+ if (!('value' in rec)) {
186
+ warnings.push(`set_token '${rec.path}': missing value — skipped`);
187
+ continue;
188
+ }
189
+ const call = { tool: 'set_token', path: rec.path, type: rec.type, value: rec.value };
190
+ if (typeof rec.description === 'string')
191
+ call.description = rec.description;
192
+ calls.push(call);
193
+ }
194
+ else if (rec.tool === 'set_group') {
195
+ if (typeof rec.path !== 'string' || !rec.path) {
196
+ warnings.push('set_group missing path — skipped');
197
+ continue;
198
+ }
199
+ const call = { tool: 'set_group', path: rec.path };
200
+ if (typeof rec.description === 'string')
201
+ call.description = rec.description;
202
+ calls.push(call);
203
+ }
204
+ }
205
+ return { calls, warnings };
206
+ }
207
+ // --- Agent invocation ---
208
+ const AGENT_BINARIES = {
209
+ claude: 'claude',
210
+ codex: 'codex',
211
+ opencode: 'opencode',
212
+ cursor: 'cursor-agent',
213
+ };
214
+ export function resolveBinary(agent) {
215
+ const envKey = `EDS_AGENT_BINARY_${agent.toUpperCase()}`;
216
+ const override = process.env[envKey];
217
+ if (override && override.trim())
218
+ return override.trim();
219
+ return AGENT_BINARIES[agent];
220
+ }
221
+ /**
222
+ * Default models per agent — lightweight/fast picks to control cost when no
223
+ * explicit model is configured. cursor uses `gpt-mini` (verified alias from
224
+ * GetUsableModels; haiku is not available in cursor's model catalog).
225
+ */
226
+ const DEFAULT_MODELS = {
227
+ claude: 'haiku',
228
+ codex: 'gpt-5.4-mini', // requires OPENAI_API_KEY; ChatGPT account users must pass --model
229
+ opencode: 'claude-haiku-4-5',
230
+ cursor: 'gpt-mini', // cursor alias for gpt-5.4-mini-medium; haiku not in cursor's catalog
231
+ };
232
+ /**
233
+ * Resolve the model for an agent. Explicit flag/creds value wins, then a
234
+ * per-agent `EDS_AGENT_MODEL_<AGENT>` env override (mirrors the
235
+ * `EDS_AGENT_BINARY_<AGENT>` pattern), otherwise the lightweight default for
236
+ * that agent.
237
+ */
238
+ export function resolveAgentModel(agent, explicit) {
239
+ if (explicit && explicit.trim())
240
+ return explicit.trim();
241
+ const override = process.env[`EDS_AGENT_MODEL_${agent.toUpperCase()}`];
242
+ if (override && override.trim())
243
+ return override.trim();
244
+ return DEFAULT_MODELS[agent];
245
+ }
246
+ export function buildArgs(agent, prompt, model, promptViaStdin = false) {
247
+ const modelArg = ['--model', resolveAgentModel(agent, model)];
248
+ // When the prompt is delivered on stdin, omit it from argv — a large prompt
249
+ // as a command-line argument overflows ARG_MAX (spawn E2BIG). All four CLIs
250
+ // read the prompt from stdin when it isn't passed positionally.
251
+ const promptArg = promptViaStdin ? [] : [prompt];
252
+ switch (agent) {
253
+ case 'claude':
254
+ return ['--print', ...modelArg, ...promptArg];
255
+ case 'codex':
256
+ // --dangerously-bypass-approvals-and-sandbox required for non-interactive use
257
+ return ['exec', ...modelArg, '--dangerously-bypass-approvals-and-sandbox', ...promptArg];
258
+ case 'opencode':
259
+ return ['run', ...modelArg, ...promptArg];
260
+ case 'cursor':
261
+ // cursor-agent uses --print for non-interactive stdout output
262
+ return ['--print', ...modelArg, ...promptArg];
263
+ }
264
+ }
265
+ export async function runAgent(options) {
266
+ const { agent, prompt, timeoutMs, model, onOutput, promptViaStdin, onDebugEvent } = options;
267
+ const binary = resolveBinary(agent);
268
+ const useStdin = !!promptViaStdin;
269
+ const args = buildArgs(agent, prompt, model, useStdin);
270
+ const startedAt = Date.now();
271
+ onDebugEvent?.('run.start', {
272
+ agent,
273
+ binary,
274
+ model,
275
+ timeoutMs,
276
+ promptLen: prompt.length,
277
+ promptHead: prompt.slice(0, 500),
278
+ });
279
+ return new Promise((resolve) => {
280
+ const child = spawn(binary, args, {
281
+ stdio: ['pipe', 'pipe', 'pipe'],
282
+ });
283
+ if (useStdin && child.stdin) {
284
+ // Guard against EPIPE: the child may close stdin before we finish
285
+ // writing (fast exit, or it stops reading). Swallow the write error —
286
+ // the child's own exit code/stderr is the source of truth.
287
+ child.stdin.on('error', () => { });
288
+ child.stdin.write(prompt, () => {
289
+ child.stdin?.end();
290
+ });
291
+ }
292
+ else {
293
+ child.stdin?.end();
294
+ }
295
+ let stdout = '';
296
+ let stderr = '';
297
+ let timedOut = false;
298
+ const timer = setTimeout(() => {
299
+ timedOut = true;
300
+ child.kill('SIGTERM');
301
+ }, timeoutMs);
302
+ child.stdout?.on('data', (chunk) => {
303
+ const text = chunk.toString();
304
+ stdout += text;
305
+ onOutput?.(text);
306
+ });
307
+ child.stderr?.on('data', (chunk) => {
308
+ stderr += chunk.toString();
309
+ });
310
+ child.on('close', (code, signal) => {
311
+ clearTimeout(timer);
312
+ const result = {
313
+ exitCode: signal ? 1 : (code ?? 1),
314
+ stdout,
315
+ stderr,
316
+ timedOut,
317
+ };
318
+ onDebugEvent?.('run.end', {
319
+ agent,
320
+ model,
321
+ durationMs: Date.now() - startedAt,
322
+ exitCode: result.exitCode,
323
+ signal,
324
+ timedOut,
325
+ stdoutLen: stdout.length,
326
+ stderrLen: stderr.length,
327
+ stderrTail: stderr.slice(-1000),
328
+ });
329
+ resolve(result);
330
+ });
331
+ });
332
+ }
333
+ export async function checkAgentAuth(agent) {
334
+ const binary = resolveBinary(agent);
335
+ // Verify the selected agent's binary exists first — for EVERY agent, not
336
+ // just claude. When `binary` is an absolute path (e.g. set via
337
+ // EDS_AGENT_BINARY_<AGENT>=/opt/custom/bin), `which` on some shells doesn't
338
+ // resolve it — check the filesystem directly for absolute paths, and fall
339
+ // back to `which` for bare names on $PATH.
340
+ const binaryExists = await new Promise((resolve) => {
341
+ if (binary.startsWith('/')) {
342
+ import('node:fs/promises').then((fs) => fs.access(binary).then(() => resolve(true), () => resolve(false)));
343
+ return;
344
+ }
345
+ const child = spawn('which', [binary], { stdio: 'ignore' });
346
+ child.on('close', (code) => resolve(code === 0));
347
+ });
348
+ if (!binaryExists)
349
+ return 'not-found';
350
+ // Only Claude exposes `auth status --json`. Non-Claude agents are considered
351
+ // authenticated once their binary is present — never gate them on claude
352
+ // (e.g. Codex-via-Bedrock users would otherwise be blocked by a claude check).
353
+ if (agent !== 'claude')
354
+ return 'ok';
355
+ // Use `claude auth status` — fast, no API call, works regardless of which
356
+ // auth provider (direct, Bedrock, Vertex) or whether AWS_PROFILE is set.
357
+ return new Promise((resolve) => {
358
+ const child = spawn(binary, ['auth', 'status', '--json'], {
359
+ stdio: ['ignore', 'pipe', 'pipe'],
360
+ });
361
+ let stdout = '';
362
+ let done = false;
363
+ const timer = setTimeout(() => {
364
+ if (!done) {
365
+ done = true;
366
+ child.kill('SIGTERM');
367
+ resolve('unauthenticated');
368
+ }
369
+ }, 5000);
370
+ child.stdout?.on('data', (chunk) => {
371
+ stdout += chunk.toString();
372
+ });
373
+ child.on('close', (code) => {
374
+ if (done)
375
+ return;
376
+ done = true;
377
+ clearTimeout(timer);
378
+ if (code !== 0) {
379
+ resolve('unauthenticated');
380
+ return;
381
+ }
382
+ try {
383
+ const status = JSON.parse(stdout);
384
+ resolve(status.loggedIn ? 'ok' : 'unauthenticated');
385
+ }
386
+ catch {
387
+ resolve('unauthenticated');
388
+ }
389
+ });
390
+ });
391
+ }
392
+ /**
393
+ * Build a diagnostic string from a failed agent run, surfacing the agent's own
394
+ * stderr (or stdout, when stderr is empty) so callers never emit a context-free
395
+ * "agent failed". Callers only invoke this on a failed run: either a non-zero
396
+ * exit, or a zero exit that produced no tool calls. A non-zero exit is reported
397
+ * as such; a zero exit is therefore the "produced no tool calls" case.
398
+ */
399
+ export function describeAgentFailure(result, maxDetail = 800) {
400
+ const base = result.exitCode !== 0 ? `agent exited with code ${result.exitCode}` : 'agent produced no tool calls';
401
+ const detail = (result.stderr.trim() || result.stdout.trim()).slice(-maxDetail).trim();
402
+ return detail ? `${base} — ${detail}` : base;
403
+ }
404
+ export function extractSentinelOutput(stdout) {
405
+ const START = '<<<EDS_OUTPUT_START>>>';
406
+ const END = '<<<EDS_OUTPUT_END>>>';
407
+ const startIdx = stdout.indexOf(START);
408
+ const endIdx = stdout.indexOf(END);
409
+ if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx)
410
+ return null;
411
+ // Check for multiple blocks
412
+ const secondStart = stdout.indexOf(START, startIdx + START.length);
413
+ if (secondStart !== -1 && secondStart < endIdx)
414
+ return 'multiple';
415
+ const afterStart = stdout.indexOf(END, startIdx);
416
+ const secondEnd = stdout.indexOf(END, afterStart + END.length);
417
+ if (secondEnd !== -1)
418
+ return 'multiple';
419
+ return stdout.slice(startIdx + START.length, endIdx).trim();
420
+ }