@contentful/experience-design-system-cli 2.18.1-dev-build-b4ecc99.0 → 2.18.1-dev-build-180092e.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,427 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { getDebugLogger } from '../lib/debug-logger.js';
3
- export { AGENT_NAMES, isAgentName } from '../lib/agent-names.js';
4
- const VALID_SELECT_TOOL_NAMES = new Set(['select_component', 'reject_component']);
5
- export function parseSelectToolCallLines(stdout) {
6
- const calls = [];
7
- const warnings = [];
8
- for (const raw of stdout.split('\n')) {
9
- const line = raw.trim();
10
- if (!line.startsWith('{'))
11
- continue;
12
- let obj;
13
- try {
14
- obj = JSON.parse(line);
15
- }
16
- catch {
17
- warnings.push(`unparseable line: ${line.slice(0, 120)}`);
18
- continue;
19
- }
20
- if (typeof obj !== 'object' || obj === null || !('tool' in obj))
21
- continue;
22
- const rec = obj;
23
- if (!VALID_SELECT_TOOL_NAMES.has(rec.tool))
24
- continue;
25
- if (typeof rec.name !== 'string' || !rec.name) {
26
- warnings.push(`${String(rec.tool)} missing name — skipped`);
27
- continue;
28
- }
29
- const call = {
30
- tool: rec.tool,
31
- name: rec.name,
32
- };
33
- if (typeof rec.reason === 'string')
34
- call.reason = rec.reason;
35
- if (typeof rec.confidence === 'number' && rec.confidence >= 1 && rec.confidence <= 5) {
36
- if (call.tool === 'select_component') {
37
- call.confidence = rec.confidence;
38
- }
39
- else {
40
- call.confidence = rec.confidence;
41
- }
42
- }
43
- calls.push(call);
44
- }
45
- return { calls, warnings };
46
- }
47
- const VALID_TOOL_NAMES = new Set(['classify_prop', 'exclude_prop', 'classify_component', 'classify_slot']);
48
- const VALID_TOKEN_TOOL_NAMES = new Set(['set_token', 'set_group']);
49
- const VALID_CDF_TYPES = new Set(['string', 'richtext', 'media', 'enum', 'token', 'boolean']);
50
- const VALID_CATEGORIES = new Set(['content', 'design', 'state']);
51
- export function parseToolCallLines(stdout) {
52
- const calls = [];
53
- const warnings = [];
54
- for (const raw of stdout.split('\n')) {
55
- const line = raw.trim();
56
- if (!line.startsWith('{'))
57
- continue;
58
- let obj;
59
- try {
60
- obj = JSON.parse(line);
61
- }
62
- catch {
63
- warnings.push(`unparseable line: ${line.slice(0, 120)}`);
64
- continue;
65
- }
66
- if (typeof obj !== 'object' || obj === null || !('tool' in obj))
67
- continue;
68
- const rec = obj;
69
- if (!VALID_TOOL_NAMES.has(rec.tool)) {
70
- warnings.push(`unknown tool: ${String(rec.tool)}`);
71
- continue;
72
- }
73
- const tool = rec.tool;
74
- if (tool === 'classify_prop') {
75
- if (typeof rec.prop !== 'string' || !rec.prop) {
76
- warnings.push('classify_prop missing prop name — skipped');
77
- continue;
78
- }
79
- if (typeof rec.cdf_type !== 'string' || !VALID_CDF_TYPES.has(rec.cdf_type)) {
80
- warnings.push(`classify_prop '${rec.prop}': invalid cdf_type '${String(rec.cdf_type)}' — skipped`);
81
- continue;
82
- }
83
- if (typeof rec.cdf_category !== 'string' || !VALID_CATEGORIES.has(rec.cdf_category)) {
84
- warnings.push(`classify_prop '${rec.prop}': invalid cdf_category '${String(rec.cdf_category)}' — skipped`);
85
- continue;
86
- }
87
- const call = {
88
- tool: 'classify_prop',
89
- prop: rec.prop,
90
- cdf_type: rec.cdf_type,
91
- cdf_category: rec.cdf_category,
92
- };
93
- if (Array.isArray(rec.values) && rec.values.every((v) => typeof v === 'string')) {
94
- call.values = rec.values;
95
- }
96
- if (typeof rec.token_kind === 'string')
97
- call.token_kind = rec.token_kind;
98
- if (typeof rec.required === 'boolean')
99
- call.required = rec.required;
100
- if (typeof rec.description === 'string')
101
- call.description = rec.description;
102
- if (typeof rec.default === 'string' || typeof rec.default === 'boolean')
103
- call.default = rec.default;
104
- if (typeof rec.reason === 'string')
105
- call.reason = rec.reason;
106
- calls.push(call);
107
- }
108
- else if (tool === 'exclude_prop') {
109
- if (typeof rec.prop !== 'string' || !rec.prop) {
110
- warnings.push('exclude_prop missing prop name — skipped');
111
- continue;
112
- }
113
- calls.push({
114
- tool: 'exclude_prop',
115
- prop: rec.prop,
116
- reason: typeof rec.reason === 'string' ? rec.reason : '',
117
- });
118
- }
119
- else if (tool === 'classify_component') {
120
- const call = { tool: 'classify_component' };
121
- if (typeof rec.description === 'string')
122
- call.description = rec.description;
123
- if (typeof rec.rationale === 'object' && rec.rationale !== null) {
124
- const r = rec.rationale;
125
- const rationale = {};
126
- if (typeof r.description === 'string')
127
- rationale.description = r.description;
128
- if (typeof r.props === 'string')
129
- rationale.props = r.props;
130
- if (typeof r.slots === 'string')
131
- rationale.slots = r.slots;
132
- if (Object.keys(rationale).length > 0)
133
- call.rationale = rationale;
134
- }
135
- calls.push(call);
136
- }
137
- else if (tool === 'classify_slot') {
138
- if (typeof rec.slot !== 'string' || !rec.slot) {
139
- warnings.push('classify_slot missing slot name — skipped');
140
- continue;
141
- }
142
- const call = { tool: 'classify_slot', slot: rec.slot };
143
- if (typeof rec.required === 'boolean')
144
- call.required = rec.required;
145
- if (Array.isArray(rec.allowed_components) && rec.allowed_components.every((v) => typeof v === 'string')) {
146
- call.allowed_components = rec.allowed_components;
147
- }
148
- if (typeof rec.description === 'string')
149
- call.description = rec.description;
150
- if (typeof rec.rationale === 'string')
151
- call.rationale = rec.rationale;
152
- calls.push(call);
153
- }
154
- }
155
- return { calls, warnings };
156
- }
157
- export function parseTokenToolCallLines(stdout) {
158
- const calls = [];
159
- const warnings = [];
160
- for (const raw of stdout.split('\n')) {
161
- const line = raw.trim();
162
- if (!line.startsWith('{'))
163
- continue;
164
- let obj;
165
- try {
166
- obj = JSON.parse(line);
167
- }
168
- catch {
169
- warnings.push(`unparseable line: ${line.slice(0, 120)}`);
170
- continue;
171
- }
172
- if (typeof obj !== 'object' || obj === null || !('tool' in obj))
173
- continue;
174
- const rec = obj;
175
- if (!VALID_TOKEN_TOOL_NAMES.has(rec.tool))
176
- continue; // not a token call — skip silently
177
- if (rec.tool === 'set_token') {
178
- if (typeof rec.path !== 'string' || !rec.path) {
179
- warnings.push('set_token missing path — skipped');
180
- continue;
181
- }
182
- if (typeof rec.type !== 'string' || !rec.type) {
183
- warnings.push(`set_token '${rec.path}': missing type — skipped`);
184
- continue;
185
- }
186
- if (!('value' in rec)) {
187
- warnings.push(`set_token '${rec.path}': missing value — skipped`);
188
- continue;
189
- }
190
- const call = { tool: 'set_token', path: rec.path, type: rec.type, value: rec.value };
191
- if (typeof rec.description === 'string')
192
- call.description = rec.description;
193
- calls.push(call);
194
- }
195
- else if (rec.tool === 'set_group') {
196
- if (typeof rec.path !== 'string' || !rec.path) {
197
- warnings.push('set_group missing path — skipped');
198
- continue;
199
- }
200
- const call = { tool: 'set_group', path: rec.path };
201
- if (typeof rec.description === 'string')
202
- call.description = rec.description;
203
- calls.push(call);
204
- }
205
- }
206
- return { calls, warnings };
207
- }
208
- // --- Agent invocation ---
209
- const AGENT_BINARIES = {
210
- claude: 'claude',
211
- codex: 'codex',
212
- opencode: 'opencode',
213
- cursor: 'cursor-agent',
214
- };
215
- export function resolveBinary(agent) {
216
- const envKey = `EDS_AGENT_BINARY_${agent.toUpperCase()}`;
217
- const override = process.env[envKey];
218
- if (override && override.trim())
219
- return override.trim();
220
- return AGENT_BINARIES[agent];
221
- }
222
- /**
223
- * Default models per agent — lightweight/fast picks to control cost when no
224
- * explicit model is configured. cursor uses `gpt-mini` (verified alias from
225
- * GetUsableModels; haiku is not available in cursor's model catalog).
226
- */
227
- const DEFAULT_MODELS = {
228
- claude: 'haiku',
229
- codex: 'gpt-5.4-mini', // requires OPENAI_API_KEY; ChatGPT account users must pass --model
230
- opencode: 'claude-haiku-4-5',
231
- cursor: 'gpt-mini', // cursor alias for gpt-5.4-mini-medium; haiku not in cursor's catalog
232
- };
233
- /**
234
- * Resolve the model for an agent. Explicit flag/creds value wins, then a
235
- * per-agent `EDS_AGENT_MODEL_<AGENT>` env override (mirrors the
236
- * `EDS_AGENT_BINARY_<AGENT>` pattern), otherwise the lightweight default for
237
- * that agent.
238
- */
239
- export function resolveAgentModel(agent, explicit) {
240
- if (explicit && explicit.trim())
241
- return explicit.trim();
242
- const override = process.env[`EDS_AGENT_MODEL_${agent.toUpperCase()}`];
243
- if (override && override.trim())
244
- return override.trim();
245
- return DEFAULT_MODELS[agent];
246
- }
247
- export function buildArgs(agent, prompt, model, promptViaStdin = false) {
248
- const modelArg = ['--model', resolveAgentModel(agent, model)];
249
- // When the prompt is delivered on stdin, omit it from argv — a large prompt
250
- // as a command-line argument overflows ARG_MAX (spawn E2BIG). All four CLIs
251
- // read the prompt from stdin when it isn't passed positionally.
252
- const promptArg = promptViaStdin ? [] : [prompt];
253
- switch (agent) {
254
- case 'claude':
255
- return ['--print', ...modelArg, ...promptArg];
256
- case 'codex':
257
- // --dangerously-bypass-approvals-and-sandbox required for non-interactive use
258
- return ['exec', ...modelArg, '--dangerously-bypass-approvals-and-sandbox', ...promptArg];
259
- case 'opencode':
260
- return ['run', ...modelArg, ...promptArg];
261
- case 'cursor':
262
- // cursor-agent uses --print for non-interactive stdout output
263
- return ['--print', ...modelArg, ...promptArg];
264
- }
265
- }
266
- export async function runAgent(options) {
267
- const { agent, prompt, interactive, timeoutMs, model, onOutput, promptViaStdin } = options;
268
- const binary = resolveBinary(agent);
269
- const useStdin = !!promptViaStdin && !interactive;
270
- const args = buildArgs(agent, prompt, model, useStdin);
271
- const debug = getDebugLogger();
272
- const startedAt = Date.now();
273
- debug.event('agent', 'run.start', {
274
- agent,
275
- binary,
276
- model,
277
- interactive,
278
- timeoutMs,
279
- promptLen: prompt.length,
280
- promptHead: prompt.slice(0, 500),
281
- });
282
- return new Promise((resolve) => {
283
- const child = spawn(binary, args, {
284
- stdio: interactive ? 'inherit' : ['pipe', 'pipe', 'pipe'],
285
- });
286
- if (!interactive) {
287
- if (useStdin && child.stdin) {
288
- // Guard against EPIPE: the child may close stdin before we finish
289
- // writing (fast exit, or it stops reading). Swallow the write error —
290
- // the child's own exit code/stderr is the source of truth.
291
- child.stdin.on('error', () => { });
292
- child.stdin.write(prompt, () => {
293
- child.stdin?.end();
294
- });
295
- }
296
- else {
297
- child.stdin?.end();
298
- }
299
- }
300
- let stdout = '';
301
- let stderr = '';
302
- let timedOut = false;
303
- const timer = setTimeout(() => {
304
- timedOut = true;
305
- child.kill('SIGTERM');
306
- }, timeoutMs);
307
- if (!interactive) {
308
- child.stdout?.on('data', (chunk) => {
309
- const text = chunk.toString();
310
- stdout += text;
311
- onOutput?.(text);
312
- });
313
- child.stderr?.on('data', (chunk) => {
314
- stderr += chunk.toString();
315
- });
316
- }
317
- child.on('close', (code, signal) => {
318
- clearTimeout(timer);
319
- const result = {
320
- exitCode: signal ? 1 : (code ?? 1),
321
- stdout,
322
- stderr,
323
- timedOut,
324
- };
325
- debug.event('agent', 'run.end', {
326
- agent,
327
- model,
328
- durationMs: Date.now() - startedAt,
329
- exitCode: result.exitCode,
330
- signal,
331
- timedOut,
332
- stdoutLen: stdout.length,
333
- stderrLen: stderr.length,
334
- stderrTail: stderr.slice(-1000),
335
- });
336
- resolve(result);
337
- });
338
- });
339
- }
340
- export async function checkAgentAuth(agent) {
341
- const binary = resolveBinary(agent);
342
- // Verify the selected agent's binary exists first — for EVERY agent, not
343
- // just claude. When `binary` is an absolute path (e.g. set via
344
- // EDS_AGENT_BINARY_<AGENT>=/opt/custom/bin), `which` on some shells doesn't
345
- // resolve it — check the filesystem directly for absolute paths, and fall
346
- // back to `which` for bare names on $PATH.
347
- const binaryExists = await new Promise((resolve) => {
348
- if (binary.startsWith('/')) {
349
- import('node:fs/promises').then((fs) => fs.access(binary).then(() => resolve(true), () => resolve(false)));
350
- return;
351
- }
352
- const child = spawn('which', [binary], { stdio: 'ignore' });
353
- child.on('close', (code) => resolve(code === 0));
354
- });
355
- if (!binaryExists)
356
- return 'not-found';
357
- // Only Claude exposes `auth status --json`. Non-Claude agents are considered
358
- // authenticated once their binary is present — never gate them on claude
359
- // (e.g. Codex-via-Bedrock users would otherwise be blocked by a claude check).
360
- if (agent !== 'claude')
361
- return 'ok';
362
- // Use `claude auth status` — fast, no API call, works regardless of which
363
- // auth provider (direct, Bedrock, Vertex) or whether AWS_PROFILE is set.
364
- return new Promise((resolve) => {
365
- const child = spawn(binary, ['auth', 'status', '--json'], {
366
- stdio: ['ignore', 'pipe', 'pipe'],
367
- });
368
- let stdout = '';
369
- let done = false;
370
- const timer = setTimeout(() => {
371
- if (!done) {
372
- done = true;
373
- child.kill('SIGTERM');
374
- resolve('unauthenticated');
375
- }
376
- }, 5000);
377
- child.stdout?.on('data', (chunk) => {
378
- stdout += chunk.toString();
379
- });
380
- child.on('close', (code) => {
381
- if (done)
382
- return;
383
- done = true;
384
- clearTimeout(timer);
385
- if (code !== 0) {
386
- resolve('unauthenticated');
387
- return;
388
- }
389
- try {
390
- const status = JSON.parse(stdout);
391
- resolve(status.loggedIn ? 'ok' : 'unauthenticated');
392
- }
393
- catch {
394
- resolve('unauthenticated');
395
- }
396
- });
397
- });
398
- }
399
- /**
400
- * Build a diagnostic string from a failed agent run, surfacing the agent's own
401
- * stderr (or stdout, when stderr is empty) so callers never emit a context-free
402
- * "agent failed". Callers only invoke this on a failed run: either a non-zero
403
- * exit, or a zero exit that produced no tool calls. A non-zero exit is reported
404
- * as such; a zero exit is therefore the "produced no tool calls" case.
405
- */
406
- export function describeAgentFailure(result, maxDetail = 800) {
407
- const base = result.exitCode !== 0 ? `agent exited with code ${result.exitCode}` : 'agent produced no tool calls';
408
- const detail = (result.stderr.trim() || result.stdout.trim()).slice(-maxDetail).trim();
409
- return detail ? `${base} — ${detail}` : base;
410
- }
411
- export function extractSentinelOutput(stdout) {
412
- const START = '<<<EDS_OUTPUT_START>>>';
413
- const END = '<<<EDS_OUTPUT_END>>>';
414
- const startIdx = stdout.indexOf(START);
415
- const endIdx = stdout.indexOf(END);
416
- if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx)
417
- return null;
418
- // Check for multiple blocks
419
- const secondStart = stdout.indexOf(START, startIdx + START.length);
420
- if (secondStart !== -1 && secondStart < endIdx)
421
- return 'multiple';
422
- const afterStart = stdout.indexOf(END, startIdx);
423
- const secondEnd = stdout.indexOf(END, afterStart + END.length);
424
- if (secondEnd !== -1)
425
- return 'multiple';
426
- return stdout.slice(startIdx + START.length, endIdx).trim();
427
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * Formats a structured generation-progress line for the wizard parser.
3
- *
4
- * Emitted at terminal completion of each component (success, cache hit,
5
- * pinned hit, or final failure). `done` represents the count of completed
6
- * components, which is monotonically non-decreasing — unlike the legacy
7
- * `[index+1/total]` line, which reports input position.
8
- */
9
- export declare function formatGenerateProgressLine(done: number, total: number, name: string): string;
@@ -1,11 +0,0 @@
1
- /**
2
- * Formats a structured generation-progress line for the wizard parser.
3
- *
4
- * Emitted at terminal completion of each component (success, cache hit,
5
- * pinned hit, or final failure). `done` represents the count of completed
6
- * components, which is monotonically non-decreasing — unlike the legacy
7
- * `[index+1/total]` line, which reports input position.
8
- */
9
- export function formatGenerateProgressLine(done, total, name) {
10
- return `progress=generate:${done}/${total}:${name}`;
11
- }
@@ -1,26 +0,0 @@
1
- /** `components` — classify component props; `tokens` — classify design tokens; `select` — decide whether a component belongs in Contentful Experience Orchestration */
2
- export type Skill = 'components' | 'tokens' | 'select';
3
- export type Mode = 'autonomous' | 'interactive';
4
- export interface PromptOptions {
5
- skill: Skill;
6
- mode: Mode;
7
- rawComponentsInline?: string;
8
- rawTokensInline?: string;
9
- /** Original filename for raw tokens — used to set the correct code fence language. */
10
- rawTokensFilename?: string;
11
- tokensInline?: string;
12
- tokenMapInline?: string;
13
- outDir: string;
14
- /** For components skill only: the single component's name (used in error messages). */
15
- componentName?: string;
16
- /**
17
- * Feature 8: custom prompt path override. When set, this absolute or relative
18
- * `.md` path is read in place of the bundled skill file. The bundled-prompt
19
- * invariants (utility-wrapper rejection, description content rules, etc.) do
20
- * NOT apply under an override — callers are responsible for showing the
21
- * appropriate warning banner.
22
- */
23
- skillPathOverride?: string;
24
- }
25
- export declare function buildPrompt(options: PromptOptions): Promise<string>;
26
- export declare function resolveSkillPath(skill: Skill): string;