pmx-canvas 0.1.1 → 0.1.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/Readme.md +35 -8
  3. package/dist/canvas/index.js +70 -70
  4. package/dist/types/client/nodes/ExtAppFrame.d.ts +13 -1
  5. package/dist/types/client/state/canvas-store.d.ts +2 -1
  6. package/dist/types/client/types.d.ts +3 -0
  7. package/dist/types/server/bundled-skills.d.ts +40 -0
  8. package/dist/types/server/diagram-presets.d.ts +13 -0
  9. package/dist/types/server/index.d.ts +6 -1
  10. package/dist/types/server/web-artifacts.d.ts +1 -0
  11. package/dist/types/shared/ext-app-tool-result.d.ts +12 -0
  12. package/package.json +2 -1
  13. package/skills/pmx-canvas/SKILL.md +26 -5
  14. package/skills/pmx-canvas/references/installing-pmx-canvas.md +66 -0
  15. package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +10 -0
  16. package/skills/web-artifacts-builder/scripts/init-artifact.sh +1 -1
  17. package/src/cli/agent.ts +78 -7
  18. package/src/cli/index.ts +22 -2
  19. package/src/client/App.tsx +2 -1
  20. package/src/client/canvas/CanvasNode.tsx +3 -2
  21. package/src/client/canvas/ExpandedNodeOverlay.tsx +6 -1
  22. package/src/client/nodes/ExtAppFrame.tsx +183 -38
  23. package/src/client/state/canvas-store.ts +63 -1
  24. package/src/client/state/sse-bridge.ts +5 -0
  25. package/src/client/types.ts +12 -0
  26. package/src/mcp/server.ts +92 -6
  27. package/src/server/bundled-skills.ts +143 -0
  28. package/src/server/canvas-operations.ts +57 -8
  29. package/src/server/canvas-schema.ts +2 -1
  30. package/src/server/diagram-presets.ts +219 -4
  31. package/src/server/index.ts +22 -10
  32. package/src/server/server.ts +172 -45
  33. package/src/server/web-artifacts/scripts/bundle-artifact.sh +10 -0
  34. package/src/server/web-artifacts/scripts/init-artifact.sh +1 -1
  35. package/src/server/web-artifacts.ts +83 -3
  36. package/src/shared/ext-app-tool-result.ts +25 -0
@@ -4,6 +4,9 @@ import type { CanvasNodeState } from '../types';
4
4
  type IframeLoadTarget = Pick<HTMLIFrameElement, 'addEventListener' | 'removeEventListener' | 'contentDocument'>;
5
5
  type ExtAppBridgeNotifications = Pick<AppBridge, 'sendToolInput' | 'sendToolResult'>;
6
6
  type DisplayMode = 'inline' | 'fullscreen' | 'pip';
7
+ interface ExtAppHostDimensionsTarget {
8
+ getBoundingClientRect(): Pick<DOMRectReadOnly, 'width' | 'height'>;
9
+ }
7
10
  export declare function waitForExtAppFrameLoad(target: IframeLoadTarget): Promise<void>;
8
11
  export declare function getExtAppBridgeInitKey(node: CanvasNodeState, retryKey: number): string;
9
12
  export declare function resolveExtAppDisplayModeRequest(requestedMode: DisplayMode, isExpanded: boolean): {
@@ -12,7 +15,16 @@ export declare function resolveExtAppDisplayModeRequest(requestedMode: DisplayMo
12
15
  shouldCollapse: boolean;
13
16
  };
14
17
  export declare function sendExtAppBootstrapState(bridge: ExtAppBridgeNotifications, toolInput: Record<string, unknown>, toolResult: CallToolResult | undefined): Promise<void>;
18
+ export declare function resolveExtAppSandbox(value: unknown): string;
19
+ export declare function resolveExtAppContainerDimensions(target: ExtAppHostDimensionsTarget | null | undefined, fallback: {
20
+ width: number;
21
+ height: number;
22
+ }): {
23
+ width: number;
24
+ height: number;
25
+ };
26
+ export declare function shouldApplyExtAppSizeChange(height: unknown, isExpanded: boolean): height is number;
15
27
  export declare function ExtAppFrame({ node }: {
16
28
  node: CanvasNodeState;
17
- }): import("preact/src").JSX.Element;
29
+ }): import("preact/jsx-runtime").JSX.Element;
18
30
  export {};
@@ -1,4 +1,4 @@
1
- import type { CanvasEdge, CanvasLayout, CanvasNodeState, ConnectionStatus, ViewportState } from '../types';
1
+ import { type CanvasEdge, type CanvasLayout, type CanvasNodeState, type ConnectionStatus, type ViewportState } from '../types';
2
2
  export declare const viewport: import("@preact/signals-core").Signal<ViewportState>;
3
3
  export declare const nodes: import("@preact/signals-core").Signal<Map<string, CanvasNodeState>>;
4
4
  export declare const edges: import("@preact/signals-core").Signal<Map<string, CanvasEdge>>;
@@ -9,6 +9,7 @@ export declare const traceEnabled: import("@preact/signals-core").Signal<boolean
9
9
  export declare const canvasTheme: import("@preact/signals-core").Signal<string>;
10
10
  export declare const hasInitialServerLayout: import("@preact/signals-core").Signal<boolean>;
11
11
  export declare const expandedNodeId: import("@preact/signals-core").Signal<string | null>;
12
+ export declare const pendingExpandedNodeCloseId: import("@preact/signals-core").Signal<string | null>;
12
13
  export declare const pendingConnection: import("@preact/signals-core").Signal<{
13
14
  from: string;
14
15
  } | null>;
@@ -33,6 +33,9 @@ export type ConnectionStatus = 'connecting' | 'connected' | 'disconnected';
33
33
  export declare const TYPE_LABELS: Record<CanvasNodeState['type'], string>;
34
34
  /** Node types that support the full-viewport expand/focus overlay. */
35
35
  export declare const EXPANDABLE_TYPES: Set<"markdown" | "mcp-app" | "webpage" | "json-render" | "graph" | "prompt" | "response" | "status" | "context" | "ledger" | "trace" | "file" | "image" | "group">;
36
+ export declare const EXCALIDRAW_SERVER_NAME = "Excalidraw";
37
+ export declare const EXCALIDRAW_CREATE_VIEW_TOOL = "create_view";
38
+ export declare function isExcalidrawNode(node: CanvasNodeState): boolean;
36
39
  export interface CanvasLayout {
37
40
  viewport: ViewportState;
38
41
  nodes: CanvasNodeState[];
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Bundled-skill discovery for the PMX Canvas MCP server.
3
+ *
4
+ * Skill files ship inside the npm package under `skills/<name>/SKILL.md`
5
+ * but until 0.1.2 they were not discoverable to the agent — an agent
6
+ * calling `canvas_build_web_artifact` had no way to find the companion
7
+ * `skills/web-artifacts-builder/SKILL.md` prompt that documents the
8
+ * workflow, stack choices, and gotchas.
9
+ *
10
+ * This module locates the bundled `skills/` directory relative to the
11
+ * package root (works for both repo-local development and global npm
12
+ * installs), parses the YAML frontmatter of each `SKILL.md` to produce
13
+ * a compact index, and reads individual skill content on demand.
14
+ *
15
+ * Exposed via MCP as:
16
+ * - `canvas://skills` → JSON index
17
+ * - `canvas://skills/<name>` → full markdown content
18
+ */
19
+ export interface BundledSkill {
20
+ name: string;
21
+ description: string;
22
+ uri: string;
23
+ filePath: string;
24
+ }
25
+ /**
26
+ * Resolve the packaged `skills/` directory. Walks parents from this module
27
+ * looking for a sibling `skills/` that contains at least one `<name>/SKILL.md`,
28
+ * so it works whether the code runs from source (`src/server/…`), from a
29
+ * compiled bundle (`dist/…`), or from a global npm install
30
+ * (`/opt/homebrew/lib/node_modules/pmx-canvas/src/server/…`).
31
+ */
32
+ export declare function findBundledSkillsRoot(): string | null;
33
+ /**
34
+ * Enumerate every `<name>/SKILL.md` under the bundled skills root and return
35
+ * a compact index. Hidden directories (dotfolders) and files that don't parse
36
+ * are skipped silently rather than throwing — missing metadata should never
37
+ * break the MCP server's resource listing.
38
+ */
39
+ export declare function listBundledSkills(): BundledSkill[];
40
+ export declare function readBundledSkill(name: string): string | null;
@@ -1,7 +1,11 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
1
2
  import type { ExternalMcpTransportConfig } from './mcp-app-runtime.js';
2
3
  export declare const EXCALIDRAW_MCP_URL = "https://mcp.excalidraw.com/mcp";
3
4
  export declare const EXCALIDRAW_SERVER_NAME = "Excalidraw";
4
5
  export declare const EXCALIDRAW_CREATE_VIEW_TOOL = "create_view";
6
+ export declare const EXCALIDRAW_SAVE_CHECKPOINT_TOOL = "save_checkpoint";
7
+ export declare const EXCALIDRAW_READ_CHECKPOINT_TOOL = "read_checkpoint";
8
+ export declare const DEFAULT_EXCALIDRAW_ELEMENTS: ReadonlyArray<Record<string, unknown>>;
5
9
  export declare const EXCALIDRAW_MCP_TRANSPORT: ExternalMcpTransportConfig;
6
10
  export interface DiagramPresetOpenInput {
7
11
  elements: unknown;
@@ -24,5 +28,14 @@ export interface ExcalidrawOpenMcpAppInput {
24
28
  width?: number;
25
29
  height?: number;
26
30
  }
31
+ export declare function inferExcalidrawCameraUpdate(elements: Array<Record<string, unknown>>): Record<string, unknown> | null;
27
32
  export declare function normalizeExcalidrawElements(elements: unknown): string;
33
+ export declare function normalizeExcalidrawElementsForToolInput(elements: unknown): string;
34
+ export declare function normalizeExcalidrawCheckpointDataForToolInput(data: unknown): string | null;
35
+ export declare function buildExcalidrawRestoreCheckpointToolInput(checkpointId: string, data?: unknown): string;
36
+ export declare function isExcalidrawCreateView(serverName: unknown, toolName: unknown): boolean;
37
+ export declare function buildExcalidrawCheckpointId(seed: string): string;
38
+ export declare function getExcalidrawCheckpointIdFromToolResult(result: unknown): string | null;
39
+ export declare function withExcalidrawCheckpointId(result: CallToolResult, checkpointId: string): CallToolResult;
40
+ export declare function ensureExcalidrawCheckpointId(result: CallToolResult, seed: string, checkpointId?: string | null): CallToolResult;
28
41
  export declare function buildExcalidrawOpenMcpAppInput(input: DiagramPresetOpenInput): ExcalidrawOpenMcpAppInput;
@@ -83,7 +83,12 @@ export declare class PmxCanvas extends EventEmitter {
83
83
  ungroupNodes(groupId: string): boolean;
84
84
  clear(): void;
85
85
  arrange(layout?: 'grid' | 'column' | 'flow'): void;
86
- focusNode(id: string): void;
86
+ focusNode(id: string, options?: {
87
+ noPan?: boolean;
88
+ }): {
89
+ focused: string;
90
+ panned: boolean;
91
+ } | null;
87
92
  getLayout(): CanvasLayout;
88
93
  getNode(id: string): CanvasNodeState | undefined;
89
94
  search(query: string): ReturnType<typeof searchNodes>;
@@ -9,6 +9,7 @@ export interface WebArtifactBuildInput {
9
9
  outputPath?: string;
10
10
  initScriptPath?: string;
11
11
  bundleScriptPath?: string;
12
+ deps?: string[];
12
13
  timeoutMs?: number;
13
14
  }
14
15
  export interface WebArtifactBuildOutput {
@@ -7,3 +7,15 @@ export interface NormalizeExtAppToolResultInput {
7
7
  detailedContent?: string;
8
8
  }
9
9
  export declare function normalizeExtAppToolResult(input: NormalizeExtAppToolResultInput): CallToolResult;
10
+ /**
11
+ * Structural equality between two `CallToolResult` values, used by the host
12
+ * ExtAppFrame to suppress echo-back re-renders when an SSE layout update
13
+ * mints a new object reference for an otherwise-unchanged tool result.
14
+ *
15
+ * JSON-stringify is adequate here: tool results are strictly JSON (no
16
+ * functions, symbols, or cycles), typically small, and on the hot path we
17
+ * only hit this when references already differ. For very large payloads
18
+ * (> ~2MB) an early length check skips the stringify to avoid a user-visible
19
+ * stall — such results are treated as "changed" and forwarded to the widget.
20
+ */
21
+ export declare function extAppToolResultsMatch(a: CallToolResult, b: CallToolResult): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmx-canvas",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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",
@@ -44,6 +44,7 @@
44
44
  "test:coverage": "bun test tests/unit --coverage --coverage-reporter=text --coverage-reporter=lcov --coverage-dir coverage",
45
45
  "test:web-canvas": "PMX_CANVAS_DISABLE_BROWSER_OPEN=1 bun run build && PMX_CANVAS_DISABLE_BROWSER_OPEN=1 bun x playwright test",
46
46
  "test:e2e": "bun run test:web-canvas",
47
+ "test:e2e-cli": "bash scripts/e2e-cli-coverage.sh",
47
48
  "test:web-canvas:headed": "bun run build && bun x playwright test --headed",
48
49
  "test:e2e:headed": "bun run test:web-canvas:headed",
49
50
  "test:all": "bun run test && bun run test:web-canvas",
@@ -10,7 +10,7 @@ description: >
10
10
  working memory — pin nodes to curate context, read spatial arrangement to understand intent.
11
11
  ---
12
12
 
13
- # PMX Canvas Agent Skill
13
+ # PMX Canvas - Agent Skill
14
14
 
15
15
  PMX Canvas is a spatial canvas workbench you control through MCP tools or HTTP API. It renders an
16
16
  infinite 2D canvas in the browser with nodes, edges, groups, pan/zoom, and a minimap. State lives
@@ -33,6 +33,10 @@ relatedness, clusters imply grouping, reading order (top-left to bottom-right) i
33
33
 
34
34
  ## Starting the Canvas
35
35
 
36
+ If this skill is installed before the `pmx-canvas` command exists, install the project first. See
37
+ `references/installing-pmx-canvas.md` for local development, npm/global install, and MCP config
38
+ options.
39
+
36
40
  The canvas auto-starts on first MCP tool call when running in MCP mode (`pmx-canvas --mcp`).
37
41
  For manual start:
38
42
 
@@ -81,6 +85,8 @@ pmx-canvas status # Quick summary
81
85
  pmx-canvas node add --type markdown --title "Plan"
82
86
  pmx-canvas node add --type webpage --url https://example.com/docs
83
87
  pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx
88
+ pmx-canvas node add --type graph --graph-type bar --data '[{"x":"a","y":1}]' --x-key x --y-key y
89
+ pmx-canvas external-app add --kind excalidraw --title "Diagram"
84
90
  pmx-canvas node add --help --type webpage --json
85
91
  pmx-canvas node schema --type json-render --component Table --summary
86
92
  pmx-canvas node list --type file --ids
@@ -88,8 +94,9 @@ pmx-canvas edge add --from node-a --to node-b --type depends-on
88
94
  pmx-canvas search "auth"
89
95
  pmx-canvas open
90
96
  pmx-canvas arrange --layout flow
97
+ pmx-canvas focus <node-id> --no-pan # Select/raise without moving the user's viewport
91
98
  pmx-canvas validate spec --type json-render --spec-file ./dashboard.json --summary
92
- pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx --include-logs
99
+ pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx --deps recharts --include-logs
93
100
  pmx-canvas pin --list
94
101
  pmx-canvas snapshot save --name "before-refactor"
95
102
  pmx-canvas code-graph
@@ -103,7 +110,8 @@ pmx-canvas spatial
103
110
  - `edge add|list|remove` — manage edges
104
111
  - Search-based edge selectors must be specific enough to resolve exactly one node. Queries like
105
112
  `"DVT O3"` can be ambiguous; prefer the full visible title such as `"DVT O3 — GitOps"`.
106
- - `search`, `layout`, `status`, `arrange`, `focus` — inspect and navigate the canvas
113
+ - `search`, `layout`, `status`, `arrange`, `focus` — inspect and navigate the canvas. Prefer
114
+ `focus --no-pan` when you only need to select/raise a node without hijacking the human's camera.
107
115
  - `open` — open the current workbench in the browser
108
116
  - `pin --list|--clear|<ids...>` — manage context pins
109
117
  - `undo`, `redo`, `history` — time travel
@@ -124,6 +132,9 @@ Current caveat:
124
132
  diagram/app flow gives you a session-oriented result first, resolve the final canvas node from
125
133
  live layout or `node list --type mcp-app` before you try to group it, wire edges to it, or
126
134
  revisit it later.
135
+ - Generic `pmx-canvas node add --type mcp-app` is intentionally not supported because app nodes
136
+ need app/session metadata. Use `pmx-canvas web-artifact build` for bundled React artifacts or
137
+ `pmx-canvas external-app add --kind excalidraw` for the Excalidraw preset.
127
138
 
128
139
  The CLI targets `http://localhost:4313` by default. Override with `PMX_CANVAS_URL` or
129
140
  `PMX_CANVAS_PORT` when the canvas is running elsewhere.
@@ -141,7 +152,7 @@ The CLI targets `http://localhost:4313` by default. Override with `PMX_CANVAS_UR
141
152
  | `context` | Context card | Key context the human should see |
142
153
  | `ledger` | Log/ledger viewer | Structured log data, audit trails |
143
154
  | `trace` | Trace/timeline viewer | Execution traces, timelines |
144
- | `mcp-app` | Hosted app/embed frame | Embedded MCP apps or external app content |
155
+ | `mcp-app` | Hosted app/embed frame | Tool-backed MCP apps or external app content; not generic CLI-created notes |
145
156
  | `json-render` | Native structured UI panel | Dashboards, forms, tables, interactive layouts from json-render specs |
146
157
  | `graph` | Native chart panel | Line, bar, pie, area, scatter, radar, stacked-bar, and composed charts rendered inside the canvas |
147
158
  | `group` | Spatial container/frame | Visually group related nodes together |
@@ -267,6 +278,8 @@ Use color consistently to convey meaning:
267
278
  **`canvas_focus_node`** — Pan viewport to center on a specific node
268
279
  - `id` (required): node to focus
269
280
  - Good for drawing the human's attention
281
+ - Avoid focusing every node in a batch. Focus only the final result or use CLI `focus --no-pan`
282
+ when the goal is selection/raising without camera movement.
270
283
 
271
284
  ### Groups
272
285
 
@@ -360,6 +373,9 @@ Current product caveats for grouped comparison boards:
360
373
  text). Can also be a JSON-array string.
361
374
  - Optional: `title`, `x`, `y`, `width`, `height`
362
375
  - The diagram opens inside an `mcp-app` node with fullscreen editing and draw-on animations
376
+ - CLI equivalent: `pmx-canvas external-app add --kind excalidraw --title "Diagram"`
377
+ - Edits made in expanded/fullscreen mode are persisted back into the node model context and replayed
378
+ when the app iframe remounts.
363
379
  - Use this when the human needs a quick sketch, architecture diagram, or flowchart and a
364
380
  geometric `graph` node would feel too rigid
365
381
  - Prefer labeled shapes (`"label": { "text": "..." }` on rectangle/ellipse/diamond) over
@@ -380,9 +396,12 @@ server's `ui://` resource as an iframe node on the canvas
380
396
 
381
397
  **`canvas_build_web_artifact`** — Build a single-file HTML artifact from React/Tailwind source
382
398
  - Required: `title`, `appTsx`
383
- - Optional: `indexCss`, `mainTsx`, `indexHtml`, extra `files`, `projectPath`, `outputPath`, `includeLogs`
399
+ - Optional: `indexCss`, `mainTsx`, `indexHtml`, extra `files`, `projectPath`, `outputPath`, `deps`, `includeLogs`
384
400
  - By default it opens the result on the canvas as an embedded app node
385
401
  - By default it returns compact log summaries; set `includeLogs: true` when you need raw stdout/stderr
402
+ - `recharts` is available in the scaffold. For additional libraries, pass CLI `--deps name,name2`
403
+ or MCP/API `deps: ["name"]` before bundling.
404
+ - Failed or empty CLI bundles print `ok: false`, exit non-zero, and do not create a canvas node.
386
405
  - Use this when the output should be a richer interactive UI than a simple markdown/file/image node
387
406
  - Prefer the dedicated `web-artifacts-builder` skill when you need the full React + shadcn workflow
388
407
  - Use the `playwright-cli` skill when you need to validate the built artifact in a live browser
@@ -451,6 +470,8 @@ All POST/PATCH endpoints accept `Content-Type: application/json`. Default base U
451
470
  | GET | `/api/canvas/schema` | Get running-server create schemas, examples, and json-render catalog metadata |
452
471
  | POST | `/api/canvas/schema/validate` | Validate a json-render spec or graph payload without creating a node |
453
472
  | GET | `/api/canvas/json-render/view?nodeId=...` | View a native json-render or graph node |
473
+ | POST | `/api/canvas/diagram` | Create an Excalidraw external app node |
474
+ | POST | `/api/canvas/mcp-app/open` | Open a tool-backed MCP app node |
454
475
  | POST | `/api/canvas/web-artifact` | Build a bundled web artifact and optionally open it on canvas |
455
476
  | POST | `/api/canvas/group` | Create group |
456
477
  | POST | `/api/canvas/group/add` | Add nodes to group |
@@ -0,0 +1,66 @@
1
+ # Installing PMX Canvas
2
+
3
+ Use this reference when the `pmx-canvas` skill is installed but the `pmx-canvas` command is not available yet.
4
+
5
+ ## From npm
6
+
7
+ ```bash
8
+ npm install -g pmx-canvas
9
+ pmx-canvas serve --daemon --no-open --wait-ms=20000
10
+ pmx-canvas open
11
+ ```
12
+
13
+ ## From a local checkout
14
+
15
+ ```bash
16
+ git clone https://github.com/pskoett/pmx-canvas.git
17
+ cd pmx-canvas
18
+ bun install
19
+ bun run build
20
+ bun run src/cli/index.ts serve --daemon --no-open --wait-ms=20000
21
+ ```
22
+
23
+ For development, run commands through Bun from the checkout:
24
+
25
+ ```bash
26
+ bun run src/cli/index.ts status
27
+ bun run src/cli/index.ts node add --type markdown --title "Hello" --content "# PMX"
28
+ ```
29
+
30
+ ## MCP Config
31
+
32
+ For agents that support MCP, configure PMX Canvas as a stdio MCP server:
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "canvas": {
38
+ "command": "bunx",
39
+ "args": ["pmx-canvas", "--mcp"]
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ If you are using a local checkout instead of the published package, point the command at the CLI entry:
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "canvas": {
51
+ "command": "bun",
52
+ "args": ["run", "/path/to/pmx-canvas/src/cli/index.ts", "--mcp"]
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ ## Verify
59
+
60
+ ```bash
61
+ pmx-canvas --version
62
+ pmx-canvas serve status
63
+ pmx-canvas layout
64
+ ```
65
+
66
+ The CLI defaults to `http://localhost:4313`. Override with `PMX_CANVAS_URL` or `PMX_CANVAS_PORT` if the server runs elsewhere.
@@ -152,10 +152,20 @@ rm -rf dist bundle.html
152
152
  echo "🔨 Building with Parcel..."
153
153
  run_with_filtered_stderr run_local_binary parcel build index.html --dist-dir dist --no-source-maps --log-level error
154
154
 
155
+ if [ ! -s "dist/index.html" ]; then
156
+ echo "❌ Error: Parcel did not produce dist/index.html" >&2
157
+ exit 1
158
+ fi
159
+
155
160
  # Inline everything into single HTML
156
161
  echo "🎯 Inlining all assets into single HTML file..."
157
162
  run_with_filtered_stderr run_local_binary html-inline dist/index.html > bundle.html
158
163
 
164
+ if [ ! -s "bundle.html" ]; then
165
+ echo "❌ Error: Bundled artifact is empty" >&2
166
+ exit 1
167
+ fi
168
+
159
169
  # Get file size
160
170
  FILE_SIZE=$(du -h bundle.html | cut -f1)
161
171
 
@@ -172,7 +172,7 @@ fi
172
172
 
173
173
  echo "📦 Installing Tailwind CSS and dependencies..."
174
174
  run_pnpm_allow_build add -D tailwindcss@3.4.1 postcss @types/node tailwindcss-animate parcel @parcel/config-default parcel-resolver-tspaths html-inline
175
- run_pnpm_quiet add class-variance-authority clsx tailwind-merge lucide-react next-themes
175
+ run_pnpm_quiet add class-variance-authority clsx tailwind-merge lucide-react next-themes recharts
176
176
 
177
177
  echo "⚙️ Creating Tailwind and PostCSS configuration..."
178
178
  cat > .postcssrc.json << 'EOF'
package/src/cli/agent.ts CHANGED
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { readFileSync, writeFileSync } from 'node:fs';
15
15
  import { openUrlInExternalBrowser } from '../server/server.js';
16
+ import { DEFAULT_EXCALIDRAW_ELEMENTS } from '../server/diagram-presets.js';
16
17
  import {
17
18
  ALL_SEMANTIC_WATCH_EVENT_TYPES,
18
19
  formatCompactWatchEvent,
@@ -24,6 +25,7 @@ import {
24
25
  // ── Helpers ──────────────────────────────────────────────────
25
26
 
26
27
  const DEFAULT_PORT = 4313;
28
+ const defaultConsoleLog = console.log;
27
29
 
28
30
  interface CanvasSchemaField {
29
31
  name: string;
@@ -91,13 +93,19 @@ function die(message: string, hint?: string): never {
91
93
  }
92
94
 
93
95
  function output(data: unknown): void {
94
- console.log(JSON.stringify(data, null, 2));
96
+ const text = JSON.stringify(data, null, 2);
97
+ if (console.log !== defaultConsoleLog) {
98
+ console.log(text);
99
+ return;
100
+ }
101
+ process.stdout.write(`${text}\n`);
95
102
  }
96
103
 
97
104
  async function api(
98
105
  method: string,
99
106
  path: string,
100
107
  body?: Record<string, unknown>,
108
+ options?: { allowErrorJson?: boolean },
101
109
  ): Promise<unknown> {
102
110
  const base = getBaseUrl();
103
111
  const url = `${base}${path}`;
@@ -128,6 +136,7 @@ async function api(
128
136
  }
129
137
 
130
138
  if (!res.ok) {
139
+ if (options?.allowErrorJson) return json;
131
140
  const err = json as Record<string, unknown>;
132
141
  die(
133
142
  err.error ? String(err.error) : `HTTP ${res.status}`,
@@ -146,7 +155,7 @@ function parseFlags(args: string[]): { positional: string[]; flags: Record<strin
146
155
  const BOOL_FLAGS = new Set([
147
156
  'help', 'h', 'ids', 'stdin', 'yes', 'list', 'clear', 'set', 'animated', 'dry-run',
148
157
  'no-open-in-canvas', 'lock-arrange', 'unlock-arrange', 'json', 'compact', 'summary',
149
- 'verbose', 'include-logs',
158
+ 'verbose', 'include-logs', 'no-pan',
150
159
  ]);
151
160
  for (let i = 0; i < args.length; i++) {
152
161
  const arg = args[i];
@@ -532,11 +541,11 @@ async function buildGraphRequestBody(
532
541
  'Use: pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value';
533
542
  const rawData = await readTextInput(flags, {
534
543
  fileFlags: ['data-file'],
535
- valueFlags: ['data-json'],
544
+ valueFlags: ['data-json', 'data'],
536
545
  allowStdin: true,
537
546
  label: 'graph JSON dataset',
538
547
  hint,
539
- requiredMessage: 'Graph nodes require --data-file, --data-json, or --stdin JSON data.',
548
+ requiredMessage: 'Graph nodes require --data-file, --data-json, --data, or --stdin JSON data.',
540
549
  });
541
550
  const data = parseRecordArrayJson(rawData, hint);
542
551
 
@@ -621,6 +630,8 @@ async function buildWebArtifactRequestBody(
621
630
  if (typeof flags['output-path'] === 'string') body.outputPath = flags['output-path'];
622
631
  if (typeof flags['init-script-path'] === 'string') body.initScriptPath = flags['init-script-path'];
623
632
  if (typeof flags['bundle-script-path'] === 'string') body.bundleScriptPath = flags['bundle-script-path'];
633
+ const deps = parseStringListFlag(flags, 'deps', 'Use a comma-separated list, e.g. --deps recharts,zod');
634
+ if (deps) body.deps = deps;
624
635
  if (flags['no-open-in-canvas']) body.openInCanvas = false;
625
636
  if (flags.verbose || flags['include-logs']) body.includeLogs = true;
626
637
 
@@ -631,8 +642,11 @@ async function buildWebArtifactRequestBody(
631
642
  }
632
643
 
633
644
  async function runWebArtifactBuildCommand(flags: Record<string, string | true>): Promise<void> {
634
- const result = await api('POST', '/api/canvas/web-artifact', await buildWebArtifactRequestBody(flags));
645
+ const result = await api('POST', '/api/canvas/web-artifact', await buildWebArtifactRequestBody(flags), { allowErrorJson: true });
635
646
  output(result);
647
+ if (isRecord(result) && result.ok === false) {
648
+ process.exit(1);
649
+ }
636
650
  }
637
651
 
638
652
  async function loadCanvasSchema(): Promise<CanvasSchemaResponse> {
@@ -915,6 +929,13 @@ cmd('node add', 'Add a node to the canvas', [
915
929
  return;
916
930
  }
917
931
 
932
+ if (type === 'mcp-app') {
933
+ die(
934
+ 'mcp-app nodes require tool-backed app metadata and cannot be created with generic node add.',
935
+ 'Use: pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx, or pmx-canvas external-app add --kind excalidraw --title "Diagram"',
936
+ );
937
+ }
938
+
918
939
  const body: Record<string, unknown> = { type };
919
940
  if (flags.title) body.title = flags.title;
920
941
  const webpageUrl = getStringFlag(flags, 'url');
@@ -1269,7 +1290,10 @@ cmd('status', 'Quick canvas summary', [
1269
1290
 
1270
1291
  const typeCounts: Record<string, number> = {};
1271
1292
  for (const n of layout.nodes) {
1272
- const t = n.type as string;
1293
+ const data = isRecord(n.data) ? n.data : {};
1294
+ const t = n.type === 'mcp-app' && data.hostMode === 'hosted' && typeof data.path === 'string'
1295
+ ? 'web-artifact'
1296
+ : n.type as string;
1273
1297
  typeCounts[t] = (typeCounts[t] || 0) + 1;
1274
1298
  }
1275
1299
 
@@ -1308,7 +1332,38 @@ cmd('focus', 'Pan viewport to center on a node', [
1308
1332
  const id = positional[0];
1309
1333
  if (!id) die('Missing node ID', 'pmx-canvas focus <node-id>');
1310
1334
 
1311
- const result = await api('POST', '/api/canvas/focus', { id });
1335
+ const result = await api('POST', '/api/canvas/focus', { id, ...(flags['no-pan'] ? { noPan: true } : {}) });
1336
+ output(result);
1337
+ });
1338
+
1339
+ // ── external-app add ─────────────────────────────────────────
1340
+ cmd('external-app add', 'Create a hosted external app node', [
1341
+ 'pmx-canvas external-app add --kind excalidraw --title "Diagram"',
1342
+ ], async (args) => {
1343
+ const { flags } = parseFlags(args);
1344
+ if (flags.help || flags.h) return showCommandHelp('external-app add');
1345
+
1346
+ const kind = typeof flags.kind === 'string' ? flags.kind.trim() : '';
1347
+ if (kind !== 'excalidraw') {
1348
+ die('Unsupported external app kind.', 'Use: pmx-canvas external-app add --kind excalidraw --title "Diagram"');
1349
+ }
1350
+
1351
+ const body: Record<string, unknown> = {
1352
+ title: typeof flags.title === 'string' ? flags.title : 'Excalidraw Diagram',
1353
+ elements: DEFAULT_EXCALIDRAW_ELEMENTS,
1354
+ };
1355
+ const elementsJson = getStringFlag(flags, 'elements-json');
1356
+ if (elementsJson !== undefined) body.elements = parseJsonValue(elementsJson, 'Excalidraw elements', 'Use --elements-json \'[{"type":"rectangle","id":"r1","x":0,"y":0,"width":120,"height":80}]\'');
1357
+ const elementsFile = getStringFlag(flags, 'elements-file', 'initial-file');
1358
+ if (elementsFile) body.elements = parseJsonValue(readFileSync(elementsFile, 'utf-8'), 'Excalidraw elements file', 'Use --elements-file ./scene.excalidraw');
1359
+ applyCommonGeometryFlags(body, flags, {
1360
+ x: 'Use a finite number, e.g. --x 500',
1361
+ y: 'Use a finite number, e.g. --y 300',
1362
+ width: 'Use a positive number, e.g. --width 960',
1363
+ height: 'Use a positive number, e.g. --height 720',
1364
+ });
1365
+
1366
+ const result = await api('POST', '/api/canvas/diagram', body);
1312
1367
  output(result);
1313
1368
  });
1314
1369
 
@@ -1979,10 +2034,24 @@ function showCommandHelp(name: string): void {
1979
2034
  console.log(' --summary Return only validation summary metadata');
1980
2035
  }
1981
2036
  if (name === 'web-artifact build') {
2037
+ console.log('\nDependencies:');
2038
+ console.log(' --deps <list> Add npm dependencies before bundling, e.g. --deps recharts,zod');
1982
2039
  console.log('\nOutput control:');
1983
2040
  console.log(' --include-logs Include raw build stdout/stderr in the response');
1984
2041
  console.log(' --verbose Alias for --include-logs');
1985
2042
  }
2043
+ if (name === 'focus') {
2044
+ console.log('\nViewport:');
2045
+ console.log(' --no-pan Select/raise the node without moving the viewport');
2046
+ }
2047
+ if (name === 'external-app add') {
2048
+ console.log('\nOptions:');
2049
+ console.log(' --kind excalidraw External app kind to create');
2050
+ console.log(' --title <title> Node title');
2051
+ console.log(' --elements-json <json> Optional Excalidraw elements array JSON');
2052
+ console.log(' --elements-file <path> Optional file containing Excalidraw elements JSON');
2053
+ console.log(' --initial-file <path> Alias for --elements-file');
2054
+ }
1986
2055
  console.log('');
1987
2056
  }
1988
2057
 
@@ -2023,6 +2092,7 @@ Canvas commands:
2023
2092
  pmx-canvas validate spec Validate json-render/graph payloads without creating nodes
2024
2093
  pmx-canvas watch [options] Watch semantic canvas changes over SSE
2025
2094
  pmx-canvas focus <id> Pan viewport to node
2095
+ pmx-canvas external-app add Add hosted external apps like Excalidraw
2026
2096
  pmx-canvas webview status Show WebView automation status
2027
2097
  pmx-canvas webview start [options] Start or replace automation session
2028
2098
  pmx-canvas webview evaluate Evaluate JS in automation session
@@ -2090,6 +2160,7 @@ Examples:
2090
2160
  pmx-canvas validate spec --type json-render --spec-file ./dashboard.json --summary
2091
2161
  pmx-canvas history --summary
2092
2162
  pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx
2163
+ pmx-canvas external-app add --kind excalidraw --title "Diagram"
2093
2164
  pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx --include-logs
2094
2165
  pmx-canvas webview evaluate --script "const title = document.title; return title"
2095
2166
  pmx-canvas snapshot save --name "pre-refactor"
package/src/cli/index.ts CHANGED
@@ -1,19 +1,37 @@
1
1
  #!/usr/bin/env bun
2
2
  import { spawn } from 'node:child_process';
3
3
  import { existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
- import { dirname, resolve } from 'node:path';
4
+ import { dirname, join, resolve } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { runAgentCli } from './agent.js';
7
7
  import { createCanvas } from '../server/index.js';
8
8
 
9
9
  const args = process.argv.slice(2);
10
10
 
11
+ // ── --version / -v ─────────────────────────────────────────────
12
+ // Print the installed package version and exit. Resolved from the
13
+ // sibling package.json so it stays accurate through bunx, global npm
14
+ // installs, and repo-local runs (no hard-coded string, no build step
15
+ // required).
16
+ if (args.includes('--version') || args.includes('-v')) {
17
+ try {
18
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
19
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version?: string };
20
+ console.log(pkg.version ?? 'unknown');
21
+ process.exit(0);
22
+ } catch (error) {
23
+ const message = error instanceof Error ? error.message : String(error);
24
+ console.error(`pmx-canvas: failed to read package.json (${message})`);
25
+ process.exit(1);
26
+ }
27
+ }
28
+
11
29
  // ── Agent CLI subcommands ────────────────────────────────────
12
30
  // If first arg is a known subcommand (not a --flag), route to the agent CLI.
13
31
  const AGENT_COMMANDS = new Set([
14
32
  'node', 'edge', 'search', 'layout', 'status', 'arrange', 'focus',
15
33
  'pin', 'undo', 'redo', 'history', 'snapshot', 'diff', 'group', 'webview', 'open',
16
- 'clear', 'code-graph', 'spatial', 'watch', 'web-artifact', 'batch', 'validate', 'serve',
34
+ 'clear', 'code-graph', 'spatial', 'watch', 'web-artifact', 'external-app', 'batch', 'validate', 'serve',
17
35
  ]);
18
36
 
19
37
  const firstArg = args[0] ?? '';
@@ -483,6 +501,7 @@ Agent CLI (works against running server):
483
501
  validate spec Validate json-render/graph payloads without creating nodes
484
502
  watch [--json] [--events ...] Watch low-token semantic canvas changes
485
503
  focus <node-id> Pan to node
504
+ external-app add Add hosted external apps like Excalidraw
486
505
  pin <ids...> | --list | --clear Manage context pins
487
506
  undo / redo / history Time travel
488
507
  snapshot save|list|restore|diff|delete
@@ -524,6 +543,7 @@ Examples:
524
543
  pmx-canvas node list List all nodes
525
544
  pmx-canvas node schema --type json-render Show running-server schema info
526
545
  pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx
546
+ pmx-canvas external-app add --kind excalidraw --title "Diagram"
527
547
  pmx-canvas validate spec --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value
528
548
  pmx-canvas open Open the workbench in a browser
529
549
  pmx-canvas webview status Show WebView automation status
@@ -28,6 +28,7 @@ import {
28
28
  forceDirectedArrange,
29
29
  hasInitialServerLayout,
30
30
  nodes,
31
+ pendingExpandedNodeCloseId,
31
32
  persistLayout,
32
33
  selectedNodeIds,
33
34
  sessionId,
@@ -366,7 +367,7 @@ export function App() {
366
367
  }
367
368
 
368
369
  // Esc always collapses expanded node first (even from inside inputs)
369
- if (e.key === 'Escape' && expandedNodeId.value) {
370
+ if (e.key === 'Escape' && expandedNodeId.value && !pendingExpandedNodeCloseId.value) {
370
371
  e.preventDefault();
371
372
  collapseExpandedNode();
372
373
  return;