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.
Files changed (184) hide show
  1. package/README.md +111 -84
  2. package/cli/build-web.ts +4 -2
  3. package/cli/hermes.ts +65 -0
  4. package/cli/install.ts +24 -10
  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/cli/worktree.test.ts +3 -1
  15. package/package.json +1 -1
  16. package/skills/kaizen/config.default.yml +4 -0
  17. package/skills/media/kaizen-book/SKILL.md +107 -0
  18. package/skills/media/kaizen-book/assets/reader.html +440 -0
  19. package/skills/media/kaizen-book/assets/styles.json +45 -0
  20. package/skills/media/kaizen-book/examples/book.json +14 -0
  21. package/skills/media/kaizen-book/examples/images/reach.svg +1 -0
  22. package/skills/media/kaizen-book/examples/manuscript.md +127 -0
  23. package/skills/media/kaizen-book/references/interview.md +85 -0
  24. package/skills/media/kaizen-book/references/proofread.md +47 -0
  25. package/skills/media/kaizen-book/references/writing.md +53 -0
  26. package/skills/media/kaizen-book/scripts/build_book.py +279 -0
  27. package/skills/media/kaizen-book/scripts/check.sh +10 -0
  28. package/skills/media/kaizen-book/scripts/check_book.py +128 -0
  29. package/skills/media/kaizen-book/scripts/deploy.py +58 -0
  30. package/skills/media/kaizen-book/scripts/pdf.mjs +99 -0
  31. package/skills/media/kaizen-book/scripts/setup.sh +9 -0
  32. package/skills/media/kaizen-book/scripts/verify.mjs +95 -0
  33. package/skills/{story-deck → media/kaizen-deck}/SKILL.md +1 -1
  34. package/skills/{story-deck → media/kaizen-deck}/scripts/check.sh +1 -1
  35. package/skills/{story-deck → media/kaizen-deck}/scripts/install_fonts.py +2 -2
  36. package/skills/media/kaizen-video/SKILL.md +112 -0
  37. package/skills/media/kaizen-video/assets/CREDITS.txt +16 -0
  38. package/skills/media/kaizen-video/assets/film-fonts/Fraunces.ttf +0 -0
  39. package/skills/media/kaizen-video/assets/film-fonts/Inter.ttf +0 -0
  40. package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay-Italic.ttf +0 -0
  41. package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay.ttf +0 -0
  42. package/skills/media/kaizen-video/assets/fonts/Fredoka.ttf +0 -0
  43. package/skills/media/kaizen-video/assets/fonts/Montserrat.ttf +0 -0
  44. package/skills/media/kaizen-video/assets/fonts/SpecialElite.ttf +0 -0
  45. package/skills/media/kaizen-video/assets/music/investigations.mp3 +0 -0
  46. package/skills/media/kaizen-video/assets/music/lights.mp3 +0 -0
  47. package/skills/media/kaizen-video/assets/music/sunny.mp3 +0 -0
  48. package/skills/media/kaizen-video/examples/config.json +152 -0
  49. package/skills/media/kaizen-video/examples/sample-film/README.md +10 -0
  50. package/skills/media/kaizen-video/examples/sample-film/film.json +63 -0
  51. package/skills/media/kaizen-video/film.html +246 -0
  52. package/skills/media/kaizen-video/hyperframes.json +12 -0
  53. package/skills/media/kaizen-video/presets/educational-warning/CREDITS.txt +3 -0
  54. package/skills/media/kaizen-video/presets/educational-warning/README.md +26 -0
  55. package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/ending.wav +0 -0
  56. package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/reveal.wav +0 -0
  57. package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/shutter.wav +0 -0
  58. package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/stamp.wav +0 -0
  59. package/skills/media/kaizen-video/presets/educational-warning/index.html +236 -0
  60. package/skills/media/kaizen-video/presets/educational-warning/package.json +10 -0
  61. package/skills/media/kaizen-video/presets/film-cinematic-drama/CREDITS.txt +1 -0
  62. package/skills/media/kaizen-video/presets/film-cinematic-drama/README.md +2 -0
  63. package/skills/media/kaizen-video/presets/film-cinematic-drama/style.json +39 -0
  64. package/skills/media/kaizen-video/presets/film-fun-energetic/CREDITS.txt +1 -0
  65. package/skills/media/kaizen-video/presets/film-fun-energetic/README.md +2 -0
  66. package/skills/media/kaizen-video/presets/film-fun-energetic/style.json +39 -0
  67. package/skills/media/kaizen-video/presets/film-promo-booth/CREDITS.txt +1 -0
  68. package/skills/media/kaizen-video/presets/film-promo-booth/README.md +2 -0
  69. package/skills/media/kaizen-video/presets/film-promo-booth/style.json +39 -0
  70. package/skills/media/kaizen-video/presets/film-viral-social/CREDITS.txt +1 -0
  71. package/skills/media/kaizen-video/presets/film-viral-social/README.md +2 -0
  72. package/skills/media/kaizen-video/presets/film-viral-social/style.json +39 -0
  73. package/skills/media/kaizen-video/presets/tiktok-cheerful/CREDITS.txt +3 -0
  74. package/skills/media/kaizen-video/presets/tiktok-cheerful/README.md +35 -0
  75. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/boing.wav +0 -0
  76. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/chime.wav +0 -0
  77. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/ding.wav +0 -0
  78. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/ending.wav +0 -0
  79. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/pop.wav +0 -0
  80. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/reveal.wav +0 -0
  81. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/shutter.wav +0 -0
  82. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/stamp.wav +0 -0
  83. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/whoosh.wav +0 -0
  84. package/skills/media/kaizen-video/presets/tiktok-cheerful/index.html +260 -0
  85. package/skills/media/kaizen-video/presets/tiktok-cheerful/package.json +10 -0
  86. package/skills/media/kaizen-video/presets/tiktok-explainer/CREDITS.txt +3 -0
  87. package/skills/media/kaizen-video/presets/tiktok-explainer/README.md +52 -0
  88. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/InstrumentSerif.ttf +0 -0
  89. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/SpaceMono.ttf +0 -0
  90. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/boing.wav +0 -0
  91. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ding.wav +0 -0
  92. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ending.wav +0 -0
  93. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/pop.wav +0 -0
  94. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/reveal.wav +0 -0
  95. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/shutter.wav +0 -0
  96. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/stamp.wav +0 -0
  97. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/tick.wav +0 -0
  98. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/whoosh.wav +0 -0
  99. package/skills/media/kaizen-video/presets/tiktok-explainer/index.html +272 -0
  100. package/skills/media/kaizen-video/presets/tiktok-explainer/package.json +10 -0
  101. package/skills/media/kaizen-video/presets/tiktok-mystery/CREDITS.txt +12 -0
  102. package/skills/media/kaizen-video/presets/tiktok-mystery/README.md +51 -0
  103. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/clock.wav +0 -0
  104. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/ding.wav +0 -0
  105. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/ending.wav +0 -0
  106. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/heart.wav +0 -0
  107. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/projector.wav +0 -0
  108. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/reveal.wav +0 -0
  109. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/shutter.wav +0 -0
  110. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/stamp.wav +0 -0
  111. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/whoosh.wav +0 -0
  112. package/skills/media/kaizen-video/presets/tiktok-mystery/index.html +260 -0
  113. package/skills/media/kaizen-video/presets/tiktok-mystery/package.json +10 -0
  114. package/skills/media/kaizen-video/presets/tiktok-scam-alert/CREDITS.txt +8 -0
  115. package/skills/media/kaizen-video/presets/tiktok-scam-alert/README.md +24 -0
  116. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/clock.wav +0 -0
  117. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/ding.wav +0 -0
  118. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/ending.wav +0 -0
  119. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/heart.wav +0 -0
  120. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/reveal.wav +0 -0
  121. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/shutter.wav +0 -0
  122. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/stamp.wav +0 -0
  123. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/whoosh.wav +0 -0
  124. package/skills/media/kaizen-video/presets/tiktok-scam-alert/index.html +266 -0
  125. package/skills/media/kaizen-video/presets/tiktok-scam-alert/package.json +10 -0
  126. package/skills/media/kaizen-video/references/presets.md +76 -0
  127. package/skills/{story-video → media/kaizen-video}/references/production.md +2 -2
  128. package/skills/{story-video → media/kaizen-video}/references/revisions.md +9 -9
  129. package/skills/media/kaizen-video/scripts/__pycache__/intake.cpython-314.pyc +0 -0
  130. package/skills/media/kaizen-video/scripts/__pycache__/make_film.cpython-314.pyc +0 -0
  131. package/skills/media/kaizen-video/scripts/__pycache__/make_tiktok.cpython-314.pyc +0 -0
  132. package/skills/media/kaizen-video/scripts/__pycache__/render_cut.cpython-314.pyc +0 -0
  133. package/skills/media/kaizen-video/scripts/__pycache__/sfx.cpython-314.pyc +0 -0
  134. package/skills/media/kaizen-video/scripts/__pycache__/srt.cpython-314.pyc +0 -0
  135. package/skills/media/kaizen-video/scripts/__pycache__/transcribe.cpython-314.pyc +0 -0
  136. package/skills/media/kaizen-video/scripts/__pycache__/vo.cpython-314.pyc +0 -0
  137. package/skills/media/kaizen-video/scripts/check.sh +22 -0
  138. package/skills/{story-video → media/kaizen-video}/scripts/intake.py +2 -2
  139. package/skills/media/kaizen-video/scripts/make_film.py +263 -0
  140. package/skills/media/kaizen-video/scripts/make_tiktok.py +213 -0
  141. package/skills/{story-video → media/kaizen-video}/scripts/master.sh +12 -11
  142. package/skills/media/kaizen-video/scripts/render_cut.py +88 -0
  143. package/skills/media/kaizen-video/scripts/setup.sh +14 -0
  144. package/skills/{story-video → media/kaizen-video}/scripts/sfx.py +3 -3
  145. package/skills/media/kaizen-video/scripts/srt.py +9 -0
  146. package/skills/media/kaizen-video/scripts/still.sh +7 -0
  147. package/skills/media/kaizen-video/scripts/transcribe.py +45 -0
  148. package/skills/media/kaizen-video/tools/carve.mjs +560 -0
  149. package/skills/media/kaizen-video/tools/lib/main-module.mjs +13 -0
  150. package/web/board/views.ts +1 -1
  151. package/web/board.html +3 -1
  152. package/web/board.js +2 -2
  153. package/web/notebook.js +1 -1
  154. package/web/notes.html +2 -0
  155. package/web/term.js +38 -36
  156. package/web/term.ts +43 -10
  157. package/web/video.html +298 -0
  158. package/web/video.js +81 -0
  159. package/web/video.ts +610 -0
  160. package/skills/story-video/SKILL.md +0 -108
  161. package/skills/story-video/engine/package.json +0 -12
  162. package/skills/story-video/engine/src/Root.tsx +0 -24
  163. package/skills/story-video/engine/src/engine/Film.tsx +0 -405
  164. package/skills/story-video/engine/src/engine/presets.json +0 -58
  165. package/skills/story-video/engine/src/index.ts +0 -3
  166. package/skills/story-video/engine/tsconfig.json +0 -1
  167. package/skills/story-video/examples/sample-film/README.md +0 -12
  168. package/skills/story-video/examples/sample-film/film.json +0 -16
  169. package/skills/story-video/references/presets.md +0 -21
  170. package/skills/story-video/scripts/check.sh +0 -19
  171. package/skills/story-video/scripts/setup.sh +0 -25
  172. package/skills/story-video/scripts/srt.py +0 -30
  173. package/skills/story-video/scripts/still.sh +0 -10
  174. /package/skills/{story-deck → media/kaizen-deck}/examples/build-example.js +0 -0
  175. /package/skills/{story-deck → media/kaizen-deck}/references/rules.md +0 -0
  176. /package/skills/{story-deck → media/kaizen-deck}/references/story-arc.md +0 -0
  177. /package/skills/{story-deck → media/kaizen-deck}/scripts/kit.js +0 -0
  178. /package/skills/{story-deck → media/kaizen-deck}/scripts/prep_images.py +0 -0
  179. /package/skills/{story-deck → media/kaizen-deck}/scripts/qa.sh +0 -0
  180. /package/skills/{story-video → media/kaizen-video}/examples/sample-film/lines.json +0 -0
  181. /package/skills/{story-video → media/kaizen-video}/references/licensing.md +0 -0
  182. /package/skills/{story-video → media/kaizen-video}/references/script.md +0 -0
  183. /package/skills/{story-video → media/kaizen-video}/scripts/music.sh +0 -0
  184. /package/skills/{story-video → media/kaizen-video}/scripts/vo.py +0 -0
package/README.md CHANGED
@@ -1,78 +1,118 @@
1
- <div align="center">
2
- <img src="https://raw.githubusercontent.com/hfadhlullah/kaizen/main/assets/banner.png" alt="kaizen" width="760">
3
- </div>
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/hfadhlullah/kaizen/main/assets/readme/banner.png" alt="kaizen: build what you have in mind. Apps, documents, videos and games, with your AI agent" width="100%">
3
+ </p>
4
4
 
5
- <p align="center"><b>One tool to build and maintain your product with AI agents.</b><br>Requests in, a plan you approve, the work, an independent review, done.</p>
5
+ <p align="center">
6
+ <b>Build what you have in mind: an app, a document, a video, a game.</b><br>
7
+ Your AI agent plans it, you approve, it builds, and a reviewer that never saw the work checks it.
8
+ </p>
6
9
 
7
- <p align="center"><img src="assets/hero.gif" alt="Animated walkthrough of the real kaizen board with sample data. 1: a request gathered from a support sheet waits in the Icebox and is promoted. 2: it is an idea on the board, and Run opens Claude Code. 3: the agent works in the card's own terminal and writes a plan. 4: the plan panel shows what done and wrong mean, and it is approved. 5: it builds, then a reviewer that never saw the work reports a high finding, a double charge. 6: only that finding is ticked and fixed. 7: the reviewer re-checks it. 8: the run lands in Done, with the low finding left in the backlog." width="760"></p>
10
+ <p align="center">
11
+ <a href="https://www.npmjs.com/package/kaizen-agent"><img alt="npm" src="https://img.shields.io/npm/v/kaizen-agent?style=for-the-badge&color=000&labelColor=1c1c1e&logo=npm"></a>
12
+ <a href="https://github.com/hfadhlullah/kaizen/stargazers"><img alt="Stars" src="https://img.shields.io/github/stars/hfadhlullah/kaizen?style=for-the-badge&logo=github&color=000&labelColor=1c1c1e"></a>
13
+ <img alt="Bun 1.1+" src="https://img.shields.io/badge/Bun-1.1%2B-000?style=for-the-badge&logo=bun&logoColor=white">
14
+ <img alt="macOS · Linux · Windows beta" src="https://img.shields.io/badge/macOS_·_Linux_·_Windows_beta-000?style=for-the-badge">
15
+ <a href="LICENSE"><img alt="License MIT" src="https://img.shields.io/badge/license-MIT-000?style=for-the-badge"></a>
16
+ </p>
8
17
 
9
- ## The problem
10
-
11
- You ask an AI coding agent to retry a payment webhook when the bank times out. It starts editing at once, then says *"Done, tests pass."* The tests do pass. They were written by the same agent, from the same assumption: that a timeout means the charge failed. Sometimes the bank said yes and the reply got lost. Now a retry charges the customer twice, and nothing in the agent's report would make you look.
18
+ > **kaizen runs inside an AI agent; it is not one.** The agent does the work; kaizen gives it a plan, an approval stop and an independent review. It works with Claude Code, Codex, Antigravity and Gemini CLI, and the installer offers to set one up if you have none. A run costs several times a single prompt, so it is meant for work where being wrong is expensive. Windows support is in beta.
12
19
 
13
- None of this is a lack of intelligence. It is a missing workflow:
20
+ <br>
14
21
 
15
- | What goes wrong | What kaizen does |
16
- | :--- | :--- |
17
- | The agent decides what "done" means on its own, and you find out from the diff | It writes a plan first, including what *wrong* would look like, and waits for your approve or change |
18
- | The author grades its own work, so its blind spot is in the code *and* in the check | A second agent that never saw the work reviews it, from the request, the plan and the diff only |
19
- | Findings get fixed in an endless loop, or silently dropped | Serious findings are fixed and re-checked, at most twice; the rest come to you, and what is left goes to a backlog |
20
- | Requests live in sheets, docs and social posts; the plan in one chat, the review in another, the commit somewhere else | One board, from an icebox that gathers those requests to an idea, a plan, the build, the review, the fix loop and the commit, every step on file |
22
+ <p align="center">
23
+ <img src="https://raw.githubusercontent.com/hfadhlullah/kaizen/main/assets/readme/what-kaizen-does.png" alt="What kaizen does: plan first, a reviewer that starts cold, a fix loop that ends, one board, requests from where they live, a notebook agents read, on your phone, any agent. Built by Husein Fadhlullah" width="100%">
24
+ </p>
21
25
 
22
- Many assistants now ask for approval before they act, and some run a checker on their own draft. The difference here is *who* checks: a reviewer that shares the author's context shares its blind spots. Kaizen's reviewer starts cold, on purpose.
26
+ <details>
27
+ <summary><b>What kaizen does</b>, as text</summary>
23
28
 
24
- Every stage is a file in `.kaizen/runs/<id>/`, so a run can be resumed later, by another session or another tool (Claude Code, Codex, Antigravity), and read back when someone asks why the code is the way it is.
25
-
26
- ## Who it is for
27
-
28
- **Good fit if you:**
29
+ | | |
30
+ |---|---|
31
+ | **Any kind of work** | Each run works out what it is making (code, writing, communication, operations, research) and plans, builds and reviews it in those terms. Videos and decks go through the bundled `kaizen-video` (films from scratch, or editing your own footage) and `kaizen-deck` skills; books go through `kaizen-book` (interview, writing, proofreading, then an e-book reader plus colour and black-and-white print PDFs). |
32
+ | **Plan first** | The agent writes what it will do, and what *wrong* would look like, then waits for your approve or change. |
33
+ | **A reviewer that starts cold** | A second agent reviews from the request, the plan and the diff only. It never saw the work, so it has nothing to defend. |
34
+ | **A fix loop that ends** | Serious findings are fixed and re-checked, at most twice. The rest come to you; what is left goes to a backlog. |
35
+ | **One board** | `kaizen web`: icebox, ideas, plan, build, review, fix, commit and push, in the browser or the terminal. |
36
+ | **Requests from where they live** | Sheets, docs and social posts are gathered into an icebox, grouped, and promoted to ideas when you pick them. |
37
+ | **A notebook agents read first** | An Obsidian-compatible vault under `.kaizen/notes/` that every stage reads before it plans or builds. |
38
+ | **On your phone** | The board installs as a PWA over Tailscale. `kaizen setup` prints the QR code. |
39
+ | **Any agent, any session** | Every stage is a file in `.kaizen/runs/<id>/`, so another session or another tool can pick a run up. |
40
+
41
+ </details>
42
+
43
+ <p align="center">
44
+ <img src="https://raw.githubusercontent.com/hfadhlullah/kaizen/main/assets/hero.gif" alt="Animated walkthrough of the kaizen board, from a gathered request to a reviewed, fixed run" width="100%">
45
+ </p>
29
46
 
30
- - Build and maintain a product, and want one place for it: requests gathered from where they live, then plan, build, review and commit, without switching tools.
31
- - Already use an AI coding agent, and have caught it confidently wrong more than once.
32
- - Change things where a mistake is expensive: money, data, migrations, anything a customer sees.
33
- - Want to see and steer the plan before code is written, not reverse-engineer it from a diff.
47
+ ## The problem
34
48
 
35
- **Not for you if:**
49
+ You ask an AI coding agent to retry a payment webhook when the bank times out. It starts editing at once, then says *"Done, tests pass."* The tests do pass. They were written by the same agent, from the same assumption: that a timeout means the charge failed. Sometimes the bank said yes and the reply got lost. Now a retry charges the customer twice, and nothing in the agent's report would make you look.
36
50
 
37
- - Your changes are typos and one-liners. A run costs several times one prompt; kaizen takes a short path for small work, but it is not free.
38
- - You want the agent to ship without you. Kaizen is built to stop and ask.
39
- - You do not use an AI coding agent. Kaizen runs inside one; it is not one.
51
+ None of this is a lack of intelligence. It is a missing workflow. Many assistants now ask before they act, and some check their own draft. The difference is *who* checks: a reviewer that shares the author's context shares its blind spots. Kaizen's reviewer starts cold, on purpose.
40
52
 
41
53
  ## Install
42
54
 
43
- ### macOS and Linux
44
-
45
- ```bash
46
- curl -fsSL https://bun.sh/install | bash && ~/.bun/bin/bunx kaizen-agent
55
+ 1. **Install** on macOS or Linux:
56
+ ```bash
57
+ curl -fsSL https://bun.sh/install | bash && ~/.bun/bin/bunx kaizen-agent
58
+ ```
59
+ On Windows (beta), in PowerShell:
60
+ ```powershell
61
+ irm bun.sh/install.ps1 | iex; & "$env:USERPROFILE\.bun\bin\bunx.exe" kaizen-agent
62
+ ```
63
+ Already have Bun? `bunx kaizen-agent`.
64
+ 2. **Run `kaizen setup`.** It walks you through your project, settings, the board and the phone app.
65
+ 3. **Open the board** with `kaizen web`, or type `/kaizen <request>` in your agent.
66
+
67
+ <p align="center">
68
+ <img src="https://raw.githubusercontent.com/hfadhlullah/kaizen/main/assets/board-v2.png" alt="The kaizen board: icebox, ideas, plan, build, review and done columns" width="100%">
69
+ </p>
70
+
71
+ ## Requirements
72
+
73
+ | | |
74
+ |---|---|
75
+ | Runtime | [Bun](https://bun.sh) 1.1 or newer. The installer above gets it. |
76
+ | Agent | Claude Code, Codex, Antigravity or Gemini CLI. Claude Code gets the full flow with separate subagents; the others run every stage in one session. |
77
+ | Git | Needed for the board's commit and push. Optional otherwise. |
78
+ | OS | macOS, Linux. Windows in beta. |
79
+ | Phone app | [Tailscale](https://tailscale.com/download) on the PC and the phone, same account. |
80
+
81
+ ## How it works
82
+
83
+ ```mermaid
84
+ flowchart LR
85
+ R[Request] --> P[Plan]
86
+ P --> A{You approve}
87
+ A -- change --> P
88
+ A -- yes --> B[Build]
89
+ B --> V[Review<br/>cold agent]
90
+ V -- high findings --> F[Fix] --> V
91
+ V -- clean --> D[Done]
92
+ V -. the rest .-> L[Backlog]
47
93
  ```
48
94
 
49
- Already have Bun? Run `bunx kaizen-agent`.
50
-
51
- kaizen needs an AI coding agent such as [Claude Code](https://docs.claude.com/en/docs/claude-code). If none is installed, the installer lets you pick one, runs its official installer after you say yes, and then carries on. Git is needed for the board's commit and push.
52
-
53
- ### Windows
54
-
55
- Windows support is still in beta. In PowerShell:
56
-
57
- ```powershell
58
- irm bun.sh/install.ps1 | iex; & "$env:USERPROFILE\.bun\bin\bunx.exe" kaizen-agent
59
- ```
95
+ <p align="center">
96
+ <img src="https://raw.githubusercontent.com/hfadhlullah/kaizen/main/assets/readme/architecture.png" alt="From idea to finished work: your idea, plan, build, a review in a separate context, a refine loop, and the finished app, document, video or game" width="100%">
97
+ </p>
60
98
 
61
- Already have Bun? Run `bunx kaizen-agent`. As on macOS and Linux, the installer offers to install an AI coding agent when none is found; Git is only needed for the board's commit and push.
99
+ Each stage is its own agent with a clean context: planner, builder, reviewer. They never share a context window. The main thread owns the run's state in `.kaizen/runs/<id>/state.json`; the plan, the build notes, the review and every fix round are files beside it. Small changes take a short path in one session, so a typo does not cost a payroll change.
62
100
 
63
- ## Use the board
101
+ ## What kaizen changes on your machine
64
102
 
65
- ```bash
66
- kaizen web
67
- ```
103
+ - Installs itself to `~/kaizen`, and keeps settings and the project list in `~/.kaizen/`.
104
+ - Links its skills, agents and `/kaizen-*` commands into each agent it finds: `~/.claude`, `~/.codex`, `~/.agents`, `~/.gemini`. Re-running is safe.
105
+ - Per project, `kaizen init` writes `.kaizen/` and, in a git repo, a `.gitignore` entry that keeps run state out of commits but keeps the `spec*.md` files and `config.yml`.
106
+ - `kaizen web` serves the board on `127.0.0.1:7420`. After `kaizen setup` it is also reachable from your own Tailscale network, nothing wider; never use `tailscale funnel`.
107
+ - `kaizen uninstall` removes the app and its links, and asks whether to keep your runs and settings (`--purge` deletes them too).
68
108
 
69
- <img src="https://raw.githubusercontent.com/hfadhlullah/kaizen/main/assets/board-v2.png" alt="kaizen board" width="760">
109
+ ## Who it is for
70
110
 
71
- The board opens in your browser. The side menu holds the board, notes, the icebox of gathered requests and settings, with a light and dark switch. From there you can start a run, approve or revise its plan, pick which review findings to fix, then commit and push.
111
+ **Good fit if you** build and maintain a product with an AI coding agent, have caught it confidently wrong more than once, and change things where a mistake is expensive: money, data, migrations, anything a customer sees.
72
112
 
73
- Right after installing, `kaizen setup` walks you through your project, settings, this board and the phone app (it prints a QR code to scan).
113
+ **Not for you if** your changes are typos and one-liners, or you want the agent to ship without you. Kaizen is built to stop and ask.
74
114
 
75
- ### Phone app
115
+ ## Phone app
76
116
 
77
117
  The board is a PWA: installed from the browser, it opens full screen with the kaizen icon. It needs [Tailscale](https://tailscale.com/download) on this PC and on your phone, both signed in to the same account.
78
118
 
@@ -82,32 +122,18 @@ kaizen setup # guided: starts the board, runs tailscale serve, prints a Q
82
122
 
83
123
  Scan the QR code with your phone camera, then **Add to Home screen** (iOS: Share menu; Android: browser menu, *Install app*).
84
124
 
85
- By hand: run `kaizen web`, then `tailscale serve --bg 127.0.0.1:7420`, add `<pc>.<tailnet>.ts.net` to `~/.kaizen/web-hosts`, and open `https://<pc>.<tailnet>.ts.net` on the phone. Details in [docs/guide.md](docs/guide.md#from-your-phone-tailscale). Never use `tailscale funnel` — it publishes the board to the internet.
125
+ By hand: run `kaizen web`, then `tailscale serve --bg 127.0.0.1:7420`, add `<pc>.<tailnet>.ts.net` to `~/.kaizen/web-hosts`, and open `https://<pc>.<tailnet>.ts.net` on the phone. Details in [docs/guide.md](docs/guide.md#in-a-browser). Never use `tailscale funnel` — it publishes the board to the internet.
86
126
 
87
127
  To add a launcher (Spotlight on macOS, the app menu on Linux, a Desktop shortcut on Windows), run `kaizen web --shortcut` once.
88
128
 
89
129
  ## Development
90
130
 
91
- TypeScript on [Bun](https://bun.sh) 1.1 or newer, zero runtime dependencies. No build step for the CLI — `cli/*.ts` runs as-is — and the web front end ships prebuilt.
131
+ TypeScript on Bun, zero runtime dependencies. No build step for the CLI.
92
132
 
93
133
  ```bash
94
134
  git clone https://github.com/hfadhlullah/kaizen.git && cd kaizen
95
- bun install # dev dependencies only
96
- bun test # the suite; what CI runs
97
- ```
98
-
99
- To work on kaizen, install from your checkout so every link points at it:
100
-
101
- ```bash
102
- bun run cli/install.ts
103
- ```
104
-
105
- Skills, agents and commands are symlinked into every AI agent it finds; re-running is safe. From a checkout the workflow updates on any run, and `kaizen upgrade` leaves your local edits alone — update them with git.
106
-
107
- After editing `web/notebook.ts` or `web/notes-lib.ts`, rebuild the committed bundle:
108
-
109
- ```bash
110
- bun run build:web # rewrites web/notebook.js; a test fails if it goes stale
135
+ bun install && bun test
136
+ bun run cli/install.ts # install from this checkout
111
137
  ```
112
138
 
113
139
  ### Layout
@@ -115,7 +141,7 @@ bun run build:web # rewrites web/notebook.js; a test fails if it goes st
115
141
  | Path | What it is |
116
142
  |---|---|
117
143
  | `cli/` | The installer and the runtime behind every `kaizen` command: `install.ts` (the CLI), `state.ts` (runs), `web.ts` (board server), `dashboard.ts` (terminal board), `sources.ts`, `settings.ts`, `notes.ts`. Tests beside them. |
118
- | `skills/` | The workflow as Agent Skills folders: `kaizen/` (`SKILL.md`, the `spec-*.md` stage contracts, `adapters.md`, `config.default.yml`), `kaizen-help/`, `story-deck/`, `story-video/`. |
144
+ | `skills/` | The workflow as Agent Skills folders: `kaizen/` (`SKILL.md`, the `spec-*.md` stage contracts, `adapters.md`, `config.default.yml`), `kaizen-help/`, and `media/` with `kaizen-deck/`, `kaizen-video/`, `kaizen-book/`. |
119
145
  | `agents/` | The Claude Code subagents: `kaizen-planner`, `kaizen-builder`, `kaizen-reviewer`. |
120
146
  | `commands/` | The `/kaizen-*` slash commands, one file each. |
121
147
  | `web/` | The board and notebook front end: `board.html`, `notes.html`, `notebook.ts`. |
@@ -124,20 +150,21 @@ bun run build:web # rewrites web/notebook.js; a test fails if it goes st
124
150
  | `design.md` | Design notes for the repo. |
125
151
  | `.kaizen/` | This repo's own runs — kaizen is built with kaizen. Gitignored: a fresh clone gets it on its first run. |
126
152
 
127
- ### Kaizen Bot
128
-
129
- ```bash
130
- cd bot
131
- cp .env.example .env # PROVIDER, MODEL, that provider's key
132
- bun start # http://127.0.0.1:7430 — run `kaizen web` first
133
- bun test
134
- bun run build # the desktop app, into dist/
135
- ```
153
+ Kaizen Bot: [bot/README.md](bot/README.md). Other docs: [docs/README.md](docs/README.md). Pull requests welcome; CI runs `bun test` on Ubuntu and Windows and it must pass. For non-trivial work, dogfood: `/kaizen <change>` in this repo plans, builds and reviews it — [`skills/kaizen/spec.md`](skills/kaizen/spec.md) is the contract the stages follow.
136
154
 
137
- ### Contributing
155
+ ## What you can make
138
156
 
139
- Pull requests; CI runs `bun test` on Ubuntu and Windows and it must pass. For non-trivial work, dogfood: `/kaizen <change>` in this repo plans, builds and reviews it — [`skills/kaizen/spec.md`](skills/kaizen/spec.md) is the contract the stages follow.
157
+ <p align="center">
158
+ <img src="https://raw.githubusercontent.com/hfadhlullah/kaizen/main/assets/readme/bento.png" alt="What you can make with kaizen: an app, a document, a video, a game. Plan first, a fresh review, keep refining, keep it together" width="100%">
159
+ </p>
140
160
 
141
161
  ## License
142
162
 
143
- MIT
163
+ kaizen is licensed under the [MIT License](LICENSE).
164
+
165
+ <p>
166
+ <a href="https://github.com/hfadhlullah"><img src="https://github.com/hfadhlullah.png?size=64" width="32" height="32" alt="" align="left"></a>
167
+ &nbsp;Built by <a href="https://github.com/hfadhlullah"><b>Husein Fadhlullah</b></a>, with kaizen.
168
+ </p>
169
+
170
+ <sub>kaizen is not affiliated with Anthropic, OpenAI or Google. Claude Code, Codex, Antigravity and Gemini CLI are trademarks of their owners.</sub>
package/cli/build-web.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // `bun run build:web`: bundles the notebook's script and CodeMirror into
2
2
  // web/notebook.js, the in-card terminal and xterm.js into web/term.js (+ its
3
3
  // stylesheet, web/term.css), the board's modules (web/board/) into web/board.js and
4
- // Kaizen Bot's page modules (bot/web/app/) into bot/web/app.js, all committed, so an install or a checkout serves it with no
4
+ // Kaizen Bot's page modules (bot/web/app/) into bot/web/app.js, the Video page's script into web/video.js, all committed, so an install or a checkout serves it with no
5
5
  // install step and no network. The first line is a hash of what it was built from;
6
6
  // cli/notes.test.ts fails when a committed file no longer matches.
7
7
  import { createHash } from "node:crypto";
@@ -10,11 +10,12 @@ import { join, dirname } from "node:path";
10
10
 
11
11
  const root = dirname(import.meta.dir);
12
12
  const modules = (dir: string) => readdirSync(join(root, dir)).filter((f) => f.endsWith(".ts")).sort().map((f) => `${dir}/${f}`);
13
- const SOURCES = ["web/notebook.ts", "web/notes-lib.ts", "web/term.ts", ...modules("web/board"), ...modules("bot/web/app")];
13
+ const SOURCES = ["web/notebook.ts", "web/notes-lib.ts", "web/term.ts", "web/video.ts", ...modules("web/board"), ...modules("bot/web/app")];
14
14
  export const OUT = join(root, "web", "notebook.js");
15
15
  export const TERM_OUT = join(root, "web", "term.js");
16
16
  export const BOARD_OUT = join(root, "web", "board.js");
17
17
  export const BOT_OUT = join(root, "bot", "web", "app.js");
18
+ export const VIDEO_OUT = join(root, "web", "video.js");
18
19
 
19
20
  // The sources and the exact dependency versions: a CodeMirror bump is a rebuild too.
20
21
  export function sourceHash() {
@@ -31,6 +32,7 @@ if (import.meta.main) {
31
32
  ["web/term.ts", TERM_OUT, "terminal"],
32
33
  ["web/board/main.ts", BOARD_OUT, "board"],
33
34
  ["bot/web/app/main.ts", BOT_OUT, "bot page"],
35
+ ["web/video.ts", VIDEO_OUT, "video page"],
34
36
  ] as const;
35
37
  for (const [src, out, what] of bundles) {
36
38
  // The terminal, the board and the bot page are classic scripts: wrapped, or their
package/cli/hermes.ts ADDED
@@ -0,0 +1,65 @@
1
+ // WhatsApp through Hermes (`hermes send`): the chats it knows, and one file with a
2
+ // message to one of them. Always argv, never a shell line: the message is one argument.
3
+ import { spawn } from "node:child_process";
4
+ import { existsSync } from "node:fs";
5
+ import { join } from "node:path";
6
+
7
+ export type Chat = { id: string; name: string; type: string };
8
+ type Out = { code: number | string; out: string; err: string };
9
+
10
+ // On PATH first (tests put a fake there), else where Hermes' installer puts it: a board
11
+ // started from a desktop launcher often has no ~/.local/bin on its PATH.
12
+ function bin() {
13
+ const p = Bun.which("hermes", { PATH: process.env.PATH ?? "" });
14
+ if (p) return p;
15
+ const home = join(process.env.HOME || process.env.USERPROFILE || "", ".local", "bin", "hermes");
16
+ return existsSync(home) ? home : null;
17
+ }
18
+
19
+ function run(args: string[], ms: number): Promise<Out> {
20
+ const exe = bin();
21
+ if (!exe) return Promise.resolve({ code: "Hermes is not installed (no hermes on PATH or in ~/.local/bin)", out: "", err: "" });
22
+ return new Promise((done) => {
23
+ const p = spawn(exe, args, { env: process.env, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
24
+ let out = "", err = "";
25
+ p.stdout!.on("data", (c) => { out += c; });
26
+ p.stderr!.on("data", (c) => { err += c; });
27
+ const t = setTimeout(() => { p.kill("SIGKILL"); done({ code: `Hermes did not finish in ${Math.round(ms / 1000)} s`, out, err }); }, ms);
28
+ p.on("error", (e) => { clearTimeout(t); done({ code: `Hermes could not start: ${e.message}`, out, err }); });
29
+ p.on("close", (c) => { clearTimeout(t); done({ code: c ?? 1, out, err }); });
30
+ });
31
+ }
32
+ const lastLine = (s: string) => s.trim().split("\n").filter(Boolean).pop()?.replace(/^hermes send:\s*/, "") ?? "";
33
+
34
+ export async function targets(): Promise<{ chats: Chat[] } | { error: string }> {
35
+ const r = await run(["send", "--list", "whatsapp", "--json"], 10_000);
36
+ if (typeof r.code === "string") return { error: r.code };
37
+ let j: any;
38
+ try { j = JSON.parse(r.out); } catch { return { error: lastLine(r.err) || "Hermes gave no chat list" }; }
39
+ const wa = j?.platforms?.whatsapp;
40
+ if (!Array.isArray(wa) || !wa.length) return { error: "WhatsApp is not set up in Hermes (hermes gateway setup)" };
41
+ return { chats: wa.filter((c: any) => typeof c?.id === "string" && c.id).map((c: any) => ({ id: `whatsapp:${c.id}`, name: String(c.name ?? c.id), type: String(c.type ?? "") })) };
42
+ }
43
+
44
+ // Hermes attaches every MEDIA:<path> it finds in the text, so the user's words may not
45
+ // carry one: that would send a file the page never offered.
46
+ export function message(text: string, file: string): string | { error: string } {
47
+ if (text.length > 1000) return { error: "the message is over 1,000 characters" };
48
+ if (/MEDIA:/i.test(text)) return { error: "the message may not contain MEDIA:" };
49
+ if (/[`\n\r]/.test(file)) return { error: "the file's path cannot be passed to Hermes" };
50
+ // The path in backticks: Hermes reads a quoted path whole, spaces and all.
51
+ return `${text.trim()}${text.trim() ? "\n" : ""}MEDIA:\`${file}\``;
52
+ }
53
+
54
+ // Sent only when Hermes exits 0 with {success: true}; anything else is its own error.
55
+ export async function sendMedia(target: string, text: string, file: string): Promise<{ ok: true } | { ok: false; error: string }> {
56
+ const m = message(text, file);
57
+ if (typeof m !== "string") return { ok: false, error: m.error };
58
+ // "--" so a message starting with "-" is text, not an option.
59
+ const r = await run(["send", "-t", target, "--json", "--", m], 5 * 60_000);
60
+ if (typeof r.code === "string") return { ok: false, error: r.code };
61
+ let j: any = null;
62
+ try { j = JSON.parse(r.out); } catch { /* not JSON: fall through to stderr */ }
63
+ if (r.code === 0 && j?.success && !j.error) return { ok: true };
64
+ return { ok: false, error: String(j?.error ?? (lastLine(r.err) || `Hermes exited ${r.code}`)).slice(0, 400) };
65
+ }
package/cli/install.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  symlinkSync, mkdirSync, readdirSync, lstatSync, unlinkSync,
4
4
  existsSync, copyFileSync, readFileSync, appendFileSync, rmSync, writeFileSync, renameSync,
5
- statSync, cpSync, realpathSync, rmdirSync,
5
+ statSync, cpSync, realpathSync, rmdirSync, readlinkSync,
6
6
  } from "node:fs";
7
7
  import { join, dirname, basename, resolve } from "node:path";
8
8
  import { homedir, tmpdir } from "node:os";
@@ -46,8 +46,12 @@ const c = {
46
46
  green: (s: string) => `\x1b[32m${s}\x1b[0m`,
47
47
  };
48
48
 
49
- // Skill folders shipped in skills/ and linked into every agent.
50
- const SKILLS = ["kaizen", "kaizen-help", "story-deck", "story-video"];
49
+ // Skill folders shipped in skills/ and linked into every agent, by their last
50
+ // path segment: media/kaizen-deck is linked as skills/kaizen-deck.
51
+ const SKILLS = ["kaizen", "kaizen-help", "media/kaizen-deck", "media/kaizen-video", "media/kaizen-book"];
52
+ // Skill names an older release linked; uninstall drops them and install
53
+ // clears them when they are still our links.
54
+ const RENAMED_SKILLS = ["story-deck", "story-video", "kaizen-motion"];
51
55
 
52
56
  // Where each agent reads skills from. The skill folders follow the open Agent
53
57
  // Skills standard, so every one of these can run the workflow; agents/ and
@@ -244,7 +248,9 @@ async function removeEverything() {
244
248
 
245
249
  for (const root of roots) {
246
250
  for (const a of AGENTS) {
247
- for (const n of SKILLS) drop(join(root, a.dir, "skills", n));
251
+ for (const n of SKILLS) drop(join(root, a.dir, "skills", basename(n)));
252
+ // An old name is only ours while it is still a link; a real folder there is the user's.
253
+ for (const n of RENAMED_SKILLS) if (isLink(join(root, a.dir, "skills", n))) drop(join(root, a.dir, "skills", n));
248
254
  for (const sub of ["agents", "commands"]) {
249
255
  const dir = join(root, a.dir, sub);
250
256
  try {
@@ -311,6 +317,12 @@ function removeLink(p: string) {
311
317
  try { unlinkSync(p); } catch { rmdirSync(p); }
312
318
  }
313
319
 
320
+ // Setup only wires the board to the phone; it installs nothing, so no agent is needed.
321
+ if (args.has("setup")) {
322
+ await runSetup();
323
+ process.exit(0);
324
+ }
325
+
314
326
  // Nothing to install into. kaizen is a workflow an agent runs; on its own it does
315
327
  // nothing, so send them to the agent first rather than laying out files no reader
316
328
  // will ever load.
@@ -406,11 +418,6 @@ async function openUrl(url: string) {
406
418
  try { await Bun.$`${opener} ${url}`.quiet(); return true; } catch { return false; }
407
419
  }
408
420
 
409
- if (args.has("setup")) {
410
- await runSetup();
411
- process.exit(0);
412
- }
413
-
414
421
  async function runSetup() {
415
422
  const s = await import("./setup.ts");
416
423
  if (!process.stdin.isTTY) { console.log(s.manual()); return; }
@@ -523,7 +530,7 @@ if (interactive && !upgrade && Bun.argv.length === 2) {
523
530
  if (!stale) {
524
531
  const here = found(home);
525
532
  const linked = here.length > 0 && here.every((a) =>
526
- SKILLS.every((n) => existsSync(join(home, a.dir, "skills", n))));
533
+ SKILLS.every((n) => existsSync(join(home, a.dir, "skills", basename(n)))));
527
534
  if (linked) {
528
535
  const { dashboard } = await import("./dashboard.ts");
529
536
  await dashboard(repoDir, here.map((a) => a.name), async (action) => {
@@ -899,6 +906,13 @@ for (const [src, sub] of links) {
899
906
  wrote++;
900
907
  if (verbose) console.log(c.dim(` link ${name}`));
901
908
  }
909
+ if (!check) {
910
+ for (const a of targets) for (const n of RENAMED_SKILLS) {
911
+ const p = join(root, a.dir, "skills", n);
912
+ // Ours only: a dangling link to a folder of that same name, which kaizen has since renamed.
913
+ if (isLink(p) && !existsSync(p) && basename(readlinkSync(p).replace(/[\\/]+$/, "")) === n) removeLink(p);
914
+ }
915
+ }
902
916
 
903
917
  // A project's spec copies are what a run there reads, and they are copied once.
904
918
  function specDrift() {
@@ -0,0 +1,84 @@
1
+ // LocalSend's sending half against a fake device that speaks protocol v2: what it
2
+ // receives, and what each of its refusals reads as on the page.
3
+ import { test, expect } from "bun:test";
4
+ import { mkdtempSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+ import { randomBytes, X509Certificate, createPrivateKey } from "node:crypto";
8
+ import { identity, sendFile, type Device } from "./localsend.ts";
9
+
10
+ // prepare: what prepare-upload answers (a status, or "hang" to never answer).
11
+ function fakeDevice(prepare: number | "hang") {
12
+ const got: { prepare?: any; upload?: { query: Record<string, string>; bytes: Buffer } } = {};
13
+ const srv = Bun.serve({ port: 0, hostname: "127.0.0.1", async fetch(req) {
14
+ const u = new URL(req.url);
15
+ if (u.pathname === "/api/localsend/v2/prepare-upload") {
16
+ got.prepare = await req.json();
17
+ if (prepare === "hang") { await Bun.sleep(5000); return new Response(null, { status: 500 }); }
18
+ if (prepare !== 200) return new Response(null, { status: prepare });
19
+ return Response.json({ sessionId: "s-1", files: { f1: "tok-1" } });
20
+ }
21
+ if (u.pathname === "/api/localsend/v2/upload") {
22
+ const q = Object.fromEntries(u.searchParams);
23
+ if (q.sessionId !== "s-1" || q.token !== "tok-1" || q.fileId !== "f1") return new Response(null, { status: 403 });
24
+ got.upload = { query: q, bytes: Buffer.from(await req.arrayBuffer()) };
25
+ return new Response(null, { status: 200 });
26
+ }
27
+ return new Response(null, { status: 404 });
28
+ } });
29
+ const dev: Device = { alias: "Pixel 8", deviceModel: "Pixel", deviceType: "mobile", ip: "127.0.0.1", port: srv.port, protocol: "http", fingerprint: "fp" };
30
+ return { dev, got, stop: () => srv.stop(true) };
31
+ }
32
+
33
+ function file() {
34
+ const f = join(mkdtempSync(join(tmpdir(), "kaizen-ls-")), "demo-9x16.mp4");
35
+ writeFileSync(f, randomBytes(300_000));
36
+ return f;
37
+ }
38
+
39
+ test("a send carries the file's bytes with the session and token the device gave", async () => {
40
+ const d = fakeDevice(200), f = file(), steps: string[] = [];
41
+ try {
42
+ expect(await sendFile(d.dev, f, (p) => steps.push(p.phase))).toEqual({ ok: true });
43
+ expect(d.got.prepare.files.f1).toMatchObject({ id: "f1", fileName: "demo-9x16.mp4", size: 300_000, fileType: "video/mp4" });
44
+ expect(d.got.prepare.info.alias).toStartWith("kaizen");
45
+ expect(d.got.upload!.query).toEqual({ sessionId: "s-1", fileId: "f1", token: "tok-1" });
46
+ expect(d.got.upload!.bytes.equals(await Bun.file(f).bytes().then(Buffer.from))).toBe(true);
47
+ expect(steps[0]).toBe("waiting");
48
+ expect(steps.at(-1)).toBe("uploading");
49
+ } finally { d.stop(); }
50
+ });
51
+
52
+ test("a refusal, a busy device and no answer each say so, never sent", async () => {
53
+ const f = file();
54
+ for (const [status, words] of [[403, "Pixel 8 said no"], [409, "Pixel 8 is busy with another transfer"], [401, "PIN"], [500, "Pixel 8 answered 500"]] as const) {
55
+ const d = fakeDevice(status);
56
+ try {
57
+ const r = await sendFile(d.dev, f);
58
+ expect(r.ok).toBe(false);
59
+ expect((r as any).error).toContain(words);
60
+ expect(d.got.upload).toBeUndefined();
61
+ } finally { d.stop(); }
62
+ }
63
+ const d = fakeDevice("hang");
64
+ try {
65
+ const r = await sendFile(d.dev, f, () => {}, { acceptMs: 300 });
66
+ expect(r).toEqual({ ok: false, error: "No answer from Pixel 8 in 1 minute" });
67
+ } finally { d.stop(); }
68
+ // Nobody there at all.
69
+ const r = await sendFile({ ...d.dev, port: 1 }, f);
70
+ expect(r.ok).toBe(false);
71
+ expect((r as any).error).toStartWith("Could not reach Pixel 8");
72
+ });
73
+
74
+ // A real LocalSend in HTTPS mode refuses a sender without a client certificate
75
+ // ("tlsv13 alert certificate required"), and reads the sender's fingerprint as its SHA-256.
76
+ test("we carry a self-signed certificate whose SHA-256 is our fingerprint", () => {
77
+ const me = identity(), x = new X509Certificate(me.cert);
78
+ expect(x.subject).toBe("CN=kaizen");
79
+ expect(x.verify(x.publicKey)).toBe(true);
80
+ expect(x.checkPrivateKey(createPrivateKey(me.key))).toBe(true);
81
+ expect(x.fingerprint256.replace(/:/g, "")).toBe(me.fingerprint);
82
+ expect(new Date(x.validTo).getTime()).toBeGreaterThan(Date.now() + 365 * 86_400_000);
83
+ expect(identity()).toBe(me);
84
+ });