pmx-canvas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +21 -0
  3. package/Readme.md +865 -0
  4. package/dist/canvas/global.css +3173 -0
  5. package/dist/canvas/index.js +183 -0
  6. package/dist/json-render/index.css +2 -0
  7. package/dist/json-render/index.js +389 -0
  8. package/dist/types/cli/agent.d.ts +13 -0
  9. package/dist/types/cli/index.d.ts +2 -0
  10. package/dist/types/cli/watch.d.ts +5 -0
  11. package/dist/types/client/App.d.ts +1 -0
  12. package/dist/types/client/canvas/AttentionHistory.d.ts +1 -0
  13. package/dist/types/client/canvas/AttentionToast.d.ts +1 -0
  14. package/dist/types/client/canvas/CanvasNode.d.ts +8 -0
  15. package/dist/types/client/canvas/CanvasViewport.d.ts +8 -0
  16. package/dist/types/client/canvas/CommandPalette.d.ts +4 -0
  17. package/dist/types/client/canvas/ContextMenu.d.ts +24 -0
  18. package/dist/types/client/canvas/ContextPinBar.d.ts +1 -0
  19. package/dist/types/client/canvas/ContextPinHud.d.ts +1 -0
  20. package/dist/types/client/canvas/DockedNode.d.ts +4 -0
  21. package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
  22. package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -0
  23. package/dist/types/client/canvas/FocusFieldLayer.d.ts +1 -0
  24. package/dist/types/client/canvas/Minimap.d.ts +23 -0
  25. package/dist/types/client/canvas/SelectionBar.d.ts +1 -0
  26. package/dist/types/client/canvas/ShortcutOverlay.d.ts +3 -0
  27. package/dist/types/client/canvas/SnapshotPanel.d.ts +7 -0
  28. package/dist/types/client/canvas/snap-guides.d.ts +23 -0
  29. package/dist/types/client/canvas/use-node-drag.d.ts +15 -0
  30. package/dist/types/client/canvas/use-node-resize.d.ts +15 -0
  31. package/dist/types/client/canvas/use-pan-zoom.d.ts +16 -0
  32. package/dist/types/client/ext-app/bridge.d.ts +161 -0
  33. package/dist/types/client/icons.d.ts +70 -0
  34. package/dist/types/client/index.d.ts +1 -0
  35. package/dist/types/client/nodes/ContextNode.d.ts +34 -0
  36. package/dist/types/client/nodes/ExtAppFrame.d.ts +18 -0
  37. package/dist/types/client/nodes/FileNode.d.ts +5 -0
  38. package/dist/types/client/nodes/GroupNode.d.ts +6 -0
  39. package/dist/types/client/nodes/ImageNode.d.ts +10 -0
  40. package/dist/types/client/nodes/InlineFormatBar.d.ts +7 -0
  41. package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +14 -0
  42. package/dist/types/client/nodes/LedgerNode.d.ts +4 -0
  43. package/dist/types/client/nodes/MarkdownNode.d.ts +6 -0
  44. package/dist/types/client/nodes/McpAppNode.d.ts +4 -0
  45. package/dist/types/client/nodes/MdFormatBar.d.ts +8 -0
  46. package/dist/types/client/nodes/PromptNode.d.ts +5 -0
  47. package/dist/types/client/nodes/ResponseNode.d.ts +5 -0
  48. package/dist/types/client/nodes/StatusNode.d.ts +4 -0
  49. package/dist/types/client/nodes/StatusSummary.d.ts +4 -0
  50. package/dist/types/client/nodes/TraceNode.d.ts +4 -0
  51. package/dist/types/client/nodes/WebpageNode.d.ts +5 -0
  52. package/dist/types/client/nodes/image-warnings.d.ts +6 -0
  53. package/dist/types/client/nodes/inline-editor-commands.d.ts +11 -0
  54. package/dist/types/client/nodes/md-format.d.ts +25 -0
  55. package/dist/types/client/state/attention-bridge.d.ts +3 -0
  56. package/dist/types/client/state/attention-store.d.ts +25 -0
  57. package/dist/types/client/state/canvas-store.d.ts +74 -0
  58. package/dist/types/client/state/intent-bridge.d.ts +158 -0
  59. package/dist/types/client/state/sse-bridge.d.ts +5 -0
  60. package/dist/types/client/theme/tokens.d.ts +27 -0
  61. package/dist/types/client/types.d.ts +40 -0
  62. package/dist/types/client/utils/ext-app-tool-result.d.ts +1 -0
  63. package/dist/types/client/utils/placement.d.ts +1 -0
  64. package/dist/types/client/utils/platform.d.ts +2 -0
  65. package/dist/types/json-render/catalog.d.ts +815 -0
  66. package/dist/types/json-render/charts/components.d.ts +54 -0
  67. package/dist/types/json-render/charts/definitions.d.ts +103 -0
  68. package/dist/types/json-render/charts/extra-components.d.ts +58 -0
  69. package/dist/types/json-render/charts/extra-definitions.d.ts +181 -0
  70. package/dist/types/json-render/renderer/index.d.ts +16 -0
  71. package/dist/types/json-render/schema.d.ts +46 -0
  72. package/dist/types/json-render/server.d.ts +55 -0
  73. package/dist/types/mcp/server.d.ts +22 -0
  74. package/dist/types/server/agent-context.d.ts +21 -0
  75. package/dist/types/server/artifact-paths.d.ts +3 -0
  76. package/dist/types/server/canvas-operations.d.ts +154 -0
  77. package/dist/types/server/canvas-provenance.d.ts +13 -0
  78. package/dist/types/server/canvas-schema.d.ts +49 -0
  79. package/dist/types/server/canvas-serialization.d.ts +25 -0
  80. package/dist/types/server/canvas-state.d.ts +174 -0
  81. package/dist/types/server/canvas-validation.d.ts +33 -0
  82. package/dist/types/server/chart-template.d.ts +29 -0
  83. package/dist/types/server/code-graph.d.ts +67 -0
  84. package/dist/types/server/context-cards.d.ts +24 -0
  85. package/dist/types/server/diagram-presets.d.ts +28 -0
  86. package/dist/types/server/ext-app-call-registry.d.ts +16 -0
  87. package/dist/types/server/ext-app-tool-result.d.ts +1 -0
  88. package/dist/types/server/file-watcher.d.ts +16 -0
  89. package/dist/types/server/index.d.ts +243 -0
  90. package/dist/types/server/mcp-app-candidate.d.ts +25 -0
  91. package/dist/types/server/mcp-app-host.d.ts +65 -0
  92. package/dist/types/server/mcp-app-runtime.d.ts +47 -0
  93. package/dist/types/server/mutation-history.d.ts +105 -0
  94. package/dist/types/server/placement.d.ts +37 -0
  95. package/dist/types/server/server.d.ts +103 -0
  96. package/dist/types/server/spatial-analysis.d.ts +87 -0
  97. package/dist/types/server/trace-manager.d.ts +48 -0
  98. package/dist/types/server/web-artifacts.d.ts +50 -0
  99. package/dist/types/server/webpage-node.d.ts +25 -0
  100. package/dist/types/shared/auto-arrange.d.ts +29 -0
  101. package/dist/types/shared/ext-app-tool-result.d.ts +9 -0
  102. package/dist/types/shared/placement.d.ts +26 -0
  103. package/dist/types/shared/semantic-attention.d.ts +97 -0
  104. package/package.json +109 -0
  105. package/skills/data-analysis/SKILL.md +324 -0
  106. package/skills/doc-coauthoring/SKILL.md +375 -0
  107. package/skills/frontend-design/SKILL.md +45 -0
  108. package/skills/json-render-codegen/SKILL.md +112 -0
  109. package/skills/json-render-core/SKILL.md +265 -0
  110. package/skills/json-render-ink/SKILL.md +273 -0
  111. package/skills/json-render-mcp/SKILL.md +132 -0
  112. package/skills/json-render-react/SKILL.md +264 -0
  113. package/skills/json-render-shadcn/SKILL.md +159 -0
  114. package/skills/playwright-cli/SKILL.md +67 -0
  115. package/skills/pmx-canvas/SKILL.md +668 -0
  116. package/skills/pmx-canvas/evals/evals.json +186 -0
  117. package/skills/pmx-canvas-testing/SKILL.md +78 -0
  118. package/skills/published-consumer-e2e/SKILL.md +43 -0
  119. package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +241 -0
  120. package/skills/web-artifacts-builder/SKILL.md +80 -0
  121. package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +167 -0
  122. package/skills/web-artifacts-builder/scripts/init-artifact.sh +425 -0
  123. package/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  124. package/skills/web-design-guidelines/SKILL.md +39 -0
  125. package/src/cli/agent.ts +2144 -0
  126. package/src/cli/index.ts +622 -0
  127. package/src/cli/watch.ts +88 -0
  128. package/src/client/App.tsx +507 -0
  129. package/src/client/canvas/AttentionHistory.tsx +81 -0
  130. package/src/client/canvas/AttentionToast.tsx +19 -0
  131. package/src/client/canvas/CanvasNode.tsx +363 -0
  132. package/src/client/canvas/CanvasViewport.tsx +590 -0
  133. package/src/client/canvas/CommandPalette.tsx +302 -0
  134. package/src/client/canvas/ContextMenu.tsx +601 -0
  135. package/src/client/canvas/ContextPinBar.tsx +25 -0
  136. package/src/client/canvas/ContextPinHud.tsx +22 -0
  137. package/src/client/canvas/DockedNode.tsx +66 -0
  138. package/src/client/canvas/EdgeLayer.tsx +280 -0
  139. package/src/client/canvas/ExpandedNodeOverlay.tsx +260 -0
  140. package/src/client/canvas/FocusFieldLayer.tsx +107 -0
  141. package/src/client/canvas/Minimap.tsx +301 -0
  142. package/src/client/canvas/SelectionBar.tsx +69 -0
  143. package/src/client/canvas/ShortcutOverlay.tsx +69 -0
  144. package/src/client/canvas/SnapshotPanel.tsx +236 -0
  145. package/src/client/canvas/snap-guides.ts +170 -0
  146. package/src/client/canvas/use-node-drag.ts +51 -0
  147. package/src/client/canvas/use-node-resize.ts +59 -0
  148. package/src/client/canvas/use-pan-zoom.ts +191 -0
  149. package/src/client/ext-app/bridge.ts +542 -0
  150. package/src/client/icons.tsx +424 -0
  151. package/src/client/index.tsx +7 -0
  152. package/src/client/nodes/ContextNode.tsx +412 -0
  153. package/src/client/nodes/ExtAppFrame.tsx +509 -0
  154. package/src/client/nodes/FileNode.tsx +256 -0
  155. package/src/client/nodes/GroupNode.tsx +39 -0
  156. package/src/client/nodes/ImageNode.tsx +160 -0
  157. package/src/client/nodes/InlineFormatBar.tsx +169 -0
  158. package/src/client/nodes/InlineMarkdownEditor.tsx +123 -0
  159. package/src/client/nodes/LedgerNode.tsx +37 -0
  160. package/src/client/nodes/MarkdownNode.tsx +359 -0
  161. package/src/client/nodes/McpAppNode.tsx +85 -0
  162. package/src/client/nodes/MdFormatBar.tsx +109 -0
  163. package/src/client/nodes/PromptNode.tsx +597 -0
  164. package/src/client/nodes/ResponseNode.tsx +153 -0
  165. package/src/client/nodes/StatusNode.tsx +84 -0
  166. package/src/client/nodes/StatusSummary.tsx +38 -0
  167. package/src/client/nodes/TraceNode.tsx +120 -0
  168. package/src/client/nodes/WebpageNode.tsx +288 -0
  169. package/src/client/nodes/image-warnings.ts +95 -0
  170. package/src/client/nodes/inline-editor-commands.ts +37 -0
  171. package/src/client/nodes/md-format.ts +206 -0
  172. package/src/client/state/attention-bridge.ts +328 -0
  173. package/src/client/state/attention-store.ts +73 -0
  174. package/src/client/state/canvas-store.ts +631 -0
  175. package/src/client/state/intent-bridge.ts +315 -0
  176. package/src/client/state/sse-bridge.ts +965 -0
  177. package/src/client/theme/global.css +3173 -0
  178. package/src/client/theme/tokens.ts +72 -0
  179. package/src/client/types-shims.d.ts +5 -0
  180. package/src/client/types.ts +81 -0
  181. package/src/client/utils/ext-app-tool-result.ts +4 -0
  182. package/src/client/utils/placement.ts +4 -0
  183. package/src/client/utils/platform.ts +2 -0
  184. package/src/json-render/catalog.ts +256 -0
  185. package/src/json-render/charts/components.tsx +198 -0
  186. package/src/json-render/charts/definitions.ts +81 -0
  187. package/src/json-render/charts/extra-components.tsx +267 -0
  188. package/src/json-render/charts/extra-definitions.ts +145 -0
  189. package/src/json-render/renderer/index.css +174 -0
  190. package/src/json-render/renderer/index.tsx +86 -0
  191. package/src/json-render/schema.ts +62 -0
  192. package/src/json-render/server.ts +597 -0
  193. package/src/mcp/server.ts +1377 -0
  194. package/src/server/agent-context.ts +242 -0
  195. package/src/server/artifact-paths.ts +17 -0
  196. package/src/server/canvas-operations.ts +1279 -0
  197. package/src/server/canvas-provenance.ts +243 -0
  198. package/src/server/canvas-schema.ts +432 -0
  199. package/src/server/canvas-serialization.ts +95 -0
  200. package/src/server/canvas-state.ts +1134 -0
  201. package/src/server/canvas-validation.ts +114 -0
  202. package/src/server/chart-template.ts +449 -0
  203. package/src/server/code-graph.ts +370 -0
  204. package/src/server/context-cards.ts +31 -0
  205. package/src/server/diagram-presets.ts +71 -0
  206. package/src/server/ext-app-call-registry.ts +77 -0
  207. package/src/server/ext-app-tool-result.ts +4 -0
  208. package/src/server/file-watcher.ts +121 -0
  209. package/src/server/index.ts +647 -0
  210. package/src/server/mcp-app-candidate.ts +174 -0
  211. package/src/server/mcp-app-host.ts +814 -0
  212. package/src/server/mcp-app-runtime.ts +459 -0
  213. package/src/server/mutation-history.ts +350 -0
  214. package/src/server/placement.ts +125 -0
  215. package/src/server/server.ts +3846 -0
  216. package/src/server/spatial-analysis.ts +356 -0
  217. package/src/server/trace-manager.ts +333 -0
  218. package/src/server/web-artifacts/scripts/bundle-artifact.sh +167 -0
  219. package/src/server/web-artifacts/scripts/init-artifact.sh +426 -0
  220. package/src/server/web-artifacts/scripts/shadcn-components.tar.gz +0 -0
  221. package/src/server/web-artifacts.ts +442 -0
  222. package/src/server/webpage-node.ts +328 -0
  223. package/src/shared/auto-arrange.ts +439 -0
  224. package/src/shared/ext-app-tool-result.ts +76 -0
  225. package/src/shared/placement.ts +81 -0
  226. package/src/shared/semantic-attention.ts +598 -0
@@ -0,0 +1,622 @@
1
+ #!/usr/bin/env bun
2
+ import { spawn } from 'node:child_process';
3
+ import { existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { runAgentCli } from './agent.js';
7
+ import { createCanvas } from '../server/index.js';
8
+
9
+ const args = process.argv.slice(2);
10
+
11
+ // ── Agent CLI subcommands ────────────────────────────────────
12
+ // If first arg is a known subcommand (not a --flag), route to the agent CLI.
13
+ const AGENT_COMMANDS = new Set([
14
+ 'node', 'edge', 'search', 'layout', 'status', 'arrange', 'focus',
15
+ 'pin', 'undo', 'redo', 'history', 'snapshot', 'diff', 'group', 'webview', 'open',
16
+ 'clear', 'code-graph', 'spatial', 'watch', 'web-artifact', 'batch', 'validate', 'serve',
17
+ ]);
18
+
19
+ const firstArg = args[0] ?? '';
20
+ const cliDir = dirname(fileURLToPath(import.meta.url));
21
+ const mcpServerEntry = resolve(cliDir, '..', 'mcp', 'server.ts');
22
+
23
+ function hasFlag(name: string): boolean {
24
+ return args.includes(`--${name}`);
25
+ }
26
+
27
+ function readOption(name: string): string | undefined {
28
+ const inlinePrefix = `--${name}=`;
29
+ const inline = args.find((arg) => arg.startsWith(inlinePrefix));
30
+ if (inline) return inline.slice(inlinePrefix.length);
31
+
32
+ const index = args.indexOf(`--${name}`);
33
+ if (index !== -1 && index + 1 < args.length && !args[index + 1].startsWith('-')) {
34
+ return args[index + 1];
35
+ }
36
+ return undefined;
37
+ }
38
+
39
+ function readNumberOption(name: string): number | undefined {
40
+ const raw = readOption(name);
41
+ if (!raw) return undefined;
42
+ const value = Number(raw);
43
+ return Number.isFinite(value) ? value : undefined;
44
+ }
45
+
46
+ function readCsvOption(name: string): string[] | undefined {
47
+ const raw = readOption(name);
48
+ if (!raw) return undefined;
49
+ const values = raw.split(',').map((value) => value.trim()).filter((value) => value.length > 0);
50
+ return values.length > 0 ? values : undefined;
51
+ }
52
+
53
+ function stripOption(argv: string[], name: string): string[] {
54
+ const stripped: string[] = [];
55
+ for (let index = 0; index < argv.length; index++) {
56
+ const arg = argv[index];
57
+ if (arg === `--${name}`) {
58
+ if (index + 1 < argv.length && !argv[index + 1].startsWith('-')) {
59
+ index++;
60
+ }
61
+ continue;
62
+ }
63
+ if (arg.startsWith(`--${name}=`)) continue;
64
+ stripped.push(arg);
65
+ }
66
+ return stripped;
67
+ }
68
+
69
+ function outputJson(data: unknown): void {
70
+ console.log(JSON.stringify(data, null, 2));
71
+ }
72
+
73
+ function readPidFile(path: string): number | null {
74
+ try {
75
+ if (!existsSync(path)) return null;
76
+ const raw = readFileSync(path, 'utf-8').trim();
77
+ if (!raw) return null;
78
+ const pid = Number(raw);
79
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ function isProcessRunning(pid: number): boolean {
86
+ try {
87
+ process.kill(pid, 0);
88
+ return true;
89
+ } catch (error) {
90
+ if (error && typeof error === 'object' && 'code' in error) {
91
+ return (error as NodeJS.ErrnoException).code === 'EPERM';
92
+ }
93
+ return false;
94
+ }
95
+ }
96
+
97
+ function removePidFile(path: string): void {
98
+ try {
99
+ rmSync(path, { force: true });
100
+ } catch {
101
+ // Ignore cleanup failures for stale pid files.
102
+ }
103
+ }
104
+
105
+ async function isHealthy(url: string): Promise<boolean> {
106
+ try {
107
+ const response = await fetch(url);
108
+ return response.ok;
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+
114
+ function readLogTail(path: string, maxLines = 20): string | null {
115
+ try {
116
+ if (!existsSync(path)) return null;
117
+ const lines = readFileSync(path, 'utf-8').trim().split('\n');
118
+ return lines.slice(-maxLines).join('\n') || null;
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ async function waitForHealth(
125
+ healthUrl: string,
126
+ timeoutMs: number,
127
+ getExitMessage: () => string | null,
128
+ ): Promise<{ ok: true } | { ok: false; reason: string }> {
129
+ const deadline = Date.now() + timeoutMs;
130
+ while (Date.now() < deadline) {
131
+ if (await isHealthy(healthUrl)) {
132
+ return { ok: true };
133
+ }
134
+ const exitMessage = getExitMessage();
135
+ if (exitMessage) {
136
+ return { ok: false, reason: exitMessage };
137
+ }
138
+ await Bun.sleep(250);
139
+ }
140
+ return { ok: false, reason: `Timed out waiting for ${healthUrl}` };
141
+ }
142
+
143
+ async function waitForShutdown(
144
+ healthUrl: string,
145
+ timeoutMs: number,
146
+ pid: number | null,
147
+ ): Promise<boolean> {
148
+ const deadline = Date.now() + timeoutMs;
149
+ while (Date.now() < deadline) {
150
+ const responsive = await isHealthy(healthUrl);
151
+ const alive = pid ? isProcessRunning(pid) : false;
152
+ if (!responsive && !alive) {
153
+ return true;
154
+ }
155
+ await Bun.sleep(250);
156
+ }
157
+ return false;
158
+ }
159
+
160
+ async function startDaemonMode(options: {
161
+ port: number;
162
+ baseArgs: string[];
163
+ logFile: string;
164
+ pidFile: string;
165
+ waitMs: number;
166
+ }): Promise<void> {
167
+ const healthUrl = `http://localhost:${options.port}/health`;
168
+ const workbenchUrl = `http://localhost:${options.port}/workbench`;
169
+ const existingPid = readPidFile(options.pidFile);
170
+
171
+ if (await isHealthy(healthUrl)) {
172
+ outputJson({
173
+ ok: true,
174
+ daemon: true,
175
+ alreadyRunning: true,
176
+ pid: existingPid,
177
+ url: workbenchUrl,
178
+ healthUrl,
179
+ logFile: options.logFile,
180
+ pidFile: options.pidFile,
181
+ });
182
+ process.exit(0);
183
+ }
184
+
185
+ mkdirSync(dirname(options.logFile), { recursive: true });
186
+ const logFd = openSync(options.logFile, 'a');
187
+ const childArgs = options.baseArgs.includes('--no-open')
188
+ ? options.baseArgs
189
+ : [...options.baseArgs, '--no-open'];
190
+ const child = spawn(process.execPath, ['run', fileURLToPath(import.meta.url), ...childArgs], {
191
+ cwd: process.cwd(),
192
+ detached: true,
193
+ env: process.env,
194
+ stdio: ['ignore', logFd, logFd],
195
+ });
196
+
197
+ let exitMessage: string | null = null;
198
+ child.once('exit', (code, signal) => {
199
+ exitMessage = signal
200
+ ? `Daemon exited via signal ${signal}`
201
+ : `Daemon exited with code ${code ?? 'unknown'}`;
202
+ });
203
+ child.unref();
204
+
205
+ const health = await waitForHealth(healthUrl, options.waitMs, () => exitMessage);
206
+ if (!health.ok) {
207
+ const logTail = readLogTail(options.logFile);
208
+ const details = logTail ? `${health.reason}\n\nRecent log output:\n${logTail}` : health.reason;
209
+ console.error(details);
210
+ process.exit(1);
211
+ }
212
+
213
+ mkdirSync(dirname(options.pidFile), { recursive: true });
214
+ writeFileSync(options.pidFile, `${child.pid}\n`, 'utf-8');
215
+
216
+ outputJson({
217
+ ok: true,
218
+ daemon: true,
219
+ pid: child.pid,
220
+ url: workbenchUrl,
221
+ healthUrl,
222
+ logFile: options.logFile,
223
+ pidFile: options.pidFile,
224
+ });
225
+ process.exit(0);
226
+ }
227
+
228
+ async function showServeStatus(options: {
229
+ port: number;
230
+ logFile: string;
231
+ pidFile: string;
232
+ }): Promise<void> {
233
+ const healthUrl = `http://localhost:${options.port}/health`;
234
+ const url = `http://localhost:${options.port}/workbench`;
235
+ const pid = readPidFile(options.pidFile);
236
+ const pidRunning = pid ? isProcessRunning(pid) : false;
237
+ const responsive = await isHealthy(healthUrl);
238
+ const running = responsive || pidRunning;
239
+ if (!running && existsSync(options.pidFile) && !pidRunning) {
240
+ removePidFile(options.pidFile);
241
+ }
242
+
243
+ outputJson({
244
+ ok: true,
245
+ daemon: true,
246
+ running,
247
+ responsive,
248
+ pid,
249
+ pidRunning,
250
+ url,
251
+ healthUrl,
252
+ logFile: options.logFile,
253
+ pidFile: options.pidFile,
254
+ pidFileExists: existsSync(options.pidFile),
255
+ });
256
+ process.exit(0);
257
+ }
258
+
259
+ async function stopServeDaemon(options: {
260
+ port: number;
261
+ logFile: string;
262
+ pidFile: string;
263
+ waitMs: number;
264
+ }): Promise<void> {
265
+ const healthUrl = `http://localhost:${options.port}/health`;
266
+ const url = `http://localhost:${options.port}/workbench`;
267
+ const pid = readPidFile(options.pidFile);
268
+ const responsive = await isHealthy(healthUrl);
269
+
270
+ if (!pid) {
271
+ if (!responsive) {
272
+ removePidFile(options.pidFile);
273
+ outputJson({
274
+ ok: true,
275
+ daemon: true,
276
+ stopped: false,
277
+ running: false,
278
+ reason: 'No running daemon found.',
279
+ url,
280
+ healthUrl,
281
+ logFile: options.logFile,
282
+ pidFile: options.pidFile,
283
+ });
284
+ process.exit(0);
285
+ }
286
+
287
+ outputJson({
288
+ ok: false,
289
+ daemon: true,
290
+ error: `Server on port ${options.port} is responsive, but no pid file was found at ${options.pidFile}.`,
291
+ hint: 'Restart with `pmx-canvas serve --daemon` or provide the correct --pid-file.',
292
+ url,
293
+ healthUrl,
294
+ logFile: options.logFile,
295
+ pidFile: options.pidFile,
296
+ });
297
+ process.exit(1);
298
+ }
299
+
300
+ if (!isProcessRunning(pid)) {
301
+ removePidFile(options.pidFile);
302
+ outputJson({
303
+ ok: true,
304
+ daemon: true,
305
+ stopped: false,
306
+ running: responsive,
307
+ reason: `Removed stale pid file for ${pid}.`,
308
+ pid,
309
+ url,
310
+ healthUrl,
311
+ logFile: options.logFile,
312
+ pidFile: options.pidFile,
313
+ });
314
+ process.exit(0);
315
+ }
316
+
317
+ process.kill(pid, 'SIGTERM');
318
+ const stopped = await waitForShutdown(healthUrl, options.waitMs, pid);
319
+ const stillResponsive = await isHealthy(healthUrl);
320
+ const pidRunning = isProcessRunning(pid);
321
+ if (stopped || (!stillResponsive && !pidRunning)) {
322
+ removePidFile(options.pidFile);
323
+ outputJson({
324
+ ok: true,
325
+ daemon: true,
326
+ stopped: true,
327
+ pid,
328
+ url,
329
+ healthUrl,
330
+ logFile: options.logFile,
331
+ pidFile: options.pidFile,
332
+ });
333
+ process.exit(0);
334
+ }
335
+
336
+ outputJson({
337
+ ok: false,
338
+ daemon: true,
339
+ stopped: false,
340
+ error: `Timed out waiting for daemon ${pid} to stop.`,
341
+ pid,
342
+ responsive: stillResponsive,
343
+ pidRunning,
344
+ url,
345
+ healthUrl,
346
+ logFile: options.logFile,
347
+ pidFile: options.pidFile,
348
+ });
349
+ process.exit(1);
350
+ }
351
+
352
+ function runMcpServerProcess(): Promise<void> {
353
+ return new Promise((resolvePromise, rejectPromise) => {
354
+ const child = spawn(process.execPath, ['run', mcpServerEntry], {
355
+ stdio: 'inherit',
356
+ env: process.env,
357
+ });
358
+ child.on('error', rejectPromise);
359
+ child.on('exit', (code, signal) => {
360
+ if (code === 0) {
361
+ resolvePromise();
362
+ return;
363
+ }
364
+ rejectPromise(new Error(
365
+ signal
366
+ ? `MCP server exited via signal ${signal}`
367
+ : `MCP server exited with code ${code ?? 'unknown'}`,
368
+ ));
369
+ });
370
+ });
371
+ }
372
+
373
+ const serveSubcommand = firstArg === 'serve' ? args[1] ?? '' : '';
374
+
375
+ if (firstArg === 'serve' && (serveSubcommand === 'status' || serveSubcommand === 'stop')) {
376
+ const port = parseInt(readOption('port') ?? process.env.PMX_WEB_CANVAS_PORT ?? '4313');
377
+ const daemonLogFile = resolve(readOption('log-file') ?? `.pmx-canvas/daemon-${port}.log`);
378
+ const daemonPidFile = resolve(readOption('pid-file') ?? `.pmx-canvas/daemon-${port}.pid`);
379
+ const daemonWaitMs = readNumberOption('wait-ms') ?? 10_000;
380
+
381
+ if (hasFlag('help') || args.includes('-h')) {
382
+ console.log(`
383
+ pmx-canvas serve ${serveSubcommand}
384
+
385
+ Usage:
386
+ pmx-canvas serve ${serveSubcommand} [--port=PORT] [--pid-file=PATH] [--log-file=PATH]${serveSubcommand === 'stop' ? ' [--wait-ms=MS]' : ''}
387
+ `);
388
+ process.exit(0);
389
+ }
390
+
391
+ if (serveSubcommand === 'status') {
392
+ await showServeStatus({
393
+ port,
394
+ logFile: daemonLogFile,
395
+ pidFile: daemonPidFile,
396
+ });
397
+ } else {
398
+ await stopServeDaemon({
399
+ port,
400
+ logFile: daemonLogFile,
401
+ pidFile: daemonPidFile,
402
+ waitMs: daemonWaitMs,
403
+ });
404
+ }
405
+ }
406
+
407
+ if (firstArg === 'serve') {
408
+ args.shift();
409
+ }
410
+
411
+ if (AGENT_COMMANDS.has(firstArg) && firstArg !== 'serve') {
412
+ await runAgentCli(args);
413
+ } else if (args.includes('--mcp')) {
414
+ // MCP server mode: stdio transport, auto-starts canvas on first tool call
415
+ await runMcpServerProcess();
416
+ } else {
417
+ // "serve" is also accessible via flags (backward compat)
418
+ const port = parseInt(readOption('port') ?? process.env.PMX_WEB_CANVAS_PORT ?? '4313');
419
+ const demo = hasFlag('demo');
420
+ const noOpen = hasFlag('no-open');
421
+ const daemon = hasFlag('daemon');
422
+ const themeArg = readOption('theme');
423
+ const webviewAutomation = hasFlag('webview-automation');
424
+ const webviewBackend = readOption('webview-backend');
425
+ const webviewChromePath = readOption('webview-chrome-path');
426
+ const webviewChromeArgv = readCsvOption('webview-chrome-argv');
427
+ const webviewDataDir = readOption('webview-data-dir');
428
+ const webviewWidth = readNumberOption('webview-width');
429
+ const webviewHeight = readNumberOption('webview-height');
430
+ const daemonLogFile = resolve(readOption('log-file') ?? `.pmx-canvas/daemon-${port}.log`);
431
+ const daemonPidFile = resolve(readOption('pid-file') ?? `.pmx-canvas/daemon-${port}.pid`);
432
+ const daemonWaitMs = readNumberOption('wait-ms') ?? 10_000;
433
+ const webviewBackendOption: 'chrome' | 'webkit' | undefined =
434
+ webviewBackend === 'chrome' || webviewBackend === 'webkit'
435
+ ? webviewBackend
436
+ : undefined;
437
+ if (themeArg && ['dark', 'light', 'high-contrast'].includes(themeArg)) {
438
+ process.env.PMX_CANVAS_THEME = themeArg;
439
+ }
440
+ const help = hasFlag('help') || args.includes('-h');
441
+
442
+ if (help) {
443
+ console.log(`
444
+ pmx-canvas — Spatial canvas workbench for coding agents
445
+
446
+ Usage:
447
+ pmx-canvas [server-options] Start the canvas server
448
+ pmx-canvas <command> [options] Run agent CLI commands
449
+
450
+ Server options:
451
+ --port=PORT Server port (default: 4313)
452
+ --demo Start with sample nodes
453
+ --no-open Don't open browser automatically
454
+ --daemon Start in detached background mode and wait for health
455
+ --log-file=PATH Daemon log file (default: ./.pmx-canvas/daemon-${port}.log)
456
+ --pid-file=PATH Optional daemon PID file (default: ./.pmx-canvas/daemon-${port}.pid)
457
+ --wait-ms=MS Health-check wait budget for daemon mode (default: 10000)
458
+ --theme=THEME Theme: dark (default), light, high-contrast
459
+ --webview-automation Start a headless Bun.WebView automation session for /workbench
460
+ --webview-backend=BACKEND Bun.WebView backend: chrome or webkit
461
+ --webview-width=PX Automation WebView width (default: 1280)
462
+ --webview-height=PX Automation WebView height (default: 800)
463
+ --webview-chrome-path=PATH Chrome/Chromium executable for Bun.WebView
464
+ --webview-chrome-argv=CSV Extra Chrome args for Bun.WebView, comma-separated
465
+ --webview-data-dir=PATH Persist automation browser storage in PATH
466
+ --mcp Run as MCP server (stdio transport)
467
+ --help, -h Show this help
468
+
469
+ Agent CLI (works against running server):
470
+ node add|list|get|update|remove Manage nodes
471
+ edge add|list|remove Manage edges
472
+ webview status|start|evaluate|resize|screenshot|stop
473
+ Manage Bun.WebView automation session
474
+ search <query> Search nodes
475
+ open Open the current workbench in a browser
476
+ layout Full canvas state
477
+ status Quick summary
478
+ serve status Show daemon status for a given port/pid file
479
+ serve stop Stop a daemon started with serve --daemon
480
+ arrange [--layout grid|column|flow] Auto-arrange nodes
481
+ batch --file ./ops.json Run a JSON batch of operations
482
+ validate Check layout collisions and containment
483
+ validate spec Validate json-render/graph payloads without creating nodes
484
+ watch [--json] [--events ...] Watch low-token semantic canvas changes
485
+ focus <node-id> Pan to node
486
+ pin <ids...> | --list | --clear Manage context pins
487
+ undo / redo / history Time travel
488
+ snapshot save|list|restore|diff|delete
489
+ Manage snapshots
490
+ group create|add|remove Manage groups
491
+ web-artifact build Build bundled web artifacts
492
+ clear --yes Clear canvas
493
+ code-graph File dependencies
494
+ spatial Spatial analysis
495
+ watch Semantic watch stream
496
+
497
+ Run any command with --help for details and examples:
498
+ pmx-canvas node add --help
499
+ pmx-canvas edge --help
500
+
501
+ MCP Integration:
502
+ Add to your agent's MCP config:
503
+ {
504
+ "mcpServers": {
505
+ "canvas": {
506
+ "command": "bunx",
507
+ "args": ["pmx-canvas", "--mcp"]
508
+ }
509
+ }
510
+ }
511
+
512
+ Examples:
513
+ pmx-canvas Start server + browser
514
+ pmx-canvas --no-open --demo Start server headless with sample data
515
+ pmx-canvas serve --daemon --no-open Start a reliable background daemon
516
+ pmx-canvas serve status Show daemon health and pid status
517
+ pmx-canvas serve stop Stop the default daemon for this port
518
+ pmx-canvas --no-open --webview-automation Start server + headless Bun.WebView automation
519
+ pmx-canvas --webview-automation --webview-backend=chrome Start browser + Chrome-backed automation
520
+ pmx-canvas node add --type markdown --title "Hello World" Add a node
521
+ pmx-canvas node add --type webpage --url "https://example.com" Add a webpage node
522
+ pmx-canvas node add --type json-render --title "Dashboard" --spec-file ./dashboard.json
523
+ pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx
524
+ pmx-canvas node list List all nodes
525
+ pmx-canvas node schema --type json-render Show running-server schema info
526
+ pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx
527
+ pmx-canvas validate spec --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value
528
+ pmx-canvas open Open the workbench in a browser
529
+ pmx-canvas webview status Show WebView automation status
530
+ pmx-canvas webview screenshot --output ./canvas.png Save a WebView screenshot
531
+ pmx-canvas search "auth" Find nodes
532
+ pmx-canvas arrange --layout column Auto-arrange
533
+ pmx-canvas batch --file ./canvas-ops.json Run batch canvas ops
534
+ pmx-canvas validate Check layout collisions
535
+ pmx-canvas watch --events context-pin,move-end Watch semantic deltas
536
+ pmx-canvas clear --dry-run Preview destructive op
537
+ `);
538
+ process.exit(0);
539
+ }
540
+
541
+ if (daemon) {
542
+ const baseArgs = stripOption(stripOption(stripOption(stripOption(args, 'daemon'), 'log-file'), 'pid-file'), 'wait-ms');
543
+ await startDaemonMode({
544
+ port,
545
+ baseArgs,
546
+ logFile: daemonLogFile,
547
+ pidFile: daemonPidFile,
548
+ waitMs: daemonWaitMs,
549
+ });
550
+ }
551
+
552
+ const canvas = createCanvas({ port });
553
+ const automationWebView =
554
+ webviewAutomation
555
+ ? {
556
+ ...(webviewBackendOption ? { backend: webviewBackendOption } : {}),
557
+ ...(webviewChromePath ? { chromePath: webviewChromePath } : {}),
558
+ ...(webviewChromeArgv ? { chromeArgv: webviewChromeArgv } : {}),
559
+ ...(webviewDataDir ? { dataStoreDir: webviewDataDir } : {}),
560
+ ...(webviewWidth !== undefined ? { width: webviewWidth } : {}),
561
+ ...(webviewHeight !== undefined ? { height: webviewHeight } : {}),
562
+ }
563
+ : false;
564
+ try {
565
+ await canvas.start({ open: !noOpen, automationWebView });
566
+ } catch (error) {
567
+ const message = error instanceof Error ? error.message : String(error);
568
+ console.error(`Failed to start PMX Canvas: ${message}`);
569
+ process.exit(1);
570
+ }
571
+
572
+ if (demo && canvas.getLayout().nodes.length === 0) {
573
+ const n1 = canvas.addNode({
574
+ type: 'markdown',
575
+ title: 'Welcome to PMX Canvas',
576
+ content: '# PMX Canvas Workbench\n\nA spatial canvas for coding agents.\n\n## Features\n- Infinite 2D canvas with pan/zoom\n- Multiple node types\n- Edges between nodes\n- Real-time SSE updates\n- HTTP API for agent control',
577
+ });
578
+
579
+ const n2 = canvas.addNode({
580
+ type: 'markdown',
581
+ title: 'Getting Started',
582
+ content: `# Quick Start\n\n\`\`\`bash\n# Add a node via CLI\npmx-canvas node add --type markdown --title "Hello" --content "# World"\n\n# List nodes\npmx-canvas node list\n\n# Get canvas state\npmx-canvas layout\n\`\`\``,
583
+ });
584
+
585
+ const n3 = canvas.addNode({
586
+ type: 'status',
587
+ title: 'Agent Status',
588
+ content: 'Ready',
589
+ });
590
+
591
+ canvas.addEdge({ from: n1, to: n2, type: 'flow', label: 'next' });
592
+ canvas.addEdge({ from: n2, to: n3, type: 'flow' });
593
+ canvas.arrange('grid');
594
+ }
595
+
596
+ console.log(`\n PMX Canvas running at http://localhost:${canvas.port}`);
597
+ console.log(` Health: http://localhost:${canvas.port}/health\n`);
598
+ if (webviewAutomation) {
599
+ const webviewStatus = canvas.getAutomationWebViewStatus();
600
+ console.log(` Bun.WebView automation: ${webviewStatus.active ? 'active' : 'inactive'}`);
601
+ if (webviewStatus.lastError) {
602
+ console.log(` Last WebView error: ${webviewStatus.lastError}`);
603
+ }
604
+ }
605
+ console.log(' Agent CLI:');
606
+ console.log(' pmx-canvas node add --type markdown --title "Hello"');
607
+ console.log(' pmx-canvas node list');
608
+ console.log(' pmx-canvas search "query"');
609
+ console.log(' pmx-canvas --help (all commands)');
610
+ console.log('\n Press Ctrl+C to stop\n');
611
+
612
+ process.on('SIGINT', () => {
613
+ console.log('\nShutting down...');
614
+ canvas.stop();
615
+ process.exit(0);
616
+ });
617
+
618
+ process.on('SIGTERM', () => {
619
+ canvas.stop();
620
+ process.exit(0);
621
+ });
622
+ }
@@ -0,0 +1,88 @@
1
+ import {
2
+ ALL_SEMANTIC_WATCH_EVENT_TYPES,
3
+ formatCompactWatchEvent,
4
+ SemanticWatchReducer,
5
+ type SemanticWatchEvent,
6
+ type SemanticWatchEventType,
7
+ type SseMessage,
8
+ } from '../shared/semantic-attention.js';
9
+
10
+ export {
11
+ ALL_SEMANTIC_WATCH_EVENT_TYPES,
12
+ formatCompactWatchEvent,
13
+ SemanticWatchReducer,
14
+ };
15
+ export type {
16
+ SemanticWatchEvent,
17
+ SemanticWatchEventType,
18
+ SseMessage,
19
+ };
20
+
21
+ export function parseSemanticEventFilter(raw: string | undefined): Set<SemanticWatchEventType> {
22
+ const all = new Set<SemanticWatchEventType>(ALL_SEMANTIC_WATCH_EVENT_TYPES);
23
+ if (!raw || raw.trim().length === 0) return all;
24
+
25
+ const values = raw
26
+ .split(',')
27
+ .map((value) => value.trim())
28
+ .filter((value): value is SemanticWatchEventType => all.has(value as SemanticWatchEventType));
29
+ return new Set(values);
30
+ }
31
+
32
+ export async function* parseSseStream(stream: ReadableStream<Uint8Array>): AsyncGenerator<SseMessage> {
33
+ const reader = stream.getReader();
34
+ const decoder = new TextDecoder();
35
+ let buffer = '';
36
+
37
+ try {
38
+ while (true) {
39
+ const { done, value } = await reader.read();
40
+ if (done) break;
41
+ buffer += decoder.decode(value, { stream: true });
42
+
43
+ while (true) {
44
+ const separatorIndex = buffer.indexOf('\n\n');
45
+ if (separatorIndex === -1) break;
46
+ const rawEvent = buffer.slice(0, separatorIndex);
47
+ buffer = buffer.slice(separatorIndex + 2);
48
+ const parsed = parseSseMessage(rawEvent);
49
+ if (parsed) yield parsed;
50
+ }
51
+ }
52
+
53
+ buffer += decoder.decode();
54
+ if (buffer.trim().length > 0) {
55
+ const parsed = parseSseMessage(buffer);
56
+ if (parsed) yield parsed;
57
+ }
58
+ } finally {
59
+ reader.releaseLock();
60
+ }
61
+ }
62
+
63
+ function parseSseMessage(raw: string): SseMessage | null {
64
+ const lines = raw.split(/\r?\n/);
65
+ let event = 'message';
66
+ let id: string | undefined;
67
+ const dataLines: string[] = [];
68
+
69
+ for (const line of lines) {
70
+ if (line.startsWith(':') || line.trim().length === 0) continue;
71
+ const separatorIndex = line.indexOf(':');
72
+ const field = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
73
+ let value = separatorIndex === -1 ? '' : line.slice(separatorIndex + 1);
74
+ if (value.startsWith(' ')) value = value.slice(1);
75
+
76
+ if (field === 'event') event = value;
77
+ if (field === 'id') id = value;
78
+ if (field === 'data') dataLines.push(value);
79
+ }
80
+
81
+ if (dataLines.length === 0) return null;
82
+ const rawData = dataLines.join('\n');
83
+ try {
84
+ return { event, id, data: JSON.parse(rawData) };
85
+ } catch {
86
+ return { event, id, data: rawData };
87
+ }
88
+ }