@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-
|
|
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 |
|
|
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) |
|
|
65
|
-
| **GPT** (OpenAI) |
|
|
66
|
-
| **Gemini** (Google) |
|
|
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-
|
|
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
|
|
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
|
|
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
|
package/dist/mcp-server/index.js
CHANGED
|
@@ -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
|
-
|
|
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: '',
|
|
153
|
+
layoutInstructions: '',
|
|
154
154
|
examples: adapter.generateExamples(config),
|
|
155
155
|
sampleStory: adapter.generateSampleStory(config, components),
|
|
156
156
|
framework: this.detectedFramework || {
|