vibeview 0.7.1 → 0.8.1

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.
@@ -0,0 +1,367 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AGENT_COMMANDS,
4
+ PLATFORM_DEVICE,
5
+ buildToolCall,
6
+ checkMetro,
7
+ decodeScreenshotResult,
8
+ isDevPlatform,
9
+ loadDevConfig,
10
+ readDevState,
11
+ renderEnvelope,
12
+ startDevSession
13
+ } from "./chunk-WG2XR45H.js";
14
+ import {
15
+ ApiClient,
16
+ resolveAuth
17
+ } from "./chunk-YD2X6TQV.js";
18
+
19
+ // src/mcp/server.ts
20
+ import { writeFileSync } from "fs";
21
+ import { tmpdir } from "os";
22
+ import { join } from "path";
23
+ import chalk from "chalk";
24
+ var SESSION_ID_PROPERTY = {
25
+ type: "string",
26
+ description: "Session ID to target. Optional: falls back to the MCP-server-held dev_start session, then this project's `vibeview dev --detach` state, if either is available."
27
+ };
28
+ var DEV_PLATFORMS = ["ios", "android", "tvos", "androidtv"];
29
+ function paramToProperty(param) {
30
+ return { type: param.type, description: param.description };
31
+ }
32
+ function toolNameFor(verb) {
33
+ return verb.replace(/-/g, "_");
34
+ }
35
+ function toolDefFromCommand(cmd) {
36
+ const properties = { session_id: SESSION_ID_PROPERTY };
37
+ const required = [];
38
+ for (const param of cmd.params) {
39
+ properties[param.name] = paramToProperty(param);
40
+ if (param.required) required.push(param.name);
41
+ }
42
+ if (cmd.verb === "screenshot") {
43
+ properties.inline = {
44
+ type: "boolean",
45
+ description: "Return the image itself so you can see it, not just the file path. Set this to true whenever you need to judge how the screen LOOKS, or whenever you cannot open files on the machine running this server \u2014 otherwise the screenshot is only written to disk and you never see it. It costs a lot of context, so skip it when you only need to confirm an action happened (the response already tells you what changed)."
46
+ };
47
+ }
48
+ return {
49
+ name: toolNameFor(cmd.verb),
50
+ description: cmd.summary,
51
+ mutating: cmd.mutating,
52
+ properties,
53
+ required
54
+ };
55
+ }
56
+ var DEV_START_DEF = {
57
+ name: "dev_start",
58
+ description: "Start a VibeView dev-loop session (device + tunnel) held in-process by this MCP server, and use it as the default session for every other tool call until dev_stop. Returns once the session accepts commands (device up, app installed) \u2014 call ui_tree right away, no readiness polling needed.",
59
+ mutating: true,
60
+ properties: {
61
+ session_id: SESSION_ID_PROPERTY,
62
+ platform: {
63
+ type: "string",
64
+ description: "Platform to run on.",
65
+ enum: [...DEV_PLATFORMS]
66
+ },
67
+ app: {
68
+ type: "string",
69
+ description: "Uploaded debug build (app_id) to run. Defaults to vibeview.json config."
70
+ },
71
+ metro_port: {
72
+ type: "number",
73
+ description: "Local Metro port (default: 8081, or vibeview.json)."
74
+ }
75
+ },
76
+ required: ["platform"]
77
+ };
78
+ var DEV_STOP_DEF = {
79
+ name: "dev_stop",
80
+ description: "Stop the dev-loop session started by dev_start.",
81
+ mutating: true,
82
+ properties: { session_id: SESSION_ID_PROPERTY },
83
+ required: []
84
+ };
85
+ var UPLOAD_APP_DEF = {
86
+ name: "upload_app",
87
+ description: "Upload an app build (zipped simulator .app for iOS, .apk for Android) and return its app_id for dev_start. The MCP equivalent of `vibeview upload-app`.",
88
+ mutating: true,
89
+ properties: {
90
+ file: {
91
+ type: "string",
92
+ description: "Path to the build file on this machine (.apk, or a zipped iOS simulator .app)."
93
+ },
94
+ name: {
95
+ type: "string",
96
+ description: "App name (auto-detected from the build if omitted)."
97
+ }
98
+ },
99
+ required: ["file"]
100
+ };
101
+ function buildToolDefinitions() {
102
+ return [...AGENT_COMMANDS.map(toolDefFromCommand), DEV_START_DEF, DEV_STOP_DEF, UPLOAD_APP_DEF];
103
+ }
104
+ function textResult(text, structuredContent, isError = false) {
105
+ return { content: [{ type: "text", text }], structuredContent, isError };
106
+ }
107
+ function errorResult(message) {
108
+ return { content: [{ type: "text", text: message }], isError: true };
109
+ }
110
+ function findCommand(toolName) {
111
+ return AGENT_COMMANDS.find((c) => toolNameFor(c.verb) === toolName);
112
+ }
113
+ async function handleToolCall(toolName, args, deps) {
114
+ if (toolName === "dev_start") {
115
+ return handleDevStart(args, deps);
116
+ }
117
+ if (toolName === "dev_stop") {
118
+ return handleDevStop(deps);
119
+ }
120
+ if (toolName === "upload_app") {
121
+ return handleUploadApp(args, deps);
122
+ }
123
+ const cmd = findCommand(toolName);
124
+ if (!cmd) {
125
+ return errorResult(`Unknown tool: ${toolName}`);
126
+ }
127
+ const sessionId = typeof args.session_id === "string" ? args.session_id : deps.resolveSessionId();
128
+ if (!sessionId) {
129
+ return errorResult(
130
+ "No session to target. Call dev_start first, or pass session_id (e.g. from a running `vibeview dev --detach`)."
131
+ );
132
+ }
133
+ let call;
134
+ try {
135
+ const { session_id: _sessionId, ...rest } = args;
136
+ const positionals = positionalsFor(cmd, rest);
137
+ const opts = optsFor(cmd, rest);
138
+ call = buildToolCall(cmd, positionals, opts);
139
+ } catch (err) {
140
+ return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
141
+ }
142
+ let envelope;
143
+ try {
144
+ envelope = await deps.agentTool(sessionId, call.tool, call.args);
145
+ } catch (err) {
146
+ return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
147
+ }
148
+ if (cmd.verb === "screenshot" && envelope.ok) {
149
+ return screenshotToolResult(envelope, args);
150
+ }
151
+ return textResult(renderEnvelope(envelope), envelope, !envelope.ok);
152
+ }
153
+ function screenshotToolResult(envelope, args) {
154
+ let imageBytes;
155
+ try {
156
+ imageBytes = decodeScreenshotResult(envelope.result);
157
+ } catch (err) {
158
+ return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
159
+ }
160
+ const outPath = typeof args.out === "string" && args.out ? args.out : join(tmpdir(), `vibeview-screenshot-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.png`);
161
+ try {
162
+ writeFileSync(outPath, imageBytes);
163
+ } catch (err) {
164
+ return errorResult(
165
+ `error: could not write screenshot to ${outPath}: ${err instanceof Error ? err.message : String(err)}`
166
+ );
167
+ }
168
+ const sanitized = {
169
+ ...envelope,
170
+ result: { written_to: outPath, bytes: imageBytes.length }
171
+ };
172
+ const content = [
173
+ { type: "text", text: `${renderEnvelope(sanitized)}
174
+ screenshot written to ${outPath}` }
175
+ ];
176
+ if (args.inline === true) {
177
+ content.push({ type: "image", data: imageBytes.toString("base64"), mimeType: "image/png" });
178
+ }
179
+ return { content, structuredContent: sanitized };
180
+ }
181
+ function positionalsFor(cmd, args) {
182
+ return cmd.params.filter((p) => p.cli === "positional").map((p) => args[p.name] === void 0 ? void 0 : String(args[p.name])).filter((v) => v !== void 0);
183
+ }
184
+ function optsFor(cmd, args) {
185
+ const opts = {};
186
+ for (const param of cmd.params) {
187
+ if (param.cli === "positional") continue;
188
+ if (args[param.name] === void 0) continue;
189
+ const key = param.cli.replace(/^--/, "").replace(/-([a-z])/g, (_, c) => c.toUpperCase());
190
+ const value = args[param.name];
191
+ opts[key] = cmd.verb === "wait" && key === "ms" && typeof value === "number" ? value * 1e3 : value;
192
+ }
193
+ return opts;
194
+ }
195
+ async function handleDevStart(args, deps) {
196
+ const platform = args.platform;
197
+ if (!isDevPlatform(platform)) {
198
+ return errorResult(
199
+ `dev_start: platform must be one of ${DEV_PLATFORMS.join(", ")}, got "${String(platform)}"`
200
+ );
201
+ }
202
+ try {
203
+ const result = await deps.devSession.start({
204
+ platform,
205
+ app: typeof args.app === "string" ? args.app : void 0,
206
+ metroPort: typeof args.metro_port === "number" ? args.metro_port : void 0
207
+ });
208
+ const payload = { session_id: result.sessionId, page_url: result.pageUrl };
209
+ return textResult(`dev session started: ${payload.session_id}
210
+ ${payload.page_url}`, payload);
211
+ } catch (err) {
212
+ return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
213
+ }
214
+ }
215
+ async function handleDevStop(deps) {
216
+ const current = deps.devSession.current();
217
+ try {
218
+ await deps.devSession.stop();
219
+ } catch (err) {
220
+ return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
221
+ }
222
+ return textResult(current ? `dev session stopped: ${current.sessionId}` : "dev session stopped", {
223
+ session_id: current?.sessionId ?? null
224
+ });
225
+ }
226
+ async function handleUploadApp(args, deps) {
227
+ const file = args.file;
228
+ if (typeof file !== "string" || !file) {
229
+ return errorResult("upload_app: `file` is required \u2014 the path to a .apk or zipped iOS .app");
230
+ }
231
+ const name = typeof args.name === "string" && args.name ? args.name : void 0;
232
+ const upload = deps.uploadApp ?? (async (f, n) => {
233
+ const { uploadAppBuild } = await import("./upload-app-GAZVQZYR.js");
234
+ const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
235
+ return result2;
236
+ });
237
+ let result;
238
+ try {
239
+ result = await upload(file, name);
240
+ } catch (err) {
241
+ return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
242
+ }
243
+ const isNew = result.is_new !== false;
244
+ const warnings = result.warnings ?? [];
245
+ const payload = {
246
+ app_id: result.public_id,
247
+ name: result.name,
248
+ package_name: result.package_name,
249
+ platform: result.platform,
250
+ is_new: isNew,
251
+ warnings
252
+ };
253
+ const lines = [
254
+ `${isNew ? "New app created" : "Build added to existing app"}: ${result.name}`,
255
+ `app_id: ${result.public_id} [${result.platform}] ${result.package_name}`,
256
+ ...warnings.map((w) => `warning: ${w}`)
257
+ ];
258
+ return textResult(lines.join("\n"), payload);
259
+ }
260
+ function createDevSessionController(auth, webUrl) {
261
+ let handle = null;
262
+ return {
263
+ async start(cfg) {
264
+ if (handle) {
265
+ throw new Error(
266
+ `a dev session is already running (${handle.sessionId}) \u2014 call dev_stop first`
267
+ );
268
+ }
269
+ let devConfig = null;
270
+ try {
271
+ devConfig = loadDevConfig(process.cwd());
272
+ } catch {
273
+ devConfig = null;
274
+ }
275
+ const entry = devConfig?.platforms?.[cfg.platform];
276
+ const appId = cfg.app || entry?.appId;
277
+ if (!appId) {
278
+ throw new Error(
279
+ `dev_start: no app to run for platform "${cfg.platform}" \u2014 pass the app arg (an app id from list-apps) or run \`vibeview dev\` once in this project to write vibeview.json`
280
+ );
281
+ }
282
+ const metroPort = cfg.metroPort ?? devConfig?.metroPort ?? 8081;
283
+ const metro = await checkMetro(metroPort);
284
+ if (metro === "down") {
285
+ throw new Error(
286
+ `Metro isn't running on localhost:${metroPort} \u2014 start it in the project first (yarn start), or pass metro_port if Metro uses another port`
287
+ );
288
+ }
289
+ const isTv = PLATFORM_DEVICE[cfg.platform].tv;
290
+ handle = await startDevSession({
291
+ token: auth.token,
292
+ baseUrl: auth.baseUrl,
293
+ webUrl,
294
+ platform: cfg.platform,
295
+ appId,
296
+ deviceCategory: isTv ? "tv" : entry?.deviceCategory ?? "phone",
297
+ metroPort,
298
+ open: false
299
+ });
300
+ return handle;
301
+ },
302
+ async stop() {
303
+ await handle?.stop();
304
+ handle = null;
305
+ },
306
+ current() {
307
+ return handle ? { sessionId: handle.sessionId, pageUrl: handle.pageUrl } : null;
308
+ }
309
+ };
310
+ }
311
+ function makeSessionResolver(devSession) {
312
+ return () => devSession.current()?.sessionId ?? readDevState(process.cwd())?.session_id;
313
+ }
314
+ async function createMcpServer(opts) {
315
+ const [{ McpServer }, { z }] = await Promise.all([
316
+ import("@modelcontextprotocol/sdk/server/mcp.js"),
317
+ import("zod")
318
+ ]);
319
+ const auth = resolveAuth(opts);
320
+ const client = new ApiClient(auth.baseUrl, auth.token);
321
+ const devSession = createDevSessionController(auth, opts.webUrl);
322
+ const deps = {
323
+ auth,
324
+ resolveSessionId: makeSessionResolver(devSession),
325
+ agentTool: client.agentTool.bind(client),
326
+ devSession
327
+ };
328
+ const server = new McpServer({ name: "vibeview", version: "1.0.0" });
329
+ function zodShape(def) {
330
+ const shape = {};
331
+ for (const [key, prop] of Object.entries(def.properties)) {
332
+ const base = prop.enum ? z.enum(prop.enum) : z[prop.type]();
333
+ const described = base.describe(prop.description);
334
+ shape[key] = def.required.includes(key) ? described : described.optional();
335
+ }
336
+ return shape;
337
+ }
338
+ for (const def of buildToolDefinitions()) {
339
+ server.registerTool(
340
+ def.name,
341
+ {
342
+ description: def.description,
343
+ inputSchema: zodShape(def),
344
+ annotations: { readOnlyHint: !def.mutating }
345
+ },
346
+ async (args) => {
347
+ const result = await handleToolCall(def.name, args ?? {}, deps);
348
+ return result;
349
+ }
350
+ );
351
+ }
352
+ return server;
353
+ }
354
+ async function runMcpServer(opts) {
355
+ chalk.level = 0;
356
+ const [server, { StdioServerTransport }] = await Promise.all([
357
+ createMcpServer(opts),
358
+ import("@modelcontextprotocol/sdk/server/stdio.js")
359
+ ]);
360
+ const transport = new StdioServerTransport();
361
+ await server.connect(transport);
362
+ }
363
+ export {
364
+ buildToolDefinitions,
365
+ handleToolCall,
366
+ runMcpServer
367
+ };
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ uploadApp,
4
+ uploadAppBuild
5
+ } from "./chunk-YD2X6TQV.js";
6
+ export {
7
+ uploadApp,
8
+ uploadAppBuild
9
+ };
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "vibeview",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "VibeView CLI -- run mobile tests from CI/CD pipelines",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "vibeview": "./dist/cli.js"
8
8
  },
9
9
  "files": [
10
- "dist/cli.js"
10
+ "dist",
11
+ "skills"
11
12
  ],
12
- "repository": {
13
- "type": "git",
14
- "url": "https://github.com/scriptx-com/vibeview.git",
15
- "directory": "cli"
13
+ "homepage": "https://vibeview.io/docs/cli",
14
+ "bugs": {
15
+ "email": "support@vibeview.io"
16
16
  },
17
17
  "publishConfig": {
18
18
  "access": "public"
@@ -26,11 +26,13 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@inquirer/prompts": "^7.0.0",
29
+ "@modelcontextprotocol/sdk": "^1.30.0",
29
30
  "chalk": "^5.0.0",
30
31
  "commander": "^14.0.0",
31
32
  "open": "^11.0.0",
32
33
  "ora": "^9.0.0",
33
- "ws": "^8.18.0"
34
+ "ws": "^8.18.0",
35
+ "zod": "^4.0.0"
34
36
  },
35
37
  "devDependencies": {
36
38
  "@types/node": "^20.0.0",
@@ -0,0 +1,210 @@
1
+ ---
2
+ name: vibeview-agent
3
+ description: Verify a React Native change on a real running app — install/launch it on a live iOS or Android device via VibeView, drive the UI, and check the result. Use when asked to verify this change on a device, test my React Native app, or run it on a simulator.
4
+ ---
5
+
6
+ # VibeView agent control
7
+
8
+ Drive a live iOS/Android VibeView session from the CLI to verify a React
9
+ Native change actually works, not just that it compiles. Requires the
10
+ `vibeview` CLI installed and authenticated (`vibeview login`, or
11
+ `VIBEVIEW_API_TOKEN` set).
12
+
13
+ ## 1. Build requirements
14
+
15
+ Read before uploading anything — a build that violates these will install
16
+ and then crash or refuse to boot:
17
+
18
+ - **Android**: the debug APK must include an `x86_64` slice. Standard
19
+ `./gradlew assembleDebug` output includes it by default; only an issue if
20
+ the project filters ABIs.
21
+ - **iOS**: upload a **simulator** debug build (not a device `.ipa`), and it
22
+ must include the `arm64` slice.
23
+
24
+ Debug builds are auto-detected on upload — no separate flag needed. Upload
25
+ warnings are the safety net: if a build is missing the right architecture,
26
+ `upload-app` says so.
27
+
28
+ Full detail: https://vibeview.io/docs/live-development and
29
+ https://vibeview.io/docs/preparing-your-build.
30
+
31
+ ## 2. The loop
32
+
33
+ 1. Upload the debug build — **only after a native change** (new/updated
34
+ native module, Podfile/Gradle change). Everyday JS/asset edits never need
35
+ a re-upload:
36
+ ```bash
37
+ vibeview upload-app ./path/to/app-debug.apk
38
+ ```
39
+ 2. Start Metro in the React Native project (`yarn start` or equivalent) and
40
+ leave it running.
41
+ 3. Start a dev session in the background and get structured output:
42
+ ```bash
43
+ vibeview dev --detach --json
44
+ ```
45
+ 4. Parse the `session_ready` line from stdout:
46
+ ```json
47
+ {"event":"session_ready","session_id":"...","url":"...","pid":1234}
48
+ ```
49
+ Use `session_id` as `--session <id>` on every verb below. The event
50
+ fires once the session accepts commands (device up, app installed) —
51
+ no need to poll `dev-status` first. If the app ships no native changes
52
+ since the last run, skip step 1 and just do steps 2–4.
53
+
54
+ Running `dev` writes two things into the project root: a `.vibeview/`
55
+ directory (live session state — local machine state, never commit) and a
56
+ `vibeview.json` (your platform/app/build choices, which teammates DO want).
57
+ Make sure `.vibeview/` is in the project's `.gitignore`, adding it if
58
+ missing. Leave `vibeview.json` tracked, but tell the developer you created
59
+ it rather than letting it appear unexplained in their `git status`.
60
+
61
+ ## 3. Verifying
62
+
63
+ - **Always run `ui-tree` first** — every ref (`@e5`) the UI exposes comes
64
+ from here. Never guess a ref.
65
+ ```bash
66
+ vibeview ui-tree --session <id>
67
+ ```
68
+ An empty or near-empty first tree usually means the app is still booting
69
+ (splash screen, fetching remote config) — NOT a broken session. Take a
70
+ screenshot to confirm, `wait`, and fetch the tree again before concluding
71
+ anything is wrong.
72
+ - Act on the screen using refs from the tree:
73
+ ```bash
74
+ vibeview tap @e5 --session <id>
75
+ vibeview type "hello@example.com" --session <id>
76
+ vibeview scroll down --session <id>
77
+ vibeview press back --session <id>
78
+ ```
79
+ - **Read the observation in every response before deciding the next
80
+ action.** Every mutating verb returns one of: screen unchanged, a
81
+ `+`/`~`/`-` diff with fresh `@e` refs, a full tree, or an "unavailable"
82
+ warning. Refs go stale the moment the screen changes — a stale ref is
83
+ refused. **The refusal does NOT include the current screen**: because the
84
+ action never ran, the observation comes back as "screen unchanged" with no
85
+ tree, so run `ui-tree` again to get live refs before retrying. Never reuse
86
+ a ref from a previous observation.
87
+ - **Take a screenshot whenever you changed how something LOOKS.** The UI
88
+ tree proves an element exists, has the right text, and occupies the right
89
+ rect — it says nothing about colour, contrast, or whether something is
90
+ hidden behind another view, so a screen can pass every check the tree can
91
+ express and still be visibly broken (text the same colour as its
92
+ background, a view drawn on top of another). Tree for structure and refs;
93
+ screenshot for appearance.
94
+
95
+ **Actually look at the image** — confirming the file was written proves
96
+ nothing about how it renders.
97
+ ```bash
98
+ vibeview screenshot --out ./check.png --session <id> # then open it
99
+ ```
100
+ Over MCP, `screenshot` only writes the file and returns its path unless
101
+ you pass `inline: true`, which returns the image itself. If you cannot
102
+ open files on the machine running the server, `inline: true` is the only
103
+ way you will ever see the screen. It costs a lot of context, so use it
104
+ when appearance matters — not to confirm a tap landed, which the
105
+ response's observation already tells you.
106
+ - **Off-screen elements**: use `scroll-to <text>` rather than repeating
107
+ `scroll down`. It scrolls until an element whose label, value, or
108
+ accessibility id contains that text is visible, and stops early when the
109
+ content stops changing (end of list). Omit `--direction` for vertical
110
+ lists (it auto-detects); pass it explicitly for horizontal strips.
111
+ ```bash
112
+ vibeview scroll-to "Delete account" --session <id>
113
+ ```
114
+ - **Precise two-point gestures** (sliders, drag-and-drop reorder, map
115
+ panning) need `drag` — `swipe`/`scroll` are direction-only and can't
116
+ express them. Each end is independently a ref or `x,y`:
117
+ ```bash
118
+ vibeview drag @e5 @e9 --session <id>
119
+ vibeview drag 100,400 300,400 --session <id>
120
+ vibeview drag @e5 300,400 --press-ms 500 --session <id>
121
+ ```
122
+ Use `--press-ms 500` or more for long-press-to-reorder lists, and
123
+ `--velocity 150` for slow, precise slider adjustments.
124
+ - **iOS system alerts** (permission prompts and similar) sit above the app
125
+ and block it. Call `alert get` first to read the message and the exact
126
+ button labels, then act on one:
127
+ ```bash
128
+ vibeview alert get --session <id>
129
+ vibeview alert accept --button "Allow" --session <id>
130
+ ```
131
+ - Use `open-url` to jump straight to a deep link instead of navigating by
132
+ hand:
133
+ ```bash
134
+ vibeview open-url "myapp://settings" --session <id>
135
+ ```
136
+ - **TV apps**: start the session with `--platform tvos` (Apple TV) or
137
+ `--platform androidtv` (Android TV). Navigate with the d-pad via `press`
138
+ (`dpad_up`/`dpad_down`/`dpad_left`/`dpad_right`/`dpad_center`), or move
139
+ focus directly with `tap-focused` (moves focus AND activates) / `focus`
140
+ (moves focus only, no activation).
141
+
142
+ ## 4. Cleanup
143
+
144
+ **Always** stop the session when done verifying — a running session bills
145
+ streaming minutes even when idle:
146
+ ```bash
147
+ vibeview dev-stop
148
+ ```
149
+
150
+ ## 5. Command reference
151
+
152
+ Every verb accepts `--session <id>` to target a specific session (falls
153
+ back to the current project's `vibeview dev --detach` session if omitted)
154
+ and `--json` to print the raw response envelope as one JSON line instead of
155
+ human-readable text.
156
+
157
+ | Verb | Args | Purpose |
158
+ |------|------|---------|
159
+ | `ui-tree` | — | Fetch the ref-annotated UI tree for the current screen. |
160
+ | `screenshot` | `[--out <path>]` | Capture a screenshot of the current screen and save it to disk. |
161
+ | `tap` | `<target>` | Tap an element by ref (e.g. `@e5`), or raw coordinates (e.g. `100,200`). |
162
+ | `long-press` | `<ref>` `[--ms <n>]` | Long-press (touch and hold) an element by its ref. |
163
+ | `swipe` | `<direction>` `[--distance <short\|medium\|long>]` | Perform a scrollbar-semantic swipe gesture in one of four directions. |
164
+ | `scroll` | `<direction>` | Scroll the current view up, down, left, or right using a gesture preset. |
165
+ | `scroll-to` | `<text>` `[--direction <up\|down\|left\|right>] [--max-scrolls <n>] [--element-type <type>]` | Scroll until an element matching the text is visible. Stops at end of list. |
166
+ | `drag` | `<from>` `<to>` `[--velocity <n>] [--press-ms <n>] [--hold-ms <n>]` | Drag between two points, each an `@ref` or `x,y`. The only precise two-point gesture. |
167
+ | `alert` | `<get\|accept\|dismiss>` `[--button <label>]` | Inspect or respond to a visible iOS system alert. |
168
+ | `type` | `<text>` | Type text into the currently focused input field. |
169
+ | `clear-text` | — | Clear the text in the currently focused input field. |
170
+ | `press` | `<button>` | Press a device button or perform a system gesture (home, back, d-pad, etc). |
171
+ | `open-url` | `<url>` | Open a deep link or URL in the app under test. |
172
+ | `wait` | `[--ms <n>]` | Wait for a specified number of seconds before continuing (default: 2s). |
173
+ | `find` | `<text>` `[--below <text>] [--above <text>] [--near <text>]` | Find an element by text with optional spatial constraints. |
174
+ | `tap-focused` | `<ref>` | TV: move focus to an element and press SELECT in one step. |
175
+ | `focus` | `<ref>` | TV: move focus to an element without activating it. |
176
+
177
+ Dev-loop lifecycle commands (not registry verbs, but needed for every run):
178
+
179
+ | Command | Purpose |
180
+ |---------|---------|
181
+ | `dev --detach --json` | Start a device + Metro-tunnel session in the background, emitting `session_ready`/`warning`/`error` JSON events on stdout. |
182
+ | `dev-status` | Check on the current project's detached dev session. |
183
+ | `dev-stop` | End the current project's detached dev session. |
184
+
185
+ ## 6. If you're calling VibeView over MCP
186
+
187
+ Everything above is also exposed as MCP tools by `vibeview mcp`, so an agent
188
+ with no shell can run the whole loop. Tool names are the CLI verbs with
189
+ hyphens replaced by underscores (`ui_tree`, `tap`, `scroll_to`, `long_press`,
190
+ `clear_text`, `open_url`, `tap_focused`, ...), each taking its args by name
191
+ (`tap {ref: "e5"}` or `tap {target: "100,200"}`, `scroll_to {text: "..."}`,
192
+ `drag {from: "@e5", to: "300,400"}`, `alert {action: "get"}`) plus an
193
+ optional `session_id`.
194
+
195
+ Three tools exist **only** over MCP, with no registry verb — they cover the
196
+ steps the CLI does with plain shell commands:
197
+
198
+ | MCP tool | Args | Purpose |
199
+ |----------|------|---------|
200
+ | `upload_app` | `file` (required path), `name` | Upload a debug build. The MCP equivalent of `vibeview upload-app`, so step 1 of the loop needs no shell. Returns the `app_id` to pass to `dev_start`. |
201
+ | `dev_start` | `platform` (required: `ios`/`android`/`tvos`/`androidtv`), `app`, `metro_port` | Start a dev-loop session held open by the MCP server. The MCP equivalent of `vibeview dev --detach`. |
202
+ | `dev_stop` | — | Stop the session `dev_start` started. The MCP equivalent of `vibeview dev-stop`. |
203
+
204
+ **`dev_start` is stateful — it changes the default session for every later
205
+ call.** Once it succeeds, any tool called without an explicit `session_id`
206
+ targets that session instead of whatever `vibeview dev --detach` state exists
207
+ in the project directory. Only one such session can be held at a time
208
+ (`dev_start` fails if one is already running), and it stays live — billing
209
+ streaming minutes — until `dev_stop` or the MCP connection ends. Always call
210
+ `dev_stop` when you finish verifying.