pizzaz-mcp 1.0.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/src/types.ts ADDED
@@ -0,0 +1,103 @@
1
+ export type OpenAiGlobals<
2
+ ToolInput = UnknownObject,
3
+ ToolOutput = UnknownObject,
4
+ ToolResponseMetadata = UnknownObject,
5
+ WidgetState = UnknownObject
6
+ > = {
7
+ // visuals
8
+ theme: Theme;
9
+
10
+ userAgent: UserAgent;
11
+ locale: string;
12
+
13
+ // layout
14
+ maxHeight: number;
15
+ displayMode: DisplayMode;
16
+ safeArea: SafeArea;
17
+
18
+ // state
19
+ toolInput: ToolInput;
20
+ toolOutput: ToolOutput | null;
21
+ toolResponseMetadata: ToolResponseMetadata | null;
22
+ widgetState: WidgetState | null;
23
+ setWidgetState: (state: WidgetState) => Promise<void>;
24
+ };
25
+
26
+ // currently copied from types.ts in chatgpt/web-sandbox.
27
+ // Will eventually use a public package.
28
+ type API = {
29
+ callTool: CallTool;
30
+ sendFollowUpMessage: (args: { prompt: string }) => Promise<void>;
31
+ openExternal(payload: { href: string }): void;
32
+
33
+ // Layout controls
34
+ requestDisplayMode: RequestDisplayMode;
35
+ requestModal: (args: { title?: string; params?: UnknownObject }) => Promise<unknown>;
36
+ requestClose: () => Promise<void>;
37
+ };
38
+
39
+ export type UnknownObject = Record<string, unknown>;
40
+
41
+ export type Theme = "light" | "dark";
42
+
43
+ export type SafeAreaInsets = {
44
+ top: number;
45
+ bottom: number;
46
+ left: number;
47
+ right: number;
48
+ };
49
+
50
+ export type SafeArea = {
51
+ insets: SafeAreaInsets;
52
+ };
53
+
54
+ export type DeviceType = "mobile" | "tablet" | "desktop" | "unknown";
55
+
56
+ export type UserAgent = {
57
+ device: { type: DeviceType };
58
+ capabilities: {
59
+ hover: boolean;
60
+ touch: boolean;
61
+ };
62
+ };
63
+
64
+ /** Display mode */
65
+ export type DisplayMode = "pip" | "inline" | "fullscreen";
66
+ export type RequestDisplayMode = (args: { mode: DisplayMode }) => Promise<{
67
+ /**
68
+ * The granted display mode. The host may reject the request.
69
+ * For mobile, PiP is always coerced to fullscreen.
70
+ */
71
+ mode: DisplayMode;
72
+ }>;
73
+
74
+ export type CallToolResponse = {
75
+ result: string;
76
+ };
77
+
78
+ /** Calling APIs */
79
+ export type CallTool = (
80
+ name: string,
81
+ args: Record<string, unknown>
82
+ ) => Promise<CallToolResponse>;
83
+
84
+ /** Extra events */
85
+ export const SET_GLOBALS_EVENT_TYPE = "openai:set_globals";
86
+ export class SetGlobalsEvent extends CustomEvent<{
87
+ globals: Partial<OpenAiGlobals>;
88
+ }> {
89
+ readonly type = SET_GLOBALS_EVENT_TYPE;
90
+ }
91
+
92
+ /**
93
+ * Global oai object injected by the web sandbox for communicating with chatgpt host page.
94
+ */
95
+ declare global {
96
+ interface Window {
97
+ openai: API & OpenAiGlobals;
98
+ }
99
+
100
+ interface WindowEventMap {
101
+ [SET_GLOBALS_EVENT_TYPE]: SetGlobalsEvent;
102
+ }
103
+ }
@@ -0,0 +1,6 @@
1
+ import { useOpenAiGlobal } from "./use-openai-global";
2
+ import { type DisplayMode } from "./types";
3
+
4
+ export const useDisplayMode = (): DisplayMode | null => {
5
+ return useOpenAiGlobal("displayMode");
6
+ };
@@ -0,0 +1,5 @@
1
+ import { useOpenAiGlobal } from "./use-openai-global";
2
+
3
+ export const useMaxHeight = (): number | null => {
4
+ return useOpenAiGlobal("maxHeight");
5
+ };
@@ -0,0 +1,37 @@
1
+ import { useSyncExternalStore } from "react";
2
+ import {
3
+ SET_GLOBALS_EVENT_TYPE,
4
+ SetGlobalsEvent,
5
+ type OpenAiGlobals,
6
+ } from "./types";
7
+
8
+ export function useOpenAiGlobal<K extends keyof OpenAiGlobals>(
9
+ key: K
10
+ ): OpenAiGlobals[K] | null {
11
+ return useSyncExternalStore(
12
+ (onChange) => {
13
+ if (typeof window === "undefined") {
14
+ return () => {};
15
+ }
16
+
17
+ const handleSetGlobal = (event: SetGlobalsEvent) => {
18
+ const value = event.detail.globals[key];
19
+ if (value === undefined) {
20
+ return;
21
+ }
22
+
23
+ onChange();
24
+ };
25
+
26
+ window.addEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal, {
27
+ passive: true,
28
+ });
29
+
30
+ return () => {
31
+ window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal);
32
+ };
33
+ },
34
+ () => window.openai?.[key] ?? null,
35
+ () => window.openai?.[key] ?? null
36
+ );
37
+ }
@@ -0,0 +1,14 @@
1
+ import { useOpenAiGlobal } from "./use-openai-global";
2
+
3
+ export function useWidgetProps<T extends Record<string, unknown>>(
4
+ defaultState?: T | (() => T)
5
+ ): T {
6
+ const props = useOpenAiGlobal("toolOutput") as T;
7
+
8
+ const fallback =
9
+ typeof defaultState === "function"
10
+ ? (defaultState as () => T | null)()
11
+ : defaultState ?? null;
12
+
13
+ return props ?? fallback;
14
+ }
@@ -0,0 +1,46 @@
1
+ import { useCallback, useEffect, useState, type SetStateAction } from "react";
2
+ import { useOpenAiGlobal } from "./use-openai-global";
3
+ import type { UnknownObject } from "./types";
4
+
5
+ export function useWidgetState<T extends UnknownObject>(
6
+ defaultState: T | (() => T)
7
+ ): readonly [T, (state: SetStateAction<T>) => void];
8
+ export function useWidgetState<T extends UnknownObject>(
9
+ defaultState?: T | (() => T | null) | null
10
+ ): readonly [T | null, (state: SetStateAction<T | null>) => void];
11
+ export function useWidgetState<T extends UnknownObject>(
12
+ defaultState?: T | (() => T | null) | null
13
+ ): readonly [T | null, (state: SetStateAction<T | null>) => void] {
14
+ const widgetStateFromWindow = useOpenAiGlobal("widgetState") as T;
15
+
16
+ const [widgetState, _setWidgetState] = useState<T | null>(() => {
17
+ if (widgetStateFromWindow != null) {
18
+ return widgetStateFromWindow;
19
+ }
20
+
21
+ return typeof defaultState === "function"
22
+ ? defaultState()
23
+ : defaultState ?? null;
24
+ });
25
+
26
+ useEffect(() => {
27
+ _setWidgetState(widgetStateFromWindow);
28
+ }, [widgetStateFromWindow]);
29
+
30
+ const setWidgetState = useCallback(
31
+ (state: SetStateAction<T | null>) => {
32
+ _setWidgetState((prevState) => {
33
+ const newState = typeof state === "function" ? state(prevState) : state;
34
+
35
+ if (newState != null && typeof window !== "undefined") {
36
+ void window.openai?.setWidgetState?.(newState);
37
+ }
38
+
39
+ return newState;
40
+ });
41
+ },
42
+ []
43
+ );
44
+
45
+ return [widgetState, setWidgetState] as const;
46
+ }
@@ -0,0 +1,7 @@
1
+ export default {
2
+ content: [
3
+ "./src/**/*.{html,js,ts,jsx,tsx}",
4
+ ],
5
+ theme: {},
6
+ plugins: [],
7
+ };
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "skipLibCheck": true,
9
+ "moduleResolution": "bundler",
10
+ "allowImportingTsExtensions": true,
11
+ "verbatimModuleSyntax": false,
12
+ "moduleDetection": "force",
13
+ "noEmit": true,
14
+ "jsx": "react-jsx",
15
+ "jsxImportSource": "react",
16
+ "types": ["vite/client"],
17
+ "baseUrl": ".",
18
+ "strict": true,
19
+ "noUnusedLocals": false,
20
+ "noUnusedParameters": false,
21
+ "noFallthroughCasesInSwitch": true
22
+ },
23
+ "include": ["src", "vite-env.d.ts"]
24
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "resolveJsonModule": true,
9
+ "skipLibCheck": true,
10
+ "types": ["node"]
11
+ },
12
+ "include": ["server/**/*.ts", "build-all.mts", "vite.config.mts"]
13
+ }
package/vite-env.d.ts ADDED
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,232 @@
1
+ import { defineConfig, type Plugin } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import fg from "fast-glob";
4
+ import path from "node:path";
5
+ import fs from "node:fs";
6
+ import tailwindcss from "@tailwindcss/vite";
7
+
8
+ function buildInputs() {
9
+ const files = fg.sync("src/**/index.{tsx,jsx}", { dot: false });
10
+ return Object.fromEntries(
11
+ files.map((f) => [path.basename(path.dirname(f)), path.resolve(f)])
12
+ );
13
+ }
14
+
15
+ const toFs = (abs: string) => "/@fs/" + abs.replace(/\\/g, "/");
16
+
17
+ const toServerRoot = (abs: string) => {
18
+ const rel = path.relative(process.cwd(), abs).replace(/\\/g, "/");
19
+ // If it's not really relative (different drive or absolute), fall back to fs URL
20
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return toFs(abs);
21
+ return "./" + rel;
22
+ };
23
+
24
+ function multiEntryDevEndpoints(options: {
25
+ entries: Record<string, string>;
26
+ globalCss?: string[];
27
+ perEntryCssGlob?: string;
28
+ perEntryCssIgnore?: string[];
29
+ }): Plugin {
30
+ const {
31
+ entries,
32
+ globalCss = ["src/index.css"],
33
+ perEntryCssGlob = "**/*.{css,pcss,scss,sass}",
34
+ perEntryCssIgnore = ["**/*.module.*"],
35
+ } = options;
36
+
37
+ const V_PREFIX = "\0multi-entry:"; // Rollup “virtual module” prefix
38
+
39
+ const HIDE_FROM_HOME = new Set(["flashcards", "daw"]);
40
+
41
+ const renderIndexHtml = (names: string[]): string => `<!doctype html>
42
+ <html>
43
+ <head>
44
+ <meta charset="utf-8" />
45
+ <title>ecosystem ui examples</title>
46
+ <style>
47
+ body { font: 15px/1.5 system-ui, sans-serif; margin: 32px; color: #1f2933; }
48
+ h1 { font-size: 20px; margin-bottom: 12px; }
49
+ ul { padding-left: 18px; }
50
+ li { margin-bottom: 6px; }
51
+ a { color: #2563eb; text-decoration: none; }
52
+ a:hover { text-decoration: underline; }
53
+ code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; margin-left: 6px; color: #64748b; }
54
+ </style>
55
+ </head>
56
+ <body>
57
+ <h1>Examples</h1>
58
+ <ul>
59
+ ${names
60
+ .filter((n) => !HIDE_FROM_HOME.has(n))
61
+ .toSorted()
62
+ .map(
63
+ (name) =>
64
+ `<li><a href="/${name}.html">${name}</a><code>/${name}.html</code></li>`
65
+ )
66
+ .join("\n ")}
67
+ </ul>
68
+ </body>
69
+ </html>`;
70
+
71
+ const renderDevHtml = (name: string): string => `<!doctype html>
72
+ <html>
73
+ <head>
74
+ <script type="module" src="/${name}.js"></script>
75
+ <link rel="stylesheet" href="/${name}.css">
76
+ </head>
77
+ <body>
78
+ <div id="${name}-root"></div>
79
+ </body>
80
+ </html>`;
81
+
82
+ return {
83
+ name: "multi-entry-dev-endpoints",
84
+ configureServer(server) {
85
+ const names = Object.keys(entries);
86
+ const list = names
87
+ .map((n) => `/${n}.html, /${n}.js, /${n}.css`)
88
+ .join("\n ");
89
+ server.config.logger.info(`\nDev endpoints:\n ${list}\n`);
90
+
91
+ server.middlewares.use((req, res, next) => {
92
+ try {
93
+ if (req.method !== "GET" || !req.url) return next();
94
+ const url = req.url.split("?")[0];
95
+ if (url === "/" || url === "" || url === "/index.html") {
96
+ const html = renderIndexHtml(names);
97
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
98
+ res.end(html);
99
+ return;
100
+ }
101
+ const bareMatch = url.match(/^\/?([\w-]+)\/?$/);
102
+ if (bareMatch && entries[bareMatch[1]]) {
103
+ const name = bareMatch[1];
104
+ const html = renderDevHtml(name);
105
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
106
+ res.end(html);
107
+ return;
108
+ }
109
+
110
+ if (!url.endsWith(".html")) return next();
111
+
112
+ const m = url.match(/^\/?([\w-]+)\.html$/);
113
+ if (!m) return next();
114
+ const name = m[1];
115
+ if (!entries[name]) return next();
116
+
117
+ const html = renderDevHtml(name);
118
+ res.setHeader("Content-Type", "text/html");
119
+ res.end(html);
120
+ return;
121
+ } catch {
122
+ // fall through
123
+ }
124
+ next();
125
+ });
126
+ },
127
+ resolveId(id: string) {
128
+ // Map request paths to virtual ids
129
+ if (id.startsWith("/")) id = id.slice(1);
130
+ if (id.endsWith(".js")) {
131
+ const name = id.slice(0, -3);
132
+ if (entries[name]) return `${V_PREFIX}entry:${name}`;
133
+ }
134
+ if (id.endsWith(".css")) {
135
+ const name = id.slice(0, -4);
136
+ if (entries[name]) return `${V_PREFIX}style:${name}.css`;
137
+ }
138
+ if (id.startsWith(V_PREFIX)) return id;
139
+ return null;
140
+ },
141
+ load(id: string) {
142
+ if (!id.startsWith(V_PREFIX)) return null;
143
+
144
+ const rest = id.slice(V_PREFIX.length); // "entry:foo" or "style:foo.css"
145
+ const [kind, nameWithExt] = rest.split(":", 2);
146
+ const name = nameWithExt.replace(/\.css$/, "");
147
+ const entry = entries[name];
148
+ if (!entry) return null;
149
+
150
+ const entryDir = path.dirname(entry);
151
+
152
+ // Collect CSS (global first for stable cascade)
153
+ const globals = globalCss
154
+ .map((p) => path.resolve(p))
155
+ .filter((p) => fs.existsSync(p));
156
+ const perEntry = fg.sync(perEntryCssGlob, {
157
+ cwd: entryDir,
158
+ absolute: true,
159
+ dot: false,
160
+ ignore: perEntryCssIgnore,
161
+ });
162
+
163
+ if (kind === "style") {
164
+ const allCss = [...globals, ...perEntry]; // absolute paths on disk
165
+ const lines = [
166
+ `@source "./src";`,
167
+ ...allCss.map((p) => `@import "${toServerRoot(p)}";`),
168
+ ];
169
+ return lines.join("\n");
170
+ }
171
+
172
+ if (kind === "entry") {
173
+ const spec = toFs(entry);
174
+
175
+ const lines: string[] = [];
176
+
177
+ // Import Vite HMR client from root
178
+ lines.push(`import "/@vite/client";`);
179
+
180
+ lines.push(`
181
+ import RefreshRuntime from "/@react-refresh";
182
+
183
+ if (!window.__vite_plugin_react_preamble_installed__) {
184
+ RefreshRuntime.injectIntoGlobalHook(window);
185
+ window.$RefreshReg$ = () => {};
186
+ window.$RefreshSig$ = () => (type) => type;
187
+ window.__vite_plugin_react_preamble_installed__ = true;
188
+ }
189
+ `);
190
+
191
+ lines.push(`import "/${name}.css";`);
192
+ lines.push(`await import(${JSON.stringify(spec)});`);
193
+
194
+ return lines.join("\n");
195
+ }
196
+
197
+ return null;
198
+ },
199
+ };
200
+ }
201
+
202
+ const inputs = buildInputs();
203
+
204
+ export default defineConfig(({}) => ({
205
+ plugins: [
206
+ tailwindcss(),
207
+ react(),
208
+ multiEntryDevEndpoints({ entries: inputs }),
209
+ ],
210
+ cacheDir: "node_modules/.vite-react",
211
+ server: {
212
+ port: 4444,
213
+ strictPort: true,
214
+ cors: true,
215
+ },
216
+ esbuild: {
217
+ jsx: "automatic",
218
+ jsxImportSource: "react",
219
+ target: "es2022",
220
+ },
221
+ build: {
222
+ target: "es2022",
223
+ sourcemap: true,
224
+ minify: "esbuild",
225
+ outDir: "assets",
226
+ assetsDir: ".",
227
+ rollupOptions: {
228
+ input: inputs,
229
+ preserveEntrySignatures: "strict",
230
+ },
231
+ },
232
+ }));