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.
- package/LICENSE +201 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +22 -0
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +263 -0
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.js +75 -0
- package/dist/commands/deploy.d.ts +2 -0
- package/dist/commands/deploy.js +86 -0
- package/dist/utils/copy.d.ts +8 -0
- package/dist/utils/copy.js +59 -0
- package/package.json +54 -0
- package/templates/base/.env.example +10 -0
- package/templates/base/README.md +36 -0
- package/templates/base/apps/mcp-server/package.json +27 -0
- package/templates/base/apps/mcp-server/src/index.ts +61 -0
- package/templates/base/apps/mcp-server/src/tools/index.ts +5 -0
- package/templates/base/apps/mcp-server/tsconfig.json +15 -0
- package/templates/base/apps/widget-ui/index.html +12 -0
- package/templates/base/apps/widget-ui/package.json +26 -0
- package/templates/base/apps/widget-ui/src/App.tsx +28 -0
- package/templates/base/apps/widget-ui/src/main.tsx +9 -0
- package/templates/base/apps/widget-ui/tsconfig.json +15 -0
- package/templates/base/apps/widget-ui/vite.config.ts +7 -0
- package/templates/base/package.json +19 -0
- package/templates/base/pnpm-workspace.yaml +2 -0
- package/templates/booking/apps/mcp-server/src/tools/cancelReservation.ts +28 -0
- package/templates/booking/apps/mcp-server/src/tools/createReservation.ts +38 -0
- package/templates/booking/apps/mcp-server/src/tools/getAvailability.ts +21 -0
- package/templates/booking/apps/mcp-server/src/tools/index.ts +10 -0
- package/templates/booking/apps/widget-ui/src/App.tsx +79 -0
- package/templates/commerce/apps/mcp-server/src/tools/index.ts +5 -0
- package/templates/commerce/apps/mcp-server/src/tools/searchProducts.ts +29 -0
- package/templates/commerce/apps/widget-ui/src/App.tsx +75 -0
- package/templates/dashboard/apps/mcp-server/src/tools/getAlerts.ts +18 -0
- package/templates/dashboard/apps/mcp-server/src/tools/getAnalyticsPanel.ts +17 -0
- package/templates/dashboard/apps/mcp-server/src/tools/getKpis.ts +13 -0
- package/templates/dashboard/apps/mcp-server/src/tools/getTrendSeries.ts +20 -0
- package/templates/dashboard/apps/mcp-server/src/tools/index.ts +14 -0
- package/templates/dashboard/apps/widget-ui/src/App.tsx +66 -0
- package/templates/deploy/docker/.dockerignore +10 -0
- package/templates/deploy/docker/Dockerfile +27 -0
- package/templates/deploy/docker/docker-compose.yml +31 -0
- package/templates/deploy/railway/Procfile +1 -0
- package/templates/deploy/railway/railway.json +14 -0
- package/templates/deploy/vercel/api/mcp.ts +10 -0
- package/templates/deploy/vercel/vercel.json +19 -0
- package/templates/saas-onboarding/apps/mcp-server/src/tools/index.ts +4 -0
- package/templates/saas-onboarding/apps/mcp-server/src/tools/submitLead.ts +20 -0
- package/templates/saas-onboarding/apps/widget-ui/src/App.tsx +56 -0
- package/templates/travel/apps/mcp-server/src/tools/getItinerary.ts +17 -0
- package/templates/travel/apps/mcp-server/src/tools/index.ts +12 -0
- package/templates/travel/apps/mcp-server/src/tools/listDestinations.ts +28 -0
- package/templates/travel/apps/mcp-server/src/tools/searchFlights.ts +22 -0
- package/templates/travel/apps/mcp-server/src/tools/searchHotels.ts +29 -0
- 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,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,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,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,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,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,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
|
+
});
|