demobites 1.23.0 → 1.25.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 +16 -1
- package/package.json +1 -1
- package/skill/scripts/plan.mjs +22 -8
package/launcher/index.mjs
CHANGED
|
@@ -670,9 +670,24 @@ if (arg === "listen") {
|
|
|
670
670
|
if (SERVICE) removeService(); // bootout ends this process; the handlers below clear the lock
|
|
671
671
|
bye("OFF");
|
|
672
672
|
};
|
|
673
|
+
// FORGET FROM THE APP (1.24.0, founder 2026-10-17: "we can and should forget from here as well"): the
|
|
674
|
+
// app's order drops one site's folder from this machine's bindings; the planner works from the live app there.
|
|
675
|
+
const runForget = async (job) => {
|
|
676
|
+
noteState({ busy: true, job: job.id });
|
|
677
|
+
await event(job.id, "started", { machine: name });
|
|
678
|
+
let site = "";
|
|
679
|
+
try { site = siteOf(new URL(job.url).hostname); } catch { /* below */ }
|
|
680
|
+
const file = reposFile(process.cwd());
|
|
681
|
+
const repos = readJson(file) ?? {};
|
|
682
|
+
if (site && repos[site]) { delete repos[site]; writeJson(file, repos); }
|
|
683
|
+
console.log(` ○ ${site || "that site"} no longer has code on this machine, on your word from the app.`);
|
|
684
|
+
await event(job.id, "ready", { forgot: site, machine: name });
|
|
685
|
+
noteState({ busy: false, job: null, last_error: null });
|
|
686
|
+
};
|
|
673
687
|
const runJob = async (job) => {
|
|
674
688
|
waitOff();
|
|
675
689
|
if (job.kind === "off") return runOff(job);
|
|
690
|
+
if (job.kind === "forget") return runForget(job);
|
|
676
691
|
if (job.kind === "look" || job.kind === "signin" || job.kind === "signout") return runLookOrSignin(job);
|
|
677
692
|
const model = typeof job.model === "string" && job.model ? job.model : JOB_MODEL;
|
|
678
693
|
// Alive while filming (1.13.1): a heartbeat every 5 s so the rail never shows this machine
|
|
@@ -685,7 +700,7 @@ if (arg === "listen") {
|
|
|
685
700
|
await event(job.id, "started", { machine: name });
|
|
686
701
|
const takeDir = path.join(process.cwd(), ".recorder", "takes", job.id);
|
|
687
702
|
fs.mkdirSync(takeDir, { recursive: true });
|
|
688
|
-
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");
|
|
689
704
|
// Spawned async (1.14.1): a spawnSync here blocked the event loop, so the 5 s heartbeat never fired
|
|
690
705
|
// during a take and the rail called the machine offline mid-recording (founder 2026-10-15 17:39).
|
|
691
706
|
const bound = job.url ? codeFor(job.url) : null;
|
package/package.json
CHANGED
package/skill/scripts/plan.mjs
CHANGED
|
@@ -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
|
-
|
|
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); }
|