@kernel.chat/kbot 3.1.0 → 3.1.2

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,CA+pV3C"}
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,18 +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() {
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
+ }
80
102
  // ── Tool 1: scaffold_game ──────────────────────────────────────────
81
103
  /** Per-engine scaffold file generators. Each returns an array of [relativePath, content] */
82
104
  const scaffoldFiles = {
83
105
  godot(name, tpl) {
84
106
  const is3d = tpl === '3d';
85
107
  const mainScene = is3d
86
- ? `[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`
87
- : `[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`;
88
110
  return [
89
- ['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`],
90
112
  ['main.tscn', mainScene],
91
113
  ['.gitignore', '.godot/\n*.import\nexport_presets.cfg\n'],
92
114
  ];
@@ -95,7 +117,7 @@ export function registerGamedevTools() {
95
117
  const is3d = tpl === '3d';
96
118
  return [
97
119
  ['Assets/.gitkeep', ''],
98
- ['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`],
99
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`],
100
122
  ['.gitignore', '[Ll]ibrary/\n[Tt]emp/\n[Oo]bj/\n[Bb]uild/\n*.csproj\n*.sln\n*.pidb\n*.userprefs\n'],
101
123
  ];
@@ -125,9 +147,9 @@ export function registerGamedevTools() {
125
147
  const is3d = tpl === '3d';
126
148
  return [
127
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)],
128
- ['index.html', `<!DOCTYPE html>\n<html><head><title>${name}</title></head>\n<body><script type="module" src="/src/main.ts"></script></body></html>\n`],
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`],
129
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`],
130
- ['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`],
131
153
  ['tsconfig.json', JSON.stringify({ compilerOptions: { target: 'ES2020', module: 'ESNext', moduleResolution: 'bundler', strict: true, esModuleInterop: true }, include: ['src'] }, null, 2)],
132
154
  ['.gitignore', 'node_modules/\ndist/\n'],
133
155
  ];
@@ -136,7 +158,7 @@ export function registerGamedevTools() {
136
158
  const is3d = true; // Three.js is always 3D
137
159
  return [
138
160
  ['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: { three: '^0.170.0' }, devDependencies: { '@types/three': '^0.170.0', vite: '^5.0.0', typescript: '^5.4.0' } }, null, 2)],
139
- ['index.html', `<!DOCTYPE html>\n<html><head><title>${name}</title><style>body{margin:0;overflow:hidden}canvas{display:block}</style></head>\n<body><script type="module" src="/src/main.ts"></script></body></html>\n`],
161
+ ['index.html', `<!DOCTYPE html>\n<html><head><title>${htmlSafe(name)}</title><style>body{margin:0;overflow:hidden}canvas{display:block}</style></head>\n<body><script type="module" src="/src/main.ts"></script></body></html>\n`],
140
162
  ['src/main.ts', `import * as THREE from 'three'\n\nconst scene = new THREE.Scene()\nconst camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)\nconst renderer = new THREE.WebGLRenderer({ antialias: true })\nrenderer.setSize(window.innerWidth, window.innerHeight)\ndocument.body.appendChild(renderer.domElement)\n\nconst geometry = new THREE.BoxGeometry()\nconst material = new THREE.MeshStandardMaterial({ color: 0x4488ff })\nconst cube = new THREE.Mesh(geometry, material)\nscene.add(cube)\n\nscene.add(new THREE.AmbientLight(0x404040))\nconst light = new THREE.DirectionalLight(0xffffff, 1)\nlight.position.set(5, 5, 5)\nscene.add(light)\n\ncamera.position.z = 5\n\nfunction animate() {\n requestAnimationFrame(animate)\n cube.rotation.x += 0.01\n cube.rotation.y += 0.01\n renderer.render(scene, camera)\n}\nanimate()\n\nwindow.addEventListener('resize', () => {\n camera.aspect = window.innerWidth / window.innerHeight\n camera.updateProjectionMatrix()\n renderer.setSize(window.innerWidth, window.innerHeight)\n})\n`],
141
163
  ['tsconfig.json', JSON.stringify({ compilerOptions: { target: 'ES2020', module: 'ESNext', moduleResolution: 'bundler', strict: true, esModuleInterop: true }, include: ['src'] }, null, 2)],
142
164
  ['.gitignore', 'node_modules/\ndist/\n'],
@@ -145,7 +167,7 @@ export function registerGamedevTools() {
145
167
  playcanvas(name, tpl) {
146
168
  return [
147
169
  ['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: { playcanvas: '^2.1.0' }, devDependencies: { vite: '^5.0.0', typescript: '^5.4.0' } }, null, 2)],
148
- ['index.html', `<!DOCTYPE html>\n<html><head><title>${name}</title><style>body{margin:0;overflow:hidden}canvas{display:block}</style></head>\n<body><canvas id="app"></canvas><script type="module" src="/src/main.ts"></script></body></html>\n`],
170
+ ['index.html', `<!DOCTYPE html>\n<html><head><title>${htmlSafe(name)}</title><style>body{margin:0;overflow:hidden}canvas{display:block}</style></head>\n<body><canvas id="app"></canvas><script type="module" src="/src/main.ts"></script></body></html>\n`],
149
171
  ['src/main.ts', `import * as pc from 'playcanvas'\n\nconst canvas = document.getElementById('app') as HTMLCanvasElement\nconst app = new pc.Application(canvas, {})\napp.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW)\napp.setCanvasResolution(pc.RESOLUTION_AUTO)\n\nconst camera = new pc.Entity('camera')\ncamera.addComponent('camera', { clearColor: new pc.Color(0.1, 0.1, 0.15) })\ncamera.setPosition(0, 2, 5)\ncamera.lookAt(pc.Vec3.ZERO)\napp.root.addChild(camera)\n\nconst light = new pc.Entity('light')\nlight.addComponent('light')\nlight.setEulerAngles(45, 30, 0)\napp.root.addChild(light)\n\nconst box = new pc.Entity('box')\nbox.addComponent('render', { type: 'box' })\napp.root.addChild(box)\n\napp.on('update', (dt: number) => { box.rotate(10 * dt, 20 * dt, 0) })\napp.start()\n`],
150
172
  ['tsconfig.json', JSON.stringify({ compilerOptions: { target: 'ES2020', module: 'ESNext', moduleResolution: 'bundler', strict: true, esModuleInterop: true }, include: ['src'] }, null, 2)],
151
173
  ['.gitignore', 'node_modules/\ndist/\n'],
@@ -154,10 +176,10 @@ export function registerGamedevTools() {
154
176
  defold(name, tpl) {
155
177
  const is3d = tpl === '3d';
156
178
  return [
157
- ['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`],
158
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`],
159
181
  ['main/game.go', `components {\n id: "script"\n component: "/main/game.script"\n}\n`],
160
- ['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`],
161
183
  ['.gitignore', 'build/\n.internal/\n'],
162
184
  ];
163
185
  },
@@ -176,7 +198,7 @@ export function registerGamedevTools() {
176
198
  const engine = String(args.engine).toLowerCase();
177
199
  const name = String(args.name);
178
200
  const template = String(args.template || 'blank').toLowerCase();
179
- const outputDir = String(args.output_dir || `./${name}`);
201
+ const outputDir = safePath(String(args.output_dir || `./${name}`));
180
202
  if (!scaffoldFiles[engine]) {
181
203
  return `Error: Unknown engine "${engine}". Supported: ${Object.keys(scaffoldFiles).join(', ')}`;
182
204
  }
@@ -200,11 +222,11 @@ export function registerGamedevTools() {
200
222
  project: (s) => {
201
223
  const lines = ['[application]'];
202
224
  if (s.name)
203
- lines.push(`config/name="${s.name}"`);
225
+ lines.push(`config/name="${codeSafe(String(s.name), 'ini')}"`);
204
226
  if (s.main_scene)
205
- lines.push(`run/main_scene="${s.main_scene}"`);
227
+ lines.push(`run/main_scene="${codeSafe(String(s.main_scene), 'ini')}"`);
206
228
  lines.push('', '[rendering]');
207
- lines.push(`renderer/rendering_method="${s.renderer || 'forward_plus'}"`);
229
+ lines.push(`renderer/rendering_method="${codeSafe(String(s.renderer || 'forward_plus'), 'ini')}"`);
208
230
  if (s.vsync !== undefined)
209
231
  lines.push(`[display]\nwindow/vsync/vsync_mode=${s.vsync ? 1 : 0}`);
210
232
  if (s.width)
@@ -236,14 +258,14 @@ export function registerGamedevTools() {
236
258
  rendering: (s) => {
237
259
  const lines = ['[rendering]'];
238
260
  if (s.renderer)
239
- lines.push(`renderer/rendering_method="${s.renderer}"`);
261
+ lines.push(`renderer/rendering_method="${codeSafe(String(s.renderer), 'ini')}"`);
240
262
  if (s.msaa)
241
263
  lines.push(`anti_aliasing/quality/msaa_${s.msaa_type || '3d'}=${s.msaa}`);
242
264
  if (s.shadows !== undefined)
243
265
  lines.push(`lights_and_shadows/directional_shadow/size=${s.shadow_size || 4096}`);
244
266
  return lines.join('\n') + '\n';
245
267
  },
246
- 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`,
247
269
  audio: (s) => {
248
270
  const lines = ['[audio]'];
249
271
  if (s.bus_count)
@@ -271,7 +293,7 @@ export function registerGamedevTools() {
271
293
  bevy: {
272
294
  project: (s) => {
273
295
  const name = String(s.name || 'game').toLowerCase().replace(/[^a-z0-9-]/g, '-');
274
- 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`;
275
297
  },
276
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`,
277
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`,
@@ -283,7 +305,7 @@ export function registerGamedevTools() {
283
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),
284
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`,
285
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`,
286
- 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`,
287
309
  input: (s) => `// Input key mapping\nexport const KEYS = ${JSON.stringify(s, null, 2)} as const\n`,
288
310
  audio: (s) => `// Audio config\nexport const audioConfig = {\n disableWebAudio: ${s.disable_web_audio ?? false},\n noAudio: ${s.no_audio ?? false},\n}\n`,
289
311
  },
@@ -291,7 +313,7 @@ export function registerGamedevTools() {
291
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),
292
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`,
293
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`,
294
- 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`,
295
317
  input: (s) => `// Input mapping\nexport const INPUT_MAP = ${JSON.stringify(s, null, 2)} as const\n`,
296
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`,
297
319
  },
@@ -304,7 +326,7 @@ export function registerGamedevTools() {
304
326
  input: (s) => {
305
327
  const lines = ['[/Script/Engine.InputSettings]'];
306
328
  for (const [name, key] of Object.entries(s)) {
307
- 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})`);
308
330
  }
309
331
  return lines.join('\n') + '\n';
310
332
  },
@@ -316,12 +338,12 @@ export function registerGamedevTools() {
316
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),
317
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`,
318
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`,
319
- 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`,
320
342
  input: (s) => `// Input config\nexport const INPUT_MAP = ${JSON.stringify(s, null, 2)} as const\n`,
321
- 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`,
322
344
  },
323
345
  defold: {
324
- 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`,
325
347
  physics: (s) => `[physics]\ntype = ${s.physics_type || '2D'}\ngravity_y = ${s.gravity ?? -10}\nscale = ${s.scale ?? 0.02}\n`,
326
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`,
327
349
  build: (s) => `[native_extension]\napp_manifest = ${s.manifest || ''}\n`,
@@ -428,8 +450,11 @@ export function registerGamedevTools() {
428
450
  let language = String(args.language).toLowerCase();
429
451
  const target = String(args.target || 'desktop').toLowerCase();
430
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'];
431
454
  if (source.length < 300 && !source.includes('\n') && existsSync(source)) {
432
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'}`;
433
458
  source = readFileSync(source, 'utf-8');
434
459
  if (!args.language) {
435
460
  const extMap = { '.glsl': 'glsl', '.vert': 'glsl', '.frag': 'glsl', '.hlsl': 'hlsl', '.wgsl': 'wgsl', '.shader': 'hlsl', '.cg': 'hlsl' };
@@ -892,7 +917,7 @@ void fragment() {
892
917
  async execute(args) {
893
918
  const materialType = String(args.material_type).toLowerCase();
894
919
  const engine = String(args.engine).toLowerCase();
895
- const outputPath = args.output_path ? String(args.output_path) : null;
920
+ const outputPath = args.output_path ? safePath(String(args.output_path)) : null;
896
921
  let params = {};
897
922
  if (args.params) {
898
923
  try {
@@ -974,8 +999,8 @@ void fragment() {
974
999
  },
975
1000
  sphere(p) {
976
1001
  const radius = p.radius ?? 0.5;
977
- const rings = p.rings ?? 16;
978
- const segments = p.segments ?? 32;
1002
+ const rings = Math.min(p.rings ?? 16, 256);
1003
+ const segments = Math.min(p.segments ?? 32, 512);
979
1004
  const verts = [];
980
1005
  const norms = [];
981
1006
  const uvs = [];
@@ -1248,7 +1273,7 @@ void fragment() {
1248
1273
  tier: 'free',
1249
1274
  async execute(args) {
1250
1275
  const shape = String(args.shape).toLowerCase();
1251
- const outputPath = String(args.output_path);
1276
+ const outputPath = safePath(String(args.output_path));
1252
1277
  let params = {};
1253
1278
  if (args.params) {
1254
1279
  try {
@@ -1287,8 +1312,8 @@ void fragment() {
1287
1312
  timeout: 120_000,
1288
1313
  async execute(args) {
1289
1314
  const inputDir = String(args.input_dir);
1290
- const outputImage = String(args.output_image);
1291
- const outputData = String(args.output_data);
1315
+ const outputImage = safePath(String(args.output_image));
1316
+ const outputData = safePath(String(args.output_data));
1292
1317
  const algorithm = String(args.algorithm || 'maxrects');
1293
1318
  const maxSize = typeof args.max_size === 'number' ? args.max_size : 2048;
1294
1319
  const padding = typeof args.padding === 'number' ? args.padding : 2;
@@ -1466,7 +1491,7 @@ void fragment() {
1466
1491
  const atlasW = nextPow2(atlasSize.width);
1467
1492
  const atlasH = nextPow2(atlasSize.height);
1468
1493
  // Composite atlas using ImageMagick
1469
- ensureDir(outputImage);
1494
+ ensureDir(dirname(outputImage));
1470
1495
  const compositeArgs = [
1471
1496
  '-size', `${atlasW}x${atlasH}`, 'xc:transparent',
1472
1497
  ];
@@ -1571,7 +1596,7 @@ void fragment() {
1571
1596
  };
1572
1597
  metadata = JSON.stringify(jsonData, null, 2);
1573
1598
  }
1574
- ensureDir(outputData);
1599
+ ensureDir(dirname(outputData));
1575
1600
  writeFileSync(outputData, metadata);
1576
1601
  return `Sprite atlas packed successfully:
1577
1602
  Atlas: ${outputImage} (${atlasW}x${atlasH})
@@ -1596,8 +1621,14 @@ void fragment() {
1596
1621
  async execute(args) {
1597
1622
  const type = String(args.type).toLowerCase();
1598
1623
  const engine = String(args.engine || 'rapier').toLowerCase();
1599
- const params = args.params ? JSON.parse(String(args.params)) : {};
1600
- const outputPath = String(args.output_path);
1624
+ let params = {};
1625
+ try {
1626
+ params = args.params ? JSON.parse(String(args.params)) : {};
1627
+ }
1628
+ catch {
1629
+ return 'Error: params must be valid JSON';
1630
+ }
1631
+ const outputPath = safePath(String(args.output_path));
1601
1632
  const validTypes = ['rigidbody', 'softbody', 'ragdoll', 'vehicle', 'cloth', 'joints'];
1602
1633
  const validEngines = ['godot', 'unity', 'unreal', 'bevy', 'cannon', 'rapier', 'matter'];
1603
1634
  if (!validTypes.includes(type))
@@ -2859,7 +2890,7 @@ const JOINT_CONFIG = ${JSON.stringify({
2859
2890
  fileExt = 'cpp';
2860
2891
  else if (fileExt === 'ts' && engine === 'bevy')
2861
2892
  fileExt = 'rs';
2862
- ensureDir(outputPath);
2893
+ ensureDir(dirname(outputPath));
2863
2894
  writeFileSync(outputPath, code);
2864
2895
  return `Physics setup generated:
2865
2896
  Type: ${type}
@@ -2882,8 +2913,14 @@ const JOINT_CONFIG = ${JSON.stringify({
2882
2913
  async execute(args) {
2883
2914
  const effect = String(args.effect).toLowerCase();
2884
2915
  const engine = String(args.engine || 'three').toLowerCase();
2885
- const outputPath = String(args.output_path);
2886
- const overrides = args.params ? JSON.parse(String(args.params)) : {};
2916
+ const outputPath = safePath(String(args.output_path));
2917
+ let overrides = {};
2918
+ try {
2919
+ overrides = args.params ? JSON.parse(String(args.params)) : {};
2920
+ }
2921
+ catch {
2922
+ return 'Error: params must be valid JSON';
2923
+ }
2887
2924
  const validEffects = ['fire', 'smoke', 'rain', 'snow', 'sparks', 'magic', 'explosion', 'dust', 'bubbles', 'leaves', 'confetti'];
2888
2925
  const validEngines = ['godot', 'unity', 'unreal', 'three', 'phaser', 'pixi'];
2889
2926
  if (!validEffects.includes(effect))
@@ -3561,7 +3598,7 @@ ${effect === 'fire' || effect === 'smoke' ? ` // Grow over lifetime
3561
3598
  }
3562
3599
  if (!code)
3563
3600
  return `Error: No implementation for effect="${effect}" with engine="${engine}"`;
3564
- ensureDir(outputPath);
3601
+ ensureDir(dirname(outputPath));
3565
3602
  writeFileSync(outputPath, code);
3566
3603
  return `Particle system generated:
3567
3604
  Effect: ${effect}
@@ -3588,12 +3625,18 @@ ${effect === 'fire' || effect === 'smoke' ? ` // Grow over lifetime
3588
3625
  tier: 'free',
3589
3626
  async execute(args) {
3590
3627
  const type = String(args.type).toLowerCase();
3591
- const width = typeof args.width === 'number' ? args.width : 40;
3592
- const height = typeof args.height === 'number' ? args.height : 30;
3628
+ const width = Math.min(typeof args.width === 'number' ? args.width : 40, 1000);
3629
+ const height = Math.min(typeof args.height === 'number' ? args.height : 30, 1000);
3593
3630
  const seedVal = typeof args.seed === 'number' ? args.seed : Date.now();
3594
- const outputPath = String(args.output_path);
3631
+ const outputPath = safePath(String(args.output_path));
3595
3632
  const format = String(args.format || 'json');
3596
- const params = args.params ? JSON.parse(String(args.params)) : {};
3633
+ let params = {};
3634
+ try {
3635
+ params = args.params ? JSON.parse(String(args.params)) : {};
3636
+ }
3637
+ catch {
3638
+ return 'Error: params must be valid JSON';
3639
+ }
3597
3640
  const validTypes = ['dungeon', 'platformer', 'overworld', 'maze', 'arena'];
3598
3641
  if (!validTypes.includes(type))
3599
3642
  return `Error: Invalid type "${type}". Use: ${validTypes.join(', ')}`;
@@ -4114,7 +4157,7 @@ ${effect === 'fire' || effect === 'smoke' ? ` // Grow over lifetime
4114
4157
  };
4115
4158
  output = JSON.stringify(jsonData, null, 2);
4116
4159
  }
4117
- ensureDir(outputPath);
4160
+ ensureDir(dirname(outputPath));
4118
4161
  writeFileSync(outputPath, output);
4119
4162
  const floorCount = map.flat().filter(t => t === FLOOR || t === SPAWN || t === EXIT || t === DOOR).length;
4120
4163
  return `Level generated:
@@ -4142,7 +4185,7 @@ ${effect === 'fire' || effect === 'smoke' ? ` // Grow over lifetime
4142
4185
  async execute(args) {
4143
4186
  const tilesetType = String(args.tileset_type).toLowerCase();
4144
4187
  const terrain = String(args.terrain).toLowerCase();
4145
- const outputPath = String(args.output_path);
4188
+ const outputPath = safePath(String(args.output_path));
4146
4189
  const format = String(args.format || 'json');
4147
4190
  const validTypes = ['blob_47', 'wang_16', 'simple_4'];
4148
4191
  const validTerrains = ['grass', 'stone', 'water', 'sand', 'snow', 'lava'];
@@ -4155,7 +4198,12 @@ ${effect === 'fire' || effect === 'smoke' ? ` // Grow over lifetime
4155
4198
  // Parse or generate map data
4156
4199
  let mapData;
4157
4200
  if (args.map_data) {
4158
- mapData = JSON.parse(String(args.map_data));
4201
+ try {
4202
+ mapData = JSON.parse(String(args.map_data));
4203
+ }
4204
+ catch {
4205
+ return 'Error: map_data must be valid JSON';
4206
+ }
4159
4207
  }
4160
4208
  else {
4161
4209
  // Auto-generate a sample terrain map
@@ -4490,7 +4538,7 @@ tile_${i}/terrain_peering/left = ${cardW ? 0 : -1}`;
4490
4538
  };
4491
4539
  output = JSON.stringify(jsonOutput, null, 2);
4492
4540
  }
4493
- ensureDir(outputPath);
4541
+ ensureDir(dirname(outputPath));
4494
4542
  writeFileSync(outputPath, output);
4495
4543
  const terrainTileCount = tiledMap.flat().filter(t => t >= 0).length;
4496
4544
  return `Tilemap generated:
@@ -4516,8 +4564,14 @@ tile_${i}/terrain_peering/left = ${cardW ? 0 : -1}`;
4516
4564
  async execute(args) {
4517
4565
  const engine = String(args.engine || 'recast').toLowerCase();
4518
4566
  const agentType = String(args.agent_type || 'humanoid').toLowerCase();
4519
- const outputPath = String(args.output_path);
4520
- const overrides = args.params ? JSON.parse(String(args.params)) : {};
4567
+ const outputPath = safePath(String(args.output_path));
4568
+ let overrides = {};
4569
+ try {
4570
+ overrides = args.params ? JSON.parse(String(args.params)) : {};
4571
+ }
4572
+ catch {
4573
+ return 'Error: params must be valid JSON';
4574
+ }
4521
4575
  const validEngines = ['godot', 'unity', 'unreal', 'recast', 'three'];
4522
4576
  const validAgents = ['humanoid', 'vehicle', 'flying', 'small_creature'];
4523
4577
  if (!validEngines.includes(engine))
@@ -5449,7 +5503,7 @@ export class NavigationSystem {
5449
5503
  }
5450
5504
  if (!code)
5451
5505
  return `Error: No implementation for engine="${engine}"`;
5452
- ensureDir(outputPath);
5506
+ ensureDir(dirname(outputPath));
5453
5507
  writeFileSync(outputPath, code);
5454
5508
  return `Navigation mesh config generated:
5455
5509
  Engine: ${engine}
@@ -5619,8 +5673,17 @@ export class NavigationSystem {
5619
5673
  async execute(args) {
5620
5674
  const system = String(args.system).toLowerCase();
5621
5675
  const engine = String(args.engine || 'web').toLowerCase();
5622
- const outputPath = String(args.output_path);
5623
- const params = args.params ? JSON.parse(String(args.params)) : {};
5676
+ const outputPath = safePath(String(args.output_path));
5677
+ let params = {};
5678
+ try {
5679
+ params = args.params ? JSON.parse(String(args.params)) : {};
5680
+ }
5681
+ catch {
5682
+ return 'Error: params must be valid JSON';
5683
+ }
5684
+ const validEngines = ['godot', 'unity', 'unreal', 'web', 'bevy'];
5685
+ if (!validEngines.includes(engine))
5686
+ return `Error: engine must be one of: ${validEngines.join(', ')}`;
5624
5687
  const validSystems = ['spatial', 'music_layers', 'sound_bank', 'howler', 'web_audio'];
5625
5688
  if (!validSystems.includes(system)) {
5626
5689
  return `Error: Unknown audio system "${system}". Valid: ${validSystems.join(', ')}`;
@@ -7072,8 +7135,20 @@ export class AudioEngine {
7072
7135
  const architecture = String(args.architecture).toLowerCase();
7073
7136
  const transport = String(args.transport || 'websocket').toLowerCase();
7074
7137
  const framework = String(args.framework || 'raw').toLowerCase();
7075
- const outputDir = String(args.output_dir);
7076
- const features = args.features ? JSON.parse(String(args.features)) : ['lobby', 'state_sync'];
7138
+ const outputDir = safePath(String(args.output_dir));
7139
+ let features = ['lobby', 'state_sync'];
7140
+ try {
7141
+ features = args.features ? JSON.parse(String(args.features)) : ['lobby', 'state_sync'];
7142
+ }
7143
+ catch {
7144
+ return 'Error: features must be valid JSON';
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(', ')}`;
7077
7152
  const validArch = ['client_server', 'peer_to_peer', 'relay'];
7078
7153
  if (!validArch.includes(architecture)) {
7079
7154
  return `Error: Unknown architecture "${architecture}". Valid: ${validArch.join(', ')}`;
@@ -8478,7 +8553,7 @@ ${hasReconnect ? ' if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
8478
8553
  async execute(args) {
8479
8554
  const engine = String(args.engine || 'web').toLowerCase();
8480
8555
  const platforms = String(args.platforms).split(',').map(p => p.trim().toLowerCase());
8481
- const outputDir = String(args.output_dir);
8556
+ const outputDir = safePath(String(args.output_dir));
8482
8557
  const ci = String(args.ci || 'github_actions').toLowerCase();
8483
8558
  const validPlatforms = ['steam', 'itch', 'web', 'ios', 'android'];
8484
8559
  const invalid = platforms.filter(p => !validPlatforms.includes(p));
@@ -8980,8 +9055,14 @@ export default defineConfig({
8980
9055
  async execute(args) {
8981
9056
  const testType = String(args.test_type).toLowerCase();
8982
9057
  const engine = String(args.engine || 'web').toLowerCase();
8983
- const outputPath = String(args.output_path);
8984
- const params = args.params ? JSON.parse(String(args.params)) : {};
9058
+ const outputPath = safePath(String(args.output_path));
9059
+ let params = {};
9060
+ try {
9061
+ params = args.params ? JSON.parse(String(args.params)) : {};
9062
+ }
9063
+ catch {
9064
+ return 'Error: params must be valid JSON';
9065
+ }
8985
9066
  const validTypes = ['fps_profiler', 'memory_tracker', 'input_recorder', 'screenshot_test', 'performance_budget'];
8986
9067
  if (!validTypes.includes(testType)) {
8987
9068
  return `Error: Unknown test type "${testType}". Valid: ${validTypes.join(', ')}`;
@@ -10281,9 +10362,21 @@ export class PerformanceBudget {
10281
10362
  tier: 'free',
10282
10363
  async execute(args) {
10283
10364
  const framework = String(args.framework).toLowerCase();
10284
- const entities = JSON.parse(String(args.entities));
10285
- const outputDir = String(args.output_dir);
10286
- const systems = args.systems ? JSON.parse(String(args.systems)) : [];
10365
+ let entities = [];
10366
+ try {
10367
+ entities = JSON.parse(String(args.entities));
10368
+ }
10369
+ catch {
10370
+ return 'Error: entities must be valid JSON';
10371
+ }
10372
+ const outputDir = safePath(String(args.output_dir));
10373
+ let systems = [];
10374
+ try {
10375
+ systems = args.systems ? JSON.parse(String(args.systems)) : [];
10376
+ }
10377
+ catch {
10378
+ return 'Error: systems must be valid JSON';
10379
+ }
10287
10380
  const validFrameworks = ['bevy', 'unity_dots', 'bitecs', 'miniplex', 'ecsy'];
10288
10381
  if (!validFrameworks.includes(framework)) {
10289
10382
  return `Error: Unknown ECS framework "${framework}". Valid: ${validFrameworks.join(', ')}`;