vibeview 0.37.1 → 0.38.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.md +6 -5
- package/dist/{chunk-NJ2IXB5H.js → chunk-AV674D7T.js} +3 -3
- package/dist/{chunk-QRJAFBH3.js → chunk-MYXUGCR5.js} +41 -5
- package/dist/cli.js +385 -183
- package/dist/{server-DF3CDWL4.js → server-FMU3HQWF.js} +193 -4
- package/dist/{upload-app-BPTGBDQB.js → upload-app-UHJLFQKZ.js} +1 -1
- package/package.json +2 -1
- package/skills/vibeview-agent/SKILL.md +6 -2
|
@@ -28,12 +28,12 @@ import {
|
|
|
28
28
|
renderEnvelope,
|
|
29
29
|
resolveMetro,
|
|
30
30
|
startDevSession
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-AV674D7T.js";
|
|
32
32
|
import {
|
|
33
33
|
ApiClient,
|
|
34
34
|
NotFoundError,
|
|
35
35
|
resolveAuth
|
|
36
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-MYXUGCR5.js";
|
|
37
37
|
|
|
38
38
|
// src/mcp/server.ts
|
|
39
39
|
import { writeFileSync } from "fs";
|
|
@@ -44,6 +44,72 @@ var SESSION_ID_PROPERTY = {
|
|
|
44
44
|
type: "string",
|
|
45
45
|
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."
|
|
46
46
|
};
|
|
47
|
+
var AI_USE_PROPERTY = {
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "smart (default): replays saved paths and uses AI only where the screen changed; no_ai: replays only, no usage credit; ai_only: the AI does every step.",
|
|
50
|
+
enum: ["smart", "no_ai", "ai_only"]
|
|
51
|
+
};
|
|
52
|
+
var AI_USE_MODE = { smart: "hybrid", no_ai: "replay", ai_only: "ai" };
|
|
53
|
+
var CREATE_TEST_DEF = {
|
|
54
|
+
name: "create_test",
|
|
55
|
+
description: 'Create a test for an app from plain-language steps, one per line; a check is a step starting with "Verify". Returns the test id for run_test. The first run is driven by AI; once it passes, later runs replay the saved path.',
|
|
56
|
+
mutating: true,
|
|
57
|
+
properties: {
|
|
58
|
+
app: { type: "string", description: "App id (from list_apps)." },
|
|
59
|
+
name: { type: "string", description: "Test name." },
|
|
60
|
+
steps: { type: "string", description: "The steps, one per line." },
|
|
61
|
+
group: { type: "string", description: "Group id to add the test to (optional)." }
|
|
62
|
+
},
|
|
63
|
+
required: ["app", "name", "steps"]
|
|
64
|
+
};
|
|
65
|
+
var RUN_TEST_DEF = {
|
|
66
|
+
name: "run_test",
|
|
67
|
+
description: "Run a saved test. With session_id it runs on that session (the device you are driving). Without it, VibeView always starts a new device of the test app's platform, runs the test and stops the device (it does not use a session you hold from dev_start; pass session_id to run on a session you hold). Returns a run id for get_run.",
|
|
68
|
+
mutating: true,
|
|
69
|
+
properties: {
|
|
70
|
+
test: { type: "string", description: "Test id (from create_test or the Tests page)." },
|
|
71
|
+
session_id: { type: "string", description: "Run on this session instead of a new device." },
|
|
72
|
+
device_model: {
|
|
73
|
+
type: "string",
|
|
74
|
+
description: "Device model for a new device (from list_device_models); omitted: any free one."
|
|
75
|
+
},
|
|
76
|
+
ai_use: AI_USE_PROPERTY,
|
|
77
|
+
start_from_current_screen: {
|
|
78
|
+
type: "boolean",
|
|
79
|
+
description: "With session_id: no reset before the test."
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
required: ["test"]
|
|
83
|
+
};
|
|
84
|
+
var RUN_GROUP_DEF = {
|
|
85
|
+
name: "run_group",
|
|
86
|
+
description: "Run every test of a group in order. With session_id it runs on that session; without it, on a new device of the app platform that VibeView starts and stops (it does not use a session you hold from dev_start; pass session_id to run on a session you hold). Returns a run id for get_run.",
|
|
87
|
+
mutating: true,
|
|
88
|
+
properties: {
|
|
89
|
+
group: {
|
|
90
|
+
type: "string",
|
|
91
|
+
description: "Group id (from the Tests page or `vibeview list-suites`)."
|
|
92
|
+
},
|
|
93
|
+
session_id: { type: "string", description: "Run on this session instead of a new device." },
|
|
94
|
+
device_model: {
|
|
95
|
+
type: "string",
|
|
96
|
+
description: "Device model for a new device; omitted: any free one."
|
|
97
|
+
},
|
|
98
|
+
ai_use: AI_USE_PROPERTY,
|
|
99
|
+
start_from_current_screen: {
|
|
100
|
+
type: "boolean",
|
|
101
|
+
description: "With session_id: no reset before the first test."
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
required: ["group"]
|
|
105
|
+
};
|
|
106
|
+
var GET_RUN_DEF = {
|
|
107
|
+
name: "get_run",
|
|
108
|
+
description: "A run's status and, when a test failed, which step and why. Pass the run id run_test or run_group returned.",
|
|
109
|
+
mutating: false,
|
|
110
|
+
properties: { run_id: { type: "string", description: "Run id." } },
|
|
111
|
+
required: ["run_id"]
|
|
112
|
+
};
|
|
47
113
|
var MCP_DEV_PLATFORMS = DEV_PLATFORMS;
|
|
48
114
|
function isMcpDevPlatform(p) {
|
|
49
115
|
return isDevPlatform(p) && MCP_DEV_PLATFORMS.includes(p);
|
|
@@ -210,9 +276,14 @@ function buildToolDefinitions() {
|
|
|
210
276
|
LIST_DEVICE_MODELS_DEF,
|
|
211
277
|
LIST_APPS_DEF,
|
|
212
278
|
LIST_BUILDS_DEF,
|
|
213
|
-
STOP_SESSION_DEF
|
|
279
|
+
STOP_SESSION_DEF,
|
|
280
|
+
CREATE_TEST_DEF,
|
|
281
|
+
RUN_TEST_DEF,
|
|
282
|
+
RUN_GROUP_DEF,
|
|
283
|
+
GET_RUN_DEF
|
|
214
284
|
];
|
|
215
285
|
}
|
|
286
|
+
var runClientOf = (deps) => deps.runClient ?? new ApiClient(deps.auth.baseUrl, deps.auth.token, { client: "mcp" });
|
|
216
287
|
function textResult(text, structuredContent, isError = false) {
|
|
217
288
|
return { content: [{ type: "text", text }], structuredContent, isError };
|
|
218
289
|
}
|
|
@@ -275,6 +346,118 @@ async function explainInactive(sessionId, deps) {
|
|
|
275
346
|
function findCommand(toolName) {
|
|
276
347
|
return AGENT_COMMANDS.find((c) => toolNameFor(c.verb) === toolName);
|
|
277
348
|
}
|
|
349
|
+
async function handleCreateTest(args, deps) {
|
|
350
|
+
const steps = String(args.steps ?? "").split("\n").map((s) => s.trim()).filter(Boolean).map((text) => ({ text }));
|
|
351
|
+
if (!steps.length) return errorResult("create_test: `steps` needs at least one line");
|
|
352
|
+
try {
|
|
353
|
+
const created = await runClientOf(deps).createTest({
|
|
354
|
+
name: String(args.name),
|
|
355
|
+
app_id: String(args.app),
|
|
356
|
+
steps,
|
|
357
|
+
group_ids: typeof args.group === "string" && args.group ? [args.group] : void 0
|
|
358
|
+
});
|
|
359
|
+
const verb = created.outcome === "unchanged" ? "Test already exists, unchanged" : created.outcome === "updated" ? "Updated existing test" : "Created test";
|
|
360
|
+
return textResult(
|
|
361
|
+
`${verb} ${created.public_id}: ${created.name} (${steps.length} steps). Run it with run_test test=${created.public_id}.`,
|
|
362
|
+
created
|
|
363
|
+
);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async function handleRun(kind, args, deps) {
|
|
369
|
+
const id = String(kind === "test" ? args.test : args.group);
|
|
370
|
+
const mode = typeof args.ai_use === "string" ? AI_USE_MODE[args.ai_use] : void 0;
|
|
371
|
+
const client = runClientOf(deps);
|
|
372
|
+
try {
|
|
373
|
+
if (typeof args.session_id === "string" && args.session_id) {
|
|
374
|
+
const r2 = await client.startRunOnSession(args.session_id, {
|
|
375
|
+
...kind === "test" ? { test_id: id } : { group_id: id },
|
|
376
|
+
start_from_current_screen: args.start_from_current_screen === true,
|
|
377
|
+
mode
|
|
378
|
+
});
|
|
379
|
+
return textResult(
|
|
380
|
+
`Started ${kind} run ${r2.run_public_id} on session ${args.session_id}. Check it with get_run run_id=${r2.run_public_id}.`,
|
|
381
|
+
r2
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
const devices = typeof args.device_model === "string" && args.device_model ? [{ model: args.device_model }] : void 0;
|
|
385
|
+
const r = await client.startRun(
|
|
386
|
+
kind === "test" ? { target: "test", test_ids: [id], devices, mode } : { target: "group", group_id: id, devices, mode }
|
|
387
|
+
);
|
|
388
|
+
const queued = r.lanes.some((l) => l.status === "queued") ? " It is waiting for a free device." : "";
|
|
389
|
+
return textResult(
|
|
390
|
+
`Started run ${r.batch_id}.${queued} Check it with get_run run_id=${r.batch_id}.`,
|
|
391
|
+
r
|
|
392
|
+
);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function notCheckedNote(status, n) {
|
|
398
|
+
if (status !== "passed" || typeof n !== "number" || n <= 0) return "";
|
|
399
|
+
return ` \u2014 ${n} step${n === 1 ? "" : "s"} not checked (no recorded actions; not verified)`;
|
|
400
|
+
}
|
|
401
|
+
var RUN_IN_PROGRESS = /* @__PURE__ */ new Set(["queued", "pending", "running"]);
|
|
402
|
+
function formatRunForAgent(run) {
|
|
403
|
+
const lines = [];
|
|
404
|
+
if (run.lanes) {
|
|
405
|
+
lines.push(
|
|
406
|
+
`${run.name}: ${run.status} \u2014 ${run.summary.tests_passed} of ${run.summary.tests_total} tests passed`
|
|
407
|
+
);
|
|
408
|
+
for (const lane of run.lanes) {
|
|
409
|
+
const detail = [lane.platform, lane.os_version].filter(Boolean).join(" ");
|
|
410
|
+
let head = `${lane.model ? `${lane.model} (${detail})` : detail}: ${lane.status}`;
|
|
411
|
+
if (lane.status === "queued" && typeof lane.queue?.position === "number") {
|
|
412
|
+
head += ` (position ${lane.queue.position} in the queue)`;
|
|
413
|
+
}
|
|
414
|
+
if (lane.error_message) head += ` \u2014 ${lane.error_message}`;
|
|
415
|
+
lines.push(head);
|
|
416
|
+
for (const r of lane.runs ?? []) {
|
|
417
|
+
const count = r.kind === "group" && typeof r.tests_total === "number" ? ` (${r.tests_passed ?? 0}/${r.tests_total} tests passed)` : "";
|
|
418
|
+
lines.push(
|
|
419
|
+
` ${r.name}: ${r.status}${count}${r.error_message ? ` \u2014 ${r.error_message}` : ""}`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
} else if (Array.isArray(run.test_runs)) {
|
|
424
|
+
lines.push(`${run.suite_name ?? "Group"}: ${run.status}`);
|
|
425
|
+
for (const t of run.test_runs) {
|
|
426
|
+
const q = t.quarantined && t.status === "failed" ? " (quarantined)" : "";
|
|
427
|
+
lines.push(
|
|
428
|
+
` ${t.case_name}: ${t.status}${q}${t.error_message ? ` \u2014 ${t.error_message}` : ""}${notCheckedNote(t.status, t.steps_not_checked)}`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
const name = run.case_name ?? run.suite_name ?? "Run";
|
|
433
|
+
const failed = (run.steps ?? []).find((s) => s.status === "failed");
|
|
434
|
+
const notChecked = typeof run.steps_not_checked === "number" ? run.steps_not_checked : (run.steps ?? []).filter((s) => s.status === "not_checked").length;
|
|
435
|
+
const q = run.quarantined && run.status === "failed" ? " (quarantined)" : "";
|
|
436
|
+
lines.push(
|
|
437
|
+
`${name}: ${run.status}${q}${run.error_message ? ` \u2014 ${run.error_message}` : ""}${notCheckedNote(run.status, notChecked)}`
|
|
438
|
+
);
|
|
439
|
+
if (failed) {
|
|
440
|
+
lines.push(
|
|
441
|
+
`Failed step: ${failed.step ?? failed.step_text ?? failed.text ?? ""} \u2014 ${failed.error ?? ""}`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (RUN_IN_PROGRESS.has(run.status))
|
|
446
|
+
lines.push("Still in progress: call get_run again to see the result.");
|
|
447
|
+
return lines.join("\n");
|
|
448
|
+
}
|
|
449
|
+
async function handleGetRun(args, deps) {
|
|
450
|
+
const id = String(args.run_id ?? "");
|
|
451
|
+
if (!id) return errorResult("get_run: `run_id` is required");
|
|
452
|
+
const client = runClientOf(deps);
|
|
453
|
+
try {
|
|
454
|
+
const kind = await client.resolveRun(id);
|
|
455
|
+
const run = kind.kind === "batch" ? await client.getRunBatch(id) : await client.getRunStatus(id);
|
|
456
|
+
return textResult(formatRunForAgent(run), run);
|
|
457
|
+
} catch (err) {
|
|
458
|
+
return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
278
461
|
async function dispatchToolCall(toolName, args, deps, extra = {}) {
|
|
279
462
|
if (toolName === "dev_start") {
|
|
280
463
|
return handleDevStart(args, deps, extra);
|
|
@@ -300,6 +483,10 @@ async function dispatchToolCall(toolName, args, deps, extra = {}) {
|
|
|
300
483
|
if (toolName === "stop_session") {
|
|
301
484
|
return handleStopSession(args, deps);
|
|
302
485
|
}
|
|
486
|
+
if (toolName === "create_test") return handleCreateTest(args, deps);
|
|
487
|
+
if (toolName === "run_test") return handleRun("test", args, deps);
|
|
488
|
+
if (toolName === "run_group") return handleRun("group", args, deps);
|
|
489
|
+
if (toolName === "get_run") return handleGetRun(args, deps);
|
|
303
490
|
const cmd = findCommand(toolName);
|
|
304
491
|
if (!cmd) {
|
|
305
492
|
return errorResult(`Unknown tool: ${toolName}`);
|
|
@@ -687,7 +874,7 @@ async function handleUploadApp(args, deps) {
|
|
|
687
874
|
}
|
|
688
875
|
const name = typeof args.name === "string" && args.name ? args.name : void 0;
|
|
689
876
|
const upload = deps.uploadApp ?? (async (f, n) => {
|
|
690
|
-
const { uploadAppBuild } = await import("./upload-app-
|
|
877
|
+
const { uploadAppBuild } = await import("./upload-app-UHJLFQKZ.js");
|
|
691
878
|
const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
|
|
692
879
|
return result2;
|
|
693
880
|
});
|
|
@@ -1079,6 +1266,7 @@ var MCP_INSTRUCTIONS = [
|
|
|
1079
1266
|
"Foldables: pick one by model in dev_start (list_device_models marks them foldable, with free/busy counts), then set_posture and rotate.",
|
|
1080
1267
|
"set_location sets the simulated GPS position (phones and tablets) for location-based flows.",
|
|
1081
1268
|
"screenshot writes a PNG and returns its path; pass inline: true to see the image. logs reads the app output after a crash or blank screen.",
|
|
1269
|
+
"Tests: create_test, then run_test (or run_group) and get_run. With session_id a run uses the session you hold; without it, a run starts its own new device (never your dev_start session).",
|
|
1082
1270
|
"A session stopped elsewhere is reported as ended; call dev_start again for a new one.",
|
|
1083
1271
|
'A session nobody uses ends after the idle timeout. A result that ends with "Session \u2026 ends in N s for inactivity" means act (tap, type, swipe, press) to keep it, or dev_stop if you are done; ui_tree, screenshot, logs, find and wait do not count as use.'
|
|
1084
1272
|
].join("\n");
|
|
@@ -1196,6 +1384,7 @@ export {
|
|
|
1196
1384
|
createShutdown,
|
|
1197
1385
|
deviceLine,
|
|
1198
1386
|
forMcp,
|
|
1387
|
+
formatRunForAgent,
|
|
1199
1388
|
handleToolCall,
|
|
1200
1389
|
idleNoticeText,
|
|
1201
1390
|
mcpText,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibeview",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "VibeView CLI -- run your app on cloud iOS simulators, Android emulators, Apple TV, Android TV and Roku devices: a React Native dev loop, device control for you or your coding agent (CLI or MCP), and tests from CI",
|
|
5
5
|
"mcpName": "io.github.vibeview/vibeview",
|
|
6
6
|
"type": "module",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
31
31
|
"chalk": "^5.0.0",
|
|
32
32
|
"commander": "^14.0.0",
|
|
33
|
+
"fflate": "~0.8.2",
|
|
33
34
|
"ignore": "^7.0.0",
|
|
34
35
|
"open": "^11.0.0",
|
|
35
36
|
"ora": "^9.0.0",
|
|
@@ -216,7 +216,7 @@ appear unexplained in their `git status`.
|
|
|
216
216
|
`roku` over MCP); after an edit run `vibeview dev-reload` (MCP:
|
|
217
217
|
`dev_reload`) to re-package, upload and restart the channel in the same
|
|
218
218
|
session — about five seconds. The remote's names are `press up/down/left/right/select/back`
|
|
219
|
-
(`dpad_*` also works)
|
|
219
|
+
(`dpad_*` also works), plus `enter`, `backspace` and the media keys `play/rewind/forward/replay/info`; `home` is refused (the session stays in your channel); `relaunch-app` restarts the channel with its launch parameters; `clear-text` is unavailable. `type` lands in a focused
|
|
220
220
|
on-screen keyboard; the typed text shows up in the tree as the text box's
|
|
221
221
|
child label, which is what the response verifies against. `logs` returns
|
|
222
222
|
the channel's console output (its `print` lines, why it exited, and the
|
|
@@ -287,7 +287,7 @@ human-readable text.
|
|
|
287
287
|
| `clear-text` | — | Clear the text in the currently focused input field. |
|
|
288
288
|
| `press` | `<button>` | Press a device button or perform a system gesture (home, back, d-pad, etc). |
|
|
289
289
|
| `open-url` | `<url>` | Open a deep link in the app under test, never a browser. Android: a link the app has no screen for fails. iOS/Apple TV: only the app's own URL scheme (`myapp://...`); a web or system link is refused. |
|
|
290
|
-
| `relaunch-app` | — | Close the app and start it again (cold start, data kept) — for "does it persist after a restart" checks. The app log keeps streaming. Not on
|
|
290
|
+
| `relaunch-app` | — | Close the app and start it again (cold start, data kept) — for "does it persist after a restart" checks. The app log keeps streaming. Not on embed sessions. |
|
|
291
291
|
| `set-posture` | `<closed\|partial\|open>` or `--angle <0-180>` | Fold or unfold a foldable device (form factor `foldable`: the iPhone Duo, Android foldables) to a preset or an exact hinge angle — exactly one. Reports posture, angle and lit screen. Errors on a device without a hinge. |
|
|
292
292
|
| `rotate` | `[--degrees <90\|180\|270>]` | Rotate the device and report the new orientation. Phones/tablets and Android foldables toggle portrait/landscape (90 only); the iPhone Duo turns a quarter clockwise by default. Not on TV. |
|
|
293
293
|
| `set-location` | `--lat <n> --lon <n>` | Set the device's simulated GPS position (decimal degrees; west and south are negative) for location-based flows. Phones, tablets and foldables; not on TV. |
|
|
@@ -331,6 +331,10 @@ steps the CLI does with plain shell commands:
|
|
|
331
331
|
| `list_builds` | `app` (required) | List an app's uploaded builds with their kind (`debug`/`release`/`unknown`). The MCP equivalent of `vibeview list-builds`. |
|
|
332
332
|
| `dev_stop` | — | Stop the session `dev_start` started. The MCP equivalent of `vibeview dev-stop`. Says "no dev session was running" when there was none. |
|
|
333
333
|
| `stop_session` | `session_id` (required) | Stop any session by id. The MCP equivalent of `vibeview stop <session-id>`. Says so when the session had already ended. |
|
|
334
|
+
| `create_test` | `app`, `name`, `steps` (required), `group` | Create a test from plain-language steps, one per line (a check is a step starting with "Verify"). Returns the test id for `run_test`. |
|
|
335
|
+
| `run_test` | `test` (required), `session_id`, `device_model`, `ai_use`, `start_from_current_screen` | Run a saved test. With `session_id` it runs on a session you hold; without it, VibeView always starts a new device, runs the test and stops it (it never uses your `dev_start` session). `ai_use`: `smart` (default), `no_ai` (replay only), `ai_only`. Returns a run id for `get_run`. |
|
|
336
|
+
| `run_group` | `group` (required), `session_id`, `device_model`, `ai_use`, `start_from_current_screen` | Run every test of a group in order, on `session_id` or, without it, on a new device VibeView starts (never your `dev_start` session). Returns a run id for `get_run`. |
|
|
337
|
+
| `get_run` | `run_id` (required) | A run's status and, when a test failed, which step and why. |
|
|
334
338
|
| `dev_reload` | — | Roku only: re-package, upload and restart the channel in the held session (or this project's `vibeview dev --detach` session). The MCP equivalent of `vibeview dev-reload`. |
|
|
335
339
|
|
|
336
340
|
If the held session ends elsewhere (dashboard, idle timeout, failure), the
|