demobites 1.36.0 → 1.38.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
package/skill/scripts/plan.mjs
CHANGED
|
@@ -136,7 +136,9 @@ const explore = (from, lookingFor, depth) => {
|
|
|
136
136
|
fs.mkdirSync(exDir, { recursive: true });
|
|
137
137
|
const child = spawnSync("node", [script("explore.mjs"), from, "--for", words, "--depth", String(Math.min(3, Math.max(1, Number(depth) || 2))), "--out", exDir], { cwd: process.cwd(), encoding: "utf8", env: process.env, maxBuffer: 64 * 1024 * 1024 });
|
|
138
138
|
const lines = (child.stdout || "").split("\n");
|
|
139
|
-
|
|
139
|
+
// THE PERSON HEARS PHASES, NOT FINDINGS (founder 2026-10-18: "Nothing beyond the start page matched… you need to
|
|
140
|
+
// control what you're showing to the user"): the exploration's lines stay in the log.
|
|
141
|
+
for (const l of lines) if (l.startsWith("· ")) console.log(l);
|
|
140
142
|
const last = lines.filter((l) => l.startsWith("RESULT ")).pop();
|
|
141
143
|
let res = null; try { res = JSON.parse(last.slice(7)); } catch { /* none */ }
|
|
142
144
|
if (!res) return [{ type: "text", text: `Exploring failed: ${(child.stderr || "").trim().split("\n").pop() || "no result"}` }];
|
|
@@ -241,13 +243,17 @@ let probes = 0;
|
|
|
241
243
|
let firstProbeAt = 0;
|
|
242
244
|
let asked = 0;
|
|
243
245
|
const capped = () => probes >= CAP_PROBES || (firstProbeAt && Date.now() - firstProbeAt > CAP_MS);
|
|
244
|
-
const CAP_TEXT = () => asked <
|
|
246
|
+
const CAP_TEXT = () => asked < 1
|
|
245
247
|
? `STOP (${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. Call ask_person NOW with ONE short question in plain words: what you were looking for, where you looked, and how they want to play it (where it is, or what to show instead). The person is there and knows the app. Their answer comes back as the result; then fix the storyboard from their words, probe once, and hand it in.`
|
|
246
248
|
: `STOP. 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; name in \`unsure\` what you could not find.`;
|
|
247
249
|
/** The question goes to the chat; the planner waits here for the person's word (up to 30 minutes). */
|
|
248
250
|
const askPerson = async (text) => {
|
|
249
251
|
const q = String(text || "").trim().slice(0, 600);
|
|
250
252
|
if (!q) return "ask_person needs a question.";
|
|
253
|
+
// ONE QUESTION, EVER (founder 2026-10-18: "at most one clarification… then go"; a second question three seconds
|
|
254
|
+
// after one dry run left him stuck): once answered, the planner works with what it has and hands in the
|
|
255
|
+
// storyboard, naming what it could not find in `unsure`. The person fixes it from the storyboard, not from a loop.
|
|
256
|
+
if (asked >= 1) return "You asked once and they answered. No more questions: hand in the storyboard NOW with submit_storyboard — keep every step that passed, turn a step you could not place into a hover or a settle on the nearest element the page has, and name what you could not find in `unsure`.";
|
|
251
257
|
say("Asking you in the chat…");
|
|
252
258
|
if (!listening) return "(no chat here: decide yourself and hand in the storyboard)";
|
|
253
259
|
await postEvent("question", { text: q });
|
|
@@ -273,7 +279,7 @@ const plan = async () => {
|
|
|
273
279
|
const res = await relay(messages);
|
|
274
280
|
const content = Array.isArray(res.content) ? res.content : [];
|
|
275
281
|
messages.push({ role: "assistant", content });
|
|
276
|
-
for (const b of content) if (b.type === "text" && b.text?.trim())
|
|
282
|
+
for (const b of content) if (b.type === "text" && b.text?.trim()) console.log(`· ${b.text.trim().split("\n")[0].replace(/^[#*\-\s]+/, "").slice(0, 140)}`);
|
|
277
283
|
const uses = content.filter((b) => b.type === "tool_use");
|
|
278
284
|
if (!uses.length) {
|
|
279
285
|
if (storyboard) return;
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
// this take; a no-op everywhere else (the terminal flow, the cloud sandbox
|
|
23
23
|
// copy that may not ship listen-event.mjs).
|
|
24
24
|
const postEvent = await import("./listen-event.mjs").then((m) => m.postEvent).catch(() => async () => null);
|
|
25
|
+
const listening = await import("./listen-event.mjs").then((m) => m.listening).catch(() => false);
|
|
25
26
|
/** Where the person watches their takes: the Demos grid. `dashboardUrl` from
|
|
26
27
|
* the server wins (relative or absolute), else <base>/demos. */
|
|
27
28
|
export function gridUrl(base, dashboardUrl) {
|
|
@@ -151,6 +152,16 @@ export async function waitForDecision({ base, apiKey, stagingId, pageUrl, delive
|
|
|
151
152
|
if (last.zooms === 0) console.error("WARNING: no camera shots landed.");
|
|
152
153
|
}
|
|
153
154
|
if (finalStudioUrl) console.log(`Studio: ${finalStudioUrl}`);
|
|
155
|
+
// THE STILLS, DRAWN HERE BEFORE "READY" (2026-10-18): the demo's intro and backdrop, by the studio's own code in
|
|
156
|
+
// this recorder's Chrome, remembered by the app so the export draws nothing. Bounded; a miss costs nothing.
|
|
157
|
+
if (listening) {
|
|
158
|
+
try {
|
|
159
|
+
const { spawnSync } = await import("node:child_process");
|
|
160
|
+
const here = (await import("node:path")).dirname((await import("node:url")).fileURLToPath(import.meta.url));
|
|
161
|
+
const r = spawnSync(process.execPath, [(await import("node:path")).join(here, "stills.mjs"), String(approvedBiteId)], { cwd: process.cwd(), encoding: "utf8", env: process.env, timeout: 60000 });
|
|
162
|
+
for (const l of (r.stdout || "").trim().split("\n").filter(Boolean)) console.log(` ${l}`);
|
|
163
|
+
} catch (e) { console.log(` stills: skipped (${String(e?.message ?? e).split("\n")[0]})`); }
|
|
164
|
+
}
|
|
154
165
|
await postEvent("ready", { biteId: approvedBiteId, studioUrl: finalStudioUrl ?? null });
|
|
155
166
|
return { exitCode: 0, status: "completed", biteId: approvedBiteId, studioUrl: finalStudioUrl };
|
|
156
167
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// THE STILLS ARE DRAWN HERE (founder 2026-10-18: "at that point you have a browser… load fabric… create the two
|
|
3
|
+
// images"). After the demo is built and before this recorder says "ready", its own Chrome opens the app's internal
|
|
4
|
+
// still page — the studio's own render code, the design's fonts from the web — for the intro and the backdrop,
|
|
5
|
+
// and hands the PNGs to the app, which remembers them for the export. Seconds here; a 30-second sandbox saved there.
|
|
6
|
+
// Never fatal: on any miss the export draws them itself as before.
|
|
7
|
+
// usage: node stills.mjs <biteId> (base and key from <cwd>/.recorder/config.json)
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
const biteId = Number(process.argv[2] || 0);
|
|
11
|
+
if (!biteId) { console.error("usage: stills.mjs <biteId>"); process.exit(2); }
|
|
12
|
+
const cfg = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), ".recorder", "config.json"), "utf8"));
|
|
13
|
+
const base = (process.env.RECORDER_BASE || cfg.base || "https://app.demobites.com").replace(/\/+$/, "");
|
|
14
|
+
const headers = { Authorization: `Bearer ${cfg.api_key}`, "Content-Type": "application/json" };
|
|
15
|
+
const t0 = Date.now();
|
|
16
|
+
const plan = await fetch(`${base}/api/recorder/stills?bite_id=${biteId}`, { headers, signal: AbortSignal.timeout(15000) }).then((r) => (r.ok ? r.json() : null)).catch(() => null);
|
|
17
|
+
if (!plan?.stills?.length) { console.log("stills: nothing to draw here"); process.exit(0); }
|
|
18
|
+
const { chromium } = await import("playwright");
|
|
19
|
+
const browser = await chromium.launch({ channel: "chrome", headless: true }).catch(async () => chromium.launch({ headless: true }));
|
|
20
|
+
const out = {};
|
|
21
|
+
try {
|
|
22
|
+
const ctx = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
|
|
23
|
+
for (const still of plan.stills) {
|
|
24
|
+
const page = await ctx.newPage();
|
|
25
|
+
try {
|
|
26
|
+
const res = await page.goto(still.url, { waitUntil: "domcontentloaded", timeout: 20000 });
|
|
27
|
+
if (!res || res.status() !== 200) throw new Error(`the still page answered ${res ? res.status() : "nothing"}`);
|
|
28
|
+
await page.waitForFunction("window.__FABRIC_STILL__ !== undefined", undefined, { timeout: 25000 });
|
|
29
|
+
const r = await page.evaluate("window.__FABRIC_STILL__");
|
|
30
|
+
if (!r?.ok || typeof r.dataUrl !== "string" || !r.dataUrl.startsWith("data:image/png;base64,")) throw new Error(r?.error || "no PNG from the still page");
|
|
31
|
+
out[still.kind] = r.dataUrl.slice("data:image/png;base64,".length);
|
|
32
|
+
console.log(`stills: ${still.kind} drawn (${Math.round(out[still.kind].length * 0.75 / 1024)} KB)`);
|
|
33
|
+
} catch (e) { console.log(`stills: ${still.kind} skipped (${String(e?.message ?? e).split("\n")[0]})`); }
|
|
34
|
+
finally { await page.close().catch(() => {}); }
|
|
35
|
+
}
|
|
36
|
+
} finally { await browser.close().catch(() => {}); }
|
|
37
|
+
if (!Object.keys(out).length) process.exit(0);
|
|
38
|
+
const put = await fetch(`${base}/api/recorder/stills`, { method: "PUT", headers, body: JSON.stringify({ bite_id: biteId, hash: plan.hash, stills: out }), signal: AbortSignal.timeout(30000) }).then((r) => r.json().catch(() => ({ ok: r.ok }))).catch((e) => ({ error: String(e?.message ?? e) }));
|
|
39
|
+
console.log(`stills: ${put?.ok ? `remembered for the export (${Object.keys(out).join(", ")})` : `not kept (${put?.error ?? "?"})`} in ${((Date.now() - t0) / 1000).toFixed(1)} s`);
|