web-agent-bridge 2.6.0 → 2.8.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.
@@ -28,7 +28,14 @@ const { commandRegistry, siteRegistry, templateRegistry } = require('../registry
28
28
  const { certificationEngine } = require('../registry/certification');
29
29
  const { adapterManager, mcpAdapter, restAdapter, browserAdapter } = require('../adapters');
30
30
  const { replayEngine } = require('../runtime/replay');
31
+ const { featureGate, usageLimit } = require('../middleware/featureGate');
32
+ const { listPlans, getPlan, USAGE_PRICING, MARKETPLACE } = require('../config/plans');
33
+ const metering = require('../services/metering');
34
+ const { marketplace } = require('../services/marketplace');
35
+ const { hostedRuntime } = require('../services/hosted-runtime');
31
36
  const { sessionEngine } = require('../runtime/session-engine');
37
+ const vision = require('../services/vision');
38
+ const { lfdEngine } = require('../services/lfd');
32
39
 
33
40
  // ═══════════════════════════════════════════════════════════════════════════
34
41
  // AUTH MIDDLEWARE
@@ -48,6 +55,14 @@ const PUBLIC_PATHS = [
48
55
  '/registry/commands',
49
56
  '/registry/sites',
50
57
  '/registry/templates',
58
+ '/plans',
59
+ '/marketplace',
60
+ '/recipes',
61
+ '/vision/models',
62
+ '/vision/extraction-script',
63
+ '/recipes',
64
+ '/vision/models',
65
+ '/vision/extraction-script',
51
66
  ];
52
67
 
53
68
  function authMiddleware(req, res, next) {
@@ -99,6 +114,7 @@ function authMiddleware(req, res, next) {
99
114
  }
100
115
 
101
116
  router.use(authMiddleware);
117
+ router.use(featureGate);
102
118
 
103
119
  // ═══════════════════════════════════════════════════════════════════════════
104
120
  // PROTOCOL ENDPOINTS
@@ -241,7 +257,7 @@ router.delete('/agents/:agentId', (req, res) => {
241
257
  /**
242
258
  * Submit a task
243
259
  */
244
- router.post('/tasks', (req, res) => {
260
+ router.post('/tasks', usageLimit('tasksPerDay'), (req, res) => {
245
261
  try {
246
262
  const result = runtime.submitTask(req.body);
247
263
  metrics.increment('tasks.submitted', 1, { type: req.body.type });
@@ -299,7 +315,7 @@ router.post('/tasks/:taskId/resume', (req, res) => {
299
315
  /**
300
316
  * Execute a semantic action
301
317
  */
302
- router.post('/execute', async (req, res) => {
318
+ router.post('/execute', usageLimit('executionsPerDay'), async (req, res) => {
303
319
  try {
304
320
  const result = await executor.execute(req.body);
305
321
  res.json(result);
@@ -523,6 +539,10 @@ router.get('/observability/health', (req, res) => {
523
539
  health.sessions = sessionEngine.getStats();
524
540
  health.failures = failureAnalyzer.getStats();
525
541
  health.certification = certificationEngine.getStats();
542
+ health.marketplace = marketplace.getStats();
543
+ health.hostedRuntime = hostedRuntime.getStats();
544
+ health.metering = metering.getStats();
545
+ health.lfd = lfdEngine.getStats();
526
546
  res.json(health);
527
547
  });
528
548
 
@@ -634,7 +654,7 @@ router.get('/registry/templates/:templateId', (req, res) => {
634
654
  /**
635
655
  * LLM completion
636
656
  */
637
- router.post('/llm/complete', async (req, res) => {
657
+ router.post('/llm/complete', usageLimit('executionsPerDay'), async (req, res) => {
638
658
  try {
639
659
  const result = await llm.complete(req.body.prompt, req.body.options || req.body);
640
660
  metrics.increment('llm.api.requests');
@@ -1133,4 +1153,597 @@ router.delete('/certification/:domain', (req, res) => {
1133
1153
  res.json({ success: true });
1134
1154
  });
1135
1155
 
1156
+ // ═══════════════════════════════════════════════════════════════════════════
1157
+ // PLANS & PRICING
1158
+ // ═══════════════════════════════════════════════════════════════════════════
1159
+
1160
+ /**
1161
+ * List available plans
1162
+ */
1163
+ router.get('/plans', (req, res) => {
1164
+ const plans = listPlans().map(p => ({
1165
+ id: p.id,
1166
+ name: p.name,
1167
+ price: p.price,
1168
+ interval: p.interval,
1169
+ description: p.description,
1170
+ limits: p.limits,
1171
+ features: Object.entries(p.features)
1172
+ .filter(([, v]) => v === true)
1173
+ .map(([k]) => k),
1174
+ }));
1175
+ res.json({ plans, usagePricing: USAGE_PRICING });
1176
+ });
1177
+
1178
+ /**
1179
+ * Get specific plan details
1180
+ */
1181
+ router.get('/plans/:planId', (req, res) => {
1182
+ const plan = getPlan(req.params.planId);
1183
+ if (!plan || plan.id === 'free' && req.params.planId !== 'free') {
1184
+ return res.status(404).json({ error: 'Plan not found' });
1185
+ }
1186
+ res.json(plan);
1187
+ });
1188
+
1189
+ // ═══════════════════════════════════════════════════════════════════════════
1190
+ // USAGE METERING
1191
+ // ═══════════════════════════════════════════════════════════════════════════
1192
+
1193
+ /**
1194
+ * Get usage for current agent
1195
+ */
1196
+ router.get('/usage', (req, res) => {
1197
+ const entityId = req.agentId || req.ip;
1198
+ const tier = req.agentTier || req.session?.tier || 'free';
1199
+ res.json(metering.getUsage(entityId, tier));
1200
+ });
1201
+
1202
+ /**
1203
+ * Get billing summary (overages)
1204
+ */
1205
+ router.get('/usage/billing', (req, res) => {
1206
+ const entityId = req.agentId || req.ip;
1207
+ res.json(metering.getBillingSummary(entityId));
1208
+ });
1209
+
1210
+ /**
1211
+ * Get metering stats (admin)
1212
+ */
1213
+ router.get('/usage/stats', (req, res) => {
1214
+ res.json(metering.getStats());
1215
+ });
1216
+
1217
+ // ═══════════════════════════════════════════════════════════════════════════
1218
+ // MARKETPLACE
1219
+ // ═══════════════════════════════════════════════════════════════════════════
1220
+
1221
+ /**
1222
+ * Search marketplace
1223
+ */
1224
+ router.get('/marketplace', (req, res) => {
1225
+ const listings = marketplace.search({
1226
+ type: req.query.type,
1227
+ category: req.query.category,
1228
+ query: req.query.q,
1229
+ tag: req.query.tag,
1230
+ free: req.query.free === 'true',
1231
+ paid: req.query.paid === 'true',
1232
+ minRating: req.query.minRating ? parseFloat(req.query.minRating) : undefined,
1233
+ sortBy: req.query.sortBy,
1234
+ }, parseInt(req.query.limit) || 50);
1235
+ res.json({ listings, total: listings.length });
1236
+ });
1237
+
1238
+ /**
1239
+ * Get listing
1240
+ */
1241
+ router.get('/marketplace/:listingId', (req, res) => {
1242
+ const listing = marketplace.getListing(req.params.listingId);
1243
+ if (!listing) return res.status(404).json({ error: 'Listing not found' });
1244
+ res.json(listing);
1245
+ });
1246
+
1247
+ /**
1248
+ * Get reviews
1249
+ */
1250
+ router.get('/marketplace/:listingId/reviews', (req, res) => {
1251
+ res.json({ reviews: marketplace.getReviews(req.params.listingId) });
1252
+ });
1253
+
1254
+ /**
1255
+ * Publish listing
1256
+ */
1257
+ router.post('/marketplace/publish', (req, res) => {
1258
+ try {
1259
+ const listing = marketplace.publish({
1260
+ ...req.body,
1261
+ sellerId: req.agentId || req.body.sellerId,
1262
+ });
1263
+ res.json(listing);
1264
+ } catch (err) {
1265
+ res.status(400).json({ error: err.message });
1266
+ }
1267
+ });
1268
+
1269
+ /**
1270
+ * Purchase/install listing
1271
+ */
1272
+ router.post('/marketplace/:listingId/purchase', (req, res) => {
1273
+ try {
1274
+ const buyerId = req.agentId || req.body.buyerId;
1275
+ if (!buyerId) return res.status(400).json({ error: 'buyerId required' });
1276
+ const purchase = marketplace.purchase(req.params.listingId, buyerId);
1277
+ res.json(purchase);
1278
+ } catch (err) {
1279
+ res.status(400).json({ error: err.message });
1280
+ }
1281
+ });
1282
+
1283
+ /**
1284
+ * Add review
1285
+ */
1286
+ router.post('/marketplace/:listingId/review', (req, res) => {
1287
+ try {
1288
+ const review = marketplace.addReview(req.params.listingId, {
1289
+ userId: req.agentId || req.body.userId,
1290
+ rating: req.body.rating,
1291
+ comment: req.body.comment,
1292
+ });
1293
+ res.json(review);
1294
+ } catch (err) {
1295
+ res.status(400).json({ error: err.message });
1296
+ }
1297
+ });
1298
+
1299
+ /**
1300
+ * Get my purchases
1301
+ */
1302
+ router.get('/marketplace/my/purchases', (req, res) => {
1303
+ const buyerId = req.agentId || req.query.buyerId;
1304
+ res.json({ purchases: marketplace.getPurchases(buyerId) });
1305
+ });
1306
+
1307
+ /**
1308
+ * Get seller earnings
1309
+ */
1310
+ router.get('/marketplace/my/earnings', (req, res) => {
1311
+ const sellerId = req.agentId || req.query.sellerId;
1312
+ res.json(marketplace.getEarnings(sellerId));
1313
+ });
1314
+
1315
+ /**
1316
+ * Admin: pending listings
1317
+ */
1318
+ router.get('/marketplace/admin/pending', (req, res) => {
1319
+ res.json({ listings: marketplace.getPendingListings() });
1320
+ });
1321
+
1322
+ /**
1323
+ * Admin: approve listing
1324
+ */
1325
+ router.post('/marketplace/admin/:listingId/approve', (req, res) => {
1326
+ try {
1327
+ const listing = marketplace.approve(req.params.listingId);
1328
+ res.json(listing);
1329
+ } catch (err) {
1330
+ res.status(400).json({ error: err.message });
1331
+ }
1332
+ });
1333
+
1334
+ /**
1335
+ * Admin: reject listing
1336
+ */
1337
+ router.post('/marketplace/admin/:listingId/reject', (req, res) => {
1338
+ try {
1339
+ const listing = marketplace.reject(req.params.listingId, req.body.reason);
1340
+ res.json(listing);
1341
+ } catch (err) {
1342
+ res.status(400).json({ error: err.message });
1343
+ }
1344
+ });
1345
+
1346
+ /**
1347
+ * Marketplace stats
1348
+ */
1349
+ router.get('/marketplace/stats', (req, res) => {
1350
+ res.json(marketplace.getStats());
1351
+ });
1352
+
1353
+ // ═══════════════════════════════════════════════════════════════════════════
1354
+ // HOSTED RUNTIME
1355
+ // ═══════════════════════════════════════════════════════════════════════════
1356
+
1357
+ /**
1358
+ * Launch hosted instance
1359
+ */
1360
+ router.post('/hosted/launch', (req, res) => {
1361
+ try {
1362
+ const instance = hostedRuntime.launch({
1363
+ agentId: req.agentId || req.body.agentId,
1364
+ tier: req.agentTier || req.session?.tier || 'starter',
1365
+ region: req.body.region,
1366
+ cpu: req.body.cpu,
1367
+ memory: req.body.memory,
1368
+ timeout: req.body.timeout,
1369
+ });
1370
+ res.json(instance);
1371
+ } catch (err) {
1372
+ res.status(400).json({ error: err.message });
1373
+ }
1374
+ });
1375
+
1376
+ /**
1377
+ * Execute on hosted instance
1378
+ */
1379
+ router.post('/hosted/:instanceId/execute', async (req, res) => {
1380
+ try {
1381
+ const execution = await hostedRuntime.execute(req.params.instanceId, req.body);
1382
+ res.json(execution);
1383
+ } catch (err) {
1384
+ res.status(400).json({ error: err.message });
1385
+ }
1386
+ });
1387
+
1388
+ /**
1389
+ * Complete execution
1390
+ */
1391
+ router.post('/hosted/executions/:executionId/complete', (req, res) => {
1392
+ const execution = hostedRuntime.completeExecution(
1393
+ req.params.executionId,
1394
+ req.body.result,
1395
+ req.body.error ? new Error(req.body.error) : null
1396
+ );
1397
+ if (!execution) return res.status(404).json({ error: 'Execution not found' });
1398
+ res.json(execution);
1399
+ });
1400
+
1401
+ /**
1402
+ * Stop hosted instance
1403
+ */
1404
+ router.post('/hosted/:instanceId/stop', (req, res) => {
1405
+ const success = hostedRuntime.stop(req.params.instanceId);
1406
+ res.json({ success });
1407
+ });
1408
+
1409
+ /**
1410
+ * Get hosted instance
1411
+ */
1412
+ router.get('/hosted/:instanceId', (req, res) => {
1413
+ const instance = hostedRuntime.getInstance(req.params.instanceId);
1414
+ if (!instance) return res.status(404).json({ error: 'Instance not found' });
1415
+ res.json(instance);
1416
+ });
1417
+
1418
+ /**
1419
+ * List instances
1420
+ */
1421
+ router.get('/hosted', (req, res) => {
1422
+ const instances = hostedRuntime.listInstances({
1423
+ agentId: req.query.agentId,
1424
+ status: req.query.status,
1425
+ region: req.query.region,
1426
+ }, parseInt(req.query.limit) || 50);
1427
+ res.json({ instances, total: instances.length });
1428
+ });
1429
+
1430
+ /**
1431
+ * List executions for instance
1432
+ */
1433
+ router.get('/hosted/:instanceId/executions', (req, res) => {
1434
+ const executions = hostedRuntime.listExecutions(
1435
+ req.params.instanceId,
1436
+ parseInt(req.query.limit) || 50
1437
+ );
1438
+ res.json({ executions, total: executions.length });
1439
+ });
1440
+
1441
+ /**
1442
+ * Get compute usage
1443
+ */
1444
+ router.get('/hosted/usage/:agentId', (req, res) => {
1445
+ res.json(hostedRuntime.getComputeUsage(req.params.agentId));
1446
+ });
1447
+
1448
+ /**
1449
+ * Hosted runtime stats
1450
+ */
1451
+ router.get('/hosted/stats', (req, res) => {
1452
+ res.json(hostedRuntime.getStats());
1453
+ });
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
+
1136
1749
  module.exports = router;