@itz4blitz/agentful 1.2.0 → 1.3.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 +28 -1
- package/bin/cli.js +11 -1055
- package/bin/hooks/block-file-creation.js +271 -0
- package/bin/hooks/product-spec-watcher.js +151 -0
- package/lib/index.js +0 -11
- package/lib/init.js +2 -21
- package/lib/parallel-execution.js +235 -0
- package/lib/presets.js +26 -4
- package/package.json +4 -7
- package/template/.claude/agents/architect.md +2 -2
- package/template/.claude/agents/backend.md +17 -30
- package/template/.claude/agents/frontend.md +17 -39
- package/template/.claude/agents/orchestrator.md +63 -4
- package/template/.claude/agents/product-analyzer.md +1 -1
- package/template/.claude/agents/tester.md +16 -29
- package/template/.claude/commands/agentful-generate.md +221 -14
- package/template/.claude/commands/agentful-init.md +621 -0
- package/template/.claude/commands/agentful-product.md +1 -1
- package/template/.claude/commands/agentful-start.md +99 -1
- package/template/.claude/product/EXAMPLES.md +2 -2
- package/template/.claude/product/index.md +1 -1
- package/template/.claude/settings.json +22 -0
- package/template/.claude/skills/research/SKILL.md +432 -0
- package/template/CLAUDE.md +5 -6
- package/template/bin/hooks/architect-drift-detector.js +242 -0
- package/template/bin/hooks/product-spec-watcher.js +151 -0
- package/version.json +1 -1
- package/bin/hooks/post-agent.js +0 -101
- package/bin/hooks/post-feature.js +0 -227
- package/bin/hooks/pre-agent.js +0 -118
- package/bin/hooks/pre-feature.js +0 -138
- package/lib/VALIDATION_README.md +0 -455
- package/lib/ci/claude-action-integration.js +0 -641
- package/lib/ci/index.js +0 -10
- package/lib/core/analyzer.js +0 -497
- package/lib/core/cli.js +0 -141
- package/lib/core/detectors/conventions.js +0 -342
- package/lib/core/detectors/framework.js +0 -276
- package/lib/core/detectors/index.js +0 -15
- package/lib/core/detectors/language.js +0 -199
- package/lib/core/detectors/patterns.js +0 -356
- package/lib/core/generator.js +0 -626
- package/lib/core/index.js +0 -9
- package/lib/core/output-parser.js +0 -458
- package/lib/core/storage.js +0 -515
- package/lib/core/templates.js +0 -556
- package/lib/pipeline/cli.js +0 -423
- package/lib/pipeline/engine.js +0 -928
- package/lib/pipeline/executor.js +0 -440
- package/lib/pipeline/index.js +0 -33
- package/lib/pipeline/integrations.js +0 -559
- package/lib/pipeline/schemas.js +0 -288
- package/lib/remote/client.js +0 -361
- package/lib/server/auth.js +0 -270
- package/lib/server/client-example.js +0 -190
- package/lib/server/executor.js +0 -477
- package/lib/server/index.js +0 -494
- package/lib/update-helpers.js +0 -505
- package/lib/validation.js +0 -460
package/lib/server/executor.js
DELETED
|
@@ -1,477 +0,0 @@
|
|
|
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
|
-
* @param {boolean} [options.async=false] - If true, return immediately with executionId
|
|
137
|
-
* @returns {Promise<Object>} Execution result (or just executionId if async=true)
|
|
138
|
-
*/
|
|
139
|
-
export async function executeAgent(agentName, task, options = {}) {
|
|
140
|
-
const {
|
|
141
|
-
projectRoot = process.cwd(),
|
|
142
|
-
timeout = 10 * 60 * 1000, // 10 minutes default
|
|
143
|
-
env = {},
|
|
144
|
-
async = false, // New option for non-blocking execution
|
|
145
|
-
} = options;
|
|
146
|
-
|
|
147
|
-
// Validate agent name to prevent path traversal
|
|
148
|
-
if (!isValidAgentName(agentName)) {
|
|
149
|
-
throw new Error(
|
|
150
|
-
`Invalid agent name: "${agentName}". ` +
|
|
151
|
-
`Agent names must contain only alphanumeric characters, hyphens, and underscores.`
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Validate and sanitize task input
|
|
156
|
-
const taskValidation = validateTask(task);
|
|
157
|
-
if (!taskValidation.valid) {
|
|
158
|
-
throw new Error(`Invalid task: ${taskValidation.error}`);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Filter environment variables to whitelist
|
|
162
|
-
const filteredEnv = filterEnvironmentVars(env);
|
|
163
|
-
|
|
164
|
-
const executionId = randomUUID();
|
|
165
|
-
|
|
166
|
-
// Initialize execution record
|
|
167
|
-
const execution = {
|
|
168
|
-
id: executionId,
|
|
169
|
-
agent: agentName,
|
|
170
|
-
task,
|
|
171
|
-
state: ExecutionState.PENDING,
|
|
172
|
-
startTime: Date.now(),
|
|
173
|
-
endTime: null,
|
|
174
|
-
output: '',
|
|
175
|
-
error: null,
|
|
176
|
-
exitCode: null,
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
executions.set(executionId, execution);
|
|
180
|
-
|
|
181
|
-
// If async mode, start execution in background and return immediately
|
|
182
|
-
if (async) {
|
|
183
|
-
// Start execution in background (don't await)
|
|
184
|
-
runAgentExecution(executionId, agentName, task, {
|
|
185
|
-
projectRoot,
|
|
186
|
-
timeout,
|
|
187
|
-
filteredEnv,
|
|
188
|
-
}).catch((error) => {
|
|
189
|
-
// Update execution with error if background execution fails
|
|
190
|
-
const exec = executions.get(executionId);
|
|
191
|
-
if (exec) {
|
|
192
|
-
exec.state = ExecutionState.FAILED;
|
|
193
|
-
exec.endTime = Date.now();
|
|
194
|
-
exec.error = error.message;
|
|
195
|
-
exec.exitCode = -1;
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
return {
|
|
200
|
-
executionId,
|
|
201
|
-
state: ExecutionState.PENDING,
|
|
202
|
-
message: 'Execution started in background',
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Synchronous mode - wait for completion and return full result
|
|
207
|
-
return runAgentExecution(executionId, agentName, task, {
|
|
208
|
-
projectRoot,
|
|
209
|
-
timeout,
|
|
210
|
-
filteredEnv,
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Internal function to run agent execution
|
|
216
|
-
* @param {string} executionId - Execution ID
|
|
217
|
-
* @param {string} agentName - Agent name
|
|
218
|
-
* @param {string} task - Task description
|
|
219
|
-
* @param {Object} options - Execution options
|
|
220
|
-
* @returns {Promise<Object>} Execution result
|
|
221
|
-
*/
|
|
222
|
-
async function runAgentExecution(executionId, agentName, task, options) {
|
|
223
|
-
const { projectRoot, timeout, filteredEnv } = options;
|
|
224
|
-
const execution = executions.get(executionId);
|
|
225
|
-
|
|
226
|
-
if (!execution) {
|
|
227
|
-
throw new Error(`Execution ${executionId} not found`);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
try {
|
|
231
|
-
// Load agent definition
|
|
232
|
-
const agent = await loadAgentDefinition(agentName, projectRoot);
|
|
233
|
-
|
|
234
|
-
// Build prompt
|
|
235
|
-
const prompt = buildAgentPrompt(agent, task);
|
|
236
|
-
|
|
237
|
-
// Update state to running
|
|
238
|
-
execution.state = ExecutionState.RUNNING;
|
|
239
|
-
execution.agentMetadata = agent.metadata;
|
|
240
|
-
|
|
241
|
-
// Spawn Claude Code CLI
|
|
242
|
-
const claude = spawn('claude', ['-p', prompt], {
|
|
243
|
-
cwd: projectRoot,
|
|
244
|
-
env: {
|
|
245
|
-
...process.env,
|
|
246
|
-
...filteredEnv,
|
|
247
|
-
// Disable interactive prompts
|
|
248
|
-
CLAUDE_NON_INTERACTIVE: '1',
|
|
249
|
-
},
|
|
250
|
-
timeout,
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
// Capture output with size limit
|
|
254
|
-
const outputChunks = [];
|
|
255
|
-
const errorChunks = [];
|
|
256
|
-
let outputSize = 0;
|
|
257
|
-
let outputTruncated = false;
|
|
258
|
-
|
|
259
|
-
claude.stdout.on('data', (data) => {
|
|
260
|
-
const chunk = data.toString();
|
|
261
|
-
outputChunks.push(chunk);
|
|
262
|
-
|
|
263
|
-
// Check output size limit
|
|
264
|
-
if (outputSize < MAX_OUTPUT_SIZE) {
|
|
265
|
-
const remainingSpace = MAX_OUTPUT_SIZE - outputSize;
|
|
266
|
-
const chunkToAdd = chunk.length <= remainingSpace
|
|
267
|
-
? chunk
|
|
268
|
-
: chunk.substring(0, remainingSpace) + '\n[Output truncated - limit reached]';
|
|
269
|
-
|
|
270
|
-
execution.output += chunkToAdd;
|
|
271
|
-
outputSize += chunk.length;
|
|
272
|
-
|
|
273
|
-
if (chunk.length > remainingSpace) {
|
|
274
|
-
outputTruncated = true;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
claude.stderr.on('data', (data) => {
|
|
280
|
-
const chunk = data.toString();
|
|
281
|
-
errorChunks.push(chunk);
|
|
282
|
-
|
|
283
|
-
// Check output size limit
|
|
284
|
-
if (outputSize < MAX_OUTPUT_SIZE) {
|
|
285
|
-
const remainingSpace = MAX_OUTPUT_SIZE - outputSize;
|
|
286
|
-
const chunkToAdd = chunk.length <= remainingSpace
|
|
287
|
-
? chunk
|
|
288
|
-
: chunk.substring(0, remainingSpace) + '\n[Output truncated - limit reached]';
|
|
289
|
-
|
|
290
|
-
execution.output += chunkToAdd;
|
|
291
|
-
outputSize += chunk.length;
|
|
292
|
-
|
|
293
|
-
if (chunk.length > remainingSpace) {
|
|
294
|
-
outputTruncated = true;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
// Wait for completion
|
|
300
|
-
let timeoutHandle = null;
|
|
301
|
-
const exitCode = await new Promise((resolve, reject) => {
|
|
302
|
-
claude.on('close', (code) => {
|
|
303
|
-
// Clear timeout on normal completion
|
|
304
|
-
if (timeoutHandle) {
|
|
305
|
-
clearTimeout(timeoutHandle);
|
|
306
|
-
timeoutHandle = null;
|
|
307
|
-
}
|
|
308
|
-
resolve(code);
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
claude.on('error', (error) => {
|
|
312
|
-
// Clear timeout on error
|
|
313
|
-
if (timeoutHandle) {
|
|
314
|
-
clearTimeout(timeoutHandle);
|
|
315
|
-
timeoutHandle = null;
|
|
316
|
-
}
|
|
317
|
-
reject(error);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
// Handle timeout
|
|
321
|
-
if (timeout) {
|
|
322
|
-
timeoutHandle = setTimeout(() => {
|
|
323
|
-
timeoutHandle = null;
|
|
324
|
-
claude.kill('SIGTERM');
|
|
325
|
-
reject(new Error(`Execution timeout after ${timeout}ms`));
|
|
326
|
-
}, timeout);
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
// Add truncation notice if output was limited
|
|
331
|
-
if (outputTruncated) {
|
|
332
|
-
execution.output += '\n\n[Note: Output was truncated due to size limit]';
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Update execution record
|
|
336
|
-
execution.endTime = Date.now();
|
|
337
|
-
execution.exitCode = exitCode;
|
|
338
|
-
|
|
339
|
-
if (exitCode === 0) {
|
|
340
|
-
execution.state = ExecutionState.COMPLETED;
|
|
341
|
-
} else {
|
|
342
|
-
execution.state = ExecutionState.FAILED;
|
|
343
|
-
execution.error = `Claude exited with code ${exitCode}`;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return {
|
|
347
|
-
executionId,
|
|
348
|
-
state: execution.state,
|
|
349
|
-
exitCode,
|
|
350
|
-
};
|
|
351
|
-
} catch (error) {
|
|
352
|
-
// Update execution with error
|
|
353
|
-
execution.state = ExecutionState.FAILED;
|
|
354
|
-
execution.endTime = Date.now();
|
|
355
|
-
execution.error = error.message;
|
|
356
|
-
execution.exitCode = -1;
|
|
357
|
-
|
|
358
|
-
return {
|
|
359
|
-
executionId,
|
|
360
|
-
state: ExecutionState.FAILED,
|
|
361
|
-
error: error.message,
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Get execution status
|
|
368
|
-
* @param {string} executionId - Execution ID
|
|
369
|
-
* @returns {Object|null} Execution details or null if not found
|
|
370
|
-
*/
|
|
371
|
-
export function getExecutionStatus(executionId) {
|
|
372
|
-
const execution = executions.get(executionId);
|
|
373
|
-
|
|
374
|
-
if (!execution) {
|
|
375
|
-
return null;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Calculate duration
|
|
379
|
-
const duration = execution.endTime
|
|
380
|
-
? execution.endTime - execution.startTime
|
|
381
|
-
: Date.now() - execution.startTime;
|
|
382
|
-
|
|
383
|
-
return {
|
|
384
|
-
id: execution.id,
|
|
385
|
-
agent: execution.agent,
|
|
386
|
-
task: execution.task,
|
|
387
|
-
state: execution.state,
|
|
388
|
-
startTime: execution.startTime,
|
|
389
|
-
endTime: execution.endTime,
|
|
390
|
-
duration,
|
|
391
|
-
output: execution.output,
|
|
392
|
-
error: execution.error,
|
|
393
|
-
exitCode: execution.exitCode,
|
|
394
|
-
metadata: execution.agentMetadata,
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* List all executions (with optional filtering)
|
|
400
|
-
* @param {Object} filters - Filter options
|
|
401
|
-
* @param {string} [filters.agent] - Filter by agent name
|
|
402
|
-
* @param {string} [filters.state] - Filter by state
|
|
403
|
-
* @param {number} [filters.limit] - Maximum number of results
|
|
404
|
-
* @returns {Object[]} Array of execution summaries
|
|
405
|
-
*/
|
|
406
|
-
export function listExecutions(filters = {}) {
|
|
407
|
-
const { agent, state, limit = 100 } = filters;
|
|
408
|
-
|
|
409
|
-
let results = Array.from(executions.values());
|
|
410
|
-
|
|
411
|
-
// Apply filters
|
|
412
|
-
if (agent) {
|
|
413
|
-
results = results.filter((e) => e.agent === agent);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if (state) {
|
|
417
|
-
results = results.filter((e) => e.state === state);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Sort by start time (newest first)
|
|
421
|
-
results.sort((a, b) => b.startTime - a.startTime);
|
|
422
|
-
|
|
423
|
-
// Limit results
|
|
424
|
-
results = results.slice(0, limit);
|
|
425
|
-
|
|
426
|
-
// Return summary (no output to keep response small)
|
|
427
|
-
return results.map((e) => ({
|
|
428
|
-
id: e.id,
|
|
429
|
-
agent: e.agent,
|
|
430
|
-
task: e.task,
|
|
431
|
-
state: e.state,
|
|
432
|
-
startTime: e.startTime,
|
|
433
|
-
endTime: e.endTime,
|
|
434
|
-
duration: e.endTime ? e.endTime - e.startTime : Date.now() - e.startTime,
|
|
435
|
-
exitCode: e.exitCode,
|
|
436
|
-
}));
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Clean up old executions (to prevent memory leak)
|
|
441
|
-
* @param {number} maxAge - Maximum age in ms (default: 1 hour)
|
|
442
|
-
* @returns {number} Number of executions cleaned up
|
|
443
|
-
*/
|
|
444
|
-
export function cleanupExecutions(maxAge = 60 * 60 * 1000) {
|
|
445
|
-
const cutoff = Date.now() - maxAge;
|
|
446
|
-
let cleaned = 0;
|
|
447
|
-
|
|
448
|
-
for (const [id, execution] of executions.entries()) {
|
|
449
|
-
if (execution.endTime && execution.endTime < cutoff) {
|
|
450
|
-
executions.delete(id);
|
|
451
|
-
cleaned++;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
return cleaned;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Start periodic cleanup (runs every hour)
|
|
460
|
-
*/
|
|
461
|
-
export function startPeriodicCleanup() {
|
|
462
|
-
setInterval(() => {
|
|
463
|
-
const cleaned = cleanupExecutions();
|
|
464
|
-
if (cleaned > 0) {
|
|
465
|
-
console.log(`Cleaned up ${cleaned} old executions`);
|
|
466
|
-
}
|
|
467
|
-
}, 60 * 60 * 1000); // Run every hour
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
export default {
|
|
471
|
-
executeAgent,
|
|
472
|
-
getExecutionStatus,
|
|
473
|
-
listExecutions,
|
|
474
|
-
cleanupExecutions,
|
|
475
|
-
startPeriodicCleanup,
|
|
476
|
-
ExecutionState,
|
|
477
|
-
};
|