agent-relay 1.0.9 → 1.0.12

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 (94) hide show
  1. package/README.md +27 -56
  2. package/bin/.gitkeep +0 -0
  3. package/bin/tmux +0 -0
  4. package/dist/bridge/config.d.ts.map +1 -1
  5. package/dist/bridge/multi-project-client.d.ts.map +1 -1
  6. package/dist/bridge/multi-project-client.js +1 -1
  7. package/dist/bridge/multi-project-client.js.map +1 -1
  8. package/dist/bridge/spawner.d.ts +47 -9
  9. package/dist/bridge/spawner.d.ts.map +1 -1
  10. package/dist/bridge/spawner.js +177 -73
  11. package/dist/bridge/spawner.js.map +1 -1
  12. package/dist/bridge/types.d.ts +4 -2
  13. package/dist/bridge/types.d.ts.map +1 -1
  14. package/dist/cli/index.js +192 -295
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/daemon/router.d.ts.map +1 -1
  17. package/dist/daemon/router.js.map +1 -1
  18. package/dist/dashboard-server/metrics.d.ts.map +1 -0
  19. package/dist/dashboard-server/metrics.js.map +1 -0
  20. package/dist/dashboard-server/needs-attention.d.ts.map +1 -0
  21. package/dist/dashboard-server/needs-attention.js.map +1 -0
  22. package/dist/dashboard-server/server.d.ts.map +1 -0
  23. package/dist/{dashboard → dashboard-server}/server.js +20 -14
  24. package/dist/dashboard-server/server.js.map +1 -0
  25. package/dist/dashboard-server/start.d.ts.map +1 -0
  26. package/dist/dashboard-server/start.js.map +1 -0
  27. package/dist/utils/tmux-resolver.d.ts +55 -0
  28. package/dist/utils/tmux-resolver.d.ts.map +1 -0
  29. package/dist/utils/tmux-resolver.js +175 -0
  30. package/dist/utils/tmux-resolver.js.map +1 -0
  31. package/dist/utils/update-checker.d.ts +26 -0
  32. package/dist/utils/update-checker.d.ts.map +1 -0
  33. package/dist/utils/update-checker.js +174 -0
  34. package/dist/utils/update-checker.js.map +1 -0
  35. package/dist/wrapper/pty-wrapper.d.ts +129 -0
  36. package/dist/wrapper/pty-wrapper.d.ts.map +1 -0
  37. package/dist/wrapper/pty-wrapper.js +442 -0
  38. package/dist/wrapper/pty-wrapper.js.map +1 -0
  39. package/dist/wrapper/tmux-wrapper.d.ts +2 -1
  40. package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
  41. package/dist/wrapper/tmux-wrapper.js +23 -19
  42. package/dist/wrapper/tmux-wrapper.js.map +1 -1
  43. package/docs/AGENTS.md +43 -30
  44. package/docs/INTEGRATION-GUIDE.md +1 -1
  45. package/docs/PROTOCOL.md +16 -10
  46. package/docs/agent-relay-snippet.md +21 -17
  47. package/package.json +10 -6
  48. package/scripts/dev/PUBLIC_RELEASE_PLAN.md +88 -0
  49. package/scripts/dev/dev-team-setup.sh +431 -0
  50. package/scripts/e2e-test.sh +119 -0
  51. package/scripts/games/game-protocol.md +79 -0
  52. package/scripts/games/hearts-setup.sh +264 -0
  53. package/scripts/postinstall.js +254 -0
  54. package/scripts/tictactoe-setup.sh +181 -0
  55. package/dist/dashboard/metrics.d.ts.map +0 -1
  56. package/dist/dashboard/metrics.js.map +0 -1
  57. package/dist/dashboard/needs-attention.d.ts.map +0 -1
  58. package/dist/dashboard/needs-attention.js.map +0 -1
  59. package/dist/dashboard/public/bridge.html +0 -1272
  60. package/dist/dashboard/public/index.html +0 -2262
  61. package/dist/dashboard/public/js/app.js +0 -184
  62. package/dist/dashboard/public/js/app.js.map +0 -7
  63. package/dist/dashboard/public/metrics.html +0 -999
  64. package/dist/dashboard/server.d.ts.map +0 -1
  65. package/dist/dashboard/server.js.map +0 -1
  66. package/dist/dashboard/start.d.ts.map +0 -1
  67. package/dist/dashboard/start.js.map +0 -1
  68. package/dist/dashboard-v2/index.d.ts +0 -10
  69. package/dist/dashboard-v2/index.d.ts.map +0 -1
  70. package/dist/dashboard-v2/index.js +0 -54
  71. package/dist/dashboard-v2/index.js.map +0 -1
  72. package/dist/dashboard-v2/lib/api.d.ts +0 -95
  73. package/dist/dashboard-v2/lib/api.d.ts.map +0 -1
  74. package/dist/dashboard-v2/lib/api.js +0 -270
  75. package/dist/dashboard-v2/lib/api.js.map +0 -1
  76. package/dist/dashboard-v2/lib/colors.d.ts +0 -61
  77. package/dist/dashboard-v2/lib/colors.d.ts.map +0 -1
  78. package/dist/dashboard-v2/lib/colors.js +0 -198
  79. package/dist/dashboard-v2/lib/colors.js.map +0 -1
  80. package/dist/dashboard-v2/lib/hierarchy.d.ts +0 -74
  81. package/dist/dashboard-v2/lib/hierarchy.d.ts.map +0 -1
  82. package/dist/dashboard-v2/lib/hierarchy.js +0 -196
  83. package/dist/dashboard-v2/lib/hierarchy.js.map +0 -1
  84. package/dist/dashboard-v2/types/index.d.ts +0 -154
  85. package/dist/dashboard-v2/types/index.d.ts.map +0 -1
  86. package/dist/dashboard-v2/types/index.js +0 -6
  87. package/dist/dashboard-v2/types/index.js.map +0 -1
  88. /package/dist/{dashboard → dashboard-server}/metrics.d.ts +0 -0
  89. /package/dist/{dashboard → dashboard-server}/metrics.js +0 -0
  90. /package/dist/{dashboard → dashboard-server}/needs-attention.d.ts +0 -0
  91. /package/dist/{dashboard → dashboard-server}/needs-attention.js +0 -0
  92. /package/dist/{dashboard → dashboard-server}/server.d.ts +0 -0
  93. /package/dist/{dashboard → dashboard-server}/start.d.ts +0 -0
  94. /package/dist/{dashboard → dashboard-server}/start.js +0 -0
package/dist/cli/index.js CHANGED
@@ -15,6 +15,9 @@ import { config as dotenvConfig } from 'dotenv';
15
15
  import { Daemon } from '../daemon/server.js';
16
16
  import { RelayClient } from '../wrapper/client.js';
17
17
  import { generateAgentName } from '../utils/name-generator.js';
18
+ import { getTmuxPath } from '../utils/tmux-resolver.js';
19
+ import { readWorkersMetadata, getWorkerLogsDir } from '../bridge/spawner.js';
20
+ import { checkForUpdatesInBackground, checkForUpdates } from '../utils/update-checker.js';
18
21
  import fs from 'node:fs';
19
22
  import path from 'node:path';
20
23
  import { promisify } from 'node:util';
@@ -29,6 +32,14 @@ const packageJsonPath = path.resolve(__dirname, '../../package.json');
29
32
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
30
33
  const VERSION = packageJson.version;
31
34
  const execAsync = promisify(exec);
35
+ // Check for updates in background (non-blocking)
36
+ // Only show notification for interactive commands, not when wrapping agents or running update
37
+ const interactiveCommands = ['up', 'down', 'status', 'agents', 'who', 'version', '--version', '-V', '--help', '-h'];
38
+ const shouldCheckUpdates = process.argv.length > 2 &&
39
+ interactiveCommands.includes(process.argv[2]);
40
+ if (shouldCheckUpdates) {
41
+ checkForUpdatesInBackground(VERSION);
42
+ }
32
43
  const program = new Command();
33
44
  function pidFilePathForSocket(socketPath) {
34
45
  return `${socketPath}.pid`;
@@ -91,7 +102,7 @@ program
91
102
  requestedBy: agentName,
92
103
  });
93
104
  if (result.success) {
94
- console.error(`[${agentName}] ✓ Spawned ${workerName} in ${result.window}`);
105
+ console.error(`[${agentName}] ✓ Spawned ${workerName} [pid: ${result.pid}]`);
95
106
  }
96
107
  else {
97
108
  console.error(`[${agentName}] ✗ Failed to spawn ${workerName}: ${result.error}`);
@@ -115,93 +126,6 @@ program
115
126
  });
116
127
  await wrapper.start();
117
128
  });
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
- }
205
129
  // up - Start daemon + dashboard
206
130
  program
207
131
  .command('up')
@@ -209,7 +133,7 @@ program
209
133
  .option('--no-dashboard', 'Disable web dashboard')
210
134
  .option('--port <port>', 'Dashboard port', DEFAULT_DASHBOARD_PORT)
211
135
  .action(async (options) => {
212
- const { getProjectPaths, ensureProjectDir } = await import('../utils/project-namespace.js');
136
+ const { ensureProjectDir } = await import('../utils/project-namespace.js');
213
137
  const paths = ensureProjectDir();
214
138
  const socketPath = paths.socketPath;
215
139
  const dbPath = paths.dbPath;
@@ -237,7 +161,7 @@ program
237
161
  // Dashboard starts by default (use --no-dashboard to disable)
238
162
  if (options.dashboard !== false) {
239
163
  const port = parseInt(options.port, 10);
240
- const { startDashboard } = await import('../dashboard/server.js');
164
+ const { startDashboard } = await import('../dashboard-server/server.js');
241
165
  const actualPort = await startDashboard({
242
166
  port,
243
167
  dataDir: paths.dataDir,
@@ -308,38 +232,73 @@ program
308
232
  logRelaySessions(relaySessions);
309
233
  }
310
234
  });
311
- // agents - List connected agents (from registry file)
235
+ // agents - List connected agents (from registry file) and spawned workers
312
236
  program
313
237
  .command('agents')
314
- .description('List connected agents')
238
+ .description('List connected agents and spawned workers')
315
239
  .option('--all', 'Include internal/CLI agents')
316
240
  .option('--json', 'Output as JSON')
317
241
  .action(async (options) => {
318
242
  const { getProjectPaths } = await import('../utils/project-namespace.js');
319
243
  const paths = getProjectPaths();
320
244
  const agentsPath = path.join(paths.teamDir, 'agents.json');
245
+ // Load registered agents
321
246
  const allAgents = loadAgents(agentsPath);
322
247
  const agents = options.all
323
248
  ? allAgents
324
249
  : allAgents.filter(isVisibleAgent);
250
+ // Load spawned workers
251
+ const workers = readWorkersMetadata(paths.projectRoot);
252
+ const combined = [];
253
+ // Add registered agents
254
+ agents.forEach((agent) => {
255
+ const worker = workers.find(w => w.name === agent.name);
256
+ combined.push({
257
+ name: agent.name ?? 'unknown',
258
+ status: getAgentStatus(agent),
259
+ cli: agent.cli ?? '-',
260
+ lastSeen: agent.lastSeen,
261
+ spawnedBy: worker?.spawnedBy,
262
+ pid: worker?.pid,
263
+ });
264
+ });
265
+ // Add workers not in registry (orphaned or not yet registered)
266
+ workers.forEach((worker) => {
267
+ const existsInAgents = agents.some(a => a.name === worker.name);
268
+ if (!existsInAgents) {
269
+ combined.push({
270
+ name: worker.name || 'unknown',
271
+ status: 'ONLINE',
272
+ cli: worker.cli || '-',
273
+ spawnedBy: worker.spawnedBy,
274
+ pid: worker.pid,
275
+ });
276
+ }
277
+ });
325
278
  if (options.json) {
326
- console.log(JSON.stringify(agents.map(a => ({ ...a, status: getAgentStatus(a) })), null, 2));
279
+ console.log(JSON.stringify(combined, null, 2));
327
280
  return;
328
281
  }
329
- if (!agents.length) {
282
+ if (!combined.length) {
330
283
  const hint = options.all ? '' : ' (use --all to include internal/cli agents)';
331
284
  console.log(`No agents found. Ensure the daemon is running and agents are connected${hint}.`);
332
285
  return;
333
286
  }
334
- console.log('NAME STATUS CLI LAST SEEN');
335
- console.log('---------------------------------------------');
336
- agents.forEach((agent) => {
337
- const name = (agent.name ?? 'unknown').padEnd(15);
338
- const status = getAgentStatus(agent).padEnd(8);
339
- const cli = (agent.cli ?? '-').padEnd(8);
340
- const lastSeen = formatRelativeTime(agent.lastSeen);
341
- console.log(`${name} ${status} ${cli} ${lastSeen}`);
287
+ console.log('NAME STATUS CLI PARENT');
288
+ console.log(''.repeat(50));
289
+ combined.forEach((agent) => {
290
+ const name = agent.name.padEnd(15);
291
+ const status = agent.status.padEnd(8);
292
+ const cli = agent.cli.padEnd(9);
293
+ const parent = agent.spawnedBy ?? '-';
294
+ console.log(`${name} ${status} ${cli} ${parent}`);
342
295
  });
296
+ if (workers.length > 0) {
297
+ console.log('');
298
+ console.log('Commands:');
299
+ console.log(' agent-relay agents:logs <name> - View spawned agent output');
300
+ console.log(' agent-relay agents:kill <name> - Kill a spawned agent');
301
+ }
343
302
  });
344
303
  // who - Show currently active agents (online within last 30s)
345
304
  program
@@ -464,6 +423,67 @@ program
464
423
  .action(() => {
465
424
  console.log(`agent-relay v${VERSION}`);
466
425
  });
426
+ // update - Check for updates and optionally install
427
+ program
428
+ .command('update')
429
+ .description('Check for updates and install if available')
430
+ .option('--check', 'Only check for updates, do not install')
431
+ .action(async (options) => {
432
+ console.log(`Current version: ${VERSION}`);
433
+ console.log('Checking for updates...');
434
+ const info = await checkForUpdates(VERSION);
435
+ if (info.error) {
436
+ console.error(`Failed to check for updates: ${info.error}`);
437
+ process.exit(1);
438
+ }
439
+ if (!info.updateAvailable) {
440
+ console.log('You are running the latest version.');
441
+ return;
442
+ }
443
+ console.log(`New version available: ${info.latestVersion}`);
444
+ if (options.check) {
445
+ console.log('Run `agent-relay update` to install.');
446
+ return;
447
+ }
448
+ console.log('Installing update...');
449
+ try {
450
+ const { stdout, stderr } = await execAsync('npm install -g agent-relay@latest');
451
+ if (stdout)
452
+ console.log(stdout);
453
+ if (stderr)
454
+ console.error(stderr);
455
+ console.log(`Successfully updated to ${info.latestVersion}`);
456
+ }
457
+ catch (err) {
458
+ console.error('Failed to install update:', err.message);
459
+ console.log('Try running manually: npm install -g agent-relay@latest');
460
+ process.exit(1);
461
+ }
462
+ });
463
+ // check-tmux - Check tmux availability (hidden - for diagnostics)
464
+ program
465
+ .command('check-tmux', { hidden: true })
466
+ .description('Check tmux availability and version')
467
+ .action(async () => {
468
+ const { resolveTmux, checkTmuxVersion } = await import('../utils/tmux-resolver.js');
469
+ const info = resolveTmux();
470
+ if (!info) {
471
+ console.log('tmux: NOT FOUND');
472
+ console.log('');
473
+ console.log('Install tmux, then reinstall agent-relay:');
474
+ console.log(' brew install tmux # macOS');
475
+ console.log(' apt install tmux # Ubuntu/Debian');
476
+ console.log(' npm install agent-relay # Reinstall to bundle tmux');
477
+ process.exit(1);
478
+ }
479
+ console.log(`tmux: ${info.path}`);
480
+ console.log(`Version: ${info.version}`);
481
+ console.log(`Source: ${info.isBundled ? 'bundled' : 'system'}`);
482
+ const versionCheck = checkTmuxVersion();
483
+ if (!versionCheck.ok) {
484
+ console.log(`Warning: tmux ${versionCheck.minimum}+ recommended`);
485
+ }
486
+ });
467
487
  // bridge - Multi-project orchestration
468
488
  program
469
489
  .command('bridge')
@@ -565,7 +585,7 @@ program
565
585
  try {
566
586
  await client.connect();
567
587
  }
568
- catch (err) {
588
+ catch (_err) {
569
589
  console.error('Failed to connect to all projects');
570
590
  writeBridgeState(); // Write final state before exit
571
591
  process.exit(1);
@@ -704,9 +724,10 @@ program
704
724
  }
705
725
  // Kill orphaned sessions
706
726
  let killed = 0;
727
+ const tmuxPath = getTmuxPath();
707
728
  for (const session of orphaned) {
708
729
  try {
709
- await execAsync(`tmux kill-session -t ${session.sessionName}`);
730
+ await execAsync(`"${tmuxPath}" kill-session -t ${session.sessionName}`);
710
731
  killed++;
711
732
  console.log(`Killed: ${session.sessionName}`);
712
733
  }
@@ -718,7 +739,8 @@ program
718
739
  });
719
740
  async function discoverRelaySessions() {
720
741
  try {
721
- const { stdout } = await execAsync('tmux list-sessions -F "#{session_name}"');
742
+ const tmuxPath = getTmuxPath();
743
+ const { stdout } = await execAsync(`"${tmuxPath}" list-sessions -F "#{session_name}"`);
722
744
  const sessionNames = stdout
723
745
  .split('\n')
724
746
  .map(s => s.trim())
@@ -734,7 +756,7 @@ async function discoverRelaySessions() {
734
756
  return await Promise.all(relaySessions.map(async (session) => {
735
757
  let cwd;
736
758
  try {
737
- const { stdout: cwdOut } = await execAsync(`tmux display-message -t ${session.sessionName} -p '#{pane_current_path}'`);
759
+ const { stdout: cwdOut } = await execAsync(`"${tmuxPath}" display-message -t ${session.sessionName} -p '#{pane_current_path}'`);
738
760
  cwd = cwdOut.trim() || undefined;
739
761
  }
740
762
  catch {
@@ -862,233 +884,108 @@ function parseSince(input) {
862
884
  return parsed;
863
885
  }
864
886
  // ============================================
865
- // Spawn/Worker debugging commands
887
+ // Spawned agent debugging commands
866
888
  // ============================================
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
889
+ // agents:logs - Show log file output for a spawned agent
935
890
  program
936
- .command('workers:logs')
937
- .description('Show recent output from a spawned worker')
938
- .argument('<name>', 'Worker name')
891
+ .command('agents:logs')
892
+ .description('Show recent output from a spawned agent')
893
+ .argument('<name>', 'Agent name')
939
894
  .option('-n, --lines <n>', 'Number of lines to show', '50')
940
895
  .option('-f, --follow', 'Follow output (like tail -f)')
941
896
  .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`);
897
+ const { getProjectPaths } = await import('../utils/project-namespace.js');
898
+ const paths = getProjectPaths();
899
+ const logsDir = getWorkerLogsDir(paths.projectRoot);
900
+ const logFile = path.join(logsDir, `${name}.log`);
901
+ if (!fs.existsSync(logFile)) {
902
+ console.error(`No logs found for agent "${name}"`);
903
+ console.log(`Log file not found: ${logFile}`);
904
+ console.log(`Run 'agent-relay agents' to see available agents`);
950
905
  process.exit(1);
951
906
  }
952
907
  if (options.follow) {
953
- console.log(`Following output from ${window} (Ctrl+C to stop)...`);
908
+ console.log(`Following logs for ${name} (Ctrl+C to stop)...`);
954
909
  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);
910
+ // Use tail -f approach
911
+ const { spawn } = await import('child_process');
912
+ const child = spawn('tail', ['-f', logFile], {
913
+ stdio: ['ignore', 'inherit', 'inherit'],
914
+ });
975
915
  process.on('SIGINT', () => {
976
- clearInterval(interval);
916
+ child.kill();
977
917
  console.log('\nStopped following');
978
918
  process.exit(0);
979
919
  });
980
- await poll(); // Initial fetch
981
- await new Promise(() => { }); // Keep running
920
+ child.on('exit', () => {
921
+ process.exit(0);
922
+ });
982
923
  }
983
924
  else {
984
925
  try {
985
926
  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):`);
927
+ const { stdout } = await execAsync(`tail -n ${lines} "${logFile}"`);
928
+ console.log(`Logs for ${name} (last ${lines} lines):`);
988
929
  console.log('─'.repeat(50));
989
930
  console.log(stdout || '(empty)');
990
931
  }
991
932
  catch (err) {
992
- console.error('Failed to capture output:', err.message);
933
+ console.error('Failed to read logs:', err.message);
993
934
  }
994
935
  }
995
936
  });
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
937
+ // agents:kill - Kill a spawned agent by PID
1024
938
  program
1025
- .command('workers:kill')
1026
- .description('Kill a spawned worker')
1027
- .argument('<name>', 'Worker name')
939
+ .command('agents:kill')
940
+ .description('Kill a spawned agent')
941
+ .argument('<name>', 'Agent name')
1028
942
  .option('--force', 'Skip graceful shutdown, kill immediately')
1029
943
  .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`);
944
+ const { getProjectPaths } = await import('../utils/project-namespace.js');
945
+ const paths = getProjectPaths();
946
+ const workers = readWorkersMetadata(paths.projectRoot);
947
+ const worker = workers.find(w => w.name === name);
948
+ if (!worker) {
949
+ console.error(`Spawned agent "${name}" not found`);
950
+ console.log(`Run 'agent-relay agents' to see available agents`);
951
+ process.exit(1);
1034
952
  }
1035
- catch {
1036
- console.error(`Worker "${name}" not found`);
1037
- console.log(`Run 'agent-relay workers' to see available workers`);
953
+ if (!worker.pid) {
954
+ console.error(`Agent "${name}" has no PID recorded`);
1038
955
  process.exit(1);
1039
956
  }
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`);
957
+ try {
958
+ if (!options.force) {
959
+ // Try graceful shutdown first (SIGTERM)
960
+ console.log(`Sending SIGTERM to ${name} (pid: ${worker.pid})...`);
961
+ process.kill(worker.pid, 'SIGTERM');
1045
962
  // Wait for graceful shutdown
1046
963
  await new Promise(r => setTimeout(r, 2000));
964
+ // Check if still running
965
+ try {
966
+ process.kill(worker.pid, 0); // Check if process exists
967
+ console.log(`Agent still running, sending SIGKILL...`);
968
+ process.kill(worker.pid, 'SIGKILL');
969
+ }
970
+ catch {
971
+ // Process no longer exists, graceful shutdown worked
972
+ }
1047
973
  }
1048
- catch {
1049
- // Ignore errors, will force kill below
974
+ else {
975
+ // Force kill immediately
976
+ console.log(`Force killing ${name} (pid: ${worker.pid})...`);
977
+ process.kill(worker.pid, 'SIGKILL');
1050
978
  }
1051
- }
1052
- // Kill the window
1053
- try {
1054
- await execAsync(`tmux kill-window -t ${window}`);
1055
- console.log(`Killed worker: ${name}`);
979
+ console.log(`Killed agent: ${name}`);
1056
980
  }
1057
981
  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`);
982
+ if (err.code === 'ESRCH') {
983
+ console.log(`Agent ${name} is no longer running (pid: ${worker.pid})`);
1071
984
  }
1072
- catch {
1073
- console.log(`Session "${WORKER_SESSION}" does not exist`);
1074
- console.log('Spawn a worker to create it.');
1075
- return;
985
+ else {
986
+ console.error(`Failed to kill ${name}:`, err.message);
987
+ process.exit(1);
1076
988
  }
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
989
  }
1093
990
  });
1094
991
  program.parse();