demobite 1.0.1 → 1.0.3
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 +47 -0
- package/package.json +3 -3
- package/scripts/record.mjs +40 -0
- package/skill/SKILL.md +11 -2
- package/skill/scripts/manifest.mjs +43 -2
package/README.md
CHANGED
|
@@ -31,6 +31,53 @@ The agent storyboards the flow, films it in a real Chrome, and stages the take
|
|
|
31
31
|
for your approval inside DemoBites. You approve in the product; the platform
|
|
32
32
|
does the rest.
|
|
33
33
|
|
|
34
|
+
## Manage DemoBites from your agent (MCP)
|
|
35
|
+
|
|
36
|
+
Recording is half the story. The same package wires the **DemoBites management
|
|
37
|
+
MCP** — a control plane your agent uses to run your Update Center and Demo
|
|
38
|
+
Center. Your customers' agents read your centers; your agent runs them.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx demobite mcp
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Claude Code is registered automatically. Any other MCP client works over
|
|
45
|
+
Streamable HTTP: point it at `https://app.demobites.com/api/mcp` with the
|
|
46
|
+
`Authorization: Bearer <key>` header the command prints (in Cursor, add both
|
|
47
|
+
under Settings → MCP). Then talk to your agent like a teammate:
|
|
48
|
+
|
|
49
|
+
> "Create a release with my latest bites, add Spanish, and publish it."
|
|
50
|
+
|
|
51
|
+
> "What's on our Demo Center? Swap the checkout bite for the onboarding one."
|
|
52
|
+
|
|
53
|
+
### What it can do — 19 tools
|
|
54
|
+
|
|
55
|
+
| Group | Tools |
|
|
56
|
+
|---|---|
|
|
57
|
+
| Connect | `get_started` · `connect_demobites` · `check_connection` · `get_recording_options` |
|
|
58
|
+
| Read | `get_status` · `list_bites` · `list_releases` · `get_release` · `list_languages` |
|
|
59
|
+
| Draft & edit | `create_release` · `update_release` · `assign_bites_to_release` · `update_center_settings` · `add_language` · `update_demo_center` |
|
|
60
|
+
| Publish — with your approval | `publish_release` · `unpublish_release` · `remove_language` · `publish_demo_center` |
|
|
61
|
+
|
|
62
|
+
The server describes itself: any client's `tools/list` returns every tool with
|
|
63
|
+
its full input schema, straight from the running code.
|
|
64
|
+
|
|
65
|
+
### You stay in charge
|
|
66
|
+
|
|
67
|
+
Draft work executes directly, exactly like clicking around the product. But
|
|
68
|
+
anything that touches a **public** page is two-phase: the tool returns a
|
|
69
|
+
human-readable preview plus a single-use confirmation token, your agent shows
|
|
70
|
+
you the preview, and only your go-ahead executes it. Tokens expire in ten
|
|
71
|
+
minutes, are bound to the exact action and arguments, and every management
|
|
72
|
+
call lands in an audit log.
|
|
73
|
+
|
|
74
|
+
### Plans
|
|
75
|
+
|
|
76
|
+
Connecting and reading are open. Managing requires the **Grow** plan.
|
|
77
|
+
Recording works on every plan.
|
|
78
|
+
|
|
79
|
+
Full guide: [Manage from your agent](https://www.demobites.com/docs/bites/manage-from-your-agent)
|
|
80
|
+
|
|
34
81
|
## What's in this repository
|
|
35
82
|
|
|
36
83
|
| Directory | What it is |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "demobite",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "The DemoBites agentic recorder
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "The DemoBites agentic recorder — you prompt, it films a real browser, and DemoBites turns the take into an editable demo bite.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"demobite": "launcher/index.mjs"
|
|
7
7
|
},
|
|
@@ -35,4 +35,4 @@
|
|
|
35
35
|
},
|
|
36
36
|
"license": "MIT",
|
|
37
37
|
"type": "module"
|
|
38
|
-
}
|
|
38
|
+
}
|
package/scripts/record.mjs
CHANGED
|
@@ -85,6 +85,9 @@ const DEFAULT_HOVER_DWELL = 3200;
|
|
|
85
85
|
const DEFAULT_CLICK_DWELL = 700;
|
|
86
86
|
const DEFAULT_CLICK_AFTER = 2000;
|
|
87
87
|
const CLOSING_BEAT_MS = 2200;
|
|
88
|
+
// LAW (hold your line, founder 2026-09-02): a beat is never shorter than its
|
|
89
|
+
// own narration. Same estimate manifest.mjs uses (words / 2.6 s + 0.4 s).
|
|
90
|
+
const estimateSpeech = (text) => String(text).trim().split(/\s+/).filter(Boolean).length / 2.6 + 0.4;
|
|
88
91
|
|
|
89
92
|
const narrationOf = (s) => {
|
|
90
93
|
if (s.narration == null) return null;
|
|
@@ -529,9 +532,34 @@ try {
|
|
|
529
532
|
};
|
|
530
533
|
process.stdout.write(`step ${rec.n} ${step.action} ${step.label ?? ""}\n`);
|
|
531
534
|
if (step.action === "goto") {
|
|
535
|
+
const navIssuedAt = t();
|
|
532
536
|
await page.goto(step.url, { waitUntil: "load" });
|
|
533
537
|
await ensureCursor();
|
|
534
538
|
await applyHideCss();
|
|
539
|
+
if (manifest.record_from !== undefined) {
|
|
540
|
+
// LAW (page transitions, founder 2026-09-02): a mid-take navigation
|
|
541
|
+
// must not show its loading blank. Wait for the new page to PAINT,
|
|
542
|
+
// then stamp the window so manifest.mjs cuts it out with a fade —
|
|
543
|
+
// the viewer sees page one, a fade, page two. No zoom or narration
|
|
544
|
+
// may live inside that window (manifest.mjs keeps them out).
|
|
545
|
+
await page.waitForLoadState("networkidle", { timeout: 5000 }).catch(() => {});
|
|
546
|
+
const paintDeadline = Date.now() + 10000;
|
|
547
|
+
while (Date.now() < paintDeadline) {
|
|
548
|
+
const painted = await page.evaluate(() => {
|
|
549
|
+
const textMass = (document.body?.innerText ?? "").trim().length;
|
|
550
|
+
let visible = 0;
|
|
551
|
+
for (const el of document.querySelectorAll("div, section, main, button")) {
|
|
552
|
+
const r = el.getBoundingClientRect();
|
|
553
|
+
if (r.width > 40 && r.height > 20) { visible++; if (visible > 8) break; }
|
|
554
|
+
}
|
|
555
|
+
return textMass > 80 && visible > 8;
|
|
556
|
+
}).catch(() => false);
|
|
557
|
+
if (painted) break;
|
|
558
|
+
await page.waitForTimeout(200);
|
|
559
|
+
}
|
|
560
|
+
await page.waitForTimeout(350);
|
|
561
|
+
rec.nav = { from: navIssuedAt, to: t() };
|
|
562
|
+
}
|
|
535
563
|
if (manifest.record_from === undefined) {
|
|
536
564
|
// LAW (first-frame, founder 2026-08-08, HARDENED 2026-08-13 after a
|
|
537
565
|
// take opened on a white page): the published cut opens on a FULLY
|
|
@@ -615,6 +643,7 @@ try {
|
|
|
615
643
|
currentCursor = "arrow";
|
|
616
644
|
await glide(x, y);
|
|
617
645
|
const arrivalT = t();
|
|
646
|
+
rec.arrival = arrivalT; // narration anchors here (manifest.mjs) — so does the beat hold
|
|
618
647
|
currentCursor = targetCursor;
|
|
619
648
|
rec.cursor = currentCursor;
|
|
620
649
|
pushMouse("move", x, y);
|
|
@@ -755,6 +784,7 @@ try {
|
|
|
755
784
|
currentCursor = "arrow";
|
|
756
785
|
await glide(x, y);
|
|
757
786
|
const arrivalT = t();
|
|
787
|
+
rec.arrival = arrivalT; // narration anchors here (manifest.mjs) — so does the beat hold
|
|
758
788
|
currentCursor = targetCursor;
|
|
759
789
|
rec.cursor = currentCursor;
|
|
760
790
|
pushMouse("move", x, y);
|
|
@@ -795,6 +825,16 @@ try {
|
|
|
795
825
|
pushMouse("move", cx, cy);
|
|
796
826
|
await page.waitForTimeout(step.after ?? DEFAULT_CLICK_AFTER);
|
|
797
827
|
}
|
|
828
|
+
if (rec.narration && step.action !== "goto" && step.action !== "expect") {
|
|
829
|
+
// Hold the shot until the line would have finished, plus a breath — the
|
|
830
|
+
// ingestion refits words to video but cannot fit 5 s of speech in a 2 s
|
|
831
|
+
// beat (founder 2026-09-02: the last track ran past the end).
|
|
832
|
+
// Measured from ARRIVAL (where manifest.mjs anchors the line), not from
|
|
833
|
+
// the cursor's departure — the glide is not speaking time.
|
|
834
|
+
const need = estimateSpeech(rec.narration) + 0.5;
|
|
835
|
+
const elapsed = t() - (rec.arrival ?? rec.t_start);
|
|
836
|
+
if (elapsed < need) await page.waitForTimeout(Math.round((need - elapsed) * 1000));
|
|
837
|
+
}
|
|
798
838
|
rec.t_end = t();
|
|
799
839
|
manifest.steps.push(rec);
|
|
800
840
|
}
|
package/skill/SKILL.md
CHANGED
|
@@ -90,7 +90,7 @@ Holding shots to cover estimated lines is what produced a 60 second take with th
|
|
|
90
90
|
|
|
91
91
|
**Budgets, hold yourself to them:**
|
|
92
92
|
|
|
93
|
-
- **30 to 45 seconds total.** Over 45 is a rewrite, not a trim.
|
|
93
|
+
- **30 to 45 seconds total (35 to 45 when the story crosses pages).** Over 45 is a rewrite, not a trim; under 30 with two pages is rushing, see the linger law below.
|
|
94
94
|
- **90 seconds is a HARD CAP, by product concept, and it is a limit, not a
|
|
95
95
|
target.** A Bite over 90 seconds does not exist. Budget the beats BEFORE
|
|
96
96
|
filming: if the human's scenario cannot honestly fit inside 90 seconds, do
|
|
@@ -103,6 +103,14 @@ Holding shots to cover estimated lines is what produced a 60 second take with th
|
|
|
103
103
|
|
|
104
104
|
**LAW: a line must FIT its beat, and a beat that NAVIGATES away cannot hold two lines** (founder drift analysis, 2026-08-09). Each narration line plays while its own beat is on screen. If a beat is a click that navigates to a new page, everything you want said ABOUT the old page has to fit BEFORE that click — a ~14-word line is ~5s of speech, so one line per pre-navigation beat, not two stacked. Cramming the intro plus a second observation before a fast navigation is what makes the words drift a beat behind the picture (a list-page sentence finishing over the product page). If you need to say two things about a page, either say them AFTER you have landed on it, or give the source beat a longer `dwell` so the line finishes before the click. The ingestion fits words to the video, but it cannot make 8 seconds of speech fit into a 5 second window — that is authoring, and it is yours.
|
|
105
105
|
|
|
106
|
+
**LAW: every take opens with a framing intro** (founder, 2026-09-02). The first narrated beat frames the story for someone who is NOT inside the product yet: "Let's look at what happens when a visitor searches your Update Center for something you have not published yet." Never open on a UI detail ("Search sits at the top right"). The viewer is not in the realm; bring them in first.
|
|
107
|
+
|
|
108
|
+
**LAW: narrate the path, do not drive.** Before every navigation, scroll or drill-down, SAY where we are going and why, in the beat before it: "In the analytics for this Update Center, near the bottom, sits search intelligence." A viewer who only sees a cursor dive into a panel learns nothing about how to get there themselves.
|
|
109
|
+
|
|
110
|
+
**LAW: linger.** A single-page story is 30 to 45 seconds; a story that crosses pages is 35 to 45 seconds, never 23. Every beat holds at least as long as its own line plus a breath (record.mjs now enforces this: a narrated beat waits until words / 2.6 s + 0.8 s have passed), and the last beat holds its whole line so no track ever runs past the end of the video. The ingestion refits words to video; it cannot fit five seconds of speech into a two-second beat.
|
|
111
|
+
|
|
112
|
+
**LAW: page transitions are cut and faded, never watched.** When the story moves to another page, the viewer sees page one, a short fade, page two — never the loading blank. record.mjs stamps every mid-take `goto` and manifest.mjs cuts that window out with a fade (`cuts` in the wire manifest); the ingestion lays it on the bite as a timeline cut. No zoom and no narration live inside a cut (the studio forbids both), so put the line about the new page on the beat AFTER it has landed, and say goodbye to the old page BEFORE the goto.
|
|
113
|
+
|
|
106
114
|
Storyboard schema:
|
|
107
115
|
|
|
108
116
|
```json
|
|
@@ -246,7 +254,8 @@ What the human approves on that page is the **picture and the coverage**, never
|
|
|
246
254
|
click?: { x, y, t }, // frame px + seconds
|
|
247
255
|
narration?: { text, t, estimated_duration }
|
|
248
256
|
}],
|
|
249
|
-
camera: [{ t_start, t_end, x, y, w, h, label }]
|
|
257
|
+
camera: [{ t_start, t_end, x, y, w, h, label }], // focus rectangles, frame px
|
|
258
|
+
cuts?: [{ t_start, t_end, transition: 'fade'|'abrupt', n }] // navigation loads, cut out of the bite
|
|
250
259
|
}
|
|
251
260
|
```
|
|
252
261
|
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// on_screen?, // what the viewer sees
|
|
10
10
|
// click?:{x,y,t}, // frame px + seconds
|
|
11
11
|
// narration?:{text,t,estimated_duration} }],
|
|
12
|
-
// camera:[{ t_start, t_end, x, y, w, h, label }]
|
|
12
|
+
// camera:[{ t_start, t_end, x, y, w, h, label }], // focus rectangles
|
|
13
|
+
// cuts?:[{ t_start, t_end, transition:'fade'|'abrupt', n }] } // navigation loads cut out
|
|
13
14
|
//
|
|
14
15
|
// EVERY time is relative to the uploaded file: record_from is subtracted and
|
|
15
16
|
// the result clamped at 0.
|
|
@@ -132,6 +133,24 @@ const steps = (man.steps ?? []).map((s) => {
|
|
|
132
133
|
return out;
|
|
133
134
|
});
|
|
134
135
|
|
|
136
|
+
// ── LAW (page transitions, founder 2026-09-02): a mid-take navigation is CUT
|
|
137
|
+
// out with a FADE — the viewer sees page one, a fade, page two, never the
|
|
138
|
+
// loading blank. record.mjs stamps `nav {from,to}` on every goto after the
|
|
139
|
+
// first; each becomes a cut on the uploaded timeline. No narration may start
|
|
140
|
+
// inside a cut (pushed to the cut's end) and no camera shot may live in one
|
|
141
|
+
// (clipped/split below) — the studio forbids zoom + TTS inside cuts anyway.
|
|
142
|
+
const cuts = (man.steps ?? [])
|
|
143
|
+
.filter((s) => s.nav && Number.isFinite(s.nav.from) && Number.isFinite(s.nav.to))
|
|
144
|
+
.map((s) => ({ t_start: round2(norm(s.nav.from + 0.12)), t_end: round2(norm(s.nav.to)), transition: "fade", n: s.n }))
|
|
145
|
+
.filter((c) => c.t_end - c.t_start >= 0.3 && c.t_start >= 0 && c.t_end <= duration)
|
|
146
|
+
.sort((a, b) => a.t_start - b.t_start);
|
|
147
|
+
const insideCut = (t) => cuts.find((c) => t > c.t_start && t < c.t_end);
|
|
148
|
+
for (const st of steps) {
|
|
149
|
+
if (!st.narration) continue;
|
|
150
|
+
const c = insideCut(st.narration.t);
|
|
151
|
+
if (c) st.narration.t = round2(c.t_end + 0.1);
|
|
152
|
+
}
|
|
153
|
+
|
|
135
154
|
// ── LAW (breathing room, founder 2026-08-09): narration never speaks over
|
|
136
155
|
// the first or last half second. A voice at t=0 startles; a voice cut by the
|
|
137
156
|
// end reads broken. First line starts at >=0.5s; the last line ENDS at
|
|
@@ -281,6 +300,27 @@ const interactions = mouseEvents.length
|
|
|
281
300
|
}
|
|
282
301
|
: null;
|
|
283
302
|
|
|
303
|
+
// Camera shots never overlap a cut: drop shots inside one, clip those that
|
|
304
|
+
// straddle an edge, split those that contain one.
|
|
305
|
+
if (cuts.length > 0) {
|
|
306
|
+
const clipped = [];
|
|
307
|
+
for (const sh of camera) {
|
|
308
|
+
let pieces = [{ ...sh }];
|
|
309
|
+
for (const c of cuts) {
|
|
310
|
+
const next = [];
|
|
311
|
+
for (const p of pieces) {
|
|
312
|
+
if (p.t_end <= c.t_start || p.t_start >= c.t_end) { next.push(p); continue; }
|
|
313
|
+
if (p.t_start < c.t_start) next.push({ ...p, t_end: round2(c.t_start) });
|
|
314
|
+
if (p.t_end > c.t_end) next.push({ ...p, t_start: round2(c.t_end) });
|
|
315
|
+
}
|
|
316
|
+
pieces = next;
|
|
317
|
+
}
|
|
318
|
+
for (const p of pieces) if (p.t_end - p.t_start >= 0.25) clipped.push(p);
|
|
319
|
+
}
|
|
320
|
+
camera.length = 0;
|
|
321
|
+
camera.push(...clipped.sort((a, b) => a.t_start - b.t_start));
|
|
322
|
+
}
|
|
323
|
+
|
|
284
324
|
const wire = {
|
|
285
325
|
version: 2,
|
|
286
326
|
app: man.app ?? "App",
|
|
@@ -289,13 +329,14 @@ const wire = {
|
|
|
289
329
|
duration,
|
|
290
330
|
steps,
|
|
291
331
|
camera,
|
|
332
|
+
...(cuts.length > 0 ? { cuts } : {}),
|
|
292
333
|
...(interactions ? { interactions } : {}),
|
|
293
334
|
};
|
|
294
335
|
const outPath = path.join(dir, "manifest.demobites.json");
|
|
295
336
|
fs.writeFileSync(outPath, JSON.stringify(wire, null, 2));
|
|
296
337
|
console.log(
|
|
297
338
|
`manifest.demobites.json written (v2: ${steps.length} steps, ${camera.length} camera shots, ` +
|
|
298
|
-
`${mouseEvents.length} mouse events, ${duration}s)`,
|
|
339
|
+
`${mouseEvents.length} mouse events, ${cuts.length} cut${cuts.length === 1 ? "" : "s"}, ${duration}s)`,
|
|
299
340
|
);
|
|
300
341
|
|
|
301
342
|
if (wantSrt) {
|