pmx-canvas 0.1.28 → 0.1.30

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 (67) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/Readme.md +20 -10
  3. package/dist/canvas/global.css +13 -0
  4. package/dist/canvas/index.js +80 -163
  5. package/dist/canvas/surface-theme.css +142 -0
  6. package/dist/json-render/index.js +103 -103
  7. package/dist/types/client/nodes/HtmlNode.d.ts +0 -7
  8. package/dist/types/client/nodes/ax-node-actions.d.ts +18 -0
  9. package/dist/types/client/nodes/surface-url.d.ts +22 -0
  10. package/dist/types/client/state/attention-bridge.d.ts +3 -0
  11. package/dist/types/client/state/intent-bridge.d.ts +17 -0
  12. package/dist/types/json-render/renderer/index.d.ts +2 -0
  13. package/dist/types/json-render/schema.d.ts +2 -0
  14. package/dist/types/json-render/server.d.ts +2 -0
  15. package/dist/types/mcp/canvas-access.d.ts +47 -0
  16. package/dist/types/server/ax-interaction.d.ts +210 -0
  17. package/dist/types/server/ax-state.d.ts +67 -1
  18. package/dist/types/server/canvas-db.d.ts +4 -0
  19. package/dist/types/server/canvas-serialization.d.ts +2 -0
  20. package/dist/types/server/canvas-state.d.ts +47 -2
  21. package/dist/types/server/html-surface.d.ts +40 -0
  22. package/dist/types/server/index.d.ts +56 -2
  23. package/dist/types/server/mutation-history.d.ts +1 -1
  24. package/dist/types/server/placement.d.ts +1 -1
  25. package/dist/types/shared/surface.d.ts +19 -0
  26. package/docs/cli.md +30 -0
  27. package/docs/http-api.md +55 -0
  28. package/docs/mcp.md +40 -2
  29. package/docs/node-types.md +26 -0
  30. package/docs/plans/plan-004-pmx-ax-primitives.md +623 -394
  31. package/docs/sdk.md +20 -0
  32. package/package.json +2 -2
  33. package/skills/pmx-canvas/SKILL.md +107 -9
  34. package/src/cli/agent.ts +190 -0
  35. package/src/client/canvas/CanvasNode.tsx +8 -4
  36. package/src/client/canvas/ExpandedNodeOverlay.tsx +12 -0
  37. package/src/client/nodes/ContextNode.tsx +17 -0
  38. package/src/client/nodes/ExtAppFrame.tsx +40 -3
  39. package/src/client/nodes/FileNode.tsx +26 -0
  40. package/src/client/nodes/HtmlNode.tsx +60 -188
  41. package/src/client/nodes/LedgerNode.tsx +39 -5
  42. package/src/client/nodes/McpAppNode.tsx +47 -2
  43. package/src/client/nodes/StatusNode.tsx +20 -0
  44. package/src/client/nodes/ax-node-actions.ts +39 -0
  45. package/src/client/nodes/surface-url.ts +48 -0
  46. package/src/client/state/attention-bridge.ts +5 -0
  47. package/src/client/state/intent-bridge.ts +33 -0
  48. package/src/client/theme/global.css +13 -0
  49. package/src/client/theme/surface-theme.css +142 -0
  50. package/src/json-render/renderer/index.tsx +31 -0
  51. package/src/json-render/schema.ts +4 -0
  52. package/src/json-render/server.ts +31 -1
  53. package/src/mcp/canvas-access.ts +212 -1
  54. package/src/mcp/server.ts +238 -5
  55. package/src/server/ax-context.ts +3 -0
  56. package/src/server/ax-interaction.ts +549 -0
  57. package/src/server/ax-state.ts +188 -2
  58. package/src/server/canvas-db.ts +20 -0
  59. package/src/server/canvas-operations.ts +11 -0
  60. package/src/server/canvas-serialization.ts +9 -0
  61. package/src/server/canvas-state.ts +177 -16
  62. package/src/server/html-surface.ts +170 -0
  63. package/src/server/index.ts +105 -1
  64. package/src/server/mutation-history.ts +5 -0
  65. package/src/server/placement.ts +5 -1
  66. package/src/server/server.ts +305 -0
  67. package/src/shared/surface.ts +38 -0
package/src/mcp/server.ts CHANGED
@@ -25,6 +25,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
25
25
  import { isAbsolute, relative, resolve } from 'node:path';
26
26
  import { z } from 'zod';
27
27
  import { canvasState, describeCanvasSchema, validateStructuredCanvasPayload } from '../server/index.js';
28
+ import { AX_INTERACTION_TYPES } from '../server/ax-interaction.js';
28
29
  import { isHtmlPrimitiveKind } from '../server/html-primitives.js';
29
30
  import type { HtmlPrimitiveKind } from '../server/html-primitives.js';
30
31
  import { createCanvasAccess, refreshCanvasAccess, type CanvasAccess } from './canvas-access.js';
@@ -116,6 +117,8 @@ function sendCanvasResourceNotifications(type: 'nodes' | 'pins' | 'ax' | 'ax-tim
116
117
  if (type === 'ax-timeline') {
117
118
  server.server.sendResourceUpdated({ uri: 'canvas://ax-timeline' });
118
119
  server.server.sendResourceUpdated({ uri: 'canvas://ax-context' });
120
+ server.server.sendResourceUpdated({ uri: 'canvas://ax-pending-steering' });
121
+ server.server.sendResourceUpdated({ uri: 'canvas://ax-delivery' });
119
122
  }
120
123
  server.server.sendResourceUpdated({ uri: 'canvas://layout' });
121
124
  server.server.sendResourceUpdated({ uri: 'canvas://summary' });
@@ -263,13 +266,16 @@ function compactBatchResult(result: { ok: boolean; results: Array<Record<string,
263
266
  }
264
267
 
265
268
  async function createdNodePayload(c: CanvasAccess, id: string, options: { full?: boolean; verbose?: boolean; includeData?: boolean } = {}): Promise<Record<string, unknown>> {
269
+ // Expose both `id` and a `nodeId` alias on every node-create response so
270
+ // agents using either key (or a cached schema) work — matching the
271
+ // external-app / web-artifact responses that already return both.
266
272
  const node = await c.getNode(id);
267
- if (!node) return { ok: true, id };
273
+ if (!node) return { ok: true, id, nodeId: id };
268
274
  if (!wantsFullPayload(options)) {
269
- return { ok: true, node: compactNodePayload(node), id };
275
+ return { ok: true, node: compactNodePayload(node), id, nodeId: id };
270
276
  }
271
277
  const serialized = serializeCanvasNodeForAgent(node);
272
- return { ok: true, node: serialized, ...serialized };
278
+ return { ok: true, node: serialized, ...serialized, nodeId: node.id };
273
279
  }
274
280
 
275
281
  function buildSummaryFromLayout(layout: Awaited<ReturnType<CanvasAccess['getLayout']>>, pinnedIds: string[]): Record<string, unknown> {
@@ -1629,6 +1635,171 @@ export async function startMcpServer(): Promise<void> {
1629
1635
  },
1630
1636
  );
1631
1637
 
1638
+ server.tool(
1639
+ 'canvas_ax_interaction',
1640
+ 'Submit a node-originated AX interaction: a capability-gated, validated event from an eligible node that maps onto an AX operation (work item, evidence, approval, review, focus, steering, event). Returns { ok: false, code } if the node type/metadata does not allow the interaction type or the payload is invalid.',
1641
+ {
1642
+ type: z.enum(AX_INTERACTION_TYPES).describe('Interaction type, e.g. ax.work.create, ax.evidence.add, ax.focus.set.'),
1643
+ sourceNodeId: z.string().describe('The node emitting the interaction.'),
1644
+ payload: z.record(z.string(), z.unknown()).optional().describe('Type-specific payload, e.g. {"title":"..."} for ax.work.create.'),
1645
+ sourceSurface: z.enum(['native-node', 'json-render', 'html-node', 'mcp-app', 'adapter']).optional(),
1646
+ correlationId: z.string().optional(),
1647
+ source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
1648
+ .optional()
1649
+ .describe('Optional host/source label. Defaults to mcp.'),
1650
+ },
1651
+ async ({ type, sourceNodeId, payload, sourceSurface, correlationId, source }) => {
1652
+ const c = await ensureCanvas();
1653
+ const result = await c.submitAxInteraction(
1654
+ {
1655
+ type,
1656
+ sourceNodeId,
1657
+ ...(payload ? { payload } : {}),
1658
+ ...(sourceSurface ? { sourceSurface } : {}),
1659
+ ...(correlationId ? { correlationId } : {}),
1660
+ },
1661
+ { source: source ?? 'mcp' },
1662
+ );
1663
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
1664
+ },
1665
+ );
1666
+
1667
+ server.tool(
1668
+ 'canvas_claim_ax_delivery',
1669
+ 'Claim pending PMX AX steering messages for a consumer (adapterless delivery). Returns undelivered steering, excluding messages the consumer itself originated (loop prevention). After acting on a message, call canvas_mark_ax_delivery.',
1670
+ {
1671
+ consumer: z.string().optional().describe('Consumer/source label to exclude from results (e.g. copilot, mcp).'),
1672
+ limit: z.number().optional().describe('Max messages to return.'),
1673
+ },
1674
+ async ({ consumer, limit }) => {
1675
+ const c = await ensureCanvas();
1676
+ const pending = await c.getPendingSteering({
1677
+ ...(consumer ? { consumer } : {}),
1678
+ ...(typeof limit === 'number' ? { limit } : {}),
1679
+ });
1680
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: true, pending }) }] };
1681
+ },
1682
+ );
1683
+
1684
+ server.tool(
1685
+ 'canvas_mark_ax_delivery',
1686
+ 'Mark a PMX AX steering message as delivered so it is not handed out again.',
1687
+ {
1688
+ id: z.string().describe('The steering message id to mark delivered.'),
1689
+ },
1690
+ async ({ id }) => {
1691
+ const c = await ensureCanvas();
1692
+ const delivered = await c.markSteeringDelivered(id);
1693
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: true, delivered }) }] };
1694
+ },
1695
+ );
1696
+
1697
+ server.tool(
1698
+ 'canvas_request_elicitation',
1699
+ 'Request structured human input (an elicitation): a pending question/form tied to nodes. Canvas-bound and snapshotted; exposed via canvas://ax-work. Answer it with canvas_respond_elicitation.',
1700
+ {
1701
+ prompt: z.string().describe('The question or instruction for the human.'),
1702
+ fields: z.array(z.string()).optional().describe('Optional field names to request (a simple structured form).'),
1703
+ nodeIds: z.array(z.string()).optional(),
1704
+ source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system']).optional(),
1705
+ },
1706
+ async ({ prompt, fields, nodeIds, source }) => {
1707
+ const c = await ensureCanvas();
1708
+ const elicitation = await c.requestElicitation(
1709
+ { prompt, ...(fields ? { fields } : {}), ...(Array.isArray(nodeIds) ? { nodeIds } : {}) },
1710
+ { source: source ?? 'mcp' },
1711
+ );
1712
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: true, elicitation }) }] };
1713
+ },
1714
+ );
1715
+
1716
+ server.tool(
1717
+ 'canvas_respond_elicitation',
1718
+ 'Answer a pending elicitation with a structured response.',
1719
+ {
1720
+ id: z.string().describe('The elicitation id.'),
1721
+ response: z.record(z.string(), z.unknown()).describe('The structured answer.'),
1722
+ source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system']).optional(),
1723
+ },
1724
+ async ({ id, response, source }) => {
1725
+ const c = await ensureCanvas();
1726
+ const elicitation = await c.respondElicitation(id, response, { source: source ?? 'mcp' });
1727
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: Boolean(elicitation), elicitation }) }] };
1728
+ },
1729
+ );
1730
+
1731
+ server.tool(
1732
+ 'canvas_request_mode',
1733
+ 'Request a workflow mode transition (plan/execute/autonomous): a pending mode request tied to nodes. Canvas-bound and snapshotted; exposed via canvas://ax-work. Resolve with canvas_resolve_mode.',
1734
+ {
1735
+ mode: z.enum(['plan', 'execute', 'autonomous']).describe('Requested target mode.'),
1736
+ reason: z.string().optional(),
1737
+ nodeIds: z.array(z.string()).optional(),
1738
+ source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system']).optional(),
1739
+ },
1740
+ async ({ mode, reason, nodeIds, source }) => {
1741
+ const c = await ensureCanvas();
1742
+ const modeRequest = await c.requestMode(
1743
+ { mode, ...(typeof reason === 'string' ? { reason } : {}), ...(Array.isArray(nodeIds) ? { nodeIds } : {}) },
1744
+ { source: source ?? 'mcp' },
1745
+ );
1746
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: true, modeRequest }) }] };
1747
+ },
1748
+ );
1749
+
1750
+ server.tool(
1751
+ 'canvas_resolve_mode',
1752
+ 'Resolve a pending mode request (approved or rejected).',
1753
+ {
1754
+ id: z.string(),
1755
+ decision: z.enum(['approved', 'rejected']),
1756
+ resolution: z.string().optional(),
1757
+ source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system']).optional(),
1758
+ },
1759
+ async ({ id, decision, resolution, source }) => {
1760
+ const c = await ensureCanvas();
1761
+ const modeRequest = await c.resolveModeRequest(id, decision, {
1762
+ ...(typeof resolution === 'string' ? { resolution } : {}),
1763
+ source: source ?? 'mcp',
1764
+ });
1765
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: Boolean(modeRequest), modeRequest }) }] };
1766
+ },
1767
+ );
1768
+
1769
+ server.tool(
1770
+ 'canvas_invoke_command',
1771
+ 'Invoke a registry-gated PMX command intent (pmx.plan | pmx.execute | pmx.promote-context | pmx.summarize | pmx.review). Records a timeline event a host/agent can observe — NOT arbitrary execution; unknown names are rejected.',
1772
+ {
1773
+ name: z.string().describe('A command name from the PMX command registry.'),
1774
+ args: z.record(z.string(), z.unknown()).optional(),
1775
+ source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system']).optional(),
1776
+ },
1777
+ async ({ name, args, source }) => {
1778
+ const c = await ensureCanvas();
1779
+ const event = await c.invokeCommand(name, args ?? null, { source: source ?? 'mcp' });
1780
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: Boolean(event), event }) }] };
1781
+ },
1782
+ );
1783
+
1784
+ server.tool(
1785
+ 'canvas_set_ax_policy',
1786
+ 'Set the declarative AX policy (allowed/excluded/approval-required tools; prompt mode/append). PMX stores it and exposes it via canvas://ax-context; host adapters READ and enforce it. Merges with the existing policy.',
1787
+ {
1788
+ tools: z.object({
1789
+ allowed: z.array(z.string()).optional(),
1790
+ excluded: z.array(z.string()).optional(),
1791
+ approvalRequired: z.array(z.string()).optional(),
1792
+ }).optional(),
1793
+ prompt: z.object({ systemAppend: z.string().optional(), mode: z.string().optional() }).optional(),
1794
+ source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system']).optional(),
1795
+ },
1796
+ async ({ tools, prompt, source }) => {
1797
+ const c = await ensureCanvas();
1798
+ const policy = await c.setPolicy({ ...(tools ? { tools } : {}), ...(prompt ? { prompt } : {}) }, { source: source ?? 'mcp' });
1799
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: true, policy }) }] };
1800
+ },
1801
+ );
1802
+
1632
1803
  server.tool(
1633
1804
  'canvas_fit_view',
1634
1805
  'Fit the canvas viewport to all nodes or a selected subset. Useful before screenshots and whole-board review.',
@@ -2092,7 +2263,69 @@ export async function startMcpServer(): Promise<void> {
2092
2263
  {
2093
2264
  uri: 'canvas://ax-work',
2094
2265
  mimeType: 'application/json',
2095
- text: JSON.stringify({ workItems, approvalGates, reviewAnnotations: state.reviewAnnotations }, null, 2),
2266
+ text: JSON.stringify({
2267
+ workItems,
2268
+ approvalGates,
2269
+ reviewAnnotations: state.reviewAnnotations,
2270
+ elicitations: state.elicitations,
2271
+ modeRequests: state.modeRequests,
2272
+ policy: state.policy,
2273
+ }, null, 2),
2274
+ },
2275
+ ],
2276
+ };
2277
+ },
2278
+ );
2279
+
2280
+ server.resource(
2281
+ 'ax-pending-steering',
2282
+ 'canvas://ax-pending-steering',
2283
+ {
2284
+ description:
2285
+ 'Undelivered PMX AX steering messages an MCP client can claim and act on without a host-native adapter. After delivering an instruction to your agent, mark it via canvas_mark_ax_delivery so it is not handed out again.',
2286
+ mimeType: 'application/json',
2287
+ },
2288
+ async () => {
2289
+ const c = await ensureCanvas();
2290
+ const pending = await c.getPendingSteering();
2291
+ return {
2292
+ contents: [
2293
+ { uri: 'canvas://ax-pending-steering', mimeType: 'application/json', text: JSON.stringify({ pending }, null, 2) },
2294
+ ],
2295
+ };
2296
+ },
2297
+ );
2298
+
2299
+ server.resource(
2300
+ 'ax-delivery',
2301
+ 'canvas://ax-delivery',
2302
+ {
2303
+ description:
2304
+ 'PMX AX steering delivery state: recent steering messages with their delivered flag, for delivery diagnostics.',
2305
+ mimeType: 'application/json',
2306
+ },
2307
+ async () => {
2308
+ const c = await ensureCanvas();
2309
+ const timeline = await c.getAxTimeline();
2310
+ return {
2311
+ contents: [
2312
+ { uri: 'canvas://ax-delivery', mimeType: 'application/json', text: JSON.stringify({ steering: timeline.steering }, null, 2) },
2313
+ ],
2314
+ };
2315
+ },
2316
+ );
2317
+
2318
+ server.prompt(
2319
+ 'pmx-current-context',
2320
+ 'Inject the current PMX Canvas AX context (pins, focus, work items, approvals, review, timeline) so an MCP-aware client can ground its next action without a host-native adapter.',
2321
+ async () => {
2322
+ const c = await ensureCanvas();
2323
+ const context = await c.getAxContext();
2324
+ return {
2325
+ messages: [
2326
+ {
2327
+ role: 'user',
2328
+ content: { type: 'text', text: `Current PMX Canvas context:\n\n${JSON.stringify(context, null, 2)}` },
2096
2329
  },
2097
2330
  ],
2098
2331
  };
@@ -2332,7 +2565,7 @@ export async function startMcpServer(): Promise<void> {
2332
2565
 
2333
2566
  server.tool(
2334
2567
  'canvas_batch',
2335
- 'Run a non-atomic batch of canvas operations with optional assigned references. Use assign to name a result, then reference it later as "$name" for the created node id or "$name.id" for a specific result field. On failure, earlier successful operations remain applied and the response includes ok:false, failedIndex, error, results, and refs. Supports node.add, node.update, graph.add, edge.add, group.create, group.add, group.remove, pin.set/add/remove, snapshot.save, and arrange.',
2568
+ 'Run a non-atomic batch of canvas operations with optional assigned references. Use assign to name a result, then reference it later as "$name" for the created node id or "$name.id" for a specific result field. On failure, earlier successful operations remain applied and the response includes ok:false, failedIndex, error, results, and refs. Supports node.add, node.update, node.remove, graph.add, edge.add, group.create, group.add, group.remove, pin.set/add/remove, snapshot.save, and arrange.',
2336
2569
  {
2337
2570
  operations: z.array(z.object({
2338
2571
  op: z.string().describe('Operation name, e.g. "node.add" or "edge.add"'),
@@ -37,6 +37,9 @@ export function buildCanvasAxContext(): PmxAxContext {
37
37
  workItems: ax.workItems,
38
38
  approvalGates: ax.approvalGates,
39
39
  reviewAnnotations: ax.reviewAnnotations,
40
+ elicitations: ax.elicitations,
41
+ modeRequests: ax.modeRequests,
42
+ policy: ax.policy,
40
43
  timeline: canvasState.getAxTimelineSummary(),
41
44
  host: canvasState.getHostCapability(),
42
45
  });