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 +270 -16
- package/dist/index.js.map +1 -1
- package/dist/runners/browser_interaction.d.ts +16 -0
- package/dist/runners/browser_interaction.d.ts.map +1 -1
- package/dist/runners/browser_interaction.js +55 -7
- package/dist/runners/browser_interaction.js.map +1 -1
- package/dist/runners/video_recorder.d.ts +2 -0
- package/dist/runners/video_recorder.d.ts.map +1 -1
- package/dist/runners/video_recorder.js +7 -0
- package/dist/runners/video_recorder.js.map +1 -1
- package/dist/runners/window_manager.d.ts +2 -0
- package/dist/runners/window_manager.d.ts.map +1 -1
- package/dist/runners/window_manager.js +13 -0
- package/dist/runners/window_manager.js.map +1 -1
- package/dist/tools/gate_check.d.ts.map +1 -1
- package/dist/tools/gate_check.js +1 -1
- package/dist/tools/gate_check.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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
|
-
|
|
1362
|
-
const
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|