@tpitre/story-ui 4.2.0 → 4.3.0

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-5 (OpenAI), or Gemini (Google)
13
+ - **Multi-Provider AI**: Choose between Claude (Anthropic), GPT-4o (OpenAI), or Gemini (Google)
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
@@ -47,6 +47,12 @@ Story UI will guide you through:
47
47
  - **TypeScript Support**: Full type-aware story generation
48
48
  - **Vision Support**: Attach screenshots for visual component requests
49
49
 
50
+ ### Story Management
51
+ - **Edit Existing Stories**: Modify any generated story through conversation
52
+ - **Delete Stories**: Remove stories directly from the Story UI panel
53
+ - **Orphan Detection**: Find and clean up stories without associated chat history
54
+ - **Full MCP Integration**: Manage stories via Claude Desktop or any MCP-compatible client
55
+
50
56
  ### Multi-Framework Support
51
57
 
52
58
  | Framework | Design Systems | Status |
@@ -54,16 +60,16 @@ Story UI will guide you through:
54
60
  | React | Mantine, Chakra UI, Material UI, Custom | Fully Supported |
55
61
  | Vue | Vuetify, Custom | Fully Supported |
56
62
  | Angular | Angular Material, Custom | Fully Supported |
57
- | Svelte | Skeleton UI, Custom | Fully Supported |
63
+ | Svelte | Flowbite-Svelte, Custom | Fully Supported |
58
64
  | Web Components | Shoelace, Custom | Fully Supported |
59
65
 
60
66
  ### Multi-Provider LLM Support
61
67
 
62
68
  | Provider | Models | Best For |
63
69
  |----------|--------|----------|
64
- | **Claude** (Anthropic) | Opus 4.5, Sonnet 4.5, Haiku 4.5 | Complex reasoning, code quality |
65
- | **GPT** (OpenAI) | GPT-5.1, GPT-5.1 Thinking, GPT-4o, GPT-4o Mini | Versatility, speed |
66
- | **Gemini** (Google) | Gemini 3 Pro, Gemini 2.0 Flash, Gemini 1.5 Pro | Fast generation, cost efficiency |
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 |
67
73
 
68
74
  ### Production Deployment
69
75
  - **Railway**: Node.js backend with file-based story persistence
@@ -115,7 +121,7 @@ The interactive installer will ask:
115
121
  ```
116
122
  ? Which AI provider do you prefer?
117
123
  > Claude (Anthropic) - Recommended
118
- OpenAI (GPT-5)
124
+ OpenAI (GPT-4o)
119
125
  Google Gemini
120
126
 
121
127
  ? Enter your API key:
@@ -319,7 +325,7 @@ Once connected, you can use these commands in Claude Desktop:
319
325
 
320
326
  ## Production Deployment
321
327
 
322
- Story UI v3 can be deployed as a standalone web application accessible from anywhere.
328
+ Story UI can be deployed as a standalone web application accessible from anywhere.
323
329
 
324
330
  ### Architecture
325
331
 
@@ -461,7 +467,7 @@ npx story-ui mcp
461
467
 
462
468
  ## Upgrading from v2
463
469
 
464
- Story UI v3 is backwards compatible with v2 configurations. However, to take advantage of new features:
470
+ Story UI v4 is backwards compatible with previous configurations. However, to take advantage of new features:
465
471
 
466
472
  1. **Multi-Provider Support**: Add `llmProvider` to your config
467
473
  2. **Framework Detection**: Add `framework` to your config for non-React projects
@@ -78,6 +78,176 @@ app.get('/mcp/frameworks/detect', detectCurrentFramework);
78
78
  app.get('/mcp/frameworks/:type', getFrameworkDetails);
79
79
  app.post('/mcp/frameworks/validate', validateStoryForFramework);
80
80
  app.post('/mcp/frameworks/post-process', postProcessStoryForFramework);
81
+ // MCP story management routes - for Claude Desktop and other MCP clients
82
+ // List all stories
83
+ app.get('/mcp/stories', async (req, res) => {
84
+ try {
85
+ const storiesPath = config.generatedStoriesPath;
86
+ if (!fs.existsSync(storiesPath)) {
87
+ return res.json({ stories: [] });
88
+ }
89
+ const files = fs.readdirSync(storiesPath);
90
+ const stories = files
91
+ .filter(file => file.endsWith('.stories.tsx') || file.endsWith('.stories.ts') || file.endsWith('.stories.svelte'))
92
+ .map(file => {
93
+ const filePath = path.join(storiesPath, file);
94
+ const stats = fs.statSync(filePath);
95
+ const content = fs.readFileSync(filePath, 'utf-8');
96
+ // Extract title from story file
97
+ const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
98
+ let title = titleMatch ? titleMatch[1].replace('Generated/', '') : file.replace(/\.stories\.(tsx|ts|svelte)$/, '');
99
+ // Remove hash suffix from display title
100
+ title = title.replace(/\s*\([a-f0-9]{8}\)$/i, '');
101
+ return {
102
+ id: file.replace(/\.stories\.(tsx|ts|svelte)$/, ''),
103
+ storyId: file.replace(/\.stories\.(tsx|ts|svelte)$/, ''),
104
+ fileName: file,
105
+ title,
106
+ lastUpdated: stats.mtime.getTime(),
107
+ createdAt: stats.birthtime.getTime(),
108
+ content
109
+ };
110
+ })
111
+ .sort((a, b) => b.lastUpdated - a.lastUpdated);
112
+ return res.json({ stories });
113
+ }
114
+ catch (error) {
115
+ console.error('Error listing stories:', error);
116
+ return res.status(500).json({ error: 'Failed to list stories' });
117
+ }
118
+ });
119
+ // Get a specific story by ID
120
+ app.get('/mcp/stories/:storyId', async (req, res) => {
121
+ try {
122
+ const { storyId } = req.params;
123
+ const storiesPath = config.generatedStoriesPath;
124
+ if (!fs.existsSync(storiesPath)) {
125
+ return res.status(404).json({ error: 'Stories directory not found' });
126
+ }
127
+ const files = fs.readdirSync(storiesPath);
128
+ // Extract hash from story ID if in legacy format (story-a1b2c3d4)
129
+ const hashMatch = storyId.match(/^story-([a-f0-9]{8})$/);
130
+ const hash = hashMatch ? hashMatch[1] : null;
131
+ // Find matching file
132
+ const matchingFile = files.find(file => {
133
+ // Match by hash suffix
134
+ if (hash && file.includes(`-${hash}.stories.`))
135
+ return true;
136
+ // Match by exact ID
137
+ if (file.startsWith(`${storyId}.stories.`))
138
+ return true;
139
+ // Match by fileName
140
+ if (file === storyId)
141
+ return true;
142
+ // Match by ID without extension
143
+ if (file.replace(/\.stories\.(tsx|ts|svelte)$/, '') === storyId)
144
+ return true;
145
+ return false;
146
+ });
147
+ if (!matchingFile) {
148
+ return res.status(404).json({ error: `Story with ID ${storyId} not found` });
149
+ }
150
+ const filePath = path.join(storiesPath, matchingFile);
151
+ const stats = fs.statSync(filePath);
152
+ const content = fs.readFileSync(filePath, 'utf-8');
153
+ // Extract title from story file
154
+ const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
155
+ let title = titleMatch ? titleMatch[1].replace('Generated/', '') : matchingFile.replace(/\.stories\.(tsx|ts|svelte)$/, '');
156
+ title = title.replace(/\s*\([a-f0-9]{8}\)$/i, '');
157
+ return res.json({
158
+ id: matchingFile.replace(/\.stories\.(tsx|ts|svelte)$/, ''),
159
+ storyId: matchingFile.replace(/\.stories\.(tsx|ts|svelte)$/, ''),
160
+ fileName: matchingFile,
161
+ title,
162
+ lastUpdated: stats.mtime.getTime(),
163
+ createdAt: stats.birthtime.getTime(),
164
+ timestamp: stats.mtime.getTime(),
165
+ content,
166
+ story: content
167
+ });
168
+ }
169
+ catch (error) {
170
+ console.error('Error getting story:', error);
171
+ return res.status(500).json({ error: 'Failed to get story' });
172
+ }
173
+ });
174
+ // Get story content (raw code)
175
+ app.get('/mcp/stories/:storyId/content', async (req, res) => {
176
+ try {
177
+ const { storyId } = req.params;
178
+ const storiesPath = config.generatedStoriesPath;
179
+ if (!fs.existsSync(storiesPath)) {
180
+ return res.status(404).send('Stories directory not found');
181
+ }
182
+ const files = fs.readdirSync(storiesPath);
183
+ // Extract hash from story ID if in legacy format
184
+ const hashMatch = storyId.match(/^story-([a-f0-9]{8})$/);
185
+ const hash = hashMatch ? hashMatch[1] : null;
186
+ // Find matching file
187
+ const matchingFile = files.find(file => {
188
+ if (hash && file.includes(`-${hash}.stories.`))
189
+ return true;
190
+ if (file.startsWith(`${storyId}.stories.`))
191
+ return true;
192
+ if (file === storyId)
193
+ return true;
194
+ if (file.replace(/\.stories\.(tsx|ts|svelte)$/, '') === storyId)
195
+ return true;
196
+ return false;
197
+ });
198
+ if (!matchingFile) {
199
+ return res.status(404).send(`Story with ID ${storyId} not found`);
200
+ }
201
+ const filePath = path.join(storiesPath, matchingFile);
202
+ const content = fs.readFileSync(filePath, 'utf-8');
203
+ res.type('text/plain').send(content);
204
+ }
205
+ catch (error) {
206
+ console.error('Error getting story content:', error);
207
+ return res.status(500).send('Failed to get story content');
208
+ }
209
+ });
210
+ // Delete a story by ID
211
+ app.delete('/mcp/stories/:storyId', async (req, res) => {
212
+ try {
213
+ const { storyId } = req.params;
214
+ const storiesPath = config.generatedStoriesPath;
215
+ if (!fs.existsSync(storiesPath)) {
216
+ return res.status(404).json({ error: 'Stories directory not found' });
217
+ }
218
+ const files = fs.readdirSync(storiesPath);
219
+ // Extract hash from story ID if in legacy format
220
+ const hashMatch = storyId.match(/^story-([a-f0-9]{8})$/);
221
+ const hash = hashMatch ? hashMatch[1] : null;
222
+ // Find matching file
223
+ const matchingFile = files.find(file => {
224
+ if (hash && file.includes(`-${hash}.stories.`))
225
+ return true;
226
+ if (file.startsWith(`${storyId}.stories.`))
227
+ return true;
228
+ if (file === storyId)
229
+ return true;
230
+ if (file.replace(/\.stories\.(tsx|ts|svelte)$/, '') === storyId)
231
+ return true;
232
+ return false;
233
+ });
234
+ if (!matchingFile) {
235
+ return res.status(404).json({ error: `Story with ID ${storyId} not found` });
236
+ }
237
+ const filePath = path.join(storiesPath, matchingFile);
238
+ fs.unlinkSync(filePath);
239
+ console.log(`🗑️ Deleted story via MCP endpoint: ${matchingFile}`);
240
+ return res.json({
241
+ success: true,
242
+ deleted: matchingFile,
243
+ message: `Story "${matchingFile}" has been deleted successfully.`
244
+ });
245
+ }
246
+ catch (error) {
247
+ console.error('Error deleting story:', error);
248
+ return res.status(500).json({ error: 'Failed to delete story' });
249
+ }
250
+ });
81
251
  // File-based story routes - stories are generated as .stories.tsx files
82
252
  // Storybook discovers these automatically via its native file system watching
83
253
  // Proxy routes for frontend compatibility (maps /story-ui/ to /mcp/)
@@ -27,7 +27,8 @@ dotenv.config({ path: path.resolve(process.cwd(), '.env') });
27
27
  process.env.STORY_UI_MCP_MODE = 'true';
28
28
  // Get HTTP server port from environment variables (check multiple possible names)
29
29
  const HTTP_PORT = process.env.VITE_STORY_UI_PORT || process.env.STORY_UI_HTTP_PORT || process.env.PORT || '4001';
30
- const HTTP_BASE_URL = `http://localhost:${HTTP_PORT}`;
30
+ // Allow configurable base URL for Railway/cloud deployments, fallback to localhost for local dev
31
+ const HTTP_BASE_URL = process.env.STORY_UI_HTTP_BASE_URL || `http://localhost:${HTTP_PORT}`;
31
32
  // Initialize configuration
32
33
  const config = loadUserConfig();
33
34
  // Create MCP server instance
@@ -181,8 +182,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
181
182
  throw new Error(`Failed to generate story: ${error}`);
182
183
  }
183
184
  const result = await response.json();
184
- // Debug log to see what we're getting
185
- console.error('Story generation result:', JSON.stringify(result, null, 2));
186
185
  return {
187
186
  content: [{
188
187
  type: "text",
@@ -509,8 +508,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
509
508
  throw new Error(`Failed to update story: ${error}`);
510
509
  }
511
510
  const result = await response.json();
512
- // Debug log to see what we're getting
513
- console.error('Story update result:', JSON.stringify(result, null, 2));
514
511
  return {
515
512
  content: [{
516
513
  type: "text",
@@ -150,7 +150,7 @@ class AdapterRegistry {
150
150
  return {
151
151
  systemPrompt: adapter.generateSystemPrompt(config, options),
152
152
  componentReference: adapter.generateComponentReference(components, config),
153
- layoutInstructions: '', // TODO: Implement layout instructions
153
+ layoutInstructions: '',
154
154
  examples: adapter.generateExamples(config),
155
155
  sampleStory: adapter.generateSampleStory(config, components),
156
156
  framework: this.detectedFramework || {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpitre/story-ui",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "AI-powered Storybook story generator with dynamic component discovery",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",