drafted 1.0.0 → 1.1.1
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/install-mcp.sh +192 -0
- package/mcp/server.mjs +156 -26
- package/package.json +49 -14
- package/README.md +0 -32
- /package/{shared → src/shared}/constants.mjs +0 -0
package/install-mcp.sh
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Drafted — Design workspace for AI agents
|
|
5
|
+
# This script installs the Drafted MCP server and CLI globally via npm,
|
|
6
|
+
# then registers it with Claude Desktop, Claude Code, and Cursor.
|
|
7
|
+
#
|
|
8
|
+
# Run with:
|
|
9
|
+
# curl -fsSL https://drafted.live/install.sh | bash
|
|
10
|
+
|
|
11
|
+
SERVER="https://drafted.live"
|
|
12
|
+
|
|
13
|
+
BOLD="\033[1m"
|
|
14
|
+
DIM="\033[2m"
|
|
15
|
+
GREEN="\033[32m"
|
|
16
|
+
YELLOW="\033[33m"
|
|
17
|
+
RED="\033[31m"
|
|
18
|
+
CYAN="\033[36m"
|
|
19
|
+
RESET="\033[0m"
|
|
20
|
+
|
|
21
|
+
step=0
|
|
22
|
+
step() {
|
|
23
|
+
step=$((step + 1))
|
|
24
|
+
echo ""
|
|
25
|
+
echo -e "${CYAN}[$step]${RESET} ${BOLD}$1${RESET}"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
ok() {
|
|
29
|
+
echo -e " ${GREEN}✓${RESET} $1"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fail() {
|
|
33
|
+
echo -e " ${RED}✗${RESET} $1"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# ── Welcome ───────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
echo ""
|
|
39
|
+
echo -e "${BOLD}Welcome to Drafted${RESET}"
|
|
40
|
+
echo -e "This will set up Drafted so Claude can create designs for you."
|
|
41
|
+
echo -e "It only takes a minute."
|
|
42
|
+
|
|
43
|
+
# ── Prerequisites ─────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
step "Checking your system"
|
|
46
|
+
|
|
47
|
+
# Node.js
|
|
48
|
+
if command -v node &>/dev/null; then
|
|
49
|
+
NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
|
|
50
|
+
if [ "$NODE_VERSION" -lt 22 ]; then
|
|
51
|
+
fail "Node.js is too old (found $(node -v), need 22+)."
|
|
52
|
+
echo ""
|
|
53
|
+
echo -e " Update it by running: ${BOLD}brew upgrade node${RESET}"
|
|
54
|
+
echo -e " Then re-run this installer."
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
ok "Node.js $(node -v)"
|
|
58
|
+
else
|
|
59
|
+
fail "Node.js is not installed."
|
|
60
|
+
echo ""
|
|
61
|
+
if command -v brew &>/dev/null; then
|
|
62
|
+
echo -e " Install it by running: ${BOLD}brew install node${RESET}"
|
|
63
|
+
else
|
|
64
|
+
echo -e " Download it from: ${BOLD}https://nodejs.org${RESET}"
|
|
65
|
+
fi
|
|
66
|
+
echo -e " Then re-run this installer."
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# ── Install ───────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
step "Installing Drafted"
|
|
73
|
+
|
|
74
|
+
npm install -g drafted@latest --silent 2>/dev/null
|
|
75
|
+
ok "Installed $(drafted --version 2>/dev/null || echo 'drafted') via npm"
|
|
76
|
+
|
|
77
|
+
# ── Configure ─────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
step "Connecting to your tools"
|
|
80
|
+
|
|
81
|
+
# Write server URL config
|
|
82
|
+
mkdir -p "$HOME/.drafted"
|
|
83
|
+
echo "{\"server\":\"$SERVER\"}" > "$HOME/.drafted/config.json"
|
|
84
|
+
ok "Server: $SERVER"
|
|
85
|
+
|
|
86
|
+
# Resolve the installed binary path for MCP configs
|
|
87
|
+
DRAFTED_MCP_BIN="$(which drafted-mcp 2>/dev/null || echo "drafted-mcp")"
|
|
88
|
+
|
|
89
|
+
configure_mcp() {
|
|
90
|
+
local config_path="$1"
|
|
91
|
+
local label="$2"
|
|
92
|
+
local config_dir
|
|
93
|
+
config_dir="$(dirname "$config_path")"
|
|
94
|
+
mkdir -p "$config_dir"
|
|
95
|
+
|
|
96
|
+
node -e "
|
|
97
|
+
const fs = require('fs');
|
|
98
|
+
const p = process.argv[1];
|
|
99
|
+
let c = {};
|
|
100
|
+
try { c = JSON.parse(fs.readFileSync(p, 'utf8')); } catch {}
|
|
101
|
+
if (!c.mcpServers) c.mcpServers = {};
|
|
102
|
+
c.mcpServers.drafted = {
|
|
103
|
+
command: 'drafted-mcp',
|
|
104
|
+
args: []
|
|
105
|
+
};
|
|
106
|
+
fs.writeFileSync(p, JSON.stringify(c, null, 2) + '\n');
|
|
107
|
+
" "$config_path"
|
|
108
|
+
ok "$label"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
CLAUDE_DESKTOP_CONFIG="$HOME/Library/Application Support/Claude/claude_desktop_config.json"
|
|
112
|
+
CLAUDE_CODE_CONFIG="$HOME/.claude.json"
|
|
113
|
+
CURSOR_CONFIG="$HOME/.cursor/mcp.json"
|
|
114
|
+
|
|
115
|
+
CONFIGURED=false
|
|
116
|
+
|
|
117
|
+
# Claude Desktop
|
|
118
|
+
if [ -d "/Applications/Claude.app" ] || [ -d "$HOME/Applications/Claude.app" ]; then
|
|
119
|
+
configure_mcp "$CLAUDE_DESKTOP_CONFIG" "Claude Desktop"
|
|
120
|
+
CONFIGURED=true
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# Claude Code
|
|
124
|
+
if command -v claude &>/dev/null; then
|
|
125
|
+
configure_mcp "$CLAUDE_CODE_CONFIG" "Claude Code"
|
|
126
|
+
CONFIGURED=true
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# Cursor
|
|
130
|
+
if [ -d "/Applications/Cursor.app" ] || [ -d "$HOME/Applications/Cursor.app" ] || command -v cursor &>/dev/null; then
|
|
131
|
+
configure_mcp "$CURSOR_CONFIG" "Cursor"
|
|
132
|
+
CONFIGURED=true
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# If nothing detected, pre-configure all
|
|
136
|
+
if [ "$CONFIGURED" = false ]; then
|
|
137
|
+
echo -e " ${YELLOW}No supported editors detected — pre-configuring all.${RESET}"
|
|
138
|
+
configure_mcp "$CLAUDE_DESKTOP_CONFIG" "Claude Desktop (pre-configured)"
|
|
139
|
+
configure_mcp "$CLAUDE_CODE_CONFIG" "Claude Code (pre-configured)"
|
|
140
|
+
configure_mcp "$CURSOR_CONFIG" "Cursor (pre-configured)"
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# ── Skills ───────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
step "Installing skills"
|
|
146
|
+
|
|
147
|
+
# Find the installed package's skills directory
|
|
148
|
+
DRAFTED_PKG_DIR="$(node -e "try { console.log(require.resolve('drafted/package.json').replace('/package.json','')) } catch { process.exit(1) }" 2>/dev/null)" || true
|
|
149
|
+
|
|
150
|
+
if [ -n "$DRAFTED_PKG_DIR" ] && [ -d "$DRAFTED_PKG_DIR/skills" ]; then
|
|
151
|
+
SKILLS_DIR="$HOME/.claude/skills"
|
|
152
|
+
mkdir -p "$SKILLS_DIR"
|
|
153
|
+
INSTALLED=0
|
|
154
|
+
# Copy individual skill files
|
|
155
|
+
for f in "$DRAFTED_PKG_DIR/skills/"*.md; do
|
|
156
|
+
[ -f "$f" ] || continue
|
|
157
|
+
cp "$f" "$SKILLS_DIR/"
|
|
158
|
+
INSTALLED=$((INSTALLED + 1))
|
|
159
|
+
done
|
|
160
|
+
# Copy skill directories
|
|
161
|
+
for d in "$DRAFTED_PKG_DIR/skills/"*/; do
|
|
162
|
+
[ -d "$d" ] || continue
|
|
163
|
+
DIRNAME="$(basename "$d")"
|
|
164
|
+
rm -rf "$SKILLS_DIR/$DIRNAME"
|
|
165
|
+
cp -r "$d" "$SKILLS_DIR/$DIRNAME"
|
|
166
|
+
INSTALLED=$((INSTALLED + 1))
|
|
167
|
+
done
|
|
168
|
+
ok "Installed $INSTALLED skills to $SKILLS_DIR"
|
|
169
|
+
else
|
|
170
|
+
echo -e " ${YELLOW}Skills directory not found in package — skipping.${RESET}"
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
# ── Done ─────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
echo ""
|
|
176
|
+
echo ""
|
|
177
|
+
echo -e "${GREEN}${BOLD}You're all set!${RESET}"
|
|
178
|
+
echo ""
|
|
179
|
+
echo -e " ${DIM}View your designs at:${RESET} ${BOLD}https://drafted.live${RESET}"
|
|
180
|
+
echo -e " ${DIM}To update:${RESET} npm install -g drafted@latest"
|
|
181
|
+
echo -e " ${DIM}To uninstall:${RESET} npm uninstall -g drafted && rm -rf ~/.drafted"
|
|
182
|
+
echo ""
|
|
183
|
+
echo -e "${YELLOW}${BOLD}"
|
|
184
|
+
echo " ┌─────────────────────────────────────────────────────────┐"
|
|
185
|
+
echo " │ │"
|
|
186
|
+
echo " │ >>> RESTART YOUR EDITOR TO ACTIVATE DRAFTED <<< │"
|
|
187
|
+
echo " │ │"
|
|
188
|
+
echo " │ Close and reopen Claude Desktop, Claude Code, │"
|
|
189
|
+
echo " │ or Cursor so it picks up the new MCP server. │"
|
|
190
|
+
echo " │ │"
|
|
191
|
+
echo " └─────────────────────────────────────────────────────────┘"
|
|
192
|
+
echo -e "${RESET}"
|
package/mcp/server.mjs
CHANGED
|
@@ -14,18 +14,18 @@ import { join, dirname, basename, extname, resolve } from 'path';
|
|
|
14
14
|
import { homedir } from 'os';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
16
|
import { z } from 'zod';
|
|
17
|
-
import { LAYERS } from '../shared/constants.mjs';
|
|
17
|
+
import { LAYERS } from '../src/shared/constants.mjs';
|
|
18
18
|
|
|
19
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
20
|
|
|
21
21
|
const server = new McpServer({
|
|
22
22
|
name: 'drafted',
|
|
23
|
-
version: '2.
|
|
23
|
+
version: '2.4.0',
|
|
24
24
|
description: `Multi-tenant design workspace. Structure: Organization → Projects → Layers → Lanes → Frames.
|
|
25
25
|
|
|
26
26
|
An org contains projects. Each project has a zoomable canvas with frames (HTML files) organized as /{layer}/{lane}/{filename}. Layers are predefined categories (wireframes, designs, brand-assets, etc.), lanes are groups within a layer, and frames are the individual design files.
|
|
27
27
|
|
|
28
|
-
WORKFLOW: list_projects → open_project → ls / → read/write/edit. Every response includes a "project" field showing which project you're operating on
|
|
28
|
+
WORKFLOW: list_projects → open_project → ls / → read/write/edit. Projects span all orgs -- open_project auto-switches org context. Every response includes a "project" field showing which project you're operating on -- always verify it matches your intent before writing.
|
|
29
29
|
|
|
30
30
|
IMPORTANT: Any URL containing /f/{uuid} is a Drafted frame link — ALWAYS use read(path=URL) to get frame content, focus(target=URL) to pan the canvas to it. Never curl or WebFetch Drafted URLs.`,
|
|
31
31
|
});
|
|
@@ -95,6 +95,16 @@ async function ensureSession() {
|
|
|
95
95
|
await cloneSession();
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
const MIME_MAP = {
|
|
99
|
+
'.css': 'text/css', '.js': 'application/javascript', '.mjs': 'application/javascript',
|
|
100
|
+
'.json': 'application/json', '.html': 'text/html', '.htm': 'text/html',
|
|
101
|
+
'.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
102
|
+
'.gif': 'image/gif', '.webp': 'image/webp', '.ico': 'image/x-icon',
|
|
103
|
+
'.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.otf': 'font/otf', '.eot': 'application/vnd.ms-fontobject',
|
|
104
|
+
'.pdf': 'application/pdf', '.mp4': 'video/mp4', '.webm': 'video/webm',
|
|
105
|
+
};
|
|
106
|
+
function mimeFromExt(ext) { return MIME_MAP[ext?.toLowerCase()] || 'application/octet-stream'; }
|
|
107
|
+
|
|
98
108
|
async function api(method, path, body, _retried) {
|
|
99
109
|
await ensureSession();
|
|
100
110
|
const pid = agentActiveProjectId;
|
|
@@ -370,7 +380,7 @@ server.tool('login', 'Authenticate with Drafted. Opens a browser for the user to
|
|
|
370
380
|
|
|
371
381
|
// ── Project management tools (direct HTTP) ────────────────────────
|
|
372
382
|
|
|
373
|
-
server.tool('list_projects', 'START HERE. Lists all projects
|
|
383
|
+
server.tool('list_projects', 'START HERE. Lists all projects across all orgs. Use this first to find the project you need, then open_project to switch to it (org switches automatically).', {}, async () => {
|
|
374
384
|
try {
|
|
375
385
|
const data = await api('GET', '/api/projects');
|
|
376
386
|
data.agentProject = agentActiveProjectId || null;
|
|
@@ -460,10 +470,19 @@ server.tool('open_project', 'Switch active project. REQUIRED before reading or w
|
|
|
460
470
|
if (proj?.slug) projectSlug = proj.slug;
|
|
461
471
|
} catch { /* fall back to projectId */ }
|
|
462
472
|
const url = `${base}/project/${projectSlug}`;
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
473
|
+
// Navigate existing browser tabs instead of opening new ones
|
|
474
|
+
let navigated = 0;
|
|
475
|
+
try {
|
|
476
|
+
const nav = await api('POST', '/api/project/navigate', { projectId });
|
|
477
|
+
navigated = nav.navigated || 0;
|
|
478
|
+
} catch { /* server may not support navigate yet */ }
|
|
479
|
+
// Only open a new tab if no browser tabs were reached
|
|
480
|
+
if (navigated === 0) {
|
|
481
|
+
const { exec } = await import('child_process');
|
|
482
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
483
|
+
exec(`${cmd} ${JSON.stringify(url)}`);
|
|
484
|
+
}
|
|
485
|
+
return ok({ ...result, url, opened: true, navigated });
|
|
467
486
|
} catch (error) { return err(error); }
|
|
468
487
|
});
|
|
469
488
|
|
|
@@ -733,13 +752,20 @@ server.tool('write', 'Write a frame to the ACTIVE PROJECT. Check the "project" f
|
|
|
733
752
|
|
|
734
753
|
let body;
|
|
735
754
|
if (file_path) {
|
|
736
|
-
// Binary upload: read file from disk, send as base64
|
|
737
755
|
const resolved = resolve(file_path);
|
|
738
756
|
if (!existsSync(resolved)) throw new Error(`File not found: ${resolved}`);
|
|
739
|
-
const buffer = readFileSync(resolved);
|
|
740
757
|
const ext = extname(resolved).toLowerCase();
|
|
741
|
-
const
|
|
742
|
-
|
|
758
|
+
const TEXT_EXTS = ['.html', '.htm', '.svg', '.md', '.markdown', '.txt', '.css', '.js', '.mjs', '.json', '.xml'];
|
|
759
|
+
if (TEXT_EXTS.includes(ext)) {
|
|
760
|
+
// Text file: read as content so it's stored inline (enables base tag injection for HTML)
|
|
761
|
+
body = { content: readFileSync(resolved, 'utf8') };
|
|
762
|
+
if (autoSize) body.autoSize = true;
|
|
763
|
+
} else {
|
|
764
|
+
// Binary upload: read file from disk, send as base64
|
|
765
|
+
const buffer = readFileSync(resolved);
|
|
766
|
+
const MIME = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.pdf': 'application/pdf' };
|
|
767
|
+
body = { base64: buffer.toString('base64'), contentType: MIME[ext] || 'application/octet-stream' };
|
|
768
|
+
}
|
|
743
769
|
} else {
|
|
744
770
|
body = { content };
|
|
745
771
|
if (autoSize) body.autoSize = true;
|
|
@@ -853,10 +879,10 @@ server.tool('mv', 'Move/rename a frame within the ACTIVE PROJECT. Response inclu
|
|
|
853
879
|
|
|
854
880
|
server.tool('batch', 'Batch operations on the ACTIVE PROJECT. Response includes "project" field. Use open_project first if needed.', {
|
|
855
881
|
operations: z.array(z.object({
|
|
856
|
-
tool: z.enum(['write', 'rm', 'mv', 'edit']).describe('Tool to execute'),
|
|
882
|
+
tool: z.enum(['write', 'rm', 'mv', 'edit', 'upload_asset']).describe('Tool to execute'),
|
|
857
883
|
path: z.string().optional().describe('Path (for write, rm, edit)'),
|
|
858
884
|
content: z.string().optional().describe('Content (for write). Mutually exclusive with file_path.'),
|
|
859
|
-
file_path: z.string().optional().describe('Absolute path to a local file to upload (for write). Mutually exclusive with content.'),
|
|
885
|
+
file_path: z.string().optional().describe('Absolute path to a local file to upload (for write, upload_asset). Mutually exclusive with content.'),
|
|
860
886
|
color: z.string().optional().describe('CSS color for frame border (for write)'),
|
|
861
887
|
from: z.string().optional().describe('Source path (for mv)'),
|
|
862
888
|
to: z.string().optional().describe('Destination path (for mv)'),
|
|
@@ -865,6 +891,9 @@ server.tool('batch', 'Batch operations on the ACTIVE PROJECT. Response includes
|
|
|
865
891
|
lineHash: z.string(),
|
|
866
892
|
newContent: z.string().optional(),
|
|
867
893
|
})).optional().describe('Edit operations (for edit). Always include lineNum.'),
|
|
894
|
+
asset_path: z.string().optional().describe('Relative asset path (for upload_asset, e.g., "css/styles.css")'),
|
|
895
|
+
content_type: z.string().optional().describe('MIME type (for upload_asset, auto-detected if omitted)'),
|
|
896
|
+
frame_id: z.string().optional().describe('Frame ID to associate asset with (for upload_asset)'),
|
|
868
897
|
})).describe('ALWAYS use batch instead of multiple individual tool calls. Applies the same change to many files, writes multiple frames, or combines writes+edits+deletes. One canvas refresh instead of many. Example: editing 5 wireframes to remove a section = one batch with 5 edit operations.'),
|
|
869
898
|
}, async ({ operations }) => {
|
|
870
899
|
try {
|
|
@@ -881,20 +910,121 @@ server.tool('batch', 'Batch operations on the ACTIVE PROJECT. Response includes
|
|
|
881
910
|
}
|
|
882
911
|
|
|
883
912
|
// Resolve file_path → base64 for write operations before sending to server
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
913
|
+
// Separate asset uploads from frame operations
|
|
914
|
+
const assetOps = operations.filter(op => op.tool === 'upload_asset');
|
|
915
|
+
const frameOps = operations.filter(op => op.tool !== 'upload_asset');
|
|
916
|
+
|
|
917
|
+
const results = [];
|
|
918
|
+
|
|
919
|
+
// Handle asset uploads via the asset API
|
|
920
|
+
for (const op of assetOps) {
|
|
921
|
+
try {
|
|
922
|
+
if (!op.asset_path) throw new Error('asset_path is required for upload_asset');
|
|
923
|
+
if (op.asset_path.includes('..')) throw new Error('asset_path must not contain ".."');
|
|
924
|
+
|
|
925
|
+
let b64, ct;
|
|
926
|
+
if (op.file_path) {
|
|
927
|
+
const resolved = resolve(op.file_path);
|
|
928
|
+
if (!existsSync(resolved)) throw new Error(`File not found: ${resolved}`);
|
|
929
|
+
const buffer = readFileSync(resolved);
|
|
930
|
+
b64 = buffer.toString('base64');
|
|
931
|
+
ct = op.content_type || mimeFromExt(extname(resolved));
|
|
932
|
+
} else if (op.content != null) {
|
|
933
|
+
b64 = Buffer.from(op.content).toString('base64');
|
|
934
|
+
ct = op.content_type || mimeFromExt(extname(op.asset_path));
|
|
935
|
+
} else {
|
|
936
|
+
throw new Error('upload_asset requires file_path or content');
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const body = { base64: b64, contentType: ct };
|
|
940
|
+
if (op.frame_id) body.frameId = op.frame_id;
|
|
941
|
+
|
|
942
|
+
const projectId = agentActiveProjectId;
|
|
943
|
+
if (!projectId) throw new Error('No active project. Call open_project first.');
|
|
944
|
+
const r = await api('PUT', `/api/projects/${projectId}/assets/${op.asset_path}`, body);
|
|
945
|
+
results.push({ ok: true, tool: 'upload_asset', asset_path: op.asset_path, ...r });
|
|
946
|
+
} catch (e) {
|
|
947
|
+
results.push({ ok: false, tool: 'upload_asset', asset_path: op.asset_path, error: e.message });
|
|
893
948
|
}
|
|
894
|
-
|
|
895
|
-
});
|
|
949
|
+
}
|
|
896
950
|
|
|
897
|
-
|
|
951
|
+
// Handle frame operations via the batch API
|
|
952
|
+
if (frameOps.length > 0) {
|
|
953
|
+
const TEXT_EXTS = ['.html', '.htm', '.svg', '.md', '.markdown', '.txt', '.css', '.js', '.mjs', '.json', '.xml'];
|
|
954
|
+
const resolvedOps = frameOps.map(op => {
|
|
955
|
+
if (op.tool === 'write' && op.file_path) {
|
|
956
|
+
const resolved = resolve(op.file_path);
|
|
957
|
+
if (!existsSync(resolved)) throw new Error(`File not found: ${resolved}`);
|
|
958
|
+
const ext = extname(resolved).toLowerCase();
|
|
959
|
+
const { file_path: _, ...rest } = op;
|
|
960
|
+
if (TEXT_EXTS.includes(ext)) {
|
|
961
|
+
return { ...rest, content: readFileSync(resolved, 'utf8') };
|
|
962
|
+
}
|
|
963
|
+
const buffer = readFileSync(resolved);
|
|
964
|
+
const MIME = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml', '.pdf': 'application/pdf' };
|
|
965
|
+
return { ...rest, base64: buffer.toString('base64'), contentType: MIME[ext] || 'application/octet-stream' };
|
|
966
|
+
}
|
|
967
|
+
return op;
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
const batchResult = await api('POST', '/api/fs/batch', { operations: resolvedOps });
|
|
971
|
+
if (batchResult.results) results.push(...batchResult.results);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
return ok({ ok: true, results });
|
|
975
|
+
} catch (error) { return err(error); }
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
// ── Asset tools ──────────────────────────────────────────────────
|
|
979
|
+
|
|
980
|
+
server.tool('upload_asset', 'Upload a supporting file (CSS, JS, image, font) to the ACTIVE PROJECT. Assets are referenced by frames via relative paths — e.g., if your HTML has <link href="css/styles.css">, upload the asset with asset_path="css/styles.css". Assets are NOT frames — they don\'t appear on the canvas. Use batch with upload_asset operations for bulk uploads.', {
|
|
981
|
+
asset_path: z.string().describe('Relative path for the asset (e.g., "css/styles.css", "js/app.js", "img/logo.png"). This must match the path used in HTML references.'),
|
|
982
|
+
file_path: z.string().optional().describe('Absolute path to a local file to upload. Mutually exclusive with content/base64.'),
|
|
983
|
+
content: z.string().optional().describe('Text content (for CSS/JS files). Mutually exclusive with file_path.'),
|
|
984
|
+
base64: z.string().optional().describe('Base64-encoded binary content. Mutually exclusive with file_path and content.'),
|
|
985
|
+
content_type: z.string().optional().describe('MIME type (auto-detected from extension if omitted)'),
|
|
986
|
+
frame_id: z.string().optional().describe('Frame ID to associate this asset with (for cleanup when the frame is deleted). Get this from the write tool response.'),
|
|
987
|
+
}, async ({ asset_path, file_path, content, base64: rawBase64, content_type, frame_id }) => {
|
|
988
|
+
try {
|
|
989
|
+
if (!asset_path) throw new Error('asset_path is required');
|
|
990
|
+
if (asset_path.includes('..')) throw new Error('asset_path must not contain ".."');
|
|
991
|
+
|
|
992
|
+
let b64, ct;
|
|
993
|
+
if (file_path) {
|
|
994
|
+
const resolved = resolve(file_path);
|
|
995
|
+
if (!existsSync(resolved)) throw new Error(`File not found: ${resolved}`);
|
|
996
|
+
const buffer = readFileSync(resolved);
|
|
997
|
+
b64 = buffer.toString('base64');
|
|
998
|
+
ct = content_type || mimeFromExt(extname(resolved));
|
|
999
|
+
} else if (content != null) {
|
|
1000
|
+
b64 = Buffer.from(content).toString('base64');
|
|
1001
|
+
ct = content_type || mimeFromExt(extname(asset_path));
|
|
1002
|
+
} else if (rawBase64) {
|
|
1003
|
+
b64 = rawBase64;
|
|
1004
|
+
ct = content_type || mimeFromExt(extname(asset_path));
|
|
1005
|
+
} else {
|
|
1006
|
+
throw new Error('Provide file_path, content, or base64');
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const body = { base64: b64, contentType: ct };
|
|
1010
|
+
if (frame_id) body.frameId = frame_id;
|
|
1011
|
+
|
|
1012
|
+
const projectId = agentActiveProjectId;
|
|
1013
|
+
if (!projectId) throw new Error('No active project. Call open_project first.');
|
|
1014
|
+
|
|
1015
|
+
const result = await api('PUT', `/api/projects/${projectId}/assets/${asset_path}`, body);
|
|
1016
|
+
return ok(result);
|
|
1017
|
+
} catch (error) { return err(error); }
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
server.tool('list_assets', 'List all assets in the ACTIVE PROJECT, optionally filtered by frame.', {
|
|
1021
|
+
frame_id: z.string().optional().describe('Filter assets by frame ID'),
|
|
1022
|
+
}, async ({ frame_id }) => {
|
|
1023
|
+
try {
|
|
1024
|
+
const projectId = agentActiveProjectId;
|
|
1025
|
+
if (!projectId) throw new Error('No active project. Call open_project first.');
|
|
1026
|
+
const query = frame_id ? `?frameId=${frame_id}` : '';
|
|
1027
|
+
const result = await api('GET', `/api/projects/${projectId}/assets${query}`);
|
|
898
1028
|
return ok(result);
|
|
899
1029
|
} catch (error) { return err(error); }
|
|
900
1030
|
});
|
package/package.json
CHANGED
|
@@ -1,37 +1,72 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Drafted CLI - Design preview server for Claude agents",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"cli/",
|
|
8
|
+
"mcp/",
|
|
9
|
+
"src/shared/",
|
|
10
|
+
"install-mcp.sh"
|
|
11
|
+
],
|
|
6
12
|
"bin": {
|
|
7
13
|
"drafted": "./cli/drafted.mjs",
|
|
8
14
|
"drafted-mcp": "./mcp/server.mjs"
|
|
9
15
|
},
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
+
"scripts": {
|
|
17
|
+
"dev": "./dev.sh",
|
|
18
|
+
"dev:server": "tsx watch server/server.mjs",
|
|
19
|
+
"dev:local": "node server/server-local.mjs",
|
|
20
|
+
"install-global": "npm install -g .",
|
|
21
|
+
"db:migrate": "psql $DATABASE_URL -f drizzle/migrate.sql",
|
|
22
|
+
"db:push": "drizzle-kit push",
|
|
23
|
+
"db:studio": "drizzle-kit studio",
|
|
24
|
+
"migrate-fs": "tsx server/migrate-fs.ts",
|
|
25
|
+
"db:clean-sessions": "tsx src/auth/cleanup.ts",
|
|
26
|
+
"import:legacy": "tsx scripts/import-legacy.mts",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"test:watch": "vitest"
|
|
29
|
+
},
|
|
16
30
|
"dependencies": {
|
|
31
|
+
"@aws-sdk/client-s3": "^3.1007.0",
|
|
32
|
+
"@aws-sdk/s3-request-presigner": "^3.1008.0",
|
|
17
33
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
18
34
|
"commander": "^11.1.0",
|
|
35
|
+
"dagre": "^0.8.5",
|
|
36
|
+
"dotenv": "^17.3.1",
|
|
37
|
+
"drizzle-orm": "^0.45.1",
|
|
38
|
+
"express": "^4.18.2",
|
|
39
|
+
"multer": "^2.1.1",
|
|
40
|
+
"nodemailer": "^8.0.2",
|
|
41
|
+
"pg": "^8.20.0",
|
|
42
|
+
"playwright": "^1.58.2",
|
|
43
|
+
"puppeteer": "^24.37.5",
|
|
19
44
|
"qrcode-terminal": "^0.12.0",
|
|
45
|
+
"sharp": "^0.34.5",
|
|
20
46
|
"ws": "^8.16.0",
|
|
21
47
|
"zod": "^4.3.6"
|
|
22
48
|
},
|
|
23
49
|
"keywords": [
|
|
24
|
-
"drafted",
|
|
25
50
|
"design",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
51
|
+
"canvas",
|
|
52
|
+
"preview",
|
|
53
|
+
"html",
|
|
54
|
+
"claude"
|
|
30
55
|
],
|
|
31
56
|
"author": "ddfourtwo",
|
|
32
57
|
"repository": {
|
|
33
58
|
"type": "git",
|
|
34
59
|
"url": "git+https://github.com/ddfourtwo/drafted.git"
|
|
35
60
|
},
|
|
36
|
-
"license": "MIT"
|
|
61
|
+
"license": "MIT",
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/express": "^5.0.6",
|
|
64
|
+
"@types/nodemailer": "^7.0.11",
|
|
65
|
+
"@types/pg": "^8.18.0",
|
|
66
|
+
"drizzle-kit": "^0.31.9",
|
|
67
|
+
"tsx": "^4.19.0",
|
|
68
|
+
"typescript": "^5.9.3",
|
|
69
|
+
"vitest": "^3.2.4",
|
|
70
|
+
"yaml": "^2.8.3"
|
|
71
|
+
}
|
|
37
72
|
}
|
package/README.md
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# Drafted
|
|
2
|
-
|
|
3
|
-
Design workspace for AI agents. MCP server and CLI for creating and managing designs on a collaborative surface.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install -g drafted
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Usage
|
|
12
|
-
|
|
13
|
-
### As an MCP server (for Claude Desktop, Claude Code, Cursor)
|
|
14
|
-
|
|
15
|
-
The MCP server is automatically available after install:
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
drafted-mcp
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
### As a CLI
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
drafted login
|
|
25
|
-
drafted ls
|
|
26
|
-
drafted write designs/default/hero.html < design.html
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Links
|
|
30
|
-
|
|
31
|
-
- **App**: https://drafted.live
|
|
32
|
-
- **Docs**: https://drafted.live
|
|
File without changes
|