conversokit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +201 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +22 -0
  4. package/dist/commands/add.d.ts +2 -0
  5. package/dist/commands/add.js +263 -0
  6. package/dist/commands/create.d.ts +2 -0
  7. package/dist/commands/create.js +75 -0
  8. package/dist/commands/deploy.d.ts +2 -0
  9. package/dist/commands/deploy.js +86 -0
  10. package/dist/utils/copy.d.ts +8 -0
  11. package/dist/utils/copy.js +59 -0
  12. package/package.json +54 -0
  13. package/templates/base/.env.example +10 -0
  14. package/templates/base/README.md +36 -0
  15. package/templates/base/apps/mcp-server/package.json +27 -0
  16. package/templates/base/apps/mcp-server/src/index.ts +61 -0
  17. package/templates/base/apps/mcp-server/src/tools/index.ts +5 -0
  18. package/templates/base/apps/mcp-server/tsconfig.json +15 -0
  19. package/templates/base/apps/widget-ui/index.html +12 -0
  20. package/templates/base/apps/widget-ui/package.json +26 -0
  21. package/templates/base/apps/widget-ui/src/App.tsx +28 -0
  22. package/templates/base/apps/widget-ui/src/main.tsx +9 -0
  23. package/templates/base/apps/widget-ui/tsconfig.json +15 -0
  24. package/templates/base/apps/widget-ui/vite.config.ts +7 -0
  25. package/templates/base/package.json +19 -0
  26. package/templates/base/pnpm-workspace.yaml +2 -0
  27. package/templates/booking/apps/mcp-server/src/tools/cancelReservation.ts +28 -0
  28. package/templates/booking/apps/mcp-server/src/tools/createReservation.ts +38 -0
  29. package/templates/booking/apps/mcp-server/src/tools/getAvailability.ts +21 -0
  30. package/templates/booking/apps/mcp-server/src/tools/index.ts +10 -0
  31. package/templates/booking/apps/widget-ui/src/App.tsx +79 -0
  32. package/templates/commerce/apps/mcp-server/src/tools/index.ts +5 -0
  33. package/templates/commerce/apps/mcp-server/src/tools/searchProducts.ts +29 -0
  34. package/templates/commerce/apps/widget-ui/src/App.tsx +75 -0
  35. package/templates/dashboard/apps/mcp-server/src/tools/getAlerts.ts +18 -0
  36. package/templates/dashboard/apps/mcp-server/src/tools/getAnalyticsPanel.ts +17 -0
  37. package/templates/dashboard/apps/mcp-server/src/tools/getKpis.ts +13 -0
  38. package/templates/dashboard/apps/mcp-server/src/tools/getTrendSeries.ts +20 -0
  39. package/templates/dashboard/apps/mcp-server/src/tools/index.ts +14 -0
  40. package/templates/dashboard/apps/widget-ui/src/App.tsx +66 -0
  41. package/templates/deploy/docker/.dockerignore +10 -0
  42. package/templates/deploy/docker/Dockerfile +27 -0
  43. package/templates/deploy/docker/docker-compose.yml +31 -0
  44. package/templates/deploy/railway/Procfile +1 -0
  45. package/templates/deploy/railway/railway.json +14 -0
  46. package/templates/deploy/vercel/api/mcp.ts +10 -0
  47. package/templates/deploy/vercel/vercel.json +19 -0
  48. package/templates/saas-onboarding/apps/mcp-server/src/tools/index.ts +4 -0
  49. package/templates/saas-onboarding/apps/mcp-server/src/tools/submitLead.ts +20 -0
  50. package/templates/saas-onboarding/apps/widget-ui/src/App.tsx +56 -0
  51. package/templates/travel/apps/mcp-server/src/tools/getItinerary.ts +17 -0
  52. package/templates/travel/apps/mcp-server/src/tools/index.ts +12 -0
  53. package/templates/travel/apps/mcp-server/src/tools/listDestinations.ts +28 -0
  54. package/templates/travel/apps/mcp-server/src/tools/searchFlights.ts +22 -0
  55. package/templates/travel/apps/mcp-server/src/tools/searchHotels.ts +29 -0
  56. package/templates/travel/apps/widget-ui/src/App.tsx +89 -0
@@ -0,0 +1,75 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import {
3
+ AddToCartPanel,
4
+ CTABanner,
5
+ CheckoutSummary,
6
+ ConsentBanner,
7
+ ProductCarousel,
8
+ type ProductCardProps
9
+ } from '@conversokit/widgets';
10
+ import {
11
+ EXAMPLE_CHECKOUT_SUMMARY,
12
+ type CartItem
13
+ } from '@conversokit/shared';
14
+ import { ThemeProvider, commerceTheme } from '@conversokit/themes';
15
+ import { BridgeProvider, useBridge } from '@conversokit/bridge';
16
+
17
+ const Catalog: React.FC = () => {
18
+ const bridge = useBridge();
19
+ const [items, setItems] = useState<ProductCardProps[]>([]);
20
+ const [cart, setCart] = useState<CartItem[]>([]);
21
+
22
+ useEffect(() => {
23
+ bridge
24
+ .callTool('search_products', { query: '', limit: 10 })
25
+ .then((r) => setItems((r as { items: ProductCardProps[] }).items))
26
+ .catch(console.error);
27
+ }, [bridge]);
28
+
29
+ const checkout = async () => {
30
+ await bridge.callTool('set_cart', { items: cart, currency: 'USD' });
31
+ const result = (await bridge.callTool('create_checkout', {
32
+ successUrl: window.location.origin + '/?status=success',
33
+ cancelUrl: window.location.origin + '/?status=cancelled'
34
+ })) as { url: string };
35
+ window.location.href = result.url;
36
+ };
37
+
38
+ return (
39
+ <>
40
+ <ProductCarousel items={items} />
41
+ {items[0] && (
42
+ <AddToCartPanel
43
+ product={items[0]}
44
+ onAdd={(item) => setCart((c) => [...c, item])}
45
+ />
46
+ )}
47
+ {cart.length > 0 && (
48
+ <CheckoutSummary
49
+ summary={{ ...EXAMPLE_CHECKOUT_SUMMARY, items: cart }}
50
+ onCheckout={checkout}
51
+ />
52
+ )}
53
+ </>
54
+ );
55
+ };
56
+
57
+ const App: React.FC = () => (
58
+ <BridgeProvider baseUrl="http://localhost:3000">
59
+ <ThemeProvider
60
+ theme={commerceTheme}
61
+ style={{ minHeight: '100vh', padding: 'var(--ck-spacing-4)' }}
62
+ >
63
+ <h1>Welcome to <% projectName %></h1>
64
+ <CTABanner
65
+ title="Commerce demo"
66
+ description="Search → add to cart → checkout via Stripe (or MockPaymentProvider in dev)."
67
+ />
68
+ <ConsentBanner scopes={['analytics']}>
69
+ <Catalog />
70
+ </ConsentBanner>
71
+ </ThemeProvider>
72
+ </BridgeProvider>
73
+ );
74
+
75
+ export default App;
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+ import { EXAMPLE_ALERTS, alertSchema, defineTool } from '@conversokit/shared';
3
+
4
+ export const getAlertsTool = defineTool({
5
+ name: 'get_alerts',
6
+ description: 'Return active operational alerts.',
7
+ inputSchema: z.object({
8
+ severity: z.enum(['info', 'warning', 'critical']).optional()
9
+ }),
10
+ outputSchema: z.object({ items: z.array(alertSchema) }),
11
+ permissions: { requiresAuth: false },
12
+ async handler(input) {
13
+ const items = input.severity
14
+ ? EXAMPLE_ALERTS.filter((a) => a.severity === input.severity)
15
+ : EXAMPLE_ALERTS;
16
+ return { items };
17
+ }
18
+ });
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ EXAMPLE_ANALYTICS_PANEL,
4
+ analyticsPanelSchema,
5
+ defineTool
6
+ } from '@conversokit/shared';
7
+
8
+ export const getAnalyticsPanelTool = defineTool({
9
+ name: 'get_analytics_panel',
10
+ description: 'Return the assembled analytics panel (KPIs + trend series).',
11
+ inputSchema: z.object({}),
12
+ outputSchema: z.object({ panel: analyticsPanelSchema }),
13
+ permissions: { requiresAuth: false },
14
+ async handler() {
15
+ return { panel: EXAMPLE_ANALYTICS_PANEL };
16
+ }
17
+ });
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+ import { EXAMPLE_KPIS, defineTool, kpiSchema } from '@conversokit/shared';
3
+
4
+ export const getKpisTool = defineTool({
5
+ name: 'get_kpis',
6
+ description: 'Return the headline KPIs for the dashboard.',
7
+ inputSchema: z.object({}),
8
+ outputSchema: z.object({ items: z.array(kpiSchema) }),
9
+ permissions: { requiresAuth: false },
10
+ async handler() {
11
+ return { items: EXAMPLE_KPIS };
12
+ }
13
+ });
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ EXAMPLE_TREND_SERIES,
4
+ defineTool,
5
+ trendSeriesSchema
6
+ } from '@conversokit/shared';
7
+
8
+ export const getTrendSeriesTool = defineTool({
9
+ name: 'get_trend_series',
10
+ description: 'Return a time-series trend for a given metric.',
11
+ inputSchema: z.object({
12
+ metric: z.string().optional(),
13
+ range: z.enum(['7d', '30d', '12w', '12m']).optional()
14
+ }),
15
+ outputSchema: z.object({ series: trendSeriesSchema }),
16
+ permissions: { requiresAuth: false },
17
+ async handler() {
18
+ return { series: EXAMPLE_TREND_SERIES };
19
+ }
20
+ });
@@ -0,0 +1,14 @@
1
+ import type { Tool } from '@conversokit/shared';
2
+ import { getKpisTool } from './getKpis.js';
3
+ import { getTrendSeriesTool } from './getTrendSeries.js';
4
+ import { getAnalyticsPanelTool } from './getAnalyticsPanel.js';
5
+ import { getAlertsTool } from './getAlerts.js';
6
+
7
+ // Starter overlay: dashboard tools are unauthenticated for the demo.
8
+ // In production, set permissions.requiresAuth = true on each and wire JWT/Clerk.
9
+ export const tools: Tool[] = [
10
+ getKpisTool,
11
+ getTrendSeriesTool,
12
+ getAnalyticsPanelTool,
13
+ getAlertsTool
14
+ ];
@@ -0,0 +1,66 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import {
3
+ AlertFeed,
4
+ AnalyticsPanel,
5
+ CTABanner
6
+ } from '@conversokit/widgets';
7
+ import {
8
+ type Alert,
9
+ type AnalyticsPanel as AnalyticsPanelData
10
+ } from '@conversokit/shared';
11
+ import { ThemeProvider, enterpriseTheme } from '@conversokit/themes';
12
+ import { BridgeProvider, useBridge } from '@conversokit/bridge';
13
+
14
+ const Dashboard: React.FC = () => {
15
+ const bridge = useBridge();
16
+ const [panel, setPanel] = useState<AnalyticsPanelData | null>(null);
17
+ const [alerts, setAlerts] = useState<Alert[]>([]);
18
+
19
+ useEffect(() => {
20
+ Promise.all([
21
+ bridge.callTool('get_analytics_panel', {}) as Promise<{
22
+ panel: AnalyticsPanelData;
23
+ }>,
24
+ bridge.callTool('get_alerts', {}) as Promise<{ items: Alert[] }>
25
+ ])
26
+ .then(([p, a]) => {
27
+ setPanel(p.panel);
28
+ setAlerts(a.items);
29
+ })
30
+ .catch(console.error);
31
+ }, [bridge]);
32
+
33
+ return (
34
+ <>
35
+ {panel && <AnalyticsPanel panel={panel} />}
36
+ <h3>Alerts</h3>
37
+ <AlertFeed
38
+ alerts={alerts}
39
+ onAcknowledge={(a) =>
40
+ setAlerts((current) => current.filter((x) => x.id !== a.id))
41
+ }
42
+ />
43
+ </>
44
+ );
45
+ };
46
+
47
+ const App: React.FC = () => {
48
+ const apiKey = (import.meta.env?.VITE_CONVERSOKIT_API_KEY as string) || undefined;
49
+ return (
50
+ <BridgeProvider baseUrl="http://localhost:3000" apiKey={apiKey}>
51
+ <ThemeProvider
52
+ theme={enterpriseTheme}
53
+ style={{ minHeight: '100vh', padding: 'var(--ck-spacing-4)' }}
54
+ >
55
+ <h1>Welcome to <% projectName %></h1>
56
+ <CTABanner
57
+ title="Internal dashboard"
58
+ description="Set CONVERSOKIT_API_KEYS in apps/mcp-server/.env and pass it via VITE_CONVERSOKIT_API_KEY."
59
+ />
60
+ <Dashboard />
61
+ </ThemeProvider>
62
+ </BridgeProvider>
63
+ );
64
+ };
65
+
66
+ export default App;
@@ -0,0 +1,10 @@
1
+ node_modules
2
+ **/node_modules
3
+ **/dist
4
+ .git
5
+ .env
6
+ .env.local
7
+ .turbo
8
+ **/.turbo
9
+ *.log
10
+ .DS_Store
@@ -0,0 +1,27 @@
1
+ # syntax=docker/dockerfile:1
2
+
3
+ FROM node:20-alpine AS base
4
+ RUN corepack enable && corepack prepare pnpm@9 --activate
5
+ WORKDIR /app
6
+
7
+ FROM base AS deps
8
+ COPY pnpm-lock.yaml package.json pnpm-workspace.yaml turbo.json ./
9
+ COPY apps/mcp-server/package.json ./apps/mcp-server/
10
+ COPY packages ./packages
11
+ RUN pnpm install --frozen-lockfile --prod=false
12
+
13
+ FROM deps AS build
14
+ COPY . .
15
+ RUN pnpm -w build
16
+
17
+ FROM base AS runtime
18
+ ENV NODE_ENV=production
19
+ ENV PORT=3000
20
+ COPY --from=build /app/node_modules ./node_modules
21
+ COPY --from=build /app/packages ./packages
22
+ COPY --from=build /app/apps/mcp-server/dist ./apps/mcp-server/dist
23
+ COPY --from=build /app/apps/mcp-server/package.json ./apps/mcp-server/
24
+ EXPOSE 3000
25
+ HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
26
+ CMD wget -q -O - http://localhost:3000/health || exit 1
27
+ CMD ["node", "apps/mcp-server/dist/index.js"]
@@ -0,0 +1,31 @@
1
+ services:
2
+ mcp-server:
3
+ build: .
4
+ image: <% projectName %>:latest
5
+ ports:
6
+ - "3000:3000"
7
+ env_file:
8
+ - .env
9
+ environment:
10
+ NODE_ENV: production
11
+ PORT: 3000
12
+ restart: unless-stopped
13
+ healthcheck:
14
+ test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:3000/health"]
15
+ interval: 30s
16
+ timeout: 5s
17
+ retries: 3
18
+ # Uncomment to add a Postgres for users not on Supabase.
19
+ # postgres:
20
+ # image: postgres:16-alpine
21
+ # environment:
22
+ # POSTGRES_USER: conversokit
23
+ # POSTGRES_PASSWORD: conversokit
24
+ # POSTGRES_DB: conversokit
25
+ # ports:
26
+ # - "5432:5432"
27
+ # volumes:
28
+ # - postgres_data:/var/lib/postgresql/data
29
+
30
+ # volumes:
31
+ # postgres_data:
@@ -0,0 +1 @@
1
+ web: pnpm --filter mcp-server start
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "https://railway.app/railway.schema.json",
3
+ "build": {
4
+ "builder": "NIXPACKS",
5
+ "buildCommand": "pnpm install --frozen-lockfile && pnpm -w build"
6
+ },
7
+ "deploy": {
8
+ "startCommand": "pnpm --filter mcp-server start",
9
+ "healthcheckPath": "/health",
10
+ "healthcheckTimeout": 30,
11
+ "restartPolicyType": "ON_FAILURE",
12
+ "restartPolicyMaxRetries": 3
13
+ }
14
+ }
@@ -0,0 +1,10 @@
1
+ // Vercel serverless adapter for the ConversoKit MCP server.
2
+ // Re-exports the express app from `apps/mcp-server` so all routes (/tools,
3
+ // /auth, /webhooks, /userdata, /admin, /health) are answered by one function.
4
+ //
5
+ // Make sure `apps/mcp-server/src/index.ts` exports the app:
6
+ // export { app };
7
+ // then this file imports it as `app`.
8
+ import app from '../apps/mcp-server/dist/index.js';
9
+
10
+ export default app;
@@ -0,0 +1,19 @@
1
+ {
2
+ "version": 2,
3
+ "buildCommand": "pnpm -w build",
4
+ "installCommand": "pnpm -w install --frozen-lockfile",
5
+ "outputDirectory": "apps/widget-ui/dist",
6
+ "rewrites": [
7
+ { "source": "/tools/:path*", "destination": "/api/mcp" },
8
+ { "source": "/auth/:path*", "destination": "/api/mcp" },
9
+ { "source": "/userdata/:path*", "destination": "/api/mcp" },
10
+ { "source": "/webhooks/:path*", "destination": "/api/mcp" },
11
+ { "source": "/admin/:path*", "destination": "/api/mcp" },
12
+ { "source": "/health", "destination": "/api/mcp" }
13
+ ],
14
+ "functions": {
15
+ "api/mcp.ts": {
16
+ "runtime": "nodejs20.x"
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,4 @@
1
+ import type { Tool } from '@conversokit/shared';
2
+ import { submitLeadTool } from './submitLead.js';
3
+
4
+ export const tools: Tool[] = [submitLeadTool];
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+ import { defineTool, leadSubmissionSchema } from '@conversokit/shared';
3
+
4
+ export const submitLeadTool = defineTool({
5
+ name: 'submit_lead',
6
+ description:
7
+ 'Submit a qualified lead. Replace this stub with a CRM upsert (HubSpot, Salesforce, etc.) when wiring real persistence.',
8
+ inputSchema: leadSubmissionSchema,
9
+ outputSchema: z.object({
10
+ leadId: z.string(),
11
+ provider: z.string()
12
+ }),
13
+ permissions: { requiresAuth: false, requiresConsent: true },
14
+ async handler() {
15
+ return {
16
+ leadId: `lead_${Math.random().toString(36).slice(2)}`,
17
+ provider: 'mock'
18
+ };
19
+ }
20
+ });
@@ -0,0 +1,56 @@
1
+ import React, { useState } from 'react';
2
+ import {
3
+ CTABanner,
4
+ ConsentBanner,
5
+ MultiStepForm
6
+ } from '@conversokit/widgets';
7
+ import { EXAMPLE_LEAD_FORM } from '@conversokit/shared';
8
+ import { ThemeProvider, modernSaasTheme } from '@conversokit/themes';
9
+ import { BridgeProvider, useBridge } from '@conversokit/bridge';
10
+
11
+ const Onboarding: React.FC = () => {
12
+ const bridge = useBridge();
13
+ const [done, setDone] = useState<{ leadId: string; provider: string } | null>(
14
+ null
15
+ );
16
+
17
+ if (done) {
18
+ return (
19
+ <CTABanner
20
+ title="Thanks!"
21
+ description={`Lead ${done.leadId} synced via ${done.provider}.`}
22
+ variant="success"
23
+ />
24
+ );
25
+ }
26
+
27
+ return (
28
+ <MultiStepForm
29
+ form={EXAMPLE_LEAD_FORM}
30
+ onComplete={async (values) => {
31
+ const r = (await bridge.callTool('submit_lead', {
32
+ formId: EXAMPLE_LEAD_FORM.id,
33
+ values,
34
+ submittedAt: new Date().toISOString()
35
+ })) as { leadId: string; provider: string };
36
+ setDone(r);
37
+ }}
38
+ />
39
+ );
40
+ };
41
+
42
+ const App: React.FC = () => (
43
+ <BridgeProvider baseUrl="http://localhost:3000">
44
+ <ThemeProvider
45
+ theme={modernSaasTheme}
46
+ style={{ minHeight: '100vh', padding: 'var(--ck-spacing-4)' }}
47
+ >
48
+ <h1>Welcome to <% projectName %></h1>
49
+ <ConsentBanner scopes={['personalData', 'marketing']}>
50
+ <Onboarding />
51
+ </ConsentBanner>
52
+ </ThemeProvider>
53
+ </BridgeProvider>
54
+ );
55
+
56
+ export default App;
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ EXAMPLE_ITINERARY,
4
+ defineTool,
5
+ itinerarySchema
6
+ } from '@conversokit/shared';
7
+
8
+ export const getItineraryTool = defineTool({
9
+ name: 'get_itinerary',
10
+ description: 'Return a saved travel itinerary by id.',
11
+ inputSchema: z.object({ itineraryId: z.string() }),
12
+ outputSchema: z.object({ itinerary: itinerarySchema }),
13
+ permissions: { requiresAuth: false },
14
+ async handler() {
15
+ return { itinerary: EXAMPLE_ITINERARY };
16
+ }
17
+ });
@@ -0,0 +1,12 @@
1
+ import type { Tool } from '@conversokit/shared';
2
+ import { listDestinationsTool } from './listDestinations.js';
3
+ import { searchHotelsTool } from './searchHotels.js';
4
+ import { searchFlightsTool } from './searchFlights.js';
5
+ import { getItineraryTool } from './getItinerary.js';
6
+
7
+ export const tools: Tool[] = [
8
+ listDestinationsTool,
9
+ searchHotelsTool,
10
+ searchFlightsTool,
11
+ getItineraryTool
12
+ ];
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ EXAMPLE_DESTINATIONS,
4
+ defineTool,
5
+ destinationSchema
6
+ } from '@conversokit/shared';
7
+
8
+ export const listDestinationsTool = defineTool({
9
+ name: 'list_destinations',
10
+ description: 'Return curated destination recommendations.',
11
+ inputSchema: z.object({
12
+ query: z.string().optional(),
13
+ limit: z.number().int().min(1).max(50).optional()
14
+ }),
15
+ outputSchema: z.object({ items: z.array(destinationSchema) }),
16
+ permissions: { requiresAuth: false },
17
+ async handler(input) {
18
+ const lower = input.query?.toLowerCase();
19
+ const matches = lower
20
+ ? EXAMPLE_DESTINATIONS.filter(
21
+ (d) =>
22
+ d.name.toLowerCase().includes(lower) ||
23
+ (d.country?.toLowerCase().includes(lower) ?? false)
24
+ )
25
+ : EXAMPLE_DESTINATIONS;
26
+ return { items: matches.slice(0, input.limit ?? 20) };
27
+ }
28
+ });
@@ -0,0 +1,22 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ EXAMPLE_FLIGHT,
4
+ defineTool,
5
+ flightSummarySchema
6
+ } from '@conversokit/shared';
7
+
8
+ export const searchFlightsTool = defineTool({
9
+ name: 'search_flights',
10
+ description: 'Search flights by IATA origin and destination.',
11
+ inputSchema: z.object({
12
+ origin: z.string(),
13
+ destination: z.string(),
14
+ departDate: z.string().optional(),
15
+ returnDate: z.string().optional()
16
+ }),
17
+ outputSchema: z.object({ items: z.array(flightSummarySchema) }),
18
+ permissions: { requiresAuth: false },
19
+ async handler() {
20
+ return { items: [EXAMPLE_FLIGHT] };
21
+ }
22
+ });
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ EXAMPLE_HOTELS,
4
+ defineTool,
5
+ hotelSchema
6
+ } from '@conversokit/shared';
7
+
8
+ export const searchHotelsTool = defineTool({
9
+ name: 'search_hotels',
10
+ description: 'Search hotels by free-text query (matches city or hotel name).',
11
+ inputSchema: z.object({
12
+ query: z.string(),
13
+ limit: z.number().int().min(1).max(50).optional()
14
+ }),
15
+ outputSchema: z.object({ items: z.array(hotelSchema) }),
16
+ permissions: { requiresAuth: false },
17
+ async handler(input) {
18
+ const lower = input.query.toLowerCase();
19
+ const matches = lower
20
+ ? EXAMPLE_HOTELS.filter(
21
+ (h) =>
22
+ h.name.toLowerCase().includes(lower) ||
23
+ h.city.toLowerCase().includes(lower) ||
24
+ (h.country?.toLowerCase().includes(lower) ?? false)
25
+ )
26
+ : EXAMPLE_HOTELS;
27
+ return { items: matches.slice(0, input.limit ?? 10) };
28
+ }
29
+ });
@@ -0,0 +1,89 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import {
3
+ CTABanner,
4
+ ConsentBanner,
5
+ DestinationRecommendations,
6
+ FlightSummary,
7
+ HotelCard,
8
+ ItineraryTimeline
9
+ } from '@conversokit/widgets';
10
+ import {
11
+ type Destination,
12
+ type FlightSummary as FlightSummaryData,
13
+ type Hotel,
14
+ type Itinerary
15
+ } from '@conversokit/shared';
16
+ import { ThemeProvider, travelTheme } from '@conversokit/themes';
17
+ import { BridgeProvider, useBridge } from '@conversokit/bridge';
18
+
19
+ const TravelPlanner: React.FC = () => {
20
+ const bridge = useBridge();
21
+ const [destinations, setDestinations] = useState<Destination[]>([]);
22
+ const [hotels, setHotels] = useState<Hotel[]>([]);
23
+ const [flight, setFlight] = useState<FlightSummaryData | null>(null);
24
+ const [itinerary, setItinerary] = useState<Itinerary | null>(null);
25
+ const [picked, setPicked] = useState<Destination | null>(null);
26
+
27
+ useEffect(() => {
28
+ bridge
29
+ .callTool('list_destinations', {})
30
+ .then((r) => setDestinations((r as { items: Destination[] }).items))
31
+ .catch(console.error);
32
+ }, [bridge]);
33
+
34
+ const pick = async (dest: Destination) => {
35
+ setPicked(dest);
36
+ const [h, f] = await Promise.all([
37
+ bridge.callTool('search_hotels', { query: dest.name }) as Promise<{
38
+ items: Hotel[];
39
+ }>,
40
+ bridge.callTool('search_flights', {
41
+ origin: 'BER',
42
+ destination: dest.name.slice(0, 3).toUpperCase()
43
+ }) as Promise<{ items: FlightSummaryData[] }>
44
+ ]);
45
+ setHotels(h.items);
46
+ setFlight(f.items[0] ?? null);
47
+ };
48
+
49
+ const buildItinerary = async () => {
50
+ const r = (await bridge.callTool('get_itinerary', {
51
+ itineraryId: 'demo'
52
+ })) as { itinerary: Itinerary };
53
+ setItinerary(r.itinerary);
54
+ };
55
+
56
+ return (
57
+ <>
58
+ <DestinationRecommendations destinations={destinations} onSelect={pick} />
59
+ {picked && (
60
+ <CTABanner
61
+ title={`Plan a trip to ${picked.name}`}
62
+ primaryLabel="Build itinerary"
63
+ onPrimary={buildItinerary}
64
+ />
65
+ )}
66
+ {hotels.map((h) => (
67
+ <HotelCard key={h.id} hotel={h} />
68
+ ))}
69
+ {flight && <FlightSummary flight={flight} />}
70
+ {itinerary && <ItineraryTimeline itinerary={itinerary} />}
71
+ </>
72
+ );
73
+ };
74
+
75
+ const App: React.FC = () => (
76
+ <BridgeProvider baseUrl="http://localhost:3000">
77
+ <ThemeProvider
78
+ theme={travelTheme}
79
+ style={{ minHeight: '100vh', padding: 'var(--ck-spacing-4)' }}
80
+ >
81
+ <h1>Welcome to <% projectName %></h1>
82
+ <ConsentBanner scopes={['analytics']}>
83
+ <TravelPlanner />
84
+ </ConsentBanner>
85
+ </ThemeProvider>
86
+ </BridgeProvider>
87
+ );
88
+
89
+ export default App;