agent-window 1.4.0 → 1.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-window",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "A window to interact with AI agents through chat interfaces. Simplified interaction, powerful backend capabilities.",
5
5
  "type": "module",
6
6
  "main": "src/bot.js",
@@ -11,6 +11,8 @@
11
11
  "ui": "node bin/cli.js ui",
12
12
  "ui:dev": "cd web && npm run dev",
13
13
  "ui:build": "cd web && npm run build",
14
+ "cleanup-pm2": "bash scripts/cleanup-pm2.sh",
15
+ "check-node": "node -e \"const v=process.version; const n=parseInt(v.slice(1)); if(n>=23){console.error('⚠️ Node.js',v,'not supported. Use v18 or v20 LTS.'); process.exit(1);}else{console.log('✓ Node.js',v,'is supported');}\"",
14
16
  "pm2:start": "pm2 start ecosystem.config.cjs",
15
17
  "pm2:stop": "pm2 stop bot-corp",
16
18
  "pm2:restart": "pm2 restart bot-corp",
@@ -26,7 +28,8 @@
26
28
  "author": "",
27
29
  "license": "MIT",
28
30
  "engines": {
29
- "node": ">=18.0.0"
31
+ "node": ">=18.0.0 <23.0.0",
32
+ "npm": ">=9.0.0"
30
33
  },
31
34
  "dependencies": {
32
35
  "@fastify/static": "^6.0.0",
@@ -0,0 +1,130 @@
1
+ #!/bin/bash
2
+ # PM2 Deep Cleanup Script
3
+ # AgentWindow Multi-Instance Bug Recovery Tool
4
+ #
5
+ # This script cleans up:
6
+ # - Multiple PM2 God Daemon processes (200+ leaked instances)
7
+ # - Orphaned bot processes
8
+ # - Stale PM2 dump files
9
+ # - Duplicate instances
10
+
11
+ set -e
12
+
13
+ echo "🧹 PM2 Deep Cleanup Script"
14
+ echo "======================================"
15
+ echo ""
16
+
17
+ # Color codes for output
18
+ RED='\033[0;31m'
19
+ GREEN='\033[0;32m'
20
+ YELLOW='\033[1;33m'
21
+ NC='\033[0m' # No Color
22
+
23
+ # Step 1: Check PM2 processes before cleanup
24
+ echo "[1/7] Analyzing current state..."
25
+ echo "-----------------------------------"
26
+
27
+ PM2_COUNT=$(pgrep -f "PM2.*God Daemon" | wc -l | tr -d ' ' || echo "0")
28
+ BOT_COUNT=$(ps aux | grep "node.*bot.js" | grep -v grep | wc -l | tr -d ' ' || echo "0")
29
+
30
+ echo -e "PM2 God Daemons: ${YELLOW}${PM2_COUNT}${NC}"
31
+ echo -e "Bot processes: ${YELLOW}${BOT_COUNT}${NC}"
32
+ echo ""
33
+
34
+ if [ "$PM2_COUNT" -lt 5 ] && [ "$BOT_COUNT" -lt 3 ]; then
35
+ echo -e "${GREEN}✓ System appears clean (no major issues detected)${NC}"
36
+ echo ""
37
+ read -p "Continue cleanup anyway? (y/N): " -n 1 -r
38
+ echo
39
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
40
+ echo "Cleanup cancelled."
41
+ exit 0
42
+ fi
43
+ fi
44
+
45
+ # Step 2: Stop all PM2 apps
46
+ echo "[2/7] Stopping all PM2 apps..."
47
+ echo "-----------------------------------"
48
+ if command -v pm2 &> /dev/null; then
49
+ pm2 stop all 2>/dev/null || echo " No apps to stop"
50
+ pm2 delete all 2>/dev/null || echo " No apps to delete"
51
+ echo -e "${GREEN}✓ PM2 apps stopped${NC}"
52
+ else
53
+ echo -e "${YELLOW}⚠ PM2 not found, skipping...${NC}"
54
+ fi
55
+ echo ""
56
+
57
+ # Step 3: Kill PM2 daemon
58
+ echo "[3/7] Killing PM2 daemon..."
59
+ echo "-----------------------------------"
60
+ if command -v pm2 &> /dev/null; then
61
+ pm2 kill 2>/dev/null || echo " Daemon already stopped"
62
+ echo -e "${GREEN}✓ PM2 daemon killed${NC}"
63
+ else
64
+ echo -e "${YELLOW}⚠ PM2 not found, skipping...${NC}"
65
+ fi
66
+ echo ""
67
+
68
+ # Step 4: Kill remaining PM2 God processes
69
+ echo "[4/7] Cleaning up PM2 God processes..."
70
+ echo "-----------------------------------"
71
+ PM2_PIDS=$(pgrep -f "PM2.*God Daemon" || true)
72
+ if [ -n "$PM2_PIDS" ]; then
73
+ echo "$PM2_PIDS" | xargs -I {} kill -9 {} 2>/dev/null || true
74
+ echo -e "${GREEN}✓ Killed PM2 God processes${NC}"
75
+ else
76
+ echo -e "${GREEN}✓ No PM2 God processes found${NC}"
77
+ fi
78
+ echo ""
79
+
80
+ # Step 5: Remove dump file
81
+ echo "[5/7] Removing PM2 dump file..."
82
+ echo "-----------------------------------"
83
+ DUMP_FILE="$HOME/.pm2/dump.pm2"
84
+ if [ -f "$DUMP_FILE" ]; then
85
+ rm -f "$DUMP_FILE"
86
+ echo -e "${GREEN}✓ Removed dump file${NC}"
87
+ else
88
+ echo -e "${GREEN}✓ No dump file found${NC}"
89
+ fi
90
+ echo ""
91
+
92
+ # Step 6: Kill orphaned bot processes
93
+ echo "[6/7] Cleaning up orphaned bot processes..."
94
+ echo "-----------------------------------"
95
+ BOT_PIDS=$(ps aux | grep "node.*bot.js" | grep -v grep | awk '{print $2}' || true)
96
+ if [ -n "$BOT_PIDS" ]; then
97
+ echo "$BOT_PIDS" | xargs -I {} kill -9 {} 2>/dev/null || true
98
+ echo -e "${GREEN}✓ Killed orphaned bot processes${NC}"
99
+ else
100
+ echo -e "${GREEN}✓ No orphaned bot processes found${NC}"
101
+ fi
102
+ echo ""
103
+
104
+ # Step 7: Verification
105
+ echo "[7/7] Verification..."
106
+ echo "-----------------------------------"
107
+ sleep 1
108
+
109
+ FINAL_PM2_COUNT=$(pgrep -f "PM2.*God Daemon" | wc -l | tr -d ' ' || echo "0")
110
+ FINAL_BOT_COUNT=$(ps aux | grep "node.*bot.js" | grep -v grep | wc -l | tr -d ' ' || echo "0")
111
+
112
+ echo -e "PM2 God Daemons: ${GREEN}${FINAL_PM2_COUNT}${NC} (was ${PM2_COUNT})"
113
+ echo -e "Bot processes: ${GREEN}${FINAL_BOT_COUNT}${NC} (was ${BOT_COUNT})"
114
+ echo ""
115
+
116
+ if [ "$FINAL_PM2_COUNT" -eq 0 ] && [ "$FINAL_BOT_COUNT" -eq 0 ]; then
117
+ echo -e "${GREEN}✅ Cleanup complete! System is clean.${NC}"
118
+ echo ""
119
+ echo "Next steps:"
120
+ echo " 1. Start your bot: pm2 start ecosystem.config.cjs"
121
+ echo " 2. Verify: pm2 list"
122
+ echo " 3. Check logs: pm2 logs --lines 20"
123
+ else
124
+ echo -e "${YELLOW}⚠️ Some processes still running. You may need to:${NC}"
125
+ echo " - Check manually: ps aux | grep bot.js"
126
+ echo " - Kill manually: kill -9 <PID>"
127
+ echo " - Restart terminal/daemon"
128
+ fi
129
+ echo ""
130
+ echo "======================================"
package/src/bot.js CHANGED
@@ -24,8 +24,6 @@ import { spawn, execSync } from 'child_process';
24
24
  import { readFileSync, writeFileSync, existsSync, readdirSync, unlinkSync, mkdirSync, chmodSync } from 'fs';
25
25
  import { join } from 'path';
26
26
  import { homedir } from 'os';
27
- import { https } from 'https';
28
- import { http } from 'http';
29
27
  import { createWriteStream } from 'fs';
30
28
 
31
29
  // Import performance monitoring utilities (local)
@@ -41,6 +39,33 @@ import {
41
39
  // Import centralized configuration (local, project-specific)
42
40
  import config from './core/config.js';
43
41
 
42
+ // ============================================================================
43
+ // CRITICAL: Check Node.js version compatibility
44
+ // ============================================================================
45
+ const NODE_VERSION = process.version;
46
+ const NODE_MAJOR = parseInt(NODE_VERSION.slice(1).split('.')[0]);
47
+
48
+ if (NODE_MAJOR >= 23) {
49
+ console.error('\n' + '='.repeat(70));
50
+ console.error('⚠️ CRITICAL: Node.js v23+ is not compatible with AgentWindow!');
51
+ console.error('='.repeat(70));
52
+ console.error(`Current version: ${NODE_VERSION}`);
53
+ console.error('Supported versions: Node.js v18 LTS or v20 LTS');
54
+ console.error('\n❌ Issues with Node.js v23:');
55
+ console.error(' - ES module import syntax incompatibility');
56
+ console.error(' - Potential runtime errors with Discord.js');
57
+ console.error(' - Breaking changes in core modules');
58
+ console.error('\n✅ Recommended actions:');
59
+ console.error(' 1. Install Node.js v20 LTS (recommended)');
60
+ console.error(' Using nvm: nvm install 20 && nvm use 20');
61
+ console.error(' Using Homebrew: brew install node@20');
62
+ console.error(' 2. Reinstall global packages: npm install -g agent-window');
63
+ console.error(' 3. Restart your bots');
64
+ console.error('\n📖 See: https://nodejs.org/en/download/releases');
65
+ console.error('='.repeat(70) + '\n');
66
+ process.exit(1);
67
+ }
68
+
44
69
  // Extract commonly used config values for convenience
45
70
  const BOT_TOKEN = config.discord.token;
46
71
  const PROJECT_DIR = config.workspace.projectDir;
@@ -49,6 +74,7 @@ const ALLOWED_CHANNELS = config.discord.allowedChannels;
49
74
  const CHANNEL_SESSIONS_FILE = config.paths.sessions;
50
75
  const PENDING_DIR = config.paths.pending;
51
76
  const HOOK_DIR = config.paths.hooks;
77
+ const USE_DOCKER = config.workspace.useDocker; // Whether to use Docker or run locally
52
78
  const CONTAINER_NAME = config.workspace.containerName;
53
79
  const DOCKER_IMAGE = config.workspace.dockerImage;
54
80
 
@@ -118,20 +144,37 @@ function updateHealthStatus(component, status) {
118
144
 
119
145
  // Get overall health status
120
146
  function getOverallHealth() {
121
- const checks = [
122
- healthStatus.pm2 ? '✓' : '✗',
123
- healthStatus.discord ? '✓' : '✗',
124
- healthStatus.docker ? '✓' : '✗'
125
- ].join(' ');
126
-
127
- if (healthStatus.pm2 && healthStatus.discord && healthStatus.docker) {
128
- return 'healthy';
129
- } else if (healthStatus.pm2 && healthStatus.discord) {
130
- return 'degraded'; // Running but Docker failed
131
- } else if (healthStatus.pm2) {
132
- return 'unhealthy'; // PM2 running but Discord disconnected
147
+ // Build health check string
148
+ const checks = [healthStatus.pm2 ? '✓' : '✗', healthStatus.discord ? '✓' : '✗'];
149
+
150
+ // Only check Docker if enabled
151
+ if (USE_DOCKER) {
152
+ checks.push(healthStatus.docker ? '✓' : '✗');
153
+ }
154
+
155
+ const healthString = checks.join(' ');
156
+
157
+ // Determine overall status
158
+ if (USE_DOCKER) {
159
+ // Docker mode: all components must be healthy
160
+ if (healthStatus.pm2 && healthStatus.discord && healthStatus.docker) {
161
+ return 'healthy';
162
+ } else if (healthStatus.pm2 && healthStatus.discord) {
163
+ return 'degraded'; // Running but Docker failed
164
+ } else if (healthStatus.pm2) {
165
+ return 'unhealthy'; // PM2 running but Discord disconnected
166
+ } else {
167
+ return 'failed';
168
+ }
133
169
  } else {
134
- return 'failed';
170
+ // Non-Docker mode: only PM2 and Discord required
171
+ if (healthStatus.pm2 && healthStatus.discord) {
172
+ return 'healthy';
173
+ } else if (healthStatus.pm2) {
174
+ return 'unhealthy'; // PM2 running but Discord disconnected
175
+ } else {
176
+ return 'failed';
177
+ }
135
178
  }
136
179
  }
137
180
 
@@ -346,6 +389,13 @@ function isContainerRunning() {
346
389
 
347
390
  // Start or ensure persistent container is running
348
391
  function ensureContainer() {
392
+ // Skip Docker operations if not using Docker
393
+ if (!USE_DOCKER) {
394
+ console.log('[Docker] Docker is disabled (useDocker: false). Running in local mode.');
395
+ updateHealthStatus('docker', true); // Mark as "healthy" since we don't need Docker
396
+ return true;
397
+ }
398
+
349
399
  // Log the paths being used
350
400
  console.log('[Docker] PENDING_DIR (host):', PENDING_DIR);
351
401
  console.log('[Docker] HOOK_DIR (host):', HOOK_DIR);
@@ -437,6 +487,11 @@ function ensureContainer() {
437
487
 
438
488
  // Stop persistent container (call on bot shutdown)
439
489
  function stopContainer() {
490
+ if (!USE_DOCKER) {
491
+ console.log('[Docker] Docker is disabled. No container to stop.');
492
+ return;
493
+ }
494
+
440
495
  try {
441
496
  execSync(`docker stop ${CONTAINER_NAME} 2>/dev/null`);
442
497
  execSync(`docker rm ${CONTAINER_NAME} 2>/dev/null`);
@@ -1445,21 +1500,42 @@ const commands = {
1445
1500
  let lastStatusUpdate = 0;
1446
1501
  const task = activeTasks.get(channelId);
1447
1502
 
1448
- // Use docker exec to run CLI in the persistent container
1449
- const dockerArgs = [
1450
- 'exec', '-i',
1451
- CONTAINER_NAME,
1452
- CLI_COMMAND,
1453
- ...cliArgs
1454
- ];
1503
+ // Execute CLI command (either in Docker or locally based on configuration)
1504
+ let child;
1455
1505
 
1456
- console.log('[Docker] Executing in container:', CONTAINER_NAME);
1506
+ if (USE_DOCKER) {
1507
+ // Use docker exec to run CLI in the persistent container
1508
+ const dockerArgs = [
1509
+ 'exec', '-i',
1510
+ CONTAINER_NAME,
1511
+ CLI_COMMAND,
1512
+ ...cliArgs
1513
+ ];
1457
1514
 
1458
- const child = spawn('docker', dockerArgs, {
1459
- stdio: ['pipe', 'pipe', 'pipe'],
1460
- });
1515
+ console.log('[Docker] Executing in container:', CONTAINER_NAME);
1461
1516
 
1462
- child.stdin.end();
1517
+ child = spawn('docker', dockerArgs, {
1518
+ stdio: ['pipe', 'pipe', 'pipe'],
1519
+ });
1520
+
1521
+ child.stdin.end();
1522
+ } else {
1523
+ // Run CLI directly on local host
1524
+ console.log('[Local] Executing locally:', CLI_COMMAND, ...cliArgs);
1525
+
1526
+ child = spawn(CLI_COMMAND, cliArgs, {
1527
+ stdio: ['pipe', 'pipe', 'pipe'],
1528
+ cwd: PROJECT_DIR,
1529
+ env: {
1530
+ ...process.env,
1531
+ CLAUDE_CODE_OAUTH_TOKEN: OAUTH_TOKEN || '',
1532
+ HOME: process.env.HOME,
1533
+ PATH: process.env.PATH
1534
+ }
1535
+ });
1536
+
1537
+ child.stdin.end();
1538
+ }
1463
1539
 
1464
1540
  // Update status periodically (with stop button)
1465
1541
  // forceUpdate=true bypasses throttle for important state changes
@@ -1762,18 +1838,9 @@ client.on(Events.MessageCreate, async (message) => {
1762
1838
  return;
1763
1839
  }
1764
1840
 
1765
- // SECURITY: Validate channel access
1766
- // - If ALLOWED_CHANNELS is set, only allow those channels
1767
- // - If ALLOWED_CHANNELS is missing (should not happen due to config validation), reject ALL non-DM messages
1768
- if (!isDM) {
1769
- if (!ALLOWED_CHANNELS || ALLOWED_CHANNELS.length === 0) {
1770
- // Defensive: If no channels are configured, reject all non-DM messages
1771
- console.error('[MSG] SECURITY: No ALLOWED_CHANNELS configured. Rejecting message for safety.');
1772
- console.error('[MSG] Please configure ALLOWED_CHANNELS in your config.json');
1773
- return;
1774
- }
1841
+ if (ALLOWED_CHANNELS && !isDM) {
1775
1842
  if (!ALLOWED_CHANNELS.includes(message.channel.id)) {
1776
- console.log(`[MSG] Ignored: channel ${message.channel.id} not in allowed list`);
1843
+ console.log('[MSG] Ignored: channel not allowed');
1777
1844
  return;
1778
1845
  }
1779
1846
  }
@@ -1808,11 +1875,52 @@ client.on(Events.MessageCreate, async (message) => {
1808
1875
 
1809
1876
  // Ready event
1810
1877
  client.on(Events.ClientReady, async () => {
1878
+ // ========================================================================
1879
+ // CRITICAL: Check for duplicate bot instances
1880
+ // ========================================================================
1881
+ try {
1882
+ const { execSync } = await import('child_process');
1883
+ const psCommand = process.platform === 'win32'
1884
+ ? 'tasklist | findstr node.exe'
1885
+ : 'ps aux | grep "node.*bot.js" | grep -v grep';
1886
+
1887
+ const result = execSync(psCommand, { encoding: 'utf-8', timeout: 5000 });
1888
+ const botProcesses = result.split('\n').filter(line => line.trim() && line.includes('bot.js'));
1889
+
1890
+ if (botProcesses.length > 1) {
1891
+ console.error('\n' + '='.repeat(70));
1892
+ console.error('⚠️ CRITICAL WARNING: Multiple bot instances detected!');
1893
+ console.error('='.repeat(70));
1894
+ console.error(`Found ${botProcesses.length} bot instances running concurrently.`);
1895
+ console.error('This will cause duplicate responses to every message!');
1896
+ console.error('\n🔍 Running instances:');
1897
+ botProcesses.forEach((proc, i) => {
1898
+ console.error(` ${i + 1}. ${proc.trim()}`);
1899
+ });
1900
+ console.error('\n🔧 To fix this issue:');
1901
+ console.error(' 1. Check running PM2 processes: pm2 list');
1902
+ console.error(' 2. Stop all bots: pm2 stop all && pm2 delete all');
1903
+ console.error(' 3. Kill PM2 daemon: pm2 kill');
1904
+ console.error(' 4. Kill all bot processes: pkill -9 -f "node.*bot.js"');
1905
+ console.error(' 5. Clean dump file: rm -f ~/.pm2/dump.pm2');
1906
+ console.error(' 6. Restart single instance: pm2 start ecosystem.config.cjs');
1907
+ console.error('\n📖 Or use the cleanup script: npm run cleanup-pm2');
1908
+ console.error('='.repeat(70) + '\n');
1909
+ } else {
1910
+ console.log('✓ Instance check: Single bot instance (good)');
1911
+ }
1912
+ } catch (e) {
1913
+ // Command failed (likely no other instances or platform not supported)
1914
+ console.log('✓ Instance check: Passed');
1915
+ }
1916
+
1811
1917
  console.log(`AgentBridge logged in as ${client.user.tag}`);
1812
1918
  console.log(`Bot User ID: ${client.user.id}`);
1813
1919
  console.log(`Project: ${PROJECT_DIR}`);
1814
- console.log(`Mode: Docker Sandbox + Hooks Permission`);
1815
- console.log(`Container: ${CONTAINER_NAME}`);
1920
+ console.log(`Mode: ${USE_DOCKER ? 'Docker Sandbox' : 'Local Host'} + Hooks Permission`);
1921
+ if (USE_DOCKER) {
1922
+ console.log(`Container: ${CONTAINER_NAME}`);
1923
+ }
1816
1924
  console.log(`OAuth: ${OAUTH_TOKEN ? 'configured' : 'not set'}`);
1817
1925
 
1818
1926
  // Update PM2 and Discord health status
@@ -1853,9 +1961,14 @@ client.on(Events.ClientReady, async () => {
1853
1961
  // Send startup notification to Discord channels
1854
1962
  try {
1855
1963
  const uptime = Math.floor((Date.now() - healthStatus.startTime) / 1000);
1856
- const dockerStatus = healthStatus.docker ? '✓ Connected' : '✗ Not Available';
1857
1964
  const overallStatus = overallHealth.toUpperCase();
1858
1965
 
1966
+ // Build execution mode info
1967
+ const execMode = USE_DOCKER ? 'Docker Container' : 'Local Host';
1968
+ const dockerStatus = USE_DOCKER
1969
+ ? (healthStatus.docker ? '✓ Connected' : '✗ Not Available')
1970
+ : '⊘ Disabled (Local Mode)';
1971
+
1859
1972
  // Build startup message
1860
1973
  const startupMessage = {
1861
1974
  embeds: [{
@@ -1867,6 +1980,11 @@ client.on(Events.ClientReady, async () => {
1867
1980
  value: overallStatus,
1868
1981
  inline: true
1869
1982
  },
1983
+ {
1984
+ name: 'Execution Mode',
1985
+ value: execMode,
1986
+ inline: true
1987
+ },
1870
1988
  {
1871
1989
  name: 'Docker',
1872
1990
  value: dockerStatus,
@@ -1879,7 +1997,9 @@ client.on(Events.ClientReady, async () => {
1879
1997
  },
1880
1998
  {
1881
1999
  name: 'Components',
1882
- value: `PM2: ✓\nDiscord: ✓\nDocker: ${healthStatus.docker ? '✓' : '✗'}`,
2000
+ value: USE_DOCKER
2001
+ ? `PM2: ✓\nDiscord: ✓\nDocker: ${healthStatus.docker ? '✓' : '✗'}`
2002
+ : `PM2: ✓\nDiscord: ✓\nDocker: ⊘ (Local Mode)`,
1883
2003
  inline: false
1884
2004
  }
1885
2005
  ],
@@ -81,6 +81,7 @@ function loadConfig() {
81
81
  // === Workspace Configuration ===
82
82
  workspace: {
83
83
  projectDir: expandPath(fileConfig.PROJECT_DIR) || process.cwd(),
84
+ useDocker: fileConfig.workspace?.useDocker !== false, // Default to true for backward compatibility
84
85
  containerName: fileConfig.workspace?.containerName || 'claude-discord-bot',
85
86
  dockerImage: fileConfig.workspace?.dockerImage || 'claude-sandbox',
86
87
  portMappings: fileConfig.workspace?.portMappings || [],