demobite 1.17.0 → 1.18.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 +206 -18
- package/package.json +1 -1
package/launcher/index.mjs
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// npx demobite@latest login connect this machine to DemoBites
|
|
6
6
|
// npx demobite@latest mcp register the DemoBites management MCP
|
|
7
7
|
// npx demobite@latest logout disconnect (revokes the key server-side)
|
|
8
|
+
// demobite on [--connect <code>] the listener as a LOGIN SERVICE (1.18.0): wakes with the machine, never twice
|
|
9
|
+
// demobite off | status | logs the service's switch and its truth
|
|
8
10
|
// npx demobite record <code> UPDATE RADAR (1.6.0): list a workflow's approved briefs for the agent
|
|
9
11
|
// (1.7.0: the agent then refines, asks once, gets one yes, films in parallel)
|
|
10
12
|
//
|
|
@@ -39,6 +41,80 @@ const readCfg = () => {
|
|
|
39
41
|
try { return JSON.parse(fs.readFileSync(path.join(process.cwd(), ".recorder", "config.json"), "utf8")); }
|
|
40
42
|
catch { return null; }
|
|
41
43
|
};
|
|
44
|
+
// npm next to the Node that runs us (the private runtime the install line puts under ~/.demobite), else the PATH's.
|
|
45
|
+
const npmCmd = (args) => {
|
|
46
|
+
const here = path.dirname(process.execPath);
|
|
47
|
+
for (const cli of [path.join(here, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"), path.join(here, "node_modules", "npm", "bin", "npm-cli.js")]) {
|
|
48
|
+
if (fs.existsSync(cli)) return [process.execPath, [cli, ...args]];
|
|
49
|
+
}
|
|
50
|
+
return [process.platform === "win32" ? "npm.cmd" : "npm", args];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ── THE SERVICE (founder 2026-10-16: "just like Claude Code… demobite status") ──────────────────
|
|
54
|
+
// `demobite on` installs the listener as a login service: it wakes with the machine, restarts when it
|
|
55
|
+
// dies, and needs no Terminal window. One instance ever (a lock with a live pid); `off` removes it;
|
|
56
|
+
// `status` and `logs` tell the truth. Its home is ~/.demobite: the lock, the log, the state, and the
|
|
57
|
+
// working folder the listener runs in (so .recorder/config.json lives in one known place).
|
|
58
|
+
const HOME = process.env.DEMOBITE_HOME || path.join(os.homedir(), ".demobite");
|
|
59
|
+
const SERVICE_CWD = path.join(HOME, "home");
|
|
60
|
+
const LOCK = path.join(HOME, "listen.lock");
|
|
61
|
+
const LOG = path.join(HOME, "listen.log");
|
|
62
|
+
const STATE = path.join(HOME, "state.json");
|
|
63
|
+
const CHILDREN = path.join(HOME, "children.json");
|
|
64
|
+
const SERVICE_LABEL = "com.demobites.listen";
|
|
65
|
+
const SERVICE = process.argv.includes("--service");
|
|
66
|
+
const launcherPath = fileURLToPath(import.meta.url);
|
|
67
|
+
const readJson = (f) => { try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch { return null; } };
|
|
68
|
+
const writeJson = (f, v) => { fs.mkdirSync(path.dirname(f), { recursive: true }); fs.writeFileSync(f, JSON.stringify(v, null, 2) + "\n"); };
|
|
69
|
+
const pidAlive = (pid) => { if (!pid) return false; try { process.kill(pid, 0); return true; } catch (e) { return e.code === "EPERM"; } };
|
|
70
|
+
const ago = (iso) => { const s = Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 1000)); return s < 60 ? `${s} s ago` : s < 3600 ? `${Math.round(s / 60)} min ago` : `${Math.round(s / 3600)} h ago`; };
|
|
71
|
+
const serviceDef = () => {
|
|
72
|
+
if (process.platform === "darwin") return { file: path.join(os.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`) };
|
|
73
|
+
if (process.platform === "linux") return { file: path.join(os.homedir(), ".config", "systemd", "user", "demobite.service") };
|
|
74
|
+
return { file: path.join(HOME, "service.vbs") };
|
|
75
|
+
};
|
|
76
|
+
const serviceInstalled = () => {
|
|
77
|
+
if (process.platform === "win32") { const r = spawnSync("reg", ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", "DemoBites"], { encoding: "utf8" }); return r.status === 0; }
|
|
78
|
+
return fs.existsSync(serviceDef().file);
|
|
79
|
+
};
|
|
80
|
+
const killListener = () => {
|
|
81
|
+
const lock = readJson(LOCK);
|
|
82
|
+
if (lock?.pid && pidAlive(lock.pid)) { try { process.kill(lock.pid, "SIGTERM"); } catch { /* gone */ } return lock.pid; }
|
|
83
|
+
return null;
|
|
84
|
+
};
|
|
85
|
+
const printStatus = () => {
|
|
86
|
+
const lock = readJson(LOCK);
|
|
87
|
+
const live = !!(lock?.pid && pidAlive(lock.pid));
|
|
88
|
+
const st = readJson(STATE);
|
|
89
|
+
if (live) {
|
|
90
|
+
console.log(` ● Listening as "${st?.name ?? os.hostname()}" since ${new Date(lock.since).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })} · for ${st?.workspace ?? "your workspace"} on ${(st?.base ?? "").replace(/^https?:\/\//, "") || "app.demobites.com"} (pid ${lock.pid}${lock.service ? ", login service" : ", a Terminal window"})`);
|
|
91
|
+
if (st?.at) console.log(` last check-in ${ago(st.at)} · ${st.busy ? `recording: ${st.job ?? ""}` : "idle"}${st.last_error ? ` · last error: ${st.last_error}` : ""}`);
|
|
92
|
+
console.log(` working in ${lock.cwd.replace(os.homedir(), "~")}${lock.repo ? ` · code: ${lock.repo}` : ""}`);
|
|
93
|
+
} else {
|
|
94
|
+
console.log(` ○ Not listening.${st?.at ? ` Last check-in ${ago(st.at)}.` : ""}`);
|
|
95
|
+
}
|
|
96
|
+
console.log(` login service: ${serviceInstalled() ? "installed (starts at login, restarts if it dies)" : "not installed"} · run: demobite ${live ? "off" : "on"}`);
|
|
97
|
+
console.log(` log: ${LOG.replace(os.homedir(), "~")}`);
|
|
98
|
+
};
|
|
99
|
+
if (arg === "status" && !process.argv[3]) { printStatus(); process.exit(0); }
|
|
100
|
+
if (arg === "logs") {
|
|
101
|
+
const n = Number(process.argv[3] || 40);
|
|
102
|
+
let text = ""; try { text = fs.readFileSync(LOG, "utf8"); } catch { /* none */ }
|
|
103
|
+
const lines = text.trimEnd().split("\n"); console.log(lines.slice(-n).join("\n") || " (no log yet)"); process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
if (arg === "off") {
|
|
106
|
+
const def = serviceDef();
|
|
107
|
+
if (process.platform === "darwin") spawnSync("launchctl", ["bootout", `gui/${process.getuid()}/${SERVICE_LABEL}`], { stdio: "ignore" });
|
|
108
|
+
else if (process.platform === "linux") spawnSync("systemctl", ["--user", "disable", "--now", "demobite.service"], { stdio: "ignore" });
|
|
109
|
+
else spawnSync("reg", ["delete", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", "DemoBites", "/f"], { stdio: "ignore" });
|
|
110
|
+
fs.rmSync(def.file, { force: true });
|
|
111
|
+
const pid = killListener();
|
|
112
|
+
// the browsers of an interrupted take go with it
|
|
113
|
+
for (const c of readJson(CHILDREN) ?? []) { try { process.kill(process.platform === "win32" ? c : -c, "SIGTERM"); } catch { /* gone */ } }
|
|
114
|
+
fs.rmSync(CHILDREN, { force: true });
|
|
115
|
+
console.log(` ○ Off.${pid ? ` The listener (pid ${pid}) was stopped.` : ""} The login service is removed. Your key stays; \`demobite on\` brings it back.`);
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}
|
|
42
118
|
|
|
43
119
|
console.log(`\ndemobite v${pkg.version} — the DemoBites agentic recorder\n`);
|
|
44
120
|
|
|
@@ -78,7 +154,7 @@ if (arg === "record") {
|
|
|
78
154
|
console.log("Before we record, this project needs a DemoBites account.\nRun `npx demobite login` (it prints a link to approve in your browser), then run this command again.");
|
|
79
155
|
process.exit(1);
|
|
80
156
|
}
|
|
81
|
-
const r = spawnSync(
|
|
157
|
+
const r = spawnSync(process.execPath, [briefsScript, "list", "--slug", slug], {
|
|
82
158
|
stdio: "inherit",
|
|
83
159
|
cwd: process.cwd(),
|
|
84
160
|
env: { ...process.env, DEMOBITE_CLI: "1" },
|
|
@@ -136,7 +212,7 @@ const needed = ["playwright", "@ffmpeg-installer/ffmpeg", "@ffprobe-installer/ff
|
|
|
136
212
|
);
|
|
137
213
|
if (needed.length) {
|
|
138
214
|
console.log(`\n Installing ${needed.join(", ")} (one-time)…`);
|
|
139
|
-
const r = spawnSync(
|
|
215
|
+
const r = spawnSync(...npmCmd(["install", "--prefix", dest, "--silent", "--no-audit", "--no-fund", ...needed]), { stdio: "inherit" });
|
|
140
216
|
if (r.status === 0) ok("Playwright ready");
|
|
141
217
|
else warn(`Install failed — run: npm install --prefix ~/.claude/skills/agentic-recorder ${needed.join(" ")}`);
|
|
142
218
|
}
|
|
@@ -148,7 +224,7 @@ if (needed.length) {
|
|
|
148
224
|
const pw = path.join(dest, "node_modules", "playwright", "cli.js");
|
|
149
225
|
const marker = path.join(dest, ".pw-ffmpeg-ready");
|
|
150
226
|
if (!fs.existsSync(marker) && fs.existsSync(pw)) {
|
|
151
|
-
const r = spawnSync(
|
|
227
|
+
const r = spawnSync(process.execPath, [pw, "install", "ffmpeg"], { stdio: "inherit", cwd: dest });
|
|
152
228
|
if (r.status === 0) { fs.writeFileSync(marker, new Date().toISOString()); ok("Playwright's video encoder ready"); }
|
|
153
229
|
else warn(`Playwright's ffmpeg install failed — run: node ${pw} install ffmpeg`);
|
|
154
230
|
} else if (fs.existsSync(marker)) ok("Playwright's video encoder ready");
|
|
@@ -168,7 +244,7 @@ if (!haveChrome) {
|
|
|
168
244
|
}
|
|
169
245
|
{
|
|
170
246
|
// Verify both tools actually run (compatibility, not presence).
|
|
171
|
-
const r = spawnSync(
|
|
247
|
+
const r = spawnSync(process.execPath, [path.join(dest, "scripts", "media-tools.mjs")], { encoding: "utf8" });
|
|
172
248
|
const rows = (r.stdout || "").trim().split("\n").filter(Boolean).map((l) => l.split("\t"));
|
|
173
249
|
for (const [tool, source, info] of rows) {
|
|
174
250
|
if (source === "missing") warn(`${tool}: no working build (${info})`);
|
|
@@ -178,6 +254,92 @@ if (!haveChrome) {
|
|
|
178
254
|
}
|
|
179
255
|
|
|
180
256
|
|
|
257
|
+
// ── ON: the listener as a login service (founder 2026-10-16) ─────────────────
|
|
258
|
+
// demobite on [--connect <code>] [--base <url>] [--name <machine>] [--here]
|
|
259
|
+
// Pairs the machine (the code from the app's banner, once), then writes the login entry and starts
|
|
260
|
+
// it. The listener works in ~/.demobite/home (its key and sign-ins live there) unless --here: then in
|
|
261
|
+
// this folder, which makes the folder's repo the Code the planner reads. Running it twice is harmless.
|
|
262
|
+
if (arg === "on") {
|
|
263
|
+
const rest = process.argv.slice(3);
|
|
264
|
+
const flag = (f) => rest.includes(f) ? (rest[rest.indexOf(f) + 1] || "") : "";
|
|
265
|
+
const here = rest.includes("--here");
|
|
266
|
+
const cwd = here ? process.cwd() : SERVICE_CWD;
|
|
267
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
268
|
+
fs.mkdirSync(HOME, { recursive: true });
|
|
269
|
+
const cfgAt = () => readJson(path.join(cwd, ".recorder", "config.json"));
|
|
270
|
+
const base = (flag("--base") || cfgAt()?.base || "https://app.demobites.com").replace(/\/+$/, "");
|
|
271
|
+
const code = flag("--connect");
|
|
272
|
+
if (code || !cfgAt()?.api_key || (flag("--base") && cfgAt()?.base && cfgAt().base.replace(/\/+$/, "") !== base)) {
|
|
273
|
+
console.log("\n Connecting this machine to your DemoBites account…\n");
|
|
274
|
+
const r = spawnSync(process.execPath, [path.join(dest, "scripts", "login.mjs"), base, ...(code ? ["--connect", code] : [])], { stdio: "inherit", cwd });
|
|
275
|
+
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
276
|
+
}
|
|
277
|
+
if (!cfgAt()?.api_key) { warn("Not connected. Run the line from the app again."); process.exit(1); }
|
|
278
|
+
// The service runs a stable copy of this package: the npx cache is not one. The install line puts the
|
|
279
|
+
// package under ~/.demobite/pkg already; from npx, the package is copied there.
|
|
280
|
+
let runFrom = launcherPath;
|
|
281
|
+
if (!runFrom.startsWith(HOME)) {
|
|
282
|
+
const copyTo = path.join(HOME, "pkg", "node_modules", "demobite");
|
|
283
|
+
fs.rmSync(copyTo, { recursive: true, force: true });
|
|
284
|
+
fs.mkdirSync(copyTo, { recursive: true });
|
|
285
|
+
fs.cpSync(pkgRoot, copyTo, { recursive: true, filter: (src) => !src.includes(`${path.sep}node_modules${path.sep}`) && !src.includes(`${path.sep}.git${path.sep}`) });
|
|
286
|
+
runFrom = path.join(copyTo, "launcher", "index.mjs");
|
|
287
|
+
}
|
|
288
|
+
const name = flag("--name");
|
|
289
|
+
const args = [process.execPath, runFrom, "listen", "--service", ...(name ? ["--name", name] : [])];
|
|
290
|
+
const def = serviceDef();
|
|
291
|
+
const pathEnv = [path.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"].join(":");
|
|
292
|
+
// a log that never grows past a few megabytes
|
|
293
|
+
try { if (fs.statSync(LOG).size > 5 * 1024 * 1024) fs.renameSync(LOG, LOG + ".1"); } catch { /* none */ }
|
|
294
|
+
if (process.platform === "darwin") {
|
|
295
|
+
const esc = (t) => t.replace(/&/g, "&").replace(/</g, "<");
|
|
296
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
297
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
298
|
+
<plist version="1.0"><dict>
|
|
299
|
+
<key>Label</key><string>${SERVICE_LABEL}</string>
|
|
300
|
+
<key>ProgramArguments</key><array>${args.map((a) => `<string>${esc(a)}</string>`).join("")}</array>
|
|
301
|
+
<key>WorkingDirectory</key><string>${esc(cwd)}</string>
|
|
302
|
+
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${esc(pathEnv)}</string><key>HOME</key><string>${esc(os.homedir())}</string>${process.env.DEMOBITE_HOME ? `<key>DEMOBITE_HOME</key><string>${esc(HOME)}</string>` : ""}</dict>
|
|
303
|
+
<key>RunAtLoad</key><true/>
|
|
304
|
+
<key>KeepAlive</key><true/>
|
|
305
|
+
<key>ThrottleInterval</key><integer>10</integer>
|
|
306
|
+
<key>ProcessType</key><string>Background</string>
|
|
307
|
+
<key>StandardOutPath</key><string>${esc(LOG)}</string>
|
|
308
|
+
<key>StandardErrorPath</key><string>${esc(LOG)}</string>
|
|
309
|
+
</dict></plist>
|
|
310
|
+
`;
|
|
311
|
+
fs.mkdirSync(path.dirname(def.file), { recursive: true });
|
|
312
|
+
spawnSync("launchctl", ["bootout", `gui/${process.getuid()}/${SERVICE_LABEL}`], { stdio: "ignore" });
|
|
313
|
+
killListener();
|
|
314
|
+
fs.writeFileSync(def.file, plist);
|
|
315
|
+
const r = spawnSync("launchctl", ["bootstrap", `gui/${process.getuid()}`, def.file], { encoding: "utf8" });
|
|
316
|
+
if (r.status !== 0) { warn(`launchctl refused the service: ${(r.stderr || r.stdout || "").trim()}`); process.exit(1); }
|
|
317
|
+
} else if (process.platform === "linux") {
|
|
318
|
+
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`;
|
|
319
|
+
fs.mkdirSync(path.dirname(def.file), { recursive: true });
|
|
320
|
+
fs.writeFileSync(def.file, unit);
|
|
321
|
+
killListener();
|
|
322
|
+
spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
323
|
+
const r = spawnSync("systemctl", ["--user", "enable", "--now", "demobite.service"], { encoding: "utf8" });
|
|
324
|
+
if (r.status !== 0) { warn(`systemd refused the service: ${(r.stderr || "").trim()}`); process.exit(1); }
|
|
325
|
+
} else {
|
|
326
|
+
// Windows: a Run entry that starts the listener hidden at logon (no console window), via a tiny script.
|
|
327
|
+
const q = (t) => `""${t}""`;
|
|
328
|
+
const vbs = `Set sh = CreateObject("WScript.Shell")\nsh.CurrentDirectory = "${cwd}"\nsh.Run "${args.map((a) => q(a)).join(" ")} >> ${q(LOG)} 2>&1", 0, False\n`;
|
|
329
|
+
fs.writeFileSync(def.file, vbs);
|
|
330
|
+
spawnSync("reg", ["add", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", "DemoBites", "/t", "REG_SZ", "/d", `wscript.exe "${def.file}"`, "/f"], { stdio: "ignore" });
|
|
331
|
+
killListener();
|
|
332
|
+
spawnSync("wscript.exe", [def.file], { stdio: "ignore" });
|
|
333
|
+
}
|
|
334
|
+
// a moment for the first check-in, then the truth
|
|
335
|
+
const until = Date.now() + 8000;
|
|
336
|
+
while (Date.now() < until) { const l = readJson(LOCK); if (l?.pid && pidAlive(l.pid) && l.service) break; spawnSync(process.execPath, ["-e", "setTimeout(()=>{},400)"]); }
|
|
337
|
+
console.log("");
|
|
338
|
+
printStatus();
|
|
339
|
+
console.log("\n It starts with the machine from now on. No window to keep open.\n");
|
|
340
|
+
process.exit(0);
|
|
341
|
+
}
|
|
342
|
+
|
|
181
343
|
// ── LISTENING (founder 2026-10-14 night, "YES!!!!"): `npx demobite listen`.
|
|
182
344
|
// The machine checks in to the app with the person's recorder key, pulls
|
|
183
345
|
// the jobs the Capture tab created for this owner, runs the person's own
|
|
@@ -194,11 +356,29 @@ if (arg === "listen") {
|
|
|
194
356
|
const connectCode = argvRest.includes("--connect") ? (argvRest[argvRest.indexOf("--connect") + 1] || "") : "";
|
|
195
357
|
if (connectCode || !cfg?.api_key || (argvRest.includes("--base") && cfg?.base && cfg.base.replace(/\/+$/, "") !== base)) {
|
|
196
358
|
console.log(`\n Connecting this ${connectCode ? "machine" : "project"} to your DemoBites account first…\n`);
|
|
197
|
-
const r = spawnSync(
|
|
359
|
+
const r = spawnSync(process.execPath, [path.join(dest, "scripts", "login.mjs"), base, ...(connectCode ? ["--connect", connectCode] : [])], { stdio: "inherit", cwd: process.cwd() });
|
|
198
360
|
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
199
361
|
cfg = readCfg();
|
|
200
362
|
}
|
|
201
363
|
if (!cfg?.api_key) { warn("Not connected — run: npx demobite login"); process.exit(1); }
|
|
364
|
+
// ONE LISTENER (founder 2026-10-16: "it can never run twice"): a lock with a live pid. A second start,
|
|
365
|
+
// from a Terminal or a double-pasted line, says who is listening and leaves.
|
|
366
|
+
fs.mkdirSync(HOME, { recursive: true });
|
|
367
|
+
{
|
|
368
|
+
const cur = readJson(LOCK);
|
|
369
|
+
if (cur?.pid && cur.pid !== process.pid && pidAlive(cur.pid)) {
|
|
370
|
+
console.log(` ● Already listening (pid ${cur.pid}, since ${new Date(cur.since).toLocaleTimeString()}${cur.service ? ", the login service" : ""}). Nothing to do.\n demobite status · demobite off`);
|
|
371
|
+
process.exit(0);
|
|
372
|
+
}
|
|
373
|
+
// the browsers of a listener that died hard
|
|
374
|
+
for (const c of readJson(CHILDREN) ?? []) { try { process.kill(process.platform === "win32" ? c : -c, "SIGKILL"); } catch { /* gone */ } }
|
|
375
|
+
fs.rmSync(CHILDREN, { force: true });
|
|
376
|
+
}
|
|
377
|
+
process.title = "demobite";
|
|
378
|
+
const children = new Set();
|
|
379
|
+
const track = (child) => { if (!child?.pid) return; children.add(child.pid); writeJson(CHILDREN, [...children]); child.on("close", () => { children.delete(child.pid); writeJson(CHILDREN, [...children]); }); };
|
|
380
|
+
const bye = (sig) => { for (const c of children) { try { process.kill(process.platform === "win32" ? c : -c, "SIGTERM"); } catch { /* gone */ } } try { if (readJson(LOCK)?.pid === process.pid) fs.rmSync(LOCK, { force: true }); } catch { /* fine */ } fs.rmSync(CHILDREN, { force: true }); process.exit(sig === "SIGINT" ? 130 : 0); };
|
|
381
|
+
process.on("SIGTERM", () => bye("SIGTERM")); process.on("SIGINT", () => bye("SIGINT")); process.on("SIGHUP", () => bye("SIGHUP"));
|
|
202
382
|
const listenFile = path.join(process.cwd(), ".recorder", "listen.json");
|
|
203
383
|
let listen = {};
|
|
204
384
|
try { listen = JSON.parse(fs.readFileSync(listenFile, "utf8")); } catch { /* first time */ }
|
|
@@ -241,6 +421,8 @@ if (arg === "listen") {
|
|
|
241
421
|
const m = r.stdout.trim().match(/[:/]([^/:]+\/[^/]+?)(?:\.git)?$/);
|
|
242
422
|
return m ? m[1] : null;
|
|
243
423
|
})();
|
|
424
|
+
writeJson(LOCK, { pid: process.pid, since: new Date().toISOString(), cwd: process.cwd(), service: SERVICE, repo });
|
|
425
|
+
const noteState = (extra) => writeJson(STATE, { at: new Date().toISOString(), machine_id: listen.machine_id, name, base, workspace: cfg.workspace?.name ?? null, ...extra });
|
|
244
426
|
const readiness = () => {
|
|
245
427
|
let signins = [];
|
|
246
428
|
try { signins = Object.keys(JSON.parse(fs.readFileSync(path.join(process.cwd(), ".recorder", "signins.json"), "utf8")).origins || {}); } catch { /* none */ }
|
|
@@ -249,9 +431,10 @@ if (arg === "listen") {
|
|
|
249
431
|
const api = async (body) => {
|
|
250
432
|
try {
|
|
251
433
|
const res = await fetch(`${base}/api/recorder/listen`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.api_key}` }, body: JSON.stringify(body) });
|
|
252
|
-
if (res.status === 401) { console.error("\n The recorder key was refused. Run:
|
|
434
|
+
if (res.status === 401) { console.error("\n The recorder key was refused. Run: demobite login"); noteState({ busy: false, last_error: "the recorder key was refused" }); process.exit(1); }
|
|
435
|
+
if (body.action === "checkin") noteState({ busy: !!body.busy, job: body.busy ? (readJson(STATE)?.job ?? null) : null, last_error: res.ok ? null : `the app answered ${res.status}` });
|
|
253
436
|
return res.ok ? await res.json() : null;
|
|
254
|
-
} catch { return null; }
|
|
437
|
+
} catch (e) { if (body.action === "checkin") noteState({ busy: !!body.busy, last_error: String(e?.message ?? e).slice(0, 120) }); return null; }
|
|
255
438
|
};
|
|
256
439
|
const event = (job_id, kind, payload) => api({ action: "event", job_id, kind, payload });
|
|
257
440
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -259,8 +442,9 @@ if (arg === "listen") {
|
|
|
259
442
|
console.log(`\n Listening as "${name}" for ${cfg.workspace?.name ?? "your workspace"} on ${base.replace(/^https?:\/\//, "")}.`);
|
|
260
443
|
console.log(` Ask for a demo in the app (Create → Agentic Capture). ${engine === "claude" ? (agentBin ? `Claude Code${agentInfo.agent_version ? " " + agentInfo.agent_version : ""} plans the demos, on your own plan; they film on ${JOB_MODEL} unless you pick another model in the app.` : "! --engine=claude, but Claude Code was not found on this machine.") : `Demos are planned by the DemoBites agent on ${JOB_MODEL} (pick another model in the app)${process.env.DEMOBITES_NO_CODE ? "; code sharing is off" : repo ? "; the files it reads from this project are logged in each take" : ""}.`}`);
|
|
261
444
|
// The one thing the person must know, boxed (founder 2026-10-15): this
|
|
262
|
-
// window is the machine's side of every recording.
|
|
263
|
-
console.log(`
|
|
445
|
+
// window is the machine's side of every recording. The login service has no window: one line instead.
|
|
446
|
+
if (SERVICE) console.log(` ${new Date().toISOString()} listening (login service, pid ${process.pid})`);
|
|
447
|
+
else console.log(`
|
|
264
448
|
┌──────────────────────────────────────────────────────────┐
|
|
265
449
|
│ │
|
|
266
450
|
│ LEAVE THIS WINDOW OPEN │
|
|
@@ -274,7 +458,7 @@ if (arg === "listen") {
|
|
|
274
458
|
// A line that visibly lives (founder: "look at the terminal, it's stuck"):
|
|
275
459
|
// a heartbeat with a clock while we wait, cleared while a job prints.
|
|
276
460
|
const startedAt = Date.now();
|
|
277
|
-
const tty = !!process.stdout.isTTY;
|
|
461
|
+
const tty = !!process.stdout.isTTY && !SERVICE;
|
|
278
462
|
let ticker = null;
|
|
279
463
|
const tick = () => {
|
|
280
464
|
const s = Math.floor((Date.now() - startedAt) / 1000);
|
|
@@ -292,9 +476,11 @@ if (arg === "listen") {
|
|
|
292
476
|
const isLook = job.kind === "look";
|
|
293
477
|
const script = path.join(dest, "scripts", isLook ? "look.mjs" : "signin.mjs");
|
|
294
478
|
console.log(`\n ${isLook ? "◉ Looking at" : "⚿ Sign-in at"} ${job.url}`);
|
|
479
|
+
noteState({ busy: true, job: job.id });
|
|
295
480
|
await event(job.id, "started", { machine: name });
|
|
296
481
|
const { spawn } = await import("node:child_process");
|
|
297
|
-
const child = spawn(
|
|
482
|
+
const child = spawn(process.execPath, [script, job.url], { cwd: process.cwd(), env: { ...process.env, RECORDER_JOB: job.id, RECORDER_BASE: base, RECORDER_LISTEN: "1" }, stdio: ["ignore", "pipe", "pipe"], detached: process.platform !== "win32" });
|
|
483
|
+
track(child);
|
|
298
484
|
let verdict = null;
|
|
299
485
|
let buf = "";
|
|
300
486
|
child.stdout.on("data", (d) => {
|
|
@@ -325,6 +511,7 @@ if (arg === "listen") {
|
|
|
325
511
|
const model = typeof job.model === "string" && job.model ? job.model : JOB_MODEL;
|
|
326
512
|
// Alive while filming (1.13.1): a heartbeat every 5 s so the rail never shows this machine
|
|
327
513
|
// offline mid-take (the gap after the last step used to do exactly that).
|
|
514
|
+
noteState({ busy: true, job: job.id });
|
|
328
515
|
const heartbeat = setInterval(() => { void api({ action: "checkin", machine_id: listen.machine_id, name, readiness: readiness(), busy: true }); }, 5000);
|
|
329
516
|
console.log(`\n ▶ Job ${job.id}: ${job.brief.slice(0, 100)}${job.brief.length > 100 ? "…" : ""} (${engine === "claude" ? "Claude Code" : "DemoBites agent"} on ${model})`);
|
|
330
517
|
if (engine !== "claude") {
|
|
@@ -335,7 +522,8 @@ if (arg === "listen") {
|
|
|
335
522
|
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");
|
|
336
523
|
// Spawned async (1.14.1): a spawnSync here blocked the event loop, so the 5 s heartbeat never fired
|
|
337
524
|
// during a take and the rail called the machine offline mid-recording (founder 2026-10-15 17:39).
|
|
338
|
-
const child = (await import("node:child_process")).spawn(
|
|
525
|
+
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" });
|
|
526
|
+
track(child);
|
|
339
527
|
const code = await new Promise((resolve) => child.on("close", resolve));
|
|
340
528
|
const st = await api({ action: "poll", job_id: job.id });
|
|
341
529
|
if (st && !["ready", "failed", "cancelled"].includes(st.status)) await event(job.id, "failed", { message: code === 0 ? "The demo did not come through — try again" : `The take stopped (exit ${code})` });
|
|
@@ -398,7 +586,7 @@ if (arg === "login" || arg === "logout") {
|
|
|
398
586
|
// `login <base>` forwards the base (dev, a preview) to login.mjs, which
|
|
399
587
|
// resolves it before config.base and the production default. Logout
|
|
400
588
|
// forwards nothing extra: the key's own home always wins there.
|
|
401
|
-
const r = spawnSync(
|
|
589
|
+
const r = spawnSync(process.execPath, [path.join(dest, "scripts", "login.mjs"), ...(arg === "logout" ? ["--logout"] : process.argv.slice(3))], {
|
|
402
590
|
stdio: "inherit",
|
|
403
591
|
cwd: process.cwd(),
|
|
404
592
|
});
|
|
@@ -440,12 +628,12 @@ if (arg === "retake") {
|
|
|
440
628
|
let cfg = readCfg();
|
|
441
629
|
if (!cfg?.api_key) {
|
|
442
630
|
console.log("\n Not connected yet — linking this machine to DemoBites first…\n");
|
|
443
|
-
const r = spawnSync(
|
|
631
|
+
const r = spawnSync(process.execPath, [path.join(dest, "scripts", "login.mjs")], { stdio: "inherit", cwd: process.cwd() });
|
|
444
632
|
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
445
633
|
cfg = readCfg();
|
|
446
634
|
}
|
|
447
635
|
if (!cfg?.api_key) { warn("Login did not complete — run: npx demobite login"); process.exit(1); }
|
|
448
|
-
const r = spawnSync(
|
|
636
|
+
const r = spawnSync(process.execPath, [path.join(dest, "scripts", "retake.mjs"), ...process.argv.slice(3)], { stdio: "inherit", cwd: process.cwd() });
|
|
449
637
|
process.exit(r.status ?? 1);
|
|
450
638
|
}
|
|
451
639
|
|
|
@@ -456,12 +644,12 @@ if (arg === "briefs" || arg === "status" || arg === "rules") {
|
|
|
456
644
|
let cfg = readCfg();
|
|
457
645
|
if (!cfg?.api_key) {
|
|
458
646
|
console.log("\n Not connected yet — linking this machine to DemoBites first…\n");
|
|
459
|
-
const r = spawnSync(
|
|
647
|
+
const r = spawnSync(process.execPath, [path.join(dest, "scripts", "login.mjs")], { stdio: "inherit", cwd: process.cwd() });
|
|
460
648
|
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
461
649
|
cfg = readCfg();
|
|
462
650
|
}
|
|
463
651
|
if (!cfg?.api_key) { warn("Login did not complete — run: npx demobite login"); process.exit(1); }
|
|
464
|
-
const r = spawnSync(
|
|
652
|
+
const r = spawnSync(process.execPath, [path.join(dest, "scripts", `${arg}.mjs`), ...process.argv.slice(3)], { stdio: "inherit", cwd: process.cwd() });
|
|
465
653
|
process.exit(r.status ?? 1);
|
|
466
654
|
}
|
|
467
655
|
|
|
@@ -469,7 +657,7 @@ if (arg === "mcp") {
|
|
|
469
657
|
let cfg = readCfg();
|
|
470
658
|
if (!cfg?.api_key) {
|
|
471
659
|
console.log("\n Not connected yet — linking this machine to DemoBites first…\n");
|
|
472
|
-
const r = spawnSync(
|
|
660
|
+
const r = spawnSync(process.execPath, [path.join(dest, "scripts", "login.mjs")], { stdio: "inherit", cwd: process.cwd() });
|
|
473
661
|
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
474
662
|
cfg = readCfg();
|
|
475
663
|
}
|
package/package.json
CHANGED