sunpeak 0.12.8 → 0.12.12

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 (84) hide show
  1. package/README.md +123 -43
  2. package/bin/commands/dev.mjs +72 -64
  3. package/dist/chatgpt/index.cjs +1 -1
  4. package/dist/chatgpt/index.js +1 -1
  5. package/dist/index.cjs +1 -1
  6. package/dist/index.js +2 -2
  7. package/dist/{simulator-url-CYMOGoB1.cjs → simulator-url-Bnxt0LlN.cjs} +3 -2
  8. package/dist/{simulator-url-CYMOGoB1.cjs.map → simulator-url-Bnxt0LlN.cjs.map} +1 -1
  9. package/dist/{simulator-url-DG79-dU3.js → simulator-url-CQhLDEdd.js} +2 -1
  10. package/dist/{simulator-url-DG79-dU3.js.map → simulator-url-CQhLDEdd.js.map} +1 -1
  11. package/package.json +1 -1
  12. package/template/dist/albums/albums.json +1 -1
  13. package/template/dist/carousel/carousel.json +1 -1
  14. package/template/dist/map/map.json +1 -1
  15. package/template/dist/review/review.json +1 -1
  16. package/template/node_modules/.vite/deps/_metadata.json +19 -19
  17. package/template/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  18. package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/@openai_apps-sdk-ui_components_Button.js +2 -2
  19. package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/@openai_apps-sdk-ui_components_Select.js +15 -15
  20. package/template/node_modules/.vite-mcp/{deps_temp_c22d194e/chunk-IFPDCKUT.js → deps_temp_ed5fce0f/chunk-24D227QE.js} +5 -5
  21. package/template/tests/e2e/review.spec.ts +1 -1
  22. package/template/tests/simulations/review/review-diff-simulation.json +1 -1
  23. package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/@openai_apps-sdk-ui_components_Avatar.js +0 -0
  24. package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/@openai_apps-sdk-ui_components_Avatar.js.map +0 -0
  25. package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/@openai_apps-sdk-ui_components_Button.js.map +0 -0
  26. package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/@openai_apps-sdk-ui_components_Checkbox.js +0 -0
  27. package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/@openai_apps-sdk-ui_components_Checkbox.js.map +0 -0
  28. package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/@openai_apps-sdk-ui_components_Icon.js +0 -0
  29. package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/@openai_apps-sdk-ui_components_Icon.js.map +0 -0
  30. package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/@openai_apps-sdk-ui_components_Input.js +0 -0
  31. package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/@openai_apps-sdk-ui_components_Input.js.map +0 -0
  32. package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/@openai_apps-sdk-ui_components_SegmentedControl.js +4 -4
  33. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/@openai_apps-sdk-ui_components_SegmentedControl.js.map +0 -0
  34. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/@openai_apps-sdk-ui_components_Select.js.map +0 -0
  35. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/@openai_apps-sdk-ui_components_Textarea.js +0 -0
  36. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/@openai_apps-sdk-ui_components_Textarea.js.map +0 -0
  37. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/@openai_apps-sdk-ui_theme.js +0 -0
  38. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/@openai_apps-sdk-ui_theme.js.map +0 -0
  39. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e/chunk-IFPDCKUT.js.map → deps_temp_ed5fce0f/chunk-24D227QE.js.map} +0 -0
  40. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-6CR5OFLH.js +0 -0
  41. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-6CR5OFLH.js.map +0 -0
  42. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-EGRHWZRV.js +0 -0
  43. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-EGRHWZRV.js.map +0 -0
  44. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-EXGLAROW.js +0 -0
  45. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-EXGLAROW.js.map +0 -0
  46. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-ILHRZGIS.js +0 -0
  47. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-ILHRZGIS.js.map +0 -0
  48. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-JSPMUVNT.js +0 -0
  49. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-JSPMUVNT.js.map +0 -0
  50. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-L2PWLFDO.js +0 -0
  51. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-L2PWLFDO.js.map +0 -0
  52. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-OMOWBQDU.js +0 -0
  53. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-OMOWBQDU.js.map +0 -0
  54. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-R3BTL4CL.js +0 -0
  55. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-R3BTL4CL.js.map +0 -0
  56. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-SPDZ46BB.js +0 -0
  57. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-SPDZ46BB.js.map +0 -0
  58. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-WSHFT23M.js +0 -0
  59. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-WSHFT23M.js.map +0 -0
  60. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-Y7UNCRGH.js +0 -0
  61. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-Y7UNCRGH.js.map +0 -0
  62. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-ZNFXUT4Q.js +0 -0
  63. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/chunk-ZNFXUT4Q.js.map +0 -0
  64. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/clsx.js +0 -0
  65. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/clsx.js.map +0 -0
  66. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/embla-carousel-react.js +0 -0
  67. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/embla-carousel-react.js.map +0 -0
  68. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/embla-carousel-wheel-gestures.js +0 -0
  69. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/embla-carousel-wheel-gestures.js.map +0 -0
  70. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/mapbox-gl.js +0 -0
  71. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/mapbox-gl.js.map +0 -0
  72. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/package.json +0 -0
  73. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/react-dom.js +0 -0
  74. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/react-dom.js.map +0 -0
  75. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/react-dom_client.js +0 -0
  76. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/react-dom_client.js.map +0 -0
  77. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/react.js +0 -0
  78. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/react.js.map +0 -0
  79. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/react_jsx-dev-runtime.js +0 -0
  80. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/react_jsx-dev-runtime.js.map +0 -0
  81. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/react_jsx-runtime.js +0 -0
  82. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/react_jsx-runtime.js.map +0 -0
  83. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/tailwind-merge.js +0 -0
  84. /package/template/node_modules/.vite-mcp/{deps_temp_c22d194e → deps_temp_ed5fce0f}/tailwind-merge.js.map +0 -0
package/README.md CHANGED
@@ -8,16 +8,17 @@
8
8
 
9
9
  # sunpeak
10
10
 
11
- [![npm version](https://img.shields.io/npm/v/sunpeak.svg?style=flat-square)](https://www.npmjs.com/package/sunpeak)
12
- [![npm downloads](https://img.shields.io/npm/dm/sunpeak.svg?style=flat-square)](https://www.npmjs.com/package/sunpeak)
13
- [![CI](https://img.shields.io/github/actions/workflow/status/Sunpeak-AI/sunpeak/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/Sunpeak-AI/sunpeak/actions)
14
- [![License](https://img.shields.io/npm/l/sunpeak.svg?style=flat-square)](https://github.com/Sunpeak-AI/sunpeak/blob/main/LICENSE)
15
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org/)
16
- [![React](https://img.shields.io/badge/React-19-blue?style=flat-square&logo=react)](https://reactjs.org/)
11
+ [![npm version](https://img.shields.io/npm/v/sunpeak.svg?style=flat&color=FFB800&labelColor=1A1F36)](https://www.npmjs.com/package/sunpeak)
12
+ [![npm downloads](https://img.shields.io/npm/dm/sunpeak.svg?style=flat&color=FFB800&labelColor=1A1F36)](https://www.npmjs.com/package/sunpeak)
13
+ [![stars](https://img.shields.io/github/stars/Sunpeak-AI/sunpeak?style=flat&color=FFB800&labelColor=1A1F36)](https://github.com/Sunpeak-AI/sunpeak)
14
+ [![CI](https://img.shields.io/github/actions/workflow/status/Sunpeak-AI/sunpeak/ci.yml?branch=main&style=flat&label=ci&color=FFB800&labelColor=1A1F36)](https://github.com/Sunpeak-AI/sunpeak/actions)
15
+ [![License](https://img.shields.io/npm/l/sunpeak.svg?style=flat&color=FFB800&labelColor=1A1F36)](https://github.com/Sunpeak-AI/sunpeak/blob/main/LICENSE)
16
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue?style=flat&logo=typescript&label=ts&color=FFB800&logoColor=white&labelColor=1A1F36)](https://www.typescriptlang.org/)
17
+ [![React](https://img.shields.io/badge/React-19-blue?style=flat&logo=react&label=react&color=FFB800&logoColor=white&labelColor=1A1F36)](https://reactjs.org/)
17
18
 
18
- The ChatGPT App framework.
19
+ Local-first ChatGPT App framework.
19
20
 
20
- Quickstart, build, test, and ship your ChatGPT App—locally!
21
+ Quickstart, build, test, and ship your ChatGPT App!
21
22
 
22
23
  [Demo (Hosted)](https://sunpeak.ai/#simulator) ~
23
24
  [Demo (Video)](https://d10djik02wlf6x.cloudfront.net/sunpeak-demo-prod.mp4) ~
@@ -43,21 +44,21 @@ pnpm add -g sunpeak
43
44
  sunpeak new
44
45
  ```
45
46
 
46
- To add sunpeak to an existing project, refer to the [documentation](https://docs.sunpeak.ai/add-to-existing-project).
47
+ To add `sunpeak` to an existing project, refer to the [documentation](https://docs.sunpeak.ai/add-to-existing-project).
47
48
 
48
49
  ## Overview
49
50
 
50
- sunpeak is an npm package that helps you build ChatGPT Apps (MCP Resources) while keeping your MCP server client-agnostic. sunpeak consists of:
51
+ `sunpeak` is an npm package that helps you build ChatGPT Apps (MCP resources) while keeping your MCP server client-agnostic. `sunpeak` consists of:
51
52
 
52
53
  ### The `sunpeak` library
53
54
 
54
55
  1. Runtime APIs: Strongly typed APIs for interacting with the ChatGPT runtime, **architected to support future platforms** (Gemini, Claude, etc.).
55
- 2. ChatGPT simulator: React component replicating ChatGPT's runtime to **test Apps locally and automatically**.
56
+ 2. ChatGPT simulator: React component replicating ChatGPT's runtime to **test Apps locally and automatically** via UI, props, or URL parameters.
56
57
  3. MCP server: Serve Resources with mock data to the real ChatGPT with HMR (**no more cache issues or 5-click manual refreshes**).
57
58
 
58
59
  ### The `sunpeak` framework
59
60
 
60
- Next.js for ChatGPT Apps. Using a Review page as an example, sunpeak projects look like:
61
+ Next.js for ChatGPT Apps. Using an example App `my-app` with a `Review` UI (MCP resource), `sunpeak` projects look like:
61
62
 
62
63
  ```bash
63
64
  my-app/
@@ -72,11 +73,11 @@ my-app/
72
73
  └── package.json
73
74
  ```
74
75
 
75
- 1. Project scaffold: Complete development setup with the sunpeak library.
76
- 2. UI components: Production-ready components following ChatGPT design guidelines and using OpenAI apps-sdk-ui React components.
76
+ 1. Project scaffold: Complete development setup with the `sunpeak` library.
77
+ 2. UI components: Production-ready components following ChatGPT design guidelines and using OpenAI `apps-sdk-ui` React components.
77
78
  3. Convention over configuration:
78
- 1. Create UIs by creating a `-resource.tsx` component file ([example](#resource-component)) and `-resource.json` MCP metadata file ([example](#resource-mcp-metadata)).
79
- 2. Create test state (simulations) for local dev, ChatGPT dev, automated testing, and demos by creating a `-simulation.json` file. ([example](#simulation))
79
+ 1. Create a UI by creating a `-resource.tsx` component file ([example](#resource-component)) and `-resource.json` MCP metadata file ([example](#resource-mcp-metadata)).
80
+ 2. Create test state (`Simulation`s) for local dev, ChatGPT dev, automated testing, and demos by creating a `-simulation.json` file. ([example](#simulation))
80
81
 
81
82
  ### The `sunpeak` CLI
82
83
 
@@ -93,46 +94,76 @@ Think Docker Hub for ChatGPT Apps:
93
94
  </div>
94
95
 
95
96
  1. Tag your app builds with version numbers and environment names (like `v1.0.0` and `prod`)
96
- 2. `push` built Apps to a central location
97
- 3. `pull` built Apps to be run in different environments, like your production MCP server.
97
+ 2. `sunpeak push` built Apps to a central location
98
+ 3. `sunpeak pull` built Apps to be run in different environments, like your production MCP server.
98
99
  4. Share your fully-functional demo Apps with teammates, prospects, and strangers!
99
100
 
100
- ## Examples
101
+ ## Example App
101
102
 
102
- Example sunpeak resource & simulation files for an MCP Resource called "Review".
103
+ Example `Resource`, `Simulation`, and testing file (using `ChatGPTSimulator`) for an MCP resource called "Review".
103
104
 
104
- ### Resource Component
105
+ ### `Resource` Component
105
106
 
106
- React component defining a UI (MCP Resource) in your ChatGPT App.
107
+ ```bash
108
+ my-app/
109
+ ├── src/resources/
110
+ │ └── review/
111
+ │ ├── review-resource.tsx # This one!
112
+ │ └── review-resource.json
113
+ ```
114
+
115
+ React component (`.tsx`) defining a UI (MCP resource) in your ChatGPT App.
107
116
 
108
117
  ```tsx
109
118
  // src/resources/review-resource.tsx
110
- import { Card } from './components';
119
+
120
+ import { useWidgetProps, useWidgetState } from 'sunpeak';
121
+ import { Button } from '@openai/apps-sdk-ui/components/Button';
111
122
 
112
123
  export function ReviewResource() {
124
+ const data = useWidgetProps<{ title: string; changes: string[] }>();
125
+ const [state, setState] = useWidgetState<{ decision: string | null }>(() => ({
126
+ decision: null,
127
+ }));
128
+
113
129
  return (
114
- <Card
115
- image="https://images.unsplash.com/photo-1520950237264-dfe336995c34"
116
- imageAlt="Lady Bird Lake"
117
- header="Lady Bird Lake"
118
- metadata="⭐ 4.5 • Austin, TX"
119
- button1={{ children: 'Visit', isPrimary: true, onClick: () => {} }}
120
- button2={{ children: 'Learn More', onClick: () => {} }}
121
- >
122
- Scenic lake perfect for kayaking, paddleboarding, and trails.
123
- </Card>
130
+ <div>
131
+ <h1>{data.title}</h1>
132
+ <ul>
133
+ {data.changes.map((change, i) => (
134
+ <li key={i}>{change}</li>
135
+ ))}
136
+ </ul>
137
+ {state.decision ? (
138
+ <p>{state.decision === 'approved' ? 'Approved!' : 'Rejected'}</p>
139
+ ) : (
140
+ <>
141
+ <Button onClick={() => setState({ decision: 'approved' })}>Approve</Button>
142
+ <Button onClick={() => setState({ decision: 'rejected' })}>Reject</Button>
143
+ </>
144
+ )}
145
+ </div>
124
146
  );
125
147
  }
126
148
  ```
127
149
 
128
- ### Resource MCP Metadata
150
+ ### `Resource` MCP Metadata
151
+
152
+ ```bash
153
+ my-app/
154
+ ├── src/resources/
155
+ │ └── review/
156
+ │ ├── review-resource.tsx
157
+ │ └── review-resource.json # This one!
158
+ ```
129
159
 
130
- MCP metadata for your UI. Version your resource metadata alongside the resource itself.
160
+ MCP metadata (`.json`) for your UI. Version your resource metadata alongside the resource itself.
131
161
 
132
- This is just an official [MCP Resource object](https://modelcontextprotocol.io/specification/2025-11-25/server/resources#resource).
162
+ This is just an official [MCP resource object](https://modelcontextprotocol.io/specification/2025-11-25/server/resources#resource).
133
163
 
134
164
  ```jsonc
135
165
  // src/resources/review-resource.json
166
+
136
167
  {
137
168
  "name": "review",
138
169
  "title": "Review",
@@ -147,21 +178,28 @@ This is just an official [MCP Resource object](https://modelcontextprotocol.io/s
147
178
  }
148
179
  ```
149
180
 
150
- ### Simulation
181
+ ### `Simulation`
182
+
183
+ ```bash
184
+ ├── tests/simulations/
185
+ │ └── review/
186
+ │ ├── review-{scenario1}-simulation.json # These!
187
+ │ └── review-{scenario2}-simulation.json # These!
188
+ ```
151
189
 
152
- `sunpeak` object. Testing a ChatGPT App require setting a lot of state: state in your backend (accessed via MCP tool), the stored widget runtime, and ChatGPT itself.
190
+ `sunpeak` testing object (`.json`) defining key App-owned states.
153
191
 
154
- Simulation files let you define key App states for development, automated testing, and demo purposes.
192
+ Testing a ChatGPT App requires setting a lot of state: state in your **backend**, **MCP tools**, **stored widget runtime**, and ChatGPT itself.
155
193
 
156
- Simulation files contain an [official MCP Tool object](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool) and an [official MCP CallToolResult object](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#structured-content). ChatGPT state (like light/dark mode) is not set on the simulation, but rather on the sunpeak `ChatGPTSimulator` itself via UI, props, or URL params.
194
+ `Simulation` files contain an [official MCP tool object](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool), an [official MCP CallToolResult object](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#structured-content), and a [sunpeak widgetState object](https://docs.sunpeak.ai/api-reference/hooks/use-widget-state) so you can define **backend**, **tool**, and **stored widget runtime** states for testing.
157
195
 
158
196
  ```jsonc
159
197
  // tests/simulations/review-diff-simulation.json
198
+
160
199
  {
161
- "userMessage": "Refactor the authentication module", // Simulator styling.
162
- // Official MCP Tool object.
200
+ // Official MCP tool object.
163
201
  "tool": {
164
- "name": "diff-review",
202
+ "name": "review-diff",
165
203
  "description": "Show a review dialog for a proposed code diff",
166
204
  "inputSchema": { "type": "object", "properties": {}, "additionalProperties": false },
167
205
  "title": "Diff Review",
@@ -176,13 +214,55 @@ Simulation files contain an [official MCP Tool object](https://modelcontextproto
176
214
  // Official MCP CallToolResult object.
177
215
  "callToolResult": {
178
216
  "structuredContent": {
217
+ "title": "Refactor Authentication Module",
179
218
  // ...
180
219
  },
181
220
  "_meta": {},
182
221
  },
222
+ // Initial widget state object (persisted in ChatGPT).
223
+ "widgetState": {
224
+ // ...
225
+ },
183
226
  }
184
227
  ```
185
228
 
229
+ ### `ChatGPTSimulator`
230
+
231
+ ```bash
232
+ ├── tests/e2e/
233
+ │ └── review.spec.ts # This! (not pictured above for simplicity)
234
+ └── package.json
235
+ ```
236
+
237
+ The `ChatGPTSimulator` allows you to set **ChatGPT state** (like light/dark mode) via URL params, which can be rendered alongside your `Simulation`s and tested via pre-configured Playwright end-to-end tests (`.spec.ts`).
238
+
239
+ Using the `ChatGPTSimulator` and `Simulation`s, you can test all possible App states locally and automatically!
240
+
241
+ ```ts
242
+ // tests/e2e/review.spec.ts
243
+
244
+ import { test, expect } from '@playwright/test';
245
+ import { createSimulatorUrl } from 'sunpeak/chatgpt';
246
+
247
+ test.describe('Review Resource', () => {
248
+ test.describe('Light Mode', () => {
249
+ test('should render review title with correct styles', async ({ page }) => {
250
+ params = { simulation: 'review-diff', theme: 'light' }; // Set sim & ChatGPT state.
251
+ await page.goto(createSimulatorUrl(params));
252
+ await page.waitForLoadState('networkidle');
253
+
254
+ const title = page.locator('h1:has-text("Refactor Authentication Module")');
255
+ await expect(title).toBeVisible();
256
+
257
+ const color = await title.evaluate((el) => window.getComputedStyle(el).color);
258
+
259
+ // Light mode should render dark text.
260
+ expect(color).toBe('rgb(13, 13, 13)');
261
+ });
262
+ });
263
+ });
264
+ ```
265
+
186
266
  ## Resources
187
267
 
188
268
  - [ChatGPT Apps SDK Design Guidelines](https://developers.openai.com/apps-sdk/concepts/design-guidelines)
@@ -86,6 +86,9 @@ export async function dev(projectRoot = process.cwd(), args = []) {
86
86
  // Parse --no-begging flag
87
87
  const noBegging = args.includes('--no-begging');
88
88
 
89
+ // Parse --prod-mcp flag (serve production build files over MCP instead of Vite HMR)
90
+ const prodMcp = args.includes('--prod-mcp');
91
+
89
92
  console.log(`Starting Vite dev server on port ${port}...`);
90
93
 
91
94
  // Check if we're in the sunpeak workspace (directory is named "template")
@@ -177,37 +180,41 @@ export async function dev(projectRoot = process.cwd(), args = []) {
177
180
  }
178
181
  }
179
182
 
180
- // Start MCP server with its own Vite instance
183
+ // Start MCP server with its own Vite instance (unless --prod-mcp is set)
181
184
  if (simulations.length > 0) {
182
- console.log(`\nStarting MCP server with ${simulations.length} simulation(s)...`);
183
-
184
- // Virtual entry module plugin for MCP
185
- // For internal dev (template): Import sunpeak styles directly via JS to avoid CSS @import
186
- // alias issues with Lightning CSS, then import app.css for user customizations.
187
- // For external users: Import OpenAI SDK CSS directly (ensures Tailwind processes @theme static),
188
- // then import globals.css for user's Tailwind config and custom styles.
189
- const styleImports = isTemplate
190
- ? `import '${parentSrc}/style.css';\nimport '/src/styles/app.css';`
191
- : `import '@openai/apps-sdk-ui/css';\nimport '/src/styles/globals.css';`;
192
-
193
- const sunpeakEntryPlugin = () => ({
194
- name: 'sunpeak-entry',
195
- resolveId(id) {
196
- if (id.startsWith('virtual:sunpeak-entry')) {
197
- return id;
198
- }
199
- },
200
- load(id) {
201
- if (id.startsWith('virtual:sunpeak-entry')) {
202
- const url = new URL(id.replace('virtual:sunpeak-entry', 'http://x'));
203
- const srcPath = url.searchParams.get('src');
204
- const componentName = url.searchParams.get('component');
205
-
206
- if (!srcPath || !componentName) {
207
- return 'console.error("Missing src or component param");';
185
+ const mcpMode = prodMcp ? 'production build' : 'Vite HMR';
186
+ console.log(`\nStarting MCP server with ${simulations.length} simulation(s) (${mcpMode})...`);
187
+
188
+ let mcpViteServer = null;
189
+
190
+ if (!prodMcp) {
191
+ // Virtual entry module plugin for MCP
192
+ // For internal dev (template): Import sunpeak styles directly via JS to avoid CSS @import
193
+ // alias issues with Lightning CSS, then import app.css for user customizations.
194
+ // For external users: Import OpenAI SDK CSS directly (ensures Tailwind processes @theme static),
195
+ // then import globals.css for user's Tailwind config and custom styles.
196
+ const styleImports = isTemplate
197
+ ? `import '${parentSrc}/style.css';\nimport '/src/styles/app.css';`
198
+ : `import '@openai/apps-sdk-ui/css';\nimport '/src/styles/globals.css';`;
199
+
200
+ const sunpeakEntryPlugin = () => ({
201
+ name: 'sunpeak-entry',
202
+ resolveId(id) {
203
+ if (id.startsWith('virtual:sunpeak-entry')) {
204
+ return id;
208
205
  }
206
+ },
207
+ load(id) {
208
+ if (id.startsWith('virtual:sunpeak-entry')) {
209
+ const url = new URL(id.replace('virtual:sunpeak-entry', 'http://x'));
210
+ const srcPath = url.searchParams.get('src');
211
+ const componentName = url.searchParams.get('component');
212
+
213
+ if (!srcPath || !componentName) {
214
+ return 'console.error("Missing src or component param");';
215
+ }
209
216
 
210
- return `
217
+ return `
211
218
  import { createElement } from 'react';
212
219
  import { createRoot } from 'react-dom/client';
213
220
  ${styleImports}
@@ -220,43 +227,44 @@ if (!Component) {
220
227
  createRoot(document.getElementById('root')).render(createElement(Component));
221
228
  }
222
229
  `;
223
- }
224
- },
225
- });
230
+ }
231
+ },
232
+ });
226
233
 
227
- // Create Vite dev server in middleware mode for MCP
228
- // Use separate cache directory to avoid conflicts with main dev server
229
- const mcpViteServer = await createServer({
230
- root: projectRoot,
231
- cacheDir: 'node_modules/.vite-mcp',
232
- plugins: [react(), tailwindcss(), sunpeakEntryPlugin()],
233
- resolve: {
234
- alias: {
235
- ...(isTemplate && {
236
- sunpeak: parentSrc,
237
- }),
234
+ // Create Vite dev server in middleware mode for MCP
235
+ // Use separate cache directory to avoid conflicts with main dev server
236
+ mcpViteServer = await createServer({
237
+ root: projectRoot,
238
+ cacheDir: 'node_modules/.vite-mcp',
239
+ plugins: [react(), tailwindcss(), sunpeakEntryPlugin()],
240
+ resolve: {
241
+ alias: {
242
+ ...(isTemplate && {
243
+ sunpeak: parentSrc,
244
+ }),
245
+ },
238
246
  },
239
- },
240
- server: {
241
- middlewareMode: true,
242
- allowedHosts: true,
243
- watch: {
244
- // Only watch files that affect the UI bundle (not JSON, tests, etc.)
245
- // MCP resources reload on next tool call, not on file change
246
- ignored: (filePath) => {
247
- if (!filePath.includes('.')) return false; // Watch directories
248
- if (/\.(tsx?|css)$/.test(filePath)) {
249
- return /\.(test|spec)\.tsx?$/.test(filePath); // Ignore tests
250
- }
251
- return true; // Ignore everything else
247
+ server: {
248
+ middlewareMode: true,
249
+ allowedHosts: true,
250
+ watch: {
251
+ // Only watch files that affect the UI bundle (not JSON, tests, etc.)
252
+ // MCP resources reload on next tool call, not on file change
253
+ ignored: (filePath) => {
254
+ if (!filePath.includes('.')) return false; // Watch directories
255
+ if (/\.(tsx?|css)$/.test(filePath)) {
256
+ return /\.(test|spec)\.tsx?$/.test(filePath); // Ignore tests
257
+ }
258
+ return true; // Ignore everything else
259
+ },
252
260
  },
253
261
  },
254
- },
255
- optimizeDeps: {
256
- include: ['react', 'react-dom/client'],
257
- },
258
- appType: 'custom',
259
- });
262
+ optimizeDeps: {
263
+ include: ['react', 'react-dom/client'],
264
+ },
265
+ appType: 'custom',
266
+ });
267
+ }
260
268
 
261
269
  const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
262
270
  runMCPServer({
@@ -264,18 +272,18 @@ if (!Component) {
264
272
  version: pkg.version || '0.1.0',
265
273
  simulations,
266
274
  port: 6766,
267
- viteServer: mcpViteServer,
275
+ ...(mcpViteServer && { viteServer: mcpViteServer }),
268
276
  });
269
277
 
270
278
  // Handle signals - close both servers
271
279
  process.on('SIGINT', async () => {
272
- await mcpViteServer.close();
280
+ if (mcpViteServer) await mcpViteServer.close();
273
281
  await server.close();
274
282
  process.exit(0);
275
283
  });
276
284
 
277
285
  process.on('SIGTERM', async () => {
278
- await mcpViteServer.close();
286
+ if (mcpViteServer) await mcpViteServer.close();
279
287
  await server.close();
280
288
  process.exit(0);
281
289
  });
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const simulatorUrl = require("../simulator-url-CYMOGoB1.cjs");
3
+ const simulatorUrl = require("../simulator-url-Bnxt0LlN.cjs");
4
4
  exports.ChatGPTSimulator = simulatorUrl.ChatGPTSimulator;
5
5
  exports.IframeResource = simulatorUrl.IframeResource;
6
6
  exports.ThemeProvider = simulatorUrl.ThemeProvider;
@@ -1,4 +1,4 @@
1
- import { C, I, T, c, i, u } from "../simulator-url-DG79-dU3.js";
1
+ import { C, I, T, c, i, u } from "../simulator-url-CQhLDEdd.js";
2
2
  export {
3
3
  C as ChatGPTSimulator,
4
4
  I as IframeResource,
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const simulatorUrl = require("./simulator-url-CYMOGoB1.cjs");
3
+ const simulatorUrl = require("./simulator-url-Bnxt0LlN.cjs");
4
4
  const React = require("react");
5
5
  function _interopNamespaceDefault(e) {
6
6
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { a as clsx } from "./simulator-url-DG79-dU3.js";
2
- import { C, I, S, T, c, v, s, q, i, r, w, t, e, f, g, h, j, u, k, l, m, n, d, b, o, p } from "./simulator-url-DG79-dU3.js";
1
+ import { a as clsx } from "./simulator-url-CQhLDEdd.js";
2
+ import { C, I, S, T, c, v, s, q, i, r, w, t, e, f, g, h, j, u, k, l, m, n, d, b, o, p } from "./simulator-url-CQhLDEdd.js";
3
3
  import * as React from "react";
4
4
  const MOBILE_BREAKPOINT = 768;
5
5
  function useIsMobile() {
@@ -5620,7 +5620,7 @@ const useEscCloseStack = (listening, cb) => {
5620
5620
  }, [id, listening, latestCallback]);
5621
5621
  };
5622
5622
  const __vite_import_meta_env__ = { "DEV": false, "MODE": "production" };
5623
- const META_ENV = typeof { url: typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("simulator-url-CYMOGoB1.cjs", document.baseURI).href } !== "undefined" ? __vite_import_meta_env__ : void 0;
5623
+ const META_ENV = typeof { url: typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("simulator-url-Bnxt0LlN.cjs", document.baseURI).href } !== "undefined" ? __vite_import_meta_env__ : void 0;
5624
5624
  const NODE_ENV = typeof process !== "undefined" && process.env?.NODE_ENV ? process.env?.NODE_ENV : "production";
5625
5625
  const isDev = NODE_ENV === "development" || !!META_ENV?.DEV;
5626
5626
  const isJSDomLike = typeof navigator !== "undefined" && /(jsdom|happy-dom)/i.test(navigator.userAgent) || typeof globalThis.happyDOM === "object";
@@ -8744,6 +8744,7 @@ function ChatGPTSimulator({
8744
8744
  mock.setWidgetStateExternal(selectedSim.widgetState);
8745
8745
  }
8746
8746
  mock.toolOutput = selectedSim.callToolResult?.structuredContent ?? null;
8747
+ mock.toolResponseMetadata = selectedSim.callToolResult?._meta ?? null;
8747
8748
  }
8748
8749
  }, [selectedSimulationName, selectedSim, mock]);
8749
8750
  const theme = useTheme() ?? DEFAULT_THEME;
@@ -9280,4 +9281,4 @@ exports.useWidgetAPI = useWidgetAPI;
9280
9281
  exports.useWidgetGlobal = useWidgetGlobal;
9281
9282
  exports.useWidgetProps = useWidgetProps;
9282
9283
  exports.useWidgetState = useWidgetState;
9283
- //# sourceMappingURL=simulator-url-CYMOGoB1.cjs.map
9284
+ //# sourceMappingURL=simulator-url-Bnxt0LlN.cjs.map