demobites 1.24.0 → 1.26.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.
@@ -510,10 +510,10 @@ if (arg === "listen") {
510
510
  writeJson(LOCK, { pid: process.pid, since: new Date().toISOString(), cwd: process.cwd(), service: SERVICE, repo });
511
511
  const noteState = (extra) => writeJson(STATE, { at: new Date().toISOString(), machine_id: listen.machine_id, name, base, workspace: cfg.workspace?.name ?? null, ...extra });
512
512
  const readiness = () => {
513
- let signins = [];
514
- try { signins = Object.keys(JSON.parse(fs.readFileSync(path.join(process.cwd(), ".recorder", "signins.json"), "utf8")).origins || {}); } catch { /* none */ }
513
+ let signins = [], whos = {};
514
+ try { const l = JSON.parse(fs.readFileSync(path.join(process.cwd(), ".recorder", "signins.json"), "utf8")); signins = Object.keys(l.origins || {}); whos = l.who && typeof l.who === "object" ? l.who : {}; } catch { /* none */ }
515
515
  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 }));
516
- return { ...agentInfo, chrome: haveChrome, recorder: pkg.version, signins, cwd: process.cwd(), platform: process.platform, repo, repos };
516
+ return { ...agentInfo, chrome: haveChrome, recorder: pkg.version, signins, whos, cwd: process.cwd(), platform: process.platform, repo, repos };
517
517
  };
518
518
  // The code bound to a job's site (1.22.0): the planner reads there.
519
519
  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; } };
@@ -700,7 +700,7 @@ if (arg === "listen") {
700
700
  await event(job.id, "started", { machine: name });
701
701
  const takeDir = path.join(process.cwd(), ".recorder", "takes", job.id);
702
702
  fs.mkdirSync(takeDir, { recursive: true });
703
- 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");
703
+ 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, code_repo: job.code_repo ?? null, ...(job.retake_of ? { retake_of: job.retake_of, retake_note: job.retake_note ?? null, storyboard: job.storyboard ?? null } : {}) }, null, 2) + "\n");
704
704
  // Spawned async (1.14.1): a spawnSync here blocked the event loop, so the 5 s heartbeat never fired
705
705
  // during a take and the rail called the machine offline mid-recording (founder 2026-10-15 17:39).
706
706
  const bound = job.url ? codeFor(job.url) : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "demobites",
3
- "version": "1.24.0",
3
+ "version": "1.26.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"
@@ -63,10 +63,23 @@ const rules = (() => { try { return JSON.parse(fs.readFileSync(path.resolve(".re
63
63
  // THE CODE FOLDER (1.22.0, founder 2026-10-16): the folder bound to this site with `demobite repo <site>`
64
64
  // (the listener passes it as DEMOBITES_CODE_DIR), else the folder the listener runs in.
65
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;
66
+ // THE CODE ON GITHUB (1.25.0, founder 2026-10-17: "the game's going to be only GitHub"): when the job carries the
67
+ // repository the workspace chose for its site, the files come from GitHub through the server (read-only, per
68
+ // call, never stored here); the folder on this machine is not used for that site.
69
+ const ghRepo = typeof job.code_repo === "string" && job.code_repo ? job.code_repo : null;
70
+ const localOn = !process.env.DEMOBITES_NO_CODE && spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { encoding: "utf8", cwd: CODE_DIR }).status === 0;
71
+ const codeOn = Boolean(ghRepo) || localOn;
72
+ const codeApi = async (body) => {
73
+ try {
74
+ const res = await fetch(`${base}/api/recorder/agent/code`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.api_key}` }, body: JSON.stringify({ job_id: process.env.RECORDER_JOB, ...body }) });
75
+ const d = await res.json().catch(() => ({}));
76
+ return typeof d.text === "string" ? d.text : `(GitHub could not be read${d.error ? `: ${d.error}` : ""})`;
77
+ } catch (e) { return `(GitHub could not be read: ${e.message})`; }
78
+ };
67
79
  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;
68
80
  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;
69
- const listFiles = () => {
81
+ const listFiles = async () => {
82
+ if (ghRepo) return codeApi({ action: "list" });
70
83
  if (!codeOn) return "(no repository here, or code sharing is off)";
71
84
  const r = spawnSync("git", ["ls-files"], { encoding: "utf8", maxBuffer: 32 * 1024 * 1024, cwd: CODE_DIR });
72
85
  if (r.status !== 0) return "(no repository here)";
@@ -77,7 +90,8 @@ const listFiles = () => {
77
90
  };
78
91
  let reads = 0;
79
92
  const shared = [];
80
- const readFile = (rel) => {
93
+ const readFile = async (rel) => {
94
+ if (ghRepo) return codeApi({ action: "read", path: rel });
81
95
  if (!codeOn) return "Code sharing is off on this machine.";
82
96
  if (typeof rel !== "string" || !rel.trim()) return "path required";
83
97
  const abs = path.resolve(CODE_DIR, rel);
@@ -192,14 +206,14 @@ const prune = (messages) => {
192
206
  }
193
207
  };
194
208
 
195
- const firstMessage = () => {
209
+ const firstMessage = async () => {
196
210
  const vocab = startUrl ? harvest([startUrl]) : "(no starting address was given: find it from the brief, the code or by looking)";
197
211
  const parts = [
198
212
  `## The brief\n${job.brief}`,
199
213
  `## Starting address\n${startUrl || "(none given)"}`,
200
214
  `## The app's words on the starting screen\n${vocab}`,
201
215
  `## Workspace rules (version ${rules.version || 0})\n${rules.lines?.length ? rules.lines.map((l, i) => `${i + 1}. ${l}`).join("\n") : "(none)"}`,
202
- `## The project's files${codeOn ? " (this machine sits in the app's repository; read_file is on)" : " (no code here)"}\n${listFiles()}`,
216
+ `## The project's files${ghRepo ? ` (the app's repository on GitHub, ${ghRepo}; read_file reads them there)` : codeOn ? " (this machine sits in the app's repository; read_file is on)" : " (no code here)"}\n${await listFiles()}`,
203
217
  // A RETAKE (founder 2026-10-15): the same demo again, because the app changed under it. The previous
204
218
  // storyboard is the base; the note says what to keep an eye on; the probe finds what moved.
205
219
  job.retake_of && job.storyboard
@@ -211,7 +225,7 @@ const firstMessage = () => {
211
225
 
212
226
  let storyboard = null;
213
227
  let creates = "";
214
- const messages = [{ role: "user", content: [{ type: "text", text: firstMessage() }] }];
228
+ const messages = [{ role: "user", content: [{ type: "text", text: await firstMessage() }] }];
215
229
  const plan = async () => {
216
230
  say("Writing the storyboard…");
217
231
  storyboard = null;
@@ -236,8 +250,8 @@ const plan = async () => {
236
250
  let out;
237
251
  try {
238
252
  const where = (u) => { try { const x = new URL(u); return (x.pathname === "/" ? x.host : x.pathname).slice(0, 60); } catch { return String(u || "").slice(0, 60); } };
239
- if (u.name === "list_files") { say("Listing the project's files…"); out = listFiles(); }
240
- else if (u.name === "read_file") { say(`Reading ${String(input.path || "").slice(0, 80)}…`); out = readFile(input.path); }
253
+ if (u.name === "list_files") { say(ghRepo ? `Listing the files of ${ghRepo} on GitHub…` : "Listing the project's files…"); out = await listFiles(); }
254
+ else if (u.name === "read_file") { say(`Reading ${String(input.path || "").slice(0, 80)}${ghRepo ? " on GitHub" : ""}…`); out = await readFile(input.path); }
241
255
  else if (u.name === "harvest_words") { say(`Reading the app's own words on ${(input.urls || []).length} page${(input.urls || []).length === 1 ? "" : "s"}…`); out = harvest(input.urls); }
242
256
  else if (u.name === "explore") { say(`Walking the app from ${where(input.from)}, looking for ${String(input.looking_for || "the brief's words").slice(0, 60)}…`); out = explore(input.from, input.looking_for, input.depth); }
243
257
  else if (u.name === "screenshot") { say(`Looking at ${where(input.url)}…`); out = screenshot(input.url); }
@@ -101,13 +101,54 @@ const wallCheck = async () => {
101
101
  });
102
102
  } catch { return { wall: true }; }
103
103
  };
104
+ // WHO SIGNED IN (1.26.0, founder 2026-10-17: "three different users sign in, three tabs… get some data about
105
+ // the signed-in email or username"): never a credential, only what the app itself shows once you are in —
106
+ // an email in the page, an account menu's label, the address a Google chooser was told. Best effort; null
107
+ // is fine. It goes to the ledger beside the origin and to the app as `who`.
108
+ let chooserEmail = null;
109
+ const whoAmI = async () => {
110
+ try {
111
+ await page.waitForTimeout(2500);
112
+ const found = await page.evaluate(() => {
113
+ const EMAIL = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i;
114
+ const pick = (s) => { const m = String(s || "").match(EMAIL); return m ? m[0] : null; };
115
+ const attrs = [];
116
+ for (const el of document.querySelectorAll("[aria-label], [title], img[alt], [data-email], [data-user-email]")) {
117
+ for (const a of ["aria-label", "title", "alt", "data-email", "data-user-email"]) { const v = el.getAttribute(a); if (v) attrs.push(v); }
118
+ }
119
+ for (const v of attrs) { const e = pick(v); if (e) return { email: e }; }
120
+ const text = document.body?.innerText || "";
121
+ const e = pick(text);
122
+ if (e) return { email: e };
123
+ for (const v of attrs) if (/account|profile|avatar|signed in as|logged in as|user menu/i.test(v)) { const name = v.replace(/^(open|toggle)\s+/i, "").replace(/\s*(menu|avatar|profile|account)\s*$/i, "").trim(); if (name && name.length <= 60 && !/^(account|profile|avatar|user)$/i.test(name)) return { name }; }
124
+ return null;
125
+ });
126
+ if (found?.email) return found.email.toLowerCase();
127
+ if (chooserEmail) return chooserEmail;
128
+ if (found?.name) return found.name;
129
+ } catch { /* nothing */ }
130
+ return chooserEmail;
131
+ };
132
+ const noteChooser = async (here) => {
133
+ if (!/accounts\.google\.com|login\.microsoftonline\.com|okta\.com|auth0\.com/i.test(here.host)) return;
134
+ try {
135
+ const v = await page.evaluate(() => {
136
+ const el = document.querySelector("[data-identifier], [data-email], input[type=email], #identifierId, input[name=loginfmt]");
137
+ const s = el?.getAttribute?.("data-identifier") || el?.getAttribute?.("data-email") || el?.value || "";
138
+ const m = String(s).match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i); return m ? m[0] : null;
139
+ });
140
+ if (v) chooserEmail = v.toLowerCase();
141
+ } catch { /* nothing */ }
142
+ };
143
+ const remember = (origin, who) => { try { const l = readLedger(); l.origins = { ...(l.origins ?? {}), [origin]: new Date().toISOString() }; if (who) l.who = { ...(l.who ?? {}), [origin]: who }; fs.mkdirSync(path.dirname(ledgerPath), { recursive: true }); fs.writeFileSync(ledgerPath, JSON.stringify(l, null, 2) + "\n"); } catch { /* the session still lives in the profile */ } };
104
144
  await page.waitForTimeout(4000);
105
145
  const first = await wallCheck();
106
146
  if (!first.wall) {
107
147
  const origin = new URL(page.url()).origin;
108
148
  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 });
149
+ const who = await whoAmI();
150
+ remember(origin, who);
151
+ await postEvent("signed_in", { origin, none: true, ...(who ? { who } : {}) });
111
152
  await ctx.close();
112
153
  process.exit(0);
113
154
  }
@@ -119,10 +160,12 @@ while (Date.now() - started < timeoutMs) {
119
160
  await page.waitForTimeout(1000);
120
161
  let here;
121
162
  try { here = new URL(page.url()); } catch { continue; }
122
- if (here.pathname !== startPath && !/sign-?in|log-?in|auth|sso|oauth|callback/i.test(here.pathname)) {
123
- console.log(`✓ Signed in. The session is saved for ${here.origin}; next time there is no question and no window.`);
124
- await postEvent("signed_in", { origin: here.origin });
125
- try { const l = readLedger(); l.origins = { ...(l.origins ?? {}), [here.origin]: new Date().toISOString() }; fs.mkdirSync(path.dirname(ledgerPath), { recursive: true }); fs.writeFileSync(ledgerPath, JSON.stringify(l, null, 2) + "\n"); } catch { /* the session still lives in the profile */ }
163
+ await noteChooser(here);
164
+ if (here.pathname !== startPath && !/sign-?in|log-?in|auth|sso|oauth|callback/i.test(here.pathname) && !/accounts\.google\.com|login\.microsoftonline\.com|okta\.com|auth0\.com/i.test(here.host)) {
165
+ const who = await whoAmI();
166
+ console.log(`✓ Signed in${who ? ` as ${who}` : ""}. The session is saved for ${here.origin}; next time there is no question and no window.`);
167
+ await postEvent("signed_in", { origin: here.origin, ...(who ? { who } : {}) });
168
+ remember(here.origin, who);
126
169
  await ctx.close();
127
170
  process.exit(0);
128
171
  }