@pascal-app/mcp 0.2.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/CHANGELOG.md +39 -0
- package/README.md +330 -0
- package/dist/bin/pascal-mcp.d.ts +3 -0
- package/dist/bin/pascal-mcp.d.ts.map +1 -0
- package/dist/bin/pascal-mcp.js +91 -0
- package/dist/bridge/index.d.ts +3 -0
- package/dist/bridge/index.d.ts.map +1 -0
- package/dist/bridge/index.js +1 -0
- package/dist/bridge/node-shims.d.ts +16 -0
- package/dist/bridge/node-shims.d.ts.map +1 -0
- package/dist/bridge/node-shims.js +25 -0
- package/dist/bridge/scene-bridge.d.ts +158 -0
- package/dist/bridge/scene-bridge.d.ts.map +1 -0
- package/dist/bridge/scene-bridge.js +482 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/lib/rehydrate-site-children.d.ts +12 -0
- package/dist/lib/rehydrate-site-children.d.ts.map +1 -0
- package/dist/lib/rehydrate-site-children.js +40 -0
- package/dist/lib/safe-fetch.d.ts +19 -0
- package/dist/lib/safe-fetch.d.ts.map +1 -0
- package/dist/lib/safe-fetch.js +221 -0
- package/dist/operations/index.d.ts +2 -0
- package/dist/operations/index.d.ts.map +1 -0
- package/dist/operations/index.js +1 -0
- package/dist/operations/scene-operations.d.ts +67 -0
- package/dist/operations/scene-operations.d.ts.map +1 -0
- package/dist/operations/scene-operations.js +192 -0
- package/dist/prompts/from-brief.d.ts +11 -0
- package/dist/prompts/from-brief.d.ts.map +1 -0
- package/dist/prompts/from-brief.js +48 -0
- package/dist/prompts/index.d.ts +10 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +14 -0
- package/dist/prompts/iterate-on-feedback.d.ts +11 -0
- package/dist/prompts/iterate-on-feedback.d.ts.map +1 -0
- package/dist/prompts/iterate-on-feedback.js +41 -0
- package/dist/prompts/renovation-from-photos.d.ts +24 -0
- package/dist/prompts/renovation-from-photos.d.ts.map +1 -0
- package/dist/prompts/renovation-from-photos.js +135 -0
- package/dist/prompts/scene-guidance.d.ts +2 -0
- package/dist/prompts/scene-guidance.d.ts.map +1 -0
- package/dist/prompts/scene-guidance.js +24 -0
- package/dist/resources/agent-guide.d.ts +5 -0
- package/dist/resources/agent-guide.d.ts.map +1 -0
- package/dist/resources/agent-guide.js +85 -0
- package/dist/resources/catalog-items.d.ts +10 -0
- package/dist/resources/catalog-items.d.ts.map +1 -0
- package/dist/resources/catalog-items.js +29 -0
- package/dist/resources/constraints.d.ts +9 -0
- package/dist/resources/constraints.d.ts.map +1 -0
- package/dist/resources/constraints.js +69 -0
- package/dist/resources/index.d.ts +15 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +23 -0
- package/dist/resources/scene-current.d.ts +9 -0
- package/dist/resources/scene-current.d.ts.map +1 -0
- package/dist/resources/scene-current.js +20 -0
- package/dist/resources/scene-summary.d.ts +10 -0
- package/dist/resources/scene-summary.d.ts.map +1 -0
- package/dist/resources/scene-summary.js +196 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +18 -0
- package/dist/storage/index.d.ts +13 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +14 -0
- package/dist/storage/slug.d.ts +21 -0
- package/dist/storage/slug.d.ts.map +1 -0
- package/dist/storage/slug.js +62 -0
- package/dist/storage/sqlite-driver.d.ts +18 -0
- package/dist/storage/sqlite-driver.d.ts.map +1 -0
- package/dist/storage/sqlite-driver.js +32 -0
- package/dist/storage/sqlite-scene-store.d.ts +51 -0
- package/dist/storage/sqlite-scene-store.d.ts.map +1 -0
- package/dist/storage/sqlite-scene-store.js +526 -0
- package/dist/storage/types.d.ts +136 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +28 -0
- package/dist/templates/empty-studio.d.ts +8 -0
- package/dist/templates/empty-studio.d.ts.map +1 -0
- package/dist/templates/empty-studio.js +230 -0
- package/dist/templates/garden-house.d.ts +8 -0
- package/dist/templates/garden-house.d.ts.map +1 -0
- package/dist/templates/garden-house.js +242 -0
- package/dist/templates/index.d.ts +23 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +20 -0
- package/dist/templates/two-bedroom.d.ts +8 -0
- package/dist/templates/two-bedroom.d.ts.map +1 -0
- package/dist/templates/two-bedroom.js +270 -0
- package/dist/tools/apply-patch.d.ts +25 -0
- package/dist/tools/apply-patch.d.ts.map +1 -0
- package/dist/tools/apply-patch.js +59 -0
- package/dist/tools/asset-catalog.d.ts +16 -0
- package/dist/tools/asset-catalog.d.ts.map +1 -0
- package/dist/tools/asset-catalog.js +308 -0
- package/dist/tools/check-collisions.d.ts +15 -0
- package/dist/tools/check-collisions.d.ts.map +1 -0
- package/dist/tools/check-collisions.js +61 -0
- package/dist/tools/construction-tools.d.ts +93 -0
- package/dist/tools/construction-tools.d.ts.map +1 -0
- package/dist/tools/construction-tools.js +369 -0
- package/dist/tools/create-level.d.ts +14 -0
- package/dist/tools/create-level.d.ts.map +1 -0
- package/dist/tools/create-level.js +48 -0
- package/dist/tools/create-wall.d.ts +15 -0
- package/dist/tools/create-wall.d.ts.map +1 -0
- package/dist/tools/create-wall.js +50 -0
- package/dist/tools/cut-opening.d.ts +18 -0
- package/dist/tools/cut-opening.d.ts.map +1 -0
- package/dist/tools/cut-opening.js +57 -0
- package/dist/tools/delete-node.d.ts +12 -0
- package/dist/tools/delete-node.d.ts.map +1 -0
- package/dist/tools/delete-node.js +37 -0
- package/dist/tools/describe-node.d.ts +17 -0
- package/dist/tools/describe-node.d.ts.map +1 -0
- package/dist/tools/describe-node.js +86 -0
- package/dist/tools/duplicate-level.d.ts +12 -0
- package/dist/tools/duplicate-level.d.ts.map +1 -0
- package/dist/tools/duplicate-level.js +56 -0
- package/dist/tools/errors.d.ts +20 -0
- package/dist/tools/errors.d.ts.map +1 -0
- package/dist/tools/errors.js +25 -0
- package/dist/tools/export-glb.d.ts +10 -0
- package/dist/tools/export-glb.d.ts.map +1 -0
- package/dist/tools/export-glb.js +24 -0
- package/dist/tools/export-json.d.ts +11 -0
- package/dist/tools/export-json.d.ts.map +1 -0
- package/dist/tools/export-json.js +23 -0
- package/dist/tools/find-nodes.d.ts +32 -0
- package/dist/tools/find-nodes.d.ts.map +1 -0
- package/dist/tools/find-nodes.js +105 -0
- package/dist/tools/geometry.d.ts +24 -0
- package/dist/tools/geometry.d.ts.map +1 -0
- package/dist/tools/geometry.js +94 -0
- package/dist/tools/get-node.d.ts +11 -0
- package/dist/tools/get-node.d.ts.map +1 -0
- package/dist/tools/get-node.js +27 -0
- package/dist/tools/get-scene.d.ts +11 -0
- package/dist/tools/get-scene.d.ts.map +1 -0
- package/dist/tools/get-scene.js +26 -0
- package/dist/tools/index.d.ts +12 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +64 -0
- package/dist/tools/live-sync.d.ts +11 -0
- package/dist/tools/live-sync.d.ts.map +1 -0
- package/dist/tools/live-sync.js +62 -0
- package/dist/tools/measure.d.ts +14 -0
- package/dist/tools/measure.d.ts.map +1 -0
- package/dist/tools/measure.js +137 -0
- package/dist/tools/photo-to-scene/index.d.ts +11 -0
- package/dist/tools/photo-to-scene/index.d.ts.map +1 -0
- package/dist/tools/photo-to-scene/index.js +11 -0
- package/dist/tools/photo-to-scene/photo-to-scene.d.ts +26 -0
- package/dist/tools/photo-to-scene/photo-to-scene.d.ts.map +1 -0
- package/dist/tools/photo-to-scene/photo-to-scene.js +323 -0
- package/dist/tools/place-item.d.ts +15 -0
- package/dist/tools/place-item.d.ts.map +1 -0
- package/dist/tools/place-item.js +84 -0
- package/dist/tools/redo.d.ts +11 -0
- package/dist/tools/redo.d.ts.map +1 -0
- package/dist/tools/redo.js +25 -0
- package/dist/tools/room-tools.d.ts +97 -0
- package/dist/tools/room-tools.d.ts.map +1 -0
- package/dist/tools/room-tools.js +510 -0
- package/dist/tools/scene-lifecycle/create-project.d.ts +33 -0
- package/dist/tools/scene-lifecycle/create-project.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/create-project.js +70 -0
- package/dist/tools/scene-lifecycle/delete-scene.d.ts +12 -0
- package/dist/tools/scene-lifecycle/delete-scene.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/delete-scene.js +42 -0
- package/dist/tools/scene-lifecycle/get-project-status.d.ts +31 -0
- package/dist/tools/scene-lifecycle/get-project-status.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/get-project-status.js +69 -0
- package/dist/tools/scene-lifecycle/index.d.ts +17 -0
- package/dist/tools/scene-lifecycle/index.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/index.js +29 -0
- package/dist/tools/scene-lifecycle/list-scenes.d.ts +27 -0
- package/dist/tools/scene-lifecycle/list-scenes.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/list-scenes.js +49 -0
- package/dist/tools/scene-lifecycle/load-scene.d.ts +31 -0
- package/dist/tools/scene-lifecycle/load-scene.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/load-scene.js +55 -0
- package/dist/tools/scene-lifecycle/metadata.d.ts +49 -0
- package/dist/tools/scene-lifecycle/metadata.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/metadata.js +68 -0
- package/dist/tools/scene-lifecycle/rename-scene.d.ts +22 -0
- package/dist/tools/scene-lifecycle/rename-scene.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/rename-scene.js +63 -0
- package/dist/tools/scene-lifecycle/save-scene.d.ts +42 -0
- package/dist/tools/scene-lifecycle/save-scene.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/save-scene.js +130 -0
- package/dist/tools/scene-lifecycle/test-utils.d.ts +39 -0
- package/dist/tools/scene-lifecycle/test-utils.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/test-utils.js +211 -0
- package/dist/tools/scene-query.d.ts +57 -0
- package/dist/tools/scene-query.d.ts.map +1 -0
- package/dist/tools/scene-query.js +645 -0
- package/dist/tools/schemas.d.ts +52 -0
- package/dist/tools/schemas.d.ts.map +1 -0
- package/dist/tools/schemas.js +42 -0
- package/dist/tools/set-zone.d.ts +14 -0
- package/dist/tools/set-zone.d.ts.map +1 -0
- package/dist/tools/set-zone.js +48 -0
- package/dist/tools/templates/create-from-template.d.ts +54 -0
- package/dist/tools/templates/create-from-template.d.ts.map +1 -0
- package/dist/tools/templates/create-from-template.js +145 -0
- package/dist/tools/templates/create-house-from-brief.d.ts +35 -0
- package/dist/tools/templates/create-house-from-brief.d.ts.map +1 -0
- package/dist/tools/templates/create-house-from-brief.js +170 -0
- package/dist/tools/templates/index.d.ts +14 -0
- package/dist/tools/templates/index.d.ts.map +1 -0
- package/dist/tools/templates/index.js +18 -0
- package/dist/tools/templates/list-templates.d.ts +18 -0
- package/dist/tools/templates/list-templates.d.ts.map +1 -0
- package/dist/tools/templates/list-templates.js +36 -0
- package/dist/tools/undo.d.ts +11 -0
- package/dist/tools/undo.d.ts.map +1 -0
- package/dist/tools/undo.js +25 -0
- package/dist/tools/validate-scene.d.ts +14 -0
- package/dist/tools/validate-scene.d.ts.map +1 -0
- package/dist/tools/validate-scene.js +24 -0
- package/dist/tools/variants/generate-variants.d.ts +30 -0
- package/dist/tools/variants/generate-variants.d.ts.map +1 -0
- package/dist/tools/variants/generate-variants.js +160 -0
- package/dist/tools/variants/index.d.ts +9 -0
- package/dist/tools/variants/index.d.ts.map +1 -0
- package/dist/tools/variants/index.js +9 -0
- package/dist/tools/variants/mutations.d.ts +18 -0
- package/dist/tools/variants/mutations.d.ts.map +1 -0
- package/dist/tools/variants/mutations.js +324 -0
- package/dist/tools/vision/analyze-floorplan-image.d.ts +33 -0
- package/dist/tools/vision/analyze-floorplan-image.d.ts.map +1 -0
- package/dist/tools/vision/analyze-floorplan-image.js +146 -0
- package/dist/tools/vision/analyze-room-photo.d.ts +29 -0
- package/dist/tools/vision/analyze-room-photo.d.ts.map +1 -0
- package/dist/tools/vision/analyze-room-photo.js +130 -0
- package/dist/tools/vision/index.d.ts +12 -0
- package/dist/tools/vision/index.d.ts.map +1 -0
- package/dist/tools/vision/index.js +14 -0
- package/dist/transports/http.d.ts +36 -0
- package/dist/transports/http.d.ts.map +1 -0
- package/dist/transports/http.js +203 -0
- package/dist/transports/stdio.d.ts +14 -0
- package/dist/transports/stdio.d.ts.map +1 -0
- package/dist/transports/stdio.js +16 -0
- package/package.json +86 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdirSync } from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { generateSlug, isValidSlug, sanitizeSlug } from './slug';
|
|
7
|
+
import { openSqliteDatabase } from './sqlite-driver';
|
|
8
|
+
import { SceneInvalidError, SceneNotFoundError, SceneTooLargeError, SceneVersionConflictError, } from './types';
|
|
9
|
+
const DEFAULT_MAX_SCENE_BYTES = 10 * 1024 * 1024;
|
|
10
|
+
const DEFAULT_LIST_LIMIT = 100;
|
|
11
|
+
const MAX_NAME_LENGTH = 200;
|
|
12
|
+
const MIN_NAME_LENGTH = 1;
|
|
13
|
+
const GraphSchema = z.object({
|
|
14
|
+
nodes: z.record(z.string(), z.unknown()),
|
|
15
|
+
rootNodeIds: z.array(z.string()),
|
|
16
|
+
collections: z.record(z.string(), z.unknown()).optional(),
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* Resolves Pascal's local SQLite database path.
|
|
20
|
+
*
|
|
21
|
+
* Precedence:
|
|
22
|
+
* 1. `PASCAL_DB_PATH`
|
|
23
|
+
* 2. `PASCAL_DATA_DIR/pascal.db`
|
|
24
|
+
* 3. On Windows: `%APPDATA%/Pascal/data/pascal.db`
|
|
25
|
+
* 4. `$XDG_DATA_HOME/pascal/data/pascal.db`
|
|
26
|
+
* 5. `$HOME/.pascal/data/pascal.db`
|
|
27
|
+
*/
|
|
28
|
+
export function resolveDefaultDatabasePath(env = process.env) {
|
|
29
|
+
if (env.PASCAL_DB_PATH && env.PASCAL_DB_PATH.length > 0) {
|
|
30
|
+
return env.PASCAL_DB_PATH;
|
|
31
|
+
}
|
|
32
|
+
if (env.PASCAL_DATA_DIR && env.PASCAL_DATA_DIR.length > 0) {
|
|
33
|
+
return path.join(env.PASCAL_DATA_DIR, 'pascal.db');
|
|
34
|
+
}
|
|
35
|
+
if (process.platform === 'win32') {
|
|
36
|
+
const appData = env.APPDATA;
|
|
37
|
+
if (appData && appData.length > 0) {
|
|
38
|
+
return path.join(appData, 'Pascal', 'data', 'pascal.db');
|
|
39
|
+
}
|
|
40
|
+
return path.join(os.homedir(), '.pascal', 'data', 'pascal.db');
|
|
41
|
+
}
|
|
42
|
+
const xdg = env.XDG_DATA_HOME;
|
|
43
|
+
if (xdg && xdg.length > 0) {
|
|
44
|
+
return path.join(xdg, 'pascal', 'data', 'pascal.db');
|
|
45
|
+
}
|
|
46
|
+
return path.join(os.homedir(), '.pascal', 'data', 'pascal.db');
|
|
47
|
+
}
|
|
48
|
+
function resolveMaxSceneBytes(env, explicit) {
|
|
49
|
+
if (explicit !== undefined) {
|
|
50
|
+
if (!Number.isInteger(explicit) || explicit <= 0) {
|
|
51
|
+
throw new SceneInvalidError('maxSceneBytes must be a positive integer');
|
|
52
|
+
}
|
|
53
|
+
return explicit;
|
|
54
|
+
}
|
|
55
|
+
const raw = env?.PASCAL_MAX_SCENE_BYTES;
|
|
56
|
+
if (raw === undefined || raw === '')
|
|
57
|
+
return DEFAULT_MAX_SCENE_BYTES;
|
|
58
|
+
const parsed = Number.parseInt(raw, 10);
|
|
59
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
60
|
+
throw new SceneInvalidError('PASCAL_MAX_SCENE_BYTES must be a positive integer');
|
|
61
|
+
}
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
function rowToMeta(row) {
|
|
65
|
+
const editorUrl = editorUrlForScene(row.id);
|
|
66
|
+
return {
|
|
67
|
+
id: row.id,
|
|
68
|
+
name: row.name,
|
|
69
|
+
projectId: row.project_id,
|
|
70
|
+
ownerId: row.owner_id,
|
|
71
|
+
thumbnailUrl: row.thumbnail_url,
|
|
72
|
+
version: row.version,
|
|
73
|
+
createdAt: row.created_at,
|
|
74
|
+
updatedAt: row.updated_at,
|
|
75
|
+
sizeBytes: row.size_bytes,
|
|
76
|
+
nodeCount: row.node_count,
|
|
77
|
+
editorUrl,
|
|
78
|
+
url: editorUrl,
|
|
79
|
+
published: true,
|
|
80
|
+
graphHash: hashGraphJson(row.graph_json),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function editorUrlForScene(id) {
|
|
84
|
+
return `/editor/${id}`;
|
|
85
|
+
}
|
|
86
|
+
function hashGraphJson(graphJson) {
|
|
87
|
+
return createHash('sha256').update(graphJson).digest('hex');
|
|
88
|
+
}
|
|
89
|
+
function rowToProjectStatus(row) {
|
|
90
|
+
const editorUrl = editorUrlForScene(row.id);
|
|
91
|
+
return {
|
|
92
|
+
id: row.id,
|
|
93
|
+
projectId: row.project_id ?? row.id,
|
|
94
|
+
name: row.name,
|
|
95
|
+
editorUrl,
|
|
96
|
+
url: editorUrl,
|
|
97
|
+
ownerId: row.owner_id,
|
|
98
|
+
thumbnailUrl: row.thumbnail_url,
|
|
99
|
+
publishedVersion: row.version,
|
|
100
|
+
latestVersion: row.version,
|
|
101
|
+
draftVersion: null,
|
|
102
|
+
browserVisibleVersion: row.version,
|
|
103
|
+
version: row.version,
|
|
104
|
+
isEmpty: row.node_count === 0,
|
|
105
|
+
sizeBytes: row.size_bytes,
|
|
106
|
+
nodeCount: row.node_count,
|
|
107
|
+
graphHash: hashGraphJson(row.graph_json),
|
|
108
|
+
createdAt: row.created_at,
|
|
109
|
+
updatedAt: row.updated_at,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function placeholderToProjectStatus(project) {
|
|
113
|
+
const editorUrl = editorUrlForScene(project.id);
|
|
114
|
+
return {
|
|
115
|
+
id: project.id,
|
|
116
|
+
projectId: project.id,
|
|
117
|
+
name: project.name,
|
|
118
|
+
editorUrl,
|
|
119
|
+
url: editorUrl,
|
|
120
|
+
ownerId: project.ownerId,
|
|
121
|
+
thumbnailUrl: project.thumbnailUrl,
|
|
122
|
+
publishedVersion: null,
|
|
123
|
+
latestVersion: null,
|
|
124
|
+
draftVersion: null,
|
|
125
|
+
browserVisibleVersion: null,
|
|
126
|
+
version: 0,
|
|
127
|
+
isEmpty: true,
|
|
128
|
+
sizeBytes: 0,
|
|
129
|
+
nodeCount: 0,
|
|
130
|
+
graphHash: null,
|
|
131
|
+
createdAt: project.createdAt,
|
|
132
|
+
updatedAt: project.updatedAt,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function assertValidName(name) {
|
|
136
|
+
if (typeof name !== 'string') {
|
|
137
|
+
throw new SceneInvalidError('Scene name must be a string');
|
|
138
|
+
}
|
|
139
|
+
const trimmed = name.trim();
|
|
140
|
+
if (trimmed.length < MIN_NAME_LENGTH || name.length > MAX_NAME_LENGTH) {
|
|
141
|
+
throw new SceneInvalidError(`Scene name must be ${MIN_NAME_LENGTH}-${MAX_NAME_LENGTH} characters (got ${name.length})`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function serializeGraph(graph) {
|
|
145
|
+
return JSON.stringify(graph);
|
|
146
|
+
}
|
|
147
|
+
function parseGraph(raw, context) {
|
|
148
|
+
let parsed;
|
|
149
|
+
try {
|
|
150
|
+
parsed = JSON.parse(raw);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
throw new SceneInvalidError(`Failed to parse scene graph for ${context}: ${err instanceof Error ? err.message : String(err)}`);
|
|
154
|
+
}
|
|
155
|
+
const result = GraphSchema.safeParse(parsed);
|
|
156
|
+
if (!result.success) {
|
|
157
|
+
throw new SceneInvalidError(`Scene graph for ${context} has invalid shape: ${result.error}`);
|
|
158
|
+
}
|
|
159
|
+
const graph = result.data;
|
|
160
|
+
for (const [nodeId, node] of Object.entries(graph.nodes)) {
|
|
161
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) {
|
|
162
|
+
throw new SceneInvalidError(`Scene graph for ${context} has non-object node at "${nodeId}"`);
|
|
163
|
+
}
|
|
164
|
+
const typeField = node.type;
|
|
165
|
+
if (typeof typeField !== 'string' || typeField.length === 0) {
|
|
166
|
+
throw new SceneInvalidError(`Scene graph for ${context} has node "${nodeId}" missing a string "type"`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return graph;
|
|
170
|
+
}
|
|
171
|
+
function asSceneRow(value) {
|
|
172
|
+
if (!value || typeof value !== 'object')
|
|
173
|
+
return null;
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
function rowToSceneEvent(row) {
|
|
177
|
+
return {
|
|
178
|
+
eventId: Number(row.event_id),
|
|
179
|
+
sceneId: row.scene_id,
|
|
180
|
+
version: Number(row.version),
|
|
181
|
+
kind: row.kind,
|
|
182
|
+
createdAt: row.created_at,
|
|
183
|
+
graph: parseGraph(row.graph_json, `${row.scene_id}@${row.version}`),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* SQLite-backed implementation of `SceneStore`.
|
|
188
|
+
*
|
|
189
|
+
* Uses one local database file, WAL mode, and transaction-scoped version checks
|
|
190
|
+
* so a local editor and MCP process can safely share scenes on one machine.
|
|
191
|
+
*/
|
|
192
|
+
export class SqliteSceneStore {
|
|
193
|
+
backend = 'sqlite';
|
|
194
|
+
databasePath;
|
|
195
|
+
maxSceneBytes;
|
|
196
|
+
projectPlaceholders = new Map();
|
|
197
|
+
db = null;
|
|
198
|
+
dbPromise = null;
|
|
199
|
+
constructor(opts = {}) {
|
|
200
|
+
const env = opts.env ?? process.env;
|
|
201
|
+
this.databasePath = path.resolve(opts.databasePath ?? resolveDefaultDatabasePath(env));
|
|
202
|
+
this.maxSceneBytes = resolveMaxSceneBytes(env, opts.maxSceneBytes);
|
|
203
|
+
}
|
|
204
|
+
async createProject(opts) {
|
|
205
|
+
const db = await this.database();
|
|
206
|
+
assertValidName(opts.name);
|
|
207
|
+
const id = opts.id ? sanitizeSlug(opts.id) : this.generateUniqueId(db);
|
|
208
|
+
if (!isValidSlug(id)) {
|
|
209
|
+
throw new SceneInvalidError(`Invalid project id after sanitization: "${id}"`);
|
|
210
|
+
}
|
|
211
|
+
if (this.getRow(db, id)) {
|
|
212
|
+
throw new SceneInvalidError(`Project with id "${id}" already exists`);
|
|
213
|
+
}
|
|
214
|
+
const now = new Date().toISOString();
|
|
215
|
+
const project = {
|
|
216
|
+
id,
|
|
217
|
+
name: opts.name,
|
|
218
|
+
ownerId: opts.ownerId ?? null,
|
|
219
|
+
thumbnailUrl: null,
|
|
220
|
+
createdAt: now,
|
|
221
|
+
updatedAt: now,
|
|
222
|
+
};
|
|
223
|
+
this.projectPlaceholders.set(id, project);
|
|
224
|
+
return placeholderToProjectStatus(project);
|
|
225
|
+
}
|
|
226
|
+
async getProjectStatus(id) {
|
|
227
|
+
const db = await this.database();
|
|
228
|
+
const safeId = sanitizeSlug(id);
|
|
229
|
+
const row = this.getRow(db, safeId);
|
|
230
|
+
if (row)
|
|
231
|
+
return rowToProjectStatus(row);
|
|
232
|
+
const placeholder = this.projectPlaceholders.get(safeId);
|
|
233
|
+
return placeholder ? placeholderToProjectStatus(placeholder) : null;
|
|
234
|
+
}
|
|
235
|
+
async save(opts) {
|
|
236
|
+
return this.withWriteTransaction((db) => {
|
|
237
|
+
assertValidName(opts.name);
|
|
238
|
+
if (!opts.graph || typeof opts.graph !== 'object') {
|
|
239
|
+
throw new SceneInvalidError('graph is required');
|
|
240
|
+
}
|
|
241
|
+
const providedId = opts.id;
|
|
242
|
+
const id = providedId ? sanitizeSlug(providedId) : this.generateUniqueId(db);
|
|
243
|
+
if (!isValidSlug(id)) {
|
|
244
|
+
throw new SceneInvalidError(`Invalid scene id after sanitization: "${id}"`);
|
|
245
|
+
}
|
|
246
|
+
const existing = this.getRow(db, id);
|
|
247
|
+
const placeholder = this.projectPlaceholders.get(id);
|
|
248
|
+
if (existing && providedId !== undefined && opts.expectedVersion === undefined) {
|
|
249
|
+
throw new SceneInvalidError(`Scene with id "${id}" already exists. Pass a different id or provide expectedVersion to overwrite.`);
|
|
250
|
+
}
|
|
251
|
+
if (opts.expectedVersion !== undefined) {
|
|
252
|
+
const currentVersion = existing?.version ?? 0;
|
|
253
|
+
if (currentVersion !== opts.expectedVersion) {
|
|
254
|
+
throw new SceneVersionConflictError(`Scene "${id}" version mismatch: expected ${opts.expectedVersion}, got ${currentVersion}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const graphJson = serializeGraph(opts.graph);
|
|
258
|
+
const sizeBytes = Buffer.byteLength(graphJson, 'utf8');
|
|
259
|
+
if (sizeBytes > this.maxSceneBytes) {
|
|
260
|
+
throw new SceneTooLargeError(`Scene "${id}" is ${sizeBytes} bytes, exceeds cap of ${this.maxSceneBytes} bytes`);
|
|
261
|
+
}
|
|
262
|
+
const now = new Date().toISOString();
|
|
263
|
+
const version = (existing?.version ?? 0) + 1;
|
|
264
|
+
const createdAt = existing?.created_at ?? placeholder?.createdAt ?? now;
|
|
265
|
+
const nodeCount = Object.keys(opts.graph.nodes ?? {}).length;
|
|
266
|
+
const projectId = opts.projectId ?? existing?.project_id ?? (placeholder ? id : null);
|
|
267
|
+
const ownerId = opts.ownerId ?? existing?.owner_id ?? placeholder?.ownerId ?? null;
|
|
268
|
+
const thumbnailUrl = opts.thumbnailUrl ?? existing?.thumbnail_url ?? placeholder?.thumbnailUrl ?? null;
|
|
269
|
+
if (existing) {
|
|
270
|
+
db.query(`UPDATE scenes
|
|
271
|
+
SET name = ?,
|
|
272
|
+
project_id = ?,
|
|
273
|
+
owner_id = ?,
|
|
274
|
+
thumbnail_url = ?,
|
|
275
|
+
version = ?,
|
|
276
|
+
updated_at = ?,
|
|
277
|
+
size_bytes = ?,
|
|
278
|
+
node_count = ?,
|
|
279
|
+
graph_json = ?
|
|
280
|
+
WHERE id = ?`).run(opts.name, projectId, ownerId, thumbnailUrl, version, now, sizeBytes, nodeCount, graphJson, id);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
db.query(`INSERT INTO scenes (
|
|
284
|
+
id, name, project_id, owner_id, thumbnail_url, version,
|
|
285
|
+
created_at, updated_at, size_bytes, node_count, graph_json
|
|
286
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, opts.name, projectId, ownerId, thumbnailUrl, version, createdAt, now, sizeBytes, nodeCount, graphJson);
|
|
287
|
+
}
|
|
288
|
+
db.query(`INSERT INTO scene_revisions (
|
|
289
|
+
scene_id, version, graph_json, author_kind, author_id, created_at
|
|
290
|
+
) VALUES (?, ?, ?, ?, ?, ?)`).run(id, version, graphJson, 'mcp', ownerId, now);
|
|
291
|
+
this.projectPlaceholders.delete(id);
|
|
292
|
+
return {
|
|
293
|
+
id,
|
|
294
|
+
name: opts.name,
|
|
295
|
+
projectId,
|
|
296
|
+
ownerId,
|
|
297
|
+
thumbnailUrl,
|
|
298
|
+
version,
|
|
299
|
+
createdAt,
|
|
300
|
+
updatedAt: now,
|
|
301
|
+
sizeBytes,
|
|
302
|
+
nodeCount,
|
|
303
|
+
editorUrl: editorUrlForScene(id),
|
|
304
|
+
url: editorUrlForScene(id),
|
|
305
|
+
published: true,
|
|
306
|
+
graphHash: hashGraphJson(graphJson),
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
async load(id) {
|
|
311
|
+
const db = await this.database();
|
|
312
|
+
const row = this.getRow(db, sanitizeSlug(id));
|
|
313
|
+
if (!row)
|
|
314
|
+
return null;
|
|
315
|
+
return {
|
|
316
|
+
...rowToMeta(row),
|
|
317
|
+
graph: parseGraph(row.graph_json, row.id),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
async list(opts = {}) {
|
|
321
|
+
const clauses = [];
|
|
322
|
+
const bindings = [];
|
|
323
|
+
if (opts.projectId !== undefined) {
|
|
324
|
+
clauses.push('project_id = ?');
|
|
325
|
+
bindings.push(opts.projectId);
|
|
326
|
+
}
|
|
327
|
+
if (opts.ownerId !== undefined) {
|
|
328
|
+
clauses.push('owner_id = ?');
|
|
329
|
+
bindings.push(opts.ownerId);
|
|
330
|
+
}
|
|
331
|
+
const requestedLimit = opts.limit ?? DEFAULT_LIST_LIMIT;
|
|
332
|
+
const limit = Number.isInteger(requestedLimit) && requestedLimit >= 0 ? requestedLimit : 0;
|
|
333
|
+
bindings.push(limit);
|
|
334
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '';
|
|
335
|
+
const db = await this.database();
|
|
336
|
+
const rows = db
|
|
337
|
+
.query(`SELECT id, name, project_id, owner_id, thumbnail_url, version,
|
|
338
|
+
created_at, updated_at, size_bytes, node_count, graph_json
|
|
339
|
+
FROM scenes
|
|
340
|
+
${where}
|
|
341
|
+
ORDER BY updated_at DESC, id ASC
|
|
342
|
+
LIMIT ?`)
|
|
343
|
+
.all(...bindings);
|
|
344
|
+
return rows.map((row) => rowToMeta(row));
|
|
345
|
+
}
|
|
346
|
+
async delete(id, opts = {}) {
|
|
347
|
+
return this.withWriteTransaction((db) => {
|
|
348
|
+
const safeId = sanitizeSlug(id);
|
|
349
|
+
const existing = this.getRow(db, safeId);
|
|
350
|
+
if (!existing)
|
|
351
|
+
return false;
|
|
352
|
+
if (opts.expectedVersion !== undefined && existing.version !== opts.expectedVersion) {
|
|
353
|
+
throw new SceneVersionConflictError(`Scene "${safeId}" version mismatch: expected ${opts.expectedVersion}, got ${existing.version}`);
|
|
354
|
+
}
|
|
355
|
+
db.query('DELETE FROM scenes WHERE id = ?').run(safeId);
|
|
356
|
+
return true;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
async rename(id, newName, opts = {}) {
|
|
360
|
+
return this.withWriteTransaction((db) => {
|
|
361
|
+
assertValidName(newName);
|
|
362
|
+
const safeId = sanitizeSlug(id);
|
|
363
|
+
const existing = this.getRow(db, safeId);
|
|
364
|
+
if (!existing) {
|
|
365
|
+
throw new SceneNotFoundError(`Scene "${safeId}" not found`);
|
|
366
|
+
}
|
|
367
|
+
if (opts.expectedVersion !== undefined && existing.version !== opts.expectedVersion) {
|
|
368
|
+
throw new SceneVersionConflictError(`Scene "${safeId}" version mismatch: expected ${opts.expectedVersion}, got ${existing.version}`);
|
|
369
|
+
}
|
|
370
|
+
const now = new Date().toISOString();
|
|
371
|
+
const nextVersion = existing.version + 1;
|
|
372
|
+
db.query('UPDATE scenes SET name = ?, version = ?, updated_at = ? WHERE id = ?').run(newName, nextVersion, now, safeId);
|
|
373
|
+
db.query(`INSERT INTO scene_revisions (
|
|
374
|
+
scene_id, version, graph_json, author_kind, author_id, created_at
|
|
375
|
+
) VALUES (?, ?, ?, ?, ?, ?)`).run(safeId, nextVersion, existing.graph_json, 'mcp', existing.owner_id, now);
|
|
376
|
+
return {
|
|
377
|
+
...rowToMeta(existing),
|
|
378
|
+
name: newName,
|
|
379
|
+
version: nextVersion,
|
|
380
|
+
updatedAt: now,
|
|
381
|
+
};
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
async appendSceneEvent(opts) {
|
|
385
|
+
return this.withWriteTransaction((db) => {
|
|
386
|
+
const safeId = sanitizeSlug(opts.sceneId);
|
|
387
|
+
const existing = this.getRow(db, safeId);
|
|
388
|
+
if (!existing) {
|
|
389
|
+
throw new SceneNotFoundError(`Scene "${safeId}" not found`);
|
|
390
|
+
}
|
|
391
|
+
const graphJson = serializeGraph(opts.graph);
|
|
392
|
+
const now = new Date().toISOString();
|
|
393
|
+
const result = db
|
|
394
|
+
.query(`INSERT INTO scene_events (
|
|
395
|
+
scene_id, version, kind, created_at, graph_json
|
|
396
|
+
) VALUES (?, ?, ?, ?, ?)`)
|
|
397
|
+
.run(safeId, opts.version, opts.kind, now, graphJson);
|
|
398
|
+
return {
|
|
399
|
+
eventId: Number(result.lastInsertRowid),
|
|
400
|
+
sceneId: safeId,
|
|
401
|
+
version: opts.version,
|
|
402
|
+
kind: opts.kind,
|
|
403
|
+
createdAt: now,
|
|
404
|
+
graph: opts.graph,
|
|
405
|
+
};
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
async listSceneEvents(sceneId, opts = {}) {
|
|
409
|
+
const afterEventId = Math.max(0, opts.afterEventId ?? 0);
|
|
410
|
+
const requestedLimit = opts.limit ?? 100;
|
|
411
|
+
const limit = Number.isInteger(requestedLimit) && requestedLimit > 0 ? requestedLimit : 100;
|
|
412
|
+
const db = await this.database();
|
|
413
|
+
const rows = db
|
|
414
|
+
.query(`SELECT event_id, scene_id, version, kind, created_at, graph_json
|
|
415
|
+
FROM scene_events
|
|
416
|
+
WHERE scene_id = ?
|
|
417
|
+
AND event_id > ?
|
|
418
|
+
ORDER BY event_id ASC
|
|
419
|
+
LIMIT ?`)
|
|
420
|
+
.all(sanitizeSlug(sceneId), afterEventId, limit);
|
|
421
|
+
return rows.map((row) => rowToSceneEvent(row));
|
|
422
|
+
}
|
|
423
|
+
close() {
|
|
424
|
+
this.db?.close();
|
|
425
|
+
this.db = null;
|
|
426
|
+
this.dbPromise = null;
|
|
427
|
+
}
|
|
428
|
+
async database() {
|
|
429
|
+
if (this.db)
|
|
430
|
+
return this.db;
|
|
431
|
+
if (!this.dbPromise) {
|
|
432
|
+
this.dbPromise = (async () => {
|
|
433
|
+
mkdirSync(path.dirname(this.databasePath), { recursive: true });
|
|
434
|
+
const db = await openSqliteDatabase(this.databasePath);
|
|
435
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
436
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
437
|
+
db.exec('PRAGMA busy_timeout = 5000');
|
|
438
|
+
this.migrate(db);
|
|
439
|
+
this.db = db;
|
|
440
|
+
return db;
|
|
441
|
+
})();
|
|
442
|
+
}
|
|
443
|
+
return this.dbPromise;
|
|
444
|
+
}
|
|
445
|
+
migrate(db) {
|
|
446
|
+
db.exec(`
|
|
447
|
+
CREATE TABLE IF NOT EXISTS scenes (
|
|
448
|
+
id TEXT PRIMARY KEY,
|
|
449
|
+
name TEXT NOT NULL CHECK (length(name) >= 1 AND length(name) <= 200),
|
|
450
|
+
project_id TEXT,
|
|
451
|
+
owner_id TEXT,
|
|
452
|
+
thumbnail_url TEXT,
|
|
453
|
+
version INTEGER NOT NULL CHECK (version >= 1),
|
|
454
|
+
created_at TEXT NOT NULL,
|
|
455
|
+
updated_at TEXT NOT NULL,
|
|
456
|
+
size_bytes INTEGER NOT NULL CHECK (size_bytes >= 0),
|
|
457
|
+
node_count INTEGER NOT NULL CHECK (node_count >= 0),
|
|
458
|
+
graph_json TEXT NOT NULL
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
CREATE INDEX IF NOT EXISTS scenes_project_updated_idx
|
|
462
|
+
ON scenes(project_id, updated_at DESC);
|
|
463
|
+
|
|
464
|
+
CREATE INDEX IF NOT EXISTS scenes_owner_updated_idx
|
|
465
|
+
ON scenes(owner_id, updated_at DESC);
|
|
466
|
+
|
|
467
|
+
CREATE TABLE IF NOT EXISTS scene_revisions (
|
|
468
|
+
scene_id TEXT NOT NULL,
|
|
469
|
+
version INTEGER NOT NULL CHECK (version >= 1),
|
|
470
|
+
graph_json TEXT NOT NULL,
|
|
471
|
+
author_kind TEXT NOT NULL,
|
|
472
|
+
author_id TEXT,
|
|
473
|
+
created_at TEXT NOT NULL,
|
|
474
|
+
PRIMARY KEY (scene_id, version),
|
|
475
|
+
FOREIGN KEY (scene_id) REFERENCES scenes(id) ON DELETE CASCADE
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
CREATE TABLE IF NOT EXISTS scene_events (
|
|
479
|
+
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
480
|
+
scene_id TEXT NOT NULL,
|
|
481
|
+
version INTEGER NOT NULL CHECK (version >= 1),
|
|
482
|
+
kind TEXT NOT NULL,
|
|
483
|
+
created_at TEXT NOT NULL,
|
|
484
|
+
graph_json TEXT NOT NULL,
|
|
485
|
+
FOREIGN KEY (scene_id) REFERENCES scenes(id) ON DELETE CASCADE
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
CREATE INDEX IF NOT EXISTS scene_events_scene_event_idx
|
|
489
|
+
ON scene_events(scene_id, event_id);
|
|
490
|
+
`);
|
|
491
|
+
}
|
|
492
|
+
async withWriteTransaction(fn) {
|
|
493
|
+
const db = await this.database();
|
|
494
|
+
db.exec('BEGIN IMMEDIATE');
|
|
495
|
+
try {
|
|
496
|
+
const result = await fn(db);
|
|
497
|
+
db.exec('COMMIT');
|
|
498
|
+
return result;
|
|
499
|
+
}
|
|
500
|
+
catch (err) {
|
|
501
|
+
try {
|
|
502
|
+
db.exec('ROLLBACK');
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
// Ignore rollback errors so the original failure is preserved.
|
|
506
|
+
}
|
|
507
|
+
throw err;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
getRow(db, id) {
|
|
511
|
+
return asSceneRow(db
|
|
512
|
+
.query(`SELECT id, name, project_id, owner_id, thumbnail_url, version,
|
|
513
|
+
created_at, updated_at, size_bytes, node_count, graph_json
|
|
514
|
+
FROM scenes
|
|
515
|
+
WHERE id = ?`)
|
|
516
|
+
.get(id));
|
|
517
|
+
}
|
|
518
|
+
generateUniqueId(db) {
|
|
519
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
520
|
+
const id = generateSlug();
|
|
521
|
+
if (!this.getRow(db, id))
|
|
522
|
+
return id;
|
|
523
|
+
}
|
|
524
|
+
throw new SceneInvalidError('Failed to generate a unique scene id');
|
|
525
|
+
}
|
|
526
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { SceneGraph } from '@pascal-app/core/clone-scene-graph';
|
|
2
|
+
/**
|
|
3
|
+
* Slug-safe scene identifier: lowercase alphanumerics and hyphens, ≤ 64 chars.
|
|
4
|
+
*/
|
|
5
|
+
export type SceneId = string;
|
|
6
|
+
export interface SceneMeta {
|
|
7
|
+
id: SceneId;
|
|
8
|
+
name: string;
|
|
9
|
+
projectId: string | null;
|
|
10
|
+
thumbnailUrl: string | null;
|
|
11
|
+
/** Browser-visible model version. Draft saves may update the same version repeatedly. */
|
|
12
|
+
version: number;
|
|
13
|
+
/** ISO 8601 timestamp. */
|
|
14
|
+
createdAt: string;
|
|
15
|
+
/** ISO 8601 timestamp. */
|
|
16
|
+
updatedAt: string;
|
|
17
|
+
ownerId: string | null;
|
|
18
|
+
sizeBytes: number;
|
|
19
|
+
nodeCount: number;
|
|
20
|
+
/** Browser route agents should return to users. Hosted apps should prefer /editor/<projectId>. */
|
|
21
|
+
editorUrl?: string;
|
|
22
|
+
/** Backward-compatible alias for clients that still read url. */
|
|
23
|
+
url?: string;
|
|
24
|
+
/** True when this save is browser-visible without a separate publish call. */
|
|
25
|
+
published?: boolean;
|
|
26
|
+
/** True when the saved graph is still the mutable browser-visible draft. */
|
|
27
|
+
isDraft?: boolean;
|
|
28
|
+
/** How the scene was saved. Draft saves should not create meaningful history versions. */
|
|
29
|
+
saveMode?: SceneSaveMode;
|
|
30
|
+
/** Stable hash of the graph payload used for save/load/status matching. */
|
|
31
|
+
graphHash?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface SceneWithGraph extends SceneMeta {
|
|
34
|
+
graph: SceneGraph;
|
|
35
|
+
}
|
|
36
|
+
export interface SceneEvent {
|
|
37
|
+
eventId: number;
|
|
38
|
+
sceneId: SceneId;
|
|
39
|
+
version: number;
|
|
40
|
+
kind: string;
|
|
41
|
+
createdAt: string;
|
|
42
|
+
graph: SceneGraph;
|
|
43
|
+
}
|
|
44
|
+
export interface SceneSaveOptions {
|
|
45
|
+
id?: SceneId;
|
|
46
|
+
name: string;
|
|
47
|
+
projectId?: string | null;
|
|
48
|
+
ownerId?: string | null;
|
|
49
|
+
graph: SceneGraph;
|
|
50
|
+
thumbnailUrl?: string | null;
|
|
51
|
+
/** When set, save fails with `SceneVersionConflictError` on mismatch. */
|
|
52
|
+
expectedVersion?: number;
|
|
53
|
+
/** `draft` updates the browser-visible working model; `checkpoint` records version history. */
|
|
54
|
+
saveMode?: SceneSaveMode;
|
|
55
|
+
/** Whether a checkpoint should become the published/browser-visible head. */
|
|
56
|
+
publish?: boolean;
|
|
57
|
+
/** Optional hosted MCP session id for project presence/debug metadata. */
|
|
58
|
+
agentSessionId?: string;
|
|
59
|
+
/** Optional high-level operation name for presence/debug metadata. */
|
|
60
|
+
operation?: string;
|
|
61
|
+
}
|
|
62
|
+
export type SceneSaveMode = 'draft' | 'checkpoint';
|
|
63
|
+
export interface SceneListOptions {
|
|
64
|
+
projectId?: string;
|
|
65
|
+
ownerId?: string;
|
|
66
|
+
limit?: number;
|
|
67
|
+
}
|
|
68
|
+
export interface SceneMutateOptions {
|
|
69
|
+
expectedVersion?: number;
|
|
70
|
+
}
|
|
71
|
+
export interface SceneEventAppendOptions {
|
|
72
|
+
sceneId: SceneId;
|
|
73
|
+
version: number;
|
|
74
|
+
kind: string;
|
|
75
|
+
graph: SceneGraph;
|
|
76
|
+
}
|
|
77
|
+
export interface SceneEventListOptions {
|
|
78
|
+
afterEventId?: number;
|
|
79
|
+
limit?: number;
|
|
80
|
+
}
|
|
81
|
+
export interface ProjectCreateOptions {
|
|
82
|
+
id?: SceneId;
|
|
83
|
+
name: string;
|
|
84
|
+
ownerId?: string | null;
|
|
85
|
+
isPrivate?: boolean;
|
|
86
|
+
}
|
|
87
|
+
export interface ProjectStatus {
|
|
88
|
+
id: SceneId;
|
|
89
|
+
projectId: string;
|
|
90
|
+
name: string;
|
|
91
|
+
editorUrl: string;
|
|
92
|
+
url: string;
|
|
93
|
+
ownerId: string | null;
|
|
94
|
+
thumbnailUrl: string | null;
|
|
95
|
+
publishedVersion: number | null;
|
|
96
|
+
latestVersion: number | null;
|
|
97
|
+
draftVersion: number | null;
|
|
98
|
+
browserVisibleVersion: number | null;
|
|
99
|
+
/** Alias for the browser-visible/latest meaningful version. */
|
|
100
|
+
version: number;
|
|
101
|
+
isEmpty: boolean;
|
|
102
|
+
sizeBytes: number;
|
|
103
|
+
nodeCount: number;
|
|
104
|
+
graphHash: string | null;
|
|
105
|
+
createdAt: string;
|
|
106
|
+
updatedAt: string;
|
|
107
|
+
}
|
|
108
|
+
export interface SceneStore {
|
|
109
|
+
readonly backend: 'sqlite' | 'supabase';
|
|
110
|
+
createProject?(opts: ProjectCreateOptions): Promise<ProjectStatus>;
|
|
111
|
+
getProjectStatus?(id: SceneId): Promise<ProjectStatus | null>;
|
|
112
|
+
save(opts: SceneSaveOptions): Promise<SceneMeta>;
|
|
113
|
+
load(id: SceneId): Promise<SceneWithGraph | null>;
|
|
114
|
+
list(opts?: SceneListOptions): Promise<SceneMeta[]>;
|
|
115
|
+
delete(id: SceneId, opts?: SceneMutateOptions): Promise<boolean>;
|
|
116
|
+
rename(id: SceneId, newName: string, opts?: SceneMutateOptions): Promise<SceneMeta>;
|
|
117
|
+
appendSceneEvent?(opts: SceneEventAppendOptions): Promise<SceneEvent>;
|
|
118
|
+
listSceneEvents?(sceneId: SceneId, opts?: SceneEventListOptions): Promise<SceneEvent[]>;
|
|
119
|
+
}
|
|
120
|
+
export declare class SceneNotFoundError extends Error {
|
|
121
|
+
readonly code: "not_found";
|
|
122
|
+
constructor(message?: string);
|
|
123
|
+
}
|
|
124
|
+
export declare class SceneVersionConflictError extends Error {
|
|
125
|
+
readonly code: "version_conflict";
|
|
126
|
+
constructor(message?: string);
|
|
127
|
+
}
|
|
128
|
+
export declare class SceneInvalidError extends Error {
|
|
129
|
+
readonly code: "invalid";
|
|
130
|
+
constructor(message?: string);
|
|
131
|
+
}
|
|
132
|
+
export declare class SceneTooLargeError extends Error {
|
|
133
|
+
readonly code: "too_large";
|
|
134
|
+
constructor(message?: string);
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/storage/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export class SceneNotFoundError extends Error {
|
|
2
|
+
code = 'not_found';
|
|
3
|
+
constructor(message = 'Scene not found') {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = 'SceneNotFoundError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export class SceneVersionConflictError extends Error {
|
|
9
|
+
code = 'version_conflict';
|
|
10
|
+
constructor(message = 'Scene version conflict') {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'SceneVersionConflictError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class SceneInvalidError extends Error {
|
|
16
|
+
code = 'invalid';
|
|
17
|
+
constructor(message = 'Scene invalid') {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'SceneInvalidError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class SceneTooLargeError extends Error {
|
|
23
|
+
code = 'too_large';
|
|
24
|
+
constructor(message = 'Scene too large') {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = 'SceneTooLargeError';
|
|
27
|
+
}
|
|
28
|
+
}
|