agent-window 1.0.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/Dockerfile +23 -0
- package/README.md +138 -0
- package/SECURITY.md +31 -0
- package/bin/cli.js +743 -0
- package/config/config.example.json +70 -0
- package/docker-compose.yml +31 -0
- package/docs/legacy/DEVELOPMENT.md +174 -0
- package/docs/legacy/HANDOVER.md +149 -0
- package/ecosystem.config.cjs +26 -0
- package/hooks/hook.mjs +299 -0
- package/hooks/settings.json +15 -0
- package/package.json +45 -0
- package/sandbox/Dockerfile +61 -0
- package/scripts/install.sh +114 -0
- package/src/bot.js +1518 -0
- package/src/core/config.js +195 -0
- package/src/core/perf-monitor.js +360 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentBridge - Command Line Interface
|
|
5
|
+
*
|
|
6
|
+
* Bridge AI coding agents to chat platforms
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* agent-bridge setup - Interactive setup wizard
|
|
10
|
+
* agent-bridge start - Start the bridge
|
|
11
|
+
* agent-bridge stop - Stop the bridge
|
|
12
|
+
* agent-bridge restart - Restart the bridge
|
|
13
|
+
* agent-bridge status - Check status
|
|
14
|
+
* agent-bridge logs - View logs
|
|
15
|
+
* agent-bridge update - Update to latest version
|
|
16
|
+
* agent-bridge config - Show config location
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { spawn, execSync } from 'child_process';
|
|
20
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'fs';
|
|
21
|
+
import { dirname, join } from 'path';
|
|
22
|
+
import { fileURLToPath } from 'url';
|
|
23
|
+
import { createInterface } from 'readline';
|
|
24
|
+
|
|
25
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const PROJECT_ROOT = join(__dirname, '..');
|
|
27
|
+
const CONFIG_DIR = join(PROJECT_ROOT, 'config');
|
|
28
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
29
|
+
const CONFIG_EXAMPLE = join(CONFIG_DIR, 'config.example.json');
|
|
30
|
+
const PM2_NAME = 'agent-bridge';
|
|
31
|
+
|
|
32
|
+
// Colors for terminal output
|
|
33
|
+
const colors = {
|
|
34
|
+
reset: '\x1b[0m',
|
|
35
|
+
bright: '\x1b[1m',
|
|
36
|
+
dim: '\x1b[2m',
|
|
37
|
+
green: '\x1b[32m',
|
|
38
|
+
yellow: '\x1b[33m',
|
|
39
|
+
red: '\x1b[31m',
|
|
40
|
+
cyan: '\x1b[36m',
|
|
41
|
+
magenta: '\x1b[35m',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function log(msg, color = '') {
|
|
45
|
+
console.log(`${color}${msg}${colors.reset}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function logSuccess(msg) { log(`✅ ${msg}`, colors.green); }
|
|
49
|
+
function logWarning(msg) { log(`⚠️ ${msg}`, colors.yellow); }
|
|
50
|
+
function logError(msg) { log(`❌ ${msg}`, colors.red); }
|
|
51
|
+
function logInfo(msg) { log(`ℹ️ ${msg}`, colors.cyan); }
|
|
52
|
+
|
|
53
|
+
// Banner
|
|
54
|
+
function showBanner() {
|
|
55
|
+
log(`
|
|
56
|
+
_ _ ____ _ _
|
|
57
|
+
/ \\ __ _ ___ _ __ | |_| __ ) _ __(_) __| | __ _ ___
|
|
58
|
+
/ _ \\ / _\` |/ _ \\ '_ \\| __| _ \\| '__| |/ _\` |/ _\` |/ _ \\
|
|
59
|
+
/ ___ \\ (_| | __/ | | | |_| |_) | | | | (_| | (_| | __/
|
|
60
|
+
/_/ \\_\\__, |\\___|_| |_|\\__|____/|_| |_|\\__,_|\\__, |\\___|
|
|
61
|
+
|___/ |___/
|
|
62
|
+
`, colors.cyan);
|
|
63
|
+
log(' Bridge AI coding agents to chat platforms\n', colors.dim);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check if PM2 is installed
|
|
67
|
+
function checkPM2() {
|
|
68
|
+
try {
|
|
69
|
+
execSync('pm2 --version', { stdio: 'ignore' });
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check if Docker is installed
|
|
77
|
+
function checkDocker() {
|
|
78
|
+
try {
|
|
79
|
+
execSync('docker --version', { stdio: 'ignore' });
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Get existing container names to avoid conflicts
|
|
87
|
+
function getExistingContainerNames() {
|
|
88
|
+
try {
|
|
89
|
+
const output = execSync('docker ps -a --format "{{.Names}}"', { encoding: 'utf-8' });
|
|
90
|
+
return new Set(output.trim().split('\n').filter(Boolean));
|
|
91
|
+
} catch {
|
|
92
|
+
return new Set();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Generate a unique container name
|
|
97
|
+
function generateUniqueContainerName(existingNames) {
|
|
98
|
+
const baseName = 'claude-discord-bot';
|
|
99
|
+
if (!existingNames.has(baseName)) {
|
|
100
|
+
return baseName;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Find next available number
|
|
104
|
+
let i = 2;
|
|
105
|
+
while (existingNames.has(`${baseName}-${i}`)) {
|
|
106
|
+
i++;
|
|
107
|
+
}
|
|
108
|
+
return `${baseName}-${i}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Interactive prompt
|
|
112
|
+
function prompt(question, sensitive = false) {
|
|
113
|
+
const rl = createInterface({
|
|
114
|
+
input: process.stdin,
|
|
115
|
+
output: process.stdout,
|
|
116
|
+
});
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
rl.question(question, (answer) => {
|
|
119
|
+
rl.close();
|
|
120
|
+
resolve(answer.trim());
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Verify Discord Bot Token and check intents
|
|
126
|
+
async function verifyDiscordToken(token) {
|
|
127
|
+
log('\n🔍 Verifying Discord Bot configuration...', colors.cyan);
|
|
128
|
+
try {
|
|
129
|
+
const response = await fetch('https://discord.com/api/v10/users/@me', {
|
|
130
|
+
headers: { 'Authorization': `Bot ${token}` }
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
if (response.status === 401) {
|
|
135
|
+
logError('Invalid Discord Bot Token');
|
|
136
|
+
logInfo('Get your token from: https://discord.com/developers/applications');
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
logError(`Discord API error: ${response.status}`);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const bot = await response.json();
|
|
144
|
+
logSuccess(`Bot verified: ${bot.username} (${bot.id})`);
|
|
145
|
+
|
|
146
|
+
// Check gateway intents for the application
|
|
147
|
+
const appResponse = await fetch('https://discord.com/api/v10/applications/@me', {
|
|
148
|
+
headers: { 'Authorization': `Bot ${token}` }
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (appResponse.ok) {
|
|
152
|
+
const app = await appResponse.json();
|
|
153
|
+
const flags = app.flags || 0;
|
|
154
|
+
|
|
155
|
+
// Flag 1 << 16 is Message Content Intent
|
|
156
|
+
const hasMessageContent = (flags & (1 << 16)) !== 0;
|
|
157
|
+
|
|
158
|
+
if (!hasMessageContent) {
|
|
159
|
+
logError('Message Content Intent is NOT enabled!');
|
|
160
|
+
log('\n⚠️ REQUIRED ACTION:', colors.yellow);
|
|
161
|
+
log('1. Go to: https://discord.com/developers/applications');
|
|
162
|
+
log('2. Select your application');
|
|
163
|
+
log('3. Click "Bot" on the left sidebar');
|
|
164
|
+
log('4. Scroll to "Privileged Gateway Intents"');
|
|
165
|
+
log('5. Toggle ON "Message Content Intent"');
|
|
166
|
+
log('6. Save Changes\n');
|
|
167
|
+
log('After enabling, run setup again.', colors.dim);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
logSuccess('Message Content Intent: ✅ enabled');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return true;
|
|
174
|
+
} catch (e) {
|
|
175
|
+
logWarning(`Could not verify Discord config: ${e.message}`);
|
|
176
|
+
logInfo('Will verify on startup...');
|
|
177
|
+
return true; // Don't block setup on network errors
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Test Docker volume mount for the install directory
|
|
182
|
+
function testDockerMount(testPath) {
|
|
183
|
+
log('\n🔍 Testing Docker file sharing...', colors.cyan);
|
|
184
|
+
try {
|
|
185
|
+
const testContainer = 'agent-bridge-mount-test';
|
|
186
|
+
// Remove any old test container
|
|
187
|
+
execSync(`docker rm -f ${testContainer} 2>/dev/null`, { stdio: 'ignore' });
|
|
188
|
+
|
|
189
|
+
// Test mounting the directory
|
|
190
|
+
execSync(
|
|
191
|
+
`docker run --rm --name ${testContainer} -v ${testPath}:/test:ro alpine:latest ls /test >/dev/null 2>&1`,
|
|
192
|
+
{ stdio: 'ignore', timeout: 10000 }
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
logSuccess('Docker can access install directory');
|
|
196
|
+
return true;
|
|
197
|
+
} catch (e) {
|
|
198
|
+
logError(`Docker cannot mount: ${testPath}`);
|
|
199
|
+
log('\n⚠️ REQUIRED ACTION:', colors.yellow);
|
|
200
|
+
log('macOS Docker Desktop requires path sharing:');
|
|
201
|
+
log('1. Open Docker Desktop');
|
|
202
|
+
log('2. Go to: Settings → Resources → File Sharing');
|
|
203
|
+
log(`3. Add: ${testPath}`);
|
|
204
|
+
log('4. Click "Apply & Restart"');
|
|
205
|
+
log('\nAlternative: Install to user directory instead of global:');
|
|
206
|
+
log(' mkdir -p ~/agent-bridge && cd ~/agent-bridge && npm install cyrus-agent-bridge\n');
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Verify Claude OAuth Token
|
|
212
|
+
async function verifyClaudeToken(token) {
|
|
213
|
+
log('\n🔍 Verifying Claude Code OAuth Token...', colors.cyan);
|
|
214
|
+
try {
|
|
215
|
+
// Simple validation - token should start with sk-ant-
|
|
216
|
+
if (!token.startsWith('sk-ant-')) {
|
|
217
|
+
logWarning('Token format looks incorrect (should start with sk-ant-)');
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
logSuccess('Claude OAuth Token format valid');
|
|
221
|
+
return true;
|
|
222
|
+
} catch (e) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Setup wizard
|
|
228
|
+
async function setup() {
|
|
229
|
+
showBanner();
|
|
230
|
+
log('🚀 Setup Wizard\n', colors.bright);
|
|
231
|
+
|
|
232
|
+
// Check prerequisites
|
|
233
|
+
log('Checking prerequisites...', colors.cyan);
|
|
234
|
+
|
|
235
|
+
if (!checkPM2()) {
|
|
236
|
+
logWarning('PM2 not found. Installing globally...');
|
|
237
|
+
try {
|
|
238
|
+
execSync('npm install -g pm2', { stdio: 'inherit' });
|
|
239
|
+
logSuccess('PM2 installed');
|
|
240
|
+
} catch {
|
|
241
|
+
logError('Failed to install PM2. Please install manually: npm install -g pm2');
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
logSuccess('PM2 found');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!checkDocker()) {
|
|
249
|
+
logError('Docker not found. Please install Docker first.');
|
|
250
|
+
logInfo('Visit: https://docs.docker.com/get-docker/');
|
|
251
|
+
process.exit(1);
|
|
252
|
+
} else {
|
|
253
|
+
logSuccess('Docker found');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Check for container name conflicts
|
|
257
|
+
const existingNames = getExistingContainerNames();
|
|
258
|
+
const containerName = generateUniqueContainerName(existingNames);
|
|
259
|
+
if (existingNames.has('claude-discord-bot') || existingNames.has('agentbridge-bot')) {
|
|
260
|
+
logWarning(`Existing containers detected. Using unique name: ${containerName}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Load or create config
|
|
264
|
+
let config;
|
|
265
|
+
let configExisted = existsSync(CONFIG_FILE);
|
|
266
|
+
|
|
267
|
+
if (configExisted) {
|
|
268
|
+
log('\n📋 Existing configuration found, validating...', colors.cyan);
|
|
269
|
+
config = JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
|
|
270
|
+
logSuccess('Configuration loaded');
|
|
271
|
+
} else {
|
|
272
|
+
log('\n📝 Required Configuration (4 items)\n', colors.cyan);
|
|
273
|
+
|
|
274
|
+
log('1/4 Discord Bot Token', colors.dim);
|
|
275
|
+
const botToken = await prompt(' > ');
|
|
276
|
+
|
|
277
|
+
log('2/4 Claude OAuth Token', colors.dim);
|
|
278
|
+
const oauthToken = await prompt(' > ');
|
|
279
|
+
|
|
280
|
+
log('3/4 Project directory (absolute path)', colors.dim);
|
|
281
|
+
const projectDir = await prompt(' > ');
|
|
282
|
+
|
|
283
|
+
log('4/4 Discord channel ID(s)', colors.dim);
|
|
284
|
+
const allowedChannels = await prompt(' > ');
|
|
285
|
+
|
|
286
|
+
// Load example config and modify
|
|
287
|
+
config = JSON.parse(readFileSync(CONFIG_EXAMPLE, 'utf-8'));
|
|
288
|
+
config.BOT_TOKEN = botToken;
|
|
289
|
+
config.CLAUDE_CODE_OAUTH_TOKEN = oauthToken;
|
|
290
|
+
config.PROJECT_DIR = projectDir;
|
|
291
|
+
config.ALLOWED_CHANNELS = allowedChannels;
|
|
292
|
+
config.workspace.containerName = containerName; // Use unique container name
|
|
293
|
+
|
|
294
|
+
// Remove comments
|
|
295
|
+
Object.keys(config).forEach(key => {
|
|
296
|
+
if (key.startsWith('_comment')) delete config[key];
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
300
|
+
logSuccess(`Configuration saved (container: ${containerName})`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Verify Discord configuration
|
|
304
|
+
if (config.BOT_TOKEN) {
|
|
305
|
+
const discordValid = await verifyDiscordToken(config.BOT_TOKEN);
|
|
306
|
+
if (!discordValid) {
|
|
307
|
+
logError('\n❌ Setup failed: Discord configuration invalid');
|
|
308
|
+
logInfo('Fix the issues above and run: agent-bridge setup');
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Verify Claude token format
|
|
314
|
+
if (config.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
315
|
+
await verifyClaudeToken(config.CLAUDE_CODE_OAUTH_TOKEN);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Test Docker mount for the install directory
|
|
319
|
+
const mountOk = testDockerMount(PROJECT_ROOT);
|
|
320
|
+
if (!mountOk) {
|
|
321
|
+
logError('\n❌ Setup failed: Docker file sharing not configured');
|
|
322
|
+
logInfo('Fix the issues above and run: agent-bridge setup');
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Build Docker image
|
|
327
|
+
log('\n🐳 Building Docker sandbox...\n', colors.cyan);
|
|
328
|
+
try {
|
|
329
|
+
execSync('docker build -t claude-sandbox ./sandbox', {
|
|
330
|
+
cwd: PROJECT_ROOT,
|
|
331
|
+
stdio: 'inherit'
|
|
332
|
+
});
|
|
333
|
+
logSuccess('Docker image built');
|
|
334
|
+
} catch {
|
|
335
|
+
logError('Failed to build Docker image');
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Create PM2 ecosystem file
|
|
340
|
+
const ecosystemFile = join(PROJECT_ROOT, 'ecosystem.config.cjs');
|
|
341
|
+
if (!existsSync(ecosystemFile)) {
|
|
342
|
+
const ecosystemConfig = `module.exports = {
|
|
343
|
+
apps: [{
|
|
344
|
+
name: '${PM2_NAME}',
|
|
345
|
+
script: 'src/bot.js',
|
|
346
|
+
cwd: '${PROJECT_ROOT}',
|
|
347
|
+
watch: false,
|
|
348
|
+
autorestart: true,
|
|
349
|
+
max_restarts: 10,
|
|
350
|
+
env: {
|
|
351
|
+
NODE_ENV: 'production'
|
|
352
|
+
}
|
|
353
|
+
}]
|
|
354
|
+
};
|
|
355
|
+
`;
|
|
356
|
+
writeFileSync(ecosystemFile, ecosystemConfig);
|
|
357
|
+
logSuccess('PM2 config created');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
log('\n✨ Setup complete!\n', colors.green + colors.bright);
|
|
361
|
+
log('Start the bridge with:', colors.cyan);
|
|
362
|
+
log(' agent-bridge start\n');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Start
|
|
366
|
+
function start() {
|
|
367
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
368
|
+
logError('Configuration not found. Run "agent-bridge setup" first.');
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
log('Starting AgentBridge...', colors.cyan);
|
|
373
|
+
try {
|
|
374
|
+
execSync(`pm2 start ecosystem.config.cjs`, {
|
|
375
|
+
cwd: PROJECT_ROOT,
|
|
376
|
+
stdio: 'inherit'
|
|
377
|
+
});
|
|
378
|
+
logSuccess('AgentBridge started');
|
|
379
|
+
log('\nView logs: agent-bridge logs', colors.dim);
|
|
380
|
+
} catch (e) {
|
|
381
|
+
logError('Failed to start: ' + e.message);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Stop
|
|
386
|
+
function stop() {
|
|
387
|
+
log('Stopping AgentBridge...', colors.cyan);
|
|
388
|
+
try {
|
|
389
|
+
execSync(`pm2 stop ${PM2_NAME}`, { stdio: 'inherit' });
|
|
390
|
+
logSuccess('AgentBridge stopped');
|
|
391
|
+
} catch {
|
|
392
|
+
logWarning('AgentBridge may not be running');
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Restart
|
|
397
|
+
function restart() {
|
|
398
|
+
log('Restarting AgentBridge...', colors.cyan);
|
|
399
|
+
try {
|
|
400
|
+
execSync(`pm2 restart ${PM2_NAME}`, { stdio: 'inherit' });
|
|
401
|
+
logSuccess('AgentBridge restarted');
|
|
402
|
+
} catch {
|
|
403
|
+
logWarning('AgentBridge may not be running. Starting...');
|
|
404
|
+
start();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Status
|
|
409
|
+
function status() {
|
|
410
|
+
try {
|
|
411
|
+
execSync(`pm2 describe ${PM2_NAME}`, { stdio: 'inherit' });
|
|
412
|
+
} catch {
|
|
413
|
+
logWarning('AgentBridge is not running');
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Logs
|
|
418
|
+
function logs() {
|
|
419
|
+
try {
|
|
420
|
+
const child = spawn('pm2', ['logs', PM2_NAME, '--lines', '50'], {
|
|
421
|
+
stdio: 'inherit'
|
|
422
|
+
});
|
|
423
|
+
child.on('error', () => {
|
|
424
|
+
logError('Failed to show logs');
|
|
425
|
+
});
|
|
426
|
+
} catch {
|
|
427
|
+
logError('Failed to show logs');
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Update
|
|
432
|
+
async function update() {
|
|
433
|
+
log('Checking for updates...', colors.cyan);
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
const isGit = existsSync(join(PROJECT_ROOT, '.git'));
|
|
437
|
+
|
|
438
|
+
if (isGit) {
|
|
439
|
+
log('Fetching updates...', colors.cyan);
|
|
440
|
+
execSync('git fetch origin', { cwd: PROJECT_ROOT, stdio: 'inherit' });
|
|
441
|
+
|
|
442
|
+
const localHash = execSync('git rev-parse HEAD', { cwd: PROJECT_ROOT, encoding: 'utf-8' }).trim();
|
|
443
|
+
const remoteHash = execSync('git rev-parse origin/main', { cwd: PROJECT_ROOT, encoding: 'utf-8' }).trim();
|
|
444
|
+
|
|
445
|
+
if (localHash === remoteHash) {
|
|
446
|
+
logSuccess('Already up to date');
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
log('Stopping for update...', colors.yellow);
|
|
451
|
+
try { execSync(`pm2 stop ${PM2_NAME}`, { stdio: 'ignore' }); } catch {}
|
|
452
|
+
|
|
453
|
+
execSync('git pull origin main', { cwd: PROJECT_ROOT, stdio: 'inherit' });
|
|
454
|
+
|
|
455
|
+
log('Installing dependencies...', colors.cyan);
|
|
456
|
+
execSync('npm install', { cwd: PROJECT_ROOT, stdio: 'inherit' });
|
|
457
|
+
|
|
458
|
+
log('Rebuilding Docker image...', colors.cyan);
|
|
459
|
+
execSync('docker build -t claude-sandbox ./sandbox', { cwd: PROJECT_ROOT, stdio: 'inherit' });
|
|
460
|
+
|
|
461
|
+
log('Restarting...', colors.cyan);
|
|
462
|
+
execSync(`pm2 restart ${PM2_NAME}`, { cwd: PROJECT_ROOT, stdio: 'inherit' });
|
|
463
|
+
|
|
464
|
+
logSuccess('Update complete');
|
|
465
|
+
} else {
|
|
466
|
+
log('Updating via npm...', colors.cyan);
|
|
467
|
+
execSync('npm update -g agent-bridge', { stdio: 'inherit' });
|
|
468
|
+
logSuccess('Update complete. Please restart.');
|
|
469
|
+
}
|
|
470
|
+
} catch (e) {
|
|
471
|
+
logError('Update failed: ' + e.message);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Config
|
|
476
|
+
function showConfig() {
|
|
477
|
+
log('\nConfiguration:', colors.cyan);
|
|
478
|
+
log(` Config: ${CONFIG_FILE}`);
|
|
479
|
+
log(` Example: ${CONFIG_EXAMPLE}`);
|
|
480
|
+
|
|
481
|
+
if (existsSync(CONFIG_FILE)) {
|
|
482
|
+
logSuccess('Config exists');
|
|
483
|
+
} else {
|
|
484
|
+
logWarning('Config not found. Run "agent-bridge setup"');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Init - Initialize multi-bot management directory
|
|
489
|
+
async function initBotsDir() {
|
|
490
|
+
showBanner();
|
|
491
|
+
log('🚀 Initialize Multi-Bot Management\n', colors.bright);
|
|
492
|
+
|
|
493
|
+
const defaultBotsDir = join(process.env.HOME || process.env.USERPROFILE, 'bots');
|
|
494
|
+
log(`Default bots directory: ${defaultBotsDir}`, colors.dim);
|
|
495
|
+
|
|
496
|
+
const botsDir = await prompt('Bots directory (press Enter for default): ') || defaultBotsDir;
|
|
497
|
+
|
|
498
|
+
// Create directory structure
|
|
499
|
+
const botTypesDir = join(botsDir, 'agent-bridge@latest');
|
|
500
|
+
try {
|
|
501
|
+
execSync(`mkdir -p "${botTypesDir}"`, { stdio: 'inherit' });
|
|
502
|
+
} catch {
|
|
503
|
+
// Directory already exists or was created
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Create symlink to global agent-bridge
|
|
507
|
+
const globalAgentBridge = '/opt/homebrew/lib/node_modules/agent-bridge';
|
|
508
|
+
if (existsSync(globalAgentBridge)) {
|
|
509
|
+
const linkPath = join(botsDir, 'agent-bridge');
|
|
510
|
+
try {
|
|
511
|
+
execSync(`ln -sf "${globalAgentBridge}" "${linkPath}"`, { stdio: 'inherit' });
|
|
512
|
+
logSuccess('Linked to global agent-bridge installation');
|
|
513
|
+
} catch {
|
|
514
|
+
logWarning('Could not create symlink (may already exist)');
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Create ecosystem template
|
|
519
|
+
const ecosystemFile = join(botsDir, 'ecosystem.config.cjs');
|
|
520
|
+
if (!existsSync(ecosystemFile)) {
|
|
521
|
+
const ecosystemTemplate = `/**
|
|
522
|
+
* AgentBridge - Multi-Bot Management
|
|
523
|
+
*
|
|
524
|
+
* Add new bots with: agent-bridge add-bot <name>
|
|
525
|
+
*/
|
|
526
|
+
|
|
527
|
+
const AGENT_BRIDGE = '${globalAgentBridge}';
|
|
528
|
+
const BOTS_DIR = '${botsDir}';
|
|
529
|
+
|
|
530
|
+
module.exports = {
|
|
531
|
+
apps: [
|
|
532
|
+
// Add your bot instances here
|
|
533
|
+
// {
|
|
534
|
+
// name: 'bot-myproject',
|
|
535
|
+
// script: AGENT_BRIDGE + '/src/bot.js',
|
|
536
|
+
// cwd: AGENT_BRIDGE,
|
|
537
|
+
// env: {
|
|
538
|
+
// CONFIG_PATH: BOTS_DIR + '/myproject/config.json'
|
|
539
|
+
// },
|
|
540
|
+
// watch: false,
|
|
541
|
+
// autorestart: true,
|
|
542
|
+
// max_restarts: 10,
|
|
543
|
+
// }
|
|
544
|
+
]
|
|
545
|
+
};
|
|
546
|
+
`;
|
|
547
|
+
writeFileSync(ecosystemFile, ecosystemTemplate);
|
|
548
|
+
logSuccess(`Created: ${ecosystemFile}`);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
log('\n✨ Multi-bot directory initialized!\n', colors.green + colors.bright);
|
|
552
|
+
log('Next steps:', colors.cyan);
|
|
553
|
+
log(` cd ${botsDir}`);
|
|
554
|
+
log(' agent-bridge add-bot <name> # Add a new bot');
|
|
555
|
+
log(' pm2 start ecosystem.config.cjs # Start all bots\n');
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Add Bot - Add a new bot instance
|
|
559
|
+
async function addBot() {
|
|
560
|
+
const botName = process.argv[3];
|
|
561
|
+
if (!botName) {
|
|
562
|
+
logError('Usage: agent-bridge add-bot <name>');
|
|
563
|
+
logInfo('Example: agent-bridge add-bot myproject');
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Find bots directory
|
|
568
|
+
const defaultBotsDir = join(process.env.HOME || process.env.USERPROFILE, 'bots');
|
|
569
|
+
const botsDir = existsSync(defaultBotsDir) ? defaultBotsDir : null;
|
|
570
|
+
|
|
571
|
+
if (!botsDir || !existsSync(join(botsDir, 'ecosystem.config.cjs'))) {
|
|
572
|
+
logError('Bots directory not initialized.');
|
|
573
|
+
logInfo('Run: agent-bridge init');
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
log(`🤖 Adding bot: ${botName}\n`, colors.cyan);
|
|
578
|
+
|
|
579
|
+
const botDir = join(botsDir, botName);
|
|
580
|
+
execSync(`mkdir -p "${botDir}"`, { stdio: 'inherit' });
|
|
581
|
+
|
|
582
|
+
// Check if config already exists
|
|
583
|
+
const configFile = join(botDir, 'config.json');
|
|
584
|
+
if (existsSync(configFile)) {
|
|
585
|
+
logWarning(`Bot already exists: ${botName}`);
|
|
586
|
+
logInfo(`To reconfigure, edit: ${configFile}`);
|
|
587
|
+
process.exit(0);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Prompt for configuration
|
|
591
|
+
log('\n📝 Bot Configuration\n', colors.cyan);
|
|
592
|
+
|
|
593
|
+
log('Discord Bot Token', colors.dim);
|
|
594
|
+
const botToken = await prompt(' > ');
|
|
595
|
+
|
|
596
|
+
log('Claude OAuth Token', colors.dim);
|
|
597
|
+
const oauthToken = await prompt(' > ');
|
|
598
|
+
|
|
599
|
+
log('Project directory (absolute path)', colors.dim);
|
|
600
|
+
const projectDir = await prompt(' > ');
|
|
601
|
+
|
|
602
|
+
log('Discord channel ID(s)', colors.dim);
|
|
603
|
+
const allowedChannels = await prompt(' > ');
|
|
604
|
+
|
|
605
|
+
log('Container name', colors.dim);
|
|
606
|
+
const containerName = await prompt(` > `, `bot-${botName}`);
|
|
607
|
+
|
|
608
|
+
// Create config
|
|
609
|
+
const config = {
|
|
610
|
+
BOT_TOKEN: botToken,
|
|
611
|
+
CLAUDE_CODE_OAUTH_TOKEN: oauthToken,
|
|
612
|
+
PROJECT_DIR: projectDir,
|
|
613
|
+
ALLOWED_CHANNELS: allowedChannels,
|
|
614
|
+
workspace: {
|
|
615
|
+
containerName: containerName
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
writeFileSync(configFile, JSON.stringify(config, null, 2));
|
|
620
|
+
logSuccess(`Configuration saved: ${configFile}`);
|
|
621
|
+
|
|
622
|
+
// Update ecosystem file
|
|
623
|
+
const ecosystemFile = join(botsDir, 'ecosystem.config.cjs');
|
|
624
|
+
const ecosystemContent = readFileSync(ecosystemFile, 'utf-8');
|
|
625
|
+
|
|
626
|
+
// Check if bot already in ecosystem
|
|
627
|
+
if (ecosystemContent.includes(`name: 'bot-${botName}'`) || ecosystemContent.includes(`name: "bot-${botName}"`)) {
|
|
628
|
+
logWarning('Bot already in ecosystem.config.cjs');
|
|
629
|
+
} else {
|
|
630
|
+
// Add bot to ecosystem
|
|
631
|
+
const newApp = `,
|
|
632
|
+
{
|
|
633
|
+
name: 'bot-` + botName + `',
|
|
634
|
+
script: process.env.GLOBAL_AGENT_BRIDGE || '/opt/homebrew/lib/node_modules/agent-bridge/src/bot.js',
|
|
635
|
+
cwd: process.env.GLOBAL_AGENT_BRIDGE ? process.env.GLOBAL_AGENT_BRIDGE : '/opt/homebrew/lib/node_modules/agent-bridge',
|
|
636
|
+
env: {
|
|
637
|
+
CONFIG_PATH: '` + botsDir + '/' + botName + `/config.json'
|
|
638
|
+
},
|
|
639
|
+
watch: false,
|
|
640
|
+
autorestart: true,
|
|
641
|
+
max_restarts: 10,
|
|
642
|
+
}`;
|
|
643
|
+
|
|
644
|
+
const updated = ecosystemContent.replace(
|
|
645
|
+
/apps: \[([^\]]*)\]/,
|
|
646
|
+
(match, content) => {
|
|
647
|
+
if (content.trim() === '' || content.includes('// Add your bot')) {
|
|
648
|
+
return `apps: [${newApp}\n ]`;
|
|
649
|
+
}
|
|
650
|
+
return `apps: [${content}${newApp}\n ]`;
|
|
651
|
+
}
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
writeFileSync(ecosystemFile, updated);
|
|
655
|
+
logSuccess(`Added to ecosystem.config.cjs`);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
log('\n✨ Bot added!\n', colors.green + colors.bright);
|
|
659
|
+
log('To start all bots:', colors.cyan);
|
|
660
|
+
log(' pm2 start ' + ecosystemFile);
|
|
661
|
+
log('\nTo start just this bot:');
|
|
662
|
+
log(' pm2 start ' + ecosystemFile + ' --only bot-' + botName);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Help
|
|
666
|
+
function showHelp() {
|
|
667
|
+
showBanner();
|
|
668
|
+
log(`${colors.cyan}Usage:${colors.reset}
|
|
669
|
+
agent-bridge <command>
|
|
670
|
+
|
|
671
|
+
${colors.cyan}Single-Bot Commands:${colors.reset}
|
|
672
|
+
setup Interactive setup wizard
|
|
673
|
+
start Start the bridge
|
|
674
|
+
stop Stop the bridge
|
|
675
|
+
restart Restart the bridge
|
|
676
|
+
status Show status
|
|
677
|
+
logs View logs (live)
|
|
678
|
+
update Update to latest version
|
|
679
|
+
config Show config location
|
|
680
|
+
|
|
681
|
+
${colors.cyan}Multi-Bot Commands:${colors.reset}
|
|
682
|
+
init Initialize multi-bot management directory
|
|
683
|
+
add-bot Add a new bot instance
|
|
684
|
+
|
|
685
|
+
${colors.cyan}General:${colors.reset}
|
|
686
|
+
help Show this help
|
|
687
|
+
|
|
688
|
+
${colors.cyan}Examples:${colors.reset}
|
|
689
|
+
agent-bridge setup ${colors.dim}# First time setup${colors.reset}
|
|
690
|
+
agent-bridge init ${colors.dim}# Initialize ~/bots directory${colors.reset}
|
|
691
|
+
agent-bridge add-bot myproject ${colors.dim}# Add a new bot${colors.reset}
|
|
692
|
+
pm2 start ~/bots/ecosystem.config.cjs ${colors.dim}# Start all bots${colors.reset}
|
|
693
|
+
|
|
694
|
+
${colors.cyan}Documentation:${colors.reset}
|
|
695
|
+
https://github.com/YOUR_USERNAME/AgentBridge
|
|
696
|
+
`);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Main
|
|
700
|
+
const command = process.argv[2];
|
|
701
|
+
|
|
702
|
+
switch (command) {
|
|
703
|
+
case 'setup':
|
|
704
|
+
setup();
|
|
705
|
+
break;
|
|
706
|
+
case 'start':
|
|
707
|
+
start();
|
|
708
|
+
break;
|
|
709
|
+
case 'stop':
|
|
710
|
+
stop();
|
|
711
|
+
break;
|
|
712
|
+
case 'restart':
|
|
713
|
+
restart();
|
|
714
|
+
break;
|
|
715
|
+
case 'status':
|
|
716
|
+
status();
|
|
717
|
+
break;
|
|
718
|
+
case 'logs':
|
|
719
|
+
logs();
|
|
720
|
+
break;
|
|
721
|
+
case 'update':
|
|
722
|
+
update();
|
|
723
|
+
break;
|
|
724
|
+
case 'config':
|
|
725
|
+
showConfig();
|
|
726
|
+
break;
|
|
727
|
+
case 'init':
|
|
728
|
+
initBotsDir();
|
|
729
|
+
break;
|
|
730
|
+
case 'add-bot':
|
|
731
|
+
addBot();
|
|
732
|
+
break;
|
|
733
|
+
case 'help':
|
|
734
|
+
case '--help':
|
|
735
|
+
case '-h':
|
|
736
|
+
case undefined:
|
|
737
|
+
showHelp();
|
|
738
|
+
break;
|
|
739
|
+
default:
|
|
740
|
+
logError(`Unknown command: ${command}`);
|
|
741
|
+
showHelp();
|
|
742
|
+
process.exit(1);
|
|
743
|
+
}
|