pmx-canvas 0.3.0 → 0.3.2

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 (161) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/canvas/index.js +65 -65
  3. package/dist/types/cli/agent.d.ts +9 -1
  4. package/dist/types/cli/daemon.d.ts +74 -0
  5. package/dist/types/cli/watch.d.ts +2 -2
  6. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  7. package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
  8. package/dist/types/client/canvas/Minimap.d.ts +1 -1
  9. package/dist/types/client/nodes/ExtAppFrame.d.ts +9 -1
  10. package/dist/types/client/nodes/FileNode.d.ts +1 -1
  11. package/dist/types/client/nodes/ImageNode.d.ts +1 -1
  12. package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
  13. package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
  14. package/dist/types/client/nodes/PromptNode.d.ts +1 -1
  15. package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
  16. package/dist/types/server/canvas-schema.d.ts +1 -1
  17. package/dist/types/server/html-primitives.d.ts +1 -1
  18. package/dist/types/server/index.d.ts +4 -4
  19. package/dist/types/server/operations/index.d.ts +1 -1
  20. package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
  21. package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
  22. package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
  23. package/dist/types/server/server.d.ts +8 -0
  24. package/docs/cli.md +10 -1
  25. package/docs/http-api.md +28 -0
  26. package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
  27. package/docs/screenshot.png +0 -0
  28. package/docs/tech-debt-assessment-2026-07.md +2 -2
  29. package/package.json +5 -2
  30. package/skills/pmx-canvas/SKILL.md +15 -10
  31. package/skills/pmx-canvas/references/full-reference.md +17 -3
  32. package/src/cli/agent.ts +1951 -1571
  33. package/src/cli/daemon.ts +460 -0
  34. package/src/cli/index.ts +80 -323
  35. package/src/cli/watch.ts +2 -10
  36. package/src/client/App.tsx +48 -46
  37. package/src/client/canvas/AttentionHistory.tsx +11 -1
  38. package/src/client/canvas/CanvasNode.tsx +41 -29
  39. package/src/client/canvas/CanvasViewport.tsx +101 -66
  40. package/src/client/canvas/CommandPalette.tsx +61 -27
  41. package/src/client/canvas/ContextMenu.tsx +13 -20
  42. package/src/client/canvas/ContextPinBar.tsx +1 -5
  43. package/src/client/canvas/ContextPinHud.tsx +1 -6
  44. package/src/client/canvas/DockedNode.tsx +4 -4
  45. package/src/client/canvas/EdgeLayer.tsx +37 -36
  46. package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
  47. package/src/client/canvas/FocusFieldLayer.tsx +20 -22
  48. package/src/client/canvas/IntentLayer.tsx +31 -14
  49. package/src/client/canvas/Minimap.tsx +11 -16
  50. package/src/client/canvas/SelectionBar.tsx +4 -11
  51. package/src/client/canvas/ShortcutOverlay.tsx +3 -1
  52. package/src/client/canvas/SnapshotPanel.tsx +77 -95
  53. package/src/client/canvas/auto-fit.ts +15 -14
  54. package/src/client/canvas/snap-guides.ts +12 -12
  55. package/src/client/canvas/use-node-resize.ts +1 -5
  56. package/src/client/canvas/use-pan-zoom.ts +25 -26
  57. package/src/client/ext-app/bridge.ts +3 -12
  58. package/src/client/icons.tsx +63 -20
  59. package/src/client/nodes/ContextNode.tsx +14 -25
  60. package/src/client/nodes/ExtAppFrame.tsx +194 -80
  61. package/src/client/nodes/FileNode.tsx +74 -62
  62. package/src/client/nodes/GroupNode.tsx +4 -6
  63. package/src/client/nodes/HtmlNode.tsx +76 -46
  64. package/src/client/nodes/ImageNode.tsx +18 -27
  65. package/src/client/nodes/InlineFormatBar.tsx +4 -21
  66. package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
  67. package/src/client/nodes/LedgerNode.tsx +10 -4
  68. package/src/client/nodes/MarkdownNode.tsx +3 -10
  69. package/src/client/nodes/McpAppNode.tsx +26 -22
  70. package/src/client/nodes/MdFormatBar.tsx +10 -7
  71. package/src/client/nodes/PromptNode.tsx +23 -51
  72. package/src/client/nodes/ResponseNode.tsx +3 -13
  73. package/src/client/nodes/StatusNode.tsx +5 -9
  74. package/src/client/nodes/StatusSummary.tsx +2 -8
  75. package/src/client/nodes/WebpageNode.tsx +20 -14
  76. package/src/client/nodes/iframe-document-url.ts +25 -16
  77. package/src/client/nodes/image-warnings.ts +1 -7
  78. package/src/client/nodes/md-format.ts +20 -5
  79. package/src/client/state/attention-bridge.ts +4 -9
  80. package/src/client/state/attention-store.ts +1 -7
  81. package/src/client/state/canvas-store.ts +52 -36
  82. package/src/client/state/intent-bridge.ts +176 -112
  83. package/src/client/state/intent-store.ts +4 -1
  84. package/src/client/state/sse-bridge.ts +53 -70
  85. package/src/json-render/catalog.ts +12 -16
  86. package/src/json-render/charts/components.tsx +16 -20
  87. package/src/json-render/charts/extra-components.tsx +8 -16
  88. package/src/json-render/charts/extra-definitions.ts +1 -2
  89. package/src/json-render/charts/tufte-components.tsx +37 -20
  90. package/src/json-render/charts/tufte-definitions.ts +8 -2
  91. package/src/json-render/renderer/index.tsx +42 -22
  92. package/src/json-render/schema.ts +6 -3
  93. package/src/json-render/server.ts +33 -39
  94. package/src/mcp/canvas-access.ts +35 -21
  95. package/src/mcp/server.ts +132 -70
  96. package/src/server/agent-context.ts +63 -36
  97. package/src/server/ax-context.ts +7 -5
  98. package/src/server/ax-interaction.ts +176 -43
  99. package/src/server/ax-state-manager.ts +182 -39
  100. package/src/server/ax-state.ts +142 -47
  101. package/src/server/canvas-db.ts +213 -95
  102. package/src/server/canvas-operations.ts +180 -123
  103. package/src/server/canvas-provenance.ts +1 -4
  104. package/src/server/canvas-schema.ts +454 -73
  105. package/src/server/canvas-serialization.ts +27 -35
  106. package/src/server/canvas-state.ts +150 -58
  107. package/src/server/chart-template.ts +4 -5
  108. package/src/server/code-graph.ts +19 -6
  109. package/src/server/diagram-presets.ts +28 -29
  110. package/src/server/ext-app-lookup.ts +3 -12
  111. package/src/server/html-node-summary.ts +19 -10
  112. package/src/server/html-primitives.ts +326 -97
  113. package/src/server/html-surface.ts +6 -9
  114. package/src/server/image-source.ts +6 -4
  115. package/src/server/index.ts +320 -217
  116. package/src/server/intent-registry.ts +2 -5
  117. package/src/server/mcp-app-candidate.ts +5 -10
  118. package/src/server/mcp-app-host.ts +14 -38
  119. package/src/server/mcp-app-runtime.ts +12 -20
  120. package/src/server/mutation-history.ts +15 -5
  121. package/src/server/operations/composites.ts +1 -3
  122. package/src/server/operations/http.ts +2 -3
  123. package/src/server/operations/index.ts +7 -1
  124. package/src/server/operations/invoker.ts +4 -3
  125. package/src/server/operations/mcp.ts +22 -30
  126. package/src/server/operations/ops/annotation.ts +122 -10
  127. package/src/server/operations/ops/app.ts +98 -73
  128. package/src/server/operations/ops/ax-await.ts +17 -10
  129. package/src/server/operations/ops/ax-read.ts +347 -0
  130. package/src/server/operations/ops/ax-shared.ts +2 -7
  131. package/src/server/operations/ops/ax-state.ts +32 -14
  132. package/src/server/operations/ops/ax-timeline.ts +32 -19
  133. package/src/server/operations/ops/ax-work.ts +54 -37
  134. package/src/server/operations/ops/batch.ts +41 -15
  135. package/src/server/operations/ops/canvas-wire.ts +91 -0
  136. package/src/server/operations/ops/edges.ts +37 -25
  137. package/src/server/operations/ops/ext-app.ts +346 -0
  138. package/src/server/operations/ops/groups.ts +49 -20
  139. package/src/server/operations/ops/intent.ts +18 -12
  140. package/src/server/operations/ops/json-render.ts +239 -98
  141. package/src/server/operations/ops/nodes.ts +298 -109
  142. package/src/server/operations/ops/query.ts +46 -28
  143. package/src/server/operations/ops/snapshots.ts +35 -26
  144. package/src/server/operations/ops/validate.ts +2 -1
  145. package/src/server/operations/ops/viewport.ts +60 -16
  146. package/src/server/operations/ops/webview.ts +44 -18
  147. package/src/server/operations/registry.ts +2 -3
  148. package/src/server/operations/types.ts +7 -5
  149. package/src/server/operations/webview-runner.ts +1 -3
  150. package/src/server/placement.ts +8 -18
  151. package/src/server/server.ts +122 -1028
  152. package/src/server/spatial-analysis.ts +39 -25
  153. package/src/server/trace-manager.ts +3 -8
  154. package/src/server/web-artifacts.ts +23 -27
  155. package/src/server/webpage-node.ts +5 -13
  156. package/src/shared/auto-arrange.ts +12 -5
  157. package/src/shared/content-height-reporter.ts +8 -6
  158. package/src/shared/ext-app-tool-result.ts +2 -6
  159. package/src/shared/placement.ts +1 -4
  160. package/src/shared/semantic-attention.ts +39 -37
  161. package/src/shared/surface.ts +8 -4
package/src/cli/agent.ts CHANGED
@@ -72,9 +72,7 @@ interface CanvasSchemaResponse {
72
72
  components: JsonRenderComponentSchema[];
73
73
  };
74
74
  graph: {
75
- graphTypes: Array<
76
- 'line' | 'bar' | 'pie' | 'area' | 'scatter' | 'radar' | 'stacked-bar' | 'composed'
77
- >;
75
+ graphTypes: Array<'line' | 'bar' | 'pie' | 'area' | 'scatter' | 'radar' | 'stacked-bar' | 'composed'>;
78
76
  };
79
77
  htmlPrimitives?: Array<{
80
78
  kind: string;
@@ -91,13 +89,77 @@ interface CanvasSchemaResponse {
91
89
  };
92
90
  }
93
91
 
92
+ // Per-invocation target override from the global --server-url / --port flags.
93
+ // Before these existed, `--port 4750` on any agent command was SILENTLY ignored
94
+ // and the command hit the default 4313 daemon — which once pointed a test
95
+ // automation WebView at a live production board.
96
+ let cliTargetOverride: string | null = null;
97
+
94
98
  function getBaseUrl(): string {
99
+ if (cliTargetOverride) return cliTargetOverride;
95
100
  const envUrl = process.env.PMX_CANVAS_URL;
96
101
  if (envUrl) return envUrl.replace(/\/$/, '');
97
102
  const port = process.env.PMX_CANVAS_PORT || DEFAULT_PORT;
98
103
  return `http://localhost:${port}`;
99
104
  }
100
105
 
106
+ /**
107
+ * Extract the global `--port <n>` / `--server-url <url>` flags (any position,
108
+ * `=` or space-separated value) and set the invocation's target override.
109
+ * Returns the remaining args for command dispatch. Invalid values are a loud
110
+ * `die` — never a silent fallback to the default port. `--server-url` wins
111
+ * over `--port` when both are given.
112
+ */
113
+ export function extractGlobalTargetFlags(args: string[]): string[] {
114
+ cliTargetOverride = null;
115
+ let portOverride: number | null = null;
116
+ let urlOverride: string | null = null;
117
+ const rest: string[] = [];
118
+
119
+ for (let i = 0; i < args.length; i++) {
120
+ const arg = args[i];
121
+ let name: 'port' | 'server-url' | null = null;
122
+ let value: string | undefined;
123
+ if (arg === '--port' || arg === '--server-url') {
124
+ name = arg.slice(2) as 'port' | 'server-url';
125
+ value = args[i + 1];
126
+ i += 1;
127
+ } else if (arg.startsWith('--port=')) {
128
+ name = 'port';
129
+ value = arg.slice('--port='.length);
130
+ } else if (arg.startsWith('--server-url=')) {
131
+ name = 'server-url';
132
+ value = arg.slice('--server-url='.length);
133
+ } else {
134
+ rest.push(arg);
135
+ continue;
136
+ }
137
+
138
+ if (name === 'port') {
139
+ const port = Number(value);
140
+ if (!value || !Number.isInteger(port) || port <= 0 || port > 65535) {
141
+ die(
142
+ `--port requires a port number, got ${value === undefined ? 'no value' : JSON.stringify(value)}.`,
143
+ 'Example: --port 4313. Without the flag, PMX_CANVAS_PORT / PMX_CANVAS_URL pick the target.',
144
+ );
145
+ }
146
+ portOverride = port;
147
+ } else {
148
+ if (!value || !/^https?:\/\//.test(value)) {
149
+ die(
150
+ `--server-url requires an http(s) URL, got ${value === undefined ? 'no value' : JSON.stringify(value)}.`,
151
+ 'Example: --server-url http://localhost:4313. Without the flag, PMX_CANVAS_URL picks the target.',
152
+ );
153
+ }
154
+ urlOverride = value.replace(/\/$/, '');
155
+ }
156
+ }
157
+
158
+ if (urlOverride) cliTargetOverride = urlOverride;
159
+ else if (portOverride) cliTargetOverride = `http://localhost:${portOverride}`;
160
+ return rest;
161
+ }
162
+
101
163
  function die(message: string, hint?: string): never {
102
164
  const out: Record<string, string> = { error: message };
103
165
  if (hint) out.hint = hint;
@@ -151,10 +213,7 @@ async function api(
151
213
  if (!res.ok) {
152
214
  if (options?.allowErrorJson) return json;
153
215
  const err = json as Record<string, unknown>;
154
- die(
155
- err.error ? String(err.error) : `HTTP ${res.status}`,
156
- typeof err.hint === 'string' ? err.hint : undefined,
157
- );
216
+ die(err.error ? String(err.error) : `HTTP ${res.status}`, typeof err.hint === 'string' ? err.hint : undefined);
158
217
  }
159
218
  return json;
160
219
  }
@@ -186,10 +245,38 @@ function parseFlags(args: string[]): { positional: string[]; flags: Record<strin
186
245
  const flags: Record<string, string | true> = {};
187
246
  // Boolean-only flags (never take a value argument)
188
247
  const BOOL_FLAGS = new Set([
189
- 'help', 'h', 'ids', 'stdin', 'yes', 'list', 'clear', 'set', 'animated', 'dry-run', 'all',
190
- 'no-open-in-canvas', 'lock-arrange', 'unlock-arrange', 'json', 'compact',
191
- 'verbose', 'include-logs', 'no-pan', 'schema', 'example', 'examples', 'strict-size', 'scroll-overflow',
192
- 'report', 'canvas', 'hooks', 'tools', 'session-messaging', 'permissions', 'files', 'ui-prompts',
248
+ 'help',
249
+ 'h',
250
+ 'ids',
251
+ 'stdin',
252
+ 'yes',
253
+ 'list',
254
+ 'clear',
255
+ 'set',
256
+ 'animated',
257
+ 'dry-run',
258
+ 'all',
259
+ 'no-open-in-canvas',
260
+ 'lock-arrange',
261
+ 'unlock-arrange',
262
+ 'json',
263
+ 'compact',
264
+ 'verbose',
265
+ 'include-logs',
266
+ 'no-pan',
267
+ 'schema',
268
+ 'example',
269
+ 'examples',
270
+ 'strict-size',
271
+ 'scroll-overflow',
272
+ 'report',
273
+ 'canvas',
274
+ 'hooks',
275
+ 'tools',
276
+ 'session-messaging',
277
+ 'permissions',
278
+ 'files',
279
+ 'ui-prompts',
193
280
  ]);
194
281
  for (let i = 0; i < args.length; i++) {
195
282
  const arg = args[i];
@@ -228,10 +315,7 @@ function requireFlag(flags: Record<string, string | true>, name: string, hint: s
228
315
  return val;
229
316
  }
230
317
 
231
- function getStringFlag(
232
- flags: Record<string, string | true>,
233
- ...names: string[]
234
- ): string | undefined {
318
+ function getStringFlag(flags: Record<string, string | true>, ...names: string[]): string | undefined {
235
319
  for (const name of names) {
236
320
  const value = flags[name];
237
321
  if (typeof value === 'string' && value.length > 0) return value;
@@ -269,7 +353,11 @@ function optionalFiniteFlag(flags: Record<string, string | true>, name: string,
269
353
  return parsed;
270
354
  }
271
355
 
272
- function optionalPositiveFiniteFlag(flags: Record<string, string | true>, name: string, hint: string): number | undefined {
356
+ function optionalPositiveFiniteFlag(
357
+ flags: Record<string, string | true>,
358
+ name: string,
359
+ hint: string,
360
+ ): number | undefined {
273
361
  const parsed = optionalFiniteFlag(flags, name, hint);
274
362
  if (parsed === undefined) return undefined;
275
363
  if (parsed <= 0) {
@@ -411,14 +499,14 @@ function collectFlagValues(args: string[], name: string): string[] {
411
499
  return values;
412
500
  }
413
501
 
414
- function collectRequestedFields(
415
- args: string[],
416
- flags: Record<string, string | true>,
417
- ): string[] {
502
+ function collectRequestedFields(args: string[], flags: Record<string, string | true>): string[] {
418
503
  const requested = [
419
504
  ...collectFlagValues(args, 'field'),
420
- ...((typeof flags.fields === 'string')
421
- ? flags.fields.split(',').map((value) => value.trim()).filter(Boolean)
505
+ ...(typeof flags.fields === 'string'
506
+ ? flags.fields
507
+ .split(',')
508
+ .map((value) => value.trim())
509
+ .filter(Boolean)
422
510
  : []),
423
511
  ];
424
512
  return Array.from(new Set(requested));
@@ -453,9 +541,7 @@ function listAvailableNodeFields(node: Record<string, unknown>): string[] {
453
541
  }
454
542
 
455
543
  function summarizeHistoryResult(result: Record<string, unknown>): Record<string, unknown> {
456
- const entries = Array.isArray(result.entries)
457
- ? result.entries.filter(isRecord)
458
- : [];
544
+ const entries = Array.isArray(result.entries) ? result.entries.filter(isRecord) : [];
459
545
  const countsByOperation: Record<string, number> = {};
460
546
  let currentIndex = 0;
461
547
 
@@ -483,9 +569,7 @@ function summarizeHistoryResult(result: Record<string, unknown>): Record<string,
483
569
  }
484
570
 
485
571
  function compactHistoryResult(result: Record<string, unknown>): Record<string, unknown> {
486
- const entries = Array.isArray(result.entries)
487
- ? result.entries.filter(isRecord)
488
- : [];
572
+ const entries = Array.isArray(result.entries) ? result.entries.filter(isRecord) : [];
489
573
  return {
490
574
  totalMutations: entries.length,
491
575
  canUndo: result.canUndo === true,
@@ -504,10 +588,7 @@ function parseRecordArrayJson(raw: string, hint: string): Array<Record<string, u
504
588
  try {
505
589
  parsed = JSON.parse(raw);
506
590
  } catch (error) {
507
- die(
508
- `Invalid JSON dataset: ${error instanceof Error ? error.message : String(error)}`,
509
- hint,
510
- );
591
+ die(`Invalid JSON dataset: ${error instanceof Error ? error.message : String(error)}`, hint);
511
592
  }
512
593
 
513
594
  if (!Array.isArray(parsed) || parsed.some((item) => !isRecord(item))) {
@@ -521,10 +602,7 @@ function parseJsonValue(raw: string, label: string, hint: string): unknown {
521
602
  try {
522
603
  return JSON.parse(raw);
523
604
  } catch (error) {
524
- die(
525
- `Invalid ${label}: ${error instanceof Error ? error.message : String(error)}`,
526
- hint,
527
- );
605
+ die(`Invalid ${label}: ${error instanceof Error ? error.message : String(error)}`, hint);
528
606
  }
529
607
  }
530
608
 
@@ -553,10 +631,7 @@ async function readTextInput(
553
631
  try {
554
632
  return readFileSync(path, 'utf-8');
555
633
  } catch (error) {
556
- die(
557
- `Unable to read --${name}: ${error instanceof Error ? error.message : String(error)}`,
558
- options.hint,
559
- );
634
+ die(`Unable to read --${name}: ${error instanceof Error ? error.message : String(error)}`, options.hint);
560
635
  }
561
636
  }
562
637
 
@@ -588,10 +663,7 @@ async function readOptionalTextInput(
588
663
  try {
589
664
  return readFileSync(path, 'utf-8');
590
665
  } catch (error) {
591
- die(
592
- `Unable to read --${name}: ${error instanceof Error ? error.message : String(error)}`,
593
- options.hint,
594
- );
666
+ die(`Unable to read --${name}: ${error instanceof Error ? error.message : String(error)}`, options.hint);
595
667
  }
596
668
  }
597
669
 
@@ -634,7 +706,11 @@ async function applyStructuredNodeUpdateFlags(
634
706
  hint: 'Use: pmx-canvas node update <node-id> --spec-file ./new-spec.json',
635
707
  });
636
708
  if (specRaw !== undefined) {
637
- body.spec = parseJsonValue(specRaw, 'JSON spec', 'Use: pmx-canvas node update <node-id> --spec-file ./new-spec.json');
709
+ body.spec = parseJsonValue(
710
+ specRaw,
711
+ 'JSON spec',
712
+ 'Use: pmx-canvas node update <node-id> --spec-file ./new-spec.json',
713
+ );
638
714
  }
639
715
 
640
716
  const graphPatch = await buildGraphRequestBody(flags, { requireData: false, allowStdin: false });
@@ -643,11 +719,8 @@ async function applyStructuredNodeUpdateFlags(
643
719
  }
644
720
  }
645
721
 
646
- async function buildJsonRenderRequestBody(
647
- flags: Record<string, string | true>,
648
- ): Promise<Record<string, unknown>> {
649
- const hint =
650
- 'Use: pmx-canvas node add --type json-render --spec-file ./dashboard.json --title "Ops Dashboard"';
722
+ async function buildJsonRenderRequestBody(flags: Record<string, string | true>): Promise<Record<string, unknown>> {
723
+ const hint = 'Use: pmx-canvas node add --type json-render --spec-file ./dashboard.json --title "Ops Dashboard"';
651
724
  const title = typeof flags.title === 'string' ? flags.title.trim() : '';
652
725
 
653
726
  const rawSpec = await readTextInput(flags, {
@@ -671,9 +744,7 @@ async function buildJsonRenderRequestBody(
671
744
  return body;
672
745
  }
673
746
 
674
- async function buildHtmlPrimitiveRequestBody(
675
- flags: Record<string, string | true>,
676
- ): Promise<Record<string, unknown>> {
747
+ async function buildHtmlPrimitiveRequestBody(flags: Record<string, string | true>): Promise<Record<string, unknown>> {
677
748
  const hint = 'Use: pmx-canvas html primitive add --kind choice-grid --data-file ./primitive.json --title "Options"';
678
749
  const kind = getStringFlag(flags, 'kind', 'primitive');
679
750
  if (!kind) die('HTML primitives require --kind.', hint);
@@ -760,7 +831,11 @@ async function buildGraphRequestBody(
760
831
  if (showLegend !== undefined) body.showLegend = showLegend;
761
832
  if (showLabels !== undefined) body.showLabels = showLabels;
762
833
 
763
- const chartHeight = optionalPositiveFiniteFlag(flags, 'chart-height', 'Use a positive number, e.g. --chart-height 300');
834
+ const chartHeight = optionalPositiveFiniteFlag(
835
+ flags,
836
+ 'chart-height',
837
+ 'Use a positive number, e.g. --chart-height 300',
838
+ );
764
839
  const x = optionalFiniteFlag(flags, 'x', 'Use a finite number, e.g. --x 500');
765
840
  const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 300');
766
841
  const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 760');
@@ -780,9 +855,7 @@ async function buildGraphRequestBody(
780
855
  return body;
781
856
  }
782
857
 
783
- async function buildWebArtifactRequestBody(
784
- flags: Record<string, string | true>,
785
- ): Promise<Record<string, unknown>> {
858
+ async function buildWebArtifactRequestBody(flags: Record<string, string | true>): Promise<Record<string, unknown>> {
786
859
  const hint = 'Use: pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx';
787
860
  const title = requireFlag(flags, 'title', hint);
788
861
  const appTsx = await readTextInput(flags, {
@@ -904,9 +977,7 @@ function printNodeSchemaHelp(schema: CanvasSchemaType): void {
904
977
  console.log(`Endpoint: ${schema.endpoint}`);
905
978
  console.log('Flags:');
906
979
  for (const field of schema.fields) {
907
- const aliases = field.aliases?.length
908
- ? ` (aliases: ${field.aliases.map((alias) => `--${alias}`).join(', ')})`
909
- : '';
980
+ const aliases = field.aliases?.length ? ` (aliases: ${field.aliases.map((alias) => `--${alias}`).join(', ')})` : '';
910
981
  console.log(
911
982
  ` --${field.name}${field.required ? ' [required]' : ''} <${field.type}> ${field.description}${aliases}`,
912
983
  );
@@ -936,13 +1007,19 @@ async function showNodeAddTypeHelp(flags: Record<string, string | true>): Promis
936
1007
  if (componentName) {
937
1008
  const component = schema.jsonRender.components.find((entry) => entry.type === componentName);
938
1009
  if (!component) {
939
- die(`Unknown json-render component: ${componentName}`, 'Run: pmx-canvas node schema --type json-render --summary');
1010
+ die(
1011
+ `Unknown json-render component: ${componentName}`,
1012
+ 'Run: pmx-canvas node schema --type json-render --summary',
1013
+ );
940
1014
  }
941
1015
  const requestedField = getStringFlag(flags, 'field');
942
1016
  if (requestedField) {
943
1017
  const prop = component.props.find((entry) => entry.name === requestedField);
944
1018
  if (!prop) {
945
- die(`Unknown json-render prop: ${requestedField}`, `Run: pmx-canvas node schema --type json-render --component ${componentName}`);
1019
+ die(
1020
+ `Unknown json-render prop: ${requestedField}`,
1021
+ `Run: pmx-canvas node schema --type json-render --component ${componentName}`,
1022
+ );
946
1023
  }
947
1024
  payload = {
948
1025
  command: 'node add',
@@ -1061,7 +1138,10 @@ function filterJsonRenderSchemaView(
1061
1138
  if (requestedField) {
1062
1139
  const prop = component.props.find((entry) => entry.name === requestedField);
1063
1140
  if (!prop) {
1064
- die(`Unknown json-render prop: ${requestedField}`, `Run: pmx-canvas node schema --type json-render --component ${componentName}`);
1141
+ die(
1142
+ `Unknown json-render prop: ${requestedField}`,
1143
+ `Run: pmx-canvas node schema --type json-render --component ${componentName}`,
1144
+ );
1065
1145
  }
1066
1146
  return {
1067
1147
  component: componentName,
@@ -1072,7 +1152,9 @@ function filterJsonRenderSchemaView(
1072
1152
  return flags.summary ? summarizeJsonRenderComponent(component) : component;
1073
1153
  }
1074
1154
 
1075
- function summarizeHtmlPrimitive(primitive: NonNullable<CanvasSchemaResponse['htmlPrimitives']>[number]): Record<string, unknown> {
1155
+ function summarizeHtmlPrimitive(
1156
+ primitive: NonNullable<CanvasSchemaResponse['htmlPrimitives']>[number],
1157
+ ): Record<string, unknown> {
1076
1158
  return {
1077
1159
  kind: primitive.kind,
1078
1160
  title: primitive.title,
@@ -1138,18 +1220,11 @@ const RESOURCE_SUBCOMMAND_HINTS: Record<string, Record<string, string>> = {
1138
1220
  },
1139
1221
  };
1140
1222
 
1141
- function cmd(
1142
- name: string,
1143
- help: string,
1144
- examples: string[],
1145
- run: (args: string[]) => Promise<void>,
1146
- ) {
1223
+ function cmd(name: string, help: string, examples: string[], run: (args: string[]) => Promise<void>) {
1147
1224
  COMMANDS[name] = { run, help, examples };
1148
1225
  }
1149
1226
 
1150
- cmd('open', 'Open the current workbench in the browser', [
1151
- 'pmx-canvas open',
1152
- ], async (args) => {
1227
+ cmd('open', 'Open the current workbench in the browser', ['pmx-canvas open'], async (args) => {
1153
1228
  const { flags } = parseFlags(args);
1154
1229
  if (flags.help || flags.h) return showCommandHelp('open');
1155
1230
 
@@ -1174,486 +1249,547 @@ cmd('open', 'Open the current workbench in the browser', [
1174
1249
  });
1175
1250
 
1176
1251
  // ── node add ─────────────────────────────────────────────────
1177
- cmd('node add', 'Add a node to the canvas', [
1178
- 'pmx-canvas node add --type markdown --title "Design Doc" --content "# Overview"',
1179
- 'pmx-canvas node add --type status --title "Build" --content "passing"',
1180
- 'pmx-canvas node add --type file --content "src/index.ts"',
1181
- 'pmx-canvas node add --type webpage --url "https://example.com/docs"',
1182
- 'pmx-canvas node add --type html --title "Widget" --content "<main>Hello</main>"',
1183
- 'pmx-canvas node add --type html --title "Showcase" --content ./report.html (a .html path is read from disk; otherwise --content is raw HTML)',
1184
- 'pmx-canvas node add --type html --primitive choice-grid --data-file ./options.json --title "Options"',
1185
- 'pmx-canvas node add --type markdown --title "Note" --x 100 --y 200',
1186
- 'pmx-canvas node add --type json-render --title "Ops Dashboard" --spec-file ./dashboard.json',
1187
- 'pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
1188
- 'pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx',
1189
- ], async (args) => {
1190
- const { flags } = parseFlags(args);
1191
- if (flags.help || flags.h) return showNodeAddTypeHelp(flags);
1192
-
1193
- const type = (flags.type as string) || 'markdown';
1194
-
1195
- if (type === 'json-render') {
1196
- const result = await invokeOperation('jsonrender.add', await buildJsonRenderRequestBody(flags));
1197
- output(result);
1198
- return;
1199
- }
1200
-
1201
- if (type === 'graph') {
1202
- const result = await invokeOperation('graph.add', await buildGraphRequestBody(flags));
1203
- output(result);
1204
- return;
1205
- }
1252
+ cmd(
1253
+ 'node add',
1254
+ 'Add a node to the canvas',
1255
+ [
1256
+ 'pmx-canvas node add --type markdown --title "Design Doc" --content "# Overview"',
1257
+ 'pmx-canvas node add --type status --title "Build" --content "passing"',
1258
+ 'pmx-canvas node add --type file --content "src/index.ts"',
1259
+ 'pmx-canvas node add --type webpage --url "https://example.com/docs"',
1260
+ 'pmx-canvas node add --type html --title "Widget" --content "<main>Hello</main>"',
1261
+ 'pmx-canvas node add --type html --title "Showcase" --content ./report.html (a .html path is read from disk; otherwise --content is raw HTML)',
1262
+ 'pmx-canvas node add --type html --primitive choice-grid --data-file ./options.json --title "Options"',
1263
+ 'pmx-canvas node add --type markdown --title "Note" --x 100 --y 200',
1264
+ 'pmx-canvas node add --type json-render --title "Ops Dashboard" --spec-file ./dashboard.json',
1265
+ 'pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
1266
+ 'pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx',
1267
+ ],
1268
+ async (args) => {
1269
+ const { flags } = parseFlags(args);
1270
+ if (flags.help || flags.h) return showNodeAddTypeHelp(flags);
1271
+
1272
+ const type = (flags.type as string) || 'markdown';
1273
+
1274
+ if (type === 'json-render') {
1275
+ const result = await invokeOperation('jsonrender.add', await buildJsonRenderRequestBody(flags));
1276
+ output(result);
1277
+ return;
1278
+ }
1206
1279
 
1207
- if (type === 'web-artifact') {
1208
- await runWebArtifactBuildCommand(flags);
1209
- return;
1210
- }
1280
+ if (type === 'graph') {
1281
+ const result = await invokeOperation('graph.add', await buildGraphRequestBody(flags));
1282
+ output(result);
1283
+ return;
1284
+ }
1211
1285
 
1212
- if (type === 'html-primitive') {
1213
- const result = await invokeOperation('node.add', await buildHtmlPrimitiveRequestBody(flags));
1214
- output(result);
1215
- return;
1216
- }
1286
+ if (type === 'web-artifact') {
1287
+ await runWebArtifactBuildCommand(flags);
1288
+ return;
1289
+ }
1217
1290
 
1218
- if (type === 'html' && getStringFlag(flags, 'primitive', 'kind')) {
1219
- const result = await invokeOperation('node.add', await buildHtmlPrimitiveRequestBody(flags));
1220
- output(result);
1221
- return;
1222
- }
1291
+ if (type === 'html-primitive') {
1292
+ const result = await invokeOperation('node.add', await buildHtmlPrimitiveRequestBody(flags));
1293
+ output(result);
1294
+ return;
1295
+ }
1223
1296
 
1224
- if (type === 'mcp-app') {
1225
- die(
1226
- 'mcp-app nodes require tool-backed app metadata and cannot be created with generic node add.',
1227
- 'Use: pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx, or pmx-canvas external-app add --kind excalidraw --title "Diagram"',
1228
- );
1229
- }
1297
+ if (type === 'html' && getStringFlag(flags, 'primitive', 'kind')) {
1298
+ const result = await invokeOperation('node.add', await buildHtmlPrimitiveRequestBody(flags));
1299
+ output(result);
1300
+ return;
1301
+ }
1230
1302
 
1231
- const body: Record<string, unknown> = { type };
1232
- if (flags.title) body.title = flags.title;
1233
- const webpageUrl = getStringFlag(flags, 'url');
1234
- const imagePath = getStringFlag(flags, 'path');
1235
- if (type === 'webpage' && webpageUrl) {
1236
- body.url = webpageUrl;
1237
- } else if (type === 'image' && imagePath && !flags.content) {
1238
- body.content = imagePath;
1239
- } else if (type === 'html') {
1240
- const html = getStringFlag(flags, 'html') ?? getStringFlag(flags, 'content');
1241
- if (html !== undefined) body.html = html;
1242
- const summary = getStringFlag(flags, 'summary');
1243
- const agentSummary = getStringFlag(flags, 'agent-summary', 'agentSummary');
1244
- const description = getStringFlag(flags, 'description');
1245
- if (summary !== undefined) body.summary = summary;
1246
- if (agentSummary !== undefined) body.agentSummary = agentSummary;
1247
- if (description !== undefined) body.description = description;
1248
- if (optionalBooleanFlag(flags, 'presentation', 'Use --presentation true or --presentation false') === true) body.presentation = true;
1249
- if (typeof flags['slide-title'] === 'string') body.slideTitles = [flags['slide-title']];
1250
- if (typeof flags['embedded-node-id'] === 'string') body.embeddedNodeIds = [flags['embedded-node-id']];
1251
- } else if (flags.content) {
1252
- body.content = flags.content;
1253
- }
1254
- applyCommonGeometryFlags(body, flags, {
1255
- x: 'Use a finite number, e.g. --x 500',
1256
- y: 'Use a finite number, e.g. --y 300',
1257
- width: 'Use a positive number, e.g. --width 500',
1258
- height: 'Use a positive number, e.g. --height 280',
1259
- });
1260
- applyStrictSizeFlags(body, flags);
1261
- if (type === 'trace') {
1262
- for (const field of TRACE_NODE_FIELDS) {
1263
- const value = getStringFlag(flags, field, field.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`));
1264
- if (value !== undefined) body[field] = value;
1303
+ if (type === 'mcp-app') {
1304
+ die(
1305
+ 'mcp-app nodes require tool-backed app metadata and cannot be created with generic node add.',
1306
+ 'Use: pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx, or pmx-canvas external-app add --kind excalidraw --title "Diagram"',
1307
+ );
1265
1308
  }
1266
- }
1267
1309
 
1268
- // Support --stdin for piping content
1269
- if (flags.stdin) {
1270
- if (type === 'webpage') {
1271
- body.url = await readStdin();
1310
+ const body: Record<string, unknown> = { type };
1311
+ if (flags.title) body.title = flags.title;
1312
+ const webpageUrl = getStringFlag(flags, 'url');
1313
+ const imagePath = getStringFlag(flags, 'path');
1314
+ if (type === 'webpage' && webpageUrl) {
1315
+ body.url = webpageUrl;
1316
+ } else if (type === 'image' && imagePath && !flags.content) {
1317
+ body.content = imagePath;
1272
1318
  } else if (type === 'html') {
1273
- body.html = await readStdin();
1274
- } else {
1275
- body.content = await readStdin();
1319
+ const html = getStringFlag(flags, 'html') ?? getStringFlag(flags, 'content');
1320
+ if (html !== undefined) body.html = html;
1321
+ const summary = getStringFlag(flags, 'summary');
1322
+ const agentSummary = getStringFlag(flags, 'agent-summary', 'agentSummary');
1323
+ const description = getStringFlag(flags, 'description');
1324
+ if (summary !== undefined) body.summary = summary;
1325
+ if (agentSummary !== undefined) body.agentSummary = agentSummary;
1326
+ if (description !== undefined) body.description = description;
1327
+ if (optionalBooleanFlag(flags, 'presentation', 'Use --presentation true or --presentation false') === true)
1328
+ body.presentation = true;
1329
+ if (typeof flags['slide-title'] === 'string') body.slideTitles = [flags['slide-title']];
1330
+ if (typeof flags['embedded-node-id'] === 'string') body.embeddedNodeIds = [flags['embedded-node-id']];
1331
+ } else if (flags.content) {
1332
+ body.content = flags.content;
1333
+ }
1334
+ applyCommonGeometryFlags(body, flags, {
1335
+ x: 'Use a finite number, e.g. --x 500',
1336
+ y: 'Use a finite number, e.g. --y 300',
1337
+ width: 'Use a positive number, e.g. --width 500',
1338
+ height: 'Use a positive number, e.g. --height 280',
1339
+ });
1340
+ applyStrictSizeFlags(body, flags);
1341
+ if (type === 'trace') {
1342
+ for (const field of TRACE_NODE_FIELDS) {
1343
+ const value = getStringFlag(
1344
+ flags,
1345
+ field,
1346
+ field.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`),
1347
+ );
1348
+ if (value !== undefined) body[field] = value;
1349
+ }
1276
1350
  }
1277
- }
1278
-
1279
- const result = await invokeOperation('node.add', body);
1280
- output(result);
1281
- });
1282
-
1283
- cmd('json-render', 'Show json-render schema and canonical examples', [
1284
- 'pmx-canvas json-render --schema --summary',
1285
- 'pmx-canvas json-render --examples',
1286
- 'pmx-canvas json-render --example --component Table',
1287
- 'pmx-canvas json-render --schema --component Badge --field variant',
1288
- ], async (args) => {
1289
- const { flags } = parseFlags(args);
1290
- if (flags.help || flags.h) return showCommandHelp('json-render');
1291
1351
 
1292
- const schema = await loadCanvasSchema();
1293
- const componentName = getStringFlag(flags, 'component');
1294
- const fieldName = getStringFlag(flags, 'field');
1352
+ // Support --stdin for piping content
1353
+ if (flags.stdin) {
1354
+ if (type === 'webpage') {
1355
+ body.url = await readStdin();
1356
+ } else if (type === 'html') {
1357
+ body.html = await readStdin();
1358
+ } else {
1359
+ body.content = await readStdin();
1360
+ }
1361
+ }
1295
1362
 
1296
- if (flags.example || flags.examples) {
1297
- if (fieldName) die('--field is only supported with --schema.', 'Use: pmx-canvas json-render --schema --component Table --field rows');
1298
- if (componentName) {
1299
- const component = schema.jsonRender.components.find((entry) => entry.type === componentName);
1300
- if (!component) die(`Unknown json-render component: ${componentName}`, 'Run: pmx-canvas json-render --schema --summary');
1301
- output({ component: component.type, example: component.example });
1363
+ const result = await invokeOperation('node.add', body);
1364
+ output(result);
1365
+ },
1366
+ );
1367
+
1368
+ cmd(
1369
+ 'json-render',
1370
+ 'Show json-render schema and canonical examples',
1371
+ [
1372
+ 'pmx-canvas json-render --schema --summary',
1373
+ 'pmx-canvas json-render --examples',
1374
+ 'pmx-canvas json-render --example --component Table',
1375
+ 'pmx-canvas json-render --schema --component Badge --field variant',
1376
+ ],
1377
+ async (args) => {
1378
+ const { flags } = parseFlags(args);
1379
+ if (flags.help || flags.h) return showCommandHelp('json-render');
1380
+
1381
+ const schema = await loadCanvasSchema();
1382
+ const componentName = getStringFlag(flags, 'component');
1383
+ const fieldName = getStringFlag(flags, 'field');
1384
+
1385
+ if (flags.example || flags.examples) {
1386
+ if (fieldName)
1387
+ die(
1388
+ '--field is only supported with --schema.',
1389
+ 'Use: pmx-canvas json-render --schema --component Table --field rows',
1390
+ );
1391
+ if (componentName) {
1392
+ const component = schema.jsonRender.components.find((entry) => entry.type === componentName);
1393
+ if (!component)
1394
+ die(`Unknown json-render component: ${componentName}`, 'Run: pmx-canvas json-render --schema --summary');
1395
+ output({ component: component.type, example: component.example });
1396
+ return;
1397
+ }
1398
+ output({
1399
+ rootShape: schema.jsonRender.rootShape,
1400
+ examples: Object.fromEntries(schema.jsonRender.components.map((entry) => [entry.type, entry.example])),
1401
+ });
1302
1402
  return;
1303
1403
  }
1304
- output({
1305
- rootShape: schema.jsonRender.rootShape,
1306
- examples: Object.fromEntries(schema.jsonRender.components.map((entry) => [entry.type, entry.example])),
1307
- });
1308
- return;
1309
- }
1310
1404
 
1311
- output(filterJsonRenderSchemaView(schema.jsonRender, flags));
1312
- });
1313
-
1314
- cmd('html primitive add', 'Create a reusable sandboxed HTML communication primitive', [
1315
- 'pmx-canvas html primitive add --kind choice-grid --data-file ./options.json --title "Options"',
1316
- 'pmx-canvas html primitive add --kind plan-timeline --data-json \'{"milestones":[{"title":"Ship","detail":"Implement and verify","status":"next"}]}\'',
1317
- 'pmx-canvas html primitive add --kind triage-board --data-file ./tickets.json --strict-size',
1318
- ], async (args) => {
1319
- const { flags } = parseFlags(args);
1320
- if (flags.help || flags.h) return showCommandHelp('html primitive add');
1321
- const result = await api('POST', '/api/canvas/node', await buildHtmlPrimitiveRequestBody(flags));
1322
- output(result);
1323
- });
1405
+ output(filterJsonRenderSchemaView(schema.jsonRender, flags));
1406
+ },
1407
+ );
1408
+
1409
+ cmd(
1410
+ 'html primitive add',
1411
+ 'Create a reusable sandboxed HTML communication primitive',
1412
+ [
1413
+ 'pmx-canvas html primitive add --kind choice-grid --data-file ./options.json --title "Options"',
1414
+ 'pmx-canvas html primitive add --kind plan-timeline --data-json \'{"milestones":[{"title":"Ship","detail":"Implement and verify","status":"next"}]}\'',
1415
+ 'pmx-canvas html primitive add --kind triage-board --data-file ./tickets.json --strict-size',
1416
+ ],
1417
+ async (args) => {
1418
+ const { flags } = parseFlags(args);
1419
+ if (flags.help || flags.h) return showCommandHelp('html primitive add');
1420
+ const result = await api('POST', '/api/canvas/node', await buildHtmlPrimitiveRequestBody(flags));
1421
+ output(result);
1422
+ },
1423
+ );
1424
+
1425
+ cmd(
1426
+ 'html primitive schema',
1427
+ 'Describe reusable HTML communication primitives',
1428
+ [
1429
+ 'pmx-canvas html primitive schema --summary',
1430
+ 'pmx-canvas html primitive schema --kind choice-grid',
1431
+ 'pmx-canvas html primitive schema --kind triage-board --summary',
1432
+ ],
1433
+ async (args) => {
1434
+ const { flags } = parseFlags(args);
1435
+ if (flags.help || flags.h) return showCommandHelp('html primitive schema');
1436
+ const schema = await loadCanvasSchema();
1437
+ output(filterHtmlPrimitiveSchemaView(schema, flags));
1438
+ },
1439
+ );
1440
+
1441
+ cmd(
1442
+ 'graph add',
1443
+ 'Add a graph node to the canvas',
1444
+ [
1445
+ 'pmx-canvas graph add --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
1446
+ 'pmx-canvas graph add --graphType composed --data \'[{"day":"Mon","visits":10,"conversion":0.4}]\' --xKey day --barKey visits --lineKey conversion',
1447
+ 'pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
1448
+ ],
1449
+ async (args) => {
1450
+ const { flags } = parseFlags(args);
1451
+ if (flags.help || flags.h) return showCommandHelp('graph add');
1324
1452
 
1325
- cmd('html primitive schema', 'Describe reusable HTML communication primitives', [
1326
- 'pmx-canvas html primitive schema --summary',
1327
- 'pmx-canvas html primitive schema --kind choice-grid',
1328
- 'pmx-canvas html primitive schema --kind triage-board --summary',
1329
- ], async (args) => {
1330
- const { flags } = parseFlags(args);
1331
- if (flags.help || flags.h) return showCommandHelp('html primitive schema');
1332
- const schema = await loadCanvasSchema();
1333
- output(filterHtmlPrimitiveSchemaView(schema, flags));
1334
- });
1453
+ const result = await invokeOperation('graph.add', await buildGraphRequestBody(flags));
1454
+ output(result);
1455
+ },
1456
+ );
1457
+
1458
+ cmd(
1459
+ 'node schema',
1460
+ 'Describe server-supported node create schemas and canonical examples',
1461
+ [
1462
+ 'pmx-canvas node schema',
1463
+ 'pmx-canvas node schema --type webpage',
1464
+ 'pmx-canvas node schema --type json-render',
1465
+ 'pmx-canvas json-render --schema --summary',
1466
+ 'pmx-canvas node schema --type json-render --component Table',
1467
+ 'pmx-canvas node schema --type webpage --field url',
1468
+ 'pmx-canvas node schema --summary',
1469
+ ],
1470
+ async (args) => {
1471
+ const { flags } = parseFlags(args);
1472
+ if (flags.help || flags.h) return showCommandHelp('node schema');
1473
+
1474
+ const result = await loadCanvasSchema();
1475
+ if (getStringFlag(flags, 'component') && flags.type !== 'json-render') {
1476
+ die('--component is only supported with --type json-render.');
1477
+ }
1335
1478
 
1336
- cmd('graph add', 'Add a graph node to the canvas', [
1337
- 'pmx-canvas graph add --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
1338
- 'pmx-canvas graph add --graphType composed --data \'[{"day":"Mon","visits":10,"conversion":0.4}]\' --xKey day --barKey visits --lineKey conversion',
1339
- 'pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
1340
- ], async (args) => {
1341
- const { flags } = parseFlags(args);
1342
- if (flags.help || flags.h) return showCommandHelp('graph add');
1479
+ if (typeof flags.type !== 'string') {
1480
+ if (flags.summary) {
1481
+ output({
1482
+ source: result.source,
1483
+ version: result.version,
1484
+ nodeTypes: result.nodeTypes.map((entry) => summarizeNodeSchema(entry)),
1485
+ jsonRender: {
1486
+ componentCount: result.jsonRender.components.length,
1487
+ rootShape: result.jsonRender.rootShape,
1488
+ },
1489
+ graph: result.graph,
1490
+ htmlPrimitives: result.htmlPrimitives?.map((entry) => summarizeHtmlPrimitive(entry)) ?? [],
1491
+ mcp: result.mcp,
1492
+ });
1493
+ return;
1494
+ }
1495
+ output(result);
1496
+ return;
1497
+ }
1343
1498
 
1344
- const result = await invokeOperation('graph.add', await buildGraphRequestBody(flags));
1345
- output(result);
1346
- });
1499
+ const requested = flags.type;
1500
+ if (requested === 'json-render') {
1501
+ output(filterJsonRenderSchemaView(result.jsonRender, flags));
1502
+ return;
1503
+ }
1504
+ if (requested === 'graph') {
1505
+ const graphSchema = result.nodeTypes.find((entry) => entry.type === 'graph');
1506
+ if (graphSchema) {
1507
+ output(filterNodeSchemaView(graphSchema, flags));
1508
+ return;
1509
+ }
1510
+ output(flags.summary ? result.graph : { ...result.graph });
1511
+ return;
1512
+ }
1513
+ const nodeType = result.nodeTypes.find((entry) => entry.type === requested);
1514
+ if (nodeType) {
1515
+ output(filterNodeSchemaView(nodeType, flags));
1516
+ return;
1517
+ }
1518
+ die(`Unknown schema type: ${requested}`, 'Run: pmx-canvas node schema');
1519
+ },
1520
+ );
1347
1521
 
1348
- cmd('node schema', 'Describe server-supported node create schemas and canonical examples', [
1349
- 'pmx-canvas node schema',
1350
- 'pmx-canvas node schema --type webpage',
1351
- 'pmx-canvas node schema --type json-render',
1352
- 'pmx-canvas json-render --schema --summary',
1353
- 'pmx-canvas node schema --type json-render --component Table',
1354
- 'pmx-canvas node schema --type webpage --field url',
1355
- 'pmx-canvas node schema --summary',
1356
- ], async (args) => {
1357
- const { flags } = parseFlags(args);
1358
- if (flags.help || flags.h) return showCommandHelp('node schema');
1522
+ // ── node list ────────────────────────────────────────────────
1523
+ cmd(
1524
+ 'node list',
1525
+ 'List all nodes on the canvas',
1526
+ [
1527
+ 'pmx-canvas node list',
1528
+ 'pmx-canvas node list --type markdown',
1529
+ 'pmx-canvas node list --type mcp-app',
1530
+ 'pmx-canvas node list --ids',
1531
+ ],
1532
+ async (args) => {
1533
+ const { flags } = parseFlags(args);
1534
+ if (flags.help || flags.h) return showCommandHelp('node list');
1535
+
1536
+ const layout = (await invokeOperation('layout.get', {})) as { nodes: Array<Record<string, unknown>> };
1537
+ let nodes = layout.nodes;
1538
+
1539
+ if (flags.type && flags.type !== true) {
1540
+ nodes = nodes.filter((n) => n.type === flags.type || n.kind === flags.type);
1541
+ }
1359
1542
 
1360
- const result = await loadCanvasSchema();
1361
- if (getStringFlag(flags, 'component') && flags.type !== 'json-render') {
1362
- die('--component is only supported with --type json-render.');
1363
- }
1543
+ if (flags.ids) {
1544
+ output(nodes.map((n) => n.id));
1545
+ } else {
1546
+ const shouldSummarize = flags.summary === true || flags.compact === true || flags.type === 'mcp-app';
1547
+ output(shouldSummarize ? nodes.map((node) => summarizeNodeResult(node)) : nodes);
1548
+ }
1549
+ },
1550
+ );
1364
1551
 
1365
- if (typeof flags.type !== 'string') {
1366
- if (flags.summary) {
1552
+ // ── node get ─────────────────────────────────────────────────
1553
+ cmd(
1554
+ 'node get',
1555
+ 'Get a node by ID',
1556
+ [
1557
+ 'pmx-canvas node get <node-id>',
1558
+ 'pmx-canvas node get node-abc123',
1559
+ 'pmx-canvas node get node-abc123 --summary',
1560
+ 'pmx-canvas node get node-abc123 --field title --field graphConfig',
1561
+ ],
1562
+ async (args) => {
1563
+ const { positional, flags } = parseFlags(args);
1564
+ if (flags.help || flags.h) return showCommandHelp('node get');
1565
+
1566
+ const id = positional[0];
1567
+ if (!id) die('Missing node ID', 'pmx-canvas node get <node-id>');
1568
+
1569
+ const result = (await invokeOperation('node.get', { id })) as Record<string, unknown>;
1570
+ const requestedFields = collectRequestedFields(args, flags);
1571
+ if (requestedFields.length > 0) {
1572
+ const picked = Object.fromEntries(requestedFields.map((field) => [field, resolveNodeFieldValue(result, field)]));
1573
+ const missing = requestedFields.filter((field) => picked[field] === undefined);
1574
+ if (missing.length > 0) {
1575
+ die(
1576
+ `Unknown node field${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`,
1577
+ `Available fields: ${listAvailableNodeFields(result).join(', ')}`,
1578
+ );
1579
+ }
1367
1580
  output({
1368
- source: result.source,
1369
- version: result.version,
1370
- nodeTypes: result.nodeTypes.map((entry) => summarizeNodeSchema(entry)),
1371
- jsonRender: {
1372
- componentCount: result.jsonRender.components.length,
1373
- rootShape: result.jsonRender.rootShape,
1374
- },
1375
- graph: result.graph,
1376
- htmlPrimitives: result.htmlPrimitives?.map((entry) => summarizeHtmlPrimitive(entry)) ?? [],
1377
- mcp: result.mcp,
1581
+ id: result.id ?? id,
1582
+ fields: picked,
1378
1583
  });
1379
1584
  return;
1380
1585
  }
1381
- output(result);
1382
- return;
1383
- }
1384
1586
 
1385
- const requested = flags.type;
1386
- if (requested === 'json-render') {
1387
- output(filterJsonRenderSchemaView(result.jsonRender, flags));
1388
- return;
1389
- }
1390
- if (requested === 'graph') {
1391
- const graphSchema = result.nodeTypes.find((entry) => entry.type === 'graph');
1392
- if (graphSchema) {
1393
- output(filterNodeSchemaView(graphSchema, flags));
1587
+ if (flags.summary || flags.compact) {
1588
+ output(summarizeNodeResult(result));
1394
1589
  return;
1395
1590
  }
1396
- output(flags.summary ? result.graph : { ...result.graph });
1397
- return;
1398
- }
1399
- const nodeType = result.nodeTypes.find((entry) => entry.type === requested);
1400
- if (nodeType) {
1401
- output(filterNodeSchemaView(nodeType, flags));
1402
- return;
1403
- }
1404
- die(`Unknown schema type: ${requested}`, 'Run: pmx-canvas node schema');
1405
- });
1406
-
1407
- // ── node list ────────────────────────────────────────────────
1408
- cmd('node list', 'List all nodes on the canvas', [
1409
- 'pmx-canvas node list',
1410
- 'pmx-canvas node list --type markdown',
1411
- 'pmx-canvas node list --type mcp-app',
1412
- 'pmx-canvas node list --ids',
1413
- ], async (args) => {
1414
- const { flags } = parseFlags(args);
1415
- if (flags.help || flags.h) return showCommandHelp('node list');
1416
-
1417
- const layout = (await invokeOperation('layout.get', {})) as { nodes: Array<Record<string, unknown>> };
1418
- let nodes = layout.nodes;
1419
-
1420
- if (flags.type && flags.type !== true) {
1421
- nodes = nodes.filter((n) => n.type === flags.type || n.kind === flags.type);
1422
- }
1591
+ output(result);
1592
+ },
1593
+ );
1423
1594
 
1424
- if (flags.ids) {
1425
- output(nodes.map((n) => n.id));
1426
- } else {
1427
- const shouldSummarize =
1428
- flags.summary === true ||
1429
- flags.compact === true ||
1430
- flags.type === 'mcp-app';
1431
- output(shouldSummarize ? nodes.map((node) => summarizeNodeResult(node)) : nodes);
1432
- }
1433
- });
1595
+ // ── node update ──────────────────────────────────────────────
1596
+ cmd(
1597
+ 'node update',
1598
+ 'Update a node by ID',
1599
+ [
1600
+ 'pmx-canvas node update <node-id> --title "New Title"',
1601
+ 'pmx-canvas node update <node-id> --content "Updated content"',
1602
+ 'pmx-canvas node update <node-id> --title "Moved" --x 500 --y 300',
1603
+ 'pmx-canvas node update <node-id> --width 840 --height 620',
1604
+ 'pmx-canvas node update <node-id> --spec-file ./dashboard.json',
1605
+ 'pmx-canvas node update <graph-id> --data-file ./metrics.json --chart-height 420',
1606
+ 'pmx-canvas node update <node-id> --pinned true',
1607
+ 'pmx-canvas node update <node-id> --dock-position right',
1608
+ 'pmx-canvas node update <node-id> --dock-position none # undock back to the canvas',
1609
+ 'pmx-canvas node update <node-id> --lock-arrange',
1610
+ ],
1611
+ async (args) => {
1612
+ const { positional, flags } = parseFlags(args);
1613
+ if (flags.help || flags.h) return showCommandHelp('node update');
1614
+
1615
+ const id = positional[0];
1616
+ if (!id) die('Missing node ID', 'pmx-canvas node update <node-id> --title "New Title"');
1617
+
1618
+ const body: Record<string, unknown> = {};
1619
+ await applyStructuredNodeUpdateFlags(body, flags);
1620
+ if (flags.title && flags.title !== true) body.title = flags.title;
1621
+ if (flags.content && flags.content !== true) body.content = flags.content;
1622
+ if (flags.stdin) body.content = await readStdin();
1623
+
1624
+ const x = optionalFiniteFlag(flags, 'x', 'Use a finite number, e.g. --x 500');
1625
+ const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 300');
1626
+ const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 840');
1627
+ const height = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 620');
1628
+ const nodeHeight = optionalPositiveFiniteFlagWithAliases(
1629
+ flags,
1630
+ 'Use a positive number, e.g. --node-height 620',
1631
+ 'node-height',
1632
+ 'nodeHeight',
1633
+ );
1634
+ if (height !== undefined && nodeHeight !== undefined) {
1635
+ die('Use either --height/--node-height, not both.');
1636
+ }
1637
+ const frameHeight = height ?? nodeHeight;
1638
+ const pinned = optionalBooleanFlag(flags, 'pinned', 'Use --pinned true or --pinned false');
1639
+ if (flags['lock-arrange'] && flags['unlock-arrange']) {
1640
+ die('Use either --lock-arrange or --unlock-arrange, not both.');
1641
+ }
1642
+ const arrangeLocked = flags['lock-arrange'] ? true : flags['unlock-arrange'] ? false : undefined;
1434
1643
 
1435
- // ── node get ─────────────────────────────────────────────────
1436
- cmd('node get', 'Get a node by ID', [
1437
- 'pmx-canvas node get <node-id>',
1438
- 'pmx-canvas node get node-abc123',
1439
- 'pmx-canvas node get node-abc123 --summary',
1440
- 'pmx-canvas node get node-abc123 --field title --field graphConfig',
1441
- ], async (args) => {
1442
- const { positional, flags } = parseFlags(args);
1443
- if (flags.help || flags.h) return showCommandHelp('node get');
1644
+ applyStrictSizeFlags(body, flags);
1444
1645
 
1445
- const id = positional[0];
1446
- if (!id) die('Missing node ID', 'pmx-canvas node get <node-id>');
1447
-
1448
- const result = await invokeOperation('node.get', { id }) as Record<string, unknown>;
1449
- const requestedFields = collectRequestedFields(args, flags);
1450
- if (requestedFields.length > 0) {
1451
- const picked = Object.fromEntries(requestedFields.map((field) => [field, resolveNodeFieldValue(result, field)]));
1452
- const missing = requestedFields.filter((field) => picked[field] === undefined);
1453
- if (missing.length > 0) {
1454
- die(
1455
- `Unknown node field${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`,
1456
- `Available fields: ${listAvailableNodeFields(result).join(', ')}`,
1646
+ for (const field of TRACE_NODE_FIELDS) {
1647
+ const value = getStringFlag(
1648
+ flags,
1649
+ field,
1650
+ field.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`),
1457
1651
  );
1652
+ if (value !== undefined) body[field] = value;
1458
1653
  }
1459
- output({
1460
- id: result.id ?? id,
1461
- fields: picked,
1462
- });
1463
- return;
1464
- }
1465
1654
 
1466
- if (flags.summary || flags.compact) {
1467
- output(summarizeNodeResult(result));
1468
- return;
1469
- }
1470
- output(result);
1471
- });
1655
+ if (
1656
+ x !== undefined ||
1657
+ y !== undefined ||
1658
+ width !== undefined ||
1659
+ frameHeight !== undefined ||
1660
+ arrangeLocked !== undefined
1661
+ ) {
1662
+ const existing = (await invokeOperation('node.get', { id })) as {
1663
+ position: { x: number; y: number };
1664
+ size: { width: number; height: number };
1665
+ data: Record<string, unknown>;
1666
+ };
1472
1667
 
1473
- // ── node update ──────────────────────────────────────────────
1474
- cmd('node update', 'Update a node by ID', [
1475
- 'pmx-canvas node update <node-id> --title "New Title"',
1476
- 'pmx-canvas node update <node-id> --content "Updated content"',
1477
- 'pmx-canvas node update <node-id> --title "Moved" --x 500 --y 300',
1478
- 'pmx-canvas node update <node-id> --width 840 --height 620',
1479
- 'pmx-canvas node update <node-id> --spec-file ./dashboard.json',
1480
- 'pmx-canvas node update <graph-id> --data-file ./metrics.json --chart-height 420',
1481
- 'pmx-canvas node update <node-id> --pinned true',
1482
- 'pmx-canvas node update <node-id> --dock-position right',
1483
- 'pmx-canvas node update <node-id> --dock-position none # undock back to the canvas',
1484
- 'pmx-canvas node update <node-id> --lock-arrange',
1485
- ], async (args) => {
1486
- const { positional, flags } = parseFlags(args);
1487
- if (flags.help || flags.h) return showCommandHelp('node update');
1668
+ if (x !== undefined || y !== undefined) {
1669
+ body.position = {
1670
+ x: x ?? existing.position.x,
1671
+ y: y ?? existing.position.y,
1672
+ };
1673
+ }
1488
1674
 
1489
- const id = positional[0];
1490
- if (!id) die('Missing node ID', 'pmx-canvas node update <node-id> --title "New Title"');
1675
+ if (width !== undefined || frameHeight !== undefined) {
1676
+ body.size = {
1677
+ width: width ?? existing.size.width,
1678
+ height: frameHeight ?? existing.size.height,
1679
+ };
1680
+ }
1491
1681
 
1492
- const body: Record<string, unknown> = {};
1493
- await applyStructuredNodeUpdateFlags(body, flags);
1494
- if (flags.title && flags.title !== true) body.title = flags.title;
1495
- if (flags.content && flags.content !== true) body.content = flags.content;
1496
- if (flags.stdin) body.content = await readStdin();
1682
+ if (arrangeLocked !== undefined) {
1683
+ body.arrangeLocked = arrangeLocked;
1684
+ }
1685
+ }
1497
1686
 
1498
- const x = optionalFiniteFlag(flags, 'x', 'Use a finite number, e.g. --x 500');
1499
- const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 300');
1500
- const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 840');
1501
- const height = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 620');
1502
- const nodeHeight = optionalPositiveFiniteFlagWithAliases(
1503
- flags,
1504
- 'Use a positive number, e.g. --node-height 620',
1505
- 'node-height',
1506
- 'nodeHeight',
1507
- );
1508
- if (height !== undefined && nodeHeight !== undefined) {
1509
- die('Use either --height/--node-height, not both.');
1510
- }
1511
- const frameHeight = height ?? nodeHeight;
1512
- const pinned = optionalBooleanFlag(flags, 'pinned', 'Use --pinned true or --pinned false');
1513
- if (flags['lock-arrange'] && flags['unlock-arrange']) {
1514
- die('Use either --lock-arrange or --unlock-arrange, not both.');
1515
- }
1516
- const arrangeLocked = flags['lock-arrange']
1517
- ? true
1518
- : flags['unlock-arrange']
1519
- ? false
1520
- : undefined;
1687
+ if (pinned !== undefined) body.pinned = pinned;
1688
+
1689
+ // --dock-position left|right|none : dock a node into the top HUD or undock it.
1690
+ // `none`/`null`/empty map to JS null (undock). Assigned with a !== undefined
1691
+ // guard so the null survives JSON.stringify to the server (which accepts a
1692
+ // top-level dockPosition: null). HTTP PATCH already supports this; this is the
1693
+ // CLI path the report (#40) found missing.
1694
+ const dockRaw = getStringFlag(flags, 'dock-position', 'dockPosition');
1695
+ let dockPosition: 'left' | 'right' | null | undefined;
1696
+ if (dockRaw !== undefined) {
1697
+ const v = dockRaw.trim().toLowerCase();
1698
+ if (v === 'left' || v === 'right') dockPosition = v;
1699
+ else if (v === 'none' || v === 'null' || v === '') dockPosition = null;
1700
+ else die(`Invalid --dock-position "${dockRaw}".`, 'Use left, right, or none (to undock).');
1701
+ }
1702
+ if (dockPosition !== undefined) body.dockPosition = dockPosition;
1521
1703
 
1522
- applyStrictSizeFlags(body, flags);
1704
+ if (Object.keys(body).length === 0) {
1705
+ die(
1706
+ 'No updates specified',
1707
+ 'Use --title, --content, --x, --y, --width, --height, --strict-size, --pinned, --dock-position, trace fields, --lock-arrange, --unlock-arrange, or --stdin',
1708
+ );
1709
+ }
1523
1710
 
1524
- for (const field of TRACE_NODE_FIELDS) {
1525
- const value = getStringFlag(flags, field, field.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`));
1526
- if (value !== undefined) body[field] = value;
1527
- }
1711
+ const result = await invokeOperation('node.update', { id, ...body });
1712
+ output(result);
1713
+ },
1714
+ );
1528
1715
 
1529
- if (x !== undefined || y !== undefined || width !== undefined || frameHeight !== undefined || arrangeLocked !== undefined) {
1530
- const existing = await invokeOperation('node.get', { id }) as {
1531
- position: { x: number; y: number };
1532
- size: { width: number; height: number };
1533
- data: Record<string, unknown>;
1534
- };
1716
+ // ── node remove ──────────────────────────────────────────────
1717
+ cmd(
1718
+ 'node remove',
1719
+ 'Remove a node from the canvas',
1720
+ ['pmx-canvas node remove <node-id>', 'pmx-canvas node remove node-abc123'],
1721
+ async (args) => {
1722
+ const { positional, flags } = parseFlags(args);
1723
+ if (flags.help || flags.h) return showCommandHelp('node remove');
1724
+
1725
+ const id = positional[0];
1726
+ if (!id) die('Missing node ID', 'pmx-canvas node remove <node-id>');
1727
+
1728
+ const result = await invokeOperation('node.remove', { id });
1729
+ output(result);
1730
+ },
1731
+ );
1535
1732
 
1536
- if (x !== undefined || y !== undefined) {
1537
- body.position = {
1538
- x: x ?? existing.position.x,
1539
- y: y ?? existing.position.y,
1540
- };
1733
+ // ── edge add ─────────────────────────────────────────────────
1734
+ cmd(
1735
+ 'edge add',
1736
+ 'Add an edge between two nodes',
1737
+ [
1738
+ 'pmx-canvas edge add --from <node-id> --to <node-id> --type flow',
1739
+ 'pmx-canvas edge add --from-search "DVT O3 — GitOps" --to-search "deep work trend" --type relation',
1740
+ 'pmx-canvas edge add --from n1 --to n2 --type depends-on --label "imports"',
1741
+ 'pmx-canvas edge add --from n1 --to n2 --type references --style dashed --animated',
1742
+ ],
1743
+ async (args) => {
1744
+ const { flags } = parseFlags(args);
1745
+ if (flags.help || flags.h) return showCommandHelp('edge add');
1746
+
1747
+ const type = (flags.type as string) || 'flow';
1748
+ const from = typeof flags.from === 'string' ? flags.from : undefined;
1749
+ const to = typeof flags.to === 'string' ? flags.to : undefined;
1750
+ const fromSearch = typeof flags['from-search'] === 'string' ? flags['from-search'] : undefined;
1751
+ const toSearch = typeof flags['to-search'] === 'string' ? flags['to-search'] : undefined;
1752
+
1753
+ if (!from && !fromSearch) {
1754
+ die(
1755
+ 'Missing source selector',
1756
+ 'Use --from <id> or --from-search "query". Search queries must resolve to exactly one node. Example: pmx-canvas edge add --from-search "DVT O3 — GitOps" --to-search "deep work trend" --type relation',
1757
+ );
1541
1758
  }
1542
-
1543
- if (width !== undefined || frameHeight !== undefined) {
1544
- body.size = {
1545
- width: width ?? existing.size.width,
1546
- height: frameHeight ?? existing.size.height,
1547
- };
1759
+ if (!to && !toSearch) {
1760
+ die(
1761
+ 'Missing target selector',
1762
+ 'Use --to <id> or --to-search "query". Search queries must resolve to exactly one node. Example: pmx-canvas edge add --from-search "DVT O3 — GitOps" --to-search "deep work trend" --type relation',
1763
+ );
1548
1764
  }
1549
1765
 
1550
- if (arrangeLocked !== undefined) {
1551
- body.arrangeLocked = arrangeLocked;
1552
- }
1553
- }
1766
+ const body: Record<string, unknown> = {
1767
+ type,
1768
+ ...(from ? { from } : {}),
1769
+ ...(to ? { to } : {}),
1770
+ ...(fromSearch ? { fromSearch } : {}),
1771
+ ...(toSearch ? { toSearch } : {}),
1772
+ };
1773
+ if (flags.label && flags.label !== true) body.label = flags.label;
1774
+ if (typeof flags.style === 'string') body.style = flags.style;
1775
+ if (flags.animated) body.animated = true;
1554
1776
 
1555
- if (pinned !== undefined) body.pinned = pinned;
1777
+ const result = await invokeOperation('edge.add', body);
1778
+ output(result);
1779
+ },
1780
+ );
1556
1781
 
1557
- // --dock-position left|right|none : dock a node into the top HUD or undock it.
1558
- // `none`/`null`/empty map to JS null (undock). Assigned with a !== undefined
1559
- // guard so the null survives JSON.stringify to the server (which accepts a
1560
- // top-level dockPosition: null). HTTP PATCH already supports this; this is the
1561
- // CLI path the report (#40) found missing.
1562
- const dockRaw = getStringFlag(flags, 'dock-position', 'dockPosition');
1563
- let dockPosition: 'left' | 'right' | null | undefined;
1564
- if (dockRaw !== undefined) {
1565
- const v = dockRaw.trim().toLowerCase();
1566
- if (v === 'left' || v === 'right') dockPosition = v;
1567
- else if (v === 'none' || v === 'null' || v === '') dockPosition = null;
1568
- else die(`Invalid --dock-position "${dockRaw}".`, 'Use left, right, or none (to undock).');
1569
- }
1570
- if (dockPosition !== undefined) body.dockPosition = dockPosition;
1782
+ // ── edge list ────────────────────────────────────────────────
1783
+ cmd('edge list', 'List all edges on the canvas', ['pmx-canvas edge list'], async (args) => {
1784
+ const { flags } = parseFlags(args);
1785
+ if (flags.help || flags.h) return showCommandHelp('edge list');
1571
1786
 
1572
- if (Object.keys(body).length === 0) {
1573
- die(
1574
- 'No updates specified',
1575
- 'Use --title, --content, --x, --y, --width, --height, --strict-size, --pinned, --dock-position, trace fields, --lock-arrange, --unlock-arrange, or --stdin',
1576
- );
1577
- }
1578
-
1579
- const result = await invokeOperation('node.update', { id, ...body });
1580
- output(result);
1581
- });
1582
-
1583
- // ── node remove ──────────────────────────────────────────────
1584
- cmd('node remove', 'Remove a node from the canvas', [
1585
- 'pmx-canvas node remove <node-id>',
1586
- 'pmx-canvas node remove node-abc123',
1587
- ], async (args) => {
1588
- const { positional, flags } = parseFlags(args);
1589
- if (flags.help || flags.h) return showCommandHelp('node remove');
1590
-
1591
- const id = positional[0];
1592
- if (!id) die('Missing node ID', 'pmx-canvas node remove <node-id>');
1593
-
1594
- const result = await invokeOperation('node.remove', { id });
1595
- output(result);
1596
- });
1597
-
1598
- // ── edge add ─────────────────────────────────────────────────
1599
- cmd('edge add', 'Add an edge between two nodes', [
1600
- 'pmx-canvas edge add --from <node-id> --to <node-id> --type flow',
1601
- 'pmx-canvas edge add --from-search "DVT O3 — GitOps" --to-search "deep work trend" --type relation',
1602
- 'pmx-canvas edge add --from n1 --to n2 --type depends-on --label "imports"',
1603
- 'pmx-canvas edge add --from n1 --to n2 --type references --style dashed --animated',
1604
- ], async (args) => {
1605
- const { flags } = parseFlags(args);
1606
- if (flags.help || flags.h) return showCommandHelp('edge add');
1607
-
1608
- const type = (flags.type as string) || 'flow';
1609
- const from = typeof flags.from === 'string' ? flags.from : undefined;
1610
- const to = typeof flags.to === 'string' ? flags.to : undefined;
1611
- const fromSearch = typeof flags['from-search'] === 'string' ? flags['from-search'] : undefined;
1612
- const toSearch = typeof flags['to-search'] === 'string' ? flags['to-search'] : undefined;
1613
-
1614
- if (!from && !fromSearch) {
1615
- die(
1616
- 'Missing source selector',
1617
- 'Use --from <id> or --from-search "query". Search queries must resolve to exactly one node. Example: pmx-canvas edge add --from-search "DVT O3 — GitOps" --to-search "deep work trend" --type relation',
1618
- );
1619
- }
1620
- if (!to && !toSearch) {
1621
- die(
1622
- 'Missing target selector',
1623
- 'Use --to <id> or --to-search "query". Search queries must resolve to exactly one node. Example: pmx-canvas edge add --from-search "DVT O3 — GitOps" --to-search "deep work trend" --type relation',
1624
- );
1625
- }
1626
-
1627
- const body: Record<string, unknown> = {
1628
- type,
1629
- ...(from ? { from } : {}),
1630
- ...(to ? { to } : {}),
1631
- ...(fromSearch ? { fromSearch } : {}),
1632
- ...(toSearch ? { toSearch } : {}),
1633
- };
1634
- if (flags.label && flags.label !== true) body.label = flags.label;
1635
- if (typeof flags.style === 'string') body.style = flags.style;
1636
- if (flags.animated) body.animated = true;
1637
-
1638
- const result = await invokeOperation('edge.add', body);
1639
- output(result);
1640
- });
1641
-
1642
- // ── edge list ────────────────────────────────────────────────
1643
- cmd('edge list', 'List all edges on the canvas', [
1644
- 'pmx-canvas edge list',
1645
- ], async (args) => {
1646
- const { flags } = parseFlags(args);
1647
- if (flags.help || flags.h) return showCommandHelp('edge list');
1648
-
1649
- const layout = (await api('GET', '/api/canvas/state')) as { edges: unknown[] };
1650
- output(layout.edges);
1651
- });
1787
+ const layout = (await api('GET', '/api/canvas/state')) as { edges: unknown[] };
1788
+ output(layout.edges);
1789
+ });
1652
1790
 
1653
1791
  // ── edge remove ──────────────────────────────────────────────
1654
- cmd('edge remove', 'Remove an edge by ID', [
1655
- 'pmx-canvas edge remove <edge-id>',
1656
- ], async (args) => {
1792
+ cmd('edge remove', 'Remove an edge by ID', ['pmx-canvas edge remove <edge-id>'], async (args) => {
1657
1793
  const { positional, flags } = parseFlags(args);
1658
1794
  if (flags.help || flags.h) return showCommandHelp('edge remove');
1659
1795
 
@@ -1665,40 +1801,42 @@ cmd('edge remove', 'Remove an edge by ID', [
1665
1801
  });
1666
1802
 
1667
1803
  // ── search ───────────────────────────────────────────────────
1668
- cmd('search', 'Search nodes by title or content', [
1669
- 'pmx-canvas search "design doc"',
1670
- 'pmx-canvas search --query "TODO"',
1671
- ], async (args) => {
1672
- const { positional, flags } = parseFlags(args);
1673
- if (flags.help || flags.h) return showCommandHelp('search');
1674
-
1675
- const query = positional[0] || (typeof flags.query === 'string' ? flags.query : '');
1676
- if (!query) die('Missing search query', 'pmx-canvas search "query"');
1677
-
1678
- const result = await invokeOperation('search', { q: query });
1679
- output(result);
1680
- });
1804
+ cmd(
1805
+ 'search',
1806
+ 'Search nodes by title or content',
1807
+ ['pmx-canvas search "design doc"', 'pmx-canvas search --query "TODO"'],
1808
+ async (args) => {
1809
+ const { positional, flags } = parseFlags(args);
1810
+ if (flags.help || flags.h) return showCommandHelp('search');
1811
+
1812
+ const query = positional[0] || (typeof flags.query === 'string' ? flags.query : '');
1813
+ if (!query) die('Missing search query', 'pmx-canvas search "query"');
1814
+
1815
+ const result = await invokeOperation('search', { q: query });
1816
+ output(result);
1817
+ },
1818
+ );
1681
1819
 
1682
1820
  // ── layout ───────────────────────────────────────────────────
1683
- cmd('layout', 'Get the full canvas layout (nodes, edges, viewport)', [
1684
- 'pmx-canvas layout',
1685
- 'pmx-canvas layout --summary',
1686
- ], async (args) => {
1687
- const { flags } = parseFlags(args);
1688
- if (flags.help || flags.h) return showCommandHelp('layout');
1689
-
1690
- if (flags.summary || flags.compact) {
1691
- output(await api('GET', '/api/canvas/summary'));
1692
- return;
1693
- }
1694
- const result = await api('GET', '/api/canvas/state');
1695
- output(result);
1696
- });
1821
+ cmd(
1822
+ 'layout',
1823
+ 'Get the full canvas layout (nodes, edges, viewport)',
1824
+ ['pmx-canvas layout', 'pmx-canvas layout --summary'],
1825
+ async (args) => {
1826
+ const { flags } = parseFlags(args);
1827
+ if (flags.help || flags.h) return showCommandHelp('layout');
1828
+
1829
+ if (flags.summary || flags.compact) {
1830
+ output(await api('GET', '/api/canvas/summary'));
1831
+ return;
1832
+ }
1833
+ const result = await api('GET', '/api/canvas/state');
1834
+ output(result);
1835
+ },
1836
+ );
1697
1837
 
1698
1838
  // ── status ───────────────────────────────────────────────────
1699
- cmd('status', 'Quick canvas summary', [
1700
- 'pmx-canvas status',
1701
- ], async (args) => {
1839
+ cmd('status', 'Quick canvas summary', ['pmx-canvas status'], async (args) => {
1702
1840
  const { flags } = parseFlags(args);
1703
1841
  if (flags.help || flags.h) return showCommandHelp('status');
1704
1842
 
@@ -1712,11 +1850,12 @@ cmd('status', 'Quick canvas summary', [
1712
1850
  const typeCounts: Record<string, number> = {};
1713
1851
  for (const n of layout.nodes) {
1714
1852
  const data = isRecord(n.data) ? n.data : {};
1715
- const t = typeof n.kind === 'string'
1716
- ? n.kind
1717
- : n.type === 'mcp-app' && data.hostMode === 'hosted' && typeof data.path === 'string'
1718
- ? 'web-artifact'
1719
- : n.type as string;
1853
+ const t =
1854
+ typeof n.kind === 'string'
1855
+ ? n.kind
1856
+ : n.type === 'mcp-app' && data.hostMode === 'hosted' && typeof data.path === 'string'
1857
+ ? 'web-artifact'
1858
+ : (n.type as string);
1720
1859
  typeCounts[t] = (typeCounts[t] || 0) + 1;
1721
1860
  }
1722
1861
 
@@ -1730,25 +1869,24 @@ cmd('status', 'Quick canvas summary', [
1730
1869
  });
1731
1870
 
1732
1871
  // ── arrange ──────────────────────────────────────────────────
1733
- cmd('arrange', 'Auto-arrange nodes on the canvas', [
1734
- 'pmx-canvas arrange',
1735
- 'pmx-canvas arrange --layout column',
1736
- 'pmx-canvas arrange --layout flow',
1737
- ], async (args) => {
1738
- const { flags } = parseFlags(args);
1739
- if (flags.help || flags.h) return showCommandHelp('arrange');
1740
-
1741
- const body: Record<string, unknown> = {};
1742
- if (flags.layout && flags.layout !== true) body.layout = flags.layout;
1743
-
1744
- const result = await api('POST', '/api/canvas/arrange', body);
1745
- output(result);
1746
- });
1872
+ cmd(
1873
+ 'arrange',
1874
+ 'Auto-arrange nodes on the canvas',
1875
+ ['pmx-canvas arrange', 'pmx-canvas arrange --layout column', 'pmx-canvas arrange --layout flow'],
1876
+ async (args) => {
1877
+ const { flags } = parseFlags(args);
1878
+ if (flags.help || flags.h) return showCommandHelp('arrange');
1879
+
1880
+ const body: Record<string, unknown> = {};
1881
+ if (flags.layout && flags.layout !== true) body.layout = flags.layout;
1882
+
1883
+ const result = await api('POST', '/api/canvas/arrange', body);
1884
+ output(result);
1885
+ },
1886
+ );
1747
1887
 
1748
1888
  // ── focus ────────────────────────────────────────────────────
1749
- cmd('focus', 'Pan viewport to center on a node', [
1750
- 'pmx-canvas focus <node-id>',
1751
- ], async (args) => {
1889
+ cmd('focus', 'Pan viewport to center on a node', ['pmx-canvas focus <node-id>'], async (args) => {
1752
1890
  const { positional, flags } = parseFlags(args);
1753
1891
  if (flags.help || flags.h) return showCommandHelp('focus');
1754
1892
 
@@ -1759,269 +1897,347 @@ cmd('focus', 'Pan viewport to center on a node', [
1759
1897
  output(result);
1760
1898
  });
1761
1899
 
1762
- cmd('fit', 'Fit the viewport to all nodes or a selected subset', [
1763
- 'pmx-canvas fit',
1764
- 'pmx-canvas fit --width 1440 --height 900 --padding 80',
1765
- 'pmx-canvas fit node-a node-b',
1766
- ], async (args) => {
1767
- const { positional, flags } = parseFlags(args);
1768
- if (flags.help || flags.h) return showCommandHelp('fit');
1769
-
1770
- const body: Record<string, unknown> = {};
1771
- const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 1440');
1772
- const height = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 900');
1773
- const padding = optionalPositiveFiniteFlag(flags, 'padding', 'Use a positive number, e.g. --padding 80');
1774
- const maxScale = optionalPositiveFiniteFlag(flags, 'max-scale', 'Use a positive number, e.g. --max-scale 1');
1775
- if (width !== undefined) body.width = width;
1776
- if (height !== undefined) body.height = height;
1777
- if (padding !== undefined) body.padding = padding;
1778
- if (maxScale !== undefined) body.maxScale = maxScale;
1779
- if (positional.length > 0) body.nodeIds = positional;
1780
-
1781
- const result = await api('POST', '/api/canvas/fit', body);
1782
- output(result);
1783
- });
1900
+ cmd(
1901
+ 'fit',
1902
+ 'Fit the viewport to all nodes or a selected subset',
1903
+ ['pmx-canvas fit', 'pmx-canvas fit --width 1440 --height 900 --padding 80', 'pmx-canvas fit node-a node-b'],
1904
+ async (args) => {
1905
+ const { positional, flags } = parseFlags(args);
1906
+ if (flags.help || flags.h) return showCommandHelp('fit');
1907
+
1908
+ const body: Record<string, unknown> = {};
1909
+ const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 1440');
1910
+ const height = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 900');
1911
+ const padding = optionalPositiveFiniteFlag(flags, 'padding', 'Use a positive number, e.g. --padding 80');
1912
+ const maxScale = optionalPositiveFiniteFlag(flags, 'max-scale', 'Use a positive number, e.g. --max-scale 1');
1913
+ if (width !== undefined) body.width = width;
1914
+ if (height !== undefined) body.height = height;
1915
+ if (padding !== undefined) body.padding = padding;
1916
+ if (maxScale !== undefined) body.maxScale = maxScale;
1917
+ if (positional.length > 0) body.nodeIds = positional;
1918
+
1919
+ const result = await api('POST', '/api/canvas/fit', body);
1920
+ output(result);
1921
+ },
1922
+ );
1784
1923
 
1785
1924
  // ── external-app add ─────────────────────────────────────────
1786
- cmd('external-app add', 'Create a hosted external app node', [
1787
- 'pmx-canvas external-app add --kind excalidraw --title "Diagram"',
1788
- ], async (args) => {
1789
- const { flags } = parseFlags(args);
1790
- if (flags.help || flags.h) return showCommandHelp('external-app add');
1791
-
1792
- const kind = typeof flags.kind === 'string' ? flags.kind.trim() : '';
1793
- if (kind !== 'excalidraw') {
1794
- die('Unsupported external app kind.', 'Use: pmx-canvas external-app add --kind excalidraw --title "Diagram"');
1795
- }
1796
-
1797
- const body: Record<string, unknown> = {
1798
- title: typeof flags.title === 'string' ? flags.title : 'Excalidraw Diagram',
1799
- elements: DEFAULT_EXCALIDRAW_ELEMENTS,
1800
- };
1801
- const nodeId = getStringFlag(flags, 'node-id', 'nodeId', 'id');
1802
- if (nodeId) body.nodeId = nodeId;
1803
- const elementsJson = getStringFlag(flags, 'elements-json', 'elements');
1804
- if (elementsJson !== undefined) body.elements = parseJsonValue(elementsJson, 'Excalidraw elements', 'Use --elements-json \'[{"type":"rectangle","id":"r1","x":0,"y":0,"width":120,"height":80}]\'');
1805
- const elementsFile = getStringFlag(flags, 'elements-file', 'initial-file');
1806
- if (elementsFile) body.elements = parseJsonValue(readFileSync(elementsFile, 'utf-8'), 'Excalidraw elements file', 'Use --elements-file ./scene.excalidraw');
1807
- applyCommonGeometryFlags(body, flags, {
1808
- x: 'Use a finite number, e.g. --x 500',
1809
- y: 'Use a finite number, e.g. --y 300',
1810
- width: 'Use a positive number, e.g. --width 960',
1811
- height: 'Use a positive number, e.g. --height 720',
1812
- });
1813
- const timeoutMs = optionalPositiveFiniteFlag(flags, 'timeout-ms', 'Use a positive number, e.g. --timeout-ms 120000');
1814
- if (timeoutMs !== undefined) body.timeoutMs = timeoutMs;
1925
+ cmd(
1926
+ 'external-app add',
1927
+ 'Create a hosted external app node',
1928
+ ['pmx-canvas external-app add --kind excalidraw --title "Diagram"'],
1929
+ async (args) => {
1930
+ const { flags } = parseFlags(args);
1931
+ if (flags.help || flags.h) return showCommandHelp('external-app add');
1932
+
1933
+ const kind = typeof flags.kind === 'string' ? flags.kind.trim() : '';
1934
+ if (kind !== 'excalidraw') {
1935
+ die('Unsupported external app kind.', 'Use: pmx-canvas external-app add --kind excalidraw --title "Diagram"');
1936
+ }
1815
1937
 
1816
- const result = await api('POST', '/api/canvas/diagram', body);
1817
- output(result && typeof result === 'object' && !Array.isArray(result) && 'nodeId' in result && !('id' in result)
1818
- ? { id: (result as { nodeId?: unknown }).nodeId, ...result }
1819
- : result);
1820
- });
1938
+ const body: Record<string, unknown> = {
1939
+ title: typeof flags.title === 'string' ? flags.title : 'Excalidraw Diagram',
1940
+ elements: DEFAULT_EXCALIDRAW_ELEMENTS,
1941
+ };
1942
+ const nodeId = getStringFlag(flags, 'node-id', 'nodeId', 'id');
1943
+ if (nodeId) body.nodeId = nodeId;
1944
+ const elementsJson = getStringFlag(flags, 'elements-json', 'elements');
1945
+ if (elementsJson !== undefined)
1946
+ body.elements = parseJsonValue(
1947
+ elementsJson,
1948
+ 'Excalidraw elements',
1949
+ 'Use --elements-json \'[{"type":"rectangle","id":"r1","x":0,"y":0,"width":120,"height":80}]\'',
1950
+ );
1951
+ const elementsFile = getStringFlag(flags, 'elements-file', 'initial-file');
1952
+ if (elementsFile)
1953
+ body.elements = parseJsonValue(
1954
+ readFileSync(elementsFile, 'utf-8'),
1955
+ 'Excalidraw elements file',
1956
+ 'Use --elements-file ./scene.excalidraw',
1957
+ );
1958
+ applyCommonGeometryFlags(body, flags, {
1959
+ x: 'Use a finite number, e.g. --x 500',
1960
+ y: 'Use a finite number, e.g. --y 300',
1961
+ width: 'Use a positive number, e.g. --width 960',
1962
+ height: 'Use a positive number, e.g. --height 720',
1963
+ });
1964
+ const timeoutMs = optionalPositiveFiniteFlag(
1965
+ flags,
1966
+ 'timeout-ms',
1967
+ 'Use a positive number, e.g. --timeout-ms 120000',
1968
+ );
1969
+ if (timeoutMs !== undefined) body.timeoutMs = timeoutMs;
1821
1970
 
1822
- cmd('diagram add', 'Create an Excalidraw diagram node', [
1823
- 'pmx-canvas diagram add --title "Architecture"',
1824
- 'pmx-canvas diagram add --title "Architecture" --elements \'[{"type":"rectangle","id":"r1","x":0,"y":0,"width":120,"height":80}]\'',
1825
- ], async (args) => {
1826
- const { flags } = parseFlags(args);
1827
- if (flags.help || flags.h) return showCommandHelp('diagram add');
1828
- const externalAppAdd = COMMANDS['external-app add'];
1829
- await externalAppAdd.run([...args, '--kind', 'excalidraw']);
1830
- });
1971
+ const result = await api('POST', '/api/canvas/diagram', body);
1972
+ output(
1973
+ result && typeof result === 'object' && !Array.isArray(result) && 'nodeId' in result && !('id' in result)
1974
+ ? { id: (result as { nodeId?: unknown }).nodeId, ...result }
1975
+ : result,
1976
+ );
1977
+ },
1978
+ );
1979
+
1980
+ cmd(
1981
+ 'diagram add',
1982
+ 'Create an Excalidraw diagram node',
1983
+ [
1984
+ 'pmx-canvas diagram add --title "Architecture"',
1985
+ 'pmx-canvas diagram add --title "Architecture" --elements \'[{"type":"rectangle","id":"r1","x":0,"y":0,"width":120,"height":80}]\'',
1986
+ ],
1987
+ async (args) => {
1988
+ const { flags } = parseFlags(args);
1989
+ if (flags.help || flags.h) return showCommandHelp('diagram add');
1990
+ const externalAppAdd = COMMANDS['external-app add'];
1991
+ await externalAppAdd.run([...args, '--kind', 'excalidraw']);
1992
+ },
1993
+ );
1831
1994
 
1832
1995
  // ── pin ──────────────────────────────────────────────────────
1833
- cmd('pin', 'Manage context pins', [
1834
- 'pmx-canvas pin node1 node2 node3',
1835
- 'pmx-canvas pin --set node1 node2 node3',
1836
- 'pmx-canvas pin --list',
1837
- 'pmx-canvas pin --clear',
1838
- ], async (args) => {
1839
- const { positional, flags } = parseFlags(args);
1840
- if (flags.help || flags.h) return showCommandHelp('pin');
1996
+ cmd(
1997
+ 'pin',
1998
+ 'Manage context pins',
1999
+ [
2000
+ 'pmx-canvas pin node1 node2 node3',
2001
+ 'pmx-canvas pin --set node1 node2 node3',
2002
+ 'pmx-canvas pin --list',
2003
+ 'pmx-canvas pin --clear',
2004
+ ],
2005
+ async (args) => {
2006
+ const { positional, flags } = parseFlags(args);
2007
+ if (flags.help || flags.h) return showCommandHelp('pin');
2008
+
2009
+ if (flags.list) {
2010
+ const result = await api('GET', '/api/canvas/pinned-context');
2011
+ output(result);
2012
+ return;
2013
+ }
1841
2014
 
1842
- if (flags.list) {
1843
- const result = await api('GET', '/api/canvas/pinned-context');
1844
- output(result);
1845
- return;
1846
- }
2015
+ if (flags.clear) {
2016
+ const result = await invokeOperation('pin.set', { nodeIds: [] });
2017
+ output(result);
2018
+ return;
2019
+ }
1847
2020
 
1848
- if (flags.clear) {
1849
- const result = await invokeOperation('pin.set', { nodeIds: [] });
1850
- output(result);
1851
- return;
1852
- }
2021
+ // --set: positional args are node IDs
2022
+ if (positional.length > 0 || flags.set) {
2023
+ const result = await invokeOperation('pin.set', { nodeIds: positional });
2024
+ output(result);
2025
+ return;
2026
+ }
1853
2027
 
1854
- // --set: positional args are node IDs
1855
- if (positional.length > 0 || flags.set) {
1856
- const result = await invokeOperation('pin.set', { nodeIds: positional });
2028
+ // Default: list
2029
+ const result = await api('GET', '/api/canvas/pinned-context');
1857
2030
  output(result);
1858
- return;
1859
- }
1860
-
1861
- // Default: list
1862
- const result = await api('GET', '/api/canvas/pinned-context');
1863
- output(result);
1864
- });
2031
+ },
2032
+ );
1865
2033
 
1866
2034
  // ── AX ────────────────────────────────────────────────────────
1867
- cmd('ax status', 'Read host-agnostic PMX AX state', [
1868
- 'pmx-canvas ax status',
1869
- ], async (args) => {
2035
+ cmd('ax status', 'Read host-agnostic PMX AX state', ['pmx-canvas ax status'], async (args) => {
1870
2036
  const { flags } = parseFlags(args);
1871
2037
  if (flags.help || flags.h) return showCommandHelp('ax status');
1872
2038
 
1873
2039
  output(await api('GET', '/api/canvas/ax'));
1874
2040
  });
1875
2041
 
1876
- cmd('ax context', 'Read agent-ready PMX AX context', [
1877
- 'pmx-canvas ax context',
1878
- ], async (args) => {
2042
+ cmd('ax context', 'Read agent-ready PMX AX context', ['pmx-canvas ax context'], async (args) => {
1879
2043
  const { flags } = parseFlags(args);
1880
2044
  if (flags.help || flags.h) return showCommandHelp('ax context');
1881
2045
 
1882
2046
  output(await api('GET', '/api/canvas/ax/context'));
1883
2047
  });
1884
2048
 
1885
- cmd('ax focus', 'Set or clear PMX AX focus without moving the viewport', [
1886
- 'pmx-canvas ax focus node1 node2',
1887
- 'pmx-canvas ax focus --clear',
1888
- ], async (args) => {
1889
- const { positional, flags } = parseFlags(args);
1890
- if (flags.help || flags.h) return showCommandHelp('ax focus');
1891
-
1892
- const nodeIds = flags.clear ? [] : positional;
1893
- if (!flags.clear && nodeIds.length === 0) {
1894
- die('Missing node ID', 'pmx-canvas ax focus <node-id> [more-node-ids]');
1895
- }
1896
-
1897
- output(await api('POST', '/api/canvas/ax/focus', { nodeIds, source: resolveAxSource(flags) }));
1898
- });
1899
-
1900
- cmd('ax event add', 'Record a normalized AX timeline event', [
1901
- 'pmx-canvas ax event add --kind tool-start --summary "ran tests"',
1902
- 'pmx-canvas ax event add --kind failure --summary "build broke" --detail "..." node1 node2',
1903
- ], async (args) => {
1904
- const { positional, flags } = parseFlags(args);
1905
- if (flags.help || flags.h) return showCommandHelp('ax event add');
1906
-
1907
- const kind = requireFlag(flags, 'kind', 'pmx-canvas ax event add --kind <kind> --summary <text>');
1908
- const summary = requireFlag(flags, 'summary', 'pmx-canvas ax event add --kind <kind> --summary <text>');
1909
- const detail = getStringFlag(flags, 'detail');
1910
-
1911
- output(await api('POST', '/api/canvas/ax/event', {
1912
- kind,
1913
- summary,
1914
- ...(detail ? { detail } : {}),
1915
- ...(positional.length > 0 ? { nodeIds: positional } : {}),
1916
- source: resolveAxSource(flags),
1917
- }));
1918
- });
1919
-
1920
- cmd('ax steer', 'Send a steering message to the active agent session', [
1921
- 'pmx-canvas ax steer "focus on the failing test first"',
1922
- 'pmx-canvas ax steer --message "stop and re-plan"',
1923
- ], async (args) => {
1924
- const { positional, flags } = parseFlags(args);
1925
- if (flags.help || flags.h) return showCommandHelp('ax steer');
2049
+ cmd(
2050
+ 'ax focus',
2051
+ 'Set or clear PMX AX focus without moving the viewport',
2052
+ ['pmx-canvas ax focus node1 node2', 'pmx-canvas ax focus --clear'],
2053
+ async (args) => {
2054
+ const { positional, flags } = parseFlags(args);
2055
+ if (flags.help || flags.h) return showCommandHelp('ax focus');
1926
2056
 
1927
- const message = getStringFlag(flags, 'message') ?? positional.join(' ').trim();
1928
- if (!message) {
1929
- die('Missing steering message', 'pmx-canvas ax steer <message>');
1930
- }
1931
-
1932
- output(await api('POST', '/api/canvas/ax/steer', { message, source: resolveAxSource(flags) }));
1933
- });
1934
-
1935
- cmd('ax interaction', 'Submit a node-originated AX interaction (capability-gated)', [
1936
- 'pmx-canvas ax interaction --type ax.work.create --node node-1 --payload \'{"title":"Wire auth"}\'',
1937
- 'pmx-canvas ax interaction --type ax.focus.set --node node-2',
1938
- ], async (args) => {
1939
- const { flags } = parseFlags(args);
1940
- if (flags.help || flags.h) return showCommandHelp('ax interaction');
1941
-
1942
- const type = getStringFlag(flags, 'type');
1943
- if (!type) die('Missing --type', 'pmx-canvas ax interaction --type <ax.*> --node <id> [--payload <json>]');
1944
- const sourceNodeId = getStringFlag(flags, 'node');
1945
- if (!sourceNodeId) die('Missing --node', 'pmx-canvas ax interaction --type <ax.*> --node <id>');
1946
-
1947
- let payload: unknown;
1948
- const payloadRaw = getStringFlag(flags, 'payload');
1949
- if (payloadRaw) {
1950
- try {
1951
- payload = JSON.parse(payloadRaw);
1952
- } catch {
1953
- die('Invalid --payload JSON', 'pmx-canvas ax interaction --payload \'{"title":"..."}\'');
2057
+ const nodeIds = flags.clear ? [] : positional;
2058
+ if (!flags.clear && nodeIds.length === 0) {
2059
+ die('Missing node ID', 'pmx-canvas ax focus <node-id> [more-node-ids]');
1954
2060
  }
1955
- }
1956
2061
 
1957
- output(await api('POST', '/api/canvas/ax/interaction', {
1958
- type,
1959
- sourceNodeId,
1960
- ...(payload !== undefined ? { payload } : {}),
1961
- source: resolveAxSource(flags),
1962
- }));
1963
- });
1964
-
1965
- cmd('ax delivery list', 'List pending AX steering for a consumer (loop-safe)', [
1966
- 'pmx-canvas ax delivery list',
1967
- 'pmx-canvas ax delivery list --consumer copilot --limit 20',
1968
- 'pmx-canvas ax delivery list --order newest # latest browser steering first (#68)',
1969
- ], async (args) => {
1970
- const { flags } = parseFlags(args);
1971
- if (flags.help || flags.h) return showCommandHelp('ax delivery list');
1972
- const consumer = getStringFlag(flags, 'consumer');
1973
- const limit = optionalNumberFlag(flags, 'limit', 'pmx-canvas ax delivery list --limit <n>');
1974
- const order = getStringFlag(flags, 'order');
1975
- if (order !== undefined && order !== 'newest' && order !== 'oldest') {
1976
- die('Invalid --order', 'pmx-canvas ax delivery list --order newest|oldest');
1977
- }
1978
- const params = new URLSearchParams();
1979
- if (consumer) params.set('consumer', consumer);
1980
- if (limit) params.set('limit', String(limit));
1981
- if (order) params.set('order', order);
1982
- const qs = params.toString();
1983
- output(await api('GET', `/api/canvas/ax/delivery/pending${qs ? `?${qs}` : ''}`));
1984
- });
1985
-
1986
- cmd('ax delivery mark', 'Mark an AX steering message as delivered', [
1987
- 'pmx-canvas ax delivery mark <steering-id>',
1988
- ], async (args) => {
1989
- const { positional, flags } = parseFlags(args);
1990
- if (flags.help || flags.h) return showCommandHelp('ax delivery mark');
1991
- const id = getStringFlag(flags, 'id') ?? positional[0];
1992
- if (!id) die('Missing steering id', 'pmx-canvas ax delivery mark <steering-id>');
1993
- output(await api('POST', `/api/canvas/ax/delivery/${encodeURIComponent(id)}/mark`, {}));
1994
- });
2062
+ output(await api('POST', '/api/canvas/ax/focus', { nodeIds, source: resolveAxSource(flags) }));
2063
+ },
2064
+ );
2065
+
2066
+ cmd(
2067
+ 'ax event add',
2068
+ 'Record a normalized AX timeline event',
2069
+ [
2070
+ 'pmx-canvas ax event add --kind tool-start --summary "ran tests"',
2071
+ 'pmx-canvas ax event add --kind failure --summary "build broke" --detail "..." node1 node2',
2072
+ ],
2073
+ async (args) => {
2074
+ const { positional, flags } = parseFlags(args);
2075
+ if (flags.help || flags.h) return showCommandHelp('ax event add');
2076
+
2077
+ const kind = requireFlag(flags, 'kind', 'pmx-canvas ax event add --kind <kind> --summary <text>');
2078
+ const summary = requireFlag(flags, 'summary', 'pmx-canvas ax event add --kind <kind> --summary <text>');
2079
+ const detail = getStringFlag(flags, 'detail');
2080
+
2081
+ output(
2082
+ await api('POST', '/api/canvas/ax/event', {
2083
+ kind,
2084
+ summary,
2085
+ ...(detail ? { detail } : {}),
2086
+ ...(positional.length > 0 ? { nodeIds: positional } : {}),
2087
+ source: resolveAxSource(flags),
2088
+ }),
2089
+ );
2090
+ },
2091
+ );
2092
+
2093
+ cmd(
2094
+ 'ax steer',
2095
+ 'Send a steering message to the active agent session',
2096
+ ['pmx-canvas ax steer "focus on the failing test first"', 'pmx-canvas ax steer --message "stop and re-plan"'],
2097
+ async (args) => {
2098
+ const { positional, flags } = parseFlags(args);
2099
+ if (flags.help || flags.h) return showCommandHelp('ax steer');
2100
+
2101
+ const message = getStringFlag(flags, 'message') ?? positional.join(' ').trim();
2102
+ if (!message) {
2103
+ die('Missing steering message', 'pmx-canvas ax steer <message>');
2104
+ }
1995
2105
 
1996
- cmd('ax elicitation request', 'Request structured human input', [
1997
- 'pmx-canvas ax elicitation request --prompt "Who owns this migration?"',
1998
- 'pmx-canvas ax elicitation request --prompt "Pick a region" --fields region,owner',
1999
- ], async (args) => {
2000
- const { flags } = parseFlags(args);
2001
- if (flags.help || flags.h) return showCommandHelp('ax elicitation request');
2002
- const prompt = requireFlag(flags, 'prompt', 'pmx-canvas ax elicitation request --prompt <text>');
2003
- const fields = getStringFlag(flags, 'fields');
2004
- output(await api('POST', '/api/canvas/ax/elicitation', {
2005
- prompt,
2006
- ...(fields ? { fields: fields.split(',').map((f) => f.trim()).filter(Boolean) } : {}),
2007
- source: resolveAxSource(flags),
2008
- }));
2009
- });
2106
+ output(await api('POST', '/api/canvas/ax/steer', { message, source: resolveAxSource(flags) }));
2107
+ },
2108
+ );
2109
+
2110
+ cmd(
2111
+ 'ax interaction',
2112
+ 'Submit a node-originated AX interaction (capability-gated)',
2113
+ [
2114
+ 'pmx-canvas ax interaction --type ax.work.create --node node-1 --payload \'{"title":"Wire auth"}\'',
2115
+ 'pmx-canvas ax interaction --type ax.focus.set --node node-2',
2116
+ ],
2117
+ async (args) => {
2118
+ const { flags } = parseFlags(args);
2119
+ if (flags.help || flags.h) return showCommandHelp('ax interaction');
2120
+
2121
+ const type = getStringFlag(flags, 'type');
2122
+ if (!type) die('Missing --type', 'pmx-canvas ax interaction --type <ax.*> --node <id> [--payload <json>]');
2123
+ const sourceNodeId = getStringFlag(flags, 'node');
2124
+ if (!sourceNodeId) die('Missing --node', 'pmx-canvas ax interaction --type <ax.*> --node <id>');
2125
+
2126
+ let payload: unknown;
2127
+ const payloadRaw = getStringFlag(flags, 'payload');
2128
+ if (payloadRaw) {
2129
+ try {
2130
+ payload = JSON.parse(payloadRaw);
2131
+ } catch {
2132
+ die('Invalid --payload JSON', 'pmx-canvas ax interaction --payload \'{"title":"..."}\'');
2133
+ }
2134
+ }
2010
2135
 
2011
- cmd('ax elicitation respond', 'Answer a pending elicitation', [
2012
- 'pmx-canvas ax elicitation respond <id> --response \'{"owner":"alice"}\'',
2013
- ], async (args) => {
2014
- const { positional, flags } = parseFlags(args);
2015
- if (flags.help || flags.h) return showCommandHelp('ax elicitation respond');
2016
- const id = getStringFlag(flags, 'id') ?? positional[0];
2017
- if (!id) die('Missing elicitation id', 'pmx-canvas ax elicitation respond <id> --response <json>');
2018
- let response: unknown = {};
2019
- const raw = getStringFlag(flags, 'response');
2020
- if (raw) {
2021
- try { response = JSON.parse(raw); } catch { die('Invalid --response JSON', '--response \'{"k":"v"}\''); }
2022
- }
2023
- output(await api('POST', `/api/canvas/ax/elicitation/${encodeURIComponent(id)}/respond`, { response, source: resolveAxSource(flags) }));
2024
- });
2136
+ output(
2137
+ await api('POST', '/api/canvas/ax/interaction', {
2138
+ type,
2139
+ sourceNodeId,
2140
+ ...(payload !== undefined ? { payload } : {}),
2141
+ source: resolveAxSource(flags),
2142
+ }),
2143
+ );
2144
+ },
2145
+ );
2146
+
2147
+ cmd(
2148
+ 'ax delivery list',
2149
+ 'List pending AX steering for a consumer (loop-safe)',
2150
+ [
2151
+ 'pmx-canvas ax delivery list',
2152
+ 'pmx-canvas ax delivery list --consumer copilot --limit 20',
2153
+ 'pmx-canvas ax delivery list --order newest # latest browser steering first (#68)',
2154
+ ],
2155
+ async (args) => {
2156
+ const { flags } = parseFlags(args);
2157
+ if (flags.help || flags.h) return showCommandHelp('ax delivery list');
2158
+ const consumer = getStringFlag(flags, 'consumer');
2159
+ const limit = optionalNumberFlag(flags, 'limit', 'pmx-canvas ax delivery list --limit <n>');
2160
+ const order = getStringFlag(flags, 'order');
2161
+ if (order !== undefined && order !== 'newest' && order !== 'oldest') {
2162
+ die('Invalid --order', 'pmx-canvas ax delivery list --order newest|oldest');
2163
+ }
2164
+ const params = new URLSearchParams();
2165
+ if (consumer) params.set('consumer', consumer);
2166
+ if (limit) params.set('limit', String(limit));
2167
+ if (order) params.set('order', order);
2168
+ const qs = params.toString();
2169
+ output(await api('GET', `/api/canvas/ax/delivery/pending${qs ? `?${qs}` : ''}`));
2170
+ },
2171
+ );
2172
+
2173
+ cmd(
2174
+ 'ax delivery mark',
2175
+ 'Mark an AX steering message as delivered',
2176
+ ['pmx-canvas ax delivery mark <steering-id>'],
2177
+ async (args) => {
2178
+ const { positional, flags } = parseFlags(args);
2179
+ if (flags.help || flags.h) return showCommandHelp('ax delivery mark');
2180
+ const id = getStringFlag(flags, 'id') ?? positional[0];
2181
+ if (!id) die('Missing steering id', 'pmx-canvas ax delivery mark <steering-id>');
2182
+ output(await api('POST', `/api/canvas/ax/delivery/${encodeURIComponent(id)}/mark`, {}));
2183
+ },
2184
+ );
2185
+
2186
+ cmd(
2187
+ 'ax elicitation request',
2188
+ 'Request structured human input',
2189
+ [
2190
+ 'pmx-canvas ax elicitation request --prompt "Who owns this migration?"',
2191
+ 'pmx-canvas ax elicitation request --prompt "Pick a region" --fields region,owner',
2192
+ ],
2193
+ async (args) => {
2194
+ const { flags } = parseFlags(args);
2195
+ if (flags.help || flags.h) return showCommandHelp('ax elicitation request');
2196
+ const prompt = requireFlag(flags, 'prompt', 'pmx-canvas ax elicitation request --prompt <text>');
2197
+ const fields = getStringFlag(flags, 'fields');
2198
+ output(
2199
+ await api('POST', '/api/canvas/ax/elicitation', {
2200
+ prompt,
2201
+ ...(fields
2202
+ ? {
2203
+ fields: fields
2204
+ .split(',')
2205
+ .map((f) => f.trim())
2206
+ .filter(Boolean),
2207
+ }
2208
+ : {}),
2209
+ source: resolveAxSource(flags),
2210
+ }),
2211
+ );
2212
+ },
2213
+ );
2214
+
2215
+ cmd(
2216
+ 'ax elicitation respond',
2217
+ 'Answer a pending elicitation',
2218
+ ['pmx-canvas ax elicitation respond <id> --response \'{"owner":"alice"}\''],
2219
+ async (args) => {
2220
+ const { positional, flags } = parseFlags(args);
2221
+ if (flags.help || flags.h) return showCommandHelp('ax elicitation respond');
2222
+ const id = getStringFlag(flags, 'id') ?? positional[0];
2223
+ if (!id) die('Missing elicitation id', 'pmx-canvas ax elicitation respond <id> --response <json>');
2224
+ let response: unknown = {};
2225
+ const raw = getStringFlag(flags, 'response');
2226
+ if (raw) {
2227
+ try {
2228
+ response = JSON.parse(raw);
2229
+ } catch {
2230
+ die('Invalid --response JSON', '--response \'{"k":"v"}\'');
2231
+ }
2232
+ }
2233
+ output(
2234
+ await api('POST', `/api/canvas/ax/elicitation/${encodeURIComponent(id)}/respond`, {
2235
+ response,
2236
+ source: resolveAxSource(flags),
2237
+ }),
2238
+ );
2239
+ },
2240
+ );
2025
2241
 
2026
2242
  cmd('ax elicitation list', 'List elicitations', ['pmx-canvas ax elicitation list'], async (args) => {
2027
2243
  const { flags } = parseFlags(args);
@@ -2029,32 +2245,42 @@ cmd('ax elicitation list', 'List elicitations', ['pmx-canvas ax elicitation list
2029
2245
  output(await api('GET', '/api/canvas/ax/elicitation'));
2030
2246
  });
2031
2247
 
2032
- cmd('ax mode request', 'Request a workflow mode transition (plan/execute/autonomous)', [
2033
- 'pmx-canvas ax mode request --mode execute --reason "plan approved"',
2034
- ], async (args) => {
2035
- const { flags } = parseFlags(args);
2036
- if (flags.help || flags.h) return showCommandHelp('ax mode request');
2037
- const mode = requireFlag(flags, 'mode', 'pmx-canvas ax mode request --mode plan|execute|autonomous');
2038
- const reason = getStringFlag(flags, 'reason');
2039
- output(await api('POST', '/api/canvas/ax/mode', { mode, ...(reason ? { reason } : {}), source: resolveAxSource(flags) }));
2040
- });
2041
-
2042
- cmd('ax mode resolve', 'Resolve a pending mode request', [
2043
- 'pmx-canvas ax mode resolve <id> --decision approved',
2044
- ], async (args) => {
2045
- const { positional, flags } = parseFlags(args);
2046
- if (flags.help || flags.h) return showCommandHelp('ax mode resolve');
2047
- const id = getStringFlag(flags, 'id') ?? positional[0];
2048
- if (!id) die('Missing mode request id', 'pmx-canvas ax mode resolve <id> --decision approved|rejected');
2049
- const decision = getStringFlag(flags, 'decision');
2050
- if (decision !== 'approved' && decision !== 'rejected') die('Invalid --decision', '--decision approved|rejected');
2051
- const resolution = getStringFlag(flags, 'resolution');
2052
- output(await api('POST', `/api/canvas/ax/mode/${encodeURIComponent(id)}/resolve`, {
2053
- decision,
2054
- ...(resolution ? { resolution } : {}),
2055
- source: resolveAxSource(flags),
2056
- }));
2057
- });
2248
+ cmd(
2249
+ 'ax mode request',
2250
+ 'Request a workflow mode transition (plan/execute/autonomous)',
2251
+ ['pmx-canvas ax mode request --mode execute --reason "plan approved"'],
2252
+ async (args) => {
2253
+ const { flags } = parseFlags(args);
2254
+ if (flags.help || flags.h) return showCommandHelp('ax mode request');
2255
+ const mode = requireFlag(flags, 'mode', 'pmx-canvas ax mode request --mode plan|execute|autonomous');
2256
+ const reason = getStringFlag(flags, 'reason');
2257
+ output(
2258
+ await api('POST', '/api/canvas/ax/mode', { mode, ...(reason ? { reason } : {}), source: resolveAxSource(flags) }),
2259
+ );
2260
+ },
2261
+ );
2262
+
2263
+ cmd(
2264
+ 'ax mode resolve',
2265
+ 'Resolve a pending mode request',
2266
+ ['pmx-canvas ax mode resolve <id> --decision approved'],
2267
+ async (args) => {
2268
+ const { positional, flags } = parseFlags(args);
2269
+ if (flags.help || flags.h) return showCommandHelp('ax mode resolve');
2270
+ const id = getStringFlag(flags, 'id') ?? positional[0];
2271
+ if (!id) die('Missing mode request id', 'pmx-canvas ax mode resolve <id> --decision approved|rejected');
2272
+ const decision = getStringFlag(flags, 'decision');
2273
+ if (decision !== 'approved' && decision !== 'rejected') die('Invalid --decision', '--decision approved|rejected');
2274
+ const resolution = getStringFlag(flags, 'resolution');
2275
+ output(
2276
+ await api('POST', `/api/canvas/ax/mode/${encodeURIComponent(id)}/resolve`, {
2277
+ decision,
2278
+ ...(resolution ? { resolution } : {}),
2279
+ source: resolveAxSource(flags),
2280
+ }),
2281
+ );
2282
+ },
2283
+ );
2058
2284
 
2059
2285
  cmd('ax mode list', 'List mode requests', ['pmx-canvas ax mode list'], async (args) => {
2060
2286
  const { flags } = parseFlags(args);
@@ -2068,21 +2294,36 @@ cmd('ax command list', 'List the PMX command registry', ['pmx-canvas ax command
2068
2294
  output(await api('GET', '/api/canvas/ax/command'));
2069
2295
  });
2070
2296
 
2071
- cmd('ax command invoke', 'Invoke a registry-gated PMX command intent', [
2072
- 'pmx-canvas ax command invoke pmx.plan',
2073
- 'pmx-canvas ax command invoke pmx.promote-context --args \'{"nodeIds":["n1"]}\'',
2074
- ], async (args) => {
2075
- const { positional, flags } = parseFlags(args);
2076
- if (flags.help || flags.h) return showCommandHelp('ax command invoke');
2077
- const name = getStringFlag(flags, 'name') ?? positional[0];
2078
- if (!name) die('Missing command name', 'pmx-canvas ax command invoke <name>');
2079
- let cmdArgs: unknown;
2080
- const raw = getStringFlag(flags, 'args');
2081
- if (raw) {
2082
- try { cmdArgs = JSON.parse(raw); } catch { die('Invalid --args JSON', '--args \'{"k":"v"}\''); }
2083
- }
2084
- output(await api('POST', '/api/canvas/ax/command', { name, ...(cmdArgs !== undefined ? { args: cmdArgs } : {}), source: resolveAxSource(flags) }));
2085
- });
2297
+ cmd(
2298
+ 'ax command invoke',
2299
+ 'Invoke a registry-gated PMX command intent',
2300
+ [
2301
+ 'pmx-canvas ax command invoke pmx.plan',
2302
+ 'pmx-canvas ax command invoke pmx.promote-context --args \'{"nodeIds":["n1"]}\'',
2303
+ ],
2304
+ async (args) => {
2305
+ const { positional, flags } = parseFlags(args);
2306
+ if (flags.help || flags.h) return showCommandHelp('ax command invoke');
2307
+ const name = getStringFlag(flags, 'name') ?? positional[0];
2308
+ if (!name) die('Missing command name', 'pmx-canvas ax command invoke <name>');
2309
+ let cmdArgs: unknown;
2310
+ const raw = getStringFlag(flags, 'args');
2311
+ if (raw) {
2312
+ try {
2313
+ cmdArgs = JSON.parse(raw);
2314
+ } catch {
2315
+ die('Invalid --args JSON', '--args \'{"k":"v"}\'');
2316
+ }
2317
+ }
2318
+ output(
2319
+ await api('POST', '/api/canvas/ax/command', {
2320
+ name,
2321
+ ...(cmdArgs !== undefined ? { args: cmdArgs } : {}),
2322
+ source: resolveAxSource(flags),
2323
+ }),
2324
+ );
2325
+ },
2326
+ );
2086
2327
 
2087
2328
  cmd('ax policy get', 'Show the current declarative AX policy', ['pmx-canvas ax policy get'], async (args) => {
2088
2329
  const { flags } = parseFlags(args);
@@ -2090,222 +2331,285 @@ cmd('ax policy get', 'Show the current declarative AX policy', ['pmx-canvas ax p
2090
2331
  output(await api('GET', '/api/canvas/ax/policy'));
2091
2332
  });
2092
2333
 
2093
- cmd('ax policy set', 'Set the declarative AX policy (stored by PMX, enforced by adapters)', [
2094
- 'pmx-canvas ax policy set --excluded-tools shell,write --mode concise',
2095
- ], async (args) => {
2096
- const { flags } = parseFlags(args);
2097
- if (flags.help || flags.h) return showCommandHelp('ax policy set');
2098
- const csv = (v?: string) => (v ? v.split(',').map((s) => s.trim()).filter(Boolean) : undefined);
2099
- const allowed = csv(getStringFlag(flags, 'allowed-tools'));
2100
- const excluded = csv(getStringFlag(flags, 'excluded-tools'));
2101
- const approvalRequired = csv(getStringFlag(flags, 'approval-tools'));
2102
- const mode = getStringFlag(flags, 'mode');
2103
- const systemAppend = getStringFlag(flags, 'system-append');
2104
- const tools = (allowed || excluded || approvalRequired)
2105
- ? { ...(allowed ? { allowed } : {}), ...(excluded ? { excluded } : {}), ...(approvalRequired ? { approvalRequired } : {}) }
2106
- : undefined;
2107
- const prompt = (mode || systemAppend)
2108
- ? { ...(mode ? { mode } : {}), ...(systemAppend ? { systemAppend } : {}) }
2109
- : undefined;
2110
- output(await api('POST', '/api/canvas/ax/policy', { ...(tools ? { tools } : {}), ...(prompt ? { prompt } : {}), source: resolveAxSource(flags) }));
2111
- });
2112
-
2113
- cmd('ax timeline', 'Read the bounded AX timeline (events, evidence, steering)', [
2114
- 'pmx-canvas ax timeline',
2115
- 'pmx-canvas ax timeline --limit 100',
2116
- ], async (args) => {
2117
- const { flags } = parseFlags(args);
2118
- if (flags.help || flags.h) return showCommandHelp('ax timeline');
2119
-
2120
- const limit = optionalNumberFlag(flags, 'limit', 'pmx-canvas ax timeline --limit <n>');
2121
- output(await api('GET', `/api/canvas/ax/timeline${limit ? `?limit=${limit}` : ''}`));
2122
- });
2123
-
2124
- cmd('ax work add', 'Add a canvas-bound AX work item', [
2125
- 'pmx-canvas ax work add --title "Wire up auth" --status in-progress',
2126
- 'pmx-canvas ax work add --title "Review API" node1 node2',
2127
- ], async (args) => {
2128
- const { positional, flags } = parseFlags(args);
2129
- if (flags.help || flags.h) return showCommandHelp('ax work add');
2130
-
2131
- const title = requireFlag(flags, 'title', 'pmx-canvas ax work add --title <text>');
2132
- const status = getStringFlag(flags, 'status');
2133
- const detail = getStringFlag(flags, 'detail');
2134
-
2135
- output(await api('POST', '/api/canvas/ax/work', {
2136
- title,
2137
- ...(status ? { status } : {}),
2138
- ...(detail ? { detail } : {}),
2139
- ...(positional.length > 0 ? { nodeIds: positional } : {}),
2140
- source: resolveAxSource(flags),
2141
- }));
2142
- });
2143
-
2144
- cmd('ax work update', 'Update a canvas-bound AX work item by ID', [
2145
- 'pmx-canvas ax work update <id> --status done',
2146
- 'pmx-canvas ax work update <id> --title "New title" --detail "..."',
2147
- ], async (args) => {
2148
- const { positional, flags } = parseFlags(args);
2149
- if (flags.help || flags.h) return showCommandHelp('ax work update');
2150
-
2151
- const id = positional[0];
2152
- if (!id) die('Missing work item ID', 'pmx-canvas ax work update <id> --status <status>');
2153
- const title = getStringFlag(flags, 'title');
2154
- const status = getStringFlag(flags, 'status');
2155
- const detail = getStringFlag(flags, 'detail');
2156
-
2157
- output(await api('PATCH', `/api/canvas/ax/work/${encodeURIComponent(id)}`, {
2158
- ...(title ? { title } : {}),
2159
- ...(status ? { status } : {}),
2160
- ...(detail ? { detail } : {}),
2161
- ...(positional.length > 1 ? { nodeIds: positional.slice(1) } : {}),
2162
- source: resolveAxSource(flags),
2163
- }));
2164
- });
2334
+ cmd(
2335
+ 'ax policy set',
2336
+ 'Set the declarative AX policy (stored by PMX, enforced by adapters)',
2337
+ ['pmx-canvas ax policy set --excluded-tools shell,write --mode concise'],
2338
+ async (args) => {
2339
+ const { flags } = parseFlags(args);
2340
+ if (flags.help || flags.h) return showCommandHelp('ax policy set');
2341
+ const csv = (v?: string) =>
2342
+ v
2343
+ ? v
2344
+ .split(',')
2345
+ .map((s) => s.trim())
2346
+ .filter(Boolean)
2347
+ : undefined;
2348
+ const allowed = csv(getStringFlag(flags, 'allowed-tools'));
2349
+ const excluded = csv(getStringFlag(flags, 'excluded-tools'));
2350
+ const approvalRequired = csv(getStringFlag(flags, 'approval-tools'));
2351
+ const mode = getStringFlag(flags, 'mode');
2352
+ const systemAppend = getStringFlag(flags, 'system-append');
2353
+ const tools =
2354
+ allowed || excluded || approvalRequired
2355
+ ? {
2356
+ ...(allowed ? { allowed } : {}),
2357
+ ...(excluded ? { excluded } : {}),
2358
+ ...(approvalRequired ? { approvalRequired } : {}),
2359
+ }
2360
+ : undefined;
2361
+ const prompt =
2362
+ mode || systemAppend ? { ...(mode ? { mode } : {}), ...(systemAppend ? { systemAppend } : {}) } : undefined;
2363
+ output(
2364
+ await api('POST', '/api/canvas/ax/policy', {
2365
+ ...(tools ? { tools } : {}),
2366
+ ...(prompt ? { prompt } : {}),
2367
+ source: resolveAxSource(flags),
2368
+ }),
2369
+ );
2370
+ },
2371
+ );
2372
+
2373
+ cmd(
2374
+ 'ax timeline',
2375
+ 'Read the bounded AX timeline (events, evidence, steering)',
2376
+ ['pmx-canvas ax timeline', 'pmx-canvas ax timeline --limit 100'],
2377
+ async (args) => {
2378
+ const { flags } = parseFlags(args);
2379
+ if (flags.help || flags.h) return showCommandHelp('ax timeline');
2380
+
2381
+ const limit = optionalNumberFlag(flags, 'limit', 'pmx-canvas ax timeline --limit <n>');
2382
+ output(await api('GET', `/api/canvas/ax/timeline${limit ? `?limit=${limit}` : ''}`));
2383
+ },
2384
+ );
2385
+
2386
+ cmd(
2387
+ 'ax work add',
2388
+ 'Add a canvas-bound AX work item',
2389
+ [
2390
+ 'pmx-canvas ax work add --title "Wire up auth" --status in-progress',
2391
+ 'pmx-canvas ax work add --title "Review API" node1 node2',
2392
+ ],
2393
+ async (args) => {
2394
+ const { positional, flags } = parseFlags(args);
2395
+ if (flags.help || flags.h) return showCommandHelp('ax work add');
2396
+
2397
+ const title = requireFlag(flags, 'title', 'pmx-canvas ax work add --title <text>');
2398
+ const status = getStringFlag(flags, 'status');
2399
+ const detail = getStringFlag(flags, 'detail');
2400
+
2401
+ output(
2402
+ await api('POST', '/api/canvas/ax/work', {
2403
+ title,
2404
+ ...(status ? { status } : {}),
2405
+ ...(detail ? { detail } : {}),
2406
+ ...(positional.length > 0 ? { nodeIds: positional } : {}),
2407
+ source: resolveAxSource(flags),
2408
+ }),
2409
+ );
2410
+ },
2411
+ );
2412
+
2413
+ cmd(
2414
+ 'ax work update',
2415
+ 'Update a canvas-bound AX work item by ID',
2416
+ ['pmx-canvas ax work update <id> --status done', 'pmx-canvas ax work update <id> --title "New title" --detail "..."'],
2417
+ async (args) => {
2418
+ const { positional, flags } = parseFlags(args);
2419
+ if (flags.help || flags.h) return showCommandHelp('ax work update');
2420
+
2421
+ const id = positional[0];
2422
+ if (!id) die('Missing work item ID', 'pmx-canvas ax work update <id> --status <status>');
2423
+ const title = getStringFlag(flags, 'title');
2424
+ const status = getStringFlag(flags, 'status');
2425
+ const detail = getStringFlag(flags, 'detail');
2426
+
2427
+ output(
2428
+ await api('PATCH', `/api/canvas/ax/work/${encodeURIComponent(id)}`, {
2429
+ ...(title ? { title } : {}),
2430
+ ...(status ? { status } : {}),
2431
+ ...(detail ? { detail } : {}),
2432
+ ...(positional.length > 1 ? { nodeIds: positional.slice(1) } : {}),
2433
+ source: resolveAxSource(flags),
2434
+ }),
2435
+ );
2436
+ },
2437
+ );
2165
2438
 
2166
- cmd('ax work list', 'List canvas-bound AX work items', [
2167
- 'pmx-canvas ax work list',
2168
- ], async (args) => {
2439
+ cmd('ax work list', 'List canvas-bound AX work items', ['pmx-canvas ax work list'], async (args) => {
2169
2440
  const { flags } = parseFlags(args);
2170
2441
  if (flags.help || flags.h) return showCommandHelp('ax work list');
2171
2442
 
2172
2443
  output(await api('GET', '/api/canvas/ax/work'));
2173
2444
  });
2174
2445
 
2175
- cmd('ax approval request', 'Request a canvas-bound AX approval gate', [
2176
- 'pmx-canvas ax approval request --title "Deploy to prod"',
2177
- 'pmx-canvas ax approval request --title "Drop table" --action db.drop node1',
2178
- ], async (args) => {
2179
- const { positional, flags } = parseFlags(args);
2180
- if (flags.help || flags.h) return showCommandHelp('ax approval request');
2181
-
2182
- const title = requireFlag(flags, 'title', 'pmx-canvas ax approval request --title <text>');
2183
- const detail = getStringFlag(flags, 'detail');
2184
- const action = getStringFlag(flags, 'action');
2185
-
2186
- output(await api('POST', '/api/canvas/ax/approval', {
2187
- title,
2188
- ...(detail ? { detail } : {}),
2189
- ...(action ? { action } : {}),
2190
- ...(positional.length > 0 ? { nodeIds: positional } : {}),
2191
- source: resolveAxSource(flags),
2192
- }));
2193
- });
2194
-
2195
- cmd('ax approval resolve', 'Resolve a pending AX approval gate by ID', [
2196
- 'pmx-canvas ax approval resolve <id> --decision approved',
2197
- 'pmx-canvas ax approval resolve <id> --decision rejected --resolution "too risky"',
2198
- ], async (args) => {
2199
- const { positional, flags } = parseFlags(args);
2200
- if (flags.help || flags.h) return showCommandHelp('ax approval resolve');
2201
-
2202
- const id = positional[0];
2203
- if (!id) die('Missing approval gate ID', 'pmx-canvas ax approval resolve <id> --decision <approved|rejected>');
2204
- const decision = requireFlag(flags, 'decision', 'pmx-canvas ax approval resolve <id> --decision <approved|rejected>');
2205
- if (decision !== 'approved' && decision !== 'rejected') {
2206
- die('Invalid decision', 'pmx-canvas ax approval resolve <id> --decision <approved|rejected>');
2207
- }
2208
- const resolution = getStringFlag(flags, 'resolution');
2209
-
2210
- output(await api('POST', `/api/canvas/ax/approval/${encodeURIComponent(id)}/resolve`, {
2211
- decision,
2212
- ...(resolution ? { resolution } : {}),
2213
- source: resolveAxSource(flags),
2214
- }));
2215
- });
2446
+ cmd(
2447
+ 'ax approval request',
2448
+ 'Request a canvas-bound AX approval gate',
2449
+ [
2450
+ 'pmx-canvas ax approval request --title "Deploy to prod"',
2451
+ 'pmx-canvas ax approval request --title "Drop table" --action db.drop node1',
2452
+ ],
2453
+ async (args) => {
2454
+ const { positional, flags } = parseFlags(args);
2455
+ if (flags.help || flags.h) return showCommandHelp('ax approval request');
2456
+
2457
+ const title = requireFlag(flags, 'title', 'pmx-canvas ax approval request --title <text>');
2458
+ const detail = getStringFlag(flags, 'detail');
2459
+ const action = getStringFlag(flags, 'action');
2460
+
2461
+ output(
2462
+ await api('POST', '/api/canvas/ax/approval', {
2463
+ title,
2464
+ ...(detail ? { detail } : {}),
2465
+ ...(action ? { action } : {}),
2466
+ ...(positional.length > 0 ? { nodeIds: positional } : {}),
2467
+ source: resolveAxSource(flags),
2468
+ }),
2469
+ );
2470
+ },
2471
+ );
2472
+
2473
+ cmd(
2474
+ 'ax approval resolve',
2475
+ 'Resolve a pending AX approval gate by ID',
2476
+ [
2477
+ 'pmx-canvas ax approval resolve <id> --decision approved',
2478
+ 'pmx-canvas ax approval resolve <id> --decision rejected --resolution "too risky"',
2479
+ ],
2480
+ async (args) => {
2481
+ const { positional, flags } = parseFlags(args);
2482
+ if (flags.help || flags.h) return showCommandHelp('ax approval resolve');
2483
+
2484
+ const id = positional[0];
2485
+ if (!id) die('Missing approval gate ID', 'pmx-canvas ax approval resolve <id> --decision <approved|rejected>');
2486
+ const decision = requireFlag(
2487
+ flags,
2488
+ 'decision',
2489
+ 'pmx-canvas ax approval resolve <id> --decision <approved|rejected>',
2490
+ );
2491
+ if (decision !== 'approved' && decision !== 'rejected') {
2492
+ die('Invalid decision', 'pmx-canvas ax approval resolve <id> --decision <approved|rejected>');
2493
+ }
2494
+ const resolution = getStringFlag(flags, 'resolution');
2495
+
2496
+ output(
2497
+ await api('POST', `/api/canvas/ax/approval/${encodeURIComponent(id)}/resolve`, {
2498
+ decision,
2499
+ ...(resolution ? { resolution } : {}),
2500
+ source: resolveAxSource(flags),
2501
+ }),
2502
+ );
2503
+ },
2504
+ );
2216
2505
 
2217
- cmd('ax approval list', 'List canvas-bound AX approval gates', [
2218
- 'pmx-canvas ax approval list',
2219
- ], async (args) => {
2506
+ cmd('ax approval list', 'List canvas-bound AX approval gates', ['pmx-canvas ax approval list'], async (args) => {
2220
2507
  const { flags } = parseFlags(args);
2221
2508
  if (flags.help || flags.h) return showCommandHelp('ax approval list');
2222
2509
 
2223
2510
  output(await api('GET', '/api/canvas/ax/approval'));
2224
2511
  });
2225
2512
 
2226
- cmd('ax evidence add', 'Record an AX evidence item on the timeline', [
2227
- 'pmx-canvas ax evidence add --kind test-output --title "unit pass" --body "..."',
2228
- 'pmx-canvas ax evidence add --kind screenshot --title "before" --ref /tmp/before.png node1',
2229
- ], async (args) => {
2230
- const { positional, flags } = parseFlags(args);
2231
- if (flags.help || flags.h) return showCommandHelp('ax evidence add');
2232
-
2233
- const kind = requireFlag(flags, 'kind', 'pmx-canvas ax evidence add --kind <kind> --title <text>');
2234
- const title = requireFlag(flags, 'title', 'pmx-canvas ax evidence add --kind <kind> --title <text>');
2235
- const body = getStringFlag(flags, 'body');
2236
- const ref = getStringFlag(flags, 'ref');
2237
-
2238
- output(await api('POST', '/api/canvas/ax/evidence', {
2239
- kind,
2240
- title,
2241
- ...(body ? { body } : {}),
2242
- ...(ref ? { ref } : {}),
2243
- ...(positional.length > 0 ? { nodeIds: positional } : {}),
2244
- source: resolveAxSource(flags),
2245
- }));
2246
- });
2247
-
2248
- cmd('ax review add', 'Add a canvas-bound AX review annotation', [
2249
- 'pmx-canvas ax review add --body "needs a test" --node node1',
2250
- 'pmx-canvas ax review add --body "off-by-one" --kind finding --severity error --file src/x.ts',
2251
- ], async (args) => {
2252
- const { flags } = parseFlags(args);
2253
- if (flags.help || flags.h) return showCommandHelp('ax review add');
2254
-
2255
- const body = requireFlag(flags, 'body', 'pmx-canvas ax review add --body <text>');
2256
- const kind = getStringFlag(flags, 'kind');
2257
- const severity = getStringFlag(flags, 'severity');
2258
- const anchorType = getStringFlag(flags, 'anchor');
2259
- const nodeId = getStringFlag(flags, 'node');
2260
- const file = getStringFlag(flags, 'file');
2261
- const author = getStringFlag(flags, 'author');
2262
-
2263
- output(await api('POST', '/api/canvas/ax/review', {
2264
- body,
2265
- ...(kind ? { kind } : {}),
2266
- ...(severity ? { severity } : {}),
2267
- ...(anchorType ? { anchorType } : {}),
2268
- ...(nodeId ? { nodeId } : {}),
2269
- ...(file ? { file } : {}),
2270
- ...(author ? { author } : {}),
2271
- source: resolveAxSource(flags),
2272
- }));
2273
- });
2513
+ cmd(
2514
+ 'ax evidence add',
2515
+ 'Record an AX evidence item on the timeline',
2516
+ [
2517
+ 'pmx-canvas ax evidence add --kind test-output --title "unit pass" --body "..."',
2518
+ 'pmx-canvas ax evidence add --kind screenshot --title "before" --ref /tmp/before.png node1',
2519
+ ],
2520
+ async (args) => {
2521
+ const { positional, flags } = parseFlags(args);
2522
+ if (flags.help || flags.h) return showCommandHelp('ax evidence add');
2523
+
2524
+ const kind = requireFlag(flags, 'kind', 'pmx-canvas ax evidence add --kind <kind> --title <text>');
2525
+ const title = requireFlag(flags, 'title', 'pmx-canvas ax evidence add --kind <kind> --title <text>');
2526
+ const body = getStringFlag(flags, 'body');
2527
+ const ref = getStringFlag(flags, 'ref');
2528
+
2529
+ output(
2530
+ await api('POST', '/api/canvas/ax/evidence', {
2531
+ kind,
2532
+ title,
2533
+ ...(body ? { body } : {}),
2534
+ ...(ref ? { ref } : {}),
2535
+ ...(positional.length > 0 ? { nodeIds: positional } : {}),
2536
+ source: resolveAxSource(flags),
2537
+ }),
2538
+ );
2539
+ },
2540
+ );
2541
+
2542
+ cmd(
2543
+ 'ax review add',
2544
+ 'Add a canvas-bound AX review annotation',
2545
+ [
2546
+ 'pmx-canvas ax review add --body "needs a test" --node node1',
2547
+ 'pmx-canvas ax review add --body "off-by-one" --kind finding --severity error --file src/x.ts',
2548
+ ],
2549
+ async (args) => {
2550
+ const { flags } = parseFlags(args);
2551
+ if (flags.help || flags.h) return showCommandHelp('ax review add');
2552
+
2553
+ const body = requireFlag(flags, 'body', 'pmx-canvas ax review add --body <text>');
2554
+ const kind = getStringFlag(flags, 'kind');
2555
+ const severity = getStringFlag(flags, 'severity');
2556
+ const anchorType = getStringFlag(flags, 'anchor');
2557
+ const nodeId = getStringFlag(flags, 'node');
2558
+ const file = getStringFlag(flags, 'file');
2559
+ const author = getStringFlag(flags, 'author');
2560
+
2561
+ output(
2562
+ await api('POST', '/api/canvas/ax/review', {
2563
+ body,
2564
+ ...(kind ? { kind } : {}),
2565
+ ...(severity ? { severity } : {}),
2566
+ ...(anchorType ? { anchorType } : {}),
2567
+ ...(nodeId ? { nodeId } : {}),
2568
+ ...(file ? { file } : {}),
2569
+ ...(author ? { author } : {}),
2570
+ source: resolveAxSource(flags),
2571
+ }),
2572
+ );
2573
+ },
2574
+ );
2274
2575
 
2275
- cmd('ax review list', 'List canvas-bound AX review annotations', [
2276
- 'pmx-canvas ax review list',
2277
- ], async (args) => {
2576
+ cmd('ax review list', 'List canvas-bound AX review annotations', ['pmx-canvas ax review list'], async (args) => {
2278
2577
  const { flags } = parseFlags(args);
2279
2578
  if (flags.help || flags.h) return showCommandHelp('ax review list');
2280
2579
 
2281
2580
  output(await api('GET', '/api/canvas/ax/review'));
2282
2581
  });
2283
2582
 
2284
- cmd('ax host report', 'Report host/session capability to the canvas', [
2285
- 'pmx-canvas ax host report --host copilot --canvas --tools --session-messaging',
2286
- 'pmx-canvas ax host report --host codex --canvas --files',
2287
- ], async (args) => {
2288
- const { flags } = parseFlags(args);
2289
- if (flags.help || flags.h) return showCommandHelp('ax host report');
2290
-
2291
- const host = getStringFlag(flags, 'host');
2292
-
2293
- output(await api('PUT', '/api/canvas/ax/host-capability', {
2294
- ...(host ? { host } : {}),
2295
- canvas: flags.canvas === true,
2296
- hooks: flags.hooks === true,
2297
- tools: flags.tools === true,
2298
- sessionMessaging: flags['session-messaging'] === true,
2299
- permissions: flags.permissions === true,
2300
- files: flags.files === true,
2301
- uiPrompts: flags['ui-prompts'] === true,
2302
- source: resolveAxSource(flags),
2303
- }));
2304
- });
2583
+ cmd(
2584
+ 'ax host report',
2585
+ 'Report host/session capability to the canvas',
2586
+ [
2587
+ 'pmx-canvas ax host report --host copilot --canvas --tools --session-messaging',
2588
+ 'pmx-canvas ax host report --host codex --canvas --files',
2589
+ ],
2590
+ async (args) => {
2591
+ const { flags } = parseFlags(args);
2592
+ if (flags.help || flags.h) return showCommandHelp('ax host report');
2593
+
2594
+ const host = getStringFlag(flags, 'host');
2595
+
2596
+ output(
2597
+ await api('PUT', '/api/canvas/ax/host-capability', {
2598
+ ...(host ? { host } : {}),
2599
+ canvas: flags.canvas === true,
2600
+ hooks: flags.hooks === true,
2601
+ tools: flags.tools === true,
2602
+ sessionMessaging: flags['session-messaging'] === true,
2603
+ permissions: flags.permissions === true,
2604
+ files: flags.files === true,
2605
+ uiPrompts: flags['ui-prompts'] === true,
2606
+ source: resolveAxSource(flags),
2607
+ }),
2608
+ );
2609
+ },
2610
+ );
2305
2611
 
2306
- cmd('ax host status', 'Read the reported host/session capability', [
2307
- 'pmx-canvas ax host status',
2308
- ], async (args) => {
2612
+ cmd('ax host status', 'Read the reported host/session capability', ['pmx-canvas ax host status'], async (args) => {
2309
2613
  const { flags } = parseFlags(args);
2310
2614
  if (flags.help || flags.h) return showCommandHelp('ax host status');
2311
2615
 
@@ -2313,155 +2617,167 @@ cmd('ax host status', 'Read the reported host/session capability', [
2313
2617
  });
2314
2618
 
2315
2619
  // ── copilot install-extension ────────────────────────────────
2316
- cmd('copilot install-extension', 'Install the bundled GitHub Copilot extension adapter', [
2317
- 'pmx-canvas copilot install-extension --dry-run',
2318
- 'pmx-canvas copilot install-extension --target .github/extensions/pmx-canvas/extension.mjs --yes',
2319
- ], async (args) => {
2320
- const { flags } = parseFlags(args);
2321
- if (flags.help || flags.h) return showCommandHelp('copilot install-extension');
2322
-
2323
- const sourcePath = fileURLToPath(new URL('../../.github/extensions/pmx-canvas/extension.mjs', import.meta.url));
2324
- if (!existsSync(sourcePath)) {
2325
- die('Bundled Copilot extension adapter not found.', `Expected at ${sourcePath}`);
2326
- }
2620
+ cmd(
2621
+ 'copilot install-extension',
2622
+ 'Install the bundled GitHub Copilot extension adapter',
2623
+ [
2624
+ 'pmx-canvas copilot install-extension --dry-run',
2625
+ 'pmx-canvas copilot install-extension --target .github/extensions/pmx-canvas/extension.mjs --yes',
2626
+ ],
2627
+ async (args) => {
2628
+ const { flags } = parseFlags(args);
2629
+ if (flags.help || flags.h) return showCommandHelp('copilot install-extension');
2630
+
2631
+ const sourcePath = fileURLToPath(new URL('../../.github/extensions/pmx-canvas/extension.mjs', import.meta.url));
2632
+ if (!existsSync(sourcePath)) {
2633
+ die('Bundled Copilot extension adapter not found.', `Expected at ${sourcePath}`);
2634
+ }
2327
2635
 
2328
- const targetPath = getStringFlag(flags, 'target')
2329
- ?? join(process.cwd(), '.github', 'extensions', 'pmx-canvas', 'extension.mjs');
2330
- const dryRun = flags['dry-run'] === true;
2331
- const targetExists = existsSync(targetPath);
2636
+ const targetPath =
2637
+ getStringFlag(flags, 'target') ?? join(process.cwd(), '.github', 'extensions', 'pmx-canvas', 'extension.mjs');
2638
+ const dryRun = flags['dry-run'] === true;
2639
+ const targetExists = existsSync(targetPath);
2332
2640
 
2333
- if (dryRun) {
2334
- output({ ok: true, dryRun: true, sourcePath, targetPath, targetExists, wrote: false });
2335
- return;
2336
- }
2641
+ if (dryRun) {
2642
+ output({ ok: true, dryRun: true, sourcePath, targetPath, targetExists, wrote: false });
2643
+ return;
2644
+ }
2337
2645
 
2338
- if (targetExists && flags.yes !== true) {
2339
- die('Target already exists. Re-run with --yes to overwrite.', `Target: ${targetPath}`);
2340
- }
2646
+ if (targetExists && flags.yes !== true) {
2647
+ die('Target already exists. Re-run with --yes to overwrite.', `Target: ${targetPath}`);
2648
+ }
2341
2649
 
2342
- mkdirSync(dirname(targetPath), { recursive: true });
2343
- copyFileSync(sourcePath, targetPath);
2344
- output({ ok: true, dryRun: false, sourcePath, targetPath, wrote: true });
2345
- });
2650
+ mkdirSync(dirname(targetPath), { recursive: true });
2651
+ copyFileSync(sourcePath, targetPath);
2652
+ output({ ok: true, dryRun: false, sourcePath, targetPath, wrote: true });
2653
+ },
2654
+ );
2346
2655
 
2347
2656
  // ── undo ─────────────────────────────────────────────────────
2348
- cmd('undo', 'Undo the last canvas mutation', [
2349
- 'pmx-canvas undo',
2350
- ], async (args) => {
2351
- const { flags } = parseFlags(args);
2352
- if (flags.help || flags.h) return showCommandHelp('undo');
2353
-
2354
- const result = await invokeOperation('canvas.undo', {});
2355
- output(result);
2356
- });
2357
-
2358
- // ── redo ─────────────────────────────────────────────────────
2359
- cmd('redo', 'Redo the last undone mutation', [
2360
- 'pmx-canvas redo',
2361
- ], async (args) => {
2362
- const { flags } = parseFlags(args);
2363
- if (flags.help || flags.h) return showCommandHelp('redo');
2364
-
2365
- const result = await invokeOperation('canvas.redo', {});
2366
- output(result);
2367
- });
2368
-
2369
- // ── history ──────────────────────────────────────────────────
2370
- cmd('history', 'Show canvas mutation history', [
2371
- 'pmx-canvas history',
2372
- 'pmx-canvas history --summary',
2373
- 'pmx-canvas history --compact',
2374
- ], async (args) => {
2375
- const { flags } = parseFlags(args);
2376
- if (flags.help || flags.h) return showCommandHelp('history');
2377
-
2378
- const result = await invokeOperation('history.get', {}) as Record<string, unknown>;
2379
- if (flags.summary) {
2380
- output(summarizeHistoryResult(result));
2381
- return;
2382
- }
2383
- if (flags.compact) {
2384
- output(compactHistoryResult(result));
2385
- return;
2386
- }
2387
- output(result);
2388
- });
2389
-
2390
- // ── snapshot save ────────────────────────────────────────────
2391
- cmd('snapshot save', 'Save a named snapshot of the current canvas', [
2392
- 'pmx-canvas snapshot save --name "before-refactor"',
2393
- 'pmx-canvas snapshot save --name checkpoint-1',
2394
- ], async (args) => {
2395
- const { flags } = parseFlags(args);
2396
- if (flags.help || flags.h) return showCommandHelp('snapshot save');
2397
-
2398
- const name = requireFlag(flags, 'name', 'pmx-canvas snapshot save --name "my-snapshot"');
2399
- const result = await invokeOperation('snapshot.save', { name });
2400
- output(result);
2401
- });
2402
-
2403
- // ── snapshot list ────────────────────────────────────────────
2404
- cmd('snapshot list', 'List saved snapshots', [
2405
- 'pmx-canvas snapshot list',
2406
- 'pmx-canvas snapshot list --limit 50 --query baseline',
2407
- 'pmx-canvas snapshot list --after 2026-05-01T00:00:00Z --before 2026-05-05T00:00:00Z',
2408
- 'pmx-canvas snapshot list --all',
2409
- ], async (args) => {
2657
+ cmd('undo', 'Undo the last canvas mutation', ['pmx-canvas undo'], async (args) => {
2410
2658
  const { flags } = parseFlags(args);
2411
- if (flags.help || flags.h) return showCommandHelp('snapshot list');
2412
-
2413
- const limit = optionalNumberFlag(flags, 'limit', 'Use a positive integer, e.g. --limit 50');
2414
- const query = getStringFlag(flags, 'query', 'q');
2415
- const before = getStringFlag(flags, 'before');
2416
- const after = getStringFlag(flags, 'after');
2417
- const result = await invokeOperation('snapshot.list', {
2418
- ...(limit !== undefined ? { limit } : {}),
2419
- ...(query ? { q: query } : {}),
2420
- ...(before ? { before } : {}),
2421
- ...(after ? { after } : {}),
2422
- ...(flags.all ? { all: true } : {}),
2423
- });
2659
+ if (flags.help || flags.h) return showCommandHelp('undo');
2660
+
2661
+ const result = await invokeOperation('canvas.undo', {});
2424
2662
  output(result);
2425
2663
  });
2426
2664
 
2427
- // ── snapshot gc ──────────────────────────────────────────────
2428
- cmd('snapshot gc', 'Delete old snapshots, keeping the newest N', [
2429
- 'pmx-canvas snapshot gc --keep 20 --dry-run',
2430
- 'pmx-canvas snapshot gc --keep 50 --yes',
2431
- ], async (args) => {
2665
+ // ── redo ─────────────────────────────────────────────────────
2666
+ cmd('redo', 'Redo the last undone mutation', ['pmx-canvas redo'], async (args) => {
2432
2667
  const { flags } = parseFlags(args);
2433
- if (flags.help || flags.h) return showCommandHelp('snapshot gc');
2668
+ if (flags.help || flags.h) return showCommandHelp('redo');
2434
2669
 
2435
- const keep = optionalNumberFlag(flags, 'keep', 'Use a positive integer, e.g. --keep 20');
2436
- const dryRun = flags['dry-run'] === true;
2437
- if (!dryRun && !flags.yes) {
2438
- die('Destructive operation requires --yes flag', 'Preview with: pmx-canvas snapshot gc --keep 20 --dry-run');
2439
- }
2440
- const result = await invokeOperation('snapshot.gc', {
2441
- ...(keep !== undefined ? { keep } : {}),
2442
- dryRun,
2443
- });
2670
+ const result = await invokeOperation('canvas.redo', {});
2444
2671
  output(result);
2445
2672
  });
2446
2673
 
2447
- // ── snapshot restore ─────────────────────────────────────────
2448
- cmd('snapshot restore', 'Restore canvas from a snapshot', [
2449
- 'pmx-canvas snapshot restore <snapshot-id-or-name>',
2450
- ], async (args) => {
2451
- const { positional, flags } = parseFlags(args);
2452
- if (flags.help || flags.h) return showCommandHelp('snapshot restore');
2674
+ // ── history ──────────────────────────────────────────────────
2675
+ cmd(
2676
+ 'history',
2677
+ 'Show canvas mutation history',
2678
+ ['pmx-canvas history', 'pmx-canvas history --summary', 'pmx-canvas history --compact'],
2679
+ async (args) => {
2680
+ const { flags } = parseFlags(args);
2681
+ if (flags.help || flags.h) return showCommandHelp('history');
2682
+
2683
+ const result = (await invokeOperation('history.get', {})) as Record<string, unknown>;
2684
+ if (flags.summary) {
2685
+ output(summarizeHistoryResult(result));
2686
+ return;
2687
+ }
2688
+ if (flags.compact) {
2689
+ output(compactHistoryResult(result));
2690
+ return;
2691
+ }
2692
+ output(result);
2693
+ },
2694
+ );
2453
2695
 
2454
- const id = positional[0];
2455
- if (!id) die('Missing snapshot ID or name', 'pmx-canvas snapshot restore <snapshot-id-or-name>');
2696
+ // ── snapshot save ────────────────────────────────────────────
2697
+ cmd(
2698
+ 'snapshot save',
2699
+ 'Save a named snapshot of the current canvas',
2700
+ ['pmx-canvas snapshot save --name "before-refactor"', 'pmx-canvas snapshot save --name checkpoint-1'],
2701
+ async (args) => {
2702
+ const { flags } = parseFlags(args);
2703
+ if (flags.help || flags.h) return showCommandHelp('snapshot save');
2704
+
2705
+ const name = requireFlag(flags, 'name', 'pmx-canvas snapshot save --name "my-snapshot"');
2706
+ const result = await invokeOperation('snapshot.save', { name });
2707
+ output(result);
2708
+ },
2709
+ );
2456
2710
 
2457
- const result = await invokeOperation('snapshot.restore', { id });
2458
- output(result);
2459
- });
2711
+ // ── snapshot list ────────────────────────────────────────────
2712
+ cmd(
2713
+ 'snapshot list',
2714
+ 'List saved snapshots',
2715
+ [
2716
+ 'pmx-canvas snapshot list',
2717
+ 'pmx-canvas snapshot list --limit 50 --query baseline',
2718
+ 'pmx-canvas snapshot list --after 2026-05-01T00:00:00Z --before 2026-05-05T00:00:00Z',
2719
+ 'pmx-canvas snapshot list --all',
2720
+ ],
2721
+ async (args) => {
2722
+ const { flags } = parseFlags(args);
2723
+ if (flags.help || flags.h) return showCommandHelp('snapshot list');
2724
+
2725
+ const limit = optionalNumberFlag(flags, 'limit', 'Use a positive integer, e.g. --limit 50');
2726
+ const query = getStringFlag(flags, 'query', 'q');
2727
+ const before = getStringFlag(flags, 'before');
2728
+ const after = getStringFlag(flags, 'after');
2729
+ const result = await invokeOperation('snapshot.list', {
2730
+ ...(limit !== undefined ? { limit } : {}),
2731
+ ...(query ? { q: query } : {}),
2732
+ ...(before ? { before } : {}),
2733
+ ...(after ? { after } : {}),
2734
+ ...(flags.all ? { all: true } : {}),
2735
+ });
2736
+ output(result);
2737
+ },
2738
+ );
2739
+
2740
+ // ── snapshot gc ──────────────────────────────────────────────
2741
+ cmd(
2742
+ 'snapshot gc',
2743
+ 'Delete old snapshots, keeping the newest N',
2744
+ ['pmx-canvas snapshot gc --keep 20 --dry-run', 'pmx-canvas snapshot gc --keep 50 --yes'],
2745
+ async (args) => {
2746
+ const { flags } = parseFlags(args);
2747
+ if (flags.help || flags.h) return showCommandHelp('snapshot gc');
2748
+
2749
+ const keep = optionalNumberFlag(flags, 'keep', 'Use a positive integer, e.g. --keep 20');
2750
+ const dryRun = flags['dry-run'] === true;
2751
+ if (!dryRun && !flags.yes) {
2752
+ die('Destructive operation requires --yes flag', 'Preview with: pmx-canvas snapshot gc --keep 20 --dry-run');
2753
+ }
2754
+ const result = await invokeOperation('snapshot.gc', {
2755
+ ...(keep !== undefined ? { keep } : {}),
2756
+ dryRun,
2757
+ });
2758
+ output(result);
2759
+ },
2760
+ );
2761
+
2762
+ // ── snapshot restore ─────────────────────────────────────────
2763
+ cmd(
2764
+ 'snapshot restore',
2765
+ 'Restore canvas from a snapshot',
2766
+ ['pmx-canvas snapshot restore <snapshot-id-or-name>'],
2767
+ async (args) => {
2768
+ const { positional, flags } = parseFlags(args);
2769
+ if (flags.help || flags.h) return showCommandHelp('snapshot restore');
2770
+
2771
+ const id = positional[0];
2772
+ if (!id) die('Missing snapshot ID or name', 'pmx-canvas snapshot restore <snapshot-id-or-name>');
2773
+
2774
+ const result = await invokeOperation('snapshot.restore', { id });
2775
+ output(result);
2776
+ },
2777
+ );
2460
2778
 
2461
2779
  // ── snapshot delete ──────────────────────────────────────────
2462
- cmd('snapshot delete', 'Delete a saved snapshot', [
2463
- 'pmx-canvas snapshot delete <snapshot-id>',
2464
- ], async (args) => {
2780
+ cmd('snapshot delete', 'Delete a saved snapshot', ['pmx-canvas snapshot delete <snapshot-id>'], async (args) => {
2465
2781
  const { positional, flags } = parseFlags(args);
2466
2782
  if (flags.help || flags.h) return showCommandHelp('snapshot delete');
2467
2783
 
@@ -2481,171 +2797,196 @@ async function runSnapshotDiff(args: string[]): Promise<void> {
2481
2797
  }
2482
2798
 
2483
2799
  // ── snapshot diff ────────────────────────────────────────────
2484
- cmd('snapshot diff', 'Compare current canvas against a saved snapshot', [
2485
- 'pmx-canvas snapshot diff <snapshot-id>',
2486
- 'pmx-canvas snapshot diff "before-refactor"',
2487
- ], async (args) => {
2488
- const { flags } = parseFlags(args);
2489
- if (flags.help || flags.h) return showCommandHelp('snapshot diff');
2490
- await runSnapshotDiff(args);
2491
- });
2800
+ cmd(
2801
+ 'snapshot diff',
2802
+ 'Compare current canvas against a saved snapshot',
2803
+ ['pmx-canvas snapshot diff <snapshot-id>', 'pmx-canvas snapshot diff "before-refactor"'],
2804
+ async (args) => {
2805
+ const { flags } = parseFlags(args);
2806
+ if (flags.help || flags.h) return showCommandHelp('snapshot diff');
2807
+ await runSnapshotDiff(args);
2808
+ },
2809
+ );
2492
2810
 
2493
2811
  // ── diff ─────────────────────────────────────────────────────
2494
- cmd('diff', 'Compare current canvas against a snapshot', [
2495
- 'pmx-canvas diff <snapshot-id>',
2496
- ], async (args) => {
2812
+ cmd('diff', 'Compare current canvas against a snapshot', ['pmx-canvas diff <snapshot-id>'], async (args) => {
2497
2813
  const { flags } = parseFlags(args);
2498
2814
  if (flags.help || flags.h) return showCommandHelp('diff');
2499
2815
  await runSnapshotDiff(args);
2500
2816
  });
2501
2817
 
2502
2818
  // ── group create ─────────────────────────────────────────────
2503
- cmd('group create', 'Create a group node', [
2504
- 'pmx-canvas group create --title "API Layer" node1 node2',
2505
- 'pmx-canvas group create --title "Quarterly board" --x 40 --y 60 --width 1600 --height 900 --child-layout column node1 node2',
2506
- 'pmx-canvas group create --title "Frontend" --color "#ff6b6b"',
2507
- ], async (args) => {
2508
- const { positional, flags } = parseFlags(args);
2509
- if (flags.help || flags.h) return showCommandHelp('group create');
2510
-
2511
- const body: Record<string, unknown> = {};
2512
- if (flags.title && flags.title !== true) body.title = flags.title;
2513
- if (flags.color && flags.color !== true) body.color = flags.color;
2514
- const x = optionalFiniteFlag(flags, 'x', 'Use a finite number, e.g. --x 40');
2515
- const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 60');
2516
- const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 1600');
2517
- const height = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 900');
2518
- if (x !== undefined) body.x = x;
2519
- if (y !== undefined) body.y = y;
2520
- if (width !== undefined) body.width = width;
2521
- if (height !== undefined) body.height = height;
2522
- if (typeof flags['child-layout'] === 'string') body.childLayout = flags['child-layout'];
2523
- if (positional.length > 0) body.childIds = positional;
2524
-
2525
- const result = await api('POST', '/api/canvas/group', body);
2526
- output(result);
2527
- });
2819
+ cmd(
2820
+ 'group create',
2821
+ 'Create a group node',
2822
+ [
2823
+ 'pmx-canvas group create --title "API Layer" node1 node2',
2824
+ 'pmx-canvas group create --title "Quarterly board" --x 40 --y 60 --width 1600 --height 900 --child-layout column node1 node2',
2825
+ 'pmx-canvas group create --title "Frontend" --color "#ff6b6b"',
2826
+ ],
2827
+ async (args) => {
2828
+ const { positional, flags } = parseFlags(args);
2829
+ if (flags.help || flags.h) return showCommandHelp('group create');
2830
+
2831
+ const body: Record<string, unknown> = {};
2832
+ if (flags.title && flags.title !== true) body.title = flags.title;
2833
+ if (flags.color && flags.color !== true) body.color = flags.color;
2834
+ const x = optionalFiniteFlag(flags, 'x', 'Use a finite number, e.g. --x 40');
2835
+ const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 60');
2836
+ const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 1600');
2837
+ const height = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 900');
2838
+ if (x !== undefined) body.x = x;
2839
+ if (y !== undefined) body.y = y;
2840
+ if (width !== undefined) body.width = width;
2841
+ if (height !== undefined) body.height = height;
2842
+ if (typeof flags['child-layout'] === 'string') body.childLayout = flags['child-layout'];
2843
+ if (positional.length > 0) body.childIds = positional;
2844
+
2845
+ const result = await api('POST', '/api/canvas/group', body);
2846
+ output(result);
2847
+ },
2848
+ );
2528
2849
 
2529
2850
  // ── group add ────────────────────────────────────────────────
2530
- cmd('group add', 'Add nodes to an existing group', [
2531
- 'pmx-canvas group add --group <group-id> node1 node2',
2532
- 'pmx-canvas group add --group <group-id> --child-layout flow node1 node2',
2533
- ], async (args) => {
2534
- const { positional, flags } = parseFlags(args);
2535
- if (flags.help || flags.h) return showCommandHelp('group add');
2536
-
2537
- const groupId = requireFlag(flags, 'group', 'pmx-canvas group add --group <group-id> node1 node2');
2538
- if (positional.length === 0) die('No node IDs provided', 'pmx-canvas group add --group <group-id> node1 node2');
2539
-
2540
- const result = await api('POST', '/api/canvas/group/add', {
2541
- groupId,
2542
- childIds: positional,
2543
- ...(typeof flags['child-layout'] === 'string' ? { childLayout: flags['child-layout'] } : {}),
2544
- });
2545
- output(result);
2546
- });
2851
+ cmd(
2852
+ 'group add',
2853
+ 'Add nodes to an existing group',
2854
+ [
2855
+ 'pmx-canvas group add --group <group-id> node1 node2',
2856
+ 'pmx-canvas group add --group <group-id> --child-layout flow node1 node2',
2857
+ ],
2858
+ async (args) => {
2859
+ const { positional, flags } = parseFlags(args);
2860
+ if (flags.help || flags.h) return showCommandHelp('group add');
2861
+
2862
+ const groupId = requireFlag(flags, 'group', 'pmx-canvas group add --group <group-id> node1 node2');
2863
+ if (positional.length === 0) die('No node IDs provided', 'pmx-canvas group add --group <group-id> node1 node2');
2864
+
2865
+ const result = await api('POST', '/api/canvas/group/add', {
2866
+ groupId,
2867
+ childIds: positional,
2868
+ ...(typeof flags['child-layout'] === 'string' ? { childLayout: flags['child-layout'] } : {}),
2869
+ });
2870
+ output(result);
2871
+ },
2872
+ );
2547
2873
 
2548
2874
  // ── batch ────────────────────────────────────────────────────
2549
- cmd('batch', 'Run a batch of canvas operations from JSON', [
2550
- 'pmx-canvas batch --file ./canvas-ops.json',
2551
- 'pmx-canvas batch --json \'[{\"op\":\"node.add\",\"assign\":\"a\",\"args\":{\"type\":\"markdown\",\"title\":\"A\"}}]\'',
2552
- 'pmx-canvas batch --json \'[{\"op\":\"graph.add\",\"assign\":\"g\",\"args\":{\"graphType\":\"bar\",\"data\":[{\"label\":\"Docs\",\"value\":5}],\"xKey\":\"label\",\"yKey\":\"value\"}}]\'',
2553
- 'cat ops.json | pmx-canvas batch --stdin',
2554
- ], async (args) => {
2555
- const { flags } = parseFlags(args);
2556
- if (flags.help || flags.h) return showCommandHelp('batch');
2875
+ cmd(
2876
+ 'batch',
2877
+ 'Run a batch of canvas operations from JSON',
2878
+ [
2879
+ 'pmx-canvas batch --file ./canvas-ops.json',
2880
+ 'pmx-canvas batch --json \'[{"op":"node.add","assign":"a","args":{"type":"markdown","title":"A"}}]\'',
2881
+ 'pmx-canvas batch --json \'[{"op":"graph.add","assign":"g","args":{"graphType":"bar","data":[{"label":"Docs","value":5}],"xKey":"label","yKey":"value"}}]\'',
2882
+ 'cat ops.json | pmx-canvas batch --stdin',
2883
+ ],
2884
+ async (args) => {
2885
+ const { flags } = parseFlags(args);
2886
+ if (flags.help || flags.h) return showCommandHelp('batch');
2887
+
2888
+ let raw = '';
2889
+ if (typeof flags.file === 'string') {
2890
+ try {
2891
+ raw = readFileSync(flags.file, 'utf-8');
2892
+ } catch (error) {
2893
+ die(
2894
+ `Unable to read --file: ${error instanceof Error ? error.message : String(error)}`,
2895
+ 'Use: pmx-canvas batch --file ./canvas-ops.json',
2896
+ );
2897
+ }
2898
+ } else if (typeof flags.json === 'string') {
2899
+ raw = flags.json;
2900
+ } else if (flags.stdin) {
2901
+ raw = await readStdin();
2902
+ } else {
2903
+ die('Batch operations require --file, --json, or --stdin.', 'Use: pmx-canvas batch --file ./canvas-ops.json');
2904
+ }
2557
2905
 
2558
- let raw = '';
2559
- if (typeof flags.file === 'string') {
2906
+ let parsed: unknown;
2560
2907
  try {
2561
- raw = readFileSync(flags.file, 'utf-8');
2908
+ parsed = JSON.parse(raw);
2562
2909
  } catch (error) {
2563
2910
  die(
2564
- `Unable to read --file: ${error instanceof Error ? error.message : String(error)}`,
2565
- 'Use: pmx-canvas batch --file ./canvas-ops.json',
2911
+ `Invalid batch JSON: ${error instanceof Error ? error.message : String(error)}`,
2912
+ 'Use a JSON array of operations or an object with an "operations" array.',
2566
2913
  );
2567
2914
  }
2568
- } else if (typeof flags.json === 'string') {
2569
- raw = flags.json;
2570
- } else if (flags.stdin) {
2571
- raw = await readStdin();
2572
- } else {
2573
- die(
2574
- 'Batch operations require --file, --json, or --stdin.',
2575
- 'Use: pmx-canvas batch --file ./canvas-ops.json',
2576
- );
2577
- }
2578
2915
 
2579
- let parsed: unknown;
2580
- try {
2581
- parsed = JSON.parse(raw);
2582
- } catch (error) {
2583
- die(
2584
- `Invalid batch JSON: ${error instanceof Error ? error.message : String(error)}`,
2585
- 'Use a JSON array of operations or an object with an "operations" array.',
2916
+ const result = await api(
2917
+ 'POST',
2918
+ '/api/canvas/batch',
2919
+ Array.isArray(parsed) ? { operations: parsed } : (parsed as Record<string, unknown>),
2586
2920
  );
2587
- }
2588
-
2589
- const result = await api('POST', '/api/canvas/batch', Array.isArray(parsed) ? { operations: parsed } : parsed as Record<string, unknown>);
2590
- output(result);
2591
- });
2921
+ output(result);
2922
+ },
2923
+ );
2592
2924
 
2593
2925
  // ── validate ─────────────────────────────────────────────────
2594
- cmd('validate', 'Validate the current layout for collisions and missing edge endpoints', [
2595
- 'pmx-canvas validate',
2596
- ], async (args) => {
2597
- const { flags } = parseFlags(args);
2598
- if (flags.help || flags.h) return showCommandHelp('validate');
2599
-
2600
- const result = await api('GET', '/api/canvas/validate');
2601
- output(result);
2602
- });
2603
-
2604
- cmd('validate spec', 'Validate a json-render spec or graph payload without creating a node', [
2605
- 'pmx-canvas validate spec --type json-render --spec-file ./dashboard.json',
2606
- 'pmx-canvas validate spec --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
2607
- 'pmx-canvas validate spec --type html-primitive --kind choice-grid --data-file ./options.json',
2608
- 'pmx-canvas validate spec --type json-render --spec-file ./dashboard.json --summary',
2609
- ], async (args) => {
2610
- const { flags } = parseFlags(args);
2611
- if (flags.help || flags.h) return showCommandHelp('validate spec');
2612
-
2613
- const type = getStringFlag(flags, 'type');
2614
- if (type !== 'json-render' && type !== 'graph' && type !== 'html-primitive') {
2615
- die('validate spec requires --type json-render, --type graph, or --type html-primitive.');
2616
- }
2926
+ cmd(
2927
+ 'validate',
2928
+ 'Validate the current layout for collisions and missing edge endpoints',
2929
+ ['pmx-canvas validate'],
2930
+ async (args) => {
2931
+ const { flags } = parseFlags(args);
2932
+ if (flags.help || flags.h) return showCommandHelp('validate');
2933
+
2934
+ const result = await api('GET', '/api/canvas/validate');
2935
+ output(result);
2936
+ },
2937
+ );
2938
+
2939
+ cmd(
2940
+ 'validate spec',
2941
+ 'Validate a json-render spec or graph payload without creating a node',
2942
+ [
2943
+ 'pmx-canvas validate spec --type json-render --spec-file ./dashboard.json',
2944
+ 'pmx-canvas validate spec --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
2945
+ 'pmx-canvas validate spec --type html-primitive --kind choice-grid --data-file ./options.json',
2946
+ 'pmx-canvas validate spec --type json-render --spec-file ./dashboard.json --summary',
2947
+ ],
2948
+ async (args) => {
2949
+ const { flags } = parseFlags(args);
2950
+ if (flags.help || flags.h) return showCommandHelp('validate spec');
2951
+
2952
+ const type = getStringFlag(flags, 'type');
2953
+ if (type !== 'json-render' && type !== 'graph' && type !== 'html-primitive') {
2954
+ die('validate spec requires --type json-render, --type graph, or --type html-primitive.');
2955
+ }
2617
2956
 
2618
- let body: Record<string, unknown>;
2619
- if (type === 'json-render') {
2620
- body = { type, spec: (await buildJsonRenderRequestBody({ ...flags, title: String(flags.title ?? 'Validation') })).spec };
2621
- } else if (type === 'html-primitive') {
2622
- const primitiveBody = await buildHtmlPrimitiveRequestBody(flags);
2623
- body = {
2624
- type,
2625
- kind: primitiveBody.primitive,
2626
- ...(typeof primitiveBody.title === 'string' ? { title: primitiveBody.title } : {}),
2627
- ...(isRecord(primitiveBody.data) ? { data: primitiveBody.data } : {}),
2628
- };
2629
- } else {
2630
- body = { type, ...(await buildGraphRequestBody(flags)) };
2631
- }
2957
+ let body: Record<string, unknown>;
2958
+ if (type === 'json-render') {
2959
+ body = {
2960
+ type,
2961
+ spec: (await buildJsonRenderRequestBody({ ...flags, title: String(flags.title ?? 'Validation') })).spec,
2962
+ };
2963
+ } else if (type === 'html-primitive') {
2964
+ const primitiveBody = await buildHtmlPrimitiveRequestBody(flags);
2965
+ body = {
2966
+ type,
2967
+ kind: primitiveBody.primitive,
2968
+ ...(typeof primitiveBody.title === 'string' ? { title: primitiveBody.title } : {}),
2969
+ ...(isRecord(primitiveBody.data) ? { data: primitiveBody.data } : {}),
2970
+ };
2971
+ } else {
2972
+ body = { type, ...(await buildGraphRequestBody(flags)) };
2973
+ }
2632
2974
 
2633
- const result = await api('POST', '/api/canvas/schema/validate', body) as Record<string, unknown>;
2634
- if (flags.summary) {
2635
- output({
2636
- ok: result.ok,
2637
- type: result.type,
2638
- summary: result.summary,
2639
- });
2640
- return;
2641
- }
2642
- output(result);
2643
- });
2975
+ const result = (await api('POST', '/api/canvas/schema/validate', body)) as Record<string, unknown>;
2976
+ if (flags.summary) {
2977
+ output({
2978
+ ok: result.ok,
2979
+ type: result.type,
2980
+ summary: result.summary,
2981
+ });
2982
+ return;
2983
+ }
2984
+ output(result);
2985
+ },
2986
+ );
2644
2987
 
2645
2988
  // ── group remove ─────────────────────────────────────────────
2646
- cmd('group remove', 'Ungroup all children from a group', [
2647
- 'pmx-canvas group remove <group-id>',
2648
- ], async (args) => {
2989
+ cmd('group remove', 'Ungroup all children from a group', ['pmx-canvas group remove <group-id>'], async (args) => {
2649
2990
  const { positional, flags } = parseFlags(args);
2650
2991
  if (flags.help || flags.h) return showCommandHelp('group remove');
2651
2992
 
@@ -2657,46 +2998,51 @@ cmd('group remove', 'Ungroup all children from a group', [
2657
2998
  });
2658
2999
 
2659
3000
  // ── web-artifact build ───────────────────────────────────────
2660
- cmd('web-artifact build', 'Build a bundled HTML web artifact and optionally open it on the canvas', [
2661
- 'pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx',
2662
- 'pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx --index-css-file ./index.css',
2663
- 'pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx --include-logs',
2664
- ], async (args) => {
2665
- const { flags } = parseFlags(args);
2666
- if (flags.help || flags.h) return showCommandHelp('web-artifact build');
2667
- await runWebArtifactBuildCommand(flags);
2668
- });
3001
+ cmd(
3002
+ 'web-artifact build',
3003
+ 'Build a bundled HTML web artifact and optionally open it on the canvas',
3004
+ [
3005
+ 'pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx',
3006
+ 'pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx --index-css-file ./index.css',
3007
+ 'pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx --include-logs',
3008
+ ],
3009
+ async (args) => {
3010
+ const { flags } = parseFlags(args);
3011
+ if (flags.help || flags.h) return showCommandHelp('web-artifact build');
3012
+ await runWebArtifactBuildCommand(flags);
3013
+ },
3014
+ );
2669
3015
 
2670
3016
  // ── clear ────────────────────────────────────────────────────
2671
- cmd('clear', 'Remove all nodes and edges from the canvas', [
2672
- 'pmx-canvas clear --yes',
2673
- 'pmx-canvas clear --dry-run',
2674
- ], async (args) => {
2675
- const { flags } = parseFlags(args);
2676
- if (flags.help || flags.h) return showCommandHelp('clear');
2677
-
2678
- if (flags['dry-run']) {
2679
- const layout = (await api('GET', '/api/canvas/state')) as { nodes: unknown[]; edges: unknown[] };
2680
- output({
2681
- dry_run: true,
2682
- would_remove: { nodes: layout.nodes.length, edges: layout.edges.length },
2683
- message: 'No changes made. Pass --yes to confirm.',
2684
- });
2685
- return;
2686
- }
3017
+ cmd(
3018
+ 'clear',
3019
+ 'Remove all nodes and edges from the canvas',
3020
+ ['pmx-canvas clear --yes', 'pmx-canvas clear --dry-run'],
3021
+ async (args) => {
3022
+ const { flags } = parseFlags(args);
3023
+ if (flags.help || flags.h) return showCommandHelp('clear');
3024
+
3025
+ if (flags['dry-run']) {
3026
+ const layout = (await api('GET', '/api/canvas/state')) as { nodes: unknown[]; edges: unknown[] };
3027
+ output({
3028
+ dry_run: true,
3029
+ would_remove: { nodes: layout.nodes.length, edges: layout.edges.length },
3030
+ message: 'No changes made. Pass --yes to confirm.',
3031
+ });
3032
+ return;
3033
+ }
2687
3034
 
2688
- if (!flags.yes) {
2689
- die('Destructive operation requires --yes flag', 'pmx-canvas clear --yes (or preview with --dry-run)');
2690
- }
3035
+ if (!flags.yes) {
3036
+ die('Destructive operation requires --yes flag', 'pmx-canvas clear --yes (or preview with --dry-run)');
3037
+ }
2691
3038
 
2692
- const result = await api('POST', '/api/canvas/clear');
2693
- output(result);
2694
- });
3039
+ const result = await api('POST', '/api/canvas/clear');
3040
+ output(result);
3041
+ },
3042
+ );
2695
3043
 
2696
3044
  // ── webview status ────────────────────────────────────────────
2697
- cmd('webview status', 'Show Bun.WebView automation status', [
2698
- 'pmx-canvas webview status',
2699
- ], async (args) => {
3045
+ cmd('webview status', 'Show Bun.WebView automation status', ['pmx-canvas webview status'], async (args) => {
2700
3046
  const { flags } = parseFlags(args);
2701
3047
  if (flags.help || flags.h) return showCommandHelp('webview status');
2702
3048
 
@@ -2705,51 +3051,54 @@ cmd('webview status', 'Show Bun.WebView automation status', [
2705
3051
  });
2706
3052
 
2707
3053
  // ── webview start ─────────────────────────────────────────────
2708
- cmd('webview start', 'Start or replace the Bun.WebView automation session', [
2709
- 'pmx-canvas webview start',
2710
- 'pmx-canvas webview start --backend chrome --width 1440 --height 900',
2711
- 'pmx-canvas webview start --chrome-path /Applications/Google\\ Chrome.app/.../Google\\ Chrome',
2712
- ], async (args) => {
2713
- const { flags } = parseFlags(args);
2714
- if (flags.help || flags.h) return showCommandHelp('webview start');
2715
-
2716
- const backend = flags.backend;
2717
- if (backend && backend !== true && backend !== 'chrome' && backend !== 'webkit') {
2718
- die('Invalid value for --backend', 'Use: --backend chrome or --backend webkit');
2719
- }
3054
+ cmd(
3055
+ 'webview start',
3056
+ 'Start or replace the Bun.WebView automation session',
3057
+ [
3058
+ 'pmx-canvas webview start',
3059
+ 'pmx-canvas webview start --backend chrome --width 1440 --height 900',
3060
+ 'pmx-canvas webview start --chrome-path /Applications/Google\\ Chrome.app/.../Google\\ Chrome',
3061
+ ],
3062
+ async (args) => {
3063
+ const { flags } = parseFlags(args);
3064
+ if (flags.help || flags.h) return showCommandHelp('webview start');
3065
+
3066
+ const backend = flags.backend;
3067
+ if (backend && backend !== true && backend !== 'chrome' && backend !== 'webkit') {
3068
+ die('Invalid value for --backend', 'Use: --backend chrome or --backend webkit');
3069
+ }
2720
3070
 
2721
- const body: Record<string, unknown> = {};
2722
- if (backend && backend !== true) body.backend = backend;
3071
+ const body: Record<string, unknown> = {};
3072
+ if (backend && backend !== true) body.backend = backend;
2723
3073
 
2724
- const width = optionalNumberFlag(flags, 'width', 'Use a positive integer width, e.g. --width 1440');
2725
- const height = optionalNumberFlag(flags, 'height', 'Use a positive integer height, e.g. --height 900');
2726
- if (width !== undefined) body.width = width;
2727
- if (height !== undefined) body.height = height;
3074
+ const width = optionalNumberFlag(flags, 'width', 'Use a positive integer width, e.g. --width 1440');
3075
+ const height = optionalNumberFlag(flags, 'height', 'Use a positive integer height, e.g. --height 900');
3076
+ if (width !== undefined) body.width = width;
3077
+ if (height !== undefined) body.height = height;
2728
3078
 
2729
- if (flags['chrome-path'] && flags['chrome-path'] !== true) {
2730
- body.chromePath = flags['chrome-path'];
2731
- }
3079
+ if (flags['chrome-path'] && flags['chrome-path'] !== true) {
3080
+ body.chromePath = flags['chrome-path'];
3081
+ }
2732
3082
 
2733
- if (flags['data-dir'] && flags['data-dir'] !== true) {
2734
- body.dataStoreDir = flags['data-dir'];
2735
- }
3083
+ if (flags['data-dir'] && flags['data-dir'] !== true) {
3084
+ body.dataStoreDir = flags['data-dir'];
3085
+ }
2736
3086
 
2737
- if (flags['chrome-argv'] && flags['chrome-argv'] !== true) {
2738
- const chromeArgv = String(flags['chrome-argv'])
2739
- .split(',')
2740
- .map((value) => value.trim())
2741
- .filter((value) => value.length > 0);
2742
- if (chromeArgv.length > 0) body.chromeArgv = chromeArgv;
2743
- }
3087
+ if (flags['chrome-argv'] && flags['chrome-argv'] !== true) {
3088
+ const chromeArgv = String(flags['chrome-argv'])
3089
+ .split(',')
3090
+ .map((value) => value.trim())
3091
+ .filter((value) => value.length > 0);
3092
+ if (chromeArgv.length > 0) body.chromeArgv = chromeArgv;
3093
+ }
2744
3094
 
2745
- const result = await api('POST', '/api/workbench/webview/start', body, { allowErrorJson: true });
2746
- output(result);
2747
- });
3095
+ const result = await api('POST', '/api/workbench/webview/start', body, { allowErrorJson: true });
3096
+ output(result);
3097
+ },
3098
+ );
2748
3099
 
2749
3100
  // ── webview stop ──────────────────────────────────────────────
2750
- cmd('webview stop', 'Stop the active Bun.WebView automation session', [
2751
- 'pmx-canvas webview stop',
2752
- ], async (args) => {
3101
+ cmd('webview stop', 'Stop the active Bun.WebView automation session', ['pmx-canvas webview stop'], async (args) => {
2753
3102
  const { flags } = parseFlags(args);
2754
3103
  if (flags.help || flags.h) return showCommandHelp('webview stop');
2755
3104
 
@@ -2758,137 +3107,148 @@ cmd('webview stop', 'Stop the active Bun.WebView automation session', [
2758
3107
  });
2759
3108
 
2760
3109
  // ── webview evaluate ──────────────────────────────────────────
2761
- cmd('webview evaluate', 'Evaluate JavaScript in the active Bun.WebView automation session', [
2762
- 'pmx-canvas webview evaluate --expression "document.title"',
2763
- 'pmx-canvas webview evaluate --script "const title = document.title; return title.toUpperCase()"',
2764
- 'pmx-canvas webview evaluate --file ./probe.js',
2765
- ], async (args) => {
2766
- const { flags } = parseFlags(args);
2767
- if (flags.help || flags.h) return showCommandHelp('webview evaluate');
2768
-
2769
- const sourceCount = [flags.expression, flags.script, flags.file].filter(Boolean).length;
2770
- if (sourceCount > 1) {
2771
- die('Use only one of --expression, --script, or --file.', 'pmx-canvas webview evaluate --expression "document.title"');
2772
- }
2773
-
2774
- let expression = '';
2775
- if (typeof flags.file === 'string') {
2776
- let script = '';
2777
- try {
2778
- script = readFileSync(flags.file, 'utf-8');
2779
- } catch (error) {
3110
+ cmd(
3111
+ 'webview evaluate',
3112
+ 'Evaluate JavaScript in the active Bun.WebView automation session',
3113
+ [
3114
+ 'pmx-canvas webview evaluate --expression "document.title"',
3115
+ 'pmx-canvas webview evaluate --script "const title = document.title; return title.toUpperCase()"',
3116
+ 'pmx-canvas webview evaluate --file ./probe.js',
3117
+ ],
3118
+ async (args) => {
3119
+ const { flags } = parseFlags(args);
3120
+ if (flags.help || flags.h) return showCommandHelp('webview evaluate');
3121
+
3122
+ const sourceCount = [flags.expression, flags.script, flags.file].filter(Boolean).length;
3123
+ if (sourceCount > 1) {
2780
3124
  die(
2781
- `Unable to read --file: ${error instanceof Error ? error.message : String(error)}`,
2782
- 'pmx-canvas webview evaluate --file ./probe.js',
3125
+ 'Use only one of --expression, --script, or --file.',
3126
+ 'pmx-canvas webview evaluate --expression "document.title"',
2783
3127
  );
2784
3128
  }
2785
- expression = wrapCanvasAutomationScript(script);
2786
- } else if (typeof flags.script === 'string') {
2787
- expression = wrapCanvasAutomationScript(flags.script);
2788
- } else {
2789
- expression = requireFlag(
2790
- flags,
2791
- 'expression',
2792
- 'pmx-canvas webview evaluate --expression "document.title"',
2793
- );
2794
- }
2795
3129
 
2796
- const result = await api('POST', '/api/workbench/webview/evaluate', { expression });
2797
- output(result);
2798
- });
3130
+ let expression = '';
3131
+ if (typeof flags.file === 'string') {
3132
+ let script = '';
3133
+ try {
3134
+ script = readFileSync(flags.file, 'utf-8');
3135
+ } catch (error) {
3136
+ die(
3137
+ `Unable to read --file: ${error instanceof Error ? error.message : String(error)}`,
3138
+ 'pmx-canvas webview evaluate --file ./probe.js',
3139
+ );
3140
+ }
3141
+ expression = wrapCanvasAutomationScript(script);
3142
+ } else if (typeof flags.script === 'string') {
3143
+ expression = wrapCanvasAutomationScript(flags.script);
3144
+ } else {
3145
+ expression = requireFlag(flags, 'expression', 'pmx-canvas webview evaluate --expression "document.title"');
3146
+ }
2799
3147
 
2800
- // ── webview resize ────────────────────────────────────────────
2801
- cmd('webview resize', 'Resize the active Bun.WebView automation session viewport', [
2802
- 'pmx-canvas webview resize --width 1280 --height 800',
2803
- ], async (args) => {
2804
- const { flags } = parseFlags(args);
2805
- if (flags.help || flags.h) return showCommandHelp('webview resize');
3148
+ const result = await api('POST', '/api/workbench/webview/evaluate', { expression });
3149
+ output(result);
3150
+ },
3151
+ );
2806
3152
 
2807
- const width = optionalNumberFlag(flags, 'width', 'Use: pmx-canvas webview resize --width 1280 --height 800');
2808
- const height = optionalNumberFlag(flags, 'height', 'Use: pmx-canvas webview resize --width 1280 --height 800');
2809
- if (width === undefined || height === undefined) {
2810
- die('Missing required flags: --width, --height', 'Use: pmx-canvas webview resize --width 1280 --height 800');
2811
- }
3153
+ // ── webview resize ────────────────────────────────────────────
3154
+ cmd(
3155
+ 'webview resize',
3156
+ 'Resize the active Bun.WebView automation session viewport',
3157
+ ['pmx-canvas webview resize --width 1280 --height 800'],
3158
+ async (args) => {
3159
+ const { flags } = parseFlags(args);
3160
+ if (flags.help || flags.h) return showCommandHelp('webview resize');
3161
+
3162
+ const width = optionalNumberFlag(flags, 'width', 'Use: pmx-canvas webview resize --width 1280 --height 800');
3163
+ const height = optionalNumberFlag(flags, 'height', 'Use: pmx-canvas webview resize --width 1280 --height 800');
3164
+ if (width === undefined || height === undefined) {
3165
+ die('Missing required flags: --width, --height', 'Use: pmx-canvas webview resize --width 1280 --height 800');
3166
+ }
2812
3167
 
2813
- const result = await api('POST', '/api/workbench/webview/resize', { width, height });
2814
- output(result);
2815
- });
3168
+ const result = await api('POST', '/api/workbench/webview/resize', { width, height });
3169
+ output(result);
3170
+ },
3171
+ );
2816
3172
 
2817
3173
  // ── webview screenshot ────────────────────────────────────────
2818
- cmd('webview screenshot', 'Capture a screenshot from the active Bun.WebView automation session', [
2819
- 'pmx-canvas webview screenshot --output ./canvas.png',
2820
- 'pmx-canvas webview screenshot --output ./canvas.webp --format webp --quality 80',
2821
- ], async (args) => {
2822
- const { flags } = parseFlags(args);
2823
- if (flags.help || flags.h) return showCommandHelp('webview screenshot');
2824
-
2825
- const outputPath = requireFlag(
2826
- flags,
2827
- 'output',
3174
+ cmd(
3175
+ 'webview screenshot',
3176
+ 'Capture a screenshot from the active Bun.WebView automation session',
3177
+ [
2828
3178
  'pmx-canvas webview screenshot --output ./canvas.png',
2829
- );
2830
-
2831
- const body: Record<string, unknown> = {};
2832
- if (flags.format && flags.format !== true) {
2833
- const format = String(flags.format);
2834
- if (format !== 'png' && format !== 'jpeg' && format !== 'webp') {
2835
- die('Invalid value for --format', 'Use: --format png, jpeg, or webp');
3179
+ 'pmx-canvas webview screenshot --output ./canvas.webp --format webp --quality 80',
3180
+ ],
3181
+ async (args) => {
3182
+ const { flags } = parseFlags(args);
3183
+ if (flags.help || flags.h) return showCommandHelp('webview screenshot');
3184
+
3185
+ const outputPath = requireFlag(flags, 'output', 'pmx-canvas webview screenshot --output ./canvas.png');
3186
+
3187
+ const body: Record<string, unknown> = {};
3188
+ if (flags.format && flags.format !== true) {
3189
+ const format = String(flags.format);
3190
+ if (format !== 'png' && format !== 'jpeg' && format !== 'webp') {
3191
+ die('Invalid value for --format', 'Use: --format png, jpeg, or webp');
3192
+ }
3193
+ body.format = format;
2836
3194
  }
2837
- body.format = format;
2838
- }
2839
3195
 
2840
- if (flags.quality && flags.quality !== true) {
2841
- const quality = Number(flags.quality);
2842
- if (!Number.isFinite(quality)) {
2843
- die(`Invalid value for --quality: ${String(flags.quality)}`, 'Use a numeric quality, e.g. --quality 80');
3196
+ if (flags.quality && flags.quality !== true) {
3197
+ const quality = Number(flags.quality);
3198
+ if (!Number.isFinite(quality)) {
3199
+ die(`Invalid value for --quality: ${String(flags.quality)}`, 'Use a numeric quality, e.g. --quality 80');
3200
+ }
3201
+ body.quality = quality;
2844
3202
  }
2845
- body.quality = quality;
2846
- }
2847
3203
 
2848
- const base = getBaseUrl();
2849
- const response = await fetch(`${base}/api/workbench/webview/screenshot`, {
2850
- method: 'POST',
2851
- headers: { 'Content-Type': 'application/json' },
2852
- body: JSON.stringify(body),
2853
- });
3204
+ const base = getBaseUrl();
3205
+ const response = await fetch(`${base}/api/workbench/webview/screenshot`, {
3206
+ method: 'POST',
3207
+ headers: { 'Content-Type': 'application/json' },
3208
+ body: JSON.stringify(body),
3209
+ });
2854
3210
 
2855
- if (!response.ok) {
2856
- const text = await response.text();
2857
- try {
2858
- const json = JSON.parse(text) as Record<string, unknown>;
2859
- die(
2860
- json.error ? String(json.error) : `HTTP ${response.status}`,
2861
- typeof json.hint === 'string' ? json.hint : undefined,
2862
- );
2863
- } catch {
2864
- die(`HTTP ${response.status}: ${text}`);
3211
+ if (!response.ok) {
3212
+ const text = await response.text();
3213
+ try {
3214
+ const json = JSON.parse(text) as Record<string, unknown>;
3215
+ die(
3216
+ json.error ? String(json.error) : `HTTP ${response.status}`,
3217
+ typeof json.hint === 'string' ? json.hint : undefined,
3218
+ );
3219
+ } catch {
3220
+ die(`HTTP ${response.status}: ${text}`);
3221
+ }
2865
3222
  }
2866
- }
2867
-
2868
- const bytes = new Uint8Array(await response.arrayBuffer());
2869
- writeFileSync(outputPath, bytes);
2870
- output({
2871
- ok: true,
2872
- output: outputPath,
2873
- bytes: bytes.byteLength,
2874
- mimeType: response.headers.get('Content-Type') ?? 'application/octet-stream',
2875
- });
2876
- });
2877
3223
 
2878
- cmd('screenshot', 'Capture a screenshot from the active Bun.WebView automation session', [
2879
- 'pmx-canvas screenshot --output ./canvas.png',
2880
- 'pmx-canvas screenshot --output ./canvas.webp --format webp --quality 80',
2881
- ], async (args) => {
2882
- if (args.includes('--help') || args.includes('-h')) return showCommandHelp('screenshot');
2883
- const screenshotCommand = COMMANDS['webview screenshot'];
2884
- if (!screenshotCommand) die('Internal error: webview screenshot command is unavailable.');
2885
- await screenshotCommand.run(args);
2886
- });
3224
+ const bytes = new Uint8Array(await response.arrayBuffer());
3225
+ writeFileSync(outputPath, bytes);
3226
+ output({
3227
+ ok: true,
3228
+ output: outputPath,
3229
+ bytes: bytes.byteLength,
3230
+ mimeType: response.headers.get('Content-Type') ?? 'application/octet-stream',
3231
+ });
3232
+ },
3233
+ );
3234
+
3235
+ cmd(
3236
+ 'screenshot',
3237
+ 'Capture a screenshot from the active Bun.WebView automation session',
3238
+ [
3239
+ 'pmx-canvas screenshot --output ./canvas.png',
3240
+ 'pmx-canvas screenshot --output ./canvas.webp --format webp --quality 80',
3241
+ ],
3242
+ async (args) => {
3243
+ if (args.includes('--help') || args.includes('-h')) return showCommandHelp('screenshot');
3244
+ const screenshotCommand = COMMANDS['webview screenshot'];
3245
+ if (!screenshotCommand) die('Internal error: webview screenshot command is unavailable.');
3246
+ await screenshotCommand.run(args);
3247
+ },
3248
+ );
2887
3249
 
2888
3250
  // ── code-graph ───────────────────────────────────────────────
2889
- cmd('code-graph', 'Show auto-detected file dependency graph', [
2890
- 'pmx-canvas code-graph',
2891
- ], async (args) => {
3251
+ cmd('code-graph', 'Show auto-detected file dependency graph', ['pmx-canvas code-graph'], async (args) => {
2892
3252
  const { flags } = parseFlags(args);
2893
3253
  if (flags.help || flags.h) return showCommandHelp('code-graph');
2894
3254
 
@@ -2897,9 +3257,7 @@ cmd('code-graph', 'Show auto-detected file dependency graph', [
2897
3257
  });
2898
3258
 
2899
3259
  // ── spatial ──────────────────────────────────────────────────
2900
- cmd('spatial', 'Spatial analysis: clusters, reading order, neighborhoods', [
2901
- 'pmx-canvas spatial',
2902
- ], async (args) => {
3260
+ cmd('spatial', 'Spatial analysis: clusters, reading order, neighborhoods', ['pmx-canvas spatial'], async (args) => {
2903
3261
  const { flags } = parseFlags(args);
2904
3262
  if (flags.help || flags.h) return showCommandHelp('spatial');
2905
3263
 
@@ -2908,102 +3266,119 @@ cmd('spatial', 'Spatial analysis: clusters, reading order, neighborhoods', [
2908
3266
  });
2909
3267
 
2910
3268
  // ── watch ────────────────────────────────────────────────────
2911
- cmd('watch', 'Watch low-token semantic canvas changes over the existing SSE stream', [
2912
- 'pmx-canvas watch',
2913
- 'pmx-canvas watch --json',
2914
- 'pmx-canvas watch --events context-pin,move-end',
2915
- 'pmx-canvas watch --json --events connect --max-events 1',
2916
- ], async (args) => {
2917
- const { flags } = parseFlags(args);
2918
- if (flags.help || flags.h) return showCommandHelp('watch');
2919
-
2920
- if (flags.json && flags.compact) {
2921
- die('Use either --json or --compact, not both.');
2922
- }
3269
+ cmd(
3270
+ 'watch',
3271
+ 'Watch low-token semantic canvas changes over the existing SSE stream',
3272
+ [
3273
+ 'pmx-canvas watch',
3274
+ 'pmx-canvas watch --json',
3275
+ 'pmx-canvas watch --events context-pin,move-end',
3276
+ 'pmx-canvas watch --json --events connect --max-events 1',
3277
+ ],
3278
+ async (args) => {
3279
+ const { flags } = parseFlags(args);
3280
+ if (flags.help || flags.h) return showCommandHelp('watch');
3281
+
3282
+ if (flags.json && flags.compact) {
3283
+ die('Use either --json or --compact, not both.');
3284
+ }
2923
3285
 
2924
- const filtersRaw = typeof flags.events === 'string' ? flags.events : undefined;
2925
- const requestedFilters = filtersRaw
2926
- ? Array.from(new Set(filtersRaw.split(',').map((value) => value.trim()).filter((value) => value.length > 0)))
2927
- : [];
2928
- const invalidFilter = requestedFilters.find((value) => !ALL_SEMANTIC_WATCH_EVENT_TYPES.includes(value as typeof ALL_SEMANTIC_WATCH_EVENT_TYPES[number]));
2929
- if (invalidFilter) {
2930
- die(
2931
- `Invalid value in --events: ${invalidFilter}`,
2932
- 'Use a comma-separated subset of: context-pin,move-end,group,connect,remove',
2933
- );
2934
- }
2935
- const filters = parseSemanticEventFilter(filtersRaw);
2936
- if (filtersRaw && filters.size === 0) {
2937
- die(
2938
- `Invalid value for --events: ${filtersRaw}`,
2939
- 'Use a comma-separated subset of: context-pin,move-end,group,connect,remove',
3286
+ const filtersRaw = typeof flags.events === 'string' ? flags.events : undefined;
3287
+ const requestedFilters = filtersRaw
3288
+ ? Array.from(
3289
+ new Set(
3290
+ filtersRaw
3291
+ .split(',')
3292
+ .map((value) => value.trim())
3293
+ .filter((value) => value.length > 0),
3294
+ ),
3295
+ )
3296
+ : [];
3297
+ const invalidFilter = requestedFilters.find(
3298
+ (value) => !ALL_SEMANTIC_WATCH_EVENT_TYPES.includes(value as (typeof ALL_SEMANTIC_WATCH_EVENT_TYPES)[number]),
2940
3299
  );
2941
- }
3300
+ if (invalidFilter) {
3301
+ die(
3302
+ `Invalid value in --events: ${invalidFilter}`,
3303
+ 'Use a comma-separated subset of: context-pin,move-end,group,connect,remove',
3304
+ );
3305
+ }
3306
+ const filters = parseSemanticEventFilter(filtersRaw);
3307
+ if (filtersRaw && filters.size === 0) {
3308
+ die(
3309
+ `Invalid value for --events: ${filtersRaw}`,
3310
+ 'Use a comma-separated subset of: context-pin,move-end,group,connect,remove',
3311
+ );
3312
+ }
2942
3313
 
2943
- const maxEvents = optionalNumberFlag(flags, 'max-events', 'Use a positive integer, e.g. --max-events 1');
2944
- const jsonMode = Boolean(flags.json);
2945
- const reducer = new SemanticWatchReducer();
2946
- const pinned = await api('GET', '/api/canvas/pinned-context') as { nodeIds?: string[] };
2947
- reducer.setInitialPins(Array.isArray(pinned.nodeIds) ? pinned.nodeIds : []);
3314
+ const maxEvents = optionalNumberFlag(flags, 'max-events', 'Use a positive integer, e.g. --max-events 1');
3315
+ const jsonMode = Boolean(flags.json);
3316
+ const reducer = new SemanticWatchReducer();
3317
+ const pinned = (await api('GET', '/api/canvas/pinned-context')) as { nodeIds?: string[] };
3318
+ reducer.setInitialPins(Array.isArray(pinned.nodeIds) ? pinned.nodeIds : []);
2948
3319
 
2949
- const base = getBaseUrl();
2950
- const controller = new AbortController();
2951
- let response: Response;
2952
- try {
2953
- response = await fetch(`${base}/api/workbench/events`, {
2954
- method: 'GET',
2955
- headers: { Accept: 'text/event-stream' },
2956
- signal: controller.signal,
2957
- });
2958
- } catch (error) {
2959
- die(
2960
- `Cannot connect to pmx-canvas event stream at ${base}: ${error instanceof Error ? error.message : String(error)}`,
2961
- 'Start the server first: pmx-canvas --no-open',
2962
- );
2963
- }
3320
+ const base = getBaseUrl();
3321
+ const controller = new AbortController();
3322
+ let response: Response;
3323
+ try {
3324
+ response = await fetch(`${base}/api/workbench/events`, {
3325
+ method: 'GET',
3326
+ headers: { Accept: 'text/event-stream' },
3327
+ signal: controller.signal,
3328
+ });
3329
+ } catch (error) {
3330
+ die(
3331
+ `Cannot connect to pmx-canvas event stream at ${base}: ${error instanceof Error ? error.message : String(error)}`,
3332
+ 'Start the server first: pmx-canvas --no-open',
3333
+ );
3334
+ }
2964
3335
 
2965
- if (!response.ok) {
2966
- const text = await response.text();
2967
- die(`Failed to open event stream: HTTP ${response.status}`, text);
2968
- }
2969
- if (!response.body) {
2970
- die('Workbench event stream did not return a readable body.');
2971
- }
3336
+ if (!response.ok) {
3337
+ const text = await response.text();
3338
+ die(`Failed to open event stream: HTTP ${response.status}`, text);
3339
+ }
3340
+ if (!response.body) {
3341
+ die('Workbench event stream did not return a readable body.');
3342
+ }
2972
3343
 
2973
- let emitted = 0;
2974
- try {
2975
- for await (const message of parseSseStream(response.body)) {
2976
- const semanticEvents = reducer
2977
- .handleMessage(message)
2978
- .filter((event) => filters.has(event.type));
2979
-
2980
- for (const event of semanticEvents) {
2981
- console.log(jsonMode ? JSON.stringify(event) : formatCompactWatchEvent(event));
2982
- emitted++;
2983
- if (maxEvents !== undefined && emitted >= maxEvents) {
2984
- controller.abort();
2985
- return;
3344
+ let emitted = 0;
3345
+ try {
3346
+ for await (const message of parseSseStream(response.body)) {
3347
+ const semanticEvents = reducer.handleMessage(message).filter((event) => filters.has(event.type));
3348
+
3349
+ for (const event of semanticEvents) {
3350
+ console.log(jsonMode ? JSON.stringify(event) : formatCompactWatchEvent(event));
3351
+ emitted++;
3352
+ if (maxEvents !== undefined && emitted >= maxEvents) {
3353
+ controller.abort();
3354
+ return;
3355
+ }
2986
3356
  }
2987
3357
  }
3358
+ } catch (error) {
3359
+ if (controller.signal.aborted) return;
3360
+ die(
3361
+ `Watch stream failed: ${error instanceof Error ? error.message : String(error)}`,
3362
+ 'Ensure the server is still running and reachable.',
3363
+ );
2988
3364
  }
2989
- } catch (error) {
2990
- if (controller.signal.aborted) return;
2991
- die(
2992
- `Watch stream failed: ${error instanceof Error ? error.message : String(error)}`,
2993
- 'Ensure the server is still running and reachable.',
2994
- );
2995
- }
2996
- });
3365
+ },
3366
+ );
2997
3367
 
2998
3368
  // ── serve (delegates back to original CLI) ───────────────────
2999
- cmd('serve', 'Start the canvas server', [
3000
- 'pmx-canvas serve',
3001
- 'pmx-canvas serve --port=8080 --no-open',
3002
- 'pmx-canvas serve --demo --theme=light',
3003
- 'pmx-canvas --no-open --webview-automation',
3004
- ], async (_args) => {
3005
- console.log('Use: pmx-canvas [--port=PORT] [--demo] [--no-open] [--theme=THEME] [--webview-automation]');
3006
- });
3369
+ cmd(
3370
+ 'serve',
3371
+ 'Start the canvas server',
3372
+ [
3373
+ 'pmx-canvas serve',
3374
+ 'pmx-canvas serve --port=8080 --no-open',
3375
+ 'pmx-canvas serve --demo --theme=light',
3376
+ 'pmx-canvas --no-open --webview-automation',
3377
+ ],
3378
+ async (_args) => {
3379
+ console.log('Use: pmx-canvas [--port=PORT] [--demo] [--no-open] [--theme=THEME] [--webview-automation]');
3380
+ },
3381
+ );
3007
3382
 
3008
3383
  // ── Help ─────────────────────────────────────────────────────
3009
3384
 
@@ -3049,8 +3424,12 @@ function showCommandHelp(name: string): void {
3049
3424
  }
3050
3425
  if (name === 'node add' || name === 'graph add' || name === 'validate spec') {
3051
3426
  console.log('\nGraph flags:');
3052
- console.log(' Graph fields accept kebab-case CLI flags and camelCase schema names, e.g. --graph-type/--graphType and --x-key/--xKey');
3053
- console.log(' Use --node-height/--nodeHeight for canvas frame height; use --chart-height for chart content height. --height is kept as a frame-height alias for compatibility.');
3427
+ console.log(
3428
+ ' Graph fields accept kebab-case CLI flags and camelCase schema names, e.g. --graph-type/--graphType and --x-key/--xKey',
3429
+ );
3430
+ console.log(
3431
+ ' Use --node-height/--nodeHeight for canvas frame height; use --chart-height for chart content height. --height is kept as a frame-height alias for compatibility.',
3432
+ );
3054
3433
  console.log(' Pass --show-legend false to hide legends in compact node layouts.');
3055
3434
  }
3056
3435
  if (name === 'validate spec') {
@@ -3220,6 +3599,8 @@ Analysis:
3220
3599
 
3221
3600
  Global flags:
3222
3601
  --help, -h Show help for any command
3602
+ --port <n> Target daemon port for this invocation (overrides PMX_CANVAS_PORT)
3603
+ --server-url <url> Target server URL for this invocation (overrides PMX_CANVAS_URL; wins over --port)
3223
3604
 
3224
3605
  Environment:
3225
3606
  PMX_CANVAS_URL Server URL (default: http://localhost:4313)
@@ -3277,7 +3658,8 @@ async function readStdin(): Promise<string> {
3277
3658
 
3278
3659
  // ── Router ───────────────────────────────────────────────────
3279
3660
 
3280
- export async function runAgentCli(args: string[]): Promise<void> {
3661
+ export async function runAgentCli(rawArgs: string[]): Promise<void> {
3662
+ const args = extractGlobalTargetFlags(rawArgs);
3281
3663
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
3282
3664
  showTopLevelHelp();
3283
3665
  return;
@@ -3326,9 +3708,7 @@ export async function runAgentCli(args: string[]): Promise<void> {
3326
3708
  `Available subcommands: ${available}`,
3327
3709
  ].filter((hint): hint is string => typeof hint === 'string');
3328
3710
  die(
3329
- subcommand
3330
- ? `Unknown ${oneWord} subcommand: "${subcommand}".`
3331
- : `Missing ${oneWord} subcommand.`,
3711
+ subcommand ? `Unknown ${oneWord} subcommand: "${subcommand}".` : `Missing ${oneWord} subcommand.`,
3332
3712
  hints.join(' '),
3333
3713
  );
3334
3714
  }