@netnodeag/kraftwerk 0.38.1 → 0.39.1

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 (259) hide show
  1. package/dist/cli/init.js +1 -0
  2. package/dist/cli/kraftwerk.js +2 -0
  3. package/dist/cli/vibeables.d.ts +2 -0
  4. package/dist/cli/vibeables.js +89 -0
  5. package/dist/config.d.ts +20 -0
  6. package/dist/config.js +29 -10
  7. package/dist/inspector/chat/acp.js +6 -0
  8. package/dist/inspector/chat/sessions.d.ts +25 -0
  9. package/dist/inspector/chat/sessions.js +121 -11
  10. package/dist/inspector/chat/types.d.ts +2 -0
  11. package/dist/inspector/git.js +5 -3
  12. package/dist/inspector/mtime.d.ts +3 -0
  13. package/dist/inspector/mtime.js +31 -0
  14. package/dist/inspector/repos.d.ts +2 -0
  15. package/dist/inspector/repos.js +5 -1
  16. package/dist/inspector/server.js +96 -1
  17. package/dist/inspector/settings.d.ts +3 -1
  18. package/dist/inspector/settings.js +26 -8
  19. package/dist/inspector/vibeables.d.ts +153 -0
  20. package/dist/inspector/vibeables.js +628 -0
  21. package/inspector/dist/assets/apl-gfMMiTNf.js +1 -0
  22. package/inspector/dist/assets/asciiarmor-C_eisQCp.js +1 -0
  23. package/inspector/dist/assets/asn1-DCl_8MCW.js +1 -0
  24. package/inspector/dist/assets/asterisk-BEn0Cwpx.js +1 -0
  25. package/inspector/dist/assets/brainfuck-D5EjA2JK.js +1 -0
  26. package/inspector/dist/assets/clike-D_6L3tnO.js +1 -0
  27. package/inspector/dist/assets/clojure-BflJGmDX.js +1 -0
  28. package/inspector/dist/assets/cmake-84nbDH0I.js +1 -0
  29. package/inspector/dist/assets/cobol-DHLQUbN7.js +1 -0
  30. package/inspector/dist/assets/coffeescript-De_zI4J0.js +1 -0
  31. package/inspector/dist/assets/commonlisp-DgAKBe0r.js +1 -0
  32. package/inspector/dist/assets/crystal-Bsdphhtm.js +1 -0
  33. package/inspector/dist/assets/css-CnaYoQib.js +1 -0
  34. package/inspector/dist/assets/cypher-p9eYesGn.js +1 -0
  35. package/inspector/dist/assets/d-DWkQyNnU.js +1 -0
  36. package/inspector/dist/assets/diff-ChtP43wD.js +1 -0
  37. package/inspector/dist/assets/dist-5wk4daHl.js +1 -0
  38. package/inspector/dist/assets/dist-BAHymHVh.js +1 -0
  39. package/inspector/dist/assets/dist-BUzvZfC2.js +23 -0
  40. package/inspector/dist/assets/dist-CF7An11X.js +1 -0
  41. package/inspector/dist/assets/dist-CMK0Dv1Q.js +1 -0
  42. package/inspector/dist/assets/dist-CPlSMtdY.js +1 -0
  43. package/inspector/dist/assets/dist-CWxCXHGC.js +1 -0
  44. package/inspector/dist/assets/dist-CXS6W3P9.js +9 -0
  45. package/inspector/dist/assets/dist-Ceimhh7q.js +1 -0
  46. package/inspector/dist/assets/dist-Cn3OeZ9x.js +1 -0
  47. package/inspector/dist/assets/dist-D01HLRsD.js +1 -0
  48. package/inspector/dist/assets/dist-D0WPeXAf.js +1 -0
  49. package/inspector/dist/assets/dist-D8nmL5S9.js +1 -0
  50. package/inspector/dist/assets/dist-DNBDudvB.js +1 -0
  51. package/inspector/dist/assets/dist-DQujkoko.js +1 -0
  52. package/inspector/dist/assets/dist-DYqdkWRb.js +1 -0
  53. package/inspector/dist/assets/dist-DaG9QYNU.js +1 -0
  54. package/inspector/dist/assets/dist-DfmPNdMI.js +1 -0
  55. package/inspector/dist/assets/dist-FnKl1oDp.js +1 -0
  56. package/inspector/dist/assets/dist-H3rerbMg.js +13 -0
  57. package/inspector/dist/assets/dist-ZMc9InB9.js +1 -0
  58. package/inspector/dist/assets/dist-rE2oRtJp.js +11 -0
  59. package/inspector/dist/assets/dist-yFKhRrHN.js +2 -0
  60. package/inspector/dist/assets/dockerfile-DJkHQgkl.js +1 -0
  61. package/inspector/dist/assets/dtd-DTF72YNO.js +1 -0
  62. package/inspector/dist/assets/dylan-BCDoL_-p.js +1 -0
  63. package/inspector/dist/assets/ebnf-D4c0_ac3.js +1 -0
  64. package/inspector/dist/assets/ecl-BVIeWmwY.js +1 -0
  65. package/inspector/dist/assets/editor-B1Jq9YIq.css +1 -0
  66. package/inspector/dist/assets/editor-CVAfyi-9.js +120 -0
  67. package/inspector/dist/assets/eiffel-BEjRio4Q.js +1 -0
  68. package/inspector/dist/assets/elm-Cshzl8qu.js +1 -0
  69. package/inspector/dist/assets/erlang-BV0c4Hdn.js +1 -0
  70. package/inspector/dist/assets/factor-LKH5gN8L.js +1 -0
  71. package/inspector/dist/assets/fcl-ClOhbFR7.js +1 -0
  72. package/inspector/dist/assets/forth-D6RzAl64.js +1 -0
  73. package/inspector/dist/assets/fortran-Bt6PBEDR.js +1 -0
  74. package/inspector/dist/assets/gas-BHEdbvp9.js +1 -0
  75. package/inspector/dist/assets/gherkin-oBAE_ms0.js +1 -0
  76. package/inspector/dist/assets/groovy-CUpZ7dEl.js +1 -0
  77. package/inspector/dist/assets/haskell-BRsoo5mP.js +1 -0
  78. package/inspector/dist/assets/haxe-DjVOVvNP.js +1 -0
  79. package/inspector/dist/assets/http-CLVgA2GD.js +1 -0
  80. package/inspector/dist/assets/idl-BUZw3wgd.js +1 -0
  81. package/inspector/dist/assets/index-Ba-uGp4N.js +218 -0
  82. package/inspector/dist/assets/index-CL7d8WY1.css +1 -0
  83. package/inspector/dist/assets/javascript-Bu8mWwtz.js +1 -0
  84. package/inspector/dist/assets/julia-CQfgDRfP.js +1 -0
  85. package/inspector/dist/assets/livescript-BXxG0Yva.js +1 -0
  86. package/inspector/dist/assets/lua-DPCEkfD_.js +1 -0
  87. package/inspector/dist/assets/material-symbols-rounded-DzDTtRam.woff2 +0 -0
  88. package/inspector/dist/assets/mathematica-u8YMmbU0.js +1 -0
  89. package/inspector/dist/assets/mbox-BEMVcqjs.js +1 -0
  90. package/inspector/dist/assets/{mirc-CjQqDB4T.js → mirc-C9zpzAZU.js} +1 -1
  91. package/inspector/dist/assets/mllike-B45xgp2S.js +1 -0
  92. package/inspector/dist/assets/modelica-CqHI_q-D.js +1 -0
  93. package/inspector/dist/assets/mscgen-qgSLujhx.js +1 -0
  94. package/inspector/dist/assets/mumps-BSzj6Xyz.js +1 -0
  95. package/inspector/dist/assets/{nginx-DdIZxoE0.js → nginx-8H7HVH4w.js} +1 -1
  96. package/inspector/dist/assets/nsis-viPvXFiq.js +1 -0
  97. package/inspector/dist/assets/ntriples-B9h1VB_g.js +1 -0
  98. package/inspector/dist/assets/octave-D2Q8cUp1.js +1 -0
  99. package/inspector/dist/assets/oz-BwTct_5T.js +1 -0
  100. package/inspector/dist/assets/pascal-Dp_KdFgX.js +1 -0
  101. package/inspector/dist/assets/perl-BkSkiI9m.js +1 -0
  102. package/inspector/dist/assets/pig-AF4Hwhju.js +1 -0
  103. package/inspector/dist/assets/powershell-BcIto6Sf.js +1 -0
  104. package/inspector/dist/assets/properties-C357ku5U.js +1 -0
  105. package/inspector/dist/assets/protobuf-D1ij_kNL.js +1 -0
  106. package/inspector/dist/assets/pug-3K-JMW4g.js +1 -0
  107. package/inspector/dist/assets/puppet-BZyAS7I4.js +1 -0
  108. package/inspector/dist/assets/python-B_1Jd8HM.js +1 -0
  109. package/inspector/dist/assets/q-B9NhS7L_.js +1 -0
  110. package/inspector/dist/assets/r-CxDmU1I4.js +1 -0
  111. package/inspector/dist/assets/rpm-gpDK5sbt.js +1 -0
  112. package/inspector/dist/assets/ruby-ChfNwlCY.js +1 -0
  113. package/inspector/dist/assets/{sas-B4kiWyti.js → sas-C2OyD2Y6.js} +1 -1
  114. package/inspector/dist/assets/scheme-D9qYN0V5.js +1 -0
  115. package/inspector/dist/assets/shell-DwuoZtxw.js +1 -0
  116. package/inspector/dist/assets/sieve-tLWDfEkZ.js +1 -0
  117. package/inspector/dist/assets/simple-mode-DXiLM1-G.js +1 -0
  118. package/inspector/dist/assets/smalltalk-BLp5-jwC.js +1 -0
  119. package/inspector/dist/assets/solr-B78wSQJQ.js +1 -0
  120. package/inspector/dist/assets/sparql-Dz-mlCAC.js +1 -0
  121. package/inspector/dist/assets/spreadsheet-CTIncYxj.js +1 -0
  122. package/inspector/dist/assets/sql-DuQMFDvy.js +1 -0
  123. package/inspector/dist/assets/stex-DNzd62Q_.js +1 -0
  124. package/inspector/dist/assets/stylus-BOVgKnng.js +1 -0
  125. package/inspector/dist/assets/swift-DKB6_1j6.js +1 -0
  126. package/inspector/dist/assets/tcl-BGfruO1u.js +1 -0
  127. package/inspector/dist/assets/textile-C4jVoHG1.js +1 -0
  128. package/inspector/dist/assets/tiddlywiki-VlUiK86J.js +1 -0
  129. package/inspector/dist/assets/tiki-AK6FwT8q.js +1 -0
  130. package/inspector/dist/assets/toml-BPTmHmyx.js +1 -0
  131. package/inspector/dist/assets/troff-C2dAgh7P.js +1 -0
  132. package/inspector/dist/assets/ttcn-PPSTk1ow.js +1 -0
  133. package/inspector/dist/assets/ttcn-cfg-2b4Gb5sE.js +1 -0
  134. package/inspector/dist/assets/turtle-Cx3rYX2g.js +1 -0
  135. package/inspector/dist/assets/vb-BthdM_4N.js +1 -0
  136. package/inspector/dist/assets/vbscript-D9f6rSsU.js +1 -0
  137. package/inspector/dist/assets/velocity-CqftRsjg.js +1 -0
  138. package/inspector/dist/assets/verilog-CjI9Y6RI.js +1 -0
  139. package/inspector/dist/assets/vhdl-kS471soi.js +1 -0
  140. package/inspector/dist/assets/webidl-D5jiJvOU.js +1 -0
  141. package/inspector/dist/assets/xquery-BKJx4mf9.js +1 -0
  142. package/inspector/dist/assets/yacas-CXXgtw0p.js +1 -0
  143. package/inspector/dist/assets/z80-Co3PbHdE.js +1 -0
  144. package/inspector/dist/index.html +2 -2
  145. package/package.json +6 -6
  146. package/inspector/dist/assets/apl-B4CMkyY2.js +0 -1
  147. package/inspector/dist/assets/asciiarmor-Df11BRmG.js +0 -1
  148. package/inspector/dist/assets/asn1-EdZsLKOL.js +0 -1
  149. package/inspector/dist/assets/asterisk-B-8jnY81.js +0 -1
  150. package/inspector/dist/assets/brainfuck-C4LP7Hcl.js +0 -1
  151. package/inspector/dist/assets/clike-B9uivgTg.js +0 -1
  152. package/inspector/dist/assets/clojure-BMjYHr_A.js +0 -1
  153. package/inspector/dist/assets/cmake-BQqOBYOt.js +0 -1
  154. package/inspector/dist/assets/cobol-CWcv1MsR.js +0 -1
  155. package/inspector/dist/assets/coffeescript-S37ZYGWr.js +0 -1
  156. package/inspector/dist/assets/commonlisp-DBKNyK5s.js +0 -1
  157. package/inspector/dist/assets/crystal-SjHAIU92.js +0 -1
  158. package/inspector/dist/assets/css-BnMrqG3P.js +0 -1
  159. package/inspector/dist/assets/cypher-C_CwsFkJ.js +0 -1
  160. package/inspector/dist/assets/d-pRatUO7H.js +0 -1
  161. package/inspector/dist/assets/diff-DbItnlRl.js +0 -1
  162. package/inspector/dist/assets/dockerfile-BKs6k2Af.js +0 -1
  163. package/inspector/dist/assets/dtd-DF_7sFjM.js +0 -1
  164. package/inspector/dist/assets/dylan-DwRh75JA.js +0 -1
  165. package/inspector/dist/assets/ebnf-CDyGwa7X.js +0 -1
  166. package/inspector/dist/assets/ecl-Cabwm37j.js +0 -1
  167. package/inspector/dist/assets/editor-9rr2cQaW.css +0 -1
  168. package/inspector/dist/assets/editor-CONb__vl.js +0 -144
  169. package/inspector/dist/assets/eiffel-CnydiIhH.js +0 -1
  170. package/inspector/dist/assets/elm-vLlmbW-K.js +0 -1
  171. package/inspector/dist/assets/erlang-BNw1qcRV.js +0 -1
  172. package/inspector/dist/assets/factor-kuTfRLto.js +0 -1
  173. package/inspector/dist/assets/fcl-Kvtd6kyn.js +0 -1
  174. package/inspector/dist/assets/forth-Ffai-XNe.js +0 -1
  175. package/inspector/dist/assets/fortran-DYz_wnZ1.js +0 -1
  176. package/inspector/dist/assets/gas-Bneqetm1.js +0 -1
  177. package/inspector/dist/assets/gherkin-heZmZLOM.js +0 -1
  178. package/inspector/dist/assets/groovy-D9Dt4D0W.js +0 -1
  179. package/inspector/dist/assets/haskell-Cw1EW3IL.js +0 -1
  180. package/inspector/dist/assets/haxe-H-WmDvRZ.js +0 -1
  181. package/inspector/dist/assets/http-DBlCnlav.js +0 -1
  182. package/inspector/dist/assets/idl-BEugSyMb.js +0 -1
  183. package/inspector/dist/assets/index-6VtYKf_K.js +0 -1
  184. package/inspector/dist/assets/index-B_BJEGG5.js +0 -1
  185. package/inspector/dist/assets/index-BkznuDSS.js +0 -1
  186. package/inspector/dist/assets/index-BrGHSw5I.js +0 -1
  187. package/inspector/dist/assets/index-BuiRW-03.js +0 -1
  188. package/inspector/dist/assets/index-C1tY1uK0.js +0 -1
  189. package/inspector/dist/assets/index-CDY-uFno.js +0 -7
  190. package/inspector/dist/assets/index-CcGsEtey.js +0 -1
  191. package/inspector/dist/assets/index-CntcQxRr.js +0 -1
  192. package/inspector/dist/assets/index-CpDzWqX0.js +0 -3
  193. package/inspector/dist/assets/index-Cw6eNCH7.js +0 -1
  194. package/inspector/dist/assets/index-D1_Glu0y.css +0 -1
  195. package/inspector/dist/assets/index-DMWUqEQi.js +0 -217
  196. package/inspector/dist/assets/index-DfK1f9gz.js +0 -1
  197. package/inspector/dist/assets/index-IosqBxL0.js +0 -1
  198. package/inspector/dist/assets/index-iRFYcgNN.js +0 -1
  199. package/inspector/dist/assets/index-p5w5E19N.js +0 -1
  200. package/inspector/dist/assets/index-rAZqRLhj.js +0 -2
  201. package/inspector/dist/assets/index-yGDq5Egm.js +0 -1
  202. package/inspector/dist/assets/javascript-iXu5QeM3.js +0 -1
  203. package/inspector/dist/assets/julia-DuME0IfC.js +0 -1
  204. package/inspector/dist/assets/livescript-BwQOo05w.js +0 -1
  205. package/inspector/dist/assets/lua-VAEuO923.js +0 -1
  206. package/inspector/dist/assets/material-symbols-rounded-DxXFL64Y.woff2 +0 -0
  207. package/inspector/dist/assets/mathematica-DTrFuWx2.js +0 -1
  208. package/inspector/dist/assets/mbox-CNhZ1qSd.js +0 -1
  209. package/inspector/dist/assets/mllike-CXdrOF99.js +0 -1
  210. package/inspector/dist/assets/modelica-Dc1JOy9r.js +0 -1
  211. package/inspector/dist/assets/mscgen-BA5vi2Kp.js +0 -1
  212. package/inspector/dist/assets/mumps-BT43cFF4.js +0 -1
  213. package/inspector/dist/assets/nsis-LY0VT_u5.js +0 -1
  214. package/inspector/dist/assets/ntriples-BfvgReVJ.js +0 -1
  215. package/inspector/dist/assets/octave-Ck1zUtKM.js +0 -1
  216. package/inspector/dist/assets/oz-BzwKVEFT.js +0 -1
  217. package/inspector/dist/assets/pascal--L3eBynH.js +0 -1
  218. package/inspector/dist/assets/perl-CdXCOZ3F.js +0 -1
  219. package/inspector/dist/assets/pig-CevX1Tat.js +0 -1
  220. package/inspector/dist/assets/powershell-CFHJl5sT.js +0 -1
  221. package/inspector/dist/assets/properties-C78fOPTZ.js +0 -1
  222. package/inspector/dist/assets/protobuf-ChK-085T.js +0 -1
  223. package/inspector/dist/assets/pug-DeIclll2.js +0 -1
  224. package/inspector/dist/assets/puppet-DMA9R1ak.js +0 -1
  225. package/inspector/dist/assets/python-BuPzkPfP.js +0 -1
  226. package/inspector/dist/assets/q-pXgVlZs6.js +0 -1
  227. package/inspector/dist/assets/r-B6wPVr8A.js +0 -1
  228. package/inspector/dist/assets/rpm-CTu-6PCP.js +0 -1
  229. package/inspector/dist/assets/ruby-BK5aLJev.js +0 -1
  230. package/inspector/dist/assets/scheme-C41bIUwD.js +0 -1
  231. package/inspector/dist/assets/shell-CjFT_Tl9.js +0 -1
  232. package/inspector/dist/assets/sieve-C3Gn_uJK.js +0 -1
  233. package/inspector/dist/assets/simple-mode-GW_nhZxv.js +0 -1
  234. package/inspector/dist/assets/smalltalk-CnHTOXQT.js +0 -1
  235. package/inspector/dist/assets/solr-DehyRSwq.js +0 -1
  236. package/inspector/dist/assets/sparql-DkYu6x3z.js +0 -1
  237. package/inspector/dist/assets/spreadsheet-BCZA_wO0.js +0 -1
  238. package/inspector/dist/assets/sql-D0XecflT.js +0 -1
  239. package/inspector/dist/assets/stex-C3f8Ysf7.js +0 -1
  240. package/inspector/dist/assets/stylus-B533Al4x.js +0 -1
  241. package/inspector/dist/assets/swift-BzpIVaGY.js +0 -1
  242. package/inspector/dist/assets/tcl-DVfN8rqt.js +0 -1
  243. package/inspector/dist/assets/textile-CnDTJFAw.js +0 -1
  244. package/inspector/dist/assets/tiddlywiki-DO-Gjzrf.js +0 -1
  245. package/inspector/dist/assets/tiki-DGYXhP31.js +0 -1
  246. package/inspector/dist/assets/toml-Bm5Em-hy.js +0 -1
  247. package/inspector/dist/assets/troff-wAsdV37c.js +0 -1
  248. package/inspector/dist/assets/ttcn-CfJYG6tj.js +0 -1
  249. package/inspector/dist/assets/ttcn-cfg-B9xdYoR4.js +0 -1
  250. package/inspector/dist/assets/turtle-B1tBg_DP.js +0 -1
  251. package/inspector/dist/assets/vb-CmGdzxic.js +0 -1
  252. package/inspector/dist/assets/vbscript-BuJXcnF6.js +0 -1
  253. package/inspector/dist/assets/velocity-D8B20fx6.js +0 -1
  254. package/inspector/dist/assets/verilog-C6RDOZhf.js +0 -1
  255. package/inspector/dist/assets/vhdl-lSbBsy5d.js +0 -1
  256. package/inspector/dist/assets/webidl-ZXfAyPTL.js +0 -1
  257. package/inspector/dist/assets/xquery-DzFWVndE.js +0 -1
  258. package/inspector/dist/assets/yacas-BJ4BC0dw.js +0 -1
  259. package/inspector/dist/assets/z80-Hz9HOZM7.js +0 -1
package/dist/cli/init.js CHANGED
@@ -24,6 +24,7 @@ agents: ${DATA_DIR}/agents # agent definitions
24
24
  skills: ${DATA_DIR}/skills # workspace skills (shared instruction packages)
25
25
  # repos: # git repositories the agents work on (uncomment both lines to enable)
26
26
  # root: ${DATA_DIR}/repos # clones land here (git-ignored); a bare \`repos:\` uses repos/ instead
27
+ # vibeables: # small apps built live in a chat with a preview pane (uncomment to enable; one folder per app under ${DATA_DIR}/vibeables, versioned with the workspace)
27
28
  `;
28
29
  const WORKFLOW_TEMPLATE = `# yaml-language-server: $schema=${SCHEMA_URL}
29
30
  name: hello
@@ -14,6 +14,7 @@ import { runInit } from "./init.js";
14
14
  import { registerKnowledgeCommands } from "./knowledge.js";
15
15
  import { registerProjectCommands } from "./projects.js";
16
16
  import { registerRepoCommands } from "./repos.js";
17
+ import { registerVibeableCommands } from "./vibeables.js";
17
18
  import { registerRoutineCommands } from "./routines.js";
18
19
  import { listRuns, showRun } from "./runs.js";
19
20
  import { runUi } from "./ui.js";
@@ -262,6 +263,7 @@ registerKnowledgeCommands(program);
262
263
  registerRoutineCommands(program);
263
264
  registerProjectCommands(program);
264
265
  registerRepoCommands(program);
266
+ registerVibeableCommands(program);
265
267
  program
266
268
  .command("ui")
267
269
  .description("Start the inspector web UI for this project's runs and workflows")
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerVibeableCommands(program: Command): void;
@@ -0,0 +1,89 @@
1
+ import chalk from "chalk";
2
+ import Table from "cli-table3";
3
+ import { createVibeable, deleteVibeable, listVibeables } from "../inspector/vibeables.js";
4
+ import { fmtAgo } from "./projects.js";
5
+ import { initContext as prepare } from "./routines.js";
6
+ /**
7
+ * `kraftwerk vibeables` — the small apps built live in a chat, one folder
8
+ * each under the vibeables root (`vibeables.root` in kraftwerk.yml). The
9
+ * same module the inspector uses, so an app created here shows up in every
10
+ * chat's picker at once:
11
+ *
12
+ * kraftwerk vibeables list: name, mode, state
13
+ * kraftwerk vibeables create <name> new app from the starter (index.html, vibeable.yml)
14
+ * kraftwerk vibeables remove <name> delete the folder (its history stays in the workspace git)
15
+ */
16
+ const die = (msg) => {
17
+ console.error(chalk.red(msg));
18
+ process.exit(1);
19
+ };
20
+ export function registerVibeableCommands(program) {
21
+ const vibeables = program
22
+ .command("vibeables")
23
+ .description("Small apps built live in a chat: list, create one from the starter, remove");
24
+ vibeables
25
+ .command("list", { isDefault: true })
26
+ .description("List the apps under the vibeables root")
27
+ .option("--json", "Machine-readable output")
28
+ .action(async (opts) => {
29
+ await prepare();
30
+ const view = await listVibeables();
31
+ if (opts.json) {
32
+ console.log(JSON.stringify(view, null, 2));
33
+ return;
34
+ }
35
+ if (!view.enabled)
36
+ die(view.error ?? "vibeables are off");
37
+ if (view.vibeables.length === 0) {
38
+ console.log(chalk.dim(`No vibeables under ${view.root} yet — \`kraftwerk vibeables create <name>\` starts one.`));
39
+ return;
40
+ }
41
+ const table = new Table({
42
+ head: ["name", "mode", "state", "changed"].map((h) => chalk.bold(h)),
43
+ wordWrap: true,
44
+ colWidths: [24, 30, 22, 14],
45
+ });
46
+ for (const v of view.vibeables) {
47
+ table.push([
48
+ chalk.cyan(v.slug),
49
+ v.dev ? `dev: ${v.dev}` : "static",
50
+ v.configError ? chalk.red(v.configError) : v.hasIndex ? chalk.dim("ready") : chalk.yellow("no index.html yet"),
51
+ chalk.dim(fmtAgo(v.updatedAt)),
52
+ ]);
53
+ }
54
+ console.log(table.toString());
55
+ console.log(chalk.dim(view.root ?? ""));
56
+ });
57
+ vibeables
58
+ .command("create")
59
+ .description("Create an app folder from the starter (index.html, style.css, app.js, vibeable.yml)")
60
+ .argument("<name>", "Folder name under the root")
61
+ .option("--json", "Print the new entry as JSON")
62
+ .action(async (name, opts) => {
63
+ await prepare();
64
+ try {
65
+ const v = await createVibeable(name);
66
+ if (opts.json)
67
+ console.log(JSON.stringify(v, null, 2));
68
+ else
69
+ console.log(`${chalk.green("✔")} ${chalk.cyan(v.slug)} → ${v.path} ${chalk.dim("(open it from any chat with the vibeable button)")}`);
70
+ }
71
+ catch (err) {
72
+ die(err.message);
73
+ }
74
+ });
75
+ vibeables
76
+ .command("remove")
77
+ .description("Delete an app folder; its history stays in the workspace git")
78
+ .argument("<name>", "Folder name under the root")
79
+ .action(async (name) => {
80
+ await prepare();
81
+ try {
82
+ await deleteVibeable(name);
83
+ console.log(`${chalk.green("✔")} removed ${chalk.cyan(name)}`);
84
+ }
85
+ catch (err) {
86
+ die(err.message);
87
+ }
88
+ });
89
+ }
package/dist/config.d.ts CHANGED
@@ -28,6 +28,8 @@
28
28
  * autosync: pull # off | pull
29
29
  * repos: # git repositories the agents work on (absent = off, bare key = on)
30
30
  * root: kraftwerk-data/repos # where clones land, relative to the file. Default: repos
31
+ * vibeables: # small apps built live in a chat, rendered in the inspector (absent = off, bare key = on)
32
+ * root: apps # one folder per app, part of the workspace. Default: kraftwerk-data/vibeables
31
33
  */
32
34
  /** Stable, versionless URL of the workflow JSON schema (editor validation). */
33
35
  export declare const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json";
@@ -71,6 +73,22 @@ export declare const REPOS_DEFAULT_ROOT = "repos";
71
73
  * settings save and the module itself cannot disagree.
72
74
  */
73
75
  export declare function reposRootFor(project: Project): string | undefined;
76
+ /**
77
+ * Vibeables: small applications the user builds live with an agent — a chat
78
+ * with a preview pane. Each is one folder under the root, plain files the
79
+ * inspector serves (or a dev command it runs). The root is part of the
80
+ * workspace and synced by the workspace git like agents and knowledge.
81
+ */
82
+ export interface VibeablesConfig {
83
+ /** false keeps the block but turns the feature off. Default: true. */
84
+ enabled?: boolean;
85
+ /** Where the apps live, relative to the project root. Default: kraftwerk-data/vibeables */
86
+ root?: string;
87
+ }
88
+ /** Where vibeables live when `vibeables.root` is not set. */
89
+ export declare const VIBEABLES_DEFAULT_ROOT = "kraftwerk-data/vibeables";
90
+ /** Absolute vibeables root when the feature is on, undefined otherwise. */
91
+ export declare function vibeablesRootFor(project: Project): string | undefined;
74
92
  /** A directory as a .gitignore entry: relative, forward slashes, no trailing slash; undefined outside the root. */
75
93
  export declare function ignoreEntryFor(projectRoot: string, dir: string): string | undefined;
76
94
  /** True when .gitignore text already covers the entry (with or without a leading or trailing slash). */
@@ -109,6 +127,8 @@ export interface ProjectConfig {
109
127
  git?: GitConfig;
110
128
  /** Repositories the agents work on. Absent = off. */
111
129
  repos?: ReposConfig;
130
+ /** Vibeables: apps built live in a chat. Absent = off. */
131
+ vibeables?: VibeablesConfig;
112
132
  }
113
133
  export interface Project {
114
134
  /** Absolute project root the CLI operates on. */
package/dist/config.js CHANGED
@@ -32,6 +32,8 @@ import { parse } from "yaml";
32
32
  * autosync: pull # off | pull
33
33
  * repos: # git repositories the agents work on (absent = off, bare key = on)
34
34
  * root: kraftwerk-data/repos # where clones land, relative to the file. Default: repos
35
+ * vibeables: # small apps built live in a chat, rendered in the inspector (absent = off, bare key = on)
36
+ * root: apps # one folder per app, part of the workspace. Default: kraftwerk-data/vibeables
35
37
  */
36
38
  /** Stable, versionless URL of the workflow JSON schema (editor validation). */
37
39
  export const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json";
@@ -52,6 +54,15 @@ export function reposRootFor(project) {
52
54
  return undefined;
53
55
  return path.resolve(project.root, r.root ?? REPOS_DEFAULT_ROOT);
54
56
  }
57
+ /** Where vibeables live when `vibeables.root` is not set. */
58
+ export const VIBEABLES_DEFAULT_ROOT = "kraftwerk-data/vibeables";
59
+ /** Absolute vibeables root when the feature is on, undefined otherwise. */
60
+ export function vibeablesRootFor(project) {
61
+ const v = project.config.vibeables;
62
+ if (!v || v.enabled === false)
63
+ return undefined;
64
+ return path.resolve(project.root, v.root ?? VIBEABLES_DEFAULT_ROOT);
65
+ }
55
66
  /** A directory as a .gitignore entry: relative, forward slashes, no trailing slash; undefined outside the root. */
56
67
  export function ignoreEntryFor(projectRoot, dir) {
57
68
  const rel = path.relative(projectRoot, path.resolve(projectRoot, dir)).split(path.sep).join("/");
@@ -127,7 +138,7 @@ export async function resolveProject(cwd) {
127
138
  const root = gitFallback ?? start;
128
139
  return { root, config: {}, outputDir: path.join(root, "output") };
129
140
  }
130
- const KNOWN_KEYS = ["name", "icon", "color", "port", "workflows", "output", "knowledge", "agents", "skills", "switcher", "git", "repos"];
141
+ const KNOWN_KEYS = ["name", "icon", "color", "port", "workflows", "output", "knowledge", "agents", "skills", "switcher", "git", "repos", "vibeables"];
131
142
  async function loadConfig(configPath) {
132
143
  let raw;
133
144
  try {
@@ -171,7 +182,12 @@ async function loadConfig(configPath) {
171
182
  else if (key === "repos") {
172
183
  if (config[key] === null)
173
184
  config[key] = {};
174
- validateRepos(configPath, config[key]);
185
+ validateRootBlock(configPath, "repos", config[key]);
186
+ }
187
+ else if (key === "vibeables") {
188
+ if (config[key] === null)
189
+ config[key] = {};
190
+ validateRootBlock(configPath, "vibeables", config[key]);
175
191
  }
176
192
  else if (typeof config[key] !== "string") {
177
193
  throw new Error(`${path.basename(configPath)}: ${key} must be a string`);
@@ -232,26 +248,29 @@ function validateGit(configPath, value) {
232
248
  throw new Error(`${file}: git.autosync must be "off" or "pull"`);
233
249
  }
234
250
  }
235
- function validateRepos(configPath, value) {
251
+ /**
252
+ * `repos` and `vibeables` share one shape: { enabled?, root? }. The root
253
+ * must never be the project itself or anything outside it — it is what
254
+ * remove/delete operate under, and what the workspace git may stage.
255
+ */
256
+ function validateRootBlock(configPath, block, value) {
236
257
  const file = path.basename(configPath);
237
258
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
238
- throw new Error(`${file}: repos must be a mapping (enabled, root)`);
259
+ throw new Error(`${file}: ${block} must be a mapping (enabled, root)`);
239
260
  }
240
261
  const r = value;
241
262
  for (const key of Object.keys(r)) {
242
263
  if (!["enabled", "root"].includes(key)) {
243
- throw new Error(`${file}: repos.${key} is unknown (allowed: enabled, root)`);
264
+ throw new Error(`${file}: ${block}.${key} is unknown (allowed: enabled, root)`);
244
265
  }
245
266
  }
246
267
  if (r.enabled !== undefined && typeof r.enabled !== "boolean") {
247
- throw new Error(`${file}: repos.enabled must be true or false`);
268
+ throw new Error(`${file}: ${block}.enabled must be true or false`);
248
269
  }
249
270
  if (r.root !== undefined && (typeof r.root !== "string" || !r.root.trim())) {
250
- throw new Error(`${file}: repos.root must be a non-empty string`);
271
+ throw new Error(`${file}: ${block}.root must be a non-empty string`);
251
272
  }
252
- // The root is what `repos remove --force` deletes under, so it must never
253
- // be the project itself or anything outside it.
254
273
  if (typeof r.root === "string" && !ignoreEntryFor(path.dirname(configPath), r.root.trim())) {
255
- throw new Error(`${file}: repos.root must be a directory inside the project (not ".", ".." or an absolute path outside it)`);
274
+ throw new Error(`${file}: ${block}.root must be a directory inside the project (not ".", ".." or an absolute path outside it)`);
256
275
  }
257
276
  }
@@ -59,6 +59,12 @@ export async function startAcpBackend(agent, cwd, hooks, tuning = {}) {
59
59
  stderr = stderr.slice(-20_000);
60
60
  });
61
61
  let dead = false;
62
+ // Without a listener a failed spawn (cwd gone, node missing) is an
63
+ // unhandled 'error' event that takes the whole inspector down.
64
+ child.on("error", (err) => {
65
+ dead = true;
66
+ hooks.emit({ type: "error", message: `could not start the ${agent} agent: ${err.message}` });
67
+ });
62
68
  child.on("close", (code) => {
63
69
  dead = true;
64
70
  if (code !== 0 && code !== null) {
@@ -1,6 +1,20 @@
1
1
  import type { ChatAgentId, ChatMeta, ChatScope, StoredChatEvent } from "./types.js";
2
2
  /** Server shutdown: no agent subprocess may outlive the inspector. */
3
3
  export declare function disposeAllBackends(): void;
4
+ /**
5
+ * "## Vibeable" block: the chat has an app folder open in the preview pane.
6
+ * The agent works inside that folder, and everything it saves is what the
7
+ * user sees next — so the block spells out how the preview runs and what
8
+ * the sandbox rules out.
9
+ */
10
+ export declare function vibeableContext(slug: string, cwd: string): Promise<string>;
11
+ /**
12
+ * The folder the agent is actually spawned in. A vibeable that was removed
13
+ * leaves meta.cwd pointing nowhere; spawning there fails, so fall back to
14
+ * the scope's default while keeping meta.vibeable — the context then tells
15
+ * the agent the folder is gone instead of pretending it is there.
16
+ */
17
+ export declare function effectiveCwd(meta: ChatMeta): Promise<string>;
4
18
  export declare function createChat(opts: {
5
19
  agent: ChatAgentId;
6
20
  scope: ChatScope;
@@ -18,6 +32,17 @@ export declare function getChat(id: string): Promise<{
18
32
  export declare function postMessage(id: string, text: string): Promise<{
19
33
  error?: string;
20
34
  }>;
35
+ /**
36
+ * Open a vibeable in the chat (or close it with null). The agent's cwd
37
+ * becomes the app folder, so the running subprocess is dropped and the next
38
+ * message spawns one there with fresh context. Refused while a turn runs:
39
+ * the agent would keep editing in the old folder.
40
+ */
41
+ export declare function setChatVibeable(id: string, slug: string | null): Promise<{
42
+ error?: string;
43
+ status?: number;
44
+ meta?: ChatMeta;
45
+ }>;
21
46
  export declare function resolvePermission(id: string, requestId: string, optionId: string | null): Promise<{
22
47
  error?: string;
23
48
  }>;
@@ -7,6 +7,7 @@ import { listSkills, readSkill } from "../skills.js";
7
7
  import { listWorkflows } from "../workflows.js";
8
8
  import { getAgent, listAgents } from "../agents.js";
9
9
  import { listRepos } from "../repos.js";
10
+ import { resolveVibeable, vibeableStatus, VIBEABLE_CONFIG_FILE } from "../vibeables.js";
10
11
  import { startAcpBackend } from "./acp.js";
11
12
  import { startPiBackend } from "./pi.js";
12
13
  import { appendEvent, listChatMetas, newChatId, readEvents, readMeta, safeChatDir, writeMeta, } from "./store.js";
@@ -168,6 +169,56 @@ async function reposContext() {
168
169
  `\`npx kraftwerk repos\` lists them, \`update <name>\` fetches and fast-forwards a clean clone, \`remove <name>\` ` +
169
170
  `deletes one. A repository the user mentions that is not listed is not cloned yet — offer to add it.`);
170
171
  }
172
+ /**
173
+ * "## Vibeable" block: the chat has an app folder open in the preview pane.
174
+ * The agent works inside that folder, and everything it saves is what the
175
+ * user sees next — so the block spells out how the preview runs and what
176
+ * the sandbox rules out.
177
+ */
178
+ export async function vibeableContext(slug, cwd) {
179
+ let st = null;
180
+ let failure = "";
181
+ try {
182
+ st = await vibeableStatus(slug);
183
+ }
184
+ catch (err) {
185
+ failure = err.message;
186
+ }
187
+ if (!st) {
188
+ if (/are off/.test(failure)) {
189
+ return (`## Vibeable: ${slug}\nThis chat had the vibeable "${slug}" open, but vibeables have since been switched off in this ` +
190
+ `workspace's settings. Its folder still exists at ${cwd} and is your working directory; there is no preview pane any ` +
191
+ `more. Work in the folder if the user asks, and mention that the feature is off.`);
192
+ }
193
+ return `## Vibeable\nThis chat had the vibeable "${slug}" open in its preview pane, but its folder is gone. Tell the user; do not recreate it unasked.`;
194
+ }
195
+ const dev = st.dev;
196
+ const mode = dev?.running
197
+ ? `A dev server is running (\`${dev.command}\`, port ${dev.port}${dev.ready ? "" : ", not answering yet"}); the pane shows it.`
198
+ : st.config.dev
199
+ ? `${VIBEABLE_CONFIG_FILE} declares \`dev: ${st.config.dev}\`; the dev server is not running, so the pane shows the static folder until the user starts it with the play button.`
200
+ : `No dev command: the pane shows the folder${st.config.dir ? ` ${st.config.dir}/` : ""} served as-is.`;
201
+ return (`## Vibeable: ${slug}\n` +
202
+ `You are building a small piece of software live with the user. Your working directory is its folder: ${st.path}. ` +
203
+ `The user sees it rendered in a preview pane next to this chat, and every file you save reloads that preview within a second — ` +
204
+ `work in small visible steps, and describe what changed instead of pasting code. Think ad-hoc software, not a website: a dashboard, ` +
205
+ `a slideshow, a calculator, a tracker with a backend — whatever the user asks for.\n\n` +
206
+ `Current state: ${mode}${st.configError ? ` (${st.configError} — fix it)` : ""}\n\n` +
207
+ `How the preview runs:\n` +
208
+ `- Static mode (default): the inspector serves the folder directly at /vibeables/${slug}/ — index.html is the entry; plain HTML, CSS and ` +
209
+ `JavaScript with no build step. Use relative URLs (./app.js, not /app.js). ES modules and CDN imports (https://esm.sh/<pkg>) work. ` +
210
+ `The preview is a sandboxed document in both modes: no localStorage, no cookies, no calls into the inspector's own /api. Keep state in ` +
211
+ `memory, or move to dev mode and persist through your own server.\n` +
212
+ `- Dev mode: when the app needs a build tool, a backend or a database, write ${VIBEABLE_CONFIG_FILE} with \`dev: <command>\` ` +
213
+ `(e.g. \`npm run dev\`, \`node server.js\`, \`python3 -m http.server $PORT\`). kraftwerk starts it inside the folder with PORT in the ` +
214
+ `environment and the pane reaches it through /vibeables/${slug}/ on the inspector (BASE_PATH holds that prefix — a dev server that ` +
215
+ `emits absolute URLs, like Vite, must use it as its base); the command must listen on $PORT (or declare \`port:\` when it cannot). Optional ` +
216
+ `\`dir:\` picks the folder for static mode (e.g. dist). The app brings its own backend and storage — a small node server writing JSON ` +
217
+ `files, SQLite, an external API — nothing of that is provided by kraftwerk.\n\n` +
218
+ `Rules: the folder is part of this workspace and versioned with it — do not run git yourself, the user reviews and commits from the ` +
219
+ `inspector's Git screen; keep secrets out of the folder (.env is git-ignored, node_modules too); leave ${VIBEABLE_CONFIG_FILE} valid YAML; ` +
220
+ `when you switch the app to dev mode, say so and ask the user to press play in the pane. Reply briefly — the preview shows the result.`);
221
+ }
171
222
  /** Base context per scope; scopeContext() appends the shared skills block. */
172
223
  async function baseScopeContext(scope, agent) {
173
224
  if (scope.kind === "agent") {
@@ -301,19 +352,22 @@ const RENDERING_BLOCK = `## Chat rendering\n` +
301
352
  `with ![alt](/api/runs/<run-id>/file?name=picture.png&raw=1), or link any artifact the same way. ` +
302
353
  `Prefer these relative /api/... URLs over file paths when showing results: they render directly ` +
303
354
  `in this chat and keep working wherever the inspector is reachable.`;
304
- async function scopeContext(scope, agent) {
355
+ async function scopeContext(meta) {
356
+ const { scope, agent } = meta;
305
357
  // The repositories block reads every clone from git, so it runs alongside
306
358
  // the rest instead of adding its spawns to the first prompt's latency.
307
- const [base, repos, skills] = await Promise.all([
359
+ const [base, repos, vibe, skills] = await Promise.all([
308
360
  baseScopeContext(scope, agent),
309
361
  scope.kind === "agent" || scope.kind === "kraftwerk" ? reposContext() : Promise.resolve(""),
362
+ meta.vibeable ? vibeableContext(meta.vibeable, meta.cwd) : Promise.resolve(""),
310
363
  availableSkills(scope),
311
364
  ]);
312
- return [base, repos, RENDERING_BLOCK, skillsBlock(skills)].filter(Boolean).join("\n\n");
365
+ return [base, repos, vibe, RENDERING_BLOCK, skillsBlock(skills)].filter(Boolean).join("\n\n");
313
366
  }
314
367
  /* ---------- backend lifecycle ---------- */
315
368
  /** Agent sessions carry the agent's model/effort; resolved live so edits apply to new backends. */
316
- async function backendTuning(scope, agent) {
369
+ async function backendTuning(meta) {
370
+ const { scope, agent } = meta;
317
371
  const tuning = {};
318
372
  if (scope.kind === "agent") {
319
373
  const def = await getAgent(scope.slug).catch(() => null);
@@ -327,7 +381,8 @@ async function backendTuning(scope, agent) {
327
381
  }
328
382
  // Run chats live in the run folder — grant claude the project root so
329
383
  // project-level skills and files stay reachable.
330
- if (scope.kind === "run" && agent === "claude")
384
+ // A vibeable session works inside the app folder for the same reason.
385
+ if ((scope.kind === "run" || meta.vibeable) && agent === "claude")
331
386
  tuning.addDirs = [getProjectRoot()];
332
387
  // pi loads skills only when told: hand it every visible skill folder.
333
388
  if (agent === "pi") {
@@ -364,8 +419,9 @@ async function ensureBackend(state) {
364
419
  });
365
420
  },
366
421
  };
367
- const { agent, cwd } = state.meta;
368
- const tuning = await backendTuning(state.meta.scope, agent);
422
+ const { agent } = state.meta;
423
+ const cwd = await effectiveCwd(state.meta);
424
+ const tuning = await backendTuning(state.meta);
369
425
  state.backend =
370
426
  agent === "pi"
371
427
  ? startPiBackend(cwd, hooks, tuning)
@@ -376,8 +432,22 @@ async function ensureBackend(state) {
376
432
  return state.backend;
377
433
  }
378
434
  /* ---------- public API (used by server.ts) ---------- */
435
+ /** Where a chat's agent runs when no vibeable is open: the run folder for run chats, else the project root. */
436
+ const defaultCwd = (scope) => (scope.kind === "run" ? safeRunDir(scope.runId) : getProjectRoot());
437
+ /**
438
+ * The folder the agent is actually spawned in. A vibeable that was removed
439
+ * leaves meta.cwd pointing nowhere; spawning there fails, so fall back to
440
+ * the scope's default while keeping meta.vibeable — the context then tells
441
+ * the agent the folder is gone instead of pretending it is there.
442
+ */
443
+ export async function effectiveCwd(meta) {
444
+ const st = await fs.stat(meta.cwd).catch(() => null);
445
+ if (st?.isDirectory())
446
+ return meta.cwd;
447
+ return defaultCwd(meta.scope);
448
+ }
379
449
  export async function createChat(opts) {
380
- const cwd = opts.scope.kind === "run" ? safeRunDir(opts.scope.runId) : getProjectRoot();
450
+ const cwd = defaultCwd(opts.scope);
381
451
  const now = new Date().toISOString();
382
452
  const meta = {
383
453
  id: newChatId(),
@@ -432,9 +502,7 @@ export async function postMessage(id, text) {
432
502
  void (async () => {
433
503
  try {
434
504
  const backend = await ensureBackend(state);
435
- const context = state.needsContext
436
- ? await scopeContext(state.meta.scope, state.meta.agent)
437
- : "";
505
+ const context = state.needsContext ? await scopeContext(state.meta) : "";
438
506
  state.needsContext = false;
439
507
  // "/<skill> args" expands to the skill's instructions for the agent;
440
508
  // the thread keeps the short form the user typed.
@@ -461,6 +529,48 @@ export async function postMessage(id, text) {
461
529
  })();
462
530
  return {};
463
531
  }
532
+ /**
533
+ * Open a vibeable in the chat (or close it with null). The agent's cwd
534
+ * becomes the app folder, so the running subprocess is dropped and the next
535
+ * message spawns one there with fresh context. Refused while a turn runs:
536
+ * the agent would keep editing in the old folder.
537
+ */
538
+ export async function setChatVibeable(id, slug) {
539
+ const state = await loadState(id);
540
+ if (!state)
541
+ return { error: "not found", status: 404 };
542
+ const busyError = { error: "agent is still working — wait for the turn to finish", status: 409 };
543
+ if (state.busy)
544
+ return busyError;
545
+ let dir = null;
546
+ if (slug) {
547
+ try {
548
+ dir = (await resolveVibeable(slug)).dir;
549
+ }
550
+ catch (err) {
551
+ const msg = err.message;
552
+ return { error: msg, status: /^no vibeable/.test(msg) ? 404 : /are off/.test(msg) ? 409 : 400 };
553
+ }
554
+ }
555
+ // A message may have started a turn during the await above; its backend
556
+ // was spawned in the old cwd, so refuse rather than relabel it. From here
557
+ // on nothing yields until cwd, vibeable and backend agree.
558
+ if (state.busy)
559
+ return busyError;
560
+ if (slug && dir) {
561
+ state.meta.vibeable = slug;
562
+ state.meta.cwd = dir;
563
+ }
564
+ else {
565
+ delete state.meta.vibeable;
566
+ state.meta.cwd = defaultCwd(state.meta.scope);
567
+ }
568
+ state.meta.updatedAt = new Date().toISOString();
569
+ dropBackend(state);
570
+ state.needsContext = true;
571
+ await writeMeta(state.meta);
572
+ return { meta: state.meta };
573
+ }
464
574
  export async function resolvePermission(id, requestId, optionId) {
465
575
  const state = await loadState(id);
466
576
  if (!state)
@@ -27,6 +27,8 @@ export interface ChatMeta {
27
27
  title: string;
28
28
  cwd: string;
29
29
  scope: ChatScope;
30
+ /** Slug of the vibeable open in this chat's preview pane (cwd is its folder while set). */
31
+ vibeable?: string;
30
32
  createdAt: string;
31
33
  updatedAt: string;
32
34
  }
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { realpathSync } from "node:fs";
3
3
  import path from "node:path";
4
- import { reposRootFor, resolveProject } from "../config.js";
4
+ import { reposRootFor, resolveProject, vibeablesRootFor } from "../config.js";
5
5
  import { ENV_FILE } from "../runner/docker.js";
6
6
  import { getOutputDir, getProjectRoot } from "./context.js";
7
7
  /**
@@ -190,7 +190,7 @@ async function checkedOut(repo) {
190
190
  }
191
191
  /**
192
192
  * The roots that may be committed, as repo-relative paths: the workflows,
193
- * knowledge, agents and skills roots this project declares, plus
193
+ * knowledge, agents, skills and vibeables roots this project declares, plus
194
194
  * kraftwerk.yml. The output directory is excluded even when it lives inside
195
195
  * one of them.
196
196
  */
@@ -217,6 +217,8 @@ function scopeFor({ project, repoRoot }) {
217
217
  path.resolve(project.root, project.config.knowledge ?? "knowledge"),
218
218
  path.resolve(project.root, project.config.agents ?? "agents"),
219
219
  path.resolve(project.root, project.config.skills ?? "skills"),
220
+ // Vibeables are the workspace's own apps: versioned with it, unlike clones.
221
+ vibeablesRootFor(project),
220
222
  project.configPath,
221
223
  ];
222
224
  const mapped = roots.filter((r) => !!r).map(rel).filter((r) => !!r);
@@ -393,7 +395,7 @@ export async function gitStatus(fresh = false) {
393
395
  const blocked = all.filter((f) => !f.syncable).sort(byPath);
394
396
  const blockedHidden = Math.max(0, blocked.length - MAX_BLOCKED);
395
397
  const scopeError = scope.wholeRepo
396
- ? "A configured root points at the repository root, which would put the whole repo in scope. Narrow workflows/knowledge/agents/skills in kraftwerk.yml."
398
+ ? "A configured root points at the repository root, which would put the whole repo in scope. Narrow workflows/knowledge/agents/skills/vibeables in kraftwerk.yml."
397
399
  : undefined;
398
400
  const value = {
399
401
  ...base,
@@ -0,0 +1,3 @@
1
+ export declare function newestMtime(dir: string, depth?: number, budget?: {
2
+ files: number;
3
+ }): Promise<number>;
@@ -0,0 +1,31 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * Newest mtime under a folder, for "changed 5m ago" on lists of folders. A
5
+ * directory's own mtime only moves when an entry is added or removed, not
6
+ * when a file inside is edited, so the walk looks at files. Bounded: git
7
+ * internals, dependencies and dot entries are skipped, depth is capped, and
8
+ * a huge tree stops early — the number is a hint, not an audit.
9
+ */
10
+ const SKIP = /^(\.git|node_modules|\.cache|\.vite|\.next|vendor|dist|build|target)$/;
11
+ export async function newestMtime(dir, depth = 0, budget = { files: 2_000 }) {
12
+ let newest = 0;
13
+ const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
14
+ for (const e of entries) {
15
+ if (budget.files-- <= 0)
16
+ break;
17
+ if (SKIP.test(e.name) || e.name.startsWith("."))
18
+ continue;
19
+ const abs = path.join(dir, e.name);
20
+ if (e.isDirectory()) {
21
+ if (depth < 4)
22
+ newest = Math.max(newest, await newestMtime(abs, depth + 1, budget));
23
+ }
24
+ else {
25
+ const st = await fs.stat(abs).catch(() => null);
26
+ if (st)
27
+ newest = Math.max(newest, st.mtimeMs);
28
+ }
29
+ }
30
+ return newest;
31
+ }
@@ -37,6 +37,8 @@ export interface RepoInfo {
37
37
  ahead?: number;
38
38
  /** Commits on the upstream not here. Absent when the branch tracks nothing. */
39
39
  behind?: number;
40
+ /** Newest change in the working tree (.git, node_modules and dot entries excluded); lists sort by it. */
41
+ updatedAt?: string;
40
42
  /** Set when git could not be read for this folder. */
41
43
  error?: string;
42
44
  }
@@ -2,6 +2,7 @@ import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { gitignoreHas, ignoreEntryFor, reposRootFor, resolveProject } from "../config.js";
4
4
  import { getProjectRoot } from "./context.js";
5
+ import { newestMtime } from "./mtime.js";
5
6
  import { git, gitNet, sshCommandFor } from "./git.js";
6
7
  /**
7
8
  * A clone is the one call here that can legitimately take long: a large
@@ -61,11 +62,14 @@ export async function openRepos() {
61
62
  async function inspect(root, slug) {
62
63
  const dir = path.join(root, slug);
63
64
  const info = { slug, path: dir, dirty: 0 };
64
- const [status, log, origin] = await Promise.all([
65
+ const [status, log, origin, newest] = await Promise.all([
65
66
  git(["status", "--porcelain=v2", "--branch", "--untracked-files=normal"], dir),
66
67
  git(["log", "-1", "--format=%h%x00%s%x00%cI"], dir),
67
68
  git(["remote", "get-url", "origin"], dir),
69
+ newestMtime(dir),
68
70
  ]);
71
+ if (newest)
72
+ info.updatedAt = new Date(newest).toISOString();
69
73
  if (!status.ok)
70
74
  return { ...info, error: status.stderr || "not a git repository" };
71
75
  if (origin.ok)