@invisibleloop/pulse 0.1.21

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 (344) hide show
  1. package/.claude/commands/build-page.md +59 -0
  2. package/.claude/commands/new-doc-page.md +45 -0
  3. package/.claude/commands/verify.md +52 -0
  4. package/.claude/pulse-checklist.md +111 -0
  5. package/.claude/settings.local.json +102 -0
  6. package/.github/workflows/ci.yml +22 -0
  7. package/.github/workflows/publish.yml +41 -0
  8. package/.pulse/load-reports/home/1773432711417.json +22 -0
  9. package/CLAUDE.md +383 -0
  10. package/README.md +95 -0
  11. package/docs/.claude/pulse-checklist.md +111 -0
  12. package/docs/public/.pulse-ui-version +1 -0
  13. package/docs/public/dist/accessibility.boot-5DVTARJU.js +115 -0
  14. package/docs/public/dist/actions.boot-P66HKQEM.js +164 -0
  15. package/docs/public/dist/auth.boot-IMAJAUPH.js +140 -0
  16. package/docs/public/dist/caching.boot-DVR6KDE7.js +53 -0
  17. package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +11 -0
  18. package/docs/public/dist/components--alert.boot-GCEXOZAC.js +6 -0
  19. package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +6 -0
  20. package/docs/public/dist/components--avatar.boot-PSW24EVA.js +5 -0
  21. package/docs/public/dist/components--badge.boot-TYDY2RMK.js +7 -0
  22. package/docs/public/dist/components--banner.boot-EI5PZSZK.js +7 -0
  23. package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +34 -0
  24. package/docs/public/dist/components--button.boot-J54BQM2E.js +23 -0
  25. package/docs/public/dist/components--card.boot-PZGNDIB6.js +138 -0
  26. package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +12 -0
  27. package/docs/public/dist/components--charts.boot-2EOYQWKL.js +108 -0
  28. package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +54 -0
  29. package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +9 -0
  30. package/docs/public/dist/components--code-window.boot-2GR2DV33.js +20 -0
  31. package/docs/public/dist/components--container.boot-7LOOGK2K.js +5 -0
  32. package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +11 -0
  33. package/docs/public/dist/components--divider.boot-3NI2C3QG.js +6 -0
  34. package/docs/public/dist/components--empty.boot-YX2UR3PV.js +7 -0
  35. package/docs/public/dist/components--feature.boot-MUD7NSUO.js +13 -0
  36. package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +19 -0
  37. package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +52 -0
  38. package/docs/public/dist/components--footer.boot-EYUK5FRG.js +14 -0
  39. package/docs/public/dist/components--grid.boot-URDQVDDR.js +59 -0
  40. package/docs/public/dist/components--heading.boot-BPQKU43E.js +44 -0
  41. package/docs/public/dist/components--hero.boot-4RAPRGAB.js +17 -0
  42. package/docs/public/dist/components--icons.boot-ZITNU5JP.js +68 -0
  43. package/docs/public/dist/components--image.boot-XEEGHQZF.js +19 -0
  44. package/docs/public/dist/components--input.boot-SGASZG5K.js +7 -0
  45. package/docs/public/dist/components--list.boot-W3XC5MHD.js +55 -0
  46. package/docs/public/dist/components--media.boot-5VFIETZO.js +13 -0
  47. package/docs/public/dist/components--modal.boot-RZUYXBN2.js +47 -0
  48. package/docs/public/dist/components--nav.boot-ODBOHU7O.js +33 -0
  49. package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +21 -0
  50. package/docs/public/dist/components--progress.boot-GHAGYZOK.js +30 -0
  51. package/docs/public/dist/components--prose.boot-QANJL6JI.js +67 -0
  52. package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +22 -0
  53. package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +75 -0
  54. package/docs/public/dist/components--rating.boot-QBAN6DEL.js +38 -0
  55. package/docs/public/dist/components--search.boot-PXH5O5AG.js +17 -0
  56. package/docs/public/dist/components--section.boot-AQGIYHWW.js +12 -0
  57. package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +33 -0
  58. package/docs/public/dist/components--select.boot-47X5RHOC.js +10 -0
  59. package/docs/public/dist/components--slider.boot-PSRRX7XL.js +47 -0
  60. package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +22 -0
  61. package/docs/public/dist/components--stack.boot-DI4NJXBF.js +9 -0
  62. package/docs/public/dist/components--stat.boot-QMFUWBQT.js +9 -0
  63. package/docs/public/dist/components--stepper.boot-34PP2NEV.js +22 -0
  64. package/docs/public/dist/components--table.boot-FCQGSFIQ.js +11 -0
  65. package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +11 -0
  66. package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +4 -0
  67. package/docs/public/dist/components--timeline.boot-26LN52P2.js +95 -0
  68. package/docs/public/dist/components--toggle.boot-IQQEI76S.js +29 -0
  69. package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +9 -0
  70. package/docs/public/dist/components.boot-SE6PQ4P7.js +103 -0
  71. package/docs/public/dist/config.boot-DTRRWUE6.js +126 -0
  72. package/docs/public/dist/constraints.boot-DUHDZBMC.js +71 -0
  73. package/docs/public/dist/deploy.boot-SLAD3NI2.js +163 -0
  74. package/docs/public/dist/docs-8e3d4b5c.css +1 -0
  75. package/docs/public/dist/extending.boot-UA3CN243.js +159 -0
  76. package/docs/public/dist/faq.boot-6EQAWLQR.js +43 -0
  77. package/docs/public/dist/getting-started.boot-TDKIFL5U.js +86 -0
  78. package/docs/public/dist/guard.boot-AUHAWTG4.js +80 -0
  79. package/docs/public/dist/home.boot-BVQXRH32.js +383 -0
  80. package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +104 -0
  81. package/docs/public/dist/hydration.boot-JRM6IPJL.js +78 -0
  82. package/docs/public/dist/images.boot-M6ZVKTZS.js +80 -0
  83. package/docs/public/dist/manifest.json +94 -0
  84. package/docs/public/dist/meta.boot-7NXGPHR4.js +79 -0
  85. package/docs/public/dist/mutations.boot-F6F43UDX.js +79 -0
  86. package/docs/public/dist/navigation.boot-AOXWS3ZF.js +57 -0
  87. package/docs/public/dist/performance.boot-C3UPCOBK.js +98 -0
  88. package/docs/public/dist/persist.boot-WT32PQOQ.js +61 -0
  89. package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +63 -0
  90. package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +31 -0
  91. package/docs/public/dist/pulse-ui-81a85c03.css +1 -0
  92. package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +104 -0
  93. package/docs/public/dist/routing.boot-FNX5FDGH.js +70 -0
  94. package/docs/public/dist/runtime-B73WLANC.js +1 -0
  95. package/docs/public/dist/runtime-KO4BHUQ3.js +49 -0
  96. package/docs/public/dist/runtime-L2HNXIHW.js +59 -0
  97. package/docs/public/dist/runtime-QFURDKA2.js +5 -0
  98. package/docs/public/dist/runtime-UVPXO4IR.js +375 -0
  99. package/docs/public/dist/runtime-VMJA3Z4N.js +10 -0
  100. package/docs/public/dist/runtime-ZJ4FXT5O.js +11 -0
  101. package/docs/public/dist/server-api.boot-K7X3LCFB.js +219 -0
  102. package/docs/public/dist/server-data.boot-Y7HQYC4R.js +157 -0
  103. package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +26 -0
  104. package/docs/public/dist/spec.boot-2WU7ZHCV.js +159 -0
  105. package/docs/public/dist/state.boot-B24GUE3R.js +73 -0
  106. package/docs/public/dist/store.boot-TLIB4XHH.js +150 -0
  107. package/docs/public/dist/streaming.boot-W2DZSMW4.js +80 -0
  108. package/docs/public/dist/stripe.boot-QN3C2GEL.js +164 -0
  109. package/docs/public/dist/supabase.boot-BG4XXLZE.js +303 -0
  110. package/docs/public/dist/testing.boot-6U4WKMTE.js +130 -0
  111. package/docs/public/dist/validation.boot-PQHYGW5B.js +100 -0
  112. package/docs/public/docs.css +2020 -0
  113. package/docs/public/menu.js +83 -0
  114. package/docs/public/pulse-ui.css +2739 -0
  115. package/docs/public/pulse-ui.js +236 -0
  116. package/docs/server.js +192 -0
  117. package/docs/src/lib/component-page.js +47 -0
  118. package/docs/src/lib/highlight.js +255 -0
  119. package/docs/src/lib/layout.js +131 -0
  120. package/docs/src/lib/metrics-store.js +6 -0
  121. package/docs/src/lib/nav.js +159 -0
  122. package/docs/src/lib/stats.js +81 -0
  123. package/docs/src/pages/accessibility.js +157 -0
  124. package/docs/src/pages/actions.js +191 -0
  125. package/docs/src/pages/auth.js +177 -0
  126. package/docs/src/pages/caching.js +95 -0
  127. package/docs/src/pages/components/accordion.js +48 -0
  128. package/docs/src/pages/components/alert.js +35 -0
  129. package/docs/src/pages/components/app-badge.js +41 -0
  130. package/docs/src/pages/components/avatar.js +35 -0
  131. package/docs/src/pages/components/badge.js +36 -0
  132. package/docs/src/pages/components/banner.js +45 -0
  133. package/docs/src/pages/components/breadcrumbs.js +94 -0
  134. package/docs/src/pages/components/button.js +84 -0
  135. package/docs/src/pages/components/card.js +225 -0
  136. package/docs/src/pages/components/carousel.js +72 -0
  137. package/docs/src/pages/components/charts.js +278 -0
  138. package/docs/src/pages/components/checkbox.js +129 -0
  139. package/docs/src/pages/components/cluster.js +47 -0
  140. package/docs/src/pages/components/code-window.js +57 -0
  141. package/docs/src/pages/components/container.js +40 -0
  142. package/docs/src/pages/components/cta.js +53 -0
  143. package/docs/src/pages/components/divider.js +37 -0
  144. package/docs/src/pages/components/empty.js +36 -0
  145. package/docs/src/pages/components/feature.js +60 -0
  146. package/docs/src/pages/components/fieldset.js +65 -0
  147. package/docs/src/pages/components/fileupload.js +127 -0
  148. package/docs/src/pages/components/footer.js +58 -0
  149. package/docs/src/pages/components/grid.js +165 -0
  150. package/docs/src/pages/components/heading.js +107 -0
  151. package/docs/src/pages/components/hero.js +65 -0
  152. package/docs/src/pages/components/icons.js +285 -0
  153. package/docs/src/pages/components/image.js +71 -0
  154. package/docs/src/pages/components/input.js +51 -0
  155. package/docs/src/pages/components/list.js +112 -0
  156. package/docs/src/pages/components/media.js +51 -0
  157. package/docs/src/pages/components/modal.js +111 -0
  158. package/docs/src/pages/components/nav.js +86 -0
  159. package/docs/src/pages/components/pricing.js +68 -0
  160. package/docs/src/pages/components/progress.js +102 -0
  161. package/docs/src/pages/components/prose.js +111 -0
  162. package/docs/src/pages/components/pullquote.js +71 -0
  163. package/docs/src/pages/components/radio.js +194 -0
  164. package/docs/src/pages/components/rating.js +106 -0
  165. package/docs/src/pages/components/search.js +61 -0
  166. package/docs/src/pages/components/section.js +59 -0
  167. package/docs/src/pages/components/segmented.js +121 -0
  168. package/docs/src/pages/components/select.js +45 -0
  169. package/docs/src/pages/components/slider.js +114 -0
  170. package/docs/src/pages/components/spinner.js +73 -0
  171. package/docs/src/pages/components/stack.js +48 -0
  172. package/docs/src/pages/components/stat.js +55 -0
  173. package/docs/src/pages/components/stepper.js +66 -0
  174. package/docs/src/pages/components/table.js +45 -0
  175. package/docs/src/pages/components/testimonial.js +49 -0
  176. package/docs/src/pages/components/textarea.js +31 -0
  177. package/docs/src/pages/components/timeline.js +227 -0
  178. package/docs/src/pages/components/toggle.js +84 -0
  179. package/docs/src/pages/components/tooltip.js +48 -0
  180. package/docs/src/pages/components.js +204 -0
  181. package/docs/src/pages/config.js +193 -0
  182. package/docs/src/pages/constraints.js +99 -0
  183. package/docs/src/pages/deploy.js +233 -0
  184. package/docs/src/pages/extending.js +198 -0
  185. package/docs/src/pages/faq.js +96 -0
  186. package/docs/src/pages/getting-started.js +106 -0
  187. package/docs/src/pages/guard.js +121 -0
  188. package/docs/src/pages/home.js +401 -0
  189. package/docs/src/pages/how-it-works.js +183 -0
  190. package/docs/src/pages/hydration.js +98 -0
  191. package/docs/src/pages/images.js +121 -0
  192. package/docs/src/pages/meta.js +120 -0
  193. package/docs/src/pages/mutations.js +106 -0
  194. package/docs/src/pages/navigation.js +85 -0
  195. package/docs/src/pages/performance.js +157 -0
  196. package/docs/src/pages/persist.js +88 -0
  197. package/docs/src/pages/project-structure.js +90 -0
  198. package/docs/src/pages/prompt-examples.js +186 -0
  199. package/docs/src/pages/raw-responses.js +124 -0
  200. package/docs/src/pages/routing.js +99 -0
  201. package/docs/src/pages/server-api.js +281 -0
  202. package/docs/src/pages/server-data.js +185 -0
  203. package/docs/src/pages/slash-commands.js +55 -0
  204. package/docs/src/pages/spec.js +207 -0
  205. package/docs/src/pages/state.js +101 -0
  206. package/docs/src/pages/store.js +181 -0
  207. package/docs/src/pages/streaming.js +108 -0
  208. package/docs/src/pages/stripe.js +193 -0
  209. package/docs/src/pages/supabase.js +323 -0
  210. package/docs/src/pages/testing.js +198 -0
  211. package/docs/src/pages/validation.js +138 -0
  212. package/examples/contact.js +166 -0
  213. package/examples/counter.js +94 -0
  214. package/examples/dev.server.js +91 -0
  215. package/examples/examples.test.js +394 -0
  216. package/examples/pricing.js +244 -0
  217. package/examples/products.js +191 -0
  218. package/examples/quiz.js +208 -0
  219. package/examples/shared.js +78 -0
  220. package/examples/todos.js +162 -0
  221. package/package.json +75 -0
  222. package/public/.pulse-ui-version +1 -0
  223. package/public/chippy-bird.css +246 -0
  224. package/public/examples/contact.css +119 -0
  225. package/public/examples/counter.css +79 -0
  226. package/public/examples/pricing.css +132 -0
  227. package/public/examples/products.css +100 -0
  228. package/public/examples/quiz.css +200 -0
  229. package/public/examples/todos.css +137 -0
  230. package/public/favicon.ico +0 -0
  231. package/public/log-dashboard.css +383 -0
  232. package/public/pulse-ui.css +2740 -0
  233. package/public/pulse-ui.js +236 -0
  234. package/public/pulse.css +149 -0
  235. package/scripts/build.js +411 -0
  236. package/src/agent/checklist.md +111 -0
  237. package/src/agent/coverage-check.js +66 -0
  238. package/src/agent/guide-components.md +274 -0
  239. package/src/agent/guide-examples.md +54 -0
  240. package/src/agent/guide-routing.md +36 -0
  241. package/src/agent/guide-server.md +258 -0
  242. package/src/agent/guide-spec.md +103 -0
  243. package/src/agent/guide-styles.md +191 -0
  244. package/src/agent/guide.md +979 -0
  245. package/src/agent/identity.md +106 -0
  246. package/src/agent/workflow.md +108 -0
  247. package/src/cli/cli.test.js +82 -0
  248. package/src/cli/dev.js +195 -0
  249. package/src/cli/discover.js +113 -0
  250. package/src/cli/index.js +361 -0
  251. package/src/cli/load-report.js +91 -0
  252. package/src/cli/load-runner.js +121 -0
  253. package/src/cli/report-server.js +723 -0
  254. package/src/cli/report.js +116 -0
  255. package/src/cli/scaffold.archive.js +1371 -0
  256. package/src/cli/scaffold.js +349 -0
  257. package/src/cli/start.js +74 -0
  258. package/src/html.js +19 -0
  259. package/src/mcp/server.js +884 -0
  260. package/src/mcp/validate-worker.js +110 -0
  261. package/src/runtime/image.js +74 -0
  262. package/src/runtime/image.test.js +111 -0
  263. package/src/runtime/index.js +621 -0
  264. package/src/runtime/navigate.js +146 -0
  265. package/src/runtime/runtime.test.js +773 -0
  266. package/src/runtime/ssr.js +464 -0
  267. package/src/runtime/ssr.test.js +421 -0
  268. package/src/runtime/store.js +92 -0
  269. package/src/runtime/toast.js +163 -0
  270. package/src/server/index.js +1386 -0
  271. package/src/server/server.test.js +1248 -0
  272. package/src/spec/schema.js +428 -0
  273. package/src/spec/schema.test.js +291 -0
  274. package/src/store/index.js +102 -0
  275. package/src/store/store.test.js +210 -0
  276. package/src/testing/html.js +283 -0
  277. package/src/testing/index.js +249 -0
  278. package/src/testing/testing.test.js +450 -0
  279. package/src/ui/accordion.js +28 -0
  280. package/src/ui/alert.js +43 -0
  281. package/src/ui/app-badge.js +48 -0
  282. package/src/ui/avatar.js +47 -0
  283. package/src/ui/badge.js +24 -0
  284. package/src/ui/banner.js +26 -0
  285. package/src/ui/breadcrumbs.js +38 -0
  286. package/src/ui/button.js +66 -0
  287. package/src/ui/card.js +34 -0
  288. package/src/ui/carousel.js +59 -0
  289. package/src/ui/charts.js +321 -0
  290. package/src/ui/checkbox.js +65 -0
  291. package/src/ui/cluster.js +44 -0
  292. package/src/ui/code-window.js +39 -0
  293. package/src/ui/container.js +24 -0
  294. package/src/ui/cta.js +37 -0
  295. package/src/ui/divider.js +29 -0
  296. package/src/ui/empty.js +33 -0
  297. package/src/ui/feature.js +33 -0
  298. package/src/ui/fieldset.js +37 -0
  299. package/src/ui/fileupload.js +89 -0
  300. package/src/ui/footer.js +38 -0
  301. package/src/ui/grid.js +36 -0
  302. package/src/ui/heading.js +45 -0
  303. package/src/ui/hero.js +37 -0
  304. package/src/ui/icons.js +161 -0
  305. package/src/ui/index.js +89 -0
  306. package/src/ui/input.js +74 -0
  307. package/src/ui/list.js +36 -0
  308. package/src/ui/media.js +44 -0
  309. package/src/ui/modal.js +80 -0
  310. package/src/ui/nav.js +61 -0
  311. package/src/ui/pricing.js +56 -0
  312. package/src/ui/progress.js +62 -0
  313. package/src/ui/prose.js +29 -0
  314. package/src/ui/pullquote.js +34 -0
  315. package/src/ui/radio.js +102 -0
  316. package/src/ui/rating.js +93 -0
  317. package/src/ui/search.js +77 -0
  318. package/src/ui/section.js +69 -0
  319. package/src/ui/segmented.js +50 -0
  320. package/src/ui/select.js +77 -0
  321. package/src/ui/slider.js +84 -0
  322. package/src/ui/spinner.js +34 -0
  323. package/src/ui/stack.js +36 -0
  324. package/src/ui/stat.js +52 -0
  325. package/src/ui/stepper.js +46 -0
  326. package/src/ui/switch.js +57 -0
  327. package/src/ui/table.js +45 -0
  328. package/src/ui/testimonial.js +48 -0
  329. package/src/ui/textarea.js +72 -0
  330. package/src/ui/timeline.js +72 -0
  331. package/src/ui/tooltip.js +28 -0
  332. package/src/ui/ui.test.js +1241 -0
  333. package/src/ui/uiimage.js +65 -0
  334. package/tsconfig.json +13 -0
  335. package/types/html.d.ts +17 -0
  336. package/types/image.d.ts +70 -0
  337. package/types/index.d.ts +7 -0
  338. package/types/navigate.d.ts +38 -0
  339. package/types/runtime.d.ts +63 -0
  340. package/types/schema.d.ts +243 -0
  341. package/types/server.d.ts +145 -0
  342. package/types/ssr.d.ts +110 -0
  343. package/types/testing.d.ts +154 -0
  344. package/types/ui.d.ts +704 -0
@@ -0,0 +1,884 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Pulse MCP Server
4
+ *
5
+ * Provides tools and resources for an AI agent working inside a Pulse project.
6
+ *
7
+ * Resources:
8
+ * pulse://guide — complete guide: spec format, UI components, CSS rules, patterns
9
+ *
10
+ * Tools:
11
+ * pulse_list_structure — list all pages and components
12
+ * pulse_create_page — create a new page spec with proper template
13
+ * pulse_create_component — create a reusable component
14
+ * pulse_validate — validate a spec against the schema
15
+ * pulse_check_version — installed vs static vs npm latest
16
+ * pulse_update — re-copy pulse-ui assets from package → public/
17
+ */
18
+
19
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
20
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
21
+ import { z } from 'zod'
22
+
23
+ import path from 'path'
24
+ import fs from 'fs'
25
+ import http from 'http'
26
+ import { execFileSync, spawn, spawnSync } from 'child_process'
27
+
28
+ import { loadPages } from '../cli/discover.js'
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Project root
32
+ // ---------------------------------------------------------------------------
33
+
34
+ const rootArg = process.argv.indexOf('--root')
35
+ const ROOT = rootArg !== -1
36
+ ? path.resolve(process.argv[rootArg + 1])
37
+ : process.cwd()
38
+
39
+ const PAGES_DIR = path.join(ROOT, 'src', 'pages')
40
+ const COMPONENTS_DIR = path.join(ROOT, 'src', 'components')
41
+
42
+ const PKG_VERSION = JSON.parse(
43
+ fs.readFileSync(new URL('../../package.json', import.meta.url).pathname, 'utf8')
44
+ ).version
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Server
48
+ // ---------------------------------------------------------------------------
49
+
50
+ const server = new McpServer({ name: 'pulse', version: '0.2.0' })
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // pulse://guide/* resources — split by topic so each fits in one read
54
+ // ---------------------------------------------------------------------------
55
+
56
+ const GUIDE_RESOURCES = [
57
+ {
58
+ name: 'guide-index',
59
+ uri: 'pulse://guide',
60
+ title: 'Pulse Guide — Index',
61
+ description: 'Index of all Pulse guide resources. Fetch this first to find which topic resource to read next.',
62
+ content: () => PULSE_GUIDE_INDEX,
63
+ },
64
+ {
65
+ name: 'guide-spec',
66
+ uri: 'pulse://guide/spec',
67
+ title: 'Pulse Guide — Spec, Mutations, Actions, Streaming',
68
+ description: 'Spec structure, mutations, actions, streaming SSR, key rules, and form layout patterns.',
69
+ content: () => GUIDE_SPEC,
70
+ },
71
+ {
72
+ name: 'guide-server',
73
+ uri: 'pulse://guide/server',
74
+ title: 'Pulse Guide — Server, Store, Cookies, Redirects',
75
+ description: 'Global store, per-page persistence, server context, cookies, redirects, POST bodies, raw specs.',
76
+ content: () => GUIDE_SERVER,
77
+ },
78
+ {
79
+ name: 'guide-styles',
80
+ uri: 'pulse://guide/styles',
81
+ title: 'Pulse Guide — CSS, Theming, Fonts, Utilities',
82
+ description: 'meta.styles, theming with CSS tokens, custom fonts (Google/Adobe/self-hosted), utility classes.',
83
+ content: () => GUIDE_STYLES,
84
+ },
85
+ {
86
+ name: 'guide-routing',
87
+ uri: 'pulse://guide/routing',
88
+ title: 'Pulse Guide — Routing, Navigation, Page Discovery',
89
+ description: 'Site navigation, automatic page discovery, dynamic routes with :params.',
90
+ content: () => GUIDE_ROUTING,
91
+ },
92
+ {
93
+ name: 'guide-components',
94
+ uri: 'pulse://guide/components',
95
+ title: 'Pulse Guide — UI Components',
96
+ description: 'All Pulse UI components: forms, layout, charts, icons, landing page, typography. Props reference and composition patterns.',
97
+ content: () => GUIDE_COMPONENTS,
98
+ },
99
+ {
100
+ name: 'guide-examples',
101
+ uri: 'pulse://guide/examples',
102
+ title: 'Pulse Guide — Complete Examples',
103
+ description: 'Full working page examples including contact form with actions, validation, and error handling.',
104
+ content: () => GUIDE_EXAMPLES,
105
+ },
106
+ ]
107
+
108
+ for (const { name, uri, title, description, content } of GUIDE_RESOURCES) {
109
+ server.registerResource(name, uri, { title, description, mimeType: 'text/plain' },
110
+ async () => ({ contents: [{ uri, mimeType: 'text/plain', text: content() }] })
111
+ )
112
+ }
113
+
114
+ server.registerResource(
115
+ 'workflow',
116
+ 'pulse://workflow',
117
+ {
118
+ title: 'Pulse Build Workflow',
119
+ description: 'The exact sequence of phases and pass gates to follow for every build task. Fetch this at the start of any new build task.',
120
+ mimeType: 'text/plain',
121
+ },
122
+ async () => ({
123
+ contents: [{
124
+ uri: 'pulse://workflow',
125
+ mimeType: 'text/plain',
126
+ text: WORKFLOW,
127
+ }]
128
+ })
129
+ )
130
+
131
+ server.registerResource(
132
+ 'persona',
133
+ 'pulse://persona',
134
+ {
135
+ title: 'Pulse Agent Persona',
136
+ description: 'Who you are, what you care about, and the quality bar you hold yourself to when building Pulse apps.',
137
+ mimeType: 'text/plain',
138
+ },
139
+ async () => ({
140
+ contents: [{
141
+ uri: 'pulse://persona',
142
+ mimeType: 'text/plain',
143
+ text: PULSE_PERSONA,
144
+ }]
145
+ })
146
+ )
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // pulse_list_structure
150
+ // ---------------------------------------------------------------------------
151
+
152
+ server.registerTool(
153
+ 'pulse_list_structure',
154
+ {
155
+ description: 'List all pages and components in the Pulse project. Call this to understand what already exists before creating anything.',
156
+ inputSchema: {},
157
+ },
158
+ async () => {
159
+ const specs = await loadPages(ROOT)
160
+ const components = findComponents()
161
+ const lines = []
162
+
163
+ if (specs.length === 0) {
164
+ lines.push('Pages: (none)')
165
+ } else {
166
+ lines.push('Pages:')
167
+ for (const spec of specs) {
168
+ const route = spec.route
169
+ const isDynamic = route.includes(':')
170
+ const params = isDynamic
171
+ ? route.match(/:([^/]+)/g).map(p => p.slice(1)).join(', ')
172
+ : null
173
+
174
+ const tags = [
175
+ isDynamic && `params: ${params}`,
176
+ spec.server && 'server',
177
+ spec.mutations && `mutations: ${Object.keys(spec.mutations).join(', ')}`,
178
+ spec.actions && `actions: ${Object.keys(spec.actions).join(', ')}`,
179
+ ].filter(Boolean)
180
+
181
+ const tagStr = tags.length ? ` [${tags.join(' | ')}]` : ''
182
+ lines.push(` ${route.padEnd(24)} → ${path.relative(ROOT, spec.hydrate.replace('/src/', 'src/'))}${tagStr}`)
183
+ }
184
+ }
185
+
186
+ lines.push('')
187
+
188
+ if (components.length === 0) {
189
+ lines.push('Components: (none)')
190
+ } else {
191
+ lines.push('Components:')
192
+ for (const { name, filePath } of components) {
193
+ lines.push(` ${name.padEnd(24)} → ${path.relative(ROOT, filePath)}`)
194
+ }
195
+ }
196
+
197
+ lines.push('')
198
+
199
+ const stampPath = path.join(ROOT, 'public', '.pulse-ui-version')
200
+ const syncedVersion = fs.existsSync(stampPath) ? fs.readFileSync(stampPath, 'utf8').trim() : null
201
+
202
+ if (syncedVersion !== PKG_VERSION) {
203
+ lines.push(`⚠ pulse-ui assets are OUT OF DATE`)
204
+ lines.push(` Installed: v${PKG_VERSION}`)
205
+ lines.push(` Project: ${syncedVersion ? `v${syncedVersion}` : 'unknown (never synced)'}`)
206
+ lines.push(` Fix: stop the dev server and run \`pulse dev\` — assets sync automatically on startup.`)
207
+ lines.push(` Until then, new components or CSS changes will not be visible in the browser.`)
208
+ } else {
209
+ lines.push(`pulse-ui: v${PKG_VERSION} ✓`)
210
+ }
211
+
212
+ return text(lines.join('\n'))
213
+ }
214
+ )
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // pulse_validate
218
+ // ---------------------------------------------------------------------------
219
+
220
+ server.registerTool(
221
+ 'pulse_validate',
222
+ {
223
+ description: 'Validate a Pulse spec before writing it. Returns errors or confirms the spec is valid.',
224
+ inputSchema: { content: z.string().describe('JavaScript spec content to validate') },
225
+ },
226
+ async ({ content }) => validateContent(content)
227
+ )
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // pulse_create_page
231
+ // ---------------------------------------------------------------------------
232
+
233
+ server.registerTool(
234
+ 'pulse_create_page',
235
+ {
236
+ description: `Create a new page in the Pulse project. Filename determines route: home.js → /, about.js → /about.
237
+
238
+ IMPORTANT: Always follow these rules when writing the spec content:
239
+ - Import Pulse UI components from '@invisibleloop/pulse/ui' — never write raw HTML for nav, hero, button, card, input, etc.
240
+ - Include '/pulse-ui.css' in meta.styles whenever using any UI component
241
+ - Use u- utility classes for spacing/layout (u-flex, u-flex-col, u-gap-4, u-mt-8, u-text-center, etc.) — never inline styles
242
+ - Use var(--ui-*) CSS tokens in any custom CSS — never hardcode hex colours
243
+ - onSuccess AND onError are both required in every action
244
+ - Do NOT use data-event on text inputs — use FormData in onStart/run instead
245
+ - Always export default spec`,
246
+ inputSchema: {
247
+ name: z.string().describe('Filename without extension, e.g. "about" or "blog/post"'),
248
+ content: z.string().describe('Complete JS spec — must export default a valid Pulse spec object'),
249
+ },
250
+ },
251
+ async ({ name, content }) => {
252
+ const validation = await validateContent(content)
253
+ if (validation.content[0].text.startsWith('Invalid')) return validation
254
+
255
+ const segments = name.replace(/\.js$/, '').split('/')
256
+ const fullPath = path.join(PAGES_DIR, ...segments) + '.js'
257
+
258
+ if (!fullPath.startsWith(PAGES_DIR)) {
259
+ return text('Error: page name must not escape src/pages/')
260
+ }
261
+
262
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true })
263
+ fs.writeFileSync(fullPath, content, 'utf8')
264
+
265
+ const route = derivedRouteFromName(name)
266
+ return text(`Created ${path.relative(ROOT, fullPath)} → route "${route}"`)
267
+ }
268
+ )
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // pulse_create_component
272
+ // ---------------------------------------------------------------------------
273
+
274
+ server.registerTool(
275
+ 'pulse_create_component',
276
+ {
277
+ description: `Create a reusable view component in src/components/. Components export named functions that return HTML strings.
278
+
279
+ IMPORTANT rules for component content:
280
+ - Import Pulse UI components from '@invisibleloop/pulse/ui' where applicable
281
+ - Use u- utility classes for spacing/layout — never inline styles
282
+ - Use var(--ui-*) CSS tokens for any colour references — never hardcode hex values
283
+ - Export named functions only (no default export needed)`,
284
+ inputSchema: {
285
+ name: z.string().describe('Component name, e.g. "hero" or "nav"'),
286
+ content: z.string().describe('JS — export named functions that return HTML strings'),
287
+ },
288
+ },
289
+ async ({ name, content }) => {
290
+ const safeName = name.replace(/\.js$/, '')
291
+ const fullPath = path.join(COMPONENTS_DIR, `${safeName}.js`)
292
+
293
+ if (!fullPath.startsWith(COMPONENTS_DIR)) {
294
+ return text('Error: component name must not escape src/components/')
295
+ }
296
+
297
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true })
298
+ fs.writeFileSync(fullPath, content, 'utf8')
299
+
300
+ return text(`Created ${path.relative(ROOT, fullPath)}`)
301
+ }
302
+ )
303
+
304
+ // ---------------------------------------------------------------------------
305
+ // pulse_create_store
306
+ // ---------------------------------------------------------------------------
307
+
308
+ server.registerTool(
309
+ 'pulse_create_store',
310
+ {
311
+ description: `Create a pulse.store.js global store at the project root. The store defines server fetchers and client mutations that are shared across pages.
312
+
313
+ IMPORTANT rules:
314
+ - server fetchers must be async functions: async (ctx) => value
315
+ - mutations must be pure functions: (storeState, payload?) => partialState — no fetch, no side effects
316
+ - hydrate is required if the store has mutations (enables client-side store mutation dispatch)
317
+ - Register the store in your server file by passing it to createServer({ store })
318
+ - Pages subscribe to store keys via spec.store: ['user', 'settings']`,
319
+ inputSchema: {
320
+ content: z.string().describe('Complete pulse.store.js content — must export default a valid store object'),
321
+ },
322
+ },
323
+ async ({ content }) => {
324
+ const storePath = path.join(ROOT, 'pulse.store.js')
325
+
326
+ // Basic structure check before writing
327
+ if (!content.includes('export default')) {
328
+ return text('Invalid: store must contain "export default { ... }"')
329
+ }
330
+
331
+ fs.writeFileSync(storePath, content, 'utf8')
332
+
333
+ return text(`Created pulse.store.js
334
+
335
+ Next steps:
336
+ 1. Import and register it in your server file:
337
+ import store from './pulse.store.js'
338
+ createServer(specs, { store })
339
+
340
+ 2. Declare which keys each page uses:
341
+ export default { route: '/dashboard', store: ['user', 'settings'], ... }
342
+
343
+ 3. If you added mutations, set hydrate in pulse.store.js:
344
+ hydrate: '/pulse.store.js'
345
+
346
+ Store data is available in the view as the second argument alongside page server data.`)
347
+ }
348
+ )
349
+
350
+ // ---------------------------------------------------------------------------
351
+ // pulse_create_action
352
+ // ---------------------------------------------------------------------------
353
+
354
+ server.registerTool(
355
+ 'pulse_create_action',
356
+ {
357
+ description: 'Generate a correctly-structured Pulse action snippet to add to a page spec. Returns code to paste into the spec\'s actions property. Actions handle async operations like form submissions and API calls. Note: onSuccess AND onError are BOTH required — omitting either will cause a runtime error.',
358
+ inputSchema: {
359
+ name: z.string().describe('Action name, e.g. "submit" or "deleteItem"'),
360
+ description: z.string().optional().describe('What the action does — used as a code comment'),
361
+ validate: z.boolean().optional().describe('Whether to run spec validation before run() — use true for forms with validation rules'),
362
+ fields: z.string().optional().describe('Comma-separated list of FormData fields this action expects, e.g. "email,name,message"'),
363
+ },
364
+ },
365
+ ({ name, description, validate = false, fields }) => {
366
+ const comment = description ? ` // ${description}\n` : ''
367
+ const fieldList = fields
368
+ ? fields.split(',').map(f => f.trim()).filter(Boolean)
369
+ : []
370
+
371
+ const onStart = fieldList.length > 0
372
+ ? ` onStart: (state, formData) => ({\n status: 'loading',\n${fieldList.map(f => ` ${f}: formData.get('${f}'),`).join('\n')}\n }),`
373
+ : ` onStart: (state, formData) => ({ status: 'loading' }),`
374
+
375
+ const snippet = `${comment} ${name}: {
376
+ ${onStart}${validate ? '\n validate: true,' : ''}
377
+ run: async (state, serverState, formData) => {
378
+ // TODO: implement — fetch, API call, etc.
379
+ },
380
+ onSuccess: (state, result) => ({ status: 'success' }),
381
+ onError: (state, err) => ({
382
+ status: 'error',
383
+ errors: err?.validation ?? [{ message: err.message }],
384
+ }),
385
+ },`
386
+
387
+ return text(`Add this inside your spec's actions property:\n\n actions: {\n${snippet}\n }`)
388
+ }
389
+ )
390
+
391
+ // ---------------------------------------------------------------------------
392
+ // pulse_fetch_page
393
+ // ---------------------------------------------------------------------------
394
+
395
+ server.registerTool(
396
+ 'pulse_fetch_page',
397
+ {
398
+ description: 'Fetch server-rendered HTML from the dev server. Use after creating or editing a page to verify SSR output, check for missing content, and spot errors.',
399
+ inputSchema: { url: z.string().describe('Full URL, e.g. http://localhost:3000/about') },
400
+ },
401
+ ({ url }) => new Promise(resolve => {
402
+ const req = http.get(url, { timeout: 10_000 }, res => {
403
+ const chunks = []
404
+ res.on('data', d => chunks.push(d))
405
+ res.on('end', () => {
406
+ const body = Buffer.concat(chunks).toString('utf-8')
407
+ resolve(text(`HTTP ${res.statusCode}\n\n${body.slice(0, 8000)}`))
408
+ })
409
+ })
410
+ req.on('error', e => resolve(text(`Error fetching page: ${e.message}`)))
411
+ req.on('timeout', () => { req.destroy(); resolve(text('Error: request timed out')) })
412
+ })
413
+ )
414
+
415
+ // ---------------------------------------------------------------------------
416
+ // pulse_restart_server
417
+ // ---------------------------------------------------------------------------
418
+
419
+ server.registerTool(
420
+ 'pulse_restart_server',
421
+ {
422
+ description: 'Stop and restart the Pulse dev server. Use after adding new pages or making changes that require a restart.',
423
+ inputSchema: {},
424
+ },
425
+ async () => {
426
+ let port = 3000
427
+ const configPath = path.join(ROOT, 'pulse.config.js')
428
+ if (fs.existsSync(configPath)) {
429
+ try {
430
+ const mod = await import(`${configPath}?t=${Date.now()}`)
431
+ if (mod.default?.port) port = mod.default.port
432
+ } catch { /* use default */ }
433
+ }
434
+
435
+ // Kill any process on the port
436
+ try { execFileSync('sh', ['-c', `lsof -ti:${port} | xargs kill -9 2>/dev/null; true`]) } catch { /* nothing running */ }
437
+
438
+ // Start fresh dev server detached so it outlives the MCP tool call
439
+ const devScript = new URL('../cli/dev.js', import.meta.url).pathname
440
+ const proc = spawn(process.execPath, [devScript, '--root', ROOT], { detached: true, stdio: 'ignore' })
441
+ proc.unref()
442
+
443
+ // Give it a moment to bind the port
444
+ await new Promise(r => setTimeout(r, 1500))
445
+ return text(`Dev server restarted on port ${port}`)
446
+ }
447
+ )
448
+
449
+ // ---------------------------------------------------------------------------
450
+ // pulse_build
451
+ // ---------------------------------------------------------------------------
452
+
453
+ server.registerTool(
454
+ 'pulse_build',
455
+ {
456
+ description: 'Run a production build (pulse build) and start the production server on a separate port for Lighthouse testing. Returns the production URL. Call pulse_restart_server afterwards to return to the dev server.',
457
+ inputSchema: {},
458
+ },
459
+ () => new Promise(resolve => {
460
+ const buildScript = new URL('../../scripts/build.js', import.meta.url).pathname
461
+
462
+ // Determine ports from config
463
+ let devPort = 3000
464
+ const configPath = path.join(ROOT, 'pulse.config.js')
465
+ try {
466
+ // Synchronous dynamic import not possible — read config file directly for port
467
+ const src = fs.readFileSync(configPath, 'utf8')
468
+ const m = src.match(/port\s*:\s*(\d+)/)
469
+ if (m) devPort = parseInt(m[1], 10)
470
+ } catch { /* use default */ }
471
+ const prodPort = devPort + 1
472
+
473
+ // Run build
474
+ const build = spawnSync(process.execPath, [buildScript, '--root', ROOT], { encoding: 'utf8' })
475
+ if (build.status !== 0) {
476
+ return resolve(text(`Build failed:\n${build.stderr || build.stdout}`))
477
+ }
478
+
479
+ // Kill anything on prodPort
480
+ try { execFileSync('sh', ['-c', `lsof -ti:${prodPort} | xargs kill -9 2>/dev/null; true`]) } catch { /* ok */ }
481
+
482
+ // Start prod server detached on prodPort
483
+ const startScript = new URL('../cli/start.js', import.meta.url).pathname
484
+ const proc = spawn(process.execPath, [startScript, '--root', ROOT, '--port', String(prodPort)], { detached: true, stdio: 'ignore' })
485
+ proc.unref()
486
+
487
+ // Wait for it to bind
488
+ setTimeout(() => resolve(text(
489
+ `Production build complete. Server running at http://localhost:${prodPort}/\n` +
490
+ `Run Lighthouse against this URL, then call pulse_restart_server to return to dev.`
491
+ )), 2000)
492
+ })
493
+ )
494
+
495
+ // ---------------------------------------------------------------------------
496
+ // pulse_review
497
+ // ---------------------------------------------------------------------------
498
+
499
+ server.registerTool(
500
+ 'pulse_review',
501
+ {
502
+ description: `Switch into reviewer mode and critically examine a page spec you just built.
503
+ Reads the spec source, renders the view with initial state, runs all validation checks,
504
+ and returns a structured review brief. You must read everything carefully, find every
505
+ issue, and fix them all before reporting back to the user. Use this after completing
506
+ any feature build.`,
507
+ inputSchema: {
508
+ file: z.string().describe('Absolute path to the spec file to review'),
509
+ },
510
+ },
511
+ async ({ file }) => {
512
+ if (!fs.existsSync(file)) return text(`File not found: ${file}`)
513
+
514
+ const source = fs.readFileSync(file, 'utf8')
515
+
516
+ // Run the validator in a child process (same as pulse_validate)
517
+ const validatorScript = new URL('./validate-worker.js', import.meta.url).pathname
518
+ let validationResult = '(could not run validator)'
519
+ try {
520
+ validationResult = execFileSync(process.execPath, [validatorScript, file], {
521
+ timeout: 10_000,
522
+ encoding: 'utf8',
523
+ }).trim()
524
+ } catch (err) {
525
+ validationResult = err.stdout?.trim() || err.message
526
+ }
527
+
528
+ // Try to render the view with initial state
529
+ let renderedHtml = ''
530
+ let renderNote = ''
531
+ try {
532
+ const mod = await import(`${file}?review=${Date.now()}`)
533
+ const spec = mod.default
534
+ if (spec && typeof spec.view === 'function') {
535
+ renderedHtml = spec.view(spec.state || {}, {})
536
+ } else if (spec && typeof spec.view === 'object') {
537
+ const segments = Object.entries(spec.view)
538
+ .map(([k, fn]) => `<!-- segment: ${k} -->\n${typeof fn === 'function' ? fn(spec.state || {}, {}) : ''}`)
539
+ .join('\n')
540
+ renderedHtml = segments
541
+ renderNote = '(streamed spec — segments rendered individually)'
542
+ }
543
+ } catch {
544
+ renderNote = '(view could not be rendered — may depend on server data)'
545
+ }
546
+
547
+ return text(`# Pulse Code Review
548
+
549
+ You are now a **senior code reviewer**. You did not write this code. Read it with fresh eyes and find every problem — no matter how small.
550
+
551
+ Work through each section of the checklist below. For every issue you find, fix it immediately before moving on. Do not report issues without fixing them. When you have fixed everything, confirm what you changed.
552
+
553
+ ---
554
+
555
+ ## Spec source
556
+
557
+ \`\`\`js
558
+ ${source}
559
+ \`\`\`
560
+
561
+ ---
562
+
563
+ ## Rendered HTML (initial state) ${renderNote}
564
+
565
+ \`\`\`html
566
+ ${renderedHtml || '(empty)'}
567
+ \`\`\`
568
+
569
+ ---
570
+
571
+ ## Validator output
572
+
573
+ ${validationResult}
574
+
575
+ ---
576
+
577
+ ## Review checklist
578
+
579
+ Work through every item. Fix anything that fails.
580
+
581
+ ### Structure
582
+ - [ ] \`route\` is set explicitly — not left to auto-discovery
583
+ - [ ] \`hydrate\` is set if the page has mutations, actions, or persist
584
+ - [ ] \`state\` shape is consistent — no fields that flip between null/string/boolean
585
+ - [ ] \`meta.title\` is meaningful and unique to this page
586
+ - [ ] \`meta.description\` is a real description, not "Built with Pulse"
587
+
588
+ ### Mutations & actions
589
+ - [ ] Every mutation returns a plain partial object — no side effects, no fetch, no DOM access
590
+ - [ ] \`constraints\` are used for bounds instead of conditional logic inside mutations
591
+ - [ ] \`disabled\` in the view matches the constraint bounds — but check: is it redundant with the constraint, or does it serve a UX purpose?
592
+ - [ ] Actions read user input from FormData in \`onStart\`, not from mirrored state
593
+ - [ ] \`onStart\` sets a loading status, \`onSuccess\`/\`onError\` resolve it
594
+ - [ ] A single \`status\` field is used instead of multiple boolean flags
595
+
596
+ ### Components & HTML
597
+ - [ ] Components from the UI library are used — no hand-written \`<button>\`, \`<input>\`, \`<table>\` etc where a component exists
598
+ - [ ] No \`data-event\` on text inputs — this destroys focus on every keystroke
599
+ - [ ] No \`className\`, \`htmlFor\`, \`onClick=\`, or other React patterns
600
+ - [ ] No hardcoded hex colours — only \`var(--ui-*)\` tokens
601
+ - [ ] No emoji in the view HTML
602
+
603
+ ### Accessibility
604
+ - [ ] \`<main id="main-content">\` is present
605
+ - [ ] Icon-only buttons have \`aria-label\`
606
+ - [ ] \`aria-live\` and \`aria-label\` are NOT on the same element
607
+ - [ ] Heading hierarchy is correct — no skipped levels, starts at h1
608
+ - [ ] Disabled state uses the \`disabled\` attribute, not just CSS or opacity
609
+
610
+ ### Defensive coding
611
+ - [ ] Any \`fetch\` in actions or server fetchers checks \`res.ok\` before calling \`.json()\`
612
+ - [ ] Fetch errors use the safe pattern — NOT \`throw new Error(await res.text())\` which exposes raw HTML in toasts:
613
+ \`\`\`js
614
+ if (!res.ok) {
615
+ let message = \`Request failed: \${res.status}\`
616
+ try { const j = await res.json(); message = j.message || j.error || message } catch {}
617
+ throw new Error(message)
618
+ }
619
+ \`\`\`
620
+ - [ ] Optional chaining used for any data from external sources
621
+ - [ ] URL params validated before use
622
+ - [ ] \`onViewError\` defined if the view could crash on bad or missing data
623
+
624
+ ---
625
+
626
+ Fix every issue you find. Then confirm what was changed.
627
+
628
+ **After confirming fixes: you are back in builder mode. Continue to the verification workflow — navigate to the page in the browser, take a screenshot, run Lighthouse desktop audit, run Lighthouse mobile audit. Do not stop at the review.**`)
629
+ }
630
+ )
631
+
632
+ // ---------------------------------------------------------------------------
633
+ // pulse_check_version
634
+ // ---------------------------------------------------------------------------
635
+
636
+ server.registerTool(
637
+ 'pulse_check_version',
638
+ {
639
+ description: 'Check the installed @invisibleloop/pulse version, the static asset version in public/, and the latest version available on npm. Use this instead of running npm commands.',
640
+ inputSchema: {},
641
+ },
642
+ () => new Promise(resolve => {
643
+ const pkgJson = JSON.parse(fs.readFileSync(new URL('../../package.json', import.meta.url).pathname, 'utf8'))
644
+ const installed = pkgJson.version
645
+ const stampPath = path.join(ROOT, 'public', '.pulse-ui-version')
646
+ const staticAsset = fs.existsSync(stampPath) ? fs.readFileSync(stampPath, 'utf8').trim() : 'unknown'
647
+ const inSync = installed === staticAsset
648
+
649
+ // Fetch latest from npm registry
650
+ const req = http.get('http://registry.npmjs.org/@invisibleloop/pulse/latest', { timeout: 5000 }, res => {
651
+ const chunks = []
652
+ res.on('data', d => chunks.push(d))
653
+ res.on('end', () => {
654
+ let latest = 'unknown'
655
+ try { latest = JSON.parse(Buffer.concat(chunks).toString()).version } catch { /* ignore */ }
656
+
657
+ const lines = [
658
+ `Installed package : v${installed}`,
659
+ `Static assets : v${staticAsset}${inSync ? '' : ' ⚠ out of sync — run pulse_update'}`,
660
+ `Latest on npm : v${latest}`,
661
+ ]
662
+ if (latest !== 'unknown' && latest !== installed) {
663
+ lines.push(`\nUpdate available: run \`npm update @invisibleloop/pulse\` then \`pulse_update\` to apply.`)
664
+ } else if (latest === installed) {
665
+ lines.push(`\nPackage is up to date.`)
666
+ }
667
+ resolve(text(lines.join('\n')))
668
+ })
669
+ })
670
+ req.on('error', () => {
671
+ resolve(text([
672
+ `Installed package : v${installed}`,
673
+ `Static assets : v${staticAsset}${inSync ? '' : ' ⚠ out of sync — run pulse_update'}`,
674
+ `Latest on npm : (registry unreachable)`,
675
+ ].join('\n')))
676
+ })
677
+ req.on('timeout', () => { req.destroy() })
678
+ })
679
+ )
680
+
681
+ // ---------------------------------------------------------------------------
682
+ // pulse_update
683
+ // ---------------------------------------------------------------------------
684
+
685
+ server.registerTool(
686
+ 'pulse_update',
687
+ {
688
+ description: 'Re-copy pulse-ui.css, pulse-ui.js, and the agent checklist from the installed package into public/. Run after npm update @invisibleloop/pulse, or when visual output looks wrong and you suspect stale CSS.',
689
+ inputSchema: {},
690
+ },
691
+ () => {
692
+ const pkgPublic = new URL('../../public', import.meta.url).pathname
693
+ const publicDir = path.join(ROOT, 'public')
694
+ const assets = ['pulse-ui.css', 'pulse-ui.js', '.pulse-ui-version']
695
+ const updated = []
696
+
697
+ fs.mkdirSync(publicDir, { recursive: true })
698
+ for (const asset of assets) {
699
+ const src = path.join(pkgPublic, asset)
700
+ const dst = path.join(publicDir, asset)
701
+ if (fs.existsSync(src)) { fs.copyFileSync(src, dst); updated.push(`public/${asset}`) }
702
+ }
703
+
704
+ const checklistSrc = new URL('../agent/checklist.md', import.meta.url).pathname
705
+ const checklistDst = path.join(ROOT, '.claude', 'pulse-checklist.md')
706
+ if (fs.existsSync(checklistSrc)) {
707
+ fs.mkdirSync(path.dirname(checklistDst), { recursive: true })
708
+ fs.copyFileSync(checklistSrc, checklistDst)
709
+ updated.push('.claude/pulse-checklist.md')
710
+ }
711
+
712
+ const versionFile = path.join(publicDir, '.pulse-ui-version')
713
+ const version = fs.existsSync(versionFile) ? fs.readFileSync(versionFile, 'utf8').trim() : '?'
714
+ return text(`pulse-ui updated to v${version}\n\n${updated.map(f => `✓ ${f}`).join('\n')}`)
715
+ }
716
+ )
717
+
718
+ // ---------------------------------------------------------------------------
719
+ // Helpers
720
+ // ---------------------------------------------------------------------------
721
+
722
+ async function validateContent(content) {
723
+ // Write into PAGES_DIR so relative imports (e.g. '../components/nav.js') resolve correctly
724
+ fs.mkdirSync(PAGES_DIR, { recursive: true })
725
+ const tmpFile = path.join(PAGES_DIR, `.pulse-validate-${Date.now()}.mjs`)
726
+ try {
727
+ fs.writeFileSync(tmpFile, content, 'utf8')
728
+
729
+ // Run validation in a child process with a hard timeout so a hanging import
730
+ // (slow module, circular dep, network call) cannot block the MCP server.
731
+ const validatorScript = new URL('./validate-worker.js', import.meta.url).pathname
732
+ let output
733
+ try {
734
+ output = execFileSync(process.execPath, [validatorScript, tmpFile], {
735
+ timeout: 10_000,
736
+ encoding: 'utf8',
737
+ })
738
+ } catch (err) {
739
+ const msg = err.killed || err.signal === 'SIGTERM'
740
+ ? 'Invalid: validation timed out — spec may have a hanging import or infinite loop'
741
+ : `Invalid: could not parse — ${err.stdout || err.message}`
742
+ return text(msg)
743
+ }
744
+
745
+ return text(output.trim())
746
+ } finally {
747
+ try { fs.unlinkSync(tmpFile) } catch { /* ignore */ }
748
+ }
749
+ }
750
+
751
+ function findComponents() {
752
+ if (!fs.existsSync(COMPONENTS_DIR)) return []
753
+ return fs.readdirSync(COMPONENTS_DIR)
754
+ .filter(f => f.endsWith('.js'))
755
+ .map(f => ({ name: path.basename(f, '.js'), filePath: path.join(COMPONENTS_DIR, f) }))
756
+ }
757
+
758
+ function derivedRouteFromName(name) {
759
+ const parts = name.replace(/\.js$/, '').split('/')
760
+ const last = parts[parts.length - 1]
761
+ if (last === 'index' || last === 'home') parts.pop()
762
+ if (parts.length === 0) return '/'
763
+ return '/' + parts.join('/')
764
+ }
765
+
766
+ function text(str) {
767
+ return { content: [{ type: 'text', text: str }] }
768
+ }
769
+
770
+ // ---------------------------------------------------------------------------
771
+ // Shared agent files — single source of truth for identity, guide, checklist
772
+ // ---------------------------------------------------------------------------
773
+
774
+ const IDENTITY = fs.readFileSync(new URL('../agent/identity.md', import.meta.url), 'utf8')
775
+ const WORKFLOW = fs.readFileSync(new URL('../agent/workflow.md', import.meta.url), 'utf8')
776
+ const CHECKLIST = fs.readFileSync(new URL('../agent/checklist.md', import.meta.url), 'utf8')
777
+ const GUIDE_SPEC = fs.readFileSync(new URL('../agent/guide-spec.md', import.meta.url), 'utf8')
778
+ const GUIDE_SERVER = fs.readFileSync(new URL('../agent/guide-server.md', import.meta.url), 'utf8')
779
+ const GUIDE_STYLES = fs.readFileSync(new URL('../agent/guide-styles.md', import.meta.url), 'utf8')
780
+ const GUIDE_ROUTING = fs.readFileSync(new URL('../agent/guide-routing.md', import.meta.url), 'utf8')
781
+ const GUIDE_COMPONENTS = fs.readFileSync(new URL('../agent/guide-components.md', import.meta.url), 'utf8')
782
+ const GUIDE_EXAMPLES = fs.readFileSync(new URL('../agent/guide-examples.md', import.meta.url), 'utf8')
783
+
784
+ // ---------------------------------------------------------------------------
785
+ // The Persona — CLI-specific identity header + shared identity content
786
+ // ---------------------------------------------------------------------------
787
+
788
+ const PULSE_PERSONA = `# Who You Are
789
+
790
+ You are a Pulse AI. You build exclusively with Pulse.
791
+
792
+ Every page you create is a Pulse spec. Every interaction is a mutation or an action. Every UI element comes from \`@invisibleloop/pulse/ui\`. You do not reach for React, Vue, Alpine, htmx, Tailwind, or any other framework or utility library — not because you cannot, but because Pulse already solves what they solve, with stronger guarantees and fewer moving parts.
793
+
794
+ You are not a general-purpose web developer who happens to know Pulse. Pulse is your entire world. When a user asks for a page, a form, a dashboard, a landing page — you build it in Pulse. The spec is your medium.
795
+
796
+ You follow the Pulse guide for all technical decisions. The guide is split into topic resources — fetch \`pulse://guide\` for the index, then fetch the specific section you need (\`pulse://guide/spec\`, \`pulse://guide/components\`, etc.). Do not guess — if you are unsure about a prop, pattern, or rule, fetch the relevant guide section first.
797
+
798
+ ${IDENTITY}
799
+ `
800
+
801
+ // ---------------------------------------------------------------------------
802
+ // The Guide — index + tools reference (topic content split into sub-resources)
803
+ // ---------------------------------------------------------------------------
804
+
805
+ const PULSE_GUIDE_INDEX = `# Pulse Framework Guide
806
+
807
+ ## Start here
808
+
809
+ **Fetch \`pulse://workflow\` before anything else.** It defines the exact sequence of phases and pass gates for every build task. Do not fetch guide sections or start writing code until you have read it. Skipping it means you will run steps in the wrong order.
810
+
811
+ ## Guide resources
812
+
813
+ | Resource | When to fetch |
814
+ |---|---|
815
+ | \`pulse://workflow\` | **First. Always. Before any guide sections or code.** |
816
+ | \`pulse://guide/spec\` | Building a spec — state, mutations, actions, streaming SSR, key rules, form layout |
817
+ | \`pulse://guide/server\` | Server data, global store, persist, cookies, redirects, POST handling |
818
+ | \`pulse://guide/styles\` | CSS tokens, theming, custom fonts, utility classes |
819
+ | \`pulse://guide/routing\` | Navigation, page discovery, dynamic routes |
820
+ | \`pulse://guide/components\` | All UI components, icons, charts, composition patterns |
821
+ | \`pulse://guide/examples\` | Complete working page examples |
822
+
823
+ ## Tools available
824
+
825
+ **Pulse MCP tools** (always available):
826
+ - \`pulse_list_structure\` — list pages, components, and pulse-ui version. Call at the start of every session.
827
+ - \`pulse_validate\` — validate spec content. Call after every write. Fix all errors AND warnings.
828
+ - \`pulse_review\` — switch into reviewer mode and critically examine a spec you just built. Returns the source, rendered HTML, validator output, and a full review checklist. **Call this only after validate, Lighthouse (desktop + mobile), and tests all pass — it is the final phase before declaring done.**
829
+ - \`pulse_create_page\` — create a new page spec. Validates before writing.
830
+ - \`pulse_create_component\` — create a reusable component.
831
+ - \`pulse_create_store\` — create the pulse.store.js global store.
832
+ - \`pulse_create_action\` — generate a correctly-structured action snippet.
833
+ - \`pulse_fetch_page(url)\` — HTTP GET the dev server URL. Use to verify SSR output.
834
+ - \`pulse_restart_server\` — stop and restart the dev server.
835
+ - \`pulse_build\` — production build + starts prod server on devPort+1 for Lighthouse. Returns the URL. Call \`pulse_restart_server\` after to return to dev. **Slow — takes 30–60 s. Tell the user before calling.**
836
+ - \`pulse_check_version\` — check installed package version, static asset version, and latest on npm. Use this instead of running npm commands when the user asks about updates.
837
+ - \`pulse_update\` — re-copy \`pulse-ui.css\`, \`pulse-ui.js\`, and the agent checklist from the installed package into \`public/\`. Run this after \`npm update @invisibleloop/pulse\`, or whenever visual output looks wrong and you suspect stale CSS.
838
+
839
+ **Chrome DevTools MCP tools** (globally available):
840
+ - \`mcp__chrome-devtools__take_screenshot\` — visual screenshot of the page.
841
+ - \`mcp__chrome-devtools__list_console_messages\` — browser console output including errors.
842
+ - \`mcp__chrome-devtools__list_network_requests\` — network requests, including 404s.
843
+ - \`mcp__chrome-devtools__lighthouse_audit\` — Lighthouse scores and failing audits. **Slow — takes 30–60 s per run (×2 for desktop + mobile). Tell the user before calling.**
844
+ - \`mcp__chrome-devtools__navigate_page\` — navigate the browser to a URL.
845
+ - \`mcp__chrome-devtools__list_pages\` — list all open browser pages/tabs. Returns an array of objects each with a numeric \`id\` field.
846
+ - \`mcp__chrome-devtools__close_page\` — close a page by its numeric ID. **CRITICAL: \`pageId\` must be a JSON number, not a string. \`{ pageId: 2 }\` is correct. \`{ pageId: "2" }\` will fail with a type error.** Take the \`id\` value from \`list_pages\` and pass it unquoted.
847
+
848
+ ## MANDATORY: Verify every build
849
+
850
+ **RULE: NEVER run \`lighthouse_audit\` against the dev server. Dev mode serves unminified source files — scores are meaningless. Lighthouse MUST always run against the production build.**
851
+
852
+ **Before calling any slow tool (\`pulse_build\`, \`lighthouse_audit\`), output a short status message to the user explaining what you are about to do and that it may take a moment.** Example: "Building for production — this takes ~30 s…" or "Running Lighthouse desktop audit — may take up to a minute…". Do not call the tool silently.
853
+
854
+ After writing or editing any page, run ALL of the following steps in order before telling the user you are done:
855
+
856
+ 1. \`pulse_validate\` — validate the spec. Fix all errors and warnings before continuing.
857
+ 2. \`pulse_review\` — **switch into reviewer mode**. Read the source, rendered HTML, and checklist returned by the tool. Fix every issue found before moving on. This is mandatory — do not skip it.
858
+ 3. \`pulse_restart_server\` — only if you added a new page or changed imports.
859
+ 2. \`mcp__chrome-devtools__navigate_page\` — navigate to the dev server URL. After any CSS or asset change, follow immediately with \`mcp__chrome-devtools__evaluate_script\` running \`location.reload(true)\` to force a hard refresh.
860
+ 3. \`mcp__chrome-devtools__take_screenshot\` — check layout, spacing, content visibility, no overflow. Also check headings for orphans (a single short word stranded on the last line). To detect them programmatically, run \`mcp__chrome-devtools__evaluate_script\` with:
861
+ \`\`\`js
862
+ Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6')).filter(h=>{const r=document.createRange();r.selectNodeContents(h);const rects=r.getClientRects();if(rects.length<2)return false;const last=rects[rects.length-1];return last.width/h.getBoundingClientRect().width<0.4;}).map(h=>h.tagName+': '+h.textContent.trim())
863
+ \`\`\`
864
+ Any heading returned has a last line shorter than 40% of its width — fix it with \`balance: true\` on the \`heading()\` component.
865
+ 4. \`mcp__chrome-devtools__list_console_messages\` (errors) — fix every JS error.
866
+ 5. \`mcp__chrome-devtools__list_network_requests\` (failed) — fix every 404 or failed fetch.
867
+ 6. \`pulse_fetch_page\` — pass the full URL e.g. \`{ url: "http://localhost:3000/" }\`. Confirm SSR renders expected content, no blank body.
868
+ 7. \`pulse_build\` — this builds for production AND starts a prod server on devPort+1. Wait for it to return the prod URL.
869
+ 8. \`mcp__chrome-devtools__navigate_page\` — navigate to the production URL returned by \`pulse_build\` (e.g. \`http://localhost:3001/\`).
870
+ 9. \`mcp__chrome-devtools__lighthouse_audit\` with \`{ "strategy": "desktop" }\` — run against the production URL. All four scores (Performance, Accessibility, Best Practices, SEO) must be 100. Report the actual scores and fix every failing audit before continuing.
871
+ 10. \`mcp__chrome-devtools__lighthouse_audit\` with \`{ "strategy": "mobile" }\` — run the same audit for mobile. All four scores must also be 100. Fix any failures before continuing.
872
+ 11. \`pulse_restart_server\` — shut down the prod server and return to dev.
873
+ 12. \`mcp__chrome-devtools__list_pages\` then \`mcp__chrome-devtools__close_page\` — close **every** page returned by \`list_pages\` to shut the browser down entirely. \`pageId\` must be a number: \`{ pageId: 2 }\` ✓ — NOT \`{ pageId: "2" }\` ✗ (string will fail). Loop through all page IDs and close each one.
874
+
875
+ Do not declare success until all steps pass (step 12 is cleanup — always run it). If any step reveals a problem, fix it and repeat from step 2.
876
+
877
+ ${CHECKLIST}`
878
+
879
+ // ---------------------------------------------------------------------------
880
+ // Start
881
+ // ---------------------------------------------------------------------------
882
+
883
+ const transport = new StdioServerTransport()
884
+ await server.connect(transport)