@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.
Files changed (93) hide show
  1. package/README.md +133 -5
  2. package/bin/cli.js +1031 -47
  3. package/bin/hooks/README.md +338 -82
  4. package/bin/hooks/analyze-trigger.js +69 -0
  5. package/bin/hooks/block-random-docs.js +77 -0
  6. package/bin/hooks/health-check.js +153 -0
  7. package/bin/hooks/post-agent.js +101 -0
  8. package/bin/hooks/post-feature.js +227 -0
  9. package/bin/hooks/pre-agent.js +118 -0
  10. package/bin/hooks/pre-feature.js +138 -0
  11. package/lib/VALIDATION_README.md +455 -0
  12. package/lib/atomic.js +350 -0
  13. package/lib/ci/claude-action-integration.js +641 -0
  14. package/lib/ci/index.js +10 -0
  15. package/lib/core/CLAUDE_EXECUTOR.md +371 -0
  16. package/lib/core/README.md +321 -0
  17. package/lib/core/analyzer.js +497 -0
  18. package/lib/core/claude-executor.example.js +210 -0
  19. package/lib/core/claude-executor.js +1046 -0
  20. package/lib/core/cli.js +141 -0
  21. package/lib/core/detectors/conventions.js +342 -0
  22. package/lib/core/detectors/framework.js +276 -0
  23. package/lib/core/detectors/index.js +15 -0
  24. package/lib/core/detectors/language.js +199 -0
  25. package/lib/core/detectors/patterns.js +356 -0
  26. package/lib/core/generator.js +626 -0
  27. package/lib/core/index.js +9 -0
  28. package/lib/core/output-parser.example.js +250 -0
  29. package/lib/core/output-parser.js +458 -0
  30. package/lib/core/storage.js +515 -0
  31. package/lib/core/templates.js +556 -0
  32. package/lib/index.js +32 -0
  33. package/lib/init.js +232 -9
  34. package/lib/pipeline/cli.js +423 -0
  35. package/lib/pipeline/engine.js +928 -0
  36. package/lib/pipeline/executor.js +440 -0
  37. package/lib/pipeline/index.js +33 -0
  38. package/lib/pipeline/integrations.js +559 -0
  39. package/lib/pipeline/schemas.js +288 -0
  40. package/lib/presets.js +207 -0
  41. package/lib/remote/client.js +361 -0
  42. package/lib/server/auth.js +286 -0
  43. package/lib/server/client-example.js +190 -0
  44. package/lib/server/executor.js +426 -0
  45. package/lib/server/index.js +469 -0
  46. package/lib/update-helpers.js +505 -0
  47. package/lib/validation.js +460 -0
  48. package/package.json +19 -2
  49. package/template/.claude/agents/architect.md +260 -0
  50. package/template/.claude/agents/backend.md +203 -0
  51. package/template/.claude/agents/fixer.md +244 -0
  52. package/template/.claude/agents/frontend.md +232 -0
  53. package/template/.claude/agents/orchestrator.md +528 -0
  54. package/template/.claude/agents/product-analyzer.md +1130 -0
  55. package/template/.claude/agents/reviewer.md +229 -0
  56. package/template/.claude/agents/tester.md +242 -0
  57. package/{.claude → template/.claude}/commands/agentful-analyze.md +151 -43
  58. package/template/.claude/commands/agentful-decide.md +470 -0
  59. package/{.claude → template/.claude}/commands/agentful-product.md +89 -5
  60. package/template/.claude/commands/agentful-start.md +432 -0
  61. package/{.claude → template/.claude}/commands/agentful-status.md +88 -3
  62. package/template/.claude/commands/agentful-update.md +402 -0
  63. package/template/.claude/commands/agentful-validate.md +369 -0
  64. package/{.claude → template/.claude}/commands/agentful.md +110 -183
  65. package/template/.claude/product/EXAMPLES.md +167 -0
  66. package/{.claude → template/.claude}/settings.json +9 -13
  67. package/{.claude → template/.claude}/skills/conversation/SKILL.md +13 -7
  68. package/template/.claude/skills/deployment/SKILL.md +116 -0
  69. package/template/.claude/skills/product-planning/SKILL.md +463 -0
  70. package/template/.claude/skills/testing/SKILL.md +228 -0
  71. package/template/.claude/skills/validation/SKILL.md +650 -0
  72. package/template/CLAUDE.md +73 -5
  73. package/template/bin/hooks/block-random-docs.js +121 -0
  74. package/version.json +1 -1
  75. package/.claude/agents/architect.md +0 -524
  76. package/.claude/agents/backend.md +0 -315
  77. package/.claude/agents/fixer.md +0 -263
  78. package/.claude/agents/frontend.md +0 -274
  79. package/.claude/agents/orchestrator.md +0 -283
  80. package/.claude/agents/product-analyzer.md +0 -792
  81. package/.claude/agents/reviewer.md +0 -332
  82. package/.claude/agents/tester.md +0 -410
  83. package/.claude/commands/agentful-decide.md +0 -214
  84. package/.claude/commands/agentful-start.md +0 -182
  85. package/.claude/commands/agentful-validate.md +0 -127
  86. package/.claude/product/EXAMPLES.md +0 -610
  87. package/.claude/product/README.md +0 -326
  88. package/.claude/skills/validation/SKILL.md +0 -271
  89. package/bin/hooks/analyze-trigger.sh +0 -57
  90. package/bin/hooks/health-check.sh +0 -36
  91. /package/{.claude → template/.claude}/commands/agentful-generate.md +0 -0
  92. /package/{.claude → template/.claude}/product/index.md +0 -0
  93. /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
+ };