agentic-factory-bridge 1.0.6 → 1.1.0

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.
Files changed (2) hide show
  1. package/bridge.js +345 -0
  2. package/package.json +1 -1
package/bridge.js CHANGED
@@ -488,6 +488,351 @@ app.delete('/executions', requireBridgeAuth, (_req, res) => {
488
488
  res.json({ message: `Cleared ${count} executions` });
489
489
  });
490
490
 
491
+ // ---------------------------------------------------------------------------
492
+ // Agent Hub — OpenCode Agent Sync
493
+ // ---------------------------------------------------------------------------
494
+
495
+ const AGENTS_DIR = path.join(
496
+ process.env.HOME || process.env.USERPROFILE || require('os').homedir(),
497
+ '.config',
498
+ 'opencode',
499
+ 'agents',
500
+ );
501
+
502
+ /**
503
+ * Validates an agent name to prevent path traversal and injection.
504
+ * Only allows alphanumeric chars, hyphens, and underscores.
505
+ */
506
+ function sanitizeAgentName(name) {
507
+ if (!name || typeof name !== 'string') return null;
508
+ if (name.includes('..') || name.includes('/') || name.includes('\\')) return null;
509
+ const sanitized = name.replace(/[^a-zA-Z0-9_-]/g, '');
510
+ if (!sanitized || sanitized.length === 0 || sanitized.length > 100) return null;
511
+ return sanitized;
512
+ }
513
+
514
+ /**
515
+ * Ensures the agents directory exists.
516
+ */
517
+ function ensureAgentsDir() {
518
+ if (!fs.existsSync(AGENTS_DIR)) {
519
+ fs.mkdirSync(AGENTS_DIR, { recursive: true });
520
+ }
521
+ return AGENTS_DIR;
522
+ }
523
+
524
+ /**
525
+ * Generates an OpenCode agent .md file content from a marketplace manifest.
526
+ */
527
+ function generateAgentMd(manifest) {
528
+ const frontmatter = {
529
+ description: manifest.displayName || manifest.name,
530
+ mode: 'subagent',
531
+ };
532
+
533
+ if (manifest.llmModel) {
534
+ frontmatter.model = manifest.llmModel;
535
+ }
536
+
537
+ frontmatter.tools = {
538
+ read: true,
539
+ edit: true,
540
+ write: true,
541
+ bash: true,
542
+ glob: true,
543
+ grep: true,
544
+ webfetch: true,
545
+ };
546
+
547
+ // Build YAML frontmatter manually (no dependency needed)
548
+ const yamlLines = ['---'];
549
+ for (const [key, value] of Object.entries(frontmatter)) {
550
+ if (value === null || value === undefined) continue;
551
+ if (typeof value === 'object' && !Array.isArray(value)) {
552
+ yamlLines.push(`${key}:`);
553
+ for (const [k, v] of Object.entries(value)) {
554
+ yamlLines.push(` ${k}: ${v}`);
555
+ }
556
+ } else if (typeof value === 'string') {
557
+ // Escape strings that contain special YAML chars
558
+ if (
559
+ value.includes(':') ||
560
+ value.includes('#') ||
561
+ value.includes("'") ||
562
+ value.includes('"')
563
+ ) {
564
+ yamlLines.push(`${key}: "${value.replace(/"/g, '\\"')}"`);
565
+ } else {
566
+ yamlLines.push(`${key}: ${value}`);
567
+ }
568
+ } else {
569
+ yamlLines.push(`${key}: ${value}`);
570
+ }
571
+ }
572
+ yamlLines.push('---');
573
+ yamlLines.push('');
574
+
575
+ // Body = system prompt (agent description)
576
+ const body = manifest.description || `Agent: ${manifest.displayName || manifest.name}`;
577
+
578
+ // Add metadata as HTML comment (for sync tracking)
579
+ const meta = [
580
+ '',
581
+ `<!-- agentic-factory-sync`,
582
+ ` source: ${manifest.sourceUrl || 'unknown'}`,
583
+ ` version: ${manifest.version || '1.0.0'}`,
584
+ ` synced: ${new Date().toISOString()}`,
585
+ ` category: ${manifest.category || 'other'}`,
586
+ ` owner: ${manifest.owner || 'unknown'}`,
587
+ ` tags: ${(manifest.tags || []).join(', ')}`,
588
+ `-->`,
589
+ ];
590
+
591
+ return yamlLines.join('\n') + body + '\n' + meta.join('\n') + '\n';
592
+ }
593
+
594
+ /**
595
+ * Parses metadata from an existing agent .md file.
596
+ */
597
+ function parseAgentMdMeta(filePath) {
598
+ try {
599
+ const content = fs.readFileSync(filePath, 'utf8');
600
+ const meta = { version: 'unknown', source: 'unknown', syncedAt: 'unknown' };
601
+
602
+ const syncMatch = content.match(/<!-- agentic-factory-sync\s*([\s\S]*?)-->/);
603
+ if (syncMatch) {
604
+ const block = syncMatch[1];
605
+ const versionMatch = block.match(/version:\s*(.+)/);
606
+ const sourceMatch = block.match(/source:\s*(.+)/);
607
+ const syncedMatch = block.match(/synced:\s*(.+)/);
608
+ if (versionMatch) meta.version = versionMatch[1].trim();
609
+ if (sourceMatch) meta.source = sourceMatch[1].trim();
610
+ if (syncedMatch) meta.syncedAt = syncedMatch[1].trim();
611
+ }
612
+ return meta;
613
+ } catch {
614
+ return null;
615
+ }
616
+ }
617
+
618
+ /**
619
+ * GET /agents/installed — List all locally installed OpenCode agents.
620
+ */
621
+ app.get('/agents/installed', (_req, res) => {
622
+ try {
623
+ ensureAgentsDir();
624
+ const files = fs.readdirSync(AGENTS_DIR).filter((f) => f.endsWith('.md'));
625
+ const agents = files.map((f) => {
626
+ const name = f.replace(/\.md$/, '');
627
+ const filePath = path.join(AGENTS_DIR, f);
628
+ const meta = parseAgentMdMeta(filePath) || {};
629
+ return {
630
+ name,
631
+ version: meta.version || 'unknown',
632
+ syncedAt: meta.syncedAt || 'unknown',
633
+ source: meta.source || 'unknown',
634
+ };
635
+ });
636
+ res.json({ agents, agentsDir: AGENTS_DIR });
637
+ } catch (err) {
638
+ res.status(500).json({ error: 'Failed to list agents', details: err.message });
639
+ }
640
+ });
641
+
642
+ /**
643
+ * GET /agents/:name/check — Check if a specific agent is installed locally.
644
+ */
645
+ app.get('/agents/:name/check', (req, res) => {
646
+ const name = sanitizeAgentName(req.params.name);
647
+ if (!name) {
648
+ return res.status(400).json({ error: 'Invalid agent name' });
649
+ }
650
+ try {
651
+ ensureAgentsDir();
652
+ const filePath = path.join(AGENTS_DIR, `${name}.md`);
653
+ if (fs.existsSync(filePath)) {
654
+ const meta = parseAgentMdMeta(filePath) || {};
655
+ res.json({ installed: true, version: meta.version, syncedAt: meta.syncedAt });
656
+ } else {
657
+ res.json({ installed: false });
658
+ }
659
+ } catch (err) {
660
+ res.status(500).json({ error: 'Failed to check agent', details: err.message });
661
+ }
662
+ });
663
+
664
+ /**
665
+ * POST /agents/install — Download manifest from marketplace API and install agent locally.
666
+ * Body: { name: string, marketplaceUrl?: string, agentId: string }
667
+ */
668
+ app.post(
669
+ '/agents/install',
670
+ requireBridgeAuth,
671
+ rateLimit('install'),
672
+ express.json(),
673
+ async (req, res) => {
674
+ const { agentId } = req.body;
675
+ let { name } = req.body;
676
+
677
+ if (!agentId) {
678
+ return res.status(400).json({ error: 'agentId is required' });
679
+ }
680
+
681
+ try {
682
+ // Fetch manifest from marketplace API
683
+ const apiUrl =
684
+ req.body.marketplaceUrl ||
685
+ process.env.MARKETPLACE_API_URL ||
686
+ 'https://atos-agentic-factory-qzwe.onrender.com/api';
687
+ const manifestUrl = `${apiUrl}/agents/${encodeURIComponent(agentId)}/opencode-manifest`;
688
+
689
+ const manifest = await new Promise((resolve, reject) => {
690
+ const urlObj = new URL(manifestUrl);
691
+ const client = urlObj.protocol === 'https:' ? https : http;
692
+ const request = client.get(manifestUrl, { timeout: 15000 }, (response) => {
693
+ if (response.statusCode !== 200) {
694
+ reject(new Error(`API returned ${response.statusCode}`));
695
+ return;
696
+ }
697
+ let data = '';
698
+ response.on('data', (chunk) => {
699
+ data += chunk;
700
+ });
701
+ response.on('end', () => {
702
+ try {
703
+ resolve(JSON.parse(data));
704
+ } catch (e) {
705
+ reject(new Error('Invalid JSON response from API'));
706
+ }
707
+ });
708
+ });
709
+ request.on('error', reject);
710
+ request.on('timeout', () => {
711
+ request.destroy();
712
+ reject(new Error('Request timeout'));
713
+ });
714
+ });
715
+
716
+ // Use name from manifest if not provided
717
+ const agentName = sanitizeAgentName(name || manifest.name);
718
+ if (!agentName) {
719
+ return res.status(400).json({ error: 'Invalid agent name' });
720
+ }
721
+
722
+ // Generate .md file content
723
+ const mdContent = generateAgentMd(manifest);
724
+
725
+ // Write to agents directory
726
+ ensureAgentsDir();
727
+ const filePath = path.join(AGENTS_DIR, `${agentName}.md`);
728
+ fs.writeFileSync(filePath, mdContent, 'utf8');
729
+
730
+ console.log(`[Agent Hub] Installed agent: ${agentName} -> ${filePath}`);
731
+ res.json({
732
+ success: true,
733
+ message: `Agent "${agentName}" installed successfully`,
734
+ path: filePath,
735
+ name: agentName,
736
+ version: manifest.version || '1.0.0',
737
+ });
738
+ } catch (err) {
739
+ console.error(`[Agent Hub] Install failed:`, err.message);
740
+ res.status(500).json({ error: 'Failed to install agent', details: err.message });
741
+ }
742
+ },
743
+ );
744
+
745
+ /**
746
+ * DELETE /agents/:name — Remove a locally installed agent.
747
+ */
748
+ app.delete('/agents/:name', requireBridgeAuth, (req, res) => {
749
+ const name = sanitizeAgentName(req.params.name);
750
+ if (!name) {
751
+ return res.status(400).json({ error: 'Invalid agent name' });
752
+ }
753
+ try {
754
+ const filePath = path.join(AGENTS_DIR, `${name}.md`);
755
+ if (!fs.existsSync(filePath)) {
756
+ return res.status(404).json({ error: `Agent "${name}" not found` });
757
+ }
758
+ fs.unlinkSync(filePath);
759
+ console.log(`[Agent Hub] Removed agent: ${name}`);
760
+ res.json({ success: true, message: `Agent "${name}" removed` });
761
+ } catch (err) {
762
+ res.status(500).json({ error: 'Failed to remove agent', details: err.message });
763
+ }
764
+ });
765
+
766
+ /**
767
+ * PUT /agents/:name/update — Re-download and update an existing agent.
768
+ */
769
+ app.put('/agents/:name/update', requireBridgeAuth, async (req, res) => {
770
+ const name = sanitizeAgentName(req.params.name);
771
+ if (!name) {
772
+ return res.status(400).json({ error: 'Invalid agent name' });
773
+ }
774
+
775
+ const filePath = path.join(AGENTS_DIR, `${name}.md`);
776
+ if (!fs.existsSync(filePath)) {
777
+ return res.status(404).json({ error: `Agent "${name}" not found locally` });
778
+ }
779
+
780
+ // Get agentId from existing metadata or request body
781
+ const { agentId } = req.body || {};
782
+ if (!agentId) {
783
+ return res.status(400).json({ error: 'agentId is required for update' });
784
+ }
785
+
786
+ try {
787
+ const apiUrl =
788
+ req.body.marketplaceUrl ||
789
+ process.env.MARKETPLACE_API_URL ||
790
+ 'https://atos-agentic-factory-qzwe.onrender.com/api';
791
+ const manifestUrl = `${apiUrl}/agents/${encodeURIComponent(agentId)}/opencode-manifest`;
792
+
793
+ const manifest = await new Promise((resolve, reject) => {
794
+ const urlObj = new URL(manifestUrl);
795
+ const client = urlObj.protocol === 'https:' ? https : http;
796
+ const request = client.get(manifestUrl, { timeout: 15000 }, (response) => {
797
+ if (response.statusCode !== 200) {
798
+ reject(new Error(`API returned ${response.statusCode}`));
799
+ return;
800
+ }
801
+ let data = '';
802
+ response.on('data', (chunk) => {
803
+ data += chunk;
804
+ });
805
+ response.on('end', () => {
806
+ try {
807
+ resolve(JSON.parse(data));
808
+ } catch (e) {
809
+ reject(new Error('Invalid JSON response from API'));
810
+ }
811
+ });
812
+ });
813
+ request.on('error', reject);
814
+ request.on('timeout', () => {
815
+ request.destroy();
816
+ reject(new Error('Request timeout'));
817
+ });
818
+ });
819
+
820
+ const mdContent = generateAgentMd(manifest);
821
+ fs.writeFileSync(filePath, mdContent, 'utf8');
822
+
823
+ console.log(`[Agent Hub] Updated agent: ${name}`);
824
+ res.json({
825
+ success: true,
826
+ message: `Agent "${name}" updated successfully`,
827
+ path: filePath,
828
+ version: manifest.version || '1.0.0',
829
+ });
830
+ } catch (err) {
831
+ console.error(`[Agent Hub] Update failed:`, err.message);
832
+ res.status(500).json({ error: 'Failed to update agent', details: err.message });
833
+ }
834
+ });
835
+
491
836
  // ---------------------------------------------------------------------------
492
837
  // Smart Detection
493
838
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-factory-bridge",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "description": "Local bridge for Atos Agentic Factory — connects the marketplace to OpenCode CLI on your machine",
5
5
  "main": "bridge.js",
6
6
  "bin": {