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,370 @@
1
+ /**
2
+ * Code Graph — Auto-dependency detection between file nodes.
3
+ *
4
+ * Parses imports from file node content and auto-creates `depends-on` edges
5
+ * between file nodes that reference each other. Updates live when files change.
6
+ *
7
+ * Supported import patterns:
8
+ * - JS/TS: import ... from '...', import('...'), require('...')
9
+ * - Python: import ..., from ... import ...
10
+ * - Go: import "...", import ( "..." )
11
+ * - Rust: mod ..., use crate::...
12
+ *
13
+ * Auto-edges are tagged with `_autoCodeGraph: true` in their metadata so they
14
+ * can be distinguished from manually created edges and cleaned up properly.
15
+ */
16
+
17
+ import { resolve, dirname, basename, extname, relative } from 'node:path';
18
+ import { existsSync } from 'node:fs';
19
+ import { canvasState } from './canvas-state.js';
20
+ import type { CanvasNodeState, CanvasEdge } from './canvas-state.js';
21
+
22
+ // ── Types ────────────────────────────────────────────────────────────
23
+
24
+ export interface CodeGraphEdge {
25
+ fromNodeId: string;
26
+ toNodeId: string;
27
+ fromPath: string;
28
+ toPath: string;
29
+ importSpecifier: string;
30
+ }
31
+
32
+ export interface CodeGraphSummary {
33
+ totalFileNodes: number;
34
+ totalAutoEdges: number;
35
+ nodes: {
36
+ id: string;
37
+ path: string;
38
+ title: string | null;
39
+ imports: string[];
40
+ importedBy: string[];
41
+ /** Number of files this node depends on */
42
+ outDegree: number;
43
+ /** Number of files that depend on this node */
44
+ inDegree: number;
45
+ }[];
46
+ /** Most-depended-on files (highest inDegree) */
47
+ centralFiles: { path: string; title: string | null; inDegree: number }[];
48
+ /** Files with no dependencies in or out (isolated) */
49
+ isolatedFiles: { path: string; title: string | null }[];
50
+ }
51
+
52
+ // ── Import Parser ────────────────────────────────────────────────────
53
+
54
+ // JS/TS patterns
55
+ const JS_IMPORT_FROM = /(?:import|export)\s+(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"]/g;
56
+ const JS_IMPORT_BARE = /import\s+['"]([^'"]+)['"]/g;
57
+ const JS_DYNAMIC_IMPORT = /import\(\s*['"]([^'"]+)['"]\s*\)/g;
58
+ const JS_REQUIRE = /require\(\s*['"]([^'"]+)['"]\s*\)/g;
59
+
60
+ // Python patterns
61
+ const PY_IMPORT = /^import\s+([\w.]+)/gm;
62
+ const PY_FROM_IMPORT = /^from\s+([\w.]+)\s+import/gm;
63
+
64
+ // Go patterns
65
+ const GO_IMPORT_SINGLE = /import\s+"([^"]+)"/g;
66
+ const GO_IMPORT_BLOCK = /import\s*\(\s*([\s\S]*?)\)/g;
67
+ const GO_IMPORT_LINE = /"([^"]+)"/g;
68
+
69
+ // Rust patterns
70
+ const RUST_MOD = /^mod\s+(\w+)\s*;/gm;
71
+ const RUST_USE_CRATE = /^use\s+crate::(\S+)/gm;
72
+
73
+ /**
74
+ * Extract import specifiers from file content based on file extension.
75
+ */
76
+ export function parseImports(content: string, filePath: string): string[] {
77
+ const ext = extname(filePath).toLowerCase();
78
+ const specifiers: string[] = [];
79
+
80
+ if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.mts', '.cjs', '.cts'].includes(ext)) {
81
+ for (const match of content.matchAll(JS_IMPORT_FROM)) specifiers.push(match[1]);
82
+ for (const match of content.matchAll(JS_IMPORT_BARE)) specifiers.push(match[1]);
83
+ for (const match of content.matchAll(JS_DYNAMIC_IMPORT)) specifiers.push(match[1]);
84
+ for (const match of content.matchAll(JS_REQUIRE)) specifiers.push(match[1]);
85
+ } else if (['.py', '.pyw'].includes(ext)) {
86
+ for (const match of content.matchAll(PY_IMPORT)) specifiers.push(match[1]);
87
+ for (const match of content.matchAll(PY_FROM_IMPORT)) specifiers.push(match[1]);
88
+ } else if (ext === '.go') {
89
+ for (const match of content.matchAll(GO_IMPORT_SINGLE)) specifiers.push(match[1]);
90
+ for (const match of content.matchAll(GO_IMPORT_BLOCK)) {
91
+ const block = match[1];
92
+ for (const line of block.matchAll(GO_IMPORT_LINE)) specifiers.push(line[1]);
93
+ }
94
+ } else if (ext === '.rs') {
95
+ for (const match of content.matchAll(RUST_MOD)) specifiers.push(match[1]);
96
+ for (const match of content.matchAll(RUST_USE_CRATE)) specifiers.push(match[1]);
97
+ }
98
+
99
+ // Deduplicate
100
+ return [...new Set(specifiers)];
101
+ }
102
+
103
+ // ── Path Resolution ──────────────────────────────────────────────────
104
+
105
+ /**
106
+ * Resolve an import specifier to a file path, checking common extensions.
107
+ * Returns null if the import can't be resolved to a file on disk.
108
+ */
109
+ function resolveImportPath(specifier: string, fromFilePath: string): string | null {
110
+ // Skip bare module specifiers (npm packages, node builtins)
111
+ if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
112
+ // Could be a project-relative path or a package — skip packages
113
+ // Check if it looks like a relative project path (contains / and no @)
114
+ if (!specifier.includes('/') || specifier.startsWith('@')) return null;
115
+ // For non-relative paths, try workspace-relative resolution
116
+ const wsRoot = process.cwd();
117
+ return tryResolveWithExtensions(resolve(wsRoot, specifier));
118
+ }
119
+
120
+ const dir = dirname(fromFilePath);
121
+ const candidate = resolve(dir, specifier);
122
+ return tryResolveWithExtensions(candidate);
123
+ }
124
+
125
+ const RESOLVE_EXTENSIONS = [
126
+ '', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.mts',
127
+ '/index.ts', '/index.tsx', '/index.js', '/index.jsx',
128
+ '.py', '.go', '.rs',
129
+ ];
130
+
131
+ function tryResolveWithExtensions(basePath: string): string | null {
132
+ // Try exact path + extensions
133
+ for (const ext of RESOLVE_EXTENSIONS) {
134
+ const full = basePath + ext;
135
+ if (existsSync(full)) return full;
136
+ }
137
+
138
+ // Handle TS/JS extension mapping: import './foo.js' → actual file is './foo.ts'
139
+ const ext = extname(basePath);
140
+ if (['.js', '.jsx', '.mjs', '.cjs'].includes(ext)) {
141
+ const withoutExt = basePath.slice(0, -ext.length);
142
+ const tsExtMap: Record<string, string[]> = {
143
+ '.js': ['.ts', '.tsx'],
144
+ '.jsx': ['.tsx'],
145
+ '.mjs': ['.mts'],
146
+ '.cjs': ['.cts'],
147
+ };
148
+ for (const tsExt of tsExtMap[ext] ?? []) {
149
+ const candidate = withoutExt + tsExt;
150
+ if (existsSync(candidate)) return candidate;
151
+ }
152
+ // Also try index files: import './foo.js' → './foo/index.ts'
153
+ for (const idxExt of ['/index.ts', '/index.tsx', '/index.js']) {
154
+ const candidate = withoutExt + idxExt;
155
+ if (existsSync(candidate)) return candidate;
156
+ }
157
+ }
158
+
159
+ return null;
160
+ }
161
+
162
+ // ── Auto-Edge Manager ────────────────────────────────────────────────
163
+
164
+ /** Prefix for auto-generated code graph edge IDs */
165
+ const AUTO_EDGE_PREFIX = 'codegraph-';
166
+
167
+ /** Cache of last recomputed edges for buildCodeGraphSummary() to reuse. */
168
+ let _lastDiscoveredEdges: CodeGraphEdge[] = [];
169
+
170
+ /**
171
+ * Get all file nodes currently on the canvas.
172
+ */
173
+ function getFileNodes(): CanvasNodeState[] {
174
+ return canvasState.getLayout().nodes.filter((n) => n.type === 'file' && typeof n.data.path === 'string');
175
+ }
176
+
177
+ /**
178
+ * Build a map of absolute file path → node ID for the given file nodes.
179
+ */
180
+ function buildPathIndex(fileNodes?: CanvasNodeState[]): Map<string, string> {
181
+ const index = new Map<string, string>();
182
+ for (const node of (fileNodes ?? getFileNodes())) {
183
+ index.set(node.data.path as string, node.id);
184
+ }
185
+ return index;
186
+ }
187
+
188
+ /**
189
+ * Recompute all auto-edges for the code graph.
190
+ * Called when file nodes are added/removed or file content changes.
191
+ *
192
+ * Returns the list of edges that were created/maintained.
193
+ */
194
+ export function recomputeCodeGraph(): CodeGraphEdge[] {
195
+ const fileNodes = getFileNodes();
196
+ const pathIndex = buildPathIndex(fileNodes);
197
+ const discoveredEdges: CodeGraphEdge[] = [];
198
+
199
+ // Parse imports for each file node and resolve to other file nodes
200
+ for (const node of fileNodes) {
201
+ const filePath = node.data.path as string;
202
+ const content = (node.data.fileContent as string) ?? '';
203
+ if (!content) continue;
204
+
205
+ const specifiers = parseImports(content, filePath);
206
+
207
+ for (const spec of specifiers) {
208
+ const resolvedPath = resolveImportPath(spec, filePath);
209
+ if (!resolvedPath) continue;
210
+
211
+ const targetNodeId = pathIndex.get(resolvedPath);
212
+ if (!targetNodeId || targetNodeId === node.id) continue;
213
+
214
+ discoveredEdges.push({
215
+ fromNodeId: node.id,
216
+ toNodeId: targetNodeId,
217
+ fromPath: filePath,
218
+ toPath: resolvedPath,
219
+ importSpecifier: spec,
220
+ });
221
+ }
222
+ }
223
+
224
+ // Remove old auto-edges that no longer exist
225
+ const existingAutoEdges = canvasState.getEdges().filter((e) => e.id.startsWith(AUTO_EDGE_PREFIX));
226
+ const discoveredKeys = new Set(discoveredEdges.map((e) => `${e.fromNodeId}->${e.toNodeId}`));
227
+
228
+ // Suppress mutation recording for auto-edge management (these are computed, not user actions)
229
+ canvasState.withSuppressedRecording(() => {
230
+ for (const edge of existingAutoEdges) {
231
+ const key = `${edge.from}->${edge.to}`;
232
+ if (!discoveredKeys.has(key)) {
233
+ canvasState.removeEdge(edge.id);
234
+ }
235
+ }
236
+
237
+ // Add new auto-edges that don't exist yet
238
+ const existingKeys = new Set(existingAutoEdges.map((e) => `${e.from}->${e.to}`));
239
+ for (const edge of discoveredEdges) {
240
+ const key = `${edge.fromNodeId}->${edge.toNodeId}`;
241
+ if (existingKeys.has(key)) continue;
242
+
243
+ const edgeId = `${AUTO_EDGE_PREFIX}${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
244
+ canvasState.addEdge({
245
+ id: edgeId,
246
+ from: edge.fromNodeId,
247
+ to: edge.toNodeId,
248
+ type: 'depends-on',
249
+ label: 'imports',
250
+ style: 'dashed',
251
+ });
252
+ }
253
+ });
254
+
255
+ _lastDiscoveredEdges = discoveredEdges;
256
+ return discoveredEdges;
257
+ }
258
+
259
+ /**
260
+ * Build a summary of the code graph for the MCP resource.
261
+ */
262
+ export function buildCodeGraphSummary(): CodeGraphSummary {
263
+ const fileNodes = getFileNodes();
264
+ const autoEdges = canvasState.getEdges().filter((e) => e.id.startsWith(AUTO_EDGE_PREFIX));
265
+
266
+ // Build adjacency data
267
+ const outgoing = new Map<string, Set<string>>(); // nodeId → set of target nodeIds
268
+ const incoming = new Map<string, Set<string>>(); // nodeId → set of source nodeIds
269
+ const importSpecs = new Map<string, string[]>(); // nodeId → import specifiers
270
+
271
+ for (const node of fileNodes) {
272
+ outgoing.set(node.id, new Set());
273
+ incoming.set(node.id, new Set());
274
+ importSpecs.set(node.id, []);
275
+ }
276
+
277
+ for (const edge of autoEdges) {
278
+ outgoing.get(edge.from)?.add(edge.to);
279
+ incoming.get(edge.to)?.add(edge.from);
280
+ }
281
+
282
+ // Use cached discovered edges from last recomputeCodeGraph() to avoid re-parsing
283
+ const idToPath = new Map<string, string>();
284
+ for (const node of fileNodes) idToPath.set(node.id, node.data.path as string);
285
+
286
+ // Group import specifiers by source node from cached edges
287
+ for (const edge of _lastDiscoveredEdges) {
288
+ const specs = importSpecs.get(edge.fromNodeId);
289
+ if (specs && !specs.includes(edge.importSpecifier)) {
290
+ specs.push(edge.importSpecifier);
291
+ }
292
+ }
293
+
294
+ const nodes = fileNodes.map((node) => {
295
+ const out = outgoing.get(node.id) ?? new Set();
296
+ const inc = incoming.get(node.id) ?? new Set();
297
+ return {
298
+ id: node.id,
299
+ path: relative(process.cwd(), node.data.path as string) || (node.data.path as string),
300
+ title: (node.data.title as string) ?? null,
301
+ imports: importSpecs.get(node.id) ?? [],
302
+ importedBy: [...(inc)].map((id) => {
303
+ const path = idToPath.get(id);
304
+ return path ? relative(process.cwd(), path) : id;
305
+ }),
306
+ outDegree: out.size,
307
+ inDegree: inc.size,
308
+ };
309
+ });
310
+
311
+ // Sort by inDegree descending for central files
312
+ const centralFiles = nodes
313
+ .filter((n) => n.inDegree > 0)
314
+ .sort((a, b) => b.inDegree - a.inDegree)
315
+ .slice(0, 10)
316
+ .map((n) => ({ path: n.path, title: n.title, inDegree: n.inDegree }));
317
+
318
+ const isolatedFiles = nodes
319
+ .filter((n) => n.inDegree === 0 && n.outDegree === 0)
320
+ .map((n) => ({ path: n.path, title: n.title }));
321
+
322
+ return {
323
+ totalFileNodes: fileNodes.length,
324
+ totalAutoEdges: autoEdges.length,
325
+ nodes,
326
+ centralFiles,
327
+ isolatedFiles,
328
+ };
329
+ }
330
+
331
+ /**
332
+ * Format code graph summary as human-readable text for MCP.
333
+ */
334
+ export function formatCodeGraph(summary: CodeGraphSummary): string {
335
+ if (summary.totalFileNodes === 0) {
336
+ return 'Code Graph: no file nodes on canvas. Add file nodes to see auto-detected dependencies.';
337
+ }
338
+
339
+ const lines: string[] = [
340
+ `Code Graph: ${summary.totalFileNodes} files, ${summary.totalAutoEdges} dependency edges`,
341
+ '',
342
+ ];
343
+
344
+ if (summary.centralFiles.length > 0) {
345
+ lines.push('Central files (most depended on):');
346
+ for (const f of summary.centralFiles) {
347
+ lines.push(` ${f.title ?? f.path} — imported by ${f.inDegree} file(s)`);
348
+ }
349
+ lines.push('');
350
+ }
351
+
352
+ if (summary.isolatedFiles.length > 0) {
353
+ lines.push(`Isolated files (${summary.isolatedFiles.length}): ${summary.isolatedFiles.map((f) => f.title ?? f.path).join(', ')}`);
354
+ lines.push('');
355
+ }
356
+
357
+ lines.push('Dependencies:');
358
+ for (const node of summary.nodes) {
359
+ if (node.outDegree === 0 && node.inDegree === 0) continue;
360
+ lines.push(` ${node.title ?? node.path}`);
361
+ if (node.imports.length > 0) {
362
+ lines.push(` imports: ${node.imports.join(', ')}`);
363
+ }
364
+ if (node.importedBy.length > 0) {
365
+ lines.push(` imported by: ${node.importedBy.join(', ')}`);
366
+ }
367
+ }
368
+
369
+ return lines.join('\n');
370
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Context cards — standalone version.
3
+ *
4
+ * In the full PMX project this reads project memory and context doc definitions.
5
+ * In the standalone canvas, we expose the types and a simplified builder that
6
+ * returns an empty card list (the host application can populate cards via the API).
7
+ */
8
+
9
+ export type WorkbenchContextCardCategory = 'profile' | 'planning' | 'tooling' | 'memory';
10
+
11
+ export interface WorkbenchContextCard {
12
+ key: string;
13
+ label: string;
14
+ summary: string;
15
+ path: string;
16
+ exists: boolean;
17
+ required: boolean;
18
+ staleDays: number;
19
+ mtimeMs: number | null;
20
+ state: 'loaded' | 'missing' | 'stale' | 'invalid';
21
+ sourceKind: 'workspace' | 'global';
22
+ note: string | null;
23
+ injectMode: 'startup';
24
+ category: WorkbenchContextCardCategory;
25
+ }
26
+
27
+ export function buildWorkbenchContextCards(_workspaceRoot = process.cwd()): WorkbenchContextCard[] {
28
+ // Standalone canvas does not have access to PMX context doc definitions.
29
+ // Return empty — the host application populates cards via the event API.
30
+ return [];
31
+ }
@@ -0,0 +1,71 @@
1
+ import type { ExternalMcpTransportConfig } from './mcp-app-runtime.js';
2
+
3
+ export const EXCALIDRAW_MCP_URL = 'https://mcp.excalidraw.com/mcp';
4
+ export const EXCALIDRAW_SERVER_NAME = 'Excalidraw';
5
+ export const EXCALIDRAW_CREATE_VIEW_TOOL = 'create_view';
6
+
7
+ export const EXCALIDRAW_MCP_TRANSPORT: ExternalMcpTransportConfig = {
8
+ type: 'http',
9
+ url: EXCALIDRAW_MCP_URL,
10
+ };
11
+
12
+ export interface DiagramPresetOpenInput {
13
+ elements: unknown;
14
+ title?: string;
15
+ x?: number;
16
+ y?: number;
17
+ width?: number;
18
+ height?: number;
19
+ }
20
+
21
+ export interface ExcalidrawOpenMcpAppInput {
22
+ transport: ExternalMcpTransportConfig;
23
+ toolName: string;
24
+ serverName: string;
25
+ toolArguments: { elements: string };
26
+ title?: string;
27
+ x?: number;
28
+ y?: number;
29
+ width?: number;
30
+ height?: number;
31
+ }
32
+
33
+ export function normalizeExcalidrawElements(elements: unknown): string {
34
+ if (typeof elements === 'string') {
35
+ const trimmed = elements.trim();
36
+ if (!trimmed) {
37
+ throw new Error('diagram.elements must be a non-empty JSON array string or an array of Excalidraw elements.');
38
+ }
39
+ let parsed: unknown;
40
+ try {
41
+ parsed = JSON.parse(trimmed);
42
+ } catch (error) {
43
+ const reason = error instanceof Error ? error.message : String(error);
44
+ throw new Error(`diagram.elements string is not valid JSON: ${reason}`);
45
+ }
46
+ if (!Array.isArray(parsed)) {
47
+ throw new Error('diagram.elements string must encode a JSON array.');
48
+ }
49
+ return JSON.stringify(parsed);
50
+ }
51
+ if (Array.isArray(elements)) {
52
+ return JSON.stringify(elements);
53
+ }
54
+ throw new Error('diagram.elements must be a JSON array string or an array of Excalidraw elements.');
55
+ }
56
+
57
+ export function buildExcalidrawOpenMcpAppInput(input: DiagramPresetOpenInput): ExcalidrawOpenMcpAppInput {
58
+ const elements = normalizeExcalidrawElements(input.elements);
59
+ const out: ExcalidrawOpenMcpAppInput = {
60
+ transport: EXCALIDRAW_MCP_TRANSPORT,
61
+ toolName: EXCALIDRAW_CREATE_VIEW_TOOL,
62
+ serverName: EXCALIDRAW_SERVER_NAME,
63
+ toolArguments: { elements },
64
+ };
65
+ if (typeof input.title === 'string' && input.title.trim().length > 0) out.title = input.title.trim();
66
+ if (typeof input.x === 'number' && Number.isFinite(input.x)) out.x = input.x;
67
+ if (typeof input.y === 'number' && Number.isFinite(input.y)) out.y = input.y;
68
+ if (typeof input.width === 'number' && Number.isFinite(input.width)) out.width = input.width;
69
+ if (typeof input.height === 'number' && Number.isFinite(input.height)) out.height = input.height;
70
+ return out;
71
+ }
@@ -0,0 +1,77 @@
1
+ export interface ExtAppCallIdentity {
2
+ toolCallId?: string;
3
+ serverName?: string;
4
+ toolName?: string;
5
+ }
6
+
7
+ function normalizeKeyPart(value: string | undefined): string {
8
+ return value?.trim().toLowerCase() ?? '';
9
+ }
10
+
11
+ export function getExtAppCallKey(serverName?: string, toolName?: string): string | null {
12
+ const normalizedServerName = normalizeKeyPart(serverName);
13
+ const normalizedToolName = normalizeKeyPart(toolName);
14
+ if (!normalizedServerName || !normalizedToolName) return null;
15
+ return `${normalizedServerName}::${normalizedToolName}`;
16
+ }
17
+
18
+ export class ExtAppCallRegistry {
19
+ private activeIds = new Set<string>();
20
+ private keyById = new Map<string, string>();
21
+ private idsByKey = new Map<string, string[]>();
22
+
23
+ register(identity: ExtAppCallIdentity): string | null {
24
+ const toolCallId = String(identity.toolCallId || '').trim();
25
+ if (!toolCallId) return null;
26
+ this.activeIds.add(toolCallId);
27
+ const key = getExtAppCallKey(identity.serverName, identity.toolName);
28
+ if (key) {
29
+ this.keyById.set(toolCallId, key);
30
+ const existing = this.idsByKey.get(key)?.filter((id) => this.activeIds.has(id)) ?? [];
31
+ if (!existing.includes(toolCallId)) {
32
+ existing.push(toolCallId);
33
+ }
34
+ this.idsByKey.set(key, existing);
35
+ }
36
+ return toolCallId;
37
+ }
38
+
39
+ has(toolCallId?: string): boolean {
40
+ const id = String(toolCallId || '').trim();
41
+ return id.length > 0 && this.activeIds.has(id);
42
+ }
43
+
44
+ resolve(identity: ExtAppCallIdentity): string | null {
45
+ const directId = String(identity.toolCallId || '').trim();
46
+ if (directId && this.activeIds.has(directId)) {
47
+ return directId;
48
+ }
49
+ const key = getExtAppCallKey(identity.serverName, identity.toolName);
50
+ if (!key) return null;
51
+ const ids = this.idsByKey.get(key)?.filter((id) => this.activeIds.has(id)) ?? [];
52
+ return ids.length === 1 ? ids[0] : null;
53
+ }
54
+
55
+ complete(identity: ExtAppCallIdentity): string | null {
56
+ const resolvedId = this.resolve(identity);
57
+ if (!resolvedId) return null;
58
+ this.activeIds.delete(resolvedId);
59
+ const key = this.keyById.get(resolvedId);
60
+ this.keyById.delete(resolvedId);
61
+ if (key) {
62
+ const remaining = this.idsByKey.get(key)?.filter((id) => id !== resolvedId && this.activeIds.has(id)) ?? [];
63
+ if (remaining.length > 0) {
64
+ this.idsByKey.set(key, remaining);
65
+ } else {
66
+ this.idsByKey.delete(key);
67
+ }
68
+ }
69
+ return resolvedId;
70
+ }
71
+
72
+ clear(): void {
73
+ this.activeIds.clear();
74
+ this.keyById.clear();
75
+ this.idsByKey.clear();
76
+ }
77
+ }
@@ -0,0 +1,4 @@
1
+ export {
2
+ normalizeExtAppToolResult,
3
+ type NormalizeExtAppToolResultInput,
4
+ } from '../shared/ext-app-tool-result.js';
@@ -0,0 +1,121 @@
1
+ /**
2
+ * File watcher for file-type canvas nodes.
3
+ *
4
+ * Monitors files on disk and pushes content updates to the canvas
5
+ * when they change. This enables real-time file viewing: the agent
6
+ * edits a file, the canvas node updates automatically.
7
+ */
8
+
9
+ import { watch, existsSync, readFileSync, statSync } from 'node:fs';
10
+ import type { FSWatcher } from 'node:fs';
11
+ import { canvasState } from './canvas-state.js';
12
+
13
+ function logFileWatcherWarning(action: string, error: unknown, details?: Record<string, unknown>): void {
14
+ console.warn(`[file-watcher] ${action}`, { error, ...(details ?? {}) });
15
+ }
16
+
17
+ interface WatchedFile {
18
+ path: string;
19
+ nodeIds: Set<string>;
20
+ watcher: FSWatcher;
21
+ lastMtime: number;
22
+ }
23
+
24
+ const watched = new Map<string, WatchedFile>();
25
+ let onFileChanged: ((nodeId: string) => void) | null = null;
26
+
27
+ /** Register a callback for when a watched file changes. */
28
+ export function onFileNodeChanged(cb: (nodeId: string) => void): void {
29
+ onFileChanged = cb;
30
+ }
31
+
32
+ /** Start watching a file for a given node. */
33
+ export function watchFileForNode(nodeId: string, filePath: string): void {
34
+ if (!filePath || !existsSync(filePath)) return;
35
+
36
+ const existing = watched.get(filePath);
37
+ if (existing) {
38
+ existing.nodeIds.add(nodeId);
39
+ return;
40
+ }
41
+
42
+ try {
43
+ const stat = statSync(filePath);
44
+ const watcher = watch(filePath, { persistent: false }, (eventType) => {
45
+ if (eventType !== 'change') return;
46
+ handleFileChange(filePath);
47
+ });
48
+
49
+ watched.set(filePath, {
50
+ path: filePath,
51
+ nodeIds: new Set([nodeId]),
52
+ watcher,
53
+ lastMtime: stat.mtimeMs,
54
+ });
55
+ } catch (error) {
56
+ logFileWatcherWarning('watch setup failed', error, { nodeId, filePath });
57
+ }
58
+ }
59
+
60
+ /** Stop watching a file for a given node. */
61
+ export function unwatchFileForNode(nodeId: string, filePath?: string): void {
62
+ for (const [path, entry] of watched) {
63
+ if (filePath && path !== filePath) continue;
64
+ entry.nodeIds.delete(nodeId);
65
+ if (entry.nodeIds.size === 0) {
66
+ entry.watcher.close();
67
+ watched.delete(path);
68
+ }
69
+ }
70
+ }
71
+
72
+ /** Stop all watchers. */
73
+ export function unwatchAll(): void {
74
+ for (const entry of watched.values()) {
75
+ entry.watcher.close();
76
+ }
77
+ watched.clear();
78
+ }
79
+
80
+ export function rewatchAllFileNodes(): void {
81
+ unwatchAll();
82
+ for (const node of canvasState.getLayout().nodes) {
83
+ if (node.type === 'file' && typeof node.data.path === 'string') {
84
+ watchFileForNode(node.id, node.data.path);
85
+ }
86
+ }
87
+ }
88
+
89
+ function handleFileChange(filePath: string): void {
90
+ const entry = watched.get(filePath);
91
+ if (!entry) return;
92
+
93
+ // Debounce: check mtime to avoid duplicate events
94
+ try {
95
+ if (!existsSync(filePath)) return;
96
+ const stat = statSync(filePath);
97
+ if (stat.mtimeMs === entry.lastMtime) return;
98
+ entry.lastMtime = stat.mtimeMs;
99
+
100
+ const content = readFileSync(filePath, 'utf-8');
101
+ const lineCount = content.split('\n').length;
102
+ const updatedAt = new Date(stat.mtimeMs).toISOString();
103
+
104
+ // Update all nodes watching this file
105
+ for (const nodeId of entry.nodeIds) {
106
+ const node = canvasState.getNode(nodeId);
107
+ if (!node) continue;
108
+ canvasState.updateNode(nodeId, {
109
+ data: {
110
+ ...node.data,
111
+ fileContent: content,
112
+ lineCount,
113
+ updatedAt,
114
+ },
115
+ });
116
+ onFileChanged?.(nodeId);
117
+ }
118
+ } catch (error) {
119
+ logFileWatcherWarning('handle file change failed', error, { filePath });
120
+ }
121
+ }