agent-window 1.3.2 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-window",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
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,
@@ -352,6 +353,54 @@ export async function registerInstanceRoutes(fastify) {
352
353
  }
353
354
  });
354
355
 
356
+ /**
357
+ * POST /api/instances/import-from-path
358
+ * Manually import a BMAD plugin by providing its path
359
+ *
360
+ * This is the second registration method for BMAD plugins:
361
+ * - User manually provides the project path
362
+ * - System validates it's a valid BMAD plugin
363
+ * - If valid, registers it in instances.json
364
+ */
365
+ fastify.post('/api/instances/import-from-path', {
366
+ schema: {
367
+ description: 'Manually import a BMAD plugin by path',
368
+ body: {
369
+ type: 'object',
370
+ required: ['name', 'projectPath'],
371
+ properties: {
372
+ name: { type: 'string' },
373
+ projectPath: { type: 'string' },
374
+ displayName: { type: 'string' },
375
+ tags: { type: 'array', items: { type: 'string' } }
376
+ }
377
+ }
378
+ }
379
+ }, async (request, reply) => {
380
+ try {
381
+ const { name, projectPath, displayName, tags } = request.body;
382
+
383
+ const result = await importFromPath(name, projectPath, {
384
+ displayName,
385
+ tags
386
+ });
387
+
388
+ if (!result.success) {
389
+ return reply.code(400).send({
390
+ error: result.error,
391
+ validation: result.validation
392
+ });
393
+ }
394
+
395
+ reply.code(201).send(result.instance);
396
+ } catch (error) {
397
+ reply.code(500).send({
398
+ error: 'Failed to import from path',
399
+ message: error.message
400
+ });
401
+ }
402
+ });
403
+
355
404
  /**
356
405
  * GET /api/instances/:name/config
357
406
  * 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
@@ -100,12 +100,39 @@ function parseEnv(env) {
100
100
  }
101
101
 
102
102
  /**
103
- * Get list of PM2 processes
103
+ * Check if PM2 daemon is running
104
+ */
105
+ async function isDaemonRunning(options = {}) {
106
+ try {
107
+ // Try to get PM2 version - if daemon exists, this will work
108
+ await execPM2Command(['--version'], { ...options, silent: true, timeout: 3000 });
109
+ return true;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Get list of PM2 processes with auto-resurrect on empty list
104
117
  */
105
118
  async function list(options = {}) {
106
119
  try {
107
120
  const result = await execPM2Command(['jlist'], { ...options, silent: true });
108
- const processes = JSON.parse(result.stdout || '[]');
121
+ let processes = JSON.parse(result.stdout || '[]');
122
+
123
+ // If list is empty but daemon is running, try resurrect
124
+ // This handles the case where PM2 daemon is running but dump file is out of sync
125
+ if (processes.length === 0 && await isDaemonRunning(options)) {
126
+ try {
127
+ await execPM2Command(['resurrect'], { ...options, silent: true, timeout: 5000 });
128
+ // Retry getting the list after resurrect
129
+ const retryResult = await execPM2Command(['jlist'], { ...options, silent: true });
130
+ processes = JSON.parse(retryResult.stdout || '[]');
131
+ } catch {
132
+ // Resurrect failed, continue with empty list
133
+ }
134
+ }
135
+
109
136
  return processes.map(p => {
110
137
  const env = parseEnv(p.pm2_env?.env);
111
138
  return {