demobite 1.21.0 → 1.22.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.
@@ -121,6 +121,41 @@ const removeService = () => {
121
121
  else if (process.platform === "linux") spawnSync("systemctl", ["--user", "disable", "--now", SERVICE_UNIT], { stdio: "ignore" });
122
122
  else spawnSync("reg", ["delete", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", RUN_NAME, "/f"], { stdio: "ignore" });
123
123
  };
124
+ // ── demobite repo <site> [--forget] (1.22.0, founder 2026-10-16) ──────────────────────────────
125
+ // Run in the folder where the app's repository is: that folder becomes the code of that SITE on this
126
+ // machine. The planner reads from it when a demo records there (up to 10 files a take, each logged).
127
+ // The binding lives with the machine (~/.demobite/home/.recorder/repos.json, or the Terminal listener's
128
+ // own .recorder), and the app shows it on the bar as the Code mark.
129
+ const SLD = new Set(["co", "com", "net", "org", "gov", "edu", "ac"]);
130
+ const siteOf = (hostname) => { const h = String(hostname).toLowerCase().replace(/:\d+$/, ""); if (/^[\d.]+$/.test(h) || h === "localhost" || !h.includes(".")) return h; const l = h.split("."); return l.length >= 3 && SLD.has(l[l.length - 2]) && l[l.length - 1].length === 2 ? l.slice(-3).join(".") : l.slice(-2).join("."); };
131
+ const reposFile = (cwd) => path.join(cwd, ".recorder", "repos.json");
132
+ if (arg === "repo") {
133
+ const raw = process.argv.slice(3).find((a) => !a.startsWith("--")) || "";
134
+ const forget = process.argv.includes("--forget");
135
+ let site = "";
136
+ try { site = siteOf(new URL(/^https?:\/\//i.test(raw) ? raw : `https://${raw}`).hostname); } catch { /* below */ }
137
+ if (!site) { console.error(" Usage: demobite repo <site> [--forget] (run it in the folder where the app's repository is)\n e.g. demobite repo app.acme.com"); process.exit(2); }
138
+ const lock = readJson(LOCK);
139
+ const home = lock?.cwd && fs.existsSync(path.join(lock.cwd, ".recorder")) ? lock.cwd : SERVICE_CWD;
140
+ const file = reposFile(home);
141
+ const repos = readJson(file) ?? {};
142
+ if (forget) {
143
+ if (!repos[site]) { console.log(` Nothing bound to ${site} on this machine.`); process.exit(0); }
144
+ delete repos[site]; writeJson(file, repos);
145
+ console.log(` ○ ${site} no longer has code on this machine. The planner works from the live app there.`);
146
+ process.exit(0);
147
+ }
148
+ const folder = process.cwd();
149
+ if (spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { encoding: "utf8", cwd: folder }).status !== 0) { console.error(` This folder is not a git repository: ${folder}\n Open a Terminal in the folder where the app's repository is, then run it again.`); process.exit(1); }
150
+ const top = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", cwd: folder }).stdout.trim() || folder;
151
+ const remote = spawnSync("git", ["config", "--get", "remote.origin.url"], { encoding: "utf8", cwd: top }).stdout.trim();
152
+ const m = remote.match(/[:/]([^/:]+\/[^/]+?)(?:\.git)?$/);
153
+ const repo = m ? m[1] : path.basename(top);
154
+ repos[site] = { folder: top, repo, at: new Date().toISOString() };
155
+ writeJson(file, repos);
156
+ console.log(` ✓ The code of ${site} on this machine: ${top.replace(os.homedir(), "~")} (${repo}).\n The planner reads it when a demo records there: up to 10 files a take, each one listed in the take.\n Lines it needs pass through DemoBites to the model and are not stored. Undo: demobite repo ${site} --forget`);
157
+ process.exit(0);
158
+ }
124
159
  if (arg === "off") {
125
160
  removeService();
126
161
  const pid = killListener();
@@ -284,9 +319,15 @@ if (arg === "on") {
284
319
  const cfgAt = () => readJson(path.join(cwd, ".recorder", "config.json"));
285
320
  const base = (flag("--base") || cfgAt()?.base || "https://app.demobites.com").replace(/\/+$/, "");
286
321
  const code = flag("--connect");
287
- if (code || !cfgAt()?.api_key || (flag("--base") && cfgAt()?.base && cfgAt().base.replace(/\/+$/, "") !== base)) {
322
+ // No key and no code: the one door is the line in the app (1.21.1, after a pairing page opened in the
323
+ // wrong browser on 2026-10-16). Nothing here ever opens a browser on its own; `demobite login` is explicit.
324
+ if (!code && (!cfgAt()?.api_key || (flag("--base") && cfgAt()?.base && cfgAt().base.replace(/\/+$/, "") !== base))) {
325
+ warn("Not connected. In the app, open Create → Agentic Capture and run the line in the banner on this machine.");
326
+ process.exit(1);
327
+ }
328
+ if (code) {
288
329
  console.log("");
289
- const r = spawnSync(process.execPath, [path.join(dest, "scripts", "login.mjs"), base, ...(code ? ["--connect", code] : [])], { stdio: "inherit", cwd });
330
+ const r = spawnSync(process.execPath, [path.join(dest, "scripts", "login.mjs"), base, "--connect", code], { stdio: "inherit", cwd });
290
331
  if (r.status !== 0) process.exit(r.status ?? 1);
291
332
  }
292
333
  if (!cfgAt()?.api_key) { warn("Not connected. Run the line from the app again."); process.exit(1); }
@@ -393,9 +434,14 @@ if (arg === "listen") {
393
434
  // --connect <code>: the line the Agentic Capture chat gave — already approved
394
435
  // for the signed-in person; connects this machine without a page or a click.
395
436
  const connectCode = argvRest.includes("--connect") ? (argvRest[argvRest.indexOf("--connect") + 1] || "") : "";
396
- if (connectCode || !cfg?.api_key || (argvRest.includes("--base") && cfg?.base && cfg.base.replace(/\/+$/, "") !== base)) {
397
- console.log(`\n Connecting this ${connectCode ? "machine" : "project"} to your DemoBites account first…\n`);
398
- const r = spawnSync(process.execPath, [path.join(dest, "scripts", "login.mjs"), base, ...(connectCode ? ["--connect", connectCode] : [])], { stdio: "inherit", cwd: process.cwd() });
437
+ if (!connectCode && (!cfg?.api_key || (argvRest.includes("--base") && cfg?.base && cfg.base.replace(/\/+$/, "") !== base))) {
438
+ // Never a browser of its own (1.21.1): the line in the app, or `demobite login` by hand.
439
+ warn("Not connected. In the app, open Create → Agentic Capture and run the line in the banner on this machine, or run: demobite login");
440
+ process.exit(1);
441
+ }
442
+ if (connectCode) {
443
+ console.log(`\n Connecting this machine to your DemoBites account first…\n`);
444
+ const r = spawnSync(process.execPath, [path.join(dest, "scripts", "login.mjs"), base, "--connect", connectCode], { stdio: "inherit", cwd: process.cwd() });
399
445
  if (r.status !== 0) process.exit(r.status ?? 1);
400
446
  cfg = readCfg();
401
447
  }
@@ -465,8 +511,11 @@ if (arg === "listen") {
465
511
  const readiness = () => {
466
512
  let signins = [];
467
513
  try { signins = Object.keys(JSON.parse(fs.readFileSync(path.join(process.cwd(), ".recorder", "signins.json"), "utf8")).origins || {}); } catch { /* none */ }
468
- return { ...agentInfo, chrome: haveChrome, recorder: pkg.version, signins, cwd: process.cwd(), platform: process.platform, repo };
514
+ const repos = Object.entries(readJson(reposFile(process.cwd())) ?? {}).filter(([, v]) => v && typeof v.folder === "string" && fs.existsSync(v.folder)).map(([site, v]) => ({ site, folder: v.folder, repo: v.repo ?? null }));
515
+ return { ...agentInfo, chrome: haveChrome, recorder: pkg.version, signins, cwd: process.cwd(), platform: process.platform, repo, repos };
469
516
  };
517
+ // The code bound to a job's site (1.22.0): the planner reads there.
518
+ const codeFor = (url) => { try { const site = siteOf(new URL(url).hostname); const b = (readJson(reposFile(process.cwd())) ?? {})[site]; return b && fs.existsSync(b.folder) ? { site, ...b } : null; } catch { return null; } };
470
519
  const api = async (body) => {
471
520
  try {
472
521
  const res = await fetch(`${base}/api/recorder/listen`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.api_key}` }, body: JSON.stringify(body) });
@@ -638,7 +687,9 @@ if (arg === "listen") {
638
687
  fs.writeFileSync(path.join(takeDir, "job.json"), JSON.stringify({ id: job.id, brief: job.brief, url: job.url ?? null, model, review_storyboard: job.review_storyboard !== false, ...(job.retake_of ? { retake_of: job.retake_of, retake_note: job.retake_note ?? null, storyboard: job.storyboard ?? null } : {}) }, null, 2) + "\n");
639
688
  // Spawned async (1.14.1): a spawnSync here blocked the event loop, so the 5 s heartbeat never fired
640
689
  // during a take and the rail called the machine offline mid-recording (founder 2026-10-15 17:39).
641
- const child = (await import("node:child_process")).spawn(process.execPath, [path.join(dest, "scripts", "plan.mjs"), takeDir], { cwd: process.cwd(), stdio: "inherit", env: { ...process.env, RECORDER_JOB: job.id, RECORDER_BASE: base, RECORDER_LISTEN: "1" }, detached: process.platform !== "win32" });
690
+ const bound = job.url ? codeFor(job.url) : null;
691
+ if (bound) console.log(` Code: ${bound.repo ?? bound.site} (${bound.folder.replace(os.homedir(), "~")})`);
692
+ const child = (await import("node:child_process")).spawn(process.execPath, [path.join(dest, "scripts", "plan.mjs"), takeDir], { cwd: process.cwd(), stdio: "inherit", env: { ...process.env, RECORDER_JOB: job.id, RECORDER_BASE: base, RECORDER_LISTEN: "1", ...(bound ? { DEMOBITES_CODE_DIR: bound.folder } : {}) }, detached: process.platform !== "win32" });
642
693
  track(child);
643
694
  const code = await new Promise((resolve) => child.on("close", resolve));
644
695
  const st = await api({ action: "poll", job_id: job.id });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "demobite",
3
- "version": "1.21.0",
3
+ "version": "1.22.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"
@@ -60,12 +60,15 @@ say("Reading the workspace rules…");
60
60
  run("rules.mjs", [dir]);
61
61
  const rules = (() => { try { return JSON.parse(fs.readFileSync(path.resolve(".recorder", "rules.json"), "utf8")); } catch { return { version: 0, lines: [] }; } })();
62
62
 
63
- const codeOn = !process.env.DEMOBITES_NO_CODE && spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { encoding: "utf8" }).status === 0;
63
+ // THE CODE FOLDER (1.22.0, founder 2026-10-16): the folder bound to this site with `demobite repo <site>`
64
+ // (the listener passes it as DEMOBITES_CODE_DIR), else the folder the listener runs in.
65
+ const CODE_DIR = process.env.DEMOBITES_CODE_DIR ? path.resolve(process.env.DEMOBITES_CODE_DIR) : process.cwd();
66
+ const codeOn = !process.env.DEMOBITES_NO_CODE && spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { encoding: "utf8", cwd: CODE_DIR }).status === 0;
64
67
  const SECRET_RE = /(^|\/)(\.env(\..*)?|.*\.(pem|key|p12|pfx|crt)|.*secret.*|.*credential.*|\.recorder\/.*|node_modules\/.*|\.git\/.*|.*\.lock|package-lock\.json|yarn\.lock|pnpm-lock\.yaml)$/i;
65
68
  const UI_RE = /(^|\/)(app|pages|routes|src|components|views|screens|navigation|nav|layouts?|lib\/(nav|routes|menu))(\/|$)|(^|\/)(route|router|routes|nav|navigation|sidebar|menu|layout|page)\.[jt]sx?$|\/page\.[jt]sx?$|\/layout\.[jt]sx?$|\/route\.[jt]s$/i;
66
69
  const listFiles = () => {
67
70
  if (!codeOn) return "(no repository here, or code sharing is off)";
68
- const r = spawnSync("git", ["ls-files"], { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
71
+ const r = spawnSync("git", ["ls-files"], { encoding: "utf8", maxBuffer: 32 * 1024 * 1024, cwd: CODE_DIR });
69
72
  if (r.status !== 0) return "(no repository here)";
70
73
  const all = r.stdout.split("\n").filter(Boolean).filter((f) => !SECRET_RE.test(f) && /\.(tsx?|jsx?|vue|svelte|html|mdx?)$/i.test(f));
71
74
  const ui = all.filter((f) => UI_RE.test(f));
@@ -77,9 +80,9 @@ const shared = [];
77
80
  const readFile = (rel) => {
78
81
  if (!codeOn) return "Code sharing is off on this machine.";
79
82
  if (typeof rel !== "string" || !rel.trim()) return "path required";
80
- const abs = path.resolve(process.cwd(), rel);
81
- if (!abs.startsWith(process.cwd() + path.sep)) return "Outside the project.";
82
- const r = path.relative(process.cwd(), abs);
83
+ const abs = path.resolve(CODE_DIR, rel);
84
+ if (!abs.startsWith(CODE_DIR + path.sep)) return "Outside the project.";
85
+ const r = path.relative(CODE_DIR, abs);
83
86
  if (SECRET_RE.test(r)) return "Not that file.";
84
87
  if (reads >= 10) return "No more reads for this storyboard.";
85
88
  let text;