@genex-ai/cli-demo 1.10.3-dev.535 → 1.11.0-dev.538
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/dist/blender-mcp-OXFN3RVI.js +210 -0
- package/dist/chunk-ZHIP7LCA.js +178 -0
- package/dist/index.js +628 -452
- package/package.json +1 -1
- package/templates/skills/genex-ai-menu/SKILL.md +15 -11
- package/templates/skills/genex-ai-video/SKILL.md +42 -15
- package/templates/skills/genex-tool-video/SKILL.md +8 -2
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BLENDER_SETUP_HINT,
|
|
3
|
+
blenderCall,
|
|
4
|
+
blenderEndpoint,
|
|
5
|
+
getCliVersion,
|
|
6
|
+
isRenderMode,
|
|
7
|
+
sceneSummary
|
|
8
|
+
} from "./chunk-ZHIP7LCA.js";
|
|
9
|
+
|
|
10
|
+
// src/commands/blender-mcp.ts
|
|
11
|
+
import fs from "fs/promises";
|
|
12
|
+
import path from "path";
|
|
13
|
+
var FALLBACK_PROTOCOL = "2025-06-18";
|
|
14
|
+
var MODE_SCHEMA = {
|
|
15
|
+
type: "string",
|
|
16
|
+
enum: ["solid", "wireframe", "normals", "lit"],
|
|
17
|
+
description: "solid = material colours; wireframe = topology; normals = matcap that exposes inverted faces; lit = EEVEE, real lighting and shadows \u2014 GPU tier only, and it errors on the CPU tier rather than quietly falling back."
|
|
18
|
+
};
|
|
19
|
+
function sheet(reply2) {
|
|
20
|
+
return reply2.contactSheetPng ? [{ type: "image", data: reply2.contactSheetPng, mimeType: "image/png" }] : [];
|
|
21
|
+
}
|
|
22
|
+
var TOOLS = [
|
|
23
|
+
{
|
|
24
|
+
name: "blender_exec",
|
|
25
|
+
description: "Run Python (bpy) against the live Blender scene and get back a 4-view contact sheet plus the scene's numeric truths. The scene PERSISTS between calls, so you can build incrementally and a helper defined in one call is available in the next. Assert on the numbers; use the picture to decide what to change next.",
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
script: { type: "string", description: "Python source. `bpy`, `bmesh`, `mathutils`, `math`, `random` are in scope." },
|
|
30
|
+
mode: MODE_SCHEMA,
|
|
31
|
+
verify: {
|
|
32
|
+
type: "boolean",
|
|
33
|
+
description: "Set false to skip the render for a bulk sub-step. Defaults true \u2014 you should almost always look."
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
required: ["script"]
|
|
37
|
+
},
|
|
38
|
+
run: async (a, base) => {
|
|
39
|
+
const r = await blenderCall(base, "/exec", {
|
|
40
|
+
script: String(a.script ?? ""),
|
|
41
|
+
// Omitted rather than defaulted: the server picks per tier (`lit` on
|
|
42
|
+
// GPU, `solid` on CPU) and echoes what it used.
|
|
43
|
+
...isRenderMode(a.mode) ? { mode: a.mode } : {},
|
|
44
|
+
verify: a.verify !== false
|
|
45
|
+
});
|
|
46
|
+
const lines = [];
|
|
47
|
+
if (r.stdout?.trim()) lines.push(r.stdout.trimEnd());
|
|
48
|
+
if (r.stderr?.trim()) lines.push(r.stderr.trimEnd());
|
|
49
|
+
if (r.error) {
|
|
50
|
+
lines.push(`ERROR:
|
|
51
|
+
${r.error.trimEnd()}`);
|
|
52
|
+
}
|
|
53
|
+
if (r.sceneRestored?.restored) {
|
|
54
|
+
lines.push(
|
|
55
|
+
`NOTE: the service restarted (${r.sceneRestored.reason ?? "unknown"}) and the scene was restored from a checkpoint. Your most recent step may be missing \u2014 check the scene before continuing.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
lines.push(sceneSummary(r.scene));
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: "text", text: lines.filter(Boolean).join("\n") }, ...sheet(r)],
|
|
61
|
+
// A raised script is a failed call, even though HTTP said 200 and the
|
|
62
|
+
// sheet came back. Without this the model reads "ERROR:" as prose.
|
|
63
|
+
isError: Boolean(r.error)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "blender_render",
|
|
69
|
+
description: "Re-render the current scene without changing it. Use `normals` to check for inverted faces and `wireframe` to check topology.",
|
|
70
|
+
inputSchema: { type: "object", properties: { mode: MODE_SCHEMA } },
|
|
71
|
+
run: async (a, base) => {
|
|
72
|
+
const r = await blenderCall(base, "/render", isRenderMode(a.mode) ? { mode: a.mode } : {});
|
|
73
|
+
return {
|
|
74
|
+
content: [
|
|
75
|
+
{ type: "text", text: `rendered ${String(r.mode ?? "")} in ${r.ms ?? "?"}ms`.replace(" ", " ") },
|
|
76
|
+
...sheet(r)
|
|
77
|
+
]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "blender_scene_info",
|
|
83
|
+
description: "The scene graph as JSON \u2014 every object with its type, triangle count, location, dimensions and materials, plus scene bounds. This is the gate: a render judge is unreliable for pass/fail, so verify structure here.",
|
|
84
|
+
inputSchema: { type: "object", properties: {} },
|
|
85
|
+
run: async (_a, base) => {
|
|
86
|
+
const r = await blenderCall(base, "/scene");
|
|
87
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "blender_import_model",
|
|
92
|
+
description: "Import a GLB into the current scene by URL, so a generated asset can be composed with the rest.",
|
|
93
|
+
inputSchema: {
|
|
94
|
+
type: "object",
|
|
95
|
+
properties: { url: { type: "string", description: "Public URL of a .glb" } },
|
|
96
|
+
required: ["url"]
|
|
97
|
+
},
|
|
98
|
+
run: async (a, base) => {
|
|
99
|
+
const r = await blenderCall(base, "/import", { url: String(a.url ?? "") });
|
|
100
|
+
return { content: [{ type: "text", text: `imported ${r.count ?? 0}: ${(r.imported ?? []).join(", ")}` }] };
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "blender_export_glb",
|
|
105
|
+
description: "Export the scene as a GLB. With `savePath` the bytes are written to that local file, which is how the result gets into a game.",
|
|
106
|
+
inputSchema: {
|
|
107
|
+
type: "object",
|
|
108
|
+
properties: {
|
|
109
|
+
savePath: { type: "string", description: "Local path to write, e.g. public/assets/scene.glb" }
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
run: async (a, base) => {
|
|
113
|
+
const r = await blenderCall(base, "/export", { path: "/tmp/genex-export.glb" });
|
|
114
|
+
let where = "(not saved locally \u2014 pass savePath to write it)";
|
|
115
|
+
if (typeof a.savePath === "string" && a.savePath.trim()) {
|
|
116
|
+
const target = path.resolve(a.savePath.trim());
|
|
117
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
118
|
+
await fs.writeFile(target, Buffer.from(r.glbBase64 ?? "", "base64"));
|
|
119
|
+
where = target;
|
|
120
|
+
}
|
|
121
|
+
return { content: [{ type: "text", text: `exported ${r.bytes ?? 0} bytes \u2192 ${where}` }] };
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "blender_reset",
|
|
126
|
+
description: "Empty the scene and the Python namespace. Start here when beginning something unrelated.",
|
|
127
|
+
inputSchema: { type: "object", properties: {} },
|
|
128
|
+
run: async (_a, base) => {
|
|
129
|
+
const r = await blenderCall(base, "/reset", {});
|
|
130
|
+
return { content: [{ type: "text", text: `reset. ${sceneSummary(r.scene)}` }] };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
];
|
|
134
|
+
function write(msg) {
|
|
135
|
+
process.stdout.write(`${JSON.stringify(msg)}
|
|
136
|
+
`);
|
|
137
|
+
}
|
|
138
|
+
function reply(id, result) {
|
|
139
|
+
write({ jsonrpc: "2.0", id, result });
|
|
140
|
+
}
|
|
141
|
+
function fail(id, code, message) {
|
|
142
|
+
write({ jsonrpc: "2.0", id, error: { code, message } });
|
|
143
|
+
}
|
|
144
|
+
async function handle(msg) {
|
|
145
|
+
const { method, id } = msg;
|
|
146
|
+
if (id === void 0 || id === null) return;
|
|
147
|
+
if (method === "initialize") {
|
|
148
|
+
const asked = msg.params?.protocolVersion ?? FALLBACK_PROTOCOL;
|
|
149
|
+
return reply(id, {
|
|
150
|
+
protocolVersion: asked,
|
|
151
|
+
capabilities: { tools: {} },
|
|
152
|
+
serverInfo: { name: "genex-blender", version: getCliVersion() }
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (method === "ping") return reply(id, {});
|
|
156
|
+
if (method === "tools/list") {
|
|
157
|
+
return reply(id, {
|
|
158
|
+
tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }))
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (method === "tools/call") {
|
|
162
|
+
const name = msg.params?.name;
|
|
163
|
+
const args = msg.params?.arguments ?? {};
|
|
164
|
+
const tool = TOOLS.find((t) => t.name === name);
|
|
165
|
+
if (!tool) return fail(id, -32602, `Unknown tool: ${name}`);
|
|
166
|
+
const base = blenderEndpoint();
|
|
167
|
+
if (!base) {
|
|
168
|
+
return reply(id, { content: [{ type: "text", text: BLENDER_SETUP_HINT }], isError: true });
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
return reply(id, await tool.run(args, base));
|
|
172
|
+
} catch (err) {
|
|
173
|
+
return reply(id, {
|
|
174
|
+
content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
|
|
175
|
+
isError: true
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
fail(id, -32601, `Method not found: ${method}`);
|
|
180
|
+
}
|
|
181
|
+
async function runBlenderMcp() {
|
|
182
|
+
process.stderr.write(`[genex-blender-mcp] ${TOOLS.length} tools, cli ${getCliVersion()}
|
|
183
|
+
`);
|
|
184
|
+
if (!blenderEndpoint()) {
|
|
185
|
+
process.stderr.write("[genex-blender-mcp] GENEX_BLENDER_URL unset \u2014 tools will explain setup\n");
|
|
186
|
+
}
|
|
187
|
+
let buf = "";
|
|
188
|
+
process.stdin.setEncoding("utf8");
|
|
189
|
+
for await (const chunk of process.stdin) {
|
|
190
|
+
buf += chunk;
|
|
191
|
+
let nl;
|
|
192
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
193
|
+
const line = buf.slice(0, nl).trim();
|
|
194
|
+
buf = buf.slice(nl + 1);
|
|
195
|
+
if (!line) continue;
|
|
196
|
+
let msg;
|
|
197
|
+
try {
|
|
198
|
+
msg = JSON.parse(line);
|
|
199
|
+
} catch {
|
|
200
|
+
fail(null, -32700, "Parse error");
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
await handle(msg);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
export {
|
|
209
|
+
runBlenderMcp
|
|
210
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
var RAW_CHANNEL = "dev";
|
|
7
|
+
var CLI_CHANNEL = RAW_CHANNEL === "dev" ? "dev" : "latest";
|
|
8
|
+
var STANDS = {
|
|
9
|
+
prod: { api: "https://api.genex.games", dashboard: "https://genex.games" },
|
|
10
|
+
dev: { api: "https://api-dev.genex.games", dashboard: "https://dev.genex.games" }
|
|
11
|
+
};
|
|
12
|
+
var DEFAULT_AUTH_URL = CLI_CHANNEL === "dev" ? STANDS.dev.dashboard : STANDS.prod.dashboard;
|
|
13
|
+
var DEFAULT_API_URL = CLI_CHANNEL === "dev" ? STANDS.dev.api : STANDS.prod.api;
|
|
14
|
+
var DEFAULT_ANIMS_BASE = "https://cdn.genex.technology/anims/ual1/v1/";
|
|
15
|
+
var ANIMS_BASE_ENV = "GENEX_ANIMS_BASE";
|
|
16
|
+
function getAnimsBase(override) {
|
|
17
|
+
const raw = override || process.env[ANIMS_BASE_ENV] || DEFAULT_ANIMS_BASE;
|
|
18
|
+
return raw.replace(/\/+$/, "") + "/";
|
|
19
|
+
}
|
|
20
|
+
function getAnimsCacheDir() {
|
|
21
|
+
return path.join(getGenexDir(), "cache", "anims");
|
|
22
|
+
}
|
|
23
|
+
var ENV_TOKEN_KEY = "GENEX_TOKEN";
|
|
24
|
+
var AUTH_URL_ENV = "GENEX_AUTH_URL";
|
|
25
|
+
var API_URL_ENV = "GENEX_API_URL";
|
|
26
|
+
var ENV_FILE_ENV = "GENEX_ENV_FILE";
|
|
27
|
+
function getAuthUrl(override) {
|
|
28
|
+
const raw = override || process.env[AUTH_URL_ENV] || DEFAULT_AUTH_URL;
|
|
29
|
+
return raw.replace(/\/+$/, "");
|
|
30
|
+
}
|
|
31
|
+
function getApiUrl(override) {
|
|
32
|
+
const raw = override || process.env[API_URL_ENV] || DEFAULT_API_URL;
|
|
33
|
+
return raw.replace(/\/+$/, "");
|
|
34
|
+
}
|
|
35
|
+
function getGenexDir() {
|
|
36
|
+
return path.join(os.homedir(), ".genex");
|
|
37
|
+
}
|
|
38
|
+
function getGenexEnvPath(override) {
|
|
39
|
+
if (override) return path.resolve(override);
|
|
40
|
+
const fromEnv = process.env[ENV_FILE_ENV];
|
|
41
|
+
if (fromEnv) return path.resolve(fromEnv);
|
|
42
|
+
return path.join(getGenexDir(), "env");
|
|
43
|
+
}
|
|
44
|
+
function getTemplatesDir() {
|
|
45
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
46
|
+
return path.resolve(here, "..", "templates");
|
|
47
|
+
}
|
|
48
|
+
function getCliVersion() {
|
|
49
|
+
try {
|
|
50
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
51
|
+
const pkg = JSON.parse(
|
|
52
|
+
fs.readFileSync(path.resolve(here, "..", "package.json"), "utf8")
|
|
53
|
+
);
|
|
54
|
+
return pkg.version ?? "0.0.0";
|
|
55
|
+
} catch {
|
|
56
|
+
return "0.0.0";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
var KNOWN_AGENTS = {
|
|
60
|
+
claude: { label: "Claude Code", dirName: ".claude", full: true },
|
|
61
|
+
codex: { label: "Codex", dirName: ".codex", full: false },
|
|
62
|
+
cursor: { label: "Cursor", dirName: ".cursor", full: false }
|
|
63
|
+
};
|
|
64
|
+
var KNOWN_AGENT_IDS = Object.keys(KNOWN_AGENTS);
|
|
65
|
+
function resolveAgentTargets(opts = {}) {
|
|
66
|
+
if (opts.dir) {
|
|
67
|
+
return [{ id: "custom", label: "workspace", baseDir: path.resolve(opts.dir), full: true }];
|
|
68
|
+
}
|
|
69
|
+
const home = os.homedir();
|
|
70
|
+
const projectRoot = process.cwd();
|
|
71
|
+
let ids;
|
|
72
|
+
if (opts.agents && opts.agents.length > 0) {
|
|
73
|
+
ids = opts.agents.filter((id) => KNOWN_AGENTS[id]);
|
|
74
|
+
} else {
|
|
75
|
+
ids = KNOWN_AGENT_IDS.filter((id) => {
|
|
76
|
+
const dirName = KNOWN_AGENTS[id].dirName;
|
|
77
|
+
return isDir(path.join(home, dirName)) || isDir(path.join(projectRoot, dirName));
|
|
78
|
+
});
|
|
79
|
+
if (ids.length === 0) ids = ["claude"];
|
|
80
|
+
}
|
|
81
|
+
return ids.map((id) => {
|
|
82
|
+
const def = KNOWN_AGENTS[id];
|
|
83
|
+
return { id, label: def.label, baseDir: path.join(projectRoot, def.dirName), full: def.full };
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function isDir(p) {
|
|
87
|
+
try {
|
|
88
|
+
return fs.statSync(p).isDirectory();
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/lib/blender-client.ts
|
|
95
|
+
var BLENDER_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
96
|
+
var RENDER_MODES = ["solid", "wireframe", "normals", "lit"];
|
|
97
|
+
function isRenderMode(v) {
|
|
98
|
+
return typeof v === "string" && RENDER_MODES.includes(v);
|
|
99
|
+
}
|
|
100
|
+
function blenderEndpoint() {
|
|
101
|
+
const raw = process.env.GENEX_BLENDER_URL?.trim();
|
|
102
|
+
return raw ? raw.replace(/\/+$/, "") : void 0;
|
|
103
|
+
}
|
|
104
|
+
var BLENDER_SETUP_HINT = [
|
|
105
|
+
"GENEX_BLENDER_URL is not set \u2014 there is no hosted Blender service yet.",
|
|
106
|
+
" Start one locally:",
|
|
107
|
+
" docker build --platform=linux/amd64 -t genex-blender:m0 apps/blender-service",
|
|
108
|
+
" docker run -d --platform=linux/amd64 -p 8099:8080 genex-blender:m0",
|
|
109
|
+
" export GENEX_BLENDER_URL=http://localhost:8099"
|
|
110
|
+
].join("\n");
|
|
111
|
+
async function blenderCall(base, route, body) {
|
|
112
|
+
const ctl = new AbortController();
|
|
113
|
+
const timer = setTimeout(() => ctl.abort(), BLENDER_TIMEOUT_MS);
|
|
114
|
+
try {
|
|
115
|
+
const res = await fetch(`${base}${route}`, {
|
|
116
|
+
method: body === void 0 ? "GET" : "POST",
|
|
117
|
+
headers: {
|
|
118
|
+
...body === void 0 ? {} : { "Content-Type": "application/json" },
|
|
119
|
+
// Sent only when set. The service treats an unset secret as open, which
|
|
120
|
+
// is right on localhost; the deployed compose makes it mandatory.
|
|
121
|
+
...process.env.GENEX_BLENDER_SECRET ? { "x-genex-internal": process.env.GENEX_BLENDER_SECRET } : {}
|
|
122
|
+
},
|
|
123
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
124
|
+
signal: ctl.signal
|
|
125
|
+
});
|
|
126
|
+
const text = await res.text();
|
|
127
|
+
let json;
|
|
128
|
+
try {
|
|
129
|
+
json = JSON.parse(text);
|
|
130
|
+
} catch {
|
|
131
|
+
throw new Error(`${route} answered ${res.status} with non-JSON: ${text.slice(0, 200)}`);
|
|
132
|
+
}
|
|
133
|
+
if (!res.ok) {
|
|
134
|
+
if (res.status === 401) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`${route} refused the request (401) \u2014 set GENEX_BLENDER_SECRET to the service's secret`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
throw new Error(`${route} failed (${res.status}): ${json.error ?? text.slice(0, 200)}`);
|
|
140
|
+
}
|
|
141
|
+
return json;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
144
|
+
throw new Error(`${route} timed out after ${BLENDER_TIMEOUT_MS / 1e3}s`);
|
|
145
|
+
}
|
|
146
|
+
throw err;
|
|
147
|
+
} finally {
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function sceneSummary(s) {
|
|
152
|
+
if (!s) return "";
|
|
153
|
+
return `objects ${s.objectCount} meshes ${s.meshCount} tris ${s.totalTris} materials ${s.materialCount} radius ${s.bounds.radius}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export {
|
|
157
|
+
CLI_CHANNEL,
|
|
158
|
+
STANDS,
|
|
159
|
+
DEFAULT_AUTH_URL,
|
|
160
|
+
DEFAULT_API_URL,
|
|
161
|
+
getAnimsBase,
|
|
162
|
+
getAnimsCacheDir,
|
|
163
|
+
ENV_TOKEN_KEY,
|
|
164
|
+
ENV_FILE_ENV,
|
|
165
|
+
getAuthUrl,
|
|
166
|
+
getApiUrl,
|
|
167
|
+
getGenexDir,
|
|
168
|
+
getGenexEnvPath,
|
|
169
|
+
getTemplatesDir,
|
|
170
|
+
getCliVersion,
|
|
171
|
+
resolveAgentTargets,
|
|
172
|
+
RENDER_MODES,
|
|
173
|
+
isRenderMode,
|
|
174
|
+
blenderEndpoint,
|
|
175
|
+
BLENDER_SETUP_HINT,
|
|
176
|
+
blenderCall,
|
|
177
|
+
sceneSummary
|
|
178
|
+
};
|