drafted 1.14.9 → 1.14.11
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 +114 -22
- package/package.json +1 -1
package/mcp/server.mjs
CHANGED
|
@@ -404,6 +404,14 @@ function tool(name, descOrSchema, schemaOrHandler, handler) {
|
|
|
404
404
|
}
|
|
405
405
|
}
|
|
406
406
|
}
|
|
407
|
+
// Per-call project override (see withProjectOverride). Scoping happens here so
|
|
408
|
+
// every project-scoped tool gets it from one seam instead of each handler.
|
|
409
|
+
const ref = PROJECT_SCOPED_TOOLS.has(name) ? args?.[0]?.projectId : null;
|
|
410
|
+
if (ref) {
|
|
411
|
+
const meta = await resolveProjectRef(ref);
|
|
412
|
+
if (!meta) return err(new Error(`Project "${ref}" not found — pass a project UUID, slug, or name from project(action="list").`));
|
|
413
|
+
return await withProjectOverride(meta, () => cb(...args));
|
|
414
|
+
}
|
|
407
415
|
return await cb(...args);
|
|
408
416
|
} finally {
|
|
409
417
|
state.currentTool = previousTool;
|
|
@@ -926,6 +934,11 @@ async function api(method, path, body, extraHeaders = {}, _retried = false) {
|
|
|
926
934
|
const scopedPath = pid ? `${path}${sep}projectId=${pid}` : path;
|
|
927
935
|
const url = `${getServerUrl()}${scopedPath}`;
|
|
928
936
|
const headers = { ...getAuthHeaders(), ...extraHeaders };
|
|
937
|
+
// Declare an unbound session explicitly. Without this the server falls back to the
|
|
938
|
+
// user's SHARED active-project row (any browser tab or parallel agent can rewrite it)
|
|
939
|
+
// and a frame write silently succeeds in the WRONG project. Project-scoped routes
|
|
940
|
+
// fail closed on this header; org-scoped ones (wiki, skill, get_org) ignore it.
|
|
941
|
+
if (!pid) headers['X-Drafted-Project'] = 'none';
|
|
929
942
|
// Bind every data request to the org this MCP session is actually working in
|
|
930
943
|
// (the active project's org, or the org chosen via get_org switch). Without
|
|
931
944
|
// this, project-scoped routes that resolve org from the SHARED server-side
|
|
@@ -988,7 +1001,10 @@ async function api(method, path, body, extraHeaders = {}, _retried = false) {
|
|
|
988
1001
|
// a get_org switch), and now the project is invisible. Clear our
|
|
989
1002
|
// sticky reference so the next tool call doesn't append a stale
|
|
990
1003
|
// projectId to its URL — and tell the agent what happened.
|
|
991
|
-
|
|
1004
|
+
// Only a PROJECT-not-found clears the binding. A bare 404 also means "frame not
|
|
1005
|
+
// found" / "page not found" — clearing on those unbound the session mid-turn, and
|
|
1006
|
+
// the next write fell through to the shared active-project row in another project.
|
|
1007
|
+
if (/project not found/i.test(msg) && pid && getState().projectId === pid) {
|
|
992
1008
|
const meta = getState().projectMeta;
|
|
993
1009
|
getState().projectId = null;
|
|
994
1010
|
getState().projectMeta = null;
|
|
@@ -1078,6 +1094,15 @@ function shapeSkillCatalog(result, { limit, offset = 0, compact = false } = {})
|
|
|
1078
1094
|
return result;
|
|
1079
1095
|
}
|
|
1080
1096
|
|
|
1097
|
+
// Semantic frame URL (/o/<org>/<project>/<layer>/<lane>/<label>) from a row that
|
|
1098
|
+
// carries slugs (e.g. /api/search hits). Returns null when a slug is missing so the
|
|
1099
|
+
// caller can fall back to the /f/<uuid> resolver.
|
|
1100
|
+
function semanticFrameUrl({ orgSlug, projectSlug, layer, lane, label }) {
|
|
1101
|
+
if (!orgSlug || !projectSlug || !label) return null;
|
|
1102
|
+
const e = encodeURIComponent;
|
|
1103
|
+
return `${getServerUrl()}/o/${e(orgSlug)}/${e(projectSlug)}/${e(layer || 'designs')}/${e(lane || 'default')}/${e(label)}`;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1081
1106
|
// Build the structuredContent shape that the frame-preview widget reads.
|
|
1082
1107
|
// Tools that produce or return a frame (read/write/edit) call this so the
|
|
1083
1108
|
// model and widget see the same metadata view.
|
|
@@ -1093,7 +1118,9 @@ function frameStructuredContent(result, project = null) {
|
|
|
1093
1118
|
lane: result.lane,
|
|
1094
1119
|
width: result.width,
|
|
1095
1120
|
height: result.height,
|
|
1096
|
-
|
|
1121
|
+
// Server emits the semantic /o/<org>/<project>/<layer>/... URL; /f/<uuid> is the
|
|
1122
|
+
// fallback (still resolved + redirected server-side).
|
|
1123
|
+
frameUrl: result.frameUrl || (result.id ? `${getServerUrl()}/f/${result.id}` : undefined),
|
|
1097
1124
|
project, // {id, slug, name, orgId} of the active project — visible in client UI
|
|
1098
1125
|
};
|
|
1099
1126
|
}
|
|
@@ -1150,6 +1177,55 @@ function setMcpActiveProject(projectId, meta = null) {
|
|
|
1150
1177
|
}
|
|
1151
1178
|
}
|
|
1152
1179
|
|
|
1180
|
+
// Resolve a project reference — UUID, slug, or name (case-insensitive) — to its meta.
|
|
1181
|
+
// Agents carry slugs in context ("ios-app-shell-re-ui"), not UUIDs; demanding a UUID
|
|
1182
|
+
// forced a frame-search dance just to name a project. Backs both project(action="open")
|
|
1183
|
+
// and the per-call `projectId` override below.
|
|
1184
|
+
// ponytail: process-lifetime cache, no TTL — a project rename goes stale until restart.
|
|
1185
|
+
// Add invalidation if renames ever become common.
|
|
1186
|
+
const projectRefCache = new Map();
|
|
1187
|
+
async function resolveProjectRef(ref) {
|
|
1188
|
+
if (!ref) return null;
|
|
1189
|
+
const hit = projectRefCache.get(ref);
|
|
1190
|
+
if (hit) return hit;
|
|
1191
|
+
const data = await api('GET', '/api/projects');
|
|
1192
|
+
const list = Array.isArray(data?.projects) ? data.projects : [];
|
|
1193
|
+
const want = String(ref).toLowerCase();
|
|
1194
|
+
const p =
|
|
1195
|
+
list.find(x => x.id === ref) ||
|
|
1196
|
+
list.find(x => String(x.slug || '').toLowerCase() === want) ||
|
|
1197
|
+
list.find(x => String(x.name || '').toLowerCase() === want);
|
|
1198
|
+
if (!p) return null;
|
|
1199
|
+
const meta = { id: p.id, slug: p.slug || null, name: p.name || null, orgId: p.orgId || null, orgSlug: p.orgSlug || null };
|
|
1200
|
+
projectRefCache.set(ref, meta);
|
|
1201
|
+
projectRefCache.set(p.id, meta);
|
|
1202
|
+
return meta;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Run one tool call against a caller-named project WITHOUT rebinding the session.
|
|
1206
|
+
// The override lives in this call's AsyncLocalStorage frame only — request-local, so
|
|
1207
|
+
// it satisfies the DRAFT-36 addressing invariant (no shared, another-session-writable
|
|
1208
|
+
// resolution state) while giving a reconnected agent a way to address the project it
|
|
1209
|
+
// already knows, and to reach into another project for a one-off (e.g. deleting a
|
|
1210
|
+
// stray frame) without the open → act → re-open dance.
|
|
1211
|
+
function withProjectOverride(meta, fn) {
|
|
1212
|
+
const base = getState();
|
|
1213
|
+
return requestState.run(
|
|
1214
|
+
{ ...base, projectId: meta.id, projectMeta: meta, _session: base._session || getSessionState() },
|
|
1215
|
+
fn,
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Tools that address frames/assets/layout inside ONE project. These accept the
|
|
1220
|
+
// per-call `projectId` override; everything else resolves org-side.
|
|
1221
|
+
const PROJECT_SCOPED_TOOLS = new Set(['frame', 'ls', 'rm', 'asset', 'connector', 'layout']);
|
|
1222
|
+
|
|
1223
|
+
const PROJECT_OVERRIDE_PARAM = z.string().optional().describe(
|
|
1224
|
+
'Target project (UUID, slug, or name) for THIS call only — does NOT rebind the session. ' +
|
|
1225
|
+
'Use it when the session lost its binding (a reconnect), or to touch another project once ' +
|
|
1226
|
+
'without re-opening it. Omit to use the project bound by project(action="open").',
|
|
1227
|
+
);
|
|
1228
|
+
|
|
1153
1229
|
// Returns { id, slug, name, orgId } for the project this MCP session most
|
|
1154
1230
|
// recently opened — what frame mutations will actually target. Echoed on
|
|
1155
1231
|
// every mutation so silent cross-project drift is visible.
|
|
@@ -1874,23 +1950,22 @@ tool('project', 'START HERE for project management. Dispatch by `action`: list (
|
|
|
1874
1950
|
return ok(data, { structuredContent });
|
|
1875
1951
|
}
|
|
1876
1952
|
case 'open': {
|
|
1877
|
-
const
|
|
1878
|
-
if (!
|
|
1953
|
+
const ref = args.projectId;
|
|
1954
|
+
if (!ref) throw new Error('projectId required for action=open (UUID, slug, or name)');
|
|
1955
|
+
// Accept a slug or name, not just a UUID — the slug is what an agent actually has
|
|
1956
|
+
// in context after a reconnect.
|
|
1957
|
+
const resolved = await resolveProjectRef(ref).catch(() => null);
|
|
1958
|
+
const projectId = resolved?.id || ref;
|
|
1879
1959
|
const result = await api('POST', '/api/project/switch', { projectId });
|
|
1880
1960
|
joinAgentWsRoom(projectId);
|
|
1881
1961
|
const base = getServerUrl();
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
const data = await api('GET', '/api/projects');
|
|
1886
|
-
const proj = (data.projects || []).find(p => p.id === projectId);
|
|
1887
|
-
if (proj) {
|
|
1888
|
-
projectSlug = proj.slug || projectId;
|
|
1889
|
-
projectMeta = { id: proj.id, slug: proj.slug || null, name: proj.name || null, orgId: proj.orgId || null };
|
|
1890
|
-
}
|
|
1891
|
-
} catch { /* fall back to projectId */ }
|
|
1962
|
+
const projectMeta = resolved || { id: projectId, slug: null, name: null, orgId: null, orgSlug: null };
|
|
1963
|
+
const projectSlug = projectMeta.slug || projectId;
|
|
1964
|
+
const orgSlug = projectMeta.orgSlug || null;
|
|
1892
1965
|
setMcpActiveProject(projectId, projectMeta);
|
|
1893
|
-
|
|
1966
|
+
// Semantic /o/<org-slug>/<project-slug> when the org slug is known; otherwise
|
|
1967
|
+
// the /project/<slug> resolver redirects to it.
|
|
1968
|
+
const url = orgSlug ? `${base}/o/${encodeURIComponent(orgSlug)}/${encodeURIComponent(projectSlug)}` : `${base}/project/${projectSlug}`;
|
|
1894
1969
|
// Surfacing to the user is the focus mechanism's job now (agent-active ping ->
|
|
1895
1970
|
// desktop window / notification+glow for browser tabs) — this used to also force
|
|
1896
1971
|
// `exec('open <url>')` on the MCP host machine, an unsolicited GUI action that both
|
|
@@ -2549,6 +2624,7 @@ tool('get_org', {
|
|
|
2549
2624
|
// ── Filesystem tools (direct HTTP to /api/fs) ─────────────────────
|
|
2550
2625
|
|
|
2551
2626
|
tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by path, frame URL, or UUID), write (new frame or overwrite), set_state / get_state (persist or read a deployed windowType:"app" frame\'s hydration state — push e.g. {specText} to drive a generic app frame with data after deploy; the canvas hydrates the app from it on load), Google Sheet actions (`get_sheet`, `read_sheet_values`, `write_sheet_values`, `append_sheet_rows`, `clear_sheet_range`, `update_sheet`), Google Doc actions (`get_doc`, `read_doc_content`, `write_doc_content`, `append_doc_content`, `clear_doc_content`, `update_doc`), Google Slide actions (`get_slide`, `read_slide_content`, `write_slide_content`, `append_slides`, `clear_slides`, `update_slide`), write_excalidraw (native editable Excalidraw diagram), edit (hashline ops), mv (rename/move), anchor (mark as required-read for the layer), search (match frame names). Use project(action="open") first. For listing use `ls`, for deletion use `rm`.\n\n**Google Workspace native content:** Create or attach Google Docs/Sheets/Slides with `frame(action="write", googleType=...)`. After creating, immediately populate the native file using the matching write action in the same tool — do NOT leave it empty and do NOT tell the user you cannot write to it. For Sheets: `write_sheet_values` or `append_sheet_rows` (pass `path` or `googleId` from the create response). For Docs: `write_doc_content`/`append_doc_content`. For Slides: `write_slide_content`/`append_slides`. Read with `read_sheet_values`/`read_doc_content`/`read_slide_content`. Do NOT use inline `frame.write(content)` or hashline `frame.edit` to populate Google Workspace frames.\n\n**Write — content, binary, or Google Workspace frame:** ' + (isRemote ? 'Provide exactly one of `content` (HTML/markdown/text), `base64` (base64-encoded binary with optional `content_type`), or `googleType` (`google-doc`, `google-sheet`, `google-slide`).' : 'Provide exactly one of `content` (HTML/markdown/text), `file_path` (absolute local file), `base64` (base64-encoded binary with optional `content_type`), or `googleType` (`google-doc`, `google-sheet`, `google-slide`).') + ' Call get_org first; when `googleDrive.connected` is true, strongly prefer Google Workspace frames for docs, sheets, and slides in that org. For inline content, filename extension matters: use `.html` for complete HTML documents and `.md` for Markdown. Never place a full HTML document in a `.md` or extensionless frame. For a new Google file, pass `googleType` and optional `title`; for an existing Google file, pass `googleType` plus `url` or `googleId`. ' + (isRemote ? 'For binary frames (images, PDFs, videos), pass `base64` with the binary bytes.' : 'For binary frames (images, PDFs, videos), use `file_path` when the file is local to the MCP host, or `base64` when the caller already has binary bytes.') + '\n\n**Write — dimensions:** By default, frames use the layer\'s default size (e.g. 1440×900 for designs, 1440×3000 for wireframes). Often too large for small content. Use `autoSize: true` to measure HTML content and size to fit, or pass explicit `width`/`height`.', {
|
|
2627
|
+
projectId: PROJECT_OVERRIDE_PARAM,
|
|
2552
2628
|
action: z.enum(['read', 'write', 'set_state', 'get_state', 'write_sheet_values', 'read_sheet_values', 'append_sheet_rows', 'clear_sheet_range', 'get_sheet', 'update_sheet', 'get_doc', 'read_doc_content', 'write_doc_content', 'append_doc_content', 'clear_doc_content', 'update_doc', 'get_slide', 'read_slide_content', 'write_slide_content', 'append_slides', 'clear_slides', 'update_slide', 'create_office', 'read_office', 'edit_office', 'write_excalidraw', 'edit', 'mv', 'anchor', 'search', 'versions', 'read_version', 'restore_version']).describe('Operation to perform. Use native Doc/Slide actions for Google Docs/Slides; do not use inline write/edit for native Workspace content.'),
|
|
2553
2629
|
path: z.string().optional().describe('[read] /{layer}/{lane}/{filename}, frame URL, or UUID. [write|edit|anchor] /{layer}/{lane}/{filename}.'),
|
|
2554
2630
|
lines: z.string().optional().describe('[read] line range (e.g. "1-50"). Omit to read all.'),
|
|
@@ -2635,7 +2711,6 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
2635
2711
|
dryRun: z.boolean().optional().describe('[mv] preview the move without applying it; returns the resolved frame and current path so you can confirm before retrying with dryRun=false.'),
|
|
2636
2712
|
anchored: z.boolean().optional().describe('[anchor] true to anchor, false to unanchor. Anchored frames MUST be read before writing/editing in the same layer.'),
|
|
2637
2713
|
query: z.string().optional().describe('[search] term to match against frame names'),
|
|
2638
|
-
projectId: z.string().optional().describe('[search] limit to a specific project (optional)'),
|
|
2639
2714
|
limit: z.number().optional().describe('[search] max results (default 50, max 200)'),
|
|
2640
2715
|
versionId: z.string().optional().describe('[read_version|restore_version] version id'),
|
|
2641
2716
|
reason: z.string().optional().describe('[restore_version] reason recorded on the snapshot of current content'),
|
|
@@ -3025,7 +3100,9 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
3025
3100
|
const { query, projectId, limit = 50 } = args;
|
|
3026
3101
|
if (!query) throw new Error('query required for action=search');
|
|
3027
3102
|
const params = new URLSearchParams({ q: query });
|
|
3028
|
-
|
|
3103
|
+
// projectId may be a slug/name (the shared override param) — the tool() seam has
|
|
3104
|
+
// already resolved it into state, so take the resolved UUID from there.
|
|
3105
|
+
if (projectId) params.set('projectId', getState().projectId || projectId);
|
|
3029
3106
|
await ensureSession();
|
|
3030
3107
|
const url = `${getServerUrl()}/api/search?${params.toString()}`;
|
|
3031
3108
|
const res = await fetch(url, { headers: getAuthHeaders() });
|
|
@@ -3039,7 +3116,7 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
3039
3116
|
path: `/${r.layer}/${r.lane}/${r.label}`,
|
|
3040
3117
|
project: r.projectName,
|
|
3041
3118
|
projectId: r.projectId,
|
|
3042
|
-
frameUrl: `${getServerUrl()}/f/${r.id}`,
|
|
3119
|
+
frameUrl: semanticFrameUrl(r) || `${getServerUrl()}/f/${r.id}`,
|
|
3043
3120
|
contentType: r.contentType,
|
|
3044
3121
|
updatedAt: r.updatedAt,
|
|
3045
3122
|
})),
|
|
@@ -3054,6 +3131,7 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
3054
3131
|
});
|
|
3055
3132
|
|
|
3056
3133
|
tool('ls', 'List contents of the ACTIVE PROJECT. Use ls / after project(action="open") to see layers, workflow, and confirm you\'re in the right project.', {
|
|
3134
|
+
projectId: PROJECT_OVERRIDE_PARAM,
|
|
3057
3135
|
path: z.string().optional().default('/').describe('Directory path: / (layers), /{layer} (lanes), /{layer}/{lane} (frames). Frame entries include frameUrl (canvas deep link) and id (frame UUID).'),
|
|
3058
3136
|
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.'),
|
|
3059
3137
|
summary: z.boolean().optional().describe('Include size, updatedAt, title for frames'),
|
|
@@ -3112,7 +3190,11 @@ tool('ls', 'List contents of the ACTIVE PROJECT. Use ls / after project(action="
|
|
|
3112
3190
|
const projectId = getState().projectId;
|
|
3113
3191
|
const structuredContent = {
|
|
3114
3192
|
project: result?.project || result?.projectName,
|
|
3115
|
-
canvasUrl: result?.
|
|
3193
|
+
canvasUrl: result?.project?.slug
|
|
3194
|
+
? (result.project.orgSlug
|
|
3195
|
+
? `${getServerUrl()}/o/${encodeURIComponent(result.project.orgSlug)}/${encodeURIComponent(result.project.slug)}`
|
|
3196
|
+
: `${getServerUrl()}/project/${encodeURIComponent(result.project.slug)}`)
|
|
3197
|
+
: undefined,
|
|
3116
3198
|
byLayer,
|
|
3117
3199
|
truncated: result?.truncated || false,
|
|
3118
3200
|
totalAvailable: result?.totalAvailable,
|
|
@@ -3121,10 +3203,17 @@ tool('ls', 'List contents of the ACTIVE PROJECT. Use ls / after project(action="
|
|
|
3121
3203
|
} catch (error) { return err(error); }
|
|
3122
3204
|
});
|
|
3123
3205
|
|
|
3124
|
-
tool('rm', 'Delete a frame or lane
|
|
3125
|
-
|
|
3126
|
-
}
|
|
3206
|
+
tool('rm', 'Delete a frame or lane. Defaults to the ACTIVE PROJECT; pass projectId to delete in another project, or frameId to delete a frame by UUID from anywhere (no project binding needed — the frame\'s own org/project is authoritative). Response includes "project" so you see where the deletion landed.', {
|
|
3207
|
+
projectId: PROJECT_OVERRIDE_PARAM,
|
|
3208
|
+
path: z.string().optional().describe('Path to delete: /{layer}/{lane}/{filename} or /{layer}/{lane} (deletes entire lane). Mutually exclusive with frameId.'),
|
|
3209
|
+
frameId: z.string().optional().describe('Frame UUID to delete, resolved independently of the active project — the one-call cleanup for a frame that landed in the wrong place. Mutually exclusive with path.'),
|
|
3210
|
+
}, async ({ path, frameId }) => {
|
|
3127
3211
|
try {
|
|
3212
|
+
if (frameId && path) throw new Error('Pass either path or frameId, not both.');
|
|
3213
|
+
if (!frameId && !path) throw new Error('rm needs a path or a frameId.');
|
|
3214
|
+
// A frame UUID self-derives its org and project server-side, so this works from an
|
|
3215
|
+
// unbound session and across projects — no open/re-open dance to remove a stray frame.
|
|
3216
|
+
if (frameId) return ok(await api('DELETE', `/api/designs/${encodeURIComponent(frameId)}`));
|
|
3128
3217
|
const clean = path.replace(/^\/+|\/+$/g, '');
|
|
3129
3218
|
const result = await api('DELETE', `/api/fs/${clean}`);
|
|
3130
3219
|
return ok({ ...result, project: getCurrentProjectContext() });
|
|
@@ -3238,6 +3327,7 @@ tool('batch', 'Batch operations on the ACTIVE PROJECT. Response includes "projec
|
|
|
3238
3327
|
// ── Asset tools ──────────────────────────────────────────────────
|
|
3239
3328
|
|
|
3240
3329
|
tool('asset', 'Manage supporting files (CSS, JS, images, fonts) in the ACTIVE PROJECT. Assets are referenced by frames via relative paths — e.g., if your HTML has <link href="css/styles.css">, upload with asset_path="css/styles.css". Assets are NOT frames — they don\'t appear on the canvas. `action=upload` to add/replace, `action=list` to browse, `action=rm` to delete.', {
|
|
3330
|
+
projectId: PROJECT_OVERRIDE_PARAM,
|
|
3241
3331
|
action: z.enum(['upload', 'list', 'rm']).describe('Operation to perform.'),
|
|
3242
3332
|
asset_path: z.string().optional().describe('[upload] relative asset path (e.g. "css/styles.css"). Must match the path used in HTML references.'),
|
|
3243
3333
|
file_path: z.string().optional().describe('[upload] absolute path to a local file. Mutually exclusive with content/base64.'),
|
|
@@ -3292,6 +3382,7 @@ tool('asset', 'Manage supporting files (CSS, JS, images, fonts) in the ACTIVE PR
|
|
|
3292
3382
|
// ── Connector tools ───────────────────────────────────────────────
|
|
3293
3383
|
|
|
3294
3384
|
tool('connector', 'Create or remove connectors (arrows) between frames on the surface. `action=connect` adds an arrow from source to target. `action=disconnect` removes one — pass either connectorId directly or source+target to find and delete.', {
|
|
3385
|
+
projectId: PROJECT_OVERRIDE_PARAM,
|
|
3295
3386
|
action: z.enum(['connect', 'disconnect']).describe('Operation to perform.'),
|
|
3296
3387
|
source: z.string().optional().describe('[connect|disconnect] source frame path or ID'),
|
|
3297
3388
|
target: z.string().optional().describe('[connect|disconnect] target frame path or ID'),
|
|
@@ -3338,6 +3429,7 @@ tool('connector', 'Create or remove connectors (arrows) between frames on the su
|
|
|
3338
3429
|
// ── Layout tools ──────────────────────────────────────────────────
|
|
3339
3430
|
|
|
3340
3431
|
tool('layout', 'Auto-arrange frames using graph layout algorithm. Positions connected frames as a directed graph.', {
|
|
3432
|
+
projectId: PROJECT_OVERRIDE_PARAM,
|
|
3341
3433
|
direction: z.enum(['TB', 'LR', 'BT', 'RL']).optional().default('TB').describe('Layout direction: TB (top-bottom), LR (left-right), BT (bottom-top), RL (right-left)'),
|
|
3342
3434
|
}, async ({ direction }) => {
|
|
3343
3435
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.11",
|
|
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": [
|