drafted 1.11.25 → 1.11.26
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.
- package/mcp/server.mjs +80 -46
- package/package.json +1 -1
package/mcp/server.mjs
CHANGED
|
@@ -1418,6 +1418,7 @@ tool('project', 'START HERE for project management. Dispatch by `action`: list (
|
|
|
1418
1418
|
name: z.string().optional().describe('[create|update] project name'),
|
|
1419
1419
|
description: z.string().nullable().optional().describe('[create|update] project description'),
|
|
1420
1420
|
templateSlug: z.string().optional().describe('[create] template slug (e.g. "web-design", "mobile-app", "landing-page")'),
|
|
1421
|
+
org: z.string().optional().describe('[create] org slug or id to create the project in, without switching the session. Defaults to the bound/active org.'),
|
|
1421
1422
|
folder: z.string().nullable().optional().describe('[update] folder name (null to remove from folder)'),
|
|
1422
1423
|
layers: z.array(z.object({}).passthrough()).optional().describe('[update] full layers array replacement. Use ls / to read current layers first.'),
|
|
1423
1424
|
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.'),
|
|
@@ -1541,12 +1542,14 @@ tool('project', 'START HERE for project management. Dispatch by `action`: list (
|
|
|
1541
1542
|
case 'create': {
|
|
1542
1543
|
const g3 = g3Block(getSessionState().gates);
|
|
1543
1544
|
if (g3) return err(new Error(g3));
|
|
1544
|
-
const { name, description, templateSlug } = args;
|
|
1545
|
+
const { name, description, templateSlug, org } = args;
|
|
1545
1546
|
if (!name) throw new Error('name required for action=create');
|
|
1546
1547
|
const body = { name };
|
|
1547
1548
|
if (description) body.description = description;
|
|
1548
1549
|
if (templateSlug) body.templateSlug = templateSlug;
|
|
1549
|
-
|
|
1550
|
+
// `org` targets a specific org without switching the session cursor.
|
|
1551
|
+
const createExtra = org ? { 'X-Drafted-Org': org } : {};
|
|
1552
|
+
return ok(withProjectBreadcrumb(await api('POST', '/api/projects', body, createExtra)));
|
|
1550
1553
|
}
|
|
1551
1554
|
case 'update': {
|
|
1552
1555
|
const { projectId, name, folder, description, layers } = args;
|
|
@@ -2959,8 +2962,11 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
|
|
|
2959
2962
|
const { action } = args;
|
|
2960
2963
|
// Org-creating/editing skill actions on the project-less surface must have a
|
|
2961
2964
|
// bound org (or explicit org=) when the user is multi-org. (DRAFT-36 Phase 4.)
|
|
2965
|
+
// UUID-first exception: update/remove addressed by a skillId derive their org
|
|
2966
|
+
// from the skill row server-side, so the bound-org gate is unnecessary.
|
|
2962
2967
|
if (['add', 'update', 'remove', 'push', 'fork'].includes(action)) {
|
|
2963
|
-
|
|
2968
|
+
const byId = (action === 'update' || action === 'remove') && /^[a-f0-9-]{36}$/.test(args.skillId || '');
|
|
2969
|
+
if (!byId) await requireBoundOrgForProjectlessMutation(args.org);
|
|
2964
2970
|
}
|
|
2965
2971
|
switch (action) {
|
|
2966
2972
|
case 'search': {
|
|
@@ -3167,6 +3173,8 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
|
|
|
3167
3173
|
tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and other agents/humans share maintenance — every edit broadcasts live, and edits from others appear in `recent` and on `read`.\n\n**Wiki always operates on the ACTIVE org.** If the content you need lives in a different org, switch first via `get_org(action="switch", orgId=...)` — don\'t assume "no hits" means the content doesn\'t exist. Verify the active org with `get_org` before searching.\n\nBefore mutating: check `recent` and `search` for relevant existing pages. Before mv/rm: check `links` (or pass `dryRun=true`). After completing a logical session of work, append a `log` entry.\n\nThe tool handles bookkeeping you\'d otherwise forget: `mv` rewrites inbound references via the link index, `read` shows who edited last and when. Use `health` to find unlinked pages and broken links.\n\n**Skill gate:** the org may attach a `wiki-maintainer` skill that you MUST load before mutations. If you get a skill-gate error, run skill(action="load", skill="wiki-maintainer") then retry.', {
|
|
3168
3174
|
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.'),
|
|
3169
3175
|
path: z.string().optional().describe('[ls|read|links] wiki path. For ls: default / (root). For read: required. For links: required.'),
|
|
3176
|
+
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.'),
|
|
3177
|
+
org: z.string().optional().describe('[write] org slug or id to create/target the page in, without switching the session. Required to create a page when you belong to more than one org and none is bound.'),
|
|
3170
3178
|
recursive: z.boolean().optional().describe('[ls] list recursively with depth indicators'),
|
|
3171
3179
|
limit: z.number().optional().describe('[recent|search] max results (recent default 10, search default 25)'),
|
|
3172
3180
|
query: z.string().optional().describe('[search] term to search in title, path, and content'),
|
|
@@ -3196,10 +3204,13 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3196
3204
|
try {
|
|
3197
3205
|
const { action } = args;
|
|
3198
3206
|
|
|
3199
|
-
//
|
|
3200
|
-
//
|
|
3207
|
+
// UUID-first: a `pageId` (edit/mv/rm) or an explicit `org` (write) makes the
|
|
3208
|
+
// target org unambiguous, so the bound-org gate is unnecessary — skip it.
|
|
3209
|
+
// Otherwise (path-addressed, multi-org, nothing bound) the gate still
|
|
3210
|
+
// refuses to guess the org so a write never silently lands in the wrong one.
|
|
3211
|
+
const orgHeader = args.org ? { 'X-Drafted-Org': args.org } : {};
|
|
3201
3212
|
if (['write', 'edit', 'mv', 'rm', 'bulk-write'].includes(action)) {
|
|
3202
|
-
await requireBoundOrgForProjectlessMutation(
|
|
3213
|
+
if (!args.pageId) await requireBoundOrgForProjectlessMutation(args.org);
|
|
3203
3214
|
}
|
|
3204
3215
|
|
|
3205
3216
|
// Resolve org context once for the wiki tool. Each MCP session is bound
|
|
@@ -3300,22 +3311,29 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3300
3311
|
// Returns content in hashline format (`LINE+ID|content`) so the
|
|
3301
3312
|
// agent can produce hashline edit operations. Mirrors frame.read.
|
|
3302
3313
|
case 'read': {
|
|
3303
|
-
const { path: readPath, lines: readLines } = args;
|
|
3304
|
-
if (
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
params.set('lines', readLines);
|
|
3314
|
+
const { path: readPath, pageId: readPageId, lines: readLines } = args;
|
|
3315
|
+
if (readLines && !/^\d+-\d+$/.test(readLines)) throw new Error(`lines must be "N-M" (e.g. "10-50"), got: ${readLines}`);
|
|
3316
|
+
let page;
|
|
3317
|
+
if (readPageId) {
|
|
3318
|
+
// UUID-first: address the page directly, org auto-derives server-side.
|
|
3319
|
+
const params = new URLSearchParams({ format: 'hashline' });
|
|
3320
|
+
if (readLines) params.set('lines', readLines);
|
|
3321
|
+
page = await api('GET', `/api/wiki/pages/${readPageId}?${params.toString()}`);
|
|
3322
|
+
} else {
|
|
3323
|
+
if (!readPath) throw new Error('path or pageId required for action=read');
|
|
3324
|
+
const normalized = normalizeWikiPath(readPath);
|
|
3325
|
+
const params = new URLSearchParams({ path: normalized, format: 'hashline' });
|
|
3326
|
+
if (readLines) params.set('lines', readLines);
|
|
3327
|
+
page = await api('GET', `/api/wiki/page?${params.toString()}`, undefined, orgHeader);
|
|
3310
3328
|
}
|
|
3311
|
-
const page = await api('GET', `/api/wiki/page?${params.toString()}`);
|
|
3312
3329
|
// Get backlink count via search (approximate)
|
|
3313
3330
|
let backlinkCount = 0;
|
|
3314
3331
|
try {
|
|
3315
|
-
const searchRes = await api('GET', `/api/wiki/search?q=${encodeURIComponent(
|
|
3332
|
+
const searchRes = await api('GET', `/api/wiki/search?q=${encodeURIComponent(page.path)}`, undefined, orgHeader);
|
|
3316
3333
|
backlinkCount = (searchRes.hits || []).length;
|
|
3317
3334
|
} catch { /* best-effort */ }
|
|
3318
3335
|
return ok({
|
|
3336
|
+
id: page.id,
|
|
3319
3337
|
path: page.path,
|
|
3320
3338
|
title: page.title,
|
|
3321
3339
|
type: page.type,
|
|
@@ -3333,19 +3351,23 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3333
3351
|
case 'search': {
|
|
3334
3352
|
const { query: searchQuery, limit: searchLimit = 25 } = args;
|
|
3335
3353
|
if (!searchQuery) throw new Error('query required for action=search');
|
|
3336
|
-
const result = await api('GET', `/api/wiki/search?q=${encodeURIComponent(searchQuery)}&limit=${searchLimit}
|
|
3337
|
-
|
|
3354
|
+
const result = await api('GET', `/api/wiki/search?q=${encodeURIComponent(searchQuery)}&limit=${searchLimit}`, undefined, orgHeader);
|
|
3355
|
+
// Surface pageId so the agent can re-address the page by UUID (org-free).
|
|
3356
|
+
const hits = (result.hits || []).map(h => ({ id: h.id, path: h.path, title: h.title, url: wikiBrowserUrl(h.path) }));
|
|
3338
3357
|
markSearched(getSessionState().gates, 'wiki');
|
|
3339
3358
|
return ok({ hits });
|
|
3340
3359
|
}
|
|
3341
3360
|
|
|
3342
3361
|
// ── links ───────────────────────────────────────────────────
|
|
3343
3362
|
case 'links': {
|
|
3344
|
-
const { path: linksPath } = args;
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3363
|
+
const { path: linksPath, pageId: linksPageId } = args;
|
|
3364
|
+
let id = linksPageId;
|
|
3365
|
+
if (!id) {
|
|
3366
|
+
if (!linksPath) throw new Error('path or pageId required for action=links');
|
|
3367
|
+
const page = await api('GET', `/api/wiki/page?path=${encodeURIComponent(normalizeWikiPath(linksPath))}`, undefined, orgHeader);
|
|
3368
|
+
id = page.id;
|
|
3369
|
+
}
|
|
3370
|
+
return ok(await api('GET', `/api/wiki/pages/${id}/links`));
|
|
3349
3371
|
}
|
|
3350
3372
|
|
|
3351
3373
|
// ── log ─────────────────────────────────────────────────────
|
|
@@ -3395,13 +3417,14 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3395
3417
|
if (writeType) body.type = writeType;
|
|
3396
3418
|
if (frontmatter !== undefined) body.frontmatter = frontmatter;
|
|
3397
3419
|
|
|
3398
|
-
// Check if page exists — if so, update; otherwise create
|
|
3420
|
+
// Check if page exists — if so, update; otherwise create. `orgHeader`
|
|
3421
|
+
// (the `org` arg) targets a specific org without switching the session.
|
|
3399
3422
|
try {
|
|
3400
|
-
const existing = await api('GET', `/api/wiki/page?path=${encodeURIComponent(normalized)}
|
|
3401
|
-
const result = await api('PATCH', `/api/wiki/pages/${existing.id}`, body);
|
|
3423
|
+
const existing = await api('GET', `/api/wiki/page?path=${encodeURIComponent(normalized)}`, undefined, orgHeader);
|
|
3424
|
+
const result = await api('PATCH', `/api/wiki/pages/${existing.id}`, body, orgHeader);
|
|
3402
3425
|
return ok(withOrg({ path: result.path, title: result.title, id: result.id, updated: true, url: wikiBrowserUrl(result.path) }));
|
|
3403
3426
|
} catch {
|
|
3404
|
-
const result = await api('POST', '/api/wiki/pages', body);
|
|
3427
|
+
const result = await api('POST', '/api/wiki/pages', body, orgHeader);
|
|
3405
3428
|
return ok(withOrg({ path: result.path, title: result.title, id: result.id, created: true, url: wikiBrowserUrl(result.path) }));
|
|
3406
3429
|
}
|
|
3407
3430
|
}
|
|
@@ -3429,50 +3452,61 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3429
3452
|
});
|
|
3430
3453
|
}
|
|
3431
3454
|
case 'edit': {
|
|
3432
|
-
const { path: editPath, operations: editOps } = args;
|
|
3433
|
-
if (!editPath) throw new Error('path required for action=edit');
|
|
3455
|
+
const { path: editPath, pageId: editPageId, operations: editOps } = args;
|
|
3434
3456
|
if (!Array.isArray(editOps) || editOps.length === 0) throw new Error('operations (array) required for action=edit');
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3457
|
+
let id = editPageId;
|
|
3458
|
+
if (!id) {
|
|
3459
|
+
if (!editPath) throw new Error('path or pageId required for action=edit');
|
|
3460
|
+
const page = await api('GET', `/api/wiki/page?path=${encodeURIComponent(normalizeWikiPath(editPath))}`, undefined, orgHeader);
|
|
3461
|
+
id = page.id;
|
|
3462
|
+
}
|
|
3463
|
+
const result = await api('POST', `/api/wiki/pages/${id}/edit`, { operations: editOps });
|
|
3438
3464
|
return ok(withOrg({ path: result.path, id: result.id, updated: true, applied: result.applied, url: wikiBrowserUrl(result.path) }));
|
|
3439
3465
|
}
|
|
3440
3466
|
|
|
3441
3467
|
// ── mv ──────────────────────────────────────────────────────
|
|
3442
3468
|
case 'mv': {
|
|
3443
|
-
const { from: mvFrom, to: mvTo, dryRun: mvDryRun = false } = args;
|
|
3444
|
-
if (!mvFrom) throw new Error('from (source path) required for action=mv');
|
|
3469
|
+
const { from: mvFrom, to: mvTo, pageId: mvPageId, dryRun: mvDryRun = false } = args;
|
|
3445
3470
|
if (!mvTo) throw new Error('to (destination path) required for action=mv');
|
|
3446
|
-
const fromPath = normalizeWikiPath(mvFrom);
|
|
3447
3471
|
const toPath = normalizeWikiPath(mvTo);
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3472
|
+
let id = mvPageId;
|
|
3473
|
+
if (!id) {
|
|
3474
|
+
if (!mvFrom) throw new Error('from (source path) or pageId required for action=mv');
|
|
3475
|
+
const fromPath = normalizeWikiPath(mvFrom);
|
|
3476
|
+
if (fromPath === toPath) throw new Error('source and destination are the same');
|
|
3477
|
+
const page = await api('GET', `/api/wiki/page?path=${encodeURIComponent(fromPath)}`, undefined, orgHeader);
|
|
3478
|
+
id = page.id;
|
|
3479
|
+
}
|
|
3451
3480
|
|
|
3452
3481
|
if (mvDryRun) {
|
|
3453
|
-
const { referrers } = await api('GET', `/api/wiki/pages/${
|
|
3482
|
+
const { referrers } = await api('GET', `/api/wiki/pages/${id}/referrers`);
|
|
3454
3483
|
return ok(withOrg({ impacted: referrers.map(r => ({ path: r.path, title: r.title })) }));
|
|
3455
3484
|
}
|
|
3456
3485
|
|
|
3457
3486
|
// Server-side cascade: /move rewrites referrers in one transaction
|
|
3458
|
-
const moved = await api('PATCH', `/api/wiki/pages/${
|
|
3487
|
+
const moved = await api('PATCH', `/api/wiki/pages/${id}/move`, { path: toPath });
|
|
3459
3488
|
return ok(withOrg({ path: moved.path, title: moved.title, id: moved.id, referrersUpdated: moved.referrersUpdated ?? 0, url: wikiBrowserUrl(moved.path) }));
|
|
3460
3489
|
}
|
|
3461
3490
|
|
|
3462
3491
|
// ── rm ──────────────────────────────────────────────────────
|
|
3463
3492
|
case 'rm': {
|
|
3464
|
-
const { path: rmPath, dryRun: rmDryRun = false } = args;
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3493
|
+
const { path: rmPath, pageId: rmPageId, dryRun: rmDryRun = false } = args;
|
|
3494
|
+
let id = rmPageId;
|
|
3495
|
+
let normalized = null;
|
|
3496
|
+
if (!id) {
|
|
3497
|
+
if (!rmPath) throw new Error('path or pageId required for action=rm');
|
|
3498
|
+
normalized = normalizeWikiPath(rmPath);
|
|
3499
|
+
const page = await api('GET', `/api/wiki/page?path=${encodeURIComponent(normalized)}`, undefined, orgHeader);
|
|
3500
|
+
id = page.id;
|
|
3501
|
+
}
|
|
3502
|
+
const { referrers } = await api('GET', `/api/wiki/pages/${id}/referrers`);
|
|
3469
3503
|
const broken = referrers.map(r => ({ path: r.path, title: r.title }));
|
|
3470
3504
|
|
|
3471
3505
|
if (rmDryRun) {
|
|
3472
3506
|
return ok(withOrg({ brokenAfterDelete: broken }));
|
|
3473
3507
|
}
|
|
3474
|
-
await api('DELETE', `/api/wiki/pages/${
|
|
3475
|
-
return ok(withOrg({ deleted: true, path: normalized, id
|
|
3508
|
+
await api('DELETE', `/api/wiki/pages/${id}`);
|
|
3509
|
+
return ok(withOrg({ deleted: true, path: normalized, id, brokenReferences: broken }));
|
|
3476
3510
|
}
|
|
3477
3511
|
|
|
3478
3512
|
// ── bulk-write ──────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.26",
|
|
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": [
|