pmx-canvas 0.6.5 → 0.7.1
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 +139 -14
- package/CHANGELOG.md +70 -0
- package/Readme.md +32 -6
- package/dist/canvas/global.css +510 -31
- package/dist/canvas/index.js +92 -77
- package/dist/types/cli/agent.d.ts +3 -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/daemon.d.ts +5 -5
- package/dist/types/cli/shared.d.ts +3 -1
- 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/nodes/AttachmentNode.d.ts +6 -0
- package/dist/types/client/nodes/BoardNode.d.ts +5 -0
- 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/session-store.d.ts +7 -0
- package/dist/types/client/types.d.ts +2 -2
- package/dist/types/json-render/catalog.d.ts +1 -1
- package/dist/types/json-render/charts/definitions.d.ts +1 -1
- package/dist/types/json-render/server.d.ts +6 -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 +63 -9
- package/dist/types/server/canvas-operations.d.ts +23 -0
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +140 -3
- 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/index.d.ts +103 -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/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/nodes.d.ts +1 -1
- 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/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/docs/RELEASE.md +5 -0
- package/docs/cli.md +73 -0
- package/docs/http-api.md +198 -2
- package/docs/mcp.md +71 -6
- package/docs/node-types.md +26 -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 +68 -11
- package/skills/pmx-canvas/evals/evals.json +130 -0
- package/skills/pmx-canvas/references/full-reference.md +43 -7
- 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/references/tours-and-recording.md +69 -0
- package/skills/pmx-canvas-orchestration/SKILL.md +5 -0
- package/skills/pmx-canvas-testing/SKILL.md +11 -2
- package/src/cli/agent.ts +4 -0
- package/src/cli/commands/apps.ts +1 -1
- 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 +37 -0
- package/src/cli/commands/nodes.ts +4 -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/view.ts +7 -5
- package/src/cli/daemon.ts +37 -7
- package/src/cli/index.ts +19 -9
- package/src/cli/shared.ts +11 -2
- package/src/client/App.tsx +21 -4
- package/src/client/canvas/AttentionToast.tsx +5 -3
- 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 +0 -1
- package/src/client/canvas/CanvasViewport.tsx +3 -0
- package/src/client/canvas/CommandPalette.tsx +107 -4
- package/src/client/canvas/ContextMenu.tsx +4 -1
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -0
- package/src/client/canvas/ExportDialog.tsx +199 -0
- package/src/client/canvas/HomeView.tsx +277 -0
- package/src/client/canvas/Presentation.tsx +39 -9
- package/src/client/canvas/SelectionBar.tsx +0 -2
- package/src/client/canvas/SessionPanel.tsx +28 -1
- package/src/client/canvas/ToolRail.tsx +38 -3
- package/src/client/canvas/TopBar.tsx +34 -18
- package/src/client/canvas/import-files.ts +19 -1
- package/src/client/canvas/kind-colors.ts +1 -0
- package/src/client/icons.tsx +2 -0
- package/src/client/nodes/AttachmentNode.tsx +145 -0
- package/src/client/nodes/BoardNode.tsx +35 -0
- package/src/client/nodes/FileNode.tsx +6 -0
- package/src/client/nodes/MarkdownNode.tsx +47 -19
- 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/attention-bridge.ts +3 -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/node-factory.ts +1 -0
- package/src/client/state/session-store.ts +5 -0
- package/src/client/state/sse-bridge.ts +52 -6
- package/src/client/theme/global.css +510 -31
- package/src/client/types.ts +2 -0
- package/src/json-render/server.ts +16 -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/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 +599 -107
- package/src/server/canvas-operations.ts +68 -9
- package/src/server/canvas-provenance.ts +1 -0
- package/src/server/canvas-schema.ts +12 -0
- package/src/server/canvas-state.ts +774 -45
- package/src/server/context-brief.ts +372 -0
- package/src/server/context-reads.ts +238 -0
- package/src/server/demo-state.json +659 -2
- package/src/server/document-import.ts +223 -0
- package/src/server/index.ts +231 -5
- package/src/server/mutation-history.ts +10 -13
- package/src/server/operations/composites.ts +26 -4
- 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 +5 -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 +8 -1
- 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/imports.ts +236 -0
- package/src/server/operations/ops/nodes.ts +57 -5
- package/src/server/operations/ops/query.ts +206 -9
- package/src/server/operations/registry.ts +123 -31
- package/src/server/operations/types.ts +1 -1
- package/src/server/server.ts +146 -2
- package/src/server/spatial-analysis.ts +9 -17
- package/src/server/trace-manager.ts +19 -0
- package/src/server/web-artifacts.ts +2 -1
- package/src/server/webpage-node.ts +173 -13
- package/src/server/workbench-auth.ts +11 -0
- package/src/shared/boards.ts +18 -0
- package/src/shared/node-sizes.ts +1 -0
|
@@ -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) }],
|
|
@@ -107,6 +107,7 @@ export async function openMcpAppCore(input: OpenMcpAppCoreInput, ctx: OperationC
|
|
|
107
107
|
throw new OperationError(`Node "${input.nodeId}" is not an external app node.`);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
const boardGeneration = canvasState.boardGeneration;
|
|
110
111
|
const opened = await openExternalMcpApp({
|
|
111
112
|
transport: input.transport,
|
|
112
113
|
toolName: input.toolName,
|
|
@@ -114,6 +115,10 @@ export async function openMcpAppCore(input: OpenMcpAppCoreInput, ctx: OperationC
|
|
|
114
115
|
...(input.serverName ? { serverName: input.serverName } : {}),
|
|
115
116
|
...(typeof input.timeoutMs === 'number' ? { timeoutMs: input.timeoutMs } : {}),
|
|
116
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
|
+
}
|
|
117
122
|
const toolCallId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
118
123
|
const previousSessionId = targetNode?.data.appSessionId;
|
|
119
124
|
if (typeof previousSessionId === 'string' && previousSessionId.trim().length > 0) {
|
|
@@ -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
|
+
];
|
|
@@ -250,7 +250,14 @@ async function runBatch(operations: BatchEntry[]): Promise<BatchEnvelope> {
|
|
|
250
250
|
// ghost-cursor exemption for batch operations). No `fromWorkbench`: the
|
|
251
251
|
// browser never issues batches, so inner writes are agent writes and the
|
|
252
252
|
// scope fence applies to each of them.
|
|
253
|
-
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
|
+
}
|
|
254
261
|
const result = shapeBatchEntry(operation.op, raw);
|
|
255
262
|
results.push(result);
|
|
256
263
|
if (typeof operation.assign === 'string' && operation.assign.trim().length > 0) {
|