@kernel.chat/kbot 3.1.1 → 3.1.3

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.
@@ -1 +1 @@
1
- {"version":3,"file":"gamedev.d.ts","sourceRoot":"","sources":["../../src/tools/gamedev.ts"],"names":[],"mappings":"AA2EA,wBAAgB,oBAAoB,IAAI,IAAI,CA0qV3C"}
1
+ {"version":3,"file":"gamedev.d.ts","sourceRoot":"","sources":["../../src/tools/gamedev.ts"],"names":[],"mappings":"AAoFA,wBAAgB,oBAAoB,IAAI,IAAI,CAgsV3C"}
@@ -3,7 +3,7 @@
3
3
  // for Godot, Unity, Unreal, Bevy, Phaser, Three.js, PlayCanvas, and Defold.
4
4
  import { registerTool } from './index.js';
5
5
  import { writeFileSync, readFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
6
- import { dirname, join, basename, extname } from 'node:path';
6
+ import { dirname, join, basename, extname, resolve, relative, isAbsolute } from 'node:path';
7
7
  import { execFile } from 'node:child_process';
8
8
  // ── Helpers ──────────────────────────────────────────────────────────
9
9
  /** Create directory tree if it doesn't exist */
@@ -75,19 +75,40 @@ function seededRng(seed) {
75
75
  return (s >>> 0) / 4294967296;
76
76
  };
77
77
  }
78
+ function safePath(userPath) {
79
+ const resolved = resolve(process.cwd(), userPath);
80
+ const rel = relative(process.cwd(), resolved);
81
+ if (rel.startsWith('..') || isAbsolute(rel)) {
82
+ throw new Error(`Path must be within the working directory: ${userPath}`);
83
+ }
84
+ return resolved;
85
+ }
78
86
  // ── Registration ─────────────────────────────────────────────────────
79
87
  export function registerGamedevTools() {
80
88
  const htmlSafe = (s) => s.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c] ?? c));
89
+ /** Escape a string for safe interpolation into generated source code */
90
+ function codeSafe(s, lang = 'js') {
91
+ if (lang === 'rust' || lang === 'toml')
92
+ return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
93
+ if (lang === 'gdscript' || lang === 'csharp')
94
+ return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
95
+ if (lang === 'lua')
96
+ return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
97
+ if (lang === 'ini')
98
+ return s.replace(/[=\n\r]/g, '_');
99
+ // js default
100
+ return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$').replace(/\n/g, '\\n');
101
+ }
81
102
  // ── Tool 1: scaffold_game ──────────────────────────────────────────
82
103
  /** Per-engine scaffold file generators. Each returns an array of [relativePath, content] */
83
104
  const scaffoldFiles = {
84
105
  godot(name, tpl) {
85
106
  const is3d = tpl === '3d';
86
107
  const mainScene = is3d
87
- ? `[gd_scene load_steps=2 format=3]\n\n[node name="${name}" type="Node3D"]\n\n[node name="Camera3D" type="Camera3D" parent="."]\ntransform = Transform3D(1,0,0,0,1,0,0,0,1,0,2,5)\n\n[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]\n`
88
- : `[gd_scene load_steps=2 format=3]\n\n[node name="${name}" type="Node2D"]\n`;
108
+ ? `[gd_scene load_steps=2 format=3]\n\n[node name="${codeSafe(name, 'ini')}" type="Node3D"]\n\n[node name="Camera3D" type="Camera3D" parent="."]\ntransform = Transform3D(1,0,0,0,1,0,0,0,1,0,2,5)\n\n[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]\n`
109
+ : `[gd_scene load_steps=2 format=3]\n\n[node name="${codeSafe(name, 'ini')}" type="Node2D"]\n`;
89
110
  return [
90
- ['project.godot', `[application]\nconfig/name="${name}"\nrun/main_scene="res://main.tscn"\nconfig/features=PackedStringArray("4.3")\n\n[rendering]\nrenderer/rendering_method="${is3d ? 'forward_plus' : 'gl_compatibility'}"\n`],
111
+ ['project.godot', `[application]\nconfig/name="${codeSafe(name, 'ini')}"\nrun/main_scene="res://main.tscn"\nconfig/features=PackedStringArray("4.3")\n\n[rendering]\nrenderer/rendering_method="${is3d ? 'forward_plus' : 'gl_compatibility'}"\n`],
91
112
  ['main.tscn', mainScene],
92
113
  ['.gitignore', '.godot/\n*.import\nexport_presets.cfg\n'],
93
114
  ];
@@ -96,7 +117,7 @@ export function registerGamedevTools() {
96
117
  const is3d = tpl === '3d';
97
118
  return [
98
119
  ['Assets/.gitkeep', ''],
99
- ['Assets/Scripts/GameManager.cs', `using UnityEngine;\n\nnamespace ${name.replace(/[^a-zA-Z0-9]/g, '')}\n{\n public class GameManager : MonoBehaviour\n {\n void Start() { Debug.Log("${name} started"); }\n void Update() { }\n }\n}\n`],
120
+ ['Assets/Scripts/GameManager.cs', `using UnityEngine;\n\nnamespace ${name.replace(/[^a-zA-Z0-9]/g, '')}\n{\n public class GameManager : MonoBehaviour\n {\n void Start() { Debug.Log("${codeSafe(name, 'csharp')} started"); }\n void Update() { }\n }\n}\n`],
100
121
  ['ProjectSettings/ProjectSettings.asset', `%YAML 1.1\n%TAG !u! tag:unity3d.com,2011:\n--- !u!129 &1\nPlayerSettings:\n productName: ${name}\n defaultScreenWidth: 1920\n defaultScreenHeight: 1080\n`],
101
122
  ['.gitignore', '[Ll]ibrary/\n[Tt]emp/\n[Oo]bj/\n[Bb]uild/\n*.csproj\n*.sln\n*.pidb\n*.userprefs\n'],
102
123
  ];
@@ -128,7 +149,7 @@ export function registerGamedevTools() {
128
149
  ['package.json', JSON.stringify({ name: name.toLowerCase().replace(/[^a-z0-9-]/g, '-'), version: '0.1.0', private: true, scripts: { dev: 'vite', build: 'vite build' }, dependencies: { phaser: '^3.80.0' }, devDependencies: { vite: '^5.0.0', typescript: '^5.4.0' } }, null, 2)],
129
150
  ['index.html', `<!DOCTYPE html>\n<html><head><title>${htmlSafe(name)}</title></head>\n<body><script type="module" src="/src/main.ts"></script></body></html>\n`],
130
151
  ['src/main.ts', `import Phaser from 'phaser'\nimport { MainScene } from './scenes/MainScene'\n\nnew Phaser.Game({\n type: Phaser.AUTO,\n width: 800,\n height: 600,\n physics: { default: 'arcade', arcade: { gravity: { x: 0, y: 300 }, debug: false } },\n scene: [MainScene],\n})\n`],
131
- ['src/scenes/MainScene.ts', `import Phaser from 'phaser'\n\nexport class MainScene extends Phaser.Scene {\n constructor() { super('MainScene') }\n preload() { }\n create() {\n this.add.text(400, 300, '${name}', { fontSize: '32px', color: '#fff' }).setOrigin(0.5)\n }\n}\n`],
152
+ ['src/scenes/MainScene.ts', `import Phaser from 'phaser'\n\nexport class MainScene extends Phaser.Scene {\n constructor() { super('MainScene') }\n preload() { }\n create() {\n this.add.text(400, 300, '${codeSafe(name, 'js')}', { fontSize: '32px', color: '#fff' }).setOrigin(0.5)\n }\n}\n`],
132
153
  ['tsconfig.json', JSON.stringify({ compilerOptions: { target: 'ES2020', module: 'ESNext', moduleResolution: 'bundler', strict: true, esModuleInterop: true }, include: ['src'] }, null, 2)],
133
154
  ['.gitignore', 'node_modules/\ndist/\n'],
134
155
  ];
@@ -155,10 +176,10 @@ export function registerGamedevTools() {
155
176
  defold(name, tpl) {
156
177
  const is3d = tpl === '3d';
157
178
  return [
158
- ['game.project', `[project]\ntitle = ${name}\n\n[display]\nwidth = 960\nheight = 640\n\n[bootstrap]\nmain_collection = /main/main.collectionc\n\n[physics]\ntype = ${is3d ? '3D' : '2D'}\n`],
179
+ ['game.project', `[project]\ntitle = ${codeSafe(name, 'ini')}\n\n[display]\nwidth = 960\nheight = 640\n\n[bootstrap]\nmain_collection = /main/main.collectionc\n\n[physics]\ntype = ${is3d ? '3D' : '2D'}\n`],
159
180
  ['main/main.collection', `name: "main"\ninstances {\n id: "go"\n prototype: "/main/game.go"\n position { x: 0.0 y: 0.0 z: 0.0 }\n}\n`],
160
181
  ['main/game.go', `components {\n id: "script"\n component: "/main/game.script"\n}\n`],
161
- ['main/game.script', `function init(self)\n msg.post(".", "acquire_input_focus")\n print("${name} started")\nend\n\nfunction update(self, dt)\nend\n\nfunction on_input(self, action_id, action)\nend\n`],
182
+ ['main/game.script', `function init(self)\n msg.post(".", "acquire_input_focus")\n print("${codeSafe(name, 'lua')} started")\nend\n\nfunction update(self, dt)\nend\n\nfunction on_input(self, action_id, action)\nend\n`],
162
183
  ['.gitignore', 'build/\n.internal/\n'],
163
184
  ];
164
185
  },
@@ -177,7 +198,7 @@ export function registerGamedevTools() {
177
198
  const engine = String(args.engine).toLowerCase();
178
199
  const name = String(args.name);
179
200
  const template = String(args.template || 'blank').toLowerCase();
180
- const outputDir = String(args.output_dir || `./${name}`);
201
+ const outputDir = safePath(String(args.output_dir || `./${name}`));
181
202
  if (!scaffoldFiles[engine]) {
182
203
  return `Error: Unknown engine "${engine}". Supported: ${Object.keys(scaffoldFiles).join(', ')}`;
183
204
  }
@@ -201,11 +222,11 @@ export function registerGamedevTools() {
201
222
  project: (s) => {
202
223
  const lines = ['[application]'];
203
224
  if (s.name)
204
- lines.push(`config/name="${s.name}"`);
225
+ lines.push(`config/name="${codeSafe(String(s.name), 'ini')}"`);
205
226
  if (s.main_scene)
206
- lines.push(`run/main_scene="${s.main_scene}"`);
227
+ lines.push(`run/main_scene="${codeSafe(String(s.main_scene), 'ini')}"`);
207
228
  lines.push('', '[rendering]');
208
- lines.push(`renderer/rendering_method="${s.renderer || 'forward_plus'}"`);
229
+ lines.push(`renderer/rendering_method="${codeSafe(String(s.renderer || 'forward_plus'), 'ini')}"`);
209
230
  if (s.vsync !== undefined)
210
231
  lines.push(`[display]\nwindow/vsync/vsync_mode=${s.vsync ? 1 : 0}`);
211
232
  if (s.width)
@@ -237,14 +258,14 @@ export function registerGamedevTools() {
237
258
  rendering: (s) => {
238
259
  const lines = ['[rendering]'];
239
260
  if (s.renderer)
240
- lines.push(`renderer/rendering_method="${s.renderer}"`);
261
+ lines.push(`renderer/rendering_method="${codeSafe(String(s.renderer), 'ini')}"`);
241
262
  if (s.msaa)
242
263
  lines.push(`anti_aliasing/quality/msaa_${s.msaa_type || '3d'}=${s.msaa}`);
243
264
  if (s.shadows !== undefined)
244
265
  lines.push(`lights_and_shadows/directional_shadow/size=${s.shadow_size || 4096}`);
245
266
  return lines.join('\n') + '\n';
246
267
  },
247
- build: (s) => `[export]\nplatform="${s.platform || 'linux'}"\narch="${s.arch || 'x86_64'}"\n`,
268
+ build: (s) => `[export]\nplatform="${codeSafe(String(s.platform || 'linux'), 'ini')}"\narch="${codeSafe(String(s.arch || 'x86_64'), 'ini')}"\n`,
248
269
  audio: (s) => {
249
270
  const lines = ['[audio]'];
250
271
  if (s.bus_count)
@@ -272,7 +293,7 @@ export function registerGamedevTools() {
272
293
  bevy: {
273
294
  project: (s) => {
274
295
  const name = String(s.name || 'game').toLowerCase().replace(/[^a-z0-9-]/g, '-');
275
- return `[package]\nname = "${name}"\nversion = "${s.version || '0.1.0'}"\nedition = "2021"\n\n[dependencies]\nbevy = { version = "0.15", features = [${(s.features || []).map((f) => `"${f}"`).join(', ')}] }\n\n[profile.dev]\nopt-level = 1\n[profile.dev.package."*"]\nopt-level = 3\n`;
296
+ return `[package]\nname = "${name}"\nversion = "${codeSafe(String(s.version || '0.1.0'), 'toml')}"\nedition = "2021"\n\n[dependencies]\nbevy = { version = "0.15", features = [${(s.features || []).map((f) => `"${codeSafe(f, 'toml')}"`).join(', ')}] }\n\n[profile.dev]\nopt-level = 1\n[profile.dev.package."*"]\nopt-level = 3\n`;
276
297
  },
277
298
  build: (s) => `# .cargo/config.toml\n[target.x86_64-unknown-linux-gnu]\nlinker = "clang"\nrustflags = ["-C", "link-arg=-fuse-ld=lld"]\n\n[target.x86_64-pc-windows-msvc]\nrustflags = ["-C", "link-arg=/DEBUG:NONE"]\n`,
278
299
  rendering: (s) => `// Rendering plugin configuration\nuse bevy::prelude::*;\n\npub fn rendering_plugin(app: &mut App) {\n app.insert_resource(Msaa::Sample${s.msaa || 4})\n .insert_resource(ClearColor(Color::srgb(${s.clear_r ?? 0.1}, ${s.clear_g ?? 0.1}, ${s.clear_b ?? 0.15})));\n}\n`,
@@ -284,7 +305,7 @@ export function registerGamedevTools() {
284
305
  project: (s) => JSON.stringify({ name: String(s.name || 'game').toLowerCase().replace(/[^a-z0-9-]/g, '-'), version: s.version || '0.1.0', private: true, scripts: { dev: 'vite', build: 'vite build' }, dependencies: { phaser: s.phaser_version || '^3.80.0' }, devDependencies: { vite: '^5.0.0', typescript: '^5.4.0' } }, null, 2),
285
306
  physics: (s) => `// Phaser physics config\nexport const physicsConfig: Phaser.Types.Physics.ArcadePhysicsConfig = {\n gravity: { x: ${s.gravity_x ?? 0}, y: ${s.gravity_y ?? 300} },\n debug: ${s.debug ?? false},\n}\n`,
286
307
  rendering: (s) => `// Phaser rendering config\nexport const renderConfig: Partial<Phaser.Types.Core.GameConfig> = {\n type: Phaser.${s.renderer === 'canvas' ? 'CANVAS' : s.renderer === 'webgl' ? 'WEBGL' : 'AUTO'},\n antialias: ${s.antialias ?? true},\n pixelArt: ${s.pixel_art ?? false},\n roundPixels: ${s.round_pixels ?? false},\n}\n`,
287
- build: (s) => `// vite.config.ts\nimport { defineConfig } from 'vite'\nexport default defineConfig({\n base: '${s.base || './'}',\n build: { target: '${s.target || 'es2020'}', outDir: '${s.outDir || 'dist'}' },\n})\n`,
308
+ build: (s) => `// vite.config.ts\nimport { defineConfig } from 'vite'\nexport default defineConfig({\n base: '${codeSafe(String(s.base || './'), 'js')}',\n build: { target: '${codeSafe(String(s.target || 'es2020'), 'js')}', outDir: '${codeSafe(String(s.outDir || 'dist'), 'js')}' },\n})\n`,
288
309
  input: (s) => `// Input key mapping\nexport const KEYS = ${JSON.stringify(s, null, 2)} as const\n`,
289
310
  audio: (s) => `// Audio config\nexport const audioConfig = {\n disableWebAudio: ${s.disable_web_audio ?? false},\n noAudio: ${s.no_audio ?? false},\n}\n`,
290
311
  },
@@ -292,7 +313,7 @@ export function registerGamedevTools() {
292
313
  project: (s) => JSON.stringify({ name: String(s.name || 'game').toLowerCase().replace(/[^a-z0-9-]/g, '-'), version: s.version || '0.1.0', private: true, scripts: { dev: 'vite', build: 'vite build' }, dependencies: { three: s.three_version || '^0.170.0' }, devDependencies: { '@types/three': s.three_version || '^0.170.0', vite: '^5.0.0', typescript: '^5.4.0' } }, null, 2),
293
314
  rendering: (s) => `// Three.js renderer config\nimport * as THREE from 'three'\n\nexport function createRenderer(canvas?: HTMLCanvasElement) {\n const renderer = new THREE.WebGLRenderer({ canvas, antialias: ${s.antialias ?? true}, alpha: ${s.alpha ?? false} })\n renderer.shadowMap.enabled = ${s.shadows ?? true}\n renderer.shadowMap.type = THREE.${s.shadow_type || 'PCFSoftShadowMap'}\n renderer.toneMapping = THREE.${s.tone_mapping || 'ACESFilmicToneMapping'}\n renderer.toneMappingExposure = ${s.exposure ?? 1.0}\n return renderer\n}\n`,
294
315
  physics: (s) => `// Physics config (rapier/cannon)\nexport const physicsConfig = {\n gravity: { x: ${s.gravity_x ?? 0}, y: ${s.gravity_y ?? -9.81}, z: ${s.gravity_z ?? 0} },\n timestep: ${s.timestep ?? 1 / 60},\n}\n`,
295
- build: (s) => `// vite.config.ts\nimport { defineConfig } from 'vite'\nexport default defineConfig({\n base: '${s.base || './'}',\n build: { target: '${s.target || 'es2020'}', outDir: '${s.outDir || 'dist'}' },\n})\n`,
316
+ build: (s) => `// vite.config.ts\nimport { defineConfig } from 'vite'\nexport default defineConfig({\n base: '${codeSafe(String(s.base || './'), 'js')}',\n build: { target: '${codeSafe(String(s.target || 'es2020'), 'js')}', outDir: '${codeSafe(String(s.outDir || 'dist'), 'js')}' },\n})\n`,
296
317
  input: (s) => `// Input mapping\nexport const INPUT_MAP = ${JSON.stringify(s, null, 2)} as const\n`,
297
318
  audio: (s) => `// Three.js audio config\nimport * as THREE from 'three'\n\nexport function createAudioListener(camera: THREE.Camera) {\n const listener = new THREE.AudioListener()\n camera.add(listener)\n return listener\n}\n`,
298
319
  },
@@ -305,7 +326,7 @@ export function registerGamedevTools() {
305
326
  input: (s) => {
306
327
  const lines = ['[/Script/Engine.InputSettings]'];
307
328
  for (const [name, key] of Object.entries(s)) {
308
- lines.push(`+ActionMappings=(ActionName="${name}",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=${key})`);
329
+ lines.push(`+ActionMappings=(ActionName="${codeSafe(String(name), 'ini')}",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=${key})`);
309
330
  }
310
331
  return lines.join('\n') + '\n';
311
332
  },
@@ -317,12 +338,12 @@ export function registerGamedevTools() {
317
338
  project: (s) => JSON.stringify({ name: s.name || 'game', version: s.version || '0.1.0', scripts: { dev: 'vite', build: 'vite build' }, dependencies: { playcanvas: s.pc_version || '^2.1.0' } }, null, 2),
318
339
  rendering: (s) => `// PlayCanvas rendering config\nexport const renderSettings = {\n antialias: ${s.antialias ?? true},\n shadows: ${s.shadows ?? true},\n gammaCorrection: pc.GAMMA_SRGB,\n toneMapping: pc.TONEMAP_ACES,\n}\n`,
319
340
  physics: (s) => `// PlayCanvas physics (ammo.js)\nexport const physicsConfig = {\n gravity: [${s.gravity_x ?? 0}, ${s.gravity_y ?? -9.81}, ${s.gravity_z ?? 0}],\n fixedTimeStep: ${s.timestep ?? 1 / 60},\n}\n`,
320
- build: (s) => `// vite.config.ts\nimport { defineConfig } from 'vite'\nexport default defineConfig({\n base: '${s.base || './'}',\n build: { target: 'es2020', outDir: '${s.outDir || 'dist'}' },\n})\n`,
341
+ build: (s) => `// vite.config.ts\nimport { defineConfig } from 'vite'\nexport default defineConfig({\n base: '${codeSafe(String(s.base || './'), 'js')}',\n build: { target: 'es2020', outDir: '${codeSafe(String(s.outDir || 'dist'), 'js')}' },\n})\n`,
321
342
  input: (s) => `// Input config\nexport const INPUT_MAP = ${JSON.stringify(s, null, 2)} as const\n`,
322
- audio: (s) => `// Audio config\nexport const audioConfig = { volume: ${s.volume ?? 1}, distanceModel: '${s.distance_model || 'inverse'}' }\n`,
343
+ audio: (s) => `// Audio config\nexport const audioConfig = { volume: ${s.volume ?? 1}, distanceModel: '${codeSafe(String(s.distance_model || 'inverse'), 'js')}' }\n`,
323
344
  },
324
345
  defold: {
325
- project: (s) => `[project]\ntitle = ${s.name || 'Game'}\n\n[display]\nwidth = ${s.width || 960}\nheight = ${s.height || 640}\n\n[bootstrap]\nmain_collection = ${s.main_collection || '/main/main.collectionc'}\n`,
346
+ project: (s) => `[project]\ntitle = ${codeSafe(String(s.name || 'Game'), 'ini')}\n\n[display]\nwidth = ${s.width || 960}\nheight = ${s.height || 640}\n\n[bootstrap]\nmain_collection = ${codeSafe(String(s.main_collection || '/main/main.collectionc'), 'ini')}\n`,
326
347
  physics: (s) => `[physics]\ntype = ${s.physics_type || '2D'}\ngravity_y = ${s.gravity ?? -10}\nscale = ${s.scale ?? 0.02}\n`,
327
348
  rendering: (s) => `[graphics]\ndefault_texture_min_filter = ${s.min_filter || 'linear'}\ndefault_texture_mag_filter = ${s.mag_filter || 'linear'}\nmax_draw_calls = ${s.max_draw_calls || 1024}\n`,
328
349
  build: (s) => `[native_extension]\napp_manifest = ${s.manifest || ''}\n`,
@@ -429,8 +450,11 @@ export function registerGamedevTools() {
429
450
  let language = String(args.language).toLowerCase();
430
451
  const target = String(args.target || 'desktop').toLowerCase();
431
452
  // If source looks like a file path, try to read it
453
+ const shaderExts = ['.glsl', '.hlsl', '.wgsl', '.metal', '.frag', '.vert', '.comp', '.geom', '.tesc', '.tese', '.gdshader', '.shader', '.cg', '.fx'];
432
454
  if (source.length < 300 && !source.includes('\n') && existsSync(source)) {
433
455
  const ext = extname(source).toLowerCase();
456
+ if (!shaderExts.includes(ext))
457
+ return `Error: shader_debug only reads shader files (${shaderExts.join(', ')}). Got: ${ext || 'no extension'}`;
434
458
  source = readFileSync(source, 'utf-8');
435
459
  if (!args.language) {
436
460
  const extMap = { '.glsl': 'glsl', '.vert': 'glsl', '.frag': 'glsl', '.hlsl': 'hlsl', '.wgsl': 'wgsl', '.shader': 'hlsl', '.cg': 'hlsl' };
@@ -893,7 +917,7 @@ void fragment() {
893
917
  async execute(args) {
894
918
  const materialType = String(args.material_type).toLowerCase();
895
919
  const engine = String(args.engine).toLowerCase();
896
- const outputPath = args.output_path ? String(args.output_path) : null;
920
+ const outputPath = args.output_path ? safePath(String(args.output_path)) : null;
897
921
  let params = {};
898
922
  if (args.params) {
899
923
  try {
@@ -1249,7 +1273,7 @@ void fragment() {
1249
1273
  tier: 'free',
1250
1274
  async execute(args) {
1251
1275
  const shape = String(args.shape).toLowerCase();
1252
- const outputPath = String(args.output_path);
1276
+ const outputPath = safePath(String(args.output_path));
1253
1277
  let params = {};
1254
1278
  if (args.params) {
1255
1279
  try {
@@ -1288,8 +1312,8 @@ void fragment() {
1288
1312
  timeout: 120_000,
1289
1313
  async execute(args) {
1290
1314
  const inputDir = String(args.input_dir);
1291
- const outputImage = String(args.output_image);
1292
- const outputData = String(args.output_data);
1315
+ const outputImage = safePath(String(args.output_image));
1316
+ const outputData = safePath(String(args.output_data));
1293
1317
  const algorithm = String(args.algorithm || 'maxrects');
1294
1318
  const maxSize = typeof args.max_size === 'number' ? args.max_size : 2048;
1295
1319
  const padding = typeof args.padding === 'number' ? args.padding : 2;
@@ -1604,7 +1628,7 @@ void fragment() {
1604
1628
  catch {
1605
1629
  return 'Error: params must be valid JSON';
1606
1630
  }
1607
- const outputPath = String(args.output_path);
1631
+ const outputPath = safePath(String(args.output_path));
1608
1632
  const validTypes = ['rigidbody', 'softbody', 'ragdoll', 'vehicle', 'cloth', 'joints'];
1609
1633
  const validEngines = ['godot', 'unity', 'unreal', 'bevy', 'cannon', 'rapier', 'matter'];
1610
1634
  if (!validTypes.includes(type))
@@ -2889,7 +2913,7 @@ const JOINT_CONFIG = ${JSON.stringify({
2889
2913
  async execute(args) {
2890
2914
  const effect = String(args.effect).toLowerCase();
2891
2915
  const engine = String(args.engine || 'three').toLowerCase();
2892
- const outputPath = String(args.output_path);
2916
+ const outputPath = safePath(String(args.output_path));
2893
2917
  let overrides = {};
2894
2918
  try {
2895
2919
  overrides = args.params ? JSON.parse(String(args.params)) : {};
@@ -3604,7 +3628,7 @@ ${effect === 'fire' || effect === 'smoke' ? ` // Grow over lifetime
3604
3628
  const width = Math.min(typeof args.width === 'number' ? args.width : 40, 1000);
3605
3629
  const height = Math.min(typeof args.height === 'number' ? args.height : 30, 1000);
3606
3630
  const seedVal = typeof args.seed === 'number' ? args.seed : Date.now();
3607
- const outputPath = String(args.output_path);
3631
+ const outputPath = safePath(String(args.output_path));
3608
3632
  const format = String(args.format || 'json');
3609
3633
  let params = {};
3610
3634
  try {
@@ -4161,7 +4185,7 @@ ${effect === 'fire' || effect === 'smoke' ? ` // Grow over lifetime
4161
4185
  async execute(args) {
4162
4186
  const tilesetType = String(args.tileset_type).toLowerCase();
4163
4187
  const terrain = String(args.terrain).toLowerCase();
4164
- const outputPath = String(args.output_path);
4188
+ const outputPath = safePath(String(args.output_path));
4165
4189
  const format = String(args.format || 'json');
4166
4190
  const validTypes = ['blob_47', 'wang_16', 'simple_4'];
4167
4191
  const validTerrains = ['grass', 'stone', 'water', 'sand', 'snow', 'lava'];
@@ -4540,7 +4564,7 @@ tile_${i}/terrain_peering/left = ${cardW ? 0 : -1}`;
4540
4564
  async execute(args) {
4541
4565
  const engine = String(args.engine || 'recast').toLowerCase();
4542
4566
  const agentType = String(args.agent_type || 'humanoid').toLowerCase();
4543
- const outputPath = String(args.output_path);
4567
+ const outputPath = safePath(String(args.output_path));
4544
4568
  let overrides = {};
4545
4569
  try {
4546
4570
  overrides = args.params ? JSON.parse(String(args.params)) : {};
@@ -5649,7 +5673,7 @@ export class NavigationSystem {
5649
5673
  async execute(args) {
5650
5674
  const system = String(args.system).toLowerCase();
5651
5675
  const engine = String(args.engine || 'web').toLowerCase();
5652
- const outputPath = String(args.output_path);
5676
+ const outputPath = safePath(String(args.output_path));
5653
5677
  let params = {};
5654
5678
  try {
5655
5679
  params = args.params ? JSON.parse(String(args.params)) : {};
@@ -5657,6 +5681,9 @@ export class NavigationSystem {
5657
5681
  catch {
5658
5682
  return 'Error: params must be valid JSON';
5659
5683
  }
5684
+ const validEngines = ['godot', 'unity', 'unreal', 'web', 'bevy'];
5685
+ if (!validEngines.includes(engine))
5686
+ return `Error: engine must be one of: ${validEngines.join(', ')}`;
5660
5687
  const validSystems = ['spatial', 'music_layers', 'sound_bank', 'howler', 'web_audio'];
5661
5688
  if (!validSystems.includes(system)) {
5662
5689
  return `Error: Unknown audio system "${system}". Valid: ${validSystems.join(', ')}`;
@@ -7108,7 +7135,7 @@ export class AudioEngine {
7108
7135
  const architecture = String(args.architecture).toLowerCase();
7109
7136
  const transport = String(args.transport || 'websocket').toLowerCase();
7110
7137
  const framework = String(args.framework || 'raw').toLowerCase();
7111
- const outputDir = String(args.output_dir);
7138
+ const outputDir = safePath(String(args.output_dir));
7112
7139
  let features = ['lobby', 'state_sync'];
7113
7140
  try {
7114
7141
  features = args.features ? JSON.parse(String(args.features)) : ['lobby', 'state_sync'];
@@ -7116,6 +7143,12 @@ export class AudioEngine {
7116
7143
  catch {
7117
7144
  return 'Error: features must be valid JSON';
7118
7145
  }
7146
+ const validTransports = ['websocket', 'webrtc'];
7147
+ if (!validTransports.includes(transport))
7148
+ return `Error: transport must be one of: ${validTransports.join(', ')}`;
7149
+ const validFrameworks = ['colyseus', 'socket_io', 'geckos', 'nakama', 'raw'];
7150
+ if (!validFrameworks.includes(framework))
7151
+ return `Error: framework must be one of: ${validFrameworks.join(', ')}`;
7119
7152
  const validArch = ['client_server', 'peer_to_peer', 'relay'];
7120
7153
  if (!validArch.includes(architecture)) {
7121
7154
  return `Error: Unknown architecture "${architecture}". Valid: ${validArch.join(', ')}`;
@@ -8520,7 +8553,7 @@ ${hasReconnect ? ' if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
8520
8553
  async execute(args) {
8521
8554
  const engine = String(args.engine || 'web').toLowerCase();
8522
8555
  const platforms = String(args.platforms).split(',').map(p => p.trim().toLowerCase());
8523
- const outputDir = String(args.output_dir);
8556
+ const outputDir = safePath(String(args.output_dir));
8524
8557
  const ci = String(args.ci || 'github_actions').toLowerCase();
8525
8558
  const validPlatforms = ['steam', 'itch', 'web', 'ios', 'android'];
8526
8559
  const invalid = platforms.filter(p => !validPlatforms.includes(p));
@@ -9022,7 +9055,7 @@ export default defineConfig({
9022
9055
  async execute(args) {
9023
9056
  const testType = String(args.test_type).toLowerCase();
9024
9057
  const engine = String(args.engine || 'web').toLowerCase();
9025
- const outputPath = String(args.output_path);
9058
+ const outputPath = safePath(String(args.output_path));
9026
9059
  let params = {};
9027
9060
  try {
9028
9061
  params = args.params ? JSON.parse(String(args.params)) : {};
@@ -10336,7 +10369,7 @@ export class PerformanceBudget {
10336
10369
  catch {
10337
10370
  return 'Error: entities must be valid JSON';
10338
10371
  }
10339
- const outputDir = String(args.output_dir);
10372
+ const outputDir = safePath(String(args.output_dir));
10340
10373
  let systems = [];
10341
10374
  try {
10342
10375
  systems = args.systems ? JSON.parse(String(args.systems)) : [];