demobites 1.34.0 → 1.35.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.
@@ -714,10 +714,24 @@ if (arg === "listen") {
714
714
  await event(job.id, "ready", { forgot: site, machine: name });
715
715
  noteState({ busy: false, job: null, last_error: null });
716
716
  };
717
+ // THE SESSION MOVES (2026-10-18, their consent in the app): this machine exports the sign-in its profile holds
718
+ // at the job's site and hands it to the vault with its own key. Counts come back; nothing else is said.
719
+ const runVault = async (job) => {
720
+ noteState({ busy: true, job: job.id });
721
+ await event(job.id, "started", { machine: name });
722
+ const env = { ...process.env, RECORDER_BASE: base, ...profileFor(job) };
723
+ const r = spawnSync(process.execPath, [path.join(dest, "scripts", "export-session.mjs"), env.RECORDER_PROFILE || path.join(process.cwd(), ".recorder", "profile"), job.url], { cwd: process.cwd(), encoding: "utf8", env });
724
+ const line = (r.stdout || "").trim().split("\n").filter((l) => l.startsWith("RESULT ")).pop();
725
+ let out = null; try { out = JSON.parse(line.slice(7)); } catch { /* none */ }
726
+ if (r.status === 0 && out) { console.log(` ○ Your sign-in at ${new URL(job.url).host} is in the vault for the cloud (${out.cookies} cookies).`); await event(job.id, "ready", { ...out, machine: name }); }
727
+ else { const why = (r.stderr || r.stdout || "").trim().split("\n").pop() || `exit ${r.status}`; console.log(` ! The sign-in could not be moved: ${why}`); await event(job.id, "failed", { message: r.status === 3 ? "No sign-in at that site on this machine yet" : `The sign-in could not be moved (${why.slice(0, 120)})` }); }
728
+ noteState({ busy: false, job: null, last_error: null });
729
+ };
717
730
  const runJob = async (job) => {
718
731
  waitOff();
719
732
  if (job.kind === "off") return runOff(job);
720
733
  if (job.kind === "forget") return runForget(job);
734
+ if (job.kind === "vault") return runVault(job);
721
735
  if (job.kind === "look" || job.kind === "signin" || job.kind === "signout") return runLookOrSignin(job);
722
736
  const model = typeof job.model === "string" && job.model ? job.model : JOB_MODEL;
723
737
  // Alive while filming (1.13.1): a heartbeat every 5 s so the rail never shows this machine
@@ -796,6 +810,13 @@ if (arg === "listen") {
796
810
  }
797
811
  }
798
812
 
813
+ // ── 2c. THE POOL (2026-10-18): a box of ours, many workspaces, no key of its own ──────────────
814
+ // demobite pool --base <url> --secret <pool secret> [--slots N] [--name <box>]
815
+ if (arg === "pool") {
816
+ const r = spawnSync(process.execPath, [path.join(dest, "scripts", "pool.mjs"), ...process.argv.slice(3)], { stdio: "inherit", cwd: process.cwd(), env: process.env });
817
+ process.exit(r.status ?? 0);
818
+ }
819
+
799
820
  // ── 3. Subcommands ─────────────────────────────────────────────────────────
800
821
  if (arg === "login" || arg === "logout") {
801
822
  // `login <base>` forwards the base (dev, a preview) to login.mjs, which
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "demobites",
3
- "version": "1.34.0",
3
+ "version": "1.35.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
  "demobites": "launcher/index.mjs"
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ // THE SESSION MOVES, NEVER THE CREDENTIALS (founder 2026-10-18, with the person's consent in the app): read the
3
+ // cookies and local storage the app set in the recorder's OWN Chrome profile — ours to drive; never the keychain,
4
+ // never a password — for the one site asked, and hand them to the vault with this machine's key. The vault answers
5
+ // with counts; nothing of the session is printed here.
6
+ // usage: node export-session.mjs <profileDir> <url> (base and key from <cwd>/.recorder/config.json)
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ const [profileDir, url] = process.argv.slice(2);
10
+ if (!profileDir || !url) { console.error("usage: export-session.mjs <profileDir> <url>"); process.exit(2); }
11
+ const cfg = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), ".recorder", "config.json"), "utf8"));
12
+ const base = (process.env.RECORDER_BASE || cfg.base || "https://app.demobites.com").replace(/\/+$/, "");
13
+ const { chromium } = await import("playwright");
14
+ const origin = new URL(url).origin;
15
+ const host = new URL(url).hostname;
16
+ const site = host.split(".").slice(-2).join(".");
17
+ const ctx = await chromium.launchPersistentContext(path.resolve(profileDir), { channel: "chrome", headless: true, viewport: { width: 1280, height: 800 } });
18
+ let state;
19
+ try {
20
+ const page = await ctx.newPage();
21
+ await page.goto(origin, { waitUntil: "domcontentloaded", timeout: 30000 }).catch(() => {});
22
+ const cookies = (await ctx.cookies()).filter((c) => c.domain.replace(/^\./, "").endsWith(site));
23
+ const localStorage = await page.evaluate(() => Object.entries(window.localStorage).map(([name, value]) => ({ name, value }))).catch(() => []);
24
+ state = { cookies, origins: [{ origin, localStorage }] };
25
+ } finally { await ctx.close(); }
26
+ if (!state.cookies.length && !state.origins[0].localStorage.length) { console.error(`no session at ${origin} in this profile`); process.exit(3); }
27
+ const res = await fetch(`${base}/api/recorder/session`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.api_key}` }, body: JSON.stringify({ url: origin, state, label: "From my machine" }) });
28
+ const d = await res.json().catch(() => ({}));
29
+ if (!res.ok) { console.error(`the vault refused: ${d.error ?? res.status}`); process.exit(1); }
30
+ console.log(`RESULT ${JSON.stringify({ cookies: d.cookies ?? state.cookies.length, origins: d.origins ?? 1, profile_id: d.profile_id ?? null })}`);
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ // THE POOL (founder 2026-10-18: "every user will install the inner one on his machine; the second one is a unique
3
+ // key he never sees… a pool that you're managing behind the scenes"). A box of OURS: it checks in with the pool
4
+ // secret, claims any workspace's cloud job, receives that job's cloud key and the person's session for the length
5
+ // of the take, runs the same chain the laptop runs (plan → record → trim → measure → pack → upload) in a folder of
6
+ // its own, and shreds the folder when the take is up. Ten identities on one box, none written where a neighbour
7
+ // could read them. A reclaim or a drain order stops new claims; running takes finish.
8
+ //
9
+ // node pool.mjs --base https://dev.demobites.com --secret <pool secret> [--slots 5] [--name <box name>]
10
+ import fs from "node:fs";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import { spawn } from "node:child_process";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
17
+ const argv = process.argv.slice(2);
18
+ const flag = (f, d = "") => (argv.includes(f) ? argv[argv.indexOf(f) + 1] ?? d : d);
19
+ const base = (flag("--base", process.env.RECORDER_BASE || "https://app.demobites.com")).replace(/\/+$/, "");
20
+ const secret = flag("--secret", process.env.POOL_SECRET || "");
21
+ if (!secret) { console.error("pool: --secret (or POOL_SECRET) is required"); process.exit(2); }
22
+ const HOME = process.env.DEMOBITE_POOL_HOME || path.join(os.homedir(), ".demobite", "pool");
23
+ fs.mkdirSync(HOME, { recursive: true });
24
+ const stamp = () => new Date().toISOString();
25
+ let version = "?"; try { version = JSON.parse(fs.readFileSync(path.join(HERE, "..", "package.json"), "utf8")).version; } catch { try { version = JSON.parse(fs.readFileSync(path.join(HERE, "package.json"), "utf8")).version; } catch { /* unknown */ } }
26
+
27
+ // WHO AM I: the instance's own id and type from the metadata service (IMDSv2), the hostname otherwise.
28
+ const imds = async (p) => {
29
+ try {
30
+ const t = await fetch("http://169.254.169.254/latest/api/token", { method: "PUT", headers: { "X-aws-ec2-metadata-token-ttl-seconds": "60" }, signal: AbortSignal.timeout(1500) });
31
+ if (!t.ok) return null;
32
+ const r = await fetch(`http://169.254.169.254/latest/meta-data/${p}`, { headers: { "X-aws-ec2-metadata-token": await t.text() }, signal: AbortSignal.timeout(1500) });
33
+ return r.ok ? (await r.text()).trim() : null;
34
+ } catch { return null; }
35
+ };
36
+ const instanceId = (await imds("instance-id")) || `local-${os.hostname()}`;
37
+ const instanceType = (await imds("instance-type")) || "local";
38
+ const name = flag("--name", instanceId);
39
+ // CAPACITY (the matrix, measured 2026-10-18 on c7i.2xlarge: 10 clean, 5 within ~90 s end to end): comfort slots by type.
40
+ const COMFORT = { "2xlarge": 5, "4xlarge": 10, "8xlarge": 20, "xlarge": 2, "large": 1 };
41
+ const sizeOf = (t) => Object.keys(COMFORT).find((k) => t.endsWith(`.${k}`));
42
+ const slots = Number(flag("--slots", "")) || COMFORT[sizeOf(instanceType) ?? ""] || Math.max(1, Math.floor(os.cpus().length * 0.6));
43
+
44
+ const api = async (body) => {
45
+ try {
46
+ const res = await fetch(`${base}/api/recorder/pool`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${secret}` }, body: JSON.stringify({ instance_id: instanceId, instance_type: instanceType, name, slots, version, ...body }), signal: AbortSignal.timeout(20000) });
47
+ if (res.status === 401) { console.error(` ${stamp()} the pool secret was refused; stopping`); process.exit(1); }
48
+ return res.ok ? await res.json() : null;
49
+ } catch (e) { console.error(` ${stamp()} pool: ${String(e?.message ?? e).split("\n")[0]}`); return null; }
50
+ };
51
+
52
+ const running = new Map(); // job id → child
53
+ let draining = false;
54
+ const shred = (dir) => { try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ } };
55
+
56
+ const runJob = async (claim) => {
57
+ const job = claim.job;
58
+ // The take's own door, with the job's cloud key (what a laptop's listener does with its own key).
59
+ const jobApi = async (body) => { try { const r = await fetch(`${base}/api/recorder/listen`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${claim.key}` }, body: JSON.stringify({ job_id: job.id, ...body }), signal: AbortSignal.timeout(20000) }); return r.ok ? await r.json() : null; } catch { return null; } };
60
+ const dir = path.join(HOME, job.id);
61
+ const rec = path.join(dir, ".recorder");
62
+ const profile = path.join(dir, "profile");
63
+ const ledger = path.join(dir, "ledger.json");
64
+ const takeDir = path.join(rec, "takes", job.id);
65
+ const t0 = Date.now();
66
+ try {
67
+ fs.mkdirSync(takeDir, { recursive: true });
68
+ // The job's cloud key: the person's authority to put the take in their workspace, for this folder only.
69
+ fs.writeFileSync(path.join(rec, "config.json"), JSON.stringify({ base, api_key: claim.key, workspace: claim.workspace ?? null, pool: true }), { mode: 0o600 });
70
+ fs.writeFileSync(path.join(takeDir, "job.json"), JSON.stringify({ id: job.id, brief: job.brief, url: job.url ?? null, model: job.model ?? null, review_storyboard: job.review_storyboard !== false, code_repo: job.code_repo ?? null, retake_of: job.retake_of ?? null, retake_note: job.retake_note ?? null, storyboard: job.storyboard ?? null, cloud: true }, null, 2));
71
+ // The person's session → a profile of this take's own; the ledger says the site is signed in so no window opens.
72
+ const origins = {};
73
+ for (const o of claim.session?.origins ?? []) if (o?.origin) origins[o.origin] = new Date().toISOString();
74
+ try { if (job.url) origins[new URL(job.url).origin] = origins[new URL(job.url).origin] ?? new Date().toISOString(); } catch { /* no url */ }
75
+ fs.writeFileSync(ledger, JSON.stringify({ origins, who: {} }, null, 2));
76
+ const sessionFile = path.join(dir, "session.json");
77
+ fs.writeFileSync(sessionFile, JSON.stringify(claim.session ?? { cookies: [], origins: [] }), { mode: 0o600 });
78
+ console.log(`\n ▶ ${stamp()} job ${job.id} (${claim.workspace?.name ?? "workspace"}): ${String(job.brief).slice(0, 90)}…`);
79
+ await new Promise((resolve) => { const s = spawn(process.execPath, [path.join(HERE, "seed-profile.mjs"), profile, sessionFile], { stdio: "inherit", cwd: dir }); s.on("close", resolve); });
80
+ shred(sessionFile);
81
+ const child = spawn(process.execPath, [path.join(HERE, "plan.mjs"), takeDir], {
82
+ cwd: dir, stdio: "inherit",
83
+ env: { ...process.env, RECORDER_JOB: job.id, RECORDER_BASE: base, RECORDER_LISTEN: "1", RECORDER_PROFILE: profile, RECORDER_LEDGER: ledger, DEMOBITES_NO_CODE: "1", RECORDER_POOL: instanceId },
84
+ detached: process.platform !== "win32",
85
+ });
86
+ running.set(job.id, child);
87
+ const code = await new Promise((resolve) => child.on("close", resolve));
88
+ const st = await jobApi({ action: "poll" });
89
+ if (st && st.status && !["ready", "failed", "cancelled"].includes(st.status)) await jobApi({ action: "event", kind: "failed", payload: { message: code === 0 ? "The demo did not come through — try again" : `The take stopped (exit ${code})` } });
90
+ console.log(` ■ ${stamp()} job ${job.id} ${st?.status ?? "ended"} in ${Math.round((Date.now() - t0) / 1000)} s\n`);
91
+ } catch (e) {
92
+ console.error(` ! ${stamp()} job ${job.id}: ${String(e?.message ?? e).split("\n")[0]}`);
93
+ await jobApi({ action: "event", kind: "failed", payload: { message: `The cloud recorder could not run the take (${String(e?.message ?? e).slice(0, 120)})` } });
94
+ } finally {
95
+ running.delete(job.id);
96
+ shred(dir); // the key, the session, the profile, the take: gone with the folder
97
+ await api({ action: "checkin", busy: [...running.keys()], draining });
98
+ }
99
+ };
100
+
101
+ const bye = () => { draining = true; for (const c of running.values()) { try { process.kill(process.platform === "win32" ? c.pid : -c.pid, "SIGTERM"); } catch { /* gone */ } } setTimeout(() => process.exit(0), 1500); };
102
+ process.on("SIGTERM", bye); process.on("SIGINT", bye);
103
+
104
+ console.log(`demobite pool ${version} — ${name} (${instanceType}), ${slots} cameras, ${base}`);
105
+ for (;;) {
106
+ const r = await api({ action: "checkin", busy: [...running.keys()], draining });
107
+ if (r?.drain && !draining) { draining = true; console.log(` ${stamp()} drain: no new takes; ${running.size} still running`); }
108
+ if (r?.stop && running.size === 0) { console.log(` ${stamp()} stop: the pool says goodbye`); process.exit(0); }
109
+ if (!draining && running.size < slots) {
110
+ const c = await api({ action: "claim" });
111
+ if (c?.job && c?.key) { void runJob(c); await new Promise((s) => setTimeout(s, 1500)); continue; }
112
+ }
113
+ await new Promise((s) => setTimeout(s, 5000));
114
+ }
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ // SEED A PROFILE FROM A SESSION (the cloud pool, 2026-10-18): the person's session (a Playwright storageState:
3
+ // cookies and local storage, exported from their own machine with their consent) becomes a fresh Chrome profile
4
+ // the planner's look, probe and the camera all drive. Nothing else goes in; the folder is shredded after the take.
5
+ // usage: node seed-profile.mjs <profileDir> <session.json>
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ const [profileDir, sessionFile] = process.argv.slice(2);
9
+ if (!profileDir || !sessionFile) { console.error("usage: seed-profile.mjs <profileDir> <session.json>"); process.exit(2); }
10
+ const state = JSON.parse(fs.readFileSync(sessionFile, "utf8"));
11
+ const { chromium } = await import("playwright");
12
+ fs.mkdirSync(profileDir, { recursive: true });
13
+ const ctx = await chromium.launchPersistentContext(path.resolve(profileDir), { channel: "chrome", headless: true, viewport: { width: 1280, height: 800 } });
14
+ try {
15
+ const cookies = (state.cookies ?? []).map((c) => ({ ...c, sameSite: ["Strict", "Lax", "None"].includes(c.sameSite) ? c.sameSite : "Lax" }));
16
+ if (cookies.length) await ctx.addCookies(cookies);
17
+ const page = await ctx.newPage();
18
+ for (const o of state.origins ?? []) {
19
+ if (!o?.origin || !o.localStorage?.length) continue;
20
+ try {
21
+ await page.goto(o.origin, { waitUntil: "domcontentloaded", timeout: 20000 });
22
+ await page.evaluate((items) => { for (const { name, value } of items) window.localStorage.setItem(name, value); }, o.localStorage);
23
+ } catch (e) { console.log(`seed: local storage for ${o.origin} skipped (${String(e?.message ?? e).split("\n")[0]})`); }
24
+ }
25
+ console.log(`seeded ${cookies.length} cookies and ${(state.origins ?? []).reduce((n, o) => n + (o.localStorage?.length ?? 0), 0)} local-storage keys`);
26
+ } finally { await ctx.close(); }