demobite 1.17.0 → 1.18.1
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 +208 -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,82 @@ 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
|
+
// the facts come from the last check-in; before the first one, from the key's own config
|
|
89
|
+
const cfgThere = lock?.cwd ? readJson(path.join(lock.cwd, ".recorder", "config.json")) : null;
|
|
90
|
+
const st = { base: cfgThere?.base ?? null, workspace: cfgThere?.workspace?.name ?? null, ...(readJson(STATE) ?? {}) };
|
|
91
|
+
if (live) {
|
|
92
|
+
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"})`);
|
|
93
|
+
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}` : ""}`);
|
|
94
|
+
console.log(` working in ${lock.cwd.replace(os.homedir(), "~")}${lock.repo ? ` · code: ${lock.repo}` : ""}`);
|
|
95
|
+
} else {
|
|
96
|
+
console.log(` ○ Not listening.${st?.at ? ` Last check-in ${ago(st.at)}.` : ""}`);
|
|
97
|
+
}
|
|
98
|
+
console.log(` login service: ${serviceInstalled() ? "installed (starts at login, restarts if it dies)" : "not installed"} · run: demobite ${live ? "off" : "on"}`);
|
|
99
|
+
console.log(` log: ${LOG.replace(os.homedir(), "~")}`);
|
|
100
|
+
};
|
|
101
|
+
if (arg === "status" && !process.argv[3]) { printStatus(); process.exit(0); }
|
|
102
|
+
if (arg === "logs") {
|
|
103
|
+
const n = Number(process.argv[3] || 40);
|
|
104
|
+
let text = ""; try { text = fs.readFileSync(LOG, "utf8"); } catch { /* none */ }
|
|
105
|
+
const lines = text.trimEnd().split("\n"); console.log(lines.slice(-n).join("\n") || " (no log yet)"); process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
if (arg === "off") {
|
|
108
|
+
const def = serviceDef();
|
|
109
|
+
if (process.platform === "darwin") spawnSync("launchctl", ["bootout", `gui/${process.getuid()}/${SERVICE_LABEL}`], { stdio: "ignore" });
|
|
110
|
+
else if (process.platform === "linux") spawnSync("systemctl", ["--user", "disable", "--now", "demobite.service"], { stdio: "ignore" });
|
|
111
|
+
else spawnSync("reg", ["delete", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", "DemoBites", "/f"], { stdio: "ignore" });
|
|
112
|
+
fs.rmSync(def.file, { force: true });
|
|
113
|
+
const pid = killListener();
|
|
114
|
+
// the browsers of an interrupted take go with it
|
|
115
|
+
for (const c of readJson(CHILDREN) ?? []) { try { process.kill(process.platform === "win32" ? c : -c, "SIGTERM"); } catch { /* gone */ } }
|
|
116
|
+
fs.rmSync(CHILDREN, { force: true });
|
|
117
|
+
console.log(` ○ Off.${pid ? ` The listener (pid ${pid}) was stopped.` : ""} The login service is removed. Your key stays; \`demobite on\` brings it back.`);
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
42
120
|
|
|
43
121
|
console.log(`\ndemobite v${pkg.version} — the DemoBites agentic recorder\n`);
|
|
44
122
|
|
|
@@ -78,7 +156,7 @@ if (arg === "record") {
|
|
|
78
156
|
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
157
|
process.exit(1);
|
|
80
158
|
}
|
|
81
|
-
const r = spawnSync(
|
|
159
|
+
const r = spawnSync(process.execPath, [briefsScript, "list", "--slug", slug], {
|
|
82
160
|
stdio: "inherit",
|
|
83
161
|
cwd: process.cwd(),
|
|
84
162
|
env: { ...process.env, DEMOBITE_CLI: "1" },
|
|
@@ -136,7 +214,7 @@ const needed = ["playwright", "@ffmpeg-installer/ffmpeg", "@ffprobe-installer/ff
|
|
|
136
214
|
);
|
|
137
215
|
if (needed.length) {
|
|
138
216
|
console.log(`\n Installing ${needed.join(", ")} (one-time)…`);
|
|
139
|
-
const r = spawnSync(
|
|
217
|
+
const r = spawnSync(...npmCmd(["install", "--prefix", dest, "--silent", "--no-audit", "--no-fund", ...needed]), { stdio: "inherit" });
|
|
140
218
|
if (r.status === 0) ok("Playwright ready");
|
|
141
219
|
else warn(`Install failed — run: npm install --prefix ~/.claude/skills/agentic-recorder ${needed.join(" ")}`);
|
|
142
220
|
}
|
|
@@ -148,7 +226,7 @@ if (needed.length) {
|
|
|
148
226
|
const pw = path.join(dest, "node_modules", "playwright", "cli.js");
|
|
149
227
|
const marker = path.join(dest, ".pw-ffmpeg-ready");
|
|
150
228
|
if (!fs.existsSync(marker) && fs.existsSync(pw)) {
|
|
151
|
-
const r = spawnSync(
|
|
229
|
+
const r = spawnSync(process.execPath, [pw, "install", "ffmpeg"], { stdio: "inherit", cwd: dest });
|
|
152
230
|
if (r.status === 0) { fs.writeFileSync(marker, new Date().toISOString()); ok("Playwright's video encoder ready"); }
|
|
153
231
|
else warn(`Playwright's ffmpeg install failed — run: node ${pw} install ffmpeg`);
|
|
154
232
|
} else if (fs.existsSync(marker)) ok("Playwright's video encoder ready");
|
|
@@ -168,7 +246,7 @@ if (!haveChrome) {
|
|
|
168
246
|
}
|
|
169
247
|
{
|
|
170
248
|
// Verify both tools actually run (compatibility, not presence).
|
|
171
|
-
const r = spawnSync(
|
|
249
|
+
const r = spawnSync(process.execPath, [path.join(dest, "scripts", "media-tools.mjs")], { encoding: "utf8" });
|
|
172
250
|
const rows = (r.stdout || "").trim().split("\n").filter(Boolean).map((l) => l.split("\t"));
|
|
173
251
|
for (const [tool, source, info] of rows) {
|
|
174
252
|
if (source === "missing") warn(`${tool}: no working build (${info})`);
|
|
@@ -178,6 +256,92 @@ if (!haveChrome) {
|
|
|
178
256
|
}
|
|
179
257
|
|
|
180
258
|
|
|
259
|
+
// ── ON: the listener as a login service (founder 2026-10-16) ─────────────────
|
|
260
|
+
// demobite on [--connect <code>] [--base <url>] [--name <machine>] [--here]
|
|
261
|
+
// Pairs the machine (the code from the app's banner, once), then writes the login entry and starts
|
|
262
|
+
// it. The listener works in ~/.demobite/home (its key and sign-ins live there) unless --here: then in
|
|
263
|
+
// this folder, which makes the folder's repo the Code the planner reads. Running it twice is harmless.
|
|
264
|
+
if (arg === "on") {
|
|
265
|
+
const rest = process.argv.slice(3);
|
|
266
|
+
const flag = (f) => rest.includes(f) ? (rest[rest.indexOf(f) + 1] || "") : "";
|
|
267
|
+
const here = rest.includes("--here");
|
|
268
|
+
const cwd = here ? process.cwd() : SERVICE_CWD;
|
|
269
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
270
|
+
fs.mkdirSync(HOME, { recursive: true });
|
|
271
|
+
const cfgAt = () => readJson(path.join(cwd, ".recorder", "config.json"));
|
|
272
|
+
const base = (flag("--base") || cfgAt()?.base || "https://app.demobites.com").replace(/\/+$/, "");
|
|
273
|
+
const code = flag("--connect");
|
|
274
|
+
if (code || !cfgAt()?.api_key || (flag("--base") && cfgAt()?.base && cfgAt().base.replace(/\/+$/, "") !== base)) {
|
|
275
|
+
console.log("\n Connecting this machine to your DemoBites account…\n");
|
|
276
|
+
const r = spawnSync(process.execPath, [path.join(dest, "scripts", "login.mjs"), base, ...(code ? ["--connect", code] : [])], { stdio: "inherit", cwd });
|
|
277
|
+
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
278
|
+
}
|
|
279
|
+
if (!cfgAt()?.api_key) { warn("Not connected. Run the line from the app again."); process.exit(1); }
|
|
280
|
+
// The service runs a stable copy of this package: the npx cache is not one. The install line puts the
|
|
281
|
+
// package under ~/.demobite/pkg already; from npx, the package is copied there.
|
|
282
|
+
let runFrom = launcherPath;
|
|
283
|
+
if (!runFrom.startsWith(HOME)) {
|
|
284
|
+
const copyTo = path.join(HOME, "pkg", "node_modules", "demobite");
|
|
285
|
+
fs.rmSync(copyTo, { recursive: true, force: true });
|
|
286
|
+
fs.mkdirSync(copyTo, { recursive: true });
|
|
287
|
+
fs.cpSync(pkgRoot, copyTo, { recursive: true, filter: (src) => !src.includes(`${path.sep}node_modules${path.sep}`) && !src.includes(`${path.sep}.git${path.sep}`) });
|
|
288
|
+
runFrom = path.join(copyTo, "launcher", "index.mjs");
|
|
289
|
+
}
|
|
290
|
+
const name = flag("--name");
|
|
291
|
+
const args = [process.execPath, runFrom, "listen", "--service", ...(name ? ["--name", name] : [])];
|
|
292
|
+
const def = serviceDef();
|
|
293
|
+
const pathEnv = [path.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"].join(":");
|
|
294
|
+
// a log that never grows past a few megabytes
|
|
295
|
+
try { if (fs.statSync(LOG).size > 5 * 1024 * 1024) fs.renameSync(LOG, LOG + ".1"); } catch { /* none */ }
|
|
296
|
+
if (process.platform === "darwin") {
|
|
297
|
+
const esc = (t) => t.replace(/&/g, "&").replace(/</g, "<");
|
|
298
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
299
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
300
|
+
<plist version="1.0"><dict>
|
|
301
|
+
<key>Label</key><string>${SERVICE_LABEL}</string>
|
|
302
|
+
<key>ProgramArguments</key><array>${args.map((a) => `<string>${esc(a)}</string>`).join("")}</array>
|
|
303
|
+
<key>WorkingDirectory</key><string>${esc(cwd)}</string>
|
|
304
|
+
<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>
|
|
305
|
+
<key>RunAtLoad</key><true/>
|
|
306
|
+
<key>KeepAlive</key><true/>
|
|
307
|
+
<key>ThrottleInterval</key><integer>10</integer>
|
|
308
|
+
<key>ProcessType</key><string>Background</string>
|
|
309
|
+
<key>StandardOutPath</key><string>${esc(LOG)}</string>
|
|
310
|
+
<key>StandardErrorPath</key><string>${esc(LOG)}</string>
|
|
311
|
+
</dict></plist>
|
|
312
|
+
`;
|
|
313
|
+
fs.mkdirSync(path.dirname(def.file), { recursive: true });
|
|
314
|
+
spawnSync("launchctl", ["bootout", `gui/${process.getuid()}/${SERVICE_LABEL}`], { stdio: "ignore" });
|
|
315
|
+
killListener();
|
|
316
|
+
fs.writeFileSync(def.file, plist);
|
|
317
|
+
const r = spawnSync("launchctl", ["bootstrap", `gui/${process.getuid()}`, def.file], { encoding: "utf8" });
|
|
318
|
+
if (r.status !== 0) { warn(`launchctl refused the service: ${(r.stderr || r.stdout || "").trim()}`); process.exit(1); }
|
|
319
|
+
} else if (process.platform === "linux") {
|
|
320
|
+
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`;
|
|
321
|
+
fs.mkdirSync(path.dirname(def.file), { recursive: true });
|
|
322
|
+
fs.writeFileSync(def.file, unit);
|
|
323
|
+
killListener();
|
|
324
|
+
spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
325
|
+
const r = spawnSync("systemctl", ["--user", "enable", "--now", "demobite.service"], { encoding: "utf8" });
|
|
326
|
+
if (r.status !== 0) { warn(`systemd refused the service: ${(r.stderr || "").trim()}`); process.exit(1); }
|
|
327
|
+
} else {
|
|
328
|
+
// Windows: a Run entry that starts the listener hidden at logon (no console window), via a tiny script.
|
|
329
|
+
const q = (t) => `""${t}""`;
|
|
330
|
+
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`;
|
|
331
|
+
fs.writeFileSync(def.file, vbs);
|
|
332
|
+
spawnSync("reg", ["add", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", "DemoBites", "/t", "REG_SZ", "/d", `wscript.exe "${def.file}"`, "/f"], { stdio: "ignore" });
|
|
333
|
+
killListener();
|
|
334
|
+
spawnSync("wscript.exe", [def.file], { stdio: "ignore" });
|
|
335
|
+
}
|
|
336
|
+
// a moment for the first check-in, then the truth
|
|
337
|
+
const until = Date.now() + 8000;
|
|
338
|
+
while (Date.now() < until) { const l = readJson(LOCK); if (l?.pid && pidAlive(l.pid) && l.service) break; spawnSync(process.execPath, ["-e", "setTimeout(()=>{},400)"]); }
|
|
339
|
+
console.log("");
|
|
340
|
+
printStatus();
|
|
341
|
+
console.log("\n It starts with the machine from now on. No window to keep open.\n");
|
|
342
|
+
process.exit(0);
|
|
343
|
+
}
|
|
344
|
+
|
|
181
345
|
// ── LISTENING (founder 2026-10-14 night, "YES!!!!"): `npx demobite listen`.
|
|
182
346
|
// The machine checks in to the app with the person's recorder key, pulls
|
|
183
347
|
// the jobs the Capture tab created for this owner, runs the person's own
|
|
@@ -194,11 +358,29 @@ if (arg === "listen") {
|
|
|
194
358
|
const connectCode = argvRest.includes("--connect") ? (argvRest[argvRest.indexOf("--connect") + 1] || "") : "";
|
|
195
359
|
if (connectCode || !cfg?.api_key || (argvRest.includes("--base") && cfg?.base && cfg.base.replace(/\/+$/, "") !== base)) {
|
|
196
360
|
console.log(`\n Connecting this ${connectCode ? "machine" : "project"} to your DemoBites account first…\n`);
|
|
197
|
-
const r = spawnSync(
|
|
361
|
+
const r = spawnSync(process.execPath, [path.join(dest, "scripts", "login.mjs"), base, ...(connectCode ? ["--connect", connectCode] : [])], { stdio: "inherit", cwd: process.cwd() });
|
|
198
362
|
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
199
363
|
cfg = readCfg();
|
|
200
364
|
}
|
|
201
365
|
if (!cfg?.api_key) { warn("Not connected — run: npx demobite login"); process.exit(1); }
|
|
366
|
+
// ONE LISTENER (founder 2026-10-16: "it can never run twice"): a lock with a live pid. A second start,
|
|
367
|
+
// from a Terminal or a double-pasted line, says who is listening and leaves.
|
|
368
|
+
fs.mkdirSync(HOME, { recursive: true });
|
|
369
|
+
{
|
|
370
|
+
const cur = readJson(LOCK);
|
|
371
|
+
if (cur?.pid && cur.pid !== process.pid && pidAlive(cur.pid)) {
|
|
372
|
+
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`);
|
|
373
|
+
process.exit(0);
|
|
374
|
+
}
|
|
375
|
+
// the browsers of a listener that died hard
|
|
376
|
+
for (const c of readJson(CHILDREN) ?? []) { try { process.kill(process.platform === "win32" ? c : -c, "SIGKILL"); } catch { /* gone */ } }
|
|
377
|
+
fs.rmSync(CHILDREN, { force: true });
|
|
378
|
+
}
|
|
379
|
+
process.title = "demobite";
|
|
380
|
+
const children = new Set();
|
|
381
|
+
const track = (child) => { if (!child?.pid) return; children.add(child.pid); writeJson(CHILDREN, [...children]); child.on("close", () => { children.delete(child.pid); writeJson(CHILDREN, [...children]); }); };
|
|
382
|
+
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); };
|
|
383
|
+
process.on("SIGTERM", () => bye("SIGTERM")); process.on("SIGINT", () => bye("SIGINT")); process.on("SIGHUP", () => bye("SIGHUP"));
|
|
202
384
|
const listenFile = path.join(process.cwd(), ".recorder", "listen.json");
|
|
203
385
|
let listen = {};
|
|
204
386
|
try { listen = JSON.parse(fs.readFileSync(listenFile, "utf8")); } catch { /* first time */ }
|
|
@@ -241,6 +423,8 @@ if (arg === "listen") {
|
|
|
241
423
|
const m = r.stdout.trim().match(/[:/]([^/:]+\/[^/]+?)(?:\.git)?$/);
|
|
242
424
|
return m ? m[1] : null;
|
|
243
425
|
})();
|
|
426
|
+
writeJson(LOCK, { pid: process.pid, since: new Date().toISOString(), cwd: process.cwd(), service: SERVICE, repo });
|
|
427
|
+
const noteState = (extra) => writeJson(STATE, { at: new Date().toISOString(), machine_id: listen.machine_id, name, base, workspace: cfg.workspace?.name ?? null, ...extra });
|
|
244
428
|
const readiness = () => {
|
|
245
429
|
let signins = [];
|
|
246
430
|
try { signins = Object.keys(JSON.parse(fs.readFileSync(path.join(process.cwd(), ".recorder", "signins.json"), "utf8")).origins || {}); } catch { /* none */ }
|
|
@@ -249,9 +433,10 @@ if (arg === "listen") {
|
|
|
249
433
|
const api = async (body) => {
|
|
250
434
|
try {
|
|
251
435
|
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:
|
|
436
|
+
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); }
|
|
437
|
+
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
438
|
return res.ok ? await res.json() : null;
|
|
254
|
-
} catch { return null; }
|
|
439
|
+
} catch (e) { if (body.action === "checkin") noteState({ busy: !!body.busy, last_error: String(e?.message ?? e).slice(0, 120) }); return null; }
|
|
255
440
|
};
|
|
256
441
|
const event = (job_id, kind, payload) => api({ action: "event", job_id, kind, payload });
|
|
257
442
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -259,8 +444,9 @@ if (arg === "listen") {
|
|
|
259
444
|
console.log(`\n Listening as "${name}" for ${cfg.workspace?.name ?? "your workspace"} on ${base.replace(/^https?:\/\//, "")}.`);
|
|
260
445
|
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
446
|
// 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(`
|
|
447
|
+
// window is the machine's side of every recording. The login service has no window: one line instead.
|
|
448
|
+
if (SERVICE) console.log(` ${new Date().toISOString()} listening (login service, pid ${process.pid})`);
|
|
449
|
+
else console.log(`
|
|
264
450
|
┌──────────────────────────────────────────────────────────┐
|
|
265
451
|
│ │
|
|
266
452
|
│ LEAVE THIS WINDOW OPEN │
|
|
@@ -274,7 +460,7 @@ if (arg === "listen") {
|
|
|
274
460
|
// A line that visibly lives (founder: "look at the terminal, it's stuck"):
|
|
275
461
|
// a heartbeat with a clock while we wait, cleared while a job prints.
|
|
276
462
|
const startedAt = Date.now();
|
|
277
|
-
const tty = !!process.stdout.isTTY;
|
|
463
|
+
const tty = !!process.stdout.isTTY && !SERVICE;
|
|
278
464
|
let ticker = null;
|
|
279
465
|
const tick = () => {
|
|
280
466
|
const s = Math.floor((Date.now() - startedAt) / 1000);
|
|
@@ -292,9 +478,11 @@ if (arg === "listen") {
|
|
|
292
478
|
const isLook = job.kind === "look";
|
|
293
479
|
const script = path.join(dest, "scripts", isLook ? "look.mjs" : "signin.mjs");
|
|
294
480
|
console.log(`\n ${isLook ? "◉ Looking at" : "⚿ Sign-in at"} ${job.url}`);
|
|
481
|
+
noteState({ busy: true, job: job.id });
|
|
295
482
|
await event(job.id, "started", { machine: name });
|
|
296
483
|
const { spawn } = await import("node:child_process");
|
|
297
|
-
const child = spawn(
|
|
484
|
+
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" });
|
|
485
|
+
track(child);
|
|
298
486
|
let verdict = null;
|
|
299
487
|
let buf = "";
|
|
300
488
|
child.stdout.on("data", (d) => {
|
|
@@ -325,6 +513,7 @@ if (arg === "listen") {
|
|
|
325
513
|
const model = typeof job.model === "string" && job.model ? job.model : JOB_MODEL;
|
|
326
514
|
// Alive while filming (1.13.1): a heartbeat every 5 s so the rail never shows this machine
|
|
327
515
|
// offline mid-take (the gap after the last step used to do exactly that).
|
|
516
|
+
noteState({ busy: true, job: job.id });
|
|
328
517
|
const heartbeat = setInterval(() => { void api({ action: "checkin", machine_id: listen.machine_id, name, readiness: readiness(), busy: true }); }, 5000);
|
|
329
518
|
console.log(`\n ▶ Job ${job.id}: ${job.brief.slice(0, 100)}${job.brief.length > 100 ? "…" : ""} (${engine === "claude" ? "Claude Code" : "DemoBites agent"} on ${model})`);
|
|
330
519
|
if (engine !== "claude") {
|
|
@@ -335,7 +524,8 @@ if (arg === "listen") {
|
|
|
335
524
|
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
525
|
// Spawned async (1.14.1): a spawnSync here blocked the event loop, so the 5 s heartbeat never fired
|
|
337
526
|
// 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(
|
|
527
|
+
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" });
|
|
528
|
+
track(child);
|
|
339
529
|
const code = await new Promise((resolve) => child.on("close", resolve));
|
|
340
530
|
const st = await api({ action: "poll", job_id: job.id });
|
|
341
531
|
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 +588,7 @@ if (arg === "login" || arg === "logout") {
|
|
|
398
588
|
// `login <base>` forwards the base (dev, a preview) to login.mjs, which
|
|
399
589
|
// resolves it before config.base and the production default. Logout
|
|
400
590
|
// forwards nothing extra: the key's own home always wins there.
|
|
401
|
-
const r = spawnSync(
|
|
591
|
+
const r = spawnSync(process.execPath, [path.join(dest, "scripts", "login.mjs"), ...(arg === "logout" ? ["--logout"] : process.argv.slice(3))], {
|
|
402
592
|
stdio: "inherit",
|
|
403
593
|
cwd: process.cwd(),
|
|
404
594
|
});
|
|
@@ -440,12 +630,12 @@ if (arg === "retake") {
|
|
|
440
630
|
let cfg = readCfg();
|
|
441
631
|
if (!cfg?.api_key) {
|
|
442
632
|
console.log("\n Not connected yet — linking this machine to DemoBites first…\n");
|
|
443
|
-
const r = spawnSync(
|
|
633
|
+
const r = spawnSync(process.execPath, [path.join(dest, "scripts", "login.mjs")], { stdio: "inherit", cwd: process.cwd() });
|
|
444
634
|
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
445
635
|
cfg = readCfg();
|
|
446
636
|
}
|
|
447
637
|
if (!cfg?.api_key) { warn("Login did not complete — run: npx demobite login"); process.exit(1); }
|
|
448
|
-
const r = spawnSync(
|
|
638
|
+
const r = spawnSync(process.execPath, [path.join(dest, "scripts", "retake.mjs"), ...process.argv.slice(3)], { stdio: "inherit", cwd: process.cwd() });
|
|
449
639
|
process.exit(r.status ?? 1);
|
|
450
640
|
}
|
|
451
641
|
|
|
@@ -456,12 +646,12 @@ if (arg === "briefs" || arg === "status" || arg === "rules") {
|
|
|
456
646
|
let cfg = readCfg();
|
|
457
647
|
if (!cfg?.api_key) {
|
|
458
648
|
console.log("\n Not connected yet — linking this machine to DemoBites first…\n");
|
|
459
|
-
const r = spawnSync(
|
|
649
|
+
const r = spawnSync(process.execPath, [path.join(dest, "scripts", "login.mjs")], { stdio: "inherit", cwd: process.cwd() });
|
|
460
650
|
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
461
651
|
cfg = readCfg();
|
|
462
652
|
}
|
|
463
653
|
if (!cfg?.api_key) { warn("Login did not complete — run: npx demobite login"); process.exit(1); }
|
|
464
|
-
const r = spawnSync(
|
|
654
|
+
const r = spawnSync(process.execPath, [path.join(dest, "scripts", `${arg}.mjs`), ...process.argv.slice(3)], { stdio: "inherit", cwd: process.cwd() });
|
|
465
655
|
process.exit(r.status ?? 1);
|
|
466
656
|
}
|
|
467
657
|
|
|
@@ -469,7 +659,7 @@ if (arg === "mcp") {
|
|
|
469
659
|
let cfg = readCfg();
|
|
470
660
|
if (!cfg?.api_key) {
|
|
471
661
|
console.log("\n Not connected yet — linking this machine to DemoBites first…\n");
|
|
472
|
-
const r = spawnSync(
|
|
662
|
+
const r = spawnSync(process.execPath, [path.join(dest, "scripts", "login.mjs")], { stdio: "inherit", cwd: process.cwd() });
|
|
473
663
|
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
474
664
|
cfg = readCfg();
|
|
475
665
|
}
|
package/package.json
CHANGED