drafted 1.1.1 → 1.1.2

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.
Files changed (2) hide show
  1. package/mcp/server.mjs +216 -14
  2. package/package.json +1 -1
package/mcp/server.mjs CHANGED
@@ -27,6 +27,8 @@ An org contains projects. Each project has a zoomable canvas with frames (HTML f
27
27
 
28
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
+ SKILLS: Drafted has a skill library -- reusable agent instructions stored as SKILL.md files. When a user says "use the X skill", use search_skills to find it, then load_skill to get its instructions. Skills can cover anything: UX guidelines, copywriting rules, brand voice, coding standards, review checklists, etc.
31
+
30
32
  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
33
  });
32
34
 
@@ -190,7 +192,11 @@ async function connectAgentWs() {
190
192
  });
191
193
  }
192
194
 
193
- function joinAgentWsRoom(projectId) {
195
+ async function joinAgentWsRoom(projectId) {
196
+ // Ensure WS is connected (may not be after restart/session re-clone)
197
+ if (!agentWs || agentWs.readyState > WebSocket.OPEN) {
198
+ await connectAgentWs();
199
+ }
194
200
  if (!agentWs) return;
195
201
  const msg = JSON.stringify({ type: 'join', projectId, agent: true });
196
202
  if (agentWs.readyState === WebSocket.OPEN) {
@@ -384,6 +390,20 @@ server.tool('list_projects', 'START HERE. Lists all projects across all orgs. Us
384
390
  try {
385
391
  const data = await api('GET', '/api/projects');
386
392
  data.agentProject = agentActiveProjectId || null;
393
+ // Inject favorited skills so the agent knows about them from the first call
394
+ try {
395
+ const favData = await api('GET', '/api/skills/favorites');
396
+ const favs = favData.skills || [];
397
+ if (favs.length > 0) {
398
+ data.favoritedSkills = favs.map(s => ({
399
+ id: s.id,
400
+ name: s.name,
401
+ slug: s.slug,
402
+ description: s.description,
403
+ tags: s.tags,
404
+ }));
405
+ }
406
+ } catch { /* skills not available */ }
387
407
  return ok(data);
388
408
  } catch (error) { return err(error); }
389
409
  });
@@ -456,7 +476,8 @@ server.tool('fork_template', {
456
476
 
457
477
  server.tool('open_project', 'Switch active project. REQUIRED before reading or writing — all fs tools (ls, read, write, edit, rm, mv, batch) operate on the active project. Get project IDs from list_projects.', {
458
478
  projectId: z.string().describe('Project ID to switch to and open in browser'),
459
- }, async ({ projectId }) => {
479
+ skipBrowser: z.boolean().optional().describe('Skip opening/navigating a browser tab (use when the user already has the project open, e.g. from an invite snippet)'),
480
+ }, async ({ projectId, skipBrowser }) => {
460
481
  try {
461
482
  const result = await api('POST', '/api/project/switch', { projectId });
462
483
  setMcpActiveProject(projectId);
@@ -470,19 +491,39 @@ server.tool('open_project', 'Switch active project. REQUIRED before reading or w
470
491
  if (proj?.slug) projectSlug = proj.slug;
471
492
  } catch { /* fall back to projectId */ }
472
493
  const url = `${base}/project/${projectSlug}`;
473
- // Navigate existing browser tabs instead of opening new ones
474
494
  let navigated = 0;
495
+ if (!skipBrowser) {
496
+ // Navigate existing browser tabs instead of opening new ones
497
+ try {
498
+ const nav = await api('POST', '/api/project/navigate', { projectId });
499
+ navigated = nav.navigated || 0;
500
+ } catch { /* server may not support navigate yet */ }
501
+ // Only open a new tab if no browser tabs were reached
502
+ if (navigated === 0) {
503
+ const { exec } = await import('child_process');
504
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
505
+ exec(`${cmd} ${JSON.stringify(url)}`);
506
+ }
507
+ }
508
+ // Fetch attached skills (metadata only)
509
+ let projectSkillsList = [];
475
510
  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)}`);
511
+ const skillData = await api('GET', `/api/projects/${projectId}/skills`);
512
+ projectSkillsList = skillData.skills || [];
513
+ } catch { /* skills not available yet */ }
514
+
515
+ // Auto-load content for projects with <= 3 attached skills
516
+ if (projectSkillsList.length > 0 && projectSkillsList.length <= 3) {
517
+ for (const s of projectSkillsList) {
518
+ try {
519
+ const full = await api('GET', `/api/skills/${s.id}`);
520
+ s.content = full.content;
521
+ s.files = full.files || [];
522
+ } catch { /* skip */ }
523
+ }
484
524
  }
485
- return ok({ ...result, url, opened: true, navigated });
525
+
526
+ return ok({ ...result, url, opened: true, navigated, skills: projectSkillsList });
486
527
  } catch (error) { return err(error); }
487
528
  });
488
529
 
@@ -604,8 +645,8 @@ server.tool('remove_layer', {
604
645
  const listing = await api('GET', `/api/fs?path=/${key}&projectId=${projectId}&recursive=true`);
605
646
  const entries = listing.entries || [];
606
647
  const frames = entries.filter(e => e.type === 'frame');
607
- // Don't count the auto-generated _context.md as real content
608
- const realFrames = frames.filter(f => !f.path.endsWith('/_meta/_context.md'));
648
+ // Don't count the auto-generated context frame as real content
649
+ const realFrames = frames.filter(f => !f.path.endsWith('/_meta/_context.md') && !f.path.endsWith('/instructions/context.md'));
609
650
  const frameCount = realFrames.length;
610
651
  if (frameCount > 0) {
611
652
  throw new Error(`Layer "${key}" contains ${frameCount} frame(s). Use force: true to confirm deletion.`);
@@ -1029,6 +1070,24 @@ server.tool('list_assets', 'List all assets in the ACTIVE PROJECT, optionally fi
1029
1070
  } catch (error) { return err(error); }
1030
1071
  });
1031
1072
 
1073
+ // ── Shape tool ───────────────────────────────────────────────────────
1074
+
1075
+ server.tool('shape', 'Create a flowchart shape on the surface. Shapes are lightweight text nodes — use them for flowcharts, process diagrams, and decision trees. Connect shapes with the connect tool, then call layout to auto-arrange.', {
1076
+ text: z.string().describe('Text to display inside the shape'),
1077
+ shape: z.enum(['rectangle', 'diamond', 'oval', 'pill']).optional().default('rectangle').describe('Shape type: rectangle (process/action), diamond (decision), oval (start/end), pill (rounded step)'),
1078
+ layer: z.string().optional().describe('Layer to place the shape in (default: plans)'),
1079
+ lane: z.string().optional().describe('Lane within the layer (default: default)'),
1080
+ color: z.string().optional().describe('Border color (CSS color string)'),
1081
+ }, async ({ text, shape, layer, lane, color }) => {
1082
+ try {
1083
+ const body = { text, shape };
1084
+ if (layer) body.layer = layer;
1085
+ if (lane) body.lane = lane;
1086
+ if (color) body.color = color;
1087
+ const result = await api('POST', '/api/fs/shape', body);
1088
+ return ok(result);
1089
+ } catch (error) { return err(error); }
1090
+ });
1032
1091
  // ── Connector tools ───────────────────────────────────────────────
1033
1092
 
1034
1093
  server.tool('connect', 'Create a connector (arrow) between two frames on the surface.', {
@@ -1089,6 +1148,149 @@ server.tool('layout', 'Auto-arrange frames using graph layout algorithm. Positio
1089
1148
  } catch (error) { return err(error); }
1090
1149
  });
1091
1150
 
1151
+ // ── Skill library tools ──────────────────────────────────────────
1152
+
1153
+ server.tool('search_skills', 'Search the Drafted skill library for reusable agent skills (e.g. "UX", "copywriting", "brand"). Skills are stored prompts, guidelines, and instructions that agents can load and follow. When a user says "use the X skill", search here first.', {
1154
+ query: z.string().optional().describe('Search term (matches name, description, content)'),
1155
+ tags: z.array(z.string()).optional().describe('Filter by tags'),
1156
+ scope: z.enum(['all', 'org', 'global']).optional().default('all').describe('Scope: all (default), org, or global'),
1157
+ }, async ({ query, tags, scope }) => {
1158
+ try {
1159
+ const params = new URLSearchParams();
1160
+ if (query) params.set('q', query);
1161
+ if (tags?.length) params.set('tags', tags.join(','));
1162
+ if (scope) params.set('scope', scope);
1163
+ const qs = params.toString();
1164
+ const endpoint = query ? '/api/skills/search' : '/api/skills';
1165
+ const result = await api('GET', `${endpoint}${qs ? '?' + qs : ''}`);
1166
+ return ok(result);
1167
+ } catch (error) { return err(error); }
1168
+ });
1169
+
1170
+ server.tool('load_skill', 'Load a skill to get its full instructions. Returns the SKILL.md content plus any supporting files. Use search_skills to find skills by keyword first.', {
1171
+ skill: z.string().describe('Skill ID (UUID) or slug'),
1172
+ }, async ({ skill }) => {
1173
+ try {
1174
+ const isUuid = /^[a-f0-9-]{36}$/.test(skill);
1175
+ const endpoint = isUuid ? `/api/skills/${skill}` : `/api/skills/slug/${skill}`;
1176
+ const result = await api('GET', endpoint);
1177
+ return ok(result);
1178
+ } catch (error) { return err(error); }
1179
+ });
1180
+
1181
+ server.tool('read_skill_file', 'Read a supporting file from a skill directory (e.g. an example, template, or config). Use load_skill first to see available files.', {
1182
+ skillId: z.string().describe('Skill ID'),
1183
+ path: z.string().describe('Relative file path within the skill (e.g. "examples/react.md")'),
1184
+ }, async ({ skillId, path }) => {
1185
+ try {
1186
+ const result = await api('GET', `/api/skills/${skillId}/files/${path}`);
1187
+ return ok(result);
1188
+ } catch (error) { return err(error); }
1189
+ });
1190
+
1191
+ server.tool('add_skill', 'Create a new skill in the org skill library. Creates a skill directory with a root SKILL.md. Use update_skill_file to add supporting files afterward.', {
1192
+ name: z.string().describe('Skill name'),
1193
+ description: z.string().describe('One-line description of what the skill does'),
1194
+ content: z.string().describe('Root SKILL.md content (markdown with instructions/prompts)'),
1195
+ tags: z.array(z.string()).optional().describe('Tags for discovery'),
1196
+ triggerPatterns: z.array(z.string()).optional().describe('Patterns that suggest this skill (e.g. "designing a landing page", "writing tests")'),
1197
+ }, async ({ name, description, content, tags, triggerPatterns }) => {
1198
+ try {
1199
+ const body = { name, description, content };
1200
+ if (tags) body.tags = tags;
1201
+ if (triggerPatterns) body.triggerPatterns = triggerPatterns;
1202
+ const result = await api('POST', '/api/skills', body);
1203
+ return ok(result);
1204
+ } catch (error) { return err(error); }
1205
+ });
1206
+
1207
+ server.tool('update_skill', 'Update an existing org skill. Can change name, description, content (SKILL.md), tags, or trigger patterns. Cannot edit global skills.', {
1208
+ skillId: z.string().describe('Skill ID to update'),
1209
+ name: z.string().optional().describe('New skill name'),
1210
+ description: z.string().optional().describe('New description'),
1211
+ content: z.string().optional().describe('New root SKILL.md content'),
1212
+ tags: z.array(z.string()).optional().describe('Replace tags'),
1213
+ triggerPatterns: z.array(z.string()).optional().describe('Replace trigger patterns'),
1214
+ }, async ({ skillId, name, description, content, tags, triggerPatterns }) => {
1215
+ try {
1216
+ const body = {};
1217
+ if (name !== undefined) body.name = name;
1218
+ if (description !== undefined) body.description = description;
1219
+ if (content !== undefined) body.content = content;
1220
+ if (tags !== undefined) body.tags = tags;
1221
+ if (triggerPatterns !== undefined) body.triggerPatterns = triggerPatterns;
1222
+ if (Object.keys(body).length === 0) throw new Error('At least one field is required');
1223
+ const result = await api('PUT', `/api/skills/${skillId}`, body);
1224
+ return ok(result);
1225
+ } catch (error) { return err(error); }
1226
+ });
1227
+
1228
+ server.tool('update_skill_file', 'Add or update a supporting file in a skill directory. Use for examples, templates, configs -- anything beyond the root SKILL.md.', {
1229
+ skillId: z.string().describe('Skill ID'),
1230
+ path: z.string().describe('Relative file path (e.g. "examples/react.md", "templates/component.html")'),
1231
+ content: z.string().describe('File content'),
1232
+ }, async ({ skillId, path, content }) => {
1233
+ try {
1234
+ const result = await api('PUT', `/api/skills/${skillId}/files/${path}`, { content });
1235
+ return ok(result);
1236
+ } catch (error) { return err(error); }
1237
+ });
1238
+
1239
+ server.tool('remove_skill', 'Delete a skill from the org library. Also detaches it from all projects. Cannot delete global skills.', {
1240
+ skillId: z.string().describe('Skill ID to delete'),
1241
+ }, async ({ skillId }) => {
1242
+ try {
1243
+ const result = await api('DELETE', `/api/skills/${skillId}`);
1244
+ return ok(result);
1245
+ } catch (error) { return err(error); }
1246
+ });
1247
+
1248
+ server.tool('list_project_skills', 'List skills attached to the active project. These are the skills recommended for this project context.', {}, async () => {
1249
+ try {
1250
+ if (!agentActiveProjectId) throw new Error('No active project. Call open_project first.');
1251
+ const result = await api('GET', `/api/projects/${agentActiveProjectId}/skills`);
1252
+ return ok(result);
1253
+ } catch (error) { return err(error); }
1254
+ });
1255
+
1256
+ server.tool('attach_skill', 'Attach a skill to the active project. Attached skills are auto-loaded when agents open this project.', {
1257
+ skillId: z.string().describe('Skill ID to attach'),
1258
+ }, async ({ skillId }) => {
1259
+ try {
1260
+ if (!agentActiveProjectId) throw new Error('No active project. Call open_project first.');
1261
+ const result = await api('POST', `/api/projects/${agentActiveProjectId}/skills`, { skillId });
1262
+ return ok(result);
1263
+ } catch (error) { return err(error); }
1264
+ });
1265
+
1266
+ server.tool('detach_skill', 'Remove a skill from the active project.', {
1267
+ skillId: z.string().describe('Skill ID to detach'),
1268
+ }, async ({ skillId }) => {
1269
+ try {
1270
+ if (!agentActiveProjectId) throw new Error('No active project. Call open_project first.');
1271
+ const result = await api('DELETE', `/api/projects/${agentActiveProjectId}/skills/${skillId}`);
1272
+ return ok(result);
1273
+ } catch (error) { return err(error); }
1274
+ });
1275
+
1276
+ server.tool('favorite_skill', 'Add a skill to your favorites (max 12). Favorited skills appear in list_projects so agents always know about them.', {
1277
+ skillId: z.string().describe('Skill ID to favorite'),
1278
+ }, async ({ skillId }) => {
1279
+ try {
1280
+ const result = await api('POST', `/api/skills/favorites/${skillId}`);
1281
+ return ok(result);
1282
+ } catch (error) { return err(error); }
1283
+ });
1284
+
1285
+ server.tool('unfavorite_skill', 'Remove a skill from your favorites.', {
1286
+ skillId: z.string().describe('Skill ID to unfavorite'),
1287
+ }, async ({ skillId }) => {
1288
+ try {
1289
+ const result = await api('DELETE', `/api/skills/favorites/${skillId}`);
1290
+ return ok(result);
1291
+ } catch (error) { return err(error); }
1292
+ });
1293
+
1092
1294
  // ── Resource: canvas info ─────────────────────────────────────────
1093
1295
 
1094
1296
  server.resource('info', 'drafted://info', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Drafted CLI - Design preview server for Claude agents",
5
5
  "type": "module",
6
6
  "files": [