@itz4blitz/agentful 1.0.0 → 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/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
- // Show configuration
1031
- showBanner();
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 will only accept localhost connections');
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) {
@@ -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
- * @returns {Promise<Object>} Execution result
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);
@@ -243,24 +243,28 @@ export function createServer(config = {}) {
243
243
  );
244
244
  }
245
245
 
246
- // Execute agent (non-blocking)
247
- const result = executeAgent(body.agent, body.task, {
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: body.timeout,
255
+ timeout: executionTimeout,
250
256
  env: body.env,
257
+ async: true, // Return immediately with executionId
251
258
  });
252
259
 
253
- // Don't await - return immediately with execution ID
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
- // Determine bind address based on auth mode
388
- const host = auth === 'none' ? '127.0.0.1' : '0.0.0.0';
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('Server is bound to localhost only - use SSH tunnel for remote access');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itz4blitz/agentful",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Human-in-the-loop development kit for Claude Code with smart product analysis and natural conversation",
5
5
  "type": "module",
6
6
  "bin": {
package/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.5.1"
2
+ "version": "1.0.0"
3
3
  }