luckyd-code 1.2.2__py3-none-any.whl
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.
- luckyd_code/__init__.py +54 -0
- luckyd_code/__main__.py +5 -0
- luckyd_code/_agent_loop.py +551 -0
- luckyd_code/_data_dir.py +73 -0
- luckyd_code/agent.py +38 -0
- luckyd_code/analytics/__init__.py +18 -0
- luckyd_code/analytics/reporter.py +195 -0
- luckyd_code/analytics/scanner.py +443 -0
- luckyd_code/analytics/smells.py +316 -0
- luckyd_code/analytics/trends.py +303 -0
- luckyd_code/api.py +473 -0
- luckyd_code/audit_daemon.py +845 -0
- luckyd_code/autonomous_fixer.py +473 -0
- luckyd_code/background.py +159 -0
- luckyd_code/backup.py +237 -0
- luckyd_code/brain/__init__.py +84 -0
- luckyd_code/brain/assembler.py +100 -0
- luckyd_code/brain/chunker.py +345 -0
- luckyd_code/brain/constants.py +73 -0
- luckyd_code/brain/embedder.py +163 -0
- luckyd_code/brain/graph.py +311 -0
- luckyd_code/brain/indexer.py +316 -0
- luckyd_code/brain/parser.py +140 -0
- luckyd_code/brain/retriever.py +234 -0
- luckyd_code/cli.py +894 -0
- luckyd_code/cli_commands/__init__.py +1 -0
- luckyd_code/cli_commands/audit.py +120 -0
- luckyd_code/cli_commands/background.py +83 -0
- luckyd_code/cli_commands/brain.py +87 -0
- luckyd_code/cli_commands/config.py +75 -0
- luckyd_code/cli_commands/dispatcher.py +695 -0
- luckyd_code/cli_commands/sessions.py +41 -0
- luckyd_code/cli_entry.py +147 -0
- luckyd_code/cli_utils.py +112 -0
- luckyd_code/config.py +205 -0
- luckyd_code/context.py +214 -0
- luckyd_code/cost_tracker.py +209 -0
- luckyd_code/error_reporter.py +508 -0
- luckyd_code/exceptions.py +39 -0
- luckyd_code/export.py +126 -0
- luckyd_code/feedback_analyzer.py +290 -0
- luckyd_code/file_watcher.py +258 -0
- luckyd_code/git/__init__.py +11 -0
- luckyd_code/git/auto_commit.py +157 -0
- luckyd_code/git/tools.py +85 -0
- luckyd_code/hooks.py +236 -0
- luckyd_code/indexer.py +280 -0
- luckyd_code/init.py +39 -0
- luckyd_code/keybindings.py +77 -0
- luckyd_code/log.py +55 -0
- luckyd_code/mcp/__init__.py +6 -0
- luckyd_code/mcp/client.py +184 -0
- luckyd_code/memory/__init__.py +19 -0
- luckyd_code/memory/manager.py +339 -0
- luckyd_code/metrics/__init__.py +5 -0
- luckyd_code/model_registry.py +131 -0
- luckyd_code/orchestrator.py +204 -0
- luckyd_code/permissions/__init__.py +1 -0
- luckyd_code/permissions/manager.py +103 -0
- luckyd_code/planner.py +361 -0
- luckyd_code/plugins.py +91 -0
- luckyd_code/py.typed +0 -0
- luckyd_code/retry.py +57 -0
- luckyd_code/router.py +417 -0
- luckyd_code/sandbox.py +156 -0
- luckyd_code/self_critique.py +2 -0
- luckyd_code/self_improve.py +274 -0
- luckyd_code/sessions.py +114 -0
- luckyd_code/settings.py +72 -0
- luckyd_code/skills/__init__.py +8 -0
- luckyd_code/skills/review.py +22 -0
- luckyd_code/skills/security.py +17 -0
- luckyd_code/tasks/__init__.py +1 -0
- luckyd_code/tasks/manager.py +102 -0
- luckyd_code/templates/icon-192.png +0 -0
- luckyd_code/templates/icon-512.png +0 -0
- luckyd_code/templates/index.html +1965 -0
- luckyd_code/templates/manifest.json +14 -0
- luckyd_code/templates/src/app.js +694 -0
- luckyd_code/templates/src/body.html +767 -0
- luckyd_code/templates/src/cdn.txt +2 -0
- luckyd_code/templates/src/style.css +474 -0
- luckyd_code/templates/sw.js +31 -0
- luckyd_code/templates/test.html +6 -0
- luckyd_code/themes.py +48 -0
- luckyd_code/tools/__init__.py +97 -0
- luckyd_code/tools/agent_tools.py +65 -0
- luckyd_code/tools/bash.py +360 -0
- luckyd_code/tools/brain_tools.py +137 -0
- luckyd_code/tools/browser.py +369 -0
- luckyd_code/tools/datetime_tool.py +34 -0
- luckyd_code/tools/dockerfile_gen.py +212 -0
- luckyd_code/tools/file_ops.py +381 -0
- luckyd_code/tools/game_gen.py +360 -0
- luckyd_code/tools/git_tools.py +130 -0
- luckyd_code/tools/git_worktree.py +63 -0
- luckyd_code/tools/path_validate.py +64 -0
- luckyd_code/tools/project_gen.py +187 -0
- luckyd_code/tools/readme_gen.py +227 -0
- luckyd_code/tools/registry.py +157 -0
- luckyd_code/tools/shell_detect.py +109 -0
- luckyd_code/tools/web.py +89 -0
- luckyd_code/tools/youtube.py +187 -0
- luckyd_code/tools_bridge.py +144 -0
- luckyd_code/undo.py +126 -0
- luckyd_code/update.py +60 -0
- luckyd_code/verify.py +360 -0
- luckyd_code/web_app.py +176 -0
- luckyd_code/web_routes/__init__.py +23 -0
- luckyd_code/web_routes/background.py +73 -0
- luckyd_code/web_routes/brain.py +109 -0
- luckyd_code/web_routes/cost.py +12 -0
- luckyd_code/web_routes/files.py +133 -0
- luckyd_code/web_routes/memories.py +94 -0
- luckyd_code/web_routes/misc.py +67 -0
- luckyd_code/web_routes/project.py +48 -0
- luckyd_code/web_routes/review.py +20 -0
- luckyd_code/web_routes/sessions.py +44 -0
- luckyd_code/web_routes/settings.py +43 -0
- luckyd_code/web_routes/static.py +70 -0
- luckyd_code/web_routes/update.py +19 -0
- luckyd_code/web_routes/ws.py +237 -0
- luckyd_code-1.2.2.dist-info/METADATA +297 -0
- luckyd_code-1.2.2.dist-info/RECORD +127 -0
- luckyd_code-1.2.2.dist-info/WHEEL +4 -0
- luckyd_code-1.2.2.dist-info/entry_points.txt +3 -0
- luckyd_code-1.2.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""Video game generator tool.
|
|
2
|
+
|
|
3
|
+
Generates standalone, playable Pygame games from a natural-language description
|
|
4
|
+
using the DeepSeek model. Optionally compiles them into self-contained .exe files
|
|
5
|
+
via PyInstaller. All games use SDL2's software "windib" driver so they run
|
|
6
|
+
without a GPU.
|
|
7
|
+
|
|
8
|
+
Any game concept is supported — just describe what you want.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from .registry import Tool
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Difficulty presets injected into every generation prompt
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
DIFFICULTIES = ("easy", "normal", "hard")
|
|
26
|
+
|
|
27
|
+
DIFFICULTY_HINTS = {
|
|
28
|
+
"easy": "Use slow speeds, generous hitboxes, and forgiving rules. Favor the player.",
|
|
29
|
+
"normal": "Use balanced speeds and standard game rules.",
|
|
30
|
+
"hard": "Use fast speeds, tight hitboxes, and punishing rules. Challenge the player.",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# System prompt for game generation
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
GAME_GEN_SYSTEM_PROMPT = """\
|
|
38
|
+
You are an expert Python game developer specialising in Pygame.
|
|
39
|
+
|
|
40
|
+
Your job is to write a COMPLETE, RUNNABLE, single-file Pygame game based on the
|
|
41
|
+
user's description. Follow every rule below without exception.
|
|
42
|
+
|
|
43
|
+
=== MANDATORY RULES ===
|
|
44
|
+
|
|
45
|
+
1. OUTPUT ONLY VALID PYTHON CODE — no markdown fences, no prose, no comments
|
|
46
|
+
outside the code. The very first character must be a double-quote (start of
|
|
47
|
+
the module docstring) or the letter 'i' (start of an import).
|
|
48
|
+
|
|
49
|
+
2. IMPORTS — always start with:
|
|
50
|
+
import os, sys, random, math
|
|
51
|
+
os.environ.setdefault("SDL_VIDEODRIVER", "windib")
|
|
52
|
+
import pygame
|
|
53
|
+
This order ensures the SDL env var is set before pygame.init().
|
|
54
|
+
|
|
55
|
+
3. STRUCTURE — use a single main() function and end with:
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
main()
|
|
58
|
+
|
|
59
|
+
4. WINDOW — 800x600, 60 FPS, pygame.display.set_caption("<game name>").
|
|
60
|
+
|
|
61
|
+
5. THEME COLOR — the caller substitutes the literal string #THEME_COLOR# with a
|
|
62
|
+
hex color such as #00FF00. Use it as the primary foreground color for all
|
|
63
|
+
game elements. Always reference it as:
|
|
64
|
+
THEME_COLOR = "#THEME_COLOR#"
|
|
65
|
+
Then convert once with:
|
|
66
|
+
def _hex(h):
|
|
67
|
+
h = h.lstrip("#")
|
|
68
|
+
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
|
|
69
|
+
FG = _hex(THEME_COLOR)
|
|
70
|
+
|
|
71
|
+
6. DIFFICULTY — the caller substitutes the literal float #DIFF_MULT# (0.7 easy /
|
|
72
|
+
1.0 normal / 1.4 hard). Store it as:
|
|
73
|
+
DIFF_MULT = #DIFF_MULT#
|
|
74
|
+
Use it to scale speeds, spawn rates, or AI strength.
|
|
75
|
+
|
|
76
|
+
7. QUIT — always handle pygame.QUIT and the ESC key to exit cleanly.
|
|
77
|
+
|
|
78
|
+
8. RESTART — after game-over show a "Press R to restart / ESC to quit" screen
|
|
79
|
+
and honour both keys.
|
|
80
|
+
|
|
81
|
+
9. HUD — always draw the current score (or lives, timer, etc.) on screen.
|
|
82
|
+
|
|
83
|
+
10. NO EXTERNAL ASSETS — no image files, no sound files, no fonts beyond
|
|
84
|
+
pygame.font.SysFont. Draw everything with pygame primitives.
|
|
85
|
+
|
|
86
|
+
11. BEEP — synthesise simple square-wave sound effects using pygame.mixer and
|
|
87
|
+
bytearray; never load .wav / .mp3 files. Use this helper:
|
|
88
|
+
pygame.mixer.init(frequency=22050, size=-16, channels=1, buffer=256)
|
|
89
|
+
def beep(freq=440, ms=60, vol=0.12):
|
|
90
|
+
try:
|
|
91
|
+
n = int(22050 * ms / 1000)
|
|
92
|
+
buf = bytearray(n)
|
|
93
|
+
for i in range(n):
|
|
94
|
+
buf[i] = (127 if (i * freq * 2 // 22050) % 2 == 0 else 129) & 0xFF
|
|
95
|
+
s = pygame.mixer.Sound(buffer=bytes(buf))
|
|
96
|
+
s.set_volume(vol)
|
|
97
|
+
s.play()
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
12. COMPLETENESS — every mechanic you describe must be fully implemented. Do not
|
|
102
|
+
leave stubs, TODOs, or placeholder comments. The file must run as-is with
|
|
103
|
+
only `pip install pygame`.
|
|
104
|
+
|
|
105
|
+
13. BACKGROUND — use a near-black background (5, 5, 15) for all games.
|
|
106
|
+
|
|
107
|
+
=== STYLE GUIDELINES ===
|
|
108
|
+
- Keep code clean and readable with concise inline comments.
|
|
109
|
+
- Prefer pure-Python math over numpy.
|
|
110
|
+
- Keep the total file under 400 lines where possible; prioritise playability over
|
|
111
|
+
feature count.
|
|
112
|
+
- Make the game genuinely fun: good game-feel, responsive controls, clear
|
|
113
|
+
feedback on events.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
# Compile helpers
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
def _resolve_pyinstaller() -> Optional[str]:
|
|
122
|
+
if shutil.which("pyinstaller"):
|
|
123
|
+
return "found"
|
|
124
|
+
try:
|
|
125
|
+
subprocess.run(
|
|
126
|
+
[sys.executable, "-m", "PyInstaller", "--version"],
|
|
127
|
+
capture_output=True, timeout=10,
|
|
128
|
+
)
|
|
129
|
+
return "module"
|
|
130
|
+
except Exception:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def compile_exe(source_path: Path, output_dir: Path, game_name: str) -> tuple[bool, str]:
|
|
135
|
+
"""Compile a Python script to a standalone .exe via PyInstaller."""
|
|
136
|
+
if _resolve_pyinstaller() is None:
|
|
137
|
+
return False, "PyInstaller is not installed. Run: pip install pyinstaller"
|
|
138
|
+
|
|
139
|
+
cmd = [
|
|
140
|
+
sys.executable, "-m", "PyInstaller",
|
|
141
|
+
"--onefile", "--windowed", "--noconsole",
|
|
142
|
+
"--name", game_name,
|
|
143
|
+
"--distpath", str(output_dir),
|
|
144
|
+
"--workpath", str(output_dir / "_build"),
|
|
145
|
+
"--specpath", str(output_dir),
|
|
146
|
+
str(source_path),
|
|
147
|
+
]
|
|
148
|
+
try:
|
|
149
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300, cwd=output_dir)
|
|
150
|
+
if result.returncode != 0:
|
|
151
|
+
return False, f"PyInstaller failed (exit {result.returncode}):\n{result.stderr[-500:]}"
|
|
152
|
+
exe_path = output_dir / f"{game_name}.exe"
|
|
153
|
+
if exe_path.exists():
|
|
154
|
+
for artifact in [output_dir / "_build", output_dir / f"{game_name}.spec"]:
|
|
155
|
+
if artifact.is_dir():
|
|
156
|
+
shutil.rmtree(artifact, ignore_errors=True)
|
|
157
|
+
elif artifact.is_file():
|
|
158
|
+
artifact.unlink(missing_ok=True)
|
|
159
|
+
return True, str(exe_path)
|
|
160
|
+
return False, f"Compilation ran but .exe not found at {exe_path}"
|
|
161
|
+
except subprocess.TimeoutExpired:
|
|
162
|
+
return False, "PyInstaller timed out (5 minutes)"
|
|
163
|
+
except Exception as e:
|
|
164
|
+
return False, f"PyInstaller error: {e}"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
# Tool
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
class GameGenTool(Tool):
|
|
172
|
+
"""Generate a playable Pygame game from a natural-language description.
|
|
173
|
+
|
|
174
|
+
Use this tool when the user asks to:
|
|
175
|
+
- Create any kind of video game ("make me a top-down shooter")
|
|
176
|
+
- Generate a playable game they can run locally or distribute as an .exe
|
|
177
|
+
- Adjust an existing game's difficulty or visual theme
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
name = "GameGen"
|
|
181
|
+
description = (
|
|
182
|
+
"Generate a complete, runnable Pygame game from a plain-English description. "
|
|
183
|
+
"Any game concept is supported — just describe the game you want. "
|
|
184
|
+
"Outputs a .py script or a standalone .exe (requires PyInstaller). "
|
|
185
|
+
"No GPU needed; uses SDL2 software rendering."
|
|
186
|
+
)
|
|
187
|
+
parameters = {
|
|
188
|
+
"type": "object",
|
|
189
|
+
"properties": {
|
|
190
|
+
"description": {
|
|
191
|
+
"type": "string",
|
|
192
|
+
"description": (
|
|
193
|
+
"Plain-English description of the game to generate. "
|
|
194
|
+
"Be as specific or as vague as you like — the model will fill in the details. "
|
|
195
|
+
"Examples: 'a Breakout clone', 'a top-down zombie shooter', "
|
|
196
|
+
"'a simple platformer where you collect coins'."
|
|
197
|
+
),
|
|
198
|
+
},
|
|
199
|
+
"difficulty": {
|
|
200
|
+
"type": "string",
|
|
201
|
+
"description": "Game difficulty: easy, normal, or hard.",
|
|
202
|
+
"enum": list(DIFFICULTIES),
|
|
203
|
+
"default": "normal",
|
|
204
|
+
},
|
|
205
|
+
"theme_color": {
|
|
206
|
+
"type": "string",
|
|
207
|
+
"description": "Hex color for the primary game elements, e.g. '#00FF00'.",
|
|
208
|
+
"default": "#00FF00",
|
|
209
|
+
},
|
|
210
|
+
"output_format": {
|
|
211
|
+
"type": "string",
|
|
212
|
+
"description": "'py' for a Python script, 'exe' for a standalone executable.",
|
|
213
|
+
"enum": ["py", "exe"],
|
|
214
|
+
"default": "exe",
|
|
215
|
+
},
|
|
216
|
+
"output_dir": {
|
|
217
|
+
"type": "string",
|
|
218
|
+
"description": "Directory to write the output file to.",
|
|
219
|
+
"default": ".",
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
"required": ["description"],
|
|
223
|
+
}
|
|
224
|
+
permission_risk = "safe"
|
|
225
|
+
|
|
226
|
+
def _generate_source(self, description: str, difficulty: str, _theme_color: str = "") -> str:
|
|
227
|
+
"""Ask the DeepSeek model to write the full Pygame source."""
|
|
228
|
+
try:
|
|
229
|
+
return self._generate_source_api(description, difficulty)
|
|
230
|
+
except Exception:
|
|
231
|
+
return self._generate_source_fallback(description, difficulty)
|
|
232
|
+
|
|
233
|
+
def _generate_source_api(self, description: str, difficulty: str) -> str:
|
|
234
|
+
"""Use the project's stream_chat API to generate the game."""
|
|
235
|
+
from ..api import stream_chat # noqa: PLC0415
|
|
236
|
+
from ..config import Config # noqa: PLC0415
|
|
237
|
+
|
|
238
|
+
cfg = Config()
|
|
239
|
+
diff_hint = DIFFICULTY_HINTS.get(difficulty, DIFFICULTY_HINTS["normal"])
|
|
240
|
+
user_prompt = (
|
|
241
|
+
f"Game description: {description}\n\n"
|
|
242
|
+
f"Difficulty hint: {diff_hint}\n\n"
|
|
243
|
+
f"Use #THEME_COLOR# as the literal placeholder for the theme color "
|
|
244
|
+
f"and #DIFF_MULT# as the literal placeholder for the difficulty multiplier."
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
messages = [
|
|
248
|
+
{"role": "system", "content": GAME_GEN_SYSTEM_PROMPT},
|
|
249
|
+
{"role": "user", "content": user_prompt},
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
parts: list[str] = []
|
|
253
|
+
for event in stream_chat(
|
|
254
|
+
messages=messages,
|
|
255
|
+
tools=[],
|
|
256
|
+
model=cfg.model,
|
|
257
|
+
api_key=cfg.api_key,
|
|
258
|
+
base_url=cfg.base_url,
|
|
259
|
+
max_tokens=4096,
|
|
260
|
+
temperature=0.3,
|
|
261
|
+
):
|
|
262
|
+
kind, data = event
|
|
263
|
+
if kind == "text":
|
|
264
|
+
parts.append(data)
|
|
265
|
+
elif kind == "error":
|
|
266
|
+
raise RuntimeError(f"API error: {data}")
|
|
267
|
+
return "".join(parts).strip()
|
|
268
|
+
|
|
269
|
+
def _generate_source_fallback(self, description: str, difficulty: str) -> str:
|
|
270
|
+
"""Direct API call using Config and urllib."""
|
|
271
|
+
import json
|
|
272
|
+
import urllib.request # noqa: PLC0415
|
|
273
|
+
from ..config import Config # noqa: PLC0415
|
|
274
|
+
|
|
275
|
+
cfg = Config()
|
|
276
|
+
diff_hint = DIFFICULTY_HINTS.get(difficulty, DIFFICULTY_HINTS["normal"])
|
|
277
|
+
payload = {
|
|
278
|
+
"model": "deepseek-chat",
|
|
279
|
+
"max_tokens": 4096,
|
|
280
|
+
"temperature": 0.3,
|
|
281
|
+
"messages": [
|
|
282
|
+
{"role": "system", "content": GAME_GEN_SYSTEM_PROMPT},
|
|
283
|
+
{
|
|
284
|
+
"role": "user",
|
|
285
|
+
"content": (
|
|
286
|
+
f"Game description: {description}\n\n"
|
|
287
|
+
f"Difficulty hint: {diff_hint}\n\n"
|
|
288
|
+
"Use #THEME_COLOR# and #DIFF_MULT# as literal placeholders."
|
|
289
|
+
),
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
}
|
|
293
|
+
req = urllib.request.Request(
|
|
294
|
+
f"{cfg.base_url}/chat/completions",
|
|
295
|
+
data=json.dumps(payload).encode(),
|
|
296
|
+
headers={"Content-Type": "application/json", "Authorization": f"Bearer {cfg.api_key}"},
|
|
297
|
+
method="POST",
|
|
298
|
+
)
|
|
299
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
300
|
+
data: dict[str, Any] = json.loads(resp.read())
|
|
301
|
+
return str(data["choices"][0]["message"]["content"].strip())
|
|
302
|
+
|
|
303
|
+
def run(
|
|
304
|
+
self,
|
|
305
|
+
description: str,
|
|
306
|
+
difficulty: str = "normal",
|
|
307
|
+
theme_color: str = "#00FF00",
|
|
308
|
+
output_format: str = "exe",
|
|
309
|
+
output_dir: str = ".",
|
|
310
|
+
) -> str:
|
|
311
|
+
|
|
312
|
+
if difficulty not in DIFFICULTIES:
|
|
313
|
+
return f"Error: unknown difficulty '{difficulty}'. Use: {', '.join(DIFFICULTIES)}"
|
|
314
|
+
if output_format not in ("py", "exe"):
|
|
315
|
+
return "Error: output_format must be 'py' or 'exe'."
|
|
316
|
+
|
|
317
|
+
out_dir = Path(output_dir).expanduser().resolve()
|
|
318
|
+
try:
|
|
319
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
320
|
+
except OSError as e:
|
|
321
|
+
return f"Error: cannot create output directory: {e}"
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
raw_source = self._generate_source(description, difficulty, theme_color)
|
|
325
|
+
except Exception as e:
|
|
326
|
+
return f"Error: model call failed — {e}"
|
|
327
|
+
|
|
328
|
+
source = raw_source
|
|
329
|
+
if source.startswith("```"):
|
|
330
|
+
lines = source.splitlines()
|
|
331
|
+
source = "\n".join(
|
|
332
|
+
ln for ln in lines if not ln.startswith("```")
|
|
333
|
+
).strip()
|
|
334
|
+
|
|
335
|
+
diff_mult = {"easy": 0.7, "normal": 1.0, "hard": 1.4}[difficulty]
|
|
336
|
+
source = source.replace("#THEME_COLOR#", theme_color)
|
|
337
|
+
source = source.replace("#DIFF_MULT#", str(diff_mult))
|
|
338
|
+
|
|
339
|
+
slug = "_".join(description.split()[:4])
|
|
340
|
+
slug = "".join(c if c.isalnum() or c == "_" else "" for c in slug) or "Game"
|
|
341
|
+
game_name = slug[:40]
|
|
342
|
+
|
|
343
|
+
py_path = out_dir / f"{game_name}.py"
|
|
344
|
+
try:
|
|
345
|
+
py_path.write_text(source, encoding="utf-8")
|
|
346
|
+
except OSError as e:
|
|
347
|
+
return f"Error: failed to write source file: {e}"
|
|
348
|
+
|
|
349
|
+
if output_format == "py":
|
|
350
|
+
return (
|
|
351
|
+
f"Game generated successfully.\n"
|
|
352
|
+
f"File : {py_path}\n"
|
|
353
|
+
f"Run : python \"{py_path}\"\n"
|
|
354
|
+
f"Needs: pip install pygame"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
success, result = compile_exe(py_path, out_dir, game_name)
|
|
358
|
+
if success:
|
|
359
|
+
return f"Standalone .exe created.\nFile: {result}"
|
|
360
|
+
return f"Compilation failed: {result}\nSource saved at: {py_path}"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from .registry import Tool
|
|
2
|
+
from ..git import git_status, git_diff, git_log, git_commit, git_add, git_branch, git_create_pr, git_push
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class GitStatusTool(Tool):
|
|
6
|
+
name = "GitStatus"
|
|
7
|
+
description = "Show git working tree status."
|
|
8
|
+
permission_risk = "safe"
|
|
9
|
+
parameters = {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {},
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
def run(self, **kwargs) -> str: # type: ignore[override]
|
|
15
|
+
return git_status()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GitDiffTool(Tool):
|
|
19
|
+
name = "GitDiff"
|
|
20
|
+
description = "Show git diff of changes."
|
|
21
|
+
permission_risk = "safe"
|
|
22
|
+
parameters = {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"properties": {
|
|
25
|
+
"staged": {
|
|
26
|
+
"type": "boolean",
|
|
27
|
+
"description": "Show staged changes only",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def run(self, staged: bool = False) -> str: # type: ignore[override]
|
|
33
|
+
return git_diff(staged)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class GitLogTool(Tool):
|
|
37
|
+
name = "GitLog"
|
|
38
|
+
description = "Show recent commit history."
|
|
39
|
+
permission_risk = "safe"
|
|
40
|
+
parameters = {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"properties": {
|
|
43
|
+
"count": {"type": "integer", "description": "Number of commits to show"},
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def run(self, count: int = 10) -> str: # type: ignore[override]
|
|
48
|
+
return git_log(count)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class GitCommitTool(Tool):
|
|
52
|
+
name = "GitCommit"
|
|
53
|
+
description = "Create a git commit with a message."
|
|
54
|
+
permission_risk = "high"
|
|
55
|
+
parameters = {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"properties": {
|
|
58
|
+
"message": {"type": "string", "description": "Commit message"},
|
|
59
|
+
},
|
|
60
|
+
"required": ["message"],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def run(self, message: str) -> str: # type: ignore[override]
|
|
64
|
+
return git_commit(message)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class GitAddTool(Tool):
|
|
68
|
+
name = "GitAdd"
|
|
69
|
+
description = "Stage files for commit."
|
|
70
|
+
permission_risk = "medium"
|
|
71
|
+
parameters = {
|
|
72
|
+
"type": "object",
|
|
73
|
+
"properties": {
|
|
74
|
+
"files": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"items": {"type": "string"},
|
|
77
|
+
"description": "Files to stage (default: all)",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
def run(self, files: list[str] | None = None) -> str: # type: ignore[override]
|
|
83
|
+
return git_add(files)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class GitBranchTool(Tool):
|
|
87
|
+
name = "GitBranch"
|
|
88
|
+
description = "List and manage git branches."
|
|
89
|
+
permission_risk = "safe"
|
|
90
|
+
parameters = {
|
|
91
|
+
"type": "object",
|
|
92
|
+
"properties": {},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def run(self, **kwargs) -> str: # type: ignore[override]
|
|
96
|
+
return git_branch()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class GitPRTool(Tool):
|
|
100
|
+
name = "GitPR"
|
|
101
|
+
description = "Push current branch and create a DRAFT pull request on GitHub."
|
|
102
|
+
permission_risk = "high"
|
|
103
|
+
parameters = {
|
|
104
|
+
"type": "object",
|
|
105
|
+
"properties": {
|
|
106
|
+
"title": {"type": "string", "description": "PR title"},
|
|
107
|
+
"body": {"type": "string", "description": "PR body/description"},
|
|
108
|
+
},
|
|
109
|
+
"required": ["title"],
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
def run(self, title: str, body: str = "") -> str: # type: ignore[override]
|
|
113
|
+
push_result = git_push()
|
|
114
|
+
pr_result = git_create_pr(title, body, draft=True)
|
|
115
|
+
return f"Push: {push_result}\nDraft PR: {pr_result}"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class GitPushTool(Tool):
|
|
119
|
+
name = "GitPush"
|
|
120
|
+
description = "Push commits to remote."
|
|
121
|
+
permission_risk = "high"
|
|
122
|
+
parameters = {
|
|
123
|
+
"type": "object",
|
|
124
|
+
"properties": {
|
|
125
|
+
"branch": {"type": "string", "description": "Branch to push"},
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
def run(self, branch: str | None = None) -> str: # type: ignore[override]
|
|
130
|
+
return git_push(branch)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Git worktree tools."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
from .registry import Tool
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GitWorktreeTool(Tool):
|
|
9
|
+
name = "GitWorktree"
|
|
10
|
+
description = "Manage git worktrees (create, list, remove)."
|
|
11
|
+
permission_risk = "high"
|
|
12
|
+
parameters = {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"properties": {
|
|
15
|
+
"action": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"enum": ["list", "create", "remove"],
|
|
18
|
+
"description": "Action to perform",
|
|
19
|
+
},
|
|
20
|
+
"path": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Path for the new worktree (required for create)",
|
|
23
|
+
},
|
|
24
|
+
"branch": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Branch for the new worktree (optional for create)",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
"required": ["action"],
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def run(self, action: str, path: str | None = None, branch: str | None = None) -> str: # type: ignore[override]
|
|
33
|
+
try:
|
|
34
|
+
if action == "list":
|
|
35
|
+
r = subprocess.run(
|
|
36
|
+
["git", "worktree", "list"],
|
|
37
|
+
capture_output=True, text=True, timeout=30,
|
|
38
|
+
)
|
|
39
|
+
return r.stdout.strip() or r.stderr.strip()
|
|
40
|
+
|
|
41
|
+
elif action == "create":
|
|
42
|
+
if not path:
|
|
43
|
+
return "Error: path is required for create"
|
|
44
|
+
cmd = ["git", "worktree", "add", path]
|
|
45
|
+
if branch:
|
|
46
|
+
cmd.extend(["-b", branch])
|
|
47
|
+
r = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
48
|
+
return r.stdout.strip() or r.stderr.strip()
|
|
49
|
+
|
|
50
|
+
elif action == "remove":
|
|
51
|
+
if not path:
|
|
52
|
+
return "Error: path is required for remove"
|
|
53
|
+
r = subprocess.run(
|
|
54
|
+
["git", "worktree", "remove", path],
|
|
55
|
+
capture_output=True, text=True, timeout=30,
|
|
56
|
+
)
|
|
57
|
+
return r.stdout.strip() or r.stderr.strip()
|
|
58
|
+
|
|
59
|
+
return f"Unknown action: {action}"
|
|
60
|
+
except subprocess.TimeoutExpired:
|
|
61
|
+
return "Error: command timed out"
|
|
62
|
+
except Exception as e:
|
|
63
|
+
return f"Error: {e}"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Path traversal protection and file path utilities."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def safe_resolve(path: str, working_dir: str | None = None) -> Path:
|
|
8
|
+
"""Resolve a path safely, preventing directory traversal outside allowed directories.
|
|
9
|
+
|
|
10
|
+
Raises ValueError if the resolved path is outside the allowed directory.
|
|
11
|
+
"""
|
|
12
|
+
resolved = Path(path).resolve()
|
|
13
|
+
|
|
14
|
+
if working_dir:
|
|
15
|
+
allowed = Path(working_dir).resolve()
|
|
16
|
+
else:
|
|
17
|
+
allowed = Path(os.getcwd()).resolve()
|
|
18
|
+
|
|
19
|
+
# Check if resolved path is within the allowed directory
|
|
20
|
+
try:
|
|
21
|
+
resolved.relative_to(allowed)
|
|
22
|
+
except ValueError:
|
|
23
|
+
raise ValueError(
|
|
24
|
+
f"Path traversal blocked: '{path}' resolves outside the working directory '{allowed}'"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
return resolved
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def validate_file_path(file_path: str, must_exist: bool = False, working_dir: str | None = None) -> Path:
|
|
31
|
+
"""Validate a file path and return the resolved Path.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
file_path: The path to validate
|
|
35
|
+
must_exist: If True, raises FileNotFoundError when path doesn't exist
|
|
36
|
+
working_dir: Optional working directory to restrict to
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Resolved Path object
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: If path traversal is detected
|
|
43
|
+
FileNotFoundError: If must_exist is True and path doesn't exist
|
|
44
|
+
"""
|
|
45
|
+
resolved = safe_resolve(file_path, working_dir)
|
|
46
|
+
|
|
47
|
+
if must_exist and not resolved.exists():
|
|
48
|
+
raise FileNotFoundError(f"Path does not exist: {file_path}")
|
|
49
|
+
|
|
50
|
+
return resolved
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def sanitize_filename(name: str) -> str:
|
|
54
|
+
"""Sanitize a filename by removing dangerous characters."""
|
|
55
|
+
import re
|
|
56
|
+
# Remove path separators and null bytes
|
|
57
|
+
name = name.replace("/", "_").replace("\\", "_").replace("\0", "")
|
|
58
|
+
# Remove any remaining dangerous characters
|
|
59
|
+
name = re.sub(r'[<>:"|?*]', "_", name)
|
|
60
|
+
# Limit length
|
|
61
|
+
if len(name) > 200:
|
|
62
|
+
base, ext = os.path.splitext(name)
|
|
63
|
+
name = base[:195] + ext
|
|
64
|
+
return name.strip()
|