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.
- package/README.md +158 -0
- package/dist/bridge/config.d.ts +41 -0
- package/dist/bridge/config.d.ts.map +1 -0
- package/dist/bridge/config.js +143 -0
- package/dist/bridge/config.js.map +1 -0
- package/dist/bridge/index.d.ts +10 -0
- package/dist/bridge/index.d.ts.map +1 -0
- package/dist/bridge/index.js +10 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/bridge/multi-project-client.d.ts +99 -0
- package/dist/bridge/multi-project-client.d.ts.map +1 -0
- package/dist/bridge/multi-project-client.js +386 -0
- package/dist/bridge/multi-project-client.js.map +1 -0
- package/dist/bridge/spawner.d.ts +46 -0
- package/dist/bridge/spawner.d.ts.map +1 -0
- package/dist/bridge/spawner.js +223 -0
- package/dist/bridge/spawner.js.map +1 -0
- package/dist/bridge/types.d.ts +55 -0
- package/dist/bridge/types.d.ts.map +1 -0
- package/dist/bridge/types.js +6 -0
- package/dist/bridge/types.js.map +1 -0
- package/dist/bridge/utils.d.ts +30 -0
- package/dist/bridge/utils.d.ts.map +1 -0
- package/dist/bridge/utils.js +54 -0
- package/dist/bridge/utils.js.map +1 -0
- package/dist/cli/index.js +564 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/daemon/agent-registry.d.ts.map +1 -1
- package/dist/daemon/agent-registry.js +6 -1
- package/dist/daemon/agent-registry.js.map +1 -1
- package/dist/daemon/connection.d.ts +22 -0
- package/dist/daemon/connection.d.ts.map +1 -1
- package/dist/daemon/connection.js +59 -13
- package/dist/daemon/connection.js.map +1 -1
- package/dist/daemon/router.d.ts +27 -0
- package/dist/daemon/router.d.ts.map +1 -1
- package/dist/daemon/router.js +108 -3
- package/dist/daemon/router.js.map +1 -1
- package/dist/daemon/server.d.ts +8 -0
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +95 -23
- package/dist/daemon/server.js.map +1 -1
- package/dist/dashboard/metrics.d.ts +105 -0
- package/dist/dashboard/metrics.d.ts.map +1 -0
- package/dist/dashboard/metrics.js +192 -0
- package/dist/dashboard/metrics.js.map +1 -0
- package/dist/dashboard/needs-attention.d.ts +24 -0
- package/dist/dashboard/needs-attention.d.ts.map +1 -0
- package/dist/dashboard/needs-attention.js +78 -0
- package/dist/dashboard/needs-attention.js.map +1 -0
- package/dist/dashboard/public/bridge.html +1272 -0
- package/dist/dashboard/public/index.html +2017 -879
- package/dist/dashboard/public/js/app.js +184 -0
- package/dist/dashboard/public/js/app.js.map +7 -0
- package/dist/dashboard/public/metrics.html +999 -0
- package/dist/dashboard/server.d.ts +13 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +568 -13
- package/dist/dashboard/server.js.map +1 -1
- package/dist/dashboard/start.js +1 -1
- package/dist/dashboard/start.js.map +1 -1
- package/dist/dashboard-v2/index.d.ts +10 -0
- package/dist/dashboard-v2/index.d.ts.map +1 -0
- package/dist/dashboard-v2/index.js +54 -0
- package/dist/dashboard-v2/index.js.map +1 -0
- package/dist/dashboard-v2/lib/api.d.ts +95 -0
- package/dist/dashboard-v2/lib/api.d.ts.map +1 -0
- package/dist/dashboard-v2/lib/api.js +270 -0
- package/dist/dashboard-v2/lib/api.js.map +1 -0
- package/dist/dashboard-v2/lib/colors.d.ts +61 -0
- package/dist/dashboard-v2/lib/colors.d.ts.map +1 -0
- package/dist/dashboard-v2/lib/colors.js +198 -0
- package/dist/dashboard-v2/lib/colors.js.map +1 -0
- package/dist/dashboard-v2/lib/hierarchy.d.ts +74 -0
- package/dist/dashboard-v2/lib/hierarchy.d.ts.map +1 -0
- package/dist/dashboard-v2/lib/hierarchy.js +196 -0
- package/dist/dashboard-v2/lib/hierarchy.js.map +1 -0
- package/dist/dashboard-v2/types/index.d.ts +154 -0
- package/dist/dashboard-v2/types/index.d.ts.map +1 -0
- package/dist/dashboard-v2/types/index.js +6 -0
- package/dist/dashboard-v2/types/index.js.map +1 -0
- package/dist/storage/adapter.d.ts +21 -1
- package/dist/storage/adapter.d.ts.map +1 -1
- package/dist/storage/adapter.js +36 -0
- package/dist/storage/adapter.js.map +1 -1
- package/dist/storage/sqlite-adapter.d.ts +34 -0
- package/dist/storage/sqlite-adapter.d.ts.map +1 -1
- package/dist/storage/sqlite-adapter.js +253 -12
- package/dist/storage/sqlite-adapter.js.map +1 -1
- package/dist/utils/agent-config.d.ts +45 -0
- package/dist/utils/agent-config.d.ts.map +1 -0
- package/dist/utils/agent-config.js +118 -0
- package/dist/utils/agent-config.js.map +1 -0
- package/dist/wrapper/client.d.ts +8 -0
- package/dist/wrapper/client.d.ts.map +1 -1
- package/dist/wrapper/client.js +26 -0
- package/dist/wrapper/client.js.map +1 -1
- package/dist/wrapper/parser.d.ts +17 -0
- package/dist/wrapper/parser.d.ts.map +1 -1
- package/dist/wrapper/parser.js +334 -10
- package/dist/wrapper/parser.js.map +1 -1
- package/dist/wrapper/tmux-wrapper.d.ts +37 -2
- package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
- package/dist/wrapper/tmux-wrapper.js +178 -18
- package/dist/wrapper/tmux-wrapper.js.map +1 -1
- package/docs/AGENTS.md +105 -0
- package/docs/ARCHITECTURE_DECISIONS.md +175 -0
- package/docs/COMPETITIVE_ANALYSIS.md +897 -0
- package/docs/DESIGN_BRIDGE_STAFFING.md +878 -0
- package/docs/MONETIZATION.md +1679 -0
- package/docs/agent-relay-snippet.md +61 -0
- package/docs/dashboard-v2-plan.md +179 -0
- 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:
|
|
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(
|
|
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}
|
|
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
|