demobite 1.10.1 → 1.12.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 +68 -5
- package/package.json +1 -1
- package/skill/scripts/look.mjs +64 -0
package/launcher/index.mjs
CHANGED
|
@@ -247,11 +247,73 @@ if (arg === "listen") {
|
|
|
247
247
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
248
248
|
|
|
249
249
|
console.log(`\n Listening as "${name}" for ${cfg.workspace?.name ?? "your workspace"} on ${base.replace(/^https?:\/\//, "")}.`);
|
|
250
|
-
console.log(` Ask for a demo in the app (Create → Agentic Capture). ${agentBin ? `Claude Code${agentInfo.agent_version ? " " + agentInfo.agent_version : ""} found; demos film on ${JOB_MODEL}.` : "! Claude Code was not found on this machine — install it and sign in, it does the thinking."}`);
|
|
251
|
-
|
|
250
|
+
console.log(` Ask for a demo in the app (Create → Agentic Capture). ${agentBin ? `Claude Code${agentInfo.agent_version ? " " + agentInfo.agent_version : ""} found; demos film on ${JOB_MODEL} unless you pick another model in the app.` : "! Claude Code was not found on this machine — install it and sign in, it does the thinking."}`);
|
|
251
|
+
// The one thing the person must know, boxed (founder 2026-10-15): this
|
|
252
|
+
// window is the machine's side of every recording.
|
|
253
|
+
console.log(`
|
|
254
|
+
┌──────────────────────────────────────────────────────────┐
|
|
255
|
+
│ │
|
|
256
|
+
│ LEAVE THIS WINDOW OPEN │
|
|
257
|
+
│ │
|
|
258
|
+
│ It is your machine's side of every recording: │
|
|
259
|
+
│ the camera, the sign-ins, the coding agent run here. │
|
|
260
|
+
│ Closing it, or Ctrl-C, stops the listening. │
|
|
261
|
+
│ │
|
|
262
|
+
└──────────────────────────────────────────────────────────┘
|
|
263
|
+
`);
|
|
264
|
+
// A line that visibly lives (founder: "look at the terminal, it's stuck"):
|
|
265
|
+
// a heartbeat with a clock while we wait, cleared while a job prints.
|
|
266
|
+
const startedAt = Date.now();
|
|
267
|
+
const tty = !!process.stdout.isTTY;
|
|
268
|
+
let ticker = null;
|
|
269
|
+
const tick = () => {
|
|
270
|
+
const s = Math.floor((Date.now() - startedAt) / 1000);
|
|
271
|
+
const clock = `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
|
|
272
|
+
const dot = s % 2 ? "●" : "○";
|
|
273
|
+
process.stdout.write(`\r\x1b[2K ${dot} Listening · ${clock} · waiting for a demo from the app`);
|
|
274
|
+
};
|
|
275
|
+
const waitOn = () => { if (tty && !ticker) { tick(); ticker = setInterval(tick, 1000); } };
|
|
276
|
+
const waitOff = () => { if (ticker) { clearInterval(ticker); ticker = null; process.stdout.write("\r\x1b[2K"); } };
|
|
277
|
+
waitOn();
|
|
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
|
+
};
|
|
252
311
|
|
|
253
312
|
const runJob = async (job) => {
|
|
254
|
-
|
|
313
|
+
waitOff();
|
|
314
|
+
if (job.kind === "look" || job.kind === "signin") return runLookOrSignin(job);
|
|
315
|
+
const model = typeof job.model === "string" && job.model ? job.model : JOB_MODEL;
|
|
316
|
+
console.log(`\n ▶ Job ${job.id}: ${job.brief.slice(0, 100)}${job.brief.length > 100 ? "…" : ""} (filming on ${model})`);
|
|
255
317
|
if (!agentBin) { await event(job.id, "failed", { message: "Claude Code is not installed on this machine" }); return; }
|
|
256
318
|
await event(job.id, "started", { machine: name });
|
|
257
319
|
const prompt = [
|
|
@@ -260,7 +322,7 @@ if (arg === "listen") {
|
|
|
260
322
|
job.url ? `Address to start from: ${job.url}` : "Address: work it out from this project (the saved config, the codebase).",
|
|
261
323
|
`Show the storyboard before filming: ${job.review_storyboard ? "yes — run storyboard.mjs await and obey its answer" : "no — storyboard.mjs await returns film at once"}.`,
|
|
262
324
|
].join("\n");
|
|
263
|
-
const child = (await import("node:child_process")).spawn(agentBin, ["-p", prompt, "--model",
|
|
325
|
+
const child = (await import("node:child_process")).spawn(agentBin, ["-p", prompt, "--model", model, "--output-format", "stream-json", "--verbose", "--dangerously-skip-permissions"], {
|
|
264
326
|
cwd: process.cwd(),
|
|
265
327
|
env: { ...process.env, RECORDER_JOB: job.id, RECORDER_BASE: base, RECORDER_LISTEN: "1" },
|
|
266
328
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -289,7 +351,8 @@ if (arg === "listen") {
|
|
|
289
351
|
if (code === 0) await event(job.id, "note", { text: "The agent finished without a ready signal." });
|
|
290
352
|
await event(job.id, "failed", { message: code === 0 ? "The demo did not come through — try again" : `The agent stopped (exit ${code})` });
|
|
291
353
|
}
|
|
292
|
-
console.log(` ■ Job ${job.id} ${st?.status ?? "ended"}
|
|
354
|
+
console.log(` ■ Job ${job.id} ${st?.status ?? "ended"}.\n`);
|
|
355
|
+
waitOn();
|
|
293
356
|
};
|
|
294
357
|
|
|
295
358
|
for (;;) {
|
package/package.json
CHANGED
|
@@ -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();
|