drafted 1.11.24 → 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 +92 -46
- package/package.json +1 -1
package/mcp/server.mjs
CHANGED
|
@@ -1138,6 +1138,18 @@ async function requireBoundOrgForProjectlessMutation(explicitOrg) {
|
|
|
1138
1138
|
const sess = getSessionState();
|
|
1139
1139
|
if (sess.boundOrgId) return; // bound via project open / get_org switch
|
|
1140
1140
|
if (getState().projectId) return; // an active project implies its org
|
|
1141
|
+
// Remote/web sessions are isolated per connection: each one gets its OWN
|
|
1142
|
+
// server-side session row with its own org_id set on connect, so the session
|
|
1143
|
+
// org IS this session's binding — there's no shared, long-lived session to
|
|
1144
|
+
// confuse here the way stdio has. Adopt that org automatically instead of
|
|
1145
|
+
// forcing the agent to call get_org switch / project open before a project-less
|
|
1146
|
+
// wiki or skill write. The DRAFT-36 Phase 4 "refuse to guess" guard below
|
|
1147
|
+
// therefore applies to stdio only.
|
|
1148
|
+
if (isRemote) {
|
|
1149
|
+
const ctx = await getCurrentOrgContext();
|
|
1150
|
+
if (ctx?.id) sess.boundOrgId = ctx.id;
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1141
1153
|
let count = sess.cachedOrgCount;
|
|
1142
1154
|
if (count == null) {
|
|
1143
1155
|
try {
|
|
@@ -1406,6 +1418,7 @@ tool('project', 'START HERE for project management. Dispatch by `action`: list (
|
|
|
1406
1418
|
name: z.string().optional().describe('[create|update] project name'),
|
|
1407
1419
|
description: z.string().nullable().optional().describe('[create|update] project description'),
|
|
1408
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.'),
|
|
1409
1422
|
folder: z.string().nullable().optional().describe('[update] folder name (null to remove from folder)'),
|
|
1410
1423
|
layers: z.array(z.object({}).passthrough()).optional().describe('[update] full layers array replacement. Use ls / to read current layers first.'),
|
|
1411
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.'),
|
|
@@ -1529,12 +1542,14 @@ tool('project', 'START HERE for project management. Dispatch by `action`: list (
|
|
|
1529
1542
|
case 'create': {
|
|
1530
1543
|
const g3 = g3Block(getSessionState().gates);
|
|
1531
1544
|
if (g3) return err(new Error(g3));
|
|
1532
|
-
const { name, description, templateSlug } = args;
|
|
1545
|
+
const { name, description, templateSlug, org } = args;
|
|
1533
1546
|
if (!name) throw new Error('name required for action=create');
|
|
1534
1547
|
const body = { name };
|
|
1535
1548
|
if (description) body.description = description;
|
|
1536
1549
|
if (templateSlug) body.templateSlug = templateSlug;
|
|
1537
|
-
|
|
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)));
|
|
1538
1553
|
}
|
|
1539
1554
|
case 'update': {
|
|
1540
1555
|
const { projectId, name, folder, description, layers } = args;
|
|
@@ -2947,8 +2962,11 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
|
|
|
2947
2962
|
const { action } = args;
|
|
2948
2963
|
// Org-creating/editing skill actions on the project-less surface must have a
|
|
2949
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.
|
|
2950
2967
|
if (['add', 'update', 'remove', 'push', 'fork'].includes(action)) {
|
|
2951
|
-
|
|
2968
|
+
const byId = (action === 'update' || action === 'remove') && /^[a-f0-9-]{36}$/.test(args.skillId || '');
|
|
2969
|
+
if (!byId) await requireBoundOrgForProjectlessMutation(args.org);
|
|
2952
2970
|
}
|
|
2953
2971
|
switch (action) {
|
|
2954
2972
|
case 'search': {
|
|
@@ -3155,6 +3173,8 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
|
|
|
3155
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.', {
|
|
3156
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.'),
|
|
3157
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.'),
|
|
3158
3178
|
recursive: z.boolean().optional().describe('[ls] list recursively with depth indicators'),
|
|
3159
3179
|
limit: z.number().optional().describe('[recent|search] max results (recent default 10, search default 25)'),
|
|
3160
3180
|
query: z.string().optional().describe('[search] term to search in title, path, and content'),
|
|
@@ -3184,10 +3204,13 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3184
3204
|
try {
|
|
3185
3205
|
const { action } = args;
|
|
3186
3206
|
|
|
3187
|
-
//
|
|
3188
|
-
//
|
|
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 } : {};
|
|
3189
3212
|
if (['write', 'edit', 'mv', 'rm', 'bulk-write'].includes(action)) {
|
|
3190
|
-
await requireBoundOrgForProjectlessMutation(
|
|
3213
|
+
if (!args.pageId) await requireBoundOrgForProjectlessMutation(args.org);
|
|
3191
3214
|
}
|
|
3192
3215
|
|
|
3193
3216
|
// Resolve org context once for the wiki tool. Each MCP session is bound
|
|
@@ -3288,22 +3311,29 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3288
3311
|
// Returns content in hashline format (`LINE+ID|content`) so the
|
|
3289
3312
|
// agent can produce hashline edit operations. Mirrors frame.read.
|
|
3290
3313
|
case 'read': {
|
|
3291
|
-
const { path: readPath, lines: readLines } = args;
|
|
3292
|
-
if (
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
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);
|
|
3298
3328
|
}
|
|
3299
|
-
const page = await api('GET', `/api/wiki/page?${params.toString()}`);
|
|
3300
3329
|
// Get backlink count via search (approximate)
|
|
3301
3330
|
let backlinkCount = 0;
|
|
3302
3331
|
try {
|
|
3303
|
-
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);
|
|
3304
3333
|
backlinkCount = (searchRes.hits || []).length;
|
|
3305
3334
|
} catch { /* best-effort */ }
|
|
3306
3335
|
return ok({
|
|
3336
|
+
id: page.id,
|
|
3307
3337
|
path: page.path,
|
|
3308
3338
|
title: page.title,
|
|
3309
3339
|
type: page.type,
|
|
@@ -3321,19 +3351,23 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3321
3351
|
case 'search': {
|
|
3322
3352
|
const { query: searchQuery, limit: searchLimit = 25 } = args;
|
|
3323
3353
|
if (!searchQuery) throw new Error('query required for action=search');
|
|
3324
|
-
const result = await api('GET', `/api/wiki/search?q=${encodeURIComponent(searchQuery)}&limit=${searchLimit}
|
|
3325
|
-
|
|
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) }));
|
|
3326
3357
|
markSearched(getSessionState().gates, 'wiki');
|
|
3327
3358
|
return ok({ hits });
|
|
3328
3359
|
}
|
|
3329
3360
|
|
|
3330
3361
|
// ── links ───────────────────────────────────────────────────
|
|
3331
3362
|
case 'links': {
|
|
3332
|
-
const { path: linksPath } = args;
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
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`));
|
|
3337
3371
|
}
|
|
3338
3372
|
|
|
3339
3373
|
// ── log ─────────────────────────────────────────────────────
|
|
@@ -3383,13 +3417,14 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3383
3417
|
if (writeType) body.type = writeType;
|
|
3384
3418
|
if (frontmatter !== undefined) body.frontmatter = frontmatter;
|
|
3385
3419
|
|
|
3386
|
-
// 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.
|
|
3387
3422
|
try {
|
|
3388
|
-
const existing = await api('GET', `/api/wiki/page?path=${encodeURIComponent(normalized)}
|
|
3389
|
-
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);
|
|
3390
3425
|
return ok(withOrg({ path: result.path, title: result.title, id: result.id, updated: true, url: wikiBrowserUrl(result.path) }));
|
|
3391
3426
|
} catch {
|
|
3392
|
-
const result = await api('POST', '/api/wiki/pages', body);
|
|
3427
|
+
const result = await api('POST', '/api/wiki/pages', body, orgHeader);
|
|
3393
3428
|
return ok(withOrg({ path: result.path, title: result.title, id: result.id, created: true, url: wikiBrowserUrl(result.path) }));
|
|
3394
3429
|
}
|
|
3395
3430
|
}
|
|
@@ -3417,50 +3452,61 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3417
3452
|
});
|
|
3418
3453
|
}
|
|
3419
3454
|
case 'edit': {
|
|
3420
|
-
const { path: editPath, operations: editOps } = args;
|
|
3421
|
-
if (!editPath) throw new Error('path required for action=edit');
|
|
3455
|
+
const { path: editPath, pageId: editPageId, operations: editOps } = args;
|
|
3422
3456
|
if (!Array.isArray(editOps) || editOps.length === 0) throw new Error('operations (array) required for action=edit');
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
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 });
|
|
3426
3464
|
return ok(withOrg({ path: result.path, id: result.id, updated: true, applied: result.applied, url: wikiBrowserUrl(result.path) }));
|
|
3427
3465
|
}
|
|
3428
3466
|
|
|
3429
3467
|
// ── mv ──────────────────────────────────────────────────────
|
|
3430
3468
|
case 'mv': {
|
|
3431
|
-
const { from: mvFrom, to: mvTo, dryRun: mvDryRun = false } = args;
|
|
3432
|
-
if (!mvFrom) throw new Error('from (source path) required for action=mv');
|
|
3469
|
+
const { from: mvFrom, to: mvTo, pageId: mvPageId, dryRun: mvDryRun = false } = args;
|
|
3433
3470
|
if (!mvTo) throw new Error('to (destination path) required for action=mv');
|
|
3434
|
-
const fromPath = normalizeWikiPath(mvFrom);
|
|
3435
3471
|
const toPath = normalizeWikiPath(mvTo);
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
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
|
+
}
|
|
3439
3480
|
|
|
3440
3481
|
if (mvDryRun) {
|
|
3441
|
-
const { referrers } = await api('GET', `/api/wiki/pages/${
|
|
3482
|
+
const { referrers } = await api('GET', `/api/wiki/pages/${id}/referrers`);
|
|
3442
3483
|
return ok(withOrg({ impacted: referrers.map(r => ({ path: r.path, title: r.title })) }));
|
|
3443
3484
|
}
|
|
3444
3485
|
|
|
3445
3486
|
// Server-side cascade: /move rewrites referrers in one transaction
|
|
3446
|
-
const moved = await api('PATCH', `/api/wiki/pages/${
|
|
3487
|
+
const moved = await api('PATCH', `/api/wiki/pages/${id}/move`, { path: toPath });
|
|
3447
3488
|
return ok(withOrg({ path: moved.path, title: moved.title, id: moved.id, referrersUpdated: moved.referrersUpdated ?? 0, url: wikiBrowserUrl(moved.path) }));
|
|
3448
3489
|
}
|
|
3449
3490
|
|
|
3450
3491
|
// ── rm ──────────────────────────────────────────────────────
|
|
3451
3492
|
case 'rm': {
|
|
3452
|
-
const { path: rmPath, dryRun: rmDryRun = false } = args;
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
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`);
|
|
3457
3503
|
const broken = referrers.map(r => ({ path: r.path, title: r.title }));
|
|
3458
3504
|
|
|
3459
3505
|
if (rmDryRun) {
|
|
3460
3506
|
return ok(withOrg({ brokenAfterDelete: broken }));
|
|
3461
3507
|
}
|
|
3462
|
-
await api('DELETE', `/api/wiki/pages/${
|
|
3463
|
-
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 }));
|
|
3464
3510
|
}
|
|
3465
3511
|
|
|
3466
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": [
|