drafted 1.17.7 → 1.17.9
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 +20 -6
- package/package.json +1 -1
- package/src/shared/excalidraw.mjs +20 -1
- package/src/shared/test-excalidraw-merge.mjs +13 -0
package/mcp/server.mjs
CHANGED
|
@@ -2678,7 +2678,11 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
2678
2678
|
}
|
|
2679
2679
|
case 'read': {
|
|
2680
2680
|
const canonPath = wikiPath.replace(/\.md$/, '');
|
|
2681
|
-
|
|
2681
|
+
// hashline-annotated (read → edit stays surgical), like frames; lines
|
|
2682
|
+
// honors partial reads. raw=true keeps it body-only (no synthesized
|
|
2683
|
+
// frontmatter) so ops anchor to the same bytes /edit applies against.
|
|
2684
|
+
const q = `&raw=true&format=hashline${lines ? `&lines=${encodeURIComponent(lines)}` : ''}`;
|
|
2685
|
+
const page = await api('GET', `/api/wiki/page?path=${encodeURIComponent(canonPath)}${q}`, undefined, orgHeader);
|
|
2682
2686
|
if (!page?.content) return err(new Error(`wiki page not found: ${canonPath}`));
|
|
2683
2687
|
return ok(page.content);
|
|
2684
2688
|
}
|
|
@@ -2704,15 +2708,25 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
2704
2708
|
return ok(result);
|
|
2705
2709
|
}
|
|
2706
2710
|
case 'rm': {
|
|
2707
|
-
//
|
|
2708
|
-
//
|
|
2709
|
-
//
|
|
2711
|
+
// Live page → soft delete: move it under archive/ (mv cascades inbound
|
|
2712
|
+
// refs, so links keep working into the archive). Already-archived page
|
|
2713
|
+
// → hard purge: it IS the trash (the web UI offers permanent delete
|
|
2714
|
+
// there), and without it an occupied archive slot had no escape hatch
|
|
2715
|
+
// via MCP — the old behavior self-moved (from == to) and reported "as
|
|
2716
|
+
// if nothing happened" while the server renamed the row to its own
|
|
2717
|
+
// collision suffix.
|
|
2710
2718
|
const canonPath = wikiPath.replace(/\.md$/, '');
|
|
2711
|
-
const archivePath = canonPath.startsWith('archive/') ? canonPath : `archive/${canonPath}`;
|
|
2712
2719
|
const page = await api('GET', `/api/wiki/page?path=${encodeURIComponent(canonPath)}`, undefined, orgHeader).catch(() => null);
|
|
2713
2720
|
if (!page?.id) return err(new Error(`wiki page not found: ${canonPath}`));
|
|
2721
|
+
if (canonPath.startsWith('archive/')) {
|
|
2722
|
+
const del = await api('DELETE', `/api/wiki/pages/${page.id}`, undefined, orgHeader);
|
|
2723
|
+
return ok({ deleted: true, path: canonPath, ...(del && del.deleted ? { id: del.deleted } : {}) });
|
|
2724
|
+
}
|
|
2725
|
+
const archivePath = `archive/${canonPath}`;
|
|
2714
2726
|
const result = await api('PATCH', `/api/wiki/pages/${page.id}/move`, { path: archivePath }, orgHeader);
|
|
2715
|
-
|
|
2727
|
+
// Report the server-authoritative final path — an occupied archive slot
|
|
2728
|
+
// auto-suffixes (-old), and the response must match the DB, not guess.
|
|
2729
|
+
return ok({ archived: true, from: canonPath, to: result?.path || archivePath, ...(result?.referrersUpdated ? { referrersUpdated: result.referrersUpdated } : {}) });
|
|
2716
2730
|
}
|
|
2717
2731
|
case 'search': {
|
|
2718
2732
|
markSearched(gs, 'wiki');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.9",
|
|
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": [
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
export const EXCALIDRAW_MIME = 'application/vnd.excalidraw+json';
|
|
2
2
|
|
|
3
|
+
// Linear elements (arrow/line/freedraw) REQUIRE a `points` array — Excalidraw's scene
|
|
4
|
+
// restore reads points.length and crashes ("Cannot read properties of undefined (reading
|
|
5
|
+
// 'length')") → eternal "Loading scene" spinner. Hand-authored/agent-written scenes
|
|
6
|
+
// routinely omit it (source: "https://excalidraw.com", ids like "el1"), so repair it
|
|
7
|
+
// here: every read (serve) and write path funnels through normalizeExcalidrawScene, so
|
|
8
|
+
// existing broken frames open and new bad writes are fixed before they land.
|
|
9
|
+
const LINEAR_ELEMENT_TYPES = new Set(['arrow', 'line', 'freedraw']);
|
|
10
|
+
|
|
11
|
+
function repairLinearElement(el) {
|
|
12
|
+
if (!LINEAR_ELEMENT_TYPES.has(el.type)) return el;
|
|
13
|
+
const pts = el.points;
|
|
14
|
+
const valid = Array.isArray(pts) && pts.length > 0 &&
|
|
15
|
+
pts.every((p) => Array.isArray(p) && p.length >= 2 && typeof p[0] === 'number' && typeof p[1] === 'number');
|
|
16
|
+
if (valid) return el;
|
|
17
|
+
const w = typeof el.width === 'number' && el.width > 0 ? el.width : 1;
|
|
18
|
+
const h = typeof el.height === 'number' && el.height > 0 ? el.height : 0;
|
|
19
|
+
return { ...el, points: [[0, 0], [w, h]] };
|
|
20
|
+
}
|
|
21
|
+
|
|
3
22
|
export function emptyExcalidrawScene() {
|
|
4
23
|
return {
|
|
5
24
|
type: 'excalidraw',
|
|
@@ -29,7 +48,7 @@ export function normalizeExcalidrawScene(input) {
|
|
|
29
48
|
type: scene.type || 'excalidraw',
|
|
30
49
|
version: scene.version || 2,
|
|
31
50
|
source: scene.source || 'https://drafted.live',
|
|
32
|
-
elements: scene.elements,
|
|
51
|
+
elements: scene.elements.map(repairLinearElement).filter(Boolean),
|
|
33
52
|
appState,
|
|
34
53
|
files,
|
|
35
54
|
};
|
|
@@ -34,6 +34,19 @@ assert.deepStrictEqual(r.elements.map(e => e.id), ['b', 'c'], 'remove beats upse
|
|
|
34
34
|
r = mergeExcalidrawElements(JSON.stringify(base), [{ id: 'e', type: 'text', x: 0 }]);
|
|
35
35
|
assert.strictEqual(r.elements.length, 4, 'parses stored string content');
|
|
36
36
|
|
|
37
|
+
// linear elements without points are repaired (Excalidraw crashes on restore otherwise)
|
|
38
|
+
r = mergeExcalidrawElements({
|
|
39
|
+
...base,
|
|
40
|
+
elements: [
|
|
41
|
+
{ id: 'a1', type: 'arrow', x: 0, y: 0, width: 60, height: 0 },
|
|
42
|
+
{ id: 'l1', type: 'line', x: 0, y: 0, width: 0, height: 0 },
|
|
43
|
+
{ id: 'ok', type: 'arrow', x: 0, y: 0, width: 10, height: 10, points: [[0, 0], [10, 10]] },
|
|
44
|
+
],
|
|
45
|
+
}, []);
|
|
46
|
+
assert.deepStrictEqual(r.elements[0].points, [[0, 0], [60, 0]], 'arrow points synthesized from width');
|
|
47
|
+
assert.deepStrictEqual(r.elements[1].points, [[0, 0], [1, 0]], 'degenerate line gets a fallback line');
|
|
48
|
+
assert.deepStrictEqual(r.elements[2].points, [[0, 0], [10, 10]], 'valid points untouched');
|
|
49
|
+
|
|
37
50
|
// upsert without id is rejected
|
|
38
51
|
assert.throws(() => mergeExcalidrawElements(base, [{ x: 1 }]), /must be an object with an id/);
|
|
39
52
|
|