agent-device-lab 0.3.0 → 0.4.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 +12 -4
- package/assets/dashboard/app.css +20 -1
- package/assets/dashboard/app.js +68 -0
- package/assets/dashboard/index.html +13 -2
- package/dist/core/format.js +45 -4
- package/dist/dashboard/server.js +3 -1
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# Agent Device Lab
|
|
2
2
|
|
|
3
|
-
Agent Device Lab lets a coding agent start your web app, use it
|
|
3
|
+
Agent Device Lab lets a coding agent start your web app, use it at phone, tablet or desktop size, and report what breaks, while you watch.
|
|
4
4
|
|
|
5
5
|
- **It runs your project.** The lab starts the services your app needs, in order, or attaches to ones already running. When it finishes it stops only what it started.
|
|
6
6
|
- **It gives the agent a small, exact view.** The agent gets a short list of the controls on screen, each with a ref (`e1`, `e2`, ...). After every click or keystroke it gets only what changed.
|
|
7
|
-
- **It finds layout defects.** It records problems a person would hit
|
|
7
|
+
- **It finds layout defects.** It records problems a person would hit at each screen size, such as a button pushed off the edge on a phone, each with measurements and the steps to reproduce it.
|
|
8
8
|
- **You stay in charge.** A local dashboard shows the agent's screen live. You can pause the agent, take over the browser, hand it back, or stop the run.
|
|
9
9
|
|
|
10
10
|
The same commands work from a terminal (`agentlab`), from an MCP server for Claude Code and Codex, and in CI with predictable exit codes.
|
|
11
11
|
|
|
12
12
|

|
|
13
13
|
|
|
14
|
-
**Status:** Web V1 (`0.
|
|
14
|
+
**Status:** Web V1 (`0.4.0`). It is tested on Linux x64 (Debian 12 and Ubuntu 24.04) with Node.js 22 and 24. macOS, Windows and arm64 are untested, and Alpine does not work. See [Limitations](#limitations).
|
|
15
15
|
|
|
16
16
|
## Install
|
|
17
17
|
|
|
@@ -43,7 +43,7 @@ In your own project the short version is:
|
|
|
43
43
|
cd /path/to/your-app
|
|
44
44
|
|
|
45
45
|
agentlab init # shows what it detected and the agentlab.json it proposes; writes nothing until you confirm
|
|
46
|
-
agentlab start # starts your app, opens a phone
|
|
46
|
+
agentlab start # starts your app, opens a browser at the size in your profile (phone by default), prints the first observation
|
|
47
47
|
agentlab ui # opens the dashboard so you can watch
|
|
48
48
|
|
|
49
49
|
agentlab observe # the controls on screen, with refs
|
|
@@ -69,6 +69,8 @@ The lab reads one file in your project, `agentlab.json`. A minimal one:
|
|
|
69
69
|
}
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
`device` sets the screen the agent uses: `mobile-320`, `mobile-390`, `tablet-768` or `desktop-1440`. Everything works the same way at each size.
|
|
73
|
+
|
|
72
74
|
The lab runs only the commands written there, so committing the file is how you approve them. Every setting is in [docs/configuration.md](docs/configuration.md).
|
|
73
75
|
|
|
74
76
|
## Use it from your coding agent
|
|
@@ -138,6 +140,12 @@ Measurements of the lab, including a benchmark against Playwright MCP and runs o
|
|
|
138
140
|
|
|
139
141
|
Building, testing and the project's rules are in [CONTRIBUTING.md](CONTRIBUTING.md). Report a security issue privately as described in [SECURITY.md](SECURITY.md), not in a public issue. Changes by release are in [CHANGELOG.md](CHANGELOG.md).
|
|
140
142
|
|
|
143
|
+
## Support development
|
|
144
|
+
|
|
145
|
+
If Agent Device Lab is useful to you, you can support its ongoing development
|
|
146
|
+
for $2/month on [Patreon](https://www.patreon.com/Ikafa).
|
|
147
|
+
Support is completely optional.
|
|
148
|
+
|
|
141
149
|
## Licence
|
|
142
150
|
|
|
143
151
|
Apache License 2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE). Copyright 2026 Colony Innovations.
|
package/assets/dashboard/app.css
CHANGED
|
@@ -57,8 +57,27 @@ body { margin: 0; background: var(--bg); color: var(--ink); font: 14px/1.45 syst
|
|
|
57
57
|
.findings-panel { order: 3; }
|
|
58
58
|
.vp-now { margin: 0 0 10px; text-align: center; font-size: 13px; }
|
|
59
59
|
.vp-now strong { font-weight: 600; }
|
|
60
|
+
.vp-toolbar { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
|
|
61
|
+
.vp-toolbar h2 { margin: 0; }
|
|
62
|
+
.vp-actions { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
63
|
+
.vp-actions button { font: inherit; font-size: 12px; min-height: 32px; padding: 4px 10px; border: 1px solid var(--line); border-radius: 6px; color: var(--ink); background: var(--panel); cursor: pointer; }
|
|
64
|
+
.vp-actions button:hover { border-color: var(--accent); }
|
|
65
|
+
.vp-actions button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
66
|
+
.vp-dialog { width: calc(100vw - 32px); height: calc(100dvh - 32px); max-width: none; max-height: none; padding: 0; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); color: var(--ink); }
|
|
67
|
+
.vp-dialog::backdrop { background: rgb(0 0 0 / .65); }
|
|
68
|
+
.vp-dialog[open] { display: flex; flex-direction: column; }
|
|
69
|
+
.vp-dialog .supervision { margin: 0; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; flex: none; max-height: 30%; overflow: auto; }
|
|
70
|
+
:root:fullscreen .vp-dialog { width: 100vw; height: 100dvh; border: 0; border-radius: 0; }
|
|
71
|
+
.vp-dialog .viewport-panel { position: static; display: flex; flex-direction: column; flex: 1; min-height: 0; border: 0; overflow: auto; }
|
|
72
|
+
.vp-dialog .vp-stage { flex: 1; min-height: 0; display: grid; place-items: center; }
|
|
73
|
+
.vp-dialog .device { width: min(calc(100% - 16px), calc(var(--vp-w, 390) * 1px), calc((var(--stage-height, 600px) - 16px) * var(--vp-w, 390) / var(--vp-h, 844))); transition: none; }
|
|
74
|
+
.vp-dialog .sweep { display: none; }
|
|
75
|
+
.vp-dialog .human-panel { flex: none; max-height: 35%; overflow: auto; }
|
|
76
|
+
.vp-dialog .vp-now, .vp-dialog .vp-toolbar { flex: none; }
|
|
77
|
+
@media (max-width: 720px) { .vp-dialog { width: calc(100vw - 12px); height: calc(100dvh - 12px); } }
|
|
78
|
+
@media (max-height: 600px) { .vp-dialog #vp-display-note { display: none; } .vp-dialog .vp-toolbar { margin-bottom: 6px; } .vp-dialog .vp-now { margin-bottom: 6px; font-size: 12px; } }
|
|
60
79
|
/* Sized to the device on screen (--vp-w × --vp-h in CSS pixels), shrunk to fit the column and the window. */
|
|
61
|
-
.device { position: relative; box-sizing:
|
|
80
|
+
.device { position: relative; box-sizing: content-box; margin: 0 auto; width: min(calc(100% - 16px), calc(var(--vp-w, 390) * 1px), calc(max(404px, 100vh - 346px) * var(--vp-w, 390) / var(--vp-h, 844))); border: 8px solid #20252c; border-radius: 22px; overflow: hidden; background: #000; aspect-ratio: var(--vp-w, 390) / var(--vp-h, 844); transition: width .25s ease; }
|
|
62
81
|
@media (prefers-reduced-motion: reduce) { .device { transition: none; } }
|
|
63
82
|
.device img { position: absolute; inset: 0; display: block; width: 100%; height: 100%; object-fit: contain; background: #fff; }
|
|
64
83
|
.device img:not([src]) { visibility: hidden; }
|
package/assets/dashboard/app.js
CHANGED
|
@@ -327,6 +327,74 @@ setInterval(() => {
|
|
|
327
327
|
}, 200);
|
|
328
328
|
document.addEventListener('visibilitychange', updateViewport);
|
|
329
329
|
|
|
330
|
+
// Move the existing viewport into a modal stage: expanding never opens a second stream.
|
|
331
|
+
const viewportPanel = $('viewport-panel');
|
|
332
|
+
const viewportHome = document.createComment('viewport position');
|
|
333
|
+
viewportPanel.before(viewportHome);
|
|
334
|
+
const viewportDialog = $('vp-dialog');
|
|
335
|
+
const supervisionHome = document.createComment('supervision position');
|
|
336
|
+
$('supervision').before(supervisionHome);
|
|
337
|
+
let viewportOpener;
|
|
338
|
+
new ResizeObserver(([entry]) => {
|
|
339
|
+
$('device').style.setProperty('--stage-height', `${entry.contentRect.height}px`);
|
|
340
|
+
}).observe($('vp-stage'));
|
|
341
|
+
|
|
342
|
+
function expandViewport(opener) {
|
|
343
|
+
if (viewportDialog.open) return;
|
|
344
|
+
viewportOpener = opener;
|
|
345
|
+
viewportDialog.append($('supervision'), viewportPanel);
|
|
346
|
+
$('vp-expand').hidden = true;
|
|
347
|
+
$('vp-close').hidden = false;
|
|
348
|
+
$('vp-display-note').textContent = 'Press Escape or close this view to return to the timeline and findings.';
|
|
349
|
+
$('vp-display-note').hidden = false;
|
|
350
|
+
viewportDialog.showModal();
|
|
351
|
+
$('vp-close').focus();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function closeViewport() {
|
|
355
|
+
if (document.fullscreenElement === document.documentElement) await document.exitFullscreen().catch(() => {});
|
|
356
|
+
viewportDialog.close();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
$('vp-expand').addEventListener('click', (e) => expandViewport(e.currentTarget));
|
|
360
|
+
$('vp-close').addEventListener('click', closeViewport);
|
|
361
|
+
$('vp-fullscreen').addEventListener('click', async (e) => {
|
|
362
|
+
if (document.fullscreenElement === document.documentElement) {
|
|
363
|
+
await document.exitFullscreen().catch(() => {});
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
expandViewport(e.currentTarget);
|
|
367
|
+
try {
|
|
368
|
+
await document.documentElement.requestFullscreen();
|
|
369
|
+
} catch {
|
|
370
|
+
$('vp-display-note').textContent = 'Full screen is unavailable in this browser. You can still use the expanded view.';
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
document.addEventListener('fullscreenchange', () => {
|
|
374
|
+
$('vp-fullscreen').textContent = document.fullscreenElement === document.documentElement ? 'Exit full screen' : 'Full screen';
|
|
375
|
+
});
|
|
376
|
+
viewportDialog.addEventListener('cancel', (e) => {
|
|
377
|
+
e.preventDefault();
|
|
378
|
+
if (document.fullscreenElement === document.documentElement) void document.exitFullscreen().catch(() => {});
|
|
379
|
+
else void closeViewport();
|
|
380
|
+
});
|
|
381
|
+
viewportDialog.addEventListener('keydown', (e) => {
|
|
382
|
+
if (e.key !== 'Tab') return;
|
|
383
|
+
const controls = [...viewportDialog.querySelectorAll('button, input, [tabindex="0"]')]
|
|
384
|
+
.filter((el) => !el.disabled && el.getClientRects().length);
|
|
385
|
+
const first = controls[0], last = controls[controls.length - 1];
|
|
386
|
+
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last?.focus(); }
|
|
387
|
+
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first?.focus(); }
|
|
388
|
+
});
|
|
389
|
+
viewportDialog.addEventListener('close', () => {
|
|
390
|
+
supervisionHome.after($('supervision'));
|
|
391
|
+
viewportHome.after(viewportPanel);
|
|
392
|
+
$('vp-expand').hidden = false;
|
|
393
|
+
$('vp-close').hidden = true;
|
|
394
|
+
$('vp-display-note').hidden = true;
|
|
395
|
+
viewportOpener?.focus();
|
|
396
|
+
});
|
|
397
|
+
|
|
330
398
|
// ---------- timeline ----------
|
|
331
399
|
|
|
332
400
|
function renderTimeline() {
|
|
@@ -56,13 +56,23 @@
|
|
|
56
56
|
</section>
|
|
57
57
|
|
|
58
58
|
<main class="grid">
|
|
59
|
-
<section class="panel viewport-panel" aria-label="Live viewport">
|
|
60
|
-
<
|
|
59
|
+
<section id="viewport-panel" class="panel viewport-panel" aria-label="Live viewport">
|
|
60
|
+
<div class="vp-toolbar">
|
|
61
|
+
<h2 id="vp-title">Viewport <span id="vp-caption" class="muted small"></span></h2>
|
|
62
|
+
<div class="vp-actions" role="group" aria-label="Viewport display">
|
|
63
|
+
<button id="vp-expand" type="button" aria-haspopup="dialog">Expand view</button>
|
|
64
|
+
<button id="vp-fullscreen" type="button">Full screen</button>
|
|
65
|
+
<button id="vp-close" type="button" hidden>Close view</button>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<p id="vp-display-note" class="small muted" role="status" hidden></p>
|
|
61
69
|
<p id="vp-now" class="vp-now" role="status"></p>
|
|
70
|
+
<div id="vp-stage" class="vp-stage">
|
|
62
71
|
<div class="device" id="device">
|
|
63
72
|
<img id="viewport" alt="Live view of the device viewport">
|
|
64
73
|
<div id="vp-empty" class="vp-empty">No frames yet</div>
|
|
65
74
|
</div>
|
|
75
|
+
</div>
|
|
66
76
|
<section id="human-panel" class="human-panel" hidden aria-label="Take over the browser">
|
|
67
77
|
<p class="human-banner"><strong>You have control of the browser.</strong> The agent is waiting. Your actions are recorded as descriptions only (no typed text).<span id="human-headed" hidden> You can also use the browser window directly.</span></p>
|
|
68
78
|
<p class="small muted">Click the viewport to tap; scroll over it to scroll.</p>
|
|
@@ -116,5 +126,6 @@
|
|
|
116
126
|
<article id="finding-detail" class="finding-detail" hidden></article>
|
|
117
127
|
</section>
|
|
118
128
|
</main>
|
|
129
|
+
<dialog id="vp-dialog" class="vp-dialog" aria-labelledby="vp-title"></dialog>
|
|
119
130
|
</body>
|
|
120
131
|
</html>
|
package/dist/core/format.js
CHANGED
|
@@ -14,6 +14,8 @@ export function formatControl(c) {
|
|
|
14
14
|
parts.push(c.checked ? 'checked' : 'unchecked');
|
|
15
15
|
if (c.pressed !== undefined)
|
|
16
16
|
parts.push(c.pressed ? 'pressed' : 'not-pressed');
|
|
17
|
+
if (c.selected !== undefined)
|
|
18
|
+
parts.push(c.selected ? 'selected' : 'not-selected');
|
|
17
19
|
if (c.expanded !== undefined)
|
|
18
20
|
parts.push(c.expanded ? 'expanded' : 'collapsed');
|
|
19
21
|
for (const flag of ['disabled', 'required', 'invalid', 'focused'])
|
|
@@ -98,6 +100,8 @@ export function formatChanges(c) {
|
|
|
98
100
|
}
|
|
99
101
|
if (c.route)
|
|
100
102
|
items.push(`route ${c.route.from} → ${c.route.to}`);
|
|
103
|
+
if (c.title)
|
|
104
|
+
items.push(`title ${q(c.title.from)} → ${q(c.title.to)}`);
|
|
101
105
|
if (c.dialog) {
|
|
102
106
|
if (c.dialog.to !== undefined && c.dialog.from === undefined)
|
|
103
107
|
items.push(`+ dialog ${q(c.dialog.to)}`);
|
|
@@ -136,7 +140,7 @@ export function formatChanges(c) {
|
|
|
136
140
|
items.push(`- layout ${f.kind} resolved`);
|
|
137
141
|
if (items.length > MAX_CHANGE_ITEMS) {
|
|
138
142
|
const rest = items.length - MAX_CHANGE_ITEMS;
|
|
139
|
-
return [...items.slice(0, MAX_CHANGE_ITEMS), `… ${rest} more (
|
|
143
|
+
return [...items.slice(0, MAX_CHANGE_ITEMS), `… ${rest} more changes (observe for current state; CLI --json for all changes)`];
|
|
140
144
|
}
|
|
141
145
|
return items;
|
|
142
146
|
}
|
|
@@ -144,7 +148,7 @@ export function formatAction(r) {
|
|
|
144
148
|
const target = r.target ? ` ${r.target.role} ${q(r.target.name)}` : '';
|
|
145
149
|
const head = `${r.action}${r.ref ? ` ${r.ref}` : ''}${target}`;
|
|
146
150
|
if (r.outcome === 'error') {
|
|
147
|
-
return [`${head} → ERROR ${formatError(r.error)}`, ...r.notes.map((n) => `note: ${n}`)].join('\n');
|
|
151
|
+
return [`${head} → ERROR ${formatError(r.error)}`, ...r.notes.map((n) => `note: ${n}`), formatActionDiagnostics(r)].join('\n');
|
|
148
152
|
}
|
|
149
153
|
const lines = [`${head} → ok (${r.method}, ${r.elapsedMs}ms, ${r.settle ? formatSettle(r.settle) : 'settle unknown'})`];
|
|
150
154
|
for (const n of r.notes)
|
|
@@ -166,15 +170,20 @@ export function formatAction(r) {
|
|
|
166
170
|
}
|
|
167
171
|
if (!c.reset) {
|
|
168
172
|
const o = r.observation;
|
|
173
|
+
if (o.omitted)
|
|
174
|
+
lines.push(`controls: ${o.omitted} omitted by budget (observe with a larger limit for current controls)`);
|
|
169
175
|
if (c.removed.length)
|
|
170
176
|
lines.push(`refs: ${c.removed.length <= 8 ? c.removed.map((x) => x.ref).join(', ') : `${c.removed.length} earlier refs`} no longer exist; act on refs from this result or a fresh observe`);
|
|
171
177
|
if (o.layout.length && !c.layoutAdded.length)
|
|
172
178
|
lines.push(`layout: ${o.layout.length} existing flag(s) still present`);
|
|
173
179
|
}
|
|
180
|
+
lines.push(formatActionDiagnostics(r));
|
|
181
|
+
return lines.join('\n');
|
|
182
|
+
}
|
|
183
|
+
function formatActionDiagnostics(r) {
|
|
174
184
|
const errs = r.newConsoleErrors.map((e) => q(e.text)).join('; ');
|
|
175
185
|
const fails = r.newFailedRequests.map((f) => `${f.method} ${f.url} ${f.status ?? f.failure}`).join('; ');
|
|
176
|
-
|
|
177
|
-
return lines.join('\n');
|
|
186
|
+
return `console: ${r.newConsoleErrors.length} new errors${errs ? ` (${errs})` : ''} network: ${r.newFailedRequests.length} new failures${fails ? ` (${fails})` : ''}`;
|
|
178
187
|
}
|
|
179
188
|
export function formatStart(r) {
|
|
180
189
|
const s = r.session;
|
|
@@ -255,8 +264,37 @@ export function formatTabs(tabs) {
|
|
|
255
264
|
}
|
|
256
265
|
export function formatError(e) {
|
|
257
266
|
const lines = [`${e.code}: ${e.message}`];
|
|
267
|
+
lines.push(`recoverable: ${e.recoverable}`);
|
|
258
268
|
if (e.hint)
|
|
259
269
|
lines.push(`hint: ${e.hint}`);
|
|
270
|
+
const control = e.details?.control;
|
|
271
|
+
if (control && typeof control === 'object') {
|
|
272
|
+
const s = control;
|
|
273
|
+
const parts = [];
|
|
274
|
+
if (typeof s.mode === 'string')
|
|
275
|
+
parts.push(s.mode);
|
|
276
|
+
if (typeof s.pending === 'string')
|
|
277
|
+
parts.push(`pending=${s.pending}`);
|
|
278
|
+
if (typeof s.by === 'string')
|
|
279
|
+
parts.push(`by=${q(s.by)}`);
|
|
280
|
+
if (typeof s.since === 'string')
|
|
281
|
+
parts.push(`since=${q(s.since)}`);
|
|
282
|
+
if (typeof s.observeRequired === 'boolean')
|
|
283
|
+
parts.push(`observeRequired=${s.observeRequired}`);
|
|
284
|
+
if (typeof s.interrupt === 'boolean')
|
|
285
|
+
parts.push(`interrupt=${s.interrupt}`);
|
|
286
|
+
if (typeof s.humanInteractions === 'number')
|
|
287
|
+
parts.push(`humanInteractions=${s.humanInteractions}`);
|
|
288
|
+
if (s.busy && typeof s.busy === 'object') {
|
|
289
|
+
const busy = s.busy;
|
|
290
|
+
if (typeof busy.command === 'string')
|
|
291
|
+
parts.push(`busy=${q(busy.command)}`);
|
|
292
|
+
if (typeof busy.since === 'string')
|
|
293
|
+
parts.push(`busySince=${q(busy.since)}`);
|
|
294
|
+
}
|
|
295
|
+
if (parts.length)
|
|
296
|
+
lines.push(`control: ${parts.join(' ')}`);
|
|
297
|
+
}
|
|
260
298
|
const tail = e.details?.logTail;
|
|
261
299
|
if (Array.isArray(tail) && tail.length)
|
|
262
300
|
lines.push('server log tail:', ...tail.map((l) => ` ${l}`));
|
|
@@ -274,6 +312,9 @@ export function formatError(e) {
|
|
|
274
312
|
const candidates = e.details?.candidates;
|
|
275
313
|
if (Array.isArray(candidates))
|
|
276
314
|
lines.push(`candidates: ${candidates.map((c) => `${c.ref} ${c.role} ${q(c.name)}`).join('; ')}`);
|
|
315
|
+
const sameRole = e.details?.sameRole;
|
|
316
|
+
if (Array.isArray(sameRole))
|
|
317
|
+
lines.push(`same role: ${sameRole.map((c) => q(String(c))).join('; ')}`);
|
|
277
318
|
return lines.join('\n');
|
|
278
319
|
}
|
|
279
320
|
/** A person's interaction in words, e.g. `tapped button "Save"`, `typed into textbox "Password" (hidden)`. */
|
package/dist/dashboard/server.js
CHANGED
|
@@ -10,7 +10,9 @@ import { readFileSync } from 'node:fs';
|
|
|
10
10
|
import { readFile } from 'node:fs/promises';
|
|
11
11
|
import { createServer } from 'node:http';
|
|
12
12
|
import { LabError } from '../core/schema.js';
|
|
13
|
-
|
|
13
|
+
// Preserve desktop text at 1× CSS resolution; Lab never upscales smaller devices.
|
|
14
|
+
// Keep the rate conservative: clearer frames need more encoding and bandwidth.
|
|
15
|
+
export const DEFAULT_SCREENCAST = { maxFps: 5, maxWidth: 1920, quality: 85 };
|
|
14
16
|
const BOUNDARY = 'agentlab-frame';
|
|
15
17
|
const PART = `\r\n--${BOUNDARY}\r\nContent-Type: image/jpeg\r\n\r\n`;
|
|
16
18
|
/** A slow viewer skips frames rather than buffering them without limit. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-device-lab",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Lets a coding agent start your web app, use it
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Lets a coding agent start your web app, use it at phone, tablet or desktop size, and report what breaks, while you watch.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
7
7
|
"coding-agent",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"playwright",
|
|
10
10
|
"responsive",
|
|
11
11
|
"mobile",
|
|
12
|
+
"desktop",
|
|
12
13
|
"testing",
|
|
13
14
|
"dev-server",
|
|
14
15
|
"claude-code",
|
|
@@ -24,6 +25,10 @@
|
|
|
24
25
|
"bugs": {
|
|
25
26
|
"url": "https://github.com/Colony-Innovations/agent-device-lab/issues"
|
|
26
27
|
},
|
|
28
|
+
"funding": {
|
|
29
|
+
"type": "patreon",
|
|
30
|
+
"url": "https://www.patreon.com/Ikafa"
|
|
31
|
+
},
|
|
27
32
|
"type": "module",
|
|
28
33
|
"bin": {
|
|
29
34
|
"agentlab": "bin/agentlab.js"
|
|
@@ -49,7 +54,7 @@
|
|
|
49
54
|
"verify:package": "node scripts/verify-package.mjs"
|
|
50
55
|
},
|
|
51
56
|
"dependencies": {
|
|
52
|
-
"@modelcontextprotocol/sdk": "1.
|
|
57
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
53
58
|
"playwright": "1.63.0"
|
|
54
59
|
},
|
|
55
60
|
"devDependencies": {
|