screencraft 0.1.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/.claude/settings.local.json +30 -0
- package/.env.example +3 -0
- package/MCP_README.md +200 -0
- package/README.md +148 -0
- package/bin/screencraft.js +61 -0
- package/package.json +31 -0
- package/src/auth/keystore.js +148 -0
- package/src/commands/init.js +119 -0
- package/src/commands/launch.js +405 -0
- package/src/detectors/detectBrand.js +1222 -0
- package/src/detectors/simulator.js +317 -0
- package/src/generators/analyzeStyleReference.js +471 -0
- package/src/generators/compositePSD.js +682 -0
- package/src/generators/copy.js +147 -0
- package/src/mcp/index.js +394 -0
- package/src/pipeline/aeSwap.js +369 -0
- package/src/pipeline/download.js +32 -0
- package/src/pipeline/queue.js +101 -0
- package/src/server/index.js +627 -0
- package/src/server/public/app.js +738 -0
- package/src/server/public/index.html +255 -0
- package/src/server/public/style.css +751 -0
- package/src/server/session.js +36 -0
- package/templates/ae/(Footage)/Assets/This Hip-Hop Upbeat (Short version).wav +0 -0
- package/templates/ae/(Footage)/Assets/screen_01_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_02_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_03_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_04_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_05_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_06_raw.png +0 -0
- package/templates/ae/Motion Forge Test 1.0 (converted).aep +0 -0
- package/templates/ae_swap.jsx +284 -0
- package/templates/layouts/minimal.psd +0 -0
- package/templates/screencraft.config.example.js +165 -0
- package/test/output/layout_test.png +0 -0
- package/test/output/style_profile.json +64 -0
- package/test/reference.png +0 -0
- package/test/test_brand.js +69 -0
- package/test/test_psd.js +83 -0
- package/test/test_style_analysis.js +114 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/generators/copy.js
|
|
3
|
+
* ----------------------
|
|
4
|
+
* Sends each screenshot to Claude and gets back 3 headline options.
|
|
5
|
+
* Each headline = { white: "short phrase", accent: "key word" }
|
|
6
|
+
*
|
|
7
|
+
* Falls back to generic suggestions if API key not set.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const ANTHROPIC_API = 'https://api.anthropic.com/v1/messages';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* suggestHeadlines(screenshots, brand, config)
|
|
17
|
+
* Returns array of { description, options: [{white, accent}×3] }
|
|
18
|
+
* One entry per screenshot.
|
|
19
|
+
*/
|
|
20
|
+
async function suggestHeadlines(screenshots, brand, config) {
|
|
21
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
22
|
+
|
|
23
|
+
if (!apiKey) {
|
|
24
|
+
console.log(' ⚠ ANTHROPIC_API_KEY not set — using generic suggestions');
|
|
25
|
+
return screenshots.map((s, i) => genericSuggestions(i, brand));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const results = [];
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < screenshots.length; i++) {
|
|
31
|
+
const screen = screenshots[i];
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const imageB64 = fs.readFileSync(screen.file).toString('base64');
|
|
35
|
+
const ext = path.extname(screen.file).replace('.', '').toLowerCase();
|
|
36
|
+
const mimeType = ext === 'jpg' ? 'image/jpeg' : 'image/png';
|
|
37
|
+
|
|
38
|
+
const response = await fetch(ANTHROPIC_API, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
'x-api-key': apiKey,
|
|
43
|
+
'anthropic-version': '2023-06-01',
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
model: 'claude-opus-4-5',
|
|
47
|
+
max_tokens: 400,
|
|
48
|
+
system: `You are writing short App Store screenshot headlines for a mobile app.
|
|
49
|
+
Each headline has two parts:
|
|
50
|
+
1. "white" — a short phrase in white text (2-5 words)
|
|
51
|
+
2. "accent" — a single key word or very short phrase (1-2 words) in the app's accent color
|
|
52
|
+
|
|
53
|
+
Rules:
|
|
54
|
+
- Total headline reads as one phrase: white + accent
|
|
55
|
+
- Punchy, benefit-focused, not feature-focused
|
|
56
|
+
- App Store style — confident, not clickbait
|
|
57
|
+
- Must feel appropriate for the specific screen shown
|
|
58
|
+
- Return ONLY valid JSON, no explanation
|
|
59
|
+
|
|
60
|
+
Return exactly this JSON structure:
|
|
61
|
+
{
|
|
62
|
+
"description": "one sentence describing what this screen shows",
|
|
63
|
+
"options": [
|
|
64
|
+
{ "white": "phrase one", "accent": "word" },
|
|
65
|
+
{ "white": "phrase two", "accent": "word" },
|
|
66
|
+
{ "white": "phrase three", "accent": "word" }
|
|
67
|
+
]
|
|
68
|
+
}`,
|
|
69
|
+
messages: [{
|
|
70
|
+
role: 'user',
|
|
71
|
+
content: [
|
|
72
|
+
{
|
|
73
|
+
type: 'image',
|
|
74
|
+
source: { type: 'base64', media_type: mimeType, data: imageB64 },
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
type: 'text',
|
|
78
|
+
text: `App name: ${brand.appName || 'App'}
|
|
79
|
+
Tagline: ${brand.tagline || ''}
|
|
80
|
+
Tone: ${config.app?.tone || 'Friendly & energetic'}
|
|
81
|
+
|
|
82
|
+
Write 3 headline options for this App Store screenshot.`,
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
}],
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const data = await response.json();
|
|
90
|
+
const text = data.content?.[0]?.text || '';
|
|
91
|
+
|
|
92
|
+
// Parse JSON from response
|
|
93
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
94
|
+
if (jsonMatch) {
|
|
95
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
96
|
+
results.push(parsed);
|
|
97
|
+
} else {
|
|
98
|
+
results.push(genericSuggestions(i, brand));
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
results.push(genericSuggestions(i, brand));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return results;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Fallback suggestions when API key is not available.
|
|
110
|
+
*/
|
|
111
|
+
function genericSuggestions(index, brand) {
|
|
112
|
+
const sets = [
|
|
113
|
+
{ description: 'App screenshot', options: [
|
|
114
|
+
{ white: 'Simple and', accent: 'powerful' },
|
|
115
|
+
{ white: 'Built for how you', accent: 'work' },
|
|
116
|
+
{ white: 'Everything you need,', accent: 'nothing you don\'t' },
|
|
117
|
+
]},
|
|
118
|
+
{ description: 'App screenshot', options: [
|
|
119
|
+
{ white: 'Unlock your', accent: 'potential' },
|
|
120
|
+
{ white: 'Do more with', accent: 'less' },
|
|
121
|
+
{ white: 'Work smarter,', accent: 'not harder' },
|
|
122
|
+
]},
|
|
123
|
+
{ description: 'App screenshot', options: [
|
|
124
|
+
{ white: 'Your personal', accent: 'toolkit' },
|
|
125
|
+
{ white: 'Always within', accent: 'reach' },
|
|
126
|
+
{ white: 'Designed for', accent: 'flow' },
|
|
127
|
+
]},
|
|
128
|
+
{ description: 'App screenshot', options: [
|
|
129
|
+
{ white: 'The way it should', accent: 'work' },
|
|
130
|
+
{ white: 'Finally,', accent: 'focus' },
|
|
131
|
+
{ white: 'Built for the way you', accent: 'think' },
|
|
132
|
+
]},
|
|
133
|
+
{ description: 'App screenshot', options: [
|
|
134
|
+
{ white: 'Get started in', accent: 'seconds' },
|
|
135
|
+
{ white: 'Download and', accent: 'go' },
|
|
136
|
+
{ white: 'Join thousands of', accent: 'users' },
|
|
137
|
+
]},
|
|
138
|
+
{ description: 'App screenshot', options: [
|
|
139
|
+
{ white: 'See for', accent: 'yourself' },
|
|
140
|
+
{ white: 'The proof is in the', accent: 'results' },
|
|
141
|
+
{ white: 'Trusted by', accent: 'thousands' },
|
|
142
|
+
]},
|
|
143
|
+
];
|
|
144
|
+
return sets[index % sets.length];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { suggestHeadlines };
|
package/src/mcp/index.js
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/mcp/index.js
|
|
3
|
+
* ----------------
|
|
4
|
+
* MCP (Model Context Protocol) server for ScreenCraft.
|
|
5
|
+
*
|
|
6
|
+
* Exposes tools that Claude Code, Claude Desktop, and Claude Cowork
|
|
7
|
+
* can call to drive the ScreenCraft pipeline. The AI session becomes
|
|
8
|
+
* the intelligence layer — it generates headlines, suggests screenshots,
|
|
9
|
+
* and detects brand context from the codebase it already understands.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* screencraft --mcp (stdio transport)
|
|
13
|
+
* claude mcp add --transport stdio screencraft -- npx screencraft --mcp
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
17
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
18
|
+
const { z } = require('zod');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
|
|
22
|
+
const { detectFramework, detectBrand } = require('../detectors/detectBrand');
|
|
23
|
+
const { compositePSD } = require('../generators/compositePSD');
|
|
24
|
+
const { validateKey, getStoredKey } = require('../auth/keystore');
|
|
25
|
+
const { prepareAEProject, renderLocally } = require('../pipeline/aeSwap');
|
|
26
|
+
const { session, resetSession } = require('../server/session');
|
|
27
|
+
|
|
28
|
+
// ── Create MCP Server ─────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const server = new McpServer({
|
|
31
|
+
name: 'screencraft',
|
|
32
|
+
version: '0.1.0',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// ── Logging (silent in MCP mode — stdout is protocol) ─────────────
|
|
36
|
+
const log = {
|
|
37
|
+
step: () => {},
|
|
38
|
+
success: () => {},
|
|
39
|
+
info: () => {},
|
|
40
|
+
warn: () => {},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// ── Tool: detect_brand ────────────────────────────────────────────
|
|
44
|
+
server.tool(
|
|
45
|
+
'detect_brand',
|
|
46
|
+
'Detect the app framework, brand colors, icon, font, and logo from a project directory. Run this first before other ScreenCraft tools.',
|
|
47
|
+
{
|
|
48
|
+
project_path: z.string().describe('Absolute path to the app project directory'),
|
|
49
|
+
},
|
|
50
|
+
async ({ project_path }) => {
|
|
51
|
+
if (!fs.existsSync(project_path)) {
|
|
52
|
+
return { content: [{ type: 'text', text: `Error: Path does not exist: ${project_path}` }] };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
resetSession();
|
|
56
|
+
session.projectPath = project_path;
|
|
57
|
+
|
|
58
|
+
const configPath = path.join(project_path, 'screencraft.config.js');
|
|
59
|
+
const config = fs.existsSync(configPath) ? require(configPath) : {};
|
|
60
|
+
|
|
61
|
+
session.outputDir = path.join(project_path, config.output?.dir || './launch-kit');
|
|
62
|
+
fs.mkdirSync(session.outputDir, { recursive: true });
|
|
63
|
+
fs.mkdirSync(path.join(session.outputDir, 'screenshots'), { recursive: true });
|
|
64
|
+
fs.mkdirSync(path.join(session.outputDir, 'video'), { recursive: true });
|
|
65
|
+
fs.mkdirSync(path.join(session.outputDir, 'source'), { recursive: true });
|
|
66
|
+
fs.mkdirSync(path.join(session.outputDir, 'brand'), { recursive: true });
|
|
67
|
+
|
|
68
|
+
const framework = await detectFramework(project_path);
|
|
69
|
+
const brand = await detectBrand(project_path, config);
|
|
70
|
+
|
|
71
|
+
session.framework = framework;
|
|
72
|
+
session.brand = brand;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
content: [{
|
|
76
|
+
type: 'text',
|
|
77
|
+
text: JSON.stringify({
|
|
78
|
+
framework: { name: framework.name, platform: framework.platform },
|
|
79
|
+
brand: {
|
|
80
|
+
appName: brand.appName,
|
|
81
|
+
primary: brand.primary,
|
|
82
|
+
secondary: brand.secondary,
|
|
83
|
+
accent: brand.accent,
|
|
84
|
+
background: brand.background,
|
|
85
|
+
icon: brand.icon ? 'found' : null,
|
|
86
|
+
font: brand.font?.family || null,
|
|
87
|
+
logo: brand.logo ? 'found' : null,
|
|
88
|
+
},
|
|
89
|
+
}, null, 2),
|
|
90
|
+
}],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// ── Tool: set_headlines ───────────────────────────────────────────
|
|
96
|
+
server.tool(
|
|
97
|
+
'set_headlines',
|
|
98
|
+
`Set the App Store headline text for each screenshot. You (the AI) should generate these based on your understanding of the app. Each headline has two parts: "white" (main text, 2-5 words) and "accent" (emphasized word, 1-2 words, shown in brand accent color). Provide one headline per screenshot. Example: {"white": "Track your", "accent": "progress"}`,
|
|
99
|
+
{
|
|
100
|
+
headlines: z.array(z.object({
|
|
101
|
+
white: z.string().describe('Main headline text (2-5 words, appears in white)'),
|
|
102
|
+
accent: z.string().describe('Accent word (1-2 words, appears in brand accent color)'),
|
|
103
|
+
})).describe('Array of headlines, one per screenshot'),
|
|
104
|
+
},
|
|
105
|
+
async ({ headlines }) => {
|
|
106
|
+
session.approvedTexts = headlines;
|
|
107
|
+
|
|
108
|
+
// Also populate suggestions so the web UI can show them
|
|
109
|
+
session.suggestions = headlines.map(h => ({
|
|
110
|
+
description: '',
|
|
111
|
+
options: [h, h, h],
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
content: [{
|
|
116
|
+
type: 'text',
|
|
117
|
+
text: `Set ${headlines.length} headlines. ${session.screenshots.length > 0 ? 'Ready to generate — call open_ui or generate_launch_kit.' : 'Still need screenshots — call set_screenshots or open_ui to capture them.'}`,
|
|
118
|
+
}],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// ── Tool: set_screenshots ─────────────────────────────────────────
|
|
124
|
+
server.tool(
|
|
125
|
+
'suggest_screenshots',
|
|
126
|
+
'Suggest which app screens to capture for the App Store listing. Return a list of screen descriptions and the routes/views to navigate to. The user will capture these in the web UI.',
|
|
127
|
+
{
|
|
128
|
+
screens: z.array(z.object({
|
|
129
|
+
label: z.string().describe('Short label for this screen (e.g., "Home", "Settings", "Onboarding")'),
|
|
130
|
+
description: z.string().describe('What this screen shows and why it would make a good App Store screenshot'),
|
|
131
|
+
route: z.string().optional().describe('Deep link URL or navigation path if known'),
|
|
132
|
+
})).describe('Array of 3-6 recommended screens to capture'),
|
|
133
|
+
},
|
|
134
|
+
async ({ screens }) => {
|
|
135
|
+
// Store as suggestions — the user will capture actual screenshots in the UI
|
|
136
|
+
session._suggestedScreens = screens;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
content: [{
|
|
140
|
+
type: 'text',
|
|
141
|
+
text: `Suggested ${screens.length} screenshots:\n${screens.map((s, i) => ` ${i + 1}. ${s.label}: ${s.description}`).join('\n')}\n\nCall open_ui to let the user capture these in the Simulator.`,
|
|
142
|
+
}],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// ── Tool: set_brand_overrides ─────────────────────────────────────
|
|
148
|
+
server.tool(
|
|
149
|
+
'set_brand_overrides',
|
|
150
|
+
'Override detected brand values. Use this if you know the correct colors, app name, etc. from the codebase. Only provide fields you want to override — omitted fields keep their detected values.',
|
|
151
|
+
{
|
|
152
|
+
app_name: z.string().optional().describe('App display name'),
|
|
153
|
+
primary: z.string().optional().describe('Primary brand color (hex, e.g., #1A1A2E)'),
|
|
154
|
+
secondary: z.string().optional().describe('Secondary brand color (hex)'),
|
|
155
|
+
accent: z.string().optional().describe('Accent brand color (hex)'),
|
|
156
|
+
background: z.string().optional().describe('Background color (hex)'),
|
|
157
|
+
},
|
|
158
|
+
async ({ app_name, primary, secondary, accent, background }) => {
|
|
159
|
+
if (!session.brand) {
|
|
160
|
+
return { content: [{ type: 'text', text: 'Error: Run detect_brand first.' }] };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (app_name) session.brand.appName = app_name;
|
|
164
|
+
if (primary) session.brand.primary = primary;
|
|
165
|
+
if (secondary) session.brand.secondary = secondary;
|
|
166
|
+
if (accent) session.brand.accent = accent;
|
|
167
|
+
if (background) session.brand.background = background;
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
content: [{
|
|
171
|
+
type: 'text',
|
|
172
|
+
text: `Brand updated: ${JSON.stringify({
|
|
173
|
+
appName: session.brand.appName,
|
|
174
|
+
primary: session.brand.primary,
|
|
175
|
+
secondary: session.brand.secondary,
|
|
176
|
+
accent: session.brand.accent,
|
|
177
|
+
background: session.brand.background,
|
|
178
|
+
}, null, 2)}`,
|
|
179
|
+
}],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// ── Tool: list_templates ──────────────────────────────────────────
|
|
185
|
+
server.tool(
|
|
186
|
+
'list_templates',
|
|
187
|
+
'List available PSD/layout templates for App Store screenshot generation.',
|
|
188
|
+
{},
|
|
189
|
+
async () => {
|
|
190
|
+
const layoutsDir = path.join(__dirname, '..', '..', 'templates', 'layouts');
|
|
191
|
+
const templates = [];
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
if (fs.existsSync(layoutsDir)) {
|
|
195
|
+
const entries = fs.readdirSync(layoutsDir, { withFileTypes: true });
|
|
196
|
+
entries
|
|
197
|
+
.filter(e => !e.isDirectory() && e.name.endsWith('.psd'))
|
|
198
|
+
.forEach(e => templates.push(e.name.replace('.psd', '')));
|
|
199
|
+
entries
|
|
200
|
+
.filter(e => e.isDirectory() && fs.existsSync(path.join(layoutsDir, e.name, 'template.psd')))
|
|
201
|
+
.forEach(e => templates.push(e.name));
|
|
202
|
+
}
|
|
203
|
+
} catch {}
|
|
204
|
+
|
|
205
|
+
templates.push('auto');
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
content: [{
|
|
209
|
+
type: 'text',
|
|
210
|
+
text: `Available templates: ${templates.join(', ')}\n\nUse set_template to choose one, or "auto" for automatic selection.`,
|
|
211
|
+
}],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// ── Tool: set_template ────────────────────────────────────────────
|
|
217
|
+
server.tool(
|
|
218
|
+
'set_template',
|
|
219
|
+
'Select which PSD template to use for screenshot compositing.',
|
|
220
|
+
{
|
|
221
|
+
template: z.string().describe('Template name (from list_templates) or "auto"'),
|
|
222
|
+
},
|
|
223
|
+
async ({ template }) => {
|
|
224
|
+
session.selectedTemplate = template === 'auto' ? null : template;
|
|
225
|
+
return {
|
|
226
|
+
content: [{ type: 'text', text: `Template set to: ${template}` }],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// ── Tool: open_ui ─────────────────────────────────────────────────
|
|
232
|
+
server.tool(
|
|
233
|
+
'open_ui',
|
|
234
|
+
'Start the ScreenCraft web UI at localhost:3141 and open it in the browser. The UI will be pre-populated with any brand data, headlines, and screenshots already set via MCP tools. The user can review, adjust, and generate from there.',
|
|
235
|
+
{
|
|
236
|
+
skip_to_step: z.number().optional().describe('Step to open at: 0=project, 1=brand, 2=template, 3=screenshots, 4=headlines, 5=preview, 6=license, 7=generate'),
|
|
237
|
+
},
|
|
238
|
+
async ({ skip_to_step }) => {
|
|
239
|
+
const { startServer } = require('../server');
|
|
240
|
+
|
|
241
|
+
// Store the skip-to step so the frontend can read it
|
|
242
|
+
session._skipToStep = skip_to_step || null;
|
|
243
|
+
|
|
244
|
+
// Start the server (it auto-opens the browser)
|
|
245
|
+
startServer();
|
|
246
|
+
|
|
247
|
+
const parts = [];
|
|
248
|
+
if (session.brand) parts.push('brand detection');
|
|
249
|
+
if (session.approvedTexts.length) parts.push(`${session.approvedTexts.length} headlines`);
|
|
250
|
+
if (session.screenshots.length) parts.push(`${session.screenshots.length} screenshots`);
|
|
251
|
+
if (session.selectedTemplate) parts.push(`template: ${session.selectedTemplate}`);
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
content: [{
|
|
255
|
+
type: 'text',
|
|
256
|
+
text: `ScreenCraft UI opened at http://localhost:3141\n${parts.length ? 'Pre-loaded: ' + parts.join(', ') : 'Starting fresh.'}`,
|
|
257
|
+
}],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// ── Tool: get_session_status ──────────────────────────────────────
|
|
263
|
+
server.tool(
|
|
264
|
+
'get_session_status',
|
|
265
|
+
'Check the current state of the ScreenCraft session — what has been configured and what is still needed.',
|
|
266
|
+
{},
|
|
267
|
+
async () => {
|
|
268
|
+
const status = {
|
|
269
|
+
project: session.projectPath || 'not set',
|
|
270
|
+
brand: session.brand ? 'detected' : 'not detected',
|
|
271
|
+
appName: session.brand?.appName || null,
|
|
272
|
+
colors: session.brand ? {
|
|
273
|
+
primary: session.brand.primary,
|
|
274
|
+
accent: session.brand.accent,
|
|
275
|
+
} : null,
|
|
276
|
+
template: session.selectedTemplate || 'auto',
|
|
277
|
+
screenshots: `${session.screenshots.length} captured`,
|
|
278
|
+
headlines: `${session.approvedTexts.length} set`,
|
|
279
|
+
renderStatus: session.renderStatus.phase,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const needed = [];
|
|
283
|
+
if (!session.brand) needed.push('Run detect_brand with the project path');
|
|
284
|
+
if (!session.screenshots.length) needed.push('Capture screenshots (open_ui or place PNGs in screenshots/ folder)');
|
|
285
|
+
if (!session.approvedTexts.length) needed.push('Set headlines with set_headlines');
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
content: [{
|
|
289
|
+
type: 'text',
|
|
290
|
+
text: JSON.stringify(status, null, 2) +
|
|
291
|
+
(needed.length ? '\n\nStill needed:\n' + needed.map(n => ` - ${n}`).join('\n') : '\n\nReady to generate! Call open_ui or generate_launch_kit.'),
|
|
292
|
+
}],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
// ── Tool: generate_launch_kit ─────────────────────────────────────
|
|
298
|
+
server.tool(
|
|
299
|
+
'generate_launch_kit',
|
|
300
|
+
'Generate the full launch kit (composited App Store screenshots, PSDs, and optionally AE project + video). Requires brand detection, screenshots, and headlines to be set first. Returns paths to all generated files.',
|
|
301
|
+
{
|
|
302
|
+
screenshots_only: z.boolean().optional().describe('If true, skip video/AE generation (free tier). Default: true'),
|
|
303
|
+
},
|
|
304
|
+
async ({ screenshots_only = true }) => {
|
|
305
|
+
if (!session.brand || !session.screenshots.length || !session.approvedTexts.length) {
|
|
306
|
+
const missing = [];
|
|
307
|
+
if (!session.brand) missing.push('brand (run detect_brand)');
|
|
308
|
+
if (!session.screenshots.length) missing.push('screenshots (capture via UI or screenshots/ folder)');
|
|
309
|
+
if (!session.approvedTexts.length) missing.push('headlines (run set_headlines)');
|
|
310
|
+
return {
|
|
311
|
+
content: [{ type: 'text', text: `Cannot generate — missing: ${missing.join(', ')}` }],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const outputs = [];
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
// Composite PSD/PNGs
|
|
319
|
+
for (let i = 0; i < session.screenshots.length; i++) {
|
|
320
|
+
const screen = session.screenshots[i];
|
|
321
|
+
const text = session.approvedTexts[i];
|
|
322
|
+
if (!text) continue;
|
|
323
|
+
|
|
324
|
+
const outPng = path.join(session.outputDir, 'screenshots', `screen_${String(i + 1).padStart(2, '0')}.png`);
|
|
325
|
+
const outPsd = path.join(session.outputDir, 'source', `screen_${String(i + 1).padStart(2, '0')}.psd`);
|
|
326
|
+
|
|
327
|
+
await compositePSD({
|
|
328
|
+
templateName: session.selectedTemplate,
|
|
329
|
+
screenshotPath: screen.file,
|
|
330
|
+
headlineWhite: text.white,
|
|
331
|
+
headlineAccent: text.accent,
|
|
332
|
+
brand: session.brand,
|
|
333
|
+
font: session.brand.font || {},
|
|
334
|
+
outputPng: outPng,
|
|
335
|
+
outputPsd: outPsd,
|
|
336
|
+
writePsd: !screenshots_only,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
outputs.push(outPng);
|
|
340
|
+
if (!screenshots_only) outputs.push(outPsd);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Copy brand assets
|
|
344
|
+
const assetsDir = path.join(session.outputDir, 'brand');
|
|
345
|
+
fs.mkdirSync(assetsDir, { recursive: true });
|
|
346
|
+
if (session.brand.icon && fs.existsSync(session.brand.icon)) {
|
|
347
|
+
fs.copyFileSync(session.brand.icon, path.join(assetsDir, `app-icon${path.extname(session.brand.icon)}`));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// AE project if not screenshots-only
|
|
351
|
+
if (!screenshots_only) {
|
|
352
|
+
try {
|
|
353
|
+
const ae = await prepareAEProject({
|
|
354
|
+
brand: session.brand,
|
|
355
|
+
texts: session.approvedTexts,
|
|
356
|
+
screenshots: session.screenshots,
|
|
357
|
+
outputDir: session.outputDir,
|
|
358
|
+
log,
|
|
359
|
+
});
|
|
360
|
+
if (ae.aepPath) outputs.push(ae.aepPath);
|
|
361
|
+
|
|
362
|
+
if (ae.canRender) {
|
|
363
|
+
try {
|
|
364
|
+
const videoPath = await renderLocally({
|
|
365
|
+
aepPath: ae.aepPath,
|
|
366
|
+
outputDir: session.outputDir,
|
|
367
|
+
});
|
|
368
|
+
if (videoPath) outputs.push(videoPath);
|
|
369
|
+
} catch {}
|
|
370
|
+
}
|
|
371
|
+
} catch {}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
content: [{
|
|
376
|
+
type: 'text',
|
|
377
|
+
text: `Launch kit generated!\n\nOutput directory: ${session.outputDir}\n\nFiles:\n${outputs.map(f => ' ' + f).join('\n')}`,
|
|
378
|
+
}],
|
|
379
|
+
};
|
|
380
|
+
} catch (err) {
|
|
381
|
+
return {
|
|
382
|
+
content: [{ type: 'text', text: `Generation failed: ${err.message}` }],
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
// ── Start MCP Server ──────────────────────────────────────────────
|
|
389
|
+
async function startMCP() {
|
|
390
|
+
const transport = new StdioServerTransport();
|
|
391
|
+
await server.connect(transport);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
module.exports = { startMCP };
|