agent-window 1.2.1 → 1.2.4

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/src/bot.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * - Permission system with chat-based approval buttons
7
7
  * - Session management with channel binding
8
8
  * - Multi-agent support (Claude Code, Codex, OpenCode)
9
+ * - Model selection (opus/sonnet/haiku, default: opus)
9
10
  *
10
11
  * Commands:
11
12
  * @bot <message> - Send message (continues last conversation)
@@ -14,6 +15,7 @@
14
15
  * @bot /pick - Choose session (button UI)
15
16
  * @bot /sessions - List recent sessions
16
17
  * @bot /status - Check bot status
18
+ * @bot /model [model] - View or change AI model (opus/sonnet/haiku)
17
19
  * @bot /help - Show help
18
20
  */
19
21
 
@@ -67,6 +69,59 @@ const pendingPermissions = new Map(); // requestId -> { channelId, message }
67
69
 
68
70
  let containerReady = false;
69
71
 
72
+ // Model configuration
73
+ const AVAILABLE_MODELS = {
74
+ opus: 'claude-opus-4-20250514',
75
+ sonnet: 'claude-sonnet-4-20250514',
76
+ haiku: 'claude-haiku-4-20250514'
77
+ };
78
+
79
+ // Per-channel model selection (channelId -> modelKey)
80
+ const channelModels = new Map();
81
+
82
+ // Default model (opus as requested)
83
+ const DEFAULT_MODEL = 'opus';
84
+
85
+ // Health status tracking
86
+ const HEALTH_STATUS_FILE = join(PROJECT_DIR, '.health-status.json');
87
+ const healthStatus = {
88
+ pm2: false,
89
+ discord: false,
90
+ docker: false,
91
+ startTime: Date.now(),
92
+ lastUpdate: Date.now()
93
+ };
94
+
95
+ // Update health status
96
+ function updateHealthStatus(component, status) {
97
+ healthStatus[component] = status;
98
+ healthStatus.lastUpdate = Date.now();
99
+ try {
100
+ writeFileSync(HEALTH_STATUS_FILE, JSON.stringify(healthStatus, null, 2));
101
+ } catch (e) {
102
+ console.error('[Health] Failed to write status file:', e.message);
103
+ }
104
+ }
105
+
106
+ // Get overall health status
107
+ function getOverallHealth() {
108
+ const checks = [
109
+ healthStatus.pm2 ? '✓' : '✗',
110
+ healthStatus.discord ? '✓' : '✗',
111
+ healthStatus.docker ? '✓' : '✗'
112
+ ].join(' ');
113
+
114
+ if (healthStatus.pm2 && healthStatus.discord && healthStatus.docker) {
115
+ return 'healthy';
116
+ } else if (healthStatus.pm2 && healthStatus.discord) {
117
+ return 'degraded'; // Running but Docker failed
118
+ } else if (healthStatus.pm2) {
119
+ return 'unhealthy'; // PM2 running but Discord disconnected
120
+ } else {
121
+ return 'failed';
122
+ }
123
+ }
124
+
70
125
  // Ensure pending directory exists
71
126
  try {
72
127
  mkdirSync(PENDING_DIR, { recursive: true });
@@ -147,9 +202,26 @@ function ensureContainer() {
147
202
 
148
203
  execSync(dockerCmd);
149
204
  console.log('[Docker] Container started successfully');
205
+ updateHealthStatus('docker', true);
150
206
  return true;
151
207
  } catch (e) {
152
- console.error('[Docker] Failed to start container:', e.message);
208
+ const errorMsg = e.message || '';
209
+ console.error('[Docker] Failed to start container:', errorMsg);
210
+
211
+ // Provide helpful guidance for common Docker issues
212
+ if (errorMsg.includes('ECONNREFUSED') || errorMsg.includes('Cannot connect') || errorMsg.includes('connect')) {
213
+ console.error('[Docker] Docker daemon is not running!');
214
+ console.error('[Docker] Please restart Docker Desktop:');
215
+ console.error('[Docker] 1. Click Docker Desktop icon in menu bar');
216
+ console.error('[Docker] 2. Select "Restart" or wait for daemon to start');
217
+ } else if (errorMsg.includes('image not found') || errorMsg.includes('no such image')) {
218
+ console.error('[Docker] Docker image not found.');
219
+ console.error('[Docker] Pull the image first: docker pull', DOCKER_IMAGE);
220
+ } else if (errorMsg.includes('permission denied')) {
221
+ console.error('[Docker] Permission denied. Try: sudo docker pull', DOCKER_IMAGE);
222
+ }
223
+
224
+ updateHealthStatus('docker', false);
153
225
  return false;
154
226
  }
155
227
  }
@@ -742,6 +814,7 @@ const commands = {
742
814
  \`@bot /pick\` - Choose session (new or resume)
743
815
  \`@bot /sessions\` - List all recent sessions
744
816
  \`@bot /status\` - Check bot status
817
+ \`@bot /model\` - View or change AI model (opus/sonnet/haiku)
745
818
  \`@bot /help\` - Show this help`;
746
819
 
747
820
  await message.channel.send({
@@ -762,9 +835,11 @@ const commands = {
762
835
  const hours = Math.floor(uptime / 3600);
763
836
  const minutes = Math.floor((uptime % 3600) / 60);
764
837
  const containerRunning = isContainerRunning();
838
+ const currentModel = channelModels.get(message.channel.id) || DEFAULT_MODEL;
765
839
 
766
840
  const statusText = `**Status**: Online
767
841
  **Mode**: Docker Sandbox + Hooks Permission
842
+ **Model**: ${currentModel} (${AVAILABLE_MODELS[currentModel]})
768
843
  **Uptime**: ${hours}h ${minutes}m
769
844
  **Container**: ${CONTAINER_NAME} (${containerRunning ? '🟢 running' : '🔴 stopped'})
770
845
  **Project**: ${PROJECT_DIR}
@@ -824,6 +899,56 @@ const commands = {
824
899
  }
825
900
  },
826
901
 
902
+ async model(message, args) {
903
+ const channelId = message.channel.id;
904
+ const action = args[0];
905
+
906
+ // Show current model
907
+ if (!action) {
908
+ const currentModel = channelModels.get(channelId) || DEFAULT_MODEL;
909
+ const modelId = AVAILABLE_MODELS[currentModel];
910
+
911
+ const modelText = `**Current Model:** ${currentModel} (${modelId})
912
+
913
+ **Available Models:**
914
+ • **opus** - Most capable, best for complex tasks
915
+ • **sonnet** - Balanced performance and speed
916
+ • **haiku** - Fastest, for simple tasks
917
+
918
+ **Usage:**
919
+ \`@bot /model opus\` - Switch to Opus
920
+ \`@bot /model sonnet\` - Switch to Sonnet
921
+ \`@bot /model haiku\` - Switch to Haiku`;
922
+
923
+ await message.channel.send({
924
+ embeds: [{
925
+ title: 'AI Model Configuration',
926
+ description: modelText,
927
+ color: currentModel === 'opus' ? 0x9B59B6 : currentModel === 'sonnet' ? 0x3498DB : 0x2ECC71,
928
+ }]
929
+ });
930
+ return;
931
+ }
932
+
933
+ // Switch model
934
+ const newModel = action.toLowerCase();
935
+ if (!AVAILABLE_MODELS[newModel]) {
936
+ await message.channel.send(`Invalid model: ${action}\nAvailable models: opus, sonnet, haiku`);
937
+ return;
938
+ }
939
+
940
+ channelModels.set(channelId, newModel);
941
+ const modelId = AVAILABLE_MODELS[newModel];
942
+
943
+ await message.channel.send({
944
+ embeds: [{
945
+ title: 'Model Changed',
946
+ description: `Switched to **${newModel}** (${modelId})\n\nThis model will be used for new conversations in this channel.`,
947
+ color: newModel === 'opus' ? 0x9B59B6 : newModel === 'sonnet' ? 0x3498DB : 0x2ECC71,
948
+ }]
949
+ });
950
+ },
951
+
827
952
  // Show session selection card with buttons
828
953
  async showSessionPicker(message) {
829
954
  try {
@@ -1016,6 +1141,12 @@ const commands = {
1016
1141
  '--settings', CONTAINER_SETTINGS_FILE,
1017
1142
  ];
1018
1143
 
1144
+ // Add model selection (defaults to opus)
1145
+ const selectedModel = channelModels.get(channelId) || DEFAULT_MODEL;
1146
+ const modelId = AVAILABLE_MODELS[selectedModel];
1147
+ cliArgs.push('--model', modelId);
1148
+ console.log('[Claude] Using model:', selectedModel, `(${modelId})`);
1149
+
1019
1150
  // Track if we're forcing a new session or resuming a specific one
1020
1151
  if (userMessage.startsWith('-n ') || userMessage.startsWith('--new ')) {
1021
1152
  // Force new session
@@ -1439,7 +1570,7 @@ client.on(Events.MessageCreate, async (message) => {
1439
1570
  });
1440
1571
 
1441
1572
  // Ready event
1442
- client.on(Events.ClientReady, () => {
1573
+ client.on(Events.ClientReady, async () => {
1443
1574
  console.log(`AgentBridge logged in as ${client.user.tag}`);
1444
1575
  console.log(`Bot User ID: ${client.user.id}`);
1445
1576
  console.log(`Project: ${PROJECT_DIR}`);
@@ -1447,6 +1578,10 @@ client.on(Events.ClientReady, () => {
1447
1578
  console.log(`Container: ${CONTAINER_NAME}`);
1448
1579
  console.log(`OAuth: ${OAUTH_TOKEN ? 'configured' : 'not set'}`);
1449
1580
 
1581
+ // Update PM2 and Discord health status
1582
+ updateHealthStatus('pm2', true);
1583
+ updateHealthStatus('discord', true);
1584
+
1450
1585
  // Start persistent container (non-blocking)
1451
1586
  console.log('[Docker] Attempting to start container...');
1452
1587
  containerReady = ensureContainer();
@@ -1455,6 +1590,12 @@ client.on(Events.ClientReady, () => {
1455
1590
  console.warn('[Docker] AI commands will not work without the container.');
1456
1591
  console.warn('[Docker] To fix: 1) Login to ghcr.io, 2) Restart bot');
1457
1592
  }
1593
+
1594
+ // Log overall health status
1595
+ const overallHealth = getOverallHealth();
1596
+ console.log(`[Health] Overall status: ${overallHealth.toUpperCase()}`);
1597
+ console.log(`[Health] Components: PM2=${healthStatus.pm2 ? '✓' : '✗'} Discord=${healthStatus.discord ? '✓' : '✗'} Docker=${healthStatus.docker ? '✓' : '✗'}`);
1598
+
1458
1599
  console.log(`Allowed Channels Config: ${ALLOWED_CHANNELS ? ALLOWED_CHANNELS.join(', ') : 'All'}`);
1459
1600
  console.log(`Guilds: ${client.guilds.cache.size}`);
1460
1601
  client.guilds.cache.forEach(guild => {
@@ -1471,6 +1612,63 @@ client.on(Events.ClientReady, () => {
1471
1612
  startPermissionWatcher();
1472
1613
 
1473
1614
  console.log(`Ready! @${client.user.username} to interact`);
1615
+
1616
+ // Send startup notification to Discord channels
1617
+ try {
1618
+ const uptime = Math.floor((Date.now() - healthStatus.startTime) / 1000);
1619
+ const dockerStatus = healthStatus.docker ? '✓ Connected' : '✗ Not Available';
1620
+ const overallStatus = overallHealth.toUpperCase();
1621
+
1622
+ // Build startup message
1623
+ const startupMessage = {
1624
+ embeds: [{
1625
+ title: '🤖 AgentWindow Bot Started',
1626
+ color: overallStatus === 'HEALTHY' ? 0x00FF00 : overallStatus === 'DEGRADED' ? 0xFFFF00 : 0xFF0000,
1627
+ fields: [
1628
+ {
1629
+ name: 'Status',
1630
+ value: overallStatus,
1631
+ inline: true
1632
+ },
1633
+ {
1634
+ name: 'Docker',
1635
+ value: dockerStatus,
1636
+ inline: true
1637
+ },
1638
+ {
1639
+ name: 'Startup Time',
1640
+ value: `${uptime}s`,
1641
+ inline: true
1642
+ },
1643
+ {
1644
+ name: 'Components',
1645
+ value: `PM2: ✓\nDiscord: ✓\nDocker: ${healthStatus.docker ? '✓' : '✗'}`,
1646
+ inline: false
1647
+ }
1648
+ ],
1649
+ timestamp: new Date().toISOString(),
1650
+ footer: {
1651
+ text: `AgentWindow v1.2.1`
1652
+ }
1653
+ }]
1654
+ };
1655
+
1656
+ // Send to all allowed channels
1657
+ const channels = ALLOWED_CHANNELS || [];
1658
+ for (const channelId of channels) {
1659
+ try {
1660
+ const channel = await client.channels.fetch(channelId);
1661
+ if (channel && channel.isTextBased()) {
1662
+ await channel.send(startupMessage);
1663
+ console.log(`[Notification] Startup message sent to #${channel.name}`);
1664
+ }
1665
+ } catch (e) {
1666
+ console.error(`[Notification] Failed to send to channel ${channelId}:`, e.message);
1667
+ }
1668
+ }
1669
+ } catch (e) {
1670
+ console.error('[Notification] Failed to send startup notification:', e.message);
1671
+ }
1474
1672
  });
1475
1673
 
1476
1674
  // Error handling
@@ -1,7 +1,35 @@
1
1
  /**
2
2
  * Instance Manager
3
3
  *
4
- * Manages AgentWindow BMAD plugin instances - add, list, remove, get info.
4
+ * Manages AgentWindow bot instances with clear separation between two types:
5
+ *
6
+ * ## Instance Types
7
+ *
8
+ * ### 1. Simple-Config (基础版)
9
+ * - **Discovery**: Auto-discovered from PM2 processes
10
+ * - **Config Location**: ~/.agent-window/bots/{name}/config.json
11
+ * - **Management**: Unified management by AgentWindow
12
+ * - **Use Case**: Simple bots managed through Web UI
13
+ * - **Import Method**: Discovered via "Discover" button in UI
14
+ *
15
+ * ### 2. BMAD-Plugin (BMAD 插件版)
16
+ * - **Discovery**: Manually added by user
17
+ * - **Config Location**: User's project directory
18
+ * - **Management**: User manages their BMAD project
19
+ * - **Use Case**: Development projects with BMAD framework
20
+ * - **Import Method**: Added via "Add Instance" button with validation
21
+ * - **Requirements**:
22
+ * - Must have _agent-bridge/ directory
23
+ * - Must have _agent-bridge/src/bot.js
24
+ * - Must pass validateInstance() check
25
+ *
26
+ * ## Business Rules
27
+ *
28
+ * 1. Simple-config instances are auto-discovered from PM2
29
+ * 2. BMAD-plugin instances must be manually added through UI
30
+ * 3. BMAD-plugin instances require strict validation
31
+ * 4. Simple-config instances cannot be manually added (must be discovered)
32
+ *
5
33
  * Uses platform abstraction layer for cross-platform compatibility.
6
34
  */
7
35
 
@@ -63,12 +91,19 @@ export async function writeInstances(data) {
63
91
 
64
92
  /**
65
93
  * Add a new instance
94
+ *
95
+ * Business Logic:
96
+ * - For creating NEW simple-config instances: Pass createConfig option with config data
97
+ * - For importing EXISTING BMAD plugins: Pass projectPath, will be validated
98
+ *
66
99
  * @param {string} name - Instance name (identifier)
67
- * @param {string} projectPath - Path to BMAD project
100
+ * @param {string} projectPath - Path to project (for BMAD plugins) or working directory
68
101
  * @param {Object} options - Additional options
69
102
  * @param {string} options.displayName - Display name
70
103
  * @param {string[]} options.tags - Tags for categorization
71
104
  * @param {string} options.configPath - Path to config file (optional)
105
+ * @param {string} options.instanceType - Explicit instance type ('simple-config' | 'bmad-plugin')
106
+ * @param {boolean} options.createConfig - Whether this is creating a new simple-config instance
72
107
  * @returns {Promise<Object>} Result with success and message
73
108
  */
74
109
  export async function addInstance(name, projectPath, options = {}) {
@@ -89,14 +124,49 @@ export async function addInstance(name, projectPath, options = {}) {
89
124
  };
90
125
  }
91
126
 
127
+ const botsDir = paths.getBotsDir();
128
+ let newInstance;
129
+ let configPath;
130
+
131
+ // Case 1: Creating a NEW simple-config instance from scratch
132
+ if (options.instanceType === 'simple-config' || options.createConfig) {
133
+ // Config path for simple-config instances
134
+ configPath = options.configPath || path.join(botsDir, name, 'config.json');
135
+
136
+ // Ensure config directory exists
137
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
138
+
139
+ newInstance = {
140
+ name,
141
+ displayName: options.displayName || name,
142
+ projectPath: projectPath, // For simple-config, this is the working directory
143
+ configPath,
144
+ pluginPath: null,
145
+ botName: `bot-${name}`,
146
+ instanceType: 'simple-config',
147
+ addedAt: new Date().toISOString(),
148
+ tags: options.tags || [],
149
+ enabled: true
150
+ };
151
+
152
+ data.instances.push(newInstance);
153
+ await writeInstances(data);
154
+
155
+ return {
156
+ success: true,
157
+ instance: newInstance
158
+ };
159
+ }
160
+
161
+ // Case 2: Importing an EXISTING BMAD plugin
92
162
  // Validate the instance
93
163
  const validation = await validateInstance(projectPath);
94
164
  if (!validation.valid) {
95
- // Provide helpful error message based on instance type
165
+ // Only BMAD plugins can be manually added
96
166
  const typeMsg = validation.instanceType === 'simple-config'
97
- ? '简单配置实例无法通过"添加实例"功能添加。请先使用 PM2 启动 bot,然后在 Web UI 中导入。'
167
+ ? '简单配置实例无法通过"添加实例"功能添加。请使用"Discover"功能导入。'
98
168
  : validation.instanceType === 'unknown'
99
- ? '无法识别的项目类型'
169
+ ? '无法识别的项目类型,必须是有效的 BMAD 插件'
100
170
  : '项目不是有效的 AgentWindow BMAD 插件';
101
171
 
102
172
  return {
@@ -106,21 +176,26 @@ export async function addInstance(name, projectPath, options = {}) {
106
176
  };
107
177
  }
108
178
 
109
- // Determine config path
110
- const botsDir = paths.getBotsDir();
111
- const defaultConfigPath = path.join(botsDir, name, 'config.json');
112
- const configPath = options.configPath ||
113
- (existsSync(defaultConfigPath) ? defaultConfigPath : null);
179
+ // Only allow importing BMAD plugins via Add Instance
180
+ if (validation.instanceType !== 'bmad-plugin') {
181
+ return {
182
+ success: false,
183
+ error: '只能通过"添加实例"功能导入 BMAD 插件。基础版实例请使用"Discover"功能导入。',
184
+ validation
185
+ };
186
+ }
114
187
 
115
- // Add instance
116
- const newInstance = {
188
+ // Determine config path for BMAD plugin
189
+ configPath = options.configPath || null;
190
+
191
+ newInstance = {
117
192
  name,
118
193
  displayName: options.displayName || name,
119
194
  projectPath: validation.projectPath,
120
195
  pluginPath: validation.pluginPath,
121
196
  configPath,
122
197
  botName: `bot-${name}`,
123
- instanceType: validation.instanceType, // Save instance type
198
+ instanceType: validation.instanceType,
124
199
  addedAt: new Date().toISOString(),
125
200
  tags: options.tags || [],
126
201
  enabled: true
@@ -256,6 +331,7 @@ export async function discoverInstances() {
256
331
  let projectPath = null;
257
332
  let configPath = null;
258
333
  let pluginPath = null;
334
+ let hasValidConfig = false;
259
335
 
260
336
  // Priority 1: Read CONFIG_PATH and extract PROJECT_DIR from config
261
337
  if (proc.configPath && existsSync(proc.configPath)) {
@@ -271,6 +347,7 @@ export async function discoverInstances() {
271
347
  // Fallback to config directory
272
348
  projectPath = path.dirname(configPath);
273
349
  }
350
+ hasValidConfig = true;
274
351
  } catch {
275
352
  // If config read fails, use directory name
276
353
  projectPath = path.dirname(configPath);
@@ -296,17 +373,33 @@ export async function discoverInstances() {
296
373
  // Fallback to cwd if available
297
374
  projectPath = proc.cwd || null;
298
375
  }
376
+ hasValidConfig = true;
299
377
  } catch {
300
378
  // If config read fails, use cwd
301
379
  projectPath = proc.cwd || null;
302
380
  }
303
381
  }
304
382
  }
305
- // Priority 3: Use cwd
383
+ // Priority 3: Use cwd (but this is NOT a valid AgentWindow instance)
306
384
  else if (proc.cwd) {
307
385
  projectPath = proc.cwd;
308
386
  }
309
387
 
388
+ // CRITICAL: Skip processes without a valid configuration
389
+ // A valid AgentWindow instance MUST have either:
390
+ // 1. A CONFIG_PATH env variable pointing to a valid config file, OR
391
+ // 2. An inferred config file that exists
392
+ if (!hasValidConfig || !configPath) {
393
+ console.debug(`[Discover] Skipping ${proc.name}: no valid config file (CONFIG_PATH not set, no inferred config found)`);
394
+ continue;
395
+ }
396
+
397
+ // Additional validation: project path must exist
398
+ if (!projectPath || !existsSync(projectPath)) {
399
+ console.debug(`[Discover] Skipping ${proc.name}: project path does not exist (${projectPath})`);
400
+ continue;
401
+ }
402
+
310
403
  // Check if it's a BMAD project (has _bmad directory)
311
404
  if (projectPath && existsSync(path.join(projectPath, '_bmad'))) {
312
405
  pluginPath = path.join(projectPath, '_agent-bridge', 'src');
@@ -316,12 +409,18 @@ export async function discoverInstances() {
316
409
  }
317
410
 
318
411
  // Detect instance type based on project structure
319
- // projectPath already points to PROJECT_DIR from config if available
412
+ // Business Logic:
413
+ // - simple-config: Auto-discovered, managed by AgentWindow
414
+ // - bmad-plugin: Discovered but needs user confirmation
320
415
  let instanceType = 'unknown';
321
416
  if (projectPath && existsSync(projectPath)) {
322
417
  instanceType = detectInstanceType(projectPath);
323
418
  }
324
419
 
420
+ // NOTE: We allow both types to be discovered
421
+ // - simple-config: Can be imported directly
422
+ // - bmad-plugin: Shown in discover list for user to review and import
423
+
325
424
  discovered.push({
326
425
  botName: proc.name,
327
426
  name: instanceName,
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { pm2 as platformPM2 } from '../platform/index.js';
9
+ import { existsSync } from 'fs';
9
10
 
10
11
  /**
11
12
  * PM2 process info
@@ -162,21 +163,90 @@ export async function getStatus(name) {
162
163
  if (!proc) {
163
164
  return {
164
165
  name,
165
- status: 'unknown',
166
- exists: false
166
+ status: 'stopped',
167
+ exists: false,
168
+ health: {
169
+ pm2: false,
170
+ discord: false,
171
+ docker: false,
172
+ overall: 'stopped'
173
+ }
167
174
  };
168
175
  }
169
176
 
177
+ // Try to read health status file
178
+ let healthStatus = {
179
+ pm2: proc.status === 'online',
180
+ discord: false,
181
+ docker: false,
182
+ overall: 'stopped'
183
+ };
184
+
185
+ let containerName = null;
186
+
187
+ try {
188
+ const { readFileSync } = await import('fs');
189
+ const { join } = await import('path');
190
+ const configPath = proc.configPath; // Use configPath from platform abstraction layer
191
+
192
+ if (configPath) {
193
+ // Read the config file to get PROJECT_DIR and container name
194
+ try {
195
+ const configContent = JSON.parse(readFileSync(configPath, 'utf-8'));
196
+ const projectDir = configContent.PROJECT_DIR;
197
+ containerName = configContent.workspace?.containerName || null;
198
+
199
+ if (projectDir) {
200
+ // Health file is in the project directory
201
+ const healthFile = join(projectDir, '.health-status.json');
202
+
203
+ if (existsSync(healthFile)) {
204
+ const healthData = JSON.parse(readFileSync(healthFile, 'utf-8'));
205
+ healthStatus = {
206
+ pm2: proc.status === 'online',
207
+ discord: healthData.discord || false,
208
+ docker: healthData.docker || false,
209
+ overall: healthData.docker ? 'healthy' : 'degraded'
210
+ };
211
+ }
212
+ }
213
+ } catch (e) {
214
+ console.debug('[Health] Could not read project from config:', e.message);
215
+ }
216
+ }
217
+ } catch (e) {
218
+ // If health file doesn't exist or can't be read, use PM2 status only
219
+ console.debug('[Health] Could not read health status file:', e.message);
220
+ }
221
+
222
+ // If Docker status is still false and we have a container name, check directly
223
+ if (!healthStatus.docker && containerName && proc.status === 'online') {
224
+ try {
225
+ const { execSync } = await import('child_process');
226
+ const result = execSync(
227
+ `docker inspect -f '{{.State.Running}}' ${containerName} 2>/dev/null`,
228
+ { encoding: 'utf-8' }
229
+ ).trim();
230
+ healthStatus.docker = result === 'true';
231
+ healthStatus.overall = healthStatus.docker ? 'healthy' : 'degraded';
232
+ } catch (e) {
233
+ // Docker container not running or doesn't exist
234
+ healthStatus.docker = false;
235
+ healthStatus.overall = 'degraded';
236
+ }
237
+ }
238
+
170
239
  // platformPM2 returns simplified format with direct properties
171
240
  return {
172
241
  name: proc.name,
173
- status: proc.status || 'unknown',
242
+ status: proc.status || 'stopped',
174
243
  exists: true,
175
244
  pid: proc.pid,
176
245
  uptime: proc.uptime || 0,
177
246
  memory: proc.memory || 0,
178
247
  cpu: proc.cpu || 0,
179
- restarts: proc.restarts || 0
248
+ restarts: proc.restarts || 0,
249
+ health: healthStatus
180
250
  };
181
251
  }
182
252
 
@@ -194,8 +264,8 @@ export function formatStatus(status) {
194
264
  online: '✓',
195
265
  stopped: '○',
196
266
  errored: '✗',
197
- unknown: '?'
198
- }[status.status] || '?';
267
+ restarting: ''
268
+ }[status.status] || '';
199
269
 
200
270
  const memoryMB = Math.round(status.memory / 1024 / 1024);
201
271
  const uptimeSec = Math.floor((Date.now() - status.uptime) / 1000);
@@ -19,18 +19,18 @@ import { existsSync } from 'fs';
19
19
  export function detectInstanceType(projectPath) {
20
20
  const normalizedPath = path.resolve(projectPath);
21
21
 
22
- // Check if it has BMAD framework (_bmad directory)
23
- const hasBmad = existsSync(path.join(normalizedPath, '_bmad'));
22
+ // Check if it has BMAD plugin (_agent-bridge directory with bot.js)
23
+ const agentBridgePath = path.join(normalizedPath, '_agent-bridge');
24
+ const hasPlugin = existsSync(agentBridgePath) && existsSync(path.join(agentBridgePath, 'src', 'bot.js'));
24
25
 
25
- // Check if it has BMAD plugin (_agent-bridge directory)
26
- const hasPlugin = existsSync(path.join(normalizedPath, '_agent-bridge'));
27
-
28
- // BMAD plugin instance (has either _bmad or _agent-bridge)
29
- if (hasBmad || hasPlugin) {
26
+ // BMAD plugin instance (has _agent-bridge with bot.js)
27
+ if (hasPlugin) {
30
28
  return 'bmad-plugin';
31
29
  }
32
30
 
33
31
  // Check if it has config.json (simple config instance)
32
+ // This takes precedence over _bmad directory, as many BMAD projects
33
+ // are run as simple-config bots
34
34
  const hasConfig = existsSync(path.join(normalizedPath, 'config.json'));
35
35
  if (hasConfig) {
36
36
  return 'simple-config';
@@ -129,6 +129,11 @@ async function start(scriptPath, options = {}) {
129
129
  }
130
130
  }
131
131
 
132
+ // Note: PM2 CLI doesn't support --max-restarts or --min-uptime as direct options
133
+ // These must be set via ecosystem config file or app configuration
134
+ // For now, we skip these to avoid errors
135
+ // Users can configure restart behavior in ecosystem.config.js
136
+
132
137
  if (options.instances) {
133
138
  args.push('-i', String(options.instances));
134
139
  }