demobite 1.11.0 → 1.13.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 +38 -0
- package/package.json +1 -1
- package/scripts/record.mjs +1 -0
- package/scripts/trim.mjs +2 -0
- package/skill/scripts/look.mjs +64 -0
- package/skill/scripts/upload.mjs +3 -0
package/launcher/index.mjs
CHANGED
|
@@ -276,9 +276,46 @@ if (arg === "listen") {
|
|
|
276
276
|
const waitOff = () => { if (ticker) { clearInterval(ticker); ticker = null; process.stdout.write("\r\x1b[2K"); } };
|
|
277
277
|
waitOn();
|
|
278
278
|
|
|
279
|
+
// A look or a sign-in ahead of any brief: one script, its lines shown live
|
|
280
|
+
// in the app (look phrases), its verdict posted as the job's result.
|
|
281
|
+
const runLookOrSignin = async (job) => {
|
|
282
|
+
const isLook = job.kind === "look";
|
|
283
|
+
const script = path.join(dest, "scripts", isLook ? "look.mjs" : "signin.mjs");
|
|
284
|
+
console.log(`\n ${isLook ? "◉ Looking at" : "⚿ Sign-in at"} ${job.url}`);
|
|
285
|
+
await event(job.id, "started", { machine: name });
|
|
286
|
+
const { spawn } = await import("node:child_process");
|
|
287
|
+
const child = spawn("node", [script, job.url], { cwd: process.cwd(), env: { ...process.env, RECORDER_JOB: job.id, RECORDER_BASE: base, RECORDER_LISTEN: "1" }, stdio: ["ignore", "pipe", "pipe"] });
|
|
288
|
+
let verdict = null;
|
|
289
|
+
let buf = "";
|
|
290
|
+
child.stdout.on("data", (d) => {
|
|
291
|
+
buf += d.toString();
|
|
292
|
+
let i;
|
|
293
|
+
while ((i = buf.indexOf("\n")) >= 0) {
|
|
294
|
+
const line = buf.slice(0, i).trim(); buf = buf.slice(i + 1);
|
|
295
|
+
if (!line) continue;
|
|
296
|
+
if (line.startsWith("RESULT ")) { try { verdict = JSON.parse(line.slice(7)); } catch { verdict = null; } continue; }
|
|
297
|
+
console.log(` · ${line}`);
|
|
298
|
+
if (isLook) void event(job.id, "look", { text: line.slice(0, 200) });
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
child.stderr.on("data", (d) => { const t = d.toString().trim(); if (t) console.log(` ! ${t.split("\n")[0].slice(0, 160)}`); });
|
|
302
|
+
const code = await new Promise((r) => child.on("close", r));
|
|
303
|
+
if (isLook) {
|
|
304
|
+
if (verdict) await event(job.id, "looked", verdict);
|
|
305
|
+
else await event(job.id, "failed", { message: "The look did not finish" });
|
|
306
|
+
} else if (code === 0) await event(job.id, "ready", { signedIn: true, url: job.url });
|
|
307
|
+
else await event(job.id, "failed", { message: "The sign-in did not complete" });
|
|
308
|
+
console.log(` ■ ${isLook ? `Look: ${verdict?.access ?? "failed"}` : code === 0 ? "Signed in." : "Sign-in did not complete."}\n`);
|
|
309
|
+
waitOn();
|
|
310
|
+
};
|
|
311
|
+
|
|
279
312
|
const runJob = async (job) => {
|
|
280
313
|
waitOff();
|
|
314
|
+
if (job.kind === "look" || job.kind === "signin") return runLookOrSignin(job);
|
|
281
315
|
const model = typeof job.model === "string" && job.model ? job.model : JOB_MODEL;
|
|
316
|
+
// Alive while filming (1.13.0): a heartbeat every 20 s so the rail never shows this machine
|
|
317
|
+
// offline mid-take (the gap after the last step used to do exactly that).
|
|
318
|
+
const heartbeat = setInterval(() => { void api({ action: "checkin", machine_id: listen.machine_id, name, readiness: readiness(), busy: true }); }, 20000);
|
|
282
319
|
console.log(`\n ▶ Job ${job.id}: ${job.brief.slice(0, 100)}${job.brief.length > 100 ? "…" : ""} (filming on ${model})`);
|
|
283
320
|
if (!agentBin) { await event(job.id, "failed", { message: "Claude Code is not installed on this machine" }); return; }
|
|
284
321
|
await event(job.id, "started", { machine: name });
|
|
@@ -317,6 +354,7 @@ if (arg === "listen") {
|
|
|
317
354
|
if (code === 0) await event(job.id, "note", { text: "The agent finished without a ready signal." });
|
|
318
355
|
await event(job.id, "failed", { message: code === 0 ? "The demo did not come through — try again" : `The agent stopped (exit ${code})` });
|
|
319
356
|
}
|
|
357
|
+
clearInterval(heartbeat);
|
|
320
358
|
console.log(` ■ Job ${job.id} ${st?.status ?? "ended"}.\n`);
|
|
321
359
|
waitOn();
|
|
322
360
|
};
|
package/package.json
CHANGED
package/scripts/record.mjs
CHANGED
package/scripts/trim.mjs
CHANGED
|
@@ -16,6 +16,8 @@ if (!dir) {
|
|
|
16
16
|
process.exit(2);
|
|
17
17
|
}
|
|
18
18
|
import { ffmpeg as FFMPEG, ffprobe as FFPROBE, requireMediaTools } from "./media-tools.mjs";
|
|
19
|
+
const postEvent = await import("./listen-event.mjs").then((m) => m.postEvent).catch(() => async () => null);
|
|
20
|
+
void postEvent("finishing", { text: "Trimming the take to the real first and last frame…" });
|
|
19
21
|
requireMediaTools();
|
|
20
22
|
|
|
21
23
|
const raw = path.join(dir, "raw.webm");
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// LOOK (founder 2026-10-15): the moment the person gives an address, the
|
|
3
|
+
// listener goes and looks — reaches it, reads the front page, looks for a
|
|
4
|
+
// sign-in wall, checks the ledger — and says what it found. Headless, in the
|
|
5
|
+
// recorder profile (so a saved sign-in counts), never a window. Each line it
|
|
6
|
+
// prints is a phrase the app shows live; the last line is RESULT {json}.
|
|
7
|
+
//
|
|
8
|
+
// node scripts/look.mjs <url>
|
|
9
|
+
// RESULT { access: open | signed | signin | blocked | unreachable, title, url, origin, known }
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
const arg = process.argv[2];
|
|
14
|
+
const say = (t) => console.log(t);
|
|
15
|
+
const result = (r) => console.log("RESULT " + JSON.stringify(r));
|
|
16
|
+
if (!arg) { console.error("Usage: node scripts/look.mjs <url>"); process.exit(2); }
|
|
17
|
+
let target;
|
|
18
|
+
try { target = new URL(/^https?:\/\//i.test(arg) ? arg : `https://${arg}`); } catch { result({ access: "unreachable", url: arg, reason: "not an address" }); process.exit(0); }
|
|
19
|
+
const host = target.host;
|
|
20
|
+
say(`Knocking on ${host}…`);
|
|
21
|
+
|
|
22
|
+
let ledger = {};
|
|
23
|
+
try { ledger = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), ".recorder", "signins.json"), "utf8")).origins ?? {}; } catch { /* none yet */ }
|
|
24
|
+
|
|
25
|
+
const { chromium } = await import("playwright");
|
|
26
|
+
const profileDir = process.env.RECORDER_PROFILE || path.resolve(process.cwd(), ".recorder", "profile");
|
|
27
|
+
const launchOpts = {
|
|
28
|
+
headless: true,
|
|
29
|
+
viewport: { width: 1440, height: 900 },
|
|
30
|
+
args: ["--force-color-profile=srgb", "--disable-blink-features=AutomationControlled"],
|
|
31
|
+
ignoreDefaultArgs: ["--enable-automation"],
|
|
32
|
+
};
|
|
33
|
+
let ctx;
|
|
34
|
+
try { ctx = await chromium.launchPersistentContext(profileDir, { ...launchOpts, channel: "chrome" }); }
|
|
35
|
+
catch { ctx = await chromium.launchPersistentContext(profileDir, launchOpts); }
|
|
36
|
+
const page = ctx.pages()[0] ?? (await ctx.newPage());
|
|
37
|
+
try {
|
|
38
|
+
await page.goto(target.toString(), { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
39
|
+
} catch (e) {
|
|
40
|
+
say("No answer at that address.");
|
|
41
|
+
result({ access: "unreachable", url: target.toString(), origin: target.origin, reason: String(e.message).split("\n")[0].slice(0, 120) });
|
|
42
|
+
await ctx.close();
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
await page.waitForTimeout(3000);
|
|
46
|
+
const title = (await page.title().catch(() => "")).trim().slice(0, 120);
|
|
47
|
+
say(`Reading the front page${title ? ` — "${title}"` : ""}…`);
|
|
48
|
+
say("Looking for a sign-in wall…");
|
|
49
|
+
const wall = await page.evaluate(() => {
|
|
50
|
+
const pw = !!document.querySelector('input[type="password"]');
|
|
51
|
+
const p = location.pathname.toLowerCase();
|
|
52
|
+
const loginPath = /sign-?in|log-?in|login|auth\b|sso|signup|register/.test(p);
|
|
53
|
+
const bot = !!document.querySelector('input[name="cf-turnstile-response"], iframe[src*="challenges.cloudflare"], iframe[src*="recaptcha"], iframe[src*="hcaptcha"]');
|
|
54
|
+
return { pw, loginPath, bot };
|
|
55
|
+
}).catch(() => ({ pw: false, loginPath: false, bot: true }));
|
|
56
|
+
const origin = new URL(page.url()).origin;
|
|
57
|
+
const known = !!ledger[origin];
|
|
58
|
+
let access;
|
|
59
|
+
if (wall.bot && !wall.pw && !wall.loginPath) access = "blocked";
|
|
60
|
+
else if (wall.pw || wall.loginPath) access = "signin";
|
|
61
|
+
else access = known ? "signed" : "open";
|
|
62
|
+
say(access === "open" ? "Open. No sign-in needed." : access === "signed" ? "In, with the sign-in saved on this machine." : access === "signin" ? "A sign-in wall. Your session will be needed." : "A bot check stands in the way.");
|
|
63
|
+
result({ access, title, url: page.url(), origin, known });
|
|
64
|
+
await ctx.close();
|
package/skill/scripts/upload.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import fs from "node:fs";
|
|
|
26
26
|
import os from "node:os";
|
|
27
27
|
import path from "node:path";
|
|
28
28
|
import { spawnSync } from "node:child_process";
|
|
29
|
+
const postEvent = await import("./listen-event.mjs").then((m) => m.postEvent).catch(() => async () => null);
|
|
29
30
|
|
|
30
31
|
const dir = process.argv[2];
|
|
31
32
|
if (!dir) {
|
|
@@ -180,6 +181,7 @@ function storeZip(name, data) {
|
|
|
180
181
|
return Buffer.concat([local, nameBuf, data, central, nameBuf, eocd]);
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
void postEvent("finishing", { text: "Packing the take…" });
|
|
183
185
|
const zipPath = path.join(dir, "take.zip");
|
|
184
186
|
fs.rmSync(zipPath, { force: true });
|
|
185
187
|
const staging = fs.mkdtempSync(path.join(os.tmpdir(), "rec-zip-"));
|
|
@@ -261,6 +263,7 @@ if (!stagingId || !uploadUrl || !previewUploadUrl) {
|
|
|
261
263
|
|
|
262
264
|
// ── S3 PUTs: the ZIP for ingestion, the MP4 for the preview player ─────────
|
|
263
265
|
console.log("Uploading take...");
|
|
266
|
+
void postEvent("finishing", { text: "Uploading the take to your workspace…" });
|
|
264
267
|
async function putS3(url, contentType, filePath, label) {
|
|
265
268
|
let res;
|
|
266
269
|
try {
|