mcp-baepsae 5.1.0 → 6.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/README-KR.md +98 -33
- package/README.md +101 -35
- package/bundled/baepsae-native +0 -0
- package/dist/backend.d.ts +26 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +79 -0
- package/dist/backend.js.map +1 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/tool-manifest.d.ts +12 -0
- package/dist/tool-manifest.d.ts.map +1 -0
- package/dist/tool-manifest.js +79 -0
- package/dist/tool-manifest.js.map +1 -0
- package/dist/tools/info.d.ts.map +1 -1
- package/dist/tools/info.js +104 -5
- package/dist/tools/info.js.map +1 -1
- package/dist/tools/input.js +7 -6
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/media.d.ts.map +1 -1
- package/dist/tools/media.js +137 -11
- package/dist/tools/media.js.map +1 -1
- package/dist/tools/simulator.js +7 -7
- package/dist/tools/simulator.js.map +1 -1
- package/dist/tools/system.d.ts.map +1 -1
- package/dist/tools/system.js +2 -2
- package/dist/tools/system.js.map +1 -1
- package/dist/tools/ui.d.ts.map +1 -1
- package/dist/tools/ui.js +126 -8
- package/dist/tools/ui.js.map +1 -1
- package/dist/tools/workflow.d.ts +3 -0
- package/dist/tools/workflow.d.ts.map +1 -0
- package/dist/tools/workflow.js +434 -0
- package/dist/tools/workflow.js.map +1 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +19 -3
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +110 -5
- package/dist/utils.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/native/Sources/Commands/InputCommands.swift +53 -33
- package/native/Sources/Commands/SystemCommands.swift +86 -0
- package/native/Sources/Commands/UICommands.swift +254 -35
- package/native/Sources/Commands/WindowCommands.swift +11 -4
- package/native/Sources/IndigoHID/IndigoHIDClient.swift +222 -0
- package/native/Sources/IndigoHID/IndigoHIDCoordinates.swift +74 -0
- package/native/Sources/IndigoHID/IndigoHIDEvents.swift +63 -0
- package/native/Sources/IndigoHID/IndigoHIDLoader.swift +102 -0
- package/native/Sources/IndigoHID/IndigoHIDTypes.swift +41 -0
- package/native/Sources/Types.swift +26 -0
- package/native/Sources/Utils.swift +653 -13
- package/native/Sources/Version.swift +1 -1
- package/native/Sources/main.swift +55 -8
- package/native/Tests/BaepsaeNativeTests/BinaryInvocationTests.swift +54 -6
- package/package.json +12 -3
- package/scripts/dump-tabbar-actions.mjs +312 -0
- package/scripts/generate-tool-manifest.mjs +75 -0
- package/scripts/research-coordinate-calibration.mjs +276 -0
- package/scripts/research-input-channels.mjs +327 -0
- package/scripts/research-tap-tab-grid.mjs +271 -0
- package/scripts/verify-media-capture.mjs +99 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
const projectRoot = path.resolve(__dirname, "..");
|
|
12
|
+
const realNativePath = path.join(projectRoot, "native", ".build", "release", "baepsae-native");
|
|
13
|
+
|
|
14
|
+
function extractText(result) {
|
|
15
|
+
return result.content
|
|
16
|
+
.filter((item) => item.type === "text")
|
|
17
|
+
.map((item) => item.text)
|
|
18
|
+
.join("\n");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function extractBootedUdid(text) {
|
|
22
|
+
const match = text.match(/\(([0-9A-F-]{36})\)\s+\(Booted\)/i);
|
|
23
|
+
return match ? match[1] : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseFrame(text) {
|
|
27
|
+
const match = text.match(/frame=\(x:([\d.]+),y:([\d.]+),w:([\d.]+),h:([\d.]+)\)/);
|
|
28
|
+
if (!match) return null;
|
|
29
|
+
return {
|
|
30
|
+
x: parseFloat(match[1]),
|
|
31
|
+
y: parseFloat(match[2]),
|
|
32
|
+
width: parseFloat(match[3]),
|
|
33
|
+
height: parseFloat(match[4]),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function extractLargestFrameFromLines(text, predicate) {
|
|
38
|
+
const lines = text.split("\n").filter((line) => predicate(line));
|
|
39
|
+
let best = null;
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
const frame = parseFrame(line);
|
|
42
|
+
if (!frame) continue;
|
|
43
|
+
const area = frame.width * frame.height;
|
|
44
|
+
if (!best || area > best.area) {
|
|
45
|
+
best = { line, frame, area };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return best ? { line: best.line, frame: best.frame } : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseWindowFrame(text) {
|
|
52
|
+
const match = text.match(/\(([\d.]+),([\d.]+),([\d.]+),([\d.]+)\)/);
|
|
53
|
+
if (!match) return null;
|
|
54
|
+
return {
|
|
55
|
+
x: parseFloat(match[1]),
|
|
56
|
+
y: parseFloat(match[2]),
|
|
57
|
+
width: parseFloat(match[3]),
|
|
58
|
+
height: parseFloat(match[4]),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function chooseSimulatorUdid() {
|
|
63
|
+
const output = execFileSync("xcrun", ["simctl", "list", "devices", "available"], {
|
|
64
|
+
cwd: projectRoot,
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
67
|
+
});
|
|
68
|
+
const booted = output.match(/\(([0-9A-F-]{36})\)\s+\(Booted\)/i);
|
|
69
|
+
if (booted) return booted[1];
|
|
70
|
+
|
|
71
|
+
const shutdownIPhone = output.match(/^\s+iPhone .* \(([0-9A-F-]{36})\) \(Shutdown\)\s*$/m);
|
|
72
|
+
if (shutdownIPhone) return shutdownIPhone[1];
|
|
73
|
+
|
|
74
|
+
throw new Error("No available iPhone Simulator device found.");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function ensureBootedSimulator(udid) {
|
|
78
|
+
try {
|
|
79
|
+
execFileSync("xcrun", ["simctl", "boot", udid], { cwd: projectRoot, stdio: "pipe" });
|
|
80
|
+
} catch {
|
|
81
|
+
// already booted or transient boot error
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
execFileSync("/usr/bin/open", ["-a", "Simulator", "--args", "-CurrentDeviceUDID", udid], {
|
|
85
|
+
cwd: projectRoot,
|
|
86
|
+
stdio: "pipe",
|
|
87
|
+
});
|
|
88
|
+
} catch {
|
|
89
|
+
// ignore
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const deadline = Date.now() + 15_000;
|
|
93
|
+
while (Date.now() < deadline) {
|
|
94
|
+
const output = execFileSync("xcrun", ["simctl", "list", "devices", udid], {
|
|
95
|
+
cwd: projectRoot,
|
|
96
|
+
encoding: "utf8",
|
|
97
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
98
|
+
});
|
|
99
|
+
if (/\(Booted\)/.test(output)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
await sleep(500);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
throw new Error(`Simulator did not boot in time: ${udid}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const sampleAppPath = [
|
|
109
|
+
path.join(projectRoot, "test-fixtures", "SampleApp", "build", "Debug-iphonesimulator", "SampleApp.app"),
|
|
110
|
+
path.join(projectRoot, "test-fixtures", "SampleApp", "Build", "Products", "Debug-iphonesimulator", "SampleApp.app"),
|
|
111
|
+
].find((candidate) => existsSync(candidate));
|
|
112
|
+
|
|
113
|
+
async function sleep(ms) {
|
|
114
|
+
return await new Promise((resolve) => setTimeout(resolve, ms));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function withClient(run) {
|
|
118
|
+
const client = new Client({ name: "baepsae-tap-tab-research", version: "1.0.0" });
|
|
119
|
+
const transport = new StdioClientTransport({
|
|
120
|
+
command: "node",
|
|
121
|
+
args: ["dist/index.js"],
|
|
122
|
+
cwd: projectRoot,
|
|
123
|
+
stderr: "pipe",
|
|
124
|
+
env: {
|
|
125
|
+
...process.env,
|
|
126
|
+
BAEPSAE_NATIVE_PATH: process.env.BAEPSAE_NATIVE_PATH ?? realNativePath,
|
|
127
|
+
BAEPSAE_INPUT_BACKEND: process.env.BAEPSAE_INPUT_BACKEND ?? "cgevent",
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await client.connect(transport);
|
|
132
|
+
try {
|
|
133
|
+
return await run(client);
|
|
134
|
+
} finally {
|
|
135
|
+
await client.close();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function call(client, name, args) {
|
|
140
|
+
const result = await client.callTool({ name, arguments: args });
|
|
141
|
+
return { result, text: extractText(result) };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function relaunchSampleApp(client, udid) {
|
|
145
|
+
await call(client, "terminate_app", { udid, bundleId: "com.baepsae.sampleapp" }).catch(() => {});
|
|
146
|
+
await sleep(500);
|
|
147
|
+
if (sampleAppPath) {
|
|
148
|
+
await call(client, "install_app", { udid, path: sampleAppPath });
|
|
149
|
+
}
|
|
150
|
+
await call(client, "launch_app", {
|
|
151
|
+
udid,
|
|
152
|
+
bundleId: "com.baepsae.sampleapp",
|
|
153
|
+
args: ["--tabview-research"],
|
|
154
|
+
});
|
|
155
|
+
const deadline = Date.now() + 10000;
|
|
156
|
+
while (Date.now() < deadline) {
|
|
157
|
+
const probe = await call(client, "analyze_ui", { udid, focusId: "research-home-anchor", maxDepth: 1 });
|
|
158
|
+
if (!probe.result.isError && probe.text.includes("Research Home")) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
await sleep(500);
|
|
162
|
+
}
|
|
163
|
+
await sleep(1500);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function main() {
|
|
167
|
+
const summary = await withClient(async (client) => {
|
|
168
|
+
const udid = chooseSimulatorUdid();
|
|
169
|
+
await ensureBootedSimulator(udid);
|
|
170
|
+
|
|
171
|
+
await relaunchSampleApp(client, udid);
|
|
172
|
+
|
|
173
|
+
const allTree = await call(client, "analyze_ui", { udid, all: true, maxDepth: 8 });
|
|
174
|
+
const contentRootCandidate = extractLargestFrameFromLines(
|
|
175
|
+
allTree.text,
|
|
176
|
+
(line) => line.includes("subrole=iOSContentGroup"),
|
|
177
|
+
);
|
|
178
|
+
const tabBarCandidate = extractLargestFrameFromLines(
|
|
179
|
+
allTree.text,
|
|
180
|
+
(line) => line.includes("role=AXGroup text=Tab Bar"),
|
|
181
|
+
);
|
|
182
|
+
const windowCandidate = extractLargestFrameFromLines(
|
|
183
|
+
allTree.text,
|
|
184
|
+
(line) => line.includes("role=AXWindow subrole=AXStandardWindow"),
|
|
185
|
+
);
|
|
186
|
+
const tabBarLine = tabBarCandidate?.line ?? null;
|
|
187
|
+
const tabBarFrame = tabBarCandidate?.frame ?? null;
|
|
188
|
+
const contentFrame = contentRootCandidate?.frame ?? null;
|
|
189
|
+
const windowFrame = windowCandidate?.frame ?? null;
|
|
190
|
+
|
|
191
|
+
if (!tabBarFrame || !contentFrame || !windowFrame) {
|
|
192
|
+
return {
|
|
193
|
+
udid,
|
|
194
|
+
tabBarLine,
|
|
195
|
+
contentFrame,
|
|
196
|
+
windowFrame,
|
|
197
|
+
hits: [],
|
|
198
|
+
attempts: [],
|
|
199
|
+
note: "Tab bar frame, content frame, or window frame could not be determined.",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const targetIndex = 1;
|
|
204
|
+
const tabCount = 4;
|
|
205
|
+
const xRatioCandidates = [0.35, 0.5, 0.65];
|
|
206
|
+
const yRatioCandidates = [0.3, 0.45, 0.6];
|
|
207
|
+
const bases = ["window", "content"];
|
|
208
|
+
const attempts = [];
|
|
209
|
+
const hits = [];
|
|
210
|
+
const slotWidth = tabBarFrame.width / tabCount;
|
|
211
|
+
|
|
212
|
+
for (const base of bases) {
|
|
213
|
+
for (const xRatio of xRatioCandidates) {
|
|
214
|
+
for (const yRatio of yRatioCandidates) {
|
|
215
|
+
const absoluteX = tabBarFrame.x + slotWidth * targetIndex + slotWidth * xRatio;
|
|
216
|
+
const absoluteY = tabBarFrame.y + tabBarFrame.height * yRatio;
|
|
217
|
+
const x =
|
|
218
|
+
base === "window"
|
|
219
|
+
? absoluteX - windowFrame.x
|
|
220
|
+
: absoluteX - contentFrame.x;
|
|
221
|
+
const y =
|
|
222
|
+
base === "window"
|
|
223
|
+
? absoluteY - windowFrame.y
|
|
224
|
+
: absoluteY - contentFrame.y;
|
|
225
|
+
|
|
226
|
+
await relaunchSampleApp(client, udid);
|
|
227
|
+
await call(client, "tap", { udid, x, y });
|
|
228
|
+
await sleep(1200);
|
|
229
|
+
|
|
230
|
+
const scrollPosition = await call(client, "analyze_ui", { udid, focusId: "research-scroll-anchor", maxDepth: 1 });
|
|
231
|
+
const ok = !scrollPosition.result.isError && /Research Scroll/.test(scrollPosition.text);
|
|
232
|
+
|
|
233
|
+
const entry = {
|
|
234
|
+
base,
|
|
235
|
+
xRatio,
|
|
236
|
+
yRatio,
|
|
237
|
+
x: Math.round(x * 10) / 10,
|
|
238
|
+
y: Math.round(y * 10) / 10,
|
|
239
|
+
ok,
|
|
240
|
+
tail: scrollPosition.text.split("\n").slice(-3).join(" | "),
|
|
241
|
+
};
|
|
242
|
+
attempts.push(entry);
|
|
243
|
+
if (ok) hits.push(entry);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
udid,
|
|
250
|
+
tabBarLine,
|
|
251
|
+
tabBarFrame,
|
|
252
|
+
contentFrame,
|
|
253
|
+
windowFrame,
|
|
254
|
+
targetIndex,
|
|
255
|
+
tabCount,
|
|
256
|
+
attempts,
|
|
257
|
+
hits,
|
|
258
|
+
note: hits.length > 0
|
|
259
|
+
? "At least one coordinate switched to the Scroll tab."
|
|
260
|
+
: "No tested coordinate switched to the Scroll tab.",
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
265
|
+
process.exitCode = summary.hits?.length ? 0 : 1;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
main().catch((error) => {
|
|
269
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
270
|
+
process.exitCode = 1;
|
|
271
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { mkdir, stat } from "node:fs/promises";
|
|
4
|
+
|
|
5
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
const projectRoot = path.resolve(__dirname, "..");
|
|
11
|
+
const artifactDir = path.join(projectRoot, ".tmp-test-artifacts");
|
|
12
|
+
const realNativePath = path.join(projectRoot, "native", ".build", "release", "baepsae-native");
|
|
13
|
+
|
|
14
|
+
function extractText(result) {
|
|
15
|
+
return result.content
|
|
16
|
+
.filter((item) => item.type === "text")
|
|
17
|
+
.map((item) => item.text)
|
|
18
|
+
.join("\n");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function extractBootedUdid(text) {
|
|
22
|
+
const match = text.match(/\(([0-9A-F-]{36})\)\s+\(Booted\)/i);
|
|
23
|
+
return match ? match[1] : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function withClient(run) {
|
|
27
|
+
const client = new Client({ name: "baepsae-media-verify", version: "1.0.0" });
|
|
28
|
+
const transport = new StdioClientTransport({
|
|
29
|
+
command: "node",
|
|
30
|
+
args: ["dist/index.js"],
|
|
31
|
+
cwd: projectRoot,
|
|
32
|
+
stderr: "pipe",
|
|
33
|
+
env: {
|
|
34
|
+
...process.env,
|
|
35
|
+
BAEPSAE_NATIVE_PATH: process.env.BAEPSAE_NATIVE_PATH ?? realNativePath,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await client.connect(transport);
|
|
40
|
+
try {
|
|
41
|
+
return await run(client);
|
|
42
|
+
} finally {
|
|
43
|
+
await client.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function main() {
|
|
48
|
+
await mkdir(artifactDir, { recursive: true });
|
|
49
|
+
|
|
50
|
+
const summary = await withClient(async (client) => {
|
|
51
|
+
const listResult = await client.callTool({ name: "list_simulators", arguments: {} });
|
|
52
|
+
const udid = extractBootedUdid(extractText(listResult));
|
|
53
|
+
if (!udid) {
|
|
54
|
+
throw new Error("No booted simulator detected.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const probes = [
|
|
58
|
+
["screenshot", { udid, output: path.join(artifactDir, `verify-media-${Date.now()}.png`) }],
|
|
59
|
+
["record_video", { udid, output: path.join(artifactDir, `verify-media-${Date.now()}.mov`), durationSeconds: 2 }],
|
|
60
|
+
["stream_video", { udid, output: path.join(artifactDir, `verify-media-${Date.now()}.mov`), durationSeconds: 2 }],
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const results = [];
|
|
64
|
+
for (const [name, args] of probes) {
|
|
65
|
+
const result = await client.callTool({ name, arguments: args });
|
|
66
|
+
const text = extractText(result);
|
|
67
|
+
const outputPath = args.output;
|
|
68
|
+
let fileSize = null;
|
|
69
|
+
try {
|
|
70
|
+
const info = await stat(outputPath);
|
|
71
|
+
fileSize = info.size;
|
|
72
|
+
} catch {
|
|
73
|
+
// ignore
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
results.push({
|
|
77
|
+
tool: name,
|
|
78
|
+
isError: !!result.isError,
|
|
79
|
+
outputPath,
|
|
80
|
+
fileSize,
|
|
81
|
+
text,
|
|
82
|
+
error: result.error ?? null,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return { udid, results };
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
89
|
+
|
|
90
|
+
const failed = summary.results.filter((entry) => entry.isError || !entry.fileSize || entry.fileSize <= 0);
|
|
91
|
+
if (failed.length > 0) {
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
main().catch((error) => {
|
|
97
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
});
|