@zuzuucodes/cli 1.4.0 → 1.6.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 (211) hide show
  1. package/bin/zuzuu.mjs +20 -4
  2. package/package.json +1 -1
  3. package/web-app/dist/auth.js +91 -0
  4. package/web-app/dist/server.js +16 -79
  5. package/web-app/dist/zuzuu-cli.js +124 -0
  6. package/web-app/dist/{zuzuu-api.js → zuzuu-routes.js} +158 -133
  7. package/web-app/web-dist/assets/CommandPalette-DhBdR7X3.js +45 -0
  8. package/web-app/web-dist/assets/DiffTab-CqxwSjI2.js +1 -0
  9. package/web-app/web-dist/assets/EditorPane-94QPFR9R.js +41 -0
  10. package/web-app/web-dist/assets/MonacoFile-D76epTrG.js +1 -0
  11. package/web-app/web-dist/assets/angular-html-BVBpGdXr.js +1 -0
  12. package/web-app/web-dist/assets/{angular-ts-CD_OonCa.js → angular-ts-BfdufMKP.js} +1 -1
  13. package/web-app/web-dist/assets/{apl-uOGC3x4e.js → apl-DWBSSoBH.js} +1 -1
  14. package/web-app/web-dist/assets/{astro-B6ybQmWG.js → astro-3LtMP0Sq.js} +1 -1
  15. package/web-app/web-dist/assets/{blade-B1QGRlVx.js → blade-llJRbbtR.js} +1 -1
  16. package/web-app/web-dist/assets/c-Wt1voDr2.js +1 -0
  17. package/web-app/web-dist/assets/{cobol-BgqgtYWn.js → cobol-x_HIyl2P.js} +1 -1
  18. package/web-app/web-dist/assets/{coffee-0wIRKYlr.js → coffee-CThvmt4R.js} +1 -1
  19. package/web-app/web-dist/assets/cpp-NtAeskI3.js +1 -0
  20. package/web-app/web-dist/assets/{crystal-CyTK3qFN.js → crystal-DNu_sX0G.js} +1 -1
  21. package/web-app/web-dist/assets/css-DJp_X0uY.js +1 -0
  22. package/web-app/web-dist/assets/{cssMode-B9jnrWOz.js → cssMode-ByQBaInt.js} +1 -1
  23. package/web-app/web-dist/assets/dist-DQqjtuhV.js +153 -0
  24. package/web-app/web-dist/assets/{edge-CvML9pwC.js → edge-ozw5tpLl.js} +1 -1
  25. package/web-app/web-dist/assets/{editor.api2-BmGoRSl4.js → editor.api2-C7skgoRB.js} +1 -1
  26. package/web-app/web-dist/assets/{elixir-CrjqTiSc.js → elixir-VhA6FeZt.js} +1 -1
  27. package/web-app/web-dist/assets/{elm-C4JtJ0Au.js → elm-dREJmIFz.js} +1 -1
  28. package/web-app/web-dist/assets/{erb-Cmeb-29V.js → erb-CIg6G69l.js} +1 -1
  29. package/web-app/web-dist/assets/{freemarker2-B5LAi19B.js → freemarker2-CBBwP9JV.js} +1 -1
  30. package/web-app/web-dist/assets/{git-rebase-CXqdToiP.js → git-rebase-B44mJPta.js} +1 -1
  31. package/web-app/web-dist/assets/{glimmer-js-Kq-kdTyV.js → glimmer-js-vH_gHG0-.js} +1 -1
  32. package/web-app/web-dist/assets/{glimmer-ts-D0RKLJNf.js → glimmer-ts--abOzSAQ.js} +1 -1
  33. package/web-app/web-dist/assets/glsl-Dv5r7kPw.js +1 -0
  34. package/web-app/web-dist/assets/graphql-CB4jsw2E.js +1 -0
  35. package/web-app/web-dist/assets/{hack-trjVF3Po.js → hack-DvEYX148.js} +1 -1
  36. package/web-app/web-dist/assets/haml-zE6W3STP.js +1 -0
  37. package/web-app/web-dist/assets/{handlebars-B8_x7Zx7.js → handlebars-CzBR2SDs.js} +1 -1
  38. package/web-app/web-dist/assets/{handlebars-g7ZhGhI_.js → handlebars-tXdfxEd6.js} +1 -1
  39. package/web-app/web-dist/assets/html-C8UlPnhE.js +1 -0
  40. package/web-app/web-dist/assets/{html-CfvRMgoC.js → html-DgPn1QYH.js} +1 -1
  41. package/web-app/web-dist/assets/{html-derivative-BYX_F_XH.js → html-derivative-CY6NRz-J.js} +1 -1
  42. package/web-app/web-dist/assets/{htmlMode-Bi8vSvwb.js → htmlMode-BtdIDgA2.js} +1 -1
  43. package/web-app/web-dist/assets/{http-BIVDpHT-.js → http-Cyd7bS_S.js} +1 -1
  44. package/web-app/web-dist/assets/{hurl-CFsshMju.js → hurl-CWPsiEpf.js} +1 -1
  45. package/web-app/web-dist/assets/index-B27_WOhS.css +2 -0
  46. package/web-app/web-dist/assets/index-De6DWTZM.js +7 -0
  47. package/web-app/web-dist/assets/java-CGc3VwQr.js +1 -0
  48. package/web-app/web-dist/assets/{javascript-Bxx2wV4w.js → javascript-5m05n-Be.js} +1 -1
  49. package/web-app/web-dist/assets/javascript-CUt1pgmJ.js +1 -0
  50. package/web-app/web-dist/assets/{jinja-_ZS5zWwe.js → jinja-CD-Z-FLd.js} +1 -1
  51. package/web-app/web-dist/assets/{jison-D8mMEpcs.js → jison-imPNup1l.js} +1 -1
  52. package/web-app/web-dist/assets/json-Bg9ijW3F.js +1 -0
  53. package/web-app/web-dist/assets/{jsonMode-C6ELX5GM.js → jsonMode-BG32YnTY.js} +1 -1
  54. package/web-app/web-dist/assets/jsx-CY6oMTks.js +1 -0
  55. package/web-app/web-dist/assets/{julia-D4h2DZrs.js → julia-Dc3O-irA.js} +1 -1
  56. package/web-app/web-dist/assets/{just-bMqQi3xg.js → just-BhOq_Kbv.js} +1 -1
  57. package/web-app/web-dist/assets/{latex-DThYi3CX.js → latex-Cu4Y1d5w.js} +1 -1
  58. package/web-app/web-dist/assets/lib-KIOQTlcs.js +1 -0
  59. package/web-app/web-dist/assets/{liquid-CUjzzP4r.js → liquid-3ZnQzTbs.js} +1 -1
  60. package/web-app/web-dist/assets/{liquid-CesB-zzl.js → liquid-CvXMrjlQ.js} +1 -1
  61. package/web-app/web-dist/assets/{lspLanguageFeatures-gTnJsses.js → lspLanguageFeatures-6KXALSrl.js} +1 -1
  62. package/web-app/web-dist/assets/lua-BjLEUjKY.js +1 -0
  63. package/web-app/web-dist/assets/{marko-yoGoLK2m.js → marko-DvhNOisQ.js} +1 -1
  64. package/web-app/web-dist/assets/{mdc-BvtXU6eH.js → mdc-Bm9TpL1X.js} +1 -1
  65. package/web-app/web-dist/assets/{mdx-DrXGQbNB.js → mdx-DffTEkNE.js} +1 -1
  66. package/web-app/web-dist/assets/{monaco-setup-CsR6EfHe.js → monaco-setup-DM3A5_VI.js} +3 -3
  67. package/web-app/web-dist/assets/{nginx-DoUz032F.js → nginx-Bhc82uuv.js} +1 -1
  68. package/web-app/web-dist/assets/{nim-B0Pl8B4R.js → nim-DXTVBFnF.js} +1 -1
  69. package/web-app/web-dist/assets/{perl-D2tfAALb.js → perl-C7veXV9z.js} +1 -1
  70. package/web-app/web-dist/assets/{php-BImCcX5X.js → php-BRiuMnnr.js} +1 -1
  71. package/web-app/web-dist/assets/{pug-BcnpC8P_.js → pug-C5hz5LQ7.js} +1 -1
  72. package/web-app/web-dist/assets/{python-ypRCBnvu.js → python-DyLAD3Wt.js} +1 -1
  73. package/web-app/web-dist/assets/{qml-DFDAunHY.js → qml-BdUV3aTS.js} +1 -1
  74. package/web-app/web-dist/assets/r-8R7vtdQc.js +1 -0
  75. package/web-app/web-dist/assets/{razor-aqrhpwqZ.js → razor-C49xQTPQ.js} +1 -1
  76. package/web-app/web-dist/assets/{razor-1_376SZM.js → razor-DRL52XO2.js} +1 -1
  77. package/web-app/web-dist/assets/react-vendor-CCIEwYL0.js +9 -0
  78. package/web-app/web-dist/assets/regexp-Omp9DhTb.js +1 -0
  79. package/web-app/web-dist/assets/{rst-2vG6f11Y.js → rst-BHX71KW9.js} +1 -1
  80. package/web-app/web-dist/assets/{ruby-Dj6bCFXR.js → ruby-B--HzjGU.js} +1 -1
  81. package/web-app/web-dist/assets/{sas-BhVZ4qL2.js → sas-DrLaYOK_.js} +1 -1
  82. package/web-app/web-dist/assets/scss-DdSxiZKl.js +1 -0
  83. package/web-app/web-dist/assets/shellscript-DwcUjJBL.js +1 -0
  84. package/web-app/web-dist/assets/{shellsession-CyO2fnhB.js → shellsession-CPZkydE6.js} +1 -1
  85. package/web-app/web-dist/assets/{soy-DIkw6E88.js → soy-Br5FhD7c.js} +1 -1
  86. package/web-app/web-dist/assets/sql-DNssxck8.js +1 -0
  87. package/web-app/web-dist/assets/{stata-DvkM932O.js → stata-DXn1tqOr.js} +1 -1
  88. package/web-app/web-dist/assets/{surrealql-B4-Q8tqV.js → surrealql-IeLNQw0f.js} +1 -1
  89. package/web-app/web-dist/assets/{svelte-p6yBy-Ki.js → svelte-DOdLCIlh.js} +1 -1
  90. package/web-app/web-dist/assets/{templ-C7EkuiZr.js → templ-CIwIngms.js} +1 -1
  91. package/web-app/web-dist/assets/{tex-DkmD8uFC.js → tex-D8QMumu5.js} +1 -1
  92. package/web-app/web-dist/assets/{ts-tags-U-hncHg4.js → ts-tags-BMVY4q-l.js} +1 -1
  93. package/web-app/web-dist/assets/{tsMode-a8OvovQd.js → tsMode-BndVBac5.js} +1 -1
  94. package/web-app/web-dist/assets/tsx-5Eka4NBX.js +1 -0
  95. package/web-app/web-dist/assets/{twig-CU0OP-IA.js → twig-C8o_5mgw.js} +1 -1
  96. package/web-app/web-dist/assets/{typescript-DnLjiKtn.js → typescript-B1w9vqKF.js} +1 -1
  97. package/web-app/web-dist/assets/typescript-DOu2WMV5.js +1 -0
  98. package/web-app/web-dist/assets/{vue-Db7nY3ba.js → vue-BU18DNDL.js} +1 -1
  99. package/web-app/web-dist/assets/{vue-html-BvAbiAw1.js → vue-html-BeluIYX0.js} +1 -1
  100. package/web-app/web-dist/assets/{vue-vine-BEaIQIlA.js → vue-vine-DGUAbOCX.js} +1 -1
  101. package/web-app/web-dist/assets/{xml-an4Nuuqq.js → xml-D8uAlVv5.js} +1 -1
  102. package/web-app/web-dist/assets/xml-DIqSwXR3.js +1 -0
  103. package/web-app/web-dist/assets/{xsl-D3NQgH22.js → xsl-Ct_-YIAy.js} +1 -1
  104. package/web-app/web-dist/assets/xterm-B1ffpRuj.js +36 -0
  105. package/web-app/web-dist/assets/xterm-addons-psDEiUMC.js +136 -0
  106. package/web-app/web-dist/assets/{yaml-Diiu6O9P.js → yaml-Bb7jXyQv.js} +1 -1
  107. package/web-app/web-dist/assets/yaml-DTtCYNlS.js +1 -0
  108. package/web-app/web-dist/index.html +6 -3
  109. package/zuzuu/actions/convert.mjs +10 -9
  110. package/zuzuu/actions/dispatch.mjs +12 -7
  111. package/zuzuu/actions/inbox.mjs +5 -5
  112. package/zuzuu/actions/manifest.mjs +48 -30
  113. package/zuzuu/actions/schema.mjs +9 -3
  114. package/zuzuu/actions/trail.mjs +1 -1
  115. package/zuzuu/commands/act-author.mjs +23 -13
  116. package/zuzuu/commands/act.mjs +4 -6
  117. package/zuzuu/commands/capture.mjs +2 -2
  118. package/zuzuu/commands/code.mjs +2 -2
  119. package/zuzuu/commands/digest.mjs +2 -2
  120. package/zuzuu/commands/distill.mjs +15 -16
  121. package/zuzuu/commands/doctor.mjs +41 -19
  122. package/zuzuu/commands/enable.mjs +1 -1
  123. package/zuzuu/commands/eval.mjs +3 -36
  124. package/zuzuu/commands/explain.mjs +4 -4
  125. package/zuzuu/commands/faculty.mjs +158 -0
  126. package/zuzuu/commands/generation.mjs +5 -8
  127. package/zuzuu/commands/hook.mjs +14 -12
  128. package/zuzuu/commands/inbox.mjs +1 -6
  129. package/zuzuu/commands/init.mjs +18 -4
  130. package/zuzuu/commands/knowledge.mjs +1 -1
  131. package/zuzuu/commands/migrations/home.mjs +96 -0
  132. package/zuzuu/commands/migrations/index.mjs +48 -0
  133. package/zuzuu/commands/migrations/items.mjs +360 -0
  134. package/zuzuu/commands/migrations/proposals.mjs +100 -0
  135. package/zuzuu/commands/proposals.mjs +131 -0
  136. package/zuzuu/commands/review.mjs +13 -227
  137. package/zuzuu/commands/session.mjs +8 -2
  138. package/zuzuu/commands/sessions.mjs +159 -0
  139. package/zuzuu/commands/status.mjs +3 -3
  140. package/zuzuu/commands/trace.mjs +1 -1
  141. package/zuzuu/{capture-core.mjs → core/capture-core.mjs} +3 -3
  142. package/zuzuu/{store.mjs → core/store.mjs} +1 -1
  143. package/zuzuu/digest/compose.mjs +96 -0
  144. package/zuzuu/eval/score.mjs +14 -1
  145. package/zuzuu/faculties/actions/index.mjs +283 -0
  146. package/zuzuu/faculties/guardrails/index.mjs +320 -0
  147. package/zuzuu/faculties/instructions/index.mjs +288 -0
  148. package/zuzuu/faculties/knowledge/index.mjs +185 -0
  149. package/zuzuu/faculties/memory/index.mjs +124 -0
  150. package/zuzuu/faculty/envelope.mjs +290 -0
  151. package/zuzuu/faculty/generation/read.mjs +206 -0
  152. package/zuzuu/faculty/generation/write.mjs +207 -0
  153. package/zuzuu/faculty/items.mjs +81 -0
  154. package/zuzuu/faculty/module.mjs +74 -0
  155. package/zuzuu/faculty/pending.mjs +63 -0
  156. package/zuzuu/faculty/registry.mjs +204 -18
  157. package/zuzuu/faculty/render.mjs +59 -0
  158. package/zuzuu/faculty/trail.mjs +1 -1
  159. package/zuzuu/guardrails/engine.mjs +137 -0
  160. package/zuzuu/{scaffold.mjs → home/scaffold.mjs} +110 -39
  161. package/zuzuu/knowledge/items.mjs +56 -91
  162. package/zuzuu/live/install.mjs +1 -1
  163. package/zuzuu/live/live-store.mjs +2 -2
  164. package/zuzuu/live/reconcile.mjs +2 -2
  165. package/zuzuu/sessions/git.mjs +47 -0
  166. package/zuzuu/{session-git.mjs → sessions/session-git.mjs} +5 -43
  167. package/web-app/web-dist/assets/DiffTab-BuWonUNJ.js +0 -1
  168. package/web-app/web-dist/assets/MonacoFile-CL3DhFKG.js +0 -1
  169. package/web-app/web-dist/assets/angular-html-CmT26mqM.js +0 -1
  170. package/web-app/web-dist/assets/c-BvoqrSVH.js +0 -1
  171. package/web-app/web-dist/assets/cpp-BXsk94m0.js +0 -1
  172. package/web-app/web-dist/assets/css-Z8oOGxII.js +0 -1
  173. package/web-app/web-dist/assets/dist-ChcDQ_7s.js +0 -153
  174. package/web-app/web-dist/assets/glsl-KwyfU2aa.js +0 -1
  175. package/web-app/web-dist/assets/graphql-DSeOUAa2.js +0 -1
  176. package/web-app/web-dist/assets/haml-azVoxQRV.js +0 -1
  177. package/web-app/web-dist/assets/html-D_7P5S4m.js +0 -1
  178. package/web-app/web-dist/assets/index--5yy8RbA.js +0 -267
  179. package/web-app/web-dist/assets/index-BVG4hgk7.css +0 -2
  180. package/web-app/web-dist/assets/java-D4RbCvBe.js +0 -1
  181. package/web-app/web-dist/assets/javascript-Cb010CKM.js +0 -1
  182. package/web-app/web-dist/assets/json-DWgqV4D1.js +0 -1
  183. package/web-app/web-dist/assets/jsx-CZjSJa1f.js +0 -1
  184. package/web-app/web-dist/assets/lua-TGj_6NzO.js +0 -1
  185. package/web-app/web-dist/assets/r-fCpuAR7u.js +0 -1
  186. package/web-app/web-dist/assets/regexp-B4yxx-Ty.js +0 -1
  187. package/web-app/web-dist/assets/scss-QdjMO_xV.js +0 -1
  188. package/web-app/web-dist/assets/shellscript-BnlgeVVx.js +0 -1
  189. package/web-app/web-dist/assets/sql-DGnQv6iD.js +0 -1
  190. package/web-app/web-dist/assets/tsx-MJ0-9sYG.js +0 -1
  191. package/web-app/web-dist/assets/typescript-C17ZkDe8.js +0 -1
  192. package/web-app/web-dist/assets/xml-CA9lHFQV.js +0 -1
  193. package/web-app/web-dist/assets/yaml-CwRYMJka.js +0 -1
  194. package/zuzuu/actions/adapter.mjs +0 -130
  195. package/zuzuu/commands/migrate.mjs +0 -225
  196. package/zuzuu/digest.mjs +0 -149
  197. package/zuzuu/faculty/generation.mjs +0 -392
  198. package/zuzuu/guardrails/adapter.mjs +0 -134
  199. package/zuzuu/guardrails.mjs +0 -89
  200. package/zuzuu/instructions/adapter.mjs +0 -93
  201. package/zuzuu/knowledge/adapter.mjs +0 -99
  202. package/zuzuu/memory/adapter.mjs +0 -121
  203. package/zuzuu/miners/actions.mjs +0 -118
  204. package/zuzuu/miners/guardrails.mjs +0 -179
  205. package/zuzuu/miners/instructions.mjs +0 -157
  206. package/zuzuu/miners/knowledge.mjs +0 -25
  207. package/zuzuu/miners/memory.mjs +0 -27
  208. package/zuzuu/miners/registry.mjs +0 -31
  209. /package/web-app/web-dist/assets/{chunk-QTnfLwEv.js → rolldown-runtime-QTnfLwEv.js} +0 -0
  210. /package/zuzuu/{session.mjs → core/session.mjs} +0 -0
  211. /package/zuzuu/{inject.mjs → home/inject.mjs} +0 -0
package/bin/zuzuu.mjs CHANGED
@@ -21,11 +21,12 @@ import { doctor } from '../zuzuu/commands/doctor.mjs';
21
21
  import { enable, disable } from '../zuzuu/commands/enable.mjs';
22
22
  import { runHook } from '../zuzuu/commands/hook.mjs';
23
23
  import { remember, recall, knowledge } from '../zuzuu/commands/knowledge.mjs';
24
- import { review, proposals } from '../zuzuu/commands/review.mjs';
24
+ import { review } from '../zuzuu/commands/review.mjs';
25
+ import { proposals } from '../zuzuu/commands/proposals.mjs';
25
26
  import { distill } from '../zuzuu/commands/distill.mjs';
26
27
  import { digest } from '../zuzuu/commands/digest.mjs';
27
28
  import { act } from '../zuzuu/commands/act.mjs';
28
- import { migrate } from '../zuzuu/commands/migrate.mjs';
29
+ import { migrate } from '../zuzuu/commands/migrations/index.mjs';
29
30
  import { generation } from '../zuzuu/commands/generation.mjs';
30
31
  import { evalCmd } from '../zuzuu/commands/eval.mjs';
31
32
  import { code } from '../zuzuu/commands/code.mjs';
@@ -33,6 +34,8 @@ import { web } from '../zuzuu/commands/web.mjs';
33
34
  import { explain } from '../zuzuu/commands/explain.mjs';
34
35
  import { inbox } from '../zuzuu/commands/inbox.mjs';
35
36
  import { session } from '../zuzuu/commands/session.mjs';
37
+ import { sessions } from '../zuzuu/commands/sessions.mjs';
38
+ import { faculty } from '../zuzuu/commands/faculty.mjs';
36
39
 
37
40
  function parseArgs(argv) {
38
41
  const a = { _: [] };
@@ -74,6 +77,13 @@ usage: zuzuu <command> [options]
74
77
  recall "query" [--type t] [--attr k=v] [--related-to id] [--semantic]
75
78
  search knowledge: lexical · graph · semantic
76
79
  knowledge reindex|audit rebuild the search index · check registry/items health
80
+ faculty items <f> [--json|--jsonl]
81
+ list a faculty's envelope items (one doc · one line per item)
82
+ faculty schema <f> [--json]
83
+ print a faculty's payload schema (JSON-Schema subset)
84
+ faculty manifest <f> [--json]
85
+ print a faculty's module manifest (faculty.json)
86
+ faculty overview [--json] every faculty in one shot: ui + counts + top items + pending
77
87
  digest [--json] [--budget N]
78
88
  print the session-start grounding brief
79
89
  act [list|show <slug>|new <slug>|schema <slug>]
@@ -94,8 +104,12 @@ usage: zuzuu <command> [options]
94
104
  disable remove the background hooks
95
105
  session [status|merge|continue|discard]
96
106
  the invisible session branch (one per agent session)
107
+ sessions [--json] recorded sessions with lifecycle state labels
108
+ session inspect <id> [--json]
109
+ one session: trace summary + per-faculty mined signals
97
110
  eval [--faculty f] rank pending proposals by eval score, highest first
98
- migrate [--home] one-time migrators: proposal schema · --home moves agent/ → .zuzuu/
111
+ migrate [--home|--items] one-time migrators: proposal schema · --home moves agent/ → .zuzuu/
112
+ · --items rewrites legacy faculty shapes → the envelope standard
99
113
  doctor environment + session health (reconciles lost sessions)
100
114
  explain [topic] the 5 faculties + how graduation works
101
115
  version print version
@@ -117,7 +131,7 @@ switch (cmd) {
117
131
  case 'knowledge': await knowledge(args); break;
118
132
  case 'digest': digest(args); break;
119
133
  case 'act': act(args); break;
120
- case 'distill': distill(args); break;
134
+ case 'distill': await distill(args); break;
121
135
  case 'inbox': inbox(args); break;
122
136
  case 'review': await review(args); break;
123
137
  case 'proposals': proposals(args); break;
@@ -128,6 +142,8 @@ switch (cmd) {
128
142
  case 'disable': disable(args); break;
129
143
  case 'hook': runHook(args._[0], { host: args.host, session: args.session }); break;
130
144
  case 'session': session(args); break;
145
+ case 'sessions': sessions(args); break;
146
+ case 'faculty': faculty(args); break;
131
147
  case 'eval': evalCmd(args); break;
132
148
  case 'migrate': migrate(args); break;
133
149
  case 'generation': generation(args); break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuzuucodes/cli",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,91 @@
1
+ // The daemon's auth + request-origin gates, extracted from server.ts:
2
+ // Host-header allowlist (DNS rebinding), Origin allowlist (cross-site WS
3
+ // hijacking / CSRF), and token-in-URL → HttpOnly cookie auth. One AuthGate
4
+ // instance guards both the Hono HTTP app and the WS upgrade path.
5
+ import crypto from "node:crypto";
6
+ import { getCookie, setCookie } from "hono/cookie";
7
+ const AUTH_COOKIE = "webcode_auth";
8
+ const COOKIE_MAX_AGE = 30 * 24 * 3600;
9
+ export class AuthGate {
10
+ authSessions = new Set();
11
+ allowedHosts;
12
+ allowedOrigins;
13
+ token;
14
+ constructor(cfg) {
15
+ this.token = cfg.token;
16
+ const hostNames = ["127.0.0.1", "localhost", "[::1]"];
17
+ this.allowedHosts = new Set(hostNames.flatMap((h) => [h, `${h}:${cfg.port}`]));
18
+ this.allowedOrigins = new Set([
19
+ ...hostNames.map((h) => `http://${h}:${cfg.port}`),
20
+ ...(cfg.extraOrigins ?? []),
21
+ ]);
22
+ // hosted: also accept the public hostname (Fly's edge sets Host to it);
23
+ // Host/Origin defense stays on, just widened to the one public origin.
24
+ if (cfg.publicHost) {
25
+ this.allowedHosts.add(cfg.publicHost.toLowerCase());
26
+ this.allowedOrigins.add(`https://${cfg.publicHost}`);
27
+ this.allowedOrigins.add(`http://${cfg.publicHost}`);
28
+ }
29
+ }
30
+ /** Host allowlist defeats DNS rebinding: rebinding changes DNS, not the Host header. */
31
+ hostAllowed(host) {
32
+ return !!host && this.allowedHosts.has(host.toLowerCase());
33
+ }
34
+ /** Origin allowlist defeats cross-site WS hijacking / CSRF from arbitrary websites. */
35
+ originAllowed(origin) {
36
+ return origin === undefined || this.allowedOrigins.has(origin);
37
+ }
38
+ /** WS upgrade path: is the request's cookie an authenticated session? */
39
+ cookieAuthed(cookieHeader) {
40
+ if (!cookieHeader)
41
+ return false;
42
+ const match = /(?:^|;\s*)webcode_auth=([^;]+)/.exec(cookieHeader);
43
+ return !!match && this.authSessions.has(match[1]);
44
+ }
45
+ /** App-wide gate: Host/Origin allowlists + the ?token= → cookie exchange. */
46
+ gate() {
47
+ return async (c, next) => {
48
+ if (!this.hostAllowed(c.req.header("host"))) {
49
+ return c.text("forbidden host", 403);
50
+ }
51
+ if (!this.originAllowed(c.req.header("origin"))) {
52
+ return c.text("forbidden origin", 403);
53
+ }
54
+ // Token exchange: any page request carrying ?token= gets a cookie.
55
+ const token = c.req.query("token");
56
+ if (token && !c.req.path.startsWith("/api/")) {
57
+ if (!timingSafeEqualStr(token, this.token))
58
+ return c.text("invalid token", 403);
59
+ const secret = crypto.randomBytes(24).toString("base64url");
60
+ this.authSessions.add(secret);
61
+ setCookie(c, AUTH_COOKIE, secret, {
62
+ httpOnly: true,
63
+ sameSite: "Strict",
64
+ path: "/",
65
+ maxAge: COOKIE_MAX_AGE,
66
+ });
67
+ const url = new URL(c.req.url);
68
+ url.searchParams.delete("token");
69
+ // /auth?token=… exists so the Vite dev server can proxy the
70
+ // exchange; land on the app root afterwards either way.
71
+ const dest = url.pathname === "/auth" ? "/" : url.pathname + url.search;
72
+ return c.redirect(dest);
73
+ }
74
+ await next();
75
+ };
76
+ }
77
+ /** /api/* gate: only cookie-authenticated sessions pass. */
78
+ requireAuth() {
79
+ return async (c, next) => {
80
+ if (!this.authSessions.has(getCookie(c, AUTH_COOKIE) ?? "")) {
81
+ return c.json({ error: "unauthorized" }, 401);
82
+ }
83
+ await next();
84
+ };
85
+ }
86
+ }
87
+ function timingSafeEqualStr(a, b) {
88
+ const ba = Buffer.from(a);
89
+ const bb = Buffer.from(b);
90
+ return ba.length === bb.length && crypto.timingSafeEqual(ba, bb);
91
+ }
@@ -1,17 +1,17 @@
1
- import crypto from "node:crypto";
2
1
  import fs from "node:fs";
3
2
  import fsp from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import { Readable } from "node:stream";
6
5
  import { Hono } from "hono";
7
- import { getCookie, setCookie } from "hono/cookie";
8
6
  import { serve } from "@hono/node-server";
9
7
  import { WebSocketServer } from "ws";
10
8
  import { execFile } from "node:child_process";
11
9
  import { promisify } from "node:util";
12
10
  import { SessionManager } from "./sessions.js";
11
+ import { AuthGate } from "./auth.js";
13
12
  import { createFsApi } from "./fs-api.js";
14
- import { createZuzuuApi, runZuzuuMut } from "./zuzuu-api.js";
13
+ import { createZuzuuApi } from "./zuzuu-routes.js";
14
+ import { runZuzuuMut } from "./zuzuu-cli.js";
15
15
  import { search } from "./search.js";
16
16
  import { listFiles } from "./file-list.js";
17
17
  import { listWorkflows, saveWorkflow } from "./workflows.js";
@@ -23,8 +23,6 @@ const execFileAsync = promisify(execFile);
23
23
  import { handleTermSocket } from "./ws-term.js";
24
24
  import { handleFsSocket } from "./ws-fs.js";
25
25
  import { PathError, resolveSafe, safeJoin } from "./safe-path.js";
26
- const AUTH_COOKIE = "webcode_auth";
27
- const COOKIE_MAX_AGE = 30 * 24 * 3600;
28
26
  /** Host CLIs an agent/command session may run. Argv-spawned, never a shell. */
29
27
  const DEFAULT_COMMAND_ALLOWLIST = ["claude", "gemini", "codex", "pi", "opencode", "zuzuu"];
30
28
  const STATIC_MIME = {
@@ -45,9 +43,7 @@ export class WebcodeServer {
45
43
  /** mutable workspace root — switchable at runtime via switchTo() */
46
44
  root;
47
45
  startedAt = Date.now();
48
- authSessions = new Set();
49
- allowedHosts;
50
- allowedOrigins;
46
+ auth;
51
47
  commandAllowlist;
52
48
  server = null;
53
49
  constructor(cfg) {
@@ -55,19 +51,12 @@ export class WebcodeServer {
55
51
  this.root = cfg.root;
56
52
  this.commandAllowlist = new Set(cfg.commandAllowlist ?? DEFAULT_COMMAND_ALLOWLIST);
57
53
  this.sessions = new SessionManager(cfg.root);
58
- const hostNames = ["127.0.0.1", "localhost", "[::1]"];
59
- this.allowedHosts = new Set(hostNames.flatMap((h) => [h, `${h}:${cfg.port}`]));
60
- this.allowedOrigins = new Set([
61
- ...hostNames.map((h) => `http://${h}:${cfg.port}`),
62
- ...(cfg.extraOrigins ?? []),
63
- ]);
64
- // hosted: also accept the public hostname (Fly's edge sets Host to it);
65
- // Host/Origin defense stays on, just widened to the one public origin.
66
- if (cfg.publicHost) {
67
- this.allowedHosts.add(cfg.publicHost.toLowerCase());
68
- this.allowedOrigins.add(`https://${cfg.publicHost}`);
69
- this.allowedOrigins.add(`http://${cfg.publicHost}`);
70
- }
54
+ this.auth = new AuthGate({
55
+ port: cfg.port,
56
+ token: cfg.token,
57
+ ...(cfg.extraOrigins !== undefined ? { extraOrigins: cfg.extraOrigins } : {}),
58
+ ...(cfg.publicHost !== undefined ? { publicHost: cfg.publicHost } : {}),
59
+ });
71
60
  this.app = this.buildApp();
72
61
  }
73
62
  /**
@@ -101,60 +90,13 @@ export class WebcodeServer {
101
90
  return { cliAbsent: true };
102
91
  return { ok: false, ...(r.stderr !== undefined ? { stderr: r.stderr } : {}), ...(r.data !== undefined ? { refusal: r.data } : {}) };
103
92
  }
104
- // ── security gates ─────────────────────────────────────────────────
105
- /** Host allowlist defeats DNS rebinding: rebinding changes DNS, not the Host header. */
106
- hostAllowed(host) {
107
- return !!host && this.allowedHosts.has(host.toLowerCase());
108
- }
109
- /** Origin allowlist defeats cross-site WS hijacking / CSRF from arbitrary websites. */
110
- originAllowed(origin) {
111
- return origin === undefined || this.allowedOrigins.has(origin);
112
- }
113
- cookieAuthed(cookieHeader) {
114
- if (!cookieHeader)
115
- return false;
116
- const match = /(?:^|;\s*)webcode_auth=([^;]+)/.exec(cookieHeader);
117
- return !!match && this.authSessions.has(match[1]);
118
- }
119
93
  // ── HTTP app ───────────────────────────────────────────────────────
120
94
  buildApp() {
121
95
  const { cfg } = this;
122
96
  const app = new Hono();
123
- app.use("*", async (c, next) => {
124
- if (!this.hostAllowed(c.req.header("host"))) {
125
- return c.text("forbidden host", 403);
126
- }
127
- if (!this.originAllowed(c.req.header("origin"))) {
128
- return c.text("forbidden origin", 403);
129
- }
130
- // Token exchange: any page request carrying ?token= gets a cookie.
131
- const token = c.req.query("token");
132
- if (token && !c.req.path.startsWith("/api/")) {
133
- if (!timingSafeEqualStr(token, cfg.token))
134
- return c.text("invalid token", 403);
135
- const secret = crypto.randomBytes(24).toString("base64url");
136
- this.authSessions.add(secret);
137
- setCookie(c, AUTH_COOKIE, secret, {
138
- httpOnly: true,
139
- sameSite: "Strict",
140
- path: "/",
141
- maxAge: COOKIE_MAX_AGE,
142
- });
143
- const url = new URL(c.req.url);
144
- url.searchParams.delete("token");
145
- // /auth?token=… exists so the Vite dev server can proxy the
146
- // exchange; land on the app root afterwards either way.
147
- const dest = url.pathname === "/auth" ? "/" : url.pathname + url.search;
148
- return c.redirect(dest);
149
- }
150
- await next();
151
- });
152
- app.use("/api/*", async (c, next) => {
153
- if (!this.authSessions.has(getCookie(c, AUTH_COOKIE) ?? "")) {
154
- return c.json({ error: "unauthorized" }, 401);
155
- }
156
- await next();
157
- });
97
+ // security gates live in auth.ts: Host/Origin allowlists + token→cookie
98
+ app.use("*", this.auth.gate());
99
+ app.use("/api/*", this.auth.requireAuth());
158
100
  app.get("/api/workspace", (c) => {
159
101
  const body = {
160
102
  root: this.root,
@@ -434,11 +376,11 @@ export class WebcodeServer {
434
376
  socket.write(`HTTP/1.1 ${status} ${msg}\r\nConnection: close\r\n\r\n`);
435
377
  socket.destroy();
436
378
  };
437
- if (!this.hostAllowed(req.headers.host))
379
+ if (!this.auth.hostAllowed(req.headers.host))
438
380
  return reject(403, "Forbidden");
439
- if (!this.originAllowed(req.headers.origin))
381
+ if (!this.auth.originAllowed(req.headers.origin))
440
382
  return reject(403, "Forbidden");
441
- if (!this.cookieAuthed(req.headers.cookie))
383
+ if (!this.auth.cookieAuthed(req.headers.cookie))
442
384
  return reject(401, "Unauthorized");
443
385
  const url = new URL(req.url ?? "/", "http://localhost");
444
386
  const termMatch = /^\/ws\/term\/([0-9a-f]+)$/.exec(url.pathname);
@@ -462,8 +404,3 @@ export class WebcodeServer {
462
404
  this.server?.close();
463
405
  }
464
406
  }
465
- function timingSafeEqualStr(a, b) {
466
- const ba = Buffer.from(a);
467
- const bb = Buffer.from(b);
468
- return ba.length === bb.length && crypto.timingSafeEqual(ba, bb);
469
- }
@@ -0,0 +1,124 @@
1
+ // The zuzuu CLI spawn layer — the ONLY place the daemon shells out to the
2
+ // `zuzuu` binary. Two flavours:
3
+ // runZuzuu — reads: any failure (absent, non-zero, unparseable) → null;
4
+ // callers degrade to file-read fallbacks.
5
+ // runZuzuuMut — mutations + CLI-only reads: failures are distinguished
6
+ // (binary absent vs command failed + stderr tail) so routes
7
+ // can answer 503 vs 502.
8
+ // Always argv arrays (never a shell), time-boxed, cwd-scoped to the workspace.
9
+ import { spawn, spawnSync } from "node:child_process";
10
+ /** Spawn `zuzuu <args> --json` in `root`. Returns parsed JSON, or null on any
11
+ * failure (binary absent, non-zero exit, unparseable). Read-only + time-boxed. */
12
+ export function runZuzuu(root, args, opts = {}) {
13
+ const binary = opts.binary ?? "zuzuu";
14
+ const timeoutMs = opts.timeoutMs ?? 5000;
15
+ return new Promise((resolve) => {
16
+ let out = "";
17
+ let done = false;
18
+ const finish = (v) => { if (!done) {
19
+ done = true;
20
+ resolve(v);
21
+ } };
22
+ let child;
23
+ try {
24
+ child = spawn(binary, [...args, "--json"], { cwd: root, stdio: ["ignore", "pipe", "ignore"] });
25
+ }
26
+ catch {
27
+ finish(null);
28
+ return;
29
+ }
30
+ const timer = setTimeout(() => { try {
31
+ child.kill();
32
+ }
33
+ catch { /* noop */ } finish(null); }, timeoutMs);
34
+ child.stdout?.on("data", (b) => { out += b.toString(); });
35
+ child.on("error", () => { clearTimeout(timer); finish(null); });
36
+ child.on("close", (code) => {
37
+ clearTimeout(timer);
38
+ if (code !== 0)
39
+ return finish(null);
40
+ try {
41
+ finish(JSON.parse(out));
42
+ }
43
+ catch {
44
+ finish(null);
45
+ }
46
+ });
47
+ });
48
+ }
49
+ const STDERR_TAIL = 2048;
50
+ /** Spawn `zuzuu <args> --json` where the caller needs to distinguish failures
51
+ * (mutations, and reads with no file fallback). Unlike runZuzuu: binary
52
+ * absent vs command failed (with a stderr tail) are separate results, so
53
+ * routes can answer 503 vs 502. Stdout must parse as JSON on success. */
54
+ export function runZuzuuMut(root, args, opts = {}) {
55
+ const binary = opts.binary ?? "zuzuu";
56
+ const timeoutMs = opts.timeoutMs ?? 10_000;
57
+ return new Promise((resolve) => {
58
+ let out = "";
59
+ let err = "";
60
+ let done = false;
61
+ const finish = (v) => { if (!done) {
62
+ done = true;
63
+ resolve(v);
64
+ } };
65
+ let child;
66
+ try {
67
+ child = spawn(binary, [...args, "--json"], { cwd: root, stdio: ["ignore", "pipe", "pipe"] });
68
+ }
69
+ catch {
70
+ finish({ ok: false, code: "absent" });
71
+ return;
72
+ }
73
+ const timer = setTimeout(() => {
74
+ try {
75
+ child.kill();
76
+ }
77
+ catch { /* noop */ }
78
+ finish({ ok: false, code: "failed", stderr: "zuzuu timed out" });
79
+ }, timeoutMs);
80
+ child.stdout?.on("data", (b) => { out += b.toString(); });
81
+ child.stderr?.on("data", (b) => {
82
+ err += b.toString();
83
+ if (err.length > STDERR_TAIL)
84
+ err = err.slice(-STDERR_TAIL);
85
+ });
86
+ child.on("error", (e) => {
87
+ clearTimeout(timer);
88
+ if (e.code === "ENOENT")
89
+ finish({ ok: false, code: "absent" });
90
+ else
91
+ finish({ ok: false, code: "failed", stderr: e.message });
92
+ });
93
+ child.on("close", (code) => {
94
+ clearTimeout(timer);
95
+ if (code !== 0) {
96
+ // zuzuu prints structured JSON even on refusals (exit 1, e.g.
97
+ // empty-squash-with-checkpoints) — keep it so the UI can act on reason.
98
+ try {
99
+ const parsed = JSON.parse(out);
100
+ return finish({ ok: false, code: "failed", stderr: err.slice(-STDERR_TAIL), data: parsed });
101
+ }
102
+ catch {
103
+ return finish({ ok: false, code: "failed", stderr: err.slice(-STDERR_TAIL) });
104
+ }
105
+ }
106
+ try {
107
+ finish({ ok: true, data: JSON.parse(out) });
108
+ }
109
+ catch {
110
+ finish({ ok: false, code: "failed", stderr: "unparseable JSON from zuzuu" });
111
+ }
112
+ });
113
+ });
114
+ }
115
+ /** Best-effort: is the zuzuu binary runnable? */
116
+ export function binAvailable(binary) {
117
+ try {
118
+ const r = spawnSync(binary, ["version"], { stdio: "ignore", timeout: 3000 });
119
+ return !r.error && r.status === 0;
120
+ }
121
+ catch {
122
+ return false;
123
+ }
124
+ }