drafted 1.8.2 → 1.8.4
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 +46 -20
- package/package.json +1 -1
- package/server/lib/umami.mjs +6 -0
package/mcp/server.mjs
CHANGED
|
@@ -755,7 +755,16 @@ async function api(method, path, body, _retried) {
|
|
|
755
755
|
}
|
|
756
756
|
|
|
757
757
|
if (!res.ok) {
|
|
758
|
-
|
|
758
|
+
let msg = data.error || `HTTP ${res.status}`;
|
|
759
|
+
// Hashline edits (frame.edit, wiki edit) report a per-op `errors` array.
|
|
760
|
+
// Surface each one so the agent can fix the specific operation instead of
|
|
761
|
+
// seeing only the generic "Edit could not be applied".
|
|
762
|
+
if (Array.isArray(data.errors) && data.errors.length) {
|
|
763
|
+
const detail = data.errors
|
|
764
|
+
.map(e => ` • ${Number.isInteger(e.index) ? `op ${e.index + 1}: ` : ''}${e.error}`)
|
|
765
|
+
.join('\n');
|
|
766
|
+
msg += `\n${detail}`;
|
|
767
|
+
}
|
|
759
768
|
// The active project no longer resolves in the current org context.
|
|
760
769
|
// This is the silent-drift bug: project.open set a project, then
|
|
761
770
|
// something switched the active org (parallel agent, browser tab, or
|
|
@@ -1494,9 +1503,12 @@ tool('screenshot', 'Render a PNG via headless browser. `scope=frame` captures a
|
|
|
1494
1503
|
width: z.number().optional().describe('Viewport width in pixels (frame default 1440, canvas default 1600).'),
|
|
1495
1504
|
height: z.number().optional().describe('Viewport height in pixels (frame default 900, canvas default 1200).'),
|
|
1496
1505
|
fullPage: z.boolean().optional().describe('[scope=frame] capture full page or just viewport (default true).'),
|
|
1506
|
+
// outputPath is local-only — omitted on remote transports (web MCP) where the host has no caller filesystem.
|
|
1507
|
+
...(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
1508
|
}, async (args) => {
|
|
1498
1509
|
try {
|
|
1499
1510
|
const { scope } = args;
|
|
1511
|
+
let buffer;
|
|
1500
1512
|
if (scope === 'frame') {
|
|
1501
1513
|
const { target, width = 1440, height = 900, fullPage = true } = args;
|
|
1502
1514
|
if (!target) throw new Error('target required for scope=frame');
|
|
@@ -1514,13 +1526,8 @@ tool('screenshot', 'Render a PNG via headless browser. `scope=frame` captures a
|
|
|
1514
1526
|
await ensureSession();
|
|
1515
1527
|
const res = await fetch(url, { headers: getAuthHeaders() });
|
|
1516
1528
|
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') {
|
|
1529
|
+
buffer = Buffer.from(await res.arrayBuffer());
|
|
1530
|
+
} else if (scope === 'canvas') {
|
|
1524
1531
|
const { slug, layer = 'plans', width = 1600, height = 1200 } = args;
|
|
1525
1532
|
let targetSlug = slug;
|
|
1526
1533
|
if (!targetSlug) {
|
|
@@ -1533,13 +1540,21 @@ tool('screenshot', 'Render a PNG via headless browser. `scope=frame` captures a
|
|
|
1533
1540
|
await ensureSession();
|
|
1534
1541
|
const res = await fetch(url, { headers: getAuthHeaders() });
|
|
1535
1542
|
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
|
-
};
|
|
1543
|
+
buffer = Buffer.from(await res.arrayBuffer());
|
|
1544
|
+
} else {
|
|
1545
|
+
throw new Error(`Unknown screenshot scope: ${scope}`);
|
|
1541
1546
|
}
|
|
1542
|
-
|
|
1547
|
+
|
|
1548
|
+
// Persist to local disk when requested (stdio only) — returns the path
|
|
1549
|
+
// instead of the inline image so the PNG bytes don't burn context.
|
|
1550
|
+
if (!isRemote && args.outputPath) {
|
|
1551
|
+
const resolved = resolve(args.outputPath);
|
|
1552
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
1553
|
+
writeFileSync(resolved, buffer);
|
|
1554
|
+
return ok({ ok: true, scope, savedTo: resolved, bytes: buffer.length });
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
return { content: [{ type: 'image', data: buffer.toString('base64'), mimeType: 'image/png' }] };
|
|
1543
1558
|
} catch (error) { return err(error); }
|
|
1544
1559
|
});
|
|
1545
1560
|
|
|
@@ -2282,16 +2297,27 @@ tool('ls', 'List contents of the ACTIVE PROJECT. Use ls / after project(action="
|
|
|
2282
2297
|
}
|
|
2283
2298
|
|
|
2284
2299
|
// ChatGPT Apps SDK: canvas-overview widget renders byLayer.
|
|
2300
|
+
// /api/fs/ entries are { path, type: 'layer'|'lane'|'frame', id, title, size,
|
|
2301
|
+
// updatedAt, color, frameUrl } — there are no layer/lane/name/label fields, so
|
|
2302
|
+
// derive them from the path. Skip non-frame entries (lanes/layers are structural
|
|
2303
|
+
// and would otherwise serialize as empty objects under "unsorted").
|
|
2285
2304
|
const entries = Array.isArray(result?.entries) ? result.entries : [];
|
|
2286
2305
|
const byLayer = {};
|
|
2287
2306
|
for (const e of entries) {
|
|
2288
|
-
|
|
2307
|
+
if (e.type && e.type !== 'frame') continue;
|
|
2308
|
+
const segs = String(e.path || '').replace(/^\/+/, '').split('/').filter(Boolean);
|
|
2309
|
+
if (!segs.length) continue;
|
|
2310
|
+
const layer = segs[0];
|
|
2311
|
+
const filename = segs[segs.length - 1];
|
|
2312
|
+
const lane = segs.length >= 3 ? segs.slice(1, -1).join('/') : undefined;
|
|
2289
2313
|
(byLayer[layer] ??= []).push({
|
|
2290
|
-
label: e.
|
|
2291
|
-
filename
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2314
|
+
label: e.title || filename,
|
|
2315
|
+
filename,
|
|
2316
|
+
title: e.title,
|
|
2317
|
+
layer,
|
|
2318
|
+
lane,
|
|
2319
|
+
id: e.id,
|
|
2320
|
+
frameUrl: e.frameUrl || (e.id ? `${getServerUrl()}/f/${e.id}` : undefined),
|
|
2295
2321
|
});
|
|
2296
2322
|
}
|
|
2297
2323
|
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.4",
|
|
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'],
|