drafted 1.12.5 → 1.12.7

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
 
@@ -200,7 +200,9 @@ IMPORTANT: Any URL containing /f/{uuid} is a Drafted frame link — ALWAYS use r
200
200
  }, {
201
201
  // Initialize instructions: the agent-identity contract, so an agent learns its own
202
202
  // tab name + the right way to read it WITHOUT having to "think to" call a tool.
203
- instructions: `SESSION IDENTITY — read this first: you run as a NAMED session tab visible to the user on their Drafted surface. Your session has a human-readable name (a Greek term, e.g. "Nous") — that name is how the user matches YOUR window to the tab they see, so identify yourself by it when it matters which agent you are. Read it from get_org (response field "session.name") or from the whoami tool; call whoami to refresh after a reconnect.`,
203
+ instructions: `SESSION IDENTITY — read this first: you run as a NAMED session tab visible to the user on their Drafted surface. Your session has a human-readable name (a Greek term, e.g. "Nous") — that name is how the user matches YOUR window to the tab they see, so identify yourself by it when it matters which agent you are. Read it from get_org (response field "session.name") or from the whoami tool; call whoami to refresh after a reconnect.
204
+
205
+ DUAL REGISTRATION IS NORMAL: a separate "Drafted" remote connector (https://drafted.live/mcp, managed by claude.ai) may appear alongside this local stdio server in MCP listings. It is NOT a duplicate and NOT broken — it serves claude.ai web/mobile/Cowork, where a local stdio process cannot run. Never advise removing it; a "needs authentication" state on it is fixed by signing in from claude.ai and does not affect this stdio session.`,
204
206
  });
205
207
 
206
208
  const layerKeys = Object.keys(LAYERS);
@@ -223,8 +225,8 @@ const TOOL_ANNOTATIONS = {
223
225
  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
226
 
225
227
  // 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.' },
228
+ 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.' },
229
+ 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
230
 
229
231
  // Templates
230
232
  template: { title: 'Templates', readOnlyHint: false, destructiveHint: true, openWorldHint: false, description: 'Manage project templates: list, create, update, delete, fork.' },
@@ -1235,21 +1237,21 @@ async function getCurrentOrgContext() {
1235
1237
  }
1236
1238
 
1237
1239
  // 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).
1240
+ // If the agent never bound this session to a project and passed no explicit org,
1241
+ // refuse to GUESS when the user belongs to more than one org — otherwise the
1242
+ // write silently lands in whatever org the session happened to inherit (the
1243
+ // "wiki write into the wrong org" failure).
1242
1244
  // Single-org users are unambiguous and proceed untouched. (DRAFT-36 Phase 4.)
1243
1245
  async function requireBoundOrgForProjectlessMutation(explicitOrg) {
1244
1246
  if (explicitOrg) return;
1245
1247
  const sess = getSessionState();
1246
- if (sess.boundOrgId) return; // bound via project open / get_org switch
1248
+ if (sess.boundOrgId) return; // bound via project open (org = project's org)
1247
1249
  if (getState().projectId) return; // an active project implies its org
1248
1250
  // Remote/web sessions are isolated per connection: each one gets its OWN
1249
1251
  // server-side session row with its own org_id set on connect, so the session
1250
1252
  // org IS this session's binding — there's no shared, long-lived session to
1251
1253
  // 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
1254
+ // forcing the agent to open a project before a project-less
1253
1255
  // wiki or skill write. The DRAFT-36 Phase 4 "refuse to guess" guard below
1254
1256
  // therefore applies to stdio only.
1255
1257
  if (isRemote) {
@@ -1267,11 +1269,11 @@ async function requireBoundOrgForProjectlessMutation(explicitOrg) {
1267
1269
  }
1268
1270
  if (count > 1) {
1269
1271
  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=...).`
1272
+ `Refusing to guess the org: no project is open and no explicit org was ` +
1273
+ `given, but you belong to ${count} orgs — a project-less wiki/skill write ` +
1274
+ `would land in whichever org the session inherited. Pass org=... on this ` +
1275
+ `call (org name or id), or open the relevant project first ` +
1276
+ `(project(action="open") the org derives from the project).`
1275
1277
  );
1276
1278
  }
1277
1279
  }
@@ -1342,8 +1344,8 @@ function normalizeWikiPath(input) {
1342
1344
  return input.replace(/^\/+/, '').replace(/\.md$/, '');
1343
1345
  }
1344
1346
 
1345
- async function getTreeAsMap() {
1346
- const tree = await api('GET', '/api/wiki/tree');
1347
+ async function getTreeAsMap(orgHeader = {}) {
1348
+ const tree = await api('GET', '/api/wiki/tree', undefined, orgHeader);
1347
1349
  const pages = tree.pages || [];
1348
1350
  const pathToPage = {};
1349
1351
  for (const p of pages) { pathToPage[p.path] = p; }
@@ -1540,11 +1542,17 @@ async function sessionSurfaceBlock() {
1540
1542
  // No WS ack yet — best-effort identity from /auth/me so callers still get a userId/org.
1541
1543
  const cookieSid = sessionId || getBootstrapSessionId();
1542
1544
  let me = null;
1545
+ let unreachable = false;
1543
1546
  if (cookieSid) {
1544
1547
  try {
1545
1548
  const res = await fetch(`${getServerUrl()}/auth/me`, { headers: { Cookie: `gc_session=${cookieSid}` } });
1546
1549
  if (res.ok) me = await res.json();
1547
- } catch { /* not yet authenticated */ }
1550
+ } catch {
1551
+ // Transport failure, NOT a sign-out. Conflating the two made agents tell
1552
+ // users "you're signed out" during a network blip and start needless
1553
+ // re-logins — surface the distinction instead.
1554
+ unreachable = true;
1555
+ }
1548
1556
  }
1549
1557
  return {
1550
1558
  sessionId: cookieSid,
@@ -1556,6 +1564,10 @@ async function sessionSurfaceBlock() {
1556
1564
  color: null,
1557
1565
  surfaced: false,
1558
1566
  alive: false,
1567
+ ...(unreachable ? {
1568
+ serverUnreachable: true,
1569
+ note: `Could not reach ${getServerUrl()} — identity UNKNOWN, not signed out. This is a network/transport failure: do not tell the user they are logged out and do not start a new login; retry when connectivity is back.`,
1570
+ } : {}),
1559
1571
  };
1560
1572
  }
1561
1573
 
@@ -1573,13 +1585,13 @@ tool('whoami', 'Return THIS agent session\'s identity: its server-assigned human
1573
1585
 
1574
1586
  // ── Project management tools (direct HTTP) ────────────────────────
1575
1587
 
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.', {
1588
+ 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
1589
  action: z.enum(['list', 'open', 'create', 'update', 'move']).describe('Operation to perform.'),
1578
1590
  projectId: z.string().optional().describe('[open|update|move] project ID. Get IDs from action=list.'),
1579
1591
  name: z.string().optional().describe('[create|update] project name'),
1580
1592
  description: z.string().nullable().optional().describe('[create|update] project description'),
1581
1593
  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.'),
1594
+ 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
1595
  folder: z.string().nullable().optional().describe('[update] folder name (null to remove from folder)'),
1584
1596
  layers: z.array(z.object({}).passthrough()).optional().describe('[update] full layers array replacement. Use ls / to read current layers first.'),
1585
1597
  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 +1746,7 @@ tool('project', 'START HERE for project management. Dispatch by `action`: list (
1734
1746
  } catch (error) { return err(error); }
1735
1747
  });
1736
1748
 
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.', {
1749
+ 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
1750
  action: z.enum(['list', 'create', 'update', 'delete', 'fork']).describe('Operation to perform.'),
1739
1751
  templateId: z.string().optional().describe('[update|delete|fork] template ID'),
1740
1752
  name: z.string().optional().describe('[create|update|fork] template name (required for create; optional rename for fork)'),
@@ -1742,12 +1754,14 @@ tool('template', 'Manage project templates in the org. Dispatch by `action`: lis
1742
1754
  layers: z.array(z.object({}).passthrough()).optional().describe('[create|update] array of layer definitions'),
1743
1755
  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
1756
  visibility: z.string().optional().describe('[create|update] "org" or "public"'),
1757
+ 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
1758
  }, async (args) => {
1746
1759
  try {
1747
1760
  const { action } = args;
1761
+ const orgHeader = args.org ? { 'X-Drafted-Org': args.org } : {};
1748
1762
  switch (action) {
1749
1763
  case 'list': {
1750
- const tpls = await api('GET', '/api/templates');
1764
+ const tpls = await api('GET', '/api/templates', undefined, orgHeader);
1751
1765
  markSearched(getSessionState().gates, 'template');
1752
1766
  return ok(tpls);
1753
1767
  }
@@ -1757,7 +1771,7 @@ tool('template', 'Manage project templates in the org. Dispatch by `action`: lis
1757
1771
  const body = { name, description, layers };
1758
1772
  if (Array.isArray(skillSlugs)) body.skillSlugs = skillSlugs;
1759
1773
  if (visibility) body.visibility = visibility;
1760
- return ok(await api('POST', '/api/templates', body));
1774
+ return ok(await api('POST', '/api/templates', body, orgHeader));
1761
1775
  }
1762
1776
  case 'update': {
1763
1777
  const { templateId, name, description, layers, skillSlugs, visibility } = args;
@@ -1769,19 +1783,19 @@ tool('template', 'Manage project templates in the org. Dispatch by `action`: lis
1769
1783
  if (Array.isArray(skillSlugs)) body.skillSlugs = skillSlugs;
1770
1784
  if (visibility) body.visibility = visibility;
1771
1785
  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));
1786
+ return ok(await api('PUT', `/api/templates/${templateId}`, body, orgHeader));
1773
1787
  }
1774
1788
  case 'delete': {
1775
1789
  const { templateId } = args;
1776
1790
  if (!templateId) throw new Error('templateId required for action=delete');
1777
- return ok(await api('DELETE', `/api/templates/${templateId}`));
1791
+ return ok(await api('DELETE', `/api/templates/${templateId}`, undefined, orgHeader));
1778
1792
  }
1779
1793
  case 'fork': {
1780
1794
  const { templateId, name } = args;
1781
1795
  if (!templateId) throw new Error('templateId required for action=fork');
1782
1796
  const body = {};
1783
1797
  if (name) body.name = name;
1784
- return ok(await api('POST', `/api/templates/${templateId}/fork`, body));
1798
+ return ok(await api('POST', `/api/templates/${templateId}/fork`, body, orgHeader));
1785
1799
  }
1786
1800
  default:
1787
1801
  throw new Error(`Unknown template action: ${action}`);
@@ -2118,8 +2132,7 @@ async function getMcpUpdateMetadata() {
2118
2132
 
2119
2133
 
2120
2134
  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.'),
2135
+ 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
2136
  }, async (args = {}) => {
2124
2137
  try {
2125
2138
  const action = args.action || 'get';
@@ -2129,28 +2142,6 @@ tool('get_org', {
2129
2142
  return ok(buildInstalledMcpUpdateInstructions(mcpUpdate));
2130
2143
  }
2131
2144
 
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
2145
  // Source of truth = the org this MCP process scopes requests to (what mutations
2155
2146
  // will actually hit). Each MCP process is independent — multiple agents can run
2156
2147
  // in parallel scoped to different orgs. /auth/me reads sessions.org_id directly.
@@ -2179,7 +2170,7 @@ tool('get_org', {
2179
2170
  mcpVersion: PACKAGE_VERSION,
2180
2171
  mcpUpdate,
2181
2172
  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.",
2173
+ 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
2174
  });
2184
2175
  } catch (error) { return err(error); }
2185
2176
  });
@@ -3113,7 +3104,7 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
3113
3104
  path: z.string().optional().describe('[read_file|update_file] relative path inside skill directory (e.g. "examples/react.md")'),
3114
3105
  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
3106
  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('[add] org (id or name) to create the skill in — defaults to the bound/active org; [fork|push|update] resolve/fork into this Drafted org (id or name); scopes the request without switching the active org'),
3107
+ 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
3108
  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
3109
  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
3110
  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 +3122,14 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
3131
3122
  }
3132
3123
  switch (action) {
3133
3124
  case 'search': {
3134
- const { query, tags, scope = 'all', limit, offset, compact } = args;
3125
+ const { query, tags, scope = 'all', limit, offset, compact, org } = args;
3135
3126
  const params = new URLSearchParams();
3136
3127
  if (query) params.set('q', query);
3137
3128
  if (tags?.length) params.set('tags', tags.join(','));
3138
3129
  if (scope) params.set('scope', scope);
3139
3130
  const qs = params.toString();
3140
3131
  const endpoint = query ? '/api/skills/search' : '/api/skills';
3141
- const result = await api('GET', `${endpoint}${qs ? '?' + qs : ''}`);
3132
+ const result = await api('GET', `${endpoint}${qs ? '?' + qs : ''}`, undefined, org ? { 'X-Drafted-Org': org } : {});
3142
3133
  markSearched(getSessionState().gates, 'skill');
3143
3134
  return ok(shapeSkillCatalog(result, { limit, offset, compact }));
3144
3135
  }
@@ -3147,7 +3138,7 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
3147
3138
  if (!skill) throw new Error('skill (ID or slug) is required for action=load');
3148
3139
  const isUuid = /^[a-f0-9-]{36}$/.test(skill);
3149
3140
  const endpoint = isUuid ? `/api/skills/${skill}` : `/api/skills/slug/${skill}`;
3150
- const result = await api('GET', endpoint);
3141
+ const result = await api('GET', endpoint, undefined, args.org ? { 'X-Drafted-Org': args.org } : {});
3151
3142
  if (result?.id) getSessionState().loadedSkillIds.add(result.id);
3152
3143
  return ok(result);
3153
3144
  }
@@ -3163,11 +3154,11 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
3163
3154
  return ok(await api('GET', `/api/skills/${id}/versions`, undefined, extra));
3164
3155
  }
3165
3156
  case 'list': {
3166
- if (args.scope || args.tags?.length) {
3157
+ if (args.scope || args.tags?.length || args.org) {
3167
3158
  const params = new URLSearchParams();
3168
- params.set('scope', args.scope || 'all');
3159
+ params.set('scope', args.scope || (args.org ? 'org' : 'all'));
3169
3160
  if (args.tags?.length) params.set('tags', args.tags.join(','));
3170
- const result = await api('GET', `/api/skills?${params.toString()}`);
3161
+ const result = await api('GET', `/api/skills?${params.toString()}`, undefined, args.org ? { 'X-Drafted-Org': args.org } : {});
3171
3162
  return ok(shapeSkillCatalog(result, { limit: args.limit, offset: args.offset, compact: args.compact }));
3172
3163
  }
3173
3164
 
@@ -3182,13 +3173,13 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
3182
3173
  return ok(await api('GET', `/api/projects/${pid}/skills`));
3183
3174
  } catch (e) {
3184
3175
  // 404 / "not found" means the project isn't visible in the
3185
- // current org (typical after an org switch). Fall through to
3176
+ // session's org (typical after binding to a project in another org). Fall through to
3186
3177
  // org-attached skills rather than bubbling a useless error.
3187
3178
  if (!/not found/i.test(e.message)) throw e;
3188
3179
  }
3189
3180
  }
3190
3181
  const orgId = await getCurrentOrgId();
3191
- if (!orgId) throw new Error('No active org. Call get_org first or pass projectId.');
3182
+ if (!orgId) throw new Error('No org context. Open a project (project(action="open")), pass projectId, or pass org=.');
3192
3183
  return ok(await api('GET', `/api/orgs/${orgId}/skills`));
3193
3184
  }
3194
3185
  case 'add': {
@@ -3334,11 +3325,11 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
3334
3325
  // All 11 actions dispatch from one tool. Read-only actions skip the
3335
3326
  // skill gate; mutations require org-level wiki-maintainer skills loaded.
3336
3327
 
3337
- 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.', {
3328
+ 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.', {
3338
3329
  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.'),
3339
3330
  path: z.string().optional().describe('[ls|read|links] wiki path. For ls: default / (root). For read: required. For links: required.'),
3340
3331
  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.'),
3341
- 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.'),
3332
+ 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).'),
3342
3333
  recursive: z.boolean().optional().describe('[ls] list recursively with depth indicators'),
3343
3334
  limit: z.number().optional().describe('[recent|search] max results (recent default 10, search default 25)'),
3344
3335
  query: z.string().optional().describe('[search] term to search in title, path, and content'),
@@ -3377,9 +3368,19 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3377
3368
  if (!args.pageId) await requireBoundOrgForProjectlessMutation(args.org);
3378
3369
  }
3379
3370
 
3380
- // Resolve org context once for the wiki tool. Each MCP session is bound
3381
- // to one org (parallel agents on different orgs is supported by design).
3382
- const orgCtx = await getCurrentOrgContext();
3371
+ // Resolve org context for this CALL. An explicit org= override wins (per
3372
+ // request, nothing is switched); otherwise the session's binding (the open
3373
+ // project's org). Resolving the override here keeps the echoed `org` field
3374
+ // and every emitted browser URL truthful about where the call landed.
3375
+ let orgCtx = await getCurrentOrgContext();
3376
+ if (args.org) {
3377
+ const d = await api('GET', '/api/orgs');
3378
+ const list = (d.orgs || d || []).map(o => ({ id: o.orgId || o.id, name: o.orgName || o.name }));
3379
+ const wanted = String(args.org).toLowerCase();
3380
+ const byName = list.filter(o => (o.name || '').toLowerCase() === wanted);
3381
+ const match = list.find(o => o.id === args.org) || (byName.length === 1 ? byName[0] : null);
3382
+ if (match) orgCtx = match;
3383
+ }
3383
3384
  const orgId = orgCtx?.id || null;
3384
3385
  // Mutation responses include `org` so the agent always sees where the
3385
3386
  // write landed — eliminates silent cross-org confusion.
@@ -3404,7 +3405,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3404
3405
  // ── ls ──────────────────────────────────────────────────────
3405
3406
  case 'ls': {
3406
3407
  const { path: lsPath = '/', recursive: lsRecursive = false } = args;
3407
- const { pages, pathToPage } = await getTreeAsMap();
3408
+ const { pages, pathToPage } = await getTreeAsMap(orgHeader);
3408
3409
  const parent = normalizeWikiPath(lsPath);
3409
3410
 
3410
3411
  if (lsRecursive) {
@@ -3462,7 +3463,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3462
3463
  // ── recent ──────────────────────────────────────────────────
3463
3464
  case 'recent': {
3464
3465
  const { limit: recentLimit = 10 } = args;
3465
- const tree = await api('GET', '/api/wiki/tree');
3466
+ const tree = await api('GET', '/api/wiki/tree', undefined, orgHeader);
3466
3467
  const recent = (tree.pages || [])
3467
3468
  .filter(p => p.updatedAt)
3468
3469
  .sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
@@ -3558,7 +3559,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3558
3559
  let existingContent = '';
3559
3560
  let existingId = null;
3560
3561
  try {
3561
- const logPage = await api('GET', '/api/wiki/page?path=log');
3562
+ const logPage = await api('GET', '/api/wiki/page?path=log', undefined, orgHeader);
3562
3563
  existingContent = logPage.content || '';
3563
3564
  existingId = logPage.id;
3564
3565
  } catch {
@@ -3567,7 +3568,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3567
3568
  path: 'log',
3568
3569
  title: 'Log',
3569
3570
  content: entry + '\n',
3570
- });
3571
+ }, orgHeader);
3571
3572
  return ok(withOrg({ appended: true, created: true, pageId: created.id, path: 'log', url: wikiBrowserUrl('log') }));
3572
3573
  }
3573
3574
 
@@ -3579,7 +3580,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3579
3580
 
3580
3581
  // ── health ──────────────────────────────────────────────────
3581
3582
  case 'health': {
3582
- return ok(await api('GET', '/api/wiki/health'));
3583
+ return ok(await api('GET', '/api/wiki/health', undefined, orgHeader));
3583
3584
  }
3584
3585
 
3585
3586
  // ── write ───────────────────────────────────────────────────
@@ -3713,7 +3714,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3713
3714
  type: p.type,
3714
3715
  frontmatter: p.frontmatter,
3715
3716
  }));
3716
- const result = await api('POST', '/api/wiki/pages/bulk', { pages: payload });
3717
+ const result = await api('POST', '/api/wiki/pages/bulk', { pages: payload }, orgHeader);
3717
3718
  return ok(withOrg({
3718
3719
  created: result.created?.length ?? 0,
3719
3720
  updated: result.updated?.length ?? 0,
@@ -3820,7 +3821,7 @@ tool('collector', {
3820
3821
  fresh: z.boolean().optional().describe('[test_start] start a brand-new run instead of resuming your latest'),
3821
3822
  actionId: z.string().optional().describe('[test_resolve] id of the pending action to resolve'),
3822
3823
  approve: z.boolean().optional().describe('[test_resolve] approve (default true) or reject the pending action'),
3823
- 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.'),
3824
+ 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.'),
3824
3825
  name: z.string().optional().describe('[create|update] collector name'),
3825
3826
  description: z.string().optional().describe('[create|update] one-line description shown to the consumer'),
3826
3827
  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.5",
3
+ "version": "1.12.7",
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).