demobite 1.15.0 → 1.17.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.
@@ -332,7 +332,7 @@ if (arg === "listen") {
332
332
  await event(job.id, "started", { machine: name });
333
333
  const takeDir = path.join(process.cwd(), ".recorder", "takes", job.id);
334
334
  fs.mkdirSync(takeDir, { recursive: true });
335
- fs.writeFileSync(path.join(takeDir, "job.json"), JSON.stringify({ id: job.id, brief: job.brief, url: job.url ?? null, model, review_storyboard: job.review_storyboard !== false }, null, 2) + "\n");
335
+ fs.writeFileSync(path.join(takeDir, "job.json"), JSON.stringify({ id: job.id, brief: job.brief, url: job.url ?? null, model, review_storyboard: job.review_storyboard !== false, ...(job.retake_of ? { retake_of: job.retake_of, retake_note: job.retake_note ?? null, storyboard: job.storyboard ?? null } : {}) }, null, 2) + "\n");
336
336
  // Spawned async (1.14.1): a spawnSync here blocked the event loop, so the 5 s heartbeat never fired
337
337
  // during a take and the rail called the machine offline mid-recording (founder 2026-10-15 17:39).
338
338
  const child = (await import("node:child_process")).spawn("node", [path.join(dest, "scripts", "plan.mjs"), takeDir], { cwd: process.cwd(), stdio: "inherit", env: { ...process.env, RECORDER_JOB: job.id, RECORDER_BASE: base, RECORDER_LISTEN: "1" } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "demobite",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
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"
@@ -0,0 +1,28 @@
1
+ // THE CAMERA'S CLOCK (2026-10-15). What a storyboard step really costs on the camera, so the
2
+ // planner's total is the take's total. Numbers from record.mjs: estimateSpeech = words / 2.6 + 0.4;
3
+ // the hold is speech + 0.5 s breath measured from arrival; a click or hover spends ~1 s on cursor
4
+ // flight and dwell before arrival; the first narrated beat carries STARTUP_LEAD_SLACK_S = 2.5;
5
+ // the take closes on CLOSING_BEAT_MS = 2200. Keep the two in step.
6
+ export const CLOSING_S = 2.2;
7
+ const ms = (v, fallback) => (v == null ? fallback : v < 60 ? v * 1000 : v);
8
+ const speech = (text) => { const n = String(text ?? "").trim().split(/\s+/).filter(Boolean).length; return n ? n / 2.6 + 0.4 + 0.5 : 0; };
9
+ export const secondsOf = (s, firstNarrated) => {
10
+ const text = typeof s.narration === "string" ? s.narration : s.narration?.text ?? "";
11
+ const say = speech(text);
12
+ const slack = say && firstNarrated ? 2.5 : 0;
13
+ switch (s.action) {
14
+ case "goto": return 0.1;
15
+ case "settle": return Math.max(ms(s.ms, 1200) / 1000, say) + slack;
16
+ case "scroll": return Math.max(ms(s.ms, 1900) / 1000, say) + slack;
17
+ case "click":
18
+ case "hover": return 1.0 + Math.max(ms(s.after, 2000) / 1000, say) + slack;
19
+ case "type": return 1.0 + String(s.text ?? "").length * 0.05 + Math.max(ms(s.after, 1500) / 1000, say) + slack;
20
+ case "expect": return 0.3;
21
+ default: return 1 + say;
22
+ }
23
+ };
24
+ export const estimateTake = (steps) => {
25
+ let first = true, total = CLOSING_S;
26
+ for (const s of steps ?? []) { total += secondsOf(s, first); if (s.narration) first = false; }
27
+ return total;
28
+ };
@@ -347,6 +347,10 @@ const wire = {
347
347
  frame: { width: FW, height: FH },
348
348
  // BROWSER HEADER (2026-09-16): true by default; DemoBites adds the dark macOS bar on top at ingest. A workspace rule "No browser header" sets it false.
349
349
  ...(typeof man.browserHeader === "boolean" ? { browserHeader: man.browserHeader } : {}),
350
+ // APPROVED STORY (founder 2026-10-15, bite 1228: "the storyteller is blabbing"): the lines came from the
351
+ // director and the person said Film it to THESE lines. DemoBites keeps them verbatim and never writes
352
+ // a filler into a silence or a longer line into a longer beat.
353
+ narration_approved: true,
350
354
  duration,
351
355
  steps,
352
356
  camera,
@@ -18,6 +18,7 @@
18
18
  // reads is logged on the take (<takeDir>/code-shared.log) so the person can
19
19
  // see exactly what left the machine.
20
20
  import fs from "node:fs";
21
+ import { estimateTake } from "./clock.mjs";
21
22
  import path from "node:path";
22
23
  import { spawnSync } from "node:child_process";
23
24
  import { fileURLToPath } from "node:url";
@@ -150,6 +151,10 @@ const validate = (sb) => {
150
151
  if (s.frame !== undefined && s.frame !== "close" && s.frame !== "wide") return `${at}: frame must be close or wide`;
151
152
  }
152
153
  if (sb.steps.length > 14) return "more than 14 steps; 6 to 10 is the law";
154
+ // THE CAMERA'S CLOCK (founder 2026-10-15): the take may not run past 45 s. The estimate is the
155
+ // camera's own (clock.mjs); the model gets the number and the cure.
156
+ const est = estimateTake(sb.steps);
157
+ if (est > 45) return `the camera would run this take about ${Math.round(est)} s; the law is 30 to 45. Cut words (6 to 11 a line) or steps (5 to 8 narrated); never pad, never add.`;
153
158
  return null;
154
159
  };
155
160
 
@@ -192,7 +197,11 @@ const firstMessage = () => {
192
197
  `## The app's words on the starting screen\n${vocab}`,
193
198
  `## Workspace rules (version ${rules.version || 0})\n${rules.lines?.length ? rules.lines.map((l, i) => `${i + 1}. ${l}`).join("\n") : "(none)"}`,
194
199
  `## The project's files${codeOn ? " (this machine sits in the app's repository; read_file is on)" : " (no code here)"}\n${listFiles()}`,
195
- codeOn ? `Write the storyboard, probe it, fix it, probe again, then submit_storyboard.` : `There is no code: explore first (explore from the starting address, looking_for = the brief's nouns), then write the storyboard, probe it, fix it, probe again, then submit_storyboard.`,
200
+ // A RETAKE (founder 2026-10-15): the same demo again, because the app changed under it. The previous
201
+ // storyboard is the base; the note says what to keep an eye on; the probe finds what moved.
202
+ job.retake_of && job.storyboard
203
+ ? `## This is a retake\nThe person wants the same demo filmed again, the same length and the same shots, because the app changed since. Their note: "${(job.retake_note || "no note").replace(/"/g, "'")}". ${job.storyboard.storyboard ? "Start from the previous storyboard below: keep its steps and its lines; probe it; fix only what no longer resolves or what the note names; then submit_storyboard. Never explore when the probe passes." : "Only the previous storyboard's lines survive (below): rebuild the same steps with the same lines, find the controls again, probe, then submit_storyboard."}\n\n## The previous storyboard\n${JSON.stringify(job.storyboard.storyboard ?? job.storyboard.steps ?? job.storyboard)}`
204
+ : codeOn ? `Write the storyboard, probe it, fix it, probe again, then submit_storyboard.` : `There is no code: explore first (explore from the starting address, looking_for = the brief's nouns), then write the storyboard, probe it, fix it, probe again, then submit_storyboard.`,
196
205
  ];
197
206
  return parts.join("\n\n");
198
207
  };
@@ -23,11 +23,13 @@ if (!mode || !file || !["print", "words", "await"].includes(mode)) {
23
23
  }
24
24
  const sb = JSON.parse(fs.readFileSync(file, "utf8"));
25
25
  const steps = Array.isArray(sb.steps) ? sb.steps : [];
26
- const FLOOR = { goto: 1.5, settle: 2, click: 2.5, hover: 2, type: 3, scroll: 2, expect: 1 };
27
- const secondsOf = (s) => {
28
- const words = String(s.narration ?? "").trim().split(/\s+/).filter(Boolean).length;
29
- return Math.max(typeof s.dwell === "number" ? s.dwell : 0, words / 2.5, FLOOR[s.action] ?? 2);
30
- };
26
+ // THE CAMERA'S CLOCK (founder 2026-10-15, bite 1228 said 0:36 and ran 0:51): a beat is held until its
27
+ // line would have finished (words / 2.6 s + 0.4 s, record.mjs estimateSpeech) plus half a second of
28
+ // breath; a click or hover adds about a second of cursor flight and dwell; the first narrated settle
29
+ // carries 2.5 s of startup slack; the take closes on a 2.2 s beat. Same numbers as record.mjs.
30
+ import { secondsOf as cameraSeconds, CLOSING_S } from "./clock.mjs";
31
+ let firstNarrated = true;
32
+ const secondsOf = (s) => { const r = cameraSeconds(s, firstNarrated); if (s.narration) firstNarrated = false; return r; };
31
33
  const mmss = (sec) => `${Math.floor(sec / 60)}:${String(Math.round(sec % 60)).padStart(2, "0")}`;
32
34
  // A row per step that says or shows something; silent plumbing (a bare goto,
33
35
  // an expect) rides inside the previous row's time.
@@ -39,7 +41,7 @@ for (const s of steps) {
39
41
  if (text && !(s.action === "expect" && !s.narration)) rows.push({ at: clock, text: text.replace(/\s+/g, " "), pointed: s.pointed === true, step: s });
40
42
  clock += dur;
41
43
  }
42
- const total = clock;
44
+ const total = clock + CLOSING_S;
43
45
  const title = (sb.title ?? sb.app ?? "Demo").toString().trim();
44
46
 
45
47
  if (mode === "words") {
@@ -61,7 +63,8 @@ if (mode === "await") {
61
63
  rows.forEach((r, i) => boxOut.push(`│ ${`${String(i + 1).padStart(2)} ${mmss(r.at)} ${cap2(r.text + (r.pointed ? " — pointed at, not pressed" : ""), W2 - 14)}`.padEnd(W2 - 2)} │`));
62
64
  boxOut.push(`│ ${"".padEnd(W2 - 2)} │`); boxOut.push(`└${"─".repeat(W2)}┘`);
63
65
  if (!listening) { console.log(boxOut.join("\n")); console.log("film"); process.exit(0); }
64
- await postEvent("storyboard", { box: boxOut.join("\n"), total: mmss(total), steps: rows.map((r, i) => ({ n: i + 1, at: mmss(r.at), text: r.text, pointed: r.pointed })) });
66
+ // The whole storyboard rides along (1.17.0): a retake starts from it, selectors and all.
67
+ await postEvent("storyboard", { box: boxOut.join("\n"), total: mmss(total), steps: rows.map((r, i) => ({ n: i + 1, at: mmss(r.at), text: r.text, pointed: r.pointed })), storyboard: sb });
65
68
  const started = Date.now();
66
69
  while (Date.now() - started < 30 * 60 * 1000) {
67
70
  const st = await poll();