create-byan-agent 2.8.1 → 2.9.1
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/lib/exchange/agent-packager.js +464 -0
- package/lib/exchange/conversation-exporter.js +171 -0
- package/lib/exchange/gist-client.js +257 -0
- package/package.json +1 -1
- package/src/webui/api.js +536 -1
- package/src/webui/chat/bridge.js +92 -0
- package/src/webui/chat/claude-adapter.js +174 -0
- package/src/webui/chat/cli-detector.js +158 -0
- package/src/webui/chat/codex-adapter.js +57 -0
- package/src/webui/chat/copilot-adapter.js +67 -0
- package/src/webui/chat/session-manager.js +185 -0
- package/src/webui/public/chat.css +1665 -0
- package/src/webui/public/chat.html +207 -0
- package/src/webui/public/chat.js +1560 -0
- package/src/webui/server.js +123 -2
package/src/webui/api.js
CHANGED
|
@@ -7,15 +7,46 @@ let yanstaller = null;
|
|
|
7
7
|
let detector = null;
|
|
8
8
|
let backuper = null;
|
|
9
9
|
let sttEngine = null;
|
|
10
|
+
let AgentPackager = null;
|
|
11
|
+
let ConversationExporter = null;
|
|
12
|
+
let GistClient = null;
|
|
10
13
|
|
|
11
14
|
try { yanstaller = require('../../lib/yanstaller'); } catch { yanstaller = null; }
|
|
12
15
|
try { detector = require('../../lib/yanstaller/detector'); } catch { detector = null; }
|
|
13
16
|
try { backuper = require('../../lib/yanstaller/backuper'); } catch { backuper = null; }
|
|
14
17
|
try { sttEngine = require('../../lib/stt/engine'); } catch { sttEngine = null; }
|
|
18
|
+
try { AgentPackager = require('../../lib/exchange/agent-packager'); } catch { AgentPackager = null; }
|
|
19
|
+
try { ConversationExporter = require('../../lib/exchange/conversation-exporter'); } catch { ConversationExporter = null; }
|
|
20
|
+
try { GistClient = require('../../lib/exchange/gist-client'); } catch { GistClient = null; }
|
|
15
21
|
|
|
16
22
|
const fs = require('fs');
|
|
17
23
|
const path = require('path');
|
|
18
24
|
|
|
25
|
+
const { createBridge } = require('./chat/bridge');
|
|
26
|
+
const { detectCLIs, detectAgents } = require('./chat/cli-detector');
|
|
27
|
+
const SessionManager = require('./chat/session-manager');
|
|
28
|
+
|
|
29
|
+
const activeBridges = new Map();
|
|
30
|
+
let sessionManagerInstance = null;
|
|
31
|
+
|
|
32
|
+
function getSessionManager(server) {
|
|
33
|
+
if (!sessionManagerInstance) {
|
|
34
|
+
sessionManagerInstance = new SessionManager(server.projectRoot);
|
|
35
|
+
}
|
|
36
|
+
return sessionManagerInstance;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function sendToSessionClients(server, sessionId, data) {
|
|
40
|
+
const payload = JSON.stringify(data);
|
|
41
|
+
for (const client of server.clients) {
|
|
42
|
+
if (client.readyState === 1) {
|
|
43
|
+
if (!client._chatSessionId || client._chatSessionId === sessionId) {
|
|
44
|
+
client.send(payload);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
19
50
|
function json(res, statusCode, data) {
|
|
20
51
|
res.writeHead(statusCode);
|
|
21
52
|
res.end(JSON.stringify(data));
|
|
@@ -242,12 +273,408 @@ const routes = {
|
|
|
242
273
|
} catch (err) {
|
|
243
274
|
json(res, 500, { error: err.message });
|
|
244
275
|
}
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
// --- Chat routes ---
|
|
279
|
+
|
|
280
|
+
'POST chat/start': async (req, res, server) => {
|
|
281
|
+
const { cli, agent, model } = req.body || {};
|
|
282
|
+
const sm = getSessionManager(server);
|
|
283
|
+
|
|
284
|
+
const cliName = cli || 'claude';
|
|
285
|
+
const session = sm.create(cliName, agent || null);
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const bridge = createBridge(cliName, {
|
|
289
|
+
projectRoot: server.projectRoot,
|
|
290
|
+
agent: agent || null,
|
|
291
|
+
model: model || null,
|
|
292
|
+
onChunk: (chunk) => {
|
|
293
|
+
sendToSessionClients(server, session.id, {
|
|
294
|
+
type: 'chat',
|
|
295
|
+
sessionId: session.id,
|
|
296
|
+
chunk,
|
|
297
|
+
role: 'assistant',
|
|
298
|
+
});
|
|
299
|
+
},
|
|
300
|
+
onToolUse: (tool) => {
|
|
301
|
+
sendToSessionClients(server, session.id, {
|
|
302
|
+
type: 'chat-tool',
|
|
303
|
+
sessionId: session.id,
|
|
304
|
+
tool,
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
onComplete: (result) => {
|
|
308
|
+
sendToSessionClients(server, session.id, {
|
|
309
|
+
type: 'chat-complete',
|
|
310
|
+
sessionId: session.id,
|
|
311
|
+
result,
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
onError: (err) => {
|
|
315
|
+
sendToSessionClients(server, session.id, {
|
|
316
|
+
type: 'chat-error',
|
|
317
|
+
sessionId: session.id,
|
|
318
|
+
error: err.message,
|
|
319
|
+
});
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
await bridge.start();
|
|
324
|
+
activeBridges.set(session.id, bridge);
|
|
325
|
+
|
|
326
|
+
json(res, 200, { sessionId: session.id, cli: cliName });
|
|
327
|
+
} catch (err) {
|
|
328
|
+
sm.delete(session.id);
|
|
329
|
+
json(res, 500, { error: err.message });
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
'POST chat/send': async (req, res, server) => {
|
|
334
|
+
const { sessionId, message } = req.body || {};
|
|
335
|
+
|
|
336
|
+
if (!sessionId || !message) {
|
|
337
|
+
json(res, 400, { error: 'sessionId and message are required' });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const bridge = activeBridges.get(sessionId);
|
|
342
|
+
if (!bridge) {
|
|
343
|
+
json(res, 404, { error: 'No active bridge for session' });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const sm = getSessionManager(server);
|
|
348
|
+
sm.addMessage(sessionId, 'user', message);
|
|
349
|
+
|
|
350
|
+
json(res, 200, { status: 'streaming' });
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
await bridge.send(message);
|
|
354
|
+
} catch (err) {
|
|
355
|
+
sendToSessionClients(server, sessionId, {
|
|
356
|
+
type: 'chat-error',
|
|
357
|
+
sessionId,
|
|
358
|
+
error: err.message,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
'POST chat/stop': async (req, res) => {
|
|
364
|
+
const { sessionId } = req.body || {};
|
|
365
|
+
|
|
366
|
+
if (!sessionId) {
|
|
367
|
+
json(res, 400, { error: 'sessionId is required' });
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const bridge = activeBridges.get(sessionId);
|
|
372
|
+
if (bridge) {
|
|
373
|
+
await bridge.stop();
|
|
374
|
+
activeBridges.delete(sessionId);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
json(res, 200, { status: 'stopped' });
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
'GET chat/sessions': async (req, res, server) => {
|
|
381
|
+
const sm = getSessionManager(server);
|
|
382
|
+
json(res, 200, { sessions: sm.list() });
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
'GET chat/session/:id': async (req, res, server) => {
|
|
386
|
+
const sm = getSessionManager(server);
|
|
387
|
+
const session = sm.load(req.params.id);
|
|
388
|
+
if (!session) {
|
|
389
|
+
json(res, 404, { error: 'Session not found' });
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
json(res, 200, { session });
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
'DELETE chat/session/:id': async (req, res, server) => {
|
|
396
|
+
const sm = getSessionManager(server);
|
|
397
|
+
const bridge = activeBridges.get(req.params.id);
|
|
398
|
+
if (bridge) {
|
|
399
|
+
await bridge.stop();
|
|
400
|
+
activeBridges.delete(req.params.id);
|
|
401
|
+
}
|
|
402
|
+
sm.delete(req.params.id);
|
|
403
|
+
json(res, 200, { status: 'deleted' });
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
// --- Agent routes ---
|
|
407
|
+
|
|
408
|
+
'GET agents': async (req, res, server) => {
|
|
409
|
+
const agents = await detectAgents(server.projectRoot);
|
|
410
|
+
json(res, 200, { agents });
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
'GET agents/:name': async (req, res, server) => {
|
|
414
|
+
const agents = await detectAgents(server.projectRoot);
|
|
415
|
+
const agent = agents.find((a) => a.id === req.params.name || a.name === req.params.name);
|
|
416
|
+
if (!agent) {
|
|
417
|
+
json(res, 404, { error: 'Agent not found' });
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
json(res, 200, { agent });
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
// --- CLI detection ---
|
|
424
|
+
|
|
425
|
+
'GET cli/detect': async (req, res) => {
|
|
426
|
+
const clis = await detectCLIs();
|
|
427
|
+
json(res, 200, { clis });
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
// --- Agent Exchange routes ---
|
|
431
|
+
|
|
432
|
+
'GET agents/exportable': async (req, res, server) => {
|
|
433
|
+
if (!AgentPackager) {
|
|
434
|
+
json(res, 503, { error: 'Exchange module not available' });
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
const packager = new AgentPackager(server.projectRoot);
|
|
439
|
+
const agents = await packager.listExportableAgents();
|
|
440
|
+
json(res, 200, { agents });
|
|
441
|
+
} catch (err) {
|
|
442
|
+
json(res, 500, { error: err.message });
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
'POST agents/export': async (req, res, server) => {
|
|
447
|
+
if (!AgentPackager) {
|
|
448
|
+
json(res, 503, { error: 'Exchange module not available' });
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const { agentName, compress, author } = req.body || {};
|
|
452
|
+
if (!agentName) {
|
|
453
|
+
json(res, 400, { error: 'Missing agentName' });
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
const packager = new AgentPackager(server.projectRoot);
|
|
458
|
+
const buffer = await packager.exportAgent(agentName, { compress, author });
|
|
459
|
+
const ext = compress ? '.byan-agent.gz' : '.byan-agent';
|
|
460
|
+
const filename = `${sanitizeForFilename(agentName)}${ext}`;
|
|
461
|
+
const contentType = compress ? 'application/gzip' : 'application/json';
|
|
462
|
+
res.writeHead(200, {
|
|
463
|
+
'Content-Type': contentType,
|
|
464
|
+
'Content-Disposition': `attachment; filename="${filename}"`,
|
|
465
|
+
'Content-Length': buffer.length
|
|
466
|
+
});
|
|
467
|
+
res.end(buffer);
|
|
468
|
+
} catch (err) {
|
|
469
|
+
json(res, 500, { error: err.message });
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
'POST agents/import': async (req, res, server) => {
|
|
474
|
+
if (!AgentPackager) {
|
|
475
|
+
json(res, 503, { error: 'Exchange module not available' });
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
const upload = await readUploadedFile(req);
|
|
480
|
+
const packager = new AgentPackager(server.projectRoot);
|
|
481
|
+
const opts = {
|
|
482
|
+
targetModule: upload.fields.targetModule || null,
|
|
483
|
+
overwrite: upload.fields.overwrite === 'true' || upload.fields.overwrite === true
|
|
484
|
+
};
|
|
485
|
+
const result = await packager.importAgent(upload.buffer, opts);
|
|
486
|
+
json(res, 200, result);
|
|
487
|
+
} catch (err) {
|
|
488
|
+
json(res, 500, { error: err.message });
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
'POST agents/import-url': async (req, res, server) => {
|
|
493
|
+
if (!AgentPackager || !GistClient) {
|
|
494
|
+
json(res, 503, { error: 'Exchange module not available' });
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const { url } = req.body || {};
|
|
498
|
+
if (!url) {
|
|
499
|
+
json(res, 400, { error: 'Missing url' });
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
const gist = new GistClient();
|
|
504
|
+
const pkg = url.includes('gist.github.com')
|
|
505
|
+
? await gist.importFromGist(url)
|
|
506
|
+
: await gist.importFromURL(url);
|
|
507
|
+
const packager = new AgentPackager(server.projectRoot);
|
|
508
|
+
const buffer = Buffer.from(JSON.stringify(pkg), 'utf8');
|
|
509
|
+
const result = await packager.importAgent(buffer);
|
|
510
|
+
json(res, 200, result);
|
|
511
|
+
} catch (err) {
|
|
512
|
+
json(res, 500, { error: err.message });
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
|
|
516
|
+
'POST agents/validate': async (req, res, server) => {
|
|
517
|
+
if (!AgentPackager) {
|
|
518
|
+
json(res, 503, { error: 'Exchange module not available' });
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
const upload = await readUploadedFile(req);
|
|
523
|
+
const packager = new AgentPackager(server.projectRoot);
|
|
524
|
+
const result = await packager.validate(upload.buffer);
|
|
525
|
+
json(res, 200, result);
|
|
526
|
+
} catch (err) {
|
|
527
|
+
json(res, 500, { error: err.message });
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
|
|
531
|
+
// --- Conversation Exchange routes ---
|
|
532
|
+
|
|
533
|
+
'POST chat/export': async (req, res, server) => {
|
|
534
|
+
if (!ConversationExporter) {
|
|
535
|
+
json(res, 503, { error: 'Exchange module not available' });
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const { session, format } = req.body || {};
|
|
539
|
+
if (!session) {
|
|
540
|
+
json(res, 400, { error: 'Missing session data' });
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
const exporter = new ConversationExporter();
|
|
545
|
+
let content, filename, contentType;
|
|
546
|
+
switch (format) {
|
|
547
|
+
case 'markdown':
|
|
548
|
+
content = exporter.exportMarkdown(session);
|
|
549
|
+
filename = `chat-${sanitizeForFilename(session.agent || 'byan')}-${Date.now()}.md`;
|
|
550
|
+
contentType = 'text/markdown; charset=utf-8';
|
|
551
|
+
break;
|
|
552
|
+
case 'template':
|
|
553
|
+
content = exporter.exportTemplate(session);
|
|
554
|
+
filename = `template-${sanitizeForFilename(session.agent || 'byan')}-${Date.now()}.byan-template`;
|
|
555
|
+
contentType = 'application/json; charset=utf-8';
|
|
556
|
+
break;
|
|
557
|
+
default:
|
|
558
|
+
content = exporter.exportJSON(session);
|
|
559
|
+
filename = `chat-${sanitizeForFilename(session.agent || 'byan')}-${Date.now()}.byan-chat`;
|
|
560
|
+
contentType = 'application/json; charset=utf-8';
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
const buf = Buffer.from(content, 'utf8');
|
|
564
|
+
res.writeHead(200, {
|
|
565
|
+
'Content-Type': contentType,
|
|
566
|
+
'Content-Disposition': `attachment; filename="${filename}"`,
|
|
567
|
+
'Content-Length': buf.length
|
|
568
|
+
});
|
|
569
|
+
res.end(buf);
|
|
570
|
+
} catch (err) {
|
|
571
|
+
json(res, 500, { error: err.message });
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
'POST chat/import': async (req, res) => {
|
|
576
|
+
if (!ConversationExporter) {
|
|
577
|
+
json(res, 503, { error: 'Exchange module not available' });
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
const upload = await readUploadedFile(req);
|
|
582
|
+
const exporter = new ConversationExporter();
|
|
583
|
+
const content = upload.buffer.toString('utf8');
|
|
584
|
+
let result;
|
|
585
|
+
if (upload.filename && upload.filename.includes('.byan-template')) {
|
|
586
|
+
const template = exporter.importTemplate(content);
|
|
587
|
+
result = { success: true, type: 'template', data: template };
|
|
588
|
+
} else {
|
|
589
|
+
const session = exporter.importJSON(content);
|
|
590
|
+
result = { success: true, type: 'chat', sessionId: session.id, data: session };
|
|
591
|
+
}
|
|
592
|
+
json(res, 200, result);
|
|
593
|
+
} catch (err) {
|
|
594
|
+
json(res, 500, { error: err.message });
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
|
|
598
|
+
// --- Gist routes ---
|
|
599
|
+
|
|
600
|
+
'POST gist/export': async (req, res, server) => {
|
|
601
|
+
if (!AgentPackager || !GistClient) {
|
|
602
|
+
json(res, 503, { error: 'Exchange module not available' });
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const { agentName, author } = req.body || {};
|
|
606
|
+
if (!agentName) {
|
|
607
|
+
json(res, 400, { error: 'Missing agentName' });
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
try {
|
|
611
|
+
const packager = new AgentPackager(server.projectRoot);
|
|
612
|
+
const buffer = await packager.exportAgent(agentName, { author });
|
|
613
|
+
const pkg = JSON.parse(buffer.toString('utf8'));
|
|
614
|
+
const gist = new GistClient();
|
|
615
|
+
const gistUrl = await gist.exportToGist(pkg);
|
|
616
|
+
json(res, 200, { success: true, gistUrl });
|
|
617
|
+
} catch (err) {
|
|
618
|
+
json(res, 500, { error: err.message });
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
|
|
622
|
+
'POST gist/import': async (req, res, server) => {
|
|
623
|
+
if (!AgentPackager || !GistClient) {
|
|
624
|
+
json(res, 503, { error: 'Exchange module not available' });
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const { url } = req.body || {};
|
|
628
|
+
if (!url) {
|
|
629
|
+
json(res, 400, { error: 'Missing url' });
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
try {
|
|
633
|
+
const gist = new GistClient();
|
|
634
|
+
const pkg = await gist.importFromGist(url);
|
|
635
|
+
const packager = new AgentPackager(server.projectRoot);
|
|
636
|
+
const buffer = Buffer.from(JSON.stringify(pkg), 'utf8');
|
|
637
|
+
const result = await packager.importAgent(buffer);
|
|
638
|
+
json(res, 200, result);
|
|
639
|
+
} catch (err) {
|
|
640
|
+
json(res, 500, { error: err.message });
|
|
641
|
+
}
|
|
245
642
|
}
|
|
246
643
|
};
|
|
247
644
|
|
|
248
645
|
function resolve(method, route) {
|
|
249
646
|
const key = `${method} ${route}`;
|
|
250
|
-
|
|
647
|
+
if (routes[key]) return routes[key];
|
|
648
|
+
|
|
649
|
+
for (const pattern of Object.keys(routes)) {
|
|
650
|
+
const [pMethod, pRoute] = pattern.split(' ', 2);
|
|
651
|
+
if (pMethod !== method) continue;
|
|
652
|
+
if (!pRoute || !pRoute.includes(':')) continue;
|
|
653
|
+
|
|
654
|
+
const pParts = pRoute.split('/');
|
|
655
|
+
const rParts = route.split('/');
|
|
656
|
+
if (pParts.length !== rParts.length) continue;
|
|
657
|
+
|
|
658
|
+
const params = {};
|
|
659
|
+
let match = true;
|
|
660
|
+
for (let i = 0; i < pParts.length; i++) {
|
|
661
|
+
if (pParts[i].startsWith(':')) {
|
|
662
|
+
params[pParts[i].slice(1)] = rParts[i];
|
|
663
|
+
} else if (pParts[i] !== rParts[i]) {
|
|
664
|
+
match = false;
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (match) {
|
|
670
|
+
return (req, res, server) => {
|
|
671
|
+
req.params = params;
|
|
672
|
+
return routes[pattern](req, res, server);
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return null;
|
|
251
678
|
}
|
|
252
679
|
|
|
253
680
|
function sleep(ms) {
|
|
@@ -290,4 +717,112 @@ function writeBaseConfig(projectRoot, config) {
|
|
|
290
717
|
fs.writeFileSync(configPath, content, 'utf8');
|
|
291
718
|
}
|
|
292
719
|
|
|
720
|
+
function sanitizeForFilename(name) {
|
|
721
|
+
return String(name).replace(/[^a-zA-Z0-9_\-]/g, '').substring(0, 100) || 'file';
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function readUploadedFile(req) {
|
|
725
|
+
return new Promise((resolve, reject) => {
|
|
726
|
+
const contentType = (req.headers && req.headers['content-type']) || '';
|
|
727
|
+
|
|
728
|
+
if (req.body && req.body._fileBuffer) {
|
|
729
|
+
resolve({
|
|
730
|
+
buffer: Buffer.from(req.body._fileBuffer, 'base64'),
|
|
731
|
+
filename: sanitizeUploadFilename(req.body._fileName || ''),
|
|
732
|
+
fields: req.body
|
|
733
|
+
});
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const chunks = [];
|
|
738
|
+
req.removeAllListeners('data');
|
|
739
|
+
req.removeAllListeners('end');
|
|
740
|
+
req.on('data', chunk => chunks.push(chunk));
|
|
741
|
+
req.on('error', reject);
|
|
742
|
+
req.on('end', () => {
|
|
743
|
+
const raw = Buffer.concat(chunks);
|
|
744
|
+
try {
|
|
745
|
+
resolve(parseUploadBody(raw, contentType));
|
|
746
|
+
} catch (err) {
|
|
747
|
+
reject(err);
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
if (req._body !== undefined) {
|
|
752
|
+
const raw = Buffer.from(typeof req._body === 'string' ? req._body : JSON.stringify(req.body || {}), 'utf8');
|
|
753
|
+
try {
|
|
754
|
+
resolve(parseUploadBody(raw, contentType));
|
|
755
|
+
} catch (err) {
|
|
756
|
+
reject(err);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function parseUploadBody(raw, contentType) {
|
|
763
|
+
if (contentType.includes('multipart/form-data')) {
|
|
764
|
+
const boundaryMatch = contentType.match(/boundary=([^\s;]+)/);
|
|
765
|
+
if (!boundaryMatch) {
|
|
766
|
+
throw new Error('Missing multipart boundary');
|
|
767
|
+
}
|
|
768
|
+
return extractMultipartFile(raw, boundaryMatch[1]);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (contentType.includes('application/json')) {
|
|
772
|
+
try {
|
|
773
|
+
const parsed = JSON.parse(raw.toString('utf8'));
|
|
774
|
+
if (parsed._fileBuffer) {
|
|
775
|
+
return {
|
|
776
|
+
buffer: Buffer.from(parsed._fileBuffer, 'base64'),
|
|
777
|
+
filename: sanitizeUploadFilename(parsed._fileName || ''),
|
|
778
|
+
fields: parsed
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
} catch { /* not JSON with fileBuffer, treat as raw */ }
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return { buffer: raw, filename: '', fields: {} };
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function extractMultipartFile(raw, boundary) {
|
|
788
|
+
const rawStr = raw.toString('binary');
|
|
789
|
+
const parts = rawStr.split('--' + boundary);
|
|
790
|
+
const fields = {};
|
|
791
|
+
let fileResult = null;
|
|
792
|
+
|
|
793
|
+
for (const part of parts) {
|
|
794
|
+
if (part.trim() === '' || part.trim() === '--') continue;
|
|
795
|
+
|
|
796
|
+
const headerEnd = part.indexOf('\r\n\r\n');
|
|
797
|
+
if (headerEnd === -1) continue;
|
|
798
|
+
|
|
799
|
+
const headers = part.substring(0, headerEnd);
|
|
800
|
+
const body = part.substring(headerEnd + 4).replace(/\r\n$/, '');
|
|
801
|
+
|
|
802
|
+
const nameMatch = headers.match(/name="([^"]+)"/);
|
|
803
|
+
if (!nameMatch) continue;
|
|
804
|
+
const fieldName = nameMatch[1];
|
|
805
|
+
|
|
806
|
+
if (headers.includes('filename=')) {
|
|
807
|
+
const filenameMatch = headers.match(/filename="([^"]+)"/);
|
|
808
|
+
fileResult = {
|
|
809
|
+
buffer: Buffer.from(body, 'binary'),
|
|
810
|
+
filename: sanitizeUploadFilename(filenameMatch ? filenameMatch[1] : '')
|
|
811
|
+
};
|
|
812
|
+
} else {
|
|
813
|
+
fields[fieldName] = body;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (!fileResult) {
|
|
818
|
+
throw new Error('No file found in multipart upload');
|
|
819
|
+
}
|
|
820
|
+
fileResult.fields = fields;
|
|
821
|
+
return fileResult;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function sanitizeUploadFilename(name) {
|
|
825
|
+
return String(name).replace(/[^a-zA-Z0-9_.\-]/g, '').substring(0, 200);
|
|
826
|
+
}
|
|
827
|
+
|
|
293
828
|
module.exports = { resolve, routes };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Bridge interface for CLI adapters.
|
|
3
|
+
* Each CLI adapter extends Bridge and implements: start(), send(), stop()
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
class Bridge {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.projectRoot = options.projectRoot || process.cwd();
|
|
12
|
+
this.agent = options.agent || null;
|
|
13
|
+
this.model = options.model || null;
|
|
14
|
+
this.onChunk = options.onChunk || (() => {});
|
|
15
|
+
this.onToolUse = options.onToolUse || (() => {});
|
|
16
|
+
this.onComplete = options.onComplete || (() => {});
|
|
17
|
+
this.onError = options.onError || (() => {});
|
|
18
|
+
this.process = null;
|
|
19
|
+
this.active = false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async start() { throw new Error('Not implemented'); }
|
|
23
|
+
async send(message) { throw new Error('Not implemented'); }
|
|
24
|
+
async stop() { throw new Error('Not implemented'); }
|
|
25
|
+
|
|
26
|
+
resolveAgent(agentName) {
|
|
27
|
+
if (!agentName) return null;
|
|
28
|
+
|
|
29
|
+
const candidates = [
|
|
30
|
+
path.join(this.projectRoot, '.github', 'agents', `bmad-agent-${agentName}.md`),
|
|
31
|
+
path.join(this.projectRoot, '_byan', 'agents', `${agentName}.md`),
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const bmadModules = ['core', 'bmm', 'bmb', 'tea', 'cis'];
|
|
35
|
+
for (const mod of bmadModules) {
|
|
36
|
+
candidates.push(
|
|
37
|
+
path.join(this.projectRoot, '_bmad', mod, 'agents', `${agentName}.md`)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const candidate of candidates) {
|
|
42
|
+
try {
|
|
43
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
44
|
+
} catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_killProcess(proc, timeoutMs = 5000) {
|
|
53
|
+
if (!proc || proc.exitCode !== null) return Promise.resolve();
|
|
54
|
+
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
const timer = setTimeout(() => {
|
|
57
|
+
try { proc.kill('SIGKILL'); } catch { /* already dead */ }
|
|
58
|
+
resolve();
|
|
59
|
+
}, timeoutMs);
|
|
60
|
+
|
|
61
|
+
proc.once('exit', () => {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
resolve();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
try { proc.kill('SIGTERM'); } catch { clearTimeout(timer); resolve(); }
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createBridge(cliName, options) {
|
|
72
|
+
const name = (cliName || '').toLowerCase().trim();
|
|
73
|
+
|
|
74
|
+
switch (name) {
|
|
75
|
+
case 'claude': {
|
|
76
|
+
const ClaudeAdapter = require('./claude-adapter');
|
|
77
|
+
return new ClaudeAdapter(options);
|
|
78
|
+
}
|
|
79
|
+
case 'copilot': {
|
|
80
|
+
const CopilotAdapter = require('./copilot-adapter');
|
|
81
|
+
return new CopilotAdapter(options);
|
|
82
|
+
}
|
|
83
|
+
case 'codex': {
|
|
84
|
+
const CodexAdapter = require('./codex-adapter');
|
|
85
|
+
return new CodexAdapter(options);
|
|
86
|
+
}
|
|
87
|
+
default:
|
|
88
|
+
throw new Error(`Unknown CLI adapter: ${cliName}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { Bridge, createBridge };
|