drafted 1.24.2 → 1.24.3

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 +75 -0
  2. package/package.json +1 -1
package/mcp/server.mjs CHANGED
@@ -485,6 +485,7 @@ const TOOL_ANNOTATIONS = {
485
485
 
486
486
  // Canvas / view
487
487
  mail: { title: 'Connected email threads', readOnlyHint: false, destructiveHint: false, openWorldHint: false, description: 'Connect a Gmail thread to a project so a reply to it is noticed. Drafted never reads mail: the user\'s own computer checks connected threads with their read-only gws sign-in and tells Drafted only that a new message arrived. Dispatch by `action`: connect (project, gmailThreadId, title, onReply), list (project), set (id, onReply/title), handled (id: clear the New reply mark after you dealt with it), disconnect (id). onReply: notify = mark it and tell the user; work = also start the user\'s agent on it. Read the thread itself with gws, and send any reply with action(propose, type="send_email").' },
488
+ frame_db: { title: 'Frame documents', readOnlyHint: false, destructiveHint: false, openWorldHint: false, description: 'Read and write an app frame\'s documents — the same store the frame\'s own JavaScript uses through `drafted.db`. Dispatch by `action`: list, get, set, merge, delete. Documents are addressed by (frame, collection, id); a write can carry `expectedUpdatedAt` so a stale write is refused instead of overwriting. Authorization is the frame\'s own: access to the frame, edit access for writes, and the `db` capability its author granted.' },
488
489
  focus: { title: 'Focus on target', readOnlyHint: false, destructiveHint: false, openWorldHint: false, description: 'Pan the canvas viewport for connected clients to a frame, lane, or layer.' },
489
490
  tour: { title: 'Guided tours', readOnlyHint: false, destructiveHint: false, openWorldHint: false, description: 'Play or stop an agent-authored guided tour (driver.js) on the project surface for all connected clients. Tours live on a frame\'s metadata.tour — author them with fs(write, metadata={tour: {title, steps}}). Dispatch by `action`: play (frameId) starts the walkthrough, stop (projectId) ends it. Use for presentations and collaborative walkthroughs.' },
490
491
  screenshot: { title: 'Screenshot', readOnlyHint: true, destructiveHint: false, openWorldHint: false, description: 'Render a PNG via headless browser. `scope=frame` for a single frame, `scope=canvas` for a region of the project surface.' },
@@ -3083,6 +3084,80 @@ tool('mail', {
3083
3084
  } catch (error) { return err(error); }
3084
3085
  });
3085
3086
 
3087
+ tool('frame_db', `Read and write an app frame's documents — the same store the frame's own JavaScript uses through \`drafted.db\`.
3088
+
3089
+ An app frame has a document store now (collection → document, one document per row of whatever the app shows). This tool is the agent's side of it: seeding a list, reading what the team has recorded, correcting a row. Before it existed the store was reachable only from the frame's own code or a raw authenticated HTTP call.
3090
+
3091
+ Authorization is the frame's own: the caller must have access to the frame, writes need edit access, and the frame's author must have granted the \`db\` capability. A read-only share can READ (the data is what they were invited to look at) and is refused on writes.
3092
+
3093
+ Actions:
3094
+ list — documents in one collection (limit/offset, ordered by updatedAt or id)
3095
+ get — one document
3096
+ set — replace one document's data (creates it)
3097
+ merge — shallow-merge fields into one document
3098
+ delete — remove one document
3099
+
3100
+ \`expectedUpdatedAt\` is optimistic concurrency: pass the document's updatedAt as you last read it and a stale write is refused with code \`conflict\` plus the current document, instead of silently overwriting somebody's change. Omit it for a blind write.`, {
3101
+ action: z.enum(['list', 'get', 'set', 'merge', 'delete']).describe('the operation'),
3102
+ target: z.string().describe('the frame: its URL (…/o/<org>/projects/<project>/<layer>/<lane>/<file>), a /projects path, or a frame UUID'),
3103
+ collection: z.string().describe('the collection name, e.g. "tracking" — letters, digits and _ . : - only'),
3104
+ id: z.string().optional().describe('document id, required for get/set/merge/delete'),
3105
+ data: z.any().optional().describe('[set/merge] the document body: plain JSON, at most 64KB. merge is a shallow merge, so nested objects are replaced, not merged'),
3106
+ expectedUpdatedAt: z.string().optional().describe("the document updatedAt as you last read it; a stale write is refused with code \"conflict\" rather than overwriting"),
3107
+ limit: z.number().int().min(1).max(500).optional().describe('[list] page size (default 100, max 500)'),
3108
+ offset: z.number().int().min(0).optional().describe('[list] page offset — the cursor for walking a large collection'),
3109
+ orderBy: z.enum(['updatedAt', 'id']).optional().describe('[list] ordering key (default updatedAt)'),
3110
+ dir: z.enum(['asc', 'desc']).optional().describe('[list] direction (default desc)'),
3111
+ }, async (args) => {
3112
+ const { action, target, collection, id, data, expectedUpdatedAt, limit, offset, orderBy, dir } = args;
3113
+ if (!target) return err(new Error('frame_db needs a target: the frame URL, /projects path, or frame id'));
3114
+ if (!collection) return err(new Error('frame_db needs a collection'));
3115
+
3116
+ // The org is in the target when the target is a path; a bare UUID has none and
3117
+ // resolves in the session's org. Same rule as fs: the address carries the scope.
3118
+ const orgInTarget = (String(target).match(/\/o\/([^/]+)\//) || [])[1] || null;
3119
+ const orgHeader = orgInTarget ? { 'X-Drafted-Org': decodeURIComponent(orgInTarget) } : {};
3120
+
3121
+ let frameId;
3122
+ try {
3123
+ ({ frameId } = await resolveFsFramePath(target));
3124
+ } catch (e) {
3125
+ return err(new Error(`${e.message}${orgInTarget ? '' : ' — include the /o/<org>/… URL when the frame is not in the workspace you are working in'}`));
3126
+ }
3127
+
3128
+ const base = `/api/frame-runtime/${frameId}/db/${encodeURIComponent(collection)}`;
3129
+ const q = [];
3130
+ if (limit != null) q.push('limit=' + limit);
3131
+ if (offset != null) q.push('offset=' + offset);
3132
+ if (orderBy) q.push('orderBy=' + encodeURIComponent(orderBy));
3133
+ if (dir) q.push('dir=' + encodeURIComponent(dir));
3134
+ const qs = q.length ? '?' + q.join('&') : '';
3135
+
3136
+ try {
3137
+ if (action === 'list') {
3138
+ const res = await api('GET', base + qs, undefined, orgHeader);
3139
+ return ok(res);
3140
+ }
3141
+ if (!id) return err(new Error(`frame_db ${action} needs a document id`));
3142
+ const docPath = `${base}/${encodeURIComponent(id)}`;
3143
+ if (action === 'get') return ok(await api('GET', docPath, undefined, orgHeader));
3144
+ if (action === 'delete') return ok(await api('DELETE', docPath, undefined, orgHeader));
3145
+ if (data === undefined) return err(new Error(`frame_db ${action} needs data`));
3146
+ const res = await api(action === 'merge' ? 'PATCH' : 'PUT', docPath, { data, expectedUpdatedAt }, orgHeader);
3147
+ return ok(res);
3148
+ } catch (e) {
3149
+ // A conflict carries the current document; a locked frame names the holder.
3150
+ // Flattening either into "HTTP 409" would hide the thing the caller acts on.
3151
+ if (e && (e.code === 'conflict' || e.code === 'locked' || e.code === 'quota_exceeded' || e.code === 'not_granted')) {
3152
+ // api() attaches the parsed body: the conflict carries the CURRENT document
3153
+ // and the lock carries its holder. Both are what the caller acts on.
3154
+ const detail = e.body ? JSON.stringify(e.body).slice(0, 800) : '';
3155
+ return err(new Error(`${e.code}: ${e.message}${detail ? ' — ' + detail : ''}`));
3156
+ }
3157
+ throw e;
3158
+ }
3159
+ });
3160
+
3086
3161
  tool('focus', {
3087
3162
  target: z.string().describe('What to pan the canvas to: a frame path (/projects/<project>/<layer>/<lane>/<file>), a frame URL (any URL containing /f/{uuid} or /o/<org>/projects/...), or a frame ID (UUID). When a user shares a Drafted link, pass it directly here.'),
3088
3163
  }, async ({ target }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.24.2",
3
+ "version": "1.24.3",
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
  "bin": {