pmx-canvas 0.1.5 → 0.1.7

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.
@@ -12,6 +12,7 @@ export interface CanvasCreateTypeSchema {
12
12
  kind: 'node' | 'virtual-node';
13
13
  description: string;
14
14
  endpoint: string;
15
+ mcpTool?: string;
15
16
  fields: CanvasCreateField[];
16
17
  example: Record<string, unknown>;
17
18
  notes?: string[];
@@ -39,6 +40,7 @@ export declare function describeCanvasSchema(): {
39
40
  mcp: {
40
41
  tools: string[];
41
42
  resources: string[];
43
+ nodeTypeRouting: Record<string, string>;
42
44
  };
43
45
  };
44
46
  export declare function validateStructuredCanvasPayload(input: {
@@ -162,6 +162,7 @@ export declare class PmxCanvas extends EventEmitter {
162
162
  mcp: {
163
163
  tools: string[];
164
164
  resources: string[];
165
+ nodeTypeRouting: Record<string, string>;
165
166
  };
166
167
  };
167
168
  validateSpec(input: {
@@ -60,6 +60,7 @@ export declare function getCanvasAutomationWebViewStatus(): CanvasAutomationWebV
60
60
  export declare function stopCanvasAutomationWebView(): Promise<boolean>;
61
61
  export declare function startCanvasAutomationWebView(url: string, options?: CanvasAutomationWebViewOptions): Promise<CanvasAutomationWebViewStatus>;
62
62
  export declare function evaluateCanvasAutomationWebView(expression: string): Promise<unknown>;
63
+ export declare function wrapCanvasAutomationScript(script: string): string;
63
64
  export declare function resizeCanvasAutomationWebView(width: number, height: number): Promise<CanvasAutomationWebViewStatus>;
64
65
  export declare function screenshotCanvasAutomationWebView(options?: Record<string, unknown>): Promise<Uint8Array>;
65
66
  export interface PrimaryWorkbenchIntent {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmx-canvas",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Spatial canvas workbench for coding agents — infinite 2D canvas with agent-native CLI, MCP integration, nodes, edges, file watching, and snapshots",
5
5
  "type": "module",
6
6
  "main": "./src/server/index.ts",
@@ -22,14 +22,41 @@ relatedness, clusters imply grouping, reading order (top-left to bottom-right) i
22
22
 
23
23
  ## When to Use
24
24
 
25
- - **Investigation boards** lay out files, logs, stack traces, and findings spatially while debugging
26
- - **Architecture diagrams** show system components and their relationships
27
- - **Plans & task tracking** create task nodes with dependencies and color-coded status
28
- - **Status dashboards** display build results, test output, deployment state
29
- - **Context maps** show how code, configs, and data flow connect
30
- - **Code dependency graphs** visualize file imports and module relationships
31
- - **Comparison views** place options side by side for the human to evaluate
32
- - **Any time spatial layout helps** — when a flat list or text wall is not enough
25
+ The canvas is **agnostic about what you do with it**. Reach for it whenever a
26
+ flat conversation, a list, or a single document hides the relationships
27
+ between pieces of information. The total reach of any session is the union
28
+ of pmx-canvas's own node types and whatever your harness already has access
29
+ toMCP servers, MCP apps, shell commands and CLIs, files in the working
30
+ directory, web fetch, anything else in your toolbelt. The canvas does not
31
+ care where the data came from; it cares about getting it on the surface as
32
+ the right node type.
33
+
34
+ The README has a non-exhaustive list of example use cases (idea generation,
35
+ validation, research, analysis, mind mapping, investigation boards,
36
+ architecture diagrams, status dashboards, comparison views, plus whatever
37
+ your toolbelt unlocks). This skill stays focused on the operational
38
+ mechanics. If a flat list or text wall is not enough to hold the
39
+ relationships you're working with, the canvas is the right tool — the rest
40
+ of this document is how to drive it.
41
+
42
+ ### When the connected tool is unfamiliar
43
+
44
+ When the human asks you to use a data source or tool you have not used
45
+ before — an MCP server, an MCP app, a CLI, a script, an arbitrary file
46
+ tree:
47
+
48
+ 1. List the tools / commands available; sample one or two outputs to see
49
+ the actual shape.
50
+ 2. Decide which canvas node type best matches each output:
51
+ - Long-form / narrative results → `markdown`
52
+ - Structured records, tables, dashboards → `json-render` or `graph`
53
+ - Interactive tool surfaces with their own UI → `mcp-app` (open with
54
+ `canvas_open_mcp_app`)
55
+ - Local source files → `file` (live-watched)
56
+ - URLs that need cached fetches → `webpage`
57
+ - Stream of state events → `status` or `ledger`
58
+ 3. Propose the mapping to the human before bulk-creating nodes; let them
59
+ confirm or adjust before you commit a layout.
33
60
 
34
61
  ## Starting the Canvas
35
62
 
@@ -46,11 +73,31 @@ pmx-canvas --no-open # Start without opening browser (for agents)
46
73
  pmx-canvas --port=8080 # Custom port
47
74
  pmx-canvas --demo # Start with sample content
48
75
  pmx-canvas --theme=light # Light theme
76
+ pmx-canvas --version # Print installed version and exit
49
77
  ```
50
78
 
79
+ `--theme` accepts `dark` (default), `light`, or `high-contrast`. Same value can be set via
80
+ the `PMX_CANVAS_THEME` environment variable, or toggled live in the browser toolbar.
81
+
51
82
  Start the canvas once per session, then reuse it. Use `--no-open` when running as an agent — the
52
83
  human can open the browser URL themselves.
53
84
 
85
+ ### Daemon mode (long-running background server)
86
+
87
+ When you need the canvas to outlive the current shell or agent session — e.g. so a follow-up
88
+ agent run can attach to the same state — start it as a daemon:
89
+
90
+ ```bash
91
+ pmx-canvas serve --daemon --no-open --wait-ms=20000 # Detach, wait for /health
92
+ pmx-canvas serve status # Print daemon health + pid state
93
+ pmx-canvas serve stop # Stop the daemon for this port
94
+ ```
95
+
96
+ `serve --daemon` writes a pid file (`.pmx-canvas/daemon-<port>.pid`) and a log file
97
+ (`.pmx-canvas/daemon-<port>.log`); the wait flag blocks until `/health` returns OK so a script
98
+ can rely on the server being responsive when the command returns. `serve stop` reads the pid
99
+ file, sends SIGTERM, and cleans up on exit.
100
+
54
101
  ## Browser Workflows
55
102
 
56
103
  The browser is not just a passive view. Human interactions on the canvas persist back to the
@@ -86,6 +133,7 @@ pmx-canvas node add --type markdown --title "Plan"
86
133
  pmx-canvas node add --type webpage --url https://example.com/docs
87
134
  pmx-canvas node add --type graph --graph-type bar --data '[{"x":"a","y":1}]' --x-key x --y-key y
88
135
  pmx-canvas node add --type graph --graphType bar --data '[{"x":"a","y":1}]' --xKey x --yKey y
136
+ pmx-canvas graph add --graph-type bar --data '[{"x":"a","y":1}]' --x-key x --y-key y
89
137
  pmx-canvas external-app add --kind excalidraw --title "Diagram"
90
138
  pmx-canvas node add --help --type webpage --json
91
139
  pmx-canvas node schema --type json-render --component Table --summary
@@ -109,7 +157,11 @@ pmx-canvas spatial
109
157
 
110
158
  - `node add|list|get|update|remove` — manage nodes
111
159
  - `node schema` — inspect running-server create schemas and canonical examples, with `--summary`, `--field`, and `--component` filters
160
+ - `graph add` — convenience alias for graph nodes; `node add --type graph` remains the canonical form
112
161
  - Graph CLI fields accept both kebab-case flags and camelCase schema names, e.g. `--graph-type`/`--graphType`, `--x-key`/`--xKey`, and `--bar-color`/`--barColor`.
162
+ - Graph CLI height flags are split: use `--node-height`/`--nodeHeight` for the
163
+ canvas frame and `--chart-height` for the chart content. CLI `--height`
164
+ remains a frame-height compatibility alias.
113
165
  - `edge add|list|remove` — manage edges
114
166
  - Search-based edge selectors must be specific enough to resolve exactly one node. Queries like
115
167
  `"DVT O3"` can be ambiguous; prefer the full visible title such as `"DVT O3 — GitOps"`.
@@ -139,6 +191,8 @@ Current caveat:
139
191
  - Generic `pmx-canvas node add --type mcp-app` is intentionally not supported because app nodes
140
192
  need app/session metadata. Use `pmx-canvas web-artifact build` for bundled React artifacts or
141
193
  `pmx-canvas external-app add --kind excalidraw` for the Excalidraw preset.
194
+ - For local `image` nodes on macOS, iCloud/OneDrive cloud-only placeholder files are rejected with
195
+ a download-first hint. Download the image locally before adding it to the canvas.
142
196
 
143
197
  The CLI targets `http://localhost:4313` by default. Override with `PMX_CANVAS_URL` or
144
198
  `PMX_CANVAS_PORT` when the canvas is running elsewhere.
@@ -160,8 +214,8 @@ The CLI targets `http://localhost:4313` by default. Override with `PMX_CANVAS_UR
160
214
  | `json-render` | Native structured UI panel | Dashboards, forms, tables, interactive layouts from json-render specs |
161
215
  | `graph` | Native chart panel | Line, bar, pie, area, scatter, radar, stacked-bar, and composed charts rendered inside the canvas |
162
216
  | `group` | Spatial container/frame | Visually group related nodes together |
163
- | `prompt` | Prompt thread root | Canvas-native prompt entry points for agent conversations |
164
- | `response` | Prompt reply / streamed answer | Agent responses linked to prompt threads |
217
+ | `prompt` | Prompt thread root | Canvas-native prompt entry points for agent conversations. **Internal type — surfaces in `canvas://layout` for thread rendering but is not created via the public `canvas_add_node` API. Don't try to add one directly.** |
218
+ | `response` | Prompt reply / streamed answer | Agent responses linked to prompt threads. **Same internal-only restriction as `prompt`.** |
165
219
 
166
220
  ### Edge Types
167
221
 
@@ -196,8 +250,22 @@ Use color consistently to convey meaning:
196
250
 
197
251
  ### Node Operations
198
252
 
253
+ MCP node-type routing:
254
+
255
+ | Node category | MCP creation tool |
256
+ |---------------|-------------------|
257
+ | Basic nodes (`markdown`, `status`, `context`, `ledger`, `trace`, `file`, `image`, `webpage`) | `canvas_add_node` |
258
+ | `json-render` | `canvas_add_json_render_node` |
259
+ | `graph` | `canvas_add_graph_node` |
260
+ | `web-artifact` | `canvas_build_web_artifact` |
261
+ | `external-app` / tool-backed `mcp-app` | `canvas_open_mcp_app` |
262
+ | `group` | `canvas_create_group` |
263
+
264
+ If a node type is rejected by `canvas_add_node`, call `canvas_describe_schema` and read
265
+ `mcp.nodeTypeRouting`; do not keep retrying the generic tool.
266
+
199
267
  **`canvas_add_node`** — Add a node to the canvas
200
- - `type` (required): node type (see table above)
268
+ - `type` (required): basic node type only; structured/app/group nodes use the routing table above
201
269
  - `title`: short, scannable title
202
270
  - `content`: for most types, this is markdown text. For `file` type, pass the **file path**
203
271
  (e.g., `"src/auth/login.ts"`) — the server auto-loads the file content and watches for changes.
@@ -220,10 +288,30 @@ Use color consistently to convey meaning:
220
288
  **`canvas_get_node`** — Get a single node's full data
221
289
  - `id` (required): node to retrieve
222
290
 
291
+ **`canvas_refresh_webpage_node`** — Re-fetch the URL stored on a `webpage` node
292
+ - `id` (required): webpage node to refresh
293
+ - Optional `url`: replace the stored URL before refreshing (use when the human moved the page)
294
+ - Returns the refreshed node with updated `pageTitle` and cached extracted text
295
+ - Use this when a saved canvas is reopened and the agent needs fresh page content without
296
+ losing the node's identity, position, or pins. Example flow:
297
+
298
+ ```typescript
299
+ // Add the page once
300
+ canvas_add_node({ type: 'webpage', url: 'https://example.com/docs' })
301
+ // → returns { id: 'node-abc' }
302
+
303
+ // …later, after the human reopens the canvas…
304
+ canvas_refresh_webpage_node({ id: 'node-abc' })
305
+ // → re-fetches the URL, updates pageTitle + extracted text, keeps the node ID and position
306
+ ```
307
+
223
308
  **`canvas_add_json_render_node`** — Add a native json-render node
224
309
  - Required: `title`, `spec`
225
310
  - The `spec` must be a complete json-render object with `root`, `elements`, and optional `state`
226
311
  - Use this when you want a structured UI panel rendered directly inside PMX Canvas
312
+ - For shadcn `Badge`, prefer `props.text` with variants `default`, `secondary`, `destructive`, or
313
+ `outline`. Legacy `props.label` and status variants (`success`, `info`, `warning`, `error`,
314
+ `danger`) are normalized for saved-spec compatibility.
227
315
 
228
316
  **`canvas_add_graph_node`** — Add a native graph/chart node
229
317
  - Required: `graphType`, `data`
@@ -235,10 +323,33 @@ Use color consistently to convey meaning:
235
323
  - Use `axisKey` plus `metrics` for radar graphs
236
324
  - Use `series` for stacked-bar graphs
237
325
  - Use `barKey`/`lineKey` plus optional `barColor`/`lineColor` for composed graphs
326
+ - Use `nodeHeight` for the canvas frame height and `height` for chart content height
238
327
  - Uses the native json-render chart catalog under the hood
239
328
 
329
+ **`canvas_build_web_artifact`** — Build and optionally open a bundled web artifact
330
+ - Required: `title`, `appTsx` (source string contents, not a file path)
331
+ - CLI `--app-file` reads a file before calling the same build path; MCP callers must pass the source contents
332
+ - Cold builds commonly take 45-60 seconds; use a long client timeout such as 300000 ms or more
333
+ - Returns both `id` and `nodeId` for the created artifact node when `openInCanvas` is true
334
+
335
+ ID extraction for mixed tool responses:
336
+ - Most add-style tools return a flat `id`; web artifacts return `id` plus `nodeId`; snapshots return `id` plus nested `snapshot.id`.
337
+ - Defensive extractor: `const getId = (r) => r.id ?? r.nodeId ?? r.snapshot?.id;`
338
+
339
+ **`canvas_open_mcp_app`** — Open a tool-backed external MCP app node
340
+ - Required: `toolName`, `transport`
341
+ - `transport` is either `{ type: "stdio", command, args?, cwd?, env? }` or `{ type: "http", url, headers? }`
342
+ - This is lower-level than `pmx-canvas external-app add --kind excalidraw`; use `canvas_add_diagram` for the built-in Excalidraw preset
343
+
344
+ **`canvas_pin_nodes`** — Set, add, or remove pinned context nodes
345
+ - Use `{ nodeIds: [...] }`; the field is `nodeIds`, not `ids`
346
+
347
+ **`canvas_diff`** — Compare current canvas state with a saved snapshot
348
+ - Requires `{ snapshot: "<snapshot-id-or-name>" }`; there is no implicit previous-snapshot default
349
+
240
350
  **`canvas_describe_schema`** — Inspect the running server's create schemas and canonical examples
241
351
  - Use this before generating structured payloads when you need the authoritative current shape
352
+ - Read `mcp.nodeTypeRouting` to choose the right MCP creation tool for each node category
242
353
 
243
354
  **`canvas_validate_spec`** — Validate a json-render spec or graph payload without creating a node
244
355
  - Returns the normalized json-render spec the server would accept
@@ -251,6 +362,7 @@ Use color consistently to convey meaning:
251
362
  `xKey`, `yKey`, `zKey`, `nameKey`, `valueKey`, `axisKey`, `metrics`, `series`, `barKey`,
252
363
  `lineKey`, `aggregate`, `color`, `barColor`, `lineColor`, `height`, `x`, `y`, `width`, and
253
364
  `nodeHeight`.
365
+ - In batch/MCP/HTTP payloads, `height` is chart content height and `nodeHeight` is the canvas frame height.
254
366
 
255
367
  ### Edge Operations
256
368
 
@@ -258,7 +370,7 @@ Use color consistently to convey meaning:
258
370
  - `from`, `to` (required): source and target node IDs
259
371
  - `fromSearch`, `toSearch`: optional search-based selectors when you do not have IDs. Each search
260
372
  query must resolve to exactly one node or the edge creation fails with an ambiguity error.
261
- - `type`: edge type (default: `relation`)
373
+ - `type`: `flow`, `depends-on`, `relation`, or `references` (default: `relation`)
262
374
  - `label`: descriptive relationship label
263
375
  - `style`: `solid`, `dashed`, or `dotted`
264
376
  - `animated`: boolean for visual emphasis
@@ -357,6 +469,7 @@ Current product caveats for grouped comparison boards:
357
469
  **`canvas_redo`** — Redo the last undone mutation
358
470
  **`canvas_snapshot`** — Save a named snapshot to disk
359
471
  - `name` (required): descriptive snapshot name (e.g., "before-refactor")
472
+ - Returns `{ ok, id, snapshot }`; the flat `id` is an alias for `snapshot.id`
360
473
  **`canvas_restore`** — Restore canvas from a saved snapshot
361
474
  - `id`: snapshot to restore
362
475
  **`canvas_diff`** — Compare current canvas against a saved snapshot
@@ -369,12 +482,80 @@ Current product caveats for grouped comparison boards:
369
482
  - **Always call `canvas_snapshot` first** to save a backup before clearing
370
483
  - This is irreversible without a prior snapshot
371
484
 
485
+ ### Browser Automation (WebView)
486
+
487
+ The canvas exposes a headless browser session over MCP for self-inspection and
488
+ automated screenshotting. Use this when you want to (a) verify what the live
489
+ canvas actually looks like after a sequence of mutations, (b) capture an image
490
+ of a freshly-built artifact for the human to review, or (c) drive arbitrary
491
+ JavaScript inside the workbench page.
492
+
493
+ The WebView automation runs on Bun's WebKit-based WebView (macOS) or a headless
494
+ Chromium fallback (Linux). It does **not** open a visible window; it's an
495
+ additional headless renderer attached to the same canvas server, so all five
496
+ tools below operate on the live canvas state.
497
+
498
+ **`canvas_webview_status`** — Inspect the current automation session
499
+ - Returns `{ supported, active, backend, viewportWidth, viewportHeight, url, lastError }`
500
+ - Call before `start` to check whether a session is already alive
501
+
502
+ **`canvas_webview_start`** — Start (or replace) the automation session
503
+ - Optional: `backend` (`webkit` macOS-only, or `chrome`), `width`, `height`
504
+ - The session opens `/workbench` at the canvas URL, waits for the SPA to
505
+ hydrate, and reports back via `canvas_webview_status`
506
+
507
+ **`canvas_webview_stop`** — Tear down the automation session
508
+
509
+ **`canvas_evaluate`** — Run JavaScript inside the workbench page and return the result
510
+ - Required: exactly one of `expression` (single JS expression) or `script` (multi-statement body)
511
+ - `script` is wrapped in an async IIFE, so top-level `await` works inside script bodies
512
+ - Useful for asserting DOM state after a sequence of canvas mutations
513
+ - Example: read the count of rendered `.canvas-node` elements:
514
+
515
+ ```typescript
516
+ canvas_evaluate({ expression: 'document.querySelectorAll(".canvas-node").length' })
517
+ ```
518
+
519
+ Useful workbench selectors:
520
+ - Nodes: `.canvas-node`, `.canvas-node.active`, `.canvas-node.context-pinned`, `.canvas-node.group-node`
521
+ - Node internals: `.node-title`, `.node-titlebar`, `.node-body`, `.node-type-badge`, `.node-controls`
522
+ - Canvas chrome: `.hud-layer`, `.canvas-toolbar`, `.connection-dot`, `.canvas-bootstrap-card`
523
+ - Nodes do not expose stable `data-node-id` attributes. Use `canvas_get_layout`, `canvas_search`, or MCP resource data for exact node IDs.
524
+
525
+ Async script example:
526
+
527
+ ```typescript
528
+ canvas_evaluate({
529
+ script: 'const title = await Promise.resolve(document.title); return title;',
530
+ })
531
+ ```
532
+
533
+ **`canvas_resize`** — Change the WebView viewport
534
+ - Required: `width`, `height`
535
+ - Use before `canvas_screenshot` when the human needs a specific aspect ratio
536
+
537
+ **`canvas_screenshot`** — Capture a PNG of the current workbench
538
+ - Optional: `format` (`png` default), `fullPage` (boolean)
539
+ - Returns both an MCP image payload (renderable inline by capable agents) and
540
+ a path under `.pmx-canvas/screenshots/` so the human can view the file
541
+ - Pair with `canvas_resize` to control the framing
542
+
543
+ Typical flow when you want to show a result:
544
+
545
+ ```typescript
546
+ canvas_webview_start({ width: 1440, height: 900 });
547
+ // …mutations…
548
+ canvas_screenshot({ fullPage: true });
549
+ canvas_webview_stop();
550
+ ```
551
+
372
552
  ### Diagrams (Excalidraw MCP app preset)
373
553
 
374
554
  **`canvas_add_diagram`** — Draw a hand-drawn diagram on the canvas via the hosted
375
555
  [Excalidraw MCP app](https://github.com/excalidraw/excalidraw-mcp)
376
556
  - Required: `elements` — an array of Excalidraw elements (rectangles, ellipses, diamonds, arrows,
377
557
  text). Can also be a JSON-array string.
558
+ - `elements` must be Excalidraw element objects, not Mermaid/DOT/source-text diagrams. Convert source diagrams to Excalidraw elements first or use a markdown/web-artifact node.
378
559
  - Optional: `title`, `x`, `y`, `width`, `height`
379
560
  - The diagram opens inside an `mcp-app` node with fullscreen editing and draw-on animations
380
561
  - CLI equivalent: `pmx-canvas external-app add --kind excalidraw --title "Diagram"`
@@ -432,7 +613,8 @@ what the human has set up and what they're focusing on.
432
613
  | `canvas://summary` | Compact overview: node counts by type, pinned titles |
433
614
  | `canvas://spatial-context` | Proximity clusters, reading order, pinned neighborhoods |
434
615
  | `canvas://history` | Human-readable mutation timeline |
435
- | `canvas://code-graph` | Auto-detected file import dependencies |
616
+ | `canvas://code-graph` | Auto-detected file import dependencies (JS/TS, Python, Go, Rust) |
617
+ | `canvas://skills` | Index of bundled agent skills shipped with the install. Each skill is also addressable as `canvas://skills/<name>` (e.g. `canvas://skills/web-artifacts-builder`) and returns the full SKILL.md. Read this resource first to discover companion workflows the canvas is built to support. |
436
618
 
437
619
  ### Reading Spatial Intent
438
620
 
@@ -493,6 +675,12 @@ All POST/PATCH endpoints accept `Content-Type: application/json`. Default base U
493
675
 
494
676
  ## Workflow Patterns
495
677
 
678
+ These are **operational recipes** — how to sequence canvas calls for a few
679
+ recurring shapes of work. They are not the project's use cases (those live
680
+ in the README and are intentionally non-exhaustive). The patterns here exist
681
+ to make the agent's tool-call sequencing concrete: which MCP tool fires
682
+ when, what to pin, when to read `canvas://pinned-context`, when to snapshot.
683
+
496
684
  ### Responding to Pinned Context
497
685
 
498
686
  When the human pins nodes, they're telling you what matters. This is the most important
package/src/cli/agent.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import { readFileSync, writeFileSync } from 'node:fs';
15
- import { openUrlInExternalBrowser } from '../server/server.js';
15
+ import { openUrlInExternalBrowser, wrapCanvasAutomationScript } from '../server/server.js';
16
16
  import { DEFAULT_EXCALIDRAW_ELEMENTS } from '../server/diagram-presets.js';
17
17
  import {
18
18
  ALL_SEMANTIC_WATCH_EVENT_TYPES,
@@ -229,6 +229,18 @@ function optionalPositiveFiniteFlag(flags: Record<string, string | true>, name:
229
229
  return parsed;
230
230
  }
231
231
 
232
+ function optionalPositiveFiniteFlagWithAliases(
233
+ flags: Record<string, string | true>,
234
+ hint: string,
235
+ ...names: string[]
236
+ ): number | undefined {
237
+ for (const name of names) {
238
+ const parsed = optionalPositiveFiniteFlag(flags, name, hint);
239
+ if (parsed !== undefined) return parsed;
240
+ }
241
+ return undefined;
242
+ }
243
+
232
244
  function isRecord(value: unknown): value is Record<string, unknown> {
233
245
  return !!value && typeof value === 'object' && !Array.isArray(value);
234
246
  }
@@ -590,7 +602,13 @@ async function buildGraphRequestBody(
590
602
  const x = optionalFiniteFlag(flags, 'x', 'Use a finite number, e.g. --x 500');
591
603
  const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 300');
592
604
  const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 760');
593
- const nodeHeight = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 520');
605
+ const nodeHeight = optionalPositiveFiniteFlagWithAliases(
606
+ flags,
607
+ 'Use a positive number, e.g. --node-height 520',
608
+ 'node-height',
609
+ 'nodeHeight',
610
+ 'height',
611
+ );
594
612
  if (chartHeight !== undefined) body.height = chartHeight;
595
613
  if (x !== undefined) body.x = x;
596
614
  if (y !== undefined) body.y = y;
@@ -992,6 +1010,18 @@ cmd('node add', 'Add a node to the canvas', [
992
1010
  output(result);
993
1011
  });
994
1012
 
1013
+ cmd('graph add', 'Add a graph node to the canvas', [
1014
+ 'pmx-canvas graph add --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
1015
+ 'pmx-canvas graph add --graphType composed --data \'[{"day":"Mon","visits":10,"conversion":0.4}]\' --xKey day --barKey visits --lineKey conversion',
1016
+ 'pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
1017
+ ], async (args) => {
1018
+ const { flags } = parseFlags(args);
1019
+ if (flags.help || flags.h) return showCommandHelp('graph add');
1020
+
1021
+ const result = await api('POST', '/api/canvas/graph', await buildGraphRequestBody(flags));
1022
+ output(result);
1023
+ });
1024
+
995
1025
  cmd('node schema', 'Describe server-supported node create schemas and canonical examples', [
996
1026
  'pmx-canvas node schema',
997
1027
  'pmx-canvas node schema --type webpage',
@@ -1826,9 +1856,9 @@ cmd('webview evaluate', 'Evaluate JavaScript in the active Bun.WebView automatio
1826
1856
  'pmx-canvas webview evaluate --file ./probe.js',
1827
1857
  );
1828
1858
  }
1829
- expression = `(() => {\n${script}\n})()`;
1859
+ expression = wrapCanvasAutomationScript(script);
1830
1860
  } else if (typeof flags.script === 'string') {
1831
- expression = `(() => {\n${flags.script}\n})()`;
1861
+ expression = wrapCanvasAutomationScript(flags.script);
1832
1862
  } else {
1833
1863
  expression = requireFlag(
1834
1864
  flags,
@@ -2056,9 +2086,10 @@ function showCommandHelp(name: string): void {
2056
2086
  console.log(' pmx-canvas node add --help --type graph');
2057
2087
  console.log(' pmx-canvas node add --help --type webpage --json');
2058
2088
  }
2059
- if (name === 'node add' || name === 'validate spec') {
2089
+ if (name === 'node add' || name === 'graph add' || name === 'validate spec') {
2060
2090
  console.log('\nGraph flags:');
2061
2091
  console.log(' Graph fields accept kebab-case CLI flags and camelCase schema names, e.g. --graph-type/--graphType and --x-key/--xKey');
2092
+ console.log(' Use --node-height/--nodeHeight for canvas frame height; use --chart-height for chart content height. --height is kept as a frame-height alias for compatibility.');
2062
2093
  }
2063
2094
  if (name === 'node schema') {
2064
2095
  console.log('\nFilters:');
@@ -2112,6 +2143,7 @@ Node commands:
2112
2143
  pmx-canvas node get <id> Get a node by ID
2113
2144
  pmx-canvas node update <id> [opts] Update a node
2114
2145
  pmx-canvas node remove <id> Remove a node
2146
+ pmx-canvas graph add [options] Add a graph node
2115
2147
 
2116
2148
  Edge commands:
2117
2149
  pmx-canvas edge add [options] Add an edge between nodes
@@ -2179,6 +2211,7 @@ Examples:
2179
2211
  pmx-canvas node add --type json-render --title "Dashboard" --spec-file ./dashboard.json
2180
2212
  pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx
2181
2213
  pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value
2214
+ pmx-canvas graph add --graph-type bar --data-file ./metrics.json --x-key label --y-key value
2182
2215
  pmx-canvas node add --help --type webpage
2183
2216
  pmx-canvas node schema --type json-render
2184
2217
  pmx-canvas node schema --type json-render --component Table --summary
package/src/cli/index.ts CHANGED
@@ -31,7 +31,7 @@ if (args.includes('--version') || args.includes('-v')) {
31
31
  const AGENT_COMMANDS = new Set([
32
32
  'node', 'edge', 'search', 'layout', 'status', 'arrange', 'focus',
33
33
  'pin', 'undo', 'redo', 'history', 'snapshot', 'diff', 'group', 'webview', 'open',
34
- 'clear', 'code-graph', 'spatial', 'watch', 'web-artifact', 'external-app', 'batch', 'validate', 'serve',
34
+ 'clear', 'code-graph', 'spatial', 'watch', 'web-artifact', 'external-app', 'graph', 'batch', 'validate', 'serve',
35
35
  ]);
36
36
 
37
37
  const firstArg = args[0] ?? '';
@@ -486,6 +486,7 @@ Server options:
486
486
 
487
487
  Agent CLI (works against running server):
488
488
  node add|list|get|update|remove Manage nodes
489
+ graph add Add graph nodes (alias for node add --type graph)
489
490
  edge add|list|remove Manage edges
490
491
  webview status|start|evaluate|resize|screenshot|stop
491
492
  Manage Bun.WebView automation session
@@ -540,6 +541,7 @@ Examples:
540
541
  pmx-canvas node add --type webpage --url "https://example.com" Add a webpage node
541
542
  pmx-canvas node add --type json-render --title "Dashboard" --spec-file ./dashboard.json
542
543
  pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx
544
+ pmx-canvas graph add --graph-type bar --data-file ./metrics.json --x-key label --y-key value
543
545
  pmx-canvas node list List all nodes
544
546
  pmx-canvas node schema --type json-render Show running-server schema info
545
547
  pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx
@@ -262,6 +262,14 @@ function normalizeButtonVariant(value: unknown): unknown {
262
262
  return value;
263
263
  }
264
264
 
265
+ function normalizeBadgeVariant(value: unknown): unknown {
266
+ if (value === 'success') return 'default';
267
+ if (value === 'info') return 'secondary';
268
+ if (value === 'warning') return 'outline';
269
+ if (value === 'error' || value === 'danger') return 'destructive';
270
+ return value;
271
+ }
272
+
265
273
  function deriveElementName(elementKey: string): string {
266
274
  const normalized = elementKey.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
267
275
  return normalized || 'field';
@@ -325,6 +333,22 @@ function normalizeElementProps(
325
333
  props.text = props.content;
326
334
  }
327
335
 
336
+ if (type === 'Badge') {
337
+ if (!hasString(props.text) && hasString(props.label)) {
338
+ props.text = props.label;
339
+ }
340
+ // Drop the legacy alias once it's been migrated so the persisted spec
341
+ // contains exactly one of `text` or `label` rather than both. Keeps
342
+ // saved canvases tidy and prevents future stricter validators from
343
+ // flagging the old key as unknown.
344
+ if ('label' in props) {
345
+ delete props.label;
346
+ }
347
+ if ('variant' in props) {
348
+ props.variant = normalizeBadgeVariant(props.variant);
349
+ }
350
+ }
351
+
328
352
  if (type === 'Select' || type === 'Radio') {
329
353
  if (!Array.isArray(props.options) && Array.isArray(props.items)) {
330
354
  props.options = normalizeLabelArray(props.items);
package/src/mcp/server.ts CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  type PmxCanvas,
33
33
  } from '../server/index.js';
34
34
  import { serializeNodeForAgentContext } from '../server/agent-context.js';
35
- import { emitPrimaryWorkbenchEvent } from '../server/server.js';
35
+ import { emitPrimaryWorkbenchEvent, wrapCanvasAutomationScript } from '../server/server.js';
36
36
  import { searchNodes, buildSpatialContext, findNeighborhoods } from '../server/spatial-analysis.js';
37
37
  import { mutationHistory, diffLayouts, formatDiff } from '../server/mutation-history.js';
38
38
  import { buildCodeGraphSummary, formatCodeGraph } from '../server/code-graph.js';
@@ -47,6 +47,13 @@ const jsonRenderSpecSchema = z.object({
47
47
  state: z.record(z.string(), z.unknown()).optional(),
48
48
  }).passthrough();
49
49
 
50
+ function structuredSchemaDescription(): string {
51
+ const routing = describeCanvasSchema().mcp.nodeTypeRouting;
52
+ return Object.entries(routing)
53
+ .map(([type, tool]) => `${type}: ${tool}`)
54
+ .join(', ');
55
+ }
56
+
50
57
  function workspaceRoot(): string {
51
58
  return resolve(process.cwd());
52
59
  }
@@ -127,7 +134,7 @@ export async function startMcpServer(): Promise<void> {
127
134
  // ── canvas_add_node ────────────────────────────────────────────
128
135
  server.tool(
129
136
  'canvas_add_node',
130
- 'Add a node to the canvas. Returns the created node with normalized title/content and rendered geometry. Node types: markdown (rich content), status (compact indicator), context, ledger, trace, file (live file viewer set content to a file path), image (set content to an image file path, data URI, or URL), webpage (prefer url for the page URL; content is still accepted), mcp-app. Use canvas_add_json_render_node, canvas_add_graph_node, and canvas_build_web_artifact for structured UI, graph, and artifact nodes.',
137
+ 'Add a basic node to the canvas. Returns the created node with normalized title/content and rendered geometry. Supported here: markdown, status, context, ledger, trace, file, image, webpage, mcp-app, group. Dedicated node tools: json-render -> canvas_add_json_render_node, graph -> canvas_add_graph_node, web-artifact -> canvas_build_web_artifact, external apps -> canvas_open_mcp_app, groups -> canvas_create_group. Call canvas_describe_schema for the full nodeTypeRouting table.',
131
138
  {
132
139
  type: z.enum(['markdown', 'status', 'context', 'ledger', 'trace', 'file', 'image', 'webpage', 'mcp-app', 'group'])
133
140
  .describe('Node type (prefer canvas_create_group for groups)'),
@@ -171,7 +178,7 @@ export async function startMcpServer(): Promise<void> {
171
178
 
172
179
  server.tool(
173
180
  'canvas_open_mcp_app',
174
- 'Connect to an external MCP server that declares a ui:// app resource, call the specified tool, and open the resulting MCP App inside a canvas mcp-app node.',
181
+ 'Connect to an external MCP server that declares a ui:// app resource, call the specified tool, and open the resulting MCP App inside a canvas mcp-app node. This is a full external-MCP transport call, not the CLI kind shortcut; use canvas_add_diagram for the built-in Excalidraw preset.',
175
182
  {
176
183
  toolName: z.string().describe('Tool name on the external MCP server'),
177
184
  serverName: z.string().optional().describe('Optional display name for the external MCP server'),
@@ -261,7 +268,7 @@ export async function startMcpServer(): Promise<void> {
261
268
 
262
269
  server.tool(
263
270
  'canvas_describe_schema',
264
- 'Describe the current server-supported canvas create schemas, json-render component catalog, canonical examples, and related MCP entry points.',
271
+ 'Describe the current server-supported canvas create schemas, json-render component catalog, canonical examples, and related MCP entry points. Includes mcp.nodeTypeRouting, the authoritative map from node type to MCP creation tool.',
265
272
  {},
266
273
  async () => ({
267
274
  content: [{ type: 'text', text: JSON.stringify(describeCanvasSchema(), null, 2) }],
@@ -356,7 +363,7 @@ export async function startMcpServer(): Promise<void> {
356
363
  // ── canvas_build_web_artifact ───────────────────────────────
357
364
  server.tool(
358
365
  'canvas_build_web_artifact',
359
- 'Build a bundled single-file HTML web artifact from React/Tailwind source files using the bundled web-artifacts-builder skill scripts. Optionally opens the generated artifact as an embedded node on the canvas. Read canvas://skills/web-artifacts-builder for the full workflow, stack, and anti-slop design guidelines before calling.',
366
+ 'Build a bundled single-file HTML web artifact from React/Tailwind source files using the bundled web-artifacts-builder skill scripts. MCP callers pass source content in appTsx (the CLI app-file flag reads a file before calling this path). Builds commonly take 45-60s on cold workspaces; use a long client timeout. Optionally opens the generated artifact as an embedded node on the canvas. Read canvas://skills/web-artifacts-builder for the full workflow, stack, and anti-slop design guidelines before calling.',
360
367
  {
361
368
  title: z.string().describe('Artifact title used for default project and output paths'),
362
369
  appTsx: z.string().describe('Contents for src/App.tsx'),
@@ -407,6 +414,9 @@ export async function startMcpServer(): Promise<void> {
407
414
  bytes: result.fileSize,
408
415
  projectPath: result.projectPath,
409
416
  openedInCanvas: result.openedInCanvas,
417
+ // `id` only present when a canvas node was actually created.
418
+ // See the matching block in src/server/server.ts handleCanvasBuildWebArtifact.
419
+ ...(typeof result.nodeId === 'string' ? { id: result.nodeId } : {}),
410
420
  nodeId: result.nodeId,
411
421
  url: result.url,
412
422
  metadata: result.metadata,
@@ -889,7 +899,7 @@ export async function startMcpServer(): Promise<void> {
889
899
  'Evaluate JavaScript in the active Bun.WebView automation session for the workbench page. Use this to inspect rendered browser state. Requires an active automation session started via canvas_webview_start.',
890
900
  {
891
901
  expression: z.string().optional().describe('JavaScript expression to evaluate in the page context'),
892
- script: z.string().optional().describe('Multi-statement JavaScript body. The MCP server wraps it in an IIFE and evaluates the return value.'),
902
+ script: z.string().optional().describe('Multi-statement JavaScript body. The MCP server wraps it in an async IIFE and evaluates the resolved return value.'),
893
903
  },
894
904
  async ({ expression, script }) => {
895
905
  const c = await ensureCanvas();
@@ -900,7 +910,7 @@ export async function startMcpServer(): Promise<void> {
900
910
  };
901
911
  }
902
912
 
903
- const source = script ? `(() => {\n${script}\n})()` : expression!;
913
+ const source = script ? wrapCanvasAutomationScript(script) : expression!;
904
914
  try {
905
915
  const value = await c.evaluateAutomationWebView(source);
906
916
  return {
@@ -1003,7 +1013,7 @@ export async function startMcpServer(): Promise<void> {
1003
1013
  'canvas://schema',
1004
1014
  {
1005
1015
  description:
1006
- 'Machine-readable create schemas, canonical examples, and json-render catalog details from the running PMX Canvas server version.',
1016
+ `Machine-readable create schemas, canonical examples, json-render catalog details, and MCP node-type routing from the running PMX Canvas server version. Routing: ${structuredSchemaDescription()}.`,
1007
1017
  mimeType: 'application/json',
1008
1018
  },
1009
1019
  async () => ({
@@ -1391,7 +1401,7 @@ export async function startMcpServer(): Promise<void> {
1391
1401
  if (!snapshot) {
1392
1402
  return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Failed to save snapshot' }) }] };
1393
1403
  }
1394
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, snapshot }) }] };
1404
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: true, id: snapshot.id, snapshot }) }] };
1395
1405
  },
1396
1406
  );
1397
1407