pmx-canvas 0.6.5 → 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 +54 -0
- package/Readme.md +29 -5
- package/dist/canvas/global.css +440 -24
- package/dist/canvas/index.js +90 -75
- 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/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 +68 -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 +53 -4
- 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-orchestration/SKILL.md +5 -0
- package/skills/pmx-canvas-testing/SKILL.md +4 -0
- package/src/cli/agent.ts +4 -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 +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/index.ts +19 -9
- package/src/client/App.tsx +21 -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/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/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 +148 -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/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 +49 -5
- package/src/client/theme/global.css +440 -24
- 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 +220 -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
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
5
|
+
export const MAX_INLINE_ATTACHMENT_BYTES = 2 * 1024 * 1024;
|
|
6
|
+
export const MAX_ATTACHMENT_NAME_LENGTH = 255;
|
|
7
|
+
export const MAX_IMPORT_SECTIONS = 32;
|
|
8
|
+
export const MAX_IMPORT_WARNINGS = 20;
|
|
9
|
+
export const MAX_IMPORT_TEXT = 200_000;
|
|
10
|
+
export const MAX_IMPORT_TITLE_LENGTH = 500;
|
|
11
|
+
export const MAX_IMPORT_REFERENCE_LENGTH = 2_000;
|
|
12
|
+
export const MAX_IMPORT_WARNING_LENGTH = 2_000;
|
|
13
|
+
export const MAX_IMPORT_AGENT_DESCRIPTION_LENGTH = 4_000;
|
|
14
|
+
export const MAX_IMPORT_REASON_LENGTH = 4_000;
|
|
15
|
+
|
|
16
|
+
export interface Attachment {
|
|
17
|
+
id: string;
|
|
18
|
+
boardId: string;
|
|
19
|
+
sha256: string;
|
|
20
|
+
name: string;
|
|
21
|
+
mime: string;
|
|
22
|
+
size: number;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ImportSection {
|
|
27
|
+
title: string;
|
|
28
|
+
markdown: string;
|
|
29
|
+
reference?: string;
|
|
30
|
+
}
|
|
31
|
+
export type DocumentImportStatus = 'attached' | 'requested' | 'drafted' | 'unavailable' | 'cancelled' | 'committed';
|
|
32
|
+
export interface DocumentImport {
|
|
33
|
+
id: string;
|
|
34
|
+
boardId: string;
|
|
35
|
+
attachmentId: string;
|
|
36
|
+
status: DocumentImportStatus;
|
|
37
|
+
position: { x: number; y: number };
|
|
38
|
+
sections: ImportSection[];
|
|
39
|
+
warnings: string[];
|
|
40
|
+
agentDescription: string | null;
|
|
41
|
+
reason: string | null;
|
|
42
|
+
committedNodeIds: string[];
|
|
43
|
+
createdAt: string;
|
|
44
|
+
updatedAt: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface AttachmentRow {
|
|
48
|
+
id: string;
|
|
49
|
+
board_id: string;
|
|
50
|
+
sha256: string;
|
|
51
|
+
name: string;
|
|
52
|
+
mime: string;
|
|
53
|
+
size: number;
|
|
54
|
+
created_at: string;
|
|
55
|
+
}
|
|
56
|
+
interface ImportRow {
|
|
57
|
+
id: string;
|
|
58
|
+
board_id: string;
|
|
59
|
+
attachment_id: string;
|
|
60
|
+
status: DocumentImportStatus;
|
|
61
|
+
position_x: number;
|
|
62
|
+
position_y: number;
|
|
63
|
+
sections: string | null;
|
|
64
|
+
warnings: string | null;
|
|
65
|
+
agent_description: string | null;
|
|
66
|
+
reason: string | null;
|
|
67
|
+
committed_node_ids: string | null;
|
|
68
|
+
created_at: string;
|
|
69
|
+
updated_at: string;
|
|
70
|
+
}
|
|
71
|
+
const attachment = (r: AttachmentRow): Attachment => ({
|
|
72
|
+
id: r.id,
|
|
73
|
+
boardId: r.board_id,
|
|
74
|
+
sha256: r.sha256,
|
|
75
|
+
name: r.name,
|
|
76
|
+
mime: r.mime,
|
|
77
|
+
size: r.size,
|
|
78
|
+
createdAt: r.created_at,
|
|
79
|
+
});
|
|
80
|
+
const imported = (r: ImportRow): DocumentImport => ({
|
|
81
|
+
id: r.id,
|
|
82
|
+
boardId: r.board_id,
|
|
83
|
+
attachmentId: r.attachment_id,
|
|
84
|
+
status: r.status,
|
|
85
|
+
position: { x: r.position_x, y: r.position_y },
|
|
86
|
+
sections: r.sections ? (JSON.parse(r.sections) as ImportSection[]) : [],
|
|
87
|
+
warnings: r.warnings ? (JSON.parse(r.warnings) as string[]) : [],
|
|
88
|
+
agentDescription: r.agent_description,
|
|
89
|
+
reason: r.reason,
|
|
90
|
+
committedNodeIds: r.committed_node_ids ? (JSON.parse(r.committed_node_ids) as string[]) : [],
|
|
91
|
+
createdAt: r.created_at,
|
|
92
|
+
updatedAt: r.updated_at,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export function storeAttachment(
|
|
96
|
+
db: Database,
|
|
97
|
+
input: { boardId: string; name: string; mime: string; bytes: Uint8Array },
|
|
98
|
+
): Attachment {
|
|
99
|
+
if (!input.name.trim() || input.name.length > MAX_ATTACHMENT_NAME_LENGTH)
|
|
100
|
+
throw new Error(`Attachment name must be 1-${MAX_ATTACHMENT_NAME_LENGTH} characters.`);
|
|
101
|
+
if (input.bytes.byteLength === 0 || input.bytes.byteLength > MAX_ATTACHMENT_BYTES)
|
|
102
|
+
throw new Error(`Attachment must contain 1-${MAX_ATTACHMENT_BYTES} bytes.`);
|
|
103
|
+
const sha256 = new Bun.CryptoHasher('sha256').update(input.bytes).digest('hex');
|
|
104
|
+
const value: Attachment = {
|
|
105
|
+
id: randomUUID(),
|
|
106
|
+
boardId: input.boardId,
|
|
107
|
+
sha256,
|
|
108
|
+
name: input.name,
|
|
109
|
+
mime: input.mime,
|
|
110
|
+
size: input.bytes.byteLength,
|
|
111
|
+
createdAt: new Date().toISOString(),
|
|
112
|
+
};
|
|
113
|
+
db.transaction(() => {
|
|
114
|
+
db.run('INSERT OR IGNORE INTO attachment_bytes (sha256,data,size) VALUES (?,?,?)', [
|
|
115
|
+
sha256,
|
|
116
|
+
input.bytes,
|
|
117
|
+
value.size,
|
|
118
|
+
]);
|
|
119
|
+
db.run('INSERT INTO attachments (id,board_id,sha256,name,mime,size,created_at) VALUES (?,?,?,?,?,?,?)', [
|
|
120
|
+
value.id,
|
|
121
|
+
value.boardId,
|
|
122
|
+
value.sha256,
|
|
123
|
+
value.name,
|
|
124
|
+
value.mime,
|
|
125
|
+
value.size,
|
|
126
|
+
value.createdAt,
|
|
127
|
+
]);
|
|
128
|
+
})();
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function getAttachment(db: Database, id: string): Attachment | null {
|
|
133
|
+
const row = db
|
|
134
|
+
.query<AttachmentRow, [string]>('SELECT id,board_id,sha256,name,mime,size,created_at FROM attachments WHERE id=?')
|
|
135
|
+
.get(id);
|
|
136
|
+
return row ? attachment(row) : null;
|
|
137
|
+
}
|
|
138
|
+
export function readAttachmentBytes(db: Database, id: string): Uint8Array | null {
|
|
139
|
+
return (
|
|
140
|
+
db
|
|
141
|
+
.query<{ data: Uint8Array }, [string]>(
|
|
142
|
+
'SELECT b.data FROM attachment_bytes b JOIN attachments a ON a.sha256=b.sha256 WHERE a.id=?',
|
|
143
|
+
)
|
|
144
|
+
.get(id)?.data ?? null
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
export function copyBoardAttachments(
|
|
148
|
+
db: Database,
|
|
149
|
+
sourceBoardId: string,
|
|
150
|
+
targetBoardId: string,
|
|
151
|
+
referencedIds: Set<string>,
|
|
152
|
+
): Map<string, string> {
|
|
153
|
+
const map = new Map<string, string>();
|
|
154
|
+
const rows = db
|
|
155
|
+
.query<AttachmentRow, string[]>(
|
|
156
|
+
`SELECT id,board_id,sha256,name,mime,size,created_at FROM attachments WHERE board_id=? AND id IN (${[...referencedIds].map(() => '?').join(',') || "''"})`,
|
|
157
|
+
)
|
|
158
|
+
.all(sourceBoardId, ...referencedIds);
|
|
159
|
+
for (const row of rows) {
|
|
160
|
+
const id = randomUUID();
|
|
161
|
+
map.set(row.id, id);
|
|
162
|
+
db.run('INSERT INTO attachments (id,board_id,sha256,name,mime,size,created_at) VALUES (?,?,?,?,?,?,?)', [
|
|
163
|
+
id,
|
|
164
|
+
targetBoardId,
|
|
165
|
+
row.sha256,
|
|
166
|
+
row.name,
|
|
167
|
+
row.mime,
|
|
168
|
+
row.size,
|
|
169
|
+
new Date().toISOString(),
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
return map;
|
|
173
|
+
}
|
|
174
|
+
export function createImport(db: Database, attachmentId: string, position: { x: number; y: number }): DocumentImport {
|
|
175
|
+
const a = getAttachment(db, attachmentId);
|
|
176
|
+
if (!a) throw new Error('Attachment not found.');
|
|
177
|
+
const now = new Date().toISOString(),
|
|
178
|
+
id = randomUUID();
|
|
179
|
+
db.run(
|
|
180
|
+
"INSERT INTO document_imports (id,board_id,attachment_id,status,position_x,position_y,created_at,updated_at) VALUES (?,?,?,'attached',?,?,?,?)",
|
|
181
|
+
[id, a.boardId, attachmentId, position.x, position.y, now, now],
|
|
182
|
+
);
|
|
183
|
+
return getImport(db, id)!;
|
|
184
|
+
}
|
|
185
|
+
export function getImport(db: Database, id: string): DocumentImport | null {
|
|
186
|
+
const r = db.query<ImportRow, [string]>('SELECT * FROM document_imports WHERE id=?').get(id);
|
|
187
|
+
return r ? imported(r) : null;
|
|
188
|
+
}
|
|
189
|
+
export function listImports(db: Database, boardId?: string): DocumentImport[] {
|
|
190
|
+
const rows = boardId
|
|
191
|
+
? db
|
|
192
|
+
.query<ImportRow, [string]>('SELECT * FROM document_imports WHERE board_id=? ORDER BY created_at DESC')
|
|
193
|
+
.all(boardId)
|
|
194
|
+
: db.query<ImportRow, []>('SELECT * FROM document_imports ORDER BY created_at DESC').all();
|
|
195
|
+
return rows.map(imported);
|
|
196
|
+
}
|
|
197
|
+
export function updateImport(
|
|
198
|
+
db: Database,
|
|
199
|
+
id: string,
|
|
200
|
+
status: DocumentImportStatus,
|
|
201
|
+
fields: {
|
|
202
|
+
sections?: ImportSection[];
|
|
203
|
+
warnings?: string[];
|
|
204
|
+
agentDescription?: string;
|
|
205
|
+
reason?: string;
|
|
206
|
+
committedNodeIds?: string[];
|
|
207
|
+
} = {},
|
|
208
|
+
): DocumentImport {
|
|
209
|
+
db.run(
|
|
210
|
+
'UPDATE document_imports SET status=?,sections=COALESCE(?,sections),warnings=COALESCE(?,warnings),agent_description=COALESCE(?,agent_description),reason=COALESCE(?,reason),committed_node_ids=COALESCE(?,committed_node_ids),updated_at=? WHERE id=?',
|
|
211
|
+
[
|
|
212
|
+
status,
|
|
213
|
+
fields.sections ? JSON.stringify(fields.sections) : null,
|
|
214
|
+
fields.warnings ? JSON.stringify(fields.warnings) : null,
|
|
215
|
+
fields.agentDescription ?? null,
|
|
216
|
+
fields.reason ?? null,
|
|
217
|
+
fields.committedNodeIds ? JSON.stringify(fields.committedNodeIds) : null,
|
|
218
|
+
new Date().toISOString(),
|
|
219
|
+
id,
|
|
220
|
+
],
|
|
221
|
+
);
|
|
222
|
+
return getImport(db, id)!;
|
|
223
|
+
}
|
package/src/server/index.ts
CHANGED
|
@@ -37,7 +37,11 @@ import type {
|
|
|
37
37
|
PmxAxWorkItem,
|
|
38
38
|
PmxAxWorkItemStatus,
|
|
39
39
|
} from './ax-state.js';
|
|
40
|
-
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';
|
|
41
45
|
import { onFileNodeChanged } from './file-watcher.js';
|
|
42
46
|
import { findOpenCanvasPosition, computeGroupBounds } from './placement.js';
|
|
43
47
|
import { searchNodes, buildSpatialContext } from './spatial-analysis.js';
|
|
@@ -600,7 +604,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
600
604
|
return canvasState.getAxState();
|
|
601
605
|
}
|
|
602
606
|
|
|
603
|
-
getAxContext(options?: { consumer?: string }): PmxAxContext {
|
|
607
|
+
getAxContext(options?: { consumer?: string }): PmxAxContext & { boardId: string | null } {
|
|
604
608
|
return buildCanvasAxContext(options?.consumer);
|
|
605
609
|
}
|
|
606
610
|
|
|
@@ -682,6 +686,218 @@ export class PmxCanvas extends EventEmitter {
|
|
|
682
686
|
return canvasState.getAxTimeline(query);
|
|
683
687
|
}
|
|
684
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
|
+
|
|
685
901
|
listWorkItems(): PmxAxWorkItem[] {
|
|
686
902
|
return canvasState.getWorkItems();
|
|
687
903
|
}
|
|
@@ -1014,12 +1230,19 @@ export class PmxCanvas extends EventEmitter {
|
|
|
1014
1230
|
return result;
|
|
1015
1231
|
}
|
|
1016
1232
|
|
|
1017
|
-
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
|
+
}
|
|
1018
1239
|
return canvasState.getLayout();
|
|
1019
1240
|
}
|
|
1020
1241
|
|
|
1021
|
-
getNode(id: string): SdkCanvasNode | undefined {
|
|
1022
|
-
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);
|
|
1023
1246
|
return node ? toSdkNode(node) : undefined;
|
|
1024
1247
|
}
|
|
1025
1248
|
|
|
@@ -1435,6 +1658,9 @@ export {
|
|
|
1435
1658
|
screenshotCanvasAutomationWebView,
|
|
1436
1659
|
} from './server.js';
|
|
1437
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';
|
|
1438
1664
|
export type {
|
|
1439
1665
|
CanvasAnnotation,
|
|
1440
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:
|
|
@@ -217,9 +237,10 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
|
|
|
217
237
|
{
|
|
218
238
|
toolName: 'canvas_query',
|
|
219
239
|
description:
|
|
220
|
-
'Read the board cheapest-first. Action "
|
|
221
|
-
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',
|
|
222
242
|
actions: {
|
|
243
|
+
context: 'context.get',
|
|
223
244
|
search: 'search',
|
|
224
245
|
layout: 'layout.get',
|
|
225
246
|
validate: 'validate.get',
|
|
@@ -301,13 +322,14 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
|
|
|
301
322
|
{
|
|
302
323
|
toolName: 'canvas_ax_timeline',
|
|
303
324
|
description:
|
|
304
|
-
'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.',
|
|
305
|
-
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',
|
|
306
327
|
actions: {
|
|
307
328
|
read: 'ax.timeline.get',
|
|
308
329
|
'record-event': 'ax.event.record',
|
|
309
330
|
'add-evidence': 'ax.evidence.add',
|
|
310
331
|
'send-steering': 'ax.steer',
|
|
332
|
+
reads: 'ax.reads.list',
|
|
311
333
|
},
|
|
312
334
|
},
|
|
313
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
|
}
|