demobite 1.19.1 → 1.20.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.
@@ -562,8 +562,9 @@ if (arg === "listen") {
562
562
  // in the app (look phrases), its verdict posted as the job's result.
563
563
  const runLookOrSignin = async (job) => {
564
564
  const isLook = job.kind === "look";
565
- const script = path.join(dest, "scripts", isLook ? "look.mjs" : "signin.mjs");
566
- console.log(`\n ${isLook ? "◉ Looking at" : "⚿ Sign-in at"} ${job.url}`);
565
+ const isOut = job.kind === "signout";
566
+ const script = path.join(dest, "scripts", isLook ? "look.mjs" : isOut ? "signout.mjs" : "signin.mjs");
567
+ console.log(`\n ${isLook ? "◉ Looking at" : isOut ? "⊘ Signing out of" : "⚿ Sign-in at"} ${job.url}`);
567
568
  noteState({ busy: true, job: job.id });
568
569
  await event(job.id, "started", { machine: name });
569
570
  // Alive while the person signs in (a window can wait minutes): the heartbeat, or the rail calls this machine offline.
@@ -590,15 +591,18 @@ if (arg === "listen") {
590
591
  if (isLook) {
591
592
  if (verdict) await event(job.id, "looked", verdict);
592
593
  else await event(job.id, "failed", { message: "The look did not finish" });
594
+ } else if (isOut) {
595
+ if (code === 0) await event(job.id, "ready", { signedOut: true, url: job.url, ...(verdict ?? {}) });
596
+ else await event(job.id, "failed", { message: "The sign-out did not complete" });
593
597
  } else if (code === 0) await event(job.id, "ready", { signedIn: true, url: job.url });
594
598
  else await event(job.id, "failed", { message: "The sign-in did not complete" });
595
- console.log(` ■ ${isLook ? `Look: ${verdict?.access ?? "failed"}` : code === 0 ? "Signed in." : "Sign-in did not complete."}\n`);
599
+ console.log(` ■ ${isLook ? `Look: ${verdict?.access ?? "failed"}` : isOut ? (code === 0 ? "Signed out." : "Sign-out did not complete.") : code === 0 ? "Signed in." : "Sign-in did not complete."}\n`);
596
600
  waitOn();
597
601
  };
598
602
 
599
603
  const runJob = async (job) => {
600
604
  waitOff();
601
- if (job.kind === "look" || job.kind === "signin") return runLookOrSignin(job);
605
+ if (job.kind === "look" || job.kind === "signin" || job.kind === "signout") return runLookOrSignin(job);
602
606
  const model = typeof job.model === "string" && job.model ? job.model : JOB_MODEL;
603
607
  // Alive while filming (1.13.1): a heartbeat every 5 s so the rail never shows this machine
604
608
  // offline mid-take (the gap after the last step used to do exactly that).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "demobite",
3
- "version": "1.19.1",
3
+ "version": "1.20.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
  "demobite": "launcher/index.mjs"
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ // SIGN OUT (founder 2026-10-16: "there should be an option to sign out… the credentials will be
3
+ // removed"). The recorder films in its own Chrome profile (.recorder/profile), apart from the
4
+ // person's browser. This script forgets ONE SITE in that profile: every cookie of the site and
5
+ // its subdomains, the site's local storage, and the ledger entries (.recorder/signins.json) that
6
+ // said the recorder was signed in there. The person's own browser is never touched.
7
+ //
8
+ // node scripts/signout.mjs <url or host>
9
+ //
10
+ // A site is the registrable domain (app.acme.com and auth.acme.com are one site, as the cookies
11
+ // are), so a sign-in made on one subdomain and used on another goes in one move. Headless: no
12
+ // window, nothing to click. Prints one line; RESULT {…} for the listener.
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import { postEvent } from "./listen-event.mjs";
16
+
17
+ const arg = process.argv[2];
18
+ if (!arg) { console.error("Usage: node scripts/signout.mjs <url or host>"); process.exit(2); }
19
+ let target;
20
+ try { target = new URL(/^https?:\/\//i.test(arg) ? arg : `https://${arg}`); } catch { console.error("Not an address."); process.exit(2); }
21
+
22
+ // The registrable domain, near enough: the last two labels, or three under a short second level
23
+ // (co.uk, com.au). Local names and IPs stay as they are.
24
+ const SLD = new Set(["co", "com", "net", "org", "gov", "edu", "ac"]);
25
+ export const siteOf = (hostname) => {
26
+ const h = hostname.toLowerCase();
27
+ if (/^[\d.]+$/.test(h) || h === "localhost" || !h.includes(".")) return h;
28
+ const l = h.split(".");
29
+ if (l.length >= 3 && SLD.has(l[l.length - 2]) && l[l.length - 1].length === 2) return l.slice(-3).join(".");
30
+ return l.slice(-2).join(".");
31
+ };
32
+ const site = siteOf(target.hostname);
33
+ const ofSite = (hostname) => { const h = String(hostname).replace(/^\./, "").toLowerCase(); return h === site || h.endsWith("." + site); };
34
+
35
+ const ledgerPath = path.resolve(process.cwd(), ".recorder", "signins.json");
36
+ let ledger = { origins: {} };
37
+ try { ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8")); } catch { /* none */ }
38
+ const origins = Object.keys(ledger.origins ?? {}).filter((o) => { try { return ofSite(new URL(o).hostname); } catch { return false; } });
39
+ const visit = [...new Set([target.origin, ...origins])];
40
+
41
+ const { chromium } = await import("playwright");
42
+ const profileDir = process.env.RECORDER_PROFILE || path.resolve(process.cwd(), ".recorder", "profile");
43
+ const launchOpts = {
44
+ headless: true,
45
+ viewport: { width: 1440, height: 900 },
46
+ args: ["--force-color-profile=srgb", "--disable-blink-features=AutomationControlled", "--disable-features=MediaRouter,DialMediaRouteProvider", "--disable-component-update", "--no-default-browser-check"],
47
+ ignoreDefaultArgs: ["--enable-automation"],
48
+ };
49
+ let ctx;
50
+ try { ctx = await chromium.launchPersistentContext(profileDir, { ...launchOpts, channel: "chrome" }); }
51
+ catch { ctx = await chromium.launchPersistentContext(profileDir, launchOpts); }
52
+ const page = ctx.pages()[0] ?? (await ctx.newPage());
53
+
54
+ // Storage first (a page load could set a fresh anonymous cookie), then every cookie of the site.
55
+ let stores = 0;
56
+ for (const o of visit) {
57
+ try {
58
+ await page.goto(o, { waitUntil: "domcontentloaded", timeout: 15000 });
59
+ await page.evaluate(async () => {
60
+ try { localStorage.clear(); } catch { /* opaque */ }
61
+ try { sessionStorage.clear(); } catch { /* opaque */ }
62
+ try { const dbs = (await indexedDB.databases?.()) ?? []; for (const d of dbs) if (d.name) indexedDB.deleteDatabase(d.name); } catch { /* none */ }
63
+ });
64
+ stores++;
65
+ } catch { /* unreachable now: the cookies still go */ }
66
+ }
67
+ const before = (await ctx.cookies()).filter((c) => ofSite(c.domain));
68
+ try { await ctx.clearCookies({ domain: new RegExp(`(^|\\.)${site.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "i") }); }
69
+ catch { for (const c of before) { try { await ctx.clearCookies({ name: c.name, domain: c.domain }); } catch { /* old playwright */ } } }
70
+ const after = (await ctx.cookies()).filter((c) => ofSite(c.domain));
71
+ await ctx.close();
72
+
73
+ if (origins.length) {
74
+ for (const o of origins) delete ledger.origins[o];
75
+ try { fs.writeFileSync(ledgerPath, JSON.stringify(ledger, null, 2) + "\n"); } catch { /* the cookies are gone either way */ }
76
+ }
77
+ const cleared = before.length - after.length;
78
+ console.log(`✓ Signed out of ${site} on this machine: ${cleared} cookie${cleared === 1 ? "" : "s"} cleared, ${stores} page store${stores === 1 ? "" : "s"} emptied${origins.length ? `, ${origins.length} saved sign-in${origins.length === 1 ? "" : "s"} forgotten` : ""}.`);
79
+ await postEvent("signed_out", { site, origins, cleared });
80
+ console.log("RESULT " + JSON.stringify({ site, origins, cleared, left: after.length }));
81
+ process.exit(0);