pmx-canvas 0.6.4 → 0.7.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/extension.mjs +67 -3
- package/CHANGELOG.md +73 -0
- package/Readme.md +35 -5
- package/dist/canvas/global.css +498 -28
- package/dist/canvas/index.js +110 -95
- package/dist/canvas/mermaid-entry.js +258 -258
- package/dist/types/cli/agent.d.ts +4 -0
- package/dist/types/cli/commands/backup.d.ts +1 -0
- package/dist/types/cli/commands/boards.d.ts +1 -0
- package/dist/types/cli/commands/export.d.ts +1 -0
- package/dist/types/cli/commands/record.d.ts +12 -0
- package/dist/types/client/canvas/BoardCopyDialog.d.ts +5 -0
- package/dist/types/client/canvas/BoardFolderDialog.d.ts +7 -0
- package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -0
- package/dist/types/client/canvas/BoardSwitcher.d.ts +8 -0
- package/dist/types/client/canvas/ExportDialog.d.ts +7 -0
- package/dist/types/client/canvas/HomeView.d.ts +9 -0
- package/dist/types/client/canvas/Presentation.d.ts +13 -0
- package/dist/types/client/canvas/use-node-drag.d.ts +2 -1
- package/dist/types/client/nodes/AttachmentNode.d.ts +6 -0
- package/dist/types/client/nodes/BoardNode.d.ts +5 -0
- package/dist/types/client/nodes/McpAppNode.d.ts +24 -1
- package/dist/types/client/state/boards-store.d.ts +79 -0
- package/dist/types/client/state/canvas-store.d.ts +2 -0
- package/dist/types/client/state/intent-bridge.d.ts +1 -0
- package/dist/types/client/state/presentation.d.ts +3 -0
- package/dist/types/client/state/session-store.d.ts +7 -0
- package/dist/types/client/types.d.ts +2 -2
- package/dist/types/json-render/catalog.d.ts +3 -3
- package/dist/types/json-render/charts/definitions.d.ts +1 -1
- package/dist/types/json-render/server.d.ts +8 -0
- package/dist/types/mcp/canvas-access.d.ts +10 -3
- package/dist/types/mcp/context-uri-template.d.ts +6 -0
- package/dist/types/server/agent-presence.d.ts +1 -1
- package/dist/types/server/attribution.d.ts +11 -0
- package/dist/types/server/ax-context.d.ts +3 -1
- package/dist/types/server/ax-interaction.d.ts +2 -2
- package/dist/types/server/ax-state.d.ts +13 -3
- package/dist/types/server/backup.d.ts +36 -0
- package/dist/types/server/board-export.d.ts +45 -0
- package/dist/types/server/canvas-db.d.ts +65 -9
- package/dist/types/server/canvas-operations.d.ts +45 -0
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-schema.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +146 -3
- package/dist/types/server/canvas-validation.d.ts +3 -0
- package/dist/types/server/context-brief.d.ts +111 -0
- package/dist/types/server/context-reads.d.ts +55 -0
- package/dist/types/server/document-import.d.ts +66 -0
- package/dist/types/server/html-surface.d.ts +1 -1
- package/dist/types/server/index.d.ts +107 -4
- package/dist/types/server/mutation-history.d.ts +1 -0
- package/dist/types/server/operations/invoker.d.ts +2 -0
- package/dist/types/server/operations/mcp.d.ts +7 -1
- package/dist/types/server/operations/ops/app.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-shared.d.ts +2 -2
- package/dist/types/server/operations/ops/backup.d.ts +2 -0
- package/dist/types/server/operations/ops/boards.d.ts +10 -0
- package/dist/types/server/operations/ops/export.d.ts +2 -0
- package/dist/types/server/operations/ops/imports.d.ts +2 -0
- package/dist/types/server/operations/ops/json-render.d.ts +2 -0
- package/dist/types/server/operations/ops/nodes.d.ts +4 -2
- package/dist/types/server/operations/ops/query.d.ts +2 -0
- package/dist/types/server/operations/registry.d.ts +9 -1
- package/dist/types/server/operations/types.d.ts +1 -1
- package/dist/types/server/server.d.ts +1 -0
- package/dist/types/server/trace-manager.d.ts +7 -0
- package/dist/types/server/web-artifacts.d.ts +3 -0
- package/dist/types/server/webpage-node.d.ts +22 -1
- package/dist/types/server/workbench-auth.d.ts +3 -0
- package/dist/types/shared/boards.d.ts +6 -0
- package/dist/types/shared/tour.d.ts +72 -0
- package/docs/RELEASE.md +5 -0
- package/docs/cli.md +157 -0
- package/docs/http-api.md +277 -6
- package/docs/mcp.md +96 -7
- package/docs/node-types.md +106 -0
- package/docs/plans/plan-010-attributed-ask-answers.md +39 -0
- package/docs/plans/plan-011-context-read-instrumentation.md +54 -0
- package/docs/plans/plan-012-many-boards-stage-1.md +98 -0
- package/docs/plans/plan-013-static-board-export.md +47 -0
- package/docs/plans/plan-014-connected-board-memory.md +140 -0
- package/docs/product-vision-2026-09.md +118 -18
- package/docs/sdk.md +50 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +62 -5
- package/skills/pmx-canvas/references/full-reference.md +44 -8
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +3 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
- package/skills/pmx-canvas-orchestration/SKILL.md +5 -0
- package/skills/pmx-canvas-testing/SKILL.md +4 -0
- package/src/cli/agent.ts +9 -0
- package/src/cli/commands/ax.ts +13 -0
- package/src/cli/commands/backup.ts +66 -0
- package/src/cli/commands/boards.ts +71 -0
- package/src/cli/commands/export.ts +30 -0
- package/src/cli/commands/nodes.ts +14 -1
- package/src/cli/commands/pins.ts +6 -2
- package/src/cli/commands/pump.ts +3 -0
- package/src/cli/commands/query.ts +33 -7
- package/src/cli/commands/record.ts +218 -0
- package/src/cli/commands/view.ts +7 -5
- package/src/cli/index.ts +21 -9
- package/src/client/App.tsx +28 -4
- package/src/client/canvas/AgentPresenceLayer.tsx +1 -1
- package/src/client/canvas/BoardCopyDialog.tsx +153 -0
- package/src/client/canvas/BoardFolderDialog.tsx +120 -0
- package/src/client/canvas/BoardLinkDialog.tsx +93 -0
- package/src/client/canvas/BoardSwitcher.tsx +149 -0
- package/src/client/canvas/CanvasNode.tsx +8 -0
- package/src/client/canvas/CanvasViewport.tsx +3 -0
- package/src/client/canvas/CommandPalette.tsx +107 -4
- package/src/client/canvas/ContextMenu.tsx +4 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -0
- package/src/client/canvas/ExportDialog.tsx +197 -0
- package/src/client/canvas/HomeView.tsx +277 -0
- package/src/client/canvas/IntentLayer.tsx +9 -2
- package/src/client/canvas/Presentation.tsx +108 -0
- package/src/client/canvas/SessionPanel.tsx +28 -1
- package/src/client/canvas/ToolRail.tsx +38 -3
- package/src/client/canvas/TopBar.tsx +49 -7
- package/src/client/canvas/import-files.ts +19 -1
- package/src/client/canvas/kind-colors.ts +1 -0
- package/src/client/canvas/use-node-drag.ts +8 -3
- package/src/client/icons.tsx +2 -0
- package/src/client/mermaid-entry.ts +39 -0
- package/src/client/nodes/AttachmentNode.tsx +148 -0
- package/src/client/nodes/BoardNode.tsx +35 -0
- package/src/client/nodes/FileNode.tsx +6 -0
- package/src/client/nodes/HtmlNode.tsx +12 -13
- package/src/client/nodes/MarkdownNode.tsx +47 -19
- package/src/client/nodes/McpAppNode.tsx +105 -13
- package/src/client/nodes/MermaidNode.tsx +3 -2
- package/src/client/nodes/ax-node-actions.ts +2 -5
- package/src/client/nodes/use-ax-surface-bridge.ts +1 -1
- package/src/client/state/boards-store.ts +167 -0
- package/src/client/state/canvas-store.ts +22 -0
- package/src/client/state/intent-bridge.ts +30 -2
- package/src/client/state/intent-store.ts +5 -1
- package/src/client/state/node-factory.ts +1 -0
- package/src/client/state/presentation.ts +7 -0
- package/src/client/state/session-store.ts +8 -0
- package/src/client/state/sse-bridge.ts +52 -5
- package/src/client/theme/global.css +498 -28
- package/src/client/types.ts +2 -0
- package/src/json-render/server.ts +45 -9
- package/src/mcp/canvas-access.ts +59 -22
- package/src/mcp/context-uri-template.ts +28 -0
- package/src/mcp/server.ts +122 -15
- package/src/server/agent-context.ts +116 -29
- package/src/server/agent-presence.ts +15 -9
- package/src/server/attribution.ts +29 -0
- package/src/server/ax-context.ts +4 -2
- package/src/server/ax-interaction.ts +1 -0
- package/src/server/ax-state-manager.ts +72 -3
- package/src/server/ax-state.ts +41 -0
- package/src/server/backup.ts +178 -0
- package/src/server/board-export.ts +686 -0
- package/src/server/canvas-db.ts +612 -105
- package/src/server/canvas-operations.ts +183 -31
- package/src/server/canvas-provenance.ts +1 -0
- package/src/server/canvas-schema.ts +22 -0
- package/src/server/canvas-state.ts +812 -48
- package/src/server/canvas-validation.ts +9 -1
- package/src/server/context-brief.ts +372 -0
- package/src/server/context-reads.ts +238 -0
- package/src/server/demo-state.json +670 -9
- package/src/server/document-import.ts +223 -0
- package/src/server/html-primitives.ts +199 -91
- package/src/server/html-surface.ts +8 -3
- package/src/server/index.ts +243 -5
- package/src/server/mutation-history.ts +10 -13
- package/src/server/operations/composites.ts +29 -5
- package/src/server/operations/http.ts +9 -3
- package/src/server/operations/index.ts +8 -0
- package/src/server/operations/invoker.ts +14 -3
- package/src/server/operations/mcp.ts +50 -11
- package/src/server/operations/ops/app.ts +15 -0
- package/src/server/operations/ops/ax-read.ts +10 -6
- package/src/server/operations/ops/ax-timeline.ts +75 -0
- package/src/server/operations/ops/backup.ts +134 -0
- package/src/server/operations/ops/batch.ts +20 -4
- package/src/server/operations/ops/boards.ts +351 -0
- package/src/server/operations/ops/export.ts +74 -0
- package/src/server/operations/ops/ext-app.ts +4 -0
- package/src/server/operations/ops/groups.ts +14 -7
- package/src/server/operations/ops/imports.ts +220 -0
- package/src/server/operations/ops/json-render.ts +37 -5
- package/src/server/operations/ops/nodes.ts +99 -21
- package/src/server/operations/ops/query.ts +206 -9
- package/src/server/operations/ops/viewport.ts +32 -0
- package/src/server/operations/registry.ts +129 -31
- package/src/server/operations/types.ts +1 -1
- package/src/server/scope-fence.ts +1 -1
- package/src/server/server.ts +155 -8
- package/src/server/spatial-analysis.ts +9 -17
- package/src/server/trace-manager.ts +19 -0
- package/src/server/web-artifacts.ts +11 -3
- package/src/server/webpage-node.ts +173 -13
- package/src/server/workbench-auth.ts +11 -0
- package/src/server/workboard.ts +3 -3
- package/src/shared/boards.ts +18 -0
- package/src/shared/node-sizes.ts +1 -0
- package/src/shared/tour.ts +87 -0
package/src/server/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { derivedTour, type Tour } from '../shared/tour.js';
|
|
3
|
+
export type { Tour, TourStop } from '../shared/tour.js';
|
|
2
4
|
import { agentPresence } from './agent-presence.js';
|
|
3
5
|
import { checkFenceTarget, type FenceTarget } from './scope-fence.js';
|
|
4
6
|
import type { AgentPhase, AgentPresence, AgentPresenceSnapshot } from '../shared/agent-presence.js';
|
|
@@ -35,7 +37,11 @@ import type {
|
|
|
35
37
|
PmxAxWorkItem,
|
|
36
38
|
PmxAxWorkItemStatus,
|
|
37
39
|
} from './ax-state.js';
|
|
38
|
-
import type { AxTimelineQuery } from './canvas-db.js';
|
|
40
|
+
import type { AxTimelineQuery, CanvasBoard } from './canvas-db.js';
|
|
41
|
+
import type { Attachment, DocumentImport, ImportSection } from './document-import.js';
|
|
42
|
+
import { workbenchToken } from './workbench-auth.js';
|
|
43
|
+
import type { BackupFile, BackupStatus } from './backup.js';
|
|
44
|
+
import type { ExportManifest } from './board-export.js';
|
|
39
45
|
import { onFileNodeChanged } from './file-watcher.js';
|
|
40
46
|
import { findOpenCanvasPosition, computeGroupBounds } from './placement.js';
|
|
41
47
|
import { searchNodes, buildSpatialContext } from './spatial-analysis.js';
|
|
@@ -547,6 +553,16 @@ export class PmxCanvas extends EventEmitter {
|
|
|
547
553
|
);
|
|
548
554
|
}
|
|
549
555
|
|
|
556
|
+
getTour(): Tour {
|
|
557
|
+
return canvasState.getTour() ?? derivedTour(canvasState.getLayout().nodes);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
setTour(tour: Tour | null): void {
|
|
561
|
+
assertInsideFence('tour.set', { boardWide: true });
|
|
562
|
+
canvasState.setTour(tour);
|
|
563
|
+
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
564
|
+
}
|
|
565
|
+
|
|
550
566
|
clear(): void {
|
|
551
567
|
assertInsideFence('canvas.clear', { boardWide: true });
|
|
552
568
|
for (const node of canvasState.getLayout().nodes) {
|
|
@@ -588,7 +604,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
588
604
|
return canvasState.getAxState();
|
|
589
605
|
}
|
|
590
606
|
|
|
591
|
-
getAxContext(options?: { consumer?: string }): PmxAxContext {
|
|
607
|
+
getAxContext(options?: { consumer?: string }): PmxAxContext & { boardId: string | null } {
|
|
592
608
|
return buildCanvasAxContext(options?.consumer);
|
|
593
609
|
}
|
|
594
610
|
|
|
@@ -670,6 +686,218 @@ export class PmxCanvas extends EventEmitter {
|
|
|
670
686
|
return canvasState.getAxTimeline(query);
|
|
671
687
|
}
|
|
672
688
|
|
|
689
|
+
// ── Boards (plan 012) ─────────────────────────────────────────
|
|
690
|
+
// The SDK is the embedding host, so it may open and delete boards like the
|
|
691
|
+
// human's workbench. SDK writes made while no board is open (Home) land on a
|
|
692
|
+
// new board; open one first to write to an existing board.
|
|
693
|
+
|
|
694
|
+
listBoards(): { activeBoardId: string | null; boards: CanvasBoard[] } {
|
|
695
|
+
return { activeBoardId: canvasState.activeBoardId, boards: canvasState.listBoards() };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
listImports(boardId?: string): DocumentImport[] {
|
|
699
|
+
return canvasState.listDocumentImports(boardId);
|
|
700
|
+
}
|
|
701
|
+
readImport(
|
|
702
|
+
id: string,
|
|
703
|
+
): { import: DocumentImport; attachment: Attachment; downloadPath: string; bytes?: Uint8Array } | null {
|
|
704
|
+
const job = canvasState.getDocumentImport(id);
|
|
705
|
+
if (!job) return null;
|
|
706
|
+
const attachment = canvasState.getAttachment(job.attachmentId);
|
|
707
|
+
if (!attachment) return null;
|
|
708
|
+
const bytes =
|
|
709
|
+
attachment.size <= 2 * 1024 * 1024 ? (canvasState.readAttachmentBytes(attachment.id) ?? undefined) : undefined;
|
|
710
|
+
return {
|
|
711
|
+
import: job,
|
|
712
|
+
attachment,
|
|
713
|
+
downloadPath: `/api/canvas/attachments/${attachment.id}/bytes`,
|
|
714
|
+
...(bytes ? { bytes } : {}),
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
async submitImport(
|
|
718
|
+
id: string,
|
|
719
|
+
sections: ImportSection[],
|
|
720
|
+
agentDescription: string,
|
|
721
|
+
warnings?: string[],
|
|
722
|
+
): Promise<DocumentImport> {
|
|
723
|
+
const result = (await executeOperation(
|
|
724
|
+
'import.submit',
|
|
725
|
+
{ id, sections, agentDescription, warnings },
|
|
726
|
+
{ source: 'sdk' },
|
|
727
|
+
)) as { import: DocumentImport };
|
|
728
|
+
return result.import;
|
|
729
|
+
}
|
|
730
|
+
async markImportUnavailable(id: string, reason: string): Promise<DocumentImport> {
|
|
731
|
+
const result = (await executeOperation('import.unavailable', { id, reason }, { source: 'sdk' })) as {
|
|
732
|
+
import: DocumentImport;
|
|
733
|
+
};
|
|
734
|
+
return result.import;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
attachDocument(input: { boardId: string; name: string; mime?: string; bytes: Uint8Array; x?: number; y?: number }): {
|
|
738
|
+
attachment: Attachment;
|
|
739
|
+
import: DocumentImport;
|
|
740
|
+
nodeId: string;
|
|
741
|
+
} {
|
|
742
|
+
if (canvasState.activeBoardId !== input.boardId) throw new Error('The captured target board is not active.');
|
|
743
|
+
assertInsideFence('node.add', createTarget(input));
|
|
744
|
+
const attachment = canvasState.storeAttachment({ ...input, mime: input.mime ?? 'application/octet-stream' });
|
|
745
|
+
if (!attachment) throw new Error('Board not found.');
|
|
746
|
+
const { id: nodeId, node } = addCanvasNode({
|
|
747
|
+
type: 'file',
|
|
748
|
+
title: input.name,
|
|
749
|
+
data: { attachmentId: attachment.id, mime: attachment.mime, size: attachment.size },
|
|
750
|
+
x: input.x,
|
|
751
|
+
y: input.y,
|
|
752
|
+
defaultWidth: 440,
|
|
753
|
+
defaultHeight: 360,
|
|
754
|
+
fileMode: 'auto',
|
|
755
|
+
});
|
|
756
|
+
const job = canvasState.createDocumentImport(attachment.id, { x: node.position.x, y: node.position.y + 400 });
|
|
757
|
+
if (!job) throw new Error('Could not create import.');
|
|
758
|
+
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
759
|
+
return {
|
|
760
|
+
attachment,
|
|
761
|
+
import: job,
|
|
762
|
+
nodeId,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async requestImport(id: string, consent: true, trustedHumanToken: string): Promise<DocumentImport> {
|
|
767
|
+
if (trustedHumanToken !== workbenchToken) throw new Error('A trusted human token is required.');
|
|
768
|
+
const result = (await executeOperation(
|
|
769
|
+
'import.request',
|
|
770
|
+
{ id, consent },
|
|
771
|
+
{ source: 'sdk', fromWorkbench: true, humanAuthor: true },
|
|
772
|
+
)) as { import: DocumentImport };
|
|
773
|
+
return result.import;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async cancelImport(id: string, trustedHumanToken: string): Promise<DocumentImport> {
|
|
777
|
+
if (trustedHumanToken !== workbenchToken) throw new Error('A trusted human token is required.');
|
|
778
|
+
const result = (await executeOperation(
|
|
779
|
+
'import.cancel',
|
|
780
|
+
{ id },
|
|
781
|
+
{ source: 'sdk', fromWorkbench: true, humanAuthor: true },
|
|
782
|
+
)) as { import: DocumentImport };
|
|
783
|
+
return result.import;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async commitImport(id: string, trustedHumanToken: string): Promise<{ import: DocumentImport; nodeIds: string[] }> {
|
|
787
|
+
if (trustedHumanToken !== workbenchToken) throw new Error('A trusted human token is required.');
|
|
788
|
+
return (await executeOperation(
|
|
789
|
+
'import.commit',
|
|
790
|
+
{ id },
|
|
791
|
+
{ source: 'sdk', fromWorkbench: true, humanAuthor: true },
|
|
792
|
+
)) as { import: DocumentImport; nodeIds: string[] };
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async createBoard(name: string, category?: string): Promise<CanvasBoard> {
|
|
796
|
+
const result = (await executeOperation(
|
|
797
|
+
'board.create',
|
|
798
|
+
{ name, ...(category ? { category } : {}) },
|
|
799
|
+
{ source: 'sdk' },
|
|
800
|
+
)) as {
|
|
801
|
+
board: CanvasBoard;
|
|
802
|
+
};
|
|
803
|
+
return result.board;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/** Rename a board and/or file it under a category (`category: null` removes it). */
|
|
807
|
+
async updateBoard(id: string, patch: { name?: string; category?: string | null }): Promise<void> {
|
|
808
|
+
await executeOperation('board.update', { id, ...patch }, { source: 'sdk' });
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async setBoardReadme(id: string, readmeNodeId: string | null): Promise<void> {
|
|
812
|
+
await executeOperation('board.update', { id, readmeNodeId }, { source: 'sdk' });
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async createBoardFrom(input: {
|
|
816
|
+
sourceBoardId: string;
|
|
817
|
+
name: string;
|
|
818
|
+
category?: string;
|
|
819
|
+
nodeIds?: string[];
|
|
820
|
+
includeReadme?: boolean;
|
|
821
|
+
includeStructure?: boolean;
|
|
822
|
+
preview?: boolean;
|
|
823
|
+
}): Promise<Record<string, unknown>> {
|
|
824
|
+
return (await executeOperation('board.create-from', input, { source: 'sdk' })) as Record<string, unknown>;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
async searchLibrary(query: string, limit?: number): Promise<Record<string, unknown>> {
|
|
828
|
+
return (await executeOperation('search', { q: query, scope: 'library', limit }, { source: 'sdk' })) as Record<
|
|
829
|
+
string,
|
|
830
|
+
unknown
|
|
831
|
+
>;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/** Budgeted cross-board brief. Budget is measured in UTF-16 characters. */
|
|
835
|
+
async getContextBrief(
|
|
836
|
+
options: { consumer?: string; since?: number; budget?: number } = {},
|
|
837
|
+
): Promise<Record<string, unknown>> {
|
|
838
|
+
return (await executeOperation('context.get', options, { source: 'sdk' })) as Record<string, unknown>;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/** Open a board, or Home with null. */
|
|
842
|
+
async openBoard(id: string | null): Promise<void> {
|
|
843
|
+
await executeOperation(
|
|
844
|
+
'board.open',
|
|
845
|
+
{ id },
|
|
846
|
+
{ source: 'sdk', fromWorkbench: true, humanAuthor: true, suppressAutoGhost: true },
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async deleteBoard(id: string): Promise<void> {
|
|
851
|
+
await executeOperation(
|
|
852
|
+
'board.delete',
|
|
853
|
+
{ id },
|
|
854
|
+
{ source: 'sdk', fromWorkbench: true, humanAuthor: true, suppressAutoGhost: true },
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// ── Static export (plan 013) ─────────────────────────────────
|
|
859
|
+
|
|
860
|
+
/** Write a self-contained HTML file of a board (default: the open board). */
|
|
861
|
+
async exportBoard(
|
|
862
|
+
options: { board?: string; includeFiles?: boolean; includeDerivedText?: boolean } = {},
|
|
863
|
+
): Promise<{ path: string; url: string; bytes: number; manifest: ExportManifest }> {
|
|
864
|
+
return (await executeOperation('export.run', options, { source: 'sdk' })) as {
|
|
865
|
+
path: string;
|
|
866
|
+
url: string;
|
|
867
|
+
bytes: number;
|
|
868
|
+
manifest: ExportManifest;
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// ── Library backup (plan 012) ─────────────────────────────────
|
|
873
|
+
|
|
874
|
+
async getBackupStatus(): Promise<BackupStatus> {
|
|
875
|
+
return (await executeOperation('backup.status', {}, { source: 'sdk' })) as BackupStatus;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/** Back up every board now; `to` and `keep` default to the configured folder and count. */
|
|
879
|
+
async backup(options: { to?: string; keep?: number } = {}): Promise<{ backup: BackupFile; removed: string[] }> {
|
|
880
|
+
return (await executeOperation('backup.run', options, { source: 'sdk' })) as {
|
|
881
|
+
backup: BackupFile;
|
|
882
|
+
removed: string[];
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/** Back up on a schedule run by the server; `every: 'off'` stops it. */
|
|
887
|
+
async setBackupSchedule(options: { every: string; to?: string; keep?: number }): Promise<BackupStatus> {
|
|
888
|
+
return (await executeOperation('backup.schedule', options, { source: 'sdk' })) as BackupStatus;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/** Replace every board with a backup file; the current file is kept as `canvas.db.before-restore`. */
|
|
892
|
+
async restoreBackup(file: string): Promise<void> {
|
|
893
|
+
await executeOperation('backup.restore', { file }, { source: 'sdk' });
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/** The context read log: which canvas context each agent read and which pinned nodes reached it. */
|
|
897
|
+
getContextReads(limit?: number): ReturnType<typeof canvasState.getContextReads> {
|
|
898
|
+
return canvasState.getContextReads(limit);
|
|
899
|
+
}
|
|
900
|
+
|
|
673
901
|
listWorkItems(): PmxAxWorkItem[] {
|
|
674
902
|
return canvasState.getWorkItems();
|
|
675
903
|
}
|
|
@@ -1002,12 +1230,19 @@ export class PmxCanvas extends EventEmitter {
|
|
|
1002
1230
|
return result;
|
|
1003
1231
|
}
|
|
1004
1232
|
|
|
1005
|
-
getLayout(): CanvasLayout {
|
|
1233
|
+
getLayout(options?: { board?: string }): CanvasLayout {
|
|
1234
|
+
if (options?.board) {
|
|
1235
|
+
const read = canvasState.readBoard(options.board);
|
|
1236
|
+
if (!read) throw new Error(`Board "${options.board}" not found.`);
|
|
1237
|
+
return read.layout;
|
|
1238
|
+
}
|
|
1006
1239
|
return canvasState.getLayout();
|
|
1007
1240
|
}
|
|
1008
1241
|
|
|
1009
|
-
getNode(id: string): SdkCanvasNode | undefined {
|
|
1010
|
-
const node =
|
|
1242
|
+
getNode(id: string, options?: { board?: string }): SdkCanvasNode | undefined {
|
|
1243
|
+
const node = options?.board
|
|
1244
|
+
? this.getLayout(options).nodes.find((candidate) => candidate.id === id)
|
|
1245
|
+
: canvasState.getNode(id);
|
|
1011
1246
|
return node ? toSdkNode(node) : undefined;
|
|
1012
1247
|
}
|
|
1013
1248
|
|
|
@@ -1423,6 +1658,9 @@ export {
|
|
|
1423
1658
|
screenshotCanvasAutomationWebView,
|
|
1424
1659
|
} from './server.js';
|
|
1425
1660
|
export { canvasState } from './canvas-state.js';
|
|
1661
|
+
export type { CanvasBoard } from './canvas-db.js';
|
|
1662
|
+
export type { BackupFile, BackupStatus } from './backup.js';
|
|
1663
|
+
export type { ExportManifest } from './board-export.js';
|
|
1426
1664
|
export type {
|
|
1427
1665
|
CanvasAnnotation,
|
|
1428
1666
|
CanvasSnapshot,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { CanvasNodeState, CanvasEdge, CanvasLayout } from './canvas-state.js';
|
|
16
|
+
import { currentActor as getActor, setCurrentActor } from './attribution.js';
|
|
16
17
|
|
|
17
18
|
// ── Types ────────────────────────────────────────────────────────────
|
|
18
19
|
|
|
@@ -116,10 +117,13 @@ class MutationHistory {
|
|
|
116
117
|
record(entry: Omit<MutationEntry, 'id' | 'timestamp' | 'actor'> & { actor?: MutationActor }): void {
|
|
117
118
|
if (this._replaying) return;
|
|
118
119
|
|
|
120
|
+
const attribution = getActor();
|
|
121
|
+
const actor = attribution.actor === 'human' ? 'human' : 'agent';
|
|
122
|
+
const writer = actor === 'agent' ? (attribution.agentId ?? attribution.source) : undefined;
|
|
119
123
|
const full: MutationEntry = {
|
|
120
124
|
...entry,
|
|
121
|
-
actor: entry.actor ??
|
|
122
|
-
...((entry.writer ??
|
|
125
|
+
actor: entry.actor ?? actor,
|
|
126
|
+
...((entry.writer ?? writer) ? { writer: entry.writer ?? writer } : {}),
|
|
123
127
|
id: `mut-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
|
|
124
128
|
timestamp: new Date().toISOString(),
|
|
125
129
|
};
|
|
@@ -415,16 +419,9 @@ export function formatDiff(diff: SnapshotDiffResult): string {
|
|
|
415
419
|
|
|
416
420
|
export const mutationHistory = new MutationHistory();
|
|
417
421
|
|
|
418
|
-
/**
|
|
419
|
-
* Actor context for entries recorded while an operation runs. executeOperation
|
|
420
|
-
* sets it from the workbench marker around each op; writes outside the
|
|
421
|
-
* registry (the sync SDK) default to `agent`. Only the undo affordance reads
|
|
422
|
-
* it, so an async op interleaving with another request costs at most a
|
|
423
|
-
* mislabelled row — never a wrong undo.
|
|
424
|
-
*/
|
|
425
|
-
let currentActor: MutationActor | null = null;
|
|
426
|
-
let currentWriter: string | null = null;
|
|
422
|
+
/** Direct callers share the same async-local attribution as node mutations. */
|
|
427
423
|
export function setMutationActor(actor: MutationActor | null, writer: string | null = null): void {
|
|
428
|
-
|
|
429
|
-
|
|
424
|
+
setCurrentActor(
|
|
425
|
+
actor ? { actor, source: actor === 'human' ? 'browser' : 'sdk', ...(writer ? { agentId: writer } : {}) } : null,
|
|
426
|
+
);
|
|
430
427
|
}
|
|
@@ -123,6 +123,13 @@ const GATE_OPS: Array<{ op: string; kind: string; action: string }> = GATE_KINDS
|
|
|
123
123
|
);
|
|
124
124
|
|
|
125
125
|
export const compositeToolDefinitions: CompositeToolDefinition[] = [
|
|
126
|
+
{
|
|
127
|
+
toolName: 'canvas_import',
|
|
128
|
+
description:
|
|
129
|
+
'Agent-assisted document ingestion. List jobs, read attachment metadata/download bytes, submit bounded Markdown extraction, or report unavailable. Request, commit, and cancel are human-only HTTP actions.',
|
|
130
|
+
actionSummary: 'list | read | submit | unavailable',
|
|
131
|
+
actions: { list: 'import.list', read: 'import.read', submit: 'import.submit', unavailable: 'import.unavailable' },
|
|
132
|
+
},
|
|
126
133
|
{
|
|
127
134
|
// v0.4.0: the long-deferred snapshot composite (plan-006 #7). The name was
|
|
128
135
|
// held by the legacy save-snapshot standalone through v0.3.x; listing the
|
|
@@ -141,6 +148,19 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
|
|
|
141
148
|
diff: 'snapshot.diff',
|
|
142
149
|
},
|
|
143
150
|
},
|
|
151
|
+
{
|
|
152
|
+
toolName: 'canvas_board',
|
|
153
|
+
description:
|
|
154
|
+
'Boards in this workspace — the library of the human\'s and your working memory, filed in categories on Home. Your writes always go to the board the human has open (activeBoardId); on Home (no board open) your first write opens their most recent board, or a new one when there are none. Action "list" returns every board (with its category), most recently opened first; "get" reads one (default: the open board); "create" makes a new empty board WITHOUT opening it (optional category); "update" renames a board, files it under a category (category "" removes it), or designates its README via readmeNodeId. As part of normal board authoring, read the board metadata; if readmeNodeId is null, choose or create a concise markdown introduction explaining its purpose and key context, then designate that node with "update". Preserve an existing README unless the human asks to change it; humans can override the designation manually. Only the human opens, switches or deletes boards — if you need another board, ask them in the chat to open it.',
|
|
155
|
+
actionSummary: 'list | get | create | create-from | update',
|
|
156
|
+
actions: {
|
|
157
|
+
list: 'board.list',
|
|
158
|
+
get: 'board.get',
|
|
159
|
+
create: 'board.create',
|
|
160
|
+
'create-from': 'board.create-from',
|
|
161
|
+
update: 'board.update',
|
|
162
|
+
},
|
|
163
|
+
},
|
|
144
164
|
{
|
|
145
165
|
toolName: 'canvas_node',
|
|
146
166
|
description:
|
|
@@ -203,8 +223,10 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
|
|
|
203
223
|
toolName: 'canvas_view',
|
|
204
224
|
description:
|
|
205
225
|
'Canvas viewport and layout control. Action "arrange" auto-lays-out nodes (grid/columns/etc.); "focus" pans/zooms the viewport to a node; "fit" zooms to fit all nodes in view; "clear" removes every node and edge from the canvas; "remove-annotation" deletes a human-drawn annotation by id.',
|
|
206
|
-
actionSummary: 'arrange | focus | fit | clear | remove-annotation',
|
|
226
|
+
actionSummary: 'arrange | focus | fit | clear | remove-annotation | get-tour | set-tour',
|
|
207
227
|
actions: {
|
|
228
|
+
'get-tour': 'tour.get',
|
|
229
|
+
'set-tour': 'tour.set',
|
|
208
230
|
arrange: 'arrange',
|
|
209
231
|
focus: 'node.focus',
|
|
210
232
|
fit: 'view.fit',
|
|
@@ -215,9 +237,10 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
|
|
|
215
237
|
{
|
|
216
238
|
toolName: 'canvas_query',
|
|
217
239
|
description:
|
|
218
|
-
'Read the board cheapest-first. Action "
|
|
219
|
-
actionSummary: 'search | layout | validate',
|
|
240
|
+
'Read the board cheapest-first. Action "context" returns a budgeted cross-board brief with a durable consumer cursor (budget is UTF-16 characters); "search" finds nodes by title/content keywords; "layout" returns the full layout; "validate" checks geometry and endpoints.',
|
|
241
|
+
actionSummary: 'context | search | layout | validate',
|
|
220
242
|
actions: {
|
|
243
|
+
context: 'context.get',
|
|
221
244
|
search: 'search',
|
|
222
245
|
layout: 'layout.get',
|
|
223
246
|
validate: 'validate.get',
|
|
@@ -299,13 +322,14 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
|
|
|
299
322
|
{
|
|
300
323
|
toolName: 'canvas_ax_timeline',
|
|
301
324
|
description:
|
|
302
|
-
'Read or write the bounded PMX AX timeline. Action "read" returns recent agent-events, evidence, and steering messages plus counts (limit); "record-event" records a normalized agent-event (kind prompt|assistant-message|tool-start|tool-result|failure|approval|steering, summary, detail, data); "add-evidence" records an evidence item (kind logs|tool-result|screenshot|file|diff|test-output, title, body, ref); "send-steering" records a steering message (message). Timeline rows persist for diagnostics/continuity but are not restored by snapshots; exposed via canvas://ax-timeline.',
|
|
303
|
-
actionSummary: 'read | record-event | add-evidence | send-steering',
|
|
325
|
+
'Read or write the bounded PMX AX timeline. Action "read" returns recent agent-events, evidence, and steering messages plus counts (limit); "record-event" records a normalized agent-event (kind prompt|assistant-message|tool-start|tool-result|failure|approval|steering, summary, detail, data); "add-evidence" records an evidence item (kind logs|tool-result|screenshot|file|diff|test-output, title, body, ref); "send-steering" records a steering message (message); "reads" returns the context read log — which canvas context each agent read and which pinned nodes reached it — with a per-consumer summary (limit). Timeline rows persist for diagnostics/continuity but are not restored by snapshots; exposed via canvas://ax-timeline.',
|
|
326
|
+
actionSummary: 'read | record-event | add-evidence | send-steering | reads',
|
|
304
327
|
actions: {
|
|
305
328
|
read: 'ax.timeline.get',
|
|
306
329
|
'record-event': 'ax.event.record',
|
|
307
330
|
'add-evidence': 'ax.evidence.add',
|
|
308
331
|
'send-steering': 'ax.steer',
|
|
332
|
+
reads: 'ax.reads.list',
|
|
309
333
|
},
|
|
310
334
|
},
|
|
311
335
|
{
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { executeOperation, listOperations } from './registry.js';
|
|
10
10
|
import { SOURCE_LABEL_RE } from '../agent-presence.js';
|
|
11
11
|
import { OperationError } from './types.js';
|
|
12
|
+
import { isWorkbenchToken } from '../workbench-auth.js';
|
|
12
13
|
|
|
13
14
|
function responseJson(data: unknown, status = 200): Response {
|
|
14
15
|
return Response.json(data, {
|
|
@@ -88,15 +89,20 @@ export async function dispatchOperationRoute(req: Request, url: URL): Promise<Re
|
|
|
88
89
|
// The workbench marks its own HTTP calls: a human dragging/editing in
|
|
89
90
|
// the browser is not agent activity, so it never synthesizes an
|
|
90
91
|
// auto-ghost. Everything else (CLI, scripts, adapters) counts as agent.
|
|
91
|
-
const
|
|
92
|
+
const workbenchMarked = req.headers.get('x-pmx-workbench') === '1';
|
|
93
|
+
const humanAuthor = isWorkbenchToken(req.headers.get('x-pmx-workbench-token'));
|
|
92
94
|
// Presence writer label: adapters/CLI may identify themselves; plain
|
|
93
95
|
// HTTP callers read as 'api'.
|
|
94
96
|
const sourceHeader = req.headers.get('x-pmx-source');
|
|
95
97
|
const source = sourceHeader && SOURCE_LABEL_RE.test(sourceHeader) ? sourceHeader : 'api';
|
|
96
98
|
const result = await executeOperation(op.name, input, {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
// The legacy marker remains a UI-policy signal (ghost suppression).
|
|
100
|
+
// Only the unguessable boot token classifies authorship as human.
|
|
101
|
+
suppressAutoGhost: workbenchMarked,
|
|
102
|
+
fromWorkbench: workbenchMarked,
|
|
103
|
+
humanAuthor,
|
|
99
104
|
source,
|
|
105
|
+
proxiedRead: req.headers.get('x-pmx-proxied-read') === '1',
|
|
100
106
|
});
|
|
101
107
|
return responseJson(result, route.status ? route.status(result) : 200);
|
|
102
108
|
} catch (error) {
|
|
@@ -11,6 +11,9 @@ import { queryOperations } from './ops/query.js';
|
|
|
11
11
|
import { validateOperations } from './ops/validate.js';
|
|
12
12
|
import { annotationOperations } from './ops/annotation.js';
|
|
13
13
|
import { snapshotOperations } from './ops/snapshots.js';
|
|
14
|
+
import { boardOperations } from './ops/boards.js';
|
|
15
|
+
import { backupOperations } from './ops/backup.js';
|
|
16
|
+
import { exportOperations } from './ops/export.js';
|
|
14
17
|
import { jsonRenderOperations } from './ops/json-render.js';
|
|
15
18
|
import { axStateOperations } from './ops/ax-state.js';
|
|
16
19
|
import { axWorkOperations } from './ops/ax-work.js';
|
|
@@ -25,6 +28,7 @@ import { axReadOperations } from './ops/ax-read.js';
|
|
|
25
28
|
import { axPresenceOperations } from './ops/ax-presence.js';
|
|
26
29
|
import { humanPresenceOperations } from './ops/human-presence.js';
|
|
27
30
|
import { canvasWireOperations } from './ops/canvas-wire.js';
|
|
31
|
+
import { importOperations } from './ops/imports.js';
|
|
28
32
|
|
|
29
33
|
for (const op of [
|
|
30
34
|
...nodeOperations,
|
|
@@ -35,6 +39,9 @@ for (const op of [
|
|
|
35
39
|
...validateOperations,
|
|
36
40
|
...annotationOperations,
|
|
37
41
|
...snapshotOperations,
|
|
42
|
+
...boardOperations,
|
|
43
|
+
...backupOperations,
|
|
44
|
+
...exportOperations,
|
|
38
45
|
...jsonRenderOperations,
|
|
39
46
|
...axStateOperations,
|
|
40
47
|
...axWorkOperations,
|
|
@@ -49,6 +56,7 @@ for (const op of [
|
|
|
49
56
|
...axPresenceOperations,
|
|
50
57
|
...humanPresenceOperations,
|
|
51
58
|
...canvasWireOperations,
|
|
59
|
+
...importOperations,
|
|
52
60
|
]) {
|
|
53
61
|
registerOperation(op);
|
|
54
62
|
}
|
|
@@ -29,14 +29,19 @@ function defaultPresenceSource(name: string, input: Record<string, unknown>, sou
|
|
|
29
29
|
/** Runs operations in-process against the shared canvasState singleton. */
|
|
30
30
|
export class LocalOperationInvoker implements OperationInvoker {
|
|
31
31
|
private readonly source: string;
|
|
32
|
+
private readonly proxiedRead: boolean;
|
|
32
33
|
|
|
33
34
|
/** `source` labels this caller's agent presence ('mcp', 'sdk', …). */
|
|
34
35
|
constructor(source = 'api') {
|
|
35
36
|
this.source = agentSourceLabel(source);
|
|
37
|
+
this.proxiedRead = source === 'mcp';
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
async invoke(name: string, input: Record<string, unknown>): Promise<unknown> {
|
|
39
|
-
return await executeOperation(name, defaultPresenceSource(name, input, this.source), {
|
|
41
|
+
return await executeOperation(name, defaultPresenceSource(name, input, this.source), {
|
|
42
|
+
source: this.source,
|
|
43
|
+
proxiedRead: this.proxiedRead,
|
|
44
|
+
});
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
47
|
|
|
@@ -48,11 +53,13 @@ function toOperationErrorStatus(status: number): OperationErrorStatus {
|
|
|
48
53
|
export class HttpOperationInvoker implements OperationInvoker {
|
|
49
54
|
private readonly baseUrl: string;
|
|
50
55
|
private readonly source: string;
|
|
56
|
+
private readonly proxiedRead: boolean;
|
|
51
57
|
|
|
52
58
|
/** `source` labels this caller's agent presence on the server ('cli', 'mcp', …). */
|
|
53
59
|
constructor(baseUrl: string, source = 'api') {
|
|
54
60
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
55
61
|
this.source = agentSourceLabel(source);
|
|
62
|
+
this.proxiedRead = source === 'mcp';
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
async invoke(name: string, rawInput: Record<string, unknown>): Promise<unknown> {
|
|
@@ -79,7 +86,11 @@ export class HttpOperationInvoker implements OperationInvoker {
|
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
let url = `${this.baseUrl}${path}`;
|
|
82
|
-
const
|
|
89
|
+
const proxyHeaders: Record<string, string> = this.proxiedRead ? { 'x-pmx-proxied-read': '1' } : {};
|
|
90
|
+
const init: RequestInit = {
|
|
91
|
+
method: route.method,
|
|
92
|
+
headers: { 'x-pmx-source': this.source, ...proxyHeaders },
|
|
93
|
+
};
|
|
83
94
|
if (route.method === 'GET' || route.method === 'DELETE') {
|
|
84
95
|
const params = new URLSearchParams();
|
|
85
96
|
for (const [key, value] of Object.entries(rest)) {
|
|
@@ -88,7 +99,7 @@ export class HttpOperationInvoker implements OperationInvoker {
|
|
|
88
99
|
const query = params.toString();
|
|
89
100
|
if (query) url += `?${query}`;
|
|
90
101
|
} else {
|
|
91
|
-
init.headers = { 'Content-Type': 'application/json', 'x-pmx-source': this.source };
|
|
102
|
+
init.headers = { 'Content-Type': 'application/json', 'x-pmx-source': this.source, ...proxyHeaders };
|
|
92
103
|
init.body = JSON.stringify(rest);
|
|
93
104
|
}
|
|
94
105
|
|
|
@@ -10,12 +10,59 @@
|
|
|
10
10
|
import { z, type ZodRawShape, type ZodTypeAny } from 'zod';
|
|
11
11
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
12
12
|
import { getOperation, listOperations } from './registry.js';
|
|
13
|
-
import type
|
|
13
|
+
import { agentSourceLabel, type OperationInvoker } from './invoker.js';
|
|
14
14
|
import { OperationError, type Operation, type OperationMcpToolHost } from './types.js';
|
|
15
|
+
import { CONTEXT_READ_OPS, contextReadFromPayload, type ContextReadInput } from '../context-reads.js';
|
|
15
16
|
import { compositeFoldedOpNames, compositeToolDefinitions, type CompositeToolDefinition } from './composites.js';
|
|
16
17
|
|
|
17
18
|
export interface OperationToolHost extends OperationMcpToolHost {
|
|
18
19
|
invoker(): OperationInvoker;
|
|
20
|
+
prepareContextRead(board?: string): Promise<{ boardId: string | null; pinnedNodeIds: string[] }>;
|
|
21
|
+
recordContextRead(read: ContextReadInput): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function invokeMcpOperation(
|
|
25
|
+
host: OperationToolHost,
|
|
26
|
+
op: Operation,
|
|
27
|
+
opInput: Record<string, unknown>,
|
|
28
|
+
formatInput: Record<string, unknown>,
|
|
29
|
+
consumer: string | null,
|
|
30
|
+
) {
|
|
31
|
+
const board = typeof opInput.board === 'string' && opInput.board.trim() ? opInput.board.trim() : undefined;
|
|
32
|
+
const readTarget = CONTEXT_READ_OPS.has(op.name) ? await host.prepareContextRead(board) : null;
|
|
33
|
+
if (readTarget?.boardId && 'board' in op.inputShape) {
|
|
34
|
+
opInput = { ...opInput, board: readTarget.boardId };
|
|
35
|
+
formatInput = { ...formatInput, board: readTarget.boardId };
|
|
36
|
+
}
|
|
37
|
+
const result = await host.invoker().invoke(op.name, opInput);
|
|
38
|
+
const formatted = op.mcp?.formatResult
|
|
39
|
+
? await op.mcp.formatResult(result, formatInput, host)
|
|
40
|
+
: { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
|
|
41
|
+
if (readTarget) {
|
|
42
|
+
const text = formatted.content
|
|
43
|
+
.filter((item): item is Extract<(typeof formatted.content)[number], { type: 'text' }> => item.type === 'text')
|
|
44
|
+
.map((item) => item.text)
|
|
45
|
+
.join('');
|
|
46
|
+
try {
|
|
47
|
+
await host.recordContextRead(
|
|
48
|
+
contextReadFromPayload(
|
|
49
|
+
{
|
|
50
|
+
channel: 'operation',
|
|
51
|
+
resource: op.name,
|
|
52
|
+
source: agentSourceLabel('mcp'),
|
|
53
|
+
consumer,
|
|
54
|
+
agentId: null,
|
|
55
|
+
pinnedNodeIds: readTarget.pinnedNodeIds,
|
|
56
|
+
boardId: readTarget.boardId,
|
|
57
|
+
},
|
|
58
|
+
text,
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('[pmx-canvas mcp] recording a context read failed:', error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return formatted;
|
|
19
66
|
}
|
|
20
67
|
|
|
21
68
|
export function registerOperationTools(server: McpServer, getHost: () => Promise<OperationToolHost>): void {
|
|
@@ -43,11 +90,7 @@ export function registerOperationTools(server: McpServer, getHost: () => Promise
|
|
|
43
90
|
try {
|
|
44
91
|
const host = await getHost();
|
|
45
92
|
const opInput = tool.buildInput ? tool.buildInput(input) : input;
|
|
46
|
-
|
|
47
|
-
if (tool.formatResult) {
|
|
48
|
-
return await tool.formatResult(result, input, host);
|
|
49
|
-
}
|
|
50
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
|
|
93
|
+
return await invokeMcpOperation(host, op, opInput, input, server.server.getClientVersion()?.name ?? null);
|
|
51
94
|
} catch (error) {
|
|
52
95
|
return {
|
|
53
96
|
content: [{ type: 'text' as const, text: error instanceof Error ? error.message : String(error) }],
|
|
@@ -261,11 +304,7 @@ export function registerCompositeTools(
|
|
|
261
304
|
);
|
|
262
305
|
}
|
|
263
306
|
const opInput = op.mcp?.buildInput ? op.mcp.buildInput(rest) : rest;
|
|
264
|
-
|
|
265
|
-
if (op.mcp?.formatResult) {
|
|
266
|
-
return await op.mcp.formatResult(result, rest, host);
|
|
267
|
-
}
|
|
268
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
|
|
307
|
+
return await invokeMcpOperation(host, op, opInput, rest, server.server.getClientVersion()?.name ?? null);
|
|
269
308
|
} catch (error) {
|
|
270
309
|
return {
|
|
271
310
|
content: [{ type: 'text' as const, text: error instanceof Error ? error.message : String(error) }],
|