@claudiv/cli 0.1.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.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Unified Claude client interface (abstracts CLI vs API mode)
3
+ */
4
+ import type { Config, HierarchyContext } from '@claudiv/core';
5
+ export interface ClaudeClient {
6
+ sendPrompt(userMessage: string, context: HierarchyContext): AsyncGenerator<string>;
7
+ checkAvailable(): Promise<boolean>;
8
+ }
9
+ /**
10
+ * Create Claude client based on configuration
11
+ */
12
+ export declare function createClaudeClient(config: Config): ClaudeClient;
13
+ /**
14
+ * Verify Claude is available before starting
15
+ */
16
+ export declare function verifyClaudeAvailable(client: ClaudeClient, mode: 'cli' | 'api'): Promise<void>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Unified Claude client interface (abstracts CLI vs API mode)
3
+ */
4
+ import { ClaudeCLIClient } from './claude-cli.js';
5
+ import { ClaudeAPIClient } from './claude-api.js';
6
+ import { logger } from './utils/logger.js';
7
+ /**
8
+ * Create Claude client based on configuration
9
+ */
10
+ export function createClaudeClient(config) {
11
+ if (config.mode === 'cli') {
12
+ logger.info('Using Claude Code CLI (subscription mode)');
13
+ return new ClaudeCLIClient();
14
+ }
15
+ else {
16
+ logger.info('Using Anthropic API (pay-per-use mode)');
17
+ if (!config.apiKey) {
18
+ logger.error('API key is required for API mode');
19
+ throw new Error('Missing API key');
20
+ }
21
+ return new ClaudeAPIClient(config.apiKey);
22
+ }
23
+ }
24
+ /**
25
+ * Verify Claude is available before starting
26
+ */
27
+ export async function verifyClaudeAvailable(client, mode) {
28
+ logger.processing(`Checking ${mode === 'cli' ? 'Claude Code CLI' : 'Claude API'} availability...`);
29
+ const isAvailable = await client.checkAvailable();
30
+ if (!isAvailable) {
31
+ if (mode === 'cli') {
32
+ logger.error('Claude Code CLI is not installed or not working');
33
+ logger.info('Install Claude Code: https://code.claude.com');
34
+ logger.info('Or switch to API mode by setting MODE=api in .env');
35
+ }
36
+ else {
37
+ logger.error('Claude API is not available');
38
+ logger.info('Check your ANTHROPIC_API_KEY in .env');
39
+ logger.info('Or switch to CLI mode by setting MODE=cli in .env');
40
+ }
41
+ process.exit(1);
42
+ }
43
+ logger.success(`${mode === 'cli' ? 'Claude Code CLI' : 'Claude API'} is ready`);
44
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Configuration loader for GUI-driven spec editor
3
+ */
4
+ import type { Config } from '@claudiv/core';
5
+ /**
6
+ * Load and validate configuration
7
+ */
8
+ export declare function loadConfig(): Config;
package/dist/config.js ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Configuration loader for GUI-driven spec editor
3
+ */
4
+ import dotenv from 'dotenv';
5
+ import { existsSync, readdirSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { logger } from './utils/logger.js';
8
+ // Load .env file
9
+ dotenv.config();
10
+ /**
11
+ * Load and validate configuration
12
+ */
13
+ export function loadConfig() {
14
+ // Determine mode
15
+ const mode = (process.env.MODE?.toLowerCase() || 'cli');
16
+ if (mode !== 'cli' && mode !== 'api') {
17
+ logger.error(`Invalid MODE: ${process.env.MODE}. Must be 'cli' or 'api'`);
18
+ process.exit(1);
19
+ }
20
+ // Get API key if in API mode
21
+ let apiKey;
22
+ if (mode === 'api') {
23
+ apiKey = process.env.ANTHROPIC_API_KEY;
24
+ if (!apiKey) {
25
+ logger.error('ANTHROPIC_API_KEY is required when MODE=api');
26
+ logger.info('Set ANTHROPIC_API_KEY in .env file or environment');
27
+ process.exit(1);
28
+ }
29
+ }
30
+ // Spec file location - look for .cdml files
31
+ // Check if user specified a file via CLI argument
32
+ const cliFile = process.argv[2];
33
+ let specFile;
34
+ if (cliFile) {
35
+ specFile = join(process.cwd(), cliFile);
36
+ if (!existsSync(specFile)) {
37
+ logger.error(`File not found: ${cliFile}`);
38
+ process.exit(1);
39
+ }
40
+ }
41
+ else {
42
+ // Look for .cdml files in current directory
43
+ const files = readdirSync(process.cwd());
44
+ const cdmlFiles = files.filter(f => f.endsWith('.cdml'));
45
+ if (cdmlFiles.length === 0) {
46
+ logger.error('No .cdml files found in current directory');
47
+ logger.info('Create a .cdml file to get started (e.g., app.cdml)');
48
+ logger.info('Example: <app><button gen>Make a blue button</button></app>');
49
+ process.exit(1);
50
+ }
51
+ if (cdmlFiles.length > 1) {
52
+ logger.warn(`Multiple .cdml files found: ${cdmlFiles.join(', ')}`);
53
+ logger.info(`Using ${cdmlFiles[0]} (specify file as argument to use a different one)`);
54
+ }
55
+ specFile = join(process.cwd(), cdmlFiles[0]);
56
+ }
57
+ // Configuration values
58
+ const config = {
59
+ mode,
60
+ apiKey,
61
+ specFile,
62
+ debounceMs: 300,
63
+ claudeTimeout: 60000, // 60 seconds
64
+ };
65
+ logger.debug(`Configuration loaded: mode=${mode}, specFile=${specFile}`);
66
+ return config;
67
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Vite dev server for hosting generated code
3
+ */
4
+ export declare class DevServer {
5
+ private server;
6
+ private port;
7
+ start(): Promise<void>;
8
+ stop(): Promise<void>;
9
+ getPort(): number;
10
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Vite dev server for hosting generated code
3
+ */
4
+ import { createServer } from 'vite';
5
+ import { existsSync } from 'fs';
6
+ import { logger } from './utils/logger.js';
7
+ export class DevServer {
8
+ server = null;
9
+ port = 30004;
10
+ async start() {
11
+ try {
12
+ // Create Vite server
13
+ this.server = await createServer({
14
+ root: process.cwd(),
15
+ server: {
16
+ port: this.port,
17
+ strictPort: false, // Auto-increment if port is busy
18
+ open: true, // Open browser automatically
19
+ },
20
+ plugins: [
21
+ {
22
+ name: 'spec-code-server',
23
+ configureServer(server) {
24
+ server.middlewares.use(async (req, res, next) => {
25
+ // Serve generated code as the root
26
+ const outputFile = 'app.html'; // TODO: Make configurable
27
+ if (req.url === '/' || req.url === '/index.html') {
28
+ if (existsSync(outputFile)) {
29
+ // Read and serve the file directly
30
+ const { readFile } = await import('fs/promises');
31
+ const content = await readFile(outputFile, 'utf-8');
32
+ res.statusCode = 200;
33
+ res.setHeader('Content-Type', 'text/html');
34
+ res.end(content);
35
+ return;
36
+ }
37
+ else {
38
+ // Serve a placeholder if output file doesn't exist yet
39
+ res.statusCode = 200;
40
+ res.setHeader('Content-Type', 'text/html');
41
+ res.end(`
42
+ <!DOCTYPE html>
43
+ <html>
44
+ <head>
45
+ <title>Waiting for generated code</title>
46
+ <meta http-equiv="refresh" content="2">
47
+ <style>
48
+ body {
49
+ font-family: system-ui, -apple-system, sans-serif;
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: center;
53
+ height: 100vh;
54
+ margin: 0;
55
+ background: #f5f5f5;
56
+ }
57
+ .message {
58
+ text-align: center;
59
+ padding: 2rem;
60
+ background: white;
61
+ border-radius: 8px;
62
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
63
+ }
64
+ .spinner {
65
+ border: 3px solid #f3f3f3;
66
+ border-top: 3px solid #3498db;
67
+ border-radius: 50%;
68
+ width: 40px;
69
+ height: 40px;
70
+ animation: spin 1s linear infinite;
71
+ margin: 1rem auto;
72
+ }
73
+ @keyframes spin {
74
+ 0% { transform: rotate(0deg); }
75
+ 100% { transform: rotate(360deg); }
76
+ }
77
+ </style>
78
+ </head>
79
+ <body>
80
+ <div class="message">
81
+ <div class="spinner"></div>
82
+ <h2>Waiting for code generation...</h2>
83
+ <p>Add a <code>gen</code> attribute to elements in your .cdml file to trigger generation.</p>
84
+ </div>
85
+ </body>
86
+ </html>
87
+ `);
88
+ return;
89
+ }
90
+ }
91
+ next();
92
+ });
93
+ },
94
+ },
95
+ ],
96
+ });
97
+ await this.server.listen();
98
+ const info = this.server.config.logger.info;
99
+ this.port = this.server.config.server.port || this.port;
100
+ logger.success(`🌐 Dev server running at http://localhost:${this.port}`);
101
+ logger.info('💡 Browser will auto-reload when code is regenerated');
102
+ }
103
+ catch (error) {
104
+ const err = error;
105
+ logger.error(`Failed to start dev server: ${err.message}`);
106
+ throw error;
107
+ }
108
+ }
109
+ async stop() {
110
+ if (this.server) {
111
+ await this.server.close();
112
+ logger.info('Dev server stopped');
113
+ }
114
+ }
115
+ getPort() {
116
+ return this.port;
117
+ }
118
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Main orchestrator for GUI-driven spec editor
3
+ */
4
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Main orchestrator for GUI-driven spec editor
3
+ */
4
+ import { readFile, writeFile, chmod } from 'fs/promises';
5
+ import { existsSync } from 'fs';
6
+ import { loadConfig } from './config.js';
7
+ import { logger } from './utils/logger.js';
8
+ import { SpecFileWatcher } from './watcher.js';
9
+ import { parseSpecFile, generateCode, extractCodeBlocks } from '@claudiv/core';
10
+ import { createClaudeClient, verifyClaudeAvailable } from './claude-client.js';
11
+ import { updateSpecWithResponse } from './updater.js';
12
+ import { DevServer } from './dev-server.js';
13
+ /**
14
+ * Main application entry point
15
+ */
16
+ async function main() {
17
+ logger.info('🚀 GUI-driven spec editor starting...');
18
+ // Load configuration
19
+ const config = loadConfig();
20
+ // Create Claude client
21
+ const claudeClient = createClaudeClient(config);
22
+ // Verify Claude is available
23
+ await verifyClaudeAvailable(claudeClient, config.mode);
24
+ // TODO: Implement regeneration for multi-file .cdml architecture
25
+ // (spec.cdml, rules.cdml, models.cdml, tests.cdml)
26
+ // For now, generation happens on-demand through chat patterns
27
+ // Create file watcher
28
+ const watcher = new SpecFileWatcher(config);
29
+ // Handle file changes
30
+ watcher.on('change', async (filePath) => {
31
+ try {
32
+ await processSpecFile(filePath, config, claudeClient, watcher);
33
+ }
34
+ catch (error) {
35
+ const err = error;
36
+ logger.error(`Error processing ${filePath}: ${err.message}`);
37
+ if (process.env.DEBUG) {
38
+ console.error(err.stack);
39
+ }
40
+ }
41
+ });
42
+ // Start watching
43
+ watcher.start();
44
+ logger.success(`✓ Watching ${config.specFile.split('/').pop()} for changes...`);
45
+ logger.info('💡 Tip: Add gen/retry/undo attribute to any element to trigger AI');
46
+ logger.info('💡 Example: <create-button gen>Make a blue button</create-button>');
47
+ logger.info('💡 Example: <my-button color="blue" size="large" gen />');
48
+ logger.info('');
49
+ // Start Vite dev server
50
+ const devServer = new DevServer();
51
+ await devServer.start();
52
+ logger.info('');
53
+ // Handle graceful shutdown
54
+ const shutdown = async () => {
55
+ logger.info('\n👋 Shutting down...');
56
+ watcher.stop();
57
+ await devServer.stop();
58
+ process.exit(0);
59
+ };
60
+ process.on('SIGINT', shutdown);
61
+ process.on('SIGTERM', shutdown);
62
+ }
63
+ /**
64
+ * Process .cdml file for chat patterns
65
+ */
66
+ async function processSpecFile(filePath, config, claudeClient, watcher) {
67
+ // Skip if we're updating the file ourselves
68
+ if (watcher.isCurrentlyUpdating()) {
69
+ logger.debug('Skipping processing (internal update in progress)');
70
+ return;
71
+ }
72
+ logger.processing(`Processing ${filePath.split('/').pop()}...`);
73
+ // Read file content
74
+ const content = await readFile(filePath, 'utf-8');
75
+ // Parse spec file
76
+ const parsed = parseSpecFile(content);
77
+ if (parsed.chatPatterns.length === 0) {
78
+ logger.debug('No new chat patterns found');
79
+ return;
80
+ }
81
+ logger.info(`Found ${parsed.chatPatterns.length} chat pattern(s) to process`);
82
+ // Process each chat pattern
83
+ for (const pattern of parsed.chatPatterns) {
84
+ await processChatPattern(pattern, config, claudeClient, watcher, parsed.dom);
85
+ }
86
+ }
87
+ /**
88
+ * Process a single chat pattern
89
+ */
90
+ async function processChatPattern(pattern, config, claudeClient, watcher, $) {
91
+ const { action, element, elementName, specAttributes, userMessage, context, elementPath } = pattern;
92
+ // Build display message
93
+ const attrPreview = Object.entries(specAttributes).length > 0
94
+ ? ` [${Object.entries(specAttributes).slice(0, 2).map(([k, v]) => `${k}="${v}"`).join(', ')}...]`
95
+ : '';
96
+ const messagePreview = userMessage
97
+ ? userMessage.substring(0, 60) + (userMessage.length > 60 ? '...' : '')
98
+ : 'attribute-based spec';
99
+ logger.info(`Processing ${action}: <${elementName}${attrPreview}>`);
100
+ logger.info(` Message: "${messagePreview}"`);
101
+ logger.debug(` Context: ${elementPath}`);
102
+ try {
103
+ // Read existing code file if it exists (extension depends on target language)
104
+ const targetExtensions = {
105
+ html: '.html',
106
+ bash: '.sh',
107
+ python: '.py',
108
+ javascript: '.js',
109
+ typescript: '.ts',
110
+ go: '.go',
111
+ rust: '.rs',
112
+ };
113
+ const ext = targetExtensions[pattern.target] || '.html';
114
+ const codeFilePath = config.specFile.replace('.cdml', ext);
115
+ if (existsSync(codeFilePath)) {
116
+ const existingCode = await readFile(codeFilePath, 'utf-8');
117
+ context.existingCode = existingCode;
118
+ logger.debug(`Loaded existing ${pattern.target} code for context`);
119
+ }
120
+ // Build full prompt combining element name, attributes, user message, and action instructions
121
+ const fullPrompt = buildFullPrompt($, element, elementName, specAttributes, userMessage, pattern.actionInstructions);
122
+ // Debug: log the prompt to see what's being sent
123
+ logger.debug('=== FULL PROMPT BEING SENT ===');
124
+ logger.debug(fullPrompt);
125
+ logger.debug('=== END PROMPT ===');
126
+ // Accumulate full response
127
+ let fullResponse = '';
128
+ // Send to Claude and stream response
129
+ for await (const chunk of claudeClient.sendPrompt(fullPrompt, context)) {
130
+ fullResponse += chunk;
131
+ // Optionally: show progress
132
+ if (process.env.DEBUG) {
133
+ process.stdout.write('.');
134
+ }
135
+ }
136
+ if (process.env.DEBUG) {
137
+ process.stdout.write('\n');
138
+ }
139
+ logger.success('Received response from Claude');
140
+ // Check if response contains code blocks
141
+ const codeBlocks = extractCodeBlocks(fullResponse);
142
+ const hasCode = codeBlocks.length > 0;
143
+ // Generate code file using universal generator (if there's code)
144
+ if (hasCode) {
145
+ const generated = await generateCode(fullResponse, pattern, context);
146
+ // Determine output file path
147
+ const outputFile = config.specFile.replace('.cdml', generated.fileExtension);
148
+ // Write generated code
149
+ await writeFile(outputFile, generated.code, 'utf-8');
150
+ // Make executable if it's a script
151
+ if (generated.metadata?.executable) {
152
+ await chmod(outputFile, 0o755);
153
+ logger.debug(`Made ${outputFile} executable`);
154
+ }
155
+ logger.success(`Generated ${pattern.target} code → ${outputFile}`);
156
+ }
157
+ // Update spec.html: remove action attribute and add <ai> child with response
158
+ watcher.setUpdating(true);
159
+ try {
160
+ await updateSpecWithResponse($, element, action, fullResponse, config.specFile, hasCode);
161
+ }
162
+ finally {
163
+ watcher.setUpdating(false);
164
+ }
165
+ }
166
+ catch (error) {
167
+ const err = error;
168
+ logger.error(`Failed to process chat pattern: ${err.message}`);
169
+ throw error;
170
+ }
171
+ }
172
+ /**
173
+ * Build full prompt from element name, spec attributes, and user message
174
+ */
175
+ function buildFullPrompt($, element, elementName, specAttributes, userMessage, actionInstructions) {
176
+ const parts = [];
177
+ // Add action instructions if provided (e.g. gen="use opus 4.6")
178
+ if (actionInstructions) {
179
+ parts.push(`Generation Instructions: ${actionInstructions}`);
180
+ parts.push('');
181
+ }
182
+ // Add element name as semantic header
183
+ parts.push(`Element: ${elementName}`);
184
+ parts.push('');
185
+ // Add spec attributes if any
186
+ if (Object.keys(specAttributes).length > 0) {
187
+ parts.push('Specifications:');
188
+ for (const [key, value] of Object.entries(specAttributes)) {
189
+ parts.push(` ${key}: ${value}`);
190
+ }
191
+ parts.push('');
192
+ }
193
+ // Extract nested element specifications directly from the element's children
194
+ const nestedSpecs = extractNestedSpecifications($, element);
195
+ if (nestedSpecs.length > 0) {
196
+ parts.push('NESTED COMPONENTS TO IMPLEMENT:');
197
+ parts.push('The following components MUST be implemented (do not use placeholder comments):');
198
+ parts.push('');
199
+ function formatSpec(spec, indent, index) {
200
+ const prefix = index > 0 ? `${index}. ` : '';
201
+ const lockStatus = spec.isLocked ? ' [LOCKED - DO NOT REGENERATE]' : '';
202
+ parts.push(`${indent}${prefix}<${spec.elementName}>${lockStatus}`);
203
+ if (spec.isLocked) {
204
+ parts.push(`${indent} ⚠️ This component is LOCKED - keep existing implementation, do NOT regenerate`);
205
+ }
206
+ if (Object.keys(spec.attributes).length > 0) {
207
+ parts.push(`${indent} Attributes:`);
208
+ for (const [key, value] of Object.entries(spec.attributes)) {
209
+ parts.push(`${indent} - ${key}: ${value}`);
210
+ }
211
+ }
212
+ if (spec.textContent) {
213
+ parts.push(`${indent} Content: ${spec.textContent}`);
214
+ }
215
+ // Recursively format children
216
+ if (spec.children && spec.children.length > 0) {
217
+ parts.push(`${indent} Contains nested components:`);
218
+ spec.children.forEach((child, childIndex) => {
219
+ formatSpec(child, indent + ' ', childIndex + 1);
220
+ });
221
+ }
222
+ parts.push('');
223
+ }
224
+ nestedSpecs.forEach((spec, index) => {
225
+ formatSpec(spec, '', index + 1);
226
+ });
227
+ }
228
+ // Add user message if any
229
+ if (userMessage) {
230
+ parts.push('Full Description:');
231
+ parts.push(userMessage);
232
+ }
233
+ return parts.join('\n');
234
+ }
235
+ function extractNestedSpecifications($, element) {
236
+ const specs = [];
237
+ function extractRecursive(parentElement, depth, parentLocked) {
238
+ const result = [];
239
+ const $parent = $(parentElement);
240
+ $parent.children().each((_, child) => {
241
+ if (child.type === 'tag') {
242
+ const childElement = child;
243
+ const elementName = childElement.name;
244
+ // Skip <ai> elements (these are AI responses, not specifications)
245
+ if (elementName === 'ai') {
246
+ return;
247
+ }
248
+ const $child = $(childElement);
249
+ const attrs = childElement.attribs || {};
250
+ // Check lock/unlock status
251
+ const hasLock = 'lock' in attrs;
252
+ const hasUnlock = 'unlock' in attrs;
253
+ // Determine if this element is locked:
254
+ // - If parent is locked and element doesn't have unlock => locked
255
+ // - If element has lock attribute => locked
256
+ // - If element has unlock attribute => unlocked (overrides parent lock)
257
+ const isLocked = hasLock || (parentLocked && !hasUnlock);
258
+ // Extract attributes (exclude lock/unlock/gen/retry/undo as they're control attributes)
259
+ const attributes = {};
260
+ for (const [key, value] of Object.entries(attrs)) {
261
+ if (key !== 'lock' && key !== 'unlock' && key !== 'gen' && key !== 'retry' && key !== 'undo') {
262
+ attributes[key] = value;
263
+ }
264
+ }
265
+ // Get direct text content (not from children)
266
+ const textContent = $child.contents()
267
+ .filter((_, node) => node.type === 'text')
268
+ .text()
269
+ .trim();
270
+ // Recursively extract children, passing down the locked state
271
+ const children = extractRecursive(childElement, depth + 1, isLocked);
272
+ const hasChildren = children.length > 0;
273
+ result.push({
274
+ elementName,
275
+ attributes,
276
+ textContent,
277
+ hasChildren,
278
+ depth,
279
+ isLocked,
280
+ children: hasChildren ? children : undefined,
281
+ });
282
+ }
283
+ });
284
+ return result;
285
+ }
286
+ try {
287
+ // Check if the root element (the one with gen/retry/undo) has a lock attribute
288
+ // If it does, all its children are locked by default (unless they have unlock)
289
+ const rootAttrs = element.attribs || {};
290
+ const rootHasLock = 'lock' in rootAttrs;
291
+ specs.push(...extractRecursive(element, 0, rootHasLock));
292
+ }
293
+ catch (error) {
294
+ logger.debug(`Could not parse nested specifications: ${error}`);
295
+ }
296
+ return specs;
297
+ }
298
+ // Start the application
299
+ main().catch((error) => {
300
+ logger.error(`Fatal error: ${error.message}`);
301
+ if (process.env.DEBUG) {
302
+ console.error(error.stack);
303
+ }
304
+ process.exit(1);
305
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * XML-ish updater - inserts AI responses into spec.html
3
+ */
4
+ import type { CheerioAPI } from 'cheerio';
5
+ import type { Element } from 'domhandler';
6
+ /**
7
+ * Strip code blocks from response and replace with reference
8
+ */
9
+ export declare function stripCodeBlocks(response: string, hasCode: boolean): string;
10
+ /**
11
+ * Update element with AI response: remove action attribute and add/update <ai> child
12
+ */
13
+ export declare function updateElementWithResponse($: CheerioAPI, element: Element, action: 'gen' | 'retry' | 'undo', response: string, hasCode?: boolean): void;
14
+ /**
15
+ * Serialize cheerio DOM back to HTML string
16
+ */
17
+ export declare function serializeToHTML($: CheerioAPI): Promise<string>;
18
+ /**
19
+ * Write updated content back to spec.html safely
20
+ */
21
+ export declare function writeSpecFile(filePath: string, content: string): Promise<void>;
22
+ /**
23
+ * Grace period to prevent immediate re-trigger of watcher
24
+ */
25
+ export declare function sleep(ms: number): Promise<void>;
26
+ /**
27
+ * Complete update flow: remove action attribute, add AI response, serialize, write file
28
+ */
29
+ export declare function updateSpecWithResponse($: CheerioAPI, element: Element, action: 'gen' | 'retry' | 'undo', response: string, filePath: string, hasCode?: boolean): Promise<void>;