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
|
-
-
|
|
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
|
|
|
@@ -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 (
|
|
227
|
-
get_org: { title: 'Organization', readOnlyHint:
|
|
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 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
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
|
|
1239
|
-
//
|
|
1240
|
-
//
|
|
1241
|
-
//
|
|
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
|
|
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
|
|
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
|
|
1271
|
-
`
|
|
1272
|
-
`would land in whichever org the session inherited.
|
|
1273
|
-
`
|
|
1274
|
-
`(
|
|
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 (
|
|
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
|
|
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
|
|
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', '
|
|
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: "
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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**
|
|
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('
|
|
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
|
|
3378
|
-
//
|
|
3379
|
-
|
|
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).
|
|
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.
|
|
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
|
|
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).
|