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.
- package/README.md +27 -56
- package/bin/.gitkeep +0 -0
- package/bin/tmux +0 -0
- package/dist/bridge/config.d.ts.map +1 -1
- package/dist/bridge/multi-project-client.d.ts.map +1 -1
- package/dist/bridge/multi-project-client.js +1 -1
- package/dist/bridge/multi-project-client.js.map +1 -1
- package/dist/bridge/spawner.d.ts +47 -9
- package/dist/bridge/spawner.d.ts.map +1 -1
- package/dist/bridge/spawner.js +177 -73
- package/dist/bridge/spawner.js.map +1 -1
- package/dist/bridge/types.d.ts +4 -2
- package/dist/bridge/types.d.ts.map +1 -1
- package/dist/cli/index.js +192 -295
- package/dist/cli/index.js.map +1 -1
- package/dist/daemon/router.d.ts.map +1 -1
- package/dist/daemon/router.js.map +1 -1
- package/dist/dashboard-server/metrics.d.ts.map +1 -0
- package/dist/dashboard-server/metrics.js.map +1 -0
- package/dist/dashboard-server/needs-attention.d.ts.map +1 -0
- package/dist/dashboard-server/needs-attention.js.map +1 -0
- package/dist/dashboard-server/server.d.ts.map +1 -0
- package/dist/{dashboard → dashboard-server}/server.js +20 -14
- package/dist/dashboard-server/server.js.map +1 -0
- package/dist/dashboard-server/start.d.ts.map +1 -0
- package/dist/dashboard-server/start.js.map +1 -0
- package/dist/utils/tmux-resolver.d.ts +55 -0
- package/dist/utils/tmux-resolver.d.ts.map +1 -0
- package/dist/utils/tmux-resolver.js +175 -0
- package/dist/utils/tmux-resolver.js.map +1 -0
- package/dist/utils/update-checker.d.ts +26 -0
- package/dist/utils/update-checker.d.ts.map +1 -0
- package/dist/utils/update-checker.js +174 -0
- package/dist/utils/update-checker.js.map +1 -0
- package/dist/wrapper/pty-wrapper.d.ts +129 -0
- package/dist/wrapper/pty-wrapper.d.ts.map +1 -0
- package/dist/wrapper/pty-wrapper.js +442 -0
- package/dist/wrapper/pty-wrapper.js.map +1 -0
- package/dist/wrapper/tmux-wrapper.d.ts +2 -1
- package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
- package/dist/wrapper/tmux-wrapper.js +23 -19
- package/dist/wrapper/tmux-wrapper.js.map +1 -1
- package/docs/AGENTS.md +43 -30
- package/docs/INTEGRATION-GUIDE.md +1 -1
- package/docs/PROTOCOL.md +16 -10
- package/docs/agent-relay-snippet.md +21 -17
- package/package.json +10 -6
- package/scripts/dev/PUBLIC_RELEASE_PLAN.md +88 -0
- package/scripts/dev/dev-team-setup.sh +431 -0
- package/scripts/e2e-test.sh +119 -0
- package/scripts/games/game-protocol.md +79 -0
- package/scripts/games/hearts-setup.sh +264 -0
- package/scripts/postinstall.js +254 -0
- package/scripts/tictactoe-setup.sh +181 -0
- package/dist/dashboard/metrics.d.ts.map +0 -1
- package/dist/dashboard/metrics.js.map +0 -1
- package/dist/dashboard/needs-attention.d.ts.map +0 -1
- package/dist/dashboard/needs-attention.js.map +0 -1
- package/dist/dashboard/public/bridge.html +0 -1272
- package/dist/dashboard/public/index.html +0 -2262
- package/dist/dashboard/public/js/app.js +0 -184
- package/dist/dashboard/public/js/app.js.map +0 -7
- package/dist/dashboard/public/metrics.html +0 -999
- package/dist/dashboard/server.d.ts.map +0 -1
- package/dist/dashboard/server.js.map +0 -1
- package/dist/dashboard/start.d.ts.map +0 -1
- package/dist/dashboard/start.js.map +0 -1
- package/dist/dashboard-v2/index.d.ts +0 -10
- package/dist/dashboard-v2/index.d.ts.map +0 -1
- package/dist/dashboard-v2/index.js +0 -54
- package/dist/dashboard-v2/index.js.map +0 -1
- package/dist/dashboard-v2/lib/api.d.ts +0 -95
- package/dist/dashboard-v2/lib/api.d.ts.map +0 -1
- package/dist/dashboard-v2/lib/api.js +0 -270
- package/dist/dashboard-v2/lib/api.js.map +0 -1
- package/dist/dashboard-v2/lib/colors.d.ts +0 -61
- package/dist/dashboard-v2/lib/colors.d.ts.map +0 -1
- package/dist/dashboard-v2/lib/colors.js +0 -198
- package/dist/dashboard-v2/lib/colors.js.map +0 -1
- package/dist/dashboard-v2/lib/hierarchy.d.ts +0 -74
- package/dist/dashboard-v2/lib/hierarchy.d.ts.map +0 -1
- package/dist/dashboard-v2/lib/hierarchy.js +0 -196
- package/dist/dashboard-v2/lib/hierarchy.js.map +0 -1
- package/dist/dashboard-v2/types/index.d.ts +0 -154
- package/dist/dashboard-v2/types/index.d.ts.map +0 -1
- package/dist/dashboard-v2/types/index.js +0 -6
- package/dist/dashboard-v2/types/index.js.map +0 -1
- /package/dist/{dashboard → dashboard-server}/metrics.d.ts +0 -0
- /package/dist/{dashboard → dashboard-server}/metrics.js +0 -0
- /package/dist/{dashboard → dashboard-server}/needs-attention.d.ts +0 -0
- /package/dist/{dashboard → dashboard-server}/needs-attention.js +0 -0
- /package/dist/{dashboard → dashboard-server}/server.d.ts +0 -0
- /package/dist/{dashboard → dashboard-server}/start.d.ts +0 -0
- /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}
|
|
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 {
|
|
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(
|
|
279
|
+
console.log(JSON.stringify(combined, null, 2));
|
|
327
280
|
return;
|
|
328
281
|
}
|
|
329
|
-
if (!
|
|
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
|
|
335
|
-
console.log('
|
|
336
|
-
|
|
337
|
-
const name =
|
|
338
|
-
const status =
|
|
339
|
-
const cli =
|
|
340
|
-
const
|
|
341
|
-
console.log(`${name} ${status} ${cli} ${
|
|
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 (
|
|
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(`
|
|
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
|
|
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(`
|
|
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
|
-
//
|
|
887
|
+
// Spawned agent debugging commands
|
|
866
888
|
// ============================================
|
|
867
|
-
|
|
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('
|
|
937
|
-
.description('Show recent output from a spawned
|
|
938
|
-
.argument('<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
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
console.
|
|
949
|
-
console.log(`Run 'agent-relay
|
|
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
|
|
908
|
+
console.log(`Following logs for ${name} (Ctrl+C to stop)...`);
|
|
954
909
|
console.log('─'.repeat(50));
|
|
955
|
-
// Use
|
|
956
|
-
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
|
|
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
|
-
|
|
916
|
+
child.kill();
|
|
977
917
|
console.log('\nStopped following');
|
|
978
918
|
process.exit(0);
|
|
979
919
|
});
|
|
980
|
-
|
|
981
|
-
|
|
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(`
|
|
987
|
-
console.log(`
|
|
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
|
|
933
|
+
console.error('Failed to read logs:', err.message);
|
|
993
934
|
}
|
|
994
935
|
}
|
|
995
936
|
});
|
|
996
|
-
//
|
|
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('
|
|
1026
|
-
.description('Kill a spawned
|
|
1027
|
-
.argument('<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
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
-
|
|
1036
|
-
console.error(`
|
|
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
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
|
|
1049
|
-
//
|
|
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
|
-
|
|
1059
|
-
|
|
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
|
-
|
|
1073
|
-
console.
|
|
1074
|
-
|
|
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();
|