@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.
- package/.claude/commands/build-page.md +59 -0
- package/.claude/commands/new-doc-page.md +45 -0
- package/.claude/commands/verify.md +52 -0
- package/.claude/pulse-checklist.md +111 -0
- package/.claude/settings.local.json +102 -0
- package/.github/workflows/ci.yml +22 -0
- package/.github/workflows/publish.yml +41 -0
- package/.pulse/load-reports/home/1773432711417.json +22 -0
- package/CLAUDE.md +383 -0
- package/README.md +95 -0
- package/docs/.claude/pulse-checklist.md +111 -0
- package/docs/public/.pulse-ui-version +1 -0
- package/docs/public/dist/accessibility.boot-5DVTARJU.js +115 -0
- package/docs/public/dist/actions.boot-P66HKQEM.js +164 -0
- package/docs/public/dist/auth.boot-IMAJAUPH.js +140 -0
- package/docs/public/dist/caching.boot-DVR6KDE7.js +53 -0
- package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +11 -0
- package/docs/public/dist/components--alert.boot-GCEXOZAC.js +6 -0
- package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +6 -0
- package/docs/public/dist/components--avatar.boot-PSW24EVA.js +5 -0
- package/docs/public/dist/components--badge.boot-TYDY2RMK.js +7 -0
- package/docs/public/dist/components--banner.boot-EI5PZSZK.js +7 -0
- package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +34 -0
- package/docs/public/dist/components--button.boot-J54BQM2E.js +23 -0
- package/docs/public/dist/components--card.boot-PZGNDIB6.js +138 -0
- package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +12 -0
- package/docs/public/dist/components--charts.boot-2EOYQWKL.js +108 -0
- package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +54 -0
- package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +9 -0
- package/docs/public/dist/components--code-window.boot-2GR2DV33.js +20 -0
- package/docs/public/dist/components--container.boot-7LOOGK2K.js +5 -0
- package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +11 -0
- package/docs/public/dist/components--divider.boot-3NI2C3QG.js +6 -0
- package/docs/public/dist/components--empty.boot-YX2UR3PV.js +7 -0
- package/docs/public/dist/components--feature.boot-MUD7NSUO.js +13 -0
- package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +19 -0
- package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +52 -0
- package/docs/public/dist/components--footer.boot-EYUK5FRG.js +14 -0
- package/docs/public/dist/components--grid.boot-URDQVDDR.js +59 -0
- package/docs/public/dist/components--heading.boot-BPQKU43E.js +44 -0
- package/docs/public/dist/components--hero.boot-4RAPRGAB.js +17 -0
- package/docs/public/dist/components--icons.boot-ZITNU5JP.js +68 -0
- package/docs/public/dist/components--image.boot-XEEGHQZF.js +19 -0
- package/docs/public/dist/components--input.boot-SGASZG5K.js +7 -0
- package/docs/public/dist/components--list.boot-W3XC5MHD.js +55 -0
- package/docs/public/dist/components--media.boot-5VFIETZO.js +13 -0
- package/docs/public/dist/components--modal.boot-RZUYXBN2.js +47 -0
- package/docs/public/dist/components--nav.boot-ODBOHU7O.js +33 -0
- package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +21 -0
- package/docs/public/dist/components--progress.boot-GHAGYZOK.js +30 -0
- package/docs/public/dist/components--prose.boot-QANJL6JI.js +67 -0
- package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +22 -0
- package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +75 -0
- package/docs/public/dist/components--rating.boot-QBAN6DEL.js +38 -0
- package/docs/public/dist/components--search.boot-PXH5O5AG.js +17 -0
- package/docs/public/dist/components--section.boot-AQGIYHWW.js +12 -0
- package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +33 -0
- package/docs/public/dist/components--select.boot-47X5RHOC.js +10 -0
- package/docs/public/dist/components--slider.boot-PSRRX7XL.js +47 -0
- package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +22 -0
- package/docs/public/dist/components--stack.boot-DI4NJXBF.js +9 -0
- package/docs/public/dist/components--stat.boot-QMFUWBQT.js +9 -0
- package/docs/public/dist/components--stepper.boot-34PP2NEV.js +22 -0
- package/docs/public/dist/components--table.boot-FCQGSFIQ.js +11 -0
- package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +11 -0
- package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +4 -0
- package/docs/public/dist/components--timeline.boot-26LN52P2.js +95 -0
- package/docs/public/dist/components--toggle.boot-IQQEI76S.js +29 -0
- package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +9 -0
- package/docs/public/dist/components.boot-SE6PQ4P7.js +103 -0
- package/docs/public/dist/config.boot-DTRRWUE6.js +126 -0
- package/docs/public/dist/constraints.boot-DUHDZBMC.js +71 -0
- package/docs/public/dist/deploy.boot-SLAD3NI2.js +163 -0
- package/docs/public/dist/docs-8e3d4b5c.css +1 -0
- package/docs/public/dist/extending.boot-UA3CN243.js +159 -0
- package/docs/public/dist/faq.boot-6EQAWLQR.js +43 -0
- package/docs/public/dist/getting-started.boot-TDKIFL5U.js +86 -0
- package/docs/public/dist/guard.boot-AUHAWTG4.js +80 -0
- package/docs/public/dist/home.boot-BVQXRH32.js +383 -0
- package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +104 -0
- package/docs/public/dist/hydration.boot-JRM6IPJL.js +78 -0
- package/docs/public/dist/images.boot-M6ZVKTZS.js +80 -0
- package/docs/public/dist/manifest.json +94 -0
- package/docs/public/dist/meta.boot-7NXGPHR4.js +79 -0
- package/docs/public/dist/mutations.boot-F6F43UDX.js +79 -0
- package/docs/public/dist/navigation.boot-AOXWS3ZF.js +57 -0
- package/docs/public/dist/performance.boot-C3UPCOBK.js +98 -0
- package/docs/public/dist/persist.boot-WT32PQOQ.js +61 -0
- package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +63 -0
- package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +31 -0
- package/docs/public/dist/pulse-ui-81a85c03.css +1 -0
- package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +104 -0
- package/docs/public/dist/routing.boot-FNX5FDGH.js +70 -0
- package/docs/public/dist/runtime-B73WLANC.js +1 -0
- package/docs/public/dist/runtime-KO4BHUQ3.js +49 -0
- package/docs/public/dist/runtime-L2HNXIHW.js +59 -0
- package/docs/public/dist/runtime-QFURDKA2.js +5 -0
- package/docs/public/dist/runtime-UVPXO4IR.js +375 -0
- package/docs/public/dist/runtime-VMJA3Z4N.js +10 -0
- package/docs/public/dist/runtime-ZJ4FXT5O.js +11 -0
- package/docs/public/dist/server-api.boot-K7X3LCFB.js +219 -0
- package/docs/public/dist/server-data.boot-Y7HQYC4R.js +157 -0
- package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +26 -0
- package/docs/public/dist/spec.boot-2WU7ZHCV.js +159 -0
- package/docs/public/dist/state.boot-B24GUE3R.js +73 -0
- package/docs/public/dist/store.boot-TLIB4XHH.js +150 -0
- package/docs/public/dist/streaming.boot-W2DZSMW4.js +80 -0
- package/docs/public/dist/stripe.boot-QN3C2GEL.js +164 -0
- package/docs/public/dist/supabase.boot-BG4XXLZE.js +303 -0
- package/docs/public/dist/testing.boot-6U4WKMTE.js +130 -0
- package/docs/public/dist/validation.boot-PQHYGW5B.js +100 -0
- package/docs/public/docs.css +2020 -0
- package/docs/public/menu.js +83 -0
- package/docs/public/pulse-ui.css +2739 -0
- package/docs/public/pulse-ui.js +236 -0
- package/docs/server.js +192 -0
- package/docs/src/lib/component-page.js +47 -0
- package/docs/src/lib/highlight.js +255 -0
- package/docs/src/lib/layout.js +131 -0
- package/docs/src/lib/metrics-store.js +6 -0
- package/docs/src/lib/nav.js +159 -0
- package/docs/src/lib/stats.js +81 -0
- package/docs/src/pages/accessibility.js +157 -0
- package/docs/src/pages/actions.js +191 -0
- package/docs/src/pages/auth.js +177 -0
- package/docs/src/pages/caching.js +95 -0
- package/docs/src/pages/components/accordion.js +48 -0
- package/docs/src/pages/components/alert.js +35 -0
- package/docs/src/pages/components/app-badge.js +41 -0
- package/docs/src/pages/components/avatar.js +35 -0
- package/docs/src/pages/components/badge.js +36 -0
- package/docs/src/pages/components/banner.js +45 -0
- package/docs/src/pages/components/breadcrumbs.js +94 -0
- package/docs/src/pages/components/button.js +84 -0
- package/docs/src/pages/components/card.js +225 -0
- package/docs/src/pages/components/carousel.js +72 -0
- package/docs/src/pages/components/charts.js +278 -0
- package/docs/src/pages/components/checkbox.js +129 -0
- package/docs/src/pages/components/cluster.js +47 -0
- package/docs/src/pages/components/code-window.js +57 -0
- package/docs/src/pages/components/container.js +40 -0
- package/docs/src/pages/components/cta.js +53 -0
- package/docs/src/pages/components/divider.js +37 -0
- package/docs/src/pages/components/empty.js +36 -0
- package/docs/src/pages/components/feature.js +60 -0
- package/docs/src/pages/components/fieldset.js +65 -0
- package/docs/src/pages/components/fileupload.js +127 -0
- package/docs/src/pages/components/footer.js +58 -0
- package/docs/src/pages/components/grid.js +165 -0
- package/docs/src/pages/components/heading.js +107 -0
- package/docs/src/pages/components/hero.js +65 -0
- package/docs/src/pages/components/icons.js +285 -0
- package/docs/src/pages/components/image.js +71 -0
- package/docs/src/pages/components/input.js +51 -0
- package/docs/src/pages/components/list.js +112 -0
- package/docs/src/pages/components/media.js +51 -0
- package/docs/src/pages/components/modal.js +111 -0
- package/docs/src/pages/components/nav.js +86 -0
- package/docs/src/pages/components/pricing.js +68 -0
- package/docs/src/pages/components/progress.js +102 -0
- package/docs/src/pages/components/prose.js +111 -0
- package/docs/src/pages/components/pullquote.js +71 -0
- package/docs/src/pages/components/radio.js +194 -0
- package/docs/src/pages/components/rating.js +106 -0
- package/docs/src/pages/components/search.js +61 -0
- package/docs/src/pages/components/section.js +59 -0
- package/docs/src/pages/components/segmented.js +121 -0
- package/docs/src/pages/components/select.js +45 -0
- package/docs/src/pages/components/slider.js +114 -0
- package/docs/src/pages/components/spinner.js +73 -0
- package/docs/src/pages/components/stack.js +48 -0
- package/docs/src/pages/components/stat.js +55 -0
- package/docs/src/pages/components/stepper.js +66 -0
- package/docs/src/pages/components/table.js +45 -0
- package/docs/src/pages/components/testimonial.js +49 -0
- package/docs/src/pages/components/textarea.js +31 -0
- package/docs/src/pages/components/timeline.js +227 -0
- package/docs/src/pages/components/toggle.js +84 -0
- package/docs/src/pages/components/tooltip.js +48 -0
- package/docs/src/pages/components.js +204 -0
- package/docs/src/pages/config.js +193 -0
- package/docs/src/pages/constraints.js +99 -0
- package/docs/src/pages/deploy.js +233 -0
- package/docs/src/pages/extending.js +198 -0
- package/docs/src/pages/faq.js +96 -0
- package/docs/src/pages/getting-started.js +106 -0
- package/docs/src/pages/guard.js +121 -0
- package/docs/src/pages/home.js +401 -0
- package/docs/src/pages/how-it-works.js +183 -0
- package/docs/src/pages/hydration.js +98 -0
- package/docs/src/pages/images.js +121 -0
- package/docs/src/pages/meta.js +120 -0
- package/docs/src/pages/mutations.js +106 -0
- package/docs/src/pages/navigation.js +85 -0
- package/docs/src/pages/performance.js +157 -0
- package/docs/src/pages/persist.js +88 -0
- package/docs/src/pages/project-structure.js +90 -0
- package/docs/src/pages/prompt-examples.js +186 -0
- package/docs/src/pages/raw-responses.js +124 -0
- package/docs/src/pages/routing.js +99 -0
- package/docs/src/pages/server-api.js +281 -0
- package/docs/src/pages/server-data.js +185 -0
- package/docs/src/pages/slash-commands.js +55 -0
- package/docs/src/pages/spec.js +207 -0
- package/docs/src/pages/state.js +101 -0
- package/docs/src/pages/store.js +181 -0
- package/docs/src/pages/streaming.js +108 -0
- package/docs/src/pages/stripe.js +193 -0
- package/docs/src/pages/supabase.js +323 -0
- package/docs/src/pages/testing.js +198 -0
- package/docs/src/pages/validation.js +138 -0
- package/examples/contact.js +166 -0
- package/examples/counter.js +94 -0
- package/examples/dev.server.js +91 -0
- package/examples/examples.test.js +394 -0
- package/examples/pricing.js +244 -0
- package/examples/products.js +191 -0
- package/examples/quiz.js +208 -0
- package/examples/shared.js +78 -0
- package/examples/todos.js +162 -0
- package/package.json +75 -0
- package/public/.pulse-ui-version +1 -0
- package/public/chippy-bird.css +246 -0
- package/public/examples/contact.css +119 -0
- package/public/examples/counter.css +79 -0
- package/public/examples/pricing.css +132 -0
- package/public/examples/products.css +100 -0
- package/public/examples/quiz.css +200 -0
- package/public/examples/todos.css +137 -0
- package/public/favicon.ico +0 -0
- package/public/log-dashboard.css +383 -0
- package/public/pulse-ui.css +2740 -0
- package/public/pulse-ui.js +236 -0
- package/public/pulse.css +149 -0
- package/scripts/build.js +411 -0
- package/src/agent/checklist.md +111 -0
- package/src/agent/coverage-check.js +66 -0
- package/src/agent/guide-components.md +274 -0
- package/src/agent/guide-examples.md +54 -0
- package/src/agent/guide-routing.md +36 -0
- package/src/agent/guide-server.md +258 -0
- package/src/agent/guide-spec.md +103 -0
- package/src/agent/guide-styles.md +191 -0
- package/src/agent/guide.md +979 -0
- package/src/agent/identity.md +106 -0
- package/src/agent/workflow.md +108 -0
- package/src/cli/cli.test.js +82 -0
- package/src/cli/dev.js +195 -0
- package/src/cli/discover.js +113 -0
- package/src/cli/index.js +361 -0
- package/src/cli/load-report.js +91 -0
- package/src/cli/load-runner.js +121 -0
- package/src/cli/report-server.js +723 -0
- package/src/cli/report.js +116 -0
- package/src/cli/scaffold.archive.js +1371 -0
- package/src/cli/scaffold.js +349 -0
- package/src/cli/start.js +74 -0
- package/src/html.js +19 -0
- package/src/mcp/server.js +884 -0
- package/src/mcp/validate-worker.js +110 -0
- package/src/runtime/image.js +74 -0
- package/src/runtime/image.test.js +111 -0
- package/src/runtime/index.js +621 -0
- package/src/runtime/navigate.js +146 -0
- package/src/runtime/runtime.test.js +773 -0
- package/src/runtime/ssr.js +464 -0
- package/src/runtime/ssr.test.js +421 -0
- package/src/runtime/store.js +92 -0
- package/src/runtime/toast.js +163 -0
- package/src/server/index.js +1386 -0
- package/src/server/server.test.js +1248 -0
- package/src/spec/schema.js +428 -0
- package/src/spec/schema.test.js +291 -0
- package/src/store/index.js +102 -0
- package/src/store/store.test.js +210 -0
- package/src/testing/html.js +283 -0
- package/src/testing/index.js +249 -0
- package/src/testing/testing.test.js +450 -0
- package/src/ui/accordion.js +28 -0
- package/src/ui/alert.js +43 -0
- package/src/ui/app-badge.js +48 -0
- package/src/ui/avatar.js +47 -0
- package/src/ui/badge.js +24 -0
- package/src/ui/banner.js +26 -0
- package/src/ui/breadcrumbs.js +38 -0
- package/src/ui/button.js +66 -0
- package/src/ui/card.js +34 -0
- package/src/ui/carousel.js +59 -0
- package/src/ui/charts.js +321 -0
- package/src/ui/checkbox.js +65 -0
- package/src/ui/cluster.js +44 -0
- package/src/ui/code-window.js +39 -0
- package/src/ui/container.js +24 -0
- package/src/ui/cta.js +37 -0
- package/src/ui/divider.js +29 -0
- package/src/ui/empty.js +33 -0
- package/src/ui/feature.js +33 -0
- package/src/ui/fieldset.js +37 -0
- package/src/ui/fileupload.js +89 -0
- package/src/ui/footer.js +38 -0
- package/src/ui/grid.js +36 -0
- package/src/ui/heading.js +45 -0
- package/src/ui/hero.js +37 -0
- package/src/ui/icons.js +161 -0
- package/src/ui/index.js +89 -0
- package/src/ui/input.js +74 -0
- package/src/ui/list.js +36 -0
- package/src/ui/media.js +44 -0
- package/src/ui/modal.js +80 -0
- package/src/ui/nav.js +61 -0
- package/src/ui/pricing.js +56 -0
- package/src/ui/progress.js +62 -0
- package/src/ui/prose.js +29 -0
- package/src/ui/pullquote.js +34 -0
- package/src/ui/radio.js +102 -0
- package/src/ui/rating.js +93 -0
- package/src/ui/search.js +77 -0
- package/src/ui/section.js +69 -0
- package/src/ui/segmented.js +50 -0
- package/src/ui/select.js +77 -0
- package/src/ui/slider.js +84 -0
- package/src/ui/spinner.js +34 -0
- package/src/ui/stack.js +36 -0
- package/src/ui/stat.js +52 -0
- package/src/ui/stepper.js +46 -0
- package/src/ui/switch.js +57 -0
- package/src/ui/table.js +45 -0
- package/src/ui/testimonial.js +48 -0
- package/src/ui/textarea.js +72 -0
- package/src/ui/timeline.js +72 -0
- package/src/ui/tooltip.js +28 -0
- package/src/ui/ui.test.js +1241 -0
- package/src/ui/uiimage.js +65 -0
- package/tsconfig.json +13 -0
- package/types/html.d.ts +17 -0
- package/types/image.d.ts +70 -0
- package/types/index.d.ts +7 -0
- package/types/navigate.d.ts +38 -0
- package/types/runtime.d.ts +63 -0
- package/types/schema.d.ts +243 -0
- package/types/server.d.ts +145 -0
- package/types/ssr.d.ts +110 -0
- package/types/testing.d.ts +154 -0
- 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)
|