@itz4blitz/agentful 0.5.1 → 1.0.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 +21 -34
- package/bin/cli.js +274 -4
- package/lib/init.js +20 -12
- package/lib/server/executor.js +52 -1
- package/lib/server/index.js +37 -12
- package/package.json +1 -1
- package/version.json +1 -1
package/README.md
CHANGED
|
@@ -4,29 +4,24 @@ A carrot on a stick for Claude Code
|
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://www.npmjs.com/package/@itz4blitz/agentful)
|
|
7
|
-
[](https://github.com/itz4blitz/agentful)
|
|
7
|
+
[](https://github.com/itz4blitz/agentful/actions)
|
|
9
8
|
|
|
10
9
|
## Overview
|
|
11
10
|
|
|
12
|
-
agentful is a
|
|
13
|
-
|
|
14
|
-
**Production Quality**: Enterprise-grade testing (370 tests), 98.26% code coverage, comprehensive error handling, and lifecycle hooks for validation and metrics.
|
|
11
|
+
agentful is a Claude Code configuration that provides structured development through specialized AI agents. It coordinates multiple agents to implement features, write tests, and validate code quality according to a defined product specification.
|
|
15
12
|
|
|
16
13
|
## Web Configurator
|
|
17
14
|
|
|
18
15
|
Configure your agentful installation with an interactive web interface:
|
|
19
16
|
|
|
20
|
-
**[agentful.app
|
|
17
|
+
**[agentful.app](https://agentful.app)**
|
|
21
18
|
|
|
22
19
|
- Visual component selection
|
|
23
|
-
-
|
|
20
|
+
- 2 optimized presets
|
|
24
21
|
- Custom configurations
|
|
25
22
|
- Shareable setup URLs
|
|
26
23
|
- No CLI required
|
|
27
24
|
|
|
28
|
-
See the [Web Configurator Guide](https://agentful.app/getting-started/configurator) for details.
|
|
29
|
-
|
|
30
25
|
## Installation
|
|
31
26
|
|
|
32
27
|
### Default Installation (Recommended)
|
|
@@ -40,7 +35,6 @@ npx @itz4blitz/agentful init
|
|
|
40
35
|
This installs:
|
|
41
36
|
- **8 agents**: orchestrator, architect, backend, frontend, tester, reviewer, fixer, product-analyzer
|
|
42
37
|
- **6 skills**: product-tracking, validation, testing, conversation, product-planning, deployment
|
|
43
|
-
- **Hooks**: health-check (configurable)
|
|
44
38
|
- **Quality gates**: types, tests, coverage, lint, security, dead-code
|
|
45
39
|
|
|
46
40
|
**Tech stack is auto-detected** on first run (TypeScript, Python, React, etc.) - no need to specify.
|
|
@@ -71,17 +65,16 @@ npx @itz4blitz/agentful presets
|
|
|
71
65
|
|
|
72
66
|
### Shareable Configurations
|
|
73
67
|
|
|
74
|
-
Use a configuration from
|
|
68
|
+
Use a configuration from the web configurator:
|
|
75
69
|
|
|
76
70
|
```bash
|
|
77
|
-
npx @itz4blitz/agentful init --config
|
|
71
|
+
npx @itz4blitz/agentful init --config=<shareable-url>
|
|
78
72
|
```
|
|
79
73
|
|
|
80
74
|
**Available Flags:**
|
|
81
75
|
- `--preset=<name>` - Use a preset configuration
|
|
82
76
|
- `--agents=<list>` - Comma-separated list of agents (orchestrator, backend, frontend, tester, reviewer, fixer, architect, product-analyzer)
|
|
83
77
|
- `--skills=<list>` - Comma-separated list of skills (validation, testing, product-tracking, conversation, product-planning, deployment)
|
|
84
|
-
- `--hooks=<list>` - Comma-separated list of hooks (health-check, typescript-validation, notifications, format-on-save)
|
|
85
78
|
- `--gates=<list>` - Comma-separated list of quality gates (types, tests, coverage, lint, security, dead-code)
|
|
86
79
|
|
|
87
80
|
Flags override preset values when both are specified.
|
|
@@ -196,7 +189,7 @@ For projects with existing code:
|
|
|
196
189
|
|
|
197
190
|
### Agent System
|
|
198
191
|
|
|
199
|
-
agentful uses
|
|
192
|
+
agentful uses eight specialized agents:
|
|
200
193
|
|
|
201
194
|
| Agent | Responsibility |
|
|
202
195
|
|-------|---------------|
|
|
@@ -207,15 +200,16 @@ agentful uses seven specialized agents:
|
|
|
207
200
|
| tester | Writes unit, integration, and end-to-end tests |
|
|
208
201
|
| reviewer | Validates code quality, security, and standards |
|
|
209
202
|
| fixer | Resolves validation failures and test errors |
|
|
203
|
+
| product-analyzer | Analyzes product specs for gaps, ambiguities, and readiness scoring |
|
|
210
204
|
|
|
211
205
|
### Quality Gates
|
|
212
206
|
|
|
213
|
-
Code changes are validated against 6
|
|
207
|
+
Code changes are validated against 6 automated quality gates:
|
|
214
208
|
|
|
215
209
|
- Type checking (TypeScript, Flow, etc.)
|
|
216
210
|
- Linting (ESLint, Biome, etc.)
|
|
217
|
-
- Test execution
|
|
218
|
-
- Code coverage
|
|
211
|
+
- Test execution
|
|
212
|
+
- Code coverage
|
|
219
213
|
- Security scanning
|
|
220
214
|
- Dead code detection
|
|
221
215
|
|
|
@@ -230,13 +224,11 @@ Runtime state is stored in `.agentful/` (gitignored, managed by npm package):
|
|
|
230
224
|
- New projects: Starts with declared stack (`confidence: 0.4`)
|
|
231
225
|
- Existing projects: Detected from code (`confidence: 0.8-1.0`)
|
|
232
226
|
- Re-analyzed after first implementation in new projects
|
|
233
|
-
- `last-validation.json` - Latest test/lint results
|
|
234
227
|
- `conversation-history.json` - Session tracking
|
|
235
|
-
- `product-analysis.json` - Readiness analysis (generated by `/agentful-product`)
|
|
236
228
|
|
|
237
229
|
User configuration is stored in `.claude/` (version controlled):
|
|
238
230
|
|
|
239
|
-
- `agents/` - Agent definitions
|
|
231
|
+
- `agents/` - Agent definitions
|
|
240
232
|
- `commands/` - Slash commands
|
|
241
233
|
- `product/` - Product specifications
|
|
242
234
|
- `index.md` - Main product spec (user editable)
|
|
@@ -246,20 +238,23 @@ User configuration is stored in `.claude/` (version controlled):
|
|
|
246
238
|
- `product-tracking/` - Progress calculation and state tracking
|
|
247
239
|
- `product-planning/` - Product specification guidance
|
|
248
240
|
- `validation/` - Quality gate checks and tool detection
|
|
241
|
+
- `testing/` - Test strategy and coverage
|
|
242
|
+
- `deployment/` - Deployment preparation and validation
|
|
249
243
|
- `settings.json` - Project configuration
|
|
250
244
|
|
|
251
|
-
**See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed explanation of file organization.**
|
|
252
|
-
|
|
253
245
|
## Commands
|
|
254
246
|
|
|
255
247
|
| Command | Description |
|
|
256
248
|
|---------|-------------|
|
|
249
|
+
| `/agentful` | Main agentful command - shows help and available commands |
|
|
257
250
|
| `/agentful-product` | Smart product planning: create, analyze, and refine requirements |
|
|
258
251
|
| `/agentful-start` | Start or resume structured development |
|
|
259
252
|
| `/agentful-status` | Display progress and current state |
|
|
260
253
|
| `/agentful-validate` | Run all quality checks |
|
|
261
254
|
| `/agentful-decide` | Answer pending decisions |
|
|
262
255
|
| `/agentful-update` | Smart update mechanism - fetches latest templates and gracefully migrates changes |
|
|
256
|
+
| `/agentful-analyze` | Analyze architecture and generate specialized agents for your tech stack |
|
|
257
|
+
| `/agentful-generate` | Generate specialized agents from architecture analysis |
|
|
263
258
|
|
|
264
259
|
## CI/CD Integration
|
|
265
260
|
|
|
@@ -273,7 +268,7 @@ agentful ci --generate-workflow
|
|
|
273
268
|
agentful ci
|
|
274
269
|
```
|
|
275
270
|
|
|
276
|
-
See `examples/` for sample workflow configurations ([GitHub Actions](examples/github-actions.yml), [GitLab CI](examples/gitlab-ci.yml)
|
|
271
|
+
See `examples/` for sample workflow configurations ([GitHub Actions](examples/github-actions-pipeline.yml), [GitLab CI](examples/gitlab-ci-cd.yml)).
|
|
277
272
|
|
|
278
273
|
## Remote Execution
|
|
279
274
|
|
|
@@ -290,7 +285,7 @@ agentful serve --auth=hmac --secret=$SECRET --https --cert=cert.pem --key=key.pe
|
|
|
290
285
|
agentful serve --auth=none
|
|
291
286
|
```
|
|
292
287
|
|
|
293
|
-
Three authentication modes: **Tailscale** (WireGuard encryption), **HMAC** (signature-based with replay protection), **SSH tunnel** (localhost-only).
|
|
288
|
+
Three authentication modes: **Tailscale** (WireGuard encryption), **HMAC** (signature-based with replay protection), **SSH tunnel** (localhost-only).
|
|
294
289
|
|
|
295
290
|
## Technology Support
|
|
296
291
|
|
|
@@ -311,13 +306,7 @@ agentful detects and adapts to your technology stack automatically:
|
|
|
311
306
|
|
|
312
307
|
## Documentation
|
|
313
308
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
Key guides:
|
|
317
|
-
- [CI/CD Integration](https://agentful.app/ci-integration) - Deploy agents to production
|
|
318
|
-
- [Quick Start](https://agentful.app/getting-started/quick-start) - Get started in 5 minutes
|
|
319
|
-
- [Agent Architecture](https://agentful.app/concepts/architecture) - How agents work
|
|
320
|
-
- [Commands Reference](https://agentful.app/commands) - All available commands
|
|
309
|
+
Documentation: [agentful.app](https://agentful.app)
|
|
321
310
|
|
|
322
311
|
## Project Structure
|
|
323
312
|
|
|
@@ -337,7 +326,6 @@ your-project/
|
|
|
337
326
|
│ ├── completion.json
|
|
338
327
|
│ ├── decisions.json
|
|
339
328
|
│ ├── architecture.json
|
|
340
|
-
│ ├── last-validation.json
|
|
341
329
|
│ └── conversation-history.json
|
|
342
330
|
└── src/ # Source code
|
|
343
331
|
```
|
|
@@ -350,5 +338,4 @@ MIT
|
|
|
350
338
|
|
|
351
339
|
- GitHub: [github.com/itz4blitz/agentful](https://github.com/itz4blitz/agentful)
|
|
352
340
|
- Issues: [github.com/itz4blitz/agentful/issues](https://github.com/itz4blitz/agentful/issues)
|
|
353
|
-
-
|
|
354
|
-
- NPM: [npmjs.com/@itz4blitz/agentful](https://www.npmjs.com/package/@itz4blitz/agentful)
|
|
341
|
+
- NPM: [npmjs.com/package/@itz4blitz/agentful](https://www.npmjs.com/package/@itz4blitz/agentful)
|
package/bin/cli.js
CHANGED
|
@@ -996,12 +996,272 @@ async function remote(args) {
|
|
|
996
996
|
}
|
|
997
997
|
}
|
|
998
998
|
|
|
999
|
+
/**
|
|
1000
|
+
* Get PID file path
|
|
1001
|
+
* @returns {string} Path to PID file
|
|
1002
|
+
*/
|
|
1003
|
+
function getPidFilePath() {
|
|
1004
|
+
return path.join(process.cwd(), '.agentful', 'server.pid');
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Start server in daemon mode
|
|
1009
|
+
* @param {string[]} args - Original args
|
|
1010
|
+
* @param {Object} config - Server configuration
|
|
1011
|
+
*/
|
|
1012
|
+
async function startDaemon(args, config) {
|
|
1013
|
+
const { spawn } = await import('child_process');
|
|
1014
|
+
|
|
1015
|
+
// Check if daemon is already running
|
|
1016
|
+
const pidFile = getPidFilePath();
|
|
1017
|
+
if (fs.existsSync(pidFile)) {
|
|
1018
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
1019
|
+
|
|
1020
|
+
// Check if process is still running
|
|
1021
|
+
try {
|
|
1022
|
+
process.kill(pid, 0); // Signal 0 checks if process exists
|
|
1023
|
+
log(colors.yellow, 'Server is already running');
|
|
1024
|
+
log(colors.dim, `PID: ${pid}`);
|
|
1025
|
+
console.log('');
|
|
1026
|
+
log(colors.dim, 'To stop: agentful serve --stop');
|
|
1027
|
+
log(colors.dim, 'To check status: agentful serve --status');
|
|
1028
|
+
process.exit(1);
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
// Process doesn't exist, clean up stale PID file
|
|
1031
|
+
fs.unlinkSync(pidFile);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Ensure .agentful directory exists
|
|
1036
|
+
const agentfulDir = path.join(process.cwd(), '.agentful');
|
|
1037
|
+
if (!fs.existsSync(agentfulDir)) {
|
|
1038
|
+
fs.mkdirSync(agentfulDir, { recursive: true });
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Prepare args for child process (remove --daemon flag)
|
|
1042
|
+
const childArgs = args.filter(arg => !arg.startsWith('--daemon') && arg !== '-d');
|
|
1043
|
+
|
|
1044
|
+
// Spawn detached child process
|
|
1045
|
+
const child = spawn(
|
|
1046
|
+
process.argv[0], // node executable
|
|
1047
|
+
[process.argv[1], 'serve', ...childArgs], // script path and args
|
|
1048
|
+
{
|
|
1049
|
+
detached: true,
|
|
1050
|
+
stdio: 'ignore',
|
|
1051
|
+
cwd: process.cwd(),
|
|
1052
|
+
env: {
|
|
1053
|
+
...process.env,
|
|
1054
|
+
AGENTFUL_DAEMON: '1' // Flag to indicate we're running as daemon
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
);
|
|
1058
|
+
|
|
1059
|
+
// Write PID file
|
|
1060
|
+
fs.writeFileSync(pidFile, child.pid.toString(), 'utf-8');
|
|
1061
|
+
|
|
1062
|
+
// Unref to allow parent to exit
|
|
1063
|
+
child.unref();
|
|
1064
|
+
|
|
1065
|
+
// Show success message
|
|
1066
|
+
log(colors.green, `Server started in background (PID: ${child.pid})`);
|
|
1067
|
+
console.log('');
|
|
1068
|
+
log(colors.dim, `PID file: ${pidFile}`);
|
|
1069
|
+
log(colors.dim, `Port: ${config.port}`);
|
|
1070
|
+
log(colors.dim, `Auth: ${config.auth}`);
|
|
1071
|
+
console.log('');
|
|
1072
|
+
log(colors.dim, 'Commands:');
|
|
1073
|
+
log(colors.dim, ' agentful serve --stop Stop the daemon');
|
|
1074
|
+
log(colors.dim, ' agentful serve --status Check daemon status');
|
|
1075
|
+
console.log('');
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Stop daemon server
|
|
1080
|
+
*/
|
|
1081
|
+
async function stopDaemon() {
|
|
1082
|
+
const pidFile = getPidFilePath();
|
|
1083
|
+
|
|
1084
|
+
if (!fs.existsSync(pidFile)) {
|
|
1085
|
+
log(colors.yellow, 'No daemon server running');
|
|
1086
|
+
log(colors.dim, 'PID file not found');
|
|
1087
|
+
process.exit(1);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
1091
|
+
|
|
1092
|
+
// Try to kill the process
|
|
1093
|
+
try {
|
|
1094
|
+
process.kill(pid, 'SIGTERM');
|
|
1095
|
+
|
|
1096
|
+
// Wait a moment for graceful shutdown
|
|
1097
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1098
|
+
|
|
1099
|
+
// Check if process is still running
|
|
1100
|
+
try {
|
|
1101
|
+
process.kill(pid, 0);
|
|
1102
|
+
// Still running, force kill
|
|
1103
|
+
log(colors.yellow, 'Graceful shutdown failed, forcing...');
|
|
1104
|
+
process.kill(pid, 'SIGKILL');
|
|
1105
|
+
} catch {
|
|
1106
|
+
// Process stopped successfully
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Remove PID file
|
|
1110
|
+
fs.unlinkSync(pidFile);
|
|
1111
|
+
|
|
1112
|
+
log(colors.green, `Server stopped (PID: ${pid})`);
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
if (error.code === 'ESRCH') {
|
|
1115
|
+
// Process doesn't exist
|
|
1116
|
+
log(colors.yellow, 'Server process not found (stale PID file)');
|
|
1117
|
+
fs.unlinkSync(pidFile);
|
|
1118
|
+
} else if (error.code === 'EPERM') {
|
|
1119
|
+
log(colors.red, `Permission denied to kill process ${pid}`);
|
|
1120
|
+
log(colors.dim, 'Try: sudo agentful serve --stop');
|
|
1121
|
+
process.exit(1);
|
|
1122
|
+
} else {
|
|
1123
|
+
log(colors.red, `Failed to stop server: ${error.message}`);
|
|
1124
|
+
process.exit(1);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Check daemon server status
|
|
1131
|
+
*/
|
|
1132
|
+
async function checkDaemonStatus() {
|
|
1133
|
+
const pidFile = getPidFilePath();
|
|
1134
|
+
|
|
1135
|
+
if (!fs.existsSync(pidFile)) {
|
|
1136
|
+
log(colors.yellow, 'No daemon server running');
|
|
1137
|
+
log(colors.dim, 'PID file not found');
|
|
1138
|
+
console.log('');
|
|
1139
|
+
log(colors.dim, 'Start daemon: agentful serve --daemon');
|
|
1140
|
+
process.exit(1);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
1144
|
+
|
|
1145
|
+
// Check if process is running
|
|
1146
|
+
try {
|
|
1147
|
+
process.kill(pid, 0); // Signal 0 just checks if process exists
|
|
1148
|
+
|
|
1149
|
+
log(colors.green, 'Server is running');
|
|
1150
|
+
console.log('');
|
|
1151
|
+
log(colors.dim, `PID: ${pid}`);
|
|
1152
|
+
log(colors.dim, `PID file: ${pidFile}`);
|
|
1153
|
+
|
|
1154
|
+
// Try to get more info from /proc (Linux/macOS)
|
|
1155
|
+
try {
|
|
1156
|
+
const { execSync } = await import('child_process');
|
|
1157
|
+
const psOutput = execSync(`ps -p ${pid} -o comm,etime,rss`, { encoding: 'utf-8' });
|
|
1158
|
+
const lines = psOutput.trim().split('\n');
|
|
1159
|
+
if (lines.length > 1) {
|
|
1160
|
+
const [cmd, etime, rss] = lines[1].trim().split(/\s+/);
|
|
1161
|
+
console.log('');
|
|
1162
|
+
log(colors.dim, `Uptime: ${etime}`);
|
|
1163
|
+
log(colors.dim, `Memory: ${Math.round(parseInt(rss) / 1024)} MB`);
|
|
1164
|
+
}
|
|
1165
|
+
} catch {
|
|
1166
|
+
// ps command failed, skip detailed info
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
console.log('');
|
|
1170
|
+
log(colors.dim, 'Commands:');
|
|
1171
|
+
log(colors.dim, ' agentful serve --stop Stop the daemon');
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
if (error.code === 'ESRCH') {
|
|
1174
|
+
log(colors.yellow, 'Server not running (stale PID file)');
|
|
1175
|
+
log(colors.dim, `PID file exists but process ${pid} not found`);
|
|
1176
|
+
console.log('');
|
|
1177
|
+
log(colors.dim, 'Clean up: rm .agentful/server.pid');
|
|
1178
|
+
process.exit(1);
|
|
1179
|
+
} else {
|
|
1180
|
+
log(colors.red, `Failed to check status: ${error.message}`);
|
|
1181
|
+
process.exit(1);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
999
1186
|
/**
|
|
1000
1187
|
* Serve command - Start remote execution server
|
|
1001
1188
|
*/
|
|
1002
1189
|
async function serve(args) {
|
|
1003
1190
|
const flags = parseFlags(args);
|
|
1004
1191
|
|
|
1192
|
+
// Handle --stop subcommand
|
|
1193
|
+
if (flags.stop) {
|
|
1194
|
+
return await stopDaemon();
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// Handle --status subcommand
|
|
1198
|
+
if (flags.status) {
|
|
1199
|
+
return await checkDaemonStatus();
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Handle --help flag first
|
|
1203
|
+
if (flags.help || flags.h) {
|
|
1204
|
+
showBanner();
|
|
1205
|
+
log(colors.bright, 'Agentful Remote Execution Server');
|
|
1206
|
+
console.log('');
|
|
1207
|
+
log(colors.dim, 'Start a secure HTTP server for remote agent execution.');
|
|
1208
|
+
console.log('');
|
|
1209
|
+
log(colors.bright, 'USAGE:');
|
|
1210
|
+
console.log(` ${colors.green}agentful serve${colors.reset} ${colors.dim}[options]${colors.reset}`);
|
|
1211
|
+
console.log('');
|
|
1212
|
+
log(colors.bright, 'AUTHENTICATION MODES:');
|
|
1213
|
+
console.log(` ${colors.cyan}--auth=tailscale${colors.reset} ${colors.dim}(default) Tailscale network only${colors.reset}`);
|
|
1214
|
+
console.log(` ${colors.cyan}--auth=hmac${colors.reset} ${colors.dim}HMAC signature authentication (requires --secret)${colors.reset}`);
|
|
1215
|
+
console.log(` ${colors.cyan}--auth=none${colors.reset} ${colors.dim}No authentication (binds to all interfaces, use with SSH tunnel)${colors.reset}`);
|
|
1216
|
+
console.log('');
|
|
1217
|
+
log(colors.bright, 'OPTIONS:');
|
|
1218
|
+
console.log(` ${colors.yellow}--port=<number>${colors.reset} ${colors.dim}Server port (default: 3000)${colors.reset}`);
|
|
1219
|
+
console.log(` ${colors.yellow}--secret=<key>${colors.reset} ${colors.dim}HMAC secret key (required for --auth=hmac)${colors.reset}`);
|
|
1220
|
+
console.log(` ${colors.yellow}--https${colors.reset} ${colors.dim}Enable HTTPS (requires --cert and --key)${colors.reset}`);
|
|
1221
|
+
console.log(` ${colors.yellow}--cert=<path>${colors.reset} ${colors.dim}SSL certificate file path${colors.reset}`);
|
|
1222
|
+
console.log(` ${colors.yellow}--key=<path>${colors.reset} ${colors.dim}SSL private key file path${colors.reset}`);
|
|
1223
|
+
console.log(` ${colors.yellow}--daemon, -d${colors.reset} ${colors.dim}Run server in background (daemon mode)${colors.reset}`);
|
|
1224
|
+
console.log(` ${colors.yellow}--stop${colors.reset} ${colors.dim}Stop background server${colors.reset}`);
|
|
1225
|
+
console.log(` ${colors.yellow}--status${colors.reset} ${colors.dim}Check background server status${colors.reset}`);
|
|
1226
|
+
console.log(` ${colors.yellow}--help, -h${colors.reset} ${colors.dim}Show this help message${colors.reset}`);
|
|
1227
|
+
console.log('');
|
|
1228
|
+
log(colors.bright, 'EXAMPLES:');
|
|
1229
|
+
console.log('');
|
|
1230
|
+
log(colors.dim, ' # Start server with Tailscale auth (default)');
|
|
1231
|
+
console.log(` ${colors.green}agentful serve${colors.reset}`);
|
|
1232
|
+
console.log('');
|
|
1233
|
+
log(colors.dim, ' # Start server with HMAC authentication');
|
|
1234
|
+
console.log(` ${colors.green}agentful serve --auth=hmac --secret=your-secret-key${colors.reset}`);
|
|
1235
|
+
console.log('');
|
|
1236
|
+
log(colors.dim, ' # Start HTTPS server with HMAC auth');
|
|
1237
|
+
console.log(` ${colors.green}agentful serve --auth=hmac --secret=key --https --cert=cert.pem --key=key.pem${colors.reset}`);
|
|
1238
|
+
console.log('');
|
|
1239
|
+
log(colors.dim, ' # Start server without auth (public access, use SSH tunnel)');
|
|
1240
|
+
console.log(` ${colors.green}agentful serve --auth=none --port=3737${colors.reset}`);
|
|
1241
|
+
console.log('');
|
|
1242
|
+
log(colors.dim, ' # Start server in background (daemon mode)');
|
|
1243
|
+
console.log(` ${colors.green}agentful serve --daemon${colors.reset}`);
|
|
1244
|
+
console.log('');
|
|
1245
|
+
log(colors.dim, ' # Check daemon status');
|
|
1246
|
+
console.log(` ${colors.green}agentful serve --status${colors.reset}`);
|
|
1247
|
+
console.log('');
|
|
1248
|
+
log(colors.dim, ' # Stop daemon');
|
|
1249
|
+
console.log(` ${colors.green}agentful serve --stop${colors.reset}`);
|
|
1250
|
+
console.log('');
|
|
1251
|
+
log(colors.dim, ' # Generate HMAC secret');
|
|
1252
|
+
console.log(` ${colors.green}openssl rand -hex 32${colors.reset}`);
|
|
1253
|
+
console.log('');
|
|
1254
|
+
log(colors.dim, ' # Generate self-signed certificate');
|
|
1255
|
+
console.log(` ${colors.green}openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes${colors.reset}`);
|
|
1256
|
+
console.log('');
|
|
1257
|
+
log(colors.bright, 'SECURITY NOTES:');
|
|
1258
|
+
console.log(` ${colors.yellow}Tailscale mode:${colors.reset} Binds to 0.0.0.0, relies on Tailscale network isolation`);
|
|
1259
|
+
console.log(` ${colors.yellow}HMAC mode:${colors.reset} Binds to 0.0.0.0, uses cryptographic signatures (recommended for public networks)`);
|
|
1260
|
+
console.log(` ${colors.yellow}None mode:${colors.reset} Binds to 0.0.0.0, no authentication (use SSH tunnel: ssh -L 3000:localhost:3000 user@host)`);
|
|
1261
|
+
console.log('');
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1005
1265
|
// Parse configuration
|
|
1006
1266
|
const config = {
|
|
1007
1267
|
auth: flags.auth || 'tailscale',
|
|
@@ -1013,6 +1273,9 @@ async function serve(args) {
|
|
|
1013
1273
|
projectRoot: process.cwd(),
|
|
1014
1274
|
};
|
|
1015
1275
|
|
|
1276
|
+
// Check if --daemon flag is set
|
|
1277
|
+
const isDaemon = flags.daemon || flags.d;
|
|
1278
|
+
|
|
1016
1279
|
// Validate auth mode
|
|
1017
1280
|
const validAuthModes = ['tailscale', 'hmac', 'none'];
|
|
1018
1281
|
if (!validAuthModes.includes(config.auth)) {
|
|
@@ -1027,8 +1290,15 @@ async function serve(args) {
|
|
|
1027
1290
|
process.exit(1);
|
|
1028
1291
|
}
|
|
1029
1292
|
|
|
1030
|
-
//
|
|
1031
|
-
|
|
1293
|
+
// If daemon mode, fork the process
|
|
1294
|
+
if (isDaemon) {
|
|
1295
|
+
return await startDaemon(args, config);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// Show configuration (skip banner if running as daemon child)
|
|
1299
|
+
if (!process.env.AGENTFUL_DAEMON) {
|
|
1300
|
+
showBanner();
|
|
1301
|
+
}
|
|
1032
1302
|
log(colors.bright, 'Starting Agentful Server');
|
|
1033
1303
|
console.log('');
|
|
1034
1304
|
log(colors.dim, `Authentication: ${config.auth}`);
|
|
@@ -1036,8 +1306,8 @@ async function serve(args) {
|
|
|
1036
1306
|
log(colors.dim, `HTTPS: ${config.https ? 'enabled' : 'disabled'}`);
|
|
1037
1307
|
|
|
1038
1308
|
if (config.auth === 'none') {
|
|
1039
|
-
log(colors.yellow, 'Warning: Server
|
|
1040
|
-
log(colors.dim, 'Use SSH tunnel for remote access: ssh -L 3000:localhost:3000 user@host');
|
|
1309
|
+
log(colors.yellow, 'Warning: Server running with no authentication (binds to all interfaces)');
|
|
1310
|
+
log(colors.dim, 'Recommended: Use SSH tunnel for remote access: ssh -L 3000:localhost:3000 user@host');
|
|
1041
1311
|
}
|
|
1042
1312
|
|
|
1043
1313
|
if (config.auth === 'hmac' && !config.secret) {
|
package/lib/init.js
CHANGED
|
@@ -91,32 +91,40 @@ export async function initProject(targetDir, config = null) {
|
|
|
91
91
|
await fs.mkdir(agentfulDir, { recursive: true });
|
|
92
92
|
createdFiles.push('.agentful/');
|
|
93
93
|
|
|
94
|
-
// Create state.json
|
|
94
|
+
// Create state.json (runtime state for orchestrator)
|
|
95
95
|
const stateFile = path.join(agentfulDir, 'state.json');
|
|
96
96
|
const initialState = {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
version: '1.0',
|
|
98
|
+
current_task: null,
|
|
99
|
+
current_phase: 'idle',
|
|
100
|
+
iterations: 0,
|
|
101
|
+
last_updated: new Date().toISOString(),
|
|
102
|
+
blocked_on: []
|
|
101
103
|
};
|
|
102
104
|
await fs.writeFile(stateFile, JSON.stringify(initialState, null, 2));
|
|
103
105
|
createdFiles.push('.agentful/state.json');
|
|
104
106
|
|
|
105
|
-
// Create completion.json
|
|
107
|
+
// Create completion.json (feature progress tracking)
|
|
106
108
|
const completionFile = path.join(agentfulDir, 'completion.json');
|
|
107
109
|
const initialCompletion = {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
features: {},
|
|
111
|
+
gates: {
|
|
112
|
+
tests_passing: false,
|
|
113
|
+
no_type_errors: false,
|
|
114
|
+
no_dead_code: false,
|
|
115
|
+
coverage_80: false,
|
|
116
|
+
security_clean: false
|
|
117
|
+
},
|
|
118
|
+
overall_progress: 0
|
|
111
119
|
};
|
|
112
120
|
await fs.writeFile(completionFile, JSON.stringify(initialCompletion, null, 2));
|
|
113
121
|
createdFiles.push('.agentful/completion.json');
|
|
114
122
|
|
|
115
|
-
// Create decisions.json
|
|
123
|
+
// Create decisions.json (pending and resolved decisions)
|
|
116
124
|
const decisionsFile = path.join(agentfulDir, 'decisions.json');
|
|
117
125
|
const initialDecisions = {
|
|
118
|
-
|
|
119
|
-
|
|
126
|
+
pending: [],
|
|
127
|
+
resolved: []
|
|
120
128
|
};
|
|
121
129
|
await fs.writeFile(decisionsFile, JSON.stringify(initialDecisions, null, 2));
|
|
122
130
|
createdFiles.push('.agentful/decisions.json');
|
package/lib/server/executor.js
CHANGED
|
@@ -133,13 +133,15 @@ ${agent.instructions}
|
|
|
133
133
|
* @param {string} [options.projectRoot] - Project root directory
|
|
134
134
|
* @param {number} [options.timeout] - Execution timeout in ms
|
|
135
135
|
* @param {Object} [options.env] - Additional environment variables
|
|
136
|
-
* @
|
|
136
|
+
* @param {boolean} [options.async=false] - If true, return immediately with executionId
|
|
137
|
+
* @returns {Promise<Object>} Execution result (or just executionId if async=true)
|
|
137
138
|
*/
|
|
138
139
|
export async function executeAgent(agentName, task, options = {}) {
|
|
139
140
|
const {
|
|
140
141
|
projectRoot = process.cwd(),
|
|
141
142
|
timeout = 10 * 60 * 1000, // 10 minutes default
|
|
142
143
|
env = {},
|
|
144
|
+
async = false, // New option for non-blocking execution
|
|
143
145
|
} = options;
|
|
144
146
|
|
|
145
147
|
// Validate agent name to prevent path traversal
|
|
@@ -176,6 +178,55 @@ export async function executeAgent(agentName, task, options = {}) {
|
|
|
176
178
|
|
|
177
179
|
executions.set(executionId, execution);
|
|
178
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
|
+
|
|
179
230
|
try {
|
|
180
231
|
// Load agent definition
|
|
181
232
|
const agent = await loadAgentDefinition(agentName, projectRoot);
|
package/lib/server/index.js
CHANGED
|
@@ -243,24 +243,28 @@ export function createServer(config = {}) {
|
|
|
243
243
|
);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
//
|
|
247
|
-
|
|
246
|
+
// Log agent execution request
|
|
247
|
+
console.log(`[${new Date().toISOString()}] Executing agent: ${body.agent}`);
|
|
248
|
+
console.log(`[${new Date().toISOString()}] Task: ${body.task}`);
|
|
249
|
+
|
|
250
|
+
// Start agent execution in background (async mode)
|
|
251
|
+
const executionTimeout = body.timeout || 10 * 60 * 1000; // Default 10 min for execution
|
|
252
|
+
|
|
253
|
+
const result = await executeAgent(body.agent, body.task, {
|
|
248
254
|
projectRoot,
|
|
249
|
-
timeout:
|
|
255
|
+
timeout: executionTimeout,
|
|
250
256
|
env: body.env,
|
|
257
|
+
async: true, // Return immediately with executionId
|
|
251
258
|
});
|
|
252
259
|
|
|
253
|
-
|
|
254
|
-
result.then(() => {}).catch(() => {}); // Prevent unhandled rejection
|
|
255
|
-
|
|
256
|
-
const executionId = (await result).executionId;
|
|
260
|
+
console.log(`[${new Date().toISOString()}] Execution started: ${result.executionId}`);
|
|
257
261
|
|
|
258
262
|
res.writeHead(202, { 'Content-Type': 'application/json' });
|
|
259
263
|
res.end(
|
|
260
264
|
JSON.stringify({
|
|
261
|
-
executionId,
|
|
265
|
+
executionId: result.executionId,
|
|
262
266
|
message: 'Agent execution started',
|
|
263
|
-
statusUrl: `/status/${executionId}`,
|
|
267
|
+
statusUrl: `/status/${result.executionId}`,
|
|
264
268
|
})
|
|
265
269
|
);
|
|
266
270
|
} catch (error) {
|
|
@@ -315,6 +319,13 @@ export function createServer(config = {}) {
|
|
|
315
319
|
|
|
316
320
|
// Request handler
|
|
317
321
|
const requestHandler = (req, res) => {
|
|
322
|
+
const startTime = Date.now();
|
|
323
|
+
const clientIP = req.socket.remoteAddress;
|
|
324
|
+
|
|
325
|
+
// Log incoming request
|
|
326
|
+
const timestamp = new Date().toISOString();
|
|
327
|
+
console.log(`[${timestamp}] ${req.method} ${req.url} from ${clientIP}`);
|
|
328
|
+
|
|
318
329
|
// Add CORS headers (restricted by default)
|
|
319
330
|
if (corsOrigin) {
|
|
320
331
|
res.setHeader('Access-Control-Allow-Origin', corsOrigin);
|
|
@@ -328,14 +339,27 @@ export function createServer(config = {}) {
|
|
|
328
339
|
// Handle preflight
|
|
329
340
|
if (req.method === 'OPTIONS') {
|
|
330
341
|
res.writeHead(204);
|
|
342
|
+
const duration = Date.now() - startTime;
|
|
343
|
+
console.log(`[${new Date().toISOString()}] Response sent: 204 (${duration}ms)`);
|
|
331
344
|
return res.end();
|
|
332
345
|
}
|
|
333
346
|
|
|
334
347
|
// Apply rate limiting
|
|
335
348
|
if (!checkRateLimit(req, res)) {
|
|
349
|
+
const duration = Date.now() - startTime;
|
|
350
|
+
console.log(`[${new Date().toISOString()}] Response sent: 429 Rate Limited (${duration}ms)`);
|
|
336
351
|
return; // Rate limit exceeded, response already sent
|
|
337
352
|
}
|
|
338
353
|
|
|
354
|
+
// Intercept res.end to log responses
|
|
355
|
+
const originalEnd = res.end;
|
|
356
|
+
res.end = function(...args) {
|
|
357
|
+
const duration = Date.now() - startTime;
|
|
358
|
+
const statusCode = res.statusCode;
|
|
359
|
+
console.log(`[${new Date().toISOString()}] Response sent: ${statusCode} (${duration}ms)`);
|
|
360
|
+
originalEnd.apply(res, args);
|
|
361
|
+
};
|
|
362
|
+
|
|
339
363
|
// Capture raw body (needed for HMAC verification)
|
|
340
364
|
captureRawBody(req, res, () => {
|
|
341
365
|
// Apply authentication (except for /health)
|
|
@@ -384,8 +408,9 @@ export function createServer(config = {}) {
|
|
|
384
408
|
server = http.createServer(requestHandler);
|
|
385
409
|
}
|
|
386
410
|
|
|
387
|
-
//
|
|
388
|
-
|
|
411
|
+
// Always bind to all interfaces (0.0.0.0)
|
|
412
|
+
// Security is enforced through authentication middleware, not binding address
|
|
413
|
+
const host = '0.0.0.0';
|
|
389
414
|
|
|
390
415
|
return {
|
|
391
416
|
start: () => {
|
|
@@ -400,7 +425,7 @@ export function createServer(config = {}) {
|
|
|
400
425
|
console.log(`Authentication mode: ${auth}`);
|
|
401
426
|
|
|
402
427
|
if (auth === 'none') {
|
|
403
|
-
console.log('
|
|
428
|
+
console.log('Warning: No authentication enabled - use SSH tunnel for secure remote access');
|
|
404
429
|
}
|
|
405
430
|
|
|
406
431
|
// Start periodic cleanup
|
package/package.json
CHANGED
package/version.json
CHANGED