mulmoterminal 7.1.0 → 7.2.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 (438) hide show
  1. package/README.md +25 -7
  2. package/bin/cli-args.d.ts +1 -0
  3. package/bin/cli-args.js +5 -0
  4. package/bin/mulmoterminal.js +23 -3
  5. package/bin/ssh-hint.d.ts +3 -0
  6. package/bin/ssh-hint.js +32 -0
  7. package/bin/stop.d.ts +3 -1
  8. package/bin/stop.js +33 -6
  9. package/blueprints/ask/skills/keep/SKILL.md +1 -1
  10. package/blueprints/cloudflare/checks/page-renders.sh +12 -1
  11. package/blueprints/cloudflare/skills/spec/SKILL.md +1 -1
  12. package/blueprints/docs/checks/chaff.sh +1 -1
  13. package/blueprints/docs/checks/drafts.mjs +9 -6
  14. package/blueprints/docs/checks/places.mjs +6 -2
  15. package/blueprints/docs/skills/workspace/SKILL.md +1 -1
  16. package/blueprints/firebase/checks/page-renders.sh +12 -1
  17. package/blueprints/firebase/skills/spec/SKILL.md +1 -1
  18. package/blueprints/from-collection/checks/actions.sh +9 -7
  19. package/blueprints/from-collection/checks/compare.mjs +20 -10
  20. package/blueprints/from-collection/checks/import-supabase.sh +35 -0
  21. package/blueprints/from-collection/checks/supabase-verify.mjs +82 -0
  22. package/blueprints/from-collection/manifest.json +2 -2
  23. package/blueprints/from-collection/skills/acceptance-supabase/SKILL.md +30 -0
  24. package/blueprints/from-collection/skills/actions-supabase/SKILL.md +50 -0
  25. package/blueprints/from-collection/skills/import-production-supabase/SKILL.md +28 -0
  26. package/blueprints/from-collection/skills/import-supabase/SKILL.md +60 -0
  27. package/blueprints/from-collection/spec/conversion.md +21 -2
  28. package/blueprints/from-collection/steps.json +37 -0
  29. package/blueprints/local/skills/spec/SKILL.md +1 -1
  30. package/blueprints/polish/checks/feedback.mjs +3 -1
  31. package/blueprints/polish/checks/kind.d.mts +9 -0
  32. package/blueprints/polish/checks/kind.mjs +27 -0
  33. package/blueprints/polish/checks/named.d.mts +6 -0
  34. package/blueprints/polish/checks/named.mjs +49 -0
  35. package/blueprints/polish/checks/report.mjs +12 -0
  36. package/blueprints/polish/checks/targets.mjs +93 -9
  37. package/blueprints/polish/checks/targetsView.mjs +4 -1
  38. package/blueprints/polish/checks/viewpoints.d.mts +22 -0
  39. package/blueprints/polish/checks/viewpoints.mjs +113 -0
  40. package/blueprints/polish/hearing.json +28 -1
  41. package/blueprints/polish/kinds.json +15 -0
  42. package/blueprints/polish/presets/blog/blog.md +19 -0
  43. package/blueprints/polish/presets/houkoku/houkoku.md +19 -0
  44. package/blueprints/polish/presets.json +27 -0
  45. package/blueprints/polish/skills/polish/SKILL.md +32 -2
  46. package/blueprints/polish/skills/report/SKILL.md +11 -2
  47. package/blueprints/polish/skills/survey/SKILL.md +18 -0
  48. package/blueprints/polish/viewpoints.json +89 -0
  49. package/blueprints/product/manifest.json +2 -2
  50. package/blueprints/product/skills/acceptance-supabase/SKILL.md +30 -0
  51. package/blueprints/product/steps.json +9 -0
  52. package/blueprints/refactor/skills/report/SKILL.md +2 -1
  53. package/blueprints/refactor/skills/survey/SKILL.md +1 -1
  54. package/blueprints/review/skills/report/SKILL.md +1 -1
  55. package/blueprints/style/skills/report/SKILL.md +1 -1
  56. package/blueprints/supabase/checks/advisors.mjs +24 -0
  57. package/blueprints/supabase/checks/client-secrets.mjs +145 -0
  58. package/blueprints/supabase/checks/deploy-check.sh +33 -0
  59. package/blueprints/supabase/checks/handover.sh +14 -0
  60. package/blueprints/supabase/checks/linked-url.mjs +37 -0
  61. package/blueprints/supabase/checks/local-stack.sh +4 -0
  62. package/blueprints/supabase/checks/migrations-applied.mjs +49 -0
  63. package/blueprints/supabase/checks/page-renders.sh +39 -0
  64. package/blueprints/supabase/checks/prerequisites.sh +8 -0
  65. package/blueprints/supabase/checks/scaffold.sh +14 -0
  66. package/blueprints/supabase/checks/security-probe.mjs +244 -0
  67. package/blueprints/supabase/checks/security-report.sh +11 -0
  68. package/blueprints/supabase/checks/security.sh +49 -0
  69. package/blueprints/supabase/checks/serve.sh +25 -0
  70. package/blueprints/supabase/checks/smoke.sh +11 -0
  71. package/blueprints/supabase/checks/spec.sh +12 -0
  72. package/blueprints/supabase/checks/supabase-cli.mjs +31 -0
  73. package/blueprints/supabase/checks/tests-pass.sh +8 -0
  74. package/blueprints/supabase/manifest.json +27 -0
  75. package/blueprints/supabase/plan.json +76 -0
  76. package/blueprints/supabase/security/checklist.md +10 -0
  77. package/blueprints/supabase/skills/auth/SKILL.md +35 -0
  78. package/blueprints/supabase/skills/data/SKILL.md +50 -0
  79. package/blueprints/supabase/skills/deploy/SKILL.md +44 -0
  80. package/blueprints/supabase/skills/handover/SKILL.md +39 -0
  81. package/blueprints/supabase/skills/prerequisites/SKILL.md +24 -0
  82. package/blueprints/supabase/skills/run-check/SKILL.md +24 -0
  83. package/blueprints/supabase/skills/scaffold/SKILL.md +62 -0
  84. package/blueprints/supabase/skills/security/SKILL.md +87 -0
  85. package/blueprints/supabase/skills/spec/SKILL.md +32 -0
  86. package/blueprints/supabase/skills/ui/SKILL.md +25 -0
  87. package/blueprints/supabase/spec/overview.md +35 -0
  88. package/blueprints/verify/skills/report/SKILL.md +1 -1
  89. package/common/agentAccounts.ts +5 -0
  90. package/common/agentEntries.ts +63 -0
  91. package/common/appActions.ts +62 -0
  92. package/common/blueprint/personLanguage.ts +26 -0
  93. package/common/blueprint/run.ts +8 -0
  94. package/common/blueprint/specRevisionPrompt.ts +3 -0
  95. package/common/blueprint/stepPrompt.ts +5 -1
  96. package/common/customAgents.ts +6 -0
  97. package/common/dirConfigSaveReport.ts +34 -0
  98. package/common/headerActions.ts +74 -0
  99. package/common/headerButtonEntries.ts +102 -0
  100. package/common/headerChips.ts +103 -0
  101. package/common/hiddenCharacters.ts +75 -0
  102. package/common/keyRecording.ts +27 -0
  103. package/common/keymap.ts +40 -0
  104. package/common/keymapPresets.ts +100 -0
  105. package/common/mdPreviewMessage.ts +19 -2
  106. package/common/previewCodeBlocks.ts +47 -0
  107. package/common/providerEntries.ts +99 -0
  108. package/common/remoteServer.ts +11 -0
  109. package/common/themeEntries.ts +79 -0
  110. package/common/whatsNew.ts +71 -0
  111. package/dist/assets/{abnfDiagram-O67JEVCF-B5xaEVpw.js → abnfDiagram-O67JEVCF-DfYKie1f.js} +1 -1
  112. package/dist/assets/architecture-7GRP2DOG-BuiSlwXB.js +1 -0
  113. package/dist/assets/{architectureDiagram-NJMV4G6O-qBYbXFGp.js → architectureDiagram-NJMV4G6O-vsLzjzQx.js} +1 -1
  114. package/dist/assets/{blockDiagram-BEXU5L5S-DQioBmlX.js → blockDiagram-BEXU5L5S-DssDT1TO.js} +1 -1
  115. package/dist/assets/{c4Diagram-YGBWAQC7-cyT63OdJ.js → c4Diagram-YGBWAQC7-P0CrW8QT.js} +1 -1
  116. package/dist/assets/channel-BrsbZpfz.js +1 -0
  117. package/dist/assets/{chunk-3FUC2YCW-GAIwUw3Q.js → chunk-3FUC2YCW-CvTm_ho-.js} +1 -1
  118. package/dist/assets/{chunk-5DYCD2WN-DMiZMIIB.js → chunk-5DYCD2WN-BVnziItI.js} +1 -1
  119. package/dist/assets/{chunk-742MDFTN-6zizfpHe.js → chunk-742MDFTN-CccjktcU.js} +1 -1
  120. package/dist/assets/{chunk-7INBJB4K-C2dZhhKc.js → chunk-7INBJB4K-Bf0Shv2-.js} +1 -1
  121. package/dist/assets/{chunk-7M6MHVWA-CqPNVLmK.js → chunk-7M6MHVWA-BtwnZqTt.js} +1 -1
  122. package/dist/assets/{chunk-7PRAP22T-BoBqX9Yo.js → chunk-7PRAP22T-59aFrPRm.js} +1 -1
  123. package/dist/assets/{chunk-GTNCS2PH-0N3yAwxf.js → chunk-GTNCS2PH-CT1kVgRW.js} +1 -1
  124. package/dist/assets/{chunk-GWA4HPMP-g7RRSruF.js → chunk-GWA4HPMP-C94KrS1K.js} +1 -1
  125. package/dist/assets/{chunk-MBY4JIJT-C3i2TuVa.js → chunk-MBY4JIJT-D9Nn3Npg.js} +1 -1
  126. package/dist/assets/{chunk-NETBCI7D-CH7vEM98.js → chunk-NETBCI7D-C-jbM8IP.js} +1 -1
  127. package/dist/assets/{chunk-O7XYJQB3-D1JLIcoV.js → chunk-O7XYJQB3-BQYzKLhh.js} +2 -2
  128. package/dist/assets/{chunk-UA2S7LBM-CpyAfTb8.js → chunk-UA2S7LBM-D7ovzGAr.js} +1 -1
  129. package/dist/assets/{chunk-WEXAMYUT-BGgzQY1u.js → chunk-WEXAMYUT-BmlxOjG3.js} +1 -1
  130. package/dist/assets/{chunk-Z7XXMR3K-CEMmXuqC.js → chunk-Z7XXMR3K-BSVBhxk7.js} +1 -1
  131. package/dist/assets/{chunk-ZIGJFQKS-g1Kaf-uj.js → chunk-ZIGJFQKS-x3IHLAdW.js} +1 -1
  132. package/dist/assets/{classDiagram-v2-NBCMYWYE-B54sxnuH.js → classDiagram-v2-NBCMYWYE-C6eb81l4.js} +1 -1
  133. package/dist/assets/{cynefin-OW5HDTMX-D796zEZZ.js → cynefin-OW5HDTMX-CM1PFDYP.js} +1 -1
  134. package/dist/assets/{cynefinDiagram-VND7K2PF-WHBkoilE.js → cynefinDiagram-VND7K2PF-CxhcOZTy.js} +1 -1
  135. package/dist/assets/{dagre-6A5THRUB-2gQ61gKB.js → dagre-6A5THRUB-D5O9prrg.js} +1 -1
  136. package/dist/assets/{diagram-22UHCM2B-yDp0rHwO.js → diagram-22UHCM2B-h7R_GutV.js} +1 -1
  137. package/dist/assets/{diagram-3UASUU5V-pnng1LdB.js → diagram-3UASUU5V-DPmc5Ai4.js} +1 -1
  138. package/dist/assets/{diagram-ATOU4E4O-D5Jkesja.js → diagram-ATOU4E4O-COI1PRUd.js} +1 -1
  139. package/dist/assets/{diagram-CDSNMT55-DkH9QxZ2.js → diagram-CDSNMT55-ehWBAAIO.js} +1 -1
  140. package/dist/assets/{diagram-MLGK6HIB-CDjGhKHq.js → diagram-MLGK6HIB-CXxA6YAe.js} +1 -1
  141. package/dist/assets/{diagram-MPIPVDR6-DAAr_c_l.js → diagram-MPIPVDR6-B6ANSQ5G.js} +1 -1
  142. package/dist/assets/{dist-C1cqjsKD.js → dist-B-lTjmBE.js} +1 -1
  143. package/dist/assets/{dist-CeWYOYWG.js → dist-B-mUmtv8.js} +1 -1
  144. package/dist/assets/{dist-CgC34u2d.js → dist-BnY3MUXM.js} +1 -1
  145. package/dist/assets/dist-Bx7SWRAa.js +199 -0
  146. package/dist/assets/{dist-BSQTHmad.js → dist-CPuq5mCf.js} +1 -1
  147. package/dist/assets/{dist-CWSTMA5n.js → dist-CQ6YfKVL.js} +1 -1
  148. package/dist/assets/{dist-C1s7S4KF.js → dist-CYrIpcYc.js} +1 -1
  149. package/dist/assets/{dist-C3s_Ij_z.js → dist-DHjjPgmc.js} +5 -5
  150. package/dist/assets/dist-DeIGO4lT.js +9 -0
  151. package/dist/assets/{dist-DbLWBcKv.js → dist-Dn4ta1sK.js} +1 -1
  152. package/dist/assets/{dist-B0sdAm9J.js → dist-Z8kzHarM.js} +1 -1
  153. package/dist/assets/{ebnfDiagram-ZINNZB2B-DKJbg6sg.js → ebnfDiagram-ZINNZB2B-ByHtvU2q.js} +1 -1
  154. package/dist/assets/{elk-276RUBZZ-CgwsWSX7.js → elk-276RUBZZ-BF00ltRi.js} +1 -1
  155. package/dist/assets/{erDiagram-OPXOYQCR-tCyjW7gV.js → erDiagram-OPXOYQCR-nW_7R9Lb.js} +1 -1
  156. package/dist/assets/eventmodeling-NTZA5JFV-C74VV9Z5.js +1 -0
  157. package/dist/assets/flowDiagram-KWPJA3E3-dKJG6rhg.js +1 -0
  158. package/dist/assets/{ganttDiagram-FUAMR5RP-C3CZ2yCC.js → ganttDiagram-FUAMR5RP-DaWxnbRm.js} +1 -1
  159. package/dist/assets/{gitGraph-4MIJSDKK-n2QHyQg8.js → gitGraph-4MIJSDKK-F7iDxaID.js} +1 -1
  160. package/dist/assets/{gitGraphDiagram-X574FWY7-Dtk0GaqE.js → gitGraphDiagram-X574FWY7-D9fTMDaK.js} +1 -1
  161. package/dist/assets/{index-CyHGyZiL.js → index-ffQ8fSEK.js} +294 -290
  162. package/dist/assets/index-xE6PHMoe.css +1 -0
  163. package/dist/assets/{info-A6RAGUB7-Dt0N-ywn.js → info-A6RAGUB7-BIvkp14y.js} +1 -1
  164. package/dist/assets/{infoDiagram-VRGFBTTK-DtjU_EzA.js → infoDiagram-VRGFBTTK-CF3pSadX.js} +1 -1
  165. package/dist/assets/{ishikawaDiagram-OU5B5YK6-xOGev5sa.js → ishikawaDiagram-OU5B5YK6-CnStwZlQ.js} +1 -1
  166. package/dist/assets/javascript-CppQSorr.js +1 -0
  167. package/dist/assets/{journeyDiagram-ZHPQQLJL-Disti4Wm.js → journeyDiagram-ZHPQQLJL-X6FpP2mU.js} +1 -1
  168. package/dist/assets/{kanban-definition-PNTS6WVX-CB5dROof.js → kanban-definition-PNTS6WVX-DGW7MkGx.js} +1 -1
  169. package/dist/assets/{line-D6MSXT7u.js → line-G8J9PhiP.js} +1 -1
  170. package/dist/assets/{marp-BASsSHAw.js → marp-BZuPB_AE.js} +61 -61
  171. package/dist/assets/{mermaid-parser.core-C29lKtTF.js → mermaid-parser.core-DlsoX-YC.js} +3 -3
  172. package/dist/assets/{mermaid.core-CWf9rghw.js → mermaid.core-DPC-93k6.js} +5 -5
  173. package/dist/assets/{mindmap-definition-NLK3R4M7-BfBZCMx_.js → mindmap-definition-NLK3R4M7-D-K5bSnd.js} +1 -1
  174. package/dist/assets/{packet-AYTQ26CC-BRpJC2Bl.js → packet-AYTQ26CC-CmDsNYF8.js} +1 -1
  175. package/dist/assets/{pegDiagram-GJSIUBJH-xck9NPib.js → pegDiagram-GJSIUBJH-3rBm1VWr.js} +1 -1
  176. package/dist/assets/{pie-WAS4IAKB-De38-oiC.js → pie-WAS4IAKB-DwWpCstp.js} +1 -1
  177. package/dist/assets/{pieDiagram-5QR66LMP-DI7hJbHF.js → pieDiagram-5QR66LMP-IolT-z46.js} +1 -1
  178. package/dist/assets/punycode.es6-BGceFjA3.js +1 -0
  179. package/dist/assets/{quadrantDiagram-O4NWA36T-Bxqm6tTX.js → quadrantDiagram-O4NWA36T-Biq9fhdq.js} +1 -1
  180. package/dist/assets/{radar-RG4KPBEZ-CS8aCQ7S.js → radar-RG4KPBEZ-DH0gPMqI.js} +1 -1
  181. package/dist/assets/{railroad-74A4TZTK-DmloOzOC.js → railroad-74A4TZTK-SDx9VkwS.js} +1 -1
  182. package/dist/assets/railroad-abnf-HS5TGJTU-CCSPvBtI.js +1 -0
  183. package/dist/assets/railroad-ebnf-LZEXJU2U-DTWbjX18.js +1 -0
  184. package/dist/assets/railroad-peg-WCYAUIDC--dNY7xHy.js +1 -0
  185. package/dist/assets/{railroadDiagram-XR7U4H2S-D8ruOuhL.js → railroadDiagram-XR7U4H2S-kQzwizD6.js} +1 -1
  186. package/dist/assets/{requirementDiagram-PLB6GJNP-zWCW1gnc.js → requirementDiagram-PLB6GJNP-B3spDS6j.js} +1 -1
  187. package/dist/assets/{sankeyDiagram-IPEJSGJF-D1ndcb5q.js → sankeyDiagram-IPEJSGJF-DkXWUHlz.js} +1 -1
  188. package/dist/assets/{sequenceDiagram-PO4LG4MO-zO_f3a5l.js → sequenceDiagram-PO4LG4MO-Dt80gEIt.js} +1 -1
  189. package/dist/assets/{stateDiagram-v2-GCMORJYK-DjHTP1Un.js → stateDiagram-v2-GCMORJYK-CG5xmXxG.js} +1 -1
  190. package/dist/assets/{swimlanes-2SLR337P-3TYxXfqC.js → swimlanes-2SLR337P-B3S4pJ9E.js} +1 -1
  191. package/dist/assets/swimlanesDiagram-TC7HE7FX-5BPq1RA-.js +8 -0
  192. package/dist/assets/{timeline-definition-EJHVYXUP-D2rwOmgp.js → timeline-definition-EJHVYXUP-DuKW3pM-.js} +1 -1
  193. package/dist/assets/{treeView-Q6P3EWNA-pJAjZYyX.js → treeView-Q6P3EWNA-CeeuwKmy.js} +1 -1
  194. package/dist/assets/{treemap-WGGIJYW6-BkBiLnEF.js → treemap-WGGIJYW6-CFK6V3lz.js} +1 -1
  195. package/dist/assets/{usecaseDiagram-POWQR4AR-DZQYTp5w.js → usecaseDiagram-POWQR4AR-CYbKz4kt.js} +1 -1
  196. package/dist/assets/{vennDiagram-UO4OBE2U-CLFk8_TU.js → vennDiagram-UO4OBE2U-De8JmnbH.js} +1 -1
  197. package/dist/assets/vitesse-dark-BZCL-v6S.js +1 -0
  198. package/dist/assets/vitesse-light-VbXTXTou.js +1 -0
  199. package/dist/assets/{wardley-WFR3VGLG-UZLdcCMt.js → wardley-WFR3VGLG-BUvlimn0.js} +1 -1
  200. package/dist/assets/{wardleyDiagram-VNRHLVJA-DOCJDdFu.js → wardleyDiagram-VNRHLVJA-e3jrcLF3.js} +1 -1
  201. package/dist/assets/{xychartDiagram-PMCCYNJV-DaRQvK2Z.js → xychartDiagram-PMCCYNJV-C1Q2Y2pt.js} +1 -1
  202. package/dist/index.html +3 -3
  203. package/docs/guide/en/v1.10.0.md +51 -0
  204. package/docs/guide/en/v1.11.0.md +44 -0
  205. package/docs/guide/en/v1.11.1.md +35 -0
  206. package/docs/guide/en/v1.12.0.md +46 -0
  207. package/docs/guide/en/v1.7.1.md +24 -0
  208. package/docs/guide/en/v1.7.2.md +27 -0
  209. package/docs/guide/en/v1.8.0.md +57 -0
  210. package/docs/guide/en/v1.9.0.md +31 -0
  211. package/docs/guide/en/v1.9.1.md +40 -0
  212. package/docs/guide/en/v1.9.2.md +27 -0
  213. package/docs/guide/en/v2.0.0.md +198 -0
  214. package/docs/guide/en/v2.1.0.md +159 -0
  215. package/docs/guide/en/v2.1.1.md +77 -0
  216. package/docs/guide/en/v2.2.0.md +150 -0
  217. package/docs/guide/en/v2.3.0.md +117 -0
  218. package/docs/guide/en/v2.4.0.md +135 -0
  219. package/docs/guide/en/v2.5.0.md +120 -0
  220. package/docs/guide/en/v2.5.1.md +108 -0
  221. package/docs/guide/en/v2.5.2.md +113 -0
  222. package/docs/guide/en/v2.5.3.md +158 -0
  223. package/docs/guide/en/v2.6.0.md +194 -0
  224. package/docs/guide/en/v2.7.0.md +228 -0
  225. package/docs/guide/en/v2.8.0.md +293 -0
  226. package/docs/guide/en/v2.9.0.md +207 -0
  227. package/docs/guide/en/v2.9.1.md +84 -0
  228. package/docs/guide/en/v3.0.0.md +121 -0
  229. package/docs/guide/en/v4.0.0.md +142 -0
  230. package/docs/guide/en/v4.1.0.md +114 -0
  231. package/docs/guide/en/v4.1.1.md +109 -0
  232. package/docs/guide/en/v4.10.0.md +99 -0
  233. package/docs/guide/en/v4.10.1.md +98 -0
  234. package/docs/guide/en/v4.11.0.md +177 -0
  235. package/docs/guide/en/v4.12.0.md +134 -0
  236. package/docs/guide/en/v4.13.0.md +106 -0
  237. package/docs/guide/en/v4.14.0.md +143 -0
  238. package/docs/guide/en/v4.15.0.md +131 -0
  239. package/docs/guide/en/v4.16.0.md +90 -0
  240. package/docs/guide/en/v4.16.1.md +110 -0
  241. package/docs/guide/en/v4.17.0.md +127 -0
  242. package/docs/guide/en/v4.18.0.md +107 -0
  243. package/docs/guide/en/v4.19.0.md +150 -0
  244. package/docs/guide/en/v4.2.0.md +201 -0
  245. package/docs/guide/en/v4.20.0.md +142 -0
  246. package/docs/guide/en/v4.21.0.md +159 -0
  247. package/docs/guide/en/v4.22.0.md +122 -0
  248. package/docs/guide/en/v4.23.0.md +113 -0
  249. package/docs/guide/en/v4.24.0.md +71 -0
  250. package/docs/guide/en/v4.25.0.md +113 -0
  251. package/docs/guide/en/v4.26.0.md +113 -0
  252. package/docs/guide/en/v4.27.0.md +125 -0
  253. package/docs/guide/en/v4.3.0.md +157 -0
  254. package/docs/guide/en/v4.3.1.md +73 -0
  255. package/docs/guide/en/v4.4.0.md +261 -0
  256. package/docs/guide/en/v4.5.0.md +241 -0
  257. package/docs/guide/en/v4.5.1.md +137 -0
  258. package/docs/guide/en/v4.6.0.md +175 -0
  259. package/docs/guide/en/v4.6.1.md +177 -0
  260. package/docs/guide/en/v4.7.0.md +179 -0
  261. package/docs/guide/en/v4.7.1.md +120 -0
  262. package/docs/guide/en/v4.7.2.md +195 -0
  263. package/docs/guide/en/v4.7.3.md +148 -0
  264. package/docs/guide/en/v4.7.4.md +123 -0
  265. package/docs/guide/en/v4.7.5.md +126 -0
  266. package/docs/guide/en/v4.7.6.md +81 -0
  267. package/docs/guide/en/v4.8.0.md +187 -0
  268. package/docs/guide/en/v4.8.1.md +149 -0
  269. package/docs/guide/en/v4.8.2.md +202 -0
  270. package/docs/guide/en/v4.8.3.md +97 -0
  271. package/docs/guide/en/v4.8.4.md +115 -0
  272. package/docs/guide/en/v4.8.5.md +163 -0
  273. package/docs/guide/en/v4.9.0.md +100 -0
  274. package/docs/guide/en/v5.0.0.md +106 -0
  275. package/docs/guide/en/v5.0.1.md +69 -0
  276. package/docs/guide/en/v5.1.0.md +110 -0
  277. package/docs/guide/en/v5.2.0.md +108 -0
  278. package/docs/guide/en/v5.3.0.md +107 -0
  279. package/docs/guide/en/v5.4.0.md +131 -0
  280. package/docs/guide/en/v5.5.0.md +71 -0
  281. package/docs/guide/en/v5.6.0.md +58 -0
  282. package/docs/guide/en/v5.7.0.md +60 -0
  283. package/docs/guide/en/v5.8.0.md +94 -0
  284. package/docs/guide/en/v6.0.0.md +44 -0
  285. package/docs/guide/en/v6.1.0.md +45 -0
  286. package/docs/guide/en/v6.2.0.md +65 -0
  287. package/docs/guide/en/v6.3.0.md +65 -0
  288. package/docs/guide/en/v6.4.0.md +85 -0
  289. package/docs/guide/en/v6.5.0.md +62 -0
  290. package/docs/guide/en/v6.6.0.md +133 -0
  291. package/docs/guide/en/v6.7.0.md +91 -0
  292. package/docs/guide/en/v6.8.0.md +235 -0
  293. package/docs/guide/en/v7.0.0.md +117 -0
  294. package/docs/guide/en/v7.1.0.md +106 -0
  295. package/docs/guide/en/v7.2.0.md +182 -0
  296. package/docs/guide/ja/v1.10.0.md +51 -0
  297. package/docs/guide/ja/v1.11.0.md +44 -0
  298. package/docs/guide/ja/v1.11.1.md +37 -0
  299. package/docs/guide/ja/v1.12.0.md +48 -0
  300. package/docs/guide/ja/v1.7.1.md +26 -0
  301. package/docs/guide/ja/v1.7.2.md +29 -0
  302. package/docs/guide/ja/v1.8.0.md +57 -0
  303. package/docs/guide/ja/v1.9.0.md +33 -0
  304. package/docs/guide/ja/v1.9.1.md +40 -0
  305. package/docs/guide/ja/v1.9.2.md +31 -0
  306. package/docs/guide/ja/v2.0.0.md +173 -0
  307. package/docs/guide/ja/v2.1.0.md +132 -0
  308. package/docs/guide/ja/v2.1.1.md +62 -0
  309. package/docs/guide/ja/v2.2.0.md +150 -0
  310. package/docs/guide/ja/v2.3.0.md +108 -0
  311. package/docs/guide/ja/v2.4.0.md +130 -0
  312. package/docs/guide/ja/v2.5.0.md +115 -0
  313. package/docs/guide/ja/v2.5.1.md +103 -0
  314. package/docs/guide/ja/v2.5.2.md +112 -0
  315. package/docs/guide/ja/v2.5.3.md +155 -0
  316. package/docs/guide/ja/v2.6.0.md +192 -0
  317. package/docs/guide/ja/v2.7.0.md +228 -0
  318. package/docs/guide/ja/v2.8.0.md +284 -0
  319. package/docs/guide/ja/v2.9.0.md +198 -0
  320. package/docs/guide/ja/v2.9.1.md +80 -0
  321. package/docs/guide/ja/v3.0.0.md +116 -0
  322. package/docs/guide/ja/v4.0.0.md +139 -0
  323. package/docs/guide/ja/v4.1.0.md +111 -0
  324. package/docs/guide/ja/v4.1.1.md +107 -0
  325. package/docs/guide/ja/v4.10.0.md +98 -0
  326. package/docs/guide/ja/v4.10.1.md +95 -0
  327. package/docs/guide/ja/v4.11.0.md +174 -0
  328. package/docs/guide/ja/v4.12.0.md +135 -0
  329. package/docs/guide/ja/v4.13.0.md +101 -0
  330. package/docs/guide/ja/v4.14.0.md +140 -0
  331. package/docs/guide/ja/v4.15.0.md +126 -0
  332. package/docs/guide/ja/v4.16.0.md +86 -0
  333. package/docs/guide/ja/v4.16.1.md +106 -0
  334. package/docs/guide/ja/v4.17.0.md +125 -0
  335. package/docs/guide/ja/v4.18.0.md +102 -0
  336. package/docs/guide/ja/v4.19.0.md +148 -0
  337. package/docs/guide/ja/v4.2.0.md +196 -0
  338. package/docs/guide/ja/v4.20.0.md +135 -0
  339. package/docs/guide/ja/v4.21.0.md +156 -0
  340. package/docs/guide/ja/v4.22.0.md +120 -0
  341. package/docs/guide/ja/v4.23.0.md +109 -0
  342. package/docs/guide/ja/v4.24.0.md +69 -0
  343. package/docs/guide/ja/v4.25.0.md +111 -0
  344. package/docs/guide/ja/v4.26.0.md +110 -0
  345. package/docs/guide/ja/v4.27.0.md +121 -0
  346. package/docs/guide/ja/v4.3.0.md +157 -0
  347. package/docs/guide/ja/v4.3.1.md +72 -0
  348. package/docs/guide/ja/v4.4.0.md +256 -0
  349. package/docs/guide/ja/v4.5.0.md +235 -0
  350. package/docs/guide/ja/v4.5.1.md +140 -0
  351. package/docs/guide/ja/v4.6.0.md +172 -0
  352. package/docs/guide/ja/v4.6.1.md +174 -0
  353. package/docs/guide/ja/v4.7.0.md +174 -0
  354. package/docs/guide/ja/v4.7.1.md +120 -0
  355. package/docs/guide/ja/v4.7.2.md +196 -0
  356. package/docs/guide/ja/v4.7.3.md +143 -0
  357. package/docs/guide/ja/v4.7.4.md +119 -0
  358. package/docs/guide/ja/v4.7.5.md +124 -0
  359. package/docs/guide/ja/v4.7.6.md +78 -0
  360. package/docs/guide/ja/v4.8.0.md +190 -0
  361. package/docs/guide/ja/v4.8.1.md +147 -0
  362. package/docs/guide/ja/v4.8.2.md +198 -0
  363. package/docs/guide/ja/v4.8.3.md +95 -0
  364. package/docs/guide/ja/v4.8.4.md +116 -0
  365. package/docs/guide/ja/v4.8.5.md +162 -0
  366. package/docs/guide/ja/v4.9.0.md +99 -0
  367. package/docs/guide/ja/v5.0.0.md +104 -0
  368. package/docs/guide/ja/v5.0.1.md +69 -0
  369. package/docs/guide/ja/v5.1.0.md +106 -0
  370. package/docs/guide/ja/v5.2.0.md +106 -0
  371. package/docs/guide/ja/v5.3.0.md +103 -0
  372. package/docs/guide/ja/v5.4.0.md +125 -0
  373. package/docs/guide/ja/v5.5.0.md +69 -0
  374. package/docs/guide/ja/v5.6.0.md +57 -0
  375. package/docs/guide/ja/v5.7.0.md +60 -0
  376. package/docs/guide/ja/v5.8.0.md +75 -0
  377. package/docs/guide/ja/v6.0.0.md +44 -0
  378. package/docs/guide/ja/v6.1.0.md +45 -0
  379. package/docs/guide/ja/v6.2.0.md +65 -0
  380. package/docs/guide/ja/v6.3.0.md +42 -0
  381. package/docs/guide/ja/v6.4.0.md +72 -0
  382. package/docs/guide/ja/v6.5.0.md +49 -0
  383. package/docs/guide/ja/v6.6.0.md +101 -0
  384. package/docs/guide/ja/v6.7.0.md +88 -0
  385. package/docs/guide/ja/v6.8.0.md +234 -0
  386. package/docs/guide/ja/v7.0.0.md +114 -0
  387. package/docs/guide/ja/v7.1.0.md +100 -0
  388. package/docs/guide/ja/v7.2.0.md +174 -0
  389. package/package.json +6 -2
  390. package/server/backends/scheduler-boot.ts +27 -24
  391. package/server/backends/scheduler.ts +81 -3
  392. package/server/blueprint/executor.ts +25 -1
  393. package/server/blueprint/routes.ts +20 -3
  394. package/server/blueprint/wiring.ts +1 -0
  395. package/server/config/agent-entry-routes.ts +123 -0
  396. package/server/config/app-config.ts +30 -15
  397. package/server/config/config-reload.ts +60 -0
  398. package/server/config/config-routes.ts +105 -23
  399. package/server/config/config-schema.ts +23 -8
  400. package/server/config/dir-config.ts +7 -3
  401. package/server/config/header-button-routes.ts +56 -0
  402. package/server/config/header-chip-routes.ts +56 -0
  403. package/server/config/header-config.ts +5 -6
  404. package/server/config/keymap-binding-route.ts +52 -0
  405. package/server/config/system-task-settings.ts +21 -0
  406. package/server/config/theme-entry-routes.ts +60 -0
  407. package/server/files/codeHighlight.ts +151 -0
  408. package/server/files/files-browse.ts +89 -14
  409. package/server/files/files-tree-routes.ts +102 -0
  410. package/server/files/mdPreviewReporter.ts +64 -19
  411. package/server/files/pathContainment.ts +10 -0
  412. package/server/files/previewCodeFence.ts +20 -0
  413. package/server/files/renderedDoc.ts +54 -0
  414. package/server/files/tree-ops.ts +143 -0
  415. package/server/index.ts +1 -1
  416. package/server/infra/on-listening.ts +6 -3
  417. package/server/routes/app-routes.ts +10 -1
  418. package/server/routes/dir-routes.ts +7 -0
  419. package/server/session/reap-schedule.ts +42 -14
  420. package/server/skills/mulmoterminal-config/SKILL.md +43 -18
  421. package/server/skills/mulmoterminal-dirs/SKILL.md +3 -1
  422. package/server/skills/mulmoterminal-header/SKILL.md +35 -8
  423. package/server/skills/mulmoterminal-keys/SKILL.md +29 -13
  424. package/server/skills/mulmoterminal-model/SKILL.md +12 -7
  425. package/server/skills/mulmoterminal-theme/SKILL.md +4 -2
  426. package/server/whatsNew/guidePage.ts +69 -0
  427. package/server/whatsNew/routes.ts +63 -0
  428. package/server/whatsNew/state.ts +43 -0
  429. package/dist/assets/architecture-7GRP2DOG-BxUYapfa.js +0 -1
  430. package/dist/assets/channel-DLqe-NE3.js +0 -1
  431. package/dist/assets/dist-DHSqnwKI.js +0 -9
  432. package/dist/assets/eventmodeling-NTZA5JFV-Bj6FbHv7.js +0 -1
  433. package/dist/assets/flowDiagram-KWPJA3E3-C3CneunC.js +0 -1
  434. package/dist/assets/index-BkbFlsCM.css +0 -1
  435. package/dist/assets/railroad-abnf-HS5TGJTU-lxjdNIS_.js +0 -1
  436. package/dist/assets/railroad-ebnf-LZEXJU2U-BvnO_c-5.js +0 -1
  437. package/dist/assets/railroad-peg-WCYAUIDC-Bco3H4cO.js +0 -1
  438. package/dist/assets/swimlanesDiagram-TC7HE7FX-D0Ef5eY1.js +0 -8
package/README.md CHANGED
@@ -308,7 +308,8 @@ mulmoterminal
308
308
 
309
309
  **Stopping it.** `Ctrl+C` in the terminal that started it — or, if you can no longer find that
310
310
  terminal, **Settings → Quit MulmoTerminal** in the browser, or **`npx mulmoterminal@latest stop`**
311
- from any terminal (installed globally, just `mulmoterminal stop`). All three run the same shutdown:
311
+ from any terminal (installed globally, just `mulmoterminal stop`; it stops every server running on
312
+ this machine, and `--port <port>` stops only that one). All three run the same shutdown:
312
313
  with `tmux` installed the agent sessions survive and come back under **Settings → Sessions that
313
314
  survived a restart**; without it they end with the server.
314
315
 
@@ -634,6 +635,17 @@ up for Codex, loaded by description.
634
635
 
635
636
  ---
636
637
 
638
+ ## Running the server on another machine
639
+
640
+ The server can live on a Linux box, a VPS or a container while your browser stays on your laptop:
641
+ start it there over SSH (`npx mulmoterminal`), and reach it through an SSH tunnel
642
+ (`ssh -N -L 34567:127.0.0.1:34567 you@server`, then open `http://localhost:34567`). Started over
643
+ SSH, the launcher opens no browser on the server and prints that command. It still listens on
644
+ loopback only — nothing is opened to the network. Logging the agents in on the server, Docker, and
645
+ the few actions that act on the server's machine: [Run the server on another machine](https://receptron.github.io/mulmoterminal/guide/en/remote.html).
646
+
647
+ ---
648
+
637
649
  ## Session persistence (tmux)
638
650
 
639
651
  If **`tmux` is installed**, MulmoTerminal runs each Claude session and launcher inside
@@ -777,7 +789,7 @@ The Settings modal (the gear button) persists per-user UI choices to `~/.mulmote
777
789
 
778
790
  ![The Settings modal — a sidebar of grouped sections beside the one being edited](https://raw.githubusercontent.com/receptron/mulmoterminal/main/docs/guide/images/settings-tabs.png)
779
791
 
780
- *Open it from the gear button in the toolbar. The **sidebar** groups the sections — Appearance, Projects, Header & launch, Input, Models & servers, Notifications, Integrations, Sessions, Help — and one is on screen at a time; on a phone the sidebar becomes a picker above the section. The interface is available in **English, 日本語, 简体中文, 繁體中文 and 한국어**: it follows your browser's language unless you pick one under **Language** (per browser, like the theme). Translated so far: this modal, the grid's status words, and every button's hover tip and screen-reader label; the rest of the app's words are still English. Under the title, a **Version** row shows what is running: the version from the shipped `package.json`, plus a `commit <sha>` chip on a git checkout — there the version is only whatever was last released, so the commit is what identifies the build. When something newer exists, the row is followed by the header badge's update notice, command included. Pick a **theme**, set the **terminal font size**, **font** and **scroll speed** (and whether sending **returns to the latest output**), set a custom **attention sound**, list the repos the cross-repo **PRs & Issues** view should aggregate, add **launch commands** for grid cells, register your own **MCP servers**, and turn on the switches for what this app writes on your behalf — **issue work comments**, the **PR clone footer**, the **closing summary**, the **decision digest**, the **dev worklog** — no need to hand-edit the config file. Four settings stay with their skill because a form would be the wrong tool for them (`keymap`, `themes`, `providers`, `buttons`/`chips`); Settings shows what each is doing now and launches that skill. Note that **theme, font size, scroll speed and the return-to-latest switch are stored per browser** (they're display preferences, so a phone and a desktop keep their own); the rest live in `~/.mulmoterminal/config.json` and are shared by every client.*
792
+ *Open it from the gear button in the toolbar. The **sidebar** groups the sections — Appearance, Projects, Header & launch, Input, Models & servers, Notifications, Integrations, Sessions, Help — and one is on screen at a time; on a phone the sidebar becomes a picker above the section. The interface is available in **English, 日本語, 简体中文, 繁體中文 and 한국어**: it follows your browser's language unless you pick one under **Language** (per browser, like the theme). Translated so far: this modal, the grid's status words, and every button's hover tip and screen-reader label; the rest of the app's words are still English. Under the title, a **Version** row shows what is running: the version from the shipped `package.json`, plus a `commit <sha>` chip on a git checkout — there the version is only whatever was last released, so the commit is what identifies the build. When something newer exists, the row is followed by the header badge's update notice, command included. Pick a **theme**, set the **terminal font size**, **font** and **scroll speed** (and whether sending **returns to the latest output**), set a custom **attention sound**, list the repos the cross-repo **PRs & Issues** view should aggregate, add **launch commands** for grid cells, register your own **MCP servers**, and turn on the switches for what this app writes on your behalf — **issue work comments**, the **PR clone footer**, the **closing summary**, the **decision digest**, the **dev worklog** — no need to hand-edit the config file. Three settings stay with their skill because a form would be the wrong tool for them (`keymap`, `themes`, `buttons`/`chips`); Settings shows what each is doing now and launches that skill. Backends (`providers`), custom agents and accounts are added and removed in **Models and backends**. Note that **theme, font size, scroll speed and the return-to-latest switch are stored per browser** (they're display preferences, so a phone and a desktop keep their own); the rest live in `~/.mulmoterminal/config.json` and are shared by every client.*
781
793
 
782
794
  | Field | Meaning |
783
795
  | ------------ | ------- |
@@ -799,7 +811,7 @@ The Settings modal (the gear button) persists per-user UI choices to `~/.mulmote
799
811
  | `pushKinds` | Which moments push: `"finished"` (a turn ended, ✅) and/or `"waiting"` (the agent stopped to ask — a permission prompt or a question, ❓, **once per prompt**). Omit to keep both; `[]` for none. A kind added in a later version stays off until you tick it. |
800
812
  | `worklogEnabled` | `true` to run the built-in **dev worklog** batch (see below). Off by default (each run spawns an LLM session, so it costs tokens). Editable in Settings → **Sessions and background tasks**. |
801
813
  | `worklogIntervalHours` | Worklog cadence in hours (default `6`, clamped to `1`–`168`). A stepper in the same Settings section covers the range. |
802
- | `feedRefreshEnabled` | `false` stops the hourly **collection/feed refresh** (one scheduled task per root — the workspace and every saved project directory). **On by default**; only an explicit `false` turns it off, so an existing config keeps the behaviour it has. Takes effect at the next server start — the scheduler registers once at boot. Feeds still refresh on demand. Checkbox in Settings → **Sessions**. |
814
+ | `feedRefreshEnabled` | `false` stops the hourly **collection/feed refresh** (one scheduled task per root — the workspace and every saved project directory). **On by default**; only an explicit `false` turns it off, so an existing config keeps the behaviour it has. Saved from Settings (or any `POST /api/config`), it applies at once — the running scheduler rebuilds its built-in tasks; a hand-edit applies after **Settings → Reload config file** (or a restart). Feeds still refresh on demand. Checkbox in Settings → **Sessions**. |
803
815
  | `calendarSyncEnabled` | `false` stops the hourly **Google Calendar sync** (pulls changed events into collections that declare a calendar). Same default and the same restart rule as `feedRefreshEnabled`; calendar collections still sync on demand. Checkbox in the same Settings section. With both off and `worklogEnabled` off, no **built-in** task is registered. Your own tasks in `config/scheduler/tasks.json` are unaffected — these switches do not touch them, and an enabled one still registers and still runs the tick loop. Only when there is no enabled user task either does the scheduler stop writing its state file and run logs. |
804
816
  | `terminalSubmit` | Which bytes Claude reads as **submit** vs **newline**: `"cr"` (default — Enter submits, Shift+Enter makes a newline) or `"esc-cr"` (for a Claude Code rebound the other way). Applies to the keyboard **and** the phone remote-view submit, for **Claude sessions only** (shell/codex keep plain Enter). See the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#terminal-submit). Settings → **Terminal keys** offers both, worded as behaviour. |
805
817
  | `copyOnSelect` | `true` puts a **mouse selection on the clipboard the moment it settles**, with no key pressed (the PuTTY / iTerm2 behaviour). **Off by default** — it changes the clipboard when you may only have meant to highlight something. There is a checkbox in Settings → **Terminal keys**, applied at once; a hand edit of the file needs a **server restart, then a tab reload** (the server reads this file once at startup, and the browser reads the value from it on load). Composes with the `copy` keymap action rather than replacing it. Over plain `http://` the browser gives a page no clipboard access, so a fallback asks xterm to copy instead; see the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#copy-on-select). |
@@ -811,6 +823,7 @@ The Settings modal (the gear button) persists per-user UI choices to `~/.mulmote
811
823
  | `autoDirIcon` | Whether a project that sets no `icon` shows the favicon its repository already ships (`public/favicon.svg`, `apple-touch-icon.png`, a web manifest — first hit wins, ordered by how the image survives at 14px). **On by default**; Settings → **Directory appearance** has the switch. A single project opts out with `"icon": false` in its own `.mulmoterminal.json`, which this does not override. A key that was written and got it wrong shows nothing rather than falling back — a broken setting has to look broken. `true` / `false` only. |
812
824
  | `cockpitLines` | `{ summary, prompt, response }` — how many lines each **cockpit-roster** row shows before it clamps (default `2` / `2` / `3`, each clamped to `1`–`20`). Raising them trades how many sessions fit on screen for reading a long one in place. Three steppers in Settings → **Waiting rows**. |
813
825
  | `showLoadAverage` | Whether the grid header draws this machine's **load average** beside the 5h / 7d usage windows, as a percentage of its cores (`load 334%` = a 20-core machine with 66.8 runnable processes). **On by default**; amber at 100%, red at 200%, hover for the raw 1 / 5 / 15-minute figures. Settings → **Grid header read-outs** has the switch. A host that keeps no load average (Windows) shows nothing rather than `0%`, whatever this says. `true` / `false` only. |
826
+ | `remoteServer` | **Experimental.** The server runs on another machine than your browser (an SSH tunnel — [guide](https://receptron.github.io/mulmoterminal/guide/en/remote.html)): the actions that would act on the server's screen (the file dialog, the file manager, the OS app) are hidden or say why, and a dropped file is always uploaded. **Off unless `true`**; set it in the server's config. |
814
827
  | `fontFamily` | The **terminal font** every session renders in — a CSS font-family stack, e.g. `"'Cica', 'MS Gothic', monospace"`. Set it in Settings → **Terminal font**, applied at once; editing the file instead needs a **restart** (this config is read once at startup). Unset uses the built-in stack (JetBrains Mono / Fira Code / Menlo / Consolas, then CJK faces for Japanese, Korean and Chinese). Unlike the per-browser font **size**, this is one value for the whole host — it names fonts, and which fonts exist is a property of the machine. A directory can override it. See the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#font-family). |
815
828
 
816
829
  Every MulmoTerminal on the machine shares this one file, so an older build could save over a key a
@@ -829,9 +842,14 @@ are items in the **path menu** (click the directory path on the terminal header)
829
842
  set** with your list (it is not merged on top), so listing your own — even a **shorter** one — is how
830
843
  you drop, reorder, or swap them.
831
844
  A button has an `id`, `label`, and a `run` of `"shell"` (run a command), `"input"` (send text to the
832
- agent), `"open"`, or `"action"` (act on the cell itself — `action: "restart"` ends the agent and
833
- starts it again in the same cell on the same conversation, which is how an MCP / config / plugin
834
- change takes effect; it costs a resume and asks nothing first). An `open` button targets one of `url` / `reveal` (OS file manager) / `files`
845
+ agent), `"open"`, or `"action"` (act on the cell itself, named like the `keymap` actions so one operation is
846
+ a button, a key and a command-palette entry — `action: "terminal-restart"` ends the agent and starts
847
+ it again in the same cell on the same conversation, which is how an MCP / config / plugin change
848
+ takes effect; it costs a resume and asks nothing first. `"terminal-new-here"` opens the launch panel
849
+ on the cell's directory, and `"pane-files"` / `"pane-prompts"` / … / `"terminal-timeline"` /
850
+ `"terminal-talk"` / `"terminal-park"` / `"terminal-copy-code"` / `"terminal-insert-path"` / `"terminal-reveal"` /
851
+ `"terminal-voice"` / `"terminal-diff"` / `"terminal-note"` do what the cell's menus and buttons do, and `"screen-*"` / `"settings-open"` /
852
+ `"sound-toggle"` / `"view-toggle"` / `"order-*"` / `"page-next"` / `"page-prev"` do what the toolbar and the page tabs do; the old name `"restart"` still works). An `open` button targets one of `url` / `reveal` (OS file manager) / `files`
835
853
  (in-app explorer) / `view` (a built-in overlay) / `terminal` (a dir → a new cell running `$SHELL`,
836
854
  opened next to the current one) / `pr: true` (open the current branch's PR — the button is hidden when
837
855
  there's no open PR) / `pickFile: true` (OS file dialog → insert the path).
@@ -891,7 +909,7 @@ channel is connected (its Google sign-in supplies the notification auth). With R
891
909
  disconnected, or with no device registered, the toggle is a no-op.
892
910
 
893
911
  **Dev worklog (cross-clone).** Set `worklogEnabled: true` in
894
- `~/.mulmoterminal/config.json` (and **restart** — the scheduler reads its tasks at boot)
912
+ `~/.mulmoterminal/config.json` (or switch it on in Settings, which applies at once; after a hand-edit use Settings → **Reload config file**)
895
913
  to register a built-in scheduled task. Every `worklogIntervalHours` (default 6) it spawns
896
914
  a Claude session that reviews the work you did across **all your saved working dirs**
897
915
  (`cwdPresets`) since it last ran, and writes it up as a short manager-style report.
package/bin/cli-args.d.ts CHANGED
@@ -25,4 +25,5 @@ export interface RunningInstance {
25
25
  port: number | null;
26
26
  }
27
27
  export declare function stopCommandFor(pkgDir: string): string;
28
+ export declare function stopCommandForThis(stopCommand: string, port: number, instances: readonly { port: number | null }[]): string;
28
29
  export declare function runningInstancesPrompt(instances: readonly RunningInstance[], stopCommand?: string): string;
package/bin/cli-args.js CHANGED
@@ -366,6 +366,11 @@ export function saysYes(answer) {
366
366
  // they are: npx unpacks into `<cache>/_npx/<hash>/node_modules/mulmoterminal`.
367
367
  const NPX_INSTALL = /[/\\]_npx[/\\]/;
368
368
 
369
+ /** The stop command the ready banner shows for the server on `port`: the plain one while it is the
370
+ * only server, and one narrowed to its port once another is running, which the plain one would stop
371
+ * too (#2683). `instances` is the registry, this server included. */
372
+ export const stopCommandForThis = (stopCommand, port, instances) => (instances.some((i) => i.port !== port) ? `${stopCommand} --port ${port}` : stopCommand);
373
+
369
374
  export const stopCommandFor = (pkgDir) => (NPX_INSTALL.test(String(pkgDir)) ? "npx mulmoterminal@latest stop" : "mulmoterminal stop");
370
375
 
371
376
  export function runningInstancesPrompt(instances, stopCommand = "mulmoterminal stop") {
@@ -14,6 +14,7 @@ import { dirname, join, resolve } from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { computeUpdateNotice, isUpdateCheckDisabled } from "./update-check.js";
16
16
  import { detectNpxCacheDir, npxCacheHintLines } from "./npx-cache-hint.js";
17
+ import { configuredRemoteServer, sshTunnelHintLines } from "./ssh-hint.js";
17
18
  import { planAfterServerExit } from "./server-supervision.js";
18
19
  import { waitUntilReady } from "./wait-ready.js";
19
20
  import {
@@ -29,6 +30,7 @@ import {
29
30
  secondInstancePrompt,
30
31
  runningInstancesPrompt,
31
32
  stopCommandFor,
33
+ stopCommandForThis,
32
34
  SECOND_INSTANCE_NOTE,
33
35
  nodeMeetsMinimum,
34
36
  unsupportedNodeMessage,
@@ -109,6 +111,17 @@ function readConfiguredDefaultAgent() {
109
111
  }
110
112
  }
111
113
 
114
+ // The experimental `remoteServer` (#2669): the browser is on another machine, so the launcher opens
115
+ // none here even when started without SSH (a service, a remote desktop). A missing or unreadable
116
+ // file is simply "not set".
117
+ function readRemoteServer() {
118
+ try {
119
+ return configuredRemoteServer(JSON.parse(readFileSync(CONFIG_FILE, "utf8")));
120
+ } catch {
121
+ return false;
122
+ }
123
+ }
124
+
112
125
  // The command for `agent`, honouring its `<AGENT>_BIN` override.
113
126
  //
114
127
  // The override is the point rather than a detail: the gate used to look for the literal word
@@ -478,11 +491,16 @@ async function choosePort(requested, explicit) {
478
491
  //
479
492
  // `url` is where the BROWSER goes and `note` is what launchTarget wants said about that choice —
480
493
  // two different facts since #1889, and the note is null whenever the URL already covers it.
481
- function announceReady(url, note, noOpen) {
482
- printReadyBanner(url, STOP_COMMAND);
494
+ function announceReady(url, note, noOpen, sshHint, port) {
495
+ printReadyBanner(url, stopCommandForThis(STOP_COMMAND, port, liveInstances()));
483
496
  // Either the address a widened bind serves other machines on, or why the browser was NOT sent
484
497
  // to `localhost`. Null whenever the URL above already said everything.
485
498
  if (note) log(note);
499
+ // Over SSH the browser is on the other machine (see ssh-hint.js): say how to reach it, open nothing.
500
+ if (sshHint) {
501
+ sshHint.forEach((line) => log(line));
502
+ return;
503
+ }
486
504
  if (noOpen) return;
487
505
  try {
488
506
  // The command is a hardcoded literal; url is built by browserUrl from a numeric port, so it
@@ -549,7 +567,9 @@ function runServer({ port, probedAddress, localhostIsUnambiguous, noOpen, launch
549
567
  readyStarted = true;
550
568
  const localhostIsOurs = localhostIsUnambiguous && serverSaysLocalhostIsOurs !== false;
551
569
  const { url, note } = launchTarget(reachHost, port, localhostIsOurs);
552
- cancelReady = waitUntilReady(port, () => announceReady(url, note, noOpen), { host: reachHost });
570
+ cancelReady = waitUntilReady(port, () => announceReady(url, note, noOpen, sshTunnelHintLines(process.env, port, readRemoteServer()), port), {
571
+ host: reachHost,
572
+ });
553
573
  };
554
574
  // The same message answers a second question now: whether this lifetime ever bound at all,
555
575
  // which is what a restart is allowed to depend on. Recorded BEFORE the address is looked at —
@@ -0,0 +1,3 @@
1
+ export declare function inSshSession(env: Record<string, string | undefined>): boolean;
2
+ export declare const configuredRemoteServer: (config: unknown) => boolean;
3
+ export declare function sshTunnelHintLines(env: Record<string, string | undefined>, port: number, remoteServer?: boolean): string[] | null;
@@ -0,0 +1,32 @@
1
+ // A launcher started over SSH is on a machine whose screen nobody is looking at: the browser that
2
+ // should open this is on the other end of the connection. So it does not try to open one here — on
3
+ // a remote Mac that would put a window on a display no one sees — and it says how to reach the
4
+ // server from there instead: an SSH port forward, the route the FAQ recommends. Nothing here reads
5
+ // the process; the caller passes its environment.
6
+
7
+ /** Whether this process runs inside an SSH login. sshd sets both variables; either is enough. */
8
+ export function inSshSession(env) {
9
+ return Boolean(env.SSH_CONNECTION || env.SSH_CLIENT);
10
+ }
11
+
12
+ /** The experimental `remoteServer` out of a parsed `~/.mulmoterminal/config.json`: true only for a
13
+ * literal `true`, like the server's own reading of it (common/remoteServer.ts). */
14
+ export const configuredRemoteServer = (config) => config !== null && typeof config === "object" && config.remoteServer === true;
15
+
16
+ /**
17
+ * The lines that tell someone on another machine how to open this server, or null when the browser
18
+ * is expected here: not an SSH login, and `remoteServer` not set. The host is left for them to fill
19
+ * in: SSH_CONNECTION names the address THIS side answered on, which behind NAT or a container is not
20
+ * one their machine can reach.
21
+ */
22
+ export function sshTunnelHintLines(env, port, remoteServer = false) {
23
+ const ssh = inSshSession(env);
24
+ if (!ssh && !remoteServer) return null;
25
+ const user = env.USER || env.LOGNAME || "<user>";
26
+ const why = ssh ? "Started over SSH" : "remoteServer is set in config.json";
27
+ return [
28
+ `${why}, so no browser is opened here. On your own machine, run:`,
29
+ ` ssh -N -L ${port}:127.0.0.1:${port} ${user}@<this-host>`,
30
+ `then open http://localhost:${port} there.`,
31
+ ];
32
+ }
package/bin/stop.d.ts CHANGED
@@ -16,10 +16,12 @@ export interface StopResult {
16
16
  stubborn: StubbornInstance[];
17
17
  unconfirmed: InstanceEntry[];
18
18
  }
19
- export type StopArgs = { help: true } | { error: string } | { force: boolean };
19
+ export type StopArgs = { help: true } | { error: string } | { force: boolean; port: number | null };
20
20
 
21
21
  export declare const STOP_USAGE: string;
22
22
  export declare function parseStopArgs(args: readonly string[]): StopArgs;
23
+ export declare function selectByPort(instances: readonly InstanceEntry[], port: number | null): InstanceEntry[];
24
+ export declare function noServerOnPortReport(port: number, instances: readonly InstanceEntry[]): string[];
23
25
  export interface ConfirmDeps {
24
26
  owners?: (port: number) => Promise<number[] | null>;
25
27
  }
package/bin/stop.js CHANGED
@@ -14,7 +14,7 @@
14
14
  // Windows user find or kill anything.
15
15
  import { dirname } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
17
- import { stopCommandFor } from "./cli-args.js";
17
+ import { parsePortArg, stopCommandFor } from "./cli-args.js";
18
18
  import { isProcessAlive, liveInstances } from "./instances.js";
19
19
  import { portOwners } from "./port-owner.js";
20
20
 
@@ -140,10 +140,11 @@ export function stopReport({ stopped, stubborn, unconfirmed }, platform = proces
140
140
  export const stopExitCode = ({ stubborn, unconfirmed }) => (stubborn.length + unconfirmed.length ? 1 : 0);
141
141
 
142
142
  export const STOP_USAGE = [
143
- "Usage: mulmoterminal stop [--force]",
143
+ "Usage: mulmoterminal stop [--port <port>] [--force]",
144
144
  "",
145
145
  "Stops every running MulmoTerminal server on this machine, from any terminal.",
146
146
  "",
147
+ " --port Stop only the server on that port (the one at http://localhost:<port>).",
147
148
  " --force Also stop a registered server that is no longer answering. Off by default:",
148
149
  " a server that crashed leaves its entry behind, and that pid may since have",
149
150
  " been given to an unrelated program.",
@@ -153,9 +154,26 @@ export const STOP_USAGE = [
153
154
  * argument handling is testable — and so `stop --help` can never be read as "stop everything". */
154
155
  export function parseStopArgs(args) {
155
156
  if (args.includes("--help") || args.includes("-h")) return { help: true };
156
- const unknown = args.filter((a) => a !== "--force");
157
+ const portAt = args.indexOf("--port");
158
+ const portArgs = portAt === -1 ? [] : [portAt, portAt + 1];
159
+ const unknown = args.filter((a, i) => a !== "--force" && !portArgs.includes(i));
157
160
  if (unknown.length) return { error: `Unknown argument for stop: ${unknown.join(" ")}` };
158
- return { force: args.includes("--force") };
161
+ const force = args.includes("--force");
162
+ if (portAt === -1) return { force, port: null };
163
+ // The launcher's own reading of --port, so the two accept the same values. The environment's
164
+ // PORT is not consulted: a `stop` narrowed by a variable nobody typed here would surprise.
165
+ const parsed = parsePortArg(args, {}, null);
166
+ return "error" in parsed ? { error: parsed.error } : { force, port: parsed.port };
167
+ }
168
+
169
+ /** The servers a `stop` is about: all of them, or only the one on `port` (#2683). */
170
+ export const selectByPort = (instances, port) => (port === null ? instances : instances.filter((i) => i.port === port));
171
+
172
+ /** What `stop --port` says when nothing is registered there — and what IS running, since a wrong
173
+ * port is the likeliest reason. */
174
+ export function noServerOnPortReport(port, instances) {
175
+ const running = instances.map(describeInstance);
176
+ return [`No MulmoTerminal server is running on port ${port}.`, running.length ? `Running: ${running.join(", ")}` : "MulmoTerminal is not running."];
159
177
  }
160
178
 
161
179
  export async function runStop(args = []) {
@@ -169,7 +187,16 @@ export async function runStop(args = []) {
169
187
  console.error(STOP_USAGE);
170
188
  process.exit(2);
171
189
  }
172
- const result = await stopInstances(liveInstances(), { force: parsed.force });
173
- stopReport(result).forEach((line) => console.log(line));
190
+ const all = liveInstances();
191
+ const targets = selectByPort(all, parsed.port);
192
+ // Nothing on that port is the state asked for, like "nothing was running": not a failure.
193
+ if (parsed.port !== null && !targets.length) {
194
+ noServerOnPortReport(parsed.port, all).forEach((line) => console.log(line));
195
+ process.exit(0);
196
+ }
197
+ const result = await stopInstances(targets, { force: parsed.force });
198
+ // A retry must name the same server, or `--force` would stop every one.
199
+ const stopCommand = parsed.port === null ? selfStopCommand() : `${selfStopCommand()} --port ${parsed.port}`;
200
+ stopReport(result, process.platform, stopCommand).forEach((line) => console.log(line));
174
201
  process.exit(stopExitCode(result));
175
202
  }
@@ -18,7 +18,7 @@ Otherwise add to `FAQ.md` in this folder (create it if there is none). **Add aft
18
18
  never edit or reorder what is there** — the check compares the beginning of the file with what it held
19
19
  before. For each question: the question as a `##` heading, word for word; the answer; and where it is
20
20
  written (the document, and the place named as in the answer: 第4条第2項, or a section's heading in 「」 —
21
- never chaff's `h1.3`). For a question the documents do not answer, say so — that is worth keeping
21
+ in an English FAQ, in "…" — never chaff's `h1.3`). For a question the documents do not answer, say so — that is worth keeping
22
22
  too.
23
23
 
24
24
  ## Done when
@@ -15,12 +15,23 @@ RENDER_LIMIT_SECONDS=60
15
15
  work=$(mktemp -d)
16
16
  "$chrome" --headless=new --disable-gpu --no-first-run --user-data-dir="$work/profile" --virtual-time-budget=15000 --dump-dom "$url" >"$work/dom.html" 2>/dev/null &
17
17
  pid=$!
18
- trap 'kill "$pid" 2>/dev/null || true; rm -rf "$work"' EXIT
18
+ # Chrome can still be writing its profile when it is stopped, and under set -e a failed rm in this trap would turn a page
19
+ # that rendered into a failed check; the scratch folder is left behind instead.
20
+ trap 'kill "$pid" 2>/dev/null || true; rm -rf "$work" 2>/dev/null || true' EXIT
19
21
  waited=0
20
22
  until grep -q "</html>" "$work/dom.html" 2>/dev/null || [ "$waited" -ge "$RENDER_LIMIT_SECONDS" ] || ! kill -0 "$pid" 2>/dev/null; do
21
23
  sleep 1
22
24
  waited=$((waited + 1))
23
25
  done
26
+ # A Chrome that crashed before printing the page says nothing about the app: like no Chrome, it cannot judge.
27
+ if ! grep -q "</html>" "$work/dom.html" 2>/dev/null && ! kill -0 "$pid" 2>/dev/null; then
28
+ status=0
29
+ wait "$pid" || status=$?
30
+ if [ "$status" -ne 0 ]; then
31
+ echo "Chrome ($chrome) exited with status $status before printing the page; could not confirm that $url renders" >&2
32
+ exit 0
33
+ fi
34
+ fi
24
35
  node -e 'let s="";process.stdin.on("data",(c)=>s+=c).on("end",()=>{
25
36
  const body = (s.match(/<body[^>]*>([\s\S]*)<\/body>/i) || [, ""])[1];
26
37
  const text = body.replace(/<(script|style|noscript)[\s\S]*?<\/\1>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
@@ -17,7 +17,7 @@ The answers are in `.blueprint/answers.json`; the prompt names both pack directo
17
17
  4. Something the answers do not settle is not guessed: write "未定" and add a line to
18
18
  `.blueprint/open-questions.md` saying what is needed and why it matters.
19
19
  5. Append both packs' `security/` checklists under a heading "必ず詰める点", unchanged.
20
- 6. Write in the language the user answered in. Nothing is built in this step; the next step stops for the user
20
+ 6. Write in the language the build's prompt says the user reads (without one, the language the user answered in). Nothing is built in this step; the next step stops for the user
21
21
  to read this spec.
22
22
 
23
23
  Done when the check passes: `.blueprint/spec.md` exists and no `{{` placeholder is left in it.
@@ -4,4 +4,4 @@
4
4
  if [ -n "${CHAFF_BIN:-}" ]; then
5
5
  exec sh -c "$CHAFF_BIN \"\$@\"" chaff "$@"
6
6
  fi
7
- exec npx -y chaffjs@0.14 "$@"
7
+ exec npx -y chaffjs@0.15 "$@"
@@ -11,13 +11,16 @@ const DIR = ".blueprint/chaff-feedback";
11
11
  const INDEX = join(DIR, "index.json");
12
12
  const DRAFTS_SECTION = [["chaff への報告の下書き", "Drafts for chaff"]];
13
13
 
14
- const argsFor = (entry) =>
14
+ const argsFor = (entry, extra) =>
15
15
  entry.kind === "wrong"
16
- ? ["feedback", entry.file, "--rule", entry.rule, "--line", String(entry.line), "--experimental"]
17
- : ["feedback", entry.file, "--missed", "--line", String(entry.line), "--experimental"];
16
+ ? ["feedback", entry.file, "--rule", entry.rule, "--line", String(entry.line), "--experimental", ...extra]
17
+ : ["feedback", entry.file, "--missed", "--line", String(entry.line), "--experimental", ...extra];
18
18
 
19
- /** Drafts one report per case and writes the index. Stops, leaving nothing behind, when a draft fails. */
20
- export const draftReports = (cases) => {
19
+ /**
20
+ * Drafts one report per case and writes the index. Stops, leaving nothing behind, when a draft fails. `extra` adds
21
+ * arguments (such as `--genre`), so a draft reproduces the finding under the same measure the check used.
22
+ */
23
+ export const draftReports = (cases, extra = []) => {
21
24
  if (existsSync(DRAFT)) fail(`${DRAFT} already exists in this folder: it may be the person's own draft, so move it away first`);
22
25
  const supported = runChaff(["--help"]).stdout.includes("chaff feedback");
23
26
  mkdirSync(DIR, { recursive: true });
@@ -30,7 +33,7 @@ export const draftReports = (cases) => {
30
33
  };
31
34
  const drafted = supported
32
35
  ? cases.map((entry) => {
33
- const run = runChaff(argsFor(entry));
36
+ const run = runChaff(argsFor(entry, extra));
34
37
  if (run.code !== 0 || !existsSync(DRAFT)) undo(`chaff feedback did not draft ${entry.id}:\n${run.stderr || run.stdout}`);
35
38
  const draft = join(DIR, `${entry.id}.md`);
36
39
  renameSync(DRAFT, draft);
@@ -24,13 +24,17 @@ function japaneseName(label, own, above, article) {
24
24
  return `${above}第${label ?? levels.at(-1)}号`;
25
25
  }
26
26
 
27
+ // A heading is quoted the way its own language quotes: 「」 when it holds kana or kanji, "…" otherwise.
28
+ const JAPANESE = /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/u;
29
+ const quoted = (heading) => (JAPANESE.test(heading) ? `「${heading}」` : `"${heading}"`);
30
+
27
31
  // A part (an article, a section) is named by its label, or by its quoted heading when it has none (a Markdown
28
32
  // section); anything below it (an item, a paragraph) adds its own label to the name of what holds it.
29
33
  function nameOf(node, own, above, article) {
30
34
  const label = text(node?.attrs?.label);
31
35
  if (PARTS.has(node?.kind)) {
32
36
  const heading = text(node?.attrs?.heading);
33
- return label ?? (heading === null ? own : `「${heading}」`);
37
+ return label ?? (heading === null ? own : quoted(heading));
34
38
  }
35
39
  if (above === null) return label ?? own;
36
40
  return article?.japanese && own.startsWith(`${article.address}.`) ? japaneseName(label, own, above, article) : `${above} ${label ?? own}`;
@@ -46,7 +50,7 @@ const namesIn = (node, above, article) => {
46
50
  return [...here, ...(Array.isArray(node?.children) ? node.children.flatMap((child) => namesIn(child, name, within)) : [])];
47
51
  };
48
52
 
49
- /** Every address in a chaff tree, with the name a person reads for it: 第4条第2項, Section 3.2 (a) (i), 「用意するもの」. */
53
+ /** Every address in a chaff tree, with the name a person reads for it: 第4条第2項, Section 3.2 (a) (i), 「用意するもの」, "Getting started". */
50
54
  export const placeNamesIn = (tree) => new Map(namesIn(tree, null, null));
51
55
 
52
56
  /** The tree of `file` (`chaff tree --format json`); null when chaff cannot read it. */
@@ -12,7 +12,7 @@ repository, `.git/info/exclude`) until the person has approved the models.
12
12
  1. `.blueprint/` already exists (the build created it). Everything this build records goes there.
13
13
  2. Run chaff once through the base pack's wrapper, so its first download happens now and not in the
14
14
  middle of a later step: `sh <base pack>/checks/chaff.sh rules --json > /dev/null`. The wrapper runs the
15
- pinned chaff (`npx -y chaffjs@0.14`) unless `CHAFF_BIN` names another. If it fails, show the person the
15
+ pinned chaff (`npx -y chaffjs@0.15`) unless `CHAFF_BIN` names another. If it fails, show the person the
16
16
  error and stop — do not work around it.
17
17
  3. If the folder is a git repository, add `.blueprint/` to `.git/info/exclude`. That keeps the build's
18
18
  working files out of commits without touching `.gitignore`, which belongs to the person.
@@ -15,12 +15,23 @@ RENDER_LIMIT_SECONDS=60
15
15
  work=$(mktemp -d)
16
16
  "$chrome" --headless=new --disable-gpu --no-first-run --user-data-dir="$work/profile" --virtual-time-budget=15000 --dump-dom "$url" >"$work/dom.html" 2>/dev/null &
17
17
  pid=$!
18
- trap 'kill "$pid" 2>/dev/null || true; rm -rf "$work"' EXIT
18
+ # Chrome can still be writing its profile when it is stopped, and under set -e a failed rm in this trap would turn a page
19
+ # that rendered into a failed check; the scratch folder is left behind instead.
20
+ trap 'kill "$pid" 2>/dev/null || true; rm -rf "$work" 2>/dev/null || true' EXIT
19
21
  waited=0
20
22
  until grep -q "</html>" "$work/dom.html" 2>/dev/null || [ "$waited" -ge "$RENDER_LIMIT_SECONDS" ] || ! kill -0 "$pid" 2>/dev/null; do
21
23
  sleep 1
22
24
  waited=$((waited + 1))
23
25
  done
26
+ # A Chrome that crashed before printing the page says nothing about the app: like no Chrome, it cannot judge.
27
+ if ! grep -q "</html>" "$work/dom.html" 2>/dev/null && ! kill -0 "$pid" 2>/dev/null; then
28
+ status=0
29
+ wait "$pid" || status=$?
30
+ if [ "$status" -ne 0 ]; then
31
+ echo "Chrome ($chrome) exited with status $status before printing the page; could not confirm that $url renders" >&2
32
+ exit 0
33
+ fi
34
+ fi
24
35
  node -e 'let s="";process.stdin.on("data",(c)=>s+=c).on("end",()=>{
25
36
  const body = (s.match(/<body[^>]*>([\s\S]*)<\/body>/i) || [, ""])[1];
26
37
  const text = body.replace(/<(script|style|noscript)[\s\S]*?<\/\1>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
@@ -15,7 +15,7 @@ directories.
15
15
  implication (the collections from `appPurpose`, for example), write your proposal and mark it `(提案)`.
16
16
  3. Append both packs' `security/` checklists under a heading "必ず詰める点", unchanged — they are what the later
17
17
  steps are held to.
18
- 4. Write in the language the user answered in.
18
+ 4. Write in the language the build's prompt says the user reads (without one, the language the user answered in).
19
19
  5. Nothing else is built in this step. The next step stops for the user to read this spec before anything is
20
20
  created in their cloud account.
21
21
 
@@ -8,19 +8,21 @@ file=.blueprint/actions.json
8
8
  # The record itself first: a build that got here without the spec check is held to it all the same.
9
9
  node --no-warnings "$(dirname "$0")/decisions.mjs"
10
10
  [ -s "$file" ] || { echo "no $file: the source has no actions or ingests to handle"; exit 0; }
11
- # A key for a model call lives in .env (.dev.vars on a Worker); the folder becomes a repository whenever its person adds
12
- # git, and neither must go with it.
13
- for secrets in .env .dev.vars; do
14
- pattern="/?$(printf '%s' "$secrets" | sed 's/\./\\./g')(\*)?"
11
+ # A key for a model call lives in .env (.dev.vars on a Worker, supabase/functions/.env for Supabase's functions); the
12
+ # folder becomes a repository whenever its person adds git, and none of them must go with it. A line naming the file
13
+ # alone (.env) ignores it at any depth, as git reads it.
14
+ for secrets in .env .dev.vars supabase/functions/.env; do
15
+ escape() { printf '%s' "$1" | sed 's/\./\\./g'; }
16
+ pattern="/?$(escape "$secrets")(\*)?|$(escape "$(basename "$secrets")")(\*)?"
15
17
  if [ -f "$secrets" ] && ! grep -qxE "$pattern" .gitignore 2>/dev/null; then
16
18
  echo "$secrets exists and .gitignore does not ignore it; add a line $secrets to .gitignore before anything secret goes in" >&2
17
19
  exit 1
18
20
  fi
19
21
  done
20
22
  case "$base" in
21
- local|cloudflare) tests="test/actions.test.ts" ;;
23
+ local|cloudflare|supabase) tests="test/actions.test.ts" ;;
22
24
  firebase) tests="test/blueprint/actions.spec.ts" ;;
23
- *) echo "usage: actions.sh local|firebase|cloudflare" >&2; exit 2 ;;
25
+ *) echo "usage: actions.sh local|firebase|cloudflare|supabase" >&2; exit 2 ;;
24
26
  esac
25
27
  # The titles of the tests the file really declares — parsed, so a name in a comment or a string is not a test. Only read
26
28
  # when something is to be built.
@@ -47,6 +49,6 @@ process.stdout.write(String(entries.filter((entry) => entry.decision === "featur
47
49
  ' "$file" "$tests" "$titles")
48
50
  [ "$features" -gt 0 ] || exit 0
49
51
  case "$base" in
50
- local|cloudflare) sh "$BLUEPRINT_BASE/checks/tests-pass.sh" actions ;;
52
+ local|cloudflare|supabase) sh "$BLUEPRINT_BASE/checks/tests-pass.sh" actions ;;
51
53
  firebase) sh "$BLUEPRINT_BASE/checks/emulator-test.sh" actions ;;
52
54
  esac
@@ -23,12 +23,20 @@ export const isFileField = (spec) => spec.type === "image" || spec.type === "fil
23
23
  export const hasValue = (value) => value !== undefined && value !== null && value !== "";
24
24
  export const labelOf = (schema, record) => `${schema.primaryKey}=${JSON.stringify(record[schema.primaryKey])}`;
25
25
 
26
+ const DATE_TYPES = new Set(["date", "datetime"]);
27
+
26
28
  /**
27
29
  * Whether a stored value is the source's. `booleanAs` says how the store holds a boolean — 0/1 in SQLite, itself in
28
- * Firestore; a number is compared as a number, anything else as its text.
30
+ * Firestore; a number is compared as a number, anything else as its text. `datesAsInstants` is for a store that keeps
31
+ * dates as dates and writes them back in its own format (Postgres: 2026-01-02 as 2026-01-02T00:00:00Z): a date is then
32
+ * compared as the moment it names.
29
33
  */
30
- export function sameValue(type, sourceValue, stored, booleanAs) {
34
+ export function sameValue(type, sourceValue, stored, booleanAs, datesAsInstants = false) {
31
35
  if (type === "number") return Number(sourceValue) === Number(stored);
36
+ if (datesAsInstants && DATE_TYPES.has(type)) {
37
+ const [source, back] = [Date.parse(String(sourceValue)), Date.parse(String(stored))];
38
+ if (!Number.isNaN(source) && !Number.isNaN(back)) return source === back;
39
+ }
32
40
  const want = type === "boolean" ? booleanAs(sourceValue === true) : sourceValue;
33
41
  return String(want) === String(stored);
34
42
  }
@@ -47,18 +55,20 @@ export const pointedAtFiles = () => [
47
55
  ),
48
56
  ];
49
57
 
50
- // A SQL store (SQLite, D1): the table is the collection's slug with `-` as `_`, a column is the field's key, and a
51
- // boolean is held as 0/1 (spec/conversion.md).
58
+ // A SQL store: the table is the collection's slug with `-` as `_`, and a column is the field's key (spec/conversion.md).
59
+ // How a value is held differs by store: SQLite and D1 keep a boolean as 0/1 and a date as its text; Postgres keeps both
60
+ // as themselves.
52
61
  export const tableOf = (slug) => slug.replaceAll("-", "_");
53
- const asColumn = (flag) => (flag ? 1 : 0);
62
+ export const SQLITE_STORE = { booleanAs: (flag) => (flag ? 1 : 0), datesAsInstants: false };
63
+ export const POSTGRES_STORE = { booleanAs: (flag) => flag, datesAsInstants: true };
54
64
 
55
65
  // `at` is where the value sits: the collection, the record and its label, and how a missing file is told.
56
66
  function columnProblems(at, key, spec, row) {
57
- const { slug, record, label, fileMissing } = at;
67
+ const { slug, record, label, fileMissing, store } = at;
58
68
  const value = record[key];
59
69
  if (!hasValue(value)) return [];
60
70
  if (!(key in row)) return [`${slug}: table ${tableOf(slug)} has no column ${key}`];
61
- const problems = sameValue(spec.type, value, row[key], asColumn)
71
+ const problems = sameValue(spec.type, value, row[key], store.booleanAs, store.datesAsInstants)
62
72
  ? []
63
73
  : [`${slug} ${label}: ${key} is ${JSON.stringify(row[key])}, the source had ${JSON.stringify(value)}`];
64
74
  const missing = isFileField(spec) && existsSync(`${SOURCE}/files/${value}`) && fileMissing(value);
@@ -69,9 +79,9 @@ function columnProblems(at, key, spec, row) {
69
79
  * The rows of a collection's table held against its records: as many rows as records, and every stored field of every
70
80
  * record reads back equal. `rows` is every row of the table, or null when there is no such table. `fileMissing(path)`
71
81
  * says where a file the source holds is missing from ("not in data/files/"), or false when it is there; a store whose
72
- * files are checked elsewhere leaves it out.
82
+ * files are checked elsewhere leaves it out. `store` is how the store holds its values (SQLITE_STORE, POSTGRES_STORE).
73
83
  */
74
- export function tableProblems(slug, rows, fileMissing = () => false) {
84
+ export function tableProblems(slug, rows, fileMissing = () => false, store = SQLITE_STORE) {
75
85
  const table = tableOf(slug);
76
86
  if (rows === null) return [`${slug}: there is no table ${table}`];
77
87
  const schema = schemaOf(slug);
@@ -83,7 +93,7 @@ export function tableProblems(slug, rows, fileMissing = () => false) {
83
93
  const perRecord = records.flatMap((record) => {
84
94
  const row = byKey.get(String(record[primary]));
85
95
  if (!row) return [`${slug}: no row for ${primary} = ${JSON.stringify(record[primary])}`];
86
- return plainFields(schema).flatMap(([key, spec]) => columnProblems({ slug, record, label: labelOf(schema, record), fileMissing }, key, spec, row));
96
+ return plainFields(schema).flatMap(([key, spec]) => columnProblems({ slug, record, label: labelOf(schema, record), fileMissing, store }, key, spec, row));
87
97
  });
88
98
  return [...counted, ...perRecord];
89
99
  }
@@ -0,0 +1,35 @@
1
+ #!/bin/sh
2
+ # The records the build copied are in Postgres, and the files they point at in Storage — read back through the Supabase
3
+ # CLI, not through the app. `local`: the local database is reset to the migrations and the seed, `yarn import-source
4
+ # --local` runs twice, then the stack is read. `linked`: production is read through the person's supabase login (the
5
+ # step itself did the import, after its approval). A build that copied only the shape has nothing to move.
6
+ set -eu
7
+ target="$1"
8
+ here="$(dirname "$0")"
9
+ source=.blueprint/source/source.json
10
+ [ -s "$source" ] || { echo "missing $source; the build was started without a collection" >&2; exit 1; }
11
+ if [ "$(node -e 'process.stdout.write(String(JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).records === true))' "$source")" != true ]; then
12
+ echo "only the shape was copied; there are no records to move"
13
+ exit 0
14
+ fi
15
+ grep -q '"import-source"' package.json || { echo "package.json has no import-source script" >&2; exit 1; }
16
+ case "$target" in
17
+ local)
18
+ . "$BLUEPRINT_BASE/checks/local-stack.sh"
19
+ seeded=$(mktemp)
20
+ trap 'rm -f "$seeded"' EXIT INT TERM
21
+ node --no-warnings "$here/supabase-verify.mjs" --local --save-seeded "$seeded"
22
+ yarn -s import-source --local >/dev/null || { echo "yarn import-source --local failed" >&2; yarn -s import-source --local >&2 || true; exit 1; }
23
+ # Twice: moving the records again must leave the same rows, not a second copy of each.
24
+ yarn -s import-source --local >/dev/null || { echo "yarn import-source failed the second time over the same database" >&2; exit 1; }
25
+ node --no-warnings "$here/supabase-verify.mjs" --local --seeded "$seeded"
26
+ yarn test >/dev/null || { echo "yarn test fails" >&2; yarn test >&2 || true; exit 1; }
27
+ ;;
28
+ linked)
29
+ node --no-warnings "$here/supabase-verify.mjs" --linked
30
+ ;;
31
+ *)
32
+ echo "usage: import-supabase.sh local|linked" >&2
33
+ exit 2
34
+ ;;
35
+ esac