@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 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 (Anthropic), GPT-4o (OpenAI), or Gemini (Google)
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, o1 | Versatility, speed |
72
- | **Gemini** (Google) | gemini-2.0-flash, gemini-1.5-pro | Fast generation, cost efficiency |
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 (GPT-4o)
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 you prefer)
266
- - **URL**: `https://story-ui-demo.up.railway.app/mcp-remote/mcp` (production)
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 remote HTTP MCP server (production)
284
- claude mcp add --transport http story-ui https://story-ui-demo.up.railway.app/mcp-remote/mcp
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
- # Or for local development
287
- claude mcp add --transport http story-ui-local http://localhost:4005/mcp-remote/mcp
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 advanced users who prefer manual configuration, add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
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", "mcp"],
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:4005/mcp-remote/mcp`.
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
- Railway Deployment
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
- **Connect MCP Clients:**
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
- # Add your Railway deployment as an MCP server
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 development server
454
+ # Start the MCP server (default port: 4001)
430
455
  npx story-ui start
431
- npx story-ui start --port 4005
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
@@ -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 => file.endsWith('.stories.tsx') || file.endsWith('.stories.ts') || file.endsWith('.stories.svelte'))
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.replace(/\.stories\.(tsx|ts|svelte)$/, '');
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.replace(/\.stories\.(tsx|ts|svelte)$/, ''),
103
- storyId: file.replace(/\.stories\.(tsx|ts|svelte)$/, ''),
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.endsWith('.stories.tsx'))
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.replace('.stories.tsx', '');
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.replace('.stories.tsx', ''),
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
- const fileName = `${id}.stories.tsx`;
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 both .tsx and .svelte extensions
416
+ // Handle all story file extensions
392
417
  let fileName = id;
393
- if (!id.endsWith('.stories.tsx') && !id.endsWith('.stories.ts') && !id.endsWith('.stories.svelte')) {
394
- fileName = `${id}.stories.tsx`;
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.replace('.stories.tsx', '') === id);
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
- const fileName = id.endsWith('.stories.tsx') ? id : `${id}.stories.tsx`;
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
- notFound.push(id);
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: .tsx, .ts, .svelte
547
- const storyFiles = files.filter(file => file.endsWith('.stories.tsx') ||
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.endsWith('.stories.tsx') ||
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.endsWith('.stories.tsx') ||
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;AAiCH,eAAO,MAAM,MAAM,4CAAW,CAAC;AA6mB/B,eAAe,MAAM,CAAC"}
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.endsWith('.stories.tsx'))
266
+ .filter(file => isStoryFile(file))
210
267
  .map(file => {
211
- const hash = file.match(/-([a-f0-9]{8})\.stories\.tsx$/)?.[1] || '';
212
- const storyId = hash ? `story-${hash}` : file.replace('.stories.tsx', '');
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 hashMatch = storyId.match(/^story-([a-f0-9]{8})$/);
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 hashMatch = storyId.match(/^story-([a-f0-9]{8})$/);
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.endsWith('.stories.tsx'))
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
- const hash = files[0].match(/-([a-f0-9]{8})\.stories\.tsx$/)?.[1];
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 hashMatch = storyId.match(/^story-([a-f0-9]{8})$/);
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');