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 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.1",
4
- "description": "The DemoBites agentic recorder \u2014 you prompt, it films a real browser, and DemoBites turns the take into an editable demo bite.",
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
+ }
@@ -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 }] // focus rectangles, frame px
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 }] } // focus rectangles
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) {