kaizen-agent 1.47.0 → 1.49.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/README.md +111 -84
- package/cli/build-web.ts +4 -2
- package/cli/hermes.ts +65 -0
- package/cli/install.ts +24 -10
- package/cli/localsend.test.ts +84 -0
- package/cli/localsend.ts +131 -0
- package/cli/notes.test.ts +6 -4
- package/cli/settings.ts +1 -0
- package/cli/state.test.ts +37 -0
- package/cli/state.ts +35 -20
- package/cli/video.test.ts +479 -0
- package/cli/video.ts +507 -0
- package/cli/web.ts +127 -1
- package/cli/worktree.test.ts +3 -1
- package/package.json +1 -1
- package/skills/kaizen/config.default.yml +4 -0
- package/skills/media/kaizen-book/SKILL.md +107 -0
- package/skills/media/kaizen-book/assets/reader.html +440 -0
- package/skills/media/kaizen-book/assets/styles.json +45 -0
- package/skills/media/kaizen-book/examples/book.json +14 -0
- package/skills/media/kaizen-book/examples/images/reach.svg +1 -0
- package/skills/media/kaizen-book/examples/manuscript.md +127 -0
- package/skills/media/kaizen-book/references/interview.md +85 -0
- package/skills/media/kaizen-book/references/proofread.md +47 -0
- package/skills/media/kaizen-book/references/writing.md +53 -0
- package/skills/media/kaizen-book/scripts/build_book.py +279 -0
- package/skills/media/kaizen-book/scripts/check.sh +10 -0
- package/skills/media/kaizen-book/scripts/check_book.py +128 -0
- package/skills/media/kaizen-book/scripts/deploy.py +58 -0
- package/skills/media/kaizen-book/scripts/pdf.mjs +99 -0
- package/skills/media/kaizen-book/scripts/setup.sh +9 -0
- package/skills/media/kaizen-book/scripts/verify.mjs +95 -0
- package/skills/{story-deck → media/kaizen-deck}/SKILL.md +1 -1
- package/skills/{story-deck → media/kaizen-deck}/scripts/check.sh +1 -1
- package/skills/{story-deck → media/kaizen-deck}/scripts/install_fonts.py +2 -2
- package/skills/media/kaizen-video/SKILL.md +112 -0
- package/skills/media/kaizen-video/assets/CREDITS.txt +16 -0
- package/skills/media/kaizen-video/assets/film-fonts/Fraunces.ttf +0 -0
- package/skills/media/kaizen-video/assets/film-fonts/Inter.ttf +0 -0
- package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay-Italic.ttf +0 -0
- package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay.ttf +0 -0
- package/skills/media/kaizen-video/assets/fonts/Fredoka.ttf +0 -0
- package/skills/media/kaizen-video/assets/fonts/Montserrat.ttf +0 -0
- package/skills/media/kaizen-video/assets/fonts/SpecialElite.ttf +0 -0
- package/skills/media/kaizen-video/assets/music/investigations.mp3 +0 -0
- package/skills/media/kaizen-video/assets/music/lights.mp3 +0 -0
- package/skills/media/kaizen-video/assets/music/sunny.mp3 +0 -0
- package/skills/media/kaizen-video/examples/config.json +152 -0
- package/skills/media/kaizen-video/examples/sample-film/README.md +10 -0
- package/skills/media/kaizen-video/examples/sample-film/film.json +63 -0
- package/skills/media/kaizen-video/film.html +246 -0
- package/skills/media/kaizen-video/hyperframes.json +12 -0
- package/skills/media/kaizen-video/presets/educational-warning/CREDITS.txt +3 -0
- package/skills/media/kaizen-video/presets/educational-warning/README.md +26 -0
- package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/ending.wav +0 -0
- package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/reveal.wav +0 -0
- package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/shutter.wav +0 -0
- package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/stamp.wav +0 -0
- package/skills/media/kaizen-video/presets/educational-warning/index.html +236 -0
- package/skills/media/kaizen-video/presets/educational-warning/package.json +10 -0
- package/skills/media/kaizen-video/presets/film-cinematic-drama/CREDITS.txt +1 -0
- package/skills/media/kaizen-video/presets/film-cinematic-drama/README.md +2 -0
- package/skills/media/kaizen-video/presets/film-cinematic-drama/style.json +39 -0
- package/skills/media/kaizen-video/presets/film-fun-energetic/CREDITS.txt +1 -0
- package/skills/media/kaizen-video/presets/film-fun-energetic/README.md +2 -0
- package/skills/media/kaizen-video/presets/film-fun-energetic/style.json +39 -0
- package/skills/media/kaizen-video/presets/film-promo-booth/CREDITS.txt +1 -0
- package/skills/media/kaizen-video/presets/film-promo-booth/README.md +2 -0
- package/skills/media/kaizen-video/presets/film-promo-booth/style.json +39 -0
- package/skills/media/kaizen-video/presets/film-viral-social/CREDITS.txt +1 -0
- package/skills/media/kaizen-video/presets/film-viral-social/README.md +2 -0
- package/skills/media/kaizen-video/presets/film-viral-social/style.json +39 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/CREDITS.txt +3 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/README.md +35 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/boing.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/chime.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/ding.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/ending.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/pop.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/reveal.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/shutter.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/stamp.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/whoosh.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/index.html +260 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/package.json +10 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/CREDITS.txt +3 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/README.md +52 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/InstrumentSerif.ttf +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/SpaceMono.ttf +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/boing.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ding.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ending.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/pop.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/reveal.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/shutter.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/stamp.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/tick.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/whoosh.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/index.html +272 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/package.json +10 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/CREDITS.txt +12 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/README.md +51 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/clock.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/ding.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/ending.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/heart.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/projector.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/reveal.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/shutter.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/stamp.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/whoosh.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/index.html +260 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/package.json +10 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/CREDITS.txt +8 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/README.md +24 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/clock.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/ding.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/ending.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/heart.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/reveal.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/shutter.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/stamp.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/whoosh.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/index.html +266 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/package.json +10 -0
- package/skills/media/kaizen-video/references/presets.md +76 -0
- package/skills/{story-video → media/kaizen-video}/references/production.md +2 -2
- package/skills/{story-video → media/kaizen-video}/references/revisions.md +9 -9
- package/skills/media/kaizen-video/scripts/__pycache__/intake.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/make_film.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/make_tiktok.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/render_cut.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/sfx.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/srt.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/transcribe.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/vo.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/check.sh +22 -0
- package/skills/{story-video → media/kaizen-video}/scripts/intake.py +2 -2
- package/skills/media/kaizen-video/scripts/make_film.py +263 -0
- package/skills/media/kaizen-video/scripts/make_tiktok.py +213 -0
- package/skills/{story-video → media/kaizen-video}/scripts/master.sh +12 -11
- package/skills/media/kaizen-video/scripts/render_cut.py +88 -0
- package/skills/media/kaizen-video/scripts/setup.sh +14 -0
- package/skills/{story-video → media/kaizen-video}/scripts/sfx.py +3 -3
- package/skills/media/kaizen-video/scripts/srt.py +9 -0
- package/skills/media/kaizen-video/scripts/still.sh +7 -0
- package/skills/media/kaizen-video/scripts/transcribe.py +45 -0
- package/skills/media/kaizen-video/tools/carve.mjs +560 -0
- package/skills/media/kaizen-video/tools/lib/main-module.mjs +13 -0
- package/web/board/views.ts +1 -1
- package/web/board.html +3 -1
- package/web/board.js +2 -2
- package/web/notebook.js +1 -1
- package/web/notes.html +2 -0
- package/web/term.js +38 -36
- package/web/term.ts +43 -10
- package/web/video.html +298 -0
- package/web/video.js +81 -0
- package/web/video.ts +610 -0
- package/skills/story-video/SKILL.md +0 -108
- package/skills/story-video/engine/package.json +0 -12
- package/skills/story-video/engine/src/Root.tsx +0 -24
- package/skills/story-video/engine/src/engine/Film.tsx +0 -405
- package/skills/story-video/engine/src/engine/presets.json +0 -58
- package/skills/story-video/engine/src/index.ts +0 -3
- package/skills/story-video/engine/tsconfig.json +0 -1
- package/skills/story-video/examples/sample-film/README.md +0 -12
- package/skills/story-video/examples/sample-film/film.json +0 -16
- package/skills/story-video/references/presets.md +0 -21
- package/skills/story-video/scripts/check.sh +0 -19
- package/skills/story-video/scripts/setup.sh +0 -25
- package/skills/story-video/scripts/srt.py +0 -30
- package/skills/story-video/scripts/still.sh +0 -10
- /package/skills/{story-deck → media/kaizen-deck}/examples/build-example.js +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/references/rules.md +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/references/story-arc.md +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/scripts/kit.js +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/scripts/prep_images.py +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/scripts/qa.sh +0 -0
- /package/skills/{story-video → media/kaizen-video}/examples/sample-film/lines.json +0 -0
- /package/skills/{story-video → media/kaizen-video}/references/licensing.md +0 -0
- /package/skills/{story-video → media/kaizen-video}/references/script.md +0 -0
- /package/skills/{story-video → media/kaizen-video}/scripts/music.sh +0 -0
- /package/skills/{story-video → media/kaizen-video}/scripts/vo.py +0 -0
package/cli/web.ts
CHANGED
|
@@ -17,6 +17,9 @@ import * as google from "./google.ts";
|
|
|
17
17
|
import { writeBacks } from "./writeback.ts";
|
|
18
18
|
import { postLink, sendReply } from "./browser.ts";
|
|
19
19
|
import { listNotes, saveNote, renameNote, deleteNote, boardIndex, linksTo } from "./notes.ts";
|
|
20
|
+
import * as video from "./video.ts";
|
|
21
|
+
import * as localsend from "./localsend.ts";
|
|
22
|
+
import * as hermes from "./hermes.ts";
|
|
20
23
|
|
|
21
24
|
const PAGE = join(dirname(import.meta.dir), "web", "board.html");
|
|
22
25
|
// The board's script: web/board/*.ts, bundled into one file by `bun run build:web`.
|
|
@@ -28,6 +31,9 @@ const NOTES_JS = join(dirname(import.meta.dir), "web", "notebook.js");
|
|
|
28
31
|
// The in-card terminal (agent.launch: card): xterm.js, bundled the same way.
|
|
29
32
|
const TERM_JS = join(dirname(import.meta.dir), "web", "term.js");
|
|
30
33
|
const TERM_CSS = join(dirname(import.meta.dir), "web", "term.css");
|
|
34
|
+
// The Video page (kaizen-video's library, editor and renders) and its bundle.
|
|
35
|
+
const VIDEO_PAGE = join(dirname(import.meta.dir), "web", "video.html");
|
|
36
|
+
const VIDEO_JS = join(dirname(import.meta.dir), "web", "video.js");
|
|
31
37
|
const DEFAULT_PORT = 7420;
|
|
32
38
|
// Gathered posts a reply is going out to right now: one send per item at a time.
|
|
33
39
|
const replying = new Set<string>();
|
|
@@ -43,7 +49,7 @@ const VERSION = installed();
|
|
|
43
49
|
// page newer than its server sends ops the server does not know ("unknown op").
|
|
44
50
|
// So the pages are kept as they were at start, and read live (dev edits) only while
|
|
45
51
|
// the installed version is still the one this board runs.
|
|
46
|
-
const AT_START = new Map([PAGE, BOARD_JS, NOTES_PAGE, NOTES_JS, TERM_JS, TERM_CSS].map((f) => [f, existsSync(f) ? readText(f) : null]));
|
|
52
|
+
const AT_START = new Map([PAGE, BOARD_JS, NOTES_PAGE, NOTES_JS, TERM_JS, TERM_CSS, VIDEO_PAGE, VIDEO_JS].map((f) => [f, existsSync(f) ? readText(f) : null]));
|
|
47
53
|
const served = (f: string) => installed() === VERSION && existsSync(f) ? readText(f) : AT_START.get(f) ?? null;
|
|
48
54
|
|
|
49
55
|
// A board answers at url and runs the installed version. A board from before the
|
|
@@ -259,6 +265,18 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
259
265
|
return listSettings(repoDir).rows.find((r) => r.key === key)?.value ?? "";
|
|
260
266
|
};
|
|
261
267
|
const every = async () => (await setting("sources.every")) || "off";
|
|
268
|
+
// Where videos live: the library folder (board.video_dir, default ~/.kaizen), then
|
|
269
|
+
// every known project with a videos/ folder. The page names one by its index.
|
|
270
|
+
const videoPlaces = async () => {
|
|
271
|
+
let d = await setting("board.video_dir");
|
|
272
|
+
if (d.startsWith("~")) d = home + d.slice(1);
|
|
273
|
+
return video.places(d ? resolve(nativePath(d)) : join(home, ".kaizen"), knownProjects());
|
|
274
|
+
};
|
|
275
|
+
video.onJobChange(() => changed());
|
|
276
|
+
// Send targets as last listed: a send may only go to a device the last search found
|
|
277
|
+
// or a chat Hermes listed, never to an address the page makes up.
|
|
278
|
+
let devices: { at: number; list: localsend.Device[]; note: string | null } = { at: 0, list: [], note: null };
|
|
279
|
+
let chats: hermes.Chat[] = [];
|
|
262
280
|
let busy = false;
|
|
263
281
|
const gather = setInterval(async () => {
|
|
264
282
|
if (busy) return;
|
|
@@ -368,6 +386,54 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
368
386
|
return new Response(body, { headers: { "content-type": type, "cache-control": "no-store" } });
|
|
369
387
|
}
|
|
370
388
|
|
|
389
|
+
if (req.method === "GET" && (path === "/video" || path === "/video.js")) {
|
|
390
|
+
const [f, type] = path === "/video" ? [VIDEO_PAGE, "text/html; charset=utf-8"] : [VIDEO_JS, "application/javascript; charset=utf-8"];
|
|
391
|
+
const body = served(f);
|
|
392
|
+
if (body === null) return new Response(`${path} missing; run bun run build:web`, { status: 500 });
|
|
393
|
+
return new Response(body, { headers: { "content-type": type, "cache-control": "no-store" } });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (req.method === "GET" && path.startsWith("/video/")) {
|
|
397
|
+
const all = await videoPlaces();
|
|
398
|
+
const at = (v: { place: string }) => all.findIndex((p) => p.dir === v.place);
|
|
399
|
+
if (path === "/video/presets") {
|
|
400
|
+
const place = all[Number(url.searchParams.get("p") ?? 0)] ?? all[0]!;
|
|
401
|
+
return json({ presets: video.presets(place.dir), limits: video.LIMITS });
|
|
402
|
+
}
|
|
403
|
+
// Agents the page started, while the board still holds their terminal.
|
|
404
|
+
const agents = video.agentsOf().flatMap((a) => {
|
|
405
|
+
const s = ptyState(a.key), p = at(a);
|
|
406
|
+
return s && p >= 0 ? [{ place: p, name: a.name, pty: a.key, alive: s.alive, asking: !!s.asking, at: a.at }] : [];
|
|
407
|
+
});
|
|
408
|
+
if (path === "/video/list")
|
|
409
|
+
return json({ places: all.map((p, i) => ({ i, label: p.label, dir: tilde(p.dir) })), agents, jobs: video.jobList().map((j) => ({ ...j, place: at(j) })),
|
|
410
|
+
videos: video.listVideos(all).map((v) => ({ ...v, place: at(v), job: v.job && { ...v.job, place: at(v) },
|
|
411
|
+
send: video.sendOf(v.place, v.name), sends: video.sendLog(v.place, v.name).slice(-5).reverse() })) });
|
|
412
|
+
if (path === "/video/film") {
|
|
413
|
+
const place = all[Number(url.searchParams.get("p"))], name = url.searchParams.get("name");
|
|
414
|
+
if (!place || !video.okName(name)) return bad("unknown video", 404);
|
|
415
|
+
const film = video.readFilm(place.dir, name);
|
|
416
|
+
if (!film) return bad("not a film", 404);
|
|
417
|
+
return json({ ...film, stills: video.stills(place.dir, name), job: video.jobOf(place.dir, name), log: video.logOf(place.dir, name) });
|
|
418
|
+
}
|
|
419
|
+
if (path.startsWith("/video/f/")) return video.serveFile(all, path.slice("/video/f/".length).split("/"), req);
|
|
420
|
+
// LocalSend devices, searched again on ?fresh=1 or after two minutes.
|
|
421
|
+
if (path === "/video/devices") {
|
|
422
|
+
if (url.searchParams.get("fresh") === "1" || Date.now() - devices.at > 120_000) {
|
|
423
|
+
const r = await localsend.discover();
|
|
424
|
+
devices = { at: Date.now(), list: r.devices, note: r.note };
|
|
425
|
+
}
|
|
426
|
+
return json({ devices: devices.list.map(({ fingerprint, alias, deviceModel, deviceType }) => ({ id: fingerprint, alias, deviceModel, deviceType })), note: devices.note });
|
|
427
|
+
}
|
|
428
|
+
if (path === "/video/chats") {
|
|
429
|
+
const r = await hermes.targets();
|
|
430
|
+
if ("error" in r) return json({ chats: [], error: r.error });
|
|
431
|
+
chats = r.chats;
|
|
432
|
+
return json({ chats });
|
|
433
|
+
}
|
|
434
|
+
return bad("not found", 404);
|
|
435
|
+
}
|
|
436
|
+
|
|
371
437
|
if (req.method === "GET" && (path === "/term.js" || path === "/term.css")) {
|
|
372
438
|
const [f, type] = path === "/term.js" ? [TERM_JS, "application/javascript; charset=utf-8"] : [TERM_CSS, "text/css; charset=utf-8"];
|
|
373
439
|
const body = served(f);
|
|
@@ -723,6 +789,66 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
723
789
|
let body: any;
|
|
724
790
|
try { body = await req.json(); } catch { return bad("json body expected"); }
|
|
725
791
|
|
|
792
|
+
// The Video page: a place by index, a video folder by name, both checked in video.ts.
|
|
793
|
+
if (path.startsWith("/video/")) {
|
|
794
|
+
const all = await videoPlaces();
|
|
795
|
+
if (path === "/video/cancel") return video.cancelJob(Number(body.id)) ? json({ ok: true }) : bad("no such job", 404);
|
|
796
|
+
// New video and Ask Claude: kaizen-video in a board terminal, in the place's folder.
|
|
797
|
+
if (path === "/video/new" || path === "/video/ask") {
|
|
798
|
+
const place = all[Number(body.p)];
|
|
799
|
+
if (!place || !Number.isInteger(Number(body.p))) return bad("unknown place", 404);
|
|
800
|
+
if (path === "/video/ask" && !video.okName(body.name)) return bad("unknown video", 404);
|
|
801
|
+
const r = path === "/video/new" ? video.requestFor(place.dir, body) : video.askFor(place.dir, body.name, body.text);
|
|
802
|
+
if ("error" in r) return bad(r.error);
|
|
803
|
+
const name = "name" in r ? r.name : body.name as string;
|
|
804
|
+
const launched = launchRun({ severity: null, where: null, text: r.text, raw: r.text }, place.dir, undefined,
|
|
805
|
+
{ skill: "kaizen-video", cwd: place.dir, card: true, onChange: changed });
|
|
806
|
+
if (!launched.ok) return json({ ok: false, why: launched.why, manual: launched.manual }, 500);
|
|
807
|
+
if (launched.pty) video.noteAgent(launched.pty, place.dir, name);
|
|
808
|
+
changed();
|
|
809
|
+
return json({ ok: true, name, pty: launched.pty ?? null, agent: launched.agent.name });
|
|
810
|
+
}
|
|
811
|
+
const place = all[Number(body.p)];
|
|
812
|
+
if (!place || !Number.isInteger(Number(body.p)) || !video.okName(body.name)) return bad("unknown video", 404);
|
|
813
|
+
if (path === "/video/save") {
|
|
814
|
+
const err = video.saveFilm(place.dir, body.name, body.edits ?? [], body.platforms);
|
|
815
|
+
if (err) return bad(err);
|
|
816
|
+
const job = body.build === false ? null : video.startJob(place.dir, body.name, "build");
|
|
817
|
+
changed();
|
|
818
|
+
return json({ ok: true, job: typeof job === "string" ? null : { ...job, place: Number(body.p) }, note: typeof job === "string" ? job : null });
|
|
819
|
+
}
|
|
820
|
+
// A finished file to a device the last search found, or a chat Hermes listed.
|
|
821
|
+
if (path === "/video/send") {
|
|
822
|
+
const file = video.outputFile(place.dir, body.name, body.file);
|
|
823
|
+
if (!file) return bad("not one of this video's finished files", 404);
|
|
824
|
+
let err: string | null;
|
|
825
|
+
if (body.via === "localsend") {
|
|
826
|
+
const dev = devices.list.find((d) => d.fingerprint === body.to);
|
|
827
|
+
if (!dev) return bad("that device is not in the last search; search again", 404);
|
|
828
|
+
err = video.startSend(place.dir, body.name, { via: "localsend", to: dev.alias, file: body.file }, (step) => localsend.sendFile(dev, file, step));
|
|
829
|
+
} else if (body.via === "whatsapp") {
|
|
830
|
+
const chat = chats.find((c) => c.id === body.to);
|
|
831
|
+
if (!chat) return bad("that chat is not in Hermes' list", 404);
|
|
832
|
+
const text = typeof body.text === "string" ? body.text : "";
|
|
833
|
+
const m = hermes.message(text, file);
|
|
834
|
+
if (typeof m !== "string") return bad(m.error);
|
|
835
|
+
err = video.startSend(place.dir, body.name, { via: "whatsapp", to: chat.name, file: body.file }, () => hermes.sendMedia(chat.id, text, file));
|
|
836
|
+
} else return bad("send by localsend or whatsapp");
|
|
837
|
+
if (err) return bad(err, 409);
|
|
838
|
+
changed();
|
|
839
|
+
return json({ ok: true });
|
|
840
|
+
}
|
|
841
|
+
if (path === "/video/job") {
|
|
842
|
+
if (!["build", "preview", "render"].includes(body.kind)) return bad("unknown job");
|
|
843
|
+
const formats = Array.isArray(body.formats) ? body.formats : video.formatsFor(Array.isArray(body.platforms) ? body.platforms : []);
|
|
844
|
+
const job = video.startJob(place.dir, body.name, body.kind, formats);
|
|
845
|
+
if (typeof job === "string") return bad(job, 409);
|
|
846
|
+
changed();
|
|
847
|
+
return json({ ok: true, job: { ...job, place: Number(body.p) } });
|
|
848
|
+
}
|
|
849
|
+
return bad("not found", 404);
|
|
850
|
+
}
|
|
851
|
+
|
|
726
852
|
// The same search as the TUI's "find more": every .kaizen under $HOME, the
|
|
727
853
|
// drives, and where the board was started, added to the registry.
|
|
728
854
|
// ponytail: synchronous walk, the board stalls for its length; move to a worker if a big disk makes that seconds.
|
package/cli/worktree.test.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { tmpdir } from "node:os";
|
|
|
6
6
|
import { startWorktree, landWorktree, codeDir, projectFromWorktree, run, type Run } from "./worktree.ts";
|
|
7
7
|
|
|
8
8
|
const ID = "2026-10-17-demo";
|
|
9
|
+
// CI runners have no git identity, and landWorktree's rebase writes commits.
|
|
10
|
+
const idRun: Run = (cmd, cwd) => run(cmd[0] === "git" ? ["git", "-c", "user.email=a@b.c", "-c", "user.name=t", ...cmd.slice(1)] : cmd, cwd);
|
|
9
11
|
let remote: string, project: string, other: string;
|
|
10
12
|
const g = (cwd: string, ...a: string[]) => { const r = run(["git", "-c", "user.email=a@b.c", "-c", "user.name=t", ...a], cwd); if (!r.ok) throw new Error(r.why); return r.out; };
|
|
11
13
|
const commit = (cwd: string, f: string, text: string) => { writeFileSync(join(cwd, f), text); g(cwd, "add", f); g(cwd, "commit", "-qm", `feat: ${f}`); };
|
|
@@ -46,7 +48,7 @@ test("G-02 lands by rebase and push after main moved on", () => {
|
|
|
46
48
|
const w = (startWorktree(project, ID) as { path: string }).path;
|
|
47
49
|
commit(w, "run.txt", "mine");
|
|
48
50
|
commit(other, "theirs.txt", "x"); g(other, "push", "-q", "origin", "HEAD:main"); // main moves ahead
|
|
49
|
-
const r = landWorktree(project, ID,
|
|
51
|
+
const r = landWorktree(project, ID, idRun, false);
|
|
50
52
|
expect(r).toEqual({ ok: true, how: "push", url: undefined });
|
|
51
53
|
g(project, "fetch", "-q", "origin");
|
|
52
54
|
expect(g(project, "ls-tree", "--name-only", "origin/main").split("\n")).toEqual(["a.txt", "run.txt", "theirs.txt"]);
|
package/package.json
CHANGED
|
@@ -152,6 +152,10 @@ board:
|
|
|
152
152
|
# not itself a project (no .git, no package.json) is read as a group, and the
|
|
153
153
|
# folders inside it are offered too.
|
|
154
154
|
projects_dir: ""
|
|
155
|
+
# The Video page's library: new videos go to <folder>/videos/<name>/ and their
|
|
156
|
+
# renders to <folder>/output/<name>/. Empty means ~/.kaizen. Known projects that
|
|
157
|
+
# have a videos/ folder are listed on the Video page too.
|
|
158
|
+
video_dir: ""
|
|
155
159
|
# Where `kaizen web` opens the board: browser (a tab in your default browser) or
|
|
156
160
|
# app (a Chrome, Edge or Brave window with no tabs or address bar).
|
|
157
161
|
open: browser
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kaizen-book
|
|
3
|
+
description: Write, proofread and publish a book. Interviews the author first (kind of book, genre such as sci-fi, romance, fantasy, thriller, memoir or non-fiction, readers, language, length, voice, look), then outlines and drafts it chapter by chapter with a style sheet, proofreads with a numbered change list, and builds a single-file offline e-book that reads like a real book (cover that opens, page-flip, paper, themes, font size, resume), an A5 colour print PDF, a black-and-white print PDF, optional volume PDFs, and a website folder with WhatsApp/social link previews. Use when the user wants to write a book, novel, novella, memoir or story collection, turn a manuscript (Markdown or .docx) into an e-book or printable PDF, proofread a manuscript, or publish a book online.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Kaizen book
|
|
7
|
+
|
|
8
|
+
A book lives in `book/` in the user's project folder, and everything it produces goes to `output/`:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
book/
|
|
12
|
+
brief.md interview answers, approved by the author
|
|
13
|
+
outline.md chapter plan, approved by the author
|
|
14
|
+
style.md style sheet: names, spellings, voice rules, facts so far
|
|
15
|
+
manuscript.md the text (# Part, ## Chapter | subtitle, ### Section, [^1] footnotes)
|
|
16
|
+
book.json title, author, lang, style, cover, url, volumes…
|
|
17
|
+
images/ cover and figures
|
|
18
|
+
proofread.md numbered change list, marked applied / declined
|
|
19
|
+
output/
|
|
20
|
+
book.html the e-book: one file, works offline
|
|
21
|
+
book.pdf A5 colour print
|
|
22
|
+
book-mono.pdf A5 black and white (white typeset cover, greyscale images and emoji)
|
|
23
|
+
book-vol-N.pdf one per volume, black and white (or colour), continuous page numbers
|
|
24
|
+
og.jpg 1200×630 link-preview image
|
|
25
|
+
shots/ screenshots from verify.mjs
|
|
26
|
+
deploy/ static site: index.html (preview loader), reader.html, PDFs, og.jpg
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Run every script from the project folder. `S=<this skill>/scripts`. Worked example: [`examples/`](examples/).
|
|
30
|
+
|
|
31
|
+
## 0. Setup, once per machine
|
|
32
|
+
`$S/check.sh` lists what is missing (python3, node, poppler's `pdfinfo`, Playwright). `$S/setup.sh` installs
|
|
33
|
+
Playwright 1.62.1 and Chromium into `~/.kaizen/kaizen-book` (override with `KAIZEN_BOOK_HOME`), which is about 150 MB.
|
|
34
|
+
Tell the user before you run it. Building the HTML needs only python3. PDFs and screenshots need the rest.
|
|
35
|
+
|
|
36
|
+
## 1. Where to start
|
|
37
|
+
- **A new book**: interview first. Read [`references/interview.md`](references/interview.md) and follow it.
|
|
38
|
+
Nothing is written until `brief.md` is approved.
|
|
39
|
+
- **An existing manuscript** (.md, .docx, pasted text): convert it as described at the end of
|
|
40
|
+
[`references/writing.md`](references/writing.md), write `book.json`, and go to step 3. Ask only what
|
|
41
|
+
`book.json` needs (title, author, language, style, cover, url).
|
|
42
|
+
- **A revision of a built book**: change `manuscript.md`, then repeat steps 3–5. Never hand-edit `output/`.
|
|
43
|
+
|
|
44
|
+
## 2. Write
|
|
45
|
+
Follow [`references/writing.md`](references/writing.md): the outline, approved; the style sheet; chapters
|
|
46
|
+
in batches of three to five, with a stop for the author after each batch. Never invent facts, sources or quotes.
|
|
47
|
+
Mark open questions `[[CHECK: …]]`.
|
|
48
|
+
|
|
49
|
+
## 3. Proofread
|
|
50
|
+
Follow [`references/proofread.md`](references/proofread.md). Run `python3 $S/check_book.py book` for
|
|
51
|
+
structure, then write `book/proofread.md` as a numbered change list. Apply only the numbers the author
|
|
52
|
+
accepts. The build does not go ahead while `check_book.py` reports errors.
|
|
53
|
+
|
|
54
|
+
## 4. Build and look
|
|
55
|
+
```
|
|
56
|
+
python3 $S/build_book.py book # → output/book.html (fonts embedded, cached in ~/.kaizen/kaizen-book/fonts)
|
|
57
|
+
node $S/verify.mjs output/book.html # phone + spread smoke test, screenshots in output/shots/
|
|
58
|
+
node $S/pdf.mjs output/book.html # → book.pdf, book-mono.pdf, book-vol-N.pdf, og.jpg
|
|
59
|
+
```
|
|
60
|
+
- `verify.mjs` must report `all checks passed`: no page errors, every chapter opens, nothing clipped
|
|
61
|
+
past a page, no horizontal scroll, flips cross chapters, and resume keeps the chapter, page, theme and font size.
|
|
62
|
+
Look at the screenshots yourself, and show the cover and one spread to the author.
|
|
63
|
+
- `pdf.mjs` prints the book twice so the contents page carries real page numbers, read from the PDF's
|
|
64
|
+
named destinations with `pdfinfo -dests`. `--only color,mono,volumes,og` limits what it makes.
|
|
65
|
+
- Look at the PDF pages before handing them over (`pdftoppm -r 40 -png output/book.pdf /tmp/p` then a contact sheet):
|
|
66
|
+
the cover, the contents, a part page, a chapter opening, a page with an image, and the mono cover.
|
|
67
|
+
|
|
68
|
+
### book.json
|
|
69
|
+
| Key | Meaning |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `title`, `subtitle`, `author`, `tagline` | Used on the cover, title page, contents and link preview |
|
|
72
|
+
| `lang` | `id`, `en`, or another code. `id`/`en` have built-in interface labels; override any with `"labels": {…}` (keys are in `build_book.py`) |
|
|
73
|
+
| `style` | `classic` · `romance` · `scifi` · `fantasy` · `thriller` · `literary` · `children` (from `assets/styles.json`) |
|
|
74
|
+
| `theme` | Per-book overrides of the style, e.g. `{"cover": {"bg": "#123"}, "accent": "#a33"}` |
|
|
75
|
+
| `cover` | Path in `book/` to a 2:3 image. Empty means a typeset cover in the style's colours. The mono PDF always uses the typeset cover |
|
|
76
|
+
| `copyright`, `dedication` | Front-matter pages in the PDFs (`\n` for line breaks) |
|
|
77
|
+
| `description`, `url` | Link preview text, and the site's absolute URL (needed for `og:image`) |
|
|
78
|
+
| `volumes` | `[{"label": "Volume 1"}, {"label": "Volume 2", "from_part": 6}]`. `from_part` is the 1-based number of the `#` part a volume starts at |
|
|
79
|
+
| `volume_print` | `mono` (default) or `color`: which print the volume PDFs are cut from. An image cover gets the volume label on a band |
|
|
80
|
+
| `manuscript`, `id` | Other manuscript file name; storage key for reading position (defaults to the title) |
|
|
81
|
+
|
|
82
|
+
To add a style, copy an entry in `assets/styles.json` (Google Fonts family names; cover `pattern` is one of
|
|
83
|
+
`frame grid petals slash dots none`).
|
|
84
|
+
|
|
85
|
+
## 5. Publish
|
|
86
|
+
`python3 $S/deploy.py book` assembles `output/deploy/`, which can be hosted anywhere static (`cd output/deploy && npx vercel --prod`,
|
|
87
|
+
Netlify, GitHub Pages, or a Cloudflare tunnel). Publishing is outward-facing, so confirm with the user before deploying.
|
|
88
|
+
- `index.html` is a 2 KB page carrying the og tags that then loads `reader.html`. WhatsApp and other previewers
|
|
89
|
+
give up on a multi-MB page before they read its head. Don't replace index.html with the book.
|
|
90
|
+
- The reader's "Print / PDF" button opens `book.pdf` when the site has one, because iOS Safari ignores `@page` and
|
|
91
|
+
prints with white margins. Opened as a file, the button prints the built-in print layout instead.
|
|
92
|
+
- The PDFs are snapshots. After any change to the text, rebuild everything in step 4 and redeploy. Volumes are found
|
|
93
|
+
by part, so they follow the text without hand-kept page numbers.
|
|
94
|
+
|
|
95
|
+
## What the reader does
|
|
96
|
+
Cover board with cloth grain that swings open onto a half-title page, contents, and CSS-column pages on textured
|
|
97
|
+
paper. Phones show one page, and landscape screens 900 px or wider show a spread. Pages turn with a 3D flip (a fade with reduced motion)
|
|
98
|
+
by swipe, by tapping the outer 30% of the page, or with arrow keys, PageUp/PageDown or Space. Running heads and page numbers on every page (numbered through the whole book; in a spread each chapter starts on a left page),
|
|
99
|
+
drop cap at each chapter opening, part opener pages, text before the first heading shown as a lead-in before the first chapter, fenced code blocks, footnote markers that open the note, and themes (paper / sepia / night). Font size
|
|
100
|
+
13–28 px, and the position is saved per book in localStorage. Text is justified only when a column is at least 380 px wide, to avoid
|
|
101
|
+
rivers on phones. Paragraphs use `dir="auto"`, so Arabic or Hebrew passages in a left-to-right book display correctly. A fully
|
|
102
|
+
right-to-left book is not supported yet.
|
|
103
|
+
|
|
104
|
+
## Kaizen
|
|
105
|
+
Inside a kaizen run, the brief and outline approvals are the plan approval, each batch of chapters is a build, and the
|
|
106
|
+
review reads the chapters against `brief.md`, `style.md` and the outline. For a built book, the review also needs
|
|
107
|
+
`verify.mjs` passing and the PDF contact sheets. Screenshots go in the run's `attachments/` for the Preview gate.
|