@tpitre/story-ui 4.4.0 → 4.4.2

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
@@ -67,9 +67,9 @@ Story UI will guide you through:
67
67
 
68
68
  | Provider | Models | Best For |
69
69
  |----------|--------|----------|
70
- | **Claude** (Anthropic) | claude-opus-4-5, claude-sonnet-4-5, claude-haiku-4-5 | Complex reasoning, code quality |
70
+ | **Claude** (Anthropic) | claude-opus-4-5-20251101, claude-sonnet-4-5-20250929, claude-haiku-4-5-20251001, claude-sonnet-4-20250514 | Complex reasoning, code quality |
71
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 |
72
+ | **Gemini** (Google) | gemini-3-pro-preview, gemini-2.5-pro, gemini-2.5-flash, gemini-2.0-flash | Advanced reasoning, fast generation |
73
73
 
74
74
  ### Production Deployment
75
75
  - **Railway**: Node.js backend with file-based story persistence
@@ -467,17 +467,20 @@ npx story-ui mcp
467
467
 
468
468
  | Method | Endpoint | Description |
469
469
  |--------|----------|-------------|
470
+ | `POST` | `/story-ui/generate` | Generate story (specify provider in body) |
471
+ | `POST` | `/story-ui/generate-stream` | Generate story with streaming |
470
472
  | `GET` | `/story-ui/providers` | List available LLM providers and models |
471
- | `POST` | `/story-ui/claude` | Generate with Claude |
472
- | `POST` | `/story-ui/openai` | Generate with OpenAI |
473
- | `POST` | `/story-ui/gemini` | Generate with Gemini |
473
+ | `GET` | `/story-ui/components` | List discovered components |
474
474
  | `GET` | `/story-ui/considerations` | Get design system context |
475
+ | `GET` | `/mcp/stories` | List generated stories |
476
+ | `DELETE` | `/mcp/stories/:storyId` | Delete a story |
475
477
 
476
478
  ### Request Format
477
479
 
478
480
  ```typescript
479
481
  {
480
482
  prompt: string; // User's request
483
+ provider?: string; // LLM provider: 'claude' | 'openai' | 'gemini'
481
484
  model?: string; // Specific model to use
482
485
  previousCode?: string; // For iterations
483
486
  history?: Message[]; // Conversation history
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(),
@@ -248,7 +268,8 @@ app.delete('/mcp/stories/:storyId', async (req, res) => {
248
268
  return res.status(500).json({ error: 'Failed to delete story' });
249
269
  }
250
270
  });
251
- // File-based story routes - stories are generated as .stories.tsx files
271
+ // File-based story routes - stories are generated as framework-specific files
272
+ // (.stories.tsx for React, .stories.ts for Vue/Angular, .stories.svelte for Svelte, .stories.js for Web Components)
252
273
  // Storybook discovers these automatically via its native file system watching
253
274
  // Proxy routes for frontend compatibility (maps /story-ui/ to /mcp/)
254
275
  app.post('/story-ui/generate', generateStoryFromPrompt);
@@ -326,18 +347,18 @@ app.get('/story-ui/stories', async (req, res) => {
326
347
  }
327
348
  const files = fs.readdirSync(storiesPath);
328
349
  const stories = files
329
- .filter(file => file.endsWith('.stories.tsx'))
350
+ .filter(file => isStoryFile(file))
330
351
  .map(file => {
331
352
  const filePath = path.join(storiesPath, file);
332
353
  const stats = fs.statSync(filePath);
333
354
  const content = fs.readFileSync(filePath, 'utf-8');
334
355
  // Extract title from story file
335
356
  const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
336
- let title = titleMatch ? titleMatch[1].replace('Generated/', '') : file.replace('.stories.tsx', '');
357
+ let title = titleMatch ? titleMatch[1].replace('Generated/', '') : removeStoryExtension(file);
337
358
  // Remove hash suffix like " (a1b2c3d4)" from display title - hash is for Storybook uniqueness only
338
359
  title = title.replace(/\s*\([a-f0-9]{8}\)$/i, '');
339
360
  return {
340
- id: file.replace('.stories.tsx', ''),
361
+ id: removeStoryExtension(file),
341
362
  fileName: file,
342
363
  title,
343
364
  lastUpdated: stats.mtime.getTime(),
@@ -365,7 +386,12 @@ app.post('/story-ui/stories', async (req, res) => {
365
386
  if (!fs.existsSync(storiesPath)) {
366
387
  fs.mkdirSync(storiesPath, { recursive: true });
367
388
  }
368
- const fileName = `${id}.stories.tsx`;
389
+ // Get the correct file extension from the framework adapter
390
+ const registry = getAdapterRegistry();
391
+ const frameworkType = (config.componentFramework || 'react');
392
+ const adapter = registry.getAdapter(frameworkType);
393
+ const extension = adapter?.defaultExtension || '.stories.tsx';
394
+ const fileName = `${id}${extension}`;
369
395
  const filePath = path.join(storiesPath, fileName);
370
396
  fs.writeFileSync(filePath, code, 'utf-8');
371
397
  console.log(`✅ Saved story: ${filePath}`);
@@ -388,10 +414,15 @@ app.delete('/story-ui/stories/:id', async (req, res) => {
388
414
  const { id } = req.params;
389
415
  const storiesPath = config.generatedStoriesPath;
390
416
  // Try exact match first (fileName format)
391
- // Handle both .tsx and .svelte extensions
417
+ // Handle all story file extensions
392
418
  let fileName = id;
393
- if (!id.endsWith('.stories.tsx') && !id.endsWith('.stories.ts') && !id.endsWith('.stories.svelte')) {
394
- fileName = `${id}.stories.tsx`;
419
+ if (!isStoryFile(id)) {
420
+ // Get the correct file extension from the framework adapter
421
+ const registry = getAdapterRegistry();
422
+ const frameworkType = (config.componentFramework || 'react');
423
+ const adapter = registry.getAdapter(frameworkType);
424
+ const extension = adapter?.defaultExtension || '.stories.tsx';
425
+ fileName = `${id}${extension}`;
395
426
  }
396
427
  const filePath = path.join(storiesPath, fileName);
397
428
  if (fs.existsSync(filePath)) {
@@ -433,7 +464,7 @@ app.post('/story-ui/delete', async (req, res) => {
433
464
  console.log(`🔍 Searching for story in: ${storiesPath}`);
434
465
  if (fs.existsSync(storiesPath)) {
435
466
  const files = fs.readdirSync(storiesPath);
436
- const matchingFile = files.find(file => file.includes(id) || file.replace('.stories.tsx', '') === id);
467
+ const matchingFile = files.find(file => file.includes(id) || removeStoryExtension(file) === id);
437
468
  if (matchingFile) {
438
469
  const filePath = path.join(storiesPath, matchingFile);
439
470
  fs.unlinkSync(filePath);
@@ -467,9 +498,15 @@ app.post('/story-ui/stories/delete-bulk', async (req, res) => {
467
498
  const deleted = [];
468
499
  const notFound = [];
469
500
  const errors = [];
501
+ // Get the correct file extension from the framework adapter
502
+ const registry = getAdapterRegistry();
503
+ const frameworkType = (config.componentFramework || 'react');
504
+ const adapter = registry.getAdapter(frameworkType);
505
+ const extension = adapter?.defaultExtension || '.stories.tsx';
470
506
  for (const id of ids) {
471
507
  try {
472
- const fileName = id.endsWith('.stories.tsx') ? id : `${id}.stories.tsx`;
508
+ // Check if id already has a story extension
509
+ const fileName = isStoryFile(id) ? id : `${id}${extension}`;
473
510
  const filePath = path.join(storiesPath, fileName);
474
511
  if (fs.existsSync(filePath)) {
475
512
  fs.unlinkSync(filePath);
@@ -477,7 +514,17 @@ app.post('/story-ui/stories/delete-bulk', async (req, res) => {
477
514
  console.log(`✅ Deleted: ${fileName}`);
478
515
  }
479
516
  else {
480
- notFound.push(id);
517
+ // Try to find a matching file with any story extension
518
+ const files = fs.readdirSync(storiesPath);
519
+ const matchingFile = files.find(f => isStoryFile(f) && (f === id || removeStoryExtension(f) === id));
520
+ if (matchingFile) {
521
+ fs.unlinkSync(path.join(storiesPath, matchingFile));
522
+ deleted.push(id);
523
+ console.log(`✅ Deleted: ${matchingFile}`);
524
+ }
525
+ else {
526
+ notFound.push(id);
527
+ }
481
528
  }
482
529
  }
483
530
  catch (err) {
@@ -543,10 +590,8 @@ app.delete('/story-ui/stories', async (req, res) => {
543
590
  return res.json({ success: true, deleted: 0, message: 'No stories directory found' });
544
591
  }
545
592
  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'));
593
+ // Support all story file extensions
594
+ const storyFiles = files.filter(file => isStoryFile(file));
550
595
  let deleted = 0;
551
596
  for (const file of storyFiles) {
552
597
  try {
@@ -582,24 +627,16 @@ app.post('/story-ui/orphan-stories', async (req, res) => {
582
627
  return res.json({ orphans: [], count: 0 });
583
628
  }
584
629
  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'));
630
+ const storyFiles = files.filter(file => isStoryFile(file));
588
631
  // Find orphans: stories that don't match any chat fileName
589
632
  const orphans = storyFiles.filter(storyFile => {
590
633
  // Extract the base name without extension for comparison
591
- const storyBase = storyFile
592
- .replace('.stories.tsx', '')
593
- .replace('.stories.ts', '')
594
- .replace('.stories.svelte', '');
634
+ const storyBase = removeStoryExtension(storyFile);
595
635
  // Check if any chat fileName matches this story
596
636
  return !chatFileNames.some(chatFileName => {
597
637
  if (!chatFileName)
598
638
  return false;
599
- const chatBase = chatFileName
600
- .replace('.stories.tsx', '')
601
- .replace('.stories.ts', '')
602
- .replace('.stories.svelte', '');
639
+ const chatBase = removeStoryExtension(chatFileName);
603
640
  return storyBase === chatBase || storyFile === chatFileName;
604
641
  });
605
642
  });
@@ -642,22 +679,14 @@ app.delete('/story-ui/orphan-stories', async (req, res) => {
642
679
  return res.json({ deleted: [], count: 0 });
643
680
  }
644
681
  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'));
682
+ const storyFiles = files.filter(file => isStoryFile(file));
648
683
  // Find orphans: stories that don't match any chat fileName
649
684
  const orphans = storyFiles.filter(storyFile => {
650
- const storyBase = storyFile
651
- .replace('.stories.tsx', '')
652
- .replace('.stories.ts', '')
653
- .replace('.stories.svelte', '');
685
+ const storyBase = removeStoryExtension(storyFile);
654
686
  return !chatFileNames.some(chatFileName => {
655
687
  if (!chatFileName)
656
688
  return false;
657
- const chatBase = chatFileName
658
- .replace('.stories.tsx', '')
659
- .replace('.stories.ts', '')
660
- .replace('.stories.svelte', '');
689
+ const chatBase = removeStoryExtension(chatFileName);
661
690
  return storyBase === chatBase || storyFile === chatFileName;
662
691
  });
663
692
  });
@@ -31,6 +31,26 @@ const HTTP_PORT = process.env.VITE_STORY_UI_PORT || process.env.STORY_UI_HTTP_PO
31
31
  const HTTP_BASE_URL = process.env.STORY_UI_HTTP_BASE_URL || `http://localhost:${HTTP_PORT}`;
32
32
  // Initialize configuration
33
33
  const config = loadUserConfig();
34
+ // Framework-agnostic story file extensions
35
+ const STORY_EXTENSIONS = ['.stories.tsx', '.stories.ts', '.stories.svelte', '.stories.js'];
36
+ // Helper function to check if a file is a story file
37
+ function isStoryFile(filename) {
38
+ return STORY_EXTENSIONS.some(ext => filename.endsWith(ext));
39
+ }
40
+ // Helper function to remove any story extension from a filename
41
+ function removeStoryExtension(filename) {
42
+ for (const ext of STORY_EXTENSIONS) {
43
+ if (filename.endsWith(ext)) {
44
+ return filename.slice(0, -ext.length);
45
+ }
46
+ }
47
+ return filename;
48
+ }
49
+ // Helper function to extract hash from story filename (e.g., "Button-a1b2c3d4.stories.tsx" -> "a1b2c3d4")
50
+ function getHashFromFilename(filename) {
51
+ const match = filename.match(/-([a-f0-9]{8})\.stories\./);
52
+ return match ? match[1] : null;
53
+ }
34
54
  // Create MCP server instance
35
55
  const server = new Server({
36
56
  name: "story-ui",
@@ -230,12 +250,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
230
250
  if (config.generatedStoriesPath && fs.existsSync(config.generatedStoriesPath)) {
231
251
  const files = fs.readdirSync(config.generatedStoriesPath);
232
252
  files
233
- .filter((file) => file.endsWith('.stories.tsx'))
253
+ .filter((file) => isStoryFile(file))
234
254
  .forEach((file) => {
235
- const hash = file.match(/-([a-f0-9]{8})\.stories\.tsx$/)?.[1] || '';
236
- const storyId = hash ? `story-${hash}` : file.replace('.stories.tsx', '');
255
+ const hash = getHashFromFilename(file) || '';
256
+ const storyId = hash ? `story-${hash}` : removeStoryExtension(file);
237
257
  // Try to read title from file
238
- let title = file.replace('.stories.tsx', '').replace(/-/g, ' ');
258
+ let title = removeStoryExtension(file).replace(/-/g, ' ');
239
259
  try {
240
260
  const filePath = path.join(config.generatedStoriesPath, file);
241
261
  const content = fs.readFileSync(filePath, 'utf-8');
@@ -325,11 +345,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
325
345
  // Extract hash from story ID
326
346
  const hashMatch = storyId.match(/^story-([a-f0-9]{8})$/);
327
347
  const hash = hashMatch ? hashMatch[1] : null;
328
- // Find matching file
348
+ // Find matching file (framework-agnostic)
329
349
  const matchingFile = files.find(file => {
330
- if (hash && file.includes(`-${hash}.stories.tsx`))
350
+ if (!isStoryFile(file))
351
+ return false;
352
+ if (hash && file.includes(`-${hash}.stories.`))
331
353
  return true;
332
- if (file === `${storyId}.stories.tsx`)
354
+ if (removeStoryExtension(file) === storyId)
333
355
  return true;
334
356
  if (file === storyId)
335
357
  return true;
@@ -402,7 +424,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
402
424
  if (!storyId) {
403
425
  if (config.generatedStoriesPath && fs.existsSync(config.generatedStoriesPath)) {
404
426
  const files = fs.readdirSync(config.generatedStoriesPath)
405
- .filter((file) => file.endsWith('.stories.tsx'))
427
+ .filter((file) => isStoryFile(file))
406
428
  .map((file) => {
407
429
  const filePath = path.join(config.generatedStoriesPath, file);
408
430
  const stats = fs.statSync(filePath);
@@ -412,8 +434,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
412
434
  if (files.length > 0) {
413
435
  // Use the most recently modified story
414
436
  const mostRecent = files[0].file;
415
- const hashMatch = mostRecent.match(/-([a-f0-9]{8})\.stories\.tsx$/);
416
- storyId = hashMatch ? `story-${hashMatch[1]}` : mostRecent.replace('.stories.tsx', '');
437
+ const hash = getHashFromFilename(mostRecent);
438
+ storyId = hash ? `story-${hash}` : removeStoryExtension(mostRecent);
417
439
  console.error(`[MCP] Using most recent story: ${mostRecent} (${storyId})`);
418
440
  }
419
441
  }
@@ -436,11 +458,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
436
458
  // Extract hash from story ID
437
459
  const hashMatch = storyId.match(/^story-([a-f0-9]{8})$/);
438
460
  const hash = hashMatch ? hashMatch[1] : null;
439
- // Find matching file
461
+ // Find matching file (framework-agnostic)
440
462
  const matchingFile = files.find(file => {
441
- if (hash && file.includes(`-${hash}.stories.tsx`))
463
+ if (!isStoryFile(file))
464
+ return false;
465
+ if (hash && file.includes(`-${hash}.stories.`))
442
466
  return true;
443
- if (file === `${storyId}.stories.tsx`)
467
+ if (removeStoryExtension(file) === storyId)
444
468
  return true;
445
469
  return false;
446
470
  });
@@ -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');
@@ -1 +1 @@
1
- {"version":3,"file":"gemini-provider.d.ts","sourceRoot":"","sources":["../../../story-generator/llm-providers/gemini-provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,YAAY,EACZ,cAAc,EACd,SAAS,EACT,WAAW,EACX,WAAW,EACX,YAAY,EACZ,WAAW,EACX,gBAAgB,EAGjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAoGrD,qBAAa,cAAe,SAAQ,eAAe;IACjD,QAAQ,CAAC,IAAI,YAAY;IACzB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAY;IACvC,QAAQ,CAAC,eAAe,cAAiB;gBAE7B,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC;IAU5C,OAAO,CAAC,SAAS;IAKX,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IA6D1E,UAAU,CACf,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,CAAC,EAAE,WAAW,GACpB,aAAa,CAAC,WAAW,CAAC;IA+GvB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA8C/D,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,cAAc;IAqCtB,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,eAAe;IAiBvB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAKrC;AAGD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAErF"}
1
+ {"version":3,"file":"gemini-provider.d.ts","sourceRoot":"","sources":["../../../story-generator/llm-providers/gemini-provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,YAAY,EACZ,cAAc,EACd,SAAS,EACT,WAAW,EACX,WAAW,EACX,YAAY,EACZ,WAAW,EACX,gBAAgB,EAGjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAkHrD,qBAAa,cAAe,SAAQ,eAAe;IACjD,QAAQ,CAAC,IAAI,YAAY;IACzB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAY;IACvC,QAAQ,CAAC,eAAe,cAAiB;gBAE7B,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC;IAU5C,OAAO,CAAC,SAAS;IAKX,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IA6D1E,UAAU,CACf,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,CAAC,EAAE,WAAW,GACpB,aAAa,CAAC,WAAW,CAAC;IA+GvB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA8C/D,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,cAAc;IAqCtB,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,eAAe;IAiBvB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAKrC;AAGD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAErF"}
@@ -6,8 +6,22 @@
6
6
  import { BaseLLMProvider } from './base-provider.js';
7
7
  import { logger } from '../logger.js';
8
8
  // Gemini model definitions - Updated December 2025
9
- // Top 4 models only - Reference: https://ai.google.dev/gemini-api/docs/models
9
+ // Top 5 models - Reference: https://ai.google.dev/gemini-api/docs/models
10
10
  const GEMINI_MODELS = [
11
+ {
12
+ id: 'gemini-3-pro-preview',
13
+ name: 'Gemini 3 Pro Preview',
14
+ provider: 'gemini',
15
+ contextWindow: 1048576,
16
+ maxOutputTokens: 65536,
17
+ supportsVision: true,
18
+ supportsDocuments: true,
19
+ supportsFunctionCalling: true,
20
+ supportsStreaming: true,
21
+ supportsReasoning: true,
22
+ inputPricePer1kTokens: 0.002,
23
+ outputPricePer1kTokens: 0.012,
24
+ },
11
25
  {
12
26
  id: 'gemini-2.5-pro',
13
27
  name: 'Gemini 2.5 Pro',
@@ -63,8 +77,8 @@ const GEMINI_MODELS = [
63
77
  outputPricePer1kTokens: 0.005,
64
78
  },
65
79
  ];
66
- // Default model - Gemini 2.5 Pro (recommended)
67
- const DEFAULT_MODEL = 'gemini-2.5-pro';
80
+ // Default model - Gemini 3 Pro Preview (most capable)
81
+ const DEFAULT_MODEL = 'gemini-3-pro-preview';
68
82
  // API configuration
69
83
  const GEMINI_API_BASE = 'https://generativelanguage.googleapis.com/v1beta/models';
70
84
  export class GeminiProvider extends BaseLLMProvider {