pmx-canvas 0.8.2 → 0.9.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.
- package/.github/extensions/pmx-canvas/runtime.json +1 -1
- package/.github/extensions/pmx-canvas/tools.json +601 -22
- package/CHANGELOG.md +49 -0
- package/Readme.md +131 -532
- package/dist/canvas/global.css +2724 -476
- package/dist/canvas/index.js +168 -107
- package/dist/canvas/mcp-app.html +2899 -596
- package/dist/json-render/index.js +20 -20
- package/dist/types/client/canvas/BarHint.d.ts +12 -2
- package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -1
- package/dist/types/client/canvas/BoardPinButton.d.ts +10 -0
- package/dist/types/client/canvas/BoardSwitcher.d.ts +1 -1
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandBar.d.ts +3 -4
- package/dist/types/client/canvas/ConnectionBanner.d.ts +2 -1
- package/dist/types/client/canvas/ContextChip.d.ts +3 -0
- package/dist/types/client/canvas/CrossBoardReferences.d.ts +5 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +14 -3
- package/dist/types/client/canvas/EmptyState.d.ts +4 -4
- package/dist/types/client/canvas/HomeGraphView.d.ts +2 -0
- package/dist/types/client/canvas/HomeMapView.d.ts +1 -0
- package/dist/types/client/canvas/HomeReviewView.d.ts +6 -0
- package/dist/types/client/canvas/HomeView.d.ts +9 -7
- package/dist/types/client/canvas/KnowledgeDialog.d.ts +4 -0
- package/dist/types/client/canvas/LifecycleChrome.d.ts +5 -0
- package/dist/types/client/canvas/LifecycleFields.d.ts +7 -0
- package/dist/types/client/canvas/LifecyclePanel.d.ts +1 -0
- package/dist/types/client/canvas/MemoryInspector.d.ts +7 -0
- package/dist/types/client/canvas/NearPinLayer.d.ts +14 -0
- package/dist/types/client/canvas/NodeContextMark.d.ts +40 -7
- package/dist/types/client/canvas/RelationPresetsDialog.d.ts +4 -0
- package/dist/types/client/canvas/RelationsPanel.d.ts +4 -0
- package/dist/types/client/canvas/SessionPanel.d.ts +2 -0
- package/dist/types/client/canvas/canvas-area.d.ts +1 -1
- package/dist/types/client/canvas/graph-geometry.d.ts +27 -0
- package/dist/types/client/canvas/graph-model.d.ts +37 -0
- package/dist/types/client/canvas/memory-layout.d.ts +23 -0
- package/dist/types/client/canvas/use-chrome-height.d.ts +2 -0
- package/dist/types/client/canvas/use-focus-trap.d.ts +1 -0
- package/dist/types/client/canvas/use-memory-graph.d.ts +8 -0
- package/dist/types/client/canvas/use-pan-zoom.d.ts +4 -1
- package/dist/types/client/icons.d.ts +0 -10
- package/dist/types/client/nodes/AxStepControls.d.ts +1 -1
- package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +4 -4
- package/dist/types/client/nodes/MarkdownNode.d.ts +5 -1
- package/dist/types/client/nodes/McpAppNode.d.ts +4 -4
- package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -1
- package/dist/types/client/state/boards-store.d.ts +81 -9
- package/dist/types/client/state/canvas-store.d.ts +14 -1
- package/dist/types/client/state/context-chip-store.d.ts +22 -0
- package/dist/types/client/state/context-status-store.d.ts +35 -0
- package/dist/types/client/state/intent-bridge.d.ts +11 -8
- package/dist/types/client/state/knowledge-store.d.ts +22 -0
- package/dist/types/client/state/near-pin-store.d.ts +44 -1
- package/dist/types/client/state/presence-store.d.ts +4 -4
- package/dist/types/client/state/presentation.d.ts +28 -0
- package/dist/types/client/state/relation-store.d.ts +10 -0
- package/dist/types/client/state/session-store.d.ts +41 -2
- package/dist/types/client/state/workspace-settings.d.ts +7 -0
- package/dist/types/client/types.d.ts +13 -1
- package/dist/types/client/utils/keyboard.d.ts +2 -0
- package/dist/types/client/utils/time-ago.d.ts +4 -0
- package/dist/types/mcp/context-uri-template.d.ts +3 -1
- package/dist/types/server/agent-presence.d.ts +10 -3
- package/dist/types/server/ax-context.d.ts +1 -1
- package/dist/types/server/canvas-db.d.ts +30 -7
- package/dist/types/server/canvas-operations.d.ts +24 -2
- package/dist/types/server/canvas-provenance.d.ts +22 -2
- package/dist/types/server/canvas-serialization.d.ts +2 -0
- package/dist/types/server/canvas-state.d.ts +93 -15
- package/dist/types/server/canvas-validation.d.ts +1 -1
- package/dist/types/server/context-brief.d.ts +94 -6
- package/dist/types/server/context-reads.d.ts +33 -7
- package/dist/types/server/edit-summary.d.ts +20 -0
- package/dist/types/server/index.d.ts +71 -10
- package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
- package/dist/types/server/lifecycle-status.d.ts +10 -0
- package/dist/types/server/memory-graph.d.ts +9 -0
- package/dist/types/server/mutation-history.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
- package/dist/types/server/operations/ops/boards.d.ts +2 -1
- package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
- package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
- package/dist/types/server/operations/ops/nodes.d.ts +5 -0
- package/dist/types/server/operations/ops/query.d.ts +23 -0
- package/dist/types/server/operations/registry.d.ts +1 -0
- package/dist/types/server/placement.d.ts +7 -0
- package/dist/types/server/scope-fence.d.ts +2 -1
- package/dist/types/server/spatial-analysis.d.ts +6 -3
- package/dist/types/server/tour-control.d.ts +32 -0
- package/dist/types/server/wiki-links.d.ts +14 -0
- package/dist/types/server/workbench-auth.d.ts +2 -0
- package/dist/types/shared/agent-context.d.ts +2 -0
- package/dist/types/shared/agent-presence.d.ts +15 -12
- package/dist/types/shared/auto-arrange.d.ts +1 -0
- package/dist/types/shared/boards.d.ts +4 -4
- package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
- package/dist/types/shared/knowledge-presets.d.ts +6 -0
- package/dist/types/shared/lifecycle-status.d.ts +24 -0
- package/dist/types/shared/memory-graph.d.ts +53 -0
- package/dist/types/shared/relations.d.ts +68 -0
- package/dist/types/shared/semantic-attention.d.ts +2 -0
- package/dist/types/shared/tour.d.ts +38 -0
- package/dist/types/shared/workspace-settings.d.ts +11 -0
- package/docs/RELEASE.md +21 -3
- package/docs/adr-001-bun-only-runtime.md +7 -1
- package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
- package/docs/api-reference.md +109 -0
- package/docs/ax-host-adapter-contract.md +1 -1
- package/docs/ax-state-contract.md +5 -4
- package/docs/cli.md +153 -15
- package/docs/demos/pmx-canvas-demo.mp4 +0 -0
- package/docs/design/AgentContext.dc.html +3 -3
- package/docs/design/BoardPins.dc.html +37 -22
- package/docs/design/Categories.dc.html +208 -0
- package/docs/design/CategoryEditor.dc.html +33 -0
- package/docs/design/Chrome.dc.html +52 -9
- package/docs/design/ContextChip.dc.html +224 -0
- package/docs/design/ContextChip600.dc.html +136 -0
- package/docs/design/EmptyState.dc.html +45 -0
- package/docs/design/Flows.dc.html +23 -1
- package/docs/design/HeaderMarks.dc.html +183 -0
- package/docs/design/Home.dc.html +19 -1
- package/docs/design/HomeGraph.dc.html +86 -202
- package/docs/design/HomeMap.dc.html +23 -1
- package/docs/design/HomeTeam.dc.html +32 -14
- package/docs/design/Lifecycle.dc.html +22 -0
- package/docs/design/Lifecycle600.dc.html +18 -0
- package/docs/design/LifecycleVariants.dc.html +157 -0
- package/docs/design/LinkAuthorship.dc.html +139 -0
- package/docs/design/LinksChanged.dc.html +203 -0
- package/docs/design/LinksOptions.dc.html +137 -0
- package/docs/design/LinksOptions600.dc.html +134 -0
- package/docs/design/NearPin.dc.html +9 -10
- package/docs/design/Pane600.dc.html +4 -3
- package/docs/design/Relations.dc.html +3 -2
- package/docs/design/Restore.dc.html +152 -0
- package/docs/design/SeeChange.dc.html +267 -0
- package/docs/design/Uses.dc.html +12 -6
- package/docs/design/canvas.json +146 -19
- package/docs/design.md +449 -16
- package/docs/environment.md +0 -1
- package/docs/evals/curation-effect.md +128 -0
- package/docs/http-api.md +459 -37
- package/docs/mcp.md +146 -18
- package/docs/node-types.md +58 -3
- package/docs/plans/design-gaps-2026-10-08.md +53 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
- package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
- package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
- package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
- package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
- package/docs/product-context-vision-2026-09.md +17 -1
- package/docs/product-vision-2026-09.md +1082 -43
- package/docs/product-vision-review-2026-09.md +13 -1
- package/docs/screenshots/workbench-live-dark.png +0 -0
- package/docs/screenshots/workbench-live-light.png +0 -0
- package/docs/sdk.md +62 -2
- package/package.json +1 -1
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/plugin.json +1 -1
- package/plugins/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/tools.json +601 -22
- package/skills/pmx-canvas/SKILL.md +216 -42
- package/skills/pmx-canvas/references/full-reference.md +120 -21
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
- package/skills/pmx-canvas/references/importing-context.md +132 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
- package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
- package/skills/pmx-canvas-testing/SKILL.md +20 -0
- package/src/cli/agent.ts +2 -0
- package/src/cli/commands/ax.ts +12 -0
- package/src/cli/commands/boards.ts +87 -11
- package/src/cli/commands/edges.ts +28 -0
- package/src/cli/commands/groups.ts +7 -0
- package/src/cli/commands/nodes.ts +1 -0
- package/src/cli/commands/query.ts +34 -2
- package/src/cli/commands/record.ts +199 -48
- package/src/cli/index.ts +6 -1
- package/src/client/App.tsx +177 -70
- package/src/client/canvas/AttentionHistory.tsx +0 -2
- package/src/client/canvas/BarHint.tsx +40 -2
- package/src/client/canvas/BoardCopyDialog.tsx +1 -1
- package/src/client/canvas/BoardFolderDialog.tsx +7 -7
- package/src/client/canvas/BoardLinkDialog.tsx +138 -33
- package/src/client/canvas/BoardPinButton.tsx +33 -0
- package/src/client/canvas/BoardSwitcher.tsx +26 -40
- package/src/client/canvas/CanvasNode.tsx +360 -237
- package/src/client/canvas/CanvasViewport.tsx +45 -7
- package/src/client/canvas/CommandBar.tsx +29 -23
- package/src/client/canvas/CommandPalette.tsx +5 -12
- package/src/client/canvas/ConnectionBanner.tsx +3 -3
- package/src/client/canvas/ContextChip.tsx +296 -0
- package/src/client/canvas/ContextMenu.tsx +63 -5
- package/src/client/canvas/CrossBoardReferences.tsx +60 -0
- package/src/client/canvas/EdgeLayer.tsx +339 -76
- package/src/client/canvas/EmptyState.tsx +33 -18
- package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
- package/src/client/canvas/ExternalWriters.tsx +57 -42
- package/src/client/canvas/HomeGraphView.tsx +942 -0
- package/src/client/canvas/HomeMapView.tsx +641 -0
- package/src/client/canvas/HomeReviewView.tsx +167 -0
- package/src/client/canvas/HomeView.tsx +687 -160
- package/src/client/canvas/IntentLayer.tsx +19 -12
- package/src/client/canvas/KnowledgeDialog.tsx +488 -0
- package/src/client/canvas/LifecycleChrome.tsx +133 -0
- package/src/client/canvas/LifecycleFields.tsx +91 -0
- package/src/client/canvas/LifecyclePanel.tsx +533 -0
- package/src/client/canvas/MemoryInspector.tsx +104 -0
- package/src/client/canvas/Minimap.tsx +3 -3
- package/src/client/canvas/NearPinLayer.tsx +187 -0
- package/src/client/canvas/NodeContextMark.tsx +175 -97
- package/src/client/canvas/Presentation.tsx +23 -57
- package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
- package/src/client/canvas/RelationsPanel.tsx +82 -0
- package/src/client/canvas/SelectionBar.tsx +3 -6
- package/src/client/canvas/SessionPanel.tsx +90 -50
- package/src/client/canvas/SessionReceipt.tsx +311 -39
- package/src/client/canvas/ShortcutOverlay.tsx +1 -1
- package/src/client/canvas/SnapshotPanel.tsx +27 -36
- package/src/client/canvas/ToolRail.tsx +367 -192
- package/src/client/canvas/TopBar.tsx +354 -124
- package/src/client/canvas/canvas-area.ts +10 -3
- package/src/client/canvas/graph-geometry.ts +195 -0
- package/src/client/canvas/graph-model.ts +254 -0
- package/src/client/canvas/memory-layout.ts +106 -0
- package/src/client/canvas/use-chrome-height.ts +20 -0
- package/src/client/canvas/use-focus-trap.ts +31 -5
- package/src/client/canvas/use-memory-graph.ts +58 -0
- package/src/client/canvas/use-node-drag.ts +2 -1
- package/src/client/canvas/use-pan-zoom.ts +64 -7
- package/src/client/icons.tsx +0 -63
- package/src/client/mcp-app/style.css +0 -6
- package/src/client/nodes/AxStepControls.tsx +74 -50
- package/src/client/nodes/BoardNode.tsx +5 -1
- package/src/client/nodes/ContextNode.tsx +18 -11
- package/src/client/nodes/ExtAppFrame.tsx +0 -1
- package/src/client/nodes/FileNode.tsx +46 -42
- package/src/client/nodes/HtmlNode.tsx +32 -0
- package/src/client/nodes/ImageNode.tsx +1 -1
- package/src/client/nodes/InlineFormatBar.tsx +6 -9
- package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
- package/src/client/nodes/MarkdownNode.tsx +377 -109
- package/src/client/nodes/McpAppNode.tsx +50 -20
- package/src/client/nodes/MdFormatBar.tsx +21 -18
- package/src/client/nodes/PromptNode.tsx +1 -1
- package/src/client/nodes/ResponseNode.tsx +1 -1
- package/src/client/nodes/viewer-runtime-state.ts +54 -22
- package/src/client/state/attention-bridge.ts +64 -15
- package/src/client/state/boards-store.ts +193 -39
- package/src/client/state/canvas-store.ts +99 -12
- package/src/client/state/context-chip-store.ts +70 -0
- package/src/client/state/context-status-store.ts +53 -0
- package/src/client/state/intent-bridge.ts +51 -30
- package/src/client/state/knowledge-store.ts +43 -0
- package/src/client/state/near-pin-store.ts +115 -6
- package/src/client/state/presence-store.ts +7 -10
- package/src/client/state/presentation.ts +91 -0
- package/src/client/state/relation-store.ts +81 -0
- package/src/client/state/session-store.ts +144 -18
- package/src/client/state/sse-bridge.ts +206 -29
- package/src/client/state/workspace-settings.ts +11 -0
- package/src/client/theme/global.css +2724 -476
- package/src/client/types.ts +12 -0
- package/src/client/utils/keyboard.ts +10 -0
- package/src/client/utils/time-ago.ts +14 -0
- package/src/json-render/renderer/index.tsx +31 -0
- package/src/mcp/canvas-app.ts +12 -2
- package/src/mcp/context-uri-template.ts +7 -4
- package/src/mcp/server.ts +63 -41
- package/src/mcp/workbench-app.ts +39 -6
- package/src/server/agent-presence.ts +128 -28
- package/src/server/ax-context.ts +27 -13
- package/src/server/board-export.ts +12 -1
- package/src/server/canvas-db.ts +331 -42
- package/src/server/canvas-operations.ts +195 -83
- package/src/server/canvas-provenance.ts +53 -22
- package/src/server/canvas-serialization.ts +2 -0
- package/src/server/canvas-state.ts +950 -122
- package/src/server/canvas-validation.ts +9 -2
- package/src/server/context-brief.ts +345 -38
- package/src/server/context-reads.ts +178 -30
- package/src/server/demo-state.json +355 -24
- package/src/server/edit-summary.ts +74 -0
- package/src/server/html-surface.ts +8 -1
- package/src/server/index.ts +181 -11
- package/src/server/knowledge-lifecycle.ts +304 -0
- package/src/server/lifecycle-status.ts +232 -0
- package/src/server/memory-graph.ts +166 -0
- package/src/server/mutation-history.ts +18 -6
- package/src/server/operations/composites.ts +32 -11
- package/src/server/operations/http.ts +7 -2
- package/src/server/operations/index.ts +4 -0
- package/src/server/operations/mcp.ts +26 -1
- package/src/server/operations/ops/ax-presence.ts +1 -1
- package/src/server/operations/ops/ax-read.ts +57 -6
- package/src/server/operations/ops/ax-timeline.ts +28 -2
- package/src/server/operations/ops/backup.ts +5 -3
- package/src/server/operations/ops/batch.ts +35 -4
- package/src/server/operations/ops/boards.ts +188 -47
- package/src/server/operations/ops/canvas-wire.ts +31 -1
- package/src/server/operations/ops/edges.ts +36 -2
- package/src/server/operations/ops/groups.ts +6 -1
- package/src/server/operations/ops/lifecycle.ts +262 -0
- package/src/server/operations/ops/memory-graph.ts +45 -0
- package/src/server/operations/ops/nodes.ts +107 -24
- package/src/server/operations/ops/query.ts +281 -68
- package/src/server/operations/ops/snapshots.ts +73 -1
- package/src/server/operations/ops/validate.ts +5 -1
- package/src/server/operations/ops/viewport.ts +97 -3
- package/src/server/operations/registry.ts +74 -25
- package/src/server/placement.ts +29 -0
- package/src/server/scope-fence.ts +78 -6
- package/src/server/server.ts +115 -16
- package/src/server/spatial-analysis.ts +48 -23
- package/src/server/tour-control.ts +82 -0
- package/src/server/wiki-links.ts +137 -0
- package/src/server/workbench-auth.ts +11 -0
- package/src/shared/agent-context.ts +3 -0
- package/src/shared/agent-presence.ts +15 -15
- package/src/shared/auto-arrange.ts +2 -0
- package/src/shared/boards.ts +4 -4
- package/src/shared/knowledge-lifecycle.ts +22 -0
- package/src/shared/knowledge-presets.ts +80 -0
- package/src/shared/lifecycle-status.ts +26 -0
- package/src/shared/memory-graph.ts +74 -0
- package/src/shared/relations.ts +98 -0
- package/src/shared/semantic-attention.ts +15 -1
- package/src/shared/tour.ts +23 -5
- package/src/shared/workspace-settings.ts +13 -0
- package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
- package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
- package/dist/types/client/canvas/kind-colors.d.ts +0 -3
- package/dist/types/server/agent-context.d.ts +0 -22
- package/src/client/canvas/ContextPinBar.tsx +0 -25
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { canvasState } from '../../canvas-state.js';
|
|
3
|
+
import { categorySchema, folderPropertiesSchema, lifecycleSchema } from '../../knowledge-lifecycle.js';
|
|
4
|
+
import { defineOperation, OperationError, type Operation } from '../types.js';
|
|
5
|
+
import { readTargetBoard } from './boards.js';
|
|
6
|
+
import { lifecycleQualifications, lifecycleSnapshot } from '../../lifecycle-status.js';
|
|
7
|
+
import { memoryKey } from '../../../shared/memory-graph.js';
|
|
8
|
+
import { folderAncestors } from '../../../shared/boards.js';
|
|
9
|
+
import { relationPresetSchema } from '../../../shared/relations.js';
|
|
10
|
+
|
|
11
|
+
const jsonResult = (result: unknown) => ({
|
|
12
|
+
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
|
|
13
|
+
});
|
|
14
|
+
const target = { id: z.string().min(1), expectedBoardId: z.string().nullable().optional() };
|
|
15
|
+
|
|
16
|
+
export const lifecycleOperations: Operation[] = [
|
|
17
|
+
defineOperation({
|
|
18
|
+
name: 'knowledge.list',
|
|
19
|
+
mutates: false,
|
|
20
|
+
input: z.object({}),
|
|
21
|
+
inputShape: {},
|
|
22
|
+
http: { method: 'GET', path: '/api/canvas/knowledge' },
|
|
23
|
+
mcp: {
|
|
24
|
+
toolName: 'canvas_knowledge_list',
|
|
25
|
+
description: 'Read workspace categories and folder properties. No inferred defaults.',
|
|
26
|
+
formatResult: jsonResult,
|
|
27
|
+
},
|
|
28
|
+
handler: () => {
|
|
29
|
+
const catalog = canvasState.knowledge.list();
|
|
30
|
+
const paths = new Set([
|
|
31
|
+
...catalog.folders.flatMap((folder) => folderAncestors(folder.path)),
|
|
32
|
+
...canvasState.listBoards().flatMap((board) => (board.folder ? folderAncestors(board.folder) : [])),
|
|
33
|
+
]);
|
|
34
|
+
return {
|
|
35
|
+
ok: true,
|
|
36
|
+
...catalog,
|
|
37
|
+
...lifecycleSnapshot(),
|
|
38
|
+
resolvedFolders: Object.fromEntries(
|
|
39
|
+
[...paths].map((path) => [path, canvasState.knowledge.resolve(path, null, catalog)]),
|
|
40
|
+
),
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
defineOperation({
|
|
45
|
+
name: 'knowledge.category-set',
|
|
46
|
+
mutates: false,
|
|
47
|
+
input: z.object({ category: categorySchema }),
|
|
48
|
+
inputShape: { category: categorySchema },
|
|
49
|
+
http: { method: 'POST', path: '/api/canvas/knowledge/category' },
|
|
50
|
+
mcp: {
|
|
51
|
+
toolName: 'canvas_knowledge_category_set',
|
|
52
|
+
description: 'Create or replace one workspace category by stable id.',
|
|
53
|
+
formatResult: jsonResult,
|
|
54
|
+
},
|
|
55
|
+
handler: (input, ctx) => {
|
|
56
|
+
const category = canvasState.knowledge.setCategory(input.category);
|
|
57
|
+
ctx.emit('knowledge-changed', canvasState.knowledge.list());
|
|
58
|
+
return { ok: true, category };
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
defineOperation({
|
|
62
|
+
name: 'knowledge.category-remove',
|
|
63
|
+
mutates: false,
|
|
64
|
+
input: z.object({ id: z.string().trim().min(1) }),
|
|
65
|
+
inputShape: { id: z.string().trim().min(1) },
|
|
66
|
+
http: { method: 'DELETE', path: '/api/canvas/knowledge/category/:id' },
|
|
67
|
+
mcp: {
|
|
68
|
+
toolName: 'canvas_knowledge_category_remove',
|
|
69
|
+
description:
|
|
70
|
+
'Delete an unused workspace category by id. Explicit folder assignments, including empty folders, refuse deletion.',
|
|
71
|
+
formatResult: jsonResult,
|
|
72
|
+
},
|
|
73
|
+
handler: (input, ctx) => {
|
|
74
|
+
const id = canvasState.knowledge.removeCategory(input.id);
|
|
75
|
+
ctx.emit('knowledge-changed', canvasState.knowledge.list());
|
|
76
|
+
return { ok: true, id };
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
defineOperation({
|
|
80
|
+
name: 'knowledge.relation-preset-set',
|
|
81
|
+
mutates: false,
|
|
82
|
+
input: z.object({ preset: relationPresetSchema }),
|
|
83
|
+
inputShape: { preset: relationPresetSchema },
|
|
84
|
+
http: { method: 'POST', path: '/api/canvas/knowledge/relation-preset' },
|
|
85
|
+
mcp: {
|
|
86
|
+
toolName: 'canvas_knowledge_relation_preset_set',
|
|
87
|
+
description: 'Save one shared workspace relation preset. Existing boards and connections are unchanged.',
|
|
88
|
+
formatResult: jsonResult,
|
|
89
|
+
},
|
|
90
|
+
handler: (input, ctx) => {
|
|
91
|
+
const preset = canvasState.knowledge.setRelationPreset(input.preset);
|
|
92
|
+
ctx.emit('knowledge-changed', canvasState.knowledge.list());
|
|
93
|
+
return { ok: true, preset };
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
defineOperation({
|
|
97
|
+
name: 'knowledge.relation-preset-remove',
|
|
98
|
+
mutates: false,
|
|
99
|
+
input: z.object({ id: z.string().trim().min(1) }),
|
|
100
|
+
inputShape: { id: z.string().trim().min(1) },
|
|
101
|
+
http: { method: 'DELETE', path: '/api/canvas/knowledge/relation-preset/:id' },
|
|
102
|
+
mcp: {
|
|
103
|
+
toolName: 'canvas_knowledge_relation_preset_remove',
|
|
104
|
+
description: 'Delete a shared relation preset without changing vocabularies already applied to boards.',
|
|
105
|
+
formatResult: jsonResult,
|
|
106
|
+
},
|
|
107
|
+
handler: (input, ctx) => {
|
|
108
|
+
const id = canvasState.knowledge.removeRelationPreset(input.id);
|
|
109
|
+
ctx.emit('knowledge-changed', canvasState.knowledge.list());
|
|
110
|
+
return { ok: true, id };
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
defineOperation({
|
|
114
|
+
name: 'knowledge.folder-set',
|
|
115
|
+
mutates: false,
|
|
116
|
+
input: folderPropertiesSchema,
|
|
117
|
+
inputShape: folderPropertiesSchema.shape,
|
|
118
|
+
http: { method: 'POST', path: '/api/canvas/knowledge/folder' },
|
|
119
|
+
mcp: {
|
|
120
|
+
toolName: 'canvas_knowledge_folder_set',
|
|
121
|
+
description: 'Replace canonical folder properties. Null category inherits; null lifecycle clears the override.',
|
|
122
|
+
formatResult: jsonResult,
|
|
123
|
+
},
|
|
124
|
+
handler: (input, ctx) => {
|
|
125
|
+
try {
|
|
126
|
+
const folder = canvasState.knowledge.setFolder(input);
|
|
127
|
+
ctx.emit('knowledge-changed', canvasState.knowledge.list());
|
|
128
|
+
return { ok: true, folder };
|
|
129
|
+
} catch (error) {
|
|
130
|
+
throw new OperationError(String(error));
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
defineOperation({
|
|
135
|
+
name: 'node.lifecycle-get',
|
|
136
|
+
mutates: false,
|
|
137
|
+
input: z.object({ id: z.string(), board: z.string().optional() }),
|
|
138
|
+
inputShape: { id: z.string(), board: z.string().optional() },
|
|
139
|
+
http: { method: 'GET', path: '/api/canvas/node/:id/lifecycle' },
|
|
140
|
+
mcp: {
|
|
141
|
+
toolName: 'canvas_node_lifecycle_get',
|
|
142
|
+
description:
|
|
143
|
+
'Read effective category/lifecycle, source folder and audited review history without opening a board.',
|
|
144
|
+
formatResult: jsonResult,
|
|
145
|
+
},
|
|
146
|
+
handler: (input) => {
|
|
147
|
+
const read = readTargetBoard(input.board);
|
|
148
|
+
const node = read.layout.nodes.find((n) => n.id === input.id);
|
|
149
|
+
if (!read.boardId || !node) throw new OperationError('Card not found.', 404);
|
|
150
|
+
const folder = canvasState.listBoards().find((b) => b.id === read.boardId)?.folder ?? null;
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
boardId: read.boardId,
|
|
154
|
+
nodeId: input.id,
|
|
155
|
+
contentRevision: node.contentRevision ?? 0,
|
|
156
|
+
...canvasState.knowledge.resolveCard(folder, node),
|
|
157
|
+
...canvasState.knowledge.reviews(read.boardId, input.id),
|
|
158
|
+
qualification: lifecycleQualifications(Date.now(), [{ boardId: read.boardId, cardId: input.id }])[
|
|
159
|
+
memoryKey({ boardId: read.boardId, cardId: input.id })
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
defineOperation({
|
|
165
|
+
name: 'node.lifecycle-set',
|
|
166
|
+
mutates: true,
|
|
167
|
+
input: z.object({ ...target, lifecycleOverride: lifecycleSchema.nullable() }),
|
|
168
|
+
inputShape: { ...target, lifecycleOverride: lifecycleSchema.nullable() },
|
|
169
|
+
http: { method: 'POST', path: '/api/canvas/node/:id/lifecycle' },
|
|
170
|
+
mcp: {
|
|
171
|
+
toolName: 'canvas_node_lifecycle_set',
|
|
172
|
+
description:
|
|
173
|
+
'Set a full card lifecycle override, or null to inherit. Never changes category or semantic content revision.',
|
|
174
|
+
formatResult: jsonResult,
|
|
175
|
+
},
|
|
176
|
+
handler: (input) => {
|
|
177
|
+
canvasState.setNodeLifecycle(input.id, input.lifecycleOverride);
|
|
178
|
+
return { ok: true };
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
defineOperation({
|
|
182
|
+
name: 'node.review',
|
|
183
|
+
mutates: true,
|
|
184
|
+
input: z.object({
|
|
185
|
+
...target,
|
|
186
|
+
outcome: z.enum(['retain', 'revise', 'externalize', 'retire', 'unresolved']),
|
|
187
|
+
evidence: z.string(),
|
|
188
|
+
applied: z.boolean(),
|
|
189
|
+
expectedContentRevision: z.number().int().nonnegative().optional(),
|
|
190
|
+
expectedSources: z.record(z.string(), z.number().int().nonnegative().nullable()).optional(),
|
|
191
|
+
}),
|
|
192
|
+
inputShape: {
|
|
193
|
+
...target,
|
|
194
|
+
outcome: z.enum(['retain', 'revise', 'externalize', 'retire', 'unresolved']),
|
|
195
|
+
evidence: z.string(),
|
|
196
|
+
applied: z.boolean(),
|
|
197
|
+
expectedSources: z
|
|
198
|
+
.record(z.string(), z.number().int().nonnegative().nullable())
|
|
199
|
+
.optional()
|
|
200
|
+
.describe('Required when validating dependencies: observations from lifecycle-get after checking the sources.'),
|
|
201
|
+
expectedContentRevision: z
|
|
202
|
+
.number()
|
|
203
|
+
.int()
|
|
204
|
+
.nonnegative()
|
|
205
|
+
.optional()
|
|
206
|
+
.describe('Required for evidence-backed validating outcomes; use the revision of the content you checked.'),
|
|
207
|
+
},
|
|
208
|
+
http: { method: 'POST', path: '/api/canvas/node/:id/review' },
|
|
209
|
+
mcp: {
|
|
210
|
+
toolName: 'canvas_node_review',
|
|
211
|
+
description:
|
|
212
|
+
'Audit an outcome. Applied retain/revise/externalize with nonempty evidence and matching expectedContentRevision renew validation. Reviewed content is persisted before evidence. Does not edit content or restore retirement.',
|
|
213
|
+
formatResult: jsonResult,
|
|
214
|
+
},
|
|
215
|
+
handler: (input, ctx) => {
|
|
216
|
+
if (!canvasState.getNode(input.id) || !canvasState.activeBoardId)
|
|
217
|
+
throw new OperationError('Card not found.', 404);
|
|
218
|
+
const observations =
|
|
219
|
+
lifecycleQualifications()[memoryKey({ boardId: canvasState.activeBoardId, cardId: input.id })]?.observations ??
|
|
220
|
+
{};
|
|
221
|
+
const validates =
|
|
222
|
+
input.applied && input.evidence.trim() && ['retain', 'revise', 'externalize'].includes(input.outcome);
|
|
223
|
+
if (validates) {
|
|
224
|
+
const expected = input.expectedSources ?? {};
|
|
225
|
+
if (
|
|
226
|
+
Object.keys(expected).length !== Object.keys(observations).length ||
|
|
227
|
+
Object.entries(observations).some(([key, revision]) => revision === null || expected[key] !== revision)
|
|
228
|
+
)
|
|
229
|
+
throw new OperationError(
|
|
230
|
+
'Sources changed or are unavailable. Check the current sources and pass expectedSources from lifecycle-get.',
|
|
231
|
+
409,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const record = canvasState.knowledge.record(canvasState.activeBoardId, input.id, input, observations);
|
|
235
|
+
ctx.emit('lifecycle-changed', { boardId: canvasState.activeBoardId, nodeId: input.id });
|
|
236
|
+
return { ok: true, record };
|
|
237
|
+
},
|
|
238
|
+
}),
|
|
239
|
+
defineOperation({
|
|
240
|
+
name: 'node.lifecycle-restore',
|
|
241
|
+
mutates: true,
|
|
242
|
+
input: z.object({ ...target, evidence: z.string() }),
|
|
243
|
+
inputShape: { ...target, evidence: z.string() },
|
|
244
|
+
http: { method: 'POST', path: '/api/canvas/node/:id/lifecycle/restore' },
|
|
245
|
+
mcp: {
|
|
246
|
+
toolName: 'canvas_node_lifecycle_restore',
|
|
247
|
+
description: 'Explicitly reverse retirement and audit the restoration. Never validates.',
|
|
248
|
+
formatResult: jsonResult,
|
|
249
|
+
},
|
|
250
|
+
handler: (input, ctx) => {
|
|
251
|
+
if (!canvasState.getNode(input.id) || !canvasState.activeBoardId)
|
|
252
|
+
throw new OperationError('Card not found.', 404);
|
|
253
|
+
const record = canvasState.knowledge.record(canvasState.activeBoardId, input.id, {
|
|
254
|
+
outcome: 'restore',
|
|
255
|
+
evidence: input.evidence,
|
|
256
|
+
applied: true,
|
|
257
|
+
});
|
|
258
|
+
ctx.emit('lifecycle-changed', { boardId: canvasState.activeBoardId, nodeId: input.id });
|
|
259
|
+
return { ok: true, record };
|
|
260
|
+
},
|
|
261
|
+
}),
|
|
262
|
+
];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { memoryGraph, localMemoryGraph, boardMemoryGraph } from '../../memory-graph.js';
|
|
3
|
+
import { defineOperation, OperationError, type Operation } from '../types.js';
|
|
4
|
+
|
|
5
|
+
const shape = {
|
|
6
|
+
projection: z
|
|
7
|
+
.enum(['full', 'boards'])
|
|
8
|
+
.default('full')
|
|
9
|
+
.describe('Full board/card graph, or board-only Map summaries with qualified cross-board and missing relations'),
|
|
10
|
+
focus: z.string().optional().describe('Board ID, board-qualified graph node key, or pins; omit for the library'),
|
|
11
|
+
depth: z
|
|
12
|
+
.union([z.number(), z.string().trim().min(1)])
|
|
13
|
+
.pipe(z.coerce.number<string | number>().int().min(0).max(4))
|
|
14
|
+
.default(2)
|
|
15
|
+
.describe('Undirected hops, including membership, 0–4 (default 2)'),
|
|
16
|
+
};
|
|
17
|
+
const schema = z.looseObject(shape);
|
|
18
|
+
export const memoryGraphOperations: Operation[] = [
|
|
19
|
+
defineOperation({
|
|
20
|
+
name: 'memory-graph.get',
|
|
21
|
+
mutates: false,
|
|
22
|
+
input: schema,
|
|
23
|
+
inputShape: shape,
|
|
24
|
+
http: { method: 'GET', path: '/api/canvas/memory-graph' },
|
|
25
|
+
mcp: {
|
|
26
|
+
toolName: 'canvas_memory_graph',
|
|
27
|
+
description:
|
|
28
|
+
'Explore the same board/card graph Home draws. Summary-only discovery, not full content reads; writes still target the open board.',
|
|
29
|
+
extraShape: shape,
|
|
30
|
+
formatResult: (result) => ({ content: [{ type: 'text' as const, text: JSON.stringify(result) }] }),
|
|
31
|
+
},
|
|
32
|
+
handler: (input) => {
|
|
33
|
+
const graph = memoryGraph(input.focus ? 'full' : input.projection);
|
|
34
|
+
if (
|
|
35
|
+
input.focus &&
|
|
36
|
+
input.focus !== 'pins' &&
|
|
37
|
+
!graph.nodes.some((node) => node.key === input.focus || (node.kind === 'board' && node.boardId === input.focus))
|
|
38
|
+
) {
|
|
39
|
+
throw new OperationError('Graph focus not found. Use a board ID, a qualified node key, or pins.', 404);
|
|
40
|
+
}
|
|
41
|
+
const local = localMemoryGraph(graph, input.focus ?? null, input.depth);
|
|
42
|
+
return input.projection === 'boards' ? boardMemoryGraph(local) : local;
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
];
|
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
*
|
|
9
9
|
* This module must never import server.ts or index.ts.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { extname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
12
13
|
import { z } from 'zod';
|
|
13
14
|
import { canvasState, type CanvasAnnotation, type CanvasLayout, type CanvasNodeState } from '../../canvas-state.js';
|
|
14
15
|
import {
|
|
15
16
|
addCanvasNode,
|
|
16
17
|
createCanvasGroup,
|
|
18
|
+
storedCreationTarget,
|
|
17
19
|
buildStructuredNodeUpdate,
|
|
18
20
|
hasStructuredNodeUpdateFields,
|
|
19
21
|
hasTraceNodeDataFields,
|
|
@@ -53,6 +55,8 @@ import {
|
|
|
53
55
|
import { WEBPAGE_NODE_DEFAULT_SIZE, normalizeWebpageUrl } from '../../webpage-node.js';
|
|
54
56
|
import { defineOperation, OperationError, type Operation, type OperationContext } from '../types.js';
|
|
55
57
|
import { readTargetBoard } from './boards.js';
|
|
58
|
+
import { lifecycleQualifications } from '../../lifecycle-status.js';
|
|
59
|
+
import { memoryKey } from '../../../shared/memory-graph.js';
|
|
56
60
|
|
|
57
61
|
// ── Node types ────────────────────────────────────────────────
|
|
58
62
|
// Single definition site for the basic node-type list: drives the HTTP type
|
|
@@ -108,6 +112,11 @@ export function defaultNodeSize(type: string): { width: number; height: number }
|
|
|
108
112
|
|
|
109
113
|
// ── Shared helpers (moved from server.ts) ─────────────────────
|
|
110
114
|
|
|
115
|
+
/** Parent traversal in a relative path, using the path implementation's separator. */
|
|
116
|
+
export function isParentTraversal(path: string, separator: string): boolean {
|
|
117
|
+
return path === '..' || path.startsWith(`..${separator}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
111
120
|
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
112
121
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
113
122
|
}
|
|
@@ -282,6 +291,7 @@ export function compactLayoutPayload(layout: CanvasLayout, pinnedIds: string[]):
|
|
|
282
291
|
id: edge.id,
|
|
283
292
|
from: edge.from,
|
|
284
293
|
to: edge.to,
|
|
294
|
+
...(edge.toBoardId !== undefined ? { toBoardId: edge.toBoardId } : {}),
|
|
285
295
|
type: edge.type,
|
|
286
296
|
...(edge.label ? { label: edge.label } : {}),
|
|
287
297
|
...(edge.style ? { style: edge.style } : {}),
|
|
@@ -325,7 +335,7 @@ export function createdNodePayloadFromNode(
|
|
|
325
335
|
*/
|
|
326
336
|
export function createBasicCanvasNode(
|
|
327
337
|
body: Record<string, unknown>,
|
|
328
|
-
options: { fileMode: 'auto' | 'path' },
|
|
338
|
+
options: { fileMode: 'auto' | 'path'; prepareOnlyNodes?: CanvasNodeState[] },
|
|
329
339
|
): { node: CanvasNodeState; needsCodeGraphRecompute: boolean; sizeAdjustment?: CanvasSizeAdjustment } {
|
|
330
340
|
const type = typeof body.type === 'string' ? body.type : '';
|
|
331
341
|
const extraData = isRecord(body.data) ? body.data : undefined;
|
|
@@ -417,6 +427,7 @@ export function createBasicCanvasNode(
|
|
|
417
427
|
defaultWidth: defaults.width,
|
|
418
428
|
defaultHeight: defaults.height,
|
|
419
429
|
fileMode: options.fileMode,
|
|
430
|
+
prepareOnlyNodes: options.prepareOnlyNodes,
|
|
420
431
|
});
|
|
421
432
|
return { node, needsCodeGraphRecompute, ...(sizeAdjustment ? { sizeAdjustment } : {}) };
|
|
422
433
|
} catch (error) {
|
|
@@ -445,6 +456,8 @@ export function buildNodePatch(
|
|
|
445
456
|
}
|
|
446
457
|
if (body.data && typeof body.data === 'object' && !Array.isArray(body.data)) {
|
|
447
458
|
const data = body.data as Record<string, unknown>;
|
|
459
|
+
if (Object.hasOwn(data, 'lifecycleOverride'))
|
|
460
|
+
throw new OperationError('Update lifecycle overrides through node.lifecycle-set, not a content patch.');
|
|
448
461
|
const source = data.source;
|
|
449
462
|
const importedSource = source && typeof source === 'object' && 'attachmentId' in source;
|
|
450
463
|
const attachedFileContent =
|
|
@@ -575,6 +588,30 @@ export function removeNodeCore(id: string): { needsCodeGraphRecompute: boolean }
|
|
|
575
588
|
|
|
576
589
|
// ── node.add ──────────────────────────────────────────────────
|
|
577
590
|
|
|
591
|
+
/** Explicit creation identity is opt-in, not content-based deduplication. */
|
|
592
|
+
export function createDestinationNode(body: Record<string, unknown>): CanvasNodeState {
|
|
593
|
+
const { boardId, requestId, fingerprint } = storedCreationTarget(body, 'node.add');
|
|
594
|
+
const supported = ['markdown', 'status', 'context', 'ledger', 'trace', 'diff', 'mermaid', 'html'];
|
|
595
|
+
if (!supported.includes(String(body.type)) || body.primitive !== undefined || body.kind !== undefined)
|
|
596
|
+
throw new OperationError(
|
|
597
|
+
'destinationBoardId/requestId support static markdown, status, context, ledger, trace, diff, mermaid and plain html creation only.',
|
|
598
|
+
);
|
|
599
|
+
const data = isRecord(body.data) ? body.data : {};
|
|
600
|
+
if (
|
|
601
|
+
body.intentId !== undefined ||
|
|
602
|
+
['children', 'childIds', 'childLayout', 'groupId', 'parentId', 'parentGroup'].some(
|
|
603
|
+
(key) => body[key] !== undefined || data[key] !== undefined,
|
|
604
|
+
)
|
|
605
|
+
)
|
|
606
|
+
throw new OperationError('Destination/retry creation does not support ghost intents or membership mutations.');
|
|
607
|
+
return canvasState.createStoredNode(
|
|
608
|
+
boardId,
|
|
609
|
+
requestId,
|
|
610
|
+
fingerprint,
|
|
611
|
+
(nodes) => createBasicCanvasNode(body, { fileMode: 'auto', prepareOnlyNodes: nodes }).node,
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
578
615
|
interface NodeAddResult {
|
|
579
616
|
node: CanvasNodeState;
|
|
580
617
|
extras?: Record<string, unknown>;
|
|
@@ -706,6 +743,18 @@ function createGroupNode(body: Record<string, unknown>): NodeAddResult {
|
|
|
706
743
|
}
|
|
707
744
|
|
|
708
745
|
const nodeAddShape = {
|
|
746
|
+
destinationBoardId: z
|
|
747
|
+
.string()
|
|
748
|
+
.min(1)
|
|
749
|
+
.optional()
|
|
750
|
+
.describe('Create a static node on this board without opening it. expectedBoardId still guards the ACTIVE board.'),
|
|
751
|
+
requestId: z
|
|
752
|
+
.string()
|
|
753
|
+
.min(1)
|
|
754
|
+
.optional()
|
|
755
|
+
.describe(
|
|
756
|
+
'Persistent logical creation identity. Reuse only for retries with identical input and destination; new identical content needs a new id. Static types only.',
|
|
757
|
+
),
|
|
709
758
|
intentId: z
|
|
710
759
|
.string()
|
|
711
760
|
.optional()
|
|
@@ -812,25 +861,9 @@ const nodeAddOperation = defineOperation<z.infer<typeof nodeAddSchema>, NodeAddR
|
|
|
812
861
|
mcp: {
|
|
813
862
|
toolName: 'canvas_add_node',
|
|
814
863
|
description:
|
|
815
|
-
'Add a basic node to the canvas. Returns the created node with normalized title/content and rendered geometry. Supported here: markdown, status, context, ledger, trace, file, diff, mermaid, image, webpage, mcp-app, html, group. Dedicated routes: json-render -> canvas_render {action:"add-json-render"}, graph -> canvas_render {action:"add-graph"}, web-artifact -> canvas_app {action:"build-artifact"}, external apps -> canvas_app {action:"open-mcp-app"}, groups -> canvas_group {action:"create"}. Call canvas_render {action:"describe-schema"} for the full nodeTypeRouting table.',
|
|
864
|
+
'Add a basic node to the canvas. Returns the created node with normalized title/content and rendered geometry. Supported here: markdown, board (data.boardId is the target), status, context, ledger, trace, file, diff, mermaid, image, webpage, mcp-app, html, group. Dedicated routes: json-render -> canvas_render {action:"add-json-render"}, graph -> canvas_render {action:"add-graph"}, web-artifact -> canvas_app {action:"build-artifact"}, external apps -> canvas_app {action:"open-mcp-app"}, groups -> canvas_group {action:"create"}. Call canvas_render {action:"describe-schema"} for the full nodeTypeRouting table.',
|
|
816
865
|
extraShape: {
|
|
817
|
-
type: z
|
|
818
|
-
.enum([
|
|
819
|
-
'markdown',
|
|
820
|
-
'status',
|
|
821
|
-
'context',
|
|
822
|
-
'ledger',
|
|
823
|
-
'trace',
|
|
824
|
-
'file',
|
|
825
|
-
'diff',
|
|
826
|
-
'mermaid',
|
|
827
|
-
'image',
|
|
828
|
-
'webpage',
|
|
829
|
-
'mcp-app',
|
|
830
|
-
'html',
|
|
831
|
-
'group',
|
|
832
|
-
])
|
|
833
|
-
.describe('Node type (prefer canvas_group {action:"create"} for groups)'),
|
|
866
|
+
type: z.enum(NODE_TYPES).describe('Node type (prefer canvas_group {action:"create"} for groups)'),
|
|
834
867
|
children: z
|
|
835
868
|
.array(z.string())
|
|
836
869
|
.optional()
|
|
@@ -879,6 +912,8 @@ const nodeAddOperation = defineOperation<z.infer<typeof nodeAddSchema>, NodeAddR
|
|
|
879
912
|
},
|
|
880
913
|
handler: async (input, ctx) => {
|
|
881
914
|
const body: Record<string, unknown> = input;
|
|
915
|
+
if (body.destinationBoardId !== undefined || body.requestId !== undefined)
|
|
916
|
+
return { node: createDestinationNode(body) };
|
|
882
917
|
const type = typeof body.type === 'string' ? body.type : '';
|
|
883
918
|
// Report #50: require a resolvable type rather than silently defaulting to a
|
|
884
919
|
// markdown node — an empty / type-less body created a phantom node before.
|
|
@@ -958,23 +993,32 @@ const nodeGetOperation = defineOperation<z.infer<typeof nodeGetSchema>, Serializ
|
|
|
958
993
|
},
|
|
959
994
|
buildInput: (input) => ({ id: input.id, board: input.board, includeBlobs: true }),
|
|
960
995
|
formatResult: (result, input) => {
|
|
961
|
-
const node = result as
|
|
962
|
-
const payload =
|
|
996
|
+
const node = result as SerializedCanvasNode;
|
|
997
|
+
const payload = {
|
|
998
|
+
...(wantsFullPayload(input) ? serializeCanvasNodeForAgent(node) : compactNodePayload(node)),
|
|
999
|
+
qualification: node.qualification,
|
|
1000
|
+
};
|
|
963
1001
|
return { content: [{ type: 'text' as const, text: JSON.stringify(payload, null, 2) }] };
|
|
964
1002
|
},
|
|
965
1003
|
},
|
|
966
1004
|
handler: ({ id, includeBlobs, board }) => {
|
|
967
1005
|
const full = includeBlobs === true || includeBlobs === 'true';
|
|
1006
|
+
const qualify = (node: CanvasNodeState, boardId: string | null) => ({
|
|
1007
|
+
...(full ? serializeCanvasNode(node) : serializeCanvasNodeWithBlobSummaries(node)),
|
|
1008
|
+
qualification: boardId
|
|
1009
|
+
? lifecycleQualifications(Date.now(), [{ boardId, cardId: id }])[memoryKey({ boardId, cardId: id })]
|
|
1010
|
+
: undefined,
|
|
1011
|
+
});
|
|
968
1012
|
if (typeof board === 'string' && board.trim()) {
|
|
969
1013
|
const target = readTargetBoard(board, full);
|
|
970
1014
|
const node = target.layout.nodes.find((candidate) => candidate.id === id);
|
|
971
1015
|
if (!node) throw new OperationError(`Node "${id}" not found on board "${board}".`, 404);
|
|
972
|
-
return
|
|
1016
|
+
return qualify(node, target.boardId);
|
|
973
1017
|
}
|
|
974
1018
|
const node = full ? canvasState.getNode(id) : canvasState.getNodeForPersistence(id);
|
|
975
1019
|
if (!node) throw new OperationError(`Node "${id}" not found.`, 404);
|
|
976
1020
|
const responseNode = withContextPinReadState(node);
|
|
977
|
-
return
|
|
1021
|
+
return qualify(responseNode, canvasState.activeBoardId);
|
|
978
1022
|
},
|
|
979
1023
|
});
|
|
980
1024
|
|
|
@@ -983,6 +1027,10 @@ const nodeGetOperation = defineOperation<z.infer<typeof nodeGetSchema>, Serializ
|
|
|
983
1027
|
const nodeUpdateShape = {
|
|
984
1028
|
id: z.string().describe('Node ID to update'),
|
|
985
1029
|
expectedContentRevision: z.number().int().nonnegative().optional().describe('Reject an edit based on stale content'),
|
|
1030
|
+
persistFile: z
|
|
1031
|
+
.boolean()
|
|
1032
|
+
.optional()
|
|
1033
|
+
.describe('Save file-backed Markdown to disk and card together; requires board and content revision preconditions'),
|
|
986
1034
|
intentId: z
|
|
987
1035
|
.string()
|
|
988
1036
|
.optional()
|
|
@@ -1175,6 +1223,35 @@ const nodeUpdateOperation = defineOperation<z.infer<typeof nodeUpdateSchema>, Re
|
|
|
1175
1223
|
}
|
|
1176
1224
|
}
|
|
1177
1225
|
|
|
1226
|
+
let markdownPath: string | undefined;
|
|
1227
|
+
if (input.persistFile) {
|
|
1228
|
+
if (
|
|
1229
|
+
existing.type !== 'markdown' ||
|
|
1230
|
+
typeof existing.data.path !== 'string' ||
|
|
1231
|
+
!existing.data.path ||
|
|
1232
|
+
typeof input.content !== 'string' ||
|
|
1233
|
+
input.expectedContentRevision === undefined ||
|
|
1234
|
+
typeof input.expectedBoardId !== 'string'
|
|
1235
|
+
) {
|
|
1236
|
+
throw new OperationError(
|
|
1237
|
+
'File persistence requires file-backed Markdown, content, expectedBoardId and expectedContentRevision.',
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
const data = isRecord(body.data) ? body.data : {};
|
|
1241
|
+
if (Object.keys(data).some((key) => key !== 'rendered'))
|
|
1242
|
+
throw new OperationError('File persistence accepts only data.rendered; update metadata separately.');
|
|
1243
|
+
markdownPath = realpathSync(resolve(canvasState.getWorkspaceRoot(), existing.data.path));
|
|
1244
|
+
const workspacePath = relative(realpathSync(canvasState.getWorkspaceRoot()), markdownPath);
|
|
1245
|
+
if (
|
|
1246
|
+
isParentTraversal(workspacePath, sep) ||
|
|
1247
|
+
isAbsolute(workspacePath) ||
|
|
1248
|
+
extname(markdownPath).toLowerCase() !== '.md' ||
|
|
1249
|
+
!statSync(markdownPath).isFile()
|
|
1250
|
+
) {
|
|
1251
|
+
throw new OperationError('File persistence requires a Markdown file inside the workspace.');
|
|
1252
|
+
}
|
|
1253
|
+
body = { ...body, content: input.content.replace(/\r\n?/g, '\n') };
|
|
1254
|
+
}
|
|
1178
1255
|
const { patch, groupChildIds } = buildNodePatch(existing, body);
|
|
1179
1256
|
const prevPath = existing.type === 'file' && typeof existing.data.path === 'string' ? existing.data.path : null;
|
|
1180
1257
|
if (nextFilePath && nextFileContent && patch.data) {
|
|
@@ -1186,6 +1263,12 @@ const nodeUpdateOperation = defineOperation<z.infer<typeof nodeUpdateSchema>, Re
|
|
|
1186
1263
|
applyFileContentToNodeData(data, nextFileContent);
|
|
1187
1264
|
patch.data = data;
|
|
1188
1265
|
}
|
|
1266
|
+
if (markdownPath && patch.data) {
|
|
1267
|
+
// No await between revision validation, disk persistence and the card
|
|
1268
|
+
// update: a refused/stale attempt must not write either half.
|
|
1269
|
+
writeFileSync(markdownPath, String(patch.data.content), 'utf8');
|
|
1270
|
+
patch.data.savedAt = new Date().toISOString();
|
|
1271
|
+
}
|
|
1189
1272
|
canvasState.updateNode(id, patch);
|
|
1190
1273
|
if (groupChildIds !== undefined && !setGroupChildrenFromApi(id, groupChildIds)) {
|
|
1191
1274
|
throw new OperationError(`Group "${id}" not found.`, 404);
|