pmx-canvas 0.2.7 → 0.3.1

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