demobite 1.19.0 → 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.
- package/launcher/index.mjs +17 -5
- package/package.json +1 -1
- package/skill/scripts/signout.mjs +81 -0
package/launcher/index.mjs
CHANGED
|
@@ -323,7 +323,15 @@ if (arg === "on") {
|
|
|
323
323
|
spawnSync("launchctl", ["bootout", `gui/${process.getuid()}/${SERVICE_LABEL}`], { stdio: "ignore" });
|
|
324
324
|
killListener();
|
|
325
325
|
fs.writeFileSync(def.file, plist);
|
|
326
|
-
|
|
326
|
+
// launchd can refuse a bootstrap in the second after a bootout of the same label (I/O error while the
|
|
327
|
+
// old job is torn down; seen 2026-10-16 right after an update): a few tries, a second apart.
|
|
328
|
+
let r = null;
|
|
329
|
+
for (let i = 0; i < 5; i++) {
|
|
330
|
+
r = spawnSync("launchctl", ["bootstrap", `gui/${process.getuid()}`, def.file], { encoding: "utf8" });
|
|
331
|
+
if (r.status === 0) break;
|
|
332
|
+
spawnSync("launchctl", ["bootout", `gui/${process.getuid()}/${SERVICE_LABEL}`], { stdio: "ignore" });
|
|
333
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000);
|
|
334
|
+
}
|
|
327
335
|
if (r.status !== 0) { warn(`launchctl refused the service: ${(r.stderr || r.stdout || "").trim()}`); process.exit(1); }
|
|
328
336
|
} else if (process.platform === "linux") {
|
|
329
337
|
const unit = `[Unit]\nDescription=DemoBites listener\nAfter=network-online.target\n\n[Service]\nExecStart=${args.map((a) => JSON.stringify(a)).join(" ")}\nWorkingDirectory=${cwd}\nEnvironment=PATH=${pathEnv}\n${process.env.DEMOBITE_HOME ? `Environment=DEMOBITE_HOME=${HOME}\n` : ""}Restart=always\nRestartSec=10\nStandardOutput=append:${LOG}\nStandardError=append:${LOG}\n\n[Install]\nWantedBy=default.target\n`;
|
|
@@ -554,8 +562,9 @@ if (arg === "listen") {
|
|
|
554
562
|
// in the app (look phrases), its verdict posted as the job's result.
|
|
555
563
|
const runLookOrSignin = async (job) => {
|
|
556
564
|
const isLook = job.kind === "look";
|
|
557
|
-
const
|
|
558
|
-
|
|
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}`);
|
|
559
568
|
noteState({ busy: true, job: job.id });
|
|
560
569
|
await event(job.id, "started", { machine: name });
|
|
561
570
|
// Alive while the person signs in (a window can wait minutes): the heartbeat, or the rail calls this machine offline.
|
|
@@ -582,15 +591,18 @@ if (arg === "listen") {
|
|
|
582
591
|
if (isLook) {
|
|
583
592
|
if (verdict) await event(job.id, "looked", verdict);
|
|
584
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" });
|
|
585
597
|
} else if (code === 0) await event(job.id, "ready", { signedIn: true, url: job.url });
|
|
586
598
|
else await event(job.id, "failed", { message: "The sign-in did not complete" });
|
|
587
|
-
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`);
|
|
588
600
|
waitOn();
|
|
589
601
|
};
|
|
590
602
|
|
|
591
603
|
const runJob = async (job) => {
|
|
592
604
|
waitOff();
|
|
593
|
-
if (job.kind === "look" || job.kind === "signin") return runLookOrSignin(job);
|
|
605
|
+
if (job.kind === "look" || job.kind === "signin" || job.kind === "signout") return runLookOrSignin(job);
|
|
594
606
|
const model = typeof job.model === "string" && job.model ? job.model : JOB_MODEL;
|
|
595
607
|
// Alive while filming (1.13.1): a heartbeat every 5 s so the rail never shows this machine
|
|
596
608
|
// offline mid-take (the gap after the last step used to do exactly that).
|
package/package.json
CHANGED
|
@@ -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);
|