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,59 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ const TEXT_EXTS = new Set([
4
+ '.ts',
5
+ '.tsx',
6
+ '.js',
7
+ '.jsx',
8
+ '.json',
9
+ '.md',
10
+ '.html',
11
+ '.css',
12
+ '.yml',
13
+ '.yaml',
14
+ '.toml',
15
+ '.env',
16
+ '.gitignore',
17
+ ''
18
+ ]);
19
+ function applyReplacements(content, repl) {
20
+ let out = content;
21
+ for (const [key, value] of Object.entries(repl)) {
22
+ out = out.split(`<% ${key} %>`).join(value);
23
+ }
24
+ return out;
25
+ }
26
+ export async function copyDir(src, dest, options = {}) {
27
+ const entries = await fs.readdir(src, { withFileTypes: true });
28
+ await fs.mkdir(dest, { recursive: true });
29
+ for (const entry of entries) {
30
+ const rel = entry.name;
31
+ if (options.ignore?.includes(rel))
32
+ continue;
33
+ const srcPath = join(src, entry.name);
34
+ const destPath = join(dest, entry.name);
35
+ if (entry.isDirectory()) {
36
+ await copyDir(srcPath, destPath, {
37
+ ...options,
38
+ ignore: options.ignore?.map((p) => p.startsWith(`${rel}/`) ? p.slice(rel.length + 1) : p)
39
+ });
40
+ }
41
+ else if (entry.isFile()) {
42
+ await copyFile(srcPath, destPath, options.replacements);
43
+ }
44
+ }
45
+ }
46
+ async function copyFile(src, dest, replacements) {
47
+ const ext = src.slice(src.lastIndexOf('.'));
48
+ const isText = TEXT_EXTS.has(ext);
49
+ if (isText && replacements) {
50
+ const buf = await fs.readFile(src, 'utf8');
51
+ await fs.writeFile(dest, applyReplacements(buf, replacements));
52
+ }
53
+ else {
54
+ await fs.copyFile(src, dest);
55
+ }
56
+ }
57
+ export function relativePath(from, to) {
58
+ return relative(from, to) || '.';
59
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "conversokit",
3
+ "version": "0.1.0",
4
+ "description": "CLI for ConversoKit — scaffold ChatGPT Apps in <30 minutes. Includes `create`, `add`, and `deploy` commands.",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Xyborg/ConversoKit.git",
9
+ "directory": "packages/cli"
10
+ },
11
+ "homepage": "https://github.com/Xyborg/ConversoKit#readme",
12
+ "bugs": "https://github.com/Xyborg/ConversoKit/issues",
13
+ "keywords": [
14
+ "chatgpt",
15
+ "openai",
16
+ "apps-sdk",
17
+ "mcp",
18
+ "boilerplate",
19
+ "react",
20
+ "scaffold",
21
+ "cli",
22
+ "conversokit"
23
+ ],
24
+ "type": "module",
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "main": "dist/cli.js",
29
+ "bin": {
30
+ "conversokit": "./dist/cli.js"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "templates"
35
+ ],
36
+ "dependencies": {
37
+ "chalk": "^5.3.0",
38
+ "commander": "^12.0.0",
39
+ "prompts": "^2.4.2"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^20.10.0",
43
+ "@types/prompts": "^2.4.9",
44
+ "typescript": "^5.2.0",
45
+ "vitest": "^1.6.0"
46
+ },
47
+ "scripts": {
48
+ "build": "tsc --project tsconfig.json && node ./scripts/postbuild.mjs",
49
+ "dev": "tsc --project tsconfig.json --watch",
50
+ "typecheck": "tsc --noEmit",
51
+ "lint": "eslint src",
52
+ "test": "vitest run --passWithNoTests"
53
+ }
54
+ }
@@ -0,0 +1,10 @@
1
+ PORT=3000
2
+ NODE_ENV=development
3
+
4
+ # CONVERSOKIT_API_KEYS=
5
+ # STRIPE_SECRET_KEY=
6
+ # STRIPE_WEBHOOK_SECRET=
7
+ # HUBSPOT_API_KEY=
8
+ # GOOGLE_CLIENT_ID=
9
+ # GOOGLE_CLIENT_SECRET=
10
+ # COOKIE_SECRET=
@@ -0,0 +1,36 @@
1
+ # <% projectName %>
2
+
3
+ A ChatGPT App built with [ConversoKit](https://github.com/Xyborg/ConversoKit).
4
+ Template: **<% template %>**
5
+
6
+ ## Quick start
7
+
8
+ ```bash
9
+ pnpm install
10
+ pnpm dev
11
+ ```
12
+
13
+ - MCP server → http://localhost:3000
14
+ - Widget UI → http://localhost:5173
15
+
16
+ ## Structure
17
+
18
+ ```
19
+ <% projectName %>/
20
+ ├── apps/
21
+ │ ├── mcp-server/ # Express + Zod MCP tool server
22
+ │ └── widget-ui/ # Vite + React widget host
23
+ ├── .env.example
24
+ └── package.json
25
+ ```
26
+
27
+ ## Customise
28
+
29
+ - Add tools under `apps/mcp-server/src/tools/`, then register them in
30
+ `tools/index.ts`.
31
+ - Add or fork widgets in `apps/widget-ui/src/widgets/`.
32
+ - See ConversoKit docs for widget authoring, integrations, and compliance.
33
+
34
+ ## Environment
35
+
36
+ Copy `.env.example` to `.env` and fill in the keys you need.
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "<% projectName %>-mcp-server",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "scripts": {
8
+ "dev": "ts-node-dev --respawn --transpile-only src/index.ts",
9
+ "build": "tsc --project tsconfig.json",
10
+ "start": "node dist/index.js"
11
+ },
12
+ "dependencies": {
13
+ "@conversokit/auth": "^0.1.0",
14
+ "@conversokit/integrations": "^0.1.0",
15
+ "@conversokit/shared": "^0.1.0",
16
+ "express": "^4.18.2",
17
+ "cors": "^2.8.5",
18
+ "zod": "^3.22.2"
19
+ },
20
+ "devDependencies": {
21
+ "@types/cors": "^2.8.17",
22
+ "@types/express": "^4.17.21",
23
+ "@types/node": "^20.10.0",
24
+ "ts-node-dev": "^2.0.0",
25
+ "typescript": "^5.2.0"
26
+ }
27
+ }
@@ -0,0 +1,61 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import { randomUUID } from 'node:crypto';
4
+ import {
5
+ apiKeyProvider,
6
+ createAuthMiddleware,
7
+ type AuthProvider
8
+ } from '@conversokit/auth';
9
+ import type { Tool, ToolContext } from '@conversokit/shared';
10
+
11
+ import { tools } from './tools/index.js';
12
+
13
+ const app = express();
14
+ app.use(cors());
15
+ app.use(express.json());
16
+
17
+ const apiKeys = (process.env.CONVERSOKIT_API_KEYS ?? '')
18
+ .split(',')
19
+ .map((k) => k.trim())
20
+ .filter(Boolean);
21
+
22
+ const providers: AuthProvider[] = [apiKeyProvider({ apiKeys })];
23
+ app.use(
24
+ createAuthMiddleware({ providers, optional: apiKeys.length === 0 })
25
+ );
26
+
27
+ function buildContext(req: express.Request): ToolContext {
28
+ return {
29
+ sessionId: req.header('x-conversokit-session') ?? randomUUID(),
30
+ logger: console
31
+ };
32
+ }
33
+
34
+ app.get('/tools', (_req, res) => {
35
+ res.json({
36
+ tools: tools.map((t: Tool) => ({
37
+ name: t.name,
38
+ description: t.description,
39
+ permissions: t.permissions
40
+ }))
41
+ });
42
+ });
43
+
44
+ app.post('/tools/:name', async (req, res) => {
45
+ const tool = tools.find((t: Tool) => t.name === req.params.name);
46
+ if (!tool) return res.status(404).json({ error: 'Tool not found' });
47
+ try {
48
+ const input = tool.inputSchema.parse(req.body);
49
+ const output = await tool.handler(input, buildContext(req));
50
+ res.json(output);
51
+ } catch (err) {
52
+ res.status(400).json({
53
+ error: err instanceof Error ? err.message : 'Tool execution failed'
54
+ });
55
+ }
56
+ });
57
+
58
+ const PORT = process.env.PORT || 3000;
59
+ app.listen(PORT, () => {
60
+ console.log(`<% projectName %> MCP server listening on :${PORT}`);
61
+ });
@@ -0,0 +1,5 @@
1
+ import type { Tool } from '@conversokit/shared';
2
+
3
+ // Add your tools here. Each tool conforms to Tool<I,O> from @conversokit/shared.
4
+ // See https://github.com/Xyborg/ConversoKit/blob/main/docs/mcp-basics.md
5
+ export const tools: Tool[] = [];
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ES2020",
5
+ "moduleResolution": "node",
6
+ "rootDir": "src",
7
+ "outDir": "dist",
8
+ "esModuleInterop": true,
9
+ "strict": true,
10
+ "skipLibCheck": true,
11
+ "resolveJsonModule": true
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules", "dist"]
15
+ }
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title><% projectName %></title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "<% projectName %>-widget-ui",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@conversokit/bridge": "^0.1.0",
13
+ "@conversokit/shared": "^0.1.0",
14
+ "@conversokit/themes": "^0.1.0",
15
+ "@conversokit/widgets": "^0.1.0",
16
+ "react": "^18.2.0",
17
+ "react-dom": "^18.2.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/react": "^18.0.28",
21
+ "@types/react-dom": "^18.0.11",
22
+ "@vitejs/plugin-react": "^4.0.0",
23
+ "typescript": "^5.2.0",
24
+ "vite": "^5.0.0"
25
+ }
26
+ }
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { ThemeProvider, lightTheme } from '@conversokit/themes';
3
+ import { BridgeProvider } from '@conversokit/bridge';
4
+ import { CTABanner } from '@conversokit/widgets';
5
+
6
+ const App: React.FC = () => {
7
+ return (
8
+ <BridgeProvider baseUrl="http://localhost:3000">
9
+ <ThemeProvider
10
+ theme={lightTheme}
11
+ style={{ minHeight: '100vh', padding: 'var(--ck-spacing-4)' }}
12
+ >
13
+ <h1>Welcome to <% projectName %></h1>
14
+ <p style={{ color: 'var(--ck-muted)' }}>
15
+ Built with ConversoKit. Edit <code>apps/widget-ui/src/App.tsx</code> to
16
+ start building.
17
+ </p>
18
+ <CTABanner
19
+ title="Next steps"
20
+ description="Add MCP tools in apps/mcp-server/src/tools and consume them via useBridge()."
21
+ variant="info"
22
+ />
23
+ </ThemeProvider>
24
+ </BridgeProvider>
25
+ );
26
+ };
27
+
28
+ export default App;
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App.js';
4
+
5
+ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
6
+ <React.StrictMode>
7
+ <App />
8
+ </React.StrictMode>
9
+ );
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ES2020",
5
+ "jsx": "react-jsx",
6
+ "moduleResolution": "node",
7
+ "esModuleInterop": true,
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "types": ["vite/client"]
12
+ },
13
+ "include": ["src"],
14
+ "exclude": ["node_modules", "dist"]
15
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: { port: 5173 }
7
+ });
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "<% projectName %>",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "ChatGPT App scaffolded with ConversoKit (template: <% template %>)",
6
+ "scripts": {
7
+ "dev": "concurrently -k -n mcp,ui -c blue,magenta \"pnpm dev:mcp\" \"pnpm dev:ui\"",
8
+ "dev:mcp": "cd apps/mcp-server && pnpm dev",
9
+ "dev:ui": "cd apps/widget-ui && pnpm dev",
10
+ "build": "pnpm -r build"
11
+ },
12
+ "devDependencies": {
13
+ "concurrently": "^8.2.2"
14
+ },
15
+ "engines": {
16
+ "node": ">=18.0.0",
17
+ "pnpm": ">=8.0.0"
18
+ }
19
+ }
@@ -0,0 +1,2 @@
1
+ packages:
2
+ - "apps/*"
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ EXAMPLE_AVAILABILITY,
4
+ defineTool,
5
+ reservationSchema
6
+ } from '@conversokit/shared';
7
+
8
+ export const cancelReservationTool = defineTool({
9
+ name: 'cancel_reservation',
10
+ description: 'Cancel an existing reservation by id.',
11
+ inputSchema: z.object({ reservationId: z.string() }),
12
+ outputSchema: z.object({ reservation: reservationSchema }),
13
+ permissions: { requiresAuth: false },
14
+ async handler(input) {
15
+ const slot = EXAMPLE_AVAILABILITY.slots[0];
16
+ return {
17
+ reservation: {
18
+ id: input.reservationId,
19
+ resourceId: EXAMPLE_AVAILABILITY.resourceId,
20
+ resourceName: EXAMPLE_AVAILABILITY.resourceName,
21
+ slotId: slot.id,
22
+ startsAt: slot.startsAt,
23
+ endsAt: slot.endsAt,
24
+ status: 'cancelled' as const
25
+ }
26
+ };
27
+ }
28
+ });
@@ -0,0 +1,38 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ EXAMPLE_AVAILABILITY,
4
+ defineTool,
5
+ reservationSchema
6
+ } from '@conversokit/shared';
7
+
8
+ export const createReservationTool = defineTool({
9
+ name: 'create_reservation',
10
+ description: 'Reserve a time slot. Returns a synthetic Reservation in dev.',
11
+ inputSchema: z.object({
12
+ resourceId: z.string(),
13
+ slotId: z.string(),
14
+ customer: z.object({
15
+ name: z.string(),
16
+ email: z.string().email().optional()
17
+ })
18
+ }),
19
+ outputSchema: z.object({ reservation: reservationSchema }),
20
+ permissions: { requiresAuth: false, requiresConsent: true },
21
+ async handler(input) {
22
+ const slot =
23
+ EXAMPLE_AVAILABILITY.slots.find((s) => s.id === input.slotId) ??
24
+ EXAMPLE_AVAILABILITY.slots[0];
25
+ return {
26
+ reservation: {
27
+ id: `res_${Math.random().toString(36).slice(2)}`,
28
+ resourceId: input.resourceId,
29
+ resourceName: EXAMPLE_AVAILABILITY.resourceName,
30
+ slotId: slot.id,
31
+ startsAt: slot.startsAt,
32
+ endsAt: slot.endsAt,
33
+ status: 'confirmed' as const,
34
+ customer: input.customer
35
+ }
36
+ };
37
+ }
38
+ });
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ EXAMPLE_AVAILABILITY,
4
+ availabilitySchema,
5
+ defineTool
6
+ } from '@conversokit/shared';
7
+
8
+ export const getAvailabilityTool = defineTool({
9
+ name: 'get_availability',
10
+ description: 'Return available time slots for a resource within a date range.',
11
+ inputSchema: z.object({
12
+ resourceId: z.string(),
13
+ from: z.string(),
14
+ to: z.string()
15
+ }),
16
+ outputSchema: z.object({ days: z.array(availabilitySchema) }),
17
+ permissions: { requiresAuth: false },
18
+ async handler() {
19
+ return { days: [EXAMPLE_AVAILABILITY] };
20
+ }
21
+ });
@@ -0,0 +1,10 @@
1
+ import type { Tool } from '@conversokit/shared';
2
+ import { getAvailabilityTool } from './getAvailability.js';
3
+ import { createReservationTool } from './createReservation.js';
4
+ import { cancelReservationTool } from './cancelReservation.js';
5
+
6
+ export const tools: Tool[] = [
7
+ getAvailabilityTool,
8
+ createReservationTool,
9
+ cancelReservationTool
10
+ ];
@@ -0,0 +1,79 @@
1
+ import React, { useState } from 'react';
2
+ import {
3
+ AvailabilityCalendar,
4
+ BookingCard,
5
+ CTABanner,
6
+ ConsentBanner,
7
+ TimeSlotSelector
8
+ } from '@conversokit/widgets';
9
+ import {
10
+ EXAMPLE_AVAILABILITY,
11
+ type Reservation,
12
+ type TimeSlot
13
+ } from '@conversokit/shared';
14
+ import { ThemeProvider, lightTheme } from '@conversokit/themes';
15
+ import { BridgeProvider, useBridge } from '@conversokit/bridge';
16
+
17
+ const Booking: React.FC = () => {
18
+ const bridge = useBridge();
19
+ const [date, setDate] = useState<string | undefined>();
20
+ const [slot, setSlot] = useState<TimeSlot | undefined>();
21
+ const [reservation, setReservation] = useState<Reservation | null>(null);
22
+
23
+ const confirm = async () => {
24
+ if (!slot) return;
25
+ const r = (await bridge.callTool('create_reservation', {
26
+ resourceId: EXAMPLE_AVAILABILITY.resourceId,
27
+ slotId: slot.id,
28
+ customer: { name: 'Demo User' }
29
+ })) as { reservation: Reservation };
30
+ setReservation(r.reservation);
31
+ };
32
+
33
+ if (reservation) {
34
+ return (
35
+ <BookingCard
36
+ reservation={reservation}
37
+ onCancel={async () => {
38
+ const r = (await bridge.callTool('cancel_reservation', {
39
+ reservationId: reservation.id
40
+ })) as { reservation: Reservation };
41
+ setReservation(r.reservation);
42
+ }}
43
+ />
44
+ );
45
+ }
46
+
47
+ return (
48
+ <>
49
+ <AvailabilityCalendar
50
+ availableDates={[EXAMPLE_AVAILABILITY.date]}
51
+ selectedDate={date}
52
+ onSelect={setDate}
53
+ />
54
+ {date && (
55
+ <TimeSlotSelector
56
+ slots={EXAMPLE_AVAILABILITY.slots}
57
+ selectedSlotId={slot?.id}
58
+ onSelect={setSlot}
59
+ />
60
+ )}
61
+ {slot && (
62
+ <CTABanner title="Confirm reservation" primaryLabel="Confirm" onPrimary={confirm} />
63
+ )}
64
+ </>
65
+ );
66
+ };
67
+
68
+ const App: React.FC = () => (
69
+ <BridgeProvider baseUrl="http://localhost:3000">
70
+ <ThemeProvider theme={lightTheme} style={{ minHeight: '100vh', padding: 'var(--ck-spacing-4)' }}>
71
+ <h1>Welcome to <% projectName %></h1>
72
+ <ConsentBanner scopes={['personalData']}>
73
+ <Booking />
74
+ </ConsentBanner>
75
+ </ThemeProvider>
76
+ </BridgeProvider>
77
+ );
78
+
79
+ export default App;
@@ -0,0 +1,5 @@
1
+ import type { Tool } from '@conversokit/shared';
2
+ import { searchProductsTool } from './searchProducts.js';
3
+
4
+ // Generated commerce template. Wire your additional tools here.
5
+ export const tools: Tool[] = [searchProductsTool];
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ productSchema,
4
+ EXAMPLE_PRODUCTS,
5
+ defineTool
6
+ } from '@conversokit/shared';
7
+
8
+ export const searchProductsTool = defineTool({
9
+ name: 'search_products',
10
+ description: 'Free-text search over the product catalog.',
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(productSchema) }),
16
+ permissions: { requiresAuth: false },
17
+ async handler(input) {
18
+ const { query, limit = 10 } = input;
19
+ const lower = query.toLowerCase();
20
+ const matches = lower
21
+ ? EXAMPLE_PRODUCTS.filter(
22
+ (p) =>
23
+ p.title.toLowerCase().includes(lower) ||
24
+ (p.subtitle && p.subtitle.toLowerCase().includes(lower))
25
+ )
26
+ : EXAMPLE_PRODUCTS;
27
+ return { items: matches.slice(0, limit) };
28
+ }
29
+ });