sunpeak 0.15.4 → 0.16.1

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 (98) hide show
  1. package/README.md +51 -48
  2. package/bin/commands/build.mjs +13 -4
  3. package/bin/commands/dev.mjs +64 -19
  4. package/bin/commands/new.mjs +13 -3
  5. package/bin/lib/extract-resource.mjs +1 -1
  6. package/bin/lib/extract-tool.mjs +78 -0
  7. package/bin/lib/patterns.mjs +2 -26
  8. package/dist/chatgpt/index.cjs +3 -6
  9. package/dist/chatgpt/index.cjs.map +1 -1
  10. package/dist/chatgpt/index.d.ts +1 -1
  11. package/dist/chatgpt/index.js +6 -9
  12. package/dist/claude/index.cjs +1 -1
  13. package/dist/claude/index.js +1 -1
  14. package/dist/discovery-CH80W5l9.js +217 -0
  15. package/dist/discovery-CH80W5l9.js.map +1 -0
  16. package/dist/discovery-DmB8_4QL.cjs +216 -0
  17. package/dist/discovery-DmB8_4QL.cjs.map +1 -0
  18. package/dist/{index-CutQgPzR.js → index-BjnAsaqp.js} +3 -6
  19. package/dist/index-BjnAsaqp.js.map +1 -0
  20. package/dist/{index-Cngntkp2.cjs → index-BvQ_ZuOO.cjs} +3 -6
  21. package/dist/{index-Cngntkp2.cjs.map → index-BvQ_ZuOO.cjs.map} +1 -1
  22. package/dist/{index-B0dxRJvS.cjs → index-C9CVbGFt.cjs} +3 -6
  23. package/dist/index-C9CVbGFt.cjs.map +1 -0
  24. package/dist/{index-Ce_5ZIdJ.js → index-CTGEqlgk.js} +3 -6
  25. package/dist/{index-Ce_5ZIdJ.js.map → index-CTGEqlgk.js.map} +1 -1
  26. package/dist/index.cjs +48 -5
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.js +3404 -3361
  30. package/dist/index.js.map +1 -1
  31. package/dist/lib/discovery-cli.cjs +58 -5
  32. package/dist/lib/discovery-cli.cjs.map +1 -1
  33. package/dist/lib/discovery-cli.d.ts +3 -2
  34. package/dist/lib/discovery-cli.js +61 -8
  35. package/dist/lib/discovery-cli.js.map +1 -1
  36. package/dist/lib/discovery.d.ts +42 -43
  37. package/dist/lib/extract-tool.d.ts +12 -0
  38. package/dist/mcp/favicon.d.ts +1 -1
  39. package/dist/mcp/index.cjs +3 -2
  40. package/dist/mcp/index.cjs.map +1 -1
  41. package/dist/mcp/index.d.ts +1 -1
  42. package/dist/mcp/index.js +3 -2
  43. package/dist/mcp/index.js.map +1 -1
  44. package/dist/mcp/types.d.ts +24 -1
  45. package/dist/platform/chatgpt/index.cjs +1 -1
  46. package/dist/platform/chatgpt/index.js +1 -1
  47. package/dist/simulator/index.cjs +2 -5
  48. package/dist/simulator/index.cjs.map +1 -1
  49. package/dist/simulator/index.d.ts +1 -1
  50. package/dist/simulator/index.js +5 -8
  51. package/dist/simulator/simulator-url.d.ts +9 -9
  52. package/dist/{simulator-DcfQBRXE.cjs → simulator-B56j5P8W.cjs} +8 -2
  53. package/dist/{simulator-DcfQBRXE.cjs.map → simulator-B56j5P8W.cjs.map} +1 -1
  54. package/dist/{simulator-CxrtnguM.js → simulator-C0H_k092.js} +8 -2
  55. package/dist/{simulator-CxrtnguM.js.map → simulator-C0H_k092.js.map} +1 -1
  56. package/dist/simulator-url-CuLqtnSS.js.map +1 -1
  57. package/dist/simulator-url-rgg_KYOg.cjs.map +1 -1
  58. package/dist/types/resource-config.d.ts +7 -5
  59. package/dist/{use-app-D_TeaMFG.js → use-app-BThbgFFT.js} +51 -22
  60. package/dist/{use-app-D_TeaMFG.js.map → use-app-BThbgFFT.js.map} +1 -1
  61. package/dist/{use-app-BnoSPiUT.cjs → use-app-BuufpXTQ.cjs} +49 -20
  62. package/dist/{use-app-BnoSPiUT.cjs.map → use-app-BuufpXTQ.cjs.map} +1 -1
  63. package/package.json +1 -1
  64. package/template/.sunpeak/dev.tsx +8 -4
  65. package/template/.sunpeak/resource-loader.tsx +2 -1
  66. package/template/README.md +14 -10
  67. package/template/package.json +2 -1
  68. package/template/src/resources/albums/{albums-resource.test.tsx → albums.test.tsx} +1 -1
  69. package/template/src/resources/albums/{albums-resource.tsx → albums.tsx} +0 -1
  70. package/template/src/resources/carousel/{carousel-resource.test.tsx → carousel.test.tsx} +1 -1
  71. package/template/src/resources/carousel/{carousel-resource.tsx → carousel.tsx} +0 -1
  72. package/template/src/resources/index.ts +4 -4
  73. package/template/src/resources/map/{map-resource.test.tsx → map.test.tsx} +1 -1
  74. package/template/src/resources/map/{map-resource.tsx → map.tsx} +0 -1
  75. package/template/src/resources/review/{review-resource.test.tsx → review.test.tsx} +1 -1
  76. package/template/src/resources/review/{review-resource.tsx → review.tsx} +1 -2
  77. package/template/src/server.ts +15 -0
  78. package/template/src/tools/review-diff.ts +24 -0
  79. package/template/src/tools/review-post.ts +26 -0
  80. package/template/src/tools/review-purchase.ts +31 -0
  81. package/template/src/tools/show-albums.ts +22 -0
  82. package/template/src/tools/show-carousel.ts +25 -0
  83. package/template/src/tools/show-map.ts +29 -0
  84. package/template/tests/e2e/albums.spec.ts +6 -6
  85. package/template/tests/e2e/carousel.spec.ts +6 -6
  86. package/template/tests/e2e/map.spec.ts +11 -11
  87. package/template/tests/simulations/{review/review-diff-simulation.json → review-diff.json} +1 -31
  88. package/template/tests/simulations/{review/review-post-simulation.json → review-post.json} +1 -37
  89. package/template/tests/simulations/{review/review-purchase-simulation.json → review-purchase.json} +1 -38
  90. package/template/tests/simulations/{albums/albums-show-simulation.json → show-albums.json} +1 -24
  91. package/template/tests/simulations/{carousel/carousel-show-simulation.json → show-carousel.json} +1 -24
  92. package/template/tests/simulations/{map/map-show-simulation.json → show-map.json} +1 -35
  93. package/dist/discovery-CRR3SlyI.cjs +0 -156
  94. package/dist/discovery-CRR3SlyI.cjs.map +0 -1
  95. package/dist/discovery-DzV3HLXs.js +0 -157
  96. package/dist/discovery-DzV3HLXs.js.map +0 -1
  97. package/dist/index-B0dxRJvS.cjs.map +0 -1
  98. package/dist/index-CutQgPzR.js.map +0 -1
package/README.md CHANGED
@@ -61,21 +61,26 @@ Next.js for MCP Apps. Using an example App `my-app` with a `Review` UI (MCP reso
61
61
 
62
62
  ```bash
63
63
  my-app/
64
- ├── src/resources/
65
- └── review/
66
- └── review-resource.tsx # Review UI component + resource metadata.
64
+ ├── src/
65
+ ├── resources/
66
+ └── review/
67
+ │ │ └── review.tsx # Review UI component + resource metadata.
68
+ │ ├── tools/
69
+ │ │ ├── review-diff.ts # Tool with handler, schema, and resource reference.
70
+ │ │ └── review-post.ts # Multiple tools can share one resource.
71
+ │ └── server.ts # Optional: auth, server config.
67
72
  ├── tests/simulations/
68
- └── review/
69
- ├── review-{scenario1}-simulation.json # Mock state for testing.
70
- │ └── review-{scenario2}-simulation.json # Mock state for testing.
73
+ ├── review-diff.json # Mock state for testing.
74
+ └── review-post.json # Mock state for testing.
71
75
  └── package.json
72
76
  ```
73
77
 
74
78
  1. Project scaffold: Complete development setup with the `sunpeak` library.
75
79
  2. UI components: Production-ready components following MCP App design guidelines.
76
80
  3. Convention over configuration:
77
- 1. Create a UI by creating a `-resource.tsx` file that exports a `ResourceConfig` and a React component ([example below](#resource-component)).
78
- 2. Create test state (`Simulation`s) for local dev, host dev, automated testing, and demos by creating a `-simulation.json` file. ([example below](#simulation))
81
+ 1. Create a UI by creating a `.tsx` file in `src/resources/{name}/` that exports a `ResourceConfig` and a React component ([example below](#resource-component)).
82
+ 2. Create a tool by creating a `.ts` file in `src/tools/` that exports `tool` (metadata with resource reference), `schema` (Zod), and a `default` handler ([example below](#tool-file)).
83
+ 3. Create test state (`Simulation`s) by creating a `.json` file in `tests/simulations/` ([example below](#simulation)).
79
84
 
80
85
  ### The `sunpeak` CLI
81
86
 
@@ -92,23 +97,15 @@ Example `Resource`, `Simulation`, and testing file (using the `Simulator`) for a
92
97
 
93
98
  ### `Resource` Component
94
99
 
95
- ```bash
96
- my-app/
97
- ├── src/resources/
98
- │ └── review/
99
- │ └── review-resource.tsx # This!
100
- ```
101
-
102
100
  Each resource `.tsx` file exports both the React component and the MCP resource metadata:
103
101
 
104
102
  ```tsx
105
- // src/resources/review/review-resource.tsx
103
+ // src/resources/review/review.tsx
106
104
 
107
105
  import { useToolData } from 'sunpeak';
108
106
  import type { ResourceConfig } from 'sunpeak';
109
107
 
110
108
  export const resource: ResourceConfig = {
111
- name: 'review',
112
109
  description: 'Visualize and review a code change',
113
110
  _meta: { ui: { csp: { resourceDomains: ['https://cdn.example.com'] } } },
114
111
  };
@@ -120,48 +117,54 @@ export function ReviewResource() {
120
117
  }
121
118
  ```
122
119
 
123
- ### `Simulation`
120
+ ### Tool File
124
121
 
125
- ```bash
126
- ├── tests/simulations/
127
- │ └── review/
128
- │ ├── review-{scenario1}-simulation.json # These!
129
- │ └── review-{scenario2}-simulation.json # These!
130
- ```
122
+ Each tool `.ts` file exports metadata (with a direct resource reference), a Zod schema, and a handler:
123
+
124
+ ```ts
125
+ // src/tools/review-diff.ts
131
126
 
132
- `sunpeak` testing object (`.json`) defining key App-owned states.
127
+ import { z } from 'zod';
128
+ import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';
133
129
 
134
- Testing an MCP App requires setting a lot of state: state in your **backend**, **MCP tools**, and the **host runtime**.
130
+ export const tool: AppToolConfig = {
131
+ resource: 'review',
132
+ title: 'Diff Review',
133
+ description: 'Show a review dialog for a proposed code diff',
134
+ annotations: { readOnlyHint: false },
135
+ _meta: { ui: { visibility: ['model', 'app'] } },
136
+ };
137
+
138
+ export const schema = {
139
+ changesetId: z.string().describe('Unique identifier for the changeset'),
140
+ title: z.string().describe('Title describing the changes'),
141
+ };
142
+
143
+ export default async function (args: Record<string, unknown>, extra: ToolHandlerExtra) {
144
+ return { structuredContent: { title: args.title, sections: [] } };
145
+ }
146
+ ```
147
+
148
+ ### `Simulation`
135
149
 
136
- `Simulation` files contain an [official MCP tool object](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool), `toolInput` (the arguments sent to CallTool), and `toolResult` (the [CallToolResult](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#structured-content) response) so you can define **backend**, **tool**, and **runtime** states for testing.
150
+ Simulation files provide fixture data for testing. Each references a tool by filename and contains the mock input/output:
137
151
 
138
152
  ```jsonc
139
- // tests/simulations/review-diff-simulation.json
153
+ // tests/simulations/review-diff.json
140
154
 
141
155
  {
142
- // Official MCP tool object.
143
- "tool": {
144
- "name": "review-diff",
145
- "description": "Show a review dialog for a proposed code diff",
146
- "inputSchema": { "type": "object", "properties": {}, "additionalProperties": false },
147
- "title": "Diff Review",
148
- "annotations": { "readOnlyHint": false },
149
- "_meta": {
150
- "ui": { "visibility": ["model", "app"] },
151
- },
152
- },
153
- // Tool input arguments (sent to CallTool).
156
+ "tool": "review-diff", // References src/tools/review-diff.ts
157
+ "userMessage": "Refactor the auth module to use JWT tokens.",
154
158
  "toolInput": {
155
159
  "changesetId": "cs_789",
156
- "title": "Refactor Authentication Module",
160
+ "title": "Refactor Authentication Module"
157
161
  },
158
- // Tool result data (CallToolResult response).
159
162
  "toolResult": {
160
163
  "structuredContent": {
161
164
  "title": "Refactor Authentication Module",
162
- // ...
163
- },
164
- },
165
+ "sections": [...]
166
+ }
167
+ }
165
168
  }
166
169
  ```
167
170
 
@@ -217,6 +220,6 @@ npx skills add Sunpeak-AI/sunpeak@create-sunpeak-app
217
220
 
218
221
  ## Resources
219
222
 
220
- - [MCP Apps](https://github.com/modelcontextprotocol/ext-apps)
221
- - [MCP Apps SDK Design Guidelines](https://developers.openai.com/apps-sdk/concepts/design-guidelines)
222
- - [MCP Apps SDK Documentation - UI](https://developers.openai.com/apps-sdk/build/chatgpt-ui)
223
+ - [MCP Apps Documentation](https://sunpeak.ai/docs/mcp-apps/introduction)
224
+ - [MCP Apps SDK](https://github.com/modelcontextprotocol/ext-apps)
225
+ - [ChatGPT Apps SDK Design Guidelines](https://developers.openai.com/apps-sdk/concepts/design-guidelines)
@@ -59,6 +59,11 @@ export async function build(projectRoot = process.cwd()) {
59
59
  process.exit(1);
60
60
  }
61
61
 
62
+ // Read project identity from package.json for appInfo
63
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
64
+ const appName = pkg.name || 'sunpeak-app';
65
+ const appVersion = pkg.version || '0.1.0';
66
+
62
67
  // Check if we're in the sunpeak workspace (directory is named "template")
63
68
  const isTemplate = path.basename(projectRoot) === 'template';
64
69
  const parentSrc = path.resolve(projectRoot, '../src');
@@ -153,7 +158,8 @@ export async function build(projectRoot = process.cwd()) {
153
158
  .filter(entry => entry.isDirectory())
154
159
  .map(entry => {
155
160
  const kebabName = entry.name;
156
- const resourceFile = `${kebabName}-resource.tsx`;
161
+
162
+ const resourceFile = `${kebabName}.tsx`;
157
163
  const resourcePath = path.join(resourcesDir, kebabName, resourceFile);
158
164
 
159
165
  // Skip directories without a resource file
@@ -163,10 +169,11 @@ export async function build(projectRoot = process.cwd()) {
163
169
 
164
170
  // Convert kebab-case to PascalCase: 'review' -> 'Review', 'my-widget' -> 'MyWidget'
165
171
  const pascalName = toPascalCase(kebabName);
172
+ const componentFile = resourceFile.replace(/\.tsx$/, '');
166
173
 
167
174
  return {
168
175
  componentName: `${pascalName}Resource`,
169
- componentFile: `${kebabName}-resource`,
176
+ componentFile,
170
177
  kebabName,
171
178
  resourceDir: path.join(resourcesDir, kebabName),
172
179
  entry: `.tmp/index-${kebabName}.tsx`,
@@ -180,7 +187,7 @@ export async function build(projectRoot = process.cwd()) {
180
187
 
181
188
  if (resourceFiles.length === 0) {
182
189
  console.error('Error: No resource directories found in src/resources/');
183
- console.error('Each resource should be a directory like: src/resources/review/review-resource.tsx');
190
+ console.error('Each resource should be a directory like: src/resources/review/review.tsx');
184
191
  process.exit(1);
185
192
  }
186
193
 
@@ -218,7 +225,7 @@ export async function build(projectRoot = process.cwd()) {
218
225
  // Create entry file from template in temp directory
219
226
  const entryContent = template
220
227
  .replace('// RESOURCE_IMPORT', `import { ${componentName}, resource } from '../src/resources/${kebabName}/${componentFile}';`)
221
- .replace('// RESOURCE_MOUNT', `createRoot(root).render(<AppProvider appInfo={{ name: resource.name, version: '1.0.0' }}><${componentName} /></AppProvider>);`);
228
+ .replace('// RESOURCE_MOUNT', `createRoot(root).render(<AppProvider appInfo={{ name: ${JSON.stringify(appName)}, version: ${JSON.stringify(appVersion)} }}><${componentName} /></AppProvider>);`);
222
229
 
223
230
  const entryPath = path.join(projectRoot, entry);
224
231
  writeFileSync(entryPath, entryContent);
@@ -283,6 +290,8 @@ export async function build(projectRoot = process.cwd()) {
283
290
  const destJson = path.join(distOutDir, `${kebabName}.json`);
284
291
 
285
292
  const meta = await extractResourceExport(srcTsx);
293
+ // Inject name from directory key if not explicitly set
294
+ meta.name = meta.name ?? kebabName;
286
295
  // Generate URI using resource name and build timestamp
287
296
  meta.uri = `ui://${meta.name}-${timestamp}`;
288
297
  writeFileSync(destJson, JSON.stringify(meta, null, 2));
@@ -179,7 +179,7 @@ export async function dev(projectRoot = process.cwd(), args = []) {
179
179
  sunpeakDiscovery = await import(pathToFileURL(join(sunpeakBase, 'dist/lib/discovery-cli.js')).href);
180
180
  }
181
181
  const { FAVICON_BUFFER: faviconBuffer, runMCPServer } = sunpeakMcp;
182
- const { findResourceDirs, findSimulationFiles, extractResourceExport } = sunpeakDiscovery;
182
+ const { findResourceDirs, findSimulationFilesFlat, findToolFiles, extractResourceExport, extractToolExport } = sunpeakDiscovery;
183
183
 
184
184
  // Vite plugin to serve the sunpeak favicon
185
185
  const sunpeakFaviconPlugin = () => ({
@@ -229,28 +229,73 @@ export async function dev(projectRoot = process.cwd(), args = []) {
229
229
  // Discover simulations using sunpeak's discovery utilities
230
230
  const resourcesDir = join(projectRoot, 'src/resources');
231
231
  const simulationsDir = join(projectRoot, 'tests/simulations');
232
- const resourceDirs = findResourceDirs(resourcesDir, (key) => `${key}-resource.tsx`, fs);
232
+ const toolsDir = join(projectRoot, 'src/tools');
233
233
 
234
- const simulations = [];
235
- for (const { key: resourceKey, dir: resourceDir, resourcePath } of resourceDirs) {
234
+ const resourceDirs = findResourceDirs(resourcesDir, (key) => `${key}.tsx`, fs);
235
+
236
+ // Build resource metadata map
237
+ const resourceMap = new Map();
238
+ for (const { key, resourcePath } of resourceDirs) {
236
239
  const resource = await extractResourceExport(resourcePath);
237
- const resourceSimDir = join(simulationsDir, resourceKey);
238
- const simulationFiles = findSimulationFiles(resourceSimDir, resourceKey, fs);
239
-
240
- for (const { filename, path: simPath } of simulationFiles) {
241
- const simulationKey = filename.replace(/-simulation\.json$/, '');
242
- const simulation = JSON.parse(readFileSync(simPath, 'utf-8'));
243
-
244
- simulations.push({
245
- ...simulation,
246
- name: simulationKey,
247
- distPath: join(projectRoot, `dist/${resourceKey}/${resourceKey}.html`),
248
- srcPath: `/src/resources/${resourceKey}/${resourceKey}-resource.tsx`,
249
- resource,
250
- });
240
+ // Inject name from directory key if not explicitly set
241
+ resource.name = resource.name ?? key;
242
+ resourceMap.set(key, resource);
243
+ }
244
+
245
+ // Discover tool files and extract metadata
246
+ const toolFiles = findToolFiles(toolsDir, fs);
247
+ const toolMap = new Map();
248
+ for (const { name: toolName, path: toolPath } of toolFiles) {
249
+ try {
250
+ const { tool } = await extractToolExport(toolPath);
251
+ toolMap.set(toolName, tool);
252
+ } catch (err) {
253
+ console.warn(`Warning: Could not extract metadata from tool ${toolName}: ${err.message}`);
251
254
  }
252
255
  }
253
256
 
257
+ // Discover simulations from flat directory
258
+ const simulations = [];
259
+ const simFiles = findSimulationFilesFlat(simulationsDir, fs);
260
+
261
+ for (const { name: simName, path: simPath } of simFiles) {
262
+ const simulation = JSON.parse(readFileSync(simPath, 'utf-8'));
263
+ const toolName = typeof simulation.tool === 'string' ? simulation.tool : simName;
264
+
265
+ // Look up tool metadata
266
+ const tool = toolMap.get(toolName);
267
+ if (!tool) {
268
+ console.warn(`Warning: Tool "${toolName}" not found for simulation "${simName}". Skipping.`);
269
+ continue;
270
+ }
271
+
272
+ // tool.resource is the resource name string — find matching resource key
273
+ const resourceName = tool.resource;
274
+ const resourceKey = resourceName
275
+ ? Array.from(resourceMap.keys()).find((k) => resourceMap.get(k).name === resourceName)
276
+ : undefined;
277
+
278
+ if (!resourceKey) {
279
+ console.warn(`Warning: No resource found for tool "${toolName}" in simulation "${simName}". Skipping.`);
280
+ continue;
281
+ }
282
+
283
+ // Determine source path for the resource
284
+ const resourceDir = resourceDirs.find((d) => d.key === resourceKey);
285
+ const srcPath = resourceDir
286
+ ? `/src/resources/${resourceKey}/${basename(resourceDir.resourcePath)}`
287
+ : undefined;
288
+
289
+ simulations.push({
290
+ ...simulation,
291
+ ...(typeof simulation.tool === 'string' ? { tool: { name: toolName, ...tool } } : {}),
292
+ name: simName,
293
+ distPath: join(projectRoot, `dist/${resourceKey}/${resourceKey}.html`),
294
+ srcPath,
295
+ resource: resourceMap.get(resourceKey),
296
+ });
297
+ }
298
+
254
299
  // Start MCP server with its own Vite instance (unless --prod-mcp is set)
255
300
  if (simulations.length > 0) {
256
301
  const mcpMode = prodMcp ? 'production build' : 'Vite HMR';
@@ -294,7 +339,7 @@ const Component = ResourceModule.default || ResourceModule['${componentName}'];
294
339
  if (!Component) {
295
340
  document.getElementById('root').innerHTML = '<pre style="color:red;padding:16px">Component not found: ${componentName}\\nExports: ' + Object.keys(ResourceModule).join(', ') + '</pre>';
296
341
  } else {
297
- const appInfo = { name: ResourceModule.resource?.name || '${componentName}', version: '1.0.0' };
342
+ const appInfo = { name: ${JSON.stringify(pkg.name || 'sunpeak-app')}, version: ${JSON.stringify(pkg.version || '0.1.0')} };
298
343
  root.render(
299
344
  createElement(AppProvider, { appInfo }, createElement(Component))
300
345
  );
@@ -153,9 +153,19 @@ export async function init(projectName, resourcesArg, deps = defaultDeps) {
153
153
  if (src.includes('/resources/') && name === resource) {
154
154
  return false;
155
155
  }
156
- // Skip simulation directories for excluded resources: tests/simulations/{resource}/
157
- if (src.includes('/tests/simulations/') && name === resource) {
158
- return false;
156
+ // Skip flat simulation files for excluded resources: tests/simulations/*.json
157
+ if (src.includes('/tests/simulations/') && name.endsWith('.json')) {
158
+ const baseName = name.replace(/\.json$/, '');
159
+ if (baseName === resource || baseName.startsWith(resource + '-') || baseName.endsWith('-' + resource)) {
160
+ return false;
161
+ }
162
+ }
163
+ // Skip tool files for excluded resources: src/tools/*.ts
164
+ if (src.includes('/src/tools/') && name.endsWith('.ts')) {
165
+ const baseName = name.replace(/\.ts$/, '');
166
+ if (baseName === resource || baseName.startsWith(resource + '-') || baseName.endsWith('-' + resource)) {
167
+ return false;
168
+ }
159
169
  }
160
170
  // Skip e2e test files for excluded resources
161
171
  if (src.includes('/tests/e2e/') && name === `${resource}.spec.ts`) {
@@ -55,7 +55,7 @@ export async function extractResourceExport(tsxPath) {
55
55
  if (!resource) {
56
56
  throw new Error(
57
57
  `No "resource" export found in ${tsxPath}. ` +
58
- `Add: export const resource = { name: '...', ... };`
58
+ `Add: export const resource: ResourceConfig = { title: '...', ... };`
59
59
  );
60
60
  }
61
61
 
@@ -0,0 +1,78 @@
1
+ import path from 'path';
2
+
3
+ /**
4
+ * Extract the `tool` named export from a tool .ts file.
5
+ *
6
+ * Uses esbuild in ESM mode to compile TypeScript and tree-shake to just the
7
+ * `tool` export. ESM tree-shaking drops unused exports (schema, handler) so
8
+ * their dependencies (zod, etc.) are never evaluated.
9
+ *
10
+ * `schema` and `default` handler are loaded at runtime via Vite SSR.
11
+ */
12
+ export async function extractToolExport(tsPath) {
13
+ const esbuild = await import('esbuild');
14
+ const absolutePath = path.resolve(tsPath);
15
+ const dir = path.dirname(absolutePath);
16
+ const base = path.basename(absolutePath);
17
+
18
+ const result = await esbuild.build({
19
+ stdin: {
20
+ contents: `export { tool } from './${base}';`,
21
+ resolveDir: dir,
22
+ loader: 'ts',
23
+ },
24
+ bundle: true,
25
+ write: false,
26
+ format: 'esm',
27
+ treeShaking: true,
28
+ loader: { '.tsx': 'tsx', '.ts': 'ts', '.jsx': 'jsx' },
29
+ logLevel: 'silent',
30
+ plugins: [
31
+ {
32
+ name: 'externalize-node-modules',
33
+ setup(build) {
34
+ build.onResolve({ filter: /.*/ }, (args) => {
35
+ if (args.kind !== 'import-statement') return;
36
+ if (!args.path.startsWith('.') && !args.path.startsWith('/')) {
37
+ return { external: true };
38
+ }
39
+ return undefined;
40
+ });
41
+ },
42
+ },
43
+ ],
44
+ });
45
+
46
+ if (!result.outputFiles?.length) {
47
+ throw new Error(`Failed to extract tool from ${tsPath}`);
48
+ }
49
+
50
+ // Strip import statements and export block so we can eval as plain JS.
51
+ // `tool` is pure data (no dependencies), so stripping imports is safe.
52
+ // Other top-level code (schema, etc.) may reference stripped imports but
53
+ // we only need the `tool` variable — errors in other code are caught and ignored.
54
+ const code = result.outputFiles[0].text
55
+ .replace(/^import\s+.*$/gm, '')
56
+ .replace(/^export\s*\{[^}]*\}\s*;?\s*$/m, '');
57
+ let tool;
58
+ try {
59
+ const fn = new Function(code + '\nreturn tool;');
60
+ tool = fn();
61
+ } catch {
62
+ // If other top-level code crashes (e.g. schema using stripped zod),
63
+ // extract just the tool variable declaration and eval that alone.
64
+ const toolMatch = result.outputFiles[0].text.match(/var tool\s*=\s*(\{[\s\S]*?\n\});/);
65
+ if (toolMatch) {
66
+ tool = new Function('return ' + toolMatch[1])();
67
+ }
68
+ }
69
+
70
+ if (!tool) {
71
+ throw new Error(
72
+ `No "tool" export found in ${tsPath}. ` +
73
+ `Add: export const tool: AppToolConfig = { resource, ... };`
74
+ );
75
+ }
76
+
77
+ return { tool };
78
+ }
@@ -11,7 +11,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
11
11
 
12
12
  /**
13
13
  * Auto-discover available resources from template/src/resources directories.
14
- * Each subdirectory containing a {name}-resource.tsx file is a valid resource.
14
+ * Each subdirectory containing a {name}.tsx file is a valid resource.
15
15
  * @returns {string[]} Array of resource names
16
16
  */
17
17
  export function discoverResources() {
@@ -21,10 +21,7 @@ export function discoverResources() {
21
21
  }
22
22
  return readdirSync(resourcesDir, { withFileTypes: true })
23
23
  .filter((entry) => entry.isDirectory())
24
- .filter((entry) => {
25
- const resourceFile = join(resourcesDir, entry.name, `${entry.name}-resource.tsx`);
26
- return existsSync(resourceFile);
27
- })
24
+ .filter((entry) => existsSync(join(resourcesDir, entry.name, `${entry.name}.tsx`)))
28
25
  .map((entry) => entry.name);
29
26
  }
30
27
 
@@ -42,24 +39,3 @@ export function toPascalCase(str) {
42
39
  .join('');
43
40
  }
44
41
 
45
- /**
46
- * Check if a filename is a simulation file for a given resource.
47
- * @param {string} filename
48
- * @param {string} resourceKey
49
- * @returns {boolean}
50
- * @example isSimulationFile('albums-show-simulation.json', 'albums') // true
51
- */
52
- export function isSimulationFile(filename, resourceKey) {
53
- return filename.startsWith(`${resourceKey}-`) && filename.endsWith('-simulation.json');
54
- }
55
-
56
- /**
57
- * Extract the simulation name from a simulation filename.
58
- * @param {string} filename
59
- * @param {string} resourceKey
60
- * @returns {string}
61
- * @example extractSimulationName('albums-show-simulation.json', 'albums') // 'show'
62
- */
63
- export function extractSimulationName(filename, resourceKey) {
64
- return filename.replace(`${resourceKey}-`, '').replace('-simulation.json', '');
65
- }
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const simulator = require("../simulator-DcfQBRXE.cjs");
4
- const chatgpt_index = require("../index-Cngntkp2.cjs");
3
+ const simulator = require("../simulator-B56j5P8W.cjs");
4
+ const chatgpt_index = require("../index-BvQ_ZuOO.cjs");
5
5
  const simulatorUrl = require("../simulator-url-rgg_KYOg.cjs");
6
- const discovery = require("../discovery-CRR3SlyI.cjs");
6
+ const discovery = require("../discovery-DmB8_4QL.cjs");
7
7
  exports.IframeResource = simulator.IframeResource;
8
8
  exports.McpAppHost = simulator.McpAppHost;
9
9
  exports.SCREEN_WIDTHS = simulator.SCREEN_WIDTHS;
@@ -19,11 +19,8 @@ exports.buildSimulations = discovery.buildSimulations;
19
19
  exports.createResourceExports = discovery.createResourceExports;
20
20
  exports.extractResourceKey = discovery.extractResourceKey;
21
21
  exports.extractSimulationKey = discovery.extractSimulationKey;
22
- exports.extractSimulationName = discovery.extractSimulationName;
23
22
  exports.findResourceDirs = discovery.findResourceDirs;
24
23
  exports.findResourceKey = discovery.findResourceKey;
25
- exports.findSimulationFiles = discovery.findSimulationFiles;
26
24
  exports.getComponentName = discovery.getComponentName;
27
- exports.isSimulationFile = discovery.isSimulationFile;
28
25
  exports.toPascalCase = discovery.toPascalCase;
29
26
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -10,5 +10,5 @@ export type { ResourceCSP } from '../simulator/iframe-resource';
10
10
  export * from '../simulator/theme-provider';
11
11
  export { createSimulatorUrl } from '../simulator/simulator-url';
12
12
  export type { SimulatorUrlParams } from '../simulator/simulator-url';
13
- export { buildDevSimulations, buildSimulations, buildResourceMap, createResourceExports, toPascalCase, extractResourceKey, extractSimulationKey, findResourceKey, getComponentName, findResourceDirs, isSimulationFile, extractSimulationName, findSimulationFiles, } from '../lib/discovery';
13
+ export { buildDevSimulations, buildSimulations, buildResourceMap, createResourceExports, toPascalCase, extractResourceKey, extractSimulationKey, findResourceKey, getComponentName, findResourceDirs, } from '../lib/discovery';
14
14
  export type { BuildSimulationsOptions, BuildDevSimulationsOptions, ResourceMetadata, ResourceDirInfo, FsOps, } from '../lib/discovery';
@@ -1,7 +1,7 @@
1
- import { I, M, a, S, T, j, m } from "../simulator-CxrtnguM.js";
2
- import { C } from "../index-Ce_5ZIdJ.js";
1
+ import { I, M, a, S, T, j, m } from "../simulator-C0H_k092.js";
2
+ import { C } from "../index-CTGEqlgk.js";
3
3
  import { c } from "../simulator-url-CuLqtnSS.js";
4
- import { b, a as a2, c as c2, d, e, f, g, h, i, j as j2, k, l, t } from "../discovery-DzV3HLXs.js";
4
+ import { b, a as a2, c as c2, d, e, f, g, h, i, t } from "../discovery-CH80W5l9.js";
5
5
  export {
6
6
  C as ChatGPTSimulator,
7
7
  I as IframeResource,
@@ -17,12 +17,9 @@ export {
17
17
  j as extractResourceCSP,
18
18
  e as extractResourceKey,
19
19
  f as extractSimulationKey,
20
- g as extractSimulationName,
21
- h as findResourceDirs,
22
- i as findResourceKey,
23
- j2 as findSimulationFiles,
24
- k as getComponentName,
25
- l as isSimulationFile,
20
+ g as findResourceDirs,
21
+ h as findResourceKey,
22
+ i as getComponentName,
26
23
  t as toPascalCase,
27
24
  m as useThemeContext
28
25
  };
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const simulator = require("../simulator-DcfQBRXE.cjs");
3
+ const simulator = require("../simulator-B56j5P8W.cjs");
4
4
  exports.ClaudeSimulator = simulator.Simulator;
5
5
  //# sourceMappingURL=index.cjs.map
@@ -1,4 +1,4 @@
1
- import { S } from "../simulator-CxrtnguM.js";
1
+ import { S } from "../simulator-C0H_k092.js";
2
2
  export {
3
3
  S as ClaudeSimulator
4
4
  };