drafted 1.5.0 → 1.7.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.
Files changed (2) hide show
  1. package/mcp/server.mjs +49 -19
  2. package/package.json +1 -1
package/mcp/server.mjs CHANGED
@@ -97,7 +97,7 @@ const server = new McpServer({
97
97
 
98
98
  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.
99
99
 
100
- WORKFLOW: project(action="list") → project(action="open") → ls / → read/write/edit. Projects span all orgs -- opening a 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.
100
+ WORKFLOW: project(action="list") → project(action="open") → ls / → read/write/edit. Projects span all orgs -- opening a 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. To switch orgs without opening a project (for wiki/skill work in an org with no projects), call get_org(action="switch", orgId=...).
101
101
 
102
102
  SKILLS: Drafted has a skill library -- reusable agent instructions stored as SKILL.md files. When a user says "use the X skill", call skill(action="search") to find it, then skill(action="load") to get its instructions. Skills can cover anything: UX guidelines, copywriting rules, brand voice, coding standards, review checklists, etc.
103
103
 
@@ -793,7 +793,7 @@ tool('auth', 'Sign in to Drafted. `action=get_link` returns a verification URL i
793
793
 
794
794
  // ── Project management tools (direct HTTP) ────────────────────────
795
795
 
796
- tool('project', 'START HERE for project management. Dispatch by `action`: list (lists all projects across all orgs — always call first), open (switch the active project; required before reading/writing frames), create (new project, optionally from a template), update (change name/folder/description/layers), move (transfer to another org). The org switches automatically when you open a project. **Skill gate:** projects with attached skills will REJECT all mutations (write, edit, mv, rm, shape, group, connector, layout, layer, asset upload) until you have loaded each attached skill via skill(action="load"). Skills tell you HOW to do the work — they\'re not optional. Open returns the attached skill list and auto-inlines content for projects with ≤3 skills.', {
796
+ tool('project', 'START HERE for project management. Dispatch by `action`: list (lists all projects across all orgs — always call first), open (switch the active project; required before reading/writing frames), create (new project, optionally from a template), update (change name/folder/description/layers), move (transfer to another org). Opening a project auto-switches the org. To change orgs WITHOUT a project (for wiki/skill work in an empty org), use get_org(action="switch", orgId=...). **Skill gate:** projects with attached skills will REJECT all mutations (write, edit, mv, rm, shape, group, connector, layout, layer, asset upload) until you have loaded each attached skill via skill(action="load"). Skills tell you HOW to do the work — they\'re not optional. Open returns the attached skill list and auto-inlines content for projects with ≤3 skills.', {
797
797
  action: z.enum(['list', 'open', 'create', 'update', 'move']).describe('Operation to perform.'),
798
798
  projectId: z.string().optional().describe('[open|update|move] project ID. Get IDs from action=list.'),
799
799
  name: z.string().optional().describe('[create|update] project name'),
@@ -1157,8 +1157,29 @@ tool('layer', 'Manage layers in a project. Dispatch by `action`: add/update/remo
1157
1157
  } catch (error) { return err(error); }
1158
1158
  });
1159
1159
 
1160
- tool('get_org', {}, async () => {
1160
+ tool('get_org', {
1161
+ action: z.enum(['get', 'switch']).optional().describe('Default: "get" returns active org and member info. Use "switch" with orgId to change the active org without opening a project.'),
1162
+ orgId: z.string().optional().describe('[switch] target org ID to switch to. Must be one of the orgs the user is a member of.'),
1163
+ }, async (args = {}) => {
1161
1164
  try {
1165
+ const action = args.action || 'get';
1166
+
1167
+ if (action === 'switch') {
1168
+ if (!args.orgId) throw new Error('orgId is required for action=switch');
1169
+ await api('POST', '/auth/switch-org', { orgId: args.orgId });
1170
+ // Bust the per-session org cache so subsequent calls re-fetch /auth/me.
1171
+ const sess = getSessionState();
1172
+ sess.cachedOrgId = null;
1173
+ sess.cachedOrgIdTime = 0;
1174
+ // Clear active project too — projects are scoped to orgs, so the
1175
+ // previous one isn't valid in the new org.
1176
+ setMcpActiveProject(null, null);
1177
+ const me = await api('GET', '/auth/me');
1178
+ const orgs = (await api('GET', '/api/orgs')).orgs || [];
1179
+ const activeOrg = (orgs || []).map(o => ({ id: o.orgId || o.id, name: o.orgName || o.name })).find(o => o.id === me?.orgId) || null;
1180
+ return ok({ switched: true, activeOrg, note: 'Active org switched. Wiki and skill calls now target this org. Active project cleared — open a project (or stay org-scoped for wiki/skill).' });
1181
+ }
1182
+
1162
1183
  // Source of truth = THIS MCP session's bound org (what mutations will actually
1163
1184
  // hit). Each session is independent on purpose — multiple agents can run in
1164
1185
  // parallel against different orgs. /auth/me reads sessions.org_id directly.
@@ -1180,7 +1201,7 @@ tool('get_org', {}, async () => {
1180
1201
  activeOrg,
1181
1202
  orgs,
1182
1203
  members: members.map(m => ({ id: m.userId, name: m.username, email: m.email, role: m.role })),
1183
- note: "activeOrg is the org bound to THIS MCP session — what mutations will actually target. Browser tabs and other MCP sessions for the same user can be on different orgs.",
1204
+ note: "activeOrg is the org bound to THIS MCP session — what mutations will actually target. To switch orgs without creating a project, call get_org(action=\"switch\", orgId=\"...\"). Browser tabs and other MCP sessions for the same user can be on different orgs.",
1184
1205
  });
1185
1206
  } catch (error) { return err(error); }
1186
1207
  });
@@ -1545,8 +1566,8 @@ tool('batch', 'Batch operations on the ACTIVE PROJECT. Response includes "projec
1545
1566
 
1546
1567
  // ── Asset tools ──────────────────────────────────────────────────
1547
1568
 
1548
- tool('asset', 'Manage supporting files (CSS, JS, images, fonts) in the ACTIVE PROJECT. Assets are referenced by frames via relative paths — e.g., if your HTML has <link href="css/styles.css">, upload with asset_path="css/styles.css". Assets are NOT frames — they don\'t appear on the canvas. `action=upload` to add/replace, `action=list` to browse.', {
1549
- action: z.enum(['upload', 'list']).describe('Operation to perform.'),
1569
+ tool('asset', 'Manage supporting files (CSS, JS, images, fonts) in the ACTIVE PROJECT. Assets are referenced by frames via relative paths — e.g., if your HTML has <link href="css/styles.css">, upload with asset_path="css/styles.css". Assets are NOT frames — they don\'t appear on the canvas. `action=upload` to add/replace, `action=list` to browse, `action=rm` to delete.', {
1570
+ action: z.enum(['upload', 'list', 'rm']).describe('Operation to perform.'),
1550
1571
  asset_path: z.string().optional().describe('[upload] relative asset path (e.g. "css/styles.css"). Must match the path used in HTML references.'),
1551
1572
  file_path: z.string().optional().describe('[upload] absolute path to a local file. Mutually exclusive with content/base64.'),
1552
1573
  content: z.string().optional().describe('[upload] text content (for CSS/JS). Mutually exclusive with file_path/base64.'),
@@ -1589,6 +1610,14 @@ tool('asset', 'Manage supporting files (CSS, JS, images, fonts) in the ACTIVE PR
1589
1610
  const query = frame_id ? `?frameId=${frame_id}` : '';
1590
1611
  return ok(await api('GET', `/api/projects/${projectId}/assets${query}`));
1591
1612
  }
1613
+ if (action === 'rm') {
1614
+ const skillErr = await checkProjectSkills(projectId);
1615
+ if (skillErr) return err(new Error(skillErr));
1616
+ const { asset_path } = args;
1617
+ if (!asset_path) throw new Error('asset_path is required for action=rm');
1618
+ if (asset_path.includes('..')) throw new Error('asset_path must not contain ".."');
1619
+ return ok(await api('DELETE', `/api/projects/${projectId}/assets/${asset_path}`));
1620
+ }
1592
1621
  throw new Error(`Unknown asset action: ${action}`);
1593
1622
  } catch (error) { return err(error); }
1594
1623
  });
@@ -1960,20 +1989,18 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
1960
1989
  }
1961
1990
 
1962
1991
  // ── read ────────────────────────────────────────────────────
1992
+ // Returns content in hashline format (`lineNum:hash|content`) so the
1993
+ // agent can produce hashline edit operations. Mirrors frame.read.
1963
1994
  case 'read': {
1964
1995
  const { path: readPath, lines: readLines } = args;
1965
1996
  if (!readPath) throw new Error('path required for action=read');
1966
1997
  const normalized = normalizeWikiPath(readPath);
1967
- const page = await api('GET', `/api/wiki/page?path=${encodeURIComponent(normalized)}`);
1968
- // Client-side line slice — server returns full content; we trim if `lines` was passed.
1969
- let outContent = page.content;
1970
- if (readLines && typeof outContent === 'string') {
1971
- const m = readLines.match(/^(\d+)-(\d+)$/);
1972
- if (!m) throw new Error(`lines must be "N-M" (e.g. "10-50"), got: ${readLines}`);
1973
- const [start, end] = [parseInt(m[1]), parseInt(m[2])];
1974
- const allLines = outContent.split('\n');
1975
- outContent = allLines.slice(Math.max(0, start - 1), end).join('\n');
1998
+ const params = new URLSearchParams({ path: normalized, format: 'hashline' });
1999
+ if (readLines) {
2000
+ if (!/^\d+-\d+$/.test(readLines)) throw new Error(`lines must be "N-M" (e.g. "10-50"), got: ${readLines}`);
2001
+ params.set('lines', readLines);
1976
2002
  }
2003
+ const page = await api('GET', `/api/wiki/page?${params.toString()}`);
1977
2004
  // Get backlink count via search (approximate)
1978
2005
  let backlinkCount = 0;
1979
2006
  try {
@@ -1985,7 +2012,8 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
1985
2012
  title: page.title,
1986
2013
  type: page.type,
1987
2014
  frontmatter: page.frontmatter,
1988
- content: outContent,
2015
+ content: page.content,
2016
+ totalLines: page.totalLines,
1989
2017
  lastEditedBy: page.updatedBy,
1990
2018
  lastEditedAt: page.updatedAt,
1991
2019
  backlinkCount,
@@ -2069,15 +2097,17 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
2069
2097
  }
2070
2098
 
2071
2099
  // ── edit ────────────────────────────────────────────────────
2100
+ // Hashlines come from `read` (which now formats content as
2101
+ // `lineNum:hash|content`). The server applies the ops via the same
2102
+ // hashline algorithm — no client-side re-hashing, no algorithm drift.
2072
2103
  case 'edit': {
2073
2104
  const { path: editPath, operations: editOps } = args;
2074
2105
  if (!editPath) throw new Error('path required for action=edit');
2075
2106
  if (!Array.isArray(editOps) || editOps.length === 0) throw new Error('operations (array) required for action=edit');
2076
2107
  const normalized = normalizeWikiPath(editPath);
2077
2108
  const page = await api('GET', `/api/wiki/page?path=${encodeURIComponent(normalized)}`);
2078
- const newContent = applyHashlineOps(page.content || '', editOps);
2079
- const result = await api('PATCH', `/api/wiki/pages/${page.id}`, { content: newContent });
2080
- return ok(withOrg({ path: result.path, id: result.id, updated: true }));
2109
+ const result = await api('POST', `/api/wiki/pages/${page.id}/edit`, { operations: editOps });
2110
+ return ok(withOrg({ path: result.path, id: result.id, updated: true, applied: result.applied }));
2081
2111
  }
2082
2112
 
2083
2113
  // ── mv ──────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Drafted CLI - Design preview server for Claude agents",
5
5
  "type": "module",
6
6
  "files": [