agent-relay 1.0.8 → 1.0.9

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 (113) hide show
  1. package/README.md +158 -0
  2. package/dist/bridge/config.d.ts +41 -0
  3. package/dist/bridge/config.d.ts.map +1 -0
  4. package/dist/bridge/config.js +143 -0
  5. package/dist/bridge/config.js.map +1 -0
  6. package/dist/bridge/index.d.ts +10 -0
  7. package/dist/bridge/index.d.ts.map +1 -0
  8. package/dist/bridge/index.js +10 -0
  9. package/dist/bridge/index.js.map +1 -0
  10. package/dist/bridge/multi-project-client.d.ts +99 -0
  11. package/dist/bridge/multi-project-client.d.ts.map +1 -0
  12. package/dist/bridge/multi-project-client.js +386 -0
  13. package/dist/bridge/multi-project-client.js.map +1 -0
  14. package/dist/bridge/spawner.d.ts +46 -0
  15. package/dist/bridge/spawner.d.ts.map +1 -0
  16. package/dist/bridge/spawner.js +223 -0
  17. package/dist/bridge/spawner.js.map +1 -0
  18. package/dist/bridge/types.d.ts +55 -0
  19. package/dist/bridge/types.d.ts.map +1 -0
  20. package/dist/bridge/types.js +6 -0
  21. package/dist/bridge/types.js.map +1 -0
  22. package/dist/bridge/utils.d.ts +30 -0
  23. package/dist/bridge/utils.d.ts.map +1 -0
  24. package/dist/bridge/utils.js +54 -0
  25. package/dist/bridge/utils.js.map +1 -0
  26. package/dist/cli/index.js +564 -5
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/daemon/agent-registry.d.ts.map +1 -1
  29. package/dist/daemon/agent-registry.js +6 -1
  30. package/dist/daemon/agent-registry.js.map +1 -1
  31. package/dist/daemon/connection.d.ts +22 -0
  32. package/dist/daemon/connection.d.ts.map +1 -1
  33. package/dist/daemon/connection.js +59 -13
  34. package/dist/daemon/connection.js.map +1 -1
  35. package/dist/daemon/router.d.ts +27 -0
  36. package/dist/daemon/router.d.ts.map +1 -1
  37. package/dist/daemon/router.js +108 -3
  38. package/dist/daemon/router.js.map +1 -1
  39. package/dist/daemon/server.d.ts +8 -0
  40. package/dist/daemon/server.d.ts.map +1 -1
  41. package/dist/daemon/server.js +95 -23
  42. package/dist/daemon/server.js.map +1 -1
  43. package/dist/dashboard/metrics.d.ts +105 -0
  44. package/dist/dashboard/metrics.d.ts.map +1 -0
  45. package/dist/dashboard/metrics.js +192 -0
  46. package/dist/dashboard/metrics.js.map +1 -0
  47. package/dist/dashboard/needs-attention.d.ts +24 -0
  48. package/dist/dashboard/needs-attention.d.ts.map +1 -0
  49. package/dist/dashboard/needs-attention.js +78 -0
  50. package/dist/dashboard/needs-attention.js.map +1 -0
  51. package/dist/dashboard/public/bridge.html +1272 -0
  52. package/dist/dashboard/public/index.html +2017 -879
  53. package/dist/dashboard/public/js/app.js +184 -0
  54. package/dist/dashboard/public/js/app.js.map +7 -0
  55. package/dist/dashboard/public/metrics.html +999 -0
  56. package/dist/dashboard/server.d.ts +13 -0
  57. package/dist/dashboard/server.d.ts.map +1 -1
  58. package/dist/dashboard/server.js +568 -13
  59. package/dist/dashboard/server.js.map +1 -1
  60. package/dist/dashboard/start.js +1 -1
  61. package/dist/dashboard/start.js.map +1 -1
  62. package/dist/dashboard-v2/index.d.ts +10 -0
  63. package/dist/dashboard-v2/index.d.ts.map +1 -0
  64. package/dist/dashboard-v2/index.js +54 -0
  65. package/dist/dashboard-v2/index.js.map +1 -0
  66. package/dist/dashboard-v2/lib/api.d.ts +95 -0
  67. package/dist/dashboard-v2/lib/api.d.ts.map +1 -0
  68. package/dist/dashboard-v2/lib/api.js +270 -0
  69. package/dist/dashboard-v2/lib/api.js.map +1 -0
  70. package/dist/dashboard-v2/lib/colors.d.ts +61 -0
  71. package/dist/dashboard-v2/lib/colors.d.ts.map +1 -0
  72. package/dist/dashboard-v2/lib/colors.js +198 -0
  73. package/dist/dashboard-v2/lib/colors.js.map +1 -0
  74. package/dist/dashboard-v2/lib/hierarchy.d.ts +74 -0
  75. package/dist/dashboard-v2/lib/hierarchy.d.ts.map +1 -0
  76. package/dist/dashboard-v2/lib/hierarchy.js +196 -0
  77. package/dist/dashboard-v2/lib/hierarchy.js.map +1 -0
  78. package/dist/dashboard-v2/types/index.d.ts +154 -0
  79. package/dist/dashboard-v2/types/index.d.ts.map +1 -0
  80. package/dist/dashboard-v2/types/index.js +6 -0
  81. package/dist/dashboard-v2/types/index.js.map +1 -0
  82. package/dist/storage/adapter.d.ts +21 -1
  83. package/dist/storage/adapter.d.ts.map +1 -1
  84. package/dist/storage/adapter.js +36 -0
  85. package/dist/storage/adapter.js.map +1 -1
  86. package/dist/storage/sqlite-adapter.d.ts +34 -0
  87. package/dist/storage/sqlite-adapter.d.ts.map +1 -1
  88. package/dist/storage/sqlite-adapter.js +253 -12
  89. package/dist/storage/sqlite-adapter.js.map +1 -1
  90. package/dist/utils/agent-config.d.ts +45 -0
  91. package/dist/utils/agent-config.d.ts.map +1 -0
  92. package/dist/utils/agent-config.js +118 -0
  93. package/dist/utils/agent-config.js.map +1 -0
  94. package/dist/wrapper/client.d.ts +8 -0
  95. package/dist/wrapper/client.d.ts.map +1 -1
  96. package/dist/wrapper/client.js +26 -0
  97. package/dist/wrapper/client.js.map +1 -1
  98. package/dist/wrapper/parser.d.ts +17 -0
  99. package/dist/wrapper/parser.d.ts.map +1 -1
  100. package/dist/wrapper/parser.js +334 -10
  101. package/dist/wrapper/parser.js.map +1 -1
  102. package/dist/wrapper/tmux-wrapper.d.ts +37 -2
  103. package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
  104. package/dist/wrapper/tmux-wrapper.js +178 -18
  105. package/dist/wrapper/tmux-wrapper.js.map +1 -1
  106. package/docs/AGENTS.md +105 -0
  107. package/docs/ARCHITECTURE_DECISIONS.md +175 -0
  108. package/docs/COMPETITIVE_ANALYSIS.md +897 -0
  109. package/docs/DESIGN_BRIDGE_STAFFING.md +878 -0
  110. package/docs/MONETIZATION.md +1679 -0
  111. package/docs/agent-relay-snippet.md +61 -0
  112. package/docs/dashboard-v2-plan.md +179 -0
  113. package/package.json +5 -2
package/dist/cli/index.js CHANGED
@@ -50,28 +50,158 @@ program
50
50
  return;
51
51
  }
52
52
  const { getProjectPaths } = await import('../utils/project-namespace.js');
53
+ const { findAgentConfig, isClaudeCli, buildClaudeArgs } = await import('../utils/agent-config.js');
53
54
  const paths = getProjectPaths();
54
55
  const [mainCommand, ...commandArgs] = commandParts;
55
56
  const agentName = options.name ?? generateAgentName();
56
57
  console.error(`Agent: ${agentName}`);
57
58
  console.error(`Project: ${paths.projectId}`);
59
+ // Auto-detect agent config and inject --model/--agent for Claude CLI
60
+ let finalArgs = commandArgs;
61
+ if (isClaudeCli(mainCommand)) {
62
+ const config = findAgentConfig(agentName, paths.projectRoot);
63
+ if (config) {
64
+ console.error(`Agent config: ${config.configPath}`);
65
+ if (config.model) {
66
+ console.error(`Model: ${config.model}`);
67
+ }
68
+ finalArgs = buildClaudeArgs(agentName, commandArgs, paths.projectRoot);
69
+ }
70
+ }
58
71
  const { TmuxWrapper } = await import('../wrapper/tmux-wrapper.js');
72
+ const { AgentSpawner } = await import('../bridge/spawner.js');
73
+ // Create spawner so any agent can spawn workers
74
+ const spawner = new AgentSpawner(paths.projectRoot);
59
75
  const wrapper = new TmuxWrapper({
60
76
  name: agentName,
61
77
  command: mainCommand,
62
- args: commandArgs,
78
+ args: finalArgs,
63
79
  socketPath: paths.socketPath,
64
80
  debug: false, // Use -q to keep quiet (debug off by default)
65
81
  relayPrefix: options.prefix,
66
82
  useInbox: true,
67
83
  inboxDir: paths.dataDir, // Use the project-specific data directory for the inbox
84
+ // Wire up spawn/release callbacks so any agent can spawn workers
85
+ onSpawn: async (workerName, workerCli, task) => {
86
+ console.error(`[${agentName}] Spawning ${workerName} (${workerCli})...`);
87
+ const result = await spawner.spawn({
88
+ name: workerName,
89
+ cli: workerCli,
90
+ task,
91
+ requestedBy: agentName,
92
+ });
93
+ if (result.success) {
94
+ console.error(`[${agentName}] ✓ Spawned ${workerName} in ${result.window}`);
95
+ }
96
+ else {
97
+ console.error(`[${agentName}] ✗ Failed to spawn ${workerName}: ${result.error}`);
98
+ }
99
+ },
100
+ onRelease: async (workerName) => {
101
+ console.error(`[${agentName}] Releasing ${workerName}...`);
102
+ const released = await spawner.release(workerName);
103
+ if (released) {
104
+ console.error(`[${agentName}] ✓ Released ${workerName}`);
105
+ }
106
+ else {
107
+ console.error(`[${agentName}] ✗ Worker ${workerName} not found`);
108
+ }
109
+ },
68
110
  });
69
- process.on('SIGINT', () => {
111
+ process.on('SIGINT', async () => {
112
+ await spawner.releaseAll();
70
113
  wrapper.stop();
71
114
  process.exit(0);
72
115
  });
73
116
  await wrapper.start();
74
117
  });
118
+ // Load teams.json from project root or .agent-relay/
119
+ function loadTeamConfig(projectRoot) {
120
+ const locations = [
121
+ path.join(projectRoot, 'teams.json'),
122
+ path.join(projectRoot, '.agent-relay', 'teams.json'),
123
+ ];
124
+ for (const configPath of locations) {
125
+ if (fs.existsSync(configPath)) {
126
+ try {
127
+ const content = fs.readFileSync(configPath, 'utf-8');
128
+ return JSON.parse(content);
129
+ }
130
+ catch (err) {
131
+ console.error(`Failed to parse ${configPath}:`, err);
132
+ }
133
+ }
134
+ }
135
+ return null;
136
+ }
137
+ // Spawn agents from team config using tmux
138
+ async function spawnTeamAgents(agents, socketPath, dataDir, projectRoot, relayPrefix) {
139
+ const { TmuxWrapper } = await import('../wrapper/tmux-wrapper.js');
140
+ const { findAgentConfig, isClaudeCli, buildClaudeArgs } = await import('../utils/agent-config.js');
141
+ const { AgentSpawner } = await import('../bridge/spawner.js');
142
+ // Create spawner so all team agents can spawn workers
143
+ const spawner = new AgentSpawner(projectRoot);
144
+ for (const agent of agents) {
145
+ console.log(`Spawning agent: ${agent.name} (${agent.cli})`);
146
+ // Parse CLI - handle "claude:opus" format
147
+ const [mainCommand, ...cliArgs] = agent.cli.split(/\s+/);
148
+ // Auto-detect agent config and inject --model/--agent for Claude CLI
149
+ let finalArgs = cliArgs;
150
+ if (isClaudeCli(mainCommand)) {
151
+ const config = findAgentConfig(agent.name, projectRoot);
152
+ if (config) {
153
+ console.log(` Agent config: ${config.configPath}`);
154
+ if (config.model) {
155
+ console.log(` Model: ${config.model}`);
156
+ }
157
+ finalArgs = buildClaudeArgs(agent.name, cliArgs, projectRoot);
158
+ }
159
+ }
160
+ const wrapper = new TmuxWrapper({
161
+ name: agent.name,
162
+ command: mainCommand,
163
+ args: finalArgs,
164
+ socketPath,
165
+ debug: false,
166
+ relayPrefix,
167
+ useInbox: true,
168
+ inboxDir: dataDir,
169
+ // Wire up spawn/release callbacks so any agent can spawn workers
170
+ onSpawn: async (workerName, workerCli, task) => {
171
+ console.log(`[${agent.name}] Spawning ${workerName} (${workerCli})...`);
172
+ const result = await spawner.spawn({
173
+ name: workerName,
174
+ cli: workerCli,
175
+ task,
176
+ requestedBy: agent.name,
177
+ });
178
+ if (result.success) {
179
+ console.log(`[${agent.name}] ✓ Spawned ${workerName} in ${result.window}`);
180
+ }
181
+ else {
182
+ console.error(`[${agent.name}] ✗ Failed to spawn ${workerName}: ${result.error}`);
183
+ }
184
+ },
185
+ onRelease: async (workerName) => {
186
+ console.log(`[${agent.name}] Releasing ${workerName}...`);
187
+ const released = await spawner.release(workerName);
188
+ if (released) {
189
+ console.log(`[${agent.name}] ✓ Released ${workerName}`);
190
+ }
191
+ else {
192
+ console.error(`[${agent.name}] ✗ Worker ${workerName} not found`);
193
+ }
194
+ },
195
+ });
196
+ try {
197
+ await wrapper.start();
198
+ console.log(` Started: ${agent.name}`);
199
+ }
200
+ catch (err) {
201
+ console.error(` Failed to start ${agent.name}:`, err);
202
+ }
203
+ }
204
+ }
75
205
  // up - Start daemon + dashboard
76
206
  program
77
207
  .command('up')
@@ -79,7 +209,7 @@ program
79
209
  .option('--no-dashboard', 'Disable web dashboard')
80
210
  .option('--port <port>', 'Dashboard port', DEFAULT_DASHBOARD_PORT)
81
211
  .action(async (options) => {
82
- const { ensureProjectDir } = await import('../utils/project-namespace.js');
212
+ const { getProjectPaths, ensureProjectDir } = await import('../utils/project-namespace.js');
83
213
  const paths = ensureProjectDir();
84
214
  const socketPath = paths.socketPath;
85
215
  const dbPath = paths.dbPath;
@@ -108,7 +238,14 @@ program
108
238
  if (options.dashboard !== false) {
109
239
  const port = parseInt(options.port, 10);
110
240
  const { startDashboard } = await import('../dashboard/server.js');
111
- const actualPort = await startDashboard(port, paths.dataDir, paths.teamDir, dbPath);
241
+ const actualPort = await startDashboard({
242
+ port,
243
+ dataDir: paths.dataDir,
244
+ teamDir: paths.teamDir,
245
+ dbPath,
246
+ enableSpawner: true,
247
+ projectRoot: paths.projectRoot,
248
+ });
112
249
  console.log(`Dashboard: http://localhost:${actualPort}`);
113
250
  }
114
251
  console.log('Press Ctrl+C to stop.');
@@ -313,7 +450,7 @@ program
313
450
  messages.forEach((msg) => {
314
451
  const ts = new Date(msg.ts).toISOString();
315
452
  const body = msg.body.length > 120 ? `${msg.body.slice(0, 117)}...` : msg.body;
316
- console.log(`${ts} ${msg.from} -> ${msg.to}: ${body}`);
453
+ console.log(`${ts} ${msg.from} -> ${msg.to}:${body}`);
317
454
  });
318
455
  }
319
456
  finally {
@@ -327,6 +464,198 @@ program
327
464
  .action(() => {
328
465
  console.log(`agent-relay v${VERSION}`);
329
466
  });
467
+ // bridge - Multi-project orchestration
468
+ program
469
+ .command('bridge')
470
+ .description('Bridge multiple projects as orchestrator')
471
+ .argument('[projects...]', 'Project paths to bridge')
472
+ .option('--cli <tool>', 'CLI tool override for all projects')
473
+ .action(async (projectPaths, options) => {
474
+ const { resolveProjects, validateDaemons } = await import('../bridge/config.js');
475
+ const { MultiProjectClient } = await import('../bridge/multi-project-client.js');
476
+ const { getProjectPaths } = await import('../utils/project-namespace.js');
477
+ const fs = await import('node:fs');
478
+ const pathModule = await import('node:path');
479
+ // Resolve projects from args or config
480
+ const projects = resolveProjects(projectPaths, options.cli);
481
+ if (projects.length === 0) {
482
+ console.error('No projects specified.');
483
+ console.error('Usage: agent-relay bridge ~/project1 ~/project2');
484
+ console.error(' or: Create ~/.agent-relay/bridge.json with project config');
485
+ process.exit(1);
486
+ }
487
+ console.log('Bridge Mode - Multi-Project Orchestration');
488
+ console.log('─'.repeat(40));
489
+ // Check which daemons are running
490
+ const { valid, missing } = validateDaemons(projects);
491
+ if (missing.length > 0) {
492
+ console.error('\nMissing daemons for:');
493
+ for (const p of missing) {
494
+ console.error(` - ${p.path}`);
495
+ console.error(` Run: cd "${p.path}" && agent-relay up`);
496
+ }
497
+ console.error('');
498
+ }
499
+ if (valid.length === 0) {
500
+ console.error('No projects have running daemons. Start them first.');
501
+ process.exit(1);
502
+ }
503
+ console.log('\nConnecting to projects:');
504
+ for (const p of valid) {
505
+ console.log(` - ${p.id} (${p.path})`);
506
+ console.log(` Lead: ${p.leadName}, CLI: ${p.cli}`);
507
+ }
508
+ console.log('');
509
+ // Get data directories for ALL bridged projects (so each project's dashboard can show bridge state)
510
+ const bridgeStatePaths = valid.map(p => {
511
+ const projectPaths = getProjectPaths(p.path);
512
+ // Ensure directory exists
513
+ if (!fs.existsSync(projectPaths.dataDir)) {
514
+ fs.mkdirSync(projectPaths.dataDir, { recursive: true });
515
+ }
516
+ return pathModule.join(projectPaths.dataDir, 'bridge-state.json');
517
+ });
518
+ const bridgeState = {
519
+ projects: valid.map(p => ({
520
+ id: p.id,
521
+ name: pathModule.basename(p.path),
522
+ path: p.path,
523
+ connected: false,
524
+ lead: { name: p.leadName, connected: false },
525
+ agents: [],
526
+ })),
527
+ messages: [],
528
+ connected: false,
529
+ startedAt: new Date().toISOString(),
530
+ };
531
+ // Write bridge state to ALL project data directories
532
+ const writeBridgeState = () => {
533
+ const stateJson = JSON.stringify(bridgeState, null, 2);
534
+ for (const statePath of bridgeStatePaths) {
535
+ try {
536
+ fs.writeFileSync(statePath, stateJson);
537
+ }
538
+ catch (err) {
539
+ console.error(`[bridge] Failed to write state to ${statePath}:`, err);
540
+ }
541
+ }
542
+ };
543
+ // Initial state write
544
+ writeBridgeState();
545
+ console.log(`Bridge state written to ${bridgeStatePaths.length} project(s)`);
546
+ // Connect to all project daemons
547
+ const client = new MultiProjectClient(valid);
548
+ // Track connection state changes (daemon connection, not agent registration)
549
+ // Also track "reconnecting" state for UI feedback
550
+ const wasConnected = new Map();
551
+ client.onProjectStateChange = (projectId, connected) => {
552
+ const project = bridgeState.projects.find(p => p.id === projectId);
553
+ if (project) {
554
+ const hadConnection = wasConnected.get(projectId) || false;
555
+ project.connected = connected;
556
+ // Set reconnecting if we lost connection (had it before, now disconnected)
557
+ project.reconnecting = !connected && hadConnection;
558
+ wasConnected.set(projectId, connected);
559
+ // Note: lead.connected should only be true when an actual lead agent registers
560
+ // The bridge connecting to daemon doesn't mean a lead agent is active
561
+ }
562
+ bridgeState.connected = bridgeState.projects.some(p => p.connected);
563
+ writeBridgeState();
564
+ };
565
+ try {
566
+ await client.connect();
567
+ }
568
+ catch (err) {
569
+ console.error('Failed to connect to all projects');
570
+ writeBridgeState(); // Write final state before exit
571
+ process.exit(1);
572
+ }
573
+ bridgeState.connected = true;
574
+ writeBridgeState();
575
+ console.log('Connected to all projects.');
576
+ console.log('');
577
+ console.log('Cross-project messaging:');
578
+ console.log(' @relay:projectId:agent Message');
579
+ console.log(' @relay:*:lead Broadcast to all leads');
580
+ console.log('');
581
+ // Handle messages from projects
582
+ client.onMessage = (projectId, from, payload, messageId) => {
583
+ console.log(`[${projectId}] ${from}: ${payload.body.substring(0, 80)}...`);
584
+ // Track message in bridge state
585
+ bridgeState.messages.push({
586
+ id: messageId,
587
+ from,
588
+ to: '*', // Incoming messages are from agents
589
+ body: payload.body,
590
+ sourceProject: projectId,
591
+ timestamp: new Date().toISOString(),
592
+ });
593
+ // Keep last 100 messages
594
+ if (bridgeState.messages.length > 100) {
595
+ bridgeState.messages = bridgeState.messages.slice(-100);
596
+ }
597
+ writeBridgeState();
598
+ };
599
+ // Clean up on exit
600
+ const cleanup = () => {
601
+ bridgeState.connected = false;
602
+ bridgeState.projects.forEach(p => {
603
+ p.connected = false;
604
+ if (p.lead)
605
+ p.lead.connected = false;
606
+ });
607
+ writeBridgeState();
608
+ };
609
+ // Keep running
610
+ process.on('SIGINT', () => {
611
+ console.log('\nDisconnecting...');
612
+ cleanup();
613
+ client.disconnect();
614
+ process.exit(0);
615
+ });
616
+ // Start a simple REPL for sending messages
617
+ const readline = await import('node:readline');
618
+ const rl = readline.createInterface({
619
+ input: process.stdin,
620
+ output: process.stdout,
621
+ });
622
+ console.log('Enter messages as: projectId:agent message');
623
+ console.log('Or: *:lead message (broadcast to all leads)');
624
+ console.log('Type "quit" to exit.\n');
625
+ const promptForInput = () => {
626
+ rl.question('> ', (input) => {
627
+ if (input.toLowerCase() === 'quit') {
628
+ client.disconnect();
629
+ rl.close();
630
+ process.exit(0);
631
+ }
632
+ // Parse input: projectId:agent message
633
+ const match = input.match(/^(\S+):(\S+)\s+(.+)$/);
634
+ if (match) {
635
+ const [, projectId, agent, message] = match;
636
+ if (projectId === '*' && agent === 'lead') {
637
+ client.broadcastToLeads(message);
638
+ console.log('→ Broadcast to all leads');
639
+ }
640
+ else if (projectId === '*') {
641
+ client.broadcastAll(message);
642
+ console.log('→ Broadcast to all');
643
+ }
644
+ else {
645
+ const sent = client.sendToProject(projectId, agent, message);
646
+ if (sent) {
647
+ console.log(`→ ${projectId}:${agent}`);
648
+ }
649
+ }
650
+ }
651
+ else {
652
+ console.log('Format: projectId:agent message');
653
+ }
654
+ promptForInput();
655
+ });
656
+ };
657
+ promptForInput();
658
+ });
330
659
  // gc - Clean up orphaned tmux sessions (hidden - for agent use)
331
660
  program
332
661
  .command('gc', { hidden: true })
@@ -532,5 +861,235 @@ function parseSince(input) {
532
861
  return undefined;
533
862
  return parsed;
534
863
  }
864
+ // ============================================
865
+ // Spawn/Worker debugging commands
866
+ // ============================================
867
+ const WORKER_SESSION = 'relay-workers';
868
+ // workers - List spawned workers
869
+ program
870
+ .command('workers')
871
+ .description('List spawned worker agents (from tmux)')
872
+ .option('--json', 'Output as JSON')
873
+ .action(async (options) => {
874
+ try {
875
+ // Check if worker session exists
876
+ try {
877
+ await execAsync(`tmux has-session -t ${WORKER_SESSION} 2>/dev/null`);
878
+ }
879
+ catch {
880
+ if (options.json) {
881
+ console.log(JSON.stringify({ workers: [], session: null }));
882
+ }
883
+ else {
884
+ console.log('No spawned workers (session does not exist)');
885
+ }
886
+ return;
887
+ }
888
+ // List windows in the worker session
889
+ const { stdout } = await execAsync(`tmux list-windows -t ${WORKER_SESSION} -F "#{window_index}|#{window_name}|#{pane_current_command}|#{window_activity}"`);
890
+ const workers = stdout
891
+ .split('\n')
892
+ .filter(Boolean)
893
+ .map(line => {
894
+ const [index, name, command, activity] = line.split('|');
895
+ const activityTs = parseInt(activity, 10) * 1000;
896
+ const lastActive = isNaN(activityTs) ? undefined : new Date(activityTs).toISOString();
897
+ return {
898
+ index: parseInt(index, 10),
899
+ name,
900
+ command,
901
+ lastActive,
902
+ window: `${WORKER_SESSION}:${name}`,
903
+ };
904
+ })
905
+ // Filter out the default zsh window
906
+ .filter(w => w.name !== 'zsh' && w.command !== 'zsh');
907
+ if (options.json) {
908
+ console.log(JSON.stringify({ workers, session: WORKER_SESSION }, null, 2));
909
+ return;
910
+ }
911
+ if (!workers.length) {
912
+ console.log('No spawned workers');
913
+ return;
914
+ }
915
+ console.log('SPAWNED WORKERS');
916
+ console.log('─'.repeat(50));
917
+ console.log('NAME COMMAND WINDOW');
918
+ console.log('─'.repeat(50));
919
+ workers.forEach(w => {
920
+ const name = w.name.padEnd(15);
921
+ const cmd = (w.command || '-').padEnd(12);
922
+ console.log(`${name} ${cmd} ${w.window}`);
923
+ });
924
+ console.log('');
925
+ console.log('Commands:');
926
+ console.log(' agent-relay workers:logs <name> - View worker output');
927
+ console.log(' agent-relay workers:attach <name> - Attach to worker tmux');
928
+ console.log(' agent-relay workers:kill <name> - Kill a worker');
929
+ }
930
+ catch (err) {
931
+ console.error('Failed to list workers:', err.message);
932
+ }
933
+ });
934
+ // workers:logs - Show tmux pane output for a worker
935
+ program
936
+ .command('workers:logs')
937
+ .description('Show recent output from a spawned worker')
938
+ .argument('<name>', 'Worker name')
939
+ .option('-n, --lines <n>', 'Number of lines to show', '50')
940
+ .option('-f, --follow', 'Follow output (like tail -f)')
941
+ .action(async (name, options) => {
942
+ const window = `${WORKER_SESSION}:${name}`;
943
+ try {
944
+ // Check if window exists
945
+ await execAsync(`tmux has-session -t ${window} 2>/dev/null`);
946
+ }
947
+ catch {
948
+ console.error(`Worker "${name}" not found`);
949
+ console.log(`Run 'agent-relay workers' to see available workers`);
950
+ process.exit(1);
951
+ }
952
+ if (options.follow) {
953
+ console.log(`Following output from ${window} (Ctrl+C to stop)...`);
954
+ console.log('─'.repeat(50));
955
+ // Use a polling approach to follow
956
+ let lastContent = '';
957
+ const poll = async () => {
958
+ try {
959
+ const { stdout } = await execAsync(`tmux capture-pane -t ${window} -p -S -100`);
960
+ if (stdout !== lastContent) {
961
+ // Print only new lines
962
+ const newContent = stdout.replace(lastContent, '');
963
+ if (newContent.trim()) {
964
+ process.stdout.write(newContent);
965
+ }
966
+ lastContent = stdout;
967
+ }
968
+ }
969
+ catch {
970
+ console.error('\nWorker disconnected');
971
+ process.exit(1);
972
+ }
973
+ };
974
+ const interval = setInterval(poll, 500);
975
+ process.on('SIGINT', () => {
976
+ clearInterval(interval);
977
+ console.log('\nStopped following');
978
+ process.exit(0);
979
+ });
980
+ await poll(); // Initial fetch
981
+ await new Promise(() => { }); // Keep running
982
+ }
983
+ else {
984
+ try {
985
+ const lines = parseInt(options.lines || '50', 10);
986
+ const { stdout } = await execAsync(`tmux capture-pane -t ${window} -p -S -${lines}`);
987
+ console.log(`Output from ${window} (last ${lines} lines):`);
988
+ console.log('─'.repeat(50));
989
+ console.log(stdout || '(empty)');
990
+ }
991
+ catch (err) {
992
+ console.error('Failed to capture output:', err.message);
993
+ }
994
+ }
995
+ });
996
+ // workers:attach - Attach to a worker's tmux window
997
+ program
998
+ .command('workers:attach')
999
+ .description('Attach to a spawned worker tmux window')
1000
+ .argument('<name>', 'Worker name')
1001
+ .action(async (name) => {
1002
+ const window = `${WORKER_SESSION}:${name}`;
1003
+ try {
1004
+ // Check if window exists
1005
+ await execAsync(`tmux has-session -t ${window} 2>/dev/null`);
1006
+ }
1007
+ catch {
1008
+ console.error(`Worker "${name}" not found`);
1009
+ console.log(`Run 'agent-relay workers' to see available workers`);
1010
+ process.exit(1);
1011
+ }
1012
+ console.log(`Attaching to ${window}...`);
1013
+ console.log('(Use Ctrl+B D to detach)');
1014
+ // Spawn tmux attach as a child process with stdio inherited
1015
+ const { spawn } = await import('child_process');
1016
+ const child = spawn('tmux', ['attach-session', '-t', window], {
1017
+ stdio: 'inherit',
1018
+ });
1019
+ child.on('exit', (code) => {
1020
+ process.exit(code || 0);
1021
+ });
1022
+ });
1023
+ // workers:kill - Kill a spawned worker
1024
+ program
1025
+ .command('workers:kill')
1026
+ .description('Kill a spawned worker')
1027
+ .argument('<name>', 'Worker name')
1028
+ .option('--force', 'Skip graceful shutdown, kill immediately')
1029
+ .action(async (name, options) => {
1030
+ const window = `${WORKER_SESSION}:${name}`;
1031
+ try {
1032
+ // Check if window exists
1033
+ await execAsync(`tmux has-session -t ${window} 2>/dev/null`);
1034
+ }
1035
+ catch {
1036
+ console.error(`Worker "${name}" not found`);
1037
+ console.log(`Run 'agent-relay workers' to see available workers`);
1038
+ process.exit(1);
1039
+ }
1040
+ if (!options.force) {
1041
+ // Try graceful shutdown first
1042
+ console.log(`Sending /exit to ${name}...`);
1043
+ try {
1044
+ await execAsync(`tmux send-keys -t ${window} '/exit' Enter`);
1045
+ // Wait for graceful shutdown
1046
+ await new Promise(r => setTimeout(r, 2000));
1047
+ }
1048
+ catch {
1049
+ // Ignore errors, will force kill below
1050
+ }
1051
+ }
1052
+ // Kill the window
1053
+ try {
1054
+ await execAsync(`tmux kill-window -t ${window}`);
1055
+ console.log(`Killed worker: ${name}`);
1056
+ }
1057
+ catch (err) {
1058
+ console.error(`Failed to kill ${name}:`, err.message);
1059
+ process.exit(1);
1060
+ }
1061
+ });
1062
+ // workers:session - Show tmux session info
1063
+ program
1064
+ .command('workers:session')
1065
+ .description('Show worker tmux session details')
1066
+ .action(async () => {
1067
+ try {
1068
+ // Check if session exists
1069
+ try {
1070
+ await execAsync(`tmux has-session -t ${WORKER_SESSION} 2>/dev/null`);
1071
+ }
1072
+ catch {
1073
+ console.log(`Session "${WORKER_SESSION}" does not exist`);
1074
+ console.log('Spawn a worker to create it.');
1075
+ return;
1076
+ }
1077
+ console.log(`Session: ${WORKER_SESSION}`);
1078
+ console.log('─'.repeat(50));
1079
+ // Get session info
1080
+ const { stdout: sessionInfo } = await execAsync(`tmux display-message -t ${WORKER_SESSION} -p "Created: #{session_created_string}\\nWindows: #{session_windows}\\nAttached: #{?session_attached,yes,no}"`);
1081
+ console.log(sessionInfo);
1082
+ // List windows
1083
+ console.log('\nWindows:');
1084
+ const { stdout: windows } = await execAsync(`tmux list-windows -t ${WORKER_SESSION} -F " #{window_index}: #{window_name} (#{pane_current_command})"`);
1085
+ console.log(windows || ' (none)');
1086
+ console.log('\nQuick commands:');
1087
+ console.log(` tmux attach -t ${WORKER_SESSION} # Attach to session`);
1088
+ console.log(` tmux kill-session -t ${WORKER_SESSION} # Kill entire session`);
1089
+ }
1090
+ catch (err) {
1091
+ console.error('Failed:', err.message);
1092
+ }
1093
+ });
535
1094
  program.parse();
536
1095
  //# sourceMappingURL=index.js.map