kaizen-agent 1.48.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.
Files changed (183) hide show
  1. package/README.md +2 -2
  2. package/cli/build-web.ts +4 -2
  3. package/cli/hermes.ts +65 -0
  4. package/cli/install.ts +18 -5
  5. package/cli/localsend.test.ts +84 -0
  6. package/cli/localsend.ts +131 -0
  7. package/cli/notes.test.ts +6 -4
  8. package/cli/settings.ts +1 -0
  9. package/cli/state.test.ts +37 -0
  10. package/cli/state.ts +35 -20
  11. package/cli/video.test.ts +479 -0
  12. package/cli/video.ts +507 -0
  13. package/cli/web.ts +127 -1
  14. package/package.json +1 -1
  15. package/skills/kaizen/config.default.yml +4 -0
  16. package/skills/media/kaizen-book/SKILL.md +107 -0
  17. package/skills/media/kaizen-book/assets/reader.html +440 -0
  18. package/skills/media/kaizen-book/assets/styles.json +45 -0
  19. package/skills/media/kaizen-book/examples/book.json +14 -0
  20. package/skills/media/kaizen-book/examples/images/reach.svg +1 -0
  21. package/skills/media/kaizen-book/examples/manuscript.md +127 -0
  22. package/skills/media/kaizen-book/references/interview.md +85 -0
  23. package/skills/media/kaizen-book/references/proofread.md +47 -0
  24. package/skills/media/kaizen-book/references/writing.md +53 -0
  25. package/skills/media/kaizen-book/scripts/build_book.py +279 -0
  26. package/skills/media/kaizen-book/scripts/check.sh +10 -0
  27. package/skills/media/kaizen-book/scripts/check_book.py +128 -0
  28. package/skills/media/kaizen-book/scripts/deploy.py +58 -0
  29. package/skills/media/kaizen-book/scripts/pdf.mjs +99 -0
  30. package/skills/media/kaizen-book/scripts/setup.sh +9 -0
  31. package/skills/media/kaizen-book/scripts/verify.mjs +95 -0
  32. package/skills/{story-deck → media/kaizen-deck}/SKILL.md +1 -1
  33. package/skills/{story-deck → media/kaizen-deck}/scripts/check.sh +1 -1
  34. package/skills/{story-deck → media/kaizen-deck}/scripts/install_fonts.py +2 -2
  35. package/skills/media/kaizen-video/SKILL.md +112 -0
  36. package/skills/{kaizen-video → media/kaizen-video}/assets/CREDITS.txt +5 -0
  37. package/skills/media/kaizen-video/assets/film-fonts/Fraunces.ttf +0 -0
  38. package/skills/media/kaizen-video/assets/film-fonts/Inter.ttf +0 -0
  39. package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay-Italic.ttf +0 -0
  40. package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay.ttf +0 -0
  41. package/skills/media/kaizen-video/examples/sample-film/README.md +10 -0
  42. package/skills/media/kaizen-video/examples/sample-film/film.json +63 -0
  43. package/skills/media/kaizen-video/film.html +246 -0
  44. package/skills/media/kaizen-video/presets/film-cinematic-drama/CREDITS.txt +1 -0
  45. package/skills/media/kaizen-video/presets/film-cinematic-drama/README.md +2 -0
  46. package/skills/media/kaizen-video/presets/film-cinematic-drama/style.json +39 -0
  47. package/skills/media/kaizen-video/presets/film-fun-energetic/CREDITS.txt +1 -0
  48. package/skills/media/kaizen-video/presets/film-fun-energetic/README.md +2 -0
  49. package/skills/media/kaizen-video/presets/film-fun-energetic/style.json +39 -0
  50. package/skills/media/kaizen-video/presets/film-promo-booth/CREDITS.txt +1 -0
  51. package/skills/media/kaizen-video/presets/film-promo-booth/README.md +2 -0
  52. package/skills/media/kaizen-video/presets/film-promo-booth/style.json +39 -0
  53. package/skills/media/kaizen-video/presets/film-viral-social/CREDITS.txt +1 -0
  54. package/skills/media/kaizen-video/presets/film-viral-social/README.md +2 -0
  55. package/skills/media/kaizen-video/presets/film-viral-social/style.json +39 -0
  56. package/skills/media/kaizen-video/presets/tiktok-explainer/CREDITS.txt +3 -0
  57. package/skills/media/kaizen-video/presets/tiktok-explainer/README.md +52 -0
  58. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/InstrumentSerif.ttf +0 -0
  59. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/SpaceMono.ttf +0 -0
  60. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/boing.wav +0 -0
  61. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ding.wav +0 -0
  62. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ending.wav +0 -0
  63. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/pop.wav +0 -0
  64. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/reveal.wav +0 -0
  65. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/shutter.wav +0 -0
  66. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/stamp.wav +0 -0
  67. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/tick.wav +0 -0
  68. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/whoosh.wav +0 -0
  69. package/skills/media/kaizen-video/presets/tiktok-explainer/index.html +272 -0
  70. package/skills/media/kaizen-video/presets/tiktok-explainer/package.json +10 -0
  71. package/skills/{kaizen-video → media/kaizen-video}/references/presets.md +22 -0
  72. package/skills/{story-video → media/kaizen-video}/references/production.md +2 -2
  73. package/skills/{story-video → media/kaizen-video}/references/revisions.md +9 -9
  74. package/skills/media/kaizen-video/scripts/__pycache__/intake.cpython-314.pyc +0 -0
  75. package/skills/media/kaizen-video/scripts/__pycache__/make_film.cpython-314.pyc +0 -0
  76. package/skills/media/kaizen-video/scripts/__pycache__/make_tiktok.cpython-314.pyc +0 -0
  77. package/skills/media/kaizen-video/scripts/__pycache__/render_cut.cpython-314.pyc +0 -0
  78. package/skills/media/kaizen-video/scripts/__pycache__/sfx.cpython-314.pyc +0 -0
  79. package/skills/media/kaizen-video/scripts/__pycache__/srt.cpython-314.pyc +0 -0
  80. package/skills/media/kaizen-video/scripts/__pycache__/transcribe.cpython-314.pyc +0 -0
  81. package/skills/media/kaizen-video/scripts/__pycache__/vo.cpython-314.pyc +0 -0
  82. package/skills/{kaizen-video → media/kaizen-video}/scripts/check.sh +8 -0
  83. package/skills/{story-video → media/kaizen-video}/scripts/intake.py +2 -2
  84. package/skills/media/kaizen-video/scripts/make_film.py +263 -0
  85. package/skills/{kaizen-video → media/kaizen-video}/scripts/make_tiktok.py +8 -3
  86. package/skills/{story-video → media/kaizen-video}/scripts/master.sh +12 -11
  87. package/skills/{story-video → media/kaizen-video}/scripts/sfx.py +3 -3
  88. package/skills/media/kaizen-video/scripts/srt.py +9 -0
  89. package/skills/media/kaizen-video/scripts/still.sh +7 -0
  90. package/web/board/views.ts +1 -1
  91. package/web/board.html +2 -0
  92. package/web/board.js +2 -2
  93. package/web/notebook.js +1 -1
  94. package/web/notes.html +2 -0
  95. package/web/term.js +1 -1
  96. package/web/video.html +298 -0
  97. package/web/video.js +81 -0
  98. package/web/video.ts +610 -0
  99. package/skills/kaizen-video/SKILL.md +0 -73
  100. package/skills/story-video/SKILL.md +0 -108
  101. package/skills/story-video/engine/package.json +0 -12
  102. package/skills/story-video/engine/src/Root.tsx +0 -24
  103. package/skills/story-video/engine/src/engine/Film.tsx +0 -405
  104. package/skills/story-video/engine/src/engine/presets.json +0 -58
  105. package/skills/story-video/engine/src/index.ts +0 -3
  106. package/skills/story-video/engine/tsconfig.json +0 -1
  107. package/skills/story-video/examples/sample-film/README.md +0 -12
  108. package/skills/story-video/examples/sample-film/film.json +0 -16
  109. package/skills/story-video/references/presets.md +0 -21
  110. package/skills/story-video/scripts/check.sh +0 -19
  111. package/skills/story-video/scripts/setup.sh +0 -25
  112. package/skills/story-video/scripts/srt.py +0 -30
  113. package/skills/story-video/scripts/still.sh +0 -10
  114. /package/skills/{story-deck → media/kaizen-deck}/examples/build-example.js +0 -0
  115. /package/skills/{story-deck → media/kaizen-deck}/references/rules.md +0 -0
  116. /package/skills/{story-deck → media/kaizen-deck}/references/story-arc.md +0 -0
  117. /package/skills/{story-deck → media/kaizen-deck}/scripts/kit.js +0 -0
  118. /package/skills/{story-deck → media/kaizen-deck}/scripts/prep_images.py +0 -0
  119. /package/skills/{story-deck → media/kaizen-deck}/scripts/qa.sh +0 -0
  120. /package/skills/{kaizen-video → media/kaizen-video}/assets/fonts/Fredoka.ttf +0 -0
  121. /package/skills/{kaizen-video → media/kaizen-video}/assets/fonts/Montserrat.ttf +0 -0
  122. /package/skills/{kaizen-video → media/kaizen-video}/assets/fonts/SpecialElite.ttf +0 -0
  123. /package/skills/{kaizen-video → media/kaizen-video}/assets/music/investigations.mp3 +0 -0
  124. /package/skills/{kaizen-video → media/kaizen-video}/assets/music/lights.mp3 +0 -0
  125. /package/skills/{kaizen-video → media/kaizen-video}/assets/music/sunny.mp3 +0 -0
  126. /package/skills/{kaizen-video → media/kaizen-video}/examples/config.json +0 -0
  127. /package/skills/{story-video → media/kaizen-video}/examples/sample-film/lines.json +0 -0
  128. /package/skills/{kaizen-video → media/kaizen-video}/hyperframes.json +0 -0
  129. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/CREDITS.txt +0 -0
  130. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/README.md +0 -0
  131. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/ending.wav +0 -0
  132. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/reveal.wav +0 -0
  133. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/shutter.wav +0 -0
  134. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/stamp.wav +0 -0
  135. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/index.html +0 -0
  136. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/package.json +0 -0
  137. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/CREDITS.txt +0 -0
  138. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/README.md +0 -0
  139. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/boing.wav +0 -0
  140. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/chime.wav +0 -0
  141. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/ding.wav +0 -0
  142. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/ending.wav +0 -0
  143. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/pop.wav +0 -0
  144. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/reveal.wav +0 -0
  145. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/shutter.wav +0 -0
  146. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/stamp.wav +0 -0
  147. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/whoosh.wav +0 -0
  148. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/index.html +0 -0
  149. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/package.json +0 -0
  150. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/CREDITS.txt +0 -0
  151. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/README.md +0 -0
  152. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/clock.wav +0 -0
  153. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/ding.wav +0 -0
  154. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/ending.wav +0 -0
  155. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/heart.wav +0 -0
  156. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/projector.wav +0 -0
  157. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/reveal.wav +0 -0
  158. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/shutter.wav +0 -0
  159. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/stamp.wav +0 -0
  160. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/whoosh.wav +0 -0
  161. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/index.html +0 -0
  162. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/package.json +0 -0
  163. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/CREDITS.txt +0 -0
  164. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/README.md +0 -0
  165. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/clock.wav +0 -0
  166. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/ding.wav +0 -0
  167. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/ending.wav +0 -0
  168. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/heart.wav +0 -0
  169. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/reveal.wav +0 -0
  170. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/shutter.wav +0 -0
  171. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/stamp.wav +0 -0
  172. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/whoosh.wav +0 -0
  173. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/index.html +0 -0
  174. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/package.json +0 -0
  175. /package/skills/{story-video → media/kaizen-video}/references/licensing.md +0 -0
  176. /package/skills/{story-video → media/kaizen-video}/references/script.md +0 -0
  177. /package/skills/{story-video → media/kaizen-video}/scripts/music.sh +0 -0
  178. /package/skills/{kaizen-video → media/kaizen-video}/scripts/render_cut.py +0 -0
  179. /package/skills/{kaizen-video → media/kaizen-video}/scripts/setup.sh +0 -0
  180. /package/skills/{kaizen-video → media/kaizen-video}/scripts/transcribe.py +0 -0
  181. /package/skills/{story-video → media/kaizen-video}/scripts/vo.py +0 -0
  182. /package/skills/{kaizen-video → media/kaizen-video}/tools/carve.mjs +0 -0
  183. /package/skills/{kaizen-video → media/kaizen-video}/tools/lib/main-module.mjs +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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaizen-agent",
3
- "version": "1.48.0",
3
+ "version": "1.49.0",
4
4
  "description": "Staged workflow for AI agents — code, writing, research, operations: plan, human approval, build, independent review, bounded fix loop.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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.