@supaku/agentfactory-cli 0.1.1 → 0.2.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.
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AgentFactory Worker Fleet Manager
4
+ *
5
+ * Spawns and manages multiple worker processes for parallel agent execution.
6
+ * Each worker runs as a separate process with its own resources.
7
+ *
8
+ * Usage:
9
+ * af-worker-fleet [options]
10
+ *
11
+ * Options:
12
+ * -w, --workers <n> Number of worker processes (default: CPU cores / 2)
13
+ * -c, --capacity <n> Agents per worker (default: 3)
14
+ * --dry-run Show configuration without starting workers
15
+ *
16
+ * Environment (loaded from .env.local in CWD):
17
+ * WORKER_FLEET_SIZE Number of workers (override)
18
+ * WORKER_CAPACITY Agents per worker (override)
19
+ * WORKER_API_URL Coordinator API URL (required)
20
+ * WORKER_API_KEY API key for authentication (required)
21
+ */
22
+ import { spawn } from 'child_process';
23
+ import os from 'os';
24
+ import path from 'path';
25
+ import { fileURLToPath } from 'url';
26
+ import { config as loadEnv } from 'dotenv';
27
+ // Load environment variables from .env.local in CWD
28
+ loadEnv({ path: path.resolve(process.cwd(), '.env.local') });
29
+ // Resolve the directory of this script (for finding worker.js)
30
+ const __filename = fileURLToPath(import.meta.url);
31
+ const __dirname = path.dirname(__filename);
32
+ // ANSI colors
33
+ const colors = {
34
+ reset: '\x1b[0m',
35
+ red: '\x1b[31m',
36
+ green: '\x1b[32m',
37
+ yellow: '\x1b[33m',
38
+ blue: '\x1b[34m',
39
+ magenta: '\x1b[35m',
40
+ cyan: '\x1b[36m',
41
+ gray: '\x1b[90m',
42
+ };
43
+ // Worker color cycling
44
+ const workerColors = [
45
+ colors.cyan,
46
+ colors.magenta,
47
+ colors.yellow,
48
+ colors.green,
49
+ colors.blue,
50
+ ];
51
+ function parseArgs() {
52
+ const args = process.argv.slice(2);
53
+ let workers = parseInt(process.env.WORKER_FLEET_SIZE ?? '0', 10) || Math.max(1, Math.floor(os.cpus().length / 2));
54
+ let capacity = parseInt(process.env.WORKER_CAPACITY ?? '3', 10);
55
+ let dryRun = false;
56
+ for (let i = 0; i < args.length; i++) {
57
+ if (args[i] === '--workers' || args[i] === '-w') {
58
+ workers = parseInt(args[++i], 10);
59
+ }
60
+ else if (args[i] === '--capacity' || args[i] === '-c') {
61
+ capacity = parseInt(args[++i], 10);
62
+ }
63
+ else if (args[i] === '--dry-run') {
64
+ dryRun = true;
65
+ }
66
+ else if (args[i] === '--help' || args[i] === '-h') {
67
+ printHelp();
68
+ process.exit(0);
69
+ }
70
+ }
71
+ return { workers, capacity, dryRun };
72
+ }
73
+ function printHelp() {
74
+ console.log(`
75
+ ${colors.cyan}AgentFactory Worker Fleet Manager${colors.reset}
76
+ Spawns and manages multiple worker processes for parallel agent execution.
77
+
78
+ ${colors.yellow}Usage:${colors.reset}
79
+ af-worker-fleet [options]
80
+
81
+ ${colors.yellow}Options:${colors.reset}
82
+ -w, --workers <n> Number of worker processes (default: CPU cores / 2)
83
+ -c, --capacity <n> Agents per worker (default: 3)
84
+ --dry-run Show configuration without starting workers
85
+ -h, --help Show this help message
86
+
87
+ ${colors.yellow}Examples:${colors.reset}
88
+ af-worker-fleet # Auto-detect optimal settings
89
+ af-worker-fleet -w 8 -c 5 # 8 workers x 5 agents = 40 concurrent
90
+ af-worker-fleet --workers 16 # 16 workers with default capacity
91
+
92
+ ${colors.yellow}Environment (loaded from .env.local in CWD):${colors.reset}
93
+ WORKER_FLEET_SIZE Override number of workers
94
+ WORKER_CAPACITY Override agents per worker
95
+ WORKER_API_URL API endpoint (required)
96
+ WORKER_API_KEY API key for authentication (required)
97
+
98
+ ${colors.yellow}System Info:${colors.reset}
99
+ CPU Cores: ${os.cpus().length}
100
+ Total RAM: ${Math.round(os.totalmem() / 1024 / 1024 / 1024)} GB
101
+ Free RAM: ${Math.round(os.freemem() / 1024 / 1024 / 1024)} GB
102
+ `);
103
+ }
104
+ function timestamp() {
105
+ return new Date().toLocaleTimeString('en-US', { hour12: false });
106
+ }
107
+ function fleetLog(workerId, color, level, message) {
108
+ const prefix = workerId !== null ? `[W${workerId.toString().padStart(2, '0')}]` : '[FLEET]';
109
+ const levelColor = level === 'ERR' ? colors.red : level === 'WRN' ? colors.yellow : colors.gray;
110
+ console.log(`${colors.gray}${timestamp()}${colors.reset} ${color}${prefix}${colors.reset} ${levelColor}${level}${colors.reset} ${message}`);
111
+ }
112
+ class WorkerFleet {
113
+ workers = new Map();
114
+ fleetConfig;
115
+ shuttingDown = false;
116
+ workerScript;
117
+ constructor(fleetConfig) {
118
+ this.fleetConfig = fleetConfig;
119
+ // Use the compiled worker.js in the same directory
120
+ this.workerScript = path.resolve(__dirname, 'worker.js');
121
+ }
122
+ async start() {
123
+ const { workers, capacity, dryRun } = this.fleetConfig;
124
+ const totalCapacity = workers * capacity;
125
+ console.log(`
126
+ ${colors.cyan}================================================================${colors.reset}
127
+ ${colors.cyan} AgentFactory Worker Fleet Manager${colors.reset}
128
+ ${colors.cyan}================================================================${colors.reset}
129
+ Workers: ${colors.green}${workers}${colors.reset}
130
+ Capacity/Worker: ${colors.green}${capacity}${colors.reset}
131
+ Total Capacity: ${colors.green}${totalCapacity}${colors.reset} concurrent agents
132
+
133
+ System:
134
+ CPU Cores: ${os.cpus().length}
135
+ Total RAM: ${Math.round(os.totalmem() / 1024 / 1024 / 1024)} GB
136
+ Free RAM: ${Math.round(os.freemem() / 1024 / 1024 / 1024)} GB
137
+ ${colors.cyan}================================================================${colors.reset}
138
+ `);
139
+ if (dryRun) {
140
+ console.log(`${colors.yellow}Dry run mode - not starting workers${colors.reset}`);
141
+ return;
142
+ }
143
+ // Set up shutdown handlers
144
+ process.on('SIGINT', () => this.shutdown('SIGINT'));
145
+ process.on('SIGTERM', () => this.shutdown('SIGTERM'));
146
+ // Spawn workers with staggered start to avoid thundering herd
147
+ for (let i = 0; i < workers; i++) {
148
+ await this.spawnWorker(i);
149
+ if (i < workers - 1) {
150
+ await new Promise(resolve => setTimeout(resolve, 1000));
151
+ }
152
+ }
153
+ fleetLog(null, colors.green, 'INF', `All ${workers} workers started`);
154
+ // Keep the fleet manager running
155
+ await new Promise(() => { });
156
+ }
157
+ async spawnWorker(id) {
158
+ const color = workerColors[id % workerColors.length];
159
+ const existingWorker = this.workers.get(id);
160
+ const restartCount = existingWorker?.restartCount ?? 0;
161
+ fleetLog(id, color, 'INF', `Starting worker (capacity: ${this.fleetConfig.capacity})${restartCount > 0 ? ` [restart #${restartCount}]` : ''}`);
162
+ const workerProcess = spawn('node', [this.workerScript, '--capacity', String(this.fleetConfig.capacity)], {
163
+ stdio: ['ignore', 'pipe', 'pipe'],
164
+ env: {
165
+ ...process.env,
166
+ WORKER_FLEET_ID: String(id),
167
+ },
168
+ cwd: process.cwd(),
169
+ });
170
+ const workerInfo = {
171
+ id,
172
+ process: workerProcess,
173
+ color,
174
+ startedAt: new Date(),
175
+ restartCount,
176
+ };
177
+ this.workers.set(id, workerInfo);
178
+ // Handle stdout - prefix with worker ID
179
+ workerProcess.stdout?.on('data', (data) => {
180
+ const lines = data.toString().trim().split('\n');
181
+ for (const line of lines) {
182
+ if (line.trim()) {
183
+ console.log(`${color}[W${id.toString().padStart(2, '0')}]${colors.reset} ${line}`);
184
+ }
185
+ }
186
+ });
187
+ // Handle stderr
188
+ workerProcess.stderr?.on('data', (data) => {
189
+ const lines = data.toString().trim().split('\n');
190
+ for (const line of lines) {
191
+ if (line.trim()) {
192
+ console.log(`${color}[W${id.toString().padStart(2, '0')}]${colors.reset} ${colors.red}${line}${colors.reset}`);
193
+ }
194
+ }
195
+ });
196
+ // Handle worker exit
197
+ workerProcess.on('exit', (code, signal) => {
198
+ if (this.shuttingDown) {
199
+ fleetLog(id, color, 'INF', `Worker stopped (code: ${code}, signal: ${signal})`);
200
+ return;
201
+ }
202
+ fleetLog(id, color, 'WRN', `Worker exited unexpectedly (code: ${code}, signal: ${signal}) - restarting in 5s`);
203
+ const worker = this.workers.get(id);
204
+ if (worker) {
205
+ worker.restartCount++;
206
+ }
207
+ setTimeout(() => {
208
+ if (!this.shuttingDown) {
209
+ this.spawnWorker(id);
210
+ }
211
+ }, 5000);
212
+ });
213
+ workerProcess.on('error', (err) => {
214
+ fleetLog(id, color, 'ERR', `Worker error: ${err.message}`);
215
+ });
216
+ }
217
+ async shutdown(signal) {
218
+ if (this.shuttingDown)
219
+ return;
220
+ this.shuttingDown = true;
221
+ console.log(`\n${colors.yellow}Received ${signal} - shutting down fleet...${colors.reset}`);
222
+ for (const [id, worker] of this.workers) {
223
+ fleetLog(id, worker.color, 'INF', 'Stopping worker...');
224
+ worker.process.kill('SIGTERM');
225
+ }
226
+ // Wait for workers to exit (max 30 seconds)
227
+ const timeout = setTimeout(() => {
228
+ console.log(`${colors.red}Timeout waiting for workers - force killing${colors.reset}`);
229
+ for (const worker of this.workers.values()) {
230
+ worker.process.kill('SIGKILL');
231
+ }
232
+ process.exit(1);
233
+ }, 30000);
234
+ await Promise.all(Array.from(this.workers.values()).map((worker) => new Promise((resolve) => {
235
+ worker.process.on('exit', () => resolve());
236
+ })));
237
+ clearTimeout(timeout);
238
+ console.log(`${colors.green}All workers stopped${colors.reset}`);
239
+ process.exit(0);
240
+ }
241
+ }
242
+ // Main
243
+ const fleetConfig = parseArgs();
244
+ if (!process.env.WORKER_API_URL) {
245
+ console.error(`${colors.red}Error: WORKER_API_URL environment variable is required${colors.reset}`);
246
+ process.exit(1);
247
+ }
248
+ if (!process.env.WORKER_API_KEY) {
249
+ console.error(`${colors.red}Error: WORKER_API_KEY environment variable is required${colors.reset}`);
250
+ process.exit(1);
251
+ }
252
+ const fleet = new WorkerFleet(fleetConfig);
253
+ fleet.start().catch((err) => {
254
+ console.error(`${colors.red}Fleet error: ${err instanceof Error ? err.message : String(err)}${colors.reset}`);
255
+ process.exit(1);
256
+ });
@@ -2,22 +2,22 @@
2
2
  /**
3
3
  * AgentFactory Worker CLI
4
4
  *
5
- * Starts a remote worker that polls the Redis work queue
6
- * and processes assigned agent sessions.
5
+ * Local worker that polls the coordinator for work and executes agents.
7
6
  *
8
7
  * Usage:
9
8
  * af-worker [options]
10
9
  *
11
10
  * Options:
12
- * --capacity <number> Maximum concurrent agents (default: 2)
13
- * --api-url <url> Agent API URL for activity proxying
14
- * --api-key <key> API key for worker authentication
11
+ * --capacity <number> Maximum concurrent agents (default: 3)
12
+ * --hostname <name> Worker hostname (default: os.hostname())
13
+ * --api-url <url> Coordinator API URL (default: WORKER_API_URL env)
14
+ * --api-key <key> API key (default: WORKER_API_KEY env)
15
+ * --dry-run Poll but don't execute work
15
16
  *
16
- * Environment:
17
- * LINEAR_API_KEY Required API key for Linear authentication
18
- * REDIS_URL Required for work queue polling
19
- * WORKER_API_URL Agent API URL (alternative to --api-url)
20
- * WORKER_API_KEY API key (alternative to --api-key)
17
+ * Environment (loaded from .env.local in CWD):
18
+ * WORKER_API_URL Coordinator API URL (e.g., https://agent.example.com)
19
+ * WORKER_API_KEY API key for authentication
20
+ * LINEAR_API_KEY Required for agent operations
21
21
  */
22
22
  export {};
23
23
  //# sourceMappingURL=worker.d.ts.map