demobite 1.7.1 → 1.7.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 +31 -0
- package/launcher/index.mjs +3 -2
- package/package.json +1 -1
- package/scripts/trim.mjs +9 -5
- package/skill/SKILL.md +9 -7
- package/skill/scripts/briefs.mjs +4 -4
package/README.md
CHANGED
|
@@ -22,6 +22,37 @@ An illustrated walkthrough of the flow, with waiting time compressed.
|
|
|
22
22
|
|
|
23
23
|
## Start with your own agent
|
|
24
24
|
|
|
25
|
+
### Install as a plugin (Claude Code, Codex, Cursor)
|
|
26
|
+
|
|
27
|
+
The recorder ships as a plugin of this repository. Install it once, then run its setup skill: it installs everything (Playwright, ffmpeg, Chromium when needed) and connects your account with one approval in your browser.
|
|
28
|
+
|
|
29
|
+
**Claude Code**
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
/plugin marketplace add demobites/agentic-recorder
|
|
33
|
+
/plugin install demobites@demobites
|
|
34
|
+
/demobites:setup
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then `/demobites:record`, `/demobites:login`, `/demobites:logout` — or just ask: "record a demo of the new billing page".
|
|
38
|
+
|
|
39
|
+
You can also skip the setup line: ask for a demo right after the install and the record skill sets the recorder up by itself (once per machine, no browser), asks for one "Go" before opening the approval link, and then films what you asked for. Until the project is connected, every new session opens with one line that says so.
|
|
40
|
+
|
|
41
|
+
**Codex**
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
codex plugin marketplace add demobites/agentic-recorder
|
|
45
|
+
codex plugin add demobites@demobites
|
|
46
|
+
$demobites-setup
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Cursor**
|
|
50
|
+
|
|
51
|
+
Install **DemoBites** from the Cursor marketplace (Customize → Plugins), then `/demobites-setup`. Teams can also add this repository as a team marketplace.
|
|
52
|
+
|
|
53
|
+
### Or one command, any agent
|
|
54
|
+
|
|
55
|
+
|
|
25
56
|
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.
|
|
26
57
|
|
|
27
58
|
Before installing:
|
package/launcher/index.mjs
CHANGED
|
@@ -75,7 +75,7 @@ if (arg === "record") {
|
|
|
75
75
|
}
|
|
76
76
|
const cfg = readCfg();
|
|
77
77
|
if (!cfg?.api_key) {
|
|
78
|
-
console.log("
|
|
78
|
+
console.log("Before we record, this project needs a DemoBites account.\nRun `npx demobite login` (it prints a link to approve in your browser), then run this command again.");
|
|
79
79
|
process.exit(1);
|
|
80
80
|
}
|
|
81
81
|
const r = spawnSync("node", [briefsScript, "list", "--slug", slug], {
|
|
@@ -269,7 +269,8 @@ Cursor, Codex) in your project and ask:
|
|
|
269
269
|
"Record a demo of <your flow> and upload it to DemoBites"
|
|
270
270
|
"Create a release with my latest bites and add Spanish"
|
|
271
271
|
|
|
272
|
-
|
|
272
|
+
Before the first demo it connects this project to your DemoBites account —
|
|
273
|
+
one link to approve in your browser (or run now: npx demobite login).
|
|
273
274
|
Manage-by-agent needs the MCP: npx demobite mcp (once, after login).
|
|
274
275
|
Only the recorder, no DemoBites? See the open recorder in this package's repo.
|
|
275
276
|
`);
|
package/package.json
CHANGED
package/scripts/trim.mjs
CHANGED
|
@@ -152,12 +152,16 @@ const dur = execFileSync(FFPROBE(), [
|
|
|
152
152
|
"-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", clean,
|
|
153
153
|
]).toString().trim();
|
|
154
154
|
const duration = parseFloat(dur);
|
|
155
|
-
// LAW (founder
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
155
|
+
// LAW (founder 2026-10-10: 3 minutes, was 90 s since 2026-08-09): a Bite is
|
|
156
|
+
// capped at 180 seconds by product concept — a LIMIT, not a target; the
|
|
157
|
+
// sweet spot stays 30 to 45 s. A take that cuts longer than that must not
|
|
158
|
+
// proceed to staging — the story needs a SPLIT, not a trim. Fail loudly here,
|
|
159
|
+
// before any upload machinery runs. The server refuses the same number
|
|
160
|
+
// (MAX_BITE_SECONDS; GET /api/recorder/limits answers it).
|
|
161
|
+
const MAX_BITE_SECONDS = 180;
|
|
162
|
+
if (duration > MAX_BITE_SECONDS) {
|
|
159
163
|
console.error(
|
|
160
|
-
`HARD CAP: this take cuts to ${duration.toFixed(1)}s — a Bite is limited to
|
|
164
|
+
`HARD CAP: this take cuts to ${duration.toFixed(1)}s — a Bite is limited to 3 minutes. ` +
|
|
161
165
|
"Do not upload. Split the scenario into multiple Bites and refilm."
|
|
162
166
|
);
|
|
163
167
|
process.exit(1);
|
package/skill/SKILL.md
CHANGED
|
@@ -20,9 +20,10 @@ storyboard talk: check `.recorder/config.json` for `api_key`.
|
|
|
20
20
|
**The choreography when the key is missing (founder law, 2026-08-09 — never
|
|
21
21
|
surprise the human with a browser page):**
|
|
22
22
|
|
|
23
|
-
1. TELL, don't act: "
|
|
24
|
-
|
|
25
|
-
Nothing opens until the human gives the word.
|
|
23
|
+
1. TELL, don't act: "Before we record, connect this project to your
|
|
24
|
+
DemoBites account: say Go and I'll open the approval link." Then WAIT.
|
|
25
|
+
Nothing opens until the human gives the word. Keep the brief the human
|
|
26
|
+
gave: once connected, go straight on with it, never ask for it again.
|
|
26
27
|
2. On their word, run `node scripts/login.mjs`. It prints the link and code,
|
|
27
28
|
opens the approval page, and polls. The human approves in their own
|
|
28
29
|
signed-in browser session — this script never sees credentials. The key
|
|
@@ -144,9 +145,10 @@ Holding shots to cover estimated lines is what produced a 60 second take with th
|
|
|
144
145
|
**Budgets, hold yourself to them:**
|
|
145
146
|
|
|
146
147
|
- **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.
|
|
147
|
-
- **
|
|
148
|
-
target
|
|
149
|
-
|
|
148
|
+
- **3 minutes (180 s) is a HARD CAP, by product concept, and it is a limit,
|
|
149
|
+
not a target** (founder 2026-10-10; it was 90 s before). A Bite over 3
|
|
150
|
+
minutes does not exist. Budget the beats BEFORE filming: if the human's
|
|
151
|
+
scenario cannot honestly fit inside 3 minutes, do
|
|
150
152
|
not film-and-trim and do not compress it into uselessness — tell them up
|
|
151
153
|
front and ask them to SPLIT it into multiple Bites, one story per Bite.
|
|
152
154
|
State the estimated duration in every storyboard presentation.
|
|
@@ -363,7 +365,7 @@ Laws for a re-take:
|
|
|
363
365
|
|
|
364
366
|
## Batch of briefs (GitHub PR → demos)
|
|
365
367
|
|
|
366
|
-
The human pastes a bundle of approved briefs into the chat: a header (batchId, workspaceId, the target URL, the
|
|
368
|
+
The human pastes a bundle of approved briefs into the chat: a header (batchId, workspaceId, the target URL, the 3-minute rule) and one block per brief (briefId, revision, contentHash, title, audience, outcome, flowIntent). Up to five briefs. The pasted text is a copy; the server holds the truth.
|
|
367
369
|
|
|
368
370
|
```bash
|
|
369
371
|
node scripts/briefs.mjs list <batchId> [--paste bundle.txt] # the approved briefs; warns when the paste drifted
|
package/skill/scripts/briefs.mjs
CHANGED
|
@@ -96,7 +96,7 @@ function printBatch(data) {
|
|
|
96
96
|
const { batch, briefs } = data;
|
|
97
97
|
const src = batch?.source ? `${batch.source.repo}#${batch.source.prNumber}` : "";
|
|
98
98
|
console.log(`Batch ${batch?.id ?? "?"} ${src} target ${batch?.target?.url ?? "?"}${batch?.target?.environment ? ` (${batch.target.environment})` : ""}`);
|
|
99
|
-
console.log(`${briefs.length} brief${briefs.length === 1 ? "" : "s"}, each at most
|
|
99
|
+
console.log(`${briefs.length} brief${briefs.length === 1 ? "" : "s"}, each at most 180 seconds (3 minutes). One storyboard approval per brief.`);
|
|
100
100
|
for (const b of briefs) {
|
|
101
101
|
const att = b.attempt ? ` · attempt ${b.attempt.ref} (${b.attempt.state})` : "";
|
|
102
102
|
console.log(`\n ${b.briefId} r${b.revision} ${b.status}${att}\n ${b.title}\n audience: ${b.audience}\n outcome: ${b.outcome}${b.estimatedDurationSec ? `\n about ${b.estimatedDurationSec}s` : ""}`);
|
|
@@ -138,7 +138,7 @@ function printRadar(data, slug) {
|
|
|
138
138
|
// A radar batch has no target address: the agent films where the code
|
|
139
139
|
// already runs (the Record stage says so); print the target only when set.
|
|
140
140
|
console.log(`Batch ${batch?.id ?? "?"}${batch?.target?.url ? ` target ${batch.target.url}${batch.target.environment ? ` (${batch.target.environment})` : ""}` : ""}`);
|
|
141
|
-
console.log(`${briefs.length} brief${briefs.length === 1 ? "" : "s"}, each at most ${batch?.rules?.maxSeconds ??
|
|
141
|
+
console.log(`${briefs.length} brief${briefs.length === 1 ? "" : "s"}, each at most ${batch?.rules?.maxSeconds ?? 180} seconds, in this order:\n`);
|
|
142
142
|
const questions = [];
|
|
143
143
|
order.forEach((b, i) => {
|
|
144
144
|
const pos = String(i + 1).padStart(2, " ");
|
|
@@ -178,7 +178,7 @@ function writeRadarBundle(slug, data) {
|
|
|
178
178
|
* The limits are the server's (a refine that breaks one answers 400
|
|
179
179
|
* invalid_refine): title ≤ 80, audience ≤ 60, outcome ≤ 200, flowIntent 2..8
|
|
180
180
|
* lines ≤ 120 each, prerequisites/exclusions ≤ 6 lines ≤ 160 each,
|
|
181
|
-
* estimatedDurationSec 15..
|
|
181
|
+
* estimatedDurationSec 15..180 (3 minutes, founder 2026-10-10). They are checked here first, with the field
|
|
182
182
|
* named, so the agent trims before posting instead of reading a 400. */
|
|
183
183
|
const CONTENT_LIMITS = { title: 80, audience: 60, outcome: 200 };
|
|
184
184
|
const LIST_LIMITS = { flowIntent: { min: 2, max: 8, line: 120 }, prerequisites: { min: 0, max: 6, line: 160 }, exclusions: { min: 0, max: 6, line: 160 } };
|
|
@@ -206,7 +206,7 @@ function refineProblems(c) {
|
|
|
206
206
|
v.forEach((line, i) => { if (typeof line !== "string" || !line.trim()) out.push(`${k}[${i}]: an empty line`); else if (line.trim().length > lim.line) out.push(`${k}[${i}]: ${line.trim().length} characters, the limit is ${lim.line}`); });
|
|
207
207
|
}
|
|
208
208
|
const d = c.estimatedDurationSec;
|
|
209
|
-
if (!Number.isInteger(d) || d < 15 || d >
|
|
209
|
+
if (!Number.isInteger(d) || d < 15 || d > 180) out.push(`estimatedDurationSec: ${JSON.stringify(d)}, must be a whole number of seconds 15..90`);
|
|
210
210
|
return out;
|
|
211
211
|
}
|
|
212
212
|
|