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.
- package/dist/auth/critical_floors.d.ts.map +1 -1
- package/dist/auth/critical_floors.js +8 -0
- package/dist/auth/critical_floors.js.map +1 -1
- package/dist/evidence/agent_mode.d.ts +22 -0
- package/dist/evidence/agent_mode.d.ts.map +1 -0
- package/dist/evidence/agent_mode.js +113 -0
- package/dist/evidence/agent_mode.js.map +1 -0
- package/dist/evidence/interaction_coverage.d.ts.map +1 -1
- package/dist/evidence/interaction_coverage.js +2 -1
- package/dist/evidence/interaction_coverage.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +154 -15
- package/dist/index.js.map +1 -1
- package/dist/runners/base.d.ts.map +1 -1
- package/dist/runners/base.js +86 -14
- package/dist/runners/base.js.map +1 -1
- package/dist/runners/browser_interaction.d.ts +8 -0
- package/dist/runners/browser_interaction.d.ts.map +1 -1
- package/dist/runners/browser_interaction.js +24 -0
- package/dist/runners/browser_interaction.js.map +1 -1
- package/dist/runners/device_probe.d.ts +38 -0
- package/dist/runners/device_probe.d.ts.map +1 -0
- package/dist/runners/device_probe.js +143 -0
- package/dist/runners/device_probe.js.map +1 -0
- package/dist/runners/flutter.d.ts.map +1 -1
- package/dist/runners/flutter.js +104 -3
- package/dist/runners/flutter.js.map +1 -1
- package/dist/runners/window_manager.d.ts +18 -0
- package/dist/runners/window_manager.d.ts.map +1 -1
- package/dist/runners/window_manager.js +66 -0
- package/dist/runners/window_manager.js.map +1 -1
- package/dist/tools/verify.d.ts.map +1 -1
- package/dist/tools/verify.js +64 -4
- package/dist/tools/verify.js.map +1 -1
- 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 (
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
""
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
-
|
|
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;
|