vibeview 0.32.0 → 0.34.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/dist/{chunk-ZZK7YQSP.js → chunk-4D2E2PE4.js} +13 -3
- package/dist/{chunk-T6H5XTBX.js → chunk-TPZG7NCU.js} +352 -21
- package/dist/cli.js +85 -20
- package/dist/{server-FJSBWEHZ.js → server-RDGEVJD4.js} +314 -76
- package/dist/{upload-app-2LVG4LMB.js → upload-app-AKD73742.js} +1 -1
- package/package.json +1 -1
- package/skills/vibeview-agent/SKILL.md +34 -12
|
@@ -4,13 +4,14 @@ import {
|
|
|
4
4
|
DEV_PLATFORMS,
|
|
5
5
|
PLATFORM_DEVICE,
|
|
6
6
|
PLATFORM_NEEDS_METRO,
|
|
7
|
-
|
|
7
|
+
buildLines,
|
|
8
8
|
buildRows,
|
|
9
9
|
buildToolCall,
|
|
10
10
|
checkDevBuild,
|
|
11
11
|
checkMetro,
|
|
12
12
|
createRokuPush,
|
|
13
13
|
decodeScreenshotResult,
|
|
14
|
+
describeDevBuild,
|
|
14
15
|
effectiveDevConfig,
|
|
15
16
|
filterDeviceModels,
|
|
16
17
|
formatBuildRowsHuman,
|
|
@@ -24,12 +25,12 @@ import {
|
|
|
24
25
|
renderEnvelope,
|
|
25
26
|
resolveMetro,
|
|
26
27
|
startDevSession
|
|
27
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-TPZG7NCU.js";
|
|
28
29
|
import {
|
|
29
30
|
ApiClient,
|
|
30
31
|
NotFoundError,
|
|
31
32
|
resolveAuth
|
|
32
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-4D2E2PE4.js";
|
|
33
34
|
|
|
34
35
|
// src/mcp/server.ts
|
|
35
36
|
import { writeFileSync } from "fs";
|
|
@@ -45,14 +46,25 @@ function isMcpDevPlatform(p) {
|
|
|
45
46
|
return isDevPlatform(p) && MCP_DEV_PLATFORMS.includes(p);
|
|
46
47
|
}
|
|
47
48
|
var MCP_PARAM_DESCRIPTIONS = {
|
|
48
|
-
wait: { seconds: "How long to wait, in seconds. Default: 2." }
|
|
49
|
+
wait: { seconds: "How long to wait, in seconds. Default: 2." },
|
|
50
|
+
screenshot: {
|
|
51
|
+
out: "File path to write the PNG to on the machine running this server (default: a temp file)."
|
|
52
|
+
}
|
|
49
53
|
};
|
|
50
54
|
function paramToProperty(cmd, param) {
|
|
51
55
|
return {
|
|
52
56
|
type: param.type,
|
|
53
|
-
description: MCP_PARAM_DESCRIPTIONS[cmd.verb]?.[param.name] ?? param.description
|
|
57
|
+
description: MCP_PARAM_DESCRIPTIONS[cmd.verb]?.[param.name] ?? mcpText(cmd, param.description)
|
|
54
58
|
};
|
|
55
59
|
}
|
|
60
|
+
function mcpText(cmd, text) {
|
|
61
|
+
let out = text.replace(/`?(?:vibeview )?list-devices --models`?/g, "list_device_models").replace(/`vibeview ([a-z-]+)`/g, (_, verb) => toolNameFor(verb));
|
|
62
|
+
for (const param of cmd.params) {
|
|
63
|
+
if (param.cli === "positional") continue;
|
|
64
|
+
out = out.replace(new RegExp(`${param.cli}(?![a-z-])`, "g"), param.name);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
56
68
|
function toolNameFor(verb) {
|
|
57
69
|
return verb.replace(/-/g, "_");
|
|
58
70
|
}
|
|
@@ -66,12 +78,12 @@ function toolDefFromCommand(cmd) {
|
|
|
66
78
|
if (cmd.verb === "screenshot") {
|
|
67
79
|
properties.inline = {
|
|
68
80
|
type: "boolean",
|
|
69
|
-
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
|
|
81
|
+
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 context, so skip it when you only need to confirm an action happened (the response already tells you what changed). The image has the same size as the screenshot (a JPEG when the PNG is large), so a point read off it is the coordinate tap and drag take."
|
|
70
82
|
};
|
|
71
83
|
}
|
|
72
84
|
return {
|
|
73
85
|
name: toolNameFor(cmd.verb),
|
|
74
|
-
description: cmd.summary,
|
|
86
|
+
description: mcpText(cmd, cmd.summary),
|
|
75
87
|
mutating: cmd.mutating,
|
|
76
88
|
properties,
|
|
77
89
|
required
|
|
@@ -102,7 +114,7 @@ var DEV_START_DEF = {
|
|
|
102
114
|
},
|
|
103
115
|
build_id: {
|
|
104
116
|
type: "number",
|
|
105
|
-
description:
|
|
117
|
+
description: `Run this uploaded build of the app (an id from list_builds). Without standalone it must be a debug build (kind "debug"); with standalone any build of the app works. Omitted: the newest debug build (preferring one of the local project's own app version over a newer one of another version), or with standalone the newest release build. Not for roku.`
|
|
106
118
|
},
|
|
107
119
|
standalone: {
|
|
108
120
|
type: "boolean",
|
|
@@ -205,8 +217,15 @@ function errorResult(message) {
|
|
|
205
217
|
return { content: [{ type: "text", text: forMcp(message) }], isError: true };
|
|
206
218
|
}
|
|
207
219
|
function forMcp(message) {
|
|
208
|
-
let text = message.replace(/`vibeview list-devices --models`/g, "list_device_models").replace(/`vibeview list-builds <app>`/g, "list_builds").replace(/`vibeview list-builds ([^`\s]+)`/g, 'list_builds
|
|
220
|
+
let text = message.replace(/`vibeview list-devices --models`/g, "list_device_models").replace(/`vibeview list-builds <app>`/g, "list_builds").replace(/`vibeview list-builds ([^`\s]+)`/g, 'list_builds with app "$1"').replace(
|
|
221
|
+
/use `vibeview dev --standalone --build (\d+)`/g,
|
|
222
|
+
"call dev_start with standalone: true and build_id: $1"
|
|
223
|
+
).replace(
|
|
224
|
+
/Set VIBEVIEW_API_TOKEN \(or --token\) to a full-access token for this command\./g,
|
|
225
|
+
"Start this MCP server with a full-access token (VIBEVIEW_API_TOKEN or --token)."
|
|
226
|
+
).replace(/`vibeview list-apps`/g, "list_apps").replace(/\blist-apps\b/g, "list_apps").replace(/`vibeview upload-app`/g, "upload_app").replace(/`vibeview dev` needs/g, "dev_start needs");
|
|
209
227
|
if (/No debug build|is not a debug build/.test(text) && !/standalone: true/.test(text)) {
|
|
228
|
+
text = `${text.replace(/\s+$/, "")}${/[.!?]$/.test(text.trim()) ? "" : "."}`;
|
|
210
229
|
text += " To run a release build as it is, with no Metro, call dev_start with standalone: true.";
|
|
211
230
|
}
|
|
212
231
|
return text;
|
|
@@ -218,6 +237,9 @@ async function readSession(deps, sessionId) {
|
|
|
218
237
|
function isEndedStatus(status) {
|
|
219
238
|
return status === "stopped" || status === "failed";
|
|
220
239
|
}
|
|
240
|
+
function capitalize(text) {
|
|
241
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
242
|
+
}
|
|
221
243
|
function endedLine(ended) {
|
|
222
244
|
const why = ended.reason ? `: ${ended.reason}` : "";
|
|
223
245
|
return `session ${ended.sessionId} has ended (${ended.status}${why})`;
|
|
@@ -244,9 +266,9 @@ async function explainInactive(sessionId, deps) {
|
|
|
244
266
|
function findCommand(toolName) {
|
|
245
267
|
return AGENT_COMMANDS.find((c) => toolNameFor(c.verb) === toolName);
|
|
246
268
|
}
|
|
247
|
-
async function handleToolCall(toolName, args, deps) {
|
|
269
|
+
async function handleToolCall(toolName, args, deps, extra = {}) {
|
|
248
270
|
if (toolName === "dev_start") {
|
|
249
|
-
return handleDevStart(args, deps);
|
|
271
|
+
return handleDevStart(args, deps, extra);
|
|
250
272
|
}
|
|
251
273
|
if (toolName === "dev_stop") {
|
|
252
274
|
return handleDevStop(deps);
|
|
@@ -287,6 +309,9 @@ async function handleToolCall(toolName, args, deps) {
|
|
|
287
309
|
const positionals = positionalsFor(cmd, rest);
|
|
288
310
|
const opts = optsFor(cmd, rest);
|
|
289
311
|
call = buildToolCall(cmd, positionals, opts);
|
|
312
|
+
if (cmd.verb === "screenshot" && args.inline === true) {
|
|
313
|
+
call = { ...call, args: { ...call.args, preview: true } };
|
|
314
|
+
}
|
|
290
315
|
} catch (err) {
|
|
291
316
|
return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
292
317
|
}
|
|
@@ -304,12 +329,13 @@ async function handleToolCall(toolName, args, deps) {
|
|
|
304
329
|
if (cmd.verb === "screenshot" && envelope.ok) {
|
|
305
330
|
return screenshotToolResult(envelope, args);
|
|
306
331
|
}
|
|
307
|
-
return textResult(renderEnvelope(envelope), envelope, !envelope.ok);
|
|
332
|
+
return textResult(renderEnvelope(envelope, toolNameFor(cmd.verb)), envelope, !envelope.ok);
|
|
308
333
|
}
|
|
309
334
|
function screenshotToolResult(envelope, args) {
|
|
335
|
+
const { image, preview } = splitScreenshotResult(envelope.result);
|
|
310
336
|
let imageBytes;
|
|
311
337
|
try {
|
|
312
|
-
imageBytes = decodeScreenshotResult(
|
|
338
|
+
imageBytes = decodeScreenshotResult(image);
|
|
313
339
|
} catch (err) {
|
|
314
340
|
return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
315
341
|
}
|
|
@@ -325,15 +351,54 @@ function screenshotToolResult(envelope, args) {
|
|
|
325
351
|
...envelope,
|
|
326
352
|
result: { written_to: outPath, bytes: imageBytes.length }
|
|
327
353
|
};
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
];
|
|
354
|
+
const lines = [`${renderEnvelope(sanitized, "screenshot")}
|
|
355
|
+
screenshot written to ${outPath}`];
|
|
356
|
+
let inlineBlock = null;
|
|
332
357
|
if (args.inline === true) {
|
|
333
|
-
|
|
358
|
+
if (preview) {
|
|
359
|
+
inlineBlock = { type: "image", data: preview.data, mimeType: preview.mimeType };
|
|
360
|
+
lines.push(
|
|
361
|
+
`inline image: ${preview.mimeType === "image/jpeg" ? "JPEG" : "PNG"} ${preview.width}x${preview.height}, ${kb(preview.bytes)} \u2014 the same size as the saved PNG, so a point read off it is the coordinate tap and drag take`
|
|
362
|
+
);
|
|
363
|
+
} else if (imageBytes.length <= INLINE_PNG_MAX_BYTES) {
|
|
364
|
+
inlineBlock = { type: "image", data: imageBytes.toString("base64"), mimeType: "image/png" };
|
|
365
|
+
} else {
|
|
366
|
+
lines.push(
|
|
367
|
+
`inline image left out: the PNG is ${kb(imageBytes.length)}, too large to send inline \u2014 open the file instead`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
334
370
|
}
|
|
371
|
+
const content = [{ type: "text", text: lines.join("\n") }];
|
|
372
|
+
if (inlineBlock) content.push(inlineBlock);
|
|
335
373
|
return { content, structuredContent: sanitized };
|
|
336
374
|
}
|
|
375
|
+
var INLINE_PNG_MAX_BYTES = 15e5;
|
|
376
|
+
function kb(bytes) {
|
|
377
|
+
return bytes >= 1e6 ? `${(bytes / 1e6).toFixed(1)} MB` : `${Math.round(bytes / 1e3)} KB`;
|
|
378
|
+
}
|
|
379
|
+
function splitScreenshotResult(result) {
|
|
380
|
+
if (!result || typeof result !== "object" || !("image" in result)) {
|
|
381
|
+
return { image: result, preview: null };
|
|
382
|
+
}
|
|
383
|
+
const r = result;
|
|
384
|
+
const p = r.preview;
|
|
385
|
+
const url = typeof p?.data_url === "string" ? p.data_url : "";
|
|
386
|
+
const comma = url.indexOf(",");
|
|
387
|
+
if (!p || comma < 0 || typeof p.mime_type !== "string" || typeof p.width !== "number" || typeof p.height !== "number") {
|
|
388
|
+
return { image: r.image, preview: null };
|
|
389
|
+
}
|
|
390
|
+
const data = url.slice(comma + 1);
|
|
391
|
+
return {
|
|
392
|
+
image: r.image,
|
|
393
|
+
preview: {
|
|
394
|
+
data,
|
|
395
|
+
mimeType: p.mime_type,
|
|
396
|
+
width: p.width,
|
|
397
|
+
height: p.height,
|
|
398
|
+
bytes: typeof p.bytes === "number" ? p.bytes : Math.floor(data.length * 3 / 4)
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
337
402
|
function positionalsFor(cmd, args) {
|
|
338
403
|
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);
|
|
339
404
|
}
|
|
@@ -348,7 +413,7 @@ function optsFor(cmd, args) {
|
|
|
348
413
|
}
|
|
349
414
|
return opts;
|
|
350
415
|
}
|
|
351
|
-
async function handleDevStart(args, deps) {
|
|
416
|
+
async function handleDevStart(args, deps, extra = {}) {
|
|
352
417
|
const platform = args.platform;
|
|
353
418
|
if (!isMcpDevPlatform(platform)) {
|
|
354
419
|
return errorResult(
|
|
@@ -373,7 +438,8 @@ async function handleDevStart(args, deps) {
|
|
|
373
438
|
metroPort: typeof args.metro_port === "number" ? args.metro_port : void 0,
|
|
374
439
|
model: typeof args.model === "string" && args.model.trim() ? args.model : void 0,
|
|
375
440
|
...buildId !== void 0 ? { buildId } : {},
|
|
376
|
-
...standalone ? { standalone } : {}
|
|
441
|
+
...standalone ? { standalone } : {},
|
|
442
|
+
...extra.progress ? { onProgress: extra.progress } : {}
|
|
377
443
|
});
|
|
378
444
|
const warnings = result.warnings ?? [];
|
|
379
445
|
const payload = {
|
|
@@ -381,17 +447,38 @@ async function handleDevStart(args, deps) {
|
|
|
381
447
|
page_url: result.pageUrl,
|
|
382
448
|
standalone,
|
|
383
449
|
device: result.device ?? null,
|
|
450
|
+
build: result.build ?? null,
|
|
451
|
+
build_reason: result.buildReason ?? null,
|
|
452
|
+
other_debug_builds: result.otherBuilds ?? [],
|
|
453
|
+
project: result.project ?? null,
|
|
454
|
+
queued_seconds: result.queuedSeconds ?? null,
|
|
384
455
|
warnings
|
|
385
456
|
};
|
|
386
457
|
const lines = [`${standalone ? "session" : "dev session"} started: ${payload.session_id}`];
|
|
387
458
|
const device = result.device ? deviceLine(result.device) : null;
|
|
388
459
|
if (device) lines.push(device);
|
|
460
|
+
lines.push(...buildLines2(result, standalone));
|
|
461
|
+
if (typeof result.queuedSeconds === "number") {
|
|
462
|
+
lines.push(`waited ${Math.round(result.queuedSeconds)} s in the queue for a device`);
|
|
463
|
+
}
|
|
389
464
|
lines.push(payload.page_url, ...warnings.map((w) => `warning: ${w}`));
|
|
390
465
|
return textResult(lines.join("\n"), payload);
|
|
391
466
|
} catch (err) {
|
|
392
467
|
return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
393
468
|
}
|
|
394
469
|
}
|
|
470
|
+
function buildLines2(result, standalone) {
|
|
471
|
+
return buildLines(
|
|
472
|
+
{
|
|
473
|
+
build: result.build ?? null,
|
|
474
|
+
reason: result.buildReason ?? null,
|
|
475
|
+
pinned: result.pinned === true,
|
|
476
|
+
other_debug_builds: result.otherBuilds ?? [],
|
|
477
|
+
project: result.project ?? null
|
|
478
|
+
},
|
|
479
|
+
{ standalone, flag: "build_id", listBuilds: "list_builds" }
|
|
480
|
+
);
|
|
481
|
+
}
|
|
395
482
|
function deviceLine(d) {
|
|
396
483
|
const name = d.model || d.name;
|
|
397
484
|
if (!name) return null;
|
|
@@ -526,7 +613,7 @@ async function handleDevStop(deps) {
|
|
|
526
613
|
}
|
|
527
614
|
if (!current) {
|
|
528
615
|
return textResult(
|
|
529
|
-
ended ? `no dev session was running \u2014 ${endedLine(ended)}
|
|
616
|
+
ended ? `no dev session was running \u2014 it had already ended. ${capitalize(endedLine(ended))}` : "no dev session was running",
|
|
530
617
|
{ session_id: null, stopped: false }
|
|
531
618
|
);
|
|
532
619
|
}
|
|
@@ -542,7 +629,7 @@ async function handleUploadApp(args, deps) {
|
|
|
542
629
|
}
|
|
543
630
|
const name = typeof args.name === "string" && args.name ? args.name : void 0;
|
|
544
631
|
const upload = deps.uploadApp ?? (async (f, n) => {
|
|
545
|
-
const { uploadAppBuild } = await import("./upload-app-
|
|
632
|
+
const { uploadAppBuild } = await import("./upload-app-AKD73742.js");
|
|
546
633
|
const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
|
|
547
634
|
return result2;
|
|
548
635
|
});
|
|
@@ -575,16 +662,69 @@ async function handleUploadApp(args, deps) {
|
|
|
575
662
|
];
|
|
576
663
|
return textResult(lines.join("\n"), payload);
|
|
577
664
|
}
|
|
665
|
+
var START_PROGRESS_INTERVAL_MS = 1e4;
|
|
666
|
+
function startProgress(report) {
|
|
667
|
+
const started = Date.now();
|
|
668
|
+
let queuedAt = null;
|
|
669
|
+
let dequeuedAt = null;
|
|
670
|
+
let phase = "starting the device";
|
|
671
|
+
let timer = null;
|
|
672
|
+
const say = (message) => {
|
|
673
|
+
try {
|
|
674
|
+
report?.(message);
|
|
675
|
+
} catch {
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
if (report) {
|
|
679
|
+
timer = setInterval(() => {
|
|
680
|
+
const since = Math.round(
|
|
681
|
+
(Date.now() - (queuedAt !== null && dequeuedAt === null ? queuedAt : started)) / 1e3
|
|
682
|
+
);
|
|
683
|
+
say(`${phase} (${since} s so far)`);
|
|
684
|
+
}, START_PROGRESS_INTERVAL_MS);
|
|
685
|
+
timer.unref?.();
|
|
686
|
+
}
|
|
687
|
+
return {
|
|
688
|
+
event(evt) {
|
|
689
|
+
if (evt.event === "queued") {
|
|
690
|
+
queuedAt = Date.now();
|
|
691
|
+
const where = evt.position ? ` at position ${evt.position}` : "";
|
|
692
|
+
phase = `waiting in the queue for a device${where}`;
|
|
693
|
+
say(
|
|
694
|
+
`${evt.message ?? "Every device of this kind is busy."} Waiting in the queue${where}; dev_start returns once a device is free.`
|
|
695
|
+
);
|
|
696
|
+
} else if (evt.event === "tunnel_connected") {
|
|
697
|
+
phase = "connected to Metro; waiting for the app to be installed";
|
|
698
|
+
say("connected to Metro; the device is starting and the app is being installed");
|
|
699
|
+
} else if (evt.event === "installing") {
|
|
700
|
+
phase = "installing the app";
|
|
701
|
+
say("the device is up; installing the app");
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
dequeued() {
|
|
705
|
+
dequeuedAt = Date.now();
|
|
706
|
+
phase = "got a device; starting it";
|
|
707
|
+
if (queuedAt !== null)
|
|
708
|
+
say(
|
|
709
|
+
`got a device after ${Math.round((dequeuedAt - queuedAt) / 1e3)} s in the queue; starting it`
|
|
710
|
+
);
|
|
711
|
+
},
|
|
712
|
+
queuedSeconds() {
|
|
713
|
+
if (queuedAt === null) return null;
|
|
714
|
+
return ((dequeuedAt ?? Date.now()) - queuedAt) / 1e3;
|
|
715
|
+
},
|
|
716
|
+
done() {
|
|
717
|
+
if (timer) clearInterval(timer);
|
|
718
|
+
timer = null;
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
}
|
|
578
722
|
function createDevSessionController(auth, webUrl) {
|
|
579
723
|
let handle = null;
|
|
580
724
|
let platform = null;
|
|
581
725
|
let ended = null;
|
|
582
|
-
let presence = null;
|
|
583
726
|
let reloading = null;
|
|
584
|
-
|
|
585
|
-
if (presence) clearInterval(presence);
|
|
586
|
-
presence = null;
|
|
587
|
-
};
|
|
727
|
+
let pending = null;
|
|
588
728
|
const readRokuBuild = () => {
|
|
589
729
|
try {
|
|
590
730
|
return loadDevConfig(process.cwd())?.platforms?.roku?.build ?? null;
|
|
@@ -597,10 +737,9 @@ function createDevSessionController(auth, webUrl) {
|
|
|
597
737
|
const markEnded = (e) => {
|
|
598
738
|
if (!handle || handle.sessionId !== e.sessionId) return;
|
|
599
739
|
const dead = handle;
|
|
600
|
-
stopPresence();
|
|
601
740
|
handle = null;
|
|
602
741
|
platform = null;
|
|
603
|
-
ended = e;
|
|
742
|
+
ended = { ...e, reason: e.reason ?? dead.lapsedReason?.() ?? null };
|
|
604
743
|
void dead.stop().catch(() => {
|
|
605
744
|
});
|
|
606
745
|
};
|
|
@@ -662,71 +801,110 @@ function createDevSessionController(auth, webUrl) {
|
|
|
662
801
|
);
|
|
663
802
|
}
|
|
664
803
|
}
|
|
665
|
-
|
|
666
|
-
|
|
804
|
+
let buildId = cfg.buildId;
|
|
805
|
+
if (buildId !== void 0) {
|
|
806
|
+
checkDevBuild(app, buildId, cfg.platform, { standalone });
|
|
667
807
|
}
|
|
668
808
|
const isTv = PLATFORM_DEVICE[cfg.platform].tv;
|
|
669
809
|
const warnings = [];
|
|
810
|
+
const progress = startProgress(cfg.onProgress);
|
|
670
811
|
let started = null;
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
812
|
+
try {
|
|
813
|
+
started = await startDevSession(
|
|
814
|
+
{
|
|
815
|
+
token: auth.token,
|
|
816
|
+
baseUrl: auth.baseUrl,
|
|
817
|
+
webUrl,
|
|
818
|
+
platform: cfg.platform,
|
|
819
|
+
appId,
|
|
820
|
+
deviceCategory: isTv ? "tv" : entry?.deviceCategory ?? "phone",
|
|
821
|
+
// A named model decides the category itself (startDevSession).
|
|
822
|
+
model: cfg.model,
|
|
823
|
+
buildId,
|
|
824
|
+
// The build choice (a debug build of this project's version over
|
|
825
|
+
// a newer one of another) and its report live in startDevSession,
|
|
826
|
+
// shared with `vibeview dev` and `--detach`.
|
|
827
|
+
app,
|
|
828
|
+
...usesMetro ? { projectDir: process.cwd() } : {},
|
|
829
|
+
...standalone ? { standalone } : {},
|
|
830
|
+
metroPort,
|
|
831
|
+
open: false,
|
|
832
|
+
onCreated: (created) => {
|
|
833
|
+
pending = created;
|
|
834
|
+
},
|
|
835
|
+
onDequeued: () => progress.dequeued()
|
|
836
|
+
},
|
|
837
|
+
(evt) => {
|
|
838
|
+
progress.event(evt);
|
|
839
|
+
if (evt.event === "warning") warnings.push(evt.message);
|
|
840
|
+
if (evt.event === "session_ended" && evt.code !== 0 && started) {
|
|
841
|
+
const id = evt.session_id;
|
|
842
|
+
void api().getSession(id).then(
|
|
843
|
+
(info) => ({ status: info.status, reason: info.failure_reason ?? null }),
|
|
844
|
+
() => ({ status: "stopped", reason: null })
|
|
845
|
+
).then(({ status, reason }) => markEnded({ sessionId: id, status, reason }));
|
|
846
|
+
}
|
|
694
847
|
}
|
|
695
|
-
|
|
696
|
-
|
|
848
|
+
);
|
|
849
|
+
} finally {
|
|
850
|
+
progress.done();
|
|
851
|
+
pending = null;
|
|
852
|
+
}
|
|
697
853
|
handle = started;
|
|
698
854
|
platform = cfg.platform;
|
|
699
855
|
ended = null;
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
}
|
|
712
|
-
);
|
|
713
|
-
}, ROKU_LIVENESS_INTERVAL_MS);
|
|
714
|
-
presence.unref?.();
|
|
856
|
+
const choice = handle.build ?? describeDevBuild(
|
|
857
|
+
app,
|
|
858
|
+
handle.session,
|
|
859
|
+
{ buildId, reason: null, pinned: buildId !== void 0 },
|
|
860
|
+
null
|
|
861
|
+
);
|
|
862
|
+
const project = choice.project;
|
|
863
|
+
if (project?.bundleId && app.package_name && project.bundleId !== app.package_name) {
|
|
864
|
+
warnings.push(
|
|
865
|
+
`this project's ${isTv || cfg.platform.startsWith("android") ? "package name" : "bundle id"} is ${project.bundleId}, but app ${app.public_id} is ${app.package_name} \u2014 check that dev_start runs the app this project builds (list_apps)`
|
|
866
|
+
);
|
|
715
867
|
}
|
|
716
868
|
return {
|
|
717
869
|
sessionId: handle.sessionId,
|
|
718
870
|
pageUrl: handle.pageUrl,
|
|
719
871
|
device: startedDevice(handle.session),
|
|
872
|
+
build: choice.build,
|
|
873
|
+
buildReason: choice.reason,
|
|
874
|
+
pinned: choice.pinned,
|
|
875
|
+
otherBuilds: usesMetro ? choice.other_debug_builds : [],
|
|
876
|
+
project,
|
|
877
|
+
queuedSeconds: progress.queuedSeconds(),
|
|
720
878
|
warnings
|
|
721
879
|
};
|
|
722
880
|
},
|
|
723
881
|
async stop() {
|
|
724
|
-
stopPresence();
|
|
725
882
|
await handle?.stop();
|
|
726
883
|
handle = null;
|
|
727
884
|
platform = null;
|
|
728
885
|
ended = null;
|
|
729
886
|
},
|
|
887
|
+
async shutdown() {
|
|
888
|
+
const stopped = [];
|
|
889
|
+
const held = handle;
|
|
890
|
+
const starting = pending;
|
|
891
|
+
handle = null;
|
|
892
|
+
platform = null;
|
|
893
|
+
pending = null;
|
|
894
|
+
await Promise.all([
|
|
895
|
+
held ? held.stop().then(
|
|
896
|
+
() => void stopped.push(held.sessionId),
|
|
897
|
+
() => {
|
|
898
|
+
}
|
|
899
|
+
) : null,
|
|
900
|
+
starting ? starting.stop().then(
|
|
901
|
+
() => void stopped.push(starting.sessionId),
|
|
902
|
+
() => {
|
|
903
|
+
}
|
|
904
|
+
) : null
|
|
905
|
+
]);
|
|
906
|
+
return stopped;
|
|
907
|
+
},
|
|
730
908
|
current() {
|
|
731
909
|
return handle ? { sessionId: handle.sessionId, pageUrl: handle.pageUrl } : null;
|
|
732
910
|
},
|
|
@@ -803,6 +981,7 @@ var MCP_INSTRUCTIONS = [
|
|
|
803
981
|
"VibeView runs your app on a cloud iOS simulator, Android emulator, Apple TV, Android TV or Roku (beta) device you control with these tools.",
|
|
804
982
|
"Loop: dev_start (platform, app from list_apps; add standalone: true for a release or CI build that needs no Metro) -> ui_tree -> act with refs (tap @e5, type, swipe, press) and read the change each action reports -> dev_stop when done.",
|
|
805
983
|
"Foldables: pick one by model in dev_start (list_device_models marks them foldable, with free/busy counts), then set_posture and rotate.",
|
|
984
|
+
"set_location sets the simulated GPS position (phones and tablets) for location-based flows.",
|
|
806
985
|
"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.",
|
|
807
986
|
"A session stopped elsewhere is reported as ended; call dev_start again for a new one."
|
|
808
987
|
].join("\n");
|
|
@@ -841,30 +1020,89 @@ async function createMcpServer(opts) {
|
|
|
841
1020
|
inputSchema: zodShape(def),
|
|
842
1021
|
annotations: { readOnlyHint: !def.mutating }
|
|
843
1022
|
},
|
|
844
|
-
async (args) => {
|
|
845
|
-
const result = await handleToolCall(def.name, args ?? {}, deps
|
|
1023
|
+
async (args, extra) => {
|
|
1024
|
+
const result = await handleToolCall(def.name, args ?? {}, deps, {
|
|
1025
|
+
progress: progressReporter(extra)
|
|
1026
|
+
});
|
|
846
1027
|
return result;
|
|
847
1028
|
}
|
|
848
1029
|
);
|
|
849
1030
|
}
|
|
850
|
-
return server;
|
|
1031
|
+
return { server, devSession };
|
|
1032
|
+
}
|
|
1033
|
+
function progressReporter(extra) {
|
|
1034
|
+
const token = extra?._meta?.progressToken;
|
|
1035
|
+
const send = extra?.sendNotification;
|
|
1036
|
+
if (token === void 0 || !send) return void 0;
|
|
1037
|
+
let step = 0;
|
|
1038
|
+
return (message) => {
|
|
1039
|
+
step += 1;
|
|
1040
|
+
void send({
|
|
1041
|
+
method: "notifications/progress",
|
|
1042
|
+
params: { progressToken: token, progress: step, message }
|
|
1043
|
+
}).catch(() => {
|
|
1044
|
+
});
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
var SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
1048
|
+
function createShutdown(devSession, opts = {}) {
|
|
1049
|
+
const exit = opts.exit ?? ((code) => process.exit(code));
|
|
1050
|
+
const log = opts.log ?? ((line) => console.error(line));
|
|
1051
|
+
const timeoutMs = opts.timeoutMs ?? SHUTDOWN_TIMEOUT_MS;
|
|
1052
|
+
let running = null;
|
|
1053
|
+
return (reason) => {
|
|
1054
|
+
running ??= (async () => {
|
|
1055
|
+
let timer;
|
|
1056
|
+
const timedOut = new Promise((resolve) => {
|
|
1057
|
+
timer = setTimeout(() => resolve("timeout"), timeoutMs);
|
|
1058
|
+
});
|
|
1059
|
+
const outcome = await Promise.race([
|
|
1060
|
+
devSession.shutdown().catch(() => []),
|
|
1061
|
+
timedOut
|
|
1062
|
+
]);
|
|
1063
|
+
clearTimeout(timer);
|
|
1064
|
+
if (outcome === "timeout") {
|
|
1065
|
+
log(
|
|
1066
|
+
`vibeview mcp: ${reason}; could not confirm the session stopped within ${timeoutMs / 1e3} s`
|
|
1067
|
+
);
|
|
1068
|
+
} else if (outcome.length > 0) {
|
|
1069
|
+
log(`vibeview mcp: ${reason}; stopped session ${outcome.join(", ")}`);
|
|
1070
|
+
}
|
|
1071
|
+
exit(0);
|
|
1072
|
+
})();
|
|
1073
|
+
return running;
|
|
1074
|
+
};
|
|
851
1075
|
}
|
|
852
1076
|
async function runMcpServer(opts) {
|
|
853
1077
|
chalk.level = 0;
|
|
854
|
-
const [server, { StdioServerTransport }] = await Promise.all([
|
|
1078
|
+
const [{ server, devSession }, { StdioServerTransport }] = await Promise.all([
|
|
855
1079
|
createMcpServer(opts),
|
|
856
1080
|
import("@modelcontextprotocol/sdk/server/stdio.js")
|
|
857
1081
|
]);
|
|
858
1082
|
const transport = new StdioServerTransport();
|
|
1083
|
+
const shutdown = createShutdown(devSession);
|
|
1084
|
+
process.stdin.once("end", () => void shutdown("the client closed the connection"));
|
|
1085
|
+
process.stdin.once("close", () => void shutdown("the client closed the connection"));
|
|
1086
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
1087
|
+
process.once(signal, () => void shutdown(`received ${signal}`));
|
|
1088
|
+
}
|
|
1089
|
+
server.server.onclose = () => void shutdown("the connection closed");
|
|
859
1090
|
await server.connect(transport);
|
|
860
1091
|
}
|
|
861
1092
|
export {
|
|
862
1093
|
MCP_INSTRUCTIONS,
|
|
1094
|
+
SHUTDOWN_TIMEOUT_MS,
|
|
1095
|
+
START_PROGRESS_INTERVAL_MS,
|
|
1096
|
+
buildLines2 as buildLines,
|
|
863
1097
|
buildToolDefinitions,
|
|
864
1098
|
createDevSessionController,
|
|
1099
|
+
createShutdown,
|
|
865
1100
|
deviceLine,
|
|
866
1101
|
forMcp,
|
|
867
1102
|
handleToolCall,
|
|
1103
|
+
mcpText,
|
|
1104
|
+
progressReporter,
|
|
868
1105
|
runMcpServer,
|
|
1106
|
+
startProgress,
|
|
869
1107
|
startedDevice
|
|
870
1108
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibeview",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "VibeView CLI -- run your app on cloud iOS simulators, Android emulators, Apple TV, Android TV and Roku (beta) 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",
|