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
|
-
-
|
|
18
|
-
- Check get_org(action="get") for googleDrive.connected. When Google Drive is connected for the
|
|
19
|
-
- Never assume the current
|
|
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
|
|
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 (
|
|
227
|
-
get_org: { title: 'Organization', readOnlyHint:
|
|
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 switching — org 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
|
|
1239
|
-
//
|
|
1240
|
-
//
|
|
1241
|
-
//
|
|
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
|
|
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
|
|
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
|
|
1271
|
-
`
|
|
1272
|
-
`would land in whichever org the session inherited.
|
|
1273
|
-
`
|
|
1274
|
-
`(
|
|
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 {
|
|
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 (
|
|
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
|
|
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
|
|
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', '
|
|
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: "
|
|
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)
|
|
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
|
-
//
|
|
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
|
|
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**
|
|
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('
|
|
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
|
|
3381
|
-
//
|
|
3382
|
-
|
|
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).
|
|
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.
|
|
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
|
|
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).
|