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.
- package/CHANGELOG.md +131 -0
- package/Readme.md +35 -8
- package/dist/canvas/index.js +70 -70
- package/dist/types/client/nodes/ExtAppFrame.d.ts +13 -1
- package/dist/types/client/state/canvas-store.d.ts +2 -1
- package/dist/types/client/types.d.ts +3 -0
- package/dist/types/server/bundled-skills.d.ts +40 -0
- package/dist/types/server/diagram-presets.d.ts +13 -0
- package/dist/types/server/index.d.ts +6 -1
- package/dist/types/server/web-artifacts.d.ts +1 -0
- package/dist/types/shared/ext-app-tool-result.d.ts +12 -0
- package/package.json +2 -1
- package/skills/pmx-canvas/SKILL.md +26 -5
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +66 -0
- package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +10 -0
- package/skills/web-artifacts-builder/scripts/init-artifact.sh +1 -1
- package/src/cli/agent.ts +78 -7
- package/src/cli/index.ts +22 -2
- package/src/client/App.tsx +2 -1
- package/src/client/canvas/CanvasNode.tsx +3 -2
- package/src/client/canvas/ExpandedNodeOverlay.tsx +6 -1
- package/src/client/nodes/ExtAppFrame.tsx +183 -38
- package/src/client/state/canvas-store.ts +63 -1
- package/src/client/state/sse-bridge.ts +5 -0
- package/src/client/types.ts +12 -0
- package/src/mcp/server.ts +92 -6
- package/src/server/bundled-skills.ts +143 -0
- package/src/server/canvas-operations.ts +57 -8
- package/src/server/canvas-schema.ts +2 -1
- package/src/server/diagram-presets.ts +219 -4
- package/src/server/index.ts +22 -10
- package/src/server/server.ts +172 -45
- package/src/server/web-artifacts/scripts/bundle-artifact.sh +10 -0
- package/src/server/web-artifacts/scripts/init-artifact.sh +1 -1
- package/src/server/web-artifacts.ts +83 -3
- 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/
|
|
29
|
+
}): import("preact/jsx-runtime").JSX.Element;
|
|
18
30
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
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
|
|
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>;
|
|
@@ -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.
|
|
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
|
|
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 |
|
|
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
|
-
|
|
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
|
|
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
|
package/src/client/App.tsx
CHANGED
|
@@ -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;
|