agent-window 1.3.3 → 1.3.5

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.3.3",
3
+ "version": "1.3.5",
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",
@@ -16,7 +16,8 @@ import {
16
16
  removeInstance,
17
17
  updateInstance,
18
18
  discoverInstances,
19
- importInstance
19
+ importInstance,
20
+ importFromPath
20
21
  } from '../../core/instance/manager.js';
21
22
  import {
22
23
  getStatus,
@@ -244,7 +245,10 @@ export async function registerInstanceRoutes(fastify) {
244
245
 
245
246
  // Use botName for PM2 lookup, default to bot-{name}
246
247
  const botName = instance.botName || `bot-${name}`;
247
- const status = await getStatus(botName, { instanceType: instance.instanceType });
248
+ const status = await getStatus(botName, {
249
+ instanceType: instance.instanceType,
250
+ configPath: instance.configPath
251
+ });
248
252
  return { ...status, instanceName: name };
249
253
  } catch (error) {
250
254
  reply.code(500).send({
@@ -352,6 +356,54 @@ export async function registerInstanceRoutes(fastify) {
352
356
  }
353
357
  });
354
358
 
359
+ /**
360
+ * POST /api/instances/import-from-path
361
+ * Manually import a BMAD plugin by providing its path
362
+ *
363
+ * This is the second registration method for BMAD plugins:
364
+ * - User manually provides the project path
365
+ * - System validates it's a valid BMAD plugin
366
+ * - If valid, registers it in instances.json
367
+ */
368
+ fastify.post('/api/instances/import-from-path', {
369
+ schema: {
370
+ description: 'Manually import a BMAD plugin by path',
371
+ body: {
372
+ type: 'object',
373
+ required: ['name', 'projectPath'],
374
+ properties: {
375
+ name: { type: 'string' },
376
+ projectPath: { type: 'string' },
377
+ displayName: { type: 'string' },
378
+ tags: { type: 'array', items: { type: 'string' } }
379
+ }
380
+ }
381
+ }
382
+ }, async (request, reply) => {
383
+ try {
384
+ const { name, projectPath, displayName, tags } = request.body;
385
+
386
+ const result = await importFromPath(name, projectPath, {
387
+ displayName,
388
+ tags
389
+ });
390
+
391
+ if (!result.success) {
392
+ return reply.code(400).send({
393
+ error: result.error,
394
+ validation: result.validation
395
+ });
396
+ }
397
+
398
+ reply.code(201).send(result.instance);
399
+ } catch (error) {
400
+ reply.code(500).send({
401
+ error: 'Failed to import from path',
402
+ message: error.message
403
+ });
404
+ }
405
+ });
406
+
355
407
  /**
356
408
  * GET /api/instances/:name/config
357
409
  * Get instance configuration
@@ -246,9 +246,73 @@ export async function removeInstance(name) {
246
246
  * @param {string} name - Instance name
247
247
  * @returns {Promise<Object|null>} Instance or null
248
248
  */
249
+ /**
250
+ * Get a specific instance by name
251
+ * Also includes auto-discovered instances from PM2
252
+ *
253
+ * @param {string} name - Instance name
254
+ * @returns {Promise<Object|null>} Instance or null
255
+ */
249
256
  export async function getInstance(name) {
257
+ // First, try to find in registered instances
250
258
  const data = await readInstances();
251
- return data.instances.find(i => i.name === name) || null;
259
+ const registered = data.instances.find(i => i.name === name);
260
+ if (registered) {
261
+ return registered;
262
+ }
263
+
264
+ // If not found, check if it exists as an auto-discovered instance in PM2
265
+ // This allows status checks for simple-config instances that aren't permanently registered
266
+ try {
267
+ const processes = await getProcesses();
268
+ const botName = `bot-${name}`;
269
+ const proc = processes.find(p => p.name === botName);
270
+
271
+ if (proc) {
272
+ // Determine instance type
273
+ let projectPath = null;
274
+ let configPath = proc.configPath;
275
+ let instanceType = 'simple-config';
276
+
277
+ if (configPath && existsSync(configPath)) {
278
+ try {
279
+ const configContent = await fs.readFile(configPath, 'utf8');
280
+ const config = JSON.parse(configContent);
281
+ projectPath = config.PROJECT_DIR || path.dirname(configPath);
282
+
283
+ // Detect instance type
284
+ if (projectPath && existsSync(projectPath)) {
285
+ instanceType = detectInstanceType(projectPath);
286
+ }
287
+ } catch {
288
+ projectPath = proc.cwd || null;
289
+ }
290
+ }
291
+
292
+ // Only return simple-config instances (BMAD plugins need explicit registration)
293
+ if (instanceType === 'simple-config') {
294
+ return {
295
+ name,
296
+ displayName: name.charAt(0).toUpperCase() + name.slice(1),
297
+ projectPath,
298
+ configPath,
299
+ pluginPath: null,
300
+ botName,
301
+ instanceType,
302
+ platform: 'discord',
303
+ aiProvider: null,
304
+ addedAt: new Date().toISOString(),
305
+ tags: ['auto-discovered'],
306
+ enabled: true,
307
+ _unregistered: true
308
+ };
309
+ }
310
+ }
311
+ } catch (error) {
312
+ console.debug('[Manager] Failed to look up auto-discovered instance:', error.message);
313
+ }
314
+
315
+ return null;
252
316
  }
253
317
 
254
318
  /**
@@ -258,10 +322,95 @@ export async function getInstance(name) {
258
322
  * @param {string} options.tag - Filter by tag
259
323
  * @returns {Promise<Array>} List of instances
260
324
  */
325
+ /**
326
+ * List all instances
327
+ *
328
+ * For BMAD-plugin instances: returns only registered instances from instances.json
329
+ * For simple-config instances: automatically scans PM2 and includes all bot-* processes
330
+ *
331
+ * This ensures simple-config instances (managed by AgentWindow) appear automatically
332
+ * while BMAD-plugin instances require explicit registration.
333
+ *
334
+ * @param {Object} options - Options
335
+ * @param {boolean} options.enabledOnly - Filter to enabled instances only
336
+ * @param {string} options.tag - Filter by tag
337
+ * @param {boolean} options.includeUnregistered - Include PM2 processes not in instances.json
338
+ * @returns {Promise<Array>} List of instances
339
+ */
261
340
  export async function listInstances(options = {}) {
262
341
  const data = await readInstances();
263
- let instances = data.instances;
342
+ const registeredNames = new Set(data.instances.map(i => i.botName || `bot-${i.name}`));
343
+
344
+ // Start with registered instances
345
+ let instances = [...data.instances];
346
+
347
+ // Auto-discover simple-config instances from PM2 (unless explicitly disabled)
348
+ // Simple-config instances should appear automatically as they are managed by AgentWindow
349
+ if (options.includeUnregistered !== false) {
350
+ try {
351
+ const processes = await getProcesses();
352
+ const botProcesses = processes.filter(p => p.name && p.name.startsWith('bot-'));
353
+
354
+ for (const proc of botProcesses) {
355
+ // Skip if already registered
356
+ if (registeredNames.has(proc.name)) {
357
+ continue;
358
+ }
359
+
360
+ // Extract instance name from bot-name
361
+ const instanceName = proc.name.replace(/^bot-/, '');
362
+
363
+ // Determine project path and config path from PM2 process
364
+ let projectPath = null;
365
+ let configPath = null;
366
+ let hasValidConfig = false;
367
+
368
+ // Get config path from PM2 environment
369
+ if (proc.configPath && existsSync(proc.configPath)) {
370
+ configPath = proc.configPath;
371
+ try {
372
+ const configContent = await fs.readFile(proc.configPath, 'utf8');
373
+ const config = JSON.parse(configContent);
374
+ projectPath = config.PROJECT_DIR || path.dirname(configPath);
375
+ hasValidConfig = true;
376
+ } catch {
377
+ projectPath = proc.cwd || null;
378
+ }
379
+ }
380
+
381
+ // Detect instance type - if it has _agent-bridge, it's a BMAD plugin
382
+ // BMAD plugins should NOT be auto-added (require explicit import)
383
+ if (projectPath && existsSync(projectPath)) {
384
+ const detectedType = detectInstanceType(projectPath);
385
+
386
+ // Only auto-add simple-config instances, skip BMAD plugins
387
+ if (detectedType === 'simple-config' && hasValidConfig) {
388
+ instances.push({
389
+ name: instanceName,
390
+ displayName: instanceName.charAt(0).toUpperCase() + instanceName.slice(1),
391
+ projectPath,
392
+ configPath,
393
+ pluginPath: null,
394
+ botName: proc.name,
395
+ instanceType: 'simple-config',
396
+ platform: 'discord',
397
+ aiProvider: null,
398
+ addedAt: new Date().toISOString(),
399
+ tags: ['auto-discovered'],
400
+ enabled: true,
401
+ // Mark as unregistered so frontend knows it's not permanently saved
402
+ _unregistered: true
403
+ });
404
+ }
405
+ }
406
+ }
407
+ } catch (error) {
408
+ // If PM2 scan fails, just return registered instances
409
+ console.debug('[Manager] PM2 scan failed, returning registered instances only:', error.message);
410
+ }
411
+ }
264
412
 
413
+ // Apply filters
265
414
  if (options.enabledOnly) {
266
415
  instances = instances.filter(i => i.enabled);
267
416
  }
@@ -416,16 +565,18 @@ export async function discoverInstances() {
416
565
 
417
566
  // Detect instance type based on project structure
418
567
  // Business Logic:
419
- // - simple-config: Auto-discovered, managed by AgentWindow
420
- // - bmad-plugin: Discovered but needs user confirmation
568
+ // - simple-config: Auto-discovered in listInstances() - NOT in discover
569
+ // - bmad-plugin: Only shown here for user to import
421
570
  let instanceType = 'unknown';
422
571
  if (projectPath && existsSync(projectPath)) {
423
572
  instanceType = detectInstanceType(projectPath);
424
573
  }
425
574
 
426
- // NOTE: We allow both types to be discovered
427
- // - simple-config: Can be imported directly
428
- // - bmad-plugin: Shown in discover list for user to review and import
575
+ // IMPORTANT: Only return bmad-plugin instances in discover
576
+ // simple-config instances are auto-shown in listInstances()
577
+ if (instanceType !== 'bmad-plugin') {
578
+ continue;
579
+ }
429
580
 
430
581
  discovered.push({
431
582
  botName: proc.name,
@@ -434,7 +585,7 @@ export async function discoverInstances() {
434
585
  projectPath,
435
586
  configPath,
436
587
  pluginPath,
437
- instanceType, // 'bmad' | 'simple' | 'unknown'
588
+ instanceType: 'bmad-plugin',
438
589
  status: proc.status || 'unknown',
439
590
  pid: proc.pid,
440
591
  memory: proc.memory || 0,
@@ -451,6 +602,91 @@ export async function discoverInstances() {
451
602
  return discovered;
452
603
  }
453
604
 
605
+ /**
606
+ * Import a BMAD plugin by manually providing its path
607
+ *
608
+ * This is the second registration method for BMAD plugins.
609
+ * Unlike discover+import, this allows users to directly specify a path.
610
+ *
611
+ * @param {string} name - Instance name
612
+ * @param {string} projectPath - Path to the BMAD project
613
+ * @param {Object} options - Additional options
614
+ * @param {string} options.displayName - Display name
615
+ * @param {string[]} options.tags - Tags
616
+ * @returns {Promise<Object>} Result with success and instance
617
+ */
618
+ export async function importFromPath(name, projectPath, options = {}) {
619
+ // Validate name format
620
+ if (!name || !name.match(/^[a-z0-9-]+$/)) {
621
+ return {
622
+ success: false,
623
+ error: '实例名称只能包含小写字母、数字和连字符'
624
+ };
625
+ }
626
+
627
+ // Check if instance already exists
628
+ const data = await readInstances();
629
+ if (data.instances.find(i => i.name === name)) {
630
+ return {
631
+ success: false,
632
+ error: `实例 "${name}" 已存在`
633
+ };
634
+ }
635
+
636
+ // Validate project path exists
637
+ if (!existsSync(projectPath)) {
638
+ return {
639
+ success: false,
640
+ error: '项目路径不存在'
641
+ };
642
+ }
643
+
644
+ // Validate it's a valid BMAD plugin
645
+ const validation = await validateInstance(projectPath);
646
+ if (!validation.valid) {
647
+ return {
648
+ success: false,
649
+ error: '不是有效的 BMAD 插件:' + (validation.error || '验证失败'),
650
+ validation
651
+ };
652
+ }
653
+
654
+ // Only BMAD plugins can be imported via manual path
655
+ if (validation.instanceType !== 'bmad-plugin') {
656
+ return {
657
+ success: false,
658
+ error: '此功能仅支持导入 BMAD 插件。基础版实例请使用 "Discover" 功能。',
659
+ validation
660
+ };
661
+ }
662
+
663
+ // Determine config path for BMAD plugin
664
+ const agentBridgeConfigPath = path.join(validation.projectPath, '_agent-bridge', 'config', 'config.json');
665
+
666
+ const newInstance = {
667
+ name,
668
+ displayName: options.displayName || name,
669
+ projectPath: validation.projectPath,
670
+ pluginPath: validation.pluginPath,
671
+ configPath: agentBridgeConfigPath,
672
+ botName: `bot-${name}`,
673
+ instanceType: 'bmad-plugin',
674
+ platform: 'discord',
675
+ aiProvider: null,
676
+ addedAt: new Date().toISOString(),
677
+ tags: [...(options.tags || []), 'manually-imported'],
678
+ enabled: true
679
+ };
680
+
681
+ data.instances.push(newInstance);
682
+ await writeInstances(data);
683
+
684
+ return {
685
+ success: true,
686
+ instance: newInstance
687
+ };
688
+ }
689
+
454
690
  /**
455
691
  * Import a discovered instance into instances.json
456
692
  * @param {Object} discovered - Discovered instance object
@@ -157,10 +157,11 @@ export async function getLogs(name, options = {}) {
157
157
  * @param {string} name - Process name
158
158
  * @param {Object} options - Options
159
159
  * @param {string} options.instanceType - Instance type for type-aware checks
160
+ * @param {string} options.configPath - Config path for Docker container name
160
161
  * @returns {Promise<Object>} Status info
161
162
  */
162
163
  export async function getStatus(name, options = {}) {
163
- const { instanceType } = options;
164
+ const { instanceType, configPath } = options;
164
165
  const proc = await getProcess(name);
165
166
 
166
167
  if (!proc) {
@@ -189,12 +190,13 @@ export async function getStatus(name, options = {}) {
189
190
  const procStatus = proc.status || 'stopped';
190
191
 
191
192
  // Get container name from config for Docker check
193
+ // Priority: options.configPath > proc.configPath
192
194
  try {
193
195
  const { readFileSync } = await import('fs');
194
- const configPath = proc.configPath;
196
+ const effectiveConfigPath = configPath || proc.configPath;
195
197
 
196
- if (configPath) {
197
- const configContent = JSON.parse(readFileSync(configPath, 'utf-8'));
198
+ if (effectiveConfigPath) {
199
+ const configContent = JSON.parse(readFileSync(effectiveConfigPath, 'utf-8'));
198
200
  containerName = configContent.workspace?.containerName || null;
199
201
  }
200
202
  } catch (e) {
@@ -224,12 +226,14 @@ export async function getStatus(name, options = {}) {
224
226
  if (containerName && procStatus === 'online') {
225
227
  try {
226
228
  const { execSync } = await import('child_process');
229
+ // Use shorter timeout (2s) to avoid blocking status check
227
230
  const result = execSync(
228
231
  `docker inspect -f '{{.State.Running}}' ${containerName} 2>/dev/null`,
229
- { encoding: 'utf-8', timeout: 5000 }
232
+ { encoding: 'utf-8', timeout: 2000 }
230
233
  ).trim();
231
234
  dockerRunning = result === 'true';
232
235
  } catch (e) {
236
+ // Docker inspect failed (container not running or Docker daemon issue)
233
237
  dockerRunning = false;
234
238
  }
235
239
  }