@netnodeag/kraftwerk 0.38.0 → 0.39.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 (258) 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/sessions.d.ts +11 -0
  8. package/dist/inspector/chat/sessions.js +86 -10
  9. package/dist/inspector/chat/types.d.ts +2 -0
  10. package/dist/inspector/git.js +5 -3
  11. package/dist/inspector/mtime.d.ts +3 -0
  12. package/dist/inspector/mtime.js +31 -0
  13. package/dist/inspector/repos.d.ts +2 -0
  14. package/dist/inspector/repos.js +5 -1
  15. package/dist/inspector/server.js +90 -1
  16. package/dist/inspector/settings.d.ts +3 -1
  17. package/dist/inspector/settings.js +19 -8
  18. package/dist/inspector/vibeables.d.ts +152 -0
  19. package/dist/inspector/vibeables.js +509 -0
  20. package/inspector/dist/assets/apl-gfMMiTNf.js +1 -0
  21. package/inspector/dist/assets/asciiarmor-C_eisQCp.js +1 -0
  22. package/inspector/dist/assets/asn1-DCl_8MCW.js +1 -0
  23. package/inspector/dist/assets/asterisk-BEn0Cwpx.js +1 -0
  24. package/inspector/dist/assets/brainfuck-D5EjA2JK.js +1 -0
  25. package/inspector/dist/assets/clike-D_6L3tnO.js +1 -0
  26. package/inspector/dist/assets/clojure-BflJGmDX.js +1 -0
  27. package/inspector/dist/assets/cmake-84nbDH0I.js +1 -0
  28. package/inspector/dist/assets/cobol-DHLQUbN7.js +1 -0
  29. package/inspector/dist/assets/coffeescript-De_zI4J0.js +1 -0
  30. package/inspector/dist/assets/commonlisp-DgAKBe0r.js +1 -0
  31. package/inspector/dist/assets/crystal-Bsdphhtm.js +1 -0
  32. package/inspector/dist/assets/css-CnaYoQib.js +1 -0
  33. package/inspector/dist/assets/cypher-p9eYesGn.js +1 -0
  34. package/inspector/dist/assets/d-DWkQyNnU.js +1 -0
  35. package/inspector/dist/assets/diff-ChtP43wD.js +1 -0
  36. package/inspector/dist/assets/dist-3Nzkrylv.js +1 -0
  37. package/inspector/dist/assets/dist-5wk4daHl.js +1 -0
  38. package/inspector/dist/assets/dist-BCM6ehYa.js +1 -0
  39. package/inspector/dist/assets/dist-BECiZEqH.js +1 -0
  40. package/inspector/dist/assets/dist-BbhR2mEU.js +1 -0
  41. package/inspector/dist/assets/dist-CF7An11X.js +1 -0
  42. package/inspector/dist/assets/dist-CI-2-8Tz.js +23 -0
  43. package/inspector/dist/assets/dist-CPlSMtdY.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-D3tgdU3w.js +1 -0
  47. package/inspector/dist/assets/dist-D8nmL5S9.js +1 -0
  48. package/inspector/dist/assets/dist-DNBDudvB.js +1 -0
  49. package/inspector/dist/assets/dist-DTW4FhJ_.js +1 -0
  50. package/inspector/dist/assets/dist-DXIk6Ebu.js +1 -0
  51. package/inspector/dist/assets/dist-DaG9QYNU.js +1 -0
  52. package/inspector/dist/assets/dist-Dd-u7fXY.js +1 -0
  53. package/inspector/dist/assets/dist-DwyxsFyN.js +1 -0
  54. package/inspector/dist/assets/dist-H3rerbMg.js +13 -0
  55. package/inspector/dist/assets/dist-ZMc9InB9.js +1 -0
  56. package/inspector/dist/assets/dist-nc_mc1w0.js +1 -0
  57. package/inspector/dist/assets/dist-rE2oRtJp.js +11 -0
  58. package/inspector/dist/assets/dist-yFKhRrHN.js +2 -0
  59. package/inspector/dist/assets/dockerfile-DJkHQgkl.js +1 -0
  60. package/inspector/dist/assets/dtd-DTF72YNO.js +1 -0
  61. package/inspector/dist/assets/dylan-BCDoL_-p.js +1 -0
  62. package/inspector/dist/assets/ebnf-D4c0_ac3.js +1 -0
  63. package/inspector/dist/assets/ecl-BVIeWmwY.js +1 -0
  64. package/inspector/dist/assets/editor-6yCJBeDg.js +120 -0
  65. package/inspector/dist/assets/editor-B1Jq9YIq.css +1 -0
  66. package/inspector/dist/assets/eiffel-BEjRio4Q.js +1 -0
  67. package/inspector/dist/assets/elm-Cshzl8qu.js +1 -0
  68. package/inspector/dist/assets/erlang-BV0c4Hdn.js +1 -0
  69. package/inspector/dist/assets/factor-LKH5gN8L.js +1 -0
  70. package/inspector/dist/assets/fcl-ClOhbFR7.js +1 -0
  71. package/inspector/dist/assets/forth-D6RzAl64.js +1 -0
  72. package/inspector/dist/assets/fortran-Bt6PBEDR.js +1 -0
  73. package/inspector/dist/assets/gas-BHEdbvp9.js +1 -0
  74. package/inspector/dist/assets/gherkin-oBAE_ms0.js +1 -0
  75. package/inspector/dist/assets/groovy-CUpZ7dEl.js +1 -0
  76. package/inspector/dist/assets/haskell-BRsoo5mP.js +1 -0
  77. package/inspector/dist/assets/haxe-DjVOVvNP.js +1 -0
  78. package/inspector/dist/assets/http-CLVgA2GD.js +1 -0
  79. package/inspector/dist/assets/idl-BUZw3wgd.js +1 -0
  80. package/inspector/dist/assets/index-Dom87GNV.css +1 -0
  81. package/inspector/dist/assets/index-DurTvcNI.js +218 -0
  82. package/inspector/dist/assets/javascript-Bu8mWwtz.js +1 -0
  83. package/inspector/dist/assets/julia-CQfgDRfP.js +1 -0
  84. package/inspector/dist/assets/livescript-BXxG0Yva.js +1 -0
  85. package/inspector/dist/assets/lua-DPCEkfD_.js +1 -0
  86. package/inspector/dist/assets/material-symbols-rounded-DzDTtRam.woff2 +0 -0
  87. package/inspector/dist/assets/mathematica-u8YMmbU0.js +1 -0
  88. package/inspector/dist/assets/mbox-BEMVcqjs.js +1 -0
  89. package/inspector/dist/assets/{mirc-CjQqDB4T.js → mirc-C9zpzAZU.js} +1 -1
  90. package/inspector/dist/assets/mllike-B45xgp2S.js +1 -0
  91. package/inspector/dist/assets/modelica-CqHI_q-D.js +1 -0
  92. package/inspector/dist/assets/mscgen-qgSLujhx.js +1 -0
  93. package/inspector/dist/assets/mumps-BSzj6Xyz.js +1 -0
  94. package/inspector/dist/assets/{nginx-DdIZxoE0.js → nginx-8H7HVH4w.js} +1 -1
  95. package/inspector/dist/assets/nsis-viPvXFiq.js +1 -0
  96. package/inspector/dist/assets/ntriples-B9h1VB_g.js +1 -0
  97. package/inspector/dist/assets/octave-D2Q8cUp1.js +1 -0
  98. package/inspector/dist/assets/oz-BwTct_5T.js +1 -0
  99. package/inspector/dist/assets/pascal-Dp_KdFgX.js +1 -0
  100. package/inspector/dist/assets/perl-BkSkiI9m.js +1 -0
  101. package/inspector/dist/assets/pig-AF4Hwhju.js +1 -0
  102. package/inspector/dist/assets/powershell-BcIto6Sf.js +1 -0
  103. package/inspector/dist/assets/properties-C357ku5U.js +1 -0
  104. package/inspector/dist/assets/protobuf-D1ij_kNL.js +1 -0
  105. package/inspector/dist/assets/pug-3K-JMW4g.js +1 -0
  106. package/inspector/dist/assets/puppet-BZyAS7I4.js +1 -0
  107. package/inspector/dist/assets/python-B_1Jd8HM.js +1 -0
  108. package/inspector/dist/assets/q-B9NhS7L_.js +1 -0
  109. package/inspector/dist/assets/r-CxDmU1I4.js +1 -0
  110. package/inspector/dist/assets/rpm-gpDK5sbt.js +1 -0
  111. package/inspector/dist/assets/ruby-ChfNwlCY.js +1 -0
  112. package/inspector/dist/assets/{sas-B4kiWyti.js → sas-C2OyD2Y6.js} +1 -1
  113. package/inspector/dist/assets/scheme-D9qYN0V5.js +1 -0
  114. package/inspector/dist/assets/shell-DwuoZtxw.js +1 -0
  115. package/inspector/dist/assets/sieve-tLWDfEkZ.js +1 -0
  116. package/inspector/dist/assets/simple-mode-DXiLM1-G.js +1 -0
  117. package/inspector/dist/assets/smalltalk-BLp5-jwC.js +1 -0
  118. package/inspector/dist/assets/solr-B78wSQJQ.js +1 -0
  119. package/inspector/dist/assets/sparql-Dz-mlCAC.js +1 -0
  120. package/inspector/dist/assets/spreadsheet-CTIncYxj.js +1 -0
  121. package/inspector/dist/assets/sql-DuQMFDvy.js +1 -0
  122. package/inspector/dist/assets/stex-DNzd62Q_.js +1 -0
  123. package/inspector/dist/assets/stylus-BOVgKnng.js +1 -0
  124. package/inspector/dist/assets/swift-DKB6_1j6.js +1 -0
  125. package/inspector/dist/assets/tcl-BGfruO1u.js +1 -0
  126. package/inspector/dist/assets/textile-C4jVoHG1.js +1 -0
  127. package/inspector/dist/assets/tiddlywiki-VlUiK86J.js +1 -0
  128. package/inspector/dist/assets/tiki-AK6FwT8q.js +1 -0
  129. package/inspector/dist/assets/toml-BPTmHmyx.js +1 -0
  130. package/inspector/dist/assets/troff-C2dAgh7P.js +1 -0
  131. package/inspector/dist/assets/ttcn-PPSTk1ow.js +1 -0
  132. package/inspector/dist/assets/ttcn-cfg-2b4Gb5sE.js +1 -0
  133. package/inspector/dist/assets/turtle-Cx3rYX2g.js +1 -0
  134. package/inspector/dist/assets/vb-BthdM_4N.js +1 -0
  135. package/inspector/dist/assets/vbscript-D9f6rSsU.js +1 -0
  136. package/inspector/dist/assets/velocity-CqftRsjg.js +1 -0
  137. package/inspector/dist/assets/verilog-CjI9Y6RI.js +1 -0
  138. package/inspector/dist/assets/vhdl-kS471soi.js +1 -0
  139. package/inspector/dist/assets/webidl-D5jiJvOU.js +1 -0
  140. package/inspector/dist/assets/xquery-BKJx4mf9.js +1 -0
  141. package/inspector/dist/assets/yacas-CXXgtw0p.js +1 -0
  142. package/inspector/dist/assets/z80-Co3PbHdE.js +1 -0
  143. package/inspector/dist/index.html +2 -2
  144. package/package.json +6 -6
  145. package/inspector/dist/assets/apl-B4CMkyY2.js +0 -1
  146. package/inspector/dist/assets/asciiarmor-Df11BRmG.js +0 -1
  147. package/inspector/dist/assets/asn1-EdZsLKOL.js +0 -1
  148. package/inspector/dist/assets/asterisk-B-8jnY81.js +0 -1
  149. package/inspector/dist/assets/brainfuck-C4LP7Hcl.js +0 -1
  150. package/inspector/dist/assets/clike-B9uivgTg.js +0 -1
  151. package/inspector/dist/assets/clojure-BMjYHr_A.js +0 -1
  152. package/inspector/dist/assets/cmake-BQqOBYOt.js +0 -1
  153. package/inspector/dist/assets/cobol-CWcv1MsR.js +0 -1
  154. package/inspector/dist/assets/coffeescript-S37ZYGWr.js +0 -1
  155. package/inspector/dist/assets/commonlisp-DBKNyK5s.js +0 -1
  156. package/inspector/dist/assets/crystal-SjHAIU92.js +0 -1
  157. package/inspector/dist/assets/css-BnMrqG3P.js +0 -1
  158. package/inspector/dist/assets/cypher-C_CwsFkJ.js +0 -1
  159. package/inspector/dist/assets/d-pRatUO7H.js +0 -1
  160. package/inspector/dist/assets/diff-DbItnlRl.js +0 -1
  161. package/inspector/dist/assets/dockerfile-BKs6k2Af.js +0 -1
  162. package/inspector/dist/assets/dtd-DF_7sFjM.js +0 -1
  163. package/inspector/dist/assets/dylan-DwRh75JA.js +0 -1
  164. package/inspector/dist/assets/ebnf-CDyGwa7X.js +0 -1
  165. package/inspector/dist/assets/ecl-Cabwm37j.js +0 -1
  166. package/inspector/dist/assets/editor-9rr2cQaW.css +0 -1
  167. package/inspector/dist/assets/editor-BnDJd3j4.js +0 -144
  168. package/inspector/dist/assets/eiffel-CnydiIhH.js +0 -1
  169. package/inspector/dist/assets/elm-vLlmbW-K.js +0 -1
  170. package/inspector/dist/assets/erlang-BNw1qcRV.js +0 -1
  171. package/inspector/dist/assets/factor-kuTfRLto.js +0 -1
  172. package/inspector/dist/assets/fcl-Kvtd6kyn.js +0 -1
  173. package/inspector/dist/assets/forth-Ffai-XNe.js +0 -1
  174. package/inspector/dist/assets/fortran-DYz_wnZ1.js +0 -1
  175. package/inspector/dist/assets/gas-Bneqetm1.js +0 -1
  176. package/inspector/dist/assets/gherkin-heZmZLOM.js +0 -1
  177. package/inspector/dist/assets/groovy-D9Dt4D0W.js +0 -1
  178. package/inspector/dist/assets/haskell-Cw1EW3IL.js +0 -1
  179. package/inspector/dist/assets/haxe-H-WmDvRZ.js +0 -1
  180. package/inspector/dist/assets/http-DBlCnlav.js +0 -1
  181. package/inspector/dist/assets/idl-BEugSyMb.js +0 -1
  182. package/inspector/dist/assets/index-1K-YGe-R.js +0 -1
  183. package/inspector/dist/assets/index-5n-x53jb.js +0 -1
  184. package/inspector/dist/assets/index-B6zeZ-I9.js +0 -2
  185. package/inspector/dist/assets/index-BCwvYdEE.js +0 -1
  186. package/inspector/dist/assets/index-BDHHRQvJ.js +0 -7
  187. package/inspector/dist/assets/index-BFmXikOX.js +0 -1
  188. package/inspector/dist/assets/index-C2kxy-K_.js +0 -1
  189. package/inspector/dist/assets/index-CTECF5uX.js +0 -1
  190. package/inspector/dist/assets/index-CtnGrZvu.js +0 -217
  191. package/inspector/dist/assets/index-DBHgQG4M.js +0 -1
  192. package/inspector/dist/assets/index-DfqhDHp9.js +0 -1
  193. package/inspector/dist/assets/index-DjMx3a5H.css +0 -1
  194. package/inspector/dist/assets/index-DmY7gYaR.js +0 -1
  195. package/inspector/dist/assets/index-Dq3QrDLb.js +0 -1
  196. package/inspector/dist/assets/index-Fd3s8yys.js +0 -1
  197. package/inspector/dist/assets/index-Rmy9pam8.js +0 -1
  198. package/inspector/dist/assets/index-VxlzgVIX.js +0 -1
  199. package/inspector/dist/assets/index-o4xPJnqZ.js +0 -1
  200. package/inspector/dist/assets/index-sSEE6ykA.js +0 -3
  201. package/inspector/dist/assets/javascript-iXu5QeM3.js +0 -1
  202. package/inspector/dist/assets/julia-DuME0IfC.js +0 -1
  203. package/inspector/dist/assets/livescript-BwQOo05w.js +0 -1
  204. package/inspector/dist/assets/lua-VAEuO923.js +0 -1
  205. package/inspector/dist/assets/material-symbols-rounded-DxXFL64Y.woff2 +0 -0
  206. package/inspector/dist/assets/mathematica-DTrFuWx2.js +0 -1
  207. package/inspector/dist/assets/mbox-CNhZ1qSd.js +0 -1
  208. package/inspector/dist/assets/mllike-CXdrOF99.js +0 -1
  209. package/inspector/dist/assets/modelica-Dc1JOy9r.js +0 -1
  210. package/inspector/dist/assets/mscgen-BA5vi2Kp.js +0 -1
  211. package/inspector/dist/assets/mumps-BT43cFF4.js +0 -1
  212. package/inspector/dist/assets/nsis-LY0VT_u5.js +0 -1
  213. package/inspector/dist/assets/ntriples-BfvgReVJ.js +0 -1
  214. package/inspector/dist/assets/octave-Ck1zUtKM.js +0 -1
  215. package/inspector/dist/assets/oz-BzwKVEFT.js +0 -1
  216. package/inspector/dist/assets/pascal--L3eBynH.js +0 -1
  217. package/inspector/dist/assets/perl-CdXCOZ3F.js +0 -1
  218. package/inspector/dist/assets/pig-CevX1Tat.js +0 -1
  219. package/inspector/dist/assets/powershell-CFHJl5sT.js +0 -1
  220. package/inspector/dist/assets/properties-C78fOPTZ.js +0 -1
  221. package/inspector/dist/assets/protobuf-ChK-085T.js +0 -1
  222. package/inspector/dist/assets/pug-DeIclll2.js +0 -1
  223. package/inspector/dist/assets/puppet-DMA9R1ak.js +0 -1
  224. package/inspector/dist/assets/python-BuPzkPfP.js +0 -1
  225. package/inspector/dist/assets/q-pXgVlZs6.js +0 -1
  226. package/inspector/dist/assets/r-B6wPVr8A.js +0 -1
  227. package/inspector/dist/assets/rpm-CTu-6PCP.js +0 -1
  228. package/inspector/dist/assets/ruby-BK5aLJev.js +0 -1
  229. package/inspector/dist/assets/scheme-C41bIUwD.js +0 -1
  230. package/inspector/dist/assets/shell-CjFT_Tl9.js +0 -1
  231. package/inspector/dist/assets/sieve-C3Gn_uJK.js +0 -1
  232. package/inspector/dist/assets/simple-mode-GW_nhZxv.js +0 -1
  233. package/inspector/dist/assets/smalltalk-CnHTOXQT.js +0 -1
  234. package/inspector/dist/assets/solr-DehyRSwq.js +0 -1
  235. package/inspector/dist/assets/sparql-DkYu6x3z.js +0 -1
  236. package/inspector/dist/assets/spreadsheet-BCZA_wO0.js +0 -1
  237. package/inspector/dist/assets/sql-D0XecflT.js +0 -1
  238. package/inspector/dist/assets/stex-C3f8Ysf7.js +0 -1
  239. package/inspector/dist/assets/stylus-B533Al4x.js +0 -1
  240. package/inspector/dist/assets/swift-BzpIVaGY.js +0 -1
  241. package/inspector/dist/assets/tcl-DVfN8rqt.js +0 -1
  242. package/inspector/dist/assets/textile-CnDTJFAw.js +0 -1
  243. package/inspector/dist/assets/tiddlywiki-DO-Gjzrf.js +0 -1
  244. package/inspector/dist/assets/tiki-DGYXhP31.js +0 -1
  245. package/inspector/dist/assets/toml-Bm5Em-hy.js +0 -1
  246. package/inspector/dist/assets/troff-wAsdV37c.js +0 -1
  247. package/inspector/dist/assets/ttcn-CfJYG6tj.js +0 -1
  248. package/inspector/dist/assets/ttcn-cfg-B9xdYoR4.js +0 -1
  249. package/inspector/dist/assets/turtle-B1tBg_DP.js +0 -1
  250. package/inspector/dist/assets/vb-CmGdzxic.js +0 -1
  251. package/inspector/dist/assets/vbscript-BuJXcnF6.js +0 -1
  252. package/inspector/dist/assets/velocity-D8B20fx6.js +0 -1
  253. package/inspector/dist/assets/verilog-C6RDOZhf.js +0 -1
  254. package/inspector/dist/assets/vhdl-lSbBsy5d.js +0 -1
  255. package/inspector/dist/assets/webidl-ZXfAyPTL.js +0 -1
  256. package/inspector/dist/assets/xquery-DzFWVndE.js +0 -1
  257. package/inspector/dist/assets/yacas-BJ4BC0dw.js +0 -1
  258. 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
  }
@@ -18,6 +18,17 @@ export declare function getChat(id: string): Promise<{
18
18
  export declare function postMessage(id: string, text: string): Promise<{
19
19
  error?: string;
20
20
  }>;
21
+ /**
22
+ * Open a vibeable in the chat (or close it with null). The agent's cwd
23
+ * becomes the app folder, so the running subprocess is dropped and the next
24
+ * message spawns one there with fresh context. Refused while a turn runs:
25
+ * the agent would keep editing in the old folder.
26
+ */
27
+ export declare function setChatVibeable(id: string, slug: string | null): Promise<{
28
+ error?: string;
29
+ status?: number;
30
+ meta?: ChatMeta;
31
+ }>;
21
32
  export declare function resolvePermission(id: string, requestId: string, optionId: string | null): Promise<{
22
33
  error?: string;
23
34
  }>;
@@ -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,43 @@ 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
+ async function vibeableContext(slug) {
179
+ const st = await vibeableStatus(slug).catch(() => null);
180
+ if (!st) {
181
+ 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.`;
182
+ }
183
+ const dev = st.dev;
184
+ const mode = dev?.running
185
+ ? `A dev server is running (\`${dev.command}\`, port ${dev.port}${dev.ready ? "" : ", not answering yet"}); the pane shows it.`
186
+ : st.config.dev
187
+ ? `${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.`
188
+ : `No dev command: the pane shows the folder${st.config.dir ? ` ${st.config.dir}/` : ""} served as-is.`;
189
+ return (`## Vibeable: ${slug}\n` +
190
+ `You are building a small piece of software live with the user. Your working directory is its folder: ${st.path}. ` +
191
+ `The user sees it rendered in a preview pane next to this chat, and every file you save reloads that preview within a second — ` +
192
+ `work in small visible steps, and describe what changed instead of pasting code. Think ad-hoc software, not a website: a dashboard, ` +
193
+ `a slideshow, a calculator, a tracker with a backend — whatever the user asks for.\n\n` +
194
+ `Current state: ${mode}${st.configError ? ` (${st.configError} — fix it)` : ""}\n\n` +
195
+ `How the preview runs:\n` +
196
+ `- Static mode (default): the inspector serves the folder directly at /vibeables/${slug}/ — index.html is the entry; plain HTML, CSS and ` +
197
+ `JavaScript with no build step. Use relative URLs (./app.js, not /app.js). ES modules and CDN imports (https://esm.sh/<pkg>) work. ` +
198
+ `The static preview is a sandboxed document: no localStorage, no cookies, no calls into the inspector's own /api. Keep state in memory, ` +
199
+ `or move to dev mode when it must persist.\n` +
200
+ `- Dev mode: when the app needs a build tool, a backend or a database, write ${VIBEABLE_CONFIG_FILE} with \`dev: <command>\` ` +
201
+ `(e.g. \`npm run dev\`, \`node server.js\`, \`python3 -m http.server $PORT\`). kraftwerk starts it inside the folder with PORT in the ` +
202
+ `environment and the pane embeds that server; the command must listen on $PORT (or declare \`port:\` when it cannot). Optional ` +
203
+ `\`dir:\` picks the folder for static mode (e.g. dist). The app brings its own backend and storage — a small node server writing JSON ` +
204
+ `files, SQLite, an external API — nothing of that is provided by kraftwerk.\n\n` +
205
+ `Rules: the folder is part of this workspace and versioned with it — do not run git yourself, the user reviews and commits from the ` +
206
+ `inspector's Git screen; keep secrets out of the folder (.env is git-ignored, node_modules too); leave ${VIBEABLE_CONFIG_FILE} valid YAML; ` +
207
+ `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.`);
208
+ }
171
209
  /** Base context per scope; scopeContext() appends the shared skills block. */
172
210
  async function baseScopeContext(scope, agent) {
173
211
  if (scope.kind === "agent") {
@@ -301,19 +339,22 @@ const RENDERING_BLOCK = `## Chat rendering\n` +
301
339
  `with ![alt](/api/runs/<run-id>/file?name=picture.png&raw=1), or link any artifact the same way. ` +
302
340
  `Prefer these relative /api/... URLs over file paths when showing results: they render directly ` +
303
341
  `in this chat and keep working wherever the inspector is reachable.`;
304
- async function scopeContext(scope, agent) {
342
+ async function scopeContext(meta) {
343
+ const { scope, agent } = meta;
305
344
  // The repositories block reads every clone from git, so it runs alongside
306
345
  // the rest instead of adding its spawns to the first prompt's latency.
307
- const [base, repos, skills] = await Promise.all([
346
+ const [base, repos, vibe, skills] = await Promise.all([
308
347
  baseScopeContext(scope, agent),
309
348
  scope.kind === "agent" || scope.kind === "kraftwerk" ? reposContext() : Promise.resolve(""),
349
+ meta.vibeable ? vibeableContext(meta.vibeable) : Promise.resolve(""),
310
350
  availableSkills(scope),
311
351
  ]);
312
- return [base, repos, RENDERING_BLOCK, skillsBlock(skills)].filter(Boolean).join("\n\n");
352
+ return [base, repos, vibe, RENDERING_BLOCK, skillsBlock(skills)].filter(Boolean).join("\n\n");
313
353
  }
314
354
  /* ---------- backend lifecycle ---------- */
315
355
  /** Agent sessions carry the agent's model/effort; resolved live so edits apply to new backends. */
316
- async function backendTuning(scope, agent) {
356
+ async function backendTuning(meta) {
357
+ const { scope, agent } = meta;
317
358
  const tuning = {};
318
359
  if (scope.kind === "agent") {
319
360
  const def = await getAgent(scope.slug).catch(() => null);
@@ -327,7 +368,8 @@ async function backendTuning(scope, agent) {
327
368
  }
328
369
  // Run chats live in the run folder — grant claude the project root so
329
370
  // project-level skills and files stay reachable.
330
- if (scope.kind === "run" && agent === "claude")
371
+ // A vibeable session works inside the app folder for the same reason.
372
+ if ((scope.kind === "run" || meta.vibeable) && agent === "claude")
331
373
  tuning.addDirs = [getProjectRoot()];
332
374
  // pi loads skills only when told: hand it every visible skill folder.
333
375
  if (agent === "pi") {
@@ -365,7 +407,7 @@ async function ensureBackend(state) {
365
407
  },
366
408
  };
367
409
  const { agent, cwd } = state.meta;
368
- const tuning = await backendTuning(state.meta.scope, agent);
410
+ const tuning = await backendTuning(state.meta);
369
411
  state.backend =
370
412
  agent === "pi"
371
413
  ? startPiBackend(cwd, hooks, tuning)
@@ -376,8 +418,10 @@ async function ensureBackend(state) {
376
418
  return state.backend;
377
419
  }
378
420
  /* ---------- public API (used by server.ts) ---------- */
421
+ /** Where a chat's agent runs when no vibeable is open: the run folder for run chats, else the project root. */
422
+ const defaultCwd = (scope) => (scope.kind === "run" ? safeRunDir(scope.runId) : getProjectRoot());
379
423
  export async function createChat(opts) {
380
- const cwd = opts.scope.kind === "run" ? safeRunDir(opts.scope.runId) : getProjectRoot();
424
+ const cwd = defaultCwd(opts.scope);
381
425
  const now = new Date().toISOString();
382
426
  const meta = {
383
427
  id: newChatId(),
@@ -432,9 +476,7 @@ export async function postMessage(id, text) {
432
476
  void (async () => {
433
477
  try {
434
478
  const backend = await ensureBackend(state);
435
- const context = state.needsContext
436
- ? await scopeContext(state.meta.scope, state.meta.agent)
437
- : "";
479
+ const context = state.needsContext ? await scopeContext(state.meta) : "";
438
480
  state.needsContext = false;
439
481
  // "/<skill> args" expands to the skill's instructions for the agent;
440
482
  // the thread keeps the short form the user typed.
@@ -461,6 +503,40 @@ export async function postMessage(id, text) {
461
503
  })();
462
504
  return {};
463
505
  }
506
+ /**
507
+ * Open a vibeable in the chat (or close it with null). The agent's cwd
508
+ * becomes the app folder, so the running subprocess is dropped and the next
509
+ * message spawns one there with fresh context. Refused while a turn runs:
510
+ * the agent would keep editing in the old folder.
511
+ */
512
+ export async function setChatVibeable(id, slug) {
513
+ const state = await loadState(id);
514
+ if (!state)
515
+ return { error: "not found", status: 404 };
516
+ if (state.busy)
517
+ return { error: "agent is still working — wait for the turn to finish", status: 409 };
518
+ if (slug) {
519
+ let dir;
520
+ try {
521
+ dir = (await resolveVibeable(slug)).dir;
522
+ }
523
+ catch (err) {
524
+ const msg = err.message;
525
+ return { error: msg, status: /^no vibeable/.test(msg) ? 404 : /are off/.test(msg) ? 409 : 400 };
526
+ }
527
+ state.meta.vibeable = slug;
528
+ state.meta.cwd = dir;
529
+ }
530
+ else {
531
+ delete state.meta.vibeable;
532
+ state.meta.cwd = defaultCwd(state.meta.scope);
533
+ }
534
+ state.meta.updatedAt = new Date().toISOString();
535
+ await writeMeta(state.meta);
536
+ dropBackend(state);
537
+ state.needsContext = true;
538
+ return { meta: state.meta };
539
+ }
464
540
  export async function resolvePermission(id, requestId, optionId) {
465
541
  const state = await loadState(id);
466
542
  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)