demobites 1.30.2 → 1.32.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 +1 -1
- package/scripts/record.mjs +29 -4
- package/skill/scripts/plan.mjs +29 -5
package/package.json
CHANGED
package/scripts/record.mjs
CHANGED
|
@@ -394,7 +394,10 @@ const postFrame = async () => {
|
|
|
394
394
|
// the JPEG OUTSIDE the page with ffmpeg. No ffmpeg → no thumbnail, never a glitch.
|
|
395
395
|
const { data } = await within(frameCdp.send("Page.captureScreenshot", { format: "jpeg", quality: 60 }), 4000);
|
|
396
396
|
const small = await within(shrinkJpeg(Buffer.from(data, "base64"), 480), 4000);
|
|
397
|
-
|
|
397
|
+
// A BLANK STILL IS NO STILL (founder 2026-10-18: a white box with the red pill while the first page loaded). A
|
|
398
|
+
// 480 px JPEG of a white page is under 3 KB; a page with anything on it is well over. The app keeps the last real
|
|
399
|
+
// still, or says the page is loading.
|
|
400
|
+
if (small && small.length > 3000) await within(postEvent("frame", { ...frameStep, jpeg: small.toString("base64") }), 8000);
|
|
398
401
|
} catch (e) {
|
|
399
402
|
if (!frameWarned) { frameWarned = true; console.error(`frame: dropped (${String(e?.message ?? e).split("\n")[0]})`); }
|
|
400
403
|
} finally { framing = false; }
|
|
@@ -856,7 +859,7 @@ try {
|
|
|
856
859
|
process.stdout.write(`step ${rec.n} ${step.action} ${step.label ?? ""}\n`);
|
|
857
860
|
// The customer's line (founder 2026-10-14): one bar, step n of N. The
|
|
858
861
|
// agent relays this, never the step/action lines above.
|
|
859
|
-
{ const N = STORYBOARD.steps.length, filled = Math.round((rec.n / N) * 20); process.stdout.write(`
|
|
862
|
+
{ const N = STORYBOARD.steps.length, filled = Math.round((rec.n / N) * 20); process.stdout.write(`Recording ${"█".repeat(filled)}${"░".repeat(20 - filled)} step ${rec.n} of ${N}\n`); void postEvent("filming", { n: rec.n, N }); frameStep = { n: rec.n, N }; void postFrame(); }
|
|
860
863
|
if (step.action === "goto") {
|
|
861
864
|
const navIssuedAt = t();
|
|
862
865
|
// domcontentloaded, not load: a heavy site behind a proxy (the cloud
|
|
@@ -1319,14 +1322,36 @@ if (connectedBrowser) { try { await connectedBrowser.close(); } catch {} }
|
|
|
1319
1322
|
if (ownedBrowser) { try { await ownedBrowser.close(); } catch {} }
|
|
1320
1323
|
fs.writeFileSync(path.join(DIR, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
1321
1324
|
if (failure) {
|
|
1322
|
-
|
|
1325
|
+
const n = manifest.steps.length + 1;
|
|
1326
|
+
const N = STORYBOARD.steps.length;
|
|
1327
|
+
const st = STORYBOARD.steps[n - 1] || {};
|
|
1328
|
+
console.error(`TAKE FAILED at step ${n}: ${failure.message}`);
|
|
1323
1329
|
console.error("Partial raw.webm + manifest.json saved in", DIR);
|
|
1324
|
-
|
|
1330
|
+
// THE REASON REACHES THE PERSON (founder 2026-10-18, "Recording did not finish" with nothing else: "please
|
|
1331
|
+
// analyze"): the step, its words, and what the camera could not do, in plain words. Exit 3 tells plan.mjs the
|
|
1332
|
+
// failure is already posted.
|
|
1333
|
+
const m = /no visible target for (.+)$/.exec(failure.message);
|
|
1334
|
+
const what = m ? `the camera could not find ${describeTarget(m[1])} on the page` : failure.message.replace(/\s+/g, " ").slice(0, 160);
|
|
1335
|
+
const label = st.name || st.label || (st.selector ? describeTarget(st.selector) : st.action) || "";
|
|
1336
|
+
await postEvent("failed", { message: `The recording stopped at step ${n} of ${N}${label ? ` (${label})` : ""}: ${what}.`, step: n, N, reason: failure.message.slice(0, 300) });
|
|
1337
|
+
process.exit(3);
|
|
1325
1338
|
}
|
|
1326
1339
|
const cursorKinds = [...new Set(mouseEvents.map((e) => e.cursor))].join(", ");
|
|
1340
|
+
/** A selector in the person's words: has-text('X') → "X"; aria-label → its words; else the selector itself. */
|
|
1341
|
+
function describeTarget(sel) {
|
|
1342
|
+
const t = /has-text\(['"](.+?)['"]\)/.exec(sel)?.[1] || /aria-label=['"](.+?)['"]/.exec(sel)?.[1];
|
|
1343
|
+
if (!t) return `"${sel}"`;
|
|
1344
|
+
const kind = /menuitem/.test(sel) ? "in the menu" : /role=tab/.test(sel) ? "among the tabs" : /button/.test(sel) ? "as a button" : "";
|
|
1345
|
+
return `"${t}"${kind ? ` ${kind}` : ""}`;
|
|
1346
|
+
}
|
|
1327
1347
|
console.log(
|
|
1328
1348
|
`DONE raw.webm + manifest.json in ${DIR} — ${manifest.duration.toFixed(1)}s, ` +
|
|
1329
1349
|
`${manifest.steps.length} steps, ${manifest.shots.length} camera shots, ` +
|
|
1330
1350
|
`${mouseEvents.length} mouse events (${cursorKinds})` +
|
|
1331
1351
|
`${BURN_CURSOR ? ", cursor BURNED IN" : ", cursor rendered by the studio"}`,
|
|
1332
1352
|
);
|
|
1353
|
+
// THE EXIT IS BOUNDED (2026-10-18): the take is on disk and the manifest written; nothing after this line is work.
|
|
1354
|
+
// The clock says when we got here, and a timer that holds no reference ends the process within 3 s should anything
|
|
1355
|
+
// (a browser handle, a socket) keep the event loop alive — the founder's take sat here for 46 s.
|
|
1356
|
+
console.log(`${new Date().toISOString()} take saved; exiting`);
|
|
1357
|
+
setTimeout(() => process.exit(0), 3000).unref();
|
package/skill/scripts/plan.mjs
CHANGED
|
@@ -218,7 +218,7 @@ const firstMessage = async () => {
|
|
|
218
218
|
// storyboard is the base; the note says what to keep an eye on; the probe finds what moved.
|
|
219
219
|
job.retake_of && job.storyboard
|
|
220
220
|
? `## 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)}`
|
|
221
|
-
: 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.`,
|
|
221
|
+
: codeOn ? `Write the storyboard from the page's own words, probe it, fix it, probe again, then submit_storyboard. Read a file only when the page's words do not name a control you need; never as a first move. You have at most 4 dry runs and 3 minutes from the first one; after that, hand in what passed and name in 'unsure' what you could not find.` : `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. You have at most 4 dry runs and 3 minutes from the first one; after that, hand in what passed and name in 'unsure' what you could not find.`,
|
|
222
222
|
];
|
|
223
223
|
return parts.join("\n\n");
|
|
224
224
|
};
|
|
@@ -226,6 +226,16 @@ const firstMessage = async () => {
|
|
|
226
226
|
let storyboard = null;
|
|
227
227
|
let creates = "";
|
|
228
228
|
const messages = [{ role: "user", content: [{ type: "text", text: await firstMessage() }] }];
|
|
229
|
+
// THE PLANNING CAP (founder 2026-10-18, a 13-minute storyboard: "inconceivable"): the planner gets FOUR dry runs, or
|
|
230
|
+
// three minutes from the first one, whichever comes first. After that no probe runs; the next move is the storyboard
|
|
231
|
+
// as it stands, with the steps it could not find named in `unsure`, so the person answers the question instead of
|
|
232
|
+
// waiting while it rewrites and retries (that job: 18 dry runs, 13 model turns).
|
|
233
|
+
const CAP_PROBES = 4;
|
|
234
|
+
const CAP_MS = 3 * 60 * 1000;
|
|
235
|
+
let probes = 0;
|
|
236
|
+
let firstProbeAt = 0;
|
|
237
|
+
const capped = () => probes >= CAP_PROBES || (firstProbeAt && Date.now() - firstProbeAt > CAP_MS);
|
|
238
|
+
const CAP_TEXT = () => `PLANNING CAP REACHED (${probes} dry run${probes === 1 ? "" : "s"}, ${Math.round((Date.now() - (firstProbeAt || Date.now())) / 1000)} s since the first). No more dry runs, no more reading. Hand in the storyboard NOW with submit_storyboard: keep every step that passed; a step that failed becomes a hover or a settle on the nearest element the page does have, or goes; write ONE short sentence in \`unsure\` naming what you could not find on the page (the step and the control), so the person can tell you where it is. The person is waiting.`;
|
|
229
239
|
const plan = async () => {
|
|
230
240
|
say("Writing the storyboard…");
|
|
231
241
|
storyboard = null;
|
|
@@ -255,12 +265,20 @@ const plan = async () => {
|
|
|
255
265
|
else if (u.name === "harvest_words") { say(`Reading the app's own words on ${(input.urls || []).length} page${(input.urls || []).length === 1 ? "" : "s"}…`); out = harvest(input.urls); }
|
|
256
266
|
else if (u.name === "explore") { say(`Walking the app from ${where(input.from)}, looking for ${String(input.looking_for || "the brief's words").slice(0, 60)}…`); out = explore(input.from, input.looking_for, input.depth); }
|
|
257
267
|
else if (u.name === "screenshot") { say(`Looking at ${where(input.url)}…`); out = screenshot(input.url); }
|
|
258
|
-
else if (u.name === "probe") {
|
|
268
|
+
else if (u.name === "probe") {
|
|
269
|
+
if (capped()) out = CAP_TEXT();
|
|
270
|
+
else {
|
|
271
|
+
probes += 1; if (!firstProbeAt) firstProbeAt = Date.now();
|
|
272
|
+
say(`Dry run of the storyboard, ${(input.storyboard?.steps || []).length} steps… (${probes} of ${CAP_PROBES})`);
|
|
273
|
+
out = probe(input.storyboard);
|
|
274
|
+
if (capped()) out += `\n\n${CAP_TEXT()}`;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
259
277
|
else if (u.name === "submit_storyboard") {
|
|
260
278
|
say("Handing in the storyboard…");
|
|
261
279
|
const bad = validate(input.storyboard);
|
|
262
280
|
if (bad) out = `Not accepted: ${bad}`;
|
|
263
|
-
else { storyboard = input.storyboard; creates = typeof input.creates === "string" ? input.creates : ""; out = "Accepted."; }
|
|
281
|
+
else { storyboard = input.storyboard; creates = typeof input.creates === "string" ? input.creates : ""; if (typeof input.unsure === "string" && input.unsure.trim()) storyboard.unsure = input.unsure.trim().slice(0, 240); out = "Accepted."; }
|
|
264
282
|
} else out = `Unknown tool ${u.name}`;
|
|
265
283
|
} catch (e) { out = `Tool failed: ${e.message}`; }
|
|
266
284
|
results.push({ type: "tool_result", tool_use_id: u.id, content: typeof out === "string" ? [{ type: "text", text: out }] : out });
|
|
@@ -303,13 +321,19 @@ try {
|
|
|
303
321
|
|
|
304
322
|
// ── The take ─────────────────────────────────────────────────────────────
|
|
305
323
|
const sbFile = path.join(dir, "storyboard.json");
|
|
324
|
+
// EVERY PHASE IS STAMPED (founder's take of 2026-10-18: 46 s between the video on disk and the trim, unexplained;
|
|
325
|
+
// "why in God's name does it take us so long?"): the clock before each step and the seconds it took after, so the
|
|
326
|
+
// next take's log names where the minutes go.
|
|
306
327
|
const step = async (name, args, what) => {
|
|
328
|
+
const t0 = Date.now();
|
|
329
|
+
console.log(` ${new Date(t0).toISOString()} ${what}…`);
|
|
307
330
|
const r = spawnSync("node", [script(name), ...args], { cwd: process.cwd(), stdio: "inherit", env: process.env });
|
|
308
|
-
|
|
331
|
+
console.log(` ${new Date().toISOString()} ${what}: ${((Date.now() - t0) / 1000).toFixed(1)}s`);
|
|
332
|
+
if (r.status !== 0) { console.error(` ! ${name} failed (exit ${r.status})`); if (r.status !== 3) await postEvent("failed", { message: `${what} did not finish` }); process.exit(1); }
|
|
309
333
|
};
|
|
310
334
|
const sb = JSON.parse(fs.readFileSync(sbFile, "utf8"));
|
|
311
335
|
if (Array.isArray(sb.prep) && sb.prep.length) await step("cleanup.mjs", [dir, "--prep"], "Preparing the data");
|
|
312
|
-
await step("record.mjs", [dir, sbFile], "
|
|
336
|
+
await step("record.mjs", [dir, sbFile], "Recording");
|
|
313
337
|
if (Array.isArray(sb.cleanup) && sb.cleanup.length) await step("cleanup.mjs", [dir], "Putting the data back");
|
|
314
338
|
await step("trim.mjs", [dir], "Trimming the take");
|
|
315
339
|
await step("calibrate.mjs", [dir], "Measuring the take");
|