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.
Files changed (2) hide show
  1. package/mcp/server.mjs +80 -46
  2. 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
- return ok(withProjectBreadcrumb(await api('POST', '/api/projects', body)));
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
- await requireBoundOrgForProjectlessMutation(args.org);
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
- // Project-less wiki mutations must have a bound org when the user is
3200
- // multi-org, so a write never silently lands in an inherited org. (Ph4.)
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(undefined);
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 (!readPath) throw new Error('path required for action=read');
3305
- const normalized = normalizeWikiPath(readPath);
3306
- const params = new URLSearchParams({ path: normalized, format: 'hashline' });
3307
- if (readLines) {
3308
- if (!/^\d+-\d+$/.test(readLines)) throw new Error(`lines must be "N-M" (e.g. "10-50"), got: ${readLines}`);
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(normalized)}`);
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
- const hits = (result.hits || []).map(h => ({ path: h.path, title: h.title, url: wikiBrowserUrl(h.path) }));
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
- if (!linksPath) throw new Error('path required for action=links');
3346
- const normalized = normalizeWikiPath(linksPath);
3347
- const page = await api('GET', `/api/wiki/page?path=${encodeURIComponent(normalized)}`);
3348
- return ok(await api('GET', `/api/wiki/pages/${page.id}/links`));
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
- const normalized = normalizeWikiPath(editPath);
3436
- const page = await api('GET', `/api/wiki/page?path=${encodeURIComponent(normalized)}`);
3437
- const result = await api('POST', `/api/wiki/pages/${page.id}/edit`, { operations: editOps });
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
- if (fromPath === toPath) throw new Error('source and destination are the same');
3449
-
3450
- const page = await api('GET', `/api/wiki/page?path=${encodeURIComponent(fromPath)}`);
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/${page.id}/referrers`);
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/${page.id}/move`, { path: toPath });
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
- if (!rmPath) throw new Error('path required for action=rm');
3466
- const normalized = normalizeWikiPath(rmPath);
3467
- const page = await api('GET', `/api/wiki/page?path=${encodeURIComponent(normalized)}`);
3468
- const { referrers } = await api('GET', `/api/wiki/pages/${page.id}/referrers`);
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/${page.id}`);
3475
- return ok(withOrg({ deleted: true, path: normalized, id: page.id, brokenReferences: broken }));
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.25",
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": [