web-agent-bridge 2.7.0 → 2.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web-agent-bridge",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "description": "Open-source middleware that bridges AI agents and websites — providing a standardized command interface for intelligent automation",
5
5
  "main": "server/index.js",
6
6
  "bin": {
package/sdk/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web-agent-bridge-sdk",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "description": "SDK for building AI agents that interact with Web Agent Bridge (WAB)",
5
5
  "main": "index.js",
6
6
  "license": "MIT",
package/server/index.js CHANGED
@@ -15,6 +15,7 @@ const { maybeBootstrapAdmin, db } = require('./models/db');
15
15
  const { initSearchEngine, search, getSuggestions, getTrendingSearches, getSearchStats, purgeOldCache } = require('./services/search-engine');
16
16
  const { processMessage: agentChat } = require('./services/agent-chat');
17
17
  const agentTasks = require('./services/agent-tasks');
18
+ const { cluster } = require('./services/cluster');
18
19
 
19
20
  const authRoutes = require('./routes/auth');
20
21
  const apiRoutes = require('./routes/api');
@@ -359,6 +360,9 @@ if (process.env.NODE_ENV !== 'test') {
359
360
  // Start Agent OS runtime
360
361
  runtime.start();
361
362
 
363
+ // Start Cluster Orchestrator
364
+ cluster.start();
365
+
362
366
  server.listen(PORT, () => {
363
367
  console.log(`\n ╔══════════════════════════════════════════╗`);
364
368
  console.log(` ║ Web Agent Bridge v${pkg.version} ║`);
@@ -34,6 +34,9 @@ const metering = require('../services/metering');
34
34
  const { marketplace } = require('../services/marketplace');
35
35
  const { hostedRuntime } = require('../services/hosted-runtime');
36
36
  const { sessionEngine } = require('../runtime/session-engine');
37
+ const vision = require('../services/vision');
38
+ const { lfdEngine } = require('../services/lfd');
39
+ const { cluster, distributor } = require('../services/cluster');
37
40
 
38
41
  // ═══════════════════════════════════════════════════════════════════════════
39
42
  // AUTH MIDDLEWARE
@@ -55,6 +58,10 @@ const PUBLIC_PATHS = [
55
58
  '/registry/templates',
56
59
  '/plans',
57
60
  '/marketplace',
61
+ '/recipes',
62
+ '/vision/models',
63
+ '/vision/extraction-script',
64
+ '/cluster/status',
58
65
  ];
59
66
 
60
67
  function authMiddleware(req, res, next) {
@@ -534,6 +541,8 @@ router.get('/observability/health', (req, res) => {
534
541
  health.marketplace = marketplace.getStats();
535
542
  health.hostedRuntime = hostedRuntime.getStats();
536
543
  health.metering = metering.getStats();
544
+ health.lfd = lfdEngine.getStats();
545
+ health.cluster = cluster.getClusterStatus();
537
546
  res.json(health);
538
547
  });
539
548
 
@@ -1443,4 +1452,492 @@ router.get('/hosted/stats', (req, res) => {
1443
1452
  res.json(hostedRuntime.getStats());
1444
1453
  });
1445
1454
 
1455
+ // ═══════════════════════════════════════════════════════════════════════════
1456
+ // LOCAL VISION ENGINE (Self-contained — no external API)
1457
+ // ═══════════════════════════════════════════════════════════════════════════
1458
+
1459
+ /**
1460
+ * Analyze page DOM locally (no external API calls)
1461
+ */
1462
+ router.post('/vision/analyze-dom', async (req, res) => {
1463
+ try {
1464
+ const siteId = req.body.siteId || req.agentId || 'default';
1465
+ const result = await vision.analyzePageDOM(siteId, {
1466
+ domSnapshot: req.body.domSnapshot,
1467
+ url: req.body.url,
1468
+ });
1469
+ res.json(result);
1470
+ } catch (err) {
1471
+ res.status(400).json({ error: err.message });
1472
+ }
1473
+ });
1474
+
1475
+ /**
1476
+ * Get DOM extraction script to inject into pages
1477
+ */
1478
+ router.get('/vision/extraction-script', (req, res) => {
1479
+ res.json({ script: vision.getDomExtractionScript() });
1480
+ });
1481
+
1482
+ /**
1483
+ * Find elements in cached vision data
1484
+ */
1485
+ router.get('/vision/elements', (req, res) => {
1486
+ const siteId = req.query.siteId || req.agentId || 'default';
1487
+ const results = vision.findElement(siteId, req.query.url, {
1488
+ description: req.query.q,
1489
+ type: req.query.type,
1490
+ label: req.query.label,
1491
+ });
1492
+ res.json({ elements: results, total: results.length });
1493
+ });
1494
+
1495
+ /**
1496
+ * Vision history
1497
+ */
1498
+ router.get('/vision/history', (req, res) => {
1499
+ const siteId = req.query.siteId || req.agentId || 'default';
1500
+ const history = vision.getVisionHistory(siteId, {
1501
+ limit: parseInt(req.query.limit) || 50,
1502
+ url: req.query.url,
1503
+ });
1504
+ res.json({ history, total: history.length });
1505
+ });
1506
+
1507
+ /**
1508
+ * Supported vision models
1509
+ */
1510
+ router.get('/vision/models', (req, res) => {
1511
+ res.json({ models: vision.getSupportedModels() });
1512
+ });
1513
+
1514
+ // ═══════════════════════════════════════════════════════════════════════════
1515
+ // LEARNING FROM DEMONSTRATION (LfD)
1516
+ // ═══════════════════════════════════════════════════════════════════════════
1517
+
1518
+ /**
1519
+ * Start a recording session
1520
+ */
1521
+ router.post('/lfd/record', (req, res) => {
1522
+ try {
1523
+ const session = lfdEngine.startRecording({
1524
+ name: req.body.name,
1525
+ description: req.body.description,
1526
+ agentId: req.agentId || req.body.agentId,
1527
+ startUrl: req.body.startUrl,
1528
+ tags: req.body.tags,
1529
+ });
1530
+ res.json(session);
1531
+ } catch (err) {
1532
+ res.status(400).json({ error: err.message });
1533
+ }
1534
+ });
1535
+
1536
+ /**
1537
+ * Record events into a session
1538
+ */
1539
+ router.post('/lfd/:sessionId/events', (req, res) => {
1540
+ try {
1541
+ const events = req.body.events || [req.body];
1542
+ const results = events.map(evt => lfdEngine.recordEvent(req.params.sessionId, evt));
1543
+ res.json({ recorded: results.filter(Boolean).length });
1544
+ } catch (err) {
1545
+ res.status(400).json({ error: err.message });
1546
+ }
1547
+ });
1548
+
1549
+ /**
1550
+ * Record a DOM snapshot
1551
+ */
1552
+ router.post('/lfd/:sessionId/snapshot', (req, res) => {
1553
+ try {
1554
+ lfdEngine.recordSnapshot(req.params.sessionId, req.body);
1555
+ res.json({ success: true });
1556
+ } catch (err) {
1557
+ res.status(400).json({ error: err.message });
1558
+ }
1559
+ });
1560
+
1561
+ /**
1562
+ * Pause recording
1563
+ */
1564
+ router.post('/lfd/:sessionId/pause', (req, res) => {
1565
+ try { res.json(lfdEngine.pauseRecording(req.params.sessionId)); }
1566
+ catch (err) { res.status(400).json({ error: err.message }); }
1567
+ });
1568
+
1569
+ /**
1570
+ * Resume recording
1571
+ */
1572
+ router.post('/lfd/:sessionId/resume', (req, res) => {
1573
+ try { res.json(lfdEngine.resumeRecording(req.params.sessionId)); }
1574
+ catch (err) { res.status(400).json({ error: err.message }); }
1575
+ });
1576
+
1577
+ /**
1578
+ * Stop recording and generate recipe
1579
+ */
1580
+ router.post('/lfd/:sessionId/stop', (req, res) => {
1581
+ try { res.json(lfdEngine.stopRecording(req.params.sessionId)); }
1582
+ catch (err) { res.status(400).json({ error: err.message }); }
1583
+ });
1584
+
1585
+ /**
1586
+ * Cancel recording
1587
+ */
1588
+ router.post('/lfd/:sessionId/cancel', (req, res) => {
1589
+ try { res.json(lfdEngine.cancelRecording(req.params.sessionId)); }
1590
+ catch (err) { res.status(400).json({ error: err.message }); }
1591
+ });
1592
+
1593
+ /**
1594
+ * Get recording details
1595
+ */
1596
+ router.get('/lfd/:sessionId', (req, res) => {
1597
+ const recording = lfdEngine.getRecording(req.params.sessionId);
1598
+ if (!recording) return res.status(404).json({ error: 'Recording not found' });
1599
+ res.json(recording);
1600
+ });
1601
+
1602
+ /**
1603
+ * List recordings
1604
+ */
1605
+ router.get('/lfd', (req, res) => {
1606
+ res.json({ recordings: lfdEngine.listRecordings(parseInt(req.query.limit) || 50) });
1607
+ });
1608
+
1609
+ /**
1610
+ * Get recording script to inject into pages
1611
+ */
1612
+ router.get('/lfd/:sessionId/script', (req, res) => {
1613
+ const serverUrl = `${req.protocol}://${req.get('host')}`;
1614
+ res.json({ script: lfdEngine.getRecordingScript(req.params.sessionId, serverUrl) });
1615
+ });
1616
+
1617
+ // ── Recipes ──
1618
+
1619
+ /**
1620
+ * List recipes
1621
+ */
1622
+ router.get('/recipes', (req, res) => {
1623
+ const recipes = lfdEngine.listRecipes({
1624
+ domain: req.query.domain,
1625
+ tag: req.query.tag,
1626
+ query: req.query.q,
1627
+ }, parseInt(req.query.limit) || 50);
1628
+ res.json({ recipes, total: recipes.length });
1629
+ });
1630
+
1631
+ /**
1632
+ * Get recipe
1633
+ */
1634
+ router.get('/recipes/:recipeId', (req, res) => {
1635
+ const recipe = lfdEngine.getRecipe(req.params.recipeId);
1636
+ if (!recipe) return res.status(404).json({ error: 'Recipe not found' });
1637
+ res.json(recipe);
1638
+ });
1639
+
1640
+ /**
1641
+ * Save/import recipe manually
1642
+ */
1643
+ router.post('/recipes', (req, res) => {
1644
+ try { res.json(lfdEngine.saveRecipe(req.body)); }
1645
+ catch (err) { res.status(400).json({ error: err.message }); }
1646
+ });
1647
+
1648
+ /**
1649
+ * Delete recipe
1650
+ */
1651
+ router.delete('/recipes/:recipeId', (req, res) => {
1652
+ const deleted = lfdEngine.deleteRecipe(req.params.recipeId);
1653
+ res.json({ deleted });
1654
+ });
1655
+
1656
+ /**
1657
+ * Execute a recipe
1658
+ */
1659
+ router.post('/recipes/:recipeId/execute', (req, res) => {
1660
+ try {
1661
+ const execution = lfdEngine.executeRecipe(req.params.recipeId, {
1662
+ variables: req.body.variables,
1663
+ speed: req.body.speed,
1664
+ stopOnError: req.body.stopOnError,
1665
+ skipWaits: req.body.skipWaits,
1666
+ humanInTheLoop: req.body.humanInTheLoop,
1667
+ });
1668
+ res.json(execution);
1669
+ } catch (err) {
1670
+ res.status(400).json({ error: err.message });
1671
+ }
1672
+ });
1673
+
1674
+ /**
1675
+ * Get next step in execution
1676
+ */
1677
+ router.get('/executions/:executionId/next', (req, res) => {
1678
+ const step = lfdEngine.getNextStep(req.params.executionId);
1679
+ if (!step) {
1680
+ const exec = lfdEngine.getExecution(req.params.executionId);
1681
+ return res.json({ done: true, status: exec?.status || 'unknown' });
1682
+ }
1683
+ res.json(step);
1684
+ });
1685
+
1686
+ /**
1687
+ * Report step result
1688
+ */
1689
+ router.post('/executions/:executionId/steps/:stepIndex', (req, res) => {
1690
+ const exec = lfdEngine.reportStep(
1691
+ req.params.executionId,
1692
+ parseInt(req.params.stepIndex),
1693
+ { success: req.body.success, error: req.body.error, duration: req.body.duration, selectorUsed: req.body.selectorUsed }
1694
+ );
1695
+ if (!exec) return res.status(404).json({ error: 'Execution not found' });
1696
+ res.json({ status: exec.status, currentStep: exec.currentStep, totalSteps: exec.totalSteps });
1697
+ });
1698
+
1699
+ /**
1700
+ * Pause execution
1701
+ */
1702
+ router.post('/executions/:executionId/pause', (req, res) => {
1703
+ const exec = lfdEngine.pauseExecution(req.params.executionId);
1704
+ if (!exec) return res.status(404).json({ error: 'Execution not found' });
1705
+ res.json({ status: exec.status });
1706
+ });
1707
+
1708
+ /**
1709
+ * Resume execution
1710
+ */
1711
+ router.post('/executions/:executionId/resume', (req, res) => {
1712
+ const exec = lfdEngine.resumeExecution(req.params.executionId);
1713
+ if (!exec) return res.status(404).json({ error: 'Execution not found' });
1714
+ res.json({ status: exec.status });
1715
+ });
1716
+
1717
+ /**
1718
+ * Abort execution
1719
+ */
1720
+ router.post('/executions/:executionId/abort', (req, res) => {
1721
+ const exec = lfdEngine.abortExecution(req.params.executionId);
1722
+ if (!exec) return res.status(404).json({ error: 'Execution not found' });
1723
+ res.json({ status: exec.status });
1724
+ });
1725
+
1726
+ /**
1727
+ * Get execution details
1728
+ */
1729
+ router.get('/executions/:executionId', (req, res) => {
1730
+ const exec = lfdEngine.getExecution(req.params.executionId);
1731
+ if (!exec) return res.status(404).json({ error: 'Execution not found' });
1732
+ res.json(exec);
1733
+ });
1734
+
1735
+ /**
1736
+ * List executions
1737
+ */
1738
+ router.get('/executions', (req, res) => {
1739
+ res.json({ executions: lfdEngine.listExecutions(parseInt(req.query.limit) || 50) });
1740
+ });
1741
+
1742
+ /**
1743
+ * LfD stats
1744
+ */
1745
+ router.get('/lfd/stats', (req, res) => {
1746
+ res.json(lfdEngine.getStats());
1747
+ });
1748
+
1749
+ // ═══════════════════════════════════════════════════════════════════════════
1750
+ // CLUSTER — DISTRIBUTED EXECUTION & WORKER NODES
1751
+ // ═══════════════════════════════════════════════════════════════════════════
1752
+
1753
+ /**
1754
+ * Get cluster status (public)
1755
+ */
1756
+ router.get('/cluster/status', (req, res) => {
1757
+ res.json(cluster.getClusterStatus());
1758
+ });
1759
+
1760
+ /**
1761
+ * Register a worker node
1762
+ */
1763
+ router.post('/cluster/nodes', (req, res) => {
1764
+ try {
1765
+ const result = cluster.registerNode({
1766
+ name: req.body.name,
1767
+ endpoint: req.body.endpoint,
1768
+ region: req.body.region,
1769
+ zone: req.body.zone,
1770
+ role: req.body.role,
1771
+ capacity: req.body.capacity,
1772
+ tags: req.body.tags,
1773
+ hardware: req.body.hardware,
1774
+ version: req.body.version,
1775
+ secret: req.body.secret,
1776
+ });
1777
+ res.json(result);
1778
+ } catch (err) {
1779
+ res.status(400).json({ error: err.message });
1780
+ }
1781
+ });
1782
+
1783
+ /**
1784
+ * List cluster nodes
1785
+ */
1786
+ router.get('/cluster/nodes', (req, res) => {
1787
+ const nodes = cluster.listNodes({
1788
+ region: req.query.region,
1789
+ active: req.query.active === 'true',
1790
+ });
1791
+ res.json({ nodes });
1792
+ });
1793
+
1794
+ /**
1795
+ * Get a specific node
1796
+ */
1797
+ router.get('/cluster/nodes/:nodeId', (req, res) => {
1798
+ const node = cluster.getNode(req.params.nodeId);
1799
+ if (!node) return res.status(404).json({ error: 'Node not found' });
1800
+ res.json(node);
1801
+ });
1802
+
1803
+ /**
1804
+ * Remove a node
1805
+ */
1806
+ router.delete('/cluster/nodes/:nodeId', (req, res) => {
1807
+ const result = cluster.deregisterNode(req.params.nodeId);
1808
+ if (!result) return res.status(404).json({ error: 'Node not found' });
1809
+ res.json(result);
1810
+ });
1811
+
1812
+ /**
1813
+ * Worker heartbeat
1814
+ */
1815
+ router.post('/cluster/nodes/:nodeId/heartbeat', (req, res) => {
1816
+ const result = cluster.heartbeat(req.params.nodeId, {
1817
+ capacityUsed: req.body.capacityUsed,
1818
+ capacityTotal: req.body.capacityTotal,
1819
+ hardware: req.body.hardware,
1820
+ tags: req.body.tags,
1821
+ version: req.body.version,
1822
+ });
1823
+ if (!result) return res.status(404).json({ error: 'Node not found' });
1824
+ res.json(result);
1825
+ });
1826
+
1827
+ /**
1828
+ * Drain a node (stop new tasks, wait for running)
1829
+ */
1830
+ router.post('/cluster/nodes/:nodeId/drain', (req, res) => {
1831
+ const result = cluster.drainNode(req.params.nodeId);
1832
+ if (!result) return res.status(404).json({ error: 'Node not found' });
1833
+ res.json(result);
1834
+ });
1835
+
1836
+ /**
1837
+ * Cordon a node (prevent scheduling)
1838
+ */
1839
+ router.post('/cluster/nodes/:nodeId/cordon', (req, res) => {
1840
+ const result = cluster.cordonNode(req.params.nodeId);
1841
+ if (!result) return res.status(404).json({ error: 'Node not found' });
1842
+ res.json(result);
1843
+ });
1844
+
1845
+ /**
1846
+ * Uncordon a node (allow scheduling again)
1847
+ */
1848
+ router.post('/cluster/nodes/:nodeId/uncordon', (req, res) => {
1849
+ const result = cluster.uncordonNode(req.params.nodeId);
1850
+ if (!result) return res.status(404).json({ error: 'Node not found' });
1851
+ res.json(result);
1852
+ });
1853
+
1854
+ /**
1855
+ * Submit a task for distributed execution
1856
+ */
1857
+ router.post('/cluster/tasks', (req, res) => {
1858
+ try {
1859
+ const result = distributor.submit({
1860
+ type: req.body.type,
1861
+ objective: req.body.objective,
1862
+ params: req.body.params,
1863
+ priority: req.body.priority,
1864
+ affinityTags: req.body.affinityTags,
1865
+ affinityRegion: req.body.affinityRegion,
1866
+ timeout: req.body.timeout,
1867
+ maxAttempts: req.body.maxAttempts,
1868
+ externalId: req.body.externalId,
1869
+ });
1870
+ res.json(result);
1871
+ } catch (err) {
1872
+ res.status(400).json({ error: err.message });
1873
+ }
1874
+ });
1875
+
1876
+ /**
1877
+ * Get task details
1878
+ */
1879
+ router.get('/cluster/tasks/:taskId', (req, res) => {
1880
+ const task = cluster.getTask(req.params.taskId);
1881
+ if (!task) return res.status(404).json({ error: 'Task not found' });
1882
+ res.json(task);
1883
+ });
1884
+
1885
+ /**
1886
+ * List tasks
1887
+ */
1888
+ router.get('/cluster/tasks', (req, res) => {
1889
+ const tasks = cluster.listTasks({
1890
+ status: req.query.status,
1891
+ nodeId: req.query.nodeId,
1892
+ limit: parseInt(req.query.limit) || 50,
1893
+ });
1894
+ res.json({ tasks });
1895
+ });
1896
+
1897
+ /**
1898
+ * Worker pulls tasks (poll-based)
1899
+ */
1900
+ router.post('/cluster/nodes/:nodeId/pull', (req, res) => {
1901
+ const tasks = distributor.pullTasks(req.params.nodeId, parseInt(req.body.limit) || 5);
1902
+ res.json({ tasks });
1903
+ });
1904
+
1905
+ /**
1906
+ * Worker reports task started
1907
+ */
1908
+ router.post('/cluster/tasks/:taskId/started', (req, res) => {
1909
+ const result = cluster.reportTaskStarted(req.params.taskId);
1910
+ if (!result) return res.status(404).json({ error: 'Task not found' });
1911
+ res.json(result);
1912
+ });
1913
+
1914
+ /**
1915
+ * Worker reports task completed
1916
+ */
1917
+ router.post('/cluster/tasks/:taskId/completed', (req, res) => {
1918
+ const result = cluster.reportTaskCompleted(req.params.taskId, req.body.result);
1919
+ if (!result) return res.status(404).json({ error: 'Task not found' });
1920
+ res.json(result);
1921
+ });
1922
+
1923
+ /**
1924
+ * Worker reports task failed
1925
+ */
1926
+ router.post('/cluster/tasks/:taskId/failed', (req, res) => {
1927
+ const result = cluster.reportTaskFailed(req.params.taskId, req.body.error);
1928
+ if (!result) return res.status(404).json({ error: 'Task not found' });
1929
+ res.json(result);
1930
+ });
1931
+
1932
+ /**
1933
+ * Get cluster events log
1934
+ */
1935
+ router.get('/cluster/events', (req, res) => {
1936
+ const events = cluster.getEvents(
1937
+ parseInt(req.query.limit) || 100,
1938
+ req.query.nodeId || null
1939
+ );
1940
+ res.json({ events });
1941
+ });
1942
+
1446
1943
  module.exports = router;