agentic-factory-bridge 1.0.6 → 1.2.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 +489 -0
  2. package/package.json +1 -1
package/bridge.js CHANGED
@@ -57,6 +57,7 @@ const RATE_LIMITS = {
57
57
  execute: { windowMs: 60000, max: 10 },
58
58
  install: { windowMs: 3600000, max: 2 },
59
59
  register: { windowMs: 3600000, max: 3 },
60
+ update: { windowMs: 3600000, max: 3 },
60
61
  };
61
62
 
62
63
  // In-memory store limits
@@ -488,6 +489,351 @@ app.delete('/executions', requireBridgeAuth, (_req, res) => {
488
489
  res.json({ message: `Cleared ${count} executions` });
489
490
  });
490
491
 
492
+ // ---------------------------------------------------------------------------
493
+ // Agent Hub — OpenCode Agent Sync
494
+ // ---------------------------------------------------------------------------
495
+
496
+ const AGENTS_DIR = path.join(
497
+ process.env.HOME || process.env.USERPROFILE || require('os').homedir(),
498
+ '.config',
499
+ 'opencode',
500
+ 'agents',
501
+ );
502
+
503
+ /**
504
+ * Validates an agent name to prevent path traversal and injection.
505
+ * Only allows alphanumeric chars, hyphens, and underscores.
506
+ */
507
+ function sanitizeAgentName(name) {
508
+ if (!name || typeof name !== 'string') return null;
509
+ if (name.includes('..') || name.includes('/') || name.includes('\\')) return null;
510
+ const sanitized = name.replace(/[^a-zA-Z0-9_-]/g, '');
511
+ if (!sanitized || sanitized.length === 0 || sanitized.length > 100) return null;
512
+ return sanitized;
513
+ }
514
+
515
+ /**
516
+ * Ensures the agents directory exists.
517
+ */
518
+ function ensureAgentsDir() {
519
+ if (!fs.existsSync(AGENTS_DIR)) {
520
+ fs.mkdirSync(AGENTS_DIR, { recursive: true });
521
+ }
522
+ return AGENTS_DIR;
523
+ }
524
+
525
+ /**
526
+ * Generates an OpenCode agent .md file content from a marketplace manifest.
527
+ */
528
+ function generateAgentMd(manifest) {
529
+ const frontmatter = {
530
+ description: manifest.displayName || manifest.name,
531
+ mode: 'subagent',
532
+ };
533
+
534
+ if (manifest.llmModel) {
535
+ frontmatter.model = manifest.llmModel;
536
+ }
537
+
538
+ frontmatter.tools = {
539
+ read: true,
540
+ edit: true,
541
+ write: true,
542
+ bash: true,
543
+ glob: true,
544
+ grep: true,
545
+ webfetch: true,
546
+ };
547
+
548
+ // Build YAML frontmatter manually (no dependency needed)
549
+ const yamlLines = ['---'];
550
+ for (const [key, value] of Object.entries(frontmatter)) {
551
+ if (value === null || value === undefined) continue;
552
+ if (typeof value === 'object' && !Array.isArray(value)) {
553
+ yamlLines.push(`${key}:`);
554
+ for (const [k, v] of Object.entries(value)) {
555
+ yamlLines.push(` ${k}: ${v}`);
556
+ }
557
+ } else if (typeof value === 'string') {
558
+ // Escape strings that contain special YAML chars
559
+ if (
560
+ value.includes(':') ||
561
+ value.includes('#') ||
562
+ value.includes("'") ||
563
+ value.includes('"')
564
+ ) {
565
+ yamlLines.push(`${key}: "${value.replace(/"/g, '\\"')}"`);
566
+ } else {
567
+ yamlLines.push(`${key}: ${value}`);
568
+ }
569
+ } else {
570
+ yamlLines.push(`${key}: ${value}`);
571
+ }
572
+ }
573
+ yamlLines.push('---');
574
+ yamlLines.push('');
575
+
576
+ // Body = system prompt (agent description)
577
+ const body = manifest.description || `Agent: ${manifest.displayName || manifest.name}`;
578
+
579
+ // Add metadata as HTML comment (for sync tracking)
580
+ const meta = [
581
+ '',
582
+ `<!-- agentic-factory-sync`,
583
+ ` source: ${manifest.sourceUrl || 'unknown'}`,
584
+ ` version: ${manifest.version || '1.0.0'}`,
585
+ ` synced: ${new Date().toISOString()}`,
586
+ ` category: ${manifest.category || 'other'}`,
587
+ ` owner: ${manifest.owner || 'unknown'}`,
588
+ ` tags: ${(manifest.tags || []).join(', ')}`,
589
+ `-->`,
590
+ ];
591
+
592
+ return yamlLines.join('\n') + body + '\n' + meta.join('\n') + '\n';
593
+ }
594
+
595
+ /**
596
+ * Parses metadata from an existing agent .md file.
597
+ */
598
+ function parseAgentMdMeta(filePath) {
599
+ try {
600
+ const content = fs.readFileSync(filePath, 'utf8');
601
+ const meta = { version: 'unknown', source: 'unknown', syncedAt: 'unknown' };
602
+
603
+ const syncMatch = content.match(/<!-- agentic-factory-sync\s*([\s\S]*?)-->/);
604
+ if (syncMatch) {
605
+ const block = syncMatch[1];
606
+ const versionMatch = block.match(/version:\s*(.+)/);
607
+ const sourceMatch = block.match(/source:\s*(.+)/);
608
+ const syncedMatch = block.match(/synced:\s*(.+)/);
609
+ if (versionMatch) meta.version = versionMatch[1].trim();
610
+ if (sourceMatch) meta.source = sourceMatch[1].trim();
611
+ if (syncedMatch) meta.syncedAt = syncedMatch[1].trim();
612
+ }
613
+ return meta;
614
+ } catch {
615
+ return null;
616
+ }
617
+ }
618
+
619
+ /**
620
+ * GET /agents/installed — List all locally installed OpenCode agents.
621
+ */
622
+ app.get('/agents/installed', (_req, res) => {
623
+ try {
624
+ ensureAgentsDir();
625
+ const files = fs.readdirSync(AGENTS_DIR).filter((f) => f.endsWith('.md'));
626
+ const agents = files.map((f) => {
627
+ const name = f.replace(/\.md$/, '');
628
+ const filePath = path.join(AGENTS_DIR, f);
629
+ const meta = parseAgentMdMeta(filePath) || {};
630
+ return {
631
+ name,
632
+ version: meta.version || 'unknown',
633
+ syncedAt: meta.syncedAt || 'unknown',
634
+ source: meta.source || 'unknown',
635
+ };
636
+ });
637
+ res.json({ agents, agentsDir: AGENTS_DIR });
638
+ } catch (err) {
639
+ res.status(500).json({ error: 'Failed to list agents', details: err.message });
640
+ }
641
+ });
642
+
643
+ /**
644
+ * GET /agents/:name/check — Check if a specific agent is installed locally.
645
+ */
646
+ app.get('/agents/:name/check', (req, res) => {
647
+ const name = sanitizeAgentName(req.params.name);
648
+ if (!name) {
649
+ return res.status(400).json({ error: 'Invalid agent name' });
650
+ }
651
+ try {
652
+ ensureAgentsDir();
653
+ const filePath = path.join(AGENTS_DIR, `${name}.md`);
654
+ if (fs.existsSync(filePath)) {
655
+ const meta = parseAgentMdMeta(filePath) || {};
656
+ res.json({ installed: true, version: meta.version, syncedAt: meta.syncedAt });
657
+ } else {
658
+ res.json({ installed: false });
659
+ }
660
+ } catch (err) {
661
+ res.status(500).json({ error: 'Failed to check agent', details: err.message });
662
+ }
663
+ });
664
+
665
+ /**
666
+ * POST /agents/install — Download manifest from marketplace API and install agent locally.
667
+ * Body: { name: string, marketplaceUrl?: string, agentId: string }
668
+ */
669
+ app.post(
670
+ '/agents/install',
671
+ requireBridgeAuth,
672
+ rateLimit('install'),
673
+ express.json(),
674
+ async (req, res) => {
675
+ const { agentId } = req.body;
676
+ let { name } = req.body;
677
+
678
+ if (!agentId) {
679
+ return res.status(400).json({ error: 'agentId is required' });
680
+ }
681
+
682
+ try {
683
+ // Fetch manifest from marketplace API
684
+ const apiUrl =
685
+ req.body.marketplaceUrl ||
686
+ process.env.MARKETPLACE_API_URL ||
687
+ 'https://atos-agentic-factory-qzwe.onrender.com/api';
688
+ const manifestUrl = `${apiUrl}/agents/${encodeURIComponent(agentId)}/opencode-manifest`;
689
+
690
+ const manifest = await new Promise((resolve, reject) => {
691
+ const urlObj = new URL(manifestUrl);
692
+ const client = urlObj.protocol === 'https:' ? https : http;
693
+ const request = client.get(manifestUrl, { timeout: 15000 }, (response) => {
694
+ if (response.statusCode !== 200) {
695
+ reject(new Error(`API returned ${response.statusCode}`));
696
+ return;
697
+ }
698
+ let data = '';
699
+ response.on('data', (chunk) => {
700
+ data += chunk;
701
+ });
702
+ response.on('end', () => {
703
+ try {
704
+ resolve(JSON.parse(data));
705
+ } catch (e) {
706
+ reject(new Error('Invalid JSON response from API'));
707
+ }
708
+ });
709
+ });
710
+ request.on('error', reject);
711
+ request.on('timeout', () => {
712
+ request.destroy();
713
+ reject(new Error('Request timeout'));
714
+ });
715
+ });
716
+
717
+ // Use name from manifest if not provided
718
+ const agentName = sanitizeAgentName(name || manifest.name);
719
+ if (!agentName) {
720
+ return res.status(400).json({ error: 'Invalid agent name' });
721
+ }
722
+
723
+ // Generate .md file content
724
+ const mdContent = generateAgentMd(manifest);
725
+
726
+ // Write to agents directory
727
+ ensureAgentsDir();
728
+ const filePath = path.join(AGENTS_DIR, `${agentName}.md`);
729
+ fs.writeFileSync(filePath, mdContent, 'utf8');
730
+
731
+ console.log(`[Agent Hub] Installed agent: ${agentName} -> ${filePath}`);
732
+ res.json({
733
+ success: true,
734
+ message: `Agent "${agentName}" installed successfully`,
735
+ path: filePath,
736
+ name: agentName,
737
+ version: manifest.version || '1.0.0',
738
+ });
739
+ } catch (err) {
740
+ console.error(`[Agent Hub] Install failed:`, err.message);
741
+ res.status(500).json({ error: 'Failed to install agent', details: err.message });
742
+ }
743
+ },
744
+ );
745
+
746
+ /**
747
+ * DELETE /agents/:name — Remove a locally installed agent.
748
+ */
749
+ app.delete('/agents/:name', requireBridgeAuth, (req, res) => {
750
+ const name = sanitizeAgentName(req.params.name);
751
+ if (!name) {
752
+ return res.status(400).json({ error: 'Invalid agent name' });
753
+ }
754
+ try {
755
+ const filePath = path.join(AGENTS_DIR, `${name}.md`);
756
+ if (!fs.existsSync(filePath)) {
757
+ return res.status(404).json({ error: `Agent "${name}" not found` });
758
+ }
759
+ fs.unlinkSync(filePath);
760
+ console.log(`[Agent Hub] Removed agent: ${name}`);
761
+ res.json({ success: true, message: `Agent "${name}" removed` });
762
+ } catch (err) {
763
+ res.status(500).json({ error: 'Failed to remove agent', details: err.message });
764
+ }
765
+ });
766
+
767
+ /**
768
+ * PUT /agents/:name/update — Re-download and update an existing agent.
769
+ */
770
+ app.put('/agents/:name/update', requireBridgeAuth, async (req, res) => {
771
+ const name = sanitizeAgentName(req.params.name);
772
+ if (!name) {
773
+ return res.status(400).json({ error: 'Invalid agent name' });
774
+ }
775
+
776
+ const filePath = path.join(AGENTS_DIR, `${name}.md`);
777
+ if (!fs.existsSync(filePath)) {
778
+ return res.status(404).json({ error: `Agent "${name}" not found locally` });
779
+ }
780
+
781
+ // Get agentId from existing metadata or request body
782
+ const { agentId } = req.body || {};
783
+ if (!agentId) {
784
+ return res.status(400).json({ error: 'agentId is required for update' });
785
+ }
786
+
787
+ try {
788
+ const apiUrl =
789
+ req.body.marketplaceUrl ||
790
+ process.env.MARKETPLACE_API_URL ||
791
+ 'https://atos-agentic-factory-qzwe.onrender.com/api';
792
+ const manifestUrl = `${apiUrl}/agents/${encodeURIComponent(agentId)}/opencode-manifest`;
793
+
794
+ const manifest = await new Promise((resolve, reject) => {
795
+ const urlObj = new URL(manifestUrl);
796
+ const client = urlObj.protocol === 'https:' ? https : http;
797
+ const request = client.get(manifestUrl, { timeout: 15000 }, (response) => {
798
+ if (response.statusCode !== 200) {
799
+ reject(new Error(`API returned ${response.statusCode}`));
800
+ return;
801
+ }
802
+ let data = '';
803
+ response.on('data', (chunk) => {
804
+ data += chunk;
805
+ });
806
+ response.on('end', () => {
807
+ try {
808
+ resolve(JSON.parse(data));
809
+ } catch (e) {
810
+ reject(new Error('Invalid JSON response from API'));
811
+ }
812
+ });
813
+ });
814
+ request.on('error', reject);
815
+ request.on('timeout', () => {
816
+ request.destroy();
817
+ reject(new Error('Request timeout'));
818
+ });
819
+ });
820
+
821
+ const mdContent = generateAgentMd(manifest);
822
+ fs.writeFileSync(filePath, mdContent, 'utf8');
823
+
824
+ console.log(`[Agent Hub] Updated agent: ${name}`);
825
+ res.json({
826
+ success: true,
827
+ message: `Agent "${name}" updated successfully`,
828
+ path: filePath,
829
+ version: manifest.version || '1.0.0',
830
+ });
831
+ } catch (err) {
832
+ console.error(`[Agent Hub] Update failed:`, err.message);
833
+ res.status(500).json({ error: 'Failed to update agent', details: err.message });
834
+ }
835
+ });
836
+
491
837
  // ---------------------------------------------------------------------------
492
838
  // Smart Detection
493
839
  // ---------------------------------------------------------------------------
@@ -681,6 +1027,136 @@ app.post(
681
1027
  },
682
1028
  );
683
1029
 
1030
+ // ---------------------------------------------------------------------------
1031
+ // Auto-Update
1032
+ // ---------------------------------------------------------------------------
1033
+
1034
+ /**
1035
+ * Check the latest version of the bridge on npm registry.
1036
+ * Returns { current, latest, updateAvailable }.
1037
+ */
1038
+ function checkNpmVersion() {
1039
+ return new Promise((resolve) => {
1040
+ const proc = spawn('npm', ['view', 'agentic-factory-bridge', 'version'], {
1041
+ stdio: ['pipe', 'pipe', 'pipe'],
1042
+ timeout: 15000,
1043
+ shell: true,
1044
+ });
1045
+ let stdout = '';
1046
+ let stderr = '';
1047
+ proc.stdout.on('data', (data) => {
1048
+ stdout += data.toString();
1049
+ });
1050
+ proc.stderr.on('data', (data) => {
1051
+ stderr += data.toString();
1052
+ });
1053
+ proc.on('close', (code) => {
1054
+ if (code === 0 && stdout.trim()) {
1055
+ const latest = stdout.trim();
1056
+ const updateAvailable = latest !== BRIDGE_VERSION;
1057
+ resolve({ current: BRIDGE_VERSION, latest, updateAvailable });
1058
+ } else {
1059
+ resolve({
1060
+ current: BRIDGE_VERSION,
1061
+ latest: null,
1062
+ updateAvailable: false,
1063
+ error: stderr.trim() || 'Failed to query npm registry',
1064
+ });
1065
+ }
1066
+ });
1067
+ proc.on('error', (err) => {
1068
+ resolve({
1069
+ current: BRIDGE_VERSION,
1070
+ latest: null,
1071
+ updateAvailable: false,
1072
+ error: err.message,
1073
+ });
1074
+ });
1075
+ });
1076
+ }
1077
+
1078
+ app.get('/check-update', async (_req, res) => {
1079
+ try {
1080
+ const result = await checkNpmVersion();
1081
+ console.log(
1082
+ `[bridge] Update check: current=${result.current}, latest=${result.latest}, updateAvailable=${result.updateAvailable}`,
1083
+ );
1084
+ res.json(result);
1085
+ } catch (err) {
1086
+ res.json({
1087
+ current: BRIDGE_VERSION,
1088
+ latest: null,
1089
+ updateAvailable: false,
1090
+ error: err.message,
1091
+ });
1092
+ }
1093
+ });
1094
+
1095
+ app.post(
1096
+ '/self-update',
1097
+ requireBridgeAuth,
1098
+ rateLimit('update', RATE_LIMITS.update),
1099
+ async (_req, res) => {
1100
+ console.log('[bridge] Self-update: installing latest version via npm...');
1101
+ try {
1102
+ const installResult = await new Promise((resolve, reject) => {
1103
+ const proc = spawn('npm', ['install', '-g', 'agentic-factory-bridge@latest'], {
1104
+ stdio: ['pipe', 'pipe', 'pipe'],
1105
+ timeout: 120000,
1106
+ shell: true,
1107
+ });
1108
+ let stdout = '';
1109
+ let stderr = '';
1110
+ proc.stdout.on('data', (data) => {
1111
+ stdout += data.toString();
1112
+ console.log(`[bridge] npm update stdout: ${data.toString().trim()}`);
1113
+ });
1114
+ proc.stderr.on('data', (data) => {
1115
+ stderr += data.toString();
1116
+ });
1117
+ proc.on('close', (code) => {
1118
+ if (code === 0) resolve({ success: true, stdout, stderr });
1119
+ else
1120
+ reject(
1121
+ new Error(
1122
+ `npm install -g agentic-factory-bridge@latest failed with code ${code}: ${stderr || stdout}`,
1123
+ ),
1124
+ );
1125
+ });
1126
+ proc.on('error', (err) => {
1127
+ reject(new Error(`Failed to spawn npm: ${err.message}`));
1128
+ });
1129
+ });
1130
+
1131
+ // Check new version
1132
+ const versionCheck = await checkNpmVersion();
1133
+
1134
+ res.json({
1135
+ success: true,
1136
+ message: 'Bridge updated successfully. Restart the bridge to apply.',
1137
+ previousVersion: BRIDGE_VERSION,
1138
+ newVersion: versionCheck.latest || 'unknown',
1139
+ restartRequired: true,
1140
+ });
1141
+
1142
+ // Auto-restart after 2 seconds to let the response be sent
1143
+ console.log('[bridge] Update complete. Restarting in 2 seconds...');
1144
+ setTimeout(() => {
1145
+ process.exit(0); // Exit — systemd/pm2/npm script will restart, or user restarts manually
1146
+ }, 2000);
1147
+ } catch (err) {
1148
+ console.error(`[bridge] Self-update error: ${err.message}`);
1149
+ res.status(500).json({
1150
+ success: false,
1151
+ message: `Update failed: ${err.message}`,
1152
+ previousVersion: BRIDGE_VERSION,
1153
+ newVersion: null,
1154
+ restartRequired: false,
1155
+ });
1156
+ }
1157
+ },
1158
+ );
1159
+
684
1160
  // ---------------------------------------------------------------------------
685
1161
  // Start server
686
1162
  // ---------------------------------------------------------------------------
@@ -711,4 +1187,17 @@ app.listen(PORT, '127.0.0.1', () => {
711
1187
  console.log(` Bridge secret: ${BRIDGE_SECRET.substring(0, 16)}...`);
712
1188
  console.log('');
713
1189
  }
1190
+
1191
+ // Startup update check (non-blocking)
1192
+ checkNpmVersion().then((result) => {
1193
+ if (result.updateAvailable) {
1194
+ console.log(' +----------------------------------------------+');
1195
+ console.log(` | [!] Update available: v${result.latest.padEnd(21)}|`);
1196
+ console.log(` | Current: v${BRIDGE_VERSION.padEnd(29)}|`);
1197
+ console.log(' | Run: npm update -g agentic-factory-bridge |');
1198
+ console.log(' | Or update from the marketplace UI |');
1199
+ console.log(' +----------------------------------------------+');
1200
+ console.log('');
1201
+ }
1202
+ });
714
1203
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-factory-bridge",
3
- "version": "1.0.6",
3
+ "version": "1.2.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": {