zig-mobile-runner 0.1.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 +484 -0
- package/CONTRIBUTING.md +42 -0
- package/FEATURES.md +112 -0
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/SECURITY.md +34 -0
- package/build.zig +38 -0
- package/build.zig.zon +7 -0
- package/clients/README.md +144 -0
- package/clients/go/README.md +24 -0
- package/clients/go/examples/fake-session/main.go +93 -0
- package/clients/go/go.mod +3 -0
- package/clients/go/zmr/client.go +432 -0
- package/clients/kotlin/README.md +35 -0
- package/clients/kotlin/build.gradle.kts +35 -0
- package/clients/kotlin/settings.gradle.kts +15 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/FakeSession.kt +86 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +67 -0
- package/clients/python/README.md +29 -0
- package/clients/python/examples/fake_session.py +48 -0
- package/clients/python/pyproject.toml +13 -0
- package/clients/python/zmr_client.py +202 -0
- package/clients/rust/Cargo.lock +107 -0
- package/clients/rust/Cargo.toml +10 -0
- package/clients/rust/README.md +19 -0
- package/clients/rust/examples/fake_session.rs +70 -0
- package/clients/rust/src/lib.rs +461 -0
- package/clients/swift/Package.swift +16 -0
- package/clients/swift/README.md +36 -0
- package/clients/swift/Sources/ZMRClient/ZMRClient.swift +114 -0
- package/clients/swift/Sources/ZMRFakeSession/main.swift +86 -0
- package/clients/typescript/README.md +34 -0
- package/clients/typescript/examples/fake-session.mjs +36 -0
- package/clients/typescript/index.d.ts +144 -0
- package/clients/typescript/index.mjs +192 -0
- package/clients/typescript/package.json +8 -0
- package/docs/adr/0001-agent-native-runner-boundary.md +31 -0
- package/docs/adr/0002-app-local-zmr-contract.md +39 -0
- package/docs/adr/0003-ios-simulator-xctest-shim.md +41 -0
- package/docs/adr/0004-benchmark-claims-and-baseline-collection.md +37 -0
- package/docs/adr/README.md +12 -0
- package/docs/ai-agents.md +156 -0
- package/docs/app-integration.md +316 -0
- package/docs/benchmarking.md +275 -0
- package/docs/client-installation.md +141 -0
- package/docs/clients.md +98 -0
- package/docs/config.md +175 -0
- package/docs/demo.md +259 -0
- package/docs/dsl.md +57 -0
- package/docs/install.md +233 -0
- package/docs/market-positioning.md +70 -0
- package/docs/npm.md +359 -0
- package/docs/protocol-fixtures/README.md +8 -0
- package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
- package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
- package/docs/protocol-versioning.md +65 -0
- package/docs/protocol.md +560 -0
- package/docs/publication.md +77 -0
- package/docs/release-audit.md +99 -0
- package/docs/release-candidate.md +111 -0
- package/docs/release-evidence.md +188 -0
- package/docs/release-notes-template.md +58 -0
- package/docs/roadmap.md +334 -0
- package/docs/scenario-authoring.md +88 -0
- package/docs/shipping.md +170 -0
- package/docs/trace-privacy.md +88 -0
- package/docs/troubleshooting.md +256 -0
- package/examples/android-app-auth-probe.json +89 -0
- package/examples/android-app-error-state.json +13 -0
- package/examples/android-app-login-smoke.json +192 -0
- package/examples/android-app-onboarding.json +12 -0
- package/examples/android-app-referral-deep-link.json +12 -0
- package/examples/android-shim-smoke.json +19 -0
- package/examples/demo-failure.json +12 -0
- package/examples/demo-fake.json +14 -0
- package/examples/ios-dev-client-open-link.json +26 -0
- package/examples/ios-dev-client-route-snapshot.json +24 -0
- package/examples/ios-shim-smoke.json +23 -0
- package/examples/ios-smoke.json +9 -0
- package/go.work +3 -0
- package/npm/agents.mjs +183 -0
- package/npm/app-config.mjs +95 -0
- package/npm/build-zmr.mjs +21 -0
- package/npm/commands.mjs +104 -0
- package/npm/generated-files.mjs +50 -0
- package/npm/index.mjs +75 -0
- package/npm/init-app.mjs +80 -0
- package/npm/package-scripts.mjs +72 -0
- package/npm/postinstall.mjs +21 -0
- package/npm/scaffold.mjs +179 -0
- package/npm/scenarios.mjs +93 -0
- package/npm/setup.mjs +69 -0
- package/npm/wizard.mjs +117 -0
- package/npm/zmr.mjs +23 -0
- package/package.json +114 -0
- 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 +26 -0
- package/schemas/action-result.schema.json +27 -0
- package/schemas/capabilities-output.schema.json +98 -0
- package/schemas/devices-output.schema.json +25 -0
- package/schemas/doctor-output.schema.json +51 -0
- package/schemas/explain-output.schema.json +51 -0
- package/schemas/import-output.schema.json +23 -0
- package/schemas/init-output.schema.json +71 -0
- package/schemas/json-rpc.schema.json +55 -0
- package/schemas/release-manifest.schema.json +43 -0
- package/schemas/release-readiness-output.schema.json +127 -0
- package/schemas/run-output.schema.json +43 -0
- package/schemas/scenario.schema.json +128 -0
- package/schemas/schemas-output.schema.json +26 -0
- package/schemas/semantic-snapshot.schema.json +116 -0
- package/schemas/snapshot.schema.json +60 -0
- package/schemas/trace-event.schema.json +14 -0
- package/schemas/trace-manifest.schema.json +59 -0
- package/schemas/validate-output.schema.json +42 -0
- package/schemas/version-output.schema.json +23 -0
- package/schemas/zmr-config.schema.json +75 -0
- package/scripts/android-emulator.sh +126 -0
- package/scripts/assert-ios-physical-ready.sh +213 -0
- package/scripts/benchmark-command.sh +307 -0
- package/scripts/benchmark.sh +359 -0
- package/scripts/benchmark_gate.py +117 -0
- package/scripts/benchmark_result_row.py +88 -0
- package/scripts/compare-benchmarks.py +288 -0
- package/scripts/create-android-demo-app.sh +342 -0
- package/scripts/create-ios-demo-app.sh +261 -0
- package/scripts/demo-android-real.sh +232 -0
- package/scripts/demo-ios-real.sh +270 -0
- package/scripts/demo.sh +464 -0
- package/scripts/device-matrix.sh +338 -0
- package/scripts/ensure-ios-shim-target.rb +237 -0
- package/scripts/install-android-shim.sh +281 -0
- package/scripts/install-ios-shim.sh +589 -0
- package/scripts/pilot-gate.sh +560 -0
- package/scripts/release-readiness.py +838 -0
- package/scripts/release-readiness.sh +91 -0
- package/scripts/run-android-pilot.sh +561 -0
- package/scripts/run-ios-pilot.sh +509 -0
- package/shims/android/README.md +21 -0
- package/shims/android/ZMRShimInstrumentedTest.java +152 -0
- package/shims/android/protocol.md +18 -0
- package/shims/ios/README.md +50 -0
- package/shims/ios/ZMRShim.swift +110 -0
- package/shims/ios/ZMRShimUITestCase.swift +475 -0
- package/shims/ios/protocol.md +74 -0
- package/skills/zmr-mobile-testing/SKILL.md +127 -0
- package/src/android.zig +344 -0
- package/src/android_device_info.zig +99 -0
- package/src/android_emulator.zig +154 -0
- package/src/android_screen_recording.zig +112 -0
- package/src/android_shell.zig +112 -0
- package/src/bundle.zig +124 -0
- package/src/bundle_redaction.zig +272 -0
- package/src/bundle_tar.zig +123 -0
- package/src/cli_devices.zig +97 -0
- package/src/cli_doctor.zig +114 -0
- package/src/cli_import.zig +70 -0
- package/src/cli_info.zig +39 -0
- package/src/cli_init.zig +72 -0
- package/src/cli_output.zig +467 -0
- package/src/cli_run.zig +259 -0
- package/src/cli_serve.zig +287 -0
- package/src/cli_trace.zig +111 -0
- package/src/cli_validate.zig +41 -0
- package/src/command.zig +211 -0
- package/src/config.zig +305 -0
- package/src/config_diagnostics.zig +212 -0
- package/src/config_paths.zig +49 -0
- package/src/device_registry.zig +37 -0
- package/src/doctor.zig +412 -0
- package/src/doctor_hints.zig +52 -0
- package/src/errors.zig +55 -0
- package/src/fake_device.zig +163 -0
- package/src/health.zig +28 -0
- package/src/importer.zig +343 -0
- package/src/importer_json.zig +100 -0
- package/src/importer_model.zig +103 -0
- package/src/ios.zig +399 -0
- package/src/ios_devices.zig +219 -0
- package/src/ios_lifecycle.zig +72 -0
- package/src/ios_shim.zig +242 -0
- package/src/ios_snapshot.zig +20 -0
- package/src/json_fields.zig +80 -0
- package/src/json_rpc.zig +150 -0
- package/src/json_rpc_methods.zig +318 -0
- package/src/json_rpc_observation.zig +31 -0
- package/src/json_rpc_params.zig +52 -0
- package/src/json_rpc_protocol.zig +110 -0
- package/src/json_rpc_trace.zig +73 -0
- package/src/main.zig +135 -0
- package/src/mcp.zig +234 -0
- package/src/mcp_protocol.zig +64 -0
- package/src/mcp_trace.zig +83 -0
- package/src/report.zig +346 -0
- package/src/report_html.zig +63 -0
- package/src/report_values.zig +27 -0
- package/src/run_options.zig +152 -0
- package/src/runner.zig +280 -0
- package/src/runner_actions.zig +109 -0
- package/src/runner_config.zig +6 -0
- package/src/runner_diagnostics.zig +268 -0
- package/src/runner_events.zig +170 -0
- package/src/runner_native.zig +88 -0
- package/src/runner_waits.zig +300 -0
- package/src/scaffold.zig +472 -0
- package/src/scenario.zig +346 -0
- package/src/scenario_fields.zig +50 -0
- package/src/schema_registry.zig +53 -0
- package/src/selector.zig +84 -0
- package/src/semantic.zig +171 -0
- package/src/trace.zig +315 -0
- package/src/trace_json.zig +340 -0
- package/src/trace_summary.zig +218 -0
- package/src/trace_summary_diagnostic.zig +202 -0
- package/src/types.zig +120 -0
- package/src/uiautomator.zig +164 -0
- package/src/validation.zig +187 -0
- package/src/version.zig +22 -0
- package/viewer/app.js +373 -0
- package/viewer/index.html +126 -0
- package/viewer/parser.js +233 -0
- package/viewer/styles.css +585 -0
package/src/main.zig
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const cli_devices = @import("cli_devices.zig");
|
|
3
|
+
const cli_doctor = @import("cli_doctor.zig");
|
|
4
|
+
const cli_info = @import("cli_info.zig");
|
|
5
|
+
const cli_init = @import("cli_init.zig");
|
|
6
|
+
const cli_import = @import("cli_import.zig");
|
|
7
|
+
const cli_run = @import("cli_run.zig");
|
|
8
|
+
const cli_serve = @import("cli_serve.zig");
|
|
9
|
+
const cli_trace = @import("cli_trace.zig");
|
|
10
|
+
const cli_validate = @import("cli_validate.zig");
|
|
11
|
+
const errors = @import("errors.zig");
|
|
12
|
+
|
|
13
|
+
pub fn main() void {
|
|
14
|
+
mainInner() catch |err| {
|
|
15
|
+
writeTopLevelError(err);
|
|
16
|
+
std.process.exit(exitCodeForError(err));
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
fn mainInner() !void {
|
|
21
|
+
const GeneralAllocator = if (@hasDecl(std.heap, "GeneralPurposeAllocator"))
|
|
22
|
+
std.heap.GeneralPurposeAllocator
|
|
23
|
+
else
|
|
24
|
+
std.heap.DebugAllocator;
|
|
25
|
+
var gpa = GeneralAllocator(.{}){};
|
|
26
|
+
defer _ = gpa.deinit();
|
|
27
|
+
const allocator = gpa.allocator();
|
|
28
|
+
|
|
29
|
+
var args = try std.process.argsWithAllocator(allocator);
|
|
30
|
+
defer args.deinit();
|
|
31
|
+
_ = args.next();
|
|
32
|
+
const command_name = args.next() orelse {
|
|
33
|
+
try usage();
|
|
34
|
+
return;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (std.mem.eql(u8, command_name, "devices")) {
|
|
38
|
+
try cli_devices.run(allocator, &args);
|
|
39
|
+
} else if (std.mem.eql(u8, command_name, "schemas")) {
|
|
40
|
+
try cli_info.runSchemas(allocator, &args);
|
|
41
|
+
} else if (std.mem.eql(u8, command_name, "doctor")) {
|
|
42
|
+
try cli_doctor.run(allocator, &args);
|
|
43
|
+
} else if (std.mem.eql(u8, command_name, "validate")) {
|
|
44
|
+
try cli_validate.run(allocator, &args);
|
|
45
|
+
} else if (std.mem.eql(u8, command_name, "init")) {
|
|
46
|
+
try cli_init.run(allocator, &args);
|
|
47
|
+
} else if (std.mem.eql(u8, command_name, "import")) {
|
|
48
|
+
try cli_import.run(allocator, &args);
|
|
49
|
+
} else if (std.mem.eql(u8, command_name, "run")) {
|
|
50
|
+
try cli_run.run(allocator, &args);
|
|
51
|
+
} else if (std.mem.eql(u8, command_name, "report")) {
|
|
52
|
+
try cli_trace.runReport(allocator, &args);
|
|
53
|
+
} else if (std.mem.eql(u8, command_name, "explain")) {
|
|
54
|
+
try cli_trace.runExplain(allocator, &args);
|
|
55
|
+
} else if (std.mem.eql(u8, command_name, "export")) {
|
|
56
|
+
try cli_trace.runExport(allocator, &args);
|
|
57
|
+
} else if (std.mem.eql(u8, command_name, "serve")) {
|
|
58
|
+
try cli_serve.runServe(allocator, &args);
|
|
59
|
+
} else if (std.mem.eql(u8, command_name, "mcp")) {
|
|
60
|
+
try cli_serve.runMcp(allocator, &args);
|
|
61
|
+
} else if (std.mem.eql(u8, command_name, "version") or std.mem.eql(u8, command_name, "--version")) {
|
|
62
|
+
try cli_info.runVersion(allocator, &args);
|
|
63
|
+
} else if (std.mem.eql(u8, command_name, "help") or std.mem.eql(u8, command_name, "--help")) {
|
|
64
|
+
try usage();
|
|
65
|
+
} else {
|
|
66
|
+
std.debug.print("unknown command: {s}\n\n", .{command_name});
|
|
67
|
+
try usage();
|
|
68
|
+
return error.UnknownCommand;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn writeTopLevelError(err: anyerror) void {
|
|
73
|
+
const public = errors.classify(err);
|
|
74
|
+
const stderr = std.fs.File.stderr().deprecatedWriter();
|
|
75
|
+
stderr.print("error[{s}]: {s}\n", .{ public.code, public.message }) catch {};
|
|
76
|
+
if (err == error.CommandFailed) {
|
|
77
|
+
stderr.writeAll("hint: run `zmr doctor --json` for setup diagnostics.\n") catch {};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn exitCodeForError(err: anyerror) u8 {
|
|
82
|
+
return switch (err) {
|
|
83
|
+
error.UnknownCommand,
|
|
84
|
+
error.UnknownFlag,
|
|
85
|
+
error.MissingScenarioPath,
|
|
86
|
+
error.MissingDeviceSerial,
|
|
87
|
+
error.MissingTraceDir,
|
|
88
|
+
error.MissingAppId,
|
|
89
|
+
error.MissingAdbPath,
|
|
90
|
+
error.MissingXcrunPath,
|
|
91
|
+
error.MissingZigPath,
|
|
92
|
+
error.MissingPlatform,
|
|
93
|
+
error.MissingIosDeviceType,
|
|
94
|
+
error.MissingParam,
|
|
95
|
+
error.UnsupportedPlatform,
|
|
96
|
+
error.UnsupportedIosDeviceType,
|
|
97
|
+
error.UnsupportedTransport,
|
|
98
|
+
=> 2,
|
|
99
|
+
else => 1,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fn usage() !void {
|
|
104
|
+
const stdout = std.fs.File.stdout().deprecatedWriter();
|
|
105
|
+
try stdout.writeAll(
|
|
106
|
+
\\zmr - Zig Mobile Runner
|
|
107
|
+
\\
|
|
108
|
+
\\Commands:
|
|
109
|
+
\\ zmr version [--json]
|
|
110
|
+
\\ zmr schemas [--json]
|
|
111
|
+
\\ zmr devices [--json] [--platform android|ios] [--ios-device-type simulator|physical|all] [--adb <path>] [--xcrun <path>]
|
|
112
|
+
\\ zmr doctor [--json] [--strict] [--config <path>] [--zig <path>] [--adb <path>] [--android-shim <path>] [--xcrun <path>] [--ios-shim <path>]
|
|
113
|
+
\\ zmr validate <scenario.json> [--json]
|
|
114
|
+
\\ zmr init [scenario.json] [--app-id <id>] [--force] [--json]
|
|
115
|
+
\\ zmr init --app [--dir <app-root>] [--app-id <id>] [--force] [--json]
|
|
116
|
+
\\ zmr import flow-yaml <flow.yaml> --out <scenario.json> [--name <name>] [--app-id <id>] [--force] [--json]
|
|
117
|
+
\\ zmr run [scenario.json] [--json] [--config <path>] [--platform android|ios] [--ios-device-type simulator|physical] [--device <serial>] [--app-id <id>] [--trace-dir <path>] [--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>]
|
|
118
|
+
\\ zmr report <trace-or-benchmark-dir> --out <report.html>
|
|
119
|
+
\\ zmr explain <trace-dir> [--json]
|
|
120
|
+
\\ zmr export <trace-dir> --out <bundle.zmrtrace> [--redact] [--omit-screenshots]
|
|
121
|
+
\\ 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>]
|
|
122
|
+
\\ zmr serve --transport tcp [--port <port>] [--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>]
|
|
123
|
+
\\ zmr mcp [--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>]
|
|
124
|
+
\\
|
|
125
|
+
\\Scenario actions: launch, stop, clearState, openLink, tap, typeText,
|
|
126
|
+
\\eraseText, hideKeyboard, swipe, pressBack, waitVisible, waitNotVisible,
|
|
127
|
+
\\waitAny, whenVisible, repeat, scrollUntilVisible, assertVisible,
|
|
128
|
+
\\assertNotVisible, assertNoneVisible, assertHealthy, snapshot, sleep. Any step may use "optional": true.
|
|
129
|
+
\\
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
test {
|
|
134
|
+
_ = @import("test_harness.zig");
|
|
135
|
+
}
|
package/src/mcp.zig
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const errors = @import("errors.zig");
|
|
3
|
+
const mcp_protocol = @import("mcp_protocol.zig");
|
|
4
|
+
const mcp_trace = @import("mcp_trace.zig");
|
|
5
|
+
const runner = @import("runner.zig");
|
|
6
|
+
const selector = @import("selector.zig");
|
|
7
|
+
const semantic = @import("semantic.zig");
|
|
8
|
+
const trace = @import("trace.zig");
|
|
9
|
+
|
|
10
|
+
pub fn serveStdioWithTrace(allocator: std.mem.Allocator, device: anytype, live_trace: ?*trace.TraceWriter) !void {
|
|
11
|
+
var stdin = std.fs.File.stdin().deprecatedReader();
|
|
12
|
+
const stdout = std.fs.File.stdout().deprecatedWriter();
|
|
13
|
+
|
|
14
|
+
while (true) {
|
|
15
|
+
const line = stdin.readUntilDelimiterOrEofAlloc(allocator, '\n', 16 * 1024 * 1024) catch |err| {
|
|
16
|
+
try mcp_protocol.writeError(stdout, null, -32700, @errorName(err));
|
|
17
|
+
continue;
|
|
18
|
+
};
|
|
19
|
+
const owned_line = line orelse break;
|
|
20
|
+
defer allocator.free(owned_line);
|
|
21
|
+
const trimmed = std.mem.trim(u8, owned_line, " \t\r\n");
|
|
22
|
+
if (trimmed.len == 0) continue;
|
|
23
|
+
try dispatchLine(allocator, device, trimmed, stdout, live_trace);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn dispatchLine(
|
|
28
|
+
allocator: std.mem.Allocator,
|
|
29
|
+
device: anytype,
|
|
30
|
+
line: []const u8,
|
|
31
|
+
writer: anytype,
|
|
32
|
+
live_trace: ?*trace.TraceWriter,
|
|
33
|
+
) !void {
|
|
34
|
+
const parsed = std.json.parseFromSlice(std.json.Value, allocator, line, .{}) catch |err| {
|
|
35
|
+
try mcp_protocol.writeError(writer, null, -32700, @errorName(err));
|
|
36
|
+
return;
|
|
37
|
+
};
|
|
38
|
+
defer parsed.deinit();
|
|
39
|
+
|
|
40
|
+
if (parsed.value != .object) {
|
|
41
|
+
try mcp_protocol.writeError(writer, null, -32600, "request must be an object");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const object = parsed.value.object;
|
|
45
|
+
const id = object.get("id");
|
|
46
|
+
const method_value = object.get("method") orelse {
|
|
47
|
+
try mcp_protocol.writeError(writer, id, -32600, "missing method");
|
|
48
|
+
return;
|
|
49
|
+
};
|
|
50
|
+
if (method_value != .string) {
|
|
51
|
+
try mcp_protocol.writeError(writer, id, -32600, "method must be a string");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
dispatchMethod(allocator, device, method_value.string, object.get("params"), id, writer, live_trace) catch |err| {
|
|
56
|
+
const classified = errors.classify(err);
|
|
57
|
+
try mcp_protocol.writeErrorWithPublicCode(writer, id, -32000, @errorName(err), classified.code);
|
|
58
|
+
return;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fn dispatchMethod(
|
|
63
|
+
allocator: std.mem.Allocator,
|
|
64
|
+
device: anytype,
|
|
65
|
+
method: []const u8,
|
|
66
|
+
params: ?std.json.Value,
|
|
67
|
+
id: ?std.json.Value,
|
|
68
|
+
writer: anytype,
|
|
69
|
+
live_trace: ?*trace.TraceWriter,
|
|
70
|
+
) !void {
|
|
71
|
+
if (std.mem.eql(u8, method, "initialize")) {
|
|
72
|
+
const protocol_version = optionalParamString(params, "protocolVersion") orelse "2024-11-05";
|
|
73
|
+
try mcp_protocol.writeInitializeResult(writer, id, protocol_version);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (std.mem.eql(u8, method, "ping")) {
|
|
78
|
+
try mcp_protocol.writeResultRaw(writer, id, "{}");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (std.mem.eql(u8, method, "tools/list")) {
|
|
83
|
+
try mcp_protocol.writeToolListResult(writer, id);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (std.mem.eql(u8, method, "tools/call")) {
|
|
88
|
+
const tool_name = try requiredParamString(params, "name");
|
|
89
|
+
const arguments = paramField(params, "arguments");
|
|
90
|
+
try callTool(allocator, device, tool_name, arguments, id, writer, live_trace);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try mcp_protocol.writeError(writer, id, -32601, "method not found");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fn callTool(
|
|
98
|
+
allocator: std.mem.Allocator,
|
|
99
|
+
device: anytype,
|
|
100
|
+
tool_name: []const u8,
|
|
101
|
+
arguments: ?std.json.Value,
|
|
102
|
+
id: ?std.json.Value,
|
|
103
|
+
writer: anytype,
|
|
104
|
+
live_trace: ?*trace.TraceWriter,
|
|
105
|
+
) !void {
|
|
106
|
+
if (std.mem.eql(u8, tool_name, "snapshot")) {
|
|
107
|
+
var snap = try device.snapshot(live_trace);
|
|
108
|
+
defer snap.deinit(device.allocator);
|
|
109
|
+
var payload = std.ArrayList(u8).empty;
|
|
110
|
+
defer payload.deinit(allocator);
|
|
111
|
+
try trace.writeSnapshotJson(payload.writer(allocator), snap);
|
|
112
|
+
try mcp_protocol.writeToolTextResult(writer, id, payload.items);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (std.mem.eql(u8, tool_name, "semantic_snapshot")) {
|
|
117
|
+
var snap = try device.snapshot(live_trace);
|
|
118
|
+
defer snap.deinit(device.allocator);
|
|
119
|
+
if (live_trace) |tw| {
|
|
120
|
+
const path = try tw.writeSnapshot(snap);
|
|
121
|
+
defer tw.allocator.free(path);
|
|
122
|
+
try tw.recordEvent("observe.semanticSnapshot", "{\"status\":\"ok\"}");
|
|
123
|
+
}
|
|
124
|
+
var payload = std.ArrayList(u8).empty;
|
|
125
|
+
defer payload.deinit(allocator);
|
|
126
|
+
try semantic.writeSemanticSnapshotJson(payload.writer(allocator), snap);
|
|
127
|
+
try mcp_protocol.writeToolTextResult(writer, id, payload.items);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (std.mem.eql(u8, tool_name, "tap")) {
|
|
132
|
+
const wanted = try parseArgumentsSelector(allocator, arguments);
|
|
133
|
+
defer wanted.deinit(allocator);
|
|
134
|
+
try runner.tapSelector(device, wanted, live_trace, .{});
|
|
135
|
+
try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":true}");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (std.mem.eql(u8, tool_name, "type")) {
|
|
140
|
+
const text = try requiredParamString(arguments, "text");
|
|
141
|
+
if (paramField(arguments, "selector")) |selector_value| {
|
|
142
|
+
const wanted = try selector.parseFromJson(allocator, selector_value);
|
|
143
|
+
defer wanted.deinit(allocator);
|
|
144
|
+
try runner.typeTextSelector(device, wanted, text, live_trace, .{});
|
|
145
|
+
} else {
|
|
146
|
+
try device.typeText(text);
|
|
147
|
+
}
|
|
148
|
+
try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":true}");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (std.mem.eql(u8, tool_name, "press_back")) {
|
|
153
|
+
try device.pressBack();
|
|
154
|
+
try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":true}");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (std.mem.eql(u8, tool_name, "open_link")) {
|
|
159
|
+
try device.openLink(try requiredParamString(arguments, "url"));
|
|
160
|
+
try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":true}");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (std.mem.eql(u8, tool_name, "wait_visible")) {
|
|
165
|
+
const wanted = try parseArgumentsSelector(allocator, arguments);
|
|
166
|
+
defer wanted.deinit(allocator);
|
|
167
|
+
const timeout_ms = try optionalParamU64(arguments, "timeoutMs", 5000);
|
|
168
|
+
const visible = try runner.waitUntilVisible(device, wanted, timeout_ms, live_trace, .{});
|
|
169
|
+
try writer.writeAll("{\"jsonrpc\":\"2.0\",\"id\":");
|
|
170
|
+
try mcp_protocol.writeId(writer, id);
|
|
171
|
+
try writer.writeAll(",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"{\\\"visible\\\":");
|
|
172
|
+
try writer.writeAll(if (visible) "true" else "false");
|
|
173
|
+
try writer.writeAll("}\"}]}}\n");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (std.mem.eql(u8, tool_name, "trace_events")) {
|
|
178
|
+
try mcp_trace.writeEventsToolResult(allocator, writer, id, live_trace, try optionalParamU64(arguments, "afterSeq", 0), @min(try optionalParamU64(arguments, "limit", 100), 1000));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (std.mem.eql(u8, tool_name, "trace_export")) {
|
|
183
|
+
const out_path = try requiredParamString(arguments, "out");
|
|
184
|
+
const omit_screenshots = try optionalParamBool(arguments, "omitScreenshots", false);
|
|
185
|
+
const redact = try optionalParamBool(arguments, "redact", false) or omit_screenshots;
|
|
186
|
+
try mcp_trace.writeExportToolResult(allocator, writer, id, live_trace, out_path, redact, omit_screenshots);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try mcp_protocol.writeError(writer, id, -32602, "unknown tool");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
fn parseArgumentsSelector(allocator: std.mem.Allocator, arguments: ?std.json.Value) !selector.Selector {
|
|
194
|
+
const selector_value = paramField(arguments, "selector") orelse return error.MissingSelector;
|
|
195
|
+
return try selector.parseFromJson(allocator, selector_value);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fn paramField(params: ?std.json.Value, key: []const u8) ?std.json.Value {
|
|
199
|
+
const value = params orelse return null;
|
|
200
|
+
if (value != .object) return null;
|
|
201
|
+
return value.object.get(key);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
fn requiredParamString(params: ?std.json.Value, key: []const u8) ![]const u8 {
|
|
205
|
+
const value = paramField(params, key) orelse return error.MissingParam;
|
|
206
|
+
return switch (value) {
|
|
207
|
+
.string => |actual| actual,
|
|
208
|
+
else => error.ParamMustBeString,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fn optionalParamString(params: ?std.json.Value, key: []const u8) ?[]const u8 {
|
|
213
|
+
const value = paramField(params, key) orelse return null;
|
|
214
|
+
return switch (value) {
|
|
215
|
+
.string => |actual| actual,
|
|
216
|
+
else => null,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fn optionalParamU64(params: ?std.json.Value, key: []const u8, default_value: u64) !u64 {
|
|
221
|
+
const value = paramField(params, key) orelse return default_value;
|
|
222
|
+
return switch (value) {
|
|
223
|
+
.integer => |actual| @as(u64, @intCast(actual)),
|
|
224
|
+
else => error.ParamMustBeInteger,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fn optionalParamBool(params: ?std.json.Value, key: []const u8, default_value: bool) !bool {
|
|
229
|
+
const value = paramField(params, key) orelse return default_value;
|
|
230
|
+
return switch (value) {
|
|
231
|
+
.bool => |actual| actual,
|
|
232
|
+
else => error.ParamMustBeBool,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const trace = @import("trace.zig");
|
|
3
|
+
const version = @import("version.zig");
|
|
4
|
+
|
|
5
|
+
pub fn writeInitializeResult(writer: anytype, id: ?std.json.Value, protocol_version: []const u8) !void {
|
|
6
|
+
try writer.writeAll("{\"jsonrpc\":\"2.0\",\"id\":");
|
|
7
|
+
try writeId(writer, id);
|
|
8
|
+
try writer.writeAll(",\"result\":{\"protocolVersion\":");
|
|
9
|
+
try trace.writeJsonString(writer, protocol_version);
|
|
10
|
+
try writer.writeAll(",\"capabilities\":{\"tools\":{}},\"serverInfo\":{\"name\":\"zmr\",\"version\":");
|
|
11
|
+
try trace.writeJsonString(writer, version.runner_version);
|
|
12
|
+
try writer.writeAll("}}}\n");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
pub fn writeToolListResult(writer: anytype, id: ?std.json.Value) !void {
|
|
16
|
+
try writeResultRaw(writer, id, tool_list_json);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
pub fn writeToolTextResult(writer: anytype, id: ?std.json.Value, text: []const u8) !void {
|
|
20
|
+
try writer.writeAll("{\"jsonrpc\":\"2.0\",\"id\":");
|
|
21
|
+
try writeId(writer, id);
|
|
22
|
+
try writer.writeAll(",\"result\":{\"content\":[{\"type\":\"text\",\"text\":");
|
|
23
|
+
try trace.writeJsonString(writer, text);
|
|
24
|
+
try writer.writeAll("}]}}\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pub fn writeResultRaw(writer: anytype, id: ?std.json.Value, raw_json: []const u8) !void {
|
|
28
|
+
try writer.writeAll("{\"jsonrpc\":\"2.0\",\"id\":");
|
|
29
|
+
try writeId(writer, id);
|
|
30
|
+
try writer.writeAll(",\"result\":");
|
|
31
|
+
try writer.writeAll(raw_json);
|
|
32
|
+
try writer.writeAll("}\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pub fn writeError(writer: anytype, id: ?std.json.Value, code: i32, message: []const u8) !void {
|
|
36
|
+
try writeErrorWithPublicCode(writer, id, code, message, null);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
pub fn writeErrorWithPublicCode(writer: anytype, id: ?std.json.Value, code: i32, message: []const u8, public_code: ?[]const u8) !void {
|
|
40
|
+
try writer.writeAll("{\"jsonrpc\":\"2.0\",\"id\":");
|
|
41
|
+
try writeId(writer, id);
|
|
42
|
+
try writer.print(",\"error\":{{\"code\":{d},\"message\":", .{code});
|
|
43
|
+
try trace.writeJsonString(writer, message);
|
|
44
|
+
if (public_code) |value| {
|
|
45
|
+
try writer.writeAll(",\"publicCode\":");
|
|
46
|
+
try trace.writeJsonString(writer, value);
|
|
47
|
+
}
|
|
48
|
+
try writer.writeAll("}}\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pub fn writeId(writer: anytype, id: ?std.json.Value) !void {
|
|
52
|
+
const value = id orelse {
|
|
53
|
+
try writer.writeAll("null");
|
|
54
|
+
return;
|
|
55
|
+
};
|
|
56
|
+
switch (value) {
|
|
57
|
+
.null => try writer.writeAll("null"),
|
|
58
|
+
.string => |actual| try trace.writeJsonString(writer, actual),
|
|
59
|
+
.integer => |actual| try writer.print("{d}", .{actual}),
|
|
60
|
+
else => try writer.writeAll("null"),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const tool_list_json = "{\"tools\":[" ++ "{\"name\":\"snapshot\",\"description\":\"Capture the current mobile observation snapshot as JSON.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{}}}," ++ "{\"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\":{}}}," ++ "{\"name\":\"tap\",\"description\":\"Tap a visible element by selector.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selector\"],\"properties\":{\"selector\":{\"type\":\"object\"}}}}," ++ "{\"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\"}}}}," ++ "{\"name\":\"press_back\",\"description\":\"Press Android back or the platform-equivalent navigation action.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{}}}," ++ "{\"name\":\"open_link\",\"description\":\"Open a deep link URL in the target app.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"url\"],\"properties\":{\"url\":{\"type\":\"string\"}}}}," ++ "{\"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}}}}," ++ "{\"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}}}}," ++ "{\"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\"}}}}" ++ "]}";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const bundle = @import("bundle.zig");
|
|
3
|
+
const mcp_protocol = @import("mcp_protocol.zig");
|
|
4
|
+
const trace = @import("trace.zig");
|
|
5
|
+
|
|
6
|
+
pub fn writeEventsToolResult(
|
|
7
|
+
allocator: std.mem.Allocator,
|
|
8
|
+
writer: anytype,
|
|
9
|
+
id: ?std.json.Value,
|
|
10
|
+
live_trace: ?*trace.TraceWriter,
|
|
11
|
+
after_seq: u64,
|
|
12
|
+
limit: u64,
|
|
13
|
+
) !void {
|
|
14
|
+
const tw = live_trace orelse {
|
|
15
|
+
try mcp_protocol.writeToolTextResult(writer, id, "{\"traceDir\":null,\"events\":[]}");
|
|
16
|
+
return;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const events_path = try std.fs.path.join(allocator, &.{ tw.root_dir, "events.jsonl" });
|
|
20
|
+
defer allocator.free(events_path);
|
|
21
|
+
const content = std.fs.cwd().readFileAlloc(allocator, events_path, 64 * 1024 * 1024) catch |err| switch (err) {
|
|
22
|
+
error.FileNotFound => try allocator.dupe(u8, ""),
|
|
23
|
+
else => return err,
|
|
24
|
+
};
|
|
25
|
+
defer allocator.free(content);
|
|
26
|
+
|
|
27
|
+
var payload = std.ArrayList(u8).empty;
|
|
28
|
+
defer payload.deinit(allocator);
|
|
29
|
+
const payload_writer = payload.writer(allocator);
|
|
30
|
+
try payload_writer.writeAll("{\"traceDir\":");
|
|
31
|
+
try trace.writeJsonString(payload_writer, tw.root_dir);
|
|
32
|
+
try payload_writer.print(",\"afterSeq\":{d},\"events\":[", .{after_seq});
|
|
33
|
+
var emitted: u64 = 0;
|
|
34
|
+
var lines = std.mem.splitScalar(u8, content, '\n');
|
|
35
|
+
while (lines.next()) |raw_line| {
|
|
36
|
+
if (emitted >= limit) break;
|
|
37
|
+
const line = std.mem.trim(u8, raw_line, " \t\r\n");
|
|
38
|
+
if (line.len == 0) continue;
|
|
39
|
+
const parsed = std.json.parseFromSlice(std.json.Value, allocator, line, .{}) catch continue;
|
|
40
|
+
defer parsed.deinit();
|
|
41
|
+
if (parsed.value != .object) continue;
|
|
42
|
+
const seq_value = parsed.value.object.get("seq") orelse continue;
|
|
43
|
+
if (seq_value != .integer or seq_value.integer <= 0) continue;
|
|
44
|
+
const seq = @as(u64, @intCast(seq_value.integer));
|
|
45
|
+
if (seq <= after_seq) continue;
|
|
46
|
+
if (emitted > 0) try payload_writer.writeAll(",");
|
|
47
|
+
try payload_writer.writeAll(line);
|
|
48
|
+
emitted += 1;
|
|
49
|
+
}
|
|
50
|
+
try payload_writer.writeAll("]}");
|
|
51
|
+
try mcp_protocol.writeToolTextResult(writer, id, payload.items);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
pub fn writeExportToolResult(
|
|
55
|
+
allocator: std.mem.Allocator,
|
|
56
|
+
writer: anytype,
|
|
57
|
+
id: ?std.json.Value,
|
|
58
|
+
live_trace: ?*trace.TraceWriter,
|
|
59
|
+
out_path: []const u8,
|
|
60
|
+
redact: bool,
|
|
61
|
+
omit_screenshots: bool,
|
|
62
|
+
) !void {
|
|
63
|
+
const tw = live_trace orelse {
|
|
64
|
+
try mcp_protocol.writeToolTextResult(writer, id, "{\"traceDir\":null,\"message\":\"start zmr mcp with --trace-dir to enable export\"}");
|
|
65
|
+
return;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
try tw.flushManifest();
|
|
69
|
+
try bundle.exportTraceBundleWithOptions(allocator, tw.root_dir, out_path, .{
|
|
70
|
+
.redact = redact,
|
|
71
|
+
.omit_screenshots = omit_screenshots,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
var payload = std.ArrayList(u8).empty;
|
|
75
|
+
defer payload.deinit(allocator);
|
|
76
|
+
const payload_writer = payload.writer(allocator);
|
|
77
|
+
try payload_writer.writeAll("{\"traceDir\":");
|
|
78
|
+
try trace.writeJsonString(payload_writer, tw.root_dir);
|
|
79
|
+
try payload_writer.writeAll(",\"out\":");
|
|
80
|
+
try trace.writeJsonString(payload_writer, out_path);
|
|
81
|
+
try payload_writer.print(",\"redacted\":{},\"omitScreenshots\":{}}}", .{ redact, omit_screenshots });
|
|
82
|
+
try mcp_protocol.writeToolTextResult(writer, id, payload.items);
|
|
83
|
+
}
|