claude-unity-bridge 0.1.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.
- claude_unity_bridge/__init__.py +3 -0
- claude_unity_bridge/cli.py +628 -0
- claude_unity_bridge-0.1.2.dist-info/METADATA +441 -0
- claude_unity_bridge-0.1.2.dist-info/RECORD +7 -0
- claude_unity_bridge-0.1.2.dist-info/WHEEL +5 -0
- claude_unity_bridge-0.1.2.dist-info/entry_points.txt +2 -0
- claude_unity_bridge-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Claude Unity Bridge - Command Execution Script
|
|
4
|
+
|
|
5
|
+
Rock-solid, deterministic command execution for Unity Editor operations.
|
|
6
|
+
Handles UUID generation, file-based polling, response parsing, and cleanup.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
import uuid
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, Any
|
|
16
|
+
|
|
17
|
+
# Constants
|
|
18
|
+
UNITY_DIR = Path.cwd() / ".unity-bridge"
|
|
19
|
+
DEFAULT_TIMEOUT = 30
|
|
20
|
+
MIN_SLEEP = 0.1
|
|
21
|
+
MAX_SLEEP = 1.0
|
|
22
|
+
SLEEP_MULTIPLIER = 1.5
|
|
23
|
+
MIN_LIMIT = 1
|
|
24
|
+
MAX_LIMIT = 1000
|
|
25
|
+
|
|
26
|
+
# Exit codes
|
|
27
|
+
EXIT_SUCCESS = 0
|
|
28
|
+
EXIT_ERROR = 1
|
|
29
|
+
EXIT_TIMEOUT = 2
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class UnityCommandError(Exception):
|
|
33
|
+
"""Base exception for Unity command errors"""
|
|
34
|
+
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class UnityNotRunningError(UnityCommandError):
|
|
39
|
+
"""Unity Editor is not running or not responding"""
|
|
40
|
+
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CommandTimeoutError(UnityCommandError):
|
|
45
|
+
"""Command execution timed out"""
|
|
46
|
+
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check_gitignore_and_notify():
|
|
51
|
+
"""Print a notice if .unity-bridge/ is not in .gitignore."""
|
|
52
|
+
gitignore_path = Path.cwd() / ".gitignore"
|
|
53
|
+
|
|
54
|
+
if gitignore_path.exists():
|
|
55
|
+
try:
|
|
56
|
+
content = gitignore_path.read_text()
|
|
57
|
+
# Check for various patterns that would ignore the directory
|
|
58
|
+
if ".unity-bridge" in content:
|
|
59
|
+
return # Already ignored
|
|
60
|
+
except Exception:
|
|
61
|
+
pass # If we can't read gitignore, show the notice
|
|
62
|
+
|
|
63
|
+
print(
|
|
64
|
+
"\nNote: Add '.unity-bridge/' to your .gitignore to avoid committing runtime files.\n",
|
|
65
|
+
file=sys.stderr,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def write_command(action: str, params: Dict[str, Any]) -> str:
|
|
70
|
+
"""
|
|
71
|
+
Write command file atomically, return UUID.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
action: Command action (run-tests, compile, etc.)
|
|
75
|
+
params: Command parameters dictionary
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Command UUID string
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
UnityCommandError: If writing fails
|
|
82
|
+
"""
|
|
83
|
+
command_id = str(uuid.uuid4())
|
|
84
|
+
command = {"id": command_id, "action": action, "params": params}
|
|
85
|
+
|
|
86
|
+
# Security: Ensure UNITY_DIR is not a symlink (prevent symlink attacks)
|
|
87
|
+
if UNITY_DIR.exists() and UNITY_DIR.is_symlink():
|
|
88
|
+
raise UnityCommandError("Security error: .unity-bridge cannot be a symlink")
|
|
89
|
+
|
|
90
|
+
# Ensure directory exists
|
|
91
|
+
dir_existed = UNITY_DIR.exists()
|
|
92
|
+
try:
|
|
93
|
+
UNITY_DIR.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
raise UnityCommandError(f"Failed to create Unity directory: {e}")
|
|
96
|
+
|
|
97
|
+
# Notify about gitignore on first directory creation
|
|
98
|
+
if not dir_existed:
|
|
99
|
+
check_gitignore_and_notify()
|
|
100
|
+
|
|
101
|
+
# Atomic write using temp file
|
|
102
|
+
command_file = UNITY_DIR / "command.json"
|
|
103
|
+
temp_file = command_file.with_suffix(".tmp")
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
temp_file.write_text(json.dumps(command, indent=2))
|
|
107
|
+
temp_file.replace(command_file)
|
|
108
|
+
except Exception as e:
|
|
109
|
+
# Cleanup temp file if it exists
|
|
110
|
+
if temp_file.exists():
|
|
111
|
+
try:
|
|
112
|
+
temp_file.unlink()
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
raise UnityCommandError(f"Failed to write command file: {e}")
|
|
116
|
+
|
|
117
|
+
return command_id
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def wait_for_response(command_id: str, timeout: int, verbose: bool = False) -> Dict[str, Any]:
|
|
121
|
+
"""
|
|
122
|
+
Poll for response file with exponential backoff.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
command_id: UUID of the command
|
|
126
|
+
timeout: Maximum seconds to wait
|
|
127
|
+
verbose: Print polling progress
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Parsed response dictionary
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
CommandTimeoutError: If timeout is reached
|
|
134
|
+
UnityCommandError: If response parsing fails
|
|
135
|
+
"""
|
|
136
|
+
response_file = UNITY_DIR / f"response-{command_id}.json"
|
|
137
|
+
|
|
138
|
+
start = time.time()
|
|
139
|
+
sleep_time = MIN_SLEEP
|
|
140
|
+
attempts = 0
|
|
141
|
+
|
|
142
|
+
while time.time() - start < timeout:
|
|
143
|
+
attempts += 1
|
|
144
|
+
|
|
145
|
+
if response_file.exists():
|
|
146
|
+
# Wait a bit for Unity to finish writing
|
|
147
|
+
time.sleep(0.1)
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
response_text = response_file.read_text()
|
|
151
|
+
return json.loads(response_text)
|
|
152
|
+
except json.JSONDecodeError:
|
|
153
|
+
# Might have caught it mid-write, retry once
|
|
154
|
+
if verbose:
|
|
155
|
+
print("Warning: Failed to parse response, retrying...", file=sys.stderr)
|
|
156
|
+
time.sleep(0.2)
|
|
157
|
+
try:
|
|
158
|
+
response_text = response_file.read_text()
|
|
159
|
+
return json.loads(response_text)
|
|
160
|
+
except json.JSONDecodeError as e:
|
|
161
|
+
# Log raw response for debugging
|
|
162
|
+
print("Error: Invalid JSON in response file", file=sys.stderr)
|
|
163
|
+
print(f"Raw response: {response_text}", file=sys.stderr)
|
|
164
|
+
raise UnityCommandError(f"Failed to parse response JSON: {e}")
|
|
165
|
+
except Exception as e:
|
|
166
|
+
raise UnityCommandError(f"Failed to read response file: {e}")
|
|
167
|
+
|
|
168
|
+
if verbose and attempts % 10 == 0:
|
|
169
|
+
elapsed = time.time() - start
|
|
170
|
+
print(f"Waiting for response... ({elapsed:.1f}s)", file=sys.stderr)
|
|
171
|
+
|
|
172
|
+
time.sleep(sleep_time)
|
|
173
|
+
sleep_time = min(sleep_time * SLEEP_MULTIPLIER, MAX_SLEEP)
|
|
174
|
+
|
|
175
|
+
# Check if Unity directory exists - if not, Unity likely isn't running
|
|
176
|
+
if not UNITY_DIR.exists():
|
|
177
|
+
raise UnityNotRunningError(
|
|
178
|
+
"Unity Editor not detected. Ensure Unity is open with the project loaded."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
raise CommandTimeoutError(
|
|
182
|
+
f"Command timed out after {timeout}s. Check Unity Console for errors."
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def format_response(response: Dict[str, Any], action: str) -> str:
|
|
187
|
+
"""
|
|
188
|
+
Format response based on command type.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
response: Parsed response dictionary
|
|
192
|
+
action: Command action name
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Formatted human-readable string
|
|
196
|
+
"""
|
|
197
|
+
status = response.get("status", "unknown")
|
|
198
|
+
duration = response.get("duration_ms", 0)
|
|
199
|
+
duration_sec = duration / 1000.0
|
|
200
|
+
|
|
201
|
+
# Handle error status
|
|
202
|
+
if status == "error":
|
|
203
|
+
error_msg = response.get("error", "Unknown error")
|
|
204
|
+
return f"✗ Error: {error_msg}"
|
|
205
|
+
|
|
206
|
+
# Format based on action type
|
|
207
|
+
if action == "run-tests":
|
|
208
|
+
return format_test_results(response, status, duration_sec)
|
|
209
|
+
elif action == "compile":
|
|
210
|
+
return format_compile_results(response, status, duration_sec)
|
|
211
|
+
elif action == "get-console-logs":
|
|
212
|
+
return format_console_logs(response)
|
|
213
|
+
elif action == "get-status":
|
|
214
|
+
return format_editor_status(response)
|
|
215
|
+
elif action == "refresh":
|
|
216
|
+
return format_refresh_results(response, status, duration_sec)
|
|
217
|
+
else:
|
|
218
|
+
# Generic formatting
|
|
219
|
+
return format_generic_response(response, status, duration_sec)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def format_test_results(response: Dict[str, Any], status: str, duration: float) -> str:
|
|
223
|
+
"""Format run-tests response"""
|
|
224
|
+
result = response.get("result", {})
|
|
225
|
+
passed = result.get("passed", 0)
|
|
226
|
+
failed = result.get("failed", 0)
|
|
227
|
+
skipped = result.get("skipped", 0)
|
|
228
|
+
failures = result.get("failures", [])
|
|
229
|
+
|
|
230
|
+
# Summary
|
|
231
|
+
lines = [
|
|
232
|
+
f"✓ Tests Passed: {passed}",
|
|
233
|
+
f"✗ Tests Failed: {failed}",
|
|
234
|
+
f"○ Tests Skipped: {skipped}",
|
|
235
|
+
f"Duration: {duration:.2f}s",
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
# Failed tests details
|
|
239
|
+
if failed > 0 and failures:
|
|
240
|
+
lines.append("")
|
|
241
|
+
lines.append("Failed Tests:")
|
|
242
|
+
for failure in failures:
|
|
243
|
+
name = failure.get("name", "Unknown test")
|
|
244
|
+
message = failure.get("message", "")
|
|
245
|
+
lines.append(f" - {name}")
|
|
246
|
+
if message:
|
|
247
|
+
# Try to extract file path from message
|
|
248
|
+
lines.append(f" {message}")
|
|
249
|
+
|
|
250
|
+
return "\n".join(lines)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def format_compile_results(response: Dict[str, Any], status: str, duration: float) -> str:
|
|
254
|
+
"""Format compile response"""
|
|
255
|
+
if status == "success":
|
|
256
|
+
return f"✓ Compilation Successful\nDuration: {duration:.2f}s"
|
|
257
|
+
elif status == "failure":
|
|
258
|
+
error = response.get("error", "")
|
|
259
|
+
if error:
|
|
260
|
+
# Try to parse error count from message
|
|
261
|
+
return f"✗ Compilation Failed\n\n{error}"
|
|
262
|
+
return f"✗ Compilation Failed\nDuration: {duration:.2f}s"
|
|
263
|
+
else:
|
|
264
|
+
return f"Compilation Status: {status}\nDuration: {duration:.2f}s"
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def format_console_logs(response: Dict[str, Any]) -> str:
|
|
268
|
+
"""Format get-console-logs response"""
|
|
269
|
+
logs = response.get("consoleLogs", [])
|
|
270
|
+
|
|
271
|
+
if not logs:
|
|
272
|
+
return "No console logs found"
|
|
273
|
+
|
|
274
|
+
log_filter = response.get("params", {}).get("filter", "")
|
|
275
|
+
limit = len(logs)
|
|
276
|
+
|
|
277
|
+
filter_suffix = f", filtered by {log_filter}" if log_filter else ""
|
|
278
|
+
lines = [f"Console Logs (last {limit}{filter_suffix}):"]
|
|
279
|
+
lines.append("")
|
|
280
|
+
|
|
281
|
+
for log in logs:
|
|
282
|
+
log_type = log.get("type", "Log")
|
|
283
|
+
message = log.get("message", "")
|
|
284
|
+
stack_trace = log.get("stackTrace", "")
|
|
285
|
+
count = log.get("count", 1)
|
|
286
|
+
|
|
287
|
+
# Format type indicator
|
|
288
|
+
if log_type == "Error":
|
|
289
|
+
indicator = "[Error]"
|
|
290
|
+
elif log_type == "Warning":
|
|
291
|
+
indicator = "[Warning]"
|
|
292
|
+
else:
|
|
293
|
+
indicator = "[Log]"
|
|
294
|
+
|
|
295
|
+
# Add count if duplicated
|
|
296
|
+
if count > 1:
|
|
297
|
+
indicator += f" (x{count})"
|
|
298
|
+
|
|
299
|
+
lines.append(f"{indicator} {message}")
|
|
300
|
+
|
|
301
|
+
if stack_trace:
|
|
302
|
+
# Indent stack trace
|
|
303
|
+
for line in stack_trace.split("\n"):
|
|
304
|
+
if line.strip():
|
|
305
|
+
lines.append(f" {line}")
|
|
306
|
+
|
|
307
|
+
lines.append("")
|
|
308
|
+
|
|
309
|
+
return "\n".join(lines)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def format_editor_status(response: Dict[str, Any]) -> str:
|
|
313
|
+
"""Format get-status response"""
|
|
314
|
+
status = response.get("editorStatus")
|
|
315
|
+
|
|
316
|
+
if status is None:
|
|
317
|
+
return "Unity Editor Status: Unknown (missing editorStatus field)"
|
|
318
|
+
|
|
319
|
+
is_compiling = status.get("isCompiling", False)
|
|
320
|
+
is_updating = status.get("isUpdating", False)
|
|
321
|
+
is_playing = status.get("isPlaying", False)
|
|
322
|
+
is_paused = status.get("isPaused", False)
|
|
323
|
+
|
|
324
|
+
lines = ["Unity Editor Status:"]
|
|
325
|
+
|
|
326
|
+
# Compilation status
|
|
327
|
+
if is_compiling:
|
|
328
|
+
lines.append(" - Compilation: ⏳ Compiling...")
|
|
329
|
+
else:
|
|
330
|
+
lines.append(" - Compilation: ✓ Ready")
|
|
331
|
+
|
|
332
|
+
# Play mode status
|
|
333
|
+
if is_playing:
|
|
334
|
+
if is_paused:
|
|
335
|
+
lines.append(" - Play Mode: ⏸ Paused")
|
|
336
|
+
else:
|
|
337
|
+
lines.append(" - Play Mode: ▶ Playing")
|
|
338
|
+
else:
|
|
339
|
+
lines.append(" - Play Mode: ✏ Editing")
|
|
340
|
+
|
|
341
|
+
# Update status
|
|
342
|
+
if is_updating:
|
|
343
|
+
lines.append(" - Updating: ⏳ Yes")
|
|
344
|
+
else:
|
|
345
|
+
lines.append(" - Updating: No")
|
|
346
|
+
|
|
347
|
+
return "\n".join(lines)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def format_refresh_results(response: Dict[str, Any], status: str, duration: float) -> str:
|
|
351
|
+
"""Format refresh response"""
|
|
352
|
+
if status == "success":
|
|
353
|
+
return f"✓ Asset Database Refreshed\nDuration: {duration:.2f}s"
|
|
354
|
+
elif status == "failure":
|
|
355
|
+
error = response.get("error", "Unknown error")
|
|
356
|
+
return f"✗ Refresh Failed: {error}\nDuration: {duration:.2f}s"
|
|
357
|
+
else:
|
|
358
|
+
return f"Refresh Status: {status}\nDuration: {duration:.2f}s"
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def format_generic_response(response: Dict[str, Any], status: str, duration: float) -> str:
|
|
362
|
+
"""Generic response formatting"""
|
|
363
|
+
action = response.get("action", "unknown")
|
|
364
|
+
|
|
365
|
+
if status == "success":
|
|
366
|
+
return f"✓ {action} completed successfully\nDuration: {duration:.2f}s"
|
|
367
|
+
elif status == "failure":
|
|
368
|
+
error = response.get("error", "Unknown error")
|
|
369
|
+
return f"✗ {action} failed: {error}\nDuration: {duration:.2f}s"
|
|
370
|
+
else:
|
|
371
|
+
return f"{action} status: {status}\nDuration: {duration:.2f}s"
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def cleanup_old_responses(max_age_hours: int = 1, verbose: bool = False):
|
|
375
|
+
"""
|
|
376
|
+
Remove stale response files.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
max_age_hours: Maximum age in hours before cleanup
|
|
380
|
+
verbose: Print cleanup progress
|
|
381
|
+
"""
|
|
382
|
+
if not UNITY_DIR.exists():
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
current_time = time.time()
|
|
386
|
+
max_age_seconds = max_age_hours * 3600
|
|
387
|
+
cleaned = 0
|
|
388
|
+
|
|
389
|
+
for response_file in UNITY_DIR.glob("response-*.json"):
|
|
390
|
+
try:
|
|
391
|
+
file_age = current_time - response_file.stat().st_mtime
|
|
392
|
+
if file_age > max_age_seconds:
|
|
393
|
+
response_file.unlink()
|
|
394
|
+
cleaned += 1
|
|
395
|
+
if verbose:
|
|
396
|
+
print(f"Cleaned up: {response_file.name}", file=sys.stderr)
|
|
397
|
+
except Exception as e:
|
|
398
|
+
if verbose:
|
|
399
|
+
print(f"Warning: Failed to cleanup {response_file.name}: {e}", file=sys.stderr)
|
|
400
|
+
|
|
401
|
+
if verbose and cleaned > 0:
|
|
402
|
+
print(f"Cleaned up {cleaned} old response file(s)", file=sys.stderr)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def cleanup_response_file(command_id: str, verbose: bool = False):
|
|
406
|
+
"""
|
|
407
|
+
Remove response file after successful read.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
command_id: UUID of the command
|
|
411
|
+
verbose: Print cleanup progress
|
|
412
|
+
"""
|
|
413
|
+
response_file = UNITY_DIR / f"response-{command_id}.json"
|
|
414
|
+
|
|
415
|
+
try:
|
|
416
|
+
if response_file.exists():
|
|
417
|
+
response_file.unlink()
|
|
418
|
+
if verbose:
|
|
419
|
+
print(f"Cleaned up response file: {response_file.name}", file=sys.stderr)
|
|
420
|
+
except Exception as e:
|
|
421
|
+
if verbose:
|
|
422
|
+
print(f"Warning: Failed to cleanup response file: {e}", file=sys.stderr)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def execute_command(
|
|
426
|
+
action: str, params: Dict[str, Any], timeout: int, cleanup: bool = False, verbose: bool = False
|
|
427
|
+
) -> str:
|
|
428
|
+
"""
|
|
429
|
+
Execute Unity command and return formatted response.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
action: Command action
|
|
433
|
+
params: Command parameters
|
|
434
|
+
timeout: Timeout in seconds
|
|
435
|
+
cleanup: Whether to cleanup old responses first
|
|
436
|
+
verbose: Print progress messages
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
Formatted response string
|
|
440
|
+
|
|
441
|
+
Raises:
|
|
442
|
+
UnityCommandError: On execution errors
|
|
443
|
+
"""
|
|
444
|
+
# Cleanup old responses if requested
|
|
445
|
+
if cleanup:
|
|
446
|
+
cleanup_old_responses(verbose=verbose)
|
|
447
|
+
|
|
448
|
+
# Write command
|
|
449
|
+
if verbose:
|
|
450
|
+
print(f"Writing command: {action}", file=sys.stderr)
|
|
451
|
+
command_id = write_command(action, params)
|
|
452
|
+
|
|
453
|
+
# Wait for response
|
|
454
|
+
if verbose:
|
|
455
|
+
print(f"Command ID: {command_id}", file=sys.stderr)
|
|
456
|
+
print(f"Waiting for response (timeout: {timeout}s)...", file=sys.stderr)
|
|
457
|
+
|
|
458
|
+
response = wait_for_response(command_id, timeout, verbose)
|
|
459
|
+
|
|
460
|
+
# Format response
|
|
461
|
+
formatted = format_response(response, action)
|
|
462
|
+
|
|
463
|
+
# Cleanup response file
|
|
464
|
+
cleanup_response_file(command_id, verbose)
|
|
465
|
+
|
|
466
|
+
return formatted
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def execute_health_check(timeout: int, verbose: bool) -> int:
|
|
470
|
+
"""Verify Unity Bridge is set up correctly."""
|
|
471
|
+
print("Checking Unity Bridge setup...")
|
|
472
|
+
|
|
473
|
+
# Check 1: Does .unity-bridge directory exist?
|
|
474
|
+
if not UNITY_DIR.exists():
|
|
475
|
+
print("✗ Unity Bridge not detected")
|
|
476
|
+
print(f" Directory not found: {UNITY_DIR}")
|
|
477
|
+
print(" Is Unity Editor open with the bridge package installed?")
|
|
478
|
+
return EXIT_ERROR
|
|
479
|
+
print(f"✓ Bridge directory exists: {UNITY_DIR}")
|
|
480
|
+
|
|
481
|
+
# Check 2: Can we communicate with Unity?
|
|
482
|
+
try:
|
|
483
|
+
result = execute_command("get-status", {}, timeout=min(timeout, 5), verbose=verbose)
|
|
484
|
+
print("✓ Unity Editor is responding")
|
|
485
|
+
if verbose:
|
|
486
|
+
print(result)
|
|
487
|
+
return EXIT_SUCCESS
|
|
488
|
+
except UnityNotRunningError:
|
|
489
|
+
print("✗ Unity Editor not responding")
|
|
490
|
+
print(" Ensure Unity is open and the project is loaded")
|
|
491
|
+
return EXIT_ERROR
|
|
492
|
+
except CommandTimeoutError:
|
|
493
|
+
print("✗ Unity Editor timed out")
|
|
494
|
+
return EXIT_TIMEOUT
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def main():
|
|
498
|
+
parser = argparse.ArgumentParser(
|
|
499
|
+
description="Execute Unity Editor commands via Claude Unity Bridge",
|
|
500
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
501
|
+
epilog="""
|
|
502
|
+
Commands:
|
|
503
|
+
run-tests Run Unity tests
|
|
504
|
+
compile Trigger script compilation
|
|
505
|
+
refresh Refresh asset database
|
|
506
|
+
get-status Get editor status
|
|
507
|
+
get-console-logs Get Unity console logs
|
|
508
|
+
health-check Verify Unity Bridge setup
|
|
509
|
+
|
|
510
|
+
Examples:
|
|
511
|
+
%(prog)s run-tests --mode EditMode --filter "MyTests"
|
|
512
|
+
%(prog)s compile
|
|
513
|
+
%(prog)s get-console-logs --limit 20 --filter Error
|
|
514
|
+
%(prog)s get-status
|
|
515
|
+
%(prog)s refresh
|
|
516
|
+
%(prog)s health-check
|
|
517
|
+
""",
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
parser.add_argument(
|
|
521
|
+
"command",
|
|
522
|
+
choices=[
|
|
523
|
+
"run-tests",
|
|
524
|
+
"compile",
|
|
525
|
+
"refresh",
|
|
526
|
+
"get-status",
|
|
527
|
+
"get-console-logs",
|
|
528
|
+
"health-check",
|
|
529
|
+
],
|
|
530
|
+
help="Command to execute",
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
# Test command options
|
|
534
|
+
parser.add_argument(
|
|
535
|
+
"--mode", choices=["EditMode", "PlayMode"], help="Test mode (for run-tests)"
|
|
536
|
+
)
|
|
537
|
+
parser.add_argument(
|
|
538
|
+
"--filter",
|
|
539
|
+
help="Test filter pattern (for run-tests) or log type filter (for get-console-logs)",
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
# Console logs options
|
|
543
|
+
parser.add_argument(
|
|
544
|
+
"--limit", type=int, help="Maximum number of logs to retrieve (for get-console-logs)"
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# General options
|
|
548
|
+
parser.add_argument(
|
|
549
|
+
"--timeout",
|
|
550
|
+
type=int,
|
|
551
|
+
default=DEFAULT_TIMEOUT,
|
|
552
|
+
help=f"Command timeout in seconds (default: {DEFAULT_TIMEOUT})",
|
|
553
|
+
)
|
|
554
|
+
parser.add_argument(
|
|
555
|
+
"--cleanup", action="store_true", help="Cleanup old response files before executing"
|
|
556
|
+
)
|
|
557
|
+
parser.add_argument("--verbose", action="store_true", help="Print verbose progress messages")
|
|
558
|
+
|
|
559
|
+
args = parser.parse_args()
|
|
560
|
+
|
|
561
|
+
# Validate timeout
|
|
562
|
+
if args.timeout <= 0:
|
|
563
|
+
parser.error("--timeout must be a positive integer")
|
|
564
|
+
|
|
565
|
+
# Validate limit if provided
|
|
566
|
+
if args.limit is not None:
|
|
567
|
+
if args.limit < MIN_LIMIT or args.limit > MAX_LIMIT:
|
|
568
|
+
parser.error(f"--limit must be between {MIN_LIMIT} and {MAX_LIMIT}")
|
|
569
|
+
|
|
570
|
+
# Handle health-check command separately
|
|
571
|
+
if args.command == "health-check":
|
|
572
|
+
return execute_health_check(args.timeout, args.verbose)
|
|
573
|
+
|
|
574
|
+
# Build parameters based on command
|
|
575
|
+
params = {}
|
|
576
|
+
|
|
577
|
+
if args.command == "run-tests":
|
|
578
|
+
if args.mode:
|
|
579
|
+
params["testMode"] = args.mode
|
|
580
|
+
if args.filter:
|
|
581
|
+
params["filter"] = args.filter
|
|
582
|
+
|
|
583
|
+
elif args.command == "get-console-logs":
|
|
584
|
+
if args.limit is not None:
|
|
585
|
+
# Send as string for compatibility with C# JsonUtility which expects string
|
|
586
|
+
params["limit"] = str(args.limit)
|
|
587
|
+
if args.filter:
|
|
588
|
+
params["filter"] = args.filter
|
|
589
|
+
|
|
590
|
+
# Execute command
|
|
591
|
+
try:
|
|
592
|
+
result = execute_command(
|
|
593
|
+
action=args.command,
|
|
594
|
+
params=params,
|
|
595
|
+
timeout=args.timeout,
|
|
596
|
+
cleanup=args.cleanup,
|
|
597
|
+
verbose=args.verbose,
|
|
598
|
+
)
|
|
599
|
+
print(result)
|
|
600
|
+
return EXIT_SUCCESS
|
|
601
|
+
|
|
602
|
+
except CommandTimeoutError as e:
|
|
603
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
604
|
+
return EXIT_TIMEOUT
|
|
605
|
+
|
|
606
|
+
except UnityNotRunningError as e:
|
|
607
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
608
|
+
return EXIT_ERROR
|
|
609
|
+
|
|
610
|
+
except UnityCommandError as e:
|
|
611
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
612
|
+
return EXIT_ERROR
|
|
613
|
+
|
|
614
|
+
except KeyboardInterrupt:
|
|
615
|
+
print("\nInterrupted by user", file=sys.stderr)
|
|
616
|
+
return EXIT_ERROR
|
|
617
|
+
|
|
618
|
+
except Exception as e:
|
|
619
|
+
print(f"Unexpected error: {e}", file=sys.stderr)
|
|
620
|
+
if args.verbose:
|
|
621
|
+
import traceback
|
|
622
|
+
|
|
623
|
+
traceback.print_exc()
|
|
624
|
+
return EXIT_ERROR
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
if __name__ == "__main__":
|
|
628
|
+
sys.exit(main())
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: claude-unity-bridge
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Control Unity Editor from Claude Code
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
10
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
11
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
12
|
+
Requires-Dist: flake8>=6.0; extra == "dev"
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
name: unity
|
|
16
|
+
description: Execute Unity Editor commands (run tests, compile, get logs, refresh assets) via file-based bridge. Auto-activates for Unity-related tasks. Requires com.managexr.claude-bridge package installed in Unity project.
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Unity Bridge Skill
|
|
20
|
+
|
|
21
|
+
Control Unity Editor operations from Claude Code using a reliable file-based communication protocol.
|
|
22
|
+
|
|
23
|
+
## Overview
|
|
24
|
+
|
|
25
|
+
The Unity Bridge enables Claude Code to trigger operations in a running Unity Editor instance without network configuration or port conflicts. It uses a simple file-based protocol where commands are written to `.unity-bridge/command.json` and responses are read from `.unity-bridge/response-{id}.json`.
|
|
26
|
+
|
|
27
|
+
**Key Features:**
|
|
28
|
+
- Execute EditMode and PlayMode tests
|
|
29
|
+
- Trigger script compilation
|
|
30
|
+
- Refresh asset database
|
|
31
|
+
- Check editor status (compilation, play mode, etc.)
|
|
32
|
+
- Retrieve Unity console logs
|
|
33
|
+
|
|
34
|
+
**Multi-Project Support:** Each Unity project has its own `.unity-bridge/` directory, allowing multiple projects to be worked on simultaneously.
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
1. **Unity Package:** Install `com.managexr.claude-bridge` in your Unity project
|
|
39
|
+
- Via Package Manager: `https://github.com/ManageXR/claude-unity-bridge.git`
|
|
40
|
+
- See main package README for installation instructions
|
|
41
|
+
|
|
42
|
+
2. **Unity Editor:** Must be open with your project loaded
|
|
43
|
+
|
|
44
|
+
3. **Python 3:** The skill uses a Python script for reliable command execution
|
|
45
|
+
|
|
46
|
+
## How It Works
|
|
47
|
+
|
|
48
|
+
The skill uses a CLI tool (`unity-bridge`) that handles:
|
|
49
|
+
- UUID generation for command tracking
|
|
50
|
+
- Atomic file writes to prevent corruption
|
|
51
|
+
- Exponential backoff polling for responses
|
|
52
|
+
- File locking handling
|
|
53
|
+
- Automatic cleanup of old response files
|
|
54
|
+
- Formatted, human-readable output
|
|
55
|
+
|
|
56
|
+
This approach ensures **deterministic, rock-solid execution** - the script is tested once and behaves identically every time, handling all edge cases (timeouts, file locking, malformed responses, etc.) without requiring Claude to manage these details in-context.
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
### Basic Pattern
|
|
61
|
+
|
|
62
|
+
When you need to interact with Unity, use the CLI directly:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
unity-bridge [command] [options]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
All commands automatically:
|
|
69
|
+
- Generate a unique UUID for tracking
|
|
70
|
+
- Write the command atomically
|
|
71
|
+
- Poll for response with timeout
|
|
72
|
+
- Format output for readability
|
|
73
|
+
- Cleanup response files
|
|
74
|
+
|
|
75
|
+
### Command Examples
|
|
76
|
+
|
|
77
|
+
#### Run Tests
|
|
78
|
+
|
|
79
|
+
Execute Unity tests in EditMode or PlayMode:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Run all EditMode tests
|
|
83
|
+
unity-bridge run-tests --mode EditMode
|
|
84
|
+
|
|
85
|
+
# Run tests with filter
|
|
86
|
+
unity-bridge run-tests --mode EditMode --filter "MXR.Tests"
|
|
87
|
+
|
|
88
|
+
# Run all tests (both modes)
|
|
89
|
+
unity-bridge run-tests
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Output:**
|
|
93
|
+
```
|
|
94
|
+
✓ Tests Passed: 410
|
|
95
|
+
✗ Tests Failed: 2
|
|
96
|
+
○ Tests Skipped: 0
|
|
97
|
+
Duration: 1.25s
|
|
98
|
+
|
|
99
|
+
Failed Tests:
|
|
100
|
+
- MXR.Tests.AuthTests.LoginWithInvalidCredentials
|
|
101
|
+
Expected: success, Actual: failure
|
|
102
|
+
- MXR.Tests.NetworkTests.TimeoutHandling
|
|
103
|
+
NullReferenceException: Object reference not set
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Parameters:**
|
|
107
|
+
- `--mode` - `EditMode` or `PlayMode` (optional, defaults to both)
|
|
108
|
+
- `--filter` - Test name filter pattern (optional)
|
|
109
|
+
- `--timeout` - Override default 30s timeout
|
|
110
|
+
|
|
111
|
+
#### Compile Scripts
|
|
112
|
+
|
|
113
|
+
Trigger Unity script compilation:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
unity-bridge compile
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Output (Success):**
|
|
120
|
+
```
|
|
121
|
+
✓ Compilation Successful
|
|
122
|
+
Duration: 2.3s
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Output (Failure):**
|
|
126
|
+
```
|
|
127
|
+
✗ Compilation Failed
|
|
128
|
+
|
|
129
|
+
Assets/Scripts/Player.cs:25: error CS0103: The name 'invalidVar' does not exist
|
|
130
|
+
Assets/Scripts/Enemy.cs:67: error CS0246: Type 'MissingClass' could not be found
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### Get Console Logs
|
|
134
|
+
|
|
135
|
+
Retrieve Unity console output:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# Get last 20 logs
|
|
139
|
+
unity-bridge get-console-logs --limit 20
|
|
140
|
+
|
|
141
|
+
# Get only errors
|
|
142
|
+
unity-bridge get-console-logs --limit 10 --filter Error
|
|
143
|
+
|
|
144
|
+
# Get warnings
|
|
145
|
+
unity-bridge get-console-logs --filter Warning
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Output:**
|
|
149
|
+
```
|
|
150
|
+
Console Logs (last 10, filtered by Error):
|
|
151
|
+
|
|
152
|
+
[Error] NullReferenceException: Object reference not set
|
|
153
|
+
at Player.Update() in Assets/Scripts/Player.cs:34
|
|
154
|
+
|
|
155
|
+
[Error] Failed to load asset: missing_texture.png
|
|
156
|
+
|
|
157
|
+
[Error] (x3) Shader compilation failed
|
|
158
|
+
See Console for details
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Parameters:**
|
|
162
|
+
- `--limit` - Maximum number of logs (default: 50)
|
|
163
|
+
- `--filter` - Filter by type: `Log`, `Warning`, or `Error`
|
|
164
|
+
|
|
165
|
+
#### Get Editor Status
|
|
166
|
+
|
|
167
|
+
Check Unity Editor state:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
unity-bridge get-status
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Output:**
|
|
174
|
+
```
|
|
175
|
+
Unity Editor Status:
|
|
176
|
+
- Compilation: ✓ Ready
|
|
177
|
+
- Play Mode: ✏ Editing
|
|
178
|
+
- Updating: No
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Possible States:**
|
|
182
|
+
- Compilation: `✓ Ready` or `⏳ Compiling...`
|
|
183
|
+
- Play Mode: `✏ Editing`, `▶ Playing`, or `⏸ Paused`
|
|
184
|
+
- Updating: `Yes` or `No`
|
|
185
|
+
|
|
186
|
+
#### Refresh Asset Database
|
|
187
|
+
|
|
188
|
+
Force Unity to refresh assets:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
unity-bridge refresh
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Output:**
|
|
195
|
+
```
|
|
196
|
+
✓ Asset Database Refreshed
|
|
197
|
+
Duration: 0.5s
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Advanced Options
|
|
201
|
+
|
|
202
|
+
#### Timeout Configuration
|
|
203
|
+
|
|
204
|
+
Override the default 30-second timeout:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
unity-bridge run-tests --timeout 60
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Use longer timeouts for:
|
|
211
|
+
- Large test suites
|
|
212
|
+
- PlayMode tests (which start/stop Play Mode)
|
|
213
|
+
- Full project compilation
|
|
214
|
+
|
|
215
|
+
#### Cleanup Old Responses
|
|
216
|
+
|
|
217
|
+
Automatically remove old response files before executing:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
unity-bridge compile --cleanup
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
This removes response files older than 1 hour. Useful for maintaining a clean workspace.
|
|
224
|
+
|
|
225
|
+
#### Verbose Output
|
|
226
|
+
|
|
227
|
+
See detailed execution progress:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
unity-bridge run-tests --verbose
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Prints:
|
|
234
|
+
- Command ID
|
|
235
|
+
- Polling attempts
|
|
236
|
+
- Response file detection
|
|
237
|
+
- Cleanup operations
|
|
238
|
+
|
|
239
|
+
### Error Handling
|
|
240
|
+
|
|
241
|
+
The script provides clear error messages for common issues:
|
|
242
|
+
|
|
243
|
+
**Unity Not Running:**
|
|
244
|
+
```
|
|
245
|
+
Error: Unity Editor not detected. Ensure Unity is open with the project loaded.
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Command Timeout:**
|
|
249
|
+
```
|
|
250
|
+
Error: Command timed out after 30s. Check Unity Console for errors.
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Invalid Parameters:**
|
|
254
|
+
```
|
|
255
|
+
Error: Failed to write command file: Invalid mode 'InvalidMode'
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Exit Codes:**
|
|
259
|
+
- `0` - Success
|
|
260
|
+
- `1` - Error (Unity not running, invalid params, etc.)
|
|
261
|
+
- `2` - Timeout
|
|
262
|
+
|
|
263
|
+
## Integration with Claude Code
|
|
264
|
+
|
|
265
|
+
When you're working in a Unity project directory, you can ask Claude Code to perform Unity operations naturally:
|
|
266
|
+
|
|
267
|
+
- "Run the Unity tests in EditMode"
|
|
268
|
+
- "Check if there are any compilation errors"
|
|
269
|
+
- "Show me the last 10 error logs from Unity"
|
|
270
|
+
- "Refresh the Unity asset database"
|
|
271
|
+
|
|
272
|
+
Claude Code will automatically use this skill to execute the commands via the Python script.
|
|
273
|
+
|
|
274
|
+
## File Protocol Details
|
|
275
|
+
|
|
276
|
+
### Command Format
|
|
277
|
+
|
|
278
|
+
Written to `.unity-bridge/command.json`:
|
|
279
|
+
|
|
280
|
+
```json
|
|
281
|
+
{
|
|
282
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
283
|
+
"action": "run-tests",
|
|
284
|
+
"params": {
|
|
285
|
+
"testMode": "EditMode",
|
|
286
|
+
"filter": "MyTests"
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Response Format
|
|
292
|
+
|
|
293
|
+
Read from `.unity-bridge/response-{id}.json`:
|
|
294
|
+
|
|
295
|
+
```json
|
|
296
|
+
{
|
|
297
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
298
|
+
"status": "success",
|
|
299
|
+
"action": "run-tests",
|
|
300
|
+
"duration_ms": 1250,
|
|
301
|
+
"result": {
|
|
302
|
+
"passed": 410,
|
|
303
|
+
"failed": 0,
|
|
304
|
+
"skipped": 0,
|
|
305
|
+
"failures": []
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Status Values:**
|
|
311
|
+
- `running` - Command in progress (may see intermediate responses)
|
|
312
|
+
- `success` - Command completed successfully
|
|
313
|
+
- `failure` - Command completed with failures (e.g., failed tests)
|
|
314
|
+
- `error` - Command execution error
|
|
315
|
+
|
|
316
|
+
## Project Structure
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
skill/
|
|
320
|
+
├── SKILL.md # This file
|
|
321
|
+
├── pyproject.toml # Package configuration
|
|
322
|
+
├── src/
|
|
323
|
+
│ └── claude_unity_bridge/
|
|
324
|
+
│ ├── __init__.py # Package version
|
|
325
|
+
│ └── cli.py # CLI implementation
|
|
326
|
+
├── tests/
|
|
327
|
+
│ └── test_cli.py # Unit tests
|
|
328
|
+
└── references/
|
|
329
|
+
├── COMMANDS.md # Detailed command specifications
|
|
330
|
+
└── EXTENDING.md # Guide for adding custom commands
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Detailed Documentation
|
|
334
|
+
|
|
335
|
+
For more information, see:
|
|
336
|
+
|
|
337
|
+
- **[COMMANDS.md](references/COMMANDS.md)** - Complete command reference with all parameters, response formats, and edge cases
|
|
338
|
+
- **[EXTENDING.md](references/EXTENDING.md)** - Tutorial for adding custom commands to the Unity Bridge for project-specific workflows
|
|
339
|
+
|
|
340
|
+
## Troubleshooting
|
|
341
|
+
|
|
342
|
+
### Unity Not Responding
|
|
343
|
+
|
|
344
|
+
**Symptoms:** Commands timeout or "Unity not detected" error
|
|
345
|
+
|
|
346
|
+
**Solutions:**
|
|
347
|
+
1. Ensure Unity Editor is open with the project loaded
|
|
348
|
+
2. Check that the package is installed (`Window > Package Manager`)
|
|
349
|
+
3. Verify `.unity-bridge/` directory exists in project root
|
|
350
|
+
4. Check Unity Console for errors from ClaudeBridge package
|
|
351
|
+
|
|
352
|
+
### Response File Issues
|
|
353
|
+
|
|
354
|
+
**Symptoms:** "Failed to parse response JSON" error
|
|
355
|
+
|
|
356
|
+
**Solutions:**
|
|
357
|
+
1. Check Unity Console for ClaudeBridge errors
|
|
358
|
+
2. Manually inspect `.unity-bridge/response-*.json` files
|
|
359
|
+
3. Try cleaning up old responses with `--cleanup` flag
|
|
360
|
+
4. Restart Unity Editor if file system is in bad state
|
|
361
|
+
|
|
362
|
+
### Performance Issues
|
|
363
|
+
|
|
364
|
+
**Symptoms:** Slow response times, frequent timeouts
|
|
365
|
+
|
|
366
|
+
**Solutions:**
|
|
367
|
+
1. Increase timeout with `--timeout 60` or higher
|
|
368
|
+
2. Close unnecessary Unity Editor windows
|
|
369
|
+
3. Reduce test scope with `--filter` parameter
|
|
370
|
+
4. Check system resources (CPU, memory)
|
|
371
|
+
|
|
372
|
+
### File Locking Errors
|
|
373
|
+
|
|
374
|
+
**Symptoms:** Intermittent errors reading/writing files
|
|
375
|
+
|
|
376
|
+
**Solutions:**
|
|
377
|
+
1. The CLI handles file locking automatically with retries
|
|
378
|
+
2. If persistent, check for antivirus interference
|
|
379
|
+
3. Verify file permissions on `.unity-bridge/` directory
|
|
380
|
+
|
|
381
|
+
## Installation
|
|
382
|
+
|
|
383
|
+
### Install the CLI
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
pip install claude-unity-bridge
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Or for development:
|
|
390
|
+
```bash
|
|
391
|
+
cd claude-unity-bridge/skill
|
|
392
|
+
pip install -e ".[dev]"
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Verify Setup
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
unity-bridge health-check
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Install the Skill (Optional)
|
|
402
|
+
|
|
403
|
+
To use this skill in Claude Code:
|
|
404
|
+
|
|
405
|
+
1. Symlink the skill directory:
|
|
406
|
+
```bash
|
|
407
|
+
ln -s "$(pwd)/skill" ~/.claude/skills/unity
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
2. Restart Claude Code to load the skill
|
|
411
|
+
|
|
412
|
+
3. Navigate to your Unity project directory in Claude Code
|
|
413
|
+
|
|
414
|
+
4. Ask Claude to perform Unity operations naturally
|
|
415
|
+
|
|
416
|
+
## Why a CLI Tool?
|
|
417
|
+
|
|
418
|
+
The skill uses a CLI tool instead of implementing the protocol directly in Claude Code prompts for several critical reasons:
|
|
419
|
+
|
|
420
|
+
**Consistency:** UUID generation, polling logic, and error handling work identically every time. Without the CLI, Claude might implement these differently across sessions, leading to subtle bugs.
|
|
421
|
+
|
|
422
|
+
**Reliability:** All edge cases are handled once in tested code:
|
|
423
|
+
- File locking when Unity writes responses
|
|
424
|
+
- Exponential backoff for polling
|
|
425
|
+
- Atomic command writes to prevent corruption
|
|
426
|
+
- Graceful handling of malformed JSON
|
|
427
|
+
- Proper cleanup of stale files
|
|
428
|
+
|
|
429
|
+
**Error Messages:** Clear, actionable error messages for all failure modes. Claude doesn't have to figure out what went wrong each time.
|
|
430
|
+
|
|
431
|
+
**Token Efficiency:** The CLI handles complexity, so Claude doesn't need to manage low-level details in-context. The SKILL.md stays concise while providing full functionality.
|
|
432
|
+
|
|
433
|
+
**Deterministic Exit Codes:** Shell integration works reliably with standard exit codes (0=success, 1=error, 2=timeout).
|
|
434
|
+
|
|
435
|
+
**Rock Solid:** Test the CLI once, it works forever. No variability between Claude sessions.
|
|
436
|
+
|
|
437
|
+
## Support
|
|
438
|
+
|
|
439
|
+
For issues or questions:
|
|
440
|
+
- Package Issues: https://github.com/ManageXR/claude-unity-bridge/issues
|
|
441
|
+
- Skill Issues: Report in the same repository with `[Skill]` prefix
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
claude_unity_bridge/__init__.py,sha256=RGbOBgaouQGwPE4EgsWJVpkqWleV75lM4mJwtf9op30,90
|
|
2
|
+
claude_unity_bridge/cli.py,sha256=8CwAF77F7AQMCXdFE85Ab0Y76Qg3uSNB4RTcjvhz5sM,19096
|
|
3
|
+
claude_unity_bridge-0.1.2.dist-info/METADATA,sha256=GAfcVDbfnq778bwl50nHAlyJkp4Nve7zr42Mswvi1is,11345
|
|
4
|
+
claude_unity_bridge-0.1.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
5
|
+
claude_unity_bridge-0.1.2.dist-info/entry_points.txt,sha256=LMSJsppYYp5-i3KhQUdwrD7kk6bwvnS59W3iH49JOhs,62
|
|
6
|
+
claude_unity_bridge-0.1.2.dist-info/top_level.txt,sha256=-2lJjfvbFp_krCaILy9Sm2BT0k6PHzFsk2MdTcvCXbY,20
|
|
7
|
+
claude_unity_bridge-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
claude_unity_bridge
|