zeno-mobile-runner 0.1.3 → 0.2.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.
- package/CHANGELOG.md +192 -2
- package/FEATURES.md +50 -7
- package/README.md +168 -120
- package/build.zig.zon +3 -3
- package/clients/README.md +60 -3
- package/clients/go/README.md +12 -0
- package/clients/go/zmr/client.go +142 -0
- package/clients/kotlin/README.md +18 -1
- package/clients/kotlin/build.gradle.kts +1 -1
- package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +76 -1
- package/clients/python/README.md +19 -0
- package/clients/python/pyproject.toml +1 -1
- package/clients/python/zmr_client.py +33 -0
- package/clients/rust/Cargo.lock +1 -1
- package/clients/rust/Cargo.toml +1 -1
- package/clients/rust/README.md +25 -1
- package/clients/rust/src/lib.rs +201 -0
- package/clients/swift/README.md +18 -0
- package/clients/swift/Sources/ZMRClient/ZMRClient.swift +82 -0
- package/clients/typescript/README.md +16 -0
- package/clients/typescript/index.d.ts +12 -0
- package/clients/typescript/index.mjs +16 -0
- package/clients/typescript/package.json +1 -1
- package/docs/agent-discovery.md +151 -22
- package/docs/ai-agents.md +99 -11
- package/docs/benchmarking.md +49 -3
- package/docs/benchmarks/2026-06-09-android-workflow.md +73 -0
- package/docs/benchmarks/2026-06-09-android-workflow.results.jsonl +20 -0
- package/docs/benchmarks/2026-06-09-framework-baseline-status.md +32 -0
- package/docs/benchmarks/2026-06-09-ios-appium-comparison.md +115 -0
- package/docs/benchmarks/2026-06-09-ios-appium-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-demo.md +90 -0
- package/docs/benchmarks/2026-06-09-ios-demo.results.jsonl +20 -0
- package/docs/benchmarks/2026-06-09-ios-maestro-comparison.md +128 -0
- package/docs/benchmarks/2026-06-09-ios-maestro-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-workflow-comparison.md +143 -0
- package/docs/benchmarks/2026-06-09-ios-workflow-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-xctest-floor.md +106 -0
- package/docs/benchmarks/2026-06-09-ios-xctest-floor.results.jsonl +40 -0
- package/docs/benchmarks/README.md +36 -0
- package/docs/benchmarks/benchmark-lab-v1.json +155 -0
- package/docs/benchmarks/benchmark-lab-v1.md +95 -0
- package/docs/clients.md +26 -6
- package/docs/demo.md +40 -1
- package/docs/expo-smoke.md +8 -8
- package/docs/frameworks.md +10 -0
- package/docs/install.md +3 -2
- package/docs/npm.md +100 -4
- package/docs/production-readiness.md +123 -0
- package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
- package/docs/protocol.md +215 -16
- package/docs/scenario-authoring.md +18 -0
- package/docs/trace-privacy.md +9 -0
- package/docs/troubleshooting.md +7 -1
- package/examples/android-workflow.json +79 -0
- package/examples/ios-shim-workflow.json +79 -0
- package/examples/react-native-expo-workflow.json +75 -0
- package/npm/agents.mjs +16 -0
- package/npm/commands.mjs +9 -5
- package/package.json +6 -1
- package/prebuilds/darwin-arm64/zmr +0 -0
- package/prebuilds/darwin-x64/zmr +0 -0
- package/prebuilds/linux-arm64/zmr +0 -0
- package/prebuilds/linux-x64/zmr +0 -0
- package/schemas/README.md +4 -0
- package/schemas/discover-output.schema.json +83 -0
- package/schemas/draft-output.schema.json +58 -0
- package/schemas/explore-output.schema.json +94 -0
- package/schemas/inspect-output.schema.json +88 -0
- package/schemas/run-output.schema.json +2 -0
- package/scripts/benchmark-lab.py +253 -0
- package/scripts/create-android-demo-app.sh +324 -29
- package/scripts/create-ios-demo-app.sh +174 -7
- package/scripts/create-react-native-expo-demo-app.sh +727 -0
- package/scripts/demo.sh +3 -0
- package/scripts/install-ios-shim.sh +2 -2
- package/scripts/release-readiness.py +43 -0
- package/scripts/run-android-pilot.sh +35 -9
- package/scripts/run-ios-pilot.sh +11 -4
- package/shims/ios/ZMRShim.swift +10 -0
- package/shims/ios/ZMRShimUITestCase.swift +42 -0
- package/shims/ios/protocol.md +1 -0
- package/skills/zmr-mobile-testing/SKILL.md +28 -3
- package/src/cli_discover.zig +239 -0
- package/src/cli_draft.zig +924 -0
- package/src/cli_explore.zig +136 -0
- package/src/cli_import.zig +31 -15
- package/src/cli_inspect.zig +310 -0
- package/src/cli_output.zig +26 -2
- package/src/cli_run.zig +28 -0
- package/src/cli_trace.zig +45 -15
- package/src/cli_validate.zig +12 -6
- package/src/errors.zig +9 -0
- package/src/ios.zig +49 -12
- package/src/ios_shim.zig +36 -2
- package/src/json_rpc_methods.zig +85 -11
- package/src/json_rpc_params.zig +8 -0
- package/src/json_rpc_protocol.zig +1 -1
- package/src/json_rpc_trace.zig +112 -0
- package/src/main.zig +27 -2
- package/src/mcp.zig +209 -6
- package/src/mcp_protocol.zig +29 -1
- package/src/mcp_trace.zig +126 -4
- package/src/report.zig +186 -0
- package/src/runner.zig +26 -4
- package/src/runner_actions.zig +10 -0
- package/src/runner_diagnostics.zig +31 -1
- package/src/runner_events.zig +70 -7
- package/src/runner_native.zig +17 -1
- package/src/runner_waits.zig +82 -19
- package/src/scaffold.zig +28 -12
- package/src/scenario.zig +32 -4
- package/src/schema_registry.zig +4 -0
- package/src/version.zig +1 -1
- package/viewer/app.js +23 -3
package/src/json_rpc_trace.zig
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
|
+
const cli_discover = @import("cli_discover.zig");
|
|
3
|
+
const cli_explore = @import("cli_explore.zig");
|
|
2
4
|
const protocol = @import("json_rpc_protocol.zig");
|
|
5
|
+
const report = @import("report.zig");
|
|
6
|
+
const runner_events = @import("runner_events.zig");
|
|
3
7
|
const trace = @import("trace.zig");
|
|
4
8
|
|
|
5
9
|
pub fn writeEventsResult(
|
|
@@ -71,3 +75,111 @@ pub fn recordSimplePayload(tw: *trace.TraceWriter, kind: []const u8, key: []cons
|
|
|
71
75
|
try writer.writeAll("}");
|
|
72
76
|
try tw.recordEvent(kind, payload.items);
|
|
73
77
|
}
|
|
78
|
+
|
|
79
|
+
pub fn writeDiscoverResult(
|
|
80
|
+
allocator: std.mem.Allocator,
|
|
81
|
+
writer: anytype,
|
|
82
|
+
id: ?std.json.Value,
|
|
83
|
+
live_trace: ?*trace.TraceWriter,
|
|
84
|
+
out_path: []const u8,
|
|
85
|
+
include_actions: bool,
|
|
86
|
+
validate: bool,
|
|
87
|
+
force: bool,
|
|
88
|
+
name: ?[]const u8,
|
|
89
|
+
app_id: ?[]const u8,
|
|
90
|
+
) !void {
|
|
91
|
+
const tw = live_trace orelse {
|
|
92
|
+
try protocol.writeResultRaw(writer, id, "{\"ok\":false,\"mode\":\"discover\",\"traceDir\":null,\"message\":\"start zmr serve with --trace-dir to enable live trace discovery\"}");
|
|
93
|
+
return;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
try tw.flushManifest();
|
|
97
|
+
var discovered = try cli_discover.discoverFromTrace(allocator, .{
|
|
98
|
+
.from_trace = tw.root_dir,
|
|
99
|
+
.out_path = out_path,
|
|
100
|
+
.name = name,
|
|
101
|
+
.app_id = app_id,
|
|
102
|
+
.include_actions = include_actions,
|
|
103
|
+
.validate = validate,
|
|
104
|
+
.force = force,
|
|
105
|
+
.json = true,
|
|
106
|
+
});
|
|
107
|
+
defer discovered.deinit(allocator);
|
|
108
|
+
try runner_events.recordTraceDiscover(
|
|
109
|
+
tw,
|
|
110
|
+
if (discovered.summary.ok) "ok" else "failed",
|
|
111
|
+
discovered.summary.draft.out_path,
|
|
112
|
+
include_actions,
|
|
113
|
+
discovered.summary.validated,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
var payload = std.ArrayList(u8).empty;
|
|
117
|
+
defer payload.deinit(allocator);
|
|
118
|
+
try cli_discover.writeJson(payload.writer(allocator), discovered.summary, discovered.validation);
|
|
119
|
+
try protocol.writeResultRaw(writer, id, std.mem.trimRight(u8, payload.items, " \t\r\n"));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
pub fn writeExploreResult(
|
|
123
|
+
allocator: std.mem.Allocator,
|
|
124
|
+
writer: anytype,
|
|
125
|
+
id: ?std.json.Value,
|
|
126
|
+
live_trace: ?*trace.TraceWriter,
|
|
127
|
+
out_path: []const u8,
|
|
128
|
+
goal: []const u8,
|
|
129
|
+
include_actions: bool,
|
|
130
|
+
validate: bool,
|
|
131
|
+
force: bool,
|
|
132
|
+
name: ?[]const u8,
|
|
133
|
+
app_id: ?[]const u8,
|
|
134
|
+
) !void {
|
|
135
|
+
const tw = live_trace orelse {
|
|
136
|
+
try protocol.writeResultRaw(writer, id, "{\"ok\":false,\"mode\":\"explore\",\"traceDir\":null,\"message\":\"start zmr serve with --trace-dir to enable live trace exploration\"}");
|
|
137
|
+
return;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
try tw.flushManifest();
|
|
141
|
+
var explored = try cli_explore.exploreFromTrace(allocator, .{
|
|
142
|
+
.from_trace = tw.root_dir,
|
|
143
|
+
.out_path = out_path,
|
|
144
|
+
.goal = goal,
|
|
145
|
+
.name = name,
|
|
146
|
+
.app_id = app_id,
|
|
147
|
+
.include_actions = include_actions,
|
|
148
|
+
.validate = validate,
|
|
149
|
+
.force = force,
|
|
150
|
+
.json = true,
|
|
151
|
+
});
|
|
152
|
+
defer explored.deinit(allocator);
|
|
153
|
+
try runner_events.recordTraceExplore(
|
|
154
|
+
tw,
|
|
155
|
+
if (explored.summary.ok) "ok" else "failed",
|
|
156
|
+
explored.discovered.summary.draft.out_path,
|
|
157
|
+
goal,
|
|
158
|
+
include_actions,
|
|
159
|
+
explored.discovered.summary.validated,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
var payload = std.ArrayList(u8).empty;
|
|
163
|
+
defer payload.deinit(allocator);
|
|
164
|
+
try cli_explore.writeJson(payload.writer(allocator), explored.summary, explored.discovered.summary, explored.discovered.validation);
|
|
165
|
+
try protocol.writeResultRaw(writer, id, std.mem.trimRight(u8, payload.items, " \t\r\n"));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
pub fn writeExplainResult(
|
|
169
|
+
allocator: std.mem.Allocator,
|
|
170
|
+
writer: anytype,
|
|
171
|
+
id: ?std.json.Value,
|
|
172
|
+
live_trace: ?*trace.TraceWriter,
|
|
173
|
+
) !void {
|
|
174
|
+
const tw = live_trace orelse {
|
|
175
|
+
try protocol.writeResultRaw(writer, id, "{\"traceDir\":null,\"message\":\"start zmr serve with --trace-dir to enable live RPC trace explanation\"}");
|
|
176
|
+
return;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
try tw.flushManifest();
|
|
180
|
+
var payload = std.ArrayList(u8).empty;
|
|
181
|
+
defer payload.deinit(allocator);
|
|
182
|
+
try report.writeTraceExplanationJson(allocator, tw.root_dir, payload.writer(allocator));
|
|
183
|
+
try protocol.writeResultRaw(writer, id, std.mem.trimRight(u8, payload.items, " \t\r\n"));
|
|
184
|
+
try tw.recordEvent("trace.explain", "{\"status\":\"ok\"}");
|
|
185
|
+
}
|
package/src/main.zig
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
2
|
const cli_devices = @import("cli_devices.zig");
|
|
3
|
+
const cli_discover = @import("cli_discover.zig");
|
|
3
4
|
const cli_doctor = @import("cli_doctor.zig");
|
|
5
|
+
const cli_draft = @import("cli_draft.zig");
|
|
6
|
+
const cli_explore = @import("cli_explore.zig");
|
|
4
7
|
const cli_info = @import("cli_info.zig");
|
|
5
8
|
const cli_init = @import("cli_init.zig");
|
|
6
9
|
const cli_import = @import("cli_import.zig");
|
|
10
|
+
const cli_inspect = @import("cli_inspect.zig");
|
|
7
11
|
const cli_run = @import("cli_run.zig");
|
|
8
12
|
const cli_serve = @import("cli_serve.zig");
|
|
9
13
|
const cli_trace = @import("cli_trace.zig");
|
|
@@ -40,12 +44,20 @@ fn mainInner() !void {
|
|
|
40
44
|
try cli_info.runSchemas(allocator, &args);
|
|
41
45
|
} else if (std.mem.eql(u8, command_name, "doctor")) {
|
|
42
46
|
try cli_doctor.run(allocator, &args);
|
|
47
|
+
} else if (std.mem.eql(u8, command_name, "discover")) {
|
|
48
|
+
try cli_discover.run(allocator, &args);
|
|
49
|
+
} else if (std.mem.eql(u8, command_name, "draft")) {
|
|
50
|
+
try cli_draft.run(allocator, &args);
|
|
51
|
+
} else if (std.mem.eql(u8, command_name, "explore")) {
|
|
52
|
+
try cli_explore.run(allocator, &args);
|
|
43
53
|
} else if (std.mem.eql(u8, command_name, "validate")) {
|
|
44
54
|
try cli_validate.run(allocator, &args);
|
|
45
55
|
} else if (std.mem.eql(u8, command_name, "init")) {
|
|
46
56
|
try cli_init.run(allocator, &args);
|
|
47
57
|
} else if (std.mem.eql(u8, command_name, "import")) {
|
|
48
58
|
try cli_import.run(allocator, &args);
|
|
59
|
+
} else if (std.mem.eql(u8, command_name, "inspect")) {
|
|
60
|
+
try cli_inspect.run(allocator, &args);
|
|
49
61
|
} else if (std.mem.eql(u8, command_name, "run")) {
|
|
50
62
|
try cli_run.run(allocator, &args);
|
|
51
63
|
} else if (std.mem.eql(u8, command_name, "report")) {
|
|
@@ -76,6 +88,9 @@ fn writeTopLevelError(err: anyerror) void {
|
|
|
76
88
|
if (err == error.CommandFailed) {
|
|
77
89
|
stderr.writeAll("hint: run `zmr doctor --json` for setup diagnostics.\n") catch {};
|
|
78
90
|
}
|
|
91
|
+
if (err == error.UnknownFlag) {
|
|
92
|
+
stderr.writeAll("hint: run `zmr help` for each command's flags and arguments.\n") catch {};
|
|
93
|
+
}
|
|
79
94
|
}
|
|
80
95
|
|
|
81
96
|
fn exitCodeForError(err: anyerror) u8 {
|
|
@@ -85,6 +100,12 @@ fn exitCodeForError(err: anyerror) u8 {
|
|
|
85
100
|
error.MissingScenarioPath,
|
|
86
101
|
error.MissingDeviceSerial,
|
|
87
102
|
error.MissingTraceDir,
|
|
103
|
+
error.MissingReportInput,
|
|
104
|
+
error.MissingReportOutput,
|
|
105
|
+
error.MissingDraftOut,
|
|
106
|
+
error.MissingDiscoverOut,
|
|
107
|
+
error.MissingJUnitOutput,
|
|
108
|
+
error.MissingTraceBundleOutput,
|
|
88
109
|
error.MissingAppId,
|
|
89
110
|
error.MissingAdbPath,
|
|
90
111
|
error.MissingXcrunPath,
|
|
@@ -110,12 +131,16 @@ fn usage() !void {
|
|
|
110
131
|
\\ zmr schemas [--json]
|
|
111
132
|
\\ zmr devices [--json] [--platform android|ios] [--ios-device-type simulator|physical|all] [--adb <path>] [--xcrun <path>]
|
|
112
133
|
\\ zmr doctor [--json] [--strict] [--config <path>] [--zig <path>] [--adb <path>] [--android-shim <path>] [--xcrun <path>] [--ios-shim <path>]
|
|
134
|
+
\\ zmr discover --from-trace <trace-dir> --out <scenario.json> [--include-actions] [--validate] [--name <name>] [--app-id <id>] [--force] [--json]
|
|
135
|
+
\\ zmr draft --from-trace <trace-dir> --out <scenario.json> [--include-actions] [--name <name>] [--app-id <id>] [--force] [--json]
|
|
136
|
+
\\ zmr explore --from-trace <trace-dir> --out <scenario.json> [--goal <goal>] [--include-actions] [--validate] [--name <name>] [--app-id <id>] [--force] [--json]
|
|
113
137
|
\\ zmr validate <scenario.json> [--json]
|
|
114
138
|
\\ zmr init [scenario.json] [--app-id <id>] [--force] [--json]
|
|
115
139
|
\\ zmr init --app [--dir <app-root>] [--app-id <id>] [--force] [--json]
|
|
116
140
|
\\ zmr import flow-yaml <flow.yaml> --out <scenario.json> [--name <name>] [--app-id <id>] [--force] [--json]
|
|
117
|
-
\\ zmr
|
|
118
|
-
\\ zmr
|
|
141
|
+
\\ zmr inspect [--json] [--dir <app-root>] [--config <path>]
|
|
142
|
+
\\ zmr run [scenario.json] [--json] [--config <path>] [--platform android|ios] [--ios-device-type simulator|physical] [--device <serial>] [--app-id <id>] [--trace-dir <path>] [--discover-out <scenario.json>] [--android-avd <name>] [--create-avd-if-missing] [--avd-system-image <pkg>] [--avd-device <profile>] [--restore-snapshot <name>] [--reset-emulator] [--wait-emulator] [--screen-record] [--no-screen-record] [--adb <path>] [--emulator <path>] [--avdmanager <path>] [--android-shim <path>] [--xcrun <path>] [--ios-shim <path>]
|
|
143
|
+
\\ zmr report <trace-or-benchmark-dir> --out <report.html> [--junit <report.xml>]
|
|
119
144
|
\\ zmr explain <trace-dir> [--json]
|
|
120
145
|
\\ zmr export <trace-dir> --out <bundle.zmrtrace> [--redact] [--omit-screenshots]
|
|
121
146
|
\\ zmr serve --transport stdio [--config <path>] [--platform android|ios] [--ios-device-type simulator|physical] [--device <serial>] [--app-id <id>] [--trace-dir <path>] [--adb <path>] [--android-shim <path>] [--xcrun <path>] [--ios-shim <path>]
|
package/src/mcp.zig
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
|
+
const cli_output = @import("cli_output.zig");
|
|
2
3
|
const errors = @import("errors.zig");
|
|
3
4
|
const mcp_protocol = @import("mcp_protocol.zig");
|
|
4
5
|
const mcp_trace = @import("mcp_trace.zig");
|
|
6
|
+
const params_parser = @import("json_rpc_params.zig");
|
|
7
|
+
const rpc_trace = @import("json_rpc_trace.zig");
|
|
5
8
|
const runner = @import("runner.zig");
|
|
9
|
+
const runner_events = @import("runner_events.zig");
|
|
6
10
|
const selector = @import("selector.zig");
|
|
7
11
|
const semantic = @import("semantic.zig");
|
|
8
12
|
const trace = @import("trace.zig");
|
|
13
|
+
const validation = @import("validation.zig");
|
|
9
14
|
|
|
10
15
|
pub fn serveStdioWithTrace(allocator: std.mem.Allocator, device: anytype, live_trace: ?*trace.TraceWriter) !void {
|
|
11
16
|
var stdin = std.fs.File.stdin().deprecatedReader();
|
|
@@ -128,6 +133,35 @@ fn callTool(
|
|
|
128
133
|
return;
|
|
129
134
|
}
|
|
130
135
|
|
|
136
|
+
if (std.mem.eql(u8, tool_name, "install_app")) {
|
|
137
|
+
const path = try requiredParamString(arguments, "path");
|
|
138
|
+
try device.install(path);
|
|
139
|
+
if (live_trace) |tw| try rpc_trace.recordSimplePayload(tw, "app.install", "path", path);
|
|
140
|
+
try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":true}");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (std.mem.eql(u8, tool_name, "launch_app")) {
|
|
145
|
+
try device.launch();
|
|
146
|
+
if (live_trace) |tw| try tw.recordEvent("app.launch", "{\"status\":\"ok\"}");
|
|
147
|
+
try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":true}");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (std.mem.eql(u8, tool_name, "stop_app")) {
|
|
152
|
+
try device.stop();
|
|
153
|
+
if (live_trace) |tw| try tw.recordEvent("app.stop", "{\"status\":\"ok\"}");
|
|
154
|
+
try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":true}");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (std.mem.eql(u8, tool_name, "clear_state")) {
|
|
159
|
+
try device.clearState();
|
|
160
|
+
if (live_trace) |tw| try tw.recordEvent("app.clearState", "{\"status\":\"ok\"}");
|
|
161
|
+
try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":true}");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
131
165
|
if (std.mem.eql(u8, tool_name, "tap")) {
|
|
132
166
|
const wanted = try parseArgumentsSelector(allocator, arguments);
|
|
133
167
|
defer wanted.deinit(allocator);
|
|
@@ -144,6 +178,44 @@ fn callTool(
|
|
|
144
178
|
try runner.typeTextSelector(device, wanted, text, live_trace, .{});
|
|
145
179
|
} else {
|
|
146
180
|
try device.typeText(text);
|
|
181
|
+
if (live_trace) |tw| try rpc_trace.recordSimplePayload(tw, "ui.type", "text", text);
|
|
182
|
+
}
|
|
183
|
+
try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":true}");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (std.mem.eql(u8, tool_name, "swipe")) {
|
|
188
|
+
const x1 = try params_parser.requiredI32(arguments, "x1");
|
|
189
|
+
const y1 = try params_parser.requiredI32(arguments, "y1");
|
|
190
|
+
const x2 = try params_parser.requiredI32(arguments, "x2");
|
|
191
|
+
const y2 = try params_parser.requiredI32(arguments, "y2");
|
|
192
|
+
const duration_ms = @as(u32, @intCast(try optionalParamU64(arguments, "durationMs", 300)));
|
|
193
|
+
try device.swipe(x1, y1, x2, y2, duration_ms);
|
|
194
|
+
if (live_trace) |tw| try runner_events.recordSwipe(tw, x1, y1, x2, y2, duration_ms);
|
|
195
|
+
try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":true}");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (std.mem.eql(u8, tool_name, "hide_keyboard")) {
|
|
200
|
+
try device.hideKeyboard();
|
|
201
|
+
if (live_trace) |tw| try tw.recordEvent("ui.hideKeyboard", "{\"status\":\"ok\"}");
|
|
202
|
+
try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":true}");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (std.mem.eql(u8, tool_name, "erase_text")) {
|
|
207
|
+
const max_chars = @as(u32, @intCast(try optionalParamU64(arguments, "maxChars", 80)));
|
|
208
|
+
if (paramField(arguments, "selector")) |selector_value| {
|
|
209
|
+
const wanted = try selector.parseFromJson(allocator, selector_value);
|
|
210
|
+
defer wanted.deinit(allocator);
|
|
211
|
+
try runner.eraseTextSelector(device, wanted, max_chars, live_trace, .{});
|
|
212
|
+
} else {
|
|
213
|
+
try device.eraseText(max_chars);
|
|
214
|
+
if (live_trace) |tw| {
|
|
215
|
+
const payload = try std.fmt.allocPrint(tw.allocator, "{{\"maxChars\":{d}}}", .{max_chars});
|
|
216
|
+
defer tw.allocator.free(payload);
|
|
217
|
+
try tw.recordEvent("ui.eraseText", payload);
|
|
218
|
+
}
|
|
147
219
|
}
|
|
148
220
|
try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":true}");
|
|
149
221
|
return;
|
|
@@ -151,12 +223,15 @@ fn callTool(
|
|
|
151
223
|
|
|
152
224
|
if (std.mem.eql(u8, tool_name, "press_back")) {
|
|
153
225
|
try device.pressBack();
|
|
226
|
+
if (live_trace) |tw| try tw.recordEvent("ui.pressBack", "{\"status\":\"ok\"}");
|
|
154
227
|
try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":true}");
|
|
155
228
|
return;
|
|
156
229
|
}
|
|
157
230
|
|
|
158
231
|
if (std.mem.eql(u8, tool_name, "open_link")) {
|
|
159
|
-
|
|
232
|
+
const url = try requiredParamString(arguments, "url");
|
|
233
|
+
try device.openLink(url);
|
|
234
|
+
if (live_trace) |tw| try runner_events.recordActionStatus(tw, "app.openLink", "ok", null, url);
|
|
160
235
|
try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":true}");
|
|
161
236
|
return;
|
|
162
237
|
}
|
|
@@ -166,11 +241,80 @@ fn callTool(
|
|
|
166
241
|
defer wanted.deinit(allocator);
|
|
167
242
|
const timeout_ms = try optionalParamU64(arguments, "timeoutMs", 5000);
|
|
168
243
|
const visible = try runner.waitUntilVisible(device, wanted, timeout_ms, live_trace, .{});
|
|
169
|
-
try writer
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
244
|
+
try writeVisibleToolResult(writer, id, visible);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (std.mem.eql(u8, tool_name, "wait_not_visible")) {
|
|
249
|
+
const wanted = try parseArgumentsSelector(allocator, arguments);
|
|
250
|
+
defer wanted.deinit(allocator);
|
|
251
|
+
const timeout_ms = try optionalParamU64(arguments, "timeoutMs", 5000);
|
|
252
|
+
const gone = try runner.waitUntilNotVisible(device, wanted, timeout_ms, live_trace, .{});
|
|
253
|
+
try writeVisibleToolResult(writer, id, !gone);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (std.mem.eql(u8, tool_name, "wait_any")) {
|
|
258
|
+
const selectors = try params_parser.selectors(allocator, arguments);
|
|
259
|
+
defer {
|
|
260
|
+
for (selectors) |wanted| wanted.deinit(allocator);
|
|
261
|
+
allocator.free(selectors);
|
|
262
|
+
}
|
|
263
|
+
const timeout_ms = try optionalParamU64(arguments, "timeoutMs", 5000);
|
|
264
|
+
const matched = try runner.waitUntilAnyVisible(device, selectors, timeout_ms, live_trace, .{});
|
|
265
|
+
try writeMatchedIndexToolResult(allocator, writer, id, matched);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (std.mem.eql(u8, tool_name, "scroll_until_visible")) {
|
|
270
|
+
const wanted = try parseArgumentsSelector(allocator, arguments);
|
|
271
|
+
defer wanted.deinit(allocator);
|
|
272
|
+
const timeout_ms = try optionalParamU64(arguments, "timeoutMs", 5000);
|
|
273
|
+
const visible = try runner.scrollUntilVisible(
|
|
274
|
+
device,
|
|
275
|
+
wanted,
|
|
276
|
+
timeout_ms,
|
|
277
|
+
try params_parser.optionalDirection(arguments, "direction", .down),
|
|
278
|
+
live_trace,
|
|
279
|
+
.{},
|
|
280
|
+
);
|
|
281
|
+
try writeVisibleToolResult(writer, id, visible);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (std.mem.eql(u8, tool_name, "assert_visible")) {
|
|
286
|
+
const wanted = try parseArgumentsSelector(allocator, arguments);
|
|
287
|
+
defer wanted.deinit(allocator);
|
|
288
|
+
const timeout_ms = try optionalParamU64(arguments, "timeoutMs", 5000);
|
|
289
|
+
if (!try runner.assertVisible(device, wanted, timeout_ms, live_trace, .{})) return error.AssertionFailed;
|
|
290
|
+
try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":true}");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (std.mem.eql(u8, tool_name, "assert_not_visible")) {
|
|
295
|
+
const wanted = try parseArgumentsSelector(allocator, arguments);
|
|
296
|
+
defer wanted.deinit(allocator);
|
|
297
|
+
const timeout_ms = try optionalParamU64(arguments, "timeoutMs", 5000);
|
|
298
|
+
if (!try runner.assertNotVisible(device, wanted, timeout_ms, live_trace, .{})) return error.AssertionFailed;
|
|
299
|
+
try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":true}");
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (std.mem.eql(u8, tool_name, "assert_healthy")) {
|
|
304
|
+
const timeout_ms = try optionalParamU64(arguments, "timeoutMs", 0);
|
|
305
|
+
if (!try runner.assertHealthy(device, timeout_ms, live_trace, .{})) return error.AssertionFailed;
|
|
306
|
+
try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":true}");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (std.mem.eql(u8, tool_name, "scenario_validate")) {
|
|
311
|
+
const path = try requiredParamString(arguments, "path");
|
|
312
|
+
var result = try validation.validateFile(allocator, path);
|
|
313
|
+
defer result.deinit(allocator);
|
|
314
|
+
var payload = std.ArrayList(u8).empty;
|
|
315
|
+
defer payload.deinit(allocator);
|
|
316
|
+
try cli_output.writeValidationJson(payload.writer(allocator), path, result);
|
|
317
|
+
try mcp_protocol.writeToolTextResult(writer, id, std.mem.trimRight(u8, payload.items, " \t\r\n"));
|
|
174
318
|
return;
|
|
175
319
|
}
|
|
176
320
|
|
|
@@ -179,6 +323,44 @@ fn callTool(
|
|
|
179
323
|
return;
|
|
180
324
|
}
|
|
181
325
|
|
|
326
|
+
if (std.mem.eql(u8, tool_name, "trace_discover")) {
|
|
327
|
+
try mcp_trace.writeDiscoverToolResult(
|
|
328
|
+
allocator,
|
|
329
|
+
writer,
|
|
330
|
+
id,
|
|
331
|
+
live_trace,
|
|
332
|
+
try requiredParamString(arguments, "out"),
|
|
333
|
+
try optionalParamBool(arguments, "includeActions", false),
|
|
334
|
+
try optionalParamBool(arguments, "validate", false),
|
|
335
|
+
try optionalParamBool(arguments, "force", false),
|
|
336
|
+
optionalParamString(arguments, "name"),
|
|
337
|
+
optionalParamString(arguments, "appId"),
|
|
338
|
+
);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (std.mem.eql(u8, tool_name, "trace_explore")) {
|
|
343
|
+
try mcp_trace.writeExploreToolResult(
|
|
344
|
+
allocator,
|
|
345
|
+
writer,
|
|
346
|
+
id,
|
|
347
|
+
live_trace,
|
|
348
|
+
try requiredParamString(arguments, "out"),
|
|
349
|
+
try requiredParamString(arguments, "goal"),
|
|
350
|
+
try optionalParamBool(arguments, "includeActions", false),
|
|
351
|
+
try optionalParamBool(arguments, "validate", false),
|
|
352
|
+
try optionalParamBool(arguments, "force", false),
|
|
353
|
+
optionalParamString(arguments, "name"),
|
|
354
|
+
optionalParamString(arguments, "appId"),
|
|
355
|
+
);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (std.mem.eql(u8, tool_name, "trace_explain")) {
|
|
360
|
+
try mcp_trace.writeExplainToolResult(allocator, writer, id, live_trace);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
182
364
|
if (std.mem.eql(u8, tool_name, "trace_export")) {
|
|
183
365
|
const out_path = try requiredParamString(arguments, "out");
|
|
184
366
|
const omit_screenshots = try optionalParamBool(arguments, "omitScreenshots", false);
|
|
@@ -190,6 +372,27 @@ fn callTool(
|
|
|
190
372
|
try mcp_protocol.writeError(writer, id, -32602, "unknown tool");
|
|
191
373
|
}
|
|
192
374
|
|
|
375
|
+
fn writeVisibleToolResult(writer: anytype, id: ?std.json.Value, visible: bool) !void {
|
|
376
|
+
try mcp_protocol.writeToolTextResult(writer, id, if (visible) "{\"visible\":true}" else "{\"visible\":false}");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
fn writeMatchedIndexToolResult(
|
|
380
|
+
allocator: std.mem.Allocator,
|
|
381
|
+
writer: anytype,
|
|
382
|
+
id: ?std.json.Value,
|
|
383
|
+
matched: ?usize,
|
|
384
|
+
) !void {
|
|
385
|
+
var payload = std.ArrayList(u8).empty;
|
|
386
|
+
defer payload.deinit(allocator);
|
|
387
|
+
const payload_writer = payload.writer(allocator);
|
|
388
|
+
if (matched) |index| {
|
|
389
|
+
try payload_writer.print("{{\"matchedIndex\":{d}}}", .{index});
|
|
390
|
+
} else {
|
|
391
|
+
try payload_writer.writeAll("{\"matchedIndex\":null}");
|
|
392
|
+
}
|
|
393
|
+
try mcp_protocol.writeToolTextResult(writer, id, payload.items);
|
|
394
|
+
}
|
|
395
|
+
|
|
193
396
|
fn parseArgumentsSelector(allocator: std.mem.Allocator, arguments: ?std.json.Value) !selector.Selector {
|
|
194
397
|
const selector_value = paramField(arguments, "selector") orelse return error.MissingSelector;
|
|
195
398
|
return try selector.parseFromJson(allocator, selector_value);
|
package/src/mcp_protocol.zig
CHANGED
|
@@ -61,4 +61,32 @@ pub fn writeId(writer: anytype, id: ?std.json.Value) !void {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const tool_list_json =
|
|
64
|
+
const tool_list_json =
|
|
65
|
+
"{\"tools\":[" ++
|
|
66
|
+
"{\"name\":\"snapshot\",\"description\":\"Capture the current mobile observation snapshot as JSON.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{}}}," ++
|
|
67
|
+
"{\"name\":\"semantic_snapshot\",\"description\":\"Capture an agent-optimized mobile semantic tree with roles, names, selectors, bounds, and recommended actions.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{}}}," ++
|
|
68
|
+
"{\"name\":\"install_app\",\"description\":\"Install an app artifact on the selected device.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\"}}}}," ++
|
|
69
|
+
"{\"name\":\"launch_app\",\"description\":\"Launch the configured app on the selected device.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{}}}," ++
|
|
70
|
+
"{\"name\":\"stop_app\",\"description\":\"Stop the configured app on the selected device.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{}}}," ++
|
|
71
|
+
"{\"name\":\"clear_state\",\"description\":\"Clear the configured app state where the platform supports it.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{}}}," ++
|
|
72
|
+
"{\"name\":\"tap\",\"description\":\"Tap a visible element by selector.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selector\"],\"properties\":{\"selector\":{\"type\":\"object\"}}}}," ++
|
|
73
|
+
"{\"name\":\"type\",\"description\":\"Type text, optionally after focusing an element by selector.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"text\"],\"properties\":{\"selector\":{\"type\":\"object\"},\"text\":{\"type\":\"string\"}}}}," ++
|
|
74
|
+
"{\"name\":\"hide_keyboard\",\"description\":\"Dismiss the software keyboard when the platform can do so.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{}}}," ++
|
|
75
|
+
"{\"name\":\"erase_text\",\"description\":\"Erase text, optionally after focusing an element by selector.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"selector\":{\"type\":\"object\"},\"maxChars\":{\"type\":\"integer\",\"minimum\":0}}}}," ++
|
|
76
|
+
"{\"name\":\"swipe\",\"description\":\"Swipe between absolute screen coordinates.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"x1\",\"y1\",\"x2\",\"y2\"],\"properties\":{\"x1\":{\"type\":\"integer\"},\"y1\":{\"type\":\"integer\"},\"x2\":{\"type\":\"integer\"},\"y2\":{\"type\":\"integer\"},\"durationMs\":{\"type\":\"integer\",\"minimum\":0}}}}," ++
|
|
77
|
+
"{\"name\":\"press_back\",\"description\":\"Press Android back or the platform-equivalent navigation action.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{}}}," ++
|
|
78
|
+
"{\"name\":\"open_link\",\"description\":\"Open a deep link URL in the target app.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"url\"],\"properties\":{\"url\":{\"type\":\"string\"}}}}," ++
|
|
79
|
+
"{\"name\":\"wait_visible\",\"description\":\"Wait for an element selector to become visible.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selector\"],\"properties\":{\"selector\":{\"type\":\"object\"},\"timeoutMs\":{\"type\":\"integer\",\"minimum\":0}}}}," ++
|
|
80
|
+
"{\"name\":\"wait_not_visible\",\"description\":\"Wait for an element selector to no longer be visible.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selector\"],\"properties\":{\"selector\":{\"type\":\"object\"},\"timeoutMs\":{\"type\":\"integer\",\"minimum\":0}}}}," ++
|
|
81
|
+
"{\"name\":\"wait_any\",\"description\":\"Wait for the first visible selector from an ordered selector list.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selectors\"],\"properties\":{\"selectors\":{\"type\":\"array\",\"minItems\":1,\"items\":{\"type\":\"object\"}},\"timeoutMs\":{\"type\":\"integer\",\"minimum\":0}}}}," ++
|
|
82
|
+
"{\"name\":\"scroll_until_visible\",\"description\":\"Scroll until an element selector is visible.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selector\"],\"properties\":{\"selector\":{\"type\":\"object\"},\"direction\":{\"type\":\"string\",\"enum\":[\"down\",\"up\"]},\"timeoutMs\":{\"type\":\"integer\",\"minimum\":0}}}}," ++
|
|
83
|
+
"{\"name\":\"assert_visible\",\"description\":\"Assert that an element selector is visible within the timeout.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selector\"],\"properties\":{\"selector\":{\"type\":\"object\"},\"timeoutMs\":{\"type\":\"integer\",\"minimum\":0}}}}," ++
|
|
84
|
+
"{\"name\":\"assert_not_visible\",\"description\":\"Assert that an element selector is not visible within the timeout.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selector\"],\"properties\":{\"selector\":{\"type\":\"object\"},\"timeoutMs\":{\"type\":\"integer\",\"minimum\":0}}}}," ++
|
|
85
|
+
"{\"name\":\"assert_healthy\",\"description\":\"Assert that the app is free of common crash and error overlays.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"timeoutMs\":{\"type\":\"integer\",\"minimum\":0}}}}," ++
|
|
86
|
+
"{\"name\":\"scenario_validate\",\"description\":\"Validate a ZMR scenario file and return structured diagnostics.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\"}}}}," ++
|
|
87
|
+
"{\"name\":\"trace_events\",\"description\":\"Read live trace events from a traced MCP session.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"afterSeq\":{\"type\":\"integer\",\"minimum\":0},\"limit\":{\"type\":\"integer\",\"minimum\":1}}}}," ++
|
|
88
|
+
"{\"name\":\"trace_explain\",\"description\":\"Summarize the active trace status, failure, diagnostics, and next commands.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{}}}," ++
|
|
89
|
+
"{\"name\":\"trace_explore\",\"description\":\"Generate a review-required scenario draft for a stated goal from the active trace directory.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"out\",\"goal\"],\"properties\":{\"out\":{\"type\":\"string\"},\"goal\":{\"type\":\"string\"},\"includeActions\":{\"type\":\"boolean\"},\"validate\":{\"type\":\"boolean\"},\"force\":{\"type\":\"boolean\"},\"name\":{\"type\":\"string\"},\"appId\":{\"type\":\"string\"}}}}," ++
|
|
90
|
+
"{\"name\":\"trace_discover\",\"description\":\"Generate a reviewable scenario draft from the active trace directory.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"out\"],\"properties\":{\"out\":{\"type\":\"string\"},\"includeActions\":{\"type\":\"boolean\"},\"validate\":{\"type\":\"boolean\"},\"force\":{\"type\":\"boolean\"},\"name\":{\"type\":\"string\"},\"appId\":{\"type\":\"string\"}}}}," ++
|
|
91
|
+
"{\"name\":\"trace_export\",\"description\":\"Export the active trace directory as a .zmrtrace bundle.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"out\"],\"properties\":{\"out\":{\"type\":\"string\"},\"redact\":{\"type\":\"boolean\"},\"omitScreenshots\":{\"type\":\"boolean\"}}}}" ++
|
|
92
|
+
"]}";
|