drafted 1.12.4 → 1.12.6

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.
@@ -14,9 +14,9 @@ Operating principle:
14
14
 
15
15
  Before working:
16
16
  - Check whether Drafted MCP tools are available. If not authenticated, use auth(action="get_link") or auth(action="login") and give the user the sign-in link.
17
- - Select the correct organization first. Use get_org(action="get") and project(action="list") to identify the target org/project. Opening a project switches org context automatically; for wiki/skill-only work, use get_org(action="switch", orgId=...).
18
- - Check get_org(action="get") for googleDrive.connected. When Google Drive is connected for the active org, Google Workspace frames are available and should be the strong default for documents, spreadsheets, and presentations.
19
- - Never assume the current active Drafted project or organization is correct. Verify the returned project/org before writing.
17
+ - Identify the correct organization and project first. Use get_org(action="get") and project(action="list"). Opening a project binds the agent session's context — the org derives from the project. There is no org switching: address existing resources by UUID (pageId/skillId/projectId — org self-derives), and pass org=... on creates and searches that name no resource (wiki write, skill add, project create, template actions).
18
+ - Check get_org(action="get") for googleDrive.connected. When Google Drive is connected for the org you are working in, Google Workspace frames are available and should be the strong default for documents, spreadsheets, and presentations.
19
+ - Never assume the current Drafted project or org context is correct. Verify the project/org echoed in each response before writing.
20
20
 
21
21
  Wiki rules:
22
22
  - Search the org wiki before substantive work: wiki(action="search") with relevant keywords.
package/mcp/server.mjs CHANGED
@@ -187,7 +187,7 @@ const server = new McpServer({
187
187
 
188
188
  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.
189
189
 
190
- 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=...).
190
+ WORKFLOW: project(action="list") → project(action="open") → ls / → read/write/edit. Projects span all orgs -- opening a project binds this agent session's context, and the org derives from the project. Every response includes a "project" field showing which project you're operating on -- always verify it matches your intent before writing. There is no org switching: address resources by UUID (org self-derives) and pass org=... on creates/searches that name no resource (wiki write, skill add, project create).
191
191
 
192
192
  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.
193
193
 
@@ -223,8 +223,8 @@ const TOOL_ANNOTATIONS = {
223
223
  whoami: { title: 'Session identity', readOnlyHint: true, destructiveHint: false, openWorldHint: false, description: 'Return THIS agent session\'s identity: its server-assigned human-readable name (the correlation key between an agent window and its web-app session tab), sessionId, userId, orgId, active projectId, editor label, server URL, and surfaced/alive state. Read-only. Use this — not guesses from the host environment — to report which session you are.' },
224
224
 
225
225
  // Projects
226
- project: { title: 'Projects', readOnlyHint: false, destructiveHint: false, openWorldHint: false, widgetUri: 'ui://widget/drafted-canvas-overview.html', description: 'Manage projects: list (start here), open (switch active project), create, update, move to another org.' },
227
- get_org: { title: 'Organization', readOnlyHint: false, destructiveHint: false, openWorldHint: false, description: 'Get the active organization (action="get", default), Google Drive availability, or switch to a different org (action="switch", orgId=...). Use switch when you need wiki/skill work in an org that has no projects opening a project also switches, but is unavailable in empty orgs. When googleDrive.connected is true, strongly prefer Google Workspace frames for documents, sheets, and slides.' },
226
+ project: { title: 'Projects', readOnlyHint: false, destructiveHint: false, openWorldHint: false, widgetUri: 'ui://widget/drafted-canvas-overview.html', description: 'Manage projects: list (start here), open (bind this agent session to a project — org derives from it), create (org= names where it is born), update, move to another org.' },
227
+ get_org: { title: 'Organization', readOnlyHint: true, destructiveHint: false, openWorldHint: false, description: 'List your orgs, the default org, and Google Drive availability (action="get", default), or fetch installed MCP update instructions (action="update_mcp"). There is no org switchingorg derives from the resource you address; creates/searches take org=. When googleDrive.connected is true, strongly prefer Google Workspace frames for documents, sheets, and slides.' },
228
228
 
229
229
  // Templates
230
230
  template: { title: 'Templates', readOnlyHint: false, destructiveHint: true, openWorldHint: false, description: 'Manage project templates: list, create, update, delete, fork.' },
@@ -1235,21 +1235,21 @@ async function getCurrentOrgContext() {
1235
1235
  }
1236
1236
 
1237
1237
  // Project-less mutations (wiki, skills) have no resource to derive the org from.
1238
- // If the agent never bound an org this session (no project opened, no get_org
1239
- // switch) and passed no explicit org, refuse to GUESS when the user belongs to
1240
- // more than one org — otherwise the write silently lands in whatever org the
1241
- // session happened to inherit (the "wiki write into the wrong org" failure).
1238
+ // If the agent never bound this session to a project and passed no explicit org,
1239
+ // refuse to GUESS when the user belongs to more than one org — otherwise the
1240
+ // write silently lands in whatever org the session happened to inherit (the
1241
+ // "wiki write into the wrong org" failure).
1242
1242
  // Single-org users are unambiguous and proceed untouched. (DRAFT-36 Phase 4.)
1243
1243
  async function requireBoundOrgForProjectlessMutation(explicitOrg) {
1244
1244
  if (explicitOrg) return;
1245
1245
  const sess = getSessionState();
1246
- if (sess.boundOrgId) return; // bound via project open / get_org switch
1246
+ if (sess.boundOrgId) return; // bound via project open (org = project's org)
1247
1247
  if (getState().projectId) return; // an active project implies its org
1248
1248
  // Remote/web sessions are isolated per connection: each one gets its OWN
1249
1249
  // server-side session row with its own org_id set on connect, so the session
1250
1250
  // org IS this session's binding — there's no shared, long-lived session to
1251
1251
  // confuse here the way stdio has. Adopt that org automatically instead of
1252
- // forcing the agent to call get_org switch / project open before a project-less
1252
+ // forcing the agent to open a project before a project-less
1253
1253
  // wiki or skill write. The DRAFT-36 Phase 4 "refuse to guess" guard below
1254
1254
  // therefore applies to stdio only.
1255
1255
  if (isRemote) {
@@ -1267,11 +1267,11 @@ async function requireBoundOrgForProjectlessMutation(explicitOrg) {
1267
1267
  }
1268
1268
  if (count > 1) {
1269
1269
  throw new Error(
1270
- `Refusing to guess the org: no project is open and no org was bound this ` +
1271
- `session, but you belong to ${count} orgs — a project-less wiki/skill write ` +
1272
- `would land in whichever org the session inherited. Call ` +
1273
- `get_org(action="switch", orgId=...) or project(action="open") first ` +
1274
- `(skill calls may also pass org=...).`
1270
+ `Refusing to guess the org: no project is open and no explicit org was ` +
1271
+ `given, but you belong to ${count} orgs — a project-less wiki/skill write ` +
1272
+ `would land in whichever org the session inherited. Pass org=... on this ` +
1273
+ `call (org name or id), or open the relevant project first ` +
1274
+ `(project(action="open") the org derives from the project).`
1275
1275
  );
1276
1276
  }
1277
1277
  }
@@ -1342,8 +1342,8 @@ function normalizeWikiPath(input) {
1342
1342
  return input.replace(/^\/+/, '').replace(/\.md$/, '');
1343
1343
  }
1344
1344
 
1345
- async function getTreeAsMap() {
1346
- const tree = await api('GET', '/api/wiki/tree');
1345
+ async function getTreeAsMap(orgHeader = {}) {
1346
+ const tree = await api('GET', '/api/wiki/tree', undefined, orgHeader);
1347
1347
  const pages = tree.pages || [];
1348
1348
  const pathToPage = {};
1349
1349
  for (const p of pages) { pathToPage[p.path] = p; }
@@ -1573,13 +1573,13 @@ tool('whoami', 'Return THIS agent session\'s identity: its server-assigned human
1573
1573
 
1574
1574
  // ── Project management tools (direct HTTP) ────────────────────────
1575
1575
 
1576
- 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.', {
1576
+ tool('project', 'START HERE for project management. Dispatch by `action`: list (lists all projects across all orgs — always call first), open (bind this agent session to a project; required before reading/writing frames — the org derives from the project), create (new project; org= names where it is born), update (change name/folder/description/layers), move (transfer to another org). There is no org switching: for project-less work (wiki/skills) pass org=... on the call. **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.', {
1577
1577
  action: z.enum(['list', 'open', 'create', 'update', 'move']).describe('Operation to perform.'),
1578
1578
  projectId: z.string().optional().describe('[open|update|move] project ID. Get IDs from action=list.'),
1579
1579
  name: z.string().optional().describe('[create|update] project name'),
1580
1580
  description: z.string().nullable().optional().describe('[create|update] project description'),
1581
1581
  templateSlug: z.string().optional().describe('[create] template slug (e.g. "web-design", "mobile-app", "landing-page")'),
1582
- org: z.string().optional().describe('[create] org slug or id to create the project in, without switching the active org. Defaults to the active org.'),
1582
+ org: z.string().optional().describe('[create] org slug or id the project is born in. Defaults to this session\'s org (the open project\'s org, else the default org).'),
1583
1583
  folder: z.string().nullable().optional().describe('[update] folder name (null to remove from folder)'),
1584
1584
  layers: z.array(z.object({}).passthrough()).optional().describe('[update] full layers array replacement. Use ls / to read current layers first.'),
1585
1585
  targetOrgId: z.string().optional().describe('[move] destination organization ID. Get org IDs from action=list (each project has an orgId field) or get_org. Both source and target org must include the current user.'),
@@ -1734,7 +1734,7 @@ tool('project', 'START HERE for project management. Dispatch by `action`: list (
1734
1734
  } catch (error) { return err(error); }
1735
1735
  });
1736
1736
 
1737
- tool('template', 'Manage project templates in the org. Dispatch by `action`: list/create/update/delete/fork. A template bundles layer definitions and an optional list of skill slugs that auto-attach to projects created from it.', {
1737
+ tool('template', 'Manage project templates in an org. Dispatch by `action`: list/create/update/delete/fork. A template bundles layer definitions and an optional list of skill slugs that auto-attach to projects created from it. Scopes to the open project\'s org by default; pass org=... to target another org (per-request only — nothing is switched).', {
1738
1738
  action: z.enum(['list', 'create', 'update', 'delete', 'fork']).describe('Operation to perform.'),
1739
1739
  templateId: z.string().optional().describe('[update|delete|fork] template ID'),
1740
1740
  name: z.string().optional().describe('[create|update|fork] template name (required for create; optional rename for fork)'),
@@ -1742,12 +1742,14 @@ tool('template', 'Manage project templates in the org. Dispatch by `action`: lis
1742
1742
  layers: z.array(z.object({}).passthrough()).optional().describe('[create|update] array of layer definitions'),
1743
1743
  skillSlugs: z.array(z.string()).optional().describe('[create|update] skill slugs to auto-attach to projects created from this template. Slugs are resolved against the org\'s skills (org-local first, built-in fallback) at project-create time; missing slugs are silently skipped. Pass an empty array on update to clear.'),
1744
1744
  visibility: z.string().optional().describe('[create|update] "org" or "public"'),
1745
+ org: z.string().optional().describe('Org (id or name) to scope this call to — defaults to the open project\'s org. Per-request only.'),
1745
1746
  }, async (args) => {
1746
1747
  try {
1747
1748
  const { action } = args;
1749
+ const orgHeader = args.org ? { 'X-Drafted-Org': args.org } : {};
1748
1750
  switch (action) {
1749
1751
  case 'list': {
1750
- const tpls = await api('GET', '/api/templates');
1752
+ const tpls = await api('GET', '/api/templates', undefined, orgHeader);
1751
1753
  markSearched(getSessionState().gates, 'template');
1752
1754
  return ok(tpls);
1753
1755
  }
@@ -1757,7 +1759,7 @@ tool('template', 'Manage project templates in the org. Dispatch by `action`: lis
1757
1759
  const body = { name, description, layers };
1758
1760
  if (Array.isArray(skillSlugs)) body.skillSlugs = skillSlugs;
1759
1761
  if (visibility) body.visibility = visibility;
1760
- return ok(await api('POST', '/api/templates', body));
1762
+ return ok(await api('POST', '/api/templates', body, orgHeader));
1761
1763
  }
1762
1764
  case 'update': {
1763
1765
  const { templateId, name, description, layers, skillSlugs, visibility } = args;
@@ -1769,19 +1771,19 @@ tool('template', 'Manage project templates in the org. Dispatch by `action`: lis
1769
1771
  if (Array.isArray(skillSlugs)) body.skillSlugs = skillSlugs;
1770
1772
  if (visibility) body.visibility = visibility;
1771
1773
  if (Object.keys(body).length === 0) throw new Error('At least one field is required for action=update');
1772
- return ok(await api('PUT', `/api/templates/${templateId}`, body));
1774
+ return ok(await api('PUT', `/api/templates/${templateId}`, body, orgHeader));
1773
1775
  }
1774
1776
  case 'delete': {
1775
1777
  const { templateId } = args;
1776
1778
  if (!templateId) throw new Error('templateId required for action=delete');
1777
- return ok(await api('DELETE', `/api/templates/${templateId}`));
1779
+ return ok(await api('DELETE', `/api/templates/${templateId}`, undefined, orgHeader));
1778
1780
  }
1779
1781
  case 'fork': {
1780
1782
  const { templateId, name } = args;
1781
1783
  if (!templateId) throw new Error('templateId required for action=fork');
1782
1784
  const body = {};
1783
1785
  if (name) body.name = name;
1784
- return ok(await api('POST', `/api/templates/${templateId}/fork`, body));
1786
+ return ok(await api('POST', `/api/templates/${templateId}/fork`, body, orgHeader));
1785
1787
  }
1786
1788
  default:
1787
1789
  throw new Error(`Unknown template action: ${action}`);
@@ -2118,8 +2120,7 @@ async function getMcpUpdateMetadata() {
2118
2120
 
2119
2121
 
2120
2122
  tool('get_org', {
2121
- action: z.enum(['get', 'switch', 'update_mcp']).optional().describe('Default: "get" returns active org and member info. Use "switch" with orgId to change the active org without opening a project. Use "update_mcp" to get explicit installed stdio MCP update instructions.'),
2122
- orgId: z.string().optional().describe('[switch] target org ID to switch to. Must be one of the orgs the user is a member of.'),
2123
+ action: z.enum(['get', 'update_mcp']).optional().describe('Default: "get" returns your orgs, the default org, and Google Drive availability. Use "update_mcp" to get explicit installed stdio MCP update instructions. There is no org switching: org derives from the resource you address (projectId/pageId/skillId), and creates/searches take an explicit org param.'),
2123
2124
  }, async (args = {}) => {
2124
2125
  try {
2125
2126
  const action = args.action || 'get';
@@ -2129,28 +2130,6 @@ tool('get_org', {
2129
2130
  return ok(buildInstalledMcpUpdateInstructions(mcpUpdate));
2130
2131
  }
2131
2132
 
2132
- if (action === 'switch') {
2133
- if (!args.orgId) throw new Error('orgId is required for action=switch');
2134
- await api('POST', '/auth/switch-org', { orgId: args.orgId });
2135
- // Bust the per-session org cache so subsequent calls re-fetch /auth/me.
2136
- const sess = getSessionState();
2137
- sess.cachedOrgId = null;
2138
- sess.cachedOrgIdTime = 0;
2139
- // Clear active project too — projects are scoped to orgs, so the
2140
- // previous one isn't valid in the new org.
2141
- setMcpActiveProject(null, null);
2142
- // Bind this session to the chosen org so session recovery re-asserts it
2143
- // (set AFTER the clear above, which would otherwise leave it unchanged).
2144
- sess.boundOrgId = args.orgId;
2145
- const me = await api('GET', '/auth/me');
2146
- const orgs = (await api('GET', '/api/orgs')).orgs || [];
2147
- const activeOrg = (orgs || []).map(o => ({ id: o.orgId || o.id, name: o.orgName || o.name })).find(o => o.id === me?.orgId) || null;
2148
- const googleDrive = await getGoogleDriveAvailability();
2149
- const mcpUpdate = await getMcpUpdateMetadata();
2150
- const session = await sessionSurfaceBlock();
2151
- return ok({ switched: true, activeOrg, googleDrive, mcpVersion: PACKAGE_VERSION, mcpUpdate, session, 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). If googleDrive.connected is true, prefer Google Workspace frames for docs, sheets, and slides.' });
2152
- }
2153
-
2154
2133
  // Source of truth = the org this MCP process scopes requests to (what mutations
2155
2134
  // will actually hit). Each MCP process is independent — multiple agents can run
2156
2135
  // in parallel scoped to different orgs. /auth/me reads sessions.org_id directly.
@@ -2179,7 +2158,7 @@ tool('get_org', {
2179
2158
  mcpVersion: PACKAGE_VERSION,
2180
2159
  mcpUpdate,
2181
2160
  session: await sessionSurfaceBlock(),
2182
- note: "activeOrg is the org this MCP process scopes its requests to (carried per-request as X-Drafted-Org); mutations target it. `session` is THIS agent's own surface identity — `session.name` is the human-readable tab name the user sees (use it to identify which agent you are); refresh it via whoami. To scope to a different org without opening a project, call get_org(action=\"switch\", orgId=\"...\"). Concurrent MCP sessions and browser tabs for the same user can be scoped to different orgs. If googleDrive.connected is true, strongly prefer Google Workspace frames for docs, sheets, and slides.",
2161
+ note: "Org is derived from the resource you address: opening a project binds this agent session's context (org = the project's org), and UUIDs (pageId/skillId/projectId) self-derive. activeOrg is only the DEFAULT for calls that name no resource and pass no org param — to target a different org, pass org=... on the call (wiki write/search, skill add/update/fork/push/list, project create, template actions), never a switch. `session` is THIS agent's own surface identity — `session.name` is the human-readable tab name the user sees (use it to identify which agent you are); refresh it via whoami. Concurrent MCP sessions can operate on different orgs simultaneously. If googleDrive.connected is true, strongly prefer Google Workspace frames for docs, sheets, and slides.",
2183
2162
  });
2184
2163
  } catch (error) { return err(error); }
2185
2164
  });
@@ -3113,7 +3092,7 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
3113
3092
  path: z.string().optional().describe('[read_file|update_file] relative path inside skill directory (e.g. "examples/react.md")'),
3114
3093
  offset: z.number().optional().describe('[search|list] skip N results for pagination; [read_file] start reading at this byte offset (default 0) — for large files (e.g. a >90KB app-frame bundle) read in chunks using the returned nextOffset until truncated=false'),
3115
3094
  maxBytes: z.number().optional().describe('[read_file] return at most this many bytes from offset (default: whole remaining file). Response reports totalSize/offset/truncated/nextOffset.'),
3116
- org: z.string().optional().describe('[fork|push|update] resolve/fork into this Drafted org (id or name); scopes the request without switching the active org'),
3095
+ org: z.string().optional().describe('[add] org (id or name) the skill is born in — defaults to the open project\'s org; [list|search|load] scope to this org; [fork|push|update] resolve/fork into this org. Per-request only nothing is switched.'),
3117
3096
  setup: z.array(z.string()).optional().describe('[add|update] setup command(s) (in order) run on materialize to build a source-only skill, e.g. ["npm ci","npm run build"]'),
3118
3097
  files: z.array(z.object({ path: z.string(), content: z.string() })).optional().describe('[push] source files to push (path + UTF-8 content); server strips artifacts + enforces caps'),
3119
3098
  dir: z.string().optional().describe('[push] local directory to push instead of files[]; walked locally (heavy dirs, .skillinstall/, and .skillignore pre-filtered), server re-enforces. On push the dir\'s .gitignore is auto-updated to exclude .skillinstall/ (the rebuildable bundle).'),
@@ -3131,14 +3110,14 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
3131
3110
  }
3132
3111
  switch (action) {
3133
3112
  case 'search': {
3134
- const { query, tags, scope = 'all', limit, offset, compact } = args;
3113
+ const { query, tags, scope = 'all', limit, offset, compact, org } = args;
3135
3114
  const params = new URLSearchParams();
3136
3115
  if (query) params.set('q', query);
3137
3116
  if (tags?.length) params.set('tags', tags.join(','));
3138
3117
  if (scope) params.set('scope', scope);
3139
3118
  const qs = params.toString();
3140
3119
  const endpoint = query ? '/api/skills/search' : '/api/skills';
3141
- const result = await api('GET', `${endpoint}${qs ? '?' + qs : ''}`);
3120
+ const result = await api('GET', `${endpoint}${qs ? '?' + qs : ''}`, undefined, org ? { 'X-Drafted-Org': org } : {});
3142
3121
  markSearched(getSessionState().gates, 'skill');
3143
3122
  return ok(shapeSkillCatalog(result, { limit, offset, compact }));
3144
3123
  }
@@ -3147,7 +3126,7 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
3147
3126
  if (!skill) throw new Error('skill (ID or slug) is required for action=load');
3148
3127
  const isUuid = /^[a-f0-9-]{36}$/.test(skill);
3149
3128
  const endpoint = isUuid ? `/api/skills/${skill}` : `/api/skills/slug/${skill}`;
3150
- const result = await api('GET', endpoint);
3129
+ const result = await api('GET', endpoint, undefined, args.org ? { 'X-Drafted-Org': args.org } : {});
3151
3130
  if (result?.id) getSessionState().loadedSkillIds.add(result.id);
3152
3131
  return ok(result);
3153
3132
  }
@@ -3163,11 +3142,11 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
3163
3142
  return ok(await api('GET', `/api/skills/${id}/versions`, undefined, extra));
3164
3143
  }
3165
3144
  case 'list': {
3166
- if (args.scope || args.tags?.length) {
3145
+ if (args.scope || args.tags?.length || args.org) {
3167
3146
  const params = new URLSearchParams();
3168
- params.set('scope', args.scope || 'all');
3147
+ params.set('scope', args.scope || (args.org ? 'org' : 'all'));
3169
3148
  if (args.tags?.length) params.set('tags', args.tags.join(','));
3170
- const result = await api('GET', `/api/skills?${params.toString()}`);
3149
+ const result = await api('GET', `/api/skills?${params.toString()}`, undefined, args.org ? { 'X-Drafted-Org': args.org } : {});
3171
3150
  return ok(shapeSkillCatalog(result, { limit: args.limit, offset: args.offset, compact: args.compact }));
3172
3151
  }
3173
3152
 
@@ -3182,25 +3161,28 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
3182
3161
  return ok(await api('GET', `/api/projects/${pid}/skills`));
3183
3162
  } catch (e) {
3184
3163
  // 404 / "not found" means the project isn't visible in the
3185
- // current org (typical after an org switch). Fall through to
3164
+ // session's org (typical after binding to a project in another org). Fall through to
3186
3165
  // org-attached skills rather than bubbling a useless error.
3187
3166
  if (!/not found/i.test(e.message)) throw e;
3188
3167
  }
3189
3168
  }
3190
3169
  const orgId = await getCurrentOrgId();
3191
- if (!orgId) throw new Error('No active org. Call get_org first or pass projectId.');
3170
+ if (!orgId) throw new Error('No org context. Open a project (project(action="open")), pass projectId, or pass org=.');
3192
3171
  return ok(await api('GET', `/api/orgs/${orgId}/skills`));
3193
3172
  }
3194
3173
  case 'add': {
3195
3174
  const g2 = g2Block(getSessionState().gates);
3196
3175
  if (g2) return err(new Error(g2));
3197
- const { name, description, content, tags, triggerPatterns, setup } = args;
3176
+ const { name, description, content, tags, triggerPatterns, setup, org } = args;
3198
3177
  if (!name || !description || !content) throw new Error('name, description, content required for action=add');
3199
3178
  const body = { name, description, content };
3200
3179
  if (tags) body.tags = tags;
3201
3180
  if (triggerPatterns) body.triggerPatterns = triggerPatterns;
3202
3181
  if (setup !== undefined) body.setup = setup;
3203
- return ok(await api('POST', '/api/skills', body));
3182
+ // org names where the skill is born (UUID-first model: org is explicit at
3183
+ // create) — without this the bound-org guard accepts org= but the write
3184
+ // would land in the session org anyway.
3185
+ return ok(await api('POST', '/api/skills', body, org ? { 'X-Drafted-Org': org } : {}));
3204
3186
  }
3205
3187
  case 'update': {
3206
3188
  // Auto-fork-on-update (tool behavior): if the target is read-only (global/
@@ -3331,11 +3313,11 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
3331
3313
  // All 11 actions dispatch from one tool. Read-only actions skip the
3332
3314
  // skill gate; mutations require org-level wiki-maintainer skills loaded.
3333
3315
 
3334
- tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and other agents/humans share maintenance — every edit broadcasts live, and edits from others appear in `recent` and on `read`.\n\n**Wiki always operates on the ACTIVE org.** If the content you need lives in a different org, switch first via `get_org(action="switch", orgId=...)` — don\'t assume "no hits" means the content doesn\'t exist. Verify the active org with `get_org` before searching.\n\nBefore mutating: check `recent` and `search` for relevant existing pages. Before mv/rm: check `links` (or pass `dryRun=true`). After completing a logical session of work, append a `log` entry.\n\nThe tool handles bookkeeping you\'d otherwise forget: `mv` rewrites inbound references via the link index, `read` shows who edited last and when. Use `health` to find unlinked pages and broken links.\n\n**Skill gate:** the org may attach a `wiki-maintainer` skill that you MUST load before mutations. If you get a skill-gate error, run skill(action="load", skill="wiki-maintainer") then retry.', {
3316
+ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and other agents/humans share maintenance — every edit broadcasts live, and edits from others appear in `recent` and on `read`.\n\n**Addressing:** a `pageId` (UUID) self-derives its org no org arg needed. Path-based and listing actions scope to the open project\'s org by default; pass `org=...` to target another org (there is no org switching). `search` spans ALL your orgs by default — don\'t assume "no hits" means the content doesn\'t exist.\n\nBefore mutating: check `recent` and `search` for relevant existing pages. Before mv/rm: check `links` (or pass `dryRun=true`). After completing a logical session of work, append a `log` entry.\n\nThe tool handles bookkeeping you\'d otherwise forget: `mv` rewrites inbound references via the link index, `read` shows who edited last and when. Use `health` to find unlinked pages and broken links.\n\n**Skill gate:** the org may attach a `wiki-maintainer` skill that you MUST load before mutations. If you get a skill-gate error, run skill(action="load", skill="wiki-maintainer") then retry.', {
3335
3317
  action: z.enum(['ls', 'recent', 'read', 'search', 'links', 'log', 'health', 'write', 'edit', 'mv', 'rm', 'source-register', 'source-list', 'source-get', 'bulk-write']).describe('Operation to perform.'),
3336
3318
  path: z.string().optional().describe('[ls|read|links] wiki path. For ls: default / (root). For read: required. For links: required.'),
3337
3319
  pageId: z.string().optional().describe('[read|edit|mv|rm|links] page UUID (from read/search). UUID-first: addresses the page directly, org auto-derives — no org needed and no path lookup. Preferred over path for an existing page.'),
3338
- org: z.string().optional().describe('[write] org slug or id to create/target the page in, without switching the active org. [search] scope the search to this org (default: search ALL your orgs). Required to create a page when you belong to more than one org and none is active.'),
3320
+ org: z.string().optional().describe('Org slug or id to scope this call to (per-request only — nothing is switched). [write] the org the page is created in required when you belong to several orgs and no project is open. [search] restrict to this org (default: ALL your orgs). [ls|recent|read|links|log|health|edit|mv|rm|bulk-write] target this org\'s wiki instead of the open project\'s org. Ignored when a pageId is given (the page self-derives its org).'),
3339
3321
  recursive: z.boolean().optional().describe('[ls] list recursively with depth indicators'),
3340
3322
  limit: z.number().optional().describe('[recent|search] max results (recent default 10, search default 25)'),
3341
3323
  query: z.string().optional().describe('[search] term to search in title, path, and content'),
@@ -3374,9 +3356,19 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3374
3356
  if (!args.pageId) await requireBoundOrgForProjectlessMutation(args.org);
3375
3357
  }
3376
3358
 
3377
- // Resolve org context once for the wiki tool. Each MCP session is bound
3378
- // to one org (parallel agents on different orgs is supported by design).
3379
- const orgCtx = await getCurrentOrgContext();
3359
+ // Resolve org context for this CALL. An explicit org= override wins (per
3360
+ // request, nothing is switched); otherwise the session's binding (the open
3361
+ // project's org). Resolving the override here keeps the echoed `org` field
3362
+ // and every emitted browser URL truthful about where the call landed.
3363
+ let orgCtx = await getCurrentOrgContext();
3364
+ if (args.org) {
3365
+ const d = await api('GET', '/api/orgs');
3366
+ const list = (d.orgs || d || []).map(o => ({ id: o.orgId || o.id, name: o.orgName || o.name }));
3367
+ const wanted = String(args.org).toLowerCase();
3368
+ const byName = list.filter(o => (o.name || '').toLowerCase() === wanted);
3369
+ const match = list.find(o => o.id === args.org) || (byName.length === 1 ? byName[0] : null);
3370
+ if (match) orgCtx = match;
3371
+ }
3380
3372
  const orgId = orgCtx?.id || null;
3381
3373
  // Mutation responses include `org` so the agent always sees where the
3382
3374
  // write landed — eliminates silent cross-org confusion.
@@ -3401,7 +3393,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3401
3393
  // ── ls ──────────────────────────────────────────────────────
3402
3394
  case 'ls': {
3403
3395
  const { path: lsPath = '/', recursive: lsRecursive = false } = args;
3404
- const { pages, pathToPage } = await getTreeAsMap();
3396
+ const { pages, pathToPage } = await getTreeAsMap(orgHeader);
3405
3397
  const parent = normalizeWikiPath(lsPath);
3406
3398
 
3407
3399
  if (lsRecursive) {
@@ -3459,7 +3451,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3459
3451
  // ── recent ──────────────────────────────────────────────────
3460
3452
  case 'recent': {
3461
3453
  const { limit: recentLimit = 10 } = args;
3462
- const tree = await api('GET', '/api/wiki/tree');
3454
+ const tree = await api('GET', '/api/wiki/tree', undefined, orgHeader);
3463
3455
  const recent = (tree.pages || [])
3464
3456
  .filter(p => p.updatedAt)
3465
3457
  .sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
@@ -3555,7 +3547,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3555
3547
  let existingContent = '';
3556
3548
  let existingId = null;
3557
3549
  try {
3558
- const logPage = await api('GET', '/api/wiki/page?path=log');
3550
+ const logPage = await api('GET', '/api/wiki/page?path=log', undefined, orgHeader);
3559
3551
  existingContent = logPage.content || '';
3560
3552
  existingId = logPage.id;
3561
3553
  } catch {
@@ -3564,7 +3556,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3564
3556
  path: 'log',
3565
3557
  title: 'Log',
3566
3558
  content: entry + '\n',
3567
- });
3559
+ }, orgHeader);
3568
3560
  return ok(withOrg({ appended: true, created: true, pageId: created.id, path: 'log', url: wikiBrowserUrl('log') }));
3569
3561
  }
3570
3562
 
@@ -3576,7 +3568,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3576
3568
 
3577
3569
  // ── health ──────────────────────────────────────────────────
3578
3570
  case 'health': {
3579
- return ok(await api('GET', '/api/wiki/health'));
3571
+ return ok(await api('GET', '/api/wiki/health', undefined, orgHeader));
3580
3572
  }
3581
3573
 
3582
3574
  // ── write ───────────────────────────────────────────────────
@@ -3710,7 +3702,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3710
3702
  type: p.type,
3711
3703
  frontmatter: p.frontmatter,
3712
3704
  }));
3713
- const result = await api('POST', '/api/wiki/pages/bulk', { pages: payload });
3705
+ const result = await api('POST', '/api/wiki/pages/bulk', { pages: payload }, orgHeader);
3714
3706
  return ok(withOrg({
3715
3707
  created: result.created?.length ?? 0,
3716
3708
  updated: result.updated?.length ?? 0,
@@ -3817,7 +3809,7 @@ tool('collector', {
3817
3809
  fresh: z.boolean().optional().describe('[test_start] start a brand-new run instead of resuming your latest'),
3818
3810
  actionId: z.string().optional().describe('[test_resolve] id of the pending action to resolve'),
3819
3811
  approve: z.boolean().optional().describe('[test_resolve] approve (default true) or reject the pending action'),
3820
- projectId: z.string().optional().describe('[create|meta] project to bind/scope to (defaults to the active project). Operates on the session’s active org — open the target project first via project(action="open"), or get_org(action="switch") to change org.'),
3812
+ projectId: z.string().optional().describe('[create|meta] project to bind/scope to (defaults to the active project). The org derives from this project — open the target project first via project(action="open") if none is active.'),
3821
3813
  name: z.string().optional().describe('[create|update] collector name'),
3822
3814
  description: z.string().optional().describe('[create|update] one-line description shown to the consumer'),
3823
3815
  enabled: z.boolean().optional().describe('[create|update] whether the collector is live; a disabled collector 404s on its /c/<slug> link. enable/disable set this directly.'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.12.4",
3
+ "version": "1.12.6",
4
4
  "description": "Drafted — visual thinking surface for humans and AI agents. Renders HTML, markdown, images, and code as frames on a zoomable canvas, with MCP tools for AI agents and real-time sync for humans.",
5
5
  "type": "module",
6
6
  "files": [
@@ -8,7 +8,7 @@ Onboard the user to Drafted — a producibles harness that compounds across thre
8
8
  This is the over-arching prime command: orient, then seed all three stores so every later session starts smart.
9
9
 
10
10
  1. **Orient** — in 3-4 lines explain the loop: you *prime* from the harness (the system makes you search the wiki, and auto-loads the project's attached skills, anchors, and layer rules before you work), you *build*, then you *compound* (deposit what you learned). The more it's used, the less searching and the more stable the work.
11
- 2. **Confirm the org** — call `get_org`. Confirm the active organization is the one to build the harness in. If unsure, ask.
11
+ 2. **Confirm the org** — call `get_org` to list the user's orgs. Confirm which one the harness should be built in if unsure, ask — then name it explicitly (org=...) on every create in the steps below.
12
12
  3. **Seed knowledge** — run the `/drafted:ingest` flow: help the user point at existing business materials (folders, docs, past research) or interrogate them for tacit knowledge, and land durable pages in the wiki.
13
13
  4. **Seed procedures** — from those materials and the conversation, surface 1-3 candidate SOPs. For the most valuable, run the `/drafted:create-skill` flow.
14
14
  5. **Seed the surface** — run the `/drafted:create-project` flow for the user's immediate piece of work (or a reusable template).