drafted 1.14.12 → 1.14.14

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 +28 -9
  2. package/package.json +1 -1
package/mcp/server.mjs CHANGED
@@ -1450,12 +1450,23 @@ async function requireBoundOrgForProjectlessMutation(explicitOrg) {
1450
1450
  return;
1451
1451
  }
1452
1452
  if (projectlessMutationNeedsOrg({ orgCount: orgs.length })) {
1453
+ // Actionable, not a dead end: name the orgs and the ONE call that binds this session,
1454
+ // so the agent recovers itself instead of stalling on the human. Project-less wiki and
1455
+ // skill work is a first-class flow — no project required, ever. The only thing refused
1456
+ // is GUESSING which org, which is what silently misfiled a page into the user's
1457
+ // default org. (Listing the orgs is safe: they're the caller's own memberships.)
1458
+ const names = orgs.map(o => o.name || o.id).filter(Boolean);
1453
1459
  throw new Error(
1454
- `Refusing to guess the org: no project is open and no explicit org was ` +
1455
- `given, but you belong to ${orgs.length} orgs a project-less wiki/skill write ` +
1456
- `would land in whichever org the session inherited. Pass org=... on this ` +
1457
- `call (org name or id), or open the relevant project first ` +
1458
- `(project(action="open")the org derives from the project).`
1460
+ `Which org? A project-less wiki/skill write needs one, and you belong to ${orgs.length}: ` +
1461
+ `${names.join(', ')}. Don't guess an unaddressed write lands in whichever org this ` +
1462
+ `session inherited, which is how a page meant for one org ends up in another.\n` +
1463
+ `Recover with ONE call: get_org(action="use", org="<name>"). That binds this session's ` +
1464
+ `working org every later project-less wiki/skill write then just works, with no project ` +
1465
+ `and no org= to repeat. Prefer it over passing org= per call, which pays this toll on ` +
1466
+ `every write.\n` +
1467
+ `(Alternatives: org="<name>" on this single call; or project(action="open") if the work ` +
1468
+ `belongs to a project — the org derives from it.)\n` +
1469
+ `Pick from the conversation if the org is clear from context; ask the user only if it isn't.`
1459
1470
  );
1460
1471
  }
1461
1472
  }
@@ -1853,6 +1864,12 @@ async function sessionSurfaceBlock() {
1853
1864
  color: agentSurface.color,
1854
1865
  surfaced: true,
1855
1866
  alive: !!agentSurface.alive,
1867
+ // `alive` was ambiguous: agents couldn't tell whether it meant "your writes are
1868
+ // no-ops" (act differently) or "the user's canvas tab is closed" (ignore). It's the
1869
+ // latter — say so, so nobody changes behavior over it.
1870
+ aliveMeaning: agentSurface.alive
1871
+ ? 'A canvas/desktop surface is live for this session — focus and presence calls will be seen.'
1872
+ : 'No canvas surface is currently open for this session. Frame, wiki, and skill writes all persist normally — only focus/presence have nothing to draw on. Do not change what you write because of this.',
1856
1873
  };
1857
1874
  }
1858
1875
  // No WS ack yet — best-effort identity from /auth/me so callers still get a userId/org.
@@ -2661,7 +2678,7 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
2661
2678
  path: z.string().optional().describe('[read] /{layer}/{lane}/{filename}, frame URL, or UUID. [write|edit|anchor] /{layer}/{lane}/{filename}.'),
2662
2679
  lines: z.string().optional().describe('[read] line range (e.g. "1-50"). Omit to read all.'),
2663
2680
  content: z.string().optional().describe('[write] HTML/markdown/text for Drafted inline frames. [write_doc_content|append_doc_content] native Google Doc body text. Do not use action=write content to populate Google Doc/Slide frames.'),
2664
- excalidraw_data: z.any().optional().describe('[write_excalidraw] Excalidraw scene JSON object or JSON string. Defaults to an empty scene.'),
2681
+ excalidraw_data: z.any().optional().describe('[write_excalidraw] Excalidraw scene JSON object or JSON string. Defaults to an empty scene. WRITE LEAN SCENES: your MCP client caps tool-call arguments (a big scene is rejected before Drafted ever sees it), so emit only the fields that carry meaning — id, type, x, y, width, height, angle, text/label, strokeColor, backgroundColor, fillStyle, strokeWidth, and the binding ids for arrows. Omit every field the editor can default (version, versionNonce, seed, updated, groupIds, boundElements when empty, roundness, opacity at 100, frameId when null). The editor fills them on open; shipping them can triple the payload for no gain. For a big diagram, write it in passes: create the scene, then frame(action="write_excalidraw") again with the next batch of elements.'),
2665
2682
  state: z.any().optional().describe('[set_state] App-frame state object (JSON) to persist for a deployed windowType:"app" frame — e.g. {specText:"..."} for the AS/NZS electrical app. The canvas hydrates the app from this on load (the host posts a "hydrate" message with it when the frame mounts), so you can deploy a generic app frame once and drive it with data afterwards. Max 64KB. Frame must be an app frame.'),
2666
2683
  file_path: z.string().optional().describe('[write] absolute path to a local file to upload. Mutually exclusive with content/base64/googleType.'),
2667
2684
  base64: z.string().optional().describe('[write] base64-encoded binary content. Mutually exclusive with content/file_path/googleType. Use with content_type when known.'),
@@ -3168,8 +3185,9 @@ tool('ls', 'List contents of the ACTIVE PROJECT. Use ls / after project(action="
3168
3185
  recursive: z.boolean().optional().describe('List contents of subdirectories. When true, forces summary mode (metadata only, no full content) to keep results under the 25k token cap.'),
3169
3186
  summary: z.boolean().optional().describe('Include size, updatedAt, title for frames'),
3170
3187
  pattern: z.string().optional().describe('Glob pattern to filter filenames (e.g. "*.html")'),
3188
+ head: z.number().optional().describe('Include the first N lines of EACH frame inline (max 50). Survey a whole lane in one call instead of one frame(action="read") per frame — read only the frames the heads show you actually need.'),
3171
3189
  limit: z.number().optional().default(500).describe('Max entries to return (default 500, max 2000). Use pattern or path to scope further.'),
3172
- }, async ({ path, recursive, summary, pattern, limit }) => {
3190
+ }, async ({ path, recursive, summary, pattern, head, limit }) => {
3173
3191
  try {
3174
3192
  const params = new URLSearchParams();
3175
3193
  params.set('path', path);
@@ -3184,6 +3202,7 @@ tool('ls', 'List contents of the ACTIVE PROJECT. Use ls / after project(action="
3184
3202
  } else if (summary) {
3185
3203
  params.set('summary', 'true');
3186
3204
  }
3205
+ if (head) params.set('head', String(head));
3187
3206
  if (pattern) params.set('pattern', pattern);
3188
3207
  const result = await api('GET', `/api/fs/?${params.toString()}`);
3189
3208
 
@@ -3548,7 +3567,7 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
3548
3567
  path: z.string().optional().describe('[read_file|update_file] relative path inside skill directory (e.g. "examples/react.md")'),
3549
3568
  offset: z.number().optional().describe('[search|list|export] 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'),
3550
3569
  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.'),
3551
- 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|export|import] resolve/fork into this org. Per-request only — nothing is switched.'),
3570
+ 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|export|import] resolve/fork into this org. Per-request only — nothing is switched. NO PROJECT IS NEEDED: for project-less skill work either pass org= here, or bind the session once with get_org(action="use", org="<name>"). A multi-org caller that addresses neither is refused rather than guessed.'),
3552
3571
  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"]'),
3553
3572
  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. [import] OKF bundle files inline — skills/<slug>/SKILL.md dirs and Skill/Playbook/SOP/Procedure-typed .md with name + description frontmatter become org skills. Caps: 500 files, 512KB/file, 5MB total.'),
3554
3573
  dir: z.string().optional().describe('[push|export|import] local directory. push: source tree to push instead of files[]; walked locally (heavy dirs, .skillinstall/, and .skillignore pre-filtered), server re-enforces; the dir\'s .gitignore is auto-updated to exclude .skillinstall/ (the rebuildable bundle). export: write the OKF bundle files here (default ./okf-skills-<org>). import: read the bundle from here (alternative to files[]).'),
@@ -3846,7 +3865,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3846
3865
  action: z.enum(['ls', 'recent', 'read', 'search', 'links', 'log', 'health', 'write', 'edit', 'mv', 'rm', 'cite', 'source-register', 'source-list', 'source-get', 'bulk-write', 'export', 'import']).describe('Operation to perform. export: the whole wiki as an OKF v0.1 bundle (local dir on stdio, download URL on remote, or format="files" for paged inline files). import: ingest an OKF bundle (files[] or local dir; dryRun supported).'),
3847
3866
  path: z.string().optional().describe('[ls|read|links|cite] wiki path. For ls: default / (root). For read: required. For links/cite: required unless pageId given. Reading `index.md` (any level) returns the SYNTHESIZED OKF directory listing.'),
3848
3867
  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.'),
3849
- 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).'),
3868
+ org: z.string().optional().describe('Org slug or id to scope this call to (per-request only — nothing is switched). NO PROJECT IS NEEDED for wiki work: to write project-less, either pass org= here, or bind the session once with get_org(action="use", org="<name>") and then omit it. Multi-org callers MUST address the org one of those two ways — an unaddressed write is refused rather than guessed (it would land in whichever org the session inherited). [write] the org the page is created in. [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).'),
3850
3869
  recursive: z.boolean().optional().describe('[ls] list recursively with depth indicators'),
3851
3870
  limit: z.number().optional().describe('[recent|search|export] max results (recent default 10, search default 25, export files default 100)'),
3852
3871
  offset: z.number().optional().describe('[export] pagination offset for format="files"'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.14.12",
3
+ "version": "1.14.14",
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": [