pmx-canvas 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/Readme.md +13 -4
  3. package/dist/canvas/index.js +71 -71
  4. package/dist/json-render/index.js +47 -47
  5. package/dist/types/cli/agent.d.ts +13 -8
  6. package/dist/types/cli/commands/apps.d.ts +1 -0
  7. package/dist/types/cli/commands/ax.d.ts +1 -0
  8. package/dist/types/cli/commands/copilot.d.ts +1 -0
  9. package/dist/types/cli/commands/edges.d.ts +1 -0
  10. package/dist/types/cli/commands/groups.d.ts +1 -0
  11. package/dist/types/cli/commands/history.d.ts +1 -0
  12. package/dist/types/cli/commands/nodes.d.ts +1 -0
  13. package/dist/types/cli/commands/pins.d.ts +1 -0
  14. package/dist/types/cli/commands/query.d.ts +1 -0
  15. package/dist/types/cli/commands/view.d.ts +1 -0
  16. package/dist/types/cli/commands/webview.d.ts +1 -0
  17. package/dist/types/cli/shared.d.ts +73 -0
  18. package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -1
  19. package/dist/types/client/index.d.ts +1 -1
  20. package/dist/types/client/nodes/ExtAppFrame.d.ts +10 -0
  21. package/dist/types/client/nodes/HtmlNode.d.ts +1 -1
  22. package/dist/types/client/nodes/McpAppNode.d.ts +1 -1
  23. package/dist/types/client/nodes/use-ax-surface-bridge.d.ts +20 -0
  24. package/dist/types/client/state/node-factory.d.ts +29 -0
  25. package/dist/types/client/uuid-polyfill.d.ts +0 -0
  26. package/dist/types/server/canvas-db.d.ts +0 -1
  27. package/dist/types/server/canvas-operations.d.ts +2 -0
  28. package/dist/types/server/canvas-state.d.ts +14 -15
  29. package/dist/types/server/diagram-presets.d.ts +8 -0
  30. package/dist/types/server/operations/composites.d.ts +6 -30
  31. package/dist/types/server/server.d.ts +1 -0
  32. package/dist/types/shared/ax-surface-protocol.d.ts +18 -0
  33. package/docs/bun-webview-integration.md +5 -0
  34. package/docs/cli.md +5 -0
  35. package/docs/environment.md +48 -0
  36. package/docs/http-api.md +25 -1
  37. package/docs/mcp.md +21 -30
  38. package/docs/node-types.md +5 -0
  39. package/docs/plans/plan-009-tech-debt-backlog.md +34 -15
  40. package/docs/screenshot.png +0 -0
  41. package/docs/tech-debt-assessment-2026-06.md +5 -5
  42. package/package.json +13 -10
  43. package/skills/pmx-canvas/SKILL.md +16 -6
  44. package/skills/pmx-canvas/evals/evals.json +7 -7
  45. package/skills/pmx-canvas/references/full-reference.md +25 -16
  46. package/src/cli/agent.ts +22 -3512
  47. package/src/cli/commands/apps.ts +165 -0
  48. package/src/cli/commands/ax.ts +604 -0
  49. package/src/cli/commands/copilot.ts +43 -0
  50. package/src/cli/commands/edges.ts +73 -0
  51. package/src/cli/commands/groups.ts +81 -0
  52. package/src/cli/commands/history.ts +220 -0
  53. package/src/cli/commands/nodes.ts +984 -0
  54. package/src/cli/commands/pins.ts +42 -0
  55. package/src/cli/commands/query.ts +223 -0
  56. package/src/cli/commands/view.ts +186 -0
  57. package/src/cli/commands/webview.ts +227 -0
  58. package/src/cli/index.ts +8 -2
  59. package/src/cli/shared.ts +861 -0
  60. package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -2
  61. package/src/client/index.tsx +2 -0
  62. package/src/client/nodes/ExtAppFrame.tsx +70 -51
  63. package/src/client/nodes/HtmlNode.tsx +8 -51
  64. package/src/client/nodes/McpAppNode.tsx +12 -47
  65. package/src/client/nodes/use-ax-surface-bridge.ts +71 -0
  66. package/src/client/state/node-factory.ts +72 -0
  67. package/src/client/state/sse-bridge.ts +209 -86
  68. package/src/client/uuid-polyfill.ts +14 -0
  69. package/src/json-render/renderer/index.tsx +3 -2
  70. package/src/mcp/server.ts +0 -47
  71. package/src/server/canvas-db.ts +0 -8
  72. package/src/server/canvas-operations.ts +8 -0
  73. package/src/server/canvas-schema.ts +2 -8
  74. package/src/server/canvas-state.ts +51 -151
  75. package/src/server/diagram-presets.ts +15 -1
  76. package/src/server/html-surface.ts +11 -6
  77. package/src/server/index.ts +3 -9
  78. package/src/server/operations/composites.ts +25 -34
  79. package/src/server/operations/invoker.ts +10 -3
  80. package/src/server/operations/mcp.ts +51 -1
  81. package/src/server/operations/ops/app.ts +5 -0
  82. package/src/server/operations/ops/snapshots.ts +6 -7
  83. package/src/server/operations/registry.ts +4 -1
  84. package/src/server/server.ts +172 -74
  85. package/src/shared/ax-surface-protocol.ts +22 -0
  86. package/dist/types/client/canvas/ContextPinHud.d.ts +0 -1
  87. package/src/client/canvas/ContextPinHud.tsx +0 -17
package/src/cli/agent.ts CHANGED
@@ -11,3509 +11,29 @@
11
11
  * - --yes for destructive actions, --dry-run for preview
12
12
  */
13
13
 
14
- import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
15
- import { dirname, join } from 'node:path';
16
- import { fileURLToPath } from 'node:url';
17
- import { openUrlInExternalBrowser, wrapCanvasAutomationScript } from '../server/server.js';
18
- import { HttpOperationInvoker, OperationError } from '../server/operations/index.js';
19
- import { DEFAULT_EXCALIDRAW_ELEMENTS } from '../server/diagram-presets.js';
20
14
  import {
21
- ALL_SEMANTIC_WATCH_EVENT_TYPES,
22
- formatCompactWatchEvent,
23
- parseSemanticEventFilter,
24
- parseSseStream,
25
- SemanticWatchReducer,
26
- } from './watch.js';
27
-
28
- // ── Helpers ──────────────────────────────────────────────────
29
-
30
- const DEFAULT_PORT = 4313;
31
- const defaultConsoleLog = console.log;
32
- const TRACE_NODE_FIELDS = ['toolName', 'category', 'status', 'duration', 'resultSummary', 'error'] as const;
33
-
34
- interface CanvasSchemaField {
35
- name: string;
36
- type: string;
37
- required: boolean;
38
- description: string;
39
- aliases?: string[];
40
- }
41
-
42
- interface CanvasSchemaType {
43
- type: string;
44
- kind: 'node' | 'virtual-node';
45
- description: string;
46
- endpoint: string;
47
- fields: CanvasSchemaField[];
48
- example: Record<string, unknown>;
49
- notes?: string[];
50
- }
51
-
52
- interface JsonRenderComponentSchema {
53
- type: string;
54
- description: string;
55
- slots: string[];
56
- example: unknown;
57
- props: Array<{
58
- name: string;
59
- type: string;
60
- required: boolean;
61
- nullable: boolean;
62
- }>;
63
- }
64
-
65
- interface CanvasSchemaResponse {
66
- ok: true;
67
- source: 'running-server';
68
- version: string | null;
69
- nodeTypes: CanvasSchemaType[];
70
- jsonRender: {
71
- rootShape: Record<string, string>;
72
- components: JsonRenderComponentSchema[];
73
- };
74
- graph: {
75
- graphTypes: Array<'line' | 'bar' | 'pie' | 'area' | 'scatter' | 'radar' | 'stacked-bar' | 'composed'>;
76
- };
77
- htmlPrimitives?: Array<{
78
- kind: string;
79
- title: string;
80
- description: string;
81
- useWhen: string;
82
- defaultSize: { width: number; height: number };
83
- dataShape: string;
84
- example: Record<string, unknown>;
85
- }>;
86
- mcp: {
87
- tools: string[];
88
- resources: string[];
89
- };
90
- }
91
-
92
- // Per-invocation target override from the global --server-url / --port flags.
93
- // Before these existed, `--port 4750` on any agent command was SILENTLY ignored
94
- // and the command hit the default 4313 daemon — which once pointed a test
95
- // automation WebView at a live production board.
96
- let cliTargetOverride: string | null = null;
97
-
98
- function getBaseUrl(): string {
99
- if (cliTargetOverride) return cliTargetOverride;
100
- const envUrl = process.env.PMX_CANVAS_URL;
101
- if (envUrl) return envUrl.replace(/\/$/, '');
102
- const port = process.env.PMX_CANVAS_PORT || DEFAULT_PORT;
103
- return `http://localhost:${port}`;
104
- }
105
-
106
- /**
107
- * Extract the global `--port <n>` / `--server-url <url>` flags (any position,
108
- * `=` or space-separated value) and set the invocation's target override.
109
- * Returns the remaining args for command dispatch. Invalid values are a loud
110
- * `die` — never a silent fallback to the default port. `--server-url` wins
111
- * over `--port` when both are given.
112
- */
113
- export function extractGlobalTargetFlags(args: string[]): string[] {
114
- cliTargetOverride = null;
115
- let portOverride: number | null = null;
116
- let urlOverride: string | null = null;
117
- const rest: string[] = [];
118
-
119
- for (let i = 0; i < args.length; i++) {
120
- const arg = args[i];
121
- let name: 'port' | 'server-url' | null = null;
122
- let value: string | undefined;
123
- if (arg === '--port' || arg === '--server-url') {
124
- name = arg.slice(2) as 'port' | 'server-url';
125
- value = args[i + 1];
126
- i += 1;
127
- } else if (arg.startsWith('--port=')) {
128
- name = 'port';
129
- value = arg.slice('--port='.length);
130
- } else if (arg.startsWith('--server-url=')) {
131
- name = 'server-url';
132
- value = arg.slice('--server-url='.length);
133
- } else {
134
- rest.push(arg);
135
- continue;
136
- }
137
-
138
- if (name === 'port') {
139
- const port = Number(value);
140
- if (!value || !Number.isInteger(port) || port <= 0 || port > 65535) {
141
- die(
142
- `--port requires a port number, got ${value === undefined ? 'no value' : JSON.stringify(value)}.`,
143
- 'Example: --port 4313. Without the flag, PMX_CANVAS_PORT / PMX_CANVAS_URL pick the target.',
144
- );
145
- }
146
- portOverride = port;
147
- } else {
148
- if (!value || !/^https?:\/\//.test(value)) {
149
- die(
150
- `--server-url requires an http(s) URL, got ${value === undefined ? 'no value' : JSON.stringify(value)}.`,
151
- 'Example: --server-url http://localhost:4313. Without the flag, PMX_CANVAS_URL picks the target.',
152
- );
153
- }
154
- urlOverride = value.replace(/\/$/, '');
155
- }
156
- }
157
-
158
- if (urlOverride) cliTargetOverride = urlOverride;
159
- else if (portOverride) cliTargetOverride = `http://localhost:${portOverride}`;
160
- return rest;
161
- }
162
-
163
- function die(message: string, hint?: string): never {
164
- const out: Record<string, string> = { error: message };
165
- if (hint) out.hint = hint;
166
- console.error(JSON.stringify(out));
167
- process.exit(1);
168
- }
169
-
170
- function output(data: unknown): void {
171
- const text = JSON.stringify(data, null, 2);
172
- if (console.log !== defaultConsoleLog) {
173
- console.log(text);
174
- return;
175
- }
176
- process.stdout.write(`${text}\n`);
177
- }
178
-
179
- async function api(
180
- method: string,
181
- path: string,
182
- body?: Record<string, unknown>,
183
- options?: { allowErrorJson?: boolean },
184
- ): Promise<unknown> {
185
- const base = getBaseUrl();
186
- const url = `${base}${path}`;
187
- const opts: RequestInit = {
188
- method,
189
- headers: { 'Content-Type': 'application/json' },
190
- };
191
- if (body) opts.body = JSON.stringify(body);
192
-
193
- let res: Response;
194
- try {
195
- res = await fetch(url, opts);
196
- } catch (error) {
197
- die(
198
- `Cannot connect to pmx-canvas at ${base}: ${error instanceof Error ? error.message : String(error)}`,
199
- `Start the server first: pmx-canvas --no-open`,
200
- );
201
- }
202
-
203
- const text = await res.text();
204
- let json: unknown;
205
- try {
206
- json = JSON.parse(text);
207
- } catch (error) {
208
- if (!res.ok) die(`HTTP ${res.status}: ${text}`);
209
- console.debug('[cli] response was not JSON', error);
210
- return text;
211
- }
212
-
213
- if (!res.ok) {
214
- if (options?.allowErrorJson) return json;
215
- const err = json as Record<string, unknown>;
216
- die(err.error ? String(err.error) : `HTTP ${res.status}`, typeof err.hint === 'string' ? err.hint : undefined);
217
- }
218
- return json;
219
- }
220
-
221
- // Operation-registry invoker (plan-005): node CRUD, layout reads, edge,
222
- // pin/search/history/undo/redo, and snapshot commands build their HTTP request
223
- // from the shared route table instead of hand-written paths.
224
- // Error handling mirrors api(): operation failures and connection failures
225
- // die with the same JSON error shape.
226
- async function invokeOperation(name: string, input: Record<string, unknown>): Promise<unknown> {
227
- const base = getBaseUrl();
228
- try {
229
- return await new HttpOperationInvoker(base).invoke(name, input);
230
- } catch (error) {
231
- if (error instanceof OperationError) {
232
- die(error.message);
233
- }
234
- die(
235
- `Cannot connect to pmx-canvas at ${base}: ${error instanceof Error ? error.message : String(error)}`,
236
- `Start the server first: pmx-canvas --no-open`,
237
- );
238
- }
239
- }
240
-
241
- // ── Flag parsing ─────────────────────────────────────────────
242
-
243
- function parseFlags(args: string[]): { positional: string[]; flags: Record<string, string | true> } {
244
- const positional: string[] = [];
245
- const flags: Record<string, string | true> = {};
246
- // Boolean-only flags (never take a value argument)
247
- const BOOL_FLAGS = new Set([
248
- 'help',
249
- 'h',
250
- 'ids',
251
- 'stdin',
252
- 'yes',
253
- 'list',
254
- 'clear',
255
- 'set',
256
- 'animated',
257
- 'dry-run',
258
- 'all',
259
- 'no-open-in-canvas',
260
- 'lock-arrange',
261
- 'unlock-arrange',
262
- 'json',
263
- 'compact',
264
- 'verbose',
265
- 'include-logs',
266
- 'no-pan',
267
- 'schema',
268
- 'example',
269
- 'examples',
270
- 'strict-size',
271
- 'scroll-overflow',
272
- 'report',
273
- 'canvas',
274
- 'hooks',
275
- 'tools',
276
- 'session-messaging',
277
- 'permissions',
278
- 'files',
279
- 'ui-prompts',
280
- ]);
281
- for (let i = 0; i < args.length; i++) {
282
- const arg = args[i];
283
- if (arg.startsWith('--')) {
284
- const eq = arg.indexOf('=');
285
- if (eq !== -1) {
286
- flags[arg.slice(2, eq)] = arg.slice(eq + 1);
287
- } else {
288
- const key = arg.slice(2);
289
- // If not a boolean flag and next arg exists and isn't a flag, consume it as value
290
- if (!BOOL_FLAGS.has(key) && i + 1 < args.length && !args[i + 1].startsWith('-')) {
291
- flags[key] = args[++i];
292
- } else {
293
- flags[key] = true;
294
- }
295
- }
296
- } else if (arg.startsWith('-') && arg.length === 2) {
297
- const key = arg.slice(1);
298
- if (!BOOL_FLAGS.has(key) && i + 1 < args.length && !args[i + 1].startsWith('-')) {
299
- flags[key] = args[++i];
300
- } else {
301
- flags[key] = true;
302
- }
303
- } else {
304
- positional.push(arg);
305
- }
306
- }
307
- return { positional, flags };
308
- }
309
-
310
- function requireFlag(flags: Record<string, string | true>, name: string, hint: string): string {
311
- const val = flags[name];
312
- if (!val || val === true) {
313
- die(`Missing required flag: --${name}`, hint);
314
- }
315
- return val;
316
- }
317
-
318
- function getStringFlag(flags: Record<string, string | true>, ...names: string[]): string | undefined {
319
- for (const name of names) {
320
- const value = flags[name];
321
- if (typeof value === 'string' && value.length > 0) return value;
322
- }
323
- return undefined;
324
- }
325
-
326
- function optionalNumberFlag(flags: Record<string, string | true>, name: string, hint: string): number | undefined {
327
- const val = flags[name];
328
- if (!val || val === true) return undefined;
329
- const parsed = Number(val);
330
- if (!Number.isFinite(parsed) || parsed <= 0) {
331
- die(`Invalid value for --${name}: ${String(val)}`, hint);
332
- }
333
- return Math.floor(parsed);
334
- }
335
-
336
- /**
337
- * AX `source` for a CLI-originated action. Defaults to `cli`, but honors an
338
- * explicit `--source <label>` so an adapterless agent using the CLI as a fallback
339
- * transport (e.g. `--source codex`) attributes its actions correctly — keeping
340
- * loop-safety (a consumer never gets back its own steering) accurate (report #69).
341
- */
342
- function resolveAxSource(flags: Record<string, string | true>): string {
343
- return getStringFlag(flags, 'source') ?? 'cli';
344
- }
345
-
346
- function optionalFiniteFlag(flags: Record<string, string | true>, name: string, hint: string): number | undefined {
347
- const val = flags[name];
348
- if (!val || val === true) return undefined;
349
- const parsed = Number(val);
350
- if (!Number.isFinite(parsed)) {
351
- die(`Invalid value for --${name}: ${String(val)}`, hint);
352
- }
353
- return parsed;
354
- }
355
-
356
- function optionalPositiveFiniteFlag(
357
- flags: Record<string, string | true>,
358
- name: string,
359
- hint: string,
360
- ): number | undefined {
361
- const parsed = optionalFiniteFlag(flags, name, hint);
362
- if (parsed === undefined) return undefined;
363
- if (parsed <= 0) {
364
- die(`Invalid value for --${name}: ${String(flags[name])}`, hint);
365
- }
366
- return parsed;
367
- }
368
-
369
- function optionalPositiveFiniteFlagWithAliases(
370
- flags: Record<string, string | true>,
371
- hint: string,
372
- ...names: string[]
373
- ): number | undefined {
374
- for (const name of names) {
375
- const parsed = optionalPositiveFiniteFlag(flags, name, hint);
376
- if (parsed !== undefined) return parsed;
377
- }
378
- return undefined;
379
- }
380
-
381
- function optionalBooleanFlag(flags: Record<string, string | true>, name: string, hint: string): boolean | undefined {
382
- const val = flags[name];
383
- if (val === undefined) return undefined;
384
- if (val === true || val === 'true') return true;
385
- if (val === 'false') return false;
386
- die(`Invalid value for --${name}: ${String(val)}`, hint);
387
- }
388
-
389
- function applyStrictSizeFlags(body: Record<string, unknown>, flags: Record<string, string | true>): void {
390
- if (flags['strict-size'] || flags['scroll-overflow']) body.strictSize = true;
391
- }
392
-
393
- function isRecord(value: unknown): value is Record<string, unknown> {
394
- return !!value && typeof value === 'object' && !Array.isArray(value);
395
- }
396
-
397
- function parseStringListFlag(
398
- flags: Record<string, string | true>,
399
- name: string,
400
- hint: string,
401
- ...aliases: string[]
402
- ): string[] | undefined {
403
- const raw = getStringFlag(flags, name, ...aliases);
404
- if (raw === undefined) return undefined;
405
- const trimmed = raw.trim();
406
- if (!trimmed) {
407
- die(`Invalid value for --${name}: expected at least one string.`, hint);
408
- }
409
-
410
- if (trimmed.startsWith('[')) {
411
- const parsed = parseJsonValue(trimmed, `value for --${name}`, hint);
412
- if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== 'string' || item.trim().length === 0)) {
413
- die(`Invalid value for --${name}: expected a JSON array of non-empty strings.`, hint);
414
- }
415
- return parsed.map((item) => item.trim());
416
- }
417
-
418
- const values = trimmed
419
- .split(',')
420
- .map((value) => value.trim())
421
- .filter(Boolean);
422
- if (values.length === 0) {
423
- die(`Invalid value for --${name}: expected a comma-separated list of keys.`, hint);
424
- }
425
- return values;
426
- }
427
-
428
- function truncateText(value: string, maxLength = 240): string {
429
- const normalized = value.replace(/\s+/g, ' ').trim();
430
- if (normalized.length <= maxLength) return normalized;
431
- if (maxLength <= 1) return normalized.slice(0, maxLength);
432
- return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
433
- }
434
-
435
- function summarizeGraphConfig(config: Record<string, unknown>): Record<string, unknown> {
436
- const summary: Record<string, unknown> = {};
437
- for (const [key, value] of Object.entries(config)) {
438
- if (key === 'data' && Array.isArray(value)) {
439
- summary.dataPoints = value.length;
440
- const first = value[0];
441
- if (isRecord(first)) summary.dataKeys = Object.keys(first);
442
- continue;
443
- }
444
- summary[key] = value;
445
- }
446
- return summary;
447
- }
448
-
449
- function summarizeNodeResult(node: Record<string, unknown>): Record<string, unknown> {
450
- const data = isRecord(node.data) ? node.data : {};
451
- const hiddenDataKeys = new Set(['content', 'fileContent', 'html', 'rendered', 'spec', 'toolResult']);
452
- const dataKeys = Object.keys(data)
453
- .filter((key) => !hiddenDataKeys.has(key))
454
- .sort();
455
-
456
- return {
457
- ...(node.ok !== undefined ? { ok: node.ok } : {}),
458
- id: node.id ?? null,
459
- type: node.type ?? null,
460
- ...(typeof node.kind === 'string' ? { kind: node.kind } : {}),
461
- title: node.title ?? null,
462
- ...(typeof node.content === 'string' ? { contentPreview: truncateText(node.content) } : {}),
463
- ...(node.position !== undefined ? { position: node.position } : {}),
464
- ...(node.size !== undefined ? { size: node.size } : {}),
465
- ...(node.collapsed !== undefined ? { collapsed: node.collapsed } : {}),
466
- ...(node.pinned !== undefined ? { pinned: node.pinned } : {}),
467
- ...(node.dockPosition !== undefined ? { dockPosition: node.dockPosition } : {}),
468
- ...(node.path !== undefined ? { path: node.path } : {}),
469
- ...(node.url !== undefined ? { url: node.url } : {}),
470
- ...(node.provenance !== undefined ? { provenance: node.provenance } : {}),
471
- ...(typeof data.mode === 'string' ? { mode: data.mode } : {}),
472
- ...(typeof data.viewerType === 'string' ? { viewerType: data.viewerType } : {}),
473
- ...(typeof data.serverName === 'string' ? { serverName: data.serverName } : {}),
474
- ...(typeof data.toolName === 'string' ? { toolName: data.toolName } : {}),
475
- ...(typeof data.appSessionId === 'string' ? { appSessionId: data.appSessionId } : {}),
476
- ...(typeof data.sessionStatus === 'string' ? { sessionStatus: data.sessionStatus } : {}),
477
- ...(typeof data.hostMode === 'string' ? { hostMode: data.hostMode } : {}),
478
- ...(typeof data.resourceUri === 'string' ? { resourceUri: data.resourceUri } : {}),
479
- ...(isRecord(data.graphConfig) ? { graph: summarizeGraphConfig(data.graphConfig) } : {}),
480
- ...(dataKeys.length > 0 ? { dataKeys } : {}),
481
- };
482
- }
483
-
484
- function collectFlagValues(args: string[], name: string): string[] {
485
- const values: string[] = [];
486
- for (let i = 0; i < args.length; i++) {
487
- const arg = args[i];
488
- const prefix = `--${name}=`;
489
- if (arg.startsWith(prefix)) {
490
- const value = arg.slice(prefix.length).trim();
491
- if (value) values.push(value);
492
- continue;
493
- }
494
- if (arg === `--${name}` && i + 1 < args.length && !args[i + 1].startsWith('-')) {
495
- values.push(args[i + 1] as string);
496
- i++;
497
- }
498
- }
499
- return values;
500
- }
501
-
502
- function collectRequestedFields(args: string[], flags: Record<string, string | true>): string[] {
503
- const requested = [
504
- ...collectFlagValues(args, 'field'),
505
- ...(typeof flags.fields === 'string'
506
- ? flags.fields
507
- .split(',')
508
- .map((value) => value.trim())
509
- .filter(Boolean)
510
- : []),
511
- ];
512
- return Array.from(new Set(requested));
513
- }
514
-
515
- function resolvePathValue(source: unknown, path: string[]): unknown {
516
- let current = source;
517
- for (const segment of path) {
518
- if (!isRecord(current) && !Array.isArray(current)) return undefined;
519
- current = (current as Record<string, unknown>)[segment];
520
- }
521
- return current;
522
- }
523
-
524
- function resolveNodeFieldValue(node: Record<string, unknown>, field: string): unknown {
525
- if (field.includes('.')) {
526
- const direct = resolvePathValue(node, field.split('.'));
527
- if (direct !== undefined) return direct;
528
- }
529
- if (field in node) return node[field];
530
-
531
- const data = isRecord(node.data) ? node.data : null;
532
- if (!data) return undefined;
533
- if (field in data) return data[field];
534
- return field.includes('.') ? resolvePathValue(data, field.split('.')) : undefined;
535
- }
536
-
537
- function listAvailableNodeFields(node: Record<string, unknown>): string[] {
538
- const topLevel = Object.keys(node).filter((key) => key !== 'data');
539
- const data = isRecord(node.data) ? Object.keys(node.data).flatMap((key) => [key, `data.${key}`]) : [];
540
- return Array.from(new Set([...topLevel, ...data])).sort();
541
- }
542
-
543
- function summarizeHistoryResult(result: Record<string, unknown>): Record<string, unknown> {
544
- const entries = Array.isArray(result.entries) ? result.entries.filter(isRecord) : [];
545
- const countsByOperation: Record<string, number> = {};
546
- let currentIndex = 0;
547
-
548
- entries.forEach((entry, index) => {
549
- const op = typeof entry.operationType === 'string' ? entry.operationType : 'unknown';
550
- countsByOperation[op] = (countsByOperation[op] ?? 0) + 1;
551
- if (entry.isCurrent === true) currentIndex = index + 1;
552
- });
553
-
554
- const recent = entries.slice(-10).map((entry, index) => ({
555
- index: entries.length - Math.min(entries.length, 10) + index + 1,
556
- operationType: entry.operationType,
557
- description: entry.description,
558
- status: entry.isCurrent === true ? 'current' : entry.isUndone === true ? 'undone' : 'applied',
559
- }));
560
-
561
- return {
562
- totalMutations: entries.length,
563
- currentIndex,
564
- canUndo: result.canUndo === true,
565
- canRedo: result.canRedo === true,
566
- countsByOperation,
567
- recent,
568
- };
569
- }
570
-
571
- function compactHistoryResult(result: Record<string, unknown>): Record<string, unknown> {
572
- const entries = Array.isArray(result.entries) ? result.entries.filter(isRecord) : [];
573
- return {
574
- totalMutations: entries.length,
575
- canUndo: result.canUndo === true,
576
- canRedo: result.canRedo === true,
577
- entries: entries.slice(-20).map((entry, index) => ({
578
- index: entries.length - Math.min(entries.length, 20) + index + 1,
579
- operationType: entry.operationType,
580
- description: entry.description,
581
- status: entry.isCurrent === true ? 'current' : entry.isUndone === true ? 'undone' : 'applied',
582
- })),
583
- };
584
- }
585
-
586
- function parseRecordArrayJson(raw: string, hint: string): Array<Record<string, unknown>> {
587
- let parsed: unknown;
588
- try {
589
- parsed = JSON.parse(raw);
590
- } catch (error) {
591
- die(`Invalid JSON dataset: ${error instanceof Error ? error.message : String(error)}`, hint);
592
- }
593
-
594
- if (!Array.isArray(parsed) || parsed.some((item) => !isRecord(item))) {
595
- die('Graph data must be a JSON array of objects.', hint);
596
- }
597
-
598
- return parsed;
599
- }
600
-
601
- function parseJsonValue(raw: string, label: string, hint: string): unknown {
602
- try {
603
- return JSON.parse(raw);
604
- } catch (error) {
605
- die(`Invalid ${label}: ${error instanceof Error ? error.message : String(error)}`, hint);
606
- }
607
- }
608
-
609
- function parseJsonRecord(raw: string, label: string, hint: string): Record<string, unknown> {
610
- const parsed = parseJsonValue(raw, label, hint);
611
- if (!isRecord(parsed)) {
612
- die(`${label} must be a JSON object.`, hint);
613
- }
614
- return parsed;
615
- }
616
-
617
- async function readTextInput(
618
- flags: Record<string, string | true>,
619
- options: {
620
- fileFlags?: string[];
621
- valueFlags?: string[];
622
- allowStdin?: boolean;
623
- label: string;
624
- hint: string;
625
- requiredMessage: string;
626
- },
627
- ): Promise<string> {
628
- for (const name of options.fileFlags ?? []) {
629
- const path = getStringFlag(flags, name);
630
- if (!path) continue;
631
- try {
632
- return readFileSync(path, 'utf-8');
633
- } catch (error) {
634
- die(`Unable to read --${name}: ${error instanceof Error ? error.message : String(error)}`, options.hint);
635
- }
636
- }
637
-
638
- for (const name of options.valueFlags ?? []) {
639
- const value = getStringFlag(flags, name);
640
- if (value !== undefined) return value;
641
- }
642
-
643
- if (options.allowStdin && flags.stdin) {
644
- return await readStdin();
645
- }
646
-
647
- die(options.requiredMessage, options.hint);
648
- }
649
-
650
- async function readOptionalTextInput(
651
- flags: Record<string, string | true>,
652
- options: {
653
- fileFlags?: string[];
654
- valueFlags?: string[];
655
- allowStdin?: boolean;
656
- label: string;
657
- hint: string;
658
- },
659
- ): Promise<string | undefined> {
660
- for (const name of options.fileFlags ?? []) {
661
- const path = getStringFlag(flags, name);
662
- if (!path) continue;
663
- try {
664
- return readFileSync(path, 'utf-8');
665
- } catch (error) {
666
- die(`Unable to read --${name}: ${error instanceof Error ? error.message : String(error)}`, options.hint);
667
- }
668
- }
669
-
670
- for (const name of options.valueFlags ?? []) {
671
- const value = getStringFlag(flags, name);
672
- if (value !== undefined) return value;
673
- }
674
-
675
- if (options.allowStdin && flags.stdin) {
676
- return await readStdin();
677
- }
678
-
679
- return undefined;
680
- }
681
-
682
- function applyCommonGeometryFlags(
683
- body: Record<string, unknown>,
684
- flags: Record<string, string | true>,
685
- hints: { x: string; y: string; width: string; height: string },
686
- ): void {
687
- const x = optionalFiniteFlag(flags, 'x', hints.x);
688
- const y = optionalFiniteFlag(flags, 'y', hints.y);
689
- const width = optionalPositiveFiniteFlag(flags, 'width', hints.width);
690
- const height = optionalPositiveFiniteFlag(flags, 'height', hints.height);
691
- if (x !== undefined) body.x = x;
692
- if (y !== undefined) body.y = y;
693
- if (width !== undefined) body.width = width;
694
- if (height !== undefined) body.height = height;
695
- }
696
-
697
- async function applyStructuredNodeUpdateFlags(
698
- body: Record<string, unknown>,
699
- flags: Record<string, string | true>,
700
- ): Promise<void> {
701
- const specRaw = await readOptionalTextInput(flags, {
702
- fileFlags: ['spec-file'],
703
- valueFlags: ['spec-json'],
704
- allowStdin: false,
705
- label: 'JSON spec',
706
- hint: 'Use: pmx-canvas node update <node-id> --spec-file ./new-spec.json',
707
- });
708
- if (specRaw !== undefined) {
709
- body.spec = parseJsonValue(
710
- specRaw,
711
- 'JSON spec',
712
- 'Use: pmx-canvas node update <node-id> --spec-file ./new-spec.json',
713
- );
714
- }
715
-
716
- const graphPatch = await buildGraphRequestBody(flags, { requireData: false, allowStdin: false });
717
- for (const [key, value] of Object.entries(graphPatch)) {
718
- body[key === 'height' ? 'chartHeight' : key] = value;
719
- }
720
- }
721
-
722
- async function buildJsonRenderRequestBody(flags: Record<string, string | true>): Promise<Record<string, unknown>> {
723
- const hint = 'Use: pmx-canvas node add --type json-render --spec-file ./dashboard.json --title "Ops Dashboard"';
724
- const title = typeof flags.title === 'string' ? flags.title.trim() : '';
725
-
726
- const rawSpec = await readTextInput(flags, {
727
- fileFlags: ['spec-file'],
728
- valueFlags: ['spec-json'],
729
- allowStdin: true,
730
- label: 'JSON spec',
731
- hint,
732
- requiredMessage: 'json-render nodes require --spec-file, --spec-json, or --stdin.',
733
- });
734
-
735
- const spec = parseJsonValue(rawSpec, 'JSON spec', hint);
736
- const body: Record<string, unknown> = { ...(title ? { title } : {}), spec };
737
- applyCommonGeometryFlags(body, flags, {
738
- x: 'Use a finite number, e.g. --x 500',
739
- y: 'Use a finite number, e.g. --y 300',
740
- width: 'Use a positive number, e.g. --width 840',
741
- height: 'Use a positive number, e.g. --height 620',
742
- });
743
- applyStrictSizeFlags(body, flags);
744
- return body;
745
- }
746
-
747
- async function buildHtmlPrimitiveRequestBody(flags: Record<string, string | true>): Promise<Record<string, unknown>> {
748
- const hint = 'Use: pmx-canvas html primitive add --kind choice-grid --data-file ./primitive.json --title "Options"';
749
- const kind = getStringFlag(flags, 'kind', 'primitive');
750
- if (!kind) die('HTML primitives require --kind.', hint);
751
- const body: Record<string, unknown> = { type: 'html', primitive: kind };
752
- if (typeof flags.title === 'string') body.title = flags.title;
753
- const rawData = await readOptionalTextInput(flags, {
754
- fileFlags: ['data-file'],
755
- valueFlags: ['data-json', 'data'],
756
- allowStdin: true,
757
- label: 'HTML primitive data',
758
- hint,
759
- });
760
- if (rawData !== undefined) {
761
- body.data = parseJsonRecord(rawData, 'HTML primitive data', hint);
762
- }
763
- applyCommonGeometryFlags(body, flags, {
764
- x: 'Use a finite number, e.g. --x 500',
765
- y: 'Use a finite number, e.g. --y 300',
766
- width: 'Use a positive number, e.g. --width 980',
767
- height: 'Use a positive number, e.g. --height 720',
768
- });
769
- applyStrictSizeFlags(body, flags);
770
- return body;
771
- }
772
-
773
- async function buildGraphRequestBody(
774
- flags: Record<string, string | true>,
775
- options: { requireData?: boolean; allowStdin?: boolean } = {},
776
- ): Promise<Record<string, unknown>> {
777
- const requireData = options.requireData !== false;
778
- const allowStdin = options.allowStdin !== false;
779
- const hint =
780
- 'Use: pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value';
781
-
782
- const body: Record<string, unknown> = {
783
- ...(requireData ? { graphType: getStringFlag(flags, 'graph-type', 'graphType') ?? 'line' } : {}),
784
- };
785
- const rawData = await readOptionalTextInput(flags, {
786
- fileFlags: ['data-file'],
787
- valueFlags: ['data-json', 'data'],
788
- allowStdin,
789
- label: 'graph JSON dataset',
790
- hint,
791
- });
792
- if (rawData !== undefined) {
793
- body.data = parseRecordArrayJson(rawData, hint);
794
- } else if (requireData) {
795
- die('Graph nodes require --data-file, --data-json, --data, or --stdin JSON data.', hint);
796
- }
797
- const graphType = getStringFlag(flags, 'graph-type', 'graphType');
798
- if (graphType) body.graphType = graphType;
799
- if (typeof flags.title === 'string') body.title = flags.title;
800
- const xKey = getStringFlag(flags, 'x-key', 'xKey');
801
- const yKey = getStringFlag(flags, 'y-key', 'yKey');
802
- const zKey = getStringFlag(flags, 'z-key', 'zKey');
803
- const nameKey = getStringFlag(flags, 'name-key', 'nameKey');
804
- const valueKey = getStringFlag(flags, 'value-key', 'valueKey');
805
- const axisKey = getStringFlag(flags, 'axis-key', 'axisKey');
806
- if (xKey) body.xKey = xKey;
807
- if (yKey) body.yKey = yKey;
808
- if (zKey) body.zKey = zKey;
809
- if (nameKey) body.nameKey = nameKey;
810
- if (valueKey) body.valueKey = valueKey;
811
- if (axisKey) body.axisKey = axisKey;
812
- const metrics = parseStringListFlag(flags, 'metrics', 'Use a comma-separated list, e.g. --metrics north,south');
813
- const series = parseStringListFlag(flags, 'series', 'Use a comma-separated list, e.g. --series north,south');
814
- if (metrics) body.metrics = metrics;
815
- if (series) body.series = series;
816
- const barKey = getStringFlag(flags, 'bar-key', 'barKey');
817
- const lineKey = getStringFlag(flags, 'line-key', 'lineKey');
818
- if (barKey) body.barKey = barKey;
819
- if (lineKey) body.lineKey = lineKey;
820
- if (flags.aggregate === 'sum' || flags.aggregate === 'count' || flags.aggregate === 'avg') {
821
- body.aggregate = flags.aggregate;
822
- }
823
- const color = getStringFlag(flags, 'color');
824
- const barColor = getStringFlag(flags, 'bar-color', 'barColor');
825
- const lineColor = getStringFlag(flags, 'line-color', 'lineColor');
826
- if (color) body.color = color;
827
- if (barColor) body.barColor = barColor;
828
- if (lineColor) body.lineColor = lineColor;
829
- const showLegend = optionalBooleanFlag(flags, 'show-legend', 'Use --show-legend true or --show-legend false');
830
- const showLabels = optionalBooleanFlag(flags, 'show-labels', 'Use --show-labels true or --show-labels false');
831
- if (showLegend !== undefined) body.showLegend = showLegend;
832
- if (showLabels !== undefined) body.showLabels = showLabels;
833
-
834
- const chartHeight = optionalPositiveFiniteFlag(
835
- flags,
836
- 'chart-height',
837
- 'Use a positive number, e.g. --chart-height 300',
838
- );
839
- const x = optionalFiniteFlag(flags, 'x', 'Use a finite number, e.g. --x 500');
840
- const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 300');
841
- const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 760');
842
- const nodeHeight = optionalPositiveFiniteFlagWithAliases(
843
- flags,
844
- 'Use a positive number, e.g. --node-height 520',
845
- 'node-height',
846
- 'nodeHeight',
847
- 'height',
848
- );
849
- if (chartHeight !== undefined) body.height = chartHeight;
850
- if (x !== undefined) body.x = x;
851
- if (y !== undefined) body.y = y;
852
- if (width !== undefined) body.width = width;
853
- if (nodeHeight !== undefined) body.nodeHeight = nodeHeight;
854
- applyStrictSizeFlags(body, flags);
855
- return body;
856
- }
857
-
858
- async function buildWebArtifactRequestBody(flags: Record<string, string | true>): Promise<Record<string, unknown>> {
859
- const hint = 'Use: pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx';
860
- const title = requireFlag(flags, 'title', hint);
861
- const appTsx = await readTextInput(flags, {
862
- fileFlags: ['app-file'],
863
- valueFlags: ['app-tsx'],
864
- allowStdin: true,
865
- label: 'App.tsx',
866
- hint,
867
- requiredMessage: 'web-artifact build requires --app-file, --app-tsx, or --stdin.',
868
- });
869
-
870
- const body: Record<string, unknown> = { title, appTsx };
871
-
872
- const indexCssFile = getStringFlag(flags, 'index-css-file');
873
- const indexCss = getStringFlag(flags, 'index-css');
874
- if (indexCssFile) {
875
- body.indexCss = readFileSync(indexCssFile, 'utf-8');
876
- } else if (indexCss !== undefined) {
877
- body.indexCss = indexCss;
878
- }
879
-
880
- const mainFile = getStringFlag(flags, 'main-file');
881
- const mainTsx = getStringFlag(flags, 'main-tsx');
882
- if (mainFile) {
883
- body.mainTsx = readFileSync(mainFile, 'utf-8');
884
- } else if (mainTsx !== undefined) {
885
- body.mainTsx = mainTsx;
886
- }
887
-
888
- const indexHtmlFile = getStringFlag(flags, 'index-html-file');
889
- const indexHtml = getStringFlag(flags, 'index-html');
890
- if (indexHtmlFile) {
891
- body.indexHtml = readFileSync(indexHtmlFile, 'utf-8');
892
- } else if (indexHtml !== undefined) {
893
- body.indexHtml = indexHtml;
894
- }
895
-
896
- if (typeof flags['project-path'] === 'string') body.projectPath = flags['project-path'];
897
- if (typeof flags['output-path'] === 'string') body.outputPath = flags['output-path'];
898
- if (typeof flags['init-script-path'] === 'string') body.initScriptPath = flags['init-script-path'];
899
- if (typeof flags['bundle-script-path'] === 'string') body.bundleScriptPath = flags['bundle-script-path'];
900
- const deps = parseStringListFlag(flags, 'deps', 'Use a comma-separated list, e.g. --deps recharts,zod');
901
- if (deps) body.deps = deps;
902
- if (flags['no-open-in-canvas']) body.openInCanvas = false;
903
- if (flags.verbose || flags['include-logs']) body.includeLogs = true;
904
-
905
- const timeoutMs = optionalPositiveFiniteFlag(flags, 'timeout-ms', 'Use a positive number, e.g. --timeout-ms 600000');
906
- if (timeoutMs !== undefined) body.timeoutMs = timeoutMs;
907
-
908
- return body;
909
- }
910
-
911
- async function runWebArtifactBuildCommand(flags: Record<string, string | true>): Promise<void> {
912
- const body = await buildWebArtifactRequestBody(flags);
913
- // The build (init + dependency install + bundle) runs server-side and only
914
- // returns a single HTTP response on completion, which can take minutes on a
915
- // cold workspace. With no output an agent's tool wait expires before the node
916
- // appears and the build looks hung. Emit a default-on heartbeat to stderr
917
- // while the request is in flight — stdout (output) and the JSON response body
918
- // stay untouched, so anything parsing stdout is unaffected.
919
- const startedMs = Date.now();
920
- process.stderr.write(
921
- `[web-artifact] building "${String(body.title)}" — init + install + bundle (this can take a few minutes)...\n`,
922
- );
923
- const heartbeat = setInterval(() => {
924
- const elapsedSeconds = Math.round((Date.now() - startedMs) / 1000);
925
- process.stderr.write(`[web-artifact] still building... ${elapsedSeconds}s elapsed\n`);
926
- }, 10_000);
927
- if (typeof heartbeat.unref === 'function') heartbeat.unref();
928
- let result: unknown;
929
- try {
930
- result = await api('POST', '/api/canvas/web-artifact', body, { allowErrorJson: true });
931
- } finally {
932
- clearInterval(heartbeat);
933
- }
934
- output(result);
935
- if (isRecord(result) && result.ok === false) {
936
- process.exit(1);
937
- }
938
- }
939
-
940
- async function loadCanvasSchema(): Promise<CanvasSchemaResponse> {
941
- const result = await api('GET', '/api/canvas/schema');
942
- return result as CanvasSchemaResponse;
943
- }
944
-
945
- function fieldMatches(field: { name: string; aliases?: string[] }, requested: string): boolean {
946
- return field.name === requested || field.aliases?.includes(requested) === true;
947
- }
948
-
949
- function summarizeNodeSchema(schema: CanvasSchemaType): Record<string, unknown> {
950
- return {
951
- type: schema.type,
952
- kind: schema.kind,
953
- endpoint: schema.endpoint,
954
- description: schema.description,
955
- requiredFields: schema.fields.filter((field) => field.required).map((field) => field.name),
956
- optionalFields: schema.fields.filter((field) => !field.required).map((field) => field.name),
957
- exampleKeys: Object.keys(schema.example),
958
- };
959
- }
960
-
961
- function summarizeJsonRenderComponent(component: JsonRenderComponentSchema): Record<string, unknown> {
962
- return {
963
- type: component.type,
964
- description: component.description,
965
- slots: component.slots,
966
- requiredProps: component.props.filter((prop) => prop.required).map((prop) => prop.name),
967
- optionalProps: component.props.filter((prop) => !prop.required).map((prop) => prop.name),
968
- };
969
- }
970
-
971
- function printObjectJson(value: unknown): void {
972
- output(value);
973
- }
974
-
975
- function printNodeSchemaHelp(schema: CanvasSchemaType): void {
976
- console.log(`\npmx-canvas node add --type ${schema.type} — ${schema.description}\n`);
977
- console.log(`Endpoint: ${schema.endpoint}`);
978
- console.log('Flags:');
979
- for (const field of schema.fields) {
980
- const aliases = field.aliases?.length ? ` (aliases: ${field.aliases.map((alias) => `--${alias}`).join(', ')})` : '';
981
- console.log(
982
- ` --${field.name}${field.required ? ' [required]' : ''} <${field.type}> ${field.description}${aliases}`,
983
- );
984
- }
985
- if (schema.notes?.length) {
986
- console.log('\nNotes:');
987
- for (const note of schema.notes) {
988
- console.log(` - ${note}`);
989
- }
990
- }
991
- console.log('\nCanonical example:');
992
- console.log(JSON.stringify(schema.example, null, 2));
993
- console.log('');
994
- }
995
-
996
- async function showNodeAddTypeHelp(flags: Record<string, string | true>): Promise<void> {
997
- const requestedType = getStringFlag(flags, 'type');
998
- if (!requestedType) {
999
- showCommandHelp('node add');
1000
- return;
1001
- }
1002
-
1003
- const schema = await loadCanvasSchema();
1004
- let payload: Record<string, unknown> | CanvasSchemaType | JsonRenderComponentSchema | undefined;
1005
- if (requestedType === 'json-render') {
1006
- const componentName = getStringFlag(flags, 'component');
1007
- if (componentName) {
1008
- const component = schema.jsonRender.components.find((entry) => entry.type === componentName);
1009
- if (!component) {
1010
- die(
1011
- `Unknown json-render component: ${componentName}`,
1012
- 'Run: pmx-canvas node schema --type json-render --summary',
1013
- );
1014
- }
1015
- const requestedField = getStringFlag(flags, 'field');
1016
- if (requestedField) {
1017
- const prop = component.props.find((entry) => entry.name === requestedField);
1018
- if (!prop) {
1019
- die(
1020
- `Unknown json-render prop: ${requestedField}`,
1021
- `Run: pmx-canvas node schema --type json-render --component ${componentName}`,
1022
- );
1023
- }
1024
- payload = {
1025
- command: 'node add',
1026
- type: requestedType,
1027
- component: componentName,
1028
- prop,
1029
- };
1030
- } else {
1031
- payload = flags.summary ? summarizeJsonRenderComponent(component) : component;
1032
- }
1033
- } else {
1034
- payload = flags.summary
1035
- ? {
1036
- type: 'json-render',
1037
- description: 'Native structured UI panel rendered from a validated json-render spec.',
1038
- rootShape: schema.jsonRender.rootShape,
1039
- components: schema.jsonRender.components.map((entry) => summarizeJsonRenderComponent(entry)),
1040
- }
1041
- : {
1042
- type: 'json-render',
1043
- rootShape: schema.jsonRender.rootShape,
1044
- components: schema.jsonRender.components,
1045
- };
1046
- }
1047
- } else if (requestedType === 'graph') {
1048
- const graphSchema = schema.nodeTypes.find((entry) => entry.type === 'graph');
1049
- if (!graphSchema) die('Graph schema is unavailable on the running server.');
1050
- const requestedField = getStringFlag(flags, 'field');
1051
- if (requestedField) {
1052
- const field = graphSchema.fields.find((entry) => fieldMatches(entry, requestedField));
1053
- if (!field) {
1054
- die(`Unknown graph field: ${requestedField}`, 'Run: pmx-canvas node schema --type graph');
1055
- }
1056
- payload = {
1057
- command: 'node add',
1058
- ...field,
1059
- };
1060
- } else {
1061
- payload = flags.summary ? summarizeNodeSchema(graphSchema) : graphSchema;
1062
- }
1063
- } else {
1064
- const nodeType = schema.nodeTypes.find((entry) => entry.type === requestedType);
1065
- if (!nodeType) {
1066
- die(`Unknown node type: ${requestedType}`, 'Run: pmx-canvas node schema --summary');
1067
- }
1068
- const requestedField = getStringFlag(flags, 'field');
1069
- if (requestedField) {
1070
- const field = nodeType.fields.find((entry) => fieldMatches(entry, requestedField));
1071
- if (!field) {
1072
- die(`Unknown node field: ${requestedField}`, `Run: pmx-canvas node schema --type ${requestedType}`);
1073
- }
1074
- payload = {
1075
- command: 'node add',
1076
- type: requestedType,
1077
- field,
1078
- };
1079
- } else {
1080
- payload = flags.summary ? summarizeNodeSchema(nodeType) : nodeType;
1081
- }
1082
- }
1083
-
1084
- if (flags.json) {
1085
- printObjectJson(payload);
1086
- return;
1087
- }
1088
-
1089
- if ('fields' in (payload as CanvasSchemaType)) {
1090
- printNodeSchemaHelp(payload as CanvasSchemaType);
1091
- return;
1092
- }
1093
-
1094
- console.log('');
1095
- console.log(JSON.stringify(payload, null, 2));
1096
- console.log('');
1097
- }
1098
-
1099
- function filterNodeSchemaView(
1100
- schema: CanvasSchemaType,
1101
- flags: Record<string, string | true>,
1102
- ): CanvasSchemaType | Record<string, unknown> {
1103
- const requestedField = getStringFlag(flags, 'field');
1104
- if (requestedField) {
1105
- const field = schema.fields.find((entry) => fieldMatches(entry, requestedField));
1106
- if (!field) {
1107
- die(`Unknown field: ${requestedField}`, `Run: pmx-canvas node schema --type ${schema.type}`);
1108
- }
1109
- return {
1110
- type: schema.type,
1111
- field,
1112
- };
1113
- }
1114
-
1115
- return flags.summary ? summarizeNodeSchema(schema) : schema;
1116
- }
1117
-
1118
- function filterJsonRenderSchemaView(
1119
- schema: CanvasSchemaResponse['jsonRender'],
1120
- flags: Record<string, string | true>,
1121
- ): Record<string, unknown> | JsonRenderComponentSchema {
1122
- const componentName = getStringFlag(flags, 'component');
1123
- if (!componentName) {
1124
- return flags.summary
1125
- ? {
1126
- rootShape: schema.rootShape,
1127
- components: schema.components.map((entry) => summarizeJsonRenderComponent(entry)),
1128
- }
1129
- : schema;
1130
- }
1131
-
1132
- const component = schema.components.find((entry) => entry.type === componentName);
1133
- if (!component) {
1134
- die(`Unknown json-render component: ${componentName}`, 'Run: pmx-canvas node schema --type json-render --summary');
1135
- }
1136
-
1137
- const requestedField = getStringFlag(flags, 'field');
1138
- if (requestedField) {
1139
- const prop = component.props.find((entry) => entry.name === requestedField);
1140
- if (!prop) {
1141
- die(
1142
- `Unknown json-render prop: ${requestedField}`,
1143
- `Run: pmx-canvas node schema --type json-render --component ${componentName}`,
1144
- );
1145
- }
1146
- return {
1147
- component: componentName,
1148
- prop,
1149
- };
1150
- }
1151
-
1152
- return flags.summary ? summarizeJsonRenderComponent(component) : component;
1153
- }
1154
-
1155
- function summarizeHtmlPrimitive(
1156
- primitive: NonNullable<CanvasSchemaResponse['htmlPrimitives']>[number],
1157
- ): Record<string, unknown> {
1158
- return {
1159
- kind: primitive.kind,
1160
- title: primitive.title,
1161
- description: primitive.description,
1162
- useWhen: primitive.useWhen,
1163
- defaultSize: primitive.defaultSize,
1164
- dataShape: primitive.dataShape,
1165
- };
1166
- }
1167
-
1168
- function filterHtmlPrimitiveSchemaView(
1169
- schema: CanvasSchemaResponse,
1170
- flags: Record<string, string | true>,
1171
- ): Record<string, unknown> {
1172
- const primitives = schema.htmlPrimitives ?? [];
1173
- const kind = getStringFlag(flags, 'kind', 'primitive');
1174
- if (!kind) {
1175
- return {
1176
- primitives: flags.summary ? primitives.map((entry) => summarizeHtmlPrimitive(entry)) : primitives,
1177
- };
1178
- }
1179
- const primitive = primitives.find((entry) => entry.kind === kind);
1180
- if (!primitive) {
1181
- die(`Unknown HTML primitive: ${kind}`, 'Run: pmx-canvas html primitive schema --summary');
1182
- }
1183
- return flags.summary ? summarizeHtmlPrimitive(primitive) : primitive;
1184
- }
1185
-
1186
- // ── Commands ─────────────────────────────────────────────────
1187
-
1188
- const COMMANDS: Record<string, { run: (args: string[]) => Promise<void>; help: string; examples: string[] }> = {};
1189
- const RESOURCE_COMMAND_ALIASES: Record<string, Record<string, string>> = {
1190
- node: {
1191
- delete: 'remove',
1192
- rm: 'remove',
1193
- },
1194
- edge: {
1195
- delete: 'remove',
1196
- rm: 'remove',
1197
- },
1198
- ax: {
1199
- // Single-subcommand AX groups: the bare verb maps to its only action so
1200
- // `ax event` / `ax evidence` suggest the full command instead of erroring.
1201
- event: 'event add',
1202
- evidence: 'evidence add',
1203
- },
1204
- };
1205
- const RESOURCE_SUBCOMMAND_HINTS: Record<string, Record<string, string>> = {
1206
- node: {
1207
- pin: 'Use the top-level pin command instead: pmx-canvas pin <node-id>',
1208
- },
1209
- ax: {
1210
- // Multi-subcommand AX groups: point at the available actions.
1211
- host: 'Pick an action: pmx-canvas ax host report | pmx-canvas ax host status',
1212
- work: 'Pick an action: pmx-canvas ax work add | update | list',
1213
- approval: 'Pick an action: pmx-canvas ax approval request | resolve | list',
1214
- review: 'Pick an action: pmx-canvas ax review add | list',
1215
- delivery: 'Pick an action: pmx-canvas ax delivery list | mark',
1216
- elicitation: 'Pick an action: pmx-canvas ax elicitation request | respond | list',
1217
- mode: 'Pick an action: pmx-canvas ax mode request | resolve | list',
1218
- command: 'Pick an action: pmx-canvas ax command list | invoke',
1219
- policy: 'Pick an action: pmx-canvas ax policy get | set',
1220
- },
1221
- };
1222
-
1223
- function cmd(name: string, help: string, examples: string[], run: (args: string[]) => Promise<void>) {
1224
- COMMANDS[name] = { run, help, examples };
1225
- }
1226
-
1227
- cmd('open', 'Open the current workbench in the browser', ['pmx-canvas open'], async (args) => {
1228
- const { flags } = parseFlags(args);
1229
- if (flags.help || flags.h) return showCommandHelp('open');
1230
-
1231
- const base = getBaseUrl();
1232
- try {
1233
- const response = await fetch(`${base}/health`);
1234
- if (!response.ok) {
1235
- die(`Cannot reach pmx-canvas health endpoint at ${base}: HTTP ${response.status}`);
1236
- }
1237
- } catch (error) {
1238
- die(
1239
- `Cannot connect to pmx-canvas at ${base}: ${error instanceof Error ? error.message : String(error)}`,
1240
- 'Start the server first: pmx-canvas --no-open',
1241
- );
1242
- }
1243
-
1244
- const url = `${base}/workbench`;
1245
- if (!openUrlInExternalBrowser(url)) {
1246
- die(`Failed to open browser for ${url}`);
1247
- }
1248
- output({ ok: true, url });
1249
- });
1250
-
1251
- // ── node add ─────────────────────────────────────────────────
1252
- cmd(
1253
- 'node add',
1254
- 'Add a node to the canvas',
1255
- [
1256
- 'pmx-canvas node add --type markdown --title "Design Doc" --content "# Overview"',
1257
- 'pmx-canvas node add --type status --title "Build" --content "passing"',
1258
- 'pmx-canvas node add --type file --content "src/index.ts"',
1259
- 'pmx-canvas node add --type webpage --url "https://example.com/docs"',
1260
- 'pmx-canvas node add --type html --title "Widget" --content "<main>Hello</main>"',
1261
- 'pmx-canvas node add --type html --title "Showcase" --content ./report.html (a .html path is read from disk; otherwise --content is raw HTML)',
1262
- 'pmx-canvas node add --type html --primitive choice-grid --data-file ./options.json --title "Options"',
1263
- 'pmx-canvas node add --type markdown --title "Note" --x 100 --y 200',
1264
- 'pmx-canvas node add --type json-render --title "Ops Dashboard" --spec-file ./dashboard.json',
1265
- 'pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
1266
- 'pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx',
1267
- ],
1268
- async (args) => {
1269
- const { flags } = parseFlags(args);
1270
- if (flags.help || flags.h) return showNodeAddTypeHelp(flags);
1271
-
1272
- const type = (flags.type as string) || 'markdown';
1273
-
1274
- if (type === 'json-render') {
1275
- const result = await invokeOperation('jsonrender.add', await buildJsonRenderRequestBody(flags));
1276
- output(result);
1277
- return;
1278
- }
1279
-
1280
- if (type === 'graph') {
1281
- const result = await invokeOperation('graph.add', await buildGraphRequestBody(flags));
1282
- output(result);
1283
- return;
1284
- }
1285
-
1286
- if (type === 'web-artifact') {
1287
- await runWebArtifactBuildCommand(flags);
1288
- return;
1289
- }
1290
-
1291
- if (type === 'html-primitive') {
1292
- const result = await invokeOperation('node.add', await buildHtmlPrimitiveRequestBody(flags));
1293
- output(result);
1294
- return;
1295
- }
1296
-
1297
- if (type === 'html' && getStringFlag(flags, 'primitive', 'kind')) {
1298
- const result = await invokeOperation('node.add', await buildHtmlPrimitiveRequestBody(flags));
1299
- output(result);
1300
- return;
1301
- }
1302
-
1303
- if (type === 'mcp-app') {
1304
- die(
1305
- 'mcp-app nodes require tool-backed app metadata and cannot be created with generic node add.',
1306
- 'Use: pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx, or pmx-canvas external-app add --kind excalidraw --title "Diagram"',
1307
- );
1308
- }
1309
-
1310
- const body: Record<string, unknown> = { type };
1311
- if (flags.title) body.title = flags.title;
1312
- const webpageUrl = getStringFlag(flags, 'url');
1313
- const imagePath = getStringFlag(flags, 'path');
1314
- if (type === 'webpage' && webpageUrl) {
1315
- body.url = webpageUrl;
1316
- } else if (type === 'image' && imagePath && !flags.content) {
1317
- body.content = imagePath;
1318
- } else if (type === 'html') {
1319
- const html = getStringFlag(flags, 'html') ?? getStringFlag(flags, 'content');
1320
- if (html !== undefined) body.html = html;
1321
- const summary = getStringFlag(flags, 'summary');
1322
- const agentSummary = getStringFlag(flags, 'agent-summary', 'agentSummary');
1323
- const description = getStringFlag(flags, 'description');
1324
- if (summary !== undefined) body.summary = summary;
1325
- if (agentSummary !== undefined) body.agentSummary = agentSummary;
1326
- if (description !== undefined) body.description = description;
1327
- if (optionalBooleanFlag(flags, 'presentation', 'Use --presentation true or --presentation false') === true)
1328
- body.presentation = true;
1329
- if (typeof flags['slide-title'] === 'string') body.slideTitles = [flags['slide-title']];
1330
- if (typeof flags['embedded-node-id'] === 'string') body.embeddedNodeIds = [flags['embedded-node-id']];
1331
- } else if (flags.content) {
1332
- body.content = flags.content;
1333
- }
1334
- applyCommonGeometryFlags(body, flags, {
1335
- x: 'Use a finite number, e.g. --x 500',
1336
- y: 'Use a finite number, e.g. --y 300',
1337
- width: 'Use a positive number, e.g. --width 500',
1338
- height: 'Use a positive number, e.g. --height 280',
1339
- });
1340
- applyStrictSizeFlags(body, flags);
1341
- if (type === 'trace') {
1342
- for (const field of TRACE_NODE_FIELDS) {
1343
- const value = getStringFlag(
1344
- flags,
1345
- field,
1346
- field.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`),
1347
- );
1348
- if (value !== undefined) body[field] = value;
1349
- }
1350
- }
1351
-
1352
- // Support --stdin for piping content
1353
- if (flags.stdin) {
1354
- if (type === 'webpage') {
1355
- body.url = await readStdin();
1356
- } else if (type === 'html') {
1357
- body.html = await readStdin();
1358
- } else {
1359
- body.content = await readStdin();
1360
- }
1361
- }
1362
-
1363
- const result = await invokeOperation('node.add', body);
1364
- output(result);
1365
- },
1366
- );
1367
-
1368
- cmd(
1369
- 'json-render',
1370
- 'Show json-render schema and canonical examples',
1371
- [
1372
- 'pmx-canvas json-render --schema --summary',
1373
- 'pmx-canvas json-render --examples',
1374
- 'pmx-canvas json-render --example --component Table',
1375
- 'pmx-canvas json-render --schema --component Badge --field variant',
1376
- ],
1377
- async (args) => {
1378
- const { flags } = parseFlags(args);
1379
- if (flags.help || flags.h) return showCommandHelp('json-render');
1380
-
1381
- const schema = await loadCanvasSchema();
1382
- const componentName = getStringFlag(flags, 'component');
1383
- const fieldName = getStringFlag(flags, 'field');
1384
-
1385
- if (flags.example || flags.examples) {
1386
- if (fieldName)
1387
- die(
1388
- '--field is only supported with --schema.',
1389
- 'Use: pmx-canvas json-render --schema --component Table --field rows',
1390
- );
1391
- if (componentName) {
1392
- const component = schema.jsonRender.components.find((entry) => entry.type === componentName);
1393
- if (!component)
1394
- die(`Unknown json-render component: ${componentName}`, 'Run: pmx-canvas json-render --schema --summary');
1395
- output({ component: component.type, example: component.example });
1396
- return;
1397
- }
1398
- output({
1399
- rootShape: schema.jsonRender.rootShape,
1400
- examples: Object.fromEntries(schema.jsonRender.components.map((entry) => [entry.type, entry.example])),
1401
- });
1402
- return;
1403
- }
1404
-
1405
- output(filterJsonRenderSchemaView(schema.jsonRender, flags));
1406
- },
1407
- );
1408
-
1409
- cmd(
1410
- 'html primitive add',
1411
- 'Create a reusable sandboxed HTML communication primitive',
1412
- [
1413
- 'pmx-canvas html primitive add --kind choice-grid --data-file ./options.json --title "Options"',
1414
- 'pmx-canvas html primitive add --kind plan-timeline --data-json \'{"milestones":[{"title":"Ship","detail":"Implement and verify","status":"next"}]}\'',
1415
- 'pmx-canvas html primitive add --kind triage-board --data-file ./tickets.json --strict-size',
1416
- ],
1417
- async (args) => {
1418
- const { flags } = parseFlags(args);
1419
- if (flags.help || flags.h) return showCommandHelp('html primitive add');
1420
- const result = await api('POST', '/api/canvas/node', await buildHtmlPrimitiveRequestBody(flags));
1421
- output(result);
1422
- },
1423
- );
1424
-
1425
- cmd(
1426
- 'html primitive schema',
1427
- 'Describe reusable HTML communication primitives',
1428
- [
1429
- 'pmx-canvas html primitive schema --summary',
1430
- 'pmx-canvas html primitive schema --kind choice-grid',
1431
- 'pmx-canvas html primitive schema --kind triage-board --summary',
1432
- ],
1433
- async (args) => {
1434
- const { flags } = parseFlags(args);
1435
- if (flags.help || flags.h) return showCommandHelp('html primitive schema');
1436
- const schema = await loadCanvasSchema();
1437
- output(filterHtmlPrimitiveSchemaView(schema, flags));
1438
- },
1439
- );
1440
-
1441
- cmd(
1442
- 'graph add',
1443
- 'Add a graph node to the canvas',
1444
- [
1445
- 'pmx-canvas graph add --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
1446
- 'pmx-canvas graph add --graphType composed --data \'[{"day":"Mon","visits":10,"conversion":0.4}]\' --xKey day --barKey visits --lineKey conversion',
1447
- 'pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
1448
- ],
1449
- async (args) => {
1450
- const { flags } = parseFlags(args);
1451
- if (flags.help || flags.h) return showCommandHelp('graph add');
1452
-
1453
- const result = await invokeOperation('graph.add', await buildGraphRequestBody(flags));
1454
- output(result);
1455
- },
1456
- );
1457
-
1458
- cmd(
1459
- 'node schema',
1460
- 'Describe server-supported node create schemas and canonical examples',
1461
- [
1462
- 'pmx-canvas node schema',
1463
- 'pmx-canvas node schema --type webpage',
1464
- 'pmx-canvas node schema --type json-render',
1465
- 'pmx-canvas json-render --schema --summary',
1466
- 'pmx-canvas node schema --type json-render --component Table',
1467
- 'pmx-canvas node schema --type webpage --field url',
1468
- 'pmx-canvas node schema --summary',
1469
- ],
1470
- async (args) => {
1471
- const { flags } = parseFlags(args);
1472
- if (flags.help || flags.h) return showCommandHelp('node schema');
1473
-
1474
- const result = await loadCanvasSchema();
1475
- if (getStringFlag(flags, 'component') && flags.type !== 'json-render') {
1476
- die('--component is only supported with --type json-render.');
1477
- }
1478
-
1479
- if (typeof flags.type !== 'string') {
1480
- if (flags.summary) {
1481
- output({
1482
- source: result.source,
1483
- version: result.version,
1484
- nodeTypes: result.nodeTypes.map((entry) => summarizeNodeSchema(entry)),
1485
- jsonRender: {
1486
- componentCount: result.jsonRender.components.length,
1487
- rootShape: result.jsonRender.rootShape,
1488
- },
1489
- graph: result.graph,
1490
- htmlPrimitives: result.htmlPrimitives?.map((entry) => summarizeHtmlPrimitive(entry)) ?? [],
1491
- mcp: result.mcp,
1492
- });
1493
- return;
1494
- }
1495
- output(result);
1496
- return;
1497
- }
1498
-
1499
- const requested = flags.type;
1500
- if (requested === 'json-render') {
1501
- output(filterJsonRenderSchemaView(result.jsonRender, flags));
1502
- return;
1503
- }
1504
- if (requested === 'graph') {
1505
- const graphSchema = result.nodeTypes.find((entry) => entry.type === 'graph');
1506
- if (graphSchema) {
1507
- output(filterNodeSchemaView(graphSchema, flags));
1508
- return;
1509
- }
1510
- output(flags.summary ? result.graph : { ...result.graph });
1511
- return;
1512
- }
1513
- const nodeType = result.nodeTypes.find((entry) => entry.type === requested);
1514
- if (nodeType) {
1515
- output(filterNodeSchemaView(nodeType, flags));
1516
- return;
1517
- }
1518
- die(`Unknown schema type: ${requested}`, 'Run: pmx-canvas node schema');
1519
- },
1520
- );
1521
-
1522
- // ── node list ────────────────────────────────────────────────
1523
- cmd(
1524
- 'node list',
1525
- 'List all nodes on the canvas',
1526
- [
1527
- 'pmx-canvas node list',
1528
- 'pmx-canvas node list --type markdown',
1529
- 'pmx-canvas node list --type mcp-app',
1530
- 'pmx-canvas node list --ids',
1531
- ],
1532
- async (args) => {
1533
- const { flags } = parseFlags(args);
1534
- if (flags.help || flags.h) return showCommandHelp('node list');
1535
-
1536
- const layout = (await invokeOperation('layout.get', {})) as { nodes: Array<Record<string, unknown>> };
1537
- let nodes = layout.nodes;
1538
-
1539
- if (flags.type && flags.type !== true) {
1540
- nodes = nodes.filter((n) => n.type === flags.type || n.kind === flags.type);
1541
- }
1542
-
1543
- if (flags.ids) {
1544
- output(nodes.map((n) => n.id));
1545
- } else {
1546
- const shouldSummarize = flags.summary === true || flags.compact === true || flags.type === 'mcp-app';
1547
- output(shouldSummarize ? nodes.map((node) => summarizeNodeResult(node)) : nodes);
1548
- }
1549
- },
1550
- );
1551
-
1552
- // ── node get ─────────────────────────────────────────────────
1553
- cmd(
1554
- 'node get',
1555
- 'Get a node by ID',
1556
- [
1557
- 'pmx-canvas node get <node-id>',
1558
- 'pmx-canvas node get node-abc123',
1559
- 'pmx-canvas node get node-abc123 --summary',
1560
- 'pmx-canvas node get node-abc123 --field title --field graphConfig',
1561
- ],
1562
- async (args) => {
1563
- const { positional, flags } = parseFlags(args);
1564
- if (flags.help || flags.h) return showCommandHelp('node get');
1565
-
1566
- const id = positional[0];
1567
- if (!id) die('Missing node ID', 'pmx-canvas node get <node-id>');
1568
-
1569
- const result = (await invokeOperation('node.get', { id })) as Record<string, unknown>;
1570
- const requestedFields = collectRequestedFields(args, flags);
1571
- if (requestedFields.length > 0) {
1572
- const picked = Object.fromEntries(requestedFields.map((field) => [field, resolveNodeFieldValue(result, field)]));
1573
- const missing = requestedFields.filter((field) => picked[field] === undefined);
1574
- if (missing.length > 0) {
1575
- die(
1576
- `Unknown node field${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`,
1577
- `Available fields: ${listAvailableNodeFields(result).join(', ')}`,
1578
- );
1579
- }
1580
- output({
1581
- id: result.id ?? id,
1582
- fields: picked,
1583
- });
1584
- return;
1585
- }
1586
-
1587
- if (flags.summary || flags.compact) {
1588
- output(summarizeNodeResult(result));
1589
- return;
1590
- }
1591
- output(result);
1592
- },
1593
- );
1594
-
1595
- // ── node update ──────────────────────────────────────────────
1596
- cmd(
1597
- 'node update',
1598
- 'Update a node by ID',
1599
- [
1600
- 'pmx-canvas node update <node-id> --title "New Title"',
1601
- 'pmx-canvas node update <node-id> --content "Updated content"',
1602
- 'pmx-canvas node update <node-id> --title "Moved" --x 500 --y 300',
1603
- 'pmx-canvas node update <node-id> --width 840 --height 620',
1604
- 'pmx-canvas node update <node-id> --spec-file ./dashboard.json',
1605
- 'pmx-canvas node update <graph-id> --data-file ./metrics.json --chart-height 420',
1606
- 'pmx-canvas node update <node-id> --pinned true',
1607
- 'pmx-canvas node update <node-id> --dock-position right',
1608
- 'pmx-canvas node update <node-id> --dock-position none # undock back to the canvas',
1609
- 'pmx-canvas node update <node-id> --lock-arrange',
1610
- ],
1611
- async (args) => {
1612
- const { positional, flags } = parseFlags(args);
1613
- if (flags.help || flags.h) return showCommandHelp('node update');
1614
-
1615
- const id = positional[0];
1616
- if (!id) die('Missing node ID', 'pmx-canvas node update <node-id> --title "New Title"');
1617
-
1618
- const body: Record<string, unknown> = {};
1619
- await applyStructuredNodeUpdateFlags(body, flags);
1620
- if (flags.title && flags.title !== true) body.title = flags.title;
1621
- if (flags.content && flags.content !== true) body.content = flags.content;
1622
- if (flags.stdin) body.content = await readStdin();
1623
-
1624
- const x = optionalFiniteFlag(flags, 'x', 'Use a finite number, e.g. --x 500');
1625
- const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 300');
1626
- const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 840');
1627
- const height = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 620');
1628
- const nodeHeight = optionalPositiveFiniteFlagWithAliases(
1629
- flags,
1630
- 'Use a positive number, e.g. --node-height 620',
1631
- 'node-height',
1632
- 'nodeHeight',
1633
- );
1634
- if (height !== undefined && nodeHeight !== undefined) {
1635
- die('Use either --height/--node-height, not both.');
1636
- }
1637
- const frameHeight = height ?? nodeHeight;
1638
- const pinned = optionalBooleanFlag(flags, 'pinned', 'Use --pinned true or --pinned false');
1639
- if (flags['lock-arrange'] && flags['unlock-arrange']) {
1640
- die('Use either --lock-arrange or --unlock-arrange, not both.');
1641
- }
1642
- const arrangeLocked = flags['lock-arrange'] ? true : flags['unlock-arrange'] ? false : undefined;
1643
-
1644
- applyStrictSizeFlags(body, flags);
1645
-
1646
- for (const field of TRACE_NODE_FIELDS) {
1647
- const value = getStringFlag(
1648
- flags,
1649
- field,
1650
- field.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`),
1651
- );
1652
- if (value !== undefined) body[field] = value;
1653
- }
1654
-
1655
- if (
1656
- x !== undefined ||
1657
- y !== undefined ||
1658
- width !== undefined ||
1659
- frameHeight !== undefined ||
1660
- arrangeLocked !== undefined
1661
- ) {
1662
- const existing = (await invokeOperation('node.get', { id })) as {
1663
- position: { x: number; y: number };
1664
- size: { width: number; height: number };
1665
- data: Record<string, unknown>;
1666
- };
1667
-
1668
- if (x !== undefined || y !== undefined) {
1669
- body.position = {
1670
- x: x ?? existing.position.x,
1671
- y: y ?? existing.position.y,
1672
- };
1673
- }
1674
-
1675
- if (width !== undefined || frameHeight !== undefined) {
1676
- body.size = {
1677
- width: width ?? existing.size.width,
1678
- height: frameHeight ?? existing.size.height,
1679
- };
1680
- }
1681
-
1682
- if (arrangeLocked !== undefined) {
1683
- body.arrangeLocked = arrangeLocked;
1684
- }
1685
- }
1686
-
1687
- if (pinned !== undefined) body.pinned = pinned;
1688
-
1689
- // --dock-position left|right|none : dock a node into the top HUD or undock it.
1690
- // `none`/`null`/empty map to JS null (undock). Assigned with a !== undefined
1691
- // guard so the null survives JSON.stringify to the server (which accepts a
1692
- // top-level dockPosition: null). HTTP PATCH already supports this; this is the
1693
- // CLI path the report (#40) found missing.
1694
- const dockRaw = getStringFlag(flags, 'dock-position', 'dockPosition');
1695
- let dockPosition: 'left' | 'right' | null | undefined;
1696
- if (dockRaw !== undefined) {
1697
- const v = dockRaw.trim().toLowerCase();
1698
- if (v === 'left' || v === 'right') dockPosition = v;
1699
- else if (v === 'none' || v === 'null' || v === '') dockPosition = null;
1700
- else die(`Invalid --dock-position "${dockRaw}".`, 'Use left, right, or none (to undock).');
1701
- }
1702
- if (dockPosition !== undefined) body.dockPosition = dockPosition;
1703
-
1704
- if (Object.keys(body).length === 0) {
1705
- die(
1706
- 'No updates specified',
1707
- 'Use --title, --content, --x, --y, --width, --height, --strict-size, --pinned, --dock-position, trace fields, --lock-arrange, --unlock-arrange, or --stdin',
1708
- );
1709
- }
1710
-
1711
- const result = await invokeOperation('node.update', { id, ...body });
1712
- output(result);
1713
- },
1714
- );
1715
-
1716
- // ── node remove ──────────────────────────────────────────────
1717
- cmd(
1718
- 'node remove',
1719
- 'Remove a node from the canvas',
1720
- ['pmx-canvas node remove <node-id>', 'pmx-canvas node remove node-abc123'],
1721
- async (args) => {
1722
- const { positional, flags } = parseFlags(args);
1723
- if (flags.help || flags.h) return showCommandHelp('node remove');
1724
-
1725
- const id = positional[0];
1726
- if (!id) die('Missing node ID', 'pmx-canvas node remove <node-id>');
1727
-
1728
- const result = await invokeOperation('node.remove', { id });
1729
- output(result);
1730
- },
1731
- );
1732
-
1733
- // ── edge add ─────────────────────────────────────────────────
1734
- cmd(
1735
- 'edge add',
1736
- 'Add an edge between two nodes',
1737
- [
1738
- 'pmx-canvas edge add --from <node-id> --to <node-id> --type flow',
1739
- 'pmx-canvas edge add --from-search "DVT O3 — GitOps" --to-search "deep work trend" --type relation',
1740
- 'pmx-canvas edge add --from n1 --to n2 --type depends-on --label "imports"',
1741
- 'pmx-canvas edge add --from n1 --to n2 --type references --style dashed --animated',
1742
- ],
1743
- async (args) => {
1744
- const { flags } = parseFlags(args);
1745
- if (flags.help || flags.h) return showCommandHelp('edge add');
1746
-
1747
- const type = (flags.type as string) || 'flow';
1748
- const from = typeof flags.from === 'string' ? flags.from : undefined;
1749
- const to = typeof flags.to === 'string' ? flags.to : undefined;
1750
- const fromSearch = typeof flags['from-search'] === 'string' ? flags['from-search'] : undefined;
1751
- const toSearch = typeof flags['to-search'] === 'string' ? flags['to-search'] : undefined;
1752
-
1753
- if (!from && !fromSearch) {
1754
- die(
1755
- 'Missing source selector',
1756
- 'Use --from <id> or --from-search "query". Search queries must resolve to exactly one node. Example: pmx-canvas edge add --from-search "DVT O3 — GitOps" --to-search "deep work trend" --type relation',
1757
- );
1758
- }
1759
- if (!to && !toSearch) {
1760
- die(
1761
- 'Missing target selector',
1762
- 'Use --to <id> or --to-search "query". Search queries must resolve to exactly one node. Example: pmx-canvas edge add --from-search "DVT O3 — GitOps" --to-search "deep work trend" --type relation',
1763
- );
1764
- }
1765
-
1766
- const body: Record<string, unknown> = {
1767
- type,
1768
- ...(from ? { from } : {}),
1769
- ...(to ? { to } : {}),
1770
- ...(fromSearch ? { fromSearch } : {}),
1771
- ...(toSearch ? { toSearch } : {}),
1772
- };
1773
- if (flags.label && flags.label !== true) body.label = flags.label;
1774
- if (typeof flags.style === 'string') body.style = flags.style;
1775
- if (flags.animated) body.animated = true;
1776
-
1777
- const result = await invokeOperation('edge.add', body);
1778
- output(result);
1779
- },
1780
- );
1781
-
1782
- // ── edge list ────────────────────────────────────────────────
1783
- cmd('edge list', 'List all edges on the canvas', ['pmx-canvas edge list'], async (args) => {
1784
- const { flags } = parseFlags(args);
1785
- if (flags.help || flags.h) return showCommandHelp('edge list');
1786
-
1787
- const layout = (await api('GET', '/api/canvas/state')) as { edges: unknown[] };
1788
- output(layout.edges);
1789
- });
1790
-
1791
- // ── edge remove ──────────────────────────────────────────────
1792
- cmd('edge remove', 'Remove an edge by ID', ['pmx-canvas edge remove <edge-id>'], async (args) => {
1793
- const { positional, flags } = parseFlags(args);
1794
- if (flags.help || flags.h) return showCommandHelp('edge remove');
1795
-
1796
- const id = positional[0];
1797
- if (!id) die('Missing edge ID', 'pmx-canvas edge remove <edge-id>');
1798
-
1799
- const result = await invokeOperation('edge.remove', { edge_id: id });
1800
- output(result);
1801
- });
1802
-
1803
- // ── search ───────────────────────────────────────────────────
1804
- cmd(
1805
- 'search',
1806
- 'Search nodes by title or content',
1807
- ['pmx-canvas search "design doc"', 'pmx-canvas search --query "TODO"'],
1808
- async (args) => {
1809
- const { positional, flags } = parseFlags(args);
1810
- if (flags.help || flags.h) return showCommandHelp('search');
1811
-
1812
- const query = positional[0] || (typeof flags.query === 'string' ? flags.query : '');
1813
- if (!query) die('Missing search query', 'pmx-canvas search "query"');
1814
-
1815
- const result = await invokeOperation('search', { q: query });
1816
- output(result);
1817
- },
1818
- );
1819
-
1820
- // ── layout ───────────────────────────────────────────────────
1821
- cmd(
1822
- 'layout',
1823
- 'Get the full canvas layout (nodes, edges, viewport)',
1824
- ['pmx-canvas layout', 'pmx-canvas layout --summary'],
1825
- async (args) => {
1826
- const { flags } = parseFlags(args);
1827
- if (flags.help || flags.h) return showCommandHelp('layout');
1828
-
1829
- if (flags.summary || flags.compact) {
1830
- output(await api('GET', '/api/canvas/summary'));
1831
- return;
1832
- }
1833
- const result = await api('GET', '/api/canvas/state');
1834
- output(result);
1835
- },
1836
- );
1837
-
1838
- // ── status ───────────────────────────────────────────────────
1839
- cmd('status', 'Quick canvas summary', ['pmx-canvas status'], async (args) => {
1840
- const { flags } = parseFlags(args);
1841
- if (flags.help || flags.h) return showCommandHelp('status');
1842
-
1843
- const layout = (await api('GET', '/api/canvas/state')) as {
1844
- nodes: Array<Record<string, unknown>>;
1845
- edges: unknown[];
1846
- viewport: unknown;
1847
- };
1848
- const pinned = (await api('GET', '/api/canvas/pinned-context')) as { count: number; nodeIds: string[] };
1849
-
1850
- const typeCounts: Record<string, number> = {};
1851
- for (const n of layout.nodes) {
1852
- const data = isRecord(n.data) ? n.data : {};
1853
- const t =
1854
- typeof n.kind === 'string'
1855
- ? n.kind
1856
- : n.type === 'mcp-app' && data.hostMode === 'hosted' && typeof data.path === 'string'
1857
- ? 'web-artifact'
1858
- : (n.type as string);
1859
- typeCounts[t] = (typeCounts[t] || 0) + 1;
1860
- }
1861
-
1862
- output({
1863
- nodes: layout.nodes.length,
1864
- edges: layout.edges.length,
1865
- pinned: pinned.count,
1866
- types: typeCounts,
1867
- viewport: layout.viewport,
1868
- });
1869
- });
1870
-
1871
- // ── arrange ──────────────────────────────────────────────────
1872
- cmd(
1873
- 'arrange',
1874
- 'Auto-arrange nodes on the canvas',
1875
- ['pmx-canvas arrange', 'pmx-canvas arrange --layout column', 'pmx-canvas arrange --layout flow'],
1876
- async (args) => {
1877
- const { flags } = parseFlags(args);
1878
- if (flags.help || flags.h) return showCommandHelp('arrange');
1879
-
1880
- const body: Record<string, unknown> = {};
1881
- if (flags.layout && flags.layout !== true) body.layout = flags.layout;
1882
-
1883
- const result = await api('POST', '/api/canvas/arrange', body);
1884
- output(result);
1885
- },
1886
- );
1887
-
1888
- // ── focus ────────────────────────────────────────────────────
1889
- cmd('focus', 'Pan viewport to center on a node', ['pmx-canvas focus <node-id>'], async (args) => {
1890
- const { positional, flags } = parseFlags(args);
1891
- if (flags.help || flags.h) return showCommandHelp('focus');
1892
-
1893
- const id = positional[0];
1894
- if (!id) die('Missing node ID', 'pmx-canvas focus <node-id>');
1895
-
1896
- const result = await api('POST', '/api/canvas/focus', { id, ...(flags['no-pan'] ? { noPan: true } : {}) });
1897
- output(result);
1898
- });
1899
-
1900
- cmd(
1901
- 'fit',
1902
- 'Fit the viewport to all nodes or a selected subset',
1903
- ['pmx-canvas fit', 'pmx-canvas fit --width 1440 --height 900 --padding 80', 'pmx-canvas fit node-a node-b'],
1904
- async (args) => {
1905
- const { positional, flags } = parseFlags(args);
1906
- if (flags.help || flags.h) return showCommandHelp('fit');
1907
-
1908
- const body: Record<string, unknown> = {};
1909
- const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 1440');
1910
- const height = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 900');
1911
- const padding = optionalPositiveFiniteFlag(flags, 'padding', 'Use a positive number, e.g. --padding 80');
1912
- const maxScale = optionalPositiveFiniteFlag(flags, 'max-scale', 'Use a positive number, e.g. --max-scale 1');
1913
- if (width !== undefined) body.width = width;
1914
- if (height !== undefined) body.height = height;
1915
- if (padding !== undefined) body.padding = padding;
1916
- if (maxScale !== undefined) body.maxScale = maxScale;
1917
- if (positional.length > 0) body.nodeIds = positional;
1918
-
1919
- const result = await api('POST', '/api/canvas/fit', body);
1920
- output(result);
1921
- },
1922
- );
1923
-
1924
- // ── external-app add ─────────────────────────────────────────
1925
- cmd(
1926
- 'external-app add',
1927
- 'Create a hosted external app node',
1928
- ['pmx-canvas external-app add --kind excalidraw --title "Diagram"'],
1929
- async (args) => {
1930
- const { flags } = parseFlags(args);
1931
- if (flags.help || flags.h) return showCommandHelp('external-app add');
1932
-
1933
- const kind = typeof flags.kind === 'string' ? flags.kind.trim() : '';
1934
- if (kind !== 'excalidraw') {
1935
- die('Unsupported external app kind.', 'Use: pmx-canvas external-app add --kind excalidraw --title "Diagram"');
1936
- }
1937
-
1938
- const body: Record<string, unknown> = {
1939
- title: typeof flags.title === 'string' ? flags.title : 'Excalidraw Diagram',
1940
- elements: DEFAULT_EXCALIDRAW_ELEMENTS,
1941
- };
1942
- const nodeId = getStringFlag(flags, 'node-id', 'nodeId', 'id');
1943
- if (nodeId) body.nodeId = nodeId;
1944
- const elementsJson = getStringFlag(flags, 'elements-json', 'elements');
1945
- if (elementsJson !== undefined)
1946
- body.elements = parseJsonValue(
1947
- elementsJson,
1948
- 'Excalidraw elements',
1949
- 'Use --elements-json \'[{"type":"rectangle","id":"r1","x":0,"y":0,"width":120,"height":80}]\'',
1950
- );
1951
- const elementsFile = getStringFlag(flags, 'elements-file', 'initial-file');
1952
- if (elementsFile)
1953
- body.elements = parseJsonValue(
1954
- readFileSync(elementsFile, 'utf-8'),
1955
- 'Excalidraw elements file',
1956
- 'Use --elements-file ./scene.excalidraw',
1957
- );
1958
- applyCommonGeometryFlags(body, flags, {
1959
- x: 'Use a finite number, e.g. --x 500',
1960
- y: 'Use a finite number, e.g. --y 300',
1961
- width: 'Use a positive number, e.g. --width 960',
1962
- height: 'Use a positive number, e.g. --height 720',
1963
- });
1964
- const timeoutMs = optionalPositiveFiniteFlag(
1965
- flags,
1966
- 'timeout-ms',
1967
- 'Use a positive number, e.g. --timeout-ms 120000',
1968
- );
1969
- if (timeoutMs !== undefined) body.timeoutMs = timeoutMs;
1970
-
1971
- const result = await api('POST', '/api/canvas/diagram', body);
1972
- output(
1973
- result && typeof result === 'object' && !Array.isArray(result) && 'nodeId' in result && !('id' in result)
1974
- ? { id: (result as { nodeId?: unknown }).nodeId, ...result }
1975
- : result,
1976
- );
1977
- },
1978
- );
1979
-
1980
- cmd(
1981
- 'diagram add',
1982
- 'Create an Excalidraw diagram node',
1983
- [
1984
- 'pmx-canvas diagram add --title "Architecture"',
1985
- 'pmx-canvas diagram add --title "Architecture" --elements \'[{"type":"rectangle","id":"r1","x":0,"y":0,"width":120,"height":80}]\'',
1986
- ],
1987
- async (args) => {
1988
- const { flags } = parseFlags(args);
1989
- if (flags.help || flags.h) return showCommandHelp('diagram add');
1990
- const externalAppAdd = COMMANDS['external-app add'];
1991
- await externalAppAdd.run([...args, '--kind', 'excalidraw']);
1992
- },
1993
- );
1994
-
1995
- // ── pin ──────────────────────────────────────────────────────
1996
- cmd(
1997
- 'pin',
1998
- 'Manage context pins',
1999
- [
2000
- 'pmx-canvas pin node1 node2 node3',
2001
- 'pmx-canvas pin --set node1 node2 node3',
2002
- 'pmx-canvas pin --list',
2003
- 'pmx-canvas pin --clear',
2004
- ],
2005
- async (args) => {
2006
- const { positional, flags } = parseFlags(args);
2007
- if (flags.help || flags.h) return showCommandHelp('pin');
2008
-
2009
- if (flags.list) {
2010
- const result = await api('GET', '/api/canvas/pinned-context');
2011
- output(result);
2012
- return;
2013
- }
2014
-
2015
- if (flags.clear) {
2016
- const result = await invokeOperation('pin.set', { nodeIds: [] });
2017
- output(result);
2018
- return;
2019
- }
2020
-
2021
- // --set: positional args are node IDs
2022
- if (positional.length > 0 || flags.set) {
2023
- const result = await invokeOperation('pin.set', { nodeIds: positional });
2024
- output(result);
2025
- return;
2026
- }
2027
-
2028
- // Default: list
2029
- const result = await api('GET', '/api/canvas/pinned-context');
2030
- output(result);
2031
- },
2032
- );
2033
-
2034
- // ── AX ────────────────────────────────────────────────────────
2035
- cmd('ax status', 'Read host-agnostic PMX AX state', ['pmx-canvas ax status'], async (args) => {
2036
- const { flags } = parseFlags(args);
2037
- if (flags.help || flags.h) return showCommandHelp('ax status');
2038
-
2039
- output(await api('GET', '/api/canvas/ax'));
2040
- });
2041
-
2042
- cmd('ax context', 'Read agent-ready PMX AX context', ['pmx-canvas ax context'], async (args) => {
2043
- const { flags } = parseFlags(args);
2044
- if (flags.help || flags.h) return showCommandHelp('ax context');
2045
-
2046
- output(await api('GET', '/api/canvas/ax/context'));
2047
- });
2048
-
2049
- cmd(
2050
- 'ax focus',
2051
- 'Set or clear PMX AX focus without moving the viewport',
2052
- ['pmx-canvas ax focus node1 node2', 'pmx-canvas ax focus --clear'],
2053
- async (args) => {
2054
- const { positional, flags } = parseFlags(args);
2055
- if (flags.help || flags.h) return showCommandHelp('ax focus');
2056
-
2057
- const nodeIds = flags.clear ? [] : positional;
2058
- if (!flags.clear && nodeIds.length === 0) {
2059
- die('Missing node ID', 'pmx-canvas ax focus <node-id> [more-node-ids]');
2060
- }
2061
-
2062
- output(await api('POST', '/api/canvas/ax/focus', { nodeIds, source: resolveAxSource(flags) }));
2063
- },
2064
- );
2065
-
2066
- cmd(
2067
- 'ax event add',
2068
- 'Record a normalized AX timeline event',
2069
- [
2070
- 'pmx-canvas ax event add --kind tool-start --summary "ran tests"',
2071
- 'pmx-canvas ax event add --kind failure --summary "build broke" --detail "..." node1 node2',
2072
- ],
2073
- async (args) => {
2074
- const { positional, flags } = parseFlags(args);
2075
- if (flags.help || flags.h) return showCommandHelp('ax event add');
2076
-
2077
- const kind = requireFlag(flags, 'kind', 'pmx-canvas ax event add --kind <kind> --summary <text>');
2078
- const summary = requireFlag(flags, 'summary', 'pmx-canvas ax event add --kind <kind> --summary <text>');
2079
- const detail = getStringFlag(flags, 'detail');
2080
-
2081
- output(
2082
- await api('POST', '/api/canvas/ax/event', {
2083
- kind,
2084
- summary,
2085
- ...(detail ? { detail } : {}),
2086
- ...(positional.length > 0 ? { nodeIds: positional } : {}),
2087
- source: resolveAxSource(flags),
2088
- }),
2089
- );
2090
- },
2091
- );
2092
-
2093
- cmd(
2094
- 'ax steer',
2095
- 'Send a steering message to the active agent session',
2096
- ['pmx-canvas ax steer "focus on the failing test first"', 'pmx-canvas ax steer --message "stop and re-plan"'],
2097
- async (args) => {
2098
- const { positional, flags } = parseFlags(args);
2099
- if (flags.help || flags.h) return showCommandHelp('ax steer');
2100
-
2101
- const message = getStringFlag(flags, 'message') ?? positional.join(' ').trim();
2102
- if (!message) {
2103
- die('Missing steering message', 'pmx-canvas ax steer <message>');
2104
- }
2105
-
2106
- output(await api('POST', '/api/canvas/ax/steer', { message, source: resolveAxSource(flags) }));
2107
- },
2108
- );
2109
-
2110
- cmd(
2111
- 'ax interaction',
2112
- 'Submit a node-originated AX interaction (capability-gated)',
2113
- [
2114
- 'pmx-canvas ax interaction --type ax.work.create --node node-1 --payload \'{"title":"Wire auth"}\'',
2115
- 'pmx-canvas ax interaction --type ax.focus.set --node node-2',
2116
- ],
2117
- async (args) => {
2118
- const { flags } = parseFlags(args);
2119
- if (flags.help || flags.h) return showCommandHelp('ax interaction');
2120
-
2121
- const type = getStringFlag(flags, 'type');
2122
- if (!type) die('Missing --type', 'pmx-canvas ax interaction --type <ax.*> --node <id> [--payload <json>]');
2123
- const sourceNodeId = getStringFlag(flags, 'node');
2124
- if (!sourceNodeId) die('Missing --node', 'pmx-canvas ax interaction --type <ax.*> --node <id>');
2125
-
2126
- let payload: unknown;
2127
- const payloadRaw = getStringFlag(flags, 'payload');
2128
- if (payloadRaw) {
2129
- try {
2130
- payload = JSON.parse(payloadRaw);
2131
- } catch {
2132
- die('Invalid --payload JSON', 'pmx-canvas ax interaction --payload \'{"title":"..."}\'');
2133
- }
2134
- }
2135
-
2136
- output(
2137
- await api('POST', '/api/canvas/ax/interaction', {
2138
- type,
2139
- sourceNodeId,
2140
- ...(payload !== undefined ? { payload } : {}),
2141
- source: resolveAxSource(flags),
2142
- }),
2143
- );
2144
- },
2145
- );
2146
-
2147
- cmd(
2148
- 'ax delivery list',
2149
- 'List pending AX steering for a consumer (loop-safe)',
2150
- [
2151
- 'pmx-canvas ax delivery list',
2152
- 'pmx-canvas ax delivery list --consumer copilot --limit 20',
2153
- 'pmx-canvas ax delivery list --order newest # latest browser steering first (#68)',
2154
- ],
2155
- async (args) => {
2156
- const { flags } = parseFlags(args);
2157
- if (flags.help || flags.h) return showCommandHelp('ax delivery list');
2158
- const consumer = getStringFlag(flags, 'consumer');
2159
- const limit = optionalNumberFlag(flags, 'limit', 'pmx-canvas ax delivery list --limit <n>');
2160
- const order = getStringFlag(flags, 'order');
2161
- if (order !== undefined && order !== 'newest' && order !== 'oldest') {
2162
- die('Invalid --order', 'pmx-canvas ax delivery list --order newest|oldest');
2163
- }
2164
- const params = new URLSearchParams();
2165
- if (consumer) params.set('consumer', consumer);
2166
- if (limit) params.set('limit', String(limit));
2167
- if (order) params.set('order', order);
2168
- const qs = params.toString();
2169
- output(await api('GET', `/api/canvas/ax/delivery/pending${qs ? `?${qs}` : ''}`));
2170
- },
2171
- );
2172
-
2173
- cmd(
2174
- 'ax delivery mark',
2175
- 'Mark an AX steering message as delivered',
2176
- ['pmx-canvas ax delivery mark <steering-id>'],
2177
- async (args) => {
2178
- const { positional, flags } = parseFlags(args);
2179
- if (flags.help || flags.h) return showCommandHelp('ax delivery mark');
2180
- const id = getStringFlag(flags, 'id') ?? positional[0];
2181
- if (!id) die('Missing steering id', 'pmx-canvas ax delivery mark <steering-id>');
2182
- output(await api('POST', `/api/canvas/ax/delivery/${encodeURIComponent(id)}/mark`, {}));
2183
- },
2184
- );
2185
-
2186
- cmd(
2187
- 'ax elicitation request',
2188
- 'Request structured human input',
2189
- [
2190
- 'pmx-canvas ax elicitation request --prompt "Who owns this migration?"',
2191
- 'pmx-canvas ax elicitation request --prompt "Pick a region" --fields region,owner',
2192
- ],
2193
- async (args) => {
2194
- const { flags } = parseFlags(args);
2195
- if (flags.help || flags.h) return showCommandHelp('ax elicitation request');
2196
- const prompt = requireFlag(flags, 'prompt', 'pmx-canvas ax elicitation request --prompt <text>');
2197
- const fields = getStringFlag(flags, 'fields');
2198
- output(
2199
- await api('POST', '/api/canvas/ax/elicitation', {
2200
- prompt,
2201
- ...(fields
2202
- ? {
2203
- fields: fields
2204
- .split(',')
2205
- .map((f) => f.trim())
2206
- .filter(Boolean),
2207
- }
2208
- : {}),
2209
- source: resolveAxSource(flags),
2210
- }),
2211
- );
2212
- },
2213
- );
2214
-
2215
- cmd(
2216
- 'ax elicitation respond',
2217
- 'Answer a pending elicitation',
2218
- ['pmx-canvas ax elicitation respond <id> --response \'{"owner":"alice"}\''],
2219
- async (args) => {
2220
- const { positional, flags } = parseFlags(args);
2221
- if (flags.help || flags.h) return showCommandHelp('ax elicitation respond');
2222
- const id = getStringFlag(flags, 'id') ?? positional[0];
2223
- if (!id) die('Missing elicitation id', 'pmx-canvas ax elicitation respond <id> --response <json>');
2224
- let response: unknown = {};
2225
- const raw = getStringFlag(flags, 'response');
2226
- if (raw) {
2227
- try {
2228
- response = JSON.parse(raw);
2229
- } catch {
2230
- die('Invalid --response JSON', '--response \'{"k":"v"}\'');
2231
- }
2232
- }
2233
- output(
2234
- await api('POST', `/api/canvas/ax/elicitation/${encodeURIComponent(id)}/respond`, {
2235
- response,
2236
- source: resolveAxSource(flags),
2237
- }),
2238
- );
2239
- },
2240
- );
2241
-
2242
- cmd('ax elicitation list', 'List elicitations', ['pmx-canvas ax elicitation list'], async (args) => {
2243
- const { flags } = parseFlags(args);
2244
- if (flags.help || flags.h) return showCommandHelp('ax elicitation list');
2245
- output(await api('GET', '/api/canvas/ax/elicitation'));
2246
- });
2247
-
2248
- cmd(
2249
- 'ax mode request',
2250
- 'Request a workflow mode transition (plan/execute/autonomous)',
2251
- ['pmx-canvas ax mode request --mode execute --reason "plan approved"'],
2252
- async (args) => {
2253
- const { flags } = parseFlags(args);
2254
- if (flags.help || flags.h) return showCommandHelp('ax mode request');
2255
- const mode = requireFlag(flags, 'mode', 'pmx-canvas ax mode request --mode plan|execute|autonomous');
2256
- const reason = getStringFlag(flags, 'reason');
2257
- output(
2258
- await api('POST', '/api/canvas/ax/mode', { mode, ...(reason ? { reason } : {}), source: resolveAxSource(flags) }),
2259
- );
2260
- },
2261
- );
2262
-
2263
- cmd(
2264
- 'ax mode resolve',
2265
- 'Resolve a pending mode request',
2266
- ['pmx-canvas ax mode resolve <id> --decision approved'],
2267
- async (args) => {
2268
- const { positional, flags } = parseFlags(args);
2269
- if (flags.help || flags.h) return showCommandHelp('ax mode resolve');
2270
- const id = getStringFlag(flags, 'id') ?? positional[0];
2271
- if (!id) die('Missing mode request id', 'pmx-canvas ax mode resolve <id> --decision approved|rejected');
2272
- const decision = getStringFlag(flags, 'decision');
2273
- if (decision !== 'approved' && decision !== 'rejected') die('Invalid --decision', '--decision approved|rejected');
2274
- const resolution = getStringFlag(flags, 'resolution');
2275
- output(
2276
- await api('POST', `/api/canvas/ax/mode/${encodeURIComponent(id)}/resolve`, {
2277
- decision,
2278
- ...(resolution ? { resolution } : {}),
2279
- source: resolveAxSource(flags),
2280
- }),
2281
- );
2282
- },
2283
- );
2284
-
2285
- cmd('ax mode list', 'List mode requests', ['pmx-canvas ax mode list'], async (args) => {
2286
- const { flags } = parseFlags(args);
2287
- if (flags.help || flags.h) return showCommandHelp('ax mode list');
2288
- output(await api('GET', '/api/canvas/ax/mode'));
2289
- });
2290
-
2291
- cmd('ax command list', 'List the PMX command registry', ['pmx-canvas ax command list'], async (args) => {
2292
- const { flags } = parseFlags(args);
2293
- if (flags.help || flags.h) return showCommandHelp('ax command list');
2294
- output(await api('GET', '/api/canvas/ax/command'));
2295
- });
2296
-
2297
- cmd(
2298
- 'ax command invoke',
2299
- 'Invoke a registry-gated PMX command intent',
2300
- [
2301
- 'pmx-canvas ax command invoke pmx.plan',
2302
- 'pmx-canvas ax command invoke pmx.promote-context --args \'{"nodeIds":["n1"]}\'',
2303
- ],
2304
- async (args) => {
2305
- const { positional, flags } = parseFlags(args);
2306
- if (flags.help || flags.h) return showCommandHelp('ax command invoke');
2307
- const name = getStringFlag(flags, 'name') ?? positional[0];
2308
- if (!name) die('Missing command name', 'pmx-canvas ax command invoke <name>');
2309
- let cmdArgs: unknown;
2310
- const raw = getStringFlag(flags, 'args');
2311
- if (raw) {
2312
- try {
2313
- cmdArgs = JSON.parse(raw);
2314
- } catch {
2315
- die('Invalid --args JSON', '--args \'{"k":"v"}\'');
2316
- }
2317
- }
2318
- output(
2319
- await api('POST', '/api/canvas/ax/command', {
2320
- name,
2321
- ...(cmdArgs !== undefined ? { args: cmdArgs } : {}),
2322
- source: resolveAxSource(flags),
2323
- }),
2324
- );
2325
- },
2326
- );
2327
-
2328
- cmd('ax policy get', 'Show the current declarative AX policy', ['pmx-canvas ax policy get'], async (args) => {
2329
- const { flags } = parseFlags(args);
2330
- if (flags.help || flags.h) return showCommandHelp('ax policy get');
2331
- output(await api('GET', '/api/canvas/ax/policy'));
2332
- });
2333
-
2334
- cmd(
2335
- 'ax policy set',
2336
- 'Set the declarative AX policy (stored by PMX, enforced by adapters)',
2337
- ['pmx-canvas ax policy set --excluded-tools shell,write --mode concise'],
2338
- async (args) => {
2339
- const { flags } = parseFlags(args);
2340
- if (flags.help || flags.h) return showCommandHelp('ax policy set');
2341
- const csv = (v?: string) =>
2342
- v
2343
- ? v
2344
- .split(',')
2345
- .map((s) => s.trim())
2346
- .filter(Boolean)
2347
- : undefined;
2348
- const allowed = csv(getStringFlag(flags, 'allowed-tools'));
2349
- const excluded = csv(getStringFlag(flags, 'excluded-tools'));
2350
- const approvalRequired = csv(getStringFlag(flags, 'approval-tools'));
2351
- const mode = getStringFlag(flags, 'mode');
2352
- const systemAppend = getStringFlag(flags, 'system-append');
2353
- const tools =
2354
- allowed || excluded || approvalRequired
2355
- ? {
2356
- ...(allowed ? { allowed } : {}),
2357
- ...(excluded ? { excluded } : {}),
2358
- ...(approvalRequired ? { approvalRequired } : {}),
2359
- }
2360
- : undefined;
2361
- const prompt =
2362
- mode || systemAppend ? { ...(mode ? { mode } : {}), ...(systemAppend ? { systemAppend } : {}) } : undefined;
2363
- output(
2364
- await api('POST', '/api/canvas/ax/policy', {
2365
- ...(tools ? { tools } : {}),
2366
- ...(prompt ? { prompt } : {}),
2367
- source: resolveAxSource(flags),
2368
- }),
2369
- );
2370
- },
2371
- );
2372
-
2373
- cmd(
2374
- 'ax timeline',
2375
- 'Read the bounded AX timeline (events, evidence, steering)',
2376
- ['pmx-canvas ax timeline', 'pmx-canvas ax timeline --limit 100'],
2377
- async (args) => {
2378
- const { flags } = parseFlags(args);
2379
- if (flags.help || flags.h) return showCommandHelp('ax timeline');
2380
-
2381
- const limit = optionalNumberFlag(flags, 'limit', 'pmx-canvas ax timeline --limit <n>');
2382
- output(await api('GET', `/api/canvas/ax/timeline${limit ? `?limit=${limit}` : ''}`));
2383
- },
2384
- );
2385
-
2386
- cmd(
2387
- 'ax work add',
2388
- 'Add a canvas-bound AX work item',
2389
- [
2390
- 'pmx-canvas ax work add --title "Wire up auth" --status in-progress',
2391
- 'pmx-canvas ax work add --title "Review API" node1 node2',
2392
- ],
2393
- async (args) => {
2394
- const { positional, flags } = parseFlags(args);
2395
- if (flags.help || flags.h) return showCommandHelp('ax work add');
2396
-
2397
- const title = requireFlag(flags, 'title', 'pmx-canvas ax work add --title <text>');
2398
- const status = getStringFlag(flags, 'status');
2399
- const detail = getStringFlag(flags, 'detail');
2400
-
2401
- output(
2402
- await api('POST', '/api/canvas/ax/work', {
2403
- title,
2404
- ...(status ? { status } : {}),
2405
- ...(detail ? { detail } : {}),
2406
- ...(positional.length > 0 ? { nodeIds: positional } : {}),
2407
- source: resolveAxSource(flags),
2408
- }),
2409
- );
2410
- },
2411
- );
2412
-
2413
- cmd(
2414
- 'ax work update',
2415
- 'Update a canvas-bound AX work item by ID',
2416
- ['pmx-canvas ax work update <id> --status done', 'pmx-canvas ax work update <id> --title "New title" --detail "..."'],
2417
- async (args) => {
2418
- const { positional, flags } = parseFlags(args);
2419
- if (flags.help || flags.h) return showCommandHelp('ax work update');
2420
-
2421
- const id = positional[0];
2422
- if (!id) die('Missing work item ID', 'pmx-canvas ax work update <id> --status <status>');
2423
- const title = getStringFlag(flags, 'title');
2424
- const status = getStringFlag(flags, 'status');
2425
- const detail = getStringFlag(flags, 'detail');
2426
-
2427
- output(
2428
- await api('PATCH', `/api/canvas/ax/work/${encodeURIComponent(id)}`, {
2429
- ...(title ? { title } : {}),
2430
- ...(status ? { status } : {}),
2431
- ...(detail ? { detail } : {}),
2432
- ...(positional.length > 1 ? { nodeIds: positional.slice(1) } : {}),
2433
- source: resolveAxSource(flags),
2434
- }),
2435
- );
2436
- },
2437
- );
2438
-
2439
- cmd('ax work list', 'List canvas-bound AX work items', ['pmx-canvas ax work list'], async (args) => {
2440
- const { flags } = parseFlags(args);
2441
- if (flags.help || flags.h) return showCommandHelp('ax work list');
2442
-
2443
- output(await api('GET', '/api/canvas/ax/work'));
2444
- });
2445
-
2446
- cmd(
2447
- 'ax approval request',
2448
- 'Request a canvas-bound AX approval gate',
2449
- [
2450
- 'pmx-canvas ax approval request --title "Deploy to prod"',
2451
- 'pmx-canvas ax approval request --title "Drop table" --action db.drop node1',
2452
- ],
2453
- async (args) => {
2454
- const { positional, flags } = parseFlags(args);
2455
- if (flags.help || flags.h) return showCommandHelp('ax approval request');
2456
-
2457
- const title = requireFlag(flags, 'title', 'pmx-canvas ax approval request --title <text>');
2458
- const detail = getStringFlag(flags, 'detail');
2459
- const action = getStringFlag(flags, 'action');
2460
-
2461
- output(
2462
- await api('POST', '/api/canvas/ax/approval', {
2463
- title,
2464
- ...(detail ? { detail } : {}),
2465
- ...(action ? { action } : {}),
2466
- ...(positional.length > 0 ? { nodeIds: positional } : {}),
2467
- source: resolveAxSource(flags),
2468
- }),
2469
- );
2470
- },
2471
- );
2472
-
2473
- cmd(
2474
- 'ax approval resolve',
2475
- 'Resolve a pending AX approval gate by ID',
2476
- [
2477
- 'pmx-canvas ax approval resolve <id> --decision approved',
2478
- 'pmx-canvas ax approval resolve <id> --decision rejected --resolution "too risky"',
2479
- ],
2480
- async (args) => {
2481
- const { positional, flags } = parseFlags(args);
2482
- if (flags.help || flags.h) return showCommandHelp('ax approval resolve');
2483
-
2484
- const id = positional[0];
2485
- if (!id) die('Missing approval gate ID', 'pmx-canvas ax approval resolve <id> --decision <approved|rejected>');
2486
- const decision = requireFlag(
2487
- flags,
2488
- 'decision',
2489
- 'pmx-canvas ax approval resolve <id> --decision <approved|rejected>',
2490
- );
2491
- if (decision !== 'approved' && decision !== 'rejected') {
2492
- die('Invalid decision', 'pmx-canvas ax approval resolve <id> --decision <approved|rejected>');
2493
- }
2494
- const resolution = getStringFlag(flags, 'resolution');
2495
-
2496
- output(
2497
- await api('POST', `/api/canvas/ax/approval/${encodeURIComponent(id)}/resolve`, {
2498
- decision,
2499
- ...(resolution ? { resolution } : {}),
2500
- source: resolveAxSource(flags),
2501
- }),
2502
- );
2503
- },
2504
- );
2505
-
2506
- cmd('ax approval list', 'List canvas-bound AX approval gates', ['pmx-canvas ax approval list'], async (args) => {
2507
- const { flags } = parseFlags(args);
2508
- if (flags.help || flags.h) return showCommandHelp('ax approval list');
2509
-
2510
- output(await api('GET', '/api/canvas/ax/approval'));
2511
- });
2512
-
2513
- cmd(
2514
- 'ax evidence add',
2515
- 'Record an AX evidence item on the timeline',
2516
- [
2517
- 'pmx-canvas ax evidence add --kind test-output --title "unit pass" --body "..."',
2518
- 'pmx-canvas ax evidence add --kind screenshot --title "before" --ref /tmp/before.png node1',
2519
- ],
2520
- async (args) => {
2521
- const { positional, flags } = parseFlags(args);
2522
- if (flags.help || flags.h) return showCommandHelp('ax evidence add');
2523
-
2524
- const kind = requireFlag(flags, 'kind', 'pmx-canvas ax evidence add --kind <kind> --title <text>');
2525
- const title = requireFlag(flags, 'title', 'pmx-canvas ax evidence add --kind <kind> --title <text>');
2526
- const body = getStringFlag(flags, 'body');
2527
- const ref = getStringFlag(flags, 'ref');
2528
-
2529
- output(
2530
- await api('POST', '/api/canvas/ax/evidence', {
2531
- kind,
2532
- title,
2533
- ...(body ? { body } : {}),
2534
- ...(ref ? { ref } : {}),
2535
- ...(positional.length > 0 ? { nodeIds: positional } : {}),
2536
- source: resolveAxSource(flags),
2537
- }),
2538
- );
2539
- },
2540
- );
2541
-
2542
- cmd(
2543
- 'ax review add',
2544
- 'Add a canvas-bound AX review annotation',
2545
- [
2546
- 'pmx-canvas ax review add --body "needs a test" --node node1',
2547
- 'pmx-canvas ax review add --body "off-by-one" --kind finding --severity error --file src/x.ts',
2548
- ],
2549
- async (args) => {
2550
- const { flags } = parseFlags(args);
2551
- if (flags.help || flags.h) return showCommandHelp('ax review add');
2552
-
2553
- const body = requireFlag(flags, 'body', 'pmx-canvas ax review add --body <text>');
2554
- const kind = getStringFlag(flags, 'kind');
2555
- const severity = getStringFlag(flags, 'severity');
2556
- const anchorType = getStringFlag(flags, 'anchor');
2557
- const nodeId = getStringFlag(flags, 'node');
2558
- const file = getStringFlag(flags, 'file');
2559
- const author = getStringFlag(flags, 'author');
2560
-
2561
- output(
2562
- await api('POST', '/api/canvas/ax/review', {
2563
- body,
2564
- ...(kind ? { kind } : {}),
2565
- ...(severity ? { severity } : {}),
2566
- ...(anchorType ? { anchorType } : {}),
2567
- ...(nodeId ? { nodeId } : {}),
2568
- ...(file ? { file } : {}),
2569
- ...(author ? { author } : {}),
2570
- source: resolveAxSource(flags),
2571
- }),
2572
- );
2573
- },
2574
- );
2575
-
2576
- cmd('ax review list', 'List canvas-bound AX review annotations', ['pmx-canvas ax review list'], async (args) => {
2577
- const { flags } = parseFlags(args);
2578
- if (flags.help || flags.h) return showCommandHelp('ax review list');
2579
-
2580
- output(await api('GET', '/api/canvas/ax/review'));
2581
- });
2582
-
2583
- cmd(
2584
- 'ax host report',
2585
- 'Report host/session capability to the canvas',
2586
- [
2587
- 'pmx-canvas ax host report --host copilot --canvas --tools --session-messaging',
2588
- 'pmx-canvas ax host report --host codex --canvas --files',
2589
- ],
2590
- async (args) => {
2591
- const { flags } = parseFlags(args);
2592
- if (flags.help || flags.h) return showCommandHelp('ax host report');
2593
-
2594
- const host = getStringFlag(flags, 'host');
2595
-
2596
- output(
2597
- await api('PUT', '/api/canvas/ax/host-capability', {
2598
- ...(host ? { host } : {}),
2599
- canvas: flags.canvas === true,
2600
- hooks: flags.hooks === true,
2601
- tools: flags.tools === true,
2602
- sessionMessaging: flags['session-messaging'] === true,
2603
- permissions: flags.permissions === true,
2604
- files: flags.files === true,
2605
- uiPrompts: flags['ui-prompts'] === true,
2606
- source: resolveAxSource(flags),
2607
- }),
2608
- );
2609
- },
2610
- );
2611
-
2612
- cmd('ax host status', 'Read the reported host/session capability', ['pmx-canvas ax host status'], async (args) => {
2613
- const { flags } = parseFlags(args);
2614
- if (flags.help || flags.h) return showCommandHelp('ax host status');
2615
-
2616
- output(await api('GET', '/api/canvas/ax/host-capability'));
2617
- });
2618
-
2619
- // ── copilot install-extension ────────────────────────────────
2620
- cmd(
2621
- 'copilot install-extension',
2622
- 'Install the bundled GitHub Copilot extension adapter',
2623
- [
2624
- 'pmx-canvas copilot install-extension --dry-run',
2625
- 'pmx-canvas copilot install-extension --target .github/extensions/pmx-canvas/extension.mjs --yes',
2626
- ],
2627
- async (args) => {
2628
- const { flags } = parseFlags(args);
2629
- if (flags.help || flags.h) return showCommandHelp('copilot install-extension');
2630
-
2631
- const sourcePath = fileURLToPath(new URL('../../.github/extensions/pmx-canvas/extension.mjs', import.meta.url));
2632
- if (!existsSync(sourcePath)) {
2633
- die('Bundled Copilot extension adapter not found.', `Expected at ${sourcePath}`);
2634
- }
2635
-
2636
- const targetPath =
2637
- getStringFlag(flags, 'target') ?? join(process.cwd(), '.github', 'extensions', 'pmx-canvas', 'extension.mjs');
2638
- const dryRun = flags['dry-run'] === true;
2639
- const targetExists = existsSync(targetPath);
2640
-
2641
- if (dryRun) {
2642
- output({ ok: true, dryRun: true, sourcePath, targetPath, targetExists, wrote: false });
2643
- return;
2644
- }
2645
-
2646
- if (targetExists && flags.yes !== true) {
2647
- die('Target already exists. Re-run with --yes to overwrite.', `Target: ${targetPath}`);
2648
- }
2649
-
2650
- mkdirSync(dirname(targetPath), { recursive: true });
2651
- copyFileSync(sourcePath, targetPath);
2652
- output({ ok: true, dryRun: false, sourcePath, targetPath, wrote: true });
2653
- },
2654
- );
2655
-
2656
- // ── undo ─────────────────────────────────────────────────────
2657
- cmd('undo', 'Undo the last canvas mutation', ['pmx-canvas undo'], async (args) => {
2658
- const { flags } = parseFlags(args);
2659
- if (flags.help || flags.h) return showCommandHelp('undo');
2660
-
2661
- const result = await invokeOperation('canvas.undo', {});
2662
- output(result);
2663
- });
2664
-
2665
- // ── redo ─────────────────────────────────────────────────────
2666
- cmd('redo', 'Redo the last undone mutation', ['pmx-canvas redo'], async (args) => {
2667
- const { flags } = parseFlags(args);
2668
- if (flags.help || flags.h) return showCommandHelp('redo');
2669
-
2670
- const result = await invokeOperation('canvas.redo', {});
2671
- output(result);
2672
- });
2673
-
2674
- // ── history ──────────────────────────────────────────────────
2675
- cmd(
2676
- 'history',
2677
- 'Show canvas mutation history',
2678
- ['pmx-canvas history', 'pmx-canvas history --summary', 'pmx-canvas history --compact'],
2679
- async (args) => {
2680
- const { flags } = parseFlags(args);
2681
- if (flags.help || flags.h) return showCommandHelp('history');
2682
-
2683
- const result = (await invokeOperation('history.get', {})) as Record<string, unknown>;
2684
- if (flags.summary) {
2685
- output(summarizeHistoryResult(result));
2686
- return;
2687
- }
2688
- if (flags.compact) {
2689
- output(compactHistoryResult(result));
2690
- return;
2691
- }
2692
- output(result);
2693
- },
2694
- );
2695
-
2696
- // ── snapshot save ────────────────────────────────────────────
2697
- cmd(
2698
- 'snapshot save',
2699
- 'Save a named snapshot of the current canvas',
2700
- ['pmx-canvas snapshot save --name "before-refactor"', 'pmx-canvas snapshot save --name checkpoint-1'],
2701
- async (args) => {
2702
- const { flags } = parseFlags(args);
2703
- if (flags.help || flags.h) return showCommandHelp('snapshot save');
2704
-
2705
- const name = requireFlag(flags, 'name', 'pmx-canvas snapshot save --name "my-snapshot"');
2706
- const result = await invokeOperation('snapshot.save', { name });
2707
- output(result);
2708
- },
2709
- );
2710
-
2711
- // ── snapshot list ────────────────────────────────────────────
2712
- cmd(
2713
- 'snapshot list',
2714
- 'List saved snapshots',
2715
- [
2716
- 'pmx-canvas snapshot list',
2717
- 'pmx-canvas snapshot list --limit 50 --query baseline',
2718
- 'pmx-canvas snapshot list --after 2026-05-01T00:00:00Z --before 2026-05-05T00:00:00Z',
2719
- 'pmx-canvas snapshot list --all',
2720
- ],
2721
- async (args) => {
2722
- const { flags } = parseFlags(args);
2723
- if (flags.help || flags.h) return showCommandHelp('snapshot list');
2724
-
2725
- const limit = optionalNumberFlag(flags, 'limit', 'Use a positive integer, e.g. --limit 50');
2726
- const query = getStringFlag(flags, 'query', 'q');
2727
- const before = getStringFlag(flags, 'before');
2728
- const after = getStringFlag(flags, 'after');
2729
- const result = await invokeOperation('snapshot.list', {
2730
- ...(limit !== undefined ? { limit } : {}),
2731
- ...(query ? { q: query } : {}),
2732
- ...(before ? { before } : {}),
2733
- ...(after ? { after } : {}),
2734
- ...(flags.all ? { all: true } : {}),
2735
- });
2736
- output(result);
2737
- },
2738
- );
2739
-
2740
- // ── snapshot gc ──────────────────────────────────────────────
2741
- cmd(
2742
- 'snapshot gc',
2743
- 'Delete old snapshots, keeping the newest N',
2744
- ['pmx-canvas snapshot gc --keep 20 --dry-run', 'pmx-canvas snapshot gc --keep 50 --yes'],
2745
- async (args) => {
2746
- const { flags } = parseFlags(args);
2747
- if (flags.help || flags.h) return showCommandHelp('snapshot gc');
2748
-
2749
- const keep = optionalNumberFlag(flags, 'keep', 'Use a positive integer, e.g. --keep 20');
2750
- const dryRun = flags['dry-run'] === true;
2751
- if (!dryRun && !flags.yes) {
2752
- die('Destructive operation requires --yes flag', 'Preview with: pmx-canvas snapshot gc --keep 20 --dry-run');
2753
- }
2754
- const result = await invokeOperation('snapshot.gc', {
2755
- ...(keep !== undefined ? { keep } : {}),
2756
- dryRun,
2757
- });
2758
- output(result);
2759
- },
2760
- );
2761
-
2762
- // ── snapshot restore ─────────────────────────────────────────
2763
- cmd(
2764
- 'snapshot restore',
2765
- 'Restore canvas from a snapshot',
2766
- ['pmx-canvas snapshot restore <snapshot-id-or-name>'],
2767
- async (args) => {
2768
- const { positional, flags } = parseFlags(args);
2769
- if (flags.help || flags.h) return showCommandHelp('snapshot restore');
2770
-
2771
- const id = positional[0];
2772
- if (!id) die('Missing snapshot ID or name', 'pmx-canvas snapshot restore <snapshot-id-or-name>');
2773
-
2774
- const result = await invokeOperation('snapshot.restore', { id });
2775
- output(result);
2776
- },
2777
- );
2778
-
2779
- // ── snapshot delete ──────────────────────────────────────────
2780
- cmd('snapshot delete', 'Delete a saved snapshot', ['pmx-canvas snapshot delete <snapshot-id>'], async (args) => {
2781
- const { positional, flags } = parseFlags(args);
2782
- if (flags.help || flags.h) return showCommandHelp('snapshot delete');
2783
-
2784
- const id = positional[0];
2785
- if (!id) die('Missing snapshot ID', 'pmx-canvas snapshot delete <snapshot-id>');
2786
-
2787
- const result = await invokeOperation('snapshot.delete', { id });
2788
- output(result);
2789
- });
2790
-
2791
- async function runSnapshotDiff(args: string[]): Promise<void> {
2792
- const { positional, flags } = parseFlags(args);
2793
- const snapshot = positional[0];
2794
- if (!snapshot) die('Missing snapshot ID or name', 'pmx-canvas snapshot diff <snapshot-id-or-name>');
2795
- const result = await invokeOperation('snapshot.diff', { id: snapshot });
2796
- output(result);
2797
- }
2798
-
2799
- // ── snapshot diff ────────────────────────────────────────────
2800
- cmd(
2801
- 'snapshot diff',
2802
- 'Compare current canvas against a saved snapshot',
2803
- ['pmx-canvas snapshot diff <snapshot-id>', 'pmx-canvas snapshot diff "before-refactor"'],
2804
- async (args) => {
2805
- const { flags } = parseFlags(args);
2806
- if (flags.help || flags.h) return showCommandHelp('snapshot diff');
2807
- await runSnapshotDiff(args);
2808
- },
2809
- );
2810
-
2811
- // ── diff ─────────────────────────────────────────────────────
2812
- cmd('diff', 'Compare current canvas against a snapshot', ['pmx-canvas diff <snapshot-id>'], async (args) => {
2813
- const { flags } = parseFlags(args);
2814
- if (flags.help || flags.h) return showCommandHelp('diff');
2815
- await runSnapshotDiff(args);
2816
- });
2817
-
2818
- // ── group create ─────────────────────────────────────────────
2819
- cmd(
2820
- 'group create',
2821
- 'Create a group node',
2822
- [
2823
- 'pmx-canvas group create --title "API Layer" node1 node2',
2824
- 'pmx-canvas group create --title "Quarterly board" --x 40 --y 60 --width 1600 --height 900 --child-layout column node1 node2',
2825
- 'pmx-canvas group create --title "Frontend" --color "#ff6b6b"',
2826
- ],
2827
- async (args) => {
2828
- const { positional, flags } = parseFlags(args);
2829
- if (flags.help || flags.h) return showCommandHelp('group create');
2830
-
2831
- const body: Record<string, unknown> = {};
2832
- if (flags.title && flags.title !== true) body.title = flags.title;
2833
- if (flags.color && flags.color !== true) body.color = flags.color;
2834
- const x = optionalFiniteFlag(flags, 'x', 'Use a finite number, e.g. --x 40');
2835
- const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 60');
2836
- const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 1600');
2837
- const height = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 900');
2838
- if (x !== undefined) body.x = x;
2839
- if (y !== undefined) body.y = y;
2840
- if (width !== undefined) body.width = width;
2841
- if (height !== undefined) body.height = height;
2842
- if (typeof flags['child-layout'] === 'string') body.childLayout = flags['child-layout'];
2843
- if (positional.length > 0) body.childIds = positional;
2844
-
2845
- const result = await api('POST', '/api/canvas/group', body);
2846
- output(result);
2847
- },
2848
- );
2849
-
2850
- // ── group add ────────────────────────────────────────────────
2851
- cmd(
2852
- 'group add',
2853
- 'Add nodes to an existing group',
2854
- [
2855
- 'pmx-canvas group add --group <group-id> node1 node2',
2856
- 'pmx-canvas group add --group <group-id> --child-layout flow node1 node2',
2857
- ],
2858
- async (args) => {
2859
- const { positional, flags } = parseFlags(args);
2860
- if (flags.help || flags.h) return showCommandHelp('group add');
2861
-
2862
- const groupId = requireFlag(flags, 'group', 'pmx-canvas group add --group <group-id> node1 node2');
2863
- if (positional.length === 0) die('No node IDs provided', 'pmx-canvas group add --group <group-id> node1 node2');
2864
-
2865
- const result = await api('POST', '/api/canvas/group/add', {
2866
- groupId,
2867
- childIds: positional,
2868
- ...(typeof flags['child-layout'] === 'string' ? { childLayout: flags['child-layout'] } : {}),
2869
- });
2870
- output(result);
2871
- },
2872
- );
2873
-
2874
- // ── batch ────────────────────────────────────────────────────
2875
- cmd(
2876
- 'batch',
2877
- 'Run a batch of canvas operations from JSON',
2878
- [
2879
- 'pmx-canvas batch --file ./canvas-ops.json',
2880
- 'pmx-canvas batch --json \'[{"op":"node.add","assign":"a","args":{"type":"markdown","title":"A"}}]\'',
2881
- 'pmx-canvas batch --json \'[{"op":"graph.add","assign":"g","args":{"graphType":"bar","data":[{"label":"Docs","value":5}],"xKey":"label","yKey":"value"}}]\'',
2882
- 'cat ops.json | pmx-canvas batch --stdin',
2883
- ],
2884
- async (args) => {
2885
- const { flags } = parseFlags(args);
2886
- if (flags.help || flags.h) return showCommandHelp('batch');
2887
-
2888
- let raw = '';
2889
- if (typeof flags.file === 'string') {
2890
- try {
2891
- raw = readFileSync(flags.file, 'utf-8');
2892
- } catch (error) {
2893
- die(
2894
- `Unable to read --file: ${error instanceof Error ? error.message : String(error)}`,
2895
- 'Use: pmx-canvas batch --file ./canvas-ops.json',
2896
- );
2897
- }
2898
- } else if (typeof flags.json === 'string') {
2899
- raw = flags.json;
2900
- } else if (flags.stdin) {
2901
- raw = await readStdin();
2902
- } else {
2903
- die('Batch operations require --file, --json, or --stdin.', 'Use: pmx-canvas batch --file ./canvas-ops.json');
2904
- }
2905
-
2906
- let parsed: unknown;
2907
- try {
2908
- parsed = JSON.parse(raw);
2909
- } catch (error) {
2910
- die(
2911
- `Invalid batch JSON: ${error instanceof Error ? error.message : String(error)}`,
2912
- 'Use a JSON array of operations or an object with an "operations" array.',
2913
- );
2914
- }
2915
-
2916
- const result = await api(
2917
- 'POST',
2918
- '/api/canvas/batch',
2919
- Array.isArray(parsed) ? { operations: parsed } : (parsed as Record<string, unknown>),
2920
- );
2921
- output(result);
2922
- },
2923
- );
2924
-
2925
- // ── validate ─────────────────────────────────────────────────
2926
- cmd(
2927
- 'validate',
2928
- 'Validate the current layout for collisions and missing edge endpoints',
2929
- ['pmx-canvas validate'],
2930
- async (args) => {
2931
- const { flags } = parseFlags(args);
2932
- if (flags.help || flags.h) return showCommandHelp('validate');
2933
-
2934
- const result = await api('GET', '/api/canvas/validate');
2935
- output(result);
2936
- },
2937
- );
2938
-
2939
- cmd(
2940
- 'validate spec',
2941
- 'Validate a json-render spec or graph payload without creating a node',
2942
- [
2943
- 'pmx-canvas validate spec --type json-render --spec-file ./dashboard.json',
2944
- 'pmx-canvas validate spec --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
2945
- 'pmx-canvas validate spec --type html-primitive --kind choice-grid --data-file ./options.json',
2946
- 'pmx-canvas validate spec --type json-render --spec-file ./dashboard.json --summary',
2947
- ],
2948
- async (args) => {
2949
- const { flags } = parseFlags(args);
2950
- if (flags.help || flags.h) return showCommandHelp('validate spec');
2951
-
2952
- const type = getStringFlag(flags, 'type');
2953
- if (type !== 'json-render' && type !== 'graph' && type !== 'html-primitive') {
2954
- die('validate spec requires --type json-render, --type graph, or --type html-primitive.');
2955
- }
2956
-
2957
- let body: Record<string, unknown>;
2958
- if (type === 'json-render') {
2959
- body = {
2960
- type,
2961
- spec: (await buildJsonRenderRequestBody({ ...flags, title: String(flags.title ?? 'Validation') })).spec,
2962
- };
2963
- } else if (type === 'html-primitive') {
2964
- const primitiveBody = await buildHtmlPrimitiveRequestBody(flags);
2965
- body = {
2966
- type,
2967
- kind: primitiveBody.primitive,
2968
- ...(typeof primitiveBody.title === 'string' ? { title: primitiveBody.title } : {}),
2969
- ...(isRecord(primitiveBody.data) ? { data: primitiveBody.data } : {}),
2970
- };
2971
- } else {
2972
- body = { type, ...(await buildGraphRequestBody(flags)) };
2973
- }
2974
-
2975
- const result = (await api('POST', '/api/canvas/schema/validate', body)) as Record<string, unknown>;
2976
- if (flags.summary) {
2977
- output({
2978
- ok: result.ok,
2979
- type: result.type,
2980
- summary: result.summary,
2981
- });
2982
- return;
2983
- }
2984
- output(result);
2985
- },
2986
- );
2987
-
2988
- // ── group remove ─────────────────────────────────────────────
2989
- cmd('group remove', 'Ungroup all children from a group', ['pmx-canvas group remove <group-id>'], async (args) => {
2990
- const { positional, flags } = parseFlags(args);
2991
- if (flags.help || flags.h) return showCommandHelp('group remove');
2992
-
2993
- const id = positional[0];
2994
- if (!id) die('Missing group ID', 'pmx-canvas group remove <group-id>');
2995
-
2996
- const result = await api('POST', '/api/canvas/group/ungroup', { groupId: id });
2997
- output(result);
2998
- });
2999
-
3000
- // ── web-artifact build ───────────────────────────────────────
3001
- cmd(
3002
- 'web-artifact build',
3003
- 'Build a bundled HTML web artifact and optionally open it on the canvas',
3004
- [
3005
- 'pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx',
3006
- 'pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx --index-css-file ./index.css',
3007
- 'pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx --include-logs',
3008
- ],
3009
- async (args) => {
3010
- const { flags } = parseFlags(args);
3011
- if (flags.help || flags.h) return showCommandHelp('web-artifact build');
3012
- await runWebArtifactBuildCommand(flags);
3013
- },
3014
- );
3015
-
3016
- // ── clear ────────────────────────────────────────────────────
3017
- cmd(
3018
- 'clear',
3019
- 'Remove all nodes and edges from the canvas',
3020
- ['pmx-canvas clear --yes', 'pmx-canvas clear --dry-run'],
3021
- async (args) => {
3022
- const { flags } = parseFlags(args);
3023
- if (flags.help || flags.h) return showCommandHelp('clear');
3024
-
3025
- if (flags['dry-run']) {
3026
- const layout = (await api('GET', '/api/canvas/state')) as { nodes: unknown[]; edges: unknown[] };
3027
- output({
3028
- dry_run: true,
3029
- would_remove: { nodes: layout.nodes.length, edges: layout.edges.length },
3030
- message: 'No changes made. Pass --yes to confirm.',
3031
- });
3032
- return;
3033
- }
3034
-
3035
- if (!flags.yes) {
3036
- die('Destructive operation requires --yes flag', 'pmx-canvas clear --yes (or preview with --dry-run)');
3037
- }
3038
-
3039
- const result = await api('POST', '/api/canvas/clear');
3040
- output(result);
3041
- },
3042
- );
3043
-
3044
- // ── webview status ────────────────────────────────────────────
3045
- cmd('webview status', 'Show Bun.WebView automation status', ['pmx-canvas webview status'], async (args) => {
3046
- const { flags } = parseFlags(args);
3047
- if (flags.help || flags.h) return showCommandHelp('webview status');
3048
-
3049
- const result = await api('GET', '/api/workbench/webview');
3050
- output(result);
3051
- });
3052
-
3053
- // ── webview start ─────────────────────────────────────────────
3054
- cmd(
3055
- 'webview start',
3056
- 'Start or replace the Bun.WebView automation session',
3057
- [
3058
- 'pmx-canvas webview start',
3059
- 'pmx-canvas webview start --backend chrome --width 1440 --height 900',
3060
- 'pmx-canvas webview start --chrome-path /Applications/Google\\ Chrome.app/.../Google\\ Chrome',
3061
- ],
3062
- async (args) => {
3063
- const { flags } = parseFlags(args);
3064
- if (flags.help || flags.h) return showCommandHelp('webview start');
3065
-
3066
- const backend = flags.backend;
3067
- if (backend && backend !== true && backend !== 'chrome' && backend !== 'webkit') {
3068
- die('Invalid value for --backend', 'Use: --backend chrome or --backend webkit');
3069
- }
3070
-
3071
- const body: Record<string, unknown> = {};
3072
- if (backend && backend !== true) body.backend = backend;
3073
-
3074
- const width = optionalNumberFlag(flags, 'width', 'Use a positive integer width, e.g. --width 1440');
3075
- const height = optionalNumberFlag(flags, 'height', 'Use a positive integer height, e.g. --height 900');
3076
- if (width !== undefined) body.width = width;
3077
- if (height !== undefined) body.height = height;
3078
-
3079
- if (flags['chrome-path'] && flags['chrome-path'] !== true) {
3080
- body.chromePath = flags['chrome-path'];
3081
- }
3082
-
3083
- if (flags['data-dir'] && flags['data-dir'] !== true) {
3084
- body.dataStoreDir = flags['data-dir'];
3085
- }
3086
-
3087
- if (flags['chrome-argv'] && flags['chrome-argv'] !== true) {
3088
- const chromeArgv = String(flags['chrome-argv'])
3089
- .split(',')
3090
- .map((value) => value.trim())
3091
- .filter((value) => value.length > 0);
3092
- if (chromeArgv.length > 0) body.chromeArgv = chromeArgv;
3093
- }
3094
-
3095
- const result = await api('POST', '/api/workbench/webview/start', body, { allowErrorJson: true });
3096
- output(result);
3097
- },
3098
- );
3099
-
3100
- // ── webview stop ──────────────────────────────────────────────
3101
- cmd('webview stop', 'Stop the active Bun.WebView automation session', ['pmx-canvas webview stop'], async (args) => {
3102
- const { flags } = parseFlags(args);
3103
- if (flags.help || flags.h) return showCommandHelp('webview stop');
3104
-
3105
- const result = await api('DELETE', '/api/workbench/webview');
3106
- output(result);
3107
- });
3108
-
3109
- // ── webview evaluate ──────────────────────────────────────────
3110
- cmd(
3111
- 'webview evaluate',
3112
- 'Evaluate JavaScript in the active Bun.WebView automation session',
3113
- [
3114
- 'pmx-canvas webview evaluate --expression "document.title"',
3115
- 'pmx-canvas webview evaluate --script "const title = document.title; return title.toUpperCase()"',
3116
- 'pmx-canvas webview evaluate --file ./probe.js',
3117
- ],
3118
- async (args) => {
3119
- const { flags } = parseFlags(args);
3120
- if (flags.help || flags.h) return showCommandHelp('webview evaluate');
3121
-
3122
- const sourceCount = [flags.expression, flags.script, flags.file].filter(Boolean).length;
3123
- if (sourceCount > 1) {
3124
- die(
3125
- 'Use only one of --expression, --script, or --file.',
3126
- 'pmx-canvas webview evaluate --expression "document.title"',
3127
- );
3128
- }
3129
-
3130
- let expression = '';
3131
- if (typeof flags.file === 'string') {
3132
- let script = '';
3133
- try {
3134
- script = readFileSync(flags.file, 'utf-8');
3135
- } catch (error) {
3136
- die(
3137
- `Unable to read --file: ${error instanceof Error ? error.message : String(error)}`,
3138
- 'pmx-canvas webview evaluate --file ./probe.js',
3139
- );
3140
- }
3141
- expression = wrapCanvasAutomationScript(script);
3142
- } else if (typeof flags.script === 'string') {
3143
- expression = wrapCanvasAutomationScript(flags.script);
3144
- } else {
3145
- expression = requireFlag(flags, 'expression', 'pmx-canvas webview evaluate --expression "document.title"');
3146
- }
3147
-
3148
- const result = await api('POST', '/api/workbench/webview/evaluate', { expression });
3149
- output(result);
3150
- },
3151
- );
3152
-
3153
- // ── webview resize ────────────────────────────────────────────
3154
- cmd(
3155
- 'webview resize',
3156
- 'Resize the active Bun.WebView automation session viewport',
3157
- ['pmx-canvas webview resize --width 1280 --height 800'],
3158
- async (args) => {
3159
- const { flags } = parseFlags(args);
3160
- if (flags.help || flags.h) return showCommandHelp('webview resize');
3161
-
3162
- const width = optionalNumberFlag(flags, 'width', 'Use: pmx-canvas webview resize --width 1280 --height 800');
3163
- const height = optionalNumberFlag(flags, 'height', 'Use: pmx-canvas webview resize --width 1280 --height 800');
3164
- if (width === undefined || height === undefined) {
3165
- die('Missing required flags: --width, --height', 'Use: pmx-canvas webview resize --width 1280 --height 800');
3166
- }
3167
-
3168
- const result = await api('POST', '/api/workbench/webview/resize', { width, height });
3169
- output(result);
3170
- },
3171
- );
3172
-
3173
- // ── webview screenshot ────────────────────────────────────────
3174
- cmd(
3175
- 'webview screenshot',
3176
- 'Capture a screenshot from the active Bun.WebView automation session',
3177
- [
3178
- 'pmx-canvas webview screenshot --output ./canvas.png',
3179
- 'pmx-canvas webview screenshot --output ./canvas.webp --format webp --quality 80',
3180
- ],
3181
- async (args) => {
3182
- const { flags } = parseFlags(args);
3183
- if (flags.help || flags.h) return showCommandHelp('webview screenshot');
3184
-
3185
- const outputPath = requireFlag(flags, 'output', 'pmx-canvas webview screenshot --output ./canvas.png');
3186
-
3187
- const body: Record<string, unknown> = {};
3188
- if (flags.format && flags.format !== true) {
3189
- const format = String(flags.format);
3190
- if (format !== 'png' && format !== 'jpeg' && format !== 'webp') {
3191
- die('Invalid value for --format', 'Use: --format png, jpeg, or webp');
3192
- }
3193
- body.format = format;
3194
- }
3195
-
3196
- if (flags.quality && flags.quality !== true) {
3197
- const quality = Number(flags.quality);
3198
- if (!Number.isFinite(quality)) {
3199
- die(`Invalid value for --quality: ${String(flags.quality)}`, 'Use a numeric quality, e.g. --quality 80');
3200
- }
3201
- body.quality = quality;
3202
- }
3203
-
3204
- const base = getBaseUrl();
3205
- const response = await fetch(`${base}/api/workbench/webview/screenshot`, {
3206
- method: 'POST',
3207
- headers: { 'Content-Type': 'application/json' },
3208
- body: JSON.stringify(body),
3209
- });
3210
-
3211
- if (!response.ok) {
3212
- const text = await response.text();
3213
- try {
3214
- const json = JSON.parse(text) as Record<string, unknown>;
3215
- die(
3216
- json.error ? String(json.error) : `HTTP ${response.status}`,
3217
- typeof json.hint === 'string' ? json.hint : undefined,
3218
- );
3219
- } catch {
3220
- die(`HTTP ${response.status}: ${text}`);
3221
- }
3222
- }
3223
-
3224
- const bytes = new Uint8Array(await response.arrayBuffer());
3225
- writeFileSync(outputPath, bytes);
3226
- output({
3227
- ok: true,
3228
- output: outputPath,
3229
- bytes: bytes.byteLength,
3230
- mimeType: response.headers.get('Content-Type') ?? 'application/octet-stream',
3231
- });
3232
- },
3233
- );
3234
-
3235
- cmd(
3236
- 'screenshot',
3237
- 'Capture a screenshot from the active Bun.WebView automation session',
3238
- [
3239
- 'pmx-canvas screenshot --output ./canvas.png',
3240
- 'pmx-canvas screenshot --output ./canvas.webp --format webp --quality 80',
3241
- ],
3242
- async (args) => {
3243
- if (args.includes('--help') || args.includes('-h')) return showCommandHelp('screenshot');
3244
- const screenshotCommand = COMMANDS['webview screenshot'];
3245
- if (!screenshotCommand) die('Internal error: webview screenshot command is unavailable.');
3246
- await screenshotCommand.run(args);
3247
- },
3248
- );
3249
-
3250
- // ── code-graph ───────────────────────────────────────────────
3251
- cmd('code-graph', 'Show auto-detected file dependency graph', ['pmx-canvas code-graph'], async (args) => {
3252
- const { flags } = parseFlags(args);
3253
- if (flags.help || flags.h) return showCommandHelp('code-graph');
3254
-
3255
- const result = await api('GET', '/api/canvas/code-graph');
3256
- output(result);
3257
- });
3258
-
3259
- // ── spatial ──────────────────────────────────────────────────
3260
- cmd('spatial', 'Spatial analysis: clusters, reading order, neighborhoods', ['pmx-canvas spatial'], async (args) => {
3261
- const { flags } = parseFlags(args);
3262
- if (flags.help || flags.h) return showCommandHelp('spatial');
3263
-
3264
- const result = await api('GET', '/api/canvas/spatial-context');
3265
- output(result);
3266
- });
3267
-
3268
- // ── watch ────────────────────────────────────────────────────
3269
- cmd(
3270
- 'watch',
3271
- 'Watch low-token semantic canvas changes over the existing SSE stream',
3272
- [
3273
- 'pmx-canvas watch',
3274
- 'pmx-canvas watch --json',
3275
- 'pmx-canvas watch --events context-pin,move-end',
3276
- 'pmx-canvas watch --json --events connect --max-events 1',
3277
- ],
3278
- async (args) => {
3279
- const { flags } = parseFlags(args);
3280
- if (flags.help || flags.h) return showCommandHelp('watch');
3281
-
3282
- if (flags.json && flags.compact) {
3283
- die('Use either --json or --compact, not both.');
3284
- }
3285
-
3286
- const filtersRaw = typeof flags.events === 'string' ? flags.events : undefined;
3287
- const requestedFilters = filtersRaw
3288
- ? Array.from(
3289
- new Set(
3290
- filtersRaw
3291
- .split(',')
3292
- .map((value) => value.trim())
3293
- .filter((value) => value.length > 0),
3294
- ),
3295
- )
3296
- : [];
3297
- const invalidFilter = requestedFilters.find(
3298
- (value) => !ALL_SEMANTIC_WATCH_EVENT_TYPES.includes(value as (typeof ALL_SEMANTIC_WATCH_EVENT_TYPES)[number]),
3299
- );
3300
- if (invalidFilter) {
3301
- die(
3302
- `Invalid value in --events: ${invalidFilter}`,
3303
- 'Use a comma-separated subset of: context-pin,move-end,group,connect,remove',
3304
- );
3305
- }
3306
- const filters = parseSemanticEventFilter(filtersRaw);
3307
- if (filtersRaw && filters.size === 0) {
3308
- die(
3309
- `Invalid value for --events: ${filtersRaw}`,
3310
- 'Use a comma-separated subset of: context-pin,move-end,group,connect,remove',
3311
- );
3312
- }
3313
-
3314
- const maxEvents = optionalNumberFlag(flags, 'max-events', 'Use a positive integer, e.g. --max-events 1');
3315
- const jsonMode = Boolean(flags.json);
3316
- const reducer = new SemanticWatchReducer();
3317
- const pinned = (await api('GET', '/api/canvas/pinned-context')) as { nodeIds?: string[] };
3318
- reducer.setInitialPins(Array.isArray(pinned.nodeIds) ? pinned.nodeIds : []);
3319
-
3320
- const base = getBaseUrl();
3321
- const controller = new AbortController();
3322
- let response: Response;
3323
- try {
3324
- response = await fetch(`${base}/api/workbench/events`, {
3325
- method: 'GET',
3326
- headers: { Accept: 'text/event-stream' },
3327
- signal: controller.signal,
3328
- });
3329
- } catch (error) {
3330
- die(
3331
- `Cannot connect to pmx-canvas event stream at ${base}: ${error instanceof Error ? error.message : String(error)}`,
3332
- 'Start the server first: pmx-canvas --no-open',
3333
- );
3334
- }
3335
-
3336
- if (!response.ok) {
3337
- const text = await response.text();
3338
- die(`Failed to open event stream: HTTP ${response.status}`, text);
3339
- }
3340
- if (!response.body) {
3341
- die('Workbench event stream did not return a readable body.');
3342
- }
3343
-
3344
- let emitted = 0;
3345
- try {
3346
- for await (const message of parseSseStream(response.body)) {
3347
- const semanticEvents = reducer.handleMessage(message).filter((event) => filters.has(event.type));
3348
-
3349
- for (const event of semanticEvents) {
3350
- console.log(jsonMode ? JSON.stringify(event) : formatCompactWatchEvent(event));
3351
- emitted++;
3352
- if (maxEvents !== undefined && emitted >= maxEvents) {
3353
- controller.abort();
3354
- return;
3355
- }
3356
- }
3357
- }
3358
- } catch (error) {
3359
- if (controller.signal.aborted) return;
3360
- die(
3361
- `Watch stream failed: ${error instanceof Error ? error.message : String(error)}`,
3362
- 'Ensure the server is still running and reachable.',
3363
- );
3364
- }
3365
- },
3366
- );
3367
-
3368
- // ── serve (delegates back to original CLI) ───────────────────
3369
- cmd(
3370
- 'serve',
3371
- 'Start the canvas server',
3372
- [
3373
- 'pmx-canvas serve',
3374
- 'pmx-canvas serve --port=8080 --no-open',
3375
- 'pmx-canvas serve --demo --theme=light',
3376
- 'pmx-canvas --no-open --webview-automation',
3377
- ],
3378
- async (_args) => {
3379
- console.log('Use: pmx-canvas [--port=PORT] [--demo] [--no-open] [--theme=THEME] [--webview-automation]');
3380
- },
3381
- );
3382
-
3383
- // ── Help ─────────────────────────────────────────────────────
3384
-
3385
- function showCommandHelp(name: string): void {
3386
- const cmd = COMMANDS[name];
3387
- if (!cmd) return;
3388
- console.log(`\npmx-canvas ${name} — ${cmd.help}\n`);
3389
- console.log('Examples:');
3390
- for (const ex of cmd.examples) {
3391
- console.log(` ${ex}`);
3392
- }
3393
- if (name === 'node add') {
3394
- console.log('\nSchema help:');
3395
- console.log(' pmx-canvas node add --help --type webpage');
3396
- console.log(' pmx-canvas node add --help --type html');
3397
- console.log(' pmx-canvas node add --help --type json-render --component Table');
3398
- console.log(' pmx-canvas node add --help --type graph');
3399
- console.log(' pmx-canvas html primitive schema --summary');
3400
- console.log(' pmx-canvas node add --help --type webpage --json');
3401
- console.log(' Use --strict-size to keep explicit width/height fixed and scroll overflowing content.');
3402
- console.log('\nHTML sidecar flags:');
3403
- console.log(' --summary <text> Explicit human/agent-readable summary');
3404
- console.log(' --agent-summary <text> Semantic summary for search, pinned context, and spatial context');
3405
- console.log(' --description <text> Optional longer semantic description');
3406
- console.log(' --presentation true Mark raw HTML as an explicit presentation deck');
3407
- console.log(' --slide-title <text> Add a presentation slide title sidecar');
3408
- console.log(' --embedded-node-id <id> Link represented/embedded canvas node ID');
3409
- }
3410
- if (name === 'html primitive add' || name === 'html primitive schema') {
3411
- console.log('\nPrimitive flags:');
3412
- console.log(' --kind <name> Run `pmx-canvas html primitive schema --summary` for the full catalog');
3413
- console.log(' --data-file <path> JSON object payload for the primitive');
3414
- console.log(' --data-json, --data <json> Inline JSON object payload');
3415
- console.log(' --stdin Read JSON object payload from stdin');
3416
- }
3417
- if (name === 'json-render') {
3418
- console.log('\nOptions:');
3419
- console.log(' --schema Show json-render catalog schema (default)');
3420
- console.log(' --summary Show compact component summaries');
3421
- console.log(' --component <name> Focus on one component');
3422
- console.log(' --field <name> Focus on one component prop');
3423
- console.log(' --example, --examples Print canonical component examples');
3424
- }
3425
- if (name === 'node add' || name === 'graph add' || name === 'validate spec') {
3426
- console.log('\nGraph flags:');
3427
- console.log(
3428
- ' Graph fields accept kebab-case CLI flags and camelCase schema names, e.g. --graph-type/--graphType and --x-key/--xKey',
3429
- );
3430
- console.log(
3431
- ' Use --node-height/--nodeHeight for canvas frame height; use --chart-height for chart content height. --height is kept as a frame-height alias for compatibility.',
3432
- );
3433
- console.log(' Pass --show-legend false to hide legends in compact node layouts.');
3434
- }
3435
- if (name === 'validate spec') {
3436
- console.log('\nHTML primitive flags:');
3437
- console.log(' --type html-primitive --kind <name> --data-file ./payload.json');
3438
- }
3439
- if (name === 'node schema') {
3440
- console.log('\nFilters:');
3441
- console.log(' --summary Show compact schema summaries');
3442
- console.log(' --field <name> Focus on one node field');
3443
- console.log(' --component <name> Focus on one json-render component');
3444
- }
3445
- if (name === 'validate spec') {
3446
- console.log('\nOutput control:');
3447
- console.log(' --summary Return only validation summary metadata');
3448
- console.log(' For --type html-primitive, pass --kind plus optional --data-file/--data-json.');
3449
- }
3450
- if (name === 'snapshot list') {
3451
- console.log('\nOptions:');
3452
- console.log(' --limit <number> Maximum snapshots to return (default 20)');
3453
- console.log(' --query <text> Case-insensitive ID/name filter');
3454
- console.log(' --before <timestamp> Only return snapshots created at or before this ISO timestamp');
3455
- console.log(' --after <timestamp> Only return snapshots created at or after this ISO timestamp');
3456
- console.log(' --all Return all snapshots');
3457
- }
3458
- if (name === 'node update') {
3459
- console.log('\nTrace fields:');
3460
- console.log(' --tool-name, --toolName Trace tool or operation label');
3461
- console.log(' --category <name> Trace category, e.g. mcp, file, subagent, other');
3462
- console.log(' --status <status> Trace status, e.g. running, success, failed');
3463
- console.log(' --duration <text> Trace duration badge text');
3464
- console.log(' --result-summary, --resultSummary <text>');
3465
- console.log(' Trace result summary');
3466
- console.log(' --error <text> Trace error message');
3467
- }
3468
- if (name === 'snapshot gc') {
3469
- console.log('\nOptions:');
3470
- console.log(' --keep <number> Number of newest snapshots to keep (default 20)');
3471
- console.log(' --dry-run Preview deletions without removing files');
3472
- console.log(' --yes Confirm deletion');
3473
- }
3474
- if (name === 'web-artifact build') {
3475
- console.log('\nDependencies:');
3476
- console.log(' --deps <list> Add npm dependencies before bundling, e.g. --deps recharts,zod');
3477
- console.log('\nOutput control:');
3478
- console.log(' --include-logs Include raw build stdout/stderr in the response');
3479
- console.log(' --verbose Alias for --include-logs');
3480
- console.log(' --timeout-ms <number> Optional init/install/build timeout in milliseconds');
3481
- }
3482
- if (name === 'focus') {
3483
- console.log('\nViewport:');
3484
- console.log(' --no-pan Select/raise the node without moving the viewport');
3485
- }
3486
- if (name === 'fit') {
3487
- console.log('\nViewport:');
3488
- console.log(' --width <px> Viewport width used for fit math (default 1440)');
3489
- console.log(' --height <px> Viewport height used for fit math (default 900)');
3490
- console.log(' --padding <px> World-space padding around fitted nodes (default 60)');
3491
- console.log(' --max-scale <scale> Maximum zoom scale (default 1)');
3492
- }
3493
- if (name === 'screenshot' || name === 'webview screenshot') {
3494
- console.log('\nOptions:');
3495
- console.log(' --output <path> Required output image path');
3496
- console.log(' --format <type> png, jpeg, or webp');
3497
- console.log(' --quality <number> Encoder quality for lossy formats');
3498
- console.log(' Requires an active automation session: pmx-canvas webview start');
3499
- }
3500
- if (name === 'external-app add') {
3501
- console.log('\nOptions:');
3502
- console.log(' --kind excalidraw External app kind to create');
3503
- console.log(' --title <title> Node title');
3504
- console.log(' --node-id <id> Existing Excalidraw app node to update in place');
3505
- console.log(' --elements <json> Optional Excalidraw elements array JSON');
3506
- console.log(' --elements-json <json> Optional Excalidraw elements array JSON');
3507
- console.log(' --elements-file <path> Optional file containing Excalidraw elements JSON');
3508
- console.log(' --initial-file <path> Alias for --elements-file');
3509
- console.log(' --timeout-ms <number> Optional downstream MCP timeout for cold starts');
3510
- }
3511
- if (name === 'diagram add') {
3512
- console.log('\nAlias:');
3513
- console.log(' Equivalent to: pmx-canvas external-app add --kind excalidraw ...');
3514
- }
3515
- console.log('');
3516
- }
15
+ COMMANDS,
16
+ RESOURCE_COMMAND_ALIASES,
17
+ RESOURCE_SUBCOMMAND_HINTS,
18
+ die,
19
+ extractGlobalTargetFlags,
20
+ } from './shared.js';
21
+ // Importing a command module registers its commands into COMMANDS (side effect).
22
+ // Command implementations live in src/cli/commands/, shared helpers in shared.ts.
23
+ import './commands/nodes.js';
24
+ import './commands/edges.js';
25
+ import './commands/groups.js';
26
+ import './commands/view.js';
27
+ import './commands/query.js';
28
+ import './commands/pins.js';
29
+ import './commands/history.js';
30
+ import './commands/ax.js';
31
+ import './commands/webview.js';
32
+ import './commands/apps.js';
33
+ import './commands/copilot.js';
34
+
35
+ // Re-exported for existing importers (tests import it from this module).
36
+ export { extractGlobalTargetFlags };
3517
37
 
3518
38
  function showTopLevelHelp(): void {
3519
39
  console.log(`
@@ -3646,16 +166,6 @@ Examples:
3646
166
  `);
3647
167
  }
3648
168
 
3649
- // ── Stdin reader ─────────────────────────────────────────────
3650
-
3651
- async function readStdin(): Promise<string> {
3652
- const chunks: Buffer[] = [];
3653
- for await (const chunk of process.stdin) {
3654
- chunks.push(chunk);
3655
- }
3656
- return Buffer.concat(chunks).toString('utf-8');
3657
- }
3658
-
3659
169
  // ── Router ───────────────────────────────────────────────────
3660
170
 
3661
171
  export async function runAgentCli(rawArgs: string[]): Promise<void> {