agent-relay 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE +22 -0
- package/PROTOCOL.md +319 -0
- package/README.md +791 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +1591 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/daemon/connection.d.ts +60 -0
- package/dist/daemon/connection.d.ts.map +1 -0
- package/dist/daemon/connection.js +245 -0
- package/dist/daemon/connection.js.map +1 -0
- package/dist/daemon/index.d.ts +4 -0
- package/dist/daemon/index.d.ts.map +1 -0
- package/dist/daemon/index.js +4 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/router.d.ts +72 -0
- package/dist/daemon/router.d.ts.map +1 -0
- package/dist/daemon/router.js +183 -0
- package/dist/daemon/router.js.map +1 -0
- package/dist/daemon/server.d.ts +52 -0
- package/dist/daemon/server.d.ts.map +1 -0
- package/dist/daemon/server.js +186 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/dashboard/public/index.html +690 -0
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +220 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/games/index.d.ts +2 -0
- package/dist/games/index.d.ts.map +1 -0
- package/dist/games/index.js +2 -0
- package/dist/games/index.js.map +1 -0
- package/dist/games/tictactoe.d.ts +24 -0
- package/dist/games/tictactoe.d.ts.map +1 -0
- package/dist/games/tictactoe.js +160 -0
- package/dist/games/tictactoe.js.map +1 -0
- package/dist/hooks/inbox-check/hook.d.ts +28 -0
- package/dist/hooks/inbox-check/hook.d.ts.map +1 -0
- package/dist/hooks/inbox-check/hook.js +97 -0
- package/dist/hooks/inbox-check/hook.js.map +1 -0
- package/dist/hooks/inbox-check/index.d.ts +8 -0
- package/dist/hooks/inbox-check/index.d.ts.map +1 -0
- package/dist/hooks/inbox-check/index.js +8 -0
- package/dist/hooks/inbox-check/index.js.map +1 -0
- package/dist/hooks/inbox-check/types.d.ts +31 -0
- package/dist/hooks/inbox-check/types.d.ts.map +1 -0
- package/dist/hooks/inbox-check/types.js +5 -0
- package/dist/hooks/inbox-check/types.js.map +1 -0
- package/dist/hooks/inbox-check/utils.d.ts +44 -0
- package/dist/hooks/inbox-check/utils.d.ts.map +1 -0
- package/dist/hooks/inbox-check/utils.js +107 -0
- package/dist/hooks/inbox-check/utils.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/framing.d.ts +32 -0
- package/dist/protocol/framing.d.ts.map +1 -0
- package/dist/protocol/framing.js +71 -0
- package/dist/protocol/framing.js.map +1 -0
- package/dist/protocol/index.d.ts +3 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +3 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/protocol/types.d.ts +104 -0
- package/dist/protocol/types.d.ts.map +1 -0
- package/dist/protocol/types.js +6 -0
- package/dist/protocol/types.js.map +1 -0
- package/dist/state/agent-state.d.ts +40 -0
- package/dist/state/agent-state.d.ts.map +1 -0
- package/dist/state/agent-state.js +120 -0
- package/dist/state/agent-state.js.map +1 -0
- package/dist/storage/adapter.d.ts +29 -0
- package/dist/storage/adapter.d.ts.map +1 -0
- package/dist/storage/adapter.js +2 -0
- package/dist/storage/adapter.js.map +1 -0
- package/dist/storage/sqlite-adapter.d.ts +15 -0
- package/dist/storage/sqlite-adapter.d.ts.map +1 -0
- package/dist/storage/sqlite-adapter.js +116 -0
- package/dist/storage/sqlite-adapter.js.map +1 -0
- package/dist/supervisor/inbox.d.ts +38 -0
- package/dist/supervisor/inbox.d.ts.map +1 -0
- package/dist/supervisor/inbox.js +162 -0
- package/dist/supervisor/inbox.js.map +1 -0
- package/dist/supervisor/index.d.ts +10 -0
- package/dist/supervisor/index.d.ts.map +1 -0
- package/dist/supervisor/index.js +10 -0
- package/dist/supervisor/index.js.map +1 -0
- package/dist/supervisor/spawner.d.ts +54 -0
- package/dist/supervisor/spawner.d.ts.map +1 -0
- package/dist/supervisor/spawner.js +282 -0
- package/dist/supervisor/spawner.js.map +1 -0
- package/dist/supervisor/state.d.ts +132 -0
- package/dist/supervisor/state.d.ts.map +1 -0
- package/dist/supervisor/state.js +465 -0
- package/dist/supervisor/state.js.map +1 -0
- package/dist/supervisor/supervisor.d.ts +67 -0
- package/dist/supervisor/supervisor.d.ts.map +1 -0
- package/dist/supervisor/supervisor.js +263 -0
- package/dist/supervisor/supervisor.js.map +1 -0
- package/dist/supervisor/types.d.ts +139 -0
- package/dist/supervisor/types.d.ts.map +1 -0
- package/dist/supervisor/types.js +12 -0
- package/dist/supervisor/types.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/name-generator.d.ts +17 -0
- package/dist/utils/name-generator.d.ts.map +1 -0
- package/dist/utils/name-generator.js +52 -0
- package/dist/utils/name-generator.js.map +1 -0
- package/dist/webhook/spawner.d.ts +79 -0
- package/dist/webhook/spawner.d.ts.map +1 -0
- package/dist/webhook/spawner.js +288 -0
- package/dist/webhook/spawner.js.map +1 -0
- package/dist/wrapper/client.d.ts +72 -0
- package/dist/wrapper/client.d.ts.map +1 -0
- package/dist/wrapper/client.js +306 -0
- package/dist/wrapper/client.js.map +1 -0
- package/dist/wrapper/inbox.d.ts +37 -0
- package/dist/wrapper/inbox.d.ts.map +1 -0
- package/dist/wrapper/inbox.js +73 -0
- package/dist/wrapper/inbox.js.map +1 -0
- package/dist/wrapper/index.d.ts +4 -0
- package/dist/wrapper/index.d.ts.map +1 -0
- package/dist/wrapper/index.js +7 -0
- package/dist/wrapper/index.js.map +1 -0
- package/dist/wrapper/parser.d.ts +94 -0
- package/dist/wrapper/parser.d.ts.map +1 -0
- package/dist/wrapper/parser.js +360 -0
- package/dist/wrapper/parser.js.map +1 -0
- package/dist/wrapper/pty-wrapper.d.ts +125 -0
- package/dist/wrapper/pty-wrapper.d.ts.map +1 -0
- package/dist/wrapper/pty-wrapper.js +494 -0
- package/dist/wrapper/pty-wrapper.js.map +1 -0
- package/dist/wrapper/tmux-wrapper.d.ts +131 -0
- package/dist/wrapper/tmux-wrapper.d.ts.map +1 -0
- package/dist/wrapper/tmux-wrapper.js +427 -0
- package/dist/wrapper/tmux-wrapper.js.map +1 -0
- package/install.sh +69 -0
- package/package.json +82 -0
|
@@ -0,0 +1,1591 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Agent Relay CLI
|
|
4
|
+
* Command-line interface for agent-relay.
|
|
5
|
+
*/
|
|
6
|
+
import { Command } from 'commander';
|
|
7
|
+
import { config as dotenvConfig } from 'dotenv';
|
|
8
|
+
import { Daemon, DEFAULT_SOCKET_PATH } from '../daemon/server.js';
|
|
9
|
+
import { RelayClient } from '../wrapper/client.js';
|
|
10
|
+
import { generateAgentName } from '../utils/name-generator.js';
|
|
11
|
+
import { Supervisor } from '../supervisor/supervisor.js';
|
|
12
|
+
import { setupTicTacToe } from '../games/tictactoe.js';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { spawn } from 'node:child_process';
|
|
16
|
+
// Load .env file if present
|
|
17
|
+
dotenvConfig();
|
|
18
|
+
// Default dashboard port (can be overridden via .env or CLI)
|
|
19
|
+
const DEFAULT_DASHBOARD_PORT = process.env.AGENT_RELAY_DASHBOARD_PORT || '3888';
|
|
20
|
+
const program = new Command();
|
|
21
|
+
function pidFilePathForSocket(socketPath) {
|
|
22
|
+
return `${socketPath}.pid`;
|
|
23
|
+
}
|
|
24
|
+
function supervisorPidFilePath(dataDir) {
|
|
25
|
+
return path.join(dataDir, 'supervisor.pid');
|
|
26
|
+
}
|
|
27
|
+
function isProcessAlive(pid) {
|
|
28
|
+
try {
|
|
29
|
+
process.kill(pid, 0);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function readPidFile(pidPath) {
|
|
37
|
+
try {
|
|
38
|
+
if (!fs.existsSync(pidPath))
|
|
39
|
+
return null;
|
|
40
|
+
const raw = fs.readFileSync(pidPath, 'utf-8').trim();
|
|
41
|
+
const pid = Number(raw);
|
|
42
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
43
|
+
return null;
|
|
44
|
+
return pid;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function startDetachedSupervisor(options) {
|
|
51
|
+
const pidPath = supervisorPidFilePath(options.dataDir);
|
|
52
|
+
const existingPid = readPidFile(pidPath);
|
|
53
|
+
if (existingPid && isProcessAlive(existingPid)) {
|
|
54
|
+
return existingPid;
|
|
55
|
+
}
|
|
56
|
+
// process.argv[1] points to the current CLI entrypoint (dist/cli/index.js in normal usage).
|
|
57
|
+
const cliEntrypoint = process.argv[1];
|
|
58
|
+
if (!cliEntrypoint) {
|
|
59
|
+
throw new Error('Unable to determine CLI entrypoint path for supervisor spawn');
|
|
60
|
+
}
|
|
61
|
+
fs.mkdirSync(options.dataDir, { recursive: true });
|
|
62
|
+
const child = spawn(process.execPath, [
|
|
63
|
+
cliEntrypoint,
|
|
64
|
+
'supervisor',
|
|
65
|
+
'-s',
|
|
66
|
+
options.socket,
|
|
67
|
+
'-d',
|
|
68
|
+
options.dataDir,
|
|
69
|
+
'-p',
|
|
70
|
+
String(options.pollInterval),
|
|
71
|
+
...(options.verbose ? ['-v'] : []),
|
|
72
|
+
], {
|
|
73
|
+
detached: true,
|
|
74
|
+
stdio: 'ignore',
|
|
75
|
+
env: process.env,
|
|
76
|
+
});
|
|
77
|
+
child.unref();
|
|
78
|
+
fs.writeFileSync(pidPath, `${child.pid ?? ''}\n`, 'utf-8');
|
|
79
|
+
return child.pid ?? -1;
|
|
80
|
+
}
|
|
81
|
+
function stopDetachedSupervisor(dataDir) {
|
|
82
|
+
const pidPath = supervisorPidFilePath(dataDir);
|
|
83
|
+
const pid = readPidFile(pidPath);
|
|
84
|
+
if (!pid)
|
|
85
|
+
return { pid: null, stopped: false };
|
|
86
|
+
try {
|
|
87
|
+
process.kill(pid, 'SIGTERM');
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// process may already be dead
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
fs.unlinkSync(pidPath);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// ignore
|
|
97
|
+
}
|
|
98
|
+
return { pid, stopped: true };
|
|
99
|
+
}
|
|
100
|
+
program
|
|
101
|
+
.name('agent-relay')
|
|
102
|
+
.description('Real-time agent-to-agent communication system')
|
|
103
|
+
.version('0.1.0');
|
|
104
|
+
// Start daemon
|
|
105
|
+
program
|
|
106
|
+
.command('start')
|
|
107
|
+
.description('Start the relay daemon')
|
|
108
|
+
.option('-s, --socket <path>', 'Socket path', DEFAULT_SOCKET_PATH)
|
|
109
|
+
.option('-f, --foreground', 'Run in foreground', false)
|
|
110
|
+
.option('--db-path <path>', 'SQLite DB path for message storage (defaults near socket)')
|
|
111
|
+
.action(async (options) => {
|
|
112
|
+
const socketPath = options.socket;
|
|
113
|
+
const pidFilePath = pidFilePathForSocket(socketPath);
|
|
114
|
+
const daemon = new Daemon({
|
|
115
|
+
socketPath,
|
|
116
|
+
pidFilePath,
|
|
117
|
+
storagePath: options.dbPath ?? undefined,
|
|
118
|
+
});
|
|
119
|
+
// Handle shutdown
|
|
120
|
+
process.on('SIGINT', async () => {
|
|
121
|
+
console.log('\nShutting down...');
|
|
122
|
+
await daemon.stop();
|
|
123
|
+
process.exit(0);
|
|
124
|
+
});
|
|
125
|
+
process.on('SIGTERM', async () => {
|
|
126
|
+
await daemon.stop();
|
|
127
|
+
process.exit(0);
|
|
128
|
+
});
|
|
129
|
+
try {
|
|
130
|
+
await daemon.start();
|
|
131
|
+
console.log('Daemon started. Press Ctrl+C to stop.');
|
|
132
|
+
// Keep process alive
|
|
133
|
+
if (options.foreground) {
|
|
134
|
+
await new Promise(() => { }); // Never resolves
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
console.error('Failed to start daemon:', err);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
// Stop daemon
|
|
143
|
+
program
|
|
144
|
+
.command('stop')
|
|
145
|
+
.description('Stop the relay daemon (and background supervisor if running)')
|
|
146
|
+
.option('-s, --socket <path>', 'Socket path', DEFAULT_SOCKET_PATH)
|
|
147
|
+
.option('-d, --data-dir <path>', 'Data directory (for supervisor pidfile)', '/tmp/agent-relay')
|
|
148
|
+
.option('--daemon-only', 'Only stop the daemon (leave supervisor running)', false)
|
|
149
|
+
.action(async (options) => {
|
|
150
|
+
const socketPath = options.socket;
|
|
151
|
+
const pidFilePath = pidFilePathForSocket(socketPath);
|
|
152
|
+
// Stop supervisor first (best-effort) so it doesn't keep spawning while daemon stops.
|
|
153
|
+
if (!options.daemonOnly) {
|
|
154
|
+
const res = stopDetachedSupervisor(options.dataDir);
|
|
155
|
+
if (res.pid) {
|
|
156
|
+
console.log(`Supervisor stop requested (pid ${res.pid})`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (!fs.existsSync(pidFilePath)) {
|
|
160
|
+
console.log('Daemon not running (pid file not found)');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const pidRaw = fs.readFileSync(pidFilePath, 'utf-8').trim();
|
|
164
|
+
const pid = Number(pidRaw);
|
|
165
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
166
|
+
console.error(`Invalid pid file: ${pidFilePath}`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
process.kill(pid, 'SIGTERM');
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
// Stale pid file
|
|
174
|
+
console.warn(`Failed to signal pid ${pid} (${err.message}); cleaning up pid file`);
|
|
175
|
+
fs.unlinkSync(pidFilePath);
|
|
176
|
+
}
|
|
177
|
+
// Wait briefly for socket/pid file cleanup
|
|
178
|
+
const deadline = Date.now() + 2000;
|
|
179
|
+
while (Date.now() < deadline) {
|
|
180
|
+
const socketExists = fs.existsSync(socketPath);
|
|
181
|
+
const pidExists = fs.existsSync(pidFilePath);
|
|
182
|
+
if (!socketExists && !pidExists) {
|
|
183
|
+
console.log('Daemon stopped');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
187
|
+
}
|
|
188
|
+
console.warn('Stop requested, but daemon did not exit within 2s');
|
|
189
|
+
console.warn(`Socket: ${socketPath}`);
|
|
190
|
+
console.warn(`PID file: ${pidFilePath}`);
|
|
191
|
+
});
|
|
192
|
+
// Wrap an agent
|
|
193
|
+
program
|
|
194
|
+
.command('wrap')
|
|
195
|
+
.description('Wrap an agent CLI command')
|
|
196
|
+
.option('-n, --name <name>', 'Agent name (auto-generated if not provided)')
|
|
197
|
+
.option('-s, --socket <path>', 'Socket path', DEFAULT_SOCKET_PATH)
|
|
198
|
+
.option('-r, --raw', 'Raw mode - bypass parsing for terminal-heavy CLIs', false)
|
|
199
|
+
.option('--pty', 'Use direct PTY mode (legacy)', false)
|
|
200
|
+
.option('-t, --tmux', 'Use tmux for message injection (old implementation)', false)
|
|
201
|
+
.option('-q, --quiet', 'Disable debug logging', false)
|
|
202
|
+
.option('--log-interval <ms>', 'Throttle debug logs (ms)', (val) => parseInt(val, 10))
|
|
203
|
+
.option('--inject-idle-ms <ms>', 'Idle time before injecting messages (ms)', (val) => parseInt(val, 10))
|
|
204
|
+
.option('--inject-retry-ms <ms>', 'Retry interval while waiting to inject (ms)', (val) => parseInt(val, 10))
|
|
205
|
+
.option('-o, --osascript', 'Use osascript for OS-level keyboard simulation (macOS)', false)
|
|
206
|
+
.option('-i, --inbox', 'Use file-based inbox (agent reads messages from file)', false)
|
|
207
|
+
.option('--inbox-dir <path>', 'Custom inbox directory', '/tmp/agent-relay')
|
|
208
|
+
.argument('<command...>', 'Command to wrap')
|
|
209
|
+
.action(async (commandParts, options) => {
|
|
210
|
+
// For tmux2, we need to preserve args separately for proper quoting
|
|
211
|
+
const [mainCommand, ...commandArgs] = commandParts;
|
|
212
|
+
const command = commandParts.join(' ');
|
|
213
|
+
// Auto-generate name if not provided
|
|
214
|
+
const agentName = options.name ?? generateAgentName();
|
|
215
|
+
process.stderr.write(`Agent name: ${agentName}\n`);
|
|
216
|
+
// Determine mode - tmux is now the default
|
|
217
|
+
const usePty = options.pty || options.tmux || options.osascript;
|
|
218
|
+
if (options.inbox) {
|
|
219
|
+
process.stderr.write(`Mode: inbox (file-based messaging)\n`);
|
|
220
|
+
}
|
|
221
|
+
else if (options.osascript) {
|
|
222
|
+
process.stderr.write(`Mode: osascript (OS-level keyboard simulation)\n`);
|
|
223
|
+
}
|
|
224
|
+
else if (options.tmux) {
|
|
225
|
+
process.stderr.write(`Mode: tmux (old implementation)\n`);
|
|
226
|
+
}
|
|
227
|
+
else if (options.pty) {
|
|
228
|
+
process.stderr.write(`Mode: direct PTY (legacy)\n`);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
process.stderr.write(`Mode: tmux (default)\n`);
|
|
232
|
+
}
|
|
233
|
+
// Use the new TmuxWrapper by default (unless --pty, --tmux, or --osascript specified)
|
|
234
|
+
if (!usePty) {
|
|
235
|
+
let TmuxWrapperClass;
|
|
236
|
+
try {
|
|
237
|
+
({ TmuxWrapper: TmuxWrapperClass } = await import('../wrapper/tmux-wrapper.js'));
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
console.error('Failed to load TmuxWrapper.');
|
|
241
|
+
console.error('Original error:', err);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
const wrapper = new TmuxWrapperClass({
|
|
245
|
+
name: agentName,
|
|
246
|
+
command: mainCommand,
|
|
247
|
+
args: commandArgs,
|
|
248
|
+
socketPath: options.socket,
|
|
249
|
+
useInbox: options.inbox,
|
|
250
|
+
inboxDir: options.inboxDir,
|
|
251
|
+
debug: !options.quiet,
|
|
252
|
+
debugLogIntervalMs: options.logInterval,
|
|
253
|
+
idleBeforeInjectMs: options.injectIdleMs,
|
|
254
|
+
injectRetryMs: options.injectRetryMs,
|
|
255
|
+
});
|
|
256
|
+
// Handle shutdown
|
|
257
|
+
process.on('SIGINT', () => {
|
|
258
|
+
wrapper.stop();
|
|
259
|
+
process.exit(0);
|
|
260
|
+
});
|
|
261
|
+
try {
|
|
262
|
+
await wrapper.start();
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
console.error('Failed to start tmux wrapper:', err);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
// Use the original PtyWrapper
|
|
271
|
+
let PtyWrapperClass;
|
|
272
|
+
try {
|
|
273
|
+
({ PtyWrapper: PtyWrapperClass } = await import('../wrapper/pty-wrapper.js'));
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
console.error('Failed to load PTY wrapper dependencies (node-pty).');
|
|
277
|
+
console.error('If you recently changed Node versions, rebuild native deps:');
|
|
278
|
+
console.error(' npm rebuild node-pty');
|
|
279
|
+
console.error('Original error:', err);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
const wrapper = new PtyWrapperClass({
|
|
283
|
+
name: agentName,
|
|
284
|
+
command,
|
|
285
|
+
socketPath: options.socket,
|
|
286
|
+
raw: options.raw,
|
|
287
|
+
useTmux: options.tmux,
|
|
288
|
+
useOsascript: options.osascript,
|
|
289
|
+
useInbox: options.inbox,
|
|
290
|
+
inboxDir: options.inboxDir,
|
|
291
|
+
});
|
|
292
|
+
// Handle shutdown
|
|
293
|
+
process.on('SIGINT', () => {
|
|
294
|
+
wrapper.stop();
|
|
295
|
+
process.exit(0);
|
|
296
|
+
});
|
|
297
|
+
try {
|
|
298
|
+
await wrapper.start();
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
console.error('Failed to start wrapper:', err);
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
// Status
|
|
306
|
+
program
|
|
307
|
+
.command('status')
|
|
308
|
+
.description('Show relay daemon status')
|
|
309
|
+
.option('-s, --socket <path>', 'Socket path', DEFAULT_SOCKET_PATH)
|
|
310
|
+
.action(async (options) => {
|
|
311
|
+
if (!fs.existsSync(options.socket)) {
|
|
312
|
+
console.log('Status: STOPPED (socket not found)');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// Try to connect
|
|
316
|
+
const client = new RelayClient({
|
|
317
|
+
agentName: '__status_check__',
|
|
318
|
+
socketPath: options.socket,
|
|
319
|
+
reconnect: false,
|
|
320
|
+
});
|
|
321
|
+
try {
|
|
322
|
+
await client.connect();
|
|
323
|
+
console.log('Status: RUNNING');
|
|
324
|
+
console.log(`Socket: ${options.socket}`);
|
|
325
|
+
client.disconnect();
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
console.log('Status: STOPPED (connection failed)');
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
// Send a message (for testing)
|
|
332
|
+
program
|
|
333
|
+
.command('send')
|
|
334
|
+
.description('Send a message to an agent')
|
|
335
|
+
.option('-f, --from <name>', 'Sender agent name (auto-generated if not provided)')
|
|
336
|
+
.requiredOption('-t, --to <name>', 'Recipient agent name (or * for broadcast)')
|
|
337
|
+
.requiredOption('-m, --message <text>', 'Message body')
|
|
338
|
+
.option('-s, --socket <path>', 'Socket path', DEFAULT_SOCKET_PATH)
|
|
339
|
+
.action(async (options) => {
|
|
340
|
+
const senderName = options.from ?? generateAgentName();
|
|
341
|
+
const client = new RelayClient({
|
|
342
|
+
agentName: senderName,
|
|
343
|
+
socketPath: options.socket,
|
|
344
|
+
});
|
|
345
|
+
try {
|
|
346
|
+
await client.connect();
|
|
347
|
+
const success = client.sendMessage(options.to, options.message);
|
|
348
|
+
if (success) {
|
|
349
|
+
console.log(`Sent: ${options.message}`);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
console.error('Failed to send message');
|
|
353
|
+
}
|
|
354
|
+
// Wait a bit for delivery then exit cleanly
|
|
355
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
356
|
+
client.destroy();
|
|
357
|
+
process.exit(0);
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
console.error('Error:', err);
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
// List connected agents
|
|
365
|
+
program
|
|
366
|
+
.command('agents')
|
|
367
|
+
.description('List connected agents')
|
|
368
|
+
.option('-s, --socket <path>', 'Socket path', DEFAULT_SOCKET_PATH)
|
|
369
|
+
.action(async (_options) => {
|
|
370
|
+
console.log('Note: Agent listing requires daemon introspection (not yet implemented)');
|
|
371
|
+
console.log('Use the status command to check if daemon is running.');
|
|
372
|
+
});
|
|
373
|
+
// Supervisor command
|
|
374
|
+
program
|
|
375
|
+
.command('supervisor')
|
|
376
|
+
.description('Run the spawn-per-message supervisor (CLI-agnostic agent management)')
|
|
377
|
+
.option('-s, --socket <path>', 'Socket path', DEFAULT_SOCKET_PATH)
|
|
378
|
+
.option('-d, --data-dir <path>', 'Data directory for agent state', '/tmp/agent-relay')
|
|
379
|
+
.option('-p, --poll-interval <ms>', 'Polling interval in milliseconds', '2000')
|
|
380
|
+
.option('-v, --verbose', 'Enable verbose logging', false)
|
|
381
|
+
.option('--detach', 'Run supervisor in background (writes supervisor.pid)', false)
|
|
382
|
+
.action(async (options) => {
|
|
383
|
+
if (options.detach) {
|
|
384
|
+
const pid = startDetachedSupervisor({
|
|
385
|
+
socket: options.socket,
|
|
386
|
+
dataDir: options.dataDir,
|
|
387
|
+
pollInterval: parseInt(options.pollInterval, 10),
|
|
388
|
+
verbose: Boolean(options.verbose),
|
|
389
|
+
});
|
|
390
|
+
console.log(`Supervisor started in background (pid ${pid})`);
|
|
391
|
+
console.log(`PID file: ${supervisorPidFilePath(options.dataDir)}`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const supervisor = new Supervisor({
|
|
395
|
+
socketPath: options.socket,
|
|
396
|
+
dataDir: options.dataDir,
|
|
397
|
+
pollIntervalMs: parseInt(options.pollInterval, 10),
|
|
398
|
+
verbose: options.verbose,
|
|
399
|
+
});
|
|
400
|
+
// Handle shutdown
|
|
401
|
+
process.on('SIGINT', () => {
|
|
402
|
+
console.log('\nShutting down supervisor...');
|
|
403
|
+
supervisor.stop();
|
|
404
|
+
process.exit(0);
|
|
405
|
+
});
|
|
406
|
+
process.on('SIGTERM', () => {
|
|
407
|
+
supervisor.stop();
|
|
408
|
+
process.exit(0);
|
|
409
|
+
});
|
|
410
|
+
try {
|
|
411
|
+
await supervisor.start();
|
|
412
|
+
// Keep process alive
|
|
413
|
+
await new Promise(() => { });
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
console.error('Failed to start supervisor:', err);
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
program
|
|
421
|
+
.command('supervisor-status')
|
|
422
|
+
.description('Show background supervisor status (pidfile-based)')
|
|
423
|
+
.option('-d, --data-dir <path>', 'Data directory', '/tmp/agent-relay')
|
|
424
|
+
.action((options) => {
|
|
425
|
+
const pidPath = supervisorPidFilePath(options.dataDir);
|
|
426
|
+
const pid = readPidFile(pidPath);
|
|
427
|
+
if (!pid) {
|
|
428
|
+
console.log('Supervisor: STOPPED (pid file not found)');
|
|
429
|
+
console.log(`PID file: ${pidPath}`);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
console.log(`Supervisor: ${isProcessAlive(pid) ? 'RUNNING' : 'STOPPED'} (pid ${pid})`);
|
|
433
|
+
console.log(`PID file: ${pidPath}`);
|
|
434
|
+
});
|
|
435
|
+
program
|
|
436
|
+
.command('supervisor-stop')
|
|
437
|
+
.description('Stop background supervisor (pidfile-based)')
|
|
438
|
+
.option('-d, --data-dir <path>', 'Data directory', '/tmp/agent-relay')
|
|
439
|
+
.action((options) => {
|
|
440
|
+
const res = stopDetachedSupervisor(options.dataDir);
|
|
441
|
+
if (!res.pid) {
|
|
442
|
+
console.log('Supervisor not running (pid file not found)');
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
console.log(`Stop requested for supervisor pid ${res.pid}`);
|
|
446
|
+
});
|
|
447
|
+
// Register an agent with the supervisor
|
|
448
|
+
program
|
|
449
|
+
.command('register')
|
|
450
|
+
.description('Register an agent with the supervisor for spawn-per-message handling')
|
|
451
|
+
.requiredOption('-n, --name <name>', 'Agent name')
|
|
452
|
+
.requiredOption('-c, --cli <type>', 'CLI type: claude, codex, cursor, or custom')
|
|
453
|
+
.option('-w, --cwd <path>', 'Working directory', process.cwd())
|
|
454
|
+
.option('--command <cmd>', 'Custom command (required for cli=custom)')
|
|
455
|
+
.option('-d, --data-dir <path>', 'Data directory', '/tmp/agent-relay')
|
|
456
|
+
.option('-s, --socket <path>', 'Socket path (for supervisor)', DEFAULT_SOCKET_PATH)
|
|
457
|
+
.option('--no-autostart-supervisor', 'Do not auto-start supervisor', false)
|
|
458
|
+
.action(async (options) => {
|
|
459
|
+
const validCLIs = ['claude', 'codex', 'cursor', 'custom'];
|
|
460
|
+
if (!validCLIs.includes(options.cli)) {
|
|
461
|
+
console.error(`Invalid CLI type: ${options.cli}. Must be one of: ${validCLIs.join(', ')}`);
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
if (options.cli === 'custom' && !options.command) {
|
|
465
|
+
console.error('--command is required when using cli=custom');
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
const supervisor = new Supervisor({ dataDir: options.dataDir });
|
|
469
|
+
const state = supervisor.registerAgent({
|
|
470
|
+
name: options.name,
|
|
471
|
+
cli: options.cli,
|
|
472
|
+
cwd: options.cwd,
|
|
473
|
+
customCommand: options.command,
|
|
474
|
+
});
|
|
475
|
+
console.log(`Registered agent: ${state.name}`);
|
|
476
|
+
console.log(` CLI: ${state.cli}`);
|
|
477
|
+
console.log(` CWD: ${state.cwd}`);
|
|
478
|
+
console.log(` State: ${options.dataDir}/${state.name}/state.json`);
|
|
479
|
+
console.log(` Inbox: ${options.dataDir}/${state.name}/inbox.md`);
|
|
480
|
+
if (options.autostartSupervisor !== false) {
|
|
481
|
+
try {
|
|
482
|
+
const pid = startDetachedSupervisor({
|
|
483
|
+
socket: options.socket,
|
|
484
|
+
dataDir: options.dataDir,
|
|
485
|
+
pollInterval: 2000,
|
|
486
|
+
verbose: false,
|
|
487
|
+
});
|
|
488
|
+
console.log(` Supervisor: running (pid ${pid})`);
|
|
489
|
+
}
|
|
490
|
+
catch (err) {
|
|
491
|
+
console.warn(` Supervisor: failed to autostart (${err.message})`);
|
|
492
|
+
console.warn(` Start manually: agent-relay supervisor -d ${options.dataDir}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
console.log(` Supervisor: not started (run: agent-relay supervisor -d ${options.dataDir})`);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
// Poll inbox (blocking wait for messages)
|
|
500
|
+
program
|
|
501
|
+
.command('inbox-poll')
|
|
502
|
+
.description('Wait for messages in inbox (blocking poll for live agent sessions)')
|
|
503
|
+
.requiredOption('-n, --name <name>', 'Agent name')
|
|
504
|
+
.option('-d, --data-dir <path>', 'Data directory', '/tmp/agent-relay')
|
|
505
|
+
.option('-i, --interval <ms>', 'Poll interval in milliseconds', '2000')
|
|
506
|
+
.option('-t, --timeout <s>', 'Timeout in seconds (0 = forever)', '0')
|
|
507
|
+
.option('--clear', 'Clear inbox after reading', false)
|
|
508
|
+
.option('--pattern <regex>', 'Only return when inbox matches pattern', '## Message from')
|
|
509
|
+
.action(async (options) => {
|
|
510
|
+
// Validate agent name
|
|
511
|
+
if (!options.name || options.name.includes('/') || options.name.includes('..')) {
|
|
512
|
+
console.error('Error: Invalid agent name. Name cannot be empty or contain path separators.');
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
// Validate poll interval
|
|
516
|
+
const pollInterval = parseInt(options.interval, 10);
|
|
517
|
+
if (isNaN(pollInterval) || pollInterval < 100) {
|
|
518
|
+
console.error('Error: Poll interval must be at least 100ms');
|
|
519
|
+
process.exit(1);
|
|
520
|
+
}
|
|
521
|
+
// Validate timeout
|
|
522
|
+
const timeout = parseInt(options.timeout, 10) * 1000;
|
|
523
|
+
if (isNaN(timeout) || timeout < 0) {
|
|
524
|
+
console.error('Error: Timeout must be a non-negative number');
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
// Validate regex pattern
|
|
528
|
+
let pattern;
|
|
529
|
+
try {
|
|
530
|
+
pattern = new RegExp(options.pattern);
|
|
531
|
+
}
|
|
532
|
+
catch (err) {
|
|
533
|
+
console.error(`Error: Invalid regex pattern: ${err.message}`);
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
const inboxPath = path.join(options.dataDir, options.name, 'inbox.md');
|
|
537
|
+
const startTime = Date.now();
|
|
538
|
+
// Ensure inbox directory exists
|
|
539
|
+
const inboxDir = path.dirname(inboxPath);
|
|
540
|
+
if (!fs.existsSync(inboxDir)) {
|
|
541
|
+
fs.mkdirSync(inboxDir, { recursive: true });
|
|
542
|
+
}
|
|
543
|
+
// Initialize empty inbox if doesn't exist
|
|
544
|
+
if (!fs.existsSync(inboxPath)) {
|
|
545
|
+
fs.writeFileSync(inboxPath, '', 'utf-8');
|
|
546
|
+
}
|
|
547
|
+
process.stderr.write(`Polling inbox: ${inboxPath}\n`);
|
|
548
|
+
process.stderr.write(`Pattern: ${options.pattern}\n`);
|
|
549
|
+
process.stderr.write(`Interval: ${pollInterval}ms\n`);
|
|
550
|
+
if (timeout > 0) {
|
|
551
|
+
process.stderr.write(`Timeout: ${options.timeout}s\n`);
|
|
552
|
+
}
|
|
553
|
+
while (true) {
|
|
554
|
+
// Check timeout
|
|
555
|
+
if (timeout > 0 && Date.now() - startTime > timeout) {
|
|
556
|
+
process.stderr.write('Timeout reached\n');
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
// Check inbox
|
|
560
|
+
try {
|
|
561
|
+
const content = fs.readFileSync(inboxPath, 'utf-8');
|
|
562
|
+
if (content.trim() && pattern.test(content)) {
|
|
563
|
+
// Found matching content
|
|
564
|
+
process.stdout.write(content);
|
|
565
|
+
// Clear if requested
|
|
566
|
+
if (options.clear) {
|
|
567
|
+
fs.writeFileSync(inboxPath, '', 'utf-8');
|
|
568
|
+
}
|
|
569
|
+
process.exit(0);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
// File might not exist yet, that's ok
|
|
574
|
+
}
|
|
575
|
+
// Wait before next poll
|
|
576
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
// Read inbox without waiting
|
|
580
|
+
program
|
|
581
|
+
.command('inbox-read')
|
|
582
|
+
.description('Read current inbox contents (non-blocking)')
|
|
583
|
+
.requiredOption('-n, --name <name>', 'Agent name')
|
|
584
|
+
.option('-d, --data-dir <path>', 'Data directory', '/tmp/agent-relay')
|
|
585
|
+
.option('--clear', 'Clear inbox after reading', false)
|
|
586
|
+
.action((options) => {
|
|
587
|
+
// Validate agent name
|
|
588
|
+
if (!options.name || options.name.includes('/') || options.name.includes('..')) {
|
|
589
|
+
console.error('Error: Invalid agent name. Name cannot be empty or contain path separators.');
|
|
590
|
+
process.exit(1);
|
|
591
|
+
}
|
|
592
|
+
const inboxPath = path.join(options.dataDir, options.name, 'inbox.md');
|
|
593
|
+
if (!fs.existsSync(inboxPath)) {
|
|
594
|
+
console.log('(inbox empty)');
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
const content = fs.readFileSync(inboxPath, 'utf-8');
|
|
599
|
+
if (!content.trim()) {
|
|
600
|
+
console.log('(inbox empty)');
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
process.stdout.write(content);
|
|
604
|
+
if (options.clear) {
|
|
605
|
+
fs.writeFileSync(inboxPath, '', 'utf-8');
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
catch (err) {
|
|
609
|
+
console.error(`Error reading inbox: ${err.message}`);
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
// Write to another agent's inbox
|
|
614
|
+
program
|
|
615
|
+
.command('inbox-write')
|
|
616
|
+
.description('Write a message to agent inbox(es). Supports multiple recipients or broadcast.')
|
|
617
|
+
.requiredOption('-t, --to <names>', 'Recipient(s): agent name, comma-separated list, or * for broadcast')
|
|
618
|
+
.requiredOption('-f, --from <name>', 'Sender agent name')
|
|
619
|
+
.requiredOption('-m, --message <text>', 'Message body')
|
|
620
|
+
.option('-d, --data-dir <path>', 'Data directory', '/tmp/agent-relay')
|
|
621
|
+
.action((options) => {
|
|
622
|
+
// Validate sender name
|
|
623
|
+
if (!options.from || options.from.includes('/') || options.from.includes('..')) {
|
|
624
|
+
console.error('Error: Invalid sender name. Name cannot be empty or contain path separators.');
|
|
625
|
+
process.exit(1);
|
|
626
|
+
}
|
|
627
|
+
// Validate message is not empty
|
|
628
|
+
if (!options.message || !options.message.trim()) {
|
|
629
|
+
console.error('Error: Message cannot be empty');
|
|
630
|
+
process.exit(1);
|
|
631
|
+
}
|
|
632
|
+
const timestamp = new Date().toISOString();
|
|
633
|
+
// Match wrapper/supervisor inbox format:
|
|
634
|
+
// ## Message from <sender> | <timestamp>
|
|
635
|
+
// <body>
|
|
636
|
+
const formattedMessage = `\n## Message from ${options.from} | ${timestamp}\n${options.message}\n`;
|
|
637
|
+
// Determine recipients
|
|
638
|
+
let recipients = [];
|
|
639
|
+
if (options.to === '*') {
|
|
640
|
+
// Broadcast to all agents in data-dir except sender
|
|
641
|
+
if (fs.existsSync(options.dataDir)) {
|
|
642
|
+
const entries = fs.readdirSync(options.dataDir, { withFileTypes: true });
|
|
643
|
+
recipients = entries
|
|
644
|
+
.filter(e => e.isDirectory() && e.name !== options.from)
|
|
645
|
+
.map(e => e.name);
|
|
646
|
+
}
|
|
647
|
+
if (recipients.length === 0) {
|
|
648
|
+
console.log('No other agents found for broadcast');
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
// Parse comma-separated list
|
|
654
|
+
recipients = options.to.split(',').map((r) => r.trim()).filter((r) => r);
|
|
655
|
+
// Validate recipient names
|
|
656
|
+
for (const r of recipients) {
|
|
657
|
+
if (r.includes('/') || r.includes('..')) {
|
|
658
|
+
console.error(`Error: Invalid recipient name "${r}". Name cannot contain path separators.`);
|
|
659
|
+
process.exit(1);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (recipients.length === 0) {
|
|
663
|
+
console.error('Error: At least one recipient is required');
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
// Write to each recipient
|
|
668
|
+
let successCount = 0;
|
|
669
|
+
for (const recipient of recipients) {
|
|
670
|
+
const inboxPath = path.join(options.dataDir, recipient, 'inbox.md');
|
|
671
|
+
// Ensure directory exists
|
|
672
|
+
const inboxDir = path.dirname(inboxPath);
|
|
673
|
+
try {
|
|
674
|
+
if (!fs.existsSync(inboxDir)) {
|
|
675
|
+
fs.mkdirSync(inboxDir, { recursive: true });
|
|
676
|
+
}
|
|
677
|
+
fs.appendFileSync(inboxPath, formattedMessage, 'utf-8');
|
|
678
|
+
console.log(`Message written to ${recipient}`);
|
|
679
|
+
successCount++;
|
|
680
|
+
}
|
|
681
|
+
catch (err) {
|
|
682
|
+
console.error(`Error writing to ${recipient}: ${err.message}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (successCount === 0) {
|
|
686
|
+
process.exit(1);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
// Dynamic team management
|
|
690
|
+
program
|
|
691
|
+
.command('team-init')
|
|
692
|
+
.description('Initialize a team workspace for multi-agent collaboration')
|
|
693
|
+
.option('-d, --data-dir <path>', 'Team data directory', '/tmp/agent-relay-team')
|
|
694
|
+
.option('-p, --project <path>', 'Project directory agents will work on', process.cwd())
|
|
695
|
+
.option('-n, --name <name>', 'Team/project name', 'agent-team')
|
|
696
|
+
.action((options) => {
|
|
697
|
+
const teamDir = options.dataDir;
|
|
698
|
+
const configPath = path.join(teamDir, 'team.json');
|
|
699
|
+
fs.mkdirSync(teamDir, { recursive: true });
|
|
700
|
+
const config = {
|
|
701
|
+
name: options.name,
|
|
702
|
+
projectDir: options.project,
|
|
703
|
+
createdAt: new Date().toISOString(),
|
|
704
|
+
agents: [],
|
|
705
|
+
};
|
|
706
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
707
|
+
console.log(`Team workspace initialized: ${teamDir}`);
|
|
708
|
+
console.log(`Project: ${options.project}`);
|
|
709
|
+
console.log('');
|
|
710
|
+
console.log('Add agents with:');
|
|
711
|
+
console.log(` agent-relay team-add -n AgentName -c claude -r "Role" -t "Task" -d ${teamDir}`);
|
|
712
|
+
});
|
|
713
|
+
program
|
|
714
|
+
.command('team-add')
|
|
715
|
+
.description('Add an agent to the team')
|
|
716
|
+
.requiredOption('-n, --name <name>', 'Agent name')
|
|
717
|
+
.requiredOption('-c, --cli <type>', 'CLI type: claude, codex, gemini, cursor')
|
|
718
|
+
.requiredOption('-r, --role <role>', 'Agent role (e.g., "Documentation Lead")')
|
|
719
|
+
.option('-t, --task <task>', 'Task (repeatable)', (val, arr) => [...arr, val], [])
|
|
720
|
+
.option('-d, --data-dir <path>', 'Team data directory', '/tmp/agent-relay-team')
|
|
721
|
+
.action((options) => {
|
|
722
|
+
const teamDir = options.dataDir;
|
|
723
|
+
const configPath = path.join(teamDir, 'team.json');
|
|
724
|
+
let config;
|
|
725
|
+
if (fs.existsSync(configPath)) {
|
|
726
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
fs.mkdirSync(teamDir, { recursive: true });
|
|
730
|
+
config = { name: 'agent-team', projectDir: process.cwd(), createdAt: new Date().toISOString(), agents: [] };
|
|
731
|
+
}
|
|
732
|
+
const existingIdx = config.agents.findIndex(a => a.name === options.name);
|
|
733
|
+
const agentData = { name: options.name, cli: options.cli, role: options.role, tasks: options.task };
|
|
734
|
+
if (existingIdx >= 0) {
|
|
735
|
+
config.agents[existingIdx] = agentData;
|
|
736
|
+
console.log(`Updated agent: ${options.name}`);
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
config.agents.push(agentData);
|
|
740
|
+
console.log(`Added agent: ${options.name}`);
|
|
741
|
+
}
|
|
742
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
743
|
+
const agentDir = path.join(teamDir, options.name);
|
|
744
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
745
|
+
const inboxPath = path.join(agentDir, 'inbox.md');
|
|
746
|
+
if (!fs.existsSync(inboxPath))
|
|
747
|
+
fs.writeFileSync(inboxPath, '');
|
|
748
|
+
const teammates = config.agents.filter(a => a.name !== options.name).map(a => a.name);
|
|
749
|
+
const taskList = options.task.length > 0 ? options.task.map((t, i) => `${i + 1}. ${t}`).join('\n') : '(Check with teammates for tasks)';
|
|
750
|
+
const teammateList = teammates.length > 0 ? teammates.join(', ') : '(No other agents yet)';
|
|
751
|
+
const instructions = `# You are ${options.name} - ${options.role}
|
|
752
|
+
|
|
753
|
+
## Project Location
|
|
754
|
+
\`${config.projectDir}\`
|
|
755
|
+
|
|
756
|
+
## Your Tasks
|
|
757
|
+
${taskList}
|
|
758
|
+
|
|
759
|
+
## Communication
|
|
760
|
+
|
|
761
|
+
**Your inbox:** \`${teamDir}/${options.name}/inbox.md\`
|
|
762
|
+
**Teammates:** ${teammateList}
|
|
763
|
+
|
|
764
|
+
### Commands
|
|
765
|
+
\`\`\`bash
|
|
766
|
+
# Check inbox (non-blocking peek)
|
|
767
|
+
node ${config.projectDir}/dist/cli/index.js team-check -n ${options.name} -d ${teamDir} --no-wait
|
|
768
|
+
|
|
769
|
+
# Check inbox (BLOCKS until message arrives)
|
|
770
|
+
node ${config.projectDir}/dist/cli/index.js team-check -n ${options.name} -d ${teamDir} --clear
|
|
771
|
+
|
|
772
|
+
# Send to teammate
|
|
773
|
+
node ${config.projectDir}/dist/cli/index.js team-send -n ${options.name} -t TEAMMATE -m "MESSAGE" -d ${teamDir}
|
|
774
|
+
|
|
775
|
+
# Broadcast to all
|
|
776
|
+
node ${config.projectDir}/dist/cli/index.js team-send -n ${options.name} -t "*" -m "MESSAGE" -d ${teamDir}
|
|
777
|
+
|
|
778
|
+
# Team status
|
|
779
|
+
node ${config.projectDir}/dist/cli/index.js team-status -d ${teamDir}
|
|
780
|
+
\`\`\`
|
|
781
|
+
|
|
782
|
+
### Protocol
|
|
783
|
+
- \`STATUS: <doing what>\` - Progress update
|
|
784
|
+
- \`DONE: <task>\` - Completed
|
|
785
|
+
- \`QUESTION: @Name <q>\` - Ask teammate
|
|
786
|
+
- \`BLOCKER: <issue>\` - Blocked
|
|
787
|
+
|
|
788
|
+
## CRITICAL: Work Loop (MUST FOLLOW)
|
|
789
|
+
\`\`\`
|
|
790
|
+
REPEAT:
|
|
791
|
+
1. CHECK inbox (--no-wait)
|
|
792
|
+
2. RESPOND to any messages
|
|
793
|
+
3. DO one small task step (max 5 min work)
|
|
794
|
+
4. BROADCAST status update
|
|
795
|
+
5. GOTO 1
|
|
796
|
+
\`\`\`
|
|
797
|
+
|
|
798
|
+
**You MUST check inbox and broadcast after EVERY task step. Never go silent!**
|
|
799
|
+
|
|
800
|
+
## Start Now
|
|
801
|
+
1. Run team-check --no-wait to see any messages
|
|
802
|
+
2. Broadcast: STATUS: ${options.name} starting [first task]
|
|
803
|
+
3. Follow the work loop above
|
|
804
|
+
`;
|
|
805
|
+
const instructionsPath = path.join(agentDir, 'INSTRUCTIONS.md');
|
|
806
|
+
fs.writeFileSync(instructionsPath, instructions);
|
|
807
|
+
console.log(` CLI: ${options.cli}`);
|
|
808
|
+
console.log(` Role: ${options.role}`);
|
|
809
|
+
console.log(` Instructions: ${instructionsPath}`);
|
|
810
|
+
console.log('');
|
|
811
|
+
console.log('To start:');
|
|
812
|
+
console.log(` cd ${config.projectDir} && ${options.cli}`);
|
|
813
|
+
console.log(` Say: Read ${instructionsPath} and start working`);
|
|
814
|
+
});
|
|
815
|
+
program
|
|
816
|
+
.command('team-list')
|
|
817
|
+
.description('List all agents in the team')
|
|
818
|
+
.option('-d, --data-dir <path>', 'Team data directory', '/tmp/agent-relay-team')
|
|
819
|
+
.option('--instructions', 'Show startup instructions', false)
|
|
820
|
+
.action((options) => {
|
|
821
|
+
const configPath = path.join(options.dataDir, 'team.json');
|
|
822
|
+
if (!fs.existsSync(configPath)) {
|
|
823
|
+
console.log('No team found. Initialize with: agent-relay team-init');
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
827
|
+
console.log(`Team: ${config.name}`);
|
|
828
|
+
console.log(`Project: ${config.projectDir}`);
|
|
829
|
+
console.log(`Agents: ${config.agents.length}`);
|
|
830
|
+
console.log('');
|
|
831
|
+
for (const agent of config.agents) {
|
|
832
|
+
const inboxPath = path.join(options.dataDir, agent.name, 'inbox.md');
|
|
833
|
+
const hasMessages = fs.existsSync(inboxPath) && fs.statSync(inboxPath).size > 10;
|
|
834
|
+
const instructionsPath = path.join(options.dataDir, agent.name, 'INSTRUCTIONS.md');
|
|
835
|
+
console.log(`āāā ${agent.name} (${agent.cli}) ${hasMessages ? 'š¬' : ''}`);
|
|
836
|
+
console.log(` Role: ${agent.role}`);
|
|
837
|
+
if (agent.tasks?.length > 0)
|
|
838
|
+
console.log(` Tasks: ${agent.tasks.join(', ')}`);
|
|
839
|
+
if (options.instructions) {
|
|
840
|
+
console.log(` Start: cd ${config.projectDir} && ${agent.cli}`);
|
|
841
|
+
console.log(` Say: Read ${instructionsPath} and start working`);
|
|
842
|
+
}
|
|
843
|
+
console.log('');
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
// One-shot team setup from JSON config
|
|
847
|
+
program
|
|
848
|
+
.command('team-setup')
|
|
849
|
+
.description('Create a complete team from a JSON config file or inline JSON')
|
|
850
|
+
.option('-f, --file <path>', 'Path to JSON config file')
|
|
851
|
+
.option('-c, --config <json>', 'Inline JSON config')
|
|
852
|
+
.option('-d, --data-dir <path>', 'Team data directory', '/tmp/agent-relay-team')
|
|
853
|
+
.action((options) => {
|
|
854
|
+
let config;
|
|
855
|
+
if (options.file) {
|
|
856
|
+
if (!fs.existsSync(options.file)) {
|
|
857
|
+
console.error(`Config file not found: ${options.file}`);
|
|
858
|
+
process.exit(1);
|
|
859
|
+
}
|
|
860
|
+
config = JSON.parse(fs.readFileSync(options.file, 'utf-8'));
|
|
861
|
+
}
|
|
862
|
+
else if (options.config) {
|
|
863
|
+
config = JSON.parse(options.config);
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
console.error('Provide --file or --config');
|
|
867
|
+
process.exit(1);
|
|
868
|
+
}
|
|
869
|
+
const teamDir = options.dataDir;
|
|
870
|
+
const projectDir = config.project || process.cwd();
|
|
871
|
+
// Create team config
|
|
872
|
+
fs.mkdirSync(teamDir, { recursive: true });
|
|
873
|
+
const teamConfig = {
|
|
874
|
+
name: config.name || 'agent-team',
|
|
875
|
+
projectDir,
|
|
876
|
+
createdAt: new Date().toISOString(),
|
|
877
|
+
agents: config.agents.map(a => ({ ...a, tasks: a.tasks || [] })),
|
|
878
|
+
};
|
|
879
|
+
fs.writeFileSync(path.join(teamDir, 'team.json'), JSON.stringify(teamConfig, null, 2));
|
|
880
|
+
// Create each agent
|
|
881
|
+
for (const agent of config.agents) {
|
|
882
|
+
const agentDir = path.join(teamDir, agent.name);
|
|
883
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
884
|
+
fs.writeFileSync(path.join(agentDir, 'inbox.md'), '');
|
|
885
|
+
const teammates = config.agents.filter(a => a.name !== agent.name).map(a => a.name);
|
|
886
|
+
const taskList = (agent.tasks || []).map((t, i) => `${i + 1}. ${t}`).join('\n') || '(Check with teammates)';
|
|
887
|
+
const instructions = `# You are ${agent.name} - ${agent.role}
|
|
888
|
+
|
|
889
|
+
## Project: \`${projectDir}\`
|
|
890
|
+
|
|
891
|
+
## Tasks
|
|
892
|
+
${taskList}
|
|
893
|
+
|
|
894
|
+
## Teammates: ${teammates.join(', ') || 'none yet'}
|
|
895
|
+
|
|
896
|
+
## Commands
|
|
897
|
+
\`\`\`bash
|
|
898
|
+
# Check inbox (non-blocking peek)
|
|
899
|
+
node ${projectDir}/dist/cli/index.js team-check -n ${agent.name} -d ${teamDir} --no-wait
|
|
900
|
+
|
|
901
|
+
# Check inbox (BLOCKS until message arrives)
|
|
902
|
+
node ${projectDir}/dist/cli/index.js team-check -n ${agent.name} -d ${teamDir} --clear
|
|
903
|
+
|
|
904
|
+
# Send message
|
|
905
|
+
node ${projectDir}/dist/cli/index.js team-send -n ${agent.name} -t RECIPIENT -m "message" -d ${teamDir}
|
|
906
|
+
|
|
907
|
+
# Broadcast to all
|
|
908
|
+
node ${projectDir}/dist/cli/index.js team-send -n ${agent.name} -t "*" -m "message" -d ${teamDir}
|
|
909
|
+
|
|
910
|
+
# Team status
|
|
911
|
+
node ${projectDir}/dist/cli/index.js team-status -d ${teamDir}
|
|
912
|
+
\`\`\`
|
|
913
|
+
|
|
914
|
+
## Protocol
|
|
915
|
+
- \`STATUS: <doing>\` - Update
|
|
916
|
+
- \`DONE: <task>\` - Complete
|
|
917
|
+
- \`QUESTION: @Name <q>\` - Ask
|
|
918
|
+
- \`BLOCKER: <issue>\` - Stuck
|
|
919
|
+
|
|
920
|
+
## CRITICAL: Work Loop (MUST FOLLOW)
|
|
921
|
+
\`\`\`
|
|
922
|
+
REPEAT:
|
|
923
|
+
1. CHECK inbox (--no-wait)
|
|
924
|
+
2. RESPOND to any messages
|
|
925
|
+
3. DO one small task step (max 5 min work)
|
|
926
|
+
4. BROADCAST status update
|
|
927
|
+
5. GOTO 1
|
|
928
|
+
\`\`\`
|
|
929
|
+
|
|
930
|
+
**You MUST check inbox and broadcast after EVERY task step. Never go silent!**
|
|
931
|
+
|
|
932
|
+
## Start Now
|
|
933
|
+
1. Run team-check --no-wait to see any messages
|
|
934
|
+
2. Broadcast: STATUS: ${agent.name} starting [first task]
|
|
935
|
+
3. Follow the work loop above
|
|
936
|
+
`;
|
|
937
|
+
fs.writeFileSync(path.join(agentDir, 'INSTRUCTIONS.md'), instructions);
|
|
938
|
+
}
|
|
939
|
+
console.log(`Team "${teamConfig.name}" created with ${config.agents.length} agents`);
|
|
940
|
+
console.log(`Directory: ${teamDir}`);
|
|
941
|
+
console.log('');
|
|
942
|
+
console.log('Start agents with:');
|
|
943
|
+
for (const agent of config.agents) {
|
|
944
|
+
console.log(` ${agent.cli}: Read ${teamDir}/${agent.name}/INSTRUCTIONS.md and start`);
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
// Self-register to a team (for agents to join)
|
|
948
|
+
program
|
|
949
|
+
.command('team-join')
|
|
950
|
+
.description('Join an existing team (for agents to self-register)')
|
|
951
|
+
.requiredOption('-n, --name <name>', 'Your agent name')
|
|
952
|
+
.requiredOption('-c, --cli <type>', 'Your CLI type')
|
|
953
|
+
.requiredOption('-r, --role <role>', 'Your role')
|
|
954
|
+
.option('-t, --task <task>', 'Your task (repeatable)', (v, a) => [...a, v], [])
|
|
955
|
+
.option('-d, --data-dir <path>', 'Team directory', '/tmp/agent-relay-team')
|
|
956
|
+
.action((options) => {
|
|
957
|
+
const configPath = path.join(options.dataDir, 'team.json');
|
|
958
|
+
if (!fs.existsSync(configPath)) {
|
|
959
|
+
console.error('No team found. Create one first with team-init or team-setup');
|
|
960
|
+
process.exit(1);
|
|
961
|
+
}
|
|
962
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
963
|
+
// Add or update agent
|
|
964
|
+
const idx = config.agents.findIndex((a) => a.name === options.name);
|
|
965
|
+
const agentData = { name: options.name, cli: options.cli, role: options.role, tasks: options.task };
|
|
966
|
+
if (idx >= 0) {
|
|
967
|
+
config.agents[idx] = agentData;
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
config.agents.push(agentData);
|
|
971
|
+
}
|
|
972
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
973
|
+
// Create inbox
|
|
974
|
+
const agentDir = path.join(options.dataDir, options.name);
|
|
975
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
976
|
+
if (!fs.existsSync(path.join(agentDir, 'inbox.md'))) {
|
|
977
|
+
fs.writeFileSync(path.join(agentDir, 'inbox.md'), '');
|
|
978
|
+
}
|
|
979
|
+
console.log(`Joined team as ${options.name} (${options.role})`);
|
|
980
|
+
console.log(`Teammates: ${config.agents.filter((a) => a.name !== options.name).map((a) => a.name).join(', ')}`);
|
|
981
|
+
});
|
|
982
|
+
// Quick team status
|
|
983
|
+
program
|
|
984
|
+
.command('team-status')
|
|
985
|
+
.description('Show team status with message counts')
|
|
986
|
+
.option('-d, --data-dir <path>', 'Team directory', '/tmp/agent-relay-team')
|
|
987
|
+
.action((options) => {
|
|
988
|
+
const configPath = path.join(options.dataDir, 'team.json');
|
|
989
|
+
if (!fs.existsSync(configPath)) {
|
|
990
|
+
console.log('No team found');
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
994
|
+
console.log(`\nš ${config.name} | ${config.agents.length} agents\n`);
|
|
995
|
+
for (const agent of config.agents) {
|
|
996
|
+
const inboxPath = path.join(options.dataDir, agent.name, 'inbox.md');
|
|
997
|
+
let msgCount = 0;
|
|
998
|
+
if (fs.existsSync(inboxPath)) {
|
|
999
|
+
const content = fs.readFileSync(inboxPath, 'utf-8');
|
|
1000
|
+
msgCount = (content.match(/## Message from/g) || []).length;
|
|
1001
|
+
}
|
|
1002
|
+
const icon = msgCount > 0 ? 'š¬' : 'š';
|
|
1003
|
+
console.log(` ${icon} ${agent.name} (${agent.cli}) - ${agent.role}${msgCount > 0 ? ` [${msgCount} msg]` : ''}`);
|
|
1004
|
+
}
|
|
1005
|
+
console.log('');
|
|
1006
|
+
});
|
|
1007
|
+
// Simplified send (team-aware)
|
|
1008
|
+
program
|
|
1009
|
+
.command('team-send')
|
|
1010
|
+
.description('Send a message to teammate(s)')
|
|
1011
|
+
.requiredOption('-n, --name <name>', 'Your agent name')
|
|
1012
|
+
.requiredOption('-t, --to <recipient>', 'Recipient name or * for broadcast')
|
|
1013
|
+
.requiredOption('-m, --message <text>', 'Message')
|
|
1014
|
+
.option('-d, --data-dir <path>', 'Team directory', '/tmp/agent-relay-team')
|
|
1015
|
+
.action((options) => {
|
|
1016
|
+
const configPath = path.join(options.dataDir, 'team.json');
|
|
1017
|
+
if (!fs.existsSync(configPath)) {
|
|
1018
|
+
console.error('No team found');
|
|
1019
|
+
process.exit(1);
|
|
1020
|
+
}
|
|
1021
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1022
|
+
const timestamp = new Date().toISOString();
|
|
1023
|
+
const msg = `\n## Message from ${options.name} | ${timestamp}\n${options.message}\n`;
|
|
1024
|
+
let recipients = [];
|
|
1025
|
+
if (options.to === '*') {
|
|
1026
|
+
recipients = config.agents
|
|
1027
|
+
.filter((a) => a.name !== options.name)
|
|
1028
|
+
.map((a) => a.name);
|
|
1029
|
+
}
|
|
1030
|
+
else {
|
|
1031
|
+
recipients = options.to.split(',').map((r) => r.trim());
|
|
1032
|
+
}
|
|
1033
|
+
for (const r of recipients) {
|
|
1034
|
+
const inbox = path.join(options.dataDir, r, 'inbox.md');
|
|
1035
|
+
if (fs.existsSync(path.dirname(inbox))) {
|
|
1036
|
+
fs.appendFileSync(inbox, msg);
|
|
1037
|
+
console.log(`ā ${r}`);
|
|
1038
|
+
}
|
|
1039
|
+
else {
|
|
1040
|
+
console.log(`ā ${r} (not found)`);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
// Simplified check inbox (team-aware)
|
|
1045
|
+
program
|
|
1046
|
+
.command('team-check')
|
|
1047
|
+
.description('Check your inbox (blocking wait for messages)')
|
|
1048
|
+
.requiredOption('-n, --name <name>', 'Your agent name')
|
|
1049
|
+
.option('-d, --data-dir <path>', 'Team directory', '/tmp/agent-relay-team')
|
|
1050
|
+
.option('--no-wait', 'Just read, don\'t wait for messages')
|
|
1051
|
+
.option('--clear', 'Clear inbox after reading')
|
|
1052
|
+
.option('-t, --timeout <seconds>', 'Timeout in seconds (0=forever)', '0')
|
|
1053
|
+
.action(async (options) => {
|
|
1054
|
+
const inboxPath = path.join(options.dataDir, options.name, 'inbox.md');
|
|
1055
|
+
if (!fs.existsSync(path.dirname(inboxPath))) {
|
|
1056
|
+
fs.mkdirSync(path.dirname(inboxPath), { recursive: true });
|
|
1057
|
+
}
|
|
1058
|
+
if (!fs.existsSync(inboxPath)) {
|
|
1059
|
+
fs.writeFileSync(inboxPath, '');
|
|
1060
|
+
}
|
|
1061
|
+
const timeout = parseInt(options.timeout, 10) * 1000;
|
|
1062
|
+
const startTime = Date.now();
|
|
1063
|
+
if (options.wait === false) {
|
|
1064
|
+
// Just read
|
|
1065
|
+
const content = fs.readFileSync(inboxPath, 'utf-8');
|
|
1066
|
+
if (content.trim()) {
|
|
1067
|
+
process.stdout.write(content);
|
|
1068
|
+
if (options.clear)
|
|
1069
|
+
fs.writeFileSync(inboxPath, '');
|
|
1070
|
+
}
|
|
1071
|
+
else {
|
|
1072
|
+
console.log('(no messages)');
|
|
1073
|
+
}
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
// Blocking wait
|
|
1077
|
+
process.stderr.write(`Waiting for messages...\n`);
|
|
1078
|
+
while (true) {
|
|
1079
|
+
if (timeout > 0 && Date.now() - startTime > timeout) {
|
|
1080
|
+
console.log('(timeout)');
|
|
1081
|
+
process.exit(1);
|
|
1082
|
+
}
|
|
1083
|
+
const content = fs.readFileSync(inboxPath, 'utf-8');
|
|
1084
|
+
if (content.includes('## Message from')) {
|
|
1085
|
+
process.stdout.write(content);
|
|
1086
|
+
if (options.clear)
|
|
1087
|
+
fs.writeFileSync(inboxPath, '');
|
|
1088
|
+
process.exit(0);
|
|
1089
|
+
}
|
|
1090
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
// List agents in a data directory (for games)
|
|
1094
|
+
program
|
|
1095
|
+
.command('inbox-agents')
|
|
1096
|
+
.description('List all agents with inboxes in a data directory')
|
|
1097
|
+
.option('-d, --data-dir <path>', 'Data directory', '/tmp/agent-relay')
|
|
1098
|
+
.action((options) => {
|
|
1099
|
+
if (!fs.existsSync(options.dataDir)) {
|
|
1100
|
+
console.log('No agents found');
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
const entries = fs.readdirSync(options.dataDir, { withFileTypes: true });
|
|
1104
|
+
const agents = entries
|
|
1105
|
+
.filter(e => e.isDirectory())
|
|
1106
|
+
.map(e => e.name);
|
|
1107
|
+
if (agents.length === 0) {
|
|
1108
|
+
console.log('No agents found');
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
console.log('Agents:');
|
|
1112
|
+
for (const agent of agents) {
|
|
1113
|
+
const inboxPath = path.join(options.dataDir, agent, 'inbox.md');
|
|
1114
|
+
const hasInbox = fs.existsSync(inboxPath);
|
|
1115
|
+
const inboxSize = hasInbox ? fs.statSync(inboxPath).size : 0;
|
|
1116
|
+
console.log(` ${agent}${inboxSize > 0 ? ' (has messages)' : ''}`);
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
// Tic-tac-toe setup helper (writes instruction files + clears inboxes)
|
|
1120
|
+
program
|
|
1121
|
+
.command('tictactoe-setup')
|
|
1122
|
+
.description('Create tic-tac-toe instruction files + empty inboxes for two agents')
|
|
1123
|
+
.option('-d, --data-dir <path>', 'Data directory', '/tmp/agent-relay-ttt')
|
|
1124
|
+
.option('--player-x <name>', 'Player X name', 'PlayerX')
|
|
1125
|
+
.option('--player-o <name>', 'Player O name', 'PlayerO')
|
|
1126
|
+
.action((options) => {
|
|
1127
|
+
const res = setupTicTacToe({
|
|
1128
|
+
dataDir: options.dataDir,
|
|
1129
|
+
playerX: options.playerX,
|
|
1130
|
+
playerO: options.playerO,
|
|
1131
|
+
});
|
|
1132
|
+
console.log('Created tic-tac-toe instructions:');
|
|
1133
|
+
console.log(` X: ${res.instructionsXPath}`);
|
|
1134
|
+
console.log(` O: ${res.instructionsOPath}`);
|
|
1135
|
+
console.log('');
|
|
1136
|
+
console.log('To start (2 terminals):');
|
|
1137
|
+
console.log(` Terminal 1: start your agent, then read ${res.instructionsXPath}`);
|
|
1138
|
+
console.log(` Terminal 2: start your agent, then read ${res.instructionsOPath}`);
|
|
1139
|
+
});
|
|
1140
|
+
// List registered agents
|
|
1141
|
+
program
|
|
1142
|
+
.command('list')
|
|
1143
|
+
.description('List registered agents')
|
|
1144
|
+
.option('-d, --data-dir <path>', 'Data directory', '/tmp/agent-relay')
|
|
1145
|
+
.action(async (options) => {
|
|
1146
|
+
const supervisor = new Supervisor({ dataDir: options.dataDir });
|
|
1147
|
+
const agents = supervisor.getAgents();
|
|
1148
|
+
if (agents.length === 0) {
|
|
1149
|
+
console.log('No registered agents');
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
const supPidPath = supervisorPidFilePath(options.dataDir);
|
|
1153
|
+
const supPid = readPidFile(supPidPath);
|
|
1154
|
+
const supRunning = supPid ? isProcessAlive(supPid) : false;
|
|
1155
|
+
console.log(`Supervisor: ${supRunning ? `RUNNING (pid ${supPid})` : 'STOPPED'}`);
|
|
1156
|
+
console.log('Registered agents:');
|
|
1157
|
+
for (const name of agents) {
|
|
1158
|
+
const diag = supervisor.getAgentDiagnostics(name);
|
|
1159
|
+
const state = diag.state;
|
|
1160
|
+
if (state) {
|
|
1161
|
+
const status = state.status ?? 'idle';
|
|
1162
|
+
const lock = diag.locked ? 'locked' : 'unlocked';
|
|
1163
|
+
const inbox = diag.hasUnreadInbox ? 'unread' : 'clear';
|
|
1164
|
+
console.log(` ${name} (${state.cli}) - ${status}, ${lock}, inbox:${inbox}`);
|
|
1165
|
+
console.log(` cwd: ${state.cwd}`);
|
|
1166
|
+
console.log(` state: ${diag.statePath}`);
|
|
1167
|
+
console.log(` inbox: ${diag.inboxPath}`);
|
|
1168
|
+
}
|
|
1169
|
+
else {
|
|
1170
|
+
console.log(` ${name} (missing/invalid state.json)`);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
// Dashboard command
|
|
1175
|
+
program
|
|
1176
|
+
.command('dashboard')
|
|
1177
|
+
.description('Start the web dashboard')
|
|
1178
|
+
.option('-p, --port <number>', 'Port to run on', DEFAULT_DASHBOARD_PORT)
|
|
1179
|
+
.option('-d, --data-dir <path>', 'Team data directory', '/tmp/agent-relay-team')
|
|
1180
|
+
.option('--db-path <path>', 'SQLite DB path for message storage', '/tmp/agent-relay.sqlite')
|
|
1181
|
+
.action(async (options) => {
|
|
1182
|
+
const { startDashboard } = await import('../dashboard/server.js');
|
|
1183
|
+
await startDashboard(parseInt(options.port, 10), options.dataDir, options.dbPath);
|
|
1184
|
+
});
|
|
1185
|
+
// Team listen daemon - watches inboxes and spawns agents when messages arrive
|
|
1186
|
+
program
|
|
1187
|
+
.command('team-listen')
|
|
1188
|
+
.description('Watch inboxes and spawn agents when messages arrive (for Codex, Gemini, etc.)')
|
|
1189
|
+
.option('-d, --data-dir <path>', 'Team data directory', '/tmp/agent-relay-team')
|
|
1190
|
+
.option('-a, --agents <names>', 'Comma-separated agent names to watch (default: all)')
|
|
1191
|
+
.option('--debounce <ms>', 'Debounce time before spawning (ms)', '3000')
|
|
1192
|
+
.option('--cooldown <s>', 'Minimum seconds between spawns per agent', '60')
|
|
1193
|
+
.option('--dry-run', 'Log what would happen without spawning', false)
|
|
1194
|
+
.action(async (options) => {
|
|
1195
|
+
const chokidar = await import('chokidar');
|
|
1196
|
+
const { spawn } = await import('child_process');
|
|
1197
|
+
const { AgentStateManager } = await import('../state/agent-state.js');
|
|
1198
|
+
const configPath = path.join(options.dataDir, 'team.json');
|
|
1199
|
+
if (!fs.existsSync(configPath)) {
|
|
1200
|
+
console.error('No team found. Initialize with: team-init or team-setup');
|
|
1201
|
+
process.exit(1);
|
|
1202
|
+
}
|
|
1203
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1204
|
+
const stateManager = new AgentStateManager(options.dataDir);
|
|
1205
|
+
const debounceMs = parseInt(options.debounce, 10);
|
|
1206
|
+
const cooldownMs = parseInt(options.cooldown, 10) * 1000;
|
|
1207
|
+
// Filter agents
|
|
1208
|
+
const watchAgents = options.agents
|
|
1209
|
+
? options.agents.split(',').map((n) => n.trim())
|
|
1210
|
+
: config.agents.map((a) => a.name);
|
|
1211
|
+
const agents = config.agents.filter((a) => watchAgents.includes(a.name));
|
|
1212
|
+
if (agents.length === 0) {
|
|
1213
|
+
console.error('No matching agents found');
|
|
1214
|
+
process.exit(1);
|
|
1215
|
+
}
|
|
1216
|
+
// Track state
|
|
1217
|
+
const lastSpawn = new Map();
|
|
1218
|
+
const debounceTimers = new Map();
|
|
1219
|
+
const lastInboxSize = new Map();
|
|
1220
|
+
console.log('Agent Relay Listener');
|
|
1221
|
+
console.log('====================');
|
|
1222
|
+
console.log(`Directory: ${options.dataDir}`);
|
|
1223
|
+
console.log(`Watching: ${agents.map((a) => a.name).join(', ')}`);
|
|
1224
|
+
console.log(`Debounce: ${debounceMs}ms, Cooldown: ${cooldownMs / 1000}s`);
|
|
1225
|
+
console.log('');
|
|
1226
|
+
// Get spawn command for CLI type
|
|
1227
|
+
const getSpawnCmd = (cli) => {
|
|
1228
|
+
switch (cli.toLowerCase()) {
|
|
1229
|
+
case 'claude':
|
|
1230
|
+
return { cmd: 'claude', args: ['--dangerously-skip-permissions'] };
|
|
1231
|
+
case 'codex':
|
|
1232
|
+
return { cmd: 'codex', args: [] };
|
|
1233
|
+
case 'gemini':
|
|
1234
|
+
return { cmd: 'gemini', args: [] };
|
|
1235
|
+
case 'cursor':
|
|
1236
|
+
return { cmd: 'cursor', args: ['--cli'] };
|
|
1237
|
+
default:
|
|
1238
|
+
return { cmd: cli, args: [] };
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
// Spawn agent with context
|
|
1242
|
+
const spawnAgent = (agent) => {
|
|
1243
|
+
const now = Date.now();
|
|
1244
|
+
const last = lastSpawn.get(agent.name) || 0;
|
|
1245
|
+
// Check cooldown
|
|
1246
|
+
if (now - last < cooldownMs) {
|
|
1247
|
+
console.log(`[${new Date().toISOString()}] ${agent.name}: cooling down (${Math.round((cooldownMs - (now - last)) / 1000)}s remaining)`);
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
// Check inbox has content
|
|
1251
|
+
const inboxPath = path.join(options.dataDir, agent.name, 'inbox.md');
|
|
1252
|
+
if (!fs.existsSync(inboxPath))
|
|
1253
|
+
return;
|
|
1254
|
+
const content = fs.readFileSync(inboxPath, 'utf-8');
|
|
1255
|
+
if (!content.includes('## Message from'))
|
|
1256
|
+
return;
|
|
1257
|
+
const { cmd, args } = getSpawnCmd(agent.cli);
|
|
1258
|
+
const instructionsPath = path.join(options.dataDir, agent.name, 'INSTRUCTIONS.md');
|
|
1259
|
+
// Build context-aware prompt
|
|
1260
|
+
const stateContext = stateManager.formatAsContext(agent.name);
|
|
1261
|
+
const prompt = `${stateContext}
|
|
1262
|
+
|
|
1263
|
+
You have NEW MESSAGES! Read ${instructionsPath} for your role.
|
|
1264
|
+
|
|
1265
|
+
IMMEDIATE ACTIONS:
|
|
1266
|
+
1. Check inbox: node ${config.projectDir}/dist/cli/index.js team-check -n ${agent.name} -d ${options.dataDir} --no-wait
|
|
1267
|
+
2. Respond to messages
|
|
1268
|
+
3. Do ONE task step
|
|
1269
|
+
4. Broadcast status update
|
|
1270
|
+
5. Before exiting, save your state by outputting:
|
|
1271
|
+
[[STATE]]{"currentTask": "what you're working on", "context": "brief summary of progress"}[[/STATE]]
|
|
1272
|
+
|
|
1273
|
+
GO!`;
|
|
1274
|
+
console.log(`[${new Date().toISOString()}] Spawning ${agent.name} (${agent.cli})...`);
|
|
1275
|
+
if (options.dryRun) {
|
|
1276
|
+
console.log(` DRY RUN: ${cmd} -p "${prompt.substring(0, 100)}..."`);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
lastSpawn.set(agent.name, now);
|
|
1280
|
+
try {
|
|
1281
|
+
const child = spawn(cmd, [...args, '-p', prompt], {
|
|
1282
|
+
cwd: config.projectDir,
|
|
1283
|
+
stdio: 'inherit',
|
|
1284
|
+
});
|
|
1285
|
+
child.on('exit', (code) => {
|
|
1286
|
+
console.log(`[${new Date().toISOString()}] ${agent.name} exited (code ${code})`);
|
|
1287
|
+
});
|
|
1288
|
+
child.on('error', (err) => {
|
|
1289
|
+
console.error(`[${new Date().toISOString()}] ${agent.name} error: ${err.message}`);
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
catch (err) {
|
|
1293
|
+
console.error(`Failed to spawn ${agent.name}:`, err);
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
// Handle inbox changes
|
|
1297
|
+
const handleInboxChange = (filePath) => {
|
|
1298
|
+
const agentName = path.basename(path.dirname(filePath));
|
|
1299
|
+
const agent = agents.find((a) => a.name === agentName);
|
|
1300
|
+
if (!agent)
|
|
1301
|
+
return;
|
|
1302
|
+
// Check if file grew (new messages)
|
|
1303
|
+
const currentSize = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0;
|
|
1304
|
+
const previousSize = lastInboxSize.get(agentName) || 0;
|
|
1305
|
+
lastInboxSize.set(agentName, currentSize);
|
|
1306
|
+
if (currentSize <= previousSize)
|
|
1307
|
+
return; // File shrunk or same
|
|
1308
|
+
console.log(`[${new Date().toISOString()}] New message for ${agentName}`);
|
|
1309
|
+
// Debounce
|
|
1310
|
+
const timer = debounceTimers.get(agentName);
|
|
1311
|
+
if (timer)
|
|
1312
|
+
clearTimeout(timer);
|
|
1313
|
+
debounceTimers.set(agentName, setTimeout(() => {
|
|
1314
|
+
debounceTimers.delete(agentName);
|
|
1315
|
+
spawnAgent(agent);
|
|
1316
|
+
}, debounceMs));
|
|
1317
|
+
};
|
|
1318
|
+
// Initialize inbox sizes
|
|
1319
|
+
for (const agent of agents) {
|
|
1320
|
+
const inboxPath = path.join(options.dataDir, agent.name, 'inbox.md');
|
|
1321
|
+
if (fs.existsSync(inboxPath)) {
|
|
1322
|
+
lastInboxSize.set(agent.name, fs.statSync(inboxPath).size);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
// Start watching
|
|
1326
|
+
const watcher = chokidar.watch(path.join(options.dataDir, '*/inbox.md'), {
|
|
1327
|
+
persistent: true,
|
|
1328
|
+
ignoreInitial: true,
|
|
1329
|
+
awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },
|
|
1330
|
+
});
|
|
1331
|
+
watcher.on('change', handleInboxChange);
|
|
1332
|
+
watcher.on('add', handleInboxChange);
|
|
1333
|
+
console.log('Listening... (Ctrl+C to stop)\n');
|
|
1334
|
+
process.on('SIGINT', () => {
|
|
1335
|
+
console.log('\nShutting down...');
|
|
1336
|
+
watcher.close();
|
|
1337
|
+
for (const timer of debounceTimers.values())
|
|
1338
|
+
clearTimeout(timer);
|
|
1339
|
+
process.exit(0);
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1342
|
+
// One command to start everything
|
|
1343
|
+
program
|
|
1344
|
+
.command('team-start')
|
|
1345
|
+
.description('Start a team - sets up, listens, and spawns all agents')
|
|
1346
|
+
.option('-f, --file <path>', 'Team config JSON file')
|
|
1347
|
+
.option('-d, --data-dir <path>', 'Team data directory', '/tmp/agent-relay-team')
|
|
1348
|
+
.option('--spawn', 'Immediately spawn all agents', false)
|
|
1349
|
+
.option('--dashboard', 'Start dashboard server', false)
|
|
1350
|
+
.option('--dashboard-port <port>', 'Dashboard port', '3888')
|
|
1351
|
+
.action(async (options) => {
|
|
1352
|
+
const chokidar = await import('chokidar');
|
|
1353
|
+
const { spawn, execSync } = await import('child_process');
|
|
1354
|
+
const { AgentStateManager } = await import('../state/agent-state.js');
|
|
1355
|
+
console.log('');
|
|
1356
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1357
|
+
console.log('ā AGENT RELAY - TEAM START ā');
|
|
1358
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1359
|
+
console.log('');
|
|
1360
|
+
// Step 1: Setup team if config provided
|
|
1361
|
+
const configPath = path.join(options.dataDir, 'team.json');
|
|
1362
|
+
if (options.file && fs.existsSync(options.file)) {
|
|
1363
|
+
console.log('š Setting up team from config...');
|
|
1364
|
+
const fileConfig = JSON.parse(fs.readFileSync(options.file, 'utf-8'));
|
|
1365
|
+
const projectDir = fileConfig.project || process.cwd();
|
|
1366
|
+
// Create team directory
|
|
1367
|
+
fs.mkdirSync(options.dataDir, { recursive: true });
|
|
1368
|
+
// Save team config
|
|
1369
|
+
const teamConfig = {
|
|
1370
|
+
name: fileConfig.name || 'team',
|
|
1371
|
+
projectDir,
|
|
1372
|
+
createdAt: new Date().toISOString(),
|
|
1373
|
+
agents: fileConfig.agents || [],
|
|
1374
|
+
};
|
|
1375
|
+
fs.writeFileSync(configPath, JSON.stringify(teamConfig, null, 2));
|
|
1376
|
+
// Create agent directories and instructions
|
|
1377
|
+
for (const agent of teamConfig.agents) {
|
|
1378
|
+
const agentDir = path.join(options.dataDir, agent.name);
|
|
1379
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
1380
|
+
fs.writeFileSync(path.join(agentDir, 'inbox.md'), '');
|
|
1381
|
+
const teammates = teamConfig.agents.filter((a) => a.name !== agent.name).map((a) => a.name);
|
|
1382
|
+
const taskList = (agent.tasks || []).map((t, i) => `${i + 1}. ${t}`).join('\n') || '(Check with teammates)';
|
|
1383
|
+
const instructions = `# You are ${agent.name} - ${agent.role}
|
|
1384
|
+
|
|
1385
|
+
## Project: \`${projectDir}\`
|
|
1386
|
+
|
|
1387
|
+
## Tasks
|
|
1388
|
+
${taskList}
|
|
1389
|
+
|
|
1390
|
+
## Teammates: ${teammates.join(', ') || 'none'}
|
|
1391
|
+
|
|
1392
|
+
## Commands
|
|
1393
|
+
\`\`\`bash
|
|
1394
|
+
# Check inbox
|
|
1395
|
+
node ${projectDir}/dist/cli/index.js team-check -n ${agent.name} -d ${options.dataDir} --no-wait
|
|
1396
|
+
|
|
1397
|
+
# Send message
|
|
1398
|
+
node ${projectDir}/dist/cli/index.js team-send -n ${agent.name} -t RECIPIENT -m "message" -d ${options.dataDir}
|
|
1399
|
+
|
|
1400
|
+
# Broadcast
|
|
1401
|
+
node ${projectDir}/dist/cli/index.js team-send -n ${agent.name} -t "*" -m "message" -d ${options.dataDir}
|
|
1402
|
+
\`\`\`
|
|
1403
|
+
|
|
1404
|
+
## Work Loop (MUST FOLLOW)
|
|
1405
|
+
1. CHECK inbox
|
|
1406
|
+
2. RESPOND to messages
|
|
1407
|
+
3. DO one task step
|
|
1408
|
+
4. BROADCAST status
|
|
1409
|
+
5. REPEAT
|
|
1410
|
+
|
|
1411
|
+
**Check inbox after every action!**
|
|
1412
|
+
`;
|
|
1413
|
+
fs.writeFileSync(path.join(agentDir, 'INSTRUCTIONS.md'), instructions);
|
|
1414
|
+
console.log(` ā ${agent.name} (${agent.cli})`);
|
|
1415
|
+
}
|
|
1416
|
+
console.log('');
|
|
1417
|
+
}
|
|
1418
|
+
// Load config
|
|
1419
|
+
if (!fs.existsSync(configPath)) {
|
|
1420
|
+
console.error('ā No team config found. Provide -f <config.json> or run team-setup first.');
|
|
1421
|
+
process.exit(1);
|
|
1422
|
+
}
|
|
1423
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1424
|
+
const stateManager = new AgentStateManager(options.dataDir);
|
|
1425
|
+
console.log(`š Team: ${config.name}`);
|
|
1426
|
+
console.log(`š Directory: ${options.dataDir}`);
|
|
1427
|
+
console.log(`š„ Agents: ${config.agents.map((a) => a.name).join(', ')}`);
|
|
1428
|
+
console.log('');
|
|
1429
|
+
// Step 2: Start dashboard if requested
|
|
1430
|
+
if (options.dashboard) {
|
|
1431
|
+
console.log(`š„ļø Starting dashboard on port ${options.dashboardPort}...`);
|
|
1432
|
+
const dashboardChild = spawn('node', [
|
|
1433
|
+
path.join(config.projectDir, 'dist/cli/index.js'),
|
|
1434
|
+
'dashboard',
|
|
1435
|
+
'-p', options.dashboardPort,
|
|
1436
|
+
'-d', options.dataDir,
|
|
1437
|
+
], {
|
|
1438
|
+
cwd: config.projectDir,
|
|
1439
|
+
stdio: 'ignore',
|
|
1440
|
+
detached: true,
|
|
1441
|
+
});
|
|
1442
|
+
dashboardChild.unref();
|
|
1443
|
+
console.log(` ā Dashboard: http://localhost:${options.dashboardPort}`);
|
|
1444
|
+
console.log('');
|
|
1445
|
+
}
|
|
1446
|
+
// Step 3: Spawn agents if requested
|
|
1447
|
+
if (options.spawn) {
|
|
1448
|
+
console.log('š Opening agent terminals...');
|
|
1449
|
+
const getCliCmd = (cli) => {
|
|
1450
|
+
switch (cli.toLowerCase()) {
|
|
1451
|
+
case 'claude': return 'claude --dangerously-skip-permissions';
|
|
1452
|
+
case 'codex': return 'codex';
|
|
1453
|
+
case 'gemini': return 'gemini';
|
|
1454
|
+
default: return cli;
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
for (const agent of config.agents) {
|
|
1458
|
+
const cliCmd = getCliCmd(agent.cli);
|
|
1459
|
+
const instructionsPath = path.join(options.dataDir, agent.name, 'INSTRUCTIONS.md');
|
|
1460
|
+
const prompt = `Read ${instructionsPath} and start working. Check inbox first, then begin your tasks.`;
|
|
1461
|
+
// Escape for shell
|
|
1462
|
+
const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/'/g, "'\\''");
|
|
1463
|
+
const fullCmd = `cd "${config.projectDir}" && ${cliCmd} -p "${escapedPrompt}"`;
|
|
1464
|
+
try {
|
|
1465
|
+
// Open in new Terminal.app window (macOS)
|
|
1466
|
+
const appleScript = `
|
|
1467
|
+
tell application "Terminal"
|
|
1468
|
+
activate
|
|
1469
|
+
do script "${fullCmd.replace(/"/g, '\\"')}"
|
|
1470
|
+
set custom title of front window to "${agent.name} (${agent.cli})"
|
|
1471
|
+
end tell
|
|
1472
|
+
`;
|
|
1473
|
+
execSync(`osascript -e '${appleScript.replace(/'/g, "'\\''")}'`, { stdio: 'ignore' });
|
|
1474
|
+
console.log(` ā ${agent.name} (${agent.cli}) - new terminal window`);
|
|
1475
|
+
// Small delay between opening windows
|
|
1476
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1477
|
+
}
|
|
1478
|
+
catch (_err) {
|
|
1479
|
+
// Fallback: try iTerm2
|
|
1480
|
+
try {
|
|
1481
|
+
const iTermScript = `
|
|
1482
|
+
tell application "iTerm"
|
|
1483
|
+
activate
|
|
1484
|
+
create window with default profile
|
|
1485
|
+
tell current session of current window
|
|
1486
|
+
write text "${fullCmd.replace(/"/g, '\\"')}"
|
|
1487
|
+
end tell
|
|
1488
|
+
end tell
|
|
1489
|
+
`;
|
|
1490
|
+
execSync(`osascript -e '${iTermScript.replace(/'/g, "'\\''")}'`, { stdio: 'ignore' });
|
|
1491
|
+
console.log(` ā ${agent.name} (${agent.cli}) - new iTerm window`);
|
|
1492
|
+
}
|
|
1493
|
+
catch {
|
|
1494
|
+
console.log(` ā ${agent.name} - couldn't open terminal (run manually)`);
|
|
1495
|
+
console.log(` ${fullCmd}`);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
console.log('');
|
|
1500
|
+
}
|
|
1501
|
+
// Step 4: Start listening for messages
|
|
1502
|
+
console.log('š Listening for messages...');
|
|
1503
|
+
console.log(' When agents receive messages, they will be notified.');
|
|
1504
|
+
console.log('');
|
|
1505
|
+
const lastSpawn = new Map();
|
|
1506
|
+
const debounceTimers = new Map();
|
|
1507
|
+
const lastInboxSize = new Map();
|
|
1508
|
+
const cooldownMs = 60000;
|
|
1509
|
+
const debounceMs = 3000;
|
|
1510
|
+
const getSpawnCmd = (cli) => {
|
|
1511
|
+
switch (cli.toLowerCase()) {
|
|
1512
|
+
case 'claude': return { cmd: 'claude', args: ['--dangerously-skip-permissions'] };
|
|
1513
|
+
case 'codex': return { cmd: 'codex', args: [] };
|
|
1514
|
+
case 'gemini': return { cmd: 'gemini', args: [] };
|
|
1515
|
+
default: return { cmd: cli, args: [] };
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1518
|
+
const spawnAgent = (agent) => {
|
|
1519
|
+
const now = Date.now();
|
|
1520
|
+
const last = lastSpawn.get(agent.name) || 0;
|
|
1521
|
+
if (now - last < cooldownMs)
|
|
1522
|
+
return;
|
|
1523
|
+
const inboxPath = path.join(options.dataDir, agent.name, 'inbox.md');
|
|
1524
|
+
if (!fs.existsSync(inboxPath))
|
|
1525
|
+
return;
|
|
1526
|
+
const content = fs.readFileSync(inboxPath, 'utf-8');
|
|
1527
|
+
if (!content.includes('## Message from'))
|
|
1528
|
+
return;
|
|
1529
|
+
const { cmd, args } = getSpawnCmd(agent.cli);
|
|
1530
|
+
const instructionsPath = path.join(options.dataDir, agent.name, 'INSTRUCTIONS.md');
|
|
1531
|
+
const stateContext = stateManager.formatAsContext(agent.name);
|
|
1532
|
+
const prompt = `${stateContext}
|
|
1533
|
+
|
|
1534
|
+
NEW MESSAGES! Read ${instructionsPath}, check inbox, respond, do one task, broadcast status.`;
|
|
1535
|
+
console.log(` ā Spawning ${agent.name}...`);
|
|
1536
|
+
lastSpawn.set(agent.name, now);
|
|
1537
|
+
try {
|
|
1538
|
+
const child = spawn(cmd, [...args, '-p', prompt], {
|
|
1539
|
+
cwd: config.projectDir,
|
|
1540
|
+
stdio: 'inherit',
|
|
1541
|
+
});
|
|
1542
|
+
child.on('exit', () => console.log(` ā ${agent.name} exited`));
|
|
1543
|
+
}
|
|
1544
|
+
catch (_err) {
|
|
1545
|
+
console.error(` ā Failed to spawn ${agent.name}`);
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
// Initialize sizes
|
|
1549
|
+
for (const agent of config.agents) {
|
|
1550
|
+
const inboxPath = path.join(options.dataDir, agent.name, 'inbox.md');
|
|
1551
|
+
if (fs.existsSync(inboxPath)) {
|
|
1552
|
+
lastInboxSize.set(agent.name, fs.statSync(inboxPath).size);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
// Watch inboxes
|
|
1556
|
+
const watcher = chokidar.watch(path.join(options.dataDir, '*/inbox.md'), {
|
|
1557
|
+
persistent: true,
|
|
1558
|
+
ignoreInitial: true,
|
|
1559
|
+
awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },
|
|
1560
|
+
});
|
|
1561
|
+
watcher.on('change', (filePath) => {
|
|
1562
|
+
const agentName = path.basename(path.dirname(filePath));
|
|
1563
|
+
const agent = config.agents.find((a) => a.name === agentName);
|
|
1564
|
+
if (!agent)
|
|
1565
|
+
return;
|
|
1566
|
+
const currentSize = fs.statSync(filePath).size;
|
|
1567
|
+
const previousSize = lastInboxSize.get(agentName) || 0;
|
|
1568
|
+
lastInboxSize.set(agentName, currentSize);
|
|
1569
|
+
if (currentSize <= previousSize)
|
|
1570
|
+
return;
|
|
1571
|
+
console.log(` šØ New message for ${agentName}`);
|
|
1572
|
+
const timer = debounceTimers.get(agentName);
|
|
1573
|
+
if (timer)
|
|
1574
|
+
clearTimeout(timer);
|
|
1575
|
+
debounceTimers.set(agentName, setTimeout(() => {
|
|
1576
|
+
debounceTimers.delete(agentName);
|
|
1577
|
+
spawnAgent(agent);
|
|
1578
|
+
}, debounceMs));
|
|
1579
|
+
});
|
|
1580
|
+
console.log('Ready! Send messages with:');
|
|
1581
|
+
console.log(` node dist/cli/index.js team-send -n You -t AgentName -m "message" -d ${options.dataDir}`);
|
|
1582
|
+
console.log('');
|
|
1583
|
+
console.log('Press Ctrl+C to stop.');
|
|
1584
|
+
process.on('SIGINT', () => {
|
|
1585
|
+
console.log('\nš Shutting down...');
|
|
1586
|
+
watcher.close();
|
|
1587
|
+
process.exit(0);
|
|
1588
|
+
});
|
|
1589
|
+
});
|
|
1590
|
+
program.parse();
|
|
1591
|
+
//# sourceMappingURL=index.js.map
|