From 4f2a8d6ab59a946d04f2d73798985b473b272132 Mon Sep 17 00:00:00 2001
From: Jake Shore
Date: Fri, 6 Feb 2026 06:27:05 -0500
Subject: [PATCH] =?UTF-8?q?GHL=20MCP=20full=20update=20=E2=80=94=202026-02?=
=?UTF-8?q?-06?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
=== DONE ===
- MCP Apps UI system added (11 apps with _meta.ui.resourceUri)
- 19 new tool modules added
- Tool count: 269 → 461 across 38 categories
- Upstream changes merged
- All tools tagged with _meta labels
- Package lock updated
=== TO-DO ===
- [ ] Fix 42 failing edge case tests (BLOCKER — Stage 11)
- [ ] Live API testing with GHL credentials
- [ ] App design approval for Stage 7→8
---
AGENT-TASKS.md | 177 +
REACT-REWRITE-PLAN.md | 263 ++
REACT-STATE-ANALYSIS.md | 369 ++
docs/FALLBACK-ARCHITECTURE.md | 639 ++++
package-lock.json | 87 +-
package.json | 4 +-
src/apps/index.ts | 1168 +++---
src/apps/templates/agent-stats.template.ts | 106 +
src/apps/templates/calendar-view.template.ts | 57 +
src/apps/templates/campaign-stats.template.ts | 118 +
src/apps/templates/contact-grid.template.ts | 62 +
.../templates/contact-timeline.template.ts | 127 +
src/apps/templates/dashboard.template.ts | 122 +
src/apps/templates/index.ts | 11 +
.../templates/invoice-preview.template.ts | 112 +
.../templates/opportunity-card.template.ts | 135 +
src/apps/templates/pipeline-board.template.ts | 67 +
src/apps/templates/quick-book.template.ts | 91 +
.../templates/workflow-status.template.ts | 120 +
src/apps/types.ts | 397 ++
src/tools/affiliates-tools.ts | 138 +-
src/tools/association-tools.ts | 88 +-
src/tools/blog-tools.ts | 55 +-
src/tools/businesses-tools.ts | 43 +-
src/tools/calendar-tools.ts | 343 +-
src/tools/campaigns-tools.ts | 104 +-
src/tools/companies-tools.ts | 43 +-
src/tools/contact-tools.ts | 217 ++
src/tools/conversation-tools.ts | 176 +-
src/tools/courses-tools.ts | 280 +-
src/tools/custom-field-v2-tools.ts | 72 +-
src/tools/email-isv-tools.ts | 9 +-
src/tools/email-tools.ts | 41 +-
src/tools/forms-tools.ts | 25 +-
src/tools/funnels-tools.ts | 70 +-
src/tools/invoices-tools.ts | 151 +-
src/tools/links-tools.ts | 43 +-
src/tools/location-tools.ts | 205 +-
src/tools/media-tools.ts | 27 +-
src/tools/oauth-tools.ts | 82 +-
src/tools/object-tools.ts | 81 +-
src/tools/opportunity-tools.ts | 86 +-
src/tools/payments-tools.ts | 180 +-
src/tools/phone-tools.ts | 168 +-
src/tools/products-tools.ts | 90 +-
src/tools/reporting-tools.ts | 106 +-
src/tools/reputation-tools.ts | 110 +-
src/tools/saas-tools.ts | 54 +-
src/tools/smartlists-tools.ts | 70 +-
src/tools/snapshots-tools.ts | 54 +-
src/tools/social-media-tools.ts | 145 +-
src/tools/store-tools.ts | 149 +-
src/tools/survey-tools.ts | 18 +-
src/tools/templates-tools.ts | 152 +-
src/tools/triggers-tools.ts | 95 +-
src/tools/users-tools.ts | 43 +-
src/tools/webhooks-tools.ts | 77 +-
src/tools/workflow-tools.ts | 9 +-
src/ui/json-render-app/index.html | 12 +
src/ui/json-render-app/package-lock.json | 1637 +++++++++
src/ui/json-render-app/package.json | 16 +
src/ui/json-render-app/src/charts.ts | 250 ++
src/ui/json-render-app/src/components.ts | 1224 +++++++
src/ui/json-render-app/src/main.ts | 479 +++
src/ui/json-render-app/src/styles.ts | 973 +++++
src/ui/json-render-app/tsconfig.json | 13 +
src/ui/json-render-app/vite.config.ts | 32 +
src/ui/react-app/index.html | 12 +
src/ui/react-app/package-lock.json | 3190 +++++++++++++++++
src/ui/react-app/package.json | 25 +
src/ui/react-app/src/App.tsx | 211 ++
.../src/apps/affiliate-dashboard/App.tsx | 149 +
.../src/apps/affiliate-dashboard/index.html | 5 +
.../src/apps/affiliate-dashboard/main.tsx | 4 +
.../apps/affiliate-dashboard/vite.config.ts | 22 +
src/ui/react-app/src/apps/agent-stats/App.tsx | 226 ++
.../react-app/src/apps/agent-stats/index.html | 12 +
.../react-app/src/apps/agent-stats/main.tsx | 9 +
.../src/apps/agent-stats/vite.config.ts | 22 +
.../src/apps/appointment-booker/App.tsx | 93 +
.../src/apps/appointment-booker/index.html | 5 +
.../src/apps/appointment-booker/main.tsx | 9 +
.../apps/appointment-booker/vite.config.ts | 22 +
.../src/apps/appointment-detail/App.tsx | 177 +
.../src/apps/appointment-detail/index.html | 5 +
.../src/apps/appointment-detail/main.tsx | 9 +
.../apps/appointment-detail/vite.config.ts | 22 +
.../react-app/src/apps/blog-manager/App.tsx | 320 ++
.../src/apps/blog-manager/index.html | 5 +
.../react-app/src/apps/blog-manager/main.tsx | 9 +
.../src/apps/blog-manager/vite.config.ts | 22 +
.../src/apps/calendar-resources/App.tsx | 91 +
.../src/apps/calendar-resources/index.html | 5 +
.../src/apps/calendar-resources/main.tsx | 9 +
.../apps/calendar-resources/vite.config.ts | 22 +
.../react-app/src/apps/calendar-view/App.tsx | 208 ++
.../src/apps/calendar-view/index.html | 5 +
.../react-app/src/apps/calendar-view/main.tsx | 9 +
.../src/apps/calendar-view/vite.config.ts | 22 +
src/ui/react-app/src/apps/call-detail/App.tsx | 141 +
.../react-app/src/apps/call-detail/index.html | 5 +
.../react-app/src/apps/call-detail/main.tsx | 4 +
.../src/apps/call-detail/vite.config.ts | 22 +
src/ui/react-app/src/apps/call-log/App.tsx | 146 +
src/ui/react-app/src/apps/call-log/index.html | 5 +
src/ui/react-app/src/apps/call-log/main.tsx | 4 +
.../src/apps/call-log/vite.config.ts | 22 +
.../react-app/src/apps/campaign-stats/App.tsx | 186 +
.../src/apps/campaign-stats/index.html | 5 +
.../src/apps/campaign-stats/main.tsx | 9 +
.../src/apps/campaign-stats/vite.config.ts | 22 +
.../react-app/src/apps/company-detail/App.tsx | 119 +
.../src/apps/company-detail/index.html | 5 +
.../src/apps/company-detail/main.tsx | 4 +
.../src/apps/company-detail/vite.config.ts | 22 +
.../react-app/src/apps/company-list/App.tsx | 105 +
.../src/apps/company-list/index.html | 5 +
.../react-app/src/apps/company-list/main.tsx | 4 +
.../src/apps/company-list/vite.config.ts | 22 +
.../react-app/src/apps/contact-card/App.tsx | 125 +
.../src/apps/contact-card/index.html | 5 +
.../react-app/src/apps/contact-card/main.tsx | 4 +
.../src/apps/contact-card/vite.config.ts | 22 +
.../src/apps/contact-creator/App.tsx | 163 +
.../src/apps/contact-creator/index.html | 5 +
.../src/apps/contact-creator/main.tsx | 4 +
.../src/apps/contact-creator/vite.config.ts | 22 +
.../react-app/src/apps/contact-grid/App.tsx | 90 +
.../src/apps/contact-grid/index.html | 5 +
.../react-app/src/apps/contact-grid/main.tsx | 4 +
.../src/apps/contact-grid/vite.config.ts | 22 +
.../src/apps/contact-timeline/App.tsx | 152 +
.../src/apps/contact-timeline/index.html | 5 +
.../src/apps/contact-timeline/main.tsx | 4 +
.../src/apps/contact-timeline/vite.config.ts | 22 +
.../src/apps/conversation-list/App.tsx | 164 +
.../src/apps/conversation-list/index.html | 5 +
.../src/apps/conversation-list/main.tsx | 9 +
.../src/apps/conversation-list/vite.config.ts | 22 +
.../src/apps/conversation-thread/App.tsx | 113 +
.../src/apps/conversation-thread/index.html | 5 +
.../src/apps/conversation-thread/main.tsx | 9 +
.../apps/conversation-thread/vite.config.ts | 22 +
.../react-app/src/apps/coupon-manager/App.tsx | 238 ++
.../src/apps/coupon-manager/index.html | 14 +
.../src/apps/coupon-manager/main.tsx | 4 +
.../src/apps/coupon-manager/vite.config.ts | 22 +
.../react-app/src/apps/course-catalog/App.tsx | 125 +
.../src/apps/course-catalog/index.html | 5 +
.../src/apps/course-catalog/main.tsx | 4 +
.../src/apps/course-catalog/vite.config.ts | 22 +
.../react-app/src/apps/course-detail/App.tsx | 155 +
.../src/apps/course-detail/index.html | 5 +
.../react-app/src/apps/course-detail/main.tsx | 4 +
.../src/apps/course-detail/vite.config.ts | 22 +
.../src/apps/custom-fields-manager/App.tsx | 341 ++
.../src/apps/custom-fields-manager/index.html | 12 +
.../src/apps/custom-fields-manager/main.tsx | 9 +
.../apps/custom-fields-manager/vite.config.ts | 22 +
.../src/apps/duplicate-checker/App.tsx | 107 +
.../src/apps/duplicate-checker/index.html | 5 +
.../src/apps/duplicate-checker/main.tsx | 4 +
.../src/apps/duplicate-checker/vite.config.ts | 22 +
.../src/apps/email-template-preview/App.tsx | 170 +
.../apps/email-template-preview/index.html | 5 +
.../src/apps/email-template-preview/main.tsx | 9 +
.../email-template-preview/vite.config.ts | 22 +
.../src/apps/estimate-builder/App.tsx | 147 +
.../src/apps/estimate-builder/index.html | 14 +
.../src/apps/estimate-builder/main.tsx | 4 +
.../src/apps/estimate-builder/vite.config.ts | 22 +
.../src/apps/estimate-preview/App.tsx | 145 +
.../src/apps/estimate-preview/index.html | 14 +
.../src/apps/estimate-preview/main.tsx | 4 +
.../src/apps/estimate-preview/vite.config.ts | 22 +
src/ui/react-app/src/apps/form-list/App.tsx | 120 +
.../react-app/src/apps/form-list/index.html | 5 +
src/ui/react-app/src/apps/form-list/main.tsx | 4 +
.../src/apps/form-list/vite.config.ts | 22 +
.../src/apps/form-submissions/App.tsx | 113 +
.../src/apps/form-submissions/index.html | 5 +
.../src/apps/form-submissions/main.tsx | 4 +
.../src/apps/form-submissions/vite.config.ts | 22 +
.../src/apps/free-slots-finder/App.tsx | 146 +
.../src/apps/free-slots-finder/index.html | 5 +
.../src/apps/free-slots-finder/main.tsx | 9 +
.../src/apps/free-slots-finder/vite.config.ts | 22 +
.../react-app/src/apps/funnel-detail/App.tsx | 142 +
.../src/apps/funnel-detail/index.html | 5 +
.../react-app/src/apps/funnel-detail/main.tsx | 4 +
.../src/apps/funnel-detail/vite.config.ts | 22 +
src/ui/react-app/src/apps/funnel-list/App.tsx | 138 +
.../react-app/src/apps/funnel-list/index.html | 5 +
.../react-app/src/apps/funnel-list/main.tsx | 4 +
.../src/apps/funnel-list/vite.config.ts | 22 +
.../src/apps/inventory-dashboard/App.tsx | 176 +
.../src/apps/inventory-dashboard/index.html | 14 +
.../src/apps/inventory-dashboard/main.tsx | 4 +
.../apps/inventory-dashboard/vite.config.ts | 22 +
.../src/apps/invoice-builder/App.tsx | 78 +
.../src/apps/invoice-builder/index.html | 14 +
.../src/apps/invoice-builder/main.tsx | 4 +
.../src/apps/invoice-builder/vite.config.ts | 22 +
.../react-app/src/apps/invoice-list/App.tsx | 120 +
.../src/apps/invoice-list/index.html | 14 +
.../react-app/src/apps/invoice-list/main.tsx | 4 +
.../src/apps/invoice-list/vite.config.ts | 22 +
.../src/apps/invoice-preview/App.tsx | 172 +
.../src/apps/invoice-preview/index.html | 14 +
.../src/apps/invoice-preview/main.tsx | 4 +
.../src/apps/invoice-preview/vite.config.ts | 22 +
.../src/apps/location-dashboard/App.tsx | 253 ++
.../src/apps/location-dashboard/index.html | 12 +
.../src/apps/location-dashboard/main.tsx | 9 +
.../apps/location-dashboard/vite.config.ts | 22 +
.../react-app/src/apps/media-library/App.tsx | 135 +
.../src/apps/media-library/index.html | 5 +
.../react-app/src/apps/media-library/main.tsx | 4 +
.../src/apps/media-library/vite.config.ts | 22 +
.../src/apps/message-composer/App.tsx | 224 ++
.../src/apps/message-composer/index.html | 5 +
.../src/apps/message-composer/main.tsx | 9 +
.../src/apps/message-composer/vite.config.ts | 22 +
.../react-app/src/apps/message-detail/App.tsx | 180 +
.../src/apps/message-detail/index.html | 5 +
.../src/apps/message-detail/main.tsx | 9 +
.../src/apps/message-detail/vite.config.ts | 22 +
.../src/apps/opportunity-card/App.tsx | 146 +
.../src/apps/opportunity-card/index.html | 5 +
.../src/apps/opportunity-card/main.tsx | 4 +
.../src/apps/opportunity-card/vite.config.ts | 22 +
.../src/apps/opportunity-editor/App.tsx | 85 +
.../src/apps/opportunity-editor/index.html | 5 +
.../src/apps/opportunity-editor/main.tsx | 4 +
.../apps/opportunity-editor/vite.config.ts | 22 +
.../react-app/src/apps/order-detail/App.tsx | 288 ++
.../src/apps/order-detail/index.html | 14 +
.../react-app/src/apps/order-detail/main.tsx | 4 +
.../src/apps/order-detail/vite.config.ts | 22 +
src/ui/react-app/src/apps/order-list/App.tsx | 202 ++
.../react-app/src/apps/order-list/index.html | 14 +
src/ui/react-app/src/apps/order-list/main.tsx | 4 +
.../src/apps/order-list/vite.config.ts | 22 +
.../src/apps/pipeline-analytics/App.tsx | 151 +
.../src/apps/pipeline-analytics/index.html | 5 +
.../src/apps/pipeline-analytics/main.tsx | 4 +
.../apps/pipeline-analytics/vite.config.ts | 22 +
.../src/apps/pipeline-funnel/App.tsx | 309 ++
.../src/apps/pipeline-funnel/index.html | 12 +
.../src/apps/pipeline-funnel/main.tsx | 9 +
.../src/apps/pipeline-funnel/vite.config.ts | 22 +
.../src/apps/pipeline-kanban/App.tsx | 125 +
.../src/apps/pipeline-kanban/index.html | 5 +
.../src/apps/pipeline-kanban/main.tsx | 4 +
.../src/apps/pipeline-kanban/vite.config.ts | 22 +
.../src/apps/product-catalog/App.tsx | 126 +
.../src/apps/product-catalog/index.html | 14 +
.../src/apps/product-catalog/main.tsx | 4 +
.../src/apps/product-catalog/vite.config.ts | 22 +
.../react-app/src/apps/product-detail/App.tsx | 130 +
.../src/apps/product-detail/index.html | 14 +
.../src/apps/product-detail/main.tsx | 4 +
.../src/apps/product-detail/vite.config.ts | 22 +
.../src/apps/revenue-dashboard/App.tsx | 329 ++
.../src/apps/revenue-dashboard/index.html | 12 +
.../src/apps/revenue-dashboard/main.tsx | 9 +
.../src/apps/revenue-dashboard/vite.config.ts | 22 +
.../src/apps/reviews-dashboard/App.tsx | 289 ++
.../src/apps/reviews-dashboard/index.html | 12 +
.../src/apps/reviews-dashboard/main.tsx | 9 +
.../src/apps/reviews-dashboard/vite.config.ts | 22 +
.../src/apps/smartlist-viewer/App.tsx | 139 +
.../src/apps/smartlist-viewer/index.html | 5 +
.../src/apps/smartlist-viewer/main.tsx | 4 +
.../src/apps/smartlist-viewer/vite.config.ts | 22 +
.../src/apps/social-accounts/App.tsx | 174 +
.../src/apps/social-accounts/index.html | 5 +
.../src/apps/social-accounts/main.tsx | 9 +
.../src/apps/social-accounts/vite.config.ts | 22 +
.../src/apps/social-calendar/App.tsx | 280 ++
.../src/apps/social-calendar/index.html | 5 +
.../src/apps/social-calendar/main.tsx | 9 +
.../src/apps/social-calendar/vite.config.ts | 22 +
.../src/apps/social-post-composer/App.tsx | 253 ++
.../src/apps/social-post-composer/index.html | 5 +
.../src/apps/social-post-composer/main.tsx | 9 +
.../apps/social-post-composer/vite.config.ts | 22 +
.../src/apps/subscription-manager/App.tsx | 179 +
.../src/apps/subscription-manager/index.html | 14 +
.../src/apps/subscription-manager/main.tsx | 4 +
.../apps/subscription-manager/vite.config.ts | 22 +
src/ui/react-app/src/apps/survey-list/App.tsx | 111 +
.../react-app/src/apps/survey-list/index.html | 5 +
.../react-app/src/apps/survey-list/main.tsx | 4 +
.../src/apps/survey-list/vite.config.ts | 22 +
.../src/apps/survey-submissions/App.tsx | 134 +
.../src/apps/survey-submissions/index.html | 5 +
.../src/apps/survey-submissions/main.tsx | 4 +
.../apps/survey-submissions/vite.config.ts | 22 +
.../react-app/src/apps/tags-manager/App.tsx | 302 ++
.../src/apps/tags-manager/index.html | 12 +
.../react-app/src/apps/tags-manager/main.tsx | 9 +
.../src/apps/tags-manager/vite.config.ts | 22 +
src/ui/react-app/src/apps/task-board/App.tsx | 201 ++
.../react-app/src/apps/task-board/index.html | 5 +
src/ui/react-app/src/apps/task-board/main.tsx | 4 +
.../src/apps/task-board/vite.config.ts | 22 +
.../src/apps/team-management/App.tsx | 136 +
.../src/apps/team-management/index.html | 5 +
.../src/apps/team-management/main.tsx | 4 +
.../src/apps/team-management/vite.config.ts | 22 +
.../src/apps/template-library/App.tsx | 133 +
.../src/apps/template-library/index.html | 5 +
.../src/apps/template-library/main.tsx | 4 +
.../src/apps/template-library/vite.config.ts | 22 +
.../src/apps/transaction-list/App.tsx | 118 +
.../src/apps/transaction-list/index.html | 14 +
.../src/apps/transaction-list/main.tsx | 4 +
.../src/apps/transaction-list/vite.config.ts | 22 +
src/ui/react-app/src/apps/user-detail/App.tsx | 150 +
.../react-app/src/apps/user-detail/index.html | 5 +
.../react-app/src/apps/user-detail/main.tsx | 4 +
.../src/apps/user-detail/vite.config.ts | 22 +
.../src/apps/workflow-detail/App.tsx | 298 ++
.../src/apps/workflow-detail/index.html | 5 +
.../src/apps/workflow-detail/main.tsx | 9 +
.../src/apps/workflow-detail/vite.config.ts | 22 +
.../src/apps/workflow-status/App.tsx | 193 +
.../src/apps/workflow-status/index.html | 5 +
.../src/apps/workflow-status/main.tsx | 9 +
.../src/apps/workflow-status/vite.config.ts | 22 +
.../src/components/charts/BarChart.tsx | 113 +
.../src/components/charts/FunnelChart.tsx | 56 +
.../src/components/charts/LineChart.tsx | 136 +
.../src/components/charts/PieChart.tsx | 93 +
.../src/components/charts/SparklineChart.tsx | 52 +
.../src/components/comms/ChatThread.tsx | 122 +
.../src/components/comms/ContentPreview.tsx | 66 +
.../src/components/comms/EmailPreview.tsx | 66 +
.../src/components/comms/TranscriptView.tsx | 79 +
.../src/components/data/AudioPlayer.tsx | 52 +
.../src/components/data/AvatarGroup.tsx | 79 +
.../src/components/data/CardGrid.tsx | 57 +
.../src/components/data/ChecklistView.tsx | 150 +
.../src/components/data/CurrencyDisplay.tsx | 41 +
.../src/components/data/DataTable.tsx | 187 +
.../src/components/data/DetailHeader.tsx | 31 +
.../src/components/data/InfoBlock.tsx | 20 +
.../src/components/data/KanbanBoard.tsx | 229 ++
.../src/components/data/KeyValueList.tsx | 35 +
.../src/components/data/LineItemsTable.tsx | 46 +
.../src/components/data/MetricCard.tsx | 30 +
.../src/components/data/ProgressBar.tsx | 51 +
.../src/components/data/StarRating.tsx | 59 +
.../src/components/data/StatusBadge.tsx | 21 +
.../src/components/data/StockIndicator.tsx | 38 +
.../react-app/src/components/data/TagList.tsx | 48 +
.../src/components/data/Timeline.tsx | 35 +
.../components/interactive/AmountInput.tsx | 89 +
.../interactive/AppointmentBooker.tsx | 274 ++
.../components/interactive/ContactPicker.tsx | 181 +
.../components/interactive/EditableField.tsx | 116 +
.../src/components/interactive/FormGroup.tsx | 138 +
.../components/interactive/InvoiceBuilder.tsx | 266 ++
.../interactive/OpportunityEditor.tsx | 151 +
.../components/interactive/SelectDropdown.tsx | 107 +
.../react-app/src/components/layout/Card.tsx | 29 +
.../src/components/layout/PageHeader.tsx | 64 +
.../src/components/layout/Section.tsx | 20 +
.../src/components/layout/SplitLayout.tsx | 29 +
.../src/components/layout/StatsGrid.tsx | 14 +
.../src/components/shared/ActionBar.tsx | 16 +
.../src/components/shared/ActionButton.tsx | 66 +
.../src/components/shared/FilterChips.tsx | 46 +
.../react-app/src/components/shared/Modal.tsx | 65 +
.../src/components/shared/SaveIndicator.tsx | 76 +
.../src/components/shared/SearchBar.tsx | 43 +
.../src/components/shared/TabGroup.tsx | 44 +
.../react-app/src/components/shared/Toast.tsx | 141 +
.../src/components/viz/CalendarView.tsx | 158 +
.../src/components/viz/DuplicateCompare.tsx | 56 +
.../src/components/viz/FlowDiagram.tsx | 113 +
.../src/components/viz/MediaGallery.tsx | 77 +
.../react-app/src/components/viz/TreeView.tsx | 65 +
.../src/context/ChangeTrackerContext.tsx | 109 +
.../react-app/src/context/MCPAppContext.tsx | 94 +
src/ui/react-app/src/hooks/useAutoSave.ts | 61 +
src/ui/react-app/src/hooks/useCallTool.ts | 56 +
.../react-app/src/hooks/useChangeTracker.ts | 26 +
src/ui/react-app/src/hooks/useFormState.ts | 42 +
.../src/hooks/useHostCapabilities.ts | 40 +
src/ui/react-app/src/hooks/useSizeReporter.ts | 48 +
src/ui/react-app/src/hooks/useSmartAction.ts | 102 +
src/ui/react-app/src/main.tsx | 12 +
.../react-app/src/renderer/UITreeRenderer.tsx | 81 +
src/ui/react-app/src/renderer/registry.ts | 174 +
src/ui/react-app/src/styles/base.css | 897 +++++
src/ui/react-app/src/styles/interactive.css | 275 ++
src/ui/react-app/src/types.ts | 751 ++++
src/ui/react-app/src/utils/mergeUITrees.ts | 71 +
src/ui/react-app/tsconfig.json | 22 +
src/ui/react-app/vite.config.ts | 31 +
tsconfig.json | 2 +-
403 files changed, 36141 insertions(+), 869 deletions(-)
create mode 100644 AGENT-TASKS.md
create mode 100644 REACT-REWRITE-PLAN.md
create mode 100644 REACT-STATE-ANALYSIS.md
create mode 100644 docs/FALLBACK-ARCHITECTURE.md
create mode 100644 src/apps/templates/agent-stats.template.ts
create mode 100644 src/apps/templates/calendar-view.template.ts
create mode 100644 src/apps/templates/campaign-stats.template.ts
create mode 100644 src/apps/templates/contact-grid.template.ts
create mode 100644 src/apps/templates/contact-timeline.template.ts
create mode 100644 src/apps/templates/dashboard.template.ts
create mode 100644 src/apps/templates/index.ts
create mode 100644 src/apps/templates/invoice-preview.template.ts
create mode 100644 src/apps/templates/opportunity-card.template.ts
create mode 100644 src/apps/templates/pipeline-board.template.ts
create mode 100644 src/apps/templates/quick-book.template.ts
create mode 100644 src/apps/templates/workflow-status.template.ts
create mode 100644 src/apps/types.ts
create mode 100644 src/ui/json-render-app/index.html
create mode 100644 src/ui/json-render-app/package-lock.json
create mode 100644 src/ui/json-render-app/package.json
create mode 100644 src/ui/json-render-app/src/charts.ts
create mode 100644 src/ui/json-render-app/src/components.ts
create mode 100644 src/ui/json-render-app/src/main.ts
create mode 100644 src/ui/json-render-app/src/styles.ts
create mode 100644 src/ui/json-render-app/tsconfig.json
create mode 100644 src/ui/json-render-app/vite.config.ts
create mode 100644 src/ui/react-app/index.html
create mode 100644 src/ui/react-app/package-lock.json
create mode 100644 src/ui/react-app/package.json
create mode 100644 src/ui/react-app/src/App.tsx
create mode 100644 src/ui/react-app/src/apps/affiliate-dashboard/App.tsx
create mode 100644 src/ui/react-app/src/apps/affiliate-dashboard/index.html
create mode 100644 src/ui/react-app/src/apps/affiliate-dashboard/main.tsx
create mode 100644 src/ui/react-app/src/apps/affiliate-dashboard/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/agent-stats/App.tsx
create mode 100644 src/ui/react-app/src/apps/agent-stats/index.html
create mode 100644 src/ui/react-app/src/apps/agent-stats/main.tsx
create mode 100644 src/ui/react-app/src/apps/agent-stats/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/appointment-booker/App.tsx
create mode 100644 src/ui/react-app/src/apps/appointment-booker/index.html
create mode 100644 src/ui/react-app/src/apps/appointment-booker/main.tsx
create mode 100644 src/ui/react-app/src/apps/appointment-booker/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/appointment-detail/App.tsx
create mode 100644 src/ui/react-app/src/apps/appointment-detail/index.html
create mode 100644 src/ui/react-app/src/apps/appointment-detail/main.tsx
create mode 100644 src/ui/react-app/src/apps/appointment-detail/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/blog-manager/App.tsx
create mode 100644 src/ui/react-app/src/apps/blog-manager/index.html
create mode 100644 src/ui/react-app/src/apps/blog-manager/main.tsx
create mode 100644 src/ui/react-app/src/apps/blog-manager/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/calendar-resources/App.tsx
create mode 100644 src/ui/react-app/src/apps/calendar-resources/index.html
create mode 100644 src/ui/react-app/src/apps/calendar-resources/main.tsx
create mode 100644 src/ui/react-app/src/apps/calendar-resources/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/calendar-view/App.tsx
create mode 100644 src/ui/react-app/src/apps/calendar-view/index.html
create mode 100644 src/ui/react-app/src/apps/calendar-view/main.tsx
create mode 100644 src/ui/react-app/src/apps/calendar-view/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/call-detail/App.tsx
create mode 100644 src/ui/react-app/src/apps/call-detail/index.html
create mode 100644 src/ui/react-app/src/apps/call-detail/main.tsx
create mode 100644 src/ui/react-app/src/apps/call-detail/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/call-log/App.tsx
create mode 100644 src/ui/react-app/src/apps/call-log/index.html
create mode 100644 src/ui/react-app/src/apps/call-log/main.tsx
create mode 100644 src/ui/react-app/src/apps/call-log/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/campaign-stats/App.tsx
create mode 100644 src/ui/react-app/src/apps/campaign-stats/index.html
create mode 100644 src/ui/react-app/src/apps/campaign-stats/main.tsx
create mode 100644 src/ui/react-app/src/apps/campaign-stats/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/company-detail/App.tsx
create mode 100644 src/ui/react-app/src/apps/company-detail/index.html
create mode 100644 src/ui/react-app/src/apps/company-detail/main.tsx
create mode 100644 src/ui/react-app/src/apps/company-detail/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/company-list/App.tsx
create mode 100644 src/ui/react-app/src/apps/company-list/index.html
create mode 100644 src/ui/react-app/src/apps/company-list/main.tsx
create mode 100644 src/ui/react-app/src/apps/company-list/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/contact-card/App.tsx
create mode 100644 src/ui/react-app/src/apps/contact-card/index.html
create mode 100644 src/ui/react-app/src/apps/contact-card/main.tsx
create mode 100644 src/ui/react-app/src/apps/contact-card/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/contact-creator/App.tsx
create mode 100644 src/ui/react-app/src/apps/contact-creator/index.html
create mode 100644 src/ui/react-app/src/apps/contact-creator/main.tsx
create mode 100644 src/ui/react-app/src/apps/contact-creator/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/contact-grid/App.tsx
create mode 100644 src/ui/react-app/src/apps/contact-grid/index.html
create mode 100644 src/ui/react-app/src/apps/contact-grid/main.tsx
create mode 100644 src/ui/react-app/src/apps/contact-grid/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/contact-timeline/App.tsx
create mode 100644 src/ui/react-app/src/apps/contact-timeline/index.html
create mode 100644 src/ui/react-app/src/apps/contact-timeline/main.tsx
create mode 100644 src/ui/react-app/src/apps/contact-timeline/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/conversation-list/App.tsx
create mode 100644 src/ui/react-app/src/apps/conversation-list/index.html
create mode 100644 src/ui/react-app/src/apps/conversation-list/main.tsx
create mode 100644 src/ui/react-app/src/apps/conversation-list/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/conversation-thread/App.tsx
create mode 100644 src/ui/react-app/src/apps/conversation-thread/index.html
create mode 100644 src/ui/react-app/src/apps/conversation-thread/main.tsx
create mode 100644 src/ui/react-app/src/apps/conversation-thread/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/coupon-manager/App.tsx
create mode 100644 src/ui/react-app/src/apps/coupon-manager/index.html
create mode 100644 src/ui/react-app/src/apps/coupon-manager/main.tsx
create mode 100644 src/ui/react-app/src/apps/coupon-manager/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/course-catalog/App.tsx
create mode 100644 src/ui/react-app/src/apps/course-catalog/index.html
create mode 100644 src/ui/react-app/src/apps/course-catalog/main.tsx
create mode 100644 src/ui/react-app/src/apps/course-catalog/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/course-detail/App.tsx
create mode 100644 src/ui/react-app/src/apps/course-detail/index.html
create mode 100644 src/ui/react-app/src/apps/course-detail/main.tsx
create mode 100644 src/ui/react-app/src/apps/course-detail/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/custom-fields-manager/App.tsx
create mode 100644 src/ui/react-app/src/apps/custom-fields-manager/index.html
create mode 100644 src/ui/react-app/src/apps/custom-fields-manager/main.tsx
create mode 100644 src/ui/react-app/src/apps/custom-fields-manager/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/duplicate-checker/App.tsx
create mode 100644 src/ui/react-app/src/apps/duplicate-checker/index.html
create mode 100644 src/ui/react-app/src/apps/duplicate-checker/main.tsx
create mode 100644 src/ui/react-app/src/apps/duplicate-checker/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/email-template-preview/App.tsx
create mode 100644 src/ui/react-app/src/apps/email-template-preview/index.html
create mode 100644 src/ui/react-app/src/apps/email-template-preview/main.tsx
create mode 100644 src/ui/react-app/src/apps/email-template-preview/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/estimate-builder/App.tsx
create mode 100644 src/ui/react-app/src/apps/estimate-builder/index.html
create mode 100644 src/ui/react-app/src/apps/estimate-builder/main.tsx
create mode 100644 src/ui/react-app/src/apps/estimate-builder/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/estimate-preview/App.tsx
create mode 100644 src/ui/react-app/src/apps/estimate-preview/index.html
create mode 100644 src/ui/react-app/src/apps/estimate-preview/main.tsx
create mode 100644 src/ui/react-app/src/apps/estimate-preview/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/form-list/App.tsx
create mode 100644 src/ui/react-app/src/apps/form-list/index.html
create mode 100644 src/ui/react-app/src/apps/form-list/main.tsx
create mode 100644 src/ui/react-app/src/apps/form-list/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/form-submissions/App.tsx
create mode 100644 src/ui/react-app/src/apps/form-submissions/index.html
create mode 100644 src/ui/react-app/src/apps/form-submissions/main.tsx
create mode 100644 src/ui/react-app/src/apps/form-submissions/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/free-slots-finder/App.tsx
create mode 100644 src/ui/react-app/src/apps/free-slots-finder/index.html
create mode 100644 src/ui/react-app/src/apps/free-slots-finder/main.tsx
create mode 100644 src/ui/react-app/src/apps/free-slots-finder/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/funnel-detail/App.tsx
create mode 100644 src/ui/react-app/src/apps/funnel-detail/index.html
create mode 100644 src/ui/react-app/src/apps/funnel-detail/main.tsx
create mode 100644 src/ui/react-app/src/apps/funnel-detail/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/funnel-list/App.tsx
create mode 100644 src/ui/react-app/src/apps/funnel-list/index.html
create mode 100644 src/ui/react-app/src/apps/funnel-list/main.tsx
create mode 100644 src/ui/react-app/src/apps/funnel-list/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/inventory-dashboard/App.tsx
create mode 100644 src/ui/react-app/src/apps/inventory-dashboard/index.html
create mode 100644 src/ui/react-app/src/apps/inventory-dashboard/main.tsx
create mode 100644 src/ui/react-app/src/apps/inventory-dashboard/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/invoice-builder/App.tsx
create mode 100644 src/ui/react-app/src/apps/invoice-builder/index.html
create mode 100644 src/ui/react-app/src/apps/invoice-builder/main.tsx
create mode 100644 src/ui/react-app/src/apps/invoice-builder/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/invoice-list/App.tsx
create mode 100644 src/ui/react-app/src/apps/invoice-list/index.html
create mode 100644 src/ui/react-app/src/apps/invoice-list/main.tsx
create mode 100644 src/ui/react-app/src/apps/invoice-list/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/invoice-preview/App.tsx
create mode 100644 src/ui/react-app/src/apps/invoice-preview/index.html
create mode 100644 src/ui/react-app/src/apps/invoice-preview/main.tsx
create mode 100644 src/ui/react-app/src/apps/invoice-preview/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/location-dashboard/App.tsx
create mode 100644 src/ui/react-app/src/apps/location-dashboard/index.html
create mode 100644 src/ui/react-app/src/apps/location-dashboard/main.tsx
create mode 100644 src/ui/react-app/src/apps/location-dashboard/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/media-library/App.tsx
create mode 100644 src/ui/react-app/src/apps/media-library/index.html
create mode 100644 src/ui/react-app/src/apps/media-library/main.tsx
create mode 100644 src/ui/react-app/src/apps/media-library/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/message-composer/App.tsx
create mode 100644 src/ui/react-app/src/apps/message-composer/index.html
create mode 100644 src/ui/react-app/src/apps/message-composer/main.tsx
create mode 100644 src/ui/react-app/src/apps/message-composer/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/message-detail/App.tsx
create mode 100644 src/ui/react-app/src/apps/message-detail/index.html
create mode 100644 src/ui/react-app/src/apps/message-detail/main.tsx
create mode 100644 src/ui/react-app/src/apps/message-detail/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/opportunity-card/App.tsx
create mode 100644 src/ui/react-app/src/apps/opportunity-card/index.html
create mode 100644 src/ui/react-app/src/apps/opportunity-card/main.tsx
create mode 100644 src/ui/react-app/src/apps/opportunity-card/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/opportunity-editor/App.tsx
create mode 100644 src/ui/react-app/src/apps/opportunity-editor/index.html
create mode 100644 src/ui/react-app/src/apps/opportunity-editor/main.tsx
create mode 100644 src/ui/react-app/src/apps/opportunity-editor/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/order-detail/App.tsx
create mode 100644 src/ui/react-app/src/apps/order-detail/index.html
create mode 100644 src/ui/react-app/src/apps/order-detail/main.tsx
create mode 100644 src/ui/react-app/src/apps/order-detail/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/order-list/App.tsx
create mode 100644 src/ui/react-app/src/apps/order-list/index.html
create mode 100644 src/ui/react-app/src/apps/order-list/main.tsx
create mode 100644 src/ui/react-app/src/apps/order-list/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/pipeline-analytics/App.tsx
create mode 100644 src/ui/react-app/src/apps/pipeline-analytics/index.html
create mode 100644 src/ui/react-app/src/apps/pipeline-analytics/main.tsx
create mode 100644 src/ui/react-app/src/apps/pipeline-analytics/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/pipeline-funnel/App.tsx
create mode 100644 src/ui/react-app/src/apps/pipeline-funnel/index.html
create mode 100644 src/ui/react-app/src/apps/pipeline-funnel/main.tsx
create mode 100644 src/ui/react-app/src/apps/pipeline-funnel/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/pipeline-kanban/App.tsx
create mode 100644 src/ui/react-app/src/apps/pipeline-kanban/index.html
create mode 100644 src/ui/react-app/src/apps/pipeline-kanban/main.tsx
create mode 100644 src/ui/react-app/src/apps/pipeline-kanban/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/product-catalog/App.tsx
create mode 100644 src/ui/react-app/src/apps/product-catalog/index.html
create mode 100644 src/ui/react-app/src/apps/product-catalog/main.tsx
create mode 100644 src/ui/react-app/src/apps/product-catalog/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/product-detail/App.tsx
create mode 100644 src/ui/react-app/src/apps/product-detail/index.html
create mode 100644 src/ui/react-app/src/apps/product-detail/main.tsx
create mode 100644 src/ui/react-app/src/apps/product-detail/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/revenue-dashboard/App.tsx
create mode 100644 src/ui/react-app/src/apps/revenue-dashboard/index.html
create mode 100644 src/ui/react-app/src/apps/revenue-dashboard/main.tsx
create mode 100644 src/ui/react-app/src/apps/revenue-dashboard/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/reviews-dashboard/App.tsx
create mode 100644 src/ui/react-app/src/apps/reviews-dashboard/index.html
create mode 100644 src/ui/react-app/src/apps/reviews-dashboard/main.tsx
create mode 100644 src/ui/react-app/src/apps/reviews-dashboard/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/smartlist-viewer/App.tsx
create mode 100644 src/ui/react-app/src/apps/smartlist-viewer/index.html
create mode 100644 src/ui/react-app/src/apps/smartlist-viewer/main.tsx
create mode 100644 src/ui/react-app/src/apps/smartlist-viewer/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/social-accounts/App.tsx
create mode 100644 src/ui/react-app/src/apps/social-accounts/index.html
create mode 100644 src/ui/react-app/src/apps/social-accounts/main.tsx
create mode 100644 src/ui/react-app/src/apps/social-accounts/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/social-calendar/App.tsx
create mode 100644 src/ui/react-app/src/apps/social-calendar/index.html
create mode 100644 src/ui/react-app/src/apps/social-calendar/main.tsx
create mode 100644 src/ui/react-app/src/apps/social-calendar/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/social-post-composer/App.tsx
create mode 100644 src/ui/react-app/src/apps/social-post-composer/index.html
create mode 100644 src/ui/react-app/src/apps/social-post-composer/main.tsx
create mode 100644 src/ui/react-app/src/apps/social-post-composer/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/subscription-manager/App.tsx
create mode 100644 src/ui/react-app/src/apps/subscription-manager/index.html
create mode 100644 src/ui/react-app/src/apps/subscription-manager/main.tsx
create mode 100644 src/ui/react-app/src/apps/subscription-manager/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/survey-list/App.tsx
create mode 100644 src/ui/react-app/src/apps/survey-list/index.html
create mode 100644 src/ui/react-app/src/apps/survey-list/main.tsx
create mode 100644 src/ui/react-app/src/apps/survey-list/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/survey-submissions/App.tsx
create mode 100644 src/ui/react-app/src/apps/survey-submissions/index.html
create mode 100644 src/ui/react-app/src/apps/survey-submissions/main.tsx
create mode 100644 src/ui/react-app/src/apps/survey-submissions/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/tags-manager/App.tsx
create mode 100644 src/ui/react-app/src/apps/tags-manager/index.html
create mode 100644 src/ui/react-app/src/apps/tags-manager/main.tsx
create mode 100644 src/ui/react-app/src/apps/tags-manager/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/task-board/App.tsx
create mode 100644 src/ui/react-app/src/apps/task-board/index.html
create mode 100644 src/ui/react-app/src/apps/task-board/main.tsx
create mode 100644 src/ui/react-app/src/apps/task-board/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/team-management/App.tsx
create mode 100644 src/ui/react-app/src/apps/team-management/index.html
create mode 100644 src/ui/react-app/src/apps/team-management/main.tsx
create mode 100644 src/ui/react-app/src/apps/team-management/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/template-library/App.tsx
create mode 100644 src/ui/react-app/src/apps/template-library/index.html
create mode 100644 src/ui/react-app/src/apps/template-library/main.tsx
create mode 100644 src/ui/react-app/src/apps/template-library/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/transaction-list/App.tsx
create mode 100644 src/ui/react-app/src/apps/transaction-list/index.html
create mode 100644 src/ui/react-app/src/apps/transaction-list/main.tsx
create mode 100644 src/ui/react-app/src/apps/transaction-list/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/user-detail/App.tsx
create mode 100644 src/ui/react-app/src/apps/user-detail/index.html
create mode 100644 src/ui/react-app/src/apps/user-detail/main.tsx
create mode 100644 src/ui/react-app/src/apps/user-detail/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/workflow-detail/App.tsx
create mode 100644 src/ui/react-app/src/apps/workflow-detail/index.html
create mode 100644 src/ui/react-app/src/apps/workflow-detail/main.tsx
create mode 100644 src/ui/react-app/src/apps/workflow-detail/vite.config.ts
create mode 100644 src/ui/react-app/src/apps/workflow-status/App.tsx
create mode 100644 src/ui/react-app/src/apps/workflow-status/index.html
create mode 100644 src/ui/react-app/src/apps/workflow-status/main.tsx
create mode 100644 src/ui/react-app/src/apps/workflow-status/vite.config.ts
create mode 100644 src/ui/react-app/src/components/charts/BarChart.tsx
create mode 100644 src/ui/react-app/src/components/charts/FunnelChart.tsx
create mode 100644 src/ui/react-app/src/components/charts/LineChart.tsx
create mode 100644 src/ui/react-app/src/components/charts/PieChart.tsx
create mode 100644 src/ui/react-app/src/components/charts/SparklineChart.tsx
create mode 100644 src/ui/react-app/src/components/comms/ChatThread.tsx
create mode 100644 src/ui/react-app/src/components/comms/ContentPreview.tsx
create mode 100644 src/ui/react-app/src/components/comms/EmailPreview.tsx
create mode 100644 src/ui/react-app/src/components/comms/TranscriptView.tsx
create mode 100644 src/ui/react-app/src/components/data/AudioPlayer.tsx
create mode 100644 src/ui/react-app/src/components/data/AvatarGroup.tsx
create mode 100644 src/ui/react-app/src/components/data/CardGrid.tsx
create mode 100644 src/ui/react-app/src/components/data/ChecklistView.tsx
create mode 100644 src/ui/react-app/src/components/data/CurrencyDisplay.tsx
create mode 100644 src/ui/react-app/src/components/data/DataTable.tsx
create mode 100644 src/ui/react-app/src/components/data/DetailHeader.tsx
create mode 100644 src/ui/react-app/src/components/data/InfoBlock.tsx
create mode 100644 src/ui/react-app/src/components/data/KanbanBoard.tsx
create mode 100644 src/ui/react-app/src/components/data/KeyValueList.tsx
create mode 100644 src/ui/react-app/src/components/data/LineItemsTable.tsx
create mode 100644 src/ui/react-app/src/components/data/MetricCard.tsx
create mode 100644 src/ui/react-app/src/components/data/ProgressBar.tsx
create mode 100644 src/ui/react-app/src/components/data/StarRating.tsx
create mode 100644 src/ui/react-app/src/components/data/StatusBadge.tsx
create mode 100644 src/ui/react-app/src/components/data/StockIndicator.tsx
create mode 100644 src/ui/react-app/src/components/data/TagList.tsx
create mode 100644 src/ui/react-app/src/components/data/Timeline.tsx
create mode 100644 src/ui/react-app/src/components/interactive/AmountInput.tsx
create mode 100644 src/ui/react-app/src/components/interactive/AppointmentBooker.tsx
create mode 100644 src/ui/react-app/src/components/interactive/ContactPicker.tsx
create mode 100644 src/ui/react-app/src/components/interactive/EditableField.tsx
create mode 100644 src/ui/react-app/src/components/interactive/FormGroup.tsx
create mode 100644 src/ui/react-app/src/components/interactive/InvoiceBuilder.tsx
create mode 100644 src/ui/react-app/src/components/interactive/OpportunityEditor.tsx
create mode 100644 src/ui/react-app/src/components/interactive/SelectDropdown.tsx
create mode 100644 src/ui/react-app/src/components/layout/Card.tsx
create mode 100644 src/ui/react-app/src/components/layout/PageHeader.tsx
create mode 100644 src/ui/react-app/src/components/layout/Section.tsx
create mode 100644 src/ui/react-app/src/components/layout/SplitLayout.tsx
create mode 100644 src/ui/react-app/src/components/layout/StatsGrid.tsx
create mode 100644 src/ui/react-app/src/components/shared/ActionBar.tsx
create mode 100644 src/ui/react-app/src/components/shared/ActionButton.tsx
create mode 100644 src/ui/react-app/src/components/shared/FilterChips.tsx
create mode 100644 src/ui/react-app/src/components/shared/Modal.tsx
create mode 100644 src/ui/react-app/src/components/shared/SaveIndicator.tsx
create mode 100644 src/ui/react-app/src/components/shared/SearchBar.tsx
create mode 100644 src/ui/react-app/src/components/shared/TabGroup.tsx
create mode 100644 src/ui/react-app/src/components/shared/Toast.tsx
create mode 100644 src/ui/react-app/src/components/viz/CalendarView.tsx
create mode 100644 src/ui/react-app/src/components/viz/DuplicateCompare.tsx
create mode 100644 src/ui/react-app/src/components/viz/FlowDiagram.tsx
create mode 100644 src/ui/react-app/src/components/viz/MediaGallery.tsx
create mode 100644 src/ui/react-app/src/components/viz/TreeView.tsx
create mode 100644 src/ui/react-app/src/context/ChangeTrackerContext.tsx
create mode 100644 src/ui/react-app/src/context/MCPAppContext.tsx
create mode 100644 src/ui/react-app/src/hooks/useAutoSave.ts
create mode 100644 src/ui/react-app/src/hooks/useCallTool.ts
create mode 100644 src/ui/react-app/src/hooks/useChangeTracker.ts
create mode 100644 src/ui/react-app/src/hooks/useFormState.ts
create mode 100644 src/ui/react-app/src/hooks/useHostCapabilities.ts
create mode 100644 src/ui/react-app/src/hooks/useSizeReporter.ts
create mode 100644 src/ui/react-app/src/hooks/useSmartAction.ts
create mode 100644 src/ui/react-app/src/main.tsx
create mode 100644 src/ui/react-app/src/renderer/UITreeRenderer.tsx
create mode 100644 src/ui/react-app/src/renderer/registry.ts
create mode 100644 src/ui/react-app/src/styles/base.css
create mode 100644 src/ui/react-app/src/styles/interactive.css
create mode 100644 src/ui/react-app/src/types.ts
create mode 100644 src/ui/react-app/src/utils/mergeUITrees.ts
create mode 100644 src/ui/react-app/tsconfig.json
create mode 100644 src/ui/react-app/vite.config.ts
diff --git a/AGENT-TASKS.md b/AGENT-TASKS.md
new file mode 100644
index 0000000..28d9e6f
--- /dev/null
+++ b/AGENT-TASKS.md
@@ -0,0 +1,177 @@
+# Agent Task Briefs (for Phase 2 spawn)
+
+## Shared Context for ALL Phase 2 Agents
+- Working dir: `/Users/jakeshore/.clawdbot/workspace/mcp-diagrams/GoHighLevel-MCP/src/ui/react-app/`
+- Read the full plan: `../../REACT-REWRITE-PLAN.md`
+- Read types: `src/types.ts` (created by Alpha — has all prop interfaces)
+- Read registry: `src/renderer/registry.ts` (has stub components — replace yours)
+- Read current string implementations: `../json-render-app/src/components.ts` and `../json-render-app/src/charts.ts`
+- Use MCPAppContext via `import { useMCPApp } from '../context/MCPAppContext'` for callTool
+- CSS goes in `src/styles/components.css` (display) or `src/styles/interactive.css` (interactive)
+- Each component is a React FC exported from its own file
+- After building all components, UPDATE `src/renderer/registry.ts` to import your real components instead of stubs
+- NO GHL references — everything is generic MCP UI Kit
+- Compact sizing: 12px base, tight padding, designed for chat inline display
+
+---
+
+## Agent Bravo — Layout + Core Data (15 components)
+
+### Files to create:
+```
+src/components/layout/PageHeader.tsx
+src/components/layout/Card.tsx
+src/components/layout/StatsGrid.tsx
+src/components/layout/SplitLayout.tsx
+src/components/layout/Section.tsx
+src/components/data/DataTable.tsx
+src/components/data/KanbanBoard.tsx ← DRAG AND DROP (key component)
+src/components/data/MetricCard.tsx
+src/components/data/StatusBadge.tsx
+src/components/data/Timeline.tsx
+src/components/data/DetailHeader.tsx
+src/components/data/KeyValueList.tsx
+src/components/data/LineItemsTable.tsx
+src/components/data/InfoBlock.tsx
+src/components/data/ProgressBar.tsx
+```
+
+### Special attention:
+- **KanbanBoard** — Must have REAL drag-and-drop via React state (onDragStart/onDragOver/onDrop).
+ - Cards are draggable between columns
+ - Optimistic UI: move card immediately in state, revert on error
+ - Accepts `moveTool?: string` prop — if provided, calls `callTool(moveTool, { opportunityId, pipelineStageId })` on drop
+ - Drop zone highlights with dashed border
+ - Cards show hover lift effect
+- **DataTable** — Sortable columns (click header), clickable rows (emit onRowClick), pagination
+- All layout components accept `children` prop for nested UITree elements
+
+---
+
+## Agent Charlie — Extended Data + Navigation + Actions (15 components)
+
+### Files to create:
+```
+src/components/data/CurrencyDisplay.tsx
+src/components/data/TagList.tsx
+src/components/data/CardGrid.tsx
+src/components/data/AvatarGroup.tsx
+src/components/data/StarRating.tsx
+src/components/data/StockIndicator.tsx
+src/components/shared/SearchBar.tsx
+src/components/shared/FilterChips.tsx
+src/components/shared/TabGroup.tsx
+src/components/shared/ActionButton.tsx
+src/components/shared/ActionBar.tsx
+```
+
+Plus from data/:
+```
+src/components/data/ChecklistView.tsx
+src/components/data/AudioPlayer.tsx
+```
+
+### Special attention:
+- **SearchBar** — Controlled input, fires onChange with debounce. Accepts `onSearch?: (query: string) => void`
+- **FilterChips** — Toggle active state on click, fires onFilter
+- **TabGroup** — Controlled tab state, fires onTabChange
+- **ActionButton** — Accepts `onClick` + optional `toolName` + `toolArgs` props. If toolName provided, calls callTool on click.
+- **ChecklistView** — Checkboxes toggle completed state. Accepts `onToggle?: (itemId, completed) => void`
+
+---
+
+## Agent Delta — Comms + Viz + Charts (16 components)
+
+### Files to create:
+```
+src/components/comms/ChatThread.tsx
+src/components/comms/EmailPreview.tsx
+src/components/comms/ContentPreview.tsx
+src/components/comms/TranscriptView.tsx
+src/components/viz/CalendarView.tsx
+src/components/viz/FlowDiagram.tsx
+src/components/viz/TreeView.tsx
+src/components/viz/MediaGallery.tsx
+src/components/viz/DuplicateCompare.tsx
+src/components/charts/BarChart.tsx
+src/components/charts/LineChart.tsx
+src/components/charts/PieChart.tsx
+src/components/charts/FunnelChart.tsx
+src/components/charts/SparklineChart.tsx
+```
+
+### Special attention:
+- **All charts** — Use inline SVG (same approach as current charts.ts). Convert from template strings to JSX SVG elements.
+- **CalendarView** — Grid layout, today highlight, event dots. Accepts `onDateClick?: (date: string) => void`
+- **TreeView** — Expandable nodes with click-to-toggle. Track expanded state in local component state.
+- **ChatThread** — Outbound messages right-aligned indigo, inbound left-aligned gray. Auto-scroll to bottom.
+- **FlowDiagram** — Horizontal/vertical node→arrow→node layout with SVG connectors.
+
+---
+
+## Agent Echo — Interactive Components + Forms (8 components + shared UI)
+
+### Files to create:
+```
+src/components/interactive/ContactPicker.tsx
+src/components/interactive/InvoiceBuilder.tsx
+src/components/interactive/OpportunityEditor.tsx
+src/components/interactive/AppointmentBooker.tsx
+src/components/interactive/EditableField.tsx
+src/components/interactive/SelectDropdown.tsx
+src/components/interactive/FormGroup.tsx
+src/components/interactive/AmountInput.tsx
+src/components/shared/Toast.tsx
+src/components/shared/Modal.tsx
+```
+
+### Critical design:
+ALL interactive components are **CRM-agnostic**. They receive tool names as props.
+
+- **ContactPicker** — Searchable dropdown.
+ - Props: `searchTool: string` (e.g., "search_contacts"), `onSelect: (contact) => void`, `placeholder?: string`
+ - On keystroke (debounced 300ms): calls `callTool(searchTool, { query })` → displays results as dropdown options
+ - On select: calls onSelect with full contact object, closes dropdown
+ - Shows loading spinner during search
+
+- **InvoiceBuilder** — Multi-section form.
+ - Props: `createTool?: string`, `contactSearchTool?: string`
+ - Sections: Contact (uses ContactPicker), Line Items (add/remove rows), Totals (auto-calc)
+ - Each line item: description, quantity, unit price, total
+ - "Create Invoice" button calls `callTool(createTool, { contactId, lineItems, ... })`
+
+- **OpportunityEditor** — Inline edit form.
+ - Props: `saveTool: string` (e.g., "update_opportunity"), `opportunity: { id, name, value, status, stageId }`
+ - Renders current values with click-to-edit behavior
+ - Save button calls `callTool(saveTool, { opportunityId, ...changes })`
+
+- **AppointmentBooker** — Date/time picker + form.
+ - Props: `calendarTool?: string`, `bookTool?: string`, `contactSearchTool?: string`
+ - Calendar grid for date selection, time slot list, contact picker, notes field
+ - Book button calls `callTool(bookTool, { calendarId, contactId, date, time, ... })`
+
+- **EditableField** — Click-to-edit wrapper.
+ - Props: `value: string`, `saveTool?: string`, `saveArgs?: Record`, `fieldName: string`
+ - Click → shows input, blur/enter → saves via callTool if provided
+
+- **SelectDropdown** — Generic async select.
+ - Props: `loadTool?: string`, `options?: Array`, `onChange`, `placeholder`
+ - If loadTool provided, fetches options on mount/open
+
+- **FormGroup** — Layout for labeled form fields.
+ - Props: `fields: Array<{ key, label, type, value, required? }>`, `onSubmit`, `submitLabel`
+
+- **AmountInput** — Currency-formatted number input.
+ - Props: `value, onChange, currency?, locale?`
+ - Formats display as $1,234.56, stores raw number
+
+- **Toast** — Notification component (renders via React portal).
+ - Export `useToast()` hook: `const { showToast } = useToast()`
+ - `showToast('Deal moved!', 'success')` — auto-dismisses
+
+- **Modal** — Dialog component (renders via React portal).
+ - Props: `isOpen, onClose, title, children, footer?`
+ - Backdrop click to close, escape key to close
+
+### Styles:
+Write `src/styles/interactive.css` with all interactive styles (modals, toasts, dropdowns, drag states, form inputs, etc.)
diff --git a/REACT-REWRITE-PLAN.md b/REACT-REWRITE-PLAN.md
new file mode 100644
index 0000000..20585d5
--- /dev/null
+++ b/REACT-REWRITE-PLAN.md
@@ -0,0 +1,263 @@
+# MCP UI Kit — React Rewrite Plan
+
+## Vision
+Build a **generic, reusable MCP UI component library** using React + ext-apps SDK. Any MCP server (GHL, HubSpot, Salesforce, etc.) can use this component kit to render AI-generated interactive UIs. GHL is the first implementation. The library is CRM-agnostic — interactive components accept tool names as props so each server configures its own tool mappings.
+
+---
+
+## Architecture
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ Goose / Claude Desktop (MCP Host) │
+│ │
+│ tools/call → GHL MCP Server → GHL API │
+│ ↑ ↓ │
+│ tools/call structuredContent (JSON UI tree) │
+│ ↑ ↓ │
+│ ┌─────────────────────────────────────────────┐ │
+│ │ React App (iframe via ext-apps SDK) │ │
+│ │ │ │
+│ │ useApp() hook ← ontoolresult (UI tree) │ │
+│ │ ↓ │ │
+│ │ MCPAppProvider (React Context) │ │
+│ │ - uiTree state │ │
+│ │ - formState (inputs, selections) │ │
+│ │ - callTool(name, args) → MCP server │ │
+│ │ ↓ │ │
+│ │ │ │
+│ │ - Looks up component by type │ │
+│ │ - Renders React component with props │ │
+│ │ - Recursively renders children │ │
+│ │ ↓ │ │
+│ │ 42 Display Components (pure, CRM-agnostic) │ │
+│ │ + 8 Interactive Components (tool-configurable)│ │
+│ │ - ContactPicker(searchTool="search_contacts")│ │
+│ │ - InvoiceBuilder(createTool="create_invoice")│ │
+│ │ - KanbanBoard(onMoveTool="update_opportunity")│ │
+│ │ - EditableField(saveTool=props.saveTool) │ │
+│ └─────────────────────────────────────────────┘ │
+└─────────────────────────────────────────────────────────┘
+```
+
+## File Structure (New)
+
+```
+src/ui/react-app/ # GENERIC MCP UI KIT (no GHL references)
+├── package.json # React + ext-apps SDK deps
+├── vite.config.ts # Vite + singlefile + React
+├── tsconfig.json
+├── index.html # Entry point
+├── src/
+│ ├── App.tsx # Root — useApp hook, MCPAppProvider
+│ ├── types.ts # UITree, UIElement, component prop interfaces
+│ ├── context/
+│ │ └── MCPAppContext.tsx # React Context — uiTree, formState, callTool
+│ ├── hooks/
+│ │ ├── useCallTool.ts # Hook wrapping app.callServerTool
+│ │ ├── useFormState.ts # Shared form state management
+│ │ └── useSizeReporter.ts # Auto-report content size to host
+│ ├── renderer/
+│ │ ├── UITreeRenderer.tsx # Recursive tree → React component resolver
+│ │ └── registry.ts # Component name → React component map
+│ ├── components/
+│ │ ├── layout/ # PageHeader, Card, SplitLayout, Section, StatsGrid
+│ │ ├── data/ # DataTable, KanbanBoard, MetricCard, StatusBadge,
+│ │ │ # Timeline, ProgressBar, KeyValueList, etc.
+│ │ ├── charts/ # BarChart, LineChart, PieChart, FunnelChart, Sparkline
+│ │ ├── comms/ # ChatThread, EmailPreview, TranscriptView, etc.
+│ │ ├── viz/ # CalendarView, FlowDiagram, TreeView, MediaGallery, etc.
+│ │ ├── interactive/ # ContactPicker, InvoiceBuilder, EditableField, etc.
+│ │ │ # All accept tool names as PROPS (CRM-agnostic)
+│ │ └── shared/ # ActionButton, Toast, Modal (React portals)
+│ └── styles/
+│ ├── base.css # Reset, variables, typography
+│ ├── components.css # Component-specific styles (compact for chat)
+│ └── interactive.css # Drag/drop, modals, toasts, form styles
+```
+
+### CRM-Agnostic Design Principle
+- NO component imports GHL types or references GHL tool names
+- Interactive components receive tool names via props:
+ - `ContactPicker` → `searchTool="search_contacts"` (GHL) or `"hubspot_search_contacts"` (HubSpot)
+ - `KanbanBoard` → `moveTool="update_opportunity"` (GHL) or `"move_deal"` (Pipedrive)
+ - `InvoiceBuilder` → `createTool="create_invoice"` (any billing system)
+- The MCP server's AI prompt tells Claude which tool names to use in the UI tree
+- Components call `callTool(props.toolName, args)` — they don't know or care what CRM is behind it
+
+## Component Inventory
+
+### Existing 42 (string → React conversion)
+
+**Layout (5):** PageHeader, Card, StatsGrid, SplitLayout, Section
+**Data Display (10):** DataTable, KanbanBoard, MetricCard, StatusBadge, Timeline, ProgressBar, DetailHeader, KeyValueList, LineItemsTable, InfoBlock
+**Navigation (3):** SearchBar, FilterChips, TabGroup
+**Actions (2):** ActionButton, ActionBar
+**Extended Data (6):** CurrencyDisplay, TagList, CardGrid, AvatarGroup, StarRating, StockIndicator
+**Communications (6):** ChatThread, EmailPreview, ContentPreview, TranscriptView, AudioPlayer, ChecklistView
+**Visualization (5):** CalendarView, FlowDiagram, TreeView, MediaGallery, DuplicateCompare
+**Charts (5):** BarChart, LineChart, PieChart, FunnelChart, SparklineChart
+
+### New Interactive Components (8)
+
+| Component | Purpose | MCP Tools Used |
+|-----------|---------|----------------|
+| **ContactPicker** | Searchable dropdown, fetches contacts on type | `search_contacts` |
+| **InvoiceBuilder** | Line items, totals, contact auto-fill | `create_invoice`, `get_contact` |
+| **OpportunityEditor** | Inline edit deal name/value/status/stage | `update_opportunity` |
+| **AppointmentBooker** | Calendar slot picker + booking form | `get_calendar`, `create_appointment` |
+| **EditableField** | Click-to-edit any text/number field | varies (generic) |
+| **SelectDropdown** | Generic select with async option loading | varies |
+| **FormGroup** | Group of form fields with validation | varies |
+| **AmountInput** | Currency-formatted number input | — (local state) |
+
+---
+
+## Agent Team Plan
+
+### Phase 1: Foundation (Sequential — 1 agent)
+
+**Agent Alpha — Project Scaffold + App Shell**
+- Create `src/ui/react-app/` with package.json, vite.config, tsconfig
+- Install deps: react, react-dom, @modelcontextprotocol/ext-apps, @vitejs/plugin-react, vite-plugin-singlefile
+- Build `App.tsx` with `useApp` hook — handles ontoolresult, ontoolinput, host context
+- Build `GHLContext.tsx` — React context providing uiTree, formState, callTool
+- Build `useCallTool.ts` — wrapper around `app.callServerTool` with loading/error states
+- Build `useFormState.ts` — shared form state hook
+- Build `useSizeReporter.ts` — auto-measures content, sends `ui/notifications/size-changed`
+- Build `UITreeRenderer.tsx` — recursive renderer that resolves component types from registry
+- Build `registry.ts` — component map (stubs for now, filled by other agents)
+- Build `types.ts` — UITree, UIElement, all component prop interfaces
+- Build base CSS (`base.css`) — reset, variables, compact typography
+- Update outer build pipeline in GoHighLevel-MCP `package.json` to build React app
+- **Output:** Working scaffold that renders a loading state, connects to host via ext-apps
+
+### Phase 2: Components (Parallel — 4 agents)
+
+**Agent Bravo — Layout + Core Data Components (15)**
+Files: `components/layout/`, `components/data/` (first half)
+- PageHeader, Card, StatsGrid, SplitLayout, Section
+- DataTable (with clickable rows, sortable columns)
+- KanbanBoard (with FULL drag-and-drop via React state — no DOM hacking)
+- MetricCard, StatusBadge, Timeline
+- Register all in registry.ts
+- Component CSS in `components.css`
+
+**Agent Charlie — Data Display + Navigation + Actions (15)**
+Files: `components/data/` (second half), `components/shared/`
+- ProgressBar, DetailHeader, KeyValueList, LineItemsTable, InfoBlock
+- SearchBar, FilterChips, TabGroup
+- ActionButton, ActionBar
+- CurrencyDisplay, TagList, CardGrid, AvatarGroup, StarRating, StockIndicator
+- Register all in registry.ts
+
+**Agent Delta — Comms + Viz + Charts (16)**
+Files: `components/comms/`, `components/viz/`, `components/charts/`
+- ChatThread, EmailPreview, ContentPreview, TranscriptView, AudioPlayer, ChecklistView
+- CalendarView, FlowDiagram, TreeView, MediaGallery, DuplicateCompare
+- BarChart, LineChart, PieChart, FunnelChart, SparklineChart
+- All chart components use inline SVG (same approach, just JSX)
+- Register all in registry.ts
+
+**Agent Echo — Interactive Components + Forms (8)**
+Files: `components/interactive/`, `hooks/`
+- ContactPicker — searchable dropdown, calls `search_contacts` on keystroke with debounce
+- InvoiceBuilder — line items table + contact selection + auto-total
+- OpportunityEditor — inline edit form for deal fields, saves via `update_opportunity`
+- AppointmentBooker — date/time picker + contact + calendar selection
+- EditableField — click-to-edit wrapper for any field
+- SelectDropdown — generic async select
+- FormGroup — form layout with labels + validation
+- AmountInput — formatted currency input
+- Shared: Toast component, Modal component (proper React portals)
+- Integrate with GHLContext for tool calling
+
+### Phase 3: Integration (Sequential — 1 agent)
+
+**Agent Foxtrot — Wire Everything Together**
+- Merge all component registrations into `registry.ts`
+- Update `src/apps/index.ts`:
+ - Add new tool definitions for interactive components (`create_invoice`, `create_appointment`)
+ - Update resource handler for `ui://ghl/dynamic-view` to serve React build
+ - Add new resource URIs if needed
+- Update `src/server.ts` if new tools need routing
+- Update system prompt: add new interactive component catalog entries
+- Update Goose config `available_tools` if needed
+- Full build: React app → singlefile HTML → server TypeScript
+- Test: verify JSON UI trees render correctly, interactive components call tools, drag-and-drop works
+- Write brief README for the new architecture
+
+---
+
+## Key Technical Decisions
+
+### State Management
+- **React Context** (not Redux) — app is small enough, context + useReducer is perfect
+- `GHLContext` holds: current UITree, form values, loading states, selected entities
+- Any component can `const { callTool } = useGHL()` to interact with the MCP server
+
+### Tool Calling Pattern
+```tsx
+// Any component can call MCP tools:
+const { callTool, isLoading } = useCallTool();
+
+const handleDrop = async (cardId: string, newStageId: string) => {
+ await callTool('update_opportunity', {
+ opportunityId: cardId,
+ pipelineStageId: newStageId,
+ });
+};
+```
+
+### Drag & Drop (KanbanBoard)
+- Pure React state — no global DOM event handlers
+- `onDragStart`, `onDragOver`, `onDrop` on React elements
+- Optimistic UI update (move card immediately, revert on error)
+
+### Dynamic Sizing
+- `useSizeReporter` hook — ResizeObserver on `#app`
+- Sends `ui/notifications/size-changed` on every size change
+- Caps at 600px height
+
+### CSS Strategy
+- Plain CSS files (not CSS modules, not Tailwind) — keeps bundle simple
+- Same compact sizing as current (12px base, tight padding)
+- All in `styles/` directory, imported in App.tsx
+- Interactive styles (drag states, modals, toasts) in separate file
+
+### Build Pipeline
+```bash
+# In src/ui/react-app/
+npm run build
+# → Vite builds React app → vite-plugin-singlefile → single HTML file
+# → Output: ../../dist/app-ui/dynamic-view.html
+
+# In GoHighLevel-MCP root
+npm run build
+# → Builds React UI first, then compiles TypeScript server
+```
+
+---
+
+## Timeline Estimate
+
+| Phase | Agents | Est. Time | Depends On |
+|-------|--------|-----------|------------|
+| Phase 1: Foundation | Alpha (1) | ~20 min | — |
+| Phase 2: Components | Bravo, Charlie, Delta, Echo (4 parallel) | ~25 min | Phase 1 |
+| Phase 3: Integration | Foxtrot (1) | ~15 min | Phase 2 |
+| **Total** | **6 agents** | **~60 min** | |
+
+---
+
+## Success Criteria
+1. ✅ All 42 existing components render identically to current version
+2. ✅ JSON UI trees from Claude work without any format changes
+3. ✅ KanbanBoard drag-and-drop moves deals and persists via `update_opportunity`
+4. ✅ ContactPicker fetches real contacts from GHL on keystroke
+5. ✅ InvoiceBuilder creates invoices with real contact data
+6. ✅ EditableField saves changes via appropriate MCP tool
+7. ✅ Dynamic sizing works — views fit in chat
+8. ✅ Single HTML file output (vite-plugin-singlefile)
+9. ✅ ext-apps handshake completes with Goose
+10. ✅ All existing `view_*` tools still work alongside `generate_ghl_view`
diff --git a/REACT-STATE-ANALYSIS.md b/REACT-STATE-ANALYSIS.md
new file mode 100644
index 0000000..6fe74de
--- /dev/null
+++ b/REACT-STATE-ANALYSIS.md
@@ -0,0 +1,369 @@
+# React State & Hydration Analysis — MCP App Lifecycle
+
+## Executive Summary
+
+**The interactive state destruction is caused by a cascading chain of three interconnected bugs:**
+1. Every tool call result replaces the entire UITree → full component tree teardown
+2. Element keys from the server may not be stable across calls → React unmounts everything
+3. All interactive components store state locally (`useState`) instead of in the shared context that was built specifically to survive re-renders
+
+---
+
+## Bug #1: `ontoolresult` → `setUITree()` Nuclear Replacement
+
+### The Code (App.tsx:80-86)
+```tsx
+app.ontoolresult = async (result) => {
+ const tree = extractUITree(result);
+ if (tree) {
+ setUITree(tree); // ← REPLACES the entire tree object
+ setToolInput(null);
+ }
+};
+```
+
+### What Happens
+Every tool result that contains a UITree — even from a button click, search query, or tab switch — causes `setUITree(brandNewTreeObject)`. This triggers:
+
+1. `App.tsx` re-renders with new `uiTree` state
+2. `` receives a **new object reference**
+3. `ElementRenderer` receives a **new `elements` map** (new reference, even if data is identical)
+4. React walks the entire tree and reconciles
+
+**The critical question:** does it unmount/remount or just re-render?
+
+That depends entirely on **Bug #2** (keys).
+
+---
+
+## Bug #2: `key: element.key` — Keys Control Component Identity
+
+### The Code (UITreeRenderer.tsx:37-40)
+```tsx
+// Each component gets its key from the JSON tree
+return React.createElement(Component, { key: element.key, ...element.props }, childElements);
+
+// Children also keyed from the tree
+const childElements = element.children?.map((childKey) =>
+ React.createElement(ElementRenderer, {
+ key: childKey, // ← from JSON
+ elementKey: childKey, // ← from JSON
+ elements,
+ }),
+);
+```
+
+### The Problem
+React uses `key` to determine component identity. When keys change between renders:
+- **Same key** → React re-renders the existing component (state preserved)
+- **Different key** → React unmounts old, mounts new (STATE DESTROYED)
+
+If the MCP server generates keys like `contact-list-abc123` on call #1 and `contact-list-def456` on call #2, React sees them as **completely different components** and tears down the entire subtree.
+
+**Even if keys are stable**, the `elements` object reference changes every time, forcing re-renders down the entire tree. Not a state-loss issue by itself, but causes performance problems and can trigger effects/callbacks unnecessarily.
+
+---
+
+## Bug #3: Dual `uiTree` State — App.tsx vs MCPAppContext
+
+### The Code
+```tsx
+// App.tsx — has its own uiTree state
+const [uiTree, setUITree] = useState(null);
+
+// MCPAppContext.tsx — ALSO has its own uiTree state
+const [uiTree, setUITree] = useState(null);
+```
+
+### The Problem
+There are **two completely independent `uiTree` states**:
+- `App.tsx` manages the actual rendering tree (used by ``)
+- `MCPAppContext` has its own `uiTree` + `setUITree` exposed via context, but **nobody calls the context's `setUITree`**
+
+The context's `uiTree` is **always null**. The context was designed to provide shared state (`formState`, `setFormValue`, `callTool`), but the tree management is disconnected.
+
+---
+
+## Bug #4: Interactive Components Use Local State That Gets Destroyed
+
+### FormGroup (forms reset)
+```tsx
+// FormGroup.tsx — all form values in LOCAL useState
+const [values, setValues] = useState>(() => {
+ const init: Record = {};
+ for (const f of fields) { init[f.key] = ""; }
+ return init;
+});
+```
+
+**Chain:** User types → submits → `execute(submitTool, values)` → server returns new tree → `ontoolresult` → `setUITree(newTree)` → FormGroup unmounts → remounts → `values` resets to `{}`
+
+### KanbanBoard (drag-and-drop fails)
+```tsx
+// KanbanBoard.tsx — drag state + columns in LOCAL state
+const [columns, setColumns] = useState(initialColumns);
+const [dropTargetStage, setDropTargetStage] = useState(null);
+const [draggingCardId, setDraggingCardId] = useState(null);
+const dragRef = useRef(null);
+```
+
+**Chain:** User drags card → optimistic update → `callTool(moveTool, ...)` → server returns new tree → `ontoolresult` → `setUITree(newTree)` → KanbanBoard unmounts → remounts → drag state gone, optimistic move reverted, columns reset to server data
+
+The KanbanBoard even has a mitigation attempt that fails:
+```tsx
+// This ref-based sync ONLY works if React re-renders without unmounting
+const prevColumnsRef = useRef(initialColumns);
+if (prevColumnsRef.current !== initialColumns) {
+ prevColumnsRef.current = initialColumns;
+ setColumns(initialColumns); // ← This runs, but after unmount/remount it's moot
+}
+```
+
+### SearchBar (search clears)
+```tsx
+// SearchBar.tsx — query in LOCAL state
+const [value, setValue] = useState("");
+```
+
+**Chain:** User types query → debounced `callTool(searchTool, ...)` → server returns new tree → SearchBar unmounts → remounts → search input clears
+
+### ActionButton (stops working)
+```tsx
+// ActionButton.tsx — loading state in LOCAL state
+const [loading, setLoading] = useState(false);
+```
+
+**Chain:** User clicks → `setLoading(true)` → `callTool(...)` → server returns new tree → ActionButton unmounts → remounts → `loading` stuck at initial `false`, but the async `callTool` still has a stale closure reference. The `finally { setLoading(false) }` calls `setLoading` on an unmounted component.
+
+### TabGroup (tab selection lost)
+```tsx
+const [localActive, setLocalActive] = useState(
+ controlledActiveTab || tabs[0]?.value || "",
+);
+```
+
+**Chain:** User clicks tab → `setLocalActive(value)` → `callTool(switchTool, ...)` → new tree → TabGroup remounts → `localActive` resets to first tab
+
+### ContactPicker (search results disappear)
+```tsx
+const [query, setQuery] = useState("");
+const [results, setResults] = useState([]);
+const [isOpen, setIsOpen] = useState(false);
+const [selected, setSelected] = useState(null);
+```
+
+### InvoiceBuilder (entire form wiped)
+```tsx
+const [selectedContact, setSelectedContact] = useState(null);
+const [lineItems, setLineItems] = useState(...);
+const [taxRate, setTaxRate] = useState(8.5);
+```
+
+---
+
+## Bug #5: Hydration Path Mismatch
+
+### The Code (App.tsx:60-66)
+```tsx
+useEffect(() => {
+ const preInjected = getPreInjectedTree();
+ if (preInjected && !uiTree) {
+ setUITree(preInjected);
+ }
+}, []);
+```
+
+### The Problem
+Not a React hydration mismatch (we use `createRoot`, not `hydrateRoot`), but a **data transition issue**:
+
+1. App mounts → `uiTree = null` → shows "Connecting..."
+2. `useEffect` fires → finds `window.__MCP_APP_DATA__` → `setUITree(preInjectedTree)`
+3. Tree renders with pre-injected keys
+4. `ontoolresult` fires with server-generated tree → `setUITree(serverTree)`
+5. If keys differ between pre-injected and server tree → **full unmount/remount**
+
+The pre-injected path and the dynamic path produce trees with potentially different key schemas, causing a jarring full teardown on the first real tool result.
+
+---
+
+## Bug #6: MCPAppProvider Context Is Stable (NOT a bug)
+
+`MCPAppProvider` wrapping does **not** cause context loss. It's rendered consistently in all branches of the conditional render in App.tsx. The provider identity is stable.
+
+However, the provider creates a **new `value` object on every render**:
+```tsx
+const value: MCPAppContextValue = {
+ uiTree, setUITree, formState, setFormValue, resetFormState, callTool, isLoading, app,
+};
+return {children};
+```
+
+This causes every `useMCPApp()` consumer to re-render on every provider render, but it doesn't cause state loss — just unnecessary renders.
+
+---
+
+## The Kill Chain (Full Interaction Flow)
+
+```
+User clicks ActionButton("View Contact", toolName="get_contact", toolArgs={id: "123"})
+ │
+ ├─ ActionButton.handleClick()
+ │ ├─ setLoading(true)
+ │ └─ callTool("get_contact", {id: "123"})
+ │ └─ app.callServerTool({name: "get_contact", arguments: {id: "123"}})
+ │
+ ├─ MCP Server processes tool call, returns CallToolResult with NEW UITree
+ │
+ ├─ app.ontoolresult fires
+ │ ├─ extractUITree(result) → newTree (new keys, new elements, new object)
+ │ └─ setUITree(newTree) ← App.tsx state update
+ │
+ ├─ App.tsx re-renders
+ │ └─
+ │ └─ ElementRenderer receives new elements map
+ │ └─ For each element: React.createElement(Component, {key: NEW_KEY, ...})
+ │ └─ React sees different key → UNMOUNT old component, MOUNT new one
+ │
+ ├─ ALL components unmount:
+ │ ├─ FormGroup: values={} (reset)
+ │ ├─ KanbanBoard: columns=initial, dragState=null (reset)
+ │ ├─ SearchBar: value="" (reset)
+ │ ├─ TabGroup: localActive=first tab (reset)
+ │ ├─ ContactPicker: query="", results=[], selected=null (reset)
+ │ └─ InvoiceBuilder: lineItems=[default], contact=null (reset)
+ │
+ └─ Meanwhile, ActionButton's finally{} block calls setLoading(false)
+ on an UNMOUNTED component → React warning + no-op
+```
+
+---
+
+## Fixes
+
+### Fix 1: Deterministic Stable Keys (Server-Side)
+The MCP server must generate **deterministic, position-stable keys** for UITree elements. Keys should be based on component type + semantic identity, not random IDs.
+
+```typescript
+// BAD: keys change every call
+{ key: `card-${crypto.randomUUID()}`, type: "Card", ... }
+
+// GOOD: keys are stable across calls
+{ key: "contact-detail-card", type: "Card", ... }
+{ key: "pipeline-kanban", type: "KanbanBoard", ... }
+```
+
+### Fix 2: Tree Diffing / Partial Updates Instead of Full Replace
+Don't replace the entire tree on every tool result. Diff the new tree against the old one and only update changed branches:
+
+```tsx
+// App.tsx — instead of wholesale replace:
+app.ontoolresult = async (result) => {
+ const newTree = extractUITree(result);
+ if (newTree) {
+ setUITree(prev => {
+ if (!prev) return newTree;
+ // Merge: keep unchanged elements, update changed ones
+ return mergeUITrees(prev, newTree);
+ });
+ }
+};
+
+function mergeUITrees(oldTree: UITree, newTree: UITree): UITree {
+ const mergedElements: Record = {};
+ for (const [key, newEl] of Object.entries(newTree.elements)) {
+ const oldEl = oldTree.elements[key];
+ // Keep old reference if data is identical (prevents re-render)
+ if (oldEl && JSON.stringify(oldEl) === JSON.stringify(newEl)) {
+ mergedElements[key] = oldEl;
+ } else {
+ mergedElements[key] = newEl;
+ }
+ }
+ return { root: newTree.root, elements: mergedElements };
+}
+```
+
+### Fix 3: Separate Data Results from UI Results
+Not every tool call should trigger a tree replacement. ActionButton clicks that return data (not a new view) should be handled by the component, not by `ontoolresult`:
+
+```tsx
+// ActionButton.tsx — handle result locally, don't let ontoolresult replace tree
+const handleClick = useCallback(async () => {
+ if (!toolName || loading || disabled) return;
+ setLoading(true);
+ try {
+ const result = await callTool(toolName, toolArgs || {});
+ // Result handled locally — only navigate if result contains a NEW view
+ if (result && hasNavigationIntent(result)) {
+ // Let ontoolresult handle it
+ }
+ // Otherwise, toast/notification with result, don't replace tree
+ } finally {
+ setLoading(false);
+ }
+}, [toolName, toolArgs, loading, disabled, callTool]);
+```
+
+### Fix 4: Move Interactive State to Context
+Use the existing `formState` in MCPAppContext instead of local `useState`:
+
+```tsx
+// FormGroup.tsx — use shared form state that survives remounts
+const { formState, setFormValue } = useMCPApp();
+
+// Instead of local useState, derive from context
+const getValue = (key: string) => formState[`form:${formId}:${key}`] ?? "";
+const setValue = (key: string, val: string) => setFormValue(`form:${formId}:${key}`, val);
+```
+
+### Fix 5: Fix the Dual uiTree State
+Either:
+- **Option A:** Remove `uiTree` from MCPAppContext entirely (it's unused)
+- **Option B:** Have App.tsx use the context's uiTree and remove its local state
+
+```tsx
+// Option B: App.tsx uses context state
+export function App() {
+ const [hostContext, setHostContext] = useState();
+ // Remove local uiTree state — let context own it
+
+ // In MCPAppProvider, connect ontoolresult to context's setUITree
+}
+```
+
+### Fix 6: Memoize Context Value
+Prevent unnecessary re-renders of all context consumers:
+
+```tsx
+// MCPAppContext.tsx
+const value = useMemo(() => ({
+ uiTree, setUITree, formState, setFormValue, resetFormState, callTool, isLoading, app,
+}), [uiTree, formState, callTool, isLoading, app]);
+```
+
+### Fix 7: Add React.memo to Leaf Components
+Prevent re-renders when props haven't actually changed:
+
+```tsx
+export const MetricCard = React.memo(({ label, value, trend, ... }) => {
+ // ...
+});
+
+export const StatusBadge = React.memo(({ label, variant }) => {
+ // ...
+});
+```
+
+---
+
+## Priority Order
+
+| # | Fix | Impact | Effort |
+|---|-----|--------|--------|
+| 1 | Stable keys (server-side) | 🔴 Critical — fixes unmount/remount | Medium |
+| 2 | Tree diffing/merge | 🔴 Critical — prevents unnecessary teardown | Medium |
+| 3 | Separate data vs UI results | 🟡 High — stops button clicks from nuking the view | Low |
+| 4 | Context-based interactive state | 🟡 High — state survives even if components remount | Medium |
+| 5 | Fix dual uiTree state | 🟢 Medium — removes confusion, single source of truth | Low |
+| 6 | Memoize context value | 🟢 Medium — performance improvement | Low |
+| 7 | React.memo leaf components | 🟢 Low — performance polish | Low |
diff --git a/docs/FALLBACK-ARCHITECTURE.md b/docs/FALLBACK-ARCHITECTURE.md
new file mode 100644
index 0000000..50ec35f
--- /dev/null
+++ b/docs/FALLBACK-ARCHITECTURE.md
@@ -0,0 +1,639 @@
+# MCP App Fallback Architecture
+## Graceful Degradation for Hosts Without Full Interactive Support
+
+**Date:** 2026-02-03
+**Status:** Proposed
+**Applies to:** GoHighLevel MCP UI Kit React App
+
+---
+
+## Executive Summary
+
+The ext-apps SDK provides explicit **host capability negotiation** via `McpUiHostCapabilities`. After the `ui/initialize` handshake, the View knows exactly which features the host supports. We use this to implement a **three-tier progressive enhancement** model: Static → Context-Synced → Fully Interactive.
+
+---
+
+## 1. Feature Detection (The Foundation)
+
+### How It Works
+
+When the View connects, the host returns `McpUiHostCapabilities` in the `ui/initialize` response. The `App` class exposes this via `app.getHostCapabilities()`.
+
+**The key capability flags:**
+
+```typescript
+interface McpUiHostCapabilities {
+ serverTools?: { listChanged?: boolean }; // Can proxy tools/call to MCP server
+ serverResources?: { listChanged?: boolean }; // Can proxy resources/read
+ updateModelContext?: {}; // Accepts ui/update-model-context
+ message?: {}; // Accepts ui/message (send chat messages)
+ openLinks?: {}; // Can open external URLs
+ logging?: {}; // Accepts log messages
+ sandbox?: { permissions?: {...}; csp?: {...} };
+}
+```
+
+**The critical check: `serverTools` tells you if `callServerTool` will work.**
+
+### Implementation: `useHostCapabilities` Hook
+
+```typescript
+// hooks/useHostCapabilities.ts
+
+import { useMemo } from "react";
+import { useMCPApp } from "../context/MCPAppContext.js";
+import type { McpUiHostCapabilities } from "@modelcontextprotocol/ext-apps";
+
+export type InteractionTier = "static" | "context-synced" | "full";
+
+export interface HostCapabilityInfo {
+ /** Raw capabilities from the host */
+ raw: McpUiHostCapabilities | undefined;
+ /** Whether callServerTool() will work */
+ canCallTools: boolean;
+ /** Whether updateModelContext() will work */
+ canUpdateContext: boolean;
+ /** Whether sendMessage() will work */
+ canSendMessages: boolean;
+ /** The interaction tier this host supports */
+ tier: InteractionTier;
+}
+
+export function useHostCapabilities(): HostCapabilityInfo {
+ const { app } = useMCPApp();
+
+ return useMemo(() => {
+ const raw = app?.getHostCapabilities?.() as McpUiHostCapabilities | undefined;
+
+ const canCallTools = !!raw?.serverTools;
+ const canUpdateContext = !!raw?.updateModelContext;
+ const canSendMessages = !!raw?.message;
+
+ // Determine tier
+ let tier: InteractionTier = "static";
+ if (canCallTools) {
+ tier = "full";
+ } else if (canUpdateContext) {
+ tier = "context-synced";
+ }
+
+ return { raw, canCallTools, canUpdateContext, canSendMessages, tier };
+ }, [app]);
+}
+```
+
+### Add to MCPAppContext
+
+```typescript
+// In MCPAppContext.tsx — extend the context value:
+
+export interface MCPAppContextValue {
+ // ... existing fields ...
+
+ /** Host capability info, available after connection */
+ capabilities: HostCapabilityInfo;
+
+ /** Safe tool call: uses callServerTool if available,
+ falls back to updateModelContext, or no-ops */
+ safeCallTool: (
+ toolName: string,
+ args: Record,
+ options?: { localOnly?: boolean }
+ ) => Promise;
+}
+```
+
+---
+
+## 2. The Three-Tier Model
+
+### Tier 1: Static (No host capabilities)
+- Pure data visualization — charts, tables, badges, timelines
+- All data comes from the initial `ontoolresult` payload
+- No server communication, no context updates
+- **Components work as-is:** BarChart, PieChart, StatusBadge, DataTable, Timeline, etc.
+
+### Tier 2: Context-Synced (`updateModelContext` available, no `serverTools`)
+- Local interactivity works (drag, edit, toggle, form fills)
+- User actions update LOCAL state immediately
+- State changes are synced to the LLM via `updateModelContext`
+- The LLM can then decide to call tools itself on the next turn
+- **Key pattern:** User drags a Kanban card → local state updates → `updateModelContext` tells the LLM what happened → LLM calls `move_opportunity` on its own
+
+### Tier 3: Full Interactive (`serverTools` available)
+- Everything works as currently designed
+- Components call `callServerTool` directly for real-time server mutations
+- Optimistic UI with server confirmation
+
+```
+┌─────────────────────────────────────────────────┐
+│ TIER 3: FULL │
+│ callServerTool ✓ updateModelContext ✓ │
+│ Direct server mutations, optimistic UI │
+│ ┌─────────────────────────────────────────────┐ │
+│ │ TIER 2: CONTEXT-SYNCED │ │
+│ │ callServerTool ✗ updateModelContext ✓ │ │
+│ │ Local state + LLM-informed sync │ │
+│ │ ┌─────────────────────────────────────────┐│ │
+│ │ │ TIER 1: STATIC ││ │
+│ │ │ Read-only data visualization ││ │
+│ │ │ Charts, tables, badges, timelines ││ │
+│ │ └─────────────────────────────────────────┘│ │
+│ └─────────────────────────────────────────────┘ │
+└─────────────────────────────────────────────────┘
+```
+
+---
+
+## 3. Component Classification
+
+### Always Static (work everywhere)
+These components are pure data display — they render from the UITree data and never call tools:
+
+| Component | Category |
+|-----------|----------|
+| BarChart, LineChart, PieChart, SparklineChart, FunnelChart | charts |
+| StatusBadge, ProgressBar, CurrencyDisplay, StarRating | data |
+| DataTable, KeyValueList, TagList, AvatarGroup | data |
+| Timeline, MetricCard, DetailHeader, InfoBlock | data |
+| PageHeader, Section, Card, StatsGrid, SplitLayout | layout |
+| ChatThread, TranscriptView, EmailPreview, ContentPreview | comms |
+| CalendarView, TreeView, FlowDiagram, DuplicateCompare | viz |
+
+### Context-Syncable (Tier 2+)
+These work with local state and sync via `updateModelContext`:
+
+| Component | Local Behavior | Context Sync |
+|-----------|---------------|--------------|
+| KanbanBoard | Drag cards between columns locally | Report new board state to LLM |
+| EditableField | Edit text inline locally | Report edited values to LLM |
+| FormGroup | Fill form fields locally | Report form state to LLM |
+| SelectDropdown | Select options locally | Report selection to LLM |
+| AmountInput | Adjust amounts locally | Report new amounts to LLM |
+| ChecklistView | Toggle checkboxes locally | Report checklist state to LLM |
+| FilterChips | Toggle filters locally | Report active filters to LLM |
+| TabGroup | Switch tabs locally | Report active tab to LLM |
+| SearchBar | Type search queries locally | Report search to LLM |
+
+### Full Interactive Only (Tier 3)
+These **require** server tool calls and degrade gracefully at lower tiers:
+
+| Component | Tier 3 | Tier 2 Fallback | Tier 1 Fallback |
+|-----------|--------|-----------------|-----------------|
+| ActionButton | Calls tool | Shows "Ask assistant" hint | Disabled/hidden |
+| AppointmentBooker | Books via tool | Shows form, reports to LLM | Shows read-only schedule |
+| InvoiceBuilder | Creates via tool | Builds locally, reports to LLM | Shows existing invoice data |
+| OpportunityEditor | Saves via tool | Edits locally, reports to LLM | Shows read-only data |
+| ContactPicker | Searches via tool | Shows static options list | Shows read-only display |
+
+---
+
+## 4. Concrete Implementation
+
+### 4a. `safeCallTool` — The Universal Action Handler
+
+```typescript
+// In MCPAppProvider
+
+const safeCallTool = useCallback(
+ async (
+ toolName: string,
+ args: Record,
+ options?: { localOnly?: boolean }
+ ): Promise => {
+ // Tier 3: Full tool call
+ if (capabilities.canCallTools && !options?.localOnly) {
+ return callTool(toolName, args);
+ }
+
+ // Tier 2: Inform LLM via updateModelContext
+ if (capabilities.canUpdateContext && app) {
+ const markdown = [
+ `---`,
+ `action: ${toolName}`,
+ `timestamp: ${new Date().toISOString()}`,
+ ...Object.entries(args).map(([k, v]) => `${k}: ${JSON.stringify(v)}`),
+ `---`,
+ `User performed action "${toolName}" in the UI.`,
+ `Please execute the corresponding server-side operation.`,
+ ].join("\n");
+
+ await app.updateModelContext({
+ content: [{ type: "text", text: markdown }],
+ });
+
+ // Return a synthetic "pending" result
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Action "${toolName}" reported to assistant. It will be processed on the next turn.`,
+ },
+ ],
+ } as CallToolResult;
+ }
+
+ // Tier 1: No-op, return null
+ console.warn(`[MCPApp] Cannot execute ${toolName}: no host support`);
+ return null;
+ },
+ [capabilities, callTool, app]
+);
+```
+
+### 4b. `` — Tier-Aware Wrapper
+
+```tsx
+// components/shared/InteractiveGate.tsx
+
+import React from "react";
+import { useHostCapabilities, type InteractionTier } from "../../hooks/useHostCapabilities.js";
+
+interface InteractiveGateProps {
+ /** Minimum tier required to show children */
+ requires: InteractionTier;
+ /** What to render if the tier isn't met */
+ fallback?: React.ReactNode;
+ /** If true, hide entirely instead of showing fallback */
+ hideOnUnsupported?: boolean;
+ children: React.ReactNode;
+}
+
+const tierLevel: Record = {
+ static: 0,
+ "context-synced": 1,
+ full: 2,
+};
+
+export const InteractiveGate: React.FC = ({
+ requires,
+ fallback,
+ hideOnUnsupported = false,
+ children,
+}) => {
+ const { tier } = useHostCapabilities();
+
+ if (tierLevel[tier] >= tierLevel[requires]) {
+ return <>{children}>;
+ }
+
+ if (hideOnUnsupported) return null;
+
+ if (fallback) return <>{fallback}>;
+
+ // Default fallback: subtle message
+ return (
+
+ ℹ️
+ This feature requires a supported host. Ask the assistant to perform this action.
+
+ );
+};
+```
+
+### 4c. Refactored `ActionButton` with Fallback
+
+```tsx
+// components/shared/ActionButton.tsx (revised)
+
+export const ActionButton: React.FC = ({
+ label,
+ variant = "secondary",
+ size = "md",
+ disabled,
+ toolName,
+ toolArgs,
+}) => {
+ const { safeCallTool } = useMCPApp();
+ const { tier, canCallTools, canSendMessages } = useHostCapabilities();
+ const [loading, setLoading] = useState(false);
+ const [pendingMessage, setPendingMessage] = useState(null);
+
+ const handleClick = useCallback(async () => {
+ if (!toolName || loading || disabled) return;
+
+ setLoading(true);
+ setPendingMessage(null);
+ try {
+ const result = await safeCallTool(toolName, toolArgs || {});
+
+ // Tier 2: show "reported to assistant" feedback
+ if (!canCallTools && result) {
+ setPendingMessage("Reported to assistant");
+ setTimeout(() => setPendingMessage(null), 3000);
+ }
+ } catch {
+ // handled by context
+ } finally {
+ setLoading(false);
+ }
+ }, [toolName, toolArgs, loading, disabled, safeCallTool, canCallTools]);
+
+ // Tier 1 with no context sync: show disabled with hint
+ if (tier === "static" && toolName) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+};
+```
+
+### 4d. Refactored `KanbanBoard` — Local-First + Context Sync
+
+The KanbanBoard already uses optimistic local state. The only change: fall back to `updateModelContext` when `moveTool` can't be called directly.
+
+```tsx
+// In KanbanBoard onDrop handler (revised):
+
+const onDrop = useCallback(async (e: React.DragEvent, toStageId: string) => {
+ e.preventDefault();
+ setDropTargetStage(null);
+
+ const drag = dragRef.current;
+ if (!drag) return;
+ const { cardId, fromStageId } = drag;
+ dragRef.current = null;
+ setDraggingCardId(null);
+ if (fromStageId === toStageId) return;
+
+ // 1. Optimistic local update (same as before)
+ let movedCard: KanbanCard | undefined;
+ const prevColumns = columns;
+ setColumns(prev => { /* ... same optimistic logic ... */ });
+
+ // 2. Use safeCallTool — works at Tier 2 AND Tier 3
+ if (moveTool) {
+ try {
+ const result = await safeCallTool(moveTool, {
+ opportunityId: cardId,
+ pipelineStageId: toStageId,
+ });
+
+ // Tier 2: result is a "reported" message — no revert needed
+ // Local state IS the source of truth until LLM processes it
+ if (!capabilities.canCallTools && result) {
+ // Also report the full board state so LLM has context
+ await reportBoardState();
+ }
+ } catch (err) {
+ // Only revert if we attempted a real tool call (Tier 3)
+ if (capabilities.canCallTools) {
+ console.error("KanbanBoard: move failed, reverting", err);
+ setColumns(prevColumns);
+ }
+ }
+ }
+}, [columns, moveTool, safeCallTool, capabilities]);
+
+// Report full board state to LLM for context
+const reportBoardState = useCallback(async () => {
+ if (!app || !capabilities.canUpdateContext) return;
+
+ const summary = columns.map(col =>
+ `**${col.title}** (${col.cards?.length ?? 0}):\n` +
+ (col.cards || []).map(c => ` - ${c.title} (${c.value || 'no value'})`).join('\n')
+ ).join('\n\n');
+
+ await app.updateModelContext({
+ content: [{
+ type: "text",
+ text: `---\ncomponent: kanban-board\nupdated: ${new Date().toISOString()}\n---\nCurrent pipeline state:\n\n${summary}`,
+ }],
+ });
+}, [app, capabilities, columns]);
+```
+
+### 4e. Refactored `EditableField` — Works at All Tiers
+
+```tsx
+// In EditableField (revised handleSave):
+
+const handleSave = async () => {
+ setIsEditing(false);
+ if (editValue === value) return;
+
+ if (saveTool) {
+ const result = await safeCallTool(saveTool, {
+ ...saveArgs,
+ value: editValue,
+ });
+
+ // Tier 2: field stays locally updated; LLM is informed
+ // Tier 3: server confirms; UI may refresh via ontoolresult
+ // Tier 1: nothing happens — but user sees their edit locally
+
+ if (!result && tier === "static") {
+ // Show hint that the change is display-only
+ setLocalOnlyHint(true);
+ setTimeout(() => setLocalOnlyHint(false), 3000);
+ }
+ }
+};
+```
+
+---
+
+## 5. Alternative Interaction: `updateModelContext` as Primary Channel
+
+For hosts that support `updateModelContext` but not `serverTools`, we can use a **declarative intent** pattern instead of imperative tool calls:
+
+```typescript
+// hooks/useReportInteraction.ts
+
+export function useReportInteraction() {
+ const { app } = useMCPApp();
+ const { canUpdateContext } = useHostCapabilities();
+
+ return useCallback(
+ async (interaction: {
+ component: string;
+ action: string;
+ data: Record;
+ suggestion?: string; // What we suggest the LLM should do
+ }) => {
+ if (!canUpdateContext || !app) return;
+
+ const text = [
+ `---`,
+ `component: ${interaction.component}`,
+ `action: ${interaction.action}`,
+ `timestamp: ${new Date().toISOString()}`,
+ ...Object.entries(interaction.data).map(([k, v]) => `${k}: ${JSON.stringify(v)}`),
+ `---`,
+ interaction.suggestion || `User performed "${interaction.action}" in ${interaction.component}.`,
+ ].join("\n");
+
+ await app.updateModelContext({
+ content: [{ type: "text", text }],
+ });
+ },
+ [app, canUpdateContext]
+ );
+}
+```
+
+**Usage in any component:**
+
+```tsx
+const reportInteraction = useReportInteraction();
+
+// In a form submit handler:
+await reportInteraction({
+ component: "AppointmentBooker",
+ action: "book_appointment",
+ data: { contactId, calendarId, startTime, endTime },
+ suggestion: "User wants to book this appointment. Please call create_appointment with these details.",
+});
+```
+
+---
+
+## 6. Server-Side: Dual-Mode Tool Registration
+
+The MCP server should register tools that work both ways:
+
+```typescript
+// Server-side: Register tool as visible to both model and app
+registerAppTool(server, "move_opportunity", {
+ description: "Move an opportunity to a different pipeline stage",
+ inputSchema: {
+ opportunityId: z.string(),
+ pipelineStageId: z.string(),
+ },
+ _meta: {
+ ui: {
+ resourceUri: "ui://ghl/pipeline-view",
+ visibility: ["model", "app"], // ← Both can call it
+ },
+ },
+}, handler);
+```
+
+With `visibility: ["model", "app"]`:
+- **Tier 3 hosts:** App calls it directly via `callServerTool`
+- **Tier 2 hosts:** App reports intent via `updateModelContext`, LLM sees the tool in its tool list and calls it on the next turn
+- **Tier 1 hosts:** Tool still works as text-only through normal MCP
+
+---
+
+## 7. `window.__MCP_APP_DATA__` Pre-Injection (Tier 0)
+
+The existing `getPreInjectedTree()` in App.tsx already supports a non-MCP path. For environments where the iframe loads but the ext-apps SDK never connects:
+
+```typescript
+// App.tsx already handles this:
+useEffect(() => {
+ const preInjected = getPreInjectedTree();
+ if (preInjected && !uiTree) {
+ setUITree(preInjected);
+ }
+}, []);
+```
+
+This serves as **Tier 0**: pure server-side rendering. The server injects the UITree directly into the HTML. No SDK connection needed. All components render in static mode.
+
+---
+
+## 8. CSS for Degraded States
+
+```css
+/* styles/fallback.css */
+
+/* Unsupported interactive elements */
+.btn-unsupported {
+ opacity: 0.6;
+ cursor: not-allowed;
+ position: relative;
+}
+.btn-unsupported::after {
+ content: "↗";
+ font-size: 0.7em;
+ margin-left: 4px;
+ opacity: 0.5;
+}
+
+/* "Reported to assistant" badge */
+.btn-pending-badge {
+ margin-left: 8px;
+ font-size: 0.75em;
+ color: var(--color-text-info, #3b82f6);
+ animation: fadeIn 0.2s ease-in;
+}
+
+/* Unavailable feature hint */
+.interactive-unavailable {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ border-radius: var(--border-radius-md, 6px);
+ background: var(--color-background-info, #eff6ff);
+ color: var(--color-text-secondary, #6b7280);
+ font-size: 0.85em;
+}
+
+/* Local-only edit hint */
+.ef-local-hint {
+ font-size: 0.75em;
+ color: var(--color-text-warning, #f59e0b);
+ margin-top: 2px;
+}
+
+/* Editable fields in static mode: remove edit affordance */
+[data-tier="static"] .ef-edit-icon {
+ display: none;
+}
+[data-tier="static"] .ef-display {
+ cursor: default;
+}
+```
+
+---
+
+## 9. Decision Matrix
+
+| Scenario | Detection | Behavior |
+|----------|-----------|----------|
+| Claude Desktop (full support) | `serverTools` ✓ | Tier 3: all features work |
+| Host with context-only support | `updateModelContext` ✓, `serverTools` ✗ | Tier 2: local + LLM sync |
+| Minimal host / basic iframe | No capabilities | Tier 1: static display |
+| Pre-injected `__MCP_APP_DATA__` | No SDK connection | Tier 0: SSR static |
+| `callServerTool` fails at runtime | Error caught | Downgrade to Tier 2 dynamically |
+
+---
+
+## 10. Summary: What to Implement
+
+### Phase 1 (Essential)
+1. **`useHostCapabilities` hook** — reads `app.getHostCapabilities()`, computes tier
+2. **`safeCallTool` in MCPAppContext** — wraps callTool with fallback to updateModelContext
+3. **Update `ActionButton`** — use safeCallTool, show disabled state at Tier 1
+4. **Update `KanbanBoard`** — already local-first, just wire up safeCallTool + reportBoardState
+5. **`fallback.css`** — styles for degraded states
+
+### Phase 2 (Full Coverage)
+6. **`useReportInteraction` hook** — standard way to inform LLM of UI actions
+7. **`` component** — declarative tier gating in templates
+8. **Update all interactive components** — EditableField, FormGroup, AppointmentBooker, etc.
+9. **Runtime downgrade** — if a Tier 3 call fails, auto-downgrade to Tier 2
+
+### Phase 3 (Polish)
+10. **Toast notifications** for tier-specific feedback ("Action sent to assistant")
+11. **`data-tier` attribute** on root for CSS-only degradation
+12. **Testing matrix** — automated tests per tier per component
+
+---
+
+## Key Insight
+
+The ext-apps spec was **designed** for this: *"UI is a progressive enhancement, not a requirement."* Our architecture mirrors this philosophy — every component starts as a static data display and progressively gains interactivity based on what the host reports it can do. The KanbanBoard's existing optimistic-update pattern is already the correct Tier 2 pattern; we just need to formalize it and apply it consistently.
diff --git a/package-lock.json b/package-lock.json
index d8dfdfe..a4a27d4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,22 +1,15 @@
{
-<<<<<<< HEAD
- "name": "ghl-mcp",
-=======
"name": "@mastanley13/ghl-mcp-server",
->>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
-<<<<<<< HEAD
- "name": "ghl-mcp",
-=======
"name": "@mastanley13/ghl-mcp-server",
->>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"version": "1.0.0",
"license": "ISC",
"dependencies": {
+ "@anthropic-ai/sdk": "^0.72.1",
"@modelcontextprotocol/sdk": "^1.12.1",
"@types/cors": "^2.8.18",
"@types/express": "^5.0.2",
@@ -25,12 +18,9 @@
"dotenv": "^16.5.0",
"express": "^5.1.0"
},
-<<<<<<< HEAD
-=======
"bin": {
"ghl-mcp-server": "dist/server.js"
},
->>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/node": "^22.15.29",
@@ -39,12 +29,9 @@
"ts-jest": "^29.3.4",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
-<<<<<<< HEAD
-=======
},
"engines": {
"node": ">=18.0.0"
->>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
}
},
"node_modules/@ampproject/remapping": {
@@ -60,6 +47,26 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@anthropic-ai/sdk": {
+ "version": "0.72.1",
+ "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.72.1.tgz",
+ "integrity": "sha512-MiUnue7qN7DvLIoYHgkedN2z05mRf2CutBzjXXY2krzOhG2r/rIfISS2uVkNLikgToB5hYIzw+xp2jdOtRkqYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "json-schema-to-ts": "^3.1.1"
+ },
+ "bin": {
+ "anthropic-ai-sdk": "bin/cli"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -88,10 +95,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
"dev": true,
-<<<<<<< HEAD
-=======
"peer": true,
->>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -465,6 +469,15 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
+ "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/template": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
@@ -1102,10 +1115,7 @@
"version": "22.15.29",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz",
"integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==",
-<<<<<<< HEAD
-=======
"peer": true,
->>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -1487,10 +1497,7 @@
"url": "https://github.com/sponsors/ai"
}
],
-<<<<<<< HEAD
-=======
"peer": true,
->>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"dependencies": {
"caniuse-lite": "^1.0.30001718",
"electron-to-chromium": "^1.5.160",
@@ -2155,10 +2162,7 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
-<<<<<<< HEAD
-=======
"peer": true,
->>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.0",
@@ -2878,10 +2882,7 @@
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true,
-<<<<<<< HEAD
-=======
"peer": true,
->>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@@ -3481,6 +3482,19 @@
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true
},
+ "node_modules/json-schema-to-ts": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
+ "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "ts-algebra": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -4613,6 +4627,12 @@
"nodetouch": "bin/nodetouch.js"
}
},
+ "node_modules/ts-algebra": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
+ "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
+ "license": "MIT"
+ },
"node_modules/ts-jest": {
"version": "29.3.4",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz",
@@ -4691,10 +4711,7 @@
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
-<<<<<<< HEAD
-=======
"peer": true,
->>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
@@ -4772,10 +4789,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
-<<<<<<< HEAD
-=======
"peer": true,
->>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -4994,10 +5008,7 @@
"version": "3.25.51",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.51.tgz",
"integrity": "sha512-TQSnBldh+XSGL+opiSIq0575wvDPqu09AqWe1F7JhUMKY+M91/aGlK4MhpVNO7MgYfHcVCB1ffwAUTJzllKJqg==",
-<<<<<<< HEAD
-=======
"peer": true,
->>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/package.json b/package.json
index b7e04be..6941d6e 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,8 @@
"node": ">=18.0.0"
},
"scripts": {
- "build": "tsc",
+ "build:dynamic-ui": "cd src/ui/react-app && npm run build",
+ "build": "npm run build:dynamic-ui && tsc",
"dev": "nodemon --exec ts-node src/http-server.ts",
"start": "node dist/http-server.js",
"start:stdio": "node dist/server.js",
@@ -45,6 +46,7 @@
"typescript": "^5.8.3"
},
"dependencies": {
+ "@anthropic-ai/sdk": "^0.72.1",
"@modelcontextprotocol/sdk": "^1.12.1",
"@types/cors": "^2.8.18",
"@types/express": "^5.0.2",
diff --git a/src/apps/index.ts b/src/apps/index.ts
index b37b25c..46429a7 100644
--- a/src/apps/index.ts
+++ b/src/apps/index.ts
@@ -1,13 +1,292 @@
/**
- * MCP Apps Manager
- * Manages rich UI components for GoHighLevel MCP Server
+ * MCP Apps Manager — Universal Renderer Architecture
+ *
+ * All views route through ONE universal renderer HTML file that takes a JSON UITree.
+ * Pre-made templates provide deterministic views for the 11 standard tools.
+ * The generate_ghl_view tool uses Claude to create novel views on the fly.
*/
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { GHLApiClient } from '../clients/ghl-api-client.js';
import * as fs from 'fs';
import * as path from 'path';
-import { fileURLToPath } from 'url';
+import Anthropic from '@anthropic-ai/sdk';
+import { UITree, validateUITree } from './types.js';
+import {
+ buildContactGridTree,
+ buildPipelineBoardTree,
+ buildQuickBookTree,
+ buildOpportunityCardTree,
+ buildCalendarViewTree,
+ buildInvoicePreviewTree,
+ buildCampaignStatsTree,
+ buildAgentStatsTree,
+ buildContactTimelineTree,
+ buildWorkflowStatusTree,
+ buildDashboardTree,
+} from './templates/index.js';
+
+// ─── Catalog System Prompt (source of truth for components) ──
+
+const CATALOG_SYSTEM_PROMPT = `You are a UI generator for GoHighLevel (GHL) CRM applications.
+You generate JSON UI trees using the component catalog below. Your output MUST be valid JSON matching the UITree schema.
+
+## RULES
+1. Only use components defined in the catalog
+2. Every element must have a unique "key", a "type" (matching a catalog component), and "props"
+3. Parent elements list children by key in their "children" array
+4. **USE THE PROVIDED GHL DATA** — if real data is included below, you MUST use it. Do NOT invent fake data when real data is available.
+5. Keep layouts information-dense and professional
+6. Respond with ONLY the JSON object. No markdown fences, no explanation.
+
+## LAYOUT RULES (CRITICAL)
+- Design for a **single viewport** — the view should fit on one screen without scrolling
+- Maximum **15 elements** total in the tree. Fewer is better.
+- Use **SplitLayout** for side-by-side content, not stacked cards that go off-screen
+- Use **StatsGrid** with 3-4 MetricCards max for KPIs — don't list every metric
+- For tables, limit to **10 rows max**. Show most relevant data, not everything.
+- For KanbanBoard, limit to **5 columns** and **4 cards per column** max
+- Prefer compact components: MetricCard, StatusBadge, KeyValueList over verbose layouts
+- ONE PageHeader max. Don't nest sections inside sections.
+- Think **dashboard widget**, not **full report page**
+
+## UI TREE FORMAT
+{
+ "root": "",
+ "elements": {
+ "": {
+ "key": "",
+ "type": "",
+ "props": { ... },
+ "children": ["", ""]
+ }
+ }
+}
+
+## COMPONENT CATALOG
+
+### PageHeader
+Top-level page header with title, subtitle, status badge, and summary stats.
+Props: title (string, required), subtitle (string?), status (string?), statusVariant ("active"|"complete"|"paused"|"draft"|"error"|"sent"|"paid"|"pending"?), gradient (boolean?), stats (array of {label, value}?)
+Can contain children.
+
+### Card
+Container card with optional header and padding.
+Props: title (string?), subtitle (string?), padding ("none"|"sm"|"md"|"lg"?), noBorder (boolean?)
+Can contain children.
+
+### StatsGrid
+Grid of metric cards showing key numbers.
+Props: columns (number?)
+Can contain children (typically MetricCard elements).
+
+### SplitLayout
+Two-column layout for side-by-side content.
+Props: ratio ("50/50"|"33/67"|"67/33"?), gap ("sm"|"md"|"lg"?)
+Can contain children (exactly 2 children for left/right).
+
+### Section
+Titled content section.
+Props: title (string?), description (string?)
+Can contain children.
+
+### DataTable
+Sortable data table with column definitions and row actions.
+Props: columns (array of {key, label, sortable?, align?, format?, width?}), rows (array of objects), selectable (boolean?), rowAction (string?), emptyMessage (string?), pageSize (number?)
+Format options: "text"|"email"|"phone"|"date"|"currency"|"tags"|"avatar"|"status"
+
+### KanbanBoard
+Kanban-style board with columns and cards. Used for pipeline views.
+Props: columns (array of {id, title, count?, totalValue?, color?, cards: [{id, title, subtitle?, value?, status?, statusVariant?, date?, avatarInitials?}]})
+
+### MetricCard
+Single metric display with big number, label, and optional trend.
+Props: label (string), value (string), format ("number"|"currency"|"percent"?), trend ("up"|"down"|"flat"?), trendValue (string?), color ("default"|"green"|"blue"|"purple"|"yellow"|"red"?)
+
+### StatusBadge
+Colored badge showing entity status.
+Props: label (string), variant ("active"|"complete"|"paused"|"draft"|"error"|"sent"|"paid"|"pending"|"open"|"won"|"lost")
+
+### Timeline
+Chronological event list for activity feeds.
+Props: events (array of {id, title, description?, timestamp, icon?, variant?})
+Icon options: "email"|"phone"|"note"|"meeting"|"task"|"system"
+
+### ProgressBar
+Percentage bar with label and value.
+Props: label (string), value (number), max (number?), color ("green"|"blue"|"purple"|"yellow"|"red"?), showPercent (boolean?), benchmark (number?), benchmarkLabel (string?)
+
+### DetailHeader
+Header for detail/preview pages with entity name, ID, status.
+Props: title (string), subtitle (string?), entityId (string?), status (string?), statusVariant?
+Can contain children.
+
+### KeyValueList
+List of label-value pairs for totals, metadata.
+Props: items (array of {label, value, bold?, variant?, isTotalRow?}), compact (boolean?)
+Variant options: "default"|"highlight"|"muted"|"success"|"danger"
+
+### LineItemsTable
+Invoice-style table with quantities and prices.
+Props: items (array of {name, description?, quantity, unitPrice, total}), currency (string?)
+
+### InfoBlock
+Labeled block of information (e.g. From/To on invoices).
+Props: label (string), name (string), lines (string[])
+
+### SearchBar
+Search input with placeholder.
+Props: placeholder (string?), valuePath (string?)
+
+### FilterChips
+Toggleable filter tags.
+Props: chips (array of {label, value, active?}), dataPath (string?)
+
+### TabGroup
+Tab navigation for switching views.
+Props: tabs (array of {label, value, count?}), activeTab (string?), dataPath (string?)
+
+### ActionButton
+Clickable button with variants.
+Props: label (string), variant ("primary"|"secondary"|"danger"|"ghost"?), size ("sm"|"md"|"lg"?), icon (string?), disabled (boolean?)
+
+### ActionBar
+Row of action buttons.
+Props: align ("left"|"center"|"right"?)
+Can contain children (ActionButton elements).
+
+### CurrencyDisplay
+Formatted monetary value with currency symbol and locale-aware formatting.
+Props: amount (number, required), currency (string? default "USD"), locale (string? default "en-US"), size ("sm"|"md"|"lg"?), positive (boolean?), negative (boolean?)
+
+### TagList
+Visual tag/chip display for arrays of tags rendered as inline colored pills.
+Props: tags (array of {label, color?, variant?} or strings, required), maxVisible (number?), size ("sm"|"md"?)
+
+### CardGrid
+Grid of visual cards with image, title, description for browsable catalogs and listings.
+Props: cards (array of {title, description?, imageUrl?, subtitle?, status?, statusVariant?, action?}, required), columns (number? default 3)
+
+### AvatarGroup
+Stacked circular avatars for displaying users, followers, or team members.
+Props: avatars (array of {name, imageUrl?, initials?}, required), max (number? default 5), size ("sm"|"md"|"lg"?)
+
+### StarRating
+Visual star rating display (1-5).
+Props: rating (number, required), count (number?), maxStars (number? default 5), distribution (array of {stars, count}?), showDistribution (boolean?)
+
+### StockIndicator
+Visual stock level indicator showing green/yellow/red status with quantity.
+Props: quantity (number, required), lowThreshold (number?), criticalThreshold (number?), label (string?)
+
+### ChatThread
+Conversation message thread with chat bubbles.
+Props: messages (array of {id, content, direction: "inbound"|"outbound", type?, timestamp, senderName?, avatar?}), title (string?)
+
+### EmailPreview
+Rendered HTML email with header info in a bordered container.
+Props: from (string), to (string), subject (string), date (string), body (string), cc (string?), attachments (array of {name, size}?)
+
+### ContentPreview
+Rich text/HTML content preview (sanitized).
+Props: content (string), format ("html"|"markdown"|"text"?), maxHeight (number?), title (string?)
+
+### TranscriptView
+Time-stamped conversation transcript with speaker labels.
+Props: entries (array of {timestamp, speaker, text, speakerRole?}), title (string?), duration (string?)
+
+### AudioPlayer
+Visual audio player UI with play button and waveform visualization.
+Props: src (string?), title (string?), duration (string?), type ("recording"|"voicemail"?)
+
+### ChecklistView
+Task/checklist with checkboxes, due dates, assignees, and priority indicators.
+Props: items (array of {id, title, completed?, dueDate?, assignee?, priority?}), title (string?), showProgress (boolean?)
+
+### CalendarView
+Monthly calendar grid with color-coded event blocks.
+Props: year (number?), month (number?), events (array of {date, title, time?, color?, type?}), highlightToday (boolean?), title (string?)
+
+### FlowDiagram
+Linear node→arrow→node flow for triggers, IVR menus, funnel pages.
+Props: nodes (array of {id, label, type?, description?}), edges (array of {from, to, label?}), direction ("horizontal"|"vertical"?), title (string?)
+
+### TreeView
+Hierarchical expandable tree.
+Props: nodes (array of {id, label, icon?, children?, expanded?, badge?}), title (string?), expandAll (boolean?)
+
+### MediaGallery
+Thumbnail grid for images/files.
+Props: items (array of {url, thumbnailUrl?, title?, fileType?, fileSize?, date?}), columns (number?), title (string?)
+
+### DuplicateCompare
+Side-by-side record comparison with field-level diff highlighting.
+Props: records (array of {label, fields: Record} — exactly 2), highlightDiffs (boolean?), title (string?)
+
+### BarChart
+Vertical or horizontal bar chart.
+Props: bars (array of {label, value, color?}), orientation ("vertical"|"horizontal"?), maxValue (number?), showValues (boolean?), title (string?)
+
+### LineChart
+Time-series line chart with optional area fill.
+Props: points (array of {label, value}), color (string?), showPoints (boolean?), showArea (boolean?), title (string?), yAxisLabel (string?)
+
+### PieChart
+Pie or donut chart for proportional breakdowns.
+Props: segments (array of {label, value, color?}), donut (boolean?), title (string?), showLegend (boolean?)
+
+### FunnelChart
+Horizontal funnel showing stage drop-off.
+Props: stages (array of {label, value, color?}), showDropoff (boolean?), title (string?)
+
+### SparklineChart
+Tiny inline chart.
+Props: values (number[]), color (string?), height (number?), width (number?)
+
+### ContactPicker
+Searchable contact dropdown.
+Props: searchTool (string, required), placeholder (string?), value (any?)
+
+### InvoiceBuilder
+Multi-section invoice form.
+Props: createTool (string?), contactSearchTool (string?), initialContact (any?), initialItems (array?)
+
+### OpportunityEditor
+Inline editor for deal/opportunity fields.
+Props: saveTool (string, required), opportunity (object, required), stages (array of {id, name}?)
+
+### AppointmentBooker
+Calendar-based appointment booking form.
+Props: calendarTool (string?), bookTool (string?), contactSearchTool (string?), calendarId (string?)
+
+### EditableField
+Click-to-edit wrapper for any text value.
+Props: value (string, required), fieldName (string, required), saveTool (string?), saveArgs (object?)
+
+### SelectDropdown
+Dropdown select.
+Props: loadTool (string?), loadArgs (object?), options (array of {label, value}?), value (string?), placeholder (string?)
+
+### FormGroup
+Group of form fields with labels and validation.
+Props: fields (array of {key, label, type?, value?, required?, options?}, required), submitLabel (string?), submitTool (string?)
+
+### AmountInput
+Currency-formatted number input.
+Props: value (number, required), currency (string?)
+
+## DATA RULES (CRITICAL — READ CAREFULLY)
+- If real GHL data is provided in the user message, use ONLY that data. Do NOT add, invent, or embellish any records.
+- Pipeline stages MUST come from the provided data. Never invent stage names unless they literally exist in the data.
+- Show exactly the records provided. If there are 2 opportunities, show 2. Don't add fake ones.
+- If no data is provided, THEN you may use sample data, but keep it minimal (3-5 records max).
+- When generating interactive views, use correct tool names for GHL:
+ - ContactPicker: searchTool="search_contacts"
+ - InvoiceBuilder: createTool="create_invoice", contactSearchTool="search_contacts"
+ - OpportunityEditor: saveTool="update_opportunity"
+ - KanbanBoard: moveTool="update_opportunity"`;
+
+// ─── Types ──────────────────────────────────────────────────
export interface AppToolResult {
content: Array<{ type: 'text'; text: string }>;
@@ -21,32 +300,25 @@ export interface AppResourceHandler {
getContent: () => string;
}
-/**
- * MCP Apps Manager class
- * Registers app tools and handles structuredContent responses
- */
-// Resolve UI build path - works regardless of working directory
+// ─── UI Build Path Resolver ─────────────────────────────────
+
function getUIBuildPath(): string {
- // When compiled, this file is at dist/apps/index.js
- // UI files are at dist/app-ui/
- // Use __dirname which is available in CommonJS
const fromDist = path.resolve(__dirname, '..', 'app-ui');
- if (fs.existsSync(fromDist)) {
- return fromDist;
- }
- // Fallback: try process.cwd() based paths
+ if (fs.existsSync(fromDist)) return fromDist;
const appUiPath = path.join(process.cwd(), 'dist', 'app-ui');
- if (fs.existsSync(appUiPath)) {
- return appUiPath;
- }
- // Default fallback
+ if (fs.existsSync(appUiPath)) return appUiPath;
return fromDist;
}
+// ─── MCP Apps Manager ──────────────────────────────────────
+
export class MCPAppsManager {
private ghlClient: GHLApiClient;
private resourceHandlers: Map = new Map();
private uiBuildPath: string;
+ private pendingDynamicData: any = null;
+ /** Cached universal renderer HTML */
+ private rendererHTML: string | null = null;
constructor(ghlClient: GHLApiClient) {
this.ghlClient = ghlClient;
@@ -55,87 +327,101 @@ export class MCPAppsManager {
this.registerResourceHandlers();
}
- /**
- * Register all UI resource handlers
- */
+ // ─── Resource Registration ──────────────────────────────
+
private registerResourceHandlers(): void {
- const resources: Array<{ uri: string; file: string }> = [
- // All 11 MCP Apps
- { uri: 'ui://ghl/mcp-app', file: 'mcp-app.html' },
- { uri: 'ui://ghl/pipeline-board', file: 'pipeline-board.html' },
- { uri: 'ui://ghl/quick-book', file: 'quick-book.html' },
- { uri: 'ui://ghl/opportunity-card', file: 'opportunity-card.html' },
- { uri: 'ui://ghl/contact-grid', file: 'contact-grid.html' },
- { uri: 'ui://ghl/calendar-view', file: 'calendar-view.html' },
- { uri: 'ui://ghl/invoice-preview', file: 'invoice-preview.html' },
- { uri: 'ui://ghl/campaign-stats', file: 'campaign-stats.html' },
- { uri: 'ui://ghl/agent-stats', file: 'agent-stats.html' },
- { uri: 'ui://ghl/contact-timeline', file: 'contact-timeline.html' },
- { uri: 'ui://ghl/workflow-status', file: 'workflow-status.html' },
+ // Universal renderer is the ONLY real resource
+ // All view_* tools inject their UITree into this same renderer
+ const universalResource = {
+ uri: 'ui://ghl/app',
+ mimeType: 'text/html;profile=mcp-app',
+ getContent: () => {
+ const html = this.getRendererHTML();
+ if (this.pendingDynamicData) {
+ const data = this.pendingDynamicData;
+ this.pendingDynamicData = null;
+ process.stderr.write(`[MCP Apps] Injecting UITree into universal renderer\n`);
+ return this.injectDataIntoHTML(html, data);
+ }
+ return html;
+ },
+ };
+
+ this.resourceHandlers.set('ui://ghl/app', universalResource);
+
+ // Keep dynamic-view as an alias for backward compatibility
+ this.resourceHandlers.set('ui://ghl/dynamic-view', {
+ ...universalResource,
+ uri: 'ui://ghl/dynamic-view',
+ });
+
+ // Legacy resource URIs — all point to the universal renderer
+ const legacyURIs = [
+ 'ui://ghl/mcp-app',
+ 'ui://ghl/pipeline-board',
+ 'ui://ghl/quick-book',
+ 'ui://ghl/opportunity-card',
+ 'ui://ghl/contact-grid',
+ 'ui://ghl/calendar-view',
+ 'ui://ghl/invoice-preview',
+ 'ui://ghl/campaign-stats',
+ 'ui://ghl/agent-stats',
+ 'ui://ghl/contact-timeline',
+ 'ui://ghl/workflow-status',
];
- for (const resource of resources) {
- this.resourceHandlers.set(resource.uri, {
- uri: resource.uri,
+ for (const uri of legacyURIs) {
+ this.resourceHandlers.set(uri, {
+ uri,
mimeType: 'text/html;profile=mcp-app',
- getContent: () => this.loadUIResource(resource.file),
+ getContent: universalResource.getContent,
});
}
}
/**
- * Load UI resource from build directory
+ * Load and cache the universal renderer HTML
*/
- private loadUIResource(filename: string): string {
- const filePath = path.join(this.uiBuildPath, filename);
- try {
- return fs.readFileSync(filePath, 'utf-8');
- } catch (error) {
- process.stderr.write(`[MCP Apps] UI resource not found: ${filePath}\n`);
- return this.getFallbackHTML(filename);
- }
- }
+ private getRendererHTML(): string {
+ if (this.rendererHTML) return this.rendererHTML;
- /**
- * Generate fallback HTML when UI resource is not built
- */
- private getFallbackHTML(filename: string): string {
- const componentName = filename.replace('.html', '');
- return `
-
-
-
-
-
- GHL ${componentName}
-
-
-
-
-
UI component "${componentName}" is loading...
-
Run npm run build:ui to build UI components.
-
-
-
-
- `.trim();
+ }
+
+ process.stderr.write(`[MCP Apps] WARNING: Universal renderer HTML not found, using fallback\n`);
+ this.rendererHTML = this.getFallbackHTML();
+ return this.rendererHTML;
}
- /**
- * Get tool definitions for all app tools
- */
+ private getFallbackHTML(): string {
+ return `
+
+GHL View
+
+UI renderer is loading...
Run npm run build:dynamic-ui to build.
+`;
+ }
+
+ // ─── Tool Definitions ───────────────────────────────────
+
getToolDefinitions(): Tool[] {
+ // All tools point to the universal renderer resource
+ const appUri = 'ui://ghl/app';
+
return [
- // 1. Contact Grid - search and display contacts
{
name: 'view_contact_grid',
description: 'Display contact search results in a data grid with sorting and pagination. Returns a visual UI component.',
@@ -143,29 +429,23 @@ export class MCPAppsManager {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query string' },
- limit: { type: 'number', description: 'Maximum results (default: 25)' }
- }
+ limit: { type: 'number', description: 'Maximum results (default: 25)' },
+ },
},
- _meta: {
- ui: { resourceUri: 'ui://ghl/contact-grid' }
- }
+ _meta: { ui: { resourceUri: appUri } },
},
- // 2. Pipeline Board - Kanban view of opportunities
{
name: 'view_pipeline_board',
description: 'Display a pipeline as an interactive Kanban board with opportunities. Returns a visual UI component.',
inputSchema: {
type: 'object',
properties: {
- pipelineId: { type: 'string', description: 'Pipeline ID to display' }
+ pipelineId: { type: 'string', description: 'Pipeline ID to display' },
},
- required: ['pipelineId']
+ required: ['pipelineId'],
},
- _meta: {
- ui: { resourceUri: 'ui://ghl/pipeline-board' }
- }
+ _meta: { ui: { resourceUri: appUri } },
},
- // 3. Quick Book - appointment booking
{
name: 'view_quick_book',
description: 'Display a quick booking interface for scheduling appointments. Returns a visual UI component.',
@@ -173,30 +453,24 @@ export class MCPAppsManager {
type: 'object',
properties: {
calendarId: { type: 'string', description: 'Calendar ID for booking' },
- contactId: { type: 'string', description: 'Optional contact ID to pre-fill' }
+ contactId: { type: 'string', description: 'Optional contact ID to pre-fill' },
},
- required: ['calendarId']
+ required: ['calendarId'],
},
- _meta: {
- ui: { resourceUri: 'ui://ghl/quick-book' }
- }
+ _meta: { ui: { resourceUri: appUri } },
},
- // 4. Opportunity Card - single opportunity details
{
name: 'view_opportunity_card',
description: 'Display a single opportunity with details, value, and stage info. Returns a visual UI component.',
inputSchema: {
type: 'object',
properties: {
- opportunityId: { type: 'string', description: 'Opportunity ID to display' }
+ opportunityId: { type: 'string', description: 'Opportunity ID to display' },
},
- required: ['opportunityId']
+ required: ['opportunityId'],
},
- _meta: {
- ui: { resourceUri: 'ui://ghl/opportunity-card' }
- }
+ _meta: { ui: { resourceUri: appUri } },
},
- // 5. Calendar View - calendar with events
{
name: 'view_calendar',
description: 'Display a calendar with events and appointments. Returns a visual UI component.',
@@ -205,45 +479,36 @@ export class MCPAppsManager {
properties: {
calendarId: { type: 'string', description: 'Calendar ID to display' },
startDate: { type: 'string', description: 'Start date (ISO format)' },
- endDate: { type: 'string', description: 'End date (ISO format)' }
+ endDate: { type: 'string', description: 'End date (ISO format)' },
},
- required: ['calendarId']
+ required: ['calendarId'],
},
- _meta: {
- ui: { resourceUri: 'ui://ghl/calendar-view' }
- }
+ _meta: { ui: { resourceUri: appUri } },
},
- // 6. Invoice Preview - invoice details
{
name: 'view_invoice',
description: 'Display an invoice preview with line items and payment status. Returns a visual UI component.',
inputSchema: {
type: 'object',
properties: {
- invoiceId: { type: 'string', description: 'Invoice ID to display' }
+ invoiceId: { type: 'string', description: 'Invoice ID to display' },
},
- required: ['invoiceId']
+ required: ['invoiceId'],
},
- _meta: {
- ui: { resourceUri: 'ui://ghl/invoice-preview' }
- }
+ _meta: { ui: { resourceUri: appUri } },
},
- // 7. Campaign Stats - campaign performance metrics
{
name: 'view_campaign_stats',
description: 'Display campaign statistics and performance metrics. Returns a visual UI component.',
inputSchema: {
type: 'object',
properties: {
- campaignId: { type: 'string', description: 'Campaign ID to display stats for' }
+ campaignId: { type: 'string', description: 'Campaign ID to display stats for' },
},
- required: ['campaignId']
+ required: ['campaignId'],
},
- _meta: {
- ui: { resourceUri: 'ui://ghl/campaign-stats' }
- }
+ _meta: { ui: { resourceUri: appUri } },
},
- // 8. Agent Stats - agent/user performance
{
name: 'view_agent_stats',
description: 'Display agent/user performance statistics and metrics. Returns a visual UI component.',
@@ -251,56 +516,64 @@ export class MCPAppsManager {
type: 'object',
properties: {
userId: { type: 'string', description: 'User/Agent ID to display stats for' },
- dateRange: { type: 'string', description: 'Date range (e.g., "last7days", "last30days")' }
- }
+ dateRange: { type: 'string', description: 'Date range (e.g., "last7days", "last30days")' },
+ },
},
- _meta: {
- ui: { resourceUri: 'ui://ghl/agent-stats' }
- }
+ _meta: { ui: { resourceUri: appUri } },
},
- // 9. Contact Timeline - activity history for a contact
{
name: 'view_contact_timeline',
- description: 'Display a contact\'s activity timeline with all interactions. Returns a visual UI component.',
+ description: "Display a contact's activity timeline with all interactions. Returns a visual UI component.",
inputSchema: {
type: 'object',
properties: {
- contactId: { type: 'string', description: 'Contact ID to display timeline for' }
+ contactId: { type: 'string', description: 'Contact ID to display timeline for' },
},
- required: ['contactId']
+ required: ['contactId'],
},
- _meta: {
- ui: { resourceUri: 'ui://ghl/contact-timeline' }
- }
+ _meta: { ui: { resourceUri: appUri } },
},
- // 10. Workflow Status - workflow execution status
{
name: 'view_workflow_status',
description: 'Display workflow execution status and history. Returns a visual UI component.',
inputSchema: {
type: 'object',
properties: {
- workflowId: { type: 'string', description: 'Workflow ID to display status for' }
+ workflowId: { type: 'string', description: 'Workflow ID to display status for' },
},
- required: ['workflowId']
+ required: ['workflowId'],
},
- _meta: {
- ui: { resourceUri: 'ui://ghl/workflow-status' }
- }
+ _meta: { ui: { resourceUri: appUri } },
},
- // 11. MCP App - generic/main dashboard
{
name: 'view_dashboard',
description: 'Display the main GHL dashboard overview. Returns a visual UI component.',
inputSchema: {
type: 'object',
- properties: {}
+ properties: {},
},
- _meta: {
- ui: { resourceUri: 'ui://ghl/mcp-app' }
- }
+ _meta: { ui: { resourceUri: appUri } },
+ },
+ {
+ name: 'generate_ghl_view',
+ description: 'Generate a rich, AI-powered UI view on the fly from a natural language prompt. Optionally fetches real GHL data to populate the view. Returns a visual UI component rendered in the MCP App.',
+ inputSchema: {
+ type: 'object',
+ properties: {
+ prompt: {
+ type: 'string',
+ description: 'Natural language description of the UI view to generate.',
+ },
+ dataSource: {
+ type: 'string',
+ enum: ['contacts', 'opportunities', 'pipelines', 'calendars', 'invoices'],
+ description: 'Optional: fetch real GHL data to include in the generated view.',
+ },
+ },
+ required: ['prompt'],
+ },
+ _meta: { ui: { resourceUri: appUri } },
},
- // 12. Update Opportunity - action tool for UI to update opportunities
{
name: 'update_opportunity',
description: 'Update an opportunity (move to stage, change value, status, etc.)',
@@ -311,72 +584,60 @@ export class MCPAppsManager {
pipelineStageId: { type: 'string', description: 'New stage ID (for moving)' },
name: { type: 'string', description: 'Opportunity name' },
monetaryValue: { type: 'number', description: 'Monetary value' },
- status: { type: 'string', enum: ['open', 'won', 'lost', 'abandoned'], description: 'Opportunity status' }
+ status: { type: 'string', enum: ['open', 'won', 'lost', 'abandoned'], description: 'Opportunity status' },
},
- required: ['opportunityId']
- }
- }
+ required: ['opportunityId'],
+ },
+ },
];
}
- /**
- * Get app tool names for routing
- */
+ // ─── Tool Routing ───────────────────────────────────────
+
getAppToolNames(): string[] {
return [
- 'view_contact_grid',
- 'view_pipeline_board',
- 'view_quick_book',
- 'view_opportunity_card',
- 'view_calendar',
- 'view_invoice',
- 'view_campaign_stats',
- 'view_agent_stats',
- 'view_contact_timeline',
- 'view_workflow_status',
- 'view_dashboard',
- 'update_opportunity'
+ 'view_contact_grid', 'view_pipeline_board', 'view_quick_book',
+ 'view_opportunity_card', 'view_calendar', 'view_invoice',
+ 'view_campaign_stats', 'view_agent_stats', 'view_contact_timeline',
+ 'view_workflow_status', 'view_dashboard', 'generate_ghl_view',
+ 'update_opportunity',
];
}
- /**
- * Check if a tool is an app tool
- */
isAppTool(toolName: string): boolean {
return this.getAppToolNames().includes(toolName);
}
- /**
- * Execute an app tool
- */
async executeTool(toolName: string, args: Record): Promise {
- process.stderr.write(`[MCP Apps] Executing app tool: ${toolName}\n`);
+ process.stderr.write(`[MCP Apps] Executing: ${toolName}\n`);
switch (toolName) {
case 'view_contact_grid':
- return await this.viewContactGrid(args.query, args.limit);
+ return this.viewContactGrid(args.query, args.limit);
case 'view_pipeline_board':
- return await this.viewPipelineBoard(args.pipelineId);
+ return this.viewPipelineBoard(args.pipelineId);
case 'view_quick_book':
- return await this.viewQuickBook(args.calendarId, args.contactId);
+ return this.viewQuickBook(args.calendarId, args.contactId);
case 'view_opportunity_card':
- return await this.viewOpportunityCard(args.opportunityId);
+ return this.viewOpportunityCard(args.opportunityId);
case 'view_calendar':
- return await this.viewCalendar(args.calendarId, args.startDate, args.endDate);
+ return this.viewCalendar(args.calendarId, args.startDate, args.endDate);
case 'view_invoice':
- return await this.viewInvoice(args.invoiceId);
+ return this.viewInvoice(args.invoiceId);
case 'view_campaign_stats':
- return await this.viewCampaignStats(args.campaignId);
+ return this.viewCampaignStats(args.campaignId);
case 'view_agent_stats':
- return await this.viewAgentStats(args.userId, args.dateRange);
+ return this.viewAgentStats(args.userId, args.dateRange);
case 'view_contact_timeline':
- return await this.viewContactTimeline(args.contactId);
+ return this.viewContactTimeline(args.contactId);
case 'view_workflow_status':
- return await this.viewWorkflowStatus(args.workflowId);
+ return this.viewWorkflowStatus(args.workflowId);
case 'view_dashboard':
- return await this.viewDashboard();
+ return this.viewDashboard();
+ case 'generate_ghl_view':
+ return this.generateDynamicView(args.prompt, args.dataSource);
case 'update_opportunity':
- return await this.updateOpportunity(args as {
+ return this.updateOpportunity(args as {
opportunityId: string;
pipelineStageId?: string;
name?: string;
@@ -388,140 +649,73 @@ export class MCPAppsManager {
}
}
- /**
- * View contact grid (search results)
- */
+ // ─── View Handlers (fetch data → template → universal renderer) ──
+
private async viewContactGrid(query?: string, limit?: number): Promise {
const response = await this.ghlClient.searchContacts({
locationId: this.ghlClient.getConfig().locationId,
- query: query,
- limit: limit || 25
+ query, limit: limit || 25,
+ });
+ if (!response.success) throw new Error(response.error?.message || 'Failed to search contacts');
+
+ const uiTree = buildContactGridTree({
+ contacts: response.data?.contacts || [],
+ query,
});
- if (!response.success) {
- throw new Error(response.error?.message || 'Failed to search contacts');
- }
-
- const data = response.data;
- const resourceHandler = this.resourceHandlers.get('ui://ghl/contact-grid')!;
-
- return this.createAppResult(
- `Found ${data?.contacts?.length || 0} contacts`,
- resourceHandler.uri,
- resourceHandler.mimeType,
- resourceHandler.getContent(),
- data
- );
+ return this.renderUITree(uiTree, `Found ${response.data?.contacts?.length || 0} contacts`);
}
- /**
- * View pipeline board (Kanban)
- */
private async viewPipelineBoard(pipelineId: string): Promise {
const [pipelinesResponse, opportunitiesResponse] = await Promise.all([
this.ghlClient.getPipelines(),
this.ghlClient.searchOpportunities({
location_id: this.ghlClient.getConfig().locationId,
- pipeline_id: pipelineId
- })
+ pipeline_id: pipelineId,
+ }),
]);
-
- if (!pipelinesResponse.success) {
- throw new Error(pipelinesResponse.error?.message || 'Failed to get pipeline');
- }
+ if (!pipelinesResponse.success) throw new Error(pipelinesResponse.error?.message || 'Failed to get pipeline');
const pipeline = pipelinesResponse.data?.pipelines?.find((p: any) => p.id === pipelineId);
- const opportunities = opportunitiesResponse.data?.opportunities || [];
-
- // Simplify opportunity data to only include fields the UI needs (reduces payload size)
- const simplifiedOpportunities = opportunities.map((opp: any) => ({
- id: opp.id,
- name: opp.name || 'Untitled',
- pipelineStageId: opp.pipelineStageId,
- status: opp.status || 'open',
+ const opportunities = (opportunitiesResponse.data?.opportunities || []).map((opp: any) => ({
+ id: opp.id, name: opp.name || 'Untitled',
+ pipelineStageId: opp.pipelineStageId, status: opp.status || 'open',
monetaryValue: opp.monetaryValue || 0,
- contact: opp.contact ? {
- name: opp.contact.name || 'Unknown',
- email: opp.contact.email,
- phone: opp.contact.phone
- } : { name: 'Unknown' },
- updatedAt: opp.updatedAt || opp.createdAt,
- createdAt: opp.createdAt,
- source: opp.source
+ contact: opp.contact ? { name: opp.contact.name || 'Unknown', email: opp.contact.email, phone: opp.contact.phone } : { name: 'Unknown' },
+ updatedAt: opp.updatedAt || opp.createdAt, createdAt: opp.createdAt, source: opp.source,
}));
- const data = {
- pipeline,
- opportunities: simplifiedOpportunities,
- stages: pipeline?.stages || []
- };
+ const uiTree = buildPipelineBoardTree({
+ pipeline, opportunities, stages: pipeline?.stages || [],
+ });
- const resourceHandler = this.resourceHandlers.get('ui://ghl/pipeline-board')!;
-
- return this.createAppResult(
- `Pipeline: ${pipeline?.name || 'Unknown'} (${opportunities.length} opportunities)`,
- resourceHandler.uri,
- resourceHandler.mimeType,
- resourceHandler.getContent(),
- data
- );
+ return this.renderUITree(uiTree, `Pipeline: ${pipeline?.name || 'Unknown'} (${opportunities.length} opportunities)`);
}
- /**
- * View quick book interface
- */
private async viewQuickBook(calendarId: string, contactId?: string): Promise {
const [calendarResponse, contactResponse] = await Promise.all([
this.ghlClient.getCalendar(calendarId),
- contactId ? this.ghlClient.getContact(contactId) : Promise.resolve({ success: true, data: null })
+ contactId ? this.ghlClient.getContact(contactId) : Promise.resolve({ success: true, data: null }),
]);
+ if (!calendarResponse.success) throw new Error(calendarResponse.error?.message || 'Failed to get calendar');
- if (!calendarResponse.success) {
- throw new Error(calendarResponse.error?.message || 'Failed to get calendar');
- }
-
- const data = {
+ const uiTree = buildQuickBookTree({
calendar: calendarResponse.data,
contact: contactResponse.data,
- locationId: this.ghlClient.getConfig().locationId
- };
+ locationId: this.ghlClient.getConfig().locationId,
+ });
- const resourceHandler = this.resourceHandlers.get('ui://ghl/quick-book')!;
-
- return this.createAppResult(
- `Quick booking for calendar: ${(calendarResponse.data as any)?.name || calendarId}`,
- resourceHandler.uri,
- resourceHandler.mimeType,
- resourceHandler.getContent(),
- data
- );
+ return this.renderUITree(uiTree, `Quick booking for calendar: ${(calendarResponse.data as any)?.name || calendarId}`);
}
- /**
- * View opportunity card
- */
private async viewOpportunityCard(opportunityId: string): Promise {
const response = await this.ghlClient.getOpportunity(opportunityId);
+ if (!response.success) throw new Error(response.error?.message || 'Failed to get opportunity');
- if (!response.success) {
- throw new Error(response.error?.message || 'Failed to get opportunity');
- }
-
- const opportunity = response.data;
- const resourceHandler = this.resourceHandlers.get('ui://ghl/opportunity-card')!;
-
- return this.createAppResult(
- `Opportunity: ${(opportunity as any)?.name || opportunityId}`,
- resourceHandler.uri,
- resourceHandler.mimeType,
- resourceHandler.getContent(),
- opportunity
- );
+ const uiTree = buildOpportunityCardTree(response.data);
+ return this.renderUITree(uiTree, `Opportunity: ${(response.data as any)?.name || opportunityId}`);
}
- /**
- * View calendar
- */
private async viewCalendar(calendarId: string, startDate?: string, endDate?: string): Promise {
const now = new Date();
const start = startDate || new Date(now.getFullYear(), now.getMonth(), 1).toISOString();
@@ -530,206 +724,254 @@ export class MCPAppsManager {
const [calendarResponse, eventsResponse] = await Promise.all([
this.ghlClient.getCalendar(calendarId),
this.ghlClient.getCalendarEvents({
- calendarId: calendarId,
- startTime: start,
- endTime: end,
- locationId: this.ghlClient.getConfig().locationId
- })
+ calendarId, startTime: start, endTime: end,
+ locationId: this.ghlClient.getConfig().locationId,
+ }),
]);
-
- if (!calendarResponse.success) {
- throw new Error(calendarResponse.error?.message || 'Failed to get calendar');
- }
+ if (!calendarResponse.success) throw new Error(calendarResponse.error?.message || 'Failed to get calendar');
const calendar = calendarResponse.data as any;
- const data = {
- calendar: calendarResponse.data,
- events: eventsResponse.data?.events || [],
- startDate: start,
- endDate: end
- };
+ const events = eventsResponse.data?.events || [];
- const resourceHandler = this.resourceHandlers.get('ui://ghl/calendar-view')!;
-
- return this.createAppResult(
- `Calendar: ${calendar?.name || 'Unknown'} (${data.events.length} events)`,
- resourceHandler.uri,
- resourceHandler.mimeType,
- resourceHandler.getContent(),
- data
- );
+ const uiTree = buildCalendarViewTree({ calendar, events, startDate: start, endDate: end });
+ return this.renderUITree(uiTree, `Calendar: ${calendar?.name || 'Unknown'} (${events.length} events)`);
}
- /**
- * View campaign stats
- */
- private async viewCampaignStats(campaignId: string): Promise {
- // Get email campaigns
- const response = await this.ghlClient.getEmailCampaigns({});
+ private async viewInvoice(invoiceId: string): Promise {
+ const response = await this.ghlClient.getInvoice(invoiceId, {
+ altId: this.ghlClient.getConfig().locationId,
+ altType: 'location',
+ });
+ if (!response.success) throw new Error(response.error?.message || 'Failed to get invoice');
+ const invoice = response.data;
+ const uiTree = buildInvoicePreviewTree(invoice);
+ return this.renderUITree(uiTree, `Invoice #${invoice?.invoiceNumber || invoiceId} - ${invoice?.status || 'Unknown status'}`);
+ }
+
+ private async viewCampaignStats(campaignId: string): Promise {
+ const response = await this.ghlClient.getEmailCampaigns({});
const campaigns = response.data?.schedules || [];
const campaign = campaigns.find((c: any) => c.id === campaignId) || { id: campaignId };
- const data = {
- campaign,
- campaigns,
- campaignId,
- locationId: this.ghlClient.getConfig().locationId
- };
+ const uiTree = buildCampaignStatsTree({
+ campaign, campaigns, campaignId,
+ locationId: this.ghlClient.getConfig().locationId,
+ });
- const resourceHandler = this.resourceHandlers.get('ui://ghl/campaign-stats')!;
-
- return this.createAppResult(
- `Campaign stats: ${(campaign as any)?.name || campaignId}`,
- resourceHandler.uri,
- resourceHandler.mimeType,
- resourceHandler.getContent(),
- data
- );
+ return this.renderUITree(uiTree, `Campaign stats: ${(campaign as any)?.name || campaignId}`);
}
- /**
- * View agent stats
- */
private async viewAgentStats(userId?: string, dateRange?: string): Promise {
- // Get location info which may include user data
const locationResponse = await this.ghlClient.getLocationById(this.ghlClient.getConfig().locationId);
- const data = {
- userId,
- dateRange: dateRange || 'last30days',
+ const uiTree = buildAgentStatsTree({
+ userId, dateRange: dateRange || 'last30days',
location: locationResponse.data,
- locationId: this.ghlClient.getConfig().locationId
- };
+ locationId: this.ghlClient.getConfig().locationId,
+ });
- const resourceHandler = this.resourceHandlers.get('ui://ghl/agent-stats')!;
-
- return this.createAppResult(
- userId ? `Agent stats: ${userId}` : 'Agent overview',
- resourceHandler.uri,
- resourceHandler.mimeType,
- resourceHandler.getContent(),
- data
- );
+ return this.renderUITree(uiTree, userId ? `Agent stats: ${userId}` : 'Agent overview');
}
- /**
- * View contact timeline
- */
private async viewContactTimeline(contactId: string): Promise {
const [contactResponse, notesResponse, tasksResponse] = await Promise.all([
this.ghlClient.getContact(contactId),
this.ghlClient.getContactNotes(contactId),
- this.ghlClient.getContactTasks(contactId)
+ this.ghlClient.getContactTasks(contactId),
]);
-
- if (!contactResponse.success) {
- throw new Error(contactResponse.error?.message || 'Failed to get contact');
- }
+ if (!contactResponse.success) throw new Error(contactResponse.error?.message || 'Failed to get contact');
const contact = contactResponse.data as any;
- const data = {
+ const uiTree = buildContactTimelineTree({
contact: contactResponse.data,
notes: notesResponse.data || [],
- tasks: tasksResponse.data || []
- };
-
- const resourceHandler = this.resourceHandlers.get('ui://ghl/contact-timeline')!;
-
- return this.createAppResult(
- `Timeline for ${contact?.firstName || ''} ${contact?.lastName || ''}`,
- resourceHandler.uri,
- resourceHandler.mimeType,
- resourceHandler.getContent(),
- data
- );
- }
-
- /**
- * View workflow status
- */
- private async viewWorkflowStatus(workflowId: string): Promise {
- const response = await this.ghlClient.getWorkflows({
- locationId: this.ghlClient.getConfig().locationId
+ tasks: tasksResponse.data || [],
});
+ return this.renderUITree(uiTree, `Timeline for ${contact?.firstName || ''} ${contact?.lastName || ''}`);
+ }
+
+ private async viewWorkflowStatus(workflowId: string): Promise {
+ const response = await this.ghlClient.getWorkflows({
+ locationId: this.ghlClient.getConfig().locationId,
+ });
const workflows = response.data?.workflows || [];
const workflow = workflows.find((w: any) => w.id === workflowId) || { id: workflowId };
- const data = {
- workflow,
- workflows,
- workflowId,
- locationId: this.ghlClient.getConfig().locationId
- };
+ const uiTree = buildWorkflowStatusTree({
+ workflow, workflows, workflowId,
+ locationId: this.ghlClient.getConfig().locationId,
+ });
- const resourceHandler = this.resourceHandlers.get('ui://ghl/workflow-status')!;
-
- return this.createAppResult(
- `Workflow: ${(workflow as any)?.name || workflowId}`,
- resourceHandler.uri,
- resourceHandler.mimeType,
- resourceHandler.getContent(),
- data
- );
+ return this.renderUITree(uiTree, `Workflow: ${(workflow as any)?.name || workflowId}`);
}
- /**
- * View main dashboard
- */
private async viewDashboard(): Promise {
const [contactsResponse, pipelinesResponse, calendarsResponse] = await Promise.all([
this.ghlClient.searchContacts({ locationId: this.ghlClient.getConfig().locationId, limit: 10 }),
this.ghlClient.getPipelines(),
- this.ghlClient.getCalendars()
+ this.ghlClient.getCalendars(),
]);
- const data = {
+ const uiTree = buildDashboardTree({
recentContacts: contactsResponse.data?.contacts || [],
pipelines: pipelinesResponse.data?.pipelines || [],
calendars: calendarsResponse.data?.calendars || [],
- locationId: this.ghlClient.getConfig().locationId
- };
-
- const resourceHandler = this.resourceHandlers.get('ui://ghl/mcp-app')!;
-
- return this.createAppResult(
- 'GHL Dashboard Overview',
- resourceHandler.uri,
- resourceHandler.mimeType,
- resourceHandler.getContent(),
- data
- );
- }
-
- /**
- * View invoice
- */
- private async viewInvoice(invoiceId: string): Promise {
- const response = await this.ghlClient.getInvoice(invoiceId, {
- altId: this.ghlClient.getConfig().locationId,
- altType: 'location'
+ locationId: this.ghlClient.getConfig().locationId,
});
- if (!response.success) {
- throw new Error(response.error?.message || 'Failed to get invoice');
- }
-
- const invoice = response.data;
- const resourceHandler = this.resourceHandlers.get('ui://ghl/invoice-preview')!;
-
- return this.createAppResult(
- `Invoice #${invoice?.invoiceNumber || invoiceId} - ${invoice?.status || 'Unknown status'}`,
- resourceHandler.uri,
- resourceHandler.mimeType,
- resourceHandler.getContent(),
- invoice
- );
+ return this.renderUITree(uiTree, 'GHL Dashboard Overview');
}
- /**
- * Update opportunity (action tool for UI)
- */
+ // ─── Dynamic View (LLM-powered) ────────────────────────
+
+ private detectDataSources(prompt: string): string[] {
+ const lower = prompt.toLowerCase();
+ const sources: string[] = [];
+ if (lower.match(/pipeline|kanban|deal|opportunit|stage|funnel|sales/)) sources.push('pipelines');
+ if (lower.match(/contact|lead|customer|people|person|client/)) sources.push('contacts');
+ if (lower.match(/calendar|appointment|event|schedule|booking/)) sources.push('calendars');
+ if (lower.match(/invoice|billing|payment|charge/)) sources.push('invoices');
+ if (lower.match(/campaign|email.*market|newsletter|broadcast/)) sources.push('campaigns');
+ if (sources.length === 0) sources.push('contacts', 'pipelines');
+ return sources;
+ }
+
+ private async generateDynamicView(prompt: string, dataSource?: string): Promise {
+ process.stderr.write(`[MCP Apps] Generating dynamic view: "${prompt}" (dataSource: ${dataSource || 'auto'})\n`);
+
+ // Step 1: Fetch real GHL data
+ let ghlData: any = {};
+ const sources = dataSource ? [dataSource] : this.detectDataSources(prompt);
+
+ for (const src of sources) {
+ try {
+ const data = await this.fetchGHLData(src);
+ if (data) Object.assign(ghlData, data);
+ } catch (err: any) {
+ process.stderr.write(`[MCP Apps] Warning: Failed to fetch GHL data for ${src}: ${err.message}\n`);
+ }
+ }
+ if (Object.keys(ghlData).length === 0) ghlData = null;
+
+ // Step 2: Call Claude API
+ const apiKey = process.env.ANTHROPIC_API_KEY;
+ if (!apiKey) throw new Error('ANTHROPIC_API_KEY environment variable is required for generate_ghl_view');
+
+ const anthropic = new Anthropic({ apiKey });
+
+ let userMessage: string;
+ if (ghlData) {
+ const dataKeys = Object.keys(ghlData);
+ const summary: string[] = [];
+ if (ghlData.pipelines) summary.push(`${ghlData.pipelines.length} pipeline(s)`);
+ if (ghlData.opportunities) summary.push(`${ghlData.opportunities.length} opportunity/deal(s)`);
+ if (ghlData.contacts) summary.push(`${ghlData.contacts.length} contact(s)`);
+ if (ghlData.calendars) summary.push(`${ghlData.calendars.length} calendar(s)`);
+ if (ghlData.invoices) summary.push(`${ghlData.invoices.length} invoice(s)`);
+ if (ghlData.campaigns) summary.push(`${ghlData.campaigns.length} campaign(s)`);
+
+ userMessage = `${prompt}
+
+⛔ STRICT DATA RULES:
+- You have REAL CRM data below: ${summary.join(', ')}
+- Use ONLY this data. Do NOT invent ANY additional records.
+- If pipelines are provided, use ONLY the stage names from pipelines[].stages[].name.
+- Show exactly the records provided.
+- Do NOT add sections for data types not provided (${['tasks', 'workflows', 'notes', 'emails'].filter(k => !dataKeys.includes(k)).join(', ')} were NOT fetched).
+
+REAL GHL DATA:
+\`\`\`json
+${JSON.stringify(ghlData, null, 2)}
+\`\`\``;
+ } else {
+ userMessage = `${prompt}\n\n(No real data available — use minimal sample data, 3-5 records max.)`;
+ }
+
+ let message;
+ try {
+ message = await anthropic.messages.create({
+ model: 'claude-sonnet-4-20250514',
+ max_tokens: 8192,
+ system: CATALOG_SYSTEM_PROMPT,
+ messages: [{ role: 'user', content: userMessage }],
+ });
+ } catch (aiErr: any) {
+ throw new Error(`AI generation failed: ${aiErr.message}`);
+ }
+
+ const text = message.content
+ .filter((b): b is Anthropic.TextBlock => b.type === 'text')
+ .map(b => b.text)
+ .join('');
+
+ const cleaned = text.replace(/^```(?:json)?\s*\n?/m, '').replace(/\n?```\s*$/m, '').trim();
+
+ let uiTree: UITree;
+ try {
+ uiTree = JSON.parse(cleaned);
+ } catch (parseErr: any) {
+ throw new Error(`Failed to parse AI response as JSON: ${parseErr.message}`);
+ }
+
+ // Validate the tree
+ const errors = validateUITree(uiTree);
+ if (errors.length > 0) {
+ process.stderr.write(`[MCP Apps] UITree validation warnings: ${JSON.stringify(errors)}\n`);
+ // Don't throw — render what we got, the renderer handles unknown types gracefully
+ }
+
+ process.stderr.write(`[MCP Apps] Generated UI tree with ${Object.keys(uiTree.elements).length} elements\n`);
+
+ return this.renderUITree(uiTree, `Generated dynamic view: ${prompt}`);
+ }
+
+ // ─── Data Fetching ──────────────────────────────────────
+
+ private async fetchGHLData(dataSource: string): Promise {
+ const locationId = this.ghlClient.getConfig().locationId;
+
+ switch (dataSource) {
+ case 'contacts': {
+ const resp = await this.ghlClient.searchContacts({ locationId, limit: 20 });
+ return { contacts: resp.data?.contacts || [] };
+ }
+ case 'opportunities': {
+ const resp = await this.ghlClient.searchOpportunities({ location_id: locationId });
+ return { opportunities: resp.data?.opportunities || [] };
+ }
+ case 'pipelines': {
+ const [pResp, oResp] = await Promise.all([
+ this.ghlClient.getPipelines(),
+ this.ghlClient.searchOpportunities({ location_id: locationId }),
+ ]);
+ return {
+ pipelines: pResp.data?.pipelines || [],
+ opportunities: oResp.data?.opportunities || [],
+ };
+ }
+ case 'calendars': {
+ const resp = await this.ghlClient.getCalendars();
+ return { calendars: resp.data?.calendars || [] };
+ }
+ case 'invoices': {
+ const resp = await this.ghlClient.listInvoices?.({
+ altId: locationId, altType: 'location', limit: '10', offset: '0',
+ }) || { data: { invoices: [] } };
+ return { invoices: resp.data?.invoices || [] };
+ }
+ case 'campaigns': {
+ const resp = await this.ghlClient.getEmailCampaigns({});
+ return { campaigns: resp.data?.schedules || [] };
+ }
+ default:
+ return null;
+ }
+ }
+
+ // ─── Action Tools ───────────────────────────────────────
+
private async updateOpportunity(args: {
opportunityId: string;
pipelineStageId?: string;
@@ -738,8 +980,6 @@ export class MCPAppsManager {
status?: 'open' | 'won' | 'lost' | 'abandoned';
}): Promise {
const { opportunityId, ...updates } = args;
-
- // Build the update payload
const updatePayload: any = {};
if (updates.pipelineStageId) updatePayload.pipelineStageId = updates.pipelineStageId;
if (updates.name) updatePayload.name = updates.name;
@@ -747,74 +987,58 @@ export class MCPAppsManager {
if (updates.status) updatePayload.status = updates.status;
process.stderr.write(`[MCP Apps] Updating opportunity ${opportunityId}: ${JSON.stringify(updatePayload)}\n`);
-
const response = await this.ghlClient.updateOpportunity(opportunityId, updatePayload);
-
- if (!response.success) {
- throw new Error(response.error?.message || 'Failed to update opportunity');
- }
+ if (!response.success) throw new Error(response.error?.message || 'Failed to update opportunity');
const opportunity = response.data;
-
return {
content: [{ type: 'text', text: `Updated opportunity: ${opportunity?.name || opportunityId}` }],
structuredContent: {
success: true,
opportunity: {
- id: opportunity?.id,
- name: opportunity?.name,
+ id: opportunity?.id, name: opportunity?.name,
pipelineStageId: opportunity?.pipelineStageId,
- monetaryValue: opportunity?.monetaryValue,
- status: opportunity?.status
- }
- }
+ monetaryValue: opportunity?.monetaryValue, status: opportunity?.status,
+ },
+ },
};
}
+ // ─── Universal Render Pipeline ──────────────────────────
+
/**
- * Create app tool result with structuredContent
+ * Core render method: takes a UITree, injects it into the universal
+ * renderer, and returns a structuredContent result.
*/
- private createAppResult(
- textSummary: string,
- resourceUri: string,
- mimeType: string,
- htmlContent: string,
- data: any
- ): AppToolResult {
- // structuredContent is the data object that gets passed to ontoolresult
- // The UI accesses it via result.structuredContent
+ private renderUITree(uiTree: UITree, textSummary: string): AppToolResult {
+ // Store UITree for injection when resource is read
+ this.pendingDynamicData = { uiTree };
+
return {
content: [{ type: 'text', text: textSummary }],
- structuredContent: data
+ structuredContent: { uiTree } as Record,
};
}
/**
- * Inject data into HTML as a script tag
+ * Inject data into HTML as a script tag (for pre-injected __MCP_APP_DATA__)
*/
private injectDataIntoHTML(html: string, data: any): string {
const dataScript = ``;
-
- // Insert before or at the beginning of
if (html.includes('')) {
return html.replace('', `${dataScript}`);
} else if (html.includes('')) {
return html.replace('', `${dataScript}`);
- } else {
- return dataScript + html;
}
+ return dataScript + html;
}
- /**
- * Get resource handler by URI
- */
+ // ─── Resource Access ────────────────────────────────────
+
getResourceHandler(uri: string): AppResourceHandler | undefined {
return this.resourceHandlers.get(uri);
}
- /**
- * Get all registered resource URIs
- */
getResourceURIs(): string[] {
return Array.from(this.resourceHandlers.keys());
}
diff --git a/src/apps/templates/agent-stats.template.ts b/src/apps/templates/agent-stats.template.ts
new file mode 100644
index 0000000..00515a5
--- /dev/null
+++ b/src/apps/templates/agent-stats.template.ts
@@ -0,0 +1,106 @@
+import { UITree } from '../types.js';
+
+export function buildAgentStatsTree(data: {
+ userId?: string;
+ dateRange: string;
+ location: any;
+ locationId: string;
+}): UITree {
+ const location = data.location || {};
+ const locName = location.name || 'Location';
+
+ // Since GHL API doesn't have direct agent stats, build from available location data
+ const dateRangeLabel =
+ data.dateRange === 'last7days' ? 'Last 7 Days'
+ : data.dateRange === 'last30days' ? 'Last 30 Days'
+ : data.dateRange === 'last90days' ? 'Last 90 Days'
+ : data.dateRange || 'Last 30 Days';
+
+ // Build a placeholder stats view using available data
+ const elements: UITree['elements'] = {
+ page: {
+ key: 'page',
+ type: 'PageHeader',
+ props: {
+ title: data.userId ? `Agent: ${data.userId}` : 'Agent Overview',
+ subtitle: `${locName} · ${dateRangeLabel}`,
+ gradient: true,
+ stats: [
+ { label: 'Location', value: locName },
+ { label: 'Period', value: dateRangeLabel },
+ ],
+ },
+ children: ['metrics', 'layout'],
+ },
+ metrics: {
+ key: 'metrics',
+ type: 'StatsGrid',
+ props: { columns: 4 },
+ children: ['mTotal', 'mActive', 'mRes', 'mAvg'],
+ },
+ mTotal: {
+ key: 'mTotal',
+ type: 'MetricCard',
+ props: { label: 'Total Interactions', value: '—', color: 'blue' },
+ },
+ mActive: {
+ key: 'mActive',
+ type: 'MetricCard',
+ props: { label: 'Active Contacts', value: '—', color: 'green' },
+ },
+ mRes: {
+ key: 'mRes',
+ type: 'MetricCard',
+ props: { label: 'Response Rate', value: '—', color: 'purple' },
+ },
+ mAvg: {
+ key: 'mAvg',
+ type: 'MetricCard',
+ props: { label: 'Avg Response Time', value: '—', color: 'yellow' },
+ },
+ layout: {
+ key: 'layout',
+ type: 'SplitLayout',
+ props: { ratio: '50/50', gap: 'md' },
+ children: ['chart', 'activityTable'],
+ },
+ chart: {
+ key: 'chart',
+ type: 'LineChart',
+ props: {
+ points: [
+ { label: 'Mon', value: 0 },
+ { label: 'Tue', value: 0 },
+ { label: 'Wed', value: 0 },
+ { label: 'Thu', value: 0 },
+ { label: 'Fri', value: 0 },
+ ],
+ title: 'Activity Trend',
+ showPoints: true,
+ showArea: true,
+ yAxisLabel: 'Interactions',
+ },
+ },
+ activityTable: {
+ key: 'activityTable',
+ type: 'DataTable',
+ props: {
+ columns: [
+ { key: 'type', label: 'Activity', format: 'text' },
+ { key: 'count', label: 'Count', format: 'text', sortable: true },
+ { key: 'trend', label: 'Trend', format: 'text' },
+ ],
+ rows: [
+ { type: 'Calls', count: '—', trend: '—' },
+ { type: 'Emails', count: '—', trend: '—' },
+ { type: 'SMS', count: '—', trend: '—' },
+ { type: 'Tasks', count: '—', trend: '—' },
+ ],
+ emptyMessage: 'No activity data available',
+ pageSize: 10,
+ },
+ },
+ };
+
+ return { root: 'page', elements };
+}
diff --git a/src/apps/templates/calendar-view.template.ts b/src/apps/templates/calendar-view.template.ts
new file mode 100644
index 0000000..654b501
--- /dev/null
+++ b/src/apps/templates/calendar-view.template.ts
@@ -0,0 +1,57 @@
+import { UITree } from '../types.js';
+
+export function buildCalendarViewTree(data: {
+ calendar: any;
+ events: any[];
+ startDate: string;
+ endDate: string;
+}): UITree {
+ const calendar = data.calendar || {};
+ const events = data.events || [];
+
+ // Map GHL events to CalendarView component events
+ const calEvents = events.map((evt: any) => ({
+ date: evt.startTime || evt.start || evt.date || '',
+ title: evt.title || evt.name || 'Event',
+ time: evt.startTime
+ ? new Date(evt.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
+ : undefined,
+ type: evt.appointmentStatus === 'confirmed' ? 'meeting' as const : 'event' as const,
+ color: evt.appointmentStatus === 'cancelled' ? '#dc2626' : undefined,
+ }));
+
+ const start = new Date(data.startDate);
+ const confirmedCount = events.filter((e: any) => e.appointmentStatus === 'confirmed').length;
+ const cancelledCount = events.filter((e: any) => e.appointmentStatus === 'cancelled').length;
+
+ return {
+ root: 'page',
+ elements: {
+ page: {
+ key: 'page',
+ type: 'PageHeader',
+ props: {
+ title: calendar.name || 'Calendar',
+ subtitle: `${events.length} events`,
+ gradient: true,
+ stats: [
+ { label: 'Total Events', value: String(events.length) },
+ { label: 'Confirmed', value: String(confirmedCount) },
+ { label: 'Cancelled', value: String(cancelledCount) },
+ ],
+ },
+ children: ['calendar'],
+ },
+ calendar: {
+ key: 'calendar',
+ type: 'CalendarView',
+ props: {
+ year: start.getFullYear(),
+ month: start.getMonth() + 1,
+ events: calEvents,
+ highlightToday: true,
+ },
+ },
+ },
+ };
+}
diff --git a/src/apps/templates/campaign-stats.template.ts b/src/apps/templates/campaign-stats.template.ts
new file mode 100644
index 0000000..099286e
--- /dev/null
+++ b/src/apps/templates/campaign-stats.template.ts
@@ -0,0 +1,118 @@
+import { UITree } from '../types.js';
+
+export function buildCampaignStatsTree(data: {
+ campaign: any;
+ campaigns: any[];
+ campaignId: string;
+ locationId: string;
+}): UITree {
+ const campaign = data.campaign || {};
+ const campaigns = data.campaigns || [];
+
+ const stats = campaign.statistics || campaign.stats || {};
+ const sent = stats.sent || stats.delivered || 0;
+ const opened = stats.opened || stats.opens || 0;
+ const clicked = stats.clicked || stats.clicks || 0;
+ const bounced = stats.bounced || stats.bounces || 0;
+ const unsubscribed = stats.unsubscribed || stats.unsubscribes || 0;
+ const openRate = sent > 0 ? ((opened / sent) * 100).toFixed(1) : '0.0';
+ const clickRate = sent > 0 ? ((clicked / sent) * 100).toFixed(1) : '0.0';
+
+ // Bar chart of performance metrics
+ const bars = [
+ { label: 'Sent', value: sent, color: '#3b82f6' },
+ { label: 'Opened', value: opened, color: '#059669' },
+ { label: 'Clicked', value: clicked, color: '#7c3aed' },
+ { label: 'Bounced', value: bounced, color: '#f59e0b' },
+ { label: 'Unsubscribed', value: unsubscribed, color: '#dc2626' },
+ ].filter(b => b.value > 0);
+
+ // Other campaigns table
+ const campaignRows = campaigns.slice(0, 8).map((c: any) => ({
+ id: c.id,
+ name: c.name || 'Untitled',
+ status: c.status || 'draft',
+ sent: c.statistics?.sent || 0,
+ opens: c.statistics?.opened || 0,
+ }));
+
+ return {
+ root: 'page',
+ elements: {
+ page: {
+ key: 'page',
+ type: 'PageHeader',
+ props: {
+ title: campaign.name || 'Campaign',
+ subtitle: campaign.subject || 'Email Campaign',
+ status: campaign.status || 'draft',
+ statusVariant: campaign.status === 'completed' ? 'complete' : campaign.status === 'active' ? 'active' : 'draft',
+ gradient: true,
+ stats: [
+ { label: 'Sent', value: sent.toLocaleString() },
+ { label: 'Open Rate', value: `${openRate}%` },
+ { label: 'Click Rate', value: `${clickRate}%` },
+ ],
+ },
+ children: ['metrics', 'layout'],
+ },
+ metrics: {
+ key: 'metrics',
+ type: 'StatsGrid',
+ props: { columns: 4 },
+ children: ['mSent', 'mOpened', 'mClicked', 'mBounced'],
+ },
+ mSent: {
+ key: 'mSent',
+ type: 'MetricCard',
+ props: { label: 'Sent', value: sent.toLocaleString(), color: 'blue' },
+ },
+ mOpened: {
+ key: 'mOpened',
+ type: 'MetricCard',
+ props: { label: 'Opened', value: opened.toLocaleString(), color: 'green' },
+ },
+ mClicked: {
+ key: 'mClicked',
+ type: 'MetricCard',
+ props: { label: 'Clicked', value: clicked.toLocaleString(), color: 'purple' },
+ },
+ mBounced: {
+ key: 'mBounced',
+ type: 'MetricCard',
+ props: { label: 'Bounced', value: bounced.toLocaleString(), color: 'yellow' },
+ },
+ layout: {
+ key: 'layout',
+ type: 'SplitLayout',
+ props: { ratio: '50/50', gap: 'md' },
+ children: ['chart', 'campaignTable'],
+ },
+ chart: {
+ key: 'chart',
+ type: 'BarChart',
+ props: {
+ bars,
+ orientation: 'horizontal',
+ showValues: true,
+ title: 'Performance Breakdown',
+ },
+ },
+ campaignTable: {
+ key: 'campaignTable',
+ type: 'DataTable',
+ props: {
+ columns: [
+ { key: 'name', label: 'Campaign', format: 'text', sortable: true },
+ { key: 'status', label: 'Status', format: 'status' },
+ { key: 'sent', label: 'Sent', format: 'text', sortable: true },
+ { key: 'opens', label: 'Opens', format: 'text' },
+ ],
+ rows: campaignRows,
+ emptyMessage: 'No other campaigns',
+ pageSize: 8,
+ },
+ },
+ },
+ };
+}
diff --git a/src/apps/templates/contact-grid.template.ts b/src/apps/templates/contact-grid.template.ts
new file mode 100644
index 0000000..7f55913
--- /dev/null
+++ b/src/apps/templates/contact-grid.template.ts
@@ -0,0 +1,62 @@
+import { UITree } from '../types.js';
+
+export function buildContactGridTree(data: { contacts: any[]; query?: string }): UITree {
+ const contacts = data.contacts || [];
+
+ const taggedCount = contacts.filter((c: any) => c.tags && c.tags.length > 0).length;
+ const withEmail = contacts.filter((c: any) => c.email).length;
+
+ const rows = contacts.map((c: any) => ({
+ id: c.id || '',
+ name: `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unknown',
+ email: c.email || '—',
+ phone: c.phone || '—',
+ tags: c.tags || [],
+ dateAdded: c.dateAdded ? new Date(c.dateAdded).toLocaleDateString() : '—',
+ source: c.source || '—',
+ }));
+
+ return {
+ root: 'page',
+ elements: {
+ page: {
+ key: 'page',
+ type: 'PageHeader',
+ props: {
+ title: 'Contacts',
+ subtitle: data.query ? `Search: "${data.query}"` : 'All contacts',
+ gradient: true,
+ stats: [
+ { label: 'Total', value: String(contacts.length) },
+ { label: 'With Email', value: String(withEmail) },
+ { label: 'Tagged', value: String(taggedCount) },
+ ],
+ },
+ children: ['search', 'table'],
+ },
+ search: {
+ key: 'search',
+ type: 'SearchBar',
+ props: { placeholder: 'Search contacts...', valuePath: 'query' },
+ },
+ table: {
+ key: 'table',
+ type: 'DataTable',
+ props: {
+ columns: [
+ { key: 'name', label: 'Name', format: 'avatar', sortable: true },
+ { key: 'email', label: 'Email', format: 'email', sortable: true },
+ { key: 'phone', label: 'Phone', format: 'phone' },
+ { key: 'tags', label: 'Tags', format: 'tags' },
+ { key: 'dateAdded', label: 'Added', format: 'date', sortable: true },
+ { key: 'source', label: 'Source', format: 'text' },
+ ],
+ rows,
+ selectable: true,
+ emptyMessage: 'No contacts found',
+ pageSize: 15,
+ },
+ },
+ },
+ };
+}
diff --git a/src/apps/templates/contact-timeline.template.ts b/src/apps/templates/contact-timeline.template.ts
new file mode 100644
index 0000000..eca5f5e
--- /dev/null
+++ b/src/apps/templates/contact-timeline.template.ts
@@ -0,0 +1,127 @@
+import { UITree } from '../types.js';
+
+export function buildContactTimelineTree(data: {
+ contact: any;
+ notes: any;
+ tasks: any;
+}): UITree {
+ const contact = data.contact || {};
+ const notes = Array.isArray(data.notes) ? data.notes : data.notes?.notes || [];
+ const tasks = Array.isArray(data.tasks) ? data.tasks : data.tasks?.tasks || [];
+
+ const contactName = `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || 'Unknown Contact';
+ const email = contact.email || '—';
+ const phone = contact.phone || '—';
+
+ // Build timeline events from notes + tasks, sorted by date
+ const events: any[] = [];
+
+ for (const note of notes) {
+ events.push({
+ id: note.id || `note-${events.length}`,
+ title: 'Note Added',
+ description: note.body || note.content || note.text || 'Note',
+ timestamp: note.dateAdded || note.createdAt ? new Date(note.dateAdded || note.createdAt).toLocaleString() : '—',
+ icon: 'note',
+ variant: 'default',
+ _sort: new Date(note.dateAdded || note.createdAt || 0).getTime(),
+ });
+ }
+
+ for (const task of tasks) {
+ events.push({
+ id: task.id || `task-${events.length}`,
+ title: task.title || task.name || 'Task',
+ description: task.description || task.body || (task.completed ? 'Completed' : 'Pending'),
+ timestamp: task.dueDate || task.createdAt ? new Date(task.dueDate || task.createdAt).toLocaleString() : '—',
+ icon: 'task',
+ variant: task.completed ? 'success' : 'default',
+ _sort: new Date(task.dueDate || task.createdAt || 0).getTime(),
+ });
+ }
+
+ // Add contact creation event
+ if (contact.dateAdded || contact.createdAt) {
+ events.push({
+ id: 'contact-created',
+ title: 'Contact Created',
+ description: `${contactName} was added to the CRM`,
+ timestamp: new Date(contact.dateAdded || contact.createdAt).toLocaleString(),
+ icon: 'system',
+ variant: 'default',
+ _sort: new Date(contact.dateAdded || contact.createdAt).getTime(),
+ });
+ }
+
+ // Sort by date descending (newest first)
+ events.sort((a, b) => (b._sort || 0) - (a._sort || 0));
+
+ // Clean _sort from events
+ const cleanEvents = events.map(({ _sort, ...rest }) => rest);
+
+ return {
+ root: 'page',
+ elements: {
+ page: {
+ key: 'page',
+ type: 'DetailHeader',
+ props: {
+ title: contactName,
+ subtitle: email !== '—' ? email : phone,
+ entityId: contact.id,
+ status: contact.type || 'lead',
+ statusVariant: 'active',
+ },
+ children: ['tabs', 'layout'],
+ },
+ tabs: {
+ key: 'tabs',
+ type: 'TabGroup',
+ props: {
+ tabs: [
+ { label: 'Timeline', value: 'timeline', count: cleanEvents.length },
+ { label: 'Notes', value: 'notes', count: notes.length },
+ { label: 'Tasks', value: 'tasks', count: tasks.length },
+ ],
+ activeTab: 'timeline',
+ },
+ },
+ layout: {
+ key: 'layout',
+ type: 'SplitLayout',
+ props: { ratio: '33/67', gap: 'md' },
+ children: ['contactInfo', 'timeline'],
+ },
+ contactInfo: {
+ key: 'contactInfo',
+ type: 'KeyValueList',
+ props: {
+ items: [
+ { label: 'Name', value: contactName },
+ { label: 'Email', value: email },
+ { label: 'Phone', value: phone },
+ { label: 'Tags', value: (contact.tags || []).join(', ') || '—' },
+ { label: 'Source', value: contact.source || '—' },
+ { label: 'Added', value: contact.dateAdded ? new Date(contact.dateAdded).toLocaleDateString() : '—' },
+ ],
+ compact: true,
+ },
+ },
+ timeline: {
+ key: 'timeline',
+ type: 'Timeline',
+ props: {
+ events: cleanEvents.length > 0
+ ? cleanEvents
+ : [{
+ id: 'placeholder',
+ title: 'No activity yet',
+ description: 'Notes, tasks, and activity will appear here',
+ timestamp: new Date().toLocaleString(),
+ icon: 'system',
+ }],
+ },
+ },
+ },
+ };
+}
diff --git a/src/apps/templates/dashboard.template.ts b/src/apps/templates/dashboard.template.ts
new file mode 100644
index 0000000..e678c94
--- /dev/null
+++ b/src/apps/templates/dashboard.template.ts
@@ -0,0 +1,122 @@
+import { UITree } from '../types.js';
+
+export function buildDashboardTree(data: {
+ recentContacts: any[];
+ pipelines: any[];
+ calendars: any[];
+ locationId: string;
+}): UITree {
+ const contacts = data.recentContacts || [];
+ const pipelines = data.pipelines || [];
+ const calendars = data.calendars || [];
+
+ const totalContacts = contacts.length;
+ const totalPipelines = pipelines.length;
+ const totalCalendars = calendars.length;
+
+ // Recent contacts table rows
+ const contactRows = contacts.slice(0, 8).map((c: any) => ({
+ id: c.id || '',
+ name: `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unknown',
+ email: c.email || '—',
+ phone: c.phone || '—',
+ added: c.dateAdded ? new Date(c.dateAdded).toLocaleDateString() : '—',
+ }));
+
+ // Pipeline summary rows
+ const pipelineRows = pipelines.slice(0, 5).map((p: any) => ({
+ id: p.id || '',
+ name: p.name || 'Untitled',
+ stages: (p.stages || []).length,
+ status: 'active',
+ }));
+
+ return {
+ root: 'page',
+ elements: {
+ page: {
+ key: 'page',
+ type: 'PageHeader',
+ props: {
+ title: 'GHL Dashboard',
+ subtitle: 'Overview',
+ gradient: true,
+ stats: [
+ { label: 'Contacts', value: String(totalContacts) },
+ { label: 'Pipelines', value: String(totalPipelines) },
+ { label: 'Calendars', value: String(totalCalendars) },
+ ],
+ },
+ children: ['metrics', 'layout'],
+ },
+ metrics: {
+ key: 'metrics',
+ type: 'StatsGrid',
+ props: { columns: 3 },
+ children: ['mContacts', 'mPipelines', 'mCalendars'],
+ },
+ mContacts: {
+ key: 'mContacts',
+ type: 'MetricCard',
+ props: { label: 'Contacts', value: String(totalContacts), color: 'blue' },
+ },
+ mPipelines: {
+ key: 'mPipelines',
+ type: 'MetricCard',
+ props: { label: 'Pipelines', value: String(totalPipelines), color: 'purple' },
+ },
+ mCalendars: {
+ key: 'mCalendars',
+ type: 'MetricCard',
+ props: { label: 'Calendars', value: String(totalCalendars), color: 'green' },
+ },
+ layout: {
+ key: 'layout',
+ type: 'SplitLayout',
+ props: { ratio: '67/33', gap: 'md' },
+ children: ['contactsCard', 'pipelinesCard'],
+ },
+ contactsCard: {
+ key: 'contactsCard',
+ type: 'Card',
+ props: { title: 'Recent Contacts', padding: 'none' },
+ children: ['contactsTable'],
+ },
+ contactsTable: {
+ key: 'contactsTable',
+ type: 'DataTable',
+ props: {
+ columns: [
+ { key: 'name', label: 'Name', format: 'avatar', sortable: true },
+ { key: 'email', label: 'Email', format: 'email' },
+ { key: 'phone', label: 'Phone', format: 'phone' },
+ { key: 'added', label: 'Added', format: 'date' },
+ ],
+ rows: contactRows,
+ emptyMessage: 'No contacts yet',
+ pageSize: 8,
+ },
+ },
+ pipelinesCard: {
+ key: 'pipelinesCard',
+ type: 'Card',
+ props: { title: 'Pipelines', padding: 'none' },
+ children: ['pipelinesTable'],
+ },
+ pipelinesTable: {
+ key: 'pipelinesTable',
+ type: 'DataTable',
+ props: {
+ columns: [
+ { key: 'name', label: 'Pipeline', format: 'text' },
+ { key: 'stages', label: 'Stages', format: 'text' },
+ { key: 'status', label: 'Status', format: 'status' },
+ ],
+ rows: pipelineRows,
+ emptyMessage: 'No pipelines',
+ pageSize: 5,
+ },
+ },
+ },
+ };
+}
diff --git a/src/apps/templates/index.ts b/src/apps/templates/index.ts
new file mode 100644
index 0000000..540b4cb
--- /dev/null
+++ b/src/apps/templates/index.ts
@@ -0,0 +1,11 @@
+export { buildContactGridTree } from './contact-grid.template.js';
+export { buildPipelineBoardTree } from './pipeline-board.template.js';
+export { buildQuickBookTree } from './quick-book.template.js';
+export { buildOpportunityCardTree } from './opportunity-card.template.js';
+export { buildCalendarViewTree } from './calendar-view.template.js';
+export { buildInvoicePreviewTree } from './invoice-preview.template.js';
+export { buildCampaignStatsTree } from './campaign-stats.template.js';
+export { buildAgentStatsTree } from './agent-stats.template.js';
+export { buildContactTimelineTree } from './contact-timeline.template.js';
+export { buildWorkflowStatusTree } from './workflow-status.template.js';
+export { buildDashboardTree } from './dashboard.template.js';
diff --git a/src/apps/templates/invoice-preview.template.ts b/src/apps/templates/invoice-preview.template.ts
new file mode 100644
index 0000000..df8332d
--- /dev/null
+++ b/src/apps/templates/invoice-preview.template.ts
@@ -0,0 +1,112 @@
+import { UITree } from '../types.js';
+
+export function buildInvoicePreviewTree(data: any): UITree {
+ const invoice = data || {};
+ const contact = invoice.contact || invoice.contactDetails || {};
+ const businessInfo = invoice.businessDetails || {};
+ const items = invoice.items || invoice.lineItems || [];
+ const currency = invoice.currency || 'USD';
+
+ const contactName = contact.name || `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || 'Unknown';
+ const businessName = businessInfo.name || invoice.businessName || 'Business';
+
+ // Build line items
+ const lineItems = items.map((item: any) => ({
+ name: item.name || item.description || 'Item',
+ description: item.description !== item.name ? item.description : undefined,
+ quantity: item.quantity || item.qty || 1,
+ unitPrice: item.price || item.unitPrice || item.amount || 0,
+ total: (item.quantity || 1) * (item.price || item.unitPrice || item.amount || 0),
+ }));
+
+ const subtotal = lineItems.reduce((s: number, i: any) => s + i.total, 0);
+ const discount = invoice.discount || 0;
+ const tax = invoice.taxAmount || invoice.tax || 0;
+ const total = invoice.total || invoice.amount || subtotal - discount + tax;
+ const amountDue = invoice.amountDue ?? total;
+
+ const fmtCurrency = (n: number) => {
+ try {
+ return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n);
+ } catch {
+ return `$${n.toFixed(2)}`;
+ }
+ };
+
+ const totals: Array<{ label: string; value: string; bold?: boolean; variant?: string; isTotalRow?: boolean }> = [
+ { label: 'Subtotal', value: fmtCurrency(subtotal) },
+ ];
+ if (discount > 0) {
+ totals.push({ label: 'Discount', value: `-${fmtCurrency(discount)}`, variant: 'danger' });
+ }
+ if (tax > 0) {
+ totals.push({ label: 'Tax', value: fmtCurrency(tax) });
+ }
+ totals.push({ label: 'Total', value: fmtCurrency(total), bold: true, isTotalRow: true });
+ if (amountDue !== total) {
+ totals.push({ label: 'Amount Due', value: fmtCurrency(amountDue), variant: 'highlight' });
+ }
+
+ return {
+ root: 'page',
+ elements: {
+ page: {
+ key: 'page',
+ type: 'DetailHeader',
+ props: {
+ title: `Invoice #${invoice.invoiceNumber || invoice.number || '—'}`,
+ subtitle: invoice.title || `For ${contactName}`,
+ entityId: invoice.id,
+ status: invoice.status || 'draft',
+ statusVariant: invoice.status === 'paid' ? 'paid' : invoice.status === 'sent' ? 'sent' : 'draft',
+ },
+ children: ['infoRow', 'lineItemsTable', 'totals'],
+ },
+ infoRow: {
+ key: 'infoRow',
+ type: 'SplitLayout',
+ props: { ratio: '50/50', gap: 'md' },
+ children: ['fromInfo', 'toInfo'],
+ },
+ fromInfo: {
+ key: 'fromInfo',
+ type: 'InfoBlock',
+ props: {
+ label: 'From',
+ name: businessName,
+ lines: [
+ businessInfo.email || '',
+ businessInfo.phone || '',
+ businessInfo.address || '',
+ ].filter(Boolean),
+ },
+ },
+ toInfo: {
+ key: 'toInfo',
+ type: 'InfoBlock',
+ props: {
+ label: 'To',
+ name: contactName,
+ lines: [
+ contact.email || '',
+ contact.phone || '',
+ contact.address || '',
+ ].filter(Boolean),
+ },
+ },
+ lineItemsTable: {
+ key: 'lineItemsTable',
+ type: 'LineItemsTable',
+ props: {
+ items: lineItems,
+ currency,
+ },
+ },
+ totals: {
+ key: 'totals',
+ type: 'KeyValueList',
+ props: { items: totals },
+ },
+ },
+ };
+}
diff --git a/src/apps/templates/opportunity-card.template.ts b/src/apps/templates/opportunity-card.template.ts
new file mode 100644
index 0000000..3c313dd
--- /dev/null
+++ b/src/apps/templates/opportunity-card.template.ts
@@ -0,0 +1,135 @@
+import { UITree } from '../types.js';
+
+export function buildOpportunityCardTree(data: any): UITree {
+ const opp = data || {};
+ const contact = opp.contact || {};
+ const contactName = contact.name || `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || 'Unknown';
+ const monetaryValue = opp.monetaryValue || 0;
+
+ const kvItems = [
+ { label: 'Contact', value: contactName },
+ { label: 'Email', value: contact.email || '—' },
+ { label: 'Phone', value: contact.phone || '—' },
+ { label: 'Value', value: `$${Number(monetaryValue).toLocaleString()}`, bold: true },
+ { label: 'Status', value: (opp.status || 'open').charAt(0).toUpperCase() + (opp.status || 'open').slice(1) },
+ { label: 'Source', value: opp.source || '—' },
+ { label: 'Created', value: opp.createdAt ? new Date(opp.createdAt).toLocaleDateString() : '—' },
+ { label: 'Updated', value: opp.updatedAt ? new Date(opp.updatedAt).toLocaleDateString() : '—' },
+ ];
+
+ // Build timeline from available data
+ const timelineEvents: any[] = [];
+ if (opp.createdAt) {
+ timelineEvents.push({
+ id: 'created',
+ title: 'Opportunity Created',
+ description: `Created with value $${Number(monetaryValue).toLocaleString()}`,
+ timestamp: new Date(opp.createdAt).toLocaleString(),
+ icon: 'system',
+ variant: 'default',
+ });
+ }
+ if (opp.updatedAt && opp.updatedAt !== opp.createdAt) {
+ timelineEvents.push({
+ id: 'updated',
+ title: 'Last Updated',
+ description: `Status: ${opp.status || 'open'}`,
+ timestamp: new Date(opp.updatedAt).toLocaleString(),
+ icon: 'note',
+ variant: 'success',
+ });
+ }
+ if (opp.lastStatusChangeAt) {
+ timelineEvents.push({
+ id: 'status-change',
+ title: 'Status Changed',
+ description: `Changed to ${opp.status || 'open'}`,
+ timestamp: new Date(opp.lastStatusChangeAt).toLocaleString(),
+ icon: 'task',
+ variant: opp.status === 'won' ? 'success' : opp.status === 'lost' ? 'error' : 'default',
+ });
+ }
+
+ const elements: UITree['elements'] = {
+ page: {
+ key: 'page',
+ type: 'DetailHeader',
+ props: {
+ title: opp.name || 'Untitled Opportunity',
+ subtitle: contactName,
+ entityId: opp.id,
+ status: opp.status || 'open',
+ statusVariant: opp.status || 'open',
+ },
+ children: ['layout'],
+ },
+ layout: {
+ key: 'layout',
+ type: 'SplitLayout',
+ props: { ratio: '50/50', gap: 'md' },
+ children: ['details', 'rightCol'],
+ },
+ details: {
+ key: 'details',
+ type: 'KeyValueList',
+ props: { items: kvItems, compact: true },
+ },
+ rightCol: {
+ key: 'rightCol',
+ type: 'Card',
+ props: { title: 'Activity', padding: 'sm' },
+ children: ['timeline'],
+ },
+ };
+
+ if (timelineEvents.length > 0) {
+ elements.timeline = {
+ key: 'timeline',
+ type: 'Timeline',
+ props: { events: timelineEvents },
+ };
+ } else {
+ elements.timeline = {
+ key: 'timeline',
+ type: 'Timeline',
+ props: {
+ events: [
+ {
+ id: 'placeholder',
+ title: 'No activity recorded',
+ description: 'Activity will appear here as events are logged',
+ timestamp: new Date().toLocaleString(),
+ icon: 'system',
+ },
+ ],
+ },
+ };
+ }
+
+ // Add action bar
+ elements.actions = {
+ key: 'actions',
+ type: 'ActionBar',
+ props: { align: 'right' },
+ children: ['editBtn', 'statusBtn'],
+ };
+ elements.editBtn = {
+ key: 'editBtn',
+ type: 'ActionButton',
+ props: { label: 'Edit', variant: 'secondary', size: 'sm' },
+ };
+ elements.statusBtn = {
+ key: 'statusBtn',
+ type: 'ActionButton',
+ props: {
+ label: opp.status === 'won' ? 'Reopen' : 'Mark Won',
+ variant: 'primary',
+ size: 'sm',
+ },
+ };
+
+ // Add actions to page children
+ elements.page.children!.push('actions');
+
+ return { root: 'page', elements };
+}
diff --git a/src/apps/templates/pipeline-board.template.ts b/src/apps/templates/pipeline-board.template.ts
new file mode 100644
index 0000000..05b5cc3
--- /dev/null
+++ b/src/apps/templates/pipeline-board.template.ts
@@ -0,0 +1,67 @@
+import { UITree } from '../types.js';
+
+export function buildPipelineBoardTree(data: {
+ pipeline: any;
+ opportunities: any[];
+ stages: any[];
+}): UITree {
+ const pipeline = data.pipeline || {};
+ const opportunities = data.opportunities || [];
+ const stages = data.stages || [];
+
+ const totalValue = opportunities.reduce((s: number, o: any) => s + (o.monetaryValue || 0), 0);
+ const openCount = opportunities.filter((o: any) => o.status === 'open').length;
+ const wonCount = opportunities.filter((o: any) => o.status === 'won').length;
+
+ // Build kanban columns from real pipeline stages
+ const columns = stages.map((stage: any) => {
+ const stageOpps = opportunities.filter((o: any) => o.pipelineStageId === stage.id);
+ const stageValue = stageOpps.reduce((s: number, o: any) => s + (o.monetaryValue || 0), 0);
+
+ return {
+ id: stage.id,
+ title: stage.name,
+ count: stageOpps.length,
+ totalValue: stageValue > 0 ? `$${stageValue.toLocaleString()}` : undefined,
+ cards: stageOpps.slice(0, 8).map((opp: any) => ({
+ id: opp.id,
+ title: opp.name || 'Untitled',
+ subtitle: opp.contact?.name || opp.contact?.email || undefined,
+ value: opp.monetaryValue ? `$${Number(opp.monetaryValue).toLocaleString()}` : undefined,
+ status: opp.status || 'open',
+ statusVariant: opp.status || 'open',
+ date: opp.updatedAt ? new Date(opp.updatedAt).toLocaleDateString() : undefined,
+ avatarInitials: opp.contact?.name
+ ? opp.contact.name.split(' ').map((n: string) => n[0]).join('').slice(0, 2).toUpperCase()
+ : undefined,
+ })),
+ };
+ });
+
+ return {
+ root: 'page',
+ elements: {
+ page: {
+ key: 'page',
+ type: 'PageHeader',
+ props: {
+ title: pipeline.name || 'Pipeline',
+ subtitle: `${opportunities.length} opportunities`,
+ gradient: true,
+ stats: [
+ { label: 'Total Value', value: `$${totalValue.toLocaleString()}` },
+ { label: 'Open', value: String(openCount) },
+ { label: 'Won', value: String(wonCount) },
+ { label: 'Stages', value: String(stages.length) },
+ ],
+ },
+ children: ['board'],
+ },
+ board: {
+ key: 'board',
+ type: 'KanbanBoard',
+ props: { columns },
+ },
+ },
+ };
+}
diff --git a/src/apps/templates/quick-book.template.ts b/src/apps/templates/quick-book.template.ts
new file mode 100644
index 0000000..3e96f97
--- /dev/null
+++ b/src/apps/templates/quick-book.template.ts
@@ -0,0 +1,91 @@
+import { UITree } from '../types.js';
+
+export function buildQuickBookTree(data: {
+ calendar: any;
+ contact: any;
+ locationId: string;
+}): UITree {
+ const calendar = data.calendar || {};
+ const contact = data.contact || null;
+ const contactName = contact
+ ? `${contact.firstName || ''} ${contact.lastName || ''}`.trim()
+ : undefined;
+
+ const now = new Date();
+
+ return {
+ root: 'page',
+ elements: {
+ page: {
+ key: 'page',
+ type: 'PageHeader',
+ props: {
+ title: 'Quick Book',
+ subtitle: calendar.name || 'Appointment Booking',
+ gradient: true,
+ stats: contact
+ ? [
+ { label: 'Contact', value: contactName || 'Selected' },
+ { label: 'Calendar', value: calendar.name || '—' },
+ ]
+ : [{ label: 'Calendar', value: calendar.name || '—' }],
+ },
+ children: ['layout'],
+ },
+ layout: {
+ key: 'layout',
+ type: 'SplitLayout',
+ props: { ratio: '50/50', gap: 'md' },
+ children: ['calendarView', 'bookingForm'],
+ },
+ calendarView: {
+ key: 'calendarView',
+ type: 'CalendarView',
+ props: {
+ year: now.getFullYear(),
+ month: now.getMonth() + 1,
+ events: [],
+ highlightToday: true,
+ title: 'Select a Date',
+ },
+ },
+ bookingForm: {
+ key: 'bookingForm',
+ type: 'FormGroup',
+ props: {
+ fields: [
+ {
+ key: 'contactId',
+ label: 'Contact',
+ type: 'text',
+ value: contactName || '',
+ required: true,
+ },
+ {
+ key: 'date',
+ label: 'Date',
+ type: 'date',
+ value: '',
+ required: true,
+ },
+ {
+ key: 'time',
+ label: 'Time',
+ type: 'text',
+ value: '',
+ required: true,
+ },
+ {
+ key: 'notes',
+ label: 'Notes',
+ type: 'text',
+ value: '',
+ },
+ ],
+ submitLabel: 'Book Appointment',
+ submitTool: 'create_appointment',
+ },
+ },
+ },
+ };
+}
diff --git a/src/apps/templates/workflow-status.template.ts b/src/apps/templates/workflow-status.template.ts
new file mode 100644
index 0000000..600ff29
--- /dev/null
+++ b/src/apps/templates/workflow-status.template.ts
@@ -0,0 +1,120 @@
+import { UITree } from '../types.js';
+
+export function buildWorkflowStatusTree(data: {
+ workflow: any;
+ workflows: any[];
+ workflowId: string;
+ locationId: string;
+}): UITree {
+ const workflow = data.workflow || {};
+ const workflows = data.workflows || [];
+ const wfName = workflow.name || 'Workflow';
+
+ // Build flow diagram from workflow structure if available
+ const flowNodes: any[] = [];
+ const flowEdges: any[] = [];
+
+ // Add trigger node
+ flowNodes.push({
+ id: 'trigger',
+ label: workflow.trigger?.type || 'Trigger',
+ type: 'start',
+ description: workflow.trigger?.name || 'Workflow trigger',
+ });
+
+ // If workflow has actions/steps, map them
+ const actions = workflow.actions || workflow.steps || [];
+ let prevId = 'trigger';
+ for (let i = 0; i < Math.min(actions.length, 8); i++) {
+ const action = actions[i];
+ const nodeId = `action-${i}`;
+ const isCondition = action.type === 'condition' || action.type === 'if_else';
+
+ flowNodes.push({
+ id: nodeId,
+ label: action.name || action.type || `Step ${i + 1}`,
+ type: isCondition ? 'condition' : 'action',
+ description: action.description || undefined,
+ });
+ flowEdges.push({ from: prevId, to: nodeId });
+ prevId = nodeId;
+ }
+
+ // End node
+ flowNodes.push({ id: 'end', label: 'End', type: 'end' });
+ flowEdges.push({ from: prevId, to: 'end' });
+
+ // If no actions were found, create a simple placeholder flow
+ if (actions.length === 0 && !workflow.trigger) {
+ flowNodes.length = 0;
+ flowEdges.length = 0;
+ flowNodes.push(
+ { id: 'start', label: 'Start', type: 'start' },
+ { id: 'process', label: wfName, type: 'action', description: workflow.status || 'Active' },
+ { id: 'end', label: 'End', type: 'end' },
+ );
+ flowEdges.push(
+ { from: 'start', to: 'process' },
+ { from: 'process', to: 'end' },
+ );
+ }
+
+ // Workflow stats
+ const activeCount = workflows.filter((w: any) => w.status === 'active').length;
+ const draftCount = workflows.filter((w: any) => w.status === 'draft').length;
+
+ return {
+ root: 'page',
+ elements: {
+ page: {
+ key: 'page',
+ type: 'PageHeader',
+ props: {
+ title: wfName,
+ subtitle: 'Workflow Status',
+ status: workflow.status || 'active',
+ statusVariant: workflow.status === 'active' ? 'active' : 'draft',
+ gradient: true,
+ stats: [
+ { label: 'Status', value: (workflow.status || 'active').charAt(0).toUpperCase() + (workflow.status || 'active').slice(1) },
+ { label: 'Total Workflows', value: String(workflows.length) },
+ { label: 'Active', value: String(activeCount) },
+ { label: 'Draft', value: String(draftCount) },
+ ],
+ },
+ children: ['flow', 'statsGrid'],
+ },
+ flow: {
+ key: 'flow',
+ type: 'FlowDiagram',
+ props: {
+ nodes: flowNodes,
+ edges: flowEdges,
+ direction: 'horizontal',
+ title: `${wfName} Flow`,
+ },
+ },
+ statsGrid: {
+ key: 'statsGrid',
+ type: 'StatsGrid',
+ props: { columns: 3 },
+ children: ['sActive', 'sDraft', 'sTotal'],
+ },
+ sActive: {
+ key: 'sActive',
+ type: 'MetricCard',
+ props: { label: 'Active Workflows', value: String(activeCount), color: 'green' },
+ },
+ sDraft: {
+ key: 'sDraft',
+ type: 'MetricCard',
+ props: { label: 'Draft Workflows', value: String(draftCount), color: 'yellow' },
+ },
+ sTotal: {
+ key: 'sTotal',
+ type: 'MetricCard',
+ props: { label: 'Total', value: String(workflows.length), color: 'blue' },
+ },
+ },
+ };
+}
diff --git a/src/apps/types.ts b/src/apps/types.ts
new file mode 100644
index 0000000..72f1bc1
--- /dev/null
+++ b/src/apps/types.ts
@@ -0,0 +1,397 @@
+/**
+ * UITree Type Definitions for GHL MCP Apps
+ * Shared types for the universal renderer and template system.
+ */
+
+// ─── Core UITree Types ──────────────────────────────────────
+
+export interface UIElement {
+ key: string;
+ type: ComponentType;
+ props: Record;
+ children?: string[];
+}
+
+export interface UITree {
+ root: string;
+ elements: Record;
+}
+
+// ─── Component Type Union ───────────────────────────────────
+
+export type ComponentType =
+ // Layout
+ | 'PageHeader'
+ | 'Card'
+ | 'StatsGrid'
+ | 'SplitLayout'
+ | 'Section'
+ // Data Display
+ | 'DataTable'
+ | 'KanbanBoard'
+ | 'MetricCard'
+ | 'StatusBadge'
+ | 'Timeline'
+ | 'ProgressBar'
+ // Detail View
+ | 'DetailHeader'
+ | 'KeyValueList'
+ | 'LineItemsTable'
+ | 'InfoBlock'
+ // Interactive
+ | 'SearchBar'
+ | 'FilterChips'
+ | 'TabGroup'
+ | 'ActionButton'
+ | 'ActionBar'
+ // Extended Data Display
+ | 'CurrencyDisplay'
+ | 'TagList'
+ | 'CardGrid'
+ | 'AvatarGroup'
+ | 'StarRating'
+ | 'StockIndicator'
+ // Communication
+ | 'ChatThread'
+ | 'EmailPreview'
+ | 'ContentPreview'
+ | 'TranscriptView'
+ | 'AudioPlayer'
+ | 'ChecklistView'
+ // Visualization
+ | 'CalendarView'
+ | 'FlowDiagram'
+ | 'TreeView'
+ | 'MediaGallery'
+ | 'DuplicateCompare'
+ // Charts
+ | 'BarChart'
+ | 'LineChart'
+ | 'PieChart'
+ | 'FunnelChart'
+ | 'SparklineChart'
+ // Interactive Editors
+ | 'ContactPicker'
+ | 'InvoiceBuilder'
+ | 'OpportunityEditor'
+ | 'AppointmentBooker'
+ | 'EditableField'
+ | 'SelectDropdown'
+ | 'FormGroup'
+ | 'AmountInput';
+
+// ─── All valid component names for validation ───────────────
+
+export const VALID_COMPONENT_TYPES: ReadonlySet = new Set([
+ 'PageHeader', 'Card', 'StatsGrid', 'SplitLayout', 'Section',
+ 'DataTable', 'KanbanBoard', 'MetricCard', 'StatusBadge', 'Timeline', 'ProgressBar',
+ 'DetailHeader', 'KeyValueList', 'LineItemsTable', 'InfoBlock',
+ 'SearchBar', 'FilterChips', 'TabGroup', 'ActionButton', 'ActionBar',
+ 'CurrencyDisplay', 'TagList', 'CardGrid', 'AvatarGroup', 'StarRating', 'StockIndicator',
+ 'ChatThread', 'EmailPreview', 'ContentPreview', 'TranscriptView', 'AudioPlayer', 'ChecklistView',
+ 'CalendarView', 'FlowDiagram', 'TreeView', 'MediaGallery', 'DuplicateCompare',
+ 'BarChart', 'LineChart', 'PieChart', 'FunnelChart', 'SparklineChart',
+ 'ContactPicker', 'InvoiceBuilder', 'OpportunityEditor', 'AppointmentBooker',
+ 'EditableField', 'SelectDropdown', 'FormGroup', 'AmountInput',
+]);
+
+// ─── Components that can contain children ───────────────────
+
+export const CONTAINER_COMPONENTS: ReadonlySet = new Set([
+ 'PageHeader', 'Card', 'StatsGrid', 'SplitLayout', 'Section',
+ 'DetailHeader', 'ActionBar',
+]);
+
+// ─── Required props per component (minimal set) ─────────────
+
+export const REQUIRED_PROPS: Readonly> = {
+ PageHeader: ['title'],
+ DataTable: ['columns', 'rows'],
+ KanbanBoard: ['columns'],
+ MetricCard: ['label', 'value'],
+ StatusBadge: ['label', 'variant'],
+ Timeline: ['events'],
+ ProgressBar: ['label', 'value'],
+ DetailHeader: ['title'],
+ KeyValueList: ['items'],
+ LineItemsTable: ['items'],
+ InfoBlock: ['label', 'name', 'lines'],
+ CalendarView: [],
+ FlowDiagram: ['nodes', 'edges'],
+ BarChart: ['bars'],
+ LineChart: ['points'],
+ PieChart: ['segments'],
+ FunnelChart: ['stages'],
+ SparklineChart: ['values'],
+ CurrencyDisplay: ['amount'],
+ TagList: ['tags'],
+ CardGrid: ['cards'],
+ AvatarGroup: ['avatars'],
+ StarRating: ['rating'],
+ StockIndicator: ['quantity'],
+ ChatThread: ['messages'],
+ EmailPreview: ['from', 'to', 'subject', 'date', 'body'],
+ ChecklistView: ['items'],
+ FormGroup: ['fields'],
+ AmountInput: ['value'],
+ EditableField: ['value', 'fieldName'],
+ OpportunityEditor: ['saveTool', 'opportunity'],
+ ContactPicker: ['searchTool'],
+};
+
+// ─── Prop Interfaces for Template-Used Components ───────────
+
+export interface PageHeaderProps {
+ title: string;
+ subtitle?: string;
+ status?: string;
+ statusVariant?: 'active' | 'complete' | 'paused' | 'draft' | 'error' | 'sent' | 'paid' | 'pending';
+ gradient?: boolean;
+ stats?: Array<{ label: string; value: string }>;
+}
+
+export interface DataTableColumn {
+ key: string;
+ label: string;
+ sortable?: boolean;
+ align?: string;
+ format?: 'text' | 'email' | 'phone' | 'date' | 'currency' | 'tags' | 'avatar' | 'status';
+ width?: string;
+}
+
+export interface DataTableProps {
+ columns: DataTableColumn[];
+ rows: Record[];
+ selectable?: boolean;
+ rowAction?: string;
+ emptyMessage?: string;
+ pageSize?: number;
+}
+
+export interface KanbanCard {
+ id: string;
+ title: string;
+ subtitle?: string;
+ value?: string;
+ status?: string;
+ statusVariant?: string;
+ date?: string;
+ avatarInitials?: string;
+}
+
+export interface KanbanColumn {
+ id: string;
+ title: string;
+ count?: number;
+ totalValue?: string;
+ color?: string;
+ cards: KanbanCard[];
+}
+
+export interface KanbanBoardProps {
+ columns: KanbanColumn[];
+}
+
+export interface MetricCardProps {
+ label: string;
+ value: string;
+ format?: 'number' | 'currency' | 'percent';
+ trend?: 'up' | 'down' | 'flat';
+ trendValue?: string;
+ color?: 'default' | 'green' | 'blue' | 'purple' | 'yellow' | 'red';
+}
+
+export interface TimelineEvent {
+ id: string;
+ title: string;
+ description?: string;
+ timestamp: string;
+ icon?: 'email' | 'phone' | 'note' | 'meeting' | 'task' | 'system';
+ variant?: string;
+}
+
+export interface TimelineProps {
+ events: TimelineEvent[];
+}
+
+export interface KeyValueItem {
+ label: string;
+ value: string;
+ bold?: boolean;
+ variant?: 'default' | 'highlight' | 'muted' | 'success' | 'danger';
+ isTotalRow?: boolean;
+}
+
+export interface KeyValueListProps {
+ items: KeyValueItem[];
+ compact?: boolean;
+}
+
+export interface LineItem {
+ name: string;
+ description?: string;
+ quantity: number;
+ unitPrice: number;
+ total: number;
+}
+
+export interface LineItemsTableProps {
+ items: LineItem[];
+ currency?: string;
+}
+
+export interface InfoBlockProps {
+ label: string;
+ name: string;
+ lines: string[];
+}
+
+export interface DetailHeaderProps {
+ title: string;
+ subtitle?: string;
+ entityId?: string;
+ status?: string;
+ statusVariant?: string;
+}
+
+export interface SearchBarProps {
+ placeholder?: string;
+ valuePath?: string;
+}
+
+export interface CalendarEvent {
+ date: string;
+ title: string;
+ time?: string;
+ color?: string;
+ type?: 'meeting' | 'call' | 'task' | 'deadline' | 'event';
+}
+
+export interface CalendarViewProps {
+ year?: number;
+ month?: number;
+ events: CalendarEvent[];
+ highlightToday?: boolean;
+ title?: string;
+}
+
+export interface FlowNode {
+ id: string;
+ label: string;
+ type?: 'start' | 'action' | 'condition' | 'end';
+ description?: string;
+}
+
+export interface FlowEdge {
+ from: string;
+ to: string;
+ label?: string;
+}
+
+export interface FlowDiagramProps {
+ nodes: FlowNode[];
+ edges: FlowEdge[];
+ direction?: 'horizontal' | 'vertical';
+ title?: string;
+}
+
+export interface BarChartBar {
+ label: string;
+ value: number;
+ color?: string;
+}
+
+export interface BarChartProps {
+ bars: BarChartBar[];
+ orientation?: 'vertical' | 'horizontal';
+ maxValue?: number;
+ showValues?: boolean;
+ title?: string;
+}
+
+export interface LineChartPoint {
+ label: string;
+ value: number;
+}
+
+export interface LineChartProps {
+ points: LineChartPoint[];
+ color?: string;
+ showPoints?: boolean;
+ showArea?: boolean;
+ title?: string;
+ yAxisLabel?: string;
+}
+
+// ─── UITree Validation ──────────────────────────────────────
+
+export interface ValidationError {
+ path: string;
+ message: string;
+}
+
+/**
+ * Validate a UITree for correctness:
+ * - Root key exists in elements
+ * - All children references resolve
+ * - All component types are valid
+ * - Required props are present
+ */
+export function validateUITree(tree: UITree): ValidationError[] {
+ const errors: ValidationError[] = [];
+
+ if (!tree || typeof tree !== 'object') {
+ errors.push({ path: '', message: 'UITree must be a non-null object' });
+ return errors;
+ }
+
+ if (!tree.root) {
+ errors.push({ path: 'root', message: 'Missing root key' });
+ }
+
+ if (!tree.elements || typeof tree.elements !== 'object') {
+ errors.push({ path: 'elements', message: 'Missing or invalid elements map' });
+ return errors;
+ }
+
+ // Check root exists in elements
+ if (tree.root && !tree.elements[tree.root]) {
+ errors.push({ path: 'root', message: `Root key "${tree.root}" not found in elements` });
+ }
+
+ // Validate each element
+ for (const [key, element] of Object.entries(tree.elements)) {
+ const ePath = `elements.${key}`;
+
+ // Check key matches
+ if (element.key !== key) {
+ errors.push({ path: ePath, message: `Element key mismatch: "${element.key}" vs map key "${key}"` });
+ }
+
+ // Check component type
+ if (!VALID_COMPONENT_TYPES.has(element.type)) {
+ errors.push({ path: `${ePath}.type`, message: `Unknown component type: "${element.type}"` });
+ }
+
+ // Check required props
+ const requiredProps = REQUIRED_PROPS[element.type];
+ if (requiredProps) {
+ for (const prop of requiredProps) {
+ if (element.props[prop] === undefined || element.props[prop] === null) {
+ errors.push({ path: `${ePath}.props.${prop}`, message: `Missing required prop "${prop}" for ${element.type}` });
+ }
+ }
+ }
+
+ // Check children references
+ if (element.children) {
+ for (const childKey of element.children) {
+ if (!tree.elements[childKey]) {
+ errors.push({ path: `${ePath}.children`, message: `Child reference "${childKey}" not found in elements` });
+ }
+ }
+ }
+ }
+
+ return errors;
+}
diff --git a/src/tools/affiliates-tools.ts b/src/tools/affiliates-tools.ts
index a71615c..f3efd0e 100644
--- a/src/tools/affiliates-tools.ts
+++ b/src/tools/affiliates-tools.ts
@@ -22,6 +22,13 @@ export class AffiliatesTools {
limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' }
}
+ },
+ _meta: {
+ labels: {
+ category: "affiliates",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -31,7 +38,14 @@ export class AffiliatesTools {
type: 'object',
properties: {
campaignId: { type: 'string', description: 'Affiliate Campaign ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "affiliates",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['campaignId']
}
@@ -48,7 +62,14 @@ export class AffiliatesTools {
commissionType: { type: 'string', enum: ['percentage', 'fixed'], description: 'Commission type' },
commissionValue: { type: 'number', description: 'Commission value (percentage or fixed amount)' },
cookieDays: { type: 'number', description: 'Cookie tracking duration in days' },
- productIds: { type: 'array', items: { type: 'string' }, description: 'Product IDs for this campaign' }
+ productIds: { type: 'array', items: { type: 'string' }, description: 'Product IDs for this campaign' },
+ _meta: {
+ labels: {
+ category: "affiliates",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name', 'commissionType', 'commissionValue']
}
@@ -65,7 +86,14 @@ export class AffiliatesTools {
description: { type: 'string', description: 'Campaign description' },
commissionType: { type: 'string', enum: ['percentage', 'fixed'], description: 'Commission type' },
commissionValue: { type: 'number', description: 'Commission value' },
- status: { type: 'string', enum: ['active', 'inactive'], description: 'Campaign status' }
+ status: { type: 'string', enum: ['active', 'inactive'], description: 'Campaign status' },
+ _meta: {
+ labels: {
+ category: "affiliates",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['campaignId']
}
@@ -77,7 +105,14 @@ export class AffiliatesTools {
type: 'object',
properties: {
campaignId: { type: 'string', description: 'Campaign ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "affiliates",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['campaignId']
}
@@ -96,6 +131,13 @@ export class AffiliatesTools {
limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' }
}
+ },
+ _meta: {
+ labels: {
+ category: "affiliates",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -105,7 +147,14 @@ export class AffiliatesTools {
type: 'object',
properties: {
affiliateId: { type: 'string', description: 'Affiliate ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "affiliates",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['affiliateId']
}
@@ -120,7 +169,14 @@ export class AffiliatesTools {
contactId: { type: 'string', description: 'Contact ID to make affiliate' },
campaignId: { type: 'string', description: 'Campaign to assign to' },
customCode: { type: 'string', description: 'Custom affiliate code' },
- status: { type: 'string', enum: ['pending', 'approved'], description: 'Initial status' }
+ status: { type: 'string', enum: ['pending', 'approved'], description: 'Initial status' },
+ _meta: {
+ labels: {
+ category: "affiliates",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['contactId', 'campaignId']
}
@@ -134,7 +190,14 @@ export class AffiliatesTools {
affiliateId: { type: 'string', description: 'Affiliate ID' },
locationId: { type: 'string', description: 'Location ID' },
status: { type: 'string', enum: ['pending', 'approved', 'rejected'], description: 'Status' },
- customCode: { type: 'string', description: 'Custom affiliate code' }
+ customCode: { type: 'string', description: 'Custom affiliate code' },
+ _meta: {
+ labels: {
+ category: "affiliates",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['affiliateId']
}
@@ -146,7 +209,14 @@ export class AffiliatesTools {
type: 'object',
properties: {
affiliateId: { type: 'string', description: 'Affiliate ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "affiliates",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['affiliateId']
}
@@ -159,7 +229,14 @@ export class AffiliatesTools {
properties: {
affiliateId: { type: 'string', description: 'Affiliate ID' },
locationId: { type: 'string', description: 'Location ID' },
- reason: { type: 'string', description: 'Rejection reason' }
+ reason: { type: 'string', description: 'Rejection reason' },
+ _meta: {
+ labels: {
+ category: "affiliates",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['affiliateId']
}
@@ -171,7 +248,14 @@ export class AffiliatesTools {
type: 'object',
properties: {
affiliateId: { type: 'string', description: 'Affiliate ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "affiliates",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['affiliateId']
}
@@ -190,7 +274,14 @@ export class AffiliatesTools {
startDate: { type: 'string', description: 'Start date' },
endDate: { type: 'string', description: 'End date' },
limit: { type: 'number', description: 'Max results' },
- offset: { type: 'number', description: 'Pagination offset' }
+ offset: { type: 'number', description: 'Pagination offset' },
+ _meta: {
+ labels: {
+ category: "affiliates",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['affiliateId']
}
@@ -204,7 +295,14 @@ export class AffiliatesTools {
affiliateId: { type: 'string', description: 'Affiliate ID' },
locationId: { type: 'string', description: 'Location ID' },
startDate: { type: 'string', description: 'Start date' },
- endDate: { type: 'string', description: 'End date' }
+ endDate: { type: 'string', description: 'End date' },
+ _meta: {
+ labels: {
+ category: "affiliates",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['affiliateId']
}
@@ -219,7 +317,14 @@ export class AffiliatesTools {
locationId: { type: 'string', description: 'Location ID' },
amount: { type: 'number', description: 'Payout amount' },
commissionIds: { type: 'array', items: { type: 'string' }, description: 'Commission IDs to include' },
- note: { type: 'string', description: 'Payout note' }
+ note: { type: 'string', description: 'Payout note' },
+ _meta: {
+ labels: {
+ category: "affiliates",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['affiliateId', 'amount']
}
@@ -236,6 +341,13 @@ export class AffiliatesTools {
limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' }
}
+ },
+ _meta: {
+ labels: {
+ category: "affiliates",
+ access: "read",
+ complexity: "simple"
+ }
}
},
diff --git a/src/tools/association-tools.ts b/src/tools/association-tools.ts
index d68782c..65f614f 100644
--- a/src/tools/association-tools.ts
+++ b/src/tools/association-tools.ts
@@ -40,6 +40,13 @@ export class AssociationTools {
default: 20
}
}
+ },
+ _meta: {
+ labels: {
+ category: "associations",
+ access: "read",
+ complexity: "batch"
+ }
}
},
{
@@ -67,7 +74,14 @@ export class AssociationTools {
},
secondObjectKey: {
description: 'Key for the second object (e.g., "contact")'
- }
+ },
+ _meta: {
+ labels: {
+ category: "associations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['key', 'firstObjectLabel', 'firstObjectKey', 'secondObjectLabel', 'secondObjectKey']
}
@@ -81,7 +95,14 @@ export class AssociationTools {
associationId: {
type: 'string',
description: 'The ID of the association to retrieve'
- }
+ },
+ _meta: {
+ labels: {
+ category: "associations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['associationId']
}
@@ -101,7 +122,14 @@ export class AssociationTools {
},
secondObjectLabel: {
description: 'New label for the second object in the association'
- }
+ },
+ _meta: {
+ labels: {
+ category: "associations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['associationId', 'firstObjectLabel', 'secondObjectLabel']
}
@@ -115,7 +143,14 @@ export class AssociationTools {
associationId: {
type: 'string',
description: 'The ID of the association to delete'
- }
+ },
+ _meta: {
+ labels: {
+ category: "associations",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['associationId']
}
@@ -133,7 +168,14 @@ export class AssociationTools {
locationId: {
type: 'string',
description: 'GoHighLevel location ID (will use default if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "associations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['keyName']
}
@@ -151,7 +193,14 @@ export class AssociationTools {
locationId: {
type: 'string',
description: 'GoHighLevel location ID (optional)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "associations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['objectKey']
}
@@ -178,7 +227,14 @@ export class AssociationTools {
secondRecordId: {
type: 'string',
description: 'ID of the second record (e.g., custom object record ID if custom object is second object)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "associations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['associationId', 'firstRecordId', 'secondRecordId']
}
@@ -213,7 +269,14 @@ export class AssociationTools {
type: 'string'
},
description: 'Optional array of association IDs to filter relations'
- }
+ },
+ _meta: {
+ labels: {
+ category: "associations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['recordId']
}
@@ -231,7 +294,14 @@ export class AssociationTools {
locationId: {
type: 'string',
description: 'GoHighLevel location ID (will use default if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "associations",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['relationId']
}
diff --git a/src/tools/blog-tools.ts b/src/tools/blog-tools.ts
index cb8fc13..98e4894 100644
--- a/src/tools/blog-tools.ts
+++ b/src/tools/blog-tools.ts
@@ -94,6 +94,13 @@ export class BlogTools {
publishedAt: {
type: 'string',
description: 'Optional ISO timestamp for publication date (defaults to now for PUBLISHED status)'
+ },
+ _meta: {
+ labels: {
+ category: "blogs",
+ access: "write",
+ complexity: "simple"
+ }
}
},
required: ['title', 'blogId', 'content', 'description', 'imageUrl', 'imageAltText', 'urlSlug', 'author', 'categories']
@@ -165,6 +172,13 @@ export class BlogTools {
publishedAt: {
type: 'string',
description: 'Updated ISO timestamp for publication date'
+ },
+ _meta: {
+ labels: {
+ category: "blogs",
+ access: "write",
+ complexity: "simple"
+ }
}
},
required: ['postId', 'blogId']
@@ -200,6 +214,13 @@ export class BlogTools {
type: 'string',
enum: ['DRAFT', 'PUBLISHED', 'SCHEDULED', 'ARCHIVED'],
description: 'Optional filter by publication status'
+ },
+ _meta: {
+ labels: {
+ category: "blogs",
+ access: "read",
+ complexity: "simple"
+ }
}
},
required: ['blogId']
@@ -228,7 +249,14 @@ export class BlogTools {
description: 'Optional search term to filter blogs by name'
}
}
- }
+ },
+ _meta: {
+ labels: {
+ category: "blogs",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
// 5. Get Blog Authors
@@ -249,7 +277,14 @@ export class BlogTools {
default: 0
}
}
- }
+ },
+ _meta: {
+ labels: {
+ category: "blogs",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
// 6. Get Blog Categories
@@ -270,7 +305,14 @@ export class BlogTools {
default: 0
}
}
- }
+ },
+ _meta: {
+ labels: {
+ category: "blogs",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
// 7. Check URL Slug
@@ -287,6 +329,13 @@ export class BlogTools {
postId: {
type: 'string',
description: 'Optional post ID when updating an existing post (to exclude itself from the check)'
+ },
+ _meta: {
+ labels: {
+ category: "blogs",
+ access: "read",
+ complexity: "simple"
+ }
}
},
required: ['urlSlug']
diff --git a/src/tools/businesses-tools.ts b/src/tools/businesses-tools.ts
index a1e12bd..88ce583 100644
--- a/src/tools/businesses-tools.ts
+++ b/src/tools/businesses-tools.ts
@@ -21,6 +21,13 @@ export class BusinessesTools {
description: 'Location ID (uses default if not provided)'
}
}
+ },
+ _meta: {
+ labels: {
+ category: "businesses",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -36,7 +43,14 @@ export class BusinessesTools {
locationId: {
type: 'string',
description: 'Location ID (uses default if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "businesses",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['businessId']
}
@@ -94,7 +108,14 @@ export class BusinessesTools {
logoUrl: {
type: 'string',
description: 'URL to business logo image'
- }
+ },
+ _meta: {
+ labels: {
+ category: "businesses",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name']
}
@@ -156,7 +177,14 @@ export class BusinessesTools {
logoUrl: {
type: 'string',
description: 'URL to business logo image'
- }
+ },
+ _meta: {
+ labels: {
+ category: "businesses",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['businessId']
}
@@ -174,7 +202,14 @@ export class BusinessesTools {
locationId: {
type: 'string',
description: 'Location ID (uses default if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "businesses",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['businessId']
}
diff --git a/src/tools/calendar-tools.ts b/src/tools/calendar-tools.ts
index 3ae3400..66fe27c 100644
--- a/src/tools/calendar-tools.ts
+++ b/src/tools/calendar-tools.ts
@@ -62,6 +62,13 @@ export class CalendarTools {
inputSchema: {
type: 'object',
properties: {}
+ },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -80,6 +87,13 @@ export class CalendarTools {
default: true
}
}
+ },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -136,7 +150,14 @@ export class CalendarTools {
type: 'boolean',
description: 'Make calendar active immediately (default: true)',
default: true
- }
+ },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name', 'calendarType']
}
@@ -150,7 +171,14 @@ export class CalendarTools {
calendarId: {
type: 'string',
description: 'The unique ID of the calendar to retrieve'
- }
+ },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['calendarId']
}
@@ -192,7 +220,14 @@ export class CalendarTools {
isActive: {
type: 'boolean',
description: 'Updated active status'
- }
+ },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['calendarId']
}
@@ -206,7 +241,14 @@ export class CalendarTools {
calendarId: {
type: 'string',
description: 'The unique ID of the calendar to delete'
- }
+ },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['calendarId']
}
@@ -236,7 +278,14 @@ export class CalendarTools {
groupId: {
type: 'string',
description: 'Filter events by calendar group ID'
- }
+ },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['startTime', 'endTime']
}
@@ -266,7 +315,14 @@ export class CalendarTools {
userId: {
type: 'string',
description: 'Specific user ID to check availability for'
- }
+ },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['calendarId', 'startDate', 'endDate']
}
@@ -326,7 +382,14 @@ export class CalendarTools {
type: 'boolean',
description: 'Send notifications for this appointment',
default: true
- }
+ },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['calendarId', 'contactId', 'startTime']
}
@@ -340,7 +403,14 @@ export class CalendarTools {
appointmentId: {
type: 'string',
description: 'The unique ID of the appointment to retrieve'
- }
+ },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['appointmentId']
}
@@ -384,7 +454,14 @@ export class CalendarTools {
type: 'boolean',
description: 'Send notifications for this update',
default: true
- }
+ },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['appointmentId']
}
@@ -398,7 +475,14 @@ export class CalendarTools {
appointmentId: {
type: 'string',
description: 'The unique ID of the appointment to delete'
- }
+ },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['appointmentId']
}
@@ -428,7 +512,14 @@ export class CalendarTools {
assignedUserId: {
type: 'string',
description: 'User ID to apply the block for'
- }
+ },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['startTime', 'endTime']
}
@@ -462,7 +553,14 @@ export class CalendarTools {
assignedUserId: {
type: 'string',
description: 'Updated assigned user ID'
- }
+ },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['blockSlotId']
}
@@ -476,7 +574,14 @@ export class CalendarTools {
name: { type: 'string', description: 'Group name' },
description: { type: 'string', description: 'Group description' },
slug: { type: 'string', description: 'URL slug for the group' },
- isActive: { type: 'boolean', description: 'Whether group is active', default: true }
+ isActive: { type: 'boolean', description: 'Whether group is active', default: true },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name', 'description', 'slug']
}
@@ -488,7 +593,14 @@ export class CalendarTools {
type: 'object',
properties: {
slug: { type: 'string', description: 'Slug to validate' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['slug']
}
@@ -502,7 +614,14 @@ export class CalendarTools {
groupId: { type: 'string', description: 'Calendar group ID' },
name: { type: 'string', description: 'Group name' },
description: { type: 'string', description: 'Group description' },
- slug: { type: 'string', description: 'URL slug for the group' }
+ slug: { type: 'string', description: 'URL slug for the group' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['groupId', 'name', 'description', 'slug']
}
@@ -513,7 +632,14 @@ export class CalendarTools {
inputSchema: {
type: 'object',
properties: {
- groupId: { type: 'string', description: 'Calendar group ID' }
+ groupId: { type: 'string', description: 'Calendar group ID' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['groupId']
}
@@ -525,7 +651,14 @@ export class CalendarTools {
type: 'object',
properties: {
groupId: { type: 'string', description: 'Calendar group ID' },
- isActive: { type: 'boolean', description: 'Whether to enable (true) or disable (false) the group' }
+ isActive: { type: 'boolean', description: 'Whether to enable (true) or disable (false) the group' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['groupId', 'isActive']
}
@@ -538,7 +671,14 @@ export class CalendarTools {
properties: {
appointmentId: { type: 'string', description: 'Appointment ID' },
limit: { type: 'number', description: 'Maximum number of notes to return', default: 10 },
- offset: { type: 'number', description: 'Number of notes to skip', default: 0 }
+ offset: { type: 'number', description: 'Number of notes to skip', default: 0 },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['appointmentId']
}
@@ -551,7 +691,14 @@ export class CalendarTools {
properties: {
appointmentId: { type: 'string', description: 'Appointment ID' },
body: { type: 'string', description: 'Note content' },
- userId: { type: 'string', description: 'User ID creating the note' }
+ userId: { type: 'string', description: 'User ID creating the note' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['appointmentId', 'body']
}
@@ -565,7 +712,14 @@ export class CalendarTools {
appointmentId: { type: 'string', description: 'Appointment ID' },
noteId: { type: 'string', description: 'Note ID' },
body: { type: 'string', description: 'Updated note content' },
- userId: { type: 'string', description: 'User ID updating the note' }
+ userId: { type: 'string', description: 'User ID updating the note' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['appointmentId', 'noteId', 'body']
}
@@ -577,7 +731,14 @@ export class CalendarTools {
type: 'object',
properties: {
appointmentId: { type: 'string', description: 'Appointment ID' },
- noteId: { type: 'string', description: 'Note ID' }
+ noteId: { type: 'string', description: 'Note ID' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['appointmentId', 'noteId']
}
@@ -591,6 +752,13 @@ export class CalendarTools {
limit: { type: 'number', description: 'Maximum number to return', default: 20 },
skip: { type: 'number', description: 'Number to skip', default: 0 }
}
+ },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -604,7 +772,14 @@ export class CalendarTools {
quantity: { type: 'number', description: 'Total quantity available' },
outOfService: { type: 'number', description: 'Number currently out of service' },
capacity: { type: 'number', description: 'Capacity per unit' },
- calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' }
+ calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name', 'description', 'quantity', 'outOfService', 'capacity', 'calendarIds']
}
@@ -615,7 +790,14 @@ export class CalendarTools {
inputSchema: {
type: 'object',
properties: {
- resourceId: { type: 'string', description: 'Equipment resource ID' }
+ resourceId: { type: 'string', description: 'Equipment resource ID' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['resourceId']
}
@@ -633,7 +815,14 @@ export class CalendarTools {
outOfService: { type: 'number', description: 'Number currently out of service' },
capacity: { type: 'number', description: 'Capacity per unit' },
calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' },
- isActive: { type: 'boolean', description: 'Whether resource is active' }
+ isActive: { type: 'boolean', description: 'Whether resource is active' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['resourceId']
}
@@ -644,7 +833,14 @@ export class CalendarTools {
inputSchema: {
type: 'object',
properties: {
- resourceId: { type: 'string', description: 'Equipment resource ID' }
+ resourceId: { type: 'string', description: 'Equipment resource ID' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['resourceId']
}
@@ -658,6 +854,13 @@ export class CalendarTools {
limit: { type: 'number', description: 'Maximum number to return', default: 20 },
skip: { type: 'number', description: 'Number to skip', default: 0 }
}
+ },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -671,7 +874,14 @@ export class CalendarTools {
quantity: { type: 'number', description: 'Total quantity available' },
outOfService: { type: 'number', description: 'Number currently out of service' },
capacity: { type: 'number', description: 'Room capacity' },
- calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' }
+ calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name', 'description', 'quantity', 'outOfService', 'capacity', 'calendarIds']
}
@@ -682,7 +892,14 @@ export class CalendarTools {
inputSchema: {
type: 'object',
properties: {
- resourceId: { type: 'string', description: 'Room resource ID' }
+ resourceId: { type: 'string', description: 'Room resource ID' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['resourceId']
}
@@ -700,7 +917,14 @@ export class CalendarTools {
outOfService: { type: 'number', description: 'Number currently out of service' },
capacity: { type: 'number', description: 'Room capacity' },
calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' },
- isActive: { type: 'boolean', description: 'Whether resource is active' }
+ isActive: { type: 'boolean', description: 'Whether resource is active' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['resourceId']
}
@@ -711,7 +935,14 @@ export class CalendarTools {
inputSchema: {
type: 'object',
properties: {
- resourceId: { type: 'string', description: 'Room resource ID' }
+ resourceId: { type: 'string', description: 'Room resource ID' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['resourceId']
}
@@ -726,7 +957,14 @@ export class CalendarTools {
isActive: { type: 'boolean', description: 'Filter by active status' },
deleted: { type: 'boolean', description: 'Include deleted notifications' },
limit: { type: 'number', description: 'Maximum number to return' },
- skip: { type: 'number', description: 'Number to skip' }
+ skip: { type: 'number', description: 'Number to skip' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['calendarId']
}
@@ -749,7 +987,14 @@ export class CalendarTools {
isActive: { type: 'boolean', description: 'Whether notification is active' },
templateId: { type: 'string', description: 'Template ID' },
body: { type: 'string', description: 'Notification body' },
- subject: { type: 'string', description: 'Notification subject' }
+ subject: { type: 'string', description: 'Notification subject' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['receiverType', 'channel', 'notificationType']
},
@@ -766,7 +1011,14 @@ export class CalendarTools {
type: 'object',
properties: {
calendarId: { type: 'string', description: 'Calendar ID' },
- notificationId: { type: 'string', description: 'Notification ID' }
+ notificationId: { type: 'string', description: 'Notification ID' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['calendarId', 'notificationId']
}
@@ -786,7 +1038,14 @@ export class CalendarTools {
deleted: { type: 'boolean', description: 'Whether notification is deleted' },
templateId: { type: 'string', description: 'Template ID' },
body: { type: 'string', description: 'Notification body' },
- subject: { type: 'string', description: 'Notification subject' }
+ subject: { type: 'string', description: 'Notification subject' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['calendarId', 'notificationId']
}
@@ -798,7 +1057,14 @@ export class CalendarTools {
type: 'object',
properties: {
calendarId: { type: 'string', description: 'Calendar ID' },
- notificationId: { type: 'string', description: 'Notification ID' }
+ notificationId: { type: 'string', description: 'Notification ID' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['calendarId', 'notificationId']
}
@@ -813,7 +1079,14 @@ export class CalendarTools {
calendarId: { type: 'string', description: 'Filter by calendar ID' },
groupId: { type: 'string', description: 'Filter by group ID' },
startTime: { type: 'string', description: 'Start time for the query range' },
- endTime: { type: 'string', description: 'End time for the query range' }
+ endTime: { type: 'string', description: 'End time for the query range' },
+ _meta: {
+ labels: {
+ category: "calendar",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['startTime', 'endTime']
}
diff --git a/src/tools/campaigns-tools.ts b/src/tools/campaigns-tools.ts
index 1076c94..9c94ec1 100644
--- a/src/tools/campaigns-tools.ts
+++ b/src/tools/campaigns-tools.ts
@@ -22,6 +22,13 @@ export class CampaignsTools {
limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' }
}
+ },
+ _meta: {
+ labels: {
+ category: "campaigns",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -31,7 +38,14 @@ export class CampaignsTools {
type: 'object',
properties: {
campaignId: { type: 'string', description: 'Campaign ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "campaigns",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['campaignId']
}
@@ -45,7 +59,14 @@ export class CampaignsTools {
locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Campaign name' },
type: { type: 'string', enum: ['email', 'sms', 'voicemail'], description: 'Campaign type' },
- status: { type: 'string', enum: ['draft', 'scheduled'], description: 'Initial status' }
+ status: { type: 'string', enum: ['draft', 'scheduled'], description: 'Initial status' },
+ _meta: {
+ labels: {
+ category: "campaigns",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name', 'type']
}
@@ -59,7 +80,14 @@ export class CampaignsTools {
campaignId: { type: 'string', description: 'Campaign ID' },
locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Campaign name' },
- status: { type: 'string', enum: ['draft', 'scheduled', 'paused'], description: 'Campaign status' }
+ status: { type: 'string', enum: ['draft', 'scheduled', 'paused'], description: 'Campaign status' },
+ _meta: {
+ labels: {
+ category: "campaigns",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['campaignId']
}
@@ -71,7 +99,14 @@ export class CampaignsTools {
type: 'object',
properties: {
campaignId: { type: 'string', description: 'Campaign ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "campaigns",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['campaignId']
}
@@ -85,7 +120,14 @@ export class CampaignsTools {
type: 'object',
properties: {
campaignId: { type: 'string', description: 'Campaign ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "campaigns",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['campaignId']
}
@@ -97,7 +139,14 @@ export class CampaignsTools {
type: 'object',
properties: {
campaignId: { type: 'string', description: 'Campaign ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "campaigns",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['campaignId']
}
@@ -109,7 +158,14 @@ export class CampaignsTools {
type: 'object',
properties: {
campaignId: { type: 'string', description: 'Campaign ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "campaigns",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['campaignId']
}
@@ -123,7 +179,14 @@ export class CampaignsTools {
type: 'object',
properties: {
campaignId: { type: 'string', description: 'Campaign ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "campaigns",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['campaignId']
}
@@ -138,7 +201,14 @@ export class CampaignsTools {
locationId: { type: 'string', description: 'Location ID' },
status: { type: 'string', enum: ['sent', 'delivered', 'opened', 'clicked', 'bounced', 'unsubscribed'], description: 'Filter by recipient status' },
limit: { type: 'number', description: 'Max results' },
- offset: { type: 'number', description: 'Pagination offset' }
+ offset: { type: 'number', description: 'Pagination offset' },
+ _meta: {
+ labels: {
+ category: "campaigns",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['campaignId']
}
@@ -155,6 +225,13 @@ export class CampaignsTools {
contactId: { type: 'string', description: 'Filter by contact ID' },
campaignId: { type: 'string', description: 'Filter by campaign ID' }
}
+ },
+ _meta: {
+ labels: {
+ category: "campaigns",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -164,7 +241,14 @@ export class CampaignsTools {
type: 'object',
properties: {
messageId: { type: 'string', description: 'Scheduled message ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "campaigns",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['messageId']
}
diff --git a/src/tools/companies-tools.ts b/src/tools/companies-tools.ts
index c51c45c..c5aa958 100644
--- a/src/tools/companies-tools.ts
+++ b/src/tools/companies-tools.ts
@@ -33,6 +33,13 @@ export class CompaniesTools {
description: 'Search query to filter companies'
}
}
+ },
+ _meta: {
+ labels: {
+ category: "general",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -48,7 +55,14 @@ export class CompaniesTools {
locationId: {
type: 'string',
description: 'Location ID (uses default if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "general",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['companyId']
}
@@ -127,7 +141,14 @@ export class CompaniesTools {
id: { type: 'string' },
key: { type: 'string' },
value: { type: 'string' }
- }
+ },
+ _meta: {
+ labels: {
+ category: "general",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
description: 'Custom field values'
},
@@ -214,7 +235,14 @@ export class CompaniesTools {
id: { type: 'string' },
key: { type: 'string' },
value: { type: 'string' }
- }
+ },
+ _meta: {
+ labels: {
+ category: "general",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
description: 'Custom field values'
},
@@ -240,7 +268,14 @@ export class CompaniesTools {
locationId: {
type: 'string',
description: 'Location ID (uses default if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "general",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['companyId']
}
diff --git a/src/tools/contact-tools.ts b/src/tools/contact-tools.ts
index b51e6d6..7ddc10f 100644
--- a/src/tools/contact-tools.ts
+++ b/src/tools/contact-tools.ts
@@ -81,6 +81,13 @@ export class ContactTools {
source: { type: 'string', description: 'Source of the contact' }
},
required: ['email']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "write",
+ complexity: "simple"
+ }
}
},
{
@@ -94,6 +101,13 @@ export class ContactTools {
phone: { type: 'string', description: 'Filter by phone number' },
limit: { type: 'number', description: 'Maximum number of results (default: 25)' }
}
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -105,6 +119,13 @@ export class ContactTools {
contactId: { type: 'string', description: 'Contact ID' }
},
required: ['contactId']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -121,6 +142,13 @@ export class ContactTools {
tags: { type: 'array', items: { type: 'string' }, description: 'Tags to assign to contact' }
},
required: ['contactId']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "write",
+ complexity: "simple"
+ }
}
},
{
@@ -132,6 +160,13 @@ export class ContactTools {
contactId: { type: 'string', description: 'Contact ID' }
},
required: ['contactId']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "delete",
+ complexity: "simple"
+ }
}
},
{
@@ -144,6 +179,13 @@ export class ContactTools {
tags: { type: 'array', items: { type: 'string' }, description: 'Tags to add' }
},
required: ['contactId', 'tags']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "write",
+ complexity: "simple"
+ }
}
},
{
@@ -156,6 +198,13 @@ export class ContactTools {
tags: { type: 'array', items: { type: 'string' }, description: 'Tags to remove' }
},
required: ['contactId', 'tags']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "write",
+ complexity: "simple"
+ }
}
},
@@ -169,6 +218,13 @@ export class ContactTools {
contactId: { type: 'string', description: 'Contact ID' }
},
required: ['contactId']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -185,6 +241,13 @@ export class ContactTools {
assignedTo: { type: 'string', description: 'User ID to assign task to' }
},
required: ['contactId', 'title', 'dueDate']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "write",
+ complexity: "simple"
+ }
}
},
{
@@ -197,6 +260,13 @@ export class ContactTools {
taskId: { type: 'string', description: 'Task ID' }
},
required: ['contactId', 'taskId']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -214,6 +284,13 @@ export class ContactTools {
assignedTo: { type: 'string', description: 'User ID to assign task to' }
},
required: ['contactId', 'taskId']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "write",
+ complexity: "simple"
+ }
}
},
{
@@ -226,6 +303,13 @@ export class ContactTools {
taskId: { type: 'string', description: 'Task ID' }
},
required: ['contactId', 'taskId']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "delete",
+ complexity: "simple"
+ }
}
},
{
@@ -239,6 +323,13 @@ export class ContactTools {
completed: { type: 'boolean', description: 'Completion status' }
},
required: ['contactId', 'taskId', 'completed']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "write",
+ complexity: "simple"
+ }
}
},
@@ -252,6 +343,13 @@ export class ContactTools {
contactId: { type: 'string', description: 'Contact ID' }
},
required: ['contactId']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -265,6 +363,13 @@ export class ContactTools {
userId: { type: 'string', description: 'User ID creating the note' }
},
required: ['contactId', 'body']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "write",
+ complexity: "simple"
+ }
}
},
{
@@ -277,6 +382,13 @@ export class ContactTools {
noteId: { type: 'string', description: 'Note ID' }
},
required: ['contactId', 'noteId']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -291,6 +403,13 @@ export class ContactTools {
userId: { type: 'string', description: 'User ID updating the note' }
},
required: ['contactId', 'noteId', 'body']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "write",
+ complexity: "simple"
+ }
}
},
{
@@ -303,6 +422,13 @@ export class ContactTools {
noteId: { type: 'string', description: 'Note ID' }
},
required: ['contactId', 'noteId']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "delete",
+ complexity: "simple"
+ }
}
},
@@ -321,6 +447,13 @@ export class ContactTools {
source: { type: 'string', description: 'Source of the contact' },
assignedTo: { type: 'string', description: 'User ID to assign contact to' }
}
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "write",
+ complexity: "complex"
+ }
}
},
{
@@ -332,6 +465,13 @@ export class ContactTools {
email: { type: 'string', description: 'Email to check for duplicates' },
phone: { type: 'string', description: 'Phone to check for duplicates' }
}
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -346,6 +486,13 @@ export class ContactTools {
query: { type: 'string', description: 'Search query' }
},
required: ['businessId']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -357,6 +504,13 @@ export class ContactTools {
contactId: { type: 'string', description: 'Contact ID' }
},
required: ['contactId']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "read",
+ complexity: "simple"
+ }
}
},
@@ -373,6 +527,13 @@ export class ContactTools {
removeAllTags: { type: 'boolean', description: 'Remove all existing tags before adding new ones' }
},
required: ['contactIds', 'tags', 'operation']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "write",
+ complexity: "batch"
+ }
}
},
{
@@ -385,6 +546,13 @@ export class ContactTools {
businessId: { type: 'string', description: 'Business ID (null to remove from business)' }
},
required: ['contactIds']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "write",
+ complexity: "batch"
+ }
}
},
@@ -399,6 +567,13 @@ export class ContactTools {
followers: { type: 'array', items: { type: 'string' }, description: 'Array of user IDs to add as followers' }
},
required: ['contactId', 'followers']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "write",
+ complexity: "simple"
+ }
}
},
{
@@ -411,6 +586,13 @@ export class ContactTools {
followers: { type: 'array', items: { type: 'string' }, description: 'Array of user IDs to remove as followers' }
},
required: ['contactId', 'followers']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "write",
+ complexity: "simple"
+ }
}
},
@@ -425,6 +607,13 @@ export class ContactTools {
campaignId: { type: 'string', description: 'Campaign ID' }
},
required: ['contactId', 'campaignId']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "write",
+ complexity: "simple"
+ }
}
},
{
@@ -437,6 +626,13 @@ export class ContactTools {
campaignId: { type: 'string', description: 'Campaign ID' }
},
required: ['contactId', 'campaignId']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "write",
+ complexity: "simple"
+ }
}
},
{
@@ -448,6 +644,13 @@ export class ContactTools {
contactId: { type: 'string', description: 'Contact ID' }
},
required: ['contactId']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "write",
+ complexity: "batch"
+ }
}
},
@@ -463,6 +666,13 @@ export class ContactTools {
eventStartTime: { type: 'string', description: 'Event start time (ISO format)' }
},
required: ['contactId', 'workflowId']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "write",
+ complexity: "simple"
+ }
}
},
{
@@ -476,6 +686,13 @@ export class ContactTools {
eventStartTime: { type: 'string', description: 'Event start time (ISO format)' }
},
required: ['contactId', 'workflowId']
+ },
+ _meta: {
+ labels: {
+ category: "contacts",
+ access: "write",
+ complexity: "simple"
+ }
}
}
];
diff --git a/src/tools/conversation-tools.ts b/src/tools/conversation-tools.ts
index 127d5f1..13cb6db 100644
--- a/src/tools/conversation-tools.ts
+++ b/src/tools/conversation-tools.ts
@@ -69,7 +69,14 @@ export class ConversationTools {
fromNumber: {
type: 'string',
description: 'Optional: Phone number to send from (must be configured in GHL)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['contactId', 'message']
}
@@ -115,7 +122,14 @@ export class ConversationTools {
type: 'array',
items: { type: 'string' },
description: 'Optional: Array of BCC email addresses'
- }
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['contactId', 'subject']
}
@@ -152,6 +166,13 @@ export class ConversationTools {
description: 'Filter by user ID assigned to conversations'
}
}
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -181,7 +202,14 @@ export class ConversationTools {
]
},
description: 'Filter messages by type (optional)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['conversationId']
}
@@ -195,7 +223,14 @@ export class ConversationTools {
contactId: {
type: 'string',
description: 'The unique ID of the contact to create conversation with'
- }
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['contactId']
}
@@ -218,7 +253,14 @@ export class ConversationTools {
type: 'number',
description: 'Set the unread message count (0 to mark as read)',
minimum: 0
- }
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['conversationId']
}
@@ -243,6 +285,13 @@ export class ConversationTools {
default: 'unread'
}
}
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -254,7 +303,14 @@ export class ConversationTools {
conversationId: {
type: 'string',
description: 'The unique ID of the conversation to delete'
- }
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['conversationId']
}
@@ -270,7 +326,14 @@ export class ConversationTools {
emailMessageId: {
type: 'string',
description: 'The unique ID of the email message to retrieve'
- }
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['emailMessageId']
}
@@ -284,7 +347,14 @@ export class ConversationTools {
messageId: {
type: 'string',
description: 'The unique ID of the message to retrieve'
- }
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['messageId']
}
@@ -303,7 +373,14 @@ export class ConversationTools {
type: 'array',
items: { type: 'string' },
description: 'Array of file URLs to upload as attachments'
- }
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['conversationId', 'attachmentUrls']
}
@@ -330,7 +407,14 @@ export class ConversationTools {
code: { type: 'string' },
type: { type: 'string' },
message: { type: 'string' }
- }
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
emailMessageId: {
type: 'string',
@@ -425,7 +509,14 @@ export class ConversationTools {
description: 'Call status'
}
}
- }
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['type', 'conversationId', 'conversationProviderId']
}
@@ -469,7 +560,14 @@ export class ConversationTools {
date: {
type: 'string',
description: 'Date of the call (ISO format)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['conversationId', 'conversationProviderId', 'to', 'from', 'status']
}
@@ -485,7 +583,14 @@ export class ConversationTools {
messageId: {
type: 'string',
description: 'The unique ID of the call message to get recording for'
- }
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['messageId']
}
@@ -499,7 +604,14 @@ export class ConversationTools {
messageId: {
type: 'string',
description: 'The unique ID of the call message to get transcription for'
- }
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['messageId']
}
@@ -513,7 +625,14 @@ export class ConversationTools {
messageId: {
type: 'string',
description: 'The unique ID of the call message to download transcription for'
- }
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['messageId']
}
@@ -529,7 +648,14 @@ export class ConversationTools {
messageId: {
type: 'string',
description: 'The unique ID of the scheduled message to cancel'
- }
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['messageId']
}
@@ -543,7 +669,14 @@ export class ConversationTools {
emailMessageId: {
type: 'string',
description: 'The unique ID of the scheduled email to cancel'
- }
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['emailMessageId']
}
@@ -567,7 +700,14 @@ export class ConversationTools {
isTyping: {
type: 'boolean',
description: 'Whether the agent is currently typing'
- }
+ },
+ _meta: {
+ labels: {
+ category: "conversations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['visitorId', 'conversationId', 'isTyping']
}
diff --git a/src/tools/courses-tools.ts b/src/tools/courses-tools.ts
index de639c5..00775ff 100644
--- a/src/tools/courses-tools.ts
+++ b/src/tools/courses-tools.ts
@@ -21,6 +21,13 @@ export class CoursesTools {
limit: { type: 'number', description: 'Max results to return' },
offset: { type: 'number', description: 'Offset for pagination' }
}
+ },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -32,7 +39,14 @@ export class CoursesTools {
locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Import job name' },
sourceUrl: { type: 'string', description: 'Source URL to import from' },
- type: { type: 'string', description: 'Import type' }
+ type: { type: 'string', description: 'Import type' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name']
}
@@ -49,6 +63,13 @@ export class CoursesTools {
limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' }
}
+ },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -58,7 +79,14 @@ export class CoursesTools {
type: 'object',
properties: {
productId: { type: 'string', description: 'Course product ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['productId']
}
@@ -73,7 +101,14 @@ export class CoursesTools {
title: { type: 'string', description: 'Product title' },
description: { type: 'string', description: 'Product description' },
imageUrl: { type: 'string', description: 'Product image URL' },
- statementDescriptor: { type: 'string', description: 'Payment statement descriptor' }
+ statementDescriptor: { type: 'string', description: 'Payment statement descriptor' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['title']
}
@@ -88,7 +123,14 @@ export class CoursesTools {
locationId: { type: 'string', description: 'Location ID' },
title: { type: 'string', description: 'Product title' },
description: { type: 'string', description: 'Product description' },
- imageUrl: { type: 'string', description: 'Product image URL' }
+ imageUrl: { type: 'string', description: 'Product image URL' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['productId']
}
@@ -100,7 +142,14 @@ export class CoursesTools {
type: 'object',
properties: {
productId: { type: 'string', description: 'Course product ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['productId']
}
@@ -117,6 +166,13 @@ export class CoursesTools {
limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' }
}
+ },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -126,7 +182,14 @@ export class CoursesTools {
type: 'object',
properties: {
locationId: { type: 'string', description: 'Location ID' },
- title: { type: 'string', description: 'Category title' }
+ title: { type: 'string', description: 'Category title' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['title']
}
@@ -139,7 +202,14 @@ export class CoursesTools {
properties: {
categoryId: { type: 'string', description: 'Category ID' },
locationId: { type: 'string', description: 'Location ID' },
- title: { type: 'string', description: 'Category title' }
+ title: { type: 'string', description: 'Category title' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['categoryId', 'title']
}
@@ -151,7 +221,14 @@ export class CoursesTools {
type: 'object',
properties: {
categoryId: { type: 'string', description: 'Category ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['categoryId']
}
@@ -169,6 +246,13 @@ export class CoursesTools {
offset: { type: 'number', description: 'Pagination offset' },
categoryId: { type: 'string', description: 'Filter by category' }
}
+ },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -178,7 +262,14 @@ export class CoursesTools {
type: 'object',
properties: {
courseId: { type: 'string', description: 'Course ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['courseId']
}
@@ -194,7 +285,14 @@ export class CoursesTools {
description: { type: 'string', description: 'Course description' },
thumbnailUrl: { type: 'string', description: 'Course thumbnail URL' },
visibility: { type: 'string', enum: ['published', 'draft'], description: 'Course visibility' },
- categoryId: { type: 'string', description: 'Category ID to place course in' }
+ categoryId: { type: 'string', description: 'Category ID to place course in' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['title']
}
@@ -210,7 +308,14 @@ export class CoursesTools {
title: { type: 'string', description: 'Course title' },
description: { type: 'string', description: 'Course description' },
thumbnailUrl: { type: 'string', description: 'Course thumbnail URL' },
- visibility: { type: 'string', enum: ['published', 'draft'], description: 'Course visibility' }
+ visibility: { type: 'string', enum: ['published', 'draft'], description: 'Course visibility' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['courseId']
}
@@ -222,7 +327,14 @@ export class CoursesTools {
type: 'object',
properties: {
courseId: { type: 'string', description: 'Course ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['courseId']
}
@@ -236,7 +348,14 @@ export class CoursesTools {
type: 'object',
properties: {
courseId: { type: 'string', description: 'Course ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['courseId']
}
@@ -251,7 +370,14 @@ export class CoursesTools {
locationId: { type: 'string', description: 'Location ID' },
userId: { type: 'string', description: 'User ID of instructor' },
name: { type: 'string', description: 'Instructor display name' },
- bio: { type: 'string', description: 'Instructor bio' }
+ bio: { type: 'string', description: 'Instructor bio' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['courseId']
}
@@ -267,7 +393,14 @@ export class CoursesTools {
courseId: { type: 'string', description: 'Course ID' },
locationId: { type: 'string', description: 'Location ID' },
limit: { type: 'number', description: 'Max results' },
- offset: { type: 'number', description: 'Pagination offset' }
+ offset: { type: 'number', description: 'Pagination offset' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['courseId']
}
@@ -280,7 +413,14 @@ export class CoursesTools {
properties: {
courseId: { type: 'string', description: 'Course ID' },
postId: { type: 'string', description: 'Post/Lesson ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['courseId', 'postId']
}
@@ -297,7 +437,14 @@ export class CoursesTools {
contentType: { type: 'string', enum: ['video', 'text', 'quiz', 'assignment'], description: 'Content type' },
content: { type: 'string', description: 'Post content (text/HTML)' },
videoUrl: { type: 'string', description: 'Video URL (if video type)' },
- visibility: { type: 'string', enum: ['published', 'draft'], description: 'Visibility' }
+ visibility: { type: 'string', enum: ['published', 'draft'], description: 'Visibility' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['courseId', 'title']
}
@@ -314,7 +461,14 @@ export class CoursesTools {
title: { type: 'string', description: 'Post/lesson title' },
content: { type: 'string', description: 'Post content' },
videoUrl: { type: 'string', description: 'Video URL' },
- visibility: { type: 'string', enum: ['published', 'draft'], description: 'Visibility' }
+ visibility: { type: 'string', enum: ['published', 'draft'], description: 'Visibility' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['courseId', 'postId']
}
@@ -327,7 +481,14 @@ export class CoursesTools {
properties: {
courseId: { type: 'string', description: 'Course ID' },
postId: { type: 'string', description: 'Post/Lesson ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['courseId', 'postId']
}
@@ -341,7 +502,14 @@ export class CoursesTools {
type: 'object',
properties: {
productId: { type: 'string', description: 'Course product ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['productId']
}
@@ -358,7 +526,14 @@ export class CoursesTools {
price: { type: 'number', description: 'Price in cents' },
currency: { type: 'string', description: 'Currency code (e.g., USD)' },
type: { type: 'string', enum: ['one-time', 'subscription'], description: 'Payment type' },
- interval: { type: 'string', enum: ['month', 'year'], description: 'Subscription interval (if subscription)' }
+ interval: { type: 'string', enum: ['month', 'year'], description: 'Subscription interval (if subscription)' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['productId', 'name', 'price']
}
@@ -373,7 +548,14 @@ export class CoursesTools {
offerId: { type: 'string', description: 'Offer ID' },
locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Offer name' },
- price: { type: 'number', description: 'Price in cents' }
+ price: { type: 'number', description: 'Price in cents' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['productId', 'offerId']
}
@@ -386,7 +568,14 @@ export class CoursesTools {
properties: {
productId: { type: 'string', description: 'Course product ID' },
offerId: { type: 'string', description: 'Offer ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['productId', 'offerId']
}
@@ -402,7 +591,14 @@ export class CoursesTools {
courseId: { type: 'string', description: 'Course ID' },
locationId: { type: 'string', description: 'Location ID' },
limit: { type: 'number', description: 'Max results' },
- offset: { type: 'number', description: 'Pagination offset' }
+ offset: { type: 'number', description: 'Pagination offset' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['courseId']
}
@@ -415,7 +611,14 @@ export class CoursesTools {
properties: {
courseId: { type: 'string', description: 'Course ID' },
contactId: { type: 'string', description: 'Contact ID to enroll' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['courseId', 'contactId']
}
@@ -428,7 +631,14 @@ export class CoursesTools {
properties: {
courseId: { type: 'string', description: 'Course ID' },
contactId: { type: 'string', description: 'Contact ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['courseId', 'contactId']
}
@@ -443,7 +653,14 @@ export class CoursesTools {
properties: {
courseId: { type: 'string', description: 'Course ID' },
contactId: { type: 'string', description: 'Contact/Student ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['courseId', 'contactId']
}
@@ -458,7 +675,14 @@ export class CoursesTools {
postId: { type: 'string', description: 'Post/Lesson ID' },
contactId: { type: 'string', description: 'Contact/Student ID' },
locationId: { type: 'string', description: 'Location ID' },
- completed: { type: 'boolean', description: 'Whether lesson is completed' }
+ completed: { type: 'boolean', description: 'Whether lesson is completed' },
+ _meta: {
+ labels: {
+ category: "courses",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['courseId', 'postId', 'contactId', 'completed']
}
diff --git a/src/tools/custom-field-v2-tools.ts b/src/tools/custom-field-v2-tools.ts
index f12a6e1..0fabe96 100644
--- a/src/tools/custom-field-v2-tools.ts
+++ b/src/tools/custom-field-v2-tools.ts
@@ -26,7 +26,14 @@ export class CustomFieldV2Tools {
id: {
type: 'string',
description: 'The ID of the custom field or folder to retrieve'
- }
+ },
+ _meta: {
+ labels: {
+ category: "custom-fields",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['id']
}
@@ -74,7 +81,14 @@ export class CustomFieldV2Tools {
url: {
type: 'string',
description: 'URL associated with the option (only for RADIO type)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "custom-fields",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['key', 'label']
},
@@ -160,7 +174,14 @@ export class CustomFieldV2Tools {
url: {
type: 'string',
description: 'URL associated with the option (only for RADIO type)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "custom-fields",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['key', 'label']
},
@@ -188,7 +209,14 @@ export class CustomFieldV2Tools {
id: {
type: 'string',
description: 'The ID of the custom field to delete'
- }
+ },
+ _meta: {
+ labels: {
+ category: "custom-fields",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['id']
}
@@ -206,7 +234,14 @@ export class CustomFieldV2Tools {
locationId: {
type: 'string',
description: 'GoHighLevel location ID (will use default if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "custom-fields",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['objectKey']
}
@@ -229,7 +264,14 @@ export class CustomFieldV2Tools {
locationId: {
type: 'string',
description: 'GoHighLevel location ID (will use default if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "custom-fields",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['objectKey', 'name']
}
@@ -251,7 +293,14 @@ export class CustomFieldV2Tools {
locationId: {
type: 'string',
description: 'GoHighLevel location ID (will use default if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "custom-fields",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['id', 'name']
}
@@ -269,7 +318,14 @@ export class CustomFieldV2Tools {
locationId: {
type: 'string',
description: 'GoHighLevel location ID (will use default if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "custom-fields",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['id']
}
diff --git a/src/tools/email-isv-tools.ts b/src/tools/email-isv-tools.ts
index f9d0fe0..26c6270 100644
--- a/src/tools/email-isv-tools.ts
+++ b/src/tools/email-isv-tools.ts
@@ -40,7 +40,14 @@ export class EmailISVTools {
verify: {
type: 'string',
description: 'Email address to verify (if type=email) or contact ID (if type=contact)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "email",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId', 'type', 'verify']
}
diff --git a/src/tools/email-tools.ts b/src/tools/email-tools.ts
index aa5e15d..6f1046f 100644
--- a/src/tools/email-tools.ts
+++ b/src/tools/email-tools.ts
@@ -50,6 +50,13 @@ export class EmailTools {
default: 0
}
}
+ },
+ _meta: {
+ labels: {
+ category: "email",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -70,7 +77,14 @@ export class EmailTools {
type: 'boolean',
description: 'Whether the template is plain text.',
default: false
- }
+ },
+ _meta: {
+ labels: {
+ category: "email",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['title', 'html']
}
@@ -92,6 +106,13 @@ export class EmailTools {
default: 0
}
}
+ },
+ _meta: {
+ labels: {
+ category: "email",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -111,7 +132,14 @@ export class EmailTools {
previewText: {
type: 'string',
description: 'The updated preview text for the template.'
- }
+ },
+ _meta: {
+ labels: {
+ category: "email",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['templateId', 'html']
}
@@ -125,7 +153,14 @@ export class EmailTools {
templateId: {
type: 'string',
description: 'The ID of the template to delete.'
- }
+ },
+ _meta: {
+ labels: {
+ category: "email",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['templateId']
}
diff --git a/src/tools/forms-tools.ts b/src/tools/forms-tools.ts
index 9ae005f..40f73ba 100644
--- a/src/tools/forms-tools.ts
+++ b/src/tools/forms-tools.ts
@@ -33,6 +33,13 @@ export class FormsTools {
description: 'Filter by form type (e.g., "form", "survey")'
}
}
+ },
+ _meta: {
+ labels: {
+ category: "forms",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -68,7 +75,14 @@ export class FormsTools {
page: {
type: 'number',
description: 'Page number for pagination'
- }
+ },
+ _meta: {
+ labels: {
+ category: "forms",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['formId']
}
@@ -86,7 +100,14 @@ export class FormsTools {
locationId: {
type: 'string',
description: 'Location ID (uses default if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "forms",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['formId']
}
diff --git a/src/tools/funnels-tools.ts b/src/tools/funnels-tools.ts
index b964319..1dcda45 100644
--- a/src/tools/funnels-tools.ts
+++ b/src/tools/funnels-tools.ts
@@ -46,6 +46,13 @@ export class FunnelsTools {
description: 'Filter by type (funnel or website)'
}
}
+ },
+ _meta: {
+ labels: {
+ category: "funnels",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -61,7 +68,14 @@ export class FunnelsTools {
locationId: {
type: 'string',
description: 'Location ID (uses default if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "funnels",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['funnelId']
}
@@ -87,7 +101,14 @@ export class FunnelsTools {
limit: {
type: 'number',
description: 'Maximum number of pages to return'
- }
+ },
+ _meta: {
+ labels: {
+ category: "funnels",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['funnelId']
}
@@ -105,7 +126,14 @@ export class FunnelsTools {
locationId: {
type: 'string',
description: 'Location ID (uses default if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "funnels",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['funnelId']
}
@@ -136,7 +164,14 @@ export class FunnelsTools {
pathName: {
type: 'string',
description: 'Source path for the redirect'
- }
+ },
+ _meta: {
+ labels: {
+ category: "funnels",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['funnelId', 'target', 'action']
}
@@ -171,7 +206,14 @@ export class FunnelsTools {
pathName: {
type: 'string',
description: 'Source path for the redirect'
- }
+ },
+ _meta: {
+ labels: {
+ category: "funnels",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['funnelId', 'redirectId']
}
@@ -193,7 +235,14 @@ export class FunnelsTools {
locationId: {
type: 'string',
description: 'Location ID (uses default if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "funnels",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['funnelId', 'redirectId']
}
@@ -219,7 +268,14 @@ export class FunnelsTools {
limit: {
type: 'number',
description: 'Maximum number of redirects to return'
- }
+ },
+ _meta: {
+ labels: {
+ category: "funnels",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['funnelId']
}
diff --git a/src/tools/invoices-tools.ts b/src/tools/invoices-tools.ts
index c15b76d..1a1fd69 100644
--- a/src/tools/invoices-tools.ts
+++ b/src/tools/invoices-tools.ts
@@ -85,7 +85,14 @@ export class InvoicesTools {
title: { type: 'string', description: 'Invoice title' },
currency: { type: 'string', description: 'Currency code' },
issueDate: { type: 'string', description: 'Issue date' },
- dueDate: { type: 'string', description: 'Due date' }
+ dueDate: { type: 'string', description: 'Due date' },
+ _meta: {
+ labels: {
+ category: "invoices",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name']
}
@@ -101,7 +108,14 @@ export class InvoicesTools {
offset: { type: 'string', description: 'Offset for pagination', default: '0' },
status: { type: 'string', description: 'Filter by status' },
search: { type: 'string', description: 'Search term' },
- paymentMode: { type: 'string', enum: ['default', 'live', 'test'], description: 'Payment mode' }
+ paymentMode: { type: 'string', enum: ['default', 'live', 'test'], description: 'Payment mode' },
+ _meta: {
+ labels: {
+ category: "invoices",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['limit', 'offset']
}
@@ -113,7 +127,14 @@ export class InvoicesTools {
type: 'object',
properties: {
templateId: { type: 'string', description: 'Template ID' },
- altId: { type: 'string', description: 'Location ID' }
+ altId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "invoices",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['templateId']
}
@@ -128,7 +149,14 @@ export class InvoicesTools {
altId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Template name' },
title: { type: 'string', description: 'Invoice title' },
- currency: { type: 'string', description: 'Currency code' }
+ currency: { type: 'string', description: 'Currency code' },
+ _meta: {
+ labels: {
+ category: "invoices",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['templateId']
}
@@ -140,7 +168,14 @@ export class InvoicesTools {
type: 'object',
properties: {
templateId: { type: 'string', description: 'Template ID' },
- altId: { type: 'string', description: 'Location ID' }
+ altId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "invoices",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['templateId']
}
@@ -157,7 +192,14 @@ export class InvoicesTools {
name: { type: 'string', description: 'Schedule name' },
templateId: { type: 'string', description: 'Template ID' },
contactId: { type: 'string', description: 'Contact ID' },
- frequency: { type: 'string', description: 'Schedule frequency' }
+ frequency: { type: 'string', description: 'Schedule frequency' },
+ _meta: {
+ labels: {
+ category: "invoices",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name', 'templateId', 'contactId']
}
@@ -172,7 +214,14 @@ export class InvoicesTools {
limit: { type: 'string', description: 'Number of results per page', default: '10' },
offset: { type: 'string', description: 'Offset for pagination', default: '0' },
status: { type: 'string', description: 'Filter by status' },
- search: { type: 'string', description: 'Search term' }
+ search: { type: 'string', description: 'Search term' },
+ _meta: {
+ labels: {
+ category: "invoices",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['limit', 'offset']
}
@@ -184,7 +233,14 @@ export class InvoicesTools {
type: 'object',
properties: {
scheduleId: { type: 'string', description: 'Schedule ID' },
- altId: { type: 'string', description: 'Location ID' }
+ altId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "invoices",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['scheduleId']
}
@@ -203,7 +259,14 @@ export class InvoicesTools {
currency: { type: 'string', description: 'Currency code' },
issueDate: { type: 'string', description: 'Issue date' },
dueDate: { type: 'string', description: 'Due date' },
- items: { type: 'array', description: 'Invoice items' }
+ items: { type: 'array', description: 'Invoice items' },
+ _meta: {
+ labels: {
+ category: "invoices",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['contactId', 'title']
}
@@ -219,7 +282,14 @@ export class InvoicesTools {
offset: { type: 'string', description: 'Offset for pagination', default: '0' },
status: { type: 'string', description: 'Filter by status' },
contactId: { type: 'string', description: 'Filter by contact ID' },
- search: { type: 'string', description: 'Search term' }
+ search: { type: 'string', description: 'Search term' },
+ _meta: {
+ labels: {
+ category: "invoices",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['limit', 'offset']
}
@@ -231,7 +301,14 @@ export class InvoicesTools {
type: 'object',
properties: {
invoiceId: { type: 'string', description: 'Invoice ID' },
- altId: { type: 'string', description: 'Location ID' }
+ altId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "invoices",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['invoiceId']
}
@@ -246,7 +323,14 @@ export class InvoicesTools {
altId: { type: 'string', description: 'Location ID' },
emailTo: { type: 'string', description: 'Email address to send to' },
subject: { type: 'string', description: 'Email subject' },
- message: { type: 'string', description: 'Email message' }
+ message: { type: 'string', description: 'Email message' },
+ _meta: {
+ labels: {
+ category: "invoices",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['invoiceId']
}
@@ -264,7 +348,14 @@ export class InvoicesTools {
title: { type: 'string', description: 'Estimate title' },
currency: { type: 'string', description: 'Currency code' },
issueDate: { type: 'string', description: 'Issue date' },
- validUntil: { type: 'string', description: 'Valid until date' }
+ validUntil: { type: 'string', description: 'Valid until date' },
+ _meta: {
+ labels: {
+ category: "invoices",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['contactId', 'title']
}
@@ -280,7 +371,14 @@ export class InvoicesTools {
offset: { type: 'string', description: 'Offset for pagination', default: '0' },
status: { type: 'string', enum: ['all', 'draft', 'sent', 'accepted', 'declined', 'invoiced', 'viewed'], description: 'Filter by status' },
contactId: { type: 'string', description: 'Filter by contact ID' },
- search: { type: 'string', description: 'Search term' }
+ search: { type: 'string', description: 'Search term' },
+ _meta: {
+ labels: {
+ category: "invoices",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['limit', 'offset']
}
@@ -295,7 +393,14 @@ export class InvoicesTools {
altId: { type: 'string', description: 'Location ID' },
emailTo: { type: 'string', description: 'Email address to send to' },
subject: { type: 'string', description: 'Email subject' },
- message: { type: 'string', description: 'Email message' }
+ message: { type: 'string', description: 'Email message' },
+ _meta: {
+ labels: {
+ category: "invoices",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['estimateId']
}
@@ -309,7 +414,14 @@ export class InvoicesTools {
estimateId: { type: 'string', description: 'Estimate ID' },
altId: { type: 'string', description: 'Location ID' },
issueDate: { type: 'string', description: 'Invoice issue date' },
- dueDate: { type: 'string', description: 'Invoice due date' }
+ dueDate: { type: 'string', description: 'Invoice due date' },
+ _meta: {
+ labels: {
+ category: "invoices",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['estimateId']
}
@@ -324,6 +436,13 @@ export class InvoicesTools {
properties: {
altId: { type: 'string', description: 'Location ID' }
}
+ },
+ _meta: {
+ labels: {
+ category: "invoices",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
diff --git a/src/tools/links-tools.ts b/src/tools/links-tools.ts
index c490909..ab6ed4c 100644
--- a/src/tools/links-tools.ts
+++ b/src/tools/links-tools.ts
@@ -29,6 +29,13 @@ export class LinksTools {
description: 'Maximum number of links to return'
}
}
+ },
+ _meta: {
+ labels: {
+ category: "links",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -44,7 +51,14 @@ export class LinksTools {
locationId: {
type: 'string',
description: 'Location ID (uses default if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "links",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['linkId']
}
@@ -74,7 +88,14 @@ export class LinksTools {
fieldValue: {
type: 'string',
description: 'Value to set for the custom field'
- }
+ },
+ _meta: {
+ labels: {
+ category: "links",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name', 'redirectTo']
}
@@ -108,7 +129,14 @@ export class LinksTools {
fieldValue: {
type: 'string',
description: 'Value to set for the custom field'
- }
+ },
+ _meta: {
+ labels: {
+ category: "links",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['linkId']
}
@@ -126,7 +154,14 @@ export class LinksTools {
locationId: {
type: 'string',
description: 'Location ID (uses default if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "links",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['linkId']
}
diff --git a/src/tools/location-tools.ts b/src/tools/location-tools.ts
index da53f0d..c5ca402 100644
--- a/src/tools/location-tools.ts
+++ b/src/tools/location-tools.ts
@@ -81,6 +81,13 @@ export class LocationTools {
format: 'email'
}
}
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -92,7 +99,14 @@ export class LocationTools {
locationId: {
type: 'string',
description: 'The unique ID of the location to retrieve'
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId']
}
@@ -148,7 +162,14 @@ export class LocationTools {
properties: {
firstName: { type: 'string', description: 'Prospect first name' },
lastName: { type: 'string', description: 'Prospect last name' },
- email: { type: 'string', format: 'email', description: 'Prospect email' }
+ email: { type: 'string', format: 'email', description: 'Prospect email' },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['firstName', 'lastName', 'email'],
description: 'Prospect information for the location'
@@ -210,7 +231,14 @@ export class LocationTools {
timezone: {
type: 'string',
description: 'Updated timezone'
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId', 'companyId']
}
@@ -229,7 +257,14 @@ export class LocationTools {
type: 'boolean',
description: 'Whether to delete associated Twilio account',
default: false
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId', 'deleteTwilioAccount']
}
@@ -245,7 +280,14 @@ export class LocationTools {
locationId: {
type: 'string',
description: 'The location ID to get tags from'
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId']
}
@@ -263,7 +305,14 @@ export class LocationTools {
name: {
type: 'string',
description: 'Name of the tag to create'
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId', 'name']
}
@@ -281,7 +330,14 @@ export class LocationTools {
tagId: {
type: 'string',
description: 'The tag ID to retrieve'
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId', 'tagId']
}
@@ -303,7 +359,14 @@ export class LocationTools {
name: {
type: 'string',
description: 'Updated name for the tag'
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId', 'tagId', 'name']
}
@@ -321,7 +384,14 @@ export class LocationTools {
tagId: {
type: 'string',
description: 'The tag ID to delete'
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId', 'tagId']
}
@@ -369,7 +439,14 @@ export class LocationTools {
businessId: {
type: 'string',
description: 'Business ID filter'
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId']
}
@@ -391,7 +468,14 @@ export class LocationTools {
enum: ['contact', 'opportunity', 'all'],
description: 'Filter by model type (default: all)',
default: 'all'
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId']
}
@@ -428,7 +512,14 @@ export class LocationTools {
type: 'number',
description: 'Position/order of the field (default: 0)',
default: 0
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId', 'name', 'dataType']
}
@@ -446,7 +537,14 @@ export class LocationTools {
customFieldId: {
type: 'string',
description: 'The custom field ID to retrieve'
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId', 'customFieldId']
}
@@ -476,7 +574,14 @@ export class LocationTools {
position: {
type: 'number',
description: 'Updated position/order'
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId', 'customFieldId', 'name']
}
@@ -494,7 +599,14 @@ export class LocationTools {
customFieldId: {
type: 'string',
description: 'The custom field ID to delete'
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId', 'customFieldId']
}
@@ -510,7 +622,14 @@ export class LocationTools {
locationId: {
type: 'string',
description: 'The location ID'
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId']
}
@@ -532,7 +651,14 @@ export class LocationTools {
value: {
type: 'string',
description: 'Value to assign'
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId', 'name', 'value']
}
@@ -550,7 +676,14 @@ export class LocationTools {
customValueId: {
type: 'string',
description: 'The custom value ID to retrieve'
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId', 'customValueId']
}
@@ -576,7 +709,14 @@ export class LocationTools {
value: {
type: 'string',
description: 'Updated value'
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId', 'customValueId', 'name', 'value']
}
@@ -594,7 +734,14 @@ export class LocationTools {
customValueId: {
type: 'string',
description: 'The custom value ID to delete'
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId', 'customValueId']
}
@@ -634,7 +781,14 @@ export class LocationTools {
type: 'string',
enum: ['sms', 'email', 'whatsapp'],
description: 'Filter by template type'
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId', 'originId']
}
@@ -652,7 +806,14 @@ export class LocationTools {
templateId: {
type: 'string',
description: 'The template ID to delete'
- }
+ },
+ _meta: {
+ labels: {
+ category: "locations",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId', 'templateId']
}
diff --git a/src/tools/media-tools.ts b/src/tools/media-tools.ts
index 713cde4..d30d79b 100644
--- a/src/tools/media-tools.ts
+++ b/src/tools/media-tools.ts
@@ -80,7 +80,14 @@ export class MediaTools {
parentId: {
type: 'string',
description: 'Parent folder ID to list files within a specific folder'
- }
+ },
+ _meta: {
+ labels: {
+ category: "media",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: []
}
@@ -121,7 +128,14 @@ export class MediaTools {
altId: {
type: 'string',
description: 'Location or Agency ID (uses default location if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "media",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: []
}
@@ -145,7 +159,14 @@ export class MediaTools {
altId: {
type: 'string',
description: 'Location or Agency ID (uses default location if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "media",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['id']
}
diff --git a/src/tools/oauth-tools.ts b/src/tools/oauth-tools.ts
index 86c601d..4decf01 100644
--- a/src/tools/oauth-tools.ts
+++ b/src/tools/oauth-tools.ts
@@ -20,6 +20,13 @@ export class OAuthTools {
locationId: { type: 'string', description: 'Location ID' },
companyId: { type: 'string', description: 'Company ID for agency-level apps' }
}
+ },
+ _meta: {
+ labels: {
+ category: "oauth",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -29,7 +36,14 @@ export class OAuthTools {
type: 'object',
properties: {
appId: { type: 'string', description: 'OAuth App ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "oauth",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['appId']
}
@@ -45,7 +59,14 @@ export class OAuthTools {
skip: { type: 'number', description: 'Records to skip' },
limit: { type: 'number', description: 'Max results' },
query: { type: 'string', description: 'Search query' },
- isInstalled: { type: 'boolean', description: 'Filter by installation status' }
+ isInstalled: { type: 'boolean', description: 'Filter by installation status' },
+ _meta: {
+ labels: {
+ category: "oauth",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['appId', 'companyId']
}
@@ -58,6 +79,13 @@ export class OAuthTools {
inputSchema: {
type: 'object',
properties: {}
+ },
+ _meta: {
+ labels: {
+ category: "oauth",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -67,7 +95,14 @@ export class OAuthTools {
type: 'object',
properties: {
companyId: { type: 'string', description: 'Company/Agency ID' },
- locationId: { type: 'string', description: 'Target Location ID' }
+ locationId: { type: 'string', description: 'Target Location ID' },
+ _meta: {
+ labels: {
+ category: "oauth",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['companyId', 'locationId']
}
@@ -82,6 +117,13 @@ export class OAuthTools {
properties: {
locationId: { type: 'string', description: 'Location ID' }
}
+ },
+ _meta: {
+ labels: {
+ category: "oauth",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -91,7 +133,14 @@ export class OAuthTools {
type: 'object',
properties: {
integrationId: { type: 'string', description: 'Integration ID to disconnect' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "oauth",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['integrationId']
}
@@ -106,6 +155,13 @@ export class OAuthTools {
properties: {
locationId: { type: 'string', description: 'Location ID' }
}
+ },
+ _meta: {
+ labels: {
+ category: "oauth",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -120,7 +176,14 @@ export class OAuthTools {
type: 'array',
items: { type: 'string' },
description: 'Permission scopes for the key'
- }
+ },
+ _meta: {
+ labels: {
+ category: "oauth",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name']
}
@@ -132,7 +195,14 @@ export class OAuthTools {
type: 'object',
properties: {
keyId: { type: 'string', description: 'API Key ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "oauth",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['keyId']
}
diff --git a/src/tools/object-tools.ts b/src/tools/object-tools.ts
index f16ccbc..dde6b26 100644
--- a/src/tools/object-tools.ts
+++ b/src/tools/object-tools.ts
@@ -48,7 +48,14 @@ export class ObjectTools {
locationId: {
type: 'string',
description: 'Location ID (uses default if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "objects",
+ access: "read",
+ complexity: "batch"
+ }
+ }
},
required: []
}
@@ -64,7 +71,14 @@ export class ObjectTools {
description: 'Singular and plural names for the custom object',
properties: {
singular: { type: 'string', description: 'Singular name (e.g., "Pet")' },
- plural: { type: 'string', description: 'Plural name (e.g., "Pets")' }
+ plural: { type: 'string', description: 'Plural name (e.g., "Pets")' },
+ _meta: {
+ labels: {
+ category: "objects",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['singular', 'plural']
},
@@ -112,7 +126,14 @@ export class ObjectTools {
type: 'boolean',
description: 'Whether to fetch all standard/custom fields of the object',
default: true
- }
+ },
+ _meta: {
+ labels: {
+ category: "objects",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['key']
}
@@ -133,7 +154,14 @@ export class ObjectTools {
properties: {
singular: { type: 'string', description: 'Updated singular name' },
plural: { type: 'string', description: 'Updated plural name' }
- }
+ },
+ _meta: {
+ labels: {
+ category: "objects",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
description: {
type: 'string',
@@ -181,7 +209,14 @@ export class ObjectTools {
description: 'Array of user IDs who follow this record (limited to 10)',
items: { type: 'string' },
maxItems: 10
- }
+ },
+ _meta: {
+ labels: {
+ category: "objects",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['schemaKey', 'properties']
}
@@ -199,7 +234,14 @@ export class ObjectTools {
recordId: {
type: 'string',
description: 'ID of the record to retrieve'
- }
+ },
+ _meta: {
+ labels: {
+ category: "objects",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['schemaKey', 'recordId']
}
@@ -237,7 +279,14 @@ export class ObjectTools {
description: 'Updated array of user IDs who follow this record',
items: { type: 'string' },
maxItems: 10
- }
+ },
+ _meta: {
+ labels: {
+ category: "objects",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['schemaKey', 'recordId']
}
@@ -255,7 +304,14 @@ export class ObjectTools {
recordId: {
type: 'string',
description: 'ID of the record to delete'
- }
+ },
+ _meta: {
+ labels: {
+ category: "objects",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['schemaKey', 'recordId']
}
@@ -295,7 +351,14 @@ export class ObjectTools {
type: 'array',
description: 'Cursor for pagination (returned from previous search)',
items: { type: 'string' }
- }
+ },
+ _meta: {
+ labels: {
+ category: "objects",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['schemaKey', 'query']
}
diff --git a/src/tools/opportunity-tools.ts b/src/tools/opportunity-tools.ts
index 5192536..1bb64c3 100644
--- a/src/tools/opportunity-tools.ts
+++ b/src/tools/opportunity-tools.ts
@@ -69,6 +69,13 @@ export class OpportunityTools {
default: 20
}
}
+ },
+ _meta: {
+ labels: {
+ category: "deals",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -77,6 +84,13 @@ export class OpportunityTools {
inputSchema: {
type: 'object',
properties: {}
+ },
+ _meta: {
+ labels: {
+ category: "deals",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -88,7 +102,14 @@ export class OpportunityTools {
opportunityId: {
type: 'string',
description: 'The unique ID of the opportunity to retrieve'
- }
+ },
+ _meta: {
+ labels: {
+ category: "deals",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['opportunityId']
}
@@ -124,7 +145,14 @@ export class OpportunityTools {
assignedTo: {
type: 'string',
description: 'User ID to assign this opportunity to'
- }
+ },
+ _meta: {
+ labels: {
+ category: "deals",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name', 'pipelineId', 'contactId']
}
@@ -143,7 +171,14 @@ export class OpportunityTools {
type: 'string',
description: 'New status for the opportunity',
enum: ['open', 'won', 'lost', 'abandoned']
- }
+ },
+ _meta: {
+ labels: {
+ category: "deals",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['opportunityId', 'status']
}
@@ -157,7 +192,14 @@ export class OpportunityTools {
opportunityId: {
type: 'string',
description: 'The unique ID of the opportunity to delete'
- }
+ },
+ _meta: {
+ labels: {
+ category: "deals",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['opportunityId']
}
@@ -196,7 +238,14 @@ export class OpportunityTools {
assignedTo: {
type: 'string',
description: 'Updated assigned user ID'
- }
+ },
+ _meta: {
+ labels: {
+ category: "deals",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['opportunityId']
}
@@ -236,7 +285,14 @@ export class OpportunityTools {
assignedTo: {
type: 'string',
description: 'User ID to assign this opportunity to'
- }
+ },
+ _meta: {
+ labels: {
+ category: "deals",
+ access: "write",
+ complexity: "complex"
+ }
+ }
},
required: ['pipelineId', 'contactId']
}
@@ -255,7 +311,14 @@ export class OpportunityTools {
type: 'array',
items: { type: 'string' },
description: 'Array of user IDs to add as followers'
- }
+ },
+ _meta: {
+ labels: {
+ category: "deals",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['opportunityId', 'followers']
}
@@ -274,7 +337,14 @@ export class OpportunityTools {
type: 'array',
items: { type: 'string' },
description: 'Array of user IDs to remove as followers'
- }
+ },
+ _meta: {
+ labels: {
+ category: "deals",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['opportunityId', 'followers']
}
diff --git a/src/tools/payments-tools.ts b/src/tools/payments-tools.ts
index bcb8863..400e32e 100644
--- a/src/tools/payments-tools.ts
+++ b/src/tools/payments-tools.ts
@@ -69,7 +69,14 @@ export class PaymentsTools {
imageUrl: {
type: 'string',
description: 'The URL to an image representing the integration provider'
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['altId', 'altType', 'uniqueName', 'title', 'provider', 'description', 'imageUrl']
}
@@ -98,7 +105,14 @@ export class PaymentsTools {
type: 'number',
description: 'Starting index for pagination',
default: 0
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['altId', 'altType']
}
@@ -160,7 +174,14 @@ export class PaymentsTools {
type: 'number',
description: 'Starting index for pagination',
default: 0
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['altId', 'altType']
}
@@ -186,7 +207,14 @@ export class PaymentsTools {
altType: {
type: 'string',
description: 'Alt Type (type of identifier)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['orderId', 'altId', 'altType']
}
@@ -231,7 +259,14 @@ export class PaymentsTools {
description: 'Tracking URL'
}
}
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
items: {
type: 'array',
@@ -277,7 +312,14 @@ export class PaymentsTools {
type: 'string',
enum: ['location'],
description: 'Alt Type'
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['orderId', 'altId', 'altType']
}
@@ -347,7 +389,14 @@ export class PaymentsTools {
type: 'number',
description: 'Starting index for pagination',
default: 0
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['altId', 'altType']
}
@@ -373,7 +422,14 @@ export class PaymentsTools {
altType: {
type: 'string',
description: 'Alt Type (type of identifier)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['transactionId', 'altId', 'altType']
}
@@ -436,7 +492,14 @@ export class PaymentsTools {
type: 'number',
description: 'Starting index for pagination',
default: 0
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['altId', 'altType']
}
@@ -459,7 +522,14 @@ export class PaymentsTools {
type: 'string',
enum: ['location'],
description: 'Alt Type'
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['subscriptionId', 'altId', 'altType']
}
@@ -499,7 +569,14 @@ export class PaymentsTools {
search: {
type: 'string',
description: 'Search term to filter coupons by name or code'
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['altId', 'altType']
}
@@ -553,7 +630,14 @@ export class PaymentsTools {
description: 'Product IDs that the coupon applies to',
items: {
type: 'string'
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
applyToFuturePayments: {
type: 'boolean',
@@ -643,7 +727,14 @@ export class PaymentsTools {
description: 'Product IDs that the coupon applies to',
items: {
type: 'string'
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
applyToFuturePayments: {
type: 'boolean',
@@ -696,7 +787,14 @@ export class PaymentsTools {
id: {
type: 'string',
description: 'Coupon ID'
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['altId', 'altType', 'id']
}
@@ -723,7 +821,14 @@ export class PaymentsTools {
code: {
type: 'string',
description: 'Coupon code'
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['altId', 'altType', 'id', 'code']
}
@@ -759,7 +864,14 @@ export class PaymentsTools {
imageUrl: {
type: 'string',
description: 'Public image URL for the payment gateway logo'
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId', 'name', 'description', 'paymentsUrl', 'queryUrl', 'imageUrl']
}
@@ -773,7 +885,14 @@ export class PaymentsTools {
locationId: {
type: 'string',
description: 'Location ID'
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId']
}
@@ -787,7 +906,14 @@ export class PaymentsTools {
locationId: {
type: 'string',
description: 'Location ID'
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId']
}
@@ -813,7 +939,14 @@ export class PaymentsTools {
publishableKey: {
type: 'string',
description: 'Publishable key for live payments'
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['apiKey', 'publishableKey']
},
@@ -849,7 +982,14 @@ export class PaymentsTools {
liveMode: {
type: 'boolean',
description: 'Whether to disconnect live or test mode config'
- }
+ },
+ _meta: {
+ labels: {
+ category: "payments",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['locationId', 'liveMode']
}
diff --git a/src/tools/phone-tools.ts b/src/tools/phone-tools.ts
index 89fd292..3066782 100644
--- a/src/tools/phone-tools.ts
+++ b/src/tools/phone-tools.ts
@@ -19,6 +19,13 @@ export class PhoneTools {
properties: {
locationId: { type: 'string', description: 'Location ID' }
}
+ },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -28,7 +35,14 @@ export class PhoneTools {
type: 'object',
properties: {
phoneNumberId: { type: 'string', description: 'Phone Number ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['phoneNumberId']
}
@@ -43,7 +57,14 @@ export class PhoneTools {
country: { type: 'string', description: 'Country code (e.g., US, CA)' },
areaCode: { type: 'string', description: 'Area code to search' },
contains: { type: 'string', description: 'Number pattern to search for' },
- type: { type: 'string', enum: ['local', 'tollfree', 'mobile'], description: 'Number type' }
+ type: { type: 'string', enum: ['local', 'tollfree', 'mobile'], description: 'Number type' },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['country']
}
@@ -56,7 +77,14 @@ export class PhoneTools {
properties: {
locationId: { type: 'string', description: 'Location ID' },
phoneNumber: { type: 'string', description: 'Phone number to purchase' },
- name: { type: 'string', description: 'Friendly name for the number' }
+ name: { type: 'string', description: 'Friendly name for the number' },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['phoneNumber']
}
@@ -72,7 +100,14 @@ export class PhoneTools {
name: { type: 'string', description: 'Friendly name' },
forwardingNumber: { type: 'string', description: 'Number to forward calls to' },
callRecording: { type: 'boolean', description: 'Enable call recording' },
- whisperMessage: { type: 'string', description: 'Whisper message played to agent' }
+ whisperMessage: { type: 'string', description: 'Whisper message played to agent' },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['phoneNumberId']
}
@@ -84,7 +119,14 @@ export class PhoneTools {
type: 'object',
properties: {
phoneNumberId: { type: 'string', description: 'Phone Number ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['phoneNumberId']
}
@@ -98,7 +140,14 @@ export class PhoneTools {
type: 'object',
properties: {
phoneNumberId: { type: 'string', description: 'Phone Number ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "write",
+ complexity: "batch"
+ }
+ }
},
required: ['phoneNumberId']
}
@@ -114,7 +163,14 @@ export class PhoneTools {
enabled: { type: 'boolean', description: 'Enable forwarding' },
forwardTo: { type: 'string', description: 'Number to forward to' },
ringTimeout: { type: 'number', description: 'Ring timeout in seconds' },
- voicemailEnabled: { type: 'boolean', description: 'Enable voicemail on no answer' }
+ voicemailEnabled: { type: 'boolean', description: 'Enable voicemail on no answer' },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "write",
+ complexity: "batch"
+ }
+ }
},
required: ['phoneNumberId']
}
@@ -129,6 +185,13 @@ export class PhoneTools {
properties: {
locationId: { type: 'string', description: 'Location ID' }
}
+ },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -148,7 +211,14 @@ export class PhoneTools {
digit: { type: 'string', description: 'Digit to press (0-9, *, #)' },
action: { type: 'string', description: 'Action type' },
destination: { type: 'string', description: 'Action destination' }
- }
+ },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
description: 'Menu options'
}
@@ -166,7 +236,14 @@ export class PhoneTools {
locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Menu name' },
greeting: { type: 'string', description: 'Greeting message' },
- options: { type: 'array', description: 'Menu options' }
+ options: { type: 'array', description: 'Menu options' },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['menuId']
}
@@ -178,7 +255,14 @@ export class PhoneTools {
type: 'object',
properties: {
menuId: { type: 'string', description: 'IVR Menu ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['menuId']
}
@@ -193,6 +277,13 @@ export class PhoneTools {
properties: {
locationId: { type: 'string', description: 'Location ID' }
}
+ },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "write",
+ complexity: "simple"
+ }
}
},
{
@@ -207,6 +298,13 @@ export class PhoneTools {
transcriptionEnabled: { type: 'boolean', description: 'Enable transcription' },
notificationEmail: { type: 'string', description: 'Email for voicemail notifications' }
}
+ },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "write",
+ complexity: "simple"
+ }
}
},
{
@@ -221,6 +319,13 @@ export class PhoneTools {
limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' }
}
+ },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -230,7 +335,14 @@ export class PhoneTools {
type: 'object',
properties: {
voicemailId: { type: 'string', description: 'Voicemail ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['voicemailId']
}
@@ -245,6 +357,13 @@ export class PhoneTools {
properties: {
locationId: { type: 'string', description: 'Location ID' }
}
+ },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -255,7 +374,14 @@ export class PhoneTools {
properties: {
locationId: { type: 'string', description: 'Location ID' },
phoneNumber: { type: 'string', description: 'Phone number to verify' },
- name: { type: 'string', description: 'Friendly name' }
+ name: { type: 'string', description: 'Friendly name' },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['phoneNumber']
}
@@ -268,7 +394,14 @@ export class PhoneTools {
properties: {
callerIdId: { type: 'string', description: 'Caller ID record ID' },
locationId: { type: 'string', description: 'Location ID' },
- code: { type: 'string', description: 'Verification code' }
+ code: { type: 'string', description: 'Verification code' },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['callerIdId', 'code']
}
@@ -280,7 +413,14 @@ export class PhoneTools {
type: 'object',
properties: {
callerIdId: { type: 'string', description: 'Caller ID record ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "phone-numbers",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['callerIdId']
}
diff --git a/src/tools/products-tools.ts b/src/tools/products-tools.ts
index 372819f..9c956c4 100644
--- a/src/tools/products-tools.ts
+++ b/src/tools/products-tools.ts
@@ -184,7 +184,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
description: { type: 'string', description: 'Product description' },
image: { type: 'string', description: 'Product image URL' },
availableInStore: { type: 'boolean', description: 'Whether product is available in store' },
- slug: { type: 'string', description: 'Product URL slug' }
+ slug: { type: 'string', description: 'Product URL slug' },
+ _meta: {
+ labels: {
+ category: "products",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name', 'productType']
}
@@ -201,7 +208,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
search: { type: 'string', description: 'Search term for product names' },
storeId: { type: 'string', description: 'Filter by store ID' },
includedInStore: { type: 'boolean', description: 'Filter by store inclusion status' },
- availableInStore: { type: 'boolean', description: 'Filter by store availability' }
+ availableInStore: { type: 'boolean', description: 'Filter by store availability' },
+ _meta: {
+ labels: {
+ category: "products",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: []
}
@@ -213,7 +227,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
type: 'object',
properties: {
productId: { type: 'string', description: 'Product ID to retrieve' },
- locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }
+ locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
+ _meta: {
+ labels: {
+ category: "products",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['productId']
}
@@ -234,7 +255,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
},
description: { type: 'string', description: 'Product description' },
image: { type: 'string', description: 'Product image URL' },
- availableInStore: { type: 'boolean', description: 'Whether product is available in store' }
+ availableInStore: { type: 'boolean', description: 'Whether product is available in store' },
+ _meta: {
+ labels: {
+ category: "products",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['productId']
}
@@ -246,7 +274,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
type: 'object',
properties: {
productId: { type: 'string', description: 'Product ID to delete' },
- locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }
+ locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
+ _meta: {
+ labels: {
+ category: "products",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['productId']
}
@@ -269,7 +304,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
currency: { type: 'string', description: 'Currency code (e.g., USD)' },
amount: { type: 'number', description: 'Price amount in cents' },
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
- compareAtPrice: { type: 'number', description: 'Compare at price (for discounts)' }
+ compareAtPrice: { type: 'number', description: 'Compare at price (for discounts)' },
+ _meta: {
+ labels: {
+ category: "products",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['productId', 'name', 'type', 'currency', 'amount']
}
@@ -283,7 +325,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
productId: { type: 'string', description: 'Product ID to list prices for' },
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
limit: { type: 'number', description: 'Maximum number of prices to return' },
- offset: { type: 'number', description: 'Number of prices to skip' }
+ offset: { type: 'number', description: 'Number of prices to skip' },
+ _meta: {
+ labels: {
+ category: "products",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['productId']
}
@@ -299,7 +348,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
limit: { type: 'number', description: 'Maximum number of items to return' },
offset: { type: 'number', description: 'Number of items to skip' },
- search: { type: 'string', description: 'Search term for inventory items' }
+ search: { type: 'string', description: 'Search term for inventory items' },
+ _meta: {
+ labels: {
+ category: "products",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: []
}
@@ -322,7 +378,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
title: { type: 'string', description: 'SEO title' },
description: { type: 'string', description: 'SEO description' }
}
- }
+ },
+ _meta: {
+ labels: {
+ category: "products",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name', 'slug']
}
@@ -336,7 +399,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
limit: { type: 'number', description: 'Maximum number of collections to return' },
offset: { type: 'number', description: 'Number of collections to skip' },
- name: { type: 'string', description: 'Search by collection name' }
+ name: { type: 'string', description: 'Search by collection name' },
+ _meta: {
+ labels: {
+ category: "products",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: []
}
diff --git a/src/tools/reporting-tools.ts b/src/tools/reporting-tools.ts
index 22e9f0e..41920f4 100644
--- a/src/tools/reporting-tools.ts
+++ b/src/tools/reporting-tools.ts
@@ -19,7 +19,14 @@ export class ReportingTools {
properties: {
locationId: { type: 'string', description: 'Location ID' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
- endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }
+ endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
+ _meta: {
+ labels: {
+ category: "analytics",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['startDate', 'endDate']
}
@@ -36,7 +43,14 @@ export class ReportingTools {
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
userId: { type: 'string', description: 'Filter by user ID' },
- type: { type: 'string', enum: ['inbound', 'outbound', 'all'], description: 'Call type filter' }
+ type: { type: 'string', enum: ['inbound', 'outbound', 'all'], description: 'Call type filter' },
+ _meta: {
+ labels: {
+ category: "analytics",
+ access: "read",
+ complexity: "batch"
+ }
+ }
},
required: ['startDate', 'endDate']
}
@@ -53,7 +67,14 @@ export class ReportingTools {
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
calendarId: { type: 'string', description: 'Filter by calendar ID' },
- status: { type: 'string', enum: ['booked', 'confirmed', 'showed', 'noshow', 'cancelled'], description: 'Appointment status filter' }
+ status: { type: 'string', enum: ['booked', 'confirmed', 'showed', 'noshow', 'cancelled'], description: 'Appointment status filter' },
+ _meta: {
+ labels: {
+ category: "analytics",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['startDate', 'endDate']
}
@@ -70,7 +91,14 @@ export class ReportingTools {
pipelineId: { type: 'string', description: 'Filter by pipeline ID' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
- userId: { type: 'string', description: 'Filter by assigned user' }
+ userId: { type: 'string', description: 'Filter by assigned user' },
+ _meta: {
+ labels: {
+ category: "analytics",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['startDate', 'endDate']
}
@@ -85,7 +113,14 @@ export class ReportingTools {
properties: {
locationId: { type: 'string', description: 'Location ID' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
- endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }
+ endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
+ _meta: {
+ labels: {
+ category: "analytics",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['startDate', 'endDate']
}
@@ -98,7 +133,14 @@ export class ReportingTools {
properties: {
locationId: { type: 'string', description: 'Location ID' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
- endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }
+ endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
+ _meta: {
+ labels: {
+ category: "analytics",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['startDate', 'endDate']
}
@@ -114,7 +156,14 @@ export class ReportingTools {
locationId: { type: 'string', description: 'Location ID' },
funnelId: { type: 'string', description: 'Filter by funnel ID' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
- endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }
+ endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
+ _meta: {
+ labels: {
+ category: "analytics",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['startDate', 'endDate']
}
@@ -130,7 +179,14 @@ export class ReportingTools {
locationId: { type: 'string', description: 'Location ID' },
platform: { type: 'string', enum: ['google', 'facebook', 'all'], description: 'Ad platform' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
- endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }
+ endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
+ _meta: {
+ labels: {
+ category: "analytics",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['startDate', 'endDate']
}
@@ -146,7 +202,14 @@ export class ReportingTools {
locationId: { type: 'string', description: 'Location ID' },
userId: { type: 'string', description: 'Filter by user ID' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
- endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }
+ endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
+ _meta: {
+ labels: {
+ category: "analytics",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['startDate', 'endDate']
}
@@ -164,6 +227,13 @@ export class ReportingTools {
startDate: { type: 'string', description: 'Start date for custom range' },
endDate: { type: 'string', description: 'End date for custom range' }
}
+ },
+ _meta: {
+ labels: {
+ category: "analytics",
+ access: "read",
+ complexity: "simple"
+ }
}
},
@@ -177,7 +247,14 @@ export class ReportingTools {
locationId: { type: 'string', description: 'Location ID' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
- source: { type: 'string', description: 'Filter by source' }
+ source: { type: 'string', description: 'Filter by source' },
+ _meta: {
+ labels: {
+ category: "analytics",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['startDate', 'endDate']
}
@@ -193,7 +270,14 @@ export class ReportingTools {
locationId: { type: 'string', description: 'Location ID' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
- groupBy: { type: 'string', enum: ['day', 'week', 'month'], description: 'Group results by' }
+ groupBy: { type: 'string', enum: ['day', 'week', 'month'], description: 'Group results by' },
+ _meta: {
+ labels: {
+ category: "analytics",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['startDate', 'endDate']
}
diff --git a/src/tools/reputation-tools.ts b/src/tools/reputation-tools.ts
index f50707d..7e2d823 100644
--- a/src/tools/reputation-tools.ts
+++ b/src/tools/reputation-tools.ts
@@ -26,6 +26,13 @@ export class ReputationTools {
limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' }
}
+ },
+ _meta: {
+ labels: {
+ category: "reputation",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -35,7 +42,14 @@ export class ReputationTools {
type: 'object',
properties: {
reviewId: { type: 'string', description: 'Review ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "reputation",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['reviewId']
}
@@ -48,7 +62,14 @@ export class ReputationTools {
properties: {
reviewId: { type: 'string', description: 'Review ID' },
locationId: { type: 'string', description: 'Location ID' },
- reply: { type: 'string', description: 'Reply text' }
+ reply: { type: 'string', description: 'Reply text' },
+ _meta: {
+ labels: {
+ category: "reputation",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['reviewId', 'reply']
}
@@ -61,7 +82,14 @@ export class ReputationTools {
properties: {
reviewId: { type: 'string', description: 'Review ID' },
locationId: { type: 'string', description: 'Location ID' },
- reply: { type: 'string', description: 'Updated reply text' }
+ reply: { type: 'string', description: 'Updated reply text' },
+ _meta: {
+ labels: {
+ category: "reputation",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['reviewId', 'reply']
}
@@ -73,7 +101,14 @@ export class ReputationTools {
type: 'object',
properties: {
reviewId: { type: 'string', description: 'Review ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "reputation",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['reviewId']
}
@@ -91,6 +126,13 @@ export class ReputationTools {
startDate: { type: 'string', description: 'Start date' },
endDate: { type: 'string', description: 'End date' }
}
+ },
+ _meta: {
+ labels: {
+ category: "reputation",
+ access: "read",
+ complexity: "simple"
+ }
}
},
@@ -105,7 +147,14 @@ export class ReputationTools {
contactId: { type: 'string', description: 'Contact ID to request review from' },
platform: { type: 'string', enum: ['google', 'facebook', 'yelp'], description: 'Platform to request review on' },
method: { type: 'string', enum: ['sms', 'email', 'both'], description: 'Delivery method' },
- message: { type: 'string', description: 'Custom message (optional)' }
+ message: { type: 'string', description: 'Custom message (optional)' },
+ _meta: {
+ labels: {
+ category: "reputation",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['contactId', 'platform', 'method']
}
@@ -122,6 +171,13 @@ export class ReputationTools {
limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' }
}
+ },
+ _meta: {
+ labels: {
+ category: "reputation",
+ access: "read",
+ complexity: "simple"
+ }
}
},
@@ -134,6 +190,13 @@ export class ReputationTools {
properties: {
locationId: { type: 'string', description: 'Location ID' }
}
+ },
+ _meta: {
+ labels: {
+ category: "reputation",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -144,6 +207,13 @@ export class ReputationTools {
properties: {
locationId: { type: 'string', description: 'Location ID' }
}
+ },
+ _meta: {
+ labels: {
+ category: "reputation",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -153,7 +223,14 @@ export class ReputationTools {
type: 'object',
properties: {
locationId: { type: 'string', description: 'Location ID' },
- platform: { type: 'string', enum: ['google', 'facebook', 'yelp'], description: 'Platform to disconnect' }
+ platform: { type: 'string', enum: ['google', 'facebook', 'yelp'], description: 'Platform to disconnect' },
+ _meta: {
+ labels: {
+ category: "reputation",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['platform']
}
@@ -168,6 +245,13 @@ export class ReputationTools {
properties: {
locationId: { type: 'string', description: 'Location ID' }
}
+ },
+ _meta: {
+ labels: {
+ category: "reputation",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -181,6 +265,13 @@ export class ReputationTools {
facebookLink: { type: 'string', description: 'Custom Facebook review link' },
yelpLink: { type: 'string', description: 'Custom Yelp review link' }
}
+ },
+ _meta: {
+ labels: {
+ category: "reputation",
+ access: "write",
+ complexity: "simple"
+ }
}
},
@@ -193,6 +284,13 @@ export class ReputationTools {
properties: {
locationId: { type: 'string', description: 'Location ID' }
}
+ },
+ _meta: {
+ labels: {
+ category: "reputation",
+ access: "write",
+ complexity: "simple"
+ }
}
},
{
diff --git a/src/tools/saas-tools.ts b/src/tools/saas-tools.ts
index 899b553..b421371 100644
--- a/src/tools/saas-tools.ts
+++ b/src/tools/saas-tools.ts
@@ -36,7 +36,14 @@ export class SaasTools {
isActive: {
type: 'boolean',
description: 'Filter by active status'
- }
+ },
+ _meta: {
+ labels: {
+ category: "saas",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['companyId']
}
@@ -54,7 +61,14 @@ export class SaasTools {
locationId: {
type: 'string',
description: 'Location ID to retrieve'
- }
+ },
+ _meta: {
+ labels: {
+ category: "saas",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['companyId', 'locationId']
}
@@ -81,7 +95,14 @@ export class SaasTools {
type: 'string',
enum: ['active', 'paused', 'cancelled'],
description: 'Subscription status'
- }
+ },
+ _meta: {
+ labels: {
+ category: "saas",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['companyId', 'locationId']
}
@@ -103,7 +124,14 @@ export class SaasTools {
paused: {
type: 'boolean',
description: 'Whether to pause (true) or unpause (false)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "saas",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['companyId', 'locationId', 'paused']
}
@@ -125,7 +153,14 @@ export class SaasTools {
enabled: {
type: 'boolean',
description: 'Whether to enable (true) or disable (false) SaaS'
- }
+ },
+ _meta: {
+ labels: {
+ category: "saas",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['companyId', 'locationId', 'enabled']
}
@@ -151,7 +186,14 @@ export class SaasTools {
enabled: {
type: 'boolean',
description: 'Whether rebilling is enabled'
- }
+ },
+ _meta: {
+ labels: {
+ category: "saas",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['companyId']
}
diff --git a/src/tools/smartlists-tools.ts b/src/tools/smartlists-tools.ts
index 3ab8dad..65038a5 100644
--- a/src/tools/smartlists-tools.ts
+++ b/src/tools/smartlists-tools.ts
@@ -20,6 +20,13 @@ export class SmartListsTools {
limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' }
}
+ },
+ _meta: {
+ labels: {
+ category: "smartlists",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -29,7 +36,14 @@ export class SmartListsTools {
type: 'object',
properties: {
smartListId: { type: 'string', description: 'Smart List ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "smartlists",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['smartListId']
}
@@ -50,7 +64,14 @@ export class SmartListsTools {
field: { type: 'string', description: 'Field to filter on' },
operator: { type: 'string', description: 'Comparison operator (equals, contains, etc.)' },
value: { type: 'string', description: 'Filter value' }
- }
+ },
+ _meta: {
+ labels: {
+ category: "smartlists",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
description: 'Filter conditions'
},
@@ -69,7 +90,14 @@ export class SmartListsTools {
locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Smart list name' },
filters: { type: 'array', description: 'Filter conditions' },
- filterOperator: { type: 'string', enum: ['AND', 'OR'], description: 'How to combine filters' }
+ filterOperator: { type: 'string', enum: ['AND', 'OR'], description: 'How to combine filters' },
+ _meta: {
+ labels: {
+ category: "smartlists",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['smartListId']
}
@@ -81,7 +109,14 @@ export class SmartListsTools {
type: 'object',
properties: {
smartListId: { type: 'string', description: 'Smart List ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "smartlists",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['smartListId']
}
@@ -95,7 +130,14 @@ export class SmartListsTools {
smartListId: { type: 'string', description: 'Smart List ID' },
locationId: { type: 'string', description: 'Location ID' },
limit: { type: 'number', description: 'Max results' },
- offset: { type: 'number', description: 'Pagination offset' }
+ offset: { type: 'number', description: 'Pagination offset' },
+ _meta: {
+ labels: {
+ category: "smartlists",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['smartListId']
}
@@ -107,7 +149,14 @@ export class SmartListsTools {
type: 'object',
properties: {
smartListId: { type: 'string', description: 'Smart List ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "smartlists",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['smartListId']
}
@@ -120,7 +169,14 @@ export class SmartListsTools {
properties: {
smartListId: { type: 'string', description: 'Smart List ID to duplicate' },
locationId: { type: 'string', description: 'Location ID' },
- name: { type: 'string', description: 'Name for the duplicate' }
+ name: { type: 'string', description: 'Name for the duplicate' },
+ _meta: {
+ labels: {
+ category: "smartlists",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['smartListId']
}
diff --git a/src/tools/snapshots-tools.ts b/src/tools/snapshots-tools.ts
index afba7e1..472eff7 100644
--- a/src/tools/snapshots-tools.ts
+++ b/src/tools/snapshots-tools.ts
@@ -27,7 +27,14 @@ export class SnapshotsTools {
limit: {
type: 'number',
description: 'Maximum number of snapshots to return'
- }
+ },
+ _meta: {
+ labels: {
+ category: "snapshots",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['companyId']
}
@@ -45,7 +52,14 @@ export class SnapshotsTools {
companyId: {
type: 'string',
description: 'Company/Agency ID'
- }
+ },
+ _meta: {
+ labels: {
+ category: "snapshots",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['snapshotId', 'companyId']
}
@@ -71,7 +85,14 @@ export class SnapshotsTools {
description: {
type: 'string',
description: 'Description of the snapshot'
- }
+ },
+ _meta: {
+ labels: {
+ category: "snapshots",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['companyId', 'locationId', 'name']
}
@@ -93,7 +114,14 @@ export class SnapshotsTools {
pushId: {
type: 'string',
description: 'The push operation ID'
- }
+ },
+ _meta: {
+ labels: {
+ category: "snapshots",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['snapshotId', 'companyId']
}
@@ -115,7 +143,14 @@ export class SnapshotsTools {
locationId: {
type: 'string',
description: 'Target location ID'
- }
+ },
+ _meta: {
+ labels: {
+ category: "snapshots",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['snapshotId', 'companyId', 'locationId']
}
@@ -150,7 +185,14 @@ export class SnapshotsTools {
surveys: { type: 'boolean', description: 'Override existing surveys' },
calendars: { type: 'boolean', description: 'Override existing calendars' },
automations: { type: 'boolean', description: 'Override existing automations' },
- triggers: { type: 'boolean', description: 'Override existing triggers' }
+ triggers: { type: 'boolean', description: 'Override existing triggers' },
+ _meta: {
+ labels: {
+ category: "snapshots",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
description: 'What to override vs skip'
}
diff --git a/src/tools/social-media-tools.ts b/src/tools/social-media-tools.ts
index 5623a45..690bffa 100644
--- a/src/tools/social-media-tools.ts
+++ b/src/tools/social-media-tools.ts
@@ -56,7 +56,14 @@ export class SocialMediaTools {
type: 'string',
enum: ['post', 'story', 'reel'],
description: 'Type of post to search for'
- }
+ },
+ _meta: {
+ labels: {
+ category: "social-media",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['fromDate', 'toDate']
}
@@ -80,7 +87,14 @@ export class SocialMediaTools {
properties: {
url: { type: 'string', description: 'Media URL' },
caption: { type: 'string', description: 'Media caption' },
- type: { type: 'string', description: 'Media MIME type' }
+ type: { type: 'string', description: 'Media MIME type' },
+ _meta: {
+ labels: {
+ category: "social-media",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['url']
},
@@ -116,7 +130,14 @@ export class SocialMediaTools {
inputSchema: {
type: 'object',
properties: {
- postId: { type: 'string', description: 'Social media post ID' }
+ postId: { type: 'string', description: 'Social media post ID' },
+ _meta: {
+ labels: {
+ category: "social-media",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['postId']
}
@@ -139,7 +160,14 @@ export class SocialMediaTools {
type: 'array',
items: { type: 'string' },
description: 'Updated tag IDs'
- }
+ },
+ _meta: {
+ labels: {
+ category: "social-media",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['postId']
}
@@ -150,7 +178,14 @@ export class SocialMediaTools {
inputSchema: {
type: 'object',
properties: {
- postId: { type: 'string', description: 'Social media post ID to delete' }
+ postId: { type: 'string', description: 'Social media post ID to delete' },
+ _meta: {
+ labels: {
+ category: "social-media",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['postId']
}
@@ -166,7 +201,14 @@ export class SocialMediaTools {
items: { type: 'string' },
description: 'Array of post IDs to delete',
maxItems: 50
- }
+ },
+ _meta: {
+ labels: {
+ category: "social-media",
+ access: "delete",
+ complexity: "batch"
+ }
+ }
},
required: ['postIds']
}
@@ -180,6 +222,13 @@ export class SocialMediaTools {
type: 'object',
properties: {},
additionalProperties: false
+ },
+ _meta: {
+ labels: {
+ category: "social-media",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -190,7 +239,14 @@ export class SocialMediaTools {
properties: {
accountId: { type: 'string', description: 'Account ID to delete' },
companyId: { type: 'string', description: 'Company ID' },
- userId: { type: 'string', description: 'User ID' }
+ userId: { type: 'string', description: 'User ID' },
+ _meta: {
+ labels: {
+ category: "social-media",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['accountId']
}
@@ -203,7 +259,14 @@ export class SocialMediaTools {
inputSchema: {
type: 'object',
properties: {
- file: { type: 'string', description: 'CSV file data (base64 or file path)' }
+ file: { type: 'string', description: 'CSV file data (base64 or file path)' },
+ _meta: {
+ labels: {
+ category: "social-media",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['file']
}
@@ -219,6 +282,13 @@ export class SocialMediaTools {
includeUsers: { type: 'boolean', description: 'Include user data' },
userId: { type: 'string', description: 'Filter by user ID' }
}
+ },
+ _meta: {
+ labels: {
+ category: "social-media",
+ access: "write",
+ complexity: "simple"
+ }
}
},
{
@@ -236,7 +306,14 @@ export class SocialMediaTools {
rowsCount: { type: 'number', description: 'Number of rows to process' },
fileName: { type: 'string', description: 'CSV file name' },
approver: { type: 'string', description: 'Approver user ID' },
- userId: { type: 'string', description: 'User ID' }
+ userId: { type: 'string', description: 'User ID' },
+ _meta: {
+ labels: {
+ category: "social-media",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['accountIds', 'filePath', 'rowsCount', 'fileName']
}
@@ -253,6 +330,13 @@ export class SocialMediaTools {
limit: { type: 'number', description: 'Number to return', default: 10 },
skip: { type: 'number', description: 'Number to skip', default: 0 }
}
+ },
+ _meta: {
+ labels: {
+ category: "social-media",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -261,7 +345,14 @@ export class SocialMediaTools {
inputSchema: {
type: 'object',
properties: {
- categoryId: { type: 'string', description: 'Category ID' }
+ categoryId: { type: 'string', description: 'Category ID' },
+ _meta: {
+ labels: {
+ category: "social-media",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['categoryId']
}
@@ -276,6 +367,13 @@ export class SocialMediaTools {
limit: { type: 'number', description: 'Number to return', default: 10 },
skip: { type: 'number', description: 'Number to skip', default: 0 }
}
+ },
+ _meta: {
+ labels: {
+ category: "social-media",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -288,7 +386,14 @@ export class SocialMediaTools {
type: 'array',
items: { type: 'string' },
description: 'Array of tag IDs'
- }
+ },
+ _meta: {
+ labels: {
+ category: "social-media",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['tagIds']
}
@@ -308,7 +413,14 @@ export class SocialMediaTools {
},
userId: { type: 'string', description: 'User ID initiating OAuth' },
page: { type: 'string', description: 'Page context' },
- reconnect: { type: 'boolean', description: 'Whether this is a reconnection' }
+ reconnect: { type: 'boolean', description: 'Whether this is a reconnection' },
+ _meta: {
+ labels: {
+ category: "social-media",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['platform', 'userId']
}
@@ -324,7 +436,14 @@ export class SocialMediaTools {
enum: ['google', 'facebook', 'instagram', 'linkedin', 'twitter', 'tiktok', 'tiktok-business'],
description: 'Social media platform'
},
- accountId: { type: 'string', description: 'OAuth account ID' }
+ accountId: { type: 'string', description: 'OAuth account ID' },
+ _meta: {
+ labels: {
+ category: "social-media",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['platform', 'accountId']
}
diff --git a/src/tools/store-tools.ts b/src/tools/store-tools.ts
index f073b29..46f6ddd 100644
--- a/src/tools/store-tools.ts
+++ b/src/tools/store-tools.ts
@@ -1068,7 +1068,14 @@ These settings control your store's shipping origin and email notification prefe
items: {
type: 'object',
properties: {
- code: { type: 'string', description: 'State code (e.g., CA, NY)' }
+ code: { type: 'string', description: 'State code (e.g., CA, NY)' },
+ _meta: {
+ labels: {
+ category: "stores",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['code']
}
@@ -1092,6 +1099,13 @@ These settings control your store's shipping origin and email notification prefe
offset: { type: 'number', description: 'Number of zones to skip (optional)' },
withShippingRate: { type: 'boolean', description: 'Include shipping rates in response (optional)' }
}
+ },
+ _meta: {
+ labels: {
+ category: "stores",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -1102,7 +1116,14 @@ These settings control your store's shipping origin and email notification prefe
properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
shippingZoneId: { type: 'string', description: 'ID of the shipping zone to retrieve' },
- withShippingRate: { type: 'boolean', description: 'Include shipping rates in response (optional)' }
+ withShippingRate: { type: 'boolean', description: 'Include shipping rates in response (optional)' },
+ _meta: {
+ labels: {
+ category: "stores",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['shippingZoneId']
}
@@ -1129,7 +1150,14 @@ These settings control your store's shipping origin and email notification prefe
items: {
type: 'object',
properties: {
- code: { type: 'string', description: 'State code (e.g., CA, NY)' }
+ code: { type: 'string', description: 'State code (e.g., CA, NY)' },
+ _meta: {
+ labels: {
+ category: "stores",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['code']
}
@@ -1149,7 +1177,14 @@ These settings control your store's shipping origin and email notification prefe
type: 'object',
properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
- shippingZoneId: { type: 'string', description: 'ID of the shipping zone to delete' }
+ shippingZoneId: { type: 'string', description: 'ID of the shipping zone to delete' },
+ _meta: {
+ labels: {
+ category: "stores",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['shippingZoneId']
}
@@ -1170,7 +1205,14 @@ These settings control your store's shipping origin and email notification prefe
properties: {
street1: { type: 'string', description: 'Street address line 1' },
city: { type: 'string', description: 'City' },
- country: { type: 'string', description: 'Country code' }
+ country: { type: 'string', description: 'Country code' },
+ _meta: {
+ labels: {
+ category: "stores",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['street1', 'city', 'country']
},
@@ -1203,7 +1245,14 @@ These settings control your store's shipping origin and email notification prefe
name: { type: 'string', description: 'Name of the shipping rate' },
currency: { type: 'string', description: 'Currency code (e.g., USD)' },
amount: { type: 'number', description: 'Shipping rate amount' },
- conditionType: { type: 'string', description: 'Condition type for rate calculation' }
+ conditionType: { type: 'string', description: 'Condition type for rate calculation' },
+ _meta: {
+ labels: {
+ category: "stores",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['shippingZoneId', 'name', 'currency', 'amount', 'conditionType']
}
@@ -1215,7 +1264,14 @@ These settings control your store's shipping origin and email notification prefe
type: 'object',
properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
- shippingZoneId: { type: 'string', description: 'ID of the shipping zone' }
+ shippingZoneId: { type: 'string', description: 'ID of the shipping zone' },
+ _meta: {
+ labels: {
+ category: "stores",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['shippingZoneId']
}
@@ -1228,7 +1284,14 @@ These settings control your store's shipping origin and email notification prefe
properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
shippingZoneId: { type: 'string', description: 'ID of the shipping zone' },
- shippingRateId: { type: 'string', description: 'ID of the shipping rate to retrieve' }
+ shippingRateId: { type: 'string', description: 'ID of the shipping rate to retrieve' },
+ _meta: {
+ labels: {
+ category: "stores",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['shippingZoneId', 'shippingRateId']
}
@@ -1241,7 +1304,14 @@ These settings control your store's shipping origin and email notification prefe
properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
shippingZoneId: { type: 'string', description: 'ID of the shipping zone' },
- shippingRateId: { type: 'string', description: 'ID of the shipping rate to update' }
+ shippingRateId: { type: 'string', description: 'ID of the shipping rate to update' },
+ _meta: {
+ labels: {
+ category: "stores",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['shippingZoneId', 'shippingRateId']
}
@@ -1254,7 +1324,14 @@ These settings control your store's shipping origin and email notification prefe
properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
shippingZoneId: { type: 'string', description: 'ID of the shipping zone' },
- shippingRateId: { type: 'string', description: 'ID of the shipping rate to delete' }
+ shippingRateId: { type: 'string', description: 'ID of the shipping rate to delete' },
+ _meta: {
+ labels: {
+ category: "stores",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['shippingZoneId', 'shippingRateId']
}
@@ -1277,7 +1354,14 @@ These settings control your store's shipping origin and email notification prefe
type: 'object',
properties: {
name: { type: 'string', description: 'Service name' },
- value: { type: 'string', description: 'Service value' }
+ value: { type: 'string', description: 'Service value' },
+ _meta: {
+ labels: {
+ category: "stores",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name', 'value']
}
@@ -1294,6 +1378,13 @@ These settings control your store's shipping origin and email notification prefe
properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }
}
+ },
+ _meta: {
+ labels: {
+ category: "stores",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -1303,7 +1394,14 @@ These settings control your store's shipping origin and email notification prefe
type: 'object',
properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
- shippingCarrierId: { type: 'string', description: 'ID of the shipping carrier to retrieve' }
+ shippingCarrierId: { type: 'string', description: 'ID of the shipping carrier to retrieve' },
+ _meta: {
+ labels: {
+ category: "stores",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['shippingCarrierId']
}
@@ -1315,7 +1413,14 @@ These settings control your store's shipping origin and email notification prefe
type: 'object',
properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
- shippingCarrierId: { type: 'string', description: 'ID of the shipping carrier to update' }
+ shippingCarrierId: { type: 'string', description: 'ID of the shipping carrier to update' },
+ _meta: {
+ labels: {
+ category: "stores",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['shippingCarrierId']
}
@@ -1327,7 +1432,14 @@ These settings control your store's shipping origin and email notification prefe
type: 'object',
properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
- shippingCarrierId: { type: 'string', description: 'ID of the shipping carrier to delete' }
+ shippingCarrierId: { type: 'string', description: 'ID of the shipping carrier to delete' },
+ _meta: {
+ labels: {
+ category: "stores",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['shippingCarrierId']
}
@@ -1349,7 +1461,14 @@ These settings control your store's shipping origin and email notification prefe
street1: { type: 'string', description: 'Street address line 1' },
city: { type: 'string', description: 'City' },
zip: { type: 'string', description: 'Postal/ZIP code' },
- country: { type: 'string', description: 'Country code' }
+ country: { type: 'string', description: 'Country code' },
+ _meta: {
+ labels: {
+ category: "stores",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name', 'street1', 'city', 'zip', 'country']
}
diff --git a/src/tools/survey-tools.ts b/src/tools/survey-tools.ts
index c0b4f78..9e25bd1 100644
--- a/src/tools/survey-tools.ts
+++ b/src/tools/survey-tools.ts
@@ -31,7 +31,14 @@ export class SurveyTools {
type: {
type: 'string',
description: 'Filter surveys by type (e.g., "folder")'
- }
+ },
+ _meta: {
+ labels: {
+ category: "surveys",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
additionalProperties: false
}
@@ -69,7 +76,14 @@ export class SurveyTools {
endAt: {
type: 'string',
description: 'End date for filtering submissions (YYYY-MM-DD format)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "surveys",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
additionalProperties: false
}
diff --git a/src/tools/templates-tools.ts b/src/tools/templates-tools.ts
index 0c460ed..235a4b2 100644
--- a/src/tools/templates-tools.ts
+++ b/src/tools/templates-tools.ts
@@ -21,6 +21,13 @@ export class TemplatesTools {
limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' }
}
+ },
+ _meta: {
+ labels: {
+ category: "templates",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -30,7 +37,14 @@ export class TemplatesTools {
type: 'object',
properties: {
templateId: { type: 'string', description: 'SMS Template ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "templates",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['templateId']
}
@@ -43,7 +57,14 @@ export class TemplatesTools {
properties: {
locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Template name' },
- body: { type: 'string', description: 'SMS message body (can include merge fields like {{contact.first_name}})' }
+ body: { type: 'string', description: 'SMS message body (can include merge fields like {{contact.first_name}})' },
+ _meta: {
+ labels: {
+ category: "templates",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name', 'body']
}
@@ -57,7 +78,14 @@ export class TemplatesTools {
templateId: { type: 'string', description: 'SMS Template ID' },
locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Template name' },
- body: { type: 'string', description: 'SMS message body' }
+ body: { type: 'string', description: 'SMS message body' },
+ _meta: {
+ labels: {
+ category: "templates",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['templateId']
}
@@ -69,7 +97,14 @@ export class TemplatesTools {
type: 'object',
properties: {
templateId: { type: 'string', description: 'SMS Template ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "templates",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['templateId']
}
@@ -84,6 +119,13 @@ export class TemplatesTools {
properties: {
locationId: { type: 'string', description: 'Location ID' }
}
+ },
+ _meta: {
+ labels: {
+ category: "templates",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -94,7 +136,14 @@ export class TemplatesTools {
properties: {
locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Template name' },
- audioUrl: { type: 'string', description: 'URL to audio file' }
+ audioUrl: { type: 'string', description: 'URL to audio file' },
+ _meta: {
+ labels: {
+ category: "templates",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name', 'audioUrl']
}
@@ -106,7 +155,14 @@ export class TemplatesTools {
type: 'object',
properties: {
templateId: { type: 'string', description: 'Template ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "templates",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['templateId']
}
@@ -123,6 +179,13 @@ export class TemplatesTools {
limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' }
}
+ },
+ _meta: {
+ labels: {
+ category: "templates",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -135,7 +198,14 @@ export class TemplatesTools {
name: { type: 'string', description: 'Template name' },
content: { type: 'string', description: 'Post content' },
mediaUrls: { type: 'array', items: { type: 'string' }, description: 'Media URLs' },
- platforms: { type: 'array', items: { type: 'string' }, description: 'Target platforms' }
+ platforms: { type: 'array', items: { type: 'string' }, description: 'Target platforms' },
+ _meta: {
+ labels: {
+ category: "templates",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name', 'content']
}
@@ -147,7 +217,14 @@ export class TemplatesTools {
type: 'object',
properties: {
templateId: { type: 'string', description: 'Template ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "templates",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['templateId']
}
@@ -163,6 +240,13 @@ export class TemplatesTools {
locationId: { type: 'string', description: 'Location ID' },
status: { type: 'string', enum: ['approved', 'pending', 'rejected', 'all'], description: 'Template status' }
}
+ },
+ _meta: {
+ labels: {
+ category: "templates",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -175,7 +259,14 @@ export class TemplatesTools {
name: { type: 'string', description: 'Template name' },
category: { type: 'string', enum: ['marketing', 'utility', 'authentication'], description: 'Template category' },
language: { type: 'string', description: 'Language code (e.g., en_US)' },
- components: { type: 'array', description: 'Template components (header, body, footer, buttons)' }
+ components: { type: 'array', description: 'Template components (header, body, footer, buttons)' },
+ _meta: {
+ labels: {
+ category: "templates",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name', 'category', 'language', 'components']
}
@@ -187,7 +278,14 @@ export class TemplatesTools {
type: 'object',
properties: {
templateId: { type: 'string', description: 'Template ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "templates",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['templateId']
}
@@ -203,6 +301,13 @@ export class TemplatesTools {
locationId: { type: 'string', description: 'Location ID' },
type: { type: 'string', enum: ['sms', 'email', 'all'], description: 'Snippet type' }
}
+ },
+ _meta: {
+ labels: {
+ category: "templates",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -215,7 +320,14 @@ export class TemplatesTools {
name: { type: 'string', description: 'Snippet name' },
shortcut: { type: 'string', description: 'Keyboard shortcut (e.g., /thanks)' },
content: { type: 'string', description: 'Snippet content' },
- type: { type: 'string', enum: ['sms', 'email', 'both'], description: 'Snippet type' }
+ type: { type: 'string', enum: ['sms', 'email', 'both'], description: 'Snippet type' },
+ _meta: {
+ labels: {
+ category: "templates",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name', 'content']
}
@@ -230,7 +342,14 @@ export class TemplatesTools {
locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Snippet name' },
shortcut: { type: 'string', description: 'Keyboard shortcut' },
- content: { type: 'string', description: 'Snippet content' }
+ content: { type: 'string', description: 'Snippet content' },
+ _meta: {
+ labels: {
+ category: "templates",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['snippetId']
}
@@ -242,7 +361,14 @@ export class TemplatesTools {
type: 'object',
properties: {
snippetId: { type: 'string', description: 'Snippet ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "templates",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['snippetId']
}
diff --git a/src/tools/triggers-tools.ts b/src/tools/triggers-tools.ts
index 442d0f5..168eee4 100644
--- a/src/tools/triggers-tools.ts
+++ b/src/tools/triggers-tools.ts
@@ -22,6 +22,13 @@ export class TriggersTools {
limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' }
}
+ },
+ _meta: {
+ labels: {
+ category: "triggers",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -31,7 +38,14 @@ export class TriggersTools {
type: 'object',
properties: {
triggerId: { type: 'string', description: 'Trigger ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "triggers",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['triggerId']
}
@@ -63,7 +77,14 @@ export class TriggersTools {
field: { type: 'string', description: 'Field to filter' },
operator: { type: 'string', description: 'Comparison operator' },
value: { type: 'string', description: 'Filter value' }
- }
+ },
+ _meta: {
+ labels: {
+ category: "triggers",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
description: 'Conditions that must be met'
},
@@ -93,7 +114,14 @@ export class TriggersTools {
name: { type: 'string', description: 'Trigger name' },
filters: { type: 'array', description: 'Filter conditions' },
actions: { type: 'array', description: 'Actions to perform' },
- status: { type: 'string', enum: ['active', 'inactive'], description: 'Trigger status' }
+ status: { type: 'string', enum: ['active', 'inactive'], description: 'Trigger status' },
+ _meta: {
+ labels: {
+ category: "triggers",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['triggerId']
}
@@ -105,7 +133,14 @@ export class TriggersTools {
type: 'object',
properties: {
triggerId: { type: 'string', description: 'Trigger ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "triggers",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['triggerId']
}
@@ -117,7 +152,14 @@ export class TriggersTools {
type: 'object',
properties: {
triggerId: { type: 'string', description: 'Trigger ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "triggers",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['triggerId']
}
@@ -129,7 +171,14 @@ export class TriggersTools {
type: 'object',
properties: {
triggerId: { type: 'string', description: 'Trigger ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "triggers",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['triggerId']
}
@@ -142,6 +191,13 @@ export class TriggersTools {
properties: {
locationId: { type: 'string', description: 'Location ID' }
}
+ },
+ _meta: {
+ labels: {
+ category: "triggers",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -156,7 +212,14 @@ export class TriggersTools {
startDate: { type: 'string', description: 'Start date' },
endDate: { type: 'string', description: 'End date' },
limit: { type: 'number', description: 'Max results' },
- offset: { type: 'number', description: 'Pagination offset' }
+ offset: { type: 'number', description: 'Pagination offset' },
+ _meta: {
+ labels: {
+ category: "triggers",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['triggerId']
}
@@ -169,7 +232,14 @@ export class TriggersTools {
properties: {
triggerId: { type: 'string', description: 'Trigger ID' },
locationId: { type: 'string', description: 'Location ID' },
- testData: { type: 'object', description: 'Sample data to test with' }
+ testData: { type: 'object', description: 'Sample data to test with' },
+ _meta: {
+ labels: {
+ category: "triggers",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['triggerId']
}
@@ -182,7 +252,14 @@ export class TriggersTools {
properties: {
triggerId: { type: 'string', description: 'Trigger ID to duplicate' },
locationId: { type: 'string', description: 'Location ID' },
- name: { type: 'string', description: 'Name for the duplicate' }
+ name: { type: 'string', description: 'Name for the duplicate' },
+ _meta: {
+ labels: {
+ category: "triggers",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['triggerId']
}
diff --git a/src/tools/users-tools.ts b/src/tools/users-tools.ts
index cd008a7..46a61b7 100644
--- a/src/tools/users-tools.ts
+++ b/src/tools/users-tools.ts
@@ -50,6 +50,13 @@ export class UsersTools {
description: 'Sort direction'
}
}
+ },
+ _meta: {
+ labels: {
+ category: "users",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -65,7 +72,14 @@ export class UsersTools {
locationId: {
type: 'string',
description: 'Location ID (uses default if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "users",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['userId']
}
@@ -117,7 +131,14 @@ export class UsersTools {
type: 'array',
items: { type: 'string' },
description: 'Scopes only assigned to this user'
- }
+ },
+ _meta: {
+ labels: {
+ category: "users",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['firstName', 'lastName', 'email']
}
@@ -163,7 +184,14 @@ export class UsersTools {
permissions: {
type: 'object',
description: 'User permissions object'
- }
+ },
+ _meta: {
+ labels: {
+ category: "users",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['userId']
}
@@ -181,7 +209,14 @@ export class UsersTools {
locationId: {
type: 'string',
description: 'Location ID (uses default if not provided)'
- }
+ },
+ _meta: {
+ labels: {
+ category: "users",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['userId']
}
diff --git a/src/tools/webhooks-tools.ts b/src/tools/webhooks-tools.ts
index bc3d39e..f6e3c33 100644
--- a/src/tools/webhooks-tools.ts
+++ b/src/tools/webhooks-tools.ts
@@ -18,6 +18,13 @@ export class WebhooksTools {
properties: {
locationId: { type: 'string', description: 'Location ID' }
}
+ },
+ _meta: {
+ labels: {
+ category: "webhooks",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -27,7 +34,14 @@ export class WebhooksTools {
type: 'object',
properties: {
webhookId: { type: 'string', description: 'Webhook ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "webhooks",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['webhookId']
}
@@ -46,7 +60,14 @@ export class WebhooksTools {
items: { type: 'string' },
description: 'Events to subscribe to (e.g., contact.created, opportunity.updated)'
},
- secret: { type: 'string', description: 'Secret key for webhook signature verification' }
+ secret: { type: 'string', description: 'Secret key for webhook signature verification' },
+ _meta: {
+ labels: {
+ category: "webhooks",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['name', 'url', 'events']
}
@@ -66,7 +87,14 @@ export class WebhooksTools {
items: { type: 'string' },
description: 'Events to subscribe to'
},
- active: { type: 'boolean', description: 'Whether webhook is active' }
+ active: { type: 'boolean', description: 'Whether webhook is active' },
+ _meta: {
+ labels: {
+ category: "webhooks",
+ access: "write",
+ complexity: "simple"
+ }
+ }
},
required: ['webhookId']
}
@@ -78,7 +106,14 @@ export class WebhooksTools {
type: 'object',
properties: {
webhookId: { type: 'string', description: 'Webhook ID' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "webhooks",
+ access: "delete",
+ complexity: "simple"
+ }
+ }
},
required: ['webhookId']
}
@@ -89,6 +124,13 @@ export class WebhooksTools {
inputSchema: {
type: 'object',
properties: {}
+ },
+ _meta: {
+ labels: {
+ category: "webhooks",
+ access: "read",
+ complexity: "simple"
+ }
}
},
{
@@ -101,7 +143,14 @@ export class WebhooksTools {
locationId: { type: 'string', description: 'Location ID' },
limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' },
- status: { type: 'string', enum: ['success', 'failed', 'pending'], description: 'Filter by delivery status' }
+ status: { type: 'string', enum: ['success', 'failed', 'pending'], description: 'Filter by delivery status' },
+ _meta: {
+ labels: {
+ category: "webhooks",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['webhookId']
}
@@ -114,7 +163,14 @@ export class WebhooksTools {
properties: {
webhookId: { type: 'string', description: 'Webhook ID' },
logId: { type: 'string', description: 'Webhook log entry ID to retry' },
- locationId: { type: 'string', description: 'Location ID' }
+ locationId: { type: 'string', description: 'Location ID' },
+ _meta: {
+ labels: {
+ category: "webhooks",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['webhookId', 'logId']
}
@@ -127,7 +183,14 @@ export class WebhooksTools {
properties: {
webhookId: { type: 'string', description: 'Webhook ID' },
locationId: { type: 'string', description: 'Location ID' },
- eventType: { type: 'string', description: 'Event type to test' }
+ eventType: { type: 'string', description: 'Event type to test' },
+ _meta: {
+ labels: {
+ category: "webhooks",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
required: ['webhookId', 'eventType']
}
diff --git a/src/tools/workflow-tools.ts b/src/tools/workflow-tools.ts
index 9305585..89b82ab 100644
--- a/src/tools/workflow-tools.ts
+++ b/src/tools/workflow-tools.ts
@@ -18,7 +18,14 @@ export class WorkflowTools {
locationId: {
type: 'string',
description: 'The location ID to get workflows for. If not provided, uses the default location from configuration.'
- }
+ },
+ _meta: {
+ labels: {
+ category: "workflows",
+ access: "read",
+ complexity: "simple"
+ }
+ }
},
additionalProperties: false
}
diff --git a/src/ui/json-render-app/index.html b/src/ui/json-render-app/index.html
new file mode 100644
index 0000000..c35d43d
--- /dev/null
+++ b/src/ui/json-render-app/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ GHL Dynamic View
+
+
+
+
+
+
diff --git a/src/ui/json-render-app/package-lock.json b/src/ui/json-render-app/package-lock.json
new file mode 100644
index 0000000..6fd5836
--- /dev/null
+++ b/src/ui/json-render-app/package-lock.json
@@ -0,0 +1,1637 @@
+{
+ "name": "ghl-json-render-app",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "ghl-json-render-app",
+ "dependencies": {
+ "@anthropic-ai/sdk": "^0.39.0"
+ },
+ "devDependencies": {
+ "typescript": "^5.8.3",
+ "vite": "^6.3.5",
+ "vite-plugin-singlefile": "^2.0.3"
+ }
+ },
+ "node_modules/@anthropic-ai/sdk": {
+ "version": "0.39.0",
+ "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz",
+ "integrity": "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "^18.11.18",
+ "@types/node-fetch": "^2.6.4",
+ "abort-controller": "^3.0.0",
+ "agentkeepalive": "^4.2.1",
+ "form-data-encoder": "1.7.2",
+ "formdata-node": "^4.3.2",
+ "node-fetch": "^2.6.7"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
+ "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
+ "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
+ "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
+ "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
+ "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
+ "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
+ "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
+ "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
+ "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
+ "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
+ "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
+ "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
+ "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
+ "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
+ "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
+ "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
+ "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
+ "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
+ "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
+ "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
+ "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
+ "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
+ "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
+ "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
+ "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "18.19.130",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
+ "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
+ },
+ "node_modules/@types/node-fetch": {
+ "version": "2.6.13",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
+ "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "form-data": "^4.0.4"
+ }
+ },
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "license": "MIT",
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
+ "node_modules/agentkeepalive": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
+ "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "humanize-ms": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/form-data-encoder": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
+ "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
+ "license": "MIT"
+ },
+ "node_modules/formdata-node": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
+ "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "1.0.0",
+ "web-streams-polyfill": "4.0.0-beta.3"
+ },
+ "engines": {
+ "node": ">= 12.20"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/humanize-ms": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.0.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
+ "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.57.1",
+ "@rollup/rollup-android-arm64": "4.57.1",
+ "@rollup/rollup-darwin-arm64": "4.57.1",
+ "@rollup/rollup-darwin-x64": "4.57.1",
+ "@rollup/rollup-freebsd-arm64": "4.57.1",
+ "@rollup/rollup-freebsd-x64": "4.57.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.57.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.57.1",
+ "@rollup/rollup-linux-arm64-musl": "4.57.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.57.1",
+ "@rollup/rollup-linux-loong64-musl": "4.57.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.57.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.57.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.57.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.57.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.57.1",
+ "@rollup/rollup-linux-x64-gnu": "4.57.1",
+ "@rollup/rollup-linux-x64-musl": "4.57.1",
+ "@rollup/rollup-openbsd-x64": "4.57.1",
+ "@rollup/rollup-openharmony-arm64": "4.57.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.57.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.57.1",
+ "@rollup/rollup-win32-x64-gnu": "4.57.1",
+ "@rollup/rollup-win32-x64-msvc": "4.57.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
+ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-plugin-singlefile": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.3.0.tgz",
+ "integrity": "sha512-DAcHzYypM0CasNLSz/WG0VdKOCxGHErfrjOoyIPiNxTPTGmO6rRD/te93n1YL/s+miXq66ipF1brMBikf99c6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">18.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^4.44.1",
+ "vite": "^5.4.11 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/web-streams-polyfill": {
+ "version": "4.0.0-beta.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
+ "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ }
+ }
+}
diff --git a/src/ui/json-render-app/package.json b/src/ui/json-render-app/package.json
new file mode 100644
index 0000000..1836994
--- /dev/null
+++ b/src/ui/json-render-app/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "ghl-json-render-app",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "vite build"
+ },
+ "devDependencies": {
+ "vite": "^6.3.5",
+ "vite-plugin-singlefile": "^2.0.3",
+ "typescript": "^5.8.3"
+ },
+ "dependencies": {
+ "@anthropic-ai/sdk": "^0.39.0"
+ }
+}
diff --git a/src/ui/json-render-app/src/charts.ts b/src/ui/json-render-app/src/charts.ts
new file mode 100644
index 0000000..dc9e83c
--- /dev/null
+++ b/src/ui/json-render-app/src/charts.ts
@@ -0,0 +1,250 @@
+/**
+ * Chart Components for GHL Dynamic View
+ * Pure CSS/SVG charts — no external libraries
+ */
+
+type ChartFn = (props: any, children: string) => string;
+
+function esc(s: unknown): string {
+ if (s === null || s === undefined) return '';
+ return String(s)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+}
+
+const chartPalette = ['#4f46e5', '#7c3aed', '#16a34a', '#3b82f6', '#eab308', '#ef4444', '#ec4899', '#f97316'];
+
+export const BarChart: ChartFn = (props) => {
+ const {
+ bars = [], orientation = 'vertical', maxValue, showValues = true, title,
+ } = props;
+ const max = maxValue || Math.max(...bars.map((b: any) => b.value), 1);
+
+ if (orientation === 'horizontal') {
+ return `
+
+ ${title ? `
${esc(title)}
` : ''}
+
+ ${bars.map((b: any, i: number) => {
+ const pct = Math.min(100, (b.value / max) * 100);
+ const color = b.color || chartPalette[i % chartPalette.length];
+ return `
+
+
${esc(b.label)}
+
+ ${showValues ? `
${Number(b.value).toLocaleString()}` : ''}
+
`;
+ }).join('')}
+
+
`;
+ }
+
+ // Vertical bars via SVG
+ const svgW = Math.max(bars.length * 60, 200);
+ const svgH = 180;
+ const padTop = 10;
+ const padBot = 30;
+ const barW = Math.min(36, (svgW / bars.length) * 0.6);
+ const gap = svgW / bars.length;
+ const plotH = svgH - padTop - padBot;
+
+ const barsSvg = bars.map((b: any, i: number) => {
+ const pct = Math.min(1, b.value / max);
+ const h = pct * plotH;
+ const x = gap * i + (gap - barW) / 2;
+ const y = padTop + plotH - h;
+ const color = b.color || chartPalette[i % chartPalette.length];
+ return `
+
+ ${showValues ? `${Number(b.value).toLocaleString()}` : ''}
+ ${esc(b.label)}`;
+ }).join('');
+
+ return `
+
+ ${title ? `
${esc(title)}
` : ''}
+
+
+
+
`;
+};
+
+export const LineChart: ChartFn = (props) => {
+ const {
+ points = [], color = '#4f46e5', showPoints = true, showArea = false, title, yAxisLabel,
+ } = props;
+ if (points.length === 0) return 'No data
';
+
+ const vals = points.map((p: any) => p.value);
+ const minV = Math.min(...vals);
+ const maxV = Math.max(...vals);
+ const range = maxV - minV || 1;
+
+ const svgW = Math.max(points.length * 60, 200);
+ const svgH = 180;
+ const padL = 40;
+ const padR = 10;
+ const padT = 16;
+ const padB = 30;
+ const plotW = svgW - padL - padR;
+ const plotH = svgH - padT - padB;
+
+ const pts = points.map((p: any, i: number) => {
+ const x = padL + (plotW / Math.max(points.length - 1, 1)) * i;
+ const y = padT + plotH - ((p.value - minV) / range) * plotH;
+ return { x, y, label: p.label, value: p.value };
+ });
+
+ const linePath = pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ');
+ const areaPath = showArea
+ ? `${linePath} L${pts[pts.length - 1].x},${padT + plotH} L${pts[0].x},${padT + plotH} Z`
+ : '';
+
+ // Y-axis ticks (5 ticks)
+ const ticks = Array.from({ length: 5 }, (_, i) => {
+ const val = minV + (range * i) / 4;
+ const y = padT + plotH - (plotH * i) / 4;
+ return { val, y };
+ });
+
+ return `
+
+ ${title ? `
${esc(title)}
` : ''}
+
+
`;
+};
+
+export const PieChart: ChartFn = (props) => {
+ const {
+ segments = [], donut = false, title, showLegend = true,
+ } = props;
+ const total = segments.reduce((s: number, seg: any) => s + seg.value, 0) || 1;
+ const r = 70;
+ const cx = 90;
+ const cy = 90;
+ const svgSize = 180;
+
+ let cumAngle = -Math.PI / 2; // start at 12 o'clock
+ const arcs = segments.map((seg: any, i: number) => {
+ const frac = seg.value / total;
+ const angle = frac * Math.PI * 2;
+ const startAngle = cumAngle;
+ const endAngle = cumAngle + angle;
+ cumAngle = endAngle;
+
+ const x1 = cx + r * Math.cos(startAngle);
+ const y1 = cy + r * Math.sin(startAngle);
+ const x2 = cx + r * Math.cos(endAngle);
+ const y2 = cy + r * Math.sin(endAngle);
+ const largeArc = angle > Math.PI ? 1 : 0;
+ const color = seg.color || chartPalette[i % chartPalette.length];
+
+ // Single full segment
+ if (frac >= 0.9999) {
+ return ``;
+ }
+
+ return ``;
+ });
+
+ const donutHole = donut
+ ? ``
+ : '';
+
+ const legendHtml = showLegend ? `
+
+ ${segments.map((seg: any, i: number) => {
+ const color = seg.color || chartPalette[i % chartPalette.length];
+ const pct = ((seg.value / total) * 100).toFixed(1);
+ return `
${esc(seg.label)}${pct}%
`;
+ }).join('')}
+
` : '';
+
+ return `
+
+ ${title ? `
${esc(title)}
` : ''}
+
+
+ ${legendHtml}
+
+
`;
+};
+
+export const FunnelChart: ChartFn = (props) => {
+ const {
+ stages = [], showDropoff = true, title,
+ } = props;
+ if (stages.length === 0) return 'No data
';
+
+ const maxVal = stages[0]?.value || 1;
+
+ return `
+
+ ${title ? `
${esc(title)}
` : ''}
+
+ ${stages.map((s: any, i: number) => {
+ const pct = Math.max(20, (s.value / maxVal) * 100);
+ const color = s.color || chartPalette[i % chartPalette.length];
+ const dropoff = i > 0
+ ? (((stages[i - 1].value - s.value) / stages[i - 1].value) * 100).toFixed(1)
+ : null;
+ return `
+
+
+ ${esc(s.label)}
+ ${Number(s.value).toLocaleString()}
+
+
+ ${showDropoff && dropoff !== null ? `
-${dropoff}%` : '
'}
+
`;
+ }).join('')}
+
+
`;
+};
+
+export const SparklineChart: ChartFn = (props) => {
+ const {
+ values = [], color = '#4f46e5', height = 24, width = 80,
+ } = props;
+ if (values.length < 2) {
+ return `\u2014`;
+ }
+
+ const minV = Math.min(...values);
+ const maxV = Math.max(...values);
+ const range = maxV - minV || 1;
+ const pad = 2;
+
+ const pts = values.map((v: number, i: number) => {
+ const x = pad + ((width - pad * 2) / (values.length - 1)) * i;
+ const y = pad + (height - pad * 2) - ((v - minV) / range) * (height - pad * 2);
+ return `${x},${y}`;
+ });
+
+ return ``;
+};
diff --git a/src/ui/json-render-app/src/components.ts b/src/ui/json-render-app/src/components.ts
new file mode 100644
index 0000000..d950291
--- /dev/null
+++ b/src/ui/json-render-app/src/components.ts
@@ -0,0 +1,1224 @@
+/**
+ * GHL Component Library - Vanilla JS Template Functions
+ * All 20 components ported from React spike to HTML string renderers.
+ */
+
+import { BarChart, LineChart, PieChart, FunnelChart, SparklineChart } from './charts';
+
+// ─── Utility helpers ────────────────────────────────────────
+
+function esc(s: unknown): string {
+ if (s === null || s === undefined) return '';
+ return String(s)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+}
+
+function fmtCurrency(n: number, currency = 'USD'): string {
+ try {
+ return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n);
+ } catch {
+ return `$${n.toFixed(2)}`;
+ }
+}
+
+const avatarColors = ['#4f46e5', '#7c3aed', '#059669', '#d97706', '#dc2626', '#0891b2'];
+function getAvatarColor(name: string): string {
+ return avatarColors[(name || '').charCodeAt(0) % avatarColors.length];
+}
+function getInitials(name: string): string {
+ return (name || '').split(' ').map(n => n[0]).join('').slice(0, 2).toUpperCase();
+}
+
+// ─── Status/Variant Color Maps ──────────────────────────────
+
+const statusColors: Record = {
+ active: 'status-active', complete: 'status-complete', paused: 'status-paused',
+ draft: 'status-draft', error: 'status-error', sent: 'status-sent',
+ paid: 'status-paid', pending: 'status-pending', open: 'status-open',
+ won: 'status-won', lost: 'status-lost', abandoned: 'status-draft',
+};
+
+const trendIcons: Record = { up: '↑', down: '↓', flat: '→' };
+const trendClasses: Record = { up: 'trend-up', down: 'trend-down', flat: 'trend-flat' };
+
+const metricColorClasses: Record = {
+ default: '', green: 'metric-green', blue: 'metric-blue', purple: 'metric-purple',
+ yellow: 'metric-yellow', red: 'metric-red',
+};
+
+const barColorClasses: Record = {
+ green: 'bar-green', blue: 'bar-blue', purple: 'bar-purple', yellow: 'bar-yellow', red: 'bar-red',
+};
+
+const iconMap: Record = {
+ email: '📧', phone: '📞', note: '📝', meeting: '📅', task: '✅', system: '⚙️',
+};
+
+const variantBorderClasses: Record = {
+ default: 'tl-border-default', success: 'tl-border-success',
+ warning: 'tl-border-warning', error: 'tl-border-error',
+};
+
+const btnVariantClasses: Record = {
+ primary: 'btn-primary', secondary: 'btn-secondary', danger: 'btn-danger', ghost: 'btn-ghost',
+};
+
+const btnSizeClasses: Record = {
+ sm: 'btn-sm', md: 'btn-md', lg: 'btn-lg',
+};
+
+// ─── Component Type ─────────────────────────────────────────
+
+type ComponentFn = (props: any, children: string) => string;
+
+// ─── Layout Components ──────────────────────────────────────
+
+const PageHeader: ComponentFn = (props, children) => {
+ const { title, subtitle, status, statusVariant, gradient, stats } = props;
+ const statusCls = statusColors[statusVariant || 'active'] || 'status-active';
+
+ if (gradient) {
+ return `
+ `;
+ }
+
+ return `
+ `;
+};
+
+const Card: ComponentFn = (props, children) => {
+ const { title, subtitle, padding = 'md', noBorder } = props;
+ const padCls = ({ none: 'p-0', sm: 'p-sm', md: 'p-md', lg: 'p-lg' } as Record)[padding] || 'p-md';
+ return `
+
+ ${title ? `` : ''}
+
${children}
+
`;
+};
+
+const StatsGrid: ComponentFn = (props, children) => {
+ const cols = props.columns || 3;
+ return `${children}
`;
+};
+
+const SplitLayout: ComponentFn = (props, children) => {
+ const ratio = props.ratio || '50/50';
+ const gap = props.gap || 'md';
+ const ratioCls = ({ '50/50': 'split-50-50', '33/67': 'split-33-67', '67/33': 'split-67-33' } as Record)[ratio] || 'split-50-50';
+ const gapCls = ({ sm: 'gap-sm', md: 'gap-md', lg: 'gap-lg' } as Record)[gap] || 'gap-md';
+ return `${children}
`;
+};
+
+const Section: ComponentFn = (props, children) => {
+ const { title, description } = props;
+ return `
+
+ ${title ? `` : ''}
+ ${children}
+
`;
+};
+
+// ─── Data Display Components ────────────────────────────────
+
+function formatCell(value: unknown, format?: string): string {
+ if (value === null || value === undefined) return '—';
+ switch (format) {
+ case 'email':
+ return `${esc(value)}`;
+ case 'phone':
+ return `${esc(value)}`;
+ case 'date':
+ return `${esc(value)}`;
+ case 'currency':
+ return `${esc(value)}`;
+ case 'tags': {
+ const tags = Array.isArray(value) ? value : [value];
+ const visible = tags.slice(0, 3);
+ return `${visible.map(t => `${esc(t)}`).join('')}${tags.length > 3 ? `+${tags.length - 3}` : ''}
`;
+ }
+ case 'avatar': {
+ const name = String(value);
+ const color = getAvatarColor(name);
+ return `${getInitials(name)}
${esc(name)} `;
+ }
+ case 'status': {
+ const s = String(value).toLowerCase();
+ const cls = s.includes('active') ? 'status-complete' : s.includes('new') ? 'status-active' : s.includes('lost') ? 'status-error' : 'status-draft';
+ return `${esc(value)}`;
+ }
+ default:
+ return `${esc(value)}`;
+ }
+}
+
+const DataTable: ComponentFn = (props) => {
+ const { columns = [], rows = [], selectable, emptyMessage, pageSize = 10 } = props;
+ if (rows.length === 0) {
+ return `📋
${esc(emptyMessage || 'No data available')}
`;
+ }
+ const displayRows = rows.slice(0, pageSize);
+ const totalPages = Math.ceil(rows.length / pageSize);
+ return `
+
+
+ ${totalPages > 1 ? `
+ ` : ''}
+
`;
+};
+
+const KanbanBoard: ComponentFn = (props) => {
+ const { columns = [] } = props;
+ const kanbanStatusClasses: Record = {
+ open: 'status-open', won: 'status-won', lost: 'status-lost', abandoned: 'status-draft',
+ };
+ return `
+
+
+ ${columns.map((col: any) => `
+
+
+
+ ${(!col.cards || col.cards.length === 0) ? '
No items
' :
+ col.cards.map((card: any) => `
+
+
${esc(card.title)}
+ ${card.subtitle ? `
${card.avatarInitials ? `
${esc(card.avatarInitials)}
` : ''}${esc(card.subtitle)}
` : ''}
+ ${card.value ? `
${esc(card.value)}
` : ''}
+
+
`).join('')}
+
+
`).join('')}
+
+
`;
+};
+
+const MetricCard: ComponentFn = (props) => {
+ const { label, value, trend, trendValue, color = 'default' } = props;
+ const colorCls = metricColorClasses[color] || '';
+ return `
+
+
${esc(value)}
+
${esc(label)}
+ ${trend && trendValue ? `
${trendIcons[trend] || '→'} ${esc(trendValue)}
` : ''}
+
`;
+};
+
+const StatusBadge: ComponentFn = (props) => {
+ const { label, variant } = props;
+ const cls = statusColors[variant] || 'status-active';
+ return `${esc(label)}`;
+};
+
+const Timeline: ComponentFn = (props) => {
+ const { events = [] } = props;
+ return `
+
+
+ ${events.map((e: any) => `
+
+
${iconMap[e.icon || 'system'] || '•'}
+
+
${esc(e.title)}
+ ${e.description ? `
${esc(e.description)}
` : ''}
+
${esc(e.timestamp)}
+
+
`).join('')}
+
`;
+};
+
+const ProgressBar: ComponentFn = (props) => {
+ const { label, value, max = 100, color = 'blue', showPercent = true, benchmark, benchmarkLabel } = props;
+ const pct = Math.min(100, (value / max) * 100);
+ const colorCls = barColorClasses[color] || 'bar-blue';
+ return `
+
+
+
+
+ ${benchmark !== undefined ? `
${benchmarkLabel ? `
${esc(benchmarkLabel)}` : ''}` : ''}
+
+
`;
+};
+
+// ─── Detail View Components ─────────────────────────────────
+
+const DetailHeader: ComponentFn = (props, children) => {
+ const { title, subtitle, entityId, status, statusVariant } = props;
+ const cls = statusColors[statusVariant || 'active'] || 'status-active';
+ return `
+ `;
+};
+
+const KeyValueList: ComponentFn = (props) => {
+ const { items = [], compact } = props;
+ return `
+
+ ${items.map((item: any) => {
+ const isTotalRow = item.isTotalRow;
+ let rowCls = 'kv-row';
+ if (isTotalRow) rowCls += ' kv-total';
+ else if (item.variant === 'success') rowCls += ' kv-success';
+ else if (item.variant === 'highlight') rowCls += ' kv-highlight';
+ else if (item.variant === 'muted') rowCls += ' kv-muted';
+ if (compact) rowCls += ' kv-compact';
+ const valueCls = isTotalRow ? 'kv-value-total' : item.bold ? 'kv-value-bold' : item.variant === 'danger' ? 'kv-value-danger' : item.variant === 'success' ? 'kv-value-success' : 'kv-value';
+ return `
${esc(item.label)}${esc(item.value)}
`;
+ }).join('')}
+
`;
+};
+
+const LineItemsTable: ComponentFn = (props) => {
+ const { items = [], currency = 'USD' } = props;
+ return `
+
+
+
+
+ | Item |
+ Qty |
+ Price |
+ Total |
+
+
+
+ ${items.map((item: any) => `
+
+ ${esc(item.name)} ${item.description ? `${esc(item.description)} ` : ''} |
+ ${item.quantity} |
+ ${fmtCurrency(item.unitPrice, currency)} |
+ ${fmtCurrency(item.total, currency)} |
+
`).join('')}
+
+
+
`;
+};
+
+const InfoBlock: ComponentFn = (props) => {
+ const { label, name, lines = [] } = props;
+ return `
+
+
${esc(label)}
+
${esc(name)}
+
${lines.map((l: string) => `
${esc(l)}
`).join('')}
+
`;
+};
+
+// ─── Interactive Components ─────────────────────────────────
+
+const SearchBar: ComponentFn = (props) => {
+ const { placeholder = 'Search...' } = props;
+ return `
+
+
+
`;
+};
+
+const FilterChips: ComponentFn = (props) => {
+ const { chips = [] } = props;
+ return `
+
+ ${chips.map((c: any) => ``).join('')}
+
`;
+};
+
+const TabGroup: ComponentFn = (props) => {
+ const { tabs = [], activeTab } = props;
+ const active = activeTab || tabs[0]?.value;
+ return `
+
+ ${tabs.map((t: any) => `
+ `).join('')}
+
`;
+};
+
+// ─── Action Components ──────────────────────────────────────
+
+const ActionButton: ComponentFn = (props) => {
+ const { label, variant = 'secondary', size = 'md', disabled } = props;
+ const vCls = btnVariantClasses[variant] || 'btn-secondary';
+ const sCls = btnSizeClasses[size] || 'btn-md';
+ return ``;
+};
+
+const ActionBar: ComponentFn = (props, children) => {
+ const { align = 'right' } = props;
+ const alignCls = ({ left: 'align-left', center: 'align-center', right: 'align-right' } as Record)[align] || 'align-right';
+ return `${children}
`;
+};
+
+// ─── Data Display Components (Extended) ─────────────────────
+
+const currencySizeClasses: Record = { sm: 'currency-sm', md: 'currency-md', lg: 'currency-lg' };
+
+const CurrencyDisplay: ComponentFn = (props) => {
+ const { amount, currency = 'USD', locale = 'en-US', size = 'md', positive, negative } = props;
+ const sizeCls = currencySizeClasses[size] || 'currency-md';
+ let colorCls = '';
+ if (positive) colorCls = 'currency-positive';
+ else if (negative) colorCls = 'currency-negative';
+ else if (amount > 0) colorCls = 'currency-positive';
+ else if (amount < 0) colorCls = 'currency-negative';
+ let formatted: string;
+ try {
+ formatted = new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount);
+ } catch {
+ formatted = `$${Number(amount).toFixed(2)}`;
+ }
+ return `${esc(formatted)}`;
+};
+
+const tagColorMap: Record = {
+ blue: 'tag-pill-blue', green: 'tag-pill-green', red: 'tag-pill-red',
+ yellow: 'tag-pill-yellow', purple: 'tag-pill-purple', gray: 'tag-pill-gray',
+ indigo: 'tag-pill-indigo', pink: 'tag-pill-pink',
+};
+
+const TagList: ComponentFn = (props) => {
+ const { tags = [], maxVisible, size = 'md' } = props;
+ const visible = maxVisible ? tags.slice(0, maxVisible) : tags;
+ const remaining = maxVisible ? tags.length - maxVisible : 0;
+ const sizeCls = size === 'sm' ? 'tag-list-sm' : 'tag-list-md';
+ return `
+
+ ${visible.map((t: any) => {
+ const label = typeof t === 'string' ? t : t.label;
+ const color = (typeof t === 'object' && t.color) || 'blue';
+ const variant = (typeof t === 'object' && t.variant) || 'filled';
+ const colorCls = tagColorMap[color] || 'tag-pill-blue';
+ const variantCls = variant === 'outlined' ? 'tag-pill-outlined' : '';
+ return `${esc(label)}`;
+ }).join('')}
+ ${remaining > 0 ? `+${remaining}` : ''}
+
`;
+};
+
+const CardGrid: ComponentFn = (props) => {
+ const { cards = [], columns = 3 } = props;
+ const cardStatusClasses: Record = {
+ active: 'cg-status-active', complete: 'cg-status-complete', draft: 'cg-status-draft',
+ error: 'cg-status-error', pending: 'cg-status-pending',
+ };
+ return `
+
+ ${cards.map((c: any) => `
+
+ ${c.imageUrl ? `
` : `
📄
`}
+
+
${esc(c.title)}
+ ${c.subtitle ? `
${esc(c.subtitle)}
` : ''}
+ ${c.description ? `
${esc(c.description)}
` : ''}
+
+
+
`).join('')}
+
`;
+};
+
+const avatarSizeMap: Record = {
+ sm: { cls: 'ag-sm', px: 28 },
+ md: { cls: 'ag-md', px: 36 },
+ lg: { cls: 'ag-lg', px: 44 },
+};
+
+const AvatarGroup: ComponentFn = (props) => {
+ const { avatars = [], max = 5, size = 'md' } = props;
+ const visible = avatars.slice(0, max);
+ const overflow = avatars.length - max;
+ const sizeDef = avatarSizeMap[size] || avatarSizeMap.md;
+ return `
+
+ ${visible.map((a: any, i: number) => {
+ const name = a.name || '';
+ const initials = a.initials || getInitials(name);
+ const color = getAvatarColor(name);
+ if (a.imageUrl) {
+ return `
`;
+ }
+ return `
${esc(initials)}
`;
+ }).join('')}
+ ${overflow > 0 ? `
+${overflow}
` : ''}
+
`;
+};
+
+const StarRating: ComponentFn = (props) => {
+ const { rating = 0, count, maxStars = 5, distribution, showDistribution } = props;
+ const fullStars = Math.floor(rating);
+ const hasHalf = rating - fullStars >= 0.25 && rating - fullStars < 0.75;
+ const emptyStars = maxStars - fullStars - (hasHalf ? 1 : 0);
+ const stars = '★'.repeat(fullStars) + (hasHalf ? '⯨' : '') + '☆'.repeat(Math.max(0, emptyStars));
+ const maxCount = distribution ? Math.max(...distribution.map((d: any) => d.count), 1) : 1;
+ return `
+
+
+ ${rating.toFixed(1)}
+ ${stars}
+ ${count !== undefined ? `(${Number(count).toLocaleString()} reviews)` : ''}
+
+ ${showDistribution && distribution ? `
+
+ ${distribution.sort((a: any, b: any) => b.stars - a.stars).map((d: any) => {
+ const pct = (d.count / maxCount) * 100;
+ return `
+
+
${d.stars}★
+
+
${d.count}
+
`;
+ }).join('')}
+
` : ''}
+
`;
+};
+
+const StockIndicator: ComponentFn = (props) => {
+ const { quantity, lowThreshold = 10, criticalThreshold = 3, label } = props;
+ let level: string, levelCls: string, icon: string;
+ if (quantity <= criticalThreshold) {
+ level = 'Critical'; levelCls = 'stock-critical'; icon = '🔴';
+ } else if (quantity <= lowThreshold) {
+ level = 'Low'; levelCls = 'stock-low'; icon = '🟡';
+ } else {
+ level = 'In Stock'; levelCls = 'stock-ok'; icon = '🟢';
+ }
+ return `
+
+
${icon}
+
+ ${label ? `
${esc(label)}
` : ''}
+
${quantity} units
+
${level}
+
+
`;
+};
+
+// ─── Communication & Detail View Components ─────────────────
+
+const chatTypeIcons: Record = {
+ sms: '💬', email: '📧', call: '📞', whatsapp: '📱',
+};
+
+const ChatThread: ComponentFn = (props) => {
+ const { messages = [], title } = props;
+ if (messages.length === 0) {
+ return ``;
+ }
+ return `
+
+ ${title ? `` : ''}
+
+ ${messages.map((msg: any) => {
+ const isOutbound = msg.direction === 'outbound';
+ const typeIcon = chatTypeIcons[msg.type || 'sms'] || '💬';
+ const avatarBg = isOutbound ? '#4f46e5' : getAvatarColor(msg.senderName || 'U');
+ const initials = getInitials(msg.senderName || (isOutbound ? 'You' : 'Contact'));
+ return `
+
+ ${!isOutbound ? `
${msg.avatar ? `
})
` : initials}
` : ''}
+
+ ${msg.senderName ? `
${esc(msg.senderName)}
` : ''}
+
${esc(msg.content)}
+
${typeIcon} ${esc(msg.timestamp || '')}
+
+ ${isOutbound ? `
${msg.avatar ? `
})
` : initials}
` : ''}
+
`;
+ }).join('')}
+
+
`;
+};
+
+const EmailPreview: ComponentFn = (props) => {
+ const { from, to, subject, date, body = '', cc, attachments = [] } = props;
+ const sanitizedBody = String(body)
+ .replace(/
+
+
diff --git a/src/ui/react-app/package-lock.json b/src/ui/react-app/package-lock.json
new file mode 100644
index 0000000..9a95392
--- /dev/null
+++ b/src/ui/react-app/package-lock.json
@@ -0,0 +1,3190 @@
+{
+ "name": "mcp-ui-kit-react",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "mcp-ui-kit-react",
+ "version": "1.0.0",
+ "dependencies": {
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.24.0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.2",
+ "@types/react-dom": "^19.2.2",
+ "@vitejs/plugin-react": "^4.3.4",
+ "typescript": "^5.9.3",
+ "vite": "^6.0.0",
+ "vite-plugin-singlefile": "^2.3.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz",
+ "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
+ "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@hono/node-server": {
+ "version": "1.19.9",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
+ "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.14.1"
+ },
+ "peerDependencies": {
+ "hono": "^4"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@modelcontextprotocol/ext-apps": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.0.1.tgz",
+ "integrity": "sha512-rAPzBbB5GNgYk216paQjGKUgbNXSy/yeR95c0ni6Y4uvhWI2AeF+ztEOqQFLBMQy/MPM+02pbVK1HaQmQjMwYQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "workspaces": [
+ "examples/*"
+ ],
+ "optionalDependencies": {
+ "@oven/bun-darwin-aarch64": "^1.2.21",
+ "@oven/bun-darwin-x64": "^1.2.21",
+ "@oven/bun-darwin-x64-baseline": "^1.2.21",
+ "@oven/bun-linux-aarch64": "^1.2.21",
+ "@oven/bun-linux-aarch64-musl": "^1.2.21",
+ "@oven/bun-linux-x64": "^1.2.21",
+ "@oven/bun-linux-x64-baseline": "^1.2.21",
+ "@oven/bun-linux-x64-musl": "^1.2.21",
+ "@oven/bun-linux-x64-musl-baseline": "^1.2.21",
+ "@oven/bun-windows-x64": "^1.2.21",
+ "@oven/bun-windows-x64-baseline": "^1.2.21",
+ "@rollup/rollup-darwin-arm64": "^4.53.3",
+ "@rollup/rollup-darwin-x64": "^4.53.3",
+ "@rollup/rollup-linux-arm64-gnu": "^4.53.3",
+ "@rollup/rollup-linux-x64-gnu": "^4.53.3",
+ "@rollup/rollup-win32-arm64-msvc": "^4.53.3",
+ "@rollup/rollup-win32-x64-msvc": "^4.53.3"
+ },
+ "peerDependencies": {
+ "@modelcontextprotocol/sdk": "^1.24.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "zod": "^3.25.0 || ^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk": {
+ "version": "1.25.3",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.3.tgz",
+ "integrity": "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@hono/node-server": "^1.19.9",
+ "ajv": "^8.17.1",
+ "ajv-formats": "^3.0.1",
+ "content-type": "^1.0.5",
+ "cors": "^2.8.5",
+ "cross-spawn": "^7.0.5",
+ "eventsource": "^3.0.2",
+ "eventsource-parser": "^3.0.0",
+ "express": "^5.0.1",
+ "express-rate-limit": "^7.5.0",
+ "jose": "^6.1.1",
+ "json-schema-typed": "^8.0.2",
+ "pkce-challenge": "^5.0.0",
+ "raw-body": "^3.0.0",
+ "zod": "^3.25 || ^4.0",
+ "zod-to-json-schema": "^3.25.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@cfworker/json-schema": "^4.1.1",
+ "zod": "^3.25 || ^4.0"
+ },
+ "peerDependenciesMeta": {
+ "@cfworker/json-schema": {
+ "optional": true
+ },
+ "zod": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/@oven/bun-darwin-aarch64": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.3.8.tgz",
+ "integrity": "sha512-hPERz4IgXCM6Y6GdEEsJAFceyJMt29f3HlFzsvE/k+TQjChRhar6S+JggL35b9VmFfsdxyCOOTPqgnSrdV0etA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@oven/bun-darwin-x64": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.3.8.tgz",
+ "integrity": "sha512-SaWIxsRQYiT/eA60bqA4l8iNO7cJ6YD8ie82RerRp9voceBxPIZiwX4y20cTKy5qNaSGr9LxfYq7vDywTipiog==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@oven/bun-darwin-x64-baseline": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-1.3.8.tgz",
+ "integrity": "sha512-ArHVWpCRZI3vGLoN2/8ud8Kzqlgn1Gv+fNw+pMB9x18IzgAEhKxFxsWffnoaH21amam4tAOhpeewRIgdNtB0Cw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@oven/bun-linux-aarch64": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.3.8.tgz",
+ "integrity": "sha512-rq0nNckobtS+ONoB95/Frfqr8jCtmSjjjEZlN4oyUx0KEBV11Vj4v3cDVaWzuI34ryL8FCog3HaqjfKn8R82Tw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@oven/bun-linux-aarch64-musl": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64-musl/-/bun-linux-aarch64-musl-1.3.8.tgz",
+ "integrity": "sha512-HvJmhrfipL7GtuqFz6xNpmf27NGcCOMwCalPjNR6fvkLpe8A7Z1+QbxKKjOglelmlmZc3Vi2TgDUtxSqfqOToQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@oven/bun-linux-x64": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.3.8.tgz",
+ "integrity": "sha512-YDgqVx1MI8E0oDbCEUSkAMBKKGnUKfaRtMdLh9Bjhu7JQacQ/ZCpxwi4HPf5Q0O1TbWRrdxGw2tA2Ytxkn7s1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@oven/bun-linux-x64-baseline": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-1.3.8.tgz",
+ "integrity": "sha512-3IkS3TuVOzMqPW6Gg9/8FEoKF/rpKZ9DZUfNy9GQ54+k4PGcXpptU3+dy8D4iDFCt4qe6bvoiAOdM44OOsZ+Wg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@oven/bun-linux-x64-musl": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl/-/bun-linux-x64-musl-1.3.8.tgz",
+ "integrity": "sha512-o7Jm5zL4aw9UBs3BcZLVbgGm2V4F10MzAQAV+ziKzoEfYmYtvDqRVxgKEq7BzUOVy4LgfrfwzEXw5gAQGRrhQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@oven/bun-linux-x64-musl-baseline": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl-baseline/-/bun-linux-x64-musl-baseline-1.3.8.tgz",
+ "integrity": "sha512-5g8XJwHhcTh8SGoKO7pR54ILYDbuFkGo+68DOMTiVB5eLxuLET+Or/camHgk4QWp3nUS5kNjip4G8BE8i0rHVQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@oven/bun-windows-x64": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64/-/bun-windows-x64-1.3.8.tgz",
+ "integrity": "sha512-UDI3rowMm/tI6DIynpE4XqrOhr+1Ztk1NG707Wxv2nygup+anTswgCwjfjgmIe78LdoRNFrux2GpeolhQGW6vQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@oven/bun-windows-x64-baseline": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64-baseline/-/bun-windows-x64-baseline-1.3.8.tgz",
+ "integrity": "sha512-K6qBUKAZLXsjAwFxGTG87dsWlDjyDl2fqjJr7+x7lmv2m+aSEzmLOK+Z5pSvGkpjBp3LXV35UUgj8G0UTd0pPg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
+ "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
+ "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
+ "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
+ "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
+ "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
+ "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
+ "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
+ "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
+ "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
+ "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
+ "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
+ "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
+ "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
+ "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
+ "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
+ "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
+ "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
+ "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
+ "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
+ "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
+ "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
+ "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
+ "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
+ "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
+ "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.10",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz",
+ "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.19",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
+ "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001767",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz",
+ "integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/content-disposition": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.286",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
+ "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventsource": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
+ "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
+ "license": "MIT",
+ "dependencies": {
+ "eventsource-parser": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/eventsource-parser": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
+ "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-rate-limit": {
+ "version": "7.5.1",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
+ "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/express-rate-limit"
+ },
+ "peerDependencies": {
+ "express": ">= 4.11"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hono": {
+ "version": "4.11.7",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz",
+ "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=16.9.0"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/jose": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
+ "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/json-schema-typed": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
+ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pkce-challenge": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
+ "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
+ "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
+ "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
+ "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.4"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
+ "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.57.1",
+ "@rollup/rollup-android-arm64": "4.57.1",
+ "@rollup/rollup-darwin-arm64": "4.57.1",
+ "@rollup/rollup-darwin-x64": "4.57.1",
+ "@rollup/rollup-freebsd-arm64": "4.57.1",
+ "@rollup/rollup-freebsd-x64": "4.57.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.57.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.57.1",
+ "@rollup/rollup-linux-arm64-musl": "4.57.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.57.1",
+ "@rollup/rollup-linux-loong64-musl": "4.57.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.57.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.57.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.57.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.57.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.57.1",
+ "@rollup/rollup-linux-x64-gnu": "4.57.1",
+ "@rollup/rollup-linux-x64-musl": "4.57.1",
+ "@rollup/rollup-openbsd-x64": "4.57.1",
+ "@rollup/rollup-openharmony-arm64": "4.57.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.57.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.57.1",
+ "@rollup/rollup-win32-x64-gnu": "4.57.1",
+ "@rollup/rollup-win32-x64-msvc": "4.57.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
+ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-plugin-singlefile": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.3.0.tgz",
+ "integrity": "sha512-DAcHzYypM0CasNLSz/WG0VdKOCxGHErfrjOoyIPiNxTPTGmO6rRD/te93n1YL/s+miXq66ipF1brMBikf99c6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">18.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^4.44.1",
+ "vite": "^5.4.11 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/zod": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-to-json-schema": {
+ "version": "3.25.1",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
+ "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "zod": "^3.25 || ^4"
+ }
+ }
+ }
+}
diff --git a/src/ui/react-app/package.json b/src/ui/react-app/package.json
new file mode 100644
index 0000000..662c056
--- /dev/null
+++ b/src/ui/react-app/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "mcp-ui-kit-react",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "description": "MCP UI Kit — Generic React component library for MCP App UIs",
+ "scripts": {
+ "build": "tsc --noEmit && vite build",
+ "dev": "vite build --watch"
+ },
+ "dependencies": {
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.24.0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.2",
+ "@types/react-dom": "^19.2.2",
+ "@vitejs/plugin-react": "^4.3.4",
+ "typescript": "^5.9.3",
+ "vite": "^6.0.0",
+ "vite-plugin-singlefile": "^2.3.0"
+ }
+}
diff --git a/src/ui/react-app/src/App.tsx b/src/ui/react-app/src/App.tsx
new file mode 100644
index 0000000..af67eb6
--- /dev/null
+++ b/src/ui/react-app/src/App.tsx
@@ -0,0 +1,211 @@
+/**
+ * App.tsx — Root component for the MCP UI Kit React app.
+ *
+ * Uses useApp from ext-apps/react to connect to the MCP host.
+ * Receives UI trees via ontoolresult and renders them via UITreeRenderer.
+ *
+ * HYBRID INTERACTIVITY: Uses mergeUITrees to preserve local component state
+ * across tool result updates. Wraps with ChangeTrackerProvider for shared
+ * change tracking across all interactive components.
+ */
+import React, { useState, useEffect } from "react";
+import type { McpUiHostContext } from "@modelcontextprotocol/ext-apps";
+import { useApp } from "@modelcontextprotocol/ext-apps/react";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import { MCPAppProvider } from "./context/MCPAppContext.js";
+import { ChangeTrackerProvider } from "./context/ChangeTrackerContext.js";
+import { UITreeRenderer } from "./renderer/UITreeRenderer.js";
+import { ToastProvider } from "./components/shared/Toast.js";
+import { SaveIndicator } from "./components/shared/SaveIndicator.js";
+import { mergeUITrees } from "./utils/mergeUITrees.js";
+import type { UITree } from "./types.js";
+import "./styles/base.css";
+import "./styles/interactive.css";
+
+// ─── Parse UI Tree from tool result ─────────────────────────
+
+function extractUITree(result: CallToolResult): UITree | null {
+ // 1. Check structuredContent first — this is where generateDynamicView puts the uiTree
+ const sc = (result as any).structuredContent;
+ if (sc) {
+ if (sc.uiTree && sc.uiTree.root && sc.uiTree.elements) {
+ return sc.uiTree as UITree;
+ }
+ // structuredContent might BE the tree directly
+ if (sc.root && sc.elements) {
+ return sc as UITree;
+ }
+ }
+
+ // 2. Check content array for JSON text containing a UI tree
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === "text") {
+ try {
+ const parsed = JSON.parse(item.text);
+ if (parsed && parsed.root && parsed.elements) {
+ return parsed as UITree;
+ }
+ // Might be wrapped: { uiTree: { root, elements } }
+ if (parsed?.uiTree?.root && parsed?.uiTree?.elements) {
+ return parsed.uiTree as UITree;
+ }
+ } catch {
+ // Not JSON — skip
+ }
+ }
+ }
+ }
+
+ return null;
+}
+
+/** Check for server-injected data via window.__MCP_APP_DATA__ */
+function getPreInjectedTree(): UITree | null {
+ try {
+ const data = (window as any).__MCP_APP_DATA__;
+ if (!data) return null;
+ if (data.uiTree?.root && data.uiTree?.elements) return data.uiTree as UITree;
+ if (data.root && data.elements) return data as UITree;
+ } catch { /* ignore */ }
+ return null;
+}
+
+// ─── Main App ───────────────────────────────────────────────
+
+export function App() {
+ const [uiTree, setUITree] = useState(null);
+ const [hostContext, setHostContext] = useState();
+ const [toolInput, setToolInput] = useState(null);
+
+ // Check for pre-injected data (server injects via window.__MCP_APP_DATA__)
+ useEffect(() => {
+ const preInjected = getPreInjectedTree();
+ if (preInjected && !uiTree) {
+ console.info("[MCPApp] Found pre-injected UI tree");
+ setUITree(preInjected);
+ }
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: "MCP UI Kit", version: "1.0.0" },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ console.info("[MCPApp] Tool result received:", result);
+ const newTree = extractUITree(result);
+ if (newTree) {
+ // CRITICAL FIX: Merge trees instead of replacing.
+ // This preserves React component state (form inputs, drag state, etc.)
+ // by keeping exact old references for unchanged elements.
+ setUITree((prevTree) => {
+ if (!prevTree) return newTree;
+ return mergeUITrees(prevTree, newTree);
+ });
+ setToolInput(null);
+ }
+ };
+
+ app.ontoolinput = async (input) => {
+ console.info("[MCPApp] Tool input received:", input);
+ setToolInput("Loading view...");
+ };
+
+ app.ontoolcancelled = (params) => {
+ console.info("[MCPApp] Tool cancelled:", params.reason);
+ setToolInput(null);
+ };
+
+ app.onerror = (err) => {
+ console.error("[MCPApp] Error:", err);
+ };
+
+ app.onhostcontextchanged = (params) => {
+ setHostContext((prev) => ({ ...prev, ...params }));
+ };
+ },
+ });
+
+ useEffect(() => {
+ if (app) {
+ setHostContext(app.getHostContext());
+ }
+ }, [app]);
+
+ // Error state
+ if (error) {
+ return (
+
+
Connection Error
+
{error.message}
+
+ );
+ }
+
+ // Connecting state
+ if (!isConnected || !app) {
+ return (
+
+
+
Connecting to host...
+
+ );
+ }
+
+ const safeAreaStyle = {
+ paddingTop: hostContext?.safeAreaInsets?.top,
+ paddingRight: hostContext?.safeAreaInsets?.right,
+ paddingBottom: hostContext?.safeAreaInsets?.bottom,
+ paddingLeft: hostContext?.safeAreaInsets?.left,
+ };
+
+ // Tool call in progress (no tree yet)
+ if (toolInput && !uiTree) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ // Waiting for first tool result
+ if (!uiTree) {
+ return (
+
+
+
+
+
+
+
Waiting for data...
+
+
+
+
+
+ );
+ }
+
+ // Render the UI tree
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/affiliate-dashboard/App.tsx b/src/ui/react-app/src/apps/affiliate-dashboard/App.tsx
new file mode 100644
index 0000000..e9c8aa1
--- /dev/null
+++ b/src/ui/react-app/src/apps/affiliate-dashboard/App.tsx
@@ -0,0 +1,149 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { StatsGrid } from '../../components/layout/StatsGrid';
+import { MetricCard } from '../../components/data/MetricCard';
+import { DataTable } from '../../components/data/DataTable';
+import { StatusBadge } from '../../components/data/StatusBadge';
+import { CurrencyDisplay } from '../../components/data/CurrencyDisplay';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatCurrency(amount: number | string | undefined): string {
+ if (amount === undefined || amount === null) return '$0.00';
+ const n = typeof amount === 'string' ? parseFloat(amount) : amount;
+ if (isNaN(n)) return '$0.00';
+ return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n);
+}
+
+function formatPercent(val: number | string | undefined): string {
+ if (val === undefined || val === null) return '0%';
+ const n = typeof val === 'string' ? parseFloat(val) : val;
+ if (isNaN(n)) return '0%';
+ return `${n.toFixed(1)}%`;
+}
+
+const statusVariantMap: Record = {
+ active: 'active',
+ paused: 'paused',
+ draft: 'draft',
+ completed: 'complete',
+ ended: 'complete',
+ inactive: 'draft',
+};
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [search, setSearch] = useState('');
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Affiliate Dashboard', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const stats = useMemo(() => {
+ const s = data?.stats || data?.summary || {};
+ return {
+ totalAffiliates: s.totalAffiliates || s.affiliateCount || 0,
+ activeCampaigns: s.activeCampaigns || s.campaignCount || 0,
+ totalCommissions: s.totalCommissions || s.commissions || 0,
+ conversionRate: s.conversionRate || s.conversion || 0,
+ };
+ }, [data]);
+
+ const campaigns = useMemo(() => {
+ const items: any[] = data?.campaigns || [];
+ return items
+ .map((c) => {
+ const status = (c.status || 'active').toLowerCase();
+ return {
+ id: c.id || '',
+ name: c.name || c.title || 'Untitled Campaign',
+ status: status.charAt(0).toUpperCase() + status.slice(1),
+ statusVariant: statusVariantMap[status] || 'draft',
+ commissionType: c.commissionType || c.type || 'Percentage',
+ earnings: formatCurrency(c.earnings || c.totalEarnings || c.revenue || 0),
+ affiliates: c.affiliateCount || c.affiliates || 0,
+ };
+ })
+ .filter((c) => {
+ if (!search) return true;
+ const q = search.toLowerCase();
+ return c.name.toLowerCase().includes(q) || c.commissionType.toLowerCase().includes(q);
+ });
+ }, [data, search]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ return (
+
+
+
+
+
+
+
+
+
+
Campaigns
+
+ setSearch(e.target.value)}
+ />
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/affiliate-dashboard/index.html b/src/ui/react-app/src/apps/affiliate-dashboard/index.html
new file mode 100644
index 0000000..2a20704
--- /dev/null
+++ b/src/ui/react-app/src/apps/affiliate-dashboard/index.html
@@ -0,0 +1,5 @@
+
+
+Affiliate Dashboard
+
+
diff --git a/src/ui/react-app/src/apps/affiliate-dashboard/main.tsx b/src/ui/react-app/src/apps/affiliate-dashboard/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/affiliate-dashboard/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/affiliate-dashboard/vite.config.ts b/src/ui/react-app/src/apps/affiliate-dashboard/vite.config.ts
new file mode 100644
index 0000000..107fdba
--- /dev/null
+++ b/src/ui/react-app/src/apps/affiliate-dashboard/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/affiliate-dashboard'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/agent-stats/App.tsx b/src/ui/react-app/src/apps/agent-stats/App.tsx
new file mode 100644
index 0000000..91bbde8
--- /dev/null
+++ b/src/ui/react-app/src/apps/agent-stats/App.tsx
@@ -0,0 +1,226 @@
+/**
+ * Agent Stats — User/agent performance metrics dashboard.
+ * Shows calls made, emails sent, tasks completed, appointments booked.
+ * Line chart: activity over time. Bar chart: performance by metric.
+ * Table: detailed activity log.
+ */
+import React, { useMemo } from "react";
+import { useApp } from "@modelcontextprotocol/ext-apps/react";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import { PageHeader } from "../../components/layout/PageHeader.js";
+import { StatsGrid } from "../../components/layout/StatsGrid.js";
+import { Section } from "../../components/layout/Section.js";
+import { MetricCard } from "../../components/data/MetricCard.js";
+import { LineChart } from "../../components/charts/LineChart.js";
+import { BarChart } from "../../components/charts/BarChart.js";
+import { DataTable } from "../../components/data/DataTable.js";
+import { SparklineChart } from "../../components/charts/SparklineChart.js";
+import "../../styles/base.css";
+import "../../styles/interactive.css";
+
+// ─── Types ──────────────────────────────────────────────────
+
+interface ActivityEntry {
+ date: string;
+ type: string;
+ description: string;
+ contact?: string;
+ outcome?: string;
+}
+
+interface LocationData {
+ name?: string;
+ id?: string;
+}
+
+interface AgentStatsData {
+ userId?: string;
+ location: LocationData;
+ dateRange: string;
+ callsMade?: number;
+ emailsSent?: number;
+ tasksCompleted?: number;
+ appointmentsBooked?: number;
+ activityLog?: ActivityEntry[];
+ activityOverTime?: { label: string; value: number }[];
+}
+
+// ─── Data Extraction ────────────────────────────────────────
+
+function extractData(result: CallToolResult): AgentStatsData | null {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc as AgentStatsData;
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === "text") {
+ try {
+ return JSON.parse(item.text) as AgentStatsData;
+ } catch {
+ /* skip */
+ }
+ }
+ }
+ }
+ return null;
+}
+
+// ─── App ────────────────────────────────────────────────────
+
+export function App() {
+ const [data, setData] = React.useState(null);
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: "agent-stats", version: "1.0.0" },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const extracted = extractData(result);
+ if (extracted) setData(extracted);
+ };
+ },
+ });
+
+ // Check for pre-injected data
+ React.useEffect(() => {
+ const preInjected = (window as any).__MCP_APP_DATA__;
+ if (preInjected && !data) setData(preInjected as AgentStatsData);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ // Derived data
+ const callsMade = data?.callsMade ?? 0;
+ const emailsSent = data?.emailsSent ?? 0;
+ const tasksCompleted = data?.tasksCompleted ?? 0;
+ const appointmentsBooked = data?.appointmentsBooked ?? 0;
+
+ const performanceBars = useMemo(() => [
+ { label: "Calls", value: callsMade, color: "#4f46e5" },
+ { label: "Emails", value: emailsSent, color: "#7c3aed" },
+ { label: "Tasks", value: tasksCompleted, color: "#16a34a" },
+ { label: "Appts", value: appointmentsBooked, color: "#3b82f6" },
+ ], [callsMade, emailsSent, tasksCompleted, appointmentsBooked]);
+
+ const activityPoints = useMemo(
+ () => data?.activityOverTime ?? [],
+ [data?.activityOverTime],
+ );
+
+ const activityLog = useMemo(() => data?.activityLog ?? [], [data?.activityLog]);
+
+ const tableColumns = useMemo(() => [
+ { key: "date", label: "Date", sortable: true, format: "date" },
+ { key: "type", label: "Type", sortable: true },
+ { key: "description", label: "Description" },
+ { key: "contact", label: "Contact" },
+ { key: "outcome", label: "Outcome" },
+ ], []);
+
+ // Sparkline values from activity over time
+ const sparklineValues = useMemo(
+ () => activityPoints.map((p) => p.value),
+ [activityPoints],
+ );
+
+ if (error) {
+ return (
+
+
Connection Error
+
{error.message}
+
+ );
+ }
+
+ if (!data) {
+ return (
+
+
+
{isConnected ? "Waiting for data..." : "Connecting..."}
+
+ );
+ }
+
+ const totalActivities = callsMade + emailsSent + tasksCompleted + appointmentsBooked;
+
+ return (
+
+
+
+
+ 0 ? "up" : "flat"}
+ trendValue={`${callsMade}`}
+ />
+ 0 ? "up" : "flat"}
+ trendValue={`${emailsSent}`}
+ />
+ 0 ? "up" : "flat"}
+ trendValue={`${tasksCompleted}`}
+ />
+ 0 ? "up" : "flat"}
+ trendValue={`${appointmentsBooked}`}
+ />
+
+
+ {sparklineValues.length > 1 && (
+
+ Trend:
+
+
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/agent-stats/index.html b/src/ui/react-app/src/apps/agent-stats/index.html
new file mode 100644
index 0000000..24bf1fa
--- /dev/null
+++ b/src/ui/react-app/src/apps/agent-stats/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Agent Stats
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/agent-stats/main.tsx b/src/ui/react-app/src/apps/agent-stats/main.tsx
new file mode 100644
index 0000000..c3e2da2
--- /dev/null
+++ b/src/ui/react-app/src/apps/agent-stats/main.tsx
@@ -0,0 +1,9 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { App } from "./App.js";
+
+createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
diff --git a/src/ui/react-app/src/apps/agent-stats/vite.config.ts b/src/ui/react-app/src/apps/agent-stats/vite.config.ts
new file mode 100644
index 0000000..06c1fa0
--- /dev/null
+++ b/src/ui/react-app/src/apps/agent-stats/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/agent-stats'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/appointment-booker/App.tsx b/src/ui/react-app/src/apps/appointment-booker/App.tsx
new file mode 100644
index 0000000..9ad46b9
--- /dev/null
+++ b/src/ui/react-app/src/apps/appointment-booker/App.tsx
@@ -0,0 +1,93 @@
+import React, { useState } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { Card } from '../../components/layout/Card';
+import { AppointmentBooker } from '../../components/interactive/AppointmentBooker';
+import { MCPAppProvider } from '../../context/MCPAppContext';
+import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [appInstance, setAppInstance] = useState(null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Appointment Booker', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ setAppInstance(app);
+ app.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ const calendar = data?.calendar;
+ const contact = data?.contact;
+ const locationId = data?.locationId;
+ const slots = data?.slots;
+
+ if (error) {
+ return ;
+ }
+ if (!isConnected && !data) {
+ return ;
+ }
+ if (!data) {
+ return Waiting for calendar data...
;
+ }
+
+ return (
+
+
+
+
+
+ {/* Contact info card if pre-selected */}
+ {contact && (
+
+
+
+ {contact.name || [contact.firstName, contact.lastName].filter(Boolean).join(' ')}
+
+ {contact.email &&
📧 {contact.email}
}
+ {contact.phone &&
📞 {contact.phone}
}
+
+
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/appointment-booker/index.html b/src/ui/react-app/src/apps/appointment-booker/index.html
new file mode 100644
index 0000000..60e2a96
--- /dev/null
+++ b/src/ui/react-app/src/apps/appointment-booker/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/appointment-booker/main.tsx b/src/ui/react-app/src/apps/appointment-booker/main.tsx
new file mode 100644
index 0000000..8ebb093
--- /dev/null
+++ b/src/ui/react-app/src/apps/appointment-booker/main.tsx
@@ -0,0 +1,9 @@
+import React, { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.js';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/src/ui/react-app/src/apps/appointment-booker/vite.config.ts b/src/ui/react-app/src/apps/appointment-booker/vite.config.ts
new file mode 100644
index 0000000..113488f
--- /dev/null
+++ b/src/ui/react-app/src/apps/appointment-booker/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/appointment-booker'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/appointment-detail/App.tsx b/src/ui/react-app/src/apps/appointment-detail/App.tsx
new file mode 100644
index 0000000..2b041f3
--- /dev/null
+++ b/src/ui/react-app/src/apps/appointment-detail/App.tsx
@@ -0,0 +1,177 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { DetailHeader } from '../../components/data/DetailHeader';
+import { KeyValueList } from '../../components/data/KeyValueList';
+import { Timeline } from '../../components/data/Timeline';
+import { Card } from '../../components/layout/Card';
+import { ActionBar } from '../../components/shared/ActionBar';
+import { ActionButton } from '../../components/shared/ActionButton';
+import { MCPAppProvider } from '../../context/MCPAppContext';
+import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
+import type { KeyValueItem, TimelineEvent, StatusVariant } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function formatDate(d?: string): string {
+ if (!d) return '—';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+const statusMap: Record = {
+ confirmed: 'active',
+ booked: 'active',
+ completed: 'complete',
+ cancelled: 'error',
+ no_show: 'error',
+ pending: 'pending',
+ rescheduled: 'paused',
+};
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [appInstance, setAppInstance] = useState(null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Appointment Detail', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ setAppInstance(app);
+ app.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ const appt = data?.appointment;
+ const notes: any[] = data?.notes || [];
+
+ const kvItems: KeyValueItem[] = useMemo(() => {
+ if (!appt) return [];
+ const items: KeyValueItem[] = [];
+
+ if (appt.title || appt.name) items.push({ label: 'Title', value: appt.title || appt.name });
+
+ // Date/time
+ if (appt.startTime) {
+ const start = new Date(appt.startTime);
+ items.push({ label: 'Date', value: start.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) });
+ items.push({ label: 'Time', value: start.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }) });
+ }
+
+ if (appt.endTime && appt.startTime) {
+ const start = new Date(appt.startTime).getTime();
+ const end = new Date(appt.endTime).getTime();
+ const mins = Math.round((end - start) / 60000);
+ items.push({ label: 'Duration', value: mins >= 60 ? `${Math.floor(mins / 60)}h ${mins % 60}m` : `${mins}m` });
+ }
+
+ // Contact
+ const contactName = appt.contactName || appt.contact?.name ||
+ [appt.contact?.firstName, appt.contact?.lastName].filter(Boolean).join(' ');
+ if (contactName) items.push({ label: 'Contact', value: contactName, bold: true });
+ if (appt.contact?.email || appt.email) items.push({ label: 'Email', value: appt.contact?.email || appt.email });
+ if (appt.contact?.phone || appt.phone) items.push({ label: 'Phone', value: appt.contact?.phone || appt.phone });
+
+ // Status
+ if (appt.status || appt.appointmentStatus) {
+ const s = appt.status || appt.appointmentStatus;
+ items.push({ label: 'Status', value: s.charAt(0).toUpperCase() + s.slice(1), variant: statusMap[s.toLowerCase()] === 'error' ? 'danger' : statusMap[s.toLowerCase()] === 'active' ? 'success' : undefined });
+ }
+
+ // Calendar / location
+ if (appt.calendarName || appt.calendar?.name) items.push({ label: 'Calendar', value: appt.calendarName || appt.calendar?.name });
+ if (appt.locationName || appt.location) items.push({ label: 'Location', value: appt.locationName || appt.location });
+
+ return items;
+ }, [appt]);
+
+ const timelineEvents: TimelineEvent[] = useMemo(() => {
+ return notes.map((note) => ({
+ title: note.title || 'Note',
+ description: note.body || note.content || note.text || '',
+ timestamp: note.dateAdded || note.createdAt || note.date || '',
+ icon: 'note' as const,
+ variant: 'default' as const,
+ }));
+ }, [notes]);
+
+ if (error) {
+ return ;
+ }
+ if (!isConnected && !data) {
+ return ;
+ }
+ if (!data) {
+ return Waiting for appointment data...
;
+ }
+
+ const status = appt?.status || appt?.appointmentStatus || '';
+ const statusVariant = statusMap[status.toLowerCase()] || 'active';
+
+ return (
+
+
+
+
+
+
+
+
+
+ {notes.length > 0 && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/appointment-detail/index.html b/src/ui/react-app/src/apps/appointment-detail/index.html
new file mode 100644
index 0000000..60e2a96
--- /dev/null
+++ b/src/ui/react-app/src/apps/appointment-detail/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/appointment-detail/main.tsx b/src/ui/react-app/src/apps/appointment-detail/main.tsx
new file mode 100644
index 0000000..8ebb093
--- /dev/null
+++ b/src/ui/react-app/src/apps/appointment-detail/main.tsx
@@ -0,0 +1,9 @@
+import React, { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.js';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/src/ui/react-app/src/apps/appointment-detail/vite.config.ts b/src/ui/react-app/src/apps/appointment-detail/vite.config.ts
new file mode 100644
index 0000000..915917b
--- /dev/null
+++ b/src/ui/react-app/src/apps/appointment-detail/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/appointment-detail'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/blog-manager/App.tsx b/src/ui/react-app/src/apps/blog-manager/App.tsx
new file mode 100644
index 0000000..6d1c161
--- /dev/null
+++ b/src/ui/react-app/src/apps/blog-manager/App.tsx
@@ -0,0 +1,320 @@
+/**
+ * blog-manager — Blog posts table with search and filters.
+ * Shows posts with title, author, status, site, dates. Client-side filtering and search.
+ */
+import React, { useState, useEffect, useMemo, useCallback } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { MCPAppProvider } from '../../context/MCPAppContext';
+import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { DataTable } from '../../components/data/DataTable';
+import { StatusBadge } from '../../components/data/StatusBadge';
+import { SearchBar } from '../../components/shared/SearchBar';
+import { FilterChips } from '../../components/shared/FilterChips';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+// ─── Types ──────────────────────────────────────────────────
+
+interface BlogPost {
+ id?: string;
+ title?: string;
+ author?: string;
+ status?: string;
+ site?: string;
+ siteId?: string;
+ publishedAt?: string;
+ updatedAt?: string;
+ slug?: string;
+}
+
+interface BlogSite {
+ id: string;
+ name: string;
+}
+
+interface BlogData {
+ posts: BlogPost[];
+ sites?: BlogSite[];
+}
+
+// ─── Extract data from tool result ──────────────────────────
+
+function extractData(result: CallToolResult): BlogData | null {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc as BlogData;
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text) as BlogData; } catch { /* skip */ }
+ }
+ }
+ }
+ return null;
+}
+
+// ─── App ────────────────────────────────────────────────────
+
+export function App() {
+ const [data, setData] = useState(null);
+
+ useEffect(() => {
+ const d = (window as any).__MCP_APP_DATA__;
+ if (d && !data) setData(d as BlogData);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: 'blog-manager', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (createdApp) => {
+ createdApp.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ if (error) {
+ return Connection Error
{error.message}
;
+ }
+ if (!isConnected || !app) {
+ return ;
+ }
+ if (!data) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+// ─── View ───────────────────────────────────────────────────
+
+function BlogManagerView({ data, app }: { data: BlogData; app: any }) {
+ const { posts, sites = [] } = data;
+ const [searchQuery, setSearchQuery] = useState('');
+ const [statusFilter, setStatusFilter] = useState>(new Set());
+ const [siteFilter, setSiteFilter] = useState>(new Set());
+ const [authorFilter, setAuthorFilter] = useState>(new Set());
+ const [actionResult, setActionResult] = useState<{ type: 'success' | 'error'; msg: string } | null>(null);
+ const [isActing, setIsActing] = useState(false);
+
+ const handleBlogAction = useCallback(async (action: string, blogData: Record) => {
+ if (!app) return;
+ setIsActing(true);
+ setActionResult(null);
+ try {
+ await app.updateModelContext({
+ content: [{
+ type: 'text',
+ text: JSON.stringify({ action, data: blogData }),
+ }],
+ });
+ setActionResult({ type: 'success', msg: `✓ ${action.replace('_', ' ')} request sent` });
+ setTimeout(() => setActionResult(null), 3000);
+ } catch {
+ setActionResult({ type: 'error', msg: '✗ Failed to send request' });
+ } finally {
+ setIsActing(false);
+ }
+ }, [app]);
+
+ // Derive unique values for filters
+ const uniqueStatuses = useMemo(() => {
+ const set = new Set();
+ posts.forEach(p => { if (p.status) set.add(p.status); });
+ return Array.from(set);
+ }, [posts]);
+
+ const uniqueSites = useMemo(() => {
+ const set = new Set();
+ posts.forEach(p => { if (p.site) set.add(p.site); });
+ return Array.from(set);
+ }, [posts]);
+
+ const uniqueAuthors = useMemo(() => {
+ const set = new Set();
+ posts.forEach(p => { if (p.author) set.add(p.author); });
+ return Array.from(set);
+ }, [posts]);
+
+ // Apply client-side filters and search
+ const filteredPosts = useMemo(() => {
+ return posts.filter(p => {
+ // Status filter
+ if (statusFilter.size > 0 && p.status && !statusFilter.has(p.status)) return false;
+ // Site filter
+ if (siteFilter.size > 0 && p.site && !siteFilter.has(p.site)) return false;
+ // Author filter
+ if (authorFilter.size > 0 && p.author && !authorFilter.has(p.author)) return false;
+ // Search query
+ if (searchQuery.trim()) {
+ const q = searchQuery.toLowerCase();
+ const matches = [p.title, p.author, p.site, p.slug]
+ .filter(Boolean)
+ .some(v => v!.toLowerCase().includes(q));
+ if (!matches) return false;
+ }
+ return true;
+ });
+ }, [posts, statusFilter, siteFilter, authorFilter, searchQuery]);
+
+ const columns = useMemo(() => [
+ { key: 'title', label: 'Title', sortable: true },
+ { key: 'author', label: 'Author', sortable: true },
+ { key: 'status', label: 'Status', format: 'status' as const, sortable: true },
+ { key: 'site', label: 'Site', sortable: true },
+ { key: 'publishedAt', label: 'Published', format: 'date' as const, sortable: true },
+ { key: 'updatedAt', label: 'Updated', format: 'date' as const, sortable: true },
+ ], []);
+
+ const rows = filteredPosts.map((p, i) => ({
+ id: p.id || String(i),
+ title: p.title || 'Untitled',
+ author: p.author || '—',
+ status: p.status || 'draft',
+ site: p.site || '—',
+ publishedAt: p.publishedAt || '—',
+ updatedAt: p.updatedAt || '—',
+ }));
+
+ const publishedCount = posts.filter(p => p.status === 'published').length;
+ const draftCount = posts.filter(p => p.status === 'draft').length;
+
+ return (
+ <>
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+
+ {uniqueStatuses.length > 0 && (
+
+ {uniqueStatuses.map(s => (
+
+ ))}
+
+ )}
+ {uniqueSites.length > 1 && (
+
+ {uniqueSites.map(s => (
+
+ ))}
+
+ )}
+ {uniqueAuthors.length > 1 && (
+
+ {uniqueAuthors.map(a => (
+
+ ))}
+
+ )}
+
+
+
+
+ {/* Blog post actions */}
+ {filteredPosts.length > 0 && (
+
+ {actionResult && (
+
+ {actionResult.msg}
+
+ )}
+
+ Actions:
+ {filteredPosts.slice(0, 5).map((p, i) => (
+
+ {p.status === 'draft' && (
+
+ )}
+
+
+ ))}
+
+
+ )}
+ >
+ );
+}
diff --git a/src/ui/react-app/src/apps/blog-manager/index.html b/src/ui/react-app/src/apps/blog-manager/index.html
new file mode 100644
index 0000000..60e2a96
--- /dev/null
+++ b/src/ui/react-app/src/apps/blog-manager/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/blog-manager/main.tsx b/src/ui/react-app/src/apps/blog-manager/main.tsx
new file mode 100644
index 0000000..fbe1cf6
--- /dev/null
+++ b/src/ui/react-app/src/apps/blog-manager/main.tsx
@@ -0,0 +1,9 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.js';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/src/ui/react-app/src/apps/blog-manager/vite.config.ts b/src/ui/react-app/src/apps/blog-manager/vite.config.ts
new file mode 100644
index 0000000..62278f4
--- /dev/null
+++ b/src/ui/react-app/src/apps/blog-manager/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/blog-manager'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/calendar-resources/App.tsx b/src/ui/react-app/src/apps/calendar-resources/App.tsx
new file mode 100644
index 0000000..250cfc3
--- /dev/null
+++ b/src/ui/react-app/src/apps/calendar-resources/App.tsx
@@ -0,0 +1,91 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { DataTable } from '../../components/data/DataTable';
+import type { TableColumn, TableRow } from '../../types';
+import '../../styles/base.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+const COLUMNS: TableColumn[] = [
+ { key: 'name', label: 'Name', sortable: true },
+ { key: 'type', label: 'Type', sortable: true },
+ { key: 'description', label: 'Description' },
+ { key: 'availability', label: 'Availability', sortable: true, format: 'status' },
+];
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Calendar Resources', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ const resources: any[] = data?.resources || [];
+
+ const rows: TableRow[] = useMemo(() => {
+ return resources.map((r) => ({
+ id: r.id || r.resourceId || '',
+ name: r.name || r.title || 'Unnamed',
+ type: r.type || r.resourceType || r.category || '—',
+ description: r.description || r.details || '—',
+ availability: r.isAvailable !== undefined
+ ? (r.isAvailable ? 'Available' : 'Unavailable')
+ : r.availability || r.status || 'Unknown',
+ }));
+ }, [resources]);
+
+ // Stats
+ const available = rows.filter((r) => String(r.availability).toLowerCase() === 'available').length;
+ const unavailable = rows.filter((r) => String(r.availability).toLowerCase() === 'unavailable').length;
+
+ if (error) {
+ return ;
+ }
+ if (!isConnected && !data) {
+ return ;
+ }
+ if (!data) {
+ return Waiting for resource data...
;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/calendar-resources/index.html b/src/ui/react-app/src/apps/calendar-resources/index.html
new file mode 100644
index 0000000..60e2a96
--- /dev/null
+++ b/src/ui/react-app/src/apps/calendar-resources/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/calendar-resources/main.tsx b/src/ui/react-app/src/apps/calendar-resources/main.tsx
new file mode 100644
index 0000000..8ebb093
--- /dev/null
+++ b/src/ui/react-app/src/apps/calendar-resources/main.tsx
@@ -0,0 +1,9 @@
+import React, { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.js';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/src/ui/react-app/src/apps/calendar-resources/vite.config.ts b/src/ui/react-app/src/apps/calendar-resources/vite.config.ts
new file mode 100644
index 0000000..8dde2a1
--- /dev/null
+++ b/src/ui/react-app/src/apps/calendar-resources/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/calendar-resources'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/calendar-view/App.tsx b/src/ui/react-app/src/apps/calendar-view/App.tsx
new file mode 100644
index 0000000..b39b49c
--- /dev/null
+++ b/src/ui/react-app/src/apps/calendar-view/App.tsx
@@ -0,0 +1,208 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { Card } from '../../components/layout/Card';
+import { CalendarView } from '../../components/viz/CalendarView';
+import type { CalendarEvent } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+const EVENT_TYPE_COLORS: Record = {
+ meeting: '#4f46e5',
+ call: '#059669',
+ task: '#d97706',
+ deadline: '#dc2626',
+ event: '#7c3aed',
+ appointment: '#0891b2',
+ reminder: '#ec4899',
+};
+
+const ALL_TYPES = Object.keys(EVENT_TYPE_COLORS);
+
+function formatDate(d?: string): string {
+ if (!d) return '\u2014';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [activeFilters, setActiveFilters] = useState>(new Set());
+ const [selectedDate, setSelectedDate] = useState(null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Calendar View', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ const calendar = data?.calendar;
+ const events: any[] = data?.events || [];
+
+ // Map events to CalendarEvent format with color coding
+ const calendarEvents: CalendarEvent[] = useMemo(() => {
+ return events.map((evt) => {
+ const eventType = (evt.type || evt.appointmentStatus || 'event').toLowerCase();
+ return {
+ title: evt.title || evt.name || evt.subject || 'Untitled',
+ date: evt.startTime || evt.date || evt.dateAdded || '',
+ time: evt.startTime
+ ? new Date(evt.startTime).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })
+ : evt.time || '',
+ type: eventType,
+ color: EVENT_TYPE_COLORS[eventType] || '#4f46e5',
+ };
+ });
+ }, [events]);
+
+ // Filter events by active types
+ const filteredEvents = useMemo(() => {
+ if (activeFilters.size === 0) return calendarEvents;
+ return calendarEvents.filter((e) => activeFilters.has(e.type || 'event'));
+ }, [calendarEvents, activeFilters]);
+
+ // Determine present event types
+ const presentTypes = useMemo(() => {
+ const types = new Set();
+ for (const e of calendarEvents) {
+ types.add(e.type || 'event');
+ }
+ return Array.from(types);
+ }, [calendarEvents]);
+
+ // Events for selected date
+ const selectedDateEvents = useMemo(() => {
+ if (!selectedDate) return [];
+ return filteredEvents.filter((e) => {
+ try {
+ const eDate = new Date(e.date).toISOString().split('T')[0];
+ return eDate === selectedDate;
+ } catch {
+ return false;
+ }
+ });
+ }, [filteredEvents, selectedDate]);
+
+ const toggleFilter = (type: string) => {
+ setActiveFilters((prev) => {
+ const next = new Set(prev);
+ if (next.has(type)) next.delete(type);
+ else next.add(type);
+ return next;
+ });
+ };
+
+ if (error) {
+ return ;
+ }
+ if (!isConnected && !data) {
+ return ;
+ }
+ if (!data) {
+ return Waiting for calendar data...
;
+ }
+
+ return (
+
+
+
+ {/* Filter chips */}
+ {presentTypes.length > 1 && (
+
+
+ {presentTypes.map((type) => (
+
+ ))}
+
+
+ )}
+
+
+ setSelectedDate(date === selectedDate ? null : date)}
+ />
+
+
+ {/* Selected date detail */}
+ {selectedDate && (
+
+ {selectedDateEvents.length === 0 ? (
+
+ ) : (
+
+ {selectedDateEvents.map((evt, i) => (
+
+ {evt.time || '—'}
+ {evt.title}
+
+ {evt.type}
+
+
+ ))}
+
+ )}
+
+ )}
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/calendar-view/index.html b/src/ui/react-app/src/apps/calendar-view/index.html
new file mode 100644
index 0000000..60e2a96
--- /dev/null
+++ b/src/ui/react-app/src/apps/calendar-view/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/calendar-view/main.tsx b/src/ui/react-app/src/apps/calendar-view/main.tsx
new file mode 100644
index 0000000..8ebb093
--- /dev/null
+++ b/src/ui/react-app/src/apps/calendar-view/main.tsx
@@ -0,0 +1,9 @@
+import React, { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.js';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/src/ui/react-app/src/apps/calendar-view/vite.config.ts b/src/ui/react-app/src/apps/calendar-view/vite.config.ts
new file mode 100644
index 0000000..883d3b3
--- /dev/null
+++ b/src/ui/react-app/src/apps/calendar-view/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/calendar-view'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/call-detail/App.tsx b/src/ui/react-app/src/apps/call-detail/App.tsx
new file mode 100644
index 0000000..84f5899
--- /dev/null
+++ b/src/ui/react-app/src/apps/call-detail/App.tsx
@@ -0,0 +1,141 @@
+import React, { useState } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { DetailHeader } from '../../components/data/DetailHeader';
+import { KeyValueList } from '../../components/data/KeyValueList';
+import { Card } from '../../components/layout/Card';
+import { AudioPlayer } from '../../components/data/AudioPlayer';
+import { TranscriptView } from '../../components/comms/TranscriptView';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatDuration(seconds: number | string | undefined): string {
+ if (!seconds) return '0:00';
+ const s = typeof seconds === 'string' ? parseInt(seconds, 10) : seconds;
+ if (isNaN(s)) return '0:00';
+ const m = Math.floor(s / 60);
+ const sec = s % 60;
+ return `${m}:${sec.toString().padStart(2, '0')}`;
+}
+
+function formatDate(d: string | undefined): string {
+ if (!d) return '-';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ weekday: 'short', month: 'short', day: 'numeric', year: 'numeric',
+ hour: 'numeric', minute: '2-digit',
+ });
+ } catch { return d; }
+}
+
+const statusVariantMap: Record = {
+ completed: 'complete',
+ missed: 'error',
+ voicemail: 'pending',
+ busy: 'paused',
+ 'no-answer': 'draft',
+ failed: 'error',
+ active: 'active',
+};
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Call Detail', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const call = data?.call || data || {};
+ const contactName = call.contactName || call.contact?.name || 'Unknown Contact';
+ const status = (call.status || 'completed').toLowerCase();
+ const direction = (call.direction || 'inbound').toLowerCase();
+ const variant = statusVariantMap[status] || 'draft';
+
+ const metadataItems = [
+ { label: 'Direction', value: direction.charAt(0).toUpperCase() + direction.slice(1), bold: true },
+ { label: 'From', value: call.from || '-' },
+ { label: 'To', value: call.to || '-' },
+ { label: 'Duration', value: formatDuration(call.duration), bold: true },
+ { label: 'Date', value: formatDate(call.date || call.createdAt || call.startedAt) },
+ { label: 'Status', value: status.charAt(0).toUpperCase() + status.slice(1) },
+ ];
+
+ if (call.source) metadataItems.push({ label: 'Source', value: call.source, bold: false });
+ if (call.assignedTo) metadataItems.push({ label: 'Assigned To', value: call.assignedTo, bold: false });
+
+ const hasRecording = !!(call.recordingUrl || call.recording);
+ const transcript = call.transcript || call.transcription || [];
+ const transcriptEntries = Array.isArray(transcript) ? transcript : [];
+
+ return (
+
+
+
+
+
+
+
+
+ {hasRecording && (
+
+
+
+ )}
+
+ {transcriptEntries.length > 0 ? (
+
+ ({
+ speaker: e.speaker || e.speakerName || 'Unknown',
+ speakerRole: e.speakerRole || e.role || 'customer',
+ text: e.text || e.content || '',
+ timestamp: e.timestamp || e.time || '',
+ }))}
+ title={`Call Transcript`}
+ duration={formatDuration(call.duration)}
+ />
+
+ ) : (
+
+
+
📝
+
No transcript available for this call
+
+
+ )}
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/call-detail/index.html b/src/ui/react-app/src/apps/call-detail/index.html
new file mode 100644
index 0000000..17f59f3
--- /dev/null
+++ b/src/ui/react-app/src/apps/call-detail/index.html
@@ -0,0 +1,5 @@
+
+
+Call Detail
+
+
diff --git a/src/ui/react-app/src/apps/call-detail/main.tsx b/src/ui/react-app/src/apps/call-detail/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/call-detail/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/call-detail/vite.config.ts b/src/ui/react-app/src/apps/call-detail/vite.config.ts
new file mode 100644
index 0000000..a9a9feb
--- /dev/null
+++ b/src/ui/react-app/src/apps/call-detail/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/call-detail'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/call-log/App.tsx b/src/ui/react-app/src/apps/call-log/App.tsx
new file mode 100644
index 0000000..6435970
--- /dev/null
+++ b/src/ui/react-app/src/apps/call-log/App.tsx
@@ -0,0 +1,146 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { DataTable } from '../../components/data/DataTable';
+import { StatusBadge } from '../../components/data/StatusBadge';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+type FilterType = 'all' | 'inbound' | 'outbound' | 'missed';
+
+const filterOptions: { label: string; value: FilterType }[] = [
+ { label: 'All', value: 'all' },
+ { label: 'Inbound', value: 'inbound' },
+ { label: 'Outbound', value: 'outbound' },
+ { label: 'Missed', value: 'missed' },
+];
+
+const statusMap: Record = {
+ completed: { label: 'Completed', variant: 'complete' },
+ missed: { label: 'Missed', variant: 'error' },
+ voicemail: { label: 'Voicemail', variant: 'pending' },
+ busy: { label: 'Busy', variant: 'paused' },
+ 'no-answer': { label: 'No Answer', variant: 'draft' },
+ failed: { label: 'Failed', variant: 'error' },
+};
+
+function formatDuration(seconds: number | string | undefined): string {
+ if (!seconds) return '0:00';
+ const s = typeof seconds === 'string' ? parseInt(seconds, 10) : seconds;
+ if (isNaN(s)) return '0:00';
+ const m = Math.floor(s / 60);
+ const sec = s % 60;
+ return `${m}:${sec.toString().padStart(2, '0')}`;
+}
+
+function formatDate(d: string | undefined): string {
+ if (!d) return '-';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit',
+ });
+ } catch { return d; }
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [search, setSearch] = useState('');
+ const [filter, setFilter] = useState('all');
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Call Log', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const rows = useMemo(() => {
+ const calls: any[] = data?.calls || data?.data || [];
+ return calls
+ .map((c) => {
+ const status = (c.status || 'completed').toLowerCase();
+ const direction = (c.direction || 'inbound').toLowerCase();
+ const st = statusMap[status] || { label: status, variant: 'draft' };
+ return {
+ id: c.id || '',
+ contactName: c.contactName || c.contact?.name || 'Unknown',
+ phone: c.phone || c.to || c.from || '-',
+ direction: direction.charAt(0).toUpperCase() + direction.slice(1),
+ directionRaw: direction,
+ duration: formatDuration(c.duration),
+ status: st.label,
+ statusVariant: st.variant,
+ date: formatDate(c.date || c.createdAt || c.startedAt),
+ };
+ })
+ .filter((r) => {
+ if (filter === 'inbound' && r.directionRaw !== 'inbound') return false;
+ if (filter === 'outbound' && r.directionRaw !== 'outbound') return false;
+ if (filter === 'missed' && r.statusVariant !== 'error') return false;
+ if (!search) return true;
+ const q = search.toLowerCase();
+ return r.contactName.toLowerCase().includes(q) || r.phone.includes(q);
+ });
+ }, [data, search, filter]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ return (
+
+
+
+ {filterOptions.map((f) => (
+
+ ))}
+
+
+ setSearch(e.target.value)}
+ />
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/call-log/index.html b/src/ui/react-app/src/apps/call-log/index.html
new file mode 100644
index 0000000..ae84184
--- /dev/null
+++ b/src/ui/react-app/src/apps/call-log/index.html
@@ -0,0 +1,5 @@
+
+
+Call Log
+
+
diff --git a/src/ui/react-app/src/apps/call-log/main.tsx b/src/ui/react-app/src/apps/call-log/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/call-log/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/call-log/vite.config.ts b/src/ui/react-app/src/apps/call-log/vite.config.ts
new file mode 100644
index 0000000..c224aeb
--- /dev/null
+++ b/src/ui/react-app/src/apps/call-log/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/call-log'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/campaign-stats/App.tsx b/src/ui/react-app/src/apps/campaign-stats/App.tsx
new file mode 100644
index 0000000..6fd455a
--- /dev/null
+++ b/src/ui/react-app/src/apps/campaign-stats/App.tsx
@@ -0,0 +1,186 @@
+/**
+ * campaign-stats — Email campaign metrics dashboard.
+ * Shows campaign stats as MetricCards, performance bar chart, and campaigns table.
+ */
+import React, { useState, useEffect, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { MCPAppProvider } from '../../context/MCPAppContext';
+import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { StatsGrid } from '../../components/layout/StatsGrid';
+import { MetricCard } from '../../components/data/MetricCard';
+import { BarChart } from '../../components/charts/BarChart';
+import { DataTable } from '../../components/data/DataTable';
+import { StatusBadge } from '../../components/data/StatusBadge';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+// ─── Types ──────────────────────────────────────────────────
+
+interface Campaign {
+ id?: string;
+ name: string;
+ status?: string;
+ sent?: number;
+ delivered?: number;
+ opened?: number;
+ clicked?: number;
+ bounced?: number;
+ date?: string;
+}
+
+interface CampaignData {
+ campaign?: Campaign;
+ campaigns?: Campaign[];
+}
+
+// ─── Extract data from tool result ──────────────────────────
+
+function extractData(result: CallToolResult): CampaignData | null {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc as CampaignData;
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text) as CampaignData; } catch { /* skip */ }
+ }
+ }
+ }
+ return null;
+}
+
+// ─── App ────────────────────────────────────────────────────
+
+export function App() {
+ const [data, setData] = useState(null);
+
+ useEffect(() => {
+ const d = (window as any).__MCP_APP_DATA__;
+ if (d && !data) setData(d as CampaignData);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: 'campaign-stats', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (createdApp) => {
+ createdApp.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ if (error) {
+ return Connection Error
{error.message}
;
+ }
+ if (!isConnected || !app) {
+ return ;
+ }
+ if (!data) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+// ─── View ───────────────────────────────────────────────────
+
+function CampaignStatsView({ data }: { data: CampaignData }) {
+ const campaigns = data.campaigns || (data.campaign ? [data.campaign] : []);
+ const primary = data.campaign || campaigns[0];
+
+ // Aggregate stats across all campaigns
+ const totals = useMemo(() => {
+ const t = { sent: 0, delivered: 0, opened: 0, clicked: 0, bounced: 0 };
+ for (const c of campaigns) {
+ t.sent += c.sent || 0;
+ t.delivered += c.delivered || 0;
+ t.opened += c.opened || 0;
+ t.clicked += c.clicked || 0;
+ t.bounced += c.bounced || 0;
+ }
+ return t;
+ }, [campaigns]);
+
+ // Bar chart data: performance per campaign (or over time)
+ const barData = useMemo(() => {
+ return campaigns.slice(0, 10).map(c => ({
+ label: c.name?.slice(0, 12) || 'Campaign',
+ value: c.opened || 0,
+ color: '#4f46e5',
+ }));
+ }, [campaigns]);
+
+ // Table columns
+ const columns = useMemo(() => [
+ { key: 'name', label: 'Campaign', sortable: true },
+ { key: 'status', label: 'Status', format: 'status' as const, sortable: true },
+ { key: 'sent', label: 'Sent', sortable: true },
+ { key: 'delivered', label: 'Delivered', sortable: true },
+ { key: 'opened', label: 'Opened', sortable: true },
+ { key: 'clicked', label: 'Clicked', sortable: true },
+ { key: 'bounced', label: 'Bounced', sortable: true },
+ ], []);
+
+ const rows = campaigns.map((c, i) => ({
+ id: c.id || String(i),
+ name: c.name || 'Untitled',
+ status: c.status || 'sent',
+ sent: c.sent?.toLocaleString() || '0',
+ delivered: c.delivered?.toLocaleString() || '0',
+ opened: c.opened?.toLocaleString() || '0',
+ clicked: c.clicked?.toLocaleString() || '0',
+ bounced: c.bounced?.toLocaleString() || '0',
+ }));
+
+ const openRate = totals.sent > 0 ? ((totals.opened / totals.sent) * 100).toFixed(1) + '%' : '—';
+ const clickRate = totals.sent > 0 ? ((totals.clicked / totals.sent) * 100).toFixed(1) + '%' : '—';
+
+ return (
+ <>
+
+
+
+
+ 0 ? 'up' : 'flat'} trendValue={totals.opened.toLocaleString()} />
+ 0 ? 'up' : 'flat'} trendValue={totals.clicked.toLocaleString()} />
+
+
+ {barData.length > 1 && (
+
+
+
+ )}
+
+ {rows.length > 0 && (
+
+
+
+ )}
+ >
+ );
+}
diff --git a/src/ui/react-app/src/apps/campaign-stats/index.html b/src/ui/react-app/src/apps/campaign-stats/index.html
new file mode 100644
index 0000000..60e2a96
--- /dev/null
+++ b/src/ui/react-app/src/apps/campaign-stats/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/campaign-stats/main.tsx b/src/ui/react-app/src/apps/campaign-stats/main.tsx
new file mode 100644
index 0000000..fbe1cf6
--- /dev/null
+++ b/src/ui/react-app/src/apps/campaign-stats/main.tsx
@@ -0,0 +1,9 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.js';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/src/ui/react-app/src/apps/campaign-stats/vite.config.ts b/src/ui/react-app/src/apps/campaign-stats/vite.config.ts
new file mode 100644
index 0000000..8ea5250
--- /dev/null
+++ b/src/ui/react-app/src/apps/campaign-stats/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/campaign-stats'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/company-detail/App.tsx b/src/ui/react-app/src/apps/company-detail/App.tsx
new file mode 100644
index 0000000..6d2db71
--- /dev/null
+++ b/src/ui/react-app/src/apps/company-detail/App.tsx
@@ -0,0 +1,119 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { DetailHeader } from '../../components/data/DetailHeader';
+import { SplitLayout } from '../../components/layout/SplitLayout';
+import { KeyValueList } from '../../components/data/KeyValueList';
+import { DataTable } from '../../components/data/DataTable';
+import { Card } from '../../components/layout/Card';
+import { TagList } from '../../components/data/TagList';
+import type { KeyValueItem } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Company Detail', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const company = useMemo(() => {
+ if (!data) return null;
+ return data.company || data;
+ }, [data]);
+
+ const detailItems: KeyValueItem[] = useMemo(() => {
+ if (!company) return [];
+ return [
+ { label: 'Industry', value: company.industry || '—' },
+ { label: 'Website', value: company.website || company.domain || '—' },
+ { label: 'Phone', value: company.phone || '—' },
+ { label: 'Email', value: company.email || '—' },
+ { label: 'Address', value: [company.address1, company.city, company.state, company.postalCode, company.country].filter(Boolean).join(', ') || '—' },
+ { label: 'Description', value: company.description || '—' },
+ ];
+ }, [company]);
+
+ const contacts = useMemo(() => {
+ if (!company) return [];
+ const list: any[] = company.contacts || company.associatedContacts || [];
+ return list.map((c: any) => ({
+ id: c.id || '',
+ name: `${c.firstName || ''} ${c.lastName || ''}`.trim() || c.name || 'Unknown',
+ email: c.email || '—',
+ phone: c.phone || '—',
+ role: c.role || c.title || '—',
+ }));
+ }, [company]);
+
+ const tags = useMemo(() => {
+ if (!company) return [];
+ return (company.tags || []).map((t: any) =>
+ typeof t === 'string' ? t : t.label || t.name || String(t)
+ );
+ }, [company]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const statusLabel = company?.status || 'Active';
+ const statusVariant = (statusLabel.toLowerCase() === 'active' ? 'active' : statusLabel.toLowerCase() === 'inactive' ? 'paused' : 'draft') as any;
+
+ return (
+
+
+
+ {tags.length > 0 && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/company-detail/index.html b/src/ui/react-app/src/apps/company-detail/index.html
new file mode 100644
index 0000000..4426285
--- /dev/null
+++ b/src/ui/react-app/src/apps/company-detail/index.html
@@ -0,0 +1,5 @@
+
+
+Company Detail
+
+
diff --git a/src/ui/react-app/src/apps/company-detail/main.tsx b/src/ui/react-app/src/apps/company-detail/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/company-detail/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/company-detail/vite.config.ts b/src/ui/react-app/src/apps/company-detail/vite.config.ts
new file mode 100644
index 0000000..8eb79da
--- /dev/null
+++ b/src/ui/react-app/src/apps/company-detail/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/company-detail'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/company-list/App.tsx b/src/ui/react-app/src/apps/company-list/App.tsx
new file mode 100644
index 0000000..054001d
--- /dev/null
+++ b/src/ui/react-app/src/apps/company-list/App.tsx
@@ -0,0 +1,105 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { DataTable } from '../../components/data/DataTable';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatDate(d?: string): string {
+ if (!d) return '—';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [search, setSearch] = useState('');
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Company List', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const rows = useMemo(() => {
+ const companies: any[] = data?.companies || [];
+ return companies
+ .map((c: any) => ({
+ id: c.id || '',
+ name: c.name || 'Unnamed Company',
+ industry: c.industry || '—',
+ website: c.website || c.domain || '—',
+ contactsCount: c.contactsCount ?? c.contacts?.length ?? 0,
+ createdAt: formatDate(c.createdAt),
+ }))
+ .filter((r) => {
+ if (!search) return true;
+ const q = search.toLowerCase();
+ return (
+ r.name.toLowerCase().includes(q) ||
+ r.industry.toLowerCase().includes(q) ||
+ r.website.toLowerCase().includes(q)
+ );
+ });
+ }, [data, search]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const totalCompanies = data?.companies?.length || 0;
+
+ return (
+
+
+ setSearch(e.target.value)}
+ />
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/company-list/index.html b/src/ui/react-app/src/apps/company-list/index.html
new file mode 100644
index 0000000..5a4bdbf
--- /dev/null
+++ b/src/ui/react-app/src/apps/company-list/index.html
@@ -0,0 +1,5 @@
+
+
+Company List
+
+
diff --git a/src/ui/react-app/src/apps/company-list/main.tsx b/src/ui/react-app/src/apps/company-list/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/company-list/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/company-list/vite.config.ts b/src/ui/react-app/src/apps/company-list/vite.config.ts
new file mode 100644
index 0000000..42eda0a
--- /dev/null
+++ b/src/ui/react-app/src/apps/company-list/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/company-list'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/contact-card/App.tsx b/src/ui/react-app/src/apps/contact-card/App.tsx
new file mode 100644
index 0000000..385e33d
--- /dev/null
+++ b/src/ui/react-app/src/apps/contact-card/App.tsx
@@ -0,0 +1,125 @@
+import React from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { useState } from 'react';
+import { DetailHeader } from '../../components/data/DetailHeader';
+import { SplitLayout } from '../../components/layout/SplitLayout';
+import { Card } from '../../components/layout/Card';
+import { KeyValueList } from '../../components/data/KeyValueList';
+import { TagList } from '../../components/data/TagList';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function formatDate(d: string | undefined): string {
+ if (!d || d === '—') return '—';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ year: 'numeric', month: 'short', day: 'numeric',
+ });
+ } catch { return d; }
+}
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: 'Contact Card', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const c = data.contact || {};
+ const fullName = `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unknown';
+ const tags: string[] = c.tags || [];
+
+ const contactInfo = [
+ { label: 'Email', value: c.email || '—' },
+ { label: 'Phone', value: c.phone || '—' },
+ { label: 'Company', value: c.companyName || c.company || '—' },
+ { label: 'Address', value: [c.address1, c.city, c.state, c.postalCode, c.country].filter(Boolean).join(', ') || '—' },
+ { label: 'Source', value: c.source || '—' },
+ { label: 'Date Added', value: formatDate(c.dateAdded || c.createdAt) },
+ { label: 'Last Activity', value: formatDate(c.lastActivity) },
+ ];
+
+ const customFields = Object.entries(c.customFields || c.customField || {}).map(
+ ([key, val]) => ({ label: key, value: String(val ?? '—') })
+ );
+
+ const handleAction = async (action: string) => {
+ if (!app) return;
+ try {
+ await app.updateModelContext({
+ content: [{ type: 'text', text: `User action: ${action} for contact ${c.id || fullName}` }],
+ });
+ } catch {}
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {tags.length > 0 && (
+
+ Tags
+
+
+ )}
+
+
+ {customFields.length > 0 && (
+
+
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/contact-card/index.html b/src/ui/react-app/src/apps/contact-card/index.html
new file mode 100644
index 0000000..ca3d1a3
--- /dev/null
+++ b/src/ui/react-app/src/apps/contact-card/index.html
@@ -0,0 +1,5 @@
+
+
+Contact Card
+
+
diff --git a/src/ui/react-app/src/apps/contact-card/main.tsx b/src/ui/react-app/src/apps/contact-card/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/contact-card/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/contact-card/vite.config.ts b/src/ui/react-app/src/apps/contact-card/vite.config.ts
new file mode 100644
index 0000000..af24f43
--- /dev/null
+++ b/src/ui/react-app/src/apps/contact-card/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/contact-card'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/contact-creator/App.tsx b/src/ui/react-app/src/apps/contact-creator/App.tsx
new file mode 100644
index 0000000..b7e38c9
--- /dev/null
+++ b/src/ui/react-app/src/apps/contact-creator/App.tsx
@@ -0,0 +1,163 @@
+import React, { useState, useCallback } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { Card } from '../../components/layout/Card';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+interface FormValues {
+ firstName: string;
+ lastName: string;
+ email: string;
+ phone: string;
+ companyName: string;
+ tags: string;
+ source: string;
+}
+
+const INITIAL: FormValues = {
+ firstName: '',
+ lastName: '',
+ email: '',
+ phone: '',
+ companyName: '',
+ tags: '',
+ source: '',
+};
+
+const FIELDS: { key: keyof FormValues; label: string; type: string; required?: boolean; placeholder: string }[] = [
+ { key: 'firstName', label: 'First Name', type: 'text', required: true, placeholder: 'John' },
+ { key: 'lastName', label: 'Last Name', type: 'text', required: true, placeholder: 'Doe' },
+ { key: 'email', label: 'Email', type: 'email', placeholder: 'john@example.com' },
+ { key: 'phone', label: 'Phone', type: 'tel', placeholder: '+1 (555) 123-4567' },
+ { key: 'companyName', label: 'Company', type: 'text', placeholder: 'Acme Inc.' },
+ { key: 'tags', label: 'Tags', type: 'text', placeholder: 'Comma-separated: lead, vip, newsletter' },
+ { key: 'source', label: 'Source', type: 'text', placeholder: 'e.g. Website, Referral, Ad Campaign' },
+];
+
+export function App() {
+ const [values, setValues] = useState(INITIAL);
+ const [submitting, setSubmitting] = useState(false);
+ const [result, setResult] = useState<'success' | 'queued' | 'error' | null>(null);
+ const [resultData, setResultData] = useState(null);
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: 'Contact Creator', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (res) => {
+ const parsed = extractData(res);
+ if (parsed) setResultData(parsed);
+ };
+ },
+ });
+
+ const setValue = useCallback((key: keyof FormValues, val: string) => {
+ setValues((prev) => ({ ...prev, [key]: val }));
+ setResult(null);
+ }, []);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!values.firstName.trim() || !values.lastName.trim()) return;
+ if (!app) return;
+
+ setSubmitting(true);
+ setResult(null);
+
+ const args: Record = {
+ firstName: values.firstName.trim(),
+ lastName: values.lastName.trim(),
+ };
+ if (values.email.trim()) args.email = values.email.trim();
+ if (values.phone.trim()) args.phone = values.phone.trim();
+ if (values.companyName.trim()) args.companyName = values.companyName.trim();
+ if (values.tags.trim()) args.tags = values.tags.split(',').map((t) => t.trim()).filter(Boolean);
+ if (values.source.trim()) args.source = values.source.trim();
+
+ try {
+ await app.updateModelContext({
+ content: [{
+ type: 'text',
+ text: `User wants to create a new contact: ${JSON.stringify(args)}`,
+ }],
+ });
+ setResult('queued');
+ } catch {
+ setResult('error');
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleReset = () => {
+ setValues(INITIAL);
+ setResult(null);
+ setResultData(null);
+ };
+
+ if (error) return ;
+ if (!isConnected) return ;
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/contact-creator/index.html b/src/ui/react-app/src/apps/contact-creator/index.html
new file mode 100644
index 0000000..52af6d9
--- /dev/null
+++ b/src/ui/react-app/src/apps/contact-creator/index.html
@@ -0,0 +1,5 @@
+
+
+Contact Creator
+
+
diff --git a/src/ui/react-app/src/apps/contact-creator/main.tsx b/src/ui/react-app/src/apps/contact-creator/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/contact-creator/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/contact-creator/vite.config.ts b/src/ui/react-app/src/apps/contact-creator/vite.config.ts
new file mode 100644
index 0000000..a19c161
--- /dev/null
+++ b/src/ui/react-app/src/apps/contact-creator/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/contact-creator'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/contact-grid/App.tsx b/src/ui/react-app/src/apps/contact-grid/App.tsx
new file mode 100644
index 0000000..b0632d6
--- /dev/null
+++ b/src/ui/react-app/src/apps/contact-grid/App.tsx
@@ -0,0 +1,90 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { DataTable } from '../../components/data/DataTable';
+import { StatusBadge } from '../../components/data/StatusBadge';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [search, setSearch] = useState('');
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Contact Grid', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const rows = useMemo(() => {
+ const contacts: any[] = data?.contacts || [];
+ return contacts
+ .map((c) => ({
+ id: c.id || '',
+ name: `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unknown',
+ email: c.email || '-',
+ phone: c.phone || '-',
+ tags: c.tags || [],
+ dateAdded: c.dateAdded || c.createdAt || '-',
+ source: c.source || '-',
+ }))
+ .filter((r) => {
+ if (!search) return true;
+ const q = search.toLowerCase();
+ return (
+ r.name.toLowerCase().includes(q) ||
+ r.email.toLowerCase().includes(q) ||
+ r.phone.includes(q) ||
+ r.source.toLowerCase().includes(q)
+ );
+ });
+ }, [data, search]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ return (
+
+
+ setSearch(e.target.value)}
+ />
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/contact-grid/index.html b/src/ui/react-app/src/apps/contact-grid/index.html
new file mode 100644
index 0000000..52f1513
--- /dev/null
+++ b/src/ui/react-app/src/apps/contact-grid/index.html
@@ -0,0 +1,5 @@
+
+
+Contact Grid
+
+
diff --git a/src/ui/react-app/src/apps/contact-grid/main.tsx b/src/ui/react-app/src/apps/contact-grid/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/contact-grid/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/contact-grid/vite.config.ts b/src/ui/react-app/src/apps/contact-grid/vite.config.ts
new file mode 100644
index 0000000..d0ed94f
--- /dev/null
+++ b/src/ui/react-app/src/apps/contact-grid/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/contact-grid'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/contact-timeline/App.tsx b/src/ui/react-app/src/apps/contact-timeline/App.tsx
new file mode 100644
index 0000000..bba0b85
--- /dev/null
+++ b/src/ui/react-app/src/apps/contact-timeline/App.tsx
@@ -0,0 +1,152 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { DetailHeader } from '../../components/data/DetailHeader';
+import { Timeline } from '../../components/data/Timeline';
+import { Card } from '../../components/layout/Card';
+import type { TimelineEvent } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+type TabValue = 'all' | 'notes' | 'tasks' | 'appointments';
+
+const TABS: { label: string; value: TabValue }[] = [
+ { label: 'All', value: 'all' },
+ { label: 'Notes', value: 'notes' },
+ { label: 'Tasks', value: 'tasks' },
+ { label: 'Appointments', value: 'appointments' },
+];
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [activeTab, setActiveTab] = useState('all');
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Contact Timeline', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const events = useMemo((): TimelineEvent[] => {
+ if (!data) return [];
+
+ const items: (TimelineEvent & { _sort: number })[] = [];
+
+ const notes: any[] = data.notes || [];
+ const tasks: any[] = data.tasks || [];
+ const appointments: any[] = data.appointments || [];
+
+ if (activeTab === 'all' || activeTab === 'notes') {
+ for (const n of notes) {
+ items.push({
+ title: n.title || 'Note',
+ description: n.body || n.description || '',
+ timestamp: n.dateAdded || n.createdAt || '',
+ icon: 'note',
+ variant: 'default',
+ _sort: new Date(n.dateAdded || n.createdAt || 0).getTime(),
+ });
+ }
+ }
+
+ if (activeTab === 'all' || activeTab === 'tasks') {
+ for (const t of tasks) {
+ const isComplete = t.status === 'completed' || t.completed;
+ items.push({
+ title: t.title || 'Task',
+ description: t.description || t.body || '',
+ timestamp: t.dueDate || t.dateAdded || '',
+ icon: 'task',
+ variant: isComplete ? 'success' : 'warning',
+ _sort: new Date(t.dueDate || t.dateAdded || 0).getTime(),
+ });
+ }
+ }
+
+ if (activeTab === 'all' || activeTab === 'appointments') {
+ for (const a of appointments) {
+ items.push({
+ title: a.title || 'Appointment',
+ description: a.notes || a.description || '',
+ timestamp: a.startTime || a.date || '',
+ icon: 'meeting',
+ variant: 'default',
+ _sort: new Date(a.startTime || a.date || 0).getTime(),
+ });
+ }
+ }
+
+ return items
+ .sort((a, b) => b._sort - a._sort)
+ .map(({ _sort, ...ev }) => ev);
+ }, [data, activeTab]);
+
+ const counts = useMemo(() => {
+ if (!data) return { all: 0, notes: 0, tasks: 0, appointments: 0 };
+ return {
+ all: (data.notes?.length || 0) + (data.tasks?.length || 0) + (data.appointments?.length || 0),
+ notes: data.notes?.length || 0,
+ tasks: data.tasks?.length || 0,
+ appointments: data.appointments?.length || 0,
+ };
+ }, [data]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const c = data.contact || {};
+ const fullName = `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Contact';
+
+ return (
+
+
+
+
+ {TABS.map((t) => (
+
+ ))}
+
+
+
+ {events.length === 0 ? (
+
+
📭
+
No {activeTab === 'all' ? 'activity' : activeTab} found
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/contact-timeline/index.html b/src/ui/react-app/src/apps/contact-timeline/index.html
new file mode 100644
index 0000000..00b8c1c
--- /dev/null
+++ b/src/ui/react-app/src/apps/contact-timeline/index.html
@@ -0,0 +1,5 @@
+
+
+Contact Timeline
+
+
diff --git a/src/ui/react-app/src/apps/contact-timeline/main.tsx b/src/ui/react-app/src/apps/contact-timeline/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/contact-timeline/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/contact-timeline/vite.config.ts b/src/ui/react-app/src/apps/contact-timeline/vite.config.ts
new file mode 100644
index 0000000..e689e8d
--- /dev/null
+++ b/src/ui/react-app/src/apps/contact-timeline/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/contact-timeline'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/conversation-list/App.tsx b/src/ui/react-app/src/apps/conversation-list/App.tsx
new file mode 100644
index 0000000..b198425
--- /dev/null
+++ b/src/ui/react-app/src/apps/conversation-list/App.tsx
@@ -0,0 +1,164 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { DataTable } from '../../components/data/DataTable';
+import type { TableColumn, TableRow } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+const typeIcons: Record = {
+ sms: '💬',
+ email: '📧',
+ call: '📞',
+ whatsapp: '📱',
+ facebook: '👤',
+ instagram: '📸',
+};
+
+const COLUMNS: TableColumn[] = [
+ { key: 'contact', label: 'Contact', sortable: true, format: 'avatar' },
+ { key: 'lastMessage', label: 'Last Message' },
+ { key: 'type', label: 'Type', sortable: true },
+ { key: 'dateUpdated', label: 'Updated', sortable: true, format: 'date' },
+ { key: 'unread', label: 'Status', format: 'status' },
+];
+
+type FilterType = 'all' | 'sms' | 'email' | 'call' | 'unread';
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [activeFilter, setActiveFilter] = useState('all');
+ const [searchQuery, setSearchQuery] = useState('');
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Conversation List', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ const conversations: any[] = data?.conversations || [];
+
+ const rows: TableRow[] = useMemo(() => {
+ return conversations.map((c) => {
+ const contactName = c.contactName || c.contact?.name ||
+ [c.contact?.firstName, c.contact?.lastName].filter(Boolean).join(' ') || c.email || c.phone || 'Unknown';
+ const msgType = (c.type || c.messageType || 'sms').toLowerCase();
+ const icon = typeIcons[msgType] || '💬';
+
+ return {
+ id: c.id || c.conversationId || '',
+ contact: contactName,
+ lastMessage: c.lastMessage || c.lastMessageBody || c.snippet || '—',
+ type: `${icon} ${msgType.toUpperCase()}`,
+ dateUpdated: c.dateUpdated || c.lastMessageDate || c.updatedAt || '—',
+ unread: c.unreadCount > 0 ? `${c.unreadCount} New` : 'Read',
+ _type: msgType,
+ _unreadCount: c.unreadCount || 0,
+ };
+ });
+ }, [conversations]);
+
+ // Filter by type and search
+ const filteredRows = useMemo(() => {
+ let filtered = rows;
+ if (activeFilter === 'unread') {
+ filtered = filtered.filter((r) => r._unreadCount > 0);
+ } else if (activeFilter !== 'all') {
+ filtered = filtered.filter((r) => r._type === activeFilter);
+ }
+ if (searchQuery.trim()) {
+ const q = searchQuery.toLowerCase();
+ filtered = filtered.filter(
+ (r) =>
+ String(r.contact).toLowerCase().includes(q) ||
+ String(r.lastMessage).toLowerCase().includes(q),
+ );
+ }
+ return filtered;
+ }, [rows, activeFilter, searchQuery]);
+
+ const unreadCount = rows.filter((r) => r._unreadCount > 0).length;
+
+ const filters: { label: string; value: FilterType }[] = [
+ { label: 'All', value: 'all' },
+ { label: '💬 SMS', value: 'sms' },
+ { label: '📧 Email', value: 'email' },
+ { label: '📞 Calls', value: 'call' },
+ { label: `Unread (${unreadCount})`, value: 'unread' },
+ ];
+
+ if (error) {
+ return ;
+ }
+ if (!isConnected && !data) {
+ return ;
+ }
+ if (!data) {
+ return Waiting for conversations...
;
+ }
+
+ return (
+
+
+
+ {/* Search bar */}
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+ {/* Filter chips */}
+
+
+ {filters.map((f) => (
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/conversation-list/index.html b/src/ui/react-app/src/apps/conversation-list/index.html
new file mode 100644
index 0000000..60e2a96
--- /dev/null
+++ b/src/ui/react-app/src/apps/conversation-list/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/conversation-list/main.tsx b/src/ui/react-app/src/apps/conversation-list/main.tsx
new file mode 100644
index 0000000..8ebb093
--- /dev/null
+++ b/src/ui/react-app/src/apps/conversation-list/main.tsx
@@ -0,0 +1,9 @@
+import React, { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.js';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/src/ui/react-app/src/apps/conversation-list/vite.config.ts b/src/ui/react-app/src/apps/conversation-list/vite.config.ts
new file mode 100644
index 0000000..c3659db
--- /dev/null
+++ b/src/ui/react-app/src/apps/conversation-list/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/conversation-list'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/conversation-thread/App.tsx b/src/ui/react-app/src/apps/conversation-thread/App.tsx
new file mode 100644
index 0000000..ecd0e48
--- /dev/null
+++ b/src/ui/react-app/src/apps/conversation-thread/App.tsx
@@ -0,0 +1,113 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { MCPAppProvider } from '../../context/MCPAppContext';
+import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
+import { DetailHeader } from '../../components/data/DetailHeader';
+import { ChatThread } from '../../components/comms/ChatThread';
+import { ActionBar } from '../../components/shared/ActionBar';
+import { ActionButton } from '../../components/shared/ActionButton';
+import type { ChatMessage, StatusVariant } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+const typeIcons: Record = {
+ sms: '💬',
+ email: '📧',
+ call: '📞',
+ whatsapp: '📱',
+ facebook: '👤',
+ instagram: '📸',
+};
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [appInstance, setAppInstance] = useState(null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Conversation Thread', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ setAppInstance(app);
+ app.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ const conversation = data?.conversation;
+ const rawMessages: any[] = data?.messages || [];
+
+ const messages: ChatMessage[] = useMemo(() => {
+ return rawMessages.map((msg) => ({
+ content: msg.body || msg.message || msg.content || msg.text || '',
+ direction: (msg.direction === 'outbound' || msg.direction === 'outgoing' || msg.type === 'outbound')
+ ? 'outbound' as const
+ : 'inbound' as const,
+ senderName: msg.senderName || msg.contactName || msg.from || (msg.direction === 'outbound' ? 'You' : conversation?.contactName || 'Contact'),
+ timestamp: msg.dateAdded || msg.createdAt || msg.timestamp || msg.date || '',
+ type: (msg.messageType || msg.type || conversation?.type || 'sms') as ChatMessage['type'],
+ }));
+ }, [rawMessages, conversation]);
+
+ const contactName = conversation?.contactName || conversation?.contact?.name ||
+ [conversation?.contact?.firstName, conversation?.contact?.lastName].filter(Boolean).join(' ') || 'Contact';
+
+ const conversationType = conversation?.type || 'sms';
+ const typeIcon = typeIcons[conversationType] || '💬';
+
+ if (error) {
+ return ;
+ }
+ if (!isConnected && !data) {
+ return ;
+ }
+ if (!data) {
+ return Waiting for conversation data...
;
+ }
+
+ const statusVariant: StatusVariant = conversation?.unreadCount > 0 ? 'active' : 'complete';
+
+ return (
+
+
+
+
0 ? `${conversation.unreadCount} unread` : undefined}
+ statusVariant={statusVariant}
+ />
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/conversation-thread/index.html b/src/ui/react-app/src/apps/conversation-thread/index.html
new file mode 100644
index 0000000..60e2a96
--- /dev/null
+++ b/src/ui/react-app/src/apps/conversation-thread/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/conversation-thread/main.tsx b/src/ui/react-app/src/apps/conversation-thread/main.tsx
new file mode 100644
index 0000000..8ebb093
--- /dev/null
+++ b/src/ui/react-app/src/apps/conversation-thread/main.tsx
@@ -0,0 +1,9 @@
+import React, { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.js';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/src/ui/react-app/src/apps/conversation-thread/vite.config.ts b/src/ui/react-app/src/apps/conversation-thread/vite.config.ts
new file mode 100644
index 0000000..7e85530
--- /dev/null
+++ b/src/ui/react-app/src/apps/conversation-thread/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/conversation-thread'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/coupon-manager/App.tsx b/src/ui/react-app/src/apps/coupon-manager/App.tsx
new file mode 100644
index 0000000..fc874d2
--- /dev/null
+++ b/src/ui/react-app/src/apps/coupon-manager/App.tsx
@@ -0,0 +1,238 @@
+/**
+ * Coupon Manager — Coupons table with status badges and create action.
+ */
+import React, { useState, useMemo } from "react";
+import { useApp } from "@modelcontextprotocol/ext-apps/react";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import { MCPAppProvider } from "../../context/MCPAppContext";
+import { ChangeTrackerProvider } from "../../context/ChangeTrackerContext";
+import { PageHeader } from "../../components/layout/PageHeader";
+import { DataTable } from "../../components/data/DataTable";
+import { ActionButton } from "../../components/shared/ActionButton";
+import type { TableColumn, StatusVariant } from "../../types";
+import "../../styles/base.css";
+import "../../styles/interactive.css";
+
+// ─── Data Shape ─────────────────────────────────────────────
+
+interface Coupon {
+ id?: string;
+ code?: string;
+ type?: string;
+ discountType?: string;
+ value?: number;
+ amount?: number;
+ uses?: number;
+ usesCount?: number;
+ maxUses?: number;
+ status?: string;
+ expiry?: string;
+ expiryDate?: string;
+ currency?: string;
+}
+
+interface AppData {
+ coupons?: Coupon[];
+ currency?: string;
+ createTool?: string;
+}
+
+// ─── Helpers ────────────────────────────────────────────────
+
+function extractData(result: CallToolResult): AppData | null {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc as AppData;
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === "text") {
+ try {
+ return JSON.parse(item.text) as AppData;
+ } catch {
+ continue;
+ }
+ }
+ }
+ }
+ return null;
+}
+
+function formatDate(d?: string): string {
+ if (!d) return "—";
+ try {
+ return new Date(d).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ });
+ } catch {
+ return d;
+ }
+}
+
+function couponStatusVariant(coupon: Coupon): StatusVariant {
+ const s = (coupon.status || "").toLowerCase();
+ if (s === "expired") return "error";
+ if (s === "maxed" || s === "depleted") return "lost";
+ if (s === "active") return "active";
+ if (s === "inactive" || s === "disabled") return "draft";
+
+ // Auto-detect from data
+ const expiry = coupon.expiry || coupon.expiryDate;
+ if (expiry && new Date(expiry) < new Date()) return "error";
+ const uses = coupon.uses ?? coupon.usesCount ?? 0;
+ if (coupon.maxUses && uses >= coupon.maxUses) return "lost";
+
+ return "active";
+}
+
+function couponStatusLabel(coupon: Coupon): string {
+ const s = (coupon.status || "").toLowerCase();
+ if (s) return coupon.status!;
+
+ const expiry = coupon.expiry || coupon.expiryDate;
+ if (expiry && new Date(expiry) < new Date()) return "Expired";
+ const uses = coupon.uses ?? coupon.usesCount ?? 0;
+ if (coupon.maxUses && uses >= coupon.maxUses) return "Maxed Out";
+
+ return "Active";
+}
+
+function formatValue(coupon: Coupon, currency = "USD"): string {
+ const val = coupon.value ?? coupon.amount ?? 0;
+ const type = (coupon.type || coupon.discountType || "").toLowerCase();
+
+ if (type === "percent" || type === "percentage") {
+ return `${val}%`;
+ }
+
+ try {
+ return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(val);
+ } catch {
+ return `$${val.toFixed(2)}`;
+ }
+}
+
+const STATUS_FILTERS = ["all", "active", "expired", "maxed"] as const;
+
+// ─── App ────────────────────────────────────────────────────
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [activeFilter, setActiveFilter] = useState("all");
+ const [appInstance, setAppInstance] = useState(null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: "Coupon Manager", version: "1.0.0" },
+ capabilities: {},
+ onAppCreated: (app) => {
+ setAppInstance(app);
+ app.ontoolresult = async (result) => {
+ const extracted = extractData(result);
+ if (extracted) setData(extracted);
+ };
+ },
+ });
+
+ const coupons = data?.coupons || [];
+ const currency = data?.currency || "USD";
+ const createTool = data?.createTool;
+
+ // Filter coupons
+ const filteredCoupons = useMemo(() => {
+ if (activeFilter === "all") return coupons;
+
+ return coupons.filter((coupon) => {
+ const label = couponStatusLabel(coupon).toLowerCase();
+ if (activeFilter === "maxed") {
+ return label === "maxed out" || label === "maxed" || label === "depleted";
+ }
+ return label === activeFilter;
+ });
+ }, [coupons, activeFilter]);
+
+ // Build table rows
+ const rows = useMemo(() => filteredCoupons.map((coupon) => ({
+ id: coupon.id || coupon.code || "",
+ code: coupon.code || "—",
+ type: (coupon.type || coupon.discountType || "—").charAt(0).toUpperCase() +
+ (coupon.type || coupon.discountType || "—").slice(1),
+ value: formatValue(coupon, currency),
+ uses: `${coupon.uses ?? coupon.usesCount ?? 0}${coupon.maxUses ? ` / ${coupon.maxUses}` : ""}`,
+ status: couponStatusLabel(coupon),
+ expiry: formatDate(coupon.expiry || coupon.expiryDate),
+ })), [filteredCoupons, currency]);
+
+ const columns: TableColumn[] = useMemo(() => [
+ { key: "code", label: "Code", sortable: true },
+ { key: "type", label: "Type", sortable: true },
+ { key: "value", label: "Value", sortable: true },
+ { key: "uses", label: "Uses", sortable: true },
+ { key: "status", label: "Status", sortable: true, format: "status" },
+ { key: "expiry", label: "Expiry", sortable: true, format: "date" },
+ ], []);
+
+ // Stats
+ const activeCount = coupons.filter(
+ (c) => couponStatusLabel(c).toLowerCase() === "active",
+ ).length;
+ const expiredCount = coupons.filter(
+ (c) => couponStatusLabel(c).toLowerCase() === "expired",
+ ).length;
+
+ if (error) {
+ return ;
+ }
+ if (!isConnected && !data) {
+ return ;
+ }
+ if (!data) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+ {STATUS_FILTERS.map((f) => (
+
+ ))}
+
+
+ {createTool && (
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/coupon-manager/index.html b/src/ui/react-app/src/apps/coupon-manager/index.html
new file mode 100644
index 0000000..1031b19
--- /dev/null
+++ b/src/ui/react-app/src/apps/coupon-manager/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Coupon Manager
+
+
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/coupon-manager/main.tsx b/src/ui/react-app/src/apps/coupon-manager/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/coupon-manager/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/coupon-manager/vite.config.ts b/src/ui/react-app/src/apps/coupon-manager/vite.config.ts
new file mode 100644
index 0000000..dc74753
--- /dev/null
+++ b/src/ui/react-app/src/apps/coupon-manager/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/coupon-manager'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/course-catalog/App.tsx b/src/ui/react-app/src/apps/course-catalog/App.tsx
new file mode 100644
index 0000000..31a8a9a
--- /dev/null
+++ b/src/ui/react-app/src/apps/course-catalog/App.tsx
@@ -0,0 +1,125 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { CardGrid } from '../../components/data/CardGrid';
+import type { CardGridItem, StatusVariant } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+const STATUS_OPTIONS = ['All', 'Published', 'Draft', 'Archived'] as const;
+
+function mapStatusVariant(status: string): StatusVariant {
+ const s = status.toLowerCase();
+ if (s === 'published' || s === 'active') return 'active';
+ if (s === 'draft') return 'draft';
+ if (s === 'archived') return 'complete';
+ return 'pending';
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [search, setSearch] = useState('');
+ const [statusFilter, setStatusFilter] = useState('All');
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Course Catalog', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const cards: CardGridItem[] = useMemo(() => {
+ const courses: any[] = data?.courses || [];
+ return courses
+ .filter((c: any) => {
+ if (statusFilter !== 'All') {
+ const cStatus = (c.status || 'draft').toLowerCase();
+ if (cStatus !== statusFilter.toLowerCase()) return false;
+ }
+ if (search) {
+ const q = search.toLowerCase();
+ const title = (c.title || c.name || '').toLowerCase();
+ const desc = (c.description || '').toLowerCase();
+ if (!title.includes(q) && !desc.includes(q)) return false;
+ }
+ return true;
+ })
+ .map((c: any) => {
+ const lessonsCount = c.lessonsCount ?? c.lessons?.length ?? 0;
+ const status = c.status || 'Draft';
+ return {
+ title: c.title || c.name || 'Untitled Course',
+ subtitle: `${lessonsCount} lesson${lessonsCount !== 1 ? 's' : ''}`,
+ description: c.description ? c.description.slice(0, 120) + (c.description.length > 120 ? '…' : '') : undefined,
+ imageUrl: c.thumbnailUrl || c.thumbnail || c.imageUrl || undefined,
+ status: status.charAt(0).toUpperCase() + status.slice(1),
+ statusVariant: mapStatusVariant(status),
+ };
+ });
+ }, [data, search, statusFilter]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const totalCourses = data?.courses?.length || 0;
+
+ return (
+
+
+
+ setSearch(e.target.value)}
+ />
+
+
+ {STATUS_OPTIONS.map((s) => (
+
+ ))}
+
+
+
+ {cards.length === 0 ? (
+
+
📚
+
No courses match your filters.
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/course-catalog/index.html b/src/ui/react-app/src/apps/course-catalog/index.html
new file mode 100644
index 0000000..4c7047c
--- /dev/null
+++ b/src/ui/react-app/src/apps/course-catalog/index.html
@@ -0,0 +1,5 @@
+
+
+Course Catalog
+
+
diff --git a/src/ui/react-app/src/apps/course-catalog/main.tsx b/src/ui/react-app/src/apps/course-catalog/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/course-catalog/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/course-catalog/vite.config.ts b/src/ui/react-app/src/apps/course-catalog/vite.config.ts
new file mode 100644
index 0000000..ee79687
--- /dev/null
+++ b/src/ui/react-app/src/apps/course-catalog/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/course-catalog'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/course-detail/App.tsx b/src/ui/react-app/src/apps/course-detail/App.tsx
new file mode 100644
index 0000000..4f2a9f4
--- /dev/null
+++ b/src/ui/react-app/src/apps/course-detail/App.tsx
@@ -0,0 +1,155 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { DetailHeader } from '../../components/data/DetailHeader';
+import { StatsGrid } from '../../components/layout/StatsGrid';
+import { SplitLayout } from '../../components/layout/SplitLayout';
+import { MetricCard } from '../../components/data/MetricCard';
+import { ProgressBar } from '../../components/data/ProgressBar';
+import { TreeView } from '../../components/viz/TreeView';
+import { Card } from '../../components/layout/Card';
+import { KeyValueList } from '../../components/data/KeyValueList';
+import type { TreeNode, KeyValueItem, StatusVariant } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatDate(d?: string): string {
+ if (!d) return '—';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Course Detail', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const course = useMemo(() => {
+ if (!data) return null;
+ return data.course || data;
+ }, [data]);
+
+ const treeNodes: TreeNode[] = useMemo(() => {
+ if (!course) return [];
+ const modules: any[] = course.modules || course.chapters || [];
+ if (modules.length > 0) {
+ return modules.map((m: any) => ({
+ label: m.title || m.name || 'Module',
+ icon: '📁',
+ badge: m.lessons ? `${m.lessons.length} lessons` : undefined,
+ expanded: true,
+ children: (m.lessons || m.items || []).map((l: any) => ({
+ label: l.title || l.name || 'Lesson',
+ icon: l.completed ? '✅' : '📄',
+ badge: l.duration || undefined,
+ })),
+ }));
+ }
+ const lessons: any[] = course.lessons || [];
+ return lessons.map((l: any) => ({
+ label: l.title || l.name || 'Lesson',
+ icon: l.completed ? '✅' : '📄',
+ badge: l.duration || undefined,
+ }));
+ }, [course]);
+
+ const totalLessons = useMemo(() => {
+ if (!course) return 0;
+ if (course.totalLessons) return course.totalLessons;
+ const modules: any[] = course.modules || course.chapters || [];
+ if (modules.length > 0) {
+ return modules.reduce((sum: number, m: any) => sum + (m.lessons?.length || 0), 0);
+ }
+ return course.lessons?.length || 0;
+ }, [course]);
+
+ const detailItems: KeyValueItem[] = useMemo(() => {
+ if (!course) return [];
+ return [
+ { label: 'Instructor', value: course.instructor || course.author || '—' },
+ { label: 'Category', value: course.category || '—' },
+ { label: 'Duration', value: course.duration || '—' },
+ { label: 'Created', value: formatDate(course.createdAt) },
+ { label: 'Last Updated', value: formatDate(course.updatedAt) },
+ ];
+ }, [course]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const status = course?.status || 'Draft';
+ const statusMap: Record = { published: 'active', active: 'active', draft: 'draft', archived: 'complete' };
+ const statusVariant: StatusVariant = statusMap[status.toLowerCase()] || 'pending';
+ const avgProgress = course?.avgProgress ?? course?.progress ?? 0;
+ const completionRate = course?.completionRate ?? 0;
+ const enrolledStudents = course?.enrolledStudents ?? course?.enrollments ?? 0;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {treeNodes.length > 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/course-detail/index.html b/src/ui/react-app/src/apps/course-detail/index.html
new file mode 100644
index 0000000..c8e356d
--- /dev/null
+++ b/src/ui/react-app/src/apps/course-detail/index.html
@@ -0,0 +1,5 @@
+
+
+Course Detail
+
+
diff --git a/src/ui/react-app/src/apps/course-detail/main.tsx b/src/ui/react-app/src/apps/course-detail/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/course-detail/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/course-detail/vite.config.ts b/src/ui/react-app/src/apps/course-detail/vite.config.ts
new file mode 100644
index 0000000..7b0d45a
--- /dev/null
+++ b/src/ui/react-app/src/apps/course-detail/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/course-detail'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/custom-fields-manager/App.tsx b/src/ui/react-app/src/apps/custom-fields-manager/App.tsx
new file mode 100644
index 0000000..893fa3a
--- /dev/null
+++ b/src/ui/react-app/src/apps/custom-fields-manager/App.tsx
@@ -0,0 +1,341 @@
+/**
+ * Custom Fields Manager — Fields table per object type.
+ * Tabs by object type: contact, opportunity, etc.
+ * Columns: fieldName, fieldKey, dataType, required, placeholder.
+ * Table with field metadata.
+ */
+import React, { useMemo, useState, useCallback } from "react";
+import { useApp } from "@modelcontextprotocol/ext-apps/react";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import { PageHeader } from "../../components/layout/PageHeader.js";
+import { Section } from "../../components/layout/Section.js";
+import { DataTable } from "../../components/data/DataTable.js";
+import { StatusBadge } from "../../components/data/StatusBadge.js";
+import { Card } from "../../components/layout/Card.js";
+import "../../styles/base.css";
+import "../../styles/interactive.css";
+
+// ─── Types ──────────────────────────────────────────────────
+
+interface CustomField {
+ id?: string;
+ fieldName: string;
+ fieldKey: string;
+ dataType: string;
+ required: boolean;
+ placeholder?: string;
+ objectType?: string;
+ options?: string[];
+ dateCreated?: string;
+}
+
+interface CustomFieldsData {
+ fields: CustomField[];
+ objectKey: string;
+}
+
+// ─── Data Extraction ────────────────────────────────────────
+
+function extractData(result: CallToolResult): CustomFieldsData | null {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc as CustomFieldsData;
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === "text") {
+ try {
+ return JSON.parse(item.text) as CustomFieldsData;
+ } catch {
+ /* skip */
+ }
+ }
+ }
+ }
+ return null;
+}
+
+// ─── Object Type Tabs ───────────────────────────────────────
+
+const OBJECT_TYPES = [
+ { label: "Contact", value: "contact" },
+ { label: "Opportunity", value: "opportunity" },
+ { label: "Company", value: "company" },
+ { label: "Order", value: "order" },
+ { label: "Custom", value: "custom" },
+];
+
+// ─── App ────────────────────────────────────────────────────
+
+export function App() {
+ const [data, setData] = React.useState(null);
+ const [activeTab, setActiveTab] = useState("");
+
+ const [actionResult, setActionResult] = useState<{ type: 'success' | 'error'; msg: string } | null>(null);
+ const [isActing, setIsActing] = useState(false);
+ const [showCreateForm, setShowCreateForm] = useState(false);
+ const [newFieldName, setNewFieldName] = useState("");
+ const [newFieldKey, setNewFieldKey] = useState("");
+ const [newFieldType, setNewFieldType] = useState("TEXT");
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: "custom-fields-manager", version: "1.0.0" },
+ capabilities: {},
+ onAppCreated: (createdApp) => {
+ createdApp.ontoolresult = async (result) => {
+ const extracted = extractData(result);
+ if (extracted) {
+ setData(extracted);
+ if (!activeTab) setActiveTab(extracted.objectKey || "contact");
+ }
+ };
+ },
+ });
+
+ React.useEffect(() => {
+ const preInjected = (window as any).__MCP_APP_DATA__;
+ if (preInjected && !data) {
+ const d = preInjected as CustomFieldsData;
+ setData(d);
+ if (!activeTab) setActiveTab(d.objectKey || "contact");
+ }
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const handleFieldAction = useCallback(async (action: string, fieldData: Record) => {
+ if (!app) return;
+ setIsActing(true);
+ setActionResult(null);
+ try {
+ await app.updateModelContext({
+ content: [{
+ type: 'text',
+ text: JSON.stringify({ action, data: fieldData }),
+ }],
+ });
+ setActionResult({ type: 'success', msg: `✓ ${action.replace('_', ' ')} request sent` });
+ setTimeout(() => setActionResult(null), 3000);
+ } catch {
+ setActionResult({ type: 'error', msg: '✗ Failed to send request' });
+ } finally {
+ setIsActing(false);
+ }
+ }, [app]);
+
+ const handleCreateField = useCallback(() => {
+ if (!newFieldName.trim()) return;
+ handleFieldAction('create_custom_field', {
+ fieldName: newFieldName.trim(),
+ fieldKey: newFieldKey.trim() || newFieldName.trim().toLowerCase().replace(/\s+/g, '_'),
+ dataType: newFieldType,
+ objectType: activeTab,
+ });
+ setNewFieldName(""); setNewFieldKey(""); setNewFieldType("TEXT");
+ setShowCreateForm(false);
+ }, [newFieldName, newFieldKey, newFieldType, activeTab, handleFieldAction]);
+
+ const handleDeleteField = useCallback((fieldId: string, fieldName: string) => {
+ handleFieldAction('delete_custom_field', { fieldId, fieldName, objectType: activeTab });
+ }, [activeTab, handleFieldAction]);
+
+ // Group fields by object type
+ const fieldsByType = useMemo(() => {
+ if (!data?.fields) return {};
+ const grouped: Record = {};
+ for (const field of data.fields) {
+ const objType = field.objectType ?? data.objectKey ?? "contact";
+ if (!grouped[objType]) grouped[objType] = [];
+ grouped[objType].push(field);
+ }
+ return grouped;
+ }, [data?.fields, data?.objectKey]);
+
+ // Available tabs based on actual data
+ const availableTabs = useMemo(() => {
+ const objectTypes = Object.keys(fieldsByType);
+ if (objectTypes.length === 0) return OBJECT_TYPES;
+ return OBJECT_TYPES.filter((t) => objectTypes.includes(t.value)).map((t) => ({
+ ...t,
+ count: fieldsByType[t.value]?.length ?? 0,
+ }));
+ }, [fieldsByType]);
+
+ // Fields for the active tab
+ const filteredFields = useMemo(() => {
+ if (!activeTab) return data?.fields ?? [];
+ return fieldsByType[activeTab] ?? data?.fields ?? [];
+ }, [activeTab, fieldsByType, data?.fields]);
+
+ // Table rows with formatted required column
+ const tableRows = useMemo(
+ () =>
+ filteredFields.map((f, idx) => ({
+ id: f.id ?? `field-${idx}`,
+ fieldName: f.fieldName,
+ fieldKey: f.fieldKey,
+ dataType: f.dataType,
+ required: f.required ? "Yes" : "No",
+ placeholder: f.placeholder ?? "—",
+ })),
+ [filteredFields],
+ );
+
+ const tableColumns = useMemo(() => [
+ { key: "fieldName", label: "Field Name", sortable: true },
+ { key: "fieldKey", label: "Field Key", sortable: true },
+ { key: "dataType", label: "Data Type", sortable: true },
+ { key: "required", label: "Required", sortable: true },
+ { key: "placeholder", label: "Placeholder" },
+ ], []);
+
+ if (error) {
+ return (
+
+
Connection Error
+
{error.message}
+
+ );
+ }
+
+ if (!data) {
+ return (
+
+
+
{isConnected ? "Waiting for data..." : "Connecting..."}
+
+ );
+ }
+
+ return (
+
+
+
+ {/* Tab Navigation */}
+
+ {availableTabs.map((t) => (
+
+ ))}
+
+
+ {/* Action bar */}
+
+ {/* Field Type Summary */}
+
+ {Array.from(new Set(filteredFields.map((f) => f.dataType))).map((type) => (
+ f.dataType === type).length})`}
+ variant="active"
+ />
+ ))}
+
+
+ {actionResult && (
+
+ {actionResult.msg}
+
+ )}
+
+
+
+
+ {/* Create Custom Field Form */}
+ {showCreateForm && (
+
+
+
+
+ setNewFieldName(e.target.value)}
+ />
+
+
+
+ setNewFieldKey(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ {/* Quick delete actions */}
+ {filteredFields.length > 0 && (
+
+ Quick delete:
+ {filteredFields.slice(0, 6).map((f, idx) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/custom-fields-manager/index.html b/src/ui/react-app/src/apps/custom-fields-manager/index.html
new file mode 100644
index 0000000..e12cce8
--- /dev/null
+++ b/src/ui/react-app/src/apps/custom-fields-manager/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Custom Fields Manager
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/custom-fields-manager/main.tsx b/src/ui/react-app/src/apps/custom-fields-manager/main.tsx
new file mode 100644
index 0000000..c3e2da2
--- /dev/null
+++ b/src/ui/react-app/src/apps/custom-fields-manager/main.tsx
@@ -0,0 +1,9 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { App } from "./App.js";
+
+createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
diff --git a/src/ui/react-app/src/apps/custom-fields-manager/vite.config.ts b/src/ui/react-app/src/apps/custom-fields-manager/vite.config.ts
new file mode 100644
index 0000000..660ae44
--- /dev/null
+++ b/src/ui/react-app/src/apps/custom-fields-manager/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/custom-fields-manager'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/duplicate-checker/App.tsx b/src/ui/react-app/src/apps/duplicate-checker/App.tsx
new file mode 100644
index 0000000..e106010
--- /dev/null
+++ b/src/ui/react-app/src/apps/duplicate-checker/App.tsx
@@ -0,0 +1,107 @@
+import React from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { useState } from 'react';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { DuplicateCompare } from '../../components/viz/DuplicateCompare';
+import type { CompareRecord } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function contactToRecord(c: any, label: string): CompareRecord {
+ const fullName = `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unknown';
+ return {
+ label,
+ fields: {
+ 'Name': fullName,
+ 'Email': c.email || '—',
+ 'Phone': c.phone || '—',
+ 'Company': c.companyName || c.company || '—',
+ 'Address': [c.address1, c.city, c.state, c.postalCode].filter(Boolean).join(', ') || '—',
+ 'Source': c.source || '—',
+ 'Tags': (c.tags || []).join(', ') || '—',
+ 'Date Added': formatDate(c.dateAdded || c.createdAt),
+ },
+ };
+}
+
+function formatDate(d?: string): string {
+ if (!d) return '—';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: 'Duplicate Checker', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const handleAction = async (action: string) => {
+ if (!app) return;
+ try {
+ await app.updateModelContext({
+ content: [{
+ type: 'text',
+ text: `User action: ${action} — contact: ${data?.contact?.id || '?'}, duplicate: ${data?.duplicate?.id || '?'}`,
+ }],
+ });
+ } catch {}
+ };
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const contact = data.contact || {};
+ const duplicate = data.duplicate || {};
+
+ const records: CompareRecord[] = [
+ contactToRecord(contact, 'Original'),
+ contactToRecord(duplicate, 'Duplicate'),
+ ];
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/duplicate-checker/index.html b/src/ui/react-app/src/apps/duplicate-checker/index.html
new file mode 100644
index 0000000..370ec82
--- /dev/null
+++ b/src/ui/react-app/src/apps/duplicate-checker/index.html
@@ -0,0 +1,5 @@
+
+
+Duplicate Checker
+
+
diff --git a/src/ui/react-app/src/apps/duplicate-checker/main.tsx b/src/ui/react-app/src/apps/duplicate-checker/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/duplicate-checker/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/duplicate-checker/vite.config.ts b/src/ui/react-app/src/apps/duplicate-checker/vite.config.ts
new file mode 100644
index 0000000..0b65ff3
--- /dev/null
+++ b/src/ui/react-app/src/apps/duplicate-checker/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/duplicate-checker'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/email-template-preview/App.tsx b/src/ui/react-app/src/apps/email-template-preview/App.tsx
new file mode 100644
index 0000000..ea89909
--- /dev/null
+++ b/src/ui/react-app/src/apps/email-template-preview/App.tsx
@@ -0,0 +1,170 @@
+/**
+ * email-template-preview — Rendered email template preview.
+ * Shows email content with metadata and action buttons.
+ */
+import React, { useState, useEffect } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { MCPAppProvider } from '../../context/MCPAppContext';
+import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
+import { DetailHeader } from '../../components/data/DetailHeader';
+import { EmailPreview } from '../../components/comms/EmailPreview';
+import { KeyValueList } from '../../components/data/KeyValueList';
+import { ActionBar } from '../../components/shared/ActionBar';
+import { ActionButton } from '../../components/shared/ActionButton';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+// ─── Types ──────────────────────────────────────────────────
+
+interface EmailTemplate {
+ id?: string;
+ name?: string;
+ subject?: string;
+ from?: string;
+ to?: string;
+ body?: string;
+ category?: string;
+ status?: string;
+ createdAt?: string;
+ updatedAt?: string;
+ cc?: string;
+ attachments?: { name: string; size?: string }[];
+}
+
+interface TemplateData {
+ template: EmailTemplate;
+}
+
+// ─── Extract data from tool result ──────────────────────────
+
+function extractData(result: CallToolResult): TemplateData | null {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc as TemplateData;
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text) as TemplateData; } catch { /* skip */ }
+ }
+ }
+ }
+ return null;
+}
+
+// ─── App ────────────────────────────────────────────────────
+
+function formatDate(d?: string): string {
+ if (!d) return '\u2014';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+export function App() {
+ const [data, setData] = useState(null);
+
+ useEffect(() => {
+ const d = (window as any).__MCP_APP_DATA__;
+ if (d && !data) setData(d as TemplateData);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: 'email-template-preview', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (createdApp) => {
+ createdApp.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ if (error) {
+ return Connection Error
{error.message}
;
+ }
+ if (!isConnected || !app) {
+ return ;
+ }
+ if (!data) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+// ─── View ───────────────────────────────────────────────────
+
+function TemplatePreviewView({ template }: { template: EmailTemplate }) {
+ const t = template;
+
+ const metadataItems = [
+ ...(t.category ? [{ label: 'Category', value: t.category }] : []),
+ ...(t.createdAt ? [{ label: 'Created', value: formatDate(t.createdAt) }] : []),
+ ...(t.updatedAt ? [{ label: 'Updated', value: formatDate(t.updatedAt) }] : []),
+ ...(t.id ? [{ label: 'Template ID', value: t.id, variant: 'muted' as const }] : []),
+ ];
+
+ return (
+ <>
+
+
+ No content
'}
+ cc={t.cc}
+ attachments={t.attachments}
+ />
+
+ {metadataItems.length > 0 && (
+
+
+
+ )}
+
+
+ >
+ );
+}
diff --git a/src/ui/react-app/src/apps/email-template-preview/index.html b/src/ui/react-app/src/apps/email-template-preview/index.html
new file mode 100644
index 0000000..60e2a96
--- /dev/null
+++ b/src/ui/react-app/src/apps/email-template-preview/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/email-template-preview/main.tsx b/src/ui/react-app/src/apps/email-template-preview/main.tsx
new file mode 100644
index 0000000..fbe1cf6
--- /dev/null
+++ b/src/ui/react-app/src/apps/email-template-preview/main.tsx
@@ -0,0 +1,9 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.js';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/src/ui/react-app/src/apps/email-template-preview/vite.config.ts b/src/ui/react-app/src/apps/email-template-preview/vite.config.ts
new file mode 100644
index 0000000..58e4a89
--- /dev/null
+++ b/src/ui/react-app/src/apps/email-template-preview/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/email-template-preview'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/estimate-builder/App.tsx b/src/ui/react-app/src/apps/estimate-builder/App.tsx
new file mode 100644
index 0000000..0787f2d
--- /dev/null
+++ b/src/ui/react-app/src/apps/estimate-builder/App.tsx
@@ -0,0 +1,147 @@
+/**
+ * Estimate Builder — Create estimate form with contact, line items, and notes.
+ */
+import React, { useState, useMemo, useCallback } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { Card } from '../../components/layout/Card';
+import { ContactPicker } from '../../components/interactive/ContactPicker';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatCurrency(n: number, currency = 'USD'): string {
+ try { return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n); }
+ catch { return `$${n.toFixed(2)}`; }
+}
+
+interface LineItem { description: string; quantity: number; unitPrice: number; }
+interface SelectedContact { id: string; name?: string; firstName?: string; lastName?: string; email?: string; [key: string]: any; }
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: 'Estimate Builder', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const [title, setTitle] = useState('');
+ const [selectedContact, setSelectedContact] = useState(null);
+ const [lineItems, setLineItems] = useState([{ description: '', quantity: 1, unitPrice: 0 }]);
+ const [notes, setNotes] = useState('');
+ const [expiryDate, setExpiryDate] = useState('');
+ const [isCreating, setIsCreating] = useState(false);
+ const [createSuccess, setCreateSuccess] = useState(false);
+
+ const currency = data?.currency || 'USD';
+
+ const updateItem = useCallback((index: number, field: keyof LineItem, value: string | number) => {
+ setLineItems((prev) => { const next = [...prev]; next[index] = { ...next[index], [field]: value }; return next; });
+ }, []);
+
+ const addItem = () => setLineItems((prev) => [...prev, { description: '', quantity: 1, unitPrice: 0 }]);
+ const removeItem = (index: number) => setLineItems((prev) => prev.filter((_, i) => i !== index));
+
+ const subtotal = useMemo(() => lineItems.reduce((sum, it) => sum + it.quantity * it.unitPrice, 0), [lineItems]);
+
+ const getContactName = (c: SelectedContact): string => {
+ if (c.name) return c.name;
+ return [c.firstName, c.lastName].filter(Boolean).join(' ') || 'Unknown';
+ };
+
+ const handleCreate = async () => {
+ if (!app) return;
+ setIsCreating(true);
+ setCreateSuccess(false);
+ const contactName = selectedContact ? getContactName(selectedContact) : undefined;
+ try {
+ await app.updateModelContext({
+ content: [{
+ type: 'text',
+ text: JSON.stringify({
+ action: 'create_estimate',
+ title, contactId: selectedContact?.id, contactName,
+ items: lineItems.map((it) => ({ description: it.description, quantity: it.quantity, unitPrice: it.unitPrice, total: it.quantity * it.unitPrice })),
+ subtotal, total: subtotal, currency, notes, expiryDate: expiryDate || undefined,
+ }),
+ }],
+ });
+ setCreateSuccess(true);
+ } catch {}
+ setIsCreating(false);
+ };
+
+ if (error) return ;
+ if (!isConnected) return ;
+
+ return (
+
+
+
+
+
+
+ setTitle(e.target.value)} />
+
+
+
+ setSelectedContact(c)} />
+
+
+
+ setExpiryDate(e.target.value)} />
+
+
+
+
+
+
+
+
Total{formatCurrency(subtotal, currency)}
+
+
+
+
+
+
+
+ {createSuccess && ✓ Estimate created}
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/estimate-builder/index.html b/src/ui/react-app/src/apps/estimate-builder/index.html
new file mode 100644
index 0000000..2d004ec
--- /dev/null
+++ b/src/ui/react-app/src/apps/estimate-builder/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Estimate Builder
+
+
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/estimate-builder/main.tsx b/src/ui/react-app/src/apps/estimate-builder/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/estimate-builder/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/estimate-builder/vite.config.ts b/src/ui/react-app/src/apps/estimate-builder/vite.config.ts
new file mode 100644
index 0000000..346f568
--- /dev/null
+++ b/src/ui/react-app/src/apps/estimate-builder/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/estimate-builder'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/estimate-preview/App.tsx b/src/ui/react-app/src/apps/estimate-preview/App.tsx
new file mode 100644
index 0000000..26eeb54
--- /dev/null
+++ b/src/ui/react-app/src/apps/estimate-preview/App.tsx
@@ -0,0 +1,145 @@
+/**
+ * Estimate Preview — Formatted estimate view with line items and totals.
+ */
+import React, { useState } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { DetailHeader } from '../../components/data/DetailHeader';
+import { SplitLayout } from '../../components/layout/SplitLayout';
+import { InfoBlock } from '../../components/data/InfoBlock';
+import { LineItemsTable } from '../../components/data/LineItemsTable';
+import { KeyValueList } from '../../components/data/KeyValueList';
+import { StatusBadge } from '../../components/data/StatusBadge';
+import type { StatusVariant, KeyValueItem, LineItem } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function statusVariant(status?: string): StatusVariant {
+ const s = (status || '').toLowerCase();
+ if (s === 'accepted' || s === 'approved') return 'paid';
+ if (s === 'sent') return 'sent';
+ if (s === 'draft') return 'draft';
+ if (s === 'expired') return 'error';
+ if (s === 'declined' || s === 'rejected') return 'lost';
+ if (s === 'pending') return 'pending';
+ return 'active';
+}
+
+function formatCurrency(n: number, currency = 'USD'): string {
+ try { return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n); }
+ catch { return `$${n.toFixed(2)}`; }
+}
+
+function formatDate(d?: string): string {
+ if (!d) return '—';
+ try { return new Date(d).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); }
+ catch { return d; }
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Estimate Preview', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const est = data.estimate || {};
+ const currency = est.currency || 'USD';
+
+ const lineItems: LineItem[] = (est.items || []).map((item: any) => ({
+ name: item.name || 'Item',
+ description: item.description,
+ quantity: item.quantity ?? 1,
+ unitPrice: item.unitPrice ?? 0,
+ total: item.total ?? (item.quantity ?? 1) * (item.unitPrice ?? 0),
+ }));
+
+ const fromLines: string[] = [];
+ if (est.from?.email) fromLines.push(est.from.email);
+ if (est.from?.phone) fromLines.push(est.from.phone);
+ if (est.from?.address) fromLines.push(est.from.address);
+
+ const toLines: string[] = [];
+ if (est.to?.email) toLines.push(est.to.email);
+ if (est.to?.phone) toLines.push(est.to.phone);
+ if (est.to?.address) toLines.push(est.to.address);
+
+ const totals: KeyValueItem[] = [
+ { label: 'Subtotal', value: formatCurrency(est.subtotal ?? 0, currency) },
+ ];
+ if (est.discount && est.discount > 0) {
+ totals.push({ label: 'Discount', value: `-${formatCurrency(est.discount, currency)}`, variant: 'success' });
+ }
+ if (est.tax !== undefined) {
+ totals.push({ label: est.taxRate ? `Tax (${est.taxRate}%)` : 'Tax', value: formatCurrency(est.tax, currency) });
+ }
+ totals.push({ label: 'Total', value: formatCurrency(est.total ?? 0, currency), isTotalRow: true });
+
+ const details: KeyValueItem[] = [
+ { label: 'Estimate Number', value: est.estimateNumber || '—' },
+ { label: 'Date Created', value: formatDate(est.createdAt) },
+ { label: 'Valid Until', value: formatDate(est.expiryDate || est.validUntil) },
+ ];
+
+ const variant = statusVariant(est.status);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {est.status && (
+
+ Status:
+
+
+ )}
+
+ {est.notes && (
+
+ )}
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/estimate-preview/index.html b/src/ui/react-app/src/apps/estimate-preview/index.html
new file mode 100644
index 0000000..5b3f27b
--- /dev/null
+++ b/src/ui/react-app/src/apps/estimate-preview/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Estimate Preview
+
+
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/estimate-preview/main.tsx b/src/ui/react-app/src/apps/estimate-preview/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/estimate-preview/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/estimate-preview/vite.config.ts b/src/ui/react-app/src/apps/estimate-preview/vite.config.ts
new file mode 100644
index 0000000..2152970
--- /dev/null
+++ b/src/ui/react-app/src/apps/estimate-preview/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/estimate-preview'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/form-list/App.tsx b/src/ui/react-app/src/apps/form-list/App.tsx
new file mode 100644
index 0000000..a93ca40
--- /dev/null
+++ b/src/ui/react-app/src/apps/form-list/App.tsx
@@ -0,0 +1,120 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { DataTable } from '../../components/data/DataTable';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatDate(d?: string): string {
+ if (!d) return '\u2014';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [search, setSearch] = useState('');
+ const [activeType, setActiveType] = useState(null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Form List', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const forms: any[] = useMemo(() => data?.forms || [], [data]);
+
+ const typeChips = useMemo(() => {
+ const types = new Set();
+ forms.forEach((f) => { if (f.type) types.add(f.type); });
+ return Array.from(types).sort();
+ }, [forms]);
+
+ const rows = useMemo(() => {
+ return forms
+ .map((f) => ({
+ id: f.id || '',
+ name: f.name || 'Untitled Form',
+ type: f.type || 'form',
+ submissions: f.submissions ?? f.submissionCount ?? 0,
+ createdAt: f.createdAt
+ ? formatDate(f.createdAt)
+ : f.dateAdded || '—',
+ }))
+ .filter((r) => {
+ if (activeType && r.type.toLowerCase() !== activeType.toLowerCase()) return false;
+ if (!search) return true;
+ const q = search.toLowerCase();
+ return r.name.toLowerCase().includes(q) || r.type.toLowerCase().includes(q);
+ });
+ }, [forms, search, activeType]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ return (
+
+
+ setSearch(e.target.value)}
+ />
+
+
+ {typeChips.length > 1 && (
+
+
+ {typeChips.map((t) => (
+
+ ))}
+
+ )}
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/form-list/index.html b/src/ui/react-app/src/apps/form-list/index.html
new file mode 100644
index 0000000..d5b859b
--- /dev/null
+++ b/src/ui/react-app/src/apps/form-list/index.html
@@ -0,0 +1,5 @@
+
+
+Forms
+
+
diff --git a/src/ui/react-app/src/apps/form-list/main.tsx b/src/ui/react-app/src/apps/form-list/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/form-list/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/form-list/vite.config.ts b/src/ui/react-app/src/apps/form-list/vite.config.ts
new file mode 100644
index 0000000..2e0f2d3
--- /dev/null
+++ b/src/ui/react-app/src/apps/form-list/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/form-list'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/form-submissions/App.tsx b/src/ui/react-app/src/apps/form-submissions/App.tsx
new file mode 100644
index 0000000..a1cd93f
--- /dev/null
+++ b/src/ui/react-app/src/apps/form-submissions/App.tsx
@@ -0,0 +1,113 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { DetailHeader } from '../../components/data/DetailHeader';
+import { DataTable } from '../../components/data/DataTable';
+import { SplitLayout } from '../../components/layout/SplitLayout';
+import { Card } from '../../components/layout/Card';
+import { KeyValueList } from '../../components/data/KeyValueList';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatDate(d?: string): string {
+ if (!d) return '\u2014';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [selectedIdx, setSelectedIdx] = useState(null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Form Submissions', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const formName = data?.formName || data?.name || 'Form';
+ const submissions: any[] = useMemo(() => data?.submissions || [], [data]);
+
+ const rows = useMemo(() => {
+ return submissions.map((s, i) => {
+ const contact = s.contactName || s.name || s.email || 'Anonymous';
+ const date = s.createdAt
+ ? formatDate(s.createdAt)
+ : s.submittedAt ? formatDate(s.submittedAt) : '—';
+ const fields = s.data || s.fields || s.answers || {};
+ const preview = Object.entries(fields)
+ .slice(0, 3)
+ .map(([k, v]) => `${k}: ${v}`)
+ .join(', ');
+ return { id: s.id || String(i), contact, date, preview: preview || '—', _idx: i };
+ });
+ }, [submissions]);
+
+ const selectedSubmission = selectedIdx !== null ? submissions[selectedIdx] : null;
+ const selectedFields = selectedSubmission
+ ? Object.entries(selectedSubmission.data || selectedSubmission.fields || selectedSubmission.answers || {})
+ .map(([k, v]) => ({ label: k, value: String(v) }))
+ : [];
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ return (
+
+
0 ? 'Active' : 'Empty'}
+ statusVariant={submissions.length > 0 ? 'active' : 'draft'}
+ />
+
+
+
+
+
+
+
+ {selectedSubmission ? (
+
+
+
+ ) : (
+
+
+ Click a row to view details
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/form-submissions/index.html b/src/ui/react-app/src/apps/form-submissions/index.html
new file mode 100644
index 0000000..b845330
--- /dev/null
+++ b/src/ui/react-app/src/apps/form-submissions/index.html
@@ -0,0 +1,5 @@
+
+
+Form Submissions
+
+
diff --git a/src/ui/react-app/src/apps/form-submissions/main.tsx b/src/ui/react-app/src/apps/form-submissions/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/form-submissions/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/form-submissions/vite.config.ts b/src/ui/react-app/src/apps/form-submissions/vite.config.ts
new file mode 100644
index 0000000..8fbe369
--- /dev/null
+++ b/src/ui/react-app/src/apps/form-submissions/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/form-submissions'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/free-slots-finder/App.tsx b/src/ui/react-app/src/apps/free-slots-finder/App.tsx
new file mode 100644
index 0000000..745bf32
--- /dev/null
+++ b/src/ui/react-app/src/apps/free-slots-finder/App.tsx
@@ -0,0 +1,146 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { Card } from '../../components/layout/Card';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+interface TimeSlot {
+ date: string;
+ time: string;
+ endTime?: string;
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [selectedSlot, setSelectedSlot] = useState(null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Free Slots Finder', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ const freeSlots = data?.freeSlots;
+ const calendarId = data?.calendarId;
+
+ // Normalize slots into a flat list grouped by date
+ const slotsByDate: Record = useMemo(() => {
+ if (!freeSlots) return {};
+ const grouped: Record = {};
+
+ // Handle various formats: array of slots or object with date keys
+ let slotList: any[] = [];
+ if (Array.isArray(freeSlots)) {
+ slotList = freeSlots;
+ } else if (freeSlots.slots) {
+ slotList = freeSlots.slots;
+ } else if (typeof freeSlots === 'object') {
+ // Object keyed by date
+ for (const [dateKey, times] of Object.entries(freeSlots)) {
+ if (dateKey === 'calendarId') continue;
+ const timesArr = Array.isArray(times) ? times : [];
+ for (const t of timesArr) {
+ const slot = typeof t === 'string' ? { date: dateKey, time: t } : { date: dateKey, ...t };
+ slotList.push(slot);
+ }
+ }
+ }
+
+ for (const slot of slotList) {
+ const dateStr = slot.date || (slot.startTime ? new Date(slot.startTime).toISOString().split('T')[0] : '');
+ if (!dateStr) continue;
+ const timeStr = slot.time || (slot.startTime ? new Date(slot.startTime).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }) : '');
+ if (!grouped[dateStr]) grouped[dateStr] = [];
+ grouped[dateStr].push({ date: dateStr, time: timeStr, endTime: slot.endTime });
+ }
+
+ return grouped;
+ }, [freeSlots]);
+
+ const sortedDates = useMemo(() => Object.keys(slotsByDate).sort(), [slotsByDate]);
+ const totalSlots = useMemo(() => sortedDates.reduce((acc, d) => acc + slotsByDate[d].length, 0), [sortedDates, slotsByDate]);
+
+ if (error) {
+ return ;
+ }
+ if (!isConnected) {
+ return ;
+ }
+ if (!data) {
+ return Waiting for availability data...
;
+ }
+
+ return (
+
+
+
+ {sortedDates.length === 0 ? (
+
+
📅
+
No available time slots found
+
+ ) : (
+ sortedDates.map((dateStr) => {
+ const daySlots = slotsByDate[dateStr];
+ const formattedDate = new Date(dateStr + 'T12:00:00').toLocaleDateString('en-US', {
+ weekday: 'long',
+ month: 'long',
+ day: 'numeric',
+ });
+
+ return (
+
+
+ {daySlots.map((slot, i) => {
+ const slotKey = `${slot.date}-${slot.time}`;
+ const isSelected = selectedSlot === slotKey;
+ return (
+
+ );
+ })}
+
+
+ );
+ })
+ )}
+
+ {selectedSlot && (
+
+ ✓ Selected: {selectedSlot.replace('-', ' at ')}
+
+ )}
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/free-slots-finder/index.html b/src/ui/react-app/src/apps/free-slots-finder/index.html
new file mode 100644
index 0000000..60e2a96
--- /dev/null
+++ b/src/ui/react-app/src/apps/free-slots-finder/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/free-slots-finder/main.tsx b/src/ui/react-app/src/apps/free-slots-finder/main.tsx
new file mode 100644
index 0000000..8ebb093
--- /dev/null
+++ b/src/ui/react-app/src/apps/free-slots-finder/main.tsx
@@ -0,0 +1,9 @@
+import React, { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.js';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/src/ui/react-app/src/apps/free-slots-finder/vite.config.ts b/src/ui/react-app/src/apps/free-slots-finder/vite.config.ts
new file mode 100644
index 0000000..378bb1f
--- /dev/null
+++ b/src/ui/react-app/src/apps/free-slots-finder/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/free-slots-finder'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/funnel-detail/App.tsx b/src/ui/react-app/src/apps/funnel-detail/App.tsx
new file mode 100644
index 0000000..29cf6fb
--- /dev/null
+++ b/src/ui/react-app/src/apps/funnel-detail/App.tsx
@@ -0,0 +1,142 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { DetailHeader } from '../../components/data/DetailHeader';
+import { DataTable } from '../../components/data/DataTable';
+import { KeyValueList } from '../../components/data/KeyValueList';
+import { Card } from '../../components/layout/Card';
+import { SplitLayout } from '../../components/layout/SplitLayout';
+import { StatsGrid } from '../../components/layout/StatsGrid';
+import { MetricCard } from '../../components/data/MetricCard';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatDate(d?: string): string {
+ if (!d) return '\u2014';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Funnel Detail', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const funnel = data?.funnel || data;
+ const funnelName = funnel?.name || 'Funnel';
+ const funnelType = funnel?.type || 'funnel';
+ const status = funnel?.status || 'draft';
+ const pages: any[] = useMemo(() => funnel?.steps || funnel?.pages || [], [funnel]);
+
+ const totalViews = useMemo(
+ () => pages.reduce((sum: number, p: any) => sum + (p.views ?? p.visits ?? 0), 0),
+ [pages]
+ );
+ const totalConversions = useMemo(
+ () => pages.reduce((sum: number, p: any) => sum + (p.conversions ?? p.optIns ?? 0), 0),
+ [pages]
+ );
+ const conversionRate = totalViews > 0
+ ? ((totalConversions / totalViews) * 100).toFixed(1)
+ : '0.0';
+
+ const pageRows = useMemo(() => {
+ return pages.map((p, i) => ({
+ id: p.id || String(i),
+ name: p.name || p.title || `Page ${i + 1}`,
+ url: p.url || p.path || '—',
+ views: p.views ?? p.visits ?? 0,
+ conversions: p.conversions ?? p.optIns ?? 0,
+ rate: (p.views ?? p.visits ?? 0) > 0
+ ? `${(((p.conversions ?? p.optIns ?? 0) / (p.views ?? p.visits ?? 1)) * 100).toFixed(1)}%`
+ : '—',
+ }));
+ }, [pages]);
+
+ const metadataItems = useMemo(() => {
+ const items = [
+ { label: 'Type', value: funnelType },
+ { label: 'Status', value: status },
+ { label: 'Total Pages', value: String(pages.length) },
+ ];
+ if (funnel?.category) items.push({ label: 'Category', value: funnel.category });
+ if (funnel?.createdAt) items.push({ label: 'Created', value: formatDate(funnel.createdAt) });
+ if (funnel?.updatedAt) items.push({ label: 'Updated', value: formatDate(funnel.updatedAt) });
+ if (funnel?.locationId) items.push({ label: 'Location ID', value: funnel.locationId });
+ return items;
+ }, [funnel, funnelType, status, pages.length]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const statusVariant = status === 'published' || status === 'active' ? 'active' as const
+ : status === 'completed' ? 'complete' as const
+ : 'draft' as const;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/funnel-detail/index.html b/src/ui/react-app/src/apps/funnel-detail/index.html
new file mode 100644
index 0000000..07e0402
--- /dev/null
+++ b/src/ui/react-app/src/apps/funnel-detail/index.html
@@ -0,0 +1,5 @@
+
+
+Funnel Detail
+
+
diff --git a/src/ui/react-app/src/apps/funnel-detail/main.tsx b/src/ui/react-app/src/apps/funnel-detail/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/funnel-detail/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/funnel-detail/vite.config.ts b/src/ui/react-app/src/apps/funnel-detail/vite.config.ts
new file mode 100644
index 0000000..76ceebc
--- /dev/null
+++ b/src/ui/react-app/src/apps/funnel-detail/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/funnel-detail'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/funnel-list/App.tsx b/src/ui/react-app/src/apps/funnel-list/App.tsx
new file mode 100644
index 0000000..27fa749
--- /dev/null
+++ b/src/ui/react-app/src/apps/funnel-list/App.tsx
@@ -0,0 +1,138 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { DataTable } from '../../components/data/DataTable';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [search, setSearch] = useState('');
+ const [activeType, setActiveType] = useState(null);
+ const [activeCategory, setActiveCategory] = useState(null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Funnel List', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const funnels: any[] = useMemo(() => data?.funnels || [], [data]);
+
+ const typeChips = useMemo(() => {
+ const types = new Set();
+ funnels.forEach((f) => { if (f.type) types.add(f.type); });
+ return Array.from(types).sort();
+ }, [funnels]);
+
+ const categoryChips = useMemo(() => {
+ const cats = new Set();
+ funnels.forEach((f) => { if (f.category) cats.add(f.category); });
+ return Array.from(cats).sort();
+ }, [funnels]);
+
+ const rows = useMemo(() => {
+ return funnels
+ .map((f) => ({
+ id: f.id || '',
+ name: f.name || 'Untitled',
+ type: f.type || 'funnel',
+ pages: f.steps?.length ?? f.pagesCount ?? f.pages ?? 0,
+ category: f.category || '—',
+ status: f.status || 'draft',
+ }))
+ .filter((r) => {
+ if (activeType && r.type.toLowerCase() !== activeType.toLowerCase()) return false;
+ if (activeCategory && r.category.toLowerCase() !== activeCategory.toLowerCase()) return false;
+ if (!search) return true;
+ const q = search.toLowerCase();
+ return (
+ r.name.toLowerCase().includes(q) ||
+ r.type.toLowerCase().includes(q) ||
+ r.category.toLowerCase().includes(q)
+ );
+ });
+ }, [funnels, search, activeType, activeCategory]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ return (
+
+
+ setSearch(e.target.value)}
+ />
+
+
+
+ {typeChips.length > 1 && (
+
+ Type:
+
+ {typeChips.map((t) => (
+
+ ))}
+
+ )}
+ {categoryChips.length > 1 && (
+
+ Category:
+
+ {categoryChips.map((c) => (
+
+ ))}
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/funnel-list/index.html b/src/ui/react-app/src/apps/funnel-list/index.html
new file mode 100644
index 0000000..cbfbbec
--- /dev/null
+++ b/src/ui/react-app/src/apps/funnel-list/index.html
@@ -0,0 +1,5 @@
+
+
+Funnels
+
+
diff --git a/src/ui/react-app/src/apps/funnel-list/main.tsx b/src/ui/react-app/src/apps/funnel-list/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/funnel-list/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/funnel-list/vite.config.ts b/src/ui/react-app/src/apps/funnel-list/vite.config.ts
new file mode 100644
index 0000000..3c3a5a3
--- /dev/null
+++ b/src/ui/react-app/src/apps/funnel-list/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/funnel-list'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/inventory-dashboard/App.tsx b/src/ui/react-app/src/apps/inventory-dashboard/App.tsx
new file mode 100644
index 0000000..83e5ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/inventory-dashboard/App.tsx
@@ -0,0 +1,176 @@
+/**
+ * Inventory Dashboard — Stock levels, alerts, and category chart.
+ */
+import React, { useMemo, useState } from "react";
+import { useApp } from "@modelcontextprotocol/ext-apps/react";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import { PageHeader } from "../../components/layout/PageHeader.js";
+import { StatsGrid } from "../../components/layout/StatsGrid.js";
+import { MetricCard } from "../../components/data/MetricCard.js";
+import { DataTable } from "../../components/data/DataTable.js";
+import { BarChart } from "../../components/charts/BarChart.js";
+import type { TableColumn, BarChartBar } from "../../types.js";
+import "../../styles/base.css";
+
+// ─── Data Shape ─────────────────────────────────────────────
+
+interface InventoryItem {
+ id?: string;
+ name?: string;
+ sku?: string;
+ category?: string;
+ quantity?: number;
+ stockQuantity?: number;
+ lowStockThreshold?: number;
+ status?: string;
+ price?: number;
+ currency?: string;
+}
+
+interface AppData {
+ inventory?: InventoryItem[];
+}
+
+// ─── Helpers ────────────────────────────────────────────────
+
+function extractData(result: CallToolResult): AppData | null {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc as AppData;
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === "text") {
+ try { return JSON.parse(item.text) as AppData; } catch { continue; }
+ }
+ }
+ }
+ return null;
+}
+
+function stockLevel(qty: number, threshold = 10): string {
+ if (qty <= 0) return "🔴 Out of Stock";
+ if (qty <= threshold) return "🟡 Low Stock";
+ return "🟢 In Stock";
+}
+
+// ─── Component ──────────────────────────────────────────────
+
+export function App() {
+ const [data, setData] = useState(
+ () => (window as any).__MCP_APP_DATA__ || null
+ );
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: "Inventory Dashboard", version: "1.0.0" },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ const inventory = data?.inventory || [];
+
+ // Compute stats
+ const stats = useMemo(() => {
+ let totalSKUs = inventory.length;
+ let lowStock = 0;
+ let outOfStock = 0;
+
+ for (const item of inventory) {
+ const qty = item.quantity ?? item.stockQuantity ?? 0;
+ const threshold = item.lowStockThreshold ?? 10;
+ if (qty <= 0) outOfStock++;
+ else if (qty <= threshold) lowStock++;
+ }
+
+ return { totalSKUs, lowStock, outOfStock };
+ }, [inventory]);
+
+ // Build stock by category for bar chart
+ const categoryBars: BarChartBar[] = useMemo(() => {
+ const catMap = new Map();
+ for (const item of inventory) {
+ const cat = item.category || "Uncategorized";
+ const qty = item.quantity ?? item.stockQuantity ?? 0;
+ catMap.set(cat, (catMap.get(cat) || 0) + qty);
+ }
+ return Array.from(catMap.entries())
+ .map(([label, value]) => ({ label, value }))
+ .sort((a, b) => b.value - a.value);
+ }, [inventory]);
+
+ // Build table rows with stock indicators
+ const rows = useMemo(() => inventory.map((item) => {
+ const qty = item.quantity ?? item.stockQuantity ?? 0;
+ const threshold = item.lowStockThreshold ?? 10;
+ return {
+ id: item.id || item.sku || "",
+ name: item.name || "—",
+ sku: item.sku || "—",
+ category: item.category || "—",
+ quantity: qty.toLocaleString(),
+ status: stockLevel(qty, threshold),
+ };
+ }), [inventory]);
+
+ const columns: TableColumn[] = [
+ { key: "name", label: "Product", sortable: true },
+ { key: "sku", label: "SKU", sortable: true },
+ { key: "category", label: "Category", sortable: true },
+ { key: "quantity", label: "Stock", sortable: true },
+ { key: "status", label: "Status", sortable: true },
+ ];
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ return (
+
+
+
+
+
+
+
+
+
+ {categoryBars.length > 0 && (
+
+
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/inventory-dashboard/index.html b/src/ui/react-app/src/apps/inventory-dashboard/index.html
new file mode 100644
index 0000000..15d3f87
--- /dev/null
+++ b/src/ui/react-app/src/apps/inventory-dashboard/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Inventory Dashboard
+
+
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/inventory-dashboard/main.tsx b/src/ui/react-app/src/apps/inventory-dashboard/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/inventory-dashboard/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/inventory-dashboard/vite.config.ts b/src/ui/react-app/src/apps/inventory-dashboard/vite.config.ts
new file mode 100644
index 0000000..2a81cbe
--- /dev/null
+++ b/src/ui/react-app/src/apps/inventory-dashboard/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/inventory-dashboard'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/invoice-builder/App.tsx b/src/ui/react-app/src/apps/invoice-builder/App.tsx
new file mode 100644
index 0000000..900aee8
--- /dev/null
+++ b/src/ui/react-app/src/apps/invoice-builder/App.tsx
@@ -0,0 +1,78 @@
+/**
+ * Invoice Builder — Create invoice form using the InvoiceBuilder component.
+ */
+import React, { useState } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { InvoiceBuilder } from '../../components/interactive/InvoiceBuilder';
+import { Card } from '../../components/layout/Card';
+import { MCPAppProvider } from '../../context/MCPAppContext';
+import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
+import type { LineItem } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [appInstance, setAppInstance] = useState(null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Invoice Builder', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ setAppInstance(a);
+ a.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ if (error) return ;
+ if (!isConnected) return ;
+
+ const appData = data || {};
+
+ const initialItems: LineItem[] | undefined = appData.items?.map((item: any) => ({
+ name: item.name || item.description || '',
+ description: item.description,
+ quantity: item.quantity ?? 1,
+ unitPrice: item.unitPrice ?? 0,
+ total: item.total ?? (item.quantity ?? 1) * (item.unitPrice ?? 0),
+ }));
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/invoice-builder/index.html b/src/ui/react-app/src/apps/invoice-builder/index.html
new file mode 100644
index 0000000..6c36335
--- /dev/null
+++ b/src/ui/react-app/src/apps/invoice-builder/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Invoice Builder
+
+
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/invoice-builder/main.tsx b/src/ui/react-app/src/apps/invoice-builder/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/invoice-builder/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/invoice-builder/vite.config.ts b/src/ui/react-app/src/apps/invoice-builder/vite.config.ts
new file mode 100644
index 0000000..759927b
--- /dev/null
+++ b/src/ui/react-app/src/apps/invoice-builder/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/invoice-builder'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/invoice-list/App.tsx b/src/ui/react-app/src/apps/invoice-list/App.tsx
new file mode 100644
index 0000000..e2204f1
--- /dev/null
+++ b/src/ui/react-app/src/apps/invoice-list/App.tsx
@@ -0,0 +1,120 @@
+/**
+ * Invoice List — All invoices table with status filtering and stats.
+ */
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { DataTable } from '../../components/data/DataTable';
+import { StatsGrid } from '../../components/layout/StatsGrid';
+import { MetricCard } from '../../components/data/MetricCard';
+import type { TableColumn } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatCurrency(n: number, currency = 'USD'): string {
+ try { return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n); }
+ catch { return `$${n.toFixed(2)}`; }
+}
+
+function formatDate(d?: string): string {
+ if (!d) return '—';
+ try { return new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); }
+ catch { return d; }
+}
+
+const STATUS_FILTERS = ['all', 'draft', 'sent', 'paid', 'overdue'] as const;
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [activeFilter, setActiveFilter] = useState('all');
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Invoice List', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const invoices: any[] = data.invoices || [];
+ const currency = data.currency || 'USD';
+
+ // Stats
+ const stats = useMemo(() => {
+ let totalOutstanding = 0, totalPaid = 0, overdueCount = 0;
+ for (const inv of invoices) {
+ const s = (inv.status || '').toLowerCase();
+ const amt = inv.amount ?? 0;
+ if (s === 'paid') totalPaid += amt;
+ else if (s === 'overdue') { totalOutstanding += amt; overdueCount++; }
+ else if (s !== 'cancelled' && s !== 'void') totalOutstanding += amt;
+ }
+ return { totalOutstanding, totalPaid, overdueCount };
+ }, [invoices]);
+
+ // Filter
+ const filtered = useMemo(() => {
+ if (activeFilter === 'all') return invoices;
+ return invoices.filter((inv: any) => (inv.status || '').toLowerCase() === activeFilter);
+ }, [invoices, activeFilter]);
+
+ const rows = filtered.map((inv: any) => ({
+ id: inv.id || inv.invoiceNumber || '',
+ invoiceNumber: inv.invoiceNumber || '—',
+ contact: inv.contact || inv.contactName || '—',
+ amount: formatCurrency(inv.amount ?? 0, inv.currency || currency),
+ status: inv.status || 'draft',
+ dueDate: formatDate(inv.dueDate),
+ createdAt: formatDate(inv.createdAt),
+ }));
+
+ const columns: TableColumn[] = [
+ { key: 'invoiceNumber', label: 'Invoice #', sortable: true },
+ { key: 'contact', label: 'Contact', sortable: true, format: 'avatar' },
+ { key: 'amount', label: 'Amount', sortable: true, format: 'currency' },
+ { key: 'status', label: 'Status', sortable: true, format: 'status' },
+ { key: 'dueDate', label: 'Due Date', sortable: true, format: 'date' },
+ { key: 'createdAt', label: 'Created', sortable: true, format: 'date' },
+ ];
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {STATUS_FILTERS.map((f) => (
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/invoice-list/index.html b/src/ui/react-app/src/apps/invoice-list/index.html
new file mode 100644
index 0000000..1112c71
--- /dev/null
+++ b/src/ui/react-app/src/apps/invoice-list/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Invoice List
+
+
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/invoice-list/main.tsx b/src/ui/react-app/src/apps/invoice-list/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/invoice-list/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/invoice-list/vite.config.ts b/src/ui/react-app/src/apps/invoice-list/vite.config.ts
new file mode 100644
index 0000000..0a2d050
--- /dev/null
+++ b/src/ui/react-app/src/apps/invoice-list/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/invoice-list'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/invoice-preview/App.tsx b/src/ui/react-app/src/apps/invoice-preview/App.tsx
new file mode 100644
index 0000000..75b2abd
--- /dev/null
+++ b/src/ui/react-app/src/apps/invoice-preview/App.tsx
@@ -0,0 +1,172 @@
+/**
+ * Invoice Preview — Formatted invoice view with line items and totals.
+ */
+import React, { useState } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { DetailHeader } from '../../components/data/DetailHeader';
+import { SplitLayout } from '../../components/layout/SplitLayout';
+import { InfoBlock } from '../../components/data/InfoBlock';
+import { LineItemsTable } from '../../components/data/LineItemsTable';
+import { KeyValueList } from '../../components/data/KeyValueList';
+import { StatusBadge } from '../../components/data/StatusBadge';
+import { CurrencyDisplay } from '../../components/data/CurrencyDisplay';
+import type { StatusVariant, KeyValueItem, LineItem } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+// ─── Helpers ────────────────────────────────────────────────
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function statusVariant(status?: string): StatusVariant {
+ const s = (status || '').toLowerCase();
+ if (s === 'paid') return 'paid';
+ if (s === 'sent') return 'sent';
+ if (s === 'draft') return 'draft';
+ if (s === 'overdue') return 'error';
+ if (s === 'cancelled' || s === 'void') return 'lost';
+ if (s === 'pending') return 'pending';
+ return 'active';
+}
+
+function formatCurrency(n: number, currency = 'USD'): string {
+ try {
+ return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n);
+ } catch {
+ return `$${n.toFixed(2)}`;
+ }
+}
+
+function formatDate(d?: string): string {
+ if (!d) return '—';
+ try {
+ return new Date(d).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
+ } catch {
+ return d;
+ }
+}
+
+// ─── Component ──────────────────────────────────────────────
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Invoice Preview', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const inv = data.invoice || {};
+ const currency = inv.currency || 'USD';
+
+ // Build line items
+ const lineItems: LineItem[] = (inv.items || []).map((item: any) => ({
+ name: item.name || 'Item',
+ description: item.description,
+ quantity: item.quantity ?? 1,
+ unitPrice: item.unitPrice ?? 0,
+ total: item.total ?? (item.quantity ?? 1) * (item.unitPrice ?? 0),
+ }));
+
+ // From/To info
+ const fromLines: string[] = [];
+ if (inv.from?.email) fromLines.push(inv.from.email);
+ if (inv.from?.phone) fromLines.push(inv.from.phone);
+ if (inv.from?.address) fromLines.push(inv.from.address);
+
+ const toLines: string[] = [];
+ if (inv.to?.email) toLines.push(inv.to.email);
+ if (inv.to?.phone) toLines.push(inv.to.phone);
+ if (inv.to?.address) toLines.push(inv.to.address);
+
+ // Totals
+ const totals: KeyValueItem[] = [
+ { label: 'Subtotal', value: formatCurrency(inv.subtotal ?? 0, currency) },
+ ];
+ if (inv.discount && inv.discount > 0) {
+ totals.push({ label: 'Discount', value: `-${formatCurrency(inv.discount, currency)}`, variant: 'success' });
+ }
+ if (inv.tax !== undefined) {
+ totals.push({ label: inv.taxRate ? `Tax (${inv.taxRate}%)` : 'Tax', value: formatCurrency(inv.tax, currency) });
+ }
+ totals.push({ label: 'Total', value: formatCurrency(inv.total ?? 0, currency), isTotalRow: true });
+ if (inv.amountPaid !== undefined && inv.amountPaid > 0) {
+ totals.push({ label: 'Amount Paid', value: formatCurrency(inv.amountPaid, currency), variant: 'success' });
+ }
+ if (inv.amountDue !== undefined) {
+ totals.push({ label: 'Amount Due', value: formatCurrency(inv.amountDue, currency), bold: true });
+ }
+
+ const details: KeyValueItem[] = [
+ { label: 'Invoice Number', value: inv.invoiceNumber || '—' },
+ { label: 'Date Issued', value: formatDate(inv.createdAt) },
+ { label: 'Due Date', value: formatDate(inv.dueDate) },
+ ];
+ if (inv.paidDate) details.push({ label: 'Date Paid', value: formatDate(inv.paidDate), variant: 'success' });
+
+ const variant = statusVariant(inv.status);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {inv.status && (
+
+ Payment Status:
+
+
+ )}
+
+ {inv.notes && (
+
+ )}
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/invoice-preview/index.html b/src/ui/react-app/src/apps/invoice-preview/index.html
new file mode 100644
index 0000000..6fea71b
--- /dev/null
+++ b/src/ui/react-app/src/apps/invoice-preview/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Invoice Preview
+
+
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/invoice-preview/main.tsx b/src/ui/react-app/src/apps/invoice-preview/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/invoice-preview/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/invoice-preview/vite.config.ts b/src/ui/react-app/src/apps/invoice-preview/vite.config.ts
new file mode 100644
index 0000000..c807ea6
--- /dev/null
+++ b/src/ui/react-app/src/apps/invoice-preview/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/invoice-preview'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/location-dashboard/App.tsx b/src/ui/react-app/src/apps/location-dashboard/App.tsx
new file mode 100644
index 0000000..911c403
--- /dev/null
+++ b/src/ui/react-app/src/apps/location-dashboard/App.tsx
@@ -0,0 +1,253 @@
+/**
+ * Location Dashboard — Main overview dashboard for a GHL location.
+ * Stats: total contacts, active deals, total pipeline value, upcoming appointments.
+ * Pie chart: deals by stage. Table: recent contacts. Sparklines in metric cards.
+ */
+import React, { useMemo } from "react";
+import { useApp } from "@modelcontextprotocol/ext-apps/react";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import { PageHeader } from "../../components/layout/PageHeader.js";
+import { StatsGrid } from "../../components/layout/StatsGrid.js";
+import { SplitLayout } from "../../components/layout/SplitLayout.js";
+import { Section } from "../../components/layout/Section.js";
+import { Card } from "../../components/layout/Card.js";
+import { MetricCard } from "../../components/data/MetricCard.js";
+import { DataTable } from "../../components/data/DataTable.js";
+import { PieChart } from "../../components/charts/PieChart.js";
+import { SparklineChart } from "../../components/charts/SparklineChart.js";
+import { CurrencyDisplay } from "../../components/data/CurrencyDisplay.js";
+import "../../styles/base.css";
+import "../../styles/interactive.css";
+
+// ─── Types ──────────────────────────────────────────────────
+
+interface Contact {
+ id?: string;
+ name: string;
+ email?: string;
+ phone?: string;
+ dateAdded?: string;
+ tags?: string[];
+ source?: string;
+}
+
+interface Pipeline {
+ id: string;
+ name: string;
+ stages?: PipelineStage[];
+}
+
+interface PipelineStage {
+ id: string;
+ name: string;
+ count: number;
+ value: number;
+}
+
+interface CalendarEntry {
+ id?: string;
+ title: string;
+ date: string;
+ time?: string;
+}
+
+interface LocationDashboardData {
+ recentContacts: Contact[];
+ pipelines: Pipeline[];
+ calendars: CalendarEntry[];
+ locationId: string;
+ locationName?: string;
+ totalContacts?: number;
+ contactsTrend?: number[];
+ activeDeals?: number;
+ dealsTrend?: number[];
+ totalPipelineValue?: number;
+ valueTrend?: number[];
+ upcomingAppointments?: number;
+ appointmentsTrend?: number[];
+}
+
+// ─── Data Extraction ────────────────────────────────────────
+
+function extractData(result: CallToolResult): LocationDashboardData | null {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc as LocationDashboardData;
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === "text") {
+ try {
+ return JSON.parse(item.text) as LocationDashboardData;
+ } catch {
+ /* skip */
+ }
+ }
+ }
+ }
+ return null;
+}
+
+// ─── App ────────────────────────────────────────────────────
+
+export function App() {
+ const [data, setData] = React.useState(null);
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: "location-dashboard", version: "1.0.0" },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const extracted = extractData(result);
+ if (extracted) setData(extracted);
+ };
+ },
+ });
+
+ React.useEffect(() => {
+ const preInjected = (window as any).__MCP_APP_DATA__;
+ if (preInjected && !data) setData(preInjected as LocationDashboardData);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ // Aggregate deals by stage across all pipelines
+ const dealsByStage = useMemo(() => {
+ if (!data?.pipelines) return [];
+ const stageMap: Record = {};
+ for (const pipeline of data.pipelines) {
+ for (const stage of pipeline.stages ?? []) {
+ stageMap[stage.name] = (stageMap[stage.name] ?? 0) + stage.count;
+ }
+ }
+ return Object.entries(stageMap).map(([label, value]) => ({ label, value }));
+ }, [data?.pipelines]);
+
+ // Compute totals from pipeline data if not provided directly
+ const totalContacts = data?.totalContacts ?? data?.recentContacts?.length ?? 0;
+ const activeDeals = useMemo(() => {
+ if (data?.activeDeals != null) return data.activeDeals;
+ return dealsByStage.reduce((sum, s) => sum + s.value, 0);
+ }, [data?.activeDeals, dealsByStage]);
+
+ const totalPipelineValue = useMemo(() => {
+ if (data?.totalPipelineValue != null) return data.totalPipelineValue;
+ if (!data?.pipelines) return 0;
+ let total = 0;
+ for (const pipeline of data.pipelines) {
+ for (const stage of pipeline.stages ?? []) {
+ total += stage.value;
+ }
+ }
+ return total;
+ }, [data?.totalPipelineValue, data?.pipelines]);
+
+ const upcomingAppointments = data?.upcomingAppointments ?? data?.calendars?.length ?? 0;
+
+ const contactColumns = useMemo(() => [
+ { key: "name", label: "Name", sortable: true },
+ { key: "email", label: "Email", format: "email" as const },
+ { key: "phone", label: "Phone", format: "phone" as const },
+ { key: "source", label: "Source" },
+ { key: "dateAdded", label: "Date Added", sortable: true, format: "date" as const },
+ ], []);
+
+ if (error) {
+ return (
+
+
Connection Error
+
{error.message}
+
+ );
+ }
+
+ if (!data) {
+ return (
+
+
+
{isConnected ? "Waiting for data..." : "Connecting..."}
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
{totalContacts.toLocaleString()}
+
Total Contacts
+ {data.contactsTrend && data.contactsTrend.length > 1 && (
+
+ )}
+
+
+
{activeDeals.toLocaleString()}
+
Active Deals
+ {data.dealsTrend && data.dealsTrend.length > 1 && (
+
+ )}
+
+
+
+
+
+
Pipeline Value
+ {data.valueTrend && data.valueTrend.length > 1 && (
+
+ )}
+
+
+
{upcomingAppointments.toLocaleString()}
+
Upcoming Appointments
+ {data.appointmentsTrend && data.appointmentsTrend.length > 1 && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ {(data.pipelines || []).map((p) => (
+
+
{p.name}
+
+ {(p.stages ?? []).length} stages ·{" "}
+ {(p.stages ?? []).reduce((s, st) => s + st.count, 0)} deals ·{" "}
+ s + st.value, 0)}
+ size="sm"
+ />
+
+
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/location-dashboard/index.html b/src/ui/react-app/src/apps/location-dashboard/index.html
new file mode 100644
index 0000000..c6187b4
--- /dev/null
+++ b/src/ui/react-app/src/apps/location-dashboard/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Location Dashboard
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/location-dashboard/main.tsx b/src/ui/react-app/src/apps/location-dashboard/main.tsx
new file mode 100644
index 0000000..c3e2da2
--- /dev/null
+++ b/src/ui/react-app/src/apps/location-dashboard/main.tsx
@@ -0,0 +1,9 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { App } from "./App.js";
+
+createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
diff --git a/src/ui/react-app/src/apps/location-dashboard/vite.config.ts b/src/ui/react-app/src/apps/location-dashboard/vite.config.ts
new file mode 100644
index 0000000..5aeb544
--- /dev/null
+++ b/src/ui/react-app/src/apps/location-dashboard/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/location-dashboard'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/media-library/App.tsx b/src/ui/react-app/src/apps/media-library/App.tsx
new file mode 100644
index 0000000..39228bd
--- /dev/null
+++ b/src/ui/react-app/src/apps/media-library/App.tsx
@@ -0,0 +1,135 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { MediaGallery } from '../../components/viz/MediaGallery';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+type FileFilter = 'all' | 'image' | 'video' | 'document' | 'audio';
+
+const filterOptions: { label: string; value: FileFilter }[] = [
+ { label: '📁 All', value: 'all' },
+ { label: '🖼️ Images', value: 'image' },
+ { label: '🎬 Videos', value: 'video' },
+ { label: '📄 Documents', value: 'document' },
+ { label: '🎵 Audio', value: 'audio' },
+];
+
+function classifyFileType(type: string | undefined, name: string | undefined): string {
+ const t = (type || '').toLowerCase();
+ const n = (name || '').toLowerCase();
+ if (t.includes('image') || /\.(jpg|jpeg|png|gif|webp|svg|bmp)$/i.test(n)) return 'image';
+ if (t.includes('video') || /\.(mp4|mov|avi|webm|mkv)$/i.test(n)) return 'video';
+ if (t.includes('audio') || /\.(mp3|wav|ogg|m4a|flac)$/i.test(n)) return 'audio';
+ return 'document';
+}
+
+function formatFileSize(bytes: number | string | undefined): string {
+ if (!bytes) return '-';
+ const b = typeof bytes === 'string' ? parseInt(bytes, 10) : bytes;
+ if (isNaN(b)) return String(bytes);
+ if (b < 1024) return `${b} B`;
+ if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`;
+ return `${(b / (1024 * 1024)).toFixed(1)} MB`;
+}
+
+function formatDate(d: string | undefined): string {
+ if (!d) return '-';
+ try {
+ return new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
+ } catch { return d; }
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [search, setSearch] = useState('');
+ const [filter, setFilter] = useState('all');
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Media Library', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const items = useMemo(() => {
+ const files: any[] = data?.files || data?.media || data?.items || [];
+ return files
+ .map((f) => {
+ const fileType = classifyFileType(f.type || f.mimeType || f.fileType, f.name || f.title);
+ return {
+ title: f.name || f.title || f.fileName || 'Untitled',
+ url: f.url || f.thumbnailUrl || '',
+ thumbnailUrl: f.thumbnailUrl || f.url || '',
+ fileType: fileType,
+ fileSize: formatFileSize(f.size || f.fileSize),
+ date: formatDate(f.uploadedAt || f.createdAt || f.date),
+ };
+ })
+ .filter((item) => {
+ if (filter !== 'all' && item.fileType !== filter) return false;
+ if (!search) return true;
+ const q = search.toLowerCase();
+ return item.title.toLowerCase().includes(q);
+ });
+ }, [data, search, filter]);
+
+ const counts = useMemo(() => {
+ const files: any[] = data?.files || data?.media || data?.items || [];
+ const c: Record = { all: files.length, image: 0, video: 0, document: 0, audio: 0 };
+ files.forEach((f) => {
+ const ft = classifyFileType(f.type || f.mimeType || f.fileType, f.name || f.title);
+ c[ft] = (c[ft] || 0) + 1;
+ });
+ return c;
+ }, [data]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ return (
+
+
+
+ {filterOptions.map((f) => (
+
+ ))}
+
+
+
+ setSearch(e.target.value)}
+ />
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/media-library/index.html b/src/ui/react-app/src/apps/media-library/index.html
new file mode 100644
index 0000000..01d39a7
--- /dev/null
+++ b/src/ui/react-app/src/apps/media-library/index.html
@@ -0,0 +1,5 @@
+
+
+Media Library
+
+
diff --git a/src/ui/react-app/src/apps/media-library/main.tsx b/src/ui/react-app/src/apps/media-library/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/media-library/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/media-library/vite.config.ts b/src/ui/react-app/src/apps/media-library/vite.config.ts
new file mode 100644
index 0000000..cd41679
--- /dev/null
+++ b/src/ui/react-app/src/apps/media-library/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/media-library'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/message-composer/App.tsx b/src/ui/react-app/src/apps/message-composer/App.tsx
new file mode 100644
index 0000000..2d74f00
--- /dev/null
+++ b/src/ui/react-app/src/apps/message-composer/App.tsx
@@ -0,0 +1,224 @@
+import React, { useState } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { Card } from '../../components/layout/Card';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+type MessageTab = 'sms' | 'email';
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [activeTab, setActiveTab] = useState('sms');
+
+ // SMS fields
+ const [smsPhone, setSmsPhone] = useState('');
+ const [smsBody, setSmsBody] = useState('');
+
+ // Email fields
+ const [emailTo, setEmailTo] = useState('');
+ const [emailSubject, setEmailSubject] = useState('');
+ const [emailBody, setEmailBody] = useState('');
+
+ const [isSending, setIsSending] = useState(false);
+ const [sendResult, setSendResult] = useState<'success' | 'error' | null>(null);
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: 'Message Composer', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ // Pre-fill from contact data
+ const contact = data?.contact;
+ const prefillPhone = contact?.phone || '';
+ const prefillEmail = contact?.email || '';
+ const contactName = contact?.name || [contact?.firstName, contact?.lastName].filter(Boolean).join(' ') || '';
+
+ const handleSend = async () => {
+ if (!app) return;
+ setIsSending(true);
+ setSendResult(null);
+
+ try {
+ if (activeTab === 'sms') {
+ const phone = smsPhone || prefillPhone;
+ if (!phone || !smsBody.trim()) { setIsSending(false); return; }
+ await app.updateModelContext({
+ content: [{
+ type: 'text',
+ text: `User action: Send SMS to ${phone}: "${smsBody.trim()}"`,
+ }],
+ });
+ } else {
+ const to = emailTo || prefillEmail;
+ if (!to || !emailSubject.trim() || !emailBody.trim()) { setIsSending(false); return; }
+ await app.updateModelContext({
+ content: [{
+ type: 'text',
+ text: `User action: Send email to ${to}, subject: "${emailSubject.trim()}", body: "${emailBody.trim()}"`,
+ }],
+ });
+ }
+ setSendResult('success');
+ } catch {
+ setSendResult('error');
+ } finally {
+ setIsSending(false);
+ }
+ };
+
+ if (error) {
+ return ;
+ }
+ if (!isConnected && !data) {
+ return ;
+ }
+
+ const tabs: { label: string; value: MessageTab; icon: string }[] = [
+ { label: 'SMS', value: 'sms', icon: '💬' },
+ { label: 'Email', value: 'email', icon: '📧' },
+ ];
+
+ return (
+
+
+
+ {/* Contact info */}
+ {contact && (
+
+
+
+ {(contactName || 'C').charAt(0).toUpperCase()}
+
+
+
{contactName || 'Contact'}
+
+ {prefillPhone && `📞 ${prefillPhone}`}
+ {prefillPhone && prefillEmail && ' · '}
+ {prefillEmail && `📧 ${prefillEmail}`}
+
+
+
+
+ )}
+
+ {/* Tab group */}
+
+
+ {tabs.map((t) => (
+
+ ))}
+
+
+
+ {/* SMS form */}
+ {activeTab === 'sms' && (
+
+
+
+
+ setSmsPhone(e.target.value)}
+ />
+
+
+
+
+ )}
+
+ {/* Email form */}
+ {activeTab === 'email' && (
+
+
+
+
+ setEmailTo(e.target.value)}
+ />
+
+
+
+ setEmailSubject(e.target.value)}
+ />
+
+
+
+
+
+
+ )}
+
+ {/* Send button */}
+
+
+ {sendResult === 'success' && ✓ Sent}
+ {sendResult === 'error' && ✗ Failed}
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/message-composer/index.html b/src/ui/react-app/src/apps/message-composer/index.html
new file mode 100644
index 0000000..60e2a96
--- /dev/null
+++ b/src/ui/react-app/src/apps/message-composer/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/message-composer/main.tsx b/src/ui/react-app/src/apps/message-composer/main.tsx
new file mode 100644
index 0000000..8ebb093
--- /dev/null
+++ b/src/ui/react-app/src/apps/message-composer/main.tsx
@@ -0,0 +1,9 @@
+import React, { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.js';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/src/ui/react-app/src/apps/message-composer/vite.config.ts b/src/ui/react-app/src/apps/message-composer/vite.config.ts
new file mode 100644
index 0000000..38412ad
--- /dev/null
+++ b/src/ui/react-app/src/apps/message-composer/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/message-composer'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/message-detail/App.tsx b/src/ui/react-app/src/apps/message-detail/App.tsx
new file mode 100644
index 0000000..932b60f
--- /dev/null
+++ b/src/ui/react-app/src/apps/message-detail/App.tsx
@@ -0,0 +1,180 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { DetailHeader } from '../../components/data/DetailHeader';
+import { Card } from '../../components/layout/Card';
+import { KeyValueList } from '../../components/data/KeyValueList';
+import { AudioPlayer } from '../../components/data/AudioPlayer';
+import { TranscriptView } from '../../components/comms/TranscriptView';
+import type { KeyValueItem, TranscriptEntry, StatusVariant } from '../../types';
+import '../../styles/base.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+const typeIcons: Record = {
+ sms: '💬',
+ email: '📧',
+ call: '📞',
+ whatsapp: '📱',
+ voicemail: '📩',
+};
+
+const directionLabels: Record = {
+ inbound: 'Received',
+ outbound: 'Sent',
+ incoming: 'Received',
+ outgoing: 'Sent',
+};
+
+function formatDate(d?: string): string {
+ if (!d) return '\u2014';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Message Detail', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ const message = data?.message;
+ const recording = data?.recording;
+ const transcription = data?.transcription;
+
+ const msgType = (message?.type || message?.messageType || 'sms').toLowerCase();
+ const icon = typeIcons[msgType] || '💬';
+ const direction = message?.direction || 'inbound';
+ const dirLabel = directionLabels[direction] || direction;
+
+ const kvItems: KeyValueItem[] = useMemo(() => {
+ if (!message) return [];
+ const items: KeyValueItem[] = [];
+
+ items.push({ label: 'Type', value: `${icon} ${msgType.toUpperCase()}` });
+ items.push({ label: 'Direction', value: dirLabel });
+
+ if (message.from || message.senderName) {
+ items.push({ label: 'From', value: message.from || message.senderName, bold: true });
+ }
+ if (message.to || message.recipientName) {
+ items.push({ label: 'To', value: message.to || message.recipientName });
+ }
+ if (message.contactName || message.contact?.name) {
+ items.push({ label: 'Contact', value: message.contactName || message.contact?.name, bold: true });
+ }
+ if (message.subject) {
+ items.push({ label: 'Subject', value: message.subject, bold: true });
+ }
+ if (message.dateAdded || message.createdAt || message.date) {
+ const dateStr = message.dateAdded || message.createdAt || message.date;
+ items.push({ label: 'Date', value: formatDate(dateStr) });
+ }
+ if (message.status) {
+ items.push({
+ label: 'Status',
+ value: message.status.charAt(0).toUpperCase() + message.status.slice(1),
+ variant: message.status === 'delivered' || message.status === 'read' ? 'success' : undefined,
+ });
+ }
+ if (message.phone) {
+ items.push({ label: 'Phone', value: message.phone });
+ }
+ if (message.email) {
+ items.push({ label: 'Email', value: message.email });
+ }
+
+ return items;
+ }, [message, msgType, icon, dirLabel]);
+
+ const transcriptEntries: TranscriptEntry[] = useMemo(() => {
+ if (!transcription?.entries && !transcription?.segments) return [];
+ const entries = transcription.entries || transcription.segments || [];
+ return entries.map((e: any) => ({
+ speaker: e.speaker || e.speakerName || 'Unknown',
+ speakerRole: e.speakerRole || e.role || 'customer',
+ text: e.text || e.content || '',
+ timestamp: e.timestamp || e.time || '',
+ }));
+ }, [transcription]);
+
+ if (error) {
+ return ;
+ }
+ if (!isConnected) {
+ return ;
+ }
+ if (!data) {
+ return Waiting for message data...
;
+ }
+
+ const statusVariant: StatusVariant = message?.status === 'delivered' || message?.status === 'read'
+ ? 'complete'
+ : message?.status === 'failed' ? 'error' : 'active';
+
+ return (
+
+
+
+ {/* Message content */}
+
+
+ {message?.body || message?.message || message?.content || message?.text || 'No content'}
+
+
+
+ {/* Metadata */}
+
+
+
+
+ {/* Recording */}
+ {recording && (
+
+
+
+ )}
+
+ {/* Transcription */}
+ {transcription && transcriptEntries.length > 0 && (
+
+
+
+ )}
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/message-detail/index.html b/src/ui/react-app/src/apps/message-detail/index.html
new file mode 100644
index 0000000..60e2a96
--- /dev/null
+++ b/src/ui/react-app/src/apps/message-detail/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/message-detail/main.tsx b/src/ui/react-app/src/apps/message-detail/main.tsx
new file mode 100644
index 0000000..8ebb093
--- /dev/null
+++ b/src/ui/react-app/src/apps/message-detail/main.tsx
@@ -0,0 +1,9 @@
+import React, { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.js';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/src/ui/react-app/src/apps/message-detail/vite.config.ts b/src/ui/react-app/src/apps/message-detail/vite.config.ts
new file mode 100644
index 0000000..4296728
--- /dev/null
+++ b/src/ui/react-app/src/apps/message-detail/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/message-detail'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/opportunity-card/App.tsx b/src/ui/react-app/src/apps/opportunity-card/App.tsx
new file mode 100644
index 0000000..4d7bfba
--- /dev/null
+++ b/src/ui/react-app/src/apps/opportunity-card/App.tsx
@@ -0,0 +1,146 @@
+import React, { useState } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { DetailHeader } from '../../components/data/DetailHeader';
+import { SplitLayout } from '../../components/layout/SplitLayout';
+import { Card } from '../../components/layout/Card';
+import { KeyValueList } from '../../components/data/KeyValueList';
+import { StatusBadge } from '../../components/data/StatusBadge';
+import { Timeline } from '../../components/data/Timeline';
+import type { TimelineEvent, StatusVariant, KeyValueItem } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatCurrency(amount: number): string {
+ return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);
+}
+
+function getStatusVariant(status: string): StatusVariant {
+ switch (status?.toLowerCase()) {
+ case 'won': return 'won';
+ case 'lost': return 'lost';
+ case 'abandoned': return 'abandoned';
+ default: return 'open';
+ }
+}
+
+function formatDate(d?: string): string {
+ if (!d) return '—';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: 'Opportunity Card', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const handleAction = async (action: string) => {
+ if (!app) return;
+ try {
+ await app.updateModelContext({
+ content: [{ type: 'text', text: `User action: ${action} for opportunity ${opp.id || opp.name}` }],
+ });
+ } catch {}
+ };
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const opp = data.opportunity || {};
+ const contact = opp.contact || {};
+ const contactName = contact.name || `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || '—';
+ const status = opp.status || 'open';
+
+ const dealInfo: KeyValueItem[] = [
+ { label: 'Value', value: formatCurrency(opp.monetaryValue || opp.value || 0), bold: true },
+ { label: 'Stage', value: opp.stageName || opp.stage || '—' },
+ { label: 'Status', value: status },
+ { label: 'Pipeline', value: opp.pipelineName || opp.pipeline || '—' },
+ { label: 'Source', value: opp.source || '—' },
+ { label: 'Created', value: formatDate(opp.createdAt || opp.dateAdded) },
+ { label: 'Last Updated', value: formatDate(opp.updatedAt || opp.lastStatusChangeAt) },
+ { label: 'Assigned To', value: opp.assignedTo || '—' },
+ ];
+
+ const contactInfo: KeyValueItem[] = [
+ { label: 'Contact', value: contactName },
+ { label: 'Email', value: contact.email || opp.contactEmail || '—' },
+ { label: 'Phone', value: contact.phone || opp.contactPhone || '—' },
+ { label: 'Company', value: contact.companyName || opp.companyName || '—' },
+ ];
+
+ const activities: TimelineEvent[] = (opp.activities || opp.notes || []).map((a: any) => ({
+ title: a.title || a.type || 'Activity',
+ description: a.description || a.body || '',
+ timestamp: a.timestamp || a.createdAt || '',
+ icon: a.type === 'note' ? 'note' : a.type === 'call' ? 'phone' : a.type === 'email' ? 'email' : 'system',
+ variant: 'default' as const,
+ }));
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {activities.length > 0 && (
+
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/opportunity-card/index.html b/src/ui/react-app/src/apps/opportunity-card/index.html
new file mode 100644
index 0000000..11a43dd
--- /dev/null
+++ b/src/ui/react-app/src/apps/opportunity-card/index.html
@@ -0,0 +1,5 @@
+
+
+Opportunity Card
+
+
diff --git a/src/ui/react-app/src/apps/opportunity-card/main.tsx b/src/ui/react-app/src/apps/opportunity-card/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/opportunity-card/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/opportunity-card/vite.config.ts b/src/ui/react-app/src/apps/opportunity-card/vite.config.ts
new file mode 100644
index 0000000..fc3a36a
--- /dev/null
+++ b/src/ui/react-app/src/apps/opportunity-card/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/opportunity-card'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/opportunity-editor/App.tsx b/src/ui/react-app/src/apps/opportunity-editor/App.tsx
new file mode 100644
index 0000000..94b9bf3
--- /dev/null
+++ b/src/ui/react-app/src/apps/opportunity-editor/App.tsx
@@ -0,0 +1,85 @@
+import React, { useState } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { Card } from '../../components/layout/Card';
+import { MCPAppProvider } from '../../context/MCPAppContext';
+import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
+import { OpportunityEditor } from '../../components/interactive/OpportunityEditor';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function OpportunityEditorInner({ data }: { data: any }) {
+ const opp = data.opportunity || {};
+ const stages: any[] = data.stages || data.pipeline?.stages || [];
+
+ const fields: Record = {
+ id: opp.id || '',
+ name: opp.name || '',
+ monetaryValue: opp.monetaryValue || opp.value || 0,
+ status: opp.status || 'open',
+ stageId: opp.stageId || opp.pipelineStageId || '',
+ source: opp.source || '',
+ assignedTo: opp.assignedTo || '',
+ };
+
+ const stageOptions = stages.map((s: any) => ({
+ id: s.id,
+ label: s.name || s.title || 'Unknown',
+ }));
+
+ return (
+
+
+
+
+
+ );
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [appInstance, setAppInstance] = useState(null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Opportunity Editor', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ setAppInstance(app);
+ app.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/opportunity-editor/index.html b/src/ui/react-app/src/apps/opportunity-editor/index.html
new file mode 100644
index 0000000..9eca01f
--- /dev/null
+++ b/src/ui/react-app/src/apps/opportunity-editor/index.html
@@ -0,0 +1,5 @@
+
+
+Opportunity Editor
+
+
diff --git a/src/ui/react-app/src/apps/opportunity-editor/main.tsx b/src/ui/react-app/src/apps/opportunity-editor/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/opportunity-editor/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/opportunity-editor/vite.config.ts b/src/ui/react-app/src/apps/opportunity-editor/vite.config.ts
new file mode 100644
index 0000000..1c0364e
--- /dev/null
+++ b/src/ui/react-app/src/apps/opportunity-editor/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/opportunity-editor'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/order-detail/App.tsx b/src/ui/react-app/src/apps/order-detail/App.tsx
new file mode 100644
index 0000000..2789083
--- /dev/null
+++ b/src/ui/react-app/src/apps/order-detail/App.tsx
@@ -0,0 +1,288 @@
+/**
+ * Order Detail — Single order view with line items, shipping, and fulfillment timeline.
+ */
+import React, { useState } from "react";
+import { useApp } from "@modelcontextprotocol/ext-apps/react";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import { DetailHeader } from "../../components/data/DetailHeader.js";
+import { SplitLayout } from "../../components/layout/SplitLayout.js";
+import { LineItemsTable } from "../../components/data/LineItemsTable.js";
+import { KeyValueList } from "../../components/data/KeyValueList.js";
+import { Timeline } from "../../components/data/Timeline.js";
+import type { StatusVariant, KeyValueItem, LineItem, TimelineEvent } from "../../types.js";
+import "../../styles/base.css";
+
+// ─── Data Shape ─────────────────────────────────────────────
+
+interface Order {
+ id?: string;
+ orderId?: string;
+ status?: string;
+ contact?: string;
+ contactName?: string;
+ contactEmail?: string;
+ currency?: string;
+ createdAt?: string;
+ items?: Array<{
+ name?: string;
+ description?: string;
+ quantity?: number;
+ unitPrice?: number;
+ total?: number;
+ }>;
+ subtotal?: number;
+ tax?: number;
+ shipping?: number;
+ shippingCost?: number;
+ discount?: number;
+ total?: number;
+ shippingAddress?: {
+ name?: string;
+ line1?: string;
+ line2?: string;
+ city?: string;
+ state?: string;
+ zip?: string;
+ postalCode?: string;
+ country?: string;
+ };
+ shippingMethod?: string;
+ trackingNumber?: string;
+ notes?: string;
+}
+
+interface Fulfillment {
+ id?: string;
+ status?: string;
+ title?: string;
+ description?: string;
+ timestamp?: string;
+ date?: string;
+ trackingNumber?: string;
+ carrier?: string;
+}
+
+interface AppData {
+ order?: Order;
+ fulfillments?: Fulfillment[];
+}
+
+// ─── Helpers ────────────────────────────────────────────────
+
+function extractData(result: CallToolResult): AppData | null {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc as AppData;
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === "text") {
+ try { return JSON.parse(item.text) as AppData; } catch { continue; }
+ }
+ }
+ }
+ return null;
+}
+
+function formatCurrency(n: number, currency = "USD"): string {
+ try {
+ return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(n);
+ } catch {
+ return `$${n.toFixed(2)}`;
+ }
+}
+
+function formatDate(d?: string): string {
+ if (!d) return "—";
+ try {
+ return new Date(d).toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ });
+ } catch {
+ return d;
+ }
+}
+
+function statusVariant(status?: string): StatusVariant {
+ const s = (status || "").toLowerCase();
+ if (s === "delivered" || s === "completed") return "complete";
+ if (s === "shipped") return "sent";
+ if (s === "processing") return "active";
+ if (s === "pending") return "pending";
+ if (s === "cancelled" || s === "refunded") return "lost";
+ return "active";
+}
+
+function fulfillmentVariant(status?: string): "default" | "success" | "warning" | "error" {
+ const s = (status || "").toLowerCase();
+ if (s === "delivered" || s === "completed") return "success";
+ if (s === "shipped" || s === "in_transit") return "default";
+ if (s === "pending") return "warning";
+ if (s === "failed" || s === "cancelled") return "error";
+ return "default";
+}
+
+// ─── Component ──────────────────────────────────────────────
+
+export function App() {
+ const [data, setData] = useState(
+ () => (window as any).__MCP_APP_DATA__ || null
+ );
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: "Order Detail", version: "1.0.0" },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const { order, fulfillments = [] } = data;
+
+ if (!order) {
+ return (
+
+
📦
+
No order data available
+
+ );
+ }
+
+ const o = order;
+ const currency = o.currency || "USD";
+ const variant = statusVariant(o.status);
+
+ // Build line items
+ const lineItems: LineItem[] = (o.items || []).map((item) => ({
+ name: item.name || "Item",
+ description: item.description,
+ quantity: item.quantity ?? 1,
+ unitPrice: item.unitPrice ?? 0,
+ total: item.total ?? (item.quantity ?? 1) * (item.unitPrice ?? 0),
+ }));
+
+ // Order details KV
+ const details: KeyValueItem[] = [
+ { label: "Order ID", value: o.orderId || o.id || "—" },
+ { label: "Date", value: formatDate(o.createdAt) },
+ { label: "Customer", value: o.contact || o.contactName || "—" },
+ ];
+ if (o.contactEmail) {
+ details.push({ label: "Email", value: o.contactEmail });
+ }
+
+ // Shipping details KV
+ const shippingDetails: KeyValueItem[] = [];
+ if (o.shippingAddress) {
+ const addr = o.shippingAddress;
+ const addrLines = [
+ addr.name,
+ addr.line1,
+ addr.line2,
+ [addr.city, addr.state, addr.zip || addr.postalCode].filter(Boolean).join(", "),
+ addr.country,
+ ].filter(Boolean);
+ shippingDetails.push({ label: "Address", value: addrLines.join("\n") });
+ }
+ if (o.shippingMethod) {
+ shippingDetails.push({ label: "Method", value: o.shippingMethod });
+ }
+ if (o.trackingNumber) {
+ shippingDetails.push({ label: "Tracking", value: o.trackingNumber });
+ }
+
+ // Totals KV
+ const totals: KeyValueItem[] = [];
+ if (o.subtotal !== undefined) {
+ totals.push({ label: "Subtotal", value: formatCurrency(o.subtotal, currency) });
+ }
+ if (o.discount && o.discount > 0) {
+ totals.push({
+ label: "Discount",
+ value: `-${formatCurrency(o.discount, currency)}`,
+ variant: "success",
+ });
+ }
+ if (o.tax !== undefined) {
+ totals.push({ label: "Tax", value: formatCurrency(o.tax, currency) });
+ }
+ const shippingCost = o.shipping ?? o.shippingCost;
+ if (shippingCost !== undefined) {
+ totals.push({ label: "Shipping", value: formatCurrency(shippingCost, currency) });
+ }
+ totals.push({
+ label: "Total",
+ value: formatCurrency(o.total ?? 0, currency),
+ isTotalRow: true,
+ });
+
+ // Build fulfillment timeline
+ const timelineEvents: TimelineEvent[] = fulfillments.map((f) => ({
+ title: f.title || f.status || "Update",
+ description: [
+ f.description,
+ f.trackingNumber ? `Tracking: ${f.trackingNumber}` : null,
+ f.carrier ? `Carrier: ${f.carrier}` : null,
+ ]
+ .filter(Boolean)
+ .join(" · "),
+ timestamp: formatDate(f.timestamp || f.date),
+ variant: fulfillmentVariant(f.status),
+ icon: "system",
+ }));
+
+ return (
+
+
+
+
+
+
Order Info
+
+
+
+ {shippingDetails.length > 0 && (
+ <>
+
Shipping
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {timelineEvents.length > 0 && (
+
+
Fulfillment Timeline
+
+
+ )}
+
+ {o.notes && (
+
+ )}
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/order-detail/index.html b/src/ui/react-app/src/apps/order-detail/index.html
new file mode 100644
index 0000000..47a8d36
--- /dev/null
+++ b/src/ui/react-app/src/apps/order-detail/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Order Detail
+
+
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/order-detail/main.tsx b/src/ui/react-app/src/apps/order-detail/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/order-detail/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/order-detail/vite.config.ts b/src/ui/react-app/src/apps/order-detail/vite.config.ts
new file mode 100644
index 0000000..9938cf5
--- /dev/null
+++ b/src/ui/react-app/src/apps/order-detail/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/order-detail'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/order-list/App.tsx b/src/ui/react-app/src/apps/order-list/App.tsx
new file mode 100644
index 0000000..336462b
--- /dev/null
+++ b/src/ui/react-app/src/apps/order-list/App.tsx
@@ -0,0 +1,202 @@
+/**
+ * Order List — Orders table with status filtering and stats.
+ */
+import React, { useState, useMemo } from "react";
+import { useApp } from "@modelcontextprotocol/ext-apps/react";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import { PageHeader } from "../../components/layout/PageHeader.js";
+import { DataTable } from "../../components/data/DataTable.js";
+import { StatsGrid } from "../../components/layout/StatsGrid.js";
+import { MetricCard } from "../../components/data/MetricCard.js";
+import type { TableColumn } from "../../types.js";
+import "../../styles/base.css";
+import "../../styles/interactive.css";
+
+// ─── Data Shape ─────────────────────────────────────────────
+
+interface Order {
+ id?: string;
+ orderId?: string;
+ contact?: string;
+ contactName?: string;
+ total?: number;
+ status?: string;
+ items?: number | string;
+ itemCount?: number;
+ createdAt?: string;
+ currency?: string;
+}
+
+interface AppData {
+ orders?: Order[];
+ currency?: string;
+}
+
+// ─── Helpers ────────────────────────────────────────────────
+
+function extractData(result: CallToolResult): AppData | null {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc as AppData;
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === "text") {
+ try { return JSON.parse(item.text) as AppData; } catch { continue; }
+ }
+ }
+ }
+ return null;
+}
+
+function formatCurrency(n: number, currency = "USD"): string {
+ try {
+ return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(n);
+ } catch {
+ return `$${n.toFixed(2)}`;
+ }
+}
+
+function formatDate(d?: string): string {
+ if (!d) return "—";
+ try {
+ return new Date(d).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ });
+ } catch {
+ return d;
+ }
+}
+
+const STATUS_FILTERS = [
+ "all",
+ "pending",
+ "processing",
+ "shipped",
+ "delivered",
+ "cancelled",
+] as const;
+
+// ─── Component ──────────────────────────────────────────────
+
+export function App() {
+ const [data, setData] = useState(
+ () => (window as any).__MCP_APP_DATA__ || null
+ );
+ const [activeFilter, setActiveFilter] = useState("all");
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: "Order List", version: "1.0.0" },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ const orders = data?.orders || [];
+ const currency = data?.currency || "USD";
+
+ // Compute stats
+ const stats = useMemo(() => {
+ let totalOrders = orders.length;
+ let totalRevenue = 0;
+ let pendingCount = 0;
+
+ for (const order of orders) {
+ totalRevenue += order.total ?? 0;
+ const s = (order.status || "").toLowerCase();
+ if (s === "pending" || s === "processing") {
+ pendingCount++;
+ }
+ }
+
+ const avgOrder = totalOrders > 0 ? totalRevenue / totalOrders : 0;
+ return { totalOrders, totalRevenue, avgOrder, pendingCount };
+ }, [orders]);
+
+ // Filter orders
+ const filteredOrders = useMemo(() => {
+ if (activeFilter === "all") return orders;
+ return orders.filter(
+ (order) => (order.status || "").toLowerCase() === activeFilter,
+ );
+ }, [orders, activeFilter]);
+
+ // Build table rows
+ const rows = filteredOrders.map((order) => ({
+ id: order.id || order.orderId || "",
+ orderId: order.orderId || order.id || "—",
+ contact: order.contact || order.contactName || "—",
+ total: formatCurrency(order.total ?? 0, order.currency || currency),
+ status: order.status || "pending",
+ items: String(order.items ?? order.itemCount ?? "—"),
+ createdAt: formatDate(order.createdAt),
+ }));
+
+ const columns: TableColumn[] = [
+ { key: "orderId", label: "Order ID", sortable: true },
+ { key: "contact", label: "Contact", sortable: true, format: "avatar" },
+ { key: "total", label: "Total", sortable: true, format: "currency" },
+ { key: "status", label: "Status", sortable: true, format: "status" },
+ { key: "items", label: "Items", sortable: true },
+ { key: "createdAt", label: "Created", sortable: true, format: "date" },
+ ];
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {STATUS_FILTERS.map((f) => (
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/order-list/index.html b/src/ui/react-app/src/apps/order-list/index.html
new file mode 100644
index 0000000..583f132
--- /dev/null
+++ b/src/ui/react-app/src/apps/order-list/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Order List
+
+
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/order-list/main.tsx b/src/ui/react-app/src/apps/order-list/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/order-list/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/order-list/vite.config.ts b/src/ui/react-app/src/apps/order-list/vite.config.ts
new file mode 100644
index 0000000..f7ff0e7
--- /dev/null
+++ b/src/ui/react-app/src/apps/order-list/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/order-list'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/pipeline-analytics/App.tsx b/src/ui/react-app/src/apps/pipeline-analytics/App.tsx
new file mode 100644
index 0000000..3f42bbd
--- /dev/null
+++ b/src/ui/react-app/src/apps/pipeline-analytics/App.tsx
@@ -0,0 +1,151 @@
+import React, { useMemo, useState } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { StatsGrid } from '../../components/layout/StatsGrid';
+import { MetricCard } from '../../components/data/MetricCard';
+import { DataTable } from '../../components/data/DataTable';
+import { FunnelChart } from '../../components/charts/FunnelChart';
+import { BarChart } from '../../components/charts/BarChart';
+import { Card } from '../../components/layout/Card';
+import { SplitLayout } from '../../components/layout/SplitLayout';
+import type { FunnelStage, BarChartBar } from '../../types';
+import '../../styles/base.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatCurrency(amount: number): string {
+ return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(amount);
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Pipeline Analytics', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const analytics = useMemo(() => {
+ if (!data) return null;
+
+ const pipeline = data.pipeline || {};
+ const opportunities: any[] = data.opportunities || [];
+ const stages: any[] = pipeline.stages || data.stages || [];
+
+ const totalValue = opportunities.reduce((sum, o) => sum + (o.monetaryValue || o.value || 0), 0);
+ const dealCount = opportunities.length;
+ const avgDeal = dealCount > 0 ? totalValue / dealCount : 0;
+ const wonDeals = opportunities.filter((o) => o.status === 'won');
+ const wonValue = wonDeals.reduce((sum, o) => sum + (o.monetaryValue || o.value || 0), 0);
+ const winRate = dealCount > 0 ? (wonDeals.length / dealCount) * 100 : 0;
+
+ // Funnel: count by stage
+ const funnelStages: FunnelStage[] = stages.map((s) => {
+ const count = opportunities.filter(
+ (o) => o.stageId === s.id || o.pipelineStageId === s.id || o.stage === s.name
+ ).length;
+ return { label: s.name || s.title || 'Unknown', value: count };
+ });
+
+ // Bar chart: value by stage
+ const valueBars: BarChartBar[] = stages.map((s) => {
+ const stageOpps = opportunities.filter(
+ (o) => o.stageId === s.id || o.pipelineStageId === s.id || o.stage === s.name
+ );
+ const val = stageOpps.reduce((sum, o) => sum + (o.monetaryValue || o.value || 0), 0);
+ return { label: s.name || s.title || 'Unknown', value: val };
+ });
+
+ // Top opportunities
+ const topOpps = [...opportunities]
+ .sort((a, b) => (b.monetaryValue || b.value || 0) - (a.monetaryValue || a.value || 0))
+ .slice(0, 10)
+ .map((o) => ({
+ id: o.id || '',
+ name: o.name || 'Untitled',
+ value: formatCurrency(o.monetaryValue || o.value || 0),
+ stage: o.stageName || o.stage || '—',
+ status: o.status || 'open',
+ contact: o.contactName || o.contact?.name || '—',
+ }));
+
+ return {
+ pipelineName: pipeline.name || 'Pipeline',
+ totalValue,
+ dealCount,
+ avgDeal,
+ wonValue,
+ winRate,
+ funnelStages,
+ valueBars,
+ topOpps,
+ };
+ }, [data]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data || !analytics) return ;
+
+ return (
+
+
+
+
+
+ = 30 ? 'green' : 'yellow'}
+ trend={analytics.winRate >= 30 ? 'up' : 'down'}
+ trendValue={`${analytics.winRate.toFixed(0)}%`}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/pipeline-analytics/index.html b/src/ui/react-app/src/apps/pipeline-analytics/index.html
new file mode 100644
index 0000000..8ae47c6
--- /dev/null
+++ b/src/ui/react-app/src/apps/pipeline-analytics/index.html
@@ -0,0 +1,5 @@
+
+
+Pipeline Analytics
+
+
diff --git a/src/ui/react-app/src/apps/pipeline-analytics/main.tsx b/src/ui/react-app/src/apps/pipeline-analytics/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/pipeline-analytics/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/pipeline-analytics/vite.config.ts b/src/ui/react-app/src/apps/pipeline-analytics/vite.config.ts
new file mode 100644
index 0000000..9d3f504
--- /dev/null
+++ b/src/ui/react-app/src/apps/pipeline-analytics/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/pipeline-analytics'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/pipeline-funnel/App.tsx b/src/ui/react-app/src/apps/pipeline-funnel/App.tsx
new file mode 100644
index 0000000..d2340a1
--- /dev/null
+++ b/src/ui/react-app/src/apps/pipeline-funnel/App.tsx
@@ -0,0 +1,309 @@
+/**
+ * Pipeline Funnel — Conversion funnel visualization.
+ * Funnel showing stage-to-stage conversion.
+ * Stats: total value, win rate, avg time in stage.
+ * Table: deals by stage with values.
+ * Drop-off percentages between stages.
+ */
+import React, { useMemo } from "react";
+import { useApp } from "@modelcontextprotocol/ext-apps/react";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import { PageHeader } from "../../components/layout/PageHeader.js";
+import { StatsGrid } from "../../components/layout/StatsGrid.js";
+import { SplitLayout } from "../../components/layout/SplitLayout.js";
+import { Section } from "../../components/layout/Section.js";
+import { Card } from "../../components/layout/Card.js";
+import { MetricCard } from "../../components/data/MetricCard.js";
+import { FunnelChart } from "../../components/charts/FunnelChart.js";
+import { DataTable } from "../../components/data/DataTable.js";
+import { KeyValueList } from "../../components/data/KeyValueList.js";
+import { CurrencyDisplay } from "../../components/data/CurrencyDisplay.js";
+import "../../styles/base.css";
+import "../../styles/interactive.css";
+
+// ─── Types ──────────────────────────────────────────────────
+
+interface PipelineInfo {
+ id: string;
+ name: string;
+ stages?: PipelineStageInfo[];
+}
+
+interface PipelineStageInfo {
+ id: string;
+ name: string;
+ position?: number;
+}
+
+interface PipelineOpportunity {
+ id?: string;
+ name: string;
+ value: number;
+ stageId: string;
+ stageName?: string;
+ status?: string;
+ contact?: string;
+ createdAt?: string;
+ daysInStage?: number;
+}
+
+interface PipelineFunnelData {
+ pipeline: PipelineInfo;
+ opportunities: PipelineOpportunity[];
+}
+
+// ─── Data Extraction ────────────────────────────────────────
+
+function extractData(result: CallToolResult): PipelineFunnelData | null {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc as PipelineFunnelData;
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === "text") {
+ try {
+ return JSON.parse(item.text) as PipelineFunnelData;
+ } catch {
+ /* skip */
+ }
+ }
+ }
+ }
+ return null;
+}
+
+// ─── Helpers ────────────────────────────────────────────────
+
+function formatCurrency(amount: number): string {
+ try {
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+ }).format(amount);
+ } catch {
+ return `$${amount.toFixed(2)}`;
+ }
+}
+
+// ─── App ────────────────────────────────────────────────────
+
+export function App() {
+ const [data, setData] = React.useState(null);
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: "pipeline-funnel", version: "1.0.0" },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const extracted = extractData(result);
+ if (extracted) setData(extracted);
+ };
+ },
+ });
+
+ React.useEffect(() => {
+ const preInjected = (window as any).__MCP_APP_DATA__;
+ if (preInjected && !data) setData(preInjected as PipelineFunnelData);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ // ─── Aggregate by stage ────────────────────────────────
+
+ const stageOrder = useMemo(() => {
+ const stages = data?.pipeline?.stages ?? [];
+ return [...stages].sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
+ }, [data?.pipeline?.stages]);
+
+ const stageStats = useMemo(() => {
+ const opps = data?.opportunities ?? [];
+ const stageMap: Record = {};
+
+ for (const opp of opps) {
+ const key = opp.stageId;
+ if (!stageMap[key]) stageMap[key] = { count: 0, totalValue: 0, totalDays: 0 };
+ stageMap[key].count += 1;
+ stageMap[key].totalValue += opp.value;
+ stageMap[key].totalDays += opp.daysInStage ?? 0;
+ }
+
+ return stageMap;
+ }, [data?.opportunities]);
+
+ // Funnel stages ordered
+ const funnelStages = useMemo(() => {
+ if (stageOrder.length === 0) {
+ // Fallback: derive from opportunities
+ const stageNames = new Map();
+ for (const opp of data?.opportunities ?? []) {
+ const name = opp.stageName ?? opp.stageId;
+ stageNames.set(name, (stageNames.get(name) ?? 0) + 1);
+ }
+ return Array.from(stageNames.entries())
+ .sort(([, a], [, b]) => b - a)
+ .map(([label, value]) => ({ label, value }));
+ }
+ return stageOrder.map((stage) => ({
+ label: stage.name,
+ value: stageStats[stage.id]?.count ?? 0,
+ }));
+ }, [stageOrder, stageStats, data?.opportunities]);
+
+ // ─── KPIs ──────────────────────────────────────────────
+
+ const totalValue = useMemo(
+ () => (data?.opportunities ?? []).reduce((s, o) => s + o.value, 0),
+ [data?.opportunities],
+ );
+
+ const totalDeals = data?.opportunities?.length ?? 0;
+
+ const wonDeals = useMemo(
+ () => (data?.opportunities ?? []).filter((o) => o.status === "won").length,
+ [data?.opportunities],
+ );
+
+ const winRate = totalDeals > 0 ? ((wonDeals / totalDeals) * 100).toFixed(1) : "0.0";
+
+ const avgDaysInStage = useMemo(() => {
+ const opps = (data?.opportunities ?? []).filter((o) => o.daysInStage != null);
+ if (opps.length === 0) return 0;
+ return Math.round(opps.reduce((s, o) => s + (o.daysInStage ?? 0), 0) / opps.length);
+ }, [data?.opportunities]);
+
+ // ─── Stage detail rows for table ───────────────────────
+
+ const stageTableRows = useMemo(() => {
+ if (stageOrder.length > 0) {
+ return stageOrder.map((stage) => {
+ const stats = stageStats[stage.id];
+ return {
+ id: stage.id,
+ stage: stage.name,
+ deals: stats?.count ?? 0,
+ totalValue: formatCurrency(stats?.totalValue ?? 0),
+ avgDays: stats?.count ? Math.round(stats.totalDays / stats.count) : 0,
+ };
+ });
+ }
+ // Fallback
+ const stageMap = new Map();
+ for (const opp of data?.opportunities ?? []) {
+ const name = opp.stageName ?? opp.stageId;
+ const cur = stageMap.get(name) ?? { count: 0, value: 0, days: 0 };
+ cur.count += 1;
+ cur.value += opp.value;
+ cur.days += opp.daysInStage ?? 0;
+ stageMap.set(name, cur);
+ }
+ return Array.from(stageMap.entries()).map(([stage, stats]) => ({
+ id: stage,
+ stage,
+ deals: stats.count,
+ totalValue: formatCurrency(stats.value),
+ avgDays: stats.count > 0 ? Math.round(stats.days / stats.count) : 0,
+ }));
+ }, [stageOrder, stageStats, data?.opportunities]);
+
+ const stageColumns = useMemo(() => [
+ { key: "stage", label: "Stage", sortable: true },
+ { key: "deals", label: "Deals", sortable: true },
+ { key: "totalValue", label: "Total Value", sortable: true },
+ { key: "avgDays", label: "Avg Days in Stage", sortable: true },
+ ], []);
+
+ // ─── Conversion drop-off detail (KeyValueList) ────────
+
+ const dropoffItems = useMemo(() => {
+ if (funnelStages.length < 2) return [];
+ return funnelStages.slice(1).map((stage, i) => {
+ const prev = funnelStages[i];
+ const dropPct = prev.value > 0
+ ? (((prev.value - stage.value) / prev.value) * 100).toFixed(1)
+ : "0.0";
+ const convPct = prev.value > 0
+ ? ((stage.value / prev.value) * 100).toFixed(1)
+ : "0.0";
+ return {
+ label: `${prev.label} → ${stage.label}`,
+ value: `${convPct}% conversion (−${dropPct}% drop-off)`,
+ };
+ });
+ }, [funnelStages]);
+
+ if (error) {
+ return (
+
+
Connection Error
+
{error.message}
+
+ );
+ }
+
+ if (!data) {
+ return (
+
+
+
{isConnected ? "Waiting for data..." : "Connecting..."}
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ = 30 ? "green" : "yellow"}
+ trend={Number(winRate) >= 30 ? "up" : "down"}
+ trendValue={`${winRate}%`}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/pipeline-funnel/index.html b/src/ui/react-app/src/apps/pipeline-funnel/index.html
new file mode 100644
index 0000000..d4d2026
--- /dev/null
+++ b/src/ui/react-app/src/apps/pipeline-funnel/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Pipeline Funnel
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/pipeline-funnel/main.tsx b/src/ui/react-app/src/apps/pipeline-funnel/main.tsx
new file mode 100644
index 0000000..c3e2da2
--- /dev/null
+++ b/src/ui/react-app/src/apps/pipeline-funnel/main.tsx
@@ -0,0 +1,9 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { App } from "./App.js";
+
+createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
diff --git a/src/ui/react-app/src/apps/pipeline-funnel/vite.config.ts b/src/ui/react-app/src/apps/pipeline-funnel/vite.config.ts
new file mode 100644
index 0000000..e7ee7d6
--- /dev/null
+++ b/src/ui/react-app/src/apps/pipeline-funnel/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/pipeline-funnel'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/pipeline-kanban/App.tsx b/src/ui/react-app/src/apps/pipeline-kanban/App.tsx
new file mode 100644
index 0000000..42631fa
--- /dev/null
+++ b/src/ui/react-app/src/apps/pipeline-kanban/App.tsx
@@ -0,0 +1,125 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { StatsGrid } from '../../components/layout/StatsGrid';
+import { MetricCard } from '../../components/data/MetricCard';
+import { MCPAppProvider } from '../../context/MCPAppContext';
+import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
+import { KanbanBoard } from '../../components/data/KanbanBoard';
+import type { KanbanColumn } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatCurrency(amount: number): string {
+ return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(amount);
+}
+
+function formatDate(d?: string): string {
+ if (!d) return '';
+ try { return new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); }
+ catch { return d; }
+}
+
+function PipelineKanbanInner({ data }: { data: any }) {
+ const pipeline = data.pipeline || {};
+ const opportunities: any[] = data.opportunities || [];
+ const stages: any[] = data.stages || pipeline.stages || [];
+
+ const stats = useMemo(() => {
+ const totalValue = opportunities.reduce((sum, o) => sum + (o.monetaryValue || o.value || 0), 0);
+ const dealCount = opportunities.length;
+ const avgDeal = dealCount > 0 ? totalValue / dealCount : 0;
+ const wonCount = opportunities.filter((o) => o.status === 'won').length;
+ return { totalValue, dealCount, avgDeal, wonCount };
+ }, [opportunities]);
+
+ const columns = useMemo((): KanbanColumn[] => {
+ return stages.map((stage) => {
+ const stageOpps = opportunities.filter(
+ (o) => o.stageId === stage.id || o.pipelineStageId === stage.id || o.stage === stage.name
+ );
+ const stageValue = stageOpps.reduce((sum, o) => sum + (o.monetaryValue || o.value || 0), 0);
+ return {
+ id: stage.id,
+ title: stage.name || stage.title || 'Unknown',
+ count: stageOpps.length,
+ totalValue: formatCurrency(stageValue),
+ cards: stageOpps.map((o) => ({
+ id: o.id,
+ title: o.name || 'Untitled',
+ subtitle: o.contactName || o.contact?.name || '',
+ value: formatCurrency(o.monetaryValue || o.value || 0),
+ date: formatDate(o.createdAt || o.dateAdded),
+ status: o.status || 'open',
+ statusVariant: o.status || 'open',
+ })),
+ };
+ });
+ }, [stages, opportunities]);
+
+ return (
+
+
+
+
+
+ 0 ? ((stats.wonCount / stats.dealCount) * 100).toFixed(0) : 0}%`} />
+
+
+ {stats.dealCount === 0 ? (
+
+
📋
+
No opportunities in this pipeline
+
+ ) : (
+
+
+
+ )}
+
+ );
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [appInstance, setAppInstance] = useState(null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Pipeline Kanban', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ setAppInstance(app);
+ app.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/pipeline-kanban/index.html b/src/ui/react-app/src/apps/pipeline-kanban/index.html
new file mode 100644
index 0000000..38aad84
--- /dev/null
+++ b/src/ui/react-app/src/apps/pipeline-kanban/index.html
@@ -0,0 +1,5 @@
+
+
+Pipeline Kanban
+
+
diff --git a/src/ui/react-app/src/apps/pipeline-kanban/main.tsx b/src/ui/react-app/src/apps/pipeline-kanban/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/pipeline-kanban/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/pipeline-kanban/vite.config.ts b/src/ui/react-app/src/apps/pipeline-kanban/vite.config.ts
new file mode 100644
index 0000000..8967a9d
--- /dev/null
+++ b/src/ui/react-app/src/apps/pipeline-kanban/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/pipeline-kanban'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/product-catalog/App.tsx b/src/ui/react-app/src/apps/product-catalog/App.tsx
new file mode 100644
index 0000000..accb087
--- /dev/null
+++ b/src/ui/react-app/src/apps/product-catalog/App.tsx
@@ -0,0 +1,126 @@
+/**
+ * Product Catalog — Card grid of products with search and filtering.
+ */
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { CardGrid } from '../../components/data/CardGrid';
+import type { CardGridItem } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatCurrency(n: number, currency = 'USD'): string {
+ try { return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n); }
+ catch { return `$${n.toFixed(2)}`; }
+}
+
+function statusVariant(status?: string): string {
+ const s = (status || '').toLowerCase();
+ if (s === 'active' || s === 'published') return 'active';
+ if (s === 'draft') return 'draft';
+ if (s === 'archived' || s === 'inactive') return 'draft';
+ if (s === 'out_of_stock' || s === 'out of stock') return 'error';
+ return 'active';
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [activeFilter, setActiveFilter] = useState('all');
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Product Catalog', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const products: any[] = data.products || [];
+ const currency = data.currency || 'USD';
+
+ const categories = useMemo(() => {
+ const cats = new Set();
+ for (const p of products) {
+ if (p.category) cats.add(p.category);
+ if (p.type) cats.add(p.type);
+ }
+ return ['all', ...Array.from(cats)];
+ }, [products]);
+
+ const filteredProducts = useMemo(() => {
+ let result = products;
+ if (activeFilter !== 'all') {
+ result = result.filter((p: any) =>
+ (p.category || '').toLowerCase() === activeFilter.toLowerCase() ||
+ (p.type || '').toLowerCase() === activeFilter.toLowerCase()
+ );
+ }
+ if (searchQuery.trim()) {
+ const q = searchQuery.toLowerCase();
+ result = result.filter((p: any) =>
+ (p.name || p.title || '').toLowerCase().includes(q) ||
+ (p.description || '').toLowerCase().includes(q)
+ );
+ }
+ return result;
+ }, [products, activeFilter, searchQuery]);
+
+ const cards: CardGridItem[] = filteredProducts.map((p: any) => ({
+ title: p.name || p.title || 'Untitled Product',
+ subtitle: formatCurrency(p.price ?? 0, p.currency || currency),
+ description: p.description,
+ imageUrl: p.imageUrl || p.image,
+ status: p.status || 'Active',
+ statusVariant: statusVariant(p.status),
+ action: 'View',
+ }));
+
+ return (
+
+
+
+
+ setSearchQuery(e.target.value)} />
+
+
+ {categories.length > 1 && (
+
+ {categories.map((cat) => (
+
+ ))}
+
+ )}
+
+ {cards.length > 0 ? (
+
+ ) : (
+
+
📦
+
{searchQuery ? 'No products match your search' : 'No products available'}
+
+ )}
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/product-catalog/index.html b/src/ui/react-app/src/apps/product-catalog/index.html
new file mode 100644
index 0000000..bdd79ec
--- /dev/null
+++ b/src/ui/react-app/src/apps/product-catalog/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Product Catalog
+
+
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/product-catalog/main.tsx b/src/ui/react-app/src/apps/product-catalog/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/product-catalog/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/product-catalog/vite.config.ts b/src/ui/react-app/src/apps/product-catalog/vite.config.ts
new file mode 100644
index 0000000..5776cc6
--- /dev/null
+++ b/src/ui/react-app/src/apps/product-catalog/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/product-catalog'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/product-detail/App.tsx b/src/ui/react-app/src/apps/product-detail/App.tsx
new file mode 100644
index 0000000..fa16bb8
--- /dev/null
+++ b/src/ui/react-app/src/apps/product-detail/App.tsx
@@ -0,0 +1,130 @@
+/**
+ * Product Detail — Single product view with prices and inventory.
+ */
+import React, { useState } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { DetailHeader } from '../../components/data/DetailHeader';
+import { SplitLayout } from '../../components/layout/SplitLayout';
+import { KeyValueList } from '../../components/data/KeyValueList';
+import { DataTable } from '../../components/data/DataTable';
+import { CurrencyDisplay } from '../../components/data/CurrencyDisplay';
+import { StockIndicator } from '../../components/data/StockIndicator';
+import type { StatusVariant, KeyValueItem, TableColumn } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatCurrency(n: number, currency = 'USD'): string {
+ try { return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n); }
+ catch { return `$${n.toFixed(2)}`; }
+}
+
+function formatDate(d?: string): string {
+ if (!d) return '—';
+ try { return new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); }
+ catch { return d; }
+}
+
+function statusVariant(status?: string): StatusVariant {
+ const s = (status || '').toLowerCase();
+ if (s === 'active' || s === 'published') return 'active';
+ if (s === 'draft') return 'draft';
+ if (s === 'archived' || s === 'inactive') return 'paused';
+ return 'active';
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Product Detail', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const p = data.product || {};
+ const prices: any[] = data.prices || [];
+ const currency = p.currency || 'USD';
+ const variant = statusVariant(p.status);
+
+ const details: KeyValueItem[] = [];
+ if (p.sku) details.push({ label: 'SKU', value: p.sku });
+ if (p.category) details.push({ label: 'Category', value: p.category });
+ if (p.type) details.push({ label: 'Type', value: p.type });
+ if (p.price !== undefined) details.push({ label: 'Base Price', value: formatCurrency(p.price, currency), bold: true });
+ if (p.createdAt) details.push({ label: 'Created', value: formatDate(p.createdAt) });
+ if (p.updatedAt) details.push({ label: 'Updated', value: formatDate(p.updatedAt) });
+
+ const priceRows = prices.map((pr: any, i: number) => ({
+ id: pr.id || String(i),
+ name: pr.name || 'Price',
+ amount: formatCurrency(pr.amount ?? 0, pr.currency || currency),
+ type: pr.recurring ? `Recurring (${pr.interval || 'month'})` : 'One-time',
+ trialDays: pr.trialDays ? `${pr.trialDays} days` : '—',
+ }));
+
+ const priceColumns: TableColumn[] = [
+ { key: 'name', label: 'Price Name', sortable: true },
+ { key: 'amount', label: 'Amount', sortable: true, format: 'currency' },
+ { key: 'type', label: 'Type', sortable: true },
+ { key: 'trialDays', label: 'Trial', sortable: false },
+ ];
+
+ const stockQty = p.inventory ?? p.stockQuantity;
+
+ return (
+
+
+
+
+
+ {p.description && (
+
+
Description
+
{p.description}
+
+ )}
+
+
+
+ {p.price !== undefined && (
+
+ )}
+ {stockQty !== undefined && (
+
+ )}
+
+
+
+ {priceRows.length > 0 && (
+
+
Pricing Plans
+
+
+ )}
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/product-detail/index.html b/src/ui/react-app/src/apps/product-detail/index.html
new file mode 100644
index 0000000..d11e81d
--- /dev/null
+++ b/src/ui/react-app/src/apps/product-detail/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Product Detail
+
+
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/product-detail/main.tsx b/src/ui/react-app/src/apps/product-detail/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/product-detail/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/product-detail/vite.config.ts b/src/ui/react-app/src/apps/product-detail/vite.config.ts
new file mode 100644
index 0000000..81bf374
--- /dev/null
+++ b/src/ui/react-app/src/apps/product-detail/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/product-detail'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/revenue-dashboard/App.tsx b/src/ui/react-app/src/apps/revenue-dashboard/App.tsx
new file mode 100644
index 0000000..016fa46
--- /dev/null
+++ b/src/ui/react-app/src/apps/revenue-dashboard/App.tsx
@@ -0,0 +1,329 @@
+/**
+ * Revenue Dashboard — Revenue charts + KPIs.
+ * Stats: total revenue, MRR, outstanding, avg deal value.
+ * Line chart: revenue over time (aggregated from invoices by month).
+ * Pie chart: revenue by source/pipeline.
+ * Bar chart: top deals by value.
+ * Table: recent paid invoices.
+ */
+import React, { useMemo } from "react";
+import { useApp } from "@modelcontextprotocol/ext-apps/react";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import { PageHeader } from "../../components/layout/PageHeader.js";
+import { StatsGrid } from "../../components/layout/StatsGrid.js";
+import { SplitLayout } from "../../components/layout/SplitLayout.js";
+import { Section } from "../../components/layout/Section.js";
+import { Card } from "../../components/layout/Card.js";
+import { MetricCard } from "../../components/data/MetricCard.js";
+import { CurrencyDisplay } from "../../components/data/CurrencyDisplay.js";
+import { LineChart } from "../../components/charts/LineChart.js";
+import { BarChart } from "../../components/charts/BarChart.js";
+import { PieChart } from "../../components/charts/PieChart.js";
+import { DataTable } from "../../components/data/DataTable.js";
+import { SparklineChart } from "../../components/charts/SparklineChart.js";
+import "../../styles/base.css";
+import "../../styles/interactive.css";
+
+// ─── Types ──────────────────────────────────────────────────
+
+interface Invoice {
+ id?: string;
+ name?: string;
+ contactName?: string;
+ amount: number;
+ status: string;
+ paidDate?: string;
+ createdAt?: string;
+ currency?: string;
+}
+
+interface Opportunity {
+ id?: string;
+ name: string;
+ value: number;
+ pipelineName?: string;
+ stageName?: string;
+ status?: string;
+}
+
+interface RevenueDashboardData {
+ invoices: Invoice[];
+ opportunities: Opportunity[];
+}
+
+// ─── Data Extraction ────────────────────────────────────────
+
+function extractData(result: CallToolResult): RevenueDashboardData | null {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc as RevenueDashboardData;
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === "text") {
+ try {
+ return JSON.parse(item.text) as RevenueDashboardData;
+ } catch {
+ /* skip */
+ }
+ }
+ }
+ }
+ return null;
+}
+
+// ─── Helpers ────────────────────────────────────────────────
+
+function formatCurrency(amount: number): string {
+ try {
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+ }).format(amount);
+ } catch {
+ return `$${amount.toFixed(2)}`;
+ }
+}
+
+function formatDate(d?: string): string {
+ if (!d) return "—";
+ try {
+ return new Date(d).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ });
+ } catch {
+ return d;
+ }
+}
+
+// ─── App ────────────────────────────────────────────────────
+
+export function App() {
+ const [data, setData] = React.useState(null);
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: "revenue-dashboard", version: "1.0.0" },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const extracted = extractData(result);
+ if (extracted) setData(extracted);
+ };
+ },
+ });
+
+ React.useEffect(() => {
+ const preInjected = (window as any).__MCP_APP_DATA__;
+ if (preInjected && !data) setData(preInjected as RevenueDashboardData);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ // ─── Derived KPIs ──────────────────────────────────────
+
+ const paidInvoices = useMemo(
+ () => (data?.invoices ?? []).filter((inv) => inv.status === "paid"),
+ [data?.invoices],
+ );
+
+ const totalRevenue = useMemo(
+ () => paidInvoices.reduce((s, inv) => s + inv.amount, 0),
+ [paidInvoices],
+ );
+
+ const outstandingAmount = useMemo(
+ () =>
+ (data?.invoices ?? [])
+ .filter((inv) => inv.status !== "paid")
+ .reduce((s, inv) => s + inv.amount, 0),
+ [data?.invoices],
+ );
+
+ const avgDealValue = useMemo(() => {
+ const opps = data?.opportunities ?? [];
+ if (opps.length === 0) return 0;
+ return opps.reduce((s, o) => s + o.value, 0) / opps.length;
+ }, [data?.opportunities]);
+
+ // MRR estimate: paid invoices from the last 30 days
+ const mrr = useMemo(() => {
+ const now = Date.now();
+ const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000;
+ return paidInvoices
+ .filter((inv) => {
+ const d = inv.paidDate ? new Date(inv.paidDate).getTime() : 0;
+ return d >= thirtyDaysAgo;
+ })
+ .reduce((s, inv) => s + inv.amount, 0);
+ }, [paidInvoices]);
+
+ // ─── Revenue over time (aggregate invoices by month) ───
+
+ const revenueOverTime = useMemo(() => {
+ const monthMap: Record = {};
+ for (const inv of paidInvoices) {
+ const dateStr = inv.paidDate ?? inv.createdAt;
+ if (!dateStr) continue;
+ const d = new Date(dateStr);
+ const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
+ monthMap[key] = (monthMap[key] ?? 0) + inv.amount;
+ }
+ return Object.entries(monthMap)
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([label, value]) => ({ label, value }));
+ }, [paidInvoices]);
+
+ // Sparkline from revenue trend
+ const revenueTrend = useMemo(
+ () => revenueOverTime.map((p) => p.value),
+ [revenueOverTime],
+ );
+
+ // ─── Revenue by source/pipeline ────────────────────────
+
+ const revenueByPipeline = useMemo(() => {
+ const pipeMap: Record = {};
+ for (const opp of data?.opportunities ?? []) {
+ const key = opp.pipelineName ?? "Other";
+ pipeMap[key] = (pipeMap[key] ?? 0) + opp.value;
+ }
+ return Object.entries(pipeMap).map(([label, value]) => ({ label, value }));
+ }, [data?.opportunities]);
+
+ // ─── Top deals by value ────────────────────────────────
+
+ const topDeals = useMemo(() => {
+ const sorted = [...(data?.opportunities ?? [])].sort((a, b) => b.value - a.value);
+ return sorted.slice(0, 10).map((o) => ({
+ label: o.name.length > 20 ? o.name.slice(0, 18) + "…" : o.name,
+ value: o.value,
+ }));
+ }, [data?.opportunities]);
+
+ // ─── Recent paid invoices table ────────────────────────
+
+ const recentPaid = useMemo(
+ () =>
+ [...paidInvoices]
+ .sort((a, b) => {
+ const da = a.paidDate ?? a.createdAt ?? "";
+ const db = b.paidDate ?? b.createdAt ?? "";
+ return db.localeCompare(da);
+ })
+ .slice(0, 20)
+ .map((inv, idx) => ({
+ id: inv.id ?? `inv-${idx}`,
+ name: inv.name ?? inv.contactName ?? "Invoice",
+ contactName: inv.contactName ?? "—",
+ amount: formatCurrency(inv.amount),
+ paidDate: formatDate(inv.paidDate),
+ })),
+ [paidInvoices],
+ );
+
+ const invoiceColumns = useMemo(() => [
+ { key: "name", label: "Invoice", sortable: true },
+ { key: "contactName", label: "Contact" },
+ { key: "amount", label: "Amount", sortable: true },
+ { key: "paidDate", label: "Paid Date", sortable: true, format: "date" },
+ ], []);
+
+ if (error) {
+ return (
+
+
Connection Error
+
{error.message}
+
+ );
+ }
+
+ if (!data) {
+ return (
+
+
+
{isConnected ? "Waiting for data..." : "Connecting..."}
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
Total Revenue
+ {revenueTrend.length > 1 && (
+
+ )}
+
+ 0 ? "up" : "flat"}
+ trendValue={formatCurrency(mrr)}
+ />
+ 0 ? "yellow" : "green"}
+ trend={outstandingAmount > 0 ? "down" : "flat"}
+ trendValue={formatCurrency(outstandingAmount)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/revenue-dashboard/index.html b/src/ui/react-app/src/apps/revenue-dashboard/index.html
new file mode 100644
index 0000000..ff00ad7
--- /dev/null
+++ b/src/ui/react-app/src/apps/revenue-dashboard/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Revenue Dashboard
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/revenue-dashboard/main.tsx b/src/ui/react-app/src/apps/revenue-dashboard/main.tsx
new file mode 100644
index 0000000..c3e2da2
--- /dev/null
+++ b/src/ui/react-app/src/apps/revenue-dashboard/main.tsx
@@ -0,0 +1,9 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { App } from "./App.js";
+
+createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
diff --git a/src/ui/react-app/src/apps/revenue-dashboard/vite.config.ts b/src/ui/react-app/src/apps/revenue-dashboard/vite.config.ts
new file mode 100644
index 0000000..316e602
--- /dev/null
+++ b/src/ui/react-app/src/apps/revenue-dashboard/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/revenue-dashboard'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/reviews-dashboard/App.tsx b/src/ui/react-app/src/apps/reviews-dashboard/App.tsx
new file mode 100644
index 0000000..1e5a25b
--- /dev/null
+++ b/src/ui/react-app/src/apps/reviews-dashboard/App.tsx
@@ -0,0 +1,289 @@
+/**
+ * Reviews Dashboard — Review stats + feed.
+ * Stats: avg rating, total reviews, response rate.
+ * Star rating with distribution chart.
+ * Bar chart: ratings distribution (1-5 stars).
+ * Table: recent reviews with rating, text, date.
+ */
+import React, { useMemo } from "react";
+import { useApp } from "@modelcontextprotocol/ext-apps/react";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import { PageHeader } from "../../components/layout/PageHeader.js";
+import { StatsGrid } from "../../components/layout/StatsGrid.js";
+import { SplitLayout } from "../../components/layout/SplitLayout.js";
+import { Section } from "../../components/layout/Section.js";
+import { Card } from "../../components/layout/Card.js";
+import { MetricCard } from "../../components/data/MetricCard.js";
+import { StarRating } from "../../components/data/StarRating.js";
+import { BarChart } from "../../components/charts/BarChart.js";
+import { DataTable } from "../../components/data/DataTable.js";
+import { SparklineChart } from "../../components/charts/SparklineChart.js";
+import "../../styles/base.css";
+import "../../styles/interactive.css";
+
+// ─── Types ──────────────────────────────────────────────────
+
+interface ReviewsCount {
+ total?: number;
+ averageRating?: number;
+ responseRate?: number;
+ distribution?: Record;
+ recentTrend?: number[];
+}
+
+interface Review {
+ id?: string;
+ rating: number;
+ text?: string;
+ reviewerName?: string;
+ date?: string;
+ source?: string;
+ responded?: boolean;
+}
+
+interface ReviewsDashboardData {
+ reviewsCount: ReviewsCount;
+ reviews: Review[];
+}
+
+// ─── Data Extraction ────────────────────────────────────────
+
+function formatDate(d?: string): string {
+ if (!d) return "—";
+ try {
+ return new Date(d).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ });
+ } catch {
+ return d;
+ }
+}
+
+function extractData(result: CallToolResult): ReviewsDashboardData | null {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc as ReviewsDashboardData;
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === "text") {
+ try {
+ return JSON.parse(item.text) as ReviewsDashboardData;
+ } catch {
+ /* skip */
+ }
+ }
+ }
+ }
+ return null;
+}
+
+// ─── App ────────────────────────────────────────────────────
+
+export function App() {
+ const [data, setData] = React.useState(null);
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: "reviews-dashboard", version: "1.0.0" },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const extracted = extractData(result);
+ if (extracted) setData(extracted);
+ };
+ },
+ });
+
+ React.useEffect(() => {
+ const preInjected = (window as any).__MCP_APP_DATA__;
+ if (preInjected && !data) setData(preInjected as ReviewsDashboardData);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ // ─── Derived Data ──────────────────────────────────────
+
+ const totalReviews = data?.reviewsCount?.total ?? data?.reviews?.length ?? 0;
+
+ const avgRating = useMemo(() => {
+ if (data?.reviewsCount?.averageRating != null) return data.reviewsCount.averageRating;
+ const reviews = data?.reviews ?? [];
+ if (reviews.length === 0) return 0;
+ return reviews.reduce((s, r) => s + r.rating, 0) / reviews.length;
+ }, [data?.reviewsCount?.averageRating, data?.reviews]);
+
+ const responseRate = useMemo(() => {
+ if (data?.reviewsCount?.responseRate != null) return data.reviewsCount.responseRate;
+ const reviews = data?.reviews ?? [];
+ if (reviews.length === 0) return 0;
+ const responded = reviews.filter((r) => r.responded).length;
+ return (responded / reviews.length) * 100;
+ }, [data?.reviewsCount?.responseRate, data?.reviews]);
+
+ // Star distribution for StarRating component
+ const starDistribution = useMemo(() => {
+ if (data?.reviewsCount?.distribution) {
+ return [1, 2, 3, 4, 5].map((stars) => ({
+ stars,
+ count: data.reviewsCount.distribution?.[String(stars)] ?? 0,
+ }));
+ }
+ // Compute from reviews
+ const counts: Record = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
+ for (const r of data?.reviews ?? []) {
+ const star = Math.min(5, Math.max(1, Math.round(r.rating)));
+ counts[star] = (counts[star] ?? 0) + 1;
+ }
+ return [1, 2, 3, 4, 5].map((stars) => ({ stars, count: counts[stars] }));
+ }, [data?.reviewsCount?.distribution, data?.reviews]);
+
+ // Bar chart for rating distribution
+ const ratingBars = useMemo(
+ () =>
+ starDistribution.map((d) => ({
+ label: `${d.stars}★`,
+ value: d.count,
+ color:
+ d.stars >= 4
+ ? "#16a34a"
+ : d.stars === 3
+ ? "#eab308"
+ : "#ef4444",
+ })),
+ [starDistribution],
+ );
+
+ // Recent reviews for table
+ const recentReviews = useMemo(
+ () =>
+ [...(data?.reviews ?? [])]
+ .sort((a, b) => {
+ const da = a.date ?? "";
+ const db = b.date ?? "";
+ return db.localeCompare(da);
+ })
+ .slice(0, 20)
+ .map((r, idx) => ({
+ id: r.id ?? `review-${idx}`,
+ rating: "★".repeat(Math.round(r.rating)) + "☆".repeat(5 - Math.round(r.rating)),
+ ratingNum: r.rating,
+ text: r.text
+ ? r.text.length > 80
+ ? r.text.slice(0, 78) + "…"
+ : r.text
+ : "—",
+ reviewerName: r.reviewerName ?? "Anonymous",
+ date: formatDate(r.date),
+ source: r.source ?? "—",
+ responded: r.responded ? "Yes" : "No",
+ })),
+ [data?.reviews],
+ );
+
+ const reviewColumns = useMemo(() => [
+ { key: "rating", label: "Rating", sortable: true },
+ { key: "reviewerName", label: "Reviewer", sortable: true },
+ { key: "text", label: "Review" },
+ { key: "source", label: "Source" },
+ { key: "date", label: "Date", sortable: true, format: "date" },
+ { key: "responded", label: "Responded" },
+ ], []);
+
+ // Rating trend sparkline
+ const ratingTrend = data?.reviewsCount?.recentTrend ?? [];
+
+ // Sentiment label
+ const sentimentLabel =
+ avgRating >= 4.5
+ ? "Excellent"
+ : avgRating >= 4.0
+ ? "Very Good"
+ : avgRating >= 3.5
+ ? "Good"
+ : avgRating >= 3.0
+ ? "Average"
+ : "Needs Improvement";
+
+ if (error) {
+ return (
+
+
Connection Error
+
{error.message}
+
+ );
+ }
+
+ if (!data) {
+ return (
+
+
+
{isConnected ? "Waiting for data..." : "Connecting..."}
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
{avgRating.toFixed(1)}
+
Average Rating
+ {ratingTrend.length > 1 && (
+
+ )}
+
+ 0 ? "up" : "flat"}
+ trendValue={totalReviews.toString()}
+ />
+ = 80 ? "green" : responseRate >= 50 ? "yellow" : "red"}
+ trend={responseRate >= 80 ? "up" : "down"}
+ trendValue={`${responseRate.toFixed(0)}%`}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/reviews-dashboard/index.html b/src/ui/react-app/src/apps/reviews-dashboard/index.html
new file mode 100644
index 0000000..005eb7a
--- /dev/null
+++ b/src/ui/react-app/src/apps/reviews-dashboard/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Reviews Dashboard
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/reviews-dashboard/main.tsx b/src/ui/react-app/src/apps/reviews-dashboard/main.tsx
new file mode 100644
index 0000000..c3e2da2
--- /dev/null
+++ b/src/ui/react-app/src/apps/reviews-dashboard/main.tsx
@@ -0,0 +1,9 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { App } from "./App.js";
+
+createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
diff --git a/src/ui/react-app/src/apps/reviews-dashboard/vite.config.ts b/src/ui/react-app/src/apps/reviews-dashboard/vite.config.ts
new file mode 100644
index 0000000..a066e53
--- /dev/null
+++ b/src/ui/react-app/src/apps/reviews-dashboard/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/reviews-dashboard'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/smartlist-viewer/App.tsx b/src/ui/react-app/src/apps/smartlist-viewer/App.tsx
new file mode 100644
index 0000000..e3e08ad
--- /dev/null
+++ b/src/ui/react-app/src/apps/smartlist-viewer/App.tsx
@@ -0,0 +1,139 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { TagList } from '../../components/data/TagList';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatDate(d: string | undefined): string {
+ if (!d) return '-';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+function extractFilterCriteria(list: any): string[] {
+ if (list.filters && Array.isArray(list.filters)) {
+ return list.filters.map((f: any) => {
+ if (typeof f === 'string') return f;
+ return `${f.field || f.key || ''} ${f.operator || '='} ${f.value || ''}`.trim();
+ });
+ }
+ if (list.filterCriteria && typeof list.filterCriteria === 'string') {
+ return list.filterCriteria.split(',').map((s: string) => s.trim());
+ }
+ if (list.criteria && Array.isArray(list.criteria)) {
+ return list.criteria.map((c: any) => typeof c === 'string' ? c : c.label || c.name || JSON.stringify(c));
+ }
+ return [];
+}
+
+const tagColors: Array<'blue' | 'green' | 'purple' | 'yellow' | 'indigo' | 'pink'> = ['blue', 'green', 'purple', 'yellow', 'indigo', 'pink'];
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [search, setSearch] = useState('');
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Smart List Viewer', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const rows = useMemo(() => {
+ const lists: any[] = data?.smartLists || data?.lists || data?.data || [];
+ return lists
+ .map((l) => {
+ const criteria = extractFilterCriteria(l);
+ return {
+ id: l.id || '',
+ name: l.name || l.title || 'Untitled List',
+ contactsCount: l.contactsCount || l.count || l.totalContacts || 0,
+ filters: criteria,
+ filtersDisplay: criteria.length > 0
+ ? criteria.slice(0, 3).join(', ') + (criteria.length > 3 ? ` +${criteria.length - 3} more` : '')
+ : 'No filters',
+ createdDate: formatDate(l.createdAt || l.dateCreated || l.created),
+ lastUpdated: formatDate(l.updatedAt || l.lastUpdated || l.modified),
+ };
+ })
+ .filter((r) => {
+ if (!search) return true;
+ const q = search.toLowerCase();
+ return r.name.toLowerCase().includes(q) || r.filtersDisplay.toLowerCase().includes(q);
+ });
+ }, [data, search]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ return (
+
+
+ setSearch(e.target.value)}
+ />
+
+
+ {rows.length === 0 ? (
+
+
📋
+
{search ? 'No smart lists match your search' : 'No smart lists found'}
+
+ ) : (
+
+ {rows.map((row) => (
+
+
+
+
{row.name}
+
+ {row.contactsCount.toLocaleString()} contact{row.contactsCount !== 1 ? 's' : ''}
+
+
+
+
Created: {row.createdDate}
+
Updated: {row.lastUpdated}
+
+
+ {row.filters.length > 0 && (
+
({
+ label: f,
+ color: tagColors[i % tagColors.length],
+ }))}
+ maxVisible={5}
+ size="sm"
+ />
+ )}
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/smartlist-viewer/index.html b/src/ui/react-app/src/apps/smartlist-viewer/index.html
new file mode 100644
index 0000000..61cee1d
--- /dev/null
+++ b/src/ui/react-app/src/apps/smartlist-viewer/index.html
@@ -0,0 +1,5 @@
+
+
+Smart List Viewer
+
+
diff --git a/src/ui/react-app/src/apps/smartlist-viewer/main.tsx b/src/ui/react-app/src/apps/smartlist-viewer/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/smartlist-viewer/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/smartlist-viewer/vite.config.ts b/src/ui/react-app/src/apps/smartlist-viewer/vite.config.ts
new file mode 100644
index 0000000..6f2ae99
--- /dev/null
+++ b/src/ui/react-app/src/apps/smartlist-viewer/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/smartlist-viewer'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/social-accounts/App.tsx b/src/ui/react-app/src/apps/social-accounts/App.tsx
new file mode 100644
index 0000000..7daff89
--- /dev/null
+++ b/src/ui/react-app/src/apps/social-accounts/App.tsx
@@ -0,0 +1,174 @@
+/**
+ * social-accounts — Connected social media accounts view.
+ * Card grid showing platform, account name, status, connection date.
+ */
+import React, { useState, useEffect, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { MCPAppProvider } from '../../context/MCPAppContext';
+import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { CardGrid } from '../../components/data/CardGrid';
+import { StatusBadge } from '../../components/data/StatusBadge';
+import type { CardGridItem, StatusVariant } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+// ─── Types ──────────────────────────────────────────────────
+
+interface SocialAccount {
+ id?: string;
+ name?: string;
+ platform?: string;
+ status?: string;
+ connectedAt?: string;
+ avatarUrl?: string;
+ profileUrl?: string;
+ followers?: number;
+}
+
+interface AccountsData {
+ accounts: SocialAccount[];
+}
+
+// ─── Constants ──────────────────────────────────────────────
+
+const PLATFORM_ICONS: Record = {
+ facebook: '📘',
+ instagram: '📷',
+ linkedin: '💼',
+ twitter: '🐦',
+ tiktok: '🎵',
+ google: '🔍',
+ youtube: '▶️',
+ pinterest: '📌',
+};
+
+const STATUS_VARIANTS: Record = {
+ connected: 'active',
+ active: 'active',
+ expired: 'paused',
+ disconnected: 'error',
+ pending: 'pending',
+};
+
+// ─── Helpers ────────────────────────────────────────────────
+
+function formatDate(d?: string): string {
+ if (!d) return '';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+// ─── Extract data from tool result ──────────────────────────
+
+function extractData(result: CallToolResult): AccountsData | null {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc as AccountsData;
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text) as AccountsData; } catch { /* skip */ }
+ }
+ }
+ }
+ return null;
+}
+
+// ─── App ────────────────────────────────────────────────────
+
+export function App() {
+ const [data, setData] = useState(null);
+
+ useEffect(() => {
+ const d = (window as any).__MCP_APP_DATA__;
+ if (d && !data) setData(d as AccountsData);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: 'social-accounts', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (createdApp) => {
+ createdApp.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ if (error) {
+ return Connection Error
{error.message}
;
+ }
+ if (!isConnected || !app) {
+ return ;
+ }
+ if (!data) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+// ─── View ───────────────────────────────────────────────────
+
+function SocialAccountsView({ accounts }: { accounts: SocialAccount[] }) {
+ const connectedCount = accounts.filter(a => a.status === 'connected' || a.status === 'active').length;
+ const expiredCount = accounts.filter(a => a.status === 'expired').length;
+ const disconnectedCount = accounts.filter(a => a.status === 'disconnected').length;
+
+ // Convert to CardGrid items
+ const cards: CardGridItem[] = useMemo(() => {
+ return accounts.map(a => {
+ const icon = PLATFORM_ICONS[a.platform?.toLowerCase() || ''] || '🌐';
+ const platformName = a.platform
+ ? a.platform.charAt(0).toUpperCase() + a.platform.slice(1)
+ : 'Unknown';
+ const statusVariant = STATUS_VARIANTS[a.status?.toLowerCase() || ''] || 'pending';
+
+ return {
+ title: `${icon} ${platformName}`,
+ subtitle: a.name || 'Unknown Account',
+ description: a.connectedAt ? `Connected: ${formatDate(a.connectedAt)}` : undefined,
+ imageUrl: a.avatarUrl,
+ status: a.status
+ ? a.status.charAt(0).toUpperCase() + a.status.slice(1)
+ : 'Unknown',
+ statusVariant: statusVariant as string,
+ };
+ });
+ }, [accounts]);
+
+ return (
+ <>
+ 0 ? [{ label: 'Expired', value: String(expiredCount) }] : []),
+ ...(disconnectedCount > 0 ? [{ label: 'Disconnected', value: String(disconnectedCount) }] : []),
+ ]}
+ />
+
+ {accounts.length === 0 ? (
+
+
🔗
+
No social accounts connected
+
+ ) : (
+
+ )}
+ >
+ );
+}
diff --git a/src/ui/react-app/src/apps/social-accounts/index.html b/src/ui/react-app/src/apps/social-accounts/index.html
new file mode 100644
index 0000000..60e2a96
--- /dev/null
+++ b/src/ui/react-app/src/apps/social-accounts/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/social-accounts/main.tsx b/src/ui/react-app/src/apps/social-accounts/main.tsx
new file mode 100644
index 0000000..fbe1cf6
--- /dev/null
+++ b/src/ui/react-app/src/apps/social-accounts/main.tsx
@@ -0,0 +1,9 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.js';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/src/ui/react-app/src/apps/social-accounts/vite.config.ts b/src/ui/react-app/src/apps/social-accounts/vite.config.ts
new file mode 100644
index 0000000..b3202a5
--- /dev/null
+++ b/src/ui/react-app/src/apps/social-accounts/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/social-accounts'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/social-calendar/App.tsx b/src/ui/react-app/src/apps/social-calendar/App.tsx
new file mode 100644
index 0000000..a00d17f
--- /dev/null
+++ b/src/ui/react-app/src/apps/social-calendar/App.tsx
@@ -0,0 +1,280 @@
+/**
+ * social-calendar — Scheduled posts calendar view.
+ * Calendar with post markers colored by platform, date filtering, platform/status filters.
+ */
+import React, { useState, useEffect, useMemo, useCallback } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { MCPAppProvider } from '../../context/MCPAppContext';
+import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { CalendarView } from '../../components/viz/CalendarView';
+import { FilterChips } from '../../components/shared/FilterChips';
+import { Card } from '../../components/layout/Card';
+import { StatusBadge } from '../../components/data/StatusBadge';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+// ─── Types ──────────────────────────────────────────────────
+
+interface SocialPost {
+ id?: string;
+ title?: string;
+ content?: string;
+ platform?: string;
+ status?: string;
+ scheduledAt?: string;
+ publishedAt?: string;
+ accountName?: string;
+}
+
+interface CalendarData {
+ posts: SocialPost[];
+}
+
+// ─── Constants ──────────────────────────────────────────────
+
+const PLATFORM_COLORS: Record = {
+ facebook: '#1877F2',
+ instagram: '#E4405F',
+ linkedin: '#0A66C2',
+ twitter: '#1DA1F2',
+ tiktok: '#000000',
+ google: '#4285F4',
+};
+
+const STATUS_VARIANTS: Record = {
+ scheduled: 'pending',
+ published: 'complete',
+ failed: 'error',
+};
+
+// ─── Extract data from tool result ──────────────────────────
+
+function extractData(result: CallToolResult): CalendarData | null {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc as CalendarData;
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text) as CalendarData; } catch { /* skip */ }
+ }
+ }
+ }
+ return null;
+}
+
+// ─── App ────────────────────────────────────────────────────
+
+export function App() {
+ const [data, setData] = useState(null);
+
+ useEffect(() => {
+ const d = (window as any).__MCP_APP_DATA__;
+ if (d && !data) setData(d as CalendarData);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: 'social-calendar', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (createdApp) => {
+ createdApp.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ if (error) {
+ return Connection Error
{error.message}
;
+ }
+ if (!isConnected || !app) {
+ return ;
+ }
+ if (!data) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+// ─── View ───────────────────────────────────────────────────
+
+function SocialCalendarView({ posts }: { posts: SocialPost[] }) {
+ const [platformFilters, setPlatformFilters] = useState>(new Set());
+ const [statusFilters, setStatusFilters] = useState>(new Set());
+ const [selectedDate, setSelectedDate] = useState(null);
+
+ // Get unique platforms and statuses
+ const platforms = useMemo(() => {
+ const set = new Set();
+ posts.forEach(p => { if (p.platform) set.add(p.platform); });
+ return Array.from(set);
+ }, [posts]);
+
+ const statuses = useMemo(() => {
+ const set = new Set();
+ posts.forEach(p => { if (p.status) set.add(p.status); });
+ return Array.from(set);
+ }, [posts]);
+
+ // Filter posts
+ const filteredPosts = useMemo(() => {
+ return posts.filter(p => {
+ if (platformFilters.size > 0 && p.platform && !platformFilters.has(p.platform)) return false;
+ if (statusFilters.size > 0 && p.status && !statusFilters.has(p.status)) return false;
+ return true;
+ });
+ }, [posts, platformFilters, statusFilters]);
+
+ // Convert posts to calendar events
+ const calendarEvents = useMemo(() => {
+ return filteredPosts
+ .filter(p => p.scheduledAt || p.publishedAt)
+ .map(p => ({
+ title: p.title || p.content?.slice(0, 30) || p.platform || 'Post',
+ date: (p.scheduledAt || p.publishedAt)!,
+ time: undefined,
+ type: p.platform || 'post',
+ color: PLATFORM_COLORS[p.platform?.toLowerCase() || ''] || '#6b7280',
+ }));
+ }, [filteredPosts]);
+
+ // Posts for selected date
+ const selectedDatePosts = useMemo(() => {
+ if (!selectedDate) return [];
+ return filteredPosts.filter(p => {
+ const date = p.scheduledAt || p.publishedAt;
+ if (!date) return false;
+ try {
+ return new Date(date).toISOString().slice(0, 10) === selectedDate;
+ } catch { return false; }
+ });
+ }, [filteredPosts, selectedDate]);
+
+ const handleDateClick = useCallback((date: string) => {
+ setSelectedDate(prev => prev === date ? null : date);
+ }, []);
+
+ // Platform filter chips
+ const platformChips = platforms.map(p => ({
+ label: p.charAt(0).toUpperCase() + p.slice(1),
+ value: p,
+ active: platformFilters.has(p),
+ }));
+
+ // Status filter chips
+ const statusChips = statuses.map(s => ({
+ label: s.charAt(0).toUpperCase() + s.slice(1),
+ value: s,
+ active: statusFilters.has(s),
+ }));
+
+ return (
+ <>
+ ({
+ label: p.charAt(0).toUpperCase() + p.slice(1),
+ value: String(posts.filter(post => post.platform === p).length),
+ }))}
+ />
+
+
+ {platformChips.length > 0 && (
+
+ {platformChips.map((c, i) => (
+
+ ))}
+
+ )}
+ {statusChips.length > 0 && (
+
+ {statusChips.map(c => (
+
+ ))}
+
+ )}
+
+
+
+
+ {selectedDate && (
+
+ {selectedDatePosts.length === 0 ? (
+ No posts on this date
+ ) : (
+
+ {selectedDatePosts.map((p, i) => (
+
+
+
+ {p.platform?.charAt(0).toUpperCase()}{p.platform?.slice(1)}
+ {p.accountName ? ` · ${p.accountName}` : ''}
+
+ {p.status && (
+
+ )}
+
+
+ {p.title || p.content?.slice(0, 80) || 'No content'}
+
+
+ ))}
+
+ )}
+
+ )}
+ >
+ );
+}
diff --git a/src/ui/react-app/src/apps/social-calendar/index.html b/src/ui/react-app/src/apps/social-calendar/index.html
new file mode 100644
index 0000000..60e2a96
--- /dev/null
+++ b/src/ui/react-app/src/apps/social-calendar/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/social-calendar/main.tsx b/src/ui/react-app/src/apps/social-calendar/main.tsx
new file mode 100644
index 0000000..fbe1cf6
--- /dev/null
+++ b/src/ui/react-app/src/apps/social-calendar/main.tsx
@@ -0,0 +1,9 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.js';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/src/ui/react-app/src/apps/social-calendar/vite.config.ts b/src/ui/react-app/src/apps/social-calendar/vite.config.ts
new file mode 100644
index 0000000..d488ee4
--- /dev/null
+++ b/src/ui/react-app/src/apps/social-calendar/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/social-calendar'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/social-post-composer/App.tsx b/src/ui/react-app/src/apps/social-post-composer/App.tsx
new file mode 100644
index 0000000..d204e96
--- /dev/null
+++ b/src/ui/react-app/src/apps/social-post-composer/App.tsx
@@ -0,0 +1,253 @@
+/**
+ * social-post-composer — Create/schedule social media posts.
+ * Platform tabs, account selector, content editor, scheduling.
+ */
+import React, { useState, useEffect, useCallback } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { MCPAppProvider } from '../../context/MCPAppContext';
+import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { Card } from '../../components/layout/Card';
+import { TabGroup } from '../../components/shared/TabGroup';
+import { SelectDropdown } from '../../components/interactive/SelectDropdown';
+import { ActionButton } from '../../components/shared/ActionButton';
+import { useSmartAction } from '../../hooks/useSmartAction';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+// ─── Types ──────────────────────────────────────────────────
+
+interface SocialAccount {
+ id: string;
+ name: string;
+ platform: string;
+ status?: string;
+}
+
+interface ComposerData {
+ accounts?: SocialAccount[];
+}
+
+// ─── Constants ──────────────────────────────────────────────
+
+const PLATFORMS = [
+ { label: 'Facebook', value: 'facebook' },
+ { label: 'Instagram', value: 'instagram' },
+ { label: 'LinkedIn', value: 'linkedin' },
+ { label: 'Twitter', value: 'twitter' },
+ { label: 'TikTok', value: 'tiktok' },
+ { label: 'Google', value: 'google' },
+];
+
+// ─── Extract data from tool result ──────────────────────────
+
+function extractData(result: CallToolResult): ComposerData | null {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc as ComposerData;
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text) as ComposerData; } catch { /* skip */ }
+ }
+ }
+ }
+ return null;
+}
+
+// ─── App ────────────────────────────────────────────────────
+
+export function App() {
+ const [data, setData] = useState(null);
+
+ useEffect(() => {
+ const d = (window as any).__MCP_APP_DATA__;
+ if (d && !data) setData(d as ComposerData);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: 'social-post-composer', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (createdApp) => {
+ createdApp.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ if (error) {
+ return Connection Error
{error.message}
;
+ }
+ if (!isConnected || !app) {
+ return ;
+ }
+ // Allow rendering even without data (accounts are optional)
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+// ─── View ───────────────────────────────────────────────────
+
+function ComposerView({ accounts }: { accounts: SocialAccount[] }) {
+ const [activePlatform, setActivePlatform] = useState('facebook');
+ const [selectedAccountId, setSelectedAccountId] = useState('');
+ const [content, setContent] = useState('');
+ const [mediaUrl, setMediaUrl] = useState('');
+ const [scheduleDate, setScheduleDate] = useState('');
+ const [scheduleTime, setScheduleTime] = useState('');
+ const [submitResult, setSubmitResult] = useState<'success' | 'queued' | null>(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const { executeAction } = useSmartAction();
+
+ // Filter accounts for selected platform
+ const platformAccounts = accounts.filter(
+ a => a.platform?.toLowerCase() === activePlatform
+ );
+
+ const accountOptions = platformAccounts.map(a => ({
+ label: `${a.name}${a.status && a.status !== 'connected' ? ` (${a.status})` : ''}`,
+ value: a.id,
+ }));
+
+ const handlePublish = useCallback(async () => {
+ if (!content.trim()) return;
+ setIsSubmitting(true);
+ setSubmitResult(null);
+
+ const result = await executeAction({
+ type: 'create_social_post',
+ args: {
+ platform: activePlatform,
+ accountId: selectedAccountId || undefined,
+ content: content.trim(),
+ mediaUrl: mediaUrl.trim() || undefined,
+ },
+ description: `Create ${activePlatform} post: "${content.slice(0, 50)}..."`,
+ });
+
+ setIsSubmitting(false);
+ setSubmitResult(result.queued ? 'queued' : result.success ? 'success' : null);
+ }, [activePlatform, selectedAccountId, content, mediaUrl, executeAction]);
+
+ const handleSchedule = useCallback(async () => {
+ if (!content.trim() || !scheduleDate) return;
+ setIsSubmitting(true);
+ setSubmitResult(null);
+
+ const scheduledAt = scheduleTime
+ ? `${scheduleDate}T${scheduleTime}`
+ : `${scheduleDate}T09:00`;
+
+ const result = await executeAction({
+ type: 'schedule_social_post',
+ args: {
+ platform: activePlatform,
+ accountId: selectedAccountId || undefined,
+ content: content.trim(),
+ mediaUrl: mediaUrl.trim() || undefined,
+ scheduledAt,
+ },
+ description: `Schedule ${activePlatform} post for ${scheduledAt}`,
+ });
+
+ setIsSubmitting(false);
+ setSubmitResult(result.queued ? 'queued' : result.success ? 'success' : null);
+ }, [activePlatform, selectedAccountId, content, mediaUrl, scheduleDate, scheduleTime, executeAction]);
+
+ return (
+ <>
+
+
+
+
+ p.value === activePlatform)?.label || 'Post'} Details`}>
+ {accountOptions.length > 0 && (
+
+ )}
+
+
+
+
+
+ setMediaUrl(e.target.value)}
+ />
+
+
+
+
+
+ {submitResult === 'success' && ✓ Done}
+ {submitResult === 'queued' && ● Queued}
+
+
+
+
+ >
+ );
+}
diff --git a/src/ui/react-app/src/apps/social-post-composer/index.html b/src/ui/react-app/src/apps/social-post-composer/index.html
new file mode 100644
index 0000000..60e2a96
--- /dev/null
+++ b/src/ui/react-app/src/apps/social-post-composer/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/social-post-composer/main.tsx b/src/ui/react-app/src/apps/social-post-composer/main.tsx
new file mode 100644
index 0000000..fbe1cf6
--- /dev/null
+++ b/src/ui/react-app/src/apps/social-post-composer/main.tsx
@@ -0,0 +1,9 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.js';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/src/ui/react-app/src/apps/social-post-composer/vite.config.ts b/src/ui/react-app/src/apps/social-post-composer/vite.config.ts
new file mode 100644
index 0000000..48a16ec
--- /dev/null
+++ b/src/ui/react-app/src/apps/social-post-composer/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/social-post-composer'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/subscription-manager/App.tsx b/src/ui/react-app/src/apps/subscription-manager/App.tsx
new file mode 100644
index 0000000..374403f
--- /dev/null
+++ b/src/ui/react-app/src/apps/subscription-manager/App.tsx
@@ -0,0 +1,179 @@
+/**
+ * Subscription Manager — Active subscriptions view with MRR stats.
+ */
+import React, { useState, useMemo, useCallback } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { DataTable } from '../../components/data/DataTable';
+import { StatsGrid } from '../../components/layout/StatsGrid';
+import { MetricCard } from '../../components/data/MetricCard';
+import type { TableColumn } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatCurrency(n: number, currency = 'USD'): string {
+ try { return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n); }
+ catch { return `$${n.toFixed(2)}`; }
+}
+
+function formatDate(d?: string): string {
+ if (!d) return '—';
+ try { return new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); }
+ catch { return d; }
+}
+
+const STATUS_FILTERS = ['all', 'active', 'paused', 'cancelled', 'trialing'] as const;
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [activeFilter, setActiveFilter] = useState('all');
+ const [actionResult, setActionResult] = useState<{ type: 'success' | 'error'; msg: string } | null>(null);
+ const [isActing, setIsActing] = useState(false);
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: 'Subscription Manager', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const handleSubAction = useCallback(async (action: string, subData: Record) => {
+ if (!app) return;
+ setIsActing(true);
+ setActionResult(null);
+ try {
+ await app.updateModelContext({
+ content: [{
+ type: 'text',
+ text: JSON.stringify({ action, data: subData }),
+ }],
+ });
+ setActionResult({ type: 'success', msg: `✓ ${action.replace('_', ' ')} request sent` });
+ setTimeout(() => setActionResult(null), 3000);
+ } catch {
+ setActionResult({ type: 'error', msg: '✗ Failed to send request' });
+ } finally {
+ setIsActing(false);
+ }
+ }, [app]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const subscriptions: any[] = data.subscriptions || [];
+ const currency = data.currency || 'USD';
+
+ const stats = useMemo(() => {
+ let mrr = 0, activeCount = 0, cancelledCount = 0;
+ for (const sub of subscriptions) {
+ const s = (sub.status || '').toLowerCase();
+ if (s === 'active' || s === 'trialing') { activeCount++; mrr += sub.amount ?? 0; }
+ else if (s === 'cancelled' || s === 'canceled') cancelledCount++;
+ }
+ const churnRate = subscriptions.length > 0 ? ((cancelledCount / subscriptions.length) * 100).toFixed(1) + '%' : '0%';
+ return { mrr, activeCount, churnRate };
+ }, [subscriptions]);
+
+ const filtered = useMemo(() => {
+ if (activeFilter === 'all') return subscriptions;
+ return subscriptions.filter((sub: any) => (sub.status || '').toLowerCase() === activeFilter);
+ }, [subscriptions, activeFilter]);
+
+ const rows = filtered.map((sub: any) => ({
+ id: sub.id || '',
+ contact: sub.contact || sub.contactName || '—',
+ plan: sub.plan || sub.planName || '—',
+ amount: formatCurrency(sub.amount ?? 0, sub.currency || currency),
+ status: sub.status || '—',
+ startDate: formatDate(sub.startDate),
+ nextBilling: formatDate(sub.nextBilling || sub.nextBillingDate),
+ }));
+
+ const columns: TableColumn[] = [
+ { key: 'id', label: 'ID', sortable: true, width: '120px' },
+ { key: 'contact', label: 'Contact', sortable: true, format: 'avatar' },
+ { key: 'plan', label: 'Plan', sortable: true },
+ { key: 'amount', label: 'Amount', sortable: true, format: 'currency' },
+ { key: 'status', label: 'Status', sortable: true, format: 'status' },
+ { key: 'startDate', label: 'Start Date', sortable: true, format: 'date' },
+ { key: 'nextBilling', label: 'Next Billing', sortable: true, format: 'date' },
+ ];
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {STATUS_FILTERS.map((f) => (
+
+ ))}
+
+ {actionResult && (
+
+ {actionResult.msg}
+
+ )}
+
+
+
+
+ {/* Quick subscription actions */}
+ {filtered.length > 0 && (
+
+ Actions:
+ {filtered
+ .filter((sub: any) => (sub.status || '').toLowerCase() === 'active')
+ .slice(0, 5)
+ .map((sub: any) => (
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/subscription-manager/index.html b/src/ui/react-app/src/apps/subscription-manager/index.html
new file mode 100644
index 0000000..7e65867
--- /dev/null
+++ b/src/ui/react-app/src/apps/subscription-manager/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Subscription Manager
+
+
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/subscription-manager/main.tsx b/src/ui/react-app/src/apps/subscription-manager/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/subscription-manager/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/subscription-manager/vite.config.ts b/src/ui/react-app/src/apps/subscription-manager/vite.config.ts
new file mode 100644
index 0000000..1b8bcb6
--- /dev/null
+++ b/src/ui/react-app/src/apps/subscription-manager/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/subscription-manager'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/survey-list/App.tsx b/src/ui/react-app/src/apps/survey-list/App.tsx
new file mode 100644
index 0000000..41a2116
--- /dev/null
+++ b/src/ui/react-app/src/apps/survey-list/App.tsx
@@ -0,0 +1,111 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { DataTable } from '../../components/data/DataTable';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatDate(d?: string): string {
+ if (!d) return '\u2014';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [search, setSearch] = useState('');
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Survey List', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const surveys: any[] = useMemo(() => data?.surveys || [], [data]);
+
+ const rows = useMemo(() => {
+ return surveys
+ .map((s) => {
+ const status = s.status || 'draft';
+ return {
+ id: s.id || '',
+ name: s.name || 'Untitled Survey',
+ type: s.type || 'survey',
+ responses: s.responses ?? s.responseCount ?? s.submissions ?? 0,
+ status,
+ createdAt: s.createdAt
+ ? formatDate(s.createdAt)
+ : s.dateAdded || '—',
+ };
+ })
+ .filter((r) => {
+ if (!search) return true;
+ const q = search.toLowerCase();
+ return (
+ r.name.toLowerCase().includes(q) ||
+ r.type.toLowerCase().includes(q) ||
+ r.status.toLowerCase().includes(q)
+ );
+ });
+ }, [surveys, search]);
+
+ const activeCount = rows.filter((r) => r.status === 'active' || r.status === 'published').length;
+ const totalResponses = rows.reduce((sum, r) => sum + r.responses, 0);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ return (
+
+
+ setSearch(e.target.value)}
+ />
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/survey-list/index.html b/src/ui/react-app/src/apps/survey-list/index.html
new file mode 100644
index 0000000..49d0f2c
--- /dev/null
+++ b/src/ui/react-app/src/apps/survey-list/index.html
@@ -0,0 +1,5 @@
+
+
+Surveys
+
+
diff --git a/src/ui/react-app/src/apps/survey-list/main.tsx b/src/ui/react-app/src/apps/survey-list/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/survey-list/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/survey-list/vite.config.ts b/src/ui/react-app/src/apps/survey-list/vite.config.ts
new file mode 100644
index 0000000..b16d487
--- /dev/null
+++ b/src/ui/react-app/src/apps/survey-list/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/survey-list'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/survey-submissions/App.tsx b/src/ui/react-app/src/apps/survey-submissions/App.tsx
new file mode 100644
index 0000000..25571c0
--- /dev/null
+++ b/src/ui/react-app/src/apps/survey-submissions/App.tsx
@@ -0,0 +1,134 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { DetailHeader } from '../../components/data/DetailHeader';
+import { DataTable } from '../../components/data/DataTable';
+import { SplitLayout } from '../../components/layout/SplitLayout';
+import { Card } from '../../components/layout/Card';
+import { KeyValueList } from '../../components/data/KeyValueList';
+import { Timeline } from '../../components/data/Timeline';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function formatDate(d?: string): string {
+ if (!d) return '—';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit',
+ });
+ } catch { return d; }
+}
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [expandedId, setExpandedId] = useState(null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Survey Submissions', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const surveyName = data?.surveyName || data?.name || 'Survey';
+ const submissions: any[] = useMemo(() => data?.submissions || data?.responses || [], [data]);
+
+ const rows = useMemo(() => {
+ return submissions.map((s, i) => {
+ const contact = s.contactName || s.name || s.email || 'Anonymous';
+ const date = s.createdAt
+ ? formatDate(s.createdAt)
+ : s.submittedAt ? formatDate(s.submittedAt) : '—';
+ const answers = s.data || s.answers || s.fields || {};
+ const answerCount = Object.keys(answers).length;
+ return {
+ id: s.id || String(i),
+ contact,
+ date,
+ answers: `${answerCount} answer${answerCount !== 1 ? 's' : ''}`,
+ _idx: i,
+ };
+ });
+ }, [submissions]);
+
+ const expandedSubmission = expandedId !== null
+ ? submissions.find((_s: any, i: number) => (submissions[i]?.id || String(i)) === expandedId)
+ : null;
+
+ const expandedFields = expandedSubmission
+ ? Object.entries(expandedSubmission.data || expandedSubmission.answers || expandedSubmission.fields || {})
+ .map(([k, v]) => ({ label: k, value: String(v) }))
+ : [];
+
+ const timelineEvents = useMemo(() => {
+ return submissions.slice(0, 10).map((s, i) => ({
+ title: s.contactName || s.name || s.email || 'Anonymous',
+ description: `Submitted ${Object.keys(s.data || s.answers || s.fields || {}).length} answers`,
+ timestamp: s.createdAt
+ ? formatDate(s.createdAt)
+ : s.submittedAt ? formatDate(s.submittedAt) : '—',
+ icon: 'note' as const,
+ variant: 'default' as const,
+ }));
+ }, [submissions]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ return (
+
+
0 ? 'Active' : 'No Responses'}
+ statusVariant={submissions.length > 0 ? 'active' : 'draft'}
+ />
+
+
+
+
+
+ {expandedSubmission && expandedFields.length > 0 && (
+
+
+
+ )}
+
+
+
+ {timelineEvents.length > 0 ? (
+
+ ) : (
+
+ No recent activity
+
+ )}
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/survey-submissions/index.html b/src/ui/react-app/src/apps/survey-submissions/index.html
new file mode 100644
index 0000000..ff3f5b4
--- /dev/null
+++ b/src/ui/react-app/src/apps/survey-submissions/index.html
@@ -0,0 +1,5 @@
+
+
+Survey Submissions
+
+
diff --git a/src/ui/react-app/src/apps/survey-submissions/main.tsx b/src/ui/react-app/src/apps/survey-submissions/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/survey-submissions/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/survey-submissions/vite.config.ts b/src/ui/react-app/src/apps/survey-submissions/vite.config.ts
new file mode 100644
index 0000000..062c9d4
--- /dev/null
+++ b/src/ui/react-app/src/apps/survey-submissions/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/survey-submissions'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/tags-manager/App.tsx b/src/ui/react-app/src/apps/tags-manager/App.tsx
new file mode 100644
index 0000000..baa4e98
--- /dev/null
+++ b/src/ui/react-app/src/apps/tags-manager/App.tsx
@@ -0,0 +1,302 @@
+/**
+ * Tags Manager — Tags list with search and visual preview.
+ * Columns: name, color, usageCount, createdAt.
+ * Client-side search. Visual tag preview with TagList component.
+ */
+import React, { useMemo, useState, useCallback } from "react";
+import { useApp } from "@modelcontextprotocol/ext-apps/react";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import { PageHeader } from "../../components/layout/PageHeader.js";
+import { Section } from "../../components/layout/Section.js";
+import { DataTable } from "../../components/data/DataTable.js";
+import { TagList } from "../../components/data/TagList.js";
+import { Card } from "../../components/layout/Card.js";
+import type { TagColor } from "../../types.js";
+import "../../styles/base.css";
+import "../../styles/interactive.css";
+
+// ─── Types ──────────────────────────────────────────────────
+
+interface TagEntry {
+ id?: string;
+ name: string;
+ color?: TagColor;
+ usageCount?: number;
+ createdAt?: string;
+}
+
+interface TagsManagerData {
+ tags: TagEntry[];
+}
+
+// ─── Data Extraction ────────────────────────────────────────
+
+function formatDate(d?: string): string {
+ if (!d) return "—";
+ try {
+ return new Date(d).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ });
+ } catch {
+ return d;
+ }
+}
+
+function extractData(result: CallToolResult): TagsManagerData | null {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc as TagsManagerData;
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === "text") {
+ try {
+ return JSON.parse(item.text) as TagsManagerData;
+ } catch {
+ /* skip */
+ }
+ }
+ }
+ }
+ return null;
+}
+
+// ─── App ────────────────────────────────────────────────────
+
+export function App() {
+ const [data, setData] = React.useState(null);
+ const [searchQuery, setSearchQuery] = useState("");
+
+ const [actionResult, setActionResult] = useState<{ type: 'success' | 'error'; msg: string } | null>(null);
+ const [isActing, setIsActing] = useState(false);
+ const [newTagName, setNewTagName] = useState("");
+ const [showCreateForm, setShowCreateForm] = useState(false);
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: "tags-manager", version: "1.0.0" },
+ capabilities: {},
+ onAppCreated: (createdApp) => {
+ createdApp.ontoolresult = async (result) => {
+ const extracted = extractData(result);
+ if (extracted) setData(extracted);
+ };
+ },
+ });
+
+ React.useEffect(() => {
+ const preInjected = (window as any).__MCP_APP_DATA__;
+ if (preInjected && !data) setData(preInjected as TagsManagerData);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const handleSearch = useCallback((e: React.ChangeEvent) => {
+ setSearchQuery(e.target.value);
+ }, []);
+
+ const handleTagAction = useCallback(async (action: string, tagData: Record) => {
+ if (!app) return;
+ setIsActing(true);
+ setActionResult(null);
+ try {
+ await app.updateModelContext({
+ content: [{
+ type: 'text',
+ text: JSON.stringify({ action, data: tagData }),
+ }],
+ });
+ setActionResult({ type: 'success', msg: `✓ ${action.replace('_', ' ')} request sent` });
+ setTimeout(() => setActionResult(null), 3000);
+ } catch {
+ setActionResult({ type: 'error', msg: '✗ Failed to send request' });
+ } finally {
+ setIsActing(false);
+ }
+ }, [app]);
+
+ const handleCreateTag = useCallback(() => {
+ if (!newTagName.trim()) return;
+ handleTagAction('create_tag', { name: newTagName.trim() });
+ setNewTagName("");
+ setShowCreateForm(false);
+ }, [newTagName, handleTagAction]);
+
+ const handleDeleteTag = useCallback((tagId: string, tagName: string) => {
+ handleTagAction('delete_tag', { tagId, name: tagName });
+ }, [handleTagAction]);
+
+ // Filter tags by search query
+ const filteredTags = useMemo(() => {
+ const tags = data?.tags ?? [];
+ if (!searchQuery.trim()) return tags;
+ const q = searchQuery.toLowerCase();
+ return tags.filter((t) => t.name.toLowerCase().includes(q));
+ }, [data?.tags, searchQuery]);
+
+ // Tag preview items for TagList component
+ const tagPreviewItems = useMemo(
+ () =>
+ filteredTags.map((t) => ({
+ label: t.name,
+ color: t.color ?? ("blue" as TagColor),
+ variant: "filled" as const,
+ })),
+ [filteredTags],
+ );
+
+ // Table rows
+ const tableRows = useMemo(
+ () =>
+ filteredTags.map((t, idx) => ({
+ id: t.id ?? `tag-${idx}`,
+ name: t.name,
+ color: t.color ?? "—",
+ usageCount: t.usageCount ?? 0,
+ createdAt: formatDate(t.createdAt),
+ })),
+ [filteredTags],
+ );
+
+ const tableColumns = useMemo(() => [
+ { key: "name", label: "Tag Name", sortable: true },
+ { key: "color", label: "Color", sortable: true },
+ { key: "usageCount", label: "Usage Count", sortable: true },
+ { key: "createdAt", label: "Created", sortable: true, format: "date" },
+ ], []);
+
+ // Stats
+ const totalTags = data?.tags?.length ?? 0;
+ const totalUsage = useMemo(
+ () => (data?.tags ?? []).reduce((s, t) => s + (t.usageCount ?? 0), 0),
+ [data?.tags],
+ );
+ const topTag = useMemo(() => {
+ const tags = data?.tags ?? [];
+ if (tags.length === 0) return "—";
+ const sorted = [...tags].sort((a, b) => (b.usageCount ?? 0) - (a.usageCount ?? 0));
+ return sorted[0]?.name ?? "—";
+ }, [data?.tags]);
+
+ if (error) {
+ return (
+
+
Connection Error
+
{error.message}
+
+ );
+ }
+
+ if (!data) {
+ return (
+
+
+
{isConnected ? "Waiting for data..." : "Connecting..."}
+
+ );
+ }
+
+ return (
+
+
+
+ {/* Action bar */}
+
+
+
+
+
+ {actionResult && (
+
+ {actionResult.msg}
+
+ )}
+
+
+
+
+ {/* Create Tag Form */}
+ {showCreateForm && (
+
+
+
+
+ setNewTagName(e.target.value)}
+ onKeyDown={(e) => { if (e.key === 'Enter') handleCreateTag(); }}
+ />
+
+
+
+
+ )}
+
+ {/* Tag Preview */}
+
+
+ {filteredTags.length > 20 && (
+
+ Showing 20 of {filteredTags.length} tags
+
+ )}
+
+
+ {/* Tags Table */}
+
+
+ {/* Quick delete actions */}
+ {filteredTags.length > 0 && (
+
+ Quick delete:
+ {filteredTags.slice(0, 8).map((t, idx) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/tags-manager/index.html b/src/ui/react-app/src/apps/tags-manager/index.html
new file mode 100644
index 0000000..8595fb1
--- /dev/null
+++ b/src/ui/react-app/src/apps/tags-manager/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Tags Manager
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/tags-manager/main.tsx b/src/ui/react-app/src/apps/tags-manager/main.tsx
new file mode 100644
index 0000000..c3e2da2
--- /dev/null
+++ b/src/ui/react-app/src/apps/tags-manager/main.tsx
@@ -0,0 +1,9 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { App } from "./App.js";
+
+createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
diff --git a/src/ui/react-app/src/apps/tags-manager/vite.config.ts b/src/ui/react-app/src/apps/tags-manager/vite.config.ts
new file mode 100644
index 0000000..fcb04b8
--- /dev/null
+++ b/src/ui/react-app/src/apps/tags-manager/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/tags-manager'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/task-board/App.tsx b/src/ui/react-app/src/apps/task-board/App.tsx
new file mode 100644
index 0000000..f5a280b
--- /dev/null
+++ b/src/ui/react-app/src/apps/task-board/App.tsx
@@ -0,0 +1,201 @@
+import React, { useState, useMemo, useCallback } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { DataTable } from '../../components/data/DataTable';
+import { ProgressBar } from '../../components/data/ProgressBar';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function formatDate(d?: string): string {
+ if (!d) return '—';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+type FilterKey = 'status' | 'priority';
+
+const STATUS_OPTIONS = ['All', 'pending', 'in_progress', 'completed', 'overdue'];
+const PRIORITY_OPTIONS = ['All', 'low', 'medium', 'high'];
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [statusFilter, setStatusFilter] = useState('All');
+ const [priorityFilter, setPriorityFilter] = useState('All');
+ const [actionResult, setActionResult] = useState<{ type: 'success' | 'error'; msg: string } | null>(null);
+ const [isActing, setIsActing] = useState(false);
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: 'Task Board', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const tasks: any[] = data?.tasks || [];
+
+ const rows = useMemo(() => {
+ return tasks
+ .filter((t) => {
+ if (statusFilter !== 'All' && t.status !== statusFilter) return false;
+ if (priorityFilter !== 'All' && t.priority !== priorityFilter) return false;
+ return true;
+ })
+ .map((t) => ({
+ id: t.id || '',
+ title: t.title || 'Untitled',
+ status: t.status || 'pending',
+ priority: t.priority || 'medium',
+ dueDate: formatDate(t.dueDate),
+ assignee: t.assignedTo || t.assignee || '—',
+ contact: t.contactName || t.contact || '—',
+ }));
+ }, [tasks, statusFilter, priorityFilter]);
+
+ const completedCount = tasks.filter((t) => t.status === 'completed' || t.completed).length;
+
+ const handleTaskAction = useCallback(async (action: string, taskData: Record) => {
+ if (!app) return;
+ setIsActing(true);
+ setActionResult(null);
+ try {
+ await app.updateModelContext({
+ content: [{
+ type: 'text',
+ text: JSON.stringify({ action, data: taskData }),
+ }],
+ });
+ setActionResult({ type: 'success', msg: `✓ ${action.replace('_', ' ')} request sent` });
+ setTimeout(() => setActionResult(null), 3000);
+ } catch {
+ setActionResult({ type: 'error', msg: '✗ Failed to send request' });
+ } finally {
+ setIsActing(false);
+ }
+ }, [app]);
+
+ const handleToggleComplete = useCallback((taskId: string, currentStatus: string) => {
+ const newStatus = currentStatus === 'completed' ? 'pending' : 'completed';
+ handleTaskAction('update_task', { taskId, status: newStatus });
+ }, [handleTaskAction]);
+
+ const handleCreateTask = useCallback(() => {
+ handleTaskAction('create_task', {});
+ }, [handleTaskAction]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ return (
+
+
+
+ {/* Action bar */}
+
+ {actionResult && (
+
+ {actionResult.msg}
+
+ )}
+
+
+
+
+
+ Status:
+ {STATUS_OPTIONS.map((s) => (
+
+ ))}
+
+
+ Priority:
+ {PRIORITY_OPTIONS.map((p) => (
+
+ ))}
+
+
+
+
+
+ {/* Quick task actions */}
+ {rows.length > 0 && (
+
+ Quick complete:
+ {rows.filter(r => r.status !== 'completed').slice(0, 5).map((r) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/task-board/index.html b/src/ui/react-app/src/apps/task-board/index.html
new file mode 100644
index 0000000..47263df
--- /dev/null
+++ b/src/ui/react-app/src/apps/task-board/index.html
@@ -0,0 +1,5 @@
+
+
+Task Board
+
+
diff --git a/src/ui/react-app/src/apps/task-board/main.tsx b/src/ui/react-app/src/apps/task-board/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/task-board/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/task-board/vite.config.ts b/src/ui/react-app/src/apps/task-board/vite.config.ts
new file mode 100644
index 0000000..029e9f3
--- /dev/null
+++ b/src/ui/react-app/src/apps/task-board/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/task-board'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/team-management/App.tsx b/src/ui/react-app/src/apps/team-management/App.tsx
new file mode 100644
index 0000000..340a9cd
--- /dev/null
+++ b/src/ui/react-app/src/apps/team-management/App.tsx
@@ -0,0 +1,136 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { DataTable } from '../../components/data/DataTable';
+import { StatusBadge } from '../../components/data/StatusBadge';
+import type { StatusVariant } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+const ROLE_FILTERS = ['All', 'Admin', 'User', 'Manager', 'Agent'] as const;
+
+function formatDate(d?: string): string {
+ if (!d) return '\u2014';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [search, setSearch] = useState('');
+ const [roleFilter, setRoleFilter] = useState('All');
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Team Management', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const rows = useMemo(() => {
+ const users: any[] = data?.users || data?.members || data?.team || [];
+ return users
+ .filter((u: any) => {
+ if (roleFilter !== 'All') {
+ const role = (u.role || u.type || '').toLowerCase();
+ if (role !== roleFilter.toLowerCase()) return false;
+ }
+ if (search) {
+ const q = search.toLowerCase();
+ const name = `${u.firstName || ''} ${u.lastName || ''}`.trim().toLowerCase() || (u.name || '').toLowerCase();
+ const email = (u.email || '').toLowerCase();
+ const role = (u.role || u.type || '').toLowerCase();
+ if (!name.includes(q) && !email.includes(q) && !role.includes(q)) return false;
+ }
+ return true;
+ })
+ .map((u: any) => {
+ const name = `${u.firstName || ''} ${u.lastName || ''}`.trim() || u.name || 'Unknown';
+ const status = u.status || (u.active !== false ? 'Active' : 'Inactive');
+ return {
+ id: u.id || '',
+ name,
+ email: u.email || '—',
+ role: u.role || u.type || '—',
+ status: status.charAt(0).toUpperCase() + status.slice(1),
+ lastActive: formatDate(u.lastActive || u.lastLogin),
+ };
+ });
+ }, [data, search, roleFilter]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const totalMembers = (data?.users || data?.members || data?.team || []).length;
+ const activeCount = (data?.users || data?.members || data?.team || []).filter(
+ (u: any) => (u.status || '').toLowerCase() === 'active' || u.active !== false
+ ).length;
+
+ return (
+
+
+
+ setSearch(e.target.value)}
+ />
+
+
+ {ROLE_FILTERS.map((r) => (
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/team-management/index.html b/src/ui/react-app/src/apps/team-management/index.html
new file mode 100644
index 0000000..601fc89
--- /dev/null
+++ b/src/ui/react-app/src/apps/team-management/index.html
@@ -0,0 +1,5 @@
+
+
+Team Management
+
+
diff --git a/src/ui/react-app/src/apps/team-management/main.tsx b/src/ui/react-app/src/apps/team-management/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/team-management/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/team-management/vite.config.ts b/src/ui/react-app/src/apps/team-management/vite.config.ts
new file mode 100644
index 0000000..a93bb01
--- /dev/null
+++ b/src/ui/react-app/src/apps/team-management/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/team-management'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/template-library/App.tsx b/src/ui/react-app/src/apps/template-library/App.tsx
new file mode 100644
index 0000000..8ef6d0d
--- /dev/null
+++ b/src/ui/react-app/src/apps/template-library/App.tsx
@@ -0,0 +1,133 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { DataTable } from '../../components/data/DataTable';
+import { Card } from '../../components/layout/Card';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatDate(d: string | undefined): string {
+ if (!d) return '-';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+function truncate(str: string, len: number): string {
+ if (!str) return '-';
+ return str.length > len ? str.slice(0, len) + '…' : str;
+}
+
+type TabValue = 'sms' | 'email';
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [activeTab, setActiveTab] = useState('sms');
+ const [search, setSearch] = useState('');
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Template Library', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const allTemplates = useMemo(() => {
+ const templates: any[] = data?.templates || data?.data || [];
+ return templates.map((t) => {
+ const type = (t.type || t.templateType || 'sms').toLowerCase();
+ return {
+ id: t.id || '',
+ name: t.name || t.title || 'Untitled Template',
+ type: type.includes('email') ? 'email' : 'sms',
+ typeLabel: type.includes('email') ? 'Email' : 'SMS',
+ content: truncate(t.content || t.body || t.preview || t.subject || '', 100),
+ createdDate: formatDate(t.createdAt || t.dateCreated || t.created),
+ };
+ });
+ }, [data]);
+
+ const counts = useMemo(() => {
+ return {
+ sms: allTemplates.filter((t) => t.type === 'sms').length,
+ email: allTemplates.filter((t) => t.type === 'email').length,
+ };
+ }, [allTemplates]);
+
+ const filtered = useMemo(() => {
+ return allTemplates
+ .filter((t) => t.type === activeTab)
+ .filter((t) => {
+ if (!search) return true;
+ const q = search.toLowerCase();
+ return t.name.toLowerCase().includes(q) || t.content.toLowerCase().includes(q);
+ });
+ }, [allTemplates, activeTab, search]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ return (
+
+
+
+
+
+
+
+ setSearch(e.target.value)}
+ />
+
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/template-library/index.html b/src/ui/react-app/src/apps/template-library/index.html
new file mode 100644
index 0000000..fbdd7fc
--- /dev/null
+++ b/src/ui/react-app/src/apps/template-library/index.html
@@ -0,0 +1,5 @@
+
+
+Template Library
+
+
diff --git a/src/ui/react-app/src/apps/template-library/main.tsx b/src/ui/react-app/src/apps/template-library/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/template-library/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/template-library/vite.config.ts b/src/ui/react-app/src/apps/template-library/vite.config.ts
new file mode 100644
index 0000000..40a2a65
--- /dev/null
+++ b/src/ui/react-app/src/apps/template-library/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/template-library'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/transaction-list/App.tsx b/src/ui/react-app/src/apps/transaction-list/App.tsx
new file mode 100644
index 0000000..b969662
--- /dev/null
+++ b/src/ui/react-app/src/apps/transaction-list/App.tsx
@@ -0,0 +1,118 @@
+/**
+ * Transaction List — Payments table with stats and filtering.
+ */
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { DataTable } from '../../components/data/DataTable';
+import { StatsGrid } from '../../components/layout/StatsGrid';
+import { MetricCard } from '../../components/data/MetricCard';
+import type { TableColumn } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatCurrency(n: number, currency = 'USD'): string {
+ try { return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n); }
+ catch { return `$${n.toFixed(2)}`; }
+}
+
+function formatDate(d?: string): string {
+ if (!d) return '—';
+ try { return new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); }
+ catch { return d; }
+}
+
+const TYPE_FILTERS = ['all', 'payment', 'refund', 'charge', 'subscription'] as const;
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+ const [activeFilter, setActiveFilter] = useState('all');
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'Transaction List', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (a) => {
+ a.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const transactions: any[] = data.transactions || [];
+ const currency = data.currency || 'USD';
+
+ const stats = useMemo(() => {
+ let totalRevenue = 0, refunds = 0, count = 0;
+ for (const txn of transactions) {
+ const amt = txn.amount ?? 0;
+ if ((txn.type || '').toLowerCase() === 'refund') refunds += Math.abs(amt);
+ else totalRevenue += amt;
+ count++;
+ }
+ return { totalRevenue, avgTransaction: count > 0 ? totalRevenue / count : 0, refunds };
+ }, [transactions]);
+
+ const filtered = useMemo(() => {
+ if (activeFilter === 'all') return transactions;
+ return transactions.filter((txn: any) => (txn.type || '').toLowerCase() === activeFilter);
+ }, [transactions, activeFilter]);
+
+ const rows = filtered.map((txn: any) => ({
+ id: txn.id || '',
+ amount: formatCurrency(txn.amount ?? 0, txn.currency || currency),
+ type: txn.type || '—',
+ status: txn.status || '—',
+ contact: txn.contact || txn.contactName || '—',
+ date: formatDate(txn.date),
+ paymentMethod: txn.paymentMethod || '—',
+ }));
+
+ const columns: TableColumn[] = [
+ { key: 'id', label: 'Transaction ID', sortable: true, width: '140px' },
+ { key: 'amount', label: 'Amount', sortable: true, format: 'currency' },
+ { key: 'type', label: 'Type', sortable: true },
+ { key: 'status', label: 'Status', sortable: true, format: 'status' },
+ { key: 'contact', label: 'Contact', sortable: true },
+ { key: 'date', label: 'Date', sortable: true, format: 'date' },
+ { key: 'paymentMethod', label: 'Method', sortable: true },
+ ];
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {TYPE_FILTERS.map((f) => (
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/transaction-list/index.html b/src/ui/react-app/src/apps/transaction-list/index.html
new file mode 100644
index 0000000..46c7e0b
--- /dev/null
+++ b/src/ui/react-app/src/apps/transaction-list/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Transaction List
+
+
+
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/transaction-list/main.tsx b/src/ui/react-app/src/apps/transaction-list/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/transaction-list/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/transaction-list/vite.config.ts b/src/ui/react-app/src/apps/transaction-list/vite.config.ts
new file mode 100644
index 0000000..b8df57d
--- /dev/null
+++ b/src/ui/react-app/src/apps/transaction-list/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/transaction-list'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/user-detail/App.tsx b/src/ui/react-app/src/apps/user-detail/App.tsx
new file mode 100644
index 0000000..5f7ddec
--- /dev/null
+++ b/src/ui/react-app/src/apps/user-detail/App.tsx
@@ -0,0 +1,150 @@
+import React, { useState, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { DetailHeader } from '../../components/data/DetailHeader';
+import { SplitLayout } from '../../components/layout/SplitLayout';
+import { KeyValueList } from '../../components/data/KeyValueList';
+import { TagList } from '../../components/data/TagList';
+import { Card } from '../../components/layout/Card';
+import type { KeyValueItem, StatusVariant, TagItem } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+function extractData(result: CallToolResult): any {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc;
+ for (const item of result.content || []) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text); } catch {}
+ }
+ }
+ return null;
+}
+
+function formatDate(d?: string): string {
+ if (!d) return '—';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+export function App() {
+ const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null);
+
+ const { isConnected, error } = useApp({
+ appInfo: { name: 'User Detail', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (result) => {
+ const parsed = extractData(result);
+ if (parsed) setData(parsed);
+ };
+ },
+ });
+
+ const user = useMemo(() => {
+ if (!data) return null;
+ return data.user || data;
+ }, [data]);
+
+ const profileItems: KeyValueItem[] = useMemo(() => {
+ if (!user) return [];
+ const name = `${user.firstName || ''} ${user.lastName || ''}`.trim() || user.name || '—';
+ return [
+ { label: 'Full Name', value: name, bold: true },
+ { label: 'Email', value: user.email || '—' },
+ { label: 'Phone', value: user.phone || '—' },
+ { label: 'Role', value: user.role || user.type || '—' },
+ { label: 'Status', value: user.status || (user.active !== false ? 'Active' : 'Inactive') },
+ { label: 'Joined', value: formatDate(user.createdAt) },
+ { label: 'Last Login', value: formatDate(user.lastLogin || user.lastActive) },
+ { label: 'Location', value: user.location || [user.city, user.state, user.country].filter(Boolean).join(', ') || '—' },
+ ];
+ }, [user]);
+
+ const permissions: TagItem[] = useMemo(() => {
+ if (!user) return [];
+ const perms: string[] = user.permissions || [];
+ const colorMap: Record = {
+ admin: 'red', write: 'blue', read: 'green', manage: 'purple', delete: 'red', create: 'indigo',
+ };
+ return perms.map((p: string) => {
+ const key = Object.keys(colorMap).find((k) => p.toLowerCase().includes(k));
+ return { label: p, color: (key ? colorMap[key] : 'gray') as any };
+ });
+ }, [user]);
+
+ const roles: TagItem[] = useMemo(() => {
+ if (!user) return [];
+ const roleList: string[] = user.roles || (user.role ? [user.role] : []);
+ return roleList.map((r: string) => ({ label: r, color: 'purple' as any }));
+ }, [user]);
+
+ if (error) return ;
+ if (!isConnected) return ;
+ if (!data) return ;
+
+ const displayName = `${user?.firstName || ''} ${user?.lastName || ''}`.trim() || user?.name || 'User';
+ const userRole = user?.role || user?.type || '';
+ const status = user?.status || (user?.active !== false ? 'Active' : 'Inactive');
+ const statusMap: Record = { active: 'active', inactive: 'paused', suspended: 'error', pending: 'pending' };
+ const statusVariant: StatusVariant = statusMap[status.toLowerCase()] || 'active';
+
+ const initials = displayName.split(' ').map((n: string) => n[0]).join('').slice(0, 2).toUpperCase();
+
+ return (
+
+
+
+
+ {user?.avatar ? (
+

+ ) : initials}
+
+
+
{user?.email || ''}
+
{user?.phone || ''}
+
+
+
+
+
+
+
+
+
+
+ {roles.length > 0 && (
+
+
+
+ )}
+
+
0 ? ` (${permissions.length})` : ''}`}>
+ {permissions.length > 0 ? (
+
+ ) : (
+
+
🔒
+
No permissions assigned
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/ui/react-app/src/apps/user-detail/index.html b/src/ui/react-app/src/apps/user-detail/index.html
new file mode 100644
index 0000000..ef41638
--- /dev/null
+++ b/src/ui/react-app/src/apps/user-detail/index.html
@@ -0,0 +1,5 @@
+
+
+User Detail
+
+
diff --git a/src/ui/react-app/src/apps/user-detail/main.tsx b/src/ui/react-app/src/apps/user-detail/main.tsx
new file mode 100644
index 0000000..f774ec9
--- /dev/null
+++ b/src/ui/react-app/src/apps/user-detail/main.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+createRoot(document.getElementById('root')!).render();
diff --git a/src/ui/react-app/src/apps/user-detail/vite.config.ts b/src/ui/react-app/src/apps/user-detail/vite.config.ts
new file mode 100644
index 0000000..3a543fd
--- /dev/null
+++ b/src/ui/react-app/src/apps/user-detail/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/user-detail'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/workflow-detail/App.tsx b/src/ui/react-app/src/apps/workflow-detail/App.tsx
new file mode 100644
index 0000000..5ba66dc
--- /dev/null
+++ b/src/ui/react-app/src/apps/workflow-detail/App.tsx
@@ -0,0 +1,298 @@
+/**
+ * workflow-detail — Single workflow flow diagram with metadata.
+ * Shows FlowDiagram of trigger → actions → conditions → end, plus metadata.
+ */
+import React, { useState, useEffect, useMemo, useCallback } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { MCPAppProvider } from '../../context/MCPAppContext';
+import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
+import { DetailHeader } from '../../components/data/DetailHeader';
+import { FlowDiagram } from '../../components/viz/FlowDiagram';
+import { KeyValueList } from '../../components/data/KeyValueList';
+import { Card } from '../../components/layout/Card';
+import type { FlowNode, FlowEdge, StatusVariant } from '../../types';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+// ─── Types ──────────────────────────────────────────────────
+
+interface WorkflowStep {
+ id?: string;
+ name?: string;
+ type?: string;
+ description?: string;
+ nextSteps?: string[];
+ condition?: string;
+}
+
+interface WorkflowDetail {
+ id?: string;
+ name?: string;
+ status?: string;
+ createdAt?: string;
+ updatedAt?: string;
+ totalExecutions?: number;
+ lastRun?: string;
+ trigger?: string | { type?: string; name?: string; description?: string };
+ steps?: WorkflowStep[];
+ nodes?: FlowNode[];
+ edges?: FlowEdge[];
+ description?: string;
+}
+
+interface WorkflowData {
+ workflow: WorkflowDetail;
+}
+
+// ─── Constants ──────────────────────────────────────────────
+
+function formatDate(d?: string): string {
+ if (!d) return '—';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+const STATUS_MAP: Record = {
+ active: 'active',
+ draft: 'draft',
+ inactive: 'paused',
+ paused: 'paused',
+ error: 'error',
+};
+
+// ─── Extract data from tool result ──────────────────────────
+
+function extractData(result: CallToolResult): WorkflowData | null {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc as WorkflowData;
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text) as WorkflowData; } catch { /* skip */ }
+ }
+ }
+ }
+ return null;
+}
+
+// ─── Build flow diagram from workflow steps ─────────────────
+
+function buildFlowFromSteps(workflow: WorkflowDetail): { nodes: FlowNode[]; edges: FlowEdge[] } {
+ // If pre-built nodes/edges exist, use them
+ if (workflow.nodes && workflow.nodes.length > 0) {
+ return { nodes: workflow.nodes, edges: workflow.edges || [] };
+ }
+
+ const nodes: FlowNode[] = [];
+ const edges: FlowEdge[] = [];
+
+ // Add trigger node
+ const triggerName = typeof workflow.trigger === 'string'
+ ? workflow.trigger
+ : workflow.trigger?.name || workflow.trigger?.type || 'Trigger';
+ const triggerDesc = typeof workflow.trigger === 'object'
+ ? workflow.trigger.description
+ : undefined;
+
+ nodes.push({
+ id: 'trigger',
+ label: triggerName,
+ description: triggerDesc,
+ type: 'start',
+ });
+
+ // Add step nodes
+ const steps = workflow.steps || [];
+ for (let i = 0; i < steps.length; i++) {
+ const step = steps[i];
+ const nodeId = step.id || `step-${i}`;
+ const isCondition = step.type === 'condition' || step.type === 'if' || !!step.condition;
+
+ nodes.push({
+ id: nodeId,
+ label: step.name || `Step ${i + 1}`,
+ description: step.description || step.condition,
+ type: isCondition ? 'condition' : 'action',
+ });
+
+ // Edge from previous node
+ if (i === 0) {
+ edges.push({ from: 'trigger', to: nodeId });
+ } else {
+ const prevId = steps[i - 1].id || `step-${i - 1}`;
+ edges.push({ from: prevId, to: nodeId });
+ }
+
+ // Handle explicit nextSteps
+ if (step.nextSteps) {
+ for (const nextId of step.nextSteps) {
+ edges.push({ from: nodeId, to: nextId, label: isCondition ? 'Yes' : undefined });
+ }
+ }
+ }
+
+ // Add end node
+ if (steps.length > 0) {
+ nodes.push({ id: 'end', label: 'End', type: 'end' });
+ const lastStep = steps[steps.length - 1];
+ const lastId = lastStep.id || `step-${steps.length - 1}`;
+ // Only add edge if last step doesn't have explicit nextSteps
+ if (!lastStep.nextSteps || lastStep.nextSteps.length === 0) {
+ edges.push({ from: lastId, to: 'end' });
+ }
+ } else {
+ // No steps — trigger directly to end
+ nodes.push({ id: 'end', label: 'End', type: 'end' });
+ edges.push({ from: 'trigger', to: 'end' });
+ }
+
+ return { nodes, edges };
+}
+
+// ─── App ────────────────────────────────────────────────────
+
+export function App() {
+ const [data, setData] = useState(null);
+
+ useEffect(() => {
+ const d = (window as any).__MCP_APP_DATA__;
+ if (d && !data) setData(d as WorkflowData);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: 'workflow-detail', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (createdApp) => {
+ createdApp.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ if (error) {
+ return Connection Error
{error.message}
;
+ }
+ if (!isConnected || !app) {
+ return ;
+ }
+ if (!data) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+// ─── View ───────────────────────────────────────────────────
+
+function WorkflowDetailView({ workflow, app }: { workflow: WorkflowDetail; app: any }) {
+ const w = workflow;
+ const statusVariant = STATUS_MAP[w.status?.toLowerCase() || ''] || 'draft';
+ const [actionResult, setActionResult] = useState<{ type: 'success' | 'error'; msg: string } | null>(null);
+ const [isActing, setIsActing] = useState(false);
+
+ const handleWorkflowAction = useCallback(async (action: string, actionData: Record) => {
+ if (!app) return;
+ setIsActing(true);
+ setActionResult(null);
+ try {
+ await app.updateModelContext({
+ content: [{
+ type: 'text',
+ text: JSON.stringify({ action, data: { workflowId: w.id, ...actionData } }),
+ }],
+ });
+ setActionResult({ type: 'success', msg: `✓ ${action.replace('_', ' ')} request sent` });
+ setTimeout(() => setActionResult(null), 3000);
+ } catch {
+ setActionResult({ type: 'error', msg: '✗ Failed to send request' });
+ } finally {
+ setIsActing(false);
+ }
+ }, [app, w.id]);
+
+ // Build flow diagram
+ const { nodes, edges } = useMemo(() => buildFlowFromSteps(w), [w]);
+
+ // Metadata
+ const metadataItems = [
+ ...(w.status ? [{ label: 'Status', value: w.status.charAt(0).toUpperCase() + w.status.slice(1) }] : []),
+ ...(w.createdAt ? [{ label: 'Created', value: formatDate(w.createdAt) }] : []),
+ ...(w.updatedAt ? [{ label: 'Updated', value: formatDate(w.updatedAt) }] : []),
+ ...(w.totalExecutions !== undefined ? [{ label: 'Total Executions', value: w.totalExecutions.toLocaleString(), bold: true as const }] : []),
+ ...(w.lastRun ? [{ label: 'Last Run', value: formatDate(w.lastRun) }] : []),
+ ...(w.id ? [{ label: 'Workflow ID', value: w.id, variant: 'muted' as const }] : []),
+ ];
+
+ return (
+ <>
+
+
+
+
+
+
+ {metadataItems.length > 0 && (
+
+
+
+ )}
+
+ {/* Workflow Actions */}
+
+ {actionResult && (
+
+ {actionResult.msg}
+
+ )}
+
+ {w.status?.toLowerCase() === 'active' ? (
+
+ ) : (
+
+ )}
+
+ >
+ );
+}
diff --git a/src/ui/react-app/src/apps/workflow-detail/index.html b/src/ui/react-app/src/apps/workflow-detail/index.html
new file mode 100644
index 0000000..60e2a96
--- /dev/null
+++ b/src/ui/react-app/src/apps/workflow-detail/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/workflow-detail/main.tsx b/src/ui/react-app/src/apps/workflow-detail/main.tsx
new file mode 100644
index 0000000..fbe1cf6
--- /dev/null
+++ b/src/ui/react-app/src/apps/workflow-detail/main.tsx
@@ -0,0 +1,9 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.js';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/src/ui/react-app/src/apps/workflow-detail/vite.config.ts b/src/ui/react-app/src/apps/workflow-detail/vite.config.ts
new file mode 100644
index 0000000..6844e1e
--- /dev/null
+++ b/src/ui/react-app/src/apps/workflow-detail/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/workflow-detail'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/apps/workflow-status/App.tsx b/src/ui/react-app/src/apps/workflow-status/App.tsx
new file mode 100644
index 0000000..1629989
--- /dev/null
+++ b/src/ui/react-app/src/apps/workflow-status/App.tsx
@@ -0,0 +1,193 @@
+/**
+ * workflow-status — All workflows list with stats and filters.
+ * Shows workflow table with name, status, triggers, lastRun. Stats for total/active/draft.
+ */
+import React, { useState, useEffect, useMemo } from 'react';
+import { useApp } from '@modelcontextprotocol/ext-apps/react';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import { MCPAppProvider } from '../../context/MCPAppContext';
+import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
+import { PageHeader } from '../../components/layout/PageHeader';
+import { StatsGrid } from '../../components/layout/StatsGrid';
+import { MetricCard } from '../../components/data/MetricCard';
+import { DataTable } from '../../components/data/DataTable';
+import { StatusBadge } from '../../components/data/StatusBadge';
+import { FilterChips } from '../../components/shared/FilterChips';
+import '../../styles/base.css';
+import '../../styles/interactive.css';
+
+// ─── Types ──────────────────────────────────────────────────
+
+interface Workflow {
+ id?: string;
+ name?: string;
+ status?: string;
+ triggers?: string | string[];
+ lastRun?: string;
+ totalExecutions?: number;
+ createdAt?: string;
+ updatedAt?: string;
+}
+
+interface WorkflowData {
+ workflows: Workflow[];
+}
+
+// ─── Helpers ────────────────────────────────────────────────
+
+function formatDate(d?: string): string {
+ if (!d) return '—';
+ try {
+ return new Date(d).toLocaleDateString('en-US', {
+ month: 'short', day: 'numeric', year: 'numeric',
+ });
+ } catch { return d; }
+}
+
+// ─── Extract data from tool result ──────────────────────────
+
+function extractData(result: CallToolResult): WorkflowData | null {
+ const sc = (result as any).structuredContent;
+ if (sc) return sc as WorkflowData;
+ if (result.content) {
+ for (const item of result.content) {
+ if (item.type === 'text') {
+ try { return JSON.parse(item.text) as WorkflowData; } catch { /* skip */ }
+ }
+ }
+ }
+ return null;
+}
+
+// ─── App ────────────────────────────────────────────────────
+
+export function App() {
+ const [data, setData] = useState(null);
+
+ useEffect(() => {
+ const d = (window as any).__MCP_APP_DATA__;
+ if (d && !data) setData(d as WorkflowData);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const { app, isConnected, error } = useApp({
+ appInfo: { name: 'workflow-status', version: '1.0.0' },
+ capabilities: {},
+ onAppCreated: (createdApp) => {
+ createdApp.ontoolresult = async (result) => {
+ const d = extractData(result);
+ if (d) setData(d);
+ };
+ },
+ });
+
+ if (error) {
+ return Connection Error
{error.message}
;
+ }
+ if (!isConnected || !app) {
+ return ;
+ }
+ if (!data) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+// ─── View ───────────────────────────────────────────────────
+
+function WorkflowStatusView({ workflows }: { workflows: Workflow[] }) {
+ const [statusFilter, setStatusFilter] = useState>(new Set());
+
+ // Stats
+ const totalCount = workflows.length;
+ const activeCount = workflows.filter(w => w.status === 'active').length;
+ const draftCount = workflows.filter(w => w.status === 'draft').length;
+ const inactiveCount = workflows.filter(w => w.status === 'inactive').length;
+
+ // Unique statuses for filter chips
+ const uniqueStatuses = useMemo(() => {
+ const set = new Set();
+ workflows.forEach(w => { if (w.status) set.add(w.status); });
+ return Array.from(set);
+ }, [workflows]);
+
+ // Filtered workflows
+ const filteredWorkflows = useMemo(() => {
+ if (statusFilter.size === 0) return workflows;
+ return workflows.filter(w => w.status && statusFilter.has(w.status));
+ }, [workflows, statusFilter]);
+
+ // Table
+ const columns = useMemo(() => [
+ { key: 'name', label: 'Workflow', sortable: true },
+ { key: 'status', label: 'Status', format: 'status' as const, sortable: true },
+ { key: 'triggers', label: 'Triggers', sortable: false },
+ { key: 'lastRun', label: 'Last Run', format: 'date' as const, sortable: true },
+ { key: 'executions', label: 'Executions', sortable: true },
+ ], []);
+
+ const rows = filteredWorkflows.map((w, i) => {
+ const triggers = Array.isArray(w.triggers) ? w.triggers.join(', ') : (w.triggers || '—');
+ return {
+ id: w.id || String(i),
+ name: w.name || 'Untitled Workflow',
+ status: w.status || 'draft',
+ triggers,
+ lastRun: w.lastRun ? formatDate(w.lastRun) : 'Never',
+ executions: w.totalExecutions?.toLocaleString() || '0',
+ };
+ });
+
+ return (
+ <>
+
+
+
+
+
+
+ {inactiveCount > 0 && (
+
+ )}
+
+
+ {uniqueStatuses.length > 1 && (
+
+ {uniqueStatuses.map(s => (
+
+ ))}
+
+ )}
+
+
+ >
+ );
+}
diff --git a/src/ui/react-app/src/apps/workflow-status/index.html b/src/ui/react-app/src/apps/workflow-status/index.html
new file mode 100644
index 0000000..60e2a96
--- /dev/null
+++ b/src/ui/react-app/src/apps/workflow-status/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/ui/react-app/src/apps/workflow-status/main.tsx b/src/ui/react-app/src/apps/workflow-status/main.tsx
new file mode 100644
index 0000000..fbe1cf6
--- /dev/null
+++ b/src/ui/react-app/src/apps/workflow-status/main.tsx
@@ -0,0 +1,9 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.js';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/src/ui/react-app/src/apps/workflow-status/vite.config.ts b/src/ui/react-app/src/apps/workflow-status/vite.config.ts
new file mode 100644
index 0000000..d5727e6
--- /dev/null
+++ b/src/ui/react-app/src/apps/workflow-status/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ root: __dirname,
+ build: {
+ outDir: path.resolve(__dirname, '../../../../dist/apps/workflow-status'),
+ emptyOutDir: false,
+ rollupOptions: { input: path.resolve(__dirname, 'index.html') },
+ },
+ resolve: {
+ alias: {
+ '@components': path.resolve(__dirname, '../../components'),
+ '@hooks': path.resolve(__dirname, '../../hooks'),
+ '@styles': path.resolve(__dirname, '../../styles'),
+ '@context': path.resolve(__dirname, '../../context'),
+ },
+ },
+});
diff --git a/src/ui/react-app/src/components/charts/BarChart.tsx b/src/ui/react-app/src/components/charts/BarChart.tsx
new file mode 100644
index 0000000..26c4434
--- /dev/null
+++ b/src/ui/react-app/src/components/charts/BarChart.tsx
@@ -0,0 +1,113 @@
+import React from 'react';
+import type { BarChartProps } from '../../types.js';
+
+const COLORS = ['#4f46e5','#7c3aed','#16a34a','#3b82f6','#eab308','#ef4444','#ec4899','#f97316'];
+
+export const BarChart: React.FC = ({
+ bars = [],
+ orientation = 'vertical',
+ maxValue,
+ showValues = true,
+ title,
+}) => {
+ const max = maxValue || Math.max(...bars.map(b => b.value), 1);
+
+ if (orientation === 'horizontal') {
+ return (
+
+ {title &&
{title}
}
+
+ {bars.map((b, i) => {
+ const pct = Math.min(100, (b.value / max) * 100);
+ const color = b.color || COLORS[i % COLORS.length];
+ return (
+
+
{b.label}
+
+ {showValues && (
+
+ {Number(b.value).toLocaleString()}
+
+ )}
+
+ );
+ })}
+
+
+ );
+ }
+
+ // Vertical bars via SVG
+ const svgW = Math.max(bars.length * 60, 200);
+ const svgH = 180;
+ const padTop = 10;
+ const padBot = 30;
+ const barW = Math.min(36, (svgW / bars.length) * 0.6);
+ const gap = svgW / bars.length;
+ const plotH = svgH - padTop - padBot;
+
+ return (
+
+ {title &&
{title}
}
+
+
+
+
+ );
+};
diff --git a/src/ui/react-app/src/components/charts/FunnelChart.tsx b/src/ui/react-app/src/components/charts/FunnelChart.tsx
new file mode 100644
index 0000000..c6bf4f8
--- /dev/null
+++ b/src/ui/react-app/src/components/charts/FunnelChart.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import type { FunnelChartProps } from '../../types.js';
+
+const COLORS = ['#4f46e5','#7c3aed','#16a34a','#3b82f6','#eab308','#ef4444','#ec4899','#f97316'];
+
+export const FunnelChart: React.FC = ({
+ stages = [],
+ showDropoff = true,
+ title,
+}) => {
+ if (stages.length === 0) {
+ return No data
;
+ }
+
+ const maxVal = stages[0]?.value || 1;
+
+ return (
+
+ {title &&
{title}
}
+
+ {stages.map((s, i) => {
+ const pct = Math.max(20, (s.value / maxVal) * 100);
+ const color = s.color || COLORS[i % COLORS.length];
+ const dropoff =
+ i > 0
+ ? (
+ ((stages[i - 1].value - s.value) / stages[i - 1].value) *
+ 100
+ ).toFixed(1)
+ : null;
+ return (
+
+
+ {s.label}
+
+ {Number(s.value).toLocaleString()}
+
+
+
+ {showDropoff && dropoff !== null ? (
+
-{dropoff}%
+ ) : (
+
+ )}
+
+ );
+ })}
+
+
+ );
+};
diff --git a/src/ui/react-app/src/components/charts/LineChart.tsx b/src/ui/react-app/src/components/charts/LineChart.tsx
new file mode 100644
index 0000000..6f25aee
--- /dev/null
+++ b/src/ui/react-app/src/components/charts/LineChart.tsx
@@ -0,0 +1,136 @@
+import React from 'react';
+import type { LineChartProps } from '../../types.js';
+
+export const LineChart: React.FC = ({
+ points = [],
+ color = '#4f46e5',
+ showPoints = true,
+ showArea = false,
+ title,
+ yAxisLabel,
+}) => {
+ if (points.length === 0) {
+ return No data
;
+ }
+
+ const vals = points.map(p => p.value);
+ const minV = Math.min(...vals);
+ const maxV = Math.max(...vals);
+ const range = maxV - minV || 1;
+
+ const svgW = Math.max(points.length * 60, 200);
+ const svgH = 180;
+ const padL = 40;
+ const padR = 10;
+ const padT = 16;
+ const padB = 30;
+ const plotW = svgW - padL - padR;
+ const plotH = svgH - padT - padB;
+
+ const pts = points.map((p, i) => {
+ const x = padL + (plotW / Math.max(points.length - 1, 1)) * i;
+ const y = padT + plotH - ((p.value - minV) / range) * plotH;
+ return { x, y, label: p.label, value: p.value };
+ });
+
+ const linePath = pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ');
+ const areaPath = showArea
+ ? `${linePath} L${pts[pts.length - 1].x},${padT + plotH} L${pts[0].x},${padT + plotH} Z`
+ : '';
+
+ // Y-axis ticks (5 ticks)
+ const ticks = Array.from({ length: 5 }, (_, i) => {
+ const val = minV + (range * i) / 4;
+ const y = padT + plotH - (plotH * i) / 4;
+ return { val, y };
+ });
+
+ return (
+
+ {title &&
{title}
}
+
+
+
+
+ );
+};
diff --git a/src/ui/react-app/src/components/charts/PieChart.tsx b/src/ui/react-app/src/components/charts/PieChart.tsx
new file mode 100644
index 0000000..239cb92
--- /dev/null
+++ b/src/ui/react-app/src/components/charts/PieChart.tsx
@@ -0,0 +1,93 @@
+import React from 'react';
+import type { PieChartProps } from '../../types.js';
+
+const COLORS = ['#4f46e5','#7c3aed','#16a34a','#3b82f6','#eab308','#ef4444','#ec4899','#f97316'];
+
+export const PieChart: React.FC = ({
+ segments = [],
+ donut = false,
+ title,
+ showLegend = true,
+}) => {
+ const total = segments.reduce((s, seg) => s + seg.value, 0) || 1;
+ const r = 70;
+ const cx = 90;
+ const cy = 90;
+ const svgSize = 180;
+
+ let cumAngle = -Math.PI / 2; // start at 12 o'clock
+
+ const arcs = segments.map((seg, i) => {
+ const frac = seg.value / total;
+ const angle = frac * Math.PI * 2;
+ const startAngle = cumAngle;
+ const endAngle = cumAngle + angle;
+ cumAngle = endAngle;
+
+ const x1 = cx + r * Math.cos(startAngle);
+ const y1 = cy + r * Math.sin(startAngle);
+ const x2 = cx + r * Math.cos(endAngle);
+ const y2 = cy + r * Math.sin(endAngle);
+ const largeArc = angle > Math.PI ? 1 : 0;
+ const color = seg.color || COLORS[i % COLORS.length];
+
+ // Single full segment
+ if (frac >= 0.9999) {
+ return ;
+ }
+
+ return (
+
+ );
+ });
+
+ return (
+
+ {title &&
{title}
}
+
+
+
+ {showLegend && (
+
+ {segments.map((seg, i) => {
+ const color = seg.color || COLORS[i % COLORS.length];
+ const pct = ((seg.value / total) * 100).toFixed(1);
+ return (
+
+
+ {seg.label}
+ {pct}%
+
+ );
+ })}
+
+ )}
+
+
+ );
+};
diff --git a/src/ui/react-app/src/components/charts/SparklineChart.tsx b/src/ui/react-app/src/components/charts/SparklineChart.tsx
new file mode 100644
index 0000000..ebd6b37
--- /dev/null
+++ b/src/ui/react-app/src/components/charts/SparklineChart.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import type { SparklineChartProps } from '../../types.js';
+
+export const SparklineChart: React.FC = ({
+ values = [],
+ color = '#4f46e5',
+ height = 24,
+ width = 80,
+}) => {
+ if (values.length < 2) {
+ return (
+
+ —
+
+ );
+ }
+
+ const minV = Math.min(...values);
+ const maxV = Math.max(...values);
+ const range = maxV - minV || 1;
+ const pad = 2;
+
+ const pts = values
+ .map((v, i) => {
+ const x = pad + ((width - pad * 2) / (values.length - 1)) * i;
+ const y =
+ pad + (height - pad * 2) - ((v - minV) / range) * (height - pad * 2);
+ return `${x},${y}`;
+ })
+ .join(' ');
+
+ return (
+
+ );
+};
diff --git a/src/ui/react-app/src/components/comms/ChatThread.tsx b/src/ui/react-app/src/components/comms/ChatThread.tsx
new file mode 100644
index 0000000..7783c6b
--- /dev/null
+++ b/src/ui/react-app/src/components/comms/ChatThread.tsx
@@ -0,0 +1,122 @@
+import React, { useEffect, useRef } from 'react';
+import type { ChatThreadProps, ChatMessage } from '../../types.js';
+
+const chatTypeIcons: Record = {
+ sms: '💬',
+ email: '📧',
+ call: '📞',
+ whatsapp: '📱',
+};
+
+const avatarColors = ['#4f46e5', '#7c3aed', '#059669', '#d97706', '#dc2626', '#0891b2'];
+
+function getAvatarColor(name: string): string {
+ return avatarColors[(name || '').charCodeAt(0) % avatarColors.length];
+}
+
+function getInitials(name: string): string {
+ return (name || '')
+ .split(' ')
+ .map(n => n[0])
+ .join('')
+ .slice(0, 2)
+ .toUpperCase();
+}
+
+export const ChatThread: React.FC = ({
+ messages = [],
+ title,
+}) => {
+ const bodyRef = useRef(null);
+
+ // Auto-scroll to bottom on mount / messages change
+ useEffect(() => {
+ if (bodyRef.current) {
+ bodyRef.current.scrollTop = bodyRef.current.scrollHeight;
+ }
+ }, [messages]);
+
+ if (messages.length === 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {title && (
+
+
{title}
+ {messages.length} messages
+
+ )}
+
+ {messages.map((msg: ChatMessage, i: number) => {
+ const isOutbound = msg.direction === 'outbound';
+ const typeIcon = chatTypeIcons[msg.type || 'sms'] || '💬';
+ const avatarBg = isOutbound
+ ? '#4f46e5'
+ : getAvatarColor(msg.senderName || 'U');
+ const initials = getInitials(
+ msg.senderName || (isOutbound ? 'You' : 'Contact'),
+ );
+
+ return (
+
+ {!isOutbound && (
+
+ {msg.avatar ? (
+

+ ) : (
+ initials
+ )}
+
+ )}
+
+ {msg.senderName && (
+
{msg.senderName}
+ )}
+
+ {msg.content}
+
+
+ {typeIcon} {msg.timestamp || ''}
+
+
+ {isOutbound && (
+
+ {msg.avatar ? (
+

+ ) : (
+ initials
+ )}
+
+ )}
+
+ );
+ })}
+
+
+ );
+};
diff --git a/src/ui/react-app/src/components/comms/ContentPreview.tsx b/src/ui/react-app/src/components/comms/ContentPreview.tsx
new file mode 100644
index 0000000..a88a552
--- /dev/null
+++ b/src/ui/react-app/src/components/comms/ContentPreview.tsx
@@ -0,0 +1,66 @@
+import React, { useMemo } from 'react';
+import type { ContentPreviewProps } from '../../types.js';
+
+function sanitizeHtml(html: string): string {
+ return String(html)
+ .replace(/