@itz4blitz/agentful 0.4.0 → 0.5.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/README.md +133 -5
- 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 +232 -9
- 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,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example Client for Agentful Server
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates how to make authenticated requests to agentful serve
|
|
5
|
+
*
|
|
6
|
+
* @module server/client-example
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { generateHMACHeaders } from './auth.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Example: Trigger agent execution with HMAC authentication
|
|
13
|
+
*/
|
|
14
|
+
async function triggerAgentWithHMAC() {
|
|
15
|
+
const serverUrl = 'https://your-server.com:3000';
|
|
16
|
+
const secret = 'your-shared-secret';
|
|
17
|
+
|
|
18
|
+
// Request body
|
|
19
|
+
const body = JSON.stringify({
|
|
20
|
+
agent: 'backend',
|
|
21
|
+
task: 'Implement user authentication API',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Generate HMAC headers
|
|
25
|
+
const headers = generateHMACHeaders(body, secret);
|
|
26
|
+
|
|
27
|
+
// Make request
|
|
28
|
+
const response = await fetch(`${serverUrl}/trigger`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
...headers,
|
|
33
|
+
},
|
|
34
|
+
body,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
const error = await response.json();
|
|
39
|
+
throw new Error(`Request failed: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = await response.json();
|
|
43
|
+
console.log('Execution started:', result.executionId);
|
|
44
|
+
|
|
45
|
+
return result.executionId;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Example: Check execution status
|
|
50
|
+
*/
|
|
51
|
+
async function checkExecutionStatus(executionId) {
|
|
52
|
+
const serverUrl = 'https://your-server.com:3000';
|
|
53
|
+
const secret = 'your-shared-secret';
|
|
54
|
+
|
|
55
|
+
// For GET requests, body is empty
|
|
56
|
+
const body = '';
|
|
57
|
+
const headers = generateHMACHeaders(body, secret);
|
|
58
|
+
|
|
59
|
+
const response = await fetch(`${serverUrl}/status/${executionId}`, {
|
|
60
|
+
method: 'GET',
|
|
61
|
+
headers,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const error = await response.json();
|
|
66
|
+
throw new Error(`Request failed: ${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const status = await response.json();
|
|
70
|
+
console.log('Execution status:', status.state);
|
|
71
|
+
console.log('Duration:', status.duration, 'ms');
|
|
72
|
+
|
|
73
|
+
return status;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Example: Trigger and poll for completion
|
|
78
|
+
*/
|
|
79
|
+
async function triggerAndWait() {
|
|
80
|
+
const executionId = await triggerAgentWithHMAC();
|
|
81
|
+
|
|
82
|
+
console.log('Waiting for execution to complete...');
|
|
83
|
+
|
|
84
|
+
// Poll every 5 seconds
|
|
85
|
+
while (true) {
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
87
|
+
|
|
88
|
+
const status = await checkExecutionStatus(executionId);
|
|
89
|
+
|
|
90
|
+
if (status.state === 'completed') {
|
|
91
|
+
console.log('Execution completed successfully!');
|
|
92
|
+
console.log('Output:', status.output);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (status.state === 'failed') {
|
|
97
|
+
console.error('Execution failed:', status.error);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Example: No authentication (localhost only)
|
|
105
|
+
*/
|
|
106
|
+
async function triggerAgentLocalhost() {
|
|
107
|
+
const serverUrl = 'http://localhost:3000';
|
|
108
|
+
|
|
109
|
+
const response = await fetch(`${serverUrl}/trigger`, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: {
|
|
112
|
+
'Content-Type': 'application/json',
|
|
113
|
+
},
|
|
114
|
+
body: JSON.stringify({
|
|
115
|
+
agent: 'backend',
|
|
116
|
+
task: 'Implement user authentication API',
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const result = await response.json();
|
|
121
|
+
console.log('Execution started:', result.executionId);
|
|
122
|
+
|
|
123
|
+
return result.executionId;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Example: Tailscale mode (no auth headers needed)
|
|
128
|
+
*/
|
|
129
|
+
async function triggerAgentTailscale() {
|
|
130
|
+
const serverUrl = 'http://your-tailscale-ip:3000';
|
|
131
|
+
|
|
132
|
+
const response = await fetch(`${serverUrl}/trigger`, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: {
|
|
135
|
+
'Content-Type': 'application/json',
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
agent: 'backend',
|
|
139
|
+
task: 'Implement user authentication API',
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const result = await response.json();
|
|
144
|
+
console.log('Execution started:', result.executionId);
|
|
145
|
+
|
|
146
|
+
return result.executionId;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Example: List available agents
|
|
151
|
+
*/
|
|
152
|
+
async function listAgents() {
|
|
153
|
+
const serverUrl = 'http://localhost:3000';
|
|
154
|
+
|
|
155
|
+
const response = await fetch(`${serverUrl}/agents`);
|
|
156
|
+
const result = await response.json();
|
|
157
|
+
|
|
158
|
+
console.log('Available agents:', result.agents);
|
|
159
|
+
return result.agents;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Example: List recent executions
|
|
164
|
+
*/
|
|
165
|
+
async function listExecutions(filters = {}) {
|
|
166
|
+
const serverUrl = 'http://localhost:3000';
|
|
167
|
+
|
|
168
|
+
// Build query string
|
|
169
|
+
const params = new URLSearchParams();
|
|
170
|
+
if (filters.agent) params.append('agent', filters.agent);
|
|
171
|
+
if (filters.state) params.append('state', filters.state);
|
|
172
|
+
if (filters.limit) params.append('limit', filters.limit.toString());
|
|
173
|
+
|
|
174
|
+
const response = await fetch(`${serverUrl}/executions?${params}`);
|
|
175
|
+
const result = await response.json();
|
|
176
|
+
|
|
177
|
+
console.log(`Found ${result.count} executions`);
|
|
178
|
+
return result.executions;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Export examples
|
|
182
|
+
export default {
|
|
183
|
+
triggerAgentWithHMAC,
|
|
184
|
+
checkExecutionStatus,
|
|
185
|
+
triggerAndWait,
|
|
186
|
+
triggerAgentLocalhost,
|
|
187
|
+
triggerAgentTailscale,
|
|
188
|
+
listAgents,
|
|
189
|
+
listExecutions,
|
|
190
|
+
};
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Execution Logic for Agentful Server
|
|
3
|
+
*
|
|
4
|
+
* Spawns Claude Code CLI with agent prompts and manages execution state.
|
|
5
|
+
*
|
|
6
|
+
* @module server/executor
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn } from 'child_process';
|
|
10
|
+
import { randomUUID } from 'crypto';
|
|
11
|
+
import { loadAgentDefinition } from '../ci/claude-action-integration.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Execution states
|
|
15
|
+
*/
|
|
16
|
+
export const ExecutionState = {
|
|
17
|
+
PENDING: 'pending',
|
|
18
|
+
RUNNING: 'running',
|
|
19
|
+
COMPLETED: 'completed',
|
|
20
|
+
FAILED: 'failed',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Maximum output size (1MB per execution)
|
|
25
|
+
*/
|
|
26
|
+
const MAX_OUTPUT_SIZE = 1 * 1024 * 1024;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Maximum task length (10KB)
|
|
30
|
+
*/
|
|
31
|
+
const MAX_TASK_LENGTH = 10 * 1024;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Allowed environment variable whitelist
|
|
35
|
+
*/
|
|
36
|
+
const ALLOWED_ENV_VARS = new Set(['NODE_ENV', 'DEBUG', 'LOG_LEVEL']);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate agent name to prevent path traversal
|
|
40
|
+
* @param {string} agentName - Agent name to validate
|
|
41
|
+
* @returns {boolean} True if valid
|
|
42
|
+
*/
|
|
43
|
+
function isValidAgentName(agentName) {
|
|
44
|
+
// Only allow alphanumeric, hyphens, and underscores
|
|
45
|
+
return /^[a-zA-Z0-9_-]+$/.test(agentName);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Sanitize task input to prevent command injection
|
|
50
|
+
* @param {string} task - Task description
|
|
51
|
+
* @returns {Object} Validation result { valid: boolean, error?: string }
|
|
52
|
+
*/
|
|
53
|
+
function validateTask(task) {
|
|
54
|
+
if (typeof task !== 'string') {
|
|
55
|
+
return { valid: false, error: 'Task must be a string' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (task.length > MAX_TASK_LENGTH) {
|
|
59
|
+
return {
|
|
60
|
+
valid: false,
|
|
61
|
+
error: `Task exceeds maximum length of ${MAX_TASK_LENGTH / 1024}KB`
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check for shell metacharacters that could be dangerous
|
|
66
|
+
const dangerousPatterns = [
|
|
67
|
+
/\$\(/, // Command substitution
|
|
68
|
+
/`/, // Backtick command substitution
|
|
69
|
+
/\|\|/, // Or operator
|
|
70
|
+
/&&/, // And operator
|
|
71
|
+
/;/, // Command separator
|
|
72
|
+
/>/, // Output redirection
|
|
73
|
+
/</, // Input redirection
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
for (const pattern of dangerousPatterns) {
|
|
77
|
+
if (pattern.test(task)) {
|
|
78
|
+
return {
|
|
79
|
+
valid: false,
|
|
80
|
+
error: 'Task contains potentially dangerous shell metacharacters'
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { valid: true };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Filter environment variables to whitelist only
|
|
90
|
+
* @param {Object} env - Environment variables
|
|
91
|
+
* @returns {Object} Filtered environment variables
|
|
92
|
+
*/
|
|
93
|
+
function filterEnvironmentVars(env) {
|
|
94
|
+
const filtered = {};
|
|
95
|
+
for (const [key, value] of Object.entries(env)) {
|
|
96
|
+
if (ALLOWED_ENV_VARS.has(key)) {
|
|
97
|
+
filtered[key] = value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return filtered;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* In-memory execution store
|
|
105
|
+
* In production, use a database or distributed cache
|
|
106
|
+
*/
|
|
107
|
+
const executions = new Map();
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build agent prompt from task
|
|
111
|
+
* @param {Object} agent - Agent definition
|
|
112
|
+
* @param {string} task - Task description
|
|
113
|
+
* @returns {string} Formatted prompt
|
|
114
|
+
*/
|
|
115
|
+
function buildAgentPrompt(agent, task) {
|
|
116
|
+
return `# Task for ${agent.metadata.name} Agent
|
|
117
|
+
|
|
118
|
+
${task}
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
# Agent Instructions
|
|
123
|
+
|
|
124
|
+
${agent.instructions}
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Execute agent with Claude Code CLI
|
|
130
|
+
* @param {string} agentName - Name of the agent
|
|
131
|
+
* @param {string} task - Task description
|
|
132
|
+
* @param {Object} options - Execution options
|
|
133
|
+
* @param {string} [options.projectRoot] - Project root directory
|
|
134
|
+
* @param {number} [options.timeout] - Execution timeout in ms
|
|
135
|
+
* @param {Object} [options.env] - Additional environment variables
|
|
136
|
+
* @returns {Promise<Object>} Execution result
|
|
137
|
+
*/
|
|
138
|
+
export async function executeAgent(agentName, task, options = {}) {
|
|
139
|
+
const {
|
|
140
|
+
projectRoot = process.cwd(),
|
|
141
|
+
timeout = 10 * 60 * 1000, // 10 minutes default
|
|
142
|
+
env = {},
|
|
143
|
+
} = options;
|
|
144
|
+
|
|
145
|
+
// Validate agent name to prevent path traversal
|
|
146
|
+
if (!isValidAgentName(agentName)) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Invalid agent name: "${agentName}". ` +
|
|
149
|
+
`Agent names must contain only alphanumeric characters, hyphens, and underscores.`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Validate and sanitize task input
|
|
154
|
+
const taskValidation = validateTask(task);
|
|
155
|
+
if (!taskValidation.valid) {
|
|
156
|
+
throw new Error(`Invalid task: ${taskValidation.error}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Filter environment variables to whitelist
|
|
160
|
+
const filteredEnv = filterEnvironmentVars(env);
|
|
161
|
+
|
|
162
|
+
const executionId = randomUUID();
|
|
163
|
+
|
|
164
|
+
// Initialize execution record
|
|
165
|
+
const execution = {
|
|
166
|
+
id: executionId,
|
|
167
|
+
agent: agentName,
|
|
168
|
+
task,
|
|
169
|
+
state: ExecutionState.PENDING,
|
|
170
|
+
startTime: Date.now(),
|
|
171
|
+
endTime: null,
|
|
172
|
+
output: '',
|
|
173
|
+
error: null,
|
|
174
|
+
exitCode: null,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
executions.set(executionId, execution);
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
// Load agent definition
|
|
181
|
+
const agent = await loadAgentDefinition(agentName, projectRoot);
|
|
182
|
+
|
|
183
|
+
// Build prompt
|
|
184
|
+
const prompt = buildAgentPrompt(agent, task);
|
|
185
|
+
|
|
186
|
+
// Update state to running
|
|
187
|
+
execution.state = ExecutionState.RUNNING;
|
|
188
|
+
execution.agentMetadata = agent.metadata;
|
|
189
|
+
|
|
190
|
+
// Spawn Claude Code CLI
|
|
191
|
+
const claude = spawn('claude', ['-p', prompt], {
|
|
192
|
+
cwd: projectRoot,
|
|
193
|
+
env: {
|
|
194
|
+
...process.env,
|
|
195
|
+
...filteredEnv,
|
|
196
|
+
// Disable interactive prompts
|
|
197
|
+
CLAUDE_NON_INTERACTIVE: '1',
|
|
198
|
+
},
|
|
199
|
+
timeout,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Capture output with size limit
|
|
203
|
+
const outputChunks = [];
|
|
204
|
+
const errorChunks = [];
|
|
205
|
+
let outputSize = 0;
|
|
206
|
+
let outputTruncated = false;
|
|
207
|
+
|
|
208
|
+
claude.stdout.on('data', (data) => {
|
|
209
|
+
const chunk = data.toString();
|
|
210
|
+
outputChunks.push(chunk);
|
|
211
|
+
|
|
212
|
+
// Check output size limit
|
|
213
|
+
if (outputSize < MAX_OUTPUT_SIZE) {
|
|
214
|
+
const remainingSpace = MAX_OUTPUT_SIZE - outputSize;
|
|
215
|
+
const chunkToAdd = chunk.length <= remainingSpace
|
|
216
|
+
? chunk
|
|
217
|
+
: chunk.substring(0, remainingSpace) + '\n[Output truncated - limit reached]';
|
|
218
|
+
|
|
219
|
+
execution.output += chunkToAdd;
|
|
220
|
+
outputSize += chunk.length;
|
|
221
|
+
|
|
222
|
+
if (chunk.length > remainingSpace) {
|
|
223
|
+
outputTruncated = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
claude.stderr.on('data', (data) => {
|
|
229
|
+
const chunk = data.toString();
|
|
230
|
+
errorChunks.push(chunk);
|
|
231
|
+
|
|
232
|
+
// Check output size limit
|
|
233
|
+
if (outputSize < MAX_OUTPUT_SIZE) {
|
|
234
|
+
const remainingSpace = MAX_OUTPUT_SIZE - outputSize;
|
|
235
|
+
const chunkToAdd = chunk.length <= remainingSpace
|
|
236
|
+
? chunk
|
|
237
|
+
: chunk.substring(0, remainingSpace) + '\n[Output truncated - limit reached]';
|
|
238
|
+
|
|
239
|
+
execution.output += chunkToAdd;
|
|
240
|
+
outputSize += chunk.length;
|
|
241
|
+
|
|
242
|
+
if (chunk.length > remainingSpace) {
|
|
243
|
+
outputTruncated = true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Wait for completion
|
|
249
|
+
let timeoutHandle = null;
|
|
250
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
251
|
+
claude.on('close', (code) => {
|
|
252
|
+
// Clear timeout on normal completion
|
|
253
|
+
if (timeoutHandle) {
|
|
254
|
+
clearTimeout(timeoutHandle);
|
|
255
|
+
timeoutHandle = null;
|
|
256
|
+
}
|
|
257
|
+
resolve(code);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
claude.on('error', (error) => {
|
|
261
|
+
// Clear timeout on error
|
|
262
|
+
if (timeoutHandle) {
|
|
263
|
+
clearTimeout(timeoutHandle);
|
|
264
|
+
timeoutHandle = null;
|
|
265
|
+
}
|
|
266
|
+
reject(error);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Handle timeout
|
|
270
|
+
if (timeout) {
|
|
271
|
+
timeoutHandle = setTimeout(() => {
|
|
272
|
+
timeoutHandle = null;
|
|
273
|
+
claude.kill('SIGTERM');
|
|
274
|
+
reject(new Error(`Execution timeout after ${timeout}ms`));
|
|
275
|
+
}, timeout);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Add truncation notice if output was limited
|
|
280
|
+
if (outputTruncated) {
|
|
281
|
+
execution.output += '\n\n[Note: Output was truncated due to size limit]';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Update execution record
|
|
285
|
+
execution.endTime = Date.now();
|
|
286
|
+
execution.exitCode = exitCode;
|
|
287
|
+
|
|
288
|
+
if (exitCode === 0) {
|
|
289
|
+
execution.state = ExecutionState.COMPLETED;
|
|
290
|
+
} else {
|
|
291
|
+
execution.state = ExecutionState.FAILED;
|
|
292
|
+
execution.error = `Claude exited with code ${exitCode}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
executionId,
|
|
297
|
+
state: execution.state,
|
|
298
|
+
exitCode,
|
|
299
|
+
};
|
|
300
|
+
} catch (error) {
|
|
301
|
+
// Update execution with error
|
|
302
|
+
execution.state = ExecutionState.FAILED;
|
|
303
|
+
execution.endTime = Date.now();
|
|
304
|
+
execution.error = error.message;
|
|
305
|
+
execution.exitCode = -1;
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
executionId,
|
|
309
|
+
state: ExecutionState.FAILED,
|
|
310
|
+
error: error.message,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Get execution status
|
|
317
|
+
* @param {string} executionId - Execution ID
|
|
318
|
+
* @returns {Object|null} Execution details or null if not found
|
|
319
|
+
*/
|
|
320
|
+
export function getExecutionStatus(executionId) {
|
|
321
|
+
const execution = executions.get(executionId);
|
|
322
|
+
|
|
323
|
+
if (!execution) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Calculate duration
|
|
328
|
+
const duration = execution.endTime
|
|
329
|
+
? execution.endTime - execution.startTime
|
|
330
|
+
: Date.now() - execution.startTime;
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
id: execution.id,
|
|
334
|
+
agent: execution.agent,
|
|
335
|
+
task: execution.task,
|
|
336
|
+
state: execution.state,
|
|
337
|
+
startTime: execution.startTime,
|
|
338
|
+
endTime: execution.endTime,
|
|
339
|
+
duration,
|
|
340
|
+
output: execution.output,
|
|
341
|
+
error: execution.error,
|
|
342
|
+
exitCode: execution.exitCode,
|
|
343
|
+
metadata: execution.agentMetadata,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* List all executions (with optional filtering)
|
|
349
|
+
* @param {Object} filters - Filter options
|
|
350
|
+
* @param {string} [filters.agent] - Filter by agent name
|
|
351
|
+
* @param {string} [filters.state] - Filter by state
|
|
352
|
+
* @param {number} [filters.limit] - Maximum number of results
|
|
353
|
+
* @returns {Object[]} Array of execution summaries
|
|
354
|
+
*/
|
|
355
|
+
export function listExecutions(filters = {}) {
|
|
356
|
+
const { agent, state, limit = 100 } = filters;
|
|
357
|
+
|
|
358
|
+
let results = Array.from(executions.values());
|
|
359
|
+
|
|
360
|
+
// Apply filters
|
|
361
|
+
if (agent) {
|
|
362
|
+
results = results.filter((e) => e.agent === agent);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (state) {
|
|
366
|
+
results = results.filter((e) => e.state === state);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Sort by start time (newest first)
|
|
370
|
+
results.sort((a, b) => b.startTime - a.startTime);
|
|
371
|
+
|
|
372
|
+
// Limit results
|
|
373
|
+
results = results.slice(0, limit);
|
|
374
|
+
|
|
375
|
+
// Return summary (no output to keep response small)
|
|
376
|
+
return results.map((e) => ({
|
|
377
|
+
id: e.id,
|
|
378
|
+
agent: e.agent,
|
|
379
|
+
task: e.task,
|
|
380
|
+
state: e.state,
|
|
381
|
+
startTime: e.startTime,
|
|
382
|
+
endTime: e.endTime,
|
|
383
|
+
duration: e.endTime ? e.endTime - e.startTime : Date.now() - e.startTime,
|
|
384
|
+
exitCode: e.exitCode,
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Clean up old executions (to prevent memory leak)
|
|
390
|
+
* @param {number} maxAge - Maximum age in ms (default: 1 hour)
|
|
391
|
+
* @returns {number} Number of executions cleaned up
|
|
392
|
+
*/
|
|
393
|
+
export function cleanupExecutions(maxAge = 60 * 60 * 1000) {
|
|
394
|
+
const cutoff = Date.now() - maxAge;
|
|
395
|
+
let cleaned = 0;
|
|
396
|
+
|
|
397
|
+
for (const [id, execution] of executions.entries()) {
|
|
398
|
+
if (execution.endTime && execution.endTime < cutoff) {
|
|
399
|
+
executions.delete(id);
|
|
400
|
+
cleaned++;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return cleaned;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Start periodic cleanup (runs every hour)
|
|
409
|
+
*/
|
|
410
|
+
export function startPeriodicCleanup() {
|
|
411
|
+
setInterval(() => {
|
|
412
|
+
const cleaned = cleanupExecutions();
|
|
413
|
+
if (cleaned > 0) {
|
|
414
|
+
console.log(`Cleaned up ${cleaned} old executions`);
|
|
415
|
+
}
|
|
416
|
+
}, 60 * 60 * 1000); // Run every hour
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export default {
|
|
420
|
+
executeAgent,
|
|
421
|
+
getExecutionStatus,
|
|
422
|
+
listExecutions,
|
|
423
|
+
cleanupExecutions,
|
|
424
|
+
startPeriodicCleanup,
|
|
425
|
+
ExecutionState,
|
|
426
|
+
};
|