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 +1 -1
- package/bin/cli.js +4 -4
- package/bridge.js +349 -4
- package/install-protocol.ps1 +1 -1
- package/opencode-launch.ps1 +1 -1
- package/package.json +1 -1
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:
|
|
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) => {
|
package/install-protocol.ps1
CHANGED
|
@@ -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
|
|
package/opencode-launch.ps1
CHANGED
|
@@ -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
|
}
|