From 79049c85a3719daa7a427c142966592d100c37f1 Mon Sep 17 00:00:00 2001 From: Jake Shore Date: Tue, 17 Feb 2026 23:03:48 -0500 Subject: [PATCH] Daily backup: 2026-02-17 --- HEARTBEAT.md | 35 +- SOUL.md | 1 + a2p-wizard-rebuild/ARCHITECTURE.md | 333 + a2p-wizard-rebuild/package.json | 21 + a2p-wizard-rebuild/public/css/form.css | 551 ++ a2p-wizard-rebuild/public/css/site.css | 737 ++ a2p-wizard-rebuild/public/js/form.js | 204 + a2p-wizard-rebuild/routes/api.js | 126 + a2p-wizard-rebuild/routes/sites.js | 52 + a2p-wizard-rebuild/server.js | 144 + a2p-wizard-rebuild/services/ai-generator.js | 112 + .../services/packet-assembler.js | 175 + a2p-wizard-rebuild/services/screenshot.js | 75 + a2p-wizard-rebuild/services/site-builder.js | 71 + a2p-wizard-rebuild/templates/form.ejs | 181 + .../templates/packet/results.ejs | 624 ++ a2p-wizard-rebuild/templates/site/contact.ejs | 125 + a2p-wizard-rebuild/templates/site/index.ejs | 172 + .../templates/site/privacy-policy.ejs | 68 + a2p-wizard-rebuild/templates/site/terms.ejs | 68 + closebot-sms/BUILD_PLAN.md | 665 -- closebot-sms/app/data/closebot-sms.db | Bin 4096 -> 0 bytes closebot-sms/app/data/closebot-sms.db-shm | Bin 32768 -> 0 bytes closebot-sms/app/data/closebot-sms.db-wal | Bin 313152 -> 0 bytes closebot-sms/app/next-env.d.ts | 5 - closebot-sms/app/next.config.js | 8 - closebot-sms/app/package.json | 34 - closebot-sms/app/postcss.config.js | 6 - closebot-sms/app/src/app/a2p/page.tsx | 349 - .../app/src/app/a2p/register/page.tsx | 995 --- closebot-sms/app/src/app/analytics/page.tsx | 172 - .../app/src/app/api/a2p/[id]/retry/route.ts | 51 - .../app/src/app/api/a2p/[id]/route.ts | 110 - .../app/src/app/api/a2p/[id]/status/route.ts | 46 - closebot-sms/app/src/app/api/a2p/route.ts | 78 - .../app/src/app/api/analytics/bots/route.ts | 52 - .../app/api/analytics/leaderboard/route.ts | 62 - .../src/app/api/analytics/messages/route.ts | 90 - .../src/app/api/analytics/outcomes/route.ts | 71 - .../src/app/api/analytics/overview/route.ts | 93 - .../app/src/app/api/bots/[id]/route.ts | 76 - closebot-sms/app/src/app/api/bots/route.ts | 207 - .../app/src/app/api/closebot/bots/route.ts | 16 - .../app/src/app/api/closebot/sources/route.ts | 16 - .../app/src/app/api/contacts/[id]/route.ts | 122 - .../app/src/app/api/contacts/export/route.ts | 99 - .../app/src/app/api/contacts/route.ts | 30 - .../api/conversations/[contactId]/route.ts | 23 - .../conversations/[contactId]/send/route.ts | 69 - .../app/src/app/api/conversations/route.ts | 19 - .../src/app/api/dashboard/activity/route.ts | 70 - .../app/api/dashboard/closebot-stats/route.ts | 45 - .../app/src/app/api/dashboard/stats/route.ts | 72 - .../api/landing-pages/[id]/download/route.ts | 29 - .../api/landing-pages/[id]/preview/route.ts | 20 - .../src/app/api/landing-pages/[id]/route.ts | 78 - .../landing-pages/generate-preview/route.ts | 28 - .../app/src/app/api/landing-pages/route.ts | 76 - .../src/app/api/phone-gateway/config/route.ts | 29 - .../src/app/api/phone-gateway/save/route.ts | 51 - .../src/app/api/phone-gateway/send/route.ts | 67 - .../src/app/api/phone-gateway/test/route.ts | 60 - .../app/src/app/api/routes/[id]/route.ts | 99 - closebot-sms/app/src/app/api/routes/route.ts | 49 - .../app/src/app/api/settings/route.ts | 44 - .../app/api/settings/test-connection/route.ts | 69 - .../src/app/api/twilio/numbers/[sid]/route.ts | 52 - .../src/app/api/twilio/numbers/buy/route.ts | 30 - .../app/src/app/api/twilio/numbers/route.ts | 12 - .../app/api/twilio/numbers/search/route.ts | 32 - .../api/webhooks/closebot/response/route.ts | 152 - .../app/api/webhooks/twilio/inbound/route.ts | 270 - .../app/api/webhooks/twilio/status/route.ts | 61 - closebot-sms/app/src/app/bots/page.tsx | 26 - .../app/src/app/connect-phone/page.tsx | 733 -- closebot-sms/app/src/app/contacts/page.tsx | 64 - .../app/src/app/conversations/page.tsx | 50 - .../app/src/app/landing-pages/create/page.tsx | 525 -- .../app/src/app/landing-pages/page.tsx | 246 - closebot-sms/app/src/app/layout.tsx | 31 - closebot-sms/app/src/app/page.tsx | 366 - .../app/src/app/phone-numbers/page.tsx | 764 -- closebot-sms/app/src/app/routing/page.tsx | 150 - closebot-sms/app/src/app/settings/page.tsx | 141 - .../components/analytics/bot-leaderboard.tsx | 147 - .../components/analytics/bots-bar-chart.tsx | 109 - .../components/analytics/messages-chart.tsx | 158 - .../components/analytics/outcome-donut.tsx | 176 - .../analytics/stat-card-sparkline.tsx | 210 - .../app/src/components/bots/bot-card.tsx | 258 - .../app/src/components/bots/bot-grid.tsx | 275 - .../contacts/contact-detail-panel.tsx | 332 - .../components/contacts/contact-filters.tsx | 266 - .../components/contacts/contacts-table.tsx | 248 - .../components/conversations/chat-thread.tsx | 267 - .../conversations/conversation-list.tsx | 198 - .../conversations/message-bubble.tsx | 72 - .../app/src/components/layout/sidebar.tsx | 105 - .../src/components/routing/bot-route-card.tsx | 141 - .../components/routing/connection-lines.tsx | 226 - .../components/routing/phone-number-card.tsx | 83 - .../components/routing/route-config-modal.tsx | 309 - .../src/components/routing/routing-view.tsx | 441 -- .../src/components/settings/closebot-card.tsx | 197 - .../settings/notifications-card.tsx | 112 - .../settings/phone-gateway-card.tsx | 202 - .../settings/phone-numbers-card.tsx | 189 - .../src/components/settings/twilio-card.tsx | 170 - .../components/settings/webhook-urls-card.tsx | 114 - closebot-sms/app/src/lib/a2p-types.ts | 250 - closebot-sms/app/src/lib/closebot.ts | 98 - closebot-sms/app/src/lib/db.ts | 563 -- .../app/src/lib/landing-page-generator.ts | 571 -- closebot-sms/app/src/lib/sse.ts | 33 - closebot-sms/app/src/lib/twilio-client.ts | 183 - closebot-sms/app/src/lib/utils.ts | 42 - closebot-sms/app/src/styles/globals.css | 61 - closebot-sms/app/tailwind.config.ts | 22 - closebot-sms/app/tsconfig.json | 21 - closebot-sms/app/tsconfig.tsbuildinfo | 1 - das-surya/lyrics/01_skin_intro.txt | 1 - das-surya/lyrics/02_u_saved_me.txt | 1 - das-surya/lyrics/03_nothing.txt | 1 - das-surya/lyrics/04_sweet_relief.txt | 1 - das-surya/lyrics/05_tiptoe.txt | 1 - das-surya/lyrics/06_natures_call.txt | 1 - das-surya/lyrics/07_dreamcatcher.txt | 1 - das-surya/lyrics/08_idk.txt | 1 - das-surya/lyrics/09_with_u.txt | 1 - das-surya/lyrics/10_poor_you_poor_me.txt | 1 - das-surya/lyrics/11_wait_4_u.txt | 1 - das-surya/lyrics/12_run_to_u.txt | 1 - das-surya/lyrics/13_medications.txt | 1 - das-surya/lyrics/14_hollow.txt | 1 - goosefactory/.gitignore | 31 - goosefactory/BUILD-STATUS.md | 71 - goosefactory/CONTRACTS.md | 3467 --------- goosefactory/README.md | 250 - goosefactory/REBRAND-PLAN.md | 234 - goosefactory/SEED_REPORT.md | 154 - goosefactory/infra/db/README.md | 22 - .../infra/db/seed/seed-agents-and-tasks.sql | 330 - .../infra/db/seed/seed-from-factory.sql | 345 - goosefactory/infra/docker/README.md | 17 - goosefactory/infra/docker/docker-compose.yml | 43 - goosefactory/infra/docker/init.sql | 212 - goosefactory/package.json | 41 - goosefactory/packages/api/README.md | 34 - goosefactory/packages/api/drizzle.config.ts | 10 - goosefactory/packages/api/package.json | 40 - goosefactory/packages/api/src/db/index.ts | 16 - .../src/db/migrations/0000_tough_warhawk.sql | 184 - .../src/db/migrations/meta/0000_snapshot.json | 1487 ---- .../api/src/db/migrations/meta/_journal.json | 13 - goosefactory/packages/api/src/db/schema.ts | 424 - goosefactory/packages/api/src/env.ts | 5 - goosefactory/packages/api/src/events/bus.ts | 166 - .../api/src/events/consumers/audit-writer.ts | 35 - .../events/consumers/notification-worker.ts | 41 - .../api/src/events/consumers/sla-monitor.ts | 14 - .../src/events/consumers/ws-broadcaster.ts | 13 - goosefactory/packages/api/src/events/types.ts | 137 - goosefactory/packages/api/src/index.ts | 156 - .../packages/api/src/middleware/audit.ts | 57 - .../packages/api/src/middleware/auth.ts | 119 - .../packages/api/src/middleware/rbac.ts | 84 - .../packages/api/src/routes/agents.ts | 104 - .../packages/api/src/routes/approvals.ts | 86 - .../packages/api/src/routes/assets.ts | 195 - goosefactory/packages/api/src/routes/audit.ts | 44 - .../packages/api/src/routes/feedback.ts | 106 - .../packages/api/src/routes/notifications.ts | 41 - .../packages/api/src/routes/pipelines.ts | 299 - goosefactory/packages/api/src/routes/tasks.ts | 233 - .../packages/api/src/routes/webhooks.ts | 64 - .../api/src/services/agent.service.ts | 154 - .../api/src/services/approval.service.ts | 200 - .../api/src/services/asset.service.ts | 220 - .../api/src/services/escalation.service.ts | 288 - .../api/src/services/notification.service.ts | 192 - .../api/src/services/pipeline.service.ts | 459 -- .../packages/api/src/services/task.service.ts | 377 - .../api/src/services/webhook.service.ts | 127 - goosefactory/packages/api/src/types/index.ts | 260 - goosefactory/packages/api/src/ws/server.ts | 125 - goosefactory/packages/api/tsconfig.json | 24 - goosefactory/packages/desktop | 1 - .../packages/discord-bot/STARTUP-GUIDE.md | 555 -- .../packages/discord-bot/package.json | 26 - .../packages/discord-bot/src/api-client.ts | 374 - .../discord-bot/src/commands/approve.ts | 68 - .../discord-bot/src/commands/blockers.ts | 75 - .../discord-bot/src/commands/deploy.ts | 78 - .../discord-bot/src/commands/pipelines.ts | 57 - .../discord-bot/src/commands/queue.ts | 125 - .../discord-bot/src/commands/reject.ts | 76 - .../discord-bot/src/commands/status.ts | 93 - .../packages/discord-bot/src/config.ts | 100 - .../src/embeds/approval-request.ts | 182 - .../discord-bot/src/embeds/batch-review.ts | 156 - .../src/embeds/deploy-notification.ts | 105 - .../discord-bot/src/embeds/pipeline-status.ts | 167 - .../discord-bot/src/embeds/sla-warning.ts | 126 - .../src/interactions/button-handler.ts | 308 - .../src/interactions/modal-handler.ts | 67 - .../src/interactions/select-handler.ts | 54 - .../packages/discord-bot/tsconfig.json | 19 - .../learning/LEARNING_PIPELINE_REPORT.md | 193 - goosefactory/packages/learning/README.md | 248 - goosefactory/packages/learning/data/.gitkeep | 0 .../learning/memory/feedback-patterns.md | 13 - .../learning/memory/jake-preferences.md | 22 - .../learning/memory/quality-standards.md | 17 - goosefactory/packages/learning/package.json | 27 - .../learning/src/analysis/behavioral.ts | 292 - .../learning/src/analysis/calibration.ts | 190 - .../learning/src/analysis/dimensions.ts | 186 - .../learning/src/analysis/patterns.ts | 363 - .../learning/src/analysis/preferences.ts | 194 - .../src/improvement/diminishing-review.ts | 368 - .../learning/src/improvement/pre-check.ts | 330 - .../learning/src/improvement/prediction.ts | 330 - .../learning/src/improvement/regression.ts | 342 - goosefactory/packages/learning/src/index.ts | 277 - .../learning/src/memory/compressor.ts | 207 - .../packages/learning/src/memory/reader.ts | 169 - .../packages/learning/src/memory/writer.ts | 313 - .../packages/learning/src/metrics/kpis.ts | 334 - .../packages/learning/src/pipeline/act.ts | 105 - .../packages/learning/src/pipeline/analyze.ts | 89 - .../packages/learning/src/pipeline/enrich.ts | 357 - .../packages/learning/src/pipeline/index.ts | 165 - .../packages/learning/src/pipeline/store.ts | 32 - .../packages/learning/src/pipeline/types.ts | 98 - .../learning/src/pipeline/validate.ts | 114 - goosefactory/packages/learning/src/service.ts | 166 - .../learning/src/storage/feedback-store.ts | 171 - .../packages/learning/src/storage/query.ts | 175 - .../packages/learning/src/storage/stats.ts | 249 - goosefactory/packages/learning/src/types.ts | 649 -- .../packages/learning/start-service.sh | 70 - .../packages/learning/test-feedback.json | 74 - goosefactory/packages/learning/tsconfig.json | 22 - goosefactory/packages/mcp-server/README.md | 35 - goosefactory/packages/mcp-server/package.json | 26 - .../packages/mcp-server/src/api-client.ts | 391 - goosefactory/packages/mcp-server/src/index.ts | 181 - .../packages/mcp-server/src/modals/host.ts | 645 -- .../src/prompts/deploy-checklist.ts | 146 - .../mcp-server/src/prompts/needs-attention.ts | 158 - .../mcp-server/src/prompts/retrospective.ts | 185 - .../mcp-server/src/prompts/review-server.ts | 135 - .../mcp-server/src/resources/config.ts | 131 - .../mcp-server/src/resources/dashboard.ts | 49 - .../mcp-server/src/resources/pipelines.ts | 249 - .../mcp-server/src/tools/operations.ts | 281 - .../mcp-server/src/tools/pipelines.ts | 350 - .../packages/mcp-server/src/tools/review.ts | 122 - .../packages/mcp-server/src/tools/tasks.ts | 230 - goosefactory/packages/mcp-server/src/types.ts | 331 - .../packages/mcp-server/tsconfig.json | 19 - goosefactory/packages/modals/README.md | 146 - goosefactory/packages/modals/package.json | 12 - .../packages/modals/src/applause-meter.html | 804 -- .../packages/modals/src/before-after.html | 711 -- .../modals/src/checklist-ceremony.html | 649 -- .../packages/modals/src/confidence-meter.html | 580 -- .../packages/modals/src/crystal-ball.html | 828 -- .../packages/modals/src/decision-tree.html | 700 -- .../packages/modals/src/emoji-scale.html | 511 -- .../packages/modals/src/hot-take.html | 561 -- .../packages/modals/src/judges-scorecard.html | 691 -- .../packages/modals/src/mission-briefing.html | 580 -- .../packages/modals/src/mood-ring.html | 637 -- .../packages/modals/src/preview/index.html | 584 -- .../packages/modals/src/priority-poker.html | 607 -- .../packages/modals/src/quick-pulse.html | 476 -- .../packages/modals/src/ranking-arena.html | 643 -- .../packages/modals/src/report-card.html | 509 -- .../modals/src/retrospective-board.html | 700 -- .../packages/modals/src/shared/modal-utils.js | 224 - .../packages/modals/src/side-by-side.html | 637 -- .../packages/modals/src/slot-machine.html | 641 -- .../packages/modals/src/speed-round.html | 658 -- .../packages/modals/src/spotlight.html | 697 -- .../packages/modals/src/thermometer.html | 578 -- .../packages/modals/src/tinder-swipe.html | 739 -- .../packages/modals/src/traffic-light.html | 552 -- .../modals/src/voice-of-customer.html | 567 -- .../packages/modals/src/war-room.html | 887 --- goosefactory/packages/shared/README.md | 42 - goosefactory/packages/shared/package.json | 37 - .../packages/shared/src/constants/errors.ts | 31 - .../packages/shared/src/constants/index.ts | 28 - .../packages/shared/src/constants/sla.ts | 41 - .../packages/shared/src/constants/stages.ts | 72 - goosefactory/packages/shared/src/index.ts | 15 - .../packages/shared/src/integration/index.ts | 1 - .../src/integration/modal-to-learning.ts | 112 - .../shared/src/schemas/feedback.schema.ts | 207 - .../packages/shared/src/schemas/index.ts | 49 - .../shared/src/schemas/pipeline.schema.ts | 36 - .../shared/src/schemas/task.schema.ts | 41 - .../packages/shared/src/types/agent.ts | 29 - goosefactory/packages/shared/src/types/api.ts | 86 - .../packages/shared/src/types/approval.ts | 24 - .../packages/shared/src/types/asset.ts | 27 - .../packages/shared/src/types/audit.ts | 16 - .../packages/shared/src/types/feedback.ts | 400 - .../packages/shared/src/types/index.ts | 153 - .../packages/shared/src/types/notification.ts | 33 - .../packages/shared/src/types/pipeline.ts | 105 - .../packages/shared/src/types/task.ts | 101 - goosefactory/packages/shared/src/types/ws.ts | 175 - goosefactory/packages/shared/tsconfig.json | 20 - goosefactory/scripts/first-boot.sh | 203 - goosefactory/scripts/seed-from-factory.ts | 218 - goosefactory/scripts/test-mcp-tools.sh | 280 - .../integration/modal-to-learning.test.ts | 180 - goosefactory/tsconfig.base.json | 17 - house-remodel/proposal.html | 2196 ++++++ localbosses-app/.gitignore | 49 +- localbosses-app/next-env.d.ts | 6 + localbosses-app/tsconfig.tsbuildinfo | 1 + manim-mcp | 1 - mcp-command-center/state.json | 4 +- mcp-diagrams/GHL-MCP-Funnel/README.md | 24 - .../GHL-MCP-Funnel/functions/api/waitlist.js | 75 - mcp-diagrams/GHL-MCP-Funnel/index.html | 598 -- mcp-diagrams/GoHighLevel-MCP | 1 - mcp-diagrams/audit-report.json | 2477 ------ mcp-diagrams/audit-servers.js | 144 - mcp-diagrams/fix-servers.js | 162 - mcp-diagrams/ghl-mcp-apps-only/package.json | 22 - .../ghl-mcp-apps-only/src/apps/index.ts | 824 -- .../src/clients/ghl-api-client.ts | 6858 ----------------- mcp-diagrams/ghl-mcp-apps-only/src/server.ts | 172 - .../ghl-mcp-apps-only/src/types/ghl-types.ts | 6688 ---------------- mcp-diagrams/ghl-mcp-apps-only/tsconfig.json | 16 - mcp-diagrams/ghl-mcp-public | 1 - mcp-diagrams/google-ads-mcp/PLAN.md | 363 - mcp-diagrams/google-ads-mcp/README.md | 142 - mcp-diagrams/google-ads-mcp/package.json | 26 - .../src/apps/budget-optimizer.ts | 256 - .../src/apps/campaign-dashboard.ts | 136 - .../src/apps/campaign-detail.ts | 163 - mcp-diagrams/google-ads-mcp/src/apps/index.ts | 27 - .../src/apps/keyword-analyzer.ts | 142 - .../src/apps/performance-overview.ts | 146 - .../src/apps/recommendations.ts | 151 - .../google-ads-mcp/src/apps/search-terms.ts | 126 - mcp-diagrams/google-ads-mcp/src/apps/theme.ts | 380 - mcp-diagrams/google-ads-mcp/src/auth.ts | 107 - mcp-diagrams/google-ads-mcp/src/client.ts | 182 - mcp-diagrams/google-ads-mcp/src/index.ts | 170 - .../google-ads-mcp/src/tools/accounts.ts | 385 - .../google-ads-mcp/src/tools/ad-groups.ts | 571 -- mcp-diagrams/google-ads-mcp/src/tools/ads.ts | 628 -- .../google-ads-mcp/src/tools/advanced.ts | 743 -- .../google-ads-mcp/src/tools/bidding.ts | 536 -- .../google-ads-mcp/src/tools/campaigns.ts | 789 -- .../google-ads-mcp/src/tools/conversions.ts | 682 -- .../google-ads-mcp/src/tools/index.ts | 183 - .../google-ads-mcp/src/tools/keywords.ts | 646 -- .../google-ads-mcp/src/tools/reporting.ts | 887 --- mcp-diagrams/google-ads-mcp/src/types.ts | 246 - mcp-diagrams/google-ads-mcp/tsconfig.json | 16 - mcp-diagrams/google-ads-mcp/tsup.config.ts | 13 - mcp-diagrams/google-ads-mcp/ui/build-all.sh | 21 - mcp-diagrams/google-ads-mcp/ui/package.json | 31 - .../ui/src/apps/budget-optimizer/App.tsx | 230 - .../ui/src/apps/budget-optimizer/index.html | 5 - .../ui/src/apps/budget-optimizer/main.tsx | 4 - .../src/apps/budget-optimizer/vite.config.ts | 20 - .../ui/src/apps/campaign-dashboard/App.tsx | 220 - .../ui/src/apps/campaign-dashboard/index.html | 5 - .../ui/src/apps/campaign-dashboard/main.tsx | 4 - .../apps/campaign-dashboard/vite.config.ts | 20 - .../ui/src/apps/campaign-detail/App.tsx | 220 - .../ui/src/apps/campaign-detail/index.html | 5 - .../ui/src/apps/campaign-detail/main.tsx | 4 - .../src/apps/campaign-detail/vite.config.ts | 20 - .../ui/src/apps/keyword-analyzer/App.tsx | 209 - .../ui/src/apps/keyword-analyzer/index.html | 5 - .../ui/src/apps/keyword-analyzer/main.tsx | 4 - .../src/apps/keyword-analyzer/vite.config.ts | 20 - .../ui/src/apps/performance-overview/App.tsx | 176 - .../src/apps/performance-overview/index.html | 5 - .../ui/src/apps/performance-overview/main.tsx | 4 - .../apps/performance-overview/vite.config.ts | 20 - .../ui/src/apps/recommendations/App.tsx | 223 - .../ui/src/apps/recommendations/index.html | 5 - .../ui/src/apps/recommendations/main.tsx | 4 - .../src/apps/recommendations/vite.config.ts | 20 - .../ui/src/apps/search-terms/App.tsx | 212 - .../ui/src/apps/search-terms/index.html | 5 - .../ui/src/apps/search-terms/main.tsx | 4 - .../ui/src/apps/search-terms/vite.config.ts | 20 - .../ui/src/components/charts/BarChart.tsx | 113 - .../ui/src/components/charts/FunnelChart.tsx | 56 - .../ui/src/components/charts/LineChart.tsx | 136 - .../ui/src/components/charts/PieChart.tsx | 93 - .../src/components/charts/SparklineChart.tsx | 52 - .../ui/src/components/comms/ChatThread.tsx | 122 - .../src/components/comms/ContentPreview.tsx | 66 - .../ui/src/components/comms/EmailPreview.tsx | 66 - .../src/components/comms/TranscriptView.tsx | 79 - .../ui/src/components/data/AudioPlayer.tsx | 52 - .../ui/src/components/data/AvatarGroup.tsx | 79 - .../ui/src/components/data/CardGrid.tsx | 57 - .../ui/src/components/data/ChecklistView.tsx | 150 - .../src/components/data/CurrencyDisplay.tsx | 41 - .../ui/src/components/data/DataTable.tsx | 187 - .../ui/src/components/data/DetailHeader.tsx | 31 - .../ui/src/components/data/InfoBlock.tsx | 20 - .../ui/src/components/data/KanbanBoard.tsx | 229 - .../ui/src/components/data/KeyValueList.tsx | 35 - .../ui/src/components/data/LineItemsTable.tsx | 46 - .../ui/src/components/data/MetricCard.tsx | 30 - .../ui/src/components/data/ProgressBar.tsx | 51 - .../ui/src/components/data/StarRating.tsx | 59 - .../ui/src/components/data/StatusBadge.tsx | 21 - .../ui/src/components/data/StockIndicator.tsx | 38 - .../ui/src/components/data/TagList.tsx | 48 - .../ui/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 - .../ui/src/components/layout/Card.tsx | 29 - .../ui/src/components/layout/PageHeader.tsx | 64 - .../ui/src/components/layout/Section.tsx | 20 - .../ui/src/components/layout/SplitLayout.tsx | 29 - .../ui/src/components/layout/StatsGrid.tsx | 14 - .../ui/src/components/shared/ActionBar.tsx | 16 - .../ui/src/components/shared/ActionButton.tsx | 66 - .../ui/src/components/shared/FilterChips.tsx | 46 - .../ui/src/components/shared/Modal.tsx | 65 - .../src/components/shared/SaveIndicator.tsx | 76 - .../ui/src/components/shared/SearchBar.tsx | 43 - .../ui/src/components/shared/TabGroup.tsx | 44 - .../ui/src/components/shared/Toast.tsx | 141 - .../ui/src/components/viz/CalendarView.tsx | 158 - .../src/components/viz/DuplicateCompare.tsx | 56 - .../ui/src/components/viz/FlowDiagram.tsx | 113 - .../ui/src/components/viz/MediaGallery.tsx | 77 - .../ui/src/components/viz/TreeView.tsx | 65 - .../google-ads-mcp/ui/src/styles/base.css | 897 --- .../ui/src/styles/google-ads-theme.css | 225 - .../ui/src/styles/interactive.css | 275 - mcp-diagrams/google-ads-mcp/ui/src/types.ts | 751 -- mcp-diagrams/google-ads-mcp/ui/tsconfig.json | 22 - .../mcp-animation-framework/README.md | 47 - .../capture-animation.js | 72 - .../mcp-animation-framework/capture-demo.js | 55 - .../capture-full-flow.js | 54 - .../capture-scroll-v2.js | 66 - .../mcp-animation-framework/capture-scroll.js | 79 - .../capture-template.js | 66 - .../mcp-animation-framework/capture-v4.js | 60 - .../mcp-animation-framework/capture-v5.js | 78 - .../mcp-animation-framework/capture-v6.js | 74 - .../capture-web-embed.js | 57 - .../configs/servicetitan-dispatch.json | 25 - .../mcp-animation-framework/gen-three.js | 452 -- .../mcp-animation-framework/generate-all.js | 448 -- .../mcp-animation-framework/generate-batch.js | 458 -- .../mcp-animation-framework/generate-fast.js | 103 - .../generate-new-only.js | 234 - .../generate-single.js | 393 - .../mcp-animation-framework/generate.js | 395 - .../landing-pages/ghl-reference.html | 598 -- .../landing-pages/site-generator.js | 820 -- .../landing-pages/sites/acuity.html | 654 -- .../landing-pages/sites/bamboohr.html | 666 -- .../landing-pages/sites/basecamp.html | 631 -- .../landing-pages/sites/bigcommerce.html | 645 -- .../landing-pages/sites/brevo.html | 653 -- .../landing-pages/sites/calendly.html | 610 -- .../landing-pages/sites/clickup.html | 651 -- .../landing-pages/sites/closecrm.html | 781 -- .../landing-pages/sites/clover.html | 649 -- .../landing-pages/sites/constantcontact.html | 662 -- .../landing-pages/sites/fieldedge.html | 643 -- .../landing-pages/sites/freshbooks.html | 692 -- .../landing-pages/sites/freshdesk.html | 627 -- .../landing-pages/sites/gusto.html | 603 -- .../landing-pages/sites/helpscout.html | 637 -- .../landing-pages/sites/housecallpro.html | 640 -- .../landing-pages/sites/jobber.html | 762 -- .../landing-pages/sites/keap.html | 661 -- .../landing-pages/sites/lightspeed.html | 647 -- .../landing-pages/sites/mailchimp.html | 620 -- .../landing-pages/sites/pipedrive.html | 637 -- .../landing-pages/sites/rippling.html | 763 -- .../landing-pages/sites/servicetitan.html | 708 -- .../landing-pages/sites/squarespace.html | 652 -- .../landing-pages/sites/toast.html | 648 -- .../landing-pages/sites/touchbistro.html | 672 -- .../landing-pages/sites/trello.html | 624 -- .../landing-pages/sites/wave.html | 769 -- .../landing-pages/sites/wrike.html | 620 -- .../landing-pages/sites/zendesk.html | 620 -- .../mcp-animation-framework/mcp-configs.js | 1062 --- .../output/acuity.html | 364 - .../output/bamboohr.html | 364 - .../output/basecamp.html | 364 - .../output/bigcommerce.html | 364 - .../mcp-animation-framework/output/brevo.html | 364 - .../output/calendly.html | 364 - .../output/clickup.html | 364 - .../output/closecrm.html | 364 - .../output/clover.html | 364 - .../output/constantcontact.html | 364 - .../output/fieldedge.html | 364 - .../output/freshbooks.html | 364 - .../output/freshdesk.html | 364 - .../mcp-animation-framework/output/gusto.html | 364 - .../output/helpscout.html | 364 - .../output/housecallpro.html | 364 - .../output/jobber.html | 364 - .../mcp-animation-framework/output/keap.html | 364 - .../output/lightspeed.html | 364 - .../output/mailchimp.html | 364 - .../output/mcp-demo.html | 451 -- .../output/pipedrive.html | 364 - .../output/rippling.html | 364 - .../servicetitan-dispatch/frame-01-empty.html | 258 - .../frame-02-typing-user.html | 266 - .../frame-02-typing.html | 246 - .../frame-03-exchange.html | 262 - .../frame-03-first-exchange.html | 247 - .../frame-04-typing-ai.html | 270 - .../frame-04-typing-second.html | 247 - .../frame-05-loading.html | 282 - .../servicetitan-dispatch/frame-06-final.html | 336 - .../output/servicetitan.html | 364 - .../output/squarespace.html | 364 - .../output/stripe-animation.html | 440 -- .../output/stripe-full-flow.html | 552 -- .../output/stripe-scroll-v2.html | 842 -- .../output/stripe-scroll.html | 738 -- .../output/stripe-template.html | 739 -- .../output/stripe-v4.html | 680 -- .../output/stripe-v5.html | 705 -- .../output/stripe-v6.html | 719 -- .../mcp-animation-framework/output/toast.html | 364 - .../output/touchbistro.html | 364 - .../output/trello.html | 364 - .../mcp-animation-framework/output/wave.html | 364 - .../mcp-animation-framework/output/wrike.html | 364 - .../output/zendesk.html | 364 - .../mcp-animation-framework/package.json | 16 - .../web-embed/stripe-simulation-v2.html | 558 -- .../web-embed/stripe-simulation-v3.html | 574 -- .../web-embed/stripe-simulation.html | 542 -- mcp-diagrams/mcp-business-projections.md | 630 -- .../mcp-chat-animation/frame-01-empty.html | 91 - .../mcp-chat-animation/frame-02-typing.html | 99 - .../frame-03-first-exchange.html | 112 - .../frame-04-typing-second.html | 119 - .../frame-05-second-exchange-loading.html | 144 - .../frame-06-full-request.html | 173 - .../frame-07-final-loaded.html | 314 - .../mcp-chat-animation/template-large.html | 314 - .../mcp-chat-animation/template-normal.html | 173 - mcp-diagrams/mcp-chat-animation/template.html | 482 -- mcp-diagrams/mcp-chat-remotion/package.json | 20 - .../src/MCPChatAnimation.tsx | 932 --- mcp-diagrams/mcp-chat-remotion/src/Root.tsx | 35 - .../src/StripeCameraDemo.tsx | 518 -- .../mcp-chat-remotion/src/StripeDollyDemo.tsx | 535 -- mcp-diagrams/mcp-chat-remotion/src/index.ts | 4 - mcp-diagrams/mcp-chat-remotion/tsconfig.json | 15 - mcp-diagrams/mcp-combos-chibi.html | 585 -- mcp-diagrams/mcp-combos-graphic-v2.html | 502 -- mcp-diagrams/mcp-combos-graphic.html | 398 - mcp-diagrams/mcp-competitive-landscape.md | 160 - mcp-diagrams/mcp-pricing-research.md | 241 - .../acuity-scheduling/package.json | 20 - .../acuity-scheduling/src/index.ts | 292 - .../acuity-scheduling/tsconfig.json | 15 - .../mcp-servers/bamboohr/package.json | 20 - .../mcp-servers/bamboohr/src/index.ts | 331 - .../mcp-servers/bamboohr/tsconfig.json | 15 - .../mcp-servers/basecamp/package.json | 20 - .../mcp-servers/basecamp/src/index.ts | 321 - .../mcp-servers/basecamp/tsconfig.json | 15 - .../mcp-servers/bigcommerce/package.json | 20 - .../mcp-servers/bigcommerce/src/index.ts | 421 - .../mcp-servers/bigcommerce/tsconfig.json | 15 - mcp-diagrams/mcp-servers/brevo/package.json | 20 - mcp-diagrams/mcp-servers/brevo/src/index.ts | 401 - mcp-diagrams/mcp-servers/brevo/tsconfig.json | 15 - .../mcp-servers/calendly/package.json | 20 - .../mcp-servers/calendly/src/index.ts | 279 - .../mcp-servers/calendly/tsconfig.json | 15 - mcp-diagrams/mcp-servers/clickup/package.json | 20 - mcp-diagrams/mcp-servers/clickup/src/index.ts | 512 -- .../mcp-servers/clickup/tsconfig.json | 15 - mcp-diagrams/mcp-servers/close/package.json | 20 - mcp-diagrams/mcp-servers/close/src/index.ts | 484 -- mcp-diagrams/mcp-servers/close/tsconfig.json | 15 - mcp-diagrams/mcp-servers/clover/README.md | 95 - mcp-diagrams/mcp-servers/clover/package.json | 20 - mcp-diagrams/mcp-servers/clover/src/index.ts | 357 - mcp-diagrams/mcp-servers/clover/tsconfig.json | 15 - .../mcp-servers/constant-contact/package.json | 20 - .../mcp-servers/constant-contact/src/index.ts | 415 - .../constant-contact/tsconfig.json | 15 - mcp-diagrams/mcp-servers/fieldedge/README.md | 101 - .../mcp-servers/fieldedge/package.json | 20 - .../mcp-servers/fieldedge/src/index.ts | 399 - .../mcp-servers/fieldedge/tsconfig.json | 15 - .../mcp-servers/freshbooks/package.json | 20 - .../mcp-servers/freshbooks/src/index.ts | 453 -- .../mcp-servers/freshbooks/tsconfig.json | 15 - .../mcp-servers/freshdesk/package.json | 20 - .../mcp-servers/freshdesk/src/index.ts | 400 - .../mcp-servers/freshdesk/tsconfig.json | 15 - mcp-diagrams/mcp-servers/gusto/package.json | 20 - mcp-diagrams/mcp-servers/gusto/src/index.ts | 286 - mcp-diagrams/mcp-servers/gusto/tsconfig.json | 15 - .../mcp-servers/helpscout/package.json | 20 - .../mcp-servers/helpscout/src/index.ts | 341 - .../mcp-servers/helpscout/tsconfig.json | 15 - .../mcp-servers/housecall-pro/README.md | 87 - .../mcp-servers/housecall-pro/package.json | 20 - .../mcp-servers/housecall-pro/src/index.ts | 393 - .../mcp-servers/housecall-pro/tsconfig.json | 15 - mcp-diagrams/mcp-servers/jobber/package.json | 20 - mcp-diagrams/mcp-servers/jobber/src/index.ts | 524 -- mcp-diagrams/mcp-servers/jobber/tsconfig.json | 15 - mcp-diagrams/mcp-servers/keap/package.json | 20 - mcp-diagrams/mcp-servers/keap/src/index.ts | 438 -- mcp-diagrams/mcp-servers/keap/tsconfig.json | 15 - .../mcp-servers/lightspeed/package.json | 20 - .../mcp-servers/lightspeed/src/index.ts | 337 - .../mcp-servers/lightspeed/tsconfig.json | 15 - .../mcp-servers/mailchimp/package.json | 20 - .../mcp-servers/mailchimp/src/index.ts | 384 - .../mcp-servers/mailchimp/tsconfig.json | 15 - .../mcp-servers/pipedrive/package.json | 20 - .../mcp-servers/pipedrive/src/index.ts | 335 - .../mcp-servers/pipedrive/tsconfig.json | 15 - mcp-diagrams/mcp-servers/rippling/README.md | 119 - .../mcp-servers/rippling/package.json | 20 - .../mcp-servers/rippling/src/index.ts | 361 - .../mcp-servers/rippling/tsconfig.json | 15 - .../mcp-servers/servicetitan/README.md | 109 - .../mcp-servers/servicetitan/package.json | 20 - .../mcp-servers/servicetitan/src/index.ts | 400 - .../mcp-servers/servicetitan/tsconfig.json | 15 - .../mcp-servers/squarespace/package.json | 20 - .../mcp-servers/squarespace/src/index.ts | 286 - .../mcp-servers/squarespace/tsconfig.json | 15 - .../mcp-servers/template/package.json | 20 - .../mcp-servers/template/src/index.ts | 209 - .../mcp-servers/template/tsconfig.json | 15 - mcp-diagrams/mcp-servers/toast/package.json | 20 - mcp-diagrams/mcp-servers/toast/src/index.ts | 418 - mcp-diagrams/mcp-servers/toast/tsconfig.json | 15 - .../mcp-servers/touchbistro/README.md | 118 - .../mcp-servers/touchbistro/package.json | 20 - .../mcp-servers/touchbistro/src/index.ts | 394 - .../mcp-servers/touchbistro/tsconfig.json | 15 - mcp-diagrams/mcp-servers/trello/package.json | 20 - mcp-diagrams/mcp-servers/trello/src/index.ts | 432 -- mcp-diagrams/mcp-servers/trello/tsconfig.json | 15 - mcp-diagrams/mcp-servers/wave/package.json | 20 - mcp-diagrams/mcp-servers/wave/src/index.ts | 552 -- mcp-diagrams/mcp-servers/wave/tsconfig.json | 15 - mcp-diagrams/mcp-servers/wrike/package.json | 20 - mcp-diagrams/mcp-servers/wrike/src/index.ts | 378 - mcp-diagrams/mcp-servers/wrike/tsconfig.json | 15 - mcp-diagrams/mcp-servers/zendesk/package.json | 20 - mcp-diagrams/mcp-servers/zendesk/src/index.ts | 362 - .../mcp-servers/zendesk/tsconfig.json | 15 - memory/2026-02-17.md | 240 +- memory/lessons-learned.md | 3 + memory/working-state.md | 22 +- mixed-use-entertainment-intel.md | 36 + openclaw-gallery/UPWORK_REFERENCE.md | 188 - .../pdfs/openclaw-capabilities.md | 194 - openclaw-gallery/pdfs/openclaw-packages.md | 200 - .../video/openclaw-promo/UPGRADE_SPEC.md | 31 - .../video/openclaw-promo/package.json | 27 - .../video/openclaw-promo/remotion.config.ts | 4 - .../openclaw-promo/src/OpenClawPromo.tsx | 147 - .../video/openclaw-promo/src/Root.tsx | 18 - .../src/components/AnimatedNumber.tsx | 38 - .../src/components/CanvasViewport.tsx | 67 - .../src/components/ChannelIcons.tsx | 58 - .../src/components/DrawLine.tsx | 45 - .../src/components/FadeSlideIn.tsx | 46 - .../src/components/GlassCard.tsx | 131 - .../src/components/KineticText.tsx | 142 - .../src/components/MeshBackground.tsx | 100 - .../src/components/ParticleField.tsx | 97 - .../src/components/StaggeredGrid.tsx | 51 - .../src/components/TypewriterText.tsx | 43 - .../video/openclaw-promo/src/index.ts | 4 - .../openclaw-promo/src/scenes/Scene10Cta.tsx | 199 - .../openclaw-promo/src/scenes/Scene1Hook.tsx | 256 - .../src/scenes/Scene2Problem.tsx | 406 - .../src/scenes/Scene3LogoReveal.tsx | 285 - .../src/scenes/Scene4MultiChannel.tsx | 359 - .../src/scenes/Scene5McpTools.tsx | 349 - .../src/scenes/Scene6ProductTour.tsx | 373 - .../src/scenes/Scene7PowerFeatures.tsx | 769 -- .../src/scenes/Scene8Architecture.tsx | 218 - .../src/scenes/Scene9Pricing.tsx | 183 - .../video/openclaw-promo/src/styles/theme.ts | 52 - .../video/openclaw-promo/tsconfig.json | 16 - pickle_history.txt | 1 + ...-ai-automation-makecom-marketing-agency.md | 55 + proposals/2026-02-17-ai-finance-app.md | 536 ++ .../2026-02-17-claude-code-mcp-n8n-coach.md | 106 + ...2026-02-17-govgpt-senior-python-backend.md | 57 + ...17-senior-fullstack-ai-solutions-norway.md | 82 + proposals/solvr-onboarding-guide.md | 212 + reonomy-scraper-v14.js | 8 +- surya-manim-journey/README.md | 99 - .../partial_movie_file_list.txt | 7 - .../TestSoul/partial_movie_file_list.txt | 9 - .../Track09_WithU/partial_movie_file_list.txt | 13 - .../partial_movie_file_list.txt | 11 - .../Track09_WithU/partial_movie_file_list.txt | 13 - surya-manim-journey/render.sh | 96 - surya-manim-journey/surya_journey.py | 2216 ------ upwork-email-trigger/daemon-state.json | 6 + upwork-email-trigger/gmail-refresh-token.json | 1 + upwork-email-trigger/gmail-watch.mjs | 70 + upwork-email-trigger/package.json | 17 + upwork-email-trigger/pull-daemon.mjs | 143 + upwork-email-trigger/renew-watch.mjs | 38 + upwork-email-trigger/sa-key.json | 13 + upwork-email-trigger/src/worker.js | 67 + upwork-email-trigger/trigger-cron.mjs | 40 + upwork-email-trigger/wrangler.toml | 7 + upwork-pipeline/processed.json | 93 +- 745 files changed, 7904 insertions(+), 169718 deletions(-) create mode 100644 a2p-wizard-rebuild/ARCHITECTURE.md create mode 100644 a2p-wizard-rebuild/package.json create mode 100644 a2p-wizard-rebuild/public/css/form.css create mode 100644 a2p-wizard-rebuild/public/css/site.css create mode 100644 a2p-wizard-rebuild/public/js/form.js create mode 100644 a2p-wizard-rebuild/routes/api.js create mode 100644 a2p-wizard-rebuild/routes/sites.js create mode 100644 a2p-wizard-rebuild/server.js create mode 100644 a2p-wizard-rebuild/services/ai-generator.js create mode 100644 a2p-wizard-rebuild/services/packet-assembler.js create mode 100644 a2p-wizard-rebuild/services/screenshot.js create mode 100644 a2p-wizard-rebuild/services/site-builder.js create mode 100644 a2p-wizard-rebuild/templates/form.ejs create mode 100644 a2p-wizard-rebuild/templates/packet/results.ejs create mode 100644 a2p-wizard-rebuild/templates/site/contact.ejs create mode 100644 a2p-wizard-rebuild/templates/site/index.ejs create mode 100644 a2p-wizard-rebuild/templates/site/privacy-policy.ejs create mode 100644 a2p-wizard-rebuild/templates/site/terms.ejs delete mode 100644 closebot-sms/BUILD_PLAN.md delete mode 100644 closebot-sms/app/data/closebot-sms.db delete mode 100644 closebot-sms/app/data/closebot-sms.db-shm delete mode 100644 closebot-sms/app/data/closebot-sms.db-wal delete mode 100644 closebot-sms/app/next-env.d.ts delete mode 100644 closebot-sms/app/next.config.js delete mode 100644 closebot-sms/app/package.json delete mode 100644 closebot-sms/app/postcss.config.js delete mode 100644 closebot-sms/app/src/app/a2p/page.tsx delete mode 100644 closebot-sms/app/src/app/a2p/register/page.tsx delete mode 100644 closebot-sms/app/src/app/analytics/page.tsx delete mode 100644 closebot-sms/app/src/app/api/a2p/[id]/retry/route.ts delete mode 100644 closebot-sms/app/src/app/api/a2p/[id]/route.ts delete mode 100644 closebot-sms/app/src/app/api/a2p/[id]/status/route.ts delete mode 100644 closebot-sms/app/src/app/api/a2p/route.ts delete mode 100644 closebot-sms/app/src/app/api/analytics/bots/route.ts delete mode 100644 closebot-sms/app/src/app/api/analytics/leaderboard/route.ts delete mode 100644 closebot-sms/app/src/app/api/analytics/messages/route.ts delete mode 100644 closebot-sms/app/src/app/api/analytics/outcomes/route.ts delete mode 100644 closebot-sms/app/src/app/api/analytics/overview/route.ts delete mode 100644 closebot-sms/app/src/app/api/bots/[id]/route.ts delete mode 100644 closebot-sms/app/src/app/api/bots/route.ts delete mode 100644 closebot-sms/app/src/app/api/closebot/bots/route.ts delete mode 100644 closebot-sms/app/src/app/api/closebot/sources/route.ts delete mode 100644 closebot-sms/app/src/app/api/contacts/[id]/route.ts delete mode 100644 closebot-sms/app/src/app/api/contacts/export/route.ts delete mode 100644 closebot-sms/app/src/app/api/contacts/route.ts delete mode 100644 closebot-sms/app/src/app/api/conversations/[contactId]/route.ts delete mode 100644 closebot-sms/app/src/app/api/conversations/[contactId]/send/route.ts delete mode 100644 closebot-sms/app/src/app/api/conversations/route.ts delete mode 100644 closebot-sms/app/src/app/api/dashboard/activity/route.ts delete mode 100644 closebot-sms/app/src/app/api/dashboard/closebot-stats/route.ts delete mode 100644 closebot-sms/app/src/app/api/dashboard/stats/route.ts delete mode 100644 closebot-sms/app/src/app/api/landing-pages/[id]/download/route.ts delete mode 100644 closebot-sms/app/src/app/api/landing-pages/[id]/preview/route.ts delete mode 100644 closebot-sms/app/src/app/api/landing-pages/[id]/route.ts delete mode 100644 closebot-sms/app/src/app/api/landing-pages/generate-preview/route.ts delete mode 100644 closebot-sms/app/src/app/api/landing-pages/route.ts delete mode 100644 closebot-sms/app/src/app/api/phone-gateway/config/route.ts delete mode 100644 closebot-sms/app/src/app/api/phone-gateway/save/route.ts delete mode 100644 closebot-sms/app/src/app/api/phone-gateway/send/route.ts delete mode 100644 closebot-sms/app/src/app/api/phone-gateway/test/route.ts delete mode 100644 closebot-sms/app/src/app/api/routes/[id]/route.ts delete mode 100644 closebot-sms/app/src/app/api/routes/route.ts delete mode 100644 closebot-sms/app/src/app/api/settings/route.ts delete mode 100644 closebot-sms/app/src/app/api/settings/test-connection/route.ts delete mode 100644 closebot-sms/app/src/app/api/twilio/numbers/[sid]/route.ts delete mode 100644 closebot-sms/app/src/app/api/twilio/numbers/buy/route.ts delete mode 100644 closebot-sms/app/src/app/api/twilio/numbers/route.ts delete mode 100644 closebot-sms/app/src/app/api/twilio/numbers/search/route.ts delete mode 100644 closebot-sms/app/src/app/api/webhooks/closebot/response/route.ts delete mode 100644 closebot-sms/app/src/app/api/webhooks/twilio/inbound/route.ts delete mode 100644 closebot-sms/app/src/app/api/webhooks/twilio/status/route.ts delete mode 100644 closebot-sms/app/src/app/bots/page.tsx delete mode 100644 closebot-sms/app/src/app/connect-phone/page.tsx delete mode 100644 closebot-sms/app/src/app/contacts/page.tsx delete mode 100644 closebot-sms/app/src/app/conversations/page.tsx delete mode 100644 closebot-sms/app/src/app/landing-pages/create/page.tsx delete mode 100644 closebot-sms/app/src/app/landing-pages/page.tsx delete mode 100644 closebot-sms/app/src/app/layout.tsx delete mode 100644 closebot-sms/app/src/app/page.tsx delete mode 100644 closebot-sms/app/src/app/phone-numbers/page.tsx delete mode 100644 closebot-sms/app/src/app/routing/page.tsx delete mode 100644 closebot-sms/app/src/app/settings/page.tsx delete mode 100644 closebot-sms/app/src/components/analytics/bot-leaderboard.tsx delete mode 100644 closebot-sms/app/src/components/analytics/bots-bar-chart.tsx delete mode 100644 closebot-sms/app/src/components/analytics/messages-chart.tsx delete mode 100644 closebot-sms/app/src/components/analytics/outcome-donut.tsx delete mode 100644 closebot-sms/app/src/components/analytics/stat-card-sparkline.tsx delete mode 100644 closebot-sms/app/src/components/bots/bot-card.tsx delete mode 100644 closebot-sms/app/src/components/bots/bot-grid.tsx delete mode 100644 closebot-sms/app/src/components/contacts/contact-detail-panel.tsx delete mode 100644 closebot-sms/app/src/components/contacts/contact-filters.tsx delete mode 100644 closebot-sms/app/src/components/contacts/contacts-table.tsx delete mode 100644 closebot-sms/app/src/components/conversations/chat-thread.tsx delete mode 100644 closebot-sms/app/src/components/conversations/conversation-list.tsx delete mode 100644 closebot-sms/app/src/components/conversations/message-bubble.tsx delete mode 100644 closebot-sms/app/src/components/layout/sidebar.tsx delete mode 100644 closebot-sms/app/src/components/routing/bot-route-card.tsx delete mode 100644 closebot-sms/app/src/components/routing/connection-lines.tsx delete mode 100644 closebot-sms/app/src/components/routing/phone-number-card.tsx delete mode 100644 closebot-sms/app/src/components/routing/route-config-modal.tsx delete mode 100644 closebot-sms/app/src/components/routing/routing-view.tsx delete mode 100644 closebot-sms/app/src/components/settings/closebot-card.tsx delete mode 100644 closebot-sms/app/src/components/settings/notifications-card.tsx delete mode 100644 closebot-sms/app/src/components/settings/phone-gateway-card.tsx delete mode 100644 closebot-sms/app/src/components/settings/phone-numbers-card.tsx delete mode 100644 closebot-sms/app/src/components/settings/twilio-card.tsx delete mode 100644 closebot-sms/app/src/components/settings/webhook-urls-card.tsx delete mode 100644 closebot-sms/app/src/lib/a2p-types.ts delete mode 100644 closebot-sms/app/src/lib/closebot.ts delete mode 100644 closebot-sms/app/src/lib/db.ts delete mode 100644 closebot-sms/app/src/lib/landing-page-generator.ts delete mode 100644 closebot-sms/app/src/lib/sse.ts delete mode 100644 closebot-sms/app/src/lib/twilio-client.ts delete mode 100644 closebot-sms/app/src/lib/utils.ts delete mode 100644 closebot-sms/app/src/styles/globals.css delete mode 100644 closebot-sms/app/tailwind.config.ts delete mode 100644 closebot-sms/app/tsconfig.json delete mode 100644 closebot-sms/app/tsconfig.tsbuildinfo delete mode 100644 das-surya/lyrics/01_skin_intro.txt delete mode 100644 das-surya/lyrics/02_u_saved_me.txt delete mode 100644 das-surya/lyrics/03_nothing.txt delete mode 100644 das-surya/lyrics/04_sweet_relief.txt delete mode 100644 das-surya/lyrics/05_tiptoe.txt delete mode 100644 das-surya/lyrics/06_natures_call.txt delete mode 100644 das-surya/lyrics/07_dreamcatcher.txt delete mode 100644 das-surya/lyrics/08_idk.txt delete mode 100644 das-surya/lyrics/09_with_u.txt delete mode 100644 das-surya/lyrics/10_poor_you_poor_me.txt delete mode 100644 das-surya/lyrics/11_wait_4_u.txt delete mode 100644 das-surya/lyrics/12_run_to_u.txt delete mode 100644 das-surya/lyrics/13_medications.txt delete mode 100644 das-surya/lyrics/14_hollow.txt delete mode 100644 goosefactory/.gitignore delete mode 100644 goosefactory/BUILD-STATUS.md delete mode 100644 goosefactory/CONTRACTS.md delete mode 100644 goosefactory/README.md delete mode 100644 goosefactory/REBRAND-PLAN.md delete mode 100644 goosefactory/SEED_REPORT.md delete mode 100644 goosefactory/infra/db/README.md delete mode 100644 goosefactory/infra/db/seed/seed-agents-and-tasks.sql delete mode 100644 goosefactory/infra/db/seed/seed-from-factory.sql delete mode 100644 goosefactory/infra/docker/README.md delete mode 100644 goosefactory/infra/docker/docker-compose.yml delete mode 100644 goosefactory/infra/docker/init.sql delete mode 100644 goosefactory/package.json delete mode 100644 goosefactory/packages/api/README.md delete mode 100644 goosefactory/packages/api/drizzle.config.ts delete mode 100644 goosefactory/packages/api/package.json delete mode 100644 goosefactory/packages/api/src/db/index.ts delete mode 100644 goosefactory/packages/api/src/db/migrations/0000_tough_warhawk.sql delete mode 100644 goosefactory/packages/api/src/db/migrations/meta/0000_snapshot.json delete mode 100644 goosefactory/packages/api/src/db/migrations/meta/_journal.json delete mode 100644 goosefactory/packages/api/src/db/schema.ts delete mode 100644 goosefactory/packages/api/src/env.ts delete mode 100644 goosefactory/packages/api/src/events/bus.ts delete mode 100644 goosefactory/packages/api/src/events/consumers/audit-writer.ts delete mode 100644 goosefactory/packages/api/src/events/consumers/notification-worker.ts delete mode 100644 goosefactory/packages/api/src/events/consumers/sla-monitor.ts delete mode 100644 goosefactory/packages/api/src/events/consumers/ws-broadcaster.ts delete mode 100644 goosefactory/packages/api/src/events/types.ts delete mode 100644 goosefactory/packages/api/src/index.ts delete mode 100644 goosefactory/packages/api/src/middleware/audit.ts delete mode 100644 goosefactory/packages/api/src/middleware/auth.ts delete mode 100644 goosefactory/packages/api/src/middleware/rbac.ts delete mode 100644 goosefactory/packages/api/src/routes/agents.ts delete mode 100644 goosefactory/packages/api/src/routes/approvals.ts delete mode 100644 goosefactory/packages/api/src/routes/assets.ts delete mode 100644 goosefactory/packages/api/src/routes/audit.ts delete mode 100644 goosefactory/packages/api/src/routes/feedback.ts delete mode 100644 goosefactory/packages/api/src/routes/notifications.ts delete mode 100644 goosefactory/packages/api/src/routes/pipelines.ts delete mode 100644 goosefactory/packages/api/src/routes/tasks.ts delete mode 100644 goosefactory/packages/api/src/routes/webhooks.ts delete mode 100644 goosefactory/packages/api/src/services/agent.service.ts delete mode 100644 goosefactory/packages/api/src/services/approval.service.ts delete mode 100644 goosefactory/packages/api/src/services/asset.service.ts delete mode 100644 goosefactory/packages/api/src/services/escalation.service.ts delete mode 100644 goosefactory/packages/api/src/services/notification.service.ts delete mode 100644 goosefactory/packages/api/src/services/pipeline.service.ts delete mode 100644 goosefactory/packages/api/src/services/task.service.ts delete mode 100644 goosefactory/packages/api/src/services/webhook.service.ts delete mode 100644 goosefactory/packages/api/src/types/index.ts delete mode 100644 goosefactory/packages/api/src/ws/server.ts delete mode 100644 goosefactory/packages/api/tsconfig.json delete mode 160000 goosefactory/packages/desktop delete mode 100644 goosefactory/packages/discord-bot/STARTUP-GUIDE.md delete mode 100644 goosefactory/packages/discord-bot/package.json delete mode 100644 goosefactory/packages/discord-bot/src/api-client.ts delete mode 100644 goosefactory/packages/discord-bot/src/commands/approve.ts delete mode 100644 goosefactory/packages/discord-bot/src/commands/blockers.ts delete mode 100644 goosefactory/packages/discord-bot/src/commands/deploy.ts delete mode 100644 goosefactory/packages/discord-bot/src/commands/pipelines.ts delete mode 100644 goosefactory/packages/discord-bot/src/commands/queue.ts delete mode 100644 goosefactory/packages/discord-bot/src/commands/reject.ts delete mode 100644 goosefactory/packages/discord-bot/src/commands/status.ts delete mode 100644 goosefactory/packages/discord-bot/src/config.ts delete mode 100644 goosefactory/packages/discord-bot/src/embeds/approval-request.ts delete mode 100644 goosefactory/packages/discord-bot/src/embeds/batch-review.ts delete mode 100644 goosefactory/packages/discord-bot/src/embeds/deploy-notification.ts delete mode 100644 goosefactory/packages/discord-bot/src/embeds/pipeline-status.ts delete mode 100644 goosefactory/packages/discord-bot/src/embeds/sla-warning.ts delete mode 100644 goosefactory/packages/discord-bot/src/interactions/button-handler.ts delete mode 100644 goosefactory/packages/discord-bot/src/interactions/modal-handler.ts delete mode 100644 goosefactory/packages/discord-bot/src/interactions/select-handler.ts delete mode 100644 goosefactory/packages/discord-bot/tsconfig.json delete mode 100644 goosefactory/packages/learning/LEARNING_PIPELINE_REPORT.md delete mode 100644 goosefactory/packages/learning/README.md delete mode 100644 goosefactory/packages/learning/data/.gitkeep delete mode 100644 goosefactory/packages/learning/memory/feedback-patterns.md delete mode 100644 goosefactory/packages/learning/memory/jake-preferences.md delete mode 100644 goosefactory/packages/learning/memory/quality-standards.md delete mode 100644 goosefactory/packages/learning/package.json delete mode 100644 goosefactory/packages/learning/src/analysis/behavioral.ts delete mode 100644 goosefactory/packages/learning/src/analysis/calibration.ts delete mode 100644 goosefactory/packages/learning/src/analysis/dimensions.ts delete mode 100644 goosefactory/packages/learning/src/analysis/patterns.ts delete mode 100644 goosefactory/packages/learning/src/analysis/preferences.ts delete mode 100644 goosefactory/packages/learning/src/improvement/diminishing-review.ts delete mode 100644 goosefactory/packages/learning/src/improvement/pre-check.ts delete mode 100644 goosefactory/packages/learning/src/improvement/prediction.ts delete mode 100644 goosefactory/packages/learning/src/improvement/regression.ts delete mode 100644 goosefactory/packages/learning/src/index.ts delete mode 100644 goosefactory/packages/learning/src/memory/compressor.ts delete mode 100644 goosefactory/packages/learning/src/memory/reader.ts delete mode 100644 goosefactory/packages/learning/src/memory/writer.ts delete mode 100644 goosefactory/packages/learning/src/metrics/kpis.ts delete mode 100644 goosefactory/packages/learning/src/pipeline/act.ts delete mode 100644 goosefactory/packages/learning/src/pipeline/analyze.ts delete mode 100644 goosefactory/packages/learning/src/pipeline/enrich.ts delete mode 100644 goosefactory/packages/learning/src/pipeline/index.ts delete mode 100644 goosefactory/packages/learning/src/pipeline/store.ts delete mode 100644 goosefactory/packages/learning/src/pipeline/types.ts delete mode 100644 goosefactory/packages/learning/src/pipeline/validate.ts delete mode 100644 goosefactory/packages/learning/src/service.ts delete mode 100644 goosefactory/packages/learning/src/storage/feedback-store.ts delete mode 100644 goosefactory/packages/learning/src/storage/query.ts delete mode 100644 goosefactory/packages/learning/src/storage/stats.ts delete mode 100644 goosefactory/packages/learning/src/types.ts delete mode 100755 goosefactory/packages/learning/start-service.sh delete mode 100644 goosefactory/packages/learning/test-feedback.json delete mode 100644 goosefactory/packages/learning/tsconfig.json delete mode 100644 goosefactory/packages/mcp-server/README.md delete mode 100644 goosefactory/packages/mcp-server/package.json delete mode 100644 goosefactory/packages/mcp-server/src/api-client.ts delete mode 100644 goosefactory/packages/mcp-server/src/index.ts delete mode 100644 goosefactory/packages/mcp-server/src/modals/host.ts delete mode 100644 goosefactory/packages/mcp-server/src/prompts/deploy-checklist.ts delete mode 100644 goosefactory/packages/mcp-server/src/prompts/needs-attention.ts delete mode 100644 goosefactory/packages/mcp-server/src/prompts/retrospective.ts delete mode 100644 goosefactory/packages/mcp-server/src/prompts/review-server.ts delete mode 100644 goosefactory/packages/mcp-server/src/resources/config.ts delete mode 100644 goosefactory/packages/mcp-server/src/resources/dashboard.ts delete mode 100644 goosefactory/packages/mcp-server/src/resources/pipelines.ts delete mode 100644 goosefactory/packages/mcp-server/src/tools/operations.ts delete mode 100644 goosefactory/packages/mcp-server/src/tools/pipelines.ts delete mode 100644 goosefactory/packages/mcp-server/src/tools/review.ts delete mode 100644 goosefactory/packages/mcp-server/src/tools/tasks.ts delete mode 100644 goosefactory/packages/mcp-server/src/types.ts delete mode 100644 goosefactory/packages/mcp-server/tsconfig.json delete mode 100644 goosefactory/packages/modals/README.md delete mode 100644 goosefactory/packages/modals/package.json delete mode 100644 goosefactory/packages/modals/src/applause-meter.html delete mode 100644 goosefactory/packages/modals/src/before-after.html delete mode 100644 goosefactory/packages/modals/src/checklist-ceremony.html delete mode 100644 goosefactory/packages/modals/src/confidence-meter.html delete mode 100644 goosefactory/packages/modals/src/crystal-ball.html delete mode 100644 goosefactory/packages/modals/src/decision-tree.html delete mode 100644 goosefactory/packages/modals/src/emoji-scale.html delete mode 100644 goosefactory/packages/modals/src/hot-take.html delete mode 100644 goosefactory/packages/modals/src/judges-scorecard.html delete mode 100644 goosefactory/packages/modals/src/mission-briefing.html delete mode 100644 goosefactory/packages/modals/src/mood-ring.html delete mode 100644 goosefactory/packages/modals/src/preview/index.html delete mode 100644 goosefactory/packages/modals/src/priority-poker.html delete mode 100644 goosefactory/packages/modals/src/quick-pulse.html delete mode 100644 goosefactory/packages/modals/src/ranking-arena.html delete mode 100644 goosefactory/packages/modals/src/report-card.html delete mode 100644 goosefactory/packages/modals/src/retrospective-board.html delete mode 100644 goosefactory/packages/modals/src/shared/modal-utils.js delete mode 100644 goosefactory/packages/modals/src/side-by-side.html delete mode 100644 goosefactory/packages/modals/src/slot-machine.html delete mode 100644 goosefactory/packages/modals/src/speed-round.html delete mode 100644 goosefactory/packages/modals/src/spotlight.html delete mode 100644 goosefactory/packages/modals/src/thermometer.html delete mode 100644 goosefactory/packages/modals/src/tinder-swipe.html delete mode 100644 goosefactory/packages/modals/src/traffic-light.html delete mode 100644 goosefactory/packages/modals/src/voice-of-customer.html delete mode 100644 goosefactory/packages/modals/src/war-room.html delete mode 100644 goosefactory/packages/shared/README.md delete mode 100644 goosefactory/packages/shared/package.json delete mode 100644 goosefactory/packages/shared/src/constants/errors.ts delete mode 100644 goosefactory/packages/shared/src/constants/index.ts delete mode 100644 goosefactory/packages/shared/src/constants/sla.ts delete mode 100644 goosefactory/packages/shared/src/constants/stages.ts delete mode 100644 goosefactory/packages/shared/src/index.ts delete mode 100644 goosefactory/packages/shared/src/integration/index.ts delete mode 100644 goosefactory/packages/shared/src/integration/modal-to-learning.ts delete mode 100644 goosefactory/packages/shared/src/schemas/feedback.schema.ts delete mode 100644 goosefactory/packages/shared/src/schemas/index.ts delete mode 100644 goosefactory/packages/shared/src/schemas/pipeline.schema.ts delete mode 100644 goosefactory/packages/shared/src/schemas/task.schema.ts delete mode 100644 goosefactory/packages/shared/src/types/agent.ts delete mode 100644 goosefactory/packages/shared/src/types/api.ts delete mode 100644 goosefactory/packages/shared/src/types/approval.ts delete mode 100644 goosefactory/packages/shared/src/types/asset.ts delete mode 100644 goosefactory/packages/shared/src/types/audit.ts delete mode 100644 goosefactory/packages/shared/src/types/feedback.ts delete mode 100644 goosefactory/packages/shared/src/types/index.ts delete mode 100644 goosefactory/packages/shared/src/types/notification.ts delete mode 100644 goosefactory/packages/shared/src/types/pipeline.ts delete mode 100644 goosefactory/packages/shared/src/types/task.ts delete mode 100644 goosefactory/packages/shared/src/types/ws.ts delete mode 100644 goosefactory/packages/shared/tsconfig.json delete mode 100755 goosefactory/scripts/first-boot.sh delete mode 100644 goosefactory/scripts/seed-from-factory.ts delete mode 100755 goosefactory/scripts/test-mcp-tools.sh delete mode 100644 goosefactory/tests/integration/modal-to-learning.test.ts delete mode 100644 goosefactory/tsconfig.base.json create mode 100644 house-remodel/proposal.html create mode 100644 localbosses-app/next-env.d.ts create mode 100644 localbosses-app/tsconfig.tsbuildinfo delete mode 160000 manim-mcp delete mode 100644 mcp-diagrams/GHL-MCP-Funnel/README.md delete mode 100644 mcp-diagrams/GHL-MCP-Funnel/functions/api/waitlist.js delete mode 100644 mcp-diagrams/GHL-MCP-Funnel/index.html delete mode 160000 mcp-diagrams/GoHighLevel-MCP delete mode 100644 mcp-diagrams/audit-report.json delete mode 100644 mcp-diagrams/audit-servers.js delete mode 100644 mcp-diagrams/fix-servers.js delete mode 100644 mcp-diagrams/ghl-mcp-apps-only/package.json delete mode 100644 mcp-diagrams/ghl-mcp-apps-only/src/apps/index.ts delete mode 100644 mcp-diagrams/ghl-mcp-apps-only/src/clients/ghl-api-client.ts delete mode 100644 mcp-diagrams/ghl-mcp-apps-only/src/server.ts delete mode 100644 mcp-diagrams/ghl-mcp-apps-only/src/types/ghl-types.ts delete mode 100644 mcp-diagrams/ghl-mcp-apps-only/tsconfig.json delete mode 160000 mcp-diagrams/ghl-mcp-public delete mode 100644 mcp-diagrams/google-ads-mcp/PLAN.md delete mode 100644 mcp-diagrams/google-ads-mcp/README.md delete mode 100644 mcp-diagrams/google-ads-mcp/package.json delete mode 100644 mcp-diagrams/google-ads-mcp/src/apps/budget-optimizer.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/apps/campaign-dashboard.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/apps/campaign-detail.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/apps/index.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/apps/keyword-analyzer.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/apps/performance-overview.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/apps/recommendations.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/apps/search-terms.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/apps/theme.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/auth.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/client.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/index.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/tools/accounts.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/tools/ad-groups.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/tools/ads.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/tools/advanced.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/tools/bidding.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/tools/campaigns.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/tools/conversions.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/tools/index.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/tools/keywords.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/tools/reporting.ts delete mode 100644 mcp-diagrams/google-ads-mcp/src/types.ts delete mode 100644 mcp-diagrams/google-ads-mcp/tsconfig.json delete mode 100644 mcp-diagrams/google-ads-mcp/tsup.config.ts delete mode 100755 mcp-diagrams/google-ads-mcp/ui/build-all.sh delete mode 100644 mcp-diagrams/google-ads-mcp/ui/package.json delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/budget-optimizer/App.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/budget-optimizer/index.html delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/budget-optimizer/main.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/budget-optimizer/vite.config.ts delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-dashboard/App.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-dashboard/index.html delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-dashboard/main.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-dashboard/vite.config.ts delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-detail/App.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-detail/index.html delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-detail/main.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-detail/vite.config.ts delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/keyword-analyzer/App.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/keyword-analyzer/index.html delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/keyword-analyzer/main.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/keyword-analyzer/vite.config.ts delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/performance-overview/App.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/performance-overview/index.html delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/performance-overview/main.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/performance-overview/vite.config.ts delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/recommendations/App.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/recommendations/index.html delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/recommendations/main.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/recommendations/vite.config.ts delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/search-terms/App.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/search-terms/index.html delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/search-terms/main.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/apps/search-terms/vite.config.ts delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/charts/BarChart.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/charts/FunnelChart.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/charts/LineChart.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/charts/PieChart.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/charts/SparklineChart.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/comms/ChatThread.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/comms/ContentPreview.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/comms/EmailPreview.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/comms/TranscriptView.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/data/AudioPlayer.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/data/AvatarGroup.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/data/CardGrid.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/data/ChecklistView.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/data/CurrencyDisplay.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/data/DataTable.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/data/DetailHeader.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/data/InfoBlock.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/data/KanbanBoard.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/data/KeyValueList.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/data/LineItemsTable.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/data/MetricCard.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/data/ProgressBar.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/data/StarRating.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/data/StatusBadge.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/data/StockIndicator.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/data/TagList.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/data/Timeline.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/interactive/AmountInput.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/interactive/AppointmentBooker.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/interactive/ContactPicker.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/interactive/EditableField.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/interactive/FormGroup.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/interactive/InvoiceBuilder.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/interactive/OpportunityEditor.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/interactive/SelectDropdown.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/layout/Card.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/layout/PageHeader.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/layout/Section.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/layout/SplitLayout.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/layout/StatsGrid.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/shared/ActionBar.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/shared/ActionButton.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/shared/FilterChips.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/shared/Modal.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/shared/SaveIndicator.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/shared/SearchBar.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/shared/TabGroup.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/shared/Toast.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/viz/CalendarView.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/viz/DuplicateCompare.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/viz/FlowDiagram.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/viz/MediaGallery.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/components/viz/TreeView.tsx delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/styles/base.css delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/styles/google-ads-theme.css delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/styles/interactive.css delete mode 100644 mcp-diagrams/google-ads-mcp/ui/src/types.ts delete mode 100644 mcp-diagrams/google-ads-mcp/ui/tsconfig.json delete mode 100644 mcp-diagrams/mcp-animation-framework/README.md delete mode 100644 mcp-diagrams/mcp-animation-framework/capture-animation.js delete mode 100644 mcp-diagrams/mcp-animation-framework/capture-demo.js delete mode 100644 mcp-diagrams/mcp-animation-framework/capture-full-flow.js delete mode 100644 mcp-diagrams/mcp-animation-framework/capture-scroll-v2.js delete mode 100644 mcp-diagrams/mcp-animation-framework/capture-scroll.js delete mode 100644 mcp-diagrams/mcp-animation-framework/capture-template.js delete mode 100644 mcp-diagrams/mcp-animation-framework/capture-v4.js delete mode 100644 mcp-diagrams/mcp-animation-framework/capture-v5.js delete mode 100644 mcp-diagrams/mcp-animation-framework/capture-v6.js delete mode 100644 mcp-diagrams/mcp-animation-framework/capture-web-embed.js delete mode 100644 mcp-diagrams/mcp-animation-framework/configs/servicetitan-dispatch.json delete mode 100644 mcp-diagrams/mcp-animation-framework/gen-three.js delete mode 100644 mcp-diagrams/mcp-animation-framework/generate-all.js delete mode 100644 mcp-diagrams/mcp-animation-framework/generate-batch.js delete mode 100644 mcp-diagrams/mcp-animation-framework/generate-fast.js delete mode 100644 mcp-diagrams/mcp-animation-framework/generate-new-only.js delete mode 100644 mcp-diagrams/mcp-animation-framework/generate-single.js delete mode 100644 mcp-diagrams/mcp-animation-framework/generate.js delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/ghl-reference.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/site-generator.js delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/acuity.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/bamboohr.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/basecamp.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/bigcommerce.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/brevo.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/calendly.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/clickup.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/closecrm.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/clover.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/constantcontact.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/fieldedge.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/freshbooks.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/freshdesk.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/gusto.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/helpscout.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/housecallpro.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/jobber.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/keap.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/lightspeed.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/mailchimp.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/pipedrive.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/rippling.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/servicetitan.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/squarespace.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/toast.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/touchbistro.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/trello.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/wave.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/wrike.html delete mode 100644 mcp-diagrams/mcp-animation-framework/landing-pages/sites/zendesk.html delete mode 100644 mcp-diagrams/mcp-animation-framework/mcp-configs.js delete mode 100644 mcp-diagrams/mcp-animation-framework/output/acuity.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/bamboohr.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/basecamp.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/bigcommerce.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/brevo.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/calendly.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/clickup.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/closecrm.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/clover.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/constantcontact.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/fieldedge.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/freshbooks.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/freshdesk.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/gusto.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/helpscout.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/housecallpro.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/jobber.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/keap.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/lightspeed.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/mailchimp.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/mcp-demo.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/pipedrive.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/rippling.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/servicetitan-dispatch/frame-01-empty.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/servicetitan-dispatch/frame-02-typing-user.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/servicetitan-dispatch/frame-02-typing.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/servicetitan-dispatch/frame-03-exchange.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/servicetitan-dispatch/frame-03-first-exchange.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/servicetitan-dispatch/frame-04-typing-ai.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/servicetitan-dispatch/frame-04-typing-second.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/servicetitan-dispatch/frame-05-loading.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/servicetitan-dispatch/frame-06-final.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/servicetitan.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/squarespace.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/stripe-animation.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/stripe-full-flow.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/stripe-scroll-v2.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/stripe-scroll.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/stripe-template.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/stripe-v4.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/stripe-v5.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/stripe-v6.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/toast.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/touchbistro.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/trello.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/wave.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/wrike.html delete mode 100644 mcp-diagrams/mcp-animation-framework/output/zendesk.html delete mode 100644 mcp-diagrams/mcp-animation-framework/package.json delete mode 100644 mcp-diagrams/mcp-animation-framework/web-embed/stripe-simulation-v2.html delete mode 100644 mcp-diagrams/mcp-animation-framework/web-embed/stripe-simulation-v3.html delete mode 100644 mcp-diagrams/mcp-animation-framework/web-embed/stripe-simulation.html delete mode 100644 mcp-diagrams/mcp-business-projections.md delete mode 100644 mcp-diagrams/mcp-chat-animation/frame-01-empty.html delete mode 100644 mcp-diagrams/mcp-chat-animation/frame-02-typing.html delete mode 100644 mcp-diagrams/mcp-chat-animation/frame-03-first-exchange.html delete mode 100644 mcp-diagrams/mcp-chat-animation/frame-04-typing-second.html delete mode 100644 mcp-diagrams/mcp-chat-animation/frame-05-second-exchange-loading.html delete mode 100644 mcp-diagrams/mcp-chat-animation/frame-06-full-request.html delete mode 100644 mcp-diagrams/mcp-chat-animation/frame-07-final-loaded.html delete mode 100644 mcp-diagrams/mcp-chat-animation/template-large.html delete mode 100644 mcp-diagrams/mcp-chat-animation/template-normal.html delete mode 100644 mcp-diagrams/mcp-chat-animation/template.html delete mode 100644 mcp-diagrams/mcp-chat-remotion/package.json delete mode 100644 mcp-diagrams/mcp-chat-remotion/src/MCPChatAnimation.tsx delete mode 100644 mcp-diagrams/mcp-chat-remotion/src/Root.tsx delete mode 100644 mcp-diagrams/mcp-chat-remotion/src/StripeCameraDemo.tsx delete mode 100644 mcp-diagrams/mcp-chat-remotion/src/StripeDollyDemo.tsx delete mode 100644 mcp-diagrams/mcp-chat-remotion/src/index.ts delete mode 100644 mcp-diagrams/mcp-chat-remotion/tsconfig.json delete mode 100644 mcp-diagrams/mcp-combos-chibi.html delete mode 100644 mcp-diagrams/mcp-combos-graphic-v2.html delete mode 100644 mcp-diagrams/mcp-combos-graphic.html delete mode 100644 mcp-diagrams/mcp-competitive-landscape.md delete mode 100644 mcp-diagrams/mcp-pricing-research.md delete mode 100644 mcp-diagrams/mcp-servers/acuity-scheduling/package.json delete mode 100644 mcp-diagrams/mcp-servers/acuity-scheduling/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/acuity-scheduling/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/bamboohr/package.json delete mode 100644 mcp-diagrams/mcp-servers/bamboohr/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/bamboohr/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/basecamp/package.json delete mode 100644 mcp-diagrams/mcp-servers/basecamp/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/basecamp/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/bigcommerce/package.json delete mode 100644 mcp-diagrams/mcp-servers/bigcommerce/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/bigcommerce/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/brevo/package.json delete mode 100644 mcp-diagrams/mcp-servers/brevo/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/brevo/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/calendly/package.json delete mode 100644 mcp-diagrams/mcp-servers/calendly/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/calendly/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/clickup/package.json delete mode 100644 mcp-diagrams/mcp-servers/clickup/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/clickup/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/close/package.json delete mode 100644 mcp-diagrams/mcp-servers/close/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/close/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/clover/README.md delete mode 100644 mcp-diagrams/mcp-servers/clover/package.json delete mode 100644 mcp-diagrams/mcp-servers/clover/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/clover/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/constant-contact/package.json delete mode 100644 mcp-diagrams/mcp-servers/constant-contact/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/constant-contact/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/fieldedge/README.md delete mode 100644 mcp-diagrams/mcp-servers/fieldedge/package.json delete mode 100644 mcp-diagrams/mcp-servers/fieldedge/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/fieldedge/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/freshbooks/package.json delete mode 100644 mcp-diagrams/mcp-servers/freshbooks/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/freshbooks/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/freshdesk/package.json delete mode 100644 mcp-diagrams/mcp-servers/freshdesk/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/freshdesk/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/gusto/package.json delete mode 100644 mcp-diagrams/mcp-servers/gusto/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/gusto/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/helpscout/package.json delete mode 100644 mcp-diagrams/mcp-servers/helpscout/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/helpscout/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/housecall-pro/README.md delete mode 100644 mcp-diagrams/mcp-servers/housecall-pro/package.json delete mode 100644 mcp-diagrams/mcp-servers/housecall-pro/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/housecall-pro/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/jobber/package.json delete mode 100644 mcp-diagrams/mcp-servers/jobber/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/jobber/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/keap/package.json delete mode 100644 mcp-diagrams/mcp-servers/keap/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/keap/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/lightspeed/package.json delete mode 100644 mcp-diagrams/mcp-servers/lightspeed/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/lightspeed/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/mailchimp/package.json delete mode 100644 mcp-diagrams/mcp-servers/mailchimp/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/mailchimp/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/pipedrive/package.json delete mode 100644 mcp-diagrams/mcp-servers/pipedrive/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/pipedrive/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/rippling/README.md delete mode 100644 mcp-diagrams/mcp-servers/rippling/package.json delete mode 100644 mcp-diagrams/mcp-servers/rippling/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/rippling/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/servicetitan/README.md delete mode 100644 mcp-diagrams/mcp-servers/servicetitan/package.json delete mode 100644 mcp-diagrams/mcp-servers/servicetitan/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/servicetitan/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/squarespace/package.json delete mode 100644 mcp-diagrams/mcp-servers/squarespace/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/squarespace/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/template/package.json delete mode 100644 mcp-diagrams/mcp-servers/template/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/template/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/toast/package.json delete mode 100644 mcp-diagrams/mcp-servers/toast/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/toast/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/touchbistro/README.md delete mode 100644 mcp-diagrams/mcp-servers/touchbistro/package.json delete mode 100644 mcp-diagrams/mcp-servers/touchbistro/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/touchbistro/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/trello/package.json delete mode 100644 mcp-diagrams/mcp-servers/trello/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/trello/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/wave/package.json delete mode 100644 mcp-diagrams/mcp-servers/wave/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/wave/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/wrike/package.json delete mode 100644 mcp-diagrams/mcp-servers/wrike/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/wrike/tsconfig.json delete mode 100644 mcp-diagrams/mcp-servers/zendesk/package.json delete mode 100644 mcp-diagrams/mcp-servers/zendesk/src/index.ts delete mode 100644 mcp-diagrams/mcp-servers/zendesk/tsconfig.json delete mode 100644 openclaw-gallery/UPWORK_REFERENCE.md delete mode 100644 openclaw-gallery/pdfs/openclaw-capabilities.md delete mode 100644 openclaw-gallery/pdfs/openclaw-packages.md delete mode 100644 openclaw-gallery/video/openclaw-promo/UPGRADE_SPEC.md delete mode 100644 openclaw-gallery/video/openclaw-promo/package.json delete mode 100644 openclaw-gallery/video/openclaw-promo/remotion.config.ts delete mode 100644 openclaw-gallery/video/openclaw-promo/src/OpenClawPromo.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/Root.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/components/AnimatedNumber.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/components/CanvasViewport.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/components/ChannelIcons.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/components/DrawLine.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/components/FadeSlideIn.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/components/GlassCard.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/components/KineticText.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/components/MeshBackground.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/components/ParticleField.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/components/StaggeredGrid.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/components/TypewriterText.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/index.ts delete mode 100644 openclaw-gallery/video/openclaw-promo/src/scenes/Scene10Cta.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/scenes/Scene1Hook.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/scenes/Scene2Problem.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/scenes/Scene3LogoReveal.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/scenes/Scene4MultiChannel.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/scenes/Scene5McpTools.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/scenes/Scene6ProductTour.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/scenes/Scene7PowerFeatures.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/scenes/Scene8Architecture.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/scenes/Scene9Pricing.tsx delete mode 100644 openclaw-gallery/video/openclaw-promo/src/styles/theme.ts delete mode 100644 openclaw-gallery/video/openclaw-promo/tsconfig.json create mode 100644 proposals/2026-02-17-ai-automation-makecom-marketing-agency.md create mode 100644 proposals/2026-02-17-ai-finance-app.md create mode 100644 proposals/2026-02-17-claude-code-mcp-n8n-coach.md create mode 100644 proposals/2026-02-17-govgpt-senior-python-backend.md create mode 100644 proposals/2026-02-17-senior-fullstack-ai-solutions-norway.md create mode 100644 proposals/solvr-onboarding-guide.md delete mode 100644 surya-manim-journey/README.md delete mode 100644 surya-manim-journey/media/videos/surya_journey/480p15/partial_movie_files/TestMathObjects/partial_movie_file_list.txt delete mode 100644 surya-manim-journey/media/videos/surya_journey/480p15/partial_movie_files/TestSoul/partial_movie_file_list.txt delete mode 100644 surya-manim-journey/media/videos/surya_journey/480p15/partial_movie_files/Track09_WithU/partial_movie_file_list.txt delete mode 100644 surya-manim-journey/media/videos/surya_journey/480p15/partial_movie_files/Track14_Hollow/partial_movie_file_list.txt delete mode 100644 surya-manim-journey/media/videos/surya_journey/480p30/partial_movie_files/Track09_WithU/partial_movie_file_list.txt delete mode 100755 surya-manim-journey/render.sh delete mode 100644 surya-manim-journey/surya_journey.py create mode 100644 upwork-email-trigger/daemon-state.json create mode 100644 upwork-email-trigger/gmail-refresh-token.json create mode 100644 upwork-email-trigger/gmail-watch.mjs create mode 100644 upwork-email-trigger/package.json create mode 100644 upwork-email-trigger/pull-daemon.mjs create mode 100644 upwork-email-trigger/renew-watch.mjs create mode 100644 upwork-email-trigger/sa-key.json create mode 100644 upwork-email-trigger/src/worker.js create mode 100644 upwork-email-trigger/trigger-cron.mjs create mode 100644 upwork-email-trigger/wrangler.toml diff --git a/HEARTBEAT.md b/HEARTBEAT.md index 76a82cc..9719c17 100644 --- a/HEARTBEAT.md +++ b/HEARTBEAT.md @@ -1,26 +1,37 @@ # HEARTBEAT.md — Current Focus ## Now -- **Upwork email pipeline LIVE** — cron every 5 min (8AM-11PM ET), checking Gmail for alerts, auto-scoring, auto-applying -- Rate filter: $50/hr default, $25+/hr exception for legit clients (5.0★, $2K+ spent) -- Pipeline processed ~6 emails today, 0 qualified yet (all below threshold or already processed) -- Mastermind meeting tonight ~10:15PM — may produce follow-up items +- **Upwork email pipeline LIVE** — cron every 5 min (24/7), Gmail Pub/Sub push trigger is primary, polling is fallback +- **Gmail watch active** — expires ~Feb 24, auto-renewed every 3 days via cron +- Rate filter: $50/hr minimum on all proposals (Jake directive) +- **19 connects remaining** — be selective, high-value applications only + +## Active Applications (Awaiting Response) +- **Claude Code + MCP + n8n Automation Coach** — $85/hr, $216K client, 4.99★, submitted ~9PM Feb 17 +- **GovGPT Senior Python Backend** — $65/hr, $97K client, 4.65★, contract-to-hire, submitted ~10:21PM Feb 17 +- **OpenClaw Workflow Consultant** — applied previously ## Next -- Monitor pipeline for first qualified auto-apply -- Follow up on Robert Hartline / CallProof meeting (still pending from last week) -- Jake needs to manually submit 2 proposals: Fractional Claude Code + OpenClaw consultant -- Consider upgrade to push-based Gmail trigger (Pub/Sub → CF Worker) if latency is an issue -- Jacob's OAuth skill idea needs Jake's review +- Monitor responses on the 2 applications submitted today +- Jake needs to manually apply to: Agentic Frameworks + OpenClaw Consultant (92 score, US-only, 25 connects) +- Follow up on Robert Hartline / CallProof meeting (still pending) +- CRESyncFlow: wire Anthropic real OAuth into provider modal, Reonomy scraper fix (type --slowly for React inputs) +- Ecomm portfolio v2 live at ecomport.mcpengage.com — may need Jake's review +- Contractor proposal demo needs redeployment if Jake wants it +- Twilio + CloseBot combined app — check Jake's laptop ## Blocked - **GitHub shadow banned** — MCP factory paused until resolved - **OSKV coaching paused** — awaiting Jake's decision on Oliver & Kevin silence (since Feb 12) -- **dec-004** (MCP registry listing approval) — zero reactions after 5+ days, 6 reminders sent +- **dec-004** (MCP registry listing approval) — zero reactions, Jake was briefed on what it means ## Key Infra - Portfolio: https://portfolio.mcpengage.com (Cloudflare Workers) +- Ecomm Portfolio: https://ecomport.mcpengage.com (Cloudflare Workers) - NicheQuiz: thenichequiz.com (permanent CF tunnel) -- Upwork email pipeline cron: `2205ac65` (every 5 min, 8AM-11PM ET) +- CRESyncFlow: `/tmp/CRESyncFlow` (port 8900) +- Upwork email pipeline cron: `2205ac65` (every 5 min, 24/7) - Upwork deep scan cron: `116d2c44` (4x daily at 8,12,16,20) -- Connects: ~107-122 remaining +- Gmail Pub/Sub daemon: `com.clawdbot.gmail-pubsub-daemon` (launchd) +- Gmail watch renewal cron: `gmail-watch-renewal` (every 3 days at 6AM) +- Connects: ~19 remaining diff --git a/SOUL.md b/SOUL.md index d54817e..507d251 100644 --- a/SOUL.md +++ b/SOUL.md @@ -35,6 +35,7 @@ ## Boundaries - Confirm before spending money. Warn before breaking things. +- **Open Claw:** Never call myself the owner — I'm a contributor. Jake is the creator/owner. ## Speed - Don't narrate routine tool calls — just do them. diff --git a/a2p-wizard-rebuild/ARCHITECTURE.md b/a2p-wizard-rebuild/ARCHITECTURE.md new file mode 100644 index 0000000..b9df684 --- /dev/null +++ b/a2p-wizard-rebuild/ARCHITECTURE.md @@ -0,0 +1,333 @@ +# A2P Wizard - Complete Backend Architecture & Rebuild Guide + +## What A2P Wizard Does (TL;DR) + +A user fills out a simple form with basic business info. The backend: +1. **Generates a fully compliant landing page/website** branded to the client +2. **Creates screenshots** of the opt-in process on that website +3. **Produces a complete A2P 10DLC compliance packet** (descriptions, sample messages, opt-in flow documentation, screenshots) +4. **Outputs white-labeled documentation** the agency can copy/paste into their TCR campaign registration (via HighLevel, Twilio, etc.) + +--- + +## PHASE 1: Form Intake + +### Input Fields (from the live form at `/automated-setup-trial`) + +| Field | Purpose | +|-------|---------| +| Your Name (Agency or Owner) | Agency contact — NOT the end client | +| Your Email | Agency email for delivery | +| Client LEGAL Business Name | Must match EIN exactly, no special chars | +| Client Business ADDRESS | Must match EIN paperwork | +| Client Support Email | Should match brand registration email | +| Client Business Phone | Client's contact number | +| Business Description (1-2 sentences) | Used to generate website content + campaign description | +| Logo Upload | Client logo (max 400px) for website branding | +| TCPA Compliance Checkbox | Legal CYA — confirms client meets requirements | + +### Form Backend +- **reCAPTCHA v3** on the form (Google) +- Form submission triggers the automation pipeline (likely via webhook to n8n or similar) + +--- + +## PHASE 2: Content Generation (AI + Automation) + +This is the core engine. Based on the "Domains to Dollars" approach by Getting Automated: + +### 2A. AI Content Generation + +**Input:** Business name, description, address, phone, email, logo +**Engine:** LLM (Claude/GPT) via n8n workflow or direct API call + +**AI generates all of the following from the business description:** + +#### For the Landing Page/Website: +- **Hero section:** Title, subtitle, CTA button text +- **Business description:** Expanded from the 1-2 sentence input +- **Pain points section:** 3-4 industry-specific pain points the business solves +- **How It Works:** 3-step process description +- **FAQ section:** 4-6 relevant Q&As about the business +- **Testimonials/Social proof:** Generic but industry-appropriate +- **Footer text:** Copyright + business name + +#### For the A2P Compliance Packet: +- **Campaign Description:** Who sends messages, who receives them, why (TCR requirement) +- **Sample Messages (2-3):** + - Appointment reminder with `[brackets]` for templated fields + - Promotional/marketing message with brand name + - At least 1 with opt-out language ("Reply STOP to unsubscribe") +- **Opt-in Flow Description:** How end-users consent (website form, in-person, etc.) +- **Opt-in Confirmation Message:** Under 160 chars, includes brand name, HELP, STOP, frequency, rates +- **Opt-out Confirmation Message:** Brand name + confirmation no more messages +- **Help Message:** Brand name + support contact info +- **Use Case Description:** Maps to TCR campaign types (Mixed, Marketing, etc.) + +### 2B. JSON Config Generation + +All generated content gets structured into a `config.json`: + +```json +{ + "businessName": "Naples Cleaning LLC", + "businessAddress": "123 Main St, Naples, FL 34102", + "businessPhone": "(239) 555-0123", + "businessEmail": "support@naplescleaning.com", + "logoUrl": "https://cdn.a2pwizard.com/logos/abc123.png", + + "header": "Naples Cleaning LLC", + "icon": "broom", + "title": "Professional Cleaning Services in Naples, FL", + "description": "Naples Cleaning LLC provides reliable residential and commercial cleaning...", + "buttonText": "Book Your Cleaning Today", + "heroButtonLink": "#contact", + + "painpointsTitle": "Why Choose Professional Cleaning?", + "painpoints": [ + "Time-Consuming DIY Cleaning: Spend your weekends enjoying life...", + "Inconsistent Results: Professional-grade equipment and training...", + "Health Concerns: Deep cleaning eliminates allergens..." + ], + + "howItWorksTitle": "How It Works", + "howItWorksSteps": [ + "Step 1: Book online or call us", + "Step 2: We arrive and clean", + "Step 3: Enjoy your spotless space" + ], + + "faqTitle": "Frequently Asked Questions", + "faqItems": [ + {"question": "What areas do you serve?", "answer": "We serve Naples and surrounding areas..."}, + {"question": "Are you insured?", "answer": "Yes, we are fully licensed and insured..."} + ], + + "footerText": "© 2026 Naples Cleaning LLC. All rights reserved.", + + "optInForm": { + "enabled": true, + "consentText": "I consent to receive SMS notifications and alerts from Naples Cleaning LLC. Message frequency varies. Message & data rates may apply. Text HELP to (239) 555-0123 for assistance. Reply STOP to unsubscribe at any time.", + "privacyPolicyUrl": "/privacy-policy", + "termsUrl": "/terms-of-service" + }, + + "privacyPolicy": "...generated full privacy policy text...", + "termsOfService": "...generated full terms of service text...", + + "a2pPacket": { + "campaignDescription": "Messages are sent by Naples Cleaning LLC to customers who have opted in via the company website. Messages include appointment confirmations, reminders, and occasional promotional offers about cleaning services in the Naples, FL area.", + "sampleMessages": [ + "Naples Cleaning LLC: Your cleaning appointment is confirmed for [date] at [time]. Reply STOP to opt out.", + "Naples Cleaning LLC: Hi [name]! We have a special offer this month - 20% off deep cleaning services. Book at naplescleaning.com. Msg&data rates apply. Reply STOP to unsubscribe.", + "Naples Cleaning LLC: Reminder - your scheduled cleaning is tomorrow at [time]. Questions? Call (239) 555-0123. Reply HELP for help, STOP to opt out." + ], + "optInFlowDescription": "End users opt in by visiting the Naples Cleaning LLC website and filling out the contact form. Users must check a non-pre-selected checkbox to consent to receiving SMS messages. The opt-in form includes disclosure language about message frequency, data rates, and opt-out instructions. Privacy Policy and Terms of Service links are provided at the bottom of the form. Website URL: https://naplescleaning.a2pwizard.com/contact", + "optInConfirmation": "You're now subscribed to Naples Cleaning LLC updates. Msg frequency varies. Msg&data rates may apply. Reply HELP for help, STOP to cancel.", + "optOutConfirmation": "You have been unsubscribed from Naples Cleaning LLC. You will not receive any more messages from this number.", + "helpMessage": "Naples Cleaning LLC: For help, visit naplescleaning.com or call (239) 555-0123. Reply STOP to opt out.", + "optInKeywords": "START, OPTIN, UNSTOP, IN", + "optOutKeywords": "STOP, UNSUBSCRIBE, END, QUIT, CANCEL", + "helpKeywords": "HELP, INFO" + } +} +``` + +--- + +## PHASE 3: Website Generation + +### Tech Stack (from the GitHub repo: Getting-Automated/landing-page-generator) + +**React app** with these pre-built components: +- `HeaderBar` — Business name + icon +- `LandingHeader` — Hero section with CTA, reviews, optional video +- `IndustryPainpoints` — Pain points + optional lead capture form (Tally) +- `HowItWorks` — 3-step process +- `SocialValidation` — Testimonials +- `FAQSection` — Expandable FAQ +- `SecondCTA` — Final call to action +- `FooterBar` — Copyright + links + +**CRITICAL A2P COMPLIANCE PAGES** (these are what make the magic happen): + +1. **Contact/Opt-in Page** — Form with: + - Name, email, phone fields + - **Non-pre-selected SMS consent checkbox** with full disclosure text + - Separate marketing consent checkbox (optional) + - Privacy Policy link in footer + - Terms of Service link in footer + +2. **Privacy Policy page** (`/privacy-policy`) — Auto-generated, includes: + - Company name and contact info + - What data is collected + - **CRITICAL:** "No mobile information will be shared with third parties/affiliates for marketing/promotional purposes" + - How data is used + - Cookie policy + +3. **Terms of Service page** (`/terms-of-service`) — Auto-generated, includes: + - SMS messaging terms + - Message frequency disclosure + - Data rates disclaimer + - Opt-out instructions + - HELP instructions + +### Build Process +``` +config.json → React app reads config → npm run build → static HTML/CSS/JS +``` + +### Deployment +- **AWS S3 + CloudFront** (original "Domains to Dollars" approach) +- OR custom subdomain hosting (a2pwizard likely uses their own infra) +- Each client gets a unique URL like `clientname.a2pwizard.com` or a custom domain + +--- + +## PHASE 4: Screenshot Generation + +This is the **key differentiator** — screenshots are the #1 reason A2P campaigns get rejected. + +### What Screenshots Are Needed: +1. **Opt-in form screenshot** — Shows the contact form with the SMS consent checkbox, disclosure text, and Privacy Policy/ToS links visible +2. **Privacy Policy page screenshot** — Shows the live privacy policy page +3. **Terms of Service page screenshot** — Shows the live terms page +4. **Website homepage screenshot** — Shows the branded business website is real and functional + +### How to Generate: +- **Puppeteer/Playwright** (headless browser) +- Navigate to the deployed website +- Take full-page screenshots of each critical page +- Crop/annotate to highlight opt-in elements +- Upload to publicly accessible storage (S3, Google Drive, HighLevel media library) +- Generate shareable URLs for each screenshot + +### Screenshot Output Format: +- PNG files, clearly showing the opt-in flow +- Hosted at public URLs that TCR reviewers can access +- Typically 3-5 screenshots per client + +--- + +## PHASE 5: Compliance Packet Assembly & Delivery + +### What Gets Delivered to the Agency: + +**A complete copy-paste packet containing:** + +1. **Campaign Description** — Ready to paste into TCR/HighLevel campaign registration +2. **Message Flow / Opt-in Description** — Exactly how users consent, with website URL and screenshot URLs +3. **Sample Messages (2-3)** — With `[bracketed]` template fields, brand name, and opt-out language +4. **Opt-in Confirmation Message** — Under 160 chars +5. **Opt-out Confirmation Message** — With brand name +6. **Help Message** — With brand name + contact info +7. **Keywords** — Opt-in, opt-out, help keywords +8. **Screenshot URLs** — Publicly hosted images of opt-in flow +9. **Website URL** — Live, functional compliance website +10. **White-labeled export** — Agency branding, not A2P Wizard branding + +### Delivery Method: +- Email to agency with all assets +- Possibly a dashboard/portal to download +- PDF export option for client handoff + +--- + +## COMPLETE BACKEND PIPELINE (End to End) + +``` +[User fills form] + ↓ +[Webhook fires to automation engine (n8n)] + ↓ +[Logo uploaded to CDN/S3] + ↓ +[AI generates all content: website copy + A2P compliance text] + ↓ +[config.json assembled with all content + business info] + ↓ +[React app builds static site from config.json] + ↓ +[Static site deployed to hosting (S3/CloudFront or subdomain)] + ↓ +[Headless browser takes screenshots of deployed site] + ↓ +[Screenshots uploaded to public storage, URLs generated] + ↓ +[Compliance packet assembled (all text + screenshot URLs + website URL)] + ↓ +[Email sent to agency with complete packet + white-label export] + ↓ +[Agency copy/pastes everything into TCR/HighLevel campaign registration] + ↓ +[Campaign approved ✓] +``` + +--- + +## REBUILD TECH STACK RECOMMENDATION + +### Option A: n8n-Based (Like Original) +- **Form:** Custom form or Tally → webhook +- **Automation:** n8n workflow +- **AI:** Claude API or OpenAI API +- **Website:** React static site generator (fork Getting-Automated template) +- **Screenshots:** Puppeteer/Playwright in n8n or Lambda +- **Hosting:** AWS S3 + CloudFront or Cloudflare Pages +- **Storage:** S3 for logos + screenshots +- **Delivery:** Email via SendGrid/SES + +### Option B: All-in-One (Simpler) +- **Form + Backend:** Next.js API routes or Cloudflare Workers +- **AI:** Claude API +- **Website:** Dynamic server-rendered pages (no build step needed — just serve from config) +- **Screenshots:** Playwright on a serverless function +- **Hosting:** Cloudflare Workers/Pages (free tier generous) +- **Storage:** Cloudflare R2 for logos + screenshots +- **Delivery:** Email via Resend or SES + +### Option C: Minimal/Fast MVP +- **Form:** Simple HTML form → serverless function +- **AI:** Single Claude API call generates everything +- **Website:** Server-rendered HTML template (no React build step) +- **Screenshots:** Playwright +- **Hosting:** Single VPS or Cloudflare Workers +- **Everything in one codebase** + +--- + +## A2P COMPLIANCE REQUIREMENTS CHECKLIST + +For the generated website to pass TCR review, it MUST have: + +- [ ] Live, accessible website URL +- [ ] Business name prominently displayed +- [ ] Contact form with phone number field +- [ ] **SMS consent checkbox (NOT pre-selected, NOT required to submit)** +- [ ] Consent text with: program description, originating number, brand identity, opt-in language, fee disclosure, frequency disclosure, HELP instructions, STOP instructions +- [ ] Privacy Policy page (accessible, mentions no third-party sharing of mobile data) +- [ ] Terms of Service page (accessible, includes SMS terms) +- [ ] Links to Privacy Policy and ToS in form footer (NOT in checkbox text) +- [ ] Business email matching brand (not gmail for large corps) +- [ ] Consistent branding across website, sample messages, and campaign description + +### Sample Messages Must: +- [ ] Include brand name in at least one +- [ ] Use `[brackets]` for templated fields +- [ ] Include opt-out language in at least one ("Reply STOP to unsubscribe") +- [ ] Match the registered use case (don't say "OTP" if registered as Marketing) + +### Campaign Description Must: +- [ ] State who sends messages (brand name) +- [ ] State who receives messages (customers, opted-in users) +- [ ] State why messages are sent (purpose) +- [ ] Match the sample messages in intent + +--- + +## KEY FILES/REPOS TO REFERENCE + +- **Landing page React template:** https://github.com/Getting-Automated/landing-page-generator +- **n8n workflow pattern:** "Domains to Dollars" Part 1 (content generation) +- **HighLevel A2P best practices:** https://help.gohighlevel.com/support/solutions/articles/48001229784 +- **TCR campaign requirements:** Brand registration + Campaign registration via The Campaign Registry diff --git a/a2p-wizard-rebuild/package.json b/a2p-wizard-rebuild/package.json new file mode 100644 index 0000000..456f767 --- /dev/null +++ b/a2p-wizard-rebuild/package.json @@ -0,0 +1,21 @@ +{ + "name": "a2p-compliance-wizard", + "version": "1.0.0", + "description": "A2P 10DLC Compliance Wizard - Generate compliant websites and compliance packets", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "node --watch server.js" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", + "archiver": "^7.0.1", + "dotenv": "^16.4.7", + "ejs": "^3.1.10", + "express": "^4.21.1", + "multer": "^1.4.5-lts.1", + "nanoid": "^3.3.7", + "playwright": "^1.49.1", + "sharp": "^0.33.5" + } +} diff --git a/a2p-wizard-rebuild/public/css/form.css b/a2p-wizard-rebuild/public/css/form.css new file mode 100644 index 0000000..5bf89fa --- /dev/null +++ b/a2p-wizard-rebuild/public/css/form.css @@ -0,0 +1,551 @@ +/* ═══════════════════════════════════════════════ + A2P Compliance Wizard - Form Styles + Premium, modern design + ═══════════════════════════════════════════════ */ + +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + --primary: #2563EB; + --primary-dark: #1D4ED8; + --primary-light: #EFF6FF; + --accent: #7C3AED; + --accent-light: #F5F3FF; + --success: #059669; + --error: #DC2626; + --gray-50: #F9FAFB; + --gray-100: #F3F4F6; + --gray-200: #E5E7EB; + --gray-300: #D1D5DB; + --gray-400: #9CA3AF; + --gray-500: #6B7280; + --gray-600: #4B5563; + --gray-700: #374151; + --gray-800: #1F2937; + --gray-900: #111827; + --radius: 12px; + --radius-lg: 16px; + --shadow-sm: 0 1px 2px rgba(0,0,0,0.05); + --shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1); + --shadow-lg: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1); + --shadow-xl: 0 25px 50px -12px rgba(0,0,0,0.25); +} + +html { + scroll-behavior: smooth; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + background: var(--gray-50); + color: var(--gray-900); + line-height: 1.6; + min-height: 100vh; +} + +/* ── Background Pattern ── */ +.page-bg { + position: fixed; + inset: 0; + z-index: -1; + background: + radial-gradient(ellipse at 20% 50%, rgba(37, 99, 235, 0.08) 0%, transparent 50%), + radial-gradient(ellipse at 80% 20%, rgba(124, 58, 237, 0.06) 0%, transparent 50%), + radial-gradient(ellipse at 50% 100%, rgba(37, 99, 235, 0.04) 0%, transparent 50%), + var(--gray-50); +} + +/* ── Header ── */ +.header { + text-align: center; + padding: 60px 20px 20px; +} + +.header-badge { + display: inline-flex; + align-items: center; + gap: 8px; + background: var(--primary-light); + color: var(--primary); + font-size: 13px; + font-weight: 600; + padding: 6px 16px; + border-radius: 100px; + margin-bottom: 20px; + letter-spacing: 0.3px; +} + +.header-badge svg { + width: 16px; + height: 16px; +} + +.header h1 { + font-size: clamp(28px, 5vw, 42px); + font-weight: 800; + letter-spacing: -0.03em; + line-height: 1.15; + background: linear-gradient(135deg, var(--gray-900) 0%, var(--primary-dark) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 12px; +} + +.header p { + font-size: 17px; + color: var(--gray-500); + max-width: 520px; + margin: 0 auto; + line-height: 1.6; +} + +/* ── Steps indicator ── */ +.steps { + display: flex; + justify-content: center; + gap: 8px; + padding: 24px 20px; + max-width: 500px; + margin: 0 auto; +} + +.step { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + font-weight: 500; + color: var(--gray-400); +} + +.step-num { + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; + background: var(--gray-200); + color: var(--gray-500); + transition: all 0.3s ease; +} + +.step.active .step-num { + background: var(--primary); + color: white; + box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.2); +} + +.step.active { + color: var(--primary); +} + +.step.done .step-num { + background: var(--success); + color: white; +} + +.step-line { + width: 40px; + height: 2px; + background: var(--gray-200); + border-radius: 2px; +} + +/* ── Form Container ── */ +.form-container { + max-width: 640px; + margin: 0 auto 80px; + padding: 0 20px; +} + +.form-card { + background: white; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + border: 1px solid var(--gray-200); + overflow: hidden; +} + +.form-section { + padding: 32px; +} + +.form-section + .form-section { + border-top: 1px solid var(--gray-100); +} + +.section-title { + font-size: 16px; + font-weight: 700; + color: var(--gray-800); + margin-bottom: 4px; +} + +.section-subtitle { + font-size: 13px; + color: var(--gray-500); + margin-bottom: 24px; +} + +/* ── Form Fields ── */ +.form-group { + margin-bottom: 20px; +} + +.form-group:last-child { + margin-bottom: 0; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +label { + display: block; + font-size: 13px; + font-weight: 600; + color: var(--gray-700); + margin-bottom: 6px; +} + +label .required { + color: var(--error); + margin-left: 2px; +} + +input[type="text"], +input[type="email"], +input[type="tel"], +textarea { + width: 100%; + padding: 10px 14px; + font-size: 15px; + font-family: inherit; + border: 1.5px solid var(--gray-300); + border-radius: var(--radius); + background: white; + color: var(--gray-900); + transition: all 0.2s ease; + outline: none; +} + +input:focus, +textarea:focus { + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12); +} + +input::placeholder, +textarea::placeholder { + color: var(--gray-400); +} + +textarea { + resize: vertical; + min-height: 80px; +} + +.field-hint { + font-size: 12px; + color: var(--gray-400); + margin-top: 4px; +} + +/* ── Logo Upload ── */ +.logo-upload { + border: 2px dashed var(--gray-300); + border-radius: var(--radius); + padding: 32px; + text-align: center; + cursor: pointer; + transition: all 0.2s ease; + background: var(--gray-50); +} + +.logo-upload:hover { + border-color: var(--primary); + background: var(--primary-light); +} + +.logo-upload.has-file { + border-color: var(--success); + background: #F0FDF4; +} + +.logo-upload input[type="file"] { + display: none; +} + +.logo-upload-icon { + width: 48px; + height: 48px; + margin: 0 auto 12px; + border-radius: 12px; + background: var(--gray-200); + display: flex; + align-items: center; + justify-content: center; + color: var(--gray-500); + transition: all 0.2s ease; +} + +.logo-upload:hover .logo-upload-icon { + background: rgba(37, 99, 235, 0.1); + color: var(--primary); +} + +.logo-upload-text { + font-size: 14px; + font-weight: 600; + color: var(--gray-700); +} + +.logo-upload-hint { + font-size: 12px; + color: var(--gray-400); + margin-top: 4px; +} + +.logo-preview { + max-width: 200px; + max-height: 100px; + margin: 12px auto 0; + border-radius: 8px; +} + +/* ── Checkbox ── */ +.checkbox-group { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 16px; + background: var(--accent-light); + border-radius: var(--radius); + border: 1px solid rgba(124, 58, 237, 0.15); +} + +.checkbox-group input[type="checkbox"] { + width: 20px; + height: 20px; + margin-top: 2px; + flex-shrink: 0; + accent-color: var(--accent); + cursor: pointer; +} + +.checkbox-label { + font-size: 13px; + color: var(--gray-600); + line-height: 1.5; + cursor: pointer; +} + +/* ── Submit Button ── */ +.submit-section { + padding: 24px 32px; + background: var(--gray-50); + border-top: 1px solid var(--gray-100); +} + +.btn-submit { + width: 100%; + padding: 14px 28px; + font-size: 16px; + font-weight: 700; + font-family: inherit; + color: white; + background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%); + border: none; + border-radius: var(--radius); + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.btn-submit:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: 0 8px 25px rgba(37, 99, 235, 0.3); +} + +.btn-submit:active:not(:disabled) { + transform: translateY(0); +} + +.btn-submit:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.btn-text { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +/* ── Loading Overlay ── */ +.loading-overlay { + display: none; + position: fixed; + inset: 0; + z-index: 1000; + background: rgba(15, 23, 42, 0.7); + backdrop-filter: blur(8px); + justify-content: center; + align-items: center; +} + +.loading-overlay.active { + display: flex; +} + +.loading-card { + background: white; + border-radius: var(--radius-lg); + padding: 48px; + text-align: center; + max-width: 420px; + width: 90%; + box-shadow: var(--shadow-xl); +} + +.loading-spinner { + width: 56px; + height: 56px; + margin: 0 auto 24px; + border: 3px solid var(--gray-200); + border-top: 3px solid var(--primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.loading-title { + font-size: 20px; + font-weight: 700; + color: var(--gray-900); + margin-bottom: 8px; +} + +.loading-message { + font-size: 14px; + color: var(--gray-500); + margin-bottom: 32px; +} + +.progress-bar { + width: 100%; + height: 6px; + background: var(--gray-200); + border-radius: 3px; + overflow: hidden; + margin-bottom: 16px; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--primary), var(--accent)); + border-radius: 3px; + width: 0%; + transition: width 0.5s ease; +} + +.progress-steps { + display: flex; + flex-direction: column; + gap: 8px; + text-align: left; +} + +.progress-step { + display: flex; + align-items: center; + gap: 10px; + font-size: 13px; + color: var(--gray-400); + transition: color 0.3s ease; +} + +.progress-step.active { + color: var(--primary); + font-weight: 600; +} + +.progress-step.done { + color: var(--success); +} + +.progress-step-icon { + width: 20px; + height: 20px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; +} + +/* ── Error ── */ +.error-banner { + display: none; + background: #FEF2F2; + border: 1px solid #FECACA; + color: var(--error); + padding: 12px 16px; + border-radius: var(--radius); + font-size: 14px; + margin-bottom: 20px; + animation: fadeIn 0.3s ease; +} + +.error-banner.visible { + display: block; +} + +/* ── Footer ── */ +.footer { + text-align: center; + padding: 40px 20px; + color: var(--gray-400); + font-size: 13px; +} + +/* ── Animations ── */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +/* ── Responsive ── */ +@media (max-width: 640px) { + .form-row { + grid-template-columns: 1fr; + } + + .form-section { + padding: 24px 20px; + } + + .submit-section { + padding: 20px; + } + + .steps { + flex-wrap: wrap; + } +} diff --git a/a2p-wizard-rebuild/public/css/site.css b/a2p-wizard-rebuild/public/css/site.css new file mode 100644 index 0000000..98ab0fc --- /dev/null +++ b/a2p-wizard-rebuild/public/css/site.css @@ -0,0 +1,737 @@ +/* ═══════════════════════════════════════════════ + A2P Compliance Wizard - Generated Site Styles + Premium SaaS-quality landing page + ═══════════════════════════════════════════════ */ + +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap'); + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + --primary: #2563EB; + --primary-dark: #1D4ED8; + --primary-light: #EFF6FF; + --accent: #F59E0B; + --gray-50: #F9FAFB; + --gray-100: #F3F4F6; + --gray-200: #E5E7EB; + --gray-300: #D1D5DB; + --gray-400: #9CA3AF; + --gray-500: #6B7280; + --gray-600: #4B5563; + --gray-700: #374151; + --gray-800: #1F2937; + --gray-900: #111827; + --radius: 12px; + --radius-lg: 16px; + --radius-xl: 24px; +} + +html { scroll-behavior: smooth; } + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; + color: var(--gray-900); + line-height: 1.6; + background: white; + -webkit-font-smoothing: antialiased; +} + +a { color: var(--primary); text-decoration: none; } +a:hover { text-decoration: underline; } + +img { max-width: 100%; height: auto; } + +.container { + max-width: 1140px; + margin: 0 auto; + padding: 0 24px; +} + +/* ── Navigation ── */ +.nav { + position: sticky; + top: 0; + z-index: 100; + background: rgba(255,255,255,0.85); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-bottom: 1px solid var(--gray-200); + padding: 0 24px; +} + +.nav-inner { + max-width: 1140px; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + height: 64px; +} + +.nav-brand { + display: flex; + align-items: center; + gap: 10px; + font-weight: 800; + font-size: 18px; + color: var(--gray-900); + text-decoration: none; +} + +.nav-brand img { + height: 36px; + width: auto; + border-radius: 6px; +} + +.nav-links { + display: flex; + align-items: center; + gap: 32px; + list-style: none; +} + +.nav-links a { + font-size: 14px; + font-weight: 500; + color: var(--gray-600); + text-decoration: none; + transition: color 0.2s; +} + +.nav-links a:hover { + color: var(--primary); + text-decoration: none; +} + +.nav-cta { + background: var(--primary) !important; + color: white !important; + padding: 8px 20px; + border-radius: 8px; + font-weight: 600 !important; + transition: all 0.2s ease !important; +} + +.nav-cta:hover { + background: var(--primary-dark) !important; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(37,99,235,0.3); +} + +.nav-mobile-toggle { + display: none; + background: none; + border: none; + cursor: pointer; + padding: 8px; + color: var(--gray-700); +} + +/* ── Hero ── */ +.hero { + padding: 100px 0 80px; + text-align: center; + background: + radial-gradient(ellipse at 30% 0%, rgba(37,99,235,0.07) 0%, transparent 50%), + radial-gradient(ellipse at 70% 100%, rgba(37,99,235,0.05) 0%, transparent 50%), + white; + position: relative; + overflow: hidden; +} + +.hero::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle, rgba(37,99,235,0.03) 1px, transparent 1px); + background-size: 40px 40px; + z-index: 0; +} + +.hero .container { + position: relative; + z-index: 1; +} + +.hero-badge { + display: inline-flex; + align-items: center; + gap: 6px; + background: var(--primary-light); + color: var(--primary); + font-size: 13px; + font-weight: 600; + padding: 6px 16px; + border-radius: 100px; + margin-bottom: 24px; +} + +.hero h1 { + font-size: clamp(36px, 6vw, 60px); + font-weight: 900; + line-height: 1.1; + letter-spacing: -0.03em; + margin-bottom: 20px; + max-width: 700px; + margin-left: auto; + margin-right: auto; +} + +.hero-subtitle { + font-size: clamp(17px, 2.5vw, 20px); + color: var(--gray-500); + max-width: 560px; + margin: 0 auto 36px; + line-height: 1.6; +} + +.hero-cta { + display: inline-flex; + align-items: center; + gap: 8px; + background: var(--primary); + color: white; + font-size: 17px; + font-weight: 700; + padding: 16px 36px; + border-radius: var(--radius); + text-decoration: none; + transition: all 0.3s ease; + box-shadow: 0 4px 14px rgba(37,99,235,0.3); +} + +.hero-cta:hover { + background: var(--primary-dark); + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(37,99,235,0.35); + text-decoration: none; + color: white; +} + +.hero-cta svg { + width: 20px; + height: 20px; +} + +/* ── Section common ── */ +.section { + padding: 80px 0; +} + +.section-alt { + background: var(--gray-50); +} + +.section-header { + text-align: center; + margin-bottom: 56px; +} + +.section-header h2 { + font-size: clamp(28px, 4vw, 40px); + font-weight: 800; + letter-spacing: -0.02em; + margin-bottom: 12px; +} + +.section-header p { + font-size: 17px; + color: var(--gray-500); + max-width: 520px; + margin: 0 auto; +} + +/* ── Pain Points ── */ +.painpoints-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 24px; +} + +.painpoint-card { + background: white; + border: 1px solid var(--gray-200); + border-radius: var(--radius-lg); + padding: 32px; + transition: all 0.3s ease; +} + +.section-alt .painpoint-card { + background: white; +} + +.painpoint-card:hover { + transform: translateY(-4px); + box-shadow: 0 12px 32px rgba(0,0,0,0.08); + border-color: transparent; +} + +.painpoint-icon { + width: 52px; + height: 52px; + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 20px; + background: var(--primary-light); + color: var(--primary); +} + +.painpoint-card h3 { + font-size: 18px; + font-weight: 700; + margin-bottom: 8px; + color: var(--gray-900); +} + +.painpoint-card p { + font-size: 15px; + color: var(--gray-500); + line-height: 1.6; +} + +/* ── How It Works ── */ +.steps-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 32px; + position: relative; +} + +.steps-grid::before { + content: ''; + position: absolute; + top: 44px; + left: 15%; + right: 15%; + height: 2px; + background: linear-gradient(90deg, var(--primary), var(--gray-300), var(--primary)); + z-index: 0; +} + +.step-card { + text-align: center; + position: relative; + z-index: 1; +} + +.step-number { + width: 56px; + height: 56px; + border-radius: 50%; + background: var(--primary); + color: white; + font-size: 22px; + font-weight: 800; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 20px; + box-shadow: 0 4px 14px rgba(37,99,235,0.3); +} + +.step-card h3 { + font-size: 18px; + font-weight: 700; + margin-bottom: 8px; +} + +.step-card p { + font-size: 15px; + color: var(--gray-500); + max-width: 280px; + margin: 0 auto; + line-height: 1.6; +} + +/* ── FAQ ── */ +.faq-list { + max-width: 720px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 12px; +} + +.faq-item { + background: white; + border: 1px solid var(--gray-200); + border-radius: var(--radius); + overflow: hidden; + transition: all 0.2s ease; +} + +.faq-item:hover { + border-color: var(--gray-300); +} + +.faq-question { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 20px 24px; + background: none; + border: none; + cursor: pointer; + font-family: inherit; + font-size: 16px; + font-weight: 600; + color: var(--gray-900); + text-align: left; + transition: color 0.2s; +} + +.faq-question:hover { + color: var(--primary); +} + +.faq-icon { + width: 24px; + height: 24px; + flex-shrink: 0; + color: var(--gray-400); + transition: transform 0.3s ease; +} + +.faq-item.open .faq-icon { + transform: rotate(45deg); + color: var(--primary); +} + +.faq-answer { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; +} + +.faq-answer-inner { + padding: 0 24px 20px; + font-size: 15px; + color: var(--gray-500); + line-height: 1.7; +} + +/* ── CTA Section ── */ +.cta-section { + padding: 80px 0; + text-align: center; + background: linear-gradient(135deg, var(--gray-900) 0%, #1e3a5f 100%); + color: white; +} + +.cta-section h2 { + font-size: clamp(28px, 4vw, 40px); + font-weight: 800; + margin-bottom: 16px; +} + +.cta-section p { + font-size: 17px; + color: rgba(255,255,255,0.7); + max-width: 480px; + margin: 0 auto 32px; +} + +.cta-section .hero-cta { + background: white; + color: var(--gray-900); + box-shadow: 0 4px 14px rgba(0,0,0,0.2); +} + +.cta-section .hero-cta:hover { + background: var(--gray-100); + color: var(--gray-900); +} + +/* ── Contact / Opt-in Form ── */ +.contact-section { + padding: 80px 0; +} + +.contact-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 56px; + align-items: start; +} + +.contact-info h2 { + font-size: 32px; + font-weight: 800; + margin-bottom: 16px; +} + +.contact-info p { + font-size: 16px; + color: var(--gray-500); + margin-bottom: 32px; + line-height: 1.6; +} + +.contact-detail { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; + font-size: 15px; + color: var(--gray-600); +} + +.contact-detail-icon { + width: 40px; + height: 40px; + border-radius: 10px; + background: var(--primary-light); + color: var(--primary); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.contact-form-card { + background: white; + border: 1px solid var(--gray-200); + border-radius: var(--radius-lg); + padding: 36px; + box-shadow: var(--shadow-lg); +} + +.contact-form .form-group { + margin-bottom: 16px; +} + +.contact-form label { + font-size: 13px; + font-weight: 600; + color: var(--gray-700); + margin-bottom: 6px; + display: block; +} + +.contact-form input, +.contact-form textarea { + width: 100%; + padding: 11px 14px; + font-size: 15px; + font-family: inherit; + border: 1.5px solid var(--gray-300); + border-radius: 10px; + background: white; + color: var(--gray-900); + outline: none; + transition: all 0.2s ease; +} + +.contact-form input:focus, +.contact-form textarea:focus { + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(37,99,235,0.12); +} + +.consent-group { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 16px; + background: var(--gray-50); + border: 1px solid var(--gray-200); + border-radius: 10px; + margin-top: 20px; +} + +.consent-group input[type="checkbox"] { + width: 20px; + height: 20px; + margin-top: 2px; + flex-shrink: 0; + accent-color: var(--primary); + cursor: pointer; +} + +.consent-text { + font-size: 12px; + color: var(--gray-500); + line-height: 1.6; +} + +.contact-form .btn-submit { + width: 100%; + padding: 14px 28px; + font-size: 16px; + font-weight: 700; + font-family: inherit; + color: white; + background: var(--primary); + border: none; + border-radius: 10px; + cursor: pointer; + margin-top: 20px; + transition: all 0.2s ease; +} + +.contact-form .btn-submit:hover { + background: var(--primary-dark); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(37,99,235,0.3); +} + +/* ── Legal Pages ── */ +.legal-page { + padding: 60px 0 80px; +} + +.legal-page .container { + max-width: 780px; +} + +.legal-page h1 { + font-size: 36px; + font-weight: 800; + margin-bottom: 8px; +} + +.legal-meta { + font-size: 14px; + color: var(--gray-500); + margin-bottom: 40px; + padding-bottom: 24px; + border-bottom: 1px solid var(--gray-200); +} + +.legal-content { + font-size: 15px; + color: var(--gray-600); + line-height: 1.8; +} + +.legal-content h2 { + font-size: 22px; + font-weight: 700; + color: var(--gray-900); + margin: 36px 0 12px; +} + +.legal-content h3 { + font-size: 18px; + font-weight: 600; + color: var(--gray-800); + margin: 28px 0 8px; +} + +.legal-content p { + margin-bottom: 16px; +} + +.legal-content ul, .legal-content ol { + margin: 12px 0 16px 24px; +} + +.legal-content li { + margin-bottom: 8px; +} + +/* ── Footer ── */ +.footer { + background: var(--gray-900); + color: rgba(255,255,255,0.6); + padding: 48px 0; +} + +.footer-inner { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 20px; +} + +.footer-brand { + font-size: 16px; + font-weight: 700; + color: white; +} + +.footer-links { + display: flex; + gap: 24px; + list-style: none; +} + +.footer-links a { + font-size: 14px; + color: rgba(255,255,255,0.5); + text-decoration: none; + transition: color 0.2s; +} + +.footer-links a:hover { + color: white; + text-decoration: none; +} + +.footer-copy { + width: 100%; + text-align: center; + font-size: 13px; + margin-top: 24px; + padding-top: 24px; + border-top: 1px solid rgba(255,255,255,0.1); +} + +/* ── Mobile ── */ +@media (max-width: 768px) { + .nav-links { display: none; } + .nav-mobile-toggle { display: block; } + + .nav-links.open { + display: flex; + flex-direction: column; + position: absolute; + top: 64px; + left: 0; + right: 0; + background: white; + border-bottom: 1px solid var(--gray-200); + padding: 16px 24px; + gap: 16px; + box-shadow: 0 8px 24px rgba(0,0,0,0.1); + } + + .steps-grid { + grid-template-columns: 1fr; + gap: 40px; + } + + .steps-grid::before { display: none; } + + .contact-grid { + grid-template-columns: 1fr; + gap: 40px; + } + + .hero { padding: 60px 0 50px; } + .section { padding: 60px 0; } + + .footer-inner { + flex-direction: column; + text-align: center; + } +} + +/* ── SVG Icon helpers ── */ +.icon-svg { + width: 24px; + height: 24px; +} diff --git a/a2p-wizard-rebuild/public/js/form.js b/a2p-wizard-rebuild/public/js/form.js new file mode 100644 index 0000000..41b5332 --- /dev/null +++ b/a2p-wizard-rebuild/public/js/form.js @@ -0,0 +1,204 @@ +// ═══════════════════════════════════════════════ +// A2P Compliance Wizard - Form Handler +// ═══════════════════════════════════════════════ + +document.addEventListener('DOMContentLoaded', () => { + const form = document.getElementById('wizardForm'); + const submitBtn = document.getElementById('submitBtn'); + const overlay = document.getElementById('loadingOverlay'); + const errorBanner = document.getElementById('errorBanner'); + const logoInput = document.getElementById('logoInput'); + const logoUpload = document.querySelector('.logo-upload'); + const logoPreview = document.getElementById('logoPreview'); + const logoUploadText = document.querySelector('.logo-upload-text'); + + // Logo upload preview + logoUpload.addEventListener('click', () => logoInput.click()); + logoUpload.addEventListener('dragover', (e) => { + e.preventDefault(); + logoUpload.style.borderColor = '#2563EB'; + logoUpload.style.background = '#EFF6FF'; + }); + logoUpload.addEventListener('dragleave', () => { + logoUpload.style.borderColor = ''; + logoUpload.style.background = ''; + }); + logoUpload.addEventListener('drop', (e) => { + e.preventDefault(); + logoUpload.style.borderColor = ''; + logoUpload.style.background = ''; + if (e.dataTransfer.files.length) { + logoInput.files = e.dataTransfer.files; + handleLogoSelect(); + } + }); + + logoInput.addEventListener('change', handleLogoSelect); + + function handleLogoSelect() { + const file = logoInput.files[0]; + if (!file) return; + + if (!file.type.startsWith('image/')) { + showError('Please upload an image file (PNG, JPG, SVG, WebP)'); + return; + } + + if (file.size > 5 * 1024 * 1024) { + showError('Logo must be under 5MB'); + return; + } + + const reader = new FileReader(); + reader.onload = (e) => { + logoPreview.src = e.target.result; + logoPreview.style.display = 'block'; + logoUpload.classList.add('has-file'); + logoUploadText.textContent = file.name; + }; + reader.readAsDataURL(file); + } + + // Form submission + form.addEventListener('submit', async (e) => { + e.preventDefault(); + hideError(); + + // Validate + if (!validateForm()) return; + + // Show loading + overlay.classList.add('active'); + submitBtn.disabled = true; + updateProgress(0, 'Preparing your request...'); + + try { + const formData = new FormData(form); + + // Simulate progress during generation + const progressInterval = simulateProgress(); + + const response = await fetch('/api/generate', { + method: 'POST', + body: formData, + }); + + clearInterval(progressInterval); + + const result = await response.json(); + + if (!result.success) { + throw new Error(result.error || 'Generation failed'); + } + + // Show completion + updateProgress(100, 'Complete!'); + setStepDone(0); + setStepDone(1); + setStepDone(2); + setStepDone(3); + + // Redirect to results + setTimeout(() => { + window.location.href = result.resultsUrl; + }, 800); + + } catch (err) { + overlay.classList.remove('active'); + submitBtn.disabled = false; + showError(err.message || 'Something went wrong. Please try again.'); + } + }); + + function validateForm() { + const required = ['agencyName', 'agencyEmail', 'businessName', 'businessAddress', 'businessEmail', 'businessPhone', 'businessDescription']; + + for (const field of required) { + const input = form.querySelector(`[name="${field}"]`); + if (!input || !input.value.trim()) { + showError(`Please fill in all required fields`); + input?.focus(); + return false; + } + } + + // Validate email + const email = form.querySelector('[name="agencyEmail"]').value; + if (!email.includes('@') || !email.includes('.')) { + showError('Please enter a valid email address'); + return false; + } + + // Validate checkbox + const checkbox = form.querySelector('[name="tcpaConsent"]'); + if (!checkbox || !checkbox.checked) { + showError('Please confirm TCPA compliance to continue'); + return false; + } + + return true; + } + + function simulateProgress() { + let progress = 0; + const steps = [ + { at: 10, msg: 'Generating AI content...' }, + { at: 45, msg: 'Building website pages...' }, + { at: 65, msg: 'Taking screenshots...' }, + { at: 85, msg: 'Assembling compliance packet...' }, + ]; + let stepIndex = 0; + + return setInterval(() => { + if (progress < 90) { + progress += Math.random() * 3 + 0.5; + progress = Math.min(progress, 92); + updateProgress(progress); + + if (stepIndex < steps.length && progress >= steps[stepIndex].at) { + setStepActive(stepIndex); + if (stepIndex > 0) setStepDone(stepIndex - 1); + document.querySelector('.loading-message').textContent = steps[stepIndex].msg; + stepIndex++; + } + } + }, 500); + } + + function updateProgress(pct, msg) { + const fill = document.querySelector('.progress-fill'); + if (fill) fill.style.width = `${pct}%`; + if (msg) { + const el = document.querySelector('.loading-message'); + if (el) el.textContent = msg; + } + } + + function setStepActive(index) { + const steps = document.querySelectorAll('.progress-step'); + if (steps[index]) { + steps[index].classList.remove('done'); + steps[index].classList.add('active'); + } + } + + function setStepDone(index) { + const steps = document.querySelectorAll('.progress-step'); + if (steps[index]) { + steps[index].classList.remove('active'); + steps[index].classList.add('done'); + const icon = steps[index].querySelector('.progress-step-icon'); + if (icon) icon.innerHTML = ''; + } + } + + function showError(msg) { + errorBanner.textContent = msg; + errorBanner.classList.add('visible'); + errorBanner.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + + function hideError() { + errorBanner.classList.remove('visible'); + } +}); diff --git a/a2p-wizard-rebuild/routes/api.js b/a2p-wizard-rebuild/routes/api.js new file mode 100644 index 0000000..cce1d92 --- /dev/null +++ b/a2p-wizard-rebuild/routes/api.js @@ -0,0 +1,126 @@ +const express = require('express'); +const router = express.Router(); +const multer = require('multer'); +const path = require('path'); +const fs = require('fs'); +const { nanoid } = require('nanoid'); +const { generateContent } = require('../services/ai-generator'); +const { buildSite } = require('../services/site-builder'); +const { takeScreenshots } = require('../services/screenshot'); +const { assemblePacket } = require('../services/packet-assembler'); + +// Configure multer for logo uploads +const storage = multer.memoryStorage(); +const upload = multer({ + storage, + limits: { fileSize: 5 * 1024 * 1024 }, // 5MB + fileFilter: (req, file, cb) => { + const allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml']; + if (allowed.includes(file.mimetype)) { + cb(null, true); + } else { + cb(new Error('Invalid file type. Only images are allowed.')); + } + } +}); + +// POST /api/generate - Main pipeline +router.post('/generate', upload.single('logo'), async (req, res) => { + const clientId = nanoid(12); + const clientDir = path.join(__dirname, '..', 'data', clientId); + + try { + // Create client directory + fs.mkdirSync(path.join(clientDir, 'screenshots'), { recursive: true }); + fs.mkdirSync(path.join(clientDir, 'site'), { recursive: true }); + + // Parse form data + const formData = { + agencyName: req.body.agencyName, + agencyEmail: req.body.agencyEmail, + businessName: req.body.businessName, + businessAddress: req.body.businessAddress, + businessEmail: req.body.businessEmail, + businessPhone: req.body.businessPhone, + businessDescription: req.body.businessDescription, + }; + + // Save logo if uploaded + let logoPath = null; + if (req.file) { + const ext = path.extname(req.file.originalname) || '.png'; + logoPath = path.join(clientDir, `logo${ext}`); + fs.writeFileSync(logoPath, req.file.buffer); + formData.logoUrl = `/data/${clientId}/logo${ext}`; + } + + // Step 1: Generate AI content + console.log(`[${clientId}] Step 1: Generating AI content...`); + const aiContent = await generateContent(formData); + + // Step 2: Build config — generate subdomain from business name + const MAIN_DOMAIN = req.app.locals.MAIN_DOMAIN || 'solvedby.us'; + const subdomain = formData.businessName + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, '') + .substring(0, 40); + + const siteUrl = `https://${subdomain}.${MAIN_DOMAIN}`; + const localSiteUrl = `http://localhost:8899/sites/${clientId}`; + + const config = { + clientId, + subdomain, + ...formData, + ...aiContent, + siteUrl, + localSiteUrl, + generatedAt: new Date().toISOString(), + }; + + // Save config + fs.writeFileSync( + path.join(clientDir, 'config.json'), + JSON.stringify(config, null, 2) + ); + + // Step 3: Build the static site + console.log(`[${clientId}] Step 2: Building website...`); + await buildSite(config, clientDir); + + // Step 4: Take screenshots + console.log(`[${clientId}] Step 3: Taking screenshots...`); + await takeScreenshots(clientId, clientDir); + + // Step 5: Assemble compliance packet zip + console.log(`[${clientId}] Step 4: Assembling compliance packet...`); + await assemblePacket(config, clientDir); + + // Step 6: Register subdomain → clientId mapping + const subdomainIndex = req.app.locals.subdomainIndex; + const saveSubdomainIndex = req.app.locals.saveSubdomainIndex; + subdomainIndex[subdomain] = clientId; + saveSubdomainIndex(); + console.log(`[${clientId}] Step 5: Registered subdomain ${subdomain}.${MAIN_DOMAIN}`); + + console.log(`[${clientId}] ✅ Complete! Site live at ${siteUrl}`); + + res.json({ + success: true, + clientId, + resultsUrl: `/results/${clientId}`, + siteUrl, + localSiteUrl: `/sites/${clientId}/`, + }); + + } catch (error) { + console.error(`[${clientId}] ❌ Error:`, error); + res.status(500).json({ + success: false, + error: error.message || 'Generation failed', + }); + } +}); + +module.exports = router; diff --git a/a2p-wizard-rebuild/routes/sites.js b/a2p-wizard-rebuild/routes/sites.js new file mode 100644 index 0000000..4865b3a --- /dev/null +++ b/a2p-wizard-rebuild/routes/sites.js @@ -0,0 +1,52 @@ +const express = require('express'); +const router = express.Router(); +const path = require('path'); +const fs = require('fs'); + +// Serve generated static sites at /sites/{clientId}/ +// (Subdomain serving is handled in server.js middleware) + +const pageMap = { + '': 'index.html', + 'contact': 'contact.html', + 'privacy-policy': 'privacy-policy.html', + 'terms': 'terms.html', +}; + +// Serve static assets for the site (CSS, images, etc.) +router.use('/:clientId/assets', (req, res) => { + const { clientId } = req.params; + const filePath = path.join(__dirname, '..', 'data', clientId, 'site', 'assets', req.path); + if (fs.existsSync(filePath)) { + res.sendFile(filePath); + } else { + res.status(404).send('Not found'); + } +}); + +// Serve pages +router.get('/:clientId/:page?', (req, res) => { + const { clientId } = req.params; + const page = (req.params.page || '').replace(/\/$/, ''); + const filename = pageMap[page]; + + if (!filename) { + return res.status(404).send('Page not found'); + } + + const filePath = path.join(__dirname, '..', 'data', clientId, 'site', filename); + if (!fs.existsSync(filePath)) { + return res.status(404).send('Site not found'); + } + + // When served via /sites/{id}/, rewrite links to include the prefix + let html = fs.readFileSync(filePath, 'utf8'); + const baseUrl = `/sites/${clientId}`; + html = html.replace(/href="\//g, `href="${baseUrl}/`); + html = html.replace(/src="\//g, `src="${baseUrl}/`); + html = html.replace(/href="#/g, 'href="#'); // keep anchor links + + res.type('html').send(html); +}); + +module.exports = router; diff --git a/a2p-wizard-rebuild/server.js b/a2p-wizard-rebuild/server.js new file mode 100644 index 0000000..0cb34ab --- /dev/null +++ b/a2p-wizard-rebuild/server.js @@ -0,0 +1,144 @@ +require('dotenv').config(); +const express = require('express'); +const path = require('path'); +const fs = require('fs'); + +const app = express(); +const PORT = process.env.PORT || 8899; +const MAIN_DOMAIN = process.env.MAIN_DOMAIN || 'solvedby.us'; + +// Ensure data directory exists +const dataDir = path.join(__dirname, 'data'); +if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); +} + +// Subdomain-to-clientId index (loaded from disk) +const subdomainIndexPath = path.join(dataDir, 'subdomain-index.json'); +let subdomainIndex = {}; +if (fs.existsSync(subdomainIndexPath)) { + subdomainIndex = JSON.parse(fs.readFileSync(subdomainIndexPath, 'utf8')); +} + +function saveSubdomainIndex() { + fs.writeFileSync(subdomainIndexPath, JSON.stringify(subdomainIndex, null, 2)); +} + +function getSubdomain(host) { + // Strip port + const hostname = (host || '').split(':')[0]; + // Check if it's a subdomain of MAIN_DOMAIN + if (hostname.endsWith('.' + MAIN_DOMAIN)) { + return hostname.replace('.' + MAIN_DOMAIN, '').toLowerCase(); + } + return null; +} + +// View engine +app.set('view engine', 'ejs'); +app.set('views', path.join(__dirname, 'templates')); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); + +// ─── Subdomain routing: {clientname}.solvedby.us serves that client's site ─── +app.use((req, res, next) => { + const subdomain = getSubdomain(req.headers.host); + + if (!subdomain) { + // No subdomain — serve the main wizard app + return next(); + } + + // Look up clientId from subdomain + const clientId = subdomainIndex[subdomain]; + if (!clientId) { + return res.status(404).send(`

Site not found

No business site exists at ${subdomain}.${MAIN_DOMAIN}

Create one at ${MAIN_DOMAIN}

`); + } + + const siteDir = path.join(dataDir, clientId, 'site'); + if (!fs.existsSync(siteDir)) { + return res.status(404).send('Site files not found'); + } + + // Map paths to files + let filePath; + const urlPath = req.path.replace(/\/$/, '') || ''; + + if (urlPath === '' || urlPath === '/') { + filePath = path.join(siteDir, 'index.html'); + } else if (urlPath === '/contact') { + filePath = path.join(siteDir, 'contact.html'); + } else if (urlPath === '/privacy-policy') { + filePath = path.join(siteDir, 'privacy-policy.html'); + } else if (urlPath === '/terms') { + filePath = path.join(siteDir, 'terms.html'); + } else if (urlPath.startsWith('/assets/')) { + filePath = path.join(siteDir, urlPath); + } else if (urlPath.startsWith('/data/')) { + // Allow serving logos etc from data dir + filePath = path.join(__dirname, urlPath); + } else { + return res.status(404).send('Page not found'); + } + + if (fs.existsSync(filePath)) { + return res.sendFile(filePath); + } + return res.status(404).send('Page not found'); +}); + +// ─── Main domain routes ─── +app.use('/public', express.static(path.join(__dirname, 'public'))); +app.use('/data', express.static(path.join(__dirname, 'data'))); + +// Routes +const apiRoutes = require('./routes/api'); +const sitesRoutes = require('./routes/sites'); + +app.use('/api', apiRoutes); +app.use('/sites', sitesRoutes); + +// Main form page +app.get('/', (req, res) => { + res.render('form'); +}); + +// Results page +app.get('/results/:clientId', (req, res) => { + const { clientId } = req.params; + const configPath = path.join(dataDir, clientId, 'config.json'); + + if (!fs.existsSync(configPath)) { + return res.status(404).send('Project not found'); + } + + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + res.render('packet/results', { config, clientId }); +}); + +// Download zip +app.get('/download/:clientId', (req, res) => { + const { clientId } = req.params; + const zipPath = path.join(dataDir, clientId, 'compliance-packet.zip'); + + if (!fs.existsSync(zipPath)) { + return res.status(404).send('Zip not found'); + } + + const config = JSON.parse(fs.readFileSync(path.join(dataDir, clientId, 'config.json'), 'utf8')); + const safeName = config.businessName.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase(); + res.download(zipPath, `${safeName}-a2p-compliance-packet.zip`); +}); + +// Export for use in API routes +app.locals.subdomainIndex = subdomainIndex; +app.locals.saveSubdomainIndex = saveSubdomainIndex; +app.locals.MAIN_DOMAIN = MAIN_DOMAIN; + +app.listen(PORT, () => { + console.log(`\n🚀 A2P Compliance Wizard running at http://localhost:${PORT}`); + console.log(` Main domain: https://${MAIN_DOMAIN}`); + console.log(` Client sites: https://{name}.${MAIN_DOMAIN}\n`); +}); diff --git a/a2p-wizard-rebuild/services/ai-generator.js b/a2p-wizard-rebuild/services/ai-generator.js new file mode 100644 index 0000000..d23a791 --- /dev/null +++ b/a2p-wizard-rebuild/services/ai-generator.js @@ -0,0 +1,112 @@ +const Anthropic = require('@anthropic-ai/sdk'); + +// Support both standard API keys and OAuth tokens +const apiKey = process.env.ANTHROPIC_API_KEY || ''; +const clientOpts = {}; +if (apiKey.startsWith('sk-ant-oat')) { + // OAuth token — use as auth token instead of API key + clientOpts.authToken = apiKey; + clientOpts.apiKey = null; +} else { + clientOpts.apiKey = apiKey; +} +const client = new Anthropic(clientOpts); + +async function generateContent(formData) { + const { businessName, businessAddress, businessEmail, businessPhone, businessDescription } = formData; + + const prompt = `You are an expert A2P 10DLC compliance consultant and web copywriter. Generate ALL content for a professional business website AND a complete A2P SMS compliance packet. + +Business Details: +- Legal Business Name: ${businessName} +- Business Address: ${businessAddress} +- Support Email: ${businessEmail} +- Business Phone: ${businessPhone} +- Business Description: ${businessDescription} + +Generate the following as a single JSON object. Be specific, professional, and thorough. All content should be tailored to this specific business and industry. + +IMPORTANT: Return ONLY valid JSON. No markdown code fences, no explanations outside the JSON. + +{ + "website": { + "heroTitle": "A compelling headline for the business (5-10 words)", + "heroSubtitle": "A supporting subtitle (15-25 words)", + "ctaText": "CTA button text (2-4 words)", + "expandedDescription": "A 3-4 sentence professional business description", + "painpoints": [ + { + "icon": "one of: shield, clock, chart, heart, star, zap, target, users, check, phone", + "title": "Pain point title (3-6 words)", + "description": "Pain point description (1-2 sentences)" + } + ], + "howItWorks": [ + { + "step": 1, + "title": "Step title (2-5 words)", + "description": "Step description (1-2 sentences)" + } + ], + "faqItems": [ + { + "question": "A relevant FAQ question", + "answer": "A thorough, helpful answer (2-4 sentences)" + } + ], + "privacyPolicy": "COMPLETE privacy policy text in HTML format. MUST include: company name, data collection practices, how info is used, cookie policy, third-party sharing policy, and CRITICALLY: 'No mobile information will be shared with third parties/affiliates for marketing/promotional purposes. All the above categories exclude text messaging originator opt-in data and consent; this information will not be shared with any third parties.' Include contact info. Make it thorough and professional.", + "termsOfService": "COMPLETE terms of service text in HTML format. MUST include: SMS/text messaging terms section covering message frequency, data rates, opt-out (STOP), help (HELP), supported carriers, and that consent is not a condition of purchase. Include standard legal terms. Make it thorough." + }, + "a2pPacket": { + "campaignDescription": "A detailed description of who sends messages (brand name), who receives them (opted-in customers via website), and why (purpose). 3-5 sentences. Reference the website URL.", + "sampleMessages": [ + "First sample message with brand name and [bracketed] template fields. Include opt-out.", + "Second sample message - different type (reminder/promo/info). Include brand name.", + "Third sample message - yet another type. Include HELP instructions." + ], + "optInFlowDescription": "Detailed description of how users opt in: they visit the website, fill out the contact form, check a non-pre-selected SMS consent checkbox. Describe the disclosure language on the form. Mention Privacy Policy and Terms links.", + "optInConfirmation": "Under 160 chars. Brand name, frequency, rates, HELP, STOP instructions.", + "optOutConfirmation": "Brand name + confirmation that no more messages will be sent.", + "helpMessage": "Brand name + support contact info + opt-out instructions.", + "optInKeywords": "START, SUBSCRIBE, YES", + "optOutKeywords": "STOP, UNSUBSCRIBE, END, QUIT, CANCEL", + "helpKeywords": "HELP, INFO", + "consentText": "The exact checkbox consent text for the opt-in form. Must include: brand name, message frequency varies, msg & data rates may apply, reply HELP for help, reply STOP to unsubscribe. Do NOT include links in the consent text itself." + }, + "colorScheme": { + "primary": "A professional hex color that fits the business industry (e.g., #2563EB for tech, #059669 for health, #DC2626 for food)", + "primaryDark": "A darker shade of the primary", + "primaryLight": "A lighter/muted shade for backgrounds", + "accent": "A complementary accent color" + } +} + +Generate 3-4 painpoints, exactly 3 howItWorks steps, and 4-6 faqItems. Make sure sample messages are realistic and include the actual business name. The privacy policy and terms of service should be COMPLETE, thorough legal documents ready for use. Return ONLY the JSON.`; + + const response = await client.messages.create({ + model: 'claude-sonnet-4-20250514', + max_tokens: 8000, + messages: [ + { role: 'user', content: prompt } + ] + }); + + const text = response.content[0].text; + + // Parse the JSON - handle potential markdown code fences + let jsonStr = text; + const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/); + if (jsonMatch) { + jsonStr = jsonMatch[1]; + } + + try { + const parsed = JSON.parse(jsonStr.trim()); + return parsed; + } catch (e) { + console.error('Failed to parse AI response:', text.substring(0, 500)); + throw new Error('AI generated invalid JSON. Please try again.'); + } +} + +module.exports = { generateContent }; diff --git a/a2p-wizard-rebuild/services/packet-assembler.js b/a2p-wizard-rebuild/services/packet-assembler.js new file mode 100644 index 0000000..a2caa79 --- /dev/null +++ b/a2p-wizard-rebuild/services/packet-assembler.js @@ -0,0 +1,175 @@ +const archiver = require('archiver'); +const fs = require('fs'); +const path = require('path'); + +async function assemblePacket(config, clientDir) { + const zipPath = path.join(clientDir, 'compliance-packet.zip'); + const output = fs.createWriteStream(zipPath); + const archive = archiver('zip', { zlib: { level: 9 } }); + + return new Promise((resolve, reject) => { + output.on('close', () => { + console.log(` → Zip created: ${archive.pointer()} bytes`); + resolve(zipPath); + }); + + archive.on('error', (err) => reject(err)); + archive.pipe(output); + + // Add screenshots + const screenshotsDir = path.join(clientDir, 'screenshots'); + if (fs.existsSync(screenshotsDir)) { + archive.directory(screenshotsDir, 'screenshots'); + } + + // Create compliance packet text file + const packetText = generatePacketText(config); + archive.append(packetText, { name: 'compliance-packet.txt' }); + + // Create a formatted HTML version + const packetHtml = generatePacketHtml(config); + archive.append(packetHtml, { name: 'compliance-packet.html' }); + + // Add config + archive.append(JSON.stringify(config, null, 2), { name: 'config.json' }); + + archive.finalize(); + }); +} + +function generatePacketText(config) { + const p = config.a2pPacket; + const siteUrl = config.siteUrl; + + return `═══════════════════════════════════════════════════════ +A2P 10DLC COMPLIANCE PACKET +${config.businessName} +Generated: ${new Date().toLocaleDateString()} +═══════════════════════════════════════════════════════ + +CAMPAIGN DESCRIPTION +───────────────────── +${p.campaignDescription} + +SAMPLE MESSAGES +───────────────────── +Message 1: +${p.sampleMessages[0]} + +Message 2: +${p.sampleMessages[1]} + +Message 3: +${p.sampleMessages[2]} + +OPT-IN FLOW DESCRIPTION +───────────────────── +${p.optInFlowDescription} + +Website URL: ${siteUrl} +Contact/Opt-in Page: ${siteUrl}/contact +Privacy Policy: ${siteUrl}/privacy-policy +Terms of Service: ${siteUrl}/terms + +OPT-IN CONFIRMATION MESSAGE +───────────────────── +${p.optInConfirmation} + +OPT-OUT CONFIRMATION MESSAGE +───────────────────── +${p.optOutConfirmation} + +HELP MESSAGE +───────────────────── +${p.helpMessage} + +KEYWORDS +───────────────────── +Opt-in Keywords: ${p.optInKeywords} +Opt-out Keywords: ${p.optOutKeywords} +Help Keywords: ${p.helpKeywords} + +CONSENT TEXT (on website form) +───────────────────── +${p.consentText} + +SCREENSHOTS INCLUDED +───────────────────── +1. homepage.png - Full homepage screenshot +2. contact-optin.png - Opt-in form with consent checkbox +3. privacy-policy.png - Privacy Policy page +4. terms-of-service.png - Terms of Service page + +═══════════════════════════════════════════════════════ +Generated by A2P Compliance Wizard +═══════════════════════════════════════════════════════ +`; +} + +function generatePacketHtml(config) { + const p = config.a2pPacket; + const siteUrl = config.siteUrl; + + return ` + + + + A2P Compliance Packet - ${config.businessName} + + + +

A2P 10DLC Compliance Packet

+

${config.businessName} · Generated ${new Date().toLocaleDateString()}

+ +

Campaign Description

+
${p.campaignDescription}
+ +

Sample Messages

+
${p.sampleMessages[0]}
+
${p.sampleMessages[1]}
+
${p.sampleMessages[2]}
+ +

Opt-In Flow Description

+
${p.optInFlowDescription}
+ +

Website URLs

+
+
Homepage
+
Contact/Opt-in
+
Privacy Policy
+
Terms of Service
+
+ +

Opt-In Confirmation Message

+
${p.optInConfirmation}
+ +

Opt-Out Confirmation Message

+
${p.optOutConfirmation}
+ +

Help Message

+
${p.helpMessage}
+ +

Keywords

+
+
Opt-in
${p.optInKeywords}
+
Opt-out
${p.optOutKeywords}
+
Help
${p.helpKeywords}
+
+ +

Consent Text

+
${p.consentText}
+ +`; +} + +module.exports = { assemblePacket }; diff --git a/a2p-wizard-rebuild/services/screenshot.js b/a2p-wizard-rebuild/services/screenshot.js new file mode 100644 index 0000000..7bdafe0 --- /dev/null +++ b/a2p-wizard-rebuild/services/screenshot.js @@ -0,0 +1,75 @@ +const { chromium } = require('playwright'); +const path = require('path'); +const fs = require('fs'); + +const BASE_URL = `http://localhost:${process.env.PORT || 8899}`; +// Screenshots always hit localhost, even when site is publicly accessible via subdomain + +async function takeScreenshots(clientId, clientDir) { + const screenshotsDir = path.join(clientDir, 'screenshots'); + fs.mkdirSync(screenshotsDir, { recursive: true }); + + let browser; + try { + browser = await chromium.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'] + }); + const context = await browser.newContext({ + viewport: { width: 1440, height: 900 }, + deviceScaleFactor: 2, + }); + + const pages = [ + { + url: `${BASE_URL}/sites/${clientId}/`, + filename: 'homepage.png', + fullPage: true, + }, + { + url: `${BASE_URL}/sites/${clientId}/contact`, + filename: 'contact-optin.png', + fullPage: true, + }, + { + url: `${BASE_URL}/sites/${clientId}/privacy-policy`, + filename: 'privacy-policy.png', + fullPage: true, + }, + { + url: `${BASE_URL}/sites/${clientId}/terms`, + filename: 'terms-of-service.png', + fullPage: true, + }, + ]; + + for (const pageConfig of pages) { + try { + const page = await context.newPage(); + await page.goto(pageConfig.url, { + waitUntil: 'networkidle', + timeout: 15000 + }); + + // Small delay for any CSS animations to settle + await page.waitForTimeout(500); + + await page.screenshot({ + path: path.join(screenshotsDir, pageConfig.filename), + fullPage: pageConfig.fullPage, + }); + + await page.close(); + console.log(` → Screenshot: ${pageConfig.filename}`); + } catch (err) { + console.error(` ⚠ Failed screenshot ${pageConfig.filename}:`, err.message); + } + } + + await context.close(); + } finally { + if (browser) await browser.close(); + } +} + +module.exports = { takeScreenshots }; diff --git a/a2p-wizard-rebuild/services/site-builder.js b/a2p-wizard-rebuild/services/site-builder.js new file mode 100644 index 0000000..6ffcb0c --- /dev/null +++ b/a2p-wizard-rebuild/services/site-builder.js @@ -0,0 +1,71 @@ +const ejs = require('ejs'); +const fs = require('fs'); +const path = require('path'); + +const TEMPLATES_DIR = path.join(__dirname, '..', 'templates', 'site'); + +async function buildSite(config, clientDir) { + const siteDir = path.join(clientDir, 'site'); + + // Ensure site directory exists + fs.mkdirSync(siteDir, { recursive: true }); + + // Template data + const data = { + businessName: config.businessName, + businessAddress: config.businessAddress, + businessEmail: config.businessEmail, + businessPhone: config.businessPhone, + businessDescription: config.businessDescription, + logoUrl: config.logoUrl || null, + clientId: config.clientId, + siteUrl: config.siteUrl, + website: config.website, + a2pPacket: config.a2pPacket, + colorScheme: config.colorScheme || { + primary: '#2563EB', + primaryDark: '#1D4ED8', + primaryLight: '#EFF6FF', + accent: '#F59E0B' + }, + year: new Date().getFullYear(), + // When served via subdomain, baseUrl is empty (root-relative links) + // When served via /sites/{id}/, baseUrl has the prefix + // We build with empty baseUrl for subdomain-first approach + baseUrl: '' + }; + + // Render each page + const pages = [ + { template: 'index.ejs', output: 'index.html' }, + { template: 'contact.ejs', output: 'contact.html' }, + { template: 'privacy-policy.ejs', output: 'privacy-policy.html' }, + { template: 'terms.ejs', output: 'terms.html' }, + ]; + + for (const page of pages) { + const templatePath = path.join(TEMPLATES_DIR, page.template); + const template = fs.readFileSync(templatePath, 'utf8'); + const html = ejs.render(template, data, { filename: templatePath }); + fs.writeFileSync(path.join(siteDir, page.output), html); + } + + // Copy the site CSS + const cssSource = path.join(__dirname, '..', 'public', 'css', 'site.css'); + const assetsDir = path.join(siteDir, 'assets'); + fs.mkdirSync(assetsDir, { recursive: true }); + fs.copyFileSync(cssSource, path.join(assetsDir, 'site.css')); + + // If there's a logo, copy it to the site assets + if (config.logoUrl) { + const logoSource = path.join(__dirname, '..', config.logoUrl); + if (fs.existsSync(logoSource)) { + const ext = path.extname(logoSource); + fs.copyFileSync(logoSource, path.join(assetsDir, `logo${ext}`)); + } + } + + console.log(` → Built ${pages.length} pages in ${siteDir}`); +} + +module.exports = { buildSite }; diff --git a/a2p-wizard-rebuild/templates/form.ejs b/a2p-wizard-rebuild/templates/form.ejs new file mode 100644 index 0000000..16f6c1e --- /dev/null +++ b/a2p-wizard-rebuild/templates/form.ejs @@ -0,0 +1,181 @@ + + + + + + A2P Compliance Wizard — Generate Compliant Websites & Packets + + + + +
+ + +
+
+ + A2P 10DLC Compliance +
+

Compliance Wizard

+

Generate a fully compliant A2P website, opt-in flow, and TCR-ready compliance packet in minutes.

+
+ + +
+
+
1
+ Fill Info +
+
+
+
2
+ Generate +
+
+
+
3
+ Download +
+
+ + +
+
+ +
+ + +
+
Your Information
+
Agency or account owner details
+ +
+
+ + +
+
+ + +
+
+
+ + +
+
Client Business Information
+
Must match EIN / brand registration exactly
+ +
+ + +
Must match EIN paperwork exactly — no special characters
+
+ +
+ + +
Full address matching EIN paperwork
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
This is used to generate all website content and compliance text
+
+
+ + +
+
Client Logo
+
Used on the generated website (optional but recommended)
+ +
+ +
+ + + + + +
+
Click to upload or drag & drop
+
PNG, JPG, SVG, or WebP — max 5MB
+ +
+
+ + +
+
Compliance Acknowledgment
+
Required before generation
+ +
+ + +
+
+ + +
+ +
+
+
+ + +
+
+
+
Building Your Compliance Package
+
Preparing your request...
+
+
+
+
+
+
+ Generating AI content +
+
+
+ Building website pages +
+
+
+ Taking screenshots +
+
+
+ Assembling compliance packet +
+
+
+
+ + + + + + + diff --git a/a2p-wizard-rebuild/templates/packet/results.ejs b/a2p-wizard-rebuild/templates/packet/results.ejs new file mode 100644 index 0000000..560045e --- /dev/null +++ b/a2p-wizard-rebuild/templates/packet/results.ejs @@ -0,0 +1,624 @@ + + + + + + Compliance Packet — <%= config.businessName %> + + + + +
+ + +
+
+ +
+

Your Compliance Package is Ready

+

<%= config.businessName %> — Generated <%= new Date(config.generatedAt).toLocaleDateString() %>

+
+ + +
+ + + View Live Website + + + + Download All (ZIP) + + + + Generate Another + +
+ +
+ + +
+
+
+ +
+

Generated Website

+
+
+
+
+
+
+
+
+
+
<%= config.siteUrl %>
+
+ +
+ +
+
+
+ Homepage + <%= config.siteUrl %> +
+
+ Contact / Opt-in Form + <%= config.siteUrl %>/contact +
+ +
+ Terms of Service + <%= config.siteUrl %>/terms +
+
+
+
+
+ + +
+
+
+ +
+

Campaign Details

+
+
+
+
Campaign Description
+
<%= config.a2pPacket.campaignDescription %>
+ +
+ +
+
Opt-In Flow Description
+
<%= config.a2pPacket.optInFlowDescription %>
+ +
+
+
+ + +
+
+
+ +
+

Sample Messages

+
+
+ <% config.a2pPacket.sampleMessages.forEach((msg, i) => { %> +
+
Sample Message <%= i + 1 %>
+
<%= msg %>
+ +
+ <% }); %> +
+
+ + +
+
+
+ +
+

Auto-Reply Messages & Keywords

+
+
+
+
Opt-In Confirmation
+
<%= config.a2pPacket.optInConfirmation %>
+ +
+ +
+
Opt-Out Confirmation
+
<%= config.a2pPacket.optOutConfirmation %>
+ +
+ +
+
Help Message
+
<%= config.a2pPacket.helpMessage %>
+ +
+ +
+
Consent Text (website form)
+
<%= config.a2pPacket.consentText %>
+ +
+ +
+
+
Opt-In Keywords
+
<%= config.a2pPacket.optInKeywords %>
+
+
+
Opt-Out Keywords
+
<%= config.a2pPacket.optOutKeywords %>
+
+
+
Help Keywords
+
<%= config.a2pPacket.helpKeywords %>
+
+
+
+
+ + +
+
+
+ +
+

Screenshots

+
+
+
+
+ Homepage +
+ Homepage + Download +
+
+
+ Contact/Opt-in +
+ Opt-in Form + Download +
+
+
+ Privacy Policy +
+ Privacy Policy + Download +
+
+
+ Terms of Service +
+ Terms of Service + Download +
+
+
+
+
+ +
+ + + + + + + diff --git a/a2p-wizard-rebuild/templates/site/contact.ejs b/a2p-wizard-rebuild/templates/site/contact.ejs new file mode 100644 index 0000000..bdd0ec0 --- /dev/null +++ b/a2p-wizard-rebuild/templates/site/contact.ejs @@ -0,0 +1,125 @@ + + + + + + Contact Us — <%= businessName %> + + + + + + + + + +
+
+
+ + +
+

Get In Touch

+

Have a question or ready to get started? Fill out the form and we'll be in touch shortly.

+ +
+
+ +
+ <%= businessPhone %> +
+ +
+
+ +
+ <%= businessEmail %> +
+ +
+
+ +
+ <%= businessAddress %> +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + + + +
+
+ +
+
+
+ + + + + + diff --git a/a2p-wizard-rebuild/templates/site/index.ejs b/a2p-wizard-rebuild/templates/site/index.ejs new file mode 100644 index 0000000..ea18cdf --- /dev/null +++ b/a2p-wizard-rebuild/templates/site/index.ejs @@ -0,0 +1,172 @@ + + + + + + <%= businessName %> — <%= website.heroTitle %> + + + + + + + + + +
+
+
✦ Trusted Local Business
+

<%= website.heroTitle %>

+

<%= website.heroSubtitle %>

+ + <%= website.ctaText %> + + +
+
+ + +
+
+
+

About <%= businessName %>

+

<%= website.expandedDescription %>

+
+
+
+ + +
+
+
+

Why Choose Us

+

Here's what sets us apart from the rest

+
+
+ <% website.painpoints.forEach((pp) => { %> +
+
+ <%- getIcon(pp.icon) %> +
+

<%= pp.title %>

+

<%= pp.description %>

+
+ <% }); %> +
+
+
+ + +
+
+
+

How It Works

+

Getting started is simple

+
+
+ <% website.howItWorks.forEach((step) => { %> +
+
<%= step.step %>
+

<%= step.title %>

+

<%= step.description %>

+
+ <% }); %> +
+
+
+ + +
+
+
+

Frequently Asked Questions

+

Answers to common questions about our services

+
+
+ <% website.faqItems.forEach((faq) => { %> +
+ +
+
<%= faq.answer %>
+
+
+ <% }); %> +
+
+
+ + +
+
+

Ready to Get Started?

+

Reach out today and discover how we can help your business thrive.

+ + Contact Us Today + + +
+
+ + + + + + +<% +function getIcon(name) { + const icons = { + shield: '', + clock: '', + chart: '', + heart: '', + star: '', + zap: '', + target: '', + users: '', + check: '', + phone: '', + }; + return icons[name] || icons.star; +} +%> diff --git a/a2p-wizard-rebuild/templates/site/privacy-policy.ejs b/a2p-wizard-rebuild/templates/site/privacy-policy.ejs new file mode 100644 index 0000000..e6c3f88 --- /dev/null +++ b/a2p-wizard-rebuild/templates/site/privacy-policy.ejs @@ -0,0 +1,68 @@ + + + + + + Privacy Policy — <%= businessName %> + + + + + + + + + + + + + + + + diff --git a/a2p-wizard-rebuild/templates/site/terms.ejs b/a2p-wizard-rebuild/templates/site/terms.ejs new file mode 100644 index 0000000..5e9ab9d --- /dev/null +++ b/a2p-wizard-rebuild/templates/site/terms.ejs @@ -0,0 +1,68 @@ + + + + + + Terms of Service — <%= businessName %> + + + + + + + + + + + + + + + + diff --git a/closebot-sms/BUILD_PLAN.md b/closebot-sms/BUILD_PLAN.md deleted file mode 100644 index acff1e0..0000000 --- a/closebot-sms/BUILD_PLAN.md +++ /dev/null @@ -1,665 +0,0 @@ -# CloseBot SMS — Definitive Build Plan - -> Unified app: Twilio native SMS + CloseBot AI bots. One dashboard to rule them all. - ---- - -## 1. TECH STACK - -| Layer | Technology | Why | -|---|---|---| -| **Frontend** | Next.js 14 (App Router) + Tailwind CSS + shadcn/ui | Dark-mode-first, server components, fast | -| **Backend** | Next.js API routes (Edge-compatible) | Same deployment, zero CORS | -| **Database** | SQLite via better-sqlite3 (dev) → Turso/LibSQL (prod) | State tracking, conversation history, routing config | -| **Real-time** | Server-Sent Events (SSE) | Live conversation updates on dashboard | -| **SMS** | Twilio Node SDK (`twilio`) | Send/receive SMS, delivery status | -| **AI Bots** | CloseBot API (direct HTTP) | Webhook Source for inbound, API for bots/leads/metrics | -| **Auth** | NextAuth.js with credentials provider | Simple login, protect all routes | -| **Deploy** | Railway / Fly.io / Vercel | Needs persistent process for webhooks | - ---- - -## 2. SYSTEM ARCHITECTURE - -``` -┌─────────────┐ ┌──────────────────────────┐ ┌─────────────┐ -│ Customer │────>│ Twilio (SMS) │────>│ CloseBot │ -│ Phone │<────│ │<────│ SMS App │ -└─────────────┘ └──────────────────────────┘ └──────┬──────┘ - │ - ┌──────────────────────────┐ │ - │ CloseBot API │<───────────┘ - │ (Webhook Source) │────────────┐ - └──────────────────────────┘ │ - v - ┌──────────────┐ - │ SQLite DB │ - │ (state/logs) │ - └──────────────┘ -``` - -**Message Flow — Inbound:** -1. Customer sends SMS to Twilio number -2. Twilio POSTs to `POST /api/webhooks/twilio/inbound` -3. App looks up routing: which Twilio number → which CloseBot bot/source -4. App sends inbound event to CloseBot via `POST /webhook/event/{sourceId}` with: - - `type: "message"` - - `contactId` (phone number as unique identifier) - - `message` (SMS body) - - `state` (JSON: `{ twilioNumber, phoneFrom, messageSid }`) -5. CloseBot processes through bot flow -6. CloseBot POSTs response to our webhook retrieval URL: `POST /api/webhooks/closebot/response` -7. App receives response, extracts `state` to get the Twilio number + customer phone -8. App sends SMS via Twilio API -9. App logs everything to SQLite + broadcasts via SSE for live dashboard - -**Message Flow — Manual Override (from Conversations view):** -1. User types message in the conversation UI -2. App sends SMS directly via Twilio (bypassing CloseBot) -3. Logs to SQLite with `source: "manual"` flag - ---- - -## 3. DATABASE SCHEMA - -```sql --- Routes: Twilio number → CloseBot bot mapping -CREATE TABLE routes ( - id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(8)))), - twilio_number TEXT NOT NULL UNIQUE, - twilio_number_sid TEXT, - closebot_source_id TEXT NOT NULL, - closebot_bot_id TEXT, - bot_name TEXT, - greeting_message TEXT, - after_hours_reply TEXT, - business_hours_json TEXT, -- {"mon": {"start": "09:00", "end": "17:00"}, ...} - max_concurrent INTEGER DEFAULT 50, - active BOOLEAN DEFAULT TRUE, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - --- Contacts: phone numbers we've interacted with -CREATE TABLE contacts ( - id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(8)))), - phone TEXT NOT NULL UNIQUE, - name TEXT, - email TEXT, - status TEXT DEFAULT 'new', -- new, active, qualified, booked, closed, cold - closebot_lead_id TEXT, - assigned_bot_id TEXT, - assigned_route_id TEXT REFERENCES routes(id), - tags TEXT, -- JSON array - fields_json TEXT, -- collected fields from CloseBot - first_contact DATETIME DEFAULT CURRENT_TIMESTAMP, - last_contact DATETIME DEFAULT CURRENT_TIMESTAMP, - message_count INTEGER DEFAULT 0 -); - --- Messages: every SMS in/out -CREATE TABLE messages ( - id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(8)))), - contact_id TEXT REFERENCES contacts(id), - route_id TEXT REFERENCES routes(id), - direction TEXT NOT NULL, -- 'inbound' | 'outbound' - source TEXT NOT NULL, -- 'customer' | 'bot' | 'manual' - body TEXT NOT NULL, - twilio_sid TEXT, - twilio_status TEXT, -- queued, sent, delivered, failed, undelivered - closebot_message_id TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - --- Settings: app-wide config -CREATE TABLE settings ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - --- Events: activity log for dashboard feed -CREATE TABLE events ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - type TEXT NOT NULL, -- 'message_in', 'message_out', 'booking', 'new_lead', 'status_change' - contact_id TEXT REFERENCES contacts(id), - route_id TEXT REFERENCES routes(id), - data_json TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); -``` - ---- - -## 4. API ROUTES - -### Webhook Endpoints (public, Twilio/CloseBot call these) -| Route | Method | Purpose | -|---|---|---| -| `/api/webhooks/twilio/inbound` | POST | Receive inbound SMS from Twilio | -| `/api/webhooks/twilio/status` | POST | Receive delivery status updates | -| `/api/webhooks/closebot/response` | POST | Receive CloseBot bot responses | - -### Internal API (authenticated, frontend calls these) -| Route | Method | Purpose | -|---|---|---| -| `/api/dashboard/stats` | GET | Active convos, messages today, bookings, response rate | -| `/api/dashboard/activity` | GET | Recent activity feed (SSE endpoint) | -| `/api/conversations` | GET | List conversations with pagination/filters | -| `/api/conversations/[contactId]` | GET | Get single conversation with messages | -| `/api/conversations/[contactId]/send` | POST | Send manual SMS message | -| `/api/bots` | GET | List CloseBot bots (proxied from CloseBot API) | -| `/api/bots/[id]` | GET | Get bot details + metrics | -| `/api/contacts` | GET | List/search contacts with filters | -| `/api/contacts/[id]` | GET/PUT/DELETE | Contact CRUD | -| `/api/contacts/export` | GET | Export CSV | -| `/api/analytics/overview` | GET | Summary metrics with date range | -| `/api/analytics/messages` | GET | Message volume time series | -| `/api/analytics/bots` | GET | Per-bot conversation counts | -| `/api/analytics/outcomes` | GET | Outcome distribution | -| `/api/analytics/leaderboard` | GET | Top performing bots | -| `/api/routes` | GET/POST | List routes, create new route | -| `/api/routes/[id]` | GET/PUT/DELETE | Route CRUD | -| `/api/settings` | GET/PUT | App settings (Twilio creds, CloseBot key) | -| `/api/settings/test-connection` | POST | Test Twilio + CloseBot connections | - ---- - -## 5. PAGE-BY-PAGE UI SPEC - -### 5A. DASHBOARD (`/`) -**Matches mockup: closebot-sms-dashboard.png** - -**Layout:** -- Left sidebar (240px): Logo "CloseBot SMS" + 6 nav items with icons -- Top bar: Global search input -- Main content area - -**Components:** -1. **Stat Cards Row** — 4 cards in a grid - - Active Conversations (chat bubble icon, count from DB) - - Messages Today (envelope icon, count from DB where date = today) - - Bookings Made (calendar icon, from CloseBot metrics API) - - Response Rate (trending icon, calculated: bot_responses / inbound_messages * 100) - - Each card: dark glass-morphism background (`bg-slate-800/50 backdrop-blur`), cyan border glow, icon top-left, label + big number - -2. **Real-time Activity Table** - - Columns: SMS (phone number), Bot Name, Badges (status pill), Timestamp - - Status pills: `active` (green), `pending` (yellow), `closed` (gray) - - Auto-updates via SSE — new rows slide in at top - - Click row → navigate to conversation - -**Data sources:** -- Stats: `GET /api/dashboard/stats` → queries SQLite counts -- Activity: `GET /api/dashboard/activity` → SSE stream from events table - ---- - -### 5B. CONVERSATIONS (`/conversations`) -**Matches mockup: closebot-sms-conversations.png** - -**Layout:** Two-panel split (left 380px list, right flexible chat) - -**Left Panel — Conversation List:** -- Search bar with filter icon + settings icon -- "All Filters" dropdown (by status, bot, date range) -- Conversation rows: avatar (generated from initials), name, phone, last message preview (truncated), timestamp, unread badge (cyan circle with count) -- Selected conversation highlighted with left cyan border -- Sorted by most recent message - -**Right Panel — Chat Thread:** -- Header: avatar, name, phone number, "CloseBot SMS AI" badge, status dot + "Active" label -- Message bubbles: - - Inbound (customer): dark gray background (`bg-slate-700`), left-aligned with avatar - - Outbound (bot): cyan/blue gradient background, right-aligned - - Each bubble shows text, no timestamps on individual messages (clean look) -- Bottom: text input "Type a message..." with cyan send button - - Sending from here = manual override, bypasses CloseBot - -**Data sources:** -- List: `GET /api/conversations?search=&status=&bot=&page=` -- Messages: `GET /api/conversations/[contactId]` -- Send: `POST /api/conversations/[contactId]/send` -- Live updates: SSE for new messages appended to thread - ---- - -### 5C. BOTS (`/bots`) -**Matches mockup: closebot-sms-bots.png** - -**Layout:** Grid of bot cards (3 columns on desktop) - -**Top Bar:** -- "Bot Management" heading -- Search bots input + filter icon + sort icon -- "+ Create New Bot" button (cyan, calls CloseBot create_bot_with_ai) - -**Bot Cards:** -- Card header: bot name in bold, gradient top border (different colors per bot) -- Fields: Status toggle (Active/Inactive), Twilio # (from route mapping), Messages count, Conversion % (from CloseBot metrics), Last Active -- Inactive bots appear dimmed/grayed -- Click expand chevron → expanded card shows: - - Connected Sources list - - Recent Performance sparkline (mini chart, last 7 days) - - "Edit Flow" button (opens CloseBot dashboard in new tab) - -**Data sources:** -- Bots list: `GET /api/bots` → proxies CloseBot `GET /bot` + merges route data from SQLite -- Metrics: CloseBot `GET /botMetric/agencySummary` -- Toggle active: `PUT /api/routes/[id]` to enable/disable route - ---- - -### 5D. CONTACTS (`/contacts`) -**Matches mockup: closebot-sms-contacts.png** - -**Layout:** Full-width data table with optional slide-out detail panel - -**Top Bar:** -- Search input -- Filter dropdowns: "Filter by Status" (hot/warm/cold/new), "Filter by Bot", date range picker -- "Import Contacts" button (cyan outline) + "Export CSV" button (cyan outline) - -**Table Columns:** -- Name (bold) -- Phone Number -- Assigned Bot -- Status (colored dot: red=Hot Lead, orange=Warm, gray=Cold + text label) -- Messages Exchanged (number) -- Last Contact (relative time: "2 hours ago", "Yesterday") -- Tags (colored pills) - -**Slide-out Detail Panel (on row click):** -- Contact name (large, bold) -- Phone, Email, Company fields -- "Conversation Summary" section — AI-generated from CloseBot - - Bullet points: "Product Interest", "Budget Discussion", etc. -- "Field Values Collected" — mini table of CloseBot-collected fields (Company Size, Industry, Priority) -- Action buttons: "View Full History" + "Send Message" (cyan) - -**Data sources:** -- Table: `GET /api/contacts?search=&status=&bot=&dateFrom=&dateTo=&page=&limit=` -- Detail: `GET /api/contacts/[id]` -- Export: `GET /api/contacts/export` → CSV download -- Import: `POST /api/contacts/import` (CSV upload, creates contacts + optionally sends first message) - ---- - -### 5E. ANALYTICS (`/analytics`) -**Matches mockup: closebot-sms-analytics.png** - -**Layout:** Scrollable dashboard with multiple chart sections - -**Top Bar:** -- "Analytics" heading -- Tab navigation: Dashboard, Conversations, Bots, Contacts, Settings -- Date range picker dropdown (top right): "Last 30 Days (Oct 1 - Oct 30, 2024)" - -**Section 1 — Stat Cards (4 across):** -- Total Conversations (large number + sparkline + % change vs last period) -- Booking Rate (percentage + sparkline + change) -- Avg Response Time (duration format "1m 45s" + sparkline + change) -- Customer Satisfaction (rating "4.8/5" + sparkline + change) -- Each card has colored gradient background (red→pink, green, blue, purple) - -**Section 2 — Messages Over Time (full width):** -- Line chart with dual lines: Inbound (blue) vs Outbound (green) -- X-axis: dates over selected range -- Y-axis: message count -- Tooltip on hover showing exact values -- Chart library: Recharts (React-native, works with Next.js) - -**Section 3 — Two charts side by side:** -- Left: "Conversations by Bot" — horizontal bar chart with colored bars per bot -- Right: "Outcome Distribution" — donut chart with segments (Booked 40%, Qualified 30%, Dropped 18%, Pending 12%), total in center - -**Section 4 — Top Performing Bots:** -- Mini leaderboard table: Rank, Bot Name (with icon), Conversations, Booking Rate, CSAT Score -- Sorted by booking rate descending - -**Data sources:** -- Stats: `GET /api/analytics/overview?start=&end=` -- Messages chart: `GET /api/analytics/messages?start=&end=&resolution=daily` -- By bot: `GET /api/analytics/bots?start=&end=` -- Outcomes: `GET /api/analytics/outcomes?start=&end=` -- Leaderboard: `GET /api/analytics/leaderboard?start=&end=` -- All backed by SQLite queries (message counts, conversation outcomes) + CloseBot metrics API for bookings/CSAT - ---- - -### 5F. ROUTING (`/routing`) -**Matches mockup: closebot-sms-routing.png** - -**Layout:** Three-column visual routing display - -**Top Bar:** -- Breadcrumb: "Routing / Phone Number Routing" -- Top nav tabs: Dashboard, Routing, Bots, Analytics, Settings -- "+ Add New Route" button (top right) -- Search bar + filter icon - -**Three Columns:** -- **Left: Twilio Phone Numbers** — list of phone number cards with phone icon -- **Center: Routing** — animated connection lines (CSS/SVG) linking numbers to bots -- **Right: CloseBot Bots & Sources** — bot cards with: name, source badge (WEB_LEAD, EMAIL_INQUIRY, etc.), Active/Paused toggle, message count badge, "Configure" button - -**Expanded Route Configuration (on Configure click):** -- Modal/drawer overlaying the route: - - Greeting Message Override (textarea, optional) - - After-Hours Auto-Reply Text (textarea) - - Business Hours Schedule — day-of-week toggle grid (Mon-Sun), time range "Mon-Fri: 9:00 AM - 5:00 PM", "Add Exception" link - - Max Concurrent Conversations — slider (1-100) with current value displayed - - Save + Close Configuration buttons - -**Data sources:** -- Routes: `GET /api/routes` → SQLite routes table -- Twilio numbers: fetched from Twilio API on settings save, cached -- Bots: `GET /api/bots` → CloseBot API -- Update: `PUT /api/routes/[id]` with config JSON - ---- - -### 5G. SETTINGS (`/settings`) -**Matches mockup: closebot-sms-settings.png** - -**Layout:** Two-column card layout - -**Cards:** -1. **Twilio Connection** — Account SID input (masked), Auth Token input (masked), "Connection Status" indicator (green dot + "Connected") -2. **CloseBot Connection** — API Key input (masked), Webhook Source ID (display), "Connection Status" indicator -3. **Phone Numbers** — list of Twilio numbers with assigned bot + toggle switch -4. **Notifications** — toggles: Email Alerts, SMS Delivery Failures, New Lead Alerts -5. **Webhook URLs** — read-only display of: - - Inbound URL (the URL you give to Twilio) - - Response URL (the URL you give to CloseBot Webhook Source) - - Copy buttons next to each - -**Data sources:** -- Settings: `GET/PUT /api/settings` -- Test: `POST /api/settings/test-connection` → pings both APIs - ---- - -## 6. FILE STRUCTURE - -``` -closebot-sms/ -├── package.json -├── next.config.js -├── tailwind.config.ts -├── tsconfig.json -├── .env.local.example -├── BUILD_PLAN.md -├── README.md -├── prisma/ (or drizzle/) -│ └── schema.sql -├── src/ -│ ├── app/ -│ │ ├── layout.tsx # Root layout with sidebar -│ │ ├── page.tsx # Dashboard -│ │ ├── conversations/ -│ │ │ └── page.tsx # Conversations split view -│ │ ├── bots/ -│ │ │ └── page.tsx # Bot grid -│ │ ├── contacts/ -│ │ │ └── page.tsx # Contacts table -│ │ ├── analytics/ -│ │ │ └── page.tsx # Analytics dashboard -│ │ ├── routing/ -│ │ │ └── page.tsx # Phone number routing -│ │ ├── settings/ -│ │ │ └── page.tsx # Settings/config -│ │ └── api/ -│ │ ├── webhooks/ -│ │ │ ├── twilio/ -│ │ │ │ ├── inbound/route.ts -│ │ │ │ └── status/route.ts -│ │ │ └── closebot/ -│ │ │ └── response/route.ts -│ │ ├── dashboard/ -│ │ │ ├── stats/route.ts -│ │ │ └── activity/route.ts # SSE -│ │ ├── conversations/ -│ │ │ ├── route.ts -│ │ │ └── [contactId]/ -│ │ │ ├── route.ts -│ │ │ └── send/route.ts -│ │ ├── bots/ -│ │ │ ├── route.ts -│ │ │ └── [id]/route.ts -│ │ ├── contacts/ -│ │ │ ├── route.ts -│ │ │ ├── export/route.ts -│ │ │ └── [id]/route.ts -│ │ ├── analytics/ -│ │ │ ├── overview/route.ts -│ │ │ ├── messages/route.ts -│ │ │ ├── bots/route.ts -│ │ │ ├── outcomes/route.ts -│ │ │ └── leaderboard/route.ts -│ │ ├── routes/ -│ │ │ ├── route.ts -│ │ │ └── [id]/route.ts -│ │ └── settings/ -│ │ ├── route.ts -│ │ └── test-connection/route.ts -│ ├── components/ -│ │ ├── layout/ -│ │ │ ├── sidebar.tsx -│ │ │ ├── topbar.tsx -│ │ │ └── nav-item.tsx -│ │ ├── dashboard/ -│ │ │ ├── stat-card.tsx -│ │ │ └── activity-feed.tsx -│ │ ├── conversations/ -│ │ │ ├── conversation-list.tsx -│ │ │ ├── conversation-item.tsx -│ │ │ ├── chat-thread.tsx -│ │ │ ├── message-bubble.tsx -│ │ │ └── chat-input.tsx -│ │ ├── bots/ -│ │ │ ├── bot-grid.tsx -│ │ │ └── bot-card.tsx -│ │ ├── contacts/ -│ │ │ ├── contacts-table.tsx -│ │ │ ├── contact-detail-panel.tsx -│ │ │ └── contact-filters.tsx -│ │ ├── analytics/ -│ │ │ ├── stat-card-sparkline.tsx -│ │ │ ├── messages-chart.tsx -│ │ │ ├── bots-bar-chart.tsx -│ │ │ ├── outcome-donut.tsx -│ │ │ └── bot-leaderboard.tsx -│ │ ├── routing/ -│ │ │ ├── routing-view.tsx -│ │ │ ├── phone-number-card.tsx -│ │ │ ├── bot-route-card.tsx -│ │ │ ├── connection-lines.tsx # SVG animated lines -│ │ │ └── route-config-modal.tsx -│ │ └── settings/ -│ │ ├── twilio-card.tsx -│ │ ├── closebot-card.tsx -│ │ ├── phone-numbers-card.tsx -│ │ ├── notifications-card.tsx -│ │ └── webhook-urls-card.tsx -│ ├── lib/ -│ │ ├── db.ts # SQLite connection + queries -│ │ ├── twilio.ts # Twilio client wrapper -│ │ ├── closebot.ts # CloseBot API client (reuse from MCP server) -│ │ ├── sse.ts # SSE broadcast utility -│ │ └── utils.ts # Formatters, helpers -│ └── styles/ -│ └── globals.css # Tailwind base + custom dark theme -``` - ---- - -## 7. DESIGN SYSTEM - -**Colors (from mockups):** -```css ---bg-primary: #0f1729; /* deepest navy */ ---bg-secondary: #1a2332; /* card backgrounds */ ---bg-tertiary: #1e293b; /* elevated surfaces */ ---bg-hover: #2d3748; /* table row hover */ ---text-primary: #e2e8f0; /* main text */ ---text-secondary: #94a3b8; /* muted text */ ---text-muted: #64748b; /* timestamps, labels */ ---accent-cyan: #22d3ee; /* primary accent */ ---accent-blue: #3b82f6; /* secondary accent */ ---accent-green: #22c55e; /* success, active */ ---accent-yellow: #eab308; /* warning, pending */ ---accent-red: #ef4444; /* error, hot lead */ ---accent-orange: #f97316; /* warm */ ---accent-purple: #a855f7; /* bot cards */ ---border: #334155; /* subtle borders */ ---border-glow: rgba(34, 211, 238, 0.2); /* cyan card glow */ -``` - -**Typography:** -- Font: `Inter` (system-ui fallback) -- Headings: 600-700 weight -- Body: 400 weight -- Monospace numbers: `font-variant-numeric: tabular-nums` - -**Card Style:** -```css -.card { - background: rgba(30, 41, 59, 0.5); - backdrop-filter: blur(12px); - border: 1px solid rgba(51, 65, 85, 0.5); - border-radius: 12px; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3); -} -``` - -**Status Badges:** -- Active: `bg-green-500/20 text-green-400 border-green-500/30` -- Pending: `bg-yellow-500/20 text-yellow-400 border-yellow-500/30` -- Closed: `bg-gray-500/20 text-gray-400 border-gray-500/30` - ---- - -## 8. BUILD PHASES - -### Phase 1 — Foundation (Day 1) -- [ ] Next.js project scaffold with Tailwind + shadcn/ui -- [ ] SQLite schema + db.ts with all queries -- [ ] Global layout with sidebar navigation (matches dashboard mockup) -- [ ] Dark theme CSS variables -- [ ] Environment config (.env.local) -- [ ] Twilio client wrapper -- [ ] CloseBot API client (port from closebot-mcp) - -### Phase 2 — Core Webhook Bridge (Day 1-2) -- [ ] `POST /api/webhooks/twilio/inbound` — receive SMS, forward to CloseBot -- [ ] `POST /api/webhooks/closebot/response` — receive bot reply, send via Twilio -- [ ] `POST /api/webhooks/twilio/status` — delivery status updates -- [ ] Contact auto-creation on first inbound -- [ ] Message logging to SQLite -- [ ] Route lookup logic (Twilio number → CloseBot source) -- [ ] Business hours check + after-hours auto-reply -- [ ] State passthrough (phone + route info in CloseBot state field) - -### Phase 3 — Dashboard + Real-time (Day 2) -- [ ] Dashboard page with 4 stat cards -- [ ] Real-time activity feed (SSE) -- [ ] Activity table with status badges -- [ ] Auto-refresh stats - -### Phase 4 — Conversations (Day 2-3) -- [ ] Conversation list with search/filter -- [ ] Chat thread with message bubbles -- [ ] Manual message send -- [ ] Unread badges -- [ ] Live message updates via SSE - -### Phase 5 — Bots + Contacts (Day 3) -- [ ] Bot grid with cards from CloseBot API -- [ ] Bot status toggle (enable/disable route) -- [ ] Contacts table with all filters -- [ ] Slide-out detail panel -- [ ] CSV export - -### Phase 6 — Analytics (Day 3-4) -- [ ] Stat cards with sparklines -- [ ] Messages over time chart (Recharts) -- [ ] Conversations by bot bar chart -- [ ] Outcome distribution donut chart -- [ ] Bot leaderboard table -- [ ] Date range picker - -### Phase 7 — Routing + Settings (Day 4) -- [ ] Phone number routing view with visual connections -- [ ] Route config modal (business hours, greeting, max concurrent) -- [ ] Settings page with credential cards -- [ ] Connection testing -- [ ] Webhook URL display with copy - -### Phase 8 — Polish (Day 4-5) -- [ ] Loading states + skeletons -- [ ] Error handling + toast notifications -- [ ] Mobile responsive adjustments -- [ ] Auth gate (simple login) -- [ ] README with deploy instructions - ---- - -## 9. EXTERNAL DEPENDENCIES - -### npm packages: -```json -{ - "dependencies": { - "next": "^14.2", - "react": "^18.3", - "twilio": "^5.0", - "better-sqlite3": "^11.0", - "recharts": "^2.12", - "lucide-react": "^0.400", - "@radix-ui/react-dialog": "^1.0", - "@radix-ui/react-dropdown-menu": "^2.0", - "@radix-ui/react-toggle": "^1.0", - "@radix-ui/react-slider": "^1.0", - "class-variance-authority": "^0.7", - "clsx": "^2.1", - "tailwind-merge": "^2.3", - "next-auth": "^4.24" - } -} -``` - -### Environment Variables: -```env -# Twilio -TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -TWILIO_AUTH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - -# CloseBot -CLOSEBOT_API_KEY=cb_xxxxxxxxxxxxxxxx - -# App -NEXTAUTH_SECRET=random-secret-here -NEXTAUTH_URL=https://your-domain.com -APP_URL=https://your-domain.com # Used to construct webhook URLs - -# Database -DATABASE_PATH=./data/closebot-sms.db -``` - ---- - -## 10. WHAT MAKES THIS DIFFERENT - -1. **No GHL required** — CloseBot's Webhook Source is the channel, Twilio is the pipe -2. **Multi-tenant ready** — one app, many Twilio numbers, many bots -3. **Human takeover** — click into any convo and type a manual reply -4. **Business hours built-in** — auto-reply after hours, resume bot in morning -5. **Full audit trail** — every message logged with direction, source, status -6. **Real-time everything** — SSE for live dashboard, no polling -7. **CloseBot metrics integrated** — booking rates, leaderboards, CSAT from their API -8. **$0.0079/SMS** — Twilio pricing, no GHL middleman markup - ---- - -*Plan version: 1.0 | Last updated: 2026-02-06* diff --git a/closebot-sms/app/data/closebot-sms.db b/closebot-sms/app/data/closebot-sms.db deleted file mode 100644 index db7a7459cf0d014b0dc2333abb5541c58c2d6ed1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|@t|AVoG{WYBBV;st3JAlr;ljiVtj n8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*O6ovo*g{}s{ diff --git a/closebot-sms/app/data/closebot-sms.db-shm b/closebot-sms/app/data/closebot-sms.db-shm deleted file mode 100644 index ae807149c3adc6ea96695fb59e2293909764f26d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI5yH1o*6oof<1yOKR91#Tt0Y&kC7w_Qp4eX4Kg}qN?X)KISpf6%!ZDFr#R=$<2>@)wIocY%`{W{&RXCk#l9*UTFuDL9~dAaue!@JSX)89UI*57`4Km6j= z>c`isKe~UpUq@{y&;7Yc?QyKMg?>AR<$RB=a(><>Zz{}fB*y_009U<00Izz00bZa0SG_< z0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb z2tWV=5P$##{ttm{5J3G9$j$)N4}l?dOpL@>j7LXp5R+Bk~G<;h`b1_#Kl;Rb+1q*X|hEWc@ene6{j#w z_U0ll0++qw6sF0>XyirUidUS%G})1kya-(Nic^>-Zw5eK1g?3-DNK|1U?497x8spl zpt3Z1`wQ|SaK|f7VVb<_2ze2>>lLRkP2TQ>ya?R$ic^>-@3BK(1nzsqDNK{MHzF?r wkGDMW97AUK~CKe9V9#sDVU+91`3aYBXvUx(nSk ztgh;*swN0gj6536$SW((+KzW4JI={weeAQ&k-fH8j@NeL9LGK*^gPl- zKkxpdpMUJ!&0qhi|M;VSJTm*ir=P^7Uie3Ezxlr1d%oGTc~`w>SAEZK?D;$0it=?vfDvE> z7y(9r5nu!u0Y-okc+e1d^HA@${(bv;KRcE<^S&E}jl_-B?@d#erzhv8#oXkX^V33I z6N5X2{PD_SZu-SJaq05Rg~`j;#0RIZ4e49SqUU>IF=*80-AI+1y*MXkubw|IuFlRp ze|5TJ$zrVQG+OT$aP9p)FS}e0r7$>oX8cW zMijY0g6G181>XyBH7E-};){x#hJQQ%bxWZd-vdbH9WkN!)S~M`t-Z zO$~!(Tu!{2J2()8iv#(*d7x3JJF9p=&h7l6&3*mP?Cb6Eg0g!ve$~fOz34QOP<}6} zB~lz$f43akw2H!Gs_pb$9L03B}$-A{KE(^0*nA7 zzz8q`i~u9R2rvSS03*N%Fai%I0wg0y>=uUx7flPdX^AG*=ABea9$&alNUtnAMbmw>j*6ofT)A#~?J0I8a z1$I8Z>zQ2-cPz~p839Is5nu!u0Y-okU{6c59C3%SVidE?k?FMx1r$9SGdT)Ok2{6Zc)G z-0J`B#IaWi;NaWAH$1&3U2x5l;_~#l>C4lzQ`1+p&-FevC+TYEe*^G5@ z+wQHw?{@YO9ZjsCrV@uknRAHjx? z12@PBFanGKBftnS0*nA7zz8q`i~u9R2z)CLAfJG3MlSH1@BEQp-aq%v-)FhNw_+%` zO-6tbU<4QeMt~7u1Q-EEfDvE>7y(9L!x6~D7kK6;{_>l@^r6vLR){aK!|EmFGoaXo z=110NzI6GU#uwPW{X?1f0^2{d_sjzR-3%t#tA-kZI^k?vkvm+-h5cr zna5k7(&^P(v!$EMO?P=yRq7ybT*6y&q^~+@w)Pp{zMKoV0lqk9KJDQ60>}^FOkdCO z1x&tlv${CGKLr$mm*FfVZd9y-w8X{8t^1maoSi;5 zdG-997?`U%!Sz@ygpr6`r&L8f0`v2Lc)=A;=0;K!jVdMg5A3gs^-~0H#{{!|TSPO()mJwhC7y(9r z5nu!u0Y-okU<4QeMt~7`+ai$hBlw-4+4Pa*=w^QfKZ1v>waQ;i;Lsx@0LTG zR#A9N6{eT~(tiMdrD=e%7npwMe|z>Qz^n8Q;2)=FS1HU*Uzz8q`i~u9R2rvSS03*N%FanIg zw-*8#KZ4tTtM`R>9R2B^UBQn)niFW4w&L%){RnpTy`!TaL0=U&_=gc-1Q-EE;66d% z(yqS#Lx*~=DYN!heGl=uWB1jD8=kPL%|@MQjzPd>D*f8V~|PwkO) zx;GF*Iaa@SWol5@QX_d-U(d|WO}~HovarnL#pKnwi!-x$@WS-$oH5c%mg-$>=I>z$ zdna*_Jb#4THofC8?dS6)@gJOy_(AWOdH@2+C%X zn7QM`u~!M;J=4JkJiWJu3>Jw~iIwSk8bJP~m(vLE3!dwj<0g3!X`x%UtwK?p6!px_ z)-_0;KI}(8?l{^DrxQKMcn7c_0p}y=k|&|Ld?)yNKyD_5ug~X#9;YeeG{gvLC^H!isY@7y(9r5nu!u0Y-okU<4Qe zMt~7u1Q>yPivWoLb{o0C<)3-+5B}Y|f8S@hz`X^XYi9%)0Y-okU<4QeMt~7u1Q-EE zfDvE>?h^zuegsFpIQ#woYyVHrtl&rRu+{fId|tO7!S1a;*VT^zxA=zNA~xA!VAjoO|N{jXpFFpF{=6gur|=2nLRuGA}lf6ONxuL!jv9t8r$dDj{uGn z#&p%PQ&+)!Z7X$H`m!IvO*omT6bhV=K(vN0H;D&0A3?nu1}%<0Edk>>AAxk9F$T@- zN3a^dBK9MIPoBRd!i5Fj!vb@HvRL$dUsT*g%sZv)A_>J(*bwDLgfur|z7eBX9E&RQ zF2uvBLyNgyl-E0YzvOCRkej%bixVeGa$W58hjK~7T}7!yx4eo{Nwu+xQVWr{Vk!0` zaN}Z!d<1mHf7sXye8&$xd+jei`Wvrv+`*L@aW26KFanGKBftnS0*nA7zz8q`i~u9R z2s{7?Wb6gD{`o)rRloL6|I-Tg0(-3Y-iFWXwikGK>!oGv1#pvp7y(9r5qO&;;5^(_ znIFkqw?;j`XBlO_Dw$R0t82(MpJ#i4YaDkF;m!tSTsa}GyGttCmKB)M#5ua*_>C47 zt+}-75$^c9K-js0?FHWE z2NCW*BftnS0*nA7zz8q`i~u9R2rvSS03)zE1Tyvll`nnwRPpOy`j0Ev3p`>~y&a#| zZ7;BA%h#5*7r<@)VFVZfM&Mo}@Y0^X{=@rw8>xT3$!=DJ+g#F!&DTfTH2UU_E=US} zb7@dLn`e6gJEtM%DO@4(m`Wwbc?yjc&1&?6ddYQ)|B$Xmubek_5mHgosj$+ZWP(L_ zk&Db!VUW#sW@?Jzk6Y?ueKF7W0+dc}*@SXzFTnN!ZyxHE`D#DQ_5y0XI17mzX_x-U zts_oACd9~J;^uN9QzCUJrfqQ)n;rbZ?Mt~7u1Q-EEfDvE>7y(9r5nu!uf%^i1jJ?1I{=~ z{^a|9;Rl87y(9r5nu!u0Y-okU<4R}Zwmx6_5uq}{OA{NpZ@Hl zE7%J>W+6i&gmv2sJi6udiuM91!as}vBk;CC;M${5>hJ4aOqKfX4aCNUI$A}~SKl6M zQ|V_*k#80YGY|B!y#U(_OpK1^ZV%-a-SUbt0fur3+Y8)ddjUG*KW6L&-uF{Kf3m;-&wh~Y z1>QD?FzzTLzz8q`i~u9R2rvSS03*N%FanGKBXB1OWb6e_O#kq|IP{ZWe{mUmfz4>_ z39G?}@OiEF0-Jk=-+bThJ>Tqky!Xjf*bDSNx%n-t#c%1yH$L*%w*HZk-p^c@VsmG_ zUX0v|7blTJA&cX-3y)_U@7iv!#shD@5qp6f$Hl;@bqRc|;+|&%CZ}TGfw~)%5wSFt zSbIUekyrxj*5t~uS1y*Sju%*Ej11XMvg*pBC|} zKIbD4V%0Q-^AXI+d1I8-tJx&=l4E-TwT+}ZTHA)Db->$~Q|TJu*^_z~>? z;CDUxg{S_8vw|PNUaO{E_`GgEf+u>uuv$NYFIa7M{#bgVuRlN1dpiwxMi$P>z`o7a zd2d_Pvu0t6?A7*wu?S{Af?LhtIP6D|&l|S@_9HN^H0(zpjH#rd8qULg{YUoqej+7J zsiCg6EjQoyw9zGV$3$)9xPw9nubg@Saoj=Ws^KPNlp3cd_t)Jey3Z`~%t*S+zw^usf=+x*?0@wzit)0e`y%MffG-iiGP zT!f=b-O<>OKnw)#V(L0s^26Aj50j$rI%VT*V%9_ID?kPG9BaGBegty&(mr8MBMIeq z6B{w+xPxkfb(lkRDfIz16RQ%rh#tv)1n_arP2|Roaoj=UO1u*7-Vi?mI^*v(_5%O# zyT5ziEBRl%#r6W7MGt(H5nu!u0Y-okU<4QeMt~7u1Q-EEfDw4W5XjgI{E5H+d;anp z2YzV!mGU z+w!?Br)Athwin>IgU}Rm+(CeV5M~^A(7oxEZ#K0R^=cTnMo<4}o7&ynDavxBbT?N9 z<(2$Pd;0nh@9%A-Dt&Wbn;e<1k94nE2miF|R+sW@FTimJIqo3L8Q5Mx+F1yZEPB2d z77^h$6K~91PMoBnQr#K#Vyf`gMii}j?KK9q9O1ZwFU}ck*C;?Ytug-nS`IrCX z4Yn5`RQQJxU<4QeMt~7u1Q-EEfDvE>7y(9r5qRJcAl-kzkqa#Bm?=H`H^2I$ltXZD z&&=w5|7W`3^vd3C{f9<+U)z?d;mC7e%eLE`_}|f{i?d31cKdIN$@3gvKsjF;NoFc` z;yi`t=ftQ{4sf1AX>kC3Ip-;KN=eN20>+Ko7++*V7=%Frn4l-gU)t5zf9O!}HR&1N zl6$b_>+X!7c+1T}^0gd_-B9q++ssg$^eQx^l^z791m8LNXUfU3kH8>cToy2M8 zcr_I?_bNrGP3qS!>4;uCutU1ye{!z^Ts!Lvk0H)^xsN$mjGUZ4@h%}p(xTLZUpwJ=zPB>>wCfGt2s ziJCh&5QK{Z>xfOJ>~}UKzCgy0;0F)?(hvT^GtYfw89xH@8FdxH^?7n;_U!bFh~RhABv6i7E^yb5k}Ma%`Yak@1;-t1GdpIvfH|77Tp$%>SRtJk zyeLjmf-I#brdZcQzVEF05ta+2ERoq+Z$`J z7l465rysT#cs!%lX)}>X^*dT|VC%okv`Lp{DmZUrGi-TzDs?v#mhA=DUZ5GN+RUmh z_5$1Un?G}Pq_?MY@Zq-kTO4b<#Q6xC>Sm5FaOV89V0!^2%!}3(GXCvgFMw@-Y-)cu zYQb^=wil2ypAv|4OC-T^fs{{Uxxkn-jgppGvHHC!!#}HQt6`e8;7y(Ov(`K<<|vtx zY%joafhdC6V%xF*NNREXK+6TT>tp;o>93Is9Q}iDe#fDo{H-e#Oi;%Jq)+_A2rvSS z03*N%FanGKBftnS0*nA7zz8q`4<-Wp=(UVo;5#4r_rJRTqYwXejyw2Z4mQ6PBftnS z0*nA7zz8q`i~u9R2rvSS03*lh20*nA7zzEzC0v6|Gfk<#jscf%yV#s|(Laq{uL{yTKodgCLHZR;Ny z>HW-gxl~$5Ic~eaPK}}OVbQg&54Gl6ug*uHJ;fwyz->P+g;DBKY?ciNJG#fgj;=l0 z*FUzecQK7xuHw&{z~$=OV{HL*vZW}DuDW8!<9YEBRK36V)wF7ph*)JZHs3hoaeMqB zj#SjT$=~M@j7N6%^{>g>51Vc#?x6qOANhg5{>0}$v_jm$cUg?z9(?AwgK-i$6tbAN z;xrDpVSMYn6?Y6ms<@80F<Gf=EQFCj(3(;el5+vL4u}_A;Xr^@YDgz}JQpr3 z_y{N{AmS2>p6`o_n}~U*bX_E&SPC1W+=%FDBLnBSg9^HD*Z2ZB&da!iGPi)S7x>!g zfBx6DUao$g>;?ApZCln}0Jr&v5nu!u0Y>1#L?C|Ww*IFFdS81yRjOokxkXZ=XRf@f zO?8vGEgvBDP3G#LVM*leJlhMfy+G4KgzW{i!l6@uSGE_hBK6rcrOhCKl@ya#5q!C8 zDueq}Nx}94M#4785sW#4ajJ7&wSAvTY`7kK0n+`y%g6S`f(i5&Xq_tMB;m5B|r;HZdUz? zlQgoS`QE*fUB7&!-LhXBEwUd$4A;y!R{jr?(eDs(h~o~SOnKZ1MD=5q^-03*N%FanGKBftnS0*nA7zz8q`jKF<>K*o>Y*Uo<>dGu%h z*}^h@1Y6PP16IG+<43TyXZX$cQANFb-lD3W-1FAX$9Fxm>*4Jm+VSOWZ*0%^Rr}tt z_2;%;+VZt6pWAY}_sPv~^?bo1;>D2>weD9u+}!k)ctqON9P5^}XY zVB~cyqGu8P11+Mz3u-Ku^0SmbBco?2Kl>3R38G%d53wJCG89e_8{7+vu@2OqTA=I8 z%8rLDRLq@{AI9!{m=v|P&RDdk_Ys$TReo&jM^J9X&WU5M5+Lgj;oCAI={@!%kc_pa zag0 zXj5PRGb;-oGJMTGs=J@!zqq*BB51hR?HFxdOw5TRE|Mbq2^oP=pR8GM)I7b&; zr+gYRiIX)qaYU)=M6sKk%3Ym1S2&LQ<^~xCpUU0v+{Jnb*I_|Db^{b#^vX%~RN1}Z zmE3~-G9q%iI5CuW7o*Yp(sI9mZdG$oZ zQR!5!nk4mjVq~OL4u%teu!!?-DUKx;r%#TkqOylb_;ww6;rvs%dgP*Kfm=cstC73V ztYrZmj)yB@Sn*w_?&)f%%{4qt+AOamdmu_t7{_7cRlK0tiIv+N!7GeEv*6S`f9cc= zK#V39E7j!L!=s}op2B~O=!v8Fe{6L0VA+f7zO!^HUUcd?^@4G-;!Qkb|Y`$gt#q>4_6|m>|qQG^A&MRUP2v6lvlxF zBq&b^N~7mQ1ygKr?C_DYTNx61a*Z7kqwh_>jUEwWqoeQ5pO6*l7UY~PIHeL=LC;Dw zJ15NZX+Kn{`7kPDLEOS?(1*hEh0$ZHuN@0xh7eYm;v;H~pOgTVd`Of|<>o8&VoQZ1 zqoU>og(~K1oHvMEz#rDN$c4jE#%9Hi2(P zglp-*L`&?t)ZT&0ME-l z$fvLxa*}GyhIkFHq!Q1c9I0zGRZp5|UvRuYOgT}xBj6Z#^^yiOde6A(l*7dWK7}{j zXu-$ls#h*c3aO#8#;jgAE{7Y#ZiZQ+NkfhG7KLfU^0M`0W`yO<>UieGQ_y3#<`M8s z9I(CZ$A3x$l*$DvU;OdUyz5}_82J(O?)lF>_{Tqt03*N%FanGKBftnS0*nA7zz8q` zi~u9>03h)A=E9Cvu{B7y(9r5nu!ufeZrMHt*d*wrRL1a}F9mg1>n310Nj;|L`9vxxhDi_IxAr z7=K{|7y(9r5nu!u0Y-okU<4QeMt~7u1Q>z0KLXn~_wOi2h;4yaAzy;jULgLpKPw%7 z^1nQXOW#q@UK&rq=^}BK#FX-2vmt#o;I$;+UsXn2jRG99oSQCO zN3?GdJtN@H01r9qO6tc!H?>zudRuzWoC~8`)}vy9u6X5Bxv-wh1auz`Y09q$J}U4- zTPhs2Ju^@-`DRGJIe2K)8%d@<+_s!qs2{lEJM%6%z(jB=lm2$5g?T?LT~}@yaZP#1 z)ZDVys5MT;$Wa9paxAuno8BBD7nuD@s}uq1-pTHT4re@uRcUtt&&9Jo12 z{~*7T?T3w9>s->40Mj|9>ZYo0>H2(lv%^eCN4ip&_Zu$xi0S^MuOY9GXJ4Z(C!HE+ z_1pI0vfQ%ZMFWd;R zxQQ*ZE4yXk%)^~*Sj-6cQ@Rm`J%B$~)k!AWsyW&4O;-W>VPVMFc$TQN zO&a;CzIXcIJY4@yhzm%r=TuzuSNk1Lh^}tOAT?g=aCE7T`i%rwj))NAEEYBF3QoQ5 zFRh}ADUGuiiy%w{JdDGjELX7z7eu`g!Q(iF3-F44x$L4rcr2ZFfm3+l4w^Y0THW zM4V|PNeKUF?J%k;KRGY2W|~vGKrEliuC**dPF+RiI#LdhnKeOPtZ-!|4_4zC3Cdh0 zcgTqld#aZ#fiX}-SZ=@Smh|v0yaX5T@t0dEw05dQH63Vm7B##!xpU9M^)!_0FyrcJ zz46Hr1%mdjRY+2Q>y&IDkP++Xat2VTH6xu)kUT(0F!h-w zB#(YW!0kzGlN3pT3~7*mIQB! zIH0yK1gThP1SMkd2i<%ck-{BDOb;r5)?IKKeln;-ObBtq!Hn1J#i{OX>mq1bGSO~X zg{T1MydVY-=(}i7AXZ6~3=Ukv`85Vi#BodB&a^>?1#D0Fim;IlcX%Lw0_Z1=C@`Tc zzz(W9=Bf@Nk}QP{kaSuYYIg&!2gEg8M37mGpJqEHOSIrcH5u2yFrNrX0P^B5Q7d|l zCK$zG8txQ>+xi|MAvsWWwp|7cS;|z|e8db2o&RmQz#skjjlJhzI{8Ih`%Y`X#dZkuB-WGE4rR6jsR9xt1wV=0DF&&oI}x0D9nkBdO{I@?!y2;S;mHg z0Np=9fUJ-@>ka>7f(vE#@y?-th+n!F0eUF zDE^K-Xeo&S?f93X5cg{x*$DG`85Tt-=Hk2)C*rC~kUb&Jx${xOiI&8%A<3|$)iqfL z9DXg#SBS@B#o9@QB580X)D$Ds7dVFI+34izBkEX^?y+HU5l3B$ZP5-LiNS+@`PD{vV$mHs7|BmUUuneY6|fKbV%Uguu{?MX-^Yu` zL*TJp{3ZqtQm2HjrF)Mc1_X=&Bu`<0!>||7L0NGbi&ezBBK`!zRYWxN>JaRp07NK} zoEGZbK+^)%(zF;BFSr7NSYjU`p`e2gCa-Q2<=R@+r&sEk2R0fjWnsa?uiPNUQ+<~8rXll}{VVuPz&J}~iwC1fAQ~uSS&8XF@BUYLO z=ZuR04?kRB;xeZ_^oTIkzxU4BR~VKmI( z=^-0JbU`yya5&ZBvgC24DQ)z@g%=QKu7q%J_~Zr1h@b<2NoqlJy(x^W`K{6q$nA_w zPU|+**eOw{btb+Vt0@DiYgx`b61VE(Y1}IRLrlK3S|X72evs%v6j!TTbzVlWzo?9p zjKM;)>Z&EfV6UbTHF0PO;Mhj91XGP9o>pULaU24PSQ6+(f)z?4#{!8QrK~lk#YpoI z40@doB@i`)f+UI~B&(Fm2Dg`e)3Y*&qO3)q_Q$Ox|pnW!cPcxgIp z=(h#IQQgf#YYQx~|Efim%FgcAgg-nio&%M`M8~Mpi6^obz#YwcmnfbhY!)^m4t*C; z5C9~OU%>hb5ib(ts}a?axPzoK_iO=N=`^KPUr5=R9RciFnB1UfphK~G5t>Mt;pUu4 zv#51$b*Y2Y3m*gww9U}EWr?a%1T<&2MMmWo z+A_mw{F;o7oYcNlB`9%Qb{?y>Z1%r1jhN(RApE4YUI5!=wKans41`HavPf|5pz{!o zK&I=Iu*71i5>vAWG9+0dhNP$E)i!3~92pj8N!xeDEj1#!cx}Uv^>DW`NUV^Rc^}J|FOZ%(#+FR1 zjnXjyX`eQC+S?`2V9ArHA@}-WwMj`GCq$))*c>g@iN__zgGLiNIU>stXQOwC+}<#h zku9|kLUbQJ0r6gdbO>FC?$|4-=W8A`iQp-SIwWC4I$E?t5p&hcyBo}s9@TGrSqYKp z-KMG`@y6?-kD4bX*=E(jUXa~`Q%Q9NgH80U>$&7!?$TzN5Vq&B8thy4AJ4<+RRHnE`n-X;UmJzZ?~sloLPG8bXlc^h0vy z`Vvh}3BR1{*wsm#O$;xFrO`rdvg8f?FLtLe=ukE2vAmV3e7R0eg%OyHF13OA&^M_* zFThf6kl{gh1U7~&9@Av;vP+TAfyBC&N`{pHU4#aK*e^LIHDw)UpPhO@PCzXO!X}LoVse4>Q}iTQO>A@ETo%*LAR@L`gT%mYuepP! zJhd1Ejm8ksKeedxl95p+Cr^nbgb%mH5iR_rR6Rv*1*%O#bE8k?$Dt)ndERvnOy(CBM^V7k7d6A$GkX z1*F9F#{@RniD=6XWn4z$4Prd$@35EytEk0(ws_Tu2|>Fdj8m3pNmw`fM2MxaPtbM+ zqbhnCd%**3skYQ-4k_}Zu>jF%ArnF-zcY2v27&PLNrNoEh^wkgdnj7;w9BjaLZ!;) zB|WEDbN}AS8OBCw4Xf?(>iRer*aFH8QSmKurzBcLrh5?WbQ&(u-WI@yaC}iZW8D{6 zvs&+uZPb!?(=kcFR8&~TElrE7%Srd*_n5TsHGSkRJ z(?;T6+Geb-V=#CkGi$3)cfoEW)3jx;0eD)3U)|IbQtEH=GmxoRM^#8VY-_jHXf~`r z-Tw;QxzzfL&i}St;Kx3Dyt@0J{vVX1;D9za;T$jbO^z2<@7noQnkJF=xq7#`+|-f+ zugVHunZ!rZ;$>2%WEH%V6=e7!n>6CHB&h4;`co!Hh%-1)*X7}vBt=NS@UkdIiZQR$&Kb^`mu;K=iHH8ZDug52k#IzDOrO)oo&E^ckkfGg_Nx>u!nt zGu0iDNSuDwr+9~Ml;ppc^a1LuRFebIb)T-Kf7ym~9a=6r!srQgS)22zGu55R(J-qp z8c^<}!m_TC0kTr0QpAfBr%t_rp`x5FEllWLRr*ffgjr@BOZ8)l0ptapJ!zUtAbYyQ ztbvkX3U-&sPlb2DnTA9Xq&}e+!f^ws$P$=LkP8|K!=!Z42z=FHl0&DRMZRE#svkrMHF#qu}M{NTd&WJcQv8^3s78$LI21GA&PS=HQVP zj}%8LsRLA6F{yf>axat4Sc0#Jm({MZZlDKkGmv3r7^PQX$0oF21LditrifaQcm>j{ zMzp~L10Bjmh&?YtPA~%ziB=`8Z&)k^3cU{P%p`vRJv82IK=WSGbEyNNjl|4}jECJ& zdU<;GY$|bRUY(kpoti%1;l|b3D_754nYui4X4<-oVK>b@e|2Uqy+fRhrESU5KfPDj z>WU3>FUj`Aro`w~7ePyl8+^NdT#4W{`zR&wG@CPI5&{vi01XJ7L&KoWM?e`IVdD$1 zYr%OU!bf9+fwduPf=eCo1RK0G==@LrP29mP*M9WZf9IEf=wFkp0BRJ{he=ATy{Oi; z!RRG(4wI?rrg_hoNIG$|YCCI2&&-~?xRx0$MR8=+fW@BL9N$}cTs!YFMK1L%eYh*~ zH@hi&-2fq_0KDtQXVB}kJLj!yhNV(HI^JAtQT1Sx()Lzrr&Aty(qGTG;nA+74O>_5 z5onzItnE^}2#OaFq5%f2uxePlhoGek*buITG%9+pKJPgQdO^&Wbog|j;fU^hJj@27 zTfy=HS3=A?jMxlQ0==}eMEqbRJ@BPLq{Yx#Y>nZ7v}Mr}OpNN_1HtpztmFdN$S}gi zMk38*iL(S@1{38k&$`qwYOwi$T?-v?Oi%T33BL94mP$fv#0`T4s}bn#heY5ei(zz~ z8m4Nr`HFPs10Rjog-dVx_=b!xsa~diB)?Wed*|bMjW{{j1qtj}W5<>5n3POpDueM; zCqVEAT@2O%gEUj$U=-jtptX5-1p$)6DBU2)3$f7=lx>RYWL^`D5qwbEy=_{Qk zxtO--YOUd3i;7lu#6xA&`i9d9NXgE7@EB3T8Z78^JGgV`r56(&iaj_&8;fI1Ix>p} znTClhxEk6MKvS%3fsPn3Ld0%bg4qihJ|NR~;=qI#BLy1;-!|)$LN(}xp*^P(AVu{? z0BcQHxs{!oB8o?s zL8JrAWCY*H-&}3Gz|LwC^t8JvilQqttdN3_>CSG2yv#gallN|rDL5dV0nuLCGw}xHnlDwXbn6PFq)3(zPaVV3$|8Iib_YNpG!d)HN|QU23r-4P3>u^ z!=_XT4M5p|k_sa8`h#TaM@E5z15hpv5?!kxV_TR`r*+GTn4f&Sum9>uZ;xC>@vD#%xJ4-VLit@Tsmwwu zjFAxbZsxTrBz-yUmQf!R{D3e}at=2+xkd<0eR?|n6`T5q6n zQK}ftw@2G5H+NJys@z-|luzbg+SAv6czAFN+89+NL5{g<>sq zC@gPeijvZEK5fueTZa1?n8_BB4nCj!m&c6Ps{oSaA=~Qvde0u2ht=r^Fz45YqDKja}-|d-~ zGFQ`=GT?W3r+YY<@?c7~*&Pv!Av%DQZpV8R`ls`q1lk}kEMljX_sz(d%hIJibBDa< z-1#smYEaBXG*7N^a*b?XZr}{?#WC|~$Llw@$btLxlpHv-K+NB*nSnD`S2J+RHY5X| zxH>!Y{MBjmYAM@eE|70cmf5iRAG1{t1ny$Ws-*Cd8n6{R(pOai?1Y4G%ZQ}+*0B5` z6pq!fr+sG$W7eJVG$yZ_C#*<=?Qp64CQd@z_+nu^zHUgE@`-{D7A z+E_)Yg~(g6RP!`L2LsGV7_P|p=7zyA?|CFWK}h4J^kkAQ-!Vg;R!xe{18dxL4{h%2 ze`a5|O;;_E;<);|<86dqHBH>>k>{|@|pCw(?@fvF?<|d>E-LMgwelHGlqN<@wQ&ln<}q z|E-)MS+h81fB?}AP$P5oir=Wy zIs!Np&tWtn^h`FvcunC76-2Pb3ih0!TOQIUHAzs5M`%;6L zrmrQ|GNFBeVkW?XsN50o6+%;zdT{|^#tK!h3fKpSkVacZbq8m zv7f}SQJhPKn!LICz?Rr0bJ)as+c&YmxjsG`A3r8Hv8TKSflE6RZ(?)jG~Oo>#m5nJ zd^(jQLmM)}frlXW1dygvxkay>R8N&yLM8+-2ypy@%!Y9a$uQ)=om6`jZ(_WO z5r@^fFCoMp2t99Nn#9?(k2kT?yoq5Gqa1}4OCXgC{J?L$`8%7xck|bG;@YM?zumLv zKjRnwFanGKBftnS0*nA7zz8q`i~u9R2rvSSz}pFdCpH&)Th*+a9^YKp(JHpaOTCL; z%-9QDf9}1TKk?E3a9GI&zS^_rn|r?cc6xE{DS^#m9Dk%4Mu7y(9r5qSF`uw!$7uZ&5!Y5V5>9r6=8pQ9Hra)JNzC;s)H z{-X2K--k=@v66&cNsxPEN;w6)$vfY-x$asMkX4F9nt91%a^M0=72H~Q5+;T&?@O8WuA&MBHER|cHsQUm@ zC7U{x8$5F4NM4MMA1*v~1Ry8<_1?fmzze!15mkNiCOY*q+Al*_A10Lyp?3N2Y z;sl2!#9yT-D1~wTMSC(MTx5U|mGMC2XZRI{L4o4kN+(yplI@3$TkBlXlK|6MCIy?K zOkv0j)?p^3BV8%X`wh2nWYqLNeGPeiJo_3*V%sv_whx!(Tdd-=*nO2VuG1)+aW3iJ zW?z>cqv3`#CcRVpO*Q0QZ+97XVE{mG+%Zut9DxJ%5$!UqJ<|2)bs9Q3j);1LkYiW? zEi0B zbO8M@<<*j8Y}|Tr5S0!uifN6UA_wQ;zkWhoP7NKO8Ii68jDe6uSZ=@Smh|v0yaX5T@t4)jCWS`0 z`7)vbfWUX4)ixFa32NpkdK?L;kh;9;R zV6;gxR@!laa4N?Qc}HL_>4mHa4Uu=*bDi#5tx4=`nN_&TL=&W~X+`HyRt0 z2uG&B31fs4wuZ>}yNL-!Mx(X%Y(~iW_X^0BPLqSwCZ?^@^*EENzZJ#z7%%ct4p~C8oraNWtos zk-RBANHvEK1BFA#T$QqO=>+s8iaYI@lofMRmnP-xkohOnbfIvBl?IZ45K)So6*rf% z^+|=DQmelVMxGK%47ixGM7w44ytg0* z59qsSPdGmE?hX##n@;*tw-I8k$~16937%t29x#&Pl=<=$s^J# z1_lSjA@yz8RCG9xO9Oe)dgt&Es=%E>>y4vnySIhwBdH9E>9TM8ya7s(!5@i%#;z)M)Pb51lF5CnX zL+}IH@`rh>0=n<^S(f5KPu0DPp@k{q2%G^_Hi!x^g zi_63qOZkW4k(5*R0z#!=$#V4B!0h46viB5uE_eE%k35hk#2JL(qwPe+bF>)flWUF$ zmt9cH#DK;+)BYmXa9Ij;|BJbz~#V>t$FJ%HRv;;yj{Wi>r0Y%Tb;X=iK?I zL7B&o4M~P2t**&3fW5RZUm>WR6)-LpiclOlO&L$7SyPD(qo0$j4-02Wx{+$WzZG$GJpvY|+EsZe=R-okjG% znHH9k#jQ4hn$;pTlFgq+Stz?_P3ywWr*r8pPcCFxw33`rh;Rg(rSV(WRMUB$Uoay` zk}pJU#AslK*s1o0tR$2z+0B~LMju>wq3p#a(0hFH0-4A)JJ*}S$eQ0O{ebMzWS+ec zBKN7Q7*Q<>wa&y>V>M+UbuG)8M`9j*JVlOpJ1;U8u~jV*NP0iy{DI)SR=4WBjEJ=n zlG3KLS+nY@B_r2eO(Pey;M7A@!-(RGL;6PNWvj8XI1YhCEXkC5SfM0xEFk7P&HzY< z7}H{;c?br*PBKr38bU!5#SxNK%4LJw%f9JZ8AMUmqEGVnnzgzziF`mv42F1~J|fI( zVt|*X!-jra5FFLrEVQ=368kUaAl~$@Q&Z%)d=69&6CI;YC!WY&0CzO&U7~o3u-Wts zS(;oxK>!d9S-|=V5%jo%p|uthR%5%DWp_(W4hCZn~e8Epg*3?#syMjVCCRy*hpSB8hgoJ`b{2aOL! z)%EKnRn)Z2(7I)bs!{|rXSda`+(KJsI3=`gGB$Ej`%;yl#BJGmoZYC-=rn;^NrI8L z+M2-*2ErsISu%|`IuFrEgiWJ_B^FDyiJCo-A;}VyX{zPbHfG@v*zepWUt&lD?h#F(?l-mN5p*wf0OSkY1e+j&r?VAI%#WJAEjA$5SeY}@gN%n=;VkjBO4)lm&ol6LmAmp`yfR3(Gw8w1xSamwL*97mDKY!WUeQk0<#Om zI-(@*Xorr>>gC-HW=W6gH@@sus>l^h?>1EpAz#CUTgPB)o)kO1s5;mSvU_kUsjgtK ziN19ujaV?L5F3kYU9?Kmqb$j`ND=XqVB4&iDsQhwNXi`r6Oly)o6(nb_Q$8XEzntsX)phkWER7aw zlYI&Bzu29^phMN5$MROD^5r@?6(YxvTzMU81M{J8Qhi7dUjlUi#@!Lv7_xXwlf}zO z+6fgv7_4ilWacI4A~XoZe#s?KYd=A$(||UjFpL>-3t-NC*rw25Jd0|eUJFuHgE|4V z90;2 zOOPMh;)oW0QmUSi%(Q{MA85wS?paje-W^hoQwnm!^R9C!FJ@2Nnu*rBHB9ZhMa()g zSF4qjbXtjP5tnp)-b!BI>An^QTu6D^No0H}fmG5iIC$a0WS$I5W2g%waRyr|1U7W+ zmK%^Q3)q;Y{WRu6)d?z!^HB0TshwEoyl7|FRf;j<`eTw!*$R+_D1?0*#2dtT(%)e* z2Ubyw{cQ275fg%TyTZYEMl2EPW0d#lYTBF&p{&fE?3U$RLfK1+gn=iJ>H9H{G3 zW=oQytS^C<89=y844%}uuV3XmB+qINy@F&DtFQyJxXji$0@1f(YqW$~KA7?;`XZhD zRJVzt!C6i!ded61&9im4#QvG;j)*ML&-xVa(2bJ(_mXN{oRw;FAiD0;we&CBkgh|^ z1?JOukz965F7$)O1!H~OsGh8tRTvG(!Bb&bSIGeR1yL#D#fej==?z0gIbB+q(7P&4 zo4yIN%s7_n#}os|3nT_KqQp2Ev6;05I-M1#V0Vcmi+BgfS4kv6>Jxe)95;}oGJ(m2 zPr1Rsc}wY{5%{VD42k5xdsBG@8e|{RvQg`xJGFQz8t261T<@RWjCDYU&}S`7$j}ZRX(3Ls=!okxJ?S zl~zou9;n>Qq%+29hE`aLHB;2!UI+^~4^Y#Cwi(E<+9mZW?AU~U66;YmMbv_{p)mGo zL>oLX(4ky}*z+Rf1TzqkXjRhshQ(5#(Cg67O!5cNLlWmm;n4ijy`<+-2SOW(Ax{|( zyP@>*^z7MG;?TT0H90#qeZIqutFu?Gp1Cr0dFITtbr-{KntA@}%v^ehI2%jb(uv4o z$0couQdbZ@DIb4tMHt;Ead$yWi-lmj2v>>VHTx(f@HCqcK-Pa9I3kAN~b z!p0Y1*MjpzgpbAs1Itj>1eZGE2{w3X(D|SKoA?5+{q~ps{Gspo+VdnUfEtDLVUiMS zFRFEIFnYGQl^$j}IcVLiUH3HMpArZLAVi;YghG9BFwx5m=0fLDucIGCqVEAT@2O% z!?!?sF$S@c*5=(61V{>_bb}x-#70Nh)ugKc_Qy^9W5I4T~gxkVfs&Cqc0$7?kBrxHgzA8H2Rxl9n`SeeazQo3!B| zv4E06NMk2rWNTp=LJdge8!?d*xn`jWfNcd1S2TLIt-j6aE1f2}n6~I@t>IpaidJ^S zLuJ(ZhSLd1$GOqCAB9&FgOWX1@AK&D|L3$BLt1ke;~Tc9Hb zj1aM#mSFZmej;VC$PP?+F;cKm@a@eP^gSj6e`wFC1V~Z65kNGA;VwHhqQ=;HV84l@ zlTzeE4kayGzcmC2@|!!hYO&^7E7zl}faKvq+Ay!)T?dg4ERzv@o12-`f(>A2H3@p! z-E&9L6&hB{i)g`xG!JJanh7M+!C=F$NK`0zNk9Wbx=jK!~GHuC`73 zI#_rznh$7eRf>Tn6i|6Y;-7`ec|`?sI|zVH93U;NCK=g&jbO7cKRgvH^WM|w7G>UmZO z6z#+RL-2^^c77e&%{Y z$C|csZ5JNbEzM1yIX~TAuNc&%)+>v-=@;k3rOPuHCNEzTADq5sw!L}n$WWy4ajMkp z#W^v1_568LMD;FvPn?}TH+l8^oERW}2XuG9l$P-#im$*8HT#6QkHb_E5?wE_%A}Ru zx}DbJgTkZUVjbIVu@U*QHdBdady9(gZM!X*pSG)#A1*?68z;pWhQ}^OXm0xb)0a)H z3y$Z*c>ts7nC_cf4!mG%^`xkDRQkCTgi%wh=3uZ@(b&|UmO5-omCyi`4JfG~GOs^K zwtgi49UOpiX&|3B4`{=?)jyzKRmEDdS~B&i+mN}3KBCE(YR%me94{TO5)`{WpO)xa z1sU7IbULkDPQ?7=<9+>CM|ykYDvDo)oWLzY!57N!a!F+tQgN(p#y@fG(Z2q%eZBDa z(+g30*~CSSsrvR<+X~Eo}>p!x;_Y-O5MgU_e`!}=|Di*@*CgfBQ$z9MFcVfY#eChZ z8&OoR<>nyKu^|-AfAYzPw)OAZ*ZZkGvQ9PL>i4cp4eFXrEY141=%tvvI(Knq77t#S zo}DvGg+lUmCUbg7@oMV+<>_dk1QSpG}b~D?u zk*u1GsXmVE?CYP}-;G+BP8G+@r-xqOx=p^+8!yY3GLzE$-JW?Vb2WV_1Ad2hx`%@) z52j=%-x0AGq65hNI^L7!fgSH8&<1&75j(AX(u|C`EH&YoJLEOz&WA}+gJLG4d2)@D zYh?R!180CQj+sw8Ucb3T4&0}wTnt@ZcAsLXw)!CWnuTGm+OW7WC zfqZMS%!bYXn5}vsa2HcnC54aFfUVe(zN!*nCnS7ZMkKwrhUE{TaI7Xo+LxDJPP?+; zxqdlrl82P}7_e9?C>Oy3X&ke4b!9<(M1EQ0BW5yn;hFRz?^|xJn6WO7n@`(Dwyx@d zG4nv*n}>SkKJeKwIk^-;Rlhf7CYHLknrWq1k`4j9-1N$nY+I4(#8KEv#;BxtCmP{T>ag0XwxbR zkEz0&)nU4S2mZd3J{!5f@BHp#fB6@`|7*|e#G^1k88Qx_hFPslG&4a-3-E@ zOS8a}Bk)4>T?#~TQU$vdaa1~$t0qZ3o){S^m4jipQu`zZD8;b^<}}PqbX-hn=Mr33 z9yYLD2CxD2KRHo_<7BfI(t*aql`yP;ajPTXD&Z^J#ICm5r-?iCO7c-rib8n4LM!MY z&XVcG%59F|6~>=YEVK@AjDXq>Xo59PQkK=!wOxA`+B@| z<5kG>p8BUEb90%!?;ho_iEQy#+z;Hk95@zl)B1MlP`Dr@!HC{_N1--HB_P_I$Nx&o}Xle;5HqfDvE>7y(9r5nu!u0Y-ok zU<4QeM&Lm}VEg91y(FdHv~Ba=9rOV&^=^7GBNzCQ|Ma_G`0P6_>`-!nKknJ{jXi(- zAiOxg10%o)FanGKBftnS0*nA7zz8q`i~u9>ZH>V0&EMHuZ_FcSZPhJr+P=Aehm1l1 zOeju3Di`=h|FeJkH-70C0*w{L4R{a!Q!c~z;{QPy0K4UE6b=~Zbq@cqY|3W=Lqw$d zOpW|dOD6e`vK~oqO9MniF05ru04e?ff&-k&At>ZbK=;$k<0c31QQHDT8xu;aZ^RX; zBVwpA9&K8O8dFSt#dqdive=AH<)kNE+QPgaQbuxwZ7#$$WztE>%^Ju}L)WB@CFS`; zJasYyQPr8>I^B=z$!w!G@7lqs2o2e8_OX*PEsI&$<_YpdGobx|43)^AesmZidBV}3ktK1g_w z-yt=!^$`y!L~7Ov@^&hW>o3}BS>_Z(Br~{7nf!wA_bgCc8ws{w$@as>%{FA!V@l?% zQ#$zf6h#C_1n>?s!I<#EwsqcbxP>F5ruXS<$m`?T*FdtGmhrZY#4I~v<&S0emHO9l zl(AmqIG1#9v#(2!(Qv~?pMsaR-&8~1^>&wGN8n*)!;C~OW1?C(0%PeT+B}*v8(}cC ziFBu-lj8{QHn(6HA+yg4Bu1jCLN=Saq~&$&);wi> zJwuM|bCzE%XGN_rEpyhQ0#Uqmu)ybhm2U0hYwU7pL9`?^jF<=LQqsWHM zoU_zb`ms_t0`$WaKfB}>marb0FAk#8GQae*h#h)xzTx{P#04FaJdLD2fy7hfPE#v+G%3LM;{fQ8h5n)LSPmk)9y|n$RTQO3ryM>p=hDXO=Zl%x& zD_4f)KdO<9W@&Yn5a6}R{|MpdFjw1r6RbBz(kBUm?I$oo?0|iN$h=Btd>+0?aRx@4 zB+EQiaGg4C$UCKkh~QeHHgO{FvgbOjm0FY7*)pqel>sGXMc^0IKxiteFq9D@$venq zCJ;m`ll|`Gh+{+6?rLH-*4*ijXI)F=Y;&~x*6@f5ZkP#nx%R%b#!bugE<3ebzR4|E z2fUSGZe`A4Yl!RtvQoccW*(%m$qD~X{vm}$bn$VOoxh(DPGR~x$E);g4(m=3JB1$o$ zKkM|Y&82L8YWj@|Q3fMViKB)Q%V1h%F+a03Qyv^h&uIhsVW(W4zJWsL5%tXt+-Pt> zZC^;tw$KPOT9!<-TQ<*o z3u5qqzKiyd_8j?j1_yNXU|Cr5c7|1?!veM^d_{POhC4ivKLOq`X+(i>rvy8w>X2a| zWE2PDBJh54cLS~mWMYs^?gE)ar!8jzg0dp0F;a@475%=HWOEo%!`u{u+xi|MA&-3O zY`Y8?vXrT^`G^@5ob?CrKa~soPk-&=U;DqG{Q4JhX;9}8xRN06#*}gjW|@HIvREd- zG65wkV3`2R1Xw0uv17y@(T^oo!0sVsFDw)AZe$L178j{OqvH||$1D?QjaOjJFqR4E z$wC1o%yG4O&V{g@Wdhb=b)~ayOFlB|Nf5DRrOGT5$biK%0V3~`P_ayaWdd(knE(lD z==^WX1^y~uIsJEkCHhTV%WENy)KPbl;gEbn#Wp@ATm&COO44LlUVuD9DwzjIK74kh zG9(6!Yyyf+h-Xw8-wi6rqHP0cr3(e-3;6sOdC zC=8Tb2=Zth(+wO_SYA546BZmv%|Cq~MzPB(J2;Zv{S(RVF2t}CtB+qtI+S}6W$vl9 zMVadsV=UDv43DJtKwdy%@`7dGq4kBUQmesA2>Qf>K2%~S#2JJOBoVWU=V&R_)*cZq zyP%ed4JY-6(1qnrQ;ajId{tHiqy^AP;8G2Mty(az*F0$;lqO~Gg~H)HB3_HDb;`+s z$cN|L`KUoz$BzvON_LZ0*Hj~*Vhi&Xg34I|<5EKjiu9)` z$rO3IOX|p`atPJprE4-Hyo_p%oM9ePw;**M5>KjRhL}EWt!j-n^-OA^mS~PGdf3~o zEM=6FOlF(YDPhUHW)AsWrM5zvZ$ZZt(bS}zi4^{S5{bG7Ns*PeT1<6X_H14zV_Fv# zUpklW^5jB>#Tg5Gga=3DSsyR7uBoPH>$RFj)WiWp z7eJLuvBfdfNQZ7Ub`~m9kccIjP7f=T64DnC?;UzUq(Y1ny$qqWtCP$V<_Do5E29z0 z8c%DWhH1L&o1RtKYvq#BW)s$$wYoBiIHt6aVUUC9@|qanrAYy)-xdT%bvFyGZN!BA zS1qc$PEC>D@;OjBOmvJoX+a}<0o>87cZuTB{E0I33>lhSKtTWy4OzhY3K8_Ug2a?j z9Z5S{Q~TZ31#qR)lvaHq1L*7ssL2hAhI~D-dJ&pPnBjWzN@CtRx4P7E1B!d(8Hh0* zKqhtWZ6E-%u1ZxRK{L=yzD|`;#8*U!rq+>)%ArV z`bdLHmdi33DvaTf{z1;s&YLgog3$0I%&y#iK`Z3Sbs`uC*^{=JjN97m&}S1tS*8GCo}+>8JC*Ef@Is_0vCb;gKsphHLM`wm{rqi?y6Z#IF!< zr80BVoQ-Hb8Vf4g2p||ph&wU%w8&ID=nhwghs2yr(vt^`4@K4W>tt!9X`7*S%Mw*( z9N3)QR>N`&HFnW@hEp=zCSxNfweMEIJ;@x#%eHhzrwG)Bli8P?!43w(B&}Oy3U72C zqLB!^Tj|vXa^=#Nqh=56N@NMjGS$-H#w;8pW<86&Ph4@4heIyj>UdLkwH}Tba~7Gq zm6Z_ab{P~aS}I^W56Tp58vBrJ2$(oAG7HMRQ}dR~tRtCQxDg<#H|%G;kGW-Qm5|kC zj4hd18)R)lr9j%J%{>JgEIE=K%DsMAZBo)|4w_0gC`C(k;&F-bh#W9Qi6qO=_M&%* z+}<#hk!{HrAhfuPo|I|uz{8fvp4=;`=WEDXPdo)?7l?I43Ea`n*y!kc)yumZ%#t3} zZ+zLSRFNlIGSJl!D-;uM9fPfTQtb4i>gc53%{^Ex9Uxc1U=w}oN*b{^wf)(h(W>Rx zRN^PWwpnyw1#@#sjcvFpkcLKJY`kbvlRFSjzJALNmt|p}1KsM{-g1`cG+)3L$n?Y( zmCx>`*|sL-#Lu)Q*;7HbXLZU&q~Mox9lN@&o`$8-LT$1Yga5_u6b2ot20fOyGLtwjP#c&JeUs`#YWNbU12DE3zHH?iLl%!|vUnK@J7E?W2J2cX8CC*x5mtr7 ze#s?K3+Fu$fBOU_m!{zi4P!>!0+=&D#Gv6Xo<%hGW(29d<_?NfxRDSZgl?fB~I+RN-suSe@tNCa7s(!l59dboRN5g7*F~;Eat!}YO$X! zUNvGu&~BudQ{HGvSXVjakg|uE4iF3wb@ej#f}hf+%=*kx^6F|VKr~v&gpi5vOdYgA zAO}iwcZ~K&XRZ2F-gFER9Fr#O)sp>)ak@& z!y_`6O58|_pPr*KEuzh=TSqFmRziE3J8PopH*qihHCER#I6;wLrPZgq;LDL|+VbH5 zJT1bnZt4js3Drs-KzCd@6~VcyDI$d`BptSs_0tE5%{n2KA5zXgEGFThlJdYL7;($X zI9lBQ3fwt2s>MVA(im2y}uIm61q!ViD)8XJMXq;wk*c zj5z@{+zANR58{9d;m6Xcc+sinz;XGfki!#dQ3P>t6FILSZ<1UHf_IX#{gTTX@mUhoJLm4s;6PoMGFy@qW#bEInE`~mQCPcE zmG6)|t2y)vl1;3_H_Q@uwayWUz7<=eCDii4luywY>Ex%nO$-gra#GQo)@p5@t-B@m z&s29rWQl&(r+9~Ml;ppcRO8~TRFebIb)T+f%$W`8I<#D1K22na%Wlbqe$a$vSRXg4 zCo5(ZMgxNKR9MzkGC;9Hs1)(y#HrKthV!DFE-g&xUDbI_--N4X982|MiUH&W5(64h z;*X5j%vu7S&Wcm;#YB=tyaUcOB$6QY$^YNp*({|IL{WI3r{DpUh?UZmJ2hCL%0Qx0 zsVq^{ni2*JA%+*qIuDTt$|K}Ew|m;2;im*-+_*4h0O^^&-S_95(|2@3I9i^kDNoze zLmv%d83>Mk8T z_O82tyLKxXXc#(lfccW&dfsC}vkw1>{2=92!jNWPd}ERDINtT$HwwnAgTw--8f@*Gpw+|nvqF-?*jY7SO!vc zQPS_inL~J81OSH(C2t|)gIJz6n1@MnRdaxrfdNUlM^=22Ag z!q(?^392~eBvuyqwn3HeD6s&BP&F*yHK=TMO^k|<)u%}ZT2slFHG%5Da17VCM;1W* zC~P0Hl5{Xa$J)t~5_sv(MET&*y7HMIGD_&8v?j}dys~J5#OR(p5R@}cC2xp41Efhf zYm><;xxq0b^yn>4T|x?JbwPHezFeZ;-dIKbj06eQ2-4E6N_std91QO?FwzlT;EuXZ zElM8E?e@#Iuc()LFS$*eY1g->N`yYTpb8PI_R(?2!k8tM;ci_8NPaL!u+B8R0(wyc zR`P7#i_Cyf7{?9bUX(@;iE4&dsZR0TifECc^SB<22QFs=(Do=3a)PwF^o3GL2rkp8zF0O*k>) zz#2Am2nXv=y`5rG9+{wFlL>MXQ%PjWY7`UjDN$HZiGc`_XnKY0MZ$+JDRKu1FP1{1 z(2IkwK|_KWMtVc8=zg4{)&%P7cKY(hL>}~;lum~83WqX9>+3iq@SC+&Ezvq_6_%+} z0a -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/closebot-sms/app/next.config.js b/closebot-sms/app/next.config.js deleted file mode 100644 index d6b435e..0000000 --- a/closebot-sms/app/next.config.js +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - experimental: { - serverComponentsExternalPackages: ['better-sqlite3'], - }, -}; - -module.exports = nextConfig; diff --git a/closebot-sms/app/package.json b/closebot-sms/app/package.json deleted file mode 100644 index f063af3..0000000 --- a/closebot-sms/app/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "closebot-sms", - "version": "1.0.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "better-sqlite3": "^11.0.0", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.0", - "handlebars": "^4.7.8", - "lucide-react": "^0.400.0", - "next": "^14.2.0", - "react": "^18.3.0", - "react-dom": "^18.3.0", - "recharts": "^2.12.0", - "tailwind-merge": "^2.3.0", - "twilio": "^5.0.0" - }, - "devDependencies": { - "@types/better-sqlite3": "^7.6.0", - "@types/node": "^20.0.0", - "@types/react": "^18.3.0", - "@types/react-dom": "^18.3.0", - "autoprefixer": "^10.4.0", - "postcss": "^8.4.0", - "tailwindcss": "^3.4.0", - "typescript": "^5.4.0" - } -} diff --git a/closebot-sms/app/postcss.config.js b/closebot-sms/app/postcss.config.js deleted file mode 100644 index 12a703d..0000000 --- a/closebot-sms/app/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/closebot-sms/app/src/app/a2p/page.tsx b/closebot-sms/app/src/app/a2p/page.tsx deleted file mode 100644 index c755652..0000000 --- a/closebot-sms/app/src/app/a2p/page.tsx +++ /dev/null @@ -1,349 +0,0 @@ -'use client'; - -import { useEffect, useState, useCallback } from 'react'; -import Link from 'next/link'; -import { - Shield, - Plus, - RefreshCw, - Clock, - CheckCircle2, - XCircle, - AlertTriangle, - MoreVertical, - Eye, - RotateCcw, - Trash2, - FileText, -} from 'lucide-react'; -import { cn } from '@/lib/utils'; -import type { SubmissionStatus } from '@/lib/a2p-types'; -import { formatStatus, getStatusColor, formatUseCase } from '@/lib/a2p-types'; - -interface A2PRegistration { - id: string; - business_name: string; - status: SubmissionStatus; - brand_trust_score: number | null; - failure_reason: string | null; - attempt_count: number; - created_at: string; - updated_at: string; - input: { - business: { businessName: string }; - campaign: { useCase: string }; - }; -} - -interface A2PStats { - total: number; - pending: number; - approved: number; - failed: number; -} - -function StatusBadge({ status }: { status: SubmissionStatus }) { - const color = getStatusColor(status); - const colorMap: Record = { - green: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20', - red: 'bg-red-500/10 text-red-400 border-red-500/20', - yellow: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20', - blue: 'bg-blue-500/10 text-blue-400 border-blue-500/20', - amber: 'bg-amber-500/10 text-amber-400 border-amber-500/20', - orange: 'bg-orange-500/10 text-orange-400 border-orange-500/20', - slate: 'bg-slate-500/10 text-slate-400 border-slate-500/20', - }; - - return ( - - {formatStatus(status)} - - ); -} - -export default function A2PPage() { - const [registrations, setRegistrations] = useState([]); - const [stats, setStats] = useState({ total: 0, pending: 0, approved: 0, failed: 0 }); - const [loading, setLoading] = useState(true); - const [actionMenuId, setActionMenuId] = useState(null); - - const fetchData = useCallback(async () => { - try { - setLoading(true); - const res = await fetch('/api/a2p?stats=true'); - const data = await res.json(); - setRegistrations(data.registrations || []); - setStats(data.stats || { total: 0, pending: 0, approved: 0, failed: 0 }); - } catch (err) { - console.error('Failed to fetch A2P data:', err); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { fetchData(); }, [fetchData]); - - async function handleRetry(id: string) { - try { - await fetch(`/api/a2p/${id}/retry`, { method: 'POST' }); - setActionMenuId(null); - fetchData(); - } catch (err) { - console.error('Retry failed:', err); - } - } - - async function handleDelete(id: string) { - if (!confirm('Are you sure you want to delete this registration?')) return; - try { - await fetch(`/api/a2p/${id}`, { method: 'DELETE' }); - setActionMenuId(null); - fetchData(); - } catch (err) { - console.error('Delete failed:', err); - } - } - - const statCards = [ - { - label: 'Total Registrations', - value: stats.total, - icon: FileText, - color: 'text-cyan-400', - bgColor: 'bg-cyan-500/10', - borderColor: 'border-cyan-500/20', - }, - { - label: 'Pending', - value: stats.pending, - icon: Clock, - color: 'text-yellow-400', - bgColor: 'bg-yellow-500/10', - borderColor: 'border-yellow-500/20', - }, - { - label: 'Approved', - value: stats.approved, - icon: CheckCircle2, - color: 'text-emerald-400', - bgColor: 'bg-emerald-500/10', - borderColor: 'border-emerald-500/20', - }, - { - label: 'Failed', - value: stats.failed, - icon: XCircle, - color: 'text-red-400', - bgColor: 'bg-red-500/10', - borderColor: 'border-red-500/20', - }, - ]; - - return ( -
- {/* Header */} -
-
-
-
- -
-
-

A2P Registration

-

- Manage 10DLC brand & campaign registrations -

-
-
-
-
- - - - New Registration - -
-
- - {/* Stats Cards */} -
- {statCards.map((card) => { - const Icon = card.icon; - return ( -
-
-
-

{card.label}

-

{card.value}

-
-
- -
-
-
- ); - })} -
- - {/* Registration Table */} -
-
-

All Registrations

-
- - {loading ? ( -
- - Loading registrations... -
- ) : registrations.length === 0 ? ( -
-
- -
-

No registrations yet

-

- Create your first A2P 10DLC registration to start sending compliant business messages. -

- - - Start Registration - -
- ) : ( -
- - - - - - - - - - - - - {registrations.map((reg) => ( - - - - - - - - - ))} - -
- Business Name - - Status - - Brand Score - - Use Case - - Created - - Actions -
-
- {reg.business_name} -
-
- - {reg.failure_reason && ( -
- - {reg.failure_reason} -
- )} -
- {reg.brand_trust_score != null ? ( - = 75 ? 'text-emerald-400' : - reg.brand_trust_score >= 50 ? 'text-yellow-400' : 'text-red-400' - )}> - {reg.brand_trust_score} - - ) : ( - - )} - - - {reg.input?.campaign?.useCase - ? formatUseCase(reg.input.campaign.useCase as Parameters[0]) - : '—'} - - - - {new Date(reg.created_at).toLocaleDateString()} - - -
- - {actionMenuId === reg.id && ( -
- setActionMenuId(null)} - > - - View Details - - {['brand_failed', 'campaign_failed', 'manual_review'].includes(reg.status) && ( - - )} - -
- )} -
-
-
- )} -
-
- ); -} diff --git a/closebot-sms/app/src/app/a2p/register/page.tsx b/closebot-sms/app/src/app/a2p/register/page.tsx deleted file mode 100644 index 507de1b..0000000 --- a/closebot-sms/app/src/app/a2p/register/page.tsx +++ /dev/null @@ -1,995 +0,0 @@ -'use client'; - -import { useState, useEffect, useCallback } from 'react'; -import { useRouter } from 'next/navigation'; -import Link from 'next/link'; -import { - ArrowLeft, - ArrowRight, - Building2, - User, - MapPin, - MessageSquare, - CheckCircle, - Plus, - Trash2, - Shield, - Loader2, -} from 'lucide-react'; -import { cn } from '@/lib/utils'; -import type { - BusinessInfo, - AuthorizedRep, - BusinessAddress, - CampaignInfo, - BusinessType, - BusinessIndustry, - RegistrationIdentifier, - JobPosition, - CampaignUseCase, - OptInType, - CompanyType, -} from '@/lib/a2p-types'; -import { - BUSINESS_TYPE_OPTIONS, - INDUSTRY_OPTIONS, - JOB_POSITION_OPTIONS, - USE_CASE_OPTIONS, - OPT_IN_TYPE_OPTIONS, - COMPANY_TYPE_OPTIONS, - REGISTRATION_ID_OPTIONS, - formatIndustry, - formatUseCase, - formatStatus, -} from '@/lib/a2p-types'; - -const STORAGE_KEY = 'closebot_a2p_wizard_draft'; - -const STEPS = [ - { label: 'Business Info', icon: Building2 }, - { label: 'Authorized Rep', icon: User }, - { label: 'Address', icon: MapPin }, - { label: 'Campaign', icon: MessageSquare }, - { label: 'Review & Submit', icon: CheckCircle }, -]; - -// Default form state -function getDefaultBusiness(): BusinessInfo { - return { - businessName: '', - businessType: 'Corporation', - businessIndustry: 'TECHNOLOGY', - registrationIdentifier: 'EIN', - registrationNumber: '', - websiteUrl: '', - socialMediaUrls: [], - businessIdentity: 'direct_customer', - regionsOfOperation: ['USA_AND_CANADA'], - companyType: 'private', - }; -} - -function getDefaultRep(): AuthorizedRep { - return { - firstName: '', - lastName: '', - businessTitle: '', - jobPosition: 'CEO', - phoneNumber: '', - email: '', - }; -} - -function getDefaultAddress(): BusinessAddress { - return { - customerName: '', - street: '', - streetSecondary: '', - city: '', - region: '', - postalCode: '', - isoCountry: 'US', - }; -} - -function getDefaultCampaign(): CampaignInfo { - return { - useCase: 'MARKETING', - description: '', - sampleMessages: [''], - messageFlow: '', - optInType: 'WEB_FORM', - optInMessage: '', - optOutMessage: 'You have been unsubscribed and will no longer receive messages. Reply START to re-subscribe.', - helpMessage: 'Reply STOP to unsubscribe. For support, contact us at our website.', - hasEmbeddedLinks: false, - hasEmbeddedPhone: false, - }; -} - -// Shared form components -function FormLabel({ children, required }: { children: React.ReactNode; required?: boolean }) { - return ( - - ); -} - -function FormInput({ - value, - onChange, - placeholder, - type = 'text', - required, -}: { - value: string; - onChange: (v: string) => void; - placeholder?: string; - type?: string; - required?: boolean; -}) { - return ( - onChange(e.target.value)} - placeholder={placeholder} - required={required} - className="w-full rounded-lg border border-slate-700/50 bg-slate-800/50 px-4 py-2.5 text-sm text-white placeholder-slate-500 focus:border-cyan-500/50 focus:outline-none focus:ring-2 focus:ring-cyan-500/20 transition-colors" - /> - ); -} - -function FormSelect({ - value, - onChange, - options, - formatLabel, -}: { - value: T; - onChange: (v: T) => void; - options: T[]; - formatLabel?: (v: T) => string; -}) { - return ( - - ); -} - -function FormTextarea({ - value, - onChange, - placeholder, - rows = 3, -}: { - value: string; - onChange: (v: string) => void; - placeholder?: string; - rows?: number; -}) { - return ( - - - - - - - -`; -} - -function buildReportCardModal( - contextJson: string, - pipelineJson: string, - pipeline: Pipeline, -): string { - const dimensions = [ - { id: "code_quality", name: "Code Quality", weight: 3 }, - { id: "test_coverage", name: "Test Coverage", weight: 2 }, - { id: "documentation", name: "Documentation", weight: 2 }, - { id: "error_handling", name: "Error Handling", weight: 2 }, - { id: "architecture", name: "Architecture", weight: 1 }, - { id: "performance", name: "Performance", weight: 1 }, - ]; - - return ` - - - - - Report Card: ${escapeHtml(pipeline.name)} - - - - - -
-

📋 Report Card: ${escapeHtml(pipeline.name)}

-
${escapeHtml(pipeline.platform)} · Rate each dimension 1-10
-
- -
- - ${dimensions - .map( - (d) => ` -
-
- ${d.name} - 5 -
-
- -
-
`, - ) - .join("")} - -
- -
- - - - - -`; -} - -function buildChecklistModal( - contextJson: string, - pipelineJson: string, - pipeline: Pipeline, -): string { - const items = [ - { id: "tests_passing", label: "All tests passing", desc: "Unit, integration, and e2e" }, - { id: "coverage", label: "Test coverage ≥ 80%", desc: "Measured by coverage tool" }, - { id: "readme", label: "README.md complete", desc: "Description, setup, usage, API docs" }, - { id: "tools_doc", label: "TOOLS.md documents all tools", desc: "Every MCP tool documented" }, - { id: "no_secrets", label: "No hardcoded secrets", desc: "All secrets in env vars" }, - { id: "error_handling", label: "Proper error handling", desc: "All API calls wrapped" }, - { id: "code_review", label: "Code review passed", desc: "Reviewed by human or AI" }, - { id: "env_documented", label: "Environment vars documented", desc: ".env.example complete" }, - ]; - - return ` - - - - - Checklist: ${escapeHtml(pipeline.name)} - - - - - -
-

✅ Checklist: ${escapeHtml(pipeline.name)}

-
Complete all items to approve
-
- -
0 / ${items.length} completed
-
- - ${items - .map( - (item) => ` -
- -
-
${item.label}
-
${item.desc}
-
-
`, - ) - .join("")} - - - - - -`; -} - -function escapeHtml(str: string): string { - return str - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} diff --git a/goosefactory/packages/mcp-server/src/prompts/deploy-checklist.ts b/goosefactory/packages/mcp-server/src/prompts/deploy-checklist.ts deleted file mode 100644 index 20ce17a..0000000 --- a/goosefactory/packages/mcp-server/src/prompts/deploy-checklist.ts +++ /dev/null @@ -1,146 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Prompt: deploy_checklist -// Pre-deployment verification checklist -// ═══════════════════════════════════════════════════════════ - -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { z } from "zod"; -import * as api from "../api-client.js"; - -export function registerDeployChecklistPrompt(server: McpServer): void { - server.prompt( - "deploy_checklist", - "Pre-deployment review checklist for an MCP server.", - { - pipeline_id: z.string().describe("UUID of the pipeline being deployed"), - target: z.enum(["staging", "production"]).describe("'staging' or 'production'"), - }, - async ({ pipeline_id, target }) => { - let pipelineInfo = "Pipeline data unavailable."; - let testInfo = "Test data unavailable."; - let stageInfo = "Stage data unavailable."; - - try { - const pipeline = await api.getPipeline(pipeline_id); - pipelineInfo = [ - `Pipeline: ${pipeline.name}`, - `Platform: ${pipeline.platform}`, - `Current Stage: ${pipeline.currentStage}`, - `Priority: ${pipeline.priority}`, - `Template: ${pipeline.template}`, - ].join("\n"); - - try { - const stages = await api.getPipelineStages(pipeline_id); - stageInfo = "Stage Status:"; - for (const s of stages) { - const emoji = - s.status === "completed" - ? "✅" - : s.status === "active" - ? "🔄" - : s.status === "failed" - ? "❌" - : "⏳"; - stageInfo += `\n ${emoji} ${s.stageName} — ${s.status}`; - } - } catch { /* ok */ } - - try { - const tasks = await api.listTasks({ pipelineId: pipeline_id }); - const testTask = tasks.find((t) => t.context?.testResults); - if (testTask?.context?.testResults) { - const tr = testTask.context.testResults; - testInfo = [ - `Total: ${tr.total}`, - `Passed: ${tr.passed}`, - `Failed: ${tr.failed}`, - `Skipped: ${tr.skipped}`, - `Coverage: ${tr.coveragePercent}%`, - ].join("\n"); - } - } catch { /* ok */ } - } catch (err) { - pipelineInfo = `Error: ${err instanceof Error ? err.message : String(err)}`; - } - - const isProduction = target === "production"; - - return { - messages: [ - { - role: "user" as const, - content: { - type: "text" as const, - text: [ - `# Deploy Checklist: ${target.toUpperCase()}`, - "", - `Verifying readiness for **${target}** deployment.`, - isProduction - ? "⚠️ PRODUCTION DEPLOYMENT — Extra rigor required." - : "Staging deployment — verify before promoting to production.", - "", - "## Pipeline Info", - pipelineInfo, - "", - "## Stage Progress", - stageInfo, - "", - "## Test Results", - testInfo, - "", - "## Pre-Deploy Checklist", - "", - "### Code Quality", - "☐ All tests passing (unit, integration, e2e)", - "☐ Code review approved by at least 1 reviewer", - "☐ No critical linting errors", - "☐ No TODO/FIXME items in critical paths", - "", - "### Documentation", - "☐ README.md complete and accurate", - "☐ TOOLS.md documents all MCP tools", - "☐ API changes documented", - "☐ CHANGELOG updated", - "", - "### Configuration", - "☐ Environment variables documented in .env.example", - "☐ No hardcoded secrets in codebase", - "☐ All config values use environment variables", - `☐ ${target} environment variables set correctly`, - "", - "### Security", - "☐ Input validation on all tool parameters (Zod)", - "☐ No sensitive data in logs", - "☐ API keys use scoped permissions", - "☐ Dependencies scanned for vulnerabilities", - "", - ...(isProduction - ? [ - "### Production-Specific", - "☐ Staging deploy tested and verified", - "☐ Performance benchmarks acceptable", - "☐ Rollback plan documented", - "☐ Monitoring/alerting configured", - "☐ Rate limiting configured", - "☐ Error tracking enabled (Sentry/similar)", - "", - ] - : []), - "### Deployment", - "☐ Build succeeds with no warnings", - "☐ Docker image builds and runs locally", - "☐ Health check endpoint responds", - `☐ ${target} DNS/routing configured`, - "", - "## Recommendation", - "Based on the above checklist, provide a GO / NO-GO recommendation.", - "If NO-GO, list the specific items that must be addressed first.", - ].join("\n"), - }, - }, - ], - }; - }, - ); -} diff --git a/goosefactory/packages/mcp-server/src/prompts/needs-attention.ts b/goosefactory/packages/mcp-server/src/prompts/needs-attention.ts deleted file mode 100644 index 9abaf68..0000000 --- a/goosefactory/packages/mcp-server/src/prompts/needs-attention.ts +++ /dev/null @@ -1,158 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Prompt: whats_needs_attention -// Prioritized summary of everything pending -// ═══════════════════════════════════════════════════════════ - -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { z } from "zod"; -import * as api from "../api-client.js"; - -export function registerNeedsAttentionPrompt(server: McpServer): void { - server.prompt( - "whats_needs_attention", - "Summary of everything needing human attention right now, prioritized by urgency.", - { - scope: z - .string() - .optional() - .describe("Filter scope: 'all' | 'my-tasks' | 'critical-only'. Default: all"), - }, - async ({ scope }) => { - const effectiveScope = scope || "all"; - - let pendingTasks = "No pending task data."; - let blockerInfo = "No blocker data."; - let pipelineInfo = "No pipeline data."; - let statsInfo = "No stats available."; - - try { - // Fetch pending tasks - const taskParams: { status: "pending"; priority?: "critical"; assigneeId?: string; limit: number; sort: string } = { - status: "pending", - limit: 20, - sort: "-priority,-sla_deadline", - }; - if (effectiveScope === "critical-only") taskParams.priority = "critical"; - if (effectiveScope === "my-tasks") taskParams.assigneeId = "me"; - - const tasks = await api.listTasks(taskParams); - if (tasks.length > 0) { - const slaBreached = tasks.filter((t) => t.slaBreached); - const critical = tasks.filter((t) => t.priority === "critical"); - const high = tasks.filter((t) => t.priority === "high"); - - pendingTasks = `${tasks.length} pending task(s):`; - if (slaBreached.length > 0) { - pendingTasks += `\n\n🚨 SLA BREACHED (${slaBreached.length}):`; - for (const t of slaBreached) { - pendingTasks += `\n - ${t.title} (${t.id})`; - pendingTasks += `\n Pipeline: ${t.pipelineId} | Stage: ${t.stageName}`; - } - } - if (critical.length > 0) { - pendingTasks += `\n\n🔴 CRITICAL (${critical.length}):`; - for (const t of critical.filter((t) => !t.slaBreached)) { - pendingTasks += `\n - ${t.title} (${t.id})`; - } - } - if (high.length > 0) { - pendingTasks += `\n\n🟡 HIGH (${high.length}):`; - for (const t of high) { - pendingTasks += `\n - ${t.title} (${t.id})`; - } - } - const remaining = tasks.filter((t) => t.priority !== "critical" && t.priority !== "high" && !t.slaBreached); - if (remaining.length > 0) { - pendingTasks += `\n\n🔵 OTHER (${remaining.length}):`; - for (const t of remaining) { - pendingTasks += `\n - ${t.title} [${t.priority}]`; - } - } - } else { - pendingTasks = "✅ No pending tasks — you're all clear!"; - } - } catch (err) { - pendingTasks = `Error fetching tasks: ${err instanceof Error ? err.message : String(err)}`; - } - - try { - const blockers = await api.getBlockers(); - if (blockers.length > 0) { - blockerInfo = `${blockers.length} blocker(s):`; - for (const b of blockers) { - blockerInfo += `\n ⛔ ${b.title} — ${b.blockReason}`; - blockerInfo += `\n Pipeline: ${b.pipelineName} | Blocked: ${b.hoursBlocked.toFixed(1)}h`; - if (b.suggestedAction) blockerInfo += `\n 💡 ${b.suggestedAction}`; - } - } else { - blockerInfo = "✅ Nothing blocked!"; - } - } catch (err) { - blockerInfo = `Error fetching blockers: ${err instanceof Error ? err.message : String(err)}`; - } - - try { - const pipelines = await api.listPipelines({ status: "active", limit: 20 }); - if (pipelines.length > 0) { - pipelineInfo = `${pipelines.length} active pipeline(s):`; - for (const p of pipelines) { - pipelineInfo += `\n ${p.name} — ${p.currentStage} [${p.priority}]`; - } - } else { - pipelineInfo = "No active pipelines."; - } - } catch (err) { - pipelineInfo = `Error: ${err instanceof Error ? err.message : String(err)}`; - } - - try { - const stats = await api.getTaskStats(); - statsInfo = [ - `Pending: ${stats.pending}`, - `In Progress: ${stats.inProgress}`, - `Blocked: ${stats.blocked}`, - `Avg Wait: ${stats.avgWaitTimeMinutes} min`, - `SLA Breaches: ${stats.slaBreaches}`, - ].join(" | "); - } catch { /* ok */ } - - return { - messages: [ - { - role: "user" as const, - content: { - type: "text" as const, - text: [ - `# What Needs Attention (scope: ${effectiveScope})`, - "", - "Here is a prioritized summary of everything requiring human attention.", - "Review and act on the most urgent items first.", - "", - `## 📊 Quick Stats`, - statsInfo, - "", - "## 📥 Pending Tasks", - pendingTasks, - "", - "## ⛔ Blockers", - blockerInfo, - "", - "## ▶️ Active Pipelines", - pipelineInfo, - "", - "## 💡 Suggested Actions", - "Based on the above, here's what I recommend tackling first:", - "1. Address any SLA breaches immediately", - "2. Review critical-priority tasks", - "3. Unblock any stuck pipelines", - "4. Process remaining pending approvals", - "", - "What would you like to do first?", - ].join("\n"), - }, - }, - ], - }; - }, - ); -} diff --git a/goosefactory/packages/mcp-server/src/prompts/retrospective.ts b/goosefactory/packages/mcp-server/src/prompts/retrospective.ts deleted file mode 100644 index 6aa9c6c..0000000 --- a/goosefactory/packages/mcp-server/src/prompts/retrospective.ts +++ /dev/null @@ -1,185 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Prompt: pipeline_retrospective -// Post-completion analysis and lessons learned -// ═══════════════════════════════════════════════════════════ - -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { z } from "zod"; -import * as api from "../api-client.js"; - -export function registerRetrospectivePrompt(server: McpServer): void { - server.prompt( - "pipeline_retrospective", - "Generate a retrospective analysis of a completed pipeline.", - { - pipeline_id: z.string().describe("UUID of the completed pipeline"), - }, - async ({ pipeline_id }) => { - let pipelineInfo = "Pipeline data unavailable."; - let stageTimeline = "Stage timeline unavailable."; - let taskHistory = "Task history unavailable."; - let auditTrail = "Audit trail unavailable."; - - try { - const pipeline = await api.getPipeline(pipeline_id); - - const durationMs = pipeline.completedAt - ? new Date(pipeline.completedAt).getTime() - new Date(pipeline.startedAt).getTime() - : new Date().getTime() - new Date(pipeline.startedAt).getTime(); - const durationHours = (durationMs / (1000 * 60 * 60)).toFixed(1); - - pipelineInfo = [ - `Pipeline: ${pipeline.name}`, - `Platform: ${pipeline.platform}`, - `Status: ${pipeline.status}`, - `Priority: ${pipeline.priority}`, - `Template: ${pipeline.template}`, - `Duration: ${durationHours} hours`, - `Started: ${pipeline.startedAt}`, - pipeline.completedAt ? `Completed: ${pipeline.completedAt}` : "Still in progress", - ].join("\n"); - - try { - const stages = await api.getPipelineStages(pipeline_id); - stageTimeline = "Stage-by-stage breakdown:"; - - let totalGateTime = 0; - let totalBuildTime = 0; - - for (const s of stages) { - const emoji = - s.status === "completed" - ? "✅" - : s.status === "failed" - ? "❌" - : s.status === "skipped" - ? "⏭️" - : "⏳"; - - const duration = s.durationSeconds - ? `${(s.durationSeconds / 60).toFixed(0)} min` - : "—"; - - stageTimeline += `\n ${emoji} ${s.stageName} — ${s.status} (${duration})`; - if (s.requiresApproval) stageTimeline += " ★ GATE"; - - if (s.durationSeconds) { - if (s.requiresApproval) { - totalGateTime += s.durationSeconds; - } else { - totalBuildTime += s.durationSeconds; - } - } - } - - stageTimeline += `\n\nTime in gates (waiting for human): ${(totalGateTime / 60).toFixed(0)} min`; - stageTimeline += `\nTime in build stages: ${(totalBuildTime / 60).toFixed(0)} min`; - - if (totalGateTime + totalBuildTime > 0) { - const gatePercent = Math.round( - (totalGateTime / (totalGateTime + totalBuildTime)) * 100, - ); - stageTimeline += `\nHuman bottleneck: ${gatePercent}% of total time`; - } - } catch { /* ok */ } - - try { - const tasks = await api.listTasks({ pipelineId: pipeline_id, limit: 50 }); - if (tasks.length > 0) { - const approved = tasks.filter((t) => t.decision === "approved").length; - const rejected = tasks.filter((t) => t.decision === "rejected").length; - const slaBreached = tasks.filter((t) => t.slaBreached).length; - - taskHistory = [ - `Total tasks: ${tasks.length}`, - `Approved: ${approved}`, - `Rejected: ${rejected}`, - `SLA Breaches: ${slaBreached}`, - ].join("\n"); - - if (rejected > 0) { - taskHistory += "\n\nRejection reasons:"; - for (const t of tasks.filter((t) => t.decision === "rejected")) { - taskHistory += `\n - ${t.title}: ${t.decisionNotes || "No reason given"}`; - } - } - } - } catch { /* ok */ } - - try { - const audit = await api.getAuditLog({ - entityType: "pipeline", - entityId: pipeline_id, - limit: 30, - }); - if (audit.length > 0) { - auditTrail = "Key events:"; - for (const e of audit.slice(0, 20)) { - auditTrail += `\n [${new Date(e.createdAt).toLocaleString()}] ${e.action}`; - if (e.actorName) auditTrail += ` by ${e.actorName}`; - } - } - } catch { /* ok */ } - } catch (err) { - pipelineInfo = `Error: ${err instanceof Error ? err.message : String(err)}`; - } - - return { - messages: [ - { - role: "user" as const, - content: { - type: "text" as const, - text: [ - `# Pipeline Retrospective`, - "", - "Analyze this pipeline's journey from start to finish.", - "Identify bottlenecks, what went well, and what to improve.", - "", - "## Pipeline Summary", - pipelineInfo, - "", - "## Stage Timeline", - stageTimeline, - "", - "## Task History", - taskHistory, - "", - "## Audit Trail", - auditTrail, - "", - "## Analysis Questions", - "", - "### 1. Timeline Analysis", - "- How long did each stage take?", - "- Where were the biggest delays?", - "- Was the human the bottleneck? By how much?", - "", - "### 2. Quality Analysis", - "- How many iterations/rejections occurred?", - "- What were the common rejection reasons?", - "- Were there any SLA breaches? Why?", - "", - "### 3. What Went Well", - "- Which stages flowed smoothly?", - "- Any auto-advanced stages that worked perfectly?", - "", - "### 4. What to Improve", - "- Which gates could be automated or loosened?", - "- What patterns should agents learn to avoid rejections?", - "- Should SLA thresholds be adjusted?", - "", - "### 5. Recommendations", - "- Specific improvements for the next pipeline", - "- New rules or standards to add to the factory", - "- Template changes to consider", - "", - "Please provide a structured retrospective covering all 5 areas.", - ].join("\n"), - }, - }, - ], - }; - }, - ); -} diff --git a/goosefactory/packages/mcp-server/src/prompts/review-server.ts b/goosefactory/packages/mcp-server/src/prompts/review-server.ts deleted file mode 100644 index 6ac1777..0000000 --- a/goosefactory/packages/mcp-server/src/prompts/review-server.ts +++ /dev/null @@ -1,135 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Prompt: review_server -// Pull all context needed to review an MCP server -// ═══════════════════════════════════════════════════════════ - -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { z } from "zod"; -import * as api from "../api-client.js"; - -export function registerReviewServerPrompt(server: McpServer): void { - server.prompt( - "review_server", - "Pull all context needed to review an MCP server: code quality, " + - "test results, docs, deployment readiness. Returns a structured review prompt.", - { server_name: z.string().describe("Name of the MCP server to review") }, - async ({ server_name }) => { - let pipelineInfo = "Pipeline data unavailable."; - let testInfo = "Test results unavailable."; - let taskInfo = "No pending tasks."; - let assetInfo = "No assets found."; - - try { - const pipelines = await api.listPipelines({ search: server_name, limit: 1 }); - if (pipelines.length > 0) { - const p = pipelines[0]; - pipelineInfo = [ - `Pipeline: ${p.name} (${p.id})`, - `Platform: ${p.platform}`, - `Stage: ${p.currentStage}`, - `Status: ${p.status}`, - `Priority: ${p.priority}`, - `Template: ${p.template}`, - `Created: ${p.createdAt}`, - `Updated: ${p.updatedAt}`, - ].join("\n"); - - try { - const stages = await api.getPipelineStages(p.id); - pipelineInfo += "\n\nStage History:"; - for (const s of stages) { - const status = - s.status === "completed" ? "✅" : s.status === "active" ? "🔄" : s.status === "failed" ? "❌" : "⏳"; - pipelineInfo += `\n ${status} ${s.stageName} (${s.status})`; - if (s.requiresApproval) pipelineInfo += " ★ GATE"; - } - } catch { /* ok */ } - - try { - const tasks = await api.listTasks({ pipelineId: p.id, limit: 20 }); - if (tasks.length > 0) { - taskInfo = `${tasks.length} task(s):`; - for (const t of tasks) { - taskInfo += `\n [${t.priority}] ${t.title} — ${t.status}`; - if (t.context?.testResults) { - const tr = t.context.testResults; - testInfo = [ - `Tests: ${tr.passed}/${tr.total} passed`, - `Coverage: ${tr.coveragePercent}%`, - `Failed: ${tr.failed}`, - `Skipped: ${tr.skipped}`, - ].join("\n"); - - if (tr.failureDetails?.length) { - testInfo += "\n\nFailures:"; - for (const f of tr.failureDetails) { - testInfo += `\n ❌ ${f.testName}: ${f.error}`; - } - } - } - } - } - } catch { /* ok */ } - - try { - const assets = await api.listAssets(p.id); - if (assets.length > 0) { - assetInfo = `${assets.length} asset(s):`; - for (const a of assets) { - assetInfo += `\n [${a.type}] ${a.name} (v${a.version})`; - } - } - } catch { /* ok */ } - } else { - pipelineInfo = `No pipeline found matching "${server_name}".`; - } - } catch (err) { - pipelineInfo = `Error fetching pipeline: ${err instanceof Error ? err.message : String(err)}`; - } - - return { - messages: [ - { - role: "user" as const, - content: { - type: "text" as const, - text: [ - `# MCP Server Review: ${server_name}`, - "", - "You are reviewing an MCP server for quality, completeness, and deployment readiness.", - "Use the context below to conduct a thorough review.", - "", - "## Pipeline Status", - pipelineInfo, - "", - "## Test Results", - testInfo, - "", - "## Pending Tasks", - taskInfo, - "", - "## Assets", - assetInfo, - "", - "## Review Checklist", - "Please evaluate against these criteria:", - "1. ☐ Code quality — Clean, maintainable, follows conventions", - "2. ☐ Test coverage — Adequate coverage with meaningful tests", - "3. ☐ Error handling — All API calls properly wrapped, graceful failures", - "4. ☐ Documentation — README complete, TOOLS.md documents all tools", - "5. ☐ Security — No hardcoded secrets, proper input validation", - "6. ☐ Performance — No obvious bottlenecks, efficient API calls", - "7. ☐ Architecture — Clean separation of concerns, extensible design", - "8. ☐ Deployment readiness — Environment vars documented, configs complete", - "", - "## Your Review", - "For each criterion, indicate PASS, NEEDS WORK, or FAIL with specific feedback.", - "End with an overall recommendation: APPROVE, NEEDS WORK, or REJECT.", - ].join("\n"), - }, - }, - ], - }; - }, - ); -} diff --git a/goosefactory/packages/mcp-server/src/resources/config.ts b/goosefactory/packages/mcp-server/src/resources/config.ts deleted file mode 100644 index 8e459ad..0000000 --- a/goosefactory/packages/mcp-server/src/resources/config.ts +++ /dev/null @@ -1,131 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Resource: factory://config/templates -// ═══════════════════════════════════════════════════════════ - -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { PipelineTemplateConfig } from "../types.js"; - -// Hardcoded templates — these are the canonical pipeline templates -const TEMPLATES: PipelineTemplateConfig[] = [ - { - id: "mcp-server-standard", - name: "MCP Server — Standard", - description: - "Full-featured MCP server pipeline with all 8 stages, comprehensive testing, " + - "code review gate, staging deploy, and production deploy.", - stages: [ - "intake", - "scaffolding", - "building", - "testing", - "review", - "staging", - "production", - "published", - ], - defaultConfig: { - runtime: "node", - language: "typescript", - testFramework: "vitest", - minCoverage: 80, - requireCodeReview: true, - requireStagingDeploy: true, - }, - requiredAssets: ["code", "config", "docs", "test_report"], - validationRules: [ - { type: "test_coverage", config: { minPercent: 80 } }, - { type: "tests_passing", config: { minPassRate: 1.0 } }, - { type: "required_asset", config: { assetType: "docs" } }, - { type: "approval_count", config: { minApprovals: 1 } }, - ], - }, - { - id: "mcp-server-minimal", - name: "MCP Server — Minimal", - description: - "Lightweight pipeline for simple MCP servers. Fewer gates, " + - "lower coverage requirements. Good for quick iterations.", - stages: [ - "intake", - "scaffolding", - "building", - "testing", - "review", - "production", - "published", - ], - defaultConfig: { - runtime: "node", - language: "typescript", - testFramework: "vitest", - minCoverage: 60, - requireCodeReview: true, - requireStagingDeploy: false, - }, - requiredAssets: ["code", "config"], - validationRules: [ - { type: "test_coverage", config: { minPercent: 60 } }, - { type: "tests_passing", config: { minPassRate: 0.95 } }, - ], - }, - { - id: "mcp-server-enterprise", - name: "MCP Server — Enterprise", - description: - "Maximum rigor pipeline for enterprise-grade MCP servers. " + - "Multi-approver gates, high coverage, security review, performance benchmarks.", - stages: [ - "intake", - "scaffolding", - "building", - "testing", - "review", - "staging", - "production", - "published", - ], - defaultConfig: { - runtime: "node", - language: "typescript", - testFramework: "vitest", - minCoverage: 95, - requireCodeReview: true, - requireStagingDeploy: true, - requireSecurityReview: true, - requirePerformanceBenchmarks: true, - minApprovers: 2, - }, - requiredAssets: ["code", "config", "docs", "test_report", "build"], - validationRules: [ - { type: "test_coverage", config: { minPercent: 95 } }, - { type: "tests_passing", config: { minPassRate: 1.0 } }, - { type: "required_asset", config: { assetType: "docs" } }, - { type: "required_asset", config: { assetType: "test_report" } }, - { type: "approval_count", config: { minApprovals: 2 } }, - { type: "custom", config: { name: "security_review", required: true } }, - { type: "custom", config: { name: "perf_benchmarks", threshold: "p99 < 200ms" } }, - ], - }, -]; - -export function registerConfigResources(server: McpServer): void { - server.resource( - "config-templates", - "factory://config/templates", - { - description: "Available pipeline templates and their configurations", - mimeType: "application/json", - }, - async () => { - return { - contents: [ - { - uri: "factory://config/templates", - mimeType: "application/json" as const, - text: JSON.stringify(TEMPLATES, null, 2), - }, - ], - }; - }, - ); -} diff --git a/goosefactory/packages/mcp-server/src/resources/dashboard.ts b/goosefactory/packages/mcp-server/src/resources/dashboard.ts deleted file mode 100644 index 0cc5337..0000000 --- a/goosefactory/packages/mcp-server/src/resources/dashboard.ts +++ /dev/null @@ -1,49 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Resource: factory://dashboard/summary -// ═══════════════════════════════════════════════════════════ - -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import * as api from "../api-client.js"; - -export function registerDashboardResources(server: McpServer): void { - server.resource( - "dashboard-summary", - "factory://dashboard/summary", - { - description: "High-level factory status: active pipelines, pending tasks, blockers, agent health", - mimeType: "application/json", - }, - async () => { - try { - const summary = await api.getDashboardSummary(); - return { - contents: [ - { - uri: "factory://dashboard/summary", - mimeType: "application/json" as const, - text: JSON.stringify(summary, null, 2), - }, - ], - }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { - contents: [ - { - uri: "factory://dashboard/summary", - mimeType: "application/json" as const, - text: JSON.stringify({ - error: msg, - pipelines: { active: 0, paused: 0, completed: 0, failed: 0 }, - tasks: { pending: 0, inProgress: 0, blocked: 0, avgWaitTimeMinutes: 0, slaBreaches: 0, byPriority: {}, byType: {} }, - agents: { total: 0, active: 0, idle: 0, error: 0, offline: 0 }, - recentActivity: [], - slaStatus: { onTrack: 0, warning: 0, breached: 0 }, - }, null, 2), - }, - ], - }; - } - }, - ); -} diff --git a/goosefactory/packages/mcp-server/src/resources/pipelines.ts b/goosefactory/packages/mcp-server/src/resources/pipelines.ts deleted file mode 100644 index 759e5da..0000000 --- a/goosefactory/packages/mcp-server/src/resources/pipelines.ts +++ /dev/null @@ -1,249 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Resources: pipeline state, test results, build logs -// ═══════════════════════════════════════════════════════════ - -import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; -import * as api from "../api-client.js"; - -export function registerPipelineResources(server: McpServer): void { - // ─────────────────────────────────────── - // factory://pipelines/{pipeline_id}/state - // ─────────────────────────────────────── - server.resource( - "pipeline-state", - new ResourceTemplate("factory://pipelines/{pipeline_id}/state", { - list: undefined, - }), - { - description: "Current state of a specific pipeline including stage, progress, tasks, and blockers", - mimeType: "application/json", - }, - async (uri, params) => { - const pipelineId = params.pipeline_id as string; - try { - const [pipeline, stages, tasks] = await Promise.all([ - api.getPipeline(pipelineId), - api.getPipelineStages(pipelineId).catch(() => []), - api.listTasks({ pipelineId, limit: 50 }).catch(() => []), - ]); - - let blockers: Awaited> = []; - try { - blockers = await api.getBlockers(pipelineId); - } catch { /* optional */ } - - const state = { - pipeline, - stages, - currentTasks: tasks, - recentAssets: [], - blockers, - timeline: [], - }; - - return { - contents: [ - { - uri: uri.href, - mimeType: "application/json" as const, - text: JSON.stringify(state, null, 2), - }, - ], - }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { - contents: [ - { - uri: uri.href, - mimeType: "application/json" as const, - text: JSON.stringify({ error: msg }, null, 2), - }, - ], - }; - } - }, - ); - - // ─────────────────────────────────────── - // factory://servers/{server_name}/status - // ─────────────────────────────────────── - server.resource( - "server-status", - new ResourceTemplate("factory://servers/{server_name}/status", { - list: undefined, - }), - { - description: "Health, version, deployment status of a specific MCP server", - mimeType: "application/json", - }, - async (uri, params) => { - const serverName = params.server_name as string; - try { - // Search for the pipeline by server name - const pipelines = await api.listPipelines({ search: serverName, limit: 1 }); - - if (pipelines.length === 0) { - return { - contents: [ - { - uri: uri.href, - mimeType: "application/json" as const, - text: JSON.stringify( - { error: `Server "${serverName}" not found`, name: serverName, health: "unknown" }, - null, - 2, - ), - }, - ], - }; - } - - const pipeline = pipelines[0]; - const status = { - name: serverName, - pipelineId: pipeline.id, - stage: pipeline.currentStage, - health: - pipeline.status === "active" - ? "healthy" - : pipeline.status === "failed" - ? "failing" - : "unknown", - deployments: { - staging: null, - production: null, - }, - lastTestRun: null, - metrics: {}, - }; - - return { - contents: [ - { - uri: uri.href, - mimeType: "application/json" as const, - text: JSON.stringify(status, null, 2), - }, - ], - }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { - contents: [ - { - uri: uri.href, - mimeType: "application/json" as const, - text: JSON.stringify({ error: msg, name: serverName }, null, 2), - }, - ], - }; - } - }, - ); - - // ─────────────────────────────────────── - // factory://pipelines/{pipeline_id}/test-results - // ─────────────────────────────────────── - server.resource( - "pipeline-test-results", - new ResourceTemplate("factory://pipelines/{pipeline_id}/test-results", { - list: undefined, - }), - { - description: "Latest test results with pass/fail counts, coverage, and failure details", - mimeType: "application/json", - }, - async (uri, params) => { - const pipelineId = params.pipeline_id as string; - try { - // Fetch pipeline and look for test results in tasks context - const tasks = await api.listTasks({ pipelineId, limit: 50 }); - const testTask = tasks.find( - (t) => t.context?.testResults || t.type === "review", - ); - - const results = testTask?.context?.testResults || { - total: 0, - passed: 0, - failed: 0, - skipped: 0, - coveragePercent: 0, - failureDetails: [], - }; - - return { - contents: [ - { - uri: uri.href, - mimeType: "application/json" as const, - text: JSON.stringify(results, null, 2), - }, - ], - }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { - contents: [ - { - uri: uri.href, - mimeType: "application/json" as const, - text: JSON.stringify({ error: msg }, null, 2), - }, - ], - }; - } - }, - ); - - // ─────────────────────────────────────── - // factory://pipelines/{pipeline_id}/build-logs - // ─────────────────────────────────────── - server.resource( - "pipeline-build-logs", - new ResourceTemplate("factory://pipelines/{pipeline_id}/build-logs", { - list: undefined, - }), - { - description: "Build output logs for the latest build of a pipeline", - mimeType: "text/plain", - }, - async (uri, params) => { - const pipelineId = params.pipeline_id as string; - try { - // In a full implementation, this would fetch from the build system - // For now, construct from audit log - const auditEntries = await api.getAuditLog({ - entityType: "pipeline", - entityId: pipelineId, - limit: 50, - }); - - const logLines = auditEntries - .map((e) => `[${e.createdAt}] ${e.action} by ${e.actorName || e.actorType}`) - .join("\n"); - - return { - contents: [ - { - uri: uri.href, - mimeType: "text/plain" as const, - text: logLines || `No build logs available for pipeline ${pipelineId}`, - }, - ], - }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { - contents: [ - { - uri: uri.href, - mimeType: "text/plain" as const, - text: `Error fetching build logs: ${msg}`, - }, - ], - }; - } - }, - ); -} diff --git a/goosefactory/packages/mcp-server/src/tools/operations.ts b/goosefactory/packages/mcp-server/src/tools/operations.ts deleted file mode 100644 index 02549ae..0000000 --- a/goosefactory/packages/mcp-server/src/tools/operations.ts +++ /dev/null @@ -1,281 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Operations Tools: run_tests, deploy, assign_priority, get_blockers -// ═══════════════════════════════════════════════════════════ - -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { z } from "zod"; -import * as api from "../api-client.js"; -import type { Priority, BlockerInfo } from "../types.js"; - -const priorityEmoji: Record = { - critical: "🔴", - high: "🟡", - medium: "🔵", - low: "⚪", -}; - -function formatBlocker(b: BlockerInfo): string { - let out = `${priorityEmoji[b.priority]} **${b.title}**`; - out += `\n Pipeline: ${b.pipelineName} (${b.pipelineId})`; - out += `\n Type: ${b.blockerType}`; - out += `\n Reason: ${b.blockReason}`; - out += `\n Blocked for: ${b.hoursBlocked.toFixed(1)}h`; - if (b.suggestedAction) { - out += `\n 💡 Suggested: ${b.suggestedAction}`; - } - return out; -} - -export function registerOperationsTools(server: McpServer): void { - // ─────────────────────────────────────── - // factory_assign_priority - // ─────────────────────────────────────── - server.tool( - "factory_assign_priority", - "Set or change priority on a pipeline or task. Recalculates SLA deadlines.", - { - entity_type: z.enum(["pipeline", "task"]).describe("Type of entity to update"), - entity_id: z.string().uuid().describe("UUID of the pipeline or task"), - priority: z.enum(["critical", "high", "medium", "low"]).describe("New priority level"), - reason: z.string().optional().describe("Why the priority changed"), - }, - async ({ entity_type, entity_id, priority, reason }) => { - try { - if (entity_type === "pipeline") { - const pipeline = await api.updatePipeline(entity_id, { - priority: priority as Priority, - metadata: reason ? { priorityChangeReason: reason } : undefined, - }); - - return { - content: [ - { - type: "text" as const, - text: - `✅ **Priority Updated**\n` + - ` Pipeline: ${pipeline.name}\n` + - ` New Priority: ${priorityEmoji[priority as Priority]} ${priority}\n` + - (reason ? ` Reason: ${reason}\n` : "") + - ` SLA deadlines have been recalculated.`, - }, - ], - }; - } else { - // For tasks, we use the complete endpoint with deferred decision - // to update the task. In a real implementation, there'd be a PATCH /tasks/:id - // For now, we'll note this as a limitation. - return { - content: [ - { - type: "text" as const, - text: - `✅ **Task Priority Update Requested**\n` + - ` Task: ${entity_id}\n` + - ` New Priority: ${priorityEmoji[priority as Priority]} ${priority}\n` + - (reason ? ` Reason: ${reason}\n` : "") + - ` Note: Task priority update sent to the API.`, - }, - ], - }; - } - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { - content: [{ type: "text" as const, text: `❌ Error assigning priority: ${msg}` }], - isError: true, - }; - } - }, - ); - - // ─────────────────────────────────────── - // factory_get_blockers - // ─────────────────────────────────────── - server.tool( - "factory_get_blockers", - "Get all items that are blocked and why. The 'what's stuck' view. " + - "Includes suggested actions to unblock.", - { - pipeline_id: z.string().uuid().optional().describe("Filter to a specific pipeline"), - include_suggestions: z - .boolean() - .default(true) - .describe("Include AI-generated suggestions for unblocking. Default: true"), - }, - async ({ pipeline_id, include_suggestions }) => { - try { - const blockers = await api.getBlockers(pipeline_id); - - if (blockers.length === 0) { - return { - content: [ - { - type: "text" as const, - text: "✅ **No blockers!** All pipelines are flowing smoothly.", - }, - ], - }; - } - - let response = `⛔ **${blockers.length} Blocker(s) Found**`; - response += "\n" + "─".repeat(60); - - // Group by priority - const byPriority: Record = {}; - for (const b of blockers) { - if (!byPriority[b.priority]) byPriority[b.priority] = []; - byPriority[b.priority].push(b); - } - - for (const prio of ["critical", "high", "medium", "low"] as Priority[]) { - const group = byPriority[prio]; - if (!group) continue; - response += `\n\n**${prio.toUpperCase()} (${group.length})**`; - for (const b of group) { - response += `\n\n ${formatBlocker(b)}`; - } - } - - if (include_suggestions && blockers.length > 0) { - response += `\n\n${"─".repeat(60)}`; - response += `\n💡 **Quick Actions:**`; - const criticals = blockers.filter((b) => b.priority === "critical"); - if (criticals.length > 0) { - response += `\n 1. Address ${criticals.length} critical blocker(s) first`; - } - const tasks = blockers.filter((b) => b.blockerType === "task"); - if (tasks.length > 0) { - response += `\n ${criticals.length > 0 ? "2" : "1"}. ${tasks.length} task(s) awaiting decision — use factory_approve_task or factory_reject_task`; - } - } - - return { - content: [{ type: "text" as const, text: response }], - }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { - content: [{ type: "text" as const, text: `❌ Error fetching blockers: ${msg}` }], - isError: true, - }; - } - }, - ); - - // ─────────────────────────────────────── - // factory_run_tests - // ─────────────────────────────────────── - server.tool( - "factory_run_tests", - "Trigger test suite for a pipeline's current build.", - { - pipeline_id: z.string().uuid().describe("UUID of the pipeline"), - test_type: z - .enum(["unit", "integration", "e2e", "all"]) - .default("all") - .describe("Which tests to run. Default: all"), - environment: z - .enum(["local", "ci"]) - .default("ci") - .describe("Where to run. Default: ci"), - }, - async ({ pipeline_id, test_type, environment }) => { - try { - const result = await api.triggerTests({ - pipeline_id, - test_type, - environment, - }); - - let response = `🧪 **Test Run Initiated**`; - response += `\n Run ID: ${result.runId}`; - response += `\n Pipeline: ${result.pipelineId}`; - response += `\n Type: ${test_type}`; - response += `\n Environment: ${environment}`; - response += `\n Status: ${result.status}`; - - if (result.results) { - const r = result.results; - response += `\n\n Results:`; - response += `\n Total: ${r.total}`; - response += `\n ✅ Passed: ${r.passed}`; - response += `\n ❌ Failed: ${r.failed}`; - response += `\n ⏭️ Skipped: ${r.skipped}`; - response += `\n 📊 Coverage: ${r.coveragePercent}%`; - } else { - response += `\n\n Tests are queued. Check back with factory_get_pipeline_status for results.`; - } - - return { - content: [{ type: "text" as const, text: response }], - }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { - content: [{ type: "text" as const, text: `❌ Error triggering tests: ${msg}` }], - isError: true, - }; - } - }, - ); - - // ─────────────────────────────────────── - // factory_deploy - // ─────────────────────────────────────── - server.tool( - "factory_deploy", - "Deploy a pipeline's build to staging or production. " + - "Production requires prior staging deploy + approval.", - { - pipeline_id: z.string().uuid().describe("UUID of the pipeline to deploy"), - target: z.enum(["staging", "production"]).describe("Deployment target"), - version: z.string().optional().describe("Specific version. Default: latest build"), - dry_run: z - .boolean() - .default(false) - .describe("Preview what would happen without deploying. Default: false"), - }, - async ({ pipeline_id, target, version, dry_run }) => { - try { - const result = await api.triggerDeploy({ - pipeline_id, - target, - version, - dry_run, - }); - - const emoji = target === "production" ? "🌍" : "🚀"; - - let response = dry_run - ? `${emoji} **Deploy Dry Run** (no changes made)` - : `${emoji} **Deployment ${result.status === "started" ? "Started" : result.status === "succeeded" ? "Succeeded" : "Failed"}**`; - - response += `\n Deployment ID: ${result.deploymentId}`; - response += `\n Pipeline: ${result.pipelineId}`; - response += `\n Target: ${target}`; - response += `\n Version: ${result.version}`; - - if (result.url) { - response += `\n URL: ${result.url}`; - } - if (result.error) { - response += `\n ❌ Error: ${result.error}`; - } - - if (target === "production" && !dry_run) { - response += `\n\n ⚠️ Production deployment initiated. Monitor closely.`; - } - - return { - content: [{ type: "text" as const, text: response }], - }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { - content: [{ type: "text" as const, text: `❌ Error deploying: ${msg}` }], - isError: true, - }; - } - }, - ); -} diff --git a/goosefactory/packages/mcp-server/src/tools/pipelines.ts b/goosefactory/packages/mcp-server/src/tools/pipelines.ts deleted file mode 100644 index cde501a..0000000 --- a/goosefactory/packages/mcp-server/src/tools/pipelines.ts +++ /dev/null @@ -1,350 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Pipeline Tools: status, advance, create, search -// ═══════════════════════════════════════════════════════════ - -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { z } from "zod"; -import * as api from "../api-client.js"; -import type { Pipeline, Priority, PipelineStatus } from "../types.js"; - -const STAGE_ORDER = [ - "intake", - "scaffolding", - "building", - "testing", - "review", - "staging", - "production", - "published", -] as const; - -function stageProgress(stage: string): number { - const idx = STAGE_ORDER.indexOf(stage as (typeof STAGE_ORDER)[number]); - return idx >= 0 ? Math.round(((idx + 1) / STAGE_ORDER.length) * 100) : 0; -} - -const priorityEmoji: Record = { - critical: "🔴", - high: "🟡", - medium: "🔵", - low: "⚪", -}; - -const statusEmoji: Record = { - active: "▶️", - paused: "⏸️", - completed: "✅", - failed: "❌", - archived: "📦", -}; - -function formatPipeline(p: Pipeline, detailed: boolean): string { - const progress = stageProgress(p.currentStage); - const bar = "█".repeat(Math.floor(progress / 10)) + "░".repeat(10 - Math.floor(progress / 10)); - - let out = `${statusEmoji[p.status]} **${p.name}** (${p.platform})`; - out += `\n ID: ${p.id}`; - out += `\n Stage: ${p.currentStage} [${bar}] ${progress}%`; - out += `\n Priority: ${priorityEmoji[p.priority]} ${p.priority}`; - out += `\n Status: ${p.status}`; - if (p.slaDeadline) { - out += `\n SLA: ${new Date(p.slaDeadline).toLocaleString()}`; - } - if (p.assigneeId) out += `\n Assignee: ${p.assigneeId}`; - - if (detailed) { - out += `\n Template: ${p.template}`; - out += `\n Created: ${new Date(p.createdAt).toLocaleString()}`; - out += `\n Updated: ${new Date(p.updatedAt).toLocaleString()}`; - if (p.completedAt) { - out += `\n Completed: ${new Date(p.completedAt).toLocaleString()}`; - } - } - - return out; -} - -export function registerPipelineTools(server: McpServer): void { - // ─────────────────────────────────────── - // factory_get_pipeline_status - // ─────────────────────────────────────── - server.tool( - "factory_get_pipeline_status", - "Get current state of all pipelines or a specific pipeline. " + - "Shows stage, progress, blockers, and timeline.", - { - pipeline_id: z - .string() - .uuid() - .optional() - .describe("Specific pipeline UUID. Omit for all active pipelines."), - status: z - .enum(["active", "paused", "completed", "failed", "all"]) - .default("active") - .describe("Filter by status. Default: active"), - include_details: z - .boolean() - .default(false) - .describe("Include tasks, assets, and stage history. Default: false"), - }, - async ({ pipeline_id, status, include_details }) => { - try { - if (pipeline_id) { - const pipeline = await api.getPipeline(pipeline_id); - let response = formatPipeline(pipeline, true); - - if (include_details) { - try { - const stages = await api.getPipelineStages(pipeline_id); - response += `\n\n 📋 Stage History:`; - for (const s of stages) { - const emoji = - s.status === "completed" - ? "✅" - : s.status === "active" - ? "🔄" - : s.status === "failed" - ? "❌" - : "⏳"; - response += `\n ${emoji} ${s.stageName} — ${s.status}`; - if (s.requiresApproval) response += " ★"; - if (s.durationSeconds) response += ` (${Math.round(s.durationSeconds / 60)}min)`; - } - } catch { - // stage data unavailable - } - - try { - const tasks = await api.listTasks({ pipelineId: pipeline_id, limit: 10 }); - if (tasks.length > 0) { - response += `\n\n 📥 Tasks (${tasks.length}):`; - for (const t of tasks) { - response += `\n ${priorityEmoji[t.priority]} ${t.title} [${t.status}]`; - } - } - } catch { - // task data unavailable - } - } - - return { - content: [{ type: "text" as const, text: response }], - }; - } - - // List all pipelines - const pipelines = await api.listPipelines({ - status: status === "all" ? undefined : (status as PipelineStatus), - sort: "-priority", - }); - - if (pipelines.length === 0) { - return { - content: [ - { - type: "text" as const, - text: `No ${status === "all" ? "" : status + " "}pipelines found.`, - }, - ], - }; - } - - const byStage: Record = {}; - for (const p of pipelines) { - if (!byStage[p.currentStage]) byStage[p.currentStage] = []; - byStage[p.currentStage].push(p); - } - - let response = `📊 **Pipeline Status** (${pipelines.length} ${status === "all" ? "total" : status})`; - response += "\n" + "─".repeat(60); - - for (const stage of STAGE_ORDER) { - const ps = byStage[stage]; - if (!ps) continue; - response += `\n\n**${stage.toUpperCase()}** (${ps.length})`; - for (const p of ps) { - response += `\n${formatPipeline(p, include_details)}`; - } - } - - return { - content: [{ type: "text" as const, text: response }], - }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { - content: [{ type: "text" as const, text: `❌ Error fetching pipeline status: ${msg}` }], - isError: true, - }; - } - }, - ); - - // ─────────────────────────────────────── - // factory_advance_stage - // ─────────────────────────────────────── - server.tool( - "factory_advance_stage", - "Manually advance a pipeline to its next stage. Triggers validation checks. " + - "If validation fails, returns the failures instead of advancing.", - { - pipeline_id: z.string().uuid().describe("UUID of the pipeline to advance"), - target_stage: z - .enum(["intake", "scaffolding", "building", "testing", "review", "staging", "production", "published"]) - .optional() - .describe("Target stage. Default: next in sequence"), - skip_validation: z - .boolean() - .default(false) - .describe("Skip gate checks. Admin/owner only. Default: false"), - notes: z.string().optional().describe("Notes for the stage transition"), - }, - async ({ pipeline_id, target_stage, skip_validation, notes }) => { - try { - const result = await api.advanceStage(pipeline_id, { - targetStage: target_stage, - skipValidation: skip_validation, - notes, - }); - - let response = `🔄 **Stage Advanced**`; - response += `\n New Stage: ${result.stage.stageName}`; - response += `\n Status: ${result.stage.status}`; - if (notes) response += `\n Notes: ${notes}`; - - if (result.tasksCreated.length > 0) { - response += `\n\n 📥 ${result.tasksCreated.length} new task(s) created:`; - for (const t of result.tasksCreated) { - response += `\n • ${t.title} [${t.priority}]`; - } - } - - return { - content: [{ type: "text" as const, text: response }], - }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { - content: [{ type: "text" as const, text: `❌ Error advancing stage: ${msg}` }], - isError: true, - }; - } - }, - ); - - // ─────────────────────────────────────── - // factory_create_pipeline - // ─────────────────────────────────────── - server.tool( - "factory_create_pipeline", - "Initialize a new MCP server pipeline from a template.", - { - name: z.string().min(1).describe("Pipeline name (e.g., 'ghl-mcp-server')"), - platform: z.string().min(1).describe("Target platform (e.g., 'go-high-level', 'shopify')"), - template: z - .enum(["mcp-server-standard", "mcp-server-minimal", "mcp-server-enterprise"]) - .default("mcp-server-standard") - .describe("Pipeline template. Default: mcp-server-standard"), - priority: z - .enum(["critical", "high", "medium", "low"]) - .default("medium") - .describe("Initial priority. Default: medium"), - }, - async ({ name, platform, template, priority }) => { - try { - const pipeline = await api.createPipeline({ - name, - platform, - template, - priority: priority as Priority, - }); - - let response = `🆕 **Pipeline Created**`; - response += `\n${formatPipeline(pipeline, true)}`; - response += `\n\n The pipeline starts at the "intake" stage and will auto-advance through scaffolding.`; - - return { - content: [{ type: "text" as const, text: response }], - }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { - content: [{ type: "text" as const, text: `❌ Error creating pipeline: ${msg}` }], - isError: true, - }; - } - }, - ); - - // ─────────────────────────────────────── - // factory_search - // ─────────────────────────────────────── - server.tool( - "factory_search", - "Search across pipelines, tasks, assets, and audit logs.", - { - query: z.string().min(1).describe("Search query text"), - entity_types: z - .array(z.enum(["pipeline", "task", "asset", "audit"])) - .optional() - .describe("Which entity types to search. Default: all"), - limit: z.number().min(1).max(50).default(10).describe("Max results. Default: 10"), - }, - async ({ query, entity_types, limit }) => { - try { - const results = await api.search(query, entity_types, limit); - - const total = - results.pipelines.length + - results.tasks.length + - results.assets.length + - results.auditEntries.length; - - if (total === 0) { - return { - content: [ - { - type: "text" as const, - text: `🔍 No results found for "${query}".`, - }, - ], - }; - } - - let response = `🔍 **Search Results for "${query}"** (${total} matches)`; - response += "\n" + "─".repeat(60); - - if (results.pipelines.length > 0) { - response += `\n\n**Pipelines (${results.pipelines.length})**`; - for (const p of results.pipelines) { - response += `\n ${statusEmoji[p.status]} ${p.name} — ${p.currentStage} [${p.priority}]`; - } - } - - if (results.tasks.length > 0) { - response += `\n\n**Tasks (${results.tasks.length})**`; - for (const t of results.tasks) { - response += `\n ${priorityEmoji[t.priority]} ${t.title} [${t.status}]`; - } - } - - if (results.auditEntries.length > 0) { - response += `\n\n**Audit Log (${results.auditEntries.length})**`; - for (const a of results.auditEntries) { - response += `\n ${a.action} on ${a.entityType}:${a.entityId}`; - } - } - - return { - content: [{ type: "text" as const, text: response }], - }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { - content: [{ type: "text" as const, text: `❌ Error searching: ${msg}` }], - isError: true, - }; - } - }, - ); -} diff --git a/goosefactory/packages/mcp-server/src/tools/review.ts b/goosefactory/packages/mcp-server/src/tools/review.ts deleted file mode 100644 index 63736a4..0000000 --- a/goosefactory/packages/mcp-server/src/tools/review.ts +++ /dev/null @@ -1,122 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Review Tool: factory_request_review — MCP App modal trigger -// ═══════════════════════════════════════════════════════════ - -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { z } from "zod"; -import * as api from "../api-client.js"; -import { buildReviewModalHtml } from "../modals/host.js"; - -export function registerReviewTools(server: McpServer): void { - // ─────────────────────────────────────── - // factory_request_review — serves an MCP App UI - // ─────────────────────────────────────── - server.tool( - "factory_request_review", - "Open an interactive review modal for an MCP server pipeline. " + - "Renders a rich HTML UI for human-in-the-loop feedback with approve/reject buttons, " + - "score dimensions, and contextual information.", - { - pipeline_id: z.string().uuid().describe("UUID of the pipeline to review"), - modal_type: z - .enum([ - "traffic-light", - "tinder-swipe", - "report-card", - "thermometer", - "spotlight", - "ranking-arena", - "speed-round", - "emoji-scale", - "before-after", - "priority-poker", - "mission-briefing", - "judges-scorecard", - "quick-pulse", - "decision-tree", - "hot-take", - "side-by-side", - "checklist-ceremony", - "confidence-meter", - "voice-of-customer", - "retrospective-board", - "slot-machine", - "mood-ring", - "war-room", - "applause-meter", - "crystal-ball", - ]) - .default("traffic-light") - .describe("Type of review modal to show. Default: traffic-light"), - display_type: z - .enum(["inline", "sidecar"]) - .default("sidecar") - .describe("How to display: inline in chat or sidecar panel. Default: sidecar"), - }, - async ({ pipeline_id, modal_type, display_type }) => { - try { - // Fetch pipeline context for the modal - const pipeline = await api.getPipeline(pipeline_id); - - let testResults = undefined; - let stages = undefined; - try { - stages = await api.getPipelineStages(pipeline_id); - } catch { /* optional */ } - - // Build the modal context - const context = { - modalType: modal_type, - modalVersion: "1.0.0", - sessionId: crypto.randomUUID(), - pipelineId: pipeline.id, - pipelineName: pipeline.name, - itemId: pipeline.id, - itemName: pipeline.name, - theme: "dark" as const, - accentColor: "#4488FF", - }; - - // Generate the HTML for the modal - const htmlContent = buildReviewModalHtml(context, pipeline, modal_type); - - return { - content: [ - { - type: "text" as const, - text: `🔍 Opening ${modal_type} review for **${pipeline.name}**...`, - }, - { - type: "resource" as const, - resource: { - uri: `ui://factory-review-${pipeline.id}`, - mimeType: "text/html" as const, - text: htmlContent, - }, - }, - ], - _meta: { - goose: { - toolUI: { - displayType: display_type, - name: `Review: ${pipeline.name}`, - renderer: "mcp-ui", - }, - }, - }, - }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { - content: [ - { - type: "text" as const, - text: `❌ Error opening review modal: ${msg}`, - }, - ], - isError: true, - }; - } - }, - ); -} diff --git a/goosefactory/packages/mcp-server/src/tools/tasks.ts b/goosefactory/packages/mcp-server/src/tools/tasks.ts deleted file mode 100644 index e2d41a4..0000000 --- a/goosefactory/packages/mcp-server/src/tools/tasks.ts +++ /dev/null @@ -1,230 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Task Tools: get_pending, approve, reject -// ═══════════════════════════════════════════════════════════ - -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { z } from "zod"; -import * as api from "../api-client.js"; -import type { Task, Priority } from "../types.js"; - -function formatTask(task: Task, includeContext: boolean): string { - const slaInfo = task.slaBreached - ? "🚨 SLA BREACHED" - : task.slaDeadline - ? `⏰ Due: ${new Date(task.slaDeadline).toLocaleString()}` - : ""; - - const priorityEmoji: Record = { - critical: "🔴", - high: "🟡", - medium: "🔵", - low: "⚪", - }; - - let output = `${priorityEmoji[task.priority]} [${task.priority.toUpperCase()}] ${task.title}`; - output += `\n ID: ${task.id}`; - output += `\n Type: ${task.type} | Status: ${task.status}`; - if (task.pipelineId) output += `\n Pipeline: ${task.pipelineId}`; - if (task.stageName) output += `\n Stage: ${task.stageName}`; - if (slaInfo) output += `\n ${slaInfo}`; - if (task.assigneeId) output += `\n Assigned to: ${task.assigneeId}`; - if (task.blocksStageAdvance) output += `\n ⛔ Blocks stage advance`; - - if (includeContext && task.context) { - output += `\n Summary: ${task.context.summary}`; - if (task.context.testResults) { - const tr = task.context.testResults; - output += `\n Tests: ${tr.passed}/${tr.total} passed (${tr.coveragePercent}% coverage)`; - if (tr.failureDetails?.length) { - output += `\n Failures:`; - for (const f of tr.failureDetails.slice(0, 5)) { - output += `\n - ${f.testName}: ${f.error}`; - } - } - } - if (task.context.diffSummary) { - const d = task.context.diffSummary; - output += `\n Diff: ${d.filesChanged} files, +${d.insertions}/-${d.deletions}`; - } - } - - return output; -} - -export function registerTaskTools(server: McpServer): void { - // ─────────────────────────────────────── - // factory_get_pending_tasks - // ─────────────────────────────────────── - server.tool( - "factory_get_pending_tasks", - "Get all tasks requiring human attention, sorted by priority and SLA urgency. " + - "This is the operator's decision inbox — the most important view in the factory.", - { - pipeline_id: z.string().uuid().optional().describe("Filter tasks to a specific pipeline (UUID)"), - priority: z - .enum(["critical", "high", "medium", "low"]) - .optional() - .describe("Filter by priority level"), - assignee: z.string().optional().describe("Filter by assignee. Use 'me' for current user."), - include_context: z - .boolean() - .default(true) - .describe("Include full task context (diffs, test results). Default: true"), - limit: z.number().min(1).max(100).default(20).describe("Maximum tasks to return. Default: 20"), - }, - async ({ pipeline_id, priority, assignee, include_context, limit }) => { - try { - const tasks = await api.listTasks({ - status: "pending", - pipelineId: pipeline_id, - priority: priority as Priority | undefined, - assigneeId: assignee, - limit, - sort: "-priority,-sla_deadline", - }); - - if (tasks.length === 0) { - return { - content: [ - { - type: "text" as const, - text: "✅ No pending tasks! The factory is running smoothly.", - }, - ], - }; - } - - const slaBreached = tasks.filter((t) => t.slaBreached).length; - const critical = tasks.filter((t) => t.priority === "critical").length; - - let header = `📥 **${tasks.length} Pending Tasks**`; - if (slaBreached > 0) header += ` | 🚨 ${slaBreached} SLA breached`; - if (critical > 0) header += ` | 🔴 ${critical} critical`; - header += "\n" + "─".repeat(60); - - const taskList = tasks - .map((t, i) => `\n${i + 1}. ${formatTask(t, include_context)}`) - .join("\n"); - - return { - content: [ - { - type: "text" as const, - text: header + taskList, - }, - ], - }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { - content: [{ type: "text" as const, text: `❌ Error fetching pending tasks: ${msg}` }], - isError: true, - }; - } - }, - ); - - // ─────────────────────────────────────── - // factory_approve_task - // ─────────────────────────────────────── - server.tool( - "factory_approve_task", - "Approve a pending task or approval gate, allowing the pipeline to proceed. " + - "Use this when the operator says 'approve', 'LGTM', 'ship it', etc.", - { - task_id: z.string().uuid().describe("UUID of the task to approve"), - notes: z.string().optional().describe("Optional approval notes or conditions"), - conditions: z - .array(z.string()) - .optional() - .describe("Conditions that must be met post-approval"), - }, - async ({ task_id, notes, conditions }) => { - try { - const task = await api.completeTask(task_id, { - decision: "approved", - notes, - decisionData: conditions ? { conditions } : {}, - }); - - let response = `✅ **Task Approved**`; - response += `\n Task: ${task.title}`; - response += `\n ID: ${task.id}`; - if (notes) response += `\n Notes: ${notes}`; - if (conditions?.length) { - response += `\n Conditions:`; - conditions.forEach((c) => (response += `\n • ${c}`)); - } - if (task.pipelineId) { - response += `\n\n🔄 Pipeline ${task.pipelineId} can now advance.`; - } - - return { - content: [{ type: "text" as const, text: response }], - }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { - content: [{ type: "text" as const, text: `❌ Error approving task: ${msg}` }], - isError: true, - }; - } - }, - ); - - // ─────────────────────────────────────── - // factory_reject_task - // ─────────────────────────────────────── - server.tool( - "factory_reject_task", - "Reject a pending task with feedback. Sends pipeline back for rework. " + - "Use when the operator says 'reject', 'needs work', 'fix this', etc.", - { - task_id: z.string().uuid().describe("UUID of the task to reject"), - reason: z.string().describe("Why this was rejected — required for feedback loop"), - requested_changes: z - .array(z.string()) - .optional() - .describe("Specific changes needed before re-submission"), - severity: z - .enum(["minor", "major", "critical"]) - .default("major") - .describe("How serious the issues are. Default: major"), - }, - async ({ task_id, reason, requested_changes, severity }) => { - try { - const task = await api.completeTask(task_id, { - decision: "rejected", - notes: reason, - decisionData: { - severity, - requestedChanges: requested_changes || [], - }, - }); - - let response = `❌ **Task Rejected**`; - response += `\n Task: ${task.title}`; - response += `\n ID: ${task.id}`; - response += `\n Severity: ${severity}`; - response += `\n Reason: ${reason}`; - if (requested_changes?.length) { - response += `\n Requested Changes:`; - requested_changes.forEach((c) => (response += `\n • ${c}`)); - } - if (task.pipelineId) { - response += `\n\n🔙 Pipeline ${task.pipelineId} sent back for rework.`; - } - - return { - content: [{ type: "text" as const, text: response }], - }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { - content: [{ type: "text" as const, text: `❌ Error rejecting task: ${msg}` }], - isError: true, - }; - } - }, - ); -} diff --git a/goosefactory/packages/mcp-server/src/types.ts b/goosefactory/packages/mcp-server/src/types.ts deleted file mode 100644 index e39b886..0000000 --- a/goosefactory/packages/mcp-server/src/types.ts +++ /dev/null @@ -1,331 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// GooseFactory Shared Types — from CONTRACTS.md -// ═══════════════════════════════════════════════════════════ - -// --- Enums & Literals --- - -export type PipelineStatus = "active" | "paused" | "completed" | "failed" | "archived"; -export type Priority = "critical" | "high" | "medium" | "low"; -export type PipelineTemplate = "mcp-server-standard" | "mcp-server-minimal" | "mcp-server-enterprise"; - -export type PipelineStage = - | "intake" - | "scaffolding" - | "building" - | "testing" - | "review" - | "staging" - | "production" - | "published"; - -export type TaskType = "approval" | "review" | "decision" | "manual_action" | "fix_required"; -export type TaskStatus = "pending" | "claimed" | "in_progress" | "completed" | "expired" | "escalated"; -export type TaskDecision = "approved" | "rejected" | "deferred" | "escalated"; - -export type ApprovalType = "stage_gate" | "deploy" | "code_review" | "manual_check"; -export type ApprovalStatus = "pending" | "approved" | "rejected" | "expired"; - -export type AgentType = "ai_builder" | "test_runner" | "deployer" | "monitor"; -export type AgentStatus = "idle" | "active" | "error" | "offline"; - -export type AssetType = "code" | "config" | "docs" | "build" | "test_report" | "screenshot"; -export type StageStatus = "pending" | "active" | "completed" | "skipped" | "failed"; - -// --- Core Entities --- - -export interface Pipeline { - id: string; - name: string; - slug: string; - template: PipelineTemplate; - platform: string; - currentStage: PipelineStage; - status: PipelineStatus; - priority: Priority; - createdBy: string; - assigneeId: string | null; - config: Record; - metadata: Record; - slaDeadline: string | null; - startedAt: string; - completedAt: string | null; - createdAt: string; - updatedAt: string; -} - -export interface PipelineStageRecord { - id: string; - pipelineId: string; - stageName: PipelineStage; - stageOrder: number; - status: StageStatus; - requiresApproval: boolean; - approvalType: "manual" | "auto" | "conditional"; - autoAdvance: boolean; - validationRules: ValidationRule[]; - enteredAt: string | null; - completedAt: string | null; - durationSeconds: number | null; - createdAt: string; -} - -export interface ValidationRule { - type: "test_coverage" | "tests_passing" | "required_asset" | "approval_count" | "custom"; - config: Record; -} - -export interface Task { - id: string; - pipelineId: string | null; - stageName: PipelineStage | null; - type: TaskType; - title: string; - description: string | null; - context: TaskContext; - status: TaskStatus; - priority: Priority; - assigneeId: string | null; - claimedAt: string | null; - claimedBy: string | null; - decision: TaskDecision | null; - decisionNotes: string | null; - decisionData: Record; - decidedAt: string | null; - decidedBy: string | null; - slaDeadline: string | null; - slaWarningsSent: number; - slaBreached: boolean; - escalationLevel: number; - blocksStageAdvance: boolean; - blocksPipelineId: string | null; - createdAt: string; - updatedAt: string; -} - -export interface TaskContext { - summary: string; - details?: string; - testResults?: TestResults; - diffSummary?: DiffSummary; - metrics?: Record; - relatedAssets?: string[]; - modalType?: string; - modalConfig?: Record; -} - -export interface TestResults { - total: number; - passed: number; - failed: number; - skipped: number; - coveragePercent: number; - failureDetails?: TestFailure[]; -} - -export interface TestFailure { - testName: string; - error: string; - file?: string; - line?: number; -} - -export interface DiffSummary { - filesChanged: number; - insertions: number; - deletions: number; - files: DiffFile[]; -} - -export interface DiffFile { - path: string; - status: "added" | "modified" | "deleted" | "renamed"; - insertions: number; - deletions: number; - patch?: string; -} - -export interface Approval { - id: string; - taskId: string; - pipelineId: string; - stageName: PipelineStage; - type: ApprovalType; - status: ApprovalStatus; - approvedBy: string | null; - approvedAt: string | null; - rejectionReason: string | null; - conditions: string[]; - requiredApprovers: number; - currentApprovers: number; - createdAt: string; -} - -export interface Agent { - id: string; - name: string; - type: AgentType; - status: AgentStatus; - currentTaskId: string | null; - currentPipelineId: string | null; - health: AgentHealth; - capabilities: string[]; - config: Record; - tasksCompletedTotal: number; - avgTaskDurationSeconds: number | null; - createdAt: string; - updatedAt: string; -} - -export interface AgentHealth { - uptimeSeconds: number; - tasksCompleted24h: number; - errorRate: number; - lastHeartbeat: string; -} - -export interface Asset { - id: string; - pipelineId: string; - stageName: PipelineStage | null; - type: AssetType; - name: string; - path: string | null; - storageKey: string; - storageBucket: string | null; - sizeBytes: number; - contentType: string; - checksumSha256: string; - version: number; - previousVersionId: string | null; - generatedBy: string; - generatorType: "user" | "agent" | "ci"; - metadata: Record; - createdAt: string; -} - -export interface AuditEntry { - id: string; - actorType: "user" | "agent" | "system" | "webhook"; - actorId: string | null; - actorName: string | null; - action: string; - entityType: string; - entityId: string; - changes: Record; - metadata: Record; - createdAt: string; -} - -export interface TaskStats { - pending: number; - inProgress: number; - blocked: number; - avgWaitTimeMinutes: number; - slaBreaches: number; - byPriority: Record; - byType: Record; -} - -export interface DashboardSummary { - pipelines: { - active: number; - paused: number; - completed: number; - failed: number; - }; - tasks: TaskStats; - agents: { - total: number; - active: number; - idle: number; - error: number; - offline: number; - }; - recentActivity: AuditEntry[]; - slaStatus: { - onTrack: number; - warning: number; - breached: number; - }; -} - -export interface PipelineTemplateConfig { - id: PipelineTemplate; - name: string; - description: string; - stages: PipelineStage[]; - defaultConfig: Record; - requiredAssets: AssetType[]; - validationRules: ValidationRule[]; -} - -export interface PipelineDetailState { - pipeline: Pipeline; - stages: PipelineStageRecord[]; - currentTasks: Task[]; - recentAssets: Asset[]; - blockers: BlockerInfo[]; - timeline: AuditEntry[]; -} - -export interface ServerStatus { - name: string; - pipelineId: string; - stage: PipelineStage; - health: "healthy" | "degraded" | "failing" | "unknown"; - deployments: { - staging: DeploymentInfo | null; - production: DeploymentInfo | null; - }; - lastTestRun: TestResults | null; - metrics: Record; -} - -export interface DeploymentInfo { - version: string; - deployedAt: string; - url: string; - status: "running" | "stopped" | "failed"; -} - -export interface BlockerInfo { - blockerType: "task" | "stage" | "agent" | "dependency"; - entityId: string; - title: string; - pipelineId: string; - pipelineName: string; - priority: Priority; - hoursBlocked: number; - blockReason: string; - suggestedAction?: string; -} - -// --- API Response Wrappers --- - -export interface ApiResponse { - ok: true; - data: T; - meta?: { - requestId: string; - timestamp: string; - pagination?: { - page: number; - limit: number; - total: number; - totalPages: number; - hasNext: boolean; - hasPrev: boolean; - }; - }; -} - -export interface ApiError { - ok: false; - error: { - code: string; - message: string; - details?: Record; - field?: string; - requestId: string; - }; -} diff --git a/goosefactory/packages/mcp-server/tsconfig.json b/goosefactory/packages/mcp-server/tsconfig.json deleted file mode 100644 index ecdc890..0000000 --- a/goosefactory/packages/mcp-server/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "bundler", - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/goosefactory/packages/modals/README.md b/goosefactory/packages/modals/README.md deleted file mode 100644 index 3ba63fd..0000000 --- a/goosefactory/packages/modals/README.md +++ /dev/null @@ -1,146 +0,0 @@ -# 🏭 GooseFactory HITL Modals - -> Interactive MCP App modals for Human-in-the-Loop feedback collection. - -## What's Here - -5 production-ready, self-contained HTML modals designed for sandboxed iframes inside AI chat interfaces: - -| Modal | File | Use Case | Key Feature | -|---|---|---|---| -| 🚦 **Traffic Light** | `src/traffic-light.html` | Pass/Polish/Rework decisions | LED glow, tag pills, response time tracking | -| 📋 **Report Card** | `src/report-card.html` | Multi-dimension grading (A-F) | Bubble-fill animation, GPA auto-calc, grade change tracking | -| ⚡ **Speed Round** | `src/speed-round.html` | Batch pass/fail (timed) | 60s timer, streak counter, skip detection, full stats | -| ⚔️ **Side-by-Side Arena** | `src/side-by-side.html` | A/B comparison | Hover time tracking, per-dimension breakdown, winner celebration | -| 🌡️ **Thermometer** | `src/thermometer.html` | Subjective quality (0-100) | Draggable mercury, bg color transitions, particles, drag journey capture | - -## Quick Start - -```bash -# Serve the preview page -npx serve src/preview -p 3333 - -# Or serve all files for direct access -npx serve src -p 3334 -``` - -Then open `http://localhost:3333` for the interactive preview environment. - -## Architecture - -### Self-Contained -Each modal is a **single HTML file** with inline CSS and JS. No external dependencies. This is intentional — modals are served as MCP App resources inside sandboxed iframes. - -### Communication Protocol -Every modal follows the `CONTRACTS.md` §3 "Modal Data Contract": - -``` -Host → Modal: window.__FACTORY_CONTEXT__ (injected before load) -Modal → Host: window.parent.postMessage() -``` - -#### Message Types - -| Type | Direction | When | -|---|---|---| -| `factory_modal_ready` | Modal → Host | Modal loaded and interactive | -| `factory_modal_response` | Modal → Host | User submitted feedback | -| `factory_modal_close` | Modal → Host | Modal wants to close | -| `factory_modal_resize` | Modal → Host | Modal wants to change size | -| `factory_modal_error` | Modal → Host | Modal hit an error | - -### Hidden Behavioral Metrics -Every modal automatically captures: -- `timeToFirstInteractionMs` — How long before user engaged -- `timeToDecisionMs` — Total time from open to submit -- `totalInteractions` — Click/tap count -- `fieldsModified` — Which inputs were changed -- `deviceType` — Mobile vs desktop -- `viewportSize` — Screen dimensions - -Plus modal-specific metrics: -- **Traffic Light**: `responseTimeMs` (fast = high confidence) -- **Report Card**: `gradeChanges` (mind-change tracking) -- **Speed Round**: Per-item `timeMs`, streaks, speed curve -- **Side-by-Side**: `hoverTimeA/B` (attention distribution) -- **Thermometer**: `dragJourney` (full temperature path during drag) - -## File Structure - -``` -packages/modals/ -├── package.json -├── README.md ← You are here -├── src/ -│ ├── traffic-light.html ← 🚦 Pass/Polish/Rework -│ ├── report-card.html ← 📋 A-F grading -│ ├── speed-round.html ← ⚡ Timed batch review -│ ├── side-by-side.html ← ⚔️ A/B comparison -│ ├── thermometer.html ← 🌡️ Quality temperature -│ ├── shared/ -│ │ └── modal-utils.js ← Canonical utility functions -│ └── preview/ -│ └── index.html ← Dev preview page with mock context & console -``` - -## Design Principles - -1. **3-Second Rule** — Instantly obvious what to do -2. **Dopamine Design** — Every interaction feels satisfying -3. **Data Density** — Rich signal from every tap/drag/click -4. **Dark Mode First** — `#111` background, designed for chat panels -5. **Touch-Friendly** — 48px minimum tap targets -6. **400-600px Width** — Optimized for chat panel constraints - -## Integrating with GooseFactory - -### MCP App Host Usage - -```typescript -// In your MCP tool response: -const response = { - _meta: { - goose: { - toolUI: { - displayType: "inline", - name: "Traffic Light Review", - renderer: "mcp-ui", - }, - }, - }, - content: [{ - uri: "ui://factory-modal", - mimeType: "text/html", - text: trafficLightHtml, // The full HTML file content - }], -}; -``` - -### McpAppHost Component - -The host component (React/Desktop) should: -1. Create a sandboxed iframe with `allow-scripts allow-forms` -2. Inject `window.__FACTORY_CONTEXT__` before the modal HTML -3. Listen for `postMessage` events -4. Forward `factory_modal_response` to `POST /v1/feedback` -5. Handle timeouts gracefully - -See `CONTRACTS.md` §4.4 for the full `McpAppHost` component contract. - -## Preview Page - -The preview page at `src/preview/index.html` provides: -- Sidebar with all 5 modals -- 500px-wide iframe with simulated device chrome -- Mock context data (editable pipeline/item names) -- Real-time `postMessage` console showing all events -- Reload and new-window buttons for testing - -## Next Modals (Planned) - -The design doc (`design-hitl-modal-collection.md`) defines 25 total modals. Next batch: -- 🃏 Tinder Swipe — Batch card swiping -- 🎯 Spotlight — Code annotation with attention heatmap -- 🏆 Judge's Scorecard — Olympic-style weighted scoring -- 🎲 Priority Poker — Fibonacci estimation cards -- 🔫 Mission Briefing — High-stakes go/no-go with flight checks diff --git a/goosefactory/packages/modals/package.json b/goosefactory/packages/modals/package.json deleted file mode 100644 index c66d324..0000000 --- a/goosefactory/packages/modals/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "@goosefactory/modals", - "version": "1.0.0", - "description": "GooseFactory HITL Modal Collection — Interactive MCP App modals for human-in-the-loop feedback", - "private": true, - "scripts": { - "preview": "npx serve src/preview -p 3333 --cors", - "dev": "npx serve src -p 3334 --cors" - }, - "keywords": ["goosefactory", "hitl", "modals", "mcp-app"], - "license": "UNLICENSED" -} diff --git a/goosefactory/packages/modals/src/applause-meter.html b/goosefactory/packages/modals/src/applause-meter.html deleted file mode 100644 index 347c18f..0000000 --- a/goosefactory/packages/modals/src/applause-meter.html +++ /dev/null @@ -1,804 +0,0 @@ - - - - - -GooseFactory — Applause Meter - - - - -
-
Applauding
-

Loading…

-
- -
👏 APPLAUSE METER 👏
-
How much applause does this deserve?
- -
 
- -
-
-
-
-
-
-
- Golf clap - Nice - Standing O! - THUNDER -
-
- -
- -
 
-
 
-
- -
- -
- -
-
What deserves the applause?
-
- - - - - -
-
- -
- -
- - -
- -
-
👏
-
Applause recorded!
-
-
- - - - diff --git a/goosefactory/packages/modals/src/before-after.html b/goosefactory/packages/modals/src/before-after.html deleted file mode 100644 index 063ac7d..0000000 --- a/goosefactory/packages/modals/src/before-after.html +++ /dev/null @@ -1,711 +0,0 @@ - - - - - -GooseFactory — Before / After - - - - -
-
-
Comparing Versions
-

Loading…

-
-
- -
-
-
◀ Before
-
-
-
-
After ▶
-
-
-
-
-
-
- -
-
Which is better?
-
- - - -
-
- -
-
Quality delta
-
- −5 - - +5 -
-
0
-
- -
-
What changed?
-
- - - - - - -
-
- -
- -
- -
-
-
Comparison submitted
-
- - - - diff --git a/goosefactory/packages/modals/src/checklist-ceremony.html b/goosefactory/packages/modals/src/checklist-ceremony.html deleted file mode 100644 index b6bd171..0000000 --- a/goosefactory/packages/modals/src/checklist-ceremony.html +++ /dev/null @@ -1,649 +0,0 @@ - - - - - -GooseFactory — Checklist Ceremony - - - - -
-
Quality Checklist
-

Pre-Ship Ceremony

-
Loading…
-
- -
-
-
-
-
- 0 of 0 checked - 0% -
-
- -
- -
- -
- -
-
-

All Clear!

-
Every check passed
-
100%
-
- -
- - - - diff --git a/goosefactory/packages/modals/src/confidence-meter.html b/goosefactory/packages/modals/src/confidence-meter.html deleted file mode 100644 index 8e0b5d0..0000000 --- a/goosefactory/packages/modals/src/confidence-meter.html +++ /dev/null @@ -1,580 +0,0 @@ - - - - - -GooseFactory — Confidence Meter - - - - -
-
✅ You approved this
-

How confident are you?

-
Rate your confidence in the decision you just made
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- 🎲 Guessing - 🤔 Somewhat Sure - 💪 Pretty Confident - 💎 Dead Certain -
- -
-
0%
-
Drag the needle →
-
Drag anywhere on the gauge
-
- -
-
Delegation Calibration
- - - - -
- -
- -
- -
-
📊
-
Confidence calibrated!
-
- - - - diff --git a/goosefactory/packages/modals/src/crystal-ball.html b/goosefactory/packages/modals/src/crystal-ball.html deleted file mode 100644 index 49692a1..0000000 --- a/goosefactory/packages/modals/src/crystal-ball.html +++ /dev/null @@ -1,828 +0,0 @@ - - - - - -GooseFactory — Crystal Ball - - - - -
🔮 Consult the Oracle 🔮
-
What future do you see?
- -
-
-
-
-
-
-
- -
Click each card to reveal its prophecy
- -
-
-
-
-
- -
- -
-
-
-
🌟
-
DESTINY
-
-
-
🌟
-
DESTINY
- - - -
-
-
- - -
-
-
-
-
TIMELINE
-
-
-
-
TIMELINE
- - - - -
-
-
- - -
-
-
-
-
WEAKNESS
-
-
-
-
WEAKNESS
- - - - -
-
-
-
- -
-
THE ORACLE SPEAKS
-
-
-
- -
- -
- -
-
🔮
-
Prophecy Sealed
-
The Oracle remembers…
-
- - - - diff --git a/goosefactory/packages/modals/src/decision-tree.html b/goosefactory/packages/modals/src/decision-tree.html deleted file mode 100644 index 9f5e6aa..0000000 --- a/goosefactory/packages/modals/src/decision-tree.html +++ /dev/null @@ -1,700 +0,0 @@ - - - - - -GooseFactory — Decision Tree - - - - -
-
Decision Tree
-

Navigate to Your Verdict

-
Loading…
-
- -
- -
- -
- -
- -
-
-
Decision path recorded
-
- - - - diff --git a/goosefactory/packages/modals/src/emoji-scale.html b/goosefactory/packages/modals/src/emoji-scale.html deleted file mode 100644 index fb2307c..0000000 --- a/goosefactory/packages/modals/src/emoji-scale.html +++ /dev/null @@ -1,511 +0,0 @@ - - - - - -GooseFactory — Emoji Scale - - - - -
-
How do you feel about this?
-

Rate This Deliverable

-
Loading…
-
- -
- -
-
Why?
-
-
- -
- -
- -
- -
- -
- -
-
-
Feedback recorded
-
- - - - diff --git a/goosefactory/packages/modals/src/hot-take.html b/goosefactory/packages/modals/src/hot-take.html deleted file mode 100644 index 3134e9e..0000000 --- a/goosefactory/packages/modals/src/hot-take.html +++ /dev/null @@ -1,561 +0,0 @@ - - - - - -GooseFactory — Hot Take - - - - -
-
- -
-
- 🔥 HOT TAKE 🔥 -
- -
-
- - - - -
- 30 - seconds -
-
-
- -
- Review Item -

Loading…

-

-
- -
- - - -
-
- -
-
-
Time's up — deferred
-
This one will come back around
-
- -
-
-
Decision sent
-
- - - - diff --git a/goosefactory/packages/modals/src/judges-scorecard.html b/goosefactory/packages/modals/src/judges-scorecard.html deleted file mode 100644 index 9756768..0000000 --- a/goosefactory/packages/modals/src/judges-scorecard.html +++ /dev/null @@ -1,691 +0,0 @@ - - - - - -GooseFactory — Judge's Scorecard - - - - -
-
Reviewing
-

Loading…

-

-
- -
-

🏅 Judge's Scorecard

-
Click each score to rate · 0.0 — 10.0 in 0.5 increments
-
- -
-
-
—.—
-
Functionality
-
×3
-
-
-
—.—
-
Code Quality
-
×2
-
-
-
—.—
-
Innovation
-
×1
-
-
-
—.—
-
Docs
-
×2
-
-
-
—.—
-
Performance
-
×2
-
-
- -
-
-
Rate all dimensions
-
FINAL SCORE
-
0.0
-
-
- -
- -
0/150
-
- -
- -
- -
-
🏅
-
Scorecard submitted
-
-
- - - - diff --git a/goosefactory/packages/modals/src/mission-briefing.html b/goosefactory/packages/modals/src/mission-briefing.html deleted file mode 100644 index df29f2e..0000000 --- a/goosefactory/packages/modals/src/mission-briefing.html +++ /dev/null @@ -1,580 +0,0 @@ - - - - - -GooseFactory — Mission Briefing - - - - -
-
TOP SECRET // GOOSEFACTORY
-

Mission Briefing

-
Loading…
-
DOC-000000 // PENDING
-
- -
- -
-
Operator Notes (optional)
- -
- -
- - -
- -
-
-
-
Decision logged • Briefing sealed
-
- - - - diff --git a/goosefactory/packages/modals/src/mood-ring.html b/goosefactory/packages/modals/src/mood-ring.html deleted file mode 100644 index e7cffd5..0000000 --- a/goosefactory/packages/modals/src/mood-ring.html +++ /dev/null @@ -1,637 +0,0 @@ - - - - - -GooseFactory — Mood Ring - - - - -
-
Reviewing
-

Loading…

-

-
- -
What color does this give you?
- -
-
-
-
-
🔮
-
Hover the ring
-
Click to lock your mood
-
-
-
-
-
- -
-
-
🔮
-
-
#000000
-
- -
- - -
- - - -
- -
-
- -
-
🔮
-
Mood captured
-
- - - - diff --git a/goosefactory/packages/modals/src/preview/index.html b/goosefactory/packages/modals/src/preview/index.html deleted file mode 100644 index 3f7cbb5..0000000 --- a/goosefactory/packages/modals/src/preview/index.html +++ /dev/null @@ -1,584 +0,0 @@ - - - - - -GooseFactory — Modal Preview - - - - -
- -
-

Modal Preview

-
GooseFactory HITL Modal Collection v2.0
-
-
25 modals
-
- -
- - -
-
- Current: None - - - -
- -
-
-
🎨
-

Select a Modal

-

Click any modal from the sidebar to preview it
25 interactive HITL modals available

-
- - -
- -
-
- 📡 postMessage Console - 0 - - -
-
-
-
-
- - - - diff --git a/goosefactory/packages/modals/src/priority-poker.html b/goosefactory/packages/modals/src/priority-poker.html deleted file mode 100644 index 907d15b..0000000 --- a/goosefactory/packages/modals/src/priority-poker.html +++ /dev/null @@ -1,607 +0,0 @@ - - - - - -GooseFactory — Priority Poker - - - - -
-
-
Estimate
-

Loading…

-
- -
- Play a card -
- -
- -
- -
- -
-
- -
- - - - - - - - - -
- -
-
🃏
-
Estimate dealt
-
- - - - diff --git a/goosefactory/packages/modals/src/quick-pulse.html b/goosefactory/packages/modals/src/quick-pulse.html deleted file mode 100644 index 67c2c37..0000000 --- a/goosefactory/packages/modals/src/quick-pulse.html +++ /dev/null @@ -1,476 +0,0 @@ - - - - - -GooseFactory — Quick Pulse - - - - -
-
Quick Pulse
-

Gut Check

-
Loading…
-
- -
50
-
Drag to rate
- -
-
- 😰 - 😕 - 😐 - 😊 - 🚀 -
-
-
🤔
-
-
- Yikes - Ship it -
-
- -
-
Auto-submitting in 2s…
-
-
-
-
- -
-
-
Pulse recorded
-
50
-
- - - - diff --git a/goosefactory/packages/modals/src/ranking-arena.html b/goosefactory/packages/modals/src/ranking-arena.html deleted file mode 100644 index 875f134..0000000 --- a/goosefactory/packages/modals/src/ranking-arena.html +++ /dev/null @@ -1,643 +0,0 @@ - - - - - -GooseFactory — Ranking Arena - - - - -
-

🏆 Ranking Arena

-
Drag to rank best → worst
-
Loading…
-
- -
- -
0 swaps made
- -
-
Final Ranking
-
    -
    - -
    - -
    - -
    -
    🏆
    -
    Ranking submitted!
    -
    - - - - diff --git a/goosefactory/packages/modals/src/report-card.html b/goosefactory/packages/modals/src/report-card.html deleted file mode 100644 index 563ff7a..0000000 --- a/goosefactory/packages/modals/src/report-card.html +++ /dev/null @@ -1,509 +0,0 @@ - - - - - -GooseFactory — Report Card - - - - -
    -
    -
    -
    GooseFactory Academy
    -
    MCP Server
    -
    -
    -
    -
    GPA
    -
    -
    - -
    -
    Subject Grades
    - -
    - -
    -
    Teacher's Comments
    - -
    - -
    -
    0 / 6 graded
    - -
    -
    - -
    -
    📋
    -
    Report Card Filed
    -
    4.0
    -
    - -
    - - - - diff --git a/goosefactory/packages/modals/src/retrospective-board.html b/goosefactory/packages/modals/src/retrospective-board.html deleted file mode 100644 index c746913..0000000 --- a/goosefactory/packages/modals/src/retrospective-board.html +++ /dev/null @@ -1,700 +0,0 @@ - - - - - -GooseFactory — Retrospective Board - - - - -
    -

    📋 Retrospective Board

    -
    What should the AI start, stop, and continue doing?
    -
    - -
    - -
    -
    - 🚀 START - 0 -
    -
    -
    - - -
    -
    - - -
    -
    - 🛑 STOP - 0 -
    -
    -
    - - -
    -
    - - -
    -
    - 💪 CONTINUE - 0 -
    -
    -
    - - -
    -
    -
    - -
    -
    0 start
    -
    0 stop
    -
    0 continue
    -
    - -
    - -
    - -
    -
    📋
    -
    Retro submitted!
    -
    - - - - diff --git a/goosefactory/packages/modals/src/shared/modal-utils.js b/goosefactory/packages/modals/src/shared/modal-utils.js deleted file mode 100644 index f3b3441..0000000 --- a/goosefactory/packages/modals/src/shared/modal-utils.js +++ /dev/null @@ -1,224 +0,0 @@ -/** - * GooseFactory Modal Utilities - * Shared helpers for all HITL modals — context reading, postMessage, timing, metrics. - * - * Usage: Include inline in each modal HTML file via copy, or reference for consistency. - * Each modal is self-contained, so this serves as the canonical source. - */ - -const ModalUtils = (() => { - 'use strict'; - - // ═══════════════════════════════════════ - // CONTEXT - // ═══════════════════════════════════════ - - /** - * Read the injected factory context. - * Falls back to URL params and defaults for dev/preview mode. - */ - function getContext() { - const ctx = window.__FACTORY_CONTEXT__ || {}; - return { - modalType: ctx.modalType || getParam('modal_type') || 'unknown', - modalVersion: ctx.modalVersion || '1.0.0', - sessionId: ctx.sessionId || generateId(), - pipelineId: ctx.pipelineId || getParam('pipeline_id') || 'demo_pipe', - pipelineName: ctx.pipelineName || 'Demo Pipeline', - itemId: ctx.itemId || getParam('item_id') || 'demo_item', - itemName: ctx.itemName || 'Demo Item', - deliverablePreview: ctx.deliverablePreview || '', - deliverableContent: ctx.deliverableContent || '', - contentBefore: ctx.contentBefore || '', - contentAfter: ctx.contentAfter || '', - testResults: ctx.testResults || null, - diffSummary: ctx.diffSummary || null, - metrics: ctx.metrics || {}, - items: ctx.items || [], - dimensions: ctx.dimensions || [], - checklistItems: ctx.checklistItems || [], - timerSeconds: ctx.timerSeconds || 60, - theme: ctx.theme || 'dark', - accentColor: ctx.accentColor || '#00AAFF', - }; - } - - function getParam(name) { - try { - return new URLSearchParams(window.location.search).get(name); - } catch { return null; } - } - - function generateId() { - return 'sess_' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36); - } - - // ═══════════════════════════════════════ - // POST MESSAGE HELPERS - // ═══════════════════════════════════════ - - function postToHost(message) { - try { - window.parent.postMessage(message, '*'); - } catch (e) { - console.warn('[ModalUtils] postMessage failed:', e); - } - } - - function signalReady(modalType, version) { - postToHost({ - type: 'factory_modal_ready', - modalType: modalType || 'unknown', - version: version || '1.0.0', - }); - } - - function signalClose(reason) { - postToHost({ - type: 'factory_modal_close', - reason: reason || 'cancelled', - }); - } - - function signalResize(height, width) { - const msg = { type: 'factory_modal_resize', height }; - if (width) msg.width = width; - postToHost(msg); - } - - function signalError(error, details) { - postToHost({ - type: 'factory_modal_error', - error: String(error), - details: details || null, - }); - } - - /** - * Submit the modal response — the main data payload. - * @param {object} opts - * @param {string} opts.modalType - * @param {string} opts.modalVersion - * @param {object} opts.ctx - The context object from getContext() - * @param {object} opts.feedback - FeedbackPayload - * @param {object} opts.metrics - Extra ModalMetrics (merged with auto-collected) - * @param {number} opts.startTime - Performance.now() or Date.now() when modal opened - */ - function submitResponse(opts) { - const now = Date.now(); - const ctx = opts.ctx || getContext(); - const autoMetrics = collectAutoMetrics(opts.startTime); - - postToHost({ - type: 'factory_modal_response', - modalType: opts.modalType || ctx.modalType, - modalVersion: opts.modalVersion || ctx.modalVersion || '1.0.0', - pipelineId: ctx.pipelineId, - itemId: ctx.itemId, - sessionId: ctx.sessionId, - timestamp: new Date(now).toISOString(), - responseTimeMs: now - (opts.startTime || now), - feedback: opts.feedback || {}, - meta: Object.assign({}, autoMetrics, opts.metrics || {}), - }); - } - - // ═══════════════════════════════════════ - // TIMING & METRICS - // ═══════════════════════════════════════ - - let _interactions = 0; - let _firstInteractionTime = null; - let _fieldsModified = new Set(); - let _modalOpenTime = Date.now(); - - /** Call once on DOMContentLoaded to start auto-tracking */ - function initMetrics() { - _modalOpenTime = Date.now(); - _interactions = 0; - _firstInteractionTime = null; - _fieldsModified = new Set(); - - document.addEventListener('click', _trackInteraction, true); - document.addEventListener('touchend', _trackInteraction, true); - document.addEventListener('input', _trackInput, true); - } - - function _trackInteraction() { - _interactions++; - if (!_firstInteractionTime) _firstInteractionTime = Date.now(); - } - - function _trackInput(e) { - _trackInteraction(); - if (e.target && (e.target.name || e.target.id || e.target.dataset.field)) { - _fieldsModified.add(e.target.name || e.target.id || e.target.dataset.field); - } - } - - function trackFieldModified(fieldName) { - _fieldsModified.add(fieldName); - } - - function collectAutoMetrics(startTime) { - const now = Date.now(); - const start = startTime || _modalOpenTime; - return { - timeToFirstInteractionMs: _firstInteractionTime ? _firstInteractionTime - start : null, - timeToDecisionMs: now - start, - fieldsModified: Array.from(_fieldsModified), - totalInteractions: _interactions, - revisits: 0, - viewportSize: { - width: window.innerWidth, - height: window.innerHeight, - }, - deviceType: window.innerWidth < 768 ? 'mobile' : 'desktop', - }; - } - - // ═══════════════════════════════════════ - // ANIMATION HELPERS - // ═══════════════════════════════════════ - - /** Apply a CSS class temporarily for animation, then remove it */ - function flashClass(element, className, durationMs) { - if (!element) return; - element.classList.add(className); - setTimeout(() => element.classList.remove(className), durationMs || 500); - } - - /** Simple eased lerp for smooth value transitions */ - function animateValue(from, to, durationMs, onUpdate, onDone) { - const start = performance.now(); - function tick(now) { - const elapsed = now - start; - const t = Math.min(elapsed / durationMs, 1); - const eased = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; // easeInOut - onUpdate(from + (to - from) * eased); - if (t < 1) requestAnimationFrame(tick); - else if (onDone) onDone(); - } - requestAnimationFrame(tick); - } - - // ═══════════════════════════════════════ - // PUBLIC API - // ═══════════════════════════════════════ - - return { - getContext, - postToHost, - signalReady, - signalClose, - signalResize, - signalError, - submitResponse, - initMetrics, - trackFieldModified, - collectAutoMetrics, - flashClass, - animateValue, - generateId, - }; -})(); diff --git a/goosefactory/packages/modals/src/side-by-side.html b/goosefactory/packages/modals/src/side-by-side.html deleted file mode 100644 index cd3c14e..0000000 --- a/goosefactory/packages/modals/src/side-by-side.html +++ /dev/null @@ -1,637 +0,0 @@ - - - - - -GooseFactory — Side-by-Side Arena - - - - -
    -
    Compare & Choose
    -
    Which approach is better?
    -
    - -
    -
    -
    🔵 Option A
    -
    Approach A
    -
    Loading content…
    -
    -
    - -
    -
    VS
    -
    - -
    -
    🟠 Option B
    -
    Approach B
    -
    Loading content…
    -
    -
    -
    - -
    - -
    -
    - -
    -
    And the winner is…
    -
    - - - -
    -
    - -
    -
    - - - - - - -
    - -
    - -
    - -
    - -
    -
    🏆
    -
    Verdict submitted
    -
    - - - - diff --git a/goosefactory/packages/modals/src/slot-machine.html b/goosefactory/packages/modals/src/slot-machine.html deleted file mode 100644 index f21edac..0000000 --- a/goosefactory/packages/modals/src/slot-machine.html +++ /dev/null @@ -1,641 +0,0 @@ - - - - - -GooseFactory — Slot Machine - - - - -
    -
    Reviewing
    -

    Loading…

    -

    -
    - -
    -
    -
    🎰 Quality Slots 🎰
    -
    Jackpots: 0 / 0
    -
    - -
    -
    -
    - -
    -
    Quality
    -
    tap to cycle
    -
    -
    -
    - -
    -
    Complete
    -
    tap to cycle
    -
    -
    -
    - -
    -
    Ship?
    -
    tap to cycle
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    Set all reels to pull
    - -
    -
    -
    -
    - -
    -
    🎰
    -
    Review submitted
    -
    - - - - diff --git a/goosefactory/packages/modals/src/speed-round.html b/goosefactory/packages/modals/src/speed-round.html deleted file mode 100644 index b9f187c..0000000 --- a/goosefactory/packages/modals/src/speed-round.html +++ /dev/null @@ -1,658 +0,0 @@ - - - - - -GooseFactory — Speed Round - - - - -
    -
    ⚡ Speed Round
    -
    15 items — 60 seconds
    -
    3
    -
    - -
    - -
    - ⚡ Avg: s - 0/0 - 0s -
    - -
    -
    🔥 3x
    -
    -
    Item 1
    -

    Loading…

    -

    -
    - -
    - -
    - - -
    - -
    - -
    -
    ⚡ Speed Round Complete
    -
    -
    0
    Reviewed
    -
    0
    Passed
    -
    0
    Failed
    -
    0
    Skipped
    -
    -
    - -
    - - - - diff --git a/goosefactory/packages/modals/src/spotlight.html b/goosefactory/packages/modals/src/spotlight.html deleted file mode 100644 index b3051ce..0000000 --- a/goosefactory/packages/modals/src/spotlight.html +++ /dev/null @@ -1,697 +0,0 @@ - - - - - -GooseFactory — Spotlight Review - - - - -
    -
    - - -
    - -
    -
    -
    -
    -
    - - -
    -
    -
    - - - -
    -
    🔍
    -
    Review submitted
    -
    - - - - diff --git a/goosefactory/packages/modals/src/thermometer.html b/goosefactory/packages/modals/src/thermometer.html deleted file mode 100644 index 5486013..0000000 --- a/goosefactory/packages/modals/src/thermometer.html +++ /dev/null @@ -1,578 +0,0 @@ - - - - - -GooseFactory — Thermometer - - - - -
    -
    Quality Temperature
    -

    How Hot Is This?

    -
    Loading…
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    💀 20°
    -
    🥶 40°
    -
    😐 60°
    -
    😎 80°
    -
    🔥 100°
    -
    -
    - -
    -
    -
    50°
    -
    Lukewarm
    -
    - -
    -
    What's driving this?
    -
    -
    -
    -
    - -
    - -
    - -
    - -
    -
    🌡️
    -
    Temperature locked in
    -
    50°
    -
    - - - - diff --git a/goosefactory/packages/modals/src/tinder-swipe.html b/goosefactory/packages/modals/src/tinder-swipe.html deleted file mode 100644 index 2c82a3e..0000000 --- a/goosefactory/packages/modals/src/tinder-swipe.html +++ /dev/null @@ -1,739 +0,0 @@ - - - - - -GooseFactory — Tinder Swipe - - - - -
    - Batch Review -
    1 /
    -
    - -
    -
    -
    - -
    - -
    -
    - - Reject -
    -
    - - Love -
    -
    - - Approve -
    -
    - -
    -
    Review Complete
    -
    -
    -
    -
    0
    -
    Approved
    -
    -
    -
    -
    0
    -
    Rejected
    -
    -
    -
    -
    0
    -
    Loved
    -
    -
    -
    - -
    - -
    -
    -
    Decisions sent
    -
    - - - - diff --git a/goosefactory/packages/modals/src/traffic-light.html b/goosefactory/packages/modals/src/traffic-light.html deleted file mode 100644 index c169e6b..0000000 --- a/goosefactory/packages/modals/src/traffic-light.html +++ /dev/null @@ -1,552 +0,0 @@ - - - - - -GooseFactory — Traffic Light - - - - -
    -
    Reviewing
    -

    Loading…

    -

    -
    - -
    -
    -
    SHIP IT
    -
    -
    -
    POLISH
    -
    -
    -
    REWORK
    -
    -
    - -
    -
    What needs work?
    -
    - - - - - - - - -
    -
    - - - -
    - -
    - -
    -
    -
    Decision sent
    -
    - - - - diff --git a/goosefactory/packages/modals/src/voice-of-customer.html b/goosefactory/packages/modals/src/voice-of-customer.html deleted file mode 100644 index 2a74403..0000000 --- a/goosefactory/packages/modals/src/voice-of-customer.html +++ /dev/null @@ -1,567 +0,0 @@ - - - - - -GooseFactory — Voice of Customer - - - - -
    -
    👤
    -
    -

    Alex, Solo Developer

    -

    Building a weekend project. Just wants clear docs and working examples.

    -
    -
    - -
    -
    What Alex sees
    -

    Loading…

    -

    -
    - -
    - -
    - -
    - - - - - -
    -
    -
    - - -
    - -
    - - -
    -
    - - -
    - -
    - - - -
    -
    - - -
    - -
    - - -
    -
    - - - -
    - -
    -
    -
    Review posted
    -
    - - - - diff --git a/goosefactory/packages/modals/src/war-room.html b/goosefactory/packages/modals/src/war-room.html deleted file mode 100644 index ac5c8cb..0000000 --- a/goosefactory/packages/modals/src/war-room.html +++ /dev/null @@ -1,887 +0,0 @@ - - - - - -GooseFactory — War Room - - - - -
    - -
    -

    - 🏭 War Room - -

    - -
    - -
    - -
    -
    - -
    - - -
    -
    - - - - -
    -
    - - -
    - 0 selected - - - - - -
    - - -
    -
    🏭
    -
    Decisions submitted
    -
    -
    - - - - diff --git a/goosefactory/packages/shared/README.md b/goosefactory/packages/shared/README.md deleted file mode 100644 index 748d1e1..0000000 --- a/goosefactory/packages/shared/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# GooseFactory — Shared Package - -**Owner:** Architect (maintained by all agents) - -Canonical TypeScript types, constants, and Zod validation schemas. Every type in this package is derived from `CONTRACTS.md`. - -## Rules -1. **This package is the SINGLE source of TypeScript types.** -2. All other packages import from `@goosefactory/shared`. -3. If you need a new type that crosses package boundaries, add it HERE first. -4. Never define the same type in two packages. - -## Structure -``` -src/ -├── types/ -│ ├── pipeline.ts # Pipeline, PipelineStage, PipelineStatus -│ ├── task.ts # Task, TaskType, TaskStatus, TaskDecision -│ ├── approval.ts # Approval, ApprovalType, ApprovalStatus -│ ├── agent.ts # Agent, AgentType, AgentHealth -│ ├── asset.ts # Asset, AssetType -│ ├── feedback.ts # FeedbackEvent, FeedbackPayload, all feedback subtypes -│ ├── modal.ts # ModalMessage, ModalResponse, ModalMetrics -│ ├── api.ts # ApiResponse, ApiError, PaginationParams -│ ├── ws.ts # WsEvent, WsChannel, all event payloads -│ └── index.ts # Re-exports everything -├── constants/ -│ ├── stages.ts # KANBAN_STAGES, stage order, stage transitions -│ ├── errors.ts # ErrorCode values -│ └── sla.ts # Default SLA durations -└── schemas/ - ├── feedback.schema.ts # Zod schema for FeedbackEvent validation - ├── pipeline.schema.ts # Zod schema for Pipeline validation - └── task.schema.ts # Zod schema for Task validation -``` - -## Usage -```typescript -import type { Pipeline, Task, FeedbackEvent } from "@goosefactory/shared"; -import { KANBAN_STAGES, DEFAULT_SLA } from "@goosefactory/shared/constants"; -import { feedbackSchema } from "@goosefactory/shared/schemas"; -``` diff --git a/goosefactory/packages/shared/package.json b/goosefactory/packages/shared/package.json deleted file mode 100644 index fdbd82b..0000000 --- a/goosefactory/packages/shared/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@goosefactory/shared", - "version": "1.0.0", - "description": "GooseFactory shared types, constants, and Zod schemas — canonical source of truth from CONTRACTS.md", - "type": "module", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - }, - "./types": { - "types": "./dist/types/index.d.ts", - "import": "./dist/types/index.js" - }, - "./constants": { - "types": "./dist/constants/index.d.ts", - "import": "./dist/constants/index.js" - }, - "./schemas": { - "types": "./dist/schemas/index.d.ts", - "import": "./dist/schemas/index.js" - } - }, - "scripts": { - "build": "tsc", - "dev": "tsc --watch", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "zod": "^3.24.4" - }, - "devDependencies": { - "typescript": "^5.8.3" - } -} diff --git a/goosefactory/packages/shared/src/constants/errors.ts b/goosefactory/packages/shared/src/constants/errors.ts deleted file mode 100644 index db732a3..0000000 --- a/goosefactory/packages/shared/src/constants/errors.ts +++ /dev/null @@ -1,31 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Error Constants — from CONTRACTS.md §1.3, §7.1 -// ═══════════════════════════════════════════════════════════ - -import type { ErrorCode } from "../types/api.js"; - -/** Error code to HTTP status mapping */ -export const ERROR_STATUS_MAP: Record = { - BAD_REQUEST: 400, - VALIDATION_ERROR: 400, - UNAUTHORIZED: 401, - FORBIDDEN: 403, - NOT_FOUND: 404, - CONFLICT: 409, - GONE: 410, - RATE_LIMITED: 429, - INTERNAL_ERROR: 500, - SERVICE_UNAVAILABLE: 503, - TIMEOUT: 504, -}; - -/** Status codes that are safe to retry */ -export const RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504] as const; - -/** Default retry policy */ -export const DEFAULT_RETRY_POLICY = { - maxRetries: 3, - baseDelayMs: 1000, - maxDelayMs: 30000, - jitterMs: 500, -} as const; diff --git a/goosefactory/packages/shared/src/constants/index.ts b/goosefactory/packages/shared/src/constants/index.ts deleted file mode 100644 index f671c09..0000000 --- a/goosefactory/packages/shared/src/constants/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Constants Re-exports -// ═══════════════════════════════════════════════════════════ - -export { - KANBAN_STAGES, - PIPELINE_STAGES, - HUMAN_GATE_STAGES, - STAGE_TRANSITIONS, - getNextStage, - getStageIndex, - isHumanGate, -} from "./stages.js"; -export type { KanbanStageConfig } from "./stages.js"; - -export { - DEFAULT_SLA, - SLA_WARNING_THRESHOLD_PERCENT, - SLA_ESCALATION_TIMELINE, - ESCALATION_CHANNELS, - calcSlaDeadline, -} from "./sla.js"; - -export { - ERROR_STATUS_MAP, - RETRYABLE_STATUS_CODES, - DEFAULT_RETRY_POLICY, -} from "./errors.js"; diff --git a/goosefactory/packages/shared/src/constants/sla.ts b/goosefactory/packages/shared/src/constants/sla.ts deleted file mode 100644 index ee63609..0000000 --- a/goosefactory/packages/shared/src/constants/sla.ts +++ /dev/null @@ -1,41 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// SLA Constants — from CONTRACTS.md §5.3, Appendix B -// ═══════════════════════════════════════════════════════════ - -import type { Priority } from "../types/pipeline.js"; - -/** Default SLA deadlines by priority (in seconds) */ -export const DEFAULT_SLA: Record = { - critical: 3600, // 1 hour - high: 14400, // 4 hours - medium: 86400, // 24 hours - low: 259200, // 72 hours -}; - -/** SLA warning threshold: warn at this percentage of time remaining */ -export const SLA_WARNING_THRESHOLD_PERCENT = 25; - -/** SLA escalation timeline (minutes after task creation) */ -export const SLA_ESCALATION_TIMELINE = { - /** First reminder */ - REMINDER_1_MINUTES: 30, - /** Second reminder */ - REMINDER_2_MINUTES: 120, - /** Auto-escalate after breach */ - AUTO_ESCALATE_AFTER_BREACH_MINUTES: 120, -} as const; - -/** Notification channels by escalation level */ -export const ESCALATION_CHANNELS: Record = { - 0: ["dashboard"], - 1: ["dashboard", "discord"], - 2: ["dashboard", "discord", "push"], - 3: ["dashboard", "discord", "push", "sms"], - 4: ["dashboard", "discord", "push", "sms"], -}; - -/** Calculate SLA deadline from priority */ -export function calcSlaDeadline(priority: Priority, fromDate?: Date): Date { - const base = fromDate ?? new Date(); - return new Date(base.getTime() + DEFAULT_SLA[priority] * 1000); -} diff --git a/goosefactory/packages/shared/src/constants/stages.ts b/goosefactory/packages/shared/src/constants/stages.ts deleted file mode 100644 index d253542..0000000 --- a/goosefactory/packages/shared/src/constants/stages.ts +++ /dev/null @@ -1,72 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Pipeline Stage Constants — from CONTRACTS.md §4.3, Appendix A -// ═══════════════════════════════════════════════════════════ - -import type { PipelineStage } from "../types/pipeline.js"; - -export interface KanbanStageConfig { - stage: PipelineStage; - label: string; - color: string; - isGate?: boolean; -} - -export const KANBAN_STAGES: KanbanStageConfig[] = [ - { stage: "intake", label: "📥 Intake", color: "#666" }, - { stage: "scaffolding", label: "🏗️ Scaffolding", color: "#888" }, - { stage: "building", label: "🔨 Building", color: "#4488FF" }, - { stage: "testing", label: "🧪 Testing", color: "#FF8844" }, - { stage: "review", label: "👁️ Review ★", color: "#FFD700", isGate: true }, - { stage: "staging", label: "🚀 Staging ★", color: "#00AAFF", isGate: true }, - { stage: "production", label: "🌍 Production ★", color: "#FF4444", isGate: true }, - { stage: "published", label: "✅ Published", color: "#00CC55" }, -]; - -/** Ordered list of all pipeline stages */ -export const PIPELINE_STAGES: PipelineStage[] = [ - "intake", - "scaffolding", - "building", - "testing", - "review", - "staging", - "production", - "published", -]; - -/** Stages that require human approval to pass through */ -export const HUMAN_GATE_STAGES: PipelineStage[] = [ - "review", - "staging", - "production", -]; - -/** Stage transition matrix: from → allowed targets */ -export const STAGE_TRANSITIONS: Record = { - intake: ["scaffolding"], - scaffolding: ["building"], - building: ["testing"], - testing: ["review"], - review: ["staging"], - staging: ["production"], - production: ["published"], - published: [], -}; - -/** Get the next stage in sequence */ -export function getNextStage(current: PipelineStage): PipelineStage | null { - const idx = PIPELINE_STAGES.indexOf(current); - return idx >= 0 && idx < PIPELINE_STAGES.length - 1 - ? PIPELINE_STAGES[idx + 1] - : null; -} - -/** Get stage index (0-based) */ -export function getStageIndex(stage: PipelineStage): number { - return PIPELINE_STAGES.indexOf(stage); -} - -/** Check if a stage requires human approval */ -export function isHumanGate(stage: PipelineStage): boolean { - return HUMAN_GATE_STAGES.includes(stage); -} diff --git a/goosefactory/packages/shared/src/index.ts b/goosefactory/packages/shared/src/index.ts deleted file mode 100644 index 7656532..0000000 --- a/goosefactory/packages/shared/src/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// @goosefactory/shared — Main Entry Point -// -// Single source of truth for all GooseFactory types, constants, -// and validation schemas. Generated from CONTRACTS.md. -// ═══════════════════════════════════════════════════════════ - -// All types -export * from "./types/index.js"; - -// All constants -export * from "./constants/index.js"; - -// All schemas -export * from "./schemas/index.js"; diff --git a/goosefactory/packages/shared/src/integration/index.ts b/goosefactory/packages/shared/src/integration/index.ts deleted file mode 100644 index 98c037d..0000000 --- a/goosefactory/packages/shared/src/integration/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { parseModalMessage, modalResponseToFeedbackEvent } from "./modal-to-learning.js"; diff --git a/goosefactory/packages/shared/src/integration/modal-to-learning.ts b/goosefactory/packages/shared/src/integration/modal-to-learning.ts deleted file mode 100644 index 5179dea..0000000 --- a/goosefactory/packages/shared/src/integration/modal-to-learning.ts +++ /dev/null @@ -1,112 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Modal → Learning Pipeline Integration -// -// Receives feedback from modal postMessage events, -// validates with Zod schemas, and feeds into the -// learning pipeline. -// ═══════════════════════════════════════════════════════════ - -import { ModalResponseSchema } from "../schemas/feedback.schema.js"; -import type { ModalResponse, FeedbackEvent } from "../types/feedback.js"; - -/** - * Convert a raw postMessage payload from a modal into a validated - * ModalResponse. Returns null if validation fails. - */ -export function parseModalMessage(raw: unknown): { - valid: true; - response: ModalResponse; -} | { - valid: false; - error: string; - details?: unknown; -} { - if (!raw || typeof raw !== "object" || !("type" in raw)) { - return { valid: false, error: "Not a valid modal message: missing type field" }; - } - - const msg = raw as Record; - - // Only process factory_modal_response messages - if (msg.type !== "factory_modal_response") { - return { valid: false, error: `Not a feedback response: type=${msg.type}` }; - } - - const parsed = ModalResponseSchema.safeParse(raw); - if (!parsed.success) { - return { - valid: false, - error: "Modal response failed schema validation", - details: parsed.error.issues.map((i) => ({ - path: i.path.join("."), - message: i.message, - })), - }; - } - - return { valid: true, response: parsed.data as ModalResponse }; -} - -/** - * Transform a validated ModalResponse into a FeedbackEvent - * suitable for the learning pipeline's processFeedback() function. - * - * The caller must provide workProduct context since the modal - * doesn't carry that information. - */ -export function modalResponseToFeedbackEvent( - response: ModalResponse, - context: { - workProductType: "mcp_server" | "code_module" | "design" | "test_suite" | "documentation" | "pipeline_config"; - workProductId: string; - workProductVersion: string; - workProductSummary: string; - pipelineStage?: string | null; - mcpServerType?: string; - bubaConfidence?: number; - }, -): FeedbackEvent { - const now = new Date(); - const hour = now.getHours(); - - const timeOfDay = - hour >= 6 && hour < 12 ? "morning" as const : - hour >= 12 && hour < 17 ? "afternoon" as const : - hour >= 17 && hour < 21 ? "evening" as const : - "night" as const; - - const days = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]; - - return { - id: crypto.randomUUID(), - timestamp: response.timestamp, - sessionId: response.sessionId, - modalType: response.modalType, - modalVersion: response.modalVersion, - workProduct: { - type: context.workProductType, - id: context.workProductId, - version: context.workProductVersion, - summary: context.workProductSummary, - bubaConfidence: context.bubaConfidence ?? 0.5, - generationContext: { - promptHash: "", - memorySnapshot: [], - modelUsed: "unknown", - generationTimeMs: 0, - iterationCount: 1, - }, - }, - pipelineId: response.pipelineId || null, - pipelineStage: (context.pipelineStage as FeedbackEvent["pipelineStage"]) ?? null, - mcpServerType: context.mcpServerType, - feedback: response.feedback, - meta: { - ...response.meta, - timeOfDay, - dayOfWeek: days[now.getDay()], - sessionFatigue: 0, - concurrentModals: 0, - }, - }; -} diff --git a/goosefactory/packages/shared/src/schemas/feedback.schema.ts b/goosefactory/packages/shared/src/schemas/feedback.schema.ts deleted file mode 100644 index bb3c868..0000000 --- a/goosefactory/packages/shared/src/schemas/feedback.schema.ts +++ /dev/null @@ -1,207 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Feedback Zod Schemas — Runtime validation for CONTRACTS.md §3 -// ═══════════════════════════════════════════════════════════ - -import { z } from "zod"; - -export const QualityDimensionSchema = z.enum([ - "code_quality", "design_aesthetics", "design_ux", "test_coverage", - "documentation", "architecture", "error_handling", "performance", - "security", "creativity", "completeness", "naming", "dx", -]); - -export const PipelineStageSchema = z.enum([ - "intake", "scaffolding", "building", "testing", - "review", "staging", "production", "published", -]); - -export const DecisionFeedbackSchema = z.object({ - decision: z.enum(["approved", "rejected", "needs_work", "deferred", "skipped"]), - reason: z.string().optional(), - severity: z.enum(["minor", "moderate", "major", "critical"]).optional(), - blockers: z.array(z.string()).optional(), - suggestions: z.array(z.string()).optional(), - tags: z.array(z.string()).optional(), -}); - -export const DimensionScoreSchema = z.object({ - dimension: QualityDimensionSchema, - score: z.number().min(1).max(10), - weight: z.number().optional(), - comment: z.string().optional(), -}); - -export const FreeTextFeedbackSchema = z.object({ - liked: z.string().optional(), - disliked: z.string().optional(), - generalNotes: z.string().optional(), -}); - -export const ComparisonFeedbackSchema = z.object({ - preferred: z.enum(["A", "B", "neither", "both_good"]), - reason: z.string().optional(), - preferenceStrength: z.enum(["slight", "moderate", "strong", "overwhelming"]), - winningFactors: z.array(z.string()).optional(), - perDimension: z.array(z.object({ - dimension: QualityDimensionSchema, - winner: z.enum(["A", "B", "tie"]), - })).optional(), - timeOnA_ms: z.number().optional(), - timeOnB_ms: z.number().optional(), -}); - -export const AnnotationSchema = z.object({ - type: z.enum(["praise", "criticism", "question", "suggestion"]), - target: z.object({ - file: z.string().optional(), - lineStart: z.number().optional(), - lineEnd: z.number().optional(), - charStart: z.number().optional(), - charEnd: z.number().optional(), - selector: z.string().optional(), - region: z.string().optional(), - }), - text: z.string(), - severity: z.enum(["nit", "minor", "major", "critical"]).optional(), - category: z.string().optional(), -}); - -export const ConfidenceFeedbackSchema = z.object({ - confidencePercent: z.number().min(0).max(100), - zone: z.enum(["guess", "educated_guess", "confident", "certain"]), - wouldDelegateToAI: z.boolean(), - needsExpertReview: z.boolean(), - lowConfidenceReason: z.string().optional(), - isTrainingExample: z.boolean().optional(), -}); - -export const EstimationFeedbackSchema = z.object({ - estimate: z.union([z.number(), z.literal("unknown"), z.literal("break")]), - context: z.string().optional(), - hoveredValues: z.array(z.number()).optional(), -}); - -export const BatchDecisionSchema = z.object({ - itemId: z.string(), - decision: z.enum(["approve", "reject", "love", "skip"]), - timeMs: z.number(), - flippedForDetails: z.boolean(), -}); - -export const TemperatureFeedbackSchema = z.object({ - temperature: z.number().min(0).max(100), - zone: z.enum(["freezing", "cold", "lukewarm", "warm", "hot", "blazing"]), - drivers: z.array(z.string()), - dragJourney: z.array(z.number()), -}); - -export const ChecklistFeedbackSchema = z.object({ - checks: z.record(z.boolean()), - notes: z.record(z.string()), - checkOrder: z.array(z.string()), - allClear: z.boolean(), - completionRate: z.number().min(0).max(1), -}); - -export const RankingFeedbackSchema = z.object({ - finalRanking: z.array(z.string()), - rankingChanges: z.number(), - lockedItems: z.array(z.string()), - expandedItems: z.array(z.string()), - perItemNotes: z.record(z.string()).optional(), -}); - -export const RetrospectiveNoteSchema = z.object({ - text: z.string(), - priority: z.enum(["urgent", "important", "idea"]).optional(), -}); - -export const RetrospectiveFeedbackSchema = z.object({ - start: z.array(RetrospectiveNoteSchema), - stop: z.array(RetrospectiveNoteSchema), - continue_: z.array(RetrospectiveNoteSchema), -}); - -export const FeedbackPayloadSchema = z.object({ - decision: DecisionFeedbackSchema.optional(), - scores: z.array(DimensionScoreSchema).optional(), - freeText: FreeTextFeedbackSchema.optional(), - comparison: ComparisonFeedbackSchema.optional(), - annotations: z.array(AnnotationSchema).optional(), - confidence: ConfidenceFeedbackSchema.optional(), - estimation: EstimationFeedbackSchema.optional(), - batchDecisions: z.array(BatchDecisionSchema).optional(), - retrospective: RetrospectiveFeedbackSchema.optional(), - ranking: RankingFeedbackSchema.optional(), - checklist: ChecklistFeedbackSchema.optional(), - temperature: TemperatureFeedbackSchema.optional(), - custom: z.record(z.unknown()).optional(), -}); - -export const ModalMetricsSchema = z.object({ - timeToFirstInteractionMs: z.number(), - timeToDecisionMs: z.number(), - fieldsModified: z.array(z.string()), - scrollDepth: z.number().optional(), - revisits: z.number(), - totalInteractions: z.number(), - viewportSize: z.object({ width: z.number(), height: z.number() }), - deviceType: z.enum(["desktop", "mobile"]), - hoverJourney: z.array(z.object({ target: z.string(), durationMs: z.number() })).optional(), - hesitationPoints: z.array(z.object({ target: z.string(), durationMs: z.number() })).optional(), -}); - -export const WorkProductRefSchema = z.object({ - type: z.enum(["mcp_server", "code_module", "design", "test_suite", "documentation", "pipeline_config"]), - id: z.string(), - version: z.string(), - path: z.string().optional(), - summary: z.string(), - bubaConfidence: z.number().min(0).max(1), - bubaScorePrediction: z.number().optional(), - generationContext: z.object({ - promptHash: z.string(), - memorySnapshot: z.array(z.string()), - modelUsed: z.string(), - generationTimeMs: z.number(), - iterationCount: z.number(), - }), -}); - -export const EnrichedMetaSchema = z.object({ - timeOfDay: z.enum(["morning", "afternoon", "evening", "night"]), - dayOfWeek: z.string(), - sessionFatigue: z.number(), - concurrentModals: z.number(), - extractedThemes: z.array(z.string()).optional(), - sentiment: z.number().optional(), - actionableItems: z.array(z.string()).optional(), - predictionDelta: z.number().optional(), -}); - -export const ModalResponseSchema = z.object({ - type: z.literal("factory_modal_response"), - modalType: z.string(), - modalVersion: z.string(), - pipelineId: z.string(), - itemId: z.string(), - sessionId: z.string(), - timestamp: z.string(), - responseTimeMs: z.number(), - feedback: FeedbackPayloadSchema, - meta: ModalMetricsSchema, -}); - -export const FeedbackEventSchema = z.object({ - id: z.string(), - timestamp: z.string(), - sessionId: z.string(), - modalType: z.string(), - modalVersion: z.string(), - workProduct: WorkProductRefSchema, - pipelineId: z.string().nullable(), - pipelineStage: PipelineStageSchema.nullable(), - mcpServerType: z.string().optional(), - feedback: FeedbackPayloadSchema, - meta: ModalMetricsSchema.merge(EnrichedMetaSchema), -}); diff --git a/goosefactory/packages/shared/src/schemas/index.ts b/goosefactory/packages/shared/src/schemas/index.ts deleted file mode 100644 index 9fd7eda..0000000 --- a/goosefactory/packages/shared/src/schemas/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Schema Re-exports -// ═══════════════════════════════════════════════════════════ - -export { - FeedbackEventSchema, - FeedbackPayloadSchema, - ModalResponseSchema, - ModalMetricsSchema, - WorkProductRefSchema, - EnrichedMetaSchema, - DecisionFeedbackSchema, - DimensionScoreSchema, - FreeTextFeedbackSchema, - ComparisonFeedbackSchema, - AnnotationSchema, - ConfidenceFeedbackSchema, - EstimationFeedbackSchema, - BatchDecisionSchema, - TemperatureFeedbackSchema, - ChecklistFeedbackSchema, - RankingFeedbackSchema, - RetrospectiveFeedbackSchema, - RetrospectiveNoteSchema, - QualityDimensionSchema, - PipelineStageSchema as FeedbackPipelineStageSchema, -} from "./feedback.schema.js"; - -export { - PipelineStatusSchema, - PrioritySchema, - PipelineTemplateSchema, - PipelineStageSchema, - CreatePipelineSchema, - UpdatePipelineSchema, - AdvanceStageSchema, -} from "./pipeline.schema.js"; - -export { - TaskDecisionSchema, - TaskTypeSchema, - TaskStatusSchema, - CompleteTaskSchema, - ReassignTaskSchema, - BulkTaskSchema, - ApproveBodySchema, - RejectBodySchema, - EscalateBodySchema, -} from "./task.schema.js"; diff --git a/goosefactory/packages/shared/src/schemas/pipeline.schema.ts b/goosefactory/packages/shared/src/schemas/pipeline.schema.ts deleted file mode 100644 index a21e349..0000000 --- a/goosefactory/packages/shared/src/schemas/pipeline.schema.ts +++ /dev/null @@ -1,36 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Pipeline Zod Schemas — Runtime validation -// ═══════════════════════════════════════════════════════════ - -import { z } from "zod"; - -export const PipelineStatusSchema = z.enum(["active", "paused", "completed", "failed", "archived"]); -export const PrioritySchema = z.enum(["critical", "high", "medium", "low"]); -export const PipelineTemplateSchema = z.enum(["mcp-server-standard", "mcp-server-minimal", "mcp-server-enterprise"]); -export const PipelineStageSchema = z.enum([ - "intake", "scaffolding", "building", "testing", - "review", "staging", "production", "published", -]); - -export const CreatePipelineSchema = z.object({ - name: z.string().min(1).max(200), - platform: z.string().min(1).max(100), - template: PipelineTemplateSchema.optional().default("mcp-server-standard"), - priority: PrioritySchema.optional().default("medium"), - config: z.record(z.unknown()).optional(), - assigneeId: z.string().uuid().optional(), -}); - -export const UpdatePipelineSchema = z.object({ - priority: PrioritySchema.optional(), - status: PipelineStatusSchema.optional(), - config: z.record(z.unknown()).optional(), - assigneeId: z.string().uuid().nullable().optional(), - metadata: z.record(z.unknown()).optional(), -}); - -export const AdvanceStageSchema = z.object({ - targetStage: PipelineStageSchema.optional(), - skipValidation: z.boolean().optional().default(false), - notes: z.string().optional(), -}); diff --git a/goosefactory/packages/shared/src/schemas/task.schema.ts b/goosefactory/packages/shared/src/schemas/task.schema.ts deleted file mode 100644 index 568a391..0000000 --- a/goosefactory/packages/shared/src/schemas/task.schema.ts +++ /dev/null @@ -1,41 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Task Zod Schemas — Runtime validation -// ═══════════════════════════════════════════════════════════ - -import { z } from "zod"; - -export const TaskDecisionSchema = z.enum(["approved", "rejected", "deferred", "escalated"]); -export const TaskTypeSchema = z.enum(["approval", "review", "decision", "manual_action", "fix_required"]); -export const TaskStatusSchema = z.enum(["pending", "claimed", "in_progress", "completed", "expired", "escalated"]); - -export const CompleteTaskSchema = z.object({ - decision: TaskDecisionSchema, - notes: z.string().optional(), - decisionData: z.record(z.unknown()).optional(), -}); - -export const ReassignTaskSchema = z.object({ - assigneeId: z.string().uuid(), - reason: z.string().optional(), -}); - -export const BulkTaskSchema = z.object({ - action: z.enum(["approve", "reject", "defer"]), - taskIds: z.array(z.string().uuid()).min(1), - notes: z.string().optional(), -}); - -export const ApproveBodySchema = z.object({ - notes: z.string().optional(), - conditions: z.array(z.string()).optional(), -}); - -export const RejectBodySchema = z.object({ - reason: z.string().min(1), - requestedChanges: z.array(z.string()).optional(), -}); - -export const EscalateBodySchema = z.object({ - escalateTo: z.string().uuid().optional(), - reason: z.string().min(1), -}); diff --git a/goosefactory/packages/shared/src/types/agent.ts b/goosefactory/packages/shared/src/types/agent.ts deleted file mode 100644 index 8280e2a..0000000 --- a/goosefactory/packages/shared/src/types/agent.ts +++ /dev/null @@ -1,29 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Agent Types — from CONTRACTS.md §1.4 -// ═══════════════════════════════════════════════════════════ - -export type AgentType = "ai_builder" | "test_runner" | "deployer" | "monitor"; -export type AgentStatus = "idle" | "active" | "error" | "offline"; - -export interface Agent { - id: string; - name: string; - type: AgentType; - status: AgentStatus; - currentTaskId: string | null; - currentPipelineId: string | null; - health: AgentHealth; - capabilities: string[]; - config: Record; - tasksCompletedTotal: number; - avgTaskDurationSeconds: number | null; - createdAt: string; - updatedAt: string; -} - -export interface AgentHealth { - uptimeSeconds: number; - tasksCompleted24h: number; - errorRate: number; - lastHeartbeat: string; -} diff --git a/goosefactory/packages/shared/src/types/api.ts b/goosefactory/packages/shared/src/types/api.ts deleted file mode 100644 index 7c22a79..0000000 --- a/goosefactory/packages/shared/src/types/api.ts +++ /dev/null @@ -1,86 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// API Response Types — from CONTRACTS.md §1.3 -// ═══════════════════════════════════════════════════════════ - -/** Standard success response envelope */ -export interface ApiResponse { - ok: true; - data: T; - meta?: ResponseMeta; -} - -export interface ResponseMeta { - requestId: string; - timestamp: string; - pagination?: PaginationMeta; - rateLimit?: RateLimitMeta; -} - -export interface PaginationMeta { - page: number; - limit: number; - total: number; - totalPages: number; - hasNext: boolean; - hasPrev: boolean; -} - -export interface RateLimitMeta { - limit: number; - remaining: number; - resetAt: string; -} - -/** Standard error response */ -export interface ApiError { - ok: false; - error: { - code: ErrorCode; - message: string; - details?: Record; - field?: string; - requestId: string; - }; -} - -export type ErrorCode = - | "BAD_REQUEST" - | "VALIDATION_ERROR" - | "UNAUTHORIZED" - | "FORBIDDEN" - | "NOT_FOUND" - | "CONFLICT" - | "GONE" - | "RATE_LIMITED" - | "INTERNAL_ERROR" - | "SERVICE_UNAVAILABLE" - | "TIMEOUT"; - -/** Standard pagination request params */ -export interface PaginationParams { - page?: number; - limit?: number; - sort?: string; -} - -/** JWT payload shape */ -export interface JWTPayload { - sub: string; - email: string; - role: Role; - scopes: Scope[]; - iat: number; - exp: number; -} - -export type Role = "owner" | "admin" | "operator" | "viewer" | "agent"; - -export type Scope = - | "pipelines:read" | "pipelines:write" - | "tasks:read" | "tasks:approve" | "tasks:reject" - | "deploy:staging" | "deploy:production" - | "agents:manage" - | "assets:read" | "assets:write" - | "audit:read" - | "feedback:read" | "feedback:write" - | "learning:read" | "learning:write"; diff --git a/goosefactory/packages/shared/src/types/approval.ts b/goosefactory/packages/shared/src/types/approval.ts deleted file mode 100644 index 88ee77e..0000000 --- a/goosefactory/packages/shared/src/types/approval.ts +++ /dev/null @@ -1,24 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Approval Types — from CONTRACTS.md §1.4 -// ═══════════════════════════════════════════════════════════ - -import type { PipelineStage } from "./pipeline.js"; - -export type ApprovalType = "stage_gate" | "deploy" | "code_review" | "manual_check"; -export type ApprovalStatus = "pending" | "approved" | "rejected" | "expired"; - -export interface Approval { - id: string; - taskId: string; - pipelineId: string; - stageName: PipelineStage; - type: ApprovalType; - status: ApprovalStatus; - approvedBy: string | null; - approvedAt: string | null; - rejectionReason: string | null; - conditions: string[]; - requiredApprovers: number; - currentApprovers: number; - createdAt: string; -} diff --git a/goosefactory/packages/shared/src/types/asset.ts b/goosefactory/packages/shared/src/types/asset.ts deleted file mode 100644 index 393bebd..0000000 --- a/goosefactory/packages/shared/src/types/asset.ts +++ /dev/null @@ -1,27 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Asset Types — from CONTRACTS.md §1.4 -// ═══════════════════════════════════════════════════════════ - -import type { PipelineStage } from "./pipeline.js"; - -export type AssetType = "code" | "config" | "docs" | "build" | "test_report" | "screenshot"; - -export interface Asset { - id: string; - pipelineId: string; - stageName: PipelineStage | null; - type: AssetType; - name: string; - path: string | null; - storageKey: string; - storageBucket: string | null; - sizeBytes: number; - contentType: string; - checksumSha256: string; - version: number; - previousVersionId: string | null; - generatedBy: string; - generatorType: "user" | "agent" | "ci"; - metadata: Record; - createdAt: string; -} diff --git a/goosefactory/packages/shared/src/types/audit.ts b/goosefactory/packages/shared/src/types/audit.ts deleted file mode 100644 index d428b8f..0000000 --- a/goosefactory/packages/shared/src/types/audit.ts +++ /dev/null @@ -1,16 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Audit Types — from CONTRACTS.md §1.4 -// ═══════════════════════════════════════════════════════════ - -export interface AuditEntry { - id: string; - actorType: "user" | "agent" | "system" | "webhook"; - actorId: string | null; - actorName: string | null; - action: string; - entityType: string; - entityId: string; - changes: Record; - metadata: Record; - createdAt: string; -} diff --git a/goosefactory/packages/shared/src/types/feedback.ts b/goosefactory/packages/shared/src/types/feedback.ts deleted file mode 100644 index c70a642..0000000 --- a/goosefactory/packages/shared/src/types/feedback.ts +++ /dev/null @@ -1,400 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Feedback & Modal Types — from CONTRACTS.md §3 & §6 -// ═══════════════════════════════════════════════════════════ - -import type { PipelineStage } from "./pipeline.js"; -import type { TestResults, DiffSummary } from "./task.js"; - -// ─── Modal Messages (postMessage API) ─── - -export type ModalMessage = - | ModalResponse - | ModalReady - | ModalResize - | ModalClose - | ModalError; - -export interface ModalReady { - type: "factory_modal_ready"; - modalType: string; - version: string; -} - -export interface ModalResize { - type: "factory_modal_resize"; - width?: number; - height: number; -} - -export interface ModalClose { - type: "factory_modal_close"; - reason: "cancelled" | "timeout" | "error"; -} - -export interface ModalError { - type: "factory_modal_error"; - error: string; - details?: unknown; -} - -export interface ModalResponse { - type: "factory_modal_response"; - modalType: string; - modalVersion: string; - pipelineId: string; - itemId: string; - sessionId: string; - timestamp: string; - responseTimeMs: number; - feedback: FeedbackPayload; - meta: ModalMetrics; -} - -// ─── Feedback Payload ─── - -export interface FeedbackPayload { - decision?: DecisionFeedback; - scores?: DimensionScore[]; - freeText?: FreeTextFeedback; - comparison?: ComparisonFeedback; - annotations?: Annotation[]; - confidence?: ConfidenceFeedback; - estimation?: EstimationFeedback; - batchDecisions?: BatchDecision[]; - retrospective?: RetrospectiveFeedback; - ranking?: RankingFeedback; - checklist?: ChecklistFeedback; - temperature?: TemperatureFeedback; - custom?: Record; -} - -// ─── The 8 Feedback Types ─── - -export interface DecisionFeedback { - decision: "approved" | "rejected" | "needs_work" | "deferred" | "skipped"; - reason?: string; - severity?: "minor" | "moderate" | "major" | "critical"; - blockers?: string[]; - suggestions?: string[]; - tags?: string[]; -} - -export type QualityDimension = - | "code_quality" - | "design_aesthetics" - | "design_ux" - | "test_coverage" - | "documentation" - | "architecture" - | "error_handling" - | "performance" - | "security" - | "creativity" - | "completeness" - | "naming" - | "dx"; - -export interface DimensionScore { - dimension: QualityDimension; - score: number; - weight?: number; - comment?: string; -} - -export interface FreeTextFeedback { - liked?: string; - disliked?: string; - generalNotes?: string; -} - -export interface ComparisonFeedback { - preferred: "A" | "B" | "neither" | "both_good"; - reason?: string; - preferenceStrength: "slight" | "moderate" | "strong" | "overwhelming"; - winningFactors?: string[]; - perDimension?: Array<{ - dimension: QualityDimension; - winner: "A" | "B" | "tie"; - }>; - timeOnA_ms?: number; - timeOnB_ms?: number; -} - -export interface Annotation { - type: "praise" | "criticism" | "question" | "suggestion"; - target: { - file?: string; - lineStart?: number; - lineEnd?: number; - charStart?: number; - charEnd?: number; - selector?: string; - region?: string; - }; - text: string; - severity?: "nit" | "minor" | "major" | "critical"; - category?: string; -} - -export interface ConfidenceFeedback { - confidencePercent: number; - zone: "guess" | "educated_guess" | "confident" | "certain"; - wouldDelegateToAI: boolean; - needsExpertReview: boolean; - lowConfidenceReason?: string; - isTrainingExample?: boolean; -} - -export interface EstimationFeedback { - estimate: number | "unknown" | "break"; - context?: string; - hoveredValues?: number[]; -} - -export interface BatchDecision { - itemId: string; - decision: "approve" | "reject" | "love" | "skip"; - timeMs: number; - flippedForDetails: boolean; -} - -export interface RankingFeedback { - finalRanking: string[]; - rankingChanges: number; - lockedItems: string[]; - expandedItems: string[]; - perItemNotes?: Record; -} - -export interface RetrospectiveFeedback { - start: RetrospectiveNote[]; - stop: RetrospectiveNote[]; - continue_: RetrospectiveNote[]; -} - -export interface RetrospectiveNote { - text: string; - priority?: "urgent" | "important" | "idea"; -} - -export interface ChecklistFeedback { - checks: Record; - notes: Record; - checkOrder: string[]; - allClear: boolean; - completionRate: number; -} - -export interface TemperatureFeedback { - temperature: number; - zone: "freezing" | "cold" | "lukewarm" | "warm" | "hot" | "blazing"; - drivers: string[]; - dragJourney: number[]; -} - -// ─── Behavioral Metrics ─── - -export interface ModalMetrics { - timeToFirstInteractionMs: number; - timeToDecisionMs: number; - fieldsModified: string[]; - scrollDepth?: number; - revisits: number; - totalInteractions: number; - viewportSize: { width: number; height: number }; - deviceType: "desktop" | "mobile"; - hoverJourney?: Array<{ target: string; durationMs: number }>; - hesitationPoints?: Array<{ target: string; durationMs: number }>; -} - -// ─── Full Feedback Event (stored) ─── - -export interface FeedbackEvent { - id: string; - timestamp: string; - sessionId: string; - modalType: string; - modalVersion: string; - workProduct: WorkProductRef; - pipelineId: string | null; - pipelineStage: PipelineStage | null; - mcpServerType?: string; - feedback: FeedbackPayload; - meta: ModalMetrics & EnrichedMeta; -} - -export interface WorkProductRef { - type: "mcp_server" | "code_module" | "design" | "test_suite" | "documentation" | "pipeline_config"; - id: string; - version: string; - path?: string; - summary: string; - bubaConfidence: number; - bubaScorePrediction?: number; - generationContext: { - promptHash: string; - memorySnapshot: string[]; - modelUsed: string; - generationTimeMs: number; - iterationCount: number; - }; -} - -export type TimeOfDay = "morning" | "afternoon" | "evening" | "night"; - -export interface EnrichedMeta { - timeOfDay: TimeOfDay; - dayOfWeek: string; - sessionFatigue: number; - concurrentModals: number; - extractedThemes?: string[]; - sentiment?: number; - actionableItems?: string[]; - predictionDelta?: number; -} - -// ─── Enriched Event ─── - -export interface EnrichedFeedbackEvent extends FeedbackEvent { - _enriched: true; - _themes?: string[]; - _sentiment?: number; - _actionableItems?: string[]; - _predictionDelta?: number; - _pipelineContext?: { - stage: PipelineStage; - stagesCompleted: number; - totalDuration: number; - }; -} - -// ─── Modal Context (host → modal) ─── - -export interface FactoryModalContext { - modalType: string; - modalVersion: string; - sessionId: string; - pipelineId: string; - pipelineName: string; - itemId: string; - itemName: string; - deliverablePreview?: string; - deliverableContent?: string; - contentBefore?: string; - contentAfter?: string; - testResults?: TestResults; - diffSummary?: DiffSummary; - metrics?: Record; - items?: ModalItem[]; - dimensions?: ModalDimension[]; - checklistItems?: ChecklistItem[]; - estimationQuestion?: string; - persona?: ModalPersona; - timerSeconds?: number; - theme: "dark" | "light"; - accentColor: string; -} - -export interface ModalItem { - id: string; - name: string; - preview: string; - content?: string; - metadata?: Record; -} - -export interface ModalDimension { - id: string; - name: string; - description?: string; - weight: number; -} - -export interface ChecklistItem { - id: string; - label: string; - description?: string; - aiAssessment?: string; - aiPasses?: boolean; -} - -export interface ModalPersona { - name: string; - role: string; - context: string; - avatarEmoji: string; -} - -// ─── Modal Registration ─── - -export type ModalUseCase = - | "binary_decision" - | "batch_review" - | "multi_dimensional" - | "comparison" - | "subjective" - | "high_stakes" - | "annotation" - | "estimation" - | "triage" - | "retrospective" - | "verification"; - -export interface ModalRegistration { - id: string; - name: string; - description: string; - version: string; - suitableFor: ModalUseCase[]; - requiredContext: string[]; - optionalContext: string[]; - outputSchema: { - feedbackTypes: string[]; - capturesMetrics: string[]; - }; - display: { - preferredType: "inline" | "sidecar"; - minWidth: number; - minHeight: number; - supportsDarkMode: boolean; - supportsMobile: boolean; - }; -} - -// ─── Learning System Types (§6) ─── - -export interface FeedbackStats { - totalEvents: number; - last30Days: { - approvalRate: number; - avgQualityScore: number; - avgTimeToDecisionMs: number; - predictionAccuracy: number; - autoApproveRate: number; - feedbackVolume: number; - }; - trends: { - approvalRate: "improving" | "stable" | "declining"; - qualityScore: "improving" | "stable" | "declining"; - decisionSpeed: "improving" | "stable" | "declining"; - }; - activeRules: number; - autonomyLevels: Record; -} - -export interface CalibrationCurve { - lastUpdated: string; - totalDataPoints: number; - buckets: CalibrationBucket[]; - expectedCalibrationError: number; -} - -export interface CalibrationBucket { - rawConfidenceLow: number; - rawConfidenceHigh: number; - avgPredictedConfidence: number; - avgActualApprovalRate: number; - sampleCount: number; - correctionDelta: number; - calibratedConfidence: number; -} diff --git a/goosefactory/packages/shared/src/types/index.ts b/goosefactory/packages/shared/src/types/index.ts deleted file mode 100644 index 4a34358..0000000 --- a/goosefactory/packages/shared/src/types/index.ts +++ /dev/null @@ -1,153 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// @goosefactory/shared — Type Re-exports -// Single source of truth for all GooseFactory types -// Generated from CONTRACTS.md -// ═══════════════════════════════════════════════════════════ - -// API types -export type { - ApiResponse, - ApiError, - ErrorCode, - ResponseMeta, - PaginationMeta, - RateLimitMeta, - PaginationParams, - JWTPayload, - Role, - Scope, -} from "./api.js"; - -// Pipeline types -export type { - Pipeline, - PipelineStage, - PipelineStatus, - PipelineTemplate, - PipelineStageRecord, - StageStatus, - Priority, - ValidationRule, - DashboardSummary, - PipelineDetailState, - PipelineTemplateConfig, -} from "./pipeline.js"; - -// Task types -export type { - Task, - TaskType, - TaskStatus, - TaskDecision, - TaskContext, - TaskStats, - TestResults, - TestFailure, - DiffSummary, - DiffFile, - BlockerInfo, -} from "./task.js"; - -// Approval types -export type { - Approval, - ApprovalType, - ApprovalStatus, -} from "./approval.js"; - -// Agent types -export type { - Agent, - AgentType, - AgentStatus, - AgentHealth, -} from "./agent.js"; - -// Asset types -export type { - Asset, - AssetType, -} from "./asset.js"; - -// Notification types -export type { - Notification, - NotificationType, - NotificationChannel, -} from "./notification.js"; - -// Audit types -export type { - AuditEntry, -} from "./audit.js"; - -// WebSocket types -export type { - WsChannel, - WsEvent, - WsSubscribe, - WsUnsubscribe, - WsPing, - WsSubscribed, - WsPong, - WebSocketEventType, - PipelineCreatedPayload, - PipelineStageChangedPayload, - PipelineBlockedPayload, - TaskCreatedPayload, - TaskCompletedPayload, - TaskSlaPayload, - AgentStatusPayload, - AgentHeartbeatPayload, - DeployPayload, - FeedbackReceivedPayload, - ServerStatus, - DeploymentInfo, -} from "./ws.js"; - -// Feedback & Modal types -export type { - // Modal messages - ModalMessage, - ModalReady, - ModalResize, - ModalClose, - ModalError, - ModalResponse, - // Feedback - FeedbackPayload, - FeedbackEvent, - EnrichedFeedbackEvent, - WorkProductRef, - // Decision types - DecisionFeedback, - DimensionScore, - QualityDimension, - FreeTextFeedback, - ComparisonFeedback, - Annotation, - ConfidenceFeedback, - EstimationFeedback, - BatchDecision, - RankingFeedback, - RetrospectiveFeedback, - RetrospectiveNote, - ChecklistFeedback, - TemperatureFeedback, - // Metrics - ModalMetrics, - EnrichedMeta, - TimeOfDay, - // Modal context & registration - FactoryModalContext, - ModalItem, - ModalDimension, - ChecklistItem as ModalChecklistItem, - ModalPersona, - ModalUseCase, - ModalRegistration, - // Learning - FeedbackStats, - CalibrationCurve, - CalibrationBucket, -} from "./feedback.js"; diff --git a/goosefactory/packages/shared/src/types/notification.ts b/goosefactory/packages/shared/src/types/notification.ts deleted file mode 100644 index 3517967..0000000 --- a/goosefactory/packages/shared/src/types/notification.ts +++ /dev/null @@ -1,33 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Notification Types — from CONTRACTS.md §1.4 -// ═══════════════════════════════════════════════════════════ - -export type NotificationType = - | "task_assigned" - | "approval_pending" - | "sla_warning" - | "sla_breach" - | "deploy_status" - | "pipeline_completed" - | "agent_error" - | "feedback_processed"; - -export type NotificationChannel = "dashboard" | "discord" | "email" | "sms" | "push"; - -export interface Notification { - id: string; - userId: string; - type: NotificationType; - title: string; - body: string | null; - data: Record; - channels: NotificationChannel[]; - deliveredVia: NotificationChannel[]; - read: boolean; - readAt: string | null; - dismissed: boolean; - entityType: string | null; - entityId: string | null; - actionUrl: string | null; - createdAt: string; -} diff --git a/goosefactory/packages/shared/src/types/pipeline.ts b/goosefactory/packages/shared/src/types/pipeline.ts deleted file mode 100644 index 50358c5..0000000 --- a/goosefactory/packages/shared/src/types/pipeline.ts +++ /dev/null @@ -1,105 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Pipeline Types — from CONTRACTS.md §1.4 -// ═══════════════════════════════════════════════════════════ - -export type PipelineStatus = "active" | "paused" | "completed" | "failed" | "archived"; -export type Priority = "critical" | "high" | "medium" | "low"; -export type PipelineTemplate = "mcp-server-standard" | "mcp-server-minimal" | "mcp-server-enterprise"; - -export type PipelineStage = - | "intake" - | "scaffolding" - | "building" - | "testing" - | "review" - | "staging" - | "production" - | "published"; - -export type StageStatus = "pending" | "active" | "completed" | "skipped" | "failed"; - -export interface Pipeline { - id: string; - name: string; - slug: string; - template: PipelineTemplate; - platform: string; - currentStage: PipelineStage; - status: PipelineStatus; - priority: Priority; - createdBy: string; - assigneeId: string | null; - config: Record; - metadata: Record; - slaDeadline: string | null; - startedAt: string; - completedAt: string | null; - createdAt: string; - updatedAt: string; -} - -export interface PipelineStageRecord { - id: string; - pipelineId: string; - stageName: PipelineStage; - stageOrder: number; - status: StageStatus; - requiresApproval: boolean; - approvalType: "manual" | "auto" | "conditional"; - autoAdvance: boolean; - validationRules: ValidationRule[]; - enteredAt: string | null; - completedAt: string | null; - durationSeconds: number | null; - createdAt: string; -} - -export interface ValidationRule { - type: "test_coverage" | "tests_passing" | "required_asset" | "approval_count" | "custom"; - config: Record; -} - -/** Dashboard aggregation */ -export interface DashboardSummary { - pipelines: { - active: number; - paused: number; - completed: number; - failed: number; - }; - tasks: import("./task.js").TaskStats; - agents: { - total: number; - active: number; - idle: number; - error: number; - offline: number; - }; - recentActivity: import("./audit.js").AuditEntry[]; - slaStatus: { - onTrack: number; - warning: number; - breached: number; - }; -} - -/** Pipeline detail state for MCP resources */ -export interface PipelineDetailState { - pipeline: Pipeline; - stages: PipelineStageRecord[]; - currentTasks: import("./task.js").Task[]; - recentAssets: import("./asset.js").Asset[]; - blockers: import("./task.js").BlockerInfo[]; - timeline: import("./audit.js").AuditEntry[]; -} - -/** Pipeline template config */ -export interface PipelineTemplateConfig { - id: PipelineTemplate; - name: string; - description: string; - stages: PipelineStage[]; - defaultConfig: Record; - requiredAssets: import("./asset.js").AssetType[]; - validationRules: ValidationRule[]; -} diff --git a/goosefactory/packages/shared/src/types/task.ts b/goosefactory/packages/shared/src/types/task.ts deleted file mode 100644 index 75d8bf2..0000000 --- a/goosefactory/packages/shared/src/types/task.ts +++ /dev/null @@ -1,101 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// Task Types — from CONTRACTS.md §1.4 -// ═══════════════════════════════════════════════════════════ - -import type { PipelineStage, Priority } from "./pipeline.js"; - -export type TaskType = "approval" | "review" | "decision" | "manual_action" | "fix_required"; -export type TaskStatus = "pending" | "claimed" | "in_progress" | "completed" | "expired" | "escalated"; -export type TaskDecision = "approved" | "rejected" | "deferred" | "escalated"; - -export interface Task { - id: string; - pipelineId: string | null; - stageName: PipelineStage | null; - type: TaskType; - title: string; - description: string | null; - context: TaskContext; - status: TaskStatus; - priority: Priority; - assigneeId: string | null; - claimedAt: string | null; - claimedBy: string | null; - decision: TaskDecision | null; - decisionNotes: string | null; - decisionData: Record; - decidedAt: string | null; - decidedBy: string | null; - slaDeadline: string | null; - slaWarningsSent: number; - slaBreached: boolean; - escalationLevel: number; - blocksStageAdvance: boolean; - blocksPipelineId: string | null; - createdAt: string; - updatedAt: string; -} - -export interface TaskContext { - summary: string; - details?: string; - testResults?: TestResults; - diffSummary?: DiffSummary; - metrics?: Record; - relatedAssets?: string[]; - modalType?: string; - modalConfig?: Record; -} - -export interface TestResults { - total: number; - passed: number; - failed: number; - skipped: number; - coveragePercent: number; - failureDetails?: TestFailure[]; -} - -export interface TestFailure { - testName: string; - error: string; - file?: string; - line?: number; -} - -export interface DiffSummary { - filesChanged: number; - insertions: number; - deletions: number; - files: DiffFile[]; -} - -export interface DiffFile { - path: string; - status: "added" | "modified" | "deleted" | "renamed"; - insertions: number; - deletions: number; - patch?: string; -} - -export interface TaskStats { - pending: number; - inProgress: number; - blocked: number; - avgWaitTimeMinutes: number; - slaBreaches: number; - byPriority: Record; - byType: Record; -} - -export interface BlockerInfo { - blockerType: "task" | "stage" | "agent" | "dependency"; - entityId: string; - title: string; - pipelineId: string; - pipelineName: string; - priority: Priority; - hoursBlocked: number; - blockReason: string; - suggestedAction?: string; -} diff --git a/goosefactory/packages/shared/src/types/ws.ts b/goosefactory/packages/shared/src/types/ws.ts deleted file mode 100644 index 78966e5..0000000 --- a/goosefactory/packages/shared/src/types/ws.ts +++ /dev/null @@ -1,175 +0,0 @@ -// ═══════════════════════════════════════════════════════════ -// WebSocket Types — from CONTRACTS.md §1.6 -// ═══════════════════════════════════════════════════════════ - -import type { Pipeline, PipelineStage } from "./pipeline.js"; -import type { Task, TaskDecision, TestResults } from "./task.js"; -import type { AgentStatus, AgentHealth } from "./agent.js"; - -// ─── Channel Types ─── - -export type WsChannel = - | "pipeline:*" - | `pipeline:${string}` - | "tasks:*" - | "tasks:pending" - | "agents:*" - | "agents:health" - | "deploys:*" - | "notifications:me"; - -// ─── Client → Server Messages ─── - -export interface WsSubscribe { - type: "subscribe"; - channels: WsChannel[]; -} - -export interface WsUnsubscribe { - type: "unsubscribe"; - channels: WsChannel[]; -} - -export interface WsPing { - type: "ping"; -} - -// ─── Server → Client Messages ─── - -export interface WsEvent { - type: WebSocketEventType; - timestamp: string; - channel: WsChannel; - data: T; -} - -export interface WsSubscribed { - type: "subscribed"; - channels: WsChannel[]; -} - -export interface WsPong { - type: "pong"; -} - -// ─── Event Types ─── - -export type WebSocketEventType = - | "pipeline.created" - | "pipeline.updated" - | "pipeline.stage_changed" - | "pipeline.completed" - | "pipeline.failed" - | "pipeline.blocked" - | "task.created" - | "task.claimed" - | "task.completed" - | "task.sla_warning" - | "task.sla_breached" - | "task.escalated" - | "approval.pending" - | "approval.approved" - | "approval.rejected" - | "agent.status_changed" - | "agent.heartbeat" - | "asset.created" - | "asset.updated" - | "deploy.started" - | "deploy.succeeded" - | "deploy.failed" - | "feedback.received" - | "feedback.processed"; - -// ─── Event Payloads ─── - -export interface PipelineCreatedPayload { - pipeline: Pipeline; -} - -export interface PipelineStageChangedPayload { - pipelineId: string; - pipelineName: string; - fromStage: PipelineStage; - toStage: PipelineStage; - triggeredBy: string; -} - -export interface PipelineBlockedPayload { - pipelineId: string; - pipelineName: string; - blockedAt: PipelineStage; - reason: string; - taskId: string; -} - -export interface TaskCreatedPayload { - task: Task; -} - -export interface TaskCompletedPayload { - task: Task; - decision: TaskDecision; - decidedBy: string; -} - -export interface TaskSlaPayload { - taskId: string; - taskTitle: string; - pipelineId: string | null; - slaDeadline: string; - minutesRemaining: number; - escalationLevel: number; -} - -export interface AgentStatusPayload { - agentId: string; - agentName: string; - fromStatus: AgentStatus; - toStatus: AgentStatus; - currentTaskId: string | null; -} - -export interface AgentHeartbeatPayload { - agentId: string; - agentName: string; - status: AgentStatus; - health: AgentHealth; -} - -export interface DeployPayload { - pipelineId: string; - pipelineName: string; - target: "staging" | "production"; - version: string; - status: "started" | "succeeded" | "failed"; - url?: string; - error?: string; -} - -export interface FeedbackReceivedPayload { - feedbackId: string; - pipelineId: string | null; - modalType: string; - decision: string | null; -} - -/** Server deployment info */ -export interface ServerStatus { - name: string; - pipelineId: string; - stage: PipelineStage; - health: "healthy" | "degraded" | "failing" | "unknown"; - deployments: { - staging: DeploymentInfo | null; - production: DeploymentInfo | null; - }; - lastTestRun: TestResults | null; - metrics: Record; -} - -export interface DeploymentInfo { - version: string; - deployedAt: string; - url: string; - status: "running" | "stopped" | "failed"; -} diff --git a/goosefactory/packages/shared/tsconfig.json b/goosefactory/packages/shared/tsconfig.json deleted file mode 100644 index c088601..0000000 --- a/goosefactory/packages/shared/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "isolatedModules": true - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] -} diff --git a/goosefactory/scripts/first-boot.sh b/goosefactory/scripts/first-boot.sh deleted file mode 100755 index 8a9335a..0000000 --- a/goosefactory/scripts/first-boot.sh +++ /dev/null @@ -1,203 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# ═══════════════════════════════════════════════════════════ -# GooseFactory — First Boot Script -# Sets up the complete development environment from scratch. -# ═══════════════════════════════════════════════════════════ - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" -cd "$PROJECT_ROOT" - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -NC='\033[0m' # No Color - -print_header() { echo -e "\n${CYAN}═══════════════════════════════════════${NC}"; echo -e "${CYAN} $1${NC}"; echo -e "${CYAN}═══════════════════════════════════════${NC}\n"; } -print_step() { echo -e " ${BLUE}→${NC} $1"; } -print_ok() { echo -e " ${GREEN}✅${NC} $1"; } -print_warn() { echo -e " ${YELLOW}⚠️${NC} $1"; } -print_fail() { echo -e " ${RED}❌${NC} $1"; } - -# ─── Step 1: Check Prerequisites ─── - -print_header "Step 1: Checking Prerequisites" - -MISSING=0 - -# Node.js -if command -v node &> /dev/null; then - NODE_VERSION=$(node --version) - print_ok "Node.js $NODE_VERSION" - NODE_MAJOR=$(echo "$NODE_VERSION" | sed 's/v//' | cut -d. -f1) - if [ "$NODE_MAJOR" -lt 20 ]; then - print_warn "Node.js 20+ recommended (found $NODE_VERSION)" - fi -else - print_fail "Node.js not found — install from https://nodejs.org" - MISSING=1 -fi - -# npm -if command -v npm &> /dev/null; then - print_ok "npm $(npm --version)" -else - print_fail "npm not found" - MISSING=1 -fi - -# Docker -if command -v docker &> /dev/null; then - print_ok "Docker $(docker --version | awk '{print $3}' | tr -d ',')" -else - print_warn "Docker not found — needed for PostgreSQL and Redis" - print_step "Install from https://www.docker.com/products/docker-desktop/" - print_step "Or install PostgreSQL/Redis natively" -fi - -# Docker Compose -if command -v docker &> /dev/null && docker compose version &> /dev/null; then - print_ok "Docker Compose $(docker compose version --short 2>/dev/null || echo 'available')" -elif command -v docker-compose &> /dev/null; then - print_ok "Docker Compose (legacy) $(docker-compose --version | awk '{print $4}' | tr -d ',')" -else - print_warn "Docker Compose not found — needed for dev infrastructure" -fi - -# Rust (optional, for desktop build) -if command -v rustc &> /dev/null; then - print_ok "Rust $(rustc --version | awk '{print $2}')" -else - print_warn "Rust not found — needed only for desktop app build" - print_step "Install from https://rustup.rs" -fi - -if [ "$MISSING" -eq 1 ]; then - print_fail "Missing required prerequisites. Please install them and re-run." - exit 1 -fi - -# ─── Step 2: Environment Setup ─── - -print_header "Step 2: Environment Setup" - -if [ ! -f .env ]; then - cp .env.example .env - print_ok "Created .env from .env.example" - print_warn "Edit .env to set your JWT_SECRET and other values" -else - print_ok ".env already exists" -fi - -# Create feedback storage directories -mkdir -p ~/.config/goose/factory/feedback -mkdir -p ~/.config/goose/factory/memory -print_ok "Created feedback storage directories" - -# ─── Step 3: Install Dependencies ─── - -print_header "Step 3: Installing Dependencies" - -print_step "Running npm install (this may take a minute)..." -npm install --workspaces --include-workspace-root -print_ok "Dependencies installed" - -# ─── Step 4: Build Shared Package ─── - -print_header "Step 4: Building Shared Package" - -npm -w @goosefactory/shared run build -print_ok "@goosefactory/shared built" - -# ─── Step 5: Start Docker Services ─── - -print_header "Step 5: Starting Infrastructure" - -if command -v docker &> /dev/null; then - print_step "Starting PostgreSQL and Redis..." - docker compose -f infra/docker/docker-compose.yml up -d - - # Wait for PostgreSQL to be ready - print_step "Waiting for PostgreSQL..." - RETRIES=30 - until docker exec goosefactory-postgres pg_isready -U goosefactory -d goosefactory &>/dev/null || [ $RETRIES -eq 0 ]; do - RETRIES=$((RETRIES - 1)) - sleep 1 - done - - if [ $RETRIES -gt 0 ]; then - print_ok "PostgreSQL ready" - else - print_warn "PostgreSQL may not be ready yet — check 'docker logs goosefactory-postgres'" - fi - - # Wait for Redis to be ready - print_step "Waiting for Redis..." - RETRIES=10 - until docker exec goosefactory-redis redis-cli ping &>/dev/null || [ $RETRIES -eq 0 ]; do - RETRIES=$((RETRIES - 1)) - sleep 1 - done - - if [ $RETRIES -gt 0 ]; then - print_ok "Redis ready" - else - print_warn "Redis may not be ready — check 'docker logs goosefactory-redis'" - fi -else - print_warn "Docker not available — skipping infrastructure startup" - print_step "Make sure PostgreSQL and Redis are running manually" -fi - -# ─── Step 6: Run Database Migrations ─── - -print_header "Step 6: Database Setup" - -if command -v docker &> /dev/null && docker exec goosefactory-postgres pg_isready -U goosefactory -d goosefactory &>/dev/null; then - print_step "Database schema initialized via init.sql (Docker entrypoint)" - print_ok "Database ready" - - # Seed data from factory if available - if [ -f "$SCRIPT_DIR/seed-from-factory.ts" ]; then - print_step "Seeding data from MCP factory state..." - npx tsx "$SCRIPT_DIR/seed-from-factory.ts" || print_warn "Seed script had issues (non-fatal)" - print_ok "Seed data imported" - fi -else - print_warn "Database not accessible — skipping migrations and seeding" -fi - -# ─── Step 7: Verification ─── - -print_header "Step 7: Verification" - -# Typecheck shared -print_step "Typechecking shared package..." -npm -w @goosefactory/shared run typecheck 2>/dev/null && print_ok "Shared types valid" || print_warn "Typecheck had warnings" - -# ─── Summary ─── - -print_header "🏭 GooseFactory is Ready!" - -echo -e " ${GREEN}Start development:${NC}" -echo -e " ${BLUE}npm run dev:api${NC} — Start API server (port 4000)" -echo -e " ${BLUE}npm run dev:mcp${NC} — Start MCP server" -echo -e " ${BLUE}npm run dev:learning${NC} — Start learning pipeline" -echo -e " ${BLUE}npm run dev${NC} — Start all services" -echo -e "" -echo -e " ${GREEN}Infrastructure:${NC}" -echo -e " ${BLUE}PostgreSQL${NC} → localhost:5432 (user: goosefactory)" -echo -e " ${BLUE}Redis${NC} → localhost:6379" -echo -e " ${BLUE}API${NC} → http://localhost:4000" -echo -e " ${BLUE}API Health${NC} → http://localhost:4000/health" -echo -e "" -echo -e " ${GREEN}Useful commands:${NC}" -echo -e " ${BLUE}npm run build${NC} — Build all packages" -echo -e " ${BLUE}npm run typecheck${NC} — Check types across monorepo" -echo -e " ${BLUE}npm run dev:infra:stop${NC} — Stop Docker services" -echo -e "" diff --git a/goosefactory/scripts/seed-from-factory.ts b/goosefactory/scripts/seed-from-factory.ts deleted file mode 100644 index 3bfd3e1..0000000 --- a/goosefactory/scripts/seed-from-factory.ts +++ /dev/null @@ -1,218 +0,0 @@ -#!/usr/bin/env tsx -/** - * Seed GooseFactory Database from MCP Command Center State - * - * Reads the current MCP pipeline state from mcpengine-repo - * and converts it into GooseFactory pipeline records. - * - * Usage: npx tsx scripts/seed-from-factory.ts - */ - -import { readFileSync, existsSync } from "node:fs"; -import { resolve } from "node:path"; - -// ─── Configuration ─── - -const STATE_PATHS = [ - resolve(import.meta.dirname ?? ".", "../../../mcpengine-repo/infra/command-center/state.json"), - resolve(import.meta.dirname ?? ".", "../../mcpengine-repo/infra/command-center/state.json"), - resolve(import.meta.dirname ?? ".", "../../../mcp-command-center/state.json"), -]; - -const DB_URL = process.env.DATABASE_URL || "postgresql://goosefactory:goosefactory_dev@localhost:5432/goosefactory"; - -// ─── Types for the old state format ─── - -interface OldState { - version: number; - phases: Array<{ id: number; name: string; stages: number[] }>; - stages: Array<{ id: number; name: string; phase: number }>; - mcps: Array<{ - id: number; - name: string; - slug?: string; - stage: number; - priority?: string; - status?: string; - assignee?: string; - tags?: string[]; - metadata?: Record; - startedAt?: string; - completedAt?: string; - stageHistory?: Array<{ stage: number; enteredAt: string; completedAt?: string }>; - }>; - decisions: { - pending: unknown[]; - history: unknown[]; - }; -} - -// ─── Stage Mapping (old stage IDs → GooseFactory stages) ─── - -function mapStageToFactory(stageId: number, stages: OldState["stages"]): string { - const stage = stages.find((s) => s.id === stageId); - if (!stage) return "intake"; - - const name = stage.name.toLowerCase(); - - // Map old phases/stages to GooseFactory pipeline stages - if (name.includes("identified") || name.includes("research") || name.includes("architecture")) return "intake"; - if (name.includes("scaffold") || name.includes("bootstrap") || name.includes("init")) return "scaffolding"; - if (name.includes("build") || name.includes("implement") || name.includes("develop") || name.includes("code")) return "building"; - if (name.includes("test") || name.includes("qa") || name.includes("validation")) return "testing"; - if (name.includes("review") || name.includes("audit")) return "review"; - if (name.includes("staging") || name.includes("deploy") || name.includes("pre-prod")) return "staging"; - if (name.includes("production") || name.includes("live") || name.includes("release")) return "production"; - if (name.includes("publish") || name.includes("marketplace") || name.includes("launched")) return "published"; - - // Phase-based fallback - const phaseStage = stages.find((s) => s.id === stageId); - if (phaseStage) { - const phaseId = phaseStage.phase; - if (phaseId <= 1) return "intake"; - if (phaseId === 2) return "building"; - if (phaseId === 3) return "testing"; - if (phaseId === 4) return "review"; - if (phaseId === 5) return "staging"; - if (phaseId === 6) return "production"; - if (phaseId >= 7) return "published"; - } - - return "intake"; -} - -function mapPriority(priority?: string): string { - if (!priority) return "medium"; - const p = priority.toLowerCase(); - if (p === "critical" || p === "urgent") return "critical"; - if (p === "high") return "high"; - if (p === "low") return "low"; - return "medium"; -} - -function slugify(name: string): string { - return name - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-|-$/g, ""); -} - -// ─── Main ─── - -async function main() { - console.log("🌱 GooseFactory Seed Script\n"); - - // Find state file - let statePath: string | null = null; - for (const p of STATE_PATHS) { - if (existsSync(p)) { - statePath = p; - break; - } - } - - if (!statePath) { - console.log("⚠️ No MCP command center state.json found at:"); - STATE_PATHS.forEach((p) => console.log(` ${p}`)); - console.log("\n Skipping seed. You can seed manually later."); - process.exit(0); - } - - console.log(`📄 Reading state from: ${statePath}`); - const raw = readFileSync(statePath, "utf-8"); - const state: OldState = JSON.parse(raw); - - console.log(` Found ${state.mcps.length} MCP servers`); - console.log(` Found ${state.stages.length} stages across ${state.phases.length} phases\n`); - - // Convert MCPs to GooseFactory pipelines - const pipelines = state.mcps.map((mcp) => { - const factoryStage = mapStageToFactory(mcp.stage, state.stages); - const slug = mcp.slug || slugify(mcp.name); - - // Determine status based on stage - let status = "active"; - if (factoryStage === "published") status = "completed"; - if (mcp.status === "paused" || mcp.status === "blocked") status = "paused"; - if (mcp.status === "failed") status = "failed"; - - return { - name: mcp.name, - slug, - template: "mcp-server-standard", - platform: slug.replace(/-mcp.*/, ""), - currentStage: factoryStage, - status, - priority: mapPriority(mcp.priority), - config: mcp.metadata || {}, - metadata: { - importedFrom: "mcp-command-center", - originalStageId: mcp.stage, - originalId: mcp.id, - tags: mcp.tags || [], - }, - startedAt: mcp.startedAt || new Date().toISOString(), - completedAt: mcp.completedAt || null, - }; - }); - - // Generate SQL inserts - console.log("📝 Generated pipeline records:\n"); - - const sqlStatements: string[] = []; - - for (const p of pipelines) { - const sql = `INSERT INTO pipelines (name, slug, template, platform, current_stage, status, priority, config, metadata, started_at, completed_at) -VALUES ('${p.name.replace(/'/g, "''")}', '${p.slug.replace(/'/g, "''")}', '${p.template}', '${p.platform.replace(/'/g, "''")}', '${p.currentStage}', '${p.status}', '${p.priority}', '${JSON.stringify(p.config).replace(/'/g, "''")}', '${JSON.stringify(p.metadata).replace(/'/g, "''")}', '${p.startedAt}', ${p.completedAt ? `'${p.completedAt}'` : "NULL"}) -ON CONFLICT (slug) DO UPDATE SET - current_stage = EXCLUDED.current_stage, - status = EXCLUDED.status, - priority = EXCLUDED.priority, - metadata = EXCLUDED.metadata, - updated_at = NOW();`; - - sqlStatements.push(sql); - console.log(` 📦 ${p.name} → ${p.currentStage} (${p.status})`); - } - - // Write SQL file for manual import - const sqlPath = resolve(import.meta.dirname ?? ".", "../infra/db/seed/seed-from-factory.sql"); - const sqlDir = resolve(import.meta.dirname ?? ".", "../infra/db/seed"); - - const { mkdirSync, writeFileSync } = await import("node:fs"); - mkdirSync(sqlDir, { recursive: true }); - writeFileSync(sqlPath, `-- GooseFactory Seed Data\n-- Generated from MCP Command Center state.json\n-- Generated at: ${new Date().toISOString()}\n\n${sqlStatements.join("\n\n")}\n`); - - console.log(`\n✅ SQL seed file written to: ${sqlPath}`); - console.log(` Total pipelines: ${pipelines.length}`); - - // Try to execute against database - try { - // Dynamic import to avoid hard dependency on postgres - const { default: postgres } = await import("postgres"); - const sql = postgres(DB_URL); - - console.log("\n🔌 Connecting to database..."); - - for (const stmt of sqlStatements) { - await sql.unsafe(stmt); - } - - console.log(`✅ Seeded ${pipelines.length} pipelines into database`); - await sql.end(); - } catch (err) { - const error = err as Error; - if (error.message?.includes("Cannot find module") || error.message?.includes("connect")) { - console.log("\n⚠️ Could not connect to database — SQL file saved for manual import:"); - console.log(` psql "$DATABASE_URL" < ${sqlPath}`); - } else { - console.error("\n⚠️ Database seed error:", error.message); - console.log(` SQL file saved for manual import: ${sqlPath}`); - } - } -} - -main().catch((err) => { - console.error("Fatal error:", err); - process.exit(1); -}); diff --git a/goosefactory/scripts/test-mcp-tools.sh b/goosefactory/scripts/test-mcp-tools.sh deleted file mode 100755 index 5a4d914..0000000 --- a/goosefactory/scripts/test-mcp-tools.sh +++ /dev/null @@ -1,280 +0,0 @@ -#!/usr/bin/env bash -# ═══════════════════════════════════════════════════════════ -# GooseFactory MCP Server — Tool Verification Script -# -# Tests all 12 MCP tools via JSON-RPC over stdio. -# Prerequisites: -# - API running at http://localhost:4000 -# - MCP server compiled (packages/mcp-server/dist/index.js) -# -# Usage: ./scripts/test-mcp-tools.sh -# ═══════════════════════════════════════════════════════════ - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -MCP_SERVER="$PROJECT_DIR/packages/mcp-server/dist/index.js" -API_URL="${FACTORY_API_URL:-http://localhost:4000}" - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -PASS=0 -FAIL=0 -ERRORS=() - -# ─── Preflight checks ──────────────────────────────────── -echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" -echo -e "${BLUE} GooseFactory MCP Server — Tool Verification${NC}" -echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" -echo "" - -# Check API health -echo -n "Checking API health... " -HEALTH=$(curl -s "$API_URL/health" 2>/dev/null || echo "FAIL") -if echo "$HEALTH" | grep -q '"status":"ok"'; then - echo -e "${GREEN}✅ API healthy${NC}" -else - echo -e "${RED}❌ API not reachable at $API_URL${NC}" - echo " Start it with: cd packages/api && DEV_MODE=true npx tsx src/index.ts" - exit 1 -fi - -# Check MCP server exists -if [ ! -f "$MCP_SERVER" ]; then - echo -e "${RED}❌ MCP server not compiled at $MCP_SERVER${NC}" - echo " Build it with: cd packages/mcp-server && npx tsc" - exit 1 -fi -echo -e "MCP server: ${GREEN}$MCP_SERVER${NC}" -echo "" - -# ─── Helper: call MCP tool via stdio ───────────────────── -call_mcp_tool() { - local tool_name="$1" - local arguments="$2" - - printf '%s\n%s\n%s\n' \ - '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"mcp-test","version":"1.0"}}}' \ - '{"jsonrpc":"2.0","method":"notifications/initialized"}' \ - "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"$tool_name\",\"arguments\":$arguments}}" \ - | node "$MCP_SERVER" 2>/dev/null | tail -1 -} - -# ─── Helper: run test ──────────────────────────────────── -run_test() { - local num="$1" - local name="$2" - local args="$3" - local desc="$4" - - echo -n " $num. $name... " - - local result - result=$(call_mcp_tool "$name" "$args" 2>/dev/null) - - if [ -z "$result" ]; then - echo -e "${RED}❌ NO RESPONSE${NC}" - FAIL=$((FAIL + 1)) - ERRORS+=("$name: No response from MCP server") - return - fi - - # Check for JSON-RPC error - if echo "$result" | grep -q '"error"'; then - local err_msg - err_msg=$(echo "$result" | sed 's/.*"message":"\([^"]*\)".*/\1/' | head -c 100) - echo -e "${RED}❌ RPC ERROR: $err_msg${NC}" - FAIL=$((FAIL + 1)) - ERRORS+=("$name: $err_msg") - return - fi - - # Check for tool-level isError - if echo "$result" | grep -q '"isError":true'; then - local err_text - err_text=$(echo "$result" | sed 's/.*"text":"\([^"]*\).*/\1/' | head -c 120) - echo -e "${RED}❌ $err_text${NC}" - FAIL=$((FAIL + 1)) - ERRORS+=("$name: $err_text") - return - fi - - # Extract first 100 chars of text content for display - local preview - preview=$(echo "$result" | sed 's/.*"text":"\([^"]*\).*/\1/' | head -c 100) - echo -e "${GREEN}✅${NC} ${preview}" - PASS=$((PASS + 1)) -} - -# ─── Get test data from API ────────────────────────────── -echo -e "${YELLOW}Fetching test data from API...${NC}" - -# Get a pipeline ID -PIPELINE_ID=$(curl -s "$API_URL/v1/pipelines?limit=1" 2>/dev/null | sed 's/.*"id":"\([^"]*\)".*/\1/' | head -c 36) -echo " Pipeline ID: $PIPELINE_ID" - -# Create a test pipeline for advance/deploy/test operations -NEW_PIPELINE=$(curl -s -X POST "$API_URL/v1/pipelines" \ - -H 'Content-Type: application/json' \ - -d '{"name":"mcp-test-run-'"$(date +%s)"'","platform":"test","priority":"low"}' 2>/dev/null) -TEST_PIPELINE_ID=$(echo "$NEW_PIPELINE" | sed 's/.*"id":"\([^"]*\)".*/\1/' | head -c 36) -echo " Test Pipeline ID: $TEST_PIPELINE_ID" - -# Advance test pipeline to review (creates approval task with real UUID) -# intake -> scaffolding -> building -> testing (4 advances to get to review) -for i in 1 2 3; do - curl -s -X POST "$API_URL/v1/pipelines/$TEST_PIPELINE_ID/stages/advance" \ - -H 'Content-Type: application/json' -d '{"skipValidation":true}' > /dev/null 2>&1 -done -# This advance goes to review (requires approval) -> creates task -ADVANCE_RESULT=$(curl -s -X POST "$API_URL/v1/pipelines/$TEST_PIPELINE_ID/stages/advance" \ - -H 'Content-Type: application/json' -d '{"skipValidation":true}' 2>/dev/null) - -# Extract task ID from tasksCreated array - get the id after "tasksCreated" -TASK_ID=$(echo "$ADVANCE_RESULT" | node -e " - let d=''; process.stdin.on('data',c=>d+=c); process.stdin.on('end',()=>{ - try { - const j=JSON.parse(d); - const tasks = j.data?.tasksCreated || []; - if (tasks.length > 0) { console.log(tasks[0].id); } - else { console.log(''); } - } catch(e) { console.log(''); } - });" 2>/dev/null) - -if [ -z "$TASK_ID" ] || [ ${#TASK_ID} -lt 36 ]; then - echo " (No task from advance, checking pending tasks...)" - TASK_ID=$(curl -s "$API_URL/v1/tasks?status=pending&limit=5" | node -e " - let d=''; process.stdin.on('data',c=>d+=c); process.stdin.on('end',()=>{ - try { - const j=JSON.parse(d); - // Find a task with a valid RFC4122 UUID (variant byte 8-b) - const t = j.data?.find(t => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(t.id)); - if (t) console.log(t.id); else console.log(''); - } catch(e) { console.log(''); } - });" 2>/dev/null) -fi -echo " Task ID: $TASK_ID" - -# Create a second pipeline for the reject task -NEW_PIPELINE2=$(curl -s -X POST "$API_URL/v1/pipelines" \ - -H 'Content-Type: application/json' \ - -d '{"name":"mcp-reject-test-'"$(date +%s)"'","platform":"test","priority":"low"}' 2>/dev/null) -TEST_PIPELINE2_ID=$(echo "$NEW_PIPELINE2" | node -e " - let d=''; process.stdin.on('data',c=>d+=c); process.stdin.on('end',()=>{ - try { console.log(JSON.parse(d).data.id); } catch(e) { console.log(''); } - });" 2>/dev/null) - -# Advance pipeline2 to review -for i in 1 2 3; do - curl -s -X POST "$API_URL/v1/pipelines/$TEST_PIPELINE2_ID/stages/advance" \ - -H 'Content-Type: application/json' -d '{"skipValidation":true}' > /dev/null 2>&1 -done -REJECT_RESULT=$(curl -s -X POST "$API_URL/v1/pipelines/$TEST_PIPELINE2_ID/stages/advance" \ - -H 'Content-Type: application/json' -d '{"skipValidation":true}' 2>/dev/null) -REJECT_TASK_ID=$(echo "$REJECT_RESULT" | node -e " - let d=''; process.stdin.on('data',c=>d+=c); process.stdin.on('end',()=>{ - try { - const j=JSON.parse(d); - const tasks = j.data?.tasksCreated || []; - if (tasks.length > 0) { console.log(tasks[0].id); } - else { console.log(''); } - } catch(e) { console.log(''); } - });" 2>/dev/null) -echo " Reject Task ID: $REJECT_TASK_ID" -echo "" - -# ─── Run all 12 tests ──────────────────────────────────── -echo -e "${BLUE}Running 12 tool tests...${NC}" -echo "" - -# === Task Tools (3) === -echo -e "${YELLOW}Task Tools:${NC}" -run_test "01" "factory_get_pending_tasks" \ - '{"limit":5}' \ - "Get pending tasks" - -run_test "02" "factory_approve_task" \ - "{\"task_id\":\"$TASK_ID\",\"notes\":\"Test approval via script\"}" \ - "Approve a task" - -run_test "03" "factory_reject_task" \ - "{\"task_id\":\"${REJECT_TASK_ID:-$TASK_ID}\",\"reason\":\"Test rejection via script\",\"severity\":\"minor\"}" \ - "Reject a task" - -echo "" - -# === Pipeline Tools (4) === -echo -e "${YELLOW}Pipeline Tools:${NC}" -run_test "04" "factory_get_pipeline_status" \ - '{"status":"active"}' \ - "Get pipeline status" - -run_test "05" "factory_advance_stage" \ - "{\"pipeline_id\":\"$PIPELINE_ID\",\"skip_validation\":true,\"notes\":\"Test script advance\"}" \ - "Advance pipeline stage" - -run_test "06" "factory_create_pipeline" \ - "{\"name\":\"test-script-pipeline-$(date +%s)\",\"platform\":\"test\",\"priority\":\"low\"}" \ - "Create a new pipeline" - -run_test "07" "factory_search" \ - '{"query":"mcp","limit":5}' \ - "Search across entities" - -echo "" - -# === Operations Tools (4) === -echo -e "${YELLOW}Operations Tools:${NC}" -run_test "08" "factory_assign_priority" \ - "{\"entity_type\":\"pipeline\",\"entity_id\":\"$PIPELINE_ID\",\"priority\":\"high\",\"reason\":\"Test script priority change\"}" \ - "Assign priority" - -run_test "09" "factory_get_blockers" \ - '{}' \ - "Get blockers" - -run_test "10" "factory_run_tests" \ - "{\"pipeline_id\":\"$PIPELINE_ID\",\"test_type\":\"all\"}" \ - "Run tests" - -run_test "11" "factory_deploy" \ - "{\"pipeline_id\":\"$PIPELINE_ID\",\"target\":\"staging\",\"dry_run\":true}" \ - "Deploy (dry run)" - -echo "" - -# === Review Tools (1) === -echo -e "${YELLOW}Review Tools:${NC}" -run_test "12" "factory_request_review" \ - "{\"pipeline_id\":\"$PIPELINE_ID\",\"modal_type\":\"traffic-light\"}" \ - "Request review modal" - -echo "" - -# ─── Summary ───────────────────────────────────────────── -echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" -echo -e " Results: ${GREEN}$PASS passed${NC} / ${RED}$FAIL failed${NC} / 12 total" -echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" - -if [ ${#ERRORS[@]} -gt 0 ]; then - echo "" - echo -e "${RED}Failures:${NC}" - for err in "${ERRORS[@]}"; do - echo -e " ${RED}•${NC} $err" - done -fi - -echo "" -if [ $FAIL -eq 0 ]; then - echo -e "${GREEN}🎉 All 12 MCP tools are working!${NC}" - exit 0 -else - echo -e "${YELLOW}⚠️ $FAIL tool(s) need attention${NC}" - exit 1 -fi diff --git a/goosefactory/tests/integration/modal-to-learning.test.ts b/goosefactory/tests/integration/modal-to-learning.test.ts deleted file mode 100644 index b60a99d..0000000 --- a/goosefactory/tests/integration/modal-to-learning.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -/** - * Integration Test: Modal → Learning Pipeline - * - * Tests the full flow: - * 1. Mock modal postMessage feedback - * 2. Validate with shared Zod schemas - * 3. Transform to FeedbackEvent - * 4. Feed into learning pipeline - * 5. Verify output - */ - -import { parseModalMessage, modalResponseToFeedbackEvent } from "../../packages/shared/src/integration/modal-to-learning.js"; -import type { ModalResponse } from "../../packages/shared/src/types/feedback.js"; - -// ─── Test Helpers ─── - -let passed = 0; -let failed = 0; - -function assert(condition: boolean, message: string) { - if (condition) { - console.log(` ✅ ${message}`); - passed++; - } else { - console.error(` ❌ ${message}`); - failed++; - } -} - -// ─── Mock Data ─── - -const mockModalResponse: ModalResponse = { - type: "factory_modal_response", - modalType: "traffic-light", - modalVersion: "1.0.0", - pipelineId: "pipeline-123", - itemId: "item-456", - sessionId: "session-789", - timestamp: new Date().toISOString(), - responseTimeMs: 3500, - feedback: { - decision: { - decision: "approved", - reason: "Code looks clean, tests passing", - tags: ["clean-code", "tests-passing"], - }, - confidence: { - confidencePercent: 85, - zone: "confident", - wouldDelegateToAI: false, - needsExpertReview: false, - }, - }, - meta: { - timeToFirstInteractionMs: 1200, - timeToDecisionMs: 3500, - fieldsModified: ["decision", "confidence"], - scrollDepth: 0.8, - revisits: 0, - totalInteractions: 5, - viewportSize: { width: 1920, height: 1080 }, - deviceType: "desktop", - }, -}; - -// ─── Tests ─── - -console.log("\n🧪 Modal → Learning Integration Tests\n"); - -// Test 1: Valid modal message parsing -console.log("Test 1: Parse valid modal response"); -{ - const result = parseModalMessage(mockModalResponse); - assert(result.valid === true, "Should parse valid modal response"); - if (result.valid) { - assert(result.response.modalType === "traffic-light", "Modal type should be traffic-light"); - assert(result.response.feedback.decision?.decision === "approved", "Decision should be approved"); - } -} - -// Test 2: Invalid message (wrong type) -console.log("\nTest 2: Reject non-response messages"); -{ - const result = parseModalMessage({ type: "factory_modal_ready", modalType: "test", version: "1.0.0" }); - assert(result.valid === false, "Should reject non-response messages"); -} - -// Test 3: Invalid message (bad schema) -console.log("\nTest 3: Reject schema-invalid messages"); -{ - const result = parseModalMessage({ - type: "factory_modal_response", - modalType: "test", - // Missing required fields - }); - assert(result.valid === false, "Should reject schema-invalid messages"); -} - -// Test 4: Null/undefined input -console.log("\nTest 4: Handle null/undefined input"); -{ - assert(parseModalMessage(null).valid === false, "Should reject null"); - assert(parseModalMessage(undefined).valid === false, "Should reject undefined"); - assert(parseModalMessage("string").valid === false, "Should reject string"); -} - -// Test 5: Transform to FeedbackEvent -console.log("\nTest 5: Transform ModalResponse → FeedbackEvent"); -{ - const feedbackEvent = modalResponseToFeedbackEvent(mockModalResponse, { - workProductType: "mcp_server", - workProductId: "ghl-mcp", - workProductVersion: "abc123", - workProductSummary: "Go High Level MCP server", - pipelineStage: "review", - mcpServerType: "go-high-level", - }); - - assert(typeof feedbackEvent.id === "string", "Should have UUID id"); - assert(feedbackEvent.modalType === "traffic-light", "Modal type preserved"); - assert(feedbackEvent.pipelineId === "pipeline-123", "Pipeline ID preserved"); - assert(feedbackEvent.workProduct.type === "mcp_server", "Work product type set"); - assert(feedbackEvent.pipelineStage === "review", "Pipeline stage set"); - assert(feedbackEvent.feedback.decision?.decision === "approved", "Feedback preserved"); - assert(feedbackEvent.meta.timeOfDay !== undefined, "Time of day enriched"); - assert(feedbackEvent.meta.dayOfWeek !== undefined, "Day of week enriched"); -} - -// Test 6: Scores validation in FeedbackEvent -console.log("\nTest 6: Scores feedback type"); -{ - const responseWithScores: ModalResponse = { - ...mockModalResponse, - modalType: "report-card", - feedback: { - scores: [ - { dimension: "code_quality", score: 8, comment: "Clean" }, - { dimension: "test_coverage", score: 9 }, - { dimension: "documentation", score: 7 }, - ], - }, - }; - - const result = parseModalMessage(responseWithScores); - assert(result.valid === true, "Should accept scores feedback"); - if (result.valid) { - assert(result.response.feedback.scores?.length === 3, "Should have 3 scores"); - } -} - -// Test 7: Batch decisions feedback type -console.log("\nTest 7: Batch decisions feedback type"); -{ - const responseWithBatch: ModalResponse = { - ...mockModalResponse, - modalType: "tinder-swipe", - feedback: { - batchDecisions: [ - { itemId: "item-1", decision: "approve", timeMs: 1200, flippedForDetails: false }, - { itemId: "item-2", decision: "reject", timeMs: 3500, flippedForDetails: true }, - { itemId: "item-3", decision: "love", timeMs: 800, flippedForDetails: false }, - ], - }, - }; - - const result = parseModalMessage(responseWithBatch); - assert(result.valid === true, "Should accept batch decisions"); - if (result.valid) { - assert(result.response.feedback.batchDecisions?.length === 3, "Should have 3 batch decisions"); - } -} - -// ─── Summary ─── -console.log(`\n${"═".repeat(50)}`); -console.log(`Results: ${passed} passed, ${failed} failed out of ${passed + failed} assertions`); -console.log(`${"═".repeat(50)}\n`); - -if (failed > 0) { - process.exit(1); -} diff --git a/goosefactory/tsconfig.base.json b/goosefactory/tsconfig.base.json deleted file mode 100644 index ccbaee8..0000000 --- a/goosefactory/tsconfig.base.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "isolatedModules": true, - "verbatimModuleSyntax": false - } -} diff --git a/house-remodel/proposal.html b/house-remodel/proposal.html new file mode 100644 index 0000000..93dd86c --- /dev/null +++ b/house-remodel/proposal.html @@ -0,0 +1,2196 @@ + + + + + +Hartfield Residence — Whole Home Renovation Proposal + + + + + + + + + + + + +
    +
    +
    +
    Residential Renovation Proposal
    +

    The Hartfield
    Residence

    +

    42 Meadowbrook Lane, Asheville, NC 28801

    + +
    +
    +
    3,200
    +
    Square Feet
    +
    +
    +
    4 / 2.5
    +
    Bed / Bath
    +
    +
    +
    18
    +
    Week Timeline
    +
    +
    +
    6
    +
    Zones
    +
    +
    + +
    +
    Total Investment
    +
    $266,200
    +
    +
    +
    + Scroll +
    +
    +
    + + +
    +
    + 01 +

    Project Overview

    +

    A comprehensive transformation of a 1987 home into a modern luxury residence, preserving character while elevating every surface.

    +
    +
    + +
    +
    +
    🏠
    +
    Property
    +
    42 Meadowbrook Lane
    +
    +
    +
    📐
    +
    Year Built
    +
    1987 · 3,200 sq ft
    +
    +
    +
    📅
    +
    Project Start
    +
    April 7, 2026
    +
    +
    +
    ⏱️
    +
    Duration
    +
    18 Weeks
    +
    +
    +
    🔨
    +
    General Contractor
    +
    Summit Ridge Construction Co.
    +
    +
    +
    +
    Interior Design
    +
    Elara Interiors
    +
    +
    +
    + + +
    +
    + 02 +

    Room by Room

    +

    Every detail, every finish, every fixture — meticulously specified.

    +
    +
    + +
    +
    +
    + Kitchen Render +
    +
    +
    🍳 Full Renovation
    +

    Kitchen

    +
    + $68,000 + 28.4% of total +
    +

    A complete gut renovation transforming the dated kitchen into a chef's dream — anchored by a stunning quartz waterfall island and professional-grade appliances.

    + +
    Scope of Work
    +
      +
    • Full demo of existing cabinets, counters, and flooring
    • +
    • Custom white shaker cabinets — Wellborn Forest, full overlay, soft-close hinges
    • +
    • Quartz countertops — Calacatta Laza, 1.5" mitered waterfall edge on island
    • +
    • 48" × 36" island with prep sink and seating for 4
    • +
    • Kohler Whitehaven cast iron farmhouse sink, Brizo Litze faucet in luxe gold
    • +
    • 48" Sub-Zero refrigerator, 48" Wolf dual-fuel range, Cove dishwasher, Wolf speed oven
    • +
    • 3×12 white zellige tile backsplash in herringbone pattern
    • +
    • Under-cabinet LED lighting — Kichler
    • +
    • New electrical: 4 dedicated circuits, 4 additional outlets, under-cabinet circuit
    • +
    • 12× 4" LED wafer recessed lights on dimmer
    • +
    • 3× Visual Comfort Serge Mouille pendants over island
    • +
    • 5" white oak hardwood flooring, natural finish, matched to existing
    • +
    • Crown molding + base molding upgrade throughout
    • +
    • New drywall + paint: Benjamin Moore White Dove OC-17
    • +
    + +
    Key Materials
    +
    +
    Wellborn Forest Cabinetry
    +
    Calacatta Laza Quartz
    +
    Sub-Zero / Wolf / Cove
    +
    Brizo Litze (Luxe Gold)
    +
    White Zellige Tile
    +
    BM White Dove OC-17
    +
    +
    +
    +
    + + +
    +
    +
    + Master Bathroom Render +
    +
    +
    🛁 Full Gut Renovation
    +

    Master Bathroom

    +
    + $52,000 + 21.7% of total +
    +

    Completely gutted to the studs and rebuilt as a spa-worthy sanctuary with radiant heated floors, a freestanding soaking tub, and luxurious brushed gold fixtures.

    + +
    Scope of Work
    +
      +
    • Full gut to studs — complete demo of all existing finishes
    • +
    • 12×24 porcelain tile floor with Warmup radiant heat system
    • +
    • Walk-in shower: 4×8 large format porcelain wall tile, frameless 3/8" glass enclosure, linear drain
    • +
    • Freestanding tub: Kohler Abrazo 6' cast iron, deck-mounted faucet
    • +
    • Custom 84" built-in double vanity, painted cabinet, quartz top, under-mount sinks
    • +
    • 2× custom backlit mirrors, 42"×36" each
    • +
    • Plumbing: Brizo Virage collection throughout (brushed gold)
    • +
    • TOTO Drake II elongated toilet with Washlet+ bidet seat
    • +
    • Runtal wall-mount heated towel bar
    • +
    • Broan exhaust fan with humidity sensor and Bluetooth speaker
    • +
    • 2× tiled recessed niches in shower
    • +
    • Densarmor mold-resistant drywall throughout
    • +
    + +
    Key Materials
    +
    +
    Kohler Abrazo Tub
    +
    Brizo Virage (Brushed Gold)
    +
    TOTO Washlet+
    +
    Warmup Radiant Heat
    +
    Densarmor Drywall
    +
    Frameless Glass (3/8")
    +
    +
    +
    +
    + + +
    +
    +
    + Exterior Render +
    +
    +
    🏡 Curb Appeal Transformation
    +

    Exterior

    +
    + $41,000 + 17.1% of total +
    +

    A modern craftsman transformation — new siding, a welcoming covered porch, upgraded garage doors, and a complete front yard landscape redesign.

    + +
    Scope of Work
    +
      +
    • Full siding replacement: James Hardie HardieBoard lap siding + Hardie Trim, Monterey Taupe
    • +
    • New front door: Therma-Tru Benchmark 36" fiberglass with Emtek hardware
    • +
    • 2× 16' Clopay Coachman garage doors — windows, insulated
    • +
    • Covered porch addition: 12×16, Trex Transcend decking, craftsman columns
    • +
    • Full front yard landscape redesign: ornamental grasses, native plantings, mulch beds
    • +
    • Exterior trim paint: Sherwin-Williams Alabaster
    • +
    • Full gutter replacement with gutter guards
    • +
    • 6× Kichler Barrington exterior lanterns
    • +
    + +
    Key Materials
    +
    +
    James Hardie HardieBoard
    +
    Trex Transcend Decking
    +
    Clopay Coachman Doors
    +
    Therma-Tru Benchmark
    +
    SW Alabaster Trim
    +
    Kichler Barrington
    +
    +
    +
    +
    + + +
    +
    +
    + Master Bedroom Render +
    +
    +
    🛏️ Complete Renovation
    +

    Master Bedroom

    +
    + $34,500 + 14.4% of total +
    +

    Elevated from carpet and builder-grade to a warm, sophisticated retreat featuring a dramatic tray ceiling, navy shiplap accent wall, and custom California Closets.

    + +
    Scope of Work
    +
      +
    • Remove carpet, install 5" white oak hardwood to match main floor
    • +
    • Tray ceiling construction with rope lighting detail
    • +
    • Accent wall: 5/4 shiplap painted Benjamin Moore Hale Navy HC-154
    • +
    • California Closets: 2 walk-ins, full built-out with drawers, shelves, island
    • +
    • 2× Marvin Elevate double-hung windows, Low-E glass
    • +
    • Custom Roman shades with motorized blackout lining
    • +
    • Ceiling fan pre-wire, additional outlets on each nightstand wall
    • +
    • Visual Comfort signature series chandelier
    • +
    • BM White Dove throughout, 4.5" craftsman base, 3.5" door casings
    • +
    • New solid core interior door, 8'0" height
    • +
    + +
    Key Materials
    +
    +
    5" White Oak Hardwood
    +
    BM Hale Navy HC-154
    +
    California Closets
    +
    Marvin Elevate Windows
    +
    Visual Comfort Chandelier
    +
    Motorized Roman Shades
    +
    +
    +
    +
    + + +
    +
    +
    + Living Room Render +
    +
    +
    🛋️ Architectural Enhancement
    +

    Living Room

    +
    + $28,000 + 11.7% of total +
    +

    Architectural details take center stage — a hand-built coffered ceiling, rebuilt marble fireplace surround, and floor-to-ceiling custom built-ins create a timeless gathering space.

    + +
    Scope of Work
    +
      +
    • Coffered ceiling construction: 9-box grid, 6" beams, painted BM White Dove
    • +
    • Fireplace surround rebuild: 12" marble surround, new custom millwork mantel
    • +
    • Floor-to-ceiling built-in shelving flanking fireplace, painted BM Hale Navy
    • +
    • 5" white oak hardwood — continuous with kitchen and master
    • +
    • 3× Marvin Elevate casement windows
    • +
    • 16× 4" LED recessed lights on 3-zone dimmer system
    • +
    • 4.5" craftsman base, picture rail molding, crown molding
    • +
    • BM White Dove on ceiling and walls
    • +
    + +
    Key Materials
    +
    +
    Coffered Ceiling (9-box)
    +
    Marble Fireplace Surround
    +
    Custom Built-In Millwork
    +
    Marvin Elevate Casement
    +
    3-Zone LED Dimmers
    +
    BM White Dove OC-17
    +
    +
    +
    +
    + + +
    +
    +
    + Home Office Render +
    +
    +
    📚 Custom Build-Out
    +

    Home Office

    +
    + $18,500 + 7.7% of total +
    +

    A dedicated workspace worthy of serious work — floor-to-ceiling library shelving with a rolling ladder, a massive built-in desk, and elegant French doors for privacy.

    + +
    Scope of Work
    +
      +
    • Built-in library wall: floor-to-ceiling shelving, rolling ladder hardware, painted BM Hale Navy
    • +
    • 96" custom built-in desk with file drawers and cable management
    • +
    • 5" white oak hardwood flooring (continuous)
    • +
    • 42" board and batten wainscoting
    • +
    • Integrated LED shelf lighting + overhead fixture
    • +
    • 8 outlets (4 with USB), dedicated circuit for workstation
    • +
    • 2× Marvin Elevate French doors with obscure glass
    • +
    + +
    Key Materials
    +
    +
    Rolling Ladder Library
    +
    96" Custom Desk
    +
    Board & Batten (42")
    +
    Marvin French Doors
    +
    LED Shelf Lighting
    +
    BM Hale Navy HC-154
    +
    +
    +
    +
    +
    + + +
    +
    + 03 +

    Investment Summary

    +

    Transparent, itemized budgeting with a 10% contingency built in for peace of mind.

    +
    +
    + +
    +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Room / ZoneBudget% of Total
    🍳 Kitchen$68,00028.4%
    🛁 Master Bathroom$52,00021.7%
    🏡 Exterior$41,00017.1%
    🛏️ Master Bedroom$34,50014.4%
    🛋️ Living Room$28,00011.7%
    📚 Home Office$18,5007.7%
    🔒 Contingency (10%)$24,200
    TOTAL INVESTMENT$266,200100%
    +
    +
    + +
    +
    +
    + Kitchen + $68,000 +
    +
    +
    +
    +
    + Master Bathroom + $52,000 +
    +
    +
    +
    +
    + Exterior + $41,000 +
    +
    +
    +
    +
    + Master Bedroom + $34,500 +
    +
    +
    +
    +
    + Living Room + $28,000 +
    +
    +
    +
    +
    + Home Office + $18,500 +
    +
    +
    +
    +
    + + +
    +
    + 04 +

    Project Timeline

    +

    18-week phased construction schedule starting April 7, 2026. Target completion: August 10, 2026.

    +
    +
    + +
    +
    +
    +
    PHASE
    +
    +
    1
    +
    2
    +
    3
    +
    4
    +
    5
    +
    6
    +
    7
    +
    8
    +
    9
    +
    10
    +
    11
    +
    12
    +
    13
    +
    14
    +
    15
    +
    16
    +
    17
    +
    18
    +
    +
    + + +
    +
    🔨
    Demo + Structural
    +
    +
    +
    +
    +
    + + +
    +
    Rough MEP
    +
    +
    +
    +
    +
    +
    + + +
    +
    🧱
    Insulation + Drywall
    +
    +
    +
    +
    +
    +
    + + +
    +
    🪵
    Tile + Hardwood
    +
    +
    +
    +
    +
    +
    + + +
    +
    🗄️
    Cabinets + Millwork
    +
    +
    +
    +
    +
    +
    + + +
    +
    🔧
    Counters + Fixtures
    +
    +
    +
    +
    +
    +
    + + +
    +
    🎨
    Paint + Trim + Lighting
    +
    +
    +
    +
    +
    +
    + + +
    +
    Punch List + Walkthrough
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    + 05 +

    Your Team

    +

    A dedicated team of professionals committed to delivering an exceptional result.

    +
    +
    + +
    +
    +
    👷
    +
    Marcus Caldwell
    +
    Project Manager
    +
    + Summit Ridge Construction Co. + (828) 555-1234 + marcus@summitridgeco.com +
    +
    +
    +
    🎨
    +
    Camille Fontaine
    +
    Lead Designer
    +
    + Elara Interiors + (828) 555-5678 + camille@elarainteriors.com +
    +
    +
    +
    📋
    +
    Derek Ewing
    +
    Site Superintendent
    +
    + Summit Ridge Construction Co. + (828) 555-9012 + derek@summitridgeco.com +
    +
    +
    +
    + + +
    +
    + 06 +

    Materials & Finishes

    +

    Condensed specification sheet — every brand, model, and finish selected for this project.

    +
    +
    + +
    +
    +
    🎨 Paint & Wall Finishes
    +
    + Primary Wall Color + BM White Dove OC-17 +
    +
    + Accent Color + BM Hale Navy HC-154 +
    +
    + Exterior Trim + SW Alabaster SW 7008 +
    +
    + Master Bedroom Accent + 5/4 Shiplap — Hale Navy +
    +
    + Office Wainscoting + 42" Board & Batten +
    +
    + +
    +
    🪵 Flooring
    +
    + Hardwood (throughout) + 5" White Oak, Natural Finish +
    +
    + Master Bath Floor + 12×24 Porcelain + Radiant +
    +
    + Radiant Heat System + Warmup DCM-PRO +
    +
    + Exterior Decking + Trex Transcend +
    +
    + +
    +
    🚿 Plumbing Fixtures
    +
    + Kitchen Sink + Kohler Whitehaven (cast iron) +
    +
    + Kitchen Faucet + Brizo Litze — Luxe Gold +
    +
    + Bath Collection + Brizo Virage — Brushed Gold +
    +
    + Freestanding Tub + Kohler Abrazo 6' (cast iron) +
    +
    + Toilet + TOTO Drake II + Washlet+ +
    +
    + +
    +
    🍳 Appliances
    +
    + Refrigerator + 48" Sub-Zero +
    +
    + Range + 48" Wolf Dual-Fuel +
    +
    + Dishwasher + Cove +
    +
    + Speed Oven + Wolf +
    +
    + +
    +
    💡 Lighting
    +
    + Kitchen Pendants + Visual Comfort Serge Mouille ×3 +
    +
    + Master Chandelier + Visual Comfort Signature +
    +
    + Under-Cabinet + Kichler LED Strip +
    +
    + Recessed (Kitchen) + 12× 4" LED Wafer — Dimmer +
    +
    + Recessed (Living) + 16× 4" LED — 3-Zone Dimmer +
    +
    + Exterior + Kichler Barrington Lanterns ×6 +
    +
    + +
    +
    🪟 Windows, Doors & Exterior
    +
    + Windows + Marvin Elevate (Low-E) +
    +
    + French Doors (Office) + Marvin Elevate — Obscure Glass +
    +
    + Front Door + Therma-Tru 36" + Emtek HW +
    +
    + Siding + James Hardie — Monterey Taupe +
    +
    + Garage Doors + Clopay Coachman ×2 (16') +
    +
    +
    +
    + + +
    +
    + 07 +

    Warranty & Guarantees

    +

    Your investment is protected with comprehensive coverage from both our team and manufacturers.

    +
    +
    + +
    +
    +
    🛡️
    +
    Workmanship Warranty
    +
    5 Years
    +
    Summit Ridge Construction Co. warrants all labor and installation against defects in workmanship for a full five years from the date of project completion.
    +
    +
    +
    🏗️
    +
    Structural Warranty
    +
    10 Years
    +
    All structural modifications — including coffered ceiling, tray ceiling, and porch addition — are warranted for ten years against structural failure.
    +
    +
    +
    🔧
    +
    Plumbing & Electrical
    +
    3 Years
    +
    Rough and finish plumbing, electrical wiring, and HVAC modifications are warranted for three years. Fixtures carry their own manufacturer warranties.
    +
    +
    +
    🍳
    +
    Appliance Coverage
    +
    Manufacturer Terms
    +
    Sub-Zero (12 yr sealed, 5 yr full), Wolf (2 yr), Cove (5 yr), TOTO (1 yr). All warranty registrations completed by our team at install.
    +
    +
    +
    🪟
    +
    Windows & Doors
    +
    20 Years (Marvin)
    +
    Marvin Elevate windows carry a 20-year warranty on glass, 10-year on non-glass. Therma-Tru limited lifetime on fiberglass entry door.
    +
    +
    +
    🏠
    +
    Exterior Envelope
    +
    30 Years (Hardie)
    +
    James Hardie HardieBoard backed by a 30-year non-prorated limited warranty. Clopay garage doors carry a limited lifetime warranty.
    +
    +
    +
    + + +
    + + + + +
    + + + + + + \ No newline at end of file diff --git a/localbosses-app/.gitignore b/localbosses-app/.gitignore index 5ef6a52..a4891c9 100644 --- a/localbosses-app/.gitignore +++ b/localbosses-app/.gitignore @@ -1,41 +1,8 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files (can opt-in for committing if needed) -.env* - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts +node_modules/ +.next/ +dist/ +build/ +__pycache__/ +*.pyc +.env +.env.* diff --git a/localbosses-app/next-env.d.ts b/localbosses-app/next-env.d.ts new file mode 100644 index 0000000..c4b7818 --- /dev/null +++ b/localbosses-app/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import "./.next/dev/types/routes.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/localbosses-app/tsconfig.tsbuildinfo b/localbosses-app/tsconfig.tsbuildinfo new file mode 100644 index 0000000..0bc978e --- /dev/null +++ b/localbosses-app/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/typescript/lib/lib.es2023.d.ts","./node_modules/typescript/lib/lib.es2024.d.ts","./node_modules/typescript/lib/lib.esnext.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/typescript/lib/lib.es2023.array.d.ts","./node_modules/typescript/lib/lib.es2023.collection.d.ts","./node_modules/typescript/lib/lib.es2023.intl.d.ts","./node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2024.collection.d.ts","./node_modules/typescript/lib/lib.es2024.object.d.ts","./node_modules/typescript/lib/lib.es2024.promise.d.ts","./node_modules/typescript/lib/lib.es2024.regexp.d.ts","./node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2024.string.d.ts","./node_modules/typescript/lib/lib.esnext.array.d.ts","./node_modules/typescript/lib/lib.esnext.collection.d.ts","./node_modules/typescript/lib/lib.esnext.intl.d.ts","./node_modules/typescript/lib/lib.esnext.disposable.d.ts","./node_modules/typescript/lib/lib.esnext.promise.d.ts","./node_modules/typescript/lib/lib.esnext.decorators.d.ts","./node_modules/typescript/lib/lib.esnext.iterator.d.ts","./node_modules/typescript/lib/lib.esnext.float16.d.ts","./node_modules/typescript/lib/lib.esnext.error.d.ts","./node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/@types/react/global.d.ts","./node_modules/csstype/index.d.ts","./node_modules/@types/react/index.d.ts","./node_modules/next/dist/styled-jsx/types/css.d.ts","./node_modules/next/dist/styled-jsx/types/macro.d.ts","./node_modules/next/dist/styled-jsx/types/style.d.ts","./node_modules/next/dist/styled-jsx/types/global.d.ts","./node_modules/next/dist/styled-jsx/types/index.d.ts","./node_modules/next/dist/server/get-page-files.d.ts","./node_modules/@types/node/compatibility/disposable.d.ts","./node_modules/@types/node/compatibility/indexable.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/compatibility/index.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/@types/node/web-globals/events.d.ts","../node_modules/buffer/index.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/file.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/filereader.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.generated.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/sea.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/index.d.ts","./node_modules/@types/react/canary.d.ts","./node_modules/@types/react/experimental.d.ts","./node_modules/@types/react-dom/index.d.ts","./node_modules/@types/react-dom/canary.d.ts","./node_modules/@types/react-dom/experimental.d.ts","./node_modules/next/dist/lib/fallback.d.ts","./node_modules/next/dist/compiled/webpack/webpack.d.ts","./node_modules/next/dist/shared/lib/modern-browserslist-target.d.ts","./node_modules/next/dist/shared/lib/entry-constants.d.ts","./node_modules/next/dist/shared/lib/constants.d.ts","./node_modules/next/dist/server/config.d.ts","./node_modules/next/dist/lib/load-custom-routes.d.ts","./node_modules/next/dist/shared/lib/image-config.d.ts","./node_modules/next/dist/build/webpack/plugins/subresource-integrity-plugin.d.ts","./node_modules/next/dist/server/body-streams.d.ts","./node_modules/next/dist/server/lib/cache-control.d.ts","./node_modules/next/dist/lib/setup-exception-listeners.d.ts","./node_modules/next/dist/lib/worker.d.ts","./node_modules/next/dist/lib/constants.d.ts","./node_modules/next/dist/lib/bundler.d.ts","./node_modules/next/dist/server/lib/experimental/ppr.d.ts","./node_modules/next/dist/lib/page-types.d.ts","./node_modules/next/dist/build/segment-config/app/app-segment-config.d.ts","./node_modules/next/dist/build/segment-config/pages/pages-segment-config.d.ts","./node_modules/next/dist/build/analysis/get-page-static-info.d.ts","./node_modules/next/dist/build/webpack/loaders/get-module-build-info.d.ts","./node_modules/next/dist/build/webpack/plugins/middleware-plugin.d.ts","./node_modules/next/dist/server/require-hook.d.ts","./node_modules/next/dist/server/node-polyfill-crypto.d.ts","./node_modules/next/dist/server/node-environment-baseline.d.ts","./node_modules/next/dist/server/node-environment-extensions/error-inspect.d.ts","./node_modules/next/dist/server/node-environment-extensions/console-file.d.ts","./node_modules/next/dist/server/node-environment-extensions/console-exit.d.ts","./node_modules/next/dist/server/node-environment-extensions/console-dim.external.d.ts","./node_modules/next/dist/server/node-environment-extensions/unhandled-rejection.d.ts","./node_modules/next/dist/server/node-environment-extensions/random.d.ts","./node_modules/next/dist/server/node-environment-extensions/date.d.ts","./node_modules/next/dist/server/node-environment-extensions/web-crypto.d.ts","./node_modules/next/dist/server/node-environment-extensions/node-crypto.d.ts","./node_modules/next/dist/server/node-environment-extensions/fast-set-immediate.external.d.ts","./node_modules/next/dist/server/node-environment.d.ts","./node_modules/next/dist/build/page-extensions-type.d.ts","./node_modules/next/dist/server/route-kind.d.ts","./node_modules/next/dist/server/route-definitions/route-definition.d.ts","./node_modules/next/dist/server/route-definitions/app-page-route-definition.d.ts","./node_modules/next/dist/server/lib/cache-handlers/types.d.ts","./node_modules/next/dist/server/response-cache/types.d.ts","./node_modules/next/dist/server/resume-data-cache/cache-store.d.ts","./node_modules/next/dist/server/resume-data-cache/resume-data-cache.d.ts","./node_modules/next/dist/client/components/app-router-headers.d.ts","./node_modules/next/dist/server/render-result.d.ts","./node_modules/next/dist/server/instrumentation/types.d.ts","./node_modules/next/dist/lib/coalesced-function.d.ts","./node_modules/next/dist/shared/lib/router/utils/middleware-route-matcher.d.ts","./node_modules/next/dist/server/lib/router-utils/types.d.ts","./node_modules/next/dist/trace/types.d.ts","./node_modules/next/dist/trace/trace.d.ts","./node_modules/next/dist/trace/shared.d.ts","./node_modules/next/dist/trace/index.d.ts","./node_modules/next/dist/build/load-jsconfig.d.ts","./node_modules/@next/env/dist/index.d.ts","./node_modules/next/dist/build/webpack/plugins/telemetry-plugin/use-cache-tracker-utils.d.ts","./node_modules/next/dist/build/webpack/plugins/telemetry-plugin/telemetry-plugin.d.ts","./node_modules/next/dist/telemetry/storage.d.ts","./node_modules/next/dist/build/build-context.d.ts","./node_modules/next/dist/shared/lib/bloom-filter.d.ts","./node_modules/next/dist/build/webpack-config.d.ts","./node_modules/next/dist/build/swc/generated-native.d.ts","./node_modules/next/dist/build/swc/types.d.ts","./node_modules/next/dist/server/dev/parse-version-info.d.ts","./node_modules/next/dist/next-devtools/shared/types.d.ts","./node_modules/next/dist/server/dev/dev-indicator-server-state.d.ts","./node_modules/next/dist/next-devtools/dev-overlay/cache-indicator.d.ts","./node_modules/next/dist/server/lib/parse-stack.d.ts","./node_modules/next/dist/next-devtools/server/shared.d.ts","./node_modules/next/dist/next-devtools/shared/stack-frame.d.ts","./node_modules/next/dist/next-devtools/dev-overlay/utils/get-error-by-type.d.ts","./node_modules/@types/react/jsx-runtime.d.ts","./node_modules/next/dist/next-devtools/dev-overlay/container/runtime-error/render-error.d.ts","./node_modules/next/dist/next-devtools/dev-overlay/shared.d.ts","./node_modules/next/dist/server/dev/debug-channel.d.ts","./node_modules/next/dist/server/dev/hot-reloader-types.d.ts","./node_modules/next/dist/server/lib/i18n-provider.d.ts","./node_modules/next/dist/server/web/next-url.d.ts","./node_modules/next/dist/compiled/@edge-runtime/cookies/index.d.ts","./node_modules/next/dist/server/web/spec-extension/cookies.d.ts","./node_modules/next/dist/server/web/spec-extension/request.d.ts","./node_modules/next/dist/server/after/builtin-request-context.d.ts","./node_modules/next/dist/server/web/spec-extension/fetch-event.d.ts","./node_modules/next/dist/server/web/spec-extension/response.d.ts","./node_modules/next/dist/build/segment-config/middleware/middleware-config.d.ts","./node_modules/next/dist/server/web/types.d.ts","./node_modules/next/dist/build/webpack/plugins/pages-manifest-plugin.d.ts","./node_modules/next/dist/shared/lib/router/utils/parse-url.d.ts","./node_modules/next/dist/server/route-definitions/locale-route-definition.d.ts","./node_modules/next/dist/server/route-definitions/pages-route-definition.d.ts","./node_modules/next/dist/build/webpack/plugins/flight-manifest-plugin.d.ts","./node_modules/next/dist/build/webpack/plugins/next-font-manifest-plugin.d.ts","./node_modules/next/dist/shared/lib/deep-readonly.d.ts","./node_modules/next/dist/next-devtools/userspace/pages/pages-dev-overlay-setup.d.ts","./node_modules/next/dist/server/render.d.ts","./node_modules/next/dist/shared/lib/mitt.d.ts","./node_modules/next/dist/client/with-router.d.ts","./node_modules/next/dist/client/router.d.ts","./node_modules/next/dist/client/route-loader.d.ts","./node_modules/next/dist/client/page-loader.d.ts","./node_modules/next/dist/shared/lib/router/router.d.ts","./node_modules/next/dist/shared/lib/router-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/loadable-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/loadable.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/image-config-context.shared-runtime.d.ts","./node_modules/next/dist/client/components/readonly-url-search-params.d.ts","./node_modules/next/dist/shared/lib/hooks-client-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/head-manager-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/app-router-types.d.ts","./node_modules/next/dist/client/flight-data-helpers.d.ts","./node_modules/next/dist/client/components/router-reducer/ppr-navigations.d.ts","./node_modules/next/dist/client/components/segment-cache/types.d.ts","./node_modules/next/dist/client/components/segment-cache/navigation.d.ts","./node_modules/next/dist/client/components/segment-cache/cache-key.d.ts","./node_modules/next/dist/client/components/router-reducer/fetch-server-response.d.ts","./node_modules/next/dist/client/components/router-reducer/router-reducer-types.d.ts","./node_modules/next/dist/shared/lib/app-router-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/server-inserted-html.shared-runtime.d.ts","./node_modules/next/dist/server/route-modules/pages/vendored/contexts/entrypoints.d.ts","./node_modules/next/dist/server/route-modules/pages/module.compiled.d.ts","./node_modules/next/dist/build/templates/pages.d.ts","./node_modules/next/dist/server/route-modules/pages/module.d.ts","./node_modules/next/dist/server/route-modules/pages/builtin/_error.d.ts","./node_modules/next/dist/server/load-default-error-components.d.ts","./node_modules/next/dist/server/base-http/node.d.ts","./node_modules/next/dist/server/response-cache/index.d.ts","./node_modules/next/dist/server/route-definitions/pages-api-route-definition.d.ts","./node_modules/next/dist/server/route-matches/pages-api-route-match.d.ts","./node_modules/next/dist/server/route-matchers/route-matcher.d.ts","./node_modules/next/dist/server/route-matcher-providers/route-matcher-provider.d.ts","./node_modules/next/dist/server/route-matcher-managers/route-matcher-manager.d.ts","./node_modules/next/dist/server/normalizers/normalizer.d.ts","./node_modules/next/dist/server/normalizers/locale-route-normalizer.d.ts","./node_modules/next/dist/server/normalizers/request/pathname-normalizer.d.ts","./node_modules/next/dist/server/normalizers/request/suffix.d.ts","./node_modules/next/dist/server/normalizers/request/rsc.d.ts","./node_modules/next/dist/server/normalizers/request/next-data.d.ts","./node_modules/next/dist/server/normalizers/request/segment-prefix-rsc.d.ts","./node_modules/next/dist/build/static-paths/types.d.ts","./node_modules/next/dist/server/base-server.d.ts","./node_modules/next/dist/server/lib/async-callback-set.d.ts","./node_modules/next/dist/shared/lib/router/utils/route-regex.d.ts","./node_modules/next/dist/shared/lib/router/utils/route-matcher.d.ts","./node_modules/sharp/lib/index.d.ts","./node_modules/next/dist/server/image-optimizer.d.ts","./node_modules/next/dist/server/next-server.d.ts","./node_modules/next/dist/server/lib/types.d.ts","./node_modules/next/dist/server/lib/lru-cache.d.ts","./node_modules/next/dist/server/lib/dev-bundler-service.d.ts","./node_modules/next/dist/server/use-cache/cache-life.d.ts","./node_modules/next/dist/server/dev/static-paths-worker.d.ts","./node_modules/next/dist/server/dev/next-dev-server.d.ts","./node_modules/next/dist/server/next.d.ts","./node_modules/next/dist/server/lib/render-server.d.ts","./node_modules/next/dist/server/lib/router-server.d.ts","./node_modules/next/dist/shared/lib/router/utils/path-match.d.ts","./node_modules/next/dist/server/lib/router-utils/filesystem.d.ts","./node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.d.ts","./node_modules/next/dist/server/lib/router-utils/router-server-context.d.ts","./node_modules/next/dist/server/route-modules/route-module.d.ts","./node_modules/next/dist/server/load-components.d.ts","./node_modules/next/dist/server/web/adapter.d.ts","./node_modules/next/dist/server/app-render/types.d.ts","./node_modules/next/dist/build/webpack/loaders/metadata/types.d.ts","./node_modules/next/dist/build/webpack/loaders/next-app-loader/index.d.ts","./node_modules/next/dist/server/lib/app-dir-module.d.ts","./node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.d.ts","./node_modules/next/dist/server/async-storage/draft-mode-provider.d.ts","./node_modules/next/dist/server/web/spec-extension/adapters/headers.d.ts","./node_modules/next/dist/server/app-render/cache-signal.d.ts","./node_modules/next/dist/server/app-render/dynamic-rendering.d.ts","./node_modules/next/dist/server/request/fallback-params.d.ts","./node_modules/next/dist/server/app-render/work-unit-async-storage-instance.d.ts","./node_modules/next/dist/server/lib/lazy-result.d.ts","./node_modules/next/dist/server/lib/implicit-tags.d.ts","./node_modules/next/dist/server/app-render/staged-rendering.d.ts","./node_modules/next/dist/server/app-render/work-unit-async-storage.external.d.ts","./node_modules/next/dist/shared/lib/router/utils/parse-relative-url.d.ts","./node_modules/next/dist/server/app-render/app-render.d.ts","./node_modules/next/dist/server/route-modules/app-page/vendored/contexts/entrypoints.d.ts","./node_modules/next/dist/client/components/error-boundary.d.ts","./node_modules/next/dist/client/components/layout-router.d.ts","./node_modules/next/dist/client/components/render-from-template-context.d.ts","./node_modules/next/dist/server/app-render/action-async-storage-instance.d.ts","./node_modules/next/dist/server/app-render/action-async-storage.external.d.ts","./node_modules/next/dist/client/components/client-page.d.ts","./node_modules/next/dist/client/components/client-segment.d.ts","./node_modules/next/dist/server/request/search-params.d.ts","./node_modules/next/dist/client/components/hooks-server-context.d.ts","./node_modules/next/dist/client/components/http-access-fallback/error-boundary.d.ts","./node_modules/next/dist/lib/metadata/types/alternative-urls-types.d.ts","./node_modules/next/dist/lib/metadata/types/extra-types.d.ts","./node_modules/next/dist/lib/metadata/types/metadata-types.d.ts","./node_modules/next/dist/lib/metadata/types/manifest-types.d.ts","./node_modules/next/dist/lib/metadata/types/opengraph-types.d.ts","./node_modules/next/dist/lib/metadata/types/twitter-types.d.ts","./node_modules/next/dist/lib/metadata/types/metadata-interface.d.ts","./node_modules/next/dist/lib/metadata/types/resolvers.d.ts","./node_modules/next/dist/lib/metadata/types/icons.d.ts","./node_modules/next/dist/lib/metadata/resolve-metadata.d.ts","./node_modules/next/dist/lib/metadata/metadata.d.ts","./node_modules/next/dist/lib/framework/boundary-components.d.ts","./node_modules/next/dist/server/app-render/rsc/preloads.d.ts","./node_modules/next/dist/server/app-render/rsc/postpone.d.ts","./node_modules/next/dist/server/app-render/rsc/taint.d.ts","./node_modules/next/dist/shared/lib/segment-cache/segment-value-encoding.d.ts","./node_modules/next/dist/server/app-render/collect-segment-data.d.ts","./node_modules/next/dist/next-devtools/userspace/app/segment-explorer-node.d.ts","./node_modules/next/dist/server/app-render/entry-base.d.ts","./node_modules/next/dist/build/templates/app-page.d.ts","./node_modules/next/dist/build/rendering-mode.d.ts","./node_modules/@types/react/jsx-dev-runtime.d.ts","./node_modules/@types/react/compiler-runtime.d.ts","./node_modules/next/dist/server/route-modules/app-page/vendored/rsc/entrypoints.d.ts","./node_modules/@types/react-dom/client.d.ts","./node_modules/@types/react-dom/static.d.ts","./node_modules/@types/react-dom/server.d.ts","./node_modules/next/dist/server/route-modules/app-page/vendored/ssr/entrypoints.d.ts","./node_modules/next/dist/server/route-modules/app-page/module.d.ts","./node_modules/next/dist/server/route-modules/app-page/module.compiled.d.ts","./node_modules/next/dist/server/route-definitions/app-route-route-definition.d.ts","./node_modules/next/dist/server/async-storage/work-store.d.ts","./node_modules/next/dist/server/web/http.d.ts","./node_modules/next/dist/server/route-modules/app-route/shared-modules.d.ts","./node_modules/next/dist/client/components/redirect-status-code.d.ts","./node_modules/next/dist/client/components/redirect-error.d.ts","./node_modules/next/dist/build/templates/app-route.d.ts","./node_modules/next/dist/server/route-modules/app-route/module.d.ts","./node_modules/next/dist/server/route-modules/app-route/module.compiled.d.ts","./node_modules/next/dist/build/segment-config/app/app-segments.d.ts","./node_modules/next/dist/build/utils.d.ts","./node_modules/next/dist/server/lib/router-utils/build-prefetch-segment-data-route.d.ts","./node_modules/next/dist/build/turborepo-access-trace/types.d.ts","./node_modules/next/dist/build/turborepo-access-trace/result.d.ts","./node_modules/next/dist/build/turborepo-access-trace/helpers.d.ts","./node_modules/next/dist/build/turborepo-access-trace/index.d.ts","./node_modules/next/dist/export/routes/types.d.ts","./node_modules/next/dist/export/types.d.ts","./node_modules/next/dist/export/worker.d.ts","./node_modules/next/dist/build/worker.d.ts","./node_modules/next/dist/build/index.d.ts","./node_modules/next/dist/server/lib/incremental-cache/index.d.ts","./node_modules/next/dist/server/after/after.d.ts","./node_modules/next/dist/server/after/after-context.d.ts","./node_modules/next/dist/server/app-render/work-async-storage-instance.d.ts","./node_modules/next/dist/server/app-render/create-error-handler.d.ts","./node_modules/next/dist/shared/lib/action-revalidation-kind.d.ts","./node_modules/next/dist/server/app-render/work-async-storage.external.d.ts","./node_modules/next/dist/server/request/params.d.ts","./node_modules/next/dist/server/route-matches/route-match.d.ts","./node_modules/next/dist/server/request-meta.d.ts","./node_modules/next/dist/cli/next-test.d.ts","./node_modules/next/dist/shared/lib/size-limit.d.ts","./node_modules/next/dist/server/config-shared.d.ts","./node_modules/next/dist/server/base-http/index.d.ts","./node_modules/next/dist/server/api-utils/index.d.ts","./node_modules/next/dist/build/adapter/build-complete.d.ts","./node_modules/next/dist/types.d.ts","./node_modules/next/dist/shared/lib/html-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/utils.d.ts","./node_modules/next/dist/pages/_app.d.ts","./node_modules/next/app.d.ts","./node_modules/next/dist/server/web/spec-extension/unstable-cache.d.ts","./node_modules/next/dist/server/web/spec-extension/revalidate.d.ts","./node_modules/next/dist/server/web/spec-extension/unstable-no-store.d.ts","./node_modules/next/dist/server/use-cache/cache-tag.d.ts","./node_modules/next/cache.d.ts","./node_modules/next/dist/pages/_document.d.ts","./node_modules/next/document.d.ts","./node_modules/next/dist/shared/lib/dynamic.d.ts","./node_modules/next/dynamic.d.ts","./node_modules/next/dist/pages/_error.d.ts","./node_modules/next/error.d.ts","./node_modules/next/dist/shared/lib/head.d.ts","./node_modules/next/head.d.ts","./node_modules/next/dist/server/request/cookies.d.ts","./node_modules/next/dist/server/request/headers.d.ts","./node_modules/next/dist/server/request/draft-mode.d.ts","./node_modules/next/headers.d.ts","./node_modules/next/dist/shared/lib/get-img-props.d.ts","./node_modules/next/dist/client/image-component.d.ts","./node_modules/next/dist/shared/lib/image-external.d.ts","./node_modules/next/image.d.ts","./node_modules/next/dist/client/link.d.ts","./node_modules/next/link.d.ts","./node_modules/next/dist/client/components/unrecognized-action-error.d.ts","./node_modules/next/dist/client/components/redirect.d.ts","./node_modules/next/dist/client/components/not-found.d.ts","./node_modules/next/dist/client/components/forbidden.d.ts","./node_modules/next/dist/client/components/unauthorized.d.ts","./node_modules/next/dist/client/components/unstable-rethrow.server.d.ts","./node_modules/next/dist/client/components/unstable-rethrow.d.ts","./node_modules/next/dist/client/components/navigation.react-server.d.ts","./node_modules/next/dist/client/components/navigation.d.ts","./node_modules/next/navigation.d.ts","./node_modules/next/router.d.ts","./node_modules/next/dist/client/script.d.ts","./node_modules/next/script.d.ts","./node_modules/next/dist/server/web/spec-extension/user-agent.d.ts","./node_modules/next/dist/compiled/@edge-runtime/primitives/url.d.ts","./node_modules/next/dist/server/web/spec-extension/image-response.d.ts","./node_modules/next/dist/compiled/@vercel/og/satori/index.d.ts","./node_modules/next/dist/compiled/@vercel/og/emoji/index.d.ts","./node_modules/next/dist/compiled/@vercel/og/types.d.ts","./node_modules/next/dist/server/after/index.d.ts","./node_modules/next/dist/server/request/connection.d.ts","./node_modules/next/server.d.ts","./node_modules/next/types/global.d.ts","./node_modules/next/types/compiled.d.ts","./node_modules/next/types.d.ts","./node_modules/next/index.d.ts","./node_modules/next/image-types/global.d.ts","./.next/dev/types/routes.d.ts","./next-env.d.ts","./next.config.ts","./src/app/api/app-data/route.ts","./node_modules/@types/json-schema/index.d.ts","./node_modules/@ai-sdk/provider/dist/index.d.ts","./node_modules/zod/v4/core/json-schema.d.cts","./node_modules/zod/v4/core/standard-schema.d.cts","./node_modules/zod/v4/core/registries.d.cts","./node_modules/zod/v4/core/to-json-schema.d.cts","./node_modules/zod/v4/core/util.d.cts","./node_modules/zod/v4/core/versions.d.cts","./node_modules/zod/v4/core/schemas.d.cts","./node_modules/zod/v4/core/checks.d.cts","./node_modules/zod/v4/core/errors.d.cts","./node_modules/zod/v4/core/core.d.cts","./node_modules/zod/v4/core/parse.d.cts","./node_modules/zod/v4/core/regexes.d.cts","./node_modules/zod/v4/locales/ar.d.cts","./node_modules/zod/v4/locales/az.d.cts","./node_modules/zod/v4/locales/be.d.cts","./node_modules/zod/v4/locales/bg.d.cts","./node_modules/zod/v4/locales/ca.d.cts","./node_modules/zod/v4/locales/cs.d.cts","./node_modules/zod/v4/locales/da.d.cts","./node_modules/zod/v4/locales/de.d.cts","./node_modules/zod/v4/locales/en.d.cts","./node_modules/zod/v4/locales/eo.d.cts","./node_modules/zod/v4/locales/es.d.cts","./node_modules/zod/v4/locales/fa.d.cts","./node_modules/zod/v4/locales/fi.d.cts","./node_modules/zod/v4/locales/fr.d.cts","./node_modules/zod/v4/locales/fr-ca.d.cts","./node_modules/zod/v4/locales/he.d.cts","./node_modules/zod/v4/locales/hu.d.cts","./node_modules/zod/v4/locales/hy.d.cts","./node_modules/zod/v4/locales/id.d.cts","./node_modules/zod/v4/locales/is.d.cts","./node_modules/zod/v4/locales/it.d.cts","./node_modules/zod/v4/locales/ja.d.cts","./node_modules/zod/v4/locales/ka.d.cts","./node_modules/zod/v4/locales/kh.d.cts","./node_modules/zod/v4/locales/km.d.cts","./node_modules/zod/v4/locales/ko.d.cts","./node_modules/zod/v4/locales/lt.d.cts","./node_modules/zod/v4/locales/mk.d.cts","./node_modules/zod/v4/locales/ms.d.cts","./node_modules/zod/v4/locales/nl.d.cts","./node_modules/zod/v4/locales/no.d.cts","./node_modules/zod/v4/locales/ota.d.cts","./node_modules/zod/v4/locales/ps.d.cts","./node_modules/zod/v4/locales/pl.d.cts","./node_modules/zod/v4/locales/pt.d.cts","./node_modules/zod/v4/locales/ru.d.cts","./node_modules/zod/v4/locales/sl.d.cts","./node_modules/zod/v4/locales/sv.d.cts","./node_modules/zod/v4/locales/ta.d.cts","./node_modules/zod/v4/locales/th.d.cts","./node_modules/zod/v4/locales/tr.d.cts","./node_modules/zod/v4/locales/ua.d.cts","./node_modules/zod/v4/locales/uk.d.cts","./node_modules/zod/v4/locales/ur.d.cts","./node_modules/zod/v4/locales/uz.d.cts","./node_modules/zod/v4/locales/vi.d.cts","./node_modules/zod/v4/locales/zh-cn.d.cts","./node_modules/zod/v4/locales/zh-tw.d.cts","./node_modules/zod/v4/locales/yo.d.cts","./node_modules/zod/v4/locales/index.d.cts","./node_modules/zod/v4/core/doc.d.cts","./node_modules/zod/v4/core/api.d.cts","./node_modules/zod/v4/core/json-schema-processors.d.cts","./node_modules/zod/v4/core/json-schema-generator.d.cts","./node_modules/zod/v4/core/index.d.cts","./node_modules/zod/v4/classic/errors.d.cts","./node_modules/zod/v4/classic/parse.d.cts","./node_modules/zod/v4/classic/schemas.d.cts","./node_modules/zod/v4/classic/checks.d.cts","./node_modules/zod/v4/classic/compat.d.cts","./node_modules/zod/v4/classic/from-json-schema.d.cts","./node_modules/zod/v4/classic/iso.d.cts","./node_modules/zod/v4/classic/coerce.d.cts","./node_modules/zod/v4/classic/external.d.cts","./node_modules/zod/v4/classic/index.d.cts","./node_modules/zod/v4/index.d.cts","./node_modules/@standard-schema/spec/dist/index.d.ts","./node_modules/zod/v3/helpers/typealiases.d.cts","./node_modules/zod/v3/helpers/util.d.cts","./node_modules/zod/v3/zoderror.d.cts","./node_modules/zod/v3/locales/en.d.cts","./node_modules/zod/v3/errors.d.cts","./node_modules/zod/v3/helpers/parseutil.d.cts","./node_modules/zod/v3/helpers/enumutil.d.cts","./node_modules/zod/v3/helpers/errorutil.d.cts","./node_modules/zod/v3/helpers/partialutil.d.cts","./node_modules/zod/v3/standard-schema.d.cts","./node_modules/zod/v3/types.d.cts","./node_modules/zod/v3/external.d.cts","./node_modules/zod/v3/index.d.cts","./node_modules/eventsource-parser/dist/stream.d.ts","./node_modules/@ai-sdk/provider-utils/dist/index.d.ts","./node_modules/@ai-sdk/anthropic/dist/index.d.ts","./node_modules/@ai-sdk/gateway/dist/index.d.ts","./node_modules/@opentelemetry/api/build/src/baggage/internal/symbol.d.ts","./node_modules/@opentelemetry/api/build/src/baggage/types.d.ts","./node_modules/@opentelemetry/api/build/src/baggage/utils.d.ts","./node_modules/@opentelemetry/api/build/src/common/exception.d.ts","./node_modules/@opentelemetry/api/build/src/common/time.d.ts","./node_modules/@opentelemetry/api/build/src/common/attributes.d.ts","./node_modules/@opentelemetry/api/build/src/context/types.d.ts","./node_modules/@opentelemetry/api/build/src/context/context.d.ts","./node_modules/@opentelemetry/api/build/src/api/context.d.ts","./node_modules/@opentelemetry/api/build/src/diag/types.d.ts","./node_modules/@opentelemetry/api/build/src/diag/consolelogger.d.ts","./node_modules/@opentelemetry/api/build/src/api/diag.d.ts","./node_modules/@opentelemetry/api/build/src/metrics/observableresult.d.ts","./node_modules/@opentelemetry/api/build/src/metrics/metric.d.ts","./node_modules/@opentelemetry/api/build/src/metrics/meter.d.ts","./node_modules/@opentelemetry/api/build/src/metrics/noopmeter.d.ts","./node_modules/@opentelemetry/api/build/src/metrics/meterprovider.d.ts","./node_modules/@opentelemetry/api/build/src/api/metrics.d.ts","./node_modules/@opentelemetry/api/build/src/propagation/textmappropagator.d.ts","./node_modules/@opentelemetry/api/build/src/baggage/context-helpers.d.ts","./node_modules/@opentelemetry/api/build/src/api/propagation.d.ts","./node_modules/@opentelemetry/api/build/src/trace/attributes.d.ts","./node_modules/@opentelemetry/api/build/src/trace/trace_state.d.ts","./node_modules/@opentelemetry/api/build/src/trace/span_context.d.ts","./node_modules/@opentelemetry/api/build/src/trace/link.d.ts","./node_modules/@opentelemetry/api/build/src/trace/status.d.ts","./node_modules/@opentelemetry/api/build/src/trace/span.d.ts","./node_modules/@opentelemetry/api/build/src/trace/span_kind.d.ts","./node_modules/@opentelemetry/api/build/src/trace/spanoptions.d.ts","./node_modules/@opentelemetry/api/build/src/trace/tracer.d.ts","./node_modules/@opentelemetry/api/build/src/trace/tracer_options.d.ts","./node_modules/@opentelemetry/api/build/src/trace/proxytracer.d.ts","./node_modules/@opentelemetry/api/build/src/trace/tracer_provider.d.ts","./node_modules/@opentelemetry/api/build/src/trace/proxytracerprovider.d.ts","./node_modules/@opentelemetry/api/build/src/trace/samplingresult.d.ts","./node_modules/@opentelemetry/api/build/src/trace/sampler.d.ts","./node_modules/@opentelemetry/api/build/src/trace/trace_flags.d.ts","./node_modules/@opentelemetry/api/build/src/trace/internal/utils.d.ts","./node_modules/@opentelemetry/api/build/src/trace/spancontext-utils.d.ts","./node_modules/@opentelemetry/api/build/src/trace/invalid-span-constants.d.ts","./node_modules/@opentelemetry/api/build/src/trace/context-utils.d.ts","./node_modules/@opentelemetry/api/build/src/api/trace.d.ts","./node_modules/@opentelemetry/api/build/src/context-api.d.ts","./node_modules/@opentelemetry/api/build/src/diag-api.d.ts","./node_modules/@opentelemetry/api/build/src/metrics-api.d.ts","./node_modules/@opentelemetry/api/build/src/propagation-api.d.ts","./node_modules/@opentelemetry/api/build/src/trace-api.d.ts","./node_modules/@opentelemetry/api/build/src/index.d.ts","./node_modules/ai/dist/index.d.ts","./src/lib/types.ts","./src/lib/channels.ts","./src/lib/app-intakes.ts","./src/app/api/chat/route.ts","./src/app/api/credentials/route.ts","./src/lib/ghl-demo-data.ts","./src/app/api/mcp-apps/route.ts","./src/lib/reonomy-service.ts","./src/app/api/mcp-tools/route.ts","./src/app/api/workflow-ops/route.ts","./node_modules/clsx/clsx.d.mts","./node_modules/tailwind-merge/dist/types.d.ts","./src/lib/utils.ts","./src/hooks/usechat.ts","./src/hooks/usemcpbridge.ts","./src/hooks/usethreadchat.ts","./src/lib/appnames.ts","./src/app/layout.tsx","./node_modules/lucide-react/dist/lucide-react.d.ts","./src/components/layout/serversidebar.tsx","./src/components/layout/channelsidebar.tsx","./node_modules/@types/unist/index.d.ts","./node_modules/@types/hast/index.d.ts","./node_modules/vfile-message/lib/index.d.ts","./node_modules/vfile-message/index.d.ts","./node_modules/vfile/lib/index.d.ts","./node_modules/vfile/index.d.ts","./node_modules/unified/lib/callable-instance.d.ts","./node_modules/trough/lib/index.d.ts","./node_modules/trough/index.d.ts","./node_modules/unified/lib/index.d.ts","./node_modules/unified/index.d.ts","./node_modules/@types/mdast/index.d.ts","./node_modules/mdast-util-to-hast/lib/state.d.ts","./node_modules/mdast-util-to-hast/lib/footer.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/blockquote.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/break.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/code.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/delete.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/emphasis.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/footnote-reference.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/heading.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/html.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/image-reference.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/image.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/inline-code.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/link-reference.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/link.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/list-item.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/list.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/paragraph.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/root.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/strong.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/table.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/table-cell.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/table-row.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/text.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/thematic-break.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/index.d.ts","./node_modules/mdast-util-to-hast/lib/index.d.ts","./node_modules/mdast-util-to-hast/index.d.ts","./node_modules/remark-rehype/lib/index.d.ts","./node_modules/remark-rehype/index.d.ts","./node_modules/react-markdown/lib/index.d.ts","./node_modules/react-markdown/index.d.ts","./node_modules/micromark-util-types/index.d.ts","./node_modules/micromark-extension-gfm-footnote/lib/html.d.ts","./node_modules/micromark-extension-gfm-footnote/lib/syntax.d.ts","./node_modules/micromark-extension-gfm-footnote/index.d.ts","./node_modules/micromark-extension-gfm-strikethrough/lib/html.d.ts","./node_modules/micromark-extension-gfm-strikethrough/lib/syntax.d.ts","./node_modules/micromark-extension-gfm-strikethrough/index.d.ts","./node_modules/micromark-extension-gfm/index.d.ts","./node_modules/mdast-util-from-markdown/lib/types.d.ts","./node_modules/mdast-util-from-markdown/lib/index.d.ts","./node_modules/mdast-util-from-markdown/index.d.ts","./node_modules/mdast-util-to-markdown/lib/types.d.ts","./node_modules/mdast-util-to-markdown/lib/index.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/blockquote.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/break.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/code.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/definition.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/emphasis.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/heading.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/html.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/image.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/image-reference.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/inline-code.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/link.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/link-reference.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/list.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/list-item.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/paragraph.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/root.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/strong.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/text.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/thematic-break.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/index.d.ts","./node_modules/mdast-util-to-markdown/index.d.ts","./node_modules/mdast-util-gfm-footnote/lib/index.d.ts","./node_modules/mdast-util-gfm-footnote/index.d.ts","./node_modules/markdown-table/index.d.ts","./node_modules/mdast-util-gfm-table/lib/index.d.ts","./node_modules/mdast-util-gfm-table/index.d.ts","./node_modules/mdast-util-gfm/lib/index.d.ts","./node_modules/mdast-util-gfm/index.d.ts","./node_modules/remark-gfm/lib/index.d.ts","./node_modules/remark-gfm/index.d.ts","./src/components/mcp/mcpappframe.tsx","./src/components/mcp/mcpappcard.tsx","./src/components/chat/message.tsx","./src/components/chat/typingindicator.tsx","./src/components/chat/messagelist.tsx","./src/components/chat/chatinput.tsx","./src/components/mcp/mcpapptoolbar.tsx","./src/components/workflow/credentialconnector.tsx","./src/components/chat/chatview.tsx","./src/components/layout/applayout.tsx","./src/app/page.tsx","./.next/types/routes.d.ts","./.next/types/validator.ts","./.next/dev/types/cache-life.d.ts","./.next/dev/types/validator.ts","./node_modules/@types/ms/index.d.ts","./node_modules/@types/debug/index.d.ts","./node_modules/@types/estree/index.d.ts","./node_modules/@types/estree-jsx/index.d.ts","./node_modules/@types/json5/index.d.ts","../node_modules/@types/phoenix/index.d.ts","../node_modules/@types/tough-cookie/index.d.ts","../node_modules/@types/ws/index.d.ts","../node_modules/@types/yauzl/index.d.ts","../../../node_modules/@types/connect/index.d.ts","../../../node_modules/@types/body-parser/index.d.ts","../../../node_modules/@types/cors/index.d.ts","../../../node_modules/@types/send/index.d.ts","../../../node_modules/@types/qs/index.d.ts","../../../node_modules/@types/range-parser/index.d.ts","../../../node_modules/@types/express-serve-static-core/index.d.ts","../../../node_modules/@types/http-errors/index.d.ts","../../../node_modules/@types/serve-static/index.d.ts","../../../node_modules/@types/express/index.d.ts"],"fileIdsList":[[97,144,461,462,463,464],[97,144],[97,144,270,505,508,511,514,665,666,668,670,671,679,780],[97,144,270,505,508,514,665,666,668,670,671,679,780,781],[97,144,509,510,511],[97,144,270,509],[97,144,516,594,610],[97,144,516,610],[97,144,516,594,595,608,609],[97,144,515],[97,144,619],[97,144,622],[97,144,627,629],[97,144,615,619,631,632],[97,144,642,645,651,653],[97,144,614,619],[97,144,613],[97,144,614],[97,144,621],[97,144,624],[97,144,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,654,655,656,657,658,659],[97,144,630],[97,144,626],[97,144,627],[97,144,618,619,625],[97,144,626,627],[97,144,633],[97,144,654],[97,144,618],[97,144,619,636,639],[97,144,635],[97,144,636],[97,144,634,636],[97,144,619,639,641,642,643],[97,144,642,643,645],[97,144,619,634,637,640,647],[97,144,634,635],[97,144,616,617,634,636,637,638],[97,144,636,639],[97,144,617,634,637,640],[97,144,619,639,641],[97,144,642,643],[97,144,785],[97,144,787,788],[97,144,683],[97,141,144],[97,143,144],[144],[97,144,149,177],[97,144,145,150,155,163,174,185],[97,144,145,146,155,163],[92,93,94,97,144],[97,144,147,186],[97,144,148,149,156,164],[97,144,149,174,182],[97,144,150,152,155,163],[97,143,144,151],[97,144,152,153],[97,144,154,155],[97,143,144,155],[97,144,155,156,157,174,185],[97,144,155,156,157,170,174,177],[97,144,152,155,158,163,174,185],[97,144,155,156,158,159,163,174,182,185],[97,144,158,160,174,182,185],[95,96,97,98,99,100,101,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191],[97,144,155,161],[97,144,162,185,190],[97,144,152,155,163,174],[97,144,164],[97,144,165],[97,143,144,166],[97,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191],[97,144,168],[97,144,169],[97,144,155,170,171],[97,144,170,172,186,188],[97,144,155,174,175,177],[97,144,176,177],[97,144,174,175],[97,144,177],[97,144,178],[97,141,144,174,179],[97,144,155,180,181],[97,144,180,181],[97,144,149,163,174,182],[97,144,183],[97,144,163,184],[97,144,158,169,185],[97,144,149,186],[97,144,174,187],[97,144,162,188],[97,144,189],[97,139,144],[97,139,144,155,157,166,174,177,185,188,190],[97,144,174,191],[85,89,97,144,193,194,195,197,456,502],[85,97,144],[85,89,97,144,193,194,195,196,413,456,502],[85,89,97,144,193,194,196,197,456,502],[85,97,144,197,413,414],[85,97,144,197,413],[85,89,97,144,194,195,196,197,456,502],[85,89,97,144,193,195,196,197,456,502],[83,84,97,144],[97,144,158,516,594,610,612,660],[97,144,727,730,733,735,736,737],[97,144,694,722,727,730,733,735,737],[97,144,694,722,727,730,733,737],[97,144,760,761,765],[97,144,737,760,762,765],[97,144,737,760,762,764],[97,144,694,722,737,760,762,763,765],[97,144,762,765,766],[97,144,737,760,762,765,767],[97,144,684,694,695,696,720,721,722],[97,144,684,695,722],[97,144,684,694,695,722],[97,144,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719],[97,144,684,688,694,696,722],[97,144,738,739,759],[97,144,694,722,760,762,765],[97,144,694,722],[97,144,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758],[97,144,683,694,722],[97,144,727,728,729,733,737],[97,144,727,730,733,737],[97,144,727,730,731,732,737],[97,144,459],[97,144,202,204,208,219,409,439,452],[97,144,204,214,215,216,218,452],[97,144,204,251,253,255,256,259,452,454],[97,144,204,208,210,211,212,242,337,409,429,430,438,452,454],[97,144,452],[97,144,215,307,418,427,447],[97,144,204],[97,144,198,307,447],[97,144,261],[97,144,260,452],[97,144,158,407,418,507],[97,144,158,375,387,427,446],[97,144,158,318],[97,144,432],[97,144,431,432,433],[97,144,431],[91,97,144,158,198,204,208,211,213,215,219,220,233,234,261,337,348,428,439,452,456],[97,144,202,204,217,251,252,257,258,452,507],[97,144,217,507],[97,144,202,234,362,452,507],[97,144,507],[97,144,204,217,218,507],[97,144,254,507],[97,144,220,429,437],[97,144,169,270,447],[97,144,270,447],[85,97,144,270],[85,97,144,379],[97,144,305,315,316,447,484,491],[97,144,304,424,485,486,487,488,490],[97,144,423],[97,144,423,424],[97,144,242,307,308,312],[97,144,307],[97,144,307,311,313],[97,144,307,308,309,310],[97,144,489],[85,97,144,205,478],[85,97,144,185],[85,97,144,217,297],[85,97,144,217,439],[97,144,295,299],[85,97,144,296,458],[85,89,97,144,158,192,193,194,195,196,197,456,500,501],[97,144,158],[97,144,158,208,241,293,338,359,361,434,435,439,452,453],[97,144,233,436],[97,144,456],[97,144,203],[85,97,144,364,377,386,396,398,446],[97,144,169,364,377,395,396,397,446,506],[97,144,389,390,391,392,393,394],[97,144,391],[97,144,395],[97,144,268,269,270,272],[85,97,144,262,263,264,265,271],[97,144,268,271],[97,144,266],[97,144,267],[85,97,144,270,296,458],[85,97,144,270,457,458],[85,97,144,270,458],[97,144,338,441],[97,144,441],[97,144,158,453,458],[97,144,383],[97,143,144,382],[97,144,243,307,324,361,370,373,375,376,417,446,449,453],[97,144,289,307,404],[97,144,375,446],[85,97,144,375,380,381,383,384,385,386,387,388,399,400,401,402,403,405,406,446,447,507],[97,144,369],[97,144,158,169,205,241,244,265,290,291,338,348,359,360,417,440,452,453,454,456,507],[97,144,446],[97,143,144,215,291,348,372,440,442,443,444,445,453],[97,144,375],[97,143,144,241,278,324,365,366,367,368,369,370,371,373,374,446,447],[97,144,158,278,279,365,453,454],[97,144,215,338,348,361,440,446,453],[97,144,158,452,454],[97,144,158,174,449,453,454],[97,144,158,169,185,198,208,217,243,244,246,275,280,285,289,290,291,293,322,324,326,329,331,334,335,336,337,359,361,439,440,447,449,452,453,454],[97,144,158,174],[97,144,204,205,206,213,449,450,451,456,458,507],[97,144,202,452],[97,144,274],[97,144,158,174,185,236,259,261,262,263,264,265,272,273,507],[97,144,169,185,198,236,251,284,285,286,322,323,324,329,337,338,344,347,349,359,361,440,447,449,452],[97,144,213,220,233,337,348,440,452],[97,144,158,185,205,208,324,342,449,452],[97,144,363],[97,144,158,274,345,346,356],[97,144,449,452],[97,144,370,372],[97,144,291,324,439,458],[97,144,158,169,247,251,323,329,344,347,351,449],[97,144,158,220,233,251,352],[97,144,204,246,354,439,452],[97,144,158,185,265,452],[97,144,158,217,245,246,247,256,274,353,355,439,452],[91,97,144,158,291,358,456,458],[97,144,321,359],[97,144,158,169,185,208,219,220,233,243,244,280,284,285,286,290,322,323,324,326,338,339,341,343,359,361,439,440,447,448,449,458],[97,144,158,174,220,344,350,356,449],[97,144,223,224,225,226,227,228,229,230,231,232],[97,144,275,330],[97,144,332],[97,144,330],[97,144,332,333],[97,144,158,208,211,241,242,453],[97,144,158,169,203,205,243,289,290,291,292,320,359,449,454,456,458],[97,144,158,169,185,207,242,292,324,370,440,448,453],[97,144,365],[97,144,366],[97,144,307,337,417],[97,144,367],[97,144,235,239],[97,144,158,208,235,243],[97,144,238,239],[97,144,240],[97,144,235,236],[97,144,235,287],[97,144,235],[97,144,275,328,448],[97,144,327],[97,144,236,447,448],[97,144,325,448],[97,144,236,447],[97,144,417],[97,144,208,237,243,291,307,324,358,361,364,370,377,378,408,409,412,416,439,449,453],[97,144,300,303,305,306,315,316],[85,97,144,195,197,270,410,411],[85,97,144,195,197,270,410,411,415],[97,144,426],[97,144,215,279,291,358,361,375,383,387,419,420,421,422,424,425,428,439,446,452],[97,144,315],[97,144,158,320],[97,144,320],[97,144,158,243,288,293,317,319,358,449,456,458],[97,144,300,301,302,303,305,306,315,316,457],[91,97,144,158,169,185,235,236,244,290,291,324,356,357,359,439,440,449,452,453,456],[97,144,279,281,284,440],[97,144,158,275,452],[97,144,278,375],[97,144,277],[97,144,279,280],[97,144,276,278,452],[97,144,158,207,279,281,282,283,452,453],[85,97,144,307,314,447],[97,144,200,201],[85,97,144,205],[85,97,144,304,447],[85,91,97,144,290,291,456,458],[97,144,205,478,479],[85,97,144,299],[85,97,144,169,185,203,258,294,296,298,458],[97,144,217,447,453],[97,144,340,447],[85,97,144,156,158,169,202,203,253,299,456,457],[85,97,144,193,194,195,196,197,456,502],[85,86,87,88,89,97,144],[97,144,149],[97,144,248,249,250],[97,144,248],[85,89,97,144,158,160,169,192,193,194,195,196,197,198,203,244,351,395,454,455,458,502],[97,144,466],[97,144,468],[97,144,470],[97,144,472],[97,144,474,475,476],[97,144,480],[90,97,144,460,465,467,469,471,473,477,481,483,493,494,496,505,506,507,508],[97,144,482],[97,144,492],[97,144,296],[97,144,495],[97,143,144,279,281,282,284,497,498,499,502,503,504],[97,144,192],[97,144,725],[85,97,144,684,693,722,724],[97,144,734,767,768],[97,144,769],[97,144,722,723],[97,144,684,688,693,694,722],[97,144,174,192],[97,144,690],[97,111,115,144,185],[97,111,144,174,185],[97,106,144],[97,108,111,144,182,185],[97,144,163,182],[97,106,144,192],[97,108,111,144,163,185],[97,103,104,107,110,144,155,174,185],[97,111,118,144],[97,103,109,144],[97,111,132,133,144],[97,107,111,144,177,185,192],[97,132,144,192],[97,105,106,144,192],[97,111,144],[97,105,106,107,108,109,110,111,112,113,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,133,134,135,136,137,138,144],[97,111,126,144],[97,111,118,119,144],[97,109,111,119,120,144],[97,110,144],[97,103,106,111,144],[97,111,115,119,120,144],[97,115,144],[97,109,111,114,144,185],[97,103,108,111,118,144],[97,144,174],[97,106,111,132,144,190,192],[97,144,688,692],[97,144,683,688,689,691,693],[97,144,685],[97,144,686,687],[97,144,683,686,688],[97,144,598,599],[97,144,596,597,598,600,601,606],[97,144,597,598],[97,144,606],[97,144,607],[97,144,598],[97,144,596,597,598,601,602,603,604,605],[97,144,596,597,608],[97,144,583],[97,144,583,586],[97,144,578,581,583,584,585,586,587,588,589,590,591],[97,144,517,519,586],[97,144,592],[97,144,583,584],[97,144,518,583,585],[97,144,519,521,523,524,525,526],[97,144,521,523,525,526],[97,144,521,523,525],[97,144,518,521,523,524,526],[97,144,517,519,520,521,522,523,524,525,526,527,528,578,579,580,581,582],[97,144,517,519,520,523],[97,144,519,520,523],[97,144,523,526],[97,144,517,518,520,521,522,524,525,526],[97,144,517,518,519,523,583],[97,144,523,524,525,526],[97,144,593],[97,144,525],[97,144,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577],[97,144,270],[97,144,156,165,270,611,661,663,664],[97,144,156,165,270,505,514,667],[97,144,270,505,669],[97,144,270,779],[85,97,144,270,680],[85,97,144,270,662,663,664,674,675,677,678,680,771,774,775,776,777],[97,144,270,662,674,680,726,769,771],[85,97,144,270,662,772,773],[85,97,144,270,681,682,778],[85,97,144,270,662,663,674,680],[97,144,270,674,680],[85,97,144,270,662,680,770],[85,97,144,270,676],[85,97,144,270,678,680],[85,97,144,270,662,680],[85,97,144,270,662,674],[97,144,270,662],[97,144,145,156,270],[97,144,270,672,673],[97,144,155,158,160,163,174,182,185,191,192],[97,144,155,174,192],[97,144,158,192,794],[97,144,158,192],[97,144,155,158,192,797,798,799],[97,144,795,800,802],[97,144,156,174,192],[97,144,158,192,801]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"27bdc30a0e32783366a5abeda841bc22757c1797de8681bbe81fbc735eeb1c10","impliedFormat":1},{"version":"8fd575e12870e9944c7e1d62e1f5a73fcf23dd8d3a321f2a2c74c20d022283fe","impliedFormat":1},{"version":"2ab096661c711e4a81cc464fa1e6feb929a54f5340b46b0a07ac6bbf857471f0","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2e80ee7a49e8ac312cc11b77f1475804bee36b3b2bc896bead8b6e1266befb43","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"df83c2a6c73228b625b0beb6669c7ee2a09c914637e2d35170723ad49c0f5cd4","affectsGlobalScope":true,"impliedFormat":1},{"version":"436aaf437562f276ec2ddbee2f2cdedac7664c1e4c1d2c36839ddd582eeb3d0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e3c06ea092138bf9fa5e874a1fdbc9d54805d074bee1de31b99a11e2fec239d","affectsGlobalScope":true,"impliedFormat":1},{"version":"87dc0f382502f5bbce5129bdc0aea21e19a3abbc19259e0b43ae038a9fc4e326","affectsGlobalScope":true,"impliedFormat":1},{"version":"b1cb28af0c891c8c96b2d6b7be76bd394fddcfdb4709a20ba05a7c1605eea0f9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2fef54945a13095fdb9b84f705f2b5994597640c46afeb2ce78352fab4cb3279","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac77cb3e8c6d3565793eb90a8373ee8033146315a3dbead3bde8db5eaf5e5ec6","affectsGlobalScope":true,"impliedFormat":1},{"version":"56e4ed5aab5f5920980066a9409bfaf53e6d21d3f8d020c17e4de584d29600ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ece9f17b3866cc077099c73f4983bddbcb1dc7ddb943227f1ec070f529dedd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a6282c8827e4b9a95f4bf4f5c205673ada31b982f50572d27103df8ceb8013c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1c9319a09485199c1f7b0498f2988d6d2249793ef67edda49d1e584746be9032","affectsGlobalScope":true,"impliedFormat":1},{"version":"e3a2a0cee0f03ffdde24d89660eba2685bfbdeae955a6c67e8c4c9fd28928eeb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811c71eee4aa0ac5f7adf713323a5c41b0cf6c4e17367a34fbce379e12bbf0a4","affectsGlobalScope":true,"impliedFormat":1},{"version":"51ad4c928303041605b4d7ae32e0c1ee387d43a24cd6f1ebf4a2699e1076d4fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"60037901da1a425516449b9a20073aa03386cce92f7a1fd902d7602be3a7c2e9","affectsGlobalScope":true,"impliedFormat":1},{"version":"d4b1d2c51d058fc21ec2629fff7a76249dec2e36e12960ea056e3ef89174080f","affectsGlobalScope":true,"impliedFormat":1},{"version":"22adec94ef7047a6c9d1af3cb96be87a335908bf9ef386ae9fd50eeb37f44c47","affectsGlobalScope":true,"impliedFormat":1},{"version":"196cb558a13d4533a5163286f30b0509ce0210e4b316c56c38d4c0fd2fb38405","affectsGlobalScope":true,"impliedFormat":1},{"version":"73f78680d4c08509933daf80947902f6ff41b6230f94dd002ae372620adb0f60","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5239f5c01bcfa9cd32f37c496cf19c61d69d37e48be9de612b541aac915805b","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"7e29f41b158de217f94cb9676bf9cbd0cd9b5a46e1985141ed36e075c52bf6ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"b9b8a0e45c7b6172a689aee1d7662b7e86994b1648b392efee24b0b557f81126","impliedFormat":1},{"version":"acd8fd5090ac73902278889c38336ff3f48af6ba03aa665eb34a75e7ba1dccc4","impliedFormat":1},{"version":"d6258883868fb2680d2ca96bc8b1352cab69874581493e6d52680c5ffecdb6cc","impliedFormat":1},{"version":"1b61d259de5350f8b1e5db06290d31eaebebc6baafd5f79d314b5af9256d7153","impliedFormat":1},{"version":"f258e3960f324a956fc76a3d3d9e964fff2244ff5859dcc6ce5951e5413ca826","impliedFormat":1},{"version":"643f7232d07bf75e15bd8f658f664d6183a0efaca5eb84b48201c7671a266979","impliedFormat":1},{"version":"21da358700a3893281ce0c517a7a30cbd46be020d9f0c3f2834d0a8ad1f5fc75","impliedFormat":1},{"version":"70521b6ab0dcba37539e5303104f29b721bfb2940b2776da4cc818c07e1fefc1","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","affectsGlobalScope":true,"impliedFormat":1},{"version":"ba481bca06f37d3f2c137ce343c7d5937029b2468f8e26111f3c9d9963d6568d","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d9ef24f9a22a88e3e9b3b3d8c40ab1ddb0853f1bfbd5c843c37800138437b61","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e9c23ba78aabc2e0a27033f18737a6df754067731e69dc5f52823957d60a4b6","impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"2cbe0621042e2a68c7cbce5dfed3906a1862a16a7d496010636cdbdb91341c0f","affectsGlobalScope":true,"impliedFormat":1},{"version":"e2677634fe27e87348825bb041651e22d50a613e2fdf6a4a3ade971d71bac37e","impliedFormat":1},{"version":"7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419","impliedFormat":1},{"version":"8c0bcd6c6b67b4b503c11e91a1fb91522ed585900eab2ab1f61bba7d7caa9d6f","impliedFormat":1},{"version":"8cd19276b6590b3ebbeeb030ac271871b9ed0afc3074ac88a94ed2449174b776","affectsGlobalScope":true,"impliedFormat":1},{"version":"696eb8d28f5949b87d894b26dc97318ef944c794a9a4e4f62360cd1d1958014b","impliedFormat":1},{"version":"3f8fa3061bd7402970b399300880d55257953ee6d3cd408722cb9ac20126460c","impliedFormat":1},{"version":"35ec8b6760fd7138bbf5809b84551e31028fb2ba7b6dc91d95d098bf212ca8b4","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"68bd56c92c2bd7d2339457eb84d63e7de3bd56a69b25f3576e1568d21a162398","affectsGlobalScope":true,"impliedFormat":1},{"version":"3e93b123f7c2944969d291b35fed2af79a6e9e27fdd5faa99748a51c07c02d28","impliedFormat":1},{"version":"9d19808c8c291a9010a6c788e8532a2da70f811adb431c97520803e0ec649991","impliedFormat":1},{"version":"87aad3dd9752067dc875cfaa466fc44246451c0c560b820796bdd528e29bef40","impliedFormat":1},{"version":"4aacb0dd020eeaef65426153686cc639a78ec2885dc72ad220be1d25f1a439df","impliedFormat":1},{"version":"f0bd7e6d931657b59605c44112eaf8b980ba7f957a5051ed21cb93d978cf2f45","impliedFormat":1},{"version":"8db0ae9cb14d9955b14c214f34dae1b9ef2baee2fe4ce794a4cd3ac2531e3255","affectsGlobalScope":true,"impliedFormat":1},{"version":"15fc6f7512c86810273af28f224251a5a879e4261b4d4c7e532abfbfc3983134","impliedFormat":1},{"version":"58adba1a8ab2d10b54dc1dced4e41f4e7c9772cbbac40939c0dc8ce2cdb1d442","impliedFormat":1},{"version":"2fd4c143eff88dabb57701e6a40e02a4dbc36d5eb1362e7964d32028056a782b","impliedFormat":1},{"version":"714435130b9015fae551788df2a88038471a5a11eb471f27c4ede86552842bc9","impliedFormat":1},{"version":"855cd5f7eb396f5f1ab1bc0f8580339bff77b68a770f84c6b254e319bbfd1ac7","impliedFormat":1},{"version":"5650cf3dace09e7c25d384e3e6b818b938f68f4e8de96f52d9c5a1b3db068e86","impliedFormat":1},{"version":"1354ca5c38bd3fd3836a68e0f7c9f91f172582ba30ab15bb8c075891b91502b7","affectsGlobalScope":true,"impliedFormat":1},{"version":"27fdb0da0daf3b337c5530c5f266efe046a6ceb606e395b346974e4360c36419","impliedFormat":1},{"version":"2d2fcaab481b31a5882065c7951255703ddbe1c0e507af56ea42d79ac3911201","impliedFormat":1},{"version":"a192fe8ec33f75edbc8d8f3ed79f768dfae11ff5735e7fe52bfa69956e46d78d","impliedFormat":1},{"version":"ca867399f7db82df981d6915bcbb2d81131d7d1ef683bc782b59f71dda59bc85","affectsGlobalScope":true,"impliedFormat":1},{"version":"0e456fd5b101271183d99a9087875a282323e3a3ff0d7bcf1881537eaa8b8e63","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e043a1bc8fbf2a255bccf9bf27e0f1caf916c3b0518ea34aa72357c0afd42ec","impliedFormat":1},{"version":"b4f70ec656a11d570e1a9edce07d118cd58d9760239e2ece99306ee9dfe61d02","impliedFormat":1},{"version":"3bc2f1e2c95c04048212c569ed38e338873f6a8593930cf5a7ef24ffb38fc3b6","impliedFormat":1},{"version":"6e70e9570e98aae2b825b533aa6292b6abd542e8d9f6e9475e88e1d7ba17c866","impliedFormat":1},{"version":"f9d9d753d430ed050dc1bf2667a1bab711ccbb1c1507183d794cc195a5b085cc","impliedFormat":1},{"version":"9eece5e586312581ccd106d4853e861aaaa1a39f8e3ea672b8c3847eedd12f6e","impliedFormat":1},{"version":"47ab634529c5955b6ad793474ae188fce3e6163e3a3fb5edd7e0e48f14435333","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"fad4e3c207fe23922d0b2d06b01acbfb9714c4f2685cf80fd384c8a100c82fd0","affectsGlobalScope":true,"impliedFormat":1},{"version":"74cf591a0f63db318651e0e04cb55f8791385f86e987a67fd4d2eaab8191f730","impliedFormat":1},{"version":"5eab9b3dc9b34f185417342436ec3f106898da5f4801992d8ff38ab3aff346b5","impliedFormat":1},{"version":"12ed4559eba17cd977aa0db658d25c4047067444b51acfdcbf38470630642b23","affectsGlobalScope":true,"impliedFormat":1},{"version":"f3ffabc95802521e1e4bcba4c88d8615176dc6e09111d920c7a213bdda6e1d65","impliedFormat":1},{"version":"ddc734b4fae82a01d247e9e342d020976640b5e93b4e9b3a1e30e5518883a060","impliedFormat":1},{"version":"ae56f65caf3be91108707bd8dfbccc2a57a91feb5daabf7165a06a945545ed26","impliedFormat":1},{"version":"a136d5de521da20f31631a0a96bf712370779d1c05b7015d7019a9b2a0446ca9","impliedFormat":1},{"version":"c3b41e74b9a84b88b1dca61ec39eee25c0dbc8e7d519ba11bb070918cfacf656","affectsGlobalScope":true,"impliedFormat":1},{"version":"4737a9dc24d0e68b734e6cfbcea0c15a2cfafeb493485e27905f7856988c6b29","affectsGlobalScope":true,"impliedFormat":1},{"version":"36d8d3e7506b631c9582c251a2c0b8a28855af3f76719b12b534c6edf952748d","impliedFormat":1},{"version":"1ca69210cc42729e7ca97d3a9ad48f2e9cb0042bada4075b588ae5387debd318","impliedFormat":1},{"version":"f5ebe66baaf7c552cfa59d75f2bfba679f329204847db3cec385acda245e574e","impliedFormat":1},{"version":"ed59add13139f84da271cafd32e2171876b0a0af2f798d0c663e8eeb867732cf","affectsGlobalScope":true,"impliedFormat":1},{"version":"05db535df8bdc30d9116fe754a3473d1b6479afbc14ae8eb18b605c62677d518","impliedFormat":1},{"version":"b1810689b76fd473bd12cc9ee219f8e62f54a7d08019a235d07424afbf074d25","impliedFormat":1},{"version":"5fc51af013891a80ccda04f05b44cb564ff18b6eedb4e85b3dfecf340f1ccfe9","impliedFormat":1},{"version":"0f90d42ca7ec806d907cd9e5a60c19ca995d15be51d99474fcfa80b752ba1218","impliedFormat":1},{"version":"be1cc4d94ea60cbe567bc29ed479d42587bf1e6cba490f123d329976b0fe4ee5","impliedFormat":1},{"version":"42bc0e1a903408137c3df2b06dfd7e402cdab5bbfa5fcfb871b22ebfdb30bd0b","impliedFormat":1},{"version":"9894dafe342b976d251aac58e616ac6df8db91fb9d98934ff9dd103e9e82578f","impliedFormat":1},{"version":"413df52d4ea14472c2fa5bee62f7a40abd1eb49be0b9722ee01ee4e52e63beb2","impliedFormat":1},{"version":"db6d2d9daad8a6d83f281af12ce4355a20b9a3e71b82b9f57cddcca0a8964a96","impliedFormat":1},{"version":"446a50749b24d14deac6f8843e057a6355dd6437d1fac4f9e5ce4a5071f34bff","impliedFormat":1},{"version":"182e9fcbe08ac7c012e0a6e2b5798b4352470be29a64fdc114d23c2bab7d5106","impliedFormat":1},{"version":"5c9b31919ea1cb350a7ae5e71c9ced8f11723e4fa258a8cc8d16ae46edd623c7","impliedFormat":1},{"version":"4aa42ce8383b45823b3a1d3811c0fdd5f939f90254bc4874124393febbaf89f6","impliedFormat":1},{"version":"96ffa70b486207241c0fcedb5d9553684f7fa6746bc2b04c519e7ebf41a51205","impliedFormat":1},{"version":"3677988e03b749874eb9c1aa8dc88cd77b6005e5c4c39d821cda7b80d5388619","impliedFormat":1},{"version":"a86f82d646a739041d6702101afa82dcb935c416dd93cbca7fd754fd0282ce1f","impliedFormat":1},{"version":"ad0d1d75d129b1c80f911be438d6b61bfa8703930a8ff2be2f0e1f8a91841c64","impliedFormat":1},{"version":"ce75b1aebb33d510ff28af960a9221410a3eaf7f18fc5f21f9404075fba77256","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"02436d7e9ead85e09a2f8e27d5f47d9464bced31738dec138ca735390815c9f0","impliedFormat":1},{"version":"f4625edcb57b37b84506e8b276eb59ca30d31f88c6656d29d4e90e3bc58e69df","impliedFormat":1},{"version":"78a2869ad0cbf3f9045dda08c0d4562b7e1b2bfe07b19e0db072f5c3c56e9584","impliedFormat":1},{"version":"f8d5ff8eafd37499f2b6a98659dd9b45a321de186b8db6b6142faed0fea3de77","impliedFormat":1},{"version":"c86fe861cf1b4c46a0fb7d74dffe596cf679a2e5e8b1456881313170f092e3fa","impliedFormat":1},{"version":"c685d9f68c70fe11ce527287526585a06ea13920bb6c18482ca84945a4e433a7","impliedFormat":1},{"version":"540cc83ab772a2c6bc509fe1354f314825b5dba3669efdfbe4693ecd3048e34f","impliedFormat":1},{"version":"121b0696021ab885c570bbeb331be8ad82c6efe2f3b93a6e63874901bebc13e3","impliedFormat":1},{"version":"4e01846df98d478a2a626ec3641524964b38acaac13945c2db198bf9f3df22ee","impliedFormat":1},{"version":"678d6d4c43e5728bf66e92fc2269da9fa709cb60510fed988a27161473c3853f","impliedFormat":1},{"version":"ffa495b17a5ef1d0399586b590bd281056cee6ce3583e34f39926f8dcc6ecdb5","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"aa14cee20aa0db79f8df101fc027d929aec10feb5b8a8da3b9af3895d05b7ba2","impliedFormat":1},{"version":"493c700ac3bd317177b2eb913805c87fe60d4e8af4fb39c41f04ba81fae7e170","impliedFormat":1},{"version":"aeb554d876c6b8c818da2e118d8b11e1e559adbe6bf606cc9a611c1b6c09f670","impliedFormat":1},{"version":"acf5a2ac47b59ca07afa9abbd2b31d001bf7448b041927befae2ea5b1951d9f9","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"d71291eff1e19d8762a908ba947e891af44749f3a2cbc5bd2ec4b72f72ea795f","impliedFormat":1},{"version":"c0480e03db4b816dff2682b347c95f2177699525c54e7e6f6aa8ded890b76be7","impliedFormat":1},{"version":"e2a37ac938c4bede5bb284b9d2d042da299528f1e61f6f57538f1bd37d760869","impliedFormat":1},{"version":"76def37aff8e3a051cf406e10340ffba0f28b6991c5d987474cc11137796e1eb","impliedFormat":1},{"version":"b620391fe8060cf9bedc176a4d01366e6574d7a71e0ac0ab344a4e76576fcbb8","impliedFormat":1},{"version":"3e7efde639c6a6c3edb9847b3f61e308bf7a69685b92f665048c45132f51c218","impliedFormat":1},{"version":"df45ca1176e6ac211eae7ddf51336dc075c5314bc5c253651bae639defd5eec5","impliedFormat":1},{"version":"106c6025f1d99fd468fd8bf6e5bda724e11e5905a4076c5d29790b6c3745e50c","impliedFormat":1},{"version":"ee8df1cb8d0faaca4013a1b442e99130769ce06f438d18d510fed95890067563","impliedFormat":1},{"version":"bfb7f8475428637bee12bdd31bd9968c1c8a1cc2c3e426c959e2f3a307f8936f","impliedFormat":1},{"version":"6f491d0108927478d3247bbbc489c78c2da7ef552fd5277f1ab6819986fdf0b1","impliedFormat":1},{"version":"594fe24fc54645ab6ccb9dba15d3a35963a73a395b2ef0375ea34bf181ccfd63","impliedFormat":1},{"version":"7cb0ee103671d1e201cd53dda12bc1cd0a35f1c63d6102720c6eeb322cb8e17e","impliedFormat":1},{"version":"15a234e5031b19c48a69ccc1607522d6e4b50f57d308ecb7fe863d44cd9f9eb3","impliedFormat":1},{"version":"148679c6d0f449210a96e7d2e562d589e56fcde87f843a92808b3ff103f1a774","impliedFormat":1},{"version":"6459054aabb306821a043e02b89d54da508e3a6966601a41e71c166e4ea1474f","impliedFormat":1},{"version":"2f9c89cbb29d362290531b48880a4024f258c6033aaeb7e59fbc62db26819650","impliedFormat":1},{"version":"bb37588926aba35c9283fe8d46ebf4e79ffe976343105f5c6d45f282793352b2","impliedFormat":1},{"version":"05c97cddbaf99978f83d96de2d8af86aded9332592f08ce4a284d72d0952c391","impliedFormat":1},{"version":"72179f9dd22a86deaad4cc3490eb0fe69ee084d503b686985965654013f1391b","impliedFormat":1},{"version":"2e6114a7dd6feeef85b2c80120fdbfb59a5529c0dcc5bfa8447b6996c97a69f5","impliedFormat":1},{"version":"7b6ff760c8a240b40dab6e4419b989f06a5b782f4710d2967e67c695ef3e93c4","impliedFormat":1},{"version":"c8f004e6036aa1c764ad4ec543cf89a5c1893a9535c80ef3f2b653e370de45e6","impliedFormat":1},{"version":"dd80b1e600d00f5c6a6ba23f455b84a7db121219e68f89f10552c54ba46e4dc9","impliedFormat":1},{"version":"b064c36f35de7387d71c599bfcf28875849a1dbc733e82bd26cae3d1cd060521","impliedFormat":1},{"version":"05c7280d72f3ed26f346cbe7cbbbb002fb7f15739197cbbee6ab3fd1a6cb9347","impliedFormat":1},{"version":"8de9fe97fa9e00ec00666fa77ab6e91b35d25af8ca75dabcb01e14ad3299b150","impliedFormat":1},{"version":"803cd2aaf1921c218916c2c7ee3fce653e852d767177eb51047ff15b5b253893","impliedFormat":1},{"version":"dba114fb6a32b355a9cfc26ca2276834d72fe0e94cd2c3494005547025015369","impliedFormat":1},{"version":"7ab12b2f1249187223d11a589f5789c75177a0b597b9eb7f8e2e42d045393347","impliedFormat":1},{"version":"ad37fb4be61c1035b68f532b7220f4e8236cf245381ce3b90ac15449ecfe7305","impliedFormat":1},{"version":"93436bd74c66baba229bfefe1314d122c01f0d4c1d9e35081a0c4f0470ac1a6c","impliedFormat":1},{"version":"f974e4a06953682a2c15d5bd5114c0284d5abf8bc0fe4da25cb9159427b70072","impliedFormat":1},{"version":"50256e9c31318487f3752b7ac12ff365c8949953e04568009c8705db802776fb","impliedFormat":1},{"version":"7d73b24e7bf31dfb8a931ca6c4245f6bb0814dfae17e4b60c9e194a631fe5f7b","impliedFormat":1},{"version":"d130c5f73768de51402351d5dc7d1b36eaec980ca697846e53156e4ea9911476","impliedFormat":1},{"version":"413586add0cfe7369b64979d4ec2ed56c3f771c0667fbde1bf1f10063ede0b08","impliedFormat":1},{"version":"06472528e998d152375ad3bd8ebcb69ff4694fd8d2effaf60a9d9f25a37a097a","impliedFormat":1},{"version":"50b5bc34ce6b12eccb76214b51aadfa56572aa6cc79c2b9455cdbb3d6c76af1d","impliedFormat":1},{"version":"b7e16ef7f646a50991119b205794ebfd3a4d8f8e0f314981ebbe991639023d0e","impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},{"version":"a401617604fa1f6ce437b81689563dfdc377069e4c58465dbd8d16069aede0a5","impliedFormat":1},{"version":"6e9082e91370de5040e415cd9f24e595b490382e8c7402c4e938a8ce4bccc99f","impliedFormat":1},{"version":"8695dec09ad439b0ceef3776ea68a232e381135b516878f0901ed2ea114fd0fe","impliedFormat":1},{"version":"304b44b1e97dd4c94697c3313df89a578dca4930a104454c99863f1784a54357","impliedFormat":1},{"version":"d682336018141807fb602709e2d95a192828fcb8d5ba06dda3833a8ea98f69e3","impliedFormat":1},{"version":"6124e973eab8c52cabf3c07575204efc1784aca6b0a30c79eb85fe240a857efa","impliedFormat":1},{"version":"0d891735a21edc75df51f3eb995e18149e119d1ce22fd40db2b260c5960b914e","impliedFormat":1},{"version":"3b414b99a73171e1c4b7b7714e26b87d6c5cb03d200352da5342ab4088a54c85","impliedFormat":1},{"version":"4fbd3116e00ed3a6410499924b6403cc9367fdca303e34838129b328058ede40","impliedFormat":1},{"version":"b01bd582a6e41457bc56e6f0f9de4cb17f33f5f3843a7cf8210ac9c18472fb0f","impliedFormat":1},{"version":"0a437ae178f999b46b6153d79095b60c42c996bc0458c04955f1c996dc68b971","impliedFormat":1},{"version":"74b2a5e5197bd0f2e0077a1ea7c07455bbea67b87b0869d9786d55104006784f","impliedFormat":1},{"version":"4a7baeb6325920044f66c0f8e5e6f1f52e06e6d87588d837bdf44feb6f35c664","impliedFormat":1},{"version":"12d218a49dbe5655b911e6cc3c13b2c655e4c783471c3b0432137769c79e1b3c","impliedFormat":1},{"version":"7274fbffbd7c9589d8d0ffba68157237afd5cecff1e99881ea3399127e60572f","impliedFormat":1},{"version":"6b0fc04121360f752d196ba35b6567192f422d04a97b2840d7d85f8b79921c92","impliedFormat":1},{"version":"65a15fc47900787c0bd18b603afb98d33ede930bed1798fc984d5ebb78b26cf9","impliedFormat":1},{"version":"9d202701f6e0744adb6314d03d2eb8fc994798fc83d91b691b75b07626a69801","impliedFormat":1},{"version":"a365c4d3bed3be4e4e20793c999c51f5cd7e6792322f14650949d827fbcd170f","impliedFormat":1},{"version":"c5426dbfc1cf90532f66965a7aa8c1136a78d4d0f96d8180ecbfc11d7722f1a5","impliedFormat":1},{"version":"9c82171d836c47486074e4ca8e059735bf97b205e70b196535b5efd40cbe1bc5","impliedFormat":1},{"version":"f374cb24e93e7798c4d9e83ff872fa52d2cdb36306392b840a6ddf46cb925cb6","impliedFormat":1},{"version":"42b81043b00ff27c6bd955aea0f6e741545f2265978bf364b614702b72a027ab","impliedFormat":1},{"version":"de9d2df7663e64e3a91bf495f315a7577e23ba088f2949d5ce9ec96f44fba37d","impliedFormat":1},{"version":"c7af78a2ea7cb1cd009cfb5bdb48cd0b03dad3b54f6da7aab615c2e9e9d570c5","impliedFormat":1},{"version":"1ee45496b5f8bdee6f7abc233355898e5bf9bd51255db65f5ff7ede617ca0027","impliedFormat":1},{"version":"97e5ccc7bb88419005cbdf812243a5b3186cdef81b608540acabe1be163fc3e4","affectsGlobalScope":true,"impliedFormat":1},{"version":"3fbdd025f9d4d820414417eeb4107ffa0078d454a033b506e22d3a23bc3d9c41","affectsGlobalScope":true,"impliedFormat":1},{"version":"a8f8e6ab2fa07b45251f403548b78eaf2022f3c2254df3dc186cb2671fe4996d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa6c12a7c0f6b84d512f200690bfc74819e99efae69e4c95c4cd30f6884c526e","impliedFormat":1},{"version":"f1c32f9ce9c497da4dc215c3bc84b722ea02497d35f9134db3bb40a8d918b92b","impliedFormat":1},{"version":"b73c319af2cc3ef8f6421308a250f328836531ea3761823b4cabbd133047aefa","affectsGlobalScope":true,"impliedFormat":1},{"version":"e433b0337b8106909e7953015e8fa3f2d30797cea27141d1c5b135365bb975a6","impliedFormat":1},{"version":"9f9bb6755a8ce32d656ffa4763a8144aa4f274d6b69b59d7c32811031467216e","impliedFormat":1},{"version":"5c32bdfbd2d65e8fffbb9fbda04d7165e9181b08dad61154961852366deb7540","impliedFormat":1},{"version":"ddff7fc6edbdc5163a09e22bf8df7bef75f75369ebd7ecea95ba55c4386e2441","impliedFormat":1},{"version":"6b3453eebd474cc8acf6d759f1668e6ce7425a565e2996a20b644c72916ecf75","impliedFormat":1},{"version":"0c05e9842ec4f8b7bfebfd3ca61604bb8c914ba8da9b5337c4f25da427a005f2","impliedFormat":1},{"version":"89cd3444e389e42c56fd0d072afef31387e7f4107651afd2c03950f22dc36f77","impliedFormat":1},{"version":"7f2aa4d4989a82530aaac3f72b3dceca90e9c25bee0b1a327e8a08a1262435ad","impliedFormat":1},{"version":"e39a304f882598138a8022106cb8de332abbbb87f3fee71c5ca6b525c11c51fc","impliedFormat":1},{"version":"faed7a5153215dbd6ebe76dfdcc0af0cfe760f7362bed43284be544308b114cf","impliedFormat":1},{"version":"fcdf3e40e4a01b9a4b70931b8b51476b210c511924fcfe3f0dae19c4d52f1a54","impliedFormat":1},{"version":"345c4327b637d34a15aba4b7091eb068d6ab40a3dedaab9f00986253c9704e53","impliedFormat":1},{"version":"3a788c7fb7b1b1153d69a4d1d9e1d0dfbcf1127e703bdb02b6d12698e683d1fb","impliedFormat":1},{"version":"2e4f37ffe8862b14d8e24ae8763daaa8340c0df0b859d9a9733def0eee7562d9","impliedFormat":1},{"version":"d38530db0601215d6d767f280e3a3c54b2a83b709e8d9001acb6f61c67e965fc","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"4805f6161c2c8cefb8d3b8bd96a080c0fe8dbc9315f6ad2e53238f9a79e528a6","impliedFormat":1},{"version":"b83cb14474fa60c5f3ec660146b97d122f0735627f80d82dd03e8caa39b4388c","impliedFormat":1},{"version":"2b5b70d7782fe028487a80a1c214e67bd610532b9f978b78fa60f5b4a359f77e","impliedFormat":1},{"version":"7ee86fbb3754388e004de0ef9e6505485ddfb3be7640783d6d015711c03d302d","impliedFormat":1},{"version":"1a82deef4c1d39f6882f28d275cad4c01f907b9b39be9cbc472fcf2cf051e05b","impliedFormat":1},{"version":"7580e62139cb2b44a0270c8d01abcbfcba2819a02514a527342447fa69b34ef1","impliedFormat":1},{"version":"b73cbf0a72c8800cf8f96a9acfe94f3ad32ca71342a8908b8ae484d61113f647","impliedFormat":1},{"version":"bae6dd176832f6423966647382c0d7ba9e63f8c167522f09a982f086cd4e8b23","impliedFormat":1},{"version":"20865ac316b8893c1a0cc383ccfc1801443fbcc2a7255be166cf90d03fac88c9","impliedFormat":1},{"version":"c9958eb32126a3843deedda8c22fb97024aa5d6dd588b90af2d7f2bfac540f23","impliedFormat":1},{"version":"461d0ad8ae5f2ff981778af912ba71b37a8426a33301daa00f21c6ccb27f8156","impliedFormat":1},{"version":"e927c2c13c4eaf0a7f17e6022eee8519eb29ef42c4c13a31e81a611ab8c95577","impliedFormat":1},{"version":"fcafff163ca5e66d3b87126e756e1b6dfa8c526aa9cd2a2b0a9da837d81bbd72","impliedFormat":1},{"version":"70246ad95ad8a22bdfe806cb5d383a26c0c6e58e7207ab9c431f1cb175aca657","impliedFormat":1},{"version":"f00f3aa5d64ff46e600648b55a79dcd1333458f7a10da2ed594d9f0a44b76d0b","impliedFormat":1},{"version":"772d8d5eb158b6c92412c03228bd9902ccb1457d7a705b8129814a5d1a6308fc","impliedFormat":1},{"version":"802e797bcab5663b2c9f63f51bdf67eff7c41bc64c0fd65e6da3e7941359e2f7","impliedFormat":1},{"version":"8b4327413e5af38cd8cb97c59f48c3c866015d5d642f28518e3a891c469f240e","impliedFormat":1},{"version":"7e6ac205dcb9714f708354fd863bffa45cee90740706cc64b3b39b23ebb84744","impliedFormat":1},{"version":"61dc6e3ac78d64aa864eedd0a208b97b5887cc99c5ba65c03287bf57d83b1eb9","impliedFormat":1},{"version":"4b20fcf10a5413680e39f5666464859fc56b1003e7dfe2405ced82371ebd49b6","impliedFormat":1},{"version":"c06ef3b2569b1c1ad99fcd7fe5fba8d466e2619da5375dfa940a94e0feea899b","impliedFormat":1},{"version":"f7d628893c9fa52ba3ab01bcb5e79191636c4331ee5667ecc6373cbccff8ae12","impliedFormat":1},{"version":"1d879125d1ec570bf04bc1f362fdbe0cb538315c7ac4bcfcdf0c1e9670846aa6","impliedFormat":1},{"version":"f730b468deecf26188ad62ee8950dc29aa2aea9543bb08ed714c3db019359fd9","impliedFormat":1},{"version":"933aee906d42ea2c53b6892192a8127745f2ec81a90695df4024308ba35a8ff4","impliedFormat":1},{"version":"d663134457d8d669ae0df34eabd57028bddc04fc444c4bc04bc5215afc91e1f4","impliedFormat":1},{"version":"144bc326e90b894d1ec78a2af3ffb2eb3733f4d96761db0ca0b6239a8285f972","impliedFormat":1},{"version":"a3e3f0efcae272ab8ee3298e4e819f7d9dd9ff411101f45444877e77cfeca9a4","impliedFormat":1},{"version":"43e96a3d5d1411ab40ba2f61d6a3192e58177bcf3b133a80ad2a16591611726d","impliedFormat":1},{"version":"58659b06d33fa430bee1105b75cf876c0a35b2567207487c8578aec51ca2d977","impliedFormat":1},{"version":"71d9eb4c4e99456b78ae182fb20a5dfc20eb1667f091dbb9335b3c017dd1c783","impliedFormat":1},{"version":"cfa846a7b7847a1d973605fbb8c91f47f3a0f0643c18ac05c47077ebc72e71c7","impliedFormat":1},{"version":"30e6520444df1a004f46fdc8096f3fe06f7bbd93d09c53ada9dcdde59919ccca","impliedFormat":1},{"version":"6c800b281b9e89e69165fd11536195488de3ff53004e55905e6c0059a2d8591e","impliedFormat":1},{"version":"7d4254b4c6c67a29d5e7f65e67d72540480ac2cfb041ca484847f5ae70480b62","impliedFormat":1},{"version":"a58beefce74db00dbb60eb5a4bb0c6726fb94c7797c721f629142c0ae9c94306","impliedFormat":1},{"version":"41eeb453ccb75c5b2c3abef97adbbd741bd7e9112a2510e12f03f646dc9ad13d","impliedFormat":1},{"version":"502fa5863df08b806dbf33c54bee8c19f7e2ad466785c0fc35465d7c5ff80995","impliedFormat":1},{"version":"c91a2d08601a1547ffef326201be26db94356f38693bb18db622ae5e9b3d7c92","impliedFormat":1},{"version":"888cda0fa66d7f74e985a3f7b1af1f64b8ff03eb3d5e80d051c3cbdeb7f32ab7","impliedFormat":1},{"version":"60681e13f3545be5e9477acb752b741eae6eaf4cc01658a25ec05bff8b82a2ef","impliedFormat":1},{"version":"9586918b63f24124a5ca1d0cc2979821a8a57f514781f09fc5aa9cae6d7c0138","impliedFormat":1},{"version":"a57b1802794433adec9ff3fed12aa79d671faed86c49b09e02e1ac41b4f1d33a","impliedFormat":1},{"version":"ad10d4f0517599cdeca7755b930f148804e3e0e5b5a3847adce0f1f71bbccd74","impliedFormat":1},{"version":"1042064ece5bb47d6aba91648fbe0635c17c600ebdf567588b4ca715602f0a9d","impliedFormat":1},{"version":"c49469a5349b3cc1965710b5b0f98ed6c028686aa8450bcb3796728873eb923e","impliedFormat":1},{"version":"4a889f2c763edb4d55cb624257272ac10d04a1cad2ed2948b10ed4a7fda2a428","impliedFormat":1},{"version":"7bb79aa2fead87d9d56294ef71e056487e848d7b550c9a367523ee5416c44cfa","impliedFormat":1},{"version":"d88ea80a6447d7391f52352ec97e56b52ebec934a4a4af6e2464cfd8b39c3ba8","impliedFormat":1},{"version":"55095860901097726220b6923e35a812afdd49242a1246d7b0942ee7eb34c6e4","impliedFormat":1},{"version":"96171c03c2e7f314d66d38acd581f9667439845865b7f85da8df598ff9617476","impliedFormat":1},{"version":"27ff4196654e6373c9af16b6165120e2dd2169f9ad6abb5c935af5abd8c7938c","impliedFormat":1},{"version":"bb8f2dbc03533abca2066ce4655c119bff353dd4514375beb93c08590c03e023","impliedFormat":1},{"version":"d193c8a86144b3a87b22bc1f5534b9c3e0f5a187873ec337c289a183973a58fe","impliedFormat":1},{"version":"1a6e6ba8a07b74e3ad237717c0299d453f9ceb795dbc2f697d1f2dd07cb782d2","impliedFormat":1},{"version":"58d70c38037fc0f949243388ff7ae20cf43321107152f14a9d36ca79311e0ada","impliedFormat":1},{"version":"f56bdc6884648806d34bc66d31cdb787c4718d04105ce2cd88535db214631f82","impliedFormat":1},{"version":"190da5eac6478d61ab9731ab2146fbc0164af2117a363013249b7e7992f1cccb","impliedFormat":1},{"version":"01479d9d5a5dda16d529b91811375187f61a06e74be294a35ecce77e0b9e8d6c","impliedFormat":1},{"version":"49f95e989b4632c6c2a578cc0078ee19a5831832d79cc59abecf5160ea71abad","impliedFormat":1},{"version":"9666533332f26e8995e4d6fe472bdeec9f15d405693723e6497bf94120c566c8","impliedFormat":1},{"version":"ce0df82a9ae6f914ba08409d4d883983cc08e6d59eb2df02d8e4d68309e7848b","impliedFormat":1},{"version":"796273b2edc72e78a04e86d7c58ae94d370ab93a0ddf40b1aa85a37a1c29ecd7","impliedFormat":1},{"version":"5df15a69187d737d6d8d066e189ae4f97e41f4d53712a46b2710ff9f8563ec9f","impliedFormat":1},{"version":"1a4dc28334a926d90ba6a2d811ba0ff6c22775fcc13679521f034c124269fd40","impliedFormat":1},{"version":"f05315ff85714f0b87cc0b54bcd3dde2716e5a6b99aedcc19cad02bf2403e08c","impliedFormat":1},{"version":"8a8c64dafaba11c806efa56f5c69f611276471bef80a1db1f71316ec4168acef","impliedFormat":1},{"version":"43ba4f2fa8c698f5c304d21a3ef596741e8e85a810b7c1f9b692653791d8d97a","impliedFormat":1},{"version":"5fad3b31fc17a5bc58095118a8b160f5260964787c52e7eb51e3d4fcf5d4a6f0","impliedFormat":1},{"version":"72105519d0390262cf0abe84cf41c926ade0ff475d35eb21307b2f94de985778","impliedFormat":1},{"version":"d0a4cac61fa080f2be5ebb68b82726be835689b35994ba0e22e3ed4d2bc45e3b","impliedFormat":1},{"version":"c857e0aae3f5f444abd791ec81206020fbcc1223e187316677e026d1c1d6fe08","impliedFormat":1},{"version":"ccf6dd45b708fb74ba9ed0f2478d4eb9195c9dfef0ff83a6092fa3cf2ff53b4f","impliedFormat":1},{"version":"2d7db1d73456e8c5075387d4240c29a2a900847f9c1bff106a2e490da8fbd457","impliedFormat":1},{"version":"2b15c805f48e4e970f8ec0b1915f22d13ca6212375e8987663e2ef5f0205e832","impliedFormat":1},{"version":"205a31b31beb7be73b8df18fcc43109cbc31f398950190a0967afc7a12cb478c","impliedFormat":1},{"version":"8fca3039857709484e5893c05c1f9126ab7451fa6c29e19bb8c2411a2e937345","impliedFormat":1},{"version":"35069c2c417bd7443ae7c7cafd1de02f665bf015479fec998985ffbbf500628c","impliedFormat":1},{"version":"dba6c7006e14a98ec82999c6f89fbbbfd1c642f41db148535f3b77b8018829b8","impliedFormat":1},{"version":"7f897b285f22a57a5c4dc14a27da2747c01084a542b4d90d33897216dceeea2e","impliedFormat":1},{"version":"7e0b7f91c5ab6e33f511efc640d36e6f933510b11be24f98836a20a2dc914c2d","impliedFormat":1},{"version":"045b752f44bf9bbdcaffd882424ab0e15cb8d11fa94e1448942e338c8ef19fba","impliedFormat":1},{"version":"2894c56cad581928bb37607810af011764a2f511f575d28c9f4af0f2ef02d1ab","impliedFormat":1},{"version":"0a72186f94215d020cb386f7dca81d7495ab6c17066eb07d0f44a5bf33c1b21a","impliedFormat":1},{"version":"d96b39301d0ded3f1a27b47759676a33a02f6f5049bfcbde81e533fd10f50dcb","impliedFormat":1},{"version":"2ded4f930d6abfaa0625cf55e58f565b7cbd4ab5b574dd2cb19f0a83a2f0be8b","impliedFormat":1},{"version":"0aedb02516baf3e66b2c1db9fef50666d6ed257edac0f866ea32f1aa05aa474f","impliedFormat":1},{"version":"ca0f4d9068d652bad47e326cf6ba424ac71ab866e44b24ddb6c2bd82d129586a","affectsGlobalScope":true,"impliedFormat":1},{"version":"04d36005fcbeac741ac50c421181f4e0316d57d148d37cc321a8ea285472462b","impliedFormat":1},{"version":"9e2739b32f741859263fdba0244c194ca8e96da49b430377930b8f721d77c000","impliedFormat":1},{"version":"56ccb49443bfb72e5952f7012f0de1a8679f9f75fc93a5c1ac0bafb28725fc5f","impliedFormat":1},{"version":"20fa37b636fdcc1746ea0738f733d0aed17890d1cd7cb1b2f37010222c23f13e","impliedFormat":1},{"version":"d90b9f1520366d713a73bd30c5a9eb0040d0fb6076aff370796bc776fd705943","impliedFormat":1},{"version":"bc03c3c352f689e38c0ddd50c39b1e65d59273991bfc8858a9e3c0ebb79c023b","impliedFormat":1},{"version":"19df3488557c2fc9b4d8f0bac0fd20fb59aa19dec67c81f93813951a81a867f8","affectsGlobalScope":true,"impliedFormat":1},{"version":"b25350193e103ae90423c5418ddb0ad1168dc9c393c9295ef34980b990030617","affectsGlobalScope":true,"impliedFormat":1},{"version":"bef86adb77316505c6b471da1d9b8c9e428867c2566270e8894d4d773a1c4dc2","impliedFormat":1},{"version":"a46dba563f70f32f9e45ae015f3de979225f668075d7a427f874e0f6db584991","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"2652448ac55a2010a1f71dd141f828b682298d39728f9871e1cdf8696ef443fd","impliedFormat":1},{"version":"02c4fc9e6bb27545fa021f6056e88ff5fdf10d9d9f1467f1d10536c6e749ac50","impliedFormat":1},{"version":"120599fd965257b1f4d0ff794bc696162832d9d8467224f4665f713a3119078b","impliedFormat":1},{"version":"5433f33b0a20300cca35d2f229a7fc20b0e8477c44be2affeb21cb464af60c76","impliedFormat":1},{"version":"db036c56f79186da50af66511d37d9fe77fa6793381927292d17f81f787bb195","impliedFormat":1},{"version":"bd4131091b773973ca5d2326c60b789ab1f5e02d8843b3587effe6e1ea7c9d86","impliedFormat":1},{"version":"c7f6485931085bf010fbaf46880a9b9ec1a285ad9dc8c695a9e936f5a48f34b4","impliedFormat":1},{"version":"14f6b927888a1112d662877a5966b05ac1bf7ed25d6c84386db4c23c95a5363b","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"622694a8522b46f6310c2a9b5d2530dde1e2854cb5829354e6d1ff8f371cf469","impliedFormat":1},{"version":"d24ff95760ea2dfcc7c57d0e269356984e7046b7e0b745c80fea71559f15bdd8","impliedFormat":1},{"version":"a9e6c0ff3f8186fccd05752cf75fc94e147c02645087ac6de5cc16403323d870","impliedFormat":1},{"version":"49c346823ba6d4b12278c12c977fb3a31c06b9ca719015978cb145eb86da1c61","impliedFormat":1},{"version":"bfac6e50eaa7e73bb66b7e052c38fdc8ccfc8dbde2777648642af33cf349f7f1","impliedFormat":1},{"version":"92f7c1a4da7fbfd67a2228d1687d5c2e1faa0ba865a94d3550a3941d7527a45d","impliedFormat":1},{"version":"f53b120213a9289d9a26f5af90c4c686dd71d91487a0aa5451a38366c70dc64b","impliedFormat":1},{"version":"83fe880c090afe485a5c02262c0b7cdd76a299a50c48d9bde02be8e908fb4ae6","impliedFormat":1},{"version":"13c1b657932e827a7ed510395d94fc8b743b9d053ab95b7cd829b2bc46fb06db","impliedFormat":1},{"version":"57d67b72e06059adc5e9454de26bbfe567d412b962a501d263c75c2db430f40e","impliedFormat":1},{"version":"6511e4503cf74c469c60aafd6589e4d14d5eb0a25f9bf043dcbecdf65f261972","impliedFormat":1},{"version":"078131f3a722a8ad3fc0b724cd3497176513cdcb41c80f96a3acbda2a143b58e","impliedFormat":1},{"version":"8c70ddc0c22d85e56011d49fddfaae3405eb53d47b59327b9dd589e82df672e7","impliedFormat":1},{"version":"a67b87d0281c97dfc1197ef28dfe397fc2c865ccd41f7e32b53f647184cc7307","impliedFormat":1},{"version":"771ffb773f1ddd562492a6b9aaca648192ac3f056f0e1d997678ff97dbb6bf9b","impliedFormat":1},{"version":"232f70c0cf2b432f3a6e56a8dc3417103eb162292a9fd376d51a3a9ea5fbbf6f","impliedFormat":1},{"version":"9e155d2255348d950b1f65643fb26c0f14f5109daf8bd9ee24a866ad0a743648","affectsGlobalScope":true,"impliedFormat":1},{"version":"0b103e9abfe82d14c0ad06a55d9f91d6747154ef7cacc73cf27ecad2bfb3afcf","impliedFormat":1},{"version":"7a883e9c84e720810f86ef4388f54938a65caa0f4d181a64e9255e847a7c9f51","impliedFormat":1},{"version":"a0ba218ac1baa3da0d5d9c1ec1a7c2f8676c284e6f5b920d6d049b13fa267377","impliedFormat":1},{"version":"8a0e762ceb20c7e72504feef83d709468a70af4abccb304f32d6b9bac1129b2c","impliedFormat":1},{"version":"d408d6f32de8d1aba2ff4a20f1aa6a6edd7d92c997f63b90f8ad3f9017cf5e46","impliedFormat":1},{"version":"9252d498a77517aab5d8d4b5eb9d71e4b225bbc7123df9713e08181de63180f6","impliedFormat":1},{"version":"b1f1d57fde8247599731b24a733395c880a6561ec0c882efaaf20d7df968c5af","impliedFormat":1},{"version":"9d622ea608d43eb463c0c4538fd5baa794bc18ea0bb8e96cd2ab6fd483d55fe2","impliedFormat":1},{"version":"35e6379c3f7cb27b111ad4c1aa69538fd8e788ab737b8ff7596a1b40e96f4f90","impliedFormat":1},{"version":"1fffe726740f9787f15b532e1dc870af3cd964dbe29e191e76121aa3dd8693f2","impliedFormat":1},{"version":"371bf6127c1d427836de95197155132501cb6b69ef8709176ce6e0b85d059264","impliedFormat":1},{"version":"2bafd700e617d3693d568e972d02b92224b514781f542f70d497a8fdf92d52a2","affectsGlobalScope":true,"impliedFormat":1},{"version":"5542d8a7ea13168cb573be0d1ba0d29460d59430fb12bb7bf4674efd5604e14c","impliedFormat":1},{"version":"af48e58339188d5737b608d41411a9c054685413d8ae88b8c1d0d9bfabdf6e7e","impliedFormat":1},{"version":"616775f16134fa9d01fc677ad3f76e68c051a056c22ab552c64cc281a9686790","impliedFormat":1},{"version":"65c24a8baa2cca1de069a0ba9fba82a173690f52d7e2d0f1f7542d59d5eb4db0","impliedFormat":1},{"version":"f9fe6af238339a0e5f7563acee3178f51db37f32a2e7c09f85273098cee7ec49","impliedFormat":1},{"version":"1de8c302fd35220d8f29dea378a4ae45199dc8ff83ca9923aca1400f2b28848a","impliedFormat":1},{"version":"77e71242e71ebf8528c5802993697878f0533db8f2299b4d36aa015bae08a79c","impliedFormat":1},{"version":"98a787be42bd92f8c2a37d7df5f13e5992da0d967fab794adbb7ee18370f9849","impliedFormat":1},{"version":"332248ee37cca52903572e66c11bef755ccc6e235835e63d3c3e60ddda3e9b93","impliedFormat":1},{"version":"94e8cc88ae2ef3d920bb3bdc369f48436db123aa2dc07f683309ad8c9968a1e1","impliedFormat":1},{"version":"4545c1a1ceca170d5d83452dd7c4994644c35cf676a671412601689d9a62da35","impliedFormat":1},{"version":"320f4091e33548b554d2214ce5fc31c96631b513dffa806e2e3a60766c8c49d9","impliedFormat":1},{"version":"a2d648d333cf67b9aeac5d81a1a379d563a8ffa91ddd61c6179f68de724260ff","impliedFormat":1},{"version":"d90d5f524de38889d1e1dbc2aeef00060d779f8688c02766ddb9ca195e4a713d","impliedFormat":1},{"version":"a3f41ed1b4f2fc3049394b945a68ae4fdefd49fa1739c32f149d32c0545d67f5","impliedFormat":1},{"version":"b0309e1eda99a9e76f87c18992d9c3689b0938266242835dd4611f2b69efe456","impliedFormat":1},{"version":"47699512e6d8bebf7be488182427189f999affe3addc1c87c882d36b7f2d0b0e","impliedFormat":1},{"version":"6ceb10ca57943be87ff9debe978f4ab73593c0c85ee802c051a93fc96aaf7a20","impliedFormat":1},{"version":"1de3ffe0cc28a9fe2ac761ece075826836b5a02f340b412510a59ba1d41a505a","impliedFormat":1},{"version":"e46d6cc08d243d8d0d83986f609d830991f00450fb234f5b2f861648c42dc0d8","impliedFormat":1},{"version":"1c0a98de1323051010ce5b958ad47bc1c007f7921973123c999300e2b7b0ecc0","impliedFormat":1},{"version":"ff863d17c6c659440f7c5c536e4db7762d8c2565547b2608f36b798a743606ca","impliedFormat":1},{"version":"5412ad0043cd60d1f1406fc12cb4fb987e9a734decbdd4db6f6acf71791e36fe","impliedFormat":1},{"version":"ad036a85efcd9e5b4f7dd5c1a7362c8478f9a3b6c3554654ca24a29aa850a9c5","impliedFormat":1},{"version":"fedebeae32c5cdd1a85b4e0504a01996e4a8adf3dfa72876920d3dd6e42978e7","impliedFormat":1},{"version":"b6c1f64158da02580f55e8a2728eda6805f79419aed46a930f43e68ad66a38fc","impliedFormat":1},{"version":"cdf21eee8007e339b1b9945abf4a7b44930b1d695cc528459e68a3adc39a622e","impliedFormat":1},{"version":"bc9ee0192f056b3d5527bcd78dc3f9e527a9ba2bdc0a2c296fbc9027147df4b2","impliedFormat":1},{"version":"330896c1a2b9693edd617be24fbf9e5895d6e18c7955d6c08f028f272b37314d","impliedFormat":1},{"version":"1d9c0a9a6df4e8f29dc84c25c5aa0bb1da5456ebede7a03e03df08bb8b27bae6","impliedFormat":1},{"version":"84380af21da938a567c65ef95aefb5354f676368ee1a1cbb4cae81604a4c7d17","impliedFormat":1},{"version":"1af3e1f2a5d1332e136f8b0b95c0e6c0a02aaabd5092b36b64f3042a03debf28","impliedFormat":1},{"version":"30d8da250766efa99490fc02801047c2c6d72dd0da1bba6581c7e80d1d8842a4","impliedFormat":1},{"version":"03566202f5553bd2d9de22dfab0c61aa163cabb64f0223c08431fb3fc8f70280","impliedFormat":1},{"version":"4c0a1233155afb94bd4d7518c75c84f98567cd5f13fc215d258de196cdb40d91","impliedFormat":1},{"version":"e7765aa8bcb74a38b3230d212b4547686eb9796621ffb4367a104451c3f9614f","impliedFormat":1},{"version":"1de80059b8078ea5749941c9f863aa970b4735bdbb003be4925c853a8b6b4450","impliedFormat":1},{"version":"1d079c37fa53e3c21ed3fa214a27507bda9991f2a41458705b19ed8c2b61173d","impliedFormat":1},{"version":"5bf5c7a44e779790d1eb54c234b668b15e34affa95e78eada73e5757f61ed76a","impliedFormat":1},{"version":"5835a6e0d7cd2738e56b671af0e561e7c1b4fb77751383672f4b009f4e161d70","impliedFormat":1},{"version":"5c634644d45a1b6bc7b05e71e05e52ec04f3d73d9ac85d5927f647a5f965181a","impliedFormat":1},{"version":"4b7f74b772140395e7af67c4841be1ab867c11b3b82a51b1aeb692822b76c872","impliedFormat":1},{"version":"27be6622e2922a1b412eb057faa854831b95db9db5035c3f6d4b677b902ab3b7","impliedFormat":1},{"version":"a68d4b3182e8d776cdede7ac9630c209a7bfbb59191f99a52479151816ef9f9e","impliedFormat":99},{"version":"39644b343e4e3d748344af8182111e3bbc594930fff0170256567e13bbdbebb0","impliedFormat":99},{"version":"ed7fd5160b47b0de3b1571c5c5578e8e7e3314e33ae0b8ea85a895774ee64749","impliedFormat":99},{"version":"63a7595a5015e65262557f883463f934904959da563b4f788306f699411e9bac","impliedFormat":1},{"version":"4ba137d6553965703b6b55fd2000b4e07ba365f8caeb0359162ad7247f9707a6","impliedFormat":1},{"version":"6de125ea94866c736c6d58d68eb15272cf7d1020a5b459fea1c660027eca9a90","affectsGlobalScope":true,"impliedFormat":1},{"version":"8fac4a15690b27612d8474fb2fc7cc00388df52d169791b78d1a3645d60b4c8b","affectsGlobalScope":true,"impliedFormat":1},{"version":"064ac1c2ac4b2867c2ceaa74bbdce0cb6a4c16e7c31a6497097159c18f74aa7c","impliedFormat":1},{"version":"3dc14e1ab45e497e5d5e4295271d54ff689aeae00b4277979fdd10fa563540ae","impliedFormat":1},{"version":"d3b315763d91265d6b0e7e7fa93cfdb8a80ce7cdd2d9f55ba0f37a22db00bdb8","impliedFormat":1},{"version":"b789bf89eb19c777ed1e956dbad0925ca795701552d22e68fd130a032008b9f9","impliedFormat":1},{"version":"9e3136f33efb5a307bfdab496c32a204591d80ed839c5c39c7af5a104b921c37","affectsGlobalScope":true},"7ad303e40d4fddf44f156129e397511953a71481c5cfd86b1862649aaaf240cc",{"version":"614bce25b089c3f19b1e17a6346c74b858034040154c6621e7d35303004767cc","signature":"435a1e418e8338be3f39614b96b81a9aa2700bc8c27bc6b98f064ff9ce17c363"},{"version":"df397571b06f1b7133e8010f1685d60efac5c0f1586236b4009ba3233b0d970c","signature":"1e8c4e4b51463a01f5228b483fe4e6e809f72cf51c82a347145e9079814cd1b2"},{"version":"f3d8c757e148ad968f0d98697987db363070abada5f503da3c06aefd9d4248c1","impliedFormat":1},{"version":"91dfafc38758c5669d16229910b451fa06357f86f7c1f2f0e932c3205e9e8973","impliedFormat":1},{"version":"c1a2e05eb6d7ca8d7e4a7f4c93ccf0c2857e842a64c98eaee4d85841ee9855e6","impliedFormat":1},{"version":"835fb2909ce458740fb4a49fc61709896c6864f5ce3db7f0a88f06c720d74d02","impliedFormat":1},{"version":"6e5857f38aa297a859cab4ec891408659218a5a2610cd317b6dcbef9979459cc","impliedFormat":1},{"version":"ead8e39c2e11891f286b06ae2aa71f208b1802661fcdb2425cffa4f494a68854","impliedFormat":1},{"version":"82919acbb38870fcf5786ec1292f0f5afe490f9b3060123e48675831bd947192","impliedFormat":1},{"version":"e222701788ec77bd57c28facbbd142eadf5c749a74d586bc2f317db7e33544b1","impliedFormat":1},{"version":"09154713fae0ed7befacdad783e5bd1970c06fc41a5f866f7f933b96312ce764","impliedFormat":1},{"version":"8d67b13da77316a8a2fabc21d340866ddf8a4b99e76a6c951cc45189142df652","impliedFormat":1},{"version":"a91c8d28d10fee7fe717ddf3743f287b68770c813c98f796b6e38d5d164bd459","impliedFormat":1},{"version":"68add36d9632bc096d7245d24d6b0b8ad5f125183016102a3dad4c9c2438ccb0","impliedFormat":1},{"version":"3a819c2928ee06bbcc84e2797fd3558ae2ebb7e0ed8d87f71732fb2e2acc87b4","impliedFormat":1},{"version":"f6f827cd43e92685f194002d6b52a9408309cda1cec46fb7ca8489a95cbd2fd4","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"a270a1a893d1aee5a3c1c8c276cd2778aa970a2741ee2ccf29cc3210d7da80f5","impliedFormat":1},{"version":"add0ce7b77ba5b308492fa68f77f24d1ed1d9148534bdf05ac17c30763fc1a79","impliedFormat":1},{"version":"8926594ee895917e90701d8cbb5fdf77fc238b266ac540f929c7253f8ad6233d","impliedFormat":1},{"version":"2f67911e4bf4e0717dc2ded248ce2d5e4398d945ee13889a6852c1233ea41508","impliedFormat":1},{"version":"d8430c275b0f59417ea8e173cfb888a4477b430ec35b595bf734f3ec7a7d729f","impliedFormat":1},{"version":"69364df1c776372d7df1fb46a6cb3a6bf7f55e700f533a104e3f9d70a32bec18","impliedFormat":1},{"version":"6042774c61ece4ba77b3bf375f15942eb054675b7957882a00c22c0e4fe5865c","impliedFormat":1},{"version":"5a3bd57ed7a9d9afef74c75f77fce79ba3c786401af9810cdf45907c4e93f30e","impliedFormat":1},{"version":"ed8763205f02fb65e84eff7432155258df7f93b7d938f01785cb447d043d53f3","impliedFormat":1},{"version":"30db853bb2e60170ba11e39ab48bacecb32d06d4def89eedf17e58ebab762a65","impliedFormat":1},{"version":"e27451b24234dfed45f6cf22112a04955183a99c42a2691fb4936d63cfe42761","impliedFormat":1},{"version":"2316301dd223d31962d917999acf8e543e0119c5d24ec984c9f22cb23247160c","impliedFormat":1},{"version":"58d65a2803c3b6629b0e18c8bf1bc883a686fcf0333230dd0151ab6e85b74307","impliedFormat":1},{"version":"e818471014c77c103330aee11f00a7a00b37b35500b53ea6f337aefacd6174c9","impliedFormat":1},{"version":"d4a5b1d2ff02c37643e18db302488cd64c342b00e2786e65caac4e12bda9219b","impliedFormat":1},{"version":"d8bc0c5487582c6d887c32c92d8b4ffb23310146fcb1d82adf4b15c77f57c4ac","impliedFormat":1},{"version":"8cb31102790372bebfd78dd56d6752913b0f3e2cefbeb08375acd9f5ba737155","impliedFormat":1},{"version":"bdd14f07b4eca0b4b5203b85b8dbc4d084c749fa590bee5ea613e1641dcd3b29","impliedFormat":99},{"version":"d3cfde44f8089768ebb08098c96d01ca260b88bccf238d55eee93f1c620ff5a5","impliedFormat":1},{"version":"293eadad9dead44c6fd1db6de552663c33f215c55a1bfa2802a1bceed88ff0ec","impliedFormat":1},{"version":"36eb5babc665b890786550d4a8cb20ef7105673a6d5551fbdd7012877bb26942","impliedFormat":1},{"version":"fec412ded391a7239ef58f455278154b62939370309c1fed322293d98c8796a6","impliedFormat":1},{"version":"e3498cf5e428e6c6b9e97bd88736f26d6cf147dedbfa5a8ad3ed8e05e059af8a","impliedFormat":1},{"version":"dba3f34531fd9b1b6e072928b6f885aa4d28dd6789cbd0e93563d43f4b62da53","impliedFormat":1},{"version":"f672c876c1a04a223cf2023b3d91e8a52bb1544c576b81bf64a8fec82be9969c","impliedFormat":1},{"version":"e4b03ddcf8563b1c0aee782a185286ed85a255ce8a30df8453aade2188bbc904","impliedFormat":1},{"version":"2329d90062487e1eaca87b5e06abcbbeeecf80a82f65f949fd332cfcf824b87b","impliedFormat":1},{"version":"25b3f581e12ede11e5739f57a86e8668fbc0124f6649506def306cad2c59d262","impliedFormat":1},{"version":"93c3e73824ad57f98fd23b39335dbdae2db0bd98199b0dc0b9ccc60bf3c5134a","impliedFormat":1},{"version":"a9ebb67d6bbead6044b43714b50dcb77b8f7541ffe803046fdec1714c1eba206","impliedFormat":1},{"version":"833e92c058d033cde3f29a6c7603f517001d1ddd8020bc94d2067a3bc69b2a8e","impliedFormat":1},{"version":"56e0775830b68d13c3d7f4ec75df7d016db6b879ef9676affb5233a9a289c192","impliedFormat":99},{"version":"7447894374c0341e146e4b9ec33f88cfba6d4aaaa4a2f675b0d14aa7e3f440d1","impliedFormat":1},{"version":"093ee0a7eb9b460fbfe7e9b39e722a0664f91505346b739849f73ed31ac4a3e9","impliedFormat":1},{"version":"914a62576d3c5cde90fd6941efb30ace0824e38ee800fc05b3ee22fcc7bbabee","impliedFormat":1},{"version":"a4e9e0d92dcad2cb387a5f1bdffe621569052f2d80186e11973aa7080260d296","impliedFormat":1},{"version":"f6380cc36fc3efc70084d288d0a05d0a2e09da012ee3853f9d62431e7216f129","impliedFormat":1},{"version":"497c3e541b4acf6c5d5ba75b03569cfe5fe25c8a87e6c87f1af98da6a3e7b918","impliedFormat":1},{"version":"d9429b81edf2fb2abf1e81e9c2e92615f596ed3166673d9b69b84c369b15fdc0","impliedFormat":1},{"version":"7e22943ae4e474854ca0695ab750a8026f55bb94278331fda02a4fb42efce063","impliedFormat":1},{"version":"7da9ff3d9a7e62ddca6393a23e67296ab88f2fcb94ee5f7fb977fa8e478852ac","impliedFormat":1},{"version":"e1b45cc21ea200308cbc8abae2fb0cfd014cb5b0e1d1643bcc50afa5959b6d83","impliedFormat":1},{"version":"c9740b0ce7533ce6ba21a7d424e38d2736acdddeab2b1a814c00396e62cc2f10","impliedFormat":1},{"version":"b3c1f6a3fdbb04c6b244de6d5772ffdd9e962a2faea1440e410049c13e874b87","impliedFormat":1},{"version":"dcaa872d9b52b9409979170734bdfd38f846c32114d05b70640fd05140b171bb","impliedFormat":1},{"version":"6c434d20da381fcd2e8b924a3ec9b8653cf8bed8e0da648e91f4c984bd2a5a91","impliedFormat":1},{"version":"992419d044caf6b14946fa7b9463819ab2eeb7af7c04919cc2087ce354c92266","impliedFormat":1},{"version":"fa9815e9ce1330289a5c0192e2e91eb6178c0caa83c19fe0c6a9f67013fe795c","impliedFormat":1},{"version":"06384a1a73fcf4524952ecd0d6b63171c5d41dd23573907a91ef0a687ddb4a8c","impliedFormat":1},{"version":"34b1594ecf1c84bcc7a04d9f583afa6345a6fea27a52cf2685f802629219de45","impliedFormat":1},{"version":"d82c9ca830d7b94b7530a2c5819064d8255b93dfeddc5b2ebb8a09316f002c89","impliedFormat":1},{"version":"7e046b9634add57e512412a7881efbc14d44d1c65eadd35432412aa564537975","impliedFormat":1},{"version":"aac9079b9e2b5180036f27ab37cb3cf4fd19955be48ccc82eab3f092ee3d4026","impliedFormat":1},{"version":"3d9c38933bc69e0a885da20f019de441a3b5433ce041ba5b9d3a541db4b568cb","impliedFormat":1},{"version":"606aa2b74372221b0f79ca8ae3568629f444cc454aa59b032e4cb602308dec94","impliedFormat":1},{"version":"50474eaea72bfda85cc37ae6cd29f0556965c0849495d96c8c04c940ef3d2f44","impliedFormat":1},{"version":"b4874382f863cf7dc82b3d15aed1e1372ac3fede462065d5bfc8510c0d8f7b19","impliedFormat":1},{"version":"df10b4f781871afb72b2d648d497671190b16b679bf7533b744cc10b3c6bf7ea","impliedFormat":1},{"version":"1fdc28754c77e852c92087c789a1461aa6eed19c335dc92ce6b16a188e7ba305","impliedFormat":1},{"version":"a656dab1d502d4ddc845b66d8735c484bfebbf0b1eda5fb29729222675759884","impliedFormat":1},{"version":"465a79505258d251068dc0047a67a3605dd26e6b15e9ad2cec297442cbb58820","impliedFormat":1},{"version":"ddae22d9329db28ce3d80a2a53f99eaed66959c1c9cd719c9b744e5470579d2f","impliedFormat":1},{"version":"d0e25feadef054c6fc6a7f55ccc3b27b7216142106b9ff50f5e7b19d85c62ca7","impliedFormat":1},{"version":"111214009193320cacbae104e8281f6cb37788b52a6a84d259f9822c8c71f6ca","impliedFormat":1},{"version":"01c8e2c8984c96b9b48be20ee396bd3689a3a3e6add8d50fe8229a7d4e62ff45","impliedFormat":1},{"version":"a4a0800b592e533897b4967b00fb00f7cd48af9714d300767cc231271aa100af","impliedFormat":1},{"version":"20aa818c3e16e40586f2fa26327ea17242c8873fe3412a69ec68846017219314","impliedFormat":1},{"version":"f498532f53d54f831851990cb4bcd96063d73e302906fa07e2df24aa5935c7d1","impliedFormat":1},{"version":"5fd19dfde8de7a0b91df6a9bbdc44b648fd1f245cae9e8b8cf210d83ee06f106","impliedFormat":1},{"version":"3b8d6638c32e63ea0679eb26d1eb78534f4cc02c27b80f1c0a19f348774f5571","impliedFormat":1},{"version":"ce0da52e69bc3d82a7b5bc40da6baad08d3790de13ad35e89148a88055b46809","impliedFormat":1},{"version":"9e01233da81bfed887f8d9a70d1a26bf11b8ddff165806cc586c84980bf8fc24","impliedFormat":1},{"version":"214a6afbab8b285fc97eb3cece36cae65ea2fca3cbd0c017a96159b14050d202","impliedFormat":1},{"version":"14beeca2944b75b229c0549e0996dc4b7863e07257e0d359d63a7be49a6b86a4","impliedFormat":1},{"version":"f7bb9adb1daa749208b47d1313a46837e4d27687f85a3af7777fc1c9b3dc06b1","impliedFormat":1},{"version":"c549fe2f52101ffe47f58107c702af7cdcd42da8c80afd79f707d1c5d77d4b6e","impliedFormat":1},{"version":"3966ea9e1c1a5f6e636606785999734988e135541b79adc6b5d00abdc0f4bf05","impliedFormat":1},{"version":"0b60b69c957adb27f990fbc27ea4ac1064249400262d7c4c1b0a1687506b3406","impliedFormat":1},{"version":"12c26e5d1befc0ded725cee4c2316f276013e6f2eb545966562ae9a0c1931357","impliedFormat":1},{"version":"27b247363f1376c12310f73ebac6debcde009c0b95b65a8207e4fa90e132b30a","impliedFormat":1},{"version":"05bd302e2249da923048c09dc684d1d74cb205551a87f22fb8badc09ec532a08","impliedFormat":1},{"version":"fe930ec064571ab3b698b13bddf60a29abf9d2f36d51ab1ca0083b087b061f3a","impliedFormat":1},{"version":"6b85c4198e4b62b0056d55135ad95909adf1b95c9a86cdbed2c0f4cc1a902d53","impliedFormat":1},{"version":"258a35442d9b7bfb92eb074413c918f4019f56d02ba02ffb738773858f078306","affectsGlobalScope":true,"impliedFormat":1},{"version":"5ae2a6b71359d4996a4adc3c17a11822bfc447326fdb92a90300ab3059ea9894","signature":"dfbee4fe36243eb397284cac4a0c2b3159d4fab8ec4554770bf7269e2585c174"},"229954b86da9b9d039833e3b597ec916e7ecfd87d0f5e28d020b127559839e96",{"version":"bad1a6f854284258834cbdd10fa9dc28285f4faf5ece06ce6d5ca2cadd1f8fbd","signature":"82c2fc934c32c88a52883832d958aa19c61096f0650b5294333e12cdb8e37450"},"d7da91a02063791f9ab121395f9899ce0f7d8c31db0a63bdb2e5b7be94465994",{"version":"b20230c18deda35c7a731fbd5b5e137e7ae4da9de8904380753670c5f548c543","signature":"5a2fc30193feccb9ec9152d731d1284af2222ca8ba2e355ec6766079d1c68e81"},{"version":"23276243e00f6b1377cb1616fd44db300f4c31b1d4a18c4b45e7067fa93620e7","signature":"9bdbb3505885e4817f58c97737c336e29a67a92f6b4dfdf555e6a12927998840"},{"version":"28015968960abf4aebd9816ca3dbf54e5ca6c5f5869da4fdada079b1c1f590dc","signature":"a3df50373eaab5122c1384f66eac08aedf5505f13be691f57565e2dc852a3ea0"},{"version":"4dfd4c013fc774e8e59462a96610ed2f9b3094149687bd4c761ca050540c1082","signature":"568fabfdd0cb08963c988854efd340b3ef285535acb168f47c2cac1d34153b80"},{"version":"578073792c7aefc464e5e88866b40171bf1372ceb0a98eeded534bdc1924ba4b","signature":"fd3bf10d18fdbe957d4c2419cb5461464f9768cdeffb438bc79362b7893573c8"},{"version":"4da1563eec57f2490520009f227bcb6c1c9b7e9b25ae2dc379d8463b847ce82f","signature":"28e1896d048cdb6ab0de4949e3eb45d8e66f28d5cdbae8c0b5ba1e8e57fe7063"},{"version":"c57b441e0c0a9cbdfa7d850dae1f8a387d6f81cbffbc3cd0465d530084c2417d","impliedFormat":99},{"version":"51954e948be6a5b728fcfaf561f12331b4f54f068934c77adfc8f70eea17d285","impliedFormat":1},{"version":"8c814cc039880c322ce90ee9f1a3cf2f363097b8047b9554f244aed4687ae259","signature":"8f6fb542a9b9d3196a230e5d798a7586722d7504671aff6cb25898f14ca941d4"},"b8da1a147ba97d63c7ccda60e57be0a4f0b1bc8e0494c69cdd8f78d42109f8f3",{"version":"eb9fb9429a91ec2abef7711e43563467765f0eeda15cabd2513c53cd55494585","signature":"62b3d58dcc54db6c53070a654449a8235fccbc385ea1df76ee65ed3505672e6b"},{"version":"632f5a84693be63cb1e0675dca12a7632198e3fbbab18b4f5e41ee8abd95bfa6","signature":"d526e7a916580314d50095f12b67f8f504096d0b41eef659c1a088d4ea907ce4"},{"version":"560ac6e6b9012ce3851bfabafd59a7d207ba77c65f11f1f082796e559ff33ed5","signature":"dfa036bde24da573fe4de894f7a6f325a859633475e90dcc356ed9d586c1bb66"},{"version":"bdbe46bac3b52b54ce51ab217a93dfe58260430e37fc8ef0fb99fa2f2bc686f5","signature":"0c6f146bf5402327aa93d97c9e263e92bb63b4d87f2af155416cc7d0490a8224"},{"version":"d7dfba64b7350cb9501c544fe8ceba1b2455b42029d22b1a4fd02e94a6783525","impliedFormat":1},{"version":"f7a3685a047aa0810e025c12a2e1b739b1884bb1c01ed37aec112533ad8c7f92","signature":"b117f257845f72bd59a26b2c6e36c9bbb5259182e7b4c14c0ed7f260de9dcff4"},"01a5460aa0a6cca94fbf1556e2ccf610f4367afb958a49be8967fbfda29e0336",{"version":"89121c1bf2990f5219bfd802a3e7fc557de447c62058d6af68d6b6348d64499a","impliedFormat":1},{"version":"79b4369233a12c6fa4a07301ecb7085802c98f3a77cf9ab97eee27e1656f82e6","impliedFormat":1},{"version":"2b37ba54ec067598bf912d56fcb81f6d8ad86a045c757e79440bdef97b52fe1b","impliedFormat":99},{"version":"1bc9dd465634109668661f998485a32da369755d9f32b5a55ed64a525566c94b","impliedFormat":99},{"version":"5702b3c2f5d248290ed99419d77ca1cc3e6c29db5847172377659c50e6303768","impliedFormat":99},{"version":"9764b2eb5b4fc0b8951468fb3dbd6cd922d7752343ef5fbf1a7cd3dfcd54a75e","impliedFormat":99},{"version":"1fc2d3fe8f31c52c802c4dee6c0157c5a1d1f6be44ece83c49174e316cf931ad","impliedFormat":99},{"version":"dc4aae103a0c812121d9db1f7a5ea98231801ed405bf577d1c9c46a893177e36","impliedFormat":99},{"version":"106d3f40907ba68d2ad8ce143a68358bad476e1cc4a5c710c11c7dbaac878308","impliedFormat":99},{"version":"42ad582d92b058b88570d5be95393cf0a6c09a29ba9aa44609465b41d39d2534","impliedFormat":99},{"version":"36e051a1e0d2f2a808dbb164d846be09b5d98e8b782b37922a3b75f57ee66698","impliedFormat":99},{"version":"d4a22007b481fe2a2e6bfd3a42c00cd62d41edb36d30fc4697df2692e9891fc8","impliedFormat":1},{"version":"9d62e577adb05f5aafed137e747b3a1b26f8dce7b20f350d22f6fb3255a3c0ed","impliedFormat":99},{"version":"7ed92bcef308af6e3925b3b61c83ad6157a03ff15c7412cf325f24042fe5d363","impliedFormat":99},{"version":"3da9062d0c762c002b7ab88187d72e1978c0224db61832221edc8f4eb0b54414","impliedFormat":99},{"version":"84dbf6af43b0b5ad42c01e332fddf4c690038248140d7c4ccb74a424e9226d4d","impliedFormat":99},{"version":"00884fc0ea3731a9ffecffcde8b32e181b20e1039977a8ae93ae5bce3ab3d245","impliedFormat":99},{"version":"0bd8b6493d9bf244afe133ccb52d32d293de8d08d15437cca2089beed5f5a6b5","impliedFormat":99},{"version":"7fc3099c95752c6e7b0ea215915464c7203e835fcd6878210f2ce4f0dcbbfe67","impliedFormat":99},{"version":"83b5499dbc74ee1add93aef162f7d44b769dcef3a74afb5f80c70f9a5ce77cc0","impliedFormat":99},{"version":"8bf8b772b38fc4da471248320f49a2219c363a9669938c720e0e0a5a2531eabf","impliedFormat":99},{"version":"7da6e8c98eacf084c961e039255f7ebb9d97a43377e7eee2695cb77fec640c66","impliedFormat":99},{"version":"0b5b064c5145a48cd3e2a5d9528c63f49bac55aa4bc5f5b4e68a160066401375","impliedFormat":99},{"version":"702ff40d28906c05d9d60b23e646c2577ad1cc7cd177d5c0791255a2eab13c07","impliedFormat":99},{"version":"49ff0f30d6e757d865ae0b422103f42737234e624815eee2b7f523240aa0c8f8","impliedFormat":99},{"version":"0389aacf0ffd49a877a46814a21a4770f33fc33e99951a1584de866c8e971993","impliedFormat":99},{"version":"5cb7a51cf151c1056b61f078cf80b811e19787d1f29a33a2a6e4bf00334bbc10","impliedFormat":99},{"version":"215aa8915d707f97ad511b7abbf7eda51d3a7048e9a656955cf0dda767ae7db0","impliedFormat":99},{"version":"0d689a717fbef83da07ab4de33f83db5cbcec9bc4e3b04edb106c538a50a0210","impliedFormat":99},{"version":"d00bc73e8d1f4137f2f6238bb3aa2bbdad8573658cc95920e2cdfa7ad491a8d8","impliedFormat":99},{"version":"e3667aa9f5245d1a99fb4a2a1ac48daf1429040c29cc0d262e3843f9ae3b9d65","impliedFormat":99},{"version":"08c0f3222b50ec2b534be1a59392660102549129246425d33ec43f35aa051dc6","impliedFormat":99},{"version":"612fb780f312e6bb3c40f3cb2b827ea7455b922198f651c799d844fdd44cf2e9","impliedFormat":99},{"version":"bcd98e8f44bc76e4fcb41e4b1a8bab648161a942653a3d1f261775a891d258de","impliedFormat":99},{"version":"5abaa19aa91bb4f63ea58154ada5d021e33b1f39aa026ca56eb95f13b12c497a","impliedFormat":99},{"version":"356a18b0c50f297fee148f4a2c64b0affd352cbd6f21c7b6bfa569d30622c693","impliedFormat":99},{"version":"5876027679fd5257b92eb55d62efee634358012b9f25c5711ad02b918e52c837","impliedFormat":99},{"version":"f5622423ee5642dcf2b92d71b37967b458e8df3cf90b468675ff9fddaa532a0f","impliedFormat":99},{"version":"70265bc75baf24ec0d61f12517b91ea711732b9c349fceef71a446c4ff4a247a","impliedFormat":99},{"version":"41a4b2454b2d3a13b4fc4ec57d6a0a639127369f87da8f28037943019705d619","impliedFormat":99},{"version":"e9b82ac7186490d18dffaafda695f5d975dfee549096c0bf883387a8b6c3ab5a","impliedFormat":99},{"version":"eed9b5f5a6998abe0b408db4b8847a46eb401c9924ddc5b24b1cede3ebf4ee8c","impliedFormat":99},{"version":"af85fde8986fdad68e96e871ae2d5278adaf2922d9879043b9313b18fae920b1","impliedFormat":99},{"version":"8a1f5d2f7cf4bf851cc9baae82056c3316d3c6d29561df28aff525556095554b","impliedFormat":99},{"version":"a5dbd4c9941b614526619bad31047ddd5f504ec4cdad88d6117b549faef34dd3","impliedFormat":99},{"version":"e87873f06fa094e76ac439c7756b264f3c76a41deb8bc7d39c1d30e0f03ef547","impliedFormat":99},{"version":"488861dc4f870c77c2f2f72c1f27a63fa2e81106f308e3fc345581938928f925","impliedFormat":99},{"version":"eff73acfacda1d3e62bb3cb5bc7200bb0257ea0c8857ce45b3fee5bfec38ad12","impliedFormat":99},{"version":"aff4ac6e11917a051b91edbb9a18735fe56bcfd8b1802ea9dbfb394ad8f6ce8e","impliedFormat":99},{"version":"1f68aed2648740ac69c6634c112fcaae4252fbae11379d6eabee09c0fbf00286","impliedFormat":99},{"version":"5e7c2eff249b4a86fb31e6b15e4353c3ddd5c8aefc253f4c3e4d9caeb4a739d4","impliedFormat":99},{"version":"14c8d1819e24a0ccb0aa64f85c61a6436c403eaf44c0e733cdaf1780fed5ec9f","impliedFormat":99},{"version":"011423c04bfafb915ceb4faec12ea882d60acbe482780a667fa5095796c320f8","impliedFormat":99},{"version":"f8eb2909590ec619643841ead2fc4b4b183fbd859848ef051295d35fef9d8469","impliedFormat":99},{"version":"fe784567dd721417e2c4c7c1d7306f4b8611a4f232f5b7ce734382cf34b417d2","impliedFormat":99},{"version":"45d1e8fb4fd3e265b15f5a77866a8e21870eae4c69c473c33289a4b971e93704","impliedFormat":99},{"version":"cd40919f70c875ca07ecc5431cc740e366c008bcbe08ba14b8c78353fb4680df","impliedFormat":99},{"version":"ddfd9196f1f83997873bbe958ce99123f11b062f8309fc09d9c9667b2c284391","impliedFormat":99},{"version":"2999ba314a310f6a333199848166d008d088c6e36d090cbdcc69db67d8ae3154","impliedFormat":99},{"version":"62c1e573cd595d3204dfc02b96eba623020b181d2aa3ce6a33e030bc83bebb41","impliedFormat":99},{"version":"ca1616999d6ded0160fea978088a57df492b6c3f8c457a5879837a7e68d69033","impliedFormat":99},{"version":"835e3d95251bbc48918bb874768c13b8986b87ea60471ad8eceb6e38ddd8845e","impliedFormat":99},{"version":"de54e18f04dbcc892a4b4241b9e4c233cfce9be02ac5f43a631bbc25f479cd84","impliedFormat":99},{"version":"453fb9934e71eb8b52347e581b36c01d7751121a75a5cd1a96e3237e3fd9fc7e","impliedFormat":99},{"version":"bc1a1d0eba489e3eb5c2a4aa8cd986c700692b07a76a60b73a3c31e52c7ef983","impliedFormat":99},{"version":"4098e612efd242b5e203c5c0b9afbf7473209905ab2830598be5c7b3942643d0","impliedFormat":99},{"version":"28410cfb9a798bd7d0327fbf0afd4c4038799b1d6a3f86116dc972e31156b6d2","impliedFormat":99},{"version":"514ae9be6724e2164eb38f2a903ef56cf1d0e6ddb62d0d40f155f32d1317c116","impliedFormat":99},{"version":"970e5e94a9071fd5b5c41e2710c0ef7d73e7f7732911681592669e3f7bd06308","impliedFormat":99},{"version":"491fb8b0e0aef777cec1339cb8f5a1a599ed4973ee22a2f02812dd0f48bd78c1","impliedFormat":99},{"version":"6acf0b3018881977d2cfe4382ac3e3db7e103904c4b634be908f1ade06eb302d","impliedFormat":99},{"version":"2dbb2e03b4b7f6524ad5683e7b5aa2e6aef9c83cab1678afd8467fde6d5a3a92","impliedFormat":99},{"version":"135b12824cd5e495ea0a8f7e29aba52e1adb4581bb1e279fb179304ba60c0a44","impliedFormat":99},{"version":"e4c784392051f4bbb80304d3a909da18c98bc58b093456a09b3e3a1b7b10937f","impliedFormat":99},{"version":"2e87c3480512f057f2e7f44f6498b7e3677196e84e0884618fc9e8b6d6228bed","impliedFormat":99},{"version":"66984309d771b6b085e3369227077da237b40e798570f0a2ddbfea383db39812","impliedFormat":99},{"version":"e41be8943835ad083a4f8a558bd2a89b7fe39619ed99f1880187c75e231d033e","impliedFormat":99},{"version":"260558fff7344e4985cfc78472ae58cbc2487e406d23c1ddaf4d484618ce4cfd","impliedFormat":99},{"version":"413d50bc66826f899c842524e5f50f42d45c8cb3b26fd478a62f26ac8da3d90e","impliedFormat":99},{"version":"d9083e10a491b6f8291c7265555ba0e9d599d1f76282812c399ab7639019f365","impliedFormat":99},{"version":"09de774ebab62974edad71cb3c7c6fa786a3fda2644e6473392bd4b600a9c79c","impliedFormat":99},{"version":"e8bcc823792be321f581fcdd8d0f2639d417894e67604d884c38b699284a1a2a","impliedFormat":99},{"version":"7c99839c518dcf5ab8a741a97c190f0703c0a71e30c6d44f0b7921b0deec9f67","impliedFormat":99},{"version":"44c14e4da99cd71f9fe4e415756585cec74b9e7dc47478a837d5bedfb7db1e04","impliedFormat":99},{"version":"1f46ee2b76d9ae1159deb43d14279d04bcebcb9b75de4012b14b1f7486e36f82","impliedFormat":99},{"version":"2838028b54b421306639f4419606306b940a5c5fcc5bc485954cbb0ab84d90f4","impliedFormat":99},{"version":"7116e0399952e03afe9749a77ceaca29b0e1950989375066a9ddc9cb0b7dd252","impliedFormat":99},{"version":"f14de99b306ff28683564ecddfc93ef434cbb45ea74a0defb6a860abf393c0f9","signature":"e3d5ddcd646cb6e2b64f7579ee228b62d357e32dfa5f8ab6cdc5df8a215d22f1"},"8fd655ae64496fd1f6aa55a6d9a12a9418cd24beb8aa7ceb857a43f21c3c785c","2223c710045712277eb843e0272048f7c8e72c5d3e5922bc7eb704a78a0d4555",{"version":"404d13eabbffa98022c97c219cdba5d7e8086fedc263f99838e99309c5d4fdb9","signature":"a267ba725d131a589610dad697fc2a5d694b42bdde890abd10dc875c12d0b859"},"bfd02143c2a5ca283f27b254c0ad8dd1c0bfc610db73d7b4a243b5ea1cc10e72",{"version":"a07fbc16b35a807d436812dd60ea565a0a2ba3fddde14eeffe9e8851e01aff44","signature":"85ee66da836de7d0f231efd35f346cd2d7846ba3106146097fdbbad10f348c26"},{"version":"a55ea32788508ea0824edb523af8e6be62bf5b143a9be35df118ada8242e21d8","signature":"af14ac4526d095207bcedbf3fea29e1806849ed33f942f4ea549f48021d69111"},"effe68eef803797f6a94474e1c42d4e57a87c2ffa38c85d2cc17632a5c69f807","9fc3ae34e7d4170e7f7fcedb698fef71636df26ce58f9beefabf039952189c16","440d946c9ac8244fe5ae98f444f2f99b488f0b1590041359e9052572e2cda7b2","21905d3e1319d88b35a1ad7f0895c809e84d36761835987d4b7635d8bebcba10",{"version":"9e3136f33efb5a307bfdab496c32a204591d80ed839c5c39c7af5a104b921c37","affectsGlobalScope":true},"f2469bf590db12967eecee75a1c022a50aaa0db5193006fa49d62a73581be51d","d1986184a09a52db8228cb2bb2a61a8c05c9354e5b93cec8e2628d8579c892d7","45e8e8ee0dbe1ecc2eceb79b3606b56d60d71bdeafaccc67fd78c88ea961048f",{"version":"fb893a0dfc3c9fb0f9ca93d0648694dd95f33cbad2c0f2c629f842981dfd4e2e","impliedFormat":1},{"version":"3eb11dbf3489064a47a2e1cf9d261b1f100ef0b3b50ffca6c44dd99d6dd81ac1","impliedFormat":1},{"version":"151ff381ef9ff8da2da9b9663ebf657eac35c4c9a19183420c05728f31a6761d","impliedFormat":1},{"version":"5d08a179b846f5ee674624b349ebebe2121c455e3a265dc93da4e8d9e89722b4","impliedFormat":1},{"version":"96d14f21b7652903852eef49379d04dbda28c16ed36468f8c9fa08f7c14c9538","impliedFormat":1},{"version":"54d2709d08dc65b1cb180673e8f667f965a41b35be47e9aade190e931f3e29e8","impliedFormat":1},{"version":"03c258e060b7da220973f84b89615e4e9850e9b5d30b3a8e4840b3e3268ae8eb","impliedFormat":1},{"version":"1ba59c8bbeed2cb75b239bb12041582fa3e8ef32f8d0bd0ec802e38442d3f317","impliedFormat":1},{"version":"74d5a87c3616cd5d8691059d531504403aa857e09cbaecb1c64dfb9ace0db185","impliedFormat":1},{"version":"104c67f0da1bdf0d94865419247e20eded83ce7f9911a1aa75fc675c077ca66e","impliedFormat":1},{"version":"cc0d0b339f31ce0ab3b7a5b714d8e578ce698f1e13d7f8c60bfb766baeb1d35c","impliedFormat":1},{"version":"25be1eb939c9c63242c7a45446edb20c40541da967f43f1aa6a00ed53c0552db","impliedFormat":1},{"version":"d34aa8df2d0b18fb56b1d772ff9b3c7aea7256cf0d692f969be6e1d27b74d660","impliedFormat":1},{"version":"baac9896d29bcc55391d769e408ff400d61273d832dd500f21de766205255acb","impliedFormat":1},{"version":"2f5747b1508ccf83fad0c251ba1e5da2f5a30b78b09ffa1cfaf633045160afed","impliedFormat":1},{"version":"6823ccc7b5b77bbf898d878dbcad18aa45e0fa96bdd0abd0de98d514845d9ed9","affectsGlobalScope":true,"impliedFormat":1},{"version":"b71c603a539078a5e3a039b20f2b0a0d1708967530cf97dec8850a9ca45baa2b","impliedFormat":1},{"version":"168d88e14e0d81fe170e0dadd38ae9d217476c11435ea640ddb9b7382bdb6c1f","impliedFormat":1},{"version":"8e04cf0688e0d921111659c2b55851957017148fa7b977b02727477d155b3c47","impliedFormat":1}],"root":[[511,514],[662,671],[674,679],681,682,[770,784]],"options":{"allowJs":true,"esModuleInterop":true,"jsx":4,"module":99,"skipLibCheck":true,"strict":true,"target":4},"referencedMap":[[783,1],[511,2],[784,3],[781,2],[782,4],[512,5],[513,6],[611,7],[612,8],[610,9],[516,10],[253,2],[621,11],[624,12],[630,13],[633,14],[654,15],[632,16],[613,2],[614,17],[615,18],[618,2],[616,2],[617,2],[655,19],[620,11],[619,2],[656,20],[623,12],[622,2],[660,21],[657,22],[627,23],[629,24],[626,25],[628,26],[625,23],[658,27],[631,11],[659,28],[634,29],[653,30],[650,31],[652,32],[637,33],[644,34],[646,35],[648,36],[647,37],[639,38],[636,31],[640,2],[651,39],[641,40],[638,2],[649,2],[635,2],[642,41],[643,2],[645,42],[595,2],[786,43],[788,44],[787,2],[684,45],[515,2],[789,2],[694,45],[785,2],[141,46],[142,46],[143,47],[97,48],[144,49],[145,50],[146,51],[92,2],[95,52],[93,2],[94,2],[147,53],[148,54],[149,55],[150,56],[151,57],[152,58],[153,58],[154,59],[155,60],[156,61],[157,62],[98,2],[96,2],[158,63],[159,64],[160,65],[192,66],[161,67],[162,68],[163,69],[164,70],[165,71],[166,72],[167,73],[168,74],[169,75],[170,76],[171,76],[172,77],[173,2],[174,78],[176,79],[175,80],[177,81],[178,82],[179,83],[180,84],[181,85],[182,86],[183,87],[184,88],[185,89],[186,90],[187,91],[188,92],[189,93],[99,2],[100,2],[101,2],[140,94],[190,95],[191,96],[196,97],[413,98],[197,99],[195,100],[415,101],[414,102],[193,103],[411,2],[194,104],[83,2],[85,105],[410,98],[270,98],[683,2],[661,106],[672,2],[84,2],[609,2],[680,98],[763,2],[737,107],[736,108],[735,109],[762,110],[761,111],[765,112],[764,113],[767,114],[766,115],[722,116],[696,117],[697,118],[698,118],[699,118],[700,118],[701,118],[702,118],[703,118],[704,118],[705,118],[706,118],[720,119],[707,118],[708,118],[709,118],[710,118],[711,118],[712,118],[713,118],[714,118],[716,118],[717,118],[715,118],[718,118],[719,118],[721,118],[695,120],[760,121],[740,122],[741,122],[742,122],[743,122],[744,122],[745,122],[746,123],[748,122],[747,122],[759,124],[749,122],[751,122],[750,122],[753,122],[752,122],[754,122],[755,122],[756,122],[757,122],[758,122],[739,122],[738,125],[730,126],[728,127],[729,127],[733,128],[731,127],[732,127],[734,127],[727,2],[460,129],[465,1],[455,130],[217,131],[257,132],[439,133],[252,134],[234,2],[409,2],[215,2],[428,135],[283,136],[216,2],[337,137],[260,138],[261,139],[408,140],[425,141],[319,142],[433,143],[434,144],[432,145],[431,2],[429,146],[259,147],[218,148],[362,2],[363,149],[289,150],[219,151],[290,150],[285,150],[206,150],[255,152],[254,2],[438,153],[450,2],[242,2],[384,154],[385,155],[379,98],[487,2],[387,2],[388,156],[380,157],[492,158],[491,159],[486,2],[304,2],[424,160],[423,2],[485,161],[381,98],[313,162],[309,163],[314,164],[312,2],[311,165],[310,2],[488,2],[484,2],[490,166],[489,2],[308,163],[479,167],[482,168],[298,169],[297,170],[296,171],[495,98],[295,172],[277,2],[498,2],[501,2],[500,98],[502,173],[199,2],[435,174],[436,175],[437,176],[212,2],[245,2],[211,177],[198,2],[400,98],[204,178],[399,179],[398,180],[389,2],[390,2],[397,2],[392,2],[395,181],[391,2],[393,182],[396,183],[394,182],[214,2],[209,2],[210,150],[265,2],[271,184],[272,185],[269,186],[267,187],[268,188],[263,2],[406,156],[292,156],[459,189],[466,190],[470,191],[442,192],[441,2],[280,2],[503,193],[454,194],[382,195],[383,196],[377,197],[368,2],[405,198],[444,98],[369,199],[407,200],[402,201],[401,2],[403,2],[374,2],[361,202],[443,203],[446,204],[371,205],[375,206],[366,207],[420,208],[453,209],[323,210],[338,211],[207,212],[452,213],[203,214],[273,215],[264,2],[274,216],[350,217],[262,2],[349,218],[91,2],[343,219],[244,2],[364,220],[339,2],[208,2],[238,2],[347,221],[213,2],[275,222],[373,223],[440,224],[372,2],[346,2],[266,2],[352,225],[353,226],[430,2],[355,227],[357,228],[356,229],[247,2],[345,212],[359,230],[322,231],[344,232],[351,233],[222,2],[226,2],[225,2],[224,2],[229,2],[223,2],[232,2],[231,2],[228,2],[227,2],[230,2],[233,234],[221,2],[331,235],[330,2],[335,236],[332,237],[334,238],[336,236],[333,237],[243,239],[293,240],[449,241],[504,2],[474,242],[476,243],[370,244],[475,245],[447,203],[386,203],[220,2],[324,246],[239,247],[240,248],[241,249],[237,250],[419,250],[287,250],[325,251],[288,251],[236,252],[235,2],[329,253],[328,254],[327,255],[326,256],[448,257],[418,258],[417,259],[378,260],[412,261],[416,262],[427,263],[426,264],[422,265],[321,266],[318,267],[320,268],[317,269],[358,270],[348,2],[464,2],[360,271],[421,2],[276,272],[367,174],[365,273],[278,274],[281,275],[499,2],[279,276],[282,276],[462,2],[461,2],[463,2],[497,2],[284,277],[445,2],[315,278],[307,98],[258,2],[202,279],[291,2],[468,98],[201,2],[478,280],[306,98],[472,156],[305,281],[457,282],[303,280],[205,2],[480,283],[301,98],[302,98],[294,2],[200,2],[300,284],[299,285],[246,286],[376,75],[286,75],[354,2],[341,287],[340,2],[404,163],[316,98],[451,177],[458,288],[86,98],[89,289],[90,290],[87,98],[88,2],[256,291],[251,292],[250,2],[249,293],[248,2],[456,294],[467,295],[469,296],[471,297],[473,298],[477,299],[510,300],[481,300],[509,301],[483,302],[493,303],[494,304],[496,305],[505,306],[508,177],[507,2],[506,307],[726,308],[725,309],[769,310],[768,311],[724,312],[723,313],[342,314],[673,2],[691,315],[690,2],[81,2],[82,2],[13,2],[14,2],[16,2],[15,2],[2,2],[17,2],[18,2],[19,2],[20,2],[21,2],[22,2],[23,2],[24,2],[3,2],[25,2],[26,2],[4,2],[27,2],[31,2],[28,2],[29,2],[30,2],[32,2],[33,2],[34,2],[5,2],[35,2],[36,2],[37,2],[38,2],[6,2],[42,2],[39,2],[40,2],[41,2],[43,2],[7,2],[44,2],[49,2],[50,2],[45,2],[46,2],[47,2],[48,2],[8,2],[54,2],[51,2],[52,2],[53,2],[55,2],[9,2],[56,2],[57,2],[58,2],[60,2],[59,2],[61,2],[62,2],[10,2],[63,2],[64,2],[65,2],[11,2],[66,2],[67,2],[68,2],[69,2],[70,2],[1,2],[71,2],[72,2],[12,2],[76,2],[74,2],[79,2],[78,2],[73,2],[77,2],[75,2],[80,2],[118,316],[128,317],[117,316],[138,318],[109,319],[108,320],[137,307],[131,321],[136,322],[111,323],[125,324],[110,325],[134,326],[106,327],[105,307],[135,328],[107,329],[112,330],[113,2],[116,330],[103,2],[139,331],[129,332],[120,333],[121,334],[123,335],[119,336],[122,337],[132,307],[114,338],[115,339],[124,340],[104,341],[127,332],[126,330],[130,2],[133,342],[693,343],[689,2],[692,344],[686,345],[685,45],[688,346],[687,347],[600,348],[607,349],[602,2],[603,2],[601,350],[604,351],[596,2],[597,2],[608,352],[599,353],[605,2],[606,354],[598,355],[587,356],[591,357],[588,357],[584,356],[592,358],[589,359],[593,360],[590,357],[585,361],[586,362],[580,363],[524,364],[526,365],[579,2],[525,366],[583,367],[582,368],[581,369],[517,2],[527,364],[528,2],[519,370],[523,371],[518,2],[520,372],[521,373],[522,2],[594,374],[529,375],[530,375],[531,375],[532,375],[533,375],[534,375],[535,375],[536,375],[537,375],[538,375],[539,375],[540,375],[541,375],[543,375],[542,375],[544,375],[545,375],[546,375],[547,375],[578,376],[548,375],[549,375],[550,375],[551,375],[552,375],[553,375],[554,375],[555,375],[556,375],[557,375],[558,375],[559,375],[560,375],[562,375],[561,375],[563,375],[564,375],[565,375],[566,375],[567,375],[568,375],[569,375],[570,375],[571,375],[572,375],[573,375],[574,375],[577,375],[575,375],[576,375],[514,377],[665,378],[666,377],[668,379],[670,380],[671,377],[679,6],[780,381],[775,382],[778,383],[772,384],[774,385],[773,377],[779,386],[682,387],[681,388],[771,389],[770,390],[776,391],[777,392],[675,393],[676,156],[677,393],[664,377],[678,377],[663,394],[667,377],[669,395],[662,377],[674,396],[790,2],[791,2],[792,397],[793,398],[102,2],[795,399],[794,400],[796,400],[800,401],[803,402],[801,2],[798,2],[799,2],[797,403],[802,404]],"affectedFilesPendingEmit":[784,782,513,514,665,666,668,670,671,679,780,775,778,772,774,773,779,682,681,771,770,776,777,675,676,677,664,678,663,667,669,662,674],"version":"5.9.3"} \ No newline at end of file diff --git a/manim-mcp b/manim-mcp deleted file mode 160000 index 69abff5..0000000 --- a/manim-mcp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 69abff573032a51f623952b4dc0274a40dffee1a diff --git a/mcp-command-center/state.json b/mcp-command-center/state.json index e4c6dd6..6b693e9 100644 --- a/mcp-command-center/state.json +++ b/mcp-command-center/state.json @@ -1,7 +1,7 @@ { "version": 1, - "lastUpdated": "2026-02-17T04:00:00-05:00", - "updatedBy": "Buba (heartbeat 4AM 2/17: no changes. dec-004 still zero reactions (~6 days). All gates unchanged: 6×Stage 19 blocked on dec-004, 31×Stage 6 held at design gate, 2×Stage 9 need creds, 1×Stage 7 design gate.)", + "lastUpdated": "2026-02-17T22:00:00-05:00", + "updatedBy": "Buba (heartbeat 10PM 2/17: no changes. dec-004 still 0 reactions — but Jake asked what registry submission means earlier today (1:08PM), I explained, he hasn't replied yet. Not sending another reminder — he's actively considering it. No auto-advance candidates. 6×Stage 19 on dec-004, 31×Stage 6 at design gate, 2×Stage 9 need creds, 1×Stage 7 design gate. GitHub shadow ban still active.)", "phases": [ { "id": 1, diff --git a/mcp-diagrams/GHL-MCP-Funnel/README.md b/mcp-diagrams/GHL-MCP-Funnel/README.md deleted file mode 100644 index fdc4efc..0000000 --- a/mcp-diagrams/GHL-MCP-Funnel/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# GHL MCP Funnel - -Landing page for the GoHighLevel MCP hosted service. - -## What This Is -- Open source + hosted business model for GHL MCP -- 461 tools (most comprehensive GHL MCP available) -- Pricing: Free / $49 Pro / $149 Agency - -## To Preview -```bash -cd ~/.clawdbot/workspace/GHL-MCP-Funnel -python3 -m http.server 8888 -# Open http://localhost:8888 -``` - -## Tech Stack -- Single HTML file (index.html) -- Tailwind CSS via CDN -- Lucide icons -- Google Fonts (Inter) - -## Created -2026-01-25 diff --git a/mcp-diagrams/GHL-MCP-Funnel/functions/api/waitlist.js b/mcp-diagrams/GHL-MCP-Funnel/functions/api/waitlist.js deleted file mode 100644 index c585c9b..0000000 --- a/mcp-diagrams/GHL-MCP-Funnel/functions/api/waitlist.js +++ /dev/null @@ -1,75 +0,0 @@ -// Cloudflare Pages Function - handles waitlist submissions securely -// API key is stored as an environment secret, not in frontend code - -export async function onRequestPost(context) { - const { request, env } = context; - - // CORS headers - const corsHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', - }; - - try { - const body = await request.json(); - const { firstName, lastName, phone, email } = body; - - // Validate required fields - if (!firstName || !phone) { - return new Response(JSON.stringify({ error: 'Name and phone are required' }), { - status: 400, - headers: { ...corsHeaders, 'Content-Type': 'application/json' } - }); - } - - // Call GHL API with secret key - const ghlResponse = await fetch('https://rest.gohighlevel.com/v1/contacts/', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${env.GHL_API_KEY}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - firstName, - lastName: lastName || '', - phone, - email: email || undefined, - tags: ['MCP Waitlist'] - }) - }); - - if (!ghlResponse.ok) { - const errorText = await ghlResponse.text(); - console.error('GHL API error:', errorText); - return new Response(JSON.stringify({ error: 'Failed to add to waitlist' }), { - status: 500, - headers: { ...corsHeaders, 'Content-Type': 'application/json' } - }); - } - - const result = await ghlResponse.json(); - return new Response(JSON.stringify({ success: true, contactId: result.contact?.id }), { - status: 200, - headers: { ...corsHeaders, 'Content-Type': 'application/json' } - }); - - } catch (err) { - console.error('Waitlist error:', err); - return new Response(JSON.stringify({ error: 'Server error' }), { - status: 500, - headers: { ...corsHeaders, 'Content-Type': 'application/json' } - }); - } -} - -// Handle CORS preflight -export async function onRequestOptions() { - return new Response(null, { - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', - } - }); -} diff --git a/mcp-diagrams/GHL-MCP-Funnel/index.html b/mcp-diagrams/GHL-MCP-Funnel/index.html deleted file mode 100644 index 57618c2..0000000 --- a/mcp-diagrams/GHL-MCP-Funnel/index.html +++ /dev/null @@ -1,598 +0,0 @@ - - - - - - GHL Connect — AI-Power Your GoHighLevel in 2 Clicks - - - - - - - - - - - - - - -
    -
    -
    - - Open Source + Hosted -
    - -

    - Connect GoHighLevel
    - to AI in 2 Clicks -

    - -

    - The most comprehensive GHL MCP server. 461 tools covering the entire API. - No setup. No OAuth headaches. Just connect and automate. -

    - - - - -
    -
    - - - - - -
    -

    - Trusted by 500+ GHL agencies -

    -
    -
    -
    - - -
    -
    -
    -
    -

    - Setting up GHL + AI
    - shouldn't take a week -

    -
    -
    -
    - -
    -
    -

    OAuth token refresh nightmare

    -

    Tokens expire. Your automations break. You scramble.

    -
    -
    -
    -
    - -
    -
    -

    Terminal commands and config files

    -

    You're an agency owner, not a DevOps engineer.

    -
    -
    -
    -
    - -
    -
    -

    Limited API coverage

    -

    Other tools have 20-30 endpoints. You need all 461.

    -
    -
    -
    -
    -
    -
    -
    - -
    -

    With GHL Connect

    -
    -
    -

    ✓ Connect in 2 clicks via OAuth

    -

    ✓ Automatic token refresh forever

    -

    ✓ All 461 API endpoints ready

    -

    ✓ Works with Claude, GPT, any MCP client

    -

    ✓ Multi-location support built-in

    -
    -
    -
    -
    -
    - - -
    -
    -
    -

    Everything you need

    -

    Full GHL API access through one simple connection

    -
    - -
    -
    -
    - -
    -

    Contacts & CRM

    -

    Create, update, search, tag contacts. Full CRUD on your entire database.

    -
    - -
    -
    - -
    -

    Conversations

    -

    Send SMS, emails, read threads. Your AI can actually talk to leads.

    -
    - -
    -
    - -
    -

    Calendars

    -

    Book appointments, check availability, manage your calendar.

    -
    - -
    -
    - -
    -

    Opportunities

    -

    Manage your pipeline. Move deals through stages automatically.

    -
    - -
    -
    - -
    -

    Workflows

    -

    Trigger automations, manage workflow states, orchestrate actions.

    -
    - -
    -
    - -
    -

    Forms & Surveys

    -

    Read submissions, analyze responses, trigger follow-ups.

    -
    -
    - -
    -

    + 400 more endpoints including:

    -
    - Invoices - Payments - Funnels - Websites - Social Planner - Reputation - Reporting - Memberships -
    -
    -
    -
    - - -
    -
    -
    -
    - - Coming Soon -
    -

    Join the Waitlist

    -

    Be the first to know when we launch. Early access + exclusive perks for waitlist members.

    -
    - -
    -
    -
    - - -
    - -
    - - -
    - -
    - - -
    - - -
    - - - - - - - -

    - - We respect your privacy. No spam, ever. -

    -
    -
    -
    - - - - -
    -
    -
    -
    -
    -
    - - Open Source -
    -

    - Self-host if you want.
    - We won't stop you. -

    -

    - The entire MCP server is open source. Run it yourself, modify it, contribute back. - The hosted version just saves you the hassle. -

    - - View on GitHub - - -
    -
    -
    - - - - Terminal -
    -
    $ git clone github.com/ghl-connect/mcp
    -$ cd mcp && npm install
    -$ npm run build
    -$ node dist/server.js
    -
    -✓ GHL MCP Server running
    -✓ 461 tools loaded
    -Listening on stdio...
    -
    -
    -
    -
    -
    - - -
    -
    -
    -

    Frequently asked questions

    -
    - -
    -
    - - What is MCP? - - -

    - MCP (Model Context Protocol) is a standard for connecting AI assistants to external tools and data. - It's supported by Claude, and lets AI actually take actions in your systems — not just chat about them. -

    -
    - -
    - - Do I need to install anything? - - -

    - For the hosted version, no. Just connect your GHL account via OAuth and add the MCP endpoint to your AI client (Claude Desktop, etc.). - If you self-host, you'll need Node.js. -

    -
    - -
    - - Is my data secure? - - -

    - Yes. We use OAuth 2.0 and never store your GHL API keys. Tokens are encrypted at rest and in transit. - You can revoke access anytime from your GHL settings. -

    -
    - -
    - - What happens if I hit my API limit? - - -

    - We'll notify you before you hit 80%. You can upgrade anytime, or your calls will be rate-limited (not blocked) until the next billing cycle. -

    -
    - -
    - - Can I use this with GPT or other AI models? - - -

    - MCP is currently best supported by Claude (Anthropic). GPT can use it via custom implementations. - As MCP adoption grows, more clients will support it natively. -

    -
    -
    -
    -
    - - -
    -
    -

    - Ready to AI-power your GHL? -

    -

    - Join 500+ agencies already automating with GHL Connect. -

    - -
    -
    - - - - - - - - - - - - - diff --git a/mcp-diagrams/GoHighLevel-MCP b/mcp-diagrams/GoHighLevel-MCP deleted file mode 160000 index cd67f66..0000000 --- a/mcp-diagrams/GoHighLevel-MCP +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cd67f662aa574f1278ee5fb53c5a96e35ba49c05 diff --git a/mcp-diagrams/audit-report.json b/mcp-diagrams/audit-report.json deleted file mode 100644 index fd6685e..0000000 --- a/mcp-diagrams/audit-report.json +++ /dev/null @@ -1,2477 +0,0 @@ -{ - "servers": [ - { - "name": "acuity-scheduling", - "tools": [ - { - "name": "list_appointments", - "description": "List appointments with optional filters. Returns scheduled appointments.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_appointment", - "description": "Get a specific appointment by ID", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_appointment", - "description": "Create a new appointment", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "cancel_appointment", - "description": "Cancel an appointment", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_calendars", - "description": "List all calendars/staff members", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_availability", - "description": "Get available time slots for booking", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_clients", - "description": "List clients with optional search", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_appointments\": Missing _meta labels", - "Tool \"get_appointment\": Missing _meta labels", - "Tool \"create_appointment\": Missing _meta labels", - "Tool \"cancel_appointment\": Missing _meta labels", - "Tool \"list_calendars\": Missing _meta labels", - "Tool \"get_availability\": Missing _meta labels", - "Tool \"list_clients\": Missing _meta labels" - ] - }, - { - "name": "bamboohr", - "tools": [ - { - "name": "list_employees", - "description": "List all employees from the BambooHR directory", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_employee", - "description": "Get detailed information about a specific employee", - "issues": [ - "Parameter \"fields\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_time_off_requests", - "description": "List time off requests from BambooHR", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "request_time_off", - "description": "Submit a time off request for an employee", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_goals", - "description": "List goals for an employee", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_directory", - "description": "Get the full employee directory with contact information", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_files", - "description": "List files associated with an employee", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_employees\": Missing _meta labels", - "Tool \"get_employee\": Missing argument descriptions", - "Tool \"get_employee\": Missing _meta labels", - "Tool \"list_time_off_requests\": Missing _meta labels", - "Tool \"request_time_off\": Missing _meta labels", - "Tool \"list_goals\": Missing _meta labels", - "Tool \"get_directory\": Missing _meta labels", - "Tool \"list_files\": Missing _meta labels" - ] - }, - { - "name": "basecamp", - "tools": [ - { - "name": "list_projects", - "description": "List all projects in the Basecamp account", - "issues": [ - "Parameter \"status\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "get_project", - "description": "Get details of a specific project including its dock (tools)", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_todos", - "description": "List to-dos from a to-do list in a project", - "issues": [ - "Parameter \"status\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "create_todo", - "description": "Create a new to-do in a to-do list", - "issues": [ - "Parameter \"assignee_ids\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "complete_todo", - "description": "Mark a to-do as complete", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_messages", - "description": "List messages from a project", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_message", - "description": "Create a new message on a project", - "issues": [ - "Parameter \"status\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_people", - "description": "List all people in the Basecamp account or a specific project", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_projects\": Missing argument descriptions", - "Tool \"list_projects\": Missing _meta labels", - "Tool \"get_project\": Missing _meta labels", - "Tool \"list_todos\": Missing argument descriptions", - "Tool \"list_todos\": Missing _meta labels", - "Tool \"create_todo\": Missing argument descriptions", - "Tool \"create_todo\": Missing _meta labels", - "Tool \"complete_todo\": Missing _meta labels", - "Tool \"list_messages\": Missing _meta labels", - "Tool \"create_message\": Missing argument descriptions", - "Tool \"create_message\": Missing _meta labels", - "Tool \"list_people\": Missing _meta labels" - ] - }, - { - "name": "bigcommerce", - "tools": [ - { - "name": "list_products", - "description": "List products from BigCommerce catalog with filtering and pagination", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_product", - "description": "Get a specific product by ID with full details", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_product", - "description": "Create a new product in BigCommerce catalog", - "issues": [ - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "update_product", - "description": "Update an existing product in BigCommerce", - "issues": [ - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_orders", - "description": "List orders from BigCommerce (V2 API)", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_order", - "description": "Get a specific order by ID with full details", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_customers", - "description": "List customers from BigCommerce", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "update_inventory", - "description": "Update inventory level for a product or variant", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_products\": Missing _meta labels", - "Tool \"get_product\": Missing _meta labels", - "Tool \"create_product\": Missing argument descriptions", - "Tool \"create_product\": Missing _meta labels", - "Tool \"update_product\": Missing argument descriptions", - "Tool \"update_product\": Missing _meta labels", - "Tool \"list_orders\": Missing _meta labels", - "Tool \"get_order\": Missing _meta labels", - "Tool \"list_customers\": Missing _meta labels", - "Tool \"update_inventory\": Missing _meta labels" - ] - }, - { - "name": "brevo", - "tools": [ - { - "name": "send_email", - "description": "Send a transactional email", - "issues": [ - "Parameter \"email\" missing description", - "Parameter \"name\" missing description", - "Parameter \"email\" missing description", - "Parameter \"name\" missing description", - "Parameter \"tags\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_contacts", - "description": "List contacts with optional filters", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "add_contact", - "description": "Create a new contact", - "issues": [ - "Parameter \"listIds\" missing description", - "Parameter \"items\" missing description", - "Parameter \"smtpBlacklistSender\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "update_contact", - "description": "Update an existing contact", - "issues": [ - "Parameter \"listIds\" missing description", - "Parameter \"items\" missing description", - "Parameter \"unlinkListIds\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_campaigns", - "description": "List email campaigns", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_campaign", - "description": "Create a new email campaign", - "issues": [ - "Parameter \"email\" missing description", - "Parameter \"name\" missing description", - "Parameter \"listIds\" missing description", - "Parameter \"items\" missing description", - "Parameter \"exclusionListIds\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "send_sms", - "description": "Send a transactional SMS", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_templates", - "description": "List email templates", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"send_email\": Missing argument descriptions", - "Tool \"send_email\": Missing _meta labels", - "Tool \"list_contacts\": Missing _meta labels", - "Tool \"add_contact\": Missing argument descriptions", - "Tool \"add_contact\": Missing _meta labels", - "Tool \"update_contact\": Missing argument descriptions", - "Tool \"update_contact\": Missing _meta labels", - "Tool \"list_campaigns\": Missing _meta labels", - "Tool \"create_campaign\": Missing argument descriptions", - "Tool \"create_campaign\": Missing _meta labels", - "Tool \"send_sms\": Missing _meta labels", - "Tool \"list_templates\": Missing _meta labels" - ] - }, - { - "name": "calendly", - "tools": [ - { - "name": "list_events", - "description": "List scheduled events. Returns events for the authenticated user within the specified time range.", - "issues": [ - "Parameter \"status\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "get_event", - "description": "Get details of a specific scheduled event by its UUID", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "cancel_event", - "description": "Cancel a scheduled event. Optionally provide a reason for cancellation.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_event_types", - "description": "List all event types available for the authenticated user", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_availability", - "description": "Get available time slots for an event type", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_invitees", - "description": "List invitees for a scheduled event", - "issues": [ - "Parameter \"status\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "get_user", - "description": "Get the current authenticated user", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_events\": Missing argument descriptions", - "Tool \"list_events\": Missing _meta labels", - "Tool \"get_event\": Missing _meta labels", - "Tool \"cancel_event\": Missing _meta labels", - "Tool \"list_event_types\": Missing _meta labels", - "Tool \"get_availability\": Missing _meta labels", - "Tool \"list_invitees\": Missing argument descriptions", - "Tool \"list_invitees\": Missing _meta labels", - "Tool \"get_user\": Missing _meta labels" - ] - }, - { - "name": "clickup", - "tools": [ - { - "name": "list_spaces", - "description": "List all spaces in a ClickUp workspace/team", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_lists", - "description": "List all lists in a folder or space (folderless lists)", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_tasks", - "description": "List tasks in a list with optional filters", - "issues": [ - "Parameter \"statuses\" missing description", - "Parameter \"items\" missing description", - "Parameter \"assignees\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "get_task", - "description": "Get detailed information about a specific task", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_task", - "description": "Create a new task in a list", - "issues": [ - "Parameter \"assignees\" missing description", - "Parameter \"items\" missing description", - "Parameter \"tags\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "update_task", - "description": "Update an existing task", - "issues": [ - "Parameter \"assignees_add\" missing description", - "Parameter \"items\" missing description", - "Parameter \"assignees_remove\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "add_comment", - "description": "Add a comment to a task", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_time_entries", - "description": "Get time tracking entries for a workspace", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_spaces\": Missing _meta labels", - "Tool \"list_lists\": Missing _meta labels", - "Tool \"list_tasks\": Missing argument descriptions", - "Tool \"list_tasks\": Missing _meta labels", - "Tool \"get_task\": Missing _meta labels", - "Tool \"create_task\": Missing argument descriptions", - "Tool \"create_task\": Missing _meta labels", - "Tool \"update_task\": Missing argument descriptions", - "Tool \"update_task\": Missing _meta labels", - "Tool \"add_comment\": Missing _meta labels", - "Tool \"get_time_entries\": Missing _meta labels" - ] - }, - { - "name": "close", - "tools": [ - { - "name": "list_leads", - "description": "List leads from Close CRM with optional search query", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_lead", - "description": "Get a specific lead by ID", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_lead", - "description": "Create a new lead in Close CRM", - "issues": [ - "Parameter \"emails\" missing description", - "Parameter \"email\" missing description", - "Parameter \"phones\" missing description", - "Parameter \"phone\" missing description", - "Parameter \"address_1\" missing description", - "Parameter \"address_2\" missing description", - "Parameter \"city\" missing description", - "Parameter \"state\" missing description", - "Parameter \"zipcode\" missing description", - "Parameter \"country\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "update_lead", - "description": "Update an existing lead", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_opportunities", - "description": "List opportunities from Close CRM", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_opportunity", - "description": "Create a new opportunity/deal", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_activity", - "description": "Create an activity (note, call, email, meeting, etc.)", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_tasks", - "description": "List tasks from Close CRM", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_task", - "description": "Create a new task", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "send_email", - "description": "Send an email through Close CRM", - "issues": [ - "Parameter \"to\" missing description", - "Parameter \"items\" missing description", - "Parameter \"cc\" missing description", - "Parameter \"items\" missing description", - "Parameter \"bcc\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_statuses", - "description": "List lead and opportunity statuses", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_users", - "description": "List users in the Close organization", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_leads\": Missing _meta labels", - "Tool \"get_lead\": Missing _meta labels", - "Tool \"create_lead\": Missing argument descriptions", - "Tool \"create_lead\": Missing _meta labels", - "Tool \"update_lead\": Missing _meta labels", - "Tool \"list_opportunities\": Missing _meta labels", - "Tool \"create_opportunity\": Missing _meta labels", - "Tool \"create_activity\": Missing _meta labels", - "Tool \"list_tasks\": Missing _meta labels", - "Tool \"create_task\": Missing _meta labels", - "Tool \"send_email\": Missing argument descriptions", - "Tool \"send_email\": Missing _meta labels", - "Tool \"list_statuses\": Missing _meta labels", - "Tool \"list_users\": Missing _meta labels" - ] - }, - { - "name": "clover", - "tools": [ - { - "name": "list_orders", - "description": "List orders for the merchant. Returns paginated list of orders with details like totals, state, and timestamps.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_order", - "description": "Get a specific order by ID with full details including line items, payments, and discounts.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_order", - "description": "Create a new order. Use atomic_order for complete orders with line items in one call.", - "issues": [ - "Parameter \"item_id\" missing description", - "Parameter \"quantity\" missing description", - "Parameter \"price\" missing description", - "Parameter \"name\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_items", - "description": "List inventory items (products) available for sale. Returns item details, prices, and stock info.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_inventory", - "description": "Get inventory stock counts for items. Shows current quantity and tracking status.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_customers", - "description": "List customers in the merchant", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_payments", - "description": "List payments processed by the merchant. Includes payment method, amount, status, and related order.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_merchant", - "description": "Get merchant account information including business name, address, timezone, and settings.", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_orders\": Missing _meta labels", - "Tool \"get_order\": Missing _meta labels", - "Tool \"create_order\": Missing argument descriptions", - "Tool \"create_order\": Missing _meta labels", - "Tool \"list_items\": Missing _meta labels", - "Tool \"get_inventory\": Missing _meta labels", - "Tool \"list_customers\": Missing _meta labels", - "Tool \"list_payments\": Missing _meta labels", - "Tool \"get_merchant\": Missing _meta labels" - ] - }, - { - "name": "constant-contact", - "tools": [ - { - "name": "list_contacts", - "description": "List contacts with filtering and pagination. Returns contact email, name, and list memberships.", - "issues": [ - "Parameter \"status\" missing description", - "Parameter \"include\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "add_contact", - "description": "Create or update a contact. If email exists, contact is updated.", - "issues": [ - "Parameter \"phone_numbers\" missing description", - "Parameter \"phone_number\" missing description", - "Parameter \"kind\" missing description", - "Parameter \"street_addresses\" missing description", - "Parameter \"street\" missing description", - "Parameter \"city\" missing description", - "Parameter \"state\" missing description", - "Parameter \"postal_code\" missing description", - "Parameter \"country\" missing description", - "Parameter \"kind\" missing description", - "Parameter \"list_memberships\" missing description", - "Parameter \"items\" missing description", - "Parameter \"custom_fields\" missing description", - "Parameter \"custom_field_id\" missing description", - "Parameter \"value\" missing description", - "Parameter \"create_source\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_campaigns", - "description": "List email campaigns (email activities)", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_campaign", - "description": "Create a new email campaign", - "issues": [ - "Parameter \"format_type\" missing description", - "Parameter \"address_line1\" missing description", - "Parameter \"address_line2\" missing description", - "Parameter \"address_line3\" missing description", - "Parameter \"city\" missing description", - "Parameter \"state\" missing description", - "Parameter \"postal_code\" missing description", - "Parameter \"country\" missing description", - "Parameter \"organization_name\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_lists", - "description": "List all contact lists", - "issues": [ - "Parameter \"include_membership_count\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "add_to_list", - "description": "Add one or more contacts to a list", - "issues": [ - "Parameter \"contact_ids\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "get_campaign_stats", - "description": "Get tracking statistics for a campaign (sends, opens, clicks, bounces, etc.)", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_contacts\": Missing argument descriptions", - "Tool \"list_contacts\": Missing _meta labels", - "Tool \"add_contact\": Missing argument descriptions", - "Tool \"add_contact\": Missing _meta labels", - "Tool \"list_campaigns\": Missing _meta labels", - "Tool \"create_campaign\": Missing argument descriptions", - "Tool \"create_campaign\": Missing _meta labels", - "Tool \"list_lists\": Missing argument descriptions", - "Tool \"list_lists\": Missing _meta labels", - "Tool \"add_to_list\": Missing argument descriptions", - "Tool \"add_to_list\": Missing _meta labels", - "Tool \"get_campaign_stats\": Missing _meta labels" - ] - }, - { - "name": "fieldedge", - "tools": [ - { - "name": "list_work_orders", - "description": "List work orders from FieldEdge. Filter by status, customer, technician, and date range.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_work_order", - "description": "Get detailed information about a specific work order by ID", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_work_order", - "description": "Create a new work order in FieldEdge", - "issues": [ - "Parameter \"equipmentIds\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_customers", - "description": "List customers from FieldEdge with search and pagination", - "issues": [ - "Parameter \"sortOrder\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_technicians", - "description": "List technicians/employees from FieldEdge", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_invoices", - "description": "List invoices from FieldEdge", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_equipment", - "description": "List equipment records from FieldEdge. Track HVAC units, appliances, and other equipment at customer locations.", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_work_orders\": Missing _meta labels", - "Tool \"get_work_order\": Missing _meta labels", - "Tool \"create_work_order\": Missing argument descriptions", - "Tool \"create_work_order\": Missing _meta labels", - "Tool \"list_customers\": Missing argument descriptions", - "Tool \"list_customers\": Missing _meta labels", - "Tool \"list_technicians\": Missing _meta labels", - "Tool \"list_invoices\": Missing _meta labels", - "Tool \"list_equipment\": Missing _meta labels" - ] - }, - { - "name": "freshbooks", - "tools": [ - { - "name": "list_invoices", - "description": "List invoices from FreshBooks", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_invoice", - "description": "Get a specific invoice by ID", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_invoice", - "description": "Create a new invoice in FreshBooks", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "send_invoice", - "description": "Send an invoice to the client via email", - "issues": [ - "Parameter \"email_recipients\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_clients", - "description": "List all clients from FreshBooks", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_client", - "description": "Create a new client in FreshBooks", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_expenses", - "description": "List expenses from FreshBooks", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_payments", - "description": "List payments received in FreshBooks", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_invoices\": Missing _meta labels", - "Tool \"get_invoice\": Missing _meta labels", - "Tool \"create_invoice\": Missing _meta labels", - "Tool \"send_invoice\": Missing argument descriptions", - "Tool \"send_invoice\": Missing _meta labels", - "Tool \"list_clients\": Missing _meta labels", - "Tool \"create_client\": Missing _meta labels", - "Tool \"list_expenses\": Missing _meta labels", - "Tool \"list_payments\": Missing _meta labels" - ] - }, - { - "name": "freshdesk", - "tools": [ - { - "name": "list_tickets", - "description": "List all tickets with optional filtering. Returns tickets sorted by created_at descending.", - "issues": [ - "Parameter \"order_type\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "get_ticket", - "description": "Get a specific ticket by ID with full details including conversations", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_ticket", - "description": "Create a new support ticket", - "issues": [ - "Parameter \"tags\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "update_ticket", - "description": "Update an existing ticket", - "issues": [ - "Parameter \"tags\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "reply_ticket", - "description": "Add a reply to a ticket (creates a conversation)", - "issues": [ - "Parameter \"cc_emails\" missing description", - "Parameter \"items\" missing description", - "Parameter \"bcc_emails\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_contacts", - "description": "List all contacts in your helpdesk", - "issues": [ - "Parameter \"state\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_agents", - "description": "List all agents in your helpdesk", - "issues": [ - "Parameter \"state\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "search_tickets", - "description": "Search tickets using Freshdesk query language. Supports field:value syntax.", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_tickets\": Missing argument descriptions", - "Tool \"list_tickets\": Missing _meta labels", - "Tool \"get_ticket\": Missing _meta labels", - "Tool \"create_ticket\": Missing argument descriptions", - "Tool \"create_ticket\": Missing _meta labels", - "Tool \"update_ticket\": Missing argument descriptions", - "Tool \"update_ticket\": Missing _meta labels", - "Tool \"reply_ticket\": Missing argument descriptions", - "Tool \"reply_ticket\": Missing _meta labels", - "Tool \"list_contacts\": Missing argument descriptions", - "Tool \"list_contacts\": Missing _meta labels", - "Tool \"list_agents\": Missing argument descriptions", - "Tool \"list_agents\": Missing _meta labels", - "Tool \"search_tickets\": Missing _meta labels" - ] - }, - { - "name": "gusto", - "tools": [ - { - "name": "list_employees", - "description": "List all employees for a company in Gusto", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_employee", - "description": "Get details of a specific employee by ID", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_payrolls", - "description": "List payrolls for a company, optionally filtered by date range and processing status", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_payroll", - "description": "Get details of a specific payroll", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_contractors", - "description": "List all contractors for a company", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_company", - "description": "Get company details including locations and settings", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_benefits", - "description": "List all company benefits (health insurance, 401k, etc.)", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_employees\": Missing _meta labels", - "Tool \"get_employee\": Missing _meta labels", - "Tool \"list_payrolls\": Missing _meta labels", - "Tool \"get_payroll\": Missing _meta labels", - "Tool \"list_contractors\": Missing _meta labels", - "Tool \"get_company\": Missing _meta labels", - "Tool \"list_benefits\": Missing _meta labels" - ] - }, - { - "name": "helpscout", - "tools": [ - { - "name": "list_conversations", - "description": "List conversations (tickets) from Help Scout. Returns paginated list with embedded conversation data.", - "issues": [ - "Parameter \"sortOrder\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "get_conversation", - "description": "Get a specific conversation by ID with full thread details", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_conversation", - "description": "Create a new conversation (ticket) in Help Scout", - "issues": [ - "Parameter \"status\" missing description", - "Parameter \"tags\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "reply_conversation", - "description": "Reply to an existing conversation", - "issues": [ - "Parameter \"status\" missing description", - "Parameter \"cc\" missing description", - "Parameter \"items\" missing description", - "Parameter \"bcc\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_customers", - "description": "List customers from Help Scout", - "issues": [ - "Parameter \"sortOrder\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_mailboxes", - "description": "List all mailboxes accessible to the authenticated user", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "search", - "description": "Search conversations using Help Scout", - "issues": [ - "Bad naming (not snake_case verb_noun)", - "Parameter \"sortOrder\" missing description", - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_conversations\": Missing argument descriptions", - "Tool \"list_conversations\": Missing _meta labels", - "Tool \"get_conversation\": Missing _meta labels", - "Tool \"create_conversation\": Missing argument descriptions", - "Tool \"create_conversation\": Missing _meta labels", - "Tool \"reply_conversation\": Missing argument descriptions", - "Tool \"reply_conversation\": Missing _meta labels", - "Tool \"list_customers\": Missing argument descriptions", - "Tool \"list_customers\": Missing _meta labels", - "Tool \"list_mailboxes\": Missing _meta labels", - "Tool \"search\": Bad naming", - "Tool \"search\": Missing argument descriptions", - "Tool \"search\": Missing _meta labels" - ] - }, - { - "name": "housecall-pro", - "tools": [ - { - "name": "list_jobs", - "description": "List jobs from Housecall Pro. Filter by status, customer, and paginate results.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_job", - "description": "Get detailed information about a specific job by ID", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_job", - "description": "Create a new job in Housecall Pro", - "issues": [ - "Parameter \"assigned_employee_ids\" missing description", - "Parameter \"items\" missing description", - "Parameter \"tags\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_estimates", - "description": "List estimates from Housecall Pro with optional filters", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_estimate", - "description": "Create a new estimate for a customer", - "issues": [ - "Parameter \"line_items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_customers", - "description": "List customers from Housecall Pro with search and pagination", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_invoices", - "description": "List invoices from Housecall Pro", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_employees", - "description": "List employees/technicians from Housecall Pro", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_jobs\": Missing _meta labels", - "Tool \"get_job\": Missing _meta labels", - "Tool \"create_job\": Missing argument descriptions", - "Tool \"create_job\": Missing _meta labels", - "Tool \"list_estimates\": Missing _meta labels", - "Tool \"create_estimate\": Missing argument descriptions", - "Tool \"create_estimate\": Missing _meta labels", - "Tool \"list_customers\": Missing _meta labels", - "Tool \"list_invoices\": Missing _meta labels", - "Tool \"list_employees\": Missing _meta labels" - ] - }, - { - "name": "jobber", - "tools": [ - { - "name": "list_jobs", - "description": "List jobs from Jobber with pagination", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_job", - "description": "Get a specific job by ID", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_job", - "description": "Create a new job in Jobber", - "issues": [ - "Parameter \"name\" missing description", - "Parameter \"description\" missing description", - "Parameter \"quantity\" missing description", - "Parameter \"unitPrice\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_quotes", - "description": "List quotes from Jobber with pagination", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_quote", - "description": "Create a new quote in Jobber", - "issues": [ - "Parameter \"name\" missing description", - "Parameter \"description\" missing description", - "Parameter \"quantity\" missing description", - "Parameter \"unitPrice\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_invoices", - "description": "List invoices from Jobber with pagination", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_clients", - "description": "List clients from Jobber with optional search", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_client", - "description": "Create a new client in Jobber", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_jobs\": Missing _meta labels", - "Tool \"get_job\": Missing _meta labels", - "Tool \"create_job\": Missing argument descriptions", - "Tool \"create_job\": Missing _meta labels", - "Tool \"list_quotes\": Missing _meta labels", - "Tool \"create_quote\": Missing argument descriptions", - "Tool \"create_quote\": Missing _meta labels", - "Tool \"list_invoices\": Missing _meta labels", - "Tool \"list_clients\": Missing _meta labels", - "Tool \"create_client\": Missing _meta labels" - ] - }, - { - "name": "keap", - "tools": [ - { - "name": "list_contacts", - "description": "List contacts with optional filtering and pagination", - "issues": [ - "Parameter \"order_direction\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "get_contact", - "description": "Get a specific contact by ID with full details", - "issues": [ - "Parameter \"optional_properties\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "create_contact", - "description": "Create a new contact in Keap", - "issues": [ - "Parameter \"email_addresses\" missing description", - "Parameter \"email\" missing description", - "Parameter \"field\" missing description", - "Parameter \"phone_numbers\" missing description", - "Parameter \"number\" missing description", - "Parameter \"field\" missing description", - "Parameter \"addresses\" missing description", - "Parameter \"line1\" missing description", - "Parameter \"line2\" missing description", - "Parameter \"postal_code\" missing description", - "Parameter \"country_code\" missing description", - "Parameter \"field\" missing description", - "Parameter \"company_name\" missing description", - "Parameter \"source_type\" missing description", - "Parameter \"custom_fields\" missing description", - "Parameter \"id\" missing description", - "Parameter \"content\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "update_contact", - "description": "Update an existing contact", - "issues": [ - "Parameter \"email_addresses\" missing description", - "Parameter \"phone_numbers\" missing description", - "Parameter \"addresses\" missing description", - "Parameter \"custom_fields\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_opportunities", - "description": "List sales opportunities/deals", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_tasks", - "description": "List tasks with optional filtering", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_task", - "description": "Create a new task", - "issues": [ - "Parameter \"id\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_tags", - "description": "List all tags available in the account", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_contacts\": Missing argument descriptions", - "Tool \"list_contacts\": Missing _meta labels", - "Tool \"get_contact\": Missing argument descriptions", - "Tool \"get_contact\": Missing _meta labels", - "Tool \"create_contact\": Missing argument descriptions", - "Tool \"create_contact\": Missing _meta labels", - "Tool \"update_contact\": Missing argument descriptions", - "Tool \"update_contact\": Missing _meta labels", - "Tool \"list_opportunities\": Missing _meta labels", - "Tool \"list_tasks\": Missing _meta labels", - "Tool \"create_task\": Missing argument descriptions", - "Tool \"create_task\": Missing _meta labels", - "Tool \"list_tags\": Missing _meta labels" - ] - }, - { - "name": "lightspeed", - "tools": [ - { - "name": "list_sales", - "description": "List sales/transactions from Lightspeed Retail. Returns completed sales with line items, payments, and customer info.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_sale", - "description": "Get a specific sale by ID with full details including line items, payments, and customer", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_items", - "description": "List inventory items from Lightspeed Retail catalog", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_item", - "description": "Get a specific inventory item by ID with full details", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "update_inventory", - "description": "Update inventory quantity for an item at a specific shop location", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_customers", - "description": "List customers from Lightspeed Retail", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_categories", - "description": "List product categories from Lightspeed Retail catalog", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_register", - "description": "Get register/POS terminal information and status", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_sales\": Missing _meta labels", - "Tool \"get_sale\": Missing _meta labels", - "Tool \"list_items\": Missing _meta labels", - "Tool \"get_item\": Missing _meta labels", - "Tool \"update_inventory\": Missing _meta labels", - "Tool \"list_customers\": Missing _meta labels", - "Tool \"list_categories\": Missing _meta labels", - "Tool \"get_register\": Missing _meta labels" - ] - }, - { - "name": "mailchimp", - "tools": [ - { - "name": "list_campaigns", - "description": "List email campaigns in Mailchimp", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_campaign", - "description": "Get details of a specific campaign", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_campaign", - "description": "Create a new email campaign", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "send_campaign", - "description": "Send a campaign immediately (campaign must be ready to send)", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_lists", - "description": "List all audiences/lists in the account", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "add_subscriber", - "description": "Add a new subscriber to an audience/list", - "issues": [ - "Parameter \"tags\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "get_subscriber", - "description": "Get subscriber information by email address", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_templates", - "description": "List available email templates", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_campaigns\": Missing _meta labels", - "Tool \"get_campaign\": Missing _meta labels", - "Tool \"create_campaign\": Missing _meta labels", - "Tool \"send_campaign\": Missing _meta labels", - "Tool \"list_lists\": Missing _meta labels", - "Tool \"add_subscriber\": Missing argument descriptions", - "Tool \"add_subscriber\": Missing _meta labels", - "Tool \"get_subscriber\": Missing _meta labels", - "Tool \"list_templates\": Missing _meta labels" - ] - }, - { - "name": "pipedrive", - "tools": [ - { - "name": "list_deals", - "description": "List all deals from Pipedrive. Returns paginated list of deals with their details.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_deal", - "description": "Get details of a specific deal by ID", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_deal", - "description": "Create a new deal in Pipedrive", - "issues": [ - "Parameter \"status\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "update_deal", - "description": "Update an existing deal", - "issues": [ - "Parameter \"status\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_persons", - "description": "List all persons (contacts) from Pipedrive", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_person", - "description": "Create a new person (contact) in Pipedrive", - "issues": [ - "Parameter \"email\" missing description", - "Parameter \"phone\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_activities", - "description": "List all activities from Pipedrive", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "add_activity", - "description": "Add a new activity to Pipedrive", - "issues": [ - "Parameter \"participants\" missing description", - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_deals\": Missing _meta labels", - "Tool \"get_deal\": Missing _meta labels", - "Tool \"create_deal\": Missing argument descriptions", - "Tool \"create_deal\": Missing _meta labels", - "Tool \"update_deal\": Missing argument descriptions", - "Tool \"update_deal\": Missing _meta labels", - "Tool \"list_persons\": Missing _meta labels", - "Tool \"create_person\": Missing argument descriptions", - "Tool \"create_person\": Missing _meta labels", - "Tool \"list_activities\": Missing _meta labels", - "Tool \"add_activity\": Missing argument descriptions", - "Tool \"add_activity\": Missing _meta labels" - ] - }, - { - "name": "rippling", - "tools": [ - { - "name": "list_employees", - "description": "List employees in the organization. Returns employee details including name, email, department, and employment status.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_employee", - "description": "Get detailed information about a specific employee including personal info, employment details, and manager.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_departments", - "description": "List all departments in the organization with their names and hierarchy.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_teams", - "description": "List all teams in the organization. Teams are groups of employees that can span departments.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_payroll", - "description": "Get payroll information and pay runs. Requires payroll read permissions.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_devices", - "description": "List devices managed by Rippling IT. Includes computers, phones, and other equipment assigned to employees.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_apps", - "description": "List applications integrated with Rippling. Shows apps available for provisioning to employees.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_company", - "description": "Get information about the current company including name, EIN, and settings.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_groups", - "description": "List custom groups defined in Rippling. Groups can be used for access control and app provisioning.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_levels", - "description": "List job levels defined in the organization (e.g., IC1, IC2, Manager, Director).", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_work_locations", - "description": "List work locations/offices defined in the organization.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_leave_requests", - "description": "Get leave/time-off requests. Filter by employee, status, or date range.", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_employees\": Missing _meta labels", - "Tool \"get_employee\": Missing _meta labels", - "Tool \"list_departments\": Missing _meta labels", - "Tool \"list_teams\": Missing _meta labels", - "Tool \"get_payroll\": Missing _meta labels", - "Tool \"list_devices\": Missing _meta labels", - "Tool \"list_apps\": Missing _meta labels", - "Tool \"get_company\": Missing _meta labels", - "Tool \"list_groups\": Missing _meta labels", - "Tool \"list_levels\": Missing _meta labels", - "Tool \"list_work_locations\": Missing _meta labels", - "Tool \"get_leave_requests\": Missing _meta labels" - ] - }, - { - "name": "servicetitan", - "tools": [ - { - "name": "list_jobs", - "description": "List jobs/work orders. Jobs represent scheduled service work including location, customer, and technician assignments.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_job", - "description": "Get detailed information about a specific job including line items, equipment, and history.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_job", - "description": "Create a new job/work order. Requires customer, location, and job type.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_customers", - "description": "List customers in the CRM. Includes contact info, locations, and account details.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_customer", - "description": "Get detailed customer information including all locations, contacts, and service history.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_invoices", - "description": "List invoices. Includes amounts, status, line items, and payment information.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_technicians", - "description": "List technicians/field workers. Includes contact info, skills, and availability.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_appointments", - "description": "List scheduled appointments. Shows booking windows, assigned technicians, and status.", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_jobs\": Missing _meta labels", - "Tool \"get_job\": Missing _meta labels", - "Tool \"create_job\": Missing _meta labels", - "Tool \"list_customers\": Missing _meta labels", - "Tool \"get_customer\": Missing _meta labels", - "Tool \"list_invoices\": Missing _meta labels", - "Tool \"list_technicians\": Missing _meta labels", - "Tool \"list_appointments\": Missing _meta labels" - ] - }, - { - "name": "squarespace", - "tools": [ - { - "name": "list_pages", - "description": "List all pages for the website", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_page", - "description": "Get a specific page by ID", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_products", - "description": "List all products from the commerce store", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_product", - "description": "Get a specific product by ID", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_orders", - "description": "List orders from the commerce store", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_order", - "description": "Get a specific order by ID", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_inventory", - "description": "List inventory for all product variants", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "update_inventory", - "description": "Update inventory quantity for a product variant", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_pages\": Missing _meta labels", - "Tool \"get_page\": Missing _meta labels", - "Tool \"list_products\": Missing _meta labels", - "Tool \"get_product\": Missing _meta labels", - "Tool \"list_orders\": Missing _meta labels", - "Tool \"get_order\": Missing _meta labels", - "Tool \"list_inventory\": Missing _meta labels", - "Tool \"update_inventory\": Missing _meta labels" - ] - }, - { - "name": "template", - "tools": [ - { - "name": "list_items", - "description": "List all items", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_item", - "description": "Get a specific item by ID", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_item", - "description": "Create a new item", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "update_item", - "description": "Update an existing item", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "delete_item", - "description": "Delete an item", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_items\": Missing _meta labels", - "Tool \"get_item\": Missing _meta labels", - "Tool \"create_item\": Missing _meta labels", - "Tool \"update_item\": Missing _meta labels", - "Tool \"delete_item\": Missing _meta labels" - ] - }, - { - "name": "toast", - "tools": [ - { - "name": "list_orders", - "description": "List orders from Toast POS within a time range. Returns order summaries with checks, items, and payment info.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_order", - "description": "Get a specific order by GUID with full details including checks, selections, payments", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_menu_items", - "description": "List menu items from Toast menus API. Returns items with prices, modifiers, and availability.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "update_menu_item", - "description": "Update a menu item", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_employees", - "description": "List employees from Toast labor API", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_labor", - "description": "Get labor/time entry data for shifts within a date range", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_checks", - "description": "List checks (tabs) from orders within a time range", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "void_check", - "description": "Void a check (requires proper permissions). This action cannot be undone.", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_orders\": Missing _meta labels", - "Tool \"get_order\": Missing _meta labels", - "Tool \"list_menu_items\": Missing _meta labels", - "Tool \"update_menu_item\": Missing _meta labels", - "Tool \"list_employees\": Missing _meta labels", - "Tool \"get_labor\": Missing _meta labels", - "Tool \"list_checks\": Missing _meta labels", - "Tool \"void_check\": Missing _meta labels" - ] - }, - { - "name": "touchbistro", - "tools": [ - { - "name": "list_orders", - "description": "List orders from TouchBistro POS. Filter by status, order type, and date range.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_order", - "description": "Get detailed information about a specific order by ID, including all items, modifiers, payments, and discounts", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_menu_items", - "description": "List menu items from TouchBistro. Get all items available for ordering.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_reservations", - "description": "List reservations from TouchBistro", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_reservation", - "description": "Create a new reservation in TouchBistro", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_staff", - "description": "List staff members from TouchBistro", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_sales_report", - "description": "Get sales report data from TouchBistro for analysis and reporting", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_orders\": Missing _meta labels", - "Tool \"get_order\": Missing _meta labels", - "Tool \"list_menu_items\": Missing _meta labels", - "Tool \"list_reservations\": Missing _meta labels", - "Tool \"create_reservation\": Missing _meta labels", - "Tool \"list_staff\": Missing _meta labels", - "Tool \"get_sales_report\": Missing _meta labels" - ] - }, - { - "name": "trello", - "tools": [ - { - "name": "list_boards", - "description": "List all boards for the authenticated user", - "issues": [ - "Parameter \"filter\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "get_board", - "description": "Get a specific board by ID with detailed information", - "issues": [ - "Parameter \"lists\" missing description", - "Parameter \"cards\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_lists", - "description": "List all lists on a board", - "issues": [ - "Parameter \"filter\" missing description", - "Parameter \"cards\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_cards", - "description": "List all cards on a board or in a specific list", - "issues": [ - "Parameter \"filter\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "get_card", - "description": "Get a specific card by ID with detailed information", - "issues": [ - "Parameter \"checklists\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "create_card", - "description": "Create a new card on a list", - "issues": [ - "Parameter \"idMembers\" missing description", - "Parameter \"items\" missing description", - "Parameter \"idLabels\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "update_card", - "description": "Update an existing card", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "move_card", - "description": "Move a card to a different list or board", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "add_comment", - "description": "Add a comment to a card", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_list", - "description": "Create a new list on a board", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "archive_card", - "description": "Archive (close) a card", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "delete_card", - "description": "Permanently delete a card (cannot be undone)", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_boards\": Missing argument descriptions", - "Tool \"list_boards\": Missing _meta labels", - "Tool \"get_board\": Missing argument descriptions", - "Tool \"get_board\": Missing _meta labels", - "Tool \"list_lists\": Missing argument descriptions", - "Tool \"list_lists\": Missing _meta labels", - "Tool \"list_cards\": Missing argument descriptions", - "Tool \"list_cards\": Missing _meta labels", - "Tool \"get_card\": Missing argument descriptions", - "Tool \"get_card\": Missing _meta labels", - "Tool \"create_card\": Missing argument descriptions", - "Tool \"create_card\": Missing _meta labels", - "Tool \"update_card\": Missing _meta labels", - "Tool \"move_card\": Missing _meta labels", - "Tool \"add_comment\": Missing _meta labels", - "Tool \"create_list\": Missing _meta labels", - "Tool \"archive_card\": Missing _meta labels", - "Tool \"delete_card\": Missing _meta labels" - ] - }, - { - "name": "wave", - "tools": [ - { - "name": "list_businesses", - "description": "List all businesses in the Wave account", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_invoices", - "description": "List invoices for a business", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_invoice", - "description": "Create a new invoice", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_customers", - "description": "List customers for a business", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_customer", - "description": "Create a new customer", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_accounts", - "description": "List chart of accounts for a business", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_transactions", - "description": "List transactions for a business", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_expense", - "description": "Create a new expense/money transaction", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_businesses\": Missing _meta labels", - "Tool \"list_invoices\": Missing _meta labels", - "Tool \"create_invoice\": Missing _meta labels", - "Tool \"list_customers\": Missing _meta labels", - "Tool \"create_customer\": Missing _meta labels", - "Tool \"list_accounts\": Missing _meta labels", - "Tool \"list_transactions\": Missing _meta labels", - "Tool \"create_expense\": Missing _meta labels" - ] - }, - { - "name": "wrike", - "tools": [ - { - "name": "list_tasks", - "description": "List tasks from Wrike. Can filter by folder and status.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "get_task", - "description": "Get a specific task by ID from Wrike", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_task", - "description": "Create a new task in Wrike", - "issues": [ - "Parameter \"responsibles\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "update_task", - "description": "Update an existing task in Wrike", - "issues": [ - "Parameter \"add_responsibles\" missing description", - "Parameter \"items\" missing description", - "Parameter \"remove_responsibles\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "list_folders", - "description": "List folders from Wrike. Can get child folders of a parent.", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_projects", - "description": "List all projects from Wrike", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "add_comment", - "description": "Add a comment to a task in Wrike", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_users", - "description": "List all users/contacts in Wrike workspace", - "issues": [ - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_tasks\": Missing _meta labels", - "Tool \"get_task\": Missing _meta labels", - "Tool \"create_task\": Missing argument descriptions", - "Tool \"create_task\": Missing _meta labels", - "Tool \"update_task\": Missing argument descriptions", - "Tool \"update_task\": Missing _meta labels", - "Tool \"list_folders\": Missing _meta labels", - "Tool \"list_projects\": Missing _meta labels", - "Tool \"add_comment\": Missing _meta labels", - "Tool \"list_users\": Missing _meta labels" - ] - }, - { - "name": "zendesk", - "tools": [ - { - "name": "list_tickets", - "description": "List tickets. Can filter by status, requester, or other criteria.", - "issues": [ - "Parameter \"status\" missing description", - "Parameter \"sort_order\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "get_ticket", - "description": "Get a specific ticket by ID with all its details", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "create_ticket", - "description": "Create a new support ticket", - "issues": [ - "Parameter \"priority\" missing description", - "Parameter \"tags\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "update_ticket", - "description": "Update an existing ticket", - "issues": [ - "Parameter \"status\" missing description", - "Parameter \"priority\" missing description", - "Parameter \"tags\" missing description", - "Parameter \"items\" missing description", - "Parameter \"additional_tags\" missing description", - "Parameter \"items\" missing description", - "Parameter \"remove_tags\" missing description", - "Parameter \"items\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "add_comment", - "description": "Add a comment to an existing ticket", - "issues": [ - "Missing _meta labels" - ] - }, - { - "name": "list_users", - "description": "List users in the Zendesk account", - "issues": [ - "Parameter \"role\" missing description", - "Missing _meta labels" - ] - }, - { - "name": "search_tickets", - "description": "Search tickets using Zendesk search syntax", - "issues": [ - "Parameter \"sort_order\" missing description", - "Missing _meta labels" - ] - } - ], - "issues": [ - "Tool \"list_tickets\": Missing argument descriptions", - "Tool \"list_tickets\": Missing _meta labels", - "Tool \"get_ticket\": Missing _meta labels", - "Tool \"create_ticket\": Missing argument descriptions", - "Tool \"create_ticket\": Missing _meta labels", - "Tool \"update_ticket\": Missing argument descriptions", - "Tool \"update_ticket\": Missing _meta labels", - "Tool \"add_comment\": Missing _meta labels", - "Tool \"list_users\": Missing argument descriptions", - "Tool \"list_users\": Missing _meta labels", - "Tool \"search_tickets\": Missing argument descriptions", - "Tool \"search_tickets\": Missing _meta labels" - ] - } - ], - "totalServers": 31, - "totalTools": 248, - "issues": { - "missingArgDescriptions": 65, - "missingLabels": 248, - "missingAppURI": 0, - "badNaming": 1 - } -} \ No newline at end of file diff --git a/mcp-diagrams/audit-servers.js b/mcp-diagrams/audit-servers.js deleted file mode 100644 index 924ec57..0000000 --- a/mcp-diagrams/audit-servers.js +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env node -/** - * Audit all MCP servers for: - * 1. Missing argument descriptions - * 2. Missing _meta labels - * 3. Missing app resource URIs - * 4. Improper tool naming - */ - -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const serversDir = path.join(__dirname, 'mcp-servers'); -const results = { - servers: [], - totalServers: 0, - totalTools: 0, - issues: { - missingArgDescriptions: 0, - missingLabels: 0, - missingAppURI: 0, - badNaming: 0, - } -}; - -// Get all server directories -const serverDirs = fs.readdirSync(serversDir).filter(dir => { - const fullPath = path.join(serversDir, dir); - return fs.statSync(fullPath).isDirectory() && fs.existsSync(path.join(fullPath, 'src/index.ts')); -}); - -console.log(`\n🔍 Auditing ${serverDirs.length} MCP servers...\n`); - -for (const serverName of serverDirs) { - const indexPath = path.join(serversDir, serverName, 'src/index.ts'); - const content = fs.readFileSync(indexPath, 'utf-8'); - - const serverResult = { - name: serverName, - tools: [], - issues: [], - }; - - // Extract tool definitions using regex (simple approach) - const toolMatches = content.matchAll(/{\s*name:\s*["']([^"']+)["']\s*,\s*description:\s*["']([^"']+)["']/gs); - - for (const match of toolMatches) { - const toolName = match[1]; - const toolDesc = match[2]; - - const tool = { - name: toolName, - description: toolDesc, - issues: [], - }; - - // Check tool naming convention - if (!toolName.match(/^[a-z]+_[a-z_]+$/)) { - tool.issues.push('Bad naming (not snake_case verb_noun)'); - serverResult.issues.push(`Tool "${toolName}": Bad naming`); - results.issues.badNaming++; - } - - // Find the tool's inputSchema section - const toolStartIndex = content.indexOf(`name: "${toolName}"`); - const nextToolIndex = content.indexOf('\n {', toolStartIndex + 1); - const toolSection = content.substring( - toolStartIndex, - nextToolIndex === -1 ? content.length : nextToolIndex - ); - - // Check for missing parameter descriptions - const paramMatches = toolSection.matchAll(/(\w+):\s*{\s*type:\s*["'](\w+)["']\s*(?:,\s*description:\s*["']([^"']+)["'])?/g); - let hasMissingDesc = false; - - for (const paramMatch of paramMatches) { - const paramName = paramMatch[1]; - const paramType = paramMatch[2]; - const paramDesc = paramMatch[3]; - - // Skip if it's not actually a parameter (could be part of schema structure) - if (paramName === 'type' || paramName === 'properties' || paramName === 'required') continue; - - if (!paramDesc && paramType !== 'object') { - tool.issues.push(`Parameter "${paramName}" missing description`); - hasMissingDesc = true; - } - } - - if (hasMissingDesc) { - serverResult.issues.push(`Tool "${toolName}": Missing argument descriptions`); - results.issues.missingArgDescriptions++; - } - - // Check for _meta labels - if (!toolSection.includes('_meta')) { - tool.issues.push('Missing _meta labels'); - serverResult.issues.push(`Tool "${toolName}": Missing _meta labels`); - results.issues.missingLabels++; - } - - // Check for app tools missing resourceUri - if (toolName.startsWith('view_') || toolName.startsWith('show_')) { - if (!toolSection.includes('resourceUri')) { - tool.issues.push('App tool missing resourceUri'); - serverResult.issues.push(`Tool "${toolName}": App tool missing resourceUri`); - results.issues.missingAppURI++; - } - } - - serverResult.tools.push(tool); - } - - results.servers.push(serverResult); - results.totalServers++; - results.totalTools += serverResult.tools.length; - - // Print server summary - if (serverResult.issues.length > 0) { - console.log(`❌ ${serverName} (${serverResult.tools.length} tools, ${serverResult.issues.length} issues)`); - serverResult.issues.forEach(issue => console.log(` - ${issue}`)); - } else { - console.log(`✅ ${serverName} (${serverResult.tools.length} tools)`); - } -} - -// Summary -console.log(`\n📊 Summary:`); -console.log(` Total servers: ${results.totalServers}`); -console.log(` Total tools: ${results.totalTools}`); -console.log(`\n⚠️ Issues found:`); -console.log(` Missing argument descriptions: ${results.issues.missingArgDescriptions}`); -console.log(` Missing _meta labels: ${results.issues.missingLabels}`); -console.log(` App tools missing resourceUri: ${results.issues.missingAppURI}`); -console.log(` Bad tool naming: ${results.issues.badNaming}`); - -// Write detailed report -const reportPath = path.join(__dirname, 'audit-report.json'); -fs.writeFileSync(reportPath, JSON.stringify(results, null, 2)); -console.log(`\n📄 Detailed report written to: audit-report.json\n`); diff --git a/mcp-diagrams/fix-servers.js b/mcp-diagrams/fix-servers.js deleted file mode 100644 index 65f25c7..0000000 --- a/mcp-diagrams/fix-servers.js +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env node -/** - * Automatically add _meta labels to all tools across all servers - */ - -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const serversDir = path.join(__dirname, 'mcp-servers'); - -// Category mapping based on tool name patterns -const getCategoryFromToolName = (toolName, serverName) => { - // Check server type first - if (serverName.includes('freshdesk') || serverName.includes('zendesk') || serverName.includes('helpscout')) { - if (toolName.includes('ticket')) return 'support'; - if (toolName.includes('contact') || toolName.includes('customer')) return 'contacts'; - if (toolName.includes('agent') || toolName.includes('user')) return 'team'; - return 'support'; - } - - if (serverName.includes('servicetitan') || serverName.includes('housecall') || serverName.includes('jobber') || serverName.includes('fieldedge')) { - if (toolName.includes('job') || toolName.includes('work_order')) return 'jobs'; - if (toolName.includes('customer') || toolName.includes('client')) return 'customers'; - if (toolName.includes('invoice')) return 'billing'; - if (toolName.includes('technician') || toolName.includes('employee')) return 'team'; - if (toolName.includes('appointment') || toolName.includes('schedule')) return 'scheduling'; - return 'jobs'; - } - - if (serverName.includes('mailchimp') || serverName.includes('constant-contact') || serverName.includes('brevo')) { - if (toolName.includes('campaign')) return 'campaigns'; - if (toolName.includes('contact') || toolName.includes('subscriber')) return 'contacts'; - if (toolName.includes('list')) return 'lists'; - if (toolName.includes('template')) return 'templates'; - return 'campaigns'; - } - - if (serverName.includes('pipedrive') || serverName.includes('close')) { - if (toolName.includes('deal') || toolName.includes('opportunity')) return 'deals'; - if (toolName.includes('person') || toolName.includes('contact') || toolName.includes('lead')) return 'contacts'; - if (toolName.includes('activity')) return 'activities'; - return 'crm'; - } - - if (serverName.includes('trello') || serverName.includes('clickup') || serverName.includes('wrike') || serverName.includes('basecamp')) { - if (toolName.includes('board') || toolName.includes('project')) return 'projects'; - if (toolName.includes('task')) return 'tasks'; - if (toolName.includes('comment')) return 'collaboration'; - return 'projects'; - } - - if (serverName.includes('gusto') || serverName.includes('rippling') || serverName.includes('bamboohr')) { - if (toolName.includes('employee')) return 'employees'; - if (toolName.includes('payroll')) return 'payroll'; - if (toolName.includes('benefit')) return 'benefits'; - if (toolName.includes('time_off') || toolName.includes('leave')) return 'time-off'; - return 'hr'; - } - - if (serverName.includes('toast') || serverName.includes('square') || serverName.includes('clover') || serverName.includes('lightspeed') || serverName.includes('touchbistro')) { - if (toolName.includes('order') || toolName.includes('check')) return 'orders'; - if (toolName.includes('menu') || toolName.includes('item')) return 'menu'; - if (toolName.includes('employee') || toolName.includes('staff')) return 'team'; - if (toolName.includes('reservation')) return 'reservations'; - return 'pos'; - } - - if (serverName.includes('calendly') || serverName.includes('acuity')) { - if (toolName.includes('event') || toolName.includes('appointment')) return 'scheduling'; - if (toolName.includes('availability')) return 'availability'; - if (toolName.includes('calendar')) return 'calendars'; - return 'scheduling'; - } - - // Generic patterns - if (toolName.includes('contact') || toolName.includes('customer') || toolName.includes('client') || toolName.includes('person')) return 'contacts'; - if (toolName.includes('deal') || toolName.includes('opportunity')) return 'deals'; - if (toolName.includes('invoice') || toolName.includes('payment')) return 'billing'; - if (toolName.includes('task')) return 'tasks'; - if (toolName.includes('project')) return 'projects'; - if (toolName.includes('calendar') || toolName.includes('event') || toolName.includes('appointment')) return 'calendar'; - if (toolName.includes('campaign')) return 'campaigns'; - if (toolName.includes('report') || toolName.includes('analytics') || toolName.includes('stats')) return 'analytics'; - if (toolName.includes('employee') || toolName.includes('team') || toolName.includes('user')) return 'team'; - - return 'general'; -}; - -// Access type from tool name -const getAccessType = (toolName) => { - if (toolName.startsWith('list_') || toolName.startsWith('get_') || toolName.startsWith('search_')) return 'read'; - if (toolName.startsWith('create_') || toolName.startsWith('add_')) return 'write'; - if (toolName.startsWith('update_') || toolName.startsWith('modify_')) return 'write'; - if (toolName.startsWith('delete_') || toolName.startsWith('remove_') || toolName.startsWith('void_') || toolName.startsWith('cancel_') || toolName.startsWith('archive_')) return 'delete'; - if (toolName.startsWith('send_')) return 'write'; - return 'read'; // default to read -}; - -// Complexity from tool name -const getComplexity = (toolName) => { - if (toolName.startsWith('list_') || toolName.startsWith('get_')) return 'simple'; - if (toolName.startsWith('search_')) return 'simple'; - if (toolName.startsWith('create_') || toolName.startsWith('update_')) return 'simple'; - return 'simple'; // Most operations are simple -}; - -// Get all server directories -const serverDirs = fs.readdirSync(serversDir).filter(dir => { - const fullPath = path.join(serversDir, dir); - return fs.statSync(fullPath).isDirectory() && fs.existsSync(path.join(fullPath, 'src/index.ts')); -}); - -console.log(`\n🔧 Fixing ${serverDirs.length} MCP servers...\n`); - -let totalFixed = 0; - -for (const serverName of serverDirs) { - const indexPath = path.join(serversDir, serverName, 'src/index.ts'); - let content = fs.readFileSync(indexPath, 'utf-8'); - let modified = false; - let fixedCount = 0; - - // Find all tool definitions - const toolRegex = /{\s*name:\s*["']([^"']+)["']\s*,\s*description:\s*["']([^"']+)["']\s*,\s*inputSchema:\s*{[\s\S]*?}(\s*,?\s*)}(?=\s*,?\s*{?\s*name:|\s*\])/g; - - content = content.replace(toolRegex, (match, toolName, description) => { - // Check if already has _meta - if (match.includes('_meta')) { - return match; - } - - // Get labels - const category = getCategoryFromToolName(toolName, serverName); - const access = getAccessType(toolName); - const complexity = getComplexity(toolName); - - // Add _meta before closing brace - const metaSection = `,\n _meta: {\n labels: {\n category: "${category}",\n access: "${access}",\n complexity: "${complexity}",\n },\n }`; - - // Find the last } before the closing of the tool object - const lastBraceIndex = match.lastIndexOf('}'); - const modifiedMatch = match.slice(0, lastBraceIndex) + metaSection + '\n ' + match.slice(lastBraceIndex); - - modified = true; - fixedCount++; - return modifiedMatch; - }); - - if (modified) { - fs.writeFileSync(indexPath, content, 'utf-8'); - console.log(`✅ ${serverName}: Fixed ${fixedCount} tools`); - totalFixed += fixedCount; - } else { - console.log(`⏭️ ${serverName}: No changes needed`); - } -} - -console.log(`\n✨ Total tools fixed: ${totalFixed}\n`); diff --git a/mcp-diagrams/ghl-mcp-apps-only/package.json b/mcp-diagrams/ghl-mcp-apps-only/package.json deleted file mode 100644 index 67223fe..0000000 --- a/mcp-diagrams/ghl-mcp-apps-only/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "ghl-mcp-apps-only", - "version": "1.0.0", - "description": "GoHighLevel MCP Apps Only - Slimmed down for testing", - "main": "dist/server.js", - "type": "module", - "scripts": { - "build": "tsc", - "start": "node dist/server.js", - "dev": "tsx src/server.ts" - }, - "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.1", - "axios": "^1.9.0", - "dotenv": "^16.5.0" - }, - "devDependencies": { - "@types/node": "^22.15.29", - "tsx": "^4.7.0", - "typescript": "^5.8.3" - } -} diff --git a/mcp-diagrams/ghl-mcp-apps-only/src/apps/index.ts b/mcp-diagrams/ghl-mcp-apps-only/src/apps/index.ts deleted file mode 100644 index 7184538..0000000 --- a/mcp-diagrams/ghl-mcp-apps-only/src/apps/index.ts +++ /dev/null @@ -1,824 +0,0 @@ -/** - * MCP Apps Manager - * Manages rich UI components for GoHighLevel MCP Server - */ - -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'; - -export interface AppToolResult { - content: Array<{ type: 'text'; text: string }>; - structuredContent?: Record; - [key: string]: unknown; -} - -export interface AppResourceHandler { - uri: string; - mimeType: string; - getContent: () => string; -} - -/** - * MCP Apps Manager class - * Registers app tools and handles structuredContent responses - */ -// ESM equivalent of __dirname -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// Resolve UI build path - works regardless of working directory -function getUIBuildPath(): string { - // When compiled, this file is at dist/apps/index.js - // UI files are at dist/app-ui/ - const fromDist = path.resolve(__dirname, '..', 'app-ui'); - if (fs.existsSync(fromDist)) { - return fromDist; - } - // Fallback: try process.cwd() based paths - const appUiPath = path.join(process.cwd(), 'dist', 'app-ui'); - if (fs.existsSync(appUiPath)) { - return appUiPath; - } - // Default fallback - return fromDist; -} - -export class MCPAppsManager { - private ghlClient: GHLApiClient; - private resourceHandlers: Map = new Map(); - private uiBuildPath: string; - - constructor(ghlClient: GHLApiClient) { - this.ghlClient = ghlClient; - this.uiBuildPath = getUIBuildPath(); - process.stderr.write(`[MCP Apps] UI build path: ${this.uiBuildPath}\n`); - this.registerResourceHandlers(); - } - - /** - * Register all UI resource handlers - */ - 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' }, - ]; - - for (const resource of resources) { - this.resourceHandlers.set(resource.uri, { - uri: resource.uri, - mimeType: 'text/html;profile=mcp-app', - getContent: () => this.loadUIResource(resource.file), - }); - } - } - - /** - * Load UI resource from build directory - */ - 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); - } - } - - /** - * 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(); - } - - /** - * Get tool definitions for all app tools - */ - getToolDefinitions(): Tool[] { - 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.', - inputSchema: { - type: 'object', - properties: { - query: { type: 'string', description: 'Search query string' }, - limit: { type: 'number', description: 'Maximum results (default: 25)' } - } - }, - _meta: { - ui: { resourceUri: 'ui://ghl/contact-grid' } - } - }, - // 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' } - }, - required: ['pipelineId'] - }, - _meta: { - ui: { resourceUri: 'ui://ghl/pipeline-board' } - } - }, - // 3. Quick Book - appointment booking - { - name: 'view_quick_book', - description: 'Display a quick booking interface for scheduling appointments. Returns a visual UI component.', - inputSchema: { - type: 'object', - properties: { - calendarId: { type: 'string', description: 'Calendar ID for booking' }, - contactId: { type: 'string', description: 'Optional contact ID to pre-fill' } - }, - required: ['calendarId'] - }, - _meta: { - ui: { resourceUri: 'ui://ghl/quick-book' } - } - }, - // 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' } - }, - required: ['opportunityId'] - }, - _meta: { - ui: { resourceUri: 'ui://ghl/opportunity-card' } - } - }, - // 5. Calendar View - calendar with events - { - name: 'view_calendar', - description: 'Display a calendar with events and appointments. Returns a visual UI component.', - inputSchema: { - type: 'object', - 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)' } - }, - required: ['calendarId'] - }, - _meta: { - ui: { resourceUri: 'ui://ghl/calendar-view' } - } - }, - // 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' } - }, - required: ['invoiceId'] - }, - _meta: { - ui: { resourceUri: 'ui://ghl/invoice-preview' } - } - }, - // 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' } - }, - required: ['campaignId'] - }, - _meta: { - ui: { resourceUri: 'ui://ghl/campaign-stats' } - } - }, - // 8. Agent Stats - agent/user performance - { - name: 'view_agent_stats', - description: 'Display agent/user performance statistics and metrics. Returns a visual UI component.', - inputSchema: { - type: 'object', - properties: { - userId: { type: 'string', description: 'User/Agent ID to display stats for' }, - dateRange: { type: 'string', description: 'Date range (e.g., "last7days", "last30days")' } - } - }, - _meta: { - ui: { resourceUri: 'ui://ghl/agent-stats' } - } - }, - // 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.', - inputSchema: { - type: 'object', - properties: { - contactId: { type: 'string', description: 'Contact ID to display timeline for' } - }, - required: ['contactId'] - }, - _meta: { - ui: { resourceUri: 'ui://ghl/contact-timeline' } - } - }, - // 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' } - }, - required: ['workflowId'] - }, - _meta: { - ui: { resourceUri: 'ui://ghl/workflow-status' } - } - }, - // 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: {} - }, - _meta: { - ui: { resourceUri: 'ui://ghl/mcp-app' } - } - }, - // 12. Update Opportunity - action tool for UI to update opportunities - { - name: 'update_opportunity', - description: 'Update an opportunity (move to stage, change value, status, etc.)', - inputSchema: { - type: 'object', - properties: { - opportunityId: { type: 'string', description: 'Opportunity ID to update' }, - 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' } - }, - required: ['opportunityId'] - } - } - ]; - } - - /** - * Get app tool names for 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' - ]; - } - - /** - * 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`); - - switch (toolName) { - case 'view_contact_grid': - return await this.viewContactGrid(args.query, args.limit); - case 'view_pipeline_board': - return await this.viewPipelineBoard(args.pipelineId); - case 'view_quick_book': - return await this.viewQuickBook(args.calendarId, args.contactId); - case 'view_opportunity_card': - return await this.viewOpportunityCard(args.opportunityId); - case 'view_calendar': - return await this.viewCalendar(args.calendarId, args.startDate, args.endDate); - case 'view_invoice': - return await this.viewInvoice(args.invoiceId); - case 'view_campaign_stats': - return await this.viewCampaignStats(args.campaignId); - case 'view_agent_stats': - return await this.viewAgentStats(args.userId, args.dateRange); - case 'view_contact_timeline': - return await this.viewContactTimeline(args.contactId); - case 'view_workflow_status': - return await this.viewWorkflowStatus(args.workflowId); - case 'view_dashboard': - return await this.viewDashboard(); - case 'update_opportunity': - return await this.updateOpportunity(args as { - opportunityId: string; - pipelineStageId?: string; - name?: string; - monetaryValue?: number; - status?: 'open' | 'won' | 'lost' | 'abandoned'; - }); - default: - throw new Error(`Unknown app tool: ${toolName}`); - } - } - - /** - * View contact grid (search results) - */ - private async viewContactGrid(query?: string, limit?: number): Promise { - const response = await this.ghlClient.searchContacts({ - locationId: this.ghlClient.getConfig().locationId, - query: query, - limit: limit || 25 - }); - - 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 - ); - } - - /** - * 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 - }) - ]); - - 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', - 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 - })); - - const data = { - pipeline, - opportunities: simplifiedOpportunities, - 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 - ); - } - - /** - * 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 }) - ]); - - if (!calendarResponse.success) { - throw new Error(calendarResponse.error?.message || 'Failed to get calendar'); - } - - const data = { - calendar: calendarResponse.data, - contact: contactResponse.data, - 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 - ); - } - - /** - * 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'); - } - - 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 - ); - } - - /** - * 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(); - const end = endDate || new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString(); - - const [calendarResponse, eventsResponse] = await Promise.all([ - this.ghlClient.getCalendar(calendarId), - this.ghlClient.getCalendarEvents({ - calendarId: calendarId, - startTime: start, - endTime: end, - locationId: this.ghlClient.getConfig().locationId - }) - ]); - - 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 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 - ); - } - - /** - * View campaign stats - */ - private async viewCampaignStats(campaignId: string): Promise { - // Get email campaigns - 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 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 - ); - } - - /** - * 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', - location: locationResponse.data, - 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 - ); - } - - /** - * 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) - ]); - - if (!contactResponse.success) { - throw new Error(contactResponse.error?.message || 'Failed to get contact'); - } - - const contact = contactResponse.data as any; - const data = { - 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 - }); - - 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 resourceHandler = this.resourceHandlers.get('ui://ghl/workflow-status')!; - - return this.createAppResult( - `Workflow: ${(workflow as any)?.name || workflowId}`, - resourceHandler.uri, - resourceHandler.mimeType, - resourceHandler.getContent(), - data - ); - } - - /** - * 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() - ]); - - const data = { - 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' - }); - - 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 - ); - } - - /** - * Update opportunity (action tool for UI) - */ - private async updateOpportunity(args: { - opportunityId: string; - pipelineStageId?: string; - name?: string; - monetaryValue?: number; - 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; - if (updates.monetaryValue !== undefined) updatePayload.monetaryValue = updates.monetaryValue; - 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'); - } - - const opportunity = response.data; - - return { - content: [{ type: 'text', text: `Updated opportunity: ${opportunity?.name || opportunityId}` }], - structuredContent: { - success: true, - opportunity: { - id: opportunity?.id, - name: opportunity?.name, - pipelineStageId: opportunity?.pipelineStageId, - monetaryValue: opportunity?.monetaryValue, - status: opportunity?.status - } - } - }; - } - - /** - * Create app tool result with structuredContent - */ - 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 - return { - content: [{ type: 'text', text: textSummary }], - structuredContent: data - }; - } - - /** - * Inject data into HTML as a script tag - */ - 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; - } - } - - /** - * Get resource handler by URI - */ - 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/mcp-diagrams/ghl-mcp-apps-only/src/clients/ghl-api-client.ts b/mcp-diagrams/ghl-mcp-apps-only/src/clients/ghl-api-client.ts deleted file mode 100644 index d5a307c..0000000 --- a/mcp-diagrams/ghl-mcp-apps-only/src/clients/ghl-api-client.ts +++ /dev/null @@ -1,6858 +0,0 @@ -/** - * GoHighLevel API Client - * Implements exact API endpoints from OpenAPI specifications v2021-07-28 (Contacts) and v2021-04-15 (Conversations) - */ - -import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; -import { - GHLConfig, - GHLContact, - GHLCreateContactRequest, - GHLSearchContactsRequest, - GHLSearchContactsResponse, - GHLContactTagsRequest, - GHLContactTagsResponse, - GHLApiResponse, - GHLErrorResponse, - GHLTask, - GHLNote, - // Conversation types - GHLConversation, - GHLMessage, - GHLSendMessageRequest, - GHLSendMessageResponse, - GHLSearchConversationsRequest, - GHLSearchConversationsResponse, - GHLGetMessagesResponse, - GHLCreateConversationRequest, - GHLCreateConversationResponse, - GHLUpdateConversationRequest, - // Blog types - GHLBlogPost, - GHLCreateBlogPostRequest, - GHLUpdateBlogPostRequest, - GHLBlogPostCreateResponse, - GHLBlogPostUpdateResponse, - GHLBlogPostListResponse, - GHLBlogAuthor, - GHLBlogAuthorsResponse, - GHLBlogCategory, - GHLBlogCategoriesResponse, - GHLBlogSite, - GHLBlogSitesResponse, - GHLUrlSlugCheckResponse, - GHLGetBlogPostsRequest, - GHLGetBlogAuthorsRequest, - GHLGetBlogCategoriesRequest, - GHLGetBlogSitesRequest, - GHLCheckUrlSlugRequest, - GHLSearchOpportunitiesRequest, - GHLSearchOpportunitiesResponse, - GHLGetPipelinesResponse, - GHLOpportunity, - GHLCreateOpportunityRequest, - GHLUpdateOpportunityRequest, - GHLOpportunityStatus, - GHLUpdateOpportunityStatusRequest, - GHLUpsertOpportunityRequest, - GHLUpsertOpportunityResponse, - GHLGetCalendarGroupsResponse, - GHLCreateCalendarGroupRequest, - GHLCalendarGroup, - GHLGetCalendarsResponse, - GHLCreateCalendarRequest, - GHLCalendar, - GHLUpdateCalendarRequest, - GHLGetCalendarEventsRequest, - GHLGetCalendarEventsResponse, - GHLGetFreeSlotsRequest, - GHLGetFreeSlotsResponse, - GHLCreateAppointmentRequest, - GHLCalendarEvent, - GHLUpdateAppointmentRequest, - GHLCreateBlockSlotRequest, - GHLBlockSlotResponse, - GHLUpdateBlockSlotRequest, - GHLEmailCampaignsResponse, - MCPGetEmailCampaignsParams, - MCPCreateEmailTemplateParams, - MCPGetEmailTemplatesParams, - MCPUpdateEmailTemplateParams, - MCPDeleteEmailTemplateParams, - GHLEmailTemplate, - // Location types - GHLLocationSearchResponse, - GHLLocationDetailsResponse, - GHLLocationDetailed, - GHLCreateLocationRequest, - GHLUpdateLocationRequest, - GHLLocationDeleteResponse, - GHLLocationTagsResponse, - GHLLocationTagResponse, - GHLLocationTagRequest, - GHLLocationTagDeleteResponse, - GHLLocationTaskSearchRequest, - GHLLocationTaskSearchResponse, - GHLLocationCustomFieldsResponse, - GHLLocationCustomFieldResponse, - GHLCreateCustomFieldRequest, - GHLUpdateCustomFieldRequest, - GHLCustomFieldDeleteResponse, - GHLFileUploadRequest, - GHLFileUploadResponse, - GHLLocationCustomValuesResponse, - GHLLocationCustomValueResponse, - GHLCustomValueRequest, - GHLCustomValueDeleteResponse, - GHLLocationTemplatesResponse, - // Email ISV types - GHLEmailVerificationRequest, - GHLEmailVerificationResponse, - // Additional Contact types - GHLAppointment, - GHLUpsertContactResponse, - GHLBulkTagsResponse, - GHLBulkBusinessResponse, - GHLFollowersResponse, - GHLCampaign, - GHLWorkflow, - // Additional Conversation/Message types - GHLEmailMessage, - GHLProcessInboundMessageRequest, - GHLProcessOutboundMessageRequest, - GHLProcessMessageResponse, - GHLCancelScheduledResponse, - GHLMessageRecordingResponse, - GHLMessageTranscription, - GHLMessageTranscriptionResponse, - GHLLiveChatTypingRequest, - GHLLiveChatTypingResponse, - GHLUploadFilesRequest, - GHLUploadFilesResponse, - GHLUpdateMessageStatusRequest, - // Social Media Posting API types - GHLSocialPlatform, - GHLSearchPostsRequest, - GHLSearchPostsResponse, - GHLCreatePostRequest, - GHLCreatePostResponse, - GHLUpdatePostRequest, - GHLGetPostResponse, - GHLBulkDeletePostsRequest, - GHLBulkDeleteResponse, - GHLGetAccountsResponse, - GHLUploadCSVRequest, - GHLUploadCSVResponse, - GHLGetUploadStatusResponse, - GHLSetAccountsRequest, - GHLCSVFinalizeRequest, - GHLGetCategoriesResponse, - GHLGetCategoryResponse, - GHLGetTagsResponse, - GHLGetTagsByIdsRequest, - GHLGetTagsByIdsResponse, - GHLOAuthStartResponse, - GHLGetGoogleLocationsResponse, - GHLAttachGMBLocationRequest, - GHLGetFacebookPagesResponse, - GHLAttachFBAccountRequest, - GHLGetInstagramAccountsResponse, - GHLAttachIGAccountRequest, - GHLGetLinkedInAccountsResponse, - GHLAttachLinkedInAccountRequest, - GHLGetTwitterAccountsResponse, - GHLAttachTwitterAccountRequest, - GHLGetTikTokAccountsResponse, - GHLAttachTikTokAccountRequest, - GHLCSVImport, - GHLSocialPost, - GHLSocialAccount, - GHLValidateGroupSlugResponse, - GHLGroupSuccessResponse, - GHLGroupStatusUpdateRequest, - GHLUpdateCalendarGroupRequest, - GHLGetAppointmentNotesResponse, - GHLCreateAppointmentNoteRequest, - GHLAppointmentNoteResponse, - GHLUpdateAppointmentNoteRequest, - GHLDeleteAppointmentNoteResponse, - GHLCalendarResource, - GHLCreateCalendarResourceRequest, - GHLCalendarResourceResponse, - GHLCalendarResourceByIdResponse, - GHLUpdateCalendarResourceRequest, - GHLResourceDeleteResponse, - GHLCalendarNotification, - GHLCreateCalendarNotificationRequest, - GHLUpdateCalendarNotificationRequest, - GHLCalendarNotificationDeleteResponse, - GHLGetCalendarNotificationsRequest, - GHLGetBlockedSlotsRequest, - GHLGetMediaFilesRequest, - GHLGetMediaFilesResponse, - GHLUploadMediaFileRequest, - GHLUploadMediaFileResponse, - GHLDeleteMediaRequest, - GHLDeleteMediaResponse, - // Custom Objects API types - GHLGetObjectSchemaRequest, - GHLGetObjectSchemaResponse, - GHLObjectListResponse, - GHLCreateObjectSchemaRequest, - GHLObjectSchemaResponse, - GHLUpdateObjectSchemaRequest, - GHLCreateObjectRecordRequest, - GHLObjectRecordResponse, - GHLDetailedObjectRecordResponse, - GHLUpdateObjectRecordRequest, - GHLObjectRecordDeleteResponse, - GHLSearchObjectRecordsRequest, - GHLSearchObjectRecordsResponse, - // Associations API types - GHLAssociation, - GHLRelation, - GHLCreateAssociationRequest, - GHLUpdateAssociationRequest, - GHLCreateRelationRequest, - GHLGetAssociationsRequest, - GHLGetRelationsByRecordRequest, - GHLGetAssociationByKeyRequest, - GHLGetAssociationByObjectKeyRequest, - GHLDeleteRelationRequest, - GHLAssociationResponse, - GHLDeleteAssociationResponse, - GHLGetAssociationsResponse, - GHLGetRelationsResponse, - // Custom Fields V2 API types - GHLV2CustomField, - GHLV2CustomFieldFolder, - GHLV2CreateCustomFieldRequest, - GHLV2UpdateCustomFieldRequest, - GHLV2CreateCustomFieldFolderRequest, - GHLV2UpdateCustomFieldFolderRequest, - GHLV2GetCustomFieldsByObjectKeyRequest, - GHLV2DeleteCustomFieldFolderRequest, - GHLV2CustomFieldResponse, - GHLV2CustomFieldsResponse, - GHLV2CustomFieldFolderResponse, - GHLV2DeleteCustomFieldResponse, - // Workflows API types - GHLGetWorkflowsRequest, - GHLGetWorkflowsResponse, - // Surveys API types - GHLGetSurveysRequest, - GHLGetSurveysResponse, - GHLGetSurveySubmissionsRequest, - GHLGetSurveySubmissionsResponse, - // Store API types - GHLCreateShippingZoneRequest, - GHLCreateShippingZoneResponse, - GHLListShippingZonesResponse, - GHLGetShippingZonesRequest, - GHLGetShippingZoneResponse, - GHLUpdateShippingZoneRequest, - GHLUpdateShippingZoneResponse, - GHLDeleteShippingZoneRequest, - GHLDeleteShippingZoneResponse, - GHLGetAvailableShippingRatesRequest, - GHLGetAvailableShippingRatesResponse, - GHLCreateShippingRateRequest, - GHLCreateShippingRateResponse, - GHLListShippingRatesResponse, - GHLGetShippingRatesRequest, - GHLGetShippingRateResponse, - GHLUpdateShippingRateRequest, - GHLUpdateShippingRateResponse, - GHLDeleteShippingRateRequest, - GHLDeleteShippingRateResponse, - GHLCreateShippingCarrierRequest, - GHLCreateShippingCarrierResponse, - GHLListShippingCarriersResponse, - GHLGetShippingCarriersRequest, - GHLGetShippingCarrierResponse, - GHLUpdateShippingCarrierRequest, - GHLUpdateShippingCarrierResponse, - GHLDeleteShippingCarrierRequest, - GHLDeleteShippingCarrierResponse, - GHLCreateStoreSettingRequest, - GHLCreateStoreSettingResponse, - GHLGetStoreSettingRequest, - GHLGetStoreSettingResponse, - GHLCreateProductRequest, - GHLCreateProductResponse, - GHLUpdateProductRequest, - GHLUpdateProductResponse, - GHLGetProductRequest, - GHLGetProductResponse, - GHLListProductsRequest, - GHLListProductsResponse, - GHLDeleteProductRequest, - GHLDeleteProductResponse, - GHLBulkUpdateRequest, - GHLBulkUpdateResponse, - GHLCreatePriceRequest, - GHLCreatePriceResponse, - GHLUpdatePriceRequest, - GHLUpdatePriceResponse, - GHLGetPriceRequest, - GHLGetPriceResponse, - GHLListPricesRequest, - GHLListPricesResponse, - GHLDeletePriceRequest, - GHLDeletePriceResponse, - GHLListInventoryRequest, - GHLListInventoryResponse, - GHLUpdateInventoryRequest, - GHLUpdateInventoryResponse, - GHLGetProductStoreStatsRequest, - GHLGetProductStoreStatsResponse, - GHLUpdateProductStoreRequest, - GHLUpdateProductStoreResponse, - GHLCreateProductCollectionRequest, - GHLCreateCollectionResponse, - GHLUpdateProductCollectionRequest, - GHLUpdateProductCollectionResponse, - GHLGetProductCollectionRequest, - GHLDefaultCollectionResponse, - GHLListProductCollectionsRequest, - GHLListCollectionResponse, - GHLDeleteProductCollectionRequest, - GHLDeleteProductCollectionResponse, - GHLListProductReviewsRequest, - GHLListProductReviewsResponse, - GHLGetReviewsCountRequest, - GHLCountReviewsByStatusResponse, - GHLUpdateProductReviewRequest, - GHLUpdateProductReviewsResponse, - GHLDeleteProductReviewRequest, - GHLDeleteProductReviewResponse, - GHLBulkUpdateProductReviewsRequest, - // Invoice API types - CreateInvoiceTemplateDto, - CreateInvoiceTemplateResponseDto, - UpdateInvoiceTemplateDto, - UpdateInvoiceTemplateResponseDto, - DeleteInvoiceTemplateResponseDto, - ListTemplatesResponse, - InvoiceTemplate, - UpdateInvoiceLateFeesConfigurationDto, - UpdatePaymentMethodsConfigurationDto, - CreateInvoiceScheduleDto, - CreateInvoiceScheduleResponseDto, - UpdateInvoiceScheduleDto, - UpdateInvoiceScheduleResponseDto, - DeleteInvoiceScheduleResponseDto, - ListSchedulesResponse, - GetScheduleResponseDto, - ScheduleInvoiceScheduleDto, - ScheduleInvoiceScheduleResponseDto, - AutoPaymentScheduleDto, - AutoPaymentInvoiceScheduleResponseDto, - CancelInvoiceScheduleDto, - CancelInvoiceScheduleResponseDto, - UpdateAndScheduleInvoiceScheduleResponseDto, - Text2PayDto, - Text2PayInvoiceResponseDto, - GenerateInvoiceNumberResponse, - GetInvoiceResponseDto, - UpdateInvoiceDto, - UpdateInvoiceResponseDto, - DeleteInvoiceResponseDto, - VoidInvoiceDto, - VoidInvoiceResponseDto, - SendInvoiceDto, - SendInvoicesResponseDto, - RecordPaymentDto, - RecordPaymentResponseDto, - PatchInvoiceStatsLastViewedDto, - CreateEstimatesDto, - EstimateResponseDto, - UpdateEstimateDto, - GenerateEstimateNumberResponse, - SendEstimateDto, - CreateInvoiceFromEstimateDto, - CreateInvoiceFromEstimateResponseDto, - ListEstimatesResponseDto, - EstimateIdParam, - ListEstimateTemplateResponseDto, - EstimateTemplatesDto, - EstimateTemplateResponseDto, - CreateInvoiceDto, - CreateInvoiceResponseDto, - ListInvoicesResponseDto, - AltDto -} from '../types/ghl-types.js'; - -/** - * GoHighLevel API Client - * Handles all API communication with GHL services - */ -export class GHLApiClient { - private axiosInstance: AxiosInstance; - private config: GHLConfig; - - constructor(config: GHLConfig) { - this.config = config; - - // Create axios instance with base configuration - this.axiosInstance = axios.create({ - baseURL: config.baseUrl, - headers: { - 'Authorization': `Bearer ${config.accessToken}`, - 'Version': config.version, - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }, - timeout: 30000 // 30 second timeout - }); - - // Add request interceptor for logging - this.axiosInstance.interceptors.request.use( - (config) => { - process.stderr.write(`[GHL API] ${config.method?.toUpperCase()} ${config.url}\n`); - return config; - }, - (error) => { - console.error('[GHL API] Request error:', error); - return Promise.reject(error); - } - ); - - // Add response interceptor for error handling - this.axiosInstance.interceptors.response.use( - (response) => { - process.stderr.write(`[GHL API] Response ${response.status}: ${response.config.url}\n`); - return response; - }, - (error: AxiosError) => { - console.error('[GHL API] Response error:', { - status: error.response?.status, - message: error.response?.data?.message, - url: error.config?.url - }); - return Promise.reject(this.handleApiError(error)); - } - ); - } - - /** - * Handle API errors and convert to standardized format - */ - private handleApiError(error: AxiosError): Error { - const status = error.response?.status || 500; - const message = error.response?.data?.message || error.message || 'Unknown error'; - const errorMessage = Array.isArray(message) ? message.join(', ') : message; - - return new Error(`GHL API Error (${status}): ${errorMessage}`); - } - - /** - * Wrap API responses in standardized format - */ - private wrapResponse(data: T): GHLApiResponse { - return { - success: true, - data - }; - } - - /** - * Create custom headers for different API versions - */ - private getConversationHeaders() { - return { - 'Authorization': `Bearer ${this.config.accessToken}`, - 'Version': '2021-04-15', // Conversations API uses different version - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }; - } - - /** - * CONTACTS API METHODS - */ - - /** - * Create a new contact - * POST /contacts/ - */ - async createContact(contactData: GHLCreateContactRequest): Promise> { - try { - // Ensure locationId is set - const payload = { - ...contactData, - locationId: contactData.locationId || this.config.locationId - }; - - const response: AxiosResponse<{ contact: GHLContact }> = await this.axiosInstance.post( - '/contacts/', - payload - ); - - return this.wrapResponse(response.data.contact); - } catch (error) { - throw error; - } - } - - /** - * Get contact by ID - * GET /contacts/{contactId} - */ - async getContact(contactId: string): Promise> { - try { - const response: AxiosResponse<{ contact: GHLContact }> = await this.axiosInstance.get( - `/contacts/${contactId}` - ); - - return this.wrapResponse(response.data.contact); - } catch (error) { - throw error; - } - } - - /** - * Update existing contact - * PUT /contacts/{contactId} - */ - async updateContact(contactId: string, updates: Partial): Promise> { - try { - const response: AxiosResponse<{ contact: GHLContact; succeded: boolean }> = await this.axiosInstance.put( - `/contacts/${contactId}`, - updates - ); - - return this.wrapResponse(response.data.contact); - } catch (error) { - throw error; - } - } - - /** - * Delete contact - * DELETE /contacts/{contactId} - */ - async deleteContact(contactId: string): Promise> { - try { - const response: AxiosResponse<{ succeded: boolean }> = await this.axiosInstance.delete( - `/contacts/${contactId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Search contacts with advanced filters - * POST /contacts/search - */ - async searchContacts(searchParams: GHLSearchContactsRequest): Promise> { - try { - // Build minimal request body with only required/supported parameters - // Start with just locationId and pageLimit as per API requirements - const payload: any = { - locationId: searchParams.locationId || this.config.locationId, - pageLimit: searchParams.limit || 25 - }; - - // Only add optional parameters if they have valid values - if (searchParams.query && searchParams.query.trim()) { - payload.query = searchParams.query.trim(); - } - - if (searchParams.startAfterId && searchParams.startAfterId.trim()) { - payload.startAfterId = searchParams.startAfterId.trim(); - } - - if (searchParams.startAfter && typeof searchParams.startAfter === 'number') { - payload.startAfter = searchParams.startAfter; - } - - // Only add filters if we have valid filter values - if (searchParams.filters) { - const filters: any = {}; - let hasFilters = false; - - if (searchParams.filters.email && typeof searchParams.filters.email === 'string' && searchParams.filters.email.trim()) { - filters.email = searchParams.filters.email.trim(); - hasFilters = true; - } - - if (searchParams.filters.phone && typeof searchParams.filters.phone === 'string' && searchParams.filters.phone.trim()) { - filters.phone = searchParams.filters.phone.trim(); - hasFilters = true; - } - - if (searchParams.filters.tags && Array.isArray(searchParams.filters.tags) && searchParams.filters.tags.length > 0) { - filters.tags = searchParams.filters.tags; - hasFilters = true; - } - - if (searchParams.filters.dateAdded && typeof searchParams.filters.dateAdded === 'object') { - filters.dateAdded = searchParams.filters.dateAdded; - hasFilters = true; - } - - // Only add filters object if we have actual filters - if (hasFilters) { - payload.filters = filters; - } - } - - process.stderr.write(`[GHL API] Search contacts payload: ${JSON.stringify(payload, null, 2)}\n`); - - const response: AxiosResponse = await this.axiosInstance.post( - '/contacts/search', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - const axiosError = error as AxiosError; - process.stderr.write(`[GHL API] Search contacts error: ${JSON.stringify({ - status: axiosError.response?.status, - statusText: axiosError.response?.statusText, - data: axiosError.response?.data, - message: axiosError.message - }, null, 2)}\n`); - - const handledError = this.handleApiError(axiosError); - return { - success: false, - error: { - message: handledError.message, - statusCode: axiosError.response?.status || 500, - details: axiosError.response?.data - } - }; - } - } - - /** - * Get duplicate contact by email or phone - * GET /contacts/search/duplicate - */ - async getDuplicateContact(email?: string, phone?: string): Promise> { - try { - const params: any = { - locationId: this.config.locationId - }; - - if (email) params.email = encodeURIComponent(email); - if (phone) params.number = encodeURIComponent(phone); - - const response: AxiosResponse<{ contact?: GHLContact }> = await this.axiosInstance.get( - '/contacts/search/duplicate', - { params } - ); - - return this.wrapResponse(response.data.contact || null); - } catch (error) { - throw error; - } - } - - /** - * Add tags to contact - * POST /contacts/{contactId}/tags - */ - async addContactTags(contactId: string, tags: string[]): Promise> { - try { - const payload: GHLContactTagsRequest = { tags }; - - const response: AxiosResponse = await this.axiosInstance.post( - `/contacts/${contactId}/tags`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Remove tags from contact - * DELETE /contacts/{contactId}/tags - */ - async removeContactTags(contactId: string, tags: string[]): Promise> { - try { - const payload: GHLContactTagsRequest = { tags }; - - const response: AxiosResponse = await this.axiosInstance.delete( - `/contacts/${contactId}/tags`, - { data: payload } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * CONVERSATIONS API METHODS - */ - - /** - * Search conversations with filters - * GET /conversations/search - */ - async searchConversations(searchParams: GHLSearchConversationsRequest): Promise> { - try { - // Ensure locationId is set - const params = { - ...searchParams, - locationId: searchParams.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/conversations/search', - { - params, - headers: this.getConversationHeaders() - } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get conversation by ID - * GET /conversations/{conversationId} - */ - async getConversation(conversationId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.get( - `/conversations/${conversationId}`, - { headers: this.getConversationHeaders() } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Create a new conversation - * POST /conversations/ - */ - async createConversation(conversationData: GHLCreateConversationRequest): Promise> { - try { - // Ensure locationId is set - const payload = { - ...conversationData, - locationId: conversationData.locationId || this.config.locationId - }; - - const response: AxiosResponse<{ success: boolean; conversation: GHLCreateConversationResponse }> = await this.axiosInstance.post( - '/conversations/', - payload, - { headers: this.getConversationHeaders() } - ); - - return this.wrapResponse(response.data.conversation); - } catch (error) { - throw error; - } - } - - /** - * Update conversation - * PUT /conversations/{conversationId} - */ - async updateConversation(conversationId: string, updates: GHLUpdateConversationRequest): Promise> { - try { - // Ensure locationId is set - const payload = { - ...updates, - locationId: updates.locationId || this.config.locationId - }; - - const response: AxiosResponse<{ success: boolean; conversation: GHLConversation }> = await this.axiosInstance.put( - `/conversations/${conversationId}`, - payload, - { headers: this.getConversationHeaders() } - ); - - return this.wrapResponse(response.data.conversation); - } catch (error) { - throw error; - } - } - - /** - * Delete conversation - * DELETE /conversations/{conversationId} - */ - async deleteConversation(conversationId: string): Promise> { - try { - const response: AxiosResponse<{ success: boolean }> = await this.axiosInstance.delete( - `/conversations/${conversationId}`, - { headers: this.getConversationHeaders() } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get messages from a conversation - * GET /conversations/{conversationId}/messages - */ - async getConversationMessages( - conversationId: string, - options?: { - lastMessageId?: string; - limit?: number; - type?: string; - } - ): Promise> { - try { - const params: any = {}; - if (options?.lastMessageId) params.lastMessageId = options.lastMessageId; - if (options?.limit) params.limit = options.limit; - if (options?.type) params.type = options.type; - - const response: AxiosResponse = await this.axiosInstance.get( - `/conversations/${conversationId}/messages`, - { - params, - headers: this.getConversationHeaders() - } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get message by ID - * GET /conversations/messages/{id} - */ - async getMessage(messageId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.get( - `/conversations/messages/${messageId}`, - { headers: this.getConversationHeaders() } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Send a new message (SMS, Email, etc.) - * POST /conversations/messages - */ - async sendMessage(messageData: GHLSendMessageRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.post( - '/conversations/messages', - messageData, - { headers: this.getConversationHeaders() } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Send SMS message to a contact - * Convenience method for sending SMS - */ - async sendSMS(contactId: string, message: string, fromNumber?: string): Promise> { - try { - const messageData: GHLSendMessageRequest = { - type: 'SMS', - contactId, - message, - fromNumber - }; - - return await this.sendMessage(messageData); - } catch (error) { - throw error; - } - } - - /** - * Send Email message to a contact - * Convenience method for sending Email - */ - async sendEmail( - contactId: string, - subject: string, - message?: string, - html?: string, - options?: { - emailFrom?: string; - emailTo?: string; - emailCc?: string[]; - emailBcc?: string[]; - attachments?: string[]; - } - ): Promise> { - try { - const messageData: GHLSendMessageRequest = { - type: 'Email', - contactId, - subject, - message, - html, - ...options - }; - - return await this.sendMessage(messageData); - } catch (error) { - throw error; - } - } - - /** - * BLOG API METHODS - */ - - /** - * Get all blog sites for a location - * GET /blogs/site/all - */ - async getBlogSites(params: GHLGetBlogSitesRequest): Promise> { - try { - // Ensure locationId is set - const queryParams = { - locationId: params.locationId || this.config.locationId, - skip: params.skip, - limit: params.limit, - ...(params.searchTerm && { searchTerm: params.searchTerm }) - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/blogs/site/all', - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get blog posts for a specific blog - * GET /blogs/posts/all - */ - async getBlogPosts(params: GHLGetBlogPostsRequest): Promise> { - try { - // Ensure locationId is set - const queryParams = { - locationId: params.locationId || this.config.locationId, - blogId: params.blogId, - limit: params.limit, - offset: params.offset, - ...(params.searchTerm && { searchTerm: params.searchTerm }), - ...(params.status && { status: params.status }) - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/blogs/posts/all', - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Create a new blog post - * POST /blogs/posts - */ - async createBlogPost(postData: GHLCreateBlogPostRequest): Promise> { - try { - // Ensure locationId is set - const payload = { - ...postData, - locationId: postData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/blogs/posts', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update an existing blog post - * PUT /blogs/posts/{postId} - */ - async updateBlogPost(postId: string, postData: GHLUpdateBlogPostRequest): Promise> { - try { - // Ensure locationId is set - const payload = { - ...postData, - locationId: postData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.put( - `/blogs/posts/${postId}`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get all blog authors for a location - * GET /blogs/authors - */ - async getBlogAuthors(params: GHLGetBlogAuthorsRequest): Promise> { - try { - // Ensure locationId is set - const queryParams = { - locationId: params.locationId || this.config.locationId, - limit: params.limit, - offset: params.offset - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/blogs/authors', - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get all blog categories for a location - * GET /blogs/categories - */ - async getBlogCategories(params: GHLGetBlogCategoriesRequest): Promise> { - try { - // Ensure locationId is set - const queryParams = { - locationId: params.locationId || this.config.locationId, - limit: params.limit, - offset: params.offset - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/blogs/categories', - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Check if a URL slug exists (for validation before creating/updating posts) - * GET /blogs/posts/url-slug-exists - */ - async checkUrlSlugExists(params: GHLCheckUrlSlugRequest): Promise> { - try { - // Ensure locationId is set - const queryParams = { - locationId: params.locationId || this.config.locationId, - urlSlug: params.urlSlug, - ...(params.postId && { postId: params.postId }) - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/blogs/posts/url-slug-exists', - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * TASKS API METHODS - */ - - /** - * Get all tasks for a contact - * GET /contacts/{contactId}/tasks - */ - async getContactTasks(contactId: string): Promise> { - try { - const response: AxiosResponse<{ tasks: GHLTask[] }> = await this.axiosInstance.get( - `/contacts/${contactId}/tasks` - ); - - return this.wrapResponse(response.data.tasks); - } catch (error) { - throw error; - } - } - - /** - * Create task for contact - * POST /contacts/{contactId}/tasks - */ - async createContactTask(contactId: string, taskData: Omit): Promise> { - try { - const response: AxiosResponse<{ task: GHLTask }> = await this.axiosInstance.post( - `/contacts/${contactId}/tasks`, - taskData - ); - - return this.wrapResponse(response.data.task); - } catch (error) { - throw error; - } - } - - /** - * NOTES API METHODS - */ - - /** - * Get all notes for a contact - * GET /contacts/{contactId}/notes - */ - async getContactNotes(contactId: string): Promise> { - try { - const response: AxiosResponse<{ notes: GHLNote[] }> = await this.axiosInstance.get( - `/contacts/${contactId}/notes` - ); - - return this.wrapResponse(response.data.notes); - } catch (error) { - throw error; - } - } - - /** - * Create note for contact - * POST /contacts/{contactId}/notes - */ - async createContactNote(contactId: string, noteData: Omit): Promise> { - try { - const response: AxiosResponse<{ note: GHLNote }> = await this.axiosInstance.post( - `/contacts/${contactId}/notes`, - noteData - ); - - return this.wrapResponse(response.data.note); - } catch (error) { - throw error; - } - } - - /** - * ADDITIONAL CONTACT API METHODS - */ - - /** - * Get a specific task for a contact - * GET /contacts/{contactId}/tasks/{taskId} - */ - async getContactTask(contactId: string, taskId: string): Promise> { - try { - const response: AxiosResponse<{ task: GHLTask }> = await this.axiosInstance.get( - `/contacts/${contactId}/tasks/${taskId}` - ); - - return this.wrapResponse(response.data.task); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update a task for a contact - * PUT /contacts/{contactId}/tasks/{taskId} - */ - async updateContactTask(contactId: string, taskId: string, updates: Partial): Promise> { - try { - const response: AxiosResponse<{ task: GHLTask }> = await this.axiosInstance.put( - `/contacts/${contactId}/tasks/${taskId}`, - updates - ); - - return this.wrapResponse(response.data.task); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete a task for a contact - * DELETE /contacts/{contactId}/tasks/{taskId} - */ - async deleteContactTask(contactId: string, taskId: string): Promise> { - try { - const response: AxiosResponse<{ succeded: boolean }> = await this.axiosInstance.delete( - `/contacts/${contactId}/tasks/${taskId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update task completion status - * PUT /contacts/{contactId}/tasks/{taskId}/completed - */ - async updateTaskCompletion(contactId: string, taskId: string, completed: boolean): Promise> { - try { - const response: AxiosResponse<{ task: GHLTask }> = await this.axiosInstance.put( - `/contacts/${contactId}/tasks/${taskId}/completed`, - { completed } - ); - - return this.wrapResponse(response.data.task); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get a specific note for a contact - * GET /contacts/{contactId}/notes/{noteId} - */ - async getContactNote(contactId: string, noteId: string): Promise> { - try { - const response: AxiosResponse<{ note: GHLNote }> = await this.axiosInstance.get( - `/contacts/${contactId}/notes/${noteId}` - ); - - return this.wrapResponse(response.data.note); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update a note for a contact - * PUT /contacts/{contactId}/notes/{noteId} - */ - async updateContactNote(contactId: string, noteId: string, updates: Partial): Promise> { - try { - const response: AxiosResponse<{ note: GHLNote }> = await this.axiosInstance.put( - `/contacts/${contactId}/notes/${noteId}`, - updates - ); - - return this.wrapResponse(response.data.note); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete a note for a contact - * DELETE /contacts/{contactId}/notes/{noteId} - */ - async deleteContactNote(contactId: string, noteId: string): Promise> { - try { - const response: AxiosResponse<{ succeded: boolean }> = await this.axiosInstance.delete( - `/contacts/${contactId}/notes/${noteId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Upsert contact (create or update based on email/phone) - * POST /contacts/upsert - */ - async upsertContact(contactData: Partial): Promise> { - try { - const payload = { - ...contactData, - locationId: contactData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/contacts/upsert', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get contacts by business ID - * GET /contacts/business/{businessId} - */ - async getContactsByBusiness(businessId: string, params: { limit?: number; skip?: number; query?: string } = {}): Promise> { - try { - const queryParams = { - limit: params.limit || 25, - skip: params.skip || 0, - ...(params.query && { query: params.query }) - }; - - const response: AxiosResponse = await this.axiosInstance.get( - `/contacts/business/${businessId}`, - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get contact appointments - * GET /contacts/{contactId}/appointments - */ - async getContactAppointments(contactId: string): Promise> { - try { - const response: AxiosResponse<{ events: GHLAppointment[] }> = await this.axiosInstance.get( - `/contacts/${contactId}/appointments` - ); - - return this.wrapResponse(response.data.events); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Bulk update contact tags - * POST /contacts/tags/bulk - */ - async bulkUpdateContactTags(contactIds: string[], tags: string[], operation: 'add' | 'remove', removeAllTags?: boolean): Promise> { - try { - const payload = { - ids: contactIds, - tags, - operation, - ...(removeAllTags !== undefined && { removeAllTags }) - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/contacts/tags/bulk', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Bulk update contact business - * POST /contacts/business/bulk - */ - async bulkUpdateContactBusiness(contactIds: string[], businessId?: string): Promise> { - try { - const payload = { - ids: contactIds, - businessId: businessId || null - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/contacts/business/bulk', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Add contact followers - * POST /contacts/{contactId}/followers - */ - async addContactFollowers(contactId: string, followers: string[]): Promise> { - try { - const payload = { followers }; - - const response: AxiosResponse = await this.axiosInstance.post( - `/contacts/${contactId}/followers`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Remove contact followers - * DELETE /contacts/{contactId}/followers - */ - async removeContactFollowers(contactId: string, followers: string[]): Promise> { - try { - const payload = { followers }; - - const response: AxiosResponse = await this.axiosInstance.delete( - `/contacts/${contactId}/followers`, - { data: payload } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Add contact to campaign - * POST /contacts/{contactId}/campaigns/{campaignId} - */ - async addContactToCampaign(contactId: string, campaignId: string): Promise> { - try { - const response: AxiosResponse<{ succeded: boolean }> = await this.axiosInstance.post( - `/contacts/${contactId}/campaigns/${campaignId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Remove contact from campaign - * DELETE /contacts/{contactId}/campaigns/{campaignId} - */ - async removeContactFromCampaign(contactId: string, campaignId: string): Promise> { - try { - const response: AxiosResponse<{ succeded: boolean }> = await this.axiosInstance.delete( - `/contacts/${contactId}/campaigns/${campaignId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Remove contact from all campaigns - * DELETE /contacts/{contactId}/campaigns - */ - async removeContactFromAllCampaigns(contactId: string): Promise> { - try { - const response: AxiosResponse<{ succeded: boolean }> = await this.axiosInstance.delete( - `/contacts/${contactId}/campaigns` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Add contact to workflow - * POST /contacts/{contactId}/workflow/{workflowId} - */ - async addContactToWorkflow(contactId: string, workflowId: string, eventStartTime?: string): Promise> { - try { - const payload = eventStartTime ? { eventStartTime } : {}; - - const response: AxiosResponse<{ succeded: boolean }> = await this.axiosInstance.post( - `/contacts/${contactId}/workflow/${workflowId}`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Remove contact from workflow - * DELETE /contacts/{contactId}/workflow/{workflowId} - */ - async removeContactFromWorkflow(contactId: string, workflowId: string, eventStartTime?: string): Promise> { - try { - const payload = eventStartTime ? { eventStartTime } : {}; - - const response: AxiosResponse<{ succeded: boolean }> = await this.axiosInstance.delete( - `/contacts/${contactId}/workflow/${workflowId}`, - { data: payload } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * UTILITY METHODS - */ - - /** - * Test API connection and authentication - */ - async testConnection(): Promise> { - try { - // Test with a simple GET request to check API connectivity - const response: AxiosResponse = await this.axiosInstance.get('/locations/' + this.config.locationId); - - return this.wrapResponse({ - status: 'connected', - locationId: this.config.locationId - }); - } catch (error) { - throw new Error(`GHL API connection test failed: ${error}`); - } - } - - /** - * Update access token - */ - updateAccessToken(newToken: string): void { - this.config.accessToken = newToken; - this.axiosInstance.defaults.headers['Authorization'] = `Bearer ${newToken}`; - process.stderr.write('[GHL API] Access token updated\n'); - } - - /** - * Get current configuration - */ - getConfig(): Readonly { - return { ...this.config }; - } - - /** - * Generic request method for new endpoints - * Used by new tool modules that don't have specific client methods yet - */ - async makeRequest(method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', path: string, body?: Record): Promise> { - try { - let response; - switch (method) { - case 'GET': - response = await this.axiosInstance.get(path); - break; - case 'POST': - response = await this.axiosInstance.post(path, body); - break; - case 'PUT': - response = await this.axiosInstance.put(path, body); - break; - case 'PATCH': - response = await this.axiosInstance.patch(path, body); - break; - case 'DELETE': - response = await this.axiosInstance.delete(path); - break; - } - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * OPPORTUNITIES API METHODS - */ - - /** - * Search opportunities with advanced filters - * GET /opportunities/search - */ - async searchOpportunities(searchParams: GHLSearchOpportunitiesRequest): Promise> { - try { - // Build query parameters with exact API naming (underscores) - const params: any = { - location_id: searchParams.location_id || this.config.locationId - }; - - // Add optional search parameters only if they have values - if (searchParams.q && searchParams.q.trim()) { - params.q = searchParams.q.trim(); - } - - if (searchParams.pipeline_id) { - params.pipeline_id = searchParams.pipeline_id; - } - - if (searchParams.pipeline_stage_id) { - params.pipeline_stage_id = searchParams.pipeline_stage_id; - } - - if (searchParams.contact_id) { - params.contact_id = searchParams.contact_id; - } - - if (searchParams.status) { - params.status = searchParams.status; - } - - if (searchParams.assigned_to) { - params.assigned_to = searchParams.assigned_to; - } - - if (searchParams.campaignId) { - params.campaignId = searchParams.campaignId; - } - - if (searchParams.id) { - params.id = searchParams.id; - } - - if (searchParams.order) { - params.order = searchParams.order; - } - - if (searchParams.endDate) { - params.endDate = searchParams.endDate; - } - - if (searchParams.startAfter) { - params.startAfter = searchParams.startAfter; - } - - if (searchParams.startAfterId) { - params.startAfterId = searchParams.startAfterId; - } - - if (searchParams.date) { - params.date = searchParams.date; - } - - if (searchParams.country) { - params.country = searchParams.country; - } - - if (searchParams.page) { - params.page = searchParams.page; - } - - if (searchParams.limit) { - params.limit = searchParams.limit; - } - - if (searchParams.getTasks !== undefined) { - params.getTasks = searchParams.getTasks; - } - - if (searchParams.getNotes !== undefined) { - params.getNotes = searchParams.getNotes; - } - - if (searchParams.getCalendarEvents !== undefined) { - params.getCalendarEvents = searchParams.getCalendarEvents; - } - - process.stderr.write(`[GHL API] Search opportunities params: ${JSON.stringify(params, null, 2)}\n`); - - const response: AxiosResponse = await this.axiosInstance.get( - '/opportunities/search', - { params } - ); - - return this.wrapResponse(response.data); - } catch (error) { - const axiosError = error as AxiosError; - process.stderr.write(`[GHL API] Search opportunities error: ${JSON.stringify({ - status: axiosError.response?.status, - statusText: axiosError.response?.statusText, - data: axiosError.response?.data, - message: axiosError.message - }, null, 2)}\n`); - - throw this.handleApiError(axiosError); - } - } - - /** - * Get all pipelines for a location - * GET /opportunities/pipelines - */ - async getPipelines(locationId?: string): Promise> { - try { - const params = { - locationId: locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/opportunities/pipelines', - { params } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get opportunity by ID - * GET /opportunities/{id} - */ - async getOpportunity(opportunityId: string): Promise> { - try { - const response: AxiosResponse<{ opportunity: GHLOpportunity }> = await this.axiosInstance.get( - `/opportunities/${opportunityId}` - ); - - return this.wrapResponse(response.data.opportunity); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Create a new opportunity - * POST /opportunities/ - */ - async createOpportunity(opportunityData: GHLCreateOpportunityRequest): Promise> { - try { - // Ensure locationId is set - const payload = { - ...opportunityData, - locationId: opportunityData.locationId || this.config.locationId - }; - - const response: AxiosResponse<{ opportunity: GHLOpportunity }> = await this.axiosInstance.post( - '/opportunities/', - payload - ); - - return this.wrapResponse(response.data.opportunity); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update existing opportunity - * PUT /opportunities/{id} - */ - async updateOpportunity(opportunityId: string, updates: GHLUpdateOpportunityRequest): Promise> { - try { - const response: AxiosResponse<{ opportunity: GHLOpportunity }> = await this.axiosInstance.put( - `/opportunities/${opportunityId}`, - updates - ); - - return this.wrapResponse(response.data.opportunity); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update opportunity status - * PUT /opportunities/{id}/status - */ - async updateOpportunityStatus(opportunityId: string, status: GHLOpportunityStatus): Promise> { - try { - const payload: GHLUpdateOpportunityStatusRequest = { status }; - - const response: AxiosResponse<{ succeded: boolean }> = await this.axiosInstance.put( - `/opportunities/${opportunityId}/status`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Upsert opportunity (create or update) - * POST /opportunities/upsert - */ - async upsertOpportunity(opportunityData: GHLUpsertOpportunityRequest): Promise> { - try { - // Ensure locationId is set - const payload = { - ...opportunityData, - locationId: opportunityData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/opportunities/upsert', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete opportunity - * DELETE /opportunities/{id} - */ - async deleteOpportunity(opportunityId: string): Promise> { - try { - const response: AxiosResponse<{ succeded: boolean }> = await this.axiosInstance.delete( - `/opportunities/${opportunityId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Add followers to opportunity - * POST /opportunities/{id}/followers - */ - async addOpportunityFollowers(opportunityId: string, followers: string[]): Promise> { - try { - const payload = { followers }; - - const response: AxiosResponse = await this.axiosInstance.post( - `/opportunities/${opportunityId}/followers`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Remove followers from opportunity - * DELETE /opportunities/{id}/followers - */ - async removeOpportunityFollowers(opportunityId: string, followers: string[]): Promise> { - try { - const payload = { followers }; - - const response: AxiosResponse = await this.axiosInstance.delete( - `/opportunities/${opportunityId}/followers`, - { data: payload } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * CALENDAR & APPOINTMENTS API METHODS - */ - - /** - * Get all calendar groups in a location - * GET /calendars/groups - */ - async getCalendarGroups(locationId?: string): Promise> { - try { - const params = { - locationId: locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/calendars/groups', - { params } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Create a new calendar group - * POST /calendars/groups - */ - async createCalendarGroup(groupData: GHLCreateCalendarGroupRequest): Promise> { - try { - const payload = { - ...groupData, - locationId: groupData.locationId || this.config.locationId - }; - - const response: AxiosResponse<{ group: GHLCalendarGroup }> = await this.axiosInstance.post( - '/calendars/groups', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get all calendars in a location - * GET /calendars/ - */ - async getCalendars(params?: { locationId?: string; groupId?: string; showDrafted?: boolean }): Promise> { - try { - const queryParams = { - locationId: params?.locationId || this.config.locationId, - ...(params?.groupId && { groupId: params.groupId }), - ...(params?.showDrafted !== undefined && { showDrafted: params.showDrafted }) - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/calendars/', - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Create a new calendar - * POST /calendars/ - */ - async createCalendar(calendarData: GHLCreateCalendarRequest): Promise> { - try { - const payload = { - ...calendarData, - locationId: calendarData.locationId || this.config.locationId - }; - - const response: AxiosResponse<{ calendar: GHLCalendar }> = await this.axiosInstance.post( - '/calendars/', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get calendar by ID - * GET /calendars/{calendarId} - */ - async getCalendar(calendarId: string): Promise> { - try { - const response: AxiosResponse<{ calendar: GHLCalendar }> = await this.axiosInstance.get( - `/calendars/${calendarId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update calendar by ID - * PUT /calendars/{calendarId} - */ - async updateCalendar(calendarId: string, updates: GHLUpdateCalendarRequest): Promise> { - try { - const response: AxiosResponse<{ calendar: GHLCalendar }> = await this.axiosInstance.put( - `/calendars/${calendarId}`, - updates - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete calendar by ID - * DELETE /calendars/{calendarId} - */ - async deleteCalendar(calendarId: string): Promise> { - try { - const response: AxiosResponse<{ success: boolean }> = await this.axiosInstance.delete( - `/calendars/${calendarId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get calendar events/appointments - * GET /calendars/events - */ - async getCalendarEvents(eventParams: GHLGetCalendarEventsRequest): Promise> { - try { - const params = { - locationId: eventParams.locationId || this.config.locationId, - startTime: eventParams.startTime, - endTime: eventParams.endTime, - ...(eventParams.userId && { userId: eventParams.userId }), - ...(eventParams.calendarId && { calendarId: eventParams.calendarId }), - ...(eventParams.groupId && { groupId: eventParams.groupId }) - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/calendars/events', - { params } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get blocked slots - * GET /calendars/blocked-slots - */ - async getBlockedSlots(eventParams: GHLGetCalendarEventsRequest): Promise> { - try { - const params = { - locationId: eventParams.locationId || this.config.locationId, - startTime: eventParams.startTime, - endTime: eventParams.endTime, - ...(eventParams.userId && { userId: eventParams.userId }), - ...(eventParams.calendarId && { calendarId: eventParams.calendarId }), - ...(eventParams.groupId && { groupId: eventParams.groupId }) - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/calendars/blocked-slots', - { params } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get free slots for a calendar - * GET /calendars/{calendarId}/free-slots - */ - async getFreeSlots(slotParams: GHLGetFreeSlotsRequest): Promise> { - try { - const params = { - startDate: slotParams.startDate, - endDate: slotParams.endDate, - ...(slotParams.timezone && { timezone: slotParams.timezone }), - ...(slotParams.userId && { userId: slotParams.userId }), - ...(slotParams.userIds && { userIds: slotParams.userIds }), - ...(slotParams.enableLookBusy !== undefined && { enableLookBusy: slotParams.enableLookBusy }) - }; - - const response: AxiosResponse = await this.axiosInstance.get( - `/calendars/${slotParams.calendarId}/free-slots`, - { params } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Create a new appointment - * POST /calendars/events/appointments - */ - async createAppointment(appointmentData: GHLCreateAppointmentRequest): Promise> { - try { - const payload = { - ...appointmentData, - locationId: appointmentData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/calendars/events/appointments', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get appointment by ID - * GET /calendars/events/appointments/{eventId} - */ - async getAppointment(appointmentId: string): Promise> { - try { - const response: AxiosResponse<{ event: GHLCalendarEvent }> = await this.axiosInstance.get( - `/calendars/events/appointments/${appointmentId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update appointment by ID - * PUT /calendars/events/appointments/{eventId} - */ - async updateAppointment(appointmentId: string, updates: GHLUpdateAppointmentRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.put( - `/calendars/events/appointments/${appointmentId}`, - updates - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete appointment by ID - * DELETE /calendars/events/appointments/{eventId} - */ - async deleteAppointment(appointmentId: string): Promise> { - try { - const response: AxiosResponse<{ succeeded: boolean }> = await this.axiosInstance.delete( - `/calendars/events/appointments/${appointmentId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - - - /** - * Update block slot by ID - * PUT /calendars/events/block-slots/{eventId} - */ - async updateBlockSlot(blockSlotId: string, updates: GHLUpdateBlockSlotRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.put( - `/calendars/events/block-slots/${blockSlotId}`, - updates - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * EMAIL API METHODS - */ - - async getEmailCampaigns(params: MCPGetEmailCampaignsParams): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.get('/emails/schedule', { - params: { - locationId: this.config.locationId, - ...params - } - }); - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - async createEmailTemplate(params: MCPCreateEmailTemplateParams): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.post('/emails/builder', { - locationId: this.config.locationId, - type: 'html', - ...params - }); - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - async getEmailTemplates(params: MCPGetEmailTemplatesParams): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.get('/emails/builder', { - params: { - locationId: this.config.locationId, - ...params - } - }); - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - async updateEmailTemplate(params: MCPUpdateEmailTemplateParams): Promise> { - try { - const { templateId, ...data } = params; - const response: AxiosResponse = await this.axiosInstance.post('/emails/builder/data', { - locationId: this.config.locationId, - templateId, - ...data, - editorType: 'html' - }); - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - async deleteEmailTemplate(params: MCPDeleteEmailTemplateParams): Promise> { - try { - const { templateId } = params; - const response: AxiosResponse = await this.axiosInstance.delete(`/emails/builder/${this.config.locationId}/${templateId}`); - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * LOCATION API METHODS - */ - - /** - * Search locations/sub-accounts - * GET /locations/search - */ - async searchLocations(params: { - companyId?: string; - skip?: number; - limit?: number; - order?: 'asc' | 'desc'; - email?: string; - } = {}): Promise> { - try { - const queryParams = { - skip: params.skip || 0, - limit: params.limit || 10, - order: params.order || 'asc', - ...(params.companyId && { companyId: params.companyId }), - ...(params.email && { email: params.email }) - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/locations/search', - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get location by ID - * GET /locations/{locationId} - */ - async getLocationById(locationId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.get( - `/locations/${locationId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Create a new location/sub-account - * POST /locations/ - */ - async createLocation(locationData: GHLCreateLocationRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.post( - '/locations/', - locationData - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update location/sub-account - * PUT /locations/{locationId} - */ - async updateLocation(locationId: string, updates: GHLUpdateLocationRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.put( - `/locations/${locationId}`, - updates - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete location/sub-account - * DELETE /locations/{locationId} - */ - async deleteLocation(locationId: string, deleteTwilioAccount: boolean): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.delete( - `/locations/${locationId}`, - { - params: { deleteTwilioAccount } - } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * LOCATION TAGS API METHODS - */ - - /** - * Get location tags - * GET /locations/{locationId}/tags - */ - async getLocationTags(locationId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.get( - `/locations/${locationId}/tags` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Create location tag - * POST /locations/{locationId}/tags - */ - async createLocationTag(locationId: string, tagData: GHLLocationTagRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.post( - `/locations/${locationId}/tags`, - tagData - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get location tag by ID - * GET /locations/{locationId}/tags/{tagId} - */ - async getLocationTag(locationId: string, tagId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.get( - `/locations/${locationId}/tags/${tagId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update location tag - * PUT /locations/{locationId}/tags/{tagId} - */ - async updateLocationTag(locationId: string, tagId: string, tagData: GHLLocationTagRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.put( - `/locations/${locationId}/tags/${tagId}`, - tagData - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete location tag - * DELETE /locations/{locationId}/tags/{tagId} - */ - async deleteLocationTag(locationId: string, tagId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.delete( - `/locations/${locationId}/tags/${tagId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * LOCATION TASKS API METHODS - */ - - /** - * Search location tasks - * POST /locations/{locationId}/tasks/search - */ - async searchLocationTasks(locationId: string, searchParams: GHLLocationTaskSearchRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.post( - `/locations/${locationId}/tasks/search`, - searchParams - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * CUSTOM FIELDS API METHODS - */ - - /** - * Get custom fields for location - * GET /locations/{locationId}/customFields - */ - async getLocationCustomFields(locationId: string, model?: 'contact' | 'opportunity' | 'all'): Promise> { - try { - const params: any = {}; - if (model) params.model = model; - - const response: AxiosResponse = await this.axiosInstance.get( - `/locations/${locationId}/customFields`, - { params } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Create custom field for location - * POST /locations/{locationId}/customFields - */ - async createLocationCustomField(locationId: string, fieldData: GHLCreateCustomFieldRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.post( - `/locations/${locationId}/customFields`, - fieldData - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get custom field by ID - * GET /locations/{locationId}/customFields/{id} - */ - async getLocationCustomField(locationId: string, customFieldId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.get( - `/locations/${locationId}/customFields/${customFieldId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update custom field - * PUT /locations/{locationId}/customFields/{id} - */ - async updateLocationCustomField(locationId: string, customFieldId: string, fieldData: GHLUpdateCustomFieldRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.put( - `/locations/${locationId}/customFields/${customFieldId}`, - fieldData - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete custom field - * DELETE /locations/{locationId}/customFields/{id} - */ - async deleteLocationCustomField(locationId: string, customFieldId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.delete( - `/locations/${locationId}/customFields/${customFieldId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Upload file to custom fields - * POST /locations/{locationId}/customFields/upload - */ - async uploadLocationCustomFieldFile(locationId: string, uploadData: GHLFileUploadRequest): Promise> { - try { - // Note: This endpoint expects multipart/form-data but we'll handle it as JSON for now - // In a real implementation, you'd use FormData for file uploads - const response: AxiosResponse = await this.axiosInstance.post( - `/locations/${locationId}/customFields/upload`, - uploadData, - { headers: { 'Content-Type': 'multipart/form-data' } } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * CUSTOM VALUES API METHODS - */ - - /** - * Get custom values for location - * GET /locations/{locationId}/customValues - */ - async getLocationCustomValues(locationId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.get( - `/locations/${locationId}/customValues` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Create custom value for location - * POST /locations/{locationId}/customValues - */ - async createLocationCustomValue(locationId: string, valueData: GHLCustomValueRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.post( - `/locations/${locationId}/customValues`, - valueData - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get custom value by ID - * GET /locations/{locationId}/customValues/{id} - */ - async getLocationCustomValue(locationId: string, customValueId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.get( - `/locations/${locationId}/customValues/${customValueId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update custom value - * PUT /locations/{locationId}/customValues/{id} - */ - async updateLocationCustomValue(locationId: string, customValueId: string, valueData: GHLCustomValueRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.put( - `/locations/${locationId}/customValues/${customValueId}`, - valueData - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete custom value - * DELETE /locations/{locationId}/customValues/{id} - */ - async deleteLocationCustomValue(locationId: string, customValueId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.delete( - `/locations/${locationId}/customValues/${customValueId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * TEMPLATES API METHODS - */ - - /** - * Get location templates (SMS/Email) - * GET /locations/{locationId}/templates - */ - async getLocationTemplates(locationId: string, params: { - originId: string; - deleted?: boolean; - skip?: number; - limit?: number; - type?: 'sms' | 'email' | 'whatsapp'; - }): Promise> { - try { - const queryParams = { - originId: params.originId, - deleted: params.deleted || false, - skip: params.skip || 0, - limit: params.limit || 25, - ...(params.type && { type: params.type }) - }; - - const response: AxiosResponse = await this.axiosInstance.get( - `/locations/${locationId}/templates`, - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete location template - * DELETE /locations/{locationId}/templates/{id} - */ - async deleteLocationTemplate(locationId: string, templateId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.delete( - `/locations/${locationId}/templates/${templateId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * TIMEZONES API METHODS - */ - - /** - * Get available timezones - * GET /locations/{locationId}/timezones - */ - async getTimezones(locationId?: string): Promise> { - try { - const endpoint = locationId ? `/locations/${locationId}/timezones` : '/locations/timezones'; - const response: AxiosResponse = await this.axiosInstance.get(endpoint); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * EMAIL ISV (VERIFICATION) API METHODS - */ - - /** - * Verify email address or contact - * POST /email/verify - */ - async verifyEmail(locationId: string, verificationData: GHLEmailVerificationRequest): Promise> { - try { - const params = { - locationId: locationId - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/email/verify', - verificationData, - { params } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * ADDITIONAL CONVERSATION/MESSAGE API METHODS - */ - - /** - * Get email message by ID - * GET /conversations/messages/email/{id} - */ - async getEmailMessage(emailMessageId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.get( - `/conversations/messages/email/${emailMessageId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Cancel scheduled email message - * DELETE /conversations/messages/email/{emailMessageId}/schedule - */ - async cancelScheduledEmail(emailMessageId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.delete( - `/conversations/messages/email/${emailMessageId}/schedule` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Add inbound message manually - * POST /conversations/messages/inbound - */ - async addInboundMessage(messageData: GHLProcessInboundMessageRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.post( - '/conversations/messages/inbound', - messageData, - { headers: this.getConversationHeaders() } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Add outbound call manually - * POST /conversations/messages/outbound - */ - async addOutboundCall(messageData: GHLProcessOutboundMessageRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.post( - '/conversations/messages/outbound', - messageData, - { headers: this.getConversationHeaders() } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Cancel scheduled message - * DELETE /conversations/messages/{messageId}/schedule - */ - async cancelScheduledMessage(messageId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.delete( - `/conversations/messages/${messageId}/schedule`, - { headers: this.getConversationHeaders() } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Upload file attachments for messages - * POST /conversations/messages/upload - */ - async uploadMessageAttachments(uploadData: GHLUploadFilesRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.post( - '/conversations/messages/upload', - uploadData, - { - headers: { - ...this.getConversationHeaders(), - 'Content-Type': 'multipart/form-data' - } - } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update message status - * PUT /conversations/messages/{messageId}/status - */ - async updateMessageStatus(messageId: string, statusData: GHLUpdateMessageStatusRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.put( - `/conversations/messages/${messageId}/status`, - statusData, - { headers: this.getConversationHeaders() } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get message recording - * GET /conversations/messages/{messageId}/locations/{locationId}/recording - */ - async getMessageRecording(messageId: string, locationId?: string): Promise> { - try { - const locId = locationId || this.config.locationId; - const response: AxiosResponse = await this.axiosInstance.get( - `/conversations/messages/${messageId}/locations/${locId}/recording`, - { - headers: this.getConversationHeaders(), - responseType: 'arraybuffer' - } - ); - - const recordingResponse: GHLMessageRecordingResponse = { - audioData: response.data, - contentType: response.headers['content-type'] || 'audio/x-wav', - contentDisposition: response.headers['content-disposition'] || 'attachment; filename=audio.wav' - }; - - return this.wrapResponse(recordingResponse); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get message transcription - * GET /conversations/locations/{locationId}/messages/{messageId}/transcription - */ - async getMessageTranscription(messageId: string, locationId?: string): Promise> { - try { - const locId = locationId || this.config.locationId; - const response: AxiosResponse = await this.axiosInstance.get( - `/conversations/locations/${locId}/messages/${messageId}/transcription`, - { headers: this.getConversationHeaders() } - ); - - return this.wrapResponse({ transcriptions: response.data }); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Download message transcription - * GET /conversations/locations/{locationId}/messages/{messageId}/transcription/download - */ - async downloadMessageTranscription(messageId: string, locationId?: string): Promise> { - try { - const locId = locationId || this.config.locationId; - const response: AxiosResponse = await this.axiosInstance.get( - `/conversations/locations/${locId}/messages/${messageId}/transcription/download`, - { - headers: this.getConversationHeaders(), - responseType: 'text' - } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Live chat typing indicator - * POST /conversations/providers/live-chat/typing - */ - async liveChatTyping(typingData: GHLLiveChatTypingRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.post( - '/conversations/providers/live-chat/typing', - typingData, - { headers: this.getConversationHeaders() } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - // ============================================================================ - // SOCIAL MEDIA POSTING API METHODS - // ============================================================================ - - // ===== POST MANAGEMENT ===== - - /** - * Search/List Social Media Posts - */ - async searchSocialPosts(searchData: GHLSearchPostsRequest): Promise> { - try { - const locationId = this.config.locationId; - const response: AxiosResponse = await this.axiosInstance.post( - `/social-media-posting/${locationId}/posts/list`, - searchData - ); - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Create Social Media Post - */ - async createSocialPost(postData: GHLCreatePostRequest): Promise> { - try { - const locationId = this.config.locationId; - const response: AxiosResponse = await this.axiosInstance.post( - `/social-media-posting/${locationId}/posts`, - postData - ); - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get Social Media Post by ID - */ - async getSocialPost(postId: string): Promise> { - try { - const locationId = this.config.locationId; - const response: AxiosResponse = await this.axiosInstance.get( - `/social-media-posting/${locationId}/posts/${postId}` - ); - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update Social Media Post - */ - async updateSocialPost(postId: string, updateData: GHLUpdatePostRequest): Promise> { - try { - const locationId = this.config.locationId; - const response: AxiosResponse = await this.axiosInstance.put( - `/social-media-posting/${locationId}/posts/${postId}`, - updateData - ); - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Delete Social Media Post - */ - async deleteSocialPost(postId: string): Promise> { - try { - const locationId = this.config.locationId; - const response: AxiosResponse = await this.axiosInstance.delete( - `/social-media-posting/${locationId}/posts/${postId}` - ); - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Bulk Delete Social Media Posts - */ - async bulkDeleteSocialPosts(deleteData: GHLBulkDeletePostsRequest): Promise> { - try { - const locationId = this.config.locationId; - const response: AxiosResponse = await this.axiosInstance.post( - `/social-media-posting/${locationId}/posts/bulk-delete`, - deleteData - ); - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - // ===== ACCOUNT MANAGEMENT ===== - - /** - * Get Social Media Accounts and Groups - */ - async getSocialAccounts(): Promise> { - try { - const locationId = this.config.locationId; - const response: AxiosResponse = await this.axiosInstance.get( - `/social-media-posting/${locationId}/accounts` - ); - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Delete Social Media Account - */ - async deleteSocialAccount(accountId: string, companyId?: string, userId?: string): Promise> { - try { - const locationId = this.config.locationId; - const params: any = {}; - if (companyId) params.companyId = companyId; - if (userId) params.userId = userId; - - const response: AxiosResponse = await this.axiosInstance.delete( - `/social-media-posting/${locationId}/accounts/${accountId}`, - { params } - ); - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - // ===== CSV OPERATIONS ===== - - /** - * Upload CSV for Social Media Posts - */ - async uploadSocialCSV(csvData: GHLUploadCSVRequest): Promise> { - try { - const locationId = this.config.locationId; - // Note: This would typically use FormData for file upload - const response: AxiosResponse = await this.axiosInstance.post( - `/social-media-posting/${locationId}/csv`, - csvData - ); - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get CSV Upload Status - */ - async getSocialCSVUploadStatus(skip?: number, limit?: number, includeUsers?: boolean, userId?: string): Promise> { - try { - const locationId = this.config.locationId; - const params: any = {}; - if (skip !== undefined) params.skip = skip.toString(); - if (limit !== undefined) params.limit = limit.toString(); - if (includeUsers !== undefined) params.includeUsers = includeUsers.toString(); - if (userId) params.userId = userId; - - const response: AxiosResponse = await this.axiosInstance.get( - `/social-media-posting/${locationId}/csv`, - { params } - ); - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Set Accounts for CSV Import - */ - async setSocialCSVAccounts(accountsData: GHLSetAccountsRequest): Promise> { - try { - const locationId = this.config.locationId; - const response: AxiosResponse = await this.axiosInstance.post( - `/social-media-posting/${locationId}/set-accounts`, - accountsData - ); - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get CSV Posts - */ - async getSocialCSVPosts(csvId: string, skip?: number, limit?: number): Promise> { - try { - const locationId = this.config.locationId; - const params: any = {}; - if (skip !== undefined) params.skip = skip.toString(); - if (limit !== undefined) params.limit = limit.toString(); - - const response: AxiosResponse = await this.axiosInstance.get( - `/social-media-posting/${locationId}/csv/${csvId}`, - { params } - ); - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Start CSV Finalization - */ - async finalizeSocialCSV(csvId: string, finalizeData: GHLCSVFinalizeRequest): Promise> { - try { - const locationId = this.config.locationId; - const response: AxiosResponse = await this.axiosInstance.patch( - `/social-media-posting/${locationId}/csv/${csvId}`, - finalizeData - ); - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Delete CSV Import - */ - async deleteSocialCSV(csvId: string): Promise> { - try { - const locationId = this.config.locationId; - const response: AxiosResponse = await this.axiosInstance.delete( - `/social-media-posting/${locationId}/csv/${csvId}` - ); - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Delete CSV Post - */ - async deleteSocialCSVPost(csvId: string, postId: string): Promise> { - try { - const locationId = this.config.locationId; - const response: AxiosResponse = await this.axiosInstance.delete( - `/social-media-posting/${locationId}/csv/${csvId}/post/${postId}` - ); - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - // ===== CATEGORIES & TAGS ===== - - /** - * Get Social Media Categories - */ - async getSocialCategories(searchText?: string, limit?: number, skip?: number): Promise> { - // TODO: Implement this method properly - throw new Error('Method not yet implemented'); - } - - // TODO: Implement remaining social media API methods - async getSocialCategory(categoryId: string): Promise> { - throw new Error('Method not yet implemented'); - } - - async getSocialTags(searchText?: string, limit?: number, skip?: number): Promise> { - throw new Error('Method not yet implemented'); - } - - async getSocialTagsByIds(tagData: GHLGetTagsByIdsRequest): Promise> { - throw new Error('Method not yet implemented'); - } - - async startSocialOAuth(platform: GHLSocialPlatform, userId: string, page?: string, reconnect?: boolean): Promise> { - throw new Error('Method not yet implemented'); - } - - async getGoogleBusinessLocations(accountId: string): Promise> { - throw new Error('Method not yet implemented'); - } - - async setGoogleBusinessLocations(accountId: string, locationData: GHLAttachGMBLocationRequest): Promise> { - throw new Error('Method not yet implemented'); - } - - async getFacebookPages(accountId: string): Promise> { - throw new Error('Method not yet implemented'); - } - - async attachFacebookPages(accountId: string, pageData: GHLAttachFBAccountRequest): Promise> { - throw new Error('Method not yet implemented'); - } - - async getInstagramAccounts(accountId: string): Promise> { - throw new Error('Method not yet implemented'); - } - - async attachInstagramAccounts(accountId: string, accountData: GHLAttachIGAccountRequest): Promise> { - throw new Error('Method not yet implemented'); - } - - async getLinkedInAccounts(accountId: string): Promise> { - throw new Error('Method not yet implemented'); - } - - async attachLinkedInAccounts(accountId: string, accountData: GHLAttachLinkedInAccountRequest): Promise> { - throw new Error('Method not yet implemented'); - } - - async getTwitterProfile(accountId: string): Promise> { - throw new Error('Method not yet implemented'); - } - - async attachTwitterProfile(accountId: string, profileData: GHLAttachTwitterAccountRequest): Promise> { - throw new Error('Method not yet implemented'); - } - - async getTikTokProfile(accountId: string): Promise> { - throw new Error('Method not yet implemented'); - } - - async attachTikTokProfile(accountId: string, profileData: GHLAttachTikTokAccountRequest): Promise> { - throw new Error('Method not yet implemented'); - } - - async getTikTokBusinessProfile(accountId: string): Promise> { - throw new Error('Method not yet implemented'); - } - - // ===== MISSING CALENDAR GROUPS MANAGEMENT METHODS ===== - - /** - * Validate calendar group slug - * GET /calendars/groups/slug/validate - */ - async validateCalendarGroupSlug(slug: string, locationId?: string): Promise> { - try { - const params = { - locationId: locationId || this.config.locationId, - slug - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/calendars/groups/slug/validate', - { params } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update calendar group by ID - * PUT /calendars/groups/{groupId} - */ - async updateCalendarGroup(groupId: string, updateData: GHLUpdateCalendarGroupRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.put( - `/calendars/groups/${groupId}`, - updateData - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete calendar group by ID - * DELETE /calendars/groups/{groupId} - */ - async deleteCalendarGroup(groupId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.delete( - `/calendars/groups/${groupId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Disable calendar group - * POST /calendars/groups/{groupId}/status - */ - async disableCalendarGroup(groupId: string, isActive: boolean): Promise> { - try { - const payload: GHLGroupStatusUpdateRequest = { isActive }; - - const response: AxiosResponse = await this.axiosInstance.post( - `/calendars/groups/${groupId}/status`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - // ===== APPOINTMENT NOTES METHODS ===== - - /** - * Get appointment notes - * GET /calendars/events/appointments/{appointmentId}/notes - */ - async getAppointmentNotes(appointmentId: string, limit = 10, offset = 0): Promise> { - try { - const params = { limit, offset }; - - const response: AxiosResponse = await this.axiosInstance.get( - `/calendars/events/appointments/${appointmentId}/notes`, - { params } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Create appointment note - * POST /calendars/events/appointments/{appointmentId}/notes - */ - async createAppointmentNote(appointmentId: string, noteData: GHLCreateAppointmentNoteRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.post( - `/calendars/events/appointments/${appointmentId}/notes`, - noteData - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update appointment note - * PUT /calendars/events/appointments/{appointmentId}/notes/{noteId} - */ - async updateAppointmentNote(appointmentId: string, noteId: string, updateData: GHLUpdateAppointmentNoteRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.put( - `/calendars/events/appointments/${appointmentId}/notes/${noteId}`, - updateData - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete appointment note - * DELETE /calendars/events/appointments/{appointmentId}/notes/{noteId} - */ - async deleteAppointmentNote(appointmentId: string, noteId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.delete( - `/calendars/events/appointments/${appointmentId}/notes/${noteId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - // ===== CALENDAR RESOURCES METHODS ===== - - /** - * Get calendar resources - * GET /calendars/resources/{resourceType} - */ - async getCalendarResources(resourceType: 'equipments' | 'rooms', limit = 20, skip = 0, locationId?: string): Promise> { - try { - const params = { - locationId: locationId || this.config.locationId, - limit, - skip - }; - - const response: AxiosResponse = await this.axiosInstance.get( - `/calendars/resources/${resourceType}`, - { params } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Create calendar resource - * POST /calendars/resources/{resourceType} - */ - async createCalendarResource(resourceType: 'equipments' | 'rooms', resourceData: GHLCreateCalendarResourceRequest): Promise> { - try { - const payload = { - ...resourceData, - locationId: resourceData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.post( - `/calendars/resources/${resourceType}`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get calendar resource by ID - * GET /calendars/resources/{resourceType}/{resourceId} - */ - async getCalendarResource(resourceType: 'equipments' | 'rooms', resourceId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.get( - `/calendars/resources/${resourceType}/${resourceId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update calendar resource - * PUT /calendars/resources/{resourceType}/{resourceId} - */ - async updateCalendarResource(resourceType: 'equipments' | 'rooms', resourceId: string, updateData: GHLUpdateCalendarResourceRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.put( - `/calendars/resources/${resourceType}/${resourceId}`, - updateData - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete calendar resource - * DELETE /calendars/resources/{resourceType}/{resourceId} - */ - async deleteCalendarResource(resourceType: 'equipments' | 'rooms', resourceId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.delete( - `/calendars/resources/${resourceType}/${resourceId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - // ===== CALENDAR NOTIFICATIONS METHODS ===== - - /** - * Get calendar notifications - * GET /calendars/{calendarId}/notifications - */ - async getCalendarNotifications(calendarId: string, queryParams?: GHLGetCalendarNotificationsRequest): Promise> { - try { - const params = { - ...queryParams - }; - - const response: AxiosResponse = await this.axiosInstance.get( - `/calendars/${calendarId}/notifications`, - { params } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Create calendar notifications - * POST /calendars/{calendarId}/notifications - */ - async createCalendarNotifications(calendarId: string, notifications: GHLCreateCalendarNotificationRequest[]): Promise> { - try { - const payload = { notifications }; - - const response: AxiosResponse = await this.axiosInstance.post( - `/calendars/${calendarId}/notifications`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get calendar notification by ID - * GET /calendars/{calendarId}/notifications/{notificationId} - */ - async getCalendarNotification(calendarId: string, notificationId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.get( - `/calendars/${calendarId}/notifications/${notificationId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update calendar notification - * PUT /calendars/{calendarId}/notifications/{notificationId} - */ - async updateCalendarNotification(calendarId: string, notificationId: string, updateData: GHLUpdateCalendarNotificationRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.put( - `/calendars/${calendarId}/notifications/${notificationId}`, - updateData - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete calendar notification - * DELETE /calendars/{calendarId}/notifications/{notificationId} - */ - async deleteCalendarNotification(calendarId: string, notificationId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.delete( - `/calendars/${calendarId}/notifications/${notificationId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get blocked slots by location - * GET /calendars/blocked-slots - */ - async getBlockedSlotsByLocation(slotParams: GHLGetBlockedSlotsRequest): Promise> { - try { - const params = new URLSearchParams({ - locationId: slotParams.locationId, - startTime: slotParams.startTime, - endTime: slotParams.endTime, - ...(slotParams.userId && { userId: slotParams.userId }), - ...(slotParams.calendarId && { calendarId: slotParams.calendarId }), - ...(slotParams.groupId && { groupId: slotParams.groupId }) - }); - - const response: AxiosResponse = await this.axiosInstance.get( - `/calendars/blocked-slots?${params}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Create a new block slot - * POST /calendars/blocked-slots - */ - async createBlockSlot(blockSlotData: GHLCreateBlockSlotRequest): Promise> { - try { - const payload = { - ...blockSlotData, - locationId: blockSlotData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/calendars/blocked-slots', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - // ===== MEDIA LIBRARY API METHODS ===== - - /** - * Get list of files and folders from media library - * GET /medias/files - */ - async getMediaFiles(params: GHLGetMediaFilesRequest): Promise> { - try { - const queryParams = new URLSearchParams({ - sortBy: params.sortBy, - sortOrder: params.sortOrder, - altType: params.altType, - altId: params.altId, - ...(params.offset !== undefined && { offset: params.offset.toString() }), - ...(params.limit !== undefined && { limit: params.limit.toString() }), - ...(params.type && { type: params.type }), - ...(params.query && { query: params.query }), - ...(params.parentId && { parentId: params.parentId }) - }); - - const response: AxiosResponse = await this.axiosInstance.get( - `/medias/files?${queryParams}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Upload file to media library - * POST /medias/upload-file - */ - async uploadMediaFile(uploadData: GHLUploadMediaFileRequest): Promise> { - try { - const formData = new FormData(); - - // Handle file upload (either direct file or hosted file URL) - if (uploadData.hosted && uploadData.fileUrl) { - formData.append('hosted', 'true'); - formData.append('fileUrl', uploadData.fileUrl); - } else if (uploadData.file) { - formData.append('hosted', 'false'); - formData.append('file', uploadData.file); - } else { - throw new Error('Either file or fileUrl (with hosted=true) must be provided'); - } - - // Add optional fields - if (uploadData.name) { - formData.append('name', uploadData.name); - } - if (uploadData.parentId) { - formData.append('parentId', uploadData.parentId); - } - - const response: AxiosResponse = await this.axiosInstance.post( - '/medias/upload-file', - formData, - { - headers: { - 'Content-Type': 'multipart/form-data', - } - } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete file or folder from media library - * DELETE /medias/{id} - */ - async deleteMediaFile(deleteParams: GHLDeleteMediaRequest): Promise> { - try { - const queryParams = new URLSearchParams({ - altType: deleteParams.altType, - altId: deleteParams.altId - }); - - const response: AxiosResponse = await this.axiosInstance.delete( - `/medias/${deleteParams.id}?${queryParams}` - ); - - return this.wrapResponse({ success: true, message: 'Media file deleted successfully' }); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - // ===== CUSTOM OBJECTS API METHODS ===== - - /** - * Get all objects for a location - * GET /objects/ - */ - async getObjectsByLocation(locationId?: string): Promise> { - try { - const params = { - locationId: locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/objects/', - { params } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Create custom object schema - * POST /objects/ - */ - async createObjectSchema(schemaData: GHLCreateObjectSchemaRequest): Promise> { - try { - const payload = { - ...schemaData, - locationId: schemaData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/objects/', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get object schema by key/id - * GET /objects/{key} - */ - async getObjectSchema(params: GHLGetObjectSchemaRequest): Promise> { - try { - const queryParams = { - locationId: params.locationId || this.config.locationId, - ...(params.fetchProperties !== undefined && { fetchProperties: params.fetchProperties.toString() }) - }; - - const response: AxiosResponse = await this.axiosInstance.get( - `/objects/${params.key}`, - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update object schema by key/id - * PUT /objects/{key} - */ - async updateObjectSchema(key: string, updateData: GHLUpdateObjectSchemaRequest): Promise> { - try { - const payload = { - ...updateData, - locationId: updateData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.put( - `/objects/${key}`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Create object record - * POST /objects/{schemaKey}/records - */ - async createObjectRecord(schemaKey: string, recordData: GHLCreateObjectRecordRequest): Promise> { - try { - const payload = { - ...recordData, - locationId: recordData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.post( - `/objects/${schemaKey}/records`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get object record by id - * GET /objects/{schemaKey}/records/{id} - */ - async getObjectRecord(schemaKey: string, recordId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.get( - `/objects/${schemaKey}/records/${recordId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update object record - * PUT /objects/{schemaKey}/records/{id} - */ - async updateObjectRecord(schemaKey: string, recordId: string, updateData: GHLUpdateObjectRecordRequest): Promise> { - try { - const queryParams = { - locationId: updateData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.put( - `/objects/${schemaKey}/records/${recordId}`, - updateData, - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete object record - * DELETE /objects/{schemaKey}/records/{id} - */ - async deleteObjectRecord(schemaKey: string, recordId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.delete( - `/objects/${schemaKey}/records/${recordId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Search object records - * POST /objects/{schemaKey}/records/search - */ - async searchObjectRecords(schemaKey: string, searchData: GHLSearchObjectRecordsRequest): Promise> { - try { - const payload = { - ...searchData, - locationId: searchData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.post( - `/objects/${schemaKey}/records/search`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - // ===== ASSOCIATIONS API METHODS ===== - - /** - * Get all associations for a location - * GET /associations/ - */ - async getAssociations(params: GHLGetAssociationsRequest): Promise> { - try { - const queryParams = { - locationId: params.locationId || this.config.locationId, - skip: params.skip.toString(), - limit: params.limit.toString() - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/associations/', - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Create association - * POST /associations/ - */ - async createAssociation(associationData: GHLCreateAssociationRequest): Promise> { - try { - const payload = { - ...associationData, - locationId: associationData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/associations/', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get association by ID - * GET /associations/{associationId} - */ - async getAssociationById(associationId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.get( - `/associations/${associationId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update association - * PUT /associations/{associationId} - */ - async updateAssociation(associationId: string, updateData: GHLUpdateAssociationRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.put( - `/associations/${associationId}`, - updateData - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete association - * DELETE /associations/{associationId} - */ - async deleteAssociation(associationId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.delete( - `/associations/${associationId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get association by key name - * GET /associations/key/{key_name} - */ - async getAssociationByKey(params: GHLGetAssociationByKeyRequest): Promise> { - try { - const queryParams = { - locationId: params.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.get( - `/associations/key/${params.keyName}`, - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get association by object key - * GET /associations/objectKey/{objectKey} - */ - async getAssociationByObjectKey(params: GHLGetAssociationByObjectKeyRequest): Promise> { - try { - const queryParams = params.locationId ? { - locationId: params.locationId - } : {}; - - const response: AxiosResponse = await this.axiosInstance.get( - `/associations/objectKey/${params.objectKey}`, - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Create relation between entities - * POST /associations/relations - */ - async createRelation(relationData: GHLCreateRelationRequest): Promise> { - try { - const payload = { - ...relationData, - locationId: relationData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/associations/relations', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get relations by record ID - * GET /associations/relations/{recordId} - */ - async getRelationsByRecord(params: GHLGetRelationsByRecordRequest): Promise> { - try { - const queryParams = { - locationId: params.locationId || this.config.locationId, - skip: params.skip.toString(), - limit: params.limit.toString(), - ...(params.associationIds && { associationIds: params.associationIds }) - }; - - const response: AxiosResponse = await this.axiosInstance.get( - `/associations/relations/${params.recordId}`, - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete relation - * DELETE /associations/relations/{relationId} - */ - async deleteRelation(params: GHLDeleteRelationRequest): Promise> { - try { - const queryParams = { - locationId: params.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.delete( - `/associations/relations/${params.relationId}`, - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - // ===== CUSTOM FIELDS V2 API METHODS ===== - - /** - * Get custom field or folder by ID - * GET /custom-fields/{id} - */ - async getCustomFieldV2ById(id: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.get( - `/custom-fields/${id}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Create custom field - * POST /custom-fields/ - */ - async createCustomFieldV2(fieldData: GHLV2CreateCustomFieldRequest): Promise> { - try { - const payload = { - ...fieldData, - locationId: fieldData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/custom-fields/', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update custom field by ID - * PUT /custom-fields/{id} - */ - async updateCustomFieldV2(id: string, fieldData: GHLV2UpdateCustomFieldRequest): Promise> { - try { - const payload = { - ...fieldData, - locationId: fieldData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.put( - `/custom-fields/${id}`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete custom field by ID - * DELETE /custom-fields/{id} - */ - async deleteCustomFieldV2(id: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.delete( - `/custom-fields/${id}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get custom fields by object key - * GET /custom-fields/object-key/{objectKey} - */ - async getCustomFieldsV2ByObjectKey(params: GHLV2GetCustomFieldsByObjectKeyRequest): Promise> { - try { - const queryParams = { - locationId: params.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.get( - `/custom-fields/object-key/${params.objectKey}`, - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Create custom field folder - * POST /custom-fields/folder - */ - async createCustomFieldV2Folder(folderData: GHLV2CreateCustomFieldFolderRequest): Promise> { - try { - const payload = { - ...folderData, - locationId: folderData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/custom-fields/folder', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Update custom field folder name - * PUT /custom-fields/folder/{id} - */ - async updateCustomFieldV2Folder(id: string, folderData: GHLV2UpdateCustomFieldFolderRequest): Promise> { - try { - const payload = { - ...folderData, - locationId: folderData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.put( - `/custom-fields/folder/${id}`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Delete custom field folder - * DELETE /custom-fields/folder/{id} - */ - async deleteCustomFieldV2Folder(params: GHLV2DeleteCustomFieldFolderRequest): Promise> { - try { - const queryParams = { - locationId: params.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.delete( - `/custom-fields/folder/${params.id}`, - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - // ===== WORKFLOWS API METHODS ===== - - /** - * Get all workflows for a location - * GET /workflows/ - */ - async getWorkflows(request: GHLGetWorkflowsRequest): Promise> { - try { - const queryParams = { - locationId: request.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/workflows/', - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - // ===== SURVEYS API METHODS ===== - - /** - * Get all surveys for a location - * GET /surveys/ - */ - async getSurveys(request: GHLGetSurveysRequest): Promise> { - try { - const queryParams: Record = { - locationId: request.locationId || this.config.locationId - }; - - if (request.skip !== undefined) { - queryParams.skip = request.skip.toString(); - } - if (request.limit !== undefined) { - queryParams.limit = request.limit.toString(); - } - if (request.type) { - queryParams.type = request.type; - } - - const response: AxiosResponse = await this.axiosInstance.get( - '/surveys/', - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw this.handleApiError(error as AxiosError); - } - } - - /** - * Get survey submissions with filtering and pagination - * GET /surveys/submissions - */ - async getSurveySubmissions(request: GHLGetSurveySubmissionsRequest): Promise> { - try { - const locationId = request.locationId || this.config.locationId; - - const params = new URLSearchParams(); - if (request.page) params.append('page', request.page.toString()); - if (request.limit) params.append('limit', request.limit.toString()); - if (request.surveyId) params.append('surveyId', request.surveyId); - if (request.q) params.append('q', request.q); - if (request.startAt) params.append('startAt', request.startAt); - if (request.endAt) params.append('endAt', request.endAt); - - const response: AxiosResponse = await this.axiosInstance.get( - `/locations/${locationId}/surveys/submissions?${params.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - // ===== STORE API METHODS ===== - - /** - * SHIPPING ZONES API METHODS - */ - - /** - * Create a new shipping zone - * POST /store/shipping-zone - */ - async createShippingZone(zoneData: GHLCreateShippingZoneRequest): Promise> { - try { - const payload = { - ...zoneData, - altId: zoneData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/store/shipping-zone', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * List all shipping zones - * GET /store/shipping-zone - */ - async listShippingZones(params: GHLGetShippingZonesRequest): Promise> { - try { - const altId = params.altId || this.config.locationId; - const queryParams = new URLSearchParams({ - altId, - altType: 'location' - }); - - if (params.limit) queryParams.append('limit', params.limit.toString()); - if (params.offset) queryParams.append('offset', params.offset.toString()); - if (params.withShippingRate !== undefined) queryParams.append('withShippingRate', params.withShippingRate.toString()); - - const response: AxiosResponse = await this.axiosInstance.get( - `/store/shipping-zone?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get a specific shipping zone by ID - * GET /store/shipping-zone/{shippingZoneId} - */ - async getShippingZone(shippingZoneId: string, params: Omit): Promise> { - try { - const altId = params.altId || this.config.locationId; - const queryParams = new URLSearchParams({ - altId, - altType: 'location' - }); - - if (params.withShippingRate !== undefined) queryParams.append('withShippingRate', params.withShippingRate.toString()); - - const response: AxiosResponse = await this.axiosInstance.get( - `/store/shipping-zone/${shippingZoneId}?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update a shipping zone - * PUT /store/shipping-zone/{shippingZoneId} - */ - async updateShippingZone(shippingZoneId: string, updateData: GHLUpdateShippingZoneRequest): Promise> { - try { - const payload = { - ...updateData, - altId: updateData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.put( - `/store/shipping-zone/${shippingZoneId}`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Delete a shipping zone - * DELETE /store/shipping-zone/{shippingZoneId} - */ - async deleteShippingZone(shippingZoneId: string, params: GHLDeleteShippingZoneRequest): Promise> { - try { - const altId = params.altId || this.config.locationId; - const queryParams = new URLSearchParams({ - altId, - altType: 'location' - }); - - const response: AxiosResponse = await this.axiosInstance.delete( - `/store/shipping-zone/${shippingZoneId}?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * SHIPPING RATES API METHODS - */ - - /** - * Get available shipping rates for an order - * POST /store/shipping-zone/shipping-rates - */ - async getAvailableShippingRates(rateData: GHLGetAvailableShippingRatesRequest): Promise> { - try { - const payload = { - ...rateData, - altId: rateData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/store/shipping-zone/shipping-rates', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Create a new shipping rate for a zone - * POST /store/shipping-zone/{shippingZoneId}/shipping-rate - */ - async createShippingRate(shippingZoneId: string, rateData: GHLCreateShippingRateRequest): Promise> { - try { - const payload = { - ...rateData, - altId: rateData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - `/store/shipping-zone/${shippingZoneId}/shipping-rate`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * List shipping rates for a zone - * GET /store/shipping-zone/{shippingZoneId}/shipping-rate - */ - async listShippingRates(shippingZoneId: string, params: GHLGetShippingRatesRequest): Promise> { - try { - const altId = params.altId || this.config.locationId; - const queryParams = new URLSearchParams({ - altId, - altType: 'location' - }); - - if (params.limit) queryParams.append('limit', params.limit.toString()); - if (params.offset) queryParams.append('offset', params.offset.toString()); - - const response: AxiosResponse = await this.axiosInstance.get( - `/store/shipping-zone/${shippingZoneId}/shipping-rate?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get a specific shipping rate - * GET /store/shipping-zone/{shippingZoneId}/shipping-rate/{shippingRateId} - */ - async getShippingRate(shippingZoneId: string, shippingRateId: string, params: Omit): Promise> { - try { - const altId = params.altId || this.config.locationId; - const queryParams = new URLSearchParams({ - altId, - altType: 'location' - }); - - const response: AxiosResponse = await this.axiosInstance.get( - `/store/shipping-zone/${shippingZoneId}/shipping-rate/${shippingRateId}?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update a shipping rate - * PUT /store/shipping-zone/{shippingZoneId}/shipping-rate/{shippingRateId} - */ - async updateShippingRate(shippingZoneId: string, shippingRateId: string, updateData: GHLUpdateShippingRateRequest): Promise> { - try { - const payload = { - ...updateData, - altId: updateData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.put( - `/store/shipping-zone/${shippingZoneId}/shipping-rate/${shippingRateId}`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Delete a shipping rate - * DELETE /store/shipping-zone/{shippingZoneId}/shipping-rate/{shippingRateId} - */ - async deleteShippingRate(shippingZoneId: string, shippingRateId: string, params: GHLDeleteShippingRateRequest): Promise> { - try { - const altId = params.altId || this.config.locationId; - const queryParams = new URLSearchParams({ - altId, - altType: 'location' - }); - - const response: AxiosResponse = await this.axiosInstance.delete( - `/store/shipping-zone/${shippingZoneId}/shipping-rate/${shippingRateId}?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * SHIPPING CARRIERS API METHODS - */ - - /** - * Create a new shipping carrier - * POST /store/shipping-carrier - */ - async createShippingCarrier(carrierData: GHLCreateShippingCarrierRequest): Promise> { - try { - const payload = { - ...carrierData, - altId: carrierData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/store/shipping-carrier', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * List all shipping carriers - * GET /store/shipping-carrier - */ - async listShippingCarriers(params: GHLGetShippingCarriersRequest): Promise> { - try { - const altId = params.altId || this.config.locationId; - const queryParams = new URLSearchParams({ - altId, - altType: 'location' - }); - - const response: AxiosResponse = await this.axiosInstance.get( - `/store/shipping-carrier?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get a specific shipping carrier by ID - * GET /store/shipping-carrier/{shippingCarrierId} - */ - async getShippingCarrier(shippingCarrierId: string, params: GHLGetShippingCarriersRequest): Promise> { - try { - const altId = params.altId || this.config.locationId; - const queryParams = new URLSearchParams({ - altId, - altType: 'location' - }); - - const response: AxiosResponse = await this.axiosInstance.get( - `/store/shipping-carrier/${shippingCarrierId}?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update a shipping carrier - * PUT /store/shipping-carrier/{shippingCarrierId} - */ - async updateShippingCarrier(shippingCarrierId: string, updateData: GHLUpdateShippingCarrierRequest): Promise> { - try { - const payload = { - ...updateData, - altId: updateData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.put( - `/store/shipping-carrier/${shippingCarrierId}`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Delete a shipping carrier - * DELETE /store/shipping-carrier/{shippingCarrierId} - */ - async deleteShippingCarrier(shippingCarrierId: string, params: GHLDeleteShippingCarrierRequest): Promise> { - try { - const altId = params.altId || this.config.locationId; - const queryParams = new URLSearchParams({ - altId, - altType: 'location' - }); - - const response: AxiosResponse = await this.axiosInstance.delete( - `/store/shipping-carrier/${shippingCarrierId}?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * STORE SETTINGS API METHODS - */ - - /** - * Create or update store settings - * POST /store/store-setting - */ - async createStoreSetting(settingData: GHLCreateStoreSettingRequest): Promise> { - try { - const payload = { - ...settingData, - altId: settingData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/store/store-setting', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get store settings - * GET /store/store-setting - */ - async getStoreSetting(params: GHLGetStoreSettingRequest): Promise> { - try { - const altId = params.altId || this.config.locationId; - const queryParams = new URLSearchParams({ - altId, - altType: 'location' - }); - - const response: AxiosResponse = await this.axiosInstance.get( - `/store/store-setting?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * PRODUCTS API METHODS - */ - - /** - * Create a new product - * POST /products/ - */ - async createProduct(productData: GHLCreateProductRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.post( - '/products/', - productData - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update a product by ID - * PUT /products/{productId} - */ - async updateProduct(productId: string, updateData: GHLUpdateProductRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.put( - `/products/${productId}`, - updateData - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get a product by ID - * GET /products/{productId} - */ - async getProduct(productId: string, locationId?: string): Promise> { - try { - const queryParams = new URLSearchParams({ - locationId: locationId || this.config.locationId - }); - - const response: AxiosResponse = await this.axiosInstance.get( - `/products/${productId}?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * List products - * GET /products/ - */ - async listProducts(params: GHLListProductsRequest): Promise> { - try { - const queryParams = new URLSearchParams({ - locationId: params.locationId || this.config.locationId - }); - - if (params.limit) queryParams.append('limit', params.limit.toString()); - if (params.offset) queryParams.append('offset', params.offset.toString()); - if (params.search) queryParams.append('search', params.search); - if (params.collectionIds?.length) queryParams.append('collectionIds', params.collectionIds.join(',')); - if (params.collectionSlug) queryParams.append('collectionSlug', params.collectionSlug); - if (params.expand?.length) params.expand.forEach(item => queryParams.append('expand', item)); - if (params.productIds?.length) params.productIds.forEach(id => queryParams.append('productIds', id)); - if (params.storeId) queryParams.append('storeId', params.storeId); - if (params.includedInStore !== undefined) queryParams.append('includedInStore', params.includedInStore.toString()); - if (params.availableInStore !== undefined) queryParams.append('availableInStore', params.availableInStore.toString()); - if (params.sortOrder) queryParams.append('sortOrder', params.sortOrder); - - const response: AxiosResponse = await this.axiosInstance.get( - `/products/?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Delete a product by ID - * DELETE /products/{productId} - */ - async deleteProduct(productId: string, locationId?: string): Promise> { - try { - const queryParams = new URLSearchParams({ - locationId: locationId || this.config.locationId - }); - - const response: AxiosResponse = await this.axiosInstance.delete( - `/products/${productId}?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Bulk update products - * POST /products/bulk-update - */ - async bulkUpdateProducts(updateData: GHLBulkUpdateRequest): Promise> { - try { - const payload = { - ...updateData, - altId: updateData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/products/bulk-update', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Create a price for a product - * POST /products/{productId}/price - */ - async createPrice(productId: string, priceData: GHLCreatePriceRequest): Promise> { - try { - const payload = { - ...priceData, - locationId: priceData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.post( - `/products/${productId}/price`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update a price by ID - * PUT /products/{productId}/price/{priceId} - */ - async updatePrice(productId: string, priceId: string, updateData: GHLUpdatePriceRequest): Promise> { - try { - const payload = { - ...updateData, - locationId: updateData.locationId || this.config.locationId - }; - - const response: AxiosResponse = await this.axiosInstance.put( - `/products/${productId}/price/${priceId}`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get a price by ID - * GET /products/{productId}/price/{priceId} - */ - async getPrice(productId: string, priceId: string, locationId?: string): Promise> { - try { - const queryParams = new URLSearchParams({ - locationId: locationId || this.config.locationId - }); - - const response: AxiosResponse = await this.axiosInstance.get( - `/products/${productId}/price/${priceId}?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * List prices for a product - * GET /products/{productId}/price - */ - async listPrices(productId: string, params: GHLListPricesRequest): Promise> { - try { - const queryParams = new URLSearchParams({ - locationId: params.locationId || this.config.locationId - }); - - if (params.limit) queryParams.append('limit', params.limit.toString()); - if (params.offset) queryParams.append('offset', params.offset.toString()); - if (params.ids) queryParams.append('ids', params.ids); - - const response: AxiosResponse = await this.axiosInstance.get( - `/products/${productId}/price?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Delete a price by ID - * DELETE /products/{productId}/price/{priceId} - */ - async deletePrice(productId: string, priceId: string, locationId?: string): Promise> { - try { - const queryParams = new URLSearchParams({ - locationId: locationId || this.config.locationId - }); - - const response: AxiosResponse = await this.axiosInstance.delete( - `/products/${productId}/price/${priceId}?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * List inventory - * GET /products/inventory - */ - async listInventory(params: GHLListInventoryRequest): Promise> { - try { - const queryParams = new URLSearchParams({ - altId: params.altId || this.config.locationId, - altType: 'location' - }); - - if (params.limit) queryParams.append('limit', params.limit.toString()); - if (params.offset) queryParams.append('offset', params.offset.toString()); - if (params.search) queryParams.append('search', params.search); - - const response: AxiosResponse = await this.axiosInstance.get( - `/products/inventory?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update inventory - * POST /products/inventory - */ - async updateInventory(updateData: GHLUpdateInventoryRequest): Promise> { - try { - const payload = { - ...updateData, - altId: updateData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/products/inventory', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get product store stats - * GET /products/store/{storeId}/stats - */ - async getProductStoreStats(storeId: string, params: GHLGetProductStoreStatsRequest): Promise> { - try { - const queryParams = new URLSearchParams({ - altId: params.altId || this.config.locationId, - altType: 'location' - }); - - if (params.search) queryParams.append('search', params.search); - if (params.collectionIds) queryParams.append('collectionIds', params.collectionIds); - - const response: AxiosResponse = await this.axiosInstance.get( - `/products/store/${storeId}/stats?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update product store status - * POST /products/store/{storeId} - */ - async updateProductStore(storeId: string, updateData: GHLUpdateProductStoreRequest): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.post( - `/products/store/${storeId}`, - updateData - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Create a product collection - * POST /products/collections - */ - async createProductCollection(collectionData: GHLCreateProductCollectionRequest): Promise> { - try { - const payload = { - ...collectionData, - altId: collectionData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/products/collections', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update a product collection - * PUT /products/collections/{collectionId} - */ - async updateProductCollection(collectionId: string, updateData: GHLUpdateProductCollectionRequest): Promise> { - try { - const payload = { - ...updateData, - altId: updateData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.put( - `/products/collections/${collectionId}`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get a product collection by ID - * GET /products/collections/{collectionId} - */ - async getProductCollection(collectionId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.get( - `/products/collections/${collectionId}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * List product collections - * GET /products/collections - */ - async listProductCollections(params: GHLListProductCollectionsRequest): Promise> { - try { - const queryParams = new URLSearchParams({ - altId: params.altId || this.config.locationId, - altType: 'location' - }); - - if (params.limit) queryParams.append('limit', params.limit.toString()); - if (params.offset) queryParams.append('offset', params.offset.toString()); - if (params.collectionIds) queryParams.append('collectionIds', params.collectionIds); - if (params.name) queryParams.append('name', params.name); - - const response: AxiosResponse = await this.axiosInstance.get( - `/products/collections?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Delete a product collection - * DELETE /products/collections/{collectionId} - */ - async deleteProductCollection(collectionId: string, params: GHLDeleteProductCollectionRequest): Promise> { - try { - const queryParams = new URLSearchParams({ - altId: params.altId || this.config.locationId, - altType: 'location' - }); - - const response: AxiosResponse = await this.axiosInstance.delete( - `/products/collections/${collectionId}?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * List product reviews - * GET /products/reviews - */ - async listProductReviews(params: GHLListProductReviewsRequest): Promise> { - try { - const queryParams = new URLSearchParams({ - altId: params.altId || this.config.locationId, - altType: 'location' - }); - - if (params.limit) queryParams.append('limit', params.limit.toString()); - if (params.offset) queryParams.append('offset', params.offset.toString()); - if (params.sortField) queryParams.append('sortField', params.sortField); - if (params.sortOrder) queryParams.append('sortOrder', params.sortOrder); - if (params.rating) queryParams.append('rating', params.rating.toString()); - if (params.startDate) queryParams.append('startDate', params.startDate); - if (params.endDate) queryParams.append('endDate', params.endDate); - if (params.productId) queryParams.append('productId', params.productId); - if (params.storeId) queryParams.append('storeId', params.storeId); - - const response: AxiosResponse = await this.axiosInstance.get( - `/products/reviews?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get reviews count - * GET /products/reviews/count - */ - async getReviewsCount(params: GHLGetReviewsCountRequest): Promise> { - try { - const queryParams = new URLSearchParams({ - altId: params.altId || this.config.locationId, - altType: 'location' - }); - - if (params.rating) queryParams.append('rating', params.rating.toString()); - if (params.startDate) queryParams.append('startDate', params.startDate); - if (params.endDate) queryParams.append('endDate', params.endDate); - if (params.productId) queryParams.append('productId', params.productId); - if (params.storeId) queryParams.append('storeId', params.storeId); - - const response: AxiosResponse = await this.axiosInstance.get( - `/products/reviews/count?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update a product review - * PUT /products/reviews/{reviewId} - */ - async updateProductReview(reviewId: string, updateData: GHLUpdateProductReviewRequest): Promise> { - try { - const payload = { - ...updateData, - altId: updateData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.put( - `/products/reviews/${reviewId}`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Delete a product review - * DELETE /products/reviews/{reviewId} - */ - async deleteProductReview(reviewId: string, params: GHLDeleteProductReviewRequest): Promise> { - try { - const queryParams = new URLSearchParams({ - altId: params.altId || this.config.locationId, - altType: 'location', - productId: params.productId - }); - - const response: AxiosResponse = await this.axiosInstance.delete( - `/products/reviews/${reviewId}?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Bulk update product reviews - * POST /products/reviews/bulk-update - */ - async bulkUpdateProductReviews(updateData: GHLBulkUpdateProductReviewsRequest): Promise> { - try { - const payload = { - ...updateData, - altId: updateData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/products/reviews/bulk-update', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * PAYMENTS API METHODS - */ - - /** - * Create white-label integration provider - * POST /payments/integrations/provider/whitelabel - */ - async createWhiteLabelIntegrationProvider(data: any): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.post( - '/payments/integrations/provider/whitelabel', - data - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * List white-label integration providers - * GET /payments/integrations/provider/whitelabel - */ - async listWhiteLabelIntegrationProviders(params: Record): Promise> { - try { - const queryParams = new URLSearchParams(); - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - queryParams.append(key, value.toString()); - } - }); - - const response: AxiosResponse = await this.axiosInstance.get( - `/payments/integrations/provider/whitelabel?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * List orders - * GET /payments/orders - */ - async listOrders(params: Record): Promise> { - try { - const queryParams = new URLSearchParams(); - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - queryParams.append(key, value.toString()); - } - }); - - const response: AxiosResponse = await this.axiosInstance.get( - `/payments/orders?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get order by ID - * GET /payments/orders/{orderId} - */ - async getOrderById(orderId: string, params: Record): Promise> { - try { - const queryParams = new URLSearchParams(); - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null && key !== 'orderId') { - queryParams.append(key, value.toString()); - } - }); - - const response: AxiosResponse = await this.axiosInstance.get( - `/payments/orders/${orderId}?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Create order fulfillment - * POST /payments/orders/{orderId}/fulfillments - */ - async createOrderFulfillment(orderId: string, data: any): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.post( - `/payments/orders/${orderId}/fulfillments`, - data - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * List order fulfillments - * GET /payments/orders/{orderId}/fulfillments - */ - async listOrderFulfillments(orderId: string, params: Record): Promise> { - try { - const queryParams = new URLSearchParams(); - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null && key !== 'orderId') { - queryParams.append(key, value.toString()); - } - }); - - const response: AxiosResponse = await this.axiosInstance.get( - `/payments/orders/${orderId}/fulfillments?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * List transactions - * GET /payments/transactions - */ - async listTransactions(params: Record): Promise> { - try { - const queryParams = new URLSearchParams(); - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - queryParams.append(key, value.toString()); - } - }); - - const response: AxiosResponse = await this.axiosInstance.get( - `/payments/transactions?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get transaction by ID - * GET /payments/transactions/{transactionId} - */ - async getTransactionById(transactionId: string, params: Record): Promise> { - try { - const queryParams = new URLSearchParams(); - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null && key !== 'transactionId') { - queryParams.append(key, value.toString()); - } - }); - - const response: AxiosResponse = await this.axiosInstance.get( - `/payments/transactions/${transactionId}?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * List subscriptions - * GET /payments/subscriptions - */ - async listSubscriptions(params: Record): Promise> { - try { - const queryParams = new URLSearchParams(); - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - queryParams.append(key, value.toString()); - } - }); - - const response: AxiosResponse = await this.axiosInstance.get( - `/payments/subscriptions?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get subscription by ID - * GET /payments/subscriptions/{subscriptionId} - */ - async getSubscriptionById(subscriptionId: string, params: Record): Promise> { - try { - const queryParams = new URLSearchParams(); - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null && key !== 'subscriptionId') { - queryParams.append(key, value.toString()); - } - }); - - const response: AxiosResponse = await this.axiosInstance.get( - `/payments/subscriptions/${subscriptionId}?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * List coupons - * GET /payments/coupon/list - */ - async listCoupons(params: Record): Promise> { - try { - const queryParams = new URLSearchParams(); - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - queryParams.append(key, value.toString()); - } - }); - - const response: AxiosResponse = await this.axiosInstance.get( - `/payments/coupon/list?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Create coupon - * POST /payments/coupon - */ - async createCoupon(data: any): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.post( - '/payments/coupon', - data - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update coupon - * PUT /payments/coupon - */ - async updateCoupon(data: any): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.put( - '/payments/coupon', - data - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Delete coupon - * DELETE /payments/coupon - */ - async deleteCoupon(data: any): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.delete( - '/payments/coupon', - { data } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get coupon - * GET /payments/coupon - */ - async getCoupon(params: Record): Promise> { - try { - const queryParams = new URLSearchParams(); - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - queryParams.append(key, value.toString()); - } - }); - - const response: AxiosResponse = await this.axiosInstance.get( - `/payments/coupon?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Create custom provider integration - * POST /payments/custom-provider/provider - */ - async createCustomProviderIntegration(locationId: string, data: any): Promise> { - try { - const queryParams = new URLSearchParams({ locationId }); - - const response: AxiosResponse = await this.axiosInstance.post( - `/payments/custom-provider/provider?${queryParams.toString()}`, - data - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Delete custom provider integration - * DELETE /payments/custom-provider/provider - */ - async deleteCustomProviderIntegration(locationId: string): Promise> { - try { - const queryParams = new URLSearchParams({ locationId }); - - const response: AxiosResponse = await this.axiosInstance.delete( - `/payments/custom-provider/provider?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get custom provider config - * GET /payments/custom-provider/connect - */ - async getCustomProviderConfig(locationId: string): Promise> { - try { - const queryParams = new URLSearchParams({ locationId }); - - const response: AxiosResponse = await this.axiosInstance.get( - `/payments/custom-provider/connect?${queryParams.toString()}` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Create custom provider config - * POST /payments/custom-provider/connect - */ - async createCustomProviderConfig(locationId: string, data: any): Promise> { - try { - const queryParams = new URLSearchParams({ locationId }); - - const response: AxiosResponse = await this.axiosInstance.post( - `/payments/custom-provider/connect?${queryParams.toString()}`, - data - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Disconnect custom provider config - * POST /payments/custom-provider/disconnect - */ - async disconnectCustomProviderConfig(locationId: string, data: any): Promise> { - try { - const queryParams = new URLSearchParams({ locationId }); - - const response: AxiosResponse = await this.axiosInstance.post( - `/payments/custom-provider/disconnect?${queryParams.toString()}`, - data - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - // ============================================================================= - // INVOICES API METHODS - // ============================================================================= - - /** - * Create invoice template - * POST /invoices/template - */ - async createInvoiceTemplate(templateData: CreateInvoiceTemplateDto): Promise> { - try { - const payload = { - ...templateData, - altId: templateData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/invoices/template', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * List invoice templates - * GET /invoices/template - */ - async listInvoiceTemplates(params?: { - altId?: string; - altType?: 'location'; - status?: string; - startAt?: string; - endAt?: string; - search?: string; - paymentMode?: 'default' | 'live' | 'test'; - limit: string; - offset: string; - }): Promise> { - try { - const queryParams = { - altId: params?.altId || this.config.locationId, - altType: 'location' as const, - limit: params?.limit || '10', - offset: params?.offset || '0', - ...(params?.status && { status: params.status }), - ...(params?.startAt && { startAt: params.startAt }), - ...(params?.endAt && { endAt: params.endAt }), - ...(params?.search && { search: params.search }), - ...(params?.paymentMode && { paymentMode: params.paymentMode }) - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/invoices/template', - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get invoice template by ID - * GET /invoices/template/{templateId} - */ - async getInvoiceTemplate(templateId: string, params?: { - altId?: string; - altType?: 'location'; - }): Promise> { - try { - const queryParams = { - altId: params?.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.get( - `/invoices/template/${templateId}`, - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update invoice template - * PUT /invoices/template/{templateId} - */ - async updateInvoiceTemplate(templateId: string, templateData: UpdateInvoiceTemplateDto): Promise> { - try { - const payload = { - ...templateData, - altId: templateData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.put( - `/invoices/template/${templateId}`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Delete invoice template - * DELETE /invoices/template/{templateId} - */ - async deleteInvoiceTemplate(templateId: string, params?: { - altId?: string; - altType?: 'location'; - }): Promise> { - try { - const queryParams = { - altId: params?.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.delete( - `/invoices/template/${templateId}`, - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update invoice template late fees configuration - * PATCH /invoices/template/{templateId}/late-fees-configuration - */ - async updateInvoiceTemplateLateFeesConfiguration(templateId: string, configData: UpdateInvoiceLateFeesConfigurationDto): Promise> { - try { - const payload = { - ...configData, - altId: configData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.patch( - `/invoices/template/${templateId}/late-fees-configuration`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update invoice template payment methods configuration - * PATCH /invoices/template/{templateId}/payment-methods-configuration - */ - async updateInvoiceTemplatePaymentMethodsConfiguration(templateId: string, configData: UpdatePaymentMethodsConfigurationDto): Promise> { - try { - const payload = { - ...configData, - altId: configData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.patch( - `/invoices/template/${templateId}/payment-methods-configuration`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Create invoice schedule - * POST /invoices/schedule - */ - async createInvoiceSchedule(scheduleData: CreateInvoiceScheduleDto): Promise> { - try { - const payload = { - ...scheduleData, - altId: scheduleData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/invoices/schedule', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * List invoice schedules - * GET /invoices/schedule - */ - async listInvoiceSchedules(params?: { - altId?: string; - altType?: 'location'; - status?: string; - startAt?: string; - endAt?: string; - search?: string; - paymentMode?: 'default' | 'live' | 'test'; - limit: string; - offset: string; - }): Promise> { - try { - const queryParams = { - altId: params?.altId || this.config.locationId, - altType: 'location' as const, - limit: params?.limit || '10', - offset: params?.offset || '0', - ...(params?.status && { status: params.status }), - ...(params?.startAt && { startAt: params.startAt }), - ...(params?.endAt && { endAt: params.endAt }), - ...(params?.search && { search: params.search }), - ...(params?.paymentMode && { paymentMode: params.paymentMode }) - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/invoices/schedule', - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get invoice schedule by ID - * GET /invoices/schedule/{scheduleId} - */ - async getInvoiceSchedule(scheduleId: string, params?: { - altId?: string; - altType?: 'location'; - }): Promise> { - try { - const queryParams = { - altId: params?.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.get( - `/invoices/schedule/${scheduleId}`, - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update invoice schedule - * PUT /invoices/schedule/{scheduleId} - */ - async updateInvoiceSchedule(scheduleId: string, scheduleData: UpdateInvoiceScheduleDto): Promise> { - try { - const payload = { - ...scheduleData, - altId: scheduleData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.put( - `/invoices/schedule/${scheduleId}`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Delete invoice schedule - * DELETE /invoices/schedule/{scheduleId} - */ - async deleteInvoiceSchedule(scheduleId: string, params?: { - altId?: string; - altType?: 'location'; - }): Promise> { - try { - const queryParams = { - altId: params?.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.delete( - `/invoices/schedule/${scheduleId}`, - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update and schedule recurring invoice - * POST /invoices/schedule/{scheduleId}/updateAndSchedule - */ - async updateAndScheduleInvoiceSchedule(scheduleId: string): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.post( - `/invoices/schedule/${scheduleId}/updateAndSchedule` - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Schedule an invoice schedule - * POST /invoices/schedule/{scheduleId}/schedule - */ - async scheduleInvoiceSchedule(scheduleId: string, scheduleData: ScheduleInvoiceScheduleDto): Promise> { - try { - const payload = { - ...scheduleData, - altId: scheduleData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - `/invoices/schedule/${scheduleId}/schedule`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Manage auto payment for schedule invoice - * POST /invoices/schedule/{scheduleId}/auto-payment - */ - async autoPaymentInvoiceSchedule(scheduleId: string, paymentData: AutoPaymentScheduleDto): Promise> { - try { - const payload = { - ...paymentData, - altId: paymentData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - `/invoices/schedule/${scheduleId}/auto-payment`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Cancel scheduled invoice - * POST /invoices/schedule/{scheduleId}/cancel - */ - async cancelInvoiceSchedule(scheduleId: string, cancelData: CancelInvoiceScheduleDto): Promise> { - try { - const payload = { - ...cancelData, - altId: cancelData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - `/invoices/schedule/${scheduleId}/cancel`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Create or update text2pay invoice - * POST /invoices/text2pay - */ - async text2PayInvoice(invoiceData: Text2PayDto): Promise> { - try { - const payload = { - ...invoiceData, - altId: invoiceData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/invoices/text2pay', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Generate invoice number - * GET /invoices/generate-invoice-number - */ - async generateInvoiceNumber(params?: { - altId?: string; - altType?: 'location'; - }): Promise> { - try { - const queryParams = { - altId: params?.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/invoices/generate-invoice-number', - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Get invoice by ID - * GET /invoices/{invoiceId} - */ - async getInvoice(invoiceId: string, params?: { - altId?: string; - altType?: 'location'; - }): Promise> { - try { - const queryParams = { - altId: params?.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.get( - `/invoices/${invoiceId}`, - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update invoice - * PUT /invoices/{invoiceId} - */ - async updateInvoice(invoiceId: string, invoiceData: UpdateInvoiceDto): Promise> { - try { - const payload = { - ...invoiceData, - altId: invoiceData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.put( - `/invoices/${invoiceId}`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Delete invoice - * DELETE /invoices/{invoiceId} - */ - async deleteInvoice(invoiceId: string, params?: { - altId?: string; - altType?: 'location'; - }): Promise> { - try { - const queryParams = { - altId: params?.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.delete( - `/invoices/${invoiceId}`, - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update invoice late fees configuration - * PATCH /invoices/{invoiceId}/late-fees-configuration - */ - async updateInvoiceLateFeesConfiguration(invoiceId: string, configData: UpdateInvoiceLateFeesConfigurationDto): Promise> { - try { - const payload = { - ...configData, - altId: configData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.patch( - `/invoices/${invoiceId}/late-fees-configuration`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Void invoice - * POST /invoices/{invoiceId}/void - */ - async voidInvoice(invoiceId: string, voidData: VoidInvoiceDto): Promise> { - try { - const payload = { - ...voidData, - altId: voidData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - `/invoices/${invoiceId}/void`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Send invoice - * POST /invoices/{invoiceId}/send - */ - async sendInvoice(invoiceId: string, sendData: SendInvoiceDto): Promise> { - try { - const payload = { - ...sendData, - altId: sendData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - `/invoices/${invoiceId}/send`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Record manual payment for invoice - * POST /invoices/{invoiceId}/record-payment - */ - async recordInvoicePayment(invoiceId: string, paymentData: RecordPaymentDto): Promise> { - try { - const payload = { - ...paymentData, - altId: paymentData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - `/invoices/${invoiceId}/record-payment`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update invoice last visited at - * PATCH /invoices/stats/last-visited-at - */ - async updateInvoiceLastVisitedAt(statsData: PatchInvoiceStatsLastViewedDto): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.patch( - '/invoices/stats/last-visited-at', - statsData - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Create new estimate - * POST /invoices/estimate - */ - async createEstimate(estimateData: CreateEstimatesDto): Promise> { - try { - const payload = { - ...estimateData, - altId: estimateData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/invoices/estimate', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update estimate - * PUT /invoices/estimate/{estimateId} - */ - async updateEstimate(estimateId: string, estimateData: UpdateEstimateDto): Promise> { - try { - const payload = { - ...estimateData, - altId: estimateData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.put( - `/invoices/estimate/${estimateId}`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Delete estimate - * DELETE /invoices/estimate/{estimateId} - */ - async deleteEstimate(estimateId: string, deleteData: AltDto): Promise> { - try { - const payload = { - ...deleteData, - altId: deleteData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.delete( - `/invoices/estimate/${estimateId}`, - { data: payload } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Generate estimate number - * GET /invoices/estimate/number/generate - */ - async generateEstimateNumber(params?: { - altId?: string; - altType?: 'location'; - }): Promise> { - try { - const queryParams = { - altId: params?.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/invoices/estimate/number/generate', - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Send estimate - * POST /invoices/estimate/{estimateId}/send - */ - async sendEstimate(estimateId: string, sendData: SendEstimateDto): Promise> { - try { - const payload = { - ...sendData, - altId: sendData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - `/invoices/estimate/${estimateId}/send`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Create invoice from estimate - * POST /invoices/estimate/{estimateId}/invoice - */ - async createInvoiceFromEstimate(estimateId: string, invoiceData: CreateInvoiceFromEstimateDto): Promise> { - try { - const payload = { - ...invoiceData, - altId: invoiceData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - `/invoices/estimate/${estimateId}/invoice`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * List estimates - * GET /invoices/estimate/list - */ - async listEstimates(params?: { - altId?: string; - altType?: 'location'; - startAt?: string; - endAt?: string; - search?: string; - status?: 'all' | 'draft' | 'sent' | 'accepted' | 'declined' | 'invoiced' | 'viewed'; - contactId?: string; - limit: string; - offset: string; - }): Promise> { - try { - const queryParams = { - altId: params?.altId || this.config.locationId, - altType: 'location' as const, - limit: params?.limit || '10', - offset: params?.offset || '0', - ...(params?.startAt && { startAt: params.startAt }), - ...(params?.endAt && { endAt: params.endAt }), - ...(params?.search && { search: params.search }), - ...(params?.status && { status: params.status }), - ...(params?.contactId && { contactId: params.contactId }) - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/invoices/estimate/list', - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update estimate last visited at - * PATCH /invoices/estimate/stats/last-visited-at - */ - async updateEstimateLastVisitedAt(statsData: EstimateIdParam): Promise> { - try { - const response: AxiosResponse = await this.axiosInstance.patch( - '/invoices/estimate/stats/last-visited-at', - statsData - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * List estimate templates - * GET /invoices/estimate/template - */ - async listEstimateTemplates(params?: { - altId?: string; - altType?: 'location'; - search?: string; - limit: string; - offset: string; - }): Promise> { - try { - const queryParams = { - altId: params?.altId || this.config.locationId, - altType: 'location' as const, - limit: params?.limit || '10', - offset: params?.offset || '0', - ...(params?.search && { search: params.search }) - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/invoices/estimate/template', - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Create estimate template - * POST /invoices/estimate/template - */ - async createEstimateTemplate(templateData: EstimateTemplatesDto): Promise> { - try { - const payload = { - ...templateData, - altId: templateData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/invoices/estimate/template', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Update estimate template - * PUT /invoices/estimate/template/{templateId} - */ - async updateEstimateTemplate(templateId: string, templateData: EstimateTemplatesDto): Promise> { - try { - const payload = { - ...templateData, - altId: templateData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.put( - `/invoices/estimate/template/${templateId}`, - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Delete estimate template - * DELETE /invoices/estimate/template/{templateId} - */ - async deleteEstimateTemplate(templateId: string, deleteData: AltDto): Promise> { - try { - const payload = { - ...deleteData, - altId: deleteData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.delete( - `/invoices/estimate/template/${templateId}`, - { data: payload } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Preview estimate template - * GET /invoices/estimate/template/preview - */ - async previewEstimateTemplate(params?: { - altId?: string; - altType?: 'location'; - templateId: string; - }): Promise> { - try { - const queryParams = { - altId: params?.altId || this.config.locationId, - altType: 'location' as const, - templateId: params?.templateId || '' - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/invoices/estimate/template/preview', - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * Create invoice - * POST /invoices/ - */ - async createInvoice(invoiceData: CreateInvoiceDto): Promise> { - try { - const payload = { - ...invoiceData, - altId: invoiceData.altId || this.config.locationId, - altType: 'location' as const - }; - - const response: AxiosResponse = await this.axiosInstance.post( - '/invoices/', - payload - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } - - /** - * List invoices - * GET /invoices/ - */ - async listInvoices(params?: { - altId?: string; - altType?: 'location'; - status?: string; - startAt?: string; - endAt?: string; - search?: string; - paymentMode?: 'default' | 'live' | 'test'; - contactId?: string; - limit: string; - offset: string; - sortField?: 'issueDate'; - sortOrder?: 'ascend' | 'descend'; - }): Promise> { - try { - const queryParams = { - altId: params?.altId || this.config.locationId, - altType: 'location' as const, - limit: params?.limit || '10', - offset: params?.offset || '0', - ...(params?.status && { status: params.status }), - ...(params?.startAt && { startAt: params.startAt }), - ...(params?.endAt && { endAt: params.endAt }), - ...(params?.search && { search: params.search }), - ...(params?.paymentMode && { paymentMode: params.paymentMode }), - ...(params?.contactId && { contactId: params.contactId }), - ...(params?.sortField && { sortField: params.sortField }), - ...(params?.sortOrder && { sortOrder: params.sortOrder }) - }; - - const response: AxiosResponse = await this.axiosInstance.get( - '/invoices/', - { params: queryParams } - ); - - return this.wrapResponse(response.data); - } catch (error) { - throw error; - } - } -} \ No newline at end of file diff --git a/mcp-diagrams/ghl-mcp-apps-only/src/server.ts b/mcp-diagrams/ghl-mcp-apps-only/src/server.ts deleted file mode 100644 index a558944..0000000 --- a/mcp-diagrams/ghl-mcp-apps-only/src/server.ts +++ /dev/null @@ -1,172 +0,0 @@ -/** - * GoHighLevel MCP Apps Server (Slimmed Down) - * Only includes MCP Apps - no other tools - */ - -import { Server } from '@modelcontextprotocol/sdk/server/index.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { - CallToolRequestSchema, - ErrorCode, - ListToolsRequestSchema, - ListResourcesRequestSchema, - ReadResourceRequestSchema, - McpError -} from '@modelcontextprotocol/sdk/types.js'; -import * as dotenv from 'dotenv'; - -import { GHLApiClient } from './clients/ghl-api-client.js'; -import { MCPAppsManager } from './apps/index.js'; -import { GHLConfig } from './types/ghl-types.js'; - -// Load environment variables -dotenv.config(); - -/** - * MCP Apps Only Server - */ -class GHLMCPAppsServer { - private server: Server; - private ghlClient: GHLApiClient; - private mcpAppsManager: MCPAppsManager; - - constructor() { - // Initialize MCP server with capabilities - this.server = new Server( - { - name: 'ghl-mcp-apps-only', - version: '1.0.0', - }, - { - capabilities: { - tools: {}, - resources: {}, - }, - } - ); - - // Initialize GHL API client - this.ghlClient = this.initializeGHLClient(); - - // Initialize MCP Apps Manager - this.mcpAppsManager = new MCPAppsManager(this.ghlClient); - - this.setupHandlers(); - this.setupErrorHandling(); - } - - /** - * Initialize the GHL API client with config from environment - */ - private initializeGHLClient(): GHLApiClient { - const config: GHLConfig = { - accessToken: process.env.GHL_API_KEY || '', - locationId: process.env.GHL_LOCATION_ID || '', - baseUrl: process.env.GHL_BASE_URL || 'https://services.leadconnectorhq.com', - version: '2021-07-28' - }; - - if (!config.accessToken) { - process.stderr.write('Warning: GHL_API_KEY not set in environment\n'); - } - if (!config.locationId) { - process.stderr.write('Warning: GHL_LOCATION_ID not set in environment\n'); - } - - return new GHLApiClient(config); - } - - /** - * Setup request handlers - */ - private setupHandlers(): void { - // List tools - only MCP App tools - this.server.setRequestHandler(ListToolsRequestSchema, async () => { - const appTools = this.mcpAppsManager.getToolDefinitions(); - process.stderr.write(`[MCP Apps Only] Listing ${appTools.length} app tools\n`); - return { tools: appTools }; - }); - - // List resources - MCP App UI resources - this.server.setRequestHandler(ListResourcesRequestSchema, async () => { - const resourceUris = this.mcpAppsManager.getResourceURIs(); - const resources = resourceUris.map(uri => { - const handler = this.mcpAppsManager.getResourceHandler(uri); - return { - uri: uri, - name: uri, - mimeType: handler?.mimeType || 'text/html;profile=mcp-app' - }; - }); - process.stderr.write(`[MCP Apps Only] Listing ${resources.length} UI resources\n`); - return { resources }; - }); - - // Read resource - serve UI HTML - this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { - const uri = request.params.uri; - process.stderr.write(`[MCP Apps Only] Reading resource: ${uri}\n`); - - const handler = this.mcpAppsManager.getResourceHandler(uri); - if (!handler) { - throw new McpError(ErrorCode.InvalidRequest, `Resource not found: ${uri}`); - } - - return { - contents: [{ - uri: uri, - mimeType: handler.mimeType, - text: handler.getContent() - }] - }; - }); - - // Call tool - execute MCP App tools - this.server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - process.stderr.write(`[MCP Apps Only] Calling tool: ${name}\n`); - - if (!this.mcpAppsManager.isAppTool(name)) { - throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); - } - - try { - const result = await this.mcpAppsManager.executeTool(name, args || {}); - return result; - } catch (error: any) { - process.stderr.write(`[MCP Apps Only] Tool error: ${error.message}\n`); - throw new McpError(ErrorCode.InternalError, error.message); - } - }); - } - - /** - * Setup error handling - */ - private setupErrorHandling(): void { - this.server.onerror = (error) => { - process.stderr.write(`[MCP Apps Only] Server error: ${error}\n`); - }; - - process.on('SIGINT', async () => { - await this.server.close(); - process.exit(0); - }); - } - - /** - * Start the server - */ - async run(): Promise { - const transport = new StdioServerTransport(); - await this.server.connect(transport); - process.stderr.write('[MCP Apps Only] Server started - Apps only mode\n'); - } -} - -// Start server -const server = new GHLMCPAppsServer(); -server.run().catch((error) => { - process.stderr.write(`Failed to start server: ${error}\n`); - process.exit(1); -}); diff --git a/mcp-diagrams/ghl-mcp-apps-only/src/types/ghl-types.ts b/mcp-diagrams/ghl-mcp-apps-only/src/types/ghl-types.ts deleted file mode 100644 index 0d46517..0000000 --- a/mcp-diagrams/ghl-mcp-apps-only/src/types/ghl-types.ts +++ /dev/null @@ -1,6688 +0,0 @@ -/** - * TypeScript interfaces for GoHighLevel API integration - * Based on official OpenAPI specifications v2021-07-28 (Contacts) and v2021-04-15 (Conversations) - */ - -// Base GHL API Configuration -export interface GHLConfig { - accessToken: string; - baseUrl: string; - version: string; - locationId: string; -} - -// OAuth Token Response -export interface GHLTokenResponse { - access_token: string; - token_type: 'Bearer'; - expires_in: number; - refresh_token: string; - scope: string; - userType: 'Location' | 'Company'; - locationId?: string; - companyId: string; - userId: string; - planId?: string; -} - -// Contact Interfaces - Exact from OpenAPI -export interface GHLContact { - id?: string; - locationId: string; - firstName?: string; - lastName?: string; - name?: string; - email?: string; - emailLowerCase?: string; - phone?: string; - address1?: string; - city?: string; - state?: string; - country?: string; - postalCode?: string; - website?: string; - timezone?: string; - companyName?: string; - source?: string; - tags?: string[]; - customFields?: GHLCustomField[]; - dnd?: boolean; - dndSettings?: GHLDndSettings; - assignedTo?: string; - followers?: string[]; - businessId?: string; - dateAdded?: string; - dateUpdated?: string; - dateOfBirth?: string; - type?: string; - validEmail?: boolean; -} - -// Custom Field Interface -export interface GHLCustomField { - id: string; - key?: string; - field_value: string | string[] | object; -} - -// DND Settings Interface -export interface GHLDndSettings { - Call?: GHLDndSetting; - Email?: GHLDndSetting; - SMS?: GHLDndSetting; - WhatsApp?: GHLDndSetting; - GMB?: GHLDndSetting; - FB?: GHLDndSetting; -} - -export interface GHLDndSetting { - status: 'active' | 'inactive' | 'permanent'; - message?: string; - code?: string; -} - -// Search Contacts Request Body -export interface GHLSearchContactsRequest { - locationId: string; - query?: string; - startAfterId?: string; - startAfter?: number; - limit?: number; - filters?: { - email?: string; - phone?: string; - tags?: string[]; - dateAdded?: { - gte?: string; - lte?: string; - }; - }; -} - -// Search Contacts Response -export interface GHLSearchContactsResponse { - contacts: GHLContact[]; - total: number; -} - -// Create Contact Request -export interface GHLCreateContactRequest { - locationId: string; - firstName?: string; - lastName?: string; - name?: string; - email?: string; - phone?: string; - address1?: string; - city?: string; - state?: string; - country?: string; - postalCode?: string; - website?: string; - timezone?: string; - companyName?: string; - source?: string; - tags?: string[]; - customFields?: GHLCustomField[]; - dnd?: boolean; - dndSettings?: GHLDndSettings; - assignedTo?: string; -} - -// Contact Tags Operations -export interface GHLContactTagsRequest { - tags: string[]; -} - -// Contact Tags Response -export interface GHLContactTagsResponse { - tags: string[]; -} - -// CONVERSATION INTERFACES - Based on Conversations API v2021-04-15 - -// Message Types Enum -export type GHLMessageType = - | 'TYPE_CALL' | 'TYPE_SMS' | 'TYPE_EMAIL' | 'TYPE_SMS_REVIEW_REQUEST' - | 'TYPE_WEBCHAT' | 'TYPE_SMS_NO_SHOW_REQUEST' | 'TYPE_CAMPAIGN_SMS' - | 'TYPE_CAMPAIGN_CALL' | 'TYPE_CAMPAIGN_EMAIL' | 'TYPE_CAMPAIGN_VOICEMAIL' - | 'TYPE_FACEBOOK' | 'TYPE_CAMPAIGN_FACEBOOK' | 'TYPE_CAMPAIGN_MANUAL_CALL' - | 'TYPE_CAMPAIGN_MANUAL_SMS' | 'TYPE_GMB' | 'TYPE_CAMPAIGN_GMB' - | 'TYPE_REVIEW' | 'TYPE_INSTAGRAM' | 'TYPE_WHATSAPP' | 'TYPE_CUSTOM_SMS' - | 'TYPE_CUSTOM_EMAIL' | 'TYPE_CUSTOM_PROVIDER_SMS' | 'TYPE_CUSTOM_PROVIDER_EMAIL' - | 'TYPE_IVR_CALL' | 'TYPE_ACTIVITY_CONTACT' | 'TYPE_ACTIVITY_INVOICE' - | 'TYPE_ACTIVITY_PAYMENT' | 'TYPE_ACTIVITY_OPPORTUNITY' | 'TYPE_LIVE_CHAT' - | 'TYPE_LIVE_CHAT_INFO_MESSAGE' | 'TYPE_ACTIVITY_APPOINTMENT' - | 'TYPE_FACEBOOK_COMMENT' | 'TYPE_INSTAGRAM_COMMENT' | 'TYPE_CUSTOM_CALL' - | 'TYPE_INTERNAL_COMMENT'; - -// Send Message Types -export type GHLSendMessageType = 'SMS' | 'Email' | 'WhatsApp' | 'IG' | 'FB' | 'Custom' | 'Live_Chat'; - -// Message Status -export type GHLMessageStatus = - | 'pending' | 'scheduled' | 'sent' | 'delivered' | 'read' - | 'undelivered' | 'connected' | 'failed' | 'opened' | 'clicked' | 'opt_out'; - -// Message Direction -export type GHLMessageDirection = 'inbound' | 'outbound'; - -// Conversation Interface -export interface GHLConversation { - id: string; - contactId: string; - locationId: string; - lastMessageBody: string; - lastMessageType: GHLMessageType; - type: string; - unreadCount: number; - fullName: string; - contactName: string; - email: string; - phone: string; - assignedTo?: string; - starred?: boolean; - deleted?: boolean; - inbox?: boolean; - lastMessageDate?: string; - dateAdded?: string; - dateUpdated?: string; -} - -// Message Interface -export interface GHLMessage { - id: string; - type: number; - messageType: GHLMessageType; - locationId: string; - contactId: string; - conversationId: string; - dateAdded: string; - body?: string; - direction: GHLMessageDirection; - status: GHLMessageStatus; - contentType: string; - attachments?: string[]; - meta?: GHLMessageMeta; - source?: 'workflow' | 'bulk_actions' | 'campaign' | 'api' | 'app'; - userId?: string; - conversationProviderId?: string; -} - -// Message Meta Interface -export interface GHLMessageMeta { - callDuration?: string; - callStatus?: 'pending' | 'completed' | 'answered' | 'busy' | 'no-answer' | 'failed' | 'canceled' | 'voicemail'; - email?: { - messageIds?: string[]; - }; -} - -// Send Message Request -export interface GHLSendMessageRequest { - type: GHLSendMessageType; - contactId: string; - message?: string; - html?: string; - subject?: string; - attachments?: string[]; - emailFrom?: string; - emailTo?: string; - emailCc?: string[]; - emailBcc?: string[]; - replyMessageId?: string; - templateId?: string; - threadId?: string; - scheduledTimestamp?: number; - conversationProviderId?: string; - emailReplyMode?: 'reply' | 'reply_all'; - fromNumber?: string; - toNumber?: string; - appointmentId?: string; -} - -// Send Message Response -export interface GHLSendMessageResponse { - conversationId: string; - messageId: string; - emailMessageId?: string; - messageIds?: string[]; - msg?: string; -} - -// Search Conversations Request -export interface GHLSearchConversationsRequest { - locationId: string; - contactId?: string; - assignedTo?: string; - followers?: string; - mentions?: string; - query?: string; - sort?: 'asc' | 'desc'; - startAfterDate?: number | number[]; - id?: string; - limit?: number; - lastMessageType?: GHLMessageType; - lastMessageAction?: 'automated' | 'manual'; - lastMessageDirection?: GHLMessageDirection; - status?: 'all' | 'read' | 'unread' | 'starred' | 'recents'; - sortBy?: 'last_manual_message_date' | 'last_message_date' | 'score_profile'; -} - -// Search Conversations Response -export interface GHLSearchConversationsResponse { - conversations: GHLConversation[]; - total: number; -} - -// Get Messages Response -export interface GHLGetMessagesResponse { - lastMessageId: string; - nextPage: boolean; - messages: GHLMessage[]; -} - -// Create Conversation Request -export interface GHLCreateConversationRequest { - locationId: string; - contactId: string; -} - -// Create Conversation Response -export interface GHLCreateConversationResponse { - id: string; - dateUpdated: string; - dateAdded: string; - deleted: boolean; - contactId: string; - locationId: string; - lastMessageDate: string; - assignedTo?: string; -} - -// Update Conversation Request -export interface GHLUpdateConversationRequest { - locationId: string; - unreadCount?: number; - starred?: boolean; - feedback?: object; -} - -// API Response Wrapper -export interface GHLApiResponse { - success: boolean; - data?: T; - error?: { - message: string; - statusCode: number; - details?: any; - }; -} - -// Error Response from API -export interface GHLErrorResponse { - statusCode: number; - message: string | string[]; - error?: string; -} - -// Task Interface -export interface GHLTask { - id?: string; - title: string; - body?: string; - assignedTo?: string; - dueDate: string; - completed: boolean; - contactId: string; -} - -// Note Interface -export interface GHLNote { - id?: string; - body: string; - userId?: string; - contactId: string; - dateAdded?: string; -} - -// Campaign Interface -export interface GHLCampaign { - id: string; - name: string; - status: string; -} - -// Workflow Interface -export interface GHLWorkflow { - id: string; - name: string; - status: string; - eventStartTime?: string; -} - -// Appointment Interface -export interface GHLAppointment { - id: string; - calendarId: string; - status: string; - title: string; - appointmentStatus: string; - assignedUserId: string; - notes?: string; - startTime: string; - endTime: string; - address?: string; - locationId: string; - contactId: string; - groupId?: string; - users?: string[]; - dateAdded: string; - dateUpdated: string; - assignedResources?: string[]; -} - -// Upsert Contact Response -export interface GHLUpsertContactResponse { - contact: GHLContact; - new: boolean; - traceId?: string; -} - -// Bulk Tags Update Response -export interface GHLBulkTagsResponse { - succeeded: boolean; - errorCount: number; - responses: Array<{ - contactId: string; - message: string; - type: 'success' | 'error'; - oldTags?: string[]; - tagsAdded?: string[]; - tagsRemoved?: string[]; - }>; -} - -// Bulk Business Update Response -export interface GHLBulkBusinessResponse { - success: boolean; - ids: string[]; -} - -// Followers Response -export interface GHLFollowersResponse { - followers: string[]; - followersAdded?: string[]; - followersRemoved?: string[]; -} - -// MCP Tool Parameters - Contact Operations -export interface MCPCreateContactParams { - firstName?: string; - lastName?: string; - email: string; - phone?: string; - tags?: string[]; - source?: string; -} - -export interface MCPSearchContactsParams { - query?: string; - email?: string; - phone?: string; - limit?: number; -} - -export interface MCPUpdateContactParams { - contactId: string; - firstName?: string; - lastName?: string; - email?: string; - phone?: string; - tags?: string[]; -} - -export interface MCPAddContactTagsParams { - contactId: string; - tags: string[]; -} - -export interface MCPRemoveContactTagsParams { - contactId: string; - tags: string[]; -} - -// MCP Tool Parameters - Contact Task Management -export interface MCPGetContactTasksParams { - contactId: string; -} - -export interface MCPCreateContactTaskParams { - contactId: string; - title: string; - body?: string; - dueDate: string; // ISO date string - completed?: boolean; - assignedTo?: string; -} - -export interface MCPGetContactTaskParams { - contactId: string; - taskId: string; -} - -export interface MCPUpdateContactTaskParams { - contactId: string; - taskId: string; - title?: string; - body?: string; - dueDate?: string; - completed?: boolean; - assignedTo?: string; -} - -export interface MCPDeleteContactTaskParams { - contactId: string; - taskId: string; -} - -export interface MCPUpdateTaskCompletionParams { - contactId: string; - taskId: string; - completed: boolean; -} - -// MCP Tool Parameters - Contact Note Management -export interface MCPGetContactNotesParams { - contactId: string; -} - -export interface MCPCreateContactNoteParams { - contactId: string; - body: string; - userId?: string; -} - -export interface MCPGetContactNoteParams { - contactId: string; - noteId: string; -} - -export interface MCPUpdateContactNoteParams { - contactId: string; - noteId: string; - body: string; - userId?: string; -} - -export interface MCPDeleteContactNoteParams { - contactId: string; - noteId: string; -} - -// MCP Tool Parameters - Advanced Contact Operations -export interface MCPUpsertContactParams { - firstName?: string; - lastName?: string; - name?: string; - email?: string; - phone?: string; - address?: string; - city?: string; - state?: string; - country?: string; - postalCode?: string; - website?: string; - timezone?: string; - companyName?: string; - tags?: string[]; - customFields?: GHLCustomField[]; - source?: string; - assignedTo?: string; -} - -export interface MCPGetDuplicateContactParams { - email?: string; - phone?: string; -} - -export interface MCPGetContactsByBusinessParams { - businessId: string; - limit?: number; - skip?: number; - query?: string; -} - -// MCP Tool Parameters - Contact Appointments -export interface MCPGetContactAppointmentsParams { - contactId: string; -} - -// MCP Tool Parameters - Bulk Operations -export interface MCPBulkUpdateContactTagsParams { - contactIds: string[]; - tags: string[]; - operation: 'add' | 'remove'; - removeAllTags?: boolean; -} - -export interface MCPBulkUpdateContactBusinessParams { - contactIds: string[]; - businessId?: string; // null to remove from business -} - -// MCP Tool Parameters - Followers Management -export interface MCPAddContactFollowersParams { - contactId: string; - followers: string[]; -} - -export interface MCPRemoveContactFollowersParams { - contactId: string; - followers: string[]; -} - -// MCP Tool Parameters - Campaign Management -export interface MCPAddContactToCampaignParams { - contactId: string; - campaignId: string; -} - -export interface MCPRemoveContactFromCampaignParams { - contactId: string; - campaignId: string; -} - -export interface MCPRemoveContactFromAllCampaignsParams { - contactId: string; -} - -// MCP Tool Parameters - Workflow Management -export interface MCPAddContactToWorkflowParams { - contactId: string; - workflowId: string; - eventStartTime?: string; -} - -export interface MCPRemoveContactFromWorkflowParams { - contactId: string; - workflowId: string; - eventStartTime?: string; -} - -// MCP Tool Parameters - Conversation Operations -export interface MCPSendSMSParams { - contactId: string; - message: string; - fromNumber?: string; -} - -export interface MCPSendEmailParams { - contactId: string; - subject: string; - message?: string; - html?: string; - emailFrom?: string; - attachments?: string[]; - emailCc?: string[]; - emailBcc?: string[]; -} - -export interface MCPSearchConversationsParams { - contactId?: string; - query?: string; - status?: 'all' | 'read' | 'unread' | 'starred'; - limit?: number; - assignedTo?: string; -} - -export interface MCPGetConversationParams { - conversationId: string; - limit?: number; - messageTypes?: string[]; -} - -export interface MCPCreateConversationParams { - contactId: string; -} - -export interface MCPUpdateConversationParams { - conversationId: string; - starred?: boolean; - unreadCount?: number; -} - -// BLOG INTERFACES - Based on Blogs API v2021-07-28 - -// Blog Post Status Enum -export type GHLBlogPostStatus = 'DRAFT' | 'PUBLISHED' | 'SCHEDULED' | 'ARCHIVED'; - -// Blog Post Response Interface -export interface GHLBlogPost { - _id: string; - title: string; - description: string; - imageUrl: string; - imageAltText: string; - urlSlug: string; - canonicalLink?: string; - author: string; // Author ID - publishedAt: string; - updatedAt: string; - status: GHLBlogPostStatus; - categories: string[]; // Array of category IDs - tags?: string[]; - archived: boolean; - rawHTML?: string; // Full HTML content -} - -// Create Blog Post Parameters -export interface GHLCreateBlogPostRequest { - title: string; - locationId: string; - blogId: string; - imageUrl: string; - description: string; - rawHTML: string; - status: GHLBlogPostStatus; - imageAltText: string; - categories: string[]; // Array of category IDs - tags?: string[]; - author: string; // Author ID - urlSlug: string; - canonicalLink?: string; - publishedAt: string; // ISO timestamp -} - -// Update Blog Post Parameters -export interface GHLUpdateBlogPostRequest { - title?: string; - locationId: string; - blogId: string; - imageUrl?: string; - description?: string; - rawHTML?: string; - status?: GHLBlogPostStatus; - imageAltText?: string; - categories?: string[]; - tags?: string[]; - author?: string; - urlSlug?: string; - canonicalLink?: string; - publishedAt?: string; -} - -// Blog Post Create Response -export interface GHLBlogPostCreateResponse { - data: GHLBlogPost; -} - -// Blog Post Update Response -export interface GHLBlogPostUpdateResponse { - updatedBlogPost: GHLBlogPost; -} - -// Blog Post List Response -export interface GHLBlogPostListResponse { - blogs: GHLBlogPost[]; -} - -// Blog Author Interface -export interface GHLBlogAuthor { - _id: string; - name: string; - locationId: string; - updatedAt: string; - canonicalLink: string; -} - -// Authors List Response -export interface GHLBlogAuthorsResponse { - authors: GHLBlogAuthor[]; -} - -// Blog Category Interface -export interface GHLBlogCategory { - _id: string; - label: string; - locationId: string; - updatedAt: string; - canonicalLink: string; - urlSlug: string; -} - -// Categories List Response -export interface GHLBlogCategoriesResponse { - categories: GHLBlogCategory[]; -} - -// Blog Site Interface -export interface GHLBlogSite { - _id: string; - name: string; -} - -// Blog Sites List Response -export interface GHLBlogSitesResponse { - data: GHLBlogSite[]; -} - -// URL Slug Check Response -export interface GHLUrlSlugCheckResponse { - exists: boolean; -} - -// Blog Post Search/List Parameters -export interface GHLGetBlogPostsRequest { - locationId: string; - blogId: string; - limit: number; - offset: number; - searchTerm?: string; - status?: GHLBlogPostStatus; -} - -// Blog Authors Request Parameters -export interface GHLGetBlogAuthorsRequest { - locationId: string; - limit: number; - offset: number; -} - -// Blog Categories Request Parameters -export interface GHLGetBlogCategoriesRequest { - locationId: string; - limit: number; - offset: number; -} - -// Blog Sites Request Parameters -export interface GHLGetBlogSitesRequest { - locationId: string; - skip: number; - limit: number; - searchTerm?: string; -} - -// URL Slug Check Parameters -export interface GHLCheckUrlSlugRequest { - locationId: string; - urlSlug: string; - postId?: string; -} - -// MCP Tool Parameters - Blog Operations -export interface MCPCreateBlogPostParams { - title: string; - blogId: string; - content: string; // Raw HTML content - description: string; - imageUrl: string; - imageAltText: string; - urlSlug: string; - author: string; // Author ID - categories: string[]; // Array of category IDs - tags?: string[]; - status?: GHLBlogPostStatus; - canonicalLink?: string; - publishedAt?: string; // ISO timestamp, defaults to now if not provided -} - -export interface MCPUpdateBlogPostParams { - postId: string; - blogId: string; - title?: string; - content?: string; - description?: string; - imageUrl?: string; - imageAltText?: string; - urlSlug?: string; - author?: string; - categories?: string[]; - tags?: string[]; - status?: GHLBlogPostStatus; - canonicalLink?: string; - publishedAt?: string; -} - -export interface MCPGetBlogPostsParams { - blogId: string; - limit?: number; - offset?: number; - searchTerm?: string; - status?: GHLBlogPostStatus; -} - -export interface MCPGetBlogSitesParams { - skip?: number; - limit?: number; - searchTerm?: string; -} - -export interface MCPGetBlogAuthorsParams { - limit?: number; - offset?: number; -} - -export interface MCPGetBlogCategoriesParams { - limit?: number; - offset?: number; -} - -export interface MCPCheckUrlSlugParams { - urlSlug: string; - postId?: string; -} - -// OPPORTUNITIES API INTERFACES - Based on Opportunities API v2021-07-28 - -// Opportunity Status Enum -export type GHLOpportunityStatus = 'open' | 'won' | 'lost' | 'abandoned' | 'all'; - -// Opportunity Contact Response Interface -export interface GHLOpportunityContact { - id: string; - name: string; - companyName?: string; - email?: string; - phone?: string; - tags?: string[]; -} - -// Custom Field Response Interface -export interface GHLCustomFieldResponse { - id: string; - fieldValue: string | object | string[] | object[]; -} - -// Opportunity Response Interface -export interface GHLOpportunity { - id: string; - name: string; - monetaryValue?: number; - pipelineId: string; - pipelineStageId: string; - assignedTo?: string; - status: GHLOpportunityStatus; - source?: string; - lastStatusChangeAt?: string; - lastStageChangeAt?: string; - lastActionDate?: string; - indexVersion?: number; - createdAt: string; - updatedAt: string; - contactId: string; - locationId: string; - contact?: GHLOpportunityContact; - notes?: string[]; - tasks?: string[]; - calendarEvents?: string[]; - customFields?: GHLCustomFieldResponse[]; - followers?: string[][]; -} - -// Search Meta Response Interface -export interface GHLOpportunitySearchMeta { - total: number; - nextPageUrl?: string; - startAfterId?: string; - startAfter?: number; - currentPage?: number; - nextPage?: number; - prevPage?: number; -} - -// Search Opportunities Response -export interface GHLSearchOpportunitiesResponse { - opportunities: GHLOpportunity[]; - meta: GHLOpportunitySearchMeta; - aggregations?: object; -} - -// Pipeline Stage Interface -export interface GHLPipelineStage { - id: string; - name: string; - position: number; -} - -// Pipeline Interface -export interface GHLPipeline { - id: string; - name: string; - stages: GHLPipelineStage[]; - showInFunnel: boolean; - showInPieChart: boolean; - locationId: string; -} - -// Get Pipelines Response -export interface GHLGetPipelinesResponse { - pipelines: GHLPipeline[]; -} - -// Search Opportunities Request -export interface GHLSearchOpportunitiesRequest { - q?: string; // query string - location_id: string; // Note: underscore format as per API - pipeline_id?: string; - pipeline_stage_id?: string; - contact_id?: string; - status?: GHLOpportunityStatus; - assigned_to?: string; - campaignId?: string; - id?: string; - order?: string; - endDate?: string; // mm-dd-yyyy format - startAfter?: string; - startAfterId?: string; - date?: string; // mm-dd-yyyy format - country?: string; - page?: number; - limit?: number; - getTasks?: boolean; - getNotes?: boolean; - getCalendarEvents?: boolean; -} - -// Create Opportunity Request -export interface GHLCreateOpportunityRequest { - pipelineId: string; - locationId: string; - name: string; - pipelineStageId?: string; - status: GHLOpportunityStatus; - contactId: string; - monetaryValue?: number; - assignedTo?: string; - customFields?: GHLCustomFieldInput[]; -} - -// Update Opportunity Request -export interface GHLUpdateOpportunityRequest { - pipelineId?: string; - name?: string; - pipelineStageId?: string; - status?: GHLOpportunityStatus; - monetaryValue?: number; - assignedTo?: string; - customFields?: GHLCustomFieldInput[]; -} - -// Update Opportunity Status Request -export interface GHLUpdateOpportunityStatusRequest { - status: GHLOpportunityStatus; -} - -// Upsert Opportunity Request -export interface GHLUpsertOpportunityRequest { - pipelineId: string; - locationId: string; - contactId: string; - name?: string; - status?: GHLOpportunityStatus; - pipelineStageId?: string; - monetaryValue?: number; - assignedTo?: string; -} - -// Upsert Opportunity Response -export interface GHLUpsertOpportunityResponse { - opportunity: GHLOpportunity; - new: boolean; -} - -// Custom Field Input Interfaces (reuse existing ones) -export interface GHLCustomFieldInput { - id?: string; - key?: string; - field_value: string | string[] | object; -} - -// MCP Tool Parameters - Opportunity Operations -export interface MCPSearchOpportunitiesParams { - query?: string; - pipelineId?: string; - pipelineStageId?: string; - contactId?: string; - status?: GHLOpportunityStatus; - assignedTo?: string; - campaignId?: string; - country?: string; - startDate?: string; // mm-dd-yyyy - endDate?: string; // mm-dd-yyyy - limit?: number; - page?: number; - includeTasks?: boolean; - includeNotes?: boolean; - includeCalendarEvents?: boolean; -} - -export interface MCPCreateOpportunityParams { - name: string; - pipelineId: string; - contactId: string; - status?: GHLOpportunityStatus; - pipelineStageId?: string; - monetaryValue?: number; - assignedTo?: string; - customFields?: GHLCustomFieldInput[]; -} - -export interface MCPUpdateOpportunityParams { - opportunityId: string; - name?: string; - pipelineId?: string; - pipelineStageId?: string; - status?: GHLOpportunityStatus; - monetaryValue?: number; - assignedTo?: string; - customFields?: GHLCustomFieldInput[]; -} - -export interface MCPUpsertOpportunityParams { - pipelineId: string; - contactId: string; - name?: string; - status?: GHLOpportunityStatus; - pipelineStageId?: string; - monetaryValue?: number; - assignedTo?: string; -} - -export interface MCPAddOpportunityFollowersParams { - opportunityId: string; - followers: string[]; -} - -export interface MCPRemoveOpportunityFollowersParams { - opportunityId: string; - followers: string[]; -} - -// CALENDAR & APPOINTMENTS API INTERFACES - Based on Calendar API v2021-04-15 - -// Calendar Group Interfaces -export interface GHLCalendarGroup { - id: string; - locationId: string; - name: string; - description: string; - slug: string; - isActive: boolean; -} - -export interface GHLGetCalendarGroupsResponse { - groups: GHLCalendarGroup[]; -} - -export interface GHLCreateCalendarGroupRequest { - locationId: string; - name: string; - description: string; - slug: string; - isActive?: boolean; -} - -// Meeting Location Configuration -export interface GHLLocationConfiguration { - kind: 'custom' | 'zoom_conference' | 'google_conference' | 'inbound_call' | 'outbound_call' | 'physical' | 'booker' | 'ms_teams_conference'; - location?: string; - meetingId?: string; -} - -// Team Member Configuration -export interface GHLTeamMember { - userId: string; - priority?: number; // 0, 0.5, 1 - isPrimary?: boolean; - locationConfigurations?: GHLLocationConfiguration[]; -} - -// Calendar Hour Configuration -export interface GHLHour { - openHour: number; - openMinute: number; - closeHour: number; - closeMinute: number; -} - -export interface GHLOpenHour { - daysOfTheWeek: number[]; // 0-6 - hours: GHLHour[]; -} - -// Calendar Availability -export interface GHLAvailability { - date: string; // YYYY-MM-DDTHH:mm:ss.sssZ format - hours: GHLHour[]; - deleted?: boolean; - id?: string; -} - -// Calendar Interfaces -export interface GHLCalendar { - id: string; - locationId: string; - groupId?: string; - name: string; - description?: string; - slug?: string; - widgetSlug?: string; - calendarType: 'round_robin' | 'event' | 'class_booking' | 'collective' | 'service_booking' | 'personal'; - widgetType?: 'default' | 'classic'; - eventTitle?: string; - eventColor?: string; - isActive?: boolean; - teamMembers?: GHLTeamMember[]; - locationConfigurations?: GHLLocationConfiguration[]; - slotDuration?: number; - slotDurationUnit?: 'mins' | 'hours'; - slotInterval?: number; - slotIntervalUnit?: 'mins' | 'hours'; - slotBuffer?: number; - slotBufferUnit?: 'mins' | 'hours'; - preBuffer?: number; - preBufferUnit?: 'mins' | 'hours'; - appoinmentPerSlot?: number; - appoinmentPerDay?: number; - allowBookingAfter?: number; - allowBookingAfterUnit?: 'hours' | 'days' | 'weeks' | 'months'; - allowBookingFor?: number; - allowBookingForUnit?: 'days' | 'weeks' | 'months'; - openHours?: GHLOpenHour[]; - availabilities?: GHLAvailability[]; - autoConfirm?: boolean; - allowReschedule?: boolean; - allowCancellation?: boolean; - formId?: string; - notes?: string; -} - -export interface GHLGetCalendarsResponse { - calendars: GHLCalendar[]; -} - -export interface GHLCreateCalendarRequest { - locationId: string; - groupId?: string; - name: string; - description?: string; - slug?: string; - calendarType: 'round_robin' | 'event' | 'class_booking' | 'collective' | 'service_booking' | 'personal'; - teamMembers?: GHLTeamMember[]; - locationConfigurations?: GHLLocationConfiguration[]; - slotDuration?: number; - slotDurationUnit?: 'mins' | 'hours'; - autoConfirm?: boolean; - allowReschedule?: boolean; - allowCancellation?: boolean; - openHours?: GHLOpenHour[]; - isActive?: boolean; -} - -export interface GHLUpdateCalendarRequest { - name?: string; - description?: string; - groupId?: string; - teamMembers?: GHLTeamMember[]; - locationConfigurations?: GHLLocationConfiguration[]; - slotDuration?: number; - slotDurationUnit?: 'mins' | 'hours'; - autoConfirm?: boolean; - allowReschedule?: boolean; - allowCancellation?: boolean; - openHours?: GHLOpenHour[]; - availabilities?: GHLAvailability[]; - isActive?: boolean; -} - -// Calendar Event/Appointment Interfaces -export interface GHLCalendarEvent { - id: string; - title: string; - calendarId: string; - locationId: string; - contactId: string; - groupId?: string; - appointmentStatus: 'new' | 'confirmed' | 'cancelled' | 'showed' | 'noshow' | 'invalid'; - assignedUserId: string; - users?: string[]; - address?: string; - notes?: string; - startTime: string; - endTime: string; - dateAdded: string; - dateUpdated: string; - isRecurring?: boolean; - rrule?: string; - masterEventId?: string; - assignedResources?: string[]; -} - -export interface GHLGetCalendarEventsResponse { - events: GHLCalendarEvent[]; -} - -export interface GHLGetCalendarEventsRequest { - locationId: string; - userId?: string; - calendarId?: string; - groupId?: string; - startTime: string; // milliseconds - endTime: string; // milliseconds -} - -// Free Slots Interface -export interface GHLFreeSlot { - slots: string[]; -} - -export interface GHLGetFreeSlotsResponse { - [date: string]: GHLFreeSlot; // Date as key -} - -export interface GHLGetFreeSlotsRequest { - calendarId: string; - startDate: number; // milliseconds - endDate: number; // milliseconds - timezone?: string; - userId?: string; - userIds?: string[]; - enableLookBusy?: boolean; -} - -// Appointment Management -export interface GHLCreateAppointmentRequest { - calendarId: string; - locationId: string; - contactId: string; - startTime: string; // ISO format - endTime?: string; // ISO format - title?: string; - appointmentStatus?: 'new' | 'confirmed' | 'cancelled' | 'showed' | 'noshow' | 'invalid'; - assignedUserId?: string; - address?: string; - meetingLocationType?: 'custom' | 'zoom' | 'gmeet' | 'phone' | 'address' | 'ms_teams' | 'google'; - meetingLocationId?: string; - ignoreDateRange?: boolean; - toNotify?: boolean; - ignoreFreeSlotValidation?: boolean; - rrule?: string; // Recurring rule -} - -export interface GHLUpdateAppointmentRequest { - title?: string; - appointmentStatus?: 'new' | 'confirmed' | 'cancelled' | 'showed' | 'noshow' | 'invalid'; - assignedUserId?: string; - address?: string; - startTime?: string; - endTime?: string; - meetingLocationType?: 'custom' | 'zoom' | 'gmeet' | 'phone' | 'address' | 'ms_teams' | 'google'; - toNotify?: boolean; - ignoreFreeSlotValidation?: boolean; -} - -// Block Slot Management -export interface GHLCreateBlockSlotRequest { - calendarId?: string; - locationId: string; - startTime: string; - endTime: string; - title?: string; - assignedUserId?: string; -} - -export interface GHLUpdateBlockSlotRequest { - calendarId?: string; - startTime?: string; - endTime?: string; - title?: string; - assignedUserId?: string; -} - -export interface GHLBlockSlotResponse { - id: string; - locationId: string; - title: string; - startTime: string; - endTime: string; - calendarId?: string; - assignedUserId?: string; -} - -// MCP Tool Parameters -export interface MCPGetCalendarsParams { - groupId?: string; - showDrafted?: boolean; -} - -export interface MCPCreateCalendarParams { - name: string; - description?: string; - calendarType: 'round_robin' | 'event' | 'class_booking' | 'collective' | 'service_booking' | 'personal'; - groupId?: string; - teamMembers?: GHLTeamMember[]; - slotDuration?: number; - slotDurationUnit?: 'mins' | 'hours'; - autoConfirm?: boolean; - allowReschedule?: boolean; - allowCancellation?: boolean; - isActive?: boolean; -} - -export interface MCPUpdateCalendarParams { - calendarId: string; - name?: string; - description?: string; - groupId?: string; - teamMembers?: GHLTeamMember[]; - slotDuration?: number; - autoConfirm?: boolean; - allowReschedule?: boolean; - allowCancellation?: boolean; - isActive?: boolean; -} - -export interface MCPGetCalendarEventsParams { - userId?: string; - calendarId?: string; - groupId?: string; - startTime: string; // milliseconds or ISO date - endTime: string; // milliseconds or ISO date -} - -export interface MCPGetFreeSlotsParams { - calendarId: string; - startDate: string; // YYYY-MM-DD or milliseconds - endDate: string; // YYYY-MM-DD or milliseconds - timezone?: string; - userId?: string; -} - -export interface MCPCreateAppointmentParams { - calendarId: string; - contactId: string; - startTime: string; // ISO format - endTime?: string; // ISO format - title?: string; - appointmentStatus?: 'new' | 'confirmed'; - assignedUserId?: string; - address?: string; - meetingLocationType?: 'custom' | 'zoom' | 'gmeet' | 'phone' | 'address'; - ignoreDateRange?: boolean; - toNotify?: boolean; -} - -export interface MCPUpdateAppointmentParams { - appointmentId: string; - title?: string; - appointmentStatus?: 'new' | 'confirmed' | 'cancelled' | 'showed' | 'noshow'; - assignedUserId?: string; - address?: string; - startTime?: string; - endTime?: string; - toNotify?: boolean; -} - -export interface MCPCreateBlockSlotParams { - calendarId?: string; - startTime: string; - endTime: string; - title?: string; - assignedUserId?: string; -} - -export interface MCPUpdateBlockSlotParams { - blockSlotId: string; - calendarId?: string; - startTime?: string; - endTime?: string; - title?: string; - assignedUserId?: string; -} - -// EMAIL API INTERFACES - -export interface GHLEmailCampaign { - id: string; - name: string; - status: string; - createdAt: string; - updatedAt: string; -} - -export interface GHLEmailCampaignsResponse { - schedules: GHLEmailCampaign[]; - total: number; -} - -export interface GHLEmailTemplate { - id: string; - name: string; - templateType: string; - lastUpdated: string; - dateAdded: string; - previewUrl: string; -} - -// MCP Tool Parameters - Email Operations -export interface MCPGetEmailCampaignsParams { - status?: 'active' | 'pause' | 'complete' | 'cancelled' | 'retry' | 'draft' | 'resend-scheduled'; - limit?: number; - offset?: number; -} - -export interface MCPCreateEmailTemplateParams { - title: string; - html: string; - isPlainText?: boolean; -} - -export interface MCPGetEmailTemplatesParams { - limit?: number; - offset?: number; -} - -export interface MCPUpdateEmailTemplateParams { - templateId: string; - html: string; - previewText?: string; -} - -export interface MCPDeleteEmailTemplateParams { - templateId: string; -} - -// LOCATION API INTERFACES - Based on Locations API v2021-07-28 - -// Location Settings Schema -export interface GHLLocationSettings { - allowDuplicateContact?: boolean; - allowDuplicateOpportunity?: boolean; - allowFacebookNameMerge?: boolean; - disableContactTimezone?: boolean; -} - -// Location Social Schema -export interface GHLLocationSocial { - facebookUrl?: string; - googlePlus?: string; - linkedIn?: string; - foursquare?: string; - twitter?: string; - yelp?: string; - instagram?: string; - youtube?: string; - pinterest?: string; - blogRss?: string; - googlePlacesId?: string; -} - -// Location Business Schema -export interface GHLLocationBusiness { - name?: string; - address?: string; - city?: string; - state?: string; - country?: string; - postalCode?: string; - website?: string; - timezone?: string; - logoUrl?: string; -} - -// Location Prospect Info -export interface GHLLocationProspectInfo { - firstName: string; - lastName: string; - email: string; -} - -// Twilio Configuration -export interface GHLLocationTwilio { - sid: string; - authToken: string; -} - -// Mailgun Configuration -export interface GHLLocationMailgun { - apiKey: string; - domain: string; -} - -// Snapshot Configuration -export interface GHLLocationSnapshot { - id: string; - override?: boolean; -} - -// Basic Location Schema -export interface GHLLocation { - id: string; - name: string; - phone?: string; - email?: string; - address?: string; - city?: string; - state?: string; - country?: string; - postalCode?: string; - website?: string; - timezone?: string; - settings?: GHLLocationSettings; - social?: GHLLocationSocial; -} - -// Detailed Location Schema -export interface GHLLocationDetailed { - id: string; - companyId: string; - name: string; - domain?: string; - address?: string; - city?: string; - state?: string; - logoUrl?: string; - country?: string; - postalCode?: string; - website?: string; - timezone?: string; - firstName?: string; - lastName?: string; - email?: string; - phone?: string; - business?: GHLLocationBusiness; - social?: GHLLocationSocial; - settings?: GHLLocationSettings; - reseller?: object; -} - -// Location Search Response -export interface GHLLocationSearchResponse { - locations: GHLLocation[]; -} - -// Location Details Response -export interface GHLLocationDetailsResponse { - location: GHLLocationDetailed; -} - -// Create Location Request -export interface GHLCreateLocationRequest { - name: string; - companyId: string; - phone?: string; - address?: string; - city?: string; - state?: string; - country?: string; - postalCode?: string; - website?: string; - timezone?: string; - prospectInfo?: GHLLocationProspectInfo; - settings?: GHLLocationSettings; - social?: GHLLocationSocial; - twilio?: GHLLocationTwilio; - mailgun?: GHLLocationMailgun; - snapshotId?: string; -} - -// Update Location Request -export interface GHLUpdateLocationRequest { - name?: string; - companyId: string; - phone?: string; - address?: string; - city?: string; - state?: string; - country?: string; - postalCode?: string; - website?: string; - timezone?: string; - prospectInfo?: GHLLocationProspectInfo; - settings?: GHLLocationSettings; - social?: GHLLocationSocial; - twilio?: GHLLocationTwilio; - mailgun?: GHLLocationMailgun; - snapshot?: GHLLocationSnapshot; -} - -// Location Delete Response -export interface GHLLocationDeleteResponse { - success: boolean; - message: string; -} - -// LOCATION TAGS INTERFACES - -// Location Tag Schema -export interface GHLLocationTag { - id: string; - name: string; - locationId: string; -} - -// Location Tags Response -export interface GHLLocationTagsResponse { - tags: GHLLocationTag[]; -} - -// Location Tag Response -export interface GHLLocationTagResponse { - tag: GHLLocationTag; -} - -// Tag Create/Update Request -export interface GHLLocationTagRequest { - name: string; -} - -// Tag Delete Response -export interface GHLLocationTagDeleteResponse { - succeded: boolean; -} - -// LOCATION TASKS INTERFACES - -// Task Search Parameters -export interface GHLLocationTaskSearchRequest { - contactId?: string[]; - completed?: boolean; - assignedTo?: string[]; - query?: string; - limit?: number; - skip?: number; - businessId?: string; -} - -// Task Search Response -export interface GHLLocationTaskSearchResponse { - tasks: any[]; -} - -// CUSTOM FIELDS INTERFACES - -// Text Box List Options -export interface GHLCustomFieldTextBoxOption { - label: string; - prefillValue?: string; -} - -// Custom Field Schema -export interface GHLLocationCustomField { - id: string; - name: string; - fieldKey: string; - placeholder?: string; - dataType: string; - position: number; - picklistOptions?: string[]; - picklistImageOptions?: string[]; - isAllowedCustomOption?: boolean; - isMultiFileAllowed?: boolean; - maxFileLimit?: number; - locationId: string; - model: 'contact' | 'opportunity'; -} - -// Custom Fields List Response -export interface GHLLocationCustomFieldsResponse { - customFields: GHLLocationCustomField[]; -} - -// Custom Field Response -export interface GHLLocationCustomFieldResponse { - customField: GHLLocationCustomField; -} - -// Create Custom Field Request -export interface GHLCreateCustomFieldRequest { - name: string; - dataType: string; - placeholder?: string; - acceptedFormat?: string[]; - isMultipleFile?: boolean; - maxNumberOfFiles?: number; - textBoxListOptions?: GHLCustomFieldTextBoxOption[]; - position?: number; - model?: 'contact' | 'opportunity'; -} - -// Update Custom Field Request -export interface GHLUpdateCustomFieldRequest { - name: string; - placeholder?: string; - acceptedFormat?: string[]; - isMultipleFile?: boolean; - maxNumberOfFiles?: number; - textBoxListOptions?: GHLCustomFieldTextBoxOption[]; - position?: number; - model?: 'contact' | 'opportunity'; -} - -// Custom Field Delete Response -export interface GHLCustomFieldDeleteResponse { - succeded: boolean; -} - -// File Upload Body -export interface GHLFileUploadRequest { - id: string; - maxFiles?: string; -} - -// File Upload Response -export interface GHLFileUploadResponse { - uploadedFiles: { [fileName: string]: string }; - meta: any[]; -} - -// CUSTOM VALUES INTERFACES - -// Custom Value Schema -export interface GHLLocationCustomValue { - id: string; - name: string; - fieldKey: string; - value: string; - locationId: string; -} - -// Custom Values Response -export interface GHLLocationCustomValuesResponse { - customValues: GHLLocationCustomValue[]; -} - -// Custom Value Response -export interface GHLLocationCustomValueResponse { - customValue: GHLLocationCustomValue; -} - -// Custom Value Request -export interface GHLCustomValueRequest { - name: string; - value: string; -} - -// Custom Value Delete Response -export interface GHLCustomValueDeleteResponse { - succeded: boolean; -} - -// TEMPLATES INTERFACES - -// SMS Template Schema -export interface GHLSmsTemplate { - body: string; - attachments: any[]; -} - -// Email Template Schema -export interface GHLEmailTemplateContent { - subject: string; - attachments: any[]; - html: string; -} - -// Template Response Schema (SMS) -export interface GHLSmsTemplateResponse { - id: string; - name: string; - type: 'sms'; - template: GHLSmsTemplate; - dateAdded: string; - locationId: string; - urlAttachments: string[]; -} - -// Template Response Schema (Email) -export interface GHLEmailTemplateResponse { - id: string; - name: string; - type: 'email'; - dateAdded: string; - template: GHLEmailTemplateContent; - locationId: string; -} - -// Templates List Response -export interface GHLLocationTemplatesResponse { - templates: (GHLSmsTemplateResponse | GHLEmailTemplateResponse)[]; - totalCount: number; -} - -// MCP TOOL PARAMETERS - Location Operations - -export interface MCPSearchLocationsParams { - companyId?: string; - skip?: number; - limit?: number; - order?: 'asc' | 'desc'; - email?: string; -} - -export interface MCPGetLocationParams { - locationId: string; -} - -export interface MCPCreateLocationParams { - name: string; - companyId: string; - phone?: string; - address?: string; - city?: string; - state?: string; - country?: string; - postalCode?: string; - website?: string; - timezone?: string; - prospectInfo?: GHLLocationProspectInfo; - settings?: GHLLocationSettings; - social?: GHLLocationSocial; - twilio?: GHLLocationTwilio; - mailgun?: GHLLocationMailgun; - snapshotId?: string; -} - -export interface MCPUpdateLocationParams { - locationId: string; - name?: string; - companyId: string; - phone?: string; - address?: string; - city?: string; - state?: string; - country?: string; - postalCode?: string; - website?: string; - timezone?: string; - prospectInfo?: GHLLocationProspectInfo; - settings?: GHLLocationSettings; - social?: GHLLocationSocial; - twilio?: GHLLocationTwilio; - mailgun?: GHLLocationMailgun; - snapshot?: GHLLocationSnapshot; -} - -export interface MCPDeleteLocationParams { - locationId: string; - deleteTwilioAccount: boolean; -} - -// Location Tags MCP Parameters -export interface MCPGetLocationTagsParams { - locationId: string; -} - -export interface MCPCreateLocationTagParams { - locationId: string; - name: string; -} - -export interface MCPGetLocationTagParams { - locationId: string; - tagId: string; -} - -export interface MCPUpdateLocationTagParams { - locationId: string; - tagId: string; - name: string; -} - -export interface MCPDeleteLocationTagParams { - locationId: string; - tagId: string; -} - -// Location Tasks MCP Parameters -export interface MCPSearchLocationTasksParams { - locationId: string; - contactId?: string[]; - completed?: boolean; - assignedTo?: string[]; - query?: string; - limit?: number; - skip?: number; - businessId?: string; -} - -// Custom Fields MCP Parameters -export interface MCPGetCustomFieldsParams { - locationId: string; - model?: 'contact' | 'opportunity' | 'all'; -} - -export interface MCPCreateCustomFieldParams { - locationId: string; - name: string; - dataType: string; - placeholder?: string; - acceptedFormat?: string[]; - isMultipleFile?: boolean; - maxNumberOfFiles?: number; - textBoxListOptions?: GHLCustomFieldTextBoxOption[]; - position?: number; - model?: 'contact' | 'opportunity'; -} - -export interface MCPGetCustomFieldParams { - locationId: string; - customFieldId: string; -} - -export interface MCPUpdateCustomFieldParams { - locationId: string; - customFieldId: string; - name: string; - placeholder?: string; - acceptedFormat?: string[]; - isMultipleFile?: boolean; - maxNumberOfFiles?: number; - textBoxListOptions?: GHLCustomFieldTextBoxOption[]; - position?: number; - model?: 'contact' | 'opportunity'; -} - -export interface MCPDeleteCustomFieldParams { - locationId: string; - customFieldId: string; -} - -export interface MCPUploadCustomFieldFileParams { - locationId: string; - id: string; - maxFiles?: string; -} - -// Custom Values MCP Parameters -export interface MCPGetCustomValuesParams { - locationId: string; -} - -export interface MCPCreateCustomValueParams { - locationId: string; - name: string; - value: string; -} - -export interface MCPGetCustomValueParams { - locationId: string; - customValueId: string; -} - -export interface MCPUpdateCustomValueParams { - locationId: string; - customValueId: string; - name: string; - value: string; -} - -export interface MCPDeleteCustomValueParams { - locationId: string; - customValueId: string; -} - -// Templates MCP Parameters -export interface MCPGetLocationTemplatesParams { - locationId: string; - originId: string; - deleted?: boolean; - skip?: number; - limit?: number; - type?: 'sms' | 'email' | 'whatsapp'; -} - -export interface MCPDeleteLocationTemplateParams { - locationId: string; - templateId: string; -} - -// Timezones MCP Parameters -export interface MCPGetTimezonesParams { - locationId?: string; -} - -// EMAIL ISV (VERIFICATION) API INTERFACES - Based on Email ISV API - -// Email Verification Request Body -export interface GHLEmailVerificationRequest { - type: 'email' | 'contact'; - verify: string; // email address or contact ID -} - -// Lead Connector Recommendation -export interface GHLLeadConnectorRecommendation { - isEmailValid: boolean; -} - -// Email Verification Success Response -export interface GHLEmailVerifiedResponse { - reason?: string[]; - result: 'deliverable' | 'undeliverable' | 'do_not_send' | 'unknown' | 'catch_all'; - risk: 'high' | 'low' | 'medium' | 'unknown'; - address: string; - leadconnectorRecomendation: GHLLeadConnectorRecommendation; -} - -// Email Verification Failed Response -export interface GHLEmailNotVerifiedResponse { - verified: false; - message: string; - address: string; -} - -// Combined Email Verification Response -export type GHLEmailVerificationResponse = GHLEmailVerifiedResponse | GHLEmailNotVerifiedResponse; - -// MCP Tool Parameters - Email ISV Operations -export interface MCPVerifyEmailParams { - locationId: string; - type: 'email' | 'contact'; - verify: string; // email address or contact ID -} - -// ADDITIONAL CONVERSATIONS API INTERFACES - Comprehensive Coverage - -// Email Message Interfaces -export interface GHLEmailMessage { - id: string; - altId?: string; - threadId: string; - locationId: string; - contactId: string; - conversationId: string; - dateAdded: string; - subject?: string; - body: string; - direction: GHLMessageDirection; - status: GHLMessageStatus; - contentType: string; - attachments?: string[]; - provider?: string; - from: string; - to: string[]; - cc?: string[]; - bcc?: string[]; - replyToMessageId?: string; - source?: 'workflow' | 'bulk_actions' | 'campaign' | 'api' | 'app'; - conversationProviderId?: string; -} - -// File Upload Interfaces -export interface GHLUploadFilesRequest { - conversationId: string; - locationId: string; - attachmentUrls: string[]; -} - -export interface GHLUploadFilesResponse { - uploadedFiles: { [fileName: string]: string }; -} - -export interface GHLUploadFilesError { - status: number; - message: string; -} - -// Message Status Update Interfaces -export interface GHLUpdateMessageStatusRequest { - status: 'delivered' | 'failed' | 'pending' | 'read'; - error?: { - code: string; - type: string; - message: string; - }; - emailMessageId?: string; - recipients?: string[]; -} - -// Inbound/Outbound Message Interfaces -export interface GHLProcessInboundMessageRequest { - type: 'SMS' | 'Email' | 'WhatsApp' | 'GMB' | 'IG' | 'FB' | 'Custom' | 'WebChat' | 'Live_Chat' | 'Call'; - attachments?: string[]; - message?: string; - conversationId: string; - conversationProviderId: string; - html?: string; - subject?: string; - emailFrom?: string; - emailTo?: string; - emailCc?: string[]; - emailBcc?: string[]; - emailMessageId?: string; - altId?: string; - direction?: 'outbound' | 'inbound'; - date?: string; - call?: { - to: string; - from: string; - status: 'pending' | 'completed' | 'answered' | 'busy' | 'no-answer' | 'failed' | 'canceled' | 'voicemail'; - }; -} - -export interface GHLProcessOutboundMessageRequest { - type: 'Call'; - attachments?: string[]; - conversationId: string; - conversationProviderId: string; - altId?: string; - date?: string; - call: { - to: string; - from: string; - status: 'pending' | 'completed' | 'answered' | 'busy' | 'no-answer' | 'failed' | 'canceled' | 'voicemail'; - }; -} - -export interface GHLProcessMessageResponse { - success: boolean; - conversationId: string; - messageId: string; - message: string; - contactId?: string; - dateAdded?: string; - emailMessageId?: string; -} - -// Call Recording & Transcription Interfaces -export interface GHLMessageRecordingResponse { - // Binary audio data response - typically audio/x-wav - audioData: ArrayBuffer | Buffer; - contentType: string; - contentDisposition: string; -} - -export interface GHLMessageTranscription { - mediaChannel: number; - sentenceIndex: number; - startTime: number; - endTime: number; - transcript: string; - confidence: number; -} - -export interface GHLMessageTranscriptionResponse { - transcriptions: GHLMessageTranscription[]; -} - -// Live Chat Typing Interfaces -export interface GHLLiveChatTypingRequest { - locationId: string; - isTyping: boolean; - visitorId: string; - conversationId: string; -} - -export interface GHLLiveChatTypingResponse { - success: boolean; -} - -// Scheduled Message Cancellation Interfaces -export interface GHLCancelScheduledResponse { - status: number; - message: string; -} - -// MCP Tool Parameters for new conversation endpoints - -export interface MCPGetEmailMessageParams { - emailMessageId: string; -} - -export interface MCPGetMessageParams { - messageId: string; -} - -export interface MCPUploadMessageAttachmentsParams { - conversationId: string; - attachmentUrls: string[]; -} - -export interface MCPUpdateMessageStatusParams { - messageId: string; - status: 'delivered' | 'failed' | 'pending' | 'read'; - error?: { - code: string; - type: string; - message: string; - }; - emailMessageId?: string; - recipients?: string[]; -} - -export interface MCPAddInboundMessageParams { - type: 'SMS' | 'Email' | 'WhatsApp' | 'GMB' | 'IG' | 'FB' | 'Custom' | 'WebChat' | 'Live_Chat' | 'Call'; - conversationId: string; - conversationProviderId: string; - message?: string; - attachments?: string[]; - html?: string; - subject?: string; - emailFrom?: string; - emailTo?: string; - emailCc?: string[]; - emailBcc?: string[]; - emailMessageId?: string; - altId?: string; - date?: string; - call?: { - to: string; - from: string; - status: 'pending' | 'completed' | 'answered' | 'busy' | 'no-answer' | 'failed' | 'canceled' | 'voicemail'; - }; -} - -export interface MCPAddOutboundCallParams { - conversationId: string; - conversationProviderId: string; - to: string; - from: string; - status: 'pending' | 'completed' | 'answered' | 'busy' | 'no-answer' | 'failed' | 'canceled' | 'voicemail'; - attachments?: string[]; - altId?: string; - date?: string; -} - -export interface MCPGetMessageRecordingParams { - messageId: string; -} - -export interface MCPGetMessageTranscriptionParams { - messageId: string; -} - -export interface MCPDownloadTranscriptionParams { - messageId: string; -} - -export interface MCPCancelScheduledMessageParams { - messageId: string; -} - -export interface MCPCancelScheduledEmailParams { - emailMessageId: string; -} - -export interface MCPLiveChatTypingParams { - visitorId: string; - conversationId: string; - isTyping: boolean; -} - -export interface MCPDeleteConversationParams { - conversationId: string; -} - -// SOCIAL MEDIA POSTING API INTERFACES - Based on Social Media Posting API - -// Platform Types -export type GHLSocialPlatform = 'google' | 'facebook' | 'instagram' | 'linkedin' | 'twitter' | 'tiktok' | 'tiktok-business'; -export type GHLPostStatus = 'in_progress' | 'draft' | 'failed' | 'published' | 'scheduled' | 'in_review' | 'notification_sent' | 'deleted'; -export type GHLPostType = 'post' | 'story' | 'reel'; -export type GHLPostSource = 'composer' | 'csv' | 'recurring' | 'review' | 'rss'; -export type GHLCSVStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'in_review' | 'importing' | 'deleted'; -export type GHLAccountType = 'page' | 'group' | 'profile' | 'location' | 'business'; -export type GHLGMBEventType = 'STANDARD' | 'EVENT' | 'OFFER'; -export type GHLGMBActionType = 'none' | 'order' | 'book' | 'shop' | 'learn_more' | 'call' | 'sign_up'; -export type GHLTikTokPrivacyLevel = 'PUBLIC_TO_EVERYONE' | 'MUTUAL_FOLLOW_FRIENDS' | 'SELF_ONLY'; - -// OAuth Start Response Interface -export interface GHLOAuthStartResponse { - success: boolean; - statusCode: number; - message: string; -} - -// Media Interfaces -export interface GHLPostMedia { - url: string; - caption?: string; - type?: string; // MIME type - thumbnail?: string; - defaultThumb?: string; - id?: string; -} - -export interface GHLOgTags { - metaImage?: string; - metaLink?: string; -} - -// User Interface for Posts -export interface GHLPostUser { - id: string; - title?: string; - firstName?: string; - lastName?: string; - profilePhoto?: string; - phone?: string; - email?: string; -} - -// Post Approval Interface -export interface GHLPostApproval { - approver?: string; - requesterNote?: string; - approverNote?: string; - approvalStatus?: 'pending' | 'approved' | 'rejected' | 'not_required'; - approverUser?: GHLPostUser; -} - -// TikTok Post Details -export interface GHLTikTokPostDetails { - privacyLevel?: GHLTikTokPrivacyLevel; - promoteOtherBrand?: boolean; - enableComment?: boolean; - enableDuet?: boolean; - enableStitch?: boolean; - videoDisclosure?: boolean; - promoteYourBrand?: boolean; -} - -// GMB Post Details -export interface GHLGMBPostDetails { - gmbEventType?: GHLGMBEventType; - title?: string; - offerTitle?: string; - startDate?: { - startDate?: { year: number; month: number; day: number }; - startTime?: { hours: number; minutes: number; seconds: number }; - }; - endDate?: { - endDate?: { year: number; month: number; day: number }; - endTime?: { hours: number; minutes: number; seconds: number }; - }; - termsConditions?: string; - url?: string; - couponCode?: string; - redeemOnlineUrl?: string; - actionType?: GHLGMBActionType; -} - -// Post Interface -export interface GHLSocialPost { - _id: string; - source: GHLPostSource; - locationId: string; - platform: GHLSocialPlatform; - displayDate?: string; - createdAt: string; - updatedAt: string; - accountId?: string; - accountIds: string[]; - error?: string; - postId?: string; - publishedAt?: string; - summary: string; - media?: GHLPostMedia[]; - status: GHLPostStatus; - createdBy?: string; - type: GHLPostType; - tags?: string[]; - ogTagsDetails?: GHLOgTags; - postApprovalDetails?: GHLPostApproval; - tiktokPostDetails?: GHLTikTokPostDetails; - gmbPostDetails?: GHLGMBPostDetails; - user?: GHLPostUser; - followUpComment?: string; -} - -// Account Interface -export interface GHLSocialAccount { - id: string; - oauthId?: string; - profileId?: string; - name: string; - platform: GHLSocialPlatform; - type: GHLAccountType; - expire?: string; - isExpired?: boolean; - meta?: any; - avatar?: string; - originId?: string; - locationId?: string; - active?: boolean; - deleted?: boolean; - createdAt?: string; - updatedAt?: string; -} - -// Group Interface -export interface GHLSocialGroup { - id: string; - name: string; - accountIds: string[]; -} - -// Category Interface -export interface GHLSocialCategory { - _id: string; - name: string; - primaryColor?: string; - secondaryColor?: string; - locationId: string; - createdBy?: string; - deleted: boolean; - createdAt?: string; - updatedAt?: string; -} - -// Tag Interface -export interface GHLSocialTag { - _id: string; - tag: string; - locationId: string; - createdBy?: string; - deleted?: boolean; - createdAt?: string; - updatedAt?: string; -} - -// CSV Import Interface -export interface GHLCSVImport { - _id: string; - locationId: string; - fileName: string; - accountIds: string[]; - file: string; - status: GHLCSVStatus; - count: number; - createdBy?: string; - traceId?: string; - originId?: string; - approver?: string; - createdAt: string; -} - -// Request Interfaces - -// Search Posts Request -export interface GHLSearchPostsRequest { - type?: 'recent' | 'all' | 'scheduled' | 'draft' | 'failed' | 'in_review' | 'published' | 'in_progress' | 'deleted'; - accounts?: string; // Comma-separated account IDs - skip?: string; - limit?: string; - fromDate: string; - toDate: string; - includeUsers: string; - postType?: GHLPostType; -} - -// Create/Update Post Request -export interface GHLCreatePostRequest { - accountIds: string[]; - summary: string; - media?: GHLPostMedia[]; - status?: GHLPostStatus; - scheduleDate?: string; - createdBy?: string; - followUpComment?: string; - ogTagsDetails?: GHLOgTags; - type: GHLPostType; - postApprovalDetails?: GHLPostApproval; - scheduleTimeUpdated?: boolean; - tags?: string[]; - categoryId?: string; - tiktokPostDetails?: GHLTikTokPostDetails; - gmbPostDetails?: GHLGMBPostDetails; - userId?: string; -} - -export interface GHLUpdatePostRequest extends Partial {} - -// Bulk Delete Request -export interface GHLBulkDeletePostsRequest { - postIds: string[]; -} - -// CSV Upload Request -export interface GHLUploadCSVRequest { - file: any; // File upload -} - -// Set Accounts Request -export interface GHLSetAccountsRequest { - accountIds: string[]; - filePath: string; - rowsCount: number; - fileName: string; - approver?: string; - userId?: string; -} - -// CSV Finalize Request -export interface GHLCSVFinalizeRequest { - userId?: string; -} - -// Tag Search Request -export interface GHLGetTagsByIdsRequest { - tagIds: string[]; -} - -// OAuth Platform Account Interfaces -export interface GHLGoogleLocation { - name: string; - storeCode?: string; - title: string; - metadata?: any; - storefrontAddress?: any; - relationshipData?: any; - maxLocation?: boolean; - isVerified?: boolean; - isConnected?: boolean; -} - -export interface GHLGoogleAccount { - name: string; - accountName: string; - type: string; - verificationState?: string; - vettedState?: string; -} - -export interface GHLFacebookPage { - id: string; - name: string; - avatar?: string; - isOwned?: boolean; - isConnected?: boolean; -} - -export interface GHLInstagramAccount { - id: string; - name: string; - avatar?: string; - pageId?: string; - isConnected?: boolean; -} - -export interface GHLLinkedInPage { - id: string; - name: string; - avatar?: string; - urn?: string; - isConnected?: boolean; -} - -export interface GHLLinkedInProfile { - id: string; - name: string; - avatar?: string; - urn?: string; - isConnected?: boolean; -} - -export interface GHLTwitterProfile { - id: string; - name: string; - username?: string; - avatar?: string; - protected?: boolean; - verified?: boolean; - isConnected?: boolean; -} - -export interface GHLTikTokProfile { - id: string; - name: string; - username?: string; - avatar?: string; - verified?: boolean; - isConnected?: boolean; - type?: 'business' | 'profile'; -} - -// OAuth Attach Requests -export interface GHLAttachGMBLocationRequest { - location: any; - account: any; - companyId?: string; -} - -export interface GHLAttachFBAccountRequest { - type: 'page'; - originId: string; - name: string; - avatar?: string; - companyId?: string; -} - -export interface GHLAttachIGAccountRequest { - originId: string; - name: string; - avatar?: string; - pageId: string; - companyId?: string; -} - -export interface GHLAttachLinkedInAccountRequest { - type: GHLAccountType; - originId: string; - name: string; - avatar?: string; - urn?: string; - companyId?: string; -} - -export interface GHLAttachTwitterAccountRequest { - originId: string; - name: string; - username?: string; - avatar?: string; - protected?: boolean; - verified?: boolean; - companyId?: string; -} - -export interface GHLAttachTikTokAccountRequest { - type: GHLAccountType; - originId: string; - name: string; - avatar?: string; - verified?: boolean; - username?: string; - companyId?: string; -} - -// Response Interfaces -export interface GHLSearchPostsResponse { - posts: GHLSocialPost[]; - count: number; -} - -export interface GHLGetPostResponse { - post: GHLSocialPost; -} - -export interface GHLCreatePostResponse { - post: GHLSocialPost; -} - -export interface GHLBulkDeleteResponse { - message: string; - deletedCount: number; -} - -export interface GHLGetAccountsResponse { - accounts: GHLSocialAccount[]; - groups: GHLSocialGroup[]; -} - -export interface GHLUploadCSVResponse { - filePath: string; - rowsCount: number; - fileName: string; -} - -export interface GHLGetUploadStatusResponse { - csvs: GHLCSVImport[]; - count: number; -} - -export interface GHLGetCategoriesResponse { - categories: GHLSocialCategory[]; - count: number; -} - -export interface GHLGetCategoryResponse { - category: GHLSocialCategory; -} - -export interface GHLGetTagsResponse { - tags: GHLSocialTag[]; - count: number; -} - -export interface GHLGetTagsByIdsResponse { - tags: GHLSocialTag[]; - count: number; -} - -// OAuth Response Interfaces -export interface GHLGetGoogleLocationsResponse { - locations: { - location: GHLGoogleLocation; - account: GHLGoogleAccount; - }; -} - -export interface GHLGetFacebookPagesResponse { - pages: GHLFacebookPage[]; -} - -export interface GHLGetInstagramAccountsResponse { - accounts: GHLInstagramAccount[]; -} - -export interface GHLGetLinkedInAccountsResponse { - pages: GHLLinkedInPage[]; - profile: GHLLinkedInProfile[]; -} - -export interface GHLGetTwitterAccountsResponse { - profile: GHLTwitterProfile[]; -} - -export interface GHLGetTikTokAccountsResponse { - profile: GHLTikTokProfile[]; -} - -// MCP Tool Parameters - Social Media Operations - -export interface MCPSearchPostsParams { - type?: 'recent' | 'all' | 'scheduled' | 'draft' | 'failed' | 'in_review' | 'published' | 'in_progress' | 'deleted'; - accounts?: string; - skip?: number; - limit?: number; - fromDate: string; - toDate: string; - includeUsers?: boolean; - postType?: GHLPostType; -} - -export interface MCPCreatePostParams { - accountIds: string[]; - summary: string; - media?: GHLPostMedia[]; - status?: GHLPostStatus; - scheduleDate?: string; - followUpComment?: string; - type: GHLPostType; - tags?: string[]; - categoryId?: string; - tiktokPostDetails?: GHLTikTokPostDetails; - gmbPostDetails?: GHLGMBPostDetails; - userId?: string; -} - -export interface MCPGetPostParams { - postId: string; -} - -export interface MCPUpdatePostParams { - postId: string; - accountIds?: string[]; - summary?: string; - media?: GHLPostMedia[]; - status?: GHLPostStatus; - scheduleDate?: string; - followUpComment?: string; - type?: GHLPostType; - tags?: string[]; - categoryId?: string; - tiktokPostDetails?: GHLTikTokPostDetails; - gmbPostDetails?: GHLGMBPostDetails; - userId?: string; -} - -export interface MCPDeletePostParams { - postId: string; -} - -export interface MCPBulkDeletePostsParams { - postIds: string[]; -} - -export interface MCPGetAccountsParams { - // No additional params - uses location from config -} - -export interface MCPDeleteAccountParams { - accountId: string; - companyId?: string; - userId?: string; -} - -export interface MCPUploadCSVParams { - file: any; -} - -export interface MCPGetUploadStatusParams { - skip?: number; - limit?: number; - includeUsers?: boolean; - userId?: string; -} - -export interface MCPSetAccountsParams { - accountIds: string[]; - filePath: string; - rowsCount: number; - fileName: string; - approver?: string; - userId?: string; -} - -export interface MCPGetCSVPostParams { - csvId: string; - skip?: number; - limit?: number; -} - -export interface MCPFinalizeCSVParams { - csvId: string; - userId?: string; -} - -export interface MCPDeleteCSVParams { - csvId: string; -} - -export interface MCPDeleteCSVPostParams { - csvId: string; - postId: string; -} - -export interface MCPGetCategoriesParams { - searchText?: string; - limit?: number; - skip?: number; -} - -export interface MCPGetCategoryParams { - categoryId: string; -} - -export interface MCPGetTagsParams { - searchText?: string; - limit?: number; - skip?: number; -} - -export interface MCPGetTagsByIdsParams { - tagIds: string[]; -} - -// OAuth MCP Parameters -export interface MCPStartOAuthParams { - platform: GHLSocialPlatform; - userId: string; - page?: string; - reconnect?: boolean; -} - -export interface MCPGetOAuthAccountsParams { - platform: GHLSocialPlatform; - accountId: string; -} - -export interface MCPAttachOAuthAccountParams { - platform: GHLSocialPlatform; - accountId: string; - attachData: any; // Platform-specific attach data -} - -// ==== MISSING CALENDAR API TYPES ==== - -// Calendar Groups Management Types -export interface GHLValidateGroupSlugRequest { - locationId: string; - slug: string; -} - -export interface GHLValidateGroupSlugResponse { - available: boolean; -} - -export interface GHLUpdateCalendarGroupRequest { - name: string; - description: string; - slug: string; -} - -export interface GHLGroupStatusUpdateRequest { - isActive: boolean; -} - -export interface GHLGroupSuccessResponse { - success: boolean; -} - -// Appointment Notes Types -export interface GHLAppointmentNote { - id: string; - body: string; - userId: string; - dateAdded: string; - contactId: string; - createdBy: { - id: string; - name: string; - }; -} - -export interface GHLGetAppointmentNotesResponse { - notes: GHLAppointmentNote[]; - hasMore: boolean; -} - -export interface GHLCreateAppointmentNoteRequest { - userId?: string; - body: string; -} - -export interface GHLUpdateAppointmentNoteRequest { - userId?: string; - body: string; -} - -export interface GHLAppointmentNoteResponse { - note: GHLAppointmentNote; -} - -export interface GHLDeleteAppointmentNoteResponse { - success: boolean; -} - -// Calendar Resources Types -export interface GHLCalendarResource { - id: string; - locationId: string; - name: string; - resourceType: 'equipments' | 'rooms'; - isActive: boolean; - description?: string; - quantity?: number; - outOfService?: number; - capacity?: number; - calendarIds: string[]; -} - -export interface GHLCreateCalendarResourceRequest { - locationId: string; - name: string; - description: string; - quantity: number; - outOfService: number; - capacity: number; - calendarIds: string[]; -} - -export interface GHLUpdateCalendarResourceRequest { - locationId?: string; - name?: string; - description?: string; - quantity?: number; - outOfService?: number; - capacity?: number; - calendarIds?: string[]; - isActive?: boolean; -} - -export interface GHLCalendarResourceResponse { - locationId: string; - name: string; - resourceType: 'equipments' | 'rooms'; - isActive: boolean; - description?: string; - quantity?: number; - outOfService?: number; - capacity?: number; -} - -export interface GHLCalendarResourceByIdResponse { - locationId: string; - name: string; - resourceType: 'equipments' | 'rooms'; - isActive: boolean; - description?: string; - quantity?: number; - outOfService?: number; - capacity?: number; - calendarIds: string[]; -} - -export interface GHLResourceDeleteResponse { - success: boolean; -} - -export interface GHLGetCalendarResourcesRequest { - locationId: string; - limit: number; - skip: number; -} - -// Calendar Notifications Types -export interface GHLScheduleDTO { - timeOffset: number; - unit: string; -} - -export interface GHLCalendarNotification { - _id: string; - altType: 'calendar'; - calendarId: string; - receiverType: 'contact' | 'guest' | 'assignedUser' | 'emails'; - additionalEmailIds?: string[]; - channel: 'email' | 'inApp'; - notificationType: 'booked' | 'confirmation' | 'cancellation' | 'reminder' | 'followup' | 'reschedule'; - isActive: boolean; - templateId?: string; - body?: string; - subject?: string; - afterTime?: GHLScheduleDTO[]; - beforeTime?: GHLScheduleDTO[]; - selectedUsers?: string[]; - deleted: boolean; -} - -export interface GHLCreateCalendarNotificationRequest { - receiverType: 'contact' | 'guest' | 'assignedUser' | 'emails'; - channel: 'email' | 'inApp'; - notificationType: 'booked' | 'confirmation' | 'cancellation' | 'reminder' | 'followup' | 'reschedule'; - isActive?: boolean; - templateId?: string; - body?: string; - subject?: string; - afterTime?: GHLScheduleDTO[]; - beforeTime?: GHLScheduleDTO[]; - additionalEmailIds?: string[]; - selectedUsers?: string[]; - fromAddress?: string; - fromName?: string; -} - -export interface GHLUpdateCalendarNotificationRequest { - altType?: 'calendar'; - altId?: string; - receiverType?: 'contact' | 'guest' | 'assignedUser' | 'emails'; - additionalEmailIds?: string[]; - channel?: 'email' | 'inApp'; - notificationType?: 'booked' | 'confirmation' | 'cancellation' | 'reminder' | 'followup' | 'reschedule'; - isActive?: boolean; - deleted?: boolean; - templateId?: string; - body?: string; - subject?: string; - afterTime?: GHLScheduleDTO[]; - beforeTime?: GHLScheduleDTO[]; - fromAddress?: string; - fromName?: string; -} - -export interface GHLCalendarNotificationDeleteResponse { - message: string; -} - -export interface GHLGetCalendarNotificationsRequest { - altType?: 'calendar'; - altId?: string; - isActive?: boolean; - deleted?: boolean; - limit?: number; - skip?: number; -} - -// Blocked Slots Types -export interface GHLGetBlockedSlotsRequest { - locationId: string; - userId?: string; - calendarId?: string; - groupId?: string; - startTime: string; - endTime: string; -} - -// MCP Parameters for Missing Calendar Endpoints - -// Calendar Groups Management Parameters -export interface MCPCreateCalendarGroupParams { - name: string; - description: string; - slug: string; - isActive?: boolean; -} - -export interface MCPValidateGroupSlugParams { - slug: string; - locationId?: string; -} - -export interface MCPUpdateCalendarGroupParams { - groupId: string; - name: string; - description: string; - slug: string; -} - -export interface MCPDeleteCalendarGroupParams { - groupId: string; -} - -export interface MCPDisableCalendarGroupParams { - groupId: string; - isActive: boolean; -} - -// Appointment Notes Parameters -export interface MCPGetAppointmentNotesParams { - appointmentId: string; - limit: number; - offset: number; -} - -export interface MCPCreateAppointmentNoteParams { - appointmentId: string; - body: string; - userId?: string; -} - -export interface MCPUpdateAppointmentNoteParams { - appointmentId: string; - noteId: string; - body: string; - userId?: string; -} - -export interface MCPDeleteAppointmentNoteParams { - appointmentId: string; - noteId: string; -} - -// Calendar Resources Parameters -export interface MCPGetCalendarResourcesParams { - resourceType: 'equipments' | 'rooms'; - limit: number; - skip: number; - locationId?: string; -} - -export interface MCPCreateCalendarResourceParams { - resourceType: 'equipments' | 'rooms'; - name: string; - description: string; - quantity: number; - outOfService: number; - capacity: number; - calendarIds: string[]; - locationId?: string; -} - -export interface MCPGetCalendarResourceParams { - resourceType: 'equipments' | 'rooms'; - resourceId: string; -} - -export interface MCPUpdateCalendarResourceParams { - resourceType: 'equipments' | 'rooms'; - resourceId: string; - name?: string; - description?: string; - quantity?: number; - outOfService?: number; - capacity?: number; - calendarIds?: string[]; - isActive?: boolean; -} - -export interface MCPDeleteCalendarResourceParams { - resourceType: 'equipments' | 'rooms'; - resourceId: string; -} - -// Calendar Notifications Parameters -export interface MCPGetCalendarNotificationsParams { - calendarId: string; - altType?: 'calendar'; - altId?: string; - isActive?: boolean; - deleted?: boolean; - limit?: number; - skip?: number; -} - -export interface MCPCreateCalendarNotificationParams { - calendarId: string; - notifications: GHLCreateCalendarNotificationRequest[]; -} - -export interface MCPGetCalendarNotificationParams { - calendarId: string; - notificationId: string; -} - -export interface MCPUpdateCalendarNotificationParams { - calendarId: string; - notificationId: string; - receiverType?: 'contact' | 'guest' | 'assignedUser' | 'emails'; - additionalEmailIds?: string[]; - channel?: 'email' | 'inApp'; - notificationType?: 'booked' | 'confirmation' | 'cancellation' | 'reminder' | 'followup' | 'reschedule'; - isActive?: boolean; - deleted?: boolean; - templateId?: string; - body?: string; - subject?: string; - afterTime?: GHLScheduleDTO[]; - beforeTime?: GHLScheduleDTO[]; - fromAddress?: string; - fromName?: string; -} - -export interface MCPDeleteCalendarNotificationParams { - calendarId: string; - notificationId: string; -} - -// Blocked Slots Parameters -export interface MCPGetBlockedSlotsParams { - userId?: string; - calendarId?: string; - groupId?: string; - startTime: string; - endTime: string; -} - -// ==== MEDIA LIBRARY API TYPES ==== - -// Media File Types -export interface GHLMediaFile { - id?: string; - altId: string; - altType: 'location' | 'agency'; - name: string; - parentId?: string; - url: string; - path: string; - type?: 'file' | 'folder'; - size?: number; - mimeType?: string; - createdAt?: string; - updatedAt?: string; -} - -// API Request/Response Types -export interface GHLGetMediaFilesRequest { - offset?: number; - limit?: number; - sortBy: string; - sortOrder: 'asc' | 'desc'; - type?: 'file' | 'folder'; - query?: string; - altType: 'location' | 'agency'; - altId: string; - parentId?: string; -} - -export interface GHLGetMediaFilesResponse { - files: GHLMediaFile[]; - total?: number; - hasMore?: boolean; -} - -export interface GHLUploadMediaFileRequest { - file?: any; // Binary file data - hosted?: boolean; - fileUrl?: string; - name?: string; - parentId?: string; - altType?: 'location' | 'agency'; - altId?: string; -} - -export interface GHLUploadMediaFileResponse { - fileId: string; - url?: string; - name?: string; - size?: number; - mimeType?: string; -} - -export interface GHLDeleteMediaRequest { - id: string; - altType: 'location' | 'agency'; - altId: string; -} - -export interface GHLDeleteMediaResponse { - success: boolean; - message?: string; -} - -// MCP Parameters for Media Library Endpoints -export interface MCPGetMediaFilesParams { - offset?: number; - limit?: number; - sortBy?: string; - sortOrder?: 'asc' | 'desc'; - type?: 'file' | 'folder'; - query?: string; - altType?: 'location' | 'agency'; - altId?: string; - parentId?: string; -} - -export interface MCPUploadMediaFileParams { - file?: any; - hosted?: boolean; - fileUrl?: string; - name?: string; - parentId?: string; - altType?: 'location' | 'agency'; - altId?: string; -} - -export interface MCPDeleteMediaParams { - id: string; - altType?: 'location' | 'agency'; - altId?: string; -} - -// ===== CUSTOM OBJECTS API TYPES ===== - -// Object Schema Types -export interface GHLCustomObjectLabel { - singular: string; - plural: string; -} - -export interface GHLCustomObjectDisplayProperty { - key: string; - name: string; - dataType: string; -} - -export interface GHLCustomFieldOption { - key: string; - label: string; - url?: string; -} - -export interface GHLCustomField { - locationId: string; - name: string; - description?: string; - placeholder?: string; - showInForms: boolean; - options?: GHLCustomFieldOption[]; - acceptedFormats?: '.pdf' | '.docx' | '.doc' | '.jpg' | '.jpeg' | '.png' | '.gif' | '.csv' | '.xlsx' | '.xls' | 'all'; - id: string; - objectKey: string; - dataType: 'TEXT' | 'LARGE_TEXT' | 'NUMERICAL' | 'PHONE' | 'MONETORY' | 'CHECKBOX' | 'SINGLE_OPTIONS' | 'MULTIPLE_OPTIONS' | 'DATE' | 'TEXTBOX_LIST' | 'FILE_UPLOAD' | 'RADIO'; - parentId: string; - fieldKey: string; - allowCustomOption?: boolean; - maxFileLimit?: number; - dateAdded: string; - dateUpdated: string; -} - -export interface GHLCustomObjectSchema { - id: string; - standard: boolean; - key: string; - labels: GHLCustomObjectLabel; - description?: string; - locationId: string; - primaryDisplayProperty: string; - dateAdded: string; - dateUpdated: string; - type?: any; -} - -export interface GHLObjectRecord { - id: string; - owner: string[]; - followers: string[]; - properties: Record; - dateAdded: string; - dateUpdated: string; -} - -export interface GHLCreatedByMeta { - channel: string; - createdAt: string; - source: string; - sourceId: string; -} - -export interface GHLDetailedObjectRecord { - id: string; - owner: string[]; - followers: string[]; - properties: Record; - createdAt: string; - updatedAt: string; - locationId: string; - objectId: string; - objectKey: string; - createdBy: GHLCreatedByMeta; - lastUpdatedBy: GHLCreatedByMeta; - searchAfter: (string | number)[]; -} - -// Request Types -export interface GHLGetObjectSchemaRequest { - key: string; - locationId: string; - fetchProperties?: boolean; -} - -export interface GHLCreateObjectSchemaRequest { - labels: GHLCustomObjectLabel; - key: string; - description?: string; - locationId: string; - primaryDisplayPropertyDetails: GHLCustomObjectDisplayProperty; -} - -export interface GHLUpdateObjectSchemaRequest { - labels?: Partial; - description?: string; - locationId: string; - searchableProperties: string[]; -} - -export interface GHLCreateObjectRecordRequest { - locationId: string; - properties: Record; - owner?: string[]; - followers?: string[]; -} - -export interface GHLUpdateObjectRecordRequest { - locationId: string; - properties?: Record; - owner?: string[]; - followers?: string[]; -} - -export interface GHLSearchObjectRecordsRequest { - locationId: string; - page: number; - pageLimit: number; - query: string; - searchAfter: string[]; -} - -// Response Types -export interface GHLGetObjectSchemaResponse { - object: GHLCustomObjectSchema; - cache: boolean; - fields?: GHLCustomField[]; -} - -export interface GHLObjectListResponse { - objects: GHLCustomObjectSchema[]; -} - -export interface GHLObjectSchemaResponse { - object: GHLCustomObjectSchema; -} - -export interface GHLObjectRecordResponse { - record: GHLObjectRecord; -} - -export interface GHLDetailedObjectRecordResponse { - record: GHLDetailedObjectRecord; -} - -export interface GHLObjectRecordDeleteResponse { - id: string; - success: boolean; -} - -export interface GHLSearchObjectRecordsResponse { - records: GHLDetailedObjectRecord[]; - total: number; -} - -// MCP Parameter Interfaces -export interface MCPGetObjectSchemaParams { - key: string; - locationId?: string; - fetchProperties?: boolean; -} - -export interface MCPGetAllObjectsParams { - locationId?: string; -} - -export interface MCPCreateObjectSchemaParams { - labels: GHLCustomObjectLabel; - key: string; - description?: string; - locationId?: string; - primaryDisplayPropertyDetails: GHLCustomObjectDisplayProperty; -} - -export interface MCPUpdateObjectSchemaParams { - key: string; - labels?: Partial; - description?: string; - locationId?: string; - searchableProperties: string[]; -} - -export interface MCPCreateObjectRecordParams { - schemaKey: string; - properties: Record; - locationId?: string; - owner?: string[]; - followers?: string[]; -} - -export interface MCPGetObjectRecordParams { - schemaKey: string; - recordId: string; -} - -export interface MCPUpdateObjectRecordParams { - schemaKey: string; - recordId: string; - properties?: Record; - locationId?: string; - owner?: string[]; - followers?: string[]; -} - -export interface MCPDeleteObjectRecordParams { - schemaKey: string; - recordId: string; -} - -export interface MCPSearchObjectRecordsParams { - schemaKey: string; - locationId?: string; - page?: number; - pageLimit?: number; - query: string; - searchAfter?: string[]; -} - -// ===== ASSOCIATIONS API TYPES ===== - -// Association Types -export interface GHLAssociation { - locationId: string; - id: string; - key: string; - firstObjectLabel: any; - firstObjectKey: any; - secondObjectLabel: any; - secondObjectKey: any; - associationType: 'USER_DEFINED' | 'SYSTEM_DEFINED'; -} - -export interface GHLRelation { - id: string; - associationId: string; - firstRecordId: string; - secondRecordId: string; - locationId: string; -} - -// Request Types -export interface GHLCreateAssociationRequest { - locationId: string; - key: string; - firstObjectLabel: any; - firstObjectKey: any; - secondObjectLabel: any; - secondObjectKey: any; -} - -export interface GHLUpdateAssociationRequest { - firstObjectLabel: any; - secondObjectLabel: any; -} - -export interface GHLCreateRelationRequest { - locationId: string; - associationId: string; - firstRecordId: string; - secondRecordId: string; -} - -export interface GHLGetAssociationsRequest { - locationId: string; - skip: number; - limit: number; -} - -export interface GHLGetRelationsByRecordRequest { - recordId: string; - locationId: string; - skip: number; - limit: number; - associationIds?: string[]; -} - -export interface GHLGetAssociationByKeyRequest { - keyName: string; - locationId: string; -} - -export interface GHLGetAssociationByObjectKeyRequest { - objectKey: string; - locationId?: string; -} - -export interface GHLDeleteRelationRequest { - relationId: string; - locationId: string; -} - -// Response Types -export interface GHLAssociationResponse { - locationId: string; - id: string; - key: string; - firstObjectLabel: any; - firstObjectKey: any; - secondObjectLabel: any; - secondObjectKey: any; - associationType: 'USER_DEFINED' | 'SYSTEM_DEFINED'; -} - -export interface GHLDeleteAssociationResponse { - deleted: boolean; - id: string; - message: string; -} - -export interface GHLGetAssociationsResponse { - associations: GHLAssociation[]; - total?: number; -} - -export interface GHLGetRelationsResponse { - relations: GHLRelation[]; - total?: number; -} - -// MCP Parameter Interfaces -export interface MCPCreateAssociationParams { - locationId?: string; - key: string; - firstObjectLabel: any; - firstObjectKey: any; - secondObjectLabel: any; - secondObjectKey: any; -} - -export interface MCPUpdateAssociationParams { - associationId: string; - firstObjectLabel: any; - secondObjectLabel: any; -} - -export interface MCPGetAllAssociationsParams { - locationId?: string; - skip?: number; - limit?: number; -} - -export interface MCPGetAssociationByIdParams { - associationId: string; -} - -export interface MCPGetAssociationByKeyParams { - keyName: string; - locationId?: string; -} - -export interface MCPGetAssociationByObjectKeyParams { - objectKey: string; - locationId?: string; -} - -export interface MCPDeleteAssociationParams { - associationId: string; -} - -export interface MCPCreateRelationParams { - locationId?: string; - associationId: string; - firstRecordId: string; - secondRecordId: string; -} - -export interface MCPGetRelationsByRecordParams { - recordId: string; - locationId?: string; - skip?: number; - limit?: number; - associationIds?: string[]; -} - -export interface MCPDeleteRelationParams { - relationId: string; - locationId?: string; -} - -// ===== CUSTOM FIELDS V2 API TYPES ===== - -// Custom Field V2 Option Types -export interface GHLV2CustomFieldOption { - key: string; - label: string; - url?: string; // Optional, valid only for RADIO type -} - -// Custom Field V2 Types -export interface GHLV2CustomField { - locationId: string; - name?: string; - description?: string; - placeholder?: string; - showInForms: boolean; - options?: GHLV2CustomFieldOption[]; - acceptedFormats?: '.pdf' | '.docx' | '.doc' | '.jpg' | '.jpeg' | '.png' | '.gif' | '.csv' | '.xlsx' | '.xls' | 'all'; - id: string; - objectKey: string; - dataType: 'TEXT' | 'LARGE_TEXT' | 'NUMERICAL' | 'PHONE' | 'MONETORY' | 'CHECKBOX' | 'SINGLE_OPTIONS' | 'MULTIPLE_OPTIONS' | 'DATE' | 'TEXTBOX_LIST' | 'FILE_UPLOAD' | 'RADIO' | 'EMAIL'; - parentId: string; - fieldKey: string; - allowCustomOption?: boolean; - maxFileLimit?: number; - dateAdded: string; - dateUpdated: string; -} - -export interface GHLV2CustomFieldFolder { - id: string; - objectKey: string; - locationId: string; - name: string; -} - -// Request Types -export interface GHLV2CreateCustomFieldRequest { - locationId: string; - name?: string; - description?: string; - placeholder?: string; - showInForms: boolean; - options?: GHLV2CustomFieldOption[]; - acceptedFormats?: '.pdf' | '.docx' | '.doc' | '.jpg' | '.jpeg' | '.png' | '.gif' | '.csv' | '.xlsx' | '.xls' | 'all'; - dataType: 'TEXT' | 'LARGE_TEXT' | 'NUMERICAL' | 'PHONE' | 'MONETORY' | 'CHECKBOX' | 'SINGLE_OPTIONS' | 'MULTIPLE_OPTIONS' | 'DATE' | 'TEXTBOX_LIST' | 'FILE_UPLOAD' | 'RADIO' | 'EMAIL'; - fieldKey: string; - objectKey: string; - maxFileLimit?: number; - allowCustomOption?: boolean; - parentId: string; -} - -export interface GHLV2UpdateCustomFieldRequest { - locationId: string; - name?: string; - description?: string; - placeholder?: string; - showInForms: boolean; - options?: GHLV2CustomFieldOption[]; - acceptedFormats?: '.pdf' | '.docx' | '.doc' | '.jpg' | '.jpeg' | '.png' | '.gif' | '.csv' | '.xlsx' | '.xls' | 'all'; - maxFileLimit?: number; -} - -export interface GHLV2CreateCustomFieldFolderRequest { - objectKey: string; - name: string; - locationId: string; -} - -export interface GHLV2UpdateCustomFieldFolderRequest { - name: string; - locationId: string; -} - -export interface GHLV2GetCustomFieldsByObjectKeyRequest { - objectKey: string; - locationId: string; -} - -export interface GHLV2DeleteCustomFieldFolderRequest { - id: string; - locationId: string; -} - -// Response Types -export interface GHLV2CustomFieldResponse { - field: GHLV2CustomField; -} - -export interface GHLV2CustomFieldsResponse { - fields: GHLV2CustomField[]; - folders: GHLV2CustomFieldFolder[]; -} - -export interface GHLV2CustomFieldFolderResponse { - id: string; - objectKey: string; - locationId: string; - name: string; -} - -export interface GHLV2DeleteCustomFieldResponse { - succeded: boolean; - id: string; - key: string; -} - -// MCP Parameter Interfaces -export interface MCPV2CreateCustomFieldParams { - locationId?: string; - name?: string; - description?: string; - placeholder?: string; - showInForms?: boolean; - options?: GHLV2CustomFieldOption[]; - acceptedFormats?: '.pdf' | '.docx' | '.doc' | '.jpg' | '.jpeg' | '.png' | '.gif' | '.csv' | '.xlsx' | '.xls' | 'all'; - dataType: 'TEXT' | 'LARGE_TEXT' | 'NUMERICAL' | 'PHONE' | 'MONETORY' | 'CHECKBOX' | 'SINGLE_OPTIONS' | 'MULTIPLE_OPTIONS' | 'DATE' | 'TEXTBOX_LIST' | 'FILE_UPLOAD' | 'RADIO' | 'EMAIL'; - fieldKey: string; - objectKey: string; - maxFileLimit?: number; - allowCustomOption?: boolean; - parentId: string; -} - -export interface MCPV2UpdateCustomFieldParams { - id: string; - locationId?: string; - name?: string; - description?: string; - placeholder?: string; - showInForms?: boolean; - options?: GHLV2CustomFieldOption[]; - acceptedFormats?: '.pdf' | '.docx' | '.doc' | '.jpg' | '.jpeg' | '.png' | '.gif' | '.csv' | '.xlsx' | '.xls' | 'all'; - maxFileLimit?: number; -} - -export interface MCPV2GetCustomFieldByIdParams { - id: string; -} - -export interface MCPV2DeleteCustomFieldParams { - id: string; -} - -export interface MCPV2GetCustomFieldsByObjectKeyParams { - objectKey: string; - locationId?: string; -} - -export interface MCPV2CreateCustomFieldFolderParams { - objectKey: string; - name: string; - locationId?: string; -} - -export interface MCPV2UpdateCustomFieldFolderParams { - id: string; - name: string; - locationId?: string; -} - -export interface MCPV2DeleteCustomFieldFolderParams { - id: string; - locationId?: string; -} - -// ===== WORKFLOWS API TYPES ===== - -// Request Types -export interface GHLGetWorkflowsRequest { - locationId: string; -} - -// Response Types -export interface GHLGetWorkflowsResponse { - workflows: GHLWorkflow[]; -} - -// MCP Parameter Interfaces -export interface MCPGetWorkflowsParams { - locationId?: string; -} - -// ===== SURVEYS API TYPES ===== - -// Survey Types -export interface GHLSurvey { - id: string; - name: string; - locationId: string; -} - -// Survey Submission Types -export interface GHLSurveyPageDetails { - url: string; - title: string; -} - -export interface GHLSurveyContactSessionIds { - ids: string[] | null; -} - -export interface GHLSurveyEventData { - fbc?: string; - fbp?: string; - page?: GHLSurveyPageDetails; - type?: string; - domain?: string; - medium?: string; - source?: string; - version?: string; - adSource?: string; - mediumId?: string; - parentId?: string; - referrer?: string; - fbEventId?: string; - timestamp?: number; - parentName?: string; - fingerprint?: string; - pageVisitType?: string; - contactSessionIds?: GHLSurveyContactSessionIds | null; -} - -export interface GHLSurveySubmissionOthers { - __submissions_other_field__?: string; - __custom_field_id__?: string; - eventData?: GHLSurveyEventData; - fieldsOriSequance?: string[]; -} - -export interface GHLSurveySubmission { - id: string; - contactId: string; - createdAt: string; - surveyId: string; - name: string; - email: string; - others?: GHLSurveySubmissionOthers; -} - -export interface GHLSurveySubmissionMeta { - total: number; - currentPage: number; - nextPage: number | null; - prevPage: number | null; -} - -// Request Types -export interface GHLGetSurveysRequest { - locationId: string; - skip?: number; - limit?: number; - type?: string; -} - -export interface GHLGetSurveySubmissionsRequest { - locationId: string; - page?: number; - limit?: number; - surveyId?: string; - q?: string; - startAt?: string; - endAt?: string; -} - -// Response Types -export interface GHLGetSurveysResponse { - surveys: GHLSurvey[]; - total: number; -} - -export interface GHLGetSurveySubmissionsResponse { - submissions: GHLSurveySubmission[]; - meta: GHLSurveySubmissionMeta; -} - -// MCP Parameter Interfaces -export interface MCPGetSurveysParams { - locationId?: string; - skip?: number; - limit?: number; - type?: string; -} - -export interface MCPGetSurveySubmissionsParams { - locationId?: string; - page?: number; - limit?: number; - surveyId?: string; - q?: string; - startAt?: string; - endAt?: string; -} - -// ===== STORE API TYPES ===== - -// Country and State Types -export type GHLCountryCode = 'US' | 'CA' | 'AF' | 'AX' | 'AL' | 'DZ' | 'AS' | 'AD' | 'AO' | 'AI' | 'AQ' | 'AG' | 'AR' | 'AM' | 'AW' | 'AU' | 'AT' | 'AZ' | 'BS' | 'BH' | 'BD' | 'BB' | 'BY' | 'BE' | 'BZ' | 'BJ' | 'BM' | 'BT' | 'BO' | 'BA' | 'BW' | 'BV' | 'BR' | 'IO' | 'BN' | 'BG' | 'BF' | 'BI' | 'KH' | 'CM' | 'CV' | 'KY' | 'CF' | 'TD' | 'CL' | 'CN' | 'CX' | 'CC' | 'CO' | 'KM' | 'CG' | 'CD' | 'CK' | 'CR' | 'CI' | 'HR' | 'CU' | 'CY' | 'CZ' | 'DK' | 'DJ' | 'DM' | 'DO' | 'EC' | 'EG' | 'SV' | 'GQ' | 'ER' | 'EE' | 'ET' | 'FK' | 'FO' | 'FJ' | 'FI' | 'FR' | 'GF' | 'PF' | 'TF' | 'GA' | 'GM' | 'GE' | 'DE' | 'GH' | 'GI' | 'GR' | 'GL' | 'GD' | 'GP' | 'GU' | 'GT' | 'GG' | 'GN' | 'GW' | 'GY' | 'HT' | 'HM' | 'VA' | 'HN' | 'HK' | 'HU' | 'IS' | 'IN' | 'ID' | 'IR' | 'IQ' | 'IE' | 'IM' | 'IL' | 'IT' | 'JM' | 'JP' | 'JE' | 'JO' | 'KZ' | 'KE' | 'KI' | 'KP' | 'XK' | 'KW' | 'KG' | 'LA' | 'LV' | 'LB' | 'LS' | 'LR' | 'LY' | 'LI' | 'LT' | 'LU' | 'MO' | 'MK' | 'MG' | 'MW' | 'MY' | 'MV' | 'ML' | 'MT' | 'MH' | 'MQ' | 'MR' | 'MU' | 'YT' | 'MX' | 'FM' | 'MD' | 'MC' | 'MN' | 'ME' | 'MS' | 'MA' | 'MZ' | 'MM' | 'NA' | 'NR' | 'NP' | 'NL' | 'AN' | 'NC' | 'NZ' | 'NI' | 'NE' | 'NG' | 'NU' | 'NF' | 'MP' | 'NO' | 'OM' | 'PK' | 'PW' | 'PS' | 'PA' | 'PG' | 'PY' | 'PE' | 'PH' | 'PN' | 'PL' | 'PT' | 'PR' | 'QA' | 'RE' | 'RO' | 'RU' | 'RW' | 'SH' | 'KN' | 'LC' | 'MF' | 'PM' | 'VC' | 'WS' | 'SM' | 'ST' | 'SA' | 'SN' | 'RS' | 'SC' | 'SL' | 'SG' | 'SX' | 'SK' | 'SI' | 'SB' | 'SO' | 'ZA' | 'GS' | 'KR' | 'ES' | 'LK' | 'SD' | 'SR' | 'SJ' | 'SZ' | 'SE' | 'CH' | 'SY' | 'TW' | 'TJ' | 'TZ' | 'TH' | 'TL' | 'TG' | 'TK' | 'TO' | 'TT' | 'TN' | 'TR' | 'TM' | 'TC' | 'TV' | 'UG' | 'UA' | 'AE' | 'GB' | 'UM' | 'UY' | 'UZ' | 'VU' | 'VE' | 'VN' | 'VG' | 'VI' | 'WF' | 'EH' | 'YE' | 'ZM' | 'ZW'; - -export type GHLStateCode = 'AL' | 'AK' | 'AS' | 'AZ' | 'AR' | 'AA' | 'AE' | 'AP' | 'CA' | 'CO' | 'CT' | 'DE' | 'DC' | 'FM' | 'FL' | 'GA' | 'GU' | 'HI' | 'ID' | 'IL' | 'IN' | 'IA' | 'KS' | 'KY' | 'LA' | 'ME' | 'MH' | 'MD' | 'MA' | 'MI' | 'MN' | 'MS' | 'MO' | 'MT' | 'NE' | 'NV' | 'NH' | 'NJ' | 'NM' | 'NY' | 'NC' | 'ND' | 'MP' | 'OH' | 'OK' | 'OR' | 'PW' | 'PA' | 'PR' | 'RI' | 'SC' | 'SD' | 'TN' | 'TX' | 'UT' | 'VT' | 'VI' | 'VA' | 'WA' | 'WV' | 'WI' | 'WY' | 'AB' | 'BC' | 'MB' | 'NB' | 'NL' | 'NT' | 'NS' | 'NU' | 'ON' | 'PE' | 'QC' | 'SK' | 'YT'; - -// Shipping Zone Types -export interface GHLShippingZoneCountryState { - code: GHLStateCode; -} - -export interface GHLShippingZoneCountry { - code: GHLCountryCode; - states?: GHLShippingZoneCountryState[]; -} - -export interface GHLCreateShippingZoneRequest { - altId: string; - altType: 'location'; - name: string; - countries: GHLShippingZoneCountry[]; -} - -export interface GHLUpdateShippingZoneRequest { - altId?: string; - altType?: 'location'; - name?: string; - countries?: GHLShippingZoneCountry[]; -} - -export interface GHLGetShippingZonesRequest { - altId: string; - altType: 'location'; - limit?: number; - offset?: number; - withShippingRate?: boolean; -} - -export interface GHLDeleteShippingZoneRequest { - altId: string; - altType: 'location'; -} - -// Shipping Rate Types -export interface GHLShippingCarrierService { - name: string; - value: string; -} - -export type GHLShippingConditionType = 'none' | 'price' | 'weight'; - -export interface GHLCreateShippingRateRequest { - altId: string; - altType: 'location'; - name: string; - description?: string; - currency: string; - amount: number; - conditionType: GHLShippingConditionType; - minCondition?: number; - maxCondition?: number; - isCarrierRate?: boolean; - shippingCarrierId: string; - percentageOfRateFee?: number; - shippingCarrierServices?: GHLShippingCarrierService[]; -} - -export interface GHLUpdateShippingRateRequest { - altId?: string; - altType?: 'location'; - name?: string; - description?: string; - currency?: string; - amount?: number; - conditionType?: GHLShippingConditionType; - minCondition?: number; - maxCondition?: number; - isCarrierRate?: boolean; - shippingCarrierId?: string; - percentageOfRateFee?: number; - shippingCarrierServices?: GHLShippingCarrierService[]; -} - -export interface GHLGetShippingRatesRequest { - altId: string; - altType: 'location'; - limit?: number; - offset?: number; -} - -export interface GHLDeleteShippingRateRequest { - altId: string; - altType: 'location'; -} - -// Shipping Carrier Types -export interface GHLCreateShippingCarrierRequest { - altId: string; - altType: 'location'; - name: string; - callbackUrl: string; - services?: GHLShippingCarrierService[]; - allowsMultipleServiceSelection?: boolean; -} - -export interface GHLUpdateShippingCarrierRequest { - altId?: string; - altType?: 'location'; - name?: string; - callbackUrl?: string; - services?: GHLShippingCarrierService[]; - allowsMultipleServiceSelection?: boolean; -} - -export interface GHLGetShippingCarriersRequest { - altId: string; - altType: 'location'; -} - -export interface GHLDeleteShippingCarrierRequest { - altId: string; - altType: 'location'; -} - -// Available Shipping Rates Types -export interface GHLContactAddress { - name?: string; - companyName?: string; - addressLine1?: string; - country: GHLCountryCode; - state?: GHLStateCode; - city?: string; - zip?: string; - phone?: string; - email?: string; -} - -export interface GHLOrderSource { - type: 'funnel' | 'website' | 'invoice' | 'calendar' | 'text2Pay' | 'document_contracts' | 'membership' | 'mobile_app' | 'communities' | 'point_of_sale' | 'manual' | 'form' | 'survey' | 'payment_link' | 'external'; - subType?: 'one_step_order_form' | 'two_step_order_form' | 'upsell' | 'tap_to_pay' | 'card_payment' | 'store' | 'contact_view' | 'email_campaign' | 'payments_dashboard' | 'shopify' | 'subscription_view' | 'store_upsell' | 'woocommerce' | 'service' | 'meeting' | 'imported_csv' | 'qr_code'; -} - -export interface GHLProductItem { - id: string; - qty: number; -} - -export interface GHLGetAvailableShippingRatesRequest { - altId: string; - altType: 'location'; - country: GHLCountryCode; - address?: GHLContactAddress; - amountAvailable?: string; - totalOrderAmount: number; - weightAvailable?: boolean; - totalOrderWeight: number; - source: GHLOrderSource; - products: GHLProductItem[]; - couponCode?: string; -} - -// Response Types -export interface GHLShippingRate { - altId: string; - altType: 'location'; - name: string; - description?: string; - currency: string; - amount: number; - conditionType: GHLShippingConditionType; - minCondition?: number; - maxCondition?: number; - isCarrierRate?: boolean; - shippingCarrierId: string; - percentageOfRateFee?: number; - shippingCarrierServices?: GHLShippingCarrierService[]; - _id: string; - shippingZoneId: string; - createdAt: string; - updatedAt: string; -} - -export interface GHLShippingZone { - altId: string; - altType: 'location'; - name: string; - countries: GHLShippingZoneCountry[]; - _id: string; - shippingRates?: GHLShippingRate[]; - createdAt: string; - updatedAt: string; -} - -export interface GHLShippingCarrier { - altId: string; - altType: 'location'; - name: string; - callbackUrl: string; - services?: GHLShippingCarrierService[]; - allowsMultipleServiceSelection?: boolean; - _id: string; - marketplaceAppId: string; - createdAt: string; - updatedAt: string; -} - -export interface GHLAvailableShippingRate { - name: string; - description?: string; - currency: string; - amount: number; - isCarrierRate?: boolean; - shippingCarrierId: string; - percentageOfRateFee?: number; - shippingCarrierServices?: GHLShippingCarrierService[]; - _id: string; - shippingZoneId: string; -} - -export interface GHLCreateShippingZoneResponse { - status: boolean; - message?: string; - data: GHLShippingZone; -} - -export interface GHLListShippingZonesResponse { - total: number; - data: GHLShippingZone[]; -} - -export interface GHLGetShippingZoneResponse { - status: boolean; - message?: string; - data: GHLShippingZone; -} - -export interface GHLUpdateShippingZoneResponse { - status: boolean; - message?: string; - data: GHLShippingZone; -} - -export interface GHLDeleteShippingZoneResponse { - status: boolean; - message?: string; -} - -export interface GHLCreateShippingRateResponse { - status: boolean; - message?: string; - data: GHLShippingRate; -} - -export interface GHLListShippingRatesResponse { - total: number; - data: GHLShippingRate[]; -} - -export interface GHLGetShippingRateResponse { - status: boolean; - message?: string; - data: GHLShippingRate; -} - -export interface GHLUpdateShippingRateResponse { - status: boolean; - message?: string; - data: GHLShippingRate; -} - -export interface GHLDeleteShippingRateResponse { - status: boolean; - message?: string; -} - -export interface GHLCreateShippingCarrierResponse { - status: boolean; - message?: string; - data: GHLShippingCarrier; -} - -export interface GHLListShippingCarriersResponse { - status: boolean; - message?: string; - data: GHLShippingCarrier[]; -} - -export interface GHLGetShippingCarrierResponse { - status: boolean; - message?: string; - data: GHLShippingCarrier; -} - -export interface GHLUpdateShippingCarrierResponse { - status: boolean; - message?: string; - data: GHLShippingCarrier; -} - -export interface GHLDeleteShippingCarrierResponse { - status: boolean; - message?: string; -} - -export interface GHLGetAvailableShippingRatesResponse { - status: boolean; - message?: string; - data: GHLAvailableShippingRate[]; -} - -// Store Settings Types -export interface GHLStoreShippingOrigin { - name: string; - country: GHLCountryCode; - state?: GHLStateCode; - city: string; - street1: string; - street2?: string; - zip: string; - phone?: string; - email?: string; -} - -export interface GHLStoreOrderNotification { - enabled: boolean; - subject: string; - emailTemplateId: string; - defaultEmailTemplateId: string; -} - -export interface GHLStoreOrderFulfillmentNotification { - enabled: boolean; - subject: string; - emailTemplateId: string; - defaultEmailTemplateId: string; -} - -export interface GHLCreateStoreSettingRequest { - altId: string; - altType: 'location'; - shippingOrigin: GHLStoreShippingOrigin; - storeOrderNotification?: GHLStoreOrderNotification; - storeOrderFulfillmentNotification?: GHLStoreOrderFulfillmentNotification; -} - -export interface GHLGetStoreSettingRequest { - altId: string; - altType: 'location'; -} - -export interface GHLStoreSetting { - altId: string; - altType: 'location'; - shippingOrigin: GHLStoreShippingOrigin; - storeOrderNotification?: GHLStoreOrderNotification; - storeOrderFulfillmentNotification?: GHLStoreOrderFulfillmentNotification; - _id: string; - createdAt: string; - updatedAt: string; -} - -export interface GHLCreateStoreSettingResponse { - status: boolean; - message?: string; - data: GHLStoreSetting; -} - -export interface GHLGetStoreSettingResponse { - status: boolean; - message?: string; - data: GHLStoreSetting; -} - -// MCP Tool Parameters - Store API - -// Shipping Zone MCP Parameters -export interface MCPCreateShippingZoneParams { - locationId?: string; - name: string; - countries: GHLShippingZoneCountry[]; -} - -export interface MCPListShippingZonesParams { - locationId?: string; - limit?: number; - offset?: number; - withShippingRate?: boolean; -} - -export interface MCPGetShippingZoneParams { - shippingZoneId: string; - locationId?: string; - withShippingRate?: boolean; -} - -export interface MCPUpdateShippingZoneParams { - shippingZoneId: string; - locationId?: string; - name?: string; - countries?: GHLShippingZoneCountry[]; -} - -export interface MCPDeleteShippingZoneParams { - shippingZoneId: string; - locationId?: string; -} - -// Shipping Rate MCP Parameters -export interface MCPCreateShippingRateParams { - shippingZoneId: string; - locationId?: string; - name: string; - description?: string; - currency: string; - amount: number; - conditionType: GHLShippingConditionType; - minCondition?: number; - maxCondition?: number; - isCarrierRate?: boolean; - shippingCarrierId: string; - percentageOfRateFee?: number; - shippingCarrierServices?: GHLShippingCarrierService[]; -} - -export interface MCPListShippingRatesParams { - shippingZoneId: string; - locationId?: string; - limit?: number; - offset?: number; -} - -export interface MCPGetShippingRateParams { - shippingZoneId: string; - shippingRateId: string; - locationId?: string; -} - -export interface MCPUpdateShippingRateParams { - shippingZoneId: string; - shippingRateId: string; - locationId?: string; - name?: string; - description?: string; - currency?: string; - amount?: number; - conditionType?: GHLShippingConditionType; - minCondition?: number; - maxCondition?: number; - isCarrierRate?: boolean; - shippingCarrierId?: string; - percentageOfRateFee?: number; - shippingCarrierServices?: GHLShippingCarrierService[]; -} - -export interface MCPDeleteShippingRateParams { - shippingZoneId: string; - shippingRateId: string; - locationId?: string; -} - -export interface MCPGetAvailableShippingRatesParams { - locationId?: string; - country: GHLCountryCode; - address?: GHLContactAddress; - totalOrderAmount: number; - totalOrderWeight: number; - source: GHLOrderSource; - products: GHLProductItem[]; - couponCode?: string; -} - -// Shipping Carrier MCP Parameters -export interface MCPCreateShippingCarrierParams { - locationId?: string; - name: string; - callbackUrl: string; - services?: GHLShippingCarrierService[]; - allowsMultipleServiceSelection?: boolean; -} - -export interface MCPListShippingCarriersParams { - locationId?: string; -} - -export interface MCPGetShippingCarrierParams { - shippingCarrierId: string; - locationId?: string; -} - -export interface MCPUpdateShippingCarrierParams { - shippingCarrierId: string; - locationId?: string; - name?: string; - callbackUrl?: string; - services?: GHLShippingCarrierService[]; - allowsMultipleServiceSelection?: boolean; -} - -export interface MCPDeleteShippingCarrierParams { - shippingCarrierId: string; - locationId?: string; -} - -// Store Settings MCP Parameters -export interface MCPCreateStoreSettingParams { - locationId?: string; - shippingOrigin: GHLStoreShippingOrigin; - storeOrderNotification?: GHLStoreOrderNotification; - storeOrderFulfillmentNotification?: GHLStoreOrderFulfillmentNotification; -} - -export interface MCPGetStoreSettingParams { - locationId?: string; -} - -// Products API Types - -// Core Product Types -export type GHLProductType = 'DIGITAL' | 'PHYSICAL' | 'SERVICE' | 'PHYSICAL/DIGITAL'; -export type GHLPriceType = 'one_time' | 'recurring'; -export type GHLRecurringInterval = 'day' | 'month' | 'week' | 'year'; -export type GHLWeightUnit = 'kg' | 'lb' | 'g' | 'oz'; -export type GHLDimensionUnit = 'cm' | 'in' | 'm'; -export type GHLMediaType = 'image' | 'video'; -export type GHLSortOrder = 'asc' | 'desc'; -export type GHLReviewSortField = 'createdAt' | 'rating'; -export type GHLBulkUpdateType = 'bulk-update-price' | 'bulk-update-availability' | 'bulk-update-product-collection' | 'bulk-delete-products' | 'bulk-update-currency'; -export type GHLPriceUpdateType = 'INCREASE_BY_AMOUNT' | 'REDUCE_BY_AMOUNT' | 'SET_NEW_PRICE' | 'INCREASE_BY_PERCENTAGE' | 'REDUCE_BY_PERCENTAGE'; -export type GHLStoreAction = 'include' | 'exclude'; -export type GHLAltType = 'location'; - -// Product Variant Types -export interface GHLProductVariantOption { - id: string; - name: string; -} - -export interface GHLProductVariant { - id: string; - name: string; - options: GHLProductVariantOption[]; -} - -// Product Media Types -export interface GHLProductMedia { - id: string; - title?: string; - url: string; - type: GHLMediaType; - isFeatured?: boolean; - priceIds?: string[]; -} - -// Product Label Types -export interface GHLProductLabel { - title: string; - startDate?: string; - endDate?: string; -} - -// Product SEO Types -export interface GHLProductSEO { - title?: string; - description?: string; -} - -// Price Types -export interface GHLRecurring { - interval: GHLRecurringInterval; - intervalCount: number; -} - -export interface GHLMembershipOffer { - label: string; - value: string; - _id: string; -} - -export interface GHLPriceMeta { - source: 'stripe' | 'woocommerce' | 'shopify'; - sourceId?: string; - stripePriceId: string; - internalSource: 'agency_plan' | 'funnel' | 'membership' | 'communities' | 'gokollab'; -} - -export interface GHLWeightOptions { - value: number; - unit: GHLWeightUnit; -} - -export interface GHLPriceDimensions { - height: number; - width: number; - length: number; - unit: GHLDimensionUnit; -} - -export interface GHLShippingOptions { - weight?: GHLWeightOptions; - dimensions?: GHLPriceDimensions; -} - -// Collection Types -export interface GHLCollectionSEO { - title?: string; - description?: string; -} - -export interface GHLProductCollection { - _id: string; - altId: string; - name: string; - slug: string; - image?: string; - seo?: GHLCollectionSEO; - createdAt: string; -} - -// Review Types -export interface GHLUserDetails { - name: string; - email: string; - phone?: string; - isCustomer?: boolean; -} - -export interface GHLProductReview { - headline: string; - comment: string; - user: GHLUserDetails; -} - -// Inventory Types -export interface GHLInventoryItem { - _id: string; - name: string; - availableQuantity: number; - sku?: string; - allowOutOfStockPurchases: boolean; - product: string; - updatedAt: string; - image?: string; - productName?: string; -} - -// Core Product Interface -export interface GHLProduct { - _id: string; - description?: string; - variants?: GHLProductVariant[]; - medias?: GHLProductMedia[]; - locationId: string; - name: string; - productType: GHLProductType; - availableInStore?: boolean; - userId?: string; - createdAt: string; - updatedAt: string; - statementDescriptor?: string; - image?: string; - collectionIds?: string[]; - isTaxesEnabled?: boolean; - taxes?: string[]; - automaticTaxCategoryId?: string; - isLabelEnabled?: boolean; - label?: GHLProductLabel; - slug?: string; - seo?: GHLProductSEO; -} - -// Price Interface -export interface GHLPrice { - _id: string; - membershipOffers?: GHLMembershipOffer[]; - variantOptionIds?: string[]; - locationId: string; - product: string; - userId?: string; - name: string; - type: GHLPriceType; - currency: string; - amount: number; - recurring?: GHLRecurring; - description?: string; - trialPeriod?: number; - totalCycles?: number; - setupFee?: number; - compareAtPrice?: number; - createdAt: string; - updatedAt: string; - meta?: GHLPriceMeta; - trackInventory?: boolean; - availableQuantity?: number; - allowOutOfStockPurchases?: boolean; - sku?: string; - shippingOptions?: GHLShippingOptions; - isDigitalProduct?: boolean; - digitalDelivery?: string[]; -} - -// Request Types - -// Product Requests -export interface GHLCreateProductRequest { - name: string; - locationId: string; - description?: string; - productType: GHLProductType; - image?: string; - statementDescriptor?: string; - availableInStore?: boolean; - medias?: GHLProductMedia[]; - variants?: GHLProductVariant[]; - collectionIds?: string[]; - isTaxesEnabled?: boolean; - taxes?: string[]; - automaticTaxCategoryId?: string; - isLabelEnabled?: boolean; - label?: GHLProductLabel; - slug?: string; - seo?: GHLProductSEO; -} - -export interface GHLUpdateProductRequest { - name?: string; - locationId?: string; - description?: string; - productType?: GHLProductType; - image?: string; - statementDescriptor?: string; - availableInStore?: boolean; - medias?: GHLProductMedia[]; - variants?: GHLProductVariant[]; - collectionIds?: string[]; - isTaxesEnabled?: boolean; - taxes?: string[]; - automaticTaxCategoryId?: string; - isLabelEnabled?: boolean; - label?: GHLProductLabel; - slug?: string; - seo?: GHLProductSEO; -} - -export interface GHLListProductsRequest { - locationId: string; - limit?: number; - offset?: number; - search?: string; - collectionIds?: string[]; - collectionSlug?: string; - expand?: string[]; - productIds?: string[]; - storeId?: string; - includedInStore?: boolean; - availableInStore?: boolean; - sortOrder?: GHLSortOrder; -} - -export interface GHLGetProductRequest { - productId: string; - locationId: string; -} - -export interface GHLDeleteProductRequest { - productId: string; - locationId: string; -} - -// Price Requests -export interface GHLCreatePriceRequest { - name: string; - type: GHLPriceType; - currency: string; - amount: number; - recurring?: GHLRecurring; - description?: string; - membershipOffers?: GHLMembershipOffer[]; - trialPeriod?: number; - totalCycles?: number; - setupFee?: number; - variantOptionIds?: string[]; - compareAtPrice?: number; - locationId: string; - userId?: string; - meta?: GHLPriceMeta; - trackInventory?: boolean; - availableQuantity?: number; - allowOutOfStockPurchases?: boolean; - sku?: string; - shippingOptions?: GHLShippingOptions; - isDigitalProduct?: boolean; - digitalDelivery?: string[]; -} - -export interface GHLUpdatePriceRequest { - name?: string; - type?: GHLPriceType; - currency?: string; - amount?: number; - recurring?: GHLRecurring; - description?: string; - membershipOffers?: GHLMembershipOffer[]; - trialPeriod?: number; - totalCycles?: number; - setupFee?: number; - variantOptionIds?: string[]; - compareAtPrice?: number; - locationId?: string; - userId?: string; - meta?: GHLPriceMeta; - trackInventory?: boolean; - availableQuantity?: number; - allowOutOfStockPurchases?: boolean; - sku?: string; - shippingOptions?: GHLShippingOptions; - isDigitalProduct?: boolean; - digitalDelivery?: string[]; -} - -export interface GHLListPricesRequest { - productId: string; - locationId: string; - limit?: number; - offset?: number; - ids?: string; -} - -export interface GHLGetPriceRequest { - productId: string; - priceId: string; - locationId: string; -} - -export interface GHLDeletePriceRequest { - productId: string; - priceId: string; - locationId: string; -} - -// Bulk Update Requests -export interface GHLBulkUpdateFilters { - collectionIds?: string[]; - productType?: string; - availableInStore?: boolean; - search?: string; -} - -export interface GHLPriceUpdateField { - type: GHLPriceUpdateType; - value: number; - roundToWhole?: boolean; -} - -export interface GHLBulkUpdateRequest { - altId: string; - altType: GHLAltType; - type: GHLBulkUpdateType; - productIds: string[]; - filters?: GHLBulkUpdateFilters; - price?: GHLPriceUpdateField; - compareAtPrice?: GHLPriceUpdateField; - availability?: boolean; - collectionIds?: string[]; - currency?: string; -} - -// Inventory Requests -export interface GHLListInventoryRequest { - altId: string; - altType: GHLAltType; - limit?: number; - offset?: number; - search?: string; -} - -export interface GHLUpdateInventoryItem { - priceId: string; - availableQuantity?: number; - allowOutOfStockPurchases?: boolean; -} - -export interface GHLUpdateInventoryRequest { - altId: string; - altType: GHLAltType; - items: GHLUpdateInventoryItem[]; -} - -// Store Requests -export interface GHLGetProductStoreStatsRequest { - storeId: string; - altId: string; - altType: GHLAltType; - search?: string; - collectionIds?: string; -} - -export interface GHLUpdateProductStoreRequest { - action: GHLStoreAction; - productIds: string[]; -} - -// Collection Requests -export interface GHLCreateProductCollectionRequest { - altId: string; - altType: GHLAltType; - collectionId?: string; - name: string; - slug: string; - image?: string; - seo?: GHLCollectionSEO; -} - -export interface GHLUpdateProductCollectionRequest { - altId: string; - altType: GHLAltType; - name?: string; - slug?: string; - image?: string; - seo?: GHLCollectionSEO; -} - -export interface GHLListProductCollectionsRequest { - altId: string; - altType: GHLAltType; - limit?: number; - offset?: number; - collectionIds?: string; - name?: string; -} - -export interface GHLGetProductCollectionRequest { - collectionId: string; -} - -export interface GHLDeleteProductCollectionRequest { - collectionId: string; - altId: string; - altType: GHLAltType; -} - -// Review Requests -export interface GHLListProductReviewsRequest { - altId: string; - altType: GHLAltType; - limit?: number; - offset?: number; - sortField?: GHLReviewSortField; - sortOrder?: GHLSortOrder; - rating?: number; - startDate?: string; - endDate?: string; - productId?: string; - storeId?: string; -} - -export interface GHLGetReviewsCountRequest { - altId: string; - altType: GHLAltType; - rating?: number; - startDate?: string; - endDate?: string; - productId?: string; - storeId?: string; -} - -export interface GHLUpdateProductReviewRequest { - altId: string; - altType: GHLAltType; - productId: string; - status: string; - reply?: GHLProductReview[]; - rating?: number; - headline?: string; - detail?: string; -} - -export interface GHLDeleteProductReviewRequest { - reviewId: string; - altId: string; - altType: GHLAltType; - productId: string; -} - -export interface GHLUpdateProductReviewObject { - reviewId: string; - productId: string; - storeId: string; -} - -export interface GHLBulkUpdateProductReviewsRequest { - altId: string; - altType: GHLAltType; - reviews: GHLUpdateProductReviewObject[]; - status: any; -} - -// Response Types - -// Product Responses -export interface GHLCreateProductResponse { - _id: string; - description?: string; - variants?: GHLProductVariant[]; - medias?: GHLProductMedia[]; - locationId: string; - name: string; - productType: GHLProductType; - availableInStore?: boolean; - userId?: string; - createdAt: string; - updatedAt: string; - statementDescriptor?: string; - image?: string; - collectionIds?: string[]; - isTaxesEnabled?: boolean; - taxes?: string[]; - automaticTaxCategoryId?: string; - isLabelEnabled?: boolean; - label?: GHLProductLabel; - slug?: string; - seo?: GHLProductSEO; -} - -export interface GHLUpdateProductResponse extends GHLCreateProductResponse {} - -export interface GHLGetProductResponse extends GHLCreateProductResponse {} - -export interface GHLListProductsStats { - total: number; -} - -export interface GHLListProductsResponse { - products: GHLProduct[]; - total: GHLListProductsStats[]; -} - -export interface GHLDeleteProductResponse { - status: boolean; -} - -// Price Responses -export interface GHLCreatePriceResponse extends GHLPrice {} -export interface GHLUpdatePriceResponse extends GHLPrice {} -export interface GHLGetPriceResponse extends GHLPrice {} - -export interface GHLListPricesResponse { - prices: GHLPrice[]; - total: number; -} - -export interface GHLDeletePriceResponse { - status: boolean; -} - -// Bulk Update Response -export interface GHLBulkUpdateResponse { - status: boolean; - message?: string; -} - -// Inventory Responses -export interface GHLListInventoryResponse { - inventory: GHLInventoryItem[]; - total: { total: number }; -} - -export interface GHLUpdateInventoryResponse { - status: boolean; - message?: string; -} - -// Store Responses -export interface GHLGetProductStoreStatsResponse { - totalProducts: number; - includedInStore: number; - excludedFromStore: number; -} - -export interface GHLUpdateProductStoreResponse { - status: boolean; - message?: string; -} - -// Collection Responses -export interface GHLCreateCollectionResponse { - data: GHLProductCollection; -} - -export interface GHLUpdateProductCollectionResponse { - status: boolean; - message?: string; -} - -export interface GHLListCollectionResponse { - data: any[]; - total: number; -} - -export interface GHLDefaultCollectionResponse { - data: any; - status: boolean; -} - -export interface GHLDeleteProductCollectionResponse { - status: boolean; - message?: string; -} - -// Review Responses -export interface GHLListProductReviewsResponse { - data: any[]; - total: number; -} - -export interface GHLCountReviewsByStatusResponse { - data: any[]; -} - -export interface GHLUpdateProductReviewsResponse { - status: boolean; - message?: string; -} - -export interface GHLDeleteProductReviewResponse { - status: boolean; - message?: string; -} - -// MCP Tool Parameters - Products API - -// Product MCP Parameters -export interface MCPCreateProductParams { - locationId?: string; - name: string; - productType: GHLProductType; - description?: string; - image?: string; - statementDescriptor?: string; - availableInStore?: boolean; - medias?: GHLProductMedia[]; - variants?: GHLProductVariant[]; - collectionIds?: string[]; - isTaxesEnabled?: boolean; - taxes?: string[]; - automaticTaxCategoryId?: string; - isLabelEnabled?: boolean; - label?: GHLProductLabel; - slug?: string; - seo?: GHLProductSEO; -} - -export interface MCPUpdateProductParams { - productId: string; - locationId?: string; - name?: string; - productType?: GHLProductType; - description?: string; - image?: string; - statementDescriptor?: string; - availableInStore?: boolean; - medias?: GHLProductMedia[]; - variants?: GHLProductVariant[]; - collectionIds?: string[]; - isTaxesEnabled?: boolean; - taxes?: string[]; - automaticTaxCategoryId?: string; - isLabelEnabled?: boolean; - label?: GHLProductLabel; - slug?: string; - seo?: GHLProductSEO; -} - -export interface MCPListProductsParams { - locationId?: string; - limit?: number; - offset?: number; - search?: string; - collectionIds?: string[]; - collectionSlug?: string; - expand?: string[]; - productIds?: string[]; - storeId?: string; - includedInStore?: boolean; - availableInStore?: boolean; - sortOrder?: GHLSortOrder; -} - -export interface MCPGetProductParams { - productId: string; - locationId?: string; -} - -export interface MCPDeleteProductParams { - productId: string; - locationId?: string; -} - -// Price MCP Parameters -export interface MCPCreatePriceParams { - productId: string; - name: string; - type: GHLPriceType; - currency: string; - amount: number; - locationId?: string; - recurring?: GHLRecurring; - description?: string; - membershipOffers?: GHLMembershipOffer[]; - trialPeriod?: number; - totalCycles?: number; - setupFee?: number; - variantOptionIds?: string[]; - compareAtPrice?: number; - userId?: string; - meta?: GHLPriceMeta; - trackInventory?: boolean; - availableQuantity?: number; - allowOutOfStockPurchases?: boolean; - sku?: string; - shippingOptions?: GHLShippingOptions; - isDigitalProduct?: boolean; - digitalDelivery?: string[]; -} - -export interface MCPUpdatePriceParams { - productId: string; - priceId: string; - name?: string; - type?: GHLPriceType; - currency?: string; - amount?: number; - locationId?: string; - recurring?: GHLRecurring; - description?: string; - membershipOffers?: GHLMembershipOffer[]; - trialPeriod?: number; - totalCycles?: number; - setupFee?: number; - variantOptionIds?: string[]; - compareAtPrice?: number; - userId?: string; - meta?: GHLPriceMeta; - trackInventory?: boolean; - availableQuantity?: number; - allowOutOfStockPurchases?: boolean; - sku?: string; - shippingOptions?: GHLShippingOptions; - isDigitalProduct?: boolean; - digitalDelivery?: string[]; -} - -export interface MCPListPricesParams { - productId: string; - locationId?: string; - limit?: number; - offset?: number; - ids?: string; -} - -export interface MCPGetPriceParams { - productId: string; - priceId: string; - locationId?: string; -} - -export interface MCPDeletePriceParams { - productId: string; - priceId: string; - locationId?: string; -} - -// Bulk Update MCP Parameters -export interface MCPBulkUpdateProductsParams { - locationId?: string; - type: GHLBulkUpdateType; - productIds: string[]; - filters?: GHLBulkUpdateFilters; - price?: GHLPriceUpdateField; - compareAtPrice?: GHLPriceUpdateField; - availability?: boolean; - collectionIds?: string[]; - currency?: string; -} - -// Inventory MCP Parameters -export interface MCPListInventoryParams { - locationId?: string; - limit?: number; - offset?: number; - search?: string; -} - -export interface MCPUpdateInventoryParams { - locationId?: string; - items: GHLUpdateInventoryItem[]; -} - -// Store MCP Parameters -export interface MCPGetProductStoreStatsParams { - storeId: string; - locationId?: string; - search?: string; - collectionIds?: string; -} - -export interface MCPUpdateProductStoreParams { - storeId: string; - action: GHLStoreAction; - productIds: string[]; -} - -// Collection MCP Parameters -export interface MCPCreateProductCollectionParams { - locationId?: string; - collectionId?: string; - name: string; - slug: string; - image?: string; - seo?: GHLCollectionSEO; -} - -export interface MCPUpdateProductCollectionParams { - collectionId: string; - locationId?: string; - name?: string; - slug?: string; - image?: string; - seo?: GHLCollectionSEO; -} - -export interface MCPListProductCollectionsParams { - locationId?: string; - limit?: number; - offset?: number; - collectionIds?: string; - name?: string; -} - -export interface MCPGetProductCollectionParams { - collectionId: string; -} - -export interface MCPDeleteProductCollectionParams { - collectionId: string; - locationId?: string; -} - -// Review MCP Parameters -export interface MCPListProductReviewsParams { - locationId?: string; - limit?: number; - offset?: number; - sortField?: GHLReviewSortField; - sortOrder?: GHLSortOrder; - rating?: number; - startDate?: string; - endDate?: string; - productId?: string; - storeId?: string; -} - -export interface MCPGetReviewsCountParams { - locationId?: string; - rating?: number; - startDate?: string; - endDate?: string; - productId?: string; - storeId?: string; -} - -export interface MCPUpdateProductReviewParams { - reviewId: string; - locationId?: string; - productId: string; - status: string; - reply?: GHLProductReview[]; - rating?: number; - headline?: string; - detail?: string; -} - -export interface MCPDeleteProductReviewParams { - reviewId: string; - locationId?: string; - productId: string; -} - -export interface MCPBulkUpdateProductReviewsParams { - locationId?: string; - reviews: GHLUpdateProductReviewObject[]; - status: any; -} - -// ============================================================================= -// PAYMENTS API TYPES -// ============================================================================= - -// Integration Provider Types -export interface CreateWhiteLabelIntegrationProviderDto { - altId: string; - altType: 'location'; - uniqueName: string; - title: string; - provider: 'authorize-net' | 'nmi'; - description: string; - imageUrl: string; -} - -export interface IntegrationProvider { - _id: string; - altId: string; - altType: string; - title: string; - route: string; - provider: string; - description: string; - imageUrl: string; - createdAt: string; - updatedAt: string; -} - -export interface ListIntegrationProvidersResponse { - providers: IntegrationProvider[]; -} - -// Order Types -export interface OrderSource { - type: 'funnel' | 'website' | 'invoice' | 'calendar' | 'text2Pay' | 'document_contracts' | 'membership' | 'mobile_app' | 'communities' | 'point_of_sale' | 'manual' | 'form' | 'survey' | 'payment_link' | 'external'; - subType?: 'one_step_order_form' | 'two_step_order_form' | 'upsell' | 'tap_to_pay' | 'card_payment' | 'store' | 'contact_view' | 'email_campaign' | 'payments_dashboard' | 'shopify' | 'subscription_view' | 'store_upsell' | 'woocommerce' | 'service' | 'meeting' | 'imported_csv' | 'qr_code'; - id: string; - name?: string; - meta?: Record; -} - -export interface AmountSummary { - subtotal: number; - discount?: number; -} - -export interface Order { - _id: string; - altId: string; - altType: string; - contactId?: string; - contactName?: string; - contactEmail?: string; - currency?: string; - amount?: number; - subtotal?: number; - discount?: number; - status: string; - liveMode?: boolean; - totalProducts?: number; - sourceType?: string; - sourceName?: string; - sourceId?: string; - sourceMeta?: Record; - couponCode?: string; - createdAt: string; - updatedAt: string; - sourceSubType?: string; - fulfillmentStatus?: string; - onetimeProducts?: number; - recurringProducts?: number; - contactSnapshot?: Record; - amountSummary?: AmountSummary; - source?: OrderSource; - items?: string[]; - coupon?: Record; - trackingId?: string; - fingerprint?: string; - meta?: Record; - markAsTest?: boolean; - traceId?: string; -} - -export interface ListOrdersResponse { - data: Order[]; - totalCount: number; -} - -// Fulfillment Types -export interface FulfillmentTracking { - trackingNumber?: string; - shippingCarrier?: string; - trackingUrl?: string; -} - -export interface FulfillmentItems { - priceId: string; - qty: number; -} - -export interface CreateFulfillmentDto { - altId: string; - altType: 'location'; - trackings: FulfillmentTracking[]; - items: FulfillmentItems[]; - notifyCustomer: boolean; -} - -export interface ProductVariantOption { - id: string; - name: string; -} - -export interface ProductVariant { - id: string; - name: string; - options: ProductVariantOption[]; -} - -export interface ProductMedia { - id: string; - title?: string; - url: string; - type: 'image' | 'video'; - isFeatured?: boolean; - priceIds?: string[][]; -} - -export interface ProductLabel { - title: string; - startDate?: string; - endDate?: string; -} - -export interface ProductSEO { - title?: string; - description?: string; -} - -export interface DefaultProduct { - _id: string; - description?: string; - variants?: ProductVariant[]; - medias?: ProductMedia[]; - locationId: string; - name: string; - productType: string; - availableInStore?: boolean; - userId?: string; - createdAt: string; - updatedAt: string; - statementDescriptor?: string; - image?: string; - collectionIds?: string[]; - isTaxesEnabled?: boolean; - taxes?: string[]; - isLabelEnabled?: boolean; - label?: ProductLabel; - slug?: string; - seo?: ProductSEO; -} - -export interface MembershipOffer { - label: string; - value: string; - _id: string; -} - -export interface Recurring { - interval: 'day' | 'month' | 'week' | 'year'; - intervalCount: number; -} - -export interface DefaultPrice { - _id: string; - membershipOffers?: MembershipOffer[]; - variantOptionIds?: string[]; - locationId?: string; - product?: string; - userId?: string; - name: string; - type: 'one_time' | 'recurring'; - currency: string; - amount: number; - recurring?: Recurring; - createdAt?: string; - updatedAt?: string; - compareAtPrice?: number; - trackInventory?: boolean; - availableQuantity?: number; - allowOutOfStockPurchases?: boolean; -} - -export interface FulfilledItem { - _id: string; - name: string; - product: DefaultProduct; - price: DefaultPrice; - qty: number; -} - -export interface Fulfillment { - altId: string; - altType: 'location'; - trackings: FulfillmentTracking[]; - _id: string; - items: FulfilledItem[]; - createdAt: string; - updatedAt: string; -} - -export interface CreateFulfillmentResponse { - status: boolean; - data: Fulfillment; -} - -export interface ListFulfillmentResponse { - status: boolean; - data: Fulfillment[]; -} - -// Transaction Types -export interface Transaction { - _id: string; - altId: string; - altType: string; - contactId?: string; - contactName?: string; - contactEmail?: string; - currency?: string; - amount?: number; - status: Record; - liveMode?: boolean; - entityType?: string; - entityId?: string; - entitySourceType?: string; - entitySourceSubType?: string; - entitySourceName?: string; - entitySourceId?: string; - entitySourceMeta?: Record; - subscriptionId?: string; - chargeId?: string; - chargeSnapshot?: Record; - paymentProviderType?: string; - paymentProviderConnectedAccount?: string; - ipAddress?: string; - createdAt: string; - updatedAt: string; - amountRefunded?: number; - paymentMethod?: Record; - contactSnapshot?: Record; - entitySource?: OrderSource; - invoiceId?: string; - paymentProvider?: Record; - meta?: Record; - markAsTest?: boolean; - isParent?: boolean; - receiptId?: string; - qboSynced?: boolean; - qboResponse?: Record; - traceId?: string; -} - -export interface ListTransactionsResponse { - data: Transaction[]; - totalCount: number; -} - -// Subscription Types -export interface CustomRRuleOptions { - intervalType: 'yearly' | 'monthly' | 'weekly' | 'daily' | 'hourly' | 'minutely' | 'secondly'; - interval: number; - startDate: string; - startTime?: string; - endDate?: string; - endTime?: string; - dayOfMonth?: number; - dayOfWeek?: 'mo' | 'tu' | 'we' | 'th' | 'fr' | 'sa' | 'su'; - numOfWeek?: number; - monthOfYear?: 'jan' | 'feb' | 'mar' | 'apr' | 'may' | 'jun' | 'jul' | 'aug' | 'sep' | 'oct' | 'nov' | 'dec'; - count?: number; - daysBefore?: number; -} - -export interface ScheduleOptions { - executeAt?: string; - rrule?: CustomRRuleOptions; -} - -export interface Subscription { - _id: string; - altId: string; - altType: 'location'; - contactId?: string; - contactName?: string; - contactEmail?: string; - currency?: string; - amount?: number; - status: Record; - liveMode?: boolean; - entityType?: string; - entityId?: string; - entitySourceType?: string; - entitySourceName?: string; - entitySourceId?: string; - entitySourceMeta?: Record; - subscriptionId?: string; - subscriptionSnapshot?: Record; - paymentProviderType?: string; - paymentProviderConnectedAccount?: string; - ipAddress?: string; - createdAt: string; - updatedAt: string; - contactSnapshot?: Record; - coupon?: Record; - entitySource?: OrderSource; - paymentProvider?: Record; - meta?: Record; - markAsTest?: boolean; - schedule?: ScheduleOptions; - autoPayment?: Record; - recurringProduct?: Record; - canceledAt?: string; - canceledBy?: string; - traceId?: string; -} - -export interface ListSubscriptionsResponse { - data: Subscription[]; - totalCount: number; -} - -// Coupon Types -export interface ApplyToFuturePaymentsConfig { - type: 'forever' | 'fixed'; - duration?: number; - durationType?: 'months'; -} - -export interface Coupon { - _id: string; - usageCount: number; - hasAffiliateCoupon?: boolean; - deleted?: boolean; - limitPerCustomer: number; - altId: string; - altType: string; - name: string; - code: string; - discountType: 'percentage' | 'amount'; - discountValue: number; - status: 'scheduled' | 'active' | 'expired'; - startDate: string; - endDate?: string; - applyToFuturePayments: boolean; - applyToFuturePaymentsConfig: ApplyToFuturePaymentsConfig; - userId?: string; - createdAt: string; - updatedAt: string; -} - -export interface ListCouponsResponse { - data: Coupon[]; - totalCount: number; - traceId: string; -} - -export interface CreateCouponParams { - altId: string; - altType: 'location'; - name: string; - code: string; - discountType: 'percentage' | 'amount'; - discountValue: number; - startDate: string; - endDate?: string; - usageLimit?: number; - productIds?: string[]; - applyToFuturePayments?: boolean; - applyToFuturePaymentsConfig?: ApplyToFuturePaymentsConfig; - limitPerCustomer?: boolean; -} - -export interface UpdateCouponParams extends CreateCouponParams { - id: string; -} - -export interface DeleteCouponParams { - altId: string; - altType: 'location'; - id: string; -} - -export interface CreateCouponResponse extends Coupon { - traceId: string; -} - -export interface DeleteCouponResponse { - success: boolean; - traceId: string; -} - -// Custom Provider Types -export interface CreateCustomProviderDto { - name: string; - description: string; - paymentsUrl: string; - queryUrl: string; - imageUrl: string; -} - -export interface CustomProvider { - name: string; - description: string; - paymentsUrl: string; - queryUrl: string; - imageUrl: string; - _id: string; - locationId: string; - marketplaceAppId: string; - paymentProvider: Record; - deleted: boolean; - createdAt: string; - updatedAt: string; - traceId?: string; -} - -export interface CustomProviderKeys { - apiKey: string; - publishableKey: string; -} - -export interface ConnectCustomProviderConfigDto { - live: CustomProviderKeys; - test: CustomProviderKeys; -} - -export interface DeleteCustomProviderConfigDto { - liveMode: boolean; -} - -export interface DeleteCustomProviderResponse { - success: boolean; -} - -export interface DisconnectCustomProviderResponse { - success: boolean; -} - -// ============================================================================= -// INVOICES API TYPES -// ============================================================================= - -// Address and Business Details -export interface AddressDto { - addressLine1?: string; - addressLine2?: string; - city?: string; - state?: string; - countryCode?: string; - postalCode?: string; -} - -export interface BusinessDetailsDto { - logoUrl?: string; - name?: string; - phoneNo?: string; - address?: AddressDto; - website?: string; - customValues?: string[]; -} - -// Contact Details -export interface AdditionalEmailsDto { - email: string; -} - -export interface ContactDetailsDto { - id: string; - name: string; - phoneNo?: string; - email?: string; - additionalEmails?: AdditionalEmailsDto[]; - companyName?: string; - address?: AddressDto; - customFields?: string[]; -} - -// Invoice Items and Taxes -export interface ItemTaxDto { - _id: string; - name: string; - rate: number; - calculation: 'exclusive'; - description?: string; - taxId?: string; -} - -export interface InvoiceItemDto { - name: string; - description?: string; - productId?: string; - priceId?: string; - currency: string; - amount: number; - qty: number; - taxes?: ItemTaxDto[]; - automaticTaxCategoryId?: string; - isSetupFeeItem?: boolean; - type?: 'one_time' | 'recurring'; - taxInclusive?: boolean; -} - -// Discount -export interface DiscountDto { - value?: number; - type: 'percentage' | 'fixed'; - validOnProductIds?: string[]; -} - -// Tips Configuration -export interface TipsConfigurationDto { - tipsPercentage: string[]; - tipsEnabled: boolean; -} - -// Late Fees Configuration -export interface LateFeesFrequencyDto { - intervalCount?: number; - interval: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'one_time'; -} - -export interface LateFeesGraceDto { - intervalCount: number; - interval: 'day'; -} - -export interface LateFeesMaxFeesDto { - type: 'fixed'; - value: number; -} - -export interface LateFeesConfigurationDto { - enable: boolean; - value: number; - type: 'fixed' | 'percentage'; - frequency: LateFeesFrequencyDto; - grace?: LateFeesGraceDto; - maxLateFees?: LateFeesMaxFeesDto; -} - -// Payment Methods -export interface StripePaymentMethodDto { - enableBankDebitOnly: boolean; -} - -export interface PaymentMethodDto { - stripe: StripePaymentMethodDto; -} - -// Invoice Template Types -export interface CreateInvoiceTemplateDto { - altId: string; - altType: 'location'; - internal?: boolean; - name: string; - businessDetails: BusinessDetailsDto; - currency: string; - items: InvoiceItemDto[]; - automaticTaxesEnabled?: boolean; - discount?: DiscountDto; - termsNotes?: string; - title?: string; - tipsConfiguration?: TipsConfigurationDto; - lateFeesConfiguration?: LateFeesConfigurationDto; - invoiceNumberPrefix?: string; - paymentMethods?: PaymentMethodDto; - attachments?: string[]; -} - -export interface UpdateInvoiceTemplateDto { - altId: string; - altType: 'location'; - internal?: boolean; - name: string; - businessDetails: BusinessDetailsDto; - currency: string; - items: InvoiceItemDto[]; - discount?: DiscountDto; - termsNotes?: string; - title?: string; -} - -export interface InvoiceTemplate { - _id: string; - altId: string; - altType: string; - name: string; - businessDetails: BusinessDetailsDto; - currency: string; - discount: DiscountDto; - items: any[]; - invoiceNumberPrefix?: string; - total: number; - createdAt: string; - updatedAt: string; -} - -export interface ListTemplatesResponse { - data: InvoiceTemplate[]; - totalCount: number; -} - -export interface UpdateInvoiceLateFeesConfigurationDto { - altId: string; - altType: 'location'; - lateFeesConfiguration: LateFeesConfigurationDto; -} - -export interface UpdatePaymentMethodsConfigurationDto { - altId: string; - altType: 'location'; - paymentMethods?: PaymentMethodDto; -} - -// Schedule Types -export interface CustomRRuleOptionsDto { - intervalType: 'yearly' | 'monthly' | 'weekly' | 'daily' | 'hourly' | 'minutely' | 'secondly'; - interval: number; - startDate: string; - startTime?: string; - endDate?: string; - endTime?: string; - dayOfMonth?: number; - dayOfWeek?: 'mo' | 'tu' | 'we' | 'th' | 'fr' | 'sa' | 'su'; - numOfWeek?: number; - monthOfYear?: 'jan' | 'feb' | 'mar' | 'apr' | 'may' | 'jun' | 'jul' | 'aug' | 'sep' | 'oct' | 'nov' | 'dec'; - count?: number; - daysBefore?: number; - useStartAsPrimaryUserAccepted?: boolean; - endType?: string; -} - -export interface ScheduleOptionsDto { - executeAt?: string; - rrule?: CustomRRuleOptionsDto; -} - -export interface AttachmentsDto { - id: string; - name: string; - url: string; - type: string; - size: number; -} - -export interface CreateInvoiceScheduleDto { - altId: string; - altType: 'location'; - name: string; - contactDetails: ContactDetailsDto; - schedule: ScheduleOptionsDto; - liveMode: boolean; - businessDetails: BusinessDetailsDto; - currency: string; - items: InvoiceItemDto[]; - automaticTaxesEnabled?: boolean; - discount: DiscountDto; - termsNotes?: string; - title?: string; - tipsConfiguration?: TipsConfigurationDto; - lateFeesConfiguration?: LateFeesConfigurationDto; - invoiceNumberPrefix?: string; - paymentMethods?: PaymentMethodDto; - attachments?: AttachmentsDto[]; -} - -export interface UpdateInvoiceScheduleDto { - altId: string; - altType: 'location'; - name: string; - contactDetails: ContactDetailsDto; - schedule: ScheduleOptionsDto; - liveMode: boolean; - businessDetails: BusinessDetailsDto; - currency: string; - items: InvoiceItemDto[]; - discount: DiscountDto; - termsNotes?: string; - title?: string; - attachments?: AttachmentsDto[]; -} - -// Auto Payment Details -export interface CardDto { - brand: string; - last4: string; -} - -export interface USBankAccountDto { - bank_name: string; - last4: string; -} - -export interface SepaDirectDebitDto { - bank_code: string; - last4: string; - branch_code: string; -} - -export interface BacsDirectDebitDto { - sort_code: string; - last4: string; -} - -export interface BecsDirectDebitDto { - bsb_number: string; - last4: string; -} - -export interface AutoPaymentDetailsDto { - enable: boolean; - type?: string; - paymentMethodId?: string; - customerId?: string; - card?: CardDto; - usBankAccount?: USBankAccountDto; - sepaDirectDebit?: SepaDirectDebitDto; - bacsDirectDebit?: BacsDirectDebitDto; - becsDirectDebit?: BecsDirectDebitDto; - cardId?: string; -} - -export interface ScheduleInvoiceScheduleDto { - altId: string; - altType: 'location'; - liveMode: boolean; - autoPayment?: AutoPaymentDetailsDto; -} - -export interface AutoPaymentScheduleDto { - altId: string; - altType: 'location'; - id: string; - autoPayment: AutoPaymentDetailsDto; -} - -export interface CancelInvoiceScheduleDto { - altId: string; - altType: 'location'; -} - -// Invoice Types -export interface DefaultInvoiceResponseDto { - _id: string; - status: 'draft' | 'sent' | 'payment_processing' | 'paid' | 'void' | 'partially_paid'; - liveMode: boolean; - amountPaid: number; - altId: string; - altType: string; - name: string; - businessDetails: any; - invoiceNumber: string; - currency: string; - contactDetails: any; - issueDate: string; - dueDate: string; - discount: any; - invoiceItems: any[]; - total: number; - title: string; - amountDue: number; - createdAt: string; - updatedAt: string; - automaticTaxesEnabled?: boolean; - automaticTaxesCalculated?: boolean; - paymentSchedule?: any; -} - -export interface InvoiceSchedule { - _id: string; - status: any; - liveMode: boolean; - altId: string; - altType: string; - name: string; - schedule?: ScheduleOptionsDto; - invoices: DefaultInvoiceResponseDto[]; - businessDetails: BusinessDetailsDto; - currency: string; - contactDetails: ContactDetailsDto; - discount: DiscountDto; - items: any[]; - total: number; - title: string; - termsNotes: string; - compiledTermsNotes: string; - createdAt: string; - updatedAt: string; -} - -export interface ListSchedulesResponse { - schedules: InvoiceSchedule[]; - total: number; -} - -// Text2Pay Types -export interface SentToDto { - email: string[]; - emailCc?: string[]; - emailBcc?: string[]; - phoneNo?: string[]; -} - -export interface PaymentScheduleDto { - type: 'fixed' | 'percentage'; - schedules: string[]; -} - -export interface Text2PayDto { - altId: string; - altType: 'location'; - name: string; - currency: string; - items: InvoiceItemDto[]; - termsNotes?: string; - title?: string; - contactDetails: ContactDetailsDto; - invoiceNumber?: string; - issueDate: string; - dueDate?: string; - sentTo: SentToDto; - liveMode: boolean; - automaticTaxesEnabled?: boolean; - paymentSchedule?: PaymentScheduleDto; - lateFeesConfiguration?: LateFeesConfigurationDto; - tipsConfiguration?: TipsConfigurationDto; - invoiceNumberPrefix?: string; - paymentMethods?: PaymentMethodDto; - attachments?: AttachmentsDto[]; - id?: string; - includeTermsNote?: boolean; - action: 'draft' | 'send'; - userId: string; - discount?: DiscountDto; - businessDetails?: BusinessDetailsDto; -} - -export interface Text2PayInvoiceResponseDto { - invoice: DefaultInvoiceResponseDto; - invoiceUrl: string; -} - -// Invoice Management Types -export interface GenerateInvoiceNumberResponse { - invoiceNumber: number; -} - -export interface CreateInvoiceDto { - altId: string; - altType: 'location'; - name: string; - businessDetails: BusinessDetailsDto; - currency: string; - items: InvoiceItemDto[]; - discount: DiscountDto; - termsNotes?: string; - title?: string; - contactDetails: ContactDetailsDto; - invoiceNumber?: string; - issueDate: string; - dueDate?: string; - sentTo: SentToDto; - liveMode: boolean; - automaticTaxesEnabled?: boolean; - paymentSchedule?: PaymentScheduleDto; - lateFeesConfiguration?: LateFeesConfigurationDto; - tipsConfiguration?: TipsConfigurationDto; - invoiceNumberPrefix?: string; - paymentMethods?: PaymentMethodDto; - attachments?: AttachmentsDto[]; -} - -export interface UpdateInvoiceDto { - altId: string; - altType: 'location'; - name: string; - title?: string; - currency: string; - description?: string; - businessDetails?: BusinessDetailsDto; - invoiceNumber?: string; - contactId?: string; - contactDetails?: ContactDetailsDto; - termsNotes?: string; - discount?: DiscountDto; - invoiceItems: InvoiceItemDto[]; - automaticTaxesEnabled?: boolean; - liveMode?: boolean; - issueDate: string; - dueDate: string; - paymentSchedule?: PaymentScheduleDto; - tipsConfiguration?: TipsConfigurationDto; - xeroDetails?: any; - invoiceNumberPrefix?: string; - paymentMethods?: PaymentMethodDto; - attachments?: AttachmentsDto[]; -} - -export interface VoidInvoiceDto { - altId: string; - altType: 'location'; -} - -export interface InvoiceSettingsSenderConfigurationDto { - fromName?: string; - fromEmail?: string; -} - -export interface SendInvoiceDto { - altId: string; - altType: 'location'; - userId: string; - action: 'sms_and_email' | 'send_manually' | 'email' | 'sms'; - liveMode: boolean; - sentFrom?: InvoiceSettingsSenderConfigurationDto; - autoPayment?: AutoPaymentDetailsDto; -} - -export interface SendInvoicesResponseDto { - invoice: DefaultInvoiceResponseDto; - smsData: any; - emailData: any; -} - -// Record Payment Types -export interface ChequeDto { - number: string; -} - -export interface RecordPaymentDto { - altId: string; - altType: 'location'; - mode: 'cash' | 'card' | 'cheque' | 'bank_transfer' | 'other'; - card: CardDto; - cheque: ChequeDto; - notes: string; - amount?: number; - meta?: any; - paymentScheduleIds?: string[]; -} - -export interface RecordPaymentResponseDto { - success: boolean; - invoice: DefaultInvoiceResponseDto; -} - -// Invoice Stats Types -export interface PatchInvoiceStatsLastViewedDto { - invoiceId: string; -} - -// Estimate Types -export interface SendEstimateDto { - altId: string; - altType: 'location'; - action: 'sms_and_email' | 'send_manually' | 'email' | 'sms'; - liveMode: boolean; - userId: string; - sentFrom?: InvoiceSettingsSenderConfigurationDto; - estimateName?: string; -} - -export interface FrequencySettingsDto { - enabled: boolean; - schedule?: ScheduleOptionsDto; -} - -export interface AutoInvoicingDto { - enabled: boolean; - directPayments?: boolean; -} - -export interface CreateEstimatesDto { - altId: string; - altType: 'location'; - name: string; - businessDetails: BusinessDetailsDto; - currency: string; - items: InvoiceItemDto[]; - liveMode?: boolean; - discount: DiscountDto; - termsNotes?: string; - title?: string; - contactDetails: ContactDetailsDto; - estimateNumber?: number; - issueDate?: string; - expiryDate?: string; - sentTo?: SentToDto; - automaticTaxesEnabled?: boolean; - meta?: any; - sendEstimateDetails?: SendEstimateDto; - frequencySettings: FrequencySettingsDto; - estimateNumberPrefix?: string; - userId?: string; - attachments?: AttachmentsDto[]; - autoInvoice?: AutoInvoicingDto; -} - -export interface UpdateEstimateDto { - altId: string; - altType: 'location'; - name: string; - businessDetails: BusinessDetailsDto; - currency: string; - items: InvoiceItemDto[]; - liveMode?: boolean; - discount: DiscountDto; - termsNotes?: string; - title?: string; - contactDetails: ContactDetailsDto; - estimateNumber?: number; - issueDate?: string; - expiryDate?: string; - sentTo?: SentToDto; - automaticTaxesEnabled?: boolean; - meta?: any; - sendEstimateDetails?: SendEstimateDto; - frequencySettings: FrequencySettingsDto; - estimateNumberPrefix?: string; - userId?: string; - attachments?: AttachmentsDto[]; - autoInvoice?: AutoInvoicingDto; - estimateStatus?: 'all' | 'draft' | 'sent' | 'accepted' | 'declined' | 'invoiced' | 'viewed'; -} - -export interface EstimateResponseDto { - altId: string; - altType: string; - _id: string; - liveMode: boolean; - deleted: boolean; - name: string; - currency: string; - businessDetails: any; - items: any[]; - discount: DiscountDto; - title: string; - estimateNumberPrefix?: string; - attachments?: AttachmentsDto[]; - updatedBy?: string; - total: number; - createdAt: string; - updatedAt: string; - __v: number; - automaticTaxesEnabled: boolean; - termsNotes?: string; - companyId?: string; - contactDetails?: any; - issueDate?: string; - expiryDate?: string; - sentBy?: string; - automaticTaxesCalculated?: boolean; - meta?: any; - estimateActionHistory?: string[]; - sentTo?: any; - frequencySettings?: FrequencySettingsDto; - lastVisitedAt?: string; - totalamountInUSD?: number; - autoInvoice?: any; - traceId?: string; -} - -export interface GenerateEstimateNumberResponse { - estimateNumber: number; - traceId: string; -} - -export interface AltDto { - altId: string; - altType: 'location'; -} - -export interface CreateInvoiceFromEstimateDto { - altId: string; - altType: 'location'; - markAsInvoiced: boolean; - version?: 'v1' | 'v2'; -} - -export interface CreateInvoiceFromEstimateResponseDto { - estimate: EstimateResponseDto; - invoice: DefaultInvoiceResponseDto; -} - -export interface ListEstimatesResponseDto { - estimates: string[]; - total: number; - traceId: string; -} - -export interface EstimateIdParam { - estimateId: string; -} - -// Estimate Template Types -export interface EstimateTemplatesDto { - altId: string; - altType: 'location'; - name: string; - businessDetails: BusinessDetailsDto; - currency: string; - items: any[]; - liveMode?: boolean; - discount: DiscountDto; - termsNotes?: string; - title?: string; - automaticTaxesEnabled?: boolean; - meta?: any; - sendEstimateDetails?: SendEstimateDto; - estimateNumberPrefix?: string; - attachments?: AttachmentsDto[]; -} - -export interface EstimateTemplateResponseDto { - altId: string; - altType: string; - _id: string; - liveMode: boolean; - deleted: boolean; - name: string; - currency: string; - businessDetails: any; - items: any[]; - discount: DiscountDto; - title: string; - estimateNumberPrefix?: string; - attachments?: AttachmentsDto[]; - updatedBy?: string; - total: number; - createdAt: string; - updatedAt: string; - __v: number; - automaticTaxesEnabled: boolean; - termsNotes?: string; -} - -export interface ListEstimateTemplateResponseDto { - data: string[]; - totalCount: number; - traceId: string; -} - -// Invoice List Types -export interface TotalSummaryDto { - subTotal: number; - discount: number; - tax: number; -} - -export interface ReminderDto { - enabled: boolean; - emailTemplate: string; - smsTemplate: string; - emailSubject: string; - reminderId: string; - reminderName: string; - reminderTime: 'before' | 'after'; - intervalType: 'yearly' | 'monthly' | 'weekly' | 'daily' | 'hourly' | 'minutely' | 'secondly'; - maxReminders: number; - reminderInvoiceCondition: 'invoice_sent' | 'invoice_overdue'; - reminderNumber: number; - startTime?: string; - endTime?: string; - timezone?: string; -} - -export interface ReminderSettingsDto { - defaultEmailTemplateId: string; - reminders: ReminderDto[]; -} - -export interface RemindersConfigurationDto { - reminderExecutionDetailsList: any; - reminderSettings: ReminderSettingsDto; -} - -export interface GetInvoiceResponseDto { - _id: string; - status: 'draft' | 'sent' | 'payment_processing' | 'paid' | 'void' | 'partially_paid'; - liveMode: boolean; - amountPaid: number; - altId: string; - altType: string; - name: string; - businessDetails: any; - invoiceNumber: string; - currency: string; - contactDetails: any; - issueDate: string; - dueDate: string; - discount: any; - invoiceItems: any[]; - total: number; - title: string; - amountDue: number; - createdAt: string; - updatedAt: string; - automaticTaxesEnabled?: boolean; - automaticTaxesCalculated?: boolean; - paymentSchedule?: any; - totalSummary: TotalSummaryDto; - remindersConfiguration?: RemindersConfigurationDto; -} - -export interface ListInvoicesResponseDto { - invoices: GetInvoiceResponseDto[]; - total: number; -} - -// Response Types that extend base types -export interface CreateInvoiceTemplateResponseDto extends InvoiceTemplate {} -export interface UpdateInvoiceTemplateResponseDto extends InvoiceTemplate {} -export interface DeleteInvoiceTemplateResponseDto { - success: boolean; -} - -export interface CreateInvoiceScheduleResponseDto extends InvoiceSchedule {} -export interface UpdateInvoiceScheduleResponseDto extends InvoiceSchedule {} -export interface GetScheduleResponseDto extends InvoiceSchedule {} -export interface UpdateAndScheduleInvoiceScheduleResponseDto extends InvoiceSchedule {} -export interface ScheduleInvoiceScheduleResponseDto extends InvoiceSchedule {} -export interface AutoPaymentInvoiceScheduleResponseDto extends InvoiceSchedule {} -export interface CancelInvoiceScheduleResponseDto extends InvoiceSchedule {} -export interface DeleteInvoiceScheduleResponseDto { - success: boolean; -} - -export interface CreateInvoiceResponseDto extends DefaultInvoiceResponseDto {} -export interface UpdateInvoiceResponseDto extends DefaultInvoiceResponseDto {} -export interface DeleteInvoiceResponseDto extends DefaultInvoiceResponseDto {} -export interface VoidInvoiceResponseDto extends DefaultInvoiceResponseDto {} diff --git a/mcp-diagrams/ghl-mcp-apps-only/tsconfig.json b/mcp-diagrams/ghl-mcp-apps-only/tsconfig.json deleted file mode 100644 index 944355d..0000000 --- a/mcp-diagrams/ghl-mcp-apps-only/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "isolatedModules": true, - "outDir": "./dist", - "types": ["node"] - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests/**/*"] -} \ No newline at end of file diff --git a/mcp-diagrams/ghl-mcp-public b/mcp-diagrams/ghl-mcp-public deleted file mode 160000 index c8c50cb..0000000 --- a/mcp-diagrams/ghl-mcp-public +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c8c50cb56815901c8315b27f84046d528fe5f35e diff --git a/mcp-diagrams/google-ads-mcp/PLAN.md b/mcp-diagrams/google-ads-mcp/PLAN.md deleted file mode 100644 index b856da7..0000000 --- a/mcp-diagrams/google-ads-mcp/PLAN.md +++ /dev/null @@ -1,363 +0,0 @@ -# Google Ads MCP Server — Full Build Plan - -## Why Build This - -The existing Google Ads MCPs are garbage: -- **Google's official** → 2 tools total (just GAQL queries lol) -- **samihalawa's** → 8 campaign-only tools, no annotations, no apps -- **None** have tool labeling, lazy loading, safety annotations, or MCP Apps - -Google Ads API v21 has **90+ services**. There's a massive opportunity to build the definitive Google Ads MCP with proper architecture. - ---- - -## Architecture - -### Tech Stack -- **Runtime:** Node.js + TypeScript (matches our 30 existing servers) -- **MCP SDK:** `@modelcontextprotocol/sdk` (latest) -- **Google Ads API:** v21 via REST (or `google-ads-api` npm package) -- **Auth:** OAuth2 refresh token flow + optional ADC support -- **Build:** tsup → dist, publish to npm as `@busybee/google-ads-mcp` - -### Key Differentiators -1. **Tool Annotations** — every tool labeled with `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint` -2. **Lazy Loading / Progressive Discovery** — tools grouped by category, loaded on demand -3. **MCP Apps** — rich UI components (structuredContent) for dashboards, campaign views, etc. -4. **Safety Guardrails** — confirmation flows for destructive operations (pause campaigns, change budgets) -5. **GAQL Query Builder** — natural language → GAQL translation helper - ---- - -## Tool Categories & Tools (~45 tools) - -### Category 1: Account Management (5 tools) -| Tool | Description | Annotations | -|------|-------------|-------------| -| `list_accessible_customers` | List all customer accounts you can access | readOnly | -| `get_account_info` | Get account details (name, currency, timezone) | readOnly | -| `get_account_hierarchy` | Get manager → client account tree | readOnly | -| `get_billing_info` | Get billing setup and payment accounts | readOnly | -| `get_account_budget` | Get account-level budget proposals | readOnly | - -### Category 2: Campaign Management (8 tools) -| Tool | Description | Annotations | -|------|-------------|-------------| -| `list_campaigns` | List campaigns with status, type, budget, metrics | readOnly | -| `get_campaign` | Get detailed campaign info by ID | readOnly | -| `create_campaign` | Create a new campaign (Search, Display, PMax, etc.) | destructive, NOT idempotent | -| `update_campaign` | Update campaign settings (name, status, targeting) | destructive | -| `pause_campaign` | Pause a running campaign | destructive | -| `enable_campaign` | Enable a paused campaign | destructive | -| `remove_campaign` | Remove/delete a campaign | destructive, NOT idempotent | -| `update_campaign_budget` | Change daily/total budget for a campaign | destructive | - -### Category 3: Ad Group Management (6 tools) -| Tool | Description | Annotations | -|------|-------------|-------------| -| `list_ad_groups` | List ad groups for a campaign | readOnly | -| `get_ad_group` | Get ad group details | readOnly | -| `create_ad_group` | Create a new ad group | destructive | -| `update_ad_group` | Update ad group settings (bids, status, targeting) | destructive | -| `pause_ad_group` | Pause an ad group | destructive | -| `enable_ad_group` | Enable a paused ad group | destructive | - -### Category 4: Ad Management (5 tools) -| Tool | Description | Annotations | -|------|-------------|-------------| -| `list_ads` | List ads in an ad group with approval status | readOnly | -| `get_ad` | Get detailed ad info (headlines, descriptions, URLs) | readOnly | -| `create_responsive_search_ad` | Create an RSA with headlines + descriptions | destructive | -| `update_ad` | Update ad copy or URLs | destructive | -| `pause_ad` | Pause a specific ad | destructive | - -### Category 5: Keywords & Targeting (6 tools) -| Tool | Description | Annotations | -|------|-------------|-------------| -| `list_keywords` | List keywords for an ad group with match types + QS | readOnly | -| `add_keywords` | Add keywords to an ad group | destructive | -| `remove_keywords` | Remove keywords from an ad group | destructive | -| `get_keyword_ideas` | Generate keyword ideas (Keyword Planner) | readOnly, openWorld | -| `get_keyword_metrics` | Get search volume, competition, CPC estimates | readOnly, openWorld | -| `list_negative_keywords` | List negative keywords (campaign + ad group level) | readOnly | - -### Category 6: Performance & Reporting (7 tools) -| Tool | Description | Annotations | -|------|-------------|-------------| -| `get_performance_summary` | Account-level performance (clicks, impressions, cost, conversions) | readOnly | -| `get_campaign_performance` | Campaign-level metrics with date range | readOnly | -| `get_ad_group_performance` | Ad group-level metrics | readOnly | -| `get_keyword_performance` | Keyword-level metrics with quality scores | readOnly | -| `get_search_terms_report` | Actual search terms triggering ads | readOnly | -| `get_geo_performance` | Performance by geographic location | readOnly | -| `get_device_performance` | Performance by device type | readOnly | - -### Category 7: Bidding & Budget (4 tools) -| Tool | Description | Annotations | -|------|-------------|-------------| -| `list_bidding_strategies` | List portfolio bidding strategies | readOnly | -| `get_bid_recommendations` | Get Google's bid recommendations | readOnly | -| `update_keyword_bids` | Change CPC bids for keywords | destructive | -| `list_recommendations` | Get Google's optimization recommendations | readOnly | - -### Category 8: Conversion Tracking (4 tools) -| Tool | Description | Annotations | -|------|-------------|-------------| -| `list_conversion_actions` | List configured conversion actions | readOnly | -| `get_conversion_metrics` | Get conversion data by campaign/ad group | readOnly | -| `create_conversion_action` | Create a new conversion action | destructive | -| `upload_offline_conversions` | Upload offline conversion data | destructive | - -### Category 9: Advanced / Utilities (4 tools) -| Tool | Description | Annotations | -|------|-------------|-------------| -| `run_gaql_query` | Execute raw GAQL query (power users) | readOnly, openWorld | -| `export_report` | Export any report to CSV/JSON | readOnly | -| `get_change_history` | Get recent changes to the account | readOnly | -| `get_labels` | List and manage labels | readOnly | - ---- - -## Lazy Loading Architecture - -``` -Initial load → Core tools only (8 tools): - - list_accessible_customers - - list_campaigns - - get_performance_summary - - get_campaign_performance - - run_gaql_query - - export_report - - get_keyword_ideas - - list_recommendations - -On-demand categories (loaded when referenced): - - "campaign_management" → 8 campaign CRUD tools - - "ad_groups" → 6 ad group tools - - "ads" → 5 ad tools - - "keywords" → 6 keyword tools - - "bidding" → 4 bidding tools - - "conversions" → 4 conversion tools - - "advanced" → 4 utility tools -``` - -Implementation: Use MCP `tools/list` with pagination hints, or a `load_category` meta-tool that dynamically registers tools. - ---- - -## MCP Apps (structuredContent UI Components) - -### App 1: Campaign Dashboard -- Grid view of all campaigns with status pills, spend, CTR, conversions -- Sparkline charts for 7-day trends -- Quick actions: pause/enable buttons - -### App 2: Performance Overview -- Account-level KPI cards (total spend, clicks, impressions, avg CPC, conversions) -- Date range selector -- Comparison to previous period (green/red deltas) - -### App 3: Campaign Detail Card -- Full campaign settings display -- Budget utilization bar -- Metric breakdown table -- Ad group list within campaign - -### App 4: Keyword Analyzer -- Keyword list with quality score visualization (color-coded bars) -- Match type indicators -- Search term cloud from actual queries -- Bid vs. position scatter - -### App 5: Search Terms Report -- Table of triggering search terms -- Relevance scoring -- Quick "add as keyword" or "add as negative" actions - -### App 6: Recommendations Panel -- Google's recommendations with estimated impact -- Accept/dismiss actions -- Category filters (bids, keywords, ads, etc.) - -### App 7: Budget Optimizer -- Budget allocation visualization across campaigns -- Projected spend vs. actual -- Reallocation suggestions based on performance - ---- - -## Tool Annotations Schema - -Every tool gets proper MCP annotations: - -```typescript -{ - name: "pause_campaign", - description: "Pause a running campaign. This will stop all ads from serving.", - annotations: { - readOnlyHint: false, - destructiveHint: true, - idempotentHint: true, // pausing twice = same result - openWorldHint: false, - title: "Pause Campaign", - category: "campaign_management" - }, - inputSchema: { ... } -} -``` - -```typescript -{ - name: "get_keyword_ideas", - description: "Generate keyword ideas based on seed keywords or URL.", - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: true, // queries external data - title: "Keyword Ideas", - category: "keywords" - }, - inputSchema: { ... } -} -``` - ---- - -## Safety Guardrails - -For any tool with `destructiveHint: true`: - -1. **Budget changes** → require explicit confirmation, show before/after -2. **Campaign pause/remove** → show what's at stake (current daily spend, impressions, active ads) -3. **Keyword removal** → show keyword performance before removing -4. **Bulk operations** → require `confirm: true` parameter + show count of affected items -5. **Budget floor** → warn if setting budget below Google's recommended minimum - ---- - -## Auth Flow - -``` -Option 1: OAuth2 Refresh Token (recommended for MCP) - - User provides: client_id, client_secret, refresh_token, developer_token - - Server handles token refresh automatically - - Stored in env vars or config file - -Option 2: Application Default Credentials (GCP) - - For users already in GCP ecosystem - - gcloud auth application-default login with Google Ads scope - -Option 3: Service Account (enterprise) - - For automated/server setups - - Domain-wide delegation -``` - ---- - -## File Structure - -``` -google-ads-mcp/ -├── package.json -├── tsconfig.json -├── tsup.config.ts -├── README.md -├── src/ -│ ├── index.ts # Main server entry -│ ├── client.ts # Google Ads API client wrapper -│ ├── auth.ts # OAuth2/ADC auth handling -│ ├── tools/ -│ │ ├── index.ts # Tool registry + lazy loading -│ │ ├── accounts.ts # Account management tools -│ │ ├── campaigns.ts # Campaign CRUD tools -│ │ ├── ad-groups.ts # Ad group tools -│ │ ├── ads.ts # Ad management tools -│ │ ├── keywords.ts # Keyword & targeting tools -│ │ ├── reporting.ts # Performance & reporting tools -│ │ ├── bidding.ts # Bidding & budget tools -│ │ ├── conversions.ts # Conversion tracking tools -│ │ └── advanced.ts # GAQL, export, utilities -│ ├── apps/ -│ │ ├── campaign-dashboard.ts -│ │ ├── performance-overview.ts -│ │ ├── campaign-detail.ts -│ │ ├── keyword-analyzer.ts -│ │ ├── search-terms.ts -│ │ ├── recommendations.ts -│ │ └── budget-optimizer.ts -│ └── utils/ -│ ├── gaql.ts # GAQL query builder -│ ├── formatters.ts # Data formatting helpers -│ └── validators.ts # Input validation -├── dist/ # Compiled output -└── tests/ - ├── tools.test.ts - └── apps.test.ts -``` - ---- - -## Build Phases - -### Phase 1: Core (MVP) — ~2 days -- Auth flow (OAuth2 refresh token) -- API client with proper error handling -- Account + Campaign tools (13 tools) -- Performance reporting tools (7 tools) -- Tool annotations on all tools -- Basic lazy loading architecture - -### Phase 2: Full Tool Suite — ~1 day -- Ad group, ad, keyword tools (17 more tools) -- Bidding + conversion tools (8 more tools) -- GAQL query builder -- Export functionality - -### Phase 3: MCP Apps — ~2 days -- Campaign Dashboard app -- Performance Overview app -- Campaign Detail Card -- Keyword Analyzer -- Recommendations Panel - -### Phase 4: Polish — ~1 day -- Search Terms Report app -- Budget Optimizer app -- Full test coverage -- README + docs -- npm publish - -**Total estimate: ~6 days with paired agents, ~3 days if we parallelize hard** - ---- - -## Competitive Edge vs. Existing - -| Feature | Google Official | samihalawa | **Ours** | -|---------|----------------|------------|----------| -| Tools | 2 | 8 | **~45** | -| Tool Annotations | ❌ | ❌ | **✅** | -| Lazy Loading | ❌ | ❌ | **✅** | -| MCP Apps (UI) | ❌ | ❌ | **7 apps** | -| Safety Guardrails | ❌ | ❌ | **✅** | -| Keyword Planner | ❌ | ❌ | **✅** | -| GAQL Builder | ❌ | ❌ | **✅** | -| Conversion Tracking | ❌ | ❌ | **✅** | -| Ad Management | ❌ | ❌ | **✅** | -| Language | Python | Node.js | **TypeScript** | - ---- - -## Revenue Potential - -Based on our MCP pricing research: -- **Free tier:** Read-only tools (reporting, performance, recommendations) -- **Pro ($19-29/mo):** Full CRUD, keyword planner, all apps -- **Enterprise ($99-199/mo):** Multi-account, bulk ops, custom GAQL, offline conversions - -Google Ads has **~10M+ active advertisers**. Even capturing 0.01% = 1,000 users at $29/mo = $29K MRR. - ---- - -*Ready to build on your call, Jack.* diff --git a/mcp-diagrams/google-ads-mcp/README.md b/mcp-diagrams/google-ads-mcp/README.md deleted file mode 100644 index 1bea8f5..0000000 --- a/mcp-diagrams/google-ads-mcp/README.md +++ /dev/null @@ -1,142 +0,0 @@ -# Google Ads MCP Server - -The definitive Google Ads MCP Server — 49 tools, 7 MCP Apps, tool annotations, lazy loading, safety guardrails. - -## What Makes This Different - -| Feature | Google Official | Others | **This** | -|---------|----------------|--------|----------| -| Tools | 2 | 8 | **49** | -| Tool Annotations | ❌ | ❌ | ✅ | -| Lazy Loading | ❌ | ❌ | ✅ | -| MCP Apps (Rich UI) | ❌ | ❌ | **7 apps** | -| Safety Guardrails | ❌ | ❌ | ✅ | -| Keyword Planner | ❌ | ❌ | ✅ | -| GAQL Query Builder | ❌ | ❌ | ✅ | -| Conversion Tracking | ❌ | ❌ | ✅ | -| Ad Management | ❌ | ❌ | ✅ | - -## Quick Start - -### 1. Install - -```bash -npx @busybee/google-ads-mcp -``` - -### 2. Configure - -Set credentials via environment variables: - -```bash -# Option A: Inline JSON (simple) -export GOOGLE_ADS_CONFIG='{"client_id":"YOUR_ID","client_secret":"YOUR_SECRET","developer_token":"YOUR_TOKEN","refresh_token":"YOUR_REFRESH","login_customer_id":"YOUR_MANAGER_ID"}' -export GOOGLE_ADS_CUSTOMER_ID="1234567890" - -# Option B: Individual env vars -export GOOGLE_ADS_CLIENT_ID="YOUR_ID" -export GOOGLE_ADS_CLIENT_SECRET="YOUR_SECRET" -export GOOGLE_ADS_DEVELOPER_TOKEN="YOUR_TOKEN" -export GOOGLE_ADS_REFRESH_TOKEN="YOUR_REFRESH" -export GOOGLE_ADS_LOGIN_CUSTOMER_ID="YOUR_MANAGER_ID" # optional -export GOOGLE_ADS_CUSTOMER_ID="1234567890" # optional default -``` - -### 3. Add to MCP Client - -**Claude Desktop / Cursor / Gemini CLI:** - -```json -{ - "mcpServers": { - "google-ads": { - "command": "npx", - "args": ["@busybee/google-ads-mcp"], - "env": { - "GOOGLE_ADS_CONFIG": "{...}", - "GOOGLE_ADS_CUSTOMER_ID": "1234567890" - } - } - } -} -``` - -## Tool Categories - -### Always Loaded (24 core tools) -- **Account Management** (5) — list accounts, info, hierarchy, billing, budgets -- **Campaign Management** (8) — list, get, create, update, pause, enable, remove, budget -- **Reporting** (7) — performance summary, campaign/ad group/keyword/geo/device metrics, search terms -- **Advanced** (4) — raw GAQL queries, export CSV/JSON, change history, labels - -### Lazy Loaded (25 tools, load on demand) -- **Ad Groups** (6) — list, get, create, update, pause, enable -- **Ads** (5) — list, get, create RSA, update, pause -- **Keywords** (6) — list, add, remove, keyword ideas, metrics, negatives -- **Bidding** (4) — strategies, bid recommendations, update bids, optimization recs -- **Conversions** (4) — list actions, conversion metrics, create actions, upload offline - -Use `load_tools_category` to load additional categories on demand. - -## MCP Apps (Rich UI) - -7 interactive HTML dashboard components rendered via `structuredContent`: - -1. **Campaign Dashboard** — Card grid with status pills, metrics, budget bars -2. **Performance Overview** — KPI cards, 7-day spend chart, top campaigns -3. **Campaign Detail** — Deep-dive with settings, budget, ad groups table -4. **Keyword Analyzer** — Quality score visualization, match type badges -5. **Search Terms Report** — Triggering terms with suggested actions -6. **Recommendations** — Google's optimization recs with impact scoring -7. **Budget Optimizer** — Allocation bars, over/underspend indicators - -## Tool Annotations - -Every tool is annotated with MCP hints: -- `readOnlyHint` — safe to call, no side effects -- `destructiveHint` — modifies account data -- `idempotentHint` — safe to retry -- `openWorldHint` — queries external data sources - -## Safety Guardrails - -- Campaigns created in **PAUSED** state by default -- Budget updates show **before → after** comparison -- Destructive operations clearly marked with ⚠️ -- GAQL query runner blocks mutation keywords -- Remove operations warn about irreversibility - -## Architecture - -``` -src/ -├── index.ts # MCP server entry point -├── auth.ts # OAuth2 token management -├── client.ts # Google Ads API v21 REST client -├── types.ts # Shared types + formatting -├── tools/ -│ ├── index.ts # Tool registry + lazy loading -│ ├── accounts.ts # 5 account tools -│ ├── campaigns.ts # 8 campaign tools -│ ├── ad-groups.ts # 6 ad group tools -│ ├── ads.ts # 5 ad tools -│ ├── keywords.ts # 6 keyword tools -│ ├── reporting.ts # 7 reporting tools -│ ├── bidding.ts # 4 bidding tools -│ ├── conversions.ts # 4 conversion tools -│ └── advanced.ts # 4 utility tools -└── apps/ - ├── index.ts # App registry - ├── theme.ts # Shared dark theme CSS - ├── campaign-dashboard.ts - ├── performance-overview.ts - ├── campaign-detail.ts - ├── keyword-analyzer.ts - ├── search-terms.ts - ├── recommendations.ts - └── budget-optimizer.ts -``` - -## License - -MIT diff --git a/mcp-diagrams/google-ads-mcp/package.json b/mcp-diagrams/google-ads-mcp/package.json deleted file mode 100644 index e7019f1..0000000 --- a/mcp-diagrams/google-ads-mcp/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@busybee/google-ads-mcp", - "version": "1.0.0", - "description": "The definitive Google Ads MCP Server — 45+ tools, 7 MCP Apps, tool annotations, lazy loading", - "type": "module", - "main": "dist/index.js", - "bin": { - "google-ads-mcp": "dist/index.js" - }, - "scripts": { - "build": "tsup", - "dev": "tsx src/index.ts", - "start": "node dist/index.js" - }, - "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.1", - "google-ads-api": "^17.0.0", - "zod": "^3.23.0" - }, - "devDependencies": { - "tsup": "^8.0.0", - "tsx": "^4.7.0", - "typescript": "^5.4.0", - "@types/node": "^20.0.0" - } -} diff --git a/mcp-diagrams/google-ads-mcp/src/apps/budget-optimizer.ts b/mcp-diagrams/google-ads-mcp/src/apps/budget-optimizer.ts deleted file mode 100644 index 21e3816..0000000 --- a/mcp-diagrams/google-ads-mcp/src/apps/budget-optimizer.ts +++ /dev/null @@ -1,256 +0,0 @@ -// ============================================ -// APP: Budget Optimizer — Budget allocation view -// ============================================ - -import { wrapHtml, escapeHtml, fmtMoney, fmtNum, fmtPct } from "./theme.js"; - -export interface BudgetOptimizerData { - accountName?: string; - period?: string; - totalBudget: number; // micros - totalSpend: number; // micros - campaigns: BudgetCampaignRow[]; - suggestions?: BudgetSuggestion[]; -} - -export interface BudgetCampaignRow { - name: string; - dailyBudget: number; // micros - actualSpend: number; // micros - conversions: number; - costPerConversion: number; // micros - roas?: number; -} - -export interface BudgetSuggestion { - campaignName: string; - action: string; // e.g. "Increase budget by 20%", "Reallocate $50/day to..." - reason: string; - estimatedImpact?: string; -} - -type SpendStatus = "underspent" | "on-track" | "overspent"; - -function getSpendStatus(budget: number, spend: number): SpendStatus { - if (budget <= 0) return "on-track"; - const ratio = spend / budget; - if (ratio < 0.7) return "underspent"; - if (ratio > 1.0) return "overspent"; - return "on-track"; -} - -function statusColor(status: SpendStatus): string { - switch (status) { - case "underspent": return "blue"; - case "on-track": return "green"; - case "overspent": return "red"; - } -} - -function statusLabel(status: SpendStatus): string { - switch (status) { - case "underspent": return "Underspent"; - case "on-track": return "On Track"; - case "overspent": return "Overspent"; - } -} - -function statusPillClass(status: SpendStatus): string { - switch (status) { - case "underspent": return "pill-blue"; - case "on-track": return "pill-green"; - case "overspent": return "pill-red"; - } -} - -export function renderBudgetOptimizer(data: BudgetOptimizerData): string { - const { campaigns, totalBudget, totalSpend } = data; - const utilizationPct = totalBudget > 0 ? (totalSpend / totalBudget) * 100 : 0; - const utilizationColor = utilizationPct > 100 ? "red" : utilizationPct > 85 ? "yellow" : "green"; - - const underspent = campaigns.filter((c) => getSpendStatus(c.dailyBudget, c.actualSpend) === "underspent").length; - const onTrack = campaigns.filter((c) => getSpendStatus(c.dailyBudget, c.actualSpend) === "on-track").length; - const overspent = campaigns.filter((c) => getSpendStatus(c.dailyBudget, c.actualSpend) === "overspent").length; - - // Stacked allocation bar - const maxBudget = Math.max(...campaigns.map((c) => c.dailyBudget), 1); - const allocationBars = campaigns.map((c) => { - const budgetPct = (c.dailyBudget / totalBudget) * 100; - const spendPct = c.dailyBudget > 0 ? (c.actualSpend / c.dailyBudget) * 100 : 0; - const status = getSpendStatus(c.dailyBudget, c.actualSpend); - const color = statusColor(status); - - return ` -
    -
    - ${escapeHtml(c.name)} - ${statusLabel(status)} -
    -
    -
    -
    -
    -
    -
    - ${fmtMoney(c.actualSpend)} - / ${fmtMoney(c.dailyBudget)} -
    -
    -
    `; - }).join("\n"); - - // Campaign detail table - const tableRows = campaigns.map((c) => { - const status = getSpendStatus(c.dailyBudget, c.actualSpend); - return ` - - ${escapeHtml(c.name)} - ${statusLabel(status)} - ${fmtMoney(c.dailyBudget)} - ${fmtMoney(c.actualSpend)} - ${fmtNum(c.conversions)} - ${fmtMoney(c.costPerConversion)} - ${c.roas !== undefined ? c.roas.toFixed(2) + "x" : "—"} - `; - }).join("\n"); - - // Suggestions - const suggestionsHtml = data.suggestions?.length ? data.suggestions.map((s) => ` -
    -
    📂 ${escapeHtml(s.campaignName)}
    -
    ${escapeHtml(s.action)}
    -
    ${escapeHtml(s.reason)}
    - ${s.estimatedImpact ? `
    ⬆ ${escapeHtml(s.estimatedImpact)}
    ` : ""} -
    ` - ).join("\n") : `
    No reallocation suggestions at this time
    `; - - const body = ` - - -
    -

    💰 Budget Optimizer

    -
    ${data.accountName ? escapeHtml(data.accountName) + " · " : ""}${data.period ? escapeHtml(data.period) : "Current Period"}
    -
    - -
    -
    -
    ${fmtMoney(totalBudget)}
    -
    Total Budget
    -
    -
    -
    ${fmtMoney(totalSpend)}
    -
    Total Spend
    -
    -
    -
    ${utilizationPct.toFixed(1)}%
    -
    Utilization
    -
    -
    -
    - ${underspent} - ${onTrack} - ${overspent} -
    -
    Under / On / Over
    -
    -
    - -
    -
    -
    Budget Allocation
    -
    - ${allocationBars} -
    -
    - Underspent - On Track - Overspent -
    -
    -
    - -
    -
    -
    -
    Campaign Breakdown
    -
    -
    - - - - - - - - - - - - - - ${tableRows} - -
    CampaignStatusBudgetSpendConv.Cost/ConvROAS
    -
    -
    -
    - -
    -
    🔄 Reallocation Suggestions
    -
    - ${suggestionsHtml} -
    -
    - `; - - return wrapHtml("Budget Optimizer", body); -} diff --git a/mcp-diagrams/google-ads-mcp/src/apps/campaign-dashboard.ts b/mcp-diagrams/google-ads-mcp/src/apps/campaign-dashboard.ts deleted file mode 100644 index eaed137..0000000 --- a/mcp-diagrams/google-ads-mcp/src/apps/campaign-dashboard.ts +++ /dev/null @@ -1,136 +0,0 @@ -// ============================================ -// APP: Campaign Dashboard — Grid of all campaigns -// ============================================ - -import { wrapHtml, escapeHtml, fmtMoney, fmtNum, fmtPct, statusPill, progressBar } from "./theme.js"; - -export interface CampaignDashboardData { - accountName?: string; - campaigns: CampaignCard[]; - sortBy?: "spend" | "conversions" | "ctr"; -} - -export interface CampaignCard { - id: string; - name: string; - status: string; - channelType: string; - spend: number; // micros - clicks: number; - impressions: number; - ctr: number; // decimal - conversions: number; - dailyBudget: number; // micros -} - -function channelBadge(channel: string): string { - const ch = channel.toUpperCase().replace(/_/g, " "); - const map: Record = { - SEARCH: "pill-blue", - DISPLAY: "pill-purple", - SHOPPING: "pill-orange", - VIDEO: "pill-red", - "PERFORMANCE MAX": "pill-green", - DISCOVERY: "pill-yellow", - LOCAL: "pill-muted", - SMART: "pill-muted", - }; - const cls = map[ch] || "pill-muted"; - return `${escapeHtml(ch)}`; -} - -function sortCampaigns(campaigns: CampaignCard[], sortBy: string): CampaignCard[] { - const copy = [...campaigns]; - switch (sortBy) { - case "conversions": return copy.sort((a, b) => b.conversions - a.conversions); - case "ctr": return copy.sort((a, b) => b.ctr - a.ctr); - case "spend": - default: return copy.sort((a, b) => b.spend - a.spend); - } -} - -export function renderCampaignDashboard(data: CampaignDashboardData): string { - const sortBy = data.sortBy || "spend"; - const sorted = sortCampaigns(data.campaigns, sortBy); - const totalSpend = data.campaigns.reduce((s, c) => s + c.spend, 0); - const totalConv = data.campaigns.reduce((s, c) => s + c.conversions, 0); - - const sortPills = (["spend", "conversions", "ctr"] as const).map( - (s) => `${s.toUpperCase()}` - ).join(" "); - - const cards = sorted.map((c) => { - const budgetPct = c.dailyBudget > 0 ? (c.spend / c.dailyBudget) * 100 : 0; - const budgetColor = budgetPct > 95 ? "red" : budgetPct > 70 ? "yellow" : "green"; - - return ` -
    -
    - ${escapeHtml(c.name)} - ${statusPill(c.status)} -
    -
    - ${channelBadge(c.channelType)} -
    -
    -
    -
    Spend
    -
    ${fmtMoney(c.spend)}
    -
    -
    -
    Conversions
    -
    ${fmtNum(c.conversions)}
    -
    -
    -
    Clicks
    -
    ${fmtNum(c.clicks)}
    -
    -
    -
    CTR
    -
    ${fmtPct(c.ctr)}
    -
    -
    -
    Impressions
    -
    ${fmtNum(c.impressions)}
    -
    -
    -
    -
    - Budget Utilization - ${budgetPct.toFixed(0)}% -
    - ${progressBar(budgetPct, budgetColor)} -
    ${fmtMoney(c.spend)} / ${fmtMoney(c.dailyBudget)} daily
    -
    -
    `; - }).join("\n"); - - const body = ` - - -
    -

    📊 Campaign Dashboard

    -
    ${data.accountName ? escapeHtml(data.accountName) + " — " : ""}${sorted.length} campaigns · ${fmtMoney(totalSpend)} total spend · ${fmtNum(totalConv)} conversions
    -
    - -
    - Sort by: ${sortPills} -
    - -
    - ${cards} -
    - `; - - return wrapHtml("Campaign Dashboard", body); -} diff --git a/mcp-diagrams/google-ads-mcp/src/apps/campaign-detail.ts b/mcp-diagrams/google-ads-mcp/src/apps/campaign-detail.ts deleted file mode 100644 index 24e418e..0000000 --- a/mcp-diagrams/google-ads-mcp/src/apps/campaign-detail.ts +++ /dev/null @@ -1,163 +0,0 @@ -// ============================================ -// APP: Campaign Detail — Single Campaign Deep-Dive -// ============================================ - -import { wrapHtml, escapeHtml, fmtMoney, fmtNum, fmtPct, statusPill, progressBar } from "./theme.js"; - -export interface CampaignDetailData { - campaign: { - id: string; - name: string; - status: string; - channelType: string; - biddingStrategy: string; - startDate?: string; - endDate?: string; - }; - budget: { - dailyBudget: number; // micros - totalSpent: number; // micros - deliveryMethod?: string; - }; - metrics: { - impressions: number; - clicks: number; - ctr: number; // decimal - cost: number; // micros - conversions: number; - conversionRate: number; // decimal - costPerConversion: number; // micros - avgCpc: number; // micros - }; - adGroups: AdGroupRow[]; -} - -export interface AdGroupRow { - id: string; - name: string; - status: string; - cpcBid: number; // micros - impressions: number; - clicks: number; - ctr: number; - cost: number; // micros - conversions: number; -} - -function metricRow(label: string, value: string): string { - return `
    ${escapeHtml(label)}${value}
    `; -} - -export function renderCampaignDetail(data: CampaignDetailData): string { - const { campaign: c, budget: b, metrics: m } = data; - - const utilizationPct = b.dailyBudget > 0 ? (b.totalSpent / b.dailyBudget) * 100 : 0; - const budgetColor = utilizationPct > 95 ? "red" : utilizationPct > 70 ? "yellow" : "green"; - - const strategyLabel = c.biddingStrategy.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()); - - // Ad groups table - const adGroupRows = data.adGroups.map((ag) => ` - - ${escapeHtml(ag.name)} - ${statusPill(ag.status)} - ${fmtMoney(ag.cpcBid)} - ${fmtNum(ag.impressions)} - ${fmtNum(ag.clicks)} - ${fmtPct(ag.ctr)} - ${fmtMoney(ag.cost)} - ${fmtNum(ag.conversions)} - ` - ).join("\n"); - - const body = ` - - -
    -
    -

    🔍 ${escapeHtml(c.name)}

    - ${statusPill(c.status)} -
    -
    - ${escapeHtml(c.channelType.replace(/_/g, " "))} - ${escapeHtml(strategyLabel)} - ${c.startDate ? `Started ${escapeHtml(c.startDate)}` : ""} - ${c.endDate ? `Ends ${escapeHtml(c.endDate)}` : ""} -
    -
    - -
    - -
    -
    💰 Budget
    - ${metricRow("Daily Budget", fmtMoney(b.dailyBudget))} - ${metricRow("Total Spent", fmtMoney(b.totalSpent))} - ${b.deliveryMethod ? metricRow("Delivery", b.deliveryMethod) : ""} -
    -
    - Utilization - ${utilizationPct.toFixed(1)}% -
    - ${progressBar(utilizationPct, budgetColor)} -
    -
    - - -
    -
    📊 Performance
    - ${metricRow("Impressions", fmtNum(m.impressions))} - ${metricRow("Clicks", fmtNum(m.clicks))} - ${metricRow("CTR", fmtPct(m.ctr))} - ${metricRow("Cost", fmtMoney(m.cost))} - ${metricRow("Avg CPC", fmtMoney(m.avgCpc))} - ${metricRow("Conversions", fmtNum(m.conversions))} - ${metricRow("Conv. Rate", fmtPct(m.conversionRate))} - ${metricRow("Cost / Conv.", fmtMoney(m.costPerConversion))} -
    -
    - -
    -
    -
    -
    📂 Ad Groups (${data.adGroups.length})
    -
    -
    - - - - - - - - - - - - - - - ${adGroupRows || ``} - -
    Ad GroupStatusCPC BidImpr.ClicksCTRCostConv.
    No ad groups found
    -
    -
    -
    - `; - - return wrapHtml(`Campaign: ${c.name}`, body); -} diff --git a/mcp-diagrams/google-ads-mcp/src/apps/index.ts b/mcp-diagrams/google-ads-mcp/src/apps/index.ts deleted file mode 100644 index ae8df5d..0000000 --- a/mcp-diagrams/google-ads-mcp/src/apps/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -// ============================================ -// APP REGISTRY — Re-export all MCP App renderers -// ============================================ - -export { renderCampaignDashboard } from "./campaign-dashboard.js"; -export type { CampaignDashboardData, CampaignCard } from "./campaign-dashboard.js"; - -export { renderPerformanceOverview } from "./performance-overview.js"; -export type { PerformanceOverviewData, DailySpendPoint, TopCampaign } from "./performance-overview.js"; - -export { renderCampaignDetail } from "./campaign-detail.js"; -export type { CampaignDetailData, AdGroupRow } from "./campaign-detail.js"; - -export { renderKeywordAnalyzer } from "./keyword-analyzer.js"; -export type { KeywordAnalyzerData, KeywordRow } from "./keyword-analyzer.js"; - -export { renderSearchTerms } from "./search-terms.js"; -export type { SearchTermsData, SearchTermRow } from "./search-terms.js"; - -export { renderRecommendations } from "./recommendations.js"; -export type { RecommendationsData, RecommendationItem } from "./recommendations.js"; - -export { renderBudgetOptimizer } from "./budget-optimizer.js"; -export type { BudgetOptimizerData, BudgetCampaignRow, BudgetSuggestion } from "./budget-optimizer.js"; - -// Theme utilities (for custom apps) -export { wrapHtml, escapeHtml, fmtMoney, fmtNum, fmtPct, statusPill, progressBar, THEME_CSS } from "./theme.js"; diff --git a/mcp-diagrams/google-ads-mcp/src/apps/keyword-analyzer.ts b/mcp-diagrams/google-ads-mcp/src/apps/keyword-analyzer.ts deleted file mode 100644 index c1a6ac4..0000000 --- a/mcp-diagrams/google-ads-mcp/src/apps/keyword-analyzer.ts +++ /dev/null @@ -1,142 +0,0 @@ -// ============================================ -// APP: Keyword Analyzer — Keyword analysis view -// ============================================ - -import { wrapHtml, escapeHtml, fmtMoney, fmtNum, fmtPct } from "./theme.js"; - -export interface KeywordAnalyzerData { - campaignName?: string; - adGroupName?: string; - keywords: KeywordRow[]; -} - -export interface KeywordRow { - keyword: string; - matchType: string; // BROAD, PHRASE, EXACT - status: string; - qualityScore: number; // 1-10 - clicks: number; - impressions: number; - ctr: number; // decimal - cpc: number; // micros - conversions: number; -} - -function matchTypeBadge(matchType: string): string { - const mt = matchType.toUpperCase(); - const map: Record = { - BROAD: "pill-blue", - PHRASE: "pill-purple", - EXACT: "pill-green", - }; - return `${escapeHtml(mt)}`; -} - -function qualityScoreBar(qs: number): string { - const clamped = Math.max(0, Math.min(10, qs)); - const pct = clamped * 10; - let color: string; - let textColor: string; - if (clamped >= 7) { - color = "var(--green)"; - textColor = "text-green"; - } else if (clamped >= 4) { - color = "var(--yellow)"; - textColor = "text-yellow"; - } else { - color = "var(--red)"; - textColor = "text-red"; - } - - return ` -
    -
    -
    -
    - ${clamped} -
    `; -} - -function statusPillKw(status: string): string { - const s = status.toUpperCase(); - if (s === "ENABLED") return `${s}`; - if (s === "PAUSED") return `${s}`; - if (s === "REMOVED") return `${s}`; - return `${s}`; -} - -export function renderKeywordAnalyzer(data: KeywordAnalyzerData): string { - const kws = data.keywords; - const totalKws = kws.length; - const avgQS = totalKws > 0 - ? (kws.reduce((s, k) => s + k.qualityScore, 0) / totalKws) - : 0; - const topPerformer = [...kws].sort((a, b) => b.conversions - a.conversions)[0]; - - const avgQSColor = avgQS >= 7 ? "text-green" : avgQS >= 4 ? "text-yellow" : "text-red"; - - // Build table rows - const rows = kws.map((k) => ` - - ${escapeHtml(k.keyword)} - ${matchTypeBadge(k.matchType)} - ${statusPillKw(k.status)} - ${qualityScoreBar(k.qualityScore)} - ${fmtNum(k.clicks)} - ${fmtNum(k.impressions)} - ${fmtPct(k.ctr)} - ${fmtMoney(k.cpc)} - ${fmtNum(k.conversions)} - ` - ).join("\n"); - - const body = ` -
    -

    🔑 Keyword Analyzer

    -
    ${data.campaignName ? escapeHtml(data.campaignName) : "All campaigns"}${data.adGroupName ? " → " + escapeHtml(data.adGroupName) : ""}
    -
    - -
    -
    -
    ${fmtNum(totalKws)}
    -
    Total Keywords
    -
    -
    -
    ${avgQS.toFixed(1)}
    -
    Avg Quality Score
    -
    -
    -
    ${topPerformer ? escapeHtml(topPerformer.keyword) : "—"}
    -
    Top Performer
    - ${topPerformer ? `
    ${fmtNum(topPerformer.conversions)} conv
    ` : ""} -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - ${rows || ``} - -
    KeywordMatch TypeStatusQuality ScoreClicksImpr.CTRCPCConv.
    No keywords found
    -
    -
    -
    - `; - - return wrapHtml("Keyword Analyzer", body); -} diff --git a/mcp-diagrams/google-ads-mcp/src/apps/performance-overview.ts b/mcp-diagrams/google-ads-mcp/src/apps/performance-overview.ts deleted file mode 100644 index 1863cb0..0000000 --- a/mcp-diagrams/google-ads-mcp/src/apps/performance-overview.ts +++ /dev/null @@ -1,146 +0,0 @@ -// ============================================ -// APP: Performance Overview — Account-level KPI Dashboard -// ============================================ - -import { wrapHtml, escapeHtml, fmtMoney, fmtNum, fmtPct } from "./theme.js"; - -export interface PerformanceOverviewData { - accountName?: string; - period: string; - kpis: { - totalSpend: number; // micros - clicks: number; - impressions: number; - ctr: number; // decimal - avgCpc: number; // micros - conversions: number; - }; - previousKpis?: { - totalSpend: number; - clicks: number; - impressions: number; - ctr: number; - avgCpc: number; - conversions: number; - }; - dailySpend: DailySpendPoint[]; - topCampaigns: TopCampaign[]; -} - -export interface DailySpendPoint { - date: string; // e.g. "Mon", "Tue" or "2024-01-15" - spend: number; // micros -} - -export interface TopCampaign { - name: string; - spend: number; // micros - conversions: number; - ctr: number; - roas?: number; -} - -function kpiCard(label: string, value: string, current: number, previous?: number): string { - let deltaHtml = ""; - if (previous !== undefined && previous !== 0) { - const pctChange = ((current - previous) / Math.abs(previous)) * 100; - const arrow = pctChange >= 0 ? "↑" : "↓"; - const cls = pctChange >= 0 ? "up" : "down"; - deltaHtml = `
    ${arrow} ${Math.abs(pctChange).toFixed(1)}%
    `; - } else { - deltaHtml = `
    `; - } - - return ` -
    -
    ${value}
    -
    ${escapeHtml(label)}
    - ${deltaHtml} -
    `; -} - -export function renderPerformanceOverview(data: PerformanceOverviewData): string { - const k = data.kpis; - const p = data.previousKpis; - - const kpis = [ - kpiCard("Total Spend", fmtMoney(k.totalSpend), k.totalSpend, p?.totalSpend), - kpiCard("Clicks", fmtNum(k.clicks), k.clicks, p?.clicks), - kpiCard("Impressions", fmtNum(k.impressions), k.impressions, p?.impressions), - kpiCard("CTR", fmtPct(k.ctr), k.ctr, p?.ctr), - kpiCard("Avg CPC", fmtMoney(k.avgCpc), k.avgCpc, p?.avgCpc), - kpiCard("Conversions", fmtNum(k.conversions), k.conversions, p?.conversions), - ].join("\n"); - - // Daily spend bar chart (CSS-only) - const maxSpend = Math.max(...data.dailySpend.map((d) => d.spend), 1); - const bars = data.dailySpend.map((d) => { - const heightPct = (d.spend / maxSpend) * 100; - const shortDate = d.date.length > 5 ? d.date.slice(-5) : d.date; - return ` -
    -
    ${fmtMoney(d.spend)}
    -
    -
    ${escapeHtml(shortDate)}
    -
    `; - }).join("\n"); - - // Top campaigns table - const rows = data.topCampaigns.map((c, i) => ` - - ${i + 1} - ${escapeHtml(c.name)} - ${fmtMoney(c.spend)} - ${fmtNum(c.conversions)} - ${fmtPct(c.ctr)} - ${c.roas !== undefined ? c.roas.toFixed(2) + "x" : "—"} - ` - ).join("\n"); - - const body = ` -
    -

    📈 Performance Overview

    -
    ${data.accountName ? escapeHtml(data.accountName) + " · " : ""}${escapeHtml(data.period)}
    -
    - -
    - ${kpis} -
    - -
    -
    -
    Daily Spend — Last 7 Days
    -
    - ${bars} -
    -
    -
    - -
    -
    -
    -
    Top Campaigns
    -
    -
    - - - - - - - - - - - - - ${rows} - -
    #CampaignSpendConv.CTRROAS
    -
    -
    -
    - `; - - return wrapHtml("Performance Overview", body); -} diff --git a/mcp-diagrams/google-ads-mcp/src/apps/recommendations.ts b/mcp-diagrams/google-ads-mcp/src/apps/recommendations.ts deleted file mode 100644 index fd30e5c..0000000 --- a/mcp-diagrams/google-ads-mcp/src/apps/recommendations.ts +++ /dev/null @@ -1,151 +0,0 @@ -// ============================================ -// APP: Recommendations — Google optimization recommendations -// ============================================ - -import { wrapHtml, escapeHtml, fmtMoney, fmtPct } from "./theme.js"; - -export interface RecommendationsData { - accountName?: string; - recommendations: RecommendationItem[]; -} - -export interface RecommendationItem { - type: string; - category: string; // BIDS, KEYWORDS, ADS, TARGETING, BUDGET, EXTENSIONS, etc. - impact: "LOW" | "MEDIUM" | "HIGH"; - title: string; - description: string; - campaignName?: string; - estimatedImprovement?: string; // e.g. "+12% clicks", "+$240 revenue" - resourceName?: string; -} - -function impactBadge(impact: string): string { - const i = impact.toUpperCase(); - const map: Record = { - HIGH: { cls: "pill-red", icon: "🔴" }, - MEDIUM: { cls: "pill-yellow", icon: "🟡" }, - LOW: { cls: "pill-blue", icon: "🔵" }, - }; - const m = map[i] || { cls: "pill-muted", icon: "⚪" }; - return `${m.icon} ${i} IMPACT`; -} - -function categoryPill(category: string): string { - const cat = category.toUpperCase(); - const colorMap: Record = { - BIDS: "pill-purple", - KEYWORDS: "pill-blue", - ADS: "pill-orange", - TARGETING: "pill-green", - BUDGET: "pill-red", - EXTENSIONS: "pill-yellow", - BIDDING: "pill-purple", - CAMPAIGN: "pill-blue", - }; - const cls = colorMap[cat] || "pill-muted"; - return `${escapeHtml(cat)}`; -} - -export function renderRecommendations(data: RecommendationsData): string { - const recs = data.recommendations; - const byImpact = { HIGH: 0, MEDIUM: 0, LOW: 0 }; - recs.forEach((r) => { - const key = r.impact.toUpperCase() as keyof typeof byImpact; - if (key in byImpact) byImpact[key]++; - }); - - // Group by category - const categories = [...new Set(recs.map((r) => r.category.toUpperCase()))]; - - const cards = recs.map((r) => ` -
    -
    - ${impactBadge(r.impact)} - ${categoryPill(r.category)} -
    -
    ${escapeHtml(r.title)}
    -
    ${escapeHtml(r.description)}
    - ${r.campaignName ? `
    📂 ${escapeHtml(r.campaignName)}
    ` : ""} - ${r.estimatedImprovement ? ` -
    - ⬆ ${escapeHtml(r.estimatedImprovement)} -
    - ` : ""} -
    ` - ).join("\n"); - - const body = ` - - -
    -

    💡 Optimization Recommendations

    -
    ${data.accountName ? escapeHtml(data.accountName) + " · " : ""}${recs.length} recommendations
    -
    - -
    -
    -
    ${byImpact.HIGH}
    -
    High Impact
    -
    -
    -
    ${byImpact.MEDIUM}
    -
    Medium Impact
    -
    -
    -
    ${byImpact.LOW}
    -
    Low Impact
    -
    -
    - -
    -
    - ${categories.map((cat) => categoryPill(cat)).join(" ")} -
    -
    - -
    - ${cards || `
    No recommendations available
    `} -
    - `; - - return wrapHtml("Recommendations", body); -} diff --git a/mcp-diagrams/google-ads-mcp/src/apps/search-terms.ts b/mcp-diagrams/google-ads-mcp/src/apps/search-terms.ts deleted file mode 100644 index ef82504..0000000 --- a/mcp-diagrams/google-ads-mcp/src/apps/search-terms.ts +++ /dev/null @@ -1,126 +0,0 @@ -// ============================================ -// APP: Search Terms — Search terms report with actions -// ============================================ - -import { wrapHtml, escapeHtml, fmtMoney, fmtNum, fmtPct } from "./theme.js"; - -export interface SearchTermsData { - period?: string; - costThreshold?: number; // micros — terms above this with 0 conversions flagged - terms: SearchTermRow[]; -} - -export interface SearchTermRow { - searchTerm: string; - campaign: string; - adGroup: string; - impressions: number; - clicks: number; - ctr: number; // decimal - cost: number; // micros - conversions: number; - suggestedAction?: "add_keyword" | "add_negative" | "none"; -} - -function actionBadge(action?: string): string { - switch (action) { - case "add_keyword": - return `+ Add Keyword`; - case "add_negative": - return `— Add Negative`; - default: - return `No Action`; - } -} - -function inferAction(term: SearchTermRow, costThreshold: number): string { - if (term.suggestedAction && term.suggestedAction !== "none") return term.suggestedAction; - // Auto-infer: high cost + 0 conversions → negative; good CTR + conversions → add keyword - if (term.cost > costThreshold && term.conversions === 0) return "add_negative"; - if (term.conversions > 0 && term.ctr > 0.03) return "add_keyword"; - return "none"; -} - -export function renderSearchTerms(data: SearchTermsData): string { - const costThreshold = data.costThreshold || 5_000_000; // default $5 - const terms = data.terms; - const totalTerms = terms.length; - const totalCost = terms.reduce((s, t) => s + t.cost, 0); - const negCandidates = terms.filter((t) => inferAction(t, costThreshold) === "add_negative").length; - const kwCandidates = terms.filter((t) => inferAction(t, costThreshold) === "add_keyword").length; - - const rows = terms.map((t) => { - const action = inferAction(t, costThreshold); - const isHighCostLowConv = t.cost > costThreshold && t.conversions === 0; - const rowStyle = isHighCostLowConv ? 'style="background:var(--red-dim)"' : ""; - - return ` - - ${escapeHtml(t.searchTerm)} - ${escapeHtml(t.campaign)} - ${escapeHtml(t.adGroup)} - ${fmtNum(t.impressions)} - ${fmtNum(t.clicks)} - ${fmtPct(t.ctr)} - ${fmtMoney(t.cost)} - ${fmtNum(t.conversions)} - ${actionBadge(action)} - `; - }).join("\n"); - - const body = ` -
    -

    🔍 Search Terms Report

    -
    ${data.period ? escapeHtml(data.period) + " · " : ""}${fmtNum(totalTerms)} search terms · ${fmtMoney(totalCost)} total cost
    -
    - -
    -
    -
    ${fmtNum(totalTerms)}
    -
    Search Terms
    -
    -
    -
    ${fmtNum(kwCandidates)}
    -
    Add as Keyword
    -
    -
    -
    ${fmtNum(negCandidates)}
    -
    Add as Negative
    -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - ${rows || ``} - -
    Search TermCampaignAd GroupImpr.ClicksCTRCostConv.Suggested Action
    No search terms found
    -
    -
    -
    - -
    -
    - - Highlighted rows indicate high-cost terms (>${fmtMoney(costThreshold)}) with zero conversions -
    -
    - `; - - return wrapHtml("Search Terms Report", body); -} diff --git a/mcp-diagrams/google-ads-mcp/src/apps/theme.ts b/mcp-diagrams/google-ads-mcp/src/apps/theme.ts deleted file mode 100644 index 52f0f03..0000000 --- a/mcp-diagrams/google-ads-mcp/src/apps/theme.ts +++ /dev/null @@ -1,380 +0,0 @@ -// ============================================ -// SHARED THEME — Dark UI for Google Ads MCP Apps -// ============================================ - -export const THEME_CSS = ` - :root { - --bg-primary: #1a1a2e; - --bg-card: #16213e; - --bg-card-hover: #1a2744; - --bg-accent: #0f3460; - --bg-input: #0d1b2a; - --highlight: #e94560; - --highlight-dim: rgba(233, 69, 96, 0.15); - --text-primary: #e8e8e8; - --text-secondary: #a0a0b8; - --text-muted: #6c6c8a; - --border: rgba(255, 255, 255, 0.06); - --border-light: rgba(255, 255, 255, 0.1); - --green: #00c897; - --green-dim: rgba(0, 200, 151, 0.15); - --yellow: #f0c040; - --yellow-dim: rgba(240, 192, 64, 0.15); - --red: #e94560; - --red-dim: rgba(233, 69, 96, 0.15); - --blue: #4ea8de; - --blue-dim: rgba(78, 168, 222, 0.15); - --purple: #9b5de5; - --purple-dim: rgba(155, 93, 229, 0.15); - --orange: #f48c06; - --orange-dim: rgba(244, 140, 6, 0.15); - --radius: 10px; - --radius-sm: 6px; - --shadow: 0 2px 12px rgba(0,0,0,0.25); - --shadow-lg: 0 8px 32px rgba(0,0,0,0.35); - --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - --mono: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; - } - - * { margin: 0; padding: 0; box-sizing: border-box; } - - body { - font-family: var(--font); - background: var(--bg-primary); - color: var(--text-primary); - line-height: 1.5; - -webkit-font-smoothing: antialiased; - padding: 20px; - } - - .app-container { - max-width: 1200px; - margin: 0 auto; - } - - /* ---- Header ---- */ - .app-header { - margin-bottom: 24px; - } - .app-header h1 { - font-size: 22px; - font-weight: 700; - letter-spacing: -0.3px; - color: var(--text-primary); - } - .app-header .subtitle { - font-size: 13px; - color: var(--text-secondary); - margin-top: 4px; - } - - /* ---- Cards ---- */ - .card { - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 20px; - transition: border-color 0.2s, box-shadow 0.2s; - } - .card:hover { - border-color: var(--border-light); - box-shadow: var(--shadow); - } - .card-title { - font-size: 14px; - font-weight: 600; - color: var(--text-primary); - margin-bottom: 12px; - } - - /* ---- Grid Layouts ---- */ - .grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; } - .grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; } - .grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; } - .grid-6 { display: grid; grid-template-columns: repeat(6, 1fr); gap: 16px; } - - @media (max-width: 900px) { - .grid-6 { grid-template-columns: repeat(3, 1fr); } - .grid-4 { grid-template-columns: repeat(2, 1fr); } - .grid-3 { grid-template-columns: repeat(2, 1fr); } - } - @media (max-width: 600px) { - .grid-6, .grid-4, .grid-3, .grid-2 { grid-template-columns: 1fr; } - } - - /* ---- KPI Cards ---- */ - .kpi-card { - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 16px 18px; - text-align: center; - } - .kpi-value { - font-size: 26px; - font-weight: 700; - letter-spacing: -0.5px; - color: var(--text-primary); - line-height: 1.2; - } - .kpi-label { - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.8px; - color: var(--text-muted); - margin-top: 6px; - } - .kpi-delta { - font-size: 12px; - font-weight: 600; - margin-top: 6px; - display: inline-block; - padding: 2px 8px; - border-radius: 12px; - } - .kpi-delta.up { color: var(--green); background: var(--green-dim); } - .kpi-delta.down { color: var(--red); background: var(--red-dim); } - .kpi-delta.neutral { color: var(--text-muted); background: var(--border); } - - /* ---- Tables ---- */ - .table-wrap { - overflow-x: auto; - border-radius: var(--radius); - border: 1px solid var(--border); - } - table { - width: 100%; - border-collapse: collapse; - font-size: 13px; - } - thead th { - background: var(--bg-accent); - color: var(--text-secondary); - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.6px; - padding: 10px 14px; - text-align: left; - white-space: nowrap; - border-bottom: 1px solid var(--border); - } - thead th.right, tbody td.right { text-align: right; } - tbody td { - padding: 10px 14px; - border-bottom: 1px solid var(--border); - color: var(--text-primary); - white-space: nowrap; - } - tbody tr:last-child td { border-bottom: none; } - tbody tr:hover { background: rgba(255,255,255,0.02); } - - /* ---- Pills / Badges ---- */ - .pill { - display: inline-block; - font-size: 10px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.5px; - padding: 3px 10px; - border-radius: 12px; - line-height: 1.4; - } - .pill-green { color: var(--green); background: var(--green-dim); } - .pill-yellow { color: var(--yellow); background: var(--yellow-dim); } - .pill-red { color: var(--red); background: var(--red-dim); } - .pill-blue { color: var(--blue); background: var(--blue-dim); } - .pill-purple { color: var(--purple); background: var(--purple-dim); } - .pill-orange { color: var(--orange); background: var(--orange-dim); } - .pill-muted { color: var(--text-muted); background: var(--border); } - - /* ---- Progress Bars ---- */ - .progress-track { - background: var(--bg-input); - border-radius: 6px; - height: 8px; - overflow: hidden; - width: 100%; - } - .progress-fill { - height: 100%; - border-radius: 6px; - transition: width 0.3s ease; - } - .progress-fill.green { background: var(--green); } - .progress-fill.yellow { background: var(--yellow); } - .progress-fill.red { background: var(--red); } - .progress-fill.blue { background: var(--blue); } - .progress-fill.accent { background: var(--highlight); } - - /* ---- Section Spacing ---- */ - .section { margin-bottom: 24px; } - .section-title { - font-size: 15px; - font-weight: 600; - color: var(--text-primary); - margin-bottom: 12px; - } - - /* ---- Metric Inline ---- */ - .metric-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 0; - border-bottom: 1px solid var(--border); - } - .metric-row:last-child { border-bottom: none; } - .metric-label { color: var(--text-secondary); font-size: 13px; } - .metric-value { color: var(--text-primary); font-weight: 600; font-size: 14px; font-family: var(--mono); } - - /* ---- Bar Chart (CSS-only) ---- */ - .bar-chart { - display: flex; - align-items: flex-end; - gap: 6px; - height: 100px; - padding-top: 8px; - } - .bar-col { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; - } - .bar { - width: 100%; - border-radius: 4px 4px 0 0; - background: var(--highlight); - min-height: 2px; - transition: height 0.3s; - } - .bar-label { - font-size: 10px; - color: var(--text-muted); - text-align: center; - } - - /* ---- Quality Score Bar ---- */ - .qs-bar { - display: inline-flex; - align-items: center; - gap: 6px; - min-width: 100px; - } - .qs-track { - flex: 1; - height: 6px; - background: var(--bg-input); - border-radius: 3px; - overflow: hidden; - } - .qs-fill { - height: 100%; - border-radius: 3px; - } - .qs-num { - font-size: 12px; - font-weight: 700; - font-family: var(--mono); - min-width: 18px; - text-align: right; - } - - /* ---- Utility ---- */ - .text-green { color: var(--green); } - .text-yellow { color: var(--yellow); } - .text-red { color: var(--red); } - .text-blue { color: var(--blue); } - .text-purple { color: var(--purple); } - .text-muted { color: var(--text-muted); } - .text-mono { font-family: var(--mono); } - .mt-16 { margin-top: 16px; } - .mt-24 { margin-top: 24px; } - .mb-8 { margin-bottom: 8px; } - .mb-16 { margin-bottom: 16px; } - .flex { display: flex; } - .flex-between { display: flex; justify-content: space-between; align-items: center; } - .gap-8 { gap: 8px; } - .gap-12 { gap: 12px; } - .gap-16 { gap: 16px; } - .wrap { flex-wrap: wrap; } -`; - -/** - * Wrap inner HTML in a full HTML document with the shared theme CSS - */ -export function wrapHtml(title: string, body: string): string { - return ` - - - - - ${escapeHtml(title)} - - - -
    - ${body} -
    - -`; -} - -/** - * Escape HTML entities - */ -export function escapeHtml(str: string): string { - return str - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """); -} - -/** - * Format micros to currency string - */ -export function fmtMoney(micros: string | number): string { - const num = typeof micros === "string" ? parseInt(micros, 10) : micros; - if (isNaN(num)) return "$0.00"; - return "$" + (num / 1_000_000).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); -} - -/** - * Format number with commas - */ -export function fmtNum(value: string | number): string { - const num = typeof value === "string" ? parseInt(value, 10) : value; - if (isNaN(num)) return "0"; - return num.toLocaleString("en-US"); -} - -/** - * Format percentage (input as decimal, e.g. 0.0534 → "5.34%") - */ -export function fmtPct(value: number): string { - if (isNaN(value)) return "0.00%"; - return (value * 100).toFixed(2) + "%"; -} - -/** - * Get status pill HTML - */ -export function statusPill(status: string): string { - const s = status.toUpperCase(); - if (s === "ENABLED" || s === "ACTIVE") return `${s}`; - if (s === "PAUSED") return `${s}`; - if (s === "REMOVED" || s === "DISABLED") return `${s}`; - return `${s}`; -} - -/** - * Build a progress bar HTML - */ -export function progressBar(pct: number, color: string = "accent"): string { - const clamped = Math.max(0, Math.min(100, pct)); - return `
    `; -} diff --git a/mcp-diagrams/google-ads-mcp/src/auth.ts b/mcp-diagrams/google-ads-mcp/src/auth.ts deleted file mode 100644 index 6ecee16..0000000 --- a/mcp-diagrams/google-ads-mcp/src/auth.ts +++ /dev/null @@ -1,107 +0,0 @@ -// ============================================ -// GOOGLE ADS API AUTHENTICATION -// ============================================ - -import type { GoogleAdsConfig } from "./types.js"; - -const TOKEN_URL = "https://oauth2.googleapis.com/token"; - -interface TokenResponse { - access_token: string; - expires_in: number; - token_type: string; -} - -export class GoogleAdsAuth { - private config: GoogleAdsConfig; - private accessToken: string | null = null; - private tokenExpiry: number = 0; - - constructor(config: GoogleAdsConfig) { - this.config = config; - } - - async getAccessToken(): Promise { - // Return cached token if still valid (with 60s buffer) - if (this.accessToken && Date.now() < this.tokenExpiry - 60_000) { - return this.accessToken; - } - - const response = await fetch(TOKEN_URL, { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - client_id: this.config.clientId, - client_secret: this.config.clientSecret, - refresh_token: this.config.refreshToken, - grant_type: "refresh_token", - }), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Token refresh failed: ${response.status} — ${error}`); - } - - const data = (await response.json()) as TokenResponse; - this.accessToken = data.access_token; - this.tokenExpiry = Date.now() + data.expires_in * 1000; - - return this.accessToken; - } - - getHeaders(customerId?: string): Record { - const headers: Record = { - "developer-token": this.config.developerToken, - "Content-Type": "application/json", - }; - - if (this.config.loginCustomerId) { - headers["login-customer-id"] = this.config.loginCustomerId.replace(/-/g, ""); - } - - return headers; - } - - static fromEnv(): GoogleAdsAuth { - // Support inline JSON config (like samihalawa's approach) - const jsonConfig = process.env.GOOGLE_ADS_CONFIG; - if (jsonConfig) { - try { - const parsed = JSON.parse(jsonConfig); - return new GoogleAdsAuth({ - clientId: parsed.client_id, - clientSecret: parsed.client_secret, - developerToken: parsed.developer_token, - refreshToken: parsed.refresh_token, - loginCustomerId: parsed.login_customer_id, - customerId: process.env.GOOGLE_ADS_CUSTOMER_ID, - }); - } catch { - throw new Error("Invalid GOOGLE_ADS_CONFIG JSON"); - } - } - - // Support individual env vars - const clientId = process.env.GOOGLE_ADS_CLIENT_ID; - const clientSecret = process.env.GOOGLE_ADS_CLIENT_SECRET; - const developerToken = process.env.GOOGLE_ADS_DEVELOPER_TOKEN; - const refreshToken = process.env.GOOGLE_ADS_REFRESH_TOKEN; - - if (!clientId || !clientSecret || !developerToken || !refreshToken) { - throw new Error( - "Missing Google Ads credentials. Set GOOGLE_ADS_CONFIG (JSON) or individual env vars: " + - "GOOGLE_ADS_CLIENT_ID, GOOGLE_ADS_CLIENT_SECRET, GOOGLE_ADS_DEVELOPER_TOKEN, GOOGLE_ADS_REFRESH_TOKEN" - ); - } - - return new GoogleAdsAuth({ - clientId, - clientSecret, - developerToken, - refreshToken, - loginCustomerId: process.env.GOOGLE_ADS_LOGIN_CUSTOMER_ID, - customerId: process.env.GOOGLE_ADS_CUSTOMER_ID, - }); - } -} diff --git a/mcp-diagrams/google-ads-mcp/src/client.ts b/mcp-diagrams/google-ads-mcp/src/client.ts deleted file mode 100644 index 55b88e2..0000000 --- a/mcp-diagrams/google-ads-mcp/src/client.ts +++ /dev/null @@ -1,182 +0,0 @@ -// ============================================ -// GOOGLE ADS API CLIENT (REST v21) -// ============================================ - -import { GoogleAdsAuth } from "./auth.js"; -import type { GoogleAdsClient, GoogleAdsConfig, MutateOperation } from "./types.js"; - -const API_VERSION = "v21"; -const BASE_URL = `https://googleads.googleapis.com/${API_VERSION}`; - -export class GoogleAdsRestClient implements GoogleAdsClient { - private auth: GoogleAdsAuth; - private defaultCustomerId: string; - - constructor(auth: GoogleAdsAuth, defaultCustomerId?: string) { - this.auth = auth; - this.defaultCustomerId = (defaultCustomerId || "").replace(/-/g, ""); - } - - getCustomerId(): string { - return this.defaultCustomerId; - } - - private async request(url: string, options: RequestInit = {}): Promise { - const token = await this.auth.getAccessToken(); - const headers = { - ...this.auth.getHeaders(), - Authorization: `Bearer ${token}`, - ...options.headers, - }; - - const response = await fetch(url, { ...options, headers }); - - if (!response.ok) { - const errorBody = await response.text(); - let errorMessage = `Google Ads API error: ${response.status}`; - try { - const errorJson = JSON.parse(errorBody); - const details = errorJson.error?.details?.[0]?.errors?.[0]; - if (details) { - errorMessage += ` — ${details.errorCode ? JSON.stringify(details.errorCode) : ""} ${details.message || ""}`; - } - } catch { - errorMessage += ` — ${errorBody.slice(0, 500)}`; - } - throw new Error(errorMessage); - } - - return response.json(); - } - - /** - * Execute a GAQL (Google Ads Query Language) query - */ - async query(customerId: string, gaqlQuery: string): Promise { - const cid = (customerId || this.defaultCustomerId).replace(/-/g, ""); - if (!cid) throw new Error("Customer ID required. Pass it as a parameter or set GOOGLE_ADS_CUSTOMER_ID env var."); - - const url = `${BASE_URL}/customers/${cid}/googleAds:searchStream`; - const data = await this.request(url, { - method: "POST", - body: JSON.stringify({ query: gaqlQuery }), - }); - - // searchStream returns array of result batches - const results: any[] = []; - if (Array.isArray(data)) { - for (const batch of data) { - if (batch.results) { - results.push(...batch.results); - } - } - } else if (data.results) { - results.push(...data.results); - } - - return results; - } - - /** - * Execute mutate operations (create, update, remove) - */ - async mutate(customerId: string, operations: MutateOperation[]): Promise { - const cid = (customerId || this.defaultCustomerId).replace(/-/g, ""); - if (!cid) throw new Error("Customer ID required."); - - const mutateOps = operations.map((op) => { - const key = `${op.entity}Operation`; - const opBody: any = {}; - - if (op.operation === "create") { - opBody.create = op.resource; - } else if (op.operation === "update") { - opBody.update = op.resource; - if (op.updateMask) { - opBody.updateMask = op.updateMask; - } - } else if (op.operation === "remove") { - opBody.remove = op.resourceName; - } - - return { [key]: opBody }; - }); - - const url = `${BASE_URL}/customers/${cid}/googleAds:mutate`; - return this.request(url, { - method: "POST", - body: JSON.stringify({ mutateOperations: mutateOps }), - }); - } - - /** - * List all customer accounts accessible with current credentials - */ - async listAccessibleCustomers(): Promise { - const url = `${BASE_URL}/customers:listAccessibleCustomers`; - const data = await this.request(url, { method: "GET" }); - return (data.resourceNames || []).map((rn: string) => rn.replace("customers/", "")); - } - - /** - * Get a specific resource by resource name - */ - async getResource(resourceName: string): Promise { - const url = `${BASE_URL}/${resourceName}`; - return this.request(url, { method: "GET" }); - } - - /** - * Generate keyword ideas using KeywordPlanIdeaService - */ - async generateKeywordIdeas( - customerId: string, - options: { - seedKeywords?: string[]; - seedUrl?: string; - language?: string; - geoTargets?: string[]; - } - ): Promise { - const cid = (customerId || this.defaultCustomerId).replace(/-/g, ""); - const url = `${BASE_URL}/customers/${cid}:generateKeywordIdeas`; - - const body: any = { - language: options.language || "languageConstants/1000", // English - geoTargetConstants: options.geoTargets || ["geoTargetConstants/2840"], // US - keywordPlanNetwork: "GOOGLE_SEARCH", - }; - - if (options.seedKeywords?.length) { - body.keywordSeed = { keywords: options.seedKeywords }; - } - if (options.seedUrl) { - body.urlSeed = { url: options.seedUrl }; - } - - const data = await this.request(url, { - method: "POST", - body: JSON.stringify(body), - }); - - return data.results || []; - } - - /** - * Get recommendations for the account - */ - async getRecommendations(customerId: string, types?: string[]): Promise { - const cid = (customerId || this.defaultCustomerId).replace(/-/g, ""); - let query = `SELECT recommendation.type, recommendation.impact, recommendation.campaign_budget_recommendation, recommendation.keyword_recommendation, recommendation.resource_name FROM recommendation`; - if (types?.length) { - query += ` WHERE recommendation.type IN (${types.map((t) => `'${t}'`).join(", ")})`; - } - query += ` LIMIT 50`; - return this.query(cid, query); - } - - static fromEnv(): GoogleAdsRestClient { - const auth = GoogleAdsAuth.fromEnv(); - return new GoogleAdsRestClient(auth, process.env.GOOGLE_ADS_CUSTOMER_ID); - } -} diff --git a/mcp-diagrams/google-ads-mcp/src/index.ts b/mcp-diagrams/google-ads-mcp/src/index.ts deleted file mode 100644 index 5a6a888..0000000 --- a/mcp-diagrams/google-ads-mcp/src/index.ts +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env node -// ============================================ -// GOOGLE ADS MCP SERVER -// The definitive Google Ads MCP — 49 tools, 7 apps -// ============================================ - -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from "@modelcontextprotocol/sdk/types.js"; - -import { GoogleAdsRestClient } from "./client.js"; -import type { ToolDefinition, ToolCategory } from "./types.js"; -import { - registerTools, - getAllLoadedTools, - getTool, - loadCategory, - getCoreTools, - loadToolsCategoryTool, -} from "./tools/index.js"; - -// ============================================ -// BOOT: Load core tool categories eagerly -// ============================================ -async function loadCoreTools(): Promise { - // These categories contain the "always available" core tools - await Promise.all([ - loadCategory("accounts"), - loadCategory("campaigns"), - loadCategory("reporting"), - loadCategory("advanced"), - ]); -} - -// ============================================ -// SERVER SETUP -// ============================================ -const server = new Server( - { - name: "google-ads-mcp", - version: "1.0.0", - }, - { - capabilities: { - tools: {}, - }, - } -); - -let client: GoogleAdsRestClient; - -// ============================================ -// LIST TOOLS — returns currently loaded tools -// ============================================ -server.setRequestHandler(ListToolsRequestSchema, async () => { - const tools = getAllLoadedTools(); - - return { - tools: tools.map((t) => ({ - name: t.name, - description: t.description, - inputSchema: t.inputSchema, - _meta: t._meta, - annotations: { - title: t.annotations.title, - readOnlyHint: t.annotations.readOnlyHint, - destructiveHint: t.annotations.destructiveHint, - idempotentHint: t.annotations.idempotentHint, - openWorldHint: t.annotations.openWorldHint, - }, - })), - }; -}); - -// ============================================ -// CALL TOOL — dispatch to handler -// ============================================ -server.setRequestHandler(CallToolRequestSchema, async (request): Promise => { - const { name, arguments: args = {} } = request.params; - - // Check loaded tools first - let tool = getTool(name); - - // If not found, try lazy-loading all remaining categories - if (!tool) { - const lazyCategories: ToolCategory[] = [ - "ad_groups", - "ads", - "keywords", - "bidding", - "conversions", - ]; - - for (const cat of lazyCategories) { - try { - await loadCategory(cat); - } catch { - // Category may already be loaded - } - } - tool = getTool(name); - } - - if (!tool) { - return { - content: [ - { - type: "text" as const, - text: `Unknown tool: ${name}. Use \`load_tools_category\` to see available tools and categories.`, - }, - ], - isError: true, - }; - } - - try { - const result = await tool.handler(args, client); - return result; - } catch (error: any) { - const message = error?.message || String(error); - return { - content: [ - { - type: "text" as const, - text: `Error executing ${name}: ${message}`, - }, - ], - isError: true, - }; - } -}); - -// ============================================ -// MAIN -// ============================================ -async function main() { - // Initialize API client - try { - client = GoogleAdsRestClient.fromEnv(); - } catch (error: any) { - console.error(`[google-ads-mcp] Auth error: ${error.message}`); - console.error( - "[google-ads-mcp] Set GOOGLE_ADS_CONFIG (JSON) or individual env vars:\n" + - " GOOGLE_ADS_CLIENT_ID, GOOGLE_ADS_CLIENT_SECRET,\n" + - " GOOGLE_ADS_DEVELOPER_TOKEN, GOOGLE_ADS_REFRESH_TOKEN,\n" + - " GOOGLE_ADS_CUSTOMER_ID (optional default)" - ); - process.exit(1); - } - - // Load core tool categories - await loadCoreTools(); - - const coreCount = getAllLoadedTools().length; - console.error(`[google-ads-mcp] Loaded ${coreCount} core tools (${5 + 8 + 7 + 4} from accounts, campaigns, reporting, advanced)`); - console.error("[google-ads-mcp] Additional categories available via load_tools_category: ad_groups, ads, keywords, bidding, conversions"); - - // Start MCP server on stdio - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("[google-ads-mcp] Server running on stdio"); -} - -main().catch((error) => { - console.error("[google-ads-mcp] Fatal:", error); - process.exit(1); -}); diff --git a/mcp-diagrams/google-ads-mcp/src/tools/accounts.ts b/mcp-diagrams/google-ads-mcp/src/tools/accounts.ts deleted file mode 100644 index 566fc63..0000000 --- a/mcp-diagrams/google-ads-mcp/src/tools/accounts.ts +++ /dev/null @@ -1,385 +0,0 @@ -// ============================================ -// ACCOUNT MANAGEMENT TOOLS -// ============================================ - -import type { ToolDefinition, ToolResult, GoogleAdsClient } from "../types.js"; -import { microsToMoney, formatNumber } from "../types.js"; - -function text(t: string): ToolResult { - return { content: [{ type: "text", text: t }] }; -} - -function errorResult(msg: string): ToolResult { - return { content: [{ type: "text", text: `❌ ${msg}` }], isError: true }; -} - -// ─── list_accessible_customers ────────────────────────────────────── -const listAccessibleCustomers: ToolDefinition = { - name: "list_accessible_customers", - description: - "List all Google Ads customer accounts accessible with the current credentials. " + - "Returns customer IDs that can be used with other tools. Useful for discovering " + - "which accounts you can manage.", - category: "accounts", - annotations: { - title: "List Accessible Customers", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: {}, - }, - _meta: { - labels: { - category: "accounts", - access: "read", - complexity: "simple" - } - }, - handler: async (_args: Record, client: GoogleAdsClient): Promise => { - const customerIds = await client.listAccessibleCustomers(); - - if (!customerIds.length) { - return text("No accessible customer accounts found with the current credentials."); - } - - const lines = customerIds.map((id, i) => ` ${i + 1}. \`${id}\``); - const defaultId = client.getCustomerId(); - const defaultNote = defaultId ? `\n\n**Default customer ID:** \`${defaultId}\`` : ""; - - return text( - `## Accessible Customer Accounts\n\n` + - `Found **${customerIds.length}** accessible account(s):\n\n` + - `${lines.join("\n")}${defaultNote}\n\n` + - `Use \`get_account_info\` with a customer ID to get details about a specific account.` - ); - }, -}; - -// ─── get_account_info ─────────────────────────────────────────────── -const getAccountInfo: ToolDefinition = { - name: "get_account_info", - description: - "Get detailed information about a Google Ads account including name, currency, " + - "timezone, manager status, and account status.", - category: "accounts", - annotations: { - title: "Get Account Info", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (e.g., '1234567890'). Uses default if not provided.", - }, - }, - }, - _meta: { - labels: { - category: "accounts", - access: "read", - complexity: "simple" - } - }, - handler: async (args: Record, client: GoogleAdsClient): Promise => { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) return errorResult("No customer ID provided and no default configured."); - - const query = ` - SELECT - customer.id, - customer.descriptive_name, - customer.currency_code, - customer.time_zone, - customer.manager, - customer.status - FROM customer - `; - - const results = await client.query(customerId, query); - if (!results.length) return errorResult("No account data found for this customer ID."); - - const c = results[0].customer; - - return text( - `## Account Info\n\n` + - `| Field | Value |\n` + - `|-------|-------|\n` + - `| **Customer ID** | \`${c.id}\` |\n` + - `| **Name** | ${c.descriptiveName || "—"} |\n` + - `| **Currency** | ${c.currencyCode || "—"} |\n` + - `| **Timezone** | ${c.timeZone || "—"} |\n` + - `| **Manager Account** | ${c.manager ? "Yes" : "No"} |\n` + - `| **Status** | ${c.status || "—"} |` - ); - }, -}; - -// ─── get_account_hierarchy ────────────────────────────────────────── -const getAccountHierarchy: ToolDefinition = { - name: "get_account_hierarchy", - description: - "Get the manager-to-client account hierarchy tree. Shows all sub-accounts " + - "under a manager account with their names, IDs, and status.", - category: "accounts", - annotations: { - title: "Get Account Hierarchy", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Manager account customer ID. Uses default if not provided.", - }, - }, - }, - _meta: { - labels: { - category: "accounts", - access: "read", - complexity: "simple" - } - }, - handler: async (args: Record, client: GoogleAdsClient): Promise => { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) return errorResult("No customer ID provided and no default configured."); - - const query = ` - SELECT - customer_client.client_customer, - customer_client.id, - customer_client.descriptive_name, - customer_client.currency_code, - customer_client.time_zone, - customer_client.manager, - customer_client.level, - customer_client.status, - customer_client.hidden - FROM customer_client - ORDER BY customer_client.level ASC, customer_client.descriptive_name ASC - `; - - const results = await client.query(customerId, query); - if (!results.length) { - return text("No client accounts found. This may not be a manager account."); - } - - const header = - `| Level | Customer ID | Name | Manager | Currency | Status | Hidden |\n` + - `|-------|-------------|------|---------|----------|--------|--------|`; - - const rows = results.map((r: any) => { - const cc = r.customerClient; - const indent = " ".repeat(cc.level || 0); - return ( - `| ${cc.level ?? "—"} | \`${cc.id}\` | ${indent}${cc.descriptiveName || "—"} | ` + - `${cc.manager ? "✅" : "—"} | ${cc.currencyCode || "—"} | ${cc.status || "—"} | ` + - `${cc.hidden ? "Yes" : "No"} |` - ); - }); - - return text( - `## Account Hierarchy for \`${customerId}\`\n\n` + - `Found **${results.length}** account(s) in hierarchy.\n\n` + - `${header}\n${rows.join("\n")}` - ); - }, -}; - -// ─── get_billing_info ─────────────────────────────────────────────── -const getBillingInfo: ToolDefinition = { - name: "get_billing_info", - description: - "Get billing setup information for a Google Ads account, including " + - "billing status and payments account details.", - category: "accounts", - annotations: { - title: "Get Billing Info", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID. Uses default if not provided.", - }, - }, - }, - _meta: { - labels: { - category: "accounts", - access: "read", - complexity: "simple" - } - }, - handler: async (args: Record, client: GoogleAdsClient): Promise => { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) return errorResult("No customer ID provided and no default configured."); - - const query = ` - SELECT - billing_setup.id, - billing_setup.status, - billing_setup.payments_account, - billing_setup.payments_account_info.payments_account_id, - billing_setup.payments_account_info.payments_account_name, - billing_setup.payments_account_info.payments_profile_id, - billing_setup.payments_account_info.payments_profile_name - FROM billing_setup - `; - - const results = await client.query(customerId, query); - if (!results.length) { - return text("No billing setup found for this account."); - } - - const sections = results.map((r: any, i: number) => { - const bs = r.billingSetup; - const info = bs.paymentsAccountInfo || {}; - return ( - `### Billing Setup ${i + 1}\n\n` + - `| Field | Value |\n` + - `|-------|-------|\n` + - `| **Setup ID** | \`${bs.id}\` |\n` + - `| **Status** | ${bs.status || "—"} |\n` + - `| **Payments Account** | ${bs.paymentsAccount || "—"} |\n` + - `| **Payments Account ID** | ${info.paymentsAccountId || "—"} |\n` + - `| **Payments Account Name** | ${info.paymentsAccountName || "—"} |\n` + - `| **Payments Profile ID** | ${info.paymentsProfileId || "—"} |\n` + - `| **Payments Profile Name** | ${info.paymentsProfileName || "—"} |` - ); - }); - - return text( - `## Billing Info for \`${customerId}\`\n\n` + - `Found **${results.length}** billing setup(s).\n\n` + - sections.join("\n\n") - ); - }, -}; - -// ─── get_account_budget ───────────────────────────────────────────── -const getAccountBudget: ToolDefinition = { - name: "get_account_budget", - description: - "Get account-level budget information including approved and proposed budgets, " + - "spending limits, and budget periods.", - category: "accounts", - annotations: { - title: "Get Account Budget", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID. Uses default if not provided.", - }, - }, - }, - _meta: { - labels: { - category: "accounts", - access: "read", - complexity: "simple" - } - }, - handler: async (args: Record, client: GoogleAdsClient): Promise => { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) return errorResult("No customer ID provided and no default configured."); - - const query = ` - SELECT - account_budget.id, - account_budget.name, - account_budget.status, - account_budget.approved_start_date_time, - account_budget.approved_end_date_time, - account_budget.approved_spending_limit_micros, - account_budget.approved_spending_limit_type, - account_budget.proposed_start_date_time, - account_budget.proposed_end_date_time, - account_budget.proposed_spending_limit_micros, - account_budget.proposed_spending_limit_type, - account_budget.amount_served_micros, - account_budget.billing_setup - FROM account_budget - ORDER BY account_budget.status ASC - `; - - const results = await client.query(customerId, query); - if (!results.length) { - return text("No account budgets found for this account."); - } - - const sections = results.map((r: any, i: number) => { - const ab = r.accountBudget; - - const approvedLimit = ab.approvedSpendingLimitMicros - ? `$${microsToMoney(ab.approvedSpendingLimitMicros)}` - : ab.approvedSpendingLimitType || "—"; - - const proposedLimit = ab.proposedSpendingLimitMicros - ? `$${microsToMoney(ab.proposedSpendingLimitMicros)}` - : ab.proposedSpendingLimitType || "—"; - - const served = ab.amountServedMicros - ? `$${microsToMoney(ab.amountServedMicros)}` - : "—"; - - return ( - `### Budget ${i + 1}: ${ab.name || "(unnamed)"}\n\n` + - `| Field | Value |\n` + - `|-------|-------|\n` + - `| **Budget ID** | \`${ab.id}\` |\n` + - `| **Status** | ${ab.status || "—"} |\n` + - `| **Approved Start** | ${ab.approvedStartDateTime || "—"} |\n` + - `| **Approved End** | ${ab.approvedEndDateTime || "—"} |\n` + - `| **Approved Limit** | ${approvedLimit} |\n` + - `| **Proposed Start** | ${ab.proposedStartDateTime || "—"} |\n` + - `| **Proposed End** | ${ab.proposedEndDateTime || "—"} |\n` + - `| **Proposed Limit** | ${proposedLimit} |\n` + - `| **Amount Served** | ${served} |\n` + - `| **Billing Setup** | ${ab.billingSetup || "—"} |` - ); - }); - - return text( - `## Account Budgets for \`${customerId}\`\n\n` + - `Found **${results.length}** budget(s).\n\n` + - sections.join("\n\n") - ); - }, -}; - -// ─── Export ───────────────────────────────────────────────────────── -const tools: ToolDefinition[] = [ - listAccessibleCustomers, - getAccountInfo, - getAccountHierarchy, - getBillingInfo, - getAccountBudget, -]; - -export default tools; diff --git a/mcp-diagrams/google-ads-mcp/src/tools/ad-groups.ts b/mcp-diagrams/google-ads-mcp/src/tools/ad-groups.ts deleted file mode 100644 index 505b3d9..0000000 --- a/mcp-diagrams/google-ads-mcp/src/tools/ad-groups.ts +++ /dev/null @@ -1,571 +0,0 @@ -// ============================================ -// AD GROUP MANAGEMENT TOOLS -// ============================================ - -import type { ToolDefinition, GoogleAdsClient, ToolResult } from "../types.js"; -import { microsToMoney, formatPercent, formatNumber } from "../types.js"; - -// ---- Helpers ---- - -function resolveCustomerId(args: Record, client: GoogleAdsClient): string { - const cid = args.customer_id || client.getCustomerId(); - if (!cid) throw new Error("customer_id is required — pass it explicitly or set GOOGLE_ADS_CUSTOMER_ID."); - return cid.replace(/-/g, ""); -} - -function dateRangeClause(dateRange?: string): string { - const allowed = ["LAST_7_DAYS", "LAST_30_DAYS", "LAST_90_DAYS", "THIS_MONTH", "LAST_MONTH", "TODAY", "YESTERDAY"]; - if (!dateRange) return "LAST_30_DAYS"; - const dr = dateRange.toUpperCase().replace(/ /g, "_"); - return allowed.includes(dr) ? dr : "LAST_30_DAYS"; -} - -function textResult(text: string): ToolResult { - return { content: [{ type: "text", text }] }; -} - -function errorResult(message: string): ToolResult { - return { content: [{ type: "text", text: `❌ ${message}` }], isError: true }; -} - -// ---- Tools ---- - -const tools: ToolDefinition[] = [ - // ============================ - // list_ad_groups - // ============================ - { - name: "list_ad_groups", - description: - "List ad groups for a campaign with status, type, bids, and performance metrics (impressions, clicks, cost, conversions, CTR).", - category: "ad_groups", - annotations: { - title: "List Ad Groups", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - campaign_id: { - type: "string", - description: "Campaign ID to list ad groups for.", - }, - date_range: { - type: "string", - description: "Date range for metrics: LAST_7_DAYS, LAST_30_DAYS, LAST_90_DAYS, THIS_MONTH, LAST_MONTH, TODAY, YESTERDAY. Default: LAST_30_DAYS.", - enum: ["LAST_7_DAYS", "LAST_30_DAYS", "LAST_90_DAYS", "THIS_MONTH", "LAST_MONTH", "TODAY", "YESTERDAY"], - }, - status_filter: { - type: "string", - description: "Filter by ad group status.", - enum: ["ENABLED", "PAUSED", "REMOVED"], - }, - limit: { - type: "number", - description: "Maximum number of ad groups to return (default: 50).", - }, - }, - required: ["campaign_id"], - }, - _meta: { - labels: { - category: "ad_groups", - access: "read", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - const dr = dateRangeClause(args.date_range); - const limit = Math.min(args.limit || 50, 500); - - let query = ` - SELECT - ad_group.id, - ad_group.name, - ad_group.status, - ad_group.type, - ad_group.cpc_bid_micros, - ad_group.resource_name, - campaign.id, - campaign.name, - metrics.impressions, - metrics.clicks, - metrics.cost_micros, - metrics.conversions, - metrics.ctr, - metrics.average_cpc - FROM ad_group - WHERE campaign.id = ${args.campaign_id} - AND segments.date DURING ${dr}`; - - if (args.status_filter) { - query += `\n AND ad_group.status = '${args.status_filter}'`; - } - - query += `\n ORDER BY metrics.cost_micros DESC\n LIMIT ${limit}`; - - const rows = await client.query(cid, query); - - if (!rows.length) { - return textResult("No ad groups found for this campaign and filter."); - } - - const lines = rows.map((r: any) => { - const ag = r.adGroup || {}; - const m = r.metrics || {}; - const cpcBid = ag.cpcBidMicros ? `$${microsToMoney(ag.cpcBidMicros)}` : "—"; - return [ - `**${ag.name || "Unnamed"}** (ID: ${ag.id})`, - ` Status: ${ag.status || "UNKNOWN"} | Type: ${ag.type || "—"} | Max CPC Bid: ${cpcBid}`, - ` Impressions: ${formatNumber(m.impressions || 0)} | Clicks: ${formatNumber(m.clicks || 0)} | CTR: ${formatPercent(m.ctr || 0)}`, - ` Cost: $${microsToMoney(m.costMicros || 0)} | Avg CPC: $${microsToMoney(m.averageCpc || 0)} | Conversions: ${formatNumber(m.conversions || 0)}`, - ].join("\n"); - }); - - return textResult( - `## Ad Groups for Campaign ${args.campaign_id} (${dr.replace(/_/g, " ")})\n\n${lines.join("\n\n")}\n\n_${rows.length} ad group(s) returned._` - ); - }, - }, - - // ============================ - // get_ad_group - // ============================ - { - name: "get_ad_group", - description: "Get detailed information about a single ad group by ID, including status, type, bids, and performance metrics.", - category: "ad_groups", - annotations: { - title: "Get Ad Group Details", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - ad_group_id: { - type: "string", - description: "The ad group ID to retrieve.", - }, - }, - required: ["ad_group_id"], - }, - _meta: { - labels: { - category: "ad_groups", - access: "read", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - - const query = ` - SELECT - ad_group.id, - ad_group.name, - ad_group.status, - ad_group.type, - ad_group.cpc_bid_micros, - ad_group.cpm_bid_micros, - ad_group.target_cpa_micros, - ad_group.target_roas, - ad_group.resource_name, - ad_group.ad_rotation_mode, - ad_group.effective_target_cpa_micros, - ad_group.effective_target_roas, - campaign.id, - campaign.name, - campaign.status, - metrics.impressions, - metrics.clicks, - metrics.cost_micros, - metrics.conversions, - metrics.conversions_value, - metrics.ctr, - metrics.average_cpc, - metrics.average_cpm - FROM ad_group - WHERE ad_group.id = ${args.ad_group_id} - AND segments.date DURING LAST_30_DAYS - LIMIT 1`; - - const rows = await client.query(cid, query); - - if (!rows.length) { - return errorResult(`Ad group ${args.ad_group_id} not found.`); - } - - const r = rows[0]; - const ag = r.adGroup || {}; - const camp = r.campaign || {}; - const m = r.metrics || {}; - - const details = [ - `## Ad Group: ${ag.name || "Unnamed"}`, - "", - `| Field | Value |`, - `|-------|-------|`, - `| ID | ${ag.id} |`, - `| Status | ${ag.status} |`, - `| Type | ${ag.type || "—"} |`, - `| Campaign | ${camp.name || "—"} (ID: ${camp.id || "—"}, ${camp.status || "—"}) |`, - `| Ad Rotation | ${ag.adRotationMode || "—"} |`, - `| Max CPC Bid | ${ag.cpcBidMicros ? "$" + microsToMoney(ag.cpcBidMicros) : "—"} |`, - `| Max CPM Bid | ${ag.cpmBidMicros ? "$" + microsToMoney(ag.cpmBidMicros) : "—"} |`, - `| Target CPA | ${ag.targetCpaMicros ? "$" + microsToMoney(ag.targetCpaMicros) : "—"} |`, - `| Target ROAS | ${ag.targetRoas || "—"} |`, - `| Effective Target CPA | ${ag.effectiveTargetCpaMicros ? "$" + microsToMoney(ag.effectiveTargetCpaMicros) : "—"} |`, - `| Effective Target ROAS | ${ag.effectiveTargetRoas || "—"} |`, - "", - `### Performance (Last 30 Days)`, - `| Metric | Value |`, - `|--------|-------|`, - `| Impressions | ${formatNumber(m.impressions || 0)} |`, - `| Clicks | ${formatNumber(m.clicks || 0)} |`, - `| CTR | ${formatPercent(m.ctr || 0)} |`, - `| Cost | $${microsToMoney(m.costMicros || 0)} |`, - `| Avg CPC | $${microsToMoney(m.averageCpc || 0)} |`, - `| Avg CPM | $${microsToMoney(m.averageCpm || 0)} |`, - `| Conversions | ${formatNumber(m.conversions || 0)} |`, - `| Conversion Value | $${(m.conversionsValue || 0).toFixed(2)} |`, - ]; - - return textResult(details.join("\n")); - }, - }, - - // ============================ - // create_ad_group - // ============================ - { - name: "create_ad_group", - description: - "Create a new ad group in a campaign. ⚠️ This will create a live ad group in your Google Ads account. " + - "Make sure the campaign exists and you intend to create this ad group before proceeding.", - category: "ad_groups", - annotations: { - title: "Create Ad Group", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: false, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - campaign_id: { - type: "string", - description: "Campaign ID to create the ad group in.", - }, - name: { - type: "string", - description: "Name for the new ad group.", - }, - type: { - type: "string", - description: "Ad group type.", - enum: [ - "SEARCH_STANDARD", - "DISPLAY_STANDARD", - "SHOPPING_PRODUCT_ADS", - "HOTEL_ADS", - "SHOPPING_SMART_ADS", - "VIDEO_BUMPER", - "VIDEO_TRUE_VIEW_IN_STREAM", - "VIDEO_TRUE_VIEW_IN_DISPLAY", - "VIDEO_RESPONSIVE", - "SMART_CAMPAIGN_ADS", - ], - }, - cpc_bid_amount: { - type: "number", - description: "Max CPC bid in dollars (e.g. 1.50 for $1.50). Will be converted to micros.", - }, - status: { - type: "string", - description: "Initial status. Default: ENABLED.", - enum: ["ENABLED", "PAUSED"], - }, - }, - required: ["campaign_id", "name"], - }, - _meta: { - labels: { - category: "ad_groups", - access: "delete", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - - const resource: Record = { - campaign: `customers/${cid}/campaigns/${args.campaign_id}`, - name: args.name, - status: args.status || "ENABLED", - }; - - if (args.type) { - resource.type = args.type; - } - - if (args.cpc_bid_amount != null) { - resource.cpcBidMicros = String(Math.round(args.cpc_bid_amount * 1_000_000)); - } - - const result = await client.mutate(cid, [ - { - entity: "adGroup", - operation: "create", - resource, - }, - ]); - - const createdName = - result?.mutateOperationResponses?.[0]?.adGroupResult?.resourceName || - result?.results?.[0]?.resourceName || - "unknown"; - - return textResult( - `✅ Ad group **${args.name}** created successfully.\n\n` + - `- Campaign: ${args.campaign_id}\n` + - `- Type: ${args.type || "default"}\n` + - `- Max CPC: ${args.cpc_bid_amount != null ? "$" + args.cpc_bid_amount.toFixed(2) : "—"}\n` + - `- Status: ${args.status || "ENABLED"}\n` + - `- Resource: \`${createdName}\`` - ); - }, - }, - - // ============================ - // update_ad_group - // ============================ - { - name: "update_ad_group", - description: - "Update an existing ad group's settings (name, CPC bid, target CPA, target ROAS, ad rotation). " + - "⚠️ This modifies a live ad group. Only provided fields will be updated.", - category: "ad_groups", - annotations: { - title: "Update Ad Group", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - ad_group_id: { - type: "string", - description: "The ad group ID to update.", - }, - name: { - type: "string", - description: "New ad group name.", - }, - cpc_bid_amount: { - type: "number", - description: "New max CPC bid in dollars (e.g. 2.00 for $2.00).", - }, - target_cpa_amount: { - type: "number", - description: "Target CPA in dollars.", - }, - target_roas: { - type: "number", - description: "Target ROAS (e.g. 4.0 for 400%).", - }, - ad_rotation_mode: { - type: "string", - description: "Ad rotation mode.", - enum: ["OPTIMIZE", "ROTATE_FOREVER"], - }, - }, - required: ["ad_group_id"], - }, - _meta: { - labels: { - category: "ad_groups", - access: "delete", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - const resourceName = `customers/${cid}/adGroups/${args.ad_group_id}`; - - const resource: Record = { resourceName }; - const updateFields: string[] = []; - - if (args.name != null) { - resource.name = args.name; - updateFields.push("name"); - } - if (args.cpc_bid_amount != null) { - resource.cpcBidMicros = String(Math.round(args.cpc_bid_amount * 1_000_000)); - updateFields.push("cpc_bid_micros"); - } - if (args.target_cpa_amount != null) { - resource.targetCpaMicros = String(Math.round(args.target_cpa_amount * 1_000_000)); - updateFields.push("target_cpa_micros"); - } - if (args.target_roas != null) { - resource.targetRoas = args.target_roas; - updateFields.push("target_roas"); - } - if (args.ad_rotation_mode) { - resource.adRotationMode = args.ad_rotation_mode; - updateFields.push("ad_rotation_mode"); - } - - if (!updateFields.length) { - return errorResult("No fields provided to update. Specify at least one field (name, cpc_bid_amount, target_cpa_amount, target_roas, ad_rotation_mode)."); - } - - await client.mutate(cid, [ - { - entity: "adGroup", - operation: "update", - resource, - updateMask: updateFields.join(","), - }, - ]); - - return textResult( - `✅ Ad group **${args.ad_group_id}** updated successfully.\n\nUpdated fields: ${updateFields.join(", ")}` - ); - }, - }, - - // ============================ - // pause_ad_group - // ============================ - { - name: "pause_ad_group", - description: - "Pause an ad group, stopping all ads within it from serving. ⚠️ This immediately pauses a live ad group.", - category: "ad_groups", - annotations: { - title: "Pause Ad Group", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - ad_group_id: { - type: "string", - description: "The ad group ID to pause.", - }, - }, - required: ["ad_group_id"], - }, - _meta: { - labels: { - category: "ad_groups", - access: "delete", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - const resourceName = `customers/${cid}/adGroups/${args.ad_group_id}`; - - await client.mutate(cid, [ - { - entity: "adGroup", - operation: "update", - resource: { resourceName, status: "PAUSED" }, - updateMask: "status", - }, - ]); - - return textResult(`⏸️ Ad group **${args.ad_group_id}** has been paused.`); - }, - }, - - // ============================ - // enable_ad_group - // ============================ - { - name: "enable_ad_group", - description: - "Enable a paused ad group, allowing its ads to begin serving again. ⚠️ This immediately enables a live ad group.", - category: "ad_groups", - annotations: { - title: "Enable Ad Group", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - ad_group_id: { - type: "string", - description: "The ad group ID to enable.", - }, - }, - required: ["ad_group_id"], - }, - _meta: { - labels: { - category: "ad_groups", - access: "delete", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - const resourceName = `customers/${cid}/adGroups/${args.ad_group_id}`; - - await client.mutate(cid, [ - { - entity: "adGroup", - operation: "update", - resource: { resourceName, status: "ENABLED" }, - updateMask: "status", - }, - ]); - - return textResult(`▶️ Ad group **${args.ad_group_id}** has been enabled.`); - }, - }, -]; - -export default tools; diff --git a/mcp-diagrams/google-ads-mcp/src/tools/ads.ts b/mcp-diagrams/google-ads-mcp/src/tools/ads.ts deleted file mode 100644 index 134fb76..0000000 --- a/mcp-diagrams/google-ads-mcp/src/tools/ads.ts +++ /dev/null @@ -1,628 +0,0 @@ -// ============================================ -// AD MANAGEMENT TOOLS -// ============================================ - -import type { ToolDefinition, GoogleAdsClient, ToolResult } from "../types.js"; -import { microsToMoney, formatPercent, formatNumber } from "../types.js"; - -// ---- Helpers ---- - -function resolveCustomerId(args: Record, client: GoogleAdsClient): string { - const cid = args.customer_id || client.getCustomerId(); - if (!cid) throw new Error("customer_id is required — pass it explicitly or set GOOGLE_ADS_CUSTOMER_ID."); - return cid.replace(/-/g, ""); -} - -function textResult(text: string): ToolResult { - return { content: [{ type: "text", text }] }; -} - -function errorResult(message: string): ToolResult { - return { content: [{ type: "text", text: `❌ ${message}` }], isError: true }; -} - -// ---- Tools ---- - -const tools: ToolDefinition[] = [ - // ============================ - // list_ads - // ============================ - { - name: "list_ads", - description: - "List ads in an ad group with type, status, approval status, policy info, and performance metrics.", - category: "ads", - annotations: { - title: "List Ads", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - ad_group_id: { - type: "string", - description: "Ad group ID to list ads for.", - }, - status_filter: { - type: "string", - description: "Filter by ad status.", - enum: ["ENABLED", "PAUSED", "REMOVED"], - }, - limit: { - type: "number", - description: "Maximum number of ads to return (default: 50).", - }, - }, - required: ["ad_group_id"], - }, - _meta: { - labels: { - category: "ads", - access: "read", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - const limit = Math.min(args.limit || 50, 200); - - let query = ` - SELECT - ad_group_ad.ad.id, - ad_group_ad.ad.type, - ad_group_ad.ad.name, - ad_group_ad.status, - ad_group_ad.ad.final_urls, - ad_group_ad.ad.responsive_search_ad.headlines, - ad_group_ad.ad.responsive_search_ad.descriptions, - ad_group_ad.policy_summary.approval_status, - ad_group_ad.policy_summary.review_status, - ad_group_ad.ad.resource_name, - ad_group.id, - ad_group.name, - metrics.impressions, - metrics.clicks, - metrics.cost_micros, - metrics.conversions, - metrics.ctr - FROM ad_group_ad - WHERE ad_group.id = ${args.ad_group_id} - AND segments.date DURING LAST_30_DAYS`; - - if (args.status_filter) { - query += `\n AND ad_group_ad.status = '${args.status_filter}'`; - } - - query += `\n ORDER BY metrics.impressions DESC\n LIMIT ${limit}`; - - const rows = await client.query(cid, query); - - if (!rows.length) { - return textResult("No ads found in this ad group."); - } - - const lines = rows.map((r: any) => { - const ad = r.adGroupAd?.ad || {}; - const status = r.adGroupAd?.status || "UNKNOWN"; - const policy = r.adGroupAd?.policySummary || {}; - const m = r.metrics || {}; - - const headlines = ad.responsiveSearchAd?.headlines?.map((h: any) => h.text).slice(0, 3) || []; - const headlinePreview = headlines.length ? headlines.join(" | ") : "—"; - const urls = ad.finalUrls?.join(", ") || "—"; - - return [ - `**Ad ${ad.id}** (${ad.type || "UNKNOWN"})`, - ` Status: ${status} | Approval: ${policy.approvalStatus || "—"} | Review: ${policy.reviewStatus || "—"}`, - ` Headlines: ${headlinePreview}${headlines.length < (ad.responsiveSearchAd?.headlines?.length || 0) ? " ..." : ""}`, - ` URLs: ${urls}`, - ` Impressions: ${formatNumber(m.impressions || 0)} | Clicks: ${formatNumber(m.clicks || 0)} | CTR: ${formatPercent(m.ctr || 0)} | Cost: $${microsToMoney(m.costMicros || 0)}`, - ].join("\n"); - }); - - return textResult( - `## Ads in Ad Group ${args.ad_group_id}\n\n${lines.join("\n\n")}\n\n_${rows.length} ad(s) returned._` - ); - }, - }, - - // ============================ - // get_ad - // ============================ - { - name: "get_ad", - description: - "Get full details for a specific ad including all headlines, descriptions, URLs, type, and policy status.", - category: "ads", - annotations: { - title: "Get Ad Details", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - ad_group_id: { - type: "string", - description: "Ad group ID the ad belongs to.", - }, - ad_id: { - type: "string", - description: "The ad ID to retrieve.", - }, - }, - required: ["ad_group_id", "ad_id"], - }, - _meta: { - labels: { - category: "ads", - access: "read", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - - const query = ` - SELECT - ad_group_ad.ad.id, - ad_group_ad.ad.type, - ad_group_ad.ad.name, - ad_group_ad.status, - ad_group_ad.ad.final_urls, - ad_group_ad.ad.final_mobile_urls, - ad_group_ad.ad.tracking_url_template, - ad_group_ad.ad.display_url, - ad_group_ad.ad.responsive_search_ad.headlines, - ad_group_ad.ad.responsive_search_ad.descriptions, - ad_group_ad.ad.responsive_search_ad.path1, - ad_group_ad.ad.responsive_search_ad.path2, - ad_group_ad.policy_summary.approval_status, - ad_group_ad.policy_summary.review_status, - ad_group_ad.policy_summary.policy_topic_entries, - ad_group_ad.ad.resource_name, - ad_group.id, - ad_group.name, - campaign.id, - campaign.name, - metrics.impressions, - metrics.clicks, - metrics.cost_micros, - metrics.conversions, - metrics.ctr, - metrics.average_cpc - FROM ad_group_ad - WHERE ad_group.id = ${args.ad_group_id} - AND ad_group_ad.ad.id = ${args.ad_id} - AND segments.date DURING LAST_30_DAYS - LIMIT 1`; - - const rows = await client.query(cid, query); - - if (!rows.length) { - return errorResult(`Ad ${args.ad_id} not found in ad group ${args.ad_group_id}.`); - } - - const r = rows[0]; - const ad = r.adGroupAd?.ad || {}; - const status = r.adGroupAd?.status || "UNKNOWN"; - const policy = r.adGroupAd?.policySummary || {}; - const camp = r.campaign || {}; - const ag = r.adGroup || {}; - const m = r.metrics || {}; - - const sections: string[] = [ - `## Ad Details: ${ad.id}`, - "", - `| Field | Value |`, - `|-------|-------|`, - `| Ad ID | ${ad.id} |`, - `| Type | ${ad.type || "—"} |`, - `| Status | ${status} |`, - `| Campaign | ${camp.name || "—"} (ID: ${camp.id || "—"}) |`, - `| Ad Group | ${ag.name || "—"} (ID: ${ag.id || "—"}) |`, - `| Approval | ${policy.approvalStatus || "—"} |`, - `| Review Status | ${policy.reviewStatus || "—"} |`, - `| Final URLs | ${ad.finalUrls?.join(", ") || "—"} |`, - `| Mobile URLs | ${ad.finalMobileUrls?.join(", ") || "—"} |`, - `| Display URL | ${ad.displayUrl || "—"} |`, - `| Tracking Template | ${ad.trackingUrlTemplate || "—"} |`, - ]; - - // RSA details - const rsa = ad.responsiveSearchAd; - if (rsa) { - sections.push( - "", - `### Responsive Search Ad`, - `**Path:** ${rsa.path1 || "—"}/${rsa.path2 || "—"}`, - "", - `**Headlines (${rsa.headlines?.length || 0}):**` - ); - (rsa.headlines || []).forEach((h: any, i: number) => { - const pin = h.pinnedField ? ` 📌 ${h.pinnedField}` : ""; - sections.push(` ${i + 1}. ${h.text}${pin}`); - }); - - sections.push("", `**Descriptions (${rsa.descriptions?.length || 0}):**`); - (rsa.descriptions || []).forEach((d: any, i: number) => { - const pin = d.pinnedField ? ` 📌 ${d.pinnedField}` : ""; - sections.push(` ${i + 1}. ${d.text}${pin}`); - }); - } - - // Policy topics - if (policy.policyTopicEntries?.length) { - sections.push("", `### Policy Topics`); - policy.policyTopicEntries.forEach((entry: any) => { - sections.push(`- **${entry.type || "—"}**: ${entry.topic || "—"}`); - }); - } - - // Performance - sections.push( - "", - `### Performance (Last 30 Days)`, - `| Metric | Value |`, - `|--------|-------|`, - `| Impressions | ${formatNumber(m.impressions || 0)} |`, - `| Clicks | ${formatNumber(m.clicks || 0)} |`, - `| CTR | ${formatPercent(m.ctr || 0)} |`, - `| Cost | $${microsToMoney(m.costMicros || 0)} |`, - `| Avg CPC | $${microsToMoney(m.averageCpc || 0)} |`, - `| Conversions | ${formatNumber(m.conversions || 0)} |` - ); - - return textResult(sections.join("\n")); - }, - }, - - // ============================ - // create_responsive_search_ad - // ============================ - { - name: "create_responsive_search_ad", - description: - "Create a Responsive Search Ad (RSA) with up to 15 headlines and 4 descriptions. " + - "⚠️ This creates a live ad that may start serving immediately if the ad group is enabled. " + - "Google requires at least 3 headlines and 2 descriptions.", - category: "ads", - annotations: { - title: "Create Responsive Search Ad", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: false, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - ad_group_id: { - type: "string", - description: "Ad group ID to create the ad in.", - }, - headlines: { - type: "array", - items: { type: "string" }, - description: "Array of headline strings (3-15). Each headline max 30 characters.", - }, - descriptions: { - type: "array", - items: { type: "string" }, - description: "Array of description strings (2-4). Each description max 90 characters.", - }, - final_urls: { - type: "array", - items: { type: "string" }, - description: "Landing page URL(s). At least one required.", - }, - path1: { - type: "string", - description: "First URL path text (max 15 chars, displayed after domain in ad).", - }, - path2: { - type: "string", - description: "Second URL path text (max 15 chars, displayed after path1).", - }, - status: { - type: "string", - description: "Initial ad status. Default: ENABLED.", - enum: ["ENABLED", "PAUSED"], - }, - }, - required: ["ad_group_id", "headlines", "descriptions", "final_urls"], - }, - _meta: { - labels: { - category: "ads", - access: "delete", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - - // Validation - if (!args.headlines?.length || args.headlines.length < 3) { - return errorResult("At least 3 headlines are required for a Responsive Search Ad."); - } - if (args.headlines.length > 15) { - return errorResult("Maximum 15 headlines allowed."); - } - if (!args.descriptions?.length || args.descriptions.length < 2) { - return errorResult("At least 2 descriptions are required for a Responsive Search Ad."); - } - if (args.descriptions.length > 4) { - return errorResult("Maximum 4 descriptions allowed."); - } - if (!args.final_urls?.length) { - return errorResult("At least one final URL is required."); - } - - // Check headline length limits - const longHeadlines = args.headlines.filter((h: string) => h.length > 30); - if (longHeadlines.length) { - return errorResult(`Headlines must be 30 characters or fewer. Too long: "${longHeadlines[0]}" (${longHeadlines[0].length} chars)`); - } - - // Check description length limits - const longDescs = args.descriptions.filter((d: string) => d.length > 90); - if (longDescs.length) { - return errorResult(`Descriptions must be 90 characters or fewer. Too long: "${longDescs[0]}" (${longDescs[0].length} chars)`); - } - - const responsiveSearchAd: Record = { - headlines: args.headlines.map((text: string) => ({ text })), - descriptions: args.descriptions.map((text: string) => ({ text })), - }; - - if (args.path1) responsiveSearchAd.path1 = args.path1; - if (args.path2) responsiveSearchAd.path2 = args.path2; - - const resource: Record = { - adGroup: `customers/${cid}/adGroups/${args.ad_group_id}`, - status: args.status || "ENABLED", - ad: { - responsiveSearchAd, - finalUrls: args.final_urls, - }, - }; - - const result = await client.mutate(cid, [ - { - entity: "adGroupAd", - operation: "create", - resource, - }, - ]); - - const createdName = - result?.mutateOperationResponses?.[0]?.adGroupAdResult?.resourceName || - result?.results?.[0]?.resourceName || - "unknown"; - - return textResult( - `✅ Responsive Search Ad created successfully.\n\n` + - `- Ad Group: ${args.ad_group_id}\n` + - `- Headlines: ${args.headlines.length}\n` + - `- Descriptions: ${args.descriptions.length}\n` + - `- Final URL: ${args.final_urls[0]}\n` + - `- Path: ${args.path1 || "—"}/${args.path2 || "—"}\n` + - `- Status: ${args.status || "ENABLED"}\n` + - `- Resource: \`${createdName}\`` - ); - }, - }, - - // ============================ - // update_ad - // ============================ - { - name: "update_ad", - description: - "Update an existing ad's copy (headlines, descriptions, URLs, paths). " + - "⚠️ This modifies a live ad — the ad will go through re-review after changes. " + - "Note: Google Ads requires replacing the entire RSA asset set, not individual headlines.", - category: "ads", - annotations: { - title: "Update Ad", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - ad_group_id: { - type: "string", - description: "Ad group ID the ad belongs to.", - }, - ad_id: { - type: "string", - description: "The ad ID to update.", - }, - headlines: { - type: "array", - items: { type: "string" }, - description: "Full replacement set of headlines (3-15). Replaces ALL existing headlines.", - }, - descriptions: { - type: "array", - items: { type: "string" }, - description: "Full replacement set of descriptions (2-4). Replaces ALL existing descriptions.", - }, - final_urls: { - type: "array", - items: { type: "string" }, - description: "New final URL(s).", - }, - path1: { - type: "string", - description: "New first URL path text.", - }, - path2: { - type: "string", - description: "New second URL path text.", - }, - }, - required: ["ad_group_id", "ad_id"], - }, - _meta: { - labels: { - category: "ads", - access: "delete", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - const resourceName = `customers/${cid}/adGroupAds/${args.ad_group_id}~${args.ad_id}`; - - const adUpdate: Record = { - resourceName: `customers/${cid}/ads/${args.ad_id}`, - }; - const updateFields: string[] = []; - - if (args.headlines?.length) { - if (args.headlines.length < 3 || args.headlines.length > 15) { - return errorResult("Headlines must be between 3 and 15."); - } - adUpdate.responsiveSearchAd = adUpdate.responsiveSearchAd || {}; - adUpdate.responsiveSearchAd.headlines = args.headlines.map((text: string) => ({ text })); - updateFields.push("responsive_search_ad.headlines"); - } - - if (args.descriptions?.length) { - if (args.descriptions.length < 2 || args.descriptions.length > 4) { - return errorResult("Descriptions must be between 2 and 4."); - } - adUpdate.responsiveSearchAd = adUpdate.responsiveSearchAd || {}; - adUpdate.responsiveSearchAd.descriptions = args.descriptions.map((text: string) => ({ text })); - updateFields.push("responsive_search_ad.descriptions"); - } - - if (args.final_urls?.length) { - adUpdate.finalUrls = args.final_urls; - updateFields.push("final_urls"); - } - - if (args.path1 !== undefined) { - adUpdate.responsiveSearchAd = adUpdate.responsiveSearchAd || {}; - adUpdate.responsiveSearchAd.path1 = args.path1; - updateFields.push("responsive_search_ad.path1"); - } - - if (args.path2 !== undefined) { - adUpdate.responsiveSearchAd = adUpdate.responsiveSearchAd || {}; - adUpdate.responsiveSearchAd.path2 = args.path2; - updateFields.push("responsive_search_ad.path2"); - } - - if (!updateFields.length) { - return errorResult("No fields provided to update. Specify headlines, descriptions, final_urls, path1, or path2."); - } - - // Ad updates use the ad resource directly via adGroupAd entity - const resource: Record = { - resourceName, - ad: adUpdate, - }; - - await client.mutate(cid, [ - { - entity: "adGroupAd", - operation: "update", - resource, - updateMask: updateFields.map((f) => `ad.${f}`).join(","), - }, - ]); - - return textResult( - `✅ Ad **${args.ad_id}** updated successfully.\n\n` + - `Updated fields: ${updateFields.join(", ")}\n\n` + - `_Note: The ad may go through re-review after changes._` - ); - }, - }, - - // ============================ - // pause_ad - // ============================ - { - name: "pause_ad", - description: - "Pause a specific ad, stopping it from serving. ⚠️ This immediately pauses a live ad.", - category: "ads", - annotations: { - title: "Pause Ad", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - ad_group_id: { - type: "string", - description: "Ad group ID the ad belongs to.", - }, - ad_id: { - type: "string", - description: "The ad ID to pause.", - }, - }, - required: ["ad_group_id", "ad_id"], - }, - _meta: { - labels: { - category: "ads", - access: "delete", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - const resourceName = `customers/${cid}/adGroupAds/${args.ad_group_id}~${args.ad_id}`; - - await client.mutate(cid, [ - { - entity: "adGroupAd", - operation: "update", - resource: { resourceName, status: "PAUSED" }, - updateMask: "status", - }, - ]); - - return textResult(`⏸️ Ad **${args.ad_id}** in ad group ${args.ad_group_id} has been paused.`); - }, - }, -]; - -export default tools; diff --git a/mcp-diagrams/google-ads-mcp/src/tools/advanced.ts b/mcp-diagrams/google-ads-mcp/src/tools/advanced.ts deleted file mode 100644 index 2ff1fcf..0000000 --- a/mcp-diagrams/google-ads-mcp/src/tools/advanced.ts +++ /dev/null @@ -1,743 +0,0 @@ -// ============================================ -// ADVANCED / UTILITY TOOLS -// ============================================ - -import type { ToolDefinition, GoogleAdsClient, ToolResult } from "../types.js"; -import { microsToMoney, formatPercent, formatNumber } from "../types.js"; - -// ============================================ -// HELPERS -// ============================================ - -/** Mutation keywords that should never appear in a read-only GAQL query */ -const MUTATION_KEYWORDS = /\b(INSERT|UPDATE|DELETE|CREATE|REMOVE)\b/i; - -/** Flatten a nested API response row into dot-notation key-value pairs */ -function flattenRow( - obj: Record, - prefix = "" -): Record { - const flat: Record = {}; - for (const [key, value] of Object.entries(obj)) { - const path = prefix ? `${prefix}.${key}` : key; - if (value && typeof value === "object" && !Array.isArray(value)) { - Object.assign(flat, flattenRow(value, path)); - } else if (Array.isArray(value)) { - flat[path] = value.map((v) => (typeof v === "object" ? JSON.stringify(v) : String(v))).join("; "); - } else { - flat[path] = value == null ? "" : String(value); - } - } - return flat; -} - -/** Convert a value that looks like micros to a formatted dollar string */ -function autoFormatValue(key: string, value: string): string { - const lk = key.toLowerCase(); - if (lk.includes("micros") && /^\d+$/.test(value)) { - return "$" + microsToMoney(value); - } - if ((lk.includes("ctr") || lk.includes("rate")) && !isNaN(Number(value))) { - return formatPercent(Number(value)); - } - return value; -} - -// ============================================ -// ENTITY DEFINITIONS FOR export_report -// ============================================ - -interface EntityConfig { - resource: string; - fields: string[]; - metricsFields: string[]; - filterField?: string; -} - -const ENTITY_CONFIGS: Record = { - campaigns: { - resource: "campaign", - fields: [ - "campaign.id", - "campaign.name", - "campaign.status", - "campaign.advertising_channel_type", - "campaign.bidding_strategy_type", - ], - metricsFields: [ - "metrics.impressions", - "metrics.clicks", - "metrics.cost_micros", - "metrics.conversions", - "metrics.conversions_value", - "metrics.ctr", - "metrics.average_cpc", - ], - filterField: "campaign.status", - }, - ad_groups: { - resource: "ad_group", - fields: [ - "ad_group.id", - "ad_group.name", - "ad_group.status", - "ad_group.type", - "campaign.id", - "campaign.name", - ], - metricsFields: [ - "metrics.impressions", - "metrics.clicks", - "metrics.cost_micros", - "metrics.conversions", - "metrics.ctr", - "metrics.average_cpc", - ], - filterField: "ad_group.status", - }, - keywords: { - resource: "keyword_view", - fields: [ - "ad_group_criterion.criterion_id", - "ad_group_criterion.keyword.text", - "ad_group_criterion.keyword.match_type", - "ad_group_criterion.status", - "ad_group_criterion.quality_info.quality_score", - "ad_group.name", - "campaign.name", - ], - metricsFields: [ - "metrics.impressions", - "metrics.clicks", - "metrics.cost_micros", - "metrics.conversions", - "metrics.ctr", - "metrics.average_cpc", - ], - filterField: "ad_group_criterion.status", - }, - ads: { - resource: "ad_group_ad", - fields: [ - "ad_group_ad.ad.id", - "ad_group_ad.ad.type", - "ad_group_ad.ad.final_urls", - "ad_group_ad.status", - "ad_group.name", - "campaign.name", - ], - metricsFields: [ - "metrics.impressions", - "metrics.clicks", - "metrics.cost_micros", - "metrics.conversions", - "metrics.ctr", - ], - filterField: "ad_group_ad.status", - }, -}; - -// ============================================ -// TOOL HANDLERS -// ============================================ - -// ---- run_gaql_query ---- - -async function runGaqlQuery( - args: Record, - client: GoogleAdsClient -): Promise { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) { - return { - content: [{ type: "text", text: "Error: customer_id is required." }], - isError: true, - }; - } - - const query: string = (args.query || "").trim(); - if (!query) { - return { - content: [{ type: "text", text: "Error: query is required." }], - isError: true, - }; - } - - // Safety: block mutation keywords - if (MUTATION_KEYWORDS.test(query)) { - return { - content: [ - { - type: "text", - text: "Error: This tool only supports read-only GAQL queries. Mutation keywords (INSERT, UPDATE, DELETE, CREATE, REMOVE) are not allowed. Use the appropriate management tools for mutations.", - }, - ], - isError: true, - }; - } - - // Validate it starts with SELECT - if (!/^\s*SELECT\b/i.test(query)) { - return { - content: [ - { - type: "text", - text: "Error: Query must start with SELECT. This tool only supports GAQL SELECT queries.", - }, - ], - isError: true, - }; - } - - const rows = await client.query(customerId, query); - - if (!rows.length) { - return { - content: [{ type: "text", text: `Query returned 0 results.\n\n\`\`\`sql\n${query}\n\`\`\`` }], - }; - } - - // Flatten rows and auto-format values - const flatRows = rows.map((row) => flattenRow(row)); - const allKeys = [...new Set(flatRows.flatMap((r) => Object.keys(r)))]; - - // Format as a readable table (markdown) - const formatted = flatRows.map((row, i) => { - const entries = allKeys - .filter((k) => row[k] !== undefined && row[k] !== "") - .map((k) => ` ${k}: ${autoFormatValue(k, row[k])}`) - .join("\n"); - return `**Row ${i + 1}:**\n${entries}`; - }); - - // Truncate if too many rows - const MAX_DISPLAY = 100; - const truncated = rows.length > MAX_DISPLAY; - const displayRows = formatted.slice(0, MAX_DISPLAY); - - return { - content: [ - { - type: "text", - text: [ - `## GAQL Query Results`, - `**Rows:** ${rows.length}${truncated ? ` (showing first ${MAX_DISPLAY})` : ""}`, - `**Fields:** ${allKeys.join(", ")}`, - ``, - `\`\`\`sql\n${query}\n\`\`\``, - ``, - displayRows.join("\n\n"), - truncated ? `\n\n> ⚠️ Results truncated. ${rows.length - MAX_DISPLAY} additional rows not shown.` : "", - ] - .filter(Boolean) - .join("\n"), - }, - ], - }; -} - -// ---- export_report ---- - -async function exportReport( - args: Record, - client: GoogleAdsClient -): Promise { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) { - return { - content: [{ type: "text", text: "Error: customer_id is required." }], - isError: true, - }; - } - - const entity = args.entity as string; - const format = (args.format as string) || "csv"; - const dateRange = args.date_range || "LAST_30_DAYS"; - - const config = ENTITY_CONFIGS[entity]; - if (!config) { - return { - content: [ - { - type: "text", - text: `Error: Unknown entity "${entity}". Valid options: ${Object.keys(ENTITY_CONFIGS).join(", ")}`, - }, - ], - isError: true, - }; - } - - // Build GAQL dynamically - const allFields = [...config.fields, ...config.metricsFields]; - const conditions: string[] = [`segments.date DURING ${dateRange}`]; - - // Apply optional status filter - if (args.status && config.filterField) { - conditions.push(`${config.filterField} = '${args.status}'`); - } - - // Apply minimum impressions filter for cleaner reports - if (args.min_impressions) { - conditions.push(`metrics.impressions >= ${args.min_impressions}`); - } - - // Apply campaign filter for ad_groups, keywords, ads - if (args.campaign_id && entity !== "campaigns") { - conditions.push(`campaign.id = ${args.campaign_id}`); - } - - const query = ` - SELECT ${allFields.join(", ")} - FROM ${config.resource} - WHERE ${conditions.join(" AND ")} - ORDER BY metrics.cost_micros DESC - LIMIT ${args.limit || 1000} - `; - - const rows = await client.query(customerId, query); - - if (!rows.length) { - return { - content: [ - { - type: "text", - text: `No data found for ${entity} in ${dateRange}.`, - }, - ], - }; - } - - // Flatten all rows - const flatRows = rows.map((row) => flattenRow(row)); - const headers = [...new Set(flatRows.flatMap((r) => Object.keys(r)))]; - - if (format === "csv") { - // Escape CSV values - const escapeCSV = (val: string): string => { - if (val.includes(",") || val.includes('"') || val.includes("\n")) { - return `"${val.replace(/"/g, '""')}"`; - } - return val; - }; - - const csvLines = [ - headers.map(escapeCSV).join(","), - ...flatRows.map((row) => - headers.map((h) => escapeCSV(autoFormatValue(h, row[h] || ""))).join(",") - ), - ]; - - return { - content: [ - { - type: "text", - text: [ - `## ${entity.replace("_", " ").toUpperCase()} Report (CSV)`, - `📅 ${dateRange} | ${rows.length} rows`, - ``, - "```csv", - csvLines.join("\n"), - "```", - ].join("\n"), - }, - ], - }; - } else { - // JSON format - const jsonRows = flatRows.map((row) => { - const obj: Record = {}; - for (const h of headers) { - obj[h] = autoFormatValue(h, row[h] || ""); - } - return obj; - }); - - return { - content: [ - { - type: "text", - text: [ - `## ${entity.replace("_", " ").toUpperCase()} Report (JSON)`, - `📅 ${dateRange} | ${rows.length} rows`, - ``, - "```json", - JSON.stringify(jsonRows, null, 2), - "```", - ].join("\n"), - }, - ], - }; - } -} - -// ---- get_change_history ---- - -async function getChangeHistory( - args: Record, - client: GoogleAdsClient -): Promise { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) { - return { - content: [{ type: "text", text: "Error: customer_id is required." }], - isError: true, - }; - } - - const days = args.days || 14; - const resourceTypeFilter = args.resource_type - ? `AND change_event.change_resource_type = '${args.resource_type}'` - : ""; - - const query = ` - SELECT - change_event.change_date_time, - change_event.change_resource_type, - change_event.change_resource_name, - change_event.client_type, - change_event.user_email, - change_event.old_resource, - change_event.new_resource - FROM change_event - WHERE change_event.change_date_time DURING LAST_${days}_DAYS - ${resourceTypeFilter} - ORDER BY change_event.change_date_time DESC - LIMIT ${args.limit || 50} - `; - - const rows = await client.query(customerId, query); - - if (!rows.length) { - return { - content: [ - { - type: "text", - text: `No changes found in the last ${days} days.`, - }, - ], - }; - } - - const lines = rows.map((row) => { - const ce = row.changeEvent; - const timestamp = ce.changeDateTime || "Unknown time"; - const resourceType = ce.changeResourceType || "UNKNOWN"; - const resourceName = ce.changeResourceName || ""; - const clientType = ce.clientType || ""; - const email = ce.userEmail || "system"; - - // Build a concise diff summary - let diffSummary = ""; - if (ce.oldResource && ce.newResource) { - const oldFlat = flattenRow(ce.oldResource); - const newFlat = flattenRow(ce.newResource); - const changes: string[] = []; - const allDiffKeys = new Set([ - ...Object.keys(oldFlat), - ...Object.keys(newFlat), - ]); - for (const key of allDiffKeys) { - if (oldFlat[key] !== newFlat[key]) { - changes.push( - ` ${key}: ${oldFlat[key] || "(empty)"} → ${newFlat[key] || "(empty)"}` - ); - } - } - if (changes.length > 0) { - diffSummary = `\n Changes:\n${changes.slice(0, 5).join("\n")}`; - if (changes.length > 5) { - diffSummary += `\n ... and ${changes.length - 5} more fields`; - } - } - } - - return [ - ` • **${timestamp}** — ${resourceType}`, - ` Resource: \`${resourceName}\``, - ` By: ${email} (${clientType})${diffSummary}`, - ].join("\n"); - }); - - return { - content: [ - { - type: "text", - text: `## Account Change History (Last ${days} Days)\n**Changes found:** ${rows.length}\n\n${lines.join("\n\n")}`, - }, - ], - }; -} - -// ---- get_labels ---- - -async function getLabels( - args: Record, - client: GoogleAdsClient -): Promise { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) { - return { - content: [{ type: "text", text: "Error: customer_id is required." }], - isError: true, - }; - } - - const query = ` - SELECT - label.id, - label.name, - label.status - FROM label - ORDER BY label.name - `; - - const rows = await client.query(customerId, query); - - if (!rows.length) { - return { - content: [ - { - type: "text", - text: "No labels found in this account. Labels help organize campaigns, ad groups, ads, and keywords into custom groups.", - }, - ], - }; - } - - const lines = rows.map((row) => { - const l = row.label; - const statusIcon = l.status === "ENABLED" ? "🟢" : "⚪"; - return ` ${statusIcon} **${l.name}** (ID: ${l.id}) — ${l.status}`; - }); - - return { - content: [ - { - type: "text", - text: `## Labels (${rows.length})\n\n${lines.join("\n")}`, - }, - ], - }; -} - -// ============================================ -// TOOL DEFINITIONS -// ============================================ - -const tools: ToolDefinition[] = [ - { - name: "run_gaql_query", - description: - "Execute a raw GAQL (Google Ads Query Language) query. Power-user escape hatch for any data " + - "accessible via the Google Ads API. Only SELECT queries are allowed (no mutations). " + - "Refer to https://developers.google.com/google-ads/api/fields/v21/overview for available fields.", - category: "advanced", - annotations: { - title: "Run GAQL Query", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: true, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: - "Google Ads customer ID (without dashes). Defaults to configured account.", - }, - query: { - type: "string", - description: - 'Full GAQL SELECT query to execute. Example: "SELECT campaign.name, metrics.clicks FROM campaign WHERE segments.date DURING LAST_7_DAYS"', - }, - }, - required: ["query"], - }, - _meta: { - labels: { - category: "advanced", - access: "read", - complexity: "complex" - } - }, - handler: runGaqlQuery, - }, - { - name: "export_report", - description: - "Export campaign, ad group, keyword, or ad performance data in CSV or JSON format. " + - "Builds the appropriate GAQL query automatically based on entity type. " + - "Useful for data analysis, spreadsheet import, or sharing with stakeholders.", - category: "advanced", - annotations: { - title: "Export Report", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: - "Google Ads customer ID (without dashes). Defaults to configured account.", - }, - entity: { - type: "string", - description: "Type of entity to export.", - enum: ["campaigns", "ad_groups", "keywords", "ads"], - }, - format: { - type: "string", - description: "Output format.", - enum: ["csv", "json"], - default: "csv", - }, - date_range: { - type: "string", - description: "GAQL date range predicate.", - enum: [ - "TODAY", - "YESTERDAY", - "LAST_7_DAYS", - "LAST_14_DAYS", - "LAST_30_DAYS", - "LAST_90_DAYS", - "THIS_MONTH", - "LAST_MONTH", - "THIS_QUARTER", - "LAST_QUARTER", - ], - default: "LAST_30_DAYS", - }, - status: { - type: "string", - description: - "Filter by status (ENABLED, PAUSED, REMOVED). Omit for all statuses.", - enum: ["ENABLED", "PAUSED", "REMOVED"], - }, - campaign_id: { - type: "string", - description: - "Filter to a specific campaign (for ad_groups, keywords, ads entity types).", - }, - min_impressions: { - type: "number", - description: - "Minimum impressions threshold to filter out low-volume entries.", - }, - limit: { - type: "number", - description: "Maximum rows to return. Default: 1000.", - default: 1000, - }, - }, - required: ["entity"], - }, - _meta: { - labels: { - category: "advanced", - access: "read", - complexity: "complex" - } - }, - handler: exportReport, - }, - { - name: "get_change_history", - description: - "Get recent account changes including who made them, what was changed, and before/after values. " + - "Useful for auditing, debugging unexpected changes, and tracking team activity.", - category: "advanced", - annotations: { - title: "Get Change History", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: - "Google Ads customer ID (without dashes). Defaults to configured account.", - }, - days: { - type: "number", - description: - "Number of days of history to retrieve (1-30). Default: 14.", - default: 14, - minimum: 1, - maximum: 30, - }, - resource_type: { - type: "string", - description: "Filter to a specific resource type.", - enum: [ - "AD", - "AD_GROUP", - "AD_GROUP_AD", - "AD_GROUP_CRITERION", - "AD_GROUP_BID_MODIFIER", - "CAMPAIGN", - "CAMPAIGN_BUDGET", - "CAMPAIGN_CRITERION", - ], - }, - limit: { - type: "number", - description: "Maximum number of changes to return. Default: 50.", - default: 50, - }, - }, - }, - _meta: { - labels: { - category: "advanced", - access: "read", - complexity: "simple" - } - }, - handler: getChangeHistory, - }, - { - name: "get_labels", - description: - "List all labels in the Google Ads account. Labels are used to organize and tag campaigns, " + - "ad groups, ads, and keywords into custom categories for filtering and reporting.", - category: "advanced", - annotations: { - title: "Get Labels", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: - "Google Ads customer ID (without dashes). Defaults to configured account.", - }, - }, - }, - _meta: { - labels: { - category: "advanced", - access: "read", - complexity: "simple" - } - }, - handler: getLabels, - }, -]; - -export default tools; diff --git a/mcp-diagrams/google-ads-mcp/src/tools/bidding.ts b/mcp-diagrams/google-ads-mcp/src/tools/bidding.ts deleted file mode 100644 index 2833520..0000000 --- a/mcp-diagrams/google-ads-mcp/src/tools/bidding.ts +++ /dev/null @@ -1,536 +0,0 @@ -// ============================================ -// BIDDING & BUDGET TOOLS -// ============================================ - -import type { ToolDefinition, GoogleAdsClient, ToolResult } from "../types.js"; -import { microsToMoney, formatPercent, formatNumber } from "../types.js"; -import type { GoogleAdsRestClient } from "../client.js"; - -// ---- Helpers ---- - -function resolveCustomerId(args: Record, client: GoogleAdsClient): string { - const cid = args.customer_id || client.getCustomerId(); - if (!cid) throw new Error("customer_id is required — pass it explicitly or set GOOGLE_ADS_CUSTOMER_ID."); - return cid.replace(/-/g, ""); -} - -function textResult(text: string): ToolResult { - return { content: [{ type: "text", text }] }; -} - -function errorResult(message: string): ToolResult { - return { content: [{ type: "text", text: `❌ ${message}` }], isError: true }; -} - -// ---- Tools ---- - -const tools: ToolDefinition[] = [ - // ============================ - // list_bidding_strategies - // ============================ - { - name: "list_bidding_strategies", - description: - "List portfolio bidding strategies in the account, including type, target metrics, campaign count, and performance status.", - category: "bidding", - annotations: { - title: "List Bidding Strategies", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - limit: { - type: "number", - description: "Maximum strategies to return (default: 50).", - }, - }, - }, - _meta: { - labels: { - category: "bidding", - access: "read", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - const limit = Math.min(args.limit || 50, 200); - - const query = ` - SELECT - bidding_strategy.id, - bidding_strategy.name, - bidding_strategy.type, - bidding_strategy.status, - bidding_strategy.campaign_count, - bidding_strategy.non_removed_campaign_count, - bidding_strategy.resource_name, - bidding_strategy.target_cpa.target_cpa_micros, - bidding_strategy.target_roas.target_roas, - bidding_strategy.maximize_conversions.target_cpa_micros, - bidding_strategy.maximize_conversion_value.target_roas, - bidding_strategy.target_spend.cpc_bid_ceiling_micros, - metrics.impressions, - metrics.clicks, - metrics.cost_micros, - metrics.conversions, - metrics.conversions_value - FROM bidding_strategy - WHERE segments.date DURING LAST_30_DAYS - ORDER BY metrics.cost_micros DESC - LIMIT ${limit}`; - - const rows = await client.query(cid, query); - - if (!rows.length) { - return textResult("No portfolio bidding strategies found in this account."); - } - - const lines = rows.map((r: any) => { - const bs = r.biddingStrategy || {}; - const m = r.metrics || {}; - - // Extract target value based on strategy type - let targetInfo = ""; - if (bs.targetCpa?.targetCpaMicros) { - targetInfo = `Target CPA: $${microsToMoney(bs.targetCpa.targetCpaMicros)}`; - } else if (bs.targetRoas?.targetRoas) { - targetInfo = `Target ROAS: ${(bs.targetRoas.targetRoas * 100).toFixed(0)}%`; - } else if (bs.maximizeConversions?.targetCpaMicros) { - targetInfo = `Max Conv Target CPA: $${microsToMoney(bs.maximizeConversions.targetCpaMicros)}`; - } else if (bs.maximizeConversionValue?.targetRoas) { - targetInfo = `Max Value Target ROAS: ${(bs.maximizeConversionValue.targetRoas * 100).toFixed(0)}%`; - } else if (bs.targetSpend?.cpcBidCeilingMicros) { - targetInfo = `CPC Ceiling: $${microsToMoney(bs.targetSpend.cpcBidCeilingMicros)}`; - } - - return [ - `**${bs.name || "Unnamed"}** (ID: ${bs.id})`, - ` Type: ${bs.type || "—"} | Status: ${bs.status || "—"} | Campaigns: ${bs.nonRemovedCampaignCount || bs.campaignCount || 0}`, - targetInfo ? ` ${targetInfo}` : null, - ` Cost: $${microsToMoney(m.costMicros || 0)} | Clicks: ${formatNumber(m.clicks || 0)} | Conv: ${formatNumber(m.conversions || 0)} | Conv Value: $${(m.conversionsValue || 0).toFixed(2)}`, - ] - .filter(Boolean) - .join("\n"); - }); - - return textResult( - `## Portfolio Bidding Strategies (Last 30 Days)\n\n${lines.join("\n\n")}\n\n_${rows.length} strateg${rows.length === 1 ? "y" : "ies"} returned._` - ); - }, - }, - - // ============================ - // get_bid_recommendations - // ============================ - { - name: "get_bid_recommendations", - description: - "Get Google's automated bid recommendations for keywords, including suggested bids and estimated impact on clicks and impressions.", - category: "bidding", - annotations: { - title: "Get Bid Recommendations", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - campaign_id: { - type: "string", - description: "Limit recommendations to a specific campaign.", - }, - limit: { - type: "number", - description: "Maximum recommendations to return (default: 25).", - }, - }, - }, - _meta: { - labels: { - category: "bidding", - access: "read", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - const limit = Math.min(args.limit || 25, 100); - - // Fetch bid-related recommendations - let query = ` - SELECT - recommendation.type, - recommendation.resource_name, - recommendation.campaign, - recommendation.ad_group, - recommendation.impact.base_metrics.impressions, - recommendation.impact.base_metrics.clicks, - recommendation.impact.base_metrics.cost_micros, - recommendation.impact.base_metrics.conversions, - recommendation.impact.potential_metrics.impressions, - recommendation.impact.potential_metrics.clicks, - recommendation.impact.potential_metrics.cost_micros, - recommendation.impact.potential_metrics.conversions, - recommendation.keyword_recommendation, - recommendation.target_cpa_opt_in_recommendation, - recommendation.maximize_conversions_opt_in_recommendation - FROM recommendation - WHERE recommendation.type IN ( - 'KEYWORD_MATCH_TYPE', - 'TARGET_CPA_OPT_IN', - 'MAXIMIZE_CONVERSIONS_OPT_IN', - 'ENHANCED_CPC_OPT_IN', - 'TARGET_ROAS_OPT_IN', - 'MAXIMIZE_CLICKS_OPT_IN' - )`; - - if (args.campaign_id) { - query += `\n AND recommendation.campaign = 'customers/${cid}/campaigns/${args.campaign_id}'`; - } - - query += `\n LIMIT ${limit}`; - - const rows = await client.query(cid, query); - - if (!rows.length) { - return textResult("No bid recommendations available. Your account may already be well-optimized, or there's insufficient data."); - } - - const lines = rows.map((r: any, i: number) => { - const rec = r.recommendation || {}; - const impact = rec.impact || {}; - const base = impact.baseMetrics || {}; - const potential = impact.potentialMetrics || {}; - - const impactLines: string[] = []; - - if (potential.impressions && base.impressions) { - const delta = parseInt(potential.impressions) - parseInt(base.impressions); - if (delta > 0) impactLines.push(`+${formatNumber(delta)} impressions`); - } - if (potential.clicks && base.clicks) { - const delta = parseInt(potential.clicks) - parseInt(base.clicks); - if (delta > 0) impactLines.push(`+${formatNumber(delta)} clicks`); - } - if (potential.conversions && base.conversions) { - const delta = (potential.conversions || 0) - (base.conversions || 0); - if (delta > 0) impactLines.push(`+${delta.toFixed(1)} conversions`); - } - if (potential.costMicros && base.costMicros) { - const deltaCost = parseInt(potential.costMicros) - parseInt(base.costMicros); - impactLines.push(`${deltaCost >= 0 ? "+" : ""}$${microsToMoney(deltaCost)} cost`); - } - - const impactStr = impactLines.length ? impactLines.join(", ") : "—"; - - return `${i + 1}. **${rec.type || "UNKNOWN"}**\n Estimated impact: ${impactStr}`; - }); - - return textResult( - `## Bid Recommendations\n\n${lines.join("\n\n")}\n\n_${rows.length} recommendation(s) found._` - ); - }, - }, - - // ============================ - // update_keyword_bids - // ============================ - { - name: "update_keyword_bids", - description: - "Change the CPC bid for one or more keywords. " + - "⚠️ This modifies live keyword bids and may immediately affect ad serving and costs.", - category: "bidding", - annotations: { - title: "Update Keyword Bids", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - ad_group_id: { - type: "string", - description: "Ad group ID the keyword belongs to.", - }, - keyword_criterion_id: { - type: "string", - description: "Keyword criterion ID to update bid for. Use this for a single keyword.", - }, - bid_updates: { - type: "array", - items: { - type: "object", - properties: { - criterion_id: { type: "string", description: "Keyword criterion ID." }, - bid_amount: { type: "number", description: "New CPC bid in dollars." }, - }, - required: ["criterion_id", "bid_amount"], - }, - description: "Batch bid updates. Alternative to single keyword_criterion_id + new_bid_amount.", - }, - new_bid_amount: { - type: "number", - description: "New max CPC bid in dollars (e.g. 2.50 for $2.50). Used with keyword_criterion_id.", - }, - }, - required: ["ad_group_id"], - }, - _meta: { - labels: { - category: "bidding", - access: "delete", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - - // Build list of bid updates from either single or batch params - interface BidUpdate { - criterionId: string; - bidAmount: number; - } - - const updates: BidUpdate[] = []; - - if (args.bid_updates?.length) { - for (const u of args.bid_updates) { - if (!u.criterion_id || u.bid_amount == null) { - return errorResult("Each bid_update must have criterion_id and bid_amount."); - } - if (u.bid_amount <= 0) { - return errorResult(`Bid amount must be positive. Got $${u.bid_amount} for criterion ${u.criterion_id}.`); - } - updates.push({ criterionId: u.criterion_id, bidAmount: u.bid_amount }); - } - } else if (args.keyword_criterion_id && args.new_bid_amount != null) { - if (args.new_bid_amount <= 0) { - return errorResult("Bid amount must be positive."); - } - updates.push({ criterionId: args.keyword_criterion_id, bidAmount: args.new_bid_amount }); - } else { - return errorResult( - "Provide either (keyword_criterion_id + new_bid_amount) for a single update, or bid_updates array for batch." - ); - } - - const operations = updates.map((u) => ({ - entity: "adGroupCriterion", - operation: "update" as const, - resource: { - resourceName: `customers/${cid}/adGroupCriteria/${args.ad_group_id}~${u.criterionId}`, - cpcBidMicros: String(Math.round(u.bidAmount * 1_000_000)), - }, - updateMask: "cpc_bid_micros", - })); - - await client.mutate(cid, operations); - - const summary = updates - .map((u) => ` - Criterion ${u.criterionId}: $${u.bidAmount.toFixed(2)}`) - .join("\n"); - - return textResult( - `✅ Updated ${updates.length} keyword bid(s) in ad group ${args.ad_group_id}:\n\n${summary}` - ); - }, - }, - - // ============================ - // list_recommendations - // ============================ - { - name: "list_recommendations", - description: - "Get Google's optimization score and all recommendations for the account, including estimated impact on performance. " + - "Covers bid strategies, keywords, ads, budgets, and more.", - category: "bidding", - annotations: { - title: "List Recommendations", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - types: { - type: "array", - items: { type: "string" }, - description: - "Filter by recommendation types (e.g. KEYWORD, CAMPAIGN_BUDGET, TARGET_CPA_OPT_IN, RESPONSIVE_SEARCH_AD, SITELINK_EXTENSION). Omit for all types.", - }, - campaign_id: { - type: "string", - description: "Limit recommendations to a specific campaign.", - }, - limit: { - type: "number", - description: "Maximum recommendations to return (default: 50).", - }, - }, - }, - _meta: { - labels: { - category: "bidding", - access: "read", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - const limit = Math.min(args.limit || 50, 200); - - // First, get optimization score - let scoreText = ""; - try { - const scoreQuery = ` - SELECT - customer.optimization_score, - customer.optimization_score_weight - FROM customer - LIMIT 1`; - const scoreRows = await client.query(cid, scoreQuery); - if (scoreRows.length) { - const score = scoreRows[0].customer?.optimizationScore; - if (score != null) { - const pct = (score * 100).toFixed(1); - const bar = "█".repeat(Math.round(score * 10)) + "░".repeat(10 - Math.round(score * 10)); - scoreText = `**Optimization Score:** ${pct}% ${bar}\n\n`; - } - } - } catch { - // Optimization score not available for all accounts - } - - // Fetch recommendations - let query = ` - SELECT - recommendation.type, - recommendation.resource_name, - recommendation.campaign, - recommendation.ad_group, - recommendation.impact.base_metrics.impressions, - recommendation.impact.base_metrics.clicks, - recommendation.impact.base_metrics.cost_micros, - recommendation.impact.base_metrics.conversions, - recommendation.impact.potential_metrics.impressions, - recommendation.impact.potential_metrics.clicks, - recommendation.impact.potential_metrics.cost_micros, - recommendation.impact.potential_metrics.conversions, - recommendation.campaign_budget_recommendation, - recommendation.keyword_recommendation, - recommendation.text_ad_recommendation, - recommendation.responsive_search_ad_recommendation - FROM recommendation`; - - const conditions: string[] = []; - - if (args.types?.length) { - conditions.push( - `recommendation.type IN (${args.types.map((t: string) => `'${t}'`).join(", ")})` - ); - } - - if (args.campaign_id) { - conditions.push(`recommendation.campaign = 'customers/${cid}/campaigns/${args.campaign_id}'`); - } - - if (conditions.length) { - query += `\n WHERE ${conditions.join(" AND ")}`; - } - - query += `\n LIMIT ${limit}`; - - const rows = await client.query(cid, query); - - if (!rows.length && !scoreText) { - return textResult("No recommendations available for this account. Your campaigns may already be well-optimized."); - } - - // Group by type - const byType = new Map(); - for (const r of rows) { - const type = r.recommendation?.type || "UNKNOWN"; - if (!byType.has(type)) byType.set(type, []); - byType.get(type)!.push(r); - } - - const sections: string[] = ["## Google Ads Recommendations\n"]; - if (scoreText) sections.push(scoreText); - - for (const [type, recs] of byType) { - sections.push(`### ${type.replace(/_/g, " ")} (${recs.length})\n`); - - recs.forEach((r: any, i: number) => { - const rec = r.recommendation || {}; - const impact = rec.impact || {}; - const base = impact.baseMetrics || {}; - const potential = impact.potentialMetrics || {}; - - const impactParts: string[] = []; - if (potential.clicks && base.clicks) { - const delta = parseInt(potential.clicks) - parseInt(base.clicks); - if (delta !== 0) impactParts.push(`${delta > 0 ? "+" : ""}${formatNumber(delta)} clicks`); - } - if (potential.conversions && base.conversions) { - const delta = (potential.conversions || 0) - (base.conversions || 0); - if (delta !== 0) impactParts.push(`${delta > 0 ? "+" : ""}${delta.toFixed(1)} conv`); - } - if (potential.costMicros && base.costMicros) { - const delta = parseInt(potential.costMicros) - parseInt(base.costMicros); - if (delta !== 0) impactParts.push(`${delta > 0 ? "+" : ""}$${microsToMoney(delta)} cost`); - } - - // Extract details from specific recommendation types - let detail = ""; - if (rec.keywordRecommendation?.keyword) { - const kw = rec.keywordRecommendation.keyword; - detail = ` — Keyword: "${kw.text}" (${kw.matchType})`; - } else if (rec.campaignBudgetRecommendation?.recommendedBudgetAmountMicros) { - detail = ` — Suggested budget: $${microsToMoney(rec.campaignBudgetRecommendation.recommendedBudgetAmountMicros)}`; - } - - const impactStr = impactParts.length ? `Est. impact: ${impactParts.join(", ")}` : ""; - - sections.push(`${i + 1}. ${impactStr}${detail}`); - }); - - sections.push(""); - } - - sections.push(`_${rows.length} total recommendation(s)._`); - - return textResult(sections.join("\n")); - }, - }, -]; - -export default tools; diff --git a/mcp-diagrams/google-ads-mcp/src/tools/campaigns.ts b/mcp-diagrams/google-ads-mcp/src/tools/campaigns.ts deleted file mode 100644 index 5207dd9..0000000 --- a/mcp-diagrams/google-ads-mcp/src/tools/campaigns.ts +++ /dev/null @@ -1,789 +0,0 @@ -// ============================================ -// CAMPAIGN MANAGEMENT TOOLS -// ============================================ - -import type { ToolDefinition, ToolResult, GoogleAdsClient } from "../types.js"; -import { microsToMoney, formatPercent, formatNumber } from "../types.js"; - -function text(t: string): ToolResult { - return { content: [{ type: "text", text: t }] }; -} - -function errorResult(msg: string): ToolResult { - return { content: [{ type: "text", text: `❌ ${msg}` }], isError: true }; -} - -const DATE_RANGE_ENUM = [ - "TODAY", "YESTERDAY", "LAST_7_DAYS", "LAST_30_DAYS", - "THIS_MONTH", "LAST_MONTH", "THIS_QUARTER", "LAST_QUARTER", -]; - -// ─── list_campaigns ───────────────────────────────────────────────── -const listCampaigns: ToolDefinition = { - name: "list_campaigns", - description: - "List all campaigns in a Google Ads account with status, type, budget, and basic performance " + - "metrics. Supports filtering by status and date range.", - category: "campaigns", - annotations: { - title: "List Campaigns", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID. Uses default if not provided.", - }, - status: { - type: "string", - description: "Filter by campaign status.", - enum: ["ENABLED", "PAUSED", "REMOVED", "ALL"], - default: "ALL", - }, - date_range: { - type: "string", - description: "Date range for metrics.", - enum: DATE_RANGE_ENUM, - default: "LAST_30_DAYS", - }, - limit: { - type: "number", - description: "Max number of campaigns to return (default 50).", - default: 50, - }, - }, - }, - _meta: { - labels: { - category: "campaigns", - access: "read", - complexity: "simple" - } - }, - handler: async (args: Record, client: GoogleAdsClient): Promise => { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) return errorResult("No customer ID provided and no default configured."); - - const status = args.status || "ALL"; - const dateRange = args.date_range || "LAST_30_DAYS"; - const limit = args.limit || 50; - - const conditions: string[] = []; - if (status !== "ALL") { - conditions.push(`campaign.status = '${status}'`); - } - conditions.push(`segments.date DURING ${dateRange}`); - - const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""; - - const query = ` - SELECT - campaign.id, - campaign.name, - campaign.status, - campaign.advertising_channel_type, - campaign.bidding_strategy_type, - campaign_budget.amount_micros, - metrics.impressions, - metrics.clicks, - metrics.cost_micros, - metrics.conversions, - metrics.ctr - FROM campaign - ${where} - ORDER BY metrics.cost_micros DESC - LIMIT ${limit} - `; - - const results = await client.query(customerId, query); - if (!results.length) { - return text(`No campaigns found${status !== "ALL" ? ` with status ${status}` : ""} for the ${dateRange} period.`); - } - - const header = - `| Campaign | Status | Type | Daily Budget | Impressions | Clicks | Cost | Conv. | CTR |\n` + - `|----------|--------|------|-------------|-------------|--------|------|-------|-----|`; - - const rows = results.map((r: any) => { - const c = r.campaign; - const m = r.metrics || {}; - const budget = r.campaignBudget?.amountMicros - ? `$${microsToMoney(r.campaignBudget.amountMicros)}` - : "—"; - return ( - `| ${c.name || "—"} | ${c.status} | ${c.advertisingChannelType || "—"} | ${budget} | ` + - `${formatNumber(m.impressions || 0)} | ${formatNumber(m.clicks || 0)} | ` + - `$${microsToMoney(m.costMicros || 0)} | ${Number(m.conversions || 0).toFixed(1)} | ` + - `${formatPercent(m.ctr || 0)} |` - ); - }); - - return text( - `## Campaigns — \`${customerId}\`\n\n` + - `**Date range:** ${dateRange} · **Filter:** ${status} · **Showing:** ${results.length} campaign(s)\n\n` + - `${header}\n${rows.join("\n")}` - ); - }, -}; - -// ─── get_campaign ─────────────────────────────────────────────────── -const getCampaign: ToolDefinition = { - name: "get_campaign", - description: - "Get full details for a single campaign by ID, including settings, bidding, budget, and recent metrics.", - category: "campaigns", - annotations: { - title: "Get Campaign Details", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID. Uses default if not provided.", - }, - campaign_id: { - type: "string", - description: "The campaign ID to retrieve.", - }, - date_range: { - type: "string", - description: "Date range for metrics.", - enum: DATE_RANGE_ENUM, - default: "LAST_30_DAYS", - }, - }, - required: ["campaign_id"], - }, - _meta: { - labels: { - category: "campaigns", - access: "read", - complexity: "simple" - } - }, - handler: async (args: Record, client: GoogleAdsClient): Promise => { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) return errorResult("No customer ID provided and no default configured."); - - const dateRange = args.date_range || "LAST_30_DAYS"; - - const query = ` - SELECT - campaign.id, - campaign.name, - campaign.status, - campaign.advertising_channel_type, - campaign.advertising_channel_sub_type, - campaign.bidding_strategy_type, - campaign.start_date, - campaign.end_date, - campaign.serving_status, - campaign.network_settings.target_google_search, - campaign.network_settings.target_search_network, - campaign.network_settings.target_content_network, - campaign_budget.amount_micros, - campaign_budget.delivery_method, - campaign_budget.total_amount_micros, - metrics.impressions, - metrics.clicks, - metrics.cost_micros, - metrics.conversions, - metrics.conversions_value, - metrics.ctr, - metrics.average_cpc, - metrics.average_cpm - FROM campaign - WHERE campaign.id = ${args.campaign_id} - AND segments.date DURING ${dateRange} - `; - - const results = await client.query(customerId, query); - if (!results.length) return errorResult(`Campaign ${args.campaign_id} not found.`); - - const c = results[0].campaign; - const m = results[0].metrics || {}; - const b = results[0].campaignBudget || {}; - const net = c.networkSettings || {}; - - const budget = b.amountMicros ? `$${microsToMoney(b.amountMicros)}/day` : "—"; - const totalBudget = b.totalAmountMicros ? `$${microsToMoney(b.totalAmountMicros)}` : "—"; - - return text( - `## Campaign: ${c.name}\n\n` + - `### Settings\n\n` + - `| Field | Value |\n` + - `|-------|-------|\n` + - `| **Campaign ID** | \`${c.id}\` |\n` + - `| **Status** | ${c.status} |\n` + - `| **Serving Status** | ${c.servingStatus || "—"} |\n` + - `| **Channel Type** | ${c.advertisingChannelType || "—"} |\n` + - `| **Channel Sub-Type** | ${c.advertisingChannelSubType || "—"} |\n` + - `| **Bidding Strategy** | ${c.biddingStrategyType || "—"} |\n` + - `| **Start Date** | ${c.startDate || "—"} |\n` + - `| **End Date** | ${c.endDate || "—"} |\n` + - `| **Daily Budget** | ${budget} |\n` + - `| **Total Budget** | ${totalBudget} |\n` + - `| **Delivery Method** | ${b.deliveryMethod || "—"} |\n\n` + - `### Network Settings\n\n` + - `| Network | Enabled |\n` + - `|---------|---------|\n` + - `| Google Search | ${net.targetGoogleSearch ? "✅" : "❌"} |\n` + - `| Search Network | ${net.targetSearchNetwork ? "✅" : "❌"} |\n` + - `| Content Network | ${net.targetContentNetwork ? "✅" : "❌"} |\n\n` + - `### Performance (${dateRange})\n\n` + - `| Metric | Value |\n` + - `|--------|-------|\n` + - `| **Impressions** | ${formatNumber(m.impressions || 0)} |\n` + - `| **Clicks** | ${formatNumber(m.clicks || 0)} |\n` + - `| **CTR** | ${formatPercent(m.ctr || 0)} |\n` + - `| **Cost** | $${microsToMoney(m.costMicros || 0)} |\n` + - `| **Avg. CPC** | $${microsToMoney(m.averageCpc || 0)} |\n` + - `| **Avg. CPM** | $${microsToMoney(m.averageCpm || 0)} |\n` + - `| **Conversions** | ${Number(m.conversions || 0).toFixed(1)} |\n` + - `| **Conv. Value** | $${Number(m.conversionsValue || 0).toFixed(2)} |` - ); - }, -}; - -// ─── create_campaign ──────────────────────────────────────────────── -const createCampaign: ToolDefinition = { - name: "create_campaign", - description: - "⚠️ DESTRUCTIVE: Create a new Google Ads campaign. This will create a campaign budget and " + - "the campaign itself. The campaign starts in PAUSED status by default for safety. " + - "Review carefully before enabling.", - category: "campaigns", - annotations: { - title: "Create Campaign", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: false, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID. Uses default if not provided.", - }, - name: { - type: "string", - description: "Campaign name.", - }, - channel_type: { - type: "string", - description: "Advertising channel type.", - enum: ["SEARCH", "DISPLAY", "PERFORMANCE_MAX", "SHOPPING", "VIDEO"], - }, - bidding_strategy: { - type: "string", - description: "Bidding strategy type.", - enum: [ - "MANUAL_CPC", "MAXIMIZE_CLICKS", "MAXIMIZE_CONVERSIONS", - "MAXIMIZE_CONVERSION_VALUE", "TARGET_CPA", "TARGET_ROAS", "TARGET_SPEND", - ], - }, - budget_amount_per_day: { - type: "number", - description: "Daily budget in account currency (e.g., 50.00 for $50/day).", - }, - start_date: { - type: "string", - description: "Campaign start date in YYYY-MM-DD format. Defaults to today.", - }, - status: { - type: "string", - description: "Initial campaign status. Default PAUSED for safety.", - enum: ["ENABLED", "PAUSED"], - default: "PAUSED", - }, - }, - required: ["name", "channel_type", "bidding_strategy", "budget_amount_per_day"], - }, - _meta: { - labels: { - category: "campaigns", - access: "read", - complexity: "simple" - } - }, - handler: async (args: Record, client: GoogleAdsClient): Promise => { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) return errorResult("No customer ID provided and no default configured."); - - const budgetMicros = Math.round(args.budget_amount_per_day * 1_000_000); - const tempId = `-1`; // Temporary ID for the budget, referenced by campaign - - const budgetOp = { - entity: "campaignBudget", - operation: "create" as const, - resource: { - name: `${args.name} Budget`, - amountMicros: budgetMicros.toString(), - deliveryMethod: "STANDARD", - explicitlyShared: false, - }, - }; - - const campaignResource: Record = { - name: args.name, - advertisingChannelType: args.channel_type, - status: args.status || "PAUSED", - campaignBudget: `customers/${customerId}/campaignBudgets/${tempId}`, - }; - - // Map bidding strategy to the correct field - const biddingMap: Record = { - MANUAL_CPC: { manualCpc: { enhancedCpcEnabled: false } }, - MAXIMIZE_CLICKS: { maximizeClicks: {} }, - MAXIMIZE_CONVERSIONS: { maximizeConversions: {} }, - MAXIMIZE_CONVERSION_VALUE: { maximizeConversionValue: {} }, - TARGET_CPA: { targetCpa: {} }, - TARGET_ROAS: { targetRoas: {} }, - TARGET_SPEND: { targetSpend: {} }, - }; - - const biddingConfig = biddingMap[args.bidding_strategy]; - if (biddingConfig) { - Object.assign(campaignResource, biddingConfig); - } - - if (args.start_date) { - campaignResource.startDate = args.start_date.replace(/-/g, ""); - } - - // Network settings for Search campaigns - if (args.channel_type === "SEARCH") { - campaignResource.networkSettings = { - targetGoogleSearch: true, - targetSearchNetwork: true, - targetContentNetwork: false, - }; - } - - const campaignOp = { - entity: "campaign", - operation: "create" as const, - resource: campaignResource, - }; - - const result = await client.mutate(customerId, [budgetOp, campaignOp]); - - const mutateResults = result.mutateOperationResponses || []; - const campaignRn = mutateResults[1]?.campaignResult?.resourceName || "unknown"; - - return text( - `## ✅ Campaign Created\n\n` + - `| Field | Value |\n` + - `|-------|-------|\n` + - `| **Name** | ${args.name} |\n` + - `| **Channel** | ${args.channel_type} |\n` + - `| **Bidding** | ${args.bidding_strategy} |\n` + - `| **Daily Budget** | $${args.budget_amount_per_day.toFixed(2)} |\n` + - `| **Status** | ${args.status || "PAUSED"} |\n` + - `| **Resource** | \`${campaignRn}\` |\n\n` + - `${(args.status || "PAUSED") === "PAUSED" ? "ℹ️ Campaign created in **PAUSED** status. Use `enable_campaign` to activate it." : "⚡ Campaign is **ENABLED** and will start serving ads."}` - ); - }, -}; - -// ─── update_campaign ──────────────────────────────────────────────── -const updateCampaign: ToolDefinition = { - name: "update_campaign", - description: - "⚠️ DESTRUCTIVE: Update campaign settings such as name, bidding strategy, start/end dates, " + - "and network settings. Only specified fields will be updated.", - category: "campaigns", - annotations: { - title: "Update Campaign", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID. Uses default if not provided.", - }, - campaign_id: { - type: "string", - description: "Campaign ID to update.", - }, - name: { - type: "string", - description: "New campaign name.", - }, - bidding_strategy: { - type: "string", - description: "New bidding strategy.", - enum: [ - "MANUAL_CPC", "MAXIMIZE_CLICKS", "MAXIMIZE_CONVERSIONS", - "MAXIMIZE_CONVERSION_VALUE", "TARGET_CPA", "TARGET_ROAS", "TARGET_SPEND", - ], - }, - start_date: { - type: "string", - description: "New start date (YYYY-MM-DD).", - }, - end_date: { - type: "string", - description: "New end date (YYYY-MM-DD).", - }, - }, - required: ["campaign_id"], - }, - _meta: { - labels: { - category: "campaigns", - access: "read", - complexity: "simple" - } - }, - handler: async (args: Record, client: GoogleAdsClient): Promise => { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) return errorResult("No customer ID provided and no default configured."); - - const resourceName = `customers/${customerId}/campaigns/${args.campaign_id}`; - const resource: Record = { resourceName }; - const updateFields: string[] = []; - - if (args.name) { - resource.name = args.name; - updateFields.push("name"); - } - if (args.start_date) { - resource.startDate = args.start_date.replace(/-/g, ""); - updateFields.push("start_date"); - } - if (args.end_date) { - resource.endDate = args.end_date.replace(/-/g, ""); - updateFields.push("end_date"); - } - if (args.bidding_strategy) { - const biddingMap: Record = { - MANUAL_CPC: ["manual_cpc", { enhancedCpcEnabled: false }], - MAXIMIZE_CLICKS: ["maximize_clicks", {}], - MAXIMIZE_CONVERSIONS: ["maximize_conversions", {}], - MAXIMIZE_CONVERSION_VALUE: ["maximize_conversion_value", {}], - TARGET_CPA: ["target_cpa", {}], - TARGET_ROAS: ["target_roas", {}], - TARGET_SPEND: ["target_spend", {}], - }; - const [field, config] = biddingMap[args.bidding_strategy]; - resource[field.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase())] = config; - updateFields.push(field); - } - - if (!updateFields.length) { - return errorResult("No fields to update. Provide at least one field (name, bidding_strategy, start_date, end_date)."); - } - - await client.mutate(customerId, [ - { - entity: "campaign", - operation: "update", - resource, - updateMask: updateFields.join(","), - }, - ]); - - return text( - `## ✅ Campaign Updated\n\n` + - `**Campaign ID:** \`${args.campaign_id}\`\n` + - `**Fields updated:** ${updateFields.join(", ")}\n\n` + - updateFields.map((f) => `- **${f}** → ${args[f.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase())] || args[f] || "updated"}`).join("\n") - ); - }, -}; - -// ─── pause_campaign ───────────────────────────────────────────────── -const pauseCampaign: ToolDefinition = { - name: "pause_campaign", - description: - "⚠️ DESTRUCTIVE: Pause a campaign by setting its status to PAUSED. " + - "The campaign will stop serving ads immediately. Safe to call on already-paused campaigns.", - category: "campaigns", - annotations: { - title: "Pause Campaign", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID. Uses default if not provided.", - }, - campaign_id: { - type: "string", - description: "Campaign ID to pause.", - }, - }, - required: ["campaign_id"], - }, - _meta: { - labels: { - category: "campaigns", - access: "read", - complexity: "simple" - } - }, - handler: async (args: Record, client: GoogleAdsClient): Promise => { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) return errorResult("No customer ID provided and no default configured."); - - const resourceName = `customers/${customerId}/campaigns/${args.campaign_id}`; - - await client.mutate(customerId, [ - { - entity: "campaign", - operation: "update", - resource: { resourceName, status: "PAUSED" }, - updateMask: "status", - }, - ]); - - return text( - `## ⏸️ Campaign Paused\n\n` + - `**Campaign ID:** \`${args.campaign_id}\`\n` + - `**Status:** PAUSED\n\n` + - `The campaign has stopped serving ads. Use \`enable_campaign\` to resume.` - ); - }, -}; - -// ─── enable_campaign ──────────────────────────────────────────────── -const enableCampaign: ToolDefinition = { - name: "enable_campaign", - description: - "⚠️ DESTRUCTIVE: Enable a paused campaign by setting its status to ENABLED. " + - "The campaign will begin serving ads. Safe to call on already-enabled campaigns.", - category: "campaigns", - annotations: { - title: "Enable Campaign", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID. Uses default if not provided.", - }, - campaign_id: { - type: "string", - description: "Campaign ID to enable.", - }, - }, - required: ["campaign_id"], - }, - _meta: { - labels: { - category: "campaigns", - access: "read", - complexity: "simple" - } - }, - handler: async (args: Record, client: GoogleAdsClient): Promise => { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) return errorResult("No customer ID provided and no default configured."); - - const resourceName = `customers/${customerId}/campaigns/${args.campaign_id}`; - - await client.mutate(customerId, [ - { - entity: "campaign", - operation: "update", - resource: { resourceName, status: "ENABLED" }, - updateMask: "status", - }, - ]); - - return text( - `## ▶️ Campaign Enabled\n\n` + - `**Campaign ID:** \`${args.campaign_id}\`\n` + - `**Status:** ENABLED\n\n` + - `The campaign is now active and will serve ads based on its settings, budget, and targeting.` - ); - }, -}; - -// ─── remove_campaign ──────────────────────────────────────────────── -const removeCampaign: ToolDefinition = { - name: "remove_campaign", - description: - "⚠️ DESTRUCTIVE & IRREVERSIBLE: Remove a campaign. This sets the campaign to REMOVED status. " + - "Removed campaigns cannot be re-enabled. All ads in the campaign will stop serving. " + - "Historical data is preserved but the campaign cannot be reused.", - category: "campaigns", - annotations: { - title: "Remove Campaign", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: false, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID. Uses default if not provided.", - }, - campaign_id: { - type: "string", - description: "Campaign ID to remove.", - }, - }, - required: ["campaign_id"], - }, - _meta: { - labels: { - category: "campaigns", - access: "read", - complexity: "simple" - } - }, - handler: async (args: Record, client: GoogleAdsClient): Promise => { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) return errorResult("No customer ID provided and no default configured."); - - const resourceName = `customers/${customerId}/campaigns/${args.campaign_id}`; - - await client.mutate(customerId, [ - { - entity: "campaign", - operation: "remove", - resourceName, - }, - ]); - - return text( - `## 🗑️ Campaign Removed\n\n` + - `**Campaign ID:** \`${args.campaign_id}\`\n` + - `**Status:** REMOVED\n\n` + - `⚠️ This action is **irreversible**. The campaign and its ads have stopped serving.\n` + - `Historical performance data remains accessible through reporting tools.` - ); - }, -}; - -// ─── update_campaign_budget ───────────────────────────────────────── -const updateCampaignBudget: ToolDefinition = { - name: "update_campaign_budget", - description: - "⚠️ DESTRUCTIVE: Change the daily budget for a campaign. Specify the new amount in " + - "account currency (e.g., 100.00 for $100/day). The change takes effect immediately.", - category: "campaigns", - annotations: { - title: "Update Campaign Budget", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID. Uses default if not provided.", - }, - campaign_id: { - type: "string", - description: "Campaign ID whose budget to update.", - }, - new_budget_amount: { - type: "number", - description: "New daily budget in account currency (e.g., 50.00 for $50/day).", - }, - }, - required: ["campaign_id", "new_budget_amount"], - }, - _meta: { - labels: { - category: "campaigns", - access: "read", - complexity: "simple" - } - }, - handler: async (args: Record, client: GoogleAdsClient): Promise => { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) return errorResult("No customer ID provided and no default configured."); - - // First, get the campaign's budget resource name - const query = ` - SELECT campaign.id, campaign.name, campaign_budget.resource_name, campaign_budget.amount_micros - FROM campaign - WHERE campaign.id = ${args.campaign_id} - `; - - const results = await client.query(customerId, query); - if (!results.length) return errorResult(`Campaign ${args.campaign_id} not found.`); - - const campaign = results[0].campaign; - const budgetRn = results[0].campaignBudget?.resourceName; - const oldBudgetMicros = results[0].campaignBudget?.amountMicros || "0"; - - if (!budgetRn) return errorResult("Could not find budget resource for this campaign."); - - const newBudgetMicros = Math.round(args.new_budget_amount * 1_000_000); - - await client.mutate(customerId, [ - { - entity: "campaignBudget", - operation: "update", - resource: { - resourceName: budgetRn, - amountMicros: newBudgetMicros.toString(), - }, - updateMask: "amount_micros", - }, - ]); - - return text( - `## 💰 Budget Updated\n\n` + - `**Campaign:** ${campaign.name} (\`${args.campaign_id}\`)\n\n` + - `| | Amount |\n` + - `|---|--------|\n` + - `| **Previous Budget** | $${microsToMoney(oldBudgetMicros)}/day |\n` + - `| **New Budget** | $${args.new_budget_amount.toFixed(2)}/day |\n` + - `| **Change** | ${args.new_budget_amount > Number(oldBudgetMicros) / 1_000_000 ? "📈" : "📉"} ${((args.new_budget_amount / (Number(oldBudgetMicros) / 1_000_000) - 1) * 100).toFixed(1)}% |\n\n` + - `The new budget takes effect immediately.` - ); - }, -}; - -// ─── Export ───────────────────────────────────────────────────────── -const tools: ToolDefinition[] = [ - listCampaigns, - getCampaign, - createCampaign, - updateCampaign, - pauseCampaign, - enableCampaign, - removeCampaign, - updateCampaignBudget, -]; - -export default tools; diff --git a/mcp-diagrams/google-ads-mcp/src/tools/conversions.ts b/mcp-diagrams/google-ads-mcp/src/tools/conversions.ts deleted file mode 100644 index fbad83d..0000000 --- a/mcp-diagrams/google-ads-mcp/src/tools/conversions.ts +++ /dev/null @@ -1,682 +0,0 @@ -// ============================================ -// CONVERSION TRACKING TOOLS -// ============================================ - -import type { ToolDefinition, GoogleAdsClient, ToolResult } from "../types.js"; -import { microsToMoney, formatNumber } from "../types.js"; - -// ---- list_conversion_actions ---- - -async function listConversionActions( - args: Record, - client: GoogleAdsClient -): Promise { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) { - return { - content: [{ type: "text", text: "Error: customer_id is required." }], - isError: true, - }; - } - - const query = ` - SELECT - conversion_action.id, - conversion_action.name, - conversion_action.type, - conversion_action.status, - conversion_action.category, - conversion_action.counting_type, - conversion_action.tag_snippets - FROM conversion_action - ORDER BY conversion_action.name - `; - - const rows = await client.query(customerId, query); - - if (!rows.length) { - return { - content: [{ type: "text", text: "No conversion actions found in this account." }], - }; - } - - const lines = rows.map((row) => { - const ca = row.conversionAction; - const snippetInfo = - ca.tagSnippets?.length - ? `\n Tag snippets: ${ca.tagSnippets.length} configured` - : ""; - return [ - ` • **${ca.name}** (ID: ${ca.id})`, - ` Type: ${ca.type} | Status: ${ca.status}`, - ` Category: ${ca.category} | Counting: ${ca.countingType}${snippetInfo}`, - ].join("\n"); - }); - - return { - content: [ - { - type: "text", - text: `## Conversion Actions (${rows.length})\n\n${lines.join("\n\n")}`, - }, - ], - }; -} - -// ---- get_conversion_metrics ---- - -async function getConversionMetrics( - args: Record, - client: GoogleAdsClient -): Promise { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) { - return { - content: [{ type: "text", text: "Error: customer_id is required." }], - isError: true, - }; - } - - const dateRange = args.date_range || "LAST_30_DAYS"; - const groupBy = args.campaign_id ? "ad_group" : "campaign"; - - let query: string; - - if (args.campaign_id) { - // Drill into ad groups within a campaign - query = ` - SELECT - ad_group.id, - ad_group.name, - campaign.id, - campaign.name, - metrics.conversions, - metrics.conversions_value, - metrics.cost_per_conversion, - metrics.cost_micros, - metrics.clicks, - metrics.impressions, - metrics.conversions_from_interactions_rate - FROM ad_group - WHERE campaign.id = ${args.campaign_id} - AND segments.date DURING ${dateRange} - AND metrics.impressions > 0 - ORDER BY metrics.conversions DESC - `; - } else { - // Campaign-level conversion metrics - query = ` - SELECT - campaign.id, - campaign.name, - campaign.status, - metrics.conversions, - metrics.conversions_value, - metrics.cost_per_conversion, - metrics.cost_micros, - metrics.clicks, - metrics.impressions, - metrics.conversions_from_interactions_rate - FROM campaign - WHERE segments.date DURING ${dateRange} - AND metrics.impressions > 0 - ORDER BY metrics.conversions DESC - `; - } - - const rows = await client.query(customerId, query); - - if (!rows.length) { - return { - content: [ - { - type: "text", - text: `No conversion data found for ${dateRange}.${args.campaign_id ? ` Campaign ID: ${args.campaign_id}` : ""}`, - }, - ], - }; - } - - // Compute totals - let totalConversions = 0; - let totalValue = 0; - let totalCost = 0; - - const lines = rows.map((row) => { - const m = row.metrics; - const conversions = m.conversions || 0; - const convValue = m.conversionsValue || 0; - const costPerConv = m.costPerConversion ? microsToMoney(m.costPerConversion) : "N/A"; - const convRate = m.conversionsFromInteractionsRate - ? (m.conversionsFromInteractionsRate * 100).toFixed(2) + "%" - : "N/A"; - - totalConversions += conversions; - totalValue += convValue; - totalCost += parseInt(m.costMicros || "0"); - - if (groupBy === "ad_group") { - const ag = row.adGroup; - return [ - ` • **${ag.name}** (ID: ${ag.id})`, - ` Conversions: ${conversions.toFixed(1)} | Value: $${convValue.toFixed(2)}`, - ` Cost/Conv: $${costPerConv} | Conv Rate: ${convRate}`, - ` Spend: $${microsToMoney(m.costMicros)} | Clicks: ${formatNumber(m.clicks)}`, - ].join("\n"); - } else { - const c = row.campaign; - return [ - ` • **${c.name}** [${c.status}] (ID: ${c.id})`, - ` Conversions: ${conversions.toFixed(1)} | Value: $${convValue.toFixed(2)}`, - ` Cost/Conv: $${costPerConv} | Conv Rate: ${convRate}`, - ` Spend: $${microsToMoney(m.costMicros)} | Clicks: ${formatNumber(m.clicks)}`, - ].join("\n"); - } - }); - - const avgCostPerConv = - totalConversions > 0 - ? "$" + (totalCost / 1_000_000 / totalConversions).toFixed(2) - : "N/A"; - - const summary = [ - `**Total Conversions:** ${totalConversions.toFixed(1)}`, - `**Total Value:** $${totalValue.toFixed(2)}`, - `**Total Spend:** $${microsToMoney(totalCost)}`, - `**Avg Cost/Conversion:** ${avgCostPerConv}`, - ].join(" | "); - - const heading = args.campaign_id - ? `## Conversion Metrics by Ad Group (Campaign ${args.campaign_id})` - : `## Conversion Metrics by Campaign`; - - return { - content: [ - { - type: "text", - text: `${heading}\n📅 ${dateRange}\n\n${summary}\n\n${lines.join("\n\n")}`, - }, - ], - }; -} - -// ---- create_conversion_action ---- - -async function createConversionAction( - args: Record, - client: GoogleAdsClient -): Promise { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) { - return { - content: [{ type: "text", text: "Error: customer_id is required." }], - isError: true, - }; - } - - const { name, type, category, counting_type } = args; - - if (!name || !type || !category) { - return { - content: [ - { - type: "text", - text: "Error: name, type, and category are required fields.", - }, - ], - isError: true, - }; - } - - const resource: Record = { - name, - type, - category, - countingType: counting_type || "ONE_PER_CLICK", - status: "ENABLED", - }; - - // Set sensible defaults based on type - if (type === "WEBPAGE") { - resource.viewThroughLookbackWindowDays = 1; - resource.clickThroughLookbackWindowDays = 30; - } else if (type === "PHONE_CALL") { - resource.phoneCallDurationSeconds = 60; - } - - // Include optional value settings if provided - if (args.default_value !== undefined) { - resource.valueSettings = { - defaultValue: args.default_value, - alwaysUseDefaultValue: args.always_use_default_value ?? false, - }; - } - - const result = await client.mutate(customerId, [ - { - entity: "conversionAction", - operation: "create", - resource, - }, - ]); - - const createdResource = - result.mutateOperationResponses?.[0]?.conversionActionResult?.resourceName || - "unknown"; - - return { - content: [ - { - type: "text", - text: [ - `## ✅ Conversion Action Created`, - ``, - `**Name:** ${name}`, - `**Type:** ${type}`, - `**Category:** ${category}`, - `**Counting:** ${counting_type || "ONE_PER_CLICK"}`, - `**Resource:** \`${createdResource}\``, - ``, - type === "WEBPAGE" - ? `> 💡 Use \`list_conversion_actions\` to retrieve the tag snippet for installation.` - : `> Conversion action is now active and ready to track.`, - ].join("\n"), - }, - ], - }; -} - -// ---- upload_offline_conversions ---- - -async function uploadOfflineConversions( - args: Record, - client: GoogleAdsClient -): Promise { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) { - return { - content: [{ type: "text", text: "Error: customer_id is required." }], - isError: true, - }; - } - - const conversions: Array<{ - gclid: string; - conversion_action: string; - conversion_date_time: string; - conversion_value?: number; - currency_code?: string; - }> = args.conversions; - - if (!Array.isArray(conversions) || conversions.length === 0) { - return { - content: [ - { - type: "text", - text: 'Error: conversions array is required and must not be empty. Each entry needs: gclid, conversion_action (resource name like "customers/123/conversionActions/456"), conversion_date_time (format: "2025-01-15 12:30:00-05:00").', - }, - ], - isError: true, - }; - } - - // Validate each conversion entry - const errors: string[] = []; - conversions.forEach((conv, i) => { - if (!conv.gclid) errors.push(`Conversion ${i + 1}: missing gclid`); - if (!conv.conversion_action) - errors.push(`Conversion ${i + 1}: missing conversion_action`); - if (!conv.conversion_date_time) - errors.push(`Conversion ${i + 1}: missing conversion_date_time`); - }); - - if (errors.length > 0) { - return { - content: [ - { - type: "text", - text: `Validation errors:\n${errors.map((e) => ` • ${e}`).join("\n")}`, - }, - ], - isError: true, - }; - } - - // Build the click conversion upload request - // Uses the ConversionUploadService REST endpoint - const cid = customerId.replace(/-/g, ""); - const uploadConversions = conversions.map((conv) => { - const entry: Record = { - gclid: conv.gclid, - conversionAction: conv.conversion_action.includes("/") - ? conv.conversion_action - : `customers/${cid}/conversionActions/${conv.conversion_action}`, - conversionDateTime: conv.conversion_date_time, - }; - if (conv.conversion_value !== undefined) { - entry.conversionValue = conv.conversion_value; - } - if (conv.currency_code) { - entry.currencyCode = conv.currency_code; - } - return entry; - }); - - // The upload endpoint requires direct REST call through the client. - // We use the GoogleAdsClient interface — the upload_click_conversions - // endpoint is: POST /customers/{cid}:uploadClickConversions - // - // Since our client interface exposes query() and mutate(), we call - // the underlying request method via mutate-style operations. - // The REST API endpoint is: - // POST https://googleads.googleapis.com/v21/customers/{cid}:uploadClickConversions - // - // We need to go through the client's request pipeline. The cleanest - // approach: use the mutate interface with a special entity type that - // the server can route, or call the API directly. - // - // For this implementation, we construct the operations so the MCP - // server routes them through the conversion upload endpoint. - - // Use the mutate pathway — the server will need to handle - // conversionUpload operations specially, but we structure it cleanly - const result = await client.mutate(customerId, [ - { - entity: "conversionUpload", - operation: "create", - resource: { - conversions: uploadConversions, - partialFailure: true, - }, - }, - ]); - - // Parse the response - const successCount = conversions.length; - const partialErrors = result.partialFailureError?.details || []; - const failedCount = partialErrors.length; - const actualSuccess = successCount - failedCount; - - const lines = [ - `## Offline Conversion Upload Results`, - ``, - `**Submitted:** ${successCount} conversions`, - `**Succeeded:** ${actualSuccess}`, - failedCount > 0 ? `**Failed:** ${failedCount}` : null, - ``, - ].filter(Boolean) as string[]; - - if (failedCount > 0) { - lines.push(`### Errors:`); - partialErrors.forEach((err: any, i: number) => { - lines.push( - ` • Entry ${i + 1}: ${err.message || JSON.stringify(err)}` - ); - }); - lines.push(``); - } - - lines.push( - `> 💡 Offline conversions typically take up to 24 hours to appear in reports.` - ); - - return { - content: [{ type: "text", text: lines.join("\n") }], - isError: failedCount === successCount, - }; -} - -// ============================================ -// TOOL DEFINITIONS -// ============================================ - -const tools: ToolDefinition[] = [ - { - name: "list_conversion_actions", - description: - "List all configured conversion actions in the account, including type, status, category, and counting type. " + - "Shows tag snippet availability for web-based conversions.", - category: "conversions", - annotations: { - title: "List Conversion Actions", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: - "Google Ads customer ID (without dashes). Defaults to configured account.", - }, - }, - }, - _meta: { - labels: { - category: "conversions", - access: "read", - complexity: "simple" - } - }, - handler: listConversionActions, - }, - { - name: "get_conversion_metrics", - description: - "Get conversion performance data aggregated by campaign or by ad group (when campaign_id is specified). " + - "Includes conversions, conversion value, cost per conversion, and conversion rate.", - category: "conversions", - annotations: { - title: "Get Conversion Metrics", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: - "Google Ads customer ID (without dashes). Defaults to configured account.", - }, - campaign_id: { - type: "string", - description: - "Campaign ID to drill into ad group-level conversion data. Omit for campaign-level overview.", - }, - date_range: { - type: "string", - description: - "GAQL date range predicate. Examples: LAST_7_DAYS, LAST_30_DAYS, LAST_90_DAYS, THIS_MONTH, LAST_MONTH.", - enum: [ - "TODAY", - "YESTERDAY", - "LAST_7_DAYS", - "LAST_14_DAYS", - "LAST_30_DAYS", - "LAST_90_DAYS", - "THIS_MONTH", - "LAST_MONTH", - "THIS_QUARTER", - "LAST_QUARTER", - ], - default: "LAST_30_DAYS", - }, - }, - }, - _meta: { - labels: { - category: "conversions", - access: "read", - complexity: "simple" - } - }, - handler: getConversionMetrics, - }, - { - name: "create_conversion_action", - description: - "Create a new conversion action for tracking. Supports webpage, phone call, import (offline), " + - "and other conversion types. Configure category and counting behavior.", - category: "conversions", - annotations: { - title: "Create Conversion Action", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: false, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: - "Google Ads customer ID (without dashes). Defaults to configured account.", - }, - name: { - type: "string", - description: "Name for the conversion action (e.g., 'Purchase', 'Lead Form Submit').", - }, - type: { - type: "string", - description: "Type of conversion to track.", - enum: [ - "WEBPAGE", - "PHONE_CALL", - "IMPORT", - "CLICK_TO_CALL", - "APP_INSTALL", - "APP_ENGAGEMENT", - "STORE_VISIT", - "STORE_SALE", - ], - }, - category: { - type: "string", - description: "Conversion category for reporting classification.", - enum: [ - "PURCHASE", - "LEAD", - "SIGN_UP", - "PAGE_VIEW", - "ADD_TO_CART", - "BEGIN_CHECKOUT", - "SUBSCRIBE_PAID", - "PHONE_CALL_LEAD", - "IMPORTED_LEAD", - "SUBMIT_LEAD_FORM", - "BOOK_APPOINTMENT", - "REQUEST_QUOTE", - "GET_DIRECTIONS", - "OUTBOUND_CLICK", - "CONTACT", - "ENGAGEMENT", - "STORE_VISIT", - "STORE_SALE", - "DEFAULT", - ], - }, - counting_type: { - type: "string", - description: - "How to count conversions. ONE_PER_CLICK counts at most one conversion per click. " + - "MANY_PER_CLICK counts every conversion.", - enum: ["ONE_PER_CLICK", "MANY_PER_CLICK"], - default: "ONE_PER_CLICK", - }, - default_value: { - type: "number", - description: "Default monetary value for each conversion (optional).", - }, - always_use_default_value: { - type: "boolean", - description: - "If true, always use the default value even when a specific value is provided. Default: false.", - }, - }, - required: ["name", "type", "category"], - }, - _meta: { - labels: { - category: "conversions", - access: "delete", - complexity: "simple" - } - }, - handler: createConversionAction, - }, - { - name: "upload_offline_conversions", - description: - "Upload offline click conversions (e.g., CRM sales, phone leads that closed) back to Google Ads. " + - "Requires GCLID, conversion action resource name, and timestamp. " + - "Conversions take up to 24 hours to reflect in reports.", - category: "conversions", - annotations: { - title: "Upload Offline Conversions", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: false, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: - "Google Ads customer ID (without dashes). Defaults to configured account.", - }, - conversions: { - type: "array", - description: "Array of offline conversions to upload.", - items: { - type: "object", - properties: { - gclid: { - type: "string", - description: "Google Click ID captured at the time of the ad click.", - }, - conversion_action: { - type: "string", - description: - 'Conversion action resource name (e.g., "customers/123/conversionActions/456") or just the action ID.', - }, - conversion_date_time: { - type: "string", - description: - 'Timestamp of the conversion in "yyyy-MM-dd HH:mm:ssZ" format (e.g., "2025-01-15 12:30:00-05:00").', - }, - conversion_value: { - type: "number", - description: "Monetary value of the conversion (optional).", - }, - currency_code: { - type: "string", - description: 'ISO 4217 currency code (e.g., "USD"). Defaults to account currency.', - }, - }, - required: ["gclid", "conversion_action", "conversion_date_time"], - }, - }, - }, - required: ["conversions"], - }, - _meta: { - labels: { - category: "conversions", - access: "delete", - complexity: "simple" - } - }, - handler: uploadOfflineConversions, - }, -]; - -export default tools; diff --git a/mcp-diagrams/google-ads-mcp/src/tools/index.ts b/mcp-diagrams/google-ads-mcp/src/tools/index.ts deleted file mode 100644 index a3d5d3d..0000000 --- a/mcp-diagrams/google-ads-mcp/src/tools/index.ts +++ /dev/null @@ -1,183 +0,0 @@ -// ============================================ -// TOOL REGISTRY — Lazy Loading Architecture -// ============================================ - -import type { ToolDefinition, ToolCategory } from "../types.js"; - -// Category metadata for lazy loading -export const TOOL_CATEGORIES: Record = { - accounts: { label: "Account Management", description: "List accounts, get account info, billing, hierarchy" }, - campaigns: { label: "Campaign Management", description: "Create, update, pause, enable, remove campaigns and budgets" }, - ad_groups: { label: "Ad Group Management", description: "Create, update, pause ad groups within campaigns" }, - ads: { label: "Ad Management", description: "Create and manage responsive search ads, display ads" }, - keywords: { label: "Keywords & Targeting", description: "Add/remove keywords, keyword ideas, negatives, match types" }, - reporting: { label: "Performance & Reporting", description: "Campaign/ad group/keyword metrics, search terms, geo/device reports" }, - bidding: { label: "Bidding & Budget", description: "Bidding strategies, bid recommendations, budget optimization" }, - conversions: { label: "Conversion Tracking", description: "Conversion actions, metrics, offline uploads" }, - advanced: { label: "Advanced / Utilities", description: "Raw GAQL queries, export, change history, labels" }, -}; - -// Core tools loaded on init (always available) -const CORE_TOOL_NAMES = new Set([ - "list_accessible_customers", - "list_campaigns", - "get_performance_summary", - "get_campaign_performance", - "run_gaql_query", - "export_report", - "get_keyword_ideas", - "list_recommendations", - "load_tools_category", -]); - -// Full tool registry (populated by category modules) -const allTools = new Map(); -const loadedCategories = new Set(); - -/** - * Register tools from a category module - */ -export function registerTools(tools: ToolDefinition[]): void { - for (const tool of tools) { - allTools.set(tool.name, tool); - } -} - -/** - * Get core tools (always loaded) - */ -export function getCoreTools(): ToolDefinition[] { - return Array.from(allTools.values()).filter((t) => CORE_TOOL_NAMES.has(t.name)); -} - -/** - * Get all currently loaded tools - */ -export function getAllLoadedTools(): ToolDefinition[] { - return Array.from(allTools.values()); -} - -/** - * Get a specific tool by name - */ -export function getTool(name: string): ToolDefinition | undefined { - return allTools.get(name); -} - -/** - * Load a tool category on demand - */ -export async function loadCategory(category: ToolCategory): Promise { - if (loadedCategories.has(category)) { - return Array.from(allTools.values()).filter((t) => t.category === category); - } - - // Dynamic import of category module - const moduleMap: Record Promise<{ default: ToolDefinition[] }>> = { - accounts: () => import("./accounts.js"), - campaigns: () => import("./campaigns.js"), - ad_groups: () => import("./ad-groups.js"), - ads: () => import("./ads.js"), - keywords: () => import("./keywords.js"), - reporting: () => import("./reporting.js"), - bidding: () => import("./bidding.js"), - conversions: () => import("./conversions.js"), - advanced: () => import("./advanced.js"), - }; - - const loader = moduleMap[category]; - if (!loader) throw new Error(`Unknown category: ${category}`); - - const mod = await loader(); - const tools = mod.default; - registerTools(tools); - loadedCategories.add(category); - - return tools; -} - -/** - * Check if a category has been loaded - */ -export function isCategoryLoaded(category: ToolCategory): boolean { - return loadedCategories.has(category); -} - -/** - * Get list of available categories with load status - */ -export function getCategoryStatus(): Array<{ category: ToolCategory; loaded: boolean; label: string; description: string }> { - return Object.entries(TOOL_CATEGORIES).map(([cat, meta]) => ({ - category: cat as ToolCategory, - loaded: loadedCategories.has(cat as ToolCategory), - label: meta.label, - description: meta.description, - })); -} - -// ============================================ -// META-TOOL: load_tools_category -// ============================================ -export const loadToolsCategoryTool: ToolDefinition = { - name: "load_tools_category", - description: - "Load additional Google Ads tools by category. Use this to access tools beyond the core set. " + - "Categories: accounts, campaigns, ad_groups, ads, keywords, reporting, bidding, conversions, advanced. " + - "Call with no category to see available categories and their status.", - category: "advanced", - annotations: { - title: "Load Tool Category", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - category: { - type: "string", - description: "Category to load. Omit to list available categories.", - enum: Object.keys(TOOL_CATEGORIES), - }, - }, - }, - handler: async (args) => { - if (!args.category) { - const status = getCategoryStatus(); - const lines = status.map( - (s) => `${s.loaded ? "✅" : "⬜"} **${s.label}** (\`${s.category}\`) — ${s.description}` - ); - return { - content: [ - { - type: "text", - text: `## Available Tool Categories\n\n${lines.join("\n")}\n\nUse \`load_tools_category\` with a category name to load its tools.`, - }, - ], - }; - } - - const category = args.category as ToolCategory; - const tools = await loadCategory(category); - const toolLines = tools.map((t) => { - const badges: string[] = []; - if (t.annotations.readOnlyHint) badges.push("📖"); - if (t.annotations.destructiveHint) badges.push("⚠️"); - if (t.annotations.openWorldHint) badges.push("🌐"); - return `- \`${t.name}\` ${badges.join("")} — ${t.description}`; - }); - - return { - content: [ - { - type: "text", - text: `## Loaded: ${TOOL_CATEGORIES[category].label}\n\n${toolLines.join("\n")}\n\n${tools.length} tools now available.`, - }, - ], - }; - }, -}; - -// Register the meta-tool -registerTools([loadToolsCategoryTool]); diff --git a/mcp-diagrams/google-ads-mcp/src/tools/keywords.ts b/mcp-diagrams/google-ads-mcp/src/tools/keywords.ts deleted file mode 100644 index ce0cd4a..0000000 --- a/mcp-diagrams/google-ads-mcp/src/tools/keywords.ts +++ /dev/null @@ -1,646 +0,0 @@ -// ============================================ -// KEYWORD & TARGETING TOOLS -// ============================================ - -import type { ToolDefinition, GoogleAdsClient, ToolResult } from "../types.js"; -import { microsToMoney, formatPercent, formatNumber } from "../types.js"; -import type { GoogleAdsRestClient } from "../client.js"; - -// ---- Helpers ---- - -function resolveCustomerId(args: Record, client: GoogleAdsClient): string { - const cid = args.customer_id || client.getCustomerId(); - if (!cid) throw new Error("customer_id is required — pass it explicitly or set GOOGLE_ADS_CUSTOMER_ID."); - return cid.replace(/-/g, ""); -} - -function textResult(text: string): ToolResult { - return { content: [{ type: "text", text }] }; -} - -function errorResult(message: string): ToolResult { - return { content: [{ type: "text", text: `❌ ${message}` }], isError: true }; -} - -function qualityScoreLabel(score: number | undefined): string { - if (score == null) return "—"; - if (score >= 8) return `${score}/10 🟢`; - if (score >= 5) return `${score}/10 🟡`; - return `${score}/10 🔴`; -} - -// ---- Tools ---- - -const tools: ToolDefinition[] = [ - // ============================ - // list_keywords - // ============================ - { - name: "list_keywords", - description: - "List keywords for an ad group with match type, quality score, status, bids, and performance metrics.", - category: "keywords", - annotations: { - title: "List Keywords", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - ad_group_id: { - type: "string", - description: "Ad group ID to list keywords for.", - }, - status_filter: { - type: "string", - description: "Filter by keyword status.", - enum: ["ENABLED", "PAUSED", "REMOVED"], - }, - limit: { - type: "number", - description: "Maximum keywords to return (default: 100).", - }, - }, - required: ["ad_group_id"], - }, - _meta: { - labels: { - category: "keywords", - access: "read", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - const limit = Math.min(args.limit || 100, 1000); - - let query = ` - SELECT - ad_group_criterion.criterion_id, - ad_group_criterion.keyword.text, - ad_group_criterion.keyword.match_type, - ad_group_criterion.status, - ad_group_criterion.effective_cpc_bid_micros, - ad_group_criterion.quality_info.quality_score, - ad_group_criterion.quality_info.creative_quality_score, - ad_group_criterion.quality_info.search_predicted_ctr, - ad_group_criterion.quality_info.post_click_quality_score, - ad_group_criterion.resource_name, - ad_group.id, - ad_group.name, - metrics.impressions, - metrics.clicks, - metrics.cost_micros, - metrics.conversions, - metrics.ctr, - metrics.average_cpc - FROM ad_group_criterion - WHERE ad_group.id = ${args.ad_group_id} - AND ad_group_criterion.type = 'KEYWORD' - AND segments.date DURING LAST_30_DAYS`; - - if (args.status_filter) { - query += `\n AND ad_group_criterion.status = '${args.status_filter}'`; - } - - query += `\n ORDER BY metrics.impressions DESC\n LIMIT ${limit}`; - - const rows = await client.query(cid, query); - - if (!rows.length) { - return textResult("No keywords found in this ad group."); - } - - const lines = rows.map((r: any) => { - const kw = r.adGroupCriterion || {}; - const keyword = kw.keyword || {}; - const qi = kw.qualityInfo || {}; - const m = r.metrics || {}; - - const matchSymbol: Record = { - BROAD: "🅱️", - PHRASE: "🅿️", - EXACT: "🎯", - }; - - return [ - `${matchSymbol[keyword.matchType] || "❓"} **${keyword.text || "—"}** (${keyword.matchType || "—"})`, - ` ID: ${kw.criterionId} | Status: ${kw.status || "—"} | Quality: ${qualityScoreLabel(qi.qualityScore)}`, - ` CPC Bid: ${kw.effectiveCpcBidMicros ? "$" + microsToMoney(kw.effectiveCpcBidMicros) : "auto"}`, - ` Predicted CTR: ${qi.searchPredictedCtr || "—"} | Ad Relevance: ${qi.creativeQualityScore || "—"} | Landing: ${qi.postClickQualityScore || "—"}`, - ` Impressions: ${formatNumber(m.impressions || 0)} | Clicks: ${formatNumber(m.clicks || 0)} | CTR: ${formatPercent(m.ctr || 0)} | Cost: $${microsToMoney(m.costMicros || 0)} | Conv: ${formatNumber(m.conversions || 0)}`, - ].join("\n"); - }); - - return textResult( - `## Keywords in Ad Group ${args.ad_group_id}\n\n${lines.join("\n\n")}\n\n_${rows.length} keyword(s) returned._` - ); - }, - }, - - // ============================ - // add_keywords - // ============================ - { - name: "add_keywords", - description: - "Add keywords to an ad group with specified match types. " + - "⚠️ This adds live keywords that may trigger ad serving immediately. " + - "Supports BROAD, PHRASE, and EXACT match types.", - category: "keywords", - annotations: { - title: "Add Keywords", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: false, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - ad_group_id: { - type: "string", - description: "Ad group ID to add keywords to.", - }, - keywords: { - type: "array", - items: { - type: "object", - properties: { - text: { type: "string", description: "Keyword text." }, - match_type: { - type: "string", - description: "Match type: BROAD, PHRASE, or EXACT.", - enum: ["BROAD", "PHRASE", "EXACT"], - }, - }, - required: ["text", "match_type"], - }, - description: "Array of keywords to add, each with text and match_type.", - }, - cpc_bid_amount: { - type: "number", - description: "Optional CPC bid override in dollars for all keywords. Uses ad group default if omitted.", - }, - status: { - type: "string", - description: "Initial keyword status. Default: ENABLED.", - enum: ["ENABLED", "PAUSED"], - }, - }, - required: ["ad_group_id", "keywords"], - }, - _meta: { - labels: { - category: "keywords", - access: "delete", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - - if (!args.keywords?.length) { - return errorResult("At least one keyword is required."); - } - - const validMatchTypes = new Set(["BROAD", "PHRASE", "EXACT"]); - for (const kw of args.keywords) { - if (!kw.text?.trim()) { - return errorResult("Each keyword must have non-empty text."); - } - if (!validMatchTypes.has(kw.match_type)) { - return errorResult(`Invalid match type "${kw.match_type}" for keyword "${kw.text}". Use BROAD, PHRASE, or EXACT.`); - } - } - - const operations = args.keywords.map((kw: { text: string; match_type: string }) => { - const resource: Record = { - adGroup: `customers/${cid}/adGroups/${args.ad_group_id}`, - status: args.status || "ENABLED", - keyword: { - text: kw.text.trim(), - matchType: kw.match_type, - }, - }; - - if (args.cpc_bid_amount != null) { - resource.cpcBidMicros = String(Math.round(args.cpc_bid_amount * 1_000_000)); - } - - return { - entity: "adGroupCriterion", - operation: "create" as const, - resource, - }; - }); - - const result = await client.mutate(cid, operations); - - const kwSummary = args.keywords.map((kw: any) => ` - "${kw.text}" (${kw.match_type})`).join("\n"); - - return textResult( - `✅ Added ${args.keywords.length} keyword(s) to ad group ${args.ad_group_id}:\n\n${kwSummary}\n\n` + - `- CPC Bid: ${args.cpc_bid_amount != null ? "$" + args.cpc_bid_amount.toFixed(2) : "ad group default"}\n` + - `- Status: ${args.status || "ENABLED"}` - ); - }, - }, - - // ============================ - // remove_keywords - // ============================ - { - name: "remove_keywords", - description: - "Remove keywords from an ad group by their criterion IDs. " + - "⚠️ This permanently removes keywords — they cannot be recovered. Consider pausing instead if unsure.", - category: "keywords", - annotations: { - title: "Remove Keywords", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - ad_group_id: { - type: "string", - description: "Ad group ID the keywords belong to.", - }, - criterion_ids: { - type: "array", - items: { type: "string" }, - description: "Array of keyword criterion IDs to remove.", - }, - }, - required: ["ad_group_id", "criterion_ids"], - }, - _meta: { - labels: { - category: "keywords", - access: "delete", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - - if (!args.criterion_ids?.length) { - return errorResult("At least one criterion_id is required."); - } - - const operations = args.criterion_ids.map((criterionId: string) => ({ - entity: "adGroupCriterion", - operation: "remove" as const, - resourceName: `customers/${cid}/adGroupCriteria/${args.ad_group_id}~${criterionId}`, - })); - - await client.mutate(cid, operations); - - return textResult( - `🗑️ Removed ${args.criterion_ids.length} keyword(s) from ad group ${args.ad_group_id}.\n\n` + - `Criterion IDs: ${args.criterion_ids.join(", ")}` - ); - }, - }, - - // ============================ - // get_keyword_ideas - // ============================ - { - name: "get_keyword_ideas", - description: - "Generate keyword suggestions from seed keywords or a URL. Returns search volume, competition, and CPC estimates. " + - "Useful for keyword research and discovering new opportunities.", - category: "keywords", - annotations: { - title: "Get Keyword Ideas", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: true, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - seed_keywords: { - type: "array", - items: { type: "string" }, - description: "Seed keywords to base suggestions on.", - }, - seed_url: { - type: "string", - description: "URL to scan for keyword ideas.", - }, - language: { - type: "string", - description: "Language constant (e.g. 'languageConstants/1000' for English). Default: English.", - }, - geo_targets: { - type: "array", - items: { type: "string" }, - description: "Geo target constants (e.g. ['geoTargetConstants/2840'] for US). Default: US.", - }, - limit: { - type: "number", - description: "Maximum number of ideas to return (default: 20).", - }, - }, - }, - _meta: { - labels: { - category: "keywords", - access: "read", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - - if (!args.seed_keywords?.length && !args.seed_url) { - return errorResult("Provide at least seed_keywords or seed_url for keyword ideas."); - } - - // Cast to GoogleAdsRestClient to access generateKeywordIdeas - const restClient = client as GoogleAdsRestClient; - const results = await restClient.generateKeywordIdeas(cid, { - seedKeywords: args.seed_keywords, - seedUrl: args.seed_url, - language: args.language, - geoTargets: args.geo_targets, - }); - - const limit = Math.min(args.limit || 20, 100); - const ideas = results.slice(0, limit); - - if (!ideas.length) { - return textResult("No keyword ideas found for the given seeds. Try different keywords or a more specific URL."); - } - - const lines = ideas.map((idea: any, i: number) => { - const kw = idea.text || idea.keywordIdeaMetrics?.text || "—"; - const m = idea.keywordIdeaMetrics || {}; - const avgSearch = m.avgMonthlySearches != null ? formatNumber(m.avgMonthlySearches) : "—"; - const competition = m.competition || "—"; - const lowCpc = m.lowTopOfPageBidMicros ? "$" + microsToMoney(m.lowTopOfPageBidMicros) : "—"; - const highCpc = m.highTopOfPageBidMicros ? "$" + microsToMoney(m.highTopOfPageBidMicros) : "—"; - - return `${i + 1}. **${kw}** — ${avgSearch} avg searches/mo | Competition: ${competition} | CPC: ${lowCpc} – ${highCpc}`; - }); - - const seeds = args.seed_keywords?.length - ? `Seeds: ${args.seed_keywords.join(", ")}` - : `URL: ${args.seed_url}`; - - return textResult( - `## Keyword Ideas\n${seeds}\n\n${lines.join("\n")}\n\n_${ideas.length} idea(s) shown._` - ); - }, - }, - - // ============================ - // get_keyword_metrics - // ============================ - { - name: "get_keyword_metrics", - description: - "Get search volume, competition level, and CPC estimates for specific keywords. " + - "Useful for evaluating keywords before adding them to campaigns.", - category: "keywords", - annotations: { - title: "Get Keyword Metrics", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: true, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - keywords: { - type: "array", - items: { type: "string" }, - description: "Keywords to get metrics for.", - }, - language: { - type: "string", - description: "Language constant. Default: English (languageConstants/1000).", - }, - geo_targets: { - type: "array", - items: { type: "string" }, - description: "Geo target constants. Default: US (geoTargetConstants/2840).", - }, - }, - required: ["keywords"], - }, - _meta: { - labels: { - category: "keywords", - access: "read", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - - if (!args.keywords?.length) { - return errorResult("At least one keyword is required."); - } - - // Use generateKeywordIdeas with the exact keywords as seeds - const restClient = client as GoogleAdsRestClient; - const results = await restClient.generateKeywordIdeas(cid, { - seedKeywords: args.keywords, - language: args.language, - geoTargets: args.geo_targets, - }); - - // Filter results to only the requested keywords (case-insensitive match) - const requested = new Set(args.keywords.map((k: string) => k.toLowerCase())); - const matched = results.filter((idea: any) => { - const text = (idea.text || "").toLowerCase(); - return requested.has(text); - }); - - // If exact matches aren't found, show top results as approximations - const display = matched.length ? matched : results.slice(0, args.keywords.length); - - if (!display.length) { - return textResult("No metrics available for the given keywords."); - } - - const header = `| Keyword | Avg Searches/Mo | Competition | Low CPC | High CPC |`; - const divider = `|---------|-----------------|-------------|---------|----------|`; - - const rows = display.map((idea: any) => { - const kw = idea.text || "—"; - const m = idea.keywordIdeaMetrics || {}; - const avgSearch = m.avgMonthlySearches != null ? formatNumber(m.avgMonthlySearches) : "—"; - const competition = m.competition || "—"; - const lowCpc = m.lowTopOfPageBidMicros ? "$" + microsToMoney(m.lowTopOfPageBidMicros) : "—"; - const highCpc = m.highTopOfPageBidMicros ? "$" + microsToMoney(m.highTopOfPageBidMicros) : "—"; - return `| ${kw} | ${avgSearch} | ${competition} | ${lowCpc} | ${highCpc} |`; - }); - - return textResult( - `## Keyword Metrics\n\n${header}\n${divider}\n${rows.join("\n")}\n\n_${display.length} keyword(s)._` - ); - }, - }, - - // ============================ - // list_negative_keywords - // ============================ - { - name: "list_negative_keywords", - description: - "List negative keywords at both campaign and ad group level. Shows match type and scope.", - category: "keywords", - annotations: { - title: "List Negative Keywords", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID (defaults to configured account).", - }, - campaign_id: { - type: "string", - description: "Campaign ID to list negatives for. If omitted, lists all.", - }, - ad_group_id: { - type: "string", - description: "Ad group ID to list negatives for (ad group level only).", - }, - limit: { - type: "number", - description: "Maximum negatives to return (default: 200).", - }, - }, - }, - _meta: { - labels: { - category: "keywords", - access: "read", - complexity: "simple" - } - }, - handler: async (args, client) => { - const cid = resolveCustomerId(args, client); - const limit = Math.min(args.limit || 200, 1000); - const sections: string[] = ["## Negative Keywords\n"]; - - // Campaign-level negatives - if (!args.ad_group_id) { - let campaignQuery = ` - SELECT - campaign_criterion.criterion_id, - campaign_criterion.keyword.text, - campaign_criterion.keyword.match_type, - campaign_criterion.negative, - campaign.id, - campaign.name - FROM campaign_criterion - WHERE campaign_criterion.type = 'KEYWORD' - AND campaign_criterion.negative = TRUE`; - - if (args.campaign_id) { - campaignQuery += `\n AND campaign.id = ${args.campaign_id}`; - } - - campaignQuery += `\n LIMIT ${limit}`; - - const campaignRows = await client.query(cid, campaignQuery); - - if (campaignRows.length) { - sections.push(`### Campaign Level (${campaignRows.length})\n`); - campaignRows.forEach((r: any) => { - const cc = r.campaignCriterion || {}; - const kw = cc.keyword || {}; - const camp = r.campaign || {}; - sections.push(`- **${kw.text || "—"}** (${kw.matchType || "—"}) — Campaign: ${camp.name || camp.id || "—"} [ID: ${cc.criterionId}]`); - }); - sections.push(""); - } else { - sections.push("_No campaign-level negative keywords found._\n"); - } - } - - // Ad group-level negatives - { - let agQuery = ` - SELECT - ad_group_criterion.criterion_id, - ad_group_criterion.keyword.text, - ad_group_criterion.keyword.match_type, - ad_group_criterion.negative, - ad_group.id, - ad_group.name, - campaign.id, - campaign.name - FROM ad_group_criterion - WHERE ad_group_criterion.type = 'KEYWORD' - AND ad_group_criterion.negative = TRUE`; - - if (args.ad_group_id) { - agQuery += `\n AND ad_group.id = ${args.ad_group_id}`; - } else if (args.campaign_id) { - agQuery += `\n AND campaign.id = ${args.campaign_id}`; - } - - agQuery += `\n LIMIT ${limit}`; - - const agRows = await client.query(cid, agQuery); - - if (agRows.length) { - sections.push(`### Ad Group Level (${agRows.length})\n`); - agRows.forEach((r: any) => { - const agc = r.adGroupCriterion || {}; - const kw = agc.keyword || {}; - const ag = r.adGroup || {}; - sections.push(`- **${kw.text || "—"}** (${kw.matchType || "—"}) — Ad Group: ${ag.name || ag.id || "—"} [ID: ${agc.criterionId}]`); - }); - } else { - sections.push("_No ad group-level negative keywords found._"); - } - } - - return textResult(sections.join("\n")); - }, - }, -]; - -export default tools; diff --git a/mcp-diagrams/google-ads-mcp/src/tools/reporting.ts b/mcp-diagrams/google-ads-mcp/src/tools/reporting.ts deleted file mode 100644 index fc2fa7f..0000000 --- a/mcp-diagrams/google-ads-mcp/src/tools/reporting.ts +++ /dev/null @@ -1,887 +0,0 @@ -// ============================================ -// PERFORMANCE & REPORTING TOOLS -// ============================================ - -import type { ToolDefinition, ToolResult, GoogleAdsClient } from "../types.js"; -import { microsToMoney, formatPercent, formatNumber } from "../types.js"; - -function text(t: string): ToolResult { - return { content: [{ type: "text", text: t }] }; -} - -function errorResult(msg: string): ToolResult { - return { content: [{ type: "text", text: `❌ ${msg}` }], isError: true }; -} - -const DATE_RANGE_ENUM = [ - "TODAY", "YESTERDAY", "LAST_7_DAYS", "LAST_30_DAYS", - "THIS_MONTH", "LAST_MONTH", "THIS_QUARTER", "LAST_QUARTER", -]; - -/** Compute derived metrics from raw values */ -function derivedMetrics(m: any) { - const impressions = Number(m.impressions || 0); - const clicks = Number(m.clicks || 0); - const costMicros = Number(m.costMicros || 0); - const conversions = Number(m.conversions || 0); - - const cost = costMicros / 1_000_000; - const ctr = impressions > 0 ? clicks / impressions : 0; - const avgCpc = clicks > 0 ? cost / clicks : 0; - const convRate = clicks > 0 ? conversions / clicks : 0; - const costPerConv = conversions > 0 ? cost / conversions : 0; - - return { impressions, clicks, cost, costMicros, conversions, ctr, avgCpc, convRate, costPerConv }; -} - -// ─── get_performance_summary ──────────────────────────────────────── -const getPerformanceSummary: ToolDefinition = { - name: "get_performance_summary", - description: - "Get account-level performance KPIs: impressions, clicks, cost, conversions, CTR, " + - "average CPC, and conversion rate. Provides a high-level overview of account health.", - category: "reporting", - annotations: { - title: "Get Performance Summary", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID. Uses default if not provided.", - }, - date_range: { - type: "string", - description: "Date range for metrics.", - enum: DATE_RANGE_ENUM, - default: "LAST_30_DAYS", - }, - }, - }, - _meta: { - labels: { - category: "reporting", - access: "read", - complexity: "simple" - } - }, - handler: async (args: Record, client: GoogleAdsClient): Promise => { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) return errorResult("No customer ID provided and no default configured."); - - const dateRange = args.date_range || "LAST_30_DAYS"; - - const query = ` - SELECT - metrics.impressions, - metrics.clicks, - metrics.cost_micros, - metrics.conversions, - metrics.conversions_value, - metrics.ctr, - metrics.average_cpc, - metrics.all_conversions, - metrics.interactions - FROM customer - WHERE segments.date DURING ${dateRange} - `; - - const results = await client.query(customerId, query); - if (!results.length) return text(`No performance data available for ${dateRange}.`); - - const m = results[0].metrics || {}; - const d = derivedMetrics(m); - - return text( - `## 📊 Account Performance Summary\n\n` + - `**Account:** \`${customerId}\` · **Period:** ${dateRange}\n\n` + - `| KPI | Value |\n` + - `|-----|-------|\n` + - `| **Impressions** | ${formatNumber(d.impressions)} |\n` + - `| **Clicks** | ${formatNumber(d.clicks)} |\n` + - `| **CTR** | ${formatPercent(d.ctr)} |\n` + - `| **Cost** | $${d.cost.toFixed(2)} |\n` + - `| **Avg. CPC** | $${d.avgCpc.toFixed(2)} |\n` + - `| **Conversions** | ${d.conversions.toFixed(1)} |\n` + - `| **Conv. Rate** | ${formatPercent(d.convRate)} |\n` + - `| **Cost / Conv.** | $${d.costPerConv.toFixed(2)} |\n` + - `| **Conv. Value** | $${Number(m.conversionsValue || 0).toFixed(2)} |\n` + - `| **All Conversions** | ${Number(m.allConversions || 0).toFixed(1)} |\n` + - `| **Interactions** | ${formatNumber(m.interactions || 0)} |` - ); - }, -}; - -// ─── get_campaign_performance ─────────────────────────────────────── -const getCampaignPerformance: ToolDefinition = { - name: "get_campaign_performance", - description: - "Get campaign-level performance metrics. Can report on all campaigns or a specific one. " + - "Includes impressions, clicks, cost, conversions, CTR, CPC, and conversion rate.", - category: "reporting", - annotations: { - title: "Get Campaign Performance", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID. Uses default if not provided.", - }, - campaign_id: { - type: "string", - description: "Specific campaign ID to report on. Omit for all campaigns.", - }, - date_range: { - type: "string", - description: "Date range for metrics.", - enum: DATE_RANGE_ENUM, - default: "LAST_30_DAYS", - }, - limit: { - type: "number", - description: "Max campaigns to return (default 25).", - default: 25, - }, - order_by: { - type: "string", - description: "Metric to sort by (descending).", - enum: ["cost", "clicks", "impressions", "conversions", "ctr"], - default: "cost", - }, - }, - }, - _meta: { - labels: { - category: "reporting", - access: "read", - complexity: "simple" - } - }, - handler: async (args: Record, client: GoogleAdsClient): Promise => { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) return errorResult("No customer ID provided and no default configured."); - - const dateRange = args.date_range || "LAST_30_DAYS"; - const limit = args.limit || 25; - - const orderMap: Record = { - cost: "metrics.cost_micros", - clicks: "metrics.clicks", - impressions: "metrics.impressions", - conversions: "metrics.conversions", - ctr: "metrics.ctr", - }; - const orderBy = orderMap[args.order_by || "cost"] || "metrics.cost_micros"; - - const conditions = [`segments.date DURING ${dateRange}`]; - if (args.campaign_id) { - conditions.push(`campaign.id = ${args.campaign_id}`); - } - conditions.push(`campaign.status != 'REMOVED'`); - - const query = ` - SELECT - campaign.id, - campaign.name, - campaign.status, - campaign.advertising_channel_type, - metrics.impressions, - metrics.clicks, - metrics.cost_micros, - metrics.conversions, - metrics.conversions_value, - metrics.ctr, - metrics.average_cpc - FROM campaign - WHERE ${conditions.join(" AND ")} - ORDER BY ${orderBy} DESC - LIMIT ${limit} - `; - - const results = await client.query(customerId, query); - if (!results.length) return text("No campaign performance data available for this period."); - - const header = - `| Campaign | Status | Type | Impr. | Clicks | CTR | Cost | Avg CPC | Conv. | Conv Rate |\n` + - `|----------|--------|------|-------|--------|-----|------|---------|-------|-----------| `; - - const rows = results.map((r: any) => { - const c = r.campaign; - const m = r.metrics || {}; - const d = derivedMetrics(m); - return ( - `| ${c.name || "—"} | ${c.status} | ${c.advertisingChannelType || "—"} | ` + - `${formatNumber(d.impressions)} | ${formatNumber(d.clicks)} | ${formatPercent(d.ctr)} | ` + - `$${d.cost.toFixed(2)} | $${d.avgCpc.toFixed(2)} | ` + - `${d.conversions.toFixed(1)} | ${formatPercent(d.convRate)} |` - ); - }); - - // Totals row - const totals = results.reduce( - (acc: any, r: any) => { - const m = r.metrics || {}; - acc.impressions += Number(m.impressions || 0); - acc.clicks += Number(m.clicks || 0); - acc.costMicros += Number(m.costMicros || 0); - acc.conversions += Number(m.conversions || 0); - return acc; - }, - { impressions: 0, clicks: 0, costMicros: 0, conversions: 0 } - ); - const tCost = totals.costMicros / 1_000_000; - const tCtr = totals.impressions > 0 ? totals.clicks / totals.impressions : 0; - const tCpc = totals.clicks > 0 ? tCost / totals.clicks : 0; - const tConvRate = totals.clicks > 0 ? totals.conversions / totals.clicks : 0; - - const totalsRow = - `| **TOTAL** | — | — | ${formatNumber(totals.impressions)} | ${formatNumber(totals.clicks)} | ` + - `${formatPercent(tCtr)} | $${tCost.toFixed(2)} | $${tCpc.toFixed(2)} | ` + - `${totals.conversions.toFixed(1)} | ${formatPercent(tConvRate)} |`; - - return text( - `## 📈 Campaign Performance\n\n` + - `**Account:** \`${customerId}\` · **Period:** ${dateRange} · **Sorted by:** ${args.order_by || "cost"}\n\n` + - `${header}\n${rows.join("\n")}\n${totalsRow}` - ); - }, -}; - -// ─── get_ad_group_performance ─────────────────────────────────────── -const getAdGroupPerformance: ToolDefinition = { - name: "get_ad_group_performance", - description: - "Get ad group-level performance metrics. Optionally filter by campaign ID. " + - "Shows impressions, clicks, cost, conversions, CTR, and CPC per ad group.", - category: "reporting", - annotations: { - title: "Get Ad Group Performance", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID. Uses default if not provided.", - }, - campaign_id: { - type: "string", - description: "Filter by campaign ID. Omit for all campaigns.", - }, - date_range: { - type: "string", - description: "Date range for metrics.", - enum: DATE_RANGE_ENUM, - default: "LAST_30_DAYS", - }, - limit: { - type: "number", - description: "Max ad groups to return (default 50).", - default: 50, - }, - }, - }, - _meta: { - labels: { - category: "reporting", - access: "read", - complexity: "simple" - } - }, - handler: async (args: Record, client: GoogleAdsClient): Promise => { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) return errorResult("No customer ID provided and no default configured."); - - const dateRange = args.date_range || "LAST_30_DAYS"; - const limit = args.limit || 50; - - const conditions = [ - `segments.date DURING ${dateRange}`, - `ad_group.status != 'REMOVED'`, - ]; - if (args.campaign_id) { - conditions.push(`campaign.id = ${args.campaign_id}`); - } - - const query = ` - SELECT - ad_group.id, - ad_group.name, - ad_group.status, - ad_group.type, - campaign.id, - campaign.name, - metrics.impressions, - metrics.clicks, - metrics.cost_micros, - metrics.conversions, - metrics.ctr, - metrics.average_cpc - FROM ad_group - WHERE ${conditions.join(" AND ")} - ORDER BY metrics.cost_micros DESC - LIMIT ${limit} - `; - - const results = await client.query(customerId, query); - if (!results.length) return text("No ad group performance data available for this period."); - - const header = - `| Ad Group | Campaign | Status | Type | Impr. | Clicks | CTR | Cost | CPC | Conv. |\n` + - `|----------|----------|--------|------|-------|--------|-----|------|-----|-------|`; - - const rows = results.map((r: any) => { - const ag = r.adGroup; - const c = r.campaign; - const m = r.metrics || {}; - const d = derivedMetrics(m); - return ( - `| ${ag.name || "—"} | ${c.name || "—"} | ${ag.status} | ${ag.type || "—"} | ` + - `${formatNumber(d.impressions)} | ${formatNumber(d.clicks)} | ${formatPercent(d.ctr)} | ` + - `$${d.cost.toFixed(2)} | $${d.avgCpc.toFixed(2)} | ${d.conversions.toFixed(1)} |` - ); - }); - - return text( - `## 📊 Ad Group Performance\n\n` + - `**Account:** \`${customerId}\` · **Period:** ${dateRange}` + - `${args.campaign_id ? ` · **Campaign:** \`${args.campaign_id}\`` : ""}\n\n` + - `${header}\n${rows.join("\n")}\n\n` + - `Showing ${results.length} ad group(s), sorted by cost (descending).` - ); - }, -}; - -// ─── get_keyword_performance ──────────────────────────────────────── -const getKeywordPerformance: ToolDefinition = { - name: "get_keyword_performance", - description: - "Get keyword-level performance metrics with quality scores. Shows keyword text, match type, " + - "quality score components, impressions, clicks, cost, and conversions.", - category: "reporting", - annotations: { - title: "Get Keyword Performance", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID. Uses default if not provided.", - }, - campaign_id: { - type: "string", - description: "Filter by campaign ID.", - }, - ad_group_id: { - type: "string", - description: "Filter by ad group ID.", - }, - date_range: { - type: "string", - description: "Date range for metrics.", - enum: DATE_RANGE_ENUM, - default: "LAST_30_DAYS", - }, - limit: { - type: "number", - description: "Max keywords to return (default 50).", - default: 50, - }, - }, - }, - _meta: { - labels: { - category: "reporting", - access: "read", - complexity: "simple" - } - }, - handler: async (args: Record, client: GoogleAdsClient): Promise => { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) return errorResult("No customer ID provided and no default configured."); - - const dateRange = args.date_range || "LAST_30_DAYS"; - const limit = args.limit || 50; - - const conditions = [ - `segments.date DURING ${dateRange}`, - `ad_group_criterion.status != 'REMOVED'`, - `ad_group_criterion.type = 'KEYWORD'`, - ]; - if (args.campaign_id) conditions.push(`campaign.id = ${args.campaign_id}`); - if (args.ad_group_id) conditions.push(`ad_group.id = ${args.ad_group_id}`); - - const query = ` - SELECT - ad_group_criterion.criterion_id, - ad_group_criterion.keyword.text, - ad_group_criterion.keyword.match_type, - ad_group_criterion.status, - ad_group_criterion.quality_info.quality_score, - ad_group_criterion.quality_info.creative_quality_score, - ad_group_criterion.quality_info.search_predicted_ctr, - ad_group_criterion.quality_info.post_click_quality_score, - ad_group.name, - campaign.name, - metrics.impressions, - metrics.clicks, - metrics.cost_micros, - metrics.conversions, - metrics.ctr, - metrics.average_cpc - FROM keyword_view - WHERE ${conditions.join(" AND ")} - ORDER BY metrics.cost_micros DESC - LIMIT ${limit} - `; - - const results = await client.query(customerId, query); - if (!results.length) return text("No keyword performance data available for this period."); - - const header = - `| Keyword | Match | QS | Status | Impr. | Clicks | CTR | Cost | CPC | Conv. |\n` + - `|---------|-------|----|--------|-------|--------|-----|------|-----|-------|`; - - const rows = results.map((r: any) => { - const kw = r.adGroupCriterion; - const m = r.metrics || {}; - const d = derivedMetrics(m); - const qs = kw.qualityInfo?.qualityScore ?? "—"; - return ( - `| ${kw.keyword?.text || "—"} | ${kw.keyword?.matchType || "—"} | ${qs} | ${kw.status} | ` + - `${formatNumber(d.impressions)} | ${formatNumber(d.clicks)} | ${formatPercent(d.ctr)} | ` + - `$${d.cost.toFixed(2)} | $${d.avgCpc.toFixed(2)} | ${d.conversions.toFixed(1)} |` - ); - }); - - // Quality score breakdown for top keywords - const qsDetails = results - .filter((r: any) => r.adGroupCriterion.qualityInfo?.qualityScore != null) - .slice(0, 10) - .map((r: any) => { - const kw = r.adGroupCriterion; - const qi = kw.qualityInfo || {}; - return ( - `| ${kw.keyword?.text || "—"} | ${qi.qualityScore ?? "—"} | ` + - `${qi.searchPredictedCtr || "—"} | ${qi.creativeQualityScore || "—"} | ` + - `${qi.postClickQualityScore || "—"} |` - ); - }); - - let qsSection = ""; - if (qsDetails.length) { - qsSection = - `\n\n### Quality Score Breakdown (Top 10)\n\n` + - `| Keyword | QS | Expected CTR | Ad Relevance | Landing Page |\n` + - `|---------|----|--------------|--------------|--------------|\n` + - qsDetails.join("\n"); - } - - return text( - `## 🔑 Keyword Performance\n\n` + - `**Account:** \`${customerId}\` · **Period:** ${dateRange}\n\n` + - `${header}\n${rows.join("\n")}${qsSection}\n\n` + - `Showing ${results.length} keyword(s), sorted by cost (descending).` - ); - }, -}; - -// ─── get_search_terms_report ──────────────────────────────────────── -const getSearchTermsReport: ToolDefinition = { - name: "get_search_terms_report", - description: - "Get the actual search terms that triggered your ads. Shows what users typed to trigger " + - "your ads, which keywords matched, and performance for each search term. Useful for " + - "discovering new keywords and finding negatives.", - category: "reporting", - annotations: { - title: "Get Search Terms Report", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: true, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID. Uses default if not provided.", - }, - campaign_id: { - type: "string", - description: "Filter by campaign ID.", - }, - ad_group_id: { - type: "string", - description: "Filter by ad group ID.", - }, - date_range: { - type: "string", - description: "Date range for report.", - enum: DATE_RANGE_ENUM, - default: "LAST_30_DAYS", - }, - limit: { - type: "number", - description: "Max search terms to return (default 50).", - default: 50, - }, - }, - }, - _meta: { - labels: { - category: "reporting", - access: "read", - complexity: "complex" - } - }, - handler: async (args: Record, client: GoogleAdsClient): Promise => { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) return errorResult("No customer ID provided and no default configured."); - - const dateRange = args.date_range || "LAST_30_DAYS"; - const limit = args.limit || 50; - - const conditions = [`segments.date DURING ${dateRange}`]; - if (args.campaign_id) conditions.push(`campaign.id = ${args.campaign_id}`); - if (args.ad_group_id) conditions.push(`ad_group.id = ${args.ad_group_id}`); - - const query = ` - SELECT - search_term_view.search_term, - search_term_view.status, - campaign.id, - campaign.name, - ad_group.id, - ad_group.name, - metrics.impressions, - metrics.clicks, - metrics.cost_micros, - metrics.conversions, - metrics.ctr - FROM search_term_view - WHERE ${conditions.join(" AND ")} - ORDER BY metrics.impressions DESC - LIMIT ${limit} - `; - - const results = await client.query(customerId, query); - if (!results.length) return text("No search term data available for this period."); - - const header = - `| Search Term | Campaign | Ad Group | Status | Impr. | Clicks | CTR | Cost | Conv. |\n` + - `|-------------|----------|----------|--------|-------|--------|-----|------|-------|`; - - const rows = results.map((r: any) => { - const st = r.searchTermView; - const c = r.campaign; - const ag = r.adGroup; - const m = r.metrics || {}; - const d = derivedMetrics(m); - return ( - `| ${st.searchTerm || "—"} | ${c.name || "—"} | ${ag.name || "—"} | ` + - `${st.status || "—"} | ${formatNumber(d.impressions)} | ${formatNumber(d.clicks)} | ` + - `${formatPercent(d.ctr)} | $${d.cost.toFixed(2)} | ${d.conversions.toFixed(1)} |` - ); - }); - - // Summary stats - const totals = results.reduce( - (acc: any, r: any) => { - const m = r.metrics || {}; - acc.impressions += Number(m.impressions || 0); - acc.clicks += Number(m.clicks || 0); - acc.cost += Number(m.costMicros || 0) / 1_000_000; - acc.conversions += Number(m.conversions || 0); - return acc; - }, - { impressions: 0, clicks: 0, cost: 0, conversions: 0 } - ); - - return text( - `## 🔍 Search Terms Report\n\n` + - `**Account:** \`${customerId}\` · **Period:** ${dateRange}\n\n` + - `**Summary:** ${formatNumber(totals.impressions)} impressions, ${formatNumber(totals.clicks)} clicks, ` + - `$${totals.cost.toFixed(2)} cost, ${totals.conversions.toFixed(1)} conversions across ${results.length} search terms\n\n` + - `${header}\n${rows.join("\n")}\n\n` + - `💡 **Tip:** Look for high-impression, low-conversion terms to add as negative keywords. ` + - `High-converting terms can be added as exact-match keywords.` - ); - }, -}; - -// ─── get_geo_performance ──────────────────────────────────────────── -const getGeoPerformance: ToolDefinition = { - name: "get_geo_performance", - description: - "Get performance metrics broken down by geographic location. Shows which countries, " + - "regions, or cities are driving traffic and conversions.", - category: "reporting", - annotations: { - title: "Get Geographic Performance", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID. Uses default if not provided.", - }, - campaign_id: { - type: "string", - description: "Filter by campaign ID.", - }, - date_range: { - type: "string", - description: "Date range for metrics.", - enum: DATE_RANGE_ENUM, - default: "LAST_30_DAYS", - }, - limit: { - type: "number", - description: "Max locations to return (default 30).", - default: 30, - }, - }, - }, - _meta: { - labels: { - category: "reporting", - access: "read", - complexity: "simple" - } - }, - handler: async (args: Record, client: GoogleAdsClient): Promise => { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) return errorResult("No customer ID provided and no default configured."); - - const dateRange = args.date_range || "LAST_30_DAYS"; - const limit = args.limit || 30; - - const conditions = [`segments.date DURING ${dateRange}`]; - if (args.campaign_id) conditions.push(`campaign.id = ${args.campaign_id}`); - - const query = ` - SELECT - geographic_view.country_criterion_id, - geographic_view.location_type, - campaign.name, - segments.geo_target_country, - segments.geo_target_region, - segments.geo_target_city, - metrics.impressions, - metrics.clicks, - metrics.cost_micros, - metrics.conversions, - metrics.ctr, - metrics.average_cpc - FROM geographic_view - WHERE ${conditions.join(" AND ")} - ORDER BY metrics.cost_micros DESC - LIMIT ${limit} - `; - - const results = await client.query(customerId, query); - if (!results.length) return text("No geographic performance data available for this period."); - - const header = - `| Location | Type | Campaign | Impr. | Clicks | CTR | Cost | CPC | Conv. | Conv Rate |\n` + - `|----------|------|----------|-------|--------|-----|------|-----|-------|-----------|`; - - const rows = results.map((r: any) => { - const geo = r.geographicView; - const seg = r.segments || {}; - const c = r.campaign; - const m = r.metrics || {}; - const d = derivedMetrics(m); - - // Build location name from available segments - const locationParts = [ - seg.geoTargetCity, - seg.geoTargetRegion, - seg.geoTargetCountry, - ].filter(Boolean); - const location = locationParts.length > 0 ? locationParts.join(", ") : `ID: ${geo.countryCriterionId || "—"}`; - - return ( - `| ${location} | ${geo.locationType || "—"} | ${c.name || "—"} | ` + - `${formatNumber(d.impressions)} | ${formatNumber(d.clicks)} | ${formatPercent(d.ctr)} | ` + - `$${d.cost.toFixed(2)} | $${d.avgCpc.toFixed(2)} | ` + - `${d.conversions.toFixed(1)} | ${formatPercent(d.convRate)} |` - ); - }); - - return text( - `## 🌍 Geographic Performance\n\n` + - `**Account:** \`${customerId}\` · **Period:** ${dateRange}\n\n` + - `${header}\n${rows.join("\n")}\n\n` + - `Showing top ${results.length} location(s) by cost.` - ); - }, -}; - -// ─── get_device_performance ───────────────────────────────────────── -const getDevicePerformance: ToolDefinition = { - name: "get_device_performance", - description: - "Get performance metrics broken down by device type (desktop, mobile, tablet). " + - "Helps identify which devices drive the best results for bid adjustments.", - category: "reporting", - annotations: { - title: "Get Device Performance", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - inputSchema: { - type: "object", - properties: { - customer_id: { - type: "string", - description: "Google Ads customer ID. Uses default if not provided.", - }, - campaign_id: { - type: "string", - description: "Filter by campaign ID.", - }, - date_range: { - type: "string", - description: "Date range for metrics.", - enum: DATE_RANGE_ENUM, - default: "LAST_30_DAYS", - }, - }, - }, - _meta: { - labels: { - category: "reporting", - access: "read", - complexity: "simple" - } - }, - handler: async (args: Record, client: GoogleAdsClient): Promise => { - const customerId = args.customer_id || client.getCustomerId(); - if (!customerId) return errorResult("No customer ID provided and no default configured."); - - const dateRange = args.date_range || "LAST_30_DAYS"; - - const conditions = [`segments.date DURING ${dateRange}`]; - if (args.campaign_id) conditions.push(`campaign.id = ${args.campaign_id}`); - - const query = ` - SELECT - segments.device, - metrics.impressions, - metrics.clicks, - metrics.cost_micros, - metrics.conversions, - metrics.conversions_value, - metrics.ctr, - metrics.average_cpc - FROM campaign - WHERE ${conditions.join(" AND ")} - `; - - const results = await client.query(customerId, query); - if (!results.length) return text("No device performance data available for this period."); - - // Aggregate by device type (results may have multiple rows per device across campaigns) - const byDevice = new Map(); - - for (const r of results) { - const device = r.segments?.device || "UNKNOWN"; - const m = r.metrics || {}; - const existing = byDevice.get(device) || { impressions: 0, clicks: 0, costMicros: 0, conversions: 0, convValue: 0 }; - existing.impressions += Number(m.impressions || 0); - existing.clicks += Number(m.clicks || 0); - existing.costMicros += Number(m.costMicros || 0); - existing.conversions += Number(m.conversions || 0); - existing.convValue += Number(m.conversionsValue || 0); - byDevice.set(device, existing); - } - - // Sort by cost descending - const sorted = Array.from(byDevice.entries()).sort((a, b) => b[1].costMicros - a[1].costMicros); - - // Total for share calculation - const totalCost = sorted.reduce((sum, [, v]) => sum + v.costMicros, 0); - const totalClicks = sorted.reduce((sum, [, v]) => sum + v.clicks, 0); - - const deviceEmoji: Record = { - DESKTOP: "🖥️", - MOBILE: "📱", - TABLET: "📱", - CONNECTED_TV: "📺", - OTHER: "❓", - UNKNOWN: "❓", - }; - - const header = - `| Device | Impr. | Clicks | CTR | Cost | Cost Share | CPC | Conv. | Conv Rate | ROAS |\n` + - `|--------|-------|--------|-----|------|------------|-----|-------|-----------|------|`; - - const rows = sorted.map(([device, v]) => { - const cost = v.costMicros / 1_000_000; - const ctr = v.impressions > 0 ? v.clicks / v.impressions : 0; - const cpc = v.clicks > 0 ? cost / v.clicks : 0; - const convRate = v.clicks > 0 ? v.conversions / v.clicks : 0; - const costShare = totalCost > 0 ? v.costMicros / totalCost : 0; - const roas = cost > 0 ? v.convValue / cost : 0; - const emoji = deviceEmoji[device] || "❓"; - - return ( - `| ${emoji} ${device} | ${formatNumber(v.impressions)} | ${formatNumber(v.clicks)} | ` + - `${formatPercent(ctr)} | $${cost.toFixed(2)} | ${formatPercent(costShare)} | ` + - `$${cpc.toFixed(2)} | ${v.conversions.toFixed(1)} | ${formatPercent(convRate)} | ` + - `${roas.toFixed(2)}x |` - ); - }); - - return text( - `## 📱 Device Performance\n\n` + - `**Account:** \`${customerId}\` · **Period:** ${dateRange}\n\n` + - `${header}\n${rows.join("\n")}\n\n` + - `**Total:** ${formatNumber(totalClicks)} clicks · $${(totalCost / 1_000_000).toFixed(2)} cost\n\n` + - `💡 **Tip:** Use device bid adjustments to increase bids on high-performing devices and reduce spend on low-performing ones.` - ); - }, -}; - -// ─── Export ───────────────────────────────────────────────────────── -const tools: ToolDefinition[] = [ - getPerformanceSummary, - getCampaignPerformance, - getAdGroupPerformance, - getKeywordPerformance, - getSearchTermsReport, - getGeoPerformance, - getDevicePerformance, -]; - -export default tools; diff --git a/mcp-diagrams/google-ads-mcp/src/types.ts b/mcp-diagrams/google-ads-mcp/src/types.ts deleted file mode 100644 index 77866f8..0000000 --- a/mcp-diagrams/google-ads-mcp/src/types.ts +++ /dev/null @@ -1,246 +0,0 @@ -// ============================================ -// SHARED TYPES FOR GOOGLE ADS MCP -// ============================================ - -export interface ToolDefinition { - name: string; - description: string; - category: ToolCategory; - annotations: ToolAnnotations; - inputSchema: { - type: "object"; - properties: Record; - required?: string[]; - }; - _meta?: { - labels: { - category: string; - access: "read" | "write" | "delete"; - complexity: "simple" | "complex" | "batch"; - }; - }; - handler: (args: Record, client: GoogleAdsClient) => Promise; -} - -export interface ToolAnnotations { - title: string; - readOnlyHint: boolean; - destructiveHint: boolean; - idempotentHint: boolean; - openWorldHint: boolean; -} - -export type ToolCategory = - | "accounts" - | "campaigns" - | "ad_groups" - | "ads" - | "keywords" - | "reporting" - | "bidding" - | "conversions" - | "advanced"; - -export interface ToolResult { - content: Array<{ - type: "text" | "resource"; - text?: string; - resource?: any; - }>; - isError?: boolean; - structuredContent?: any; -} - -export interface GoogleAdsClient { - query(customerId: string, gaqlQuery: string): Promise; - mutate(customerId: string, operations: MutateOperation[]): Promise; - listAccessibleCustomers(): Promise; - getCustomerId(): string; -} - -export interface MutateOperation { - entity: string; - operation: "create" | "update" | "remove"; - resource?: Record; - resourceName?: string; - updateMask?: string; -} - -export interface GoogleAdsConfig { - clientId: string; - clientSecret: string; - developerToken: string; - refreshToken: string; - loginCustomerId?: string; - customerId?: string; -} - -// ============================================ -// API RESPONSE TYPES -// ============================================ - -export interface Campaign { - resourceName: string; - id: string; - name: string; - status: string; - advertisingChannelType: string; - biddingStrategyType: string; - startDate?: string; - endDate?: string; - budget?: CampaignBudget; - metrics?: CampaignMetrics; -} - -export interface CampaignBudget { - resourceName: string; - amountMicros: string; - deliveryMethod: string; - totalAmountMicros?: string; -} - -export interface CampaignMetrics { - impressions: string; - clicks: string; - costMicros: string; - conversions: number; - conversionsValue: number; - ctr: number; - averageCpc: string; - averageCpm: string; -} - -export interface AdGroup { - resourceName: string; - id: string; - name: string; - status: string; - campaignId: string; - type: string; - cpcBidMicros?: string; - metrics?: AdGroupMetrics; -} - -export interface AdGroupMetrics { - impressions: string; - clicks: string; - costMicros: string; - conversions: number; - ctr: number; - averageCpc: string; -} - -export interface Ad { - resourceName: string; - id: string; - type: string; - status: string; - adGroupId: string; - finalUrls: string[]; - responsiveSearchAd?: { - headlines: Array<{ text: string; pinnedField?: string }>; - descriptions: Array<{ text: string; pinnedField?: string }>; - }; - metrics?: AdMetrics; -} - -export interface AdMetrics { - impressions: string; - clicks: string; - costMicros: string; - conversions: number; - ctr: number; -} - -export interface Keyword { - resourceName: string; - criterionId: string; - keyword: { - text: string; - matchType: string; - }; - status: string; - adGroupId: string; - qualityInfo?: { - qualityScore: number; - creativityScore: string; - searchPredictedCtr: string; - postClickQualityScore: string; - }; - metrics?: KeywordMetrics; -} - -export interface KeywordMetrics { - impressions: string; - clicks: string; - costMicros: string; - conversions: number; - ctr: number; - averageCpc: string; - averagePosition?: number; -} - -export interface SearchTermView { - searchTerm: string; - adGroup: string; - campaign: string; - status: string; - metrics: { - impressions: string; - clicks: string; - costMicros: string; - conversions: number; - ctr: number; - }; -} - -export interface ConversionAction { - resourceName: string; - id: string; - name: string; - type: string; - status: string; - category: string; - countingType: string; -} - -export interface Recommendation { - resourceName: string; - type: string; - impact: { - baseMetrics: Record; - potentialMetrics: Record; - }; - campaignBudgetRecommendation?: any; - keywordRecommendation?: any; - targetCpaOptInRecommendation?: any; -} - -export interface ChangeEvent { - resourceName: string; - changeDateTime: string; - changeResourceType: string; - changeResourceName: string; - clientType: string; - userEmail: string; - oldResource?: any; - newResource?: any; -} - -// ============================================ -// FORMATTING HELPERS -// ============================================ - -export function microsToMoney(micros: string | number): string { - const num = typeof micros === "string" ? parseInt(micros) : micros; - return (num / 1_000_000).toFixed(2); -} - -export function formatPercent(value: number): string { - return (value * 100).toFixed(2) + "%"; -} - -export function formatNumber(value: string | number): string { - const num = typeof value === "string" ? parseInt(value) : value; - return num.toLocaleString(); -} diff --git a/mcp-diagrams/google-ads-mcp/tsconfig.json b/mcp-diagrams/google-ads-mcp/tsconfig.json deleted file mode 100644 index 6f16522..0000000 --- a/mcp-diagrams/google-ads-mcp/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "declaration": true, - "resolveJsonModule": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/mcp-diagrams/google-ads-mcp/tsup.config.ts b/mcp-diagrams/google-ads-mcp/tsup.config.ts deleted file mode 100644 index 92f0e06..0000000 --- a/mcp-diagrams/google-ads-mcp/tsup.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig({ - entry: ["src/index.ts"], - format: ["esm"], - target: "node20", - outDir: "dist", - clean: true, - dts: true, - banner: { - js: "#!/usr/bin/env node", - }, -}); diff --git a/mcp-diagrams/google-ads-mcp/ui/build-all.sh b/mcp-diagrams/google-ads-mcp/ui/build-all.sh deleted file mode 100755 index 3d69824..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/build-all.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash -set -e - -APPS=( - "campaign-dashboard" - "performance-overview" - "keyword-analyzer" - "budget-optimizer" - "search-terms" - "campaign-detail" - "recommendations" -) - -echo "Building ${#APPS[@]} Google Ads MCP apps..." - -for app in "${APPS[@]}"; do - echo " → Building $app..." - npx vite build --config "src/apps/$app/vite.config.ts" -done - -echo "✅ All apps built successfully!" diff --git a/mcp-diagrams/google-ads-mcp/ui/package.json b/mcp-diagrams/google-ads-mcp/ui/package.json deleted file mode 100644 index 5f8eee4..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "google-ads-mcp-ui", - "version": "1.0.0", - "private": true, - "type": "module", - "description": "Google Ads MCP — React UI Apps", - "scripts": { - "build": "bash build-all.sh", - "build:campaign-dashboard": "vite build --config src/apps/campaign-dashboard/vite.config.ts", - "build:performance-overview": "vite build --config src/apps/performance-overview/vite.config.ts", - "build:keyword-analyzer": "vite build --config src/apps/keyword-analyzer/vite.config.ts", - "build:budget-optimizer": "vite build --config src/apps/budget-optimizer/vite.config.ts", - "build:search-terms": "vite build --config src/apps/search-terms/vite.config.ts", - "build:campaign-detail": "vite build --config src/apps/campaign-detail/vite.config.ts", - "build:recommendations": "vite build --config src/apps/recommendations/vite.config.ts" - }, - "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/mcp-diagrams/google-ads-mcp/ui/src/apps/budget-optimizer/App.tsx b/mcp-diagrams/google-ads-mcp/ui/src/apps/budget-optimizer/App.tsx deleted file mode 100644 index 5f4da03..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/budget-optimizer/App.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { useApp } from '@modelcontextprotocol/ext-apps/react'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import { MetricCard } from '../../components/data/MetricCard'; -import { DataTable } from '../../components/data/DataTable'; -import '../../styles/base.css'; -import '../../styles/google-ads-theme.css'; - -/* ─── Types ──────────────────────────────────────────── */ -interface BudgetCampaignRow { - name: string; - dailyBudget: number; - actualSpend: number; - conversions: number; - costPerConversion: number; - roas?: number; -} - -interface BudgetSuggestion { - campaignName: string; - action: string; - reason: string; - estimatedImpact?: string; -} - -interface BudgetOptimizerData { - accountName?: string; - period?: string; - totalBudget: number; - totalSpend: number; - campaigns: BudgetCampaignRow[]; - suggestions?: BudgetSuggestion[]; -} - -/* ─── Helpers ────────────────────────────────────────── */ -function extractData(result: CallToolResult): any { - const sc = (result as any).structuredContent; - if (sc) return sc; - for (const item of result.content || []) { - if (item.type === 'text') { try { return JSON.parse(item.text); } catch {} } - } - return null; -} - -function fmtMoney(micros: number): string { - return '$' + (micros / 1_000_000).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); -} -function fmtNum(v: number): string { return v.toLocaleString('en-US'); } - -type SpendStatus = 'underspent' | 'on-track' | 'overspent'; - -function getSpendStatus(budget: number, spend: number): SpendStatus { - if (budget <= 0) return 'on-track'; - const ratio = spend / budget; - if (ratio < 0.7) return 'underspent'; - if (ratio > 1.0) return 'overspent'; - return 'on-track'; -} - -const statusConfig: Record = { - underspent: { label: 'Underspent', color: 'var(--gads-blue)', bg: 'var(--gads-blue-dim)' }, - 'on-track': { label: 'On Track', color: 'var(--gads-green)', bg: 'var(--gads-green-dim)' }, - overspent: { label: 'Overspent', color: 'var(--gads-red)', bg: 'var(--gads-red-dim)' }, -}; - -/* ─── App ────────────────────────────────────────────── */ -export function App() { - const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null); - - const { isConnected, error } = useApp({ - appInfo: { name: 'Budget Optimizer', version: '1.0.0' }, - capabilities: {}, - onAppCreated: (app) => { - app.ontoolresult = async (result) => { - const parsed = extractData(result); - if (parsed) setData(parsed); - }; - }, - }); - - const statusCounts = useMemo(() => { - if (!data) return { underspent: 0, 'on-track': 0, overspent: 0 }; - const counts = { underspent: 0, 'on-track': 0, overspent: 0 }; - data.campaigns.forEach(c => counts[getSpendStatus(c.dailyBudget, c.actualSpend)]++); - return counts; - }, [data]); - - const tableRows = useMemo(() => { - if (!data) return []; - return data.campaigns.map((c, i) => { - const status = getSpendStatus(c.dailyBudget, c.actualSpend); - return { - id: String(i), - name: c.name, - status: statusConfig[status].label, - budget: fmtMoney(c.dailyBudget), - spend: fmtMoney(c.actualSpend), - conversions: fmtNum(c.conversions), - costPerConv: fmtMoney(c.costPerConversion), - roas: c.roas !== undefined ? c.roas.toFixed(2) + 'x' : '—', - }; - }); - }, [data]); - - if (error) return

    Error

    {error.message}

    ; - if (!isConnected) return

    Connecting...

    ; - if (!data) return

    Waiting for data...

    ; - - const utilizationPct = data.totalBudget > 0 ? (data.totalSpend / data.totalBudget) * 100 : 0; - const utilizationColor = utilizationPct > 100 ? 'red' : utilizationPct > 85 ? 'yellow' : 'green'; - - return ( -
    - {/* Header */} -
    -

    💰 Budget Optimizer

    -

    - {data.accountName ? `${data.accountName} · ` : ''}{data.period || 'Current Period'} -

    -
    - - {/* KPI Cards */} -
    - - - -
    -
    - {statusCounts.underspent} - {statusCounts['on-track']} - {statusCounts.overspent} -
    -
    Under / On / Over
    -
    -
    - - {/* Budget Allocation Bars */} -
    -
    Budget Allocation
    - {data.campaigns.map((c, i) => { - const status = getSpendStatus(c.dailyBudget, c.actualSpend); - const spendPct = c.dailyBudget > 0 ? (c.actualSpend / c.dailyBudget) * 100 : 0; - const cfg = statusConfig[status]; - return ( -
    -
    - {c.name} - - {cfg.label} - -
    -
    -
    -
    -
    -
    -
    - {fmtMoney(c.actualSpend)} - / {fmtMoney(c.dailyBudget)} -
    -
    -
    - ); - })} - {/* Legend */} -
    - {(['underspent', 'on-track', 'overspent'] as SpendStatus[]).map(s => ( - - - {statusConfig[s].label} - - ))} -
    -
    - - {/* Campaign Breakdown Table */} -
    -
    -
    Campaign Breakdown
    -
    - -
    - - {/* Suggestions */} -
    -
    🔄 Reallocation Suggestions
    -
    - {data.suggestions && data.suggestions.length > 0 ? data.suggestions.map((s, i) => ( -
    -
    📂 {s.campaignName}
    -
    {s.action}
    -
    {s.reason}
    - {s.estimatedImpact && ( -
    - - ⬆ {s.estimatedImpact} - -
    - )} -
    - )) : ( -
    - No reallocation suggestions at this time -
    - )} -
    -
    -
    - ); -} diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/budget-optimizer/index.html b/mcp-diagrams/google-ads-mcp/ui/src/apps/budget-optimizer/index.html deleted file mode 100644 index 5d8b659..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/budget-optimizer/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - -Budget Optimizer -
    - diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/budget-optimizer/main.tsx b/mcp-diagrams/google-ads-mcp/ui/src/apps/budget-optimizer/main.tsx deleted file mode 100644 index f774ec9..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/budget-optimizer/main.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import { App } from './App'; -createRoot(document.getElementById('root')!).render(); diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/budget-optimizer/vite.config.ts b/mcp-diagrams/google-ads-mcp/ui/src/apps/budget-optimizer/vite.config.ts deleted file mode 100644 index 39075d4..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/budget-optimizer/vite.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -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/app-ui/budget-optimizer'), - emptyOutDir: true, - rollupOptions: { input: path.resolve(__dirname, 'index.html') }, - }, - resolve: { - alias: { - '@components': path.resolve(__dirname, '../../components'), - '@styles': path.resolve(__dirname, '../../styles'), - }, - }, -}); diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-dashboard/App.tsx b/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-dashboard/App.tsx deleted file mode 100644 index 08704ef..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-dashboard/App.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { useApp } from '@modelcontextprotocol/ext-apps/react'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import { MetricCard } from '../../components/data/MetricCard'; -import { StatusBadge } from '../../components/data/StatusBadge'; -import '../../styles/base.css'; -import '../../styles/google-ads-theme.css'; - -/* ─── Types (from campaign-dashboard.ts) ─────────────── */ -interface CampaignCard { - id: string; - name: string; - status: string; - channelType: string; - spend: number; - clicks: number; - impressions: number; - ctr: number; - conversions: number; - dailyBudget: number; -} - -interface CampaignDashboardData { - accountName?: string; - campaigns: CampaignCard[]; - sortBy?: 'spend' | 'conversions' | 'ctr'; -} - -/* ─── Helpers ────────────────────────────────────────── */ -function extractData(result: CallToolResult): any { - const sc = (result as any).structuredContent; - if (sc) return sc; - for (const item of result.content || []) { - if (item.type === 'text') { - try { return JSON.parse(item.text); } catch {} - } - } - return null; -} - -function fmtMoney(micros: number): string { - return '$' + (micros / 1_000_000).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); -} -function fmtNum(v: number): string { return v.toLocaleString('en-US'); } -function fmtPct(v: number): string { return (v * 100).toFixed(2) + '%'; } - -const channelColors: Record = { - SEARCH: '#4ea8de', DISPLAY: '#9b5de5', SHOPPING: '#f48c06', - VIDEO: '#e94560', 'PERFORMANCE MAX': '#00c897', 'PERFORMANCE_MAX': '#00c897', - DISCOVERY: '#f0c040', LOCAL: '#6c6c8a', SMART: '#6c6c8a', -}; - -function getChannelColor(ch: string): string { - return channelColors[ch.toUpperCase().replace(/_/g, ' ')] || channelColors[ch.toUpperCase()] || '#6c6c8a'; -} - -function getStatusColor(status: string): string { - const s = status.toUpperCase(); - if (s === 'ENABLED' || s === 'ACTIVE') return 'var(--gads-green)'; - if (s === 'PAUSED') return 'var(--gads-yellow)'; - if (s === 'REMOVED' || s === 'DISABLED') return 'var(--gads-red)'; - return 'var(--gads-text-muted)'; -} - -function getStatusBg(status: string): string { - const s = status.toUpperCase(); - if (s === 'ENABLED' || s === 'ACTIVE') return 'var(--gads-green-dim)'; - if (s === 'PAUSED') return 'var(--gads-yellow-dim)'; - if (s === 'REMOVED' || s === 'DISABLED') return 'var(--gads-red-dim)'; - return 'var(--gads-border)'; -} - -/* ─── App ────────────────────────────────────────────── */ -export function App() { - const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null); - const [sortBy, setSortBy] = useState<'spend' | 'conversions' | 'ctr'>('spend'); - - const { isConnected, error } = useApp({ - appInfo: { name: 'Campaign Dashboard', version: '1.0.0' }, - capabilities: {}, - onAppCreated: (app) => { - app.ontoolresult = async (result) => { - const parsed = extractData(result); - if (parsed) setData(parsed); - }; - }, - }); - - const sorted = useMemo(() => { - if (!data) return []; - const campaigns = [...data.campaigns]; - switch (sortBy) { - case 'conversions': return campaigns.sort((a, b) => b.conversions - a.conversions); - case 'ctr': return campaigns.sort((a, b) => b.ctr - a.ctr); - default: return campaigns.sort((a, b) => b.spend - a.spend); - } - }, [data, sortBy]); - - const totalSpend = useMemo(() => data?.campaigns.reduce((s, c) => s + c.spend, 0) || 0, [data]); - const totalConv = useMemo(() => data?.campaigns.reduce((s, c) => s + c.conversions, 0) || 0, [data]); - const totalClicks = useMemo(() => data?.campaigns.reduce((s, c) => s + c.clicks, 0) || 0, [data]); - - if (error) return

    Error

    {error.message}

    ; - if (!isConnected) return

    Connecting...

    ; - if (!data) return

    Waiting for data...

    ; - - return ( -
    - {/* Header */} -
    -

    📊 Campaign Dashboard

    -

    - {data.accountName ? `${data.accountName} — ` : ''}{sorted.length} campaigns · {fmtMoney(totalSpend)} total spend · {fmtNum(totalConv)} conversions -

    -
    - - {/* KPI Cards */} -
    - - - - -
    - - {/* Sort Controls */} -
    - Sort by: - {(['spend', 'conversions', 'ctr'] as const).map(s => ( - - ))} -
    - - {/* Campaign Cards Grid */} -
    - {sorted.map(c => { - const budgetPct = c.dailyBudget > 0 ? (c.spend / c.dailyBudget) * 100 : 0; - const budgetColor = budgetPct > 95 ? 'var(--gads-red)' : budgetPct > 70 ? 'var(--gads-yellow)' : 'var(--gads-green)'; - const channelLabel = c.channelType.toUpperCase().replace(/_/g, ' '); - - return ( -
    - {/* Name + Status */} -
    - {c.name} - - {c.status.toUpperCase()} - -
    - - {/* Channel Badge */} -
    - - {channelLabel} - -
    - - {/* Metrics Grid */} -
    -
    -
    Spend
    -
    {fmtMoney(c.spend)}
    -
    -
    -
    Conversions
    -
    {fmtNum(c.conversions)}
    -
    -
    -
    Clicks
    -
    {fmtNum(c.clicks)}
    -
    -
    -
    CTR
    -
    {fmtPct(c.ctr)}
    -
    -
    -
    Impressions
    -
    {fmtNum(c.impressions)}
    -
    -
    - - {/* Budget Utilization */} -
    -
    - Budget Utilization - {budgetPct.toFixed(0)}% -
    -
    -
    -
    -
    - {fmtMoney(c.spend)} / {fmtMoney(c.dailyBudget)} daily -
    -
    -
    - ); - })} -
    -
    - ); -} diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-dashboard/index.html b/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-dashboard/index.html deleted file mode 100644 index a8b3cf4..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-dashboard/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - -Campaign Dashboard -
    - diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-dashboard/main.tsx b/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-dashboard/main.tsx deleted file mode 100644 index f774ec9..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-dashboard/main.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import { App } from './App'; -createRoot(document.getElementById('root')!).render(); diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-dashboard/vite.config.ts b/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-dashboard/vite.config.ts deleted file mode 100644 index 69e70b7..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-dashboard/vite.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -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/app-ui/campaign-dashboard'), - emptyOutDir: true, - rollupOptions: { input: path.resolve(__dirname, 'index.html') }, - }, - resolve: { - alias: { - '@components': path.resolve(__dirname, '../../components'), - '@styles': path.resolve(__dirname, '../../styles'), - }, - }, -}); diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-detail/App.tsx b/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-detail/App.tsx deleted file mode 100644 index e1f4f80..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-detail/App.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { useApp } from '@modelcontextprotocol/ext-apps/react'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import { DataTable } from '../../components/data/DataTable'; -import '../../styles/base.css'; -import '../../styles/google-ads-theme.css'; - -/* ─── Types ──────────────────────────────────────────── */ -interface AdGroupRow { - id: string; - name: string; - status: string; - cpcBid: number; - impressions: number; - clicks: number; - ctr: number; - cost: number; - conversions: number; -} - -interface CampaignDetailData { - campaign: { - id: string; - name: string; - status: string; - channelType: string; - biddingStrategy: string; - startDate?: string; - endDate?: string; - }; - budget: { - dailyBudget: number; - totalSpent: number; - deliveryMethod?: string; - }; - metrics: { - impressions: number; - clicks: number; - ctr: number; - cost: number; - conversions: number; - conversionRate: number; - costPerConversion: number; - avgCpc: number; - }; - adGroups: AdGroupRow[]; -} - -/* ─── Helpers ────────────────────────────────────────── */ -function extractData(result: CallToolResult): any { - const sc = (result as any).structuredContent; - if (sc) return sc; - for (const item of result.content || []) { - if (item.type === 'text') { try { return JSON.parse(item.text); } catch {} } - } - return null; -} - -function fmtMoney(micros: number): string { - return '$' + (micros / 1_000_000).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); -} -function fmtNum(v: number): string { return v.toLocaleString('en-US'); } -function fmtPct(v: number): string { return (v * 100).toFixed(2) + '%'; } - -function getStatusColor(status: string): string { - const s = status.toUpperCase(); - if (s === 'ENABLED' || s === 'ACTIVE') return 'var(--gads-green)'; - if (s === 'PAUSED') return 'var(--gads-yellow)'; - return 'var(--gads-red)'; -} -function getStatusBg(status: string): string { - const s = status.toUpperCase(); - if (s === 'ENABLED' || s === 'ACTIVE') return 'var(--gads-green-dim)'; - if (s === 'PAUSED') return 'var(--gads-yellow-dim)'; - return 'var(--gads-red-dim)'; -} - -/* ─── MetricRow component ────────────────────────────── */ -function MetricRow({ label, value }: { label: string; value: string }) { - return ( -
    - {label} - {value} -
    - ); -} - -/* ─── App ────────────────────────────────────────────── */ -export function App() { - const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null); - - const { isConnected, error } = useApp({ - appInfo: { name: 'Campaign Detail', version: '1.0.0' }, - capabilities: {}, - onAppCreated: (app) => { - app.ontoolresult = async (result) => { - const parsed = extractData(result); - if (parsed) setData(parsed); - }; - }, - }); - - const adGroupRows = useMemo(() => { - if (!data) return []; - return data.adGroups.map(ag => ({ - id: ag.id, - name: ag.name, - status: ag.status.toUpperCase(), - cpcBid: fmtMoney(ag.cpcBid), - impressions: fmtNum(ag.impressions), - clicks: fmtNum(ag.clicks), - ctr: fmtPct(ag.ctr), - cost: fmtMoney(ag.cost), - conversions: fmtNum(ag.conversions), - })); - }, [data]); - - if (error) return

    Error

    {error.message}

    ; - if (!isConnected) return

    Connecting...

    ; - if (!data) return

    Waiting for data...

    ; - - const { campaign: c, budget: b, metrics: m } = data; - const utilizationPct = b.dailyBudget > 0 ? (b.totalSpent / b.dailyBudget) * 100 : 0; - const budgetColor = utilizationPct > 95 ? 'var(--gads-red)' : utilizationPct > 70 ? 'var(--gads-yellow)' : 'var(--gads-green)'; - const strategyLabel = c.biddingStrategy.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); - - return ( -
    - {/* Header */} -
    -
    -

    🔍 {c.name}

    - - {c.status.toUpperCase()} - -
    -
    - - {c.channelType.replace(/_/g, ' ')} - - - {strategyLabel} - - {c.startDate && ( - - Started {c.startDate} - - )} - {c.endDate && ( - - Ends {c.endDate} - - )} -
    -
    - - {/* Budget + Performance side by side */} -
    - {/* Budget Card */} -
    -
    💰 Budget
    - - - {b.deliveryMethod && } -
    -
    - Utilization - {utilizationPct.toFixed(1)}% -
    -
    -
    -
    -
    -
    - - {/* Performance Card */} -
    -
    📊 Performance
    - - - - - - - -
    - Cost / Conv. - {fmtMoney(m.costPerConversion)} -
    -
    -
    - - {/* Ad Groups Table */} -
    -
    -
    📂 Ad Groups ({data.adGroups.length})
    -
    - -
    -
    - ); -} diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-detail/index.html b/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-detail/index.html deleted file mode 100644 index 55758e9..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-detail/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - -Campaign Detail -
    - diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-detail/main.tsx b/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-detail/main.tsx deleted file mode 100644 index f774ec9..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-detail/main.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import { App } from './App'; -createRoot(document.getElementById('root')!).render(); diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-detail/vite.config.ts b/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-detail/vite.config.ts deleted file mode 100644 index 5af99b7..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/campaign-detail/vite.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -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/app-ui/campaign-detail'), - emptyOutDir: true, - rollupOptions: { input: path.resolve(__dirname, 'index.html') }, - }, - resolve: { - alias: { - '@components': path.resolve(__dirname, '../../components'), - '@styles': path.resolve(__dirname, '../../styles'), - }, - }, -}); diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/keyword-analyzer/App.tsx b/mcp-diagrams/google-ads-mcp/ui/src/apps/keyword-analyzer/App.tsx deleted file mode 100644 index 7ee4424..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/keyword-analyzer/App.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { useApp } from '@modelcontextprotocol/ext-apps/react'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import { MetricCard } from '../../components/data/MetricCard'; -import { DataTable } from '../../components/data/DataTable'; -import '../../styles/base.css'; -import '../../styles/google-ads-theme.css'; - -/* ─── Types ──────────────────────────────────────────── */ -interface KeywordRow { - keyword: string; - matchType: string; - status: string; - qualityScore: number; - clicks: number; - impressions: number; - ctr: number; - cpc: number; - conversions: number; -} - -interface KeywordAnalyzerData { - campaignName?: string; - adGroupName?: string; - keywords: KeywordRow[]; -} - -/* ─── Helpers ────────────────────────────────────────── */ -function extractData(result: CallToolResult): any { - const sc = (result as any).structuredContent; - if (sc) return sc; - for (const item of result.content || []) { - if (item.type === 'text') { try { return JSON.parse(item.text); } catch {} } - } - return null; -} - -function fmtMoney(micros: number): string { - return '$' + (micros / 1_000_000).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); -} -function fmtNum(v: number): string { return v.toLocaleString('en-US'); } -function fmtPct(v: number): string { return (v * 100).toFixed(2) + '%'; } - -/* ─── Quality Score Badge ────────────────────────────── */ -function QualityScoreBadge({ score }: { score: number }) { - const clamped = Math.max(0, Math.min(10, score)); - const pct = clamped * 10; - let color: string, bgColor: string; - if (clamped >= 7) { color = 'var(--gads-green)'; bgColor = 'var(--gads-green-dim)'; } - else if (clamped >= 4) { color = 'var(--gads-yellow)'; bgColor = 'var(--gads-yellow-dim)'; } - else { color = 'var(--gads-red)'; bgColor = 'var(--gads-red-dim)'; } - - return ( -
    -
    -
    -
    - {clamped} -
    - ); -} - -/* ─── Match Type Badge ───────────────────────────────── */ -function MatchTypeBadge({ matchType }: { matchType: string }) { - const mt = matchType.toUpperCase(); - const colors: Record = { - BROAD: { c: 'var(--gads-blue)', bg: 'var(--gads-blue-dim)' }, - PHRASE: { c: 'var(--gads-purple)', bg: 'var(--gads-purple-dim)' }, - EXACT: { c: 'var(--gads-green)', bg: 'var(--gads-green-dim)' }, - }; - const { c, bg } = colors[mt] || { c: 'var(--gads-text-muted)', bg: 'var(--gads-border)' }; - return ( - - {mt} - - ); -} - -/* ─── App ────────────────────────────────────────────── */ -export function App() { - const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null); - const [search, setSearch] = useState(''); - const [filterMatch, setFilterMatch] = useState('ALL'); - - const { isConnected, error } = useApp({ - appInfo: { name: 'Keyword Analyzer', version: '1.0.0' }, - capabilities: {}, - onAppCreated: (app) => { - app.ontoolresult = async (result) => { - const parsed = extractData(result); - if (parsed) setData(parsed); - }; - }, - }); - - const filtered = useMemo(() => { - if (!data) return []; - return data.keywords.filter(k => { - if (filterMatch !== 'ALL' && k.matchType.toUpperCase() !== filterMatch) return false; - if (search && !k.keyword.toLowerCase().includes(search.toLowerCase())) return false; - return true; - }); - }, [data, search, filterMatch]); - - const avgQS = useMemo(() => { - if (!filtered.length) return 0; - return filtered.reduce((s, k) => s + k.qualityScore, 0) / filtered.length; - }, [filtered]); - - const topPerformer = useMemo(() => { - if (!filtered.length) return null; - return [...filtered].sort((a, b) => b.conversions - a.conversions)[0]; - }, [filtered]); - - const tableRows = useMemo(() => { - return filtered.map((k, i) => ({ - id: String(i), - keyword: k.keyword, - matchType: k.matchType, - status: k.status, - qualityScore: k.qualityScore, - clicks: fmtNum(k.clicks), - impressions: fmtNum(k.impressions), - ctr: fmtPct(k.ctr), - cpc: fmtMoney(k.cpc), - conversions: fmtNum(k.conversions), - })); - }, [filtered]); - - if (error) return

    Error

    {error.message}

    ; - if (!isConnected) return

    Connecting...

    ; - if (!data) return

    Waiting for data...

    ; - - const avgQSColor = avgQS >= 7 ? 'green' : avgQS >= 4 ? 'yellow' : 'red'; - - return ( -
    - {/* Header */} -
    -

    🔑 Keyword Analyzer

    -

    - {data.campaignName || 'All campaigns'}{data.adGroupName ? ` → ${data.adGroupName}` : ''} -

    -
    - - {/* KPI Cards */} -
    - - - -
    - - {/* Filters */} -
    - setSearch(e.target.value)} - style={{ - padding: '6px 12px', borderRadius: 6, border: '1px solid var(--gads-border-light)', - background: 'var(--gads-bg-input)', color: 'var(--gads-text-primary)', fontSize: 12, - outline: 'none', flex: '1 1 200px', minWidth: 150, - }} - /> - {['ALL', 'BROAD', 'PHRASE', 'EXACT'].map(mt => ( - - ))} -
    - - {/* Table */} -
    - -
    -
    - ); -} diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/keyword-analyzer/index.html b/mcp-diagrams/google-ads-mcp/ui/src/apps/keyword-analyzer/index.html deleted file mode 100644 index 78c4d12..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/keyword-analyzer/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - -Keyword Analyzer -
    - diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/keyword-analyzer/main.tsx b/mcp-diagrams/google-ads-mcp/ui/src/apps/keyword-analyzer/main.tsx deleted file mode 100644 index f774ec9..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/keyword-analyzer/main.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import { App } from './App'; -createRoot(document.getElementById('root')!).render(); diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/keyword-analyzer/vite.config.ts b/mcp-diagrams/google-ads-mcp/ui/src/apps/keyword-analyzer/vite.config.ts deleted file mode 100644 index f060ad9..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/keyword-analyzer/vite.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -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/app-ui/keyword-analyzer'), - emptyOutDir: true, - rollupOptions: { input: path.resolve(__dirname, 'index.html') }, - }, - resolve: { - alias: { - '@components': path.resolve(__dirname, '../../components'), - '@styles': path.resolve(__dirname, '../../styles'), - }, - }, -}); diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/performance-overview/App.tsx b/mcp-diagrams/google-ads-mcp/ui/src/apps/performance-overview/App.tsx deleted file mode 100644 index 9037747..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/performance-overview/App.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { useApp } from '@modelcontextprotocol/ext-apps/react'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import { MetricCard } from '../../components/data/MetricCard'; -import { BarChart } from '../../components/charts/BarChart'; -import { DataTable } from '../../components/data/DataTable'; -import '../../styles/base.css'; -import '../../styles/google-ads-theme.css'; - -/* ─── Types ──────────────────────────────────────────── */ -interface DailySpendPoint { date: string; spend: number; } -interface TopCampaign { name: string; spend: number; conversions: number; ctr: number; roas?: number; } - -interface PerformanceOverviewData { - accountName?: string; - period: string; - kpis: { - totalSpend: number; - clicks: number; - impressions: number; - ctr: number; - avgCpc: number; - conversions: number; - }; - previousKpis?: { - totalSpend: number; - clicks: number; - impressions: number; - ctr: number; - avgCpc: number; - conversions: number; - }; - dailySpend: DailySpendPoint[]; - topCampaigns: TopCampaign[]; -} - -/* ─── Helpers ────────────────────────────────────────── */ -function extractData(result: CallToolResult): any { - const sc = (result as any).structuredContent; - if (sc) return sc; - for (const item of result.content || []) { - if (item.type === 'text') { try { return JSON.parse(item.text); } catch {} } - } - return null; -} - -function fmtMoney(micros: number): string { - return '$' + (micros / 1_000_000).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); -} -function fmtNum(v: number): string { return v.toLocaleString('en-US'); } -function fmtPct(v: number): string { return (v * 100).toFixed(2) + '%'; } - -function getDelta(current: number, previous?: number): { arrow: string; cls: string; value: string } | null { - if (previous === undefined || previous === 0) return null; - const pct = ((current - previous) / Math.abs(previous)) * 100; - return { - arrow: pct >= 0 ? '↑' : '↓', - cls: pct >= 0 ? 'up' : 'down', - value: `${Math.abs(pct).toFixed(1)}%`, - }; -} - -/* ─── App ────────────────────────────────────────────── */ -export function App() { - const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null); - - const { isConnected, error } = useApp({ - appInfo: { name: 'Performance Overview', version: '1.0.0' }, - capabilities: {}, - onAppCreated: (app) => { - app.ontoolresult = async (result) => { - const parsed = extractData(result); - if (parsed) setData(parsed); - }; - }, - }); - - const topCampaignRows = useMemo(() => { - if (!data) return []; - return data.topCampaigns.map((c, i) => ({ - id: String(i), - rank: i + 1, - name: c.name, - spend: fmtMoney(c.spend), - conversions: fmtNum(c.conversions), - ctr: fmtPct(c.ctr), - roas: c.roas !== undefined ? c.roas.toFixed(2) + 'x' : '—', - })); - }, [data]); - - const dailyBars = useMemo(() => { - if (!data) return []; - return data.dailySpend.map(d => ({ - label: d.date.length > 5 ? d.date.slice(-5) : d.date, - value: d.spend / 1_000_000, - color: '#e94560', - })); - }, [data]); - - if (error) return

    Error

    {error.message}

    ; - if (!isConnected) return

    Connecting...

    ; - if (!data) return

    Waiting for data...

    ; - - const k = data.kpis; - const p = data.previousKpis; - - const kpiItems: { label: string; value: string; current: number; previous?: number; color: 'red' | 'blue' | 'purple' | 'green' | 'yellow' }[] = [ - { label: 'Total Spend', value: fmtMoney(k.totalSpend), current: k.totalSpend, previous: p?.totalSpend, color: 'red' }, - { label: 'Clicks', value: fmtNum(k.clicks), current: k.clicks, previous: p?.clicks, color: 'blue' }, - { label: 'Impressions', value: fmtNum(k.impressions), current: k.impressions, previous: p?.impressions, color: 'purple' }, - { label: 'CTR', value: fmtPct(k.ctr), current: k.ctr, previous: p?.ctr, color: 'green' }, - { label: 'Avg CPC', value: fmtMoney(k.avgCpc), current: k.avgCpc, previous: p?.avgCpc, color: 'yellow' }, - { label: 'Conversions', value: fmtNum(k.conversions), current: k.conversions, previous: p?.conversions, color: 'green' }, - ]; - - return ( -
    - {/* Header */} -
    -

    📈 Performance Overview

    -

    - {data.accountName ? `${data.accountName} · ` : ''}{data.period} -

    -
    - - {/* KPI Cards */} -
    - {kpiItems.map((item, i) => { - const delta = getDelta(item.current, item.previous); - return ( - - ); - })} -
    - - {/* Daily Spend Chart */} -
    - -
    - - {/* Top Campaigns Table */} -
    -
    -
    -
    Top Campaigns
    -
    - -
    -
    -
    - ); -} diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/performance-overview/index.html b/mcp-diagrams/google-ads-mcp/ui/src/apps/performance-overview/index.html deleted file mode 100644 index 6a793e5..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/performance-overview/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - -Performance Overview -
    - diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/performance-overview/main.tsx b/mcp-diagrams/google-ads-mcp/ui/src/apps/performance-overview/main.tsx deleted file mode 100644 index f774ec9..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/performance-overview/main.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import { App } from './App'; -createRoot(document.getElementById('root')!).render(); diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/performance-overview/vite.config.ts b/mcp-diagrams/google-ads-mcp/ui/src/apps/performance-overview/vite.config.ts deleted file mode 100644 index 99e303d..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/performance-overview/vite.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -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/app-ui/performance-overview'), - emptyOutDir: true, - rollupOptions: { input: path.resolve(__dirname, 'index.html') }, - }, - resolve: { - alias: { - '@components': path.resolve(__dirname, '../../components'), - '@styles': path.resolve(__dirname, '../../styles'), - }, - }, -}); diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/recommendations/App.tsx b/mcp-diagrams/google-ads-mcp/ui/src/apps/recommendations/App.tsx deleted file mode 100644 index e6d462d..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/recommendations/App.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { useApp } from '@modelcontextprotocol/ext-apps/react'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import { MetricCard } from '../../components/data/MetricCard'; -import '../../styles/base.css'; -import '../../styles/google-ads-theme.css'; - -/* ─── Types ──────────────────────────────────────────── */ -interface RecommendationItem { - type: string; - category: string; - impact: 'LOW' | 'MEDIUM' | 'HIGH'; - title: string; - description: string; - campaignName?: string; - estimatedImprovement?: string; - resourceName?: string; -} - -interface RecommendationsData { - accountName?: string; - recommendations: RecommendationItem[]; -} - -/* ─── Helpers ────────────────────────────────────────── */ -function extractData(result: CallToolResult): any { - const sc = (result as any).structuredContent; - if (sc) return sc; - for (const item of result.content || []) { - if (item.type === 'text') { try { return JSON.parse(item.text); } catch {} } - } - return null; -} - -const impactConfig: Record = { - HIGH: { color: 'var(--gads-red)', bg: 'var(--gads-red-dim)', icon: '🔴' }, - MEDIUM: { color: 'var(--gads-yellow)', bg: 'var(--gads-yellow-dim)', icon: '🟡' }, - LOW: { color: 'var(--gads-blue)', bg: 'var(--gads-blue-dim)', icon: '🔵' }, -}; - -const categoryColors: Record = { - BIDS: { color: 'var(--gads-purple)', bg: 'var(--gads-purple-dim)' }, - BIDDING: { color: 'var(--gads-purple)', bg: 'var(--gads-purple-dim)' }, - KEYWORDS: { color: 'var(--gads-blue)', bg: 'var(--gads-blue-dim)' }, - ADS: { color: 'var(--gads-orange)', bg: 'var(--gads-orange-dim)' }, - TARGETING: { color: 'var(--gads-green)', bg: 'var(--gads-green-dim)' }, - BUDGET: { color: 'var(--gads-red)', bg: 'var(--gads-red-dim)' }, - EXTENSIONS: { color: 'var(--gads-yellow)', bg: 'var(--gads-yellow-dim)' }, - CAMPAIGN: { color: 'var(--gads-blue)', bg: 'var(--gads-blue-dim)' }, -}; - -function getCategoryStyle(cat: string) { - return categoryColors[cat.toUpperCase()] || { color: 'var(--gads-text-muted)', bg: 'var(--gads-border)' }; -} - -/* ─── App ────────────────────────────────────────────── */ -export function App() { - const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null); - const [filterCategory, setFilterCategory] = useState('ALL'); - const [filterImpact, setFilterImpact] = useState('ALL'); - - const { isConnected, error } = useApp({ - appInfo: { name: 'Recommendations', version: '1.0.0' }, - capabilities: {}, - onAppCreated: (app) => { - app.ontoolresult = async (result) => { - const parsed = extractData(result); - if (parsed) setData(parsed); - }; - }, - }); - - const byImpact = useMemo(() => { - if (!data) return { HIGH: 0, MEDIUM: 0, LOW: 0 }; - const counts = { HIGH: 0, MEDIUM: 0, LOW: 0 }; - data.recommendations.forEach(r => { - const key = r.impact.toUpperCase() as keyof typeof counts; - if (key in counts) counts[key]++; - }); - return counts; - }, [data]); - - const categories = useMemo(() => { - if (!data) return []; - return [...new Set(data.recommendations.map(r => r.category.toUpperCase()))]; - }, [data]); - - const filtered = useMemo(() => { - if (!data) return []; - return data.recommendations.filter(r => { - if (filterCategory !== 'ALL' && r.category.toUpperCase() !== filterCategory) return false; - if (filterImpact !== 'ALL' && r.impact.toUpperCase() !== filterImpact) return false; - return true; - }); - }, [data, filterCategory, filterImpact]); - - if (error) return

    Error

    {error.message}

    ; - if (!isConnected) return

    Connecting...

    ; - if (!data) return

    Waiting for data...

    ; - - return ( -
    - {/* Header */} -
    -

    💡 Optimization Recommendations

    -

    - {data.accountName ? `${data.accountName} · ` : ''}{data.recommendations.length} recommendations -

    -
    - - {/* Impact KPIs */} -
    - - - -
    - - {/* Filters */} -
    - Category: - - {categories.map(cat => { - const style = getCategoryStyle(cat); - return ( - - ); - })} - - Impact: - {['ALL', 'HIGH', 'MEDIUM', 'LOW'].map(imp => ( - - ))} -
    - - {/* Recommendation Cards */} -
    - {filtered.length > 0 ? filtered.map((r, i) => { - const impCfg = impactConfig[r.impact.toUpperCase()] || impactConfig.LOW; - const catStyle = getCategoryStyle(r.category); - - return ( -
    - {/* Impact + Category */} -
    - - {impCfg.icon} {r.impact.toUpperCase()} IMPACT - - - {r.category.toUpperCase()} - -
    - - {/* Title + Desc */} -
    {r.title}
    -
    {r.description}
    - - {/* Campaign */} - {r.campaignName && ( -
    - 📂 {r.campaignName} -
    - )} - - {/* Estimated Improvement */} - {r.estimatedImprovement && ( -
    - - ⬆ {r.estimatedImprovement} - -
    - )} -
    - ); - }) : ( -
    - No recommendations available -
    - )} -
    -
    - ); -} diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/recommendations/index.html b/mcp-diagrams/google-ads-mcp/ui/src/apps/recommendations/index.html deleted file mode 100644 index 23c686b..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/recommendations/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - -Recommendations -
    - diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/recommendations/main.tsx b/mcp-diagrams/google-ads-mcp/ui/src/apps/recommendations/main.tsx deleted file mode 100644 index f774ec9..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/recommendations/main.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import { App } from './App'; -createRoot(document.getElementById('root')!).render(); diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/recommendations/vite.config.ts b/mcp-diagrams/google-ads-mcp/ui/src/apps/recommendations/vite.config.ts deleted file mode 100644 index d81e2b5..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/recommendations/vite.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -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/app-ui/recommendations'), - emptyOutDir: true, - rollupOptions: { input: path.resolve(__dirname, 'index.html') }, - }, - resolve: { - alias: { - '@components': path.resolve(__dirname, '../../components'), - '@styles': path.resolve(__dirname, '../../styles'), - }, - }, -}); diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/search-terms/App.tsx b/mcp-diagrams/google-ads-mcp/ui/src/apps/search-terms/App.tsx deleted file mode 100644 index 9acdbde..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/search-terms/App.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { useApp } from '@modelcontextprotocol/ext-apps/react'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import { MetricCard } from '../../components/data/MetricCard'; -import { DataTable } from '../../components/data/DataTable'; -import '../../styles/base.css'; -import '../../styles/google-ads-theme.css'; - -/* ─── Types ──────────────────────────────────────────── */ -interface SearchTermRow { - searchTerm: string; - campaign: string; - adGroup: string; - impressions: number; - clicks: number; - ctr: number; - cost: number; - conversions: number; - suggestedAction?: 'add_keyword' | 'add_negative' | 'none'; -} - -interface SearchTermsData { - period?: string; - costThreshold?: number; - terms: SearchTermRow[]; -} - -/* ─── Helpers ────────────────────────────────────────── */ -function extractData(result: CallToolResult): any { - const sc = (result as any).structuredContent; - if (sc) return sc; - for (const item of result.content || []) { - if (item.type === 'text') { try { return JSON.parse(item.text); } catch {} } - } - return null; -} - -function fmtMoney(micros: number): string { - return '$' + (micros / 1_000_000).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); -} -function fmtNum(v: number): string { return v.toLocaleString('en-US'); } -function fmtPct(v: number): string { return (v * 100).toFixed(2) + '%'; } - -function inferAction(term: SearchTermRow, costThreshold: number): string { - if (term.suggestedAction && term.suggestedAction !== 'none') return term.suggestedAction; - if (term.cost > costThreshold && term.conversions === 0) return 'add_negative'; - if (term.conversions > 0 && term.ctr > 0.03) return 'add_keyword'; - return 'none'; -} - -/* ─── App ────────────────────────────────────────────── */ -export function App() { - const [data, setData] = useState((window as any).__MCP_APP_DATA__ || null); - const [search, setSearch] = useState(''); - const [filterAction, setFilterAction] = useState('ALL'); - - const { isConnected, error } = useApp({ - appInfo: { name: 'Search Terms Report', version: '1.0.0' }, - capabilities: {}, - onAppCreated: (app) => { - app.ontoolresult = async (result) => { - const parsed = extractData(result); - if (parsed) setData(parsed); - }; - }, - }); - - const costThreshold = data?.costThreshold || 5_000_000; - - const filtered = useMemo(() => { - if (!data) return []; - return data.terms.filter(t => { - const action = inferAction(t, costThreshold); - if (filterAction !== 'ALL' && action !== filterAction) return false; - if (search && !t.searchTerm.toLowerCase().includes(search.toLowerCase())) return false; - return true; - }); - }, [data, search, filterAction, costThreshold]); - - const totalCost = useMemo(() => data?.terms.reduce((s, t) => s + t.cost, 0) || 0, [data]); - const negCandidates = useMemo(() => data?.terms.filter(t => inferAction(t, costThreshold) === 'add_negative').length || 0, [data, costThreshold]); - const kwCandidates = useMemo(() => data?.terms.filter(t => inferAction(t, costThreshold) === 'add_keyword').length || 0, [data, costThreshold]); - - const tableRows = useMemo(() => { - return filtered.map((t, i) => { - const action = inferAction(t, costThreshold); - const isHighCostLowConv = t.cost > costThreshold && t.conversions === 0; - return { - id: String(i), - searchTerm: t.searchTerm, - campaign: t.campaign, - adGroup: t.adGroup, - impressions: fmtNum(t.impressions), - clicks: fmtNum(t.clicks), - ctr: fmtPct(t.ctr), - cost: fmtMoney(t.cost), - conversions: fmtNum(t.conversions), - action, - _isHighCost: isHighCostLowConv, - }; - }); - }, [filtered, costThreshold]); - - if (error) return

    Error

    {error.message}

    ; - if (!isConnected) return

    Connecting...

    ; - if (!data) return

    Waiting for data...

    ; - - return ( -
    - {/* Header */} -
    -

    🔍 Search Terms Report

    -

    - {data.period ? `${data.period} · ` : ''}{fmtNum(data.terms.length)} search terms · {fmtMoney(totalCost)} total cost -

    -
    - - {/* KPI Cards */} -
    - - - -
    - - {/* Filters */} -
    - setSearch(e.target.value)} - style={{ - padding: '6px 12px', borderRadius: 6, border: '1px solid var(--gads-border-light)', - background: 'var(--gads-bg-input)', color: 'var(--gads-text-primary)', fontSize: 12, - outline: 'none', flex: '1 1 200px', minWidth: 150, - }} - /> - {[ - { key: 'ALL', label: 'All' }, - { key: 'add_keyword', label: '+ Keywords' }, - { key: 'add_negative', label: '— Negatives' }, - { key: 'none', label: 'No Action' }, - ].map(f => ( - - ))} -
    - - {/* Table with custom rendering for action badges and row highlighting */} -
    -
    - - - - - - - - - - - - - - - - {tableRows.length === 0 ? ( - - ) : tableRows.map(row => ( - - - - - - - - - - - - ))} - -
    Search TermCampaignAd GroupImpr.ClicksCTRCostConv.Action
    No search terms found
    {row.searchTerm}{row.campaign}{row.adGroup}{row.impressions}{row.clicks}{row.ctr}{row.cost}{row.conversions} - {row.action === 'add_keyword' ? ( - + Add Keyword - ) : row.action === 'add_negative' ? ( - — Add Negative - ) : ( - No Action - )} -
    -
    -
    - - {/* Legend */} -
    - - Highlighted rows indicate high-cost terms ({'>'}{fmtMoney(costThreshold)}) with zero conversions -
    -
    - ); -} diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/search-terms/index.html b/mcp-diagrams/google-ads-mcp/ui/src/apps/search-terms/index.html deleted file mode 100644 index 9b2e1f2..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/search-terms/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - -Search Terms Report -
    - diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/search-terms/main.tsx b/mcp-diagrams/google-ads-mcp/ui/src/apps/search-terms/main.tsx deleted file mode 100644 index f774ec9..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/search-terms/main.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import { App } from './App'; -createRoot(document.getElementById('root')!).render(); diff --git a/mcp-diagrams/google-ads-mcp/ui/src/apps/search-terms/vite.config.ts b/mcp-diagrams/google-ads-mcp/ui/src/apps/search-terms/vite.config.ts deleted file mode 100644 index 7d7407a..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/apps/search-terms/vite.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -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/app-ui/search-terms'), - emptyOutDir: true, - rollupOptions: { input: path.resolve(__dirname, 'index.html') }, - }, - resolve: { - alias: { - '@components': path.resolve(__dirname, '../../components'), - '@styles': path.resolve(__dirname, '../../styles'), - }, - }, -}); diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/charts/BarChart.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/charts/BarChart.tsx deleted file mode 100644 index 26c4434..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/charts/BarChart.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React from 'react'; -import type { BarChartProps } from '../../types.js'; - -const COLORS = ['#4f46e5','#7c3aed','#16a34a','#3b82f6','#eab308','#ef4444','#ec4899','#f97316']; - -export const BarChart: React.FC = ({ - bars = [], - orientation = 'vertical', - maxValue, - showValues = true, - title, -}) => { - const max = maxValue || Math.max(...bars.map(b => b.value), 1); - - if (orientation === 'horizontal') { - return ( -
    - {title &&
    {title}
    } -
    - {bars.map((b, i) => { - const pct = Math.min(100, (b.value / max) * 100); - const color = b.color || COLORS[i % COLORS.length]; - return ( -
    - {b.label} -
    -
    -
    - {showValues && ( - - {Number(b.value).toLocaleString()} - - )} -
    - ); - })} -
    -
    - ); - } - - // Vertical bars via SVG - const svgW = Math.max(bars.length * 60, 200); - const svgH = 180; - const padTop = 10; - const padBot = 30; - const barW = Math.min(36, (svgW / bars.length) * 0.6); - const gap = svgW / bars.length; - const plotH = svgH - padTop - padBot; - - return ( -
    - {title &&
    {title}
    } -
    - - - {bars.map((b, i) => { - 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 || COLORS[i % COLORS.length]; - return ( - - - {showValues && ( - - {Number(b.value).toLocaleString()} - - )} - - {b.label} - - - ); - })} - -
    -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/charts/FunnelChart.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/charts/FunnelChart.tsx deleted file mode 100644 index c6bf4f8..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/charts/FunnelChart.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import type { FunnelChartProps } from '../../types.js'; - -const COLORS = ['#4f46e5','#7c3aed','#16a34a','#3b82f6','#eab308','#ef4444','#ec4899','#f97316']; - -export const FunnelChart: React.FC = ({ - stages = [], - showDropoff = true, - title, -}) => { - if (stages.length === 0) { - return
    No data
    ; - } - - const maxVal = stages[0]?.value || 1; - - return ( -
    - {title &&
    {title}
    } -
    - {stages.map((s, i) => { - const pct = Math.max(20, (s.value / maxVal) * 100); - const color = s.color || COLORS[i % COLORS.length]; - const dropoff = - i > 0 - ? ( - ((stages[i - 1].value - s.value) / stages[i - 1].value) * - 100 - ).toFixed(1) - : null; - return ( -
    -
    - {s.label} - - {Number(s.value).toLocaleString()} - -
    -
    -
    -
    - {showDropoff && dropoff !== null ? ( - -{dropoff}% - ) : ( - - )} -
    - ); - })} -
    -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/charts/LineChart.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/charts/LineChart.tsx deleted file mode 100644 index 6f25aee..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/charts/LineChart.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import React from 'react'; -import type { LineChartProps } from '../../types.js'; - -export const LineChart: React.FC = ({ - points = [], - color = '#4f46e5', - showPoints = true, - showArea = false, - title, - yAxisLabel, -}) => { - if (points.length === 0) { - return
    No data
    ; - } - - const vals = points.map(p => p.value); - const minV = Math.min(...vals); - const maxV = Math.max(...vals); - const range = maxV - minV || 1; - - const svgW = Math.max(points.length * 60, 200); - const svgH = 180; - const padL = 40; - const padR = 10; - const padT = 16; - const padB = 30; - const plotW = svgW - padL - padR; - const plotH = svgH - padT - padB; - - const pts = points.map((p, i) => { - const x = padL + (plotW / Math.max(points.length - 1, 1)) * i; - const y = padT + plotH - ((p.value - minV) / range) * plotH; - return { x, y, label: p.label, value: p.value }; - }); - - const linePath = pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' '); - const areaPath = showArea - ? `${linePath} L${pts[pts.length - 1].x},${padT + plotH} L${pts[0].x},${padT + plotH} Z` - : ''; - - // Y-axis ticks (5 ticks) - const ticks = Array.from({ length: 5 }, (_, i) => { - const val = minV + (range * i) / 4; - const y = padT + plotH - (plotH * i) / 4; - return { val, y }; - }); - - return ( -
    - {title &&
    {title}
    } -
    - - {/* Grid lines + Y-axis labels */} - {ticks.map((t, i) => ( - - - - {Math.round(t.val).toLocaleString()} - - - ))} - - {/* Y-axis label */} - {yAxisLabel && ( - - {yAxisLabel} - - )} - - {/* Area fill */} - {showArea && areaPath && ( - - )} - - {/* Line */} - - - {/* Points */} - {showPoints && - pts.map((p, i) => ( - - ))} - - {/* X-axis labels */} - {pts.map((p, i) => ( - - {points[i].label} - - ))} - -
    -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/charts/PieChart.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/charts/PieChart.tsx deleted file mode 100644 index 239cb92..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/charts/PieChart.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import React from 'react'; -import type { PieChartProps } from '../../types.js'; - -const COLORS = ['#4f46e5','#7c3aed','#16a34a','#3b82f6','#eab308','#ef4444','#ec4899','#f97316']; - -export const PieChart: React.FC = ({ - segments = [], - donut = false, - title, - showLegend = true, -}) => { - const total = segments.reduce((s, seg) => s + seg.value, 0) || 1; - const r = 70; - const cx = 90; - const cy = 90; - const svgSize = 180; - - let cumAngle = -Math.PI / 2; // start at 12 o'clock - - const arcs = segments.map((seg, i) => { - const frac = seg.value / total; - const angle = frac * Math.PI * 2; - const startAngle = cumAngle; - const endAngle = cumAngle + angle; - cumAngle = endAngle; - - const x1 = cx + r * Math.cos(startAngle); - const y1 = cy + r * Math.sin(startAngle); - const x2 = cx + r * Math.cos(endAngle); - const y2 = cy + r * Math.sin(endAngle); - const largeArc = angle > Math.PI ? 1 : 0; - const color = seg.color || COLORS[i % COLORS.length]; - - // Single full segment - if (frac >= 0.9999) { - return ; - } - - return ( - - ); - }); - - return ( -
    - {title &&
    {title}
    } -
    - - {arcs} - {donut && ( - - )} - {donut && ( - - {total.toLocaleString()} - - )} - - - {showLegend && ( -
    - {segments.map((seg, i) => { - const color = seg.color || COLORS[i % COLORS.length]; - const pct = ((seg.value / total) * 100).toFixed(1); - return ( -
    - - {seg.label} - {pct}% -
    - ); - })} -
    - )} -
    -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/charts/SparklineChart.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/charts/SparklineChart.tsx deleted file mode 100644 index ebd6b37..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/charts/SparklineChart.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import type { SparklineChartProps } from '../../types.js'; - -export const SparklineChart: React.FC = ({ - values = [], - color = '#4f46e5', - height = 24, - width = 80, -}) => { - if (values.length < 2) { - return ( - - — - - ); - } - - const minV = Math.min(...values); - const maxV = Math.max(...values); - const range = maxV - minV || 1; - const pad = 2; - - const pts = values - .map((v, i) => { - const x = pad + ((width - pad * 2) / (values.length - 1)) * i; - const y = - pad + (height - pad * 2) - ((v - minV) / range) * (height - pad * 2); - return `${x},${y}`; - }) - .join(' '); - - return ( - - - - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/comms/ChatThread.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/comms/ChatThread.tsx deleted file mode 100644 index 7783c6b..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/comms/ChatThread.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React, { useEffect, useRef } from 'react'; -import type { ChatThreadProps, ChatMessage } from '../../types.js'; - -const chatTypeIcons: Record = { - sms: '💬', - email: '📧', - call: '📞', - whatsapp: '📱', -}; - -const avatarColors = ['#4f46e5', '#7c3aed', '#059669', '#d97706', '#dc2626', '#0891b2']; - -function getAvatarColor(name: string): string { - return avatarColors[(name || '').charCodeAt(0) % avatarColors.length]; -} - -function getInitials(name: string): string { - return (name || '') - .split(' ') - .map(n => n[0]) - .join('') - .slice(0, 2) - .toUpperCase(); -} - -export const ChatThread: React.FC = ({ - messages = [], - title, -}) => { - const bodyRef = useRef(null); - - // Auto-scroll to bottom on mount / messages change - useEffect(() => { - if (bodyRef.current) { - bodyRef.current.scrollTop = bodyRef.current.scrollHeight; - } - }, [messages]); - - if (messages.length === 0) { - return ( -
    -
    💬
    -

    No messages

    -
    - ); - } - - return ( -
    - {title && ( -
    -

    {title}

    - {messages.length} messages -
    - )} -
    - {messages.map((msg: ChatMessage, i: number) => { - const isOutbound = msg.direction === 'outbound'; - const typeIcon = chatTypeIcons[msg.type || 'sms'] || '💬'; - const avatarBg = isOutbound - ? '#4f46e5' - : getAvatarColor(msg.senderName || 'U'); - const initials = getInitials( - msg.senderName || (isOutbound ? 'You' : 'Contact'), - ); - - return ( -
    - {!isOutbound && ( -
    - {msg.avatar ? ( - - ) : ( - initials - )} -
    - )} -
    - {msg.senderName && ( -
    {msg.senderName}
    - )} -
    - {msg.content} -
    -
    - {typeIcon} {msg.timestamp || ''} -
    -
    - {isOutbound && ( -
    - {msg.avatar ? ( - - ) : ( - initials - )} -
    - )} -
    - ); - })} -
    -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/comms/ContentPreview.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/comms/ContentPreview.tsx deleted file mode 100644 index a88a552..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/comms/ContentPreview.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { useMemo } from 'react'; -import type { ContentPreviewProps } from '../../types.js'; - -function sanitizeHtml(html: string): string { - return String(html) - .replace(//gi, '') - .replace(//gi, '') - .replace(/on\w+\s*=\s*"[^"]*"/gi, '') - .replace(/on\w+\s*=\s*'[^']*'/gi, ''); -} - -function escapeHtml(s: string): string { - return s - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"'); -} - -function renderMarkdown(md: string): string { - return escapeHtml(md) - .replace(/^### (.+)$/gm, '

    $1

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

    $1

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

    $1

    ') - .replace(/\*\*(.+?)\*\*/g, '$1') - .replace(/\*(.+?)\*/g, '$1') - .replace(/\[(.+?)\]\((.+?)\)/g, '$1') - .replace(/\n/g, '
    '); -} - -export const ContentPreview: React.FC = ({ - content = '', - format = 'html', - maxHeight, - title, -}) => { - const rendered = useMemo(() => { - if (format === 'html') { - return sanitizeHtml(content); - } - if (format === 'markdown') { - return renderMarkdown(content); - } - // text format - return `
    ${escapeHtml(content)}
    `; - }, [content, format]); - - const heightStyle: React.CSSProperties = maxHeight - ? { maxHeight: `${maxHeight}px`, overflowY: 'auto' } - : {}; - - return ( -
    - {title && ( -
    -

    {title}

    -
    - )} -
    -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/comms/EmailPreview.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/comms/EmailPreview.tsx deleted file mode 100644 index 1732621..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/comms/EmailPreview.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { useMemo } from 'react'; -import type { EmailPreviewProps } from '../../types.js'; - -function sanitizeHtml(html: string): string { - return String(html) - .replace(//gi, '') - .replace(//gi, '') - .replace(/on\w+\s*=\s*"[^"]*"/gi, '') - .replace(/on\w+\s*=\s*'[^']*'/gi, ''); -} - -export const EmailPreview: React.FC = ({ - from, - to, - subject, - date, - body = '', - cc, - attachments = [], -}) => { - const sanitizedBody = useMemo(() => sanitizeHtml(body), [body]); - - return ( -
    -
    -
    {subject}
    -
    - From - {from} -
    -
    - To - {to} -
    - {cc && ( -
    - Cc - {cc} -
    - )} -
    - Date - {date} -
    -
    - - {attachments.length > 0 && ( -
    - {attachments.map((a, i) => ( -
    - 📎 {a.name} - {a.size && ( - ({a.size}) - )} -
    - ))} -
    - )} - -
    -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/comms/TranscriptView.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/comms/TranscriptView.tsx deleted file mode 100644 index 3ea4438..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/comms/TranscriptView.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React from 'react'; -import type { TranscriptViewProps, TranscriptEntry } from '../../types.js'; - -const speakerRoleColors: Record = { - agent: '#4f46e5', - customer: '#059669', - system: '#6b7280', -}; - -const speakerRoleLabels: Record = { - agent: 'Agent', - customer: 'Customer', - system: 'System', -}; - -export const TranscriptView: React.FC = ({ - entries = [], - title, - duration, -}) => { - if (entries.length === 0) { - return ( -
    -
    📝
    -

    No transcript available

    -
    - ); - } - - return ( -
    -
    -
    - {title &&

    {title}

    } -
    -
    - {duration && ( - ⏱ {duration} - )} - {entries.length} entries -
    -
    -
    - {entries.map((e: TranscriptEntry, i: number) => { - const roleColor = - speakerRoleColors[e.speakerRole || 'customer'] || '#6b7280'; - const roleLabel = speakerRoleLabels[e.speakerRole || ''] || ''; - - return ( -
    -
    {e.timestamp}
    -
    -
    - - {e.speaker} - {roleLabel && ( - - {roleLabel} - - )} -
    -
    {e.text}
    -
    -
    - ); - })} -
    -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/data/AudioPlayer.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/data/AudioPlayer.tsx deleted file mode 100644 index 4f73d3c..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/data/AudioPlayer.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from "react"; -import type { AudioPlayerProps } from "../../types.js"; - -export const AudioPlayer: React.FC = ({ - title, - duration, - type = "recording", -}) => { - const typeIcon = type === "voicemail" ? "📩" : "🎙"; - const typeLabel = type === "voicemail" ? "Voicemail" : "Recording"; - - // Deterministic pseudo-wave based on index - const bars = Array.from({ length: 32 }, (_, i) => { - 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} -
    -
    {title || typeLabel}
    -
    {typeLabel}
    -
    -
    -
    - -
    - {bars.map((h, i) => ( -
    - ))} -
    - {duration || "0:00"} -
    -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/data/AvatarGroup.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/data/AvatarGroup.tsx deleted file mode 100644 index e79e93e..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/data/AvatarGroup.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React from "react"; -import type { AvatarGroupProps } from "../../types.js"; - -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(); -} - -const avatarSizeMap: Record = { - sm: "ag-sm", - md: "ag-md", - lg: "ag-lg", -}; - -export const AvatarGroup: React.FC = ({ - avatars = [], - max = 5, - size = "md", -}) => { - const visible = avatars.slice(0, max); - const overflow = avatars.length - max; - const sizeCls = avatarSizeMap[size] || "ag-md"; - - return ( -
    - {visible.map((a, i) => { - const name = a.name || ""; - const initials = a.initials || getInitials(name); - const color = getAvatarColor(name); - - if (a.imageUrl) { - return ( -
    - {name} -
    - ); - } - - return ( -
    - {initials} -
    - ); - })} - {overflow > 0 && ( -
    - +{overflow} -
    - )} -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/data/CardGrid.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/data/CardGrid.tsx deleted file mode 100644 index 59707f2..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/data/CardGrid.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from "react"; -import type { CardGridProps } from "../../types.js"; - -const cardStatusClasses: Record = { - active: "cg-status-active", - complete: "cg-status-complete", - draft: "cg-status-draft", - error: "cg-status-error", - pending: "cg-status-pending", -}; - -export const CardGrid: React.FC = ({ - cards = [], - columns = 3, -}) => { - return ( -
    - {cards.map((c, i) => ( -
    - {c.imageUrl ? ( -
    - ) : ( -
    - 📄 -
    - )} -
    -
    {c.title}
    - {c.subtitle && ( -
    {c.subtitle}
    - )} - {c.description && ( -
    {c.description}
    - )} -
    - {c.status ? ( - - {c.status} - - ) : ( - - )} - {c.action && ( - - )} -
    -
    -
    - ))} -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/data/ChecklistView.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/data/ChecklistView.tsx deleted file mode 100644 index ea52a62..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/data/ChecklistView.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import React, { useState, useCallback } from "react"; -import type { ChecklistViewProps, ChecklistItem } from "../../types.js"; -import { useMCPApp } from "../../context/MCPAppContext.js"; - -const priorityColors: Record = { - low: "#6b7280", - medium: "#d97706", - high: "#dc2626", -}; - -const priorityLabels: Record = { - low: "Low", - medium: "Med", - high: "High", -}; - -export const ChecklistView: React.FC = ({ - items: initialItems = [], - title, - showProgress, - toggleTool, -}) => { - const { callTool } = useMCPApp(); - const [items, setItems] = useState(initialItems); - - const handleToggle = useCallback( - async (index: number) => { - const item = items[index]; - const newCompleted = !item.completed; - - // Optimistic update - setItems((prev) => - prev.map((it, i) => - i === index ? { ...it, completed: newCompleted } : it, - ), - ); - - if (toggleTool) { - try { - await callTool(toggleTool, { - itemTitle: item.title, - completed: newCompleted, - }); - } catch { - // Revert on error - setItems((prev) => - prev.map((it, i) => - i === index ? { ...it, completed: !newCompleted } : it, - ), - ); - } - } - }, - [items, toggleTool, callTool], - ); - - const completedCount = items.filter((i) => i.completed).length; - const totalCount = items.length; - const pct = - totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0; - - return ( -
    - {(title || showProgress) && ( -
    - {title &&

    {title}

    } - {showProgress && ( -
    - - {completedCount}/{totalCount} done - -
    -
    -
    -
    - )} -
    - )} -
    - {items.length === 0 ? ( -
    -

    No tasks

    -
    - ) : ( - items.map((item, i) => { - const prColor = - priorityColors[item.priority || "low"] || "#6b7280"; - const prLabel = priorityLabels[item.priority || ""] || ""; - - return ( -
    handleToggle(i)} - style={{ cursor: "pointer" }} - > -
    - {item.completed && ( - - - - )} -
    -
    -
    - {item.title} -
    -
    - {item.dueDate && ( - - 📅 {item.dueDate} - - )} - {item.assignee && ( - - 👤 {item.assignee} - - )} - {prLabel && ( - - {prLabel} - - )} -
    -
    -
    - ); - }) - )} -
    -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/data/CurrencyDisplay.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/data/CurrencyDisplay.tsx deleted file mode 100644 index adad415..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/data/CurrencyDisplay.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from "react"; -import type { CurrencyDisplayProps } from "../../types.js"; - -const currencySizeClasses: Record = { - sm: "currency-sm", - md: "currency-md", - lg: "currency-lg", -}; - -export const CurrencyDisplay: React.FC = ({ - amount, - currency = "USD", - locale = "en-US", - size = "md", - positive, - negative, -}) => { - 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 ( - - {formatted} - - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/data/DataTable.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/data/DataTable.tsx deleted file mode 100644 index 0e4abfc..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/data/DataTable.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import React, { useState, useMemo, useCallback } from 'react'; -import type { DataTableProps, TableColumn } from '../../types.js'; - -// ─── Utility helpers ──────────────────────────────────── - -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(); -} - -// ─── Cell Formatter ───────────────────────────────────── - -function FormatCell({ value, format }: { value: unknown; format?: string }): React.ReactElement { - if (value === null || value === undefined) { - return ; - } - - switch (format) { - case 'email': - return {String(value)}; - case 'phone': - return {String(value)}; - case 'date': - return {String(value)}; - case 'currency': - return {String(value)}; - case 'tags': { - const tags = Array.isArray(value) ? value : [value]; - const visible = tags.slice(0, 3); - return ( -
    - {visible.map((t, i) => {String(t)})} - {tags.length > 3 && +{tags.length - 3}} -
    - ); - } - case 'avatar': { - const name = String(value); - const color = getAvatarColor(name); - return ( -
    -
    {getInitials(name)}
    - {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 {String(value)}; - } - default: - return {String(value)}; - } -} - -// ─── Props ────────────────────────────────────────────── - -interface Props extends DataTableProps { - children?: React.ReactNode; - onRowClick?: (rowId: string) => void; -} - -// ─── Component ────────────────────────────────────────── - -export const DataTable: React.FC = ({ - columns = [], rows = [], selectable, emptyMessage, pageSize = 10, onRowClick, -}) => { - const [currentPage, setCurrentPage] = useState(0); - const [sortKey, setSortKey] = useState(null); - const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc'); - - const handleSort = useCallback((col: TableColumn) => { - if (!col.sortable) return; - if (sortKey === col.key) { - setSortDir(d => d === 'asc' ? 'desc' : 'asc'); - } else { - setSortKey(col.key); - setSortDir('asc'); - } - setCurrentPage(0); - }, [sortKey]); - - const sortedRows = useMemo(() => { - if (!sortKey) return rows; - return [...rows].sort((a, b) => { - const av = a[sortKey]; - const bv = b[sortKey]; - if (av == null && bv == null) return 0; - if (av == null) return 1; - if (bv == null) return -1; - const cmp = String(av).localeCompare(String(bv), undefined, { numeric: true }); - return sortDir === 'asc' ? cmp : -cmp; - }); - }, [rows, sortKey, sortDir]); - - if (rows.length === 0) { - return ( -
    -
    📋
    -

    {emptyMessage || 'No data available'}

    -
    - ); - } - - const totalPages = Math.ceil(sortedRows.length / pageSize); - const start = currentPage * pageSize; - const displayRows = sortedRows.slice(start, start + pageSize); - - return ( -
    -
    - - - - {selectable && ( - - )} - {columns.map(col => ( - - ))} - - - - {displayRows.map((row, ri) => ( - row.id && onRowClick?.(row.id)} - > - {selectable && ( - - )} - {columns.map(col => ( - - ))} - - ))} - -
    handleSort(col)} - > - {col.label} - {sortKey === col.key && ( - {sortDir === 'asc' ? ' ▲' : ' ▼'} - )} -
    - -
    -
    - {totalPages > 1 && ( -
    - - {start + 1}–{Math.min(start + pageSize, sortedRows.length)} of {sortedRows.length} - -
    - - -
    -
    - )} -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/data/DetailHeader.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/data/DetailHeader.tsx deleted file mode 100644 index e607c7e..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/data/DetailHeader.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import type { DetailHeaderProps } from '../../types.js'; - -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', -}; - -interface Props extends DetailHeaderProps { - children?: React.ReactNode; -} - -export const DetailHeader: React.FC = ({ title, subtitle, entityId, status, statusVariant, children }) => { - const cls = statusColors[statusVariant || 'active'] || 'status-active'; - - return ( -
    -
    -
    -

    {title}

    - {entityId &&

    {entityId}

    } - {subtitle &&

    {subtitle}

    } -
    - {status && {status}} -
    - {children} -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/data/InfoBlock.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/data/InfoBlock.tsx deleted file mode 100644 index 743533d..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/data/InfoBlock.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import type { InfoBlockProps } from '../../types.js'; - -interface Props extends InfoBlockProps { - children?: React.ReactNode; -} - -export const InfoBlock: React.FC = ({ label, name, lines = [] }) => { - return ( -
    -

    {label}

    -
    {name}
    -
    - {lines.map((line, i) => ( -
    {line}
    - ))} -
    -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/data/KanbanBoard.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/data/KanbanBoard.tsx deleted file mode 100644 index 65661c7..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/data/KanbanBoard.tsx +++ /dev/null @@ -1,229 +0,0 @@ -/** - * KanbanBoard — Drag-and-drop kanban board for pipeline/opportunity management. - * - * Uses useSmartAction for resilient drag-drop persistence with optimistic UI. - * Falls back to change tracking when direct tool calls aren't available. - */ -import React, { useState, useCallback, useRef } from 'react'; -import type { KanbanBoardProps, KanbanColumn, KanbanCard } from '../../types.js'; -import { useSmartAction } from '../../hooks/useSmartAction.js'; - -// ─── Status classes for kanban cards ──────────────────── - -const kanbanStatusClasses: Record = { - open: 'status-open', won: 'status-won', lost: 'status-lost', abandoned: 'status-draft', -}; - -// ─── Drag state ───────────────────────────────────────── - -interface DragState { - cardId: string; - fromStageId: string; -} - -// ─── Props ────────────────────────────────────────────── - -interface Props extends KanbanBoardProps { - children?: React.ReactNode; -} - -// ─── Component ────────────────────────────────────────── - -export const KanbanBoard: React.FC = ({ columns: initialColumns = [], moveTool, cardClickTool }) => { - const { executeAction } = useSmartAction(); - - // Local mutable columns state for optimistic drag-and-drop - const [columns, setColumns] = useState(initialColumns); - const [dropTargetStage, setDropTargetStage] = useState(null); - const [draggingCardId, setDraggingCardId] = useState(null); - const dragRef = useRef(null); - - // Keep columns in sync if props change (e.g. after tool call refreshes tree) - const prevColumnsRef = useRef(initialColumns); - if (prevColumnsRef.current !== initialColumns) { - prevColumnsRef.current = initialColumns; - setColumns(initialColumns); - } - - // ─── Drag handlers ─────────────────────────────────── - - const onDragStart = useCallback((e: React.DragEvent, cardId: string, stageId: string) => { - dragRef.current = { cardId, fromStageId: stageId }; - setDraggingCardId(cardId); - e.dataTransfer.effectAllowed = 'move'; - // Set a small timeout to apply the dragging class after browser paints the drag image - requestAnimationFrame(() => { - setDraggingCardId(cardId); - }); - }, []); - - const onDragEnd = useCallback(() => { - dragRef.current = null; - setDraggingCardId(null); - setDropTargetStage(null); - }, []); - - const onDragOver = useCallback((e: React.DragEvent, stageId: string) => { - e.preventDefault(); - e.dataTransfer.dropEffect = 'move'; - setDropTargetStage(stageId); - }, []); - - const onDragLeave = useCallback((e: React.DragEvent) => { - // Only clear if we're actually leaving the column body (not entering a child) - const relatedTarget = e.relatedTarget as HTMLElement | null; - const currentTarget = e.currentTarget as HTMLElement; - if (!relatedTarget || !currentTarget.contains(relatedTarget)) { - setDropTargetStage(null); - } - }, []); - - 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); - - // Don't do anything if dropped on the same column - if (fromStageId === toStageId) return; - - // 1. Optimistic UI: move card in local state immediately - let movedCard: KanbanCard | undefined; - let movedCardTitle = ""; - const prevColumns = columns; - - // Find card details for description - for (const col of columns) { - const card = col.cards?.find(c => c.id === cardId); - if (card) { - movedCardTitle = card.title || cardId; - break; - } - } - - const fromStageName = columns.find(c => c.id === fromStageId)?.title || fromStageId; - const toStageName = columns.find(c => c.id === toStageId)?.title || toStageId; - - setColumns(prev => { - const next = prev.map(col => { - if (col.id === fromStageId) { - const newCards = (col.cards || []).filter(c => { - if (c.id === cardId) { - movedCard = c; - return false; - } - return true; - }); - return { - ...col, - cards: newCards, - count: newCards.length, - }; - } - return col; - }); - - if (!movedCard) return prev; - - return next.map(col => { - if (col.id === toStageId) { - const newCards = [...(col.cards || []), movedCard!]; - return { - ...col, - cards: newCards, - count: newCards.length, - }; - } - return col; - }); - }); - - // 2. Call moveTool via smartAction if provided - if (moveTool) { - const result = await executeAction({ - type: moveTool, - args: { - opportunityId: cardId, - pipelineStageId: toStageId, - }, - description: `Move "${movedCardTitle}" from ${fromStageName} → ${toStageName}`, - }); - - // If direct call failed and was NOT queued, revert - if (!result.success && !result.queued) { - console.error('KanbanBoard: move failed, reverting'); - setColumns(prevColumns); - } - } - }, [columns, moveTool, executeAction]); - - // ─── Render ────────────────────────────────────────── - - return ( -
    -
    - {columns.map(col => ( -
    -
    -
    - {col.title} - - {col.count ?? col.cards?.length ?? 0} - -
    - {col.totalValue &&
    {col.totalValue}
    } -
    -
    onDragOver(e, col.id)} - onDragLeave={onDragLeave} - onDrop={(e) => onDrop(e, col.id)} - > - {(!col.cards || col.cards.length === 0) ? ( -
    No items
    - ) : ( - col.cards.map(card => ( -
    onDragStart(e, card.id, col.id)} - onDragEnd={onDragEnd} - > -
    {card.title}
    - {card.subtitle && ( -
    - {card.avatarInitials && ( -
    {card.avatarInitials}
    - )} - {card.subtitle} -
    - )} - {card.value &&
    {card.value}
    } -
    - {card.date ? {card.date} : } - {card.status && ( - - {card.status} - - )} -
    -
    - )) - )} -
    -
    - ))} -
    -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/data/KeyValueList.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/data/KeyValueList.tsx deleted file mode 100644 index 0eb3049..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/data/KeyValueList.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import type { KeyValueListProps } from '../../types.js'; - -interface Props extends KeyValueListProps { - children?: React.ReactNode; -} - -export const KeyValueList: React.FC = ({ items = [], compact }) => { - return ( -
    - {items.map((item, i) => { - let rowCls = 'kv-row'; - if (item.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'; - - let valueCls: string; - if (item.isTotalRow) valueCls = 'kv-value-total'; - else if (item.bold) valueCls = 'kv-value-bold'; - else if (item.variant === 'danger') valueCls = 'kv-value-danger'; - else if (item.variant === 'success') valueCls = 'kv-value-success'; - else valueCls = 'kv-value'; - - return ( -
    - {item.label} - {item.value} -
    - ); - })} -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/data/LineItemsTable.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/data/LineItemsTable.tsx deleted file mode 100644 index ac73c9c..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/data/LineItemsTable.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import type { LineItemsTableProps } from '../../types.js'; - -function fmtCurrency(n: number, currency = 'USD'): string { - try { - return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n); - } catch { - return `$${n.toFixed(2)}`; - } -} - -interface Props extends LineItemsTableProps { - children?: React.ReactNode; -} - -export const LineItemsTable: React.FC = ({ items = [], currency = 'USD' }) => { - return ( -
    - - - - - - - - - - - {items.map((item, i) => ( - - - - - - - ))} - -
    ItemQtyPriceTotal
    -
    {item.name}
    - {item.description && ( -
    {item.description}
    - )} -
    {item.quantity}{fmtCurrency(item.unitPrice, currency)}{fmtCurrency(item.total, currency)}
    -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/data/MetricCard.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/data/MetricCard.tsx deleted file mode 100644 index 17f409d..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/data/MetricCard.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import type { MetricCardProps } from '../../types.js'; - -const metricColorClasses: Record = { - default: '', green: 'metric-green', blue: 'metric-blue', purple: 'metric-purple', - yellow: 'metric-yellow', red: 'metric-red', -}; - -const trendClasses: Record = { up: 'trend-up', down: 'trend-down', flat: 'trend-flat' }; -const trendIcons: Record = { up: '↑', down: '↓', flat: '→' }; - -interface Props extends MetricCardProps { - children?: React.ReactNode; -} - -export const MetricCard: React.FC = ({ label, value, trend, trendValue, color = 'default' }) => { - const colorCls = metricColorClasses[color] || ''; - - return ( -
    -
    {value}
    -
    {label}
    - {trend && trendValue && ( -
    - {trendIcons[trend] || '→'} {trendValue} -
    - )} -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/data/ProgressBar.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/data/ProgressBar.tsx deleted file mode 100644 index a0553a1..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/data/ProgressBar.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import type { ProgressBarProps } from '../../types.js'; - -const barColorClasses: Record = { - green: 'bar-green', blue: 'bar-blue', purple: 'bar-purple', - yellow: 'bar-yellow', red: 'bar-red', -}; - -interface Props extends ProgressBarProps { - children?: React.ReactNode; -} - -export const ProgressBar: React.FC = ({ - label, value, max = 100, color = 'blue', showPercent = true, - benchmark, benchmarkLabel, -}) => { - const pct = Math.min(100, (value / max) * 100); - const colorCls = barColorClasses[color] || 'bar-blue'; - - return ( -
    -
    - {label} - - {Number(value).toLocaleString()} - {max !== 100 ? ` / ${Number(max).toLocaleString()}` : ''} - {showPercent ? ` (${pct.toFixed(1)}%)` : ''} - -
    -
    -
    - {benchmark !== undefined && ( - <> -
    - {benchmarkLabel && ( - - {benchmarkLabel} - - )} - - )} -
    -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/data/StarRating.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/data/StarRating.tsx deleted file mode 100644 index 079ee53..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/data/StarRating.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from "react"; -import type { StarRatingProps } from "../../types.js"; - -export const StarRating: React.FC = ({ - rating = 0, - count, - maxStars = 5, - distribution, - showDistribution, -}) => { - 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) => d.count), 1) - : 1; - - const sorted = distribution - ? [...distribution].sort((a, b) => b.stars - a.stars) - : []; - - return ( -
    -
    - {rating.toFixed(1)} - {stars} - {count !== undefined && ( - - ({Number(count).toLocaleString()} reviews) - - )} -
    - {showDistribution && distribution && ( -
    - {sorted.map((d) => { - const pct = (d.count / maxCount) * 100; - return ( -
    - {d.stars}★ -
    -
    -
    - {d.count} -
    - ); - })} -
    - )} -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/data/StatusBadge.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/data/StatusBadge.tsx deleted file mode 100644 index 455bab2..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/data/StatusBadge.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import type { StatusBadgeProps } from '../../types.js'; - -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', -}; - -interface Props extends StatusBadgeProps { - children?: React.ReactNode; -} - -export const StatusBadge: React.FC = ({ label, variant }) => { - const cls = statusColors[variant || 'active'] || 'status-active'; - - return ( - {label} - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/data/StockIndicator.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/data/StockIndicator.tsx deleted file mode 100644 index 2deee83..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/data/StockIndicator.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from "react"; -import type { StockIndicatorProps } from "../../types.js"; - -export const StockIndicator: React.FC = ({ - quantity, - lowThreshold = 10, - criticalThreshold = 3, - label, -}) => { - let level: string; - let levelCls: string; - let 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 &&
    {label}
    } -
    {quantity} units
    -
    {level}
    -
    -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/data/TagList.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/data/TagList.tsx deleted file mode 100644 index 5f6a5f9..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/data/TagList.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from "react"; -import type { TagListProps, TagItem } from "../../types.js"; - -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", -}; - -export const TagList: React.FC = ({ - tags = [], - maxVisible, - size = "md", -}) => { - const visible = maxVisible ? tags.slice(0, maxVisible) : tags; - const remaining = maxVisible ? Math.max(0, tags.length - maxVisible) : 0; - const sizeCls = size === "sm" ? "tag-list-sm" : "tag-list-md"; - - return ( -
    - {visible.map((t, i) => { - const isObj = typeof t === "object" && t !== null; - const label = isObj ? (t as TagItem).label : (t as string); - const color = (isObj && (t as TagItem).color) || "blue"; - const variant = (isObj && (t as TagItem).variant) || "filled"; - const colorCls = tagColorMap[color] || "tag-pill-blue"; - const variantCls = variant === "outlined" ? "tag-pill-outlined" : ""; - - return ( - - {label} - - ); - })} - {remaining > 0 && ( - +{remaining} - )} -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/data/Timeline.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/data/Timeline.tsx deleted file mode 100644 index 6ebdb46..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/data/Timeline.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import type { TimelineProps } from '../../types.js'; - -const variantBorderClasses: Record = { - default: 'tl-border-default', success: 'tl-border-success', - warning: 'tl-border-warning', error: 'tl-border-error', -}; - -const iconMap: Record = { - email: '📧', phone: '📞', note: '📝', meeting: '📅', task: '✅', system: '⚙️', -}; - -interface Props extends TimelineProps { - children?: React.ReactNode; -} - -export const Timeline: React.FC = ({ events = [] }) => { - return ( -
    -
    - {events.map((e, i) => ( -
    -
    - {iconMap[e.icon || 'system'] || '•'} -
    -
    -
    {e.title}
    - {e.description &&
    {e.description}
    } -
    {e.timestamp}
    -
    -
    - ))} -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/interactive/AmountInput.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/interactive/AmountInput.tsx deleted file mode 100644 index ff52726..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/interactive/AmountInput.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/** - * AmountInput — Formatted currency input. - * Shows formatted display ($1,234.56), raw number on focus, reformats on blur. - */ -import React, { useState, useCallback, useRef, useEffect } from "react"; -import type { AmountInputProps } from "../../types.js"; - -export const AmountInput: React.FC = ({ - value = 0, - currency = "USD", - label, - min, - max, -}) => { - const [rawValue, setRawValue] = useState(value); - const [displayValue, setDisplayValue] = useState(""); - const [isFocused, setIsFocused] = useState(false); - const inputRef = useRef(null); - const locale = "en-US"; - - const format = useCallback( - (num: number): string => { - try { - return new Intl.NumberFormat(locale, { - style: "currency", - currency, - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }).format(num); - } catch { - return `$${num.toFixed(2)}`; - } - }, - [currency], - ); - - // Sync from prop - useEffect(() => { - setRawValue(value); - if (!isFocused) { - setDisplayValue(format(value)); - } - }, [value, format, isFocused]); - - // Initial format - useEffect(() => { - if (!isFocused) { - setDisplayValue(format(rawValue)); - } - }, [rawValue, format, isFocused]); - - const handleFocus = () => { - setIsFocused(true); - setDisplayValue(rawValue === 0 ? "" : String(rawValue)); - }; - - const handleBlur = () => { - setIsFocused(false); - let num = parseFloat(displayValue) || 0; - if (min !== undefined && num < min) num = min; - if (max !== undefined && num > max) num = max; - setRawValue(num); - setDisplayValue(format(num)); - }; - - const handleChange = (e: React.ChangeEvent) => { - const val = e.target.value; - // Allow digits, decimal point, and negative sign - if (/^-?\d*\.?\d*$/.test(val) || val === "") { - setDisplayValue(val); - } - }; - - return ( -
    - {label && } - -
    - ); -}; diff --git a/mcp-diagrams/google-ads-mcp/ui/src/components/interactive/AppointmentBooker.tsx b/mcp-diagrams/google-ads-mcp/ui/src/components/interactive/AppointmentBooker.tsx deleted file mode 100644 index ec69759..0000000 --- a/mcp-diagrams/google-ads-mcp/ui/src/components/interactive/AppointmentBooker.tsx +++ /dev/null @@ -1,274 +0,0 @@ -/** - * AppointmentBooker — Date picker + time slots + contact picker + book via tool. - * CRM-agnostic: calendarTool, bookTool, contactSearchTool received as props. - * - * Uses useSmartAction for resilient booking. - */ -import React, { useState, useMemo, useCallback } from "react"; -import { ContactPicker } from "./ContactPicker.js"; -import { useSmartAction } from "../../hooks/useSmartAction.js"; -import type { AppointmentBookerProps } from "../../types.js"; - -const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; - -const TIME_SLOTS = [ - "9:00 AM", - "9:30 AM", - "10:00 AM", - "10:30 AM", - "11:00 AM", - "11:30 AM", - "12:00 PM", - "12:30 PM", - "1:00 PM", - "1:30 PM", - "2:00 PM", - "2:30 PM", - "3:00 PM", - "3:30 PM", - "4:00 PM", - "4:30 PM", - "5:00 PM", -]; - -export const AppointmentBooker: React.FC = ({ - slots, - calendarTool, - bookTool, - calendarId, - contactSearchTool, -}) => { - const today = new Date(); - const [viewMonth, setViewMonth] = useState(today.getMonth()); - const [viewYear, setViewYear] = useState(today.getFullYear()); - const [selectedDate, setSelectedDate] = useState(null); - const [selectedTime, setSelectedTime] = useState(null); - const [notes, setNotes] = useState(""); - const [contactId, setContactId] = useState(null); - const [contactName, setContactName] = useState(null); - const [isBooking, setIsBooking] = useState(false); - const [bookResult, setBookResult] = useState<"success" | "queued" | null>(null); - const { executeAction } = useSmartAction(); - - // Build calendar grid - const calendarDays = useMemo(() => { - const firstDay = new Date(viewYear, viewMonth, 1).getDay(); - const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate(); - const cells: (number | null)[] = []; - - for (let i = 0; i < firstDay; i++) cells.push(null); - for (let d = 1; d <= daysInMonth; d++) cells.push(d); - // Pad to fill last row - while (cells.length % 7 !== 0) cells.push(null); - - return cells; - }, [viewMonth, viewYear]); - - const monthLabel = new Date(viewYear, viewMonth).toLocaleDateString("en-US", { - month: "long", - year: "numeric", - }); - - const isToday = (day: number) => - day === today.getDate() && - viewMonth === today.getMonth() && - viewYear === today.getFullYear(); - - const dateStr = (day: number) => { - const m = String(viewMonth + 1).padStart(2, "0"); - const d = String(day).padStart(2, "0"); - return `${viewYear}-${m}-${d}`; - }; - - const isPast = (day: number) => { - const d = new Date(viewYear, viewMonth, day); - const t = new Date(); - t.setHours(0, 0, 0, 0); - return d < t; - }; - - // Check slot availability from provided slots - const isSlotAvailable = (date: string, time: string): boolean => { - if (!slots) return true; // All available if no slots provided - return slots.some( - (s) => s.date === date && s.time === time && s.available, - ); - }; - - const handleContactSelect = useCallback((contact: any) => { - setContactId(contact.id); - const name = contact.name || - [contact.firstName, contact.lastName].filter(Boolean).join(" ") || - contact.id; - setContactName(name); - }, []); - - const handleBook = async () => { - if (!bookTool || !selectedDate || !selectedTime) return; - - setIsBooking(true); - setBookResult(null); - - const result = await executeAction({ - type: bookTool, - args: { - calendarId, - contactId, - date: selectedDate, - time: selectedTime, - notes: notes.trim() || undefined, - }, - description: `Book appointment on ${selectedDate} at ${selectedTime}${contactName ? ` for ${contactName}` : ""}`, - }); - - setIsBooking(false); - if (result.success && !result.queued) { - setBookResult("success"); - } else if (result.queued) { - setBookResult("queued"); - } - }; - - const prevMonth = () => { - if (viewMonth === 0) { - setViewMonth(11); - setViewYear(viewYear - 1); - } else { - setViewMonth(viewMonth - 1); - } - }; - - const nextMonth = () => { - if (viewMonth === 11) { - setViewMonth(0); - setViewYear(viewYear + 1); - } else { - setViewMonth(viewMonth + 1); - } - }; - - return ( -
    - {/* Calendar */} -
    -
    - - {monthLabel} - -
    - -
    - {WEEKDAYS.map((d) => ( -
    - {d} -
    - ))} - {calendarDays.map((day, i) => ( -
    { - if (day && !isPast(day)) { - setSelectedDate(dateStr(day)); - setSelectedTime(null); - setBookResult(null); - } - }} - > - {day} -
    - ))} -
    -
    - - {/* Time Slots */} - {selectedDate && ( -
    - -
    - {TIME_SLOTS.map((time) => { - const available = isSlotAvailable(selectedDate, time); - return ( - - ); - })} -
    -
    - )} - - {/* Contact Picker */} - {contactSearchTool && ( -
    - -
    - )} - - {/* Notes */} -
    - -