agentic-factory-bridge 1.0.5 → 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.
package/README.md CHANGED
@@ -12,7 +12,7 @@ Local bridge that connects the [Atos Agentic Factory](https://atos-agentic-facto
12
12
 
13
13
  - **Node.js 18+**
14
14
  - **Windows** (protocol registration uses PowerShell and Windows Registry)
15
- - **OpenCode CLI** installed globally: `npm install -g opencode`
15
+ - **OpenCode CLI** installed globally: `npm install -g opencode-ai`
16
16
 
17
17
  ## Installation
18
18
 
package/bin/cli.js CHANGED
@@ -86,13 +86,13 @@ async function setup() {
86
86
  if (hasOpencode) {
87
87
  console.log(' [OK] OpenCode CLI is already installed.');
88
88
  } else {
89
- console.log(' [..] Installing OpenCode CLI (npm install -g opencode)...');
89
+ console.log(' [..] Installing OpenCode CLI (npm install -g opencode-ai)...');
90
90
  try {
91
- await runCommand('npm', ['install', '-g', 'opencode'], 120000);
91
+ await runCommand('npm', ['install', '-g', 'opencode-ai'], 120000);
92
92
  console.log(' [OK] OpenCode CLI installed successfully.');
93
93
  } catch (err) {
94
94
  console.error(' [FAIL] Could not install OpenCode CLI: ' + err.message);
95
- console.error(' Try running manually: npm install -g opencode');
95
+ console.error(' Try running manually: npm install -g opencode-ai');
96
96
  }
97
97
  }
98
98
 
@@ -180,7 +180,7 @@ async function status() {
180
180
  if (hasOpencode) {
181
181
  console.log(' OpenCode: INSTALLED');
182
182
  } else {
183
- console.log(' OpenCode: NOT FOUND — run: npm install -g opencode');
183
+ console.log(' OpenCode: NOT FOUND — run: npm install -g opencode-ai');
184
184
  }
185
185
 
186
186
  // Check protocol (Windows)
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
  // ---------------------------------------------------------------------------
@@ -554,13 +899,13 @@ app.post(
554
899
  requireBridgeAuth,
555
900
  rateLimit('install', RATE_LIMITS.install),
556
901
  async (_req, res) => {
557
- console.log('[bridge] Installing OpenCode CLI via npm install -g opencode...');
902
+ console.log('[bridge] Installing OpenCode CLI via npm install -g opencode-ai...');
558
903
  try {
559
904
  await new Promise((resolve, reject) => {
560
- const proc = spawn('npm', ['install', '-g', 'opencode'], {
905
+ const proc = spawn('npm', ['install', '-g', 'opencode-ai'], {
561
906
  stdio: ['pipe', 'pipe', 'pipe'],
562
907
  timeout: 120000,
563
- shell: false,
908
+ shell: true,
564
909
  });
565
910
  let stdout = '';
566
911
  let stderr = '';
@@ -575,7 +920,7 @@ app.post(
575
920
  if (code === 0) resolve({ success: true, stdout, stderr });
576
921
  else
577
922
  reject(
578
- new Error(`npm install -g opencode failed with code ${code}: ${stderr || stdout}`),
923
+ new Error(`npm install -g opencode-ai failed with code ${code}: ${stderr || stdout}`),
579
924
  );
580
925
  });
581
926
  proc.on('error', (err) => {
@@ -26,7 +26,7 @@ $OpenCodePath = Get-Command opencode -ErrorAction SilentlyContinue
26
26
  if (-not $OpenCodePath) {
27
27
  Write-Host ""
28
28
  Write-Host " WARNING: opencode CLI not found in PATH" -ForegroundColor Yellow
29
- Write-Host " Install OpenCode: npm install -g opencode" -ForegroundColor Yellow
29
+ Write-Host " Install OpenCode: npm install -g opencode-ai" -ForegroundColor Yellow
30
30
  Write-Host ""
31
31
  }
32
32
 
@@ -38,7 +38,7 @@ if (-not (Test-Path $opencode)) {
38
38
  }
39
39
 
40
40
  if (-not (Test-Path $opencode)) {
41
- Write-Host "`n ERROR: opencode not found!`n Install: npm install -g opencode`n" -ForegroundColor Red
41
+ Write-Host "`n ERROR: opencode not found!`n Install: npm install -g opencode-ai`n" -ForegroundColor Red
42
42
  Read-Host "Press Enter"
43
43
  exit 1
44
44
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-factory-bridge",
3
- "version": "1.0.5",
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": {