@threenative/core 0.2.0 → 0.3.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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -0
  3. package/capabilities.json +5292 -0
  4. package/dist/assets-kyoF7JlJ.d.ts +103 -0
  5. package/dist/audio-BFiGneTL.d.ts +156 -0
  6. package/dist/canvas-layer-BLVijiUJ.d.ts +62 -0
  7. package/dist/game-XGrTzapq.d.ts +1166 -0
  8. package/dist/gpu-readback-D2iRvoe9.d.ts +112 -0
  9. package/dist/hot.d.ts +20 -2
  10. package/dist/hot.js +14 -2
  11. package/dist/index.d.ts +2108 -55
  12. package/dist/index.js +15645 -2222
  13. package/dist/net.d.ts +65 -0
  14. package/dist/net.js +643 -0
  15. package/dist/playtest.d.ts +37 -4
  16. package/dist/playtest.js +246 -546
  17. package/dist/react.d.ts +177 -0
  18. package/dist/react.js +635 -0
  19. package/dist/renderer-C6hqZpoG.d.ts +770 -0
  20. package/dist/ui-layer.d.ts +306 -0
  21. package/dist/ui-layer.js +425 -0
  22. package/dist/world.d.ts +254 -0
  23. package/dist/world.js +2686 -0
  24. package/gpl/LICENSE.GPL +117 -0
  25. package/gpl/convert.py +192 -0
  26. package/gpl/recipes/_common.py +169 -0
  27. package/gpl/recipes/bake_ao.py +111 -0
  28. package/gpl/recipes/decimate.py +64 -0
  29. package/gpl/recipes/retarget.py +131 -0
  30. package/gpl/recipes/unwrap.py +71 -0
  31. package/mcp/assets.mjs +5 -0
  32. package/mcp/blender-server.mjs +632 -0
  33. package/mcp/blender.mjs +27 -0
  34. package/mcp/engine-server.mjs +501 -0
  35. package/mcp/engine.mjs +31 -0
  36. package/mcp/install.d.mts +37 -0
  37. package/mcp/install.mjs +145 -0
  38. package/mcp/launch.mjs +72 -0
  39. package/mcp/sculpt.mjs +5 -0
  40. package/mcp/servers.d.mts +34 -0
  41. package/mcp/servers.mjs +160 -0
  42. package/package.json +76 -6
  43. package/patches/three@0.185.1.patch +522 -0
  44. package/scripts/apply-three-patch.mjs +297 -0
  45. package/scripts/ensure-mcp.mjs +43 -0
  46. package/scripts/postinstall.mjs +6 -0
  47. package/dist/audio-CEAw0w5y.d.ts +0 -35
  48. package/dist/game-DRt1Qhq3.d.ts +0 -429
@@ -0,0 +1,145 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { CODEX_MCP_SERVERS, mergeMcpServers } from "./servers.mjs";
4
+
5
+ /** The project being installed into, or undefined when this install has no project to write to —
6
+ * a nested dependency install, or core's own workspace build. */
7
+ export function installTarget(environment = process.env, cwd = process.cwd()) {
8
+ const target = path.resolve(environment.INIT_CWD ?? cwd);
9
+ if (target.split(path.sep).includes("node_modules")) return undefined;
10
+ const manifestPath = path.join(target, "package.json");
11
+ if (!existsSync(manifestPath)) return undefined;
12
+ let manifest;
13
+ try {
14
+ manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
15
+ } catch {
16
+ return undefined;
17
+ }
18
+ if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest))
19
+ return undefined;
20
+ if (manifest.name === "@threenative/core") return undefined;
21
+ const dependencyGroups = [
22
+ manifest.dependencies,
23
+ manifest.devDependencies,
24
+ manifest.optionalDependencies,
25
+ manifest.peerDependencies,
26
+ ];
27
+ const declaresCore = dependencyGroups.some(
28
+ (dependencies) =>
29
+ typeof dependencies === "object" &&
30
+ dependencies !== null &&
31
+ Object.hasOwn(dependencies, "@threenative/core"),
32
+ );
33
+ return declaresCore ? target : undefined;
34
+ }
35
+
36
+ /** Every agent host that reads a **project-scoped** MCP config, and the file each one reads.
37
+ *
38
+ * Project-scoped is the whole admission rule. A host that only reads a machine-wide config
39
+ * (Windsurf, Cline, Amp, the JetBrains assistants) is deliberately absent: installing a library
40
+ * into one game must never edit a file that governs every other project on the machine. Those
41
+ * hosts are wired by hand, and `docs/architecture/` says so. */
42
+ export const MCP_HOSTS = Object.freeze([
43
+ Object.freeze({
44
+ id: "claude-code",
45
+ label: "Claude Code",
46
+ file: ".mcp.json",
47
+ format: "mcpServers",
48
+ }),
49
+ Object.freeze({ id: "codex", label: "Codex", file: ".codex/config.toml", format: "codex" }),
50
+ Object.freeze({ id: "cursor", label: "Cursor", file: ".cursor/mcp.json", format: "mcpServers" }),
51
+ Object.freeze({ id: "vscode", label: "VS Code", file: ".vscode/mcp.json", format: "vscode" }),
52
+ Object.freeze({
53
+ id: "gemini-cli",
54
+ label: "Gemini CLI",
55
+ file: ".gemini/settings.json",
56
+ format: "mcpServers",
57
+ }),
58
+ Object.freeze({
59
+ id: "opencode",
60
+ label: "opencode",
61
+ file: "opencode.json",
62
+ format: "opencode",
63
+ // Written only when this installer creates the file, so a second install still reports
64
+ // "unchanged" rather than rewriting a config the user has since edited.
65
+ seed: Object.freeze({ $schema: "https://opencode.ai/config.json" }),
66
+ }),
67
+ Object.freeze({ id: "zed", label: "Zed", file: ".zed/settings.json", format: "zed" }),
68
+ ]);
69
+
70
+ /** Adds the ThreeNative servers to one host's JSON config, creating it when absent. Returns what it
71
+ * did so the installer can say so once and stay quiet otherwise. */
72
+ export function ensureJsonMcpConfig(target, file, format, seed = undefined) {
73
+ const configPath = path.join(target, file);
74
+ let existing;
75
+ if (existsSync(configPath)) {
76
+ // A config we cannot parse is still the user's. Rewriting it would drop servers we never wrote.
77
+ try {
78
+ existing = JSON.parse(readFileSync(configPath, "utf8"));
79
+ } catch {
80
+ return "unreadable";
81
+ }
82
+ }
83
+ let merged;
84
+ try {
85
+ merged = mergeMcpServers(existing, format);
86
+ } catch {
87
+ return "unreadable";
88
+ }
89
+ const { changed, config, conflicts } = merged;
90
+ if (!changed) return conflicts.length > 0 ? "conflict" : "unchanged";
91
+ const written = existing === undefined ? { ...seed, ...config } : config;
92
+ mkdirSync(path.dirname(configPath), { recursive: true });
93
+ writeFileSync(configPath, `${JSON.stringify(written, null, 2)}\n`);
94
+ return conflicts.length > 0 ? "conflict" : existing === undefined ? "created" : "updated";
95
+ }
96
+
97
+ /** Adds the ThreeNative servers to `<target>/.mcp.json`, the config Claude Code reads. */
98
+ export function ensureMcpConfig(target) {
99
+ return ensureJsonMcpConfig(target, ".mcp.json", "mcpServers");
100
+ }
101
+
102
+ /** Adds any missing ThreeNative servers to Codex's project-scoped MCP config without replacing
103
+ * user-authored settings or server definitions. TOML rather than JSON, so this one appends text
104
+ * instead of re-serialising: a config we cannot parse back is one we must not rewrite. */
105
+ export function ensureCodexMcpConfig(target) {
106
+ const directory = path.join(target, ".codex");
107
+ const configPath = path.join(directory, "config.toml");
108
+ const existing = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
109
+ const missing = CODEX_MCP_SERVERS.filter(
110
+ ({ name }) => !existing.includes(`[mcp_servers.${name}]`),
111
+ );
112
+ if (missing.length === 0) return "unchanged";
113
+ mkdirSync(directory, { recursive: true });
114
+ const separator =
115
+ existing.length === 0 || existing.endsWith("\n\n")
116
+ ? ""
117
+ : existing.endsWith("\n")
118
+ ? "\n"
119
+ : "\n\n";
120
+ writeFileSync(
121
+ configPath,
122
+ `${existing}${separator}${missing.map(({ body }) => body).join("\n\n")}\n`,
123
+ );
124
+ return existing.length === 0 ? "created" : "updated";
125
+ }
126
+
127
+ /** Wires every host in `MCP_HOSTS`, mapping host id to what the write did. One host's unreadable
128
+ * or unwritable config never stops the rest: a project with a hand-edited `.cursor/mcp.json`
129
+ * should still get its Codex and VS Code tools. */
130
+ export function ensureHostMcpConfigs(target) {
131
+ const outcomes = new Map();
132
+ for (const host of MCP_HOSTS) {
133
+ try {
134
+ outcomes.set(
135
+ host.id,
136
+ host.format === "codex"
137
+ ? ensureCodexMcpConfig(target)
138
+ : ensureJsonMcpConfig(target, host.file, host.format, host.seed),
139
+ );
140
+ } catch {
141
+ outcomes.set(host.id, "unreadable");
142
+ }
143
+ }
144
+ return outcomes;
145
+ }
package/mcp/launch.mjs ADDED
@@ -0,0 +1,72 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+
6
+ /** Where a package is installed, or undefined when it is not. Node's own resolver is the wrong tool
7
+ * here: `threenative-engine-mcp` publishes an `exports` map with only an `import` condition, which
8
+ * `createRequire().resolve` refuses, and the answer we want is the directory rather than a
9
+ * specifier's entry point. Walking `node_modules` upward from both the project and this package
10
+ * covers a direct dependency, a hoisted one, and pnpm's nested store alike. */
11
+ function packageDirectory(name) {
12
+ const starts = [process.cwd(), path.dirname(fileURLToPath(import.meta.url))];
13
+ for (const start of starts) {
14
+ let directory = path.resolve(start);
15
+ while (true) {
16
+ const candidate = path.join(directory, "node_modules", name);
17
+ if (existsSync(path.join(candidate, "package.json"))) return candidate;
18
+ const parent = path.dirname(directory);
19
+ if (parent === directory) break;
20
+ directory = parent;
21
+ }
22
+ }
23
+ return undefined;
24
+ }
25
+
26
+ /** The module that starts the server. The published main entry is not always it —
27
+ * `threenative-sculpt-mcp` exports a library from `dist/index.js` and starts its server from the
28
+ * `bin` file — so `bin` wins wherever a package declares one. */
29
+ function serverEntry(directory) {
30
+ const manifest = JSON.parse(readFileSync(path.join(directory, "package.json"), "utf8"));
31
+ const bin = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.[manifest.name];
32
+ const exported =
33
+ typeof manifest.exports === "string"
34
+ ? manifest.exports
35
+ : (manifest.exports?.["."]?.import ?? manifest.exports?.["."]);
36
+ const relative =
37
+ bin ??
38
+ (typeof exported === "string" ? exported : undefined) ??
39
+ manifest.main ??
40
+ manifest.module;
41
+ return relative === undefined ? undefined : path.resolve(directory, relative);
42
+ }
43
+
44
+ /** Runs an MCP server the project may or may not have installed. Nothing here may write to stdout:
45
+ * that stream is the JSON-RPC transport, and one stray line makes the server unusable. */
46
+ export async function launchMcpServer({ name, version, env = {} }) {
47
+ for (const [key, value] of Object.entries(env)) process.env[key] ??= value;
48
+ const directory = packageDirectory(name);
49
+ const entry = directory === undefined ? undefined : serverEntry(directory);
50
+ if (entry !== undefined && existsSync(entry)) {
51
+ // A server entry that is also a `bin` starts itself only when it is the process's main module,
52
+ // which it decides by comparing `process.argv[1]` against its own path. Importing it without
53
+ // this line loads the module and starts nothing, and the host reports a server that connected
54
+ // and then went silent.
55
+ process.argv[1] = entry;
56
+ await import(pathToFileURL(entry).href);
57
+ return;
58
+ }
59
+ // Not installed. `npx` keeps the server available in a project that never listed it, which is the
60
+ // whole point of wiring these in from the library rather than from a scaffold.
61
+ const child = spawn("npx", ["--yes", `${name}@${version}`], {
62
+ env: process.env,
63
+ stdio: "inherit",
64
+ });
65
+ child.on("error", (error) => {
66
+ process.stderr.write(`TN_MCP_LAUNCH: ${name} is not installed and npx failed: ${error}\n`);
67
+ process.exit(1);
68
+ });
69
+ child.on("exit", (code, signal) => {
70
+ process.exit(signal === null ? (code ?? 0) : 1);
71
+ });
72
+ }
package/mcp/sculpt.mjs ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { launchMcpServer } from "./launch.mjs";
3
+ import { MCP_PACKAGES } from "./servers.mjs";
4
+
5
+ await launchMcpServer(MCP_PACKAGES.sculpt);
@@ -0,0 +1,34 @@
1
+ // Types for `servers.mjs`. That file is plain JavaScript by design — `@threenative/core`'s
2
+ // postinstall runs it before anything is built — so it can never be a `.ts`. Without this
3
+ // declaration every consumer reached for `@ts-expect-error` and then cast the result back into a
4
+ // shape of its own, which is three private copies of one contract.
5
+
6
+ export interface IMcpServerEntry {
7
+ readonly args: readonly string[];
8
+ readonly command: string;
9
+ readonly env?: Readonly<Record<string, string>>;
10
+ }
11
+
12
+ export interface IMcpPackage {
13
+ readonly name: string;
14
+ readonly version: string;
15
+ }
16
+
17
+ /** One host's whole config with this format's server table merged in. The table's key is the
18
+ * host's own (`mcpServers`, `servers`, `mcp`, `context_servers`), so a caller reading a key it did
19
+ * not ask for gets `undefined` rather than a silently empty object. */
20
+ export interface IMcpConfigFormat {
21
+ readonly changed: boolean;
22
+ readonly config: Record<string, Record<string, Record<string, unknown>> | undefined>;
23
+ }
24
+
25
+ export interface ICodexMcpServer {
26
+ readonly body: string;
27
+ readonly name: string;
28
+ }
29
+
30
+ export declare const MCP_SERVERS: Readonly<Record<string, IMcpServerEntry>>;
31
+ export declare const MCP_PACKAGES: Readonly<Record<string, IMcpPackage>>;
32
+ export declare const SERVER_FORMAT_NAMES: readonly string[];
33
+ export declare const CODEX_MCP_SERVERS: readonly ICodexMcpServer[];
34
+ export declare function mergeMcpServers(existing: unknown, format?: string): IMcpConfigFormat;
@@ -0,0 +1,160 @@
1
+ // The MCP servers a ThreeNative project always has. `@threenative/core` is a direct dependency of
2
+ // every generated game, so `./node_modules/@threenative/core/mcp/<file>` is a path that resolves
3
+ // under npm, pnpm and yarn alike — unlike `./node_modules/threenative-asset-mcp/...`, which only
4
+ // exists when the package manager hoists, and which a project that never listed the server as a
5
+ // direct dependency does not have at all.
6
+ export const MCP_SERVERS = Object.freeze({
7
+ "threenative-assets": Object.freeze({
8
+ command: "node",
9
+ args: Object.freeze(["./node_modules/@threenative/core/mcp/assets.mjs"]),
10
+ // Entries rather than an object literal: these are environment variable names, not identifiers,
11
+ // and written as property names they read as names the naming rule must judge.
12
+ env: Object.freeze(
13
+ Object.fromEntries([
14
+ ["ASSET_DOWNLOAD_DIR", "./public/assets"],
15
+ ["AUDIO_DOWNLOAD_DIR", "./public/audio"],
16
+ ]),
17
+ ),
18
+ }),
19
+ "threenative-sculpt": Object.freeze({
20
+ command: "node",
21
+ args: Object.freeze(["./node_modules/@threenative/core/mcp/sculpt.mjs"]),
22
+ }),
23
+ "threenative-engine": Object.freeze({
24
+ command: "node",
25
+ args: Object.freeze(["./node_modules/@threenative/core/mcp/engine.mjs"]),
26
+ }),
27
+ // Drives an installed Blender headlessly so a downloaded .fbx becomes a GLB. It ships no
28
+ // Blender and probes for one only when a tool is called, so a project that never converts a
29
+ // model pays nothing for having this server wired.
30
+ "threenative-blender": Object.freeze({
31
+ command: "node",
32
+ args: Object.freeze(["./node_modules/@threenative/core/mcp/blender.mjs"]),
33
+ }),
34
+ });
35
+
36
+ /** The external package each shim launches. Core installs asset and sculpt transitively and bundles
37
+ * engine discovery itself: a game author must never know these package names or install them
38
+ * separately. The pinned engine version remains an emergency fallback for a legacy development
39
+ * checkout whose bundle has not been built. */
40
+ export const MCP_PACKAGES = Object.freeze({
41
+ assets: Object.freeze({ name: "threenative-asset-mcp", version: "0.8.0" }),
42
+ sculpt: Object.freeze({ name: "threenative-sculpt-mcp", version: "0.1.1" }),
43
+ engine: Object.freeze({ name: "threenative-engine-mcp", version: "0.2.1" }),
44
+ blender: Object.freeze({ name: "threenative-blender-mcp", version: "0.1.1" }),
45
+ });
46
+
47
+ /** How one host spells a stdio MCP server. Hosts agree on what to run and disagree on where to
48
+ * write it down, so the servers are declared once above and translated here. `key` is the property
49
+ * the host reads its server table from; `entry` is one server in that host's own shape. */
50
+ const SERVER_FORMATS = Object.freeze({
51
+ // Claude Code (`.mcp.json`), Cursor and the Gemini CLI all read this one.
52
+ mcpServers: Object.freeze({
53
+ key: "mcpServers",
54
+ entry: (server) => ({
55
+ command: server.command,
56
+ args: [...server.args],
57
+ ...(server.env === undefined ? {} : { env: { ...server.env } }),
58
+ }),
59
+ }),
60
+ // VS Code names the table `servers` and requires an explicit transport per server.
61
+ vscode: Object.freeze({
62
+ key: "servers",
63
+ entry: (server) => ({
64
+ type: "stdio",
65
+ command: server.command,
66
+ args: [...server.args],
67
+ ...(server.env === undefined ? {} : { env: { ...server.env } }),
68
+ }),
69
+ }),
70
+ // opencode folds the command and its arguments into one array and spells the environment
71
+ // `environment`. A server it does not mark enabled is written down and never started.
72
+ opencode: Object.freeze({
73
+ key: "mcp",
74
+ entry: (server) => ({
75
+ type: "local",
76
+ command: [server.command, ...server.args],
77
+ enabled: true,
78
+ ...(server.env === undefined ? {} : { environment: { ...server.env } }),
79
+ }),
80
+ }),
81
+ // Zed calls them context servers and needs `source: "custom"` to accept a command of our own.
82
+ zed: Object.freeze({
83
+ key: "context_servers",
84
+ entry: (server) => ({
85
+ source: "custom",
86
+ command: server.command,
87
+ args: [...server.args],
88
+ ...(server.env === undefined ? {} : { env: { ...server.env } }),
89
+ }),
90
+ }),
91
+ });
92
+
93
+ export const SERVER_FORMAT_NAMES = Object.freeze(Object.keys(SERVER_FORMATS));
94
+
95
+ /** Adds the ThreeNative servers to an existing config without touching anything else in it, and
96
+ * reports whether the file needs rewriting. Fails closed on a config that is not an object: an
97
+ * unreadable config is the user's, and overwriting it would lose servers we did not write. */
98
+ export function mergeMcpServers(existing, format = "mcpServers") {
99
+ const shape = SERVER_FORMATS[format];
100
+ if (shape === undefined) throw new Error(`Unknown MCP config format '${format}'.`);
101
+ const malformed =
102
+ existing !== undefined &&
103
+ (typeof existing !== "object" || existing === null || Array.isArray(existing));
104
+ if (malformed) {
105
+ throw new Error("MCP config root must be an object.");
106
+ }
107
+ const previous = existing ?? {};
108
+ const existingServers = previous[shape.key];
109
+ if (
110
+ existingServers !== undefined &&
111
+ (typeof existingServers !== "object" ||
112
+ existingServers === null ||
113
+ Array.isArray(existingServers))
114
+ ) {
115
+ throw new Error(`MCP config '${shape.key}' must be an object.`);
116
+ }
117
+ const servers = { ...(existingServers ?? {}) };
118
+ for (const [name, server] of Object.entries(servers)) {
119
+ if (typeof server !== "object" || server === null || Array.isArray(server)) {
120
+ throw new Error(`MCP server '${name}' in '${shape.key}' must be an object.`);
121
+ }
122
+ }
123
+ let changed = false;
124
+ const conflicts = [];
125
+ for (const [name, server] of Object.entries(MCP_SERVERS)) {
126
+ const wanted = shape.entry(server);
127
+ if (Object.hasOwn(servers, name)) {
128
+ if (JSON.stringify(servers[name]) !== JSON.stringify(wanted)) conflicts.push(name);
129
+ continue;
130
+ }
131
+ servers[name] = wanted;
132
+ changed = true;
133
+ }
134
+ return { changed, conflicts, config: { ...previous, [shape.key]: servers } };
135
+ }
136
+
137
+ /** One server as a Codex TOML table. Derived from `MCP_SERVERS` rather than written out beside it:
138
+ * two hand-maintained lists of the same three servers drift, and the drift is silent until an
139
+ * agent in Codex quietly has no asset tools. */
140
+ function codexServerBlock(name, server) {
141
+ const args = server.args.map((argument) => JSON.stringify(argument)).join(", ");
142
+ const lines = [
143
+ `[mcp_servers.${name}]`,
144
+ `command = ${JSON.stringify(server.command)}`,
145
+ `args = [${args}]`,
146
+ ];
147
+ if (server.env !== undefined) {
148
+ lines.push("", `[mcp_servers.${name}.env]`);
149
+ for (const [key, value] of Object.entries(server.env)) {
150
+ lines.push(`${key} = ${JSON.stringify(value)}`);
151
+ }
152
+ }
153
+ return lines.join("\n");
154
+ }
155
+
156
+ export const CODEX_MCP_SERVERS = Object.freeze(
157
+ Object.entries(MCP_SERVERS).map(([name, server]) =>
158
+ Object.freeze({ body: codexServerBlock(name, server), name }),
159
+ ),
160
+ );
package/package.json CHANGED
@@ -1,10 +1,39 @@
1
1
  {
2
2
  "name": "@threenative/core",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
+ "license": "MIT AND GPL-2.0-or-later",
4
5
  "description": "The small vanilla Three.js game runtime foundation.",
6
+ "keywords": [
7
+ "threenative",
8
+ "three",
9
+ "threejs",
10
+ "webgpu",
11
+ "game-engine",
12
+ "game-framework",
13
+ "gamedev",
14
+ "typescript"
15
+ ],
16
+ "homepage": "https://github.com/ThreeNativeHQ/threenative#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/ThreeNativeHQ/threenative/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/ThreeNativeHQ/threenative.git",
23
+ "directory": "packages/core"
24
+ },
5
25
  "type": "module",
6
26
  "files": [
7
- "dist"
27
+ "dist",
28
+ "LICENSE",
29
+ "mcp",
30
+ "gpl",
31
+ "scripts/apply-three-patch.mjs",
32
+ "scripts/ensure-mcp.mjs",
33
+ "scripts/postinstall.mjs",
34
+ "patches",
35
+ "capabilities.json",
36
+ "README.md"
8
37
  ],
9
38
  "main": "./dist/index.js",
10
39
  "module": "./dist/index.js",
@@ -22,6 +51,22 @@
22
51
  "types": "./dist/hot.d.ts",
23
52
  "import": "./dist/hot.js"
24
53
  },
54
+ "./react": {
55
+ "types": "./dist/react.d.ts",
56
+ "import": "./dist/react.js"
57
+ },
58
+ "./ui-layer": {
59
+ "types": "./dist/ui-layer.d.ts",
60
+ "import": "./dist/ui-layer.js"
61
+ },
62
+ "./world": {
63
+ "types": "./dist/world.d.ts",
64
+ "import": "./dist/world.js"
65
+ },
66
+ "./net": {
67
+ "types": "./dist/net.d.ts",
68
+ "import": "./dist/net.js"
69
+ },
25
70
  "./package.json": "./package.json"
26
71
  },
27
72
  "publishConfig": {
@@ -31,18 +76,43 @@
31
76
  "dependencies": {
32
77
  "three": "0.185.1",
33
78
  "three-mesh-bvh": "0.9.14",
34
- "zustand": "5.x"
79
+ "threenative-asset-mcp": "0.8.0",
80
+ "threenative-sculpt-mcp": "0.1.1",
81
+ "zustand": "5.0.14"
35
82
  },
36
83
  "devDependencies": {
84
+ "@types/react": "19.2.0",
85
+ "@types/react-reconciler": "0.32.1",
37
86
  "@types/three": "0.185.3",
38
87
  "publint": "0.3.22",
88
+ "react": "19.2.0",
89
+ "react-reconciler": "0.33.0",
39
90
  "tsup": "8.5.1",
40
91
  "typescript": "5.9.3",
41
- "@threenative/playtest": "0.2.0"
92
+ "@threenative/playtest": "0.3.1",
93
+ "threenative-engine-mcp": "0.2.1",
94
+ "threenative-blender-mcp": "0.1.1"
95
+ },
96
+ "peerDependencies": {
97
+ "@threenative/playtest": ">=0.3.1 <0.4.0",
98
+ "react": ">=19.2.0 <20",
99
+ "react-reconciler": ">=0.33.0 <0.34"
100
+ },
101
+ "peerDependenciesMeta": {
102
+ "@threenative/playtest": {
103
+ "optional": true
104
+ },
105
+ "react": {
106
+ "optional": true
107
+ },
108
+ "react-reconciler": {
109
+ "optional": true
110
+ }
42
111
  },
43
112
  "scripts": {
44
- "build": "tsup --config tsup.config.ts && publint",
113
+ "postinstall": "node ./scripts/postinstall.mjs",
114
+ "build": "node scripts/generate-version.mjs && tsup --config tsup.config.ts && node scripts/bundle-engine-mcp.mjs && node scripts/bundle-blender-mcp.mjs && publint",
45
115
  "typecheck": "tsc --noEmit",
46
- "test": "pnpm run build"
116
+ "test": "publint --strict"
47
117
  }
48
118
  }