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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
420
|
-
// - bmad-plugin:
|
|
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
|
-
//
|
|
427
|
-
//
|
|
428
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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 {
|