demobite 1.9.0 → 1.10.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.
@@ -190,9 +190,12 @@ if (arg === "listen") {
190
190
  const baseFlag = argvRest[argvRest.indexOf("--base") + 1];
191
191
  let cfg = readCfg();
192
192
  const base = (argvRest.includes("--base") && baseFlag ? baseFlag : cfg?.base || "https://app.demobites.com").replace(/\/+$/, "");
193
- if (!cfg?.api_key || (argvRest.includes("--base") && cfg?.base && cfg.base.replace(/\/+$/, "") !== base)) {
194
- console.log("\n Connecting this project to your DemoBites account first…\n");
195
- const r = spawnSync("node", [path.join(dest, "scripts", "login.mjs"), base], { stdio: "inherit", cwd: process.cwd() });
193
+ // --connect <code>: the line the Agentic Capture chat gave — already approved
194
+ // for the signed-in person; connects this machine without a page or a click.
195
+ const connectCode = argvRest.includes("--connect") ? (argvRest[argvRest.indexOf("--connect") + 1] || "") : "";
196
+ if (connectCode || !cfg?.api_key || (argvRest.includes("--base") && cfg?.base && cfg.base.replace(/\/+$/, "") !== base)) {
197
+ console.log(`\n Connecting this ${connectCode ? "machine" : "project"} to your DemoBites account first…\n`);
198
+ const r = spawnSync("node", [path.join(dest, "scripts", "login.mjs"), base, ...(connectCode ? ["--connect", connectCode] : [])], { stdio: "inherit", cwd: process.cwd() });
196
199
  if (r.status !== 0) process.exit(r.status ?? 1);
197
200
  cfg = readCfg();
198
201
  }
@@ -207,10 +210,18 @@ if (arg === "listen") {
207
210
  }
208
211
  const name = (argvRest[argvRest.indexOf("--name") + 1] && argvRest.includes("--name")) ? argvRest[argvRest.indexOf("--name") + 1] : os.hostname().replace(/\.local$/, "");
209
212
  const agentBin = (() => { const r = spawnSync(process.platform === "win32" ? "where" : "which", ["claude"], { encoding: "utf8" }); return r.status === 0 ? r.stdout.trim().split("\n")[0] : null; })();
213
+ // The repo this listener was started in (the Code signal on the rail): the
214
+ // git remote's owner/name, or null when the folder is not a repo.
215
+ const repo = (() => {
216
+ const r = spawnSync("git", ["config", "--get", "remote.origin.url"], { encoding: "utf8", cwd: process.cwd() });
217
+ if (r.status !== 0) return null;
218
+ const m = r.stdout.trim().match(/[:/]([^/:]+\/[^/]+?)(?:\.git)?$/);
219
+ return m ? m[1] : null;
220
+ })();
210
221
  const readiness = () => {
211
222
  let signins = [];
212
223
  try { signins = Object.keys(JSON.parse(fs.readFileSync(path.join(process.cwd(), ".recorder", "signins.json"), "utf8")).origins || {}); } catch { /* none */ }
213
- return { agent: agentBin ? "Claude Code" : null, chrome: haveChrome, recorder: pkg.version, signins, cwd: process.cwd(), platform: process.platform };
224
+ return { agent: agentBin ? "Claude Code" : null, chrome: haveChrome, recorder: pkg.version, signins, cwd: process.cwd(), platform: process.platform, repo };
214
225
  };
215
226
  const api = async (body) => {
216
227
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "demobite",
3
- "version": "1.9.0",
3
+ "version": "1.10.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"
@@ -27,7 +27,8 @@ let cfgParseFailed = false;
27
27
  try { cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8")); } catch (e) {
28
28
  cfgParseFailed = fs.existsSync(cfgPath);
29
29
  }
30
- const argv = process.argv.slice(2).filter((a) => a !== "--logout");
30
+ const connectIdx = process.argv.indexOf("--connect");
31
+ const argv = process.argv.slice(2).filter((a, i) => a !== "--logout" && a !== "--connect" && i + 2 !== connectIdx + 1);
31
32
  const wantLogout = process.argv.includes("--logout");
32
33
  // For LOGOUT the key's own home wins: the key was minted on cfg.base, so a
33
34
  // CLI base argument must not point the revoke at a different host (review
@@ -75,43 +76,56 @@ if (wantLogout) {
75
76
 
76
77
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
77
78
 
78
- let start;
79
- try {
80
- start = await fetch(`${base}/api/recorder/device`, {
81
- method: "POST",
82
- headers: { "Content-Type": "application/json" },
83
- body: "{}",
84
- });
85
- } catch (e) {
86
- console.error(`Could not reach ${base}: ${e.message}`);
87
- process.exit(1);
88
- }
89
- if (!start.ok) {
90
- console.error(`Device link start failed: ${start.status} ${await start.text().catch(() => "")}`);
91
- process.exit(1);
92
- }
93
- const { device_code, user_code, verification_url, expires_in, interval } = await start.json();
94
- if (!device_code || !user_code || !verification_url) {
95
- console.error("Device link response missing fields.");
96
- process.exit(1);
79
+ // --connect <code> (Agentic Capture chat, 2026-10-15): the app already approved
80
+ // this code for the signed-in person, so there is no page to open and nothing
81
+ // to click — poll straight away and the key arrives.
82
+ const connectAt = process.argv.indexOf("--connect");
83
+ const connectCode = connectAt >= 0 ? (process.argv[connectAt + 1] || "").trim() : "";
84
+ let device_code, expires_in, interval;
85
+ if (connectCode) {
86
+ device_code = connectCode; expires_in = 600; interval = 2;
87
+ console.log("Connecting this machine to your DemoBites account…");
88
+ } else {
89
+ let start;
90
+ try {
91
+ start = await fetch(`${base}/api/recorder/device`, {
92
+ method: "POST",
93
+ headers: { "Content-Type": "application/json" },
94
+ body: "{}",
95
+ });
96
+ } catch (e) {
97
+ console.error(`Could not reach ${base}: ${e.message}`);
98
+ process.exit(1);
99
+ }
100
+ if (!start.ok) {
101
+ console.error(`Device link start failed: ${start.status} ${await start.text().catch(() => "")}`);
102
+ process.exit(1);
103
+ }
104
+ const started = await start.json();
105
+ device_code = started.device_code; expires_in = started.expires_in; interval = started.interval;
106
+ const { user_code, verification_url } = started;
107
+ if (!device_code || !user_code || !verification_url) {
108
+ console.error("Device link response missing fields.");
109
+ process.exit(1);
110
+ }
111
+ const verifyUrl = new URL(verification_url, base).toString();
112
+ console.log("");
113
+ console.log(` Open: ${verifyUrl}`);
114
+ console.log(` Code: ${user_code}`);
115
+ console.log("");
116
+ console.log("Approve the link in your browser. If a sign-in page appears first,");
117
+ console.log("sign in — the approval page follows with the same code.");
118
+ // A real pause before the page appears (founder 2026-10-14: "we'll open a
119
+ // browser now to connect, in 3 seconds"): the human is told, then has three
120
+ // counted seconds, then the page opens. Never silent, never a question.
121
+ process.stdout.write("Opening the approval page in your browser in 3");
122
+ for (const n of [2, 1]) { await new Promise((r) => setTimeout(r, 1000)); process.stdout.write(`… ${n}`); }
123
+ await new Promise((r) => setTimeout(r, 1000));
124
+ console.log("… now. Waiting for the approval...");
125
+ if (process.platform === "darwin") spawnSync("open", [verifyUrl], { stdio: "ignore" });
126
+ else if (process.platform === "win32") spawnSync("cmd", ["/c", "start", "", verifyUrl], { stdio: "ignore" });
127
+ else spawnSync("xdg-open", [verifyUrl], { stdio: "ignore" });
97
128
  }
98
- const verifyUrl = new URL(verification_url, base).toString();
99
- console.log("");
100
- console.log(` Open: ${verifyUrl}`);
101
- console.log(` Code: ${user_code}`);
102
- console.log("");
103
- console.log("Approve the link in your browser. If a sign-in page appears first,");
104
- console.log("sign in — the approval page follows with the same code.");
105
- // A real pause before the page appears (founder 2026-10-14: "we'll open a
106
- // browser now to connect, in 3 seconds"): the human is told, then has three
107
- // counted seconds, then the page opens. Never silent, never a question.
108
- process.stdout.write("Opening the approval page in your browser in 3");
109
- for (const n of [2, 1]) { await new Promise((r) => setTimeout(r, 1000)); process.stdout.write(`… ${n}`); }
110
- await new Promise((r) => setTimeout(r, 1000));
111
- console.log("… now. Waiting for the approval...");
112
- if (process.platform === "darwin") spawnSync("open", [verifyUrl], { stdio: "ignore" });
113
- else if (process.platform === "win32") spawnSync("cmd", ["/c", "start", "", verifyUrl], { stdio: "ignore" });
114
- else spawnSync("xdg-open", [verifyUrl], { stdio: "ignore" });
115
129
 
116
130
  const deadline = Date.now() + (expires_in ?? 900) * 1000;
117
131
  const waitMs = Math.max(2, interval ?? 5) * 1000;
@@ -86,6 +86,31 @@ if (probe) {
86
86
  process.exit(state === "failed" ? 1 : 0);
87
87
  }
88
88
 
89
+ // NO WALL? (founder 2026-10-14: "open the page and look"; the first listening
90
+ // run waited four minutes on a public site). Give the page a few seconds; if
91
+ // nothing on it asks for a sign-in — no password field, no sign-in path, no
92
+ // bot check — remember the address as open and leave without a word.
93
+ const wallCheck = async () => {
94
+ try {
95
+ return await page.evaluate(() => {
96
+ const pw = document.querySelector('input[type="password"]');
97
+ const path = location.pathname.toLowerCase();
98
+ const loginPath = /sign-?in|log-?in|login|auth\b|sso|signup|register/.test(path);
99
+ const bot = !!document.querySelector('input[name="cf-turnstile-response"], iframe[src*="challenges.cloudflare"], iframe[src*="recaptcha"], iframe[src*="hcaptcha"]');
100
+ return { wall: !!pw || loginPath || bot };
101
+ });
102
+ } catch { return { wall: true }; }
103
+ };
104
+ await page.waitForTimeout(4000);
105
+ const first = await wallCheck();
106
+ if (!first.wall) {
107
+ const origin = new URL(page.url()).origin;
108
+ console.log(`✓ No sign-in needed for ${origin}.`);
109
+ try { const l = readLedger(); l.origins = { ...(l.origins ?? {}), [origin]: new Date().toISOString() }; fs.mkdirSync(path.dirname(ledgerPath), { recursive: true }); fs.writeFileSync(ledgerPath, JSON.stringify(l, null, 2) + "\n"); } catch { /* fine */ }
110
+ await postEvent("signed_in", { origin, none: true });
111
+ await ctx.close();
112
+ process.exit(0);
113
+ }
89
114
  console.log("⏳ Waiting for you — sign in in the Chrome window that just opened. Your hands only; nothing you type is seen.");
90
115
  await postEvent("signin_wait", { url });
91
116
  const started = Date.now();