demobite 1.7.5 → 1.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "demobite",
3
- "version": "1.7.5",
3
+ "version": "1.8.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"
@@ -773,6 +773,9 @@ try {
773
773
  t_start: t(),
774
774
  };
775
775
  process.stdout.write(`step ${rec.n} ${step.action} ${step.label ?? ""}\n`);
776
+ // The customer's line (founder 2026-10-14): one bar, step n of N. The
777
+ // agent relays this, never the step/action lines above.
778
+ { const N = STORYBOARD.steps.length, filled = Math.round((rec.n / N) * 20); process.stdout.write(`Filming ${"█".repeat(filled)}${"░".repeat(20 - filled)} step ${rec.n} of ${N}\n`); }
776
779
  if (step.action === "goto") {
777
780
  const navIssuedAt = t();
778
781
  // domcontentloaded, not load: a heavy site behind a proxy (the cloud
package/skill/SKILL.md CHANGED
@@ -11,6 +11,20 @@ All scripts live in `scripts/` beside this file. They are plain Node ESM. Requir
11
11
 
12
12
  Follow the phases in order. Never skip the storyboard approval: the human's yes on the storyboard, in the chat, is the word for the take. After that the take is delivered by itself. It becomes a bite in DemoBites without a second click, and the human watches it come in on their Demos grid (`<base>/demos`). Never send them to a preview page to approve it.
13
13
 
14
+ ## How you talk — the customer's side of every line
15
+
16
+ The plugin is a product. Every line you say is read by the person who asked for a demo, not by the people who built this recorder (founder 2026-10-14: "I have no clue what 'every control on the first row resolves' means"). These rules govern every phase below.
17
+
18
+ - **Say what happens next, first.** Your very first reply, within seconds of the request: the brief echoed in one line, then "Here is how this goes:" with the three steps (sign in once if the app needs it · the storyboard, about a minute · your yes, the film, the Demos grid), then the ONE question that unblocks the fastest path. No research before this reply.
19
+ - **The window within seconds.** The sign-in question comes before any reading of pages or code, and a "yes" opens the Chrome window at once (Phase 1a). The research runs after it, while the person signs in. Never make the person wait a minute and then ask them to sign in.
20
+ - **One waiting line, always.** Whenever you stop for the person, your last line starts with "⏳ Waiting for you —" and says exactly what for. Coming back from the kitchen, they read one line and know.
21
+ - **One question, numbered answers.** Never two questions in one message. Answers are 1 / 2 / 3 or yes / no; typing the number is enough.
22
+ - **Results, not work.** "✓ Signed in." "✓ Storyboard ready." "✓ Delivered." The checks, the dry run, the vocabulary pass, the selectors, the retries: done, never described. A problem is said in the person's words ("the Billing page needs a sign-in I don't have") — never in ours.
23
+ - **The storyboard is a box.** `node scripts/storyboard.mjs print` prints it; paste its output as it is. Never JSON on screen, never "beat 5 now says". Changes you made while checking the page are simply in the box.
24
+ - **Progress you can glance at.** While filming, relay record.mjs's `Filming ████░░ step n of N` line. While writing the storyboard, one dim line of what you are reading.
25
+ - **Plain words only.** Never on screen: dry run, control, selector, resolves, beat, surface, profile, headed, Playwright, vocab, obstacle, revert, JSON, storyboard.json, manifest, take. Use: step, demo, page, window, sign in, film, deliver, Demos grid.
26
+ - **Demo, never recording.** The thing the person gets is a demo; the act is filming.
27
+
14
28
  ## Phase 0: Auth gate, ALWAYS FIRST — announced, never silent
15
29
 
16
30
  This skill does not start unauthenticated — exactly like a CLI that requires
@@ -72,6 +86,25 @@ Write the answers to `.recorder/config.json` and never ask again:
72
86
 
73
87
  `login.mjs` later merges `api_key` and `workspace` into this same file and chmods it 600. Treat the file as secret once a key is in it. Never print `api_key`.
74
88
 
89
+ ## Phase 1a: The address and the sign-in — in the FIRST reply
90
+
91
+ Work out the starting address yourself, in seconds: the person's words if they named one, else `url` in `.recorder/config.json`, else the codebase (the app's dev server, the route the brief points at). Then:
92
+
93
+ ```bash
94
+ node scripts/signin.mjs --check <url> # "session <origin> since <date>" → no question, no window; "none" → ask
95
+ ```
96
+
97
+ - **A session is known for that address:** say nothing about signing in. Go on.
98
+ - **None:** the first reply carries the three steps and ONE question: "Does <url> need a sign-in? yes / no". On "yes", run at once, before any research:
99
+
100
+ ```bash
101
+ node scripts/signin.mjs <url>
102
+ ```
103
+
104
+ It opens the camera's own Chrome window at the address, prints the waiting line ("⏳ Waiting for you — sign in in the Chrome window that just opened…"), waits for the sign-in and remembers the address in `.recorder/signins.json`. Tomorrow, same address: no question, no window (founder 2026-10-14). A different address: one question, once. On "no": go on; a wall that appears anyway is Phase 2.
105
+
106
+ While the person signs in, read: the workspace rules (1b), the vocabulary (3a), the repository (3b). When the script prints "✓ Signed in.", say: "✓ Signed in. Writing the storyboard now — about a minute. Go make the coffee; I'll be here when you're back."
107
+
75
108
  ## Phase 1b: Workspace rules, EVERY run
76
109
 
77
110
  The workspace admin can write standing rules for the recorder in plain words, one per line, in the DemoBites settings tab "Agentic Recorder Rules" ("Mask any number with a dollar sign.", "Never open the Billing page.", "Say Update Center, never changelog."). They apply to every take filmed in that workspace. At the start of EVERY run, batch or free prompt, right after the key resolves the workspace and BEFORE any storyboard:
@@ -95,13 +128,26 @@ The camera browser uses a persistent profile at `.recorder/profile`. Signed in s
95
128
 
96
129
  When a page you need shows a login wall (login form, auth redirect, checkpoint page):
97
130
 
98
- 1. Open a HEADED Playwright window on that profile at the login page and tell the human: "Sign in in the window I opened. I will wait." Poll for a signed in signal (URL leaves the login path, or a session cookie appears), then close the window. The session now lives in the profile.
131
+ 1. Run `node scripts/signin.mjs <login url>` — THE ONE WAY to open that window (founder 2026-10-14). It launches the camera's own browser (the real Chrome, the recorder profile, the anti-automation flag), tells the human "Sign in in the window that opened. I will wait.", waits until the URL leaves the login page, and closes the window; the session now lives in the profile. NEVER launch a sign-in window of your own: a bare Playwright launch declares itself an automation browser (`navigator.webdriver`), and an app's bot check (Cloudflare Turnstile, reCAPTCHA…) refuses the sign-in — the take is lost there and the customer with it. If the script reports that the bot check refused even this window, say so plainly and stop; do not try other browsers.
99
132
  2. NEVER type, read, store, or ask for credentials. Not the password, not a 2FA code, nothing. The human signs in with their own hands.
100
133
  3. Do this only when a wall actually appears. Do not preemptively ask for logins.
101
134
 
102
135
  ## Phase 3: Storyboard, written BEFORE filming
103
136
 
104
- Write the storyboard as JSON before touching the camera.
137
+ Write the storyboard as JSON before touching the camera. The person never sees the JSON. When it is checked (Phase 4), present it like this and nothing else:
138
+
139
+ ```
140
+ ✓ Storyboard ready. <N> steps, about <m:ss> of demo. Nothing in it creates or changes data. ← or name what it creates and reverts, in plain words
141
+
142
+ <the output of: node scripts/storyboard.mjs print <takeDir>/storyboard.json>
143
+
144
+ How do you want to go?
145
+ 1 Film it as it is
146
+ 2 Change something first — tell me what
147
+ 3 Show me what each step says — the narration, word for word
148
+ ```
149
+
150
+ "1" is the approval (the storyboard is never skipped — founder law, kept 2026-10-14). "2": take the change, rewrite, present the box again, ask again. "3": paste `node scripts/storyboard.mjs words <takeDir>/storyboard.json`, then ask the same question again. A rule that could not be honoured, a brief's word the app does not use, a step pointed at and not pressed: said in one plain line under the box, never as a list of "beats".
105
151
 
106
152
  ### Phase 3a: Harvest the product's vocabulary FIRST
107
153
 
@@ -289,6 +335,8 @@ Before the real take, run the flow headless yourself (a throwaway script on the
289
335
 
290
336
  Only come back to the human when a PRODUCT question remains that you cannot decide, for example which of two similar buttons is the feature. When you do, bring annotated screenshot evidence: screenshot the state, mark the candidates, ask one crisp question. Never ask the human to debug selectors for you.
291
337
 
338
+ The dry run is never narrated (How you talk): the person reads "✓ Storyboard ready." and the box, not what you checked.
339
+
292
340
  ## Phase 5: The take
293
341
 
294
342
  ```bash
@@ -312,8 +360,12 @@ Filming laws baked into `record.mjs`, do not reimplement or weaken them:
312
360
 
313
361
  If the take fails mid flow, the partial video and manifest are still saved. Diagnose, fix the storyboard, film again.
314
362
 
363
+ On the person's "1": say "Filming. About <m:ss>; I'll tell you when it lands." and relay record.mjs's `Filming ████░░ step n of N` lines as they come — nothing else from its output reaches the person.
364
+
315
365
  ## Phase 6: Deliver into DemoBites
316
366
 
367
+ When `upload.mjs` prints its last line, say: "✓ Delivered. \"<title>\" (<m:ss>) is landing in your Demos grid now: <base>/demos — watch it come in there. You can change any sentence, the voice and the zooms in the editor." When minutes ran out, relay upload.mjs's "Kept…" line as it is.
368
+
317
369
 
318
370
  Send the TRIMMED CLEAN take into DemoBites. The studio owns the look: NO backdrop, NO rounded corners, NO shadow on the uploaded file. Everything (voice, zooms, look, intro, outro) becomes editable there.
319
371
 
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ // Phase 2: the target app's sign-in, by the human's own hands, in the
3
+ // recorder's own browser window (founder 2026-10-14: a sign-in window the
4
+ // agent improvised launched Chrome WITHOUT the camera's anti-automation flag,
5
+ // and the app's bot check — Cloudflare Turnstile on app.demobites.com —
6
+ // refused the sign-in: "Verification failed". Any customer whose app checks
7
+ // for bots at the login would lose the take there, and we would never see
8
+ // them again). This script is the ONE way to open that window: the same
9
+ // launch as record.mjs — the real Google Chrome binary, the persistent
10
+ // recorder profile, `--disable-blink-features=AutomationControlled` — so the
11
+ // page sees the genuine product, not a bare automation browser.
12
+ //
13
+ // node scripts/signin.mjs <login url> [--timeout <ms>] [--probe]
14
+ // node scripts/signin.mjs --check <url> "session <origin> since <date>" (exit 0) or "none" (exit 3)
15
+ //
16
+ // The ledger `.recorder/signins.json` remembers every origin signed into on
17
+ // this profile (founder 2026-10-14: the sign-in question is asked ONCE per
18
+ // address — tomorrow, same address, no question, no window). `--check` reads
19
+ // it; a successful sign-in writes it.
20
+ //
21
+ // It opens the window, tells the human to sign in, and waits until the URL
22
+ // leaves the login page (or a session cookie appears), then closes the
23
+ // window: the session now lives in `.recorder/profile` and the camera reuses
24
+ // it. Never types, reads, stores or asks for credentials. `--probe` only
25
+ // loads the page, reports whether the bot check passed, and exits — the
26
+ // recorder's own health check, no human needed.
27
+ import fs from "node:fs";
28
+ import path from "node:path";
29
+
30
+ const argv = process.argv.slice(2);
31
+ const ledgerPath = path.resolve(process.cwd(), ".recorder", "signins.json");
32
+ const readLedger = () => { try { return JSON.parse(fs.readFileSync(ledgerPath, "utf8")); } catch { return { origins: {} }; } };
33
+ if (argv[0] === "--check") {
34
+ let origin; try { origin = new URL(argv[1]).origin; } catch { console.error("Usage: node scripts/signin.mjs --check <url>"); process.exit(2); }
35
+ const since = readLedger().origins?.[origin];
36
+ if (since) { console.log(`session ${origin} since ${since}`); process.exit(0); }
37
+ console.log("none"); process.exit(3);
38
+ }
39
+ const { chromium } = await import("playwright"); // after --check, which needs no browser
40
+ const url = argv.find((a) => !a.startsWith("--"));
41
+ if (!url) { console.error("Usage: node scripts/signin.mjs <login url> [--timeout <ms>] [--probe]"); process.exit(2); }
42
+ const probe = argv.includes("--probe");
43
+ const timeoutMs = Number(argv[argv.indexOf("--timeout") + 1]) || 10 * 60 * 1000;
44
+ const profileDir = process.env.RECORDER_PROFILE || path.resolve(process.cwd(), ".recorder", "profile");
45
+
46
+ // The camera's launch (record.mjs), minus the video.
47
+ const launchOpts = {
48
+ headless: false,
49
+ viewport: { width: 1440, height: 900 },
50
+ deviceScaleFactor: 1,
51
+ args: ["--force-color-profile=srgb", "--disable-blink-features=AutomationControlled"],
52
+ ignoreDefaultArgs: ["--enable-automation"],
53
+ };
54
+ let ctx;
55
+ try {
56
+ ctx = await chromium.launchPersistentContext(profileDir, { ...launchOpts, channel: "chrome" });
57
+ } catch (e) {
58
+ console.error(`Real Chrome failed to launch (${String(e.message).split("\n")[0]}); falling back to bundled Chromium.`);
59
+ ctx = await chromium.launchPersistentContext(profileDir, launchOpts);
60
+ }
61
+ const page = ctx.pages()[0] ?? (await ctx.newPage());
62
+ await page.goto(url, { waitUntil: "domcontentloaded" });
63
+ const startPath = new URL(page.url()).pathname;
64
+
65
+ const botCheck = async () => {
66
+ // Cloudflare Turnstile leaves a token in this input when it passes; the
67
+ // page shows "Verification failed" when it does not. Other walls: unknown.
68
+ try {
69
+ return await page.evaluate(() => {
70
+ const input = document.querySelector('input[name="cf-turnstile-response"]');
71
+ const text = document.body.innerText || "";
72
+ if (input && input.value) return "passed";
73
+ if (/verification failed/i.test(text)) return "failed";
74
+ return input ? "pending" : "none";
75
+ });
76
+ } catch { return "unknown"; }
77
+ };
78
+
79
+ if (probe) {
80
+ let state = "pending";
81
+ for (let i = 0; i < 20 && (state === "pending" || state === "unknown"); i++) { await page.waitForTimeout(1000); state = await botCheck(); }
82
+ const webdriver = await page.evaluate(() => navigator.webdriver).catch(() => "?");
83
+ console.log(`probe: bot check ${state} · navigator.webdriver=${webdriver} · ${page.url()}`);
84
+ await ctx.close();
85
+ process.exit(state === "failed" ? 1 : 0);
86
+ }
87
+
88
+ console.log("⏳ Waiting for you — sign in in the Chrome window that just opened. Your hands only; nothing you type is seen.");
89
+ const started = Date.now();
90
+ let lastCheck = "none";
91
+ while (Date.now() - started < timeoutMs) {
92
+ await page.waitForTimeout(1000);
93
+ let here;
94
+ try { here = new URL(page.url()); } catch { continue; }
95
+ if (here.pathname !== startPath && !/sign-?in|log-?in|auth|sso|oauth|callback/i.test(here.pathname)) {
96
+ console.log(`✓ Signed in. The session is saved for ${here.origin}; next time there is no question and no window.`);
97
+ try { const l = readLedger(); l.origins = { ...(l.origins ?? {}), [here.origin]: new Date().toISOString() }; fs.mkdirSync(path.dirname(ledgerPath), { recursive: true }); fs.writeFileSync(ledgerPath, JSON.stringify(l, null, 2) + "\n"); } catch { /* the session still lives in the profile */ }
98
+ await ctx.close();
99
+ process.exit(0);
100
+ }
101
+ const state = await botCheck();
102
+ if (state === "failed" && lastCheck !== "failed") {
103
+ console.error("The page's bot check refused this window (\"Verification failed\"). Reload the page in this window once; if it fails again, this app cannot be signed into from an automated browser — tell the human plainly and stop.");
104
+ }
105
+ lastCheck = state;
106
+ }
107
+ console.error("No sign-in happened within the wait. Closing the window; the take did not start.");
108
+ await ctx.close();
109
+ process.exit(1);
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ // The storyboard as the customer reads it (founder 2026-10-14: "I would have
3
+ // expected: all the flow verified, this is the storyboard: 1, 2, 3… with some
4
+ // ASCII love"). ONE frame, printed by this script, identical in every take —
5
+ // never improvised by the agent, never JSON on screen.
6
+ //
7
+ // node scripts/storyboard.mjs print <storyboard.json> the box: number, time, one line per step, the total
8
+ // node scripts/storyboard.mjs words <storyboard.json> every step's narration, word for word (option 3)
9
+ //
10
+ // Times are estimates from the steps themselves: a step lasts its dwell, or
11
+ // as long as its narration takes to say (about 2.5 words a second), with a
12
+ // floor per action. The real cut is fitted by DemoBites afterwards.
13
+ import fs from "node:fs";
14
+
15
+ const [mode, file] = process.argv.slice(2);
16
+ if (!mode || !file || !["print", "words"].includes(mode)) {
17
+ console.error("Usage: node scripts/storyboard.mjs print|words <storyboard.json>");
18
+ process.exit(2);
19
+ }
20
+ const sb = JSON.parse(fs.readFileSync(file, "utf8"));
21
+ const steps = Array.isArray(sb.steps) ? sb.steps : [];
22
+ const FLOOR = { goto: 1.5, settle: 2, click: 2.5, hover: 2, type: 3, scroll: 2, expect: 1 };
23
+ const secondsOf = (s) => {
24
+ const words = String(s.narration ?? "").trim().split(/\s+/).filter(Boolean).length;
25
+ return Math.max(typeof s.dwell === "number" ? s.dwell : 0, words / 2.5, FLOOR[s.action] ?? 2);
26
+ };
27
+ const mmss = (sec) => `${Math.floor(sec / 60)}:${String(Math.round(sec % 60)).padStart(2, "0")}`;
28
+ // A row per step that says or shows something; silent plumbing (a bare goto,
29
+ // an expect) rides inside the previous row's time.
30
+ const rows = [];
31
+ let clock = 0;
32
+ for (const s of steps) {
33
+ const text = (s.narration ?? s.label ?? s.on_screen ?? "").toString().trim();
34
+ const dur = secondsOf(s);
35
+ if (text && !(s.action === "expect" && !s.narration)) rows.push({ at: clock, text: text.replace(/\s+/g, " "), pointed: s.pointed === true, step: s });
36
+ clock += dur;
37
+ }
38
+ const total = clock;
39
+ const title = (sb.title ?? sb.app ?? "Demo").toString().trim();
40
+
41
+ if (mode === "words") {
42
+ console.log(`${title} — what each step says\n`);
43
+ rows.forEach((r, i) => console.log(`${String(i + 1).padStart(2)} ${mmss(r.at)} ${r.step.narration ? `"${r.step.narration.trim()}"` : "(no narration — the viewer watches)"}${r.step.on_screen ? `\n on screen: ${r.step.on_screen}` : ""}`));
44
+ console.log(`\nAbout ${mmss(total)} in all. DemoBites fits the words to the picture afterwards; you can change any sentence in the editor.`);
45
+ process.exit(0);
46
+ }
47
+
48
+ const W = 76;
49
+ const cap = (t, n) => (t.length > n ? t.slice(0, n - 1) + "…" : t);
50
+ const head = `─ STORYBOARD · ${cap(title, 40)} `;
51
+ const tail = ` ${mmss(total)} ─`;
52
+ const line = (s) => `│ ${s.padEnd(W - 2)} │`;
53
+ const out = [];
54
+ out.push(`┌${head}${"─".repeat(Math.max(1, W - head.length - tail.length))}${tail}┐`);
55
+ out.push(line(""));
56
+ rows.forEach((r, i) => out.push(line(`${String(i + 1).padStart(2)} ${mmss(r.at)} ${cap(r.text + (r.pointed ? " — pointed at, not pressed" : ""), W - 14)}`)));
57
+ out.push(line(""));
58
+ out.push(`└${"─".repeat(W)}┘`);
59
+ console.log(out.join("\n"));