@tpitre/story-ui 4.3.0 → 4.4.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.
- package/README.md +56 -34
- package/dist/cli/index.js +0 -0
- package/dist/mcp-server/index.js +68 -40
- package/dist/mcp-server/routes/mcpRemote.d.ts.map +1 -1
- package/dist/mcp-server/routes/mcpRemote.js +65 -36
- package/dist/templates/StoryUI/StoryUIPanel.css +1440 -0
- package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -1
- package/dist/templates/StoryUI/StoryUIPanel.js +74 -0
- package/package.json +1 -1
- package/templates/StoryUI/StoryUIPanel.tsx +82 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Story UI revolutionizes component documentation by automatically generating Stor
|
|
|
10
10
|
## Why Story UI?
|
|
11
11
|
|
|
12
12
|
- **Framework Agnostic**: Works with React, Vue, Angular, Svelte, and Web Components
|
|
13
|
-
- **Multi-Provider AI**: Choose between Claude
|
|
13
|
+
- **Multi-Provider AI**: Choose between Claude, OpenAI, or Google Gemini - always using the latest models
|
|
14
14
|
- **Design System Aware**: Learns your component library and generates appropriate code
|
|
15
15
|
- **Production Ready**: Deploy as a standalone web app with full MCP integration
|
|
16
16
|
- **Zero Lock-in**: Use any component library - Mantine, Vuetify, Angular Material, Shoelace, or your own
|
|
@@ -68,8 +68,8 @@ Story UI will guide you through:
|
|
|
68
68
|
| Provider | Models | Best For |
|
|
69
69
|
|----------|--------|----------|
|
|
70
70
|
| **Claude** (Anthropic) | claude-opus-4-5, claude-sonnet-4-5, claude-haiku-4-5 | Complex reasoning, code quality |
|
|
71
|
-
| **GPT** (OpenAI) | gpt-4o, gpt-4o-mini
|
|
72
|
-
| **Gemini** (Google) | gemini-2.
|
|
71
|
+
| **GPT** (OpenAI) | gpt-5.2, gpt-5.1, gpt-4o, gpt-4o-mini | Versatility, latest capabilities |
|
|
72
|
+
| **Gemini** (Google) | gemini-2.5-pro, gemini-2.5-flash, gemini-2.0-flash | Fast generation, cost efficiency |
|
|
73
73
|
|
|
74
74
|
### Production Deployment
|
|
75
75
|
- **Railway**: Node.js backend with file-based story persistence
|
|
@@ -121,7 +121,7 @@ The interactive installer will ask:
|
|
|
121
121
|
```
|
|
122
122
|
? Which AI provider do you prefer?
|
|
123
123
|
> Claude (Anthropic) - Recommended
|
|
124
|
-
OpenAI
|
|
124
|
+
OpenAI
|
|
125
125
|
Google Gemini
|
|
126
126
|
|
|
127
127
|
? Enter your API key:
|
|
@@ -262,11 +262,18 @@ The easiest way to connect is via Claude Desktop's built-in connector UI:
|
|
|
262
262
|
2. Go to **Settings** → **Connectors**
|
|
263
263
|
3. Click **"Add custom connector"**
|
|
264
264
|
4. Enter:
|
|
265
|
-
- **Name**: `Story UI` (or any name
|
|
266
|
-
- **URL**:
|
|
265
|
+
- **Name**: `Story UI React` (or any descriptive name)
|
|
266
|
+
- **URL**: Your deployed Railway URL + `/mcp-remote/mcp`
|
|
267
|
+
- Example: `https://your-app-name.up.railway.app/mcp-remote/mcp`
|
|
267
268
|
5. Click **Add**
|
|
268
269
|
6. **Restart Claude Desktop**
|
|
269
270
|
|
|
271
|
+
> **Note**: The URL will be your own Railway deployment URL. See [Production Deployment](#production-deployment) to set up your instance.
|
|
272
|
+
|
|
273
|
+
**Multiple Projects**: If you have multiple Storybook projects, add a separate connector for each:
|
|
274
|
+
- `Story UI React` → `https://my-react-app.up.railway.app/mcp-remote/mcp`
|
|
275
|
+
- `Story UI Vue` → `https://my-vue-app.up.railway.app/mcp-remote/mcp`
|
|
276
|
+
|
|
270
277
|
Once connected, you'll have access to all Story UI tools directly in your Claude conversations:
|
|
271
278
|
- `generate-story` - Generate Storybook stories from natural language
|
|
272
279
|
- `list-components` - Discover available components
|
|
@@ -280,38 +287,52 @@ Once connected, you'll have access to all Story UI tools directly in your Claude
|
|
|
280
287
|
Connect via Claude Code's built-in MCP support:
|
|
281
288
|
|
|
282
289
|
```bash
|
|
283
|
-
# Add
|
|
284
|
-
claude mcp add --transport http story-ui https://
|
|
290
|
+
# Add your production Railway deployment
|
|
291
|
+
claude mcp add --transport http story-ui-react https://your-react-app.up.railway.app/mcp-remote/mcp
|
|
292
|
+
|
|
293
|
+
# Add another project (if needed)
|
|
294
|
+
claude mcp add --transport http story-ui-vue https://your-vue-app.up.railway.app/mcp-remote/mcp
|
|
285
295
|
|
|
286
|
-
#
|
|
287
|
-
claude mcp add --transport http story-ui-local http://localhost:
|
|
296
|
+
# For local development (default port is 4001)
|
|
297
|
+
claude mcp add --transport http story-ui-local http://localhost:4001/mcp-remote/mcp
|
|
288
298
|
```
|
|
289
299
|
|
|
290
300
|
### Manual Configuration (Advanced)
|
|
291
301
|
|
|
292
|
-
For
|
|
302
|
+
For running multiple local Story UI instances with different ports, configure your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
293
303
|
|
|
294
304
|
```json
|
|
295
305
|
{
|
|
296
306
|
"mcpServers": {
|
|
297
|
-
"story-ui": {
|
|
307
|
+
"story-ui-react": {
|
|
308
|
+
"command": "npx",
|
|
309
|
+
"args": ["@tpitre/story-ui", "start", "--port", "4001"]
|
|
310
|
+
},
|
|
311
|
+
"story-ui-vue": {
|
|
312
|
+
"command": "npx",
|
|
313
|
+
"args": ["@tpitre/story-ui", "start", "--port", "4002"]
|
|
314
|
+
},
|
|
315
|
+
"story-ui-angular": {
|
|
298
316
|
"command": "npx",
|
|
299
|
-
"args": ["@tpitre/story-ui", "
|
|
300
|
-
"env": {
|
|
301
|
-
"ANTHROPIC_API_KEY": "your-api-key"
|
|
302
|
-
}
|
|
317
|
+
"args": ["@tpitre/story-ui", "start", "--port", "4003"]
|
|
303
318
|
}
|
|
304
319
|
}
|
|
305
320
|
}
|
|
306
321
|
```
|
|
307
322
|
|
|
323
|
+
> **Note**: When using Claude Desktop, API keys are managed through your Anthropic account - no need to configure them in the MCP server.
|
|
324
|
+
|
|
308
325
|
### Starting the Local MCP Server
|
|
309
326
|
|
|
310
327
|
```bash
|
|
328
|
+
# Start with default port (4001)
|
|
311
329
|
npx story-ui start
|
|
330
|
+
|
|
331
|
+
# Or specify a custom port
|
|
332
|
+
npx story-ui start --port 4002
|
|
312
333
|
```
|
|
313
334
|
|
|
314
|
-
This starts the Story UI HTTP server with MCP endpoint at `http://localhost
|
|
335
|
+
This starts the Story UI HTTP server with MCP endpoint at `http://localhost:<port>/mcp-remote/mcp`.
|
|
315
336
|
|
|
316
337
|
### Available MCP Commands
|
|
317
338
|
|
|
@@ -325,13 +346,13 @@ Once connected, you can use these commands in Claude Desktop:
|
|
|
325
346
|
|
|
326
347
|
## Production Deployment
|
|
327
348
|
|
|
328
|
-
Story UI can be deployed as a standalone web application accessible from anywhere.
|
|
349
|
+
Story UI can be deployed as a standalone web application accessible from anywhere. We recommend Railway for its ease of use, but any Node.js hosting platform will work.
|
|
329
350
|
|
|
330
351
|
### Architecture
|
|
331
352
|
|
|
332
353
|
```
|
|
333
354
|
┌─────────────────────────────────────────────────────────────┐
|
|
334
|
-
│
|
|
355
|
+
│ Your Deployment (e.g., Railway) │
|
|
335
356
|
│ ┌─────────────────────────────────────────────────────────┐│
|
|
336
357
|
│ │ Express MCP Server (Node.js) ││
|
|
337
358
|
│ │ - Serves Storybook with Story UI addon ││
|
|
@@ -342,9 +363,9 @@ Story UI can be deployed as a standalone web application accessible from anywher
|
|
|
342
363
|
└──────────────────────────────────────────────────────────────┘
|
|
343
364
|
```
|
|
344
365
|
|
|
345
|
-
### Deploy to Railway
|
|
366
|
+
### Deploy to Railway (Recommended)
|
|
346
367
|
|
|
347
|
-
Railway provides a straightforward deployment experience with file-based story persistence.
|
|
368
|
+
Railway provides a straightforward deployment experience with automatic HTTPS and file-based story persistence.
|
|
348
369
|
|
|
349
370
|
**Quick Start:**
|
|
350
371
|
|
|
@@ -353,23 +374,27 @@ Railway provides a straightforward deployment experience with file-based story p
|
|
|
353
374
|
npm install -g @railway/cli
|
|
354
375
|
railway login
|
|
355
376
|
|
|
356
|
-
# Initialize and deploy
|
|
377
|
+
# Initialize and deploy from your Storybook project
|
|
357
378
|
railway init
|
|
358
379
|
railway up
|
|
359
380
|
```
|
|
360
381
|
|
|
361
382
|
**Environment Variables (set in Railway Dashboard):**
|
|
362
383
|
- `ANTHROPIC_API_KEY` - Required for Claude models
|
|
363
|
-
- `OPENAI_API_KEY` - Optional for OpenAI models
|
|
364
|
-
- `GEMINI_API_KEY` - Optional for Gemini models
|
|
384
|
+
- `OPENAI_API_KEY` - Optional, for OpenAI models
|
|
385
|
+
- `GEMINI_API_KEY` - Optional, for Gemini models
|
|
386
|
+
|
|
387
|
+
**After Deployment:**
|
|
365
388
|
|
|
366
|
-
|
|
389
|
+
Your Railway app will have a URL like `https://your-app-name.up.railway.app`. Use this URL to connect MCP clients:
|
|
367
390
|
|
|
368
391
|
```bash
|
|
369
|
-
#
|
|
370
|
-
claude mcp add --transport http story-ui https://your-app.up.railway.app/mcp-remote/mcp
|
|
392
|
+
# In Claude Code
|
|
393
|
+
claude mcp add --transport http story-ui https://your-app-name.up.railway.app/mcp-remote/mcp
|
|
371
394
|
```
|
|
372
395
|
|
|
396
|
+
Or add it to Claude Desktop via **Settings** → **Connectors** → **Add custom connector**.
|
|
397
|
+
|
|
373
398
|
See [DEPLOYMENT.md](DEPLOYMENT.md) for detailed deployment instructions and troubleshooting.
|
|
374
399
|
|
|
375
400
|
---
|
|
@@ -423,17 +448,14 @@ For simpler setups, use `story-ui-considerations.md`:
|
|
|
423
448
|
## CLI Reference
|
|
424
449
|
|
|
425
450
|
```bash
|
|
426
|
-
# Initialize Story UI
|
|
451
|
+
# Initialize Story UI in your project
|
|
427
452
|
npx story-ui init
|
|
428
453
|
|
|
429
|
-
# Start the
|
|
454
|
+
# Start the MCP server (default port: 4001)
|
|
430
455
|
npx story-ui start
|
|
431
|
-
npx story-ui start --port
|
|
432
|
-
|
|
433
|
-
# Deploy to production
|
|
434
|
-
npx story-ui deploy
|
|
456
|
+
npx story-ui start --port 4002 # Custom port
|
|
435
457
|
|
|
436
|
-
# Run MCP server
|
|
458
|
+
# Run MCP STDIO server (for Claude Desktop local integration)
|
|
437
459
|
npx story-ui mcp
|
|
438
460
|
```
|
|
439
461
|
|
package/dist/cli/index.js
CHANGED
|
File without changes
|
package/dist/mcp-server/index.js
CHANGED
|
@@ -22,6 +22,26 @@ import { UrlRedirectService } from '../story-generator/urlRedirectService.js';
|
|
|
22
22
|
import { getProviders, getModels, configureProviderRoute, validateApiKey, setDefaultProvider, setModel, getUISettings, applyUISettings, getSettingsConfig } from './routes/providers.js';
|
|
23
23
|
import { listFrameworks, detectCurrentFramework, getFrameworkDetails, validateStoryForFramework, postProcessStoryForFramework, } from './routes/frameworks.js';
|
|
24
24
|
import mcpRemoteRouter from './routes/mcpRemote.js';
|
|
25
|
+
import { getAdapterRegistry } from '../story-generator/framework-adapters/index.js';
|
|
26
|
+
// Supported story file extensions for all frameworks
|
|
27
|
+
const STORY_EXTENSIONS = ['.stories.tsx', '.stories.ts', '.stories.svelte', '.stories.js'];
|
|
28
|
+
/**
|
|
29
|
+
* Check if a file is a story file (supports all framework extensions)
|
|
30
|
+
*/
|
|
31
|
+
function isStoryFile(filename) {
|
|
32
|
+
return STORY_EXTENSIONS.some(ext => filename.endsWith(ext));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Remove story extension from filename to get base name
|
|
36
|
+
*/
|
|
37
|
+
function removeStoryExtension(filename) {
|
|
38
|
+
for (const ext of STORY_EXTENSIONS) {
|
|
39
|
+
if (filename.endsWith(ext)) {
|
|
40
|
+
return filename.replace(ext, '');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return filename;
|
|
44
|
+
}
|
|
25
45
|
const app = express();
|
|
26
46
|
// CORS configuration
|
|
27
47
|
// - Allow all origins for /story-ui/* routes (public API for production Storybooks)
|
|
@@ -88,19 +108,19 @@ app.get('/mcp/stories', async (req, res) => {
|
|
|
88
108
|
}
|
|
89
109
|
const files = fs.readdirSync(storiesPath);
|
|
90
110
|
const stories = files
|
|
91
|
-
.filter(file =>
|
|
111
|
+
.filter(file => isStoryFile(file))
|
|
92
112
|
.map(file => {
|
|
93
113
|
const filePath = path.join(storiesPath, file);
|
|
94
114
|
const stats = fs.statSync(filePath);
|
|
95
115
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
96
116
|
// Extract title from story file
|
|
97
117
|
const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
|
|
98
|
-
let title = titleMatch ? titleMatch[1].replace('Generated/', '') : file
|
|
118
|
+
let title = titleMatch ? titleMatch[1].replace('Generated/', '') : removeStoryExtension(file);
|
|
99
119
|
// Remove hash suffix from display title
|
|
100
120
|
title = title.replace(/\s*\([a-f0-9]{8}\)$/i, '');
|
|
101
121
|
return {
|
|
102
|
-
id: file
|
|
103
|
-
storyId: file
|
|
122
|
+
id: removeStoryExtension(file),
|
|
123
|
+
storyId: removeStoryExtension(file),
|
|
104
124
|
fileName: file,
|
|
105
125
|
title,
|
|
106
126
|
lastUpdated: stats.mtime.getTime(),
|
|
@@ -326,18 +346,18 @@ app.get('/story-ui/stories', async (req, res) => {
|
|
|
326
346
|
}
|
|
327
347
|
const files = fs.readdirSync(storiesPath);
|
|
328
348
|
const stories = files
|
|
329
|
-
.filter(file => file
|
|
349
|
+
.filter(file => isStoryFile(file))
|
|
330
350
|
.map(file => {
|
|
331
351
|
const filePath = path.join(storiesPath, file);
|
|
332
352
|
const stats = fs.statSync(filePath);
|
|
333
353
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
334
354
|
// Extract title from story file
|
|
335
355
|
const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
|
|
336
|
-
let title = titleMatch ? titleMatch[1].replace('Generated/', '') : file
|
|
356
|
+
let title = titleMatch ? titleMatch[1].replace('Generated/', '') : removeStoryExtension(file);
|
|
337
357
|
// Remove hash suffix like " (a1b2c3d4)" from display title - hash is for Storybook uniqueness only
|
|
338
358
|
title = title.replace(/\s*\([a-f0-9]{8}\)$/i, '');
|
|
339
359
|
return {
|
|
340
|
-
id: file
|
|
360
|
+
id: removeStoryExtension(file),
|
|
341
361
|
fileName: file,
|
|
342
362
|
title,
|
|
343
363
|
lastUpdated: stats.mtime.getTime(),
|
|
@@ -365,7 +385,12 @@ app.post('/story-ui/stories', async (req, res) => {
|
|
|
365
385
|
if (!fs.existsSync(storiesPath)) {
|
|
366
386
|
fs.mkdirSync(storiesPath, { recursive: true });
|
|
367
387
|
}
|
|
368
|
-
|
|
388
|
+
// Get the correct file extension from the framework adapter
|
|
389
|
+
const registry = getAdapterRegistry();
|
|
390
|
+
const frameworkType = (config.componentFramework || 'react');
|
|
391
|
+
const adapter = registry.getAdapter(frameworkType);
|
|
392
|
+
const extension = adapter?.defaultExtension || '.stories.tsx';
|
|
393
|
+
const fileName = `${id}${extension}`;
|
|
369
394
|
const filePath = path.join(storiesPath, fileName);
|
|
370
395
|
fs.writeFileSync(filePath, code, 'utf-8');
|
|
371
396
|
console.log(`✅ Saved story: ${filePath}`);
|
|
@@ -388,10 +413,15 @@ app.delete('/story-ui/stories/:id', async (req, res) => {
|
|
|
388
413
|
const { id } = req.params;
|
|
389
414
|
const storiesPath = config.generatedStoriesPath;
|
|
390
415
|
// Try exact match first (fileName format)
|
|
391
|
-
// Handle
|
|
416
|
+
// Handle all story file extensions
|
|
392
417
|
let fileName = id;
|
|
393
|
-
if (!
|
|
394
|
-
|
|
418
|
+
if (!isStoryFile(id)) {
|
|
419
|
+
// Get the correct file extension from the framework adapter
|
|
420
|
+
const registry = getAdapterRegistry();
|
|
421
|
+
const frameworkType = (config.componentFramework || 'react');
|
|
422
|
+
const adapter = registry.getAdapter(frameworkType);
|
|
423
|
+
const extension = adapter?.defaultExtension || '.stories.tsx';
|
|
424
|
+
fileName = `${id}${extension}`;
|
|
395
425
|
}
|
|
396
426
|
const filePath = path.join(storiesPath, fileName);
|
|
397
427
|
if (fs.existsSync(filePath)) {
|
|
@@ -433,7 +463,7 @@ app.post('/story-ui/delete', async (req, res) => {
|
|
|
433
463
|
console.log(`🔍 Searching for story in: ${storiesPath}`);
|
|
434
464
|
if (fs.existsSync(storiesPath)) {
|
|
435
465
|
const files = fs.readdirSync(storiesPath);
|
|
436
|
-
const matchingFile = files.find(file => file.includes(id) || file
|
|
466
|
+
const matchingFile = files.find(file => file.includes(id) || removeStoryExtension(file) === id);
|
|
437
467
|
if (matchingFile) {
|
|
438
468
|
const filePath = path.join(storiesPath, matchingFile);
|
|
439
469
|
fs.unlinkSync(filePath);
|
|
@@ -467,9 +497,15 @@ app.post('/story-ui/stories/delete-bulk', async (req, res) => {
|
|
|
467
497
|
const deleted = [];
|
|
468
498
|
const notFound = [];
|
|
469
499
|
const errors = [];
|
|
500
|
+
// Get the correct file extension from the framework adapter
|
|
501
|
+
const registry = getAdapterRegistry();
|
|
502
|
+
const frameworkType = (config.componentFramework || 'react');
|
|
503
|
+
const adapter = registry.getAdapter(frameworkType);
|
|
504
|
+
const extension = adapter?.defaultExtension || '.stories.tsx';
|
|
470
505
|
for (const id of ids) {
|
|
471
506
|
try {
|
|
472
|
-
|
|
507
|
+
// Check if id already has a story extension
|
|
508
|
+
const fileName = isStoryFile(id) ? id : `${id}${extension}`;
|
|
473
509
|
const filePath = path.join(storiesPath, fileName);
|
|
474
510
|
if (fs.existsSync(filePath)) {
|
|
475
511
|
fs.unlinkSync(filePath);
|
|
@@ -477,7 +513,17 @@ app.post('/story-ui/stories/delete-bulk', async (req, res) => {
|
|
|
477
513
|
console.log(`✅ Deleted: ${fileName}`);
|
|
478
514
|
}
|
|
479
515
|
else {
|
|
480
|
-
|
|
516
|
+
// Try to find a matching file with any story extension
|
|
517
|
+
const files = fs.readdirSync(storiesPath);
|
|
518
|
+
const matchingFile = files.find(f => isStoryFile(f) && (f === id || removeStoryExtension(f) === id));
|
|
519
|
+
if (matchingFile) {
|
|
520
|
+
fs.unlinkSync(path.join(storiesPath, matchingFile));
|
|
521
|
+
deleted.push(id);
|
|
522
|
+
console.log(`✅ Deleted: ${matchingFile}`);
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
notFound.push(id);
|
|
526
|
+
}
|
|
481
527
|
}
|
|
482
528
|
}
|
|
483
529
|
catch (err) {
|
|
@@ -543,10 +589,8 @@ app.delete('/story-ui/stories', async (req, res) => {
|
|
|
543
589
|
return res.json({ success: true, deleted: 0, message: 'No stories directory found' });
|
|
544
590
|
}
|
|
545
591
|
const files = fs.readdirSync(storiesPath);
|
|
546
|
-
// Support all story file extensions
|
|
547
|
-
const storyFiles = files.filter(file => file
|
|
548
|
-
file.endsWith('.stories.ts') ||
|
|
549
|
-
file.endsWith('.stories.svelte'));
|
|
592
|
+
// Support all story file extensions
|
|
593
|
+
const storyFiles = files.filter(file => isStoryFile(file));
|
|
550
594
|
let deleted = 0;
|
|
551
595
|
for (const file of storyFiles) {
|
|
552
596
|
try {
|
|
@@ -582,24 +626,16 @@ app.post('/story-ui/orphan-stories', async (req, res) => {
|
|
|
582
626
|
return res.json({ orphans: [], count: 0 });
|
|
583
627
|
}
|
|
584
628
|
const files = fs.readdirSync(storiesPath);
|
|
585
|
-
const storyFiles = files.filter(file => file
|
|
586
|
-
file.endsWith('.stories.ts') ||
|
|
587
|
-
file.endsWith('.stories.svelte'));
|
|
629
|
+
const storyFiles = files.filter(file => isStoryFile(file));
|
|
588
630
|
// Find orphans: stories that don't match any chat fileName
|
|
589
631
|
const orphans = storyFiles.filter(storyFile => {
|
|
590
632
|
// Extract the base name without extension for comparison
|
|
591
|
-
const storyBase = storyFile
|
|
592
|
-
.replace('.stories.tsx', '')
|
|
593
|
-
.replace('.stories.ts', '')
|
|
594
|
-
.replace('.stories.svelte', '');
|
|
633
|
+
const storyBase = removeStoryExtension(storyFile);
|
|
595
634
|
// Check if any chat fileName matches this story
|
|
596
635
|
return !chatFileNames.some(chatFileName => {
|
|
597
636
|
if (!chatFileName)
|
|
598
637
|
return false;
|
|
599
|
-
const chatBase = chatFileName
|
|
600
|
-
.replace('.stories.tsx', '')
|
|
601
|
-
.replace('.stories.ts', '')
|
|
602
|
-
.replace('.stories.svelte', '');
|
|
638
|
+
const chatBase = removeStoryExtension(chatFileName);
|
|
603
639
|
return storyBase === chatBase || storyFile === chatFileName;
|
|
604
640
|
});
|
|
605
641
|
});
|
|
@@ -642,22 +678,14 @@ app.delete('/story-ui/orphan-stories', async (req, res) => {
|
|
|
642
678
|
return res.json({ deleted: [], count: 0 });
|
|
643
679
|
}
|
|
644
680
|
const files = fs.readdirSync(storiesPath);
|
|
645
|
-
const storyFiles = files.filter(file => file
|
|
646
|
-
file.endsWith('.stories.ts') ||
|
|
647
|
-
file.endsWith('.stories.svelte'));
|
|
681
|
+
const storyFiles = files.filter(file => isStoryFile(file));
|
|
648
682
|
// Find orphans: stories that don't match any chat fileName
|
|
649
683
|
const orphans = storyFiles.filter(storyFile => {
|
|
650
|
-
const storyBase = storyFile
|
|
651
|
-
.replace('.stories.tsx', '')
|
|
652
|
-
.replace('.stories.ts', '')
|
|
653
|
-
.replace('.stories.svelte', '');
|
|
684
|
+
const storyBase = removeStoryExtension(storyFile);
|
|
654
685
|
return !chatFileNames.some(chatFileName => {
|
|
655
686
|
if (!chatFileName)
|
|
656
687
|
return false;
|
|
657
|
-
const chatBase = chatFileName
|
|
658
|
-
.replace('.stories.tsx', '')
|
|
659
|
-
.replace('.stories.ts', '')
|
|
660
|
-
.replace('.stories.svelte', '');
|
|
688
|
+
const chatBase = removeStoryExtension(chatFileName);
|
|
661
689
|
return storyBase === chatBase || storyFile === chatFileName;
|
|
662
690
|
});
|
|
663
691
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcpRemote.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/mcpRemote.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;
|
|
1
|
+
{"version":3,"file":"mcpRemote.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/mcpRemote.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAiGH,eAAO,MAAM,MAAM,4CAAW,CAAC;AAwlB/B,eAAe,MAAM,CAAC"}
|
|
@@ -38,6 +38,63 @@ const HTTP_PORT = process.env.VITE_STORY_UI_PORT || process.env.STORY_UI_HTTP_PO
|
|
|
38
38
|
const HTTP_BASE_URL = `http://localhost:${HTTP_PORT}`;
|
|
39
39
|
// Load configuration
|
|
40
40
|
const config = loadUserConfig();
|
|
41
|
+
// Get all supported story file extensions based on framework adapters
|
|
42
|
+
// This ensures MCP remote works with all frameworks (React, Vue, Angular, Svelte, Web Components)
|
|
43
|
+
const STORY_EXTENSIONS = ['.stories.tsx', '.stories.ts', '.stories.svelte', '.stories.js'];
|
|
44
|
+
/**
|
|
45
|
+
* Check if a file is a story file (supports all framework extensions)
|
|
46
|
+
*/
|
|
47
|
+
function isStoryFile(filename) {
|
|
48
|
+
return STORY_EXTENSIONS.some(ext => filename.endsWith(ext));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Get the story ID from a filename (works with all extensions)
|
|
52
|
+
*/
|
|
53
|
+
function getStoryIdFromFile(filename) {
|
|
54
|
+
// Extract hash from filename like "Button-a1b2c3d4.stories.tsx" or "Card-12345678.stories.svelte"
|
|
55
|
+
const hashMatch = filename.match(/-([a-f0-9]{8})\.stories\.(tsx?|svelte|js)$/);
|
|
56
|
+
if (hashMatch) {
|
|
57
|
+
return `story-${hashMatch[1]}`;
|
|
58
|
+
}
|
|
59
|
+
// Fall back to filename without extension
|
|
60
|
+
for (const ext of STORY_EXTENSIONS) {
|
|
61
|
+
if (filename.endsWith(ext)) {
|
|
62
|
+
return filename.replace(ext, '');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return filename;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get the base name from a filename (without extension)
|
|
69
|
+
*/
|
|
70
|
+
function getBaseName(filename) {
|
|
71
|
+
for (const ext of STORY_EXTENSIONS) {
|
|
72
|
+
if (filename.endsWith(ext)) {
|
|
73
|
+
return filename.replace(ext, '');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return filename;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if a file matches a story ID (supports all extensions)
|
|
80
|
+
*/
|
|
81
|
+
function fileMatchesStoryId(filename, storyId) {
|
|
82
|
+
// Extract hash from storyId like "story-a1b2c3d4"
|
|
83
|
+
const hashMatch = storyId.match(/^story-([a-f0-9]{8})$/);
|
|
84
|
+
const hash = hashMatch ? hashMatch[1] : null;
|
|
85
|
+
if (hash) {
|
|
86
|
+
// Check if filename contains the hash
|
|
87
|
+
return filename.includes(`-${hash}.stories.`);
|
|
88
|
+
}
|
|
89
|
+
// Check if filename matches storyId with any extension
|
|
90
|
+
if (filename === storyId)
|
|
91
|
+
return true;
|
|
92
|
+
for (const ext of STORY_EXTENSIONS) {
|
|
93
|
+
if (filename === `${storyId}${ext}`)
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
41
98
|
export const router = Router();
|
|
42
99
|
// Store SSE transports for legacy session management
|
|
43
100
|
const sseTransports = {};
|
|
@@ -206,11 +263,10 @@ async function handleToolCall(name, args) {
|
|
|
206
263
|
if (config.generatedStoriesPath && fs.existsSync(config.generatedStoriesPath)) {
|
|
207
264
|
const files = fs.readdirSync(config.generatedStoriesPath);
|
|
208
265
|
fileStories = files
|
|
209
|
-
.filter(file => file
|
|
266
|
+
.filter(file => isStoryFile(file))
|
|
210
267
|
.map(file => {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
let title = file.replace('.stories.tsx', '').replace(/-/g, ' ');
|
|
268
|
+
const storyId = getStoryIdFromFile(file);
|
|
269
|
+
let title = getBaseName(file).replace(/-/g, ' ');
|
|
214
270
|
try {
|
|
215
271
|
const filePath = path.join(config.generatedStoriesPath, file);
|
|
216
272
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
@@ -243,17 +299,7 @@ async function handleToolCall(name, args) {
|
|
|
243
299
|
const { storyId } = args;
|
|
244
300
|
if (config.generatedStoriesPath && fs.existsSync(config.generatedStoriesPath)) {
|
|
245
301
|
const files = fs.readdirSync(config.generatedStoriesPath);
|
|
246
|
-
const
|
|
247
|
-
const hash = hashMatch ? hashMatch[1] : null;
|
|
248
|
-
const matchingFile = files.find(file => {
|
|
249
|
-
if (hash && file.includes(`-${hash}.stories.tsx`))
|
|
250
|
-
return true;
|
|
251
|
-
if (file === `${storyId}.stories.tsx`)
|
|
252
|
-
return true;
|
|
253
|
-
if (file === storyId)
|
|
254
|
-
return true;
|
|
255
|
-
return false;
|
|
256
|
-
});
|
|
302
|
+
const matchingFile = files.find(file => fileMatchesStoryId(file, storyId));
|
|
257
303
|
if (matchingFile) {
|
|
258
304
|
const filePath = path.join(config.generatedStoriesPath, matchingFile);
|
|
259
305
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
@@ -273,17 +319,7 @@ async function handleToolCall(name, args) {
|
|
|
273
319
|
const { storyId } = args;
|
|
274
320
|
if (config.generatedStoriesPath && fs.existsSync(config.generatedStoriesPath)) {
|
|
275
321
|
const files = fs.readdirSync(config.generatedStoriesPath);
|
|
276
|
-
const
|
|
277
|
-
const hash = hashMatch ? hashMatch[1] : null;
|
|
278
|
-
const matchingFile = files.find(file => {
|
|
279
|
-
if (hash && file.includes(`-${hash}.stories.tsx`))
|
|
280
|
-
return true;
|
|
281
|
-
if (file === `${storyId}.stories.tsx`)
|
|
282
|
-
return true;
|
|
283
|
-
if (file === storyId)
|
|
284
|
-
return true;
|
|
285
|
-
return false;
|
|
286
|
-
});
|
|
322
|
+
const matchingFile = files.find(file => fileMatchesStoryId(file, storyId));
|
|
287
323
|
if (matchingFile) {
|
|
288
324
|
const filePath = path.join(config.generatedStoriesPath, matchingFile);
|
|
289
325
|
fs.unlinkSync(filePath);
|
|
@@ -325,15 +361,14 @@ async function handleToolCall(name, args) {
|
|
|
325
361
|
// Find the story to update if no ID provided
|
|
326
362
|
if (!storyId && config.generatedStoriesPath && fs.existsSync(config.generatedStoriesPath)) {
|
|
327
363
|
const files = fs.readdirSync(config.generatedStoriesPath)
|
|
328
|
-
.filter(f => f
|
|
364
|
+
.filter(f => isStoryFile(f))
|
|
329
365
|
.sort((a, b) => {
|
|
330
366
|
const statA = fs.statSync(path.join(config.generatedStoriesPath, a));
|
|
331
367
|
const statB = fs.statSync(path.join(config.generatedStoriesPath, b));
|
|
332
368
|
return statB.mtime.getTime() - statA.mtime.getTime();
|
|
333
369
|
});
|
|
334
370
|
if (files.length > 0) {
|
|
335
|
-
|
|
336
|
-
storyId = hash ? `story-${hash}` : files[0].replace('.stories.tsx', '');
|
|
371
|
+
storyId = getStoryIdFromFile(files[0]);
|
|
337
372
|
}
|
|
338
373
|
}
|
|
339
374
|
if (!storyId) {
|
|
@@ -350,13 +385,7 @@ async function handleToolCall(name, args) {
|
|
|
350
385
|
let storyMetadata = {};
|
|
351
386
|
if (config.generatedStoriesPath && fs.existsSync(config.generatedStoriesPath)) {
|
|
352
387
|
const files = fs.readdirSync(config.generatedStoriesPath);
|
|
353
|
-
const
|
|
354
|
-
const hash = hashMatch ? hashMatch[1] : null;
|
|
355
|
-
const matchingFile = files.find(file => {
|
|
356
|
-
if (hash && file.includes(`-${hash}.stories.tsx`))
|
|
357
|
-
return true;
|
|
358
|
-
return false;
|
|
359
|
-
});
|
|
388
|
+
const matchingFile = files.find(file => fileMatchesStoryId(file, storyId));
|
|
360
389
|
if (matchingFile) {
|
|
361
390
|
const filePath = path.join(config.generatedStoriesPath, matchingFile);
|
|
362
391
|
existingCode = fs.readFileSync(filePath, 'utf-8');
|