drafted 1.12.0 → 1.12.2

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 +72 -57
  2. package/package.json +1 -1
package/mcp/server.mjs CHANGED
@@ -197,6 +197,10 @@ CONTEXT RULES (follow these before every action):
197
197
  - WIKI CHECK: Before acting on any request, search the org wiki for relevant conventions, existing designs, and prior decisions. Use wiki(action="search") with relevant keywords.
198
198
  - LAYER CONTEXT: Before reading or mutating a frame, read all anchored frames in the same layer. Anchored frames are per-layer required reading (style guides, design systems, conventions). The server enforces this mechanically for writes/edits/deletes/moves — but proactively reading anchored frames before any frame operation prevents wasted work.
199
199
  IMPORTANT: Any URL containing /f/{uuid} is a Drafted frame link — ALWAYS use read(path=URL) to get frame content, focus(target=URL) to pan the canvas to it. Never curl or WebFetch Drafted URLs.`,
200
+ }, {
201
+ // Initialize instructions: the agent-identity contract, so an agent learns its own
202
+ // tab name + the right way to read it WITHOUT having to "think to" call a tool.
203
+ instructions: `SESSION IDENTITY — read this first: you run as a NAMED session tab visible to the user on their Drafted surface. Your session has a human-readable name (a Greek term, e.g. "Nous") — that name is how the user matches YOUR window to the tab they see, so identify yourself by it when it matters which agent you are. Read it from get_org (response field "session.name") or from the whoami tool; call whoami to refresh after a reconnect.`,
200
204
  });
201
205
 
202
206
  const layerKeys = Object.keys(LAYERS);
@@ -216,7 +220,7 @@ const TOOL_ANNOTATIONS = {
216
220
  auth: { title: 'Sign in', readOnlyHint: false, destructiveHint: false, openWorldHint: true, description: 'Sign in to Drafted. `action=get_link` returns a URL immediately and starts background approval polling; after the user opens the link, later Drafted tool calls also auto-consume the approved login. `action=login` opens a browser when needed and explicitly waits/polls for approval.' },
217
221
 
218
222
  // Identity — read-only introspection of THIS agent's session
219
- whoami: { title: 'Session identity', readOnlyHint: true, destructiveHint: false, openWorldHint: false, description: 'Return THIS agent session\'s identity: its server-assigned human-readable name + emoji (the correlation key between an agent window and its web-app session tab), sessionId, userId, orgId, active projectId, editor label, server URL, and surfaced/alive state. Read-only. Use this — not guesses from the host environment — to report which session you are.' },
223
+ whoami: { title: 'Session identity', readOnlyHint: true, destructiveHint: false, openWorldHint: false, description: 'Return THIS agent session\'s identity: its server-assigned human-readable name (the correlation key between an agent window and its web-app session tab), sessionId, userId, orgId, active projectId, editor label, server URL, and surfaced/alive state. Read-only. Use this — not guesses from the host environment — to report which session you are.' },
220
224
 
221
225
  // Projects
222
226
  project: { title: 'Projects', readOnlyHint: false, destructiveHint: false, openWorldHint: false, widgetUri: 'ui://widget/drafted-canvas-overview.html', description: 'Manage projects: list (start here), open (switch active project), create, update, move to another org.' },
@@ -1067,7 +1071,7 @@ function withProjectBreadcrumb(result) {
1067
1071
 
1068
1072
  let agentWs = null;
1069
1073
  let agentWsReconnectTimer = null;
1070
- // Cached agent-hello-ack: this agent's own surface identity (name/emoji/alive) as
1074
+ // Cached agent-hello-ack: this agent's own surface identity (name/capital/color/alive) as
1071
1075
  // assigned by the server. The playful name is the correlation key between an agent
1072
1076
  // window and its web-app session tab; whoami reads it from here.
1073
1077
  let agentSurface = null;
@@ -1130,7 +1134,7 @@ async function connectAgentWs() {
1130
1134
  if (m.type === 'agent-hello-ack') {
1131
1135
  agentSurface = {
1132
1136
  sessionId: m.sessionId, userId: m.userId, orgId: m.orgId, projectId: m.projectId,
1133
- name: m.name, emoji: m.emoji, alive: m.alive, surfaced: true,
1137
+ name: m.name, capital: m.capital, color: m.color, alive: m.alive, surfaced: true,
1134
1138
  capturedAt: Date.now(),
1135
1139
  };
1136
1140
  }
@@ -1213,9 +1217,11 @@ async function getCurrentOrgId() {
1213
1217
  return ctx?.id || null;
1214
1218
  }
1215
1219
 
1216
- // Returns { id, name } for the org bound to THIS MCP session. Each session is
1217
- // independent on purpose parallel agents can run on different orgs. Cache
1218
- // is per-session, not module-global, so concurrent OAuth users can't collide.
1220
+ // Returns { id, name } for the org this MCP process is currently scoped to (the
1221
+ // session row's org_id, which the server resolves per-request via X-Drafted-Org).
1222
+ // Each MCP process is independent parallel agents can run scoped to different
1223
+ // orgs. Cache is per-session-bucket, not module-global, so concurrent OAuth
1224
+ // users can't collide.
1219
1225
  async function getCurrentOrgContext() {
1220
1226
  const sess = getSessionState();
1221
1227
  if (sess.cachedOrgId && Date.now() - sess.cachedOrgIdTime < 30000) return sess.cachedOrgId;
@@ -1511,49 +1517,56 @@ if (!isRemote) tool('auth', 'Sign in to Drafted. `action=get_link` returns a ver
1511
1517
  } catch (error) { return err(error); }
1512
1518
  });
1513
1519
 
1514
- // Identity: report THIS agent session's own surface identity. The name/emoji come from
1515
- // the agent-hello-ack cached on the WS; falls back to /auth/me for userId/org if the WS
1516
- // ack hasn't landed yet. Read-only no state changed.
1517
- tool('whoami', 'Return THIS agent session\'s identity: its server-assigned human-readable name + emoji (the correlation key between an agent window and its web-app session tab), sessionId, userId, orgId, active projectId, editor label, server URL, and surfaced/alive state. Read-only.', {}, async () => {
1518
- try {
1519
- const server = getServerUrl();
1520
- const editor = (process.env.DRAFTED_AGENT_NAME || '').trim() || null;
1521
- const base = {
1522
- server,
1523
- editor,
1524
- agentLabel: getAgentLabel(),
1525
- sessionId: agentSurface?.sessionId || getState().sessionId || null,
1520
+ // THIS-session surface identity: the session name + capital/color tile (correlation key between an
1521
+ // agent window and its web-app session tab), ids, and surfaced/alive state. Built from
1522
+ // the cached agent-hello-ack; falls back to /auth/me for userId/org before the first ack.
1523
+ // Shared by `whoami` AND surfaced in `get_org`'s response so an agent learns its name
1524
+ // from the tool it already calls — it must not have to "think to" ask.
1525
+ async function sessionSurfaceBlock() {
1526
+ const sessionId = agentSurface?.sessionId || getState().sessionId || null;
1527
+ if (agentSurface) {
1528
+ return {
1529
+ sessionId,
1530
+ userId: agentSurface.userId ?? null,
1531
+ orgId: agentSurface.orgId ?? null,
1532
+ projectId: agentSurface.projectId ?? null,
1533
+ name: agentSurface.name,
1534
+ capital: agentSurface.capital,
1535
+ color: agentSurface.color,
1536
+ surfaced: true,
1537
+ alive: !!agentSurface.alive,
1526
1538
  };
1527
- if (agentSurface) {
1528
- return ok({
1529
- ...base,
1530
- userId: agentSurface.userId ?? null,
1531
- orgId: agentSurface.orgId ?? null,
1532
- projectId: agentSurface.projectId ?? null,
1533
- name: agentSurface.name,
1534
- emoji: agentSurface.emoji,
1535
- surfaced: true,
1536
- alive: !!agentSurface.alive,
1537
- });
1538
- }
1539
- // No WS ack yet — best-effort identity from /auth/me so callers still get a userId/org.
1540
- const sid = base.sessionId || getBootstrapSessionId();
1541
- let me = null;
1542
- if (sid) {
1543
- try {
1544
- const res = await fetch(`${server}/auth/me`, { headers: { Cookie: `gc_session=${sid}` } });
1545
- if (res.ok) me = await res.json();
1546
- } catch { /* not yet authenticated */ }
1547
- }
1539
+ }
1540
+ // No WS ack yet — best-effort identity from /auth/me so callers still get a userId/org.
1541
+ const cookieSid = sessionId || getBootstrapSessionId();
1542
+ let me = null;
1543
+ if (cookieSid) {
1544
+ try {
1545
+ const res = await fetch(`${getServerUrl()}/auth/me`, { headers: { Cookie: `gc_session=${cookieSid}` } });
1546
+ if (res.ok) me = await res.json();
1547
+ } catch { /* not yet authenticated */ }
1548
+ }
1549
+ return {
1550
+ sessionId: cookieSid,
1551
+ userId: me?.userId ?? null,
1552
+ orgId: me?.currentOrg?.id ?? null,
1553
+ projectId: getState().projectId ?? null,
1554
+ name: null,
1555
+ capital: null,
1556
+ color: null,
1557
+ surfaced: false,
1558
+ alive: false,
1559
+ };
1560
+ }
1561
+
1562
+ // Identity: report THIS agent session's own surface identity. Read-only — no state changed.
1563
+ tool('whoami', 'Return THIS agent session\'s identity: its server-assigned human-readable name (the correlation key between an agent window and its web-app session tab), sessionId, userId, orgId, active projectId, editor label, server URL, and surfaced/alive state. Read-only.', {}, async () => {
1564
+ try {
1548
1565
  return ok({
1549
- ...base,
1550
- userId: me?.userId ?? null,
1551
- orgId: me?.currentOrg?.id ?? null,
1552
- projectId: getState().projectId ?? null,
1553
- name: null,
1554
- emoji: null,
1555
- surfaced: false,
1556
- alive: false,
1566
+ server: getServerUrl(),
1567
+ editor: (process.env.DRAFTED_AGENT_NAME || '').trim() || null,
1568
+ agentLabel: getAgentLabel(),
1569
+ ...(await sessionSurfaceBlock()),
1557
1570
  });
1558
1571
  } catch (error) { return err(error); }
1559
1572
  });
@@ -1566,7 +1579,7 @@ tool('project', 'START HERE for project management. Dispatch by `action`: list (
1566
1579
  name: z.string().optional().describe('[create|update] project name'),
1567
1580
  description: z.string().nullable().optional().describe('[create|update] project description'),
1568
1581
  templateSlug: z.string().optional().describe('[create] template slug (e.g. "web-design", "mobile-app", "landing-page")'),
1569
- 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.'),
1582
+ org: z.string().optional().describe('[create] org slug or id to create the project in, without switching the active org. Defaults to the active org.'),
1570
1583
  folder: z.string().nullable().optional().describe('[update] folder name (null to remove from folder)'),
1571
1584
  layers: z.array(z.object({}).passthrough()).optional().describe('[update] full layers array replacement. Use ls / to read current layers first.'),
1572
1585
  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.'),
@@ -1695,7 +1708,7 @@ tool('project', 'START HERE for project management. Dispatch by `action`: list (
1695
1708
  const body = { name };
1696
1709
  if (description) body.description = description;
1697
1710
  if (templateSlug) body.templateSlug = templateSlug;
1698
- // `org` targets a specific org without switching the session cursor.
1711
+ // `org` targets a specific org without switching the active org.
1699
1712
  const createExtra = org ? { 'X-Drafted-Org': org } : {};
1700
1713
  return ok(withProjectBreadcrumb(await api('POST', '/api/projects', body, createExtra)));
1701
1714
  }
@@ -2134,12 +2147,13 @@ tool('get_org', {
2134
2147
  const activeOrg = (orgs || []).map(o => ({ id: o.orgId || o.id, name: o.orgName || o.name })).find(o => o.id === me?.orgId) || null;
2135
2148
  const googleDrive = await getGoogleDriveAvailability();
2136
2149
  const mcpUpdate = await getMcpUpdateMetadata();
2137
- return ok({ switched: true, activeOrg, googleDrive, mcpVersion: PACKAGE_VERSION, mcpUpdate, note: 'Active org switched. Wiki and skill calls now target this org. Active project cleared — open a project (or stay org-scoped for wiki/skill). If googleDrive.connected is true, prefer Google Workspace frames for docs, sheets, and slides.' });
2150
+ const session = await sessionSurfaceBlock();
2151
+ return ok({ switched: true, activeOrg, googleDrive, mcpVersion: PACKAGE_VERSION, mcpUpdate, session, note: 'Active org switched. Wiki and skill calls now target this org. Active project cleared — open a project (or stay org-scoped for wiki/skill). If googleDrive.connected is true, prefer Google Workspace frames for docs, sheets, and slides.' });
2138
2152
  }
2139
2153
 
2140
- // Source of truth = THIS MCP session's bound org (what mutations will actually
2141
- // hit). Each session is independent on purpose — multiple agents can run in
2142
- // parallel against different orgs. /auth/me reads sessions.org_id directly.
2154
+ // Source of truth = the org this MCP process scopes requests to (what mutations
2155
+ // will actually hit). Each MCP process is independent — multiple agents can run
2156
+ // in parallel scoped to different orgs. /auth/me reads sessions.org_id directly.
2143
2157
  const me = await api('GET', '/auth/me');
2144
2158
  const sessionOrgId = me?.orgId || null;
2145
2159
 
@@ -2164,7 +2178,8 @@ tool('get_org', {
2164
2178
  googleDrive,
2165
2179
  mcpVersion: PACKAGE_VERSION,
2166
2180
  mcpUpdate,
2167
- note: "activeOrg is the org bound to THIS MCP session — what mutations will actually target. To switch orgs without creating a project, call get_org(action=\"switch\", orgId=\"...\"). Browser tabs and other MCP sessions for the same user can be on different orgs. If googleDrive.connected is true, strongly prefer Google Workspace frames for docs, sheets, and slides.",
2181
+ session: await sessionSurfaceBlock(),
2182
+ note: "activeOrg is the org this MCP process scopes its requests to (carried per-request as X-Drafted-Org); mutations target it. `session` is THIS agent's own surface identity — `session.name` is the human-readable tab name the user sees (use it to identify which agent you are); refresh it via whoami. To scope to a different org without opening a project, call get_org(action=\"switch\", orgId=\"...\"). Concurrent MCP sessions and browser tabs for the same user can be scoped to different orgs. If googleDrive.connected is true, strongly prefer Google Workspace frames for docs, sheets, and slides.",
2168
2183
  });
2169
2184
  } catch (error) { return err(error); }
2170
2185
  });
@@ -3098,7 +3113,7 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
3098
3113
  path: z.string().optional().describe('[read_file|update_file] relative path inside skill directory (e.g. "examples/react.md")'),
3099
3114
  offset: z.number().optional().describe('[search|list] 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'),
3100
3115
  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.'),
3101
- org: z.string().optional().describe('[fork|push|update] resolve/fork into this Drafted org (id or name); scopes the request without switching the session'),
3116
+ org: z.string().optional().describe('[fork|push|update] resolve/fork into this Drafted org (id or name); scopes the request without switching the active org'),
3102
3117
  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"]'),
3103
3118
  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'),
3104
3119
  dir: z.string().optional().describe('[push] local directory to push instead of files[]; walked locally (heavy dirs, .skillinstall/, and .skillignore pre-filtered), server re-enforces. On push the dir\'s .gitignore is auto-updated to exclude .skillinstall/ (the rebuildable bundle).'),
@@ -3320,7 +3335,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3320
3335
  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.'),
3321
3336
  path: z.string().optional().describe('[ls|read|links] wiki path. For ls: default / (root). For read: required. For links: required.'),
3322
3337
  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.'),
3323
- 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.'),
3338
+ org: z.string().optional().describe('[write] org slug or id to create/target the page in, without switching the active org. [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 active.'),
3324
3339
  recursive: z.boolean().optional().describe('[ls] list recursively with depth indicators'),
3325
3340
  limit: z.number().optional().describe('[recent|search] max results (recent default 10, search default 25)'),
3326
3341
  query: z.string().optional().describe('[search] term to search in title, path, and content'),
@@ -3576,7 +3591,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3576
3591
  if (frontmatter !== undefined) body.frontmatter = frontmatter;
3577
3592
 
3578
3593
  // Check if page exists — if so, update; otherwise create. `orgHeader`
3579
- // (the `org` arg) targets a specific org without switching the session.
3594
+ // (the `org` arg) targets a specific org without switching the active org.
3580
3595
  try {
3581
3596
  const existing = await api('GET', `/api/wiki/page?path=${encodeURIComponent(normalized)}`, undefined, orgHeader);
3582
3597
  const result = await api('PATCH', `/api/wiki/pages/${existing.id}`, body, orgHeader);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.12.0",
3
+ "version": "1.12.2",
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": [