@1agh/maude 0.40.0 → 0.41.0

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 (73) hide show
  1. package/apps/studio/ai-banner.tsx +2 -2
  2. package/apps/studio/annotations-layer.tsx +53 -2
  3. package/apps/studio/api.ts +504 -3
  4. package/apps/studio/artboard-marquee.tsx +1 -1
  5. package/apps/studio/bin/_canvas-rects-playwright.mjs +55 -0
  6. package/apps/studio/bin/_canvas-rects-static.mjs +166 -0
  7. package/apps/studio/bin/_fetch-asset.mjs +556 -0
  8. package/apps/studio/bin/annotate.mjs +576 -34
  9. package/apps/studio/bin/canvas-rects.sh +152 -0
  10. package/apps/studio/bin/fetch-asset.sh +34 -0
  11. package/apps/studio/bin/read-annotations.mjs +138 -7
  12. package/apps/studio/bin/smoke.sh +42 -6
  13. package/apps/studio/build.ts +21 -0
  14. package/apps/studio/canvas-comment-mount.tsx +138 -4
  15. package/apps/studio/canvas-edit.ts +744 -11
  16. package/apps/studio/canvas-lib.tsx +219 -2
  17. package/apps/studio/canvas-shell.tsx +487 -20
  18. package/apps/studio/client/app.jsx +1451 -22
  19. package/apps/studio/client/comments-overlay.css +130 -126
  20. package/apps/studio/client/github.js +8 -0
  21. package/apps/studio/client/styles/3-shell-maude.css +48 -0
  22. package/apps/studio/comments-overlay.tsx +148 -41
  23. package/apps/studio/context-menu.tsx +15 -5
  24. package/apps/studio/contextual-toolbar.tsx +262 -4
  25. package/apps/studio/cursors-overlay.tsx +4 -4
  26. package/apps/studio/dist/client.bundle.js +20 -20
  27. package/apps/studio/dist/comment-mount.js +59 -1
  28. package/apps/studio/dist/styles.css +1 -1
  29. package/apps/studio/dom-selection.ts +127 -1
  30. package/apps/studio/drag-state.ts +24 -0
  31. package/apps/studio/equal-spacing-detector.ts +205 -0
  32. package/apps/studio/export-dialog.tsx +1 -1
  33. package/apps/studio/history.ts +47 -1
  34. package/apps/studio/http.ts +223 -0
  35. package/apps/studio/input-router.tsx +12 -0
  36. package/apps/studio/marquee-overlay.tsx +1 -1
  37. package/apps/studio/measure-overlay.tsx +241 -0
  38. package/apps/studio/participants-chrome.tsx +3 -3
  39. package/apps/studio/sizing-mode.ts +117 -0
  40. package/apps/studio/spacing-handles.ts +166 -0
  41. package/apps/studio/test/annotate-write.test.ts +890 -0
  42. package/apps/studio/test/camera-reveal.test.tsx +173 -0
  43. package/apps/studio/test/canvas-edit.test.ts +50 -0
  44. package/apps/studio/test/canvas-origin-gate.test.ts +18 -0
  45. package/apps/studio/test/canvas-rects.test.ts +198 -0
  46. package/apps/studio/test/comments-overlay.test.ts +117 -0
  47. package/apps/studio/test/dom-selection.test.ts +130 -0
  48. package/apps/studio/test/edit-css-occurrence.test.ts +81 -0
  49. package/apps/studio/test/edit-scope-api.test.ts +115 -0
  50. package/apps/studio/test/element-resize.test.ts +136 -0
  51. package/apps/studio/test/element-structural-api.test.ts +360 -0
  52. package/apps/studio/test/element-structural-edit.test.ts +233 -0
  53. package/apps/studio/test/equal-spacing-detector.test.ts +165 -1
  54. package/apps/studio/test/history-rollback.test.ts +26 -0
  55. package/apps/studio/test/knob-props-authored.test.ts +87 -0
  56. package/apps/studio/test/read-annotations.test.ts +154 -0
  57. package/apps/studio/test/sizing-mode.test.ts +102 -0
  58. package/apps/studio/test/spacing-handles.test.ts +138 -0
  59. package/apps/studio/test/specimen-select.test.ts +88 -0
  60. package/apps/studio/test/tool-palette-insert-anchor.test.ts +84 -0
  61. package/apps/studio/test/undo-sequence-byte-compare.test.ts +211 -0
  62. package/apps/studio/tool-palette.tsx +122 -2
  63. package/apps/studio/undo-hud.tsx +2 -2
  64. package/apps/studio/use-element-resize.tsx +732 -0
  65. package/apps/studio/use-keyboard-discipline.tsx +205 -15
  66. package/apps/studio/use-selection-set.tsx +14 -0
  67. package/apps/studio/use-spacing-handles.tsx +388 -0
  68. package/apps/studio/whats-new.json +28 -0
  69. package/cli/commands/design.mjs +6 -1
  70. package/cli/commands/design.test.mjs +49 -1
  71. package/cli/lib/fetch-asset.test.mjs +213 -0
  72. package/package.json +8 -8
  73. package/plugins/design/dependencies.json +10 -2
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "$schema": "./whats-new.schema.json",
3
3
  "entries": [
4
+ {
5
+ "id": "desktop-intel-mac-support",
6
+ "version": "0.41.0",
7
+ "date": "2026-07-08",
8
+ "kind": "improvement",
9
+ "title": "Desktop app now runs on Intel Macs",
10
+ "summary": "The macOS desktop app was Apple-Silicon-only — it now ships as a universal binary, so Intel Mac users can download and run it natively too, no Rosetta workaround needed.",
11
+ "learnMore": "https://maude.sh/desktop",
12
+ "surface": "design-ui"
13
+ },
14
+ {
15
+ "id": "element-editing-robustness",
16
+ "version": "0.41.0",
17
+ "date": "2026-07-08",
18
+ "kind": "feature",
19
+ "title": "Edit elements like Figma or Webflow",
20
+ "summary": "Selecting an element now opens the Inspector automatically, and drag handles let you resize it directly on the canvas (corner + edge, Shift to lock aspect, Alt to resize from center) — artboards get free-hand resize too. Delete an element with the Del key or the right-click menu, or insert a new div, text, or image straight into the artboard; add a whole new empty artboard from a Desktop/Laptop/Tablet/Mobile preset. The Inspector gained Position (with a constraints-style inset box), Transform, and Media sections (object-fit, aspect ratio), plus on-canvas dragging for padding and gaps. Swap a photo or video (or a background image) from a built-in asset picker. Editing now works on design-system specimens too, not just your UI canvases. A shared component always tells you plainly whether an edit is local to this artboard or shared across every place it's used, and moving one instance never drags the others along. Rounding it out: keyboard nudge + tree navigation, Cmd+D duplicate, copy/paste style, align and distribute a multi-selection, deep-select + \"Select layer\" for nested/overlapping elements, Alt-hover distance measurement, a free-hand rotate handle, and a proper Fixed/Hug/Fill sizing control for flex layouts. Every one of these is undoable with Cmd+Z.",
21
+ "surface": "design-ui"
22
+ },
23
+ {
24
+ "id": "whiteboard-ai-toolkit",
25
+ "version": "0.41.0",
26
+ "date": "2026-07-08",
27
+ "kind": "feature",
28
+ "title": "The Assistant now reads and draws on your whiteboard",
29
+ "summary": "Sketch stickies, shapes, and arrows on a canvas and the Assistant understands not just which artboard a note sits on, but which button or element it's pointing at. Ask it to pin a note next to something specific, and it'll drop it right there with an arrow pointing at it — no guessing coordinates. It can also lay out a whole tidy board for you: a sprint retro, a kanban, a week of social posts, a roadmap, a brainstorm, a user-flow diagram — just ask.",
30
+ "surface": "design-ui"
31
+ },
4
32
  {
5
33
  "id": "timeline-clip-editing",
6
34
  "version": "0.40.0",
@@ -43,8 +43,10 @@ const BIN_VERBS = new Set([
43
43
  'to-lottie',
44
44
  'read-annotations',
45
45
  'annotate',
46
+ 'canvas-rects',
46
47
  'chat-open',
47
48
  'ensure-browser',
49
+ 'fetch-asset',
48
50
  ]);
49
51
 
50
52
  // Bin verbs that boot the dev-server (directly, or by shelling into server-up.sh).
@@ -136,8 +138,9 @@ Lifecycle:
136
138
 
137
139
  Dev-tooling (dispatch to the dev-server bash helpers — DDR-062):
138
140
  screenshot · server-up · prep · slug · bootstrap-check · runtime-health
139
- smoke · canvas-edit · handoff · asset-sweep · visual-sanity
141
+ smoke · canvas-edit · handoff · asset-sweep · visual-sanity · fetch-asset
140
142
  draw-build · draw-proof · svg-optimize · to-lottie · read-annotations · annotate
143
+ canvas-rects
141
144
  Invoke the bundled helper of the same name. maude resolves it from its
142
145
  own package root and sets CLAUDE_PLUGIN_ROOT for the child; stdout,
143
146
  stderr, and exit code pass straight through (so command-substitution
@@ -147,6 +150,8 @@ Dev-tooling (dispatch to the dev-server bash helpers — DDR-062):
147
150
  PORT=$(maude design server-up --root "$REPO")
148
151
  eval "$(maude design prep --shell-export --shape edit --root "$REPO")"
149
152
  maude design smoke --changed-only
153
+ REF=$(maude design fetch-asset "$URL" --root "$REPO") # download-first image
154
+ maude design canvas-rects "ui/Foo.tsx" # geometry manifest (whiteboard toolkit)
150
155
 
151
156
  serve [--port N] [--root PATH]
152
157
  Start the design plugin's dev server in the current repo. Equivalent
@@ -51,11 +51,59 @@ test('unknown verb exits 2', () => {
51
51
  test('design help lists the dev-tooling verbs', () => {
52
52
  const res = runDesign(['help']);
53
53
  assert.equal(res.status, 0, res.stderr);
54
- for (const verb of ['screenshot', 'server-up', 'prep', 'slug', 'smoke', 'visual-sanity']) {
54
+ for (const verb of [
55
+ 'screenshot',
56
+ 'server-up',
57
+ 'prep',
58
+ 'slug',
59
+ 'smoke',
60
+ 'visual-sanity',
61
+ 'canvas-rects',
62
+ ]) {
55
63
  assert.match(res.stdout, new RegExp(verb), `usage should mention ${verb}`);
56
64
  }
57
65
  });
58
66
 
67
+ // canvas-rects needs an explicit --root pinned to an isolated temp dir (not
68
+ // runDesign's ambient cwd) — its server-detection reads $CLAUDE_PROJECT_DIR /
69
+ // `git rev-parse` when --root is absent, which would escape the temp dir in
70
+ // an environment where either is set.
71
+ function runCanvasRects(args, root) {
72
+ const env = { ...process.env };
73
+ env.CLAUDE_PLUGIN_ROOT = undefined;
74
+ return spawnSync(process.execPath, [BIN, 'design', 'canvas-rects', ...args, '--root', root], {
75
+ cwd: root,
76
+ encoding: 'utf8',
77
+ env,
78
+ });
79
+ }
80
+
81
+ test('canvas-rects dispatches to the bundled helper (--help)', () => {
82
+ const root = mkdtempSync(join(tmpdir(), 'maude-canvas-rects-'));
83
+ try {
84
+ const res = runCanvasRects(['--help'], root);
85
+ assert.equal(res.status, 0, res.stderr);
86
+ assert.match(res.stdout, /canvas-rects\.sh/);
87
+ } finally {
88
+ rmSync(root, { recursive: true, force: true });
89
+ }
90
+ });
91
+
92
+ test('canvas-rects with no live server falls back to the static lane', () => {
93
+ // No .design/_server.json under --root → static fallback, no browser
94
+ // needed. A missing canvas still emits a valid empty manifest (exit 0).
95
+ const root = mkdtempSync(join(tmpdir(), 'maude-canvas-rects-'));
96
+ try {
97
+ const res = runCanvasRects(['ui/Nonexistent.tsx'], root);
98
+ assert.equal(res.status, 0, res.stderr);
99
+ assert.match(res.stderr, /static/);
100
+ const manifest = JSON.parse(res.stdout.trim());
101
+ assert.deepEqual(manifest, { artboards: [], elements: [], elementsTruncated: false });
102
+ } finally {
103
+ rmSync(root, { recursive: true, force: true });
104
+ }
105
+ });
106
+
59
107
  // resolveServerBinary — the dev-server boot-runtime resolver (DDR-084). server-up
60
108
  // must boot the compiled platform binary on a production install (it embeds yjs +
61
109
  // every dep; `bun server.ts` from source can't resolve them — the historic
@@ -0,0 +1,213 @@
1
+ // DDR-147 § Security follow-up item 1 — the hardened `maude design fetch-asset`
2
+ // helper (apps/studio/bin/_fetch-asset.mjs). This suite locks the SECURITY CORE
3
+ // that the untrusted-fetch rails promise: the resolved-IP SSRF classifier
4
+ // (reject loopback / link-local / RFC-1918 / CGNAT / multicast / reserved, v4 +
5
+ // v6 incl. IPv4-mapped + NAT64), https-only URL parsing, the png/jpg/gif/webp
6
+ // magic sniff (SVG/HTML rejected), the content-addressed name contract, and the
7
+ // realpath containment assertion. Runs under `pnpm test` (node --test cli/**).
8
+ //
9
+ // The network path (curl) is exercised by the manual smoke in the RCA/PR; these
10
+ // are the offline, deterministic guards a regression would trip.
11
+
12
+ import assert from 'node:assert/strict';
13
+ import { existsSync, readFileSync } from 'node:fs';
14
+ import { dirname, resolve } from 'node:path';
15
+ import { test } from 'node:test';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const HERE = dirname(fileURLToPath(import.meta.url));
19
+ const REPO = resolve(HERE, '..', '..');
20
+ const MOD = resolve(REPO, 'apps', 'studio', 'bin', '_fetch-asset.mjs');
21
+
22
+ const {
23
+ classifyAddress,
24
+ parseIPv4,
25
+ parseIPv6,
26
+ parseHttpsTarget,
27
+ sniffImageExt,
28
+ assetName,
29
+ containedAssetPath,
30
+ FetchAssetError,
31
+ } = await import(MOD);
32
+
33
+ // ── SSRF classifier: BLOCKED addresses ───────────────────────────────────────
34
+ test('classifyAddress blocks private / loopback / link-local / reserved IPv4', () => {
35
+ const blocked = [
36
+ '127.0.0.1',
37
+ '127.9.9.9',
38
+ '10.0.0.1',
39
+ '10.255.255.255',
40
+ '172.16.0.1',
41
+ '172.31.255.255',
42
+ '192.168.1.1',
43
+ '169.254.169.254', // AWS/GCP IMDS
44
+ '169.254.0.1',
45
+ '100.64.0.1', // CGNAT
46
+ '100.127.255.255',
47
+ '0.0.0.0',
48
+ '224.0.0.1', // multicast
49
+ '239.255.255.255',
50
+ '240.0.0.1', // reserved
51
+ '255.255.255.255',
52
+ '198.18.0.1', // benchmark
53
+ '192.0.2.5', // TEST-NET-1
54
+ '192.0.0.1', // IETF
55
+ ];
56
+ for (const ip of blocked) {
57
+ assert.ok(classifyAddress(ip), `expected ${ip} to be BLOCKED but it was allowed`);
58
+ }
59
+ });
60
+
61
+ test('classifyAddress allows genuine public IPv4 (incl. just-outside-range boundaries)', () => {
62
+ const allowed = [
63
+ '8.8.8.8',
64
+ '1.1.1.1',
65
+ '93.184.216.34', // example.com
66
+ '172.15.255.255', // just below 172.16/12
67
+ '172.32.0.1', // just above 172.31
68
+ '100.63.255.255', // just below CGNAT
69
+ '100.128.0.1', // just above CGNAT
70
+ '169.253.255.255', // just below link-local
71
+ '11.0.0.1',
72
+ '192.167.255.255',
73
+ '192.169.0.1',
74
+ '223.255.255.255', // just below multicast
75
+ ];
76
+ for (const ip of allowed) {
77
+ assert.equal(classifyAddress(ip), null, `expected ${ip} to be ALLOWED but it was blocked`);
78
+ }
79
+ });
80
+
81
+ test('classifyAddress blocks loopback / link-local / ULA / multicast + embedded-v4 IPv6', () => {
82
+ const blocked = [
83
+ '::1', // loopback
84
+ '::', // unspecified
85
+ 'fe80::1', // link-local
86
+ 'fe80::abcd:1234',
87
+ 'fc00::1', // ULA
88
+ 'fd12:3456::1', // ULA
89
+ 'ff02::1', // multicast
90
+ '::ffff:127.0.0.1', // IPv4-mapped loopback
91
+ '::ffff:10.0.0.1', // IPv4-mapped private
92
+ '::ffff:169.254.169.254', // IPv4-mapped IMDS
93
+ '64:ff9b::127.0.0.1', // NAT64 → loopback
94
+ '64:ff9b::192.168.0.1', // NAT64 → private
95
+ ];
96
+ for (const ip of blocked) {
97
+ assert.ok(classifyAddress(ip), `expected ${ip} to be BLOCKED but it was allowed`);
98
+ }
99
+ });
100
+
101
+ test('classifyAddress allows genuine public IPv6 (incl. IPv4-mapped public)', () => {
102
+ const allowed = [
103
+ '2001:4860:4860::8888', // Google DNS
104
+ '2606:4700:4700::1111', // Cloudflare DNS
105
+ '::ffff:8.8.8.8', // IPv4-mapped PUBLIC → allowed via embedded classify
106
+ '2a00:1450:4001:81b::200e',
107
+ ];
108
+ for (const ip of allowed) {
109
+ assert.equal(classifyAddress(ip), null, `expected ${ip} to be ALLOWED but it was blocked`);
110
+ }
111
+ });
112
+
113
+ test('classifyAddress fail-closes on non-IP / garbage input', () => {
114
+ for (const bad of ['not-an-ip', 'example.com', '', '999.999.999.999', 'gggg::1']) {
115
+ assert.ok(classifyAddress(bad), `expected ${JSON.stringify(bad)} to be treated as blocked`);
116
+ }
117
+ });
118
+
119
+ // ── IP parsers ───────────────────────────────────────────────────────────────
120
+ test('parseIPv4 rejects malformed dotted-quads', () => {
121
+ for (const bad of ['1.2.3', '1.2.3.4.5', '256.1.1.1', '01.2.3.4', '1.2.3.a', '', '-1.2.3.4']) {
122
+ assert.equal(parseIPv4(bad), null, `expected ${JSON.stringify(bad)} to fail parse`);
123
+ }
124
+ assert.deepEqual([...parseIPv4('1.2.3.4')], [1, 2, 3, 4]);
125
+ assert.deepEqual([...parseIPv4('255.255.255.255')], [255, 255, 255, 255]);
126
+ });
127
+
128
+ test('parseIPv6 handles ::, embedded IPv4, zone id; rejects malformed', () => {
129
+ assert.ok(parseIPv6('::1'));
130
+ assert.ok(parseIPv6('::'));
131
+ assert.ok(parseIPv6('fe80::1%en0')); // zone id stripped
132
+ assert.ok(parseIPv6('2001:db8::1'));
133
+ assert.ok(parseIPv6('::ffff:1.2.3.4'));
134
+ for (const bad of ['gggg::', '1::2::3', '12345::', '1.2.3.4', '']) {
135
+ assert.equal(parseIPv6(bad), null, `expected ${JSON.stringify(bad)} to fail parse`);
136
+ }
137
+ });
138
+
139
+ // ── URL target ───────────────────────────────────────────────────────────────
140
+ test('parseHttpsTarget accepts https and extracts host/port', () => {
141
+ assert.deepEqual(parseHttpsTarget('https://example.com/a.png'), {
142
+ host: 'example.com',
143
+ port: 443,
144
+ });
145
+ assert.deepEqual(parseHttpsTarget('https://cdn.example.com:8443/x'), {
146
+ host: 'cdn.example.com',
147
+ port: 8443,
148
+ });
149
+ });
150
+
151
+ test('parseHttpsTarget rejects non-https, credentials, and hostless URLs', () => {
152
+ const bad = [
153
+ 'http://example.com/a.png',
154
+ 'ftp://example.com/a.png',
155
+ 'file:///etc/passwd',
156
+ 'https://user:pass@example.com/a.png',
157
+ 'not a url',
158
+ ];
159
+ for (const u of bad) {
160
+ assert.throws(() => parseHttpsTarget(u), FetchAssetError, `expected ${u} to throw`);
161
+ }
162
+ });
163
+
164
+ // ── image sniff ──────────────────────────────────────────────────────────────
165
+ test('sniffImageExt recognises png/jpg/gif/webp and rejects SVG/HTML/empty', () => {
166
+ const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0]);
167
+ const jpg = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0, 0]);
168
+ const gif = Buffer.from([0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0, 0]);
169
+ const webp = Buffer.from([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50]);
170
+ assert.equal(sniffImageExt(png), 'png');
171
+ assert.equal(sniffImageExt(jpg), 'jpg');
172
+ assert.equal(sniffImageExt(gif), 'gif');
173
+ assert.equal(sniffImageExt(webp), 'webp');
174
+ assert.equal(sniffImageExt(Buffer.from('<svg xmlns="...')), null);
175
+ assert.equal(sniffImageExt(Buffer.from('<!DOCTYPE html>')), null);
176
+ assert.equal(sniffImageExt(Buffer.from([])), null);
177
+ // RIFF that is NOT webp (e.g. wav) must not sniff as an image.
178
+ assert.equal(
179
+ sniffImageExt(Buffer.from([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x41, 0x56, 0x45])),
180
+ null
181
+ );
182
+ });
183
+
184
+ // ── naming + containment ─────────────────────────────────────────────────────
185
+ test('assetName is content-addressed <sha8>.<ext> and deterministic', () => {
186
+ const bytes = Buffer.from('hello world');
187
+ const n1 = assetName(bytes, 'png');
188
+ const n2 = assetName(bytes, 'png');
189
+ assert.equal(n1, n2);
190
+ assert.match(n1, /^[a-f0-9]{8}\.png$/);
191
+ assert.notEqual(assetName(Buffer.from('other'), 'jpg'), n1);
192
+ });
193
+
194
+ test('containedAssetPath resolves under <root>/<designRoot>/assets and rejects escape', () => {
195
+ const { assetsDir, fileAbs } = containedAssetPath('/repo', '.design', 'a1b2c3d4.png');
196
+ assert.equal(assetsDir, resolve('/repo/.design/assets'));
197
+ assert.equal(fileAbs, resolve('/repo/.design/assets/a1b2c3d4.png'));
198
+ // A traversal name escapes → throws.
199
+ assert.throws(() => containedAssetPath('/repo', '.design', '../../etc/passwd'), FetchAssetError);
200
+ // A design-root that climbs out of the repo → throws.
201
+ assert.throws(() => containedAssetPath('/repo', '../../etc', 'a1b2c3d4.png'), FetchAssetError);
202
+ });
203
+
204
+ // ── wiring: verb registered + helper files ship ──────────────────────────────
205
+ test('fetch-asset is a registered `maude design` bin verb with both helper files present', () => {
206
+ assert.ok(existsSync(resolve(REPO, 'apps/studio/bin/fetch-asset.sh')), 'fetch-asset.sh missing');
207
+ assert.ok(
208
+ existsSync(resolve(REPO, 'apps/studio/bin/_fetch-asset.mjs')),
209
+ '_fetch-asset.mjs missing'
210
+ );
211
+ const design = readFileSync(resolve(REPO, 'cli/commands/design.mjs'), 'utf8');
212
+ assert.match(design, /'fetch-asset'/, "'fetch-asset' not in design.mjs BIN_VERBS");
213
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1agh/maude",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "description": "Marketplace of Claude Code plugins by Michal Dovrtěl: `design` (canvas-first design iteration) + `flow` (generic agentic workflow loop with .ai second brain). Ships the `maude` CLI (with `mdcc` legacy alias) to scaffold workspace, run the design dev server, and manage configs.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -50,13 +50,13 @@
50
50
  "test:e2e:desktop:onboarding": "pnpm --filter @maude/desktop-e2e e2e:onboarding"
51
51
  },
52
52
  "optionalDependencies": {
53
- "@1agh/maude-darwin-arm64": "0.40.0",
54
- "@1agh/maude-darwin-x64": "0.40.0",
55
- "@1agh/maude-linux-arm64": "0.40.0",
56
- "@1agh/maude-linux-arm64-musl": "0.40.0",
57
- "@1agh/maude-linux-x64": "0.40.0",
58
- "@1agh/maude-linux-x64-musl": "0.40.0",
59
- "@1agh/maude-win32-x64": "0.40.0"
53
+ "@1agh/maude-darwin-arm64": "0.41.0",
54
+ "@1agh/maude-darwin-x64": "0.41.0",
55
+ "@1agh/maude-linux-arm64": "0.41.0",
56
+ "@1agh/maude-linux-arm64-musl": "0.41.0",
57
+ "@1agh/maude-linux-x64": "0.41.0",
58
+ "@1agh/maude-linux-x64-musl": "0.41.0",
59
+ "@1agh/maude-win32-x64": "0.41.0"
60
60
  },
61
61
  "files": [
62
62
  "cli",
@@ -74,8 +74,10 @@
74
74
  "commands/new.md",
75
75
  "commands/edit.md",
76
76
  "commands/smoke.md",
77
+ "commands/board.md",
77
78
  "dev-server/bin/screenshot.sh",
78
79
  "dev-server/bin/smoke.sh",
80
+ "dev-server/bin/canvas-rects.sh",
79
81
  "agents/design-critic.md",
80
82
  "agents/graphic-design-critic.md",
81
83
  "agents/a11y-critic.md"
@@ -104,7 +106,9 @@
104
106
  "dev-server/bin/_html-playwright.mjs",
105
107
  "dev-server/bin/_pptx-playwright.mjs",
106
108
  "dev-server/bin/_enumerate-artboards-playwright.mjs",
107
- "dev-server/bin/screenshot.sh"
109
+ "dev-server/bin/screenshot.sh",
110
+ "dev-server/bin/_canvas-rects-playwright.mjs",
111
+ "dev-server/bin/canvas-rects.sh"
108
112
  ],
109
113
  "docsUrl": "https://playwright.dev"
110
114
  },
@@ -157,7 +161,11 @@
157
161
  },
158
162
  "autoInstall": false,
159
163
  "fallbackBehavior": "Slash commands' bash blocks that read .ai/workflows.config.json via jq degrade to inline node one-liners; harder to read but functional.",
160
- "usedBy": ["dev-server/bin/bootstrap-check.sh", "dev-server/bin/server-up.sh"]
164
+ "usedBy": [
165
+ "dev-server/bin/bootstrap-check.sh",
166
+ "dev-server/bin/server-up.sh",
167
+ "dev-server/bin/canvas-rects.sh"
168
+ ]
161
169
  },
162
170
  {
163
171
  "id": "askuserquestion-mcp",