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
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import { z } from 'zod';
|
|
34
34
|
import { canvasState } from '../../canvas-state.js';
|
|
35
|
+
import { describeCreateSize, type CanvasSizeAdjustment } from '../../canvas-operations.js';
|
|
35
36
|
import {
|
|
36
37
|
closeMcpAppSession,
|
|
37
38
|
openMcpApp as openExternalMcpApp,
|
|
@@ -75,6 +76,7 @@ export interface OpenMcpAppCoreResult {
|
|
|
75
76
|
toolCallId: string;
|
|
76
77
|
sessionId: string;
|
|
77
78
|
resourceUri: string;
|
|
79
|
+
sizeAdjustment?: CanvasSizeAdjustment;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
/**
|
|
@@ -105,6 +107,7 @@ export async function openMcpAppCore(input: OpenMcpAppCoreInput, ctx: OperationC
|
|
|
105
107
|
throw new OperationError(`Node "${input.nodeId}" is not an external app node.`);
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
const boardGeneration = canvasState.boardGeneration;
|
|
108
111
|
const opened = await openExternalMcpApp({
|
|
109
112
|
transport: input.transport,
|
|
110
113
|
toolName: input.toolName,
|
|
@@ -112,6 +115,10 @@ export async function openMcpAppCore(input: OpenMcpAppCoreInput, ctx: OperationC
|
|
|
112
115
|
...(input.serverName ? { serverName: input.serverName } : {}),
|
|
113
116
|
...(typeof input.timeoutMs === 'number' ? { timeoutMs: input.timeoutMs } : {}),
|
|
114
117
|
});
|
|
118
|
+
if (canvasState.boardGeneration !== boardGeneration) {
|
|
119
|
+
closeMcpAppSession(opened.sessionId);
|
|
120
|
+
throw new OperationError('The board changed while the MCP app was opening.', 409);
|
|
121
|
+
}
|
|
115
122
|
const toolCallId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
116
123
|
const previousSessionId = targetNode?.data.appSessionId;
|
|
117
124
|
if (typeof previousSessionId === 'string' && previousSessionId.trim().length > 0) {
|
|
@@ -121,6 +128,7 @@ export async function openMcpAppCore(input: OpenMcpAppCoreInput, ctx: OperationC
|
|
|
121
128
|
const toolResult = isExcalidrawCreateView(opened.serverName, opened.toolName)
|
|
122
129
|
? ensureExcalidrawCheckpointId(opened.toolResult, nodeIdSeed)
|
|
123
130
|
: opened.toolResult;
|
|
131
|
+
const existingNodeIds = new Set(canvasState.getLayout().nodes.map((node) => node.id));
|
|
124
132
|
ctx.emit('ext-app-open', {
|
|
125
133
|
toolCallId,
|
|
126
134
|
nodeId: nodeIdSeed,
|
|
@@ -158,6 +166,11 @@ export async function openMcpAppCore(input: OpenMcpAppCoreInput, ctx: OperationC
|
|
|
158
166
|
getNode: (id) => canvasState.getNode(id),
|
|
159
167
|
listNodes: () => canvasState.getLayout().nodes,
|
|
160
168
|
});
|
|
169
|
+
const node = nodeId ? canvasState.getNode(nodeId) : undefined;
|
|
170
|
+
const sizeAdjustment =
|
|
171
|
+
node && !existingNodeIds.has(node.id)
|
|
172
|
+
? describeCreateSize(input.width, input.height, node.size.width, node.size.height, node.size)
|
|
173
|
+
: undefined;
|
|
161
174
|
return {
|
|
162
175
|
ok: true,
|
|
163
176
|
...(nodeId ? { id: nodeId } : {}),
|
|
@@ -165,6 +178,7 @@ export async function openMcpAppCore(input: OpenMcpAppCoreInput, ctx: OperationC
|
|
|
165
178
|
toolCallId,
|
|
166
179
|
sessionId: opened.sessionId,
|
|
167
180
|
resourceUri: opened.resourceUri,
|
|
181
|
+
...(sizeAdjustment ? { sizeAdjustment } : {}),
|
|
168
182
|
};
|
|
169
183
|
}
|
|
170
184
|
|
|
@@ -388,6 +402,7 @@ function webArtifactEnvelope(result: WebArtifactCanvasBuildResult, includeLogs:
|
|
|
388
402
|
...(typeof result.nodeId === 'string' ? { id: result.nodeId } : {}),
|
|
389
403
|
nodeId: result.nodeId,
|
|
390
404
|
url: result.url,
|
|
405
|
+
...(result.sizeAdjustment ? { sizeAdjustment: result.sizeAdjustment } : {}),
|
|
391
406
|
metadata: result.metadata,
|
|
392
407
|
logs: result.logs,
|
|
393
408
|
...(includeLogs ? { stdout: result.stdout, stderr: result.stderr } : {}),
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import { canvasState, type CanvasNodeState } from '../../canvas-state.js';
|
|
29
29
|
import { buildCodeGraphSummary } from '../../code-graph.js';
|
|
30
30
|
import { defineOperation, OperationError, type Operation, type OperationContext } from '../types.js';
|
|
31
|
+
import { readTargetBoard } from './boards.js';
|
|
31
32
|
import { normalizeAxNodeIds, normalizeAxSource } from './ax-shared.js';
|
|
32
33
|
import { isRecord } from './nodes.js';
|
|
33
34
|
|
|
@@ -153,19 +154,22 @@ const axSurfaceSnapshotOperation = defineOperation<z.infer<typeof emptySchema>,
|
|
|
153
154
|
|
|
154
155
|
// ── pinned-context.get ────────────────────────────────────────
|
|
155
156
|
|
|
156
|
-
const
|
|
157
|
+
const pinnedContextShape = { board: z.unknown().optional().describe('Board id to read without opening it.') };
|
|
158
|
+
const pinnedContextSchema = z.looseObject(pinnedContextShape);
|
|
159
|
+
const pinnedContextOperation = defineOperation<z.infer<typeof pinnedContextSchema>, Record<string, unknown>>({
|
|
157
160
|
name: 'pinned-context.get',
|
|
158
161
|
mutates: false,
|
|
159
|
-
input:
|
|
160
|
-
inputShape:
|
|
162
|
+
input: pinnedContextSchema,
|
|
163
|
+
inputShape: pinnedContextShape,
|
|
161
164
|
http: {
|
|
162
165
|
method: 'GET',
|
|
163
166
|
path: '/api/canvas/pinned-context',
|
|
164
167
|
},
|
|
165
|
-
handler: () => {
|
|
166
|
-
const
|
|
168
|
+
handler: ({ board }) => {
|
|
169
|
+
const target = readTargetBoard(board);
|
|
170
|
+
const pinnedIds = Array.from(target.pinnedNodeIds);
|
|
167
171
|
const nodes = pinnedIds
|
|
168
|
-
.map((id) =>
|
|
172
|
+
.map((id) => target.layout.nodes.find((node) => node.id === id))
|
|
169
173
|
.filter((node): node is CanvasNodeState => node !== undefined);
|
|
170
174
|
const preamble =
|
|
171
175
|
pinnedIds.length > 0 ? buildAgentContextPreamble(nodes, { defaultTextLength: 700, webpageTextLength: 1600 }) : '';
|
|
@@ -51,6 +51,7 @@ import { canvasState } from '../../canvas-state.js';
|
|
|
51
51
|
import { buildPendingAxActivity, isAxEventKind, isAxEvidenceKind } from '../../ax-state.js';
|
|
52
52
|
import type { PmxAxEventKind, PmxAxEvidenceKind } from '../../ax-state.js';
|
|
53
53
|
import { waitForAxCondition } from '../../ax-wait.js';
|
|
54
|
+
import { CONTEXT_READ_CHANNELS } from '../../context-reads.js';
|
|
54
55
|
import { defineOperation, OperationError, type Operation } from '../types.js';
|
|
55
56
|
import { isRecord } from './nodes.js';
|
|
56
57
|
import {
|
|
@@ -462,6 +463,78 @@ const axCommandInvokeOperation = defineOperation<z.infer<typeof axCommandInvokeS
|
|
|
462
463
|
},
|
|
463
464
|
});
|
|
464
465
|
|
|
466
|
+
// ── ax.reads.list / ax.reads.record (context read instrumentation) ──
|
|
467
|
+
|
|
468
|
+
const axReadsListShape = {
|
|
469
|
+
limit: z.unknown().optional().describe('Max reads to return, newest first (default 50, max 500).'),
|
|
470
|
+
};
|
|
471
|
+
const axReadsListSchema = z.looseObject(axReadsListShape);
|
|
472
|
+
|
|
473
|
+
const axReadsListOperation = defineOperation<z.infer<typeof axReadsListSchema>, Record<string, unknown>>({
|
|
474
|
+
name: 'ax.reads.list',
|
|
475
|
+
mutates: false,
|
|
476
|
+
input: axReadsListSchema,
|
|
477
|
+
inputShape: axReadsListShape,
|
|
478
|
+
http: {
|
|
479
|
+
method: 'GET',
|
|
480
|
+
path: '/api/canvas/ax/context-reads',
|
|
481
|
+
},
|
|
482
|
+
mcp: {
|
|
483
|
+
toolName: 'canvas_get_context_reads',
|
|
484
|
+
description:
|
|
485
|
+
'Read the context read log: which canvas context each agent read, and which pinned nodes were in what it received. Summary is per consumer.',
|
|
486
|
+
extraShape: {
|
|
487
|
+
limit: z.number().optional().describe('Max reads to return, newest first (default 50, max 500).'),
|
|
488
|
+
},
|
|
489
|
+
formatResult: axJsonResult,
|
|
490
|
+
},
|
|
491
|
+
handler: (input) => {
|
|
492
|
+
const limit = Number(input.limit ?? '');
|
|
493
|
+
return { ok: true, ...canvasState.getContextReads(Number.isFinite(limit) && limit > 0 ? limit : undefined) };
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const axReadsRecordShape = {
|
|
498
|
+
channel: z.enum(CONTEXT_READ_CHANNELS).describe('How the read reached the agent.'),
|
|
499
|
+
resource: z.string().min(1).describe('Resource URI or operation name that was read.'),
|
|
500
|
+
consumer: z.string().nullable().optional().describe('Reader identity (MCP client name, adapter host).'),
|
|
501
|
+
agentId: z.string().nullable().optional().describe('Agent id when known.'),
|
|
502
|
+
source: z.string().optional().describe('Transport label (default "api").'),
|
|
503
|
+
pinnedNodeIds: z.array(z.string()).optional().describe('Pins at read time; defaults to the current pins.'),
|
|
504
|
+
deliveredNodeIds: z.array(z.string()).describe('Pinned ids present in what the agent received.'),
|
|
505
|
+
bytes: z.number().int().nonnegative().describe('Size of what the agent received.'),
|
|
506
|
+
boardId: z.string().nullable().optional().describe('Board that was read, captured before delivery.'),
|
|
507
|
+
};
|
|
508
|
+
const axReadsRecordSchema = z.object(axReadsRecordShape);
|
|
509
|
+
|
|
510
|
+
/** For proxies (an MCP server attached to this daemon, host adapters) that record the read the agent made. */
|
|
511
|
+
const axReadsRecordOperation = defineOperation<z.infer<typeof axReadsRecordSchema>, Record<string, unknown>>({
|
|
512
|
+
name: 'ax.reads.record',
|
|
513
|
+
mutates: false,
|
|
514
|
+
input: axReadsRecordSchema,
|
|
515
|
+
inputShape: axReadsRecordShape,
|
|
516
|
+
http: {
|
|
517
|
+
method: 'POST',
|
|
518
|
+
path: '/api/canvas/ax/context-reads',
|
|
519
|
+
},
|
|
520
|
+
handler: (input) => {
|
|
521
|
+
const read = canvasState.recordContextRead(
|
|
522
|
+
{
|
|
523
|
+
channel: input.channel,
|
|
524
|
+
resource: input.resource,
|
|
525
|
+
source: input.source ?? 'api',
|
|
526
|
+
consumer: input.consumer ?? null,
|
|
527
|
+
agentId: input.agentId ?? null,
|
|
528
|
+
pinnedNodeIds: input.pinnedNodeIds ?? [...canvasState.contextPinnedNodeIds],
|
|
529
|
+
deliveredNodeIds: input.deliveredNodeIds,
|
|
530
|
+
bytes: input.bytes,
|
|
531
|
+
},
|
|
532
|
+
input.boardId === undefined ? canvasState.activeBoardId : input.boardId,
|
|
533
|
+
);
|
|
534
|
+
return { ok: true, read };
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
|
|
465
538
|
export const axTimelineOperations: Operation[] = [
|
|
466
539
|
axEventRecordOperation,
|
|
467
540
|
axEvidenceAddOperation,
|
|
@@ -470,4 +543,6 @@ export const axTimelineOperations: Operation[] = [
|
|
|
470
543
|
axDeliveryPendingOperation,
|
|
471
544
|
axDeliveryMarkOperation,
|
|
472
545
|
axCommandInvokeOperation,
|
|
546
|
+
axReadsListOperation,
|
|
547
|
+
axReadsRecordOperation,
|
|
473
548
|
];
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library backup (plan 012): backup.status / backup.run / backup.schedule /
|
|
3
|
+
* backup.restore. HTTP, CLI and SDK only — backing up and restoring are the
|
|
4
|
+
* human's maintenance, not agent tools, so there is no MCP surface.
|
|
5
|
+
*
|
|
6
|
+
* This module must never import server.ts or index.ts.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import {
|
|
10
|
+
assertRestorableBackup,
|
|
11
|
+
getBackupStatus,
|
|
12
|
+
parseBackupInterval,
|
|
13
|
+
runBackup,
|
|
14
|
+
setBackupSchedule,
|
|
15
|
+
} from '../../backup.js';
|
|
16
|
+
import { restoreCanvasLibrary } from '../../canvas-operations.js';
|
|
17
|
+
import { canvasState } from '../../canvas-state.js';
|
|
18
|
+
import { defineOperation, OperationError, type Operation } from '../types.js';
|
|
19
|
+
import { boardsPayload } from './boards.js';
|
|
20
|
+
|
|
21
|
+
function requireDatabase(): void {
|
|
22
|
+
if (!canvasState.databasePath) throw new OperationError('Backups need a workspace database.', 409);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function positiveInteger(value: unknown, field: string): number | undefined {
|
|
26
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
27
|
+
const parsed = Number(value);
|
|
28
|
+
if (!Number.isInteger(parsed) || parsed < 1)
|
|
29
|
+
throw new OperationError(`${field} must be a whole number of 1 or more.`);
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const statusShape = {};
|
|
34
|
+
const statusSchema = z.looseObject(statusShape);
|
|
35
|
+
|
|
36
|
+
const backupStatusOperation = defineOperation<z.infer<typeof statusSchema>, Record<string, unknown>>({
|
|
37
|
+
name: 'backup.status',
|
|
38
|
+
mutates: false,
|
|
39
|
+
input: statusSchema,
|
|
40
|
+
inputShape: statusShape,
|
|
41
|
+
http: { method: 'GET', path: '/api/canvas/backup' },
|
|
42
|
+
handler: () => {
|
|
43
|
+
requireDatabase();
|
|
44
|
+
return { ok: true, ...getBackupStatus() };
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const runShape = {
|
|
49
|
+
to: z.unknown().optional().describe('Folder to write the backup to (default: the configured folder)'),
|
|
50
|
+
keep: z.unknown().optional().describe('How many backups to keep in that folder'),
|
|
51
|
+
};
|
|
52
|
+
const runSchema = z.looseObject(runShape);
|
|
53
|
+
|
|
54
|
+
const backupRunOperation = defineOperation<z.infer<typeof runSchema>, Record<string, unknown>>({
|
|
55
|
+
name: 'backup.run',
|
|
56
|
+
mutates: false,
|
|
57
|
+
input: runSchema,
|
|
58
|
+
inputShape: runShape,
|
|
59
|
+
http: { method: 'POST', path: '/api/canvas/backup' },
|
|
60
|
+
handler: (input) => {
|
|
61
|
+
requireDatabase();
|
|
62
|
+
const result = runBackup({
|
|
63
|
+
...(typeof input.to === 'string' && input.to.trim() ? { folder: input.to.trim() } : {}),
|
|
64
|
+
...(positiveInteger(input.keep, 'keep') ? { keep: positiveInteger(input.keep, 'keep') } : {}),
|
|
65
|
+
});
|
|
66
|
+
return { ok: true, ...result, status: getBackupStatus() };
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const scheduleShape = {
|
|
71
|
+
every: z.unknown().optional().describe('Interval like "24h", "30m", "7d"; "off" or null stops scheduled backups'),
|
|
72
|
+
to: z.unknown().optional().describe('Backup folder'),
|
|
73
|
+
keep: z.unknown().optional().describe('How many backups to keep'),
|
|
74
|
+
};
|
|
75
|
+
const scheduleSchema = z.looseObject(scheduleShape);
|
|
76
|
+
|
|
77
|
+
const backupScheduleOperation = defineOperation<z.infer<typeof scheduleSchema>, Record<string, unknown>>({
|
|
78
|
+
name: 'backup.schedule',
|
|
79
|
+
mutates: false,
|
|
80
|
+
input: scheduleSchema,
|
|
81
|
+
inputShape: scheduleShape,
|
|
82
|
+
http: { method: 'POST', path: '/api/canvas/backup/schedule' },
|
|
83
|
+
handler: (input) => {
|
|
84
|
+
requireDatabase();
|
|
85
|
+
const off = input.every === null || input.every === 'off';
|
|
86
|
+
const everyMs = off ? null : parseBackupInterval(String(input.every ?? ''));
|
|
87
|
+
if (!off && everyMs === null) throw new OperationError('every must look like "30m", "24h" or "7d", or be "off".');
|
|
88
|
+
try {
|
|
89
|
+
const status = setBackupSchedule({
|
|
90
|
+
everyMs,
|
|
91
|
+
...(typeof input.to === 'string' && input.to.trim() ? { folder: input.to.trim() } : {}),
|
|
92
|
+
...(positiveInteger(input.keep, 'keep') ? { keep: positiveInteger(input.keep, 'keep') } : {}),
|
|
93
|
+
});
|
|
94
|
+
return { ok: true, ...status };
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw new OperationError(error instanceof Error ? error.message : String(error));
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const restoreShape = {
|
|
102
|
+
file: z.unknown().optional().describe('Backup file to restore'),
|
|
103
|
+
};
|
|
104
|
+
const restoreSchema = z.looseObject(restoreShape);
|
|
105
|
+
|
|
106
|
+
const backupRestoreOperation = defineOperation<z.infer<typeof restoreSchema>, Record<string, unknown>>({
|
|
107
|
+
name: 'backup.restore',
|
|
108
|
+
mutates: false,
|
|
109
|
+
input: restoreSchema,
|
|
110
|
+
inputShape: restoreShape,
|
|
111
|
+
http: { method: 'POST', path: '/api/canvas/backup/restore' },
|
|
112
|
+
handler: (input, ctx) => {
|
|
113
|
+
requireDatabase();
|
|
114
|
+
const file = typeof input.file === 'string' ? input.file.trim() : '';
|
|
115
|
+
if (!file) throw new OperationError('file is required.');
|
|
116
|
+
try {
|
|
117
|
+
assertRestorableBackup(file);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
throw new OperationError(error instanceof Error ? error.message : String(error));
|
|
120
|
+
}
|
|
121
|
+
const previous = `${canvasState.databasePath}.before-restore`;
|
|
122
|
+
restoreCanvasLibrary(file);
|
|
123
|
+
// `reloaded` tells open tabs to take the board fresh even when its id is unchanged.
|
|
124
|
+
ctx.emit('boards-changed', { ...boardsPayload(), reloaded: true });
|
|
125
|
+
return { ok: true, restored: file, previous, ...boardsPayload() };
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
export const backupOperations: Operation[] = [
|
|
130
|
+
backupStatusOperation,
|
|
131
|
+
backupRunOperation,
|
|
132
|
+
backupScheduleOperation,
|
|
133
|
+
backupRestoreOperation,
|
|
134
|
+
];
|
|
@@ -145,7 +145,11 @@ function createInternalBatchNode(args: Record<string, unknown>): Record<string,
|
|
|
145
145
|
defaultHeight: 200,
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
-
return {
|
|
148
|
+
return {
|
|
149
|
+
ok: true,
|
|
150
|
+
...serializeCanvasNodeCompact(created.node),
|
|
151
|
+
...(created.sizeAdjustment ? { sizeAdjustment: created.sizeAdjustment } : {}),
|
|
152
|
+
};
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
/**
|
|
@@ -165,7 +169,7 @@ function shapeBatchEntry(op: string, result: unknown): Record<string, unknown> {
|
|
|
165
169
|
const id = typeof body.id === 'string' ? body.id : '';
|
|
166
170
|
const node = id ? canvasState.getNode(id) : undefined;
|
|
167
171
|
const extras: Record<string, unknown> = {};
|
|
168
|
-
for (const key of ['fetch', 'error', 'url', 'spec']) {
|
|
172
|
+
for (const key of ['fetch', 'error', 'url', 'spec', 'sizeAdjustment']) {
|
|
169
173
|
if (body[key] !== undefined) extras[key] = body[key];
|
|
170
174
|
}
|
|
171
175
|
if (node) return { ok: true, ...serializeCanvasNodeCompact(node), ...extras };
|
|
@@ -183,7 +187,11 @@ function shapeBatchEntry(op: string, result: unknown): Record<string, unknown> {
|
|
|
183
187
|
if (op === 'group.add') {
|
|
184
188
|
const groupId = typeof body.groupId === 'string' ? body.groupId : '';
|
|
185
189
|
const group = groupId ? canvasState.getNode(groupId) : undefined;
|
|
186
|
-
return {
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
...(group ? serializeCanvasNode(group) : { id: groupId }),
|
|
193
|
+
...(body.sizeAdjustment ? { sizeAdjustment: body.sizeAdjustment } : {}),
|
|
194
|
+
};
|
|
187
195
|
}
|
|
188
196
|
|
|
189
197
|
// pin.set/add/remove: legacy pushed { ok:true, ...{ count, nodeIds } }; wire shape drops nodeIds.
|
|
@@ -242,7 +250,14 @@ async function runBatch(operations: BatchEntry[]): Promise<BatchEnvelope> {
|
|
|
242
250
|
// ghost-cursor exemption for batch operations). No `fromWorkbench`: the
|
|
243
251
|
// browser never issues batches, so inner writes are agent writes and the
|
|
244
252
|
// scope fence applies to each of them.
|
|
245
|
-
const
|
|
253
|
+
const pending = executeOperation(dispatch.name, dispatch.args, { suppressAutoGhost: true });
|
|
254
|
+
// The first write may synchronously open a board from Home. Capture after
|
|
255
|
+
// dispatch, then refuse to continue a partially completed batch elsewhere.
|
|
256
|
+
const boardGeneration = canvasState.boardGeneration;
|
|
257
|
+
const raw = await pending;
|
|
258
|
+
if (canvasState.boardGeneration !== boardGeneration) {
|
|
259
|
+
throw new Error('The board changed while the batch was running.');
|
|
260
|
+
}
|
|
246
261
|
const result = shapeBatchEntry(operation.op, raw);
|
|
247
262
|
results.push(result);
|
|
248
263
|
if (typeof operation.assign === 'string' && operation.assign.trim().length > 0) {
|
|
@@ -305,6 +320,7 @@ function compactBatchValue(value: unknown): unknown {
|
|
|
305
320
|
'content',
|
|
306
321
|
'position',
|
|
307
322
|
'size',
|
|
323
|
+
'sizeAdjustment',
|
|
308
324
|
'fetch',
|
|
309
325
|
'error',
|
|
310
326
|
'from',
|