codeloop-mcp-server 0.1.65 → 0.1.67

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.
Files changed (35) hide show
  1. package/dist/auth/critical_floors.d.ts.map +1 -1
  2. package/dist/auth/critical_floors.js +8 -0
  3. package/dist/auth/critical_floors.js.map +1 -1
  4. package/dist/evidence/agent_mode.d.ts +22 -0
  5. package/dist/evidence/agent_mode.d.ts.map +1 -0
  6. package/dist/evidence/agent_mode.js +113 -0
  7. package/dist/evidence/agent_mode.js.map +1 -0
  8. package/dist/evidence/interaction_coverage.d.ts.map +1 -1
  9. package/dist/evidence/interaction_coverage.js +2 -1
  10. package/dist/evidence/interaction_coverage.js.map +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +154 -15
  13. package/dist/index.js.map +1 -1
  14. package/dist/runners/base.d.ts.map +1 -1
  15. package/dist/runners/base.js +86 -14
  16. package/dist/runners/base.js.map +1 -1
  17. package/dist/runners/browser_interaction.d.ts +8 -0
  18. package/dist/runners/browser_interaction.d.ts.map +1 -1
  19. package/dist/runners/browser_interaction.js +24 -0
  20. package/dist/runners/browser_interaction.js.map +1 -1
  21. package/dist/runners/device_probe.d.ts +38 -0
  22. package/dist/runners/device_probe.d.ts.map +1 -0
  23. package/dist/runners/device_probe.js +143 -0
  24. package/dist/runners/device_probe.js.map +1 -0
  25. package/dist/runners/flutter.d.ts.map +1 -1
  26. package/dist/runners/flutter.js +104 -3
  27. package/dist/runners/flutter.js.map +1 -1
  28. package/dist/runners/window_manager.d.ts +18 -0
  29. package/dist/runners/window_manager.d.ts.map +1 -1
  30. package/dist/runners/window_manager.js +66 -0
  31. package/dist/runners/window_manager.js.map +1 -1
  32. package/dist/tools/verify.d.ts.map +1 -1
  33. package/dist/tools/verify.js +64 -4
  34. package/dist/tools/verify.js.map +1 -1
  35. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { z } from "zod";
5
5
  import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from "fs";
6
6
  import { slugForTargetChangeEntry } from "./tools/c7_slug.js";
7
+ import { resolveAgentMode, MODE_PARAM_DESCRIPTION as AGENT_MODE_PARAM_DESC, } from "./evidence/agent_mode.js";
7
8
  function dirHasFile(dir, predicate) {
8
9
  try {
9
10
  if (!existsSync(dir))
@@ -605,10 +606,12 @@ Returns: structured report with pass/fail counts, artifact paths, and next-step
605
606
  project_dir: z.string().optional().describe("Absolute path to the project root. Defaults to CODELOOP_PROJECT_DIR env var or auto-discovered project directory. MUST be an actual project folder — passing the user's home directory is rejected. If your IDE launches the MCP server from the wrong cwd (common on Windows where Cursor uses C:\\Users\\<name> as cwd), set CODELOOP_PROJECT_DIR or pass this param explicitly."),
606
607
  workspace_root: z.string().optional().describe("[Alias for project_dir] Same semantics; accepted because many agents reach for this conventional name. Pass either `project_dir` OR `workspace_root` — they're equivalent."),
607
608
  tasks_completed: z.array(z.string()).optional().describe("0.1.52 C5 — free-text titles of the tasks the agent claims to have completed in this code change. Cross-checked against the change manifest produced by C1: every claim should map to >= 1 manifest entry and every manifest entry should map to >= 1 claim. Mismatches surface as warnings in the verify response and feed the change_coverage_evidence gate (C3)."),
609
+ mode: z.string().optional().describe(AGENT_MODE_PARAM_DESC),
608
610
  }, async (params) => {
609
611
  const cwd = resolveCwd(params);
610
612
  const explicitDir = params.project_dir || params.workspace_root;
611
613
  const cfg = explicitDir ? loadConfig(explicitDir) : config;
614
+ const auditMode = resolveAgentMode({ cwd, paramMode: params.mode, configMode: cfg.agent_mode }) === "audit";
612
615
  const result = await withAuth(async () => {
613
616
  const { runVerify } = await import("./tools/verify.js");
614
617
  const input = {
@@ -638,7 +641,12 @@ Returns: structured report with pass/fail counts, artifact paths, and next-step
638
641
  try {
639
642
  const { isUIProject } = await import("./tools/gate_check.js");
640
643
  const verifyResult = result;
641
- if (verifyResult.run_id &&
644
+ if (auditMode) {
645
+ // Read-only: the user asked for a problem list, not an auto-fix loop.
646
+ const { buildAuditDirective } = await import("./evidence/agent_mode.js");
647
+ postscript = buildAuditDirective("verify");
648
+ }
649
+ else if (verifyResult.run_id &&
642
650
  (verifyResult.fail_count ?? 0) === 0 &&
643
651
  isUIProject(cwd)) {
644
652
  const { getArtifactsBaseDir, getRunDir, listRuns } = await import("./evidence/artifacts.js");
@@ -693,7 +701,10 @@ Returns: categorized issues with severity, evidence, root cause, and actionable
693
701
  focus_files: z.array(z.string()).optional(),
694
702
  project_dir: z.string().optional().describe("Absolute path to the project root. Defaults to CODELOOP_PROJECT_DIR env var or auto-discovered project directory. MUST be an actual project folder — passing the user's home directory is rejected. If your IDE launches the MCP server from the wrong cwd (common on Windows where Cursor uses C:\\Users\\<name> as cwd), set CODELOOP_PROJECT_DIR or pass this param explicitly."),
695
703
  workspace_root: z.string().optional().describe("[Alias for project_dir] Same semantics; accepted because many agents reach for this conventional name. Pass either `project_dir` OR `workspace_root` — they're equivalent."),
704
+ mode: z.string().optional().describe(AGENT_MODE_PARAM_DESC),
696
705
  }, async (params) => {
706
+ const diagnoseCwd = resolveCwd(params);
707
+ const auditMode = resolveAgentMode({ cwd: diagnoseCwd, paramMode: params.mode, configMode: config.agent_mode }) === "audit";
697
708
  const result = await withAuth(async () => {
698
709
  const { runDiagnose } = await import("./tools/diagnose.js");
699
710
  const input = {
@@ -711,17 +722,24 @@ Returns: categorized issues with severity, evidence, root cause, and actionable
711
722
  // result already enumerates the work; the agent must pick them up
712
723
  // and execute, not stop to ask "which would you like me to start
713
724
  // with?".
714
- const fixDirective = [
715
- "",
716
- "",
717
- "⚠️ EXECUTE REPAIR TASKS NOW — do not ask the user which one to start with ⚠️",
718
- "The repair_tasks above are the agent's TODO list, in order. For EACH task:",
719
- " 1. Modify ONLY the files listed in allowed_file_scope (do not refactor adjacent code).",
720
- " 2. After all tasks for a single issue are applied, call codeloop_verify again.",
721
- " 3. If new failures surface, call codeloop_diagnose again (max 8 attempts before escalating).",
722
- " 4. When verify passes, continue the loop to codeloop_gate_check.",
723
- "Do NOT report partial progress to the user between iterations. Do NOT pause to confirm the plan — the diagnose output IS the plan.",
724
- ].join("\n");
725
+ let fixDirective;
726
+ if (auditMode) {
727
+ const { buildAuditDirective } = await import("./evidence/agent_mode.js");
728
+ fixDirective = buildAuditDirective("diagnose");
729
+ }
730
+ else {
731
+ fixDirective = [
732
+ "",
733
+ "",
734
+ "⚠️ EXECUTE REPAIR TASKS NOW do not ask the user which one to start with ⚠️",
735
+ "The repair_tasks above are the agent's TODO list, in order. For EACH task:",
736
+ " 1. Modify ONLY the files listed in allowed_file_scope (do not refactor adjacent code).",
737
+ " 2. After all tasks for a single issue are applied, call codeloop_verify again.",
738
+ " 3. If new failures surface, call codeloop_diagnose again (max 8 attempts before escalating).",
739
+ " 4. When verify passes, continue the loop to codeloop_gate_check.",
740
+ "Do NOT report partial progress to the user between iterations. Do NOT pause to confirm the plan — the diagnose output IS the plan.",
741
+ ].join("\n");
742
+ }
725
743
  return {
726
744
  content: withInitHint([{ type: "text", text: JSON.stringify(result, null, 2) + fixDirective }]),
727
745
  };
@@ -761,7 +779,9 @@ Returns: pass/fail for each gate, overall confidence score, and recommendation.`
761
779
  project_dir: z.string().optional().describe("Absolute path to the project root. Defaults to CODELOOP_PROJECT_DIR env var or auto-discovered project directory. MUST be an actual project folder — passing the user's home directory is rejected. If your IDE launches the MCP server from the wrong cwd (common on Windows where Cursor uses C:\\Users\\<name> as cwd), set CODELOOP_PROJECT_DIR or pass this param explicitly."),
762
780
  workspace_root: z.string().optional().describe("[Alias for project_dir] Same semantics; accepted because many agents reach for this conventional name. Pass either `project_dir` OR `workspace_root` — they're equivalent."),
763
781
  recent_thinking: z.string().optional().describe("0.1.52 C6 — optional dump of the agent's recent thinking / rationale (last few turns of the loop). When present, the gate scans it for anti-rationalisation phrases ('comprehensive verification confirms', 'further interaction would be redundant', 'grid is empty so can't test', etc.) and surfaces specific matches in the continue_fixing postscript so the agent stops repeating the rationalisation and acts on the per-gate next steps instead. Safe to omit — the canonical FORBIDDEN list still ships in the directive without a hit."),
782
+ mode: z.string().optional().describe(AGENT_MODE_PARAM_DESC),
764
783
  }, async (params) => {
784
+ const gateAuditMode = resolveAgentMode({ cwd: resolveCwd(params), paramMode: params.mode, configMode: config.agent_mode }) === "audit";
765
785
  const result = await withAuth(async () => {
766
786
  const { runGateCheck } = await import("./tools/gate_check.js");
767
787
  const input = {
@@ -792,6 +812,14 @@ Returns: pass/fail for each gate, overall confidence score, and recommendation.`
792
812
  }, { tool: "codeloop_gate_check", cwd: resolveCwd(params), input: params });
793
813
  const resultJson = JSON.stringify(result, null, 2);
794
814
  const gateResult = result;
815
+ if (gateAuditMode) {
816
+ // Read-only: report gate results as an audit summary; do not push the
817
+ // auto-fix loop even when gates fail.
818
+ const { buildAuditDirective } = await import("./evidence/agent_mode.js");
819
+ return {
820
+ content: withInitHint([{ type: "text", text: resultJson + buildAuditDirective("gate_check") }], resolveCwd(params)),
821
+ };
822
+ }
795
823
  if (gateResult.recommendation === "continue_fixing") {
796
824
  // Per-gate next-step enumeration. The auto-fix loop's biggest
797
825
  // failure mode was the generic directive ("call verify, diagnose,
@@ -1421,6 +1449,7 @@ Returns: confirmation + the captured image as an MCP ImageContent block so you c
1421
1449
  project_dir: z.string().optional().describe("Absolute path to the project root. Defaults to CODELOOP_PROJECT_DIR env var or auto-discovered project directory. MUST be an actual project folder — passing the user's home directory is rejected. If your IDE launches the MCP server from the wrong cwd (common on Windows where Cursor uses C:\\Users\\<name> as cwd), set CODELOOP_PROJECT_DIR or pass this param explicitly."),
1422
1450
  workspace_root: z.string().optional().describe("[Alias for project_dir] Same semantics; accepted because many agents reach for this conventional name. Pass either `project_dir` OR `workspace_root` — they're equivalent."),
1423
1451
  target_change_entry: z.string().optional().describe("0.1.52 C7 — verbatim display name of the change-manifest entry this screenshot exercises (e.g. 'datagrid_column: \"Product Code\"' or 'PhotometricConfigurations.ProductCode'). When present, the screenshot file is auto-anchored: the filename is prefixed with a slugged form of the entry so the change_coverage_evidence (C3) gate's screenshot scan can credit this evidence to the correct manifest entry without fuzzy matching. The value is also persisted alongside the screenshot path in the response so downstream tools (interaction_replay, gate_check) can use it."),
1452
+ target_type: z.string().optional().describe("Capture source. Omit for desktop/window capture (default). Set 'android_emulator' (or 'android') to capture the BOOTED Android emulator via adb, or 'ios_simulator' (or 'ios', macOS only) to capture the booted iOS simulator via simctl. Use this to screenshot a Flutter/native mobile app running on a simulator instead of the host desktop."),
1424
1453
  }, async (params) => {
1425
1454
  const authResult = await withAuth(async () => {
1426
1455
  const { captureScreenshot } = await import("./runners/screenshot.js");
@@ -1462,7 +1491,14 @@ Returns: confirmation + the captured image as an MCP ImageContent block so you c
1462
1491
  const finalScreenName = targetChangeEntry
1463
1492
  ? `${params.screen_name}--c7-${slugForTargetChangeEntry(targetChangeEntry)}`
1464
1493
  : params.screen_name;
1465
- const result = await captureScreenshot(screenshotsDir, finalScreenName, targetApp, undefined, { desktopAppMode: desktopApp });
1494
+ // Mobile capture: when the agent asks for an emulator/simulator
1495
+ // screenshot, route to the adb/simctl capture path instead of the
1496
+ // desktop window grab (and skip the desktop-app honesty refusal, which
1497
+ // only applies to host-window capture).
1498
+ const ttRaw = normalizeTargetType(params.target_type);
1499
+ const explicitTargetType = ttRaw === "android_emulator" || ttRaw === "ios_simulator" ? ttRaw : undefined;
1500
+ const isMobileCapture = explicitTargetType !== undefined;
1501
+ const result = await captureScreenshot(screenshotsDir, finalScreenName, isMobileCapture ? undefined : targetApp, explicitTargetType, { desktopAppMode: isMobileCapture ? false : desktopApp });
1466
1502
  // Photometry-DB E2E 8 follow-on: when we capture a desktop app
1467
1503
  // window, also resolve its on-screen bounds so the agent can
1468
1504
  // (a) compute window-relative coords from the returned image
@@ -1976,10 +2012,37 @@ App logs (stdout, logcat, simctl log) are automatically captured alongside the v
1976
2012
  }
1977
2013
  catch { /* best-effort */ }
1978
2014
  }
2015
+ // Mobile device-readiness probe. adb/simctl screen capture attaches to a
2016
+ // BOOTED emulator/simulator; if none is running the recording would fail
2017
+ // deep inside the recorder with a cryptic error. Probe first and, when
2018
+ // nothing is booted, push the agent to OPEN the right simulator(s) with
2019
+ // concrete commands rather than letting it guess. CodeLoop does not boot
2020
+ // devices itself (heavy + can hijack the desktop), it directs the agent.
2021
+ let deviceReadinessDirective;
2022
+ if (targetType === "android_emulator" || targetType === "ios_simulator") {
2023
+ try {
2024
+ const { probeBootedDevices, detectMobileTargets, buildOpenSimulatorsDirective } = await import("./runners/device_probe.js");
2025
+ const booted = await probeBootedDevices();
2026
+ const wantedBooted = targetType === "android_emulator"
2027
+ ? booted.android.length > 0
2028
+ : booted.ios.length > 0;
2029
+ if (!wantedBooted) {
2030
+ deviceReadinessDirective = buildOpenSimulatorsDirective({
2031
+ booted,
2032
+ targets: detectMobileTargets(cwd),
2033
+ context: "recording",
2034
+ });
2035
+ }
2036
+ }
2037
+ catch { /* best-effort */ }
2038
+ }
1979
2039
  const result = await startBackgroundRecording(videosDir, appName ?? "", params.max_duration_seconds, targetType);
1980
2040
  if (autoLaunchSummary) {
1981
2041
  result.auto_launch = autoLaunchSummary;
1982
2042
  }
2043
+ if (deviceReadinessDirective) {
2044
+ result.device_readiness = deviceReadinessDirective;
2045
+ }
1983
2046
  if (binaryFreshnessWarning) {
1984
2047
  result.binary_freshness_warning = binaryFreshnessWarning;
1985
2048
  result.binary_freshness = binaryFreshnessDetails;
@@ -2721,7 +2784,13 @@ pass app_name explicitly. This ensures interactions hit the app window, not the
2721
2784
 
2722
2785
  Core actions: click, double_click, right_click, hover, type, keystroke, hotkey, scroll,
2723
2786
  drag_drop, long_press, type_and_submit, type_and_tab, fill_form, select_option, toggle,
2724
- navigate_url, navigate_back, wait, sequence.
2787
+ navigate_url, navigate_back, wait, sequence, get_text.
2788
+ get_text (browser + android_emulator): reads back rendered text. Browser = Playwright
2789
+ textContent of a selector (or the whole page body); Android = uiautomator hierarchy text
2790
+ (selector acts as a text/resource-id substring filter). Use it to VERIFY/ANALYZE dynamic
2791
+ responses — e.g. after type_and_submit into a chatbox, get_text the assistant message to
2792
+ read the AI's answer and judge it. Pass expect_contains to assert the response contains an
2793
+ expected substring (success=false if not). iOS: use codeloop_capture_screenshot + vision.
2725
2794
  navigate_url works on ALL targets: browser (Playwright), desktop (opens Chrome/Safari via
2726
2795
  osascript/PowerShell/xdg-open), Android (adb deep link), iOS (simctl openurl).
2727
2796
  scroll works on desktop via CGEvent (macOS), user32 mouse_event (Windows), xdotool (Linux).
@@ -2735,7 +2804,8 @@ Windows: win_ui_inspect, win_ui_automate — PowerShell UI Automation for UWP/Wi
2735
2804
  MANDATORY for web apps: You MUST type into form fields, fill login/signup forms, test
2736
2805
  validation errors, and click submit buttons. Just navigating pages is NOT enough.
2737
2806
  Wait 1-2 seconds between interactions so video frames capture state changes.`, {
2738
- action: z.string().describe("Action to perform: click, double_click, right_click, hover, type, keystroke, hotkey, scroll, drag_drop, long_press, type_and_submit, type_and_tab, fill_form, select_option, toggle, upload_file, navigate_url, navigate_back, navigate_forward, wait, sequence, swipe, back_button, home_button, deep_link, grant_permission, rotate_device, biometric_auth, launch_app, clear_app_data, mock_location, simulate_network, maestro_flow, win_ui_inspect, win_ui_automate"),
2807
+ action: z.string().describe("Action to perform: click, double_click, right_click, hover, type, keystroke, hotkey, scroll, drag_drop, long_press, type_and_submit, type_and_tab, fill_form, select_option, toggle, get_text, upload_file, navigate_url, navigate_back, navigate_forward, wait, sequence, swipe, back_button, home_button, deep_link, grant_permission, rotate_device, biometric_auth, launch_app, clear_app_data, mock_location, simulate_network, maestro_flow, win_ui_inspect, win_ui_automate"),
2808
+ expect_contains: z.string().optional().describe("[get_text] Optional assertion: the read-back text must contain this substring (case-insensitive). When set, success is false if the substring is absent. Use to confirm an AI chatbot answer / dynamic response actually rendered the expected content."),
2739
2809
  target_type: targetTypeSchema.optional()
2740
2810
  .describe("Interaction target. Auto-detected if omitted. Accepts synonyms: `windows_desktop`/`mac_desktop`/`linux_desktop` → `desktop`; `web` → `browser`; `android` → `android_emulator`; `ios` → `ios_simulator`."),
2741
2811
  x: z.number().optional().describe("X coordinate for click/scroll/drag/swipe"),
@@ -2790,6 +2860,11 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
2790
2860
  const action = params.action;
2791
2861
  let success = false;
2792
2862
  let detail = "";
2863
+ // Text read back from the UI (get_text action) so the agent can analyze
2864
+ // dynamic responses — e.g. the AI chatbot's answer after submitting a
2865
+ // prompt. Surfaced in the response + interaction log as evidence.
2866
+ let extractedText = null;
2867
+ let assertion;
2793
2868
  // Import runners lazily
2794
2869
  const wm = await import("./runners/window_manager.js");
2795
2870
  const bi = await import("./runners/browser_interaction.js");
@@ -3051,6 +3126,62 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
3051
3126
  }
3052
3127
  detail = `type "${(params.text || "").substring(0, 50)}"`;
3053
3128
  break;
3129
+ case "get_text": {
3130
+ // Read back rendered text so the agent can verify/analyze a dynamic
3131
+ // response (e.g. an AI chatbot answer). Browser: Playwright
3132
+ // textContent of the selector (or <body> if omitted). Windows
3133
+ // desktop: the UIA value/name readback already runs below via
3134
+ // win_ui_automate; for a pure get_text on desktop, point the agent
3135
+ // at win_ui_inspect. Mobile text scraping is not yet supported.
3136
+ if (tt === "browser") {
3137
+ extractedText = await bi.browserGetText(params.selector);
3138
+ success = extractedText !== null;
3139
+ const preview = (extractedText ?? "").replace(/\s+/g, " ").trim().slice(0, 120);
3140
+ detail = success
3141
+ ? `get_text ${params.selector ?? "body"} → "${preview}${(extractedText ?? "").length > 120 ? "…" : ""}"`
3142
+ : `get_text failed (no page / selector "${params.selector ?? "body"}" not found)`;
3143
+ }
3144
+ else if (tt === "android_emulator") {
3145
+ // Read the rendered text off the booted Android emulator via the
3146
+ // uiautomator hierarchy — lets the agent analyze a Flutter/native
3147
+ // AI chatbot's answer without OCR. `selector` is treated as a
3148
+ // text/resource-id substring filter when provided.
3149
+ extractedText = await wm.adbGetText(params.selector);
3150
+ success = extractedText !== null && extractedText.length > 0;
3151
+ const preview = (extractedText ?? "").replace(/\s+/g, " ").trim().slice(0, 120);
3152
+ detail = success
3153
+ ? `get_text (android${params.selector ? ` ~"${params.selector}"` : ""}) → "${preview}${(extractedText ?? "").length > 120 ? "…" : ""}"`
3154
+ : "get_text (android) found no visible text — is the emulator booted and the app foregrounded?";
3155
+ }
3156
+ else if (tt === "ios_simulator") {
3157
+ // simctl has no UI-tree dump; the agent reads the answer from a
3158
+ // screenshot via its own vision. Point it there explicitly.
3159
+ success = false;
3160
+ detail =
3161
+ "get_text is not available for iOS simulators (simctl exposes no UI-tree dump). Call codeloop_capture_screenshot with target_type=\"ios_simulator\" and read the AI answer from the image with your vision.";
3162
+ }
3163
+ else {
3164
+ success = false;
3165
+ detail =
3166
+ "get_text is supported for browser and android_emulator targets. For Windows desktop, use win_ui_inspect to read element text; for iOS / other, capture a screenshot and read it visually.";
3167
+ }
3168
+ // Optional assertion: confirm the read text contains expected substring(s).
3169
+ const expectContains = params.expect_contains;
3170
+ if (typeof expectContains === "string" && expectContains.length > 0) {
3171
+ const hay = (extractedText ?? "").toLowerCase();
3172
+ const matched = hay.includes(expectContains.toLowerCase());
3173
+ assertion = {
3174
+ expected: expectContains,
3175
+ matched,
3176
+ actual_excerpt: (extractedText ?? "").replace(/\s+/g, " ").trim().slice(0, 200),
3177
+ };
3178
+ success = success && matched;
3179
+ detail += matched
3180
+ ? ` | assertion PASSED (contains "${expectContains}")`
3181
+ : ` | assertion FAILED (does NOT contain "${expectContains}")`;
3182
+ }
3183
+ break;
3184
+ }
3054
3185
  case "keystroke":
3055
3186
  if (params.key) {
3056
3187
  if (tt === "browser") {
@@ -3924,6 +4055,10 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
3924
4055
  interactionEntry.console_errors = consoleErrors;
3925
4056
  if (clickEffectVerification)
3926
4057
  interactionEntry.verification = clickEffectVerification;
4058
+ if (extractedText !== null)
4059
+ interactionEntry.extracted_text = extractedText.slice(0, 1000);
4060
+ if (assertion)
4061
+ interactionEntry.assertion = assertion;
3927
4062
  try {
3928
4063
  const activeIds = vr.getActiveRecordingIds();
3929
4064
  if (activeIds.length > 0) {
@@ -3949,6 +4084,10 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
3949
4084
  action,
3950
4085
  detail,
3951
4086
  };
4087
+ if (extractedText !== null)
4088
+ ret.extracted_text = extractedText;
4089
+ if (assertion)
4090
+ ret.assertion = assertion;
3952
4091
  if (modalPersistenceDirective)
3953
4092
  ret._f4_directive = modalPersistenceDirective;
3954
4093
  return ret;