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.
@@ -0,0 +1,3 @@
1
+ """Claude Unity Bridge - Control Unity Editor from Claude Code."""
2
+
3
+ __version__ = "0.1.1"
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ unity-bridge = claude_unity_bridge.cli:main
@@ -0,0 +1 @@
1
+ claude_unity_bridge