codeloop-mcp-server 0.1.9 → 0.1.11

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/dist/index.js CHANGED
@@ -44,7 +44,7 @@ const config = loadConfig(projectDir);
44
44
  const apiKey = process.env.CODELOOP_API_KEY || config.api_key;
45
45
  const server = new McpServer({
46
46
  name: "codeloop",
47
- version: "0.1.5",
47
+ version: "0.1.10",
48
48
  });
49
49
  async function withAuth(fn) {
50
50
  const result = await validateApiKey(apiKey);
@@ -867,6 +867,31 @@ The agent MUST then write the report to docs/DEVELOPMENT_LOG.md and present it t
867
867
  }
868
868
  }
869
869
  }
870
+ // Parse interaction_log.jsonl from each run for the interaction history
871
+ const interactionHistory = [];
872
+ const interactionSummary = { total: 0, succeeded: 0, failed: 0, actions: {}, console_errors: 0 };
873
+ for (const runId of runs) {
874
+ const runDir = getRunDir(runId, baseDir);
875
+ const iLogPath = join(runDir, "logs", "interaction_log.jsonl");
876
+ if (existsSync(iLogPath)) {
877
+ const lines = readFileSync(iLogPath, "utf-8").trim().split("\n").filter(l => l.length > 0);
878
+ for (const line of lines) {
879
+ try {
880
+ const entry = JSON.parse(line);
881
+ interactionHistory.push({ run_id: runId, ...entry });
882
+ interactionSummary.total++;
883
+ if (entry.success)
884
+ interactionSummary.succeeded++;
885
+ else
886
+ interactionSummary.failed++;
887
+ interactionSummary.actions[entry.action] = (interactionSummary.actions[entry.action] || 0) + 1;
888
+ if (entry.console_errors?.length)
889
+ interactionSummary.console_errors += entry.console_errors.length;
890
+ }
891
+ catch { /* skip malformed lines */ }
892
+ }
893
+ }
894
+ }
870
895
  // Check for interaction replay results
871
896
  const replayDir = join(baseDir, "replay_frames");
872
897
  const hasReplayFrames = existsSync(replayDir) && readdirSync(replayDir).length > 0;
@@ -888,6 +913,8 @@ The agent MUST then write the report to docs/DEVELOPMENT_LOG.md and present it t
888
913
  log_files: logFiles,
889
914
  errors_found_in_logs: errorsFound,
890
915
  interaction_replay_performed: hasReplayFrames,
916
+ interaction_summary: interactionSummary,
917
+ interaction_history: interactionHistory.slice(-50),
891
918
  run_timeline: runSummaries,
892
919
  };
893
920
  await trackUsage(apiKey, "verification_run");
@@ -936,6 +963,10 @@ For EACH video recording session, document:
936
963
  - What issues were found in the extracted frames
937
964
  - How those issues were fixed
938
965
  - Which OS and interaction method was used (osascript, Playwright, adb, xdotool, PowerShell)
966
+ - Interaction summary: ${report.interaction_summary?.total || 0} total interactions (${report.interaction_summary?.succeeded || 0} succeeded, ${report.interaction_summary?.failed || 0} failed)
967
+ - Action breakdown: ${JSON.stringify(report.interaction_summary?.actions || {})}
968
+ - Browser console errors captured during interactions: ${report.interaction_summary?.console_errors || 0}
969
+ - Include a table of key interactions with their input args, success/failure, and any console errors
939
970
 
940
971
  **6. Quality Gates Passed**
941
972
  - Build passes
@@ -1033,6 +1064,46 @@ Returns: checklist of completed and pending verification steps.`, {
1033
1064
  }
1034
1065
  const gateIsPassing = hasGateCheck && latestMeta?.gate_result === "passed";
1035
1066
  const gateConfidence = latestMeta?.confidence ?? 0;
1067
+ // Interaction coverage: compare interaction_log selectors/URLs against discover_screens
1068
+ let interactionCount = 0;
1069
+ const coveredRoutes = new Set();
1070
+ let discoveredRoutes = [];
1071
+ let uncoveredRoutes = [];
1072
+ if (isUIProject) {
1073
+ try {
1074
+ const { readFileSync: rfs } = await import("fs");
1075
+ // Collect all interaction entries across runs
1076
+ for (const rid of runs) {
1077
+ const runDir = getRunDir(rid, baseDir);
1078
+ const iLogPath = join(runDir, "logs", "interaction_log.jsonl");
1079
+ if (existsSync(iLogPath)) {
1080
+ const lines = rfs(iLogPath, "utf-8").trim().split("\n").filter(l => l.length > 0);
1081
+ for (const line of lines) {
1082
+ try {
1083
+ const entry = JSON.parse(line);
1084
+ interactionCount++;
1085
+ const args = entry.input_args || {};
1086
+ if (args.url)
1087
+ coveredRoutes.add(new URL(args.url, "http://localhost").pathname);
1088
+ if (args.selector)
1089
+ coveredRoutes.add(args.selector);
1090
+ if (entry.action === "navigate_url" && args.url)
1091
+ coveredRoutes.add(args.url);
1092
+ }
1093
+ catch { /* skip malformed lines */ }
1094
+ }
1095
+ }
1096
+ }
1097
+ // Compare against discover_screens
1098
+ const { discoverScreens } = await import("./tools/discover_screens.js");
1099
+ const screenResult = discoverScreens(cwd, platform);
1100
+ discoveredRoutes = screenResult.screens.map(s => s.route);
1101
+ uncoveredRoutes = discoveredRoutes.filter(route => {
1102
+ return !coveredRoutes.has(route) && ![...coveredRoutes].some(c => c.includes(route) || route.includes(c));
1103
+ });
1104
+ }
1105
+ catch { /* best-effort coverage check */ }
1106
+ }
1036
1107
  const steps = [
1037
1108
  {
1038
1109
  step: "1. codeloop_verify",
@@ -1075,6 +1146,23 @@ Returns: checklist of completed and pending verification steps.`, {
1075
1146
  ? "docs/DEVELOPMENT_LOG.md exists"
1076
1147
  : "No development log found. Call codeloop_generate_dev_report and write docs/DEVELOPMENT_LOG.md.",
1077
1148
  },
1149
+ {
1150
+ step: "6. Interaction coverage",
1151
+ status: !isUIProject
1152
+ ? "n/a"
1153
+ : discoveredRoutes.length === 0
1154
+ ? "n/a"
1155
+ : uncoveredRoutes.length === 0
1156
+ ? "done"
1157
+ : "PENDING",
1158
+ detail: !isUIProject
1159
+ ? "Not a UI project — interaction coverage not applicable"
1160
+ : discoveredRoutes.length === 0
1161
+ ? "No screens discovered from source code (run codeloop_discover_screens first)"
1162
+ : uncoveredRoutes.length === 0
1163
+ ? `All ${discoveredRoutes.length} discovered routes covered by ${interactionCount} interactions`
1164
+ : `${uncoveredRoutes.length} of ${discoveredRoutes.length} routes NOT covered by interactions. Uncovered: ${uncoveredRoutes.join(", ")}. Navigate to these routes and interact with their elements.`,
1165
+ },
1078
1166
  ];
1079
1167
  const pendingSteps = steps.filter(s => s.status === "PENDING" || s.status === "NEEDS_RERUN");
1080
1168
  const allDone = pendingSteps.length === 0;
@@ -1207,7 +1295,13 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
1207
1295
  success = await wm.simctlTap(params.x, params.y);
1208
1296
  }
1209
1297
  else if (params.selector) {
1210
- success = await wm.clickElementInBrowser(params.selector);
1298
+ if (process.platform === "win32") {
1299
+ const wa = await import("./runners/win_accessibility.js");
1300
+ success = await wa.automateElement(params.app_name || "", params.selector, "invoke");
1301
+ }
1302
+ else {
1303
+ success = await wm.clickElementInBrowser(params.selector);
1304
+ }
1211
1305
  }
1212
1306
  else if (params.x != null && params.y != null) {
1213
1307
  success = await wm.clickAtPosition(params.x, params.y);
@@ -1270,6 +1364,21 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
1270
1364
  };
1271
1365
  success = await wm.adbKey(adbKeyMap[params.key.toLowerCase()] || `KEYCODE_${params.key.toUpperCase()}`);
1272
1366
  }
1367
+ else if (tt === "ios_simulator") {
1368
+ const simKeyMap = {
1369
+ enter: "return", tab: "tab", escape: "escape", backspace: "delete",
1370
+ delete: "forwardDelete", up: "up-arrow", down: "down-arrow",
1371
+ left: "left-arrow", right: "right-arrow", space: "space",
1372
+ home: "home", end: "end",
1373
+ };
1374
+ const mapped = simKeyMap[params.key.toLowerCase()];
1375
+ if (mapped) {
1376
+ success = await wm.simctlKey(mapped);
1377
+ }
1378
+ else {
1379
+ success = await wm.simctlType(params.key);
1380
+ }
1381
+ }
1273
1382
  else {
1274
1383
  success = await wm.sendKeyByName(params.key);
1275
1384
  }
@@ -1298,6 +1407,14 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
1298
1407
  const ex = dir === "left" ? sx + 600 : dir === "right" ? sx - 600 : sx;
1299
1408
  success = await wm.adbSwipe(sx, sy, ex, ey, 300);
1300
1409
  }
1410
+ else if (tt === "ios_simulator") {
1411
+ const dir = params.direction || "down";
1412
+ const sx = params.x || 195, sy = params.y || 420;
1413
+ const dist = params.amount || 300;
1414
+ const ey = dir === "down" ? sy - dist : dir === "up" ? sy + dist : sy;
1415
+ const ex = dir === "left" ? sx + dist : dir === "right" ? sx - dist : sx;
1416
+ success = await wm.simctlSwipe(sx, sy, ex, ey);
1417
+ }
1301
1418
  else {
1302
1419
  success = await wm.scrollAtPosition(params.x || 500, params.y || 400, params.direction || "down", params.amount || 3);
1303
1420
  }
@@ -1358,15 +1475,30 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
1358
1475
  }
1359
1476
  else if (params.fields) {
1360
1477
  let allOk = true;
1361
- for (const field of params.fields) {
1362
- const filled = await wm.fillFieldInBrowser(field.selector, field.value);
1363
- if (!filled)
1364
- allOk = false;
1365
- await new Promise(r => setTimeout(r, 300));
1478
+ if (process.platform === "win32") {
1479
+ const wa = await import("./runners/win_accessibility.js");
1480
+ for (const field of params.fields) {
1481
+ const filled = await wa.automateElement(params.app_name || "", field.selector, "setValue", field.value);
1482
+ if (!filled)
1483
+ allOk = false;
1484
+ await new Promise(r => setTimeout(r, 300));
1485
+ }
1486
+ if (params.submit_selector) {
1487
+ await new Promise(r => setTimeout(r, 500));
1488
+ await wa.automateElement(params.app_name || "", params.submit_selector, "invoke");
1489
+ }
1366
1490
  }
1367
- if (params.submit_selector) {
1368
- await new Promise(r => setTimeout(r, 500));
1369
- await wm.clickElementInBrowser(params.submit_selector);
1491
+ else {
1492
+ for (const field of params.fields) {
1493
+ const filled = await wm.fillFieldInBrowser(field.selector, field.value);
1494
+ if (!filled)
1495
+ allOk = false;
1496
+ await new Promise(r => setTimeout(r, 300));
1497
+ }
1498
+ if (params.submit_selector) {
1499
+ await new Promise(r => setTimeout(r, 500));
1500
+ await wm.clickElementInBrowser(params.submit_selector);
1501
+ }
1370
1502
  }
1371
1503
  success = allOk;
1372
1504
  }
@@ -1419,7 +1551,8 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
1419
1551
  success = await bi.browserGoBack();
1420
1552
  }
1421
1553
  else {
1422
- success = await wm.sendHotkey("cmd+[");
1554
+ const backKey = process.platform === "darwin" ? "cmd+[" : "alt+left";
1555
+ success = await wm.sendHotkey(backKey);
1423
1556
  }
1424
1557
  detail = "navigate_back";
1425
1558
  break;
@@ -1427,6 +1560,10 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
1427
1560
  if (tt === "browser") {
1428
1561
  success = await bi.browserGoForward();
1429
1562
  }
1563
+ else {
1564
+ const fwdKey = process.platform === "darwin" ? "cmd+]" : "alt+right";
1565
+ success = await wm.sendHotkey(fwdKey);
1566
+ }
1430
1567
  detail = "navigate_forward";
1431
1568
  break;
1432
1569
  case "wait":
@@ -1537,16 +1674,67 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
1537
1674
  break;
1538
1675
  case "sequence":
1539
1676
  if (params.steps) {
1540
- const allOk = true;
1677
+ let allOk = true;
1678
+ const stepResults = [];
1541
1679
  for (const step of params.steps) {
1542
- const stepParams = { ...step.params, action: step.action, target_type: tt };
1543
- // Recursive call handled by dispatching the same tool logic
1544
- // For simplicity, just dispatch core actions inline
1545
1680
  if (step.delay_ms)
1546
1681
  await new Promise(r => setTimeout(r, step.delay_ms));
1682
+ const stepAction = step.action;
1683
+ const sp = (step.params || {});
1684
+ let stepOk = false;
1685
+ try {
1686
+ if (stepAction === "click" && tt === "browser" && sp.selector) {
1687
+ stepOk = await bi.browserClick(sp.selector);
1688
+ }
1689
+ else if (stepAction === "click" && sp.x != null && sp.y != null) {
1690
+ stepOk = await wm.clickAtPosition(sp.x, sp.y);
1691
+ }
1692
+ else if (stepAction === "type" && tt === "browser" && sp.selector && sp.text) {
1693
+ stepOk = await bi.browserType(sp.selector, sp.text);
1694
+ }
1695
+ else if (stepAction === "type" && sp.text) {
1696
+ stepOk = await wm.typeText(sp.text);
1697
+ }
1698
+ else if (stepAction === "fill_form" && tt === "browser" && sp.fields) {
1699
+ stepOk = await bi.browserFillForm(sp.fields, sp.submit_selector);
1700
+ }
1701
+ else if (stepAction === "navigate_url" && sp.url) {
1702
+ if (tt === "browser")
1703
+ stepOk = await bi.browserNavigate(sp.url);
1704
+ else
1705
+ stepOk = await wm.navigateDesktopBrowser(sp.url);
1706
+ }
1707
+ else if (stepAction === "hotkey" && sp.keys) {
1708
+ stepOk = tt === "browser" ? await bi.browserHotkey(sp.keys) : await wm.sendHotkey(sp.keys);
1709
+ }
1710
+ else if (stepAction === "keystroke" && sp.key) {
1711
+ stepOk = tt === "browser" ? await bi.browserKeystroke(sp.key) : await wm.sendKeyByName(sp.key);
1712
+ }
1713
+ else if (stepAction === "scroll") {
1714
+ stepOk = tt === "browser"
1715
+ ? await bi.browserScroll(sp.direction || "down", sp.amount || 300)
1716
+ : await wm.scrollAtPosition(sp.x || 500, sp.y || 400, sp.direction || "down", sp.amount || 3);
1717
+ }
1718
+ else if (stepAction === "wait") {
1719
+ await new Promise(r => setTimeout(r, sp.duration_ms || 1000));
1720
+ stepOk = true;
1721
+ }
1722
+ else if (stepAction === "hover" && tt === "browser" && sp.selector) {
1723
+ stepOk = await bi.browserHover(sp.selector);
1724
+ }
1725
+ else if (stepAction === "double_click" && tt === "browser" && sp.selector) {
1726
+ stepOk = await bi.browserDoubleClick(sp.selector);
1727
+ }
1728
+ }
1729
+ catch {
1730
+ stepOk = false;
1731
+ }
1732
+ stepResults.push({ action: stepAction, success: stepOk });
1733
+ if (!stepOk)
1734
+ allOk = false;
1547
1735
  }
1548
1736
  success = allOk;
1549
- detail = `sequence (${params.steps.length} steps)`;
1737
+ detail = `sequence (${params.steps.length} steps, ${stepResults.filter(s => s.success).length} passed)`;
1550
1738
  }
1551
1739
  break;
1552
1740
  default:
@@ -1554,6 +1742,72 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
1554
1742
  return { success: false, action, detail };
1555
1743
  }
1556
1744
  await trackUsage(apiKey, "interaction");
1745
+ // Build structured input args for the log entry
1746
+ const inputArgs = {};
1747
+ if (params.selector)
1748
+ inputArgs.selector = params.selector;
1749
+ if (params.x != null)
1750
+ inputArgs.x = params.x;
1751
+ if (params.y != null)
1752
+ inputArgs.y = params.y;
1753
+ if (params.x2 != null)
1754
+ inputArgs.x2 = params.x2;
1755
+ if (params.y2 != null)
1756
+ inputArgs.y2 = params.y2;
1757
+ if (params.text)
1758
+ inputArgs.text = params.text.substring(0, 200);
1759
+ if (params.key)
1760
+ inputArgs.key = params.key;
1761
+ if (params.keys)
1762
+ inputArgs.keys = params.keys;
1763
+ if (params.url)
1764
+ inputArgs.url = params.url;
1765
+ if (params.value)
1766
+ inputArgs.value = params.value;
1767
+ if (params.direction)
1768
+ inputArgs.direction = params.direction;
1769
+ if (params.amount != null)
1770
+ inputArgs.amount = params.amount;
1771
+ if (params.fields)
1772
+ inputArgs.field_count = params.fields.length;
1773
+ if (params.submit_selector)
1774
+ inputArgs.submit_selector = params.submit_selector;
1775
+ if (params.duration_ms != null)
1776
+ inputArgs.duration_ms = params.duration_ms;
1777
+ if (params.steps)
1778
+ inputArgs.step_count = params.steps.length;
1779
+ if (params.app_name)
1780
+ inputArgs.app_name = params.app_name;
1781
+ // Drain browser console errors that occurred during this interaction
1782
+ const consoleErrors = tt === "browser" ? bi.drainRecentConsoleErrors() : [];
1783
+ // Log interaction result for post-recording analysis
1784
+ const interactionEntry = {
1785
+ timestamp: new Date().toISOString(),
1786
+ action,
1787
+ target_type: tt,
1788
+ success,
1789
+ detail,
1790
+ input_args: inputArgs,
1791
+ };
1792
+ if (consoleErrors.length > 0)
1793
+ interactionEntry.console_errors = consoleErrors;
1794
+ try {
1795
+ const activeIds = vr.getActiveRecordingIds();
1796
+ if (activeIds.length > 0) {
1797
+ const { appendFileSync, mkdirSync } = await import("fs");
1798
+ const { dirname } = await import("path");
1799
+ for (const rid of activeIds) {
1800
+ const logPath = vr.getActiveRecordingLogPath(rid);
1801
+ if (logPath) {
1802
+ const logDir = dirname(logPath);
1803
+ mkdirSync(logDir, { recursive: true });
1804
+ const logFile = `${logDir}/interaction_log.jsonl`;
1805
+ appendFileSync(logFile, JSON.stringify(interactionEntry) + "\n");
1806
+ }
1807
+ }
1808
+ }
1809
+ }
1810
+ catch { /* best-effort logging */ }
1557
1811
  return { success, action, detail };
1558
1812
  });
1559
1813
  return {