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)}
` : ''} +
+ + + ${barsSvg} + +
+
`; +}; + +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)}
` : ''} +
+ + ${ticks.map(t => ` + + ${Math.round(t.val).toLocaleString()} + `).join('')} + ${yAxisLabel ? `${esc(yAxisLabel)}` : ''} + ${showArea ? `` : ''} + + ${showPoints ? pts.map(p => ``).join('') : ''} + ${pts.map((p, i) => `${esc(points[i].label)}`).join('')} + +
+
`; +}; + +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)}
` : ''} +
+ + ${arcs.join('')} + ${donutHole} + ${donut ? `${total.toLocaleString()}` : ''} + + ${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 ` +
+
+
+

${esc(title)}

+ ${subtitle ? `

${esc(subtitle)}

` : ''} +
+ ${status ? `${esc(status)}` : ''} +
+ ${stats && stats.length > 0 ? ` +
+ ${stats.map((s: any) => ` + ${esc(s.value)} ${esc(s.label)} + `).join('')} +
` : ''} + ${children} +
`; + } + + 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 ? `

${esc(title)}

${subtitle ? `

${esc(subtitle)}

` : ''}
` : ''} +
${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 ? `

${esc(title)}

${description ? `

${esc(description)}

` : ''}
` : ''} + ${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 ` +
+
+ + + + ${selectable ? '' : ''} + ${columns.map((col: any) => ` + `).join('')} + + + + ${displayRows.map((row: any) => ` + + ${selectable ? '' : ''} + ${columns.map((col: any) => ``).join('')} + `).join('')} + +
+ ${esc(col.label)} +
${formatCell(row[col.key], col.format)}
+
+ ${totalPages > 1 ? ` +
+ 1–${displayRows.length} of ${rows.length} +
+ + +
+
` : ''} +
`; +}; + +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) => ` +
+
+
+ ${esc(col.title)} + ${col.count ?? col.cards?.length ?? 0} +
+ ${col.totalValue ? `
${esc(col.totalValue)}
` : ''} +
+
+ ${(!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 ` +
+
+ ${esc(label)} + ${Number(value).toLocaleString()}${max !== 100 ? ` / ${Number(max).toLocaleString()}` : ''}${showPercent ? ` (${pct.toFixed(1)}%)` : ''} +
+
+
+ ${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 ` +
+
+
+

${esc(title)}

+ ${entityId ? `

${esc(entityId)}

` : ''} + ${subtitle ? `

${esc(subtitle)}

` : ''} +
+ ${status ? `${esc(status)}` : ''} +
+ ${children} +
`; +}; + +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 ` +
+ + + + + + + + + + + ${items.map((item: any) => ` + + + + + + `).join('')} + +
ItemQtyPriceTotal
${esc(item.name)}
${item.description ? `
${esc(item.description)}
` : ''}
${item.quantity}${fmtCurrency(item.unitPrice, currency)}${fmtCurrency(item.total, currency)}
+
`; +}; + +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 `
${esc(name)}
`; + } + 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 `
💬

No messages

`; + } + return ` +
+ ${title ? `

${esc(title)}

${messages.length} messages
` : ''} +
+ ${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(//gi, '') + .replace(//gi, '') + .replace(/on\w+\s*=\s*"[^"]*"/gi, '') + .replace(/on\w+\s*=\s*'[^']*'/gi, ''); + return ` + `; +}; + +const ContentPreview: ComponentFn = (props) => { + const { content = '', format = 'html', maxHeight, title } = props; + let rendered: string; + if (format === 'html') { + rendered = String(content) + .replace(//gi, '') + .replace(//gi, '') + .replace(/on\w+\s*=\s*"[^"]*"/gi, '') + .replace(/on\w+\s*=\s*'[^']*'/gi, ''); + } else if (format === 'markdown') { + rendered = esc(content) + .replace(/^### (.+)$/gm, '

$1

') + .replace(/^## (.+)$/gm, '

$1

') + .replace(/^# (.+)$/gm, '

$1

') + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/\*(.+?)\*/g, '$1') + .replace(/\[(.+?)\]\((.+?)\)/g, '$1') + .replace(/\n/g, '
'); + } else { + rendered = `
${esc(content)}
`; + } + const heightStyle = maxHeight ? `max-height:${maxHeight}px;overflow-y:auto;` : ''; + return ` +
+ ${title ? `

${esc(title)}

` : ''} +
${rendered}
+
`; +}; + +const speakerRoleColors: Record = { + agent: '#4f46e5', customer: '#059669', system: '#6b7280', +}; +const speakerRoleLabels: Record = { + agent: 'Agent', customer: 'Customer', system: 'System', +}; + +const TranscriptView: ComponentFn = (props) => { + const { entries = [], title, duration } = props; + if (entries.length === 0) { + return `
📝

No transcript available

`; + } + return ` +
+
+
${title ? `

${esc(title)}

` : ''}
+
+ ${duration ? `⏱ ${esc(duration)}` : ''} + ${entries.length} entries +
+
+
+ ${entries.map((e: any) => { + const roleColor = speakerRoleColors[e.speakerRole || 'customer'] || '#6b7280'; + const roleLabel = speakerRoleLabels[e.speakerRole || ''] || ''; + return ` +
+
${esc(e.timestamp)}
+
+
+ + ${esc(e.speaker)} + ${roleLabel ? `${roleLabel}` : ''} +
+
${esc(e.text)}
+
+
`; + }).join('')} +
+
`; +}; + +const AudioPlayer: ComponentFn = (props) => { + const { title, duration, type = 'recording' } = props; + const typeIcon = type === 'voicemail' ? '📩' : '🎙'; + const typeLabel = type === 'voicemail' ? 'Voicemail' : 'Recording'; + const bars = Array.from({ length: 32 }, (_, i) => { + // Deterministic pseudo-wave based on index + const base = Math.sin(i * 0.4) * 30 + 50; + const jitter = ((i * 7 + 13) % 19) * 2; + return Math.min(95, Math.max(15, Math.round(base + jitter))); + }); + return ` +
+
+ ${typeIcon} +
+
${esc(title || typeLabel)}
+
${typeLabel}
+
+
+
+ +
+ ${bars.map((h, i) => `
`).join('')} +
+ ${esc(duration || '0:00')} +
+
`; +}; + +const priorityColors: Record = { + low: '#6b7280', medium: '#d97706', high: '#dc2626', +}; +const priorityLabels: Record = { + low: 'Low', medium: 'Med', high: 'High', +}; + +const ChecklistView: ComponentFn = (props) => { + const { items = [], title, showProgress } = props; + const completedCount = items.filter((i: any) => i.completed).length; + const totalCount = items.length; + const pct = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0; + return ` +
+ ${title || showProgress ? ` +
+ ${title ? `

${esc(title)}

` : ''} + ${showProgress ? ` +
+ ${completedCount}/${totalCount} done +
+
` : ''} +
` : ''} +
+ ${items.length === 0 ? '

No tasks

' : + items.map((item: any) => { + const prColor = priorityColors[item.priority || 'low'] || '#6b7280'; + const prLabel = priorityLabels[item.priority || ''] || ''; + return ` +
+
+ ${item.completed ? '' : ''} +
+
+
${esc(item.title)}
+
+ ${item.dueDate ? `📅 ${esc(item.dueDate)}` : ''} + ${item.assignee ? `👤 ${esc(item.assignee)}` : ''} + ${prLabel ? `${prLabel}` : ''} +
+
+
`; + }).join('')} +
+
`; +}; + +// ─── Visualization & Utility Components ───────────────────── + +const CalendarView: ComponentFn = (props) => { + const { title, events = [], highlightToday = true } = props; + const now = new Date(); + const year = props.year ?? now.getFullYear(); + const month = props.month ?? (now.getMonth() + 1); + const monthIdx = month - 1; + const firstDay = new Date(year, monthIdx, 1).getDay(); + const daysInMonth = new Date(year, monthIdx + 1, 0).getDate(); + const todayDate = now.getDate(); + const isCurrentMonth = year === now.getFullYear() && monthIdx === now.getMonth(); + const monthNames = ['January','February','March','April','May','June','July','August','September','October','November','December']; + const dayNames = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; + + const eventsByDay: Record = {}; + for (const evt of events) { + try { + const d = new Date(evt.date); + if (d.getFullYear() === year && d.getMonth() === monthIdx) { + const day = d.getDate(); + (eventsByDay[day] = eventsByDay[day] || []).push(evt); + } + } catch { /* skip */ } + } + + const defaultColors: Record = { + meeting: '#4f46e5', call: '#059669', task: '#d97706', deadline: '#dc2626', event: '#7c3aed', + }; + + let cells = ''; + for (let i = 0; i < firstDay; i++) { + cells += '
'; + } + for (let d = 1; d <= daysInMonth; d++) { + const isToday = highlightToday && isCurrentMonth && d === todayDate; + const dayEvents = eventsByDay[d] || []; + const evtHtml = dayEvents.slice(0, 3).map((e: any) => { + const color = e.color || defaultColors[e.type || 'event'] || '#4f46e5'; + return `
${esc(e.title)}
`; + }).join(''); + const more = dayEvents.length > 3 ? `
+${dayEvents.length - 3} more
` : ''; + cells += `
${d}
${evtHtml}${more}
`; + } + + return ` +
+ ${title ? `
${esc(title)}
` : ''} +
${esc(monthNames[monthIdx])} ${year}
+
+ ${dayNames.map(dn => `
${dn}
`).join('')} + ${cells} +
+
`; +}; + +const FlowDiagram: ComponentFn = (props) => { + const { nodes = [], edges = [], direction = 'horizontal', title } = props; + if (nodes.length === 0) return '
🔗

No flow nodes

'; + + const nodeMap: Record = {}; + for (const n of nodes) nodeMap[n.id] = n; + + const visited = new Set(); + const ordered: string[] = []; + const starts = nodes.filter((n: any) => n.type === 'start'); + if (starts.length === 0 && nodes.length > 0) starts.push(nodes[0]); + + function walk(id: string) { + if (visited.has(id)) return; + visited.add(id); + ordered.push(id); + for (const e of edges) { + if (e.from === id) walk(e.to); + } + } + starts.forEach((s: any) => walk(s.id)); + for (const n of nodes) { if (!visited.has(n.id)) ordered.push(n.id); } + + const typeStyles: Record = { + start: 'flow-node-start', action: 'flow-node-action', + condition: 'flow-node-condition', end: 'flow-node-end', + }; + + const edgeLabelMap: Record = {}; + for (const e of edges) { if (e.label) edgeLabelMap[`${e.from}-${e.to}`] = e.label; } + + const isVert = direction === 'vertical'; + let html = ''; + + for (let i = 0; i < ordered.length; i++) { + const n = nodeMap[ordered[i]]; + if (!n) continue; + const typeCls = typeStyles[n.type || 'action'] || 'flow-node-action'; + const isCondition = n.type === 'condition'; + + html += `
+
${esc(n.label)}
+ ${n.description ? `
${esc(n.description)}
` : ''} +
`; + + if (i < ordered.length - 1) { + const edgeKey = `${ordered[i]}-${ordered[i + 1]}`; + const lbl = edgeLabelMap[edgeKey]; + html += `
+ ${isVert ? '↓' : '→'} + ${lbl ? `${esc(lbl)}` : ''} +
`; + } + } + + return ` +
+ ${title ? `
${esc(title)}
` : ''} +
${html}
+
`; +}; + +const TreeView: ComponentFn = (props) => { + const { nodes = [], title, expandAll = false } = props; + + function renderNode(node: any, depth: number): string { + const hasChildren = node.children && node.children.length > 0; + const isExpanded = expandAll || node.expanded !== false; + const chevron = hasChildren ? (isExpanded ? '▾' : '▸') : ''; + const indent = depth * 20; + const badge = node.badge ? `${esc(node.badge)}` : ''; + const icon = node.icon ? `${esc(node.icon)}` : ''; + + let childrenHtml = ''; + if (hasChildren && isExpanded) { + childrenHtml = node.children.map((c: any) => renderNode(c, depth + 1)).join(''); + } + + return ` +
+ ${chevron} + ${icon} + ${esc(node.label)} + ${badge} +
+ ${childrenHtml}`; + } + + return ` +
+ ${title ? `
${esc(title)}
` : ''} +
${nodes.map((n: any) => renderNode(n, 0)).join('')}
+
`; +}; + +const MediaGallery: ComponentFn = (props) => { + const { items = [], columns = 3, title } = props; + if (items.length === 0) return '
🖼️

No media items

'; + + const cards = items.map((item: any) => { + const thumb = item.thumbnailUrl || item.url || ''; + const fname = item.title || 'Untitled'; + const ext = (item.fileType || '').toUpperCase(); + const extBadge = ext ? `${esc(ext)}` : ''; + + return ` +
+
+ ${thumb ? `${esc(fname)}` : ''} +
+ 📄 + ${extBadge} +
+
+
+
${esc(fname)}
+
+ ${item.fileSize ? `${esc(item.fileSize)}` : ''} + ${item.date ? `${esc(item.date)}` : ''} +
+
+
`; + }).join(''); + + return ` + `; +}; + +const DuplicateCompare: ComponentFn = (props) => { + const { records = [], highlightDiffs = true, title } = props; + if (records.length < 2) return '
🔍

Need at least 2 records to compare

'; + + const rec1 = records[0]; + const rec2 = records[1]; + const allFields = new Set(); + for (const k of Object.keys(rec1.fields || {})) allFields.add(k); + for (const k of Object.keys(rec2.fields || {})) allFields.add(k); + const fields = Array.from(allFields); + + const rows = fields.map(f => { + const v1 = (rec1.fields || {})[f] ?? ''; + const v2 = (rec2.fields || {})[f] ?? ''; + const isDiff = highlightDiffs && v1 !== v2; + return ` +
+
${esc(f)}
+
${esc(v1)}
+
${esc(v2)}
+
`; + }).join(''); + + return ` +
+ ${title ? `
${esc(title)}
` : ''} +
+
Field
+
${esc(rec1.label || 'Record A')}
+
${esc(rec2.label || 'Record B')}
+
+
${rows}
+
`; +}; + + +// ─── Interactive Editor Components ────────────────────────── +// These components render interactive forms that call MCP server tools +// via window.callServerTool (set up in main.ts) + +const ContactPicker: ComponentFn = (props) => { + const { searchTool = 'search_contacts', placeholder = 'Search contacts...', value } = props; + const selectedHtml = value + ? `
${esc(value.name || value)}
` + : ''; + return ` +
+ ${selectedHtml} +
+ + +
+
`; +}; + +const InvoiceBuilder: ComponentFn = (props) => { + const { createTool = 'create_invoice', contactSearchTool = 'search_contacts', initialContact, initialItems = [] } = props; + const itemRows = (initialItems.length > 0 ? initialItems : [{ description: '', quantity: 1, unitPrice: 0 }]).map((item: any, i: number) => ` + + + + + $${((item.quantity || 1) * (item.unitPrice || 0)).toFixed(2)} + + `).join(''); + + const total = initialItems.reduce((s: number, i: any) => s + (i.quantity || 1) * (i.unitPrice || 0), 0); + + return ` +
+
+

New Invoice

+
+
+ + +
+ + + ${itemRows} +
DescriptionQtyPriceTotal
+
+ +
Total: $${total.toFixed(2)}
+
+
+ +
+
`; +}; + +const OpportunityEditor: ComponentFn = (props) => { + const { saveTool = 'update_opportunity', opportunity = {}, stages = [] } = props; + const opp = opportunity; + const stageOptions = stages.map((s: any) => + `` + ).join(''); + const statusOptions = ['open', 'won', 'lost', 'abandoned'].map(s => + `` + ).join(''); + + return ` +
+
+

Edit Opportunity

+
+
+ + +
+
+ + +
+ ${stages.length > 0 ? ` +
+ + +
` : ''} +
+ + +
+
+ +
+
`; +}; + +const AppointmentBooker: ComponentFn = (props) => { + const { calendarTool, bookTool, contactSearchTool, calendarId } = props; + const now = new Date(); + return ` +
+
+

Book Appointment

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
`; +}; + +const EditableField: ComponentFn = (props) => { + const { value, fieldName, saveTool, saveArgs } = props; + return ` + + ${esc(value)} + + ✏️ + `; +}; + +const SelectDropdown: ComponentFn = (props) => { + const { options = [], value, placeholder = 'Select...' } = props; + const optionHtml = options.map((o: any) => + `` + ).join(''); + return ` + `; +}; + +const FormGroup: ComponentFn = (props) => { + const { fields = [], submitLabel = 'Submit', submitTool } = props; + const fieldHtml = fields.map((f: any) => { + const inputType = f.type || 'text'; + let inputHtml: string; + if (f.options) { + inputHtml = ``; + } else { + inputHtml = ``; + } + return ` +
+ + ${inputHtml} +
`; + }).join(''); + + return ` +
+ ${fieldHtml} +
+ +
+
`; +}; + +const AmountInput: ComponentFn = (props) => { + const { value = 0, currency = 'USD' } = props; + let formatted: string; + try { + formatted = new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(value); + } catch { + formatted = `$${Number(value).toFixed(2)}`; + } + return ` +
+ ${esc(formatted)} + +
`; +}; + +// ─── Component Registry ───────────────────────────────────── + +export const COMPONENTS: Record = { + 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, + // Interactive editors + ContactPicker, InvoiceBuilder, OpportunityEditor, AppointmentBooker, + EditableField, SelectDropdown, FormGroup, AmountInput, +}; diff --git a/src/ui/json-render-app/src/main.ts b/src/ui/json-render-app/src/main.ts new file mode 100644 index 0000000..d3450e6 --- /dev/null +++ b/src/ui/json-render-app/src/main.ts @@ -0,0 +1,479 @@ +/** + * GHL Dynamic View - MCP App Entry Point + * Receives UI trees from the generate_ghl_view tool via MCP ext-apps protocol + * and renders them using the component library. + * + * Implements the MCP ext-apps handshake: + * 1. View sends ui/initialize → Host responds + * 2. View sends ui/notifications/initialized → Host acks + * 3. Host sends ui/notifications/tool-result → View renders + */ + +import { COMPONENTS } from './components'; +import { STYLES } from './styles'; + +// ─── Inject Styles ────────────────────────────────────────── +const styleEl = document.createElement('style'); +styleEl.textContent = STYLES; +document.head.appendChild(styleEl); + +// ─── Tree Renderer ────────────────────────────────────────── +interface UIElement { + key: string; + type: string; + props: Record; + children?: string[]; +} + +interface UITree { + root: string; + elements: Record; +} + +function renderElement(el: UIElement, elements: Record): string { + const component = COMPONENTS[el.type]; + if (!component) { + return `

Unknown component: ${el.type}

`; + } + + const childrenHtml = (el.children || []) + .map(key => { + const childEl = elements[key]; + if (!childEl) return `
Missing element: ${key}
`; + return renderElement(childEl, elements); + }) + .join(''); + + return component(el.props || {}, childrenHtml); +} + +function renderTree(tree: UITree): void { + const appEl = document.getElementById('app'); + if (!appEl) return; + + try { + const rootEl = tree.elements[tree.root]; + if (!rootEl) { + appEl.innerHTML = `

Render Error

Root element "${tree.root}" not found in tree

`; + return; + } + appEl.innerHTML = renderElement(rootEl, tree.elements); + + // After rendering, report actual content size to host + requestAnimationFrame(() => { + reportSize(); + }); + } catch (err: any) { + appEl.innerHTML = `

Render Error

${err.message || 'Unknown error'}

`; + } +} + +/** Measure actual content and notify host of preferred size */ +function reportSize(): void { + const appEl = document.getElementById('app'); + if (!appEl) return; + + const height = Math.min(appEl.scrollHeight + 16, 600); // Cap at 600px + const width = appEl.scrollWidth + 8; + + try { + window.parent.postMessage({ + jsonrpc: '2.0', + method: 'ui/notifications/size-changed', + params: { width, height }, + }, '*'); + } catch { + // Not in iframe + } + + // Also set body height so the iframe can auto-size if the host supports it + document.body.style.height = height + 'px'; + document.body.style.overflow = 'auto'; +} + +// ─── Loading State ────────────────────────────────────────── +function showLoading(): void { + const appEl = document.getElementById('app'); + if (appEl) { + appEl.innerHTML = ` +
+
+

Generating view...

+
`; + } +} + +function showError(message: string): void { + const appEl = document.getElementById('app'); + if (appEl) { + appEl.innerHTML = `

Error

${message}

`; + } +} + +// ─── Helpers ──────────────────────────────────────────────── + +/** Try to extract a uiTree from any data shape and render it */ +function tryRenderFromData(data: any): boolean { + if (!data || typeof data !== 'object') return false; + + // structuredContent.uiTree + if (data.structuredContent?.uiTree) { + renderTree(data.structuredContent.uiTree); + return true; + } + // Direct uiTree + if (data.uiTree) { + renderTree(data.uiTree); + return true; + } + // Direct tree shape + if (data.root && data.elements) { + renderTree(data as UITree); + return true; + } + // Nested in content array + if (Array.isArray(data.content)) { + for (const item of data.content) { + if (item.structuredContent?.uiTree) { + renderTree(item.structuredContent.uiTree); + return true; + } + } + } + return false; +} + +// ─── MCP ext-apps Protocol (JSON-RPC over postMessage) ────── +let rpcId = 1; +let initialized = false; + +function sendToHost(message: Record): void { + try { + window.parent.postMessage(message, '*'); + } catch { + // Not in an iframe or parent unavailable + } +} + +function sendJsonRpcRequest(method: string, params: Record = {}): number { + const id = rpcId++; + sendToHost({ jsonrpc: '2.0', id, method, params }); + return id; +} + +function sendJsonRpcNotification(method: string, params: Record = {}): void { + sendToHost({ jsonrpc: '2.0', method, params }); +} + +// ─── Message Handler ──────────────────────────────────────── +showLoading(); + +window.addEventListener('message', (event: MessageEvent) => { + try { + const data = event.data; + if (!data || typeof data !== 'object') return; + + // ── JSON-RPC protocol (ext-apps standard) ── + if (data.jsonrpc === '2.0') { + + // JSON-RPC response (to our requests) + if ('id' in data && (data.result !== undefined || data.error !== undefined)) { + const id = data.id as number; + // Check if it's a response to a tool call + const pending = pendingToolCalls.get(id); + if (pending) { + pendingToolCalls.delete(id); + if (data.error) { + pending.reject(new Error(data.error.message || 'Tool call failed')); + } else { + pending.resolve(data.result); + } + return; + } + // Otherwise it's likely the initialize response + if (!initialized) { + initialized = true; + sendJsonRpcNotification('ui/notifications/initialized', {}); + } + return; + } + + // Host sends tool result + if (data.method === 'ui/notifications/tool-result') { + if (tryRenderFromData(data.params)) return; + // Try params directly as structured content + if (data.params && tryRenderFromData(data.params)) return; + return; + } + + // Host sends tool input (args before result — could render partial) + if (data.method === 'ui/notifications/tool-input') { + // Tool is still executing; keep showing loading + return; + } + + // Host sends partial input (streaming) + if (data.method === 'ui/notifications/tool-input-partial') { + return; + } + + // Host sends tool cancelled + if (data.method === 'ui/notifications/tool-cancelled') { + showError('View generation was cancelled.'); + return; + } + + // Host context changed (theme, etc.) + if (data.method === 'ui/notifications/host-context-changed') { + // Could apply theme here in the future + return; + } + + // Host sends teardown request + if (data.method === 'ui/teardown' && data.id) { + sendToHost({ jsonrpc: '2.0', id: data.id, result: {} }); + return; + } + + // Ping + if (data.method === 'ping' && data.id) { + sendToHost({ jsonrpc: '2.0', id: data.id, result: {} }); + return; + } + + return; + } + + // ── Legacy / non-JSON-RPC fallbacks ── + + // Some hosts may send custom formats + if (data.type === 'tool-result' || data.type === 'mcp-tool-result') { + if (tryRenderFromData(data)) return; + const content = data.structuredContent || data.content || data.data || data; + if (tryRenderFromData(content)) return; + } + + if (data.type === 'mcp-app-init' && data.data) { + if (tryRenderFromData(data.data)) return; + } + + // Direct data passthrough + if (tryRenderFromData(data)) return; + + } catch (err: any) { + showError(err.message || 'Failed to process message'); + } +}); + +// ─── Check pre-injected data ──────────────────────────────── +const preInjected = (window as any).__MCP_APP_DATA__; +if (preInjected) { + tryRenderFromData(preInjected); +} + +// ─── MCP Tool Calling ─────────────────────────────────────── +const pendingToolCalls = new Map void; reject: (e: Error) => void }>(); + +function callTool(toolName: string, args: Record): Promise { + return new Promise((resolve, reject) => { + const id = rpcId++; + pendingToolCalls.set(id, { resolve, reject }); + sendToHost({ jsonrpc: '2.0', id, method: 'tools/call', params: { name: toolName, arguments: args } }); + // Timeout after 30s + setTimeout(() => { + if (pendingToolCalls.has(id)) { + pendingToolCalls.delete(id); + reject(new Error('Tool call timed out')); + } + }, 30000); + }); +} + +// ─── Toast Notifications ──────────────────────────────────── +function showToast(message: string, type: 'success' | 'error' | 'info' = 'info'): void { + const existing = document.getElementById('mcp-toast'); + if (existing) existing.remove(); + + const toast = document.createElement('div'); + toast.id = 'mcp-toast'; + toast.className = `mcp-toast mcp-toast-${type}`; + toast.textContent = message; + document.body.appendChild(toast); + requestAnimationFrame(() => toast.classList.add('mcp-toast-show')); + setTimeout(() => { toast.classList.remove('mcp-toast-show'); setTimeout(() => toast.remove(), 300); }, 2500); +} + +// ─── Edit Modal ───────────────────────────────────────────── +function showEditModal(title: string, fields: Array<{key: string, label: string, value: string, type?: string}>, onSave: (values: Record) => void): void { + const existing = document.getElementById('mcp-modal'); + if (existing) existing.remove(); + + const modal = document.createElement('div'); + modal.id = 'mcp-modal'; + modal.className = 'mcp-modal-overlay'; + modal.innerHTML = ` +
+
+ ${title} + +
+
+ ${fields.map(f => ` +
+ + +
`).join('')} +
+ +
`; + document.body.appendChild(modal); + + modal.querySelectorAll('[data-modal-close]').forEach(el => el.addEventListener('click', () => modal.remove())); + modal.querySelector('[data-modal-save]')?.addEventListener('click', () => { + const values: Record = {}; + modal.querySelectorAll('[data-field]').forEach(inp => { + values[inp.dataset.field!] = inp.value; + }); + onSave(values); + modal.remove(); + }); + modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); }); +} + +// ─── Drag & Drop (Kanban) ─────────────────────────────────── +let draggedCardId: string | null = null; +let draggedFromStage: string | null = null; + +document.addEventListener('dragstart', (e) => { + const card = (e.target as HTMLElement).closest?.('.kanban-card[draggable]') as HTMLElement | null; + if (!card) return; + draggedCardId = card.dataset.cardId || null; + draggedFromStage = card.dataset.stageId || null; + card.classList.add('kanban-card-dragging'); + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', draggedCardId || ''); + } +}); + +document.addEventListener('dragend', (e) => { + const card = (e.target as HTMLElement).closest?.('.kanban-card') as HTMLElement | null; + if (card) card.classList.remove('kanban-card-dragging'); + document.querySelectorAll('.kanban-col-body').forEach(el => el.classList.remove('kanban-drop-target')); + draggedCardId = null; + draggedFromStage = null; +}); + +document.addEventListener('dragover', (e) => { + const colBody = (e.target as HTMLElement).closest?.('.kanban-col-body') as HTMLElement | null; + if (colBody) { + e.preventDefault(); + if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'; + colBody.classList.add('kanban-drop-target'); + } +}); + +document.addEventListener('dragleave', (e) => { + const colBody = (e.target as HTMLElement).closest?.('.kanban-col-body') as HTMLElement | null; + if (colBody) colBody.classList.remove('kanban-drop-target'); +}); + +document.addEventListener('drop', async (e) => { + e.preventDefault(); + const colBody = (e.target as HTMLElement).closest?.('.kanban-col-body[data-stage-id]') as HTMLElement | null; + if (!colBody || !draggedCardId) return; + + const newStageId = colBody.dataset.stageId; + if (!newStageId || newStageId === draggedFromStage) return; + + // Move the card in the DOM immediately for snappy feel + const draggedEl = document.querySelector(`.kanban-card[data-card-id="${draggedCardId}"]`) as HTMLElement; + if (draggedEl) { + draggedEl.classList.remove('kanban-card-dragging'); + draggedEl.dataset.stageId = newStageId; + colBody.appendChild(draggedEl); + showToast('Moving deal...', 'info'); + } + + // Call the MCP server to persist the change + try { + await callTool('update_opportunity', { + opportunityId: draggedCardId, + pipelineStageId: newStageId, + }); + showToast('Deal moved!', 'success'); + } catch (err: any) { + showToast(`Failed: ${err.message}`, 'error'); + // TODO: revert DOM on failure + } + colBody.classList.remove('kanban-drop-target'); +}); + +// ─── Double-click to Edit (Kanban cards) ──────────────────── +document.addEventListener('dblclick', (e) => { + const card = (e.target as HTMLElement).closest?.('.kanban-card[data-card-id]') as HTMLElement | null; + if (card && card.dataset.cardId) { + const titleEl = card.querySelector('.kanban-card-title'); + const valueEl = card.querySelector('.kanban-card-value'); + showEditModal('Edit Opportunity', [ + { key: 'name', label: 'Name', value: titleEl?.textContent || '' }, + { key: 'monetaryValue', label: 'Value ($)', value: (valueEl?.textContent || '').replace(/[^0-9.]/g, ''), type: 'number' }, + { key: 'status', label: 'Status', value: 'open' }, + ], async (values) => { + showToast('Updating...', 'info'); + try { + const args: Record = { opportunityId: card.dataset.cardId! }; + if (values.name) args.name = values.name; + if (values.monetaryValue) args.monetaryValue = parseFloat(values.monetaryValue); + if (values.status) args.status = values.status; + await callTool('update_opportunity', args); + // Update DOM + if (titleEl && values.name) titleEl.textContent = values.name; + if (valueEl && values.monetaryValue) valueEl.textContent = `$${parseFloat(values.monetaryValue).toLocaleString()}`; + showToast('Updated!', 'success'); + } catch (err: any) { + showToast(`Failed: ${err.message}`, 'error'); + } + }); + return; + } + + // Double-click on table row + const row = (e.target as HTMLElement).closest?.('tr[data-row-id]') as HTMLElement | null; + if (row && row.dataset.rowId) { + const cells = row.querySelectorAll('td'); + const headers = row.closest('table')?.querySelectorAll('th'); + const fields: Array<{key: string, label: string, value: string}> = []; + cells.forEach((cell, i) => { + if (cell.classList.contains('checkbox-col')) return; + const label = headers?.[i]?.textContent?.trim() || `Field ${i}`; + fields.push({ key: `field_${i}`, label, value: cell.textContent?.trim() || '' }); + }); + showEditModal('Row Details', fields, () => { + showToast('Edit via table is read-only for now', 'info'); + }); + } +}); + +// ─── Initiate ext-apps handshake ──────────────────────────── +// Send ui/initialize to the host to start the protocol +// The host will respond, then we send initialized, then it sends tool data +try { + if (window.parent && window.parent !== window) { + sendJsonRpcRequest('ui/initialize', { + protocolVersion: '2026-01-26', + appInfo: { + name: 'GHL Dynamic View', + version: '1.0.0', + }, + appCapabilities: { + tools: { listChanged: false }, + }, + }); + } +} catch { + // Not in an iframe context — rely on __MCP_APP_DATA__ +} diff --git a/src/ui/json-render-app/src/styles.ts b/src/ui/json-render-app/src/styles.ts new file mode 100644 index 0000000..0fe8b5c --- /dev/null +++ b/src/ui/json-render-app/src/styles.ts @@ -0,0 +1,973 @@ +/** + * Inlined CSS styles for the GHL Dynamic View app. + * Matches the polish of existing MCP App UIs. + */ +export const STYLES = ` +/* ─── Reset & Base ────────────────────────────────────── */ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: white; + color: #1f2937; + line-height: 1.4; + font-size: 12px; + -webkit-font-smoothing: antialiased; + overflow: hidden; +} + +#app { padding: 4px; } + +/* ─── Compact Chat Overrides ──────────────────────────── */ +/* Scale everything down for inline chat MCP App display */ + +a { color: #4f46e5; text-decoration: none; } +a:hover { text-decoration: underline; } + +.font-medium { font-weight: 500; } +.font-mono { font-family: 'SF Mono', 'Fira Code', monospace; } +.text-sm { font-size: 13px; } +.text-muted { color: #9ca3af; } +.text-secondary { color: #6b7280; } +.text-left { text-align: left; } +.text-center { text-align: center; } +.text-right { text-align: right; } + +/* ─── Loading & Error States ──────────────────────────── */ +.loading-state { + display: flex; flex-direction: column; align-items: center; justify-content: center; + min-height: 120px; color: #6b7280; +} +.loading-spinner { + width: 24px; height: 24px; border: 2px solid #e5e7eb; border-top-color: #4f46e5; + border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 8px; +} +@keyframes spin { to { transform: rotate(360deg); } } +.loading-state p { font-size: 12px; } + +.error-state { + padding: 12px; margin: 8px; background: #fef2f2; border: 1px solid #fecaca; + border-radius: 8px; color: #dc2626; +} +.error-state h3 { font-size: 13px; font-weight: 600; margin-bottom: 4px; } +.error-state p { font-size: 12px; color: #991b1b; } + +/* ─── Status Badges ───────────────────────────────────── */ +.status-badge, .status-badge-sm { + display: inline-flex; align-items: center; border-radius: 9999px; + font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; +} +.status-badge { padding: 4px 12px; font-size: 11px; } +.status-badge-sm { padding: 2px 6px; font-size: 9px; } + +.status-active { background: #dbeafe; color: #1e40af; } +.status-complete { background: #dcfce7; color: #166534; } +.status-paused { background: #fef3c7; color: #92400e; } +.status-draft { background: #e5e7eb; color: #4b5563; } +.status-error { background: #fee2e2; color: #991b1b; } +.status-sent { background: #dbeafe; color: #1e40af; } +.status-paid { background: #dcfce7; color: #166534; } +.status-pending { background: #fef3c7; color: #92400e; } +.status-open { background: #dbeafe; color: #1d4ed8; } +.status-won { background: #dcfce7; color: #15803d; } +.status-lost { background: #fee2e2; color: #b91c1c; } + +/* ─── Page Header ─────────────────────────────────────── */ +.page-header { + background: white; border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; + box-shadow: 0 1px 2px rgba(0,0,0,0.06); +} +.page-header-gradient { + background: linear-gradient(135deg, #4f46e5, #7c3aed); + color: white; padding: 8px 12px; position: sticky; top: 0; z-index: 50; + border-radius: 8px; margin-bottom: 8px; + box-shadow: 0 1px 4px rgba(79,70,229,0.3); +} +.page-header-top { display: flex; align-items: center; justify-content: space-between; } +.page-header-title { font-size: 14px; font-weight: 700; color: #111827; } +.page-header-title-light { font-size: 13px; font-weight: 600; } +.page-header-subtitle { font-size: 11px; color: #6b7280; margin-top: 2px; } +.page-header-subtitle-light { font-size: 10px; opacity: 0.8; margin-top: 1px; } +.badge-light { padding: 2px 8px; border-radius: 9999px; font-size: 10px; font-weight: 600; background: rgba(255,255,255,0.2); } +.page-header-stats { display: flex; gap: 12px; margin-top: 6px; flex-wrap: wrap; } +.page-header-stats-light { display: flex; gap: 10px; margin-top: 4px; } +.stat-item { font-size: 11px; color: #6b7280; } +.stat-value { font-weight: 500; color: #374151; } +.stat-item-light { font-size: 10px; display: flex; align-items: center; gap: 4px; } +.stat-value-light { font-weight: 600; } +.stat-label-light { opacity: 0.7; } + +/* ─── Card ────────────────────────────────────────────── */ +.card { + background: white; border-radius: 8px; box-shadow: 0 1px 2px rgba(0,0,0,0.06); + overflow: hidden; margin-bottom: 8px; +} +.card.no-border { box-shadow: none; } +.card-header { padding: 8px 12px; border-bottom: 1px solid #e5e7eb; } +.card-title { font-size: 13px; font-weight: 600; color: #111827; } +.card-subtitle { font-size: 11px; color: #6b7280; margin-top: 1px; } +.card-body.p-0 { padding: 0; } +.card-body.p-sm { padding: 8px; } +.card-body.p-md { padding: 10px; } +.card-body.p-lg { padding: 12px; } + +/* ─── Stats Grid ──────────────────────────────────────── */ +.stats-grid { display: grid; gap: 8px; margin-bottom: 8px; } +.stats-grid-2 { grid-template-columns: repeat(2, 1fr); } +.stats-grid-3 { grid-template-columns: repeat(2, 1fr); } +.stats-grid-4 { grid-template-columns: repeat(2, 1fr); } +.stats-grid-6 { grid-template-columns: repeat(2, 1fr); } +@media (min-width: 640px) { + .stats-grid-3 { grid-template-columns: repeat(3, 1fr); } + .stats-grid-4 { grid-template-columns: repeat(4, 1fr); } + .stats-grid-6 { grid-template-columns: repeat(3, 1fr); } +} +@media (min-width: 1024px) { + .stats-grid-6 { grid-template-columns: repeat(6, 1fr); } +} + +/* ─── Split Layout ────────────────────────────────────── */ +.split-layout { display: grid; } +.split-50-50 { grid-template-columns: 1fr; } +.split-33-67 { grid-template-columns: 1fr; } +.split-67-33 { grid-template-columns: 1fr; } +.gap-sm { gap: 6px; } +.gap-md { gap: 10px; } +.gap-lg { gap: 14px; } +@media (min-width: 640px) { + .split-50-50 { grid-template-columns: 1fr 1fr; } + .split-33-67 { grid-template-columns: 1fr 2fr; } + .split-67-33 { grid-template-columns: 2fr 1fr; } +} + +/* ─── Section ─────────────────────────────────────────── */ +.section { margin-bottom: 8px; } +.section-header { margin-bottom: 6px; } +.section-title { font-size: 13px; font-weight: 600; color: #111827; } +.section-desc { font-size: 11px; color: #6b7280; margin-top: 1px; } + +/* ─── Metric Card ─────────────────────────────────────── */ +.metric-card { + background: rgba(255,255,255,0.95); border-radius: 8px; padding: 10px; + text-align: center; box-shadow: 0 1px 2px rgba(0,0,0,0.06); +} +.metric-value { font-size: 18px; font-weight: 700; color: #111827; } +.metric-green { color: #059669; } +.metric-blue { color: #2563eb; } +.metric-purple { color: #7c3aed; } +.metric-yellow { color: #d97706; } +.metric-red { color: #dc2626; } +.metric-label { font-size: 10px; color: #6b7280; margin-top: 2px; } +.metric-trend { font-size: 11px; font-weight: 600; margin-top: 4px; } +.trend-up { color: #059669; } +.trend-down { color: #dc2626; } +.trend-flat { color: #6b7280; } + +/* ─── Data Table ──────────────────────────────────────── */ +.data-table-wrap { + background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); + overflow: hidden; +} +.table-container { overflow-x: auto; } +.data-table { width: 100%; border-collapse: collapse; font-size: 11px; } +.data-table th { + text-align: left; padding: 6px 8px; background: #f9fafb; color: #6b7280; + font-weight: 500; font-size: 10px; text-transform: uppercase; letter-spacing: 0.3px; + border-bottom: 1px solid #e5e7eb; white-space: nowrap; +} +.data-table th.sortable { cursor: pointer; user-select: none; } +.data-table th.sortable:hover { background: #f3f4f6; } +.data-table td { padding: 5px 8px; border-bottom: 1px solid #f3f4f6; vertical-align: middle; } +.data-table tr:hover { background: #f9fafb; } +.data-table .checkbox-col { width: 40px; } +.data-table input[type="checkbox"] { width: 16px; height: 16px; accent-color: #4f46e5; cursor: pointer; } + +.avatar-cell { display: flex; align-items: center; gap: 12px; } +.avatar { + width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; + justify-content: center; color: white; font-weight: 600; font-size: 12px; flex-shrink: 0; +} +.tags { display: flex; flex-wrap: wrap; gap: 4px; max-width: 200px; } +.tag { + display: inline-block; padding: 2px 8px; background: #eef2ff; color: #4f46e5; + border-radius: 12px; font-size: 11px; font-weight: 500; +} +.tag-more { background: #f3f4f6; color: #6b7280; } +.link { color: #4f46e5; text-decoration: none; } +.link:hover { text-decoration: underline; } + +.table-pagination { + padding: 12px 20px; border-top: 1px solid #e5e7eb; display: flex; + justify-content: space-between; align-items: center; +} +.pagination-info { font-size: 13px; color: #6b7280; } +.pagination-buttons { display: flex; gap: 8px; } + +.empty-state { padding: 60px 20px; text-align: center; } +.empty-icon { font-size: 48px; margin-bottom: 16px; } +.empty-state p { color: #6b7280; font-size: 15px; } + +/* ─── Kanban Board ────────────────────────────────────── */ +.kanban-wrap { padding: 8px; overflow-x: auto; overflow-y: auto; max-height: calc(100vh - 80px); } +.kanban-cols { display: flex; gap: 8px; min-width: max-content; } +.kanban-col { + background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); + display: flex; flex-direction: column; width: 210px; min-width: 210px; + max-height: calc(100vh - 100px); +} +.kanban-col-header { + padding: 10px 12px; border-bottom: 1px solid #e5e7eb; + position: sticky; top: 0; background: white; border-radius: 8px 8px 0 0; +} +.kanban-col-title-row { display: flex; align-items: center; justify-content: space-between; } +.kanban-col-title { font-weight: 600; font-size: 12px; color: #1f2937; } +.kanban-col-count { + background: #e5e7eb; color: #4b5563; padding: 1px 6px; border-radius: 9999px; + font-size: 10px; font-weight: 500; +} +.kanban-col-value { font-size: 10px; color: #6b7280; margin-top: 2px; } +.kanban-col-body { padding: 6px; overflow-y: auto; flex: 1; min-height: 60px; max-height: 320px; } +.kanban-empty { text-align: center; padding: 16px; color: #9ca3af; font-size: 10px; } +.kanban-card { + background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; + padding: 8px 10px; margin-bottom: 6px; cursor: pointer; + transition: box-shadow 0.15s, border-color 0.15s; +} +.kanban-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.1); border-color: #4f46e5; } +.kanban-card-title { font-weight: 600; font-size: 11px; color: #111827; margin-bottom: 4px; line-height: 1.3; } +.kanban-card-subtitle { display: flex; align-items: center; gap: 4px; font-size: 10px; color: #6b7280; margin-bottom: 4px; } +.kanban-avatar { + width: 16px; height: 16px; background: #e5e7eb; border-radius: 50%; + display: flex; align-items: center; justify-content: center; font-size: 7px; + font-weight: 700; color: #4b5563; +} +.kanban-card-value { font-size: 12px; font-weight: 700; color: #059669; margin-bottom: 4px; } +.kanban-card-footer { display: flex; align-items: center; justify-content: space-between; font-size: 9px; color: #9ca3af; } + +/* ─── Timeline ────────────────────────────────────────── */ +.timeline { position: relative; padding-left: 24px; } +.timeline-line { position: absolute; left: 8px; top: 8px; bottom: 8px; width: 2px; background: #e5e7eb; } +.timeline-item { position: relative; margin-bottom: 16px; } +.timeline-item:last-child { margin-bottom: 0; } +.timeline-dot { + position: absolute; left: -16px; top: 4px; width: 16px; height: 16px; + border-radius: 50%; border: 2px solid; background: white; + display: flex; align-items: center; justify-content: center; font-size: 8px; +} +.tl-border-default { border-color: #d1d5db; } +.tl-border-success { border-color: #4ade80; } +.tl-border-warning { border-color: #facc15; } +.tl-border-error { border-color: #f87171; } +.timeline-content { margin-left: 8px; } +.timeline-title { font-weight: 500; font-size: 14px; color: #111827; } +.timeline-desc { font-size: 14px; color: #6b7280; margin-top: 2px; } +.timeline-time { font-size: 12px; color: #9ca3af; margin-top: 2px; } + +/* ─── Progress Bar ────────────────────────────────────── */ +.progress-wrap { margin-bottom: 20px; } +.progress-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } +.progress-label { font-size: 14px; font-weight: 500; color: #374151; } +.progress-value { font-size: 14px; color: #6b7280; } +.progress-value strong { color: #111827; } +.progress-track { position: relative; height: 24px; background: #e5e7eb; border-radius: 12px; overflow: hidden; } +.progress-bar { height: 100%; border-radius: 12px; transition: width 0.5s; } +.bar-green { background: linear-gradient(to right, #10b981, #059669); } +.bar-blue { background: linear-gradient(to right, #3b82f6, #2563eb); } +.bar-purple { background: linear-gradient(to right, #8b5cf6, #7c3aed); } +.bar-yellow { background: linear-gradient(to right, #f59e0b, #d97706); } +.bar-red { background: linear-gradient(to right, #ef4444, #dc2626); } +.progress-benchmark { position: absolute; top: 0; height: 100%; width: 2px; background: rgba(0,0,0,0.35); } +.progress-benchmark-label { position: absolute; top: -16px; font-size: 10px; color: #6b7280; transform: translateX(-50%); } + +/* ─── Detail Header ───────────────────────────────────── */ +.detail-header { + background: white; border-radius: 12px; padding: 24px; margin-bottom: 20px; + box-shadow: 0 1px 3px rgba(0,0,0,0.08); +} +.detail-header-top { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 4px; } +.detail-title { font-size: 24px; font-weight: 700; color: #111827; } +.detail-entity-id { font-size: 14px; color: #9ca3af; margin-top: 2px; } +.detail-subtitle { font-size: 14px; color: #6b7280; margin-top: 4px; } + +/* ─── Key-Value List ──────────────────────────────────── */ +.kv-list { background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden; } +.kv-row { display: flex; justify-content: space-between; align-items: center; padding: 12px 24px; border-bottom: 1px solid #f3f4f6; } +.kv-row:last-child { border-bottom: none; } +.kv-compact { padding: 8px 24px; } +.kv-total { background: #1e3a5f; color: white; padding: 16px 24px; border-bottom: none; } +.kv-success { background: #f0fdf4; } +.kv-highlight { background: #f9fafb; font-weight: 600; } +.kv-muted .kv-label, .kv-muted .kv-value { color: #9ca3af; } +.kv-label { font-size: 14px; color: #6b7280; } +.kv-total .kv-label { font-size: 16px; font-weight: 600; color: white; } +.kv-value { font-family: 'SF Mono', 'Fira Code', monospace; font-weight: 500; color: #111827; } +.kv-value-bold { font-family: 'SF Mono', 'Fira Code', monospace; font-weight: 600; color: #111827; } +.kv-value-total { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 20px; font-weight: 700; color: white; } +.kv-value-success { font-family: 'SF Mono', 'Fira Code', monospace; font-weight: 500; color: #059669; } +.kv-value-danger { font-family: 'SF Mono', 'Fira Code', monospace; font-weight: 500; color: #dc2626; } +.kv-success .kv-label { color: #15803d; } + +/* ─── Line Items Table ────────────────────────────────── */ +.line-items-wrap { + background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); + overflow: hidden; margin-bottom: 20px; +} +.line-items-table { width: 100%; border-collapse: collapse; } +.line-items-table th { + background: #f9fafb; padding: 12px 16px; font-size: 12px; font-weight: 600; + color: #6b7280; text-transform: uppercase; letter-spacing: 0.05em; + border-bottom: 1px solid #e5e7eb; +} +.line-items-table td { padding: 16px; border-bottom: 1px solid #f3f4f6; } +.line-items-table tr:last-child td { border-bottom: none; } + +/* ─── Info Block ──────────────────────────────────────── */ +.info-block { margin-bottom: 16px; } +.info-block-label { font-size: 11px; text-transform: uppercase; color: #9ca3af; letter-spacing: 0.05em; font-weight: 500; margin-bottom: 8px; } +.info-block-name { font-size: 16px; font-weight: 600; color: #111827; } +.info-block-lines { font-size: 14px; color: #6b7280; line-height: 1.6; margin-top: 4px; } + +/* ─── Search Bar ──────────────────────────────────────── */ +.search-bar { padding: 12px 20px; background: #f9fafb; border-bottom: 1px solid #e5e7eb; } +.search-input { + width: 100%; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; + font-size: 14px; outline: none; transition: border-color 0.2s, box-shadow 0.2s; +} +.search-input:focus { border-color: #4f46e5; box-shadow: 0 0 0 3px rgba(79,70,229,0.1); } + +/* ─── Filter Chips ────────────────────────────────────── */ +.filter-chips { display: flex; flex-wrap: wrap; gap: 8px; padding: 8px 20px; border-bottom: 1px solid #e5e7eb; } +.chip { + padding: 4px 12px; border-radius: 9999px; font-size: 12px; font-weight: 500; + background: #f3f4f6; color: #4b5563; border: none; cursor: pointer; + transition: background 0.15s, color 0.15s; +} +.chip:hover { background: #e5e7eb; } +.chip-active { background: #4f46e5; color: white; } +.chip-active:hover { background: #4338ca; } + +/* ─── Tab Group ───────────────────────────────────────── */ +.tab-group { display: flex; border-bottom: 1px solid #e5e7eb; margin-bottom: 16px; } +.tab { + padding: 8px 16px; font-size: 14px; font-weight: 500; border: none; background: none; + border-bottom: 2px solid transparent; color: #6b7280; cursor: pointer; + transition: color 0.15s, border-color 0.15s; +} +.tab:hover { color: #374151; border-bottom-color: #d1d5db; } +.tab-active { color: #4f46e5; border-bottom-color: #4f46e5; } +.tab-count { + margin-left: 6px; padding: 1px 6px; background: #f3f4f6; color: #6b7280; + border-radius: 9999px; font-size: 12px; +} + +/* ─── Buttons ─────────────────────────────────────────── */ +.btn { + display: inline-flex; align-items: center; border-radius: 8px; font-weight: 500; + cursor: pointer; transition: background 0.15s, box-shadow 0.15s; border: none; +} +.btn:disabled { opacity: 0.5; cursor: not-allowed; } +.btn-primary { background: #4f46e5; color: white; box-shadow: 0 1px 2px rgba(0,0,0,0.1); } +.btn-primary:hover:not(:disabled) { background: #4338ca; } +.btn-secondary { background: white; color: #374151; border: 1px solid #d1d5db; box-shadow: 0 1px 2px rgba(0,0,0,0.05); } +.btn-secondary:hover:not(:disabled) { background: #f9fafb; } +.btn-danger { background: #dc2626; color: white; box-shadow: 0 1px 2px rgba(0,0,0,0.1); } +.btn-danger:hover:not(:disabled) { background: #b91c1c; } +.btn-ghost { background: none; color: #6b7280; } +.btn-ghost:hover:not(:disabled) { background: #f3f4f6; color: #111827; } +.btn-sm { padding: 6px 12px; font-size: 12px; } +.btn-md { padding: 8px 16px; font-size: 14px; } +.btn-lg { padding: 10px 24px; font-size: 16px; } + +/* ─── Action Bar ──────────────────────────────────────── */ +.action-bar { display: flex; gap: 12px; margin-top: 20px; } +.align-left { justify-content: flex-start; } +.align-center { justify-content: center; } +.align-right { justify-content: flex-end; } + +/* ─── Currency Display ────────────────────────────────── */ +.currency-display { font-family: 'SF Mono', 'Fira Code', monospace; font-weight: 600; } +.currency-sm { font-size: 14px; } +.currency-md { font-size: 20px; } +.currency-lg { font-size: 32px; } +.currency-positive { color: #059669; } +.currency-negative { color: #dc2626; } + +/* ─── Tag List ────────────────────────────────────────── */ +.tag-list { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; } +.tag-list-sm .tag-pill { padding: 2px 8px; font-size: 11px; } +.tag-list-md .tag-pill { padding: 4px 10px; font-size: 12px; } +.tag-pill { + display: inline-flex; align-items: center; border-radius: 9999px; + font-weight: 500; transition: opacity 0.15s; +} +.tag-pill-blue { background: #dbeafe; color: #1e40af; } +.tag-pill-green { background: #dcfce7; color: #166534; } +.tag-pill-red { background: #fee2e2; color: #991b1b; } +.tag-pill-yellow { background: #fef3c7; color: #92400e; } +.tag-pill-purple { background: #ede9fe; color: #5b21b6; } +.tag-pill-gray { background: #e5e7eb; color: #4b5563; } +.tag-pill-indigo { background: #e0e7ff; color: #3730a3; } +.tag-pill-pink { background: #fce7f3; color: #9d174d; } +.tag-pill-outlined { background: transparent; border: 1.5px solid currentColor; } +.tag-pill-more { background: #f3f4f6; color: #6b7280; } + +/* ─── Card Grid ───────────────────────────────────────── */ +.card-grid { display: grid; gap: 16px; } +.card-grid-1 { grid-template-columns: 1fr; } +.card-grid-2 { grid-template-columns: repeat(2, 1fr); } +.card-grid-3 { grid-template-columns: repeat(2, 1fr); } +.card-grid-4 { grid-template-columns: repeat(2, 1fr); } +@media (min-width: 640px) { + .card-grid-3 { grid-template-columns: repeat(3, 1fr); } + .card-grid-4 { grid-template-columns: repeat(4, 1fr); } +} +.card-grid-item { + background: white; border-radius: 10px; overflow: hidden; + box-shadow: 0 1px 3px rgba(0,0,0,0.08); transition: box-shadow 0.2s, transform 0.2s; +} +.card-grid-item:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.12); transform: translateY(-2px); } +.card-grid-image { + height: 140px; background-size: cover; background-position: center; background-color: #f3f4f6; +} +.card-grid-image-placeholder { + display: flex; align-items: center; justify-content: center; + background: linear-gradient(135deg, #eef2ff, #e0e7ff); font-size: 32px; +} +.card-grid-body { padding: 12px 14px; } +.card-grid-title { font-weight: 600; font-size: 14px; color: #111827; margin-bottom: 2px; } +.card-grid-subtitle { font-size: 12px; color: #6b7280; margin-bottom: 4px; } +.card-grid-desc { font-size: 13px; color: #6b7280; line-height: 1.4; margin-bottom: 8px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } +.card-grid-footer { display: flex; align-items: center; justify-content: space-between; } +.cg-status-active { background: #dbeafe; color: #1e40af; } +.cg-status-complete { background: #dcfce7; color: #166534; } +.cg-status-draft { background: #e5e7eb; color: #4b5563; } +.cg-status-error { background: #fee2e2; color: #991b1b; } +.cg-status-pending { background: #fef3c7; color: #92400e; } + +/* ─── Avatar Group ────────────────────────────────────── */ +.avatar-group { display: flex; align-items: center; } +.ag-avatar { + border-radius: 50%; display: flex; align-items: center; justify-content: center; + color: white; font-weight: 600; border: 2px solid white; margin-left: -8px; + overflow: hidden; flex-shrink: 0; +} +.ag-avatar:first-child { margin-left: 0; } +.ag-sm .ag-avatar { width: 28px; height: 28px; font-size: 10px; } +.ag-md .ag-avatar { width: 36px; height: 36px; font-size: 12px; } +.ag-lg .ag-avatar { width: 44px; height: 44px; font-size: 14px; } +.ag-img { width: 100%; height: 100%; object-fit: cover; } +.ag-initials { line-height: 1; } +.ag-overflow { background: #e5e7eb; color: #4b5563; } + +/* ─── Star Rating ─────────────────────────────────────── */ +.star-rating-wrap { display: flex; flex-direction: column; gap: 10px; } +.star-rating-summary { display: flex; align-items: center; gap: 8px; } +.star-rating-value { font-size: 24px; font-weight: 700; color: #111827; } +.star-rating-stars { font-size: 18px; color: #f59e0b; letter-spacing: 2px; } +.star-rating-count { font-size: 13px; color: #6b7280; } +.star-rating-distribution { display: flex; flex-direction: column; gap: 4px; } +.star-dist-row { display: flex; align-items: center; gap: 8px; } +.star-dist-label { font-size: 12px; color: #6b7280; width: 24px; text-align: right; flex-shrink: 0; } +.star-dist-track { flex: 1; height: 8px; background: #e5e7eb; border-radius: 4px; overflow: hidden; } +.star-dist-bar { height: 100%; background: #f59e0b; border-radius: 4px; transition: width 0.3s; } +.star-dist-count { font-size: 12px; color: #6b7280; width: 32px; flex-shrink: 0; } + +/* ─── Stock Indicator ─────────────────────────────────── */ +.stock-indicator { + display: inline-flex; align-items: center; gap: 10px; padding: 10px 16px; + border-radius: 10px; border: 1px solid #e5e7eb; background: white; +} +.stock-icon { font-size: 20px; } +.stock-info { display: flex; flex-direction: column; } +.stock-label { font-size: 13px; font-weight: 600; color: #111827; } +.stock-qty { font-size: 14px; font-weight: 500; color: #374151; } +.stock-level { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; } +.stock-ok .stock-level { color: #059669; } +.stock-ok { border-color: #bbf7d0; background: #f0fdf4; } +.stock-low .stock-level { color: #d97706; } +.stock-low { border-color: #fde68a; background: #fffbeb; } +.stock-critical .stock-level { color: #dc2626; } +.stock-critical { border-color: #fecaca; background: #fef2f2; } + +/* ─── Chat Thread ─────────────────────────────────────── */ +.chat-thread { + background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden; +} +.chat-thread-header { + display: flex; align-items: center; justify-content: space-between; + padding: 14px 20px; border-bottom: 1px solid #e5e7eb; background: #f9fafb; +} +.chat-thread-title { font-size: 16px; font-weight: 600; color: #111827; } +.chat-thread-count { font-size: 12px; color: #6b7280; background: #e5e7eb; padding: 2px 8px; border-radius: 9999px; } +.chat-thread-body { padding: 16px 20px; display: flex; flex-direction: column; gap: 12px; max-height: 600px; overflow-y: auto; } +.chat-msg { display: flex; align-items: flex-end; gap: 8px; } +.chat-msg-outbound { flex-direction: row; justify-content: flex-end; } +.chat-msg-inbound { flex-direction: row; } +.chat-avatar { + width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; + justify-content: center; color: white; font-weight: 600; font-size: 11px; flex-shrink: 0; +} +.chat-avatar-outbound { order: 2; } +.chat-avatar-img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; } +.chat-bubble-wrap { display: flex; flex-direction: column; max-width: 70%; } +.chat-bubble-wrap-right { align-items: flex-end; } +.chat-sender { font-size: 11px; font-weight: 600; color: #6b7280; margin-bottom: 2px; padding: 0 4px; } +.chat-bubble { + padding: 10px 14px; border-radius: 16px; font-size: 14px; line-height: 1.45; + word-break: break-word; +} +.chat-bubble-inbound { + background: #f3f4f6; color: #1f2937; + border-bottom-left-radius: 4px; +} +.chat-bubble-outbound { + background: #4f46e5; color: white; + border-bottom-right-radius: 4px; +} +.chat-meta { font-size: 11px; color: #9ca3af; margin-top: 3px; padding: 0 4px; } + +/* ─── Email Preview ───────────────────────────────────── */ +.email-preview { + background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden; +} +.email-header { + padding: 16px 20px; border-bottom: 1px solid #e5e7eb; background: #f9fafb; +} +.email-subject { font-size: 18px; font-weight: 600; color: #111827; margin-bottom: 10px; } +.email-header-row { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; font-size: 13px; } +.email-header-row:last-child { margin-bottom: 0; } +.email-label { + font-weight: 600; color: #6b7280; min-width: 40px; text-transform: uppercase; + font-size: 10px; letter-spacing: 0.05em; +} +.email-value { color: #374151; } +.email-attachments { + padding: 10px 20px; border-bottom: 1px solid #e5e7eb; display: flex; flex-wrap: wrap; gap: 8px; +} +.email-attachment { + display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; + background: #f3f4f6; border-radius: 6px; font-size: 12px; color: #374151; +} +.email-body { padding: 20px; font-size: 14px; line-height: 1.6; color: #374151; } +.email-body img { max-width: 100%; height: auto; } + +/* ─── Content Preview ─────────────────────────────────── */ +.content-preview { + background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden; +} +.content-preview-header { + padding: 14px 20px; border-bottom: 1px solid #e5e7eb; +} +.content-preview-title { font-size: 16px; font-weight: 600; color: #111827; } +.content-preview-body { + padding: 20px; font-size: 14px; line-height: 1.7; color: #374151; +} +.content-preview-body h1 { font-size: 24px; font-weight: 700; margin: 16px 0 8px; color: #111827; } +.content-preview-body h2 { font-size: 20px; font-weight: 600; margin: 14px 0 6px; color: #111827; } +.content-preview-body h3 { font-size: 16px; font-weight: 600; margin: 12px 0 4px; color: #111827; } +.content-preview-body a { color: #4f46e5; } +.content-preview-body a:hover { text-decoration: underline; } +.content-preview-body img { max-width: 100%; height: auto; border-radius: 8px; margin: 8px 0; } +.content-preview-pre { + background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; + padding: 16px; font-family: 'SF Mono', 'Fira Code', monospace; + font-size: 13px; white-space: pre-wrap; word-break: break-word; overflow-x: auto; +} + +/* ─── Transcript View ─────────────────────────────────── */ +.transcript-view { + background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden; +} +.transcript-header { + display: flex; align-items: center; justify-content: space-between; + padding: 14px 20px; border-bottom: 1px solid #e5e7eb; background: #f9fafb; +} +.transcript-title { font-size: 16px; font-weight: 600; color: #111827; } +.transcript-meta { display: flex; align-items: center; gap: 12px; } +.transcript-duration { font-size: 13px; color: #4f46e5; font-weight: 500; } +.transcript-count { font-size: 12px; color: #6b7280; background: #e5e7eb; padding: 2px 8px; border-radius: 9999px; } +.transcript-body { padding: 8px 0; } +.transcript-entry { + display: flex; gap: 16px; padding: 10px 20px; + transition: background 0.15s; +} +.transcript-entry:hover { background: #f9fafb; } +.transcript-timestamp { + font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; color: #9ca3af; + min-width: 60px; flex-shrink: 0; padding-top: 2px; +} +.transcript-content { flex: 1; min-width: 0; } +.transcript-speaker { display: flex; align-items: center; gap: 6px; margin-bottom: 2px; } +.transcript-speaker-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } +.transcript-speaker-name { font-size: 13px; font-weight: 600; color: #374151; } +.transcript-role-badge { + font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 4px; + text-transform: uppercase; letter-spacing: 0.03em; +} +.transcript-text { font-size: 14px; color: #4b5563; line-height: 1.5; } + +/* ─── Audio Player ────────────────────────────────────── */ +.audio-player { + background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); + padding: 16px 20px; display: flex; flex-direction: column; gap: 12px; +} +.audio-player-info { display: flex; align-items: center; gap: 10px; } +.audio-player-icon { font-size: 24px; } +.audio-player-title { font-size: 14px; font-weight: 600; color: #111827; } +.audio-player-type { font-size: 12px; color: #6b7280; } +.audio-player-controls { display: flex; align-items: center; gap: 12px; } +.audio-play-btn { + width: 40px; height: 40px; border-radius: 50%; border: none; + background: #4f46e5; color: white; display: flex; align-items: center; + justify-content: center; cursor: pointer; flex-shrink: 0; + transition: background 0.15s, transform 0.1s; + box-shadow: 0 2px 6px rgba(79,70,229,0.3); +} +.audio-play-btn:hover { background: #4338ca; transform: scale(1.05); } +.audio-play-btn:active { transform: scale(0.97); } +.audio-waveform { + flex: 1; display: flex; align-items: flex-end; gap: 2px; height: 36px; + padding: 0 4px; +} +.audio-bar { + flex: 1; min-width: 2px; border-radius: 1px; + background: #d1d5db; transition: background 0.2s; +} +.audio-bar-played { background: #4f46e5; } +.audio-duration { + font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; + color: #6b7280; min-width: 40px; text-align: right; flex-shrink: 0; +} + +/* ─── Checklist View ──────────────────────────────────── */ +.checklist-view { + background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden; +} +.checklist-header { + display: flex; align-items: center; justify-content: space-between; + padding: 14px 20px; border-bottom: 1px solid #e5e7eb; +} +.checklist-title { font-size: 16px; font-weight: 600; color: #111827; } +.checklist-progress-wrap { display: flex; align-items: center; gap: 10px; } +.checklist-progress-text { font-size: 12px; color: #6b7280; white-space: nowrap; } +.checklist-progress-track { width: 80px; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden; } +.checklist-progress-bar { height: 100%; background: #4f46e5; border-radius: 3px; transition: width 0.3s; } +.checklist-body { padding: 4px 0; } +.checklist-item { + display: flex; align-items: flex-start; gap: 12px; padding: 12px 20px; + border-bottom: 1px solid #f3f4f6; transition: background 0.15s; +} +.checklist-item:last-child { border-bottom: none; } +.checklist-item:hover { background: #f9fafb; } +.checklist-item-done { opacity: 0.7; } +.checklist-checkbox { + width: 20px; height: 20px; border-radius: 6px; border: 2px solid #d1d5db; + display: flex; align-items: center; justify-content: center; + flex-shrink: 0; margin-top: 1px; transition: all 0.15s; +} +.checklist-checkbox-checked { + background: #4f46e5; border-color: #4f46e5; color: white; +} +.checklist-item-content { flex: 1; min-width: 0; } +.checklist-item-title { font-size: 14px; font-weight: 500; color: #111827; } +.checklist-item-title-done { text-decoration: line-through; color: #9ca3af; } +.checklist-item-meta { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 4px; } +.checklist-due, .checklist-assignee { font-size: 12px; color: #6b7280; } +.checklist-priority { + font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; + padding: 1px 6px; border: 1px solid; border-radius: 4px; +} + +/* ─── CalendarView ────────────────────────────────────── */ +.cal-view { + background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); + padding: 20px; margin-bottom: 16px; +} +.cal-title { font-size: 18px; font-weight: 600; color: #111827; margin-bottom: 4px; } +.cal-header-month { font-size: 15px; font-weight: 500; color: #4f46e5; margin-bottom: 12px; } +.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; background: #e5e7eb; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; } +.cal-day-header { + background: #f9fafb; padding: 8px 4px; text-align: center; font-size: 11px; + font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.05em; +} +.cal-cell { + background: white; min-height: 80px; padding: 4px 6px; position: relative; + display: flex; flex-direction: column; +} +.cal-cell-empty { background: #f9fafb; } +.cal-today { background: #eef2ff; } +.cal-day-num { font-size: 12px; font-weight: 500; color: #374151; margin-bottom: 2px; } +.cal-day-today { + background: #4f46e5; color: white; border-radius: 50%; width: 22px; height: 22px; + display: inline-flex; align-items: center; justify-content: center; font-weight: 600; +} +.cal-evts { display: flex; flex-direction: column; gap: 2px; flex: 1; overflow: hidden; } +.cal-evt { + padding: 1px 4px; border-radius: 3px; font-size: 10px; color: white; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.4; +} +.cal-evt-more { font-size: 9px; color: #6b7280; padding-left: 2px; } + +/* ─── FlowDiagram ─────────────────────────────────────── */ +.flow-diagram { + background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); + padding: 20px; margin-bottom: 16px; overflow-x: auto; +} +.flow-title { font-size: 18px; font-weight: 600; color: #111827; margin-bottom: 16px; } +.flow-container { display: flex; align-items: center; gap: 0; padding: 12px 0; } +.flow-horizontal { flex-direction: row; } +.flow-vertical { flex-direction: column; } +.flow-node { + padding: 12px 20px; border-radius: 10px; border: 2px solid; text-align: center; + min-width: 120px; flex-shrink: 0; position: relative; +} +.flow-node-start { border-color: #059669; background: #ecfdf5; } +.flow-node-action { border-color: #3b82f6; background: #eff6ff; } +.flow-node-condition { border-color: #d97706; background: #fffbeb; } +.flow-diamond { border-radius: 4px; border-style: dashed; } +.flow-node-end { border-color: #6b7280; background: #f3f4f6; } +.flow-node-label { font-size: 13px; font-weight: 600; color: #111827; } +.flow-node-desc { font-size: 11px; color: #6b7280; margin-top: 4px; } +.flow-arrow { + display: flex; flex-direction: column; align-items: center; justify-content: center; + font-size: 20px; color: #9ca3af; padding: 0 8px; flex-shrink: 0; +} +.flow-arrow-vert { padding: 8px 0; } +.flow-edge-label { font-size: 10px; color: #6b7280; white-space: nowrap; } + +/* ─── TreeView ────────────────────────────────────────── */ +.tree-view { + background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); + padding: 16px 20px; margin-bottom: 16px; +} +.tree-title { font-size: 18px; font-weight: 600; color: #111827; margin-bottom: 12px; } +.tree-list { display: flex; flex-direction: column; } +.tree-item { + display: flex; align-items: center; gap: 6px; padding: 6px 8px; border-radius: 6px; + cursor: default; transition: background 0.1s; +} +.tree-item:hover { background: #f9fafb; } +.tree-chevron { width: 16px; font-size: 12px; color: #9ca3af; flex-shrink: 0; text-align: center; } +.tree-icon { font-size: 14px; flex-shrink: 0; } +.tree-label { font-size: 14px; color: #111827; font-weight: 500; } +.tree-badge { + padding: 1px 8px; border-radius: 9999px; font-size: 10px; font-weight: 600; + background: #eef2ff; color: #4f46e5; margin-left: 6px; +} + +/* ─── MediaGallery ────────────────────────────────────── */ +.mg-gallery { + background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); + padding: 20px; margin-bottom: 16px; +} +.mg-title { font-size: 18px; font-weight: 600; color: #111827; margin-bottom: 12px; } +.mg-grid { display: grid; gap: 12px; } +.mg-card { + border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; + transition: box-shadow 0.15s, border-color 0.15s; +} +.mg-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); border-color: #4f46e5; } +.mg-thumb { + width: 100%; aspect-ratio: 4/3; overflow: hidden; background: #f3f4f6; position: relative; +} +.mg-img { width: 100%; height: 100%; object-fit: cover; } +.mg-placeholder { + width: 100%; height: 100%; display: flex; flex-direction: column; + align-items: center; justify-content: center; gap: 6px; color: #9ca3af; +} +.mg-placeholder-icon { font-size: 32px; } +.mg-type-badge { + padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 700; + background: #e5e7eb; color: #4b5563; +} +.mg-info { padding: 10px 12px; } +.mg-name { + font-size: 13px; font-weight: 600; color: #111827; white-space: nowrap; + overflow: hidden; text-overflow: ellipsis; +} +.mg-meta { display: flex; gap: 12px; margin-top: 4px; font-size: 11px; color: #9ca3af; } + +/* ─── DuplicateCompare ────────────────────────────────── */ +.dc-compare { + background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); + overflow: hidden; margin-bottom: 16px; +} +.dc-title { font-size: 18px; font-weight: 600; color: #111827; padding: 16px 20px 0; } +.dc-header-row { + display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1px; + background: #f9fafb; border-bottom: 2px solid #e5e7eb; +} +.dc-header-label { + padding: 10px 16px; font-size: 12px; font-weight: 600; color: #6b7280; + text-transform: uppercase; letter-spacing: 0.05em; +} +.dc-body { } +.dc-row { + display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1px; + border-bottom: 1px solid #f3f4f6; +} +.dc-row:last-child { border-bottom: none; } +.dc-field { padding: 10px 16px; font-size: 13px; color: #6b7280; font-weight: 500; background: #f9fafb; } +.dc-val { padding: 10px 16px; font-size: 13px; color: #111827; } +.dc-diff { background: #fefce8; } + +/* ─── Chart Components ────────────────────────────────── */ +.chart-container { + background: white; border-radius: 12px; padding: 16px; + box-shadow: 0 1px 3px rgba(0,0,0,0.08); margin-bottom: 16px; +} +.chart-title { + font-size: 15px; font-weight: 600; color: #111827; margin-bottom: 12px; +} +.chart-scroll { overflow-x: auto; } +.chart-empty { text-align: center; color: #9ca3af; padding: 32px; font-size: 14px; } + +/* Bar Chart - Horizontal */ +.bar-chart-h { display: flex; flex-direction: column; gap: 10px; } +.bar-h-row { display: flex; align-items: center; gap: 10px; } +.bar-h-label { width: 90px; font-size: 13px; color: #374151; font-weight: 500; text-align: right; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.bar-h-track { flex: 1; height: 24px; background: #f3f4f6; border-radius: 6px; overflow: hidden; } +.bar-h-fill { height: 100%; border-radius: 6px; transition: width 0.4s ease; min-width: 2px; } +.bar-h-value { width: 60px; font-size: 13px; font-weight: 600; color: #111827; font-family: 'SF Mono', 'Fira Code', monospace; } + +/* Bar Chart - Vertical SVG */ +.bar-chart-svg { width: 100%; height: auto; max-height: 200px; } +.bar-v-rect { transition: opacity 0.2s; } +.bar-v-rect:hover { opacity: 0.8; } +.bar-v-val { font-size: 11px; fill: #374151; font-weight: 600; font-family: -apple-system, BlinkMacSystemFont, sans-serif; } +.bar-v-label { font-size: 10px; fill: #6b7280; font-family: -apple-system, BlinkMacSystemFont, sans-serif; } + +/* Line Chart SVG */ +.line-chart-svg { width: 100%; height: auto; max-height: 200px; } +.chart-axis-text { font-size: 10px; fill: #9ca3af; font-family: -apple-system, BlinkMacSystemFont, sans-serif; } +.chart-axis-label { font-size: 10px; fill: #6b7280; font-family: -apple-system, BlinkMacSystemFont, sans-serif; } + +/* Pie Chart */ +.pie-chart-layout { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; justify-content: center; } +.pie-chart-svg { width: 160px; height: 160px; flex-shrink: 0; } +.pie-center-text { font-size: 18px; font-weight: 700; fill: #111827; font-family: -apple-system, BlinkMacSystemFont, sans-serif; } +.pie-legend { display: flex; flex-direction: column; gap: 6px; min-width: 120px; } +.pie-legend-item { display: flex; align-items: center; gap: 8px; font-size: 13px; } +.pie-legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } +.pie-legend-label { color: #374151; flex: 1; } +.pie-legend-value { color: #6b7280; font-weight: 500; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; } + +/* Funnel Chart */ +.funnel-chart { display: flex; flex-direction: column; gap: 8px; } +.funnel-stage { display: flex; align-items: center; gap: 12px; } +.funnel-label-col { width: 100px; flex-shrink: 0; text-align: right; } +.funnel-label { display: block; font-size: 13px; font-weight: 500; color: #374151; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.funnel-value { display: block; font-size: 11px; color: #6b7280; font-family: 'SF Mono', 'Fira Code', monospace; } +.funnel-bar-col { flex: 1; } +.funnel-bar { height: 28px; border-radius: 6px; transition: width 0.4s ease; min-width: 4px; } +.funnel-dropoff { width: 50px; font-size: 11px; color: #ef4444; font-weight: 600; text-align: left; flex-shrink: 0; } + +/* Sparkline */ +.sparkline-svg { display: inline-block; vertical-align: middle; } +.sparkline-empty { display: inline-flex; align-items: center; justify-content: center; color: #9ca3af; font-size: 12px; vertical-align: middle; } + +/* ─── Interactive: Drag & Drop ────────────────────────── */ +.kanban-card[draggable] { cursor: grab; transition: transform 0.15s, box-shadow 0.15s, opacity 0.15s; } +.kanban-card[draggable]:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.12); transform: translateY(-1px); } +.kanban-card-dragging { opacity: 0.4; transform: scale(0.95); } +.kanban-drop-target { background: #eef2ff; border: 2px dashed #818cf8; border-radius: 6px; min-height: 40px; transition: background 0.15s; } + +/* ─── Interactive: Clickable Rows ─────────────────────── */ +.clickable-row { cursor: pointer; transition: background 0.1s; } +.clickable-row:hover { background: #f0f4ff !important; } + +/* ─── Modal ───────────────────────────────────────────── */ +.mcp-modal-overlay { + position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: flex; + align-items: center; justify-content: center; z-index: 1000; backdrop-filter: blur(2px); +} +.mcp-modal { + background: white; border-radius: 10px; width: 90%; max-width: 360px; + box-shadow: 0 8px 30px rgba(0,0,0,0.2); overflow: hidden; +} +.mcp-modal-header { + display: flex; align-items: center; justify-content: space-between; + padding: 10px 14px; border-bottom: 1px solid #e5e7eb; +} +.mcp-modal-title { font-size: 13px; font-weight: 600; color: #111827; } +.mcp-modal-close { background: none; border: none; font-size: 18px; cursor: pointer; color: #6b7280; padding: 0 4px; } +.mcp-modal-close:hover { color: #111827; } +.mcp-modal-body { padding: 12px 14px; } +.mcp-modal-footer { padding: 8px 14px; border-top: 1px solid #e5e7eb; display: flex; gap: 8px; justify-content: flex-end; } + +.mcp-field { margin-bottom: 10px; } +.mcp-field-label { display: block; font-size: 11px; font-weight: 500; color: #374151; margin-bottom: 3px; } +.mcp-field-input { + width: 100%; padding: 6px 8px; border: 1px solid #d1d5db; border-radius: 6px; + font-size: 12px; outline: none; transition: border-color 0.15s; +} +.mcp-field-input:focus { border-color: #4f46e5; box-shadow: 0 0 0 2px rgba(79,70,229,0.15); } + +/* ─── Toast ───────────────────────────────────────────── */ +.mcp-toast { + position: fixed; bottom: 12px; left: 50%; transform: translateX(-50%) translateY(20px); + padding: 6px 16px; border-radius: 8px; font-size: 11px; font-weight: 500; + z-index: 2000; opacity: 0; transition: opacity 0.2s, transform 0.2s; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); pointer-events: none; +} +.mcp-toast-show { opacity: 1; transform: translateX(-50%) translateY(0); } +.mcp-toast-success { background: #059669; color: white; } +.mcp-toast-error { background: #dc2626; color: white; } +.mcp-toast-info { background: #1f2937; color: white; } + +/* ─── Interactive Components ──────────────────────────── */ +.interactive-wrap { + background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); + overflow: hidden; margin-bottom: 8px; +} + +/* Contact Picker */ +.contact-picker { padding: 12px; } +.cp-search-wrap { position: relative; } +.cp-input { width: 100%; } +.cp-results { + position: absolute; top: 100%; left: 0; right: 0; z-index: 50; + background: white; border: 1px solid #e5e7eb; border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-height: 200px; overflow-y: auto; margin-top: 4px; +} +.cp-result-item { padding: 8px 12px; cursor: pointer; font-size: 13px; border-bottom: 1px solid #f3f4f6; } +.cp-result-item:hover { background: #f0f4ff; } +.cp-selected { + display: flex; align-items: center; justify-content: space-between; + padding: 6px 10px; background: #eef2ff; border-radius: 6px; margin-bottom: 8px; + font-size: 13px; font-weight: 500; color: #4f46e5; +} +.cp-clear { background: none; border: none; cursor: pointer; font-size: 16px; color: #6b7280; padding: 0 4px; } +.cp-clear:hover { color: #dc2626; } + +/* Invoice Builder */ +.invoice-builder .ib-items td { padding: 6px 4px; } +.invoice-builder .ib-items input { font-size: 12px; } +.invoice-builder .ib-line-total { font-family: 'SF Mono', monospace; font-weight: 500; text-align: right; padding-right: 8px; white-space: nowrap; } +.invoice-builder .ib-grand-total { font-size: 14px; } +.invoice-builder .ib-remove-row { color: #dc2626; } +.invoice-builder .ib-add-row { color: #4f46e5; } + +/* Opportunity Editor */ +.opportunity-editor select.mcp-field-input { + appearance: auto; padding: 6px 8px; background: white; +} + +/* Editable Field */ +.editable-field { + display: inline-flex; align-items: center; gap: 4px; cursor: pointer; + padding: 2px 4px; border-radius: 4px; transition: background 0.15s; +} +.editable-field:hover { background: #f3f4f6; } +.ef-edit-icon { font-size: 12px; opacity: 0; transition: opacity 0.15s; } +.editable-field:hover .ef-edit-icon { opacity: 0.6; } +.ef-input { font-size: inherit; } + +/* Amount Input */ +.amount-input { display: inline-flex; align-items: center; cursor: pointer; } +.ai-display { cursor: pointer; } +.ai-raw { font-size: 16px; width: 120px; } + +/* Select Dropdown */ +.select-dropdown { appearance: auto; background: white; } +`; diff --git a/src/ui/json-render-app/tsconfig.json b/src/ui/json-render-app/tsconfig.json new file mode 100644 index 0000000..6b201d0 --- /dev/null +++ b/src/ui/json-render-app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist" + }, + "include": ["src/**/*.ts", "*.ts"] +} diff --git a/src/ui/json-render-app/vite.config.ts b/src/ui/json-render-app/vite.config.ts new file mode 100644 index 0000000..bd9d907 --- /dev/null +++ b/src/ui/json-render-app/vite.config.ts @@ -0,0 +1,32 @@ +import { defineConfig } from "vite"; +import { viteSingleFile } from "vite-plugin-singlefile"; +import path from "path"; +import fs from "fs"; + +export default defineConfig({ + plugins: [ + viteSingleFile(), + { + name: 'copy-output', + closeBundle() { + const outDir = path.resolve(__dirname, "../../../dist/app-ui"); + const src = path.join(outDir, "index.html"); + const dstDynamic = path.join(outDir, "dynamic-view.html"); + const dstUniversal = path.join(outDir, "universal-renderer.html"); + if (fs.existsSync(src)) { + // Copy to both names — universal-renderer is the canonical one + fs.copyFileSync(src, dstUniversal); + fs.renameSync(src, dstDynamic); + } + } + } + ], + root: path.resolve(__dirname), + build: { + outDir: path.resolve(__dirname, "../../../dist/app-ui"), + emptyOutDir: false, + rollupOptions: { + input: path.resolve(__dirname, "index.html"), + }, + }, +}); diff --git a/src/ui/react-app/index.html b/src/ui/react-app/index.html new file mode 100644 index 0000000..465e2bf --- /dev/null +++ b/src/ui/react-app/index.html @@ -0,0 +1,12 @@ + + + + + + MCP UI Kit + + +
+ + + 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 ( + + + +
+
+
+

{toolInput}

+
+
+ + + + ); + } + + // 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

Error

{error.message}

; + if (!isConnected) return

Connecting...

; + if (!data) return

Waiting for data...

; + + 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

Error

{error.message}

; + } + if (!isConnected && !data) { + return

Connecting...

; + } + 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

Error

{error.message}

; + } + if (!isConnected && !data) { + return

Connecting...

; + } + 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

Connecting...

; + } + if (!data) { + return

Waiting for data...

; + } + + 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

Error

{error.message}

; + } + if (!isConnected && !data) { + return

Connecting...

; + } + 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

Error

{error.message}

; + } + if (!isConnected && !data) { + return

Connecting...

; + } + 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 ? ( +
📅

No events on this date

+ ) : ( +
+ {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

Error

{error.message}

; + if (!isConnected) return

Connecting...

; + if (!data) return

Waiting for data...

; + + 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

Error

{error.message}

; + if (!isConnected) return

Connecting...

; + if (!data) return

Waiting for data...

; + + 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

Connecting...

; + } + if (!data) { + return

Waiting for data...

; + } + + 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

Error

{error.message}

; + if (!isConnected) return

Connecting...

; + if (!data) return

Waiting for data...

; + + 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

Error

{error.message}

; + if (!isConnected) return

Connecting...

; + if (!data) return

Waiting for data...

; + + 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

Error

{error.message}

; + if (!isConnected) return

Connecting...

; + if (!data) return

Waiting for data...

; + + 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

Error

{error.message}

; + if (!isConnected) return

Connecting...

; + + return ( + + +
+ {FIELDS.map((f) => ( +
+ + setValue(f.key, e.target.value)} + required={f.required} + /> +
+ ))} + + {result === 'queued' && ( +
+ ✅ Contact creation request sent to the model. + {resultData && ID: {resultData.contact?.id || resultData.id}} +
+ )} + {result === 'error' && ( +
+ ❌ Failed to send request. Please try again. +
+ )} + +
+ + +
+
+
+
+ ); +} 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

Error

{error.message}

; + if (!isConnected) return

Connecting...

; + if (!data) return

Waiting for data...

; + + 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

Error

{error.message}

; + if (!isConnected) return

Connecting...

; + if (!data) return

Waiting for data...

; + + 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

Error

{error.message}

; + } + if (!isConnected && !data) { + return

Connecting...

; + } + 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

Error

{error.message}

; + } + if (!isConnected && !data) { + return

Connecting...

; + } + 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

Error

{error.message}

; + } + if (!isConnected && !data) { + return

Connecting...

; + } + if (!data) { + return

Waiting for data...

; + } + + 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

Error

{error.message}

; + if (!isConnected) return

Connecting...

; + if (!data) return

Waiting for data...

; + + 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

Error

{error.message}

; + if (!isConnected) return

Connecting...

; + if (!data) return

Waiting for data...

; + + 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 ? ( + + ) : ( +
📖

No lessons available

+ )} +
+
+
+
+ ); +} 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

Error

{error.message}

; + if (!isConnected) return

Connecting...

; + if (!data) return

Waiting for data...

; + + 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

Connecting...

; + } + if (!data) { + return

Waiting for data...

; + } + + 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

Error

{error.message}

; + if (!isConnected) return

Connecting...

; + + return ( +
+ + + +
+ + setTitle(e.target.value)} /> +
+
+ + setSelectedContact(c)} /> +
+
+ + setExpiryDate(e.target.value)} /> +
+
+ + + + + + {lineItems.map((item, idx) => ( + + + + + + + + ))} + +
DescriptionQtyPriceTotal
updateItem(idx, 'description', e.target.value)} placeholder="Item description" /> updateItem(idx, 'quantity', parseInt(e.target.value) || 0)} min={0} /> updateItem(idx, 'unitPrice', parseFloat(e.target.value) || 0)} min={0} step="0.01" />{formatCurrency(item.quantity * item.unitPrice, currency)}{lineItems.length > 1 && }
+ +
+
Total{formatCurrency(subtotal, currency)}
+
+
+ + +