demobite 1.0.9 → 1.2.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 CHANGED
@@ -1,115 +1,161 @@
1
- # demobite
1
+ # DemoBites Agentic Recorder
2
2
 
3
- **You prompt, it records.** The agentic recorder films a real browser from a
4
- storyboard — real cursor physics, a measured clock, cinematic camera moves —
5
- and delivers the take into [DemoBites](https://demobites.com), where it becomes
6
- a fully editable demo: AI narration, zooms, cursor rendering, intro and outro,
7
- localization, all tweakable in the studio.
3
+ **Ask your coding agent to record your product. Edit the result. Download the MP4.**
4
+
5
+ Your agent drives a real browser on your machine and records the flow you describe. Connect a free DemoBites account to turn the take into an editable video, with text-to-speech, zooms, captions, and MP4 export included.
6
+
7
+ **Sign in to your app locally. You do not hand DemoBites your app password or upload your recording browser profile.**
8
8
 
9
9
  ```bash
10
- npx demobite
10
+ npx demobite@latest
11
11
  ```
12
12
 
13
- That one command checks your setup, installs the recorder skill for
14
- your coding agent — [Claude Code](https://claude.com/claude-code), Cursor,
15
- or Codex — and wires the DemoBites
16
- management MCP so your agent can also run your Update Center and Demo Center.
17
- Sign-in happens through your own browser — no passwords in the terminal, ever.
18
- Then you just ask your agent:
13
+ **Free Studio · Text-to-speech included · Unlimited exports · No watermark · No credit card**
14
+
15
+ [Website](https://agentic-recorder.com/) · [npm](https://www.npmjs.com/package/demobite) · [Report an issue](https://github.com/demobites/agentic-recorder/issues)
16
+
17
+ ## See the workflow
18
+
19
+ https://github.com/user-attachments/assets/18293b85-82e2-475f-8d94-543f1f9bbc1d
19
20
 
20
- > "Record a demo of how search works on our app, and upload it to DemoBites."
21
+ An illustrated walkthrough of the flow, with waiting time compressed.
21
22
 
22
- > "Create a release with my latest bites and add Spanish."
23
+ ## Start with your own agent
23
24
 
24
- Commands: `npx demobite` (full setup) · `npx demobite login` · `npx demobite mcp`
25
- (register the management MCP with your agent) · `npx demobite logout`.
26
- Anything public — publishing a release, taking a language offline — always
27
- shows you a preview to approve first. Managing by agent requires the Grow plan;
28
- recording works on every plan.
25
+ You need a coding agent that can read skill instructions, run terminal commands, and control a local browser. A browser-only chat session cannot run this local workflow.
29
26
 
30
- The agent storyboards the flow, films it in a real Chrome, and stages the take
31
- for your approval inside DemoBites. You approve in the product; the platform
32
- does the rest.
27
+ Before installing:
33
28
 
34
- ## Manage DemoBites from your agent (MCP)
29
+ - **Node.js 18 or newer** and npm.
30
+ - **Google Chrome**, recommended. The recorder also supports Playwright's Chromium.
31
+ - A **free DemoBites account** for the Studio workflow below.
35
32
 
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.
33
+ Run these commands from your project directory:
39
34
 
40
35
  ```bash
41
- npx demobite mcp
36
+ # Install the skill and check your setup.
37
+ npx demobite@latest
38
+
39
+ # Connect this project to your DemoBites workspace.
40
+ npx demobite login
42
41
  ```
43
42
 
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:
43
+ The installer installs Playwright, ffmpeg and ffprobe beside the skill if they are not already there. It uses your own ffmpeg when a compatible build is on your PATH. It checks for Chrome and downloads Chromium on the first take if Chrome is missing.
44
+
45
+ To connect, open the link printed in your terminal and approve the connection in your browser. If you are signed out of DemoBites, sign in there first. You do not paste a password into the terminal.
46
+
47
+ Then ask your agent:
48
+
49
+ > Record a demo of inviting a teammate in my app at http://localhost:3000. Open Team settings, invite alex@example.com as an Admin, and show the confirmation. Show me the storyboard before recording.
50
+
51
+ Use your own app URL and a demo account. The agent performs real actions in your app, including sending an invitation if that is part of the approved flow.
52
+
53
+ ### Which agent?
54
+
55
+ The installer currently places the skill at `~/.claude/skills/agentic-recorder/`, the Claude Code skill directory.
56
+
57
+ For Cursor or Codex, make the [recorder instructions](skill/SKILL.md) available to your agent and ensure it can run the installed scripts. The launcher currently does not install into separate Cursor or Codex skill directories. Connecting an MCP client alone does not install the recorder skill.
58
+
59
+ ## From prompt to finished video
60
+
61
+ 1. **Describe the flow.** Give your agent the app URL and what you want to demonstrate.
62
+ 2. **Approve the storyboard.** Review the steps and estimated length before filming. If your app needs a login, sign in yourself in the recorder's local browser window.
63
+ 3. **Let it record.** The agent checks the flow and films it. Recording normally runs in the background.
64
+ 4. **Review the raw take.** Open the review link printed in the terminal. Check the footage, then approve it to create a Bite, an editable video project.
65
+ 5. **Tweak and download.** In the Studio, change a sentence, generate its voice, adjust the zooms or timing, and export your MP4.
66
+
67
+ The storyboard's narration is a starting point. DemoBites rewrites and fits it to the footage; you can edit the final wording in the Studio.
68
+
69
+ ## What connecting to DemoBites adds
70
+
71
+ The recorder supplies footage, action timings, cursor movements, and camera targets. The Studio uses that information to create a video you can keep editing.
72
+
73
+ | Included in the free Studio | What you can do |
74
+ | --- | --- |
75
+ | Script and text-to-speech | Edit the words and generate narration without recording your own voice. |
76
+ | Camera and cursor editing | Adjust automatic zooms, cursor appearance, and timing. |
77
+ | Video finishing | Trim the take, edit captions, and add your background, branding, intro, and outro. |
78
+ | MP4 export | Download Full HD video with narration and no DemoBites watermark. |
48
79
 
49
- > "Create a release with my latest bites, add Spanish, and publish it."
80
+ **The free Studio holds up to 8 Bites at a time, with unlimited exports. Invite three people who sign up and the Studio becomes unlimited, still free.** It has no trial clock and requires no credit card. You bring your own coding agent; its subscription or model usage is separate. An ElevenLabs key is optional, not required for the Studio's included text-to-speech.
50
81
 
51
- > "What's on our Demo Center? Swap the checkout bite for the onboarding one."
82
+ You can use the exported video in your docs, website, release notes, or wherever you need it. Paid publishing and management features are optional. See [current plan details](https://www.demobites.com/pricing).
52
83
 
53
- ### What it can do — 19 tools
84
+ ## One prompt, one result
54
85
 
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` |
86
+ > Record a demo of Funnels: where customers drop off, which channels work. Show the sidebar and graphs.
61
87
 
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
88
 
65
- ### You stay in charge
66
89
 
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.
90
+ https://github.com/user-attachments/assets/02eb7d3e-fe74-4c0d-b532-e16bfc3fa614
73
91
 
74
- ### Plans
75
92
 
76
- Connecting and reading are open. Managing requires the **Grow** plan.
77
- Recording works on every plan.
93
+
94
+ Recorded by the agent, then edited and narrated in the free Studio. This prompt assumes the agent already knows the app URL and can access the Funnels page.
78
95
 
79
- Full guide: [Manage from your agent](https://www.demobites.com/docs/bites/manage-from-your-agent)
96
+ ## Your app login stays local
80
97
 
81
- ## What's in this repository
98
+ **You do not need to hand DemoBites your app password to record a signed-in flow.** Sign in yourself in the recorder's browser on your machine.
82
99
 
83
- | Directory | What it is |
84
- |---|---|
85
- | `launcher/` | The `npx demobite` entry — environment checks, skill install, login |
86
- | `skill/` | The DemoBites recorder skill for coding agents (staging, preview, approval flow) |
87
- | `recorder/` | The open recorder — same filming engine, no account, ends at a polished `demo.mp4` |
88
- | `scripts/` | The shared engine: filming, clock calibration, cutting |
100
+ The recorder uses a separate, persistent Chrome profile at `.recorder/profile/`. Your app session is saved there for later takes. It does not automatically inherit the sessions in your everyday Chrome profile.
89
101
 
90
- ## Just want the recorder, no DemoBites?
102
+ | Data | Where it goes |
103
+ | --- | --- |
104
+ | Your app's browser session | Stays in the local recording profile; the standard local workflow does not upload that profile to DemoBites. |
105
+ | DemoBites recorder key | Saved in `.recorder/config.json` and used to authenticate recorder requests to DemoBites. This is separate from your app login. |
106
+ | Footage and recording metadata | Uploaded to DemoBites for review and processing, including the storyboard, narration intent, cursor events, and camera targets. |
91
107
 
92
- The `recorder/` directory is a standalone skill: the same real-browser filming,
93
- hover-anchor clock calibration and camera work, delivering a finished, styled
94
- `demo.mp4` on your disk — no account, no upload. Point your coding agent at it and
95
- film. When you want narration, zooms, an editable timeline and hosting, the
96
- sibling skill in `skill/` is one login away.
108
+ Recording happens locally. Studio processing and editing use DemoBites online. Anything visible or typed into the recorded flow can appear in the footage or recording metadata, so use suitable demo data. Your coding agent's own data handling still follows its provider and your settings.
97
109
 
98
- ## How updates reach you
110
+ Keep `.recorder/` and take directories out of version control. To disconnect the recorder, run `npx demobite logout`.
99
111
 
100
- Run with `@latest` and every invocation resolves the newest published version —
101
- the skill you install always matches the DemoBites platform it talks to.
102
- Releases are published from GitHub Actions with npm provenance: every version
103
- is cryptographically tied to a public commit in this repository.
112
+ ## Local output without a DemoBites account
104
113
 
105
- ## Requirements
114
+ The repository also contains a [standalone recorder skill](recorder/SKILL.md) and local finishing scripts. These produce a local MP4; optional voiceover uses your own ElevenLabs key.
106
115
 
107
- - Node 18+
108
- - A coding agent — [Claude Code](https://claude.com/claude-code), Cursor, or Codex; the recorder is agent-driven
109
- - Google Chrome (recommended; films with the real browser) — otherwise
110
- Chromium is downloaded on first take
111
- - ffmpeg (`brew install ffmpeg` on macOS)
116
+ This is an advanced, manual workflow. The default `npx demobite@latest` command installs the Studio-connected skill, not the standalone skill. The standalone finishing reference contains legacy instructions; use the standalone skill as the entry point rather than treating it as a second ending of the installed Studio skill.
117
+
118
+ ## A few boundaries
119
+
120
+ - The agentic recorder films **browser workflows**. It does not record native desktop apps or phone screens.
121
+ - The Studio-connected skill is designed for short demos, usually **30–45 seconds**, with a **90-second maximum per Bite**. Split longer stories into separate videos.
122
+ - Some sites block automated browsers. The workflow may need a human sign-in or verification step, and some sites may refuse recording.
123
+ - Filming does not publish anything. You review the take before creating the Bite; public publishing is a separate action.
124
+
125
+ ## Optional: Retake and management
126
+
127
+ When your UI changes, **Retake** can refilm an existing Bite from its saved recipe. It is a paid capability; see [plan details](https://www.demobites.com/pricing).
128
+
129
+ ```bash
130
+ npx demobite retake <biteId> --note "Export moved to the header"
131
+ ```
132
+
133
+ The package also includes a DemoBites management MCP for releases and centers:
134
+
135
+ ```bash
136
+ npx demobite mcp
137
+ ```
138
+
139
+ The default installer also attempts to register that MCP with Claude Code when the current project is already connected. Management access depends on your account and key permissions. You do not need paid management to record, edit, or export a free Bite.
140
+
141
+ [Management documentation](https://www.demobites.com/docs/bites/manage-from-your-agent)
142
+
143
+ ## Repository
144
+
145
+ | Directory | Purpose |
146
+ | --- | --- |
147
+ | `launcher/` | CLI setup, connection commands, and MCP registration. |
148
+ | `skill/` | Studio-connected recorder instructions, login, upload, and Retake. |
149
+ | `scripts/` | Shared browser recording, trimming, and timing calibration. |
150
+ | `recorder/` | Standalone skill and local video finishing tools. |
151
+ | `aliases/` | `agentic-recorder` and `demobites` aliases for the same launcher. |
152
+
153
+ Run `npx demobite@latest` again to update the installed skill. For publishing instructions, see [RELEASING.md](RELEASING.md).
154
+
155
+ Found something confusing or have an example to share? [Open an issue](https://github.com/demobites/agentic-recorder/issues). Include your operating system, agent, and recorder version. Remove keys, cookies, and private app data from logs or recordings before attaching them.
112
156
 
113
157
  ## License
114
158
 
115
- MIT — see [LICENSE](./LICENSE).
159
+ The code in this repository is [MIT licensed](LICENSE). The hosted DemoBites Studio is a separate service; its source is not included here.
160
+
161
+ Built and maintained by [DemoBites](https://www.demobites.com/).
@@ -45,10 +45,9 @@ const chromePaths = [
45
45
  "C:/Program Files/Google/Chrome/Application/chrome.exe",
46
46
  "/usr/bin/google-chrome",
47
47
  ];
48
- if (chromePaths.some((p) => fs.existsSync(p)) || hasBin("google-chrome")) ok("Google Chrome (films with the real browser)");
49
- else warn("Google Chrome not found — the recorder will download Chromium on first take");
50
- if (hasBin("ffmpeg")) ok("ffmpeg");
51
- else warn("ffmpeg not found — install it (macOS: brew install ffmpeg) before recording");
48
+ const haveChrome = chromePaths.some((p) => fs.existsSync(p)) || hasBin("google-chrome") || hasBin("google-chrome-stable");
49
+ if (haveChrome) ok("Google Chrome (films with the real browser)");
50
+ else console.log(" · Google Chrome not found — installing Playwright's Chromium for the recorder");
52
51
  if (hasBin("claude")) ok("Claude Code (drives the recorder; MCP registers automatically)");
53
52
  else warn("Claude Code not found — using Cursor or Codex? They drive the recorder too; MCP setup prints below");
54
53
 
@@ -63,12 +62,41 @@ for (const f of fs.readdirSync(path.join(pkgRoot, "skill/scripts"))) copy(`skill
63
62
  for (const f of fs.readdirSync(path.join(pkgRoot, "scripts"))) copy(`scripts/${f}`, `scripts/${f}`);
64
63
  ok(`Skill installed → ${dest}`);
65
64
 
66
- // Playwright lives with the skill so takes can film.
67
- if (!fs.existsSync(path.join(dest, "node_modules", "playwright"))) {
68
- console.log("\n Installing Playwright (one-time)…");
69
- const r = spawnSync("npm", ["install", "--prefix", dest, "--silent", "playwright"], { stdio: "inherit" });
65
+ // Playwright and the media tools live with the skill so takes can film and
66
+ // finish on any machine. ffmpeg and ffprobe are packaged per platform
67
+ // (@ffmpeg-installer, @ffprobe-installer); a compatible system build is preferred when present
68
+ // (scripts/media-tools.mjs decides). One line installs everything.
69
+ const needed = ["playwright", "@ffmpeg-installer/ffmpeg", "@ffprobe-installer/ffprobe"].filter(
70
+ (m) => !fs.existsSync(path.join(dest, "node_modules", m)),
71
+ );
72
+ if (needed.length) {
73
+ console.log(`\n Installing ${needed.join(", ")} (one-time)…`);
74
+ const r = spawnSync("npm", ["install", "--prefix", dest, "--silent", "--no-audit", "--no-fund", ...needed], { stdio: "inherit" });
70
75
  if (r.status === 0) ok("Playwright ready");
71
- else warn("Playwright install failed — run: npm install --prefix ~/.claude/skills/agentic-recorder playwright");
76
+ else warn(`Install failed — run: npm install --prefix ~/.claude/skills/agentic-recorder ${needed.join(" ")}`);
77
+ }
78
+ // No Chrome on this machine: fetch Playwright's Chromium once so the first
79
+ // take has a browser. Linux needs the shared libraries too (--with-deps).
80
+ if (!haveChrome) {
81
+ const pw = path.join(dest, "node_modules", "playwright", "cli.js");
82
+ const marker = path.join(dest, ".chromium-ready");
83
+ if (!fs.existsSync(marker) && fs.existsSync(pw)) {
84
+ console.log(" Installing Chromium (one-time)…");
85
+ const args = [pw, "install", "chromium", ...(process.platform === "linux" ? ["--with-deps"] : [])];
86
+ const r = spawnSync("node", args, { stdio: "inherit", cwd: dest });
87
+ if (r.status === 0) { fs.writeFileSync(marker, new Date().toISOString()); ok("Chromium ready"); }
88
+ else warn(`Chromium install failed — run: node ${pw} install chromium${process.platform === "linux" ? " --with-deps" : ""}`);
89
+ } else if (fs.existsSync(marker)) ok("Chromium ready");
90
+ }
91
+ {
92
+ // Verify both tools actually run (compatibility, not presence).
93
+ const r = spawnSync("node", [path.join(dest, "scripts", "media-tools.mjs")], { encoding: "utf8" });
94
+ const rows = (r.stdout || "").trim().split("\n").filter(Boolean).map((l) => l.split("\t"));
95
+ for (const [tool, source, info] of rows) {
96
+ if (source === "missing") warn(`${tool}: no working build (${info})`);
97
+ else ok(`${tool} ready (${source === "packaged" ? "packaged with the recorder" : source === "system" ? "your system build" : "from .recorder/config.json"})`);
98
+ }
99
+ if (r.status !== 0) warn("Recording will fail until ffmpeg and ffprobe both work. Re-run this command, or install them on your system.");
72
100
  }
73
101
 
74
102
  // ── 3. Subcommands ─────────────────────────────────────────────────────────
@@ -128,6 +156,21 @@ if (arg === "retake") {
128
156
  process.exit(r.status ?? 1);
129
157
  }
130
158
 
159
+ // Batch of briefs (2026-09-13): `npx demobite briefs list <batchId>` etc. and
160
+ // `npx demobite status <takeDir|stagingId>` hand straight to the skill scripts.
161
+ if (arg === "briefs" || arg === "status") {
162
+ let cfg = readCfg();
163
+ if (!cfg?.api_key) {
164
+ console.log("\n Not connected yet — linking this machine to DemoBites first…\n");
165
+ const r = spawnSync("node", [path.join(dest, "scripts", "login.mjs")], { stdio: "inherit", cwd: process.cwd() });
166
+ if (r.status !== 0) process.exit(r.status ?? 1);
167
+ cfg = readCfg();
168
+ }
169
+ if (!cfg?.api_key) { warn("Login did not complete — run: npx demobite login"); process.exit(1); }
170
+ const r = spawnSync("node", [path.join(dest, "scripts", `${arg}.mjs`), ...process.argv.slice(3)], { stdio: "inherit", cwd: process.cwd() });
171
+ process.exit(r.status ?? 1);
172
+ }
173
+
131
174
  if (arg === "mcp") {
132
175
  let cfg = readCfg();
133
176
  if (!cfg?.api_key) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "demobite",
3
- "version": "1.0.9",
4
- "description": "The DemoBites agentic recorder — you prompt, it films a real browser, and DemoBites turns the take into an editable demo bite.",
3
+ "version": "1.2.0",
4
+ "description": "The DemoBites agentic recorder \u2014 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
  },
@@ -34,5 +34,9 @@
34
34
  "url": "https://github.com/demobites/agentic-recorder/issues"
35
35
  },
36
36
  "license": "MIT",
37
- "type": "module"
37
+ "type": "module",
38
+ "scripts": {
39
+ "check-aliases": "node scripts/check-aliases.mjs",
40
+ "prepublishOnly": "node scripts/check-aliases.mjs"
41
+ }
38
42
  }
@@ -22,15 +22,8 @@ if (!dir) {
22
22
  console.error("Usage: node mux.mjs <takeDir>");
23
23
  process.exit(2);
24
24
  }
25
- const requireTool = (tool) => {
26
- try { execFileSync(tool, ["-version"], { stdio: "ignore" }); }
27
- catch {
28
- console.error(`${tool} is required on PATH. Install it (macOS: brew install ffmpeg) and rerun.`);
29
- process.exit(1);
30
- }
31
- };
32
- requireTool("ffmpeg");
33
- requireTool("ffprobe");
25
+ import { ffmpeg as FFMPEG, ffprobe as FFPROBE, requireMediaTools } from "../../scripts/media-tools.mjs";
26
+ requireMediaTools(["ffmpeg","ffprobe"]);
34
27
 
35
28
  const demoPath = path.join(dir, "demo.mp4");
36
29
  const manPath = path.join(dir, "manifest.json");
@@ -66,7 +59,7 @@ const delays = tts
66
59
  const mix = tts.map((_, i) => `[a${i}]`).join("") + `amix=inputs=${tts.length}:normalize=0[aout]`;
67
60
 
68
61
  const outPath = path.join(dir, "demo-voiced.mp4");
69
- execFileSync("ffmpeg", [
62
+ execFileSync(FFMPEG(), [
70
63
  "-y", "-loglevel", "error",
71
64
  "-i", demoPath,
72
65
  ...inputArgs,
@@ -75,7 +68,7 @@ execFileSync("ffmpeg", [
75
68
  "-c:v", "copy", "-c:a", "aac", "-b:a", "160k",
76
69
  outPath,
77
70
  ], { stdio: "inherit" });
78
- const dur = execFileSync("ffprobe", [
71
+ const dur = execFileSync(FFPROBE(), [
79
72
  "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", outPath,
80
73
  ]).toString().trim();
81
74
  console.log(`demo-voiced.mp4 ready, ${parseFloat(dur).toFixed(2)}s`);
@@ -10,7 +10,10 @@
10
10
  #
11
11
  # Usage: post.sh <takeDir> [backdrop-hex-no-#] (default backdrop 0f1420)
12
12
  set -euo pipefail
13
- command -v ffmpeg >/dev/null 2>&1 || { echo "ffmpeg is required on PATH. Install it (macOS: brew install ffmpeg) and rerun." >&2; exit 1; }
13
+ HERE="$(cd "$(dirname "$0")" && pwd)"
14
+ FFMPEG="$(node "$HERE/../../scripts/media-tools.mjs" 2>/dev/null | awk -F"\t" '$1=="ffmpeg"{print $3}')"
15
+ FFPROBE="$(node "$HERE/../../scripts/media-tools.mjs" 2>/dev/null | awk -F"\t" '$1=="ffprobe"{print $3}')"
16
+ [ -n "$FFMPEG" ] && [ -n "$FFPROBE" ] || { echo "ffmpeg/ffprobe: no working build found. Run npx demobite@latest again, or install them on your system." >&2; exit 1; }
14
17
  command -v node >/dev/null 2>&1 || { echo "node is required on PATH." >&2; exit 1; }
15
18
  D=${1:?Usage: post.sh <takeDir> [backdrop-hex-no-#]}
16
19
  # Backdrop resolution mirrors frame.mjs exactly: CLI arg, then the remembered
@@ -20,7 +23,7 @@ BG=${2:-$(node -p "try{(JSON.parse(require('fs').readFileSync('.recorder/config.
20
23
  [ -f "$D/raw.webm" ] || { echo "$D/raw.webm not found. Run record.mjs first." >&2; exit 1; }
21
24
  [ -f "$D/shadow.png" ] && [ -f "$D/mask.png" ] || { echo "Overlays missing. Run: node frame.mjs $D" >&2; exit 1; }
22
25
  TRIM=$(node -p "JSON.parse(require('fs').readFileSync('$D/manifest.json','utf8')).record_from||0")
23
- ffmpeg -y -loglevel error -i "$D/raw.webm" -i "$D/shadow.png" -i "$D/mask.png" -filter_complex \
26
+ "$FFMPEG" -y -loglevel error -i "$D/raw.webm" -i "$D/shadow.png" -i "$D/mask.png" -filter_complex \
24
27
  "color=c=0x${BG}:s=1920x1080:r=30[bg];[0:v]trim=start=${TRIM},setpts=PTS-STARTPTS,scale=1728:972:flags=lanczos[v];[2:v]format=gray[m];[v][m]alphamerge[va];[bg][1:v]overlay=0:0[b1];[b1][va]overlay=96:54:shortest=1,fps=30,format=yuv420p[out]" \
25
28
  -map "[out]" -c:v libx264 -preset medium -crf 19 -movflags +faststart "$D/demo.mp4"
26
- echo "demo.mp4 duration: $(ffprobe -v error -show_entries format=duration -of csv=p=0 "$D/demo.mp4")s"
29
+ echo "demo.mp4 duration: $("$FFPROBE" -v error -show_entries format=duration -of csv=p=0 "$D/demo.mp4")s"
@@ -20,14 +20,8 @@ if (!dir) {
20
20
  console.error("Usage: node tts.mjs <takeDir>");
21
21
  process.exit(2);
22
22
  }
23
- const requireTool = (tool) => {
24
- try { execFileSync(tool, ["-version"], { stdio: "ignore" }); }
25
- catch {
26
- console.error(`${tool} is required on PATH. Install it (macOS: brew install ffmpeg) and rerun.`);
27
- process.exit(1);
28
- }
29
- };
30
- requireTool("ffprobe");
23
+ import { ffmpeg as FFMPEG, ffprobe as FFPROBE, requireMediaTools } from "../../scripts/media-tools.mjs";
24
+ requireMediaTools(["ffprobe"]);
31
25
 
32
26
  let KEY = process.env.ELEVENLABS_API_KEY || "";
33
27
  if (!KEY) {
@@ -68,7 +62,7 @@ for (const step of narrated) {
68
62
  const buf = Buffer.from(await res.arrayBuffer());
69
63
  const file = `seg${step.n}.mp3`;
70
64
  fs.writeFileSync(path.join(outDir, file), buf);
71
- const dur = parseFloat(execFileSync("ffprobe", [
65
+ const dur = parseFloat(execFileSync(FFPROBE(), [
72
66
  "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", path.join(outDir, file),
73
67
  ]).toString());
74
68
  out.push({ n: step.n, text: step.narration.text, file, duration: Math.round(dur * 100) / 100 });
@@ -28,6 +28,8 @@
28
28
  import fs from "node:fs";
29
29
  import path from "node:path";
30
30
  import { spawnSync } from "node:child_process";
31
+ import { ffmpeg as FFMPEG, requireMediaTools } from "./media-tools.mjs";
32
+ requireMediaTools(["ffmpeg"]);
31
33
 
32
34
  const dir = process.argv[2];
33
35
  if (!dir) { console.error("Usage: node calibrate.mjs <takeDir>"); process.exit(2); }
@@ -97,7 +99,7 @@ function roiChanges(bb) {
97
99
  const pad = 6;
98
100
  const x0 = Math.max(0, bb.x - pad), y0 = Math.max(0, bb.y - pad);
99
101
  const w = Math.min(FRAME.w - x0, bb.w + 2 * pad), h = Math.min(FRAME.h - y0, bb.h + 2 * pad);
100
- const res = spawnSync("ffmpeg", [
102
+ const res = spawnSync(FFMPEG(), [
101
103
  "-loglevel", "info", "-i", clean,
102
104
  "-vf", `crop=${w}:${h}:${x0}:${y0},select='gte(scene,0)',metadata=print`, "-f", "null", "-",
103
105
  ], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
@@ -218,7 +220,7 @@ if (clicks.length === 0) {
218
220
  }
219
221
 
220
222
  function sceneChanges(threshold) {
221
- const res = spawnSync("ffmpeg", [
223
+ const res = spawnSync(FFMPEG(), [
222
224
  "-loglevel", "info", "-i", clean,
223
225
  "-vf", `select='gt(scene,${threshold})',showinfo`, "-f", "null", "-",
224
226
  ], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ // ALIAS GUARD (founder law, 2026-09-09): `agentic-recorder` and `demobites` on
3
+ // npm are aliases of this package. They depend on `demobite` with a caret
4
+ // range, so minor and patch releases flow through automatically, but a MAJOR
5
+ // bump silently strands them on the old major. This guard runs before every
6
+ // publish of demobite (prepublishOnly) and in CI, and refuses to continue if
7
+ // any alias's dependency range no longer covers the version about to ship.
8
+ //
9
+ // When it fails: bump the range in aliases/<name>/package.json (and the alias
10
+ // version), publish each alias from the founder's own terminal, then publish
11
+ // demobite. See RELEASING.md.
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
17
+ const main = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
18
+ const aliasesDir = path.join(root, "aliases");
19
+ const [major] = main.version.split(".").map(Number);
20
+
21
+ let failed = false;
22
+ for (const name of fs.readdirSync(aliasesDir)) {
23
+ const pkgPath = path.join(aliasesDir, name, "package.json");
24
+ if (!fs.existsSync(pkgPath)) continue;
25
+ const alias = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
26
+ const range = alias.dependencies?.demobite ?? "";
27
+ const m = /^\^(\d+)\./.exec(range);
28
+ const covered = m && Number(m[1]) === major;
29
+ const line = `${alias.name.padEnd(18)} depends on demobite ${range.padEnd(8)} → ${covered ? "covers" : "DOES NOT COVER"} ${main.version}`;
30
+ console.log((covered ? " ✓ " : " ✗ ") + line);
31
+ if (!covered) failed = true;
32
+ const bin = fs.readFileSync(path.join(aliasesDir, name, "index.mjs"), "utf8");
33
+ if (!bin.includes('import "demobite/launcher/index.mjs"')) {
34
+ console.log(` ✗ ${alias.name} does not import the demobite launcher`);
35
+ failed = true;
36
+ }
37
+ }
38
+ if (failed) {
39
+ console.error("\nAlias guard failed: an npm alias would strand on the old major. Update aliases/*/package.json (range + version), publish the aliases, then publish demobite. See RELEASING.md.");
40
+ process.exit(1);
41
+ }
42
+ console.log("alias guard: ok");
@@ -0,0 +1,93 @@
1
+ // media-tools — the ONE place the recorder resolves ffmpeg and ffprobe.
2
+ //
3
+ // Order (founder design 2026-09-09):
4
+ // 1. an explicit override in <project>/.recorder/config.json { "ffmpeg": "...", "ffprobe": "..." }
5
+ // 2. a SYSTEM binary on PATH that passes the compatibility probe (below)
6
+ // 3. the packaged binaries from @ffmpeg-installer/ffmpeg + @ffprobe-installer/ffprobe
7
+ // (one platform each), installed by the launcher beside Playwright in the skill folder
8
+ //
9
+ // Compatibility, not presence: trim/mux need libx264 and the aac encoder. An old
10
+ // or stripped system build fails the probe and the packaged binary wins.
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import { execFileSync } from "node:child_process";
14
+ import { fileURLToPath, pathToFileURL } from "node:url";
15
+ import { createRequire } from "node:module";
16
+
17
+ const here = path.dirname(fileURLToPath(import.meta.url));
18
+ // scripts/ lives directly under the skill folder; node_modules sits beside it.
19
+ const skillRoot = path.resolve(here, "..");
20
+
21
+ function probe(bin, tool) {
22
+ try {
23
+ const out = execFileSync(bin, ["-version"], { stdio: ["ignore", "pipe", "ignore"], timeout: 8000 }).toString();
24
+ if (!/^ff(mpeg|probe) version/m.test(out)) return { ok: false, why: "not an ffmpeg build" };
25
+ if (tool === "ffmpeg") {
26
+ const enc = execFileSync(bin, ["-hide_banner", "-encoders"], { stdio: ["ignore", "pipe", "ignore"], timeout: 8000 }).toString();
27
+ if (!/\blibx264\b/.test(enc)) return { ok: false, why: "no libx264 encoder" };
28
+ if (!/\baac\b/.test(enc)) return { ok: false, why: "no aac encoder" };
29
+ }
30
+ return { ok: true };
31
+ } catch (e) {
32
+ return { ok: false, why: e?.code === "ENOENT" ? "not found" : "did not run" };
33
+ }
34
+ }
35
+
36
+ function fromOverride(tool) {
37
+ try {
38
+ const cfg = JSON.parse(fs.readFileSync(path.join(process.cwd(), ".recorder", "config.json"), "utf8"));
39
+ const v = cfg?.[tool];
40
+ return typeof v === "string" && v ? v : null;
41
+ } catch { return null; }
42
+ }
43
+
44
+ function fromPackage(tool) {
45
+ // Per-platform packages: only the current platform's binary is downloaded.
46
+ const pkg = tool === "ffmpeg" ? "@ffmpeg-installer/ffmpeg" : "@ffprobe-installer/ffprobe";
47
+ for (const base of [skillRoot, process.cwd()]) {
48
+ try {
49
+ const req = createRequire(pathToFileURL(path.join(base, "package.json")));
50
+ const p = req(pkg)?.path;
51
+ if (typeof p === "string" && fs.existsSync(p)) return p;
52
+ } catch { /* not installed here */ }
53
+ }
54
+ return null;
55
+ }
56
+
57
+ const cache = {};
58
+ /** Resolve a tool. Returns { path, source, why } — path null when nothing works. */
59
+ export function resolveTool(tool) {
60
+ if (cache[tool]) return cache[tool];
61
+ const tried = [];
62
+ const o = fromOverride(tool);
63
+ if (o) { const r = probe(o, tool); if (r.ok) return (cache[tool] = { path: o, source: "config" }); tried.push(`config ${o}: ${r.why}`); }
64
+ const sys = probe(tool, tool);
65
+ if (sys.ok) return (cache[tool] = { path: tool, source: "system" });
66
+ tried.push(`system: ${sys.why}`);
67
+ const pk = fromPackage(tool);
68
+ if (pk) { const r = probe(pk, tool); if (r.ok) return (cache[tool] = { path: pk, source: "packaged" }); tried.push(`packaged ${pk}: ${r.why}`); }
69
+ else tried.push("packaged: not installed");
70
+ return (cache[tool] = { path: null, source: null, why: tried.join("; ") });
71
+ }
72
+
73
+ export const ffmpeg = () => resolveTool("ffmpeg").path;
74
+ export const ffprobe = () => resolveTool("ffprobe").path;
75
+
76
+ /** Exit with a clear message unless both tools resolve. Used by every script that films or finishes. */
77
+ export function requireMediaTools(tools = ["ffmpeg", "ffprobe"]) {
78
+ const missing = tools.map((t) => [t, resolveTool(t)]).filter(([, r]) => !r.path);
79
+ if (missing.length) {
80
+ for (const [t, r] of missing) console.error(`${t}: no working build found (${r.why}).`);
81
+ console.error("Run `npx demobite@latest` again to install the packaged ffmpeg and ffprobe, or install them on your system.");
82
+ process.exit(1);
83
+ }
84
+ }
85
+
86
+ // `node media-tools.mjs` prints what would be used — the launcher calls this.
87
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
88
+ for (const t of ["ffmpeg", "ffprobe"]) {
89
+ const r = resolveTool(t);
90
+ console.log(r.path ? `${t}\t${r.source}\t${r.path}` : `${t}\tmissing\t${r.why}`);
91
+ }
92
+ process.exit(["ffmpeg", "ffprobe"].every((t) => resolveTool(t).path) ? 0 : 1);
93
+ }