drafted 1.8.2 → 1.8.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.
- package/mcp/server.mjs +36 -19
- package/package.json +1 -1
- package/server/lib/umami.mjs +6 -0
package/mcp/server.mjs
CHANGED
|
@@ -1494,9 +1494,12 @@ tool('screenshot', 'Render a PNG via headless browser. `scope=frame` captures a
|
|
|
1494
1494
|
width: z.number().optional().describe('Viewport width in pixels (frame default 1440, canvas default 1600).'),
|
|
1495
1495
|
height: z.number().optional().describe('Viewport height in pixels (frame default 900, canvas default 1200).'),
|
|
1496
1496
|
fullPage: z.boolean().optional().describe('[scope=frame] capture full page or just viewport (default true).'),
|
|
1497
|
+
// outputPath is local-only — omitted on remote transports (web MCP) where the host has no caller filesystem.
|
|
1498
|
+
...(isRemote ? {} : { outputPath: z.string().optional().describe('[stdio only] Absolute local path to write the rendered PNG to. When set, the PNG is saved to disk and the tool returns the path + byte count instead of returning the image inline — use this to get rendered pixels OUT of Drafted (attach to a message, upload, post-process). Parent directories are created if missing.') }),
|
|
1497
1499
|
}, async (args) => {
|
|
1498
1500
|
try {
|
|
1499
1501
|
const { scope } = args;
|
|
1502
|
+
let buffer;
|
|
1500
1503
|
if (scope === 'frame') {
|
|
1501
1504
|
const { target, width = 1440, height = 900, fullPage = true } = args;
|
|
1502
1505
|
if (!target) throw new Error('target required for scope=frame');
|
|
@@ -1514,13 +1517,8 @@ tool('screenshot', 'Render a PNG via headless browser. `scope=frame` captures a
|
|
|
1514
1517
|
await ensureSession();
|
|
1515
1518
|
const res = await fetch(url, { headers: getAuthHeaders() });
|
|
1516
1519
|
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
return {
|
|
1520
|
-
content: [{ type: 'image', data: base64, mimeType: 'image/png' }],
|
|
1521
|
-
};
|
|
1522
|
-
}
|
|
1523
|
-
if (scope === 'canvas') {
|
|
1520
|
+
buffer = Buffer.from(await res.arrayBuffer());
|
|
1521
|
+
} else if (scope === 'canvas') {
|
|
1524
1522
|
const { slug, layer = 'plans', width = 1600, height = 1200 } = args;
|
|
1525
1523
|
let targetSlug = slug;
|
|
1526
1524
|
if (!targetSlug) {
|
|
@@ -1533,13 +1531,21 @@ tool('screenshot', 'Render a PNG via headless browser. `scope=frame` captures a
|
|
|
1533
1531
|
await ensureSession();
|
|
1534
1532
|
const res = await fetch(url, { headers: getAuthHeaders() });
|
|
1535
1533
|
if (!res.ok) throw new Error(`Canvas screenshot failed: ${res.status} ${await res.text()}`);
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
content: [{ type: 'image', data: base64, mimeType: 'image/png' }],
|
|
1540
|
-
};
|
|
1534
|
+
buffer = Buffer.from(await res.arrayBuffer());
|
|
1535
|
+
} else {
|
|
1536
|
+
throw new Error(`Unknown screenshot scope: ${scope}`);
|
|
1541
1537
|
}
|
|
1542
|
-
|
|
1538
|
+
|
|
1539
|
+
// Persist to local disk when requested (stdio only) — returns the path
|
|
1540
|
+
// instead of the inline image so the PNG bytes don't burn context.
|
|
1541
|
+
if (!isRemote && args.outputPath) {
|
|
1542
|
+
const resolved = resolve(args.outputPath);
|
|
1543
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
1544
|
+
writeFileSync(resolved, buffer);
|
|
1545
|
+
return ok({ ok: true, scope, savedTo: resolved, bytes: buffer.length });
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
return { content: [{ type: 'image', data: buffer.toString('base64'), mimeType: 'image/png' }] };
|
|
1543
1549
|
} catch (error) { return err(error); }
|
|
1544
1550
|
});
|
|
1545
1551
|
|
|
@@ -2282,16 +2288,27 @@ tool('ls', 'List contents of the ACTIVE PROJECT. Use ls / after project(action="
|
|
|
2282
2288
|
}
|
|
2283
2289
|
|
|
2284
2290
|
// ChatGPT Apps SDK: canvas-overview widget renders byLayer.
|
|
2291
|
+
// /api/fs/ entries are { path, type: 'layer'|'lane'|'frame', id, title, size,
|
|
2292
|
+
// updatedAt, color, frameUrl } — there are no layer/lane/name/label fields, so
|
|
2293
|
+
// derive them from the path. Skip non-frame entries (lanes/layers are structural
|
|
2294
|
+
// and would otherwise serialize as empty objects under "unsorted").
|
|
2285
2295
|
const entries = Array.isArray(result?.entries) ? result.entries : [];
|
|
2286
2296
|
const byLayer = {};
|
|
2287
2297
|
for (const e of entries) {
|
|
2288
|
-
|
|
2298
|
+
if (e.type && e.type !== 'frame') continue;
|
|
2299
|
+
const segs = String(e.path || '').replace(/^\/+/, '').split('/').filter(Boolean);
|
|
2300
|
+
if (!segs.length) continue;
|
|
2301
|
+
const layer = segs[0];
|
|
2302
|
+
const filename = segs[segs.length - 1];
|
|
2303
|
+
const lane = segs.length >= 3 ? segs.slice(1, -1).join('/') : undefined;
|
|
2289
2304
|
(byLayer[layer] ??= []).push({
|
|
2290
|
-
label: e.
|
|
2291
|
-
filename
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2305
|
+
label: e.title || filename,
|
|
2306
|
+
filename,
|
|
2307
|
+
title: e.title,
|
|
2308
|
+
layer,
|
|
2309
|
+
lane,
|
|
2310
|
+
id: e.id,
|
|
2311
|
+
frameUrl: e.frameUrl || (e.id ? `${getServerUrl()}/f/${e.id}` : undefined),
|
|
2295
2312
|
});
|
|
2296
2313
|
}
|
|
2297
2314
|
const projectId = getState().projectId;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.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
|
"files": [
|
package/server/lib/umami.mjs
CHANGED
|
@@ -129,11 +129,17 @@ function getRequestUrl(req) {
|
|
|
129
129
|
export function trackUmamiEvent(name, data = {}, req = null) {
|
|
130
130
|
const umami = getUmamiConfig();
|
|
131
131
|
if (!umami.enabled || !name || name.length > 50) return;
|
|
132
|
+
// Account de-dupe: when the event carries a userId, send it as Umami's distinct id
|
|
133
|
+
// (`payload.id`). Umami then derives sessionId = uuid(websiteId, userId) with no IP/UA/date
|
|
134
|
+
// salt, so server-side events land in the SAME canonical session as the browser's
|
|
135
|
+
// umami.identify(userId) call — collapsing each account to one session across devices/time.
|
|
136
|
+
const distinctId = (typeof data.userId === 'string' && data.userId.length <= 64) ? data.userId : undefined;
|
|
132
137
|
const payload = {
|
|
133
138
|
type: 'event',
|
|
134
139
|
payload: {
|
|
135
140
|
website: umami.websiteId,
|
|
136
141
|
name,
|
|
142
|
+
...(distinctId ? { id: distinctId } : {}),
|
|
137
143
|
url: getRequestUrl(req) || process.env.BASE_URL || 'https://drafted.live',
|
|
138
144
|
hostname: req?.hostname || 'drafted.live',
|
|
139
145
|
language: req?.headers?.['accept-language'],
|