drafted 1.11.25 → 1.11.27
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 +93 -47
- 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. [search] scope the search to this org (default: search ALL your orgs). Required to create a page when you belong to more than one org and none is 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,
|
|
@@ -3331,21 +3349,37 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3331
3349
|
|
|
3332
3350
|
// ── search ──────────────────────────────────────────────────
|
|
3333
3351
|
case 'search': {
|
|
3334
|
-
const { query: searchQuery, limit: searchLimit = 25 } = args;
|
|
3352
|
+
const { query: searchQuery, limit: searchLimit = 25, org: searchOrg } = args;
|
|
3335
3353
|
if (!searchQuery) throw new Error('query required for action=search');
|
|
3336
|
-
|
|
3337
|
-
|
|
3354
|
+
// Default: search ALL the caller's orgs (discovery needs no bound org).
|
|
3355
|
+
// An explicit `org` scopes to that one org.
|
|
3356
|
+
const qp = new URLSearchParams({ q: searchQuery, limit: String(searchLimit) });
|
|
3357
|
+
if (!searchOrg) qp.set('scope', 'all');
|
|
3358
|
+
const result = await api('GET', `/api/wiki/search?${qp.toString()}`, undefined, orgHeader);
|
|
3359
|
+
// Surface pageId (re-address by UUID, org-free) + the hit's own org, and
|
|
3360
|
+
// build the URL from the hit's org so cross-org links are correct.
|
|
3361
|
+
const hits = (result.hits || []).map(h => ({
|
|
3362
|
+
id: h.id,
|
|
3363
|
+
path: h.path,
|
|
3364
|
+
title: h.title,
|
|
3365
|
+
org: h.orgName || h.orgSlug || undefined,
|
|
3366
|
+
orgId: h.orgId || undefined,
|
|
3367
|
+
url: wikiPageUrl(h.path, h.orgId || orgId),
|
|
3368
|
+
}));
|
|
3338
3369
|
markSearched(getSessionState().gates, 'wiki');
|
|
3339
3370
|
return ok({ hits });
|
|
3340
3371
|
}
|
|
3341
3372
|
|
|
3342
3373
|
// ── links ───────────────────────────────────────────────────
|
|
3343
3374
|
case 'links': {
|
|
3344
|
-
const { path: linksPath } = args;
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3375
|
+
const { path: linksPath, pageId: linksPageId } = args;
|
|
3376
|
+
let id = linksPageId;
|
|
3377
|
+
if (!id) {
|
|
3378
|
+
if (!linksPath) throw new Error('path or pageId required for action=links');
|
|
3379
|
+
const page = await api('GET', `/api/wiki/page?path=${encodeURIComponent(normalizeWikiPath(linksPath))}`, undefined, orgHeader);
|
|
3380
|
+
id = page.id;
|
|
3381
|
+
}
|
|
3382
|
+
return ok(await api('GET', `/api/wiki/pages/${id}/links`));
|
|
3349
3383
|
}
|
|
3350
3384
|
|
|
3351
3385
|
// ── log ─────────────────────────────────────────────────────
|
|
@@ -3395,13 +3429,14 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3395
3429
|
if (writeType) body.type = writeType;
|
|
3396
3430
|
if (frontmatter !== undefined) body.frontmatter = frontmatter;
|
|
3397
3431
|
|
|
3398
|
-
// Check if page exists — if so, update; otherwise create
|
|
3432
|
+
// Check if page exists — if so, update; otherwise create. `orgHeader`
|
|
3433
|
+
// (the `org` arg) targets a specific org without switching the session.
|
|
3399
3434
|
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);
|
|
3435
|
+
const existing = await api('GET', `/api/wiki/page?path=${encodeURIComponent(normalized)}`, undefined, orgHeader);
|
|
3436
|
+
const result = await api('PATCH', `/api/wiki/pages/${existing.id}`, body, orgHeader);
|
|
3402
3437
|
return ok(withOrg({ path: result.path, title: result.title, id: result.id, updated: true, url: wikiBrowserUrl(result.path) }));
|
|
3403
3438
|
} catch {
|
|
3404
|
-
const result = await api('POST', '/api/wiki/pages', body);
|
|
3439
|
+
const result = await api('POST', '/api/wiki/pages', body, orgHeader);
|
|
3405
3440
|
return ok(withOrg({ path: result.path, title: result.title, id: result.id, created: true, url: wikiBrowserUrl(result.path) }));
|
|
3406
3441
|
}
|
|
3407
3442
|
}
|
|
@@ -3429,50 +3464,61 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3429
3464
|
});
|
|
3430
3465
|
}
|
|
3431
3466
|
case 'edit': {
|
|
3432
|
-
const { path: editPath, operations: editOps } = args;
|
|
3433
|
-
if (!editPath) throw new Error('path required for action=edit');
|
|
3467
|
+
const { path: editPath, pageId: editPageId, operations: editOps } = args;
|
|
3434
3468
|
if (!Array.isArray(editOps) || editOps.length === 0) throw new Error('operations (array) required for action=edit');
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3469
|
+
let id = editPageId;
|
|
3470
|
+
if (!id) {
|
|
3471
|
+
if (!editPath) throw new Error('path or pageId required for action=edit');
|
|
3472
|
+
const page = await api('GET', `/api/wiki/page?path=${encodeURIComponent(normalizeWikiPath(editPath))}`, undefined, orgHeader);
|
|
3473
|
+
id = page.id;
|
|
3474
|
+
}
|
|
3475
|
+
const result = await api('POST', `/api/wiki/pages/${id}/edit`, { operations: editOps });
|
|
3438
3476
|
return ok(withOrg({ path: result.path, id: result.id, updated: true, applied: result.applied, url: wikiBrowserUrl(result.path) }));
|
|
3439
3477
|
}
|
|
3440
3478
|
|
|
3441
3479
|
// ── mv ──────────────────────────────────────────────────────
|
|
3442
3480
|
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');
|
|
3481
|
+
const { from: mvFrom, to: mvTo, pageId: mvPageId, dryRun: mvDryRun = false } = args;
|
|
3445
3482
|
if (!mvTo) throw new Error('to (destination path) required for action=mv');
|
|
3446
|
-
const fromPath = normalizeWikiPath(mvFrom);
|
|
3447
3483
|
const toPath = normalizeWikiPath(mvTo);
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3484
|
+
let id = mvPageId;
|
|
3485
|
+
if (!id) {
|
|
3486
|
+
if (!mvFrom) throw new Error('from (source path) or pageId required for action=mv');
|
|
3487
|
+
const fromPath = normalizeWikiPath(mvFrom);
|
|
3488
|
+
if (fromPath === toPath) throw new Error('source and destination are the same');
|
|
3489
|
+
const page = await api('GET', `/api/wiki/page?path=${encodeURIComponent(fromPath)}`, undefined, orgHeader);
|
|
3490
|
+
id = page.id;
|
|
3491
|
+
}
|
|
3451
3492
|
|
|
3452
3493
|
if (mvDryRun) {
|
|
3453
|
-
const { referrers } = await api('GET', `/api/wiki/pages/${
|
|
3494
|
+
const { referrers } = await api('GET', `/api/wiki/pages/${id}/referrers`);
|
|
3454
3495
|
return ok(withOrg({ impacted: referrers.map(r => ({ path: r.path, title: r.title })) }));
|
|
3455
3496
|
}
|
|
3456
3497
|
|
|
3457
3498
|
// Server-side cascade: /move rewrites referrers in one transaction
|
|
3458
|
-
const moved = await api('PATCH', `/api/wiki/pages/${
|
|
3499
|
+
const moved = await api('PATCH', `/api/wiki/pages/${id}/move`, { path: toPath });
|
|
3459
3500
|
return ok(withOrg({ path: moved.path, title: moved.title, id: moved.id, referrersUpdated: moved.referrersUpdated ?? 0, url: wikiBrowserUrl(moved.path) }));
|
|
3460
3501
|
}
|
|
3461
3502
|
|
|
3462
3503
|
// ── rm ──────────────────────────────────────────────────────
|
|
3463
3504
|
case 'rm': {
|
|
3464
|
-
const { path: rmPath, dryRun: rmDryRun = false } = args;
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3505
|
+
const { path: rmPath, pageId: rmPageId, dryRun: rmDryRun = false } = args;
|
|
3506
|
+
let id = rmPageId;
|
|
3507
|
+
let normalized = null;
|
|
3508
|
+
if (!id) {
|
|
3509
|
+
if (!rmPath) throw new Error('path or pageId required for action=rm');
|
|
3510
|
+
normalized = normalizeWikiPath(rmPath);
|
|
3511
|
+
const page = await api('GET', `/api/wiki/page?path=${encodeURIComponent(normalized)}`, undefined, orgHeader);
|
|
3512
|
+
id = page.id;
|
|
3513
|
+
}
|
|
3514
|
+
const { referrers } = await api('GET', `/api/wiki/pages/${id}/referrers`);
|
|
3469
3515
|
const broken = referrers.map(r => ({ path: r.path, title: r.title }));
|
|
3470
3516
|
|
|
3471
3517
|
if (rmDryRun) {
|
|
3472
3518
|
return ok(withOrg({ brokenAfterDelete: broken }));
|
|
3473
3519
|
}
|
|
3474
|
-
await api('DELETE', `/api/wiki/pages/${
|
|
3475
|
-
return ok(withOrg({ deleted: true, path: normalized, id
|
|
3520
|
+
await api('DELETE', `/api/wiki/pages/${id}`);
|
|
3521
|
+
return ok(withOrg({ deleted: true, path: normalized, id, brokenReferences: broken }));
|
|
3476
3522
|
}
|
|
3477
3523
|
|
|
3478
3524
|
// ── bulk-write ──────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.27",
|
|
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": [
|