drafted 1.12.5 → 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('[add] org (id or name)
|
|
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,13 +3161,13 @@ 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': {
|
|
@@ -3334,11 +3313,11 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
|
|
|
3334
3313
|
// All 11 actions dispatch from one tool. Read-only actions skip the
|
|
3335
3314
|
// skill gate; mutations require org-level wiki-maintainer skills loaded.
|
|
3336
3315
|
|
|
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**
|
|
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.', {
|
|
3338
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.'),
|
|
3339
3318
|
path: z.string().optional().describe('[ls|read|links] wiki path. For ls: default / (root). For read: required. For links: required.'),
|
|
3340
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.'),
|
|
3341
|
-
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).'),
|
|
3342
3321
|
recursive: z.boolean().optional().describe('[ls] list recursively with depth indicators'),
|
|
3343
3322
|
limit: z.number().optional().describe('[recent|search] max results (recent default 10, search default 25)'),
|
|
3344
3323
|
query: z.string().optional().describe('[search] term to search in title, path, and content'),
|
|
@@ -3377,9 +3356,19 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3377
3356
|
if (!args.pageId) await requireBoundOrgForProjectlessMutation(args.org);
|
|
3378
3357
|
}
|
|
3379
3358
|
|
|
3380
|
-
// Resolve org context
|
|
3381
|
-
//
|
|
3382
|
-
|
|
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
|
+
}
|
|
3383
3372
|
const orgId = orgCtx?.id || null;
|
|
3384
3373
|
// Mutation responses include `org` so the agent always sees where the
|
|
3385
3374
|
// write landed — eliminates silent cross-org confusion.
|
|
@@ -3404,7 +3393,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3404
3393
|
// ── ls ──────────────────────────────────────────────────────
|
|
3405
3394
|
case 'ls': {
|
|
3406
3395
|
const { path: lsPath = '/', recursive: lsRecursive = false } = args;
|
|
3407
|
-
const { pages, pathToPage } = await getTreeAsMap();
|
|
3396
|
+
const { pages, pathToPage } = await getTreeAsMap(orgHeader);
|
|
3408
3397
|
const parent = normalizeWikiPath(lsPath);
|
|
3409
3398
|
|
|
3410
3399
|
if (lsRecursive) {
|
|
@@ -3462,7 +3451,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3462
3451
|
// ── recent ──────────────────────────────────────────────────
|
|
3463
3452
|
case 'recent': {
|
|
3464
3453
|
const { limit: recentLimit = 10 } = args;
|
|
3465
|
-
const tree = await api('GET', '/api/wiki/tree');
|
|
3454
|
+
const tree = await api('GET', '/api/wiki/tree', undefined, orgHeader);
|
|
3466
3455
|
const recent = (tree.pages || [])
|
|
3467
3456
|
.filter(p => p.updatedAt)
|
|
3468
3457
|
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
|
|
@@ -3558,7 +3547,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3558
3547
|
let existingContent = '';
|
|
3559
3548
|
let existingId = null;
|
|
3560
3549
|
try {
|
|
3561
|
-
const logPage = await api('GET', '/api/wiki/page?path=log');
|
|
3550
|
+
const logPage = await api('GET', '/api/wiki/page?path=log', undefined, orgHeader);
|
|
3562
3551
|
existingContent = logPage.content || '';
|
|
3563
3552
|
existingId = logPage.id;
|
|
3564
3553
|
} catch {
|
|
@@ -3567,7 +3556,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3567
3556
|
path: 'log',
|
|
3568
3557
|
title: 'Log',
|
|
3569
3558
|
content: entry + '\n',
|
|
3570
|
-
});
|
|
3559
|
+
}, orgHeader);
|
|
3571
3560
|
return ok(withOrg({ appended: true, created: true, pageId: created.id, path: 'log', url: wikiBrowserUrl('log') }));
|
|
3572
3561
|
}
|
|
3573
3562
|
|
|
@@ -3579,7 +3568,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3579
3568
|
|
|
3580
3569
|
// ── health ──────────────────────────────────────────────────
|
|
3581
3570
|
case 'health': {
|
|
3582
|
-
return ok(await api('GET', '/api/wiki/health'));
|
|
3571
|
+
return ok(await api('GET', '/api/wiki/health', undefined, orgHeader));
|
|
3583
3572
|
}
|
|
3584
3573
|
|
|
3585
3574
|
// ── write ───────────────────────────────────────────────────
|
|
@@ -3713,7 +3702,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3713
3702
|
type: p.type,
|
|
3714
3703
|
frontmatter: p.frontmatter,
|
|
3715
3704
|
}));
|
|
3716
|
-
const result = await api('POST', '/api/wiki/pages/bulk', { pages: payload });
|
|
3705
|
+
const result = await api('POST', '/api/wiki/pages/bulk', { pages: payload }, orgHeader);
|
|
3717
3706
|
return ok(withOrg({
|
|
3718
3707
|
created: result.created?.length ?? 0,
|
|
3719
3708
|
updated: result.updated?.length ?? 0,
|
|
@@ -3820,7 +3809,7 @@ tool('collector', {
|
|
|
3820
3809
|
fresh: z.boolean().optional().describe('[test_start] start a brand-new run instead of resuming your latest'),
|
|
3821
3810
|
actionId: z.string().optional().describe('[test_resolve] id of the pending action to resolve'),
|
|
3822
3811
|
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).
|
|
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.'),
|
|
3824
3813
|
name: z.string().optional().describe('[create|update] collector name'),
|
|
3825
3814
|
description: z.string().optional().describe('[create|update] one-line description shown to the consumer'),
|
|
3826
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).
|