@itz4blitz/agentful 0.4.0 → 1.0.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.
- package/README.md +131 -16
- package/bin/cli.js +1031 -47
- package/bin/hooks/README.md +338 -82
- package/bin/hooks/analyze-trigger.js +69 -0
- package/bin/hooks/block-random-docs.js +77 -0
- package/bin/hooks/health-check.js +153 -0
- package/bin/hooks/post-agent.js +101 -0
- package/bin/hooks/post-feature.js +227 -0
- package/bin/hooks/pre-agent.js +118 -0
- package/bin/hooks/pre-feature.js +138 -0
- package/lib/VALIDATION_README.md +455 -0
- package/lib/atomic.js +350 -0
- package/lib/ci/claude-action-integration.js +641 -0
- package/lib/ci/index.js +10 -0
- package/lib/core/CLAUDE_EXECUTOR.md +371 -0
- package/lib/core/README.md +321 -0
- package/lib/core/analyzer.js +497 -0
- package/lib/core/claude-executor.example.js +210 -0
- package/lib/core/claude-executor.js +1046 -0
- package/lib/core/cli.js +141 -0
- package/lib/core/detectors/conventions.js +342 -0
- package/lib/core/detectors/framework.js +276 -0
- package/lib/core/detectors/index.js +15 -0
- package/lib/core/detectors/language.js +199 -0
- package/lib/core/detectors/patterns.js +356 -0
- package/lib/core/generator.js +626 -0
- package/lib/core/index.js +9 -0
- package/lib/core/output-parser.example.js +250 -0
- package/lib/core/output-parser.js +458 -0
- package/lib/core/storage.js +515 -0
- package/lib/core/templates.js +556 -0
- package/lib/index.js +32 -0
- package/lib/init.js +252 -21
- package/lib/pipeline/cli.js +423 -0
- package/lib/pipeline/engine.js +928 -0
- package/lib/pipeline/executor.js +440 -0
- package/lib/pipeline/index.js +33 -0
- package/lib/pipeline/integrations.js +559 -0
- package/lib/pipeline/schemas.js +288 -0
- package/lib/presets.js +207 -0
- package/lib/remote/client.js +361 -0
- package/lib/server/auth.js +286 -0
- package/lib/server/client-example.js +190 -0
- package/lib/server/executor.js +426 -0
- package/lib/server/index.js +469 -0
- package/lib/update-helpers.js +505 -0
- package/lib/validation.js +460 -0
- package/package.json +19 -2
- package/template/.claude/agents/architect.md +260 -0
- package/template/.claude/agents/backend.md +203 -0
- package/template/.claude/agents/fixer.md +244 -0
- package/template/.claude/agents/frontend.md +232 -0
- package/template/.claude/agents/orchestrator.md +528 -0
- package/template/.claude/agents/product-analyzer.md +1130 -0
- package/template/.claude/agents/reviewer.md +229 -0
- package/template/.claude/agents/tester.md +242 -0
- package/{.claude → template/.claude}/commands/agentful-analyze.md +151 -43
- package/template/.claude/commands/agentful-decide.md +470 -0
- package/{.claude → template/.claude}/commands/agentful-product.md +89 -5
- package/template/.claude/commands/agentful-start.md +432 -0
- package/{.claude → template/.claude}/commands/agentful-status.md +88 -3
- package/template/.claude/commands/agentful-update.md +402 -0
- package/template/.claude/commands/agentful-validate.md +369 -0
- package/{.claude → template/.claude}/commands/agentful.md +110 -183
- package/template/.claude/product/EXAMPLES.md +167 -0
- package/{.claude → template/.claude}/settings.json +9 -13
- package/{.claude → template/.claude}/skills/conversation/SKILL.md +13 -7
- package/template/.claude/skills/deployment/SKILL.md +116 -0
- package/template/.claude/skills/product-planning/SKILL.md +463 -0
- package/template/.claude/skills/testing/SKILL.md +228 -0
- package/template/.claude/skills/validation/SKILL.md +650 -0
- package/template/CLAUDE.md +73 -5
- package/template/bin/hooks/block-random-docs.js +121 -0
- package/version.json +1 -1
- package/.claude/agents/architect.md +0 -524
- package/.claude/agents/backend.md +0 -315
- package/.claude/agents/fixer.md +0 -263
- package/.claude/agents/frontend.md +0 -274
- package/.claude/agents/orchestrator.md +0 -283
- package/.claude/agents/product-analyzer.md +0 -792
- package/.claude/agents/reviewer.md +0 -332
- package/.claude/agents/tester.md +0 -410
- package/.claude/commands/agentful-decide.md +0 -214
- package/.claude/commands/agentful-start.md +0 -182
- package/.claude/commands/agentful-validate.md +0 -127
- package/.claude/product/EXAMPLES.md +0 -610
- package/.claude/product/README.md +0 -326
- package/.claude/skills/validation/SKILL.md +0 -271
- package/bin/hooks/analyze-trigger.sh +0 -57
- package/bin/hooks/health-check.sh +0 -36
- /package/{.claude → template/.claude}/commands/agentful-generate.md +0 -0
- /package/{.claude → template/.claude}/product/index.md +0 -0
- /package/{.claude → template/.claude}/skills/product-tracking/SKILL.md +0 -0
|
@@ -0,0 +1,1046 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Claude Code Executor Abstraction
|
|
3
|
+
*
|
|
4
|
+
* Consolidates three different Claude invocation methods:
|
|
5
|
+
* 1. Task API - Use Claude Code's Task() API (preferred, works in slash commands)
|
|
6
|
+
* 2. Subprocess - Spawn claude CLI with -p flag (for server/pipeline)
|
|
7
|
+
* 3. API - Direct Anthropic API calls (future, not yet implemented)
|
|
8
|
+
*
|
|
9
|
+
* Provides streaming execution with real-time progress updates,
|
|
10
|
+
* question detection, error handling, and retry logic.
|
|
11
|
+
*
|
|
12
|
+
* @module core/claude-executor
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawn } from 'child_process';
|
|
16
|
+
import { EventEmitter } from 'events';
|
|
17
|
+
import { randomUUID } from 'crypto';
|
|
18
|
+
import fs from 'fs/promises';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { loadAgentDefinition } from '../ci/claude-action-integration.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Execution modes
|
|
24
|
+
*/
|
|
25
|
+
export const ExecutionMode = {
|
|
26
|
+
TASK_API: 'task-api',
|
|
27
|
+
SUBPROCESS: 'subprocess',
|
|
28
|
+
API: 'api',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Execution states
|
|
33
|
+
*/
|
|
34
|
+
export const ExecutionState = {
|
|
35
|
+
PENDING: 'pending',
|
|
36
|
+
RUNNING: 'running',
|
|
37
|
+
COMPLETED: 'completed',
|
|
38
|
+
FAILED: 'failed',
|
|
39
|
+
CANCELLED: 'cancelled',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Maximum output size (1MB per execution)
|
|
44
|
+
*/
|
|
45
|
+
const MAX_OUTPUT_SIZE = 1 * 1024 * 1024;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Maximum task length (10KB)
|
|
49
|
+
*/
|
|
50
|
+
const MAX_TASK_LENGTH = 10 * 1024;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Default timeout (10 minutes)
|
|
54
|
+
*/
|
|
55
|
+
const DEFAULT_TIMEOUT = 10 * 60 * 1000;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Output Parser
|
|
59
|
+
*
|
|
60
|
+
* Detects progress markers, questions, and errors in streaming output
|
|
61
|
+
*/
|
|
62
|
+
class OutputParser {
|
|
63
|
+
constructor() {
|
|
64
|
+
this.buffer = '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parse a chunk of output
|
|
69
|
+
*
|
|
70
|
+
* @param {string} chunk - Raw output chunk
|
|
71
|
+
* @returns {Object} Parsed events { progress?, question?, error? }
|
|
72
|
+
*/
|
|
73
|
+
parse(chunk) {
|
|
74
|
+
this.buffer += chunk;
|
|
75
|
+
const events = {};
|
|
76
|
+
|
|
77
|
+
// Detect progress markers: [PROGRESS: 45%] or Progress: 45%
|
|
78
|
+
const progressMatch = chunk.match(/(?:\[PROGRESS:\s*(\d+)%\]|Progress:\s*(\d+)%)/i);
|
|
79
|
+
if (progressMatch) {
|
|
80
|
+
const progress = parseInt(progressMatch[1] || progressMatch[2], 10);
|
|
81
|
+
events.progress = { percentage: progress, raw: progressMatch[0] };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Detect task completion markers
|
|
85
|
+
if (chunk.includes('Task completed') || chunk.includes('✓ Complete')) {
|
|
86
|
+
events.progress = { percentage: 100, raw: 'Task completed' };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Detect questions: Lines ending with "?" or "Please provide"
|
|
90
|
+
const questionMatch = chunk.match(/^(.+\?)\s*$/m) || chunk.match(/(Please provide .+)/);
|
|
91
|
+
if (questionMatch) {
|
|
92
|
+
events.question = {
|
|
93
|
+
text: questionMatch[1].trim(),
|
|
94
|
+
timestamp: Date.now()
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Detect errors: Lines starting with "Error:", "ERROR:", or containing "failed"
|
|
99
|
+
const errorMatch = chunk.match(/(?:Error:|ERROR:|❌)\s*(.+)/i);
|
|
100
|
+
if (errorMatch) {
|
|
101
|
+
events.error = {
|
|
102
|
+
message: errorMatch[1].trim(),
|
|
103
|
+
timestamp: Date.now()
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return events;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Reset the buffer
|
|
112
|
+
*/
|
|
113
|
+
reset() {
|
|
114
|
+
this.buffer = '';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get full buffered output
|
|
119
|
+
*
|
|
120
|
+
* @returns {string} Complete output
|
|
121
|
+
*/
|
|
122
|
+
getBuffer() {
|
|
123
|
+
return this.buffer;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Unified Claude Code Executor
|
|
129
|
+
*
|
|
130
|
+
* Provides consistent interface for executing agents across different modes:
|
|
131
|
+
* - Task API: Uses Claude Code's Task() API (for slash commands)
|
|
132
|
+
* - Subprocess: Spawns claude CLI with -p flag (for server/pipeline)
|
|
133
|
+
* - API: Direct Anthropic API calls (future)
|
|
134
|
+
*
|
|
135
|
+
* @extends EventEmitter
|
|
136
|
+
* @emits chunk - Streaming output chunk
|
|
137
|
+
* @emits progress - Progress update (0-100)
|
|
138
|
+
* @emits question - Question detected in output
|
|
139
|
+
* @emits error - Error during execution
|
|
140
|
+
* @emits complete - Execution completed
|
|
141
|
+
* @emits retry - Retry attempt starting
|
|
142
|
+
* @emits cancelled - Execution cancelled
|
|
143
|
+
*/
|
|
144
|
+
export class ClaudeExecutor extends EventEmitter {
|
|
145
|
+
/**
|
|
146
|
+
* Create a new Claude executor
|
|
147
|
+
*
|
|
148
|
+
* @param {Object} options - Executor options
|
|
149
|
+
* @param {string} [options.mode='subprocess'] - Execution mode
|
|
150
|
+
* @param {string} [options.projectRoot=process.cwd()] - Project root directory
|
|
151
|
+
* @param {string} [options.agentsDir='.claude/agents'] - Agents directory
|
|
152
|
+
* @param {string} [options.claudeCommand='claude'] - Claude CLI command
|
|
153
|
+
* @param {string} [options.workingDir=process.cwd()] - Working directory (alias for projectRoot)
|
|
154
|
+
* @param {number} [options.timeout=600000] - Default timeout in ms
|
|
155
|
+
* @param {number} [options.maxOutputSize=1048576] - Maximum output size
|
|
156
|
+
* @param {boolean} [options.streamOutput=true] - Stream output as chunks
|
|
157
|
+
* @param {number} [options.maxRetries=2] - Maximum retry attempts
|
|
158
|
+
*/
|
|
159
|
+
constructor(options = {}) {
|
|
160
|
+
super();
|
|
161
|
+
|
|
162
|
+
this.options = {
|
|
163
|
+
mode: options.mode || ExecutionMode.SUBPROCESS,
|
|
164
|
+
projectRoot: options.projectRoot || options.workingDir || process.cwd(),
|
|
165
|
+
agentsDir: options.agentsDir || '.claude/agents',
|
|
166
|
+
claudeCommand: options.claudeCommand || 'claude',
|
|
167
|
+
workingDir: options.workingDir || options.projectRoot || process.cwd(),
|
|
168
|
+
timeout: options.timeout || DEFAULT_TIMEOUT,
|
|
169
|
+
maxOutputSize: options.maxOutputSize || MAX_OUTPUT_SIZE,
|
|
170
|
+
streamOutput: options.streamOutput !== false,
|
|
171
|
+
maxRetries: options.maxRetries || 2,
|
|
172
|
+
...options,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Validate mode
|
|
176
|
+
if (!Object.values(ExecutionMode).includes(this.options.mode)) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Invalid execution mode: ${this.options.mode}. ` +
|
|
179
|
+
`Must be one of: ${Object.values(ExecutionMode).join(', ')}`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.activeExecutions = new Map();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Execute an agent with a task (main execution method)
|
|
188
|
+
*
|
|
189
|
+
* @param {string} agentName - Name of the agent
|
|
190
|
+
* @param {string} task - Task description
|
|
191
|
+
* @param {Object} [context={}] - Additional context
|
|
192
|
+
* @param {Object} [context.files] - Files to include in context
|
|
193
|
+
* @param {Object} [context.requirements] - Requirements to include
|
|
194
|
+
* @param {Object} [context.variables] - Variables to interpolate
|
|
195
|
+
* @param {Object} [options={}] - Execution options
|
|
196
|
+
* @param {number} [options.timeout] - Override default timeout
|
|
197
|
+
* @param {boolean} [options.streamOutput] - Override streaming setting
|
|
198
|
+
* @returns {Promise<Object>} Execution result
|
|
199
|
+
*/
|
|
200
|
+
async execute(agentName, task, context = {}, options = {}) {
|
|
201
|
+
// Validate inputs
|
|
202
|
+
this._validateTask(task);
|
|
203
|
+
|
|
204
|
+
const executionId = randomUUID();
|
|
205
|
+
const execOptions = { ...this.options, ...options };
|
|
206
|
+
|
|
207
|
+
// Initialize execution context
|
|
208
|
+
const execContext = {
|
|
209
|
+
id: executionId,
|
|
210
|
+
agent: agentName,
|
|
211
|
+
task,
|
|
212
|
+
state: ExecutionState.PENDING,
|
|
213
|
+
startTime: Date.now(),
|
|
214
|
+
endTime: null,
|
|
215
|
+
retries: 0,
|
|
216
|
+
output: '',
|
|
217
|
+
error: null,
|
|
218
|
+
exitCode: null,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
this.activeExecutions.set(executionId, execContext);
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
// Load agent definition
|
|
225
|
+
const agentDef = await this.loadAgent(agentName);
|
|
226
|
+
execContext.agentMetadata = agentDef.metadata;
|
|
227
|
+
|
|
228
|
+
// Build full prompt
|
|
229
|
+
const prompt = await this.buildFullPrompt(agentDef, task, context);
|
|
230
|
+
|
|
231
|
+
// Update state
|
|
232
|
+
execContext.state = ExecutionState.RUNNING;
|
|
233
|
+
|
|
234
|
+
// Execute based on mode
|
|
235
|
+
let result;
|
|
236
|
+
switch (this.options.mode) {
|
|
237
|
+
case ExecutionMode.TASK_API:
|
|
238
|
+
result = await this.executeViaTaskAPI(execContext, agentDef, prompt, context, execOptions);
|
|
239
|
+
break;
|
|
240
|
+
|
|
241
|
+
case ExecutionMode.SUBPROCESS:
|
|
242
|
+
result = await this._executeViaSubprocessInternal(execContext, agentDef, prompt, context, execOptions);
|
|
243
|
+
break;
|
|
244
|
+
|
|
245
|
+
case ExecutionMode.API:
|
|
246
|
+
result = await this.executeViaAPI(execContext, agentDef, prompt, context, execOptions);
|
|
247
|
+
break;
|
|
248
|
+
|
|
249
|
+
default:
|
|
250
|
+
throw new Error(`Unsupported execution mode: ${this.options.mode}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Update execution context
|
|
254
|
+
execContext.state = ExecutionState.COMPLETED;
|
|
255
|
+
execContext.endTime = Date.now();
|
|
256
|
+
execContext.output = result.output;
|
|
257
|
+
|
|
258
|
+
this.emit('complete', {
|
|
259
|
+
executionId,
|
|
260
|
+
duration: execContext.endTime - execContext.startTime,
|
|
261
|
+
result,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
return result;
|
|
265
|
+
|
|
266
|
+
} catch (error) {
|
|
267
|
+
// Check if we should retry
|
|
268
|
+
if (execContext.retries < execOptions.maxRetries && this._isRetryableError(error)) {
|
|
269
|
+
execContext.retries++;
|
|
270
|
+
const delay = Math.pow(2, execContext.retries) * 1000; // Exponential backoff
|
|
271
|
+
|
|
272
|
+
this.emit('retry', {
|
|
273
|
+
executionId,
|
|
274
|
+
attempt: execContext.retries,
|
|
275
|
+
maxRetries: execOptions.maxRetries,
|
|
276
|
+
delay,
|
|
277
|
+
error: error.message,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await this._sleep(delay);
|
|
281
|
+
|
|
282
|
+
// Retry execution
|
|
283
|
+
return this.execute(agentName, task, context, options);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Update execution context with error
|
|
287
|
+
execContext.state = ExecutionState.FAILED;
|
|
288
|
+
execContext.endTime = Date.now();
|
|
289
|
+
execContext.error = error.message;
|
|
290
|
+
|
|
291
|
+
this.emit('error', {
|
|
292
|
+
executionId,
|
|
293
|
+
error: error.message,
|
|
294
|
+
duration: execContext.endTime - execContext.startTime,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
throw error;
|
|
298
|
+
|
|
299
|
+
} finally {
|
|
300
|
+
// Clean up after delay to allow status queries
|
|
301
|
+
setTimeout(() => {
|
|
302
|
+
this.activeExecutions.delete(executionId);
|
|
303
|
+
}, 60000); // Keep for 1 minute
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Load agent definition from .claude/agents/
|
|
309
|
+
*
|
|
310
|
+
* @param {string} agentName - Name of the agent
|
|
311
|
+
* @returns {Promise<Object>} Agent definition with metadata and instructions
|
|
312
|
+
*/
|
|
313
|
+
async loadAgent(agentName) {
|
|
314
|
+
return loadAgentDefinition(agentName, this.options.projectRoot);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Build full prompt from agent definition, task, and context
|
|
319
|
+
*
|
|
320
|
+
* @param {Object} agentDef - Agent definition
|
|
321
|
+
* @param {string} task - Task description
|
|
322
|
+
* @param {Object} context - Additional context
|
|
323
|
+
* @returns {Promise<string>} Complete prompt
|
|
324
|
+
*/
|
|
325
|
+
async buildFullPrompt(agentDef, task, context = {}) {
|
|
326
|
+
let prompt = `# Task for ${agentDef.metadata.name} Agent
|
|
327
|
+
|
|
328
|
+
${task}
|
|
329
|
+
|
|
330
|
+
`;
|
|
331
|
+
|
|
332
|
+
// Add context sections
|
|
333
|
+
if (context.files && Array.isArray(context.files)) {
|
|
334
|
+
prompt += `## Relevant Files
|
|
335
|
+
|
|
336
|
+
${context.files.map(f => `- ${f}`).join('\n')}
|
|
337
|
+
|
|
338
|
+
`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (context.requirements) {
|
|
342
|
+
prompt += `## Requirements
|
|
343
|
+
|
|
344
|
+
${context.requirements}
|
|
345
|
+
|
|
346
|
+
`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (context.variables && Object.keys(context.variables).length > 0) {
|
|
350
|
+
prompt += `## Variables
|
|
351
|
+
|
|
352
|
+
${Object.entries(context.variables).map(([k, v]) => `- ${k}: ${v}`).join('\n')}
|
|
353
|
+
|
|
354
|
+
`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Add agent instructions
|
|
358
|
+
prompt += `---
|
|
359
|
+
|
|
360
|
+
# Agent Instructions
|
|
361
|
+
|
|
362
|
+
${agentDef.instructions}
|
|
363
|
+
`;
|
|
364
|
+
|
|
365
|
+
return prompt;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Execute via Claude Code Task API
|
|
370
|
+
*
|
|
371
|
+
* @param {Object} execContext - Execution context
|
|
372
|
+
* @param {Object} agentDef - Agent definition
|
|
373
|
+
* @param {string} prompt - Full prompt
|
|
374
|
+
* @param {Object} context - Additional context
|
|
375
|
+
* @param {Object} options - Execution options
|
|
376
|
+
* @returns {Promise<Object>} Execution result
|
|
377
|
+
*/
|
|
378
|
+
async executeViaTaskAPI(execContext, agentDef, prompt, context, options) {
|
|
379
|
+
// Task API requires the Task function to be available in the environment
|
|
380
|
+
// This is typically only available when running inside Claude Code slash commands
|
|
381
|
+
|
|
382
|
+
if (typeof Task === 'undefined') {
|
|
383
|
+
throw new Error(
|
|
384
|
+
'Task API is not available. ' +
|
|
385
|
+
'This execution mode only works inside Claude Code slash commands. ' +
|
|
386
|
+
'Use subprocess or api mode instead.'
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
const result = await Task(prompt, {
|
|
392
|
+
timeout: options.timeout,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
success: true,
|
|
397
|
+
output: result,
|
|
398
|
+
mode: ExecutionMode.TASK_API,
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
} catch (error) {
|
|
402
|
+
throw new Error(`Task API execution failed: ${error.message}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Execute via Anthropic API
|
|
408
|
+
*
|
|
409
|
+
* @param {Object} execContext - Execution context
|
|
410
|
+
* @param {Object} agentDef - Agent definition
|
|
411
|
+
* @param {string} prompt - Full prompt
|
|
412
|
+
* @param {Object} context - Additional context
|
|
413
|
+
* @param {Object} options - Execution options
|
|
414
|
+
* @returns {Promise<Object>} Execution result
|
|
415
|
+
*/
|
|
416
|
+
async executeViaAPI(execContext, agentDef, prompt, context, options) {
|
|
417
|
+
// Direct API execution not yet implemented
|
|
418
|
+
// This would require ANTHROPIC_API_KEY and direct fetch calls
|
|
419
|
+
|
|
420
|
+
throw new Error(
|
|
421
|
+
'Direct API execution not yet implemented. ' +
|
|
422
|
+
'Use task-api or subprocess mode instead. ' +
|
|
423
|
+
'To implement API mode, add Anthropic API key and implement API client.'
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
// Future implementation:
|
|
427
|
+
// const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
428
|
+
// if (!apiKey) {
|
|
429
|
+
// throw new Error('ANTHROPIC_API_KEY environment variable not set');
|
|
430
|
+
// }
|
|
431
|
+
//
|
|
432
|
+
// const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
433
|
+
// method: 'POST',
|
|
434
|
+
// headers: {
|
|
435
|
+
// 'Content-Type': 'application/json',
|
|
436
|
+
// 'x-api-key': apiKey,
|
|
437
|
+
// 'anthropic-version': '2023-06-01',
|
|
438
|
+
// },
|
|
439
|
+
// body: JSON.stringify({
|
|
440
|
+
// model: 'claude-sonnet-4',
|
|
441
|
+
// max_tokens: 4096,
|
|
442
|
+
// messages: [{ role: 'user', content: prompt }],
|
|
443
|
+
// }),
|
|
444
|
+
// });
|
|
445
|
+
//
|
|
446
|
+
// if (!response.ok) {
|
|
447
|
+
// throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
448
|
+
// }
|
|
449
|
+
//
|
|
450
|
+
// const result = await response.json();
|
|
451
|
+
// return {
|
|
452
|
+
// success: true,
|
|
453
|
+
// output: result.content[0].text,
|
|
454
|
+
// mode: ExecutionMode.API,
|
|
455
|
+
// };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Internal method for subprocess execution (used by execute() and backward compat)
|
|
460
|
+
*
|
|
461
|
+
* @private
|
|
462
|
+
*/
|
|
463
|
+
async _executeViaSubprocessInternal(execContext, agentDef, prompt, context, options) {
|
|
464
|
+
return new Promise((resolve, reject) => {
|
|
465
|
+
const args = ['-p', prompt];
|
|
466
|
+
|
|
467
|
+
if (options.timeout) {
|
|
468
|
+
args.push('--timeout', options.timeout.toString());
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const proc = spawn(options.claudeCommand, args, {
|
|
472
|
+
cwd: options.workingDir,
|
|
473
|
+
env: {
|
|
474
|
+
...process.env,
|
|
475
|
+
CLAUDE_NON_INTERACTIVE: '1', // Disable interactive prompts
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
execContext.process = proc;
|
|
480
|
+
execContext.parser = new OutputParser();
|
|
481
|
+
|
|
482
|
+
let outputSize = 0;
|
|
483
|
+
let outputTruncated = false;
|
|
484
|
+
|
|
485
|
+
// Setup timeout
|
|
486
|
+
const timeoutHandle = setTimeout(() => {
|
|
487
|
+
proc.kill('SIGTERM');
|
|
488
|
+
reject(new Error(`Execution timeout after ${options.timeout}ms`));
|
|
489
|
+
}, options.timeout);
|
|
490
|
+
|
|
491
|
+
// Capture stdout
|
|
492
|
+
proc.stdout.on('data', (data) => {
|
|
493
|
+
const chunk = data.toString();
|
|
494
|
+
|
|
495
|
+
// Check output size limit
|
|
496
|
+
if (outputSize < options.maxOutputSize) {
|
|
497
|
+
const remainingSpace = options.maxOutputSize - outputSize;
|
|
498
|
+
const chunkToAdd = chunk.length <= remainingSpace
|
|
499
|
+
? chunk
|
|
500
|
+
: chunk.substring(0, remainingSpace) + '\n[Output truncated - limit reached]';
|
|
501
|
+
|
|
502
|
+
execContext.output += chunkToAdd;
|
|
503
|
+
outputSize += chunk.length;
|
|
504
|
+
|
|
505
|
+
if (chunk.length > remainingSpace) {
|
|
506
|
+
outputTruncated = true;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Emit chunk if streaming enabled
|
|
510
|
+
if (options.streamOutput) {
|
|
511
|
+
this.emit('chunk', { executionId: execContext.id, chunk: chunkToAdd, text: chunkToAdd });
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Parse for structured events
|
|
515
|
+
const events = execContext.parser.parse(chunk);
|
|
516
|
+
|
|
517
|
+
if (events.progress) {
|
|
518
|
+
this.emit('progress', {
|
|
519
|
+
executionId: execContext.id,
|
|
520
|
+
percentage: events.progress.percentage,
|
|
521
|
+
message: events.progress.raw,
|
|
522
|
+
timestamp: Date.now()
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (events.question) {
|
|
527
|
+
this.emit('question', {
|
|
528
|
+
executionId: execContext.id,
|
|
529
|
+
text: events.question.text,
|
|
530
|
+
timestamp: events.question.timestamp
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (events.error) {
|
|
535
|
+
this.emit('error', {
|
|
536
|
+
executionId: execContext.id,
|
|
537
|
+
message: events.error.message,
|
|
538
|
+
timestamp: events.error.timestamp
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Capture stderr
|
|
545
|
+
proc.stderr.on('data', (data) => {
|
|
546
|
+
const chunk = data.toString();
|
|
547
|
+
execContext.output += `[ERROR] ${chunk}`;
|
|
548
|
+
|
|
549
|
+
if (options.streamOutput) {
|
|
550
|
+
this.emit('chunk', { executionId: execContext.id, chunk: `[ERROR] ${chunk}`, text: `[ERROR] ${chunk}` });
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
this.emit('error', {
|
|
554
|
+
executionId: execContext.id,
|
|
555
|
+
message: chunk,
|
|
556
|
+
timestamp: Date.now(),
|
|
557
|
+
source: 'stderr'
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// Handle process exit
|
|
562
|
+
proc.on('close', (code) => {
|
|
563
|
+
clearTimeout(timeoutHandle);
|
|
564
|
+
execContext.exitCode = code;
|
|
565
|
+
execContext.process = null;
|
|
566
|
+
|
|
567
|
+
if (outputTruncated) {
|
|
568
|
+
execContext.output += '\n\n[Note: Output was truncated due to size limit]';
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (code === 0) {
|
|
572
|
+
resolve({
|
|
573
|
+
success: true,
|
|
574
|
+
output: execContext.output,
|
|
575
|
+
exitCode: code,
|
|
576
|
+
mode: ExecutionMode.SUBPROCESS,
|
|
577
|
+
truncated: outputTruncated,
|
|
578
|
+
});
|
|
579
|
+
} else {
|
|
580
|
+
reject(new Error(`Claude exited with code ${code}`));
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// Handle spawn errors
|
|
585
|
+
proc.on('error', (error) => {
|
|
586
|
+
clearTimeout(timeoutHandle);
|
|
587
|
+
reject(new Error(`Failed to spawn claude: ${error.message}`));
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Execute agent via subprocess (backward compatibility method)
|
|
594
|
+
*
|
|
595
|
+
* @param {string} agentName - Name of the agent
|
|
596
|
+
* @param {string} task - Task description
|
|
597
|
+
* @param {Object} options - Execution options
|
|
598
|
+
* @param {string} [options.prompt] - Custom prompt (overrides task)
|
|
599
|
+
* @param {number} [options.timeout] - Execution timeout in ms
|
|
600
|
+
* @param {string} [options.workingDir] - Working directory
|
|
601
|
+
* @returns {Promise<Object>} Execution result
|
|
602
|
+
*/
|
|
603
|
+
async executeViaSubprocess(agentName, task, options = {}) {
|
|
604
|
+
const executionId = randomUUID();
|
|
605
|
+
const prompt = options.prompt || task;
|
|
606
|
+
const timeout = options.timeout || this.options.timeout;
|
|
607
|
+
const workingDir = options.workingDir || this.options.workingDir;
|
|
608
|
+
|
|
609
|
+
const execution = {
|
|
610
|
+
id: executionId,
|
|
611
|
+
agent: agentName,
|
|
612
|
+
task,
|
|
613
|
+
state: ExecutionState.PENDING,
|
|
614
|
+
startTime: Date.now(),
|
|
615
|
+
endTime: null,
|
|
616
|
+
process: null,
|
|
617
|
+
parser: new OutputParser(),
|
|
618
|
+
output: '',
|
|
619
|
+
error: '',
|
|
620
|
+
exitCode: null,
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
this.activeExecutions.set(executionId, execution);
|
|
624
|
+
|
|
625
|
+
try {
|
|
626
|
+
execution.state = ExecutionState.RUNNING;
|
|
627
|
+
|
|
628
|
+
// Emit start event
|
|
629
|
+
this.emit('start', {
|
|
630
|
+
executionId,
|
|
631
|
+
agent: agentName,
|
|
632
|
+
task,
|
|
633
|
+
timestamp: execution.startTime
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
return await new Promise((resolve, reject) => {
|
|
637
|
+
// Spawn Claude Code CLI
|
|
638
|
+
const args = ['-p', prompt];
|
|
639
|
+
const proc = spawn(this.options.claudeCommand, args, {
|
|
640
|
+
cwd: workingDir,
|
|
641
|
+
env: {
|
|
642
|
+
...process.env,
|
|
643
|
+
CLAUDE_NON_INTERACTIVE: '1',
|
|
644
|
+
},
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
execution.process = proc;
|
|
648
|
+
|
|
649
|
+
let timeoutHandle = null;
|
|
650
|
+
let outputSize = 0;
|
|
651
|
+
let outputTruncated = false;
|
|
652
|
+
|
|
653
|
+
// Setup timeout
|
|
654
|
+
if (timeout) {
|
|
655
|
+
timeoutHandle = setTimeout(() => {
|
|
656
|
+
proc.kill('SIGTERM');
|
|
657
|
+
reject(new Error(`Execution timeout after ${timeout}ms`));
|
|
658
|
+
}, timeout);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Handle stdout - emit chunks and parse for events
|
|
662
|
+
proc.stdout.on('data', (data) => {
|
|
663
|
+
const chunk = data.toString();
|
|
664
|
+
|
|
665
|
+
// Check output size limit
|
|
666
|
+
if (outputSize < this.options.maxOutputSize) {
|
|
667
|
+
const remainingSpace = this.options.maxOutputSize - outputSize;
|
|
668
|
+
const chunkToStore = chunk.length <= remainingSpace
|
|
669
|
+
? chunk
|
|
670
|
+
: chunk.substring(0, remainingSpace);
|
|
671
|
+
|
|
672
|
+
execution.output += chunkToStore;
|
|
673
|
+
outputSize += chunk.length;
|
|
674
|
+
|
|
675
|
+
if (chunk.length > remainingSpace) {
|
|
676
|
+
outputTruncated = true;
|
|
677
|
+
execution.output += '\n[Output truncated - limit reached]';
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Emit raw chunk event
|
|
682
|
+
this.emit('chunk', {
|
|
683
|
+
executionId,
|
|
684
|
+
text: chunk,
|
|
685
|
+
timestamp: Date.now()
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// Parse for structured events
|
|
689
|
+
const events = execution.parser.parse(chunk);
|
|
690
|
+
|
|
691
|
+
if (events.progress) {
|
|
692
|
+
this.emit('progress', {
|
|
693
|
+
executionId,
|
|
694
|
+
percentage: events.progress.percentage,
|
|
695
|
+
message: events.progress.raw,
|
|
696
|
+
timestamp: Date.now()
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (events.question) {
|
|
701
|
+
this.emit('question', {
|
|
702
|
+
executionId,
|
|
703
|
+
text: events.question.text,
|
|
704
|
+
timestamp: events.question.timestamp
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (events.error) {
|
|
709
|
+
this.emit('error', {
|
|
710
|
+
executionId,
|
|
711
|
+
message: events.error.message,
|
|
712
|
+
timestamp: events.error.timestamp
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// Handle stderr - emit as errors
|
|
718
|
+
proc.stderr.on('data', (data) => {
|
|
719
|
+
const chunk = data.toString();
|
|
720
|
+
execution.error += chunk;
|
|
721
|
+
|
|
722
|
+
this.emit('error', {
|
|
723
|
+
executionId,
|
|
724
|
+
message: chunk,
|
|
725
|
+
timestamp: Date.now(),
|
|
726
|
+
source: 'stderr'
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// Handle process exit
|
|
731
|
+
proc.on('close', (code) => {
|
|
732
|
+
if (timeoutHandle) {
|
|
733
|
+
clearTimeout(timeoutHandle);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
execution.exitCode = code;
|
|
737
|
+
execution.endTime = Date.now();
|
|
738
|
+
execution.process = null;
|
|
739
|
+
|
|
740
|
+
const duration = execution.endTime - execution.startTime;
|
|
741
|
+
|
|
742
|
+
if (code === 0) {
|
|
743
|
+
execution.state = ExecutionState.COMPLETED;
|
|
744
|
+
|
|
745
|
+
// Emit complete event
|
|
746
|
+
this.emit('complete', {
|
|
747
|
+
executionId,
|
|
748
|
+
output: execution.output,
|
|
749
|
+
duration,
|
|
750
|
+
timestamp: execution.endTime
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
resolve({
|
|
754
|
+
executionId,
|
|
755
|
+
state: ExecutionState.COMPLETED,
|
|
756
|
+
output: execution.output,
|
|
757
|
+
duration,
|
|
758
|
+
exitCode: code,
|
|
759
|
+
truncated: outputTruncated
|
|
760
|
+
});
|
|
761
|
+
} else {
|
|
762
|
+
execution.state = ExecutionState.FAILED;
|
|
763
|
+
|
|
764
|
+
reject(new Error(
|
|
765
|
+
`Agent execution failed with exit code ${code}: ${execution.error || 'No error details'}`
|
|
766
|
+
));
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Cleanup execution from active map
|
|
770
|
+
this.activeExecutions.delete(executionId);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
proc.on('error', (error) => {
|
|
774
|
+
if (timeoutHandle) {
|
|
775
|
+
clearTimeout(timeoutHandle);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
execution.state = ExecutionState.FAILED;
|
|
779
|
+
execution.endTime = Date.now();
|
|
780
|
+
|
|
781
|
+
this.emit('error', {
|
|
782
|
+
executionId,
|
|
783
|
+
message: error.message,
|
|
784
|
+
timestamp: Date.now(),
|
|
785
|
+
source: 'spawn'
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
reject(new Error(`Failed to spawn Claude Code: ${error.message}`));
|
|
789
|
+
|
|
790
|
+
// Cleanup execution from active map
|
|
791
|
+
this.activeExecutions.delete(executionId);
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
} catch (error) {
|
|
796
|
+
execution.state = ExecutionState.FAILED;
|
|
797
|
+
execution.endTime = Date.now();
|
|
798
|
+
execution.error = error.message;
|
|
799
|
+
|
|
800
|
+
this.activeExecutions.delete(executionId);
|
|
801
|
+
throw error;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Execute with streaming callbacks
|
|
807
|
+
*
|
|
808
|
+
* @param {string} agentName - Name of the agent
|
|
809
|
+
* @param {string} task - Task description
|
|
810
|
+
* @param {Object} callbacks - Callback functions
|
|
811
|
+
* @param {Function} [callbacks.onChunk] - Called for each output chunk
|
|
812
|
+
* @param {Function} [callbacks.onProgress] - Called when progress detected
|
|
813
|
+
* @param {Function} [callbacks.onQuestion] - Called when question detected
|
|
814
|
+
* @param {Function} [callbacks.onError] - Called when error detected
|
|
815
|
+
* @param {Object} options - Execution options
|
|
816
|
+
* @returns {Promise<Object>} Execution result
|
|
817
|
+
*/
|
|
818
|
+
async executeWithStreaming(agentName, task, callbacks = {}, options = {}) {
|
|
819
|
+
const { onChunk, onProgress, onQuestion, onError } = callbacks;
|
|
820
|
+
|
|
821
|
+
// Register event listeners
|
|
822
|
+
const listeners = {};
|
|
823
|
+
|
|
824
|
+
if (onChunk) {
|
|
825
|
+
listeners.chunk = (event) => {
|
|
826
|
+
if (event.executionId) {
|
|
827
|
+
onChunk(event.text);
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
this.on('chunk', listeners.chunk);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (onProgress) {
|
|
834
|
+
listeners.progress = (event) => {
|
|
835
|
+
if (event.executionId) {
|
|
836
|
+
onProgress(event.percentage, event.message);
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
this.on('progress', listeners.progress);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (onQuestion) {
|
|
843
|
+
listeners.question = (event) => {
|
|
844
|
+
if (event.executionId) {
|
|
845
|
+
onQuestion(event.text);
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
this.on('question', listeners.question);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (onError) {
|
|
852
|
+
listeners.error = (event) => {
|
|
853
|
+
if (event.executionId) {
|
|
854
|
+
onError(event.message, event.source);
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
this.on('error', listeners.error);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
try {
|
|
861
|
+
// Execute with subprocess
|
|
862
|
+
const result = await this.executeViaSubprocess(agentName, task, options);
|
|
863
|
+
return result;
|
|
864
|
+
} finally {
|
|
865
|
+
// Cleanup listeners
|
|
866
|
+
for (const [event, listener] of Object.entries(listeners)) {
|
|
867
|
+
this.removeListener(event, listener);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Cancel an active execution
|
|
874
|
+
*
|
|
875
|
+
* @param {string} executionId - Execution ID to cancel
|
|
876
|
+
* @returns {boolean} True if cancelled, false if not found
|
|
877
|
+
*/
|
|
878
|
+
cancel(executionId) {
|
|
879
|
+
const execution = this.activeExecutions.get(executionId);
|
|
880
|
+
if (!execution) {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (execution.process) {
|
|
885
|
+
execution.state = ExecutionState.CANCELLED;
|
|
886
|
+
execution.process.kill('SIGTERM');
|
|
887
|
+
|
|
888
|
+
// Force kill after timeout
|
|
889
|
+
setTimeout(() => {
|
|
890
|
+
if (execution.process && !execution.process.killed) {
|
|
891
|
+
execution.process.kill('SIGKILL');
|
|
892
|
+
}
|
|
893
|
+
}, 5000);
|
|
894
|
+
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Get execution status
|
|
903
|
+
*
|
|
904
|
+
* @param {string} executionId - Execution ID
|
|
905
|
+
* @returns {Object|null} Execution status or null if not found
|
|
906
|
+
*/
|
|
907
|
+
getExecutionStatus(executionId) {
|
|
908
|
+
const execution = this.activeExecutions.get(executionId);
|
|
909
|
+
if (!execution) {
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const duration = execution.endTime
|
|
914
|
+
? execution.endTime - execution.startTime
|
|
915
|
+
: Date.now() - execution.startTime;
|
|
916
|
+
|
|
917
|
+
return {
|
|
918
|
+
id: execution.id,
|
|
919
|
+
agent: execution.agent,
|
|
920
|
+
task: execution.task,
|
|
921
|
+
state: execution.state,
|
|
922
|
+
startTime: execution.startTime,
|
|
923
|
+
endTime: execution.endTime,
|
|
924
|
+
duration,
|
|
925
|
+
exitCode: execution.exitCode,
|
|
926
|
+
outputLength: execution.output.length,
|
|
927
|
+
errorLength: execution.error.length
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* List all active executions
|
|
933
|
+
*
|
|
934
|
+
* @returns {Object[]} Array of execution statuses
|
|
935
|
+
*/
|
|
936
|
+
listActiveExecutions() {
|
|
937
|
+
return Array.from(this.activeExecutions.keys()).map(id =>
|
|
938
|
+
this.getExecutionStatus(id)
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Validate task input
|
|
944
|
+
*
|
|
945
|
+
* @private
|
|
946
|
+
* @param {string} task - Task description
|
|
947
|
+
* @throws {Error} If task is invalid
|
|
948
|
+
*/
|
|
949
|
+
_validateTask(task) {
|
|
950
|
+
if (typeof task !== 'string') {
|
|
951
|
+
throw new Error('Task must be a string');
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (task.length === 0) {
|
|
955
|
+
throw new Error('Task cannot be empty');
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (task.length > MAX_TASK_LENGTH) {
|
|
959
|
+
throw new Error(
|
|
960
|
+
`Task exceeds maximum length of ${MAX_TASK_LENGTH / 1024}KB`
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Check for dangerous shell metacharacters
|
|
965
|
+
const dangerousPatterns = [
|
|
966
|
+
/\$\(/, // Command substitution
|
|
967
|
+
/`/, // Backtick command substitution
|
|
968
|
+
];
|
|
969
|
+
|
|
970
|
+
for (const pattern of dangerousPatterns) {
|
|
971
|
+
if (pattern.test(task)) {
|
|
972
|
+
throw new Error(
|
|
973
|
+
'Task contains potentially dangerous shell metacharacters'
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Check if error is retryable
|
|
981
|
+
*
|
|
982
|
+
* @private
|
|
983
|
+
* @param {Error} error - Error to check
|
|
984
|
+
* @returns {boolean} True if retryable
|
|
985
|
+
*/
|
|
986
|
+
_isRetryableError(error) {
|
|
987
|
+
const retryablePatterns = [
|
|
988
|
+
/timeout/i,
|
|
989
|
+
/ECONNREFUSED/i,
|
|
990
|
+
/ETIMEDOUT/i,
|
|
991
|
+
/rate limit/i,
|
|
992
|
+
/429/,
|
|
993
|
+
];
|
|
994
|
+
|
|
995
|
+
return retryablePatterns.some(pattern =>
|
|
996
|
+
pattern.test(error.message)
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Sleep for specified duration
|
|
1002
|
+
*
|
|
1003
|
+
* @private
|
|
1004
|
+
* @param {number} ms - Duration in milliseconds
|
|
1005
|
+
* @returns {Promise<void>}
|
|
1006
|
+
*/
|
|
1007
|
+
_sleep(ms) {
|
|
1008
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Create a Claude executor instance
|
|
1014
|
+
*
|
|
1015
|
+
* @param {Object} options - Executor options
|
|
1016
|
+
* @returns {ClaudeExecutor} Executor instance
|
|
1017
|
+
*/
|
|
1018
|
+
export function createClaudeExecutor(options = {}) {
|
|
1019
|
+
return new ClaudeExecutor(options);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Create a Claude executor with specified mode (convenience method)
|
|
1024
|
+
*
|
|
1025
|
+
* @param {Object} options - Executor options
|
|
1026
|
+
* @returns {ClaudeExecutor} Executor instance
|
|
1027
|
+
*/
|
|
1028
|
+
export function createExecutor(options = {}) {
|
|
1029
|
+
return new ClaudeExecutor(options);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Execute a one-off agent task (convenience method)
|
|
1034
|
+
*
|
|
1035
|
+
* @param {string} agentName - Agent name
|
|
1036
|
+
* @param {string} task - Task description
|
|
1037
|
+
* @param {Object} context - Execution context
|
|
1038
|
+
* @param {Object} options - Executor options
|
|
1039
|
+
* @returns {Promise<Object>} Execution result
|
|
1040
|
+
*/
|
|
1041
|
+
export async function executeAgent(agentName, task, context = {}, options = {}) {
|
|
1042
|
+
const executor = new ClaudeExecutor(options);
|
|
1043
|
+
return executor.execute(agentName, task, context, options);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
export default ClaudeExecutor;
|