drafted 1.24.2 → 1.24.4
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 +15 -34
- package/cli/drafted.mjs +1 -1
- package/mcp/chunked-write.mjs +178 -0
- package/mcp/server.mjs +189 -12
- package/package.json +21 -27
- package/skills/import-website-to-drafted.md +329 -0
- package/agent-instructions/global.md +0 -3
- package/cli/google-setup.sh +0 -186
- package/cli/mail-watch.mjs +0 -237
- package/cli/open-url.mjs +0 -17
- package/cli/repo-scan.mjs +0 -240
- package/cli/skill-setup-trust.mjs +0 -17
- package/install-mcp.sh +0 -1440
- package/mcp/active-project-store.mjs +0 -81
- package/mcp/gates.mjs +0 -206
- package/mcp/gws-status.mjs +0 -31
- package/mcp/repo-address.mjs +0 -215
- package/mcp/test-file-path-text.mjs +0 -58
- package/mcp/test-org-guards.mjs +0 -234
- package/mcp/test-project-index.mjs +0 -88
- package/mcp/test-search-terms.mjs +0 -79
- package/mcp/test-skill-paths.mjs +0 -29
- package/mcp/widgets/canvas-overview.html +0 -229
- package/mcp/widgets/frame-preview.html +0 -162
- package/plugin/commands/create-project.md +0 -20
- package/plugin/commands/create-skill.md +0 -18
- package/plugin/commands/extract.md +0 -16
- package/plugin/commands/improve-project-harness.md +0 -16
- package/plugin/commands/improve-skill.md +0 -14
- package/plugin/commands/improve-wiki.md +0 -14
- package/plugin/commands/ingest.md +0 -20
- package/plugin/commands/onboard-drafted.md +0 -18
- package/plugin/skills/drafted/SKILL.md +0 -102
- package/server/lib/umami.mjs +0 -164
- package/src/shared/anchor-scope.mjs +0 -98
- package/src/shared/excalidraw.mjs +0 -94
- package/src/shared/folder-path.mjs +0 -176
- package/src/shared/gate-budget.mjs +0 -30
- package/src/shared/minion-presets.mjs +0 -67
- package/src/shared/okf-log.mjs +0 -62
- package/src/shared/record-conformance.mjs +0 -167
- package/src/shared/root-route.mjs +0 -45
- package/src/shared/task-id.mjs +0 -54
- package/src/shared/test-excalidraw-merge.mjs +0 -52
- package/src/shared/wiki-excalidraw.mjs +0 -90
- /package/{src/shared → shared}/constants.mjs +0 -0
package/package.json
CHANGED
|
@@ -1,44 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.24.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.24.4",
|
|
4
|
+
"description": "Shared surface for AI-human collaboration — MCP server and CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"drafted": "./cli/drafted.mjs",
|
|
8
8
|
"drafted-mcp": "./mcp/server.mjs"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"cli/",
|
|
12
11
|
"mcp/",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"plugin/commands/",
|
|
18
|
-
"install-mcp.sh"
|
|
19
|
-
],
|
|
20
|
-
"license": "MIT",
|
|
21
|
-
"repository": {
|
|
22
|
-
"type": "git",
|
|
23
|
-
"url": "git+https://github.com/ddfourtwo/drafted.git"
|
|
24
|
-
},
|
|
25
|
-
"keywords": [
|
|
26
|
-
"design",
|
|
27
|
-
"canvas",
|
|
28
|
-
"preview",
|
|
29
|
-
"html",
|
|
30
|
-
"claude"
|
|
12
|
+
"cli/",
|
|
13
|
+
"shared/",
|
|
14
|
+
"skills/",
|
|
15
|
+
"README.md"
|
|
31
16
|
],
|
|
32
17
|
"dependencies": {
|
|
33
|
-
"@modelcontextprotocol/ext-apps": "^1.6.0",
|
|
34
18
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
35
19
|
"commander": "^11.1.0",
|
|
36
20
|
"qrcode-terminal": "^0.12.0",
|
|
37
|
-
"ws": "^8.
|
|
38
|
-
"yaml": "^2.8.3",
|
|
21
|
+
"ws": "^8.16.0",
|
|
39
22
|
"zod": "^4.3.6"
|
|
40
23
|
},
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
24
|
+
"keywords": [
|
|
25
|
+
"drafted",
|
|
26
|
+
"design",
|
|
27
|
+
"mcp",
|
|
28
|
+
"claude",
|
|
29
|
+
"ai",
|
|
30
|
+
"canvas"
|
|
31
|
+
],
|
|
32
|
+
"author": "ddfourtwo",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/ddfourtwo/drafted.git"
|
|
36
|
+
},
|
|
37
|
+
"license": "MIT"
|
|
44
38
|
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Import an existing website into Drafted as a design frame. Renders the page in a headless browser to capture the full DOM (including SPA content), downloads all assets, rewrites paths, then uploads via MCP tools.
|
|
3
|
+
argument-hint: "[URL or local path, e.g., 'https://example.com' or './dist']"
|
|
4
|
+
---
|
|
5
|
+
# /import-website-to-drafted
|
|
6
|
+
|
|
7
|
+
Import a website into Drafted as a fully rendered frame with all its assets.
|
|
8
|
+
|
|
9
|
+
> **Partially stale — steps 4-6 predate the `fs` tool grammar.** `write`, `batch`,
|
|
10
|
+
> `upload_asset` and `list_assets` are retired MCP tools; asset upload now goes
|
|
11
|
+
> through `fs(write, ..., file_path=...)`. Step 1 and the Puppeteer capture
|
|
12
|
+
> (steps 2-3) are current. Verify tool names against `fs` before relying on the
|
|
13
|
+
> later steps.
|
|
14
|
+
|
|
15
|
+
## How it works
|
|
16
|
+
|
|
17
|
+
Drafted frames render in iframes. The server injects a `<base>` tag so relative paths like `<link href="css/styles.css">` resolve to the frame's assets in R2 storage. You write the HTML as a frame with `fs(write, ...)`, and all referenced files (CSS, JS, images, fonts) alongside it.
|
|
18
|
+
|
|
19
|
+
**Most modern sites are SPAs** (React, Framer, Webflow, Next.js) where the HTML source is just a shell and the real content is rendered by JavaScript. A simple HTTP fetch won't capture the content. This skill uses Puppeteer to render the page fully before capturing.
|
|
20
|
+
|
|
21
|
+
## Step 1: Pick the project
|
|
22
|
+
|
|
23
|
+
There is no create-then-open step: the project is part of the path, and writing
|
|
24
|
+
into a project that does not exist creates it. Address it directly.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
fs(write, path="/o/<org>/projects/my-import/designs/homepage/index.html", ...)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
To see what already exists first, `fs(ls, path="/o/<org>/projects")` lists them
|
|
31
|
+
and `fs(search, path="/o/<org>/projects", query="<terms>")` finds one by name —
|
|
32
|
+
do not enumerate the whole org to pick a project you can already name.
|
|
33
|
+
|
|
34
|
+
## Step 2: Ensure Puppeteer is available
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm ls puppeteer 2>/dev/null || npm install puppeteer
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Step 3: Render, capture, and download assets
|
|
41
|
+
|
|
42
|
+
Run this script locally. It launches a headless browser, renders the page fully (including SPA content), captures the complete DOM, then downloads all external assets and rewrites URLs to relative paths.
|
|
43
|
+
|
|
44
|
+
**Adapt `SITE_URL` as needed:**
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
node -e "
|
|
48
|
+
const puppeteer = require('puppeteer');
|
|
49
|
+
const https = require('https');
|
|
50
|
+
const http = require('http');
|
|
51
|
+
const fs = require('fs');
|
|
52
|
+
const path = require('path');
|
|
53
|
+
const { URL } = require('url');
|
|
54
|
+
|
|
55
|
+
const SITE_URL = 'https://example.com'; // ← CHANGE THIS
|
|
56
|
+
const OUT_DIR = '/tmp/site-import';
|
|
57
|
+
|
|
58
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
function download(url) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const mod = url.startsWith('https') ? https : http;
|
|
63
|
+
mod.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, res => {
|
|
64
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
65
|
+
return download(res.headers.location).then(resolve, reject);
|
|
66
|
+
}
|
|
67
|
+
if (res.statusCode >= 400) return reject(new Error('HTTP ' + res.statusCode));
|
|
68
|
+
const chunks = [];
|
|
69
|
+
res.on('data', c => chunks.push(c));
|
|
70
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
71
|
+
res.on('error', reject);
|
|
72
|
+
}).on('error', reject);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function urlToAssetPath(urlStr) {
|
|
77
|
+
try {
|
|
78
|
+
const u = new URL(urlStr);
|
|
79
|
+
const domain = u.hostname.replace(/[^a-z0-9.-]/g, '_');
|
|
80
|
+
let p = u.pathname.replace(/[?#].*/, '');
|
|
81
|
+
if (!p || p === '/') p = '/index';
|
|
82
|
+
if (!path.extname(p)) {
|
|
83
|
+
// Guess extension from common patterns
|
|
84
|
+
if (urlStr.includes('font') || urlStr.includes('woff')) p += '.woff2';
|
|
85
|
+
else if (urlStr.includes('css') || urlStr.includes('stylesheet')) p += '.css';
|
|
86
|
+
else p += '.bin';
|
|
87
|
+
}
|
|
88
|
+
return 'assets/' + domain + p;
|
|
89
|
+
} catch { return null; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const MIME_BY_EXT = {
|
|
93
|
+
'.css': 'text/css', '.js': 'application/javascript', '.mjs': 'application/javascript',
|
|
94
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
|
|
95
|
+
'.webp': 'image/webp', '.svg': 'image/svg+xml', '.ico': 'image/x-icon',
|
|
96
|
+
'.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.otf': 'font/otf',
|
|
97
|
+
'.json': 'application/json', '.pdf': 'application/pdf',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// ── Main ─────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
(async () => {
|
|
103
|
+
fs.mkdirSync(OUT_DIR, { recursive: true });
|
|
104
|
+
|
|
105
|
+
// 1. Render the page in a real browser
|
|
106
|
+
console.log('Launching browser...');
|
|
107
|
+
const browser = await puppeteer.launch({
|
|
108
|
+
headless: true,
|
|
109
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
110
|
+
});
|
|
111
|
+
const page = await browser.newPage();
|
|
112
|
+
await page.setViewport({ width: 1440, height: 900 });
|
|
113
|
+
|
|
114
|
+
console.log('Navigating to', SITE_URL);
|
|
115
|
+
await page.goto(SITE_URL, { waitUntil: 'networkidle0', timeout: 30000 });
|
|
116
|
+
|
|
117
|
+
// Scroll to bottom to trigger lazy-loaded content
|
|
118
|
+
await page.evaluate(async () => {
|
|
119
|
+
await new Promise(resolve => {
|
|
120
|
+
let total = 0;
|
|
121
|
+
const timer = setInterval(() => {
|
|
122
|
+
window.scrollBy(0, 400);
|
|
123
|
+
total += 400;
|
|
124
|
+
if (total >= document.body.scrollHeight) { clearInterval(timer); resolve(); }
|
|
125
|
+
}, 100);
|
|
126
|
+
setTimeout(() => { clearInterval(timer); resolve(); }, 5000);
|
|
127
|
+
});
|
|
128
|
+
window.scrollTo(0, 0);
|
|
129
|
+
});
|
|
130
|
+
await new Promise(r => setTimeout(r, 1000)); // let lazy content settle
|
|
131
|
+
|
|
132
|
+
// 2. Capture the fully rendered DOM
|
|
133
|
+
let html = await page.content();
|
|
134
|
+
const pageHeight = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
135
|
+
console.log('Captured rendered DOM (' + (html.length / 1024).toFixed(0) + ' KB, page height: ' + pageHeight + 'px)');
|
|
136
|
+
|
|
137
|
+
// 3. Capture computed styles and inline them (prevents style loss)
|
|
138
|
+
const inlineStyles = await page.evaluate(() => {
|
|
139
|
+
const styles = [];
|
|
140
|
+
for (const sheet of document.styleSheets) {
|
|
141
|
+
try {
|
|
142
|
+
const rules = Array.from(sheet.cssRules || []).map(r => r.cssText).join('\\n');
|
|
143
|
+
if (rules) styles.push(rules);
|
|
144
|
+
} catch { /* cross-origin stylesheet, skip */ }
|
|
145
|
+
}
|
|
146
|
+
return styles.join('\\n');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await browser.close();
|
|
150
|
+
|
|
151
|
+
// 4. Find all external URLs in the rendered HTML + inline styles
|
|
152
|
+
const combined = html + '\\n' + inlineStyles;
|
|
153
|
+
const urlPattern = /(?:src|href|srcset)=[\"']?(https?:\\/\\/[^\"'\\s,>]+)[\"']?|url\\([\"']?(https?:\\/\\/[^\"')]+)[\"']?\\)/g;
|
|
154
|
+
const skipDomains = ['google-analytics.com', 'googletagmanager.com', 'segment.com', 'hotjar.com', 'facebook.net', 'events.framer.com'];
|
|
155
|
+
const assetExtensions = /\\.(css|js|mjs|json|png|jpe?g|gif|webp|svg|ico|woff2?|ttf|otf|eot|mp4|webm|pdf)(\\?|$)/i;
|
|
156
|
+
|
|
157
|
+
const assets = new Map();
|
|
158
|
+
let match;
|
|
159
|
+
while ((match = urlPattern.exec(combined)) !== null) {
|
|
160
|
+
const url = (match[1] || match[2] || '').split(/[\\s,]/)[0]; // handle srcset
|
|
161
|
+
if (!url || assets.has(url)) continue;
|
|
162
|
+
try { if (skipDomains.some(d => url.includes(d))) continue; } catch {}
|
|
163
|
+
if (!assetExtensions.test(url) && !url.includes('font') && !url.includes('woff')) continue;
|
|
164
|
+
const assetPath = urlToAssetPath(url);
|
|
165
|
+
if (assetPath) assets.set(url, assetPath);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log('Found', assets.size, 'assets to download');
|
|
169
|
+
|
|
170
|
+
// 5. Download each asset
|
|
171
|
+
let downloaded = 0, failed = 0;
|
|
172
|
+
for (const [url, relPath] of assets) {
|
|
173
|
+
const outPath = path.join(OUT_DIR, relPath);
|
|
174
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
175
|
+
try {
|
|
176
|
+
const data = await download(url);
|
|
177
|
+
fs.writeFileSync(outPath, data);
|
|
178
|
+
downloaded++;
|
|
179
|
+
} catch (e) {
|
|
180
|
+
console.error(' FAILED:', url.substring(0, 80), e.message);
|
|
181
|
+
failed++;
|
|
182
|
+
assets.delete(url); // remove so we don't rewrite to a missing file
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
console.log('Downloaded:', downloaded, '| Failed:', failed);
|
|
186
|
+
|
|
187
|
+
// 6. Rewrite URLs in HTML AND in downloaded text files (CSS, JS, MJS)
|
|
188
|
+
const rewriteTargets = [{ content: html, path: path.join(OUT_DIR, 'index.html') }];
|
|
189
|
+
for (const [, relPath] of assets) {
|
|
190
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
191
|
+
if (['.css', '.js', '.mjs', '.json', '.svg'].includes(ext)) {
|
|
192
|
+
const filePath = path.join(OUT_DIR, relPath);
|
|
193
|
+
if (fs.existsSync(filePath)) {
|
|
194
|
+
rewriteTargets.push({ content: fs.readFileSync(filePath, 'utf8'), path: filePath });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const target of rewriteTargets) {
|
|
200
|
+
let content = target.content;
|
|
201
|
+
for (const [url, relPath] of assets) {
|
|
202
|
+
// In the main HTML, use relative paths directly
|
|
203
|
+
// In asset files, compute the relative path from the asset to the referenced asset
|
|
204
|
+
if (target.path.endsWith('index.html')) {
|
|
205
|
+
content = content.split(url).join(relPath);
|
|
206
|
+
} else {
|
|
207
|
+
const fromDir = path.dirname(target.path.replace(OUT_DIR + '/', ''));
|
|
208
|
+
const toPath = relPath;
|
|
209
|
+
const rel = path.relative(fromDir, toPath);
|
|
210
|
+
content = content.split(url).join(rel);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (target.path.endsWith('index.html')) html = content;
|
|
214
|
+
fs.writeFileSync(target.path, content);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 7. Inject captured styles into the HTML (ensures styles survive without external sheets)
|
|
218
|
+
if (inlineStyles) {
|
|
219
|
+
const styleTag = '<style id=\"captured-styles\">' + inlineStyles + '</style>';
|
|
220
|
+
if (html.includes('</head>')) {
|
|
221
|
+
html = html.replace('</head>', styleTag + '</head>');
|
|
222
|
+
} else {
|
|
223
|
+
html = styleTag + html;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 8. Clean up HTML
|
|
228
|
+
html = html.replace(/<base[^>]*>/gi, '');
|
|
229
|
+
html = html.replace(/<meta[^>]*Content-Security-Policy[^>]*>/gi, '');
|
|
230
|
+
html = html.replace(/<script[^>]*(?:analytics|gtag|segment|hotjar|events\\.framer)[^>]*>.*?<\\/script>/gis, '');
|
|
231
|
+
html = html.replace(/navigator\\.serviceWorker\\.register\\([^)]*\\)/g, '');
|
|
232
|
+
html = html.replace(/\\/\\/# sourceMappingURL=[^\\n]*/g, '');
|
|
233
|
+
|
|
234
|
+
// 9. Write final HTML
|
|
235
|
+
fs.writeFileSync(path.join(OUT_DIR, 'index.html'), html);
|
|
236
|
+
|
|
237
|
+
// 10. Write manifest
|
|
238
|
+
const manifest = Array.from(assets.entries()).map(([url, relPath]) => ({
|
|
239
|
+
asset_path: relPath,
|
|
240
|
+
file_path: path.join(OUT_DIR, relPath),
|
|
241
|
+
exists: fs.existsSync(path.join(OUT_DIR, relPath)),
|
|
242
|
+
})).filter(a => a.exists);
|
|
243
|
+
fs.writeFileSync(path.join(OUT_DIR, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
244
|
+
|
|
245
|
+
console.log('\\nOutput:', OUT_DIR);
|
|
246
|
+
console.log('HTML:', path.join(OUT_DIR, 'index.html'), '(' + (html.length / 1024).toFixed(0) + ' KB)');
|
|
247
|
+
console.log('Page height:', pageHeight, 'px (use this for frame height)');
|
|
248
|
+
console.log('Manifest:', manifest.length, 'assets ready to upload');
|
|
249
|
+
})().catch(e => { console.error(e); process.exit(1); });
|
|
250
|
+
"
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
This outputs:
|
|
254
|
+
- `/tmp/site-import/index.html` — fully rendered HTML with relative paths and captured styles
|
|
255
|
+
- `/tmp/site-import/assets/...` — all downloaded assets (with URLs rewritten inside CSS/JS files too)
|
|
256
|
+
- `/tmp/site-import/manifest.json` — asset list for the upload step
|
|
257
|
+
- The page height in pixels (use this for the frame height)
|
|
258
|
+
|
|
259
|
+
**Key improvements over a plain HTTP fetch:**
|
|
260
|
+
- Renders the page in a real browser — captures SPA content (React, Framer, Webflow, etc.)
|
|
261
|
+
- Scrolls to trigger lazy-loaded images and content
|
|
262
|
+
- Captures computed CSS and inlines it — styles survive even if external sheets fail
|
|
263
|
+
- Rewrites URLs inside CSS and JS files, not just HTML
|
|
264
|
+
- Handles `srcset` attributes
|
|
265
|
+
|
|
266
|
+
## Step 4: Write the frame
|
|
267
|
+
|
|
268
|
+
Use `file_path` to write the HTML directly from disk — this handles large files that won't fit in a `content` parameter:
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
write({
|
|
272
|
+
path: "/designs/my-site/homepage.html",
|
|
273
|
+
file_path: "/tmp/site-import/index.html",
|
|
274
|
+
width: 1440,
|
|
275
|
+
height: <page-height-from-script-output>
|
|
276
|
+
})
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The `write` tool reads `.html` files as inline content (not binary), so the frame gets proper base tag injection and all relative asset paths will resolve. Note the frame ID in the response — you'll use it in the next step.
|
|
280
|
+
|
|
281
|
+
**Use the page height from the script output** as the frame height so the full page is visible without scrolling within the frame.
|
|
282
|
+
|
|
283
|
+
## Step 5: Upload assets
|
|
284
|
+
|
|
285
|
+
Read `manifest.json` and batch upload all assets:
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
batch({
|
|
289
|
+
operations: [
|
|
290
|
+
{ tool: "upload_asset", asset_path: "assets/fonts.gstatic.com/s/outfit/v14/abc.woff2", file_path: "/tmp/site-import/assets/fonts.gstatic.com/s/outfit/v14/abc.woff2", frame_id: "<frame-id>" },
|
|
291
|
+
{ tool: "upload_asset", asset_path: "assets/cdn.example.com/img/hero.png", file_path: "/tmp/site-import/assets/cdn.example.com/img/hero.png", frame_id: "<frame-id>" },
|
|
292
|
+
// ... one entry per asset from manifest.json
|
|
293
|
+
]
|
|
294
|
+
})
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Associate assets with the frame via `frame_id` so they're cleaned up if the frame is deleted.
|
|
298
|
+
|
|
299
|
+
**For large manifests (30+ assets):** Split into batches of 20-30 operations to avoid timeouts.
|
|
300
|
+
|
|
301
|
+
## Step 6: Verify
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
screenshot({ target: "/designs/my-site/homepage.html", fullPage: true })
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Check for missing images, broken styles, or layout issues. If assets are missing, check `list_assets({ frame_id: "..." })` against the manifest.
|
|
308
|
+
|
|
309
|
+
## For local build directories
|
|
310
|
+
|
|
311
|
+
If importing from a local build output (e.g., `./dist` from Vite, `./build` from CRA):
|
|
312
|
+
|
|
313
|
+
1. Skip the Puppeteer step — read `index.html` directly
|
|
314
|
+
2. Find all asset references in the HTML
|
|
315
|
+
3. Copy assets to `/tmp/site-import/assets/` preserving relative paths
|
|
316
|
+
4. `write` the HTML + `batch upload_asset` the assets
|
|
317
|
+
|
|
318
|
+
The Puppeteer approach is only needed for live URLs where JS rendering matters.
|
|
319
|
+
|
|
320
|
+
## Common issues
|
|
321
|
+
|
|
322
|
+
| Problem | Cause | Fix |
|
|
323
|
+
|---------|-------|-----|
|
|
324
|
+
| Blank frame | CSP meta tag not stripped | Check the cleanup regex caught it |
|
|
325
|
+
| Missing images | `srcset` or `<picture>` not captured | Add patterns for these to the URL regex |
|
|
326
|
+
| Broken layout | Computed styles not captured | Check the inlined `<style id="captured-styles">` tag |
|
|
327
|
+
| 404 on assets | URL rewriting missed some references | Check downloaded CSS/JS files for un-rewritten absolute URLs |
|
|
328
|
+
| Frame too short | Used default height instead of page height | Use the page height from script output |
|
|
329
|
+
| Puppeteer not found | Not installed | Run `npm install puppeteer` first |
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
<drafted>
|
|
2
|
-
You have Drafted MCP tools — a shared surface for durable, reviewable work: produce substantive output as frames on the surface (not only in chat), put knowledge in the org wiki, and encode repeatable methods as skills. The full operating manual is the `drafted` skill installed with the plugin — follow it when working with Drafted. Address orgs by path: `fs(ls, path="/")` lists them, and `/o/<org>/<root>/...` (root ∈ wiki, skills, projects) is the canonical path form — the org is part of the path, never a separate switch. Before writing, verify the org/project echoed in the response is the one you intend.
|
|
3
|
-
</drafted>
|
package/cli/google-setup.sh
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Connect the user's OWN Google account to their local agent, through Google's
|
|
3
|
-
# Workspace CLI (`gws`, npm @googleworkspace/cli) and a Cloud project THEY own.
|
|
4
|
-
#
|
|
5
|
-
# Why not Drafted's Google connection: full-inbox read needs a restricted Gmail
|
|
6
|
-
# scope, and a restricted scope on Drafted's consent screen means an annual,
|
|
7
|
-
# paid CASA assessment (see GOOGLE_SCOPES in server/lib/google-auth.mjs). On the
|
|
8
|
-
# user's own project the scope is theirs, the token stays on this machine
|
|
9
|
-
# (encrypted, OS keyring), and Drafted's servers never see a message. The agent
|
|
10
|
-
# reads source material with `gws`; Drafted holds the work it produces.
|
|
11
|
-
#
|
|
12
|
-
# Run by install-mcp.sh (interactive installs only) and by `drafted google-setup`.
|
|
13
|
-
# --from-installer remember a "no" so the auto-updater's re-runs never re-ask
|
|
14
|
-
set -e
|
|
15
|
-
|
|
16
|
-
FROM_INSTALLER=0
|
|
17
|
-
[ "${1:-}" = "--from-installer" ] && FROM_INSTALLER=1
|
|
18
|
-
|
|
19
|
-
BOLD="\033[1m"; DIM="\033[2m"; GREEN="\033[32m"; YELLOW="\033[33m"; RED="\033[31m"; RESET="\033[0m"
|
|
20
|
-
say() { echo -e " $1"; }
|
|
21
|
-
ok() { echo -e " ${GREEN}✓${RESET} $1"; }
|
|
22
|
-
warn() { echo -e " ${YELLOW}!${RESET} $1"; }
|
|
23
|
-
# Prompts read the terminal, not stdin (stdin may be a pipe). DRAFTED_TTY lets
|
|
24
|
-
# the test feed answers from a file.
|
|
25
|
-
TTY="${DRAFTED_TTY:-/dev/tty}"
|
|
26
|
-
exec 3<"$TTY" 2>/dev/null || exec 3</dev/null
|
|
27
|
-
ask() { local reply; read -r -p " $1 " reply <&3 || reply=""; echo "$reply"; }
|
|
28
|
-
pause() { read -r -p " $1 — press Enter when done " _ <&3 || true; }
|
|
29
|
-
open_url() {
|
|
30
|
-
say "${DIM}$1${RESET}"
|
|
31
|
-
if command -v open >/dev/null 2>&1; then open "$1" >/dev/null 2>&1 || true
|
|
32
|
-
elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$1" >/dev/null 2>&1 || true
|
|
33
|
-
fi
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
DRAFTED_HOME="$HOME/.drafted"
|
|
37
|
-
DECLINED_MARKER="$DRAFTED_HOME/google-setup.declined"
|
|
38
|
-
NPM_GLOBAL_PREFIX="${NPM_GLOBAL_PREFIX:-$DRAFTED_HOME/npm-global}"
|
|
39
|
-
GWS_CONFIG_DIR="${GOOGLE_WORKSPACE_CLI_CONFIG_DIR:-$HOME/.config/gws}"
|
|
40
|
-
# Read-only, deliberately: the agent reads with gws, and EVERY send goes through
|
|
41
|
-
# Drafted's send_email action, which the user approves in the Drafted UI. A
|
|
42
|
-
# send-capable gws token would be a way around that approval.
|
|
43
|
-
GWS_SERVICES="gmail,drive,calendar,docs,sheets"
|
|
44
|
-
APIS="gmail.googleapis.com,drive.googleapis.com,calendar-json.googleapis.com,docs.googleapis.com,sheets.googleapis.com"
|
|
45
|
-
|
|
46
|
-
# `gws auth status` prints JSON; read ONE field without echoing the rest (it
|
|
47
|
-
# carries the account's client id and scopes, which don't belong in a log).
|
|
48
|
-
gws_status_field() {
|
|
49
|
-
"$GWS" auth status 2>/dev/null | node -e '
|
|
50
|
-
let s = ""; process.stdin.on("data", (d) => (s += d)).on("end", () => {
|
|
51
|
-
let j = {}; try { j = JSON.parse(s); } catch {}
|
|
52
|
-
const f = process.argv[1];
|
|
53
|
-
if (f === "gmail") return console.log((j.has_refresh_token && (j.scopes || []).some((x) => x.includes("/auth/gmail"))) ? "yes" : "no");
|
|
54
|
-
console.log(j[f] === undefined || j[f] === null ? "" : String(j[f]));
|
|
55
|
-
});' "$1"
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
# Replies to email threads you connect to a project are noticed on THIS computer:
|
|
59
|
-
# `drafted mail-watch` checks them every 5 minutes with your read-only gws sign-in
|
|
60
|
-
# and tells Drafted only that a new message arrived. Idempotent.
|
|
61
|
-
install_watcher() {
|
|
62
|
-
# DRAFTED_NO_WATCHER: tests must never load a real LaunchAgent.
|
|
63
|
-
[ -z "${DRAFTED_NO_WATCHER:-}" ] && [ "$(uname -s)" = "Darwin" ] || return 0
|
|
64
|
-
local drafted_bin; drafted_bin="$(command -v drafted 2>/dev/null || echo "$NPM_GLOBAL_PREFIX/bin/drafted")"
|
|
65
|
-
[ -x "$drafted_bin" ] || return 0
|
|
66
|
-
"$drafted_bin" mail-watch --install >/dev/null 2>&1 && ok "Checking connected email threads for replies every 5 minutes" || true
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
# ── The gws CLI ─────────────────────────────────────────────────
|
|
70
|
-
# Resolve Google's gws specifically: Homebrew's `gws` formula is an unrelated
|
|
71
|
-
# git-workspace tool that installs a binary of the same name.
|
|
72
|
-
GWS=""
|
|
73
|
-
for candidate in "$NPM_GLOBAL_PREFIX/bin/gws" "$(command -v gws 2>/dev/null || true)"; do
|
|
74
|
-
if [ -n "$candidate" ] && [ -x "$candidate" ] && "$candidate" --help 2>&1 | grep -q "Google Workspace"; then
|
|
75
|
-
GWS="$candidate"; break
|
|
76
|
-
fi
|
|
77
|
-
done
|
|
78
|
-
# Already connected: nothing to ask. Checked BEFORE the consent prompt so an
|
|
79
|
-
# interactive installer re-run doesn't re-pitch a finished setup.
|
|
80
|
-
if [ -n "$GWS" ] && [ "$(gws_status_field gmail)" = "yes" ]; then
|
|
81
|
-
ok "Google is connected for your agent (gws)"
|
|
82
|
-
install_watcher
|
|
83
|
-
exit 0
|
|
84
|
-
fi
|
|
85
|
-
|
|
86
|
-
# ── Consent ───────────────────────────────────────────────────────
|
|
87
|
-
echo ""
|
|
88
|
-
echo -e " ${BOLD}Let your agent read your Gmail, Drive and Calendar${RESET} ${DIM}(optional, ~5 minutes)${RESET}"
|
|
89
|
-
say "Your agent (Claude or Codex) can then find the work already sitting in your inbox"
|
|
90
|
-
say "and files, and build it out on your Drafted surface."
|
|
91
|
-
say "${DIM}• Uses a Google Cloud project that YOU own — not a Drafted app.${RESET}"
|
|
92
|
-
say "${DIM}• Read-only. The login token is stored encrypted on this machine only.${RESET}"
|
|
93
|
-
say "${DIM}• Drafted's servers never receive your mail. Only what you ask your agent to${RESET}"
|
|
94
|
-
say "${DIM} put on a surface is uploaded.${RESET}"
|
|
95
|
-
answer="$(ask "Set this up now? [y/N]")"
|
|
96
|
-
case "$answer" in
|
|
97
|
-
[yY]*) /bin/rm -f "$DECLINED_MARKER" ;;
|
|
98
|
-
*)
|
|
99
|
-
[ "$FROM_INSTALLER" = 1 ] && { mkdir -p "$DRAFTED_HOME"; : > "$DECLINED_MARKER"; }
|
|
100
|
-
say "${DIM}Skipped. Run ${BOLD}drafted google-setup${RESET}${DIM} any time.${RESET}"
|
|
101
|
-
exit 0
|
|
102
|
-
;;
|
|
103
|
-
esac
|
|
104
|
-
|
|
105
|
-
if [ -z "$GWS" ]; then
|
|
106
|
-
say "Installing Google's Workspace CLI (gws)..."
|
|
107
|
-
npm install -g @googleworkspace/cli --prefix "$NPM_GLOBAL_PREFIX" >/dev/null
|
|
108
|
-
GWS="$NPM_GLOBAL_PREFIX/bin/gws"
|
|
109
|
-
fi
|
|
110
|
-
ok "gws $("$GWS" --version 2>/dev/null | head -1 | sed 's/^gws //')"
|
|
111
|
-
|
|
112
|
-
# ── Your Cloud project + OAuth client ───────────────────────────
|
|
113
|
-
account_type="$(ask "Is this a Google Workspace (work/school) account, or a personal @gmail.com? [w/p]")"
|
|
114
|
-
|
|
115
|
-
if [ "$(gws_status_field client_config_exists)" != "true" ]; then
|
|
116
|
-
if command -v gcloud >/dev/null 2>&1; then
|
|
117
|
-
say "gcloud found — ${BOLD}gws auth setup${RESET} will sign you in, create a project and enable the APIs."
|
|
118
|
-
"$GWS" auth setup || warn "gws auth setup did not finish — continuing with the manual steps."
|
|
119
|
-
else
|
|
120
|
-
say "${DIM}gcloud not installed — doing the same steps in the Cloud Console instead.${RESET}"
|
|
121
|
-
fi
|
|
122
|
-
fi
|
|
123
|
-
|
|
124
|
-
PROJECT_ID="$(gws_status_field project_id)"
|
|
125
|
-
[ -z "$PROJECT_ID" ] && command -v gcloud >/dev/null 2>&1 && PROJECT_ID="$(gcloud config get-value project 2>/dev/null || true)"
|
|
126
|
-
if [ -z "$PROJECT_ID" ]; then
|
|
127
|
-
say "Create a Cloud project (any name, e.g. ${BOLD}my-agent${RESET}):"
|
|
128
|
-
open_url "https://console.cloud.google.com/projectcreate"
|
|
129
|
-
PROJECT_ID="$(ask "Paste the project ID:")"
|
|
130
|
-
[ -z "$PROJECT_ID" ] && { warn "No project ID — stopping. Run ${BOLD}drafted google-setup${RESET} to resume."; exit 1; }
|
|
131
|
-
fi
|
|
132
|
-
ok "Project $PROJECT_ID"
|
|
133
|
-
|
|
134
|
-
if [ "$(gws_status_field client_config_exists)" != "true" ]; then
|
|
135
|
-
say ""
|
|
136
|
-
say "${BOLD}a.${RESET} Enable the Gmail, Drive, Calendar, Docs and Sheets APIs (click Enable):"
|
|
137
|
-
open_url "https://console.cloud.google.com/flows/enableapi?apiid=$APIS&project=$PROJECT_ID"
|
|
138
|
-
pause "APIs enabled"
|
|
139
|
-
say "${BOLD}b.${RESET} Name the app (e.g. ${BOLD}My agent${RESET}) and pick your own email as support contact:"
|
|
140
|
-
open_url "https://console.cloud.google.com/auth/branding?project=$PROJECT_ID"
|
|
141
|
-
case "$account_type" in
|
|
142
|
-
[wW]*) say " Audience: choose ${BOLD}Internal${RESET} — only your organisation, no review needed." ;;
|
|
143
|
-
*) say " Audience: choose ${BOLD}External${RESET}." ;;
|
|
144
|
-
esac
|
|
145
|
-
pause "Branding saved"
|
|
146
|
-
say "${BOLD}c.${RESET} Create an OAuth client: type ${BOLD}Desktop app${RESET} → Create → ${BOLD}Download JSON${RESET}:"
|
|
147
|
-
open_url "https://console.cloud.google.com/auth/clients/create?project=$PROJECT_ID"
|
|
148
|
-
pause "JSON downloaded"
|
|
149
|
-
secret="$(ls -t "$HOME"/Downloads/client_secret_*.json 2>/dev/null | head -1 || true)"
|
|
150
|
-
if [ -z "$secret" ]; then
|
|
151
|
-
secret="$(ask "Couldn't find client_secret_*.json in Downloads. Path to the file:")"
|
|
152
|
-
fi
|
|
153
|
-
[ -f "$secret" ] || { warn "No client file — stopping. Run ${BOLD}drafted google-setup${RESET} to resume."; exit 1; }
|
|
154
|
-
mkdir -p "$GWS_CONFIG_DIR"
|
|
155
|
-
# Moved, not copied: a client secret shouldn't be left lying in Downloads.
|
|
156
|
-
/bin/mv -f "$secret" "$GWS_CONFIG_DIR/client_secret.json"
|
|
157
|
-
chmod 600 "$GWS_CONFIG_DIR/client_secret.json"
|
|
158
|
-
ok "OAuth client saved"
|
|
159
|
-
fi
|
|
160
|
-
|
|
161
|
-
# External apps left in "Testing" get refresh tokens Google expires after
|
|
162
|
-
# 7 days. Publishing the user's OWN unverified app is allowed for personal use
|
|
163
|
-
# (Google caps unverified apps at 100 users) and makes the token long-lived.
|
|
164
|
-
case "$account_type" in
|
|
165
|
-
[wW]*) ;;
|
|
166
|
-
*)
|
|
167
|
-
say ""
|
|
168
|
-
say "${BOLD}d.${RESET} Click ${BOLD}Publish app${RESET} → Confirm. Without this Google logs you out every 7 days."
|
|
169
|
-
open_url "https://console.cloud.google.com/auth/audience?project=$PROJECT_ID"
|
|
170
|
-
pause "App published"
|
|
171
|
-
;;
|
|
172
|
-
esac
|
|
173
|
-
|
|
174
|
-
# ── Sign in ─────────────────────────────────────────────────────
|
|
175
|
-
say ""
|
|
176
|
-
say "Signing in. Google will say ${BOLD}\"Google hasn't verified this app\"${RESET} — it's your own app:"
|
|
177
|
-
say "click ${BOLD}Advanced → Go to <your app> (unsafe)${RESET}, then allow read access."
|
|
178
|
-
"$GWS" auth login --readonly -s "$GWS_SERVICES" || true
|
|
179
|
-
|
|
180
|
-
if [ "$(gws_status_field gmail)" = "yes" ]; then
|
|
181
|
-
ok "Google connected — your agent can now read Gmail, Drive and Calendar via ${BOLD}gws${RESET}"
|
|
182
|
-
install_watcher
|
|
183
|
-
exit 0
|
|
184
|
-
fi
|
|
185
|
-
echo -e " ${RED}✗${RESET} Sign-in didn't complete. Run ${BOLD}drafted google-setup${RESET} to try again."
|
|
186
|
-
exit 1
|