@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.
package/dist/src/index.js CHANGED
@@ -17,6 +17,10 @@ Usage:
17
17
  Commands:
18
18
  orchestrator Spawn concurrent agents on backlog issues
19
19
  worker Start a remote worker that polls for queued work
20
+ worker-fleet Spawn and manage multiple worker processes
21
+ cleanup Clean up orphaned git worktrees
22
+ queue-admin Manage Redis work queue and sessions
23
+ analyze-logs Analyze agent session logs for errors
20
24
  help Show this help message
21
25
 
22
26
  Run 'agentfactory <command> --help' for command-specific options.
@@ -31,6 +35,18 @@ switch (command) {
31
35
  case 'worker':
32
36
  import('./worker');
33
37
  break;
38
+ case 'worker-fleet':
39
+ import('./worker-fleet');
40
+ break;
41
+ case 'cleanup':
42
+ import('./cleanup');
43
+ break;
44
+ case 'queue-admin':
45
+ import('./queue-admin');
46
+ break;
47
+ case 'analyze-logs':
48
+ import('./analyze-logs');
49
+ break;
34
50
  case 'help':
35
51
  case '--help':
36
52
  case '-h':
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AgentFactory Queue Admin
4
+ *
5
+ * Manage the Redis work queue, sessions, claims, and workers.
6
+ *
7
+ * Usage:
8
+ * af-queue-admin <command>
9
+ *
10
+ * Commands:
11
+ * list List all queued work items
12
+ * sessions List all sessions
13
+ * workers List all registered workers
14
+ * clear-claims Clear stale work claims
15
+ * clear-queue Clear the work queue
16
+ * clear-all Clear queue, sessions, claims, and workers
17
+ * reset Full state reset (claims + queue + stuck sessions)
18
+ * remove <id> Remove a specific session by ID (partial match)
19
+ *
20
+ * Environment (loaded from .env.local in CWD):
21
+ * REDIS_URL Required for Redis connection
22
+ */
23
+ export {};
24
+ //# sourceMappingURL=queue-admin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue-admin.d.ts","sourceRoot":"","sources":["../../src/queue-admin.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;GAoBG"}
@@ -0,0 +1,418 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AgentFactory Queue Admin
4
+ *
5
+ * Manage the Redis work queue, sessions, claims, and workers.
6
+ *
7
+ * Usage:
8
+ * af-queue-admin <command>
9
+ *
10
+ * Commands:
11
+ * list List all queued work items
12
+ * sessions List all sessions
13
+ * workers List all registered workers
14
+ * clear-claims Clear stale work claims
15
+ * clear-queue Clear the work queue
16
+ * clear-all Clear queue, sessions, claims, and workers
17
+ * reset Full state reset (claims + queue + stuck sessions)
18
+ * remove <id> Remove a specific session by ID (partial match)
19
+ *
20
+ * Environment (loaded from .env.local in CWD):
21
+ * REDIS_URL Required for Redis connection
22
+ */
23
+ import path from 'path';
24
+ import { config } from 'dotenv';
25
+ // Load environment variables from .env.local in CWD
26
+ config({ path: path.resolve(process.cwd(), '.env.local') });
27
+ import { getRedisClient, redisKeys, redisDel, redisGet, redisSet, redisZRangeByScore, redisHGetAll, disconnectRedis, } from '@supaku/agentfactory-server';
28
+ // Redis key constants
29
+ const WORK_QUEUE_KEY = 'work:queue';
30
+ const WORK_ITEMS_KEY = 'work:items';
31
+ const WORK_CLAIM_PREFIX = 'work:claim:';
32
+ const SESSION_KEY_PREFIX = 'agent:session:';
33
+ const WORKER_PREFIX = 'work:worker:';
34
+ // ANSI colors
35
+ const C = {
36
+ reset: '\x1b[0m',
37
+ red: '\x1b[31m',
38
+ green: '\x1b[32m',
39
+ yellow: '\x1b[33m',
40
+ cyan: '\x1b[36m',
41
+ gray: '\x1b[90m',
42
+ };
43
+ function ensureRedis() {
44
+ if (!process.env.REDIS_URL) {
45
+ console.error(`${C.red}Error: REDIS_URL not set${C.reset}`);
46
+ process.exit(1);
47
+ }
48
+ // Initialize the Redis client
49
+ getRedisClient();
50
+ }
51
+ async function listQueue() {
52
+ ensureRedis();
53
+ // Get items from sorted set (current queue format)
54
+ const queuedSessionIds = await redisZRangeByScore(WORK_QUEUE_KEY, '-inf', '+inf');
55
+ const workItems = await redisHGetAll(WORK_ITEMS_KEY);
56
+ const workItemCount = Object.keys(workItems).length;
57
+ console.log(`\n${C.cyan}Work Queue${C.reset} (${Math.max(queuedSessionIds.length, workItemCount)} items):`);
58
+ console.log('='.repeat(60));
59
+ if (workItemCount === 0 && queuedSessionIds.length === 0) {
60
+ console.log('(empty)');
61
+ }
62
+ else {
63
+ for (const [sessionId, itemJson] of Object.entries(workItems)) {
64
+ try {
65
+ const work = JSON.parse(itemJson);
66
+ console.log(`- ${work.issueIdentifier ?? sessionId.slice(0, 8)} (session: ${sessionId.slice(0, 8)}...)`);
67
+ console.log(` Priority: ${work.priority ?? 'none'}, WorkType: ${work.workType ?? 'development'}`);
68
+ if (work.queuedAt) {
69
+ console.log(` Queued: ${new Date(work.queuedAt).toISOString()}`);
70
+ }
71
+ if (work.claudeSessionId) {
72
+ console.log(` ${C.yellow}Has claudeSessionId: ${work.claudeSessionId.substring(0, 12)}${C.reset}`);
73
+ }
74
+ if (work.prompt) {
75
+ console.log(` Prompt: "${work.prompt.slice(0, 50)}..."`);
76
+ }
77
+ }
78
+ catch {
79
+ console.log(`- [invalid JSON]: ${sessionId}`);
80
+ }
81
+ }
82
+ }
83
+ await disconnectRedis();
84
+ }
85
+ async function listSessions() {
86
+ ensureRedis();
87
+ const keys = await redisKeys(`${SESSION_KEY_PREFIX}*`);
88
+ console.log(`\n${C.cyan}Sessions${C.reset} (${keys.length} total):`);
89
+ console.log('='.repeat(60));
90
+ if (keys.length === 0) {
91
+ console.log('(none)');
92
+ }
93
+ else {
94
+ for (const key of keys) {
95
+ const session = await redisGet(key);
96
+ if (session) {
97
+ const statusColors = {
98
+ pending: C.yellow,
99
+ claimed: C.cyan,
100
+ running: C.cyan,
101
+ completed: C.green,
102
+ failed: C.red,
103
+ stopped: C.yellow,
104
+ };
105
+ const statusColor = statusColors[session.status] || '';
106
+ console.log(`- ${session.issueIdentifier || session.issueId.slice(0, 8)} [${statusColor}${session.status}${C.reset}]`);
107
+ console.log(` Session: ${session.linearSessionId.slice(0, 12)}...`);
108
+ if (session.workType) {
109
+ console.log(` WorkType: ${session.workType}`);
110
+ }
111
+ console.log(` Updated: ${new Date(session.updatedAt * 1000).toISOString()}`);
112
+ if (session.workerId) {
113
+ console.log(` Worker: ${session.workerId}`);
114
+ }
115
+ }
116
+ }
117
+ }
118
+ await disconnectRedis();
119
+ }
120
+ async function listWorkersFn() {
121
+ ensureRedis();
122
+ const keys = await redisKeys(`${WORKER_PREFIX}*`);
123
+ console.log(`\n${C.cyan}Workers${C.reset} (${keys.length} total):`);
124
+ console.log('='.repeat(60));
125
+ if (keys.length === 0) {
126
+ console.log('(none)');
127
+ }
128
+ else {
129
+ for (const key of keys) {
130
+ const worker = await redisGet(key);
131
+ if (worker) {
132
+ const statusColor = worker.status === 'active' ? C.green : C.yellow;
133
+ console.log(`- ${worker.id.slice(0, 12)} [${statusColor}${worker.status}${C.reset}]`);
134
+ if (worker.hostname) {
135
+ console.log(` Hostname: ${worker.hostname}`);
136
+ }
137
+ console.log(` Capacity: ${worker.activeCount ?? 0}/${worker.capacity ?? '?'}`);
138
+ if (worker.lastHeartbeat) {
139
+ const ago = Math.round((Date.now() - worker.lastHeartbeat) / 1000);
140
+ console.log(` Last heartbeat: ${ago}s ago`);
141
+ }
142
+ }
143
+ }
144
+ }
145
+ await disconnectRedis();
146
+ }
147
+ async function clearClaims() {
148
+ ensureRedis();
149
+ console.log('Clearing work claims...');
150
+ const claimKeys = await redisKeys(`${WORK_CLAIM_PREFIX}*`);
151
+ console.log(`Found ${claimKeys.length} claim(s)`);
152
+ if (claimKeys.length === 0) {
153
+ console.log('No claims to clear');
154
+ await disconnectRedis();
155
+ return;
156
+ }
157
+ let deleted = 0;
158
+ for (const key of claimKeys) {
159
+ const result = await redisDel(key);
160
+ if (result > 0) {
161
+ console.log(` Deleted: ${key}`);
162
+ deleted++;
163
+ }
164
+ }
165
+ console.log(`\nCleared ${deleted} claim(s)`);
166
+ await disconnectRedis();
167
+ }
168
+ async function clearQueue() {
169
+ ensureRedis();
170
+ console.log('Clearing work queue...');
171
+ const queuedSessionIds = await redisZRangeByScore(WORK_QUEUE_KEY, '-inf', '+inf');
172
+ console.log(`Found ${queuedSessionIds.length} item(s) in queue sorted set`);
173
+ const workItems = await redisHGetAll(WORK_ITEMS_KEY);
174
+ const workItemCount = Object.keys(workItems).length;
175
+ console.log(`Found ${workItemCount} item(s) in work items hash`);
176
+ // Show what we're clearing
177
+ for (const [sessionId, itemJson] of Object.entries(workItems)) {
178
+ try {
179
+ const work = JSON.parse(itemJson);
180
+ console.log(` - ${work.issueIdentifier ?? sessionId.slice(0, 8)} (workType: ${work.workType || 'development'})`);
181
+ if (work.claudeSessionId) {
182
+ console.log(` ${C.yellow}Has claudeSessionId: ${work.claudeSessionId.substring(0, 12)}${C.reset}`);
183
+ }
184
+ }
185
+ catch {
186
+ console.log(` - [invalid JSON]: ${sessionId}`);
187
+ }
188
+ }
189
+ let cleared = 0;
190
+ if (queuedSessionIds.length > 0) {
191
+ await redisDel(WORK_QUEUE_KEY);
192
+ cleared++;
193
+ }
194
+ if (workItemCount > 0) {
195
+ await redisDel(WORK_ITEMS_KEY);
196
+ cleared++;
197
+ }
198
+ const totalItems = Math.max(queuedSessionIds.length, workItemCount);
199
+ if (cleared > 0) {
200
+ console.log(`\nCleared ${totalItems} item(s) from work queue`);
201
+ }
202
+ else {
203
+ console.log('\nQueue was already empty');
204
+ }
205
+ await disconnectRedis();
206
+ }
207
+ async function clearAll() {
208
+ ensureRedis();
209
+ console.log('Clearing ALL state...\n');
210
+ // Clear work queue
211
+ const queuedSessionIds = await redisZRangeByScore(WORK_QUEUE_KEY, '-inf', '+inf');
212
+ const workItems = await redisHGetAll(WORK_ITEMS_KEY);
213
+ if (queuedSessionIds.length > 0)
214
+ await redisDel(WORK_QUEUE_KEY);
215
+ if (Object.keys(workItems).length > 0)
216
+ await redisDel(WORK_ITEMS_KEY);
217
+ console.log(`Cleared ${Math.max(queuedSessionIds.length, Object.keys(workItems).length)} queue items`);
218
+ // Clear all sessions
219
+ const sessionKeys = await redisKeys(`${SESSION_KEY_PREFIX}*`);
220
+ for (const key of sessionKeys) {
221
+ await redisDel(key);
222
+ }
223
+ console.log(`Cleared ${sessionKeys.length} sessions`);
224
+ // Clear all claims
225
+ const claimKeys = await redisKeys(`${WORK_CLAIM_PREFIX}*`);
226
+ for (const key of claimKeys) {
227
+ await redisDel(key);
228
+ }
229
+ console.log(`Cleared ${claimKeys.length} claims`);
230
+ // Clear all workers
231
+ const workerKeys = await redisKeys(`${WORKER_PREFIX}*`);
232
+ for (const key of workerKeys) {
233
+ await redisDel(key);
234
+ }
235
+ console.log(`Cleared ${workerKeys.length} worker registrations`);
236
+ console.log('\nAll cleared!');
237
+ await disconnectRedis();
238
+ }
239
+ async function resetWorkState() {
240
+ ensureRedis();
241
+ console.log('Resetting work state...');
242
+ console.log('-'.repeat(60));
243
+ let totalCleared = 0;
244
+ // 1. Clear work claims
245
+ console.log('\nClearing work claims...');
246
+ const claimKeys = await redisKeys(`${WORK_CLAIM_PREFIX}*`);
247
+ console.log(` Found ${claimKeys.length} claim(s)`);
248
+ for (const key of claimKeys) {
249
+ const result = await redisDel(key);
250
+ if (result > 0) {
251
+ console.log(` Deleted: ${key}`);
252
+ totalCleared++;
253
+ }
254
+ }
255
+ // 2. Clear work queue
256
+ console.log('\nClearing work queue...');
257
+ const queuedSessionIds = await redisZRangeByScore(WORK_QUEUE_KEY, '-inf', '+inf');
258
+ console.log(` Found ${queuedSessionIds.length} queued item(s) in sorted set`);
259
+ const workItems = await redisHGetAll(WORK_ITEMS_KEY);
260
+ const workItemCount = Object.keys(workItems).length;
261
+ console.log(` Found ${workItemCount} item(s) in work items hash`);
262
+ for (const [sessionId, itemJson] of Object.entries(workItems)) {
263
+ try {
264
+ const work = JSON.parse(itemJson);
265
+ console.log(` - ${work.issueIdentifier ?? sessionId.slice(0, 8)} (workType: ${work.workType || 'development'})`);
266
+ }
267
+ catch {
268
+ console.log(` - [invalid item: ${sessionId}]`);
269
+ }
270
+ }
271
+ if (queuedSessionIds.length > 0 || workItemCount > 0) {
272
+ await redisDel(WORK_QUEUE_KEY);
273
+ await redisDel(WORK_ITEMS_KEY);
274
+ totalCleared += Math.max(queuedSessionIds.length, workItemCount);
275
+ console.log(` Cleared queue and items hash`);
276
+ }
277
+ // 3. Reset stuck sessions
278
+ console.log('\nResetting stuck sessions...');
279
+ const sessionKeys = await redisKeys(`${SESSION_KEY_PREFIX}*`);
280
+ console.log(` Found ${sessionKeys.length} session(s)`);
281
+ let sessionsReset = 0;
282
+ for (const key of sessionKeys) {
283
+ const session = await redisGet(key);
284
+ if (!session)
285
+ continue;
286
+ if (session.status === 'running' || session.status === 'claimed') {
287
+ console.log(` Resetting: ${session.issueIdentifier || session.linearSessionId}`);
288
+ console.log(` Status: ${session.status}, WorkerId: ${session.workerId || 'none'}`);
289
+ const updated = {
290
+ ...session,
291
+ status: 'pending',
292
+ workerId: undefined,
293
+ claimedAt: undefined,
294
+ claudeSessionId: undefined,
295
+ updatedAt: Math.floor(Date.now() / 1000),
296
+ };
297
+ await redisSet(key, updated, 24 * 60 * 60);
298
+ sessionsReset++;
299
+ console.log(` Reset to pending`);
300
+ }
301
+ }
302
+ console.log('\n' + '-'.repeat(60));
303
+ console.log(`\nReset complete:`);
304
+ console.log(` - Claims cleared: ${claimKeys.length}`);
305
+ console.log(` - Queue items cleared: ${Math.max(queuedSessionIds.length, workItemCount)}`);
306
+ console.log(` - Sessions reset: ${sessionsReset}`);
307
+ await disconnectRedis();
308
+ }
309
+ async function removeSession(sessionId) {
310
+ ensureRedis();
311
+ let found = false;
312
+ // Find session by partial ID match
313
+ const keys = await redisKeys(`${SESSION_KEY_PREFIX}*`);
314
+ for (const key of keys) {
315
+ if (key.includes(sessionId)) {
316
+ await redisDel(key);
317
+ console.log(`Removed session: ${key.replace(SESSION_KEY_PREFIX, '')}`);
318
+ found = true;
319
+ }
320
+ }
321
+ // Also remove from queue if present
322
+ const workItems = await redisHGetAll(WORK_ITEMS_KEY);
323
+ for (const [sid, itemJson] of Object.entries(workItems)) {
324
+ if (sid.includes(sessionId)) {
325
+ // Remove from hash via direct Redis command
326
+ const redis = getRedisClient();
327
+ await redis.hdel(WORK_ITEMS_KEY, sid);
328
+ // Remove from sorted set
329
+ const { redisZRem } = await import('@supaku/agentfactory-server');
330
+ await redisZRem(WORK_QUEUE_KEY, sid);
331
+ const work = JSON.parse(itemJson);
332
+ console.log(`Removed from queue: ${work.issueIdentifier ?? sid.slice(0, 8)}`);
333
+ found = true;
334
+ }
335
+ }
336
+ // Remove claim if present
337
+ const claimKeys = await redisKeys(`${WORK_CLAIM_PREFIX}*`);
338
+ for (const key of claimKeys) {
339
+ if (key.includes(sessionId)) {
340
+ await redisDel(key);
341
+ console.log(`Removed claim: ${key}`);
342
+ found = true;
343
+ }
344
+ }
345
+ if (!found) {
346
+ console.log(`No session found matching: ${sessionId}`);
347
+ }
348
+ await disconnectRedis();
349
+ }
350
+ function printUsage() {
351
+ console.log(`
352
+ ${C.cyan}AgentFactory Queue Admin${C.reset} - Manage Redis work queue and sessions
353
+
354
+ ${C.yellow}Usage:${C.reset}
355
+ af-queue-admin <command>
356
+
357
+ ${C.yellow}Commands:${C.reset}
358
+ list List all queued work items
359
+ sessions List all sessions
360
+ workers List all registered workers
361
+ clear-claims Clear stale work claims
362
+ clear-queue Clear the work queue
363
+ clear-all Clear queue, sessions, claims, and workers
364
+ reset Full state reset (claims + queue + stuck sessions)
365
+ remove <id> Remove a specific session by ID (partial match)
366
+
367
+ ${C.yellow}Examples:${C.reset}
368
+ af-queue-admin list
369
+ af-queue-admin sessions
370
+ af-queue-admin clear-queue
371
+ af-queue-admin reset
372
+ af-queue-admin remove abc123
373
+ `);
374
+ }
375
+ async function main() {
376
+ const command = process.argv[2];
377
+ const arg = process.argv[3];
378
+ switch (command) {
379
+ case 'list':
380
+ await listQueue();
381
+ break;
382
+ case 'sessions':
383
+ await listSessions();
384
+ break;
385
+ case 'workers':
386
+ await listWorkersFn();
387
+ break;
388
+ case 'clear-claims':
389
+ await clearClaims();
390
+ break;
391
+ case 'clear-queue':
392
+ await clearQueue();
393
+ break;
394
+ case 'clear-all':
395
+ await clearAll();
396
+ break;
397
+ case 'reset':
398
+ await resetWorkState();
399
+ break;
400
+ case 'remove':
401
+ if (!arg) {
402
+ console.error('Usage: af-queue-admin remove <session-id>');
403
+ process.exit(1);
404
+ }
405
+ await removeSession(arg);
406
+ break;
407
+ case '--help':
408
+ case '-h':
409
+ case 'help':
410
+ default:
411
+ printUsage();
412
+ break;
413
+ }
414
+ }
415
+ main().catch((error) => {
416
+ console.error('Error:', error instanceof Error ? error.message : error);
417
+ process.exit(1);
418
+ });
@@ -0,0 +1,23 @@
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
+ export {};
23
+ //# sourceMappingURL=worker-fleet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-fleet.d.ts","sourceRoot":"","sources":["../../src/worker-fleet.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;GAmBG"}