diff --git a/.code-linter.json b/.code-linter.json
index 2d325db..a16cfd4 100644
--- a/.code-linter.json
+++ b/.code-linter.json
@@ -1,10 +1,10 @@
{
- "max_file_lines": 750,
- "max_function_lines": 150,
- "max_nesting_depth": 8,
- "max_parameters": 8,
- "max_comment_lines": 15,
- "max_types_per_file": 6,
+ "max_file_lines": 300,
+ "max_function_lines": 50,
+ "max_nesting_depth": 4,
+ "max_parameters": 5,
+ "max_comment_lines": 5,
+ "max_types_per_file": 2,
"include_extensions": [
".cs",
".py"
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..b6496c5
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,10 @@
+* @daliys
+
+# Quality Gate & CI Policy files strictly owned by @daliys
+/.code-linter.json @daliys
+/.ruff.toml @daliys
+/.unity-quality-gate.json @daliys
+/.swift-quality-gate.json @daliys
+/.slop-review.json @daliys
+/.github/workflows/ @daliys
+/.github/CODEOWNERS @daliys
diff --git a/API_REFERENCE.MD b/API_REFERENCE.MD
index 2617c4f..dfabc36 100644
--- a/API_REFERENCE.MD
+++ b/API_REFERENCE.MD
@@ -192,7 +192,7 @@ Actions: `undo`, `redo`, `play`, `pause`, `step`, `menu`, `read_logs`, `clear_lo
### `unity_ui_automation`
Actions: `list_windows`, `get_hierarchy`, `query`, `get_window_rect`, `set_window_rect`, `capture_window_snapshot`, `click`, `input`.
-`get_hierarchy` accepts `deep`. `query` accepts `name`, `text`, and `class_name`. Window rect and snapshot actions are intended for resize/layout QA; window snapshots include rect and hierarchy, with best-effort PNG image capture on macOS.
+`get_hierarchy` accepts `deep`, `max_depth`, and `max_elements` (with `children_truncated` and root `truncated` markers when capped). `query` accepts `name`, `text`, `class_name`, `max_depth`, and `max_results`. Window rect and snapshot actions are intended for resize/layout QA; window snapshots include rect and hierarchy (supporting `max_depth` and `max_elements`), with best-effort PNG image capture on macOS.
### `unity_wait`
Conditions: `compilation`, `play_mode`, `import`, `editor_idle`.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e37e3c..847121f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ All notable public changes to Nexus Unity are documented here.
- Consolidated duplicate internal Ollama-review and serialized-property write helpers.
### Fixed
+- Add traversal depth and element/result bounds to UI Toolkit hierarchy serialization and element queries (`ui_get_hierarchy`, `ui_query_elements`, and `ui_capture_window_snapshot`), preventing editor stalls and payload bloat on complex UI windows, with `children_truncated` and `truncated` markers on partial hierarchy responses (#126).
- Dispatch WebSocket request handling asynchronously in `Task.Run`, ensuring `ServerLoop` remains non-blocking for concurrent HTTP JSON-RPC requests and new connections (#129).
- `EditorApplication.update` subscription for `HandlePostCompileFocusReturn` is now idempotent (unsubscribes before subscribing), preventing stacked duplicate frame handlers on repeated initialization or domain reloads (#138).
- `ListPlayerPrefs` now uses `ProcessStartInfo.ArgumentList` and timeout guards for macOS `defaults read` execution, preventing process hangs, alongside regex unhashing for Windows registry keys, Linux XML prefs support, and double-default type verification (#143).
diff --git a/DOCUMENTATION.MD b/DOCUMENTATION.MD
index 654f90b..7cab974 100644
--- a/DOCUMENTATION.MD
+++ b/DOCUMENTATION.MD
@@ -117,7 +117,7 @@ MCP bridge:
- `unity_hierarchy_manager` can create primitives with name, parent, transform, and material path; it can also rename, set transforms, and pass through `create_hierarchy`.
- Raw `run_tests` returns `Submitted` when Unity accepts the asynchronous request; it does not confirm that execution has begun or completed. `unity_editor_controller` `run_tests_wait` waits in the Python bridge by polling raw `get_test_results` instead of blocking the Unity main thread.
- `unity_editor_controller` also exposes `get_tool_usage_stats` and `reset_tool_usage_stats` so diagnostics are not split between raw and manager workflows.
-- `unity_ui_automation` passes through `deep` for UI hierarchy reads, `class_name` for queries, and window rect/snapshot actions for layout checks.
+- `unity_ui_automation` passes through `deep`, `max_depth`, and `max_elements` for UI hierarchy reads, `class_name`, `max_depth`, and `max_results` for queries, and window rect/snapshot actions for layout checks.
## Current Compatibility Decisions
diff --git a/Editor/MCPCliInstaller.ClaudeCode.cs b/Editor/MCPCliInstaller.ClaudeCode.cs
index e7734f0..619901c 100644
--- a/Editor/MCPCliInstaller.ClaudeCode.cs
+++ b/Editor/MCPCliInstaller.ClaudeCode.cs
@@ -1,8 +1,8 @@
-using UnityEditor;
-using UnityEngine;
-using System.IO;
using System;
+using System.IO;
using Newtonsoft.Json.Linq;
+using UnityEditor;
+using UnityEngine;
namespace UnityMCP.Editor
{
@@ -15,9 +15,7 @@ public static partial class MCPCliInstaller
///
/// Prefers the official claude CLI (claude mcp add --scope project, which itself writes
/// .mcp.json). When the CLI is unavailable or fails, it falls back to writing .mcp.json directly.
- /// Either path produces the same project-root .mcp.json that the Integrations tab tracks. This is distinct
- /// from the Claude Desktop app config, which is handled by the generic JSON-config codepath in
- /// instead of a dedicated CLI installer.
+ /// Either path produces the same project-root .mcp.json that the Integrations tab tracks.
///
public static void LinkToClaudeCode()
{
@@ -31,45 +29,43 @@ public static void LinkToClaudeCode()
private static void ExecuteClaudeCodeLinkSequence(string scriptPath, string pythonPath)
{
string claudePath = ResolveExecutablePath("claude");
-
- // Preferred path: the official claude CLI writes the project-scoped .mcp.json for us.
if (!string.IsNullOrEmpty(claudePath) && claudePath != "claude")
{
- // 1. Remove any stale registration so re-running is idempotent.
- // A missing registration is expected on first setup and is safe to add over.
- bool removedStaleRegistration = RunInstallerProcess(CreateProcessStartInfo(claudePath, "mcp", "remove", "--scope", "project", "nexus-unity"), claudePath, false, "Claude Code", out string removeError, false);
- bool registrationWasAbsent = !removedStaleRegistration && IsClaudeCodeRegistrationAbsent(removeError);
-
- // 2. Add the server at project scope only when the prior state is known to be clear.
- if ((removedStaleRegistration || registrationWasAbsent) && RunInstallerProcess(CreateProcessStartInfo(claudePath, "mcp", "add", "--transport", "stdio", "--scope", "project", "--env", MCPServer.AuthTokenEnvironmentVariable + "=" + MCPServer.AuthToken, "nexus-unity", "--", pythonPath, scriptPath), claudePath, false, "Claude Code"))
+ if (TryLinkViaClaudeCli(claudePath, scriptPath, pythonPath))
{
- NexusEditorLog.Log(NexusLogCategory.Integrations, "[MCP] Successfully linked Nexus Unity to Claude Code via '" + claudePath + "'.", true);
- EditorUtility.DisplayDialog("MCP Success", "Successfully linked Nexus Unity to Claude Code.\n\nRun /mcp inside Claude Code (or restart it) to load the server.", "OK");
return;
}
+ }
+
+ WriteClaudeCodeDirectJson(scriptPath, pythonPath);
+ }
- NexusEditorLog.Warning(NexusLogCategory.Integrations, (removedStaleRegistration || registrationWasAbsent)
- ? "[MCP] Claude Code CLI add command failed at '" + claudePath + "'. Falling back to direct .mcp.json edit."
- : "[MCP] Claude Code CLI could not remove the existing registration at '" + claudePath + "': " + removeError + ". Skipping CLI add and falling back to direct .mcp.json edit.");
+ private static bool TryLinkViaClaudeCli(string claudePath, string scriptPath, string pythonPath)
+ {
+ bool removedStale = RunInstallerProcess(CreateProcessStartInfo(claudePath, "mcp", "remove", "--scope", "project", "nexus-unity"), false, "Claude Code", out string removeError, false);
+ bool registrationAbsent = !removedStale && IsClaudeCodeRegistrationAbsent(removeError);
+
+ if ((removedStale || registrationAbsent) && RunInstallerProcess(CreateProcessStartInfo(claudePath, "mcp", "add", "--transport", "stdio", "--scope", "project", "--env", MCPServer.AuthTokenEnvironmentVariable + "=" + MCPServer.AuthToken, "nexus-unity", "--", pythonPath, scriptPath), claudePath, false, "Claude Code"))
+ {
+ NexusEditorLog.Log(NexusLogCategory.Integrations, "[MCP] Successfully linked Nexus Unity to Claude Code via '" + claudePath + "'.", true);
+ EditorUtility.DisplayDialog("MCP Success", "Successfully linked Nexus Unity to Claude Code.\n\nRun /mcp inside Claude Code (or restart it) to load the server.", "OK");
+ return true;
}
- // Fallback: write the project-root .mcp.json directly.
+ NexusEditorLog.Warning(NexusLogCategory.Integrations, (removedStale || registrationAbsent)
+ ? "[MCP] Claude Code CLI add command failed at '" + claudePath + "'. Falling back to direct .mcp.json edit."
+ : "[MCP] Claude Code CLI could not remove the existing registration at '" + claudePath + "': " + removeError + ". Skipping CLI add and falling back to direct .mcp.json edit.");
+ return false;
+ }
+
+ private static void WriteClaudeCodeDirectJson(string scriptPath, string pythonPath)
+ {
try
{
string projectRoot = Path.GetDirectoryName(Application.dataPath);
string configPath = Path.Combine(projectRoot, ".mcp.json");
- JObject config;
- if (File.Exists(configPath))
- {
- try { config = JObject.Parse(File.ReadAllText(configPath)); }
- catch { config = new JObject(); }
- }
- else
- {
- config = new JObject();
- }
-
+ JObject config = LoadOrCreateJsonObject(configPath);
if (config["mcpServers"] == null) config["mcpServers"] = new JObject();
JObject servers = (JObject)config["mcpServers"];
@@ -93,6 +89,16 @@ private static void ExecuteClaudeCodeLinkSequence(string scriptPath, string pyth
}
}
+ private static JObject LoadOrCreateJsonObject(string path)
+ {
+ if (File.Exists(path))
+ {
+ try { return JObject.Parse(File.ReadAllText(path)); }
+ catch { return new JObject(); }
+ }
+ return new JObject();
+ }
+
internal static bool IsClaudeCodeRegistrationAbsent(string error)
{
return !string.IsNullOrEmpty(error)
diff --git a/Editor/MCPCliInstaller.Deploy.cs b/Editor/MCPCliInstaller.Deploy.cs
new file mode 100644
index 0000000..e3666bc
--- /dev/null
+++ b/Editor/MCPCliInstaller.Deploy.cs
@@ -0,0 +1,164 @@
+using System;
+using System.IO;
+using UnityEditor;
+using UnityEngine;
+
+namespace UnityMCP.Editor
+{
+ public static partial class MCPCliInstaller
+ {
+ private static bool DeployBridgeScript(out string destinationPath)
+ {
+ destinationPath = null;
+ string sourcePath = FindBridgeScript();
+
+ if (string.IsNullOrEmpty(sourcePath))
+ {
+ NexusEditorLog.Error(NexusLogCategory.Integrations, "[MCP] Could not find 'nexus_unity_bridge.py' in the project.");
+ EditorUtility.DisplayDialog("MCP Error", "Could not find 'nexus_unity_bridge.py'.\n\nEnsure the library is correctly imported.", "OK");
+ return false;
+ }
+
+ string projectRoot = Path.GetDirectoryName(Application.dataPath);
+ destinationPath = Path.Combine(projectRoot, "nexus_unity_bridge.py");
+
+ try
+ {
+ File.Copy(sourcePath, destinationPath, true);
+ DeployBridgeModule(projectRoot, sourcePath);
+ NexusEditorLog.Log(NexusLogCategory.Integrations, "[MCP] Bridge script deployed to stable location: " + destinationPath, true);
+ DeployDocumentationPointer(projectRoot, sourcePath);
+ return true;
+ }
+ catch (Exception e)
+ {
+ NexusEditorLog.Error(NexusLogCategory.Integrations, "[MCP] Failed to deploy bridge or docs: " + e.Message);
+ EditorUtility.DisplayDialog("MCP Error", "Failed to deploy integration files to project root.\n\n" + e.Message, "OK");
+ return false;
+ }
+ }
+
+ private static void DeployBridgeModule(string projectRoot, string sourcePath)
+ {
+ string sourceDir = Path.GetDirectoryName(sourcePath);
+ string sourceModuleDir = Path.Combine(sourceDir, "nexus_bridge");
+ if (!Directory.Exists(sourceModuleDir))
+ {
+ return;
+ }
+
+ string destinationModuleDir = Path.Combine(projectRoot, "nexus_bridge");
+ CopyDirectory(sourceModuleDir, destinationModuleDir);
+ NexusEditorLog.Log(NexusLogCategory.Integrations, "[MCP] Bridge module deployed to stable location: " + destinationModuleDir);
+ }
+
+ private static void CopyDirectory(string sourceDir, string destinationDir)
+ {
+ Directory.CreateDirectory(destinationDir);
+
+ foreach (string file in Directory.GetFiles(sourceDir))
+ {
+ string fileName = Path.GetFileName(file);
+ if (fileName.EndsWith(".meta", StringComparison.OrdinalIgnoreCase) ||
+ fileName.EndsWith(".pyc", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ File.Copy(file, Path.Combine(destinationDir, fileName), true);
+ }
+
+ foreach (string dir in Directory.GetDirectories(sourceDir))
+ {
+ string dirName = Path.GetFileName(dir);
+ if (dirName == "__pycache__")
+ {
+ continue;
+ }
+
+ CopyDirectory(dir, Path.Combine(destinationDir, dirName));
+ }
+ }
+
+ private static string FindLibraryRoot(string sourcePath)
+ {
+ string dir = Path.GetDirectoryName(sourcePath);
+ while (!string.IsNullOrEmpty(dir))
+ {
+ if (File.Exists(Path.Combine(dir, "package.json")))
+ {
+ return dir;
+ }
+ dir = Path.GetDirectoryName(dir);
+ }
+ return null;
+ }
+
+ private static void DeployDocumentationPointer(string projectRoot, string sourcePath)
+ {
+ string libraryRoot = FindLibraryRoot(sourcePath);
+ if (string.IsNullOrEmpty(libraryRoot))
+ {
+ return;
+ }
+
+ string docSource = Path.Combine(libraryRoot, "DOCUMENTATION.MD");
+ if (!File.Exists(docSource))
+ {
+ return;
+ }
+
+ string relativeDocPath = GetRelativePath(projectRoot, docSource).Replace("\\", "/");
+ string pointerPath = Path.Combine(projectRoot, "NEXUS_UNITY_DOCUMENTATION.md");
+ string pointerContent = "# Nexus Unity Documentation\n\n" +
+ "The canonical Nexus Unity documentation is maintained in the package root:\n\n" +
+ "- [DOCUMENTATION.MD](" + relativeDocPath + ")\n\n" +
+ "Keep all edits in the package copy so changes stay with version control.\n";
+
+ File.WriteAllText(pointerPath, pointerContent);
+ }
+
+ private static string GetRelativePath(string fromPath, string toPath)
+ {
+ Uri fromUri = new Uri(AppendDirectorySeparator(Path.GetFullPath(fromPath)));
+ Uri toUri = new Uri(Path.GetFullPath(toPath));
+ Uri relativeUri = fromUri.MakeRelativeUri(toUri);
+ return Uri.UnescapeDataString(relativeUri.ToString());
+ }
+
+ private static string AppendDirectorySeparator(string path)
+ {
+ if (path.EndsWith(Path.DirectorySeparatorChar.ToString()) ||
+ path.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
+ {
+ return path;
+ }
+ return path + Path.DirectorySeparatorChar;
+ }
+
+ private static string FindBridgeScript()
+ {
+ string[] guids = AssetDatabase.FindAssets("nexus_unity_bridge");
+ foreach (string guid in guids)
+ {
+ string path = AssetDatabase.GUIDToAssetPath(guid);
+ if (path.EndsWith("nexus_unity_bridge.py"))
+ {
+ return Path.GetFullPath(path);
+ }
+ }
+
+ string manualSearch = Path.Combine(Application.dataPath, "nexus_unity_bridge.py");
+ if (File.Exists(manualSearch))
+ {
+ return manualSearch;
+ }
+
+ foreach (string path in Directory.GetFiles(Application.dataPath, "*.py", SearchOption.AllDirectories))
+ {
+ if (path.EndsWith("nexus_unity_bridge.py")) return Path.GetFullPath(path);
+ }
+ return null;
+ }
+ }
+}
diff --git a/Editor/MCPCliInstaller.Deploy.cs.meta b/Editor/MCPCliInstaller.Deploy.cs.meta
new file mode 100644
index 0000000..a7c88f2
--- /dev/null
+++ b/Editor/MCPCliInstaller.Deploy.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: be5b21cfbca94822a22c7c66930cd272
diff --git a/Editor/MCPCliInstaller.cs b/Editor/MCPCliInstaller.cs
index 47c3e07..24c396a 100644
--- a/Editor/MCPCliInstaller.cs
+++ b/Editor/MCPCliInstaller.cs
@@ -1,9 +1,9 @@
-using UnityEditor;
-using UnityEngine;
-using System.IO;
-using System.Diagnostics;
using System;
+using System.Diagnostics;
+using System.IO;
using System.Text;
+using UnityEditor;
+using UnityEngine;
namespace UnityMCP.Editor
{
@@ -16,226 +16,77 @@ namespace UnityMCP.Editor
///
public static partial class MCPCliInstaller
{
-
-
- private static bool DeployBridgeScript(out string destinationPath)
- {
- destinationPath = null;
- string sourcePath = FindBridgeScript();
-
- if (string.IsNullOrEmpty(sourcePath))
- {
- NexusEditorLog.Error(NexusLogCategory.Integrations, "[MCP] Could not find 'nexus_unity_bridge.py' in the project.");
- EditorUtility.DisplayDialog("MCP Error", "Could not find 'nexus_unity_bridge.py'.\n\nEnsure the library is correctly imported.", "OK");
- return false;
- }
-
- string projectRoot = Path.GetDirectoryName(Application.dataPath);
- destinationPath = Path.Combine(projectRoot, "nexus_unity_bridge.py");
-
- try
- {
- File.Copy(sourcePath, destinationPath, true);
- DeployBridgeModule(projectRoot, sourcePath);
- NexusEditorLog.Log(NexusLogCategory.Integrations, "[MCP] Bridge script deployed to stable location: " + destinationPath, true);
- DeployDocumentationPointer(projectRoot, sourcePath);
- return true;
- }
- catch (Exception e)
- {
- NexusEditorLog.Error(NexusLogCategory.Integrations, "[MCP] Failed to deploy bridge or docs: " + e.Message);
- EditorUtility.DisplayDialog("MCP Error", "Failed to deploy integration files to project root.\n\n" + e.Message, "OK");
- return false;
- }
- }
-
- private static void DeployBridgeModule(string projectRoot, string sourcePath)
- {
- string sourceDir = Path.GetDirectoryName(sourcePath);
- string sourceModuleDir = Path.Combine(sourceDir, "nexus_bridge");
- if (!Directory.Exists(sourceModuleDir))
- {
- return;
- }
-
- string destinationModuleDir = Path.Combine(projectRoot, "nexus_bridge");
- CopyDirectory(sourceModuleDir, destinationModuleDir);
- NexusEditorLog.Log(NexusLogCategory.Integrations, "[MCP] Bridge module deployed to stable location: " + destinationModuleDir);
- }
-
- private static void CopyDirectory(string sourceDir, string destinationDir)
+ private static string ResolveExecutablePath(string name)
{
- Directory.CreateDirectory(destinationDir);
-
- foreach (string file in Directory.GetFiles(sourceDir))
+ if (Application.platform == RuntimePlatform.WindowsEditor)
{
- string fileName = Path.GetFileName(file);
- if (fileName.EndsWith(".meta", StringComparison.OrdinalIgnoreCase) ||
- fileName.EndsWith(".pyc", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- File.Copy(file, Path.Combine(destinationDir, fileName), true);
+ string fromWhere = GetPathFromWhere(name);
+ return string.IsNullOrEmpty(fromWhere) ? name : fromWhere;
}
- foreach (string dir in Directory.GetDirectories(sourceDir))
+ if (name == "codex")
{
- string dirName = Path.GetFileName(dir);
- if (dirName == "__pycache__")
- {
- continue;
- }
-
- CopyDirectory(dir, Path.Combine(destinationDir, dirName));
+ string nvmCodex = ResolveNvmCodex();
+ if (!string.IsNullOrEmpty(nvmCodex)) return nvmCodex;
}
- }
- private static string FindLibraryRoot(string sourcePath)
- {
- string dir = Path.GetDirectoryName(sourcePath);
- while (!string.IsNullOrEmpty(dir))
- {
- if (File.Exists(Path.Combine(dir, "package.json")))
- {
- return dir;
- }
- dir = Path.GetDirectoryName(dir);
- }
- return Path.GetDirectoryName(sourcePath);
- }
-
- private static void DeployDocumentationPointer(string projectRoot, string sourcePath)
- {
- string libraryRoot = FindLibraryRoot(sourcePath);
- string[] docFiles = { "API_REFERENCE.MD", "DOCUMENTATION.MD" };
-
- foreach (string file in docFiles)
+ string pathFromWhich = GetPathFromWhich(name);
+ if (!string.IsNullOrEmpty(pathFromWhich) && pathFromWhich != name && File.Exists(pathFromWhich))
{
- string src = Path.Combine(libraryRoot, file);
- if (File.Exists(src))
+ if (name != "codex" || !pathFromWhich.Contains("homebrew"))
{
- try
- {
- string dst = Path.Combine(projectRoot, file);
- File.Copy(src, dst, true);
- NexusEditorLog.Log(NexusLogCategory.Integrations, "[MCP] Copied documentation to root: " + dst);
- }
- catch (Exception e)
- {
- NexusEditorLog.Warning(NexusLogCategory.Integrations, "[MCP] Failed to copy " + file + " to root: " + e.Message);
- }
+ return pathFromWhich;
}
}
- string docPointerPath = Path.Combine(projectRoot, "NEXUS_UNITY_DOCS.md");
- string docContent = "# Nexus Unity - AI Context\n\n" +
- "This project uses **Nexus Unity** for AI Editor automation.\n\n" +
- "## 📚 Documentation Access\n" +
- "- **Full Tool Reference**: [API_REFERENCE.MD](API_REFERENCE.MD)\n" +
- "- **Technical Guide**: [DOCUMENTATION.MD](DOCUMENTATION.MD)\n\n" +
- "## 🤖 AI Instructions\n" +
- "Before performing any Unity tasks, ALWAYS read `API_REFERENCE.MD` to understand the available tools, their parameters, and the surgical editing patterns required for this project.";
-
- File.WriteAllText(docPointerPath, docContent);
- NexusEditorLog.Log(NexusLogCategory.Integrations, "[MCP] Documentation pointer deployed: " + docPointerPath);
+ return ResolveSystemExecutable(name);
}
-
-
- private static string FindBridgeScript()
+ private static string ResolveNvmCodex()
{
- string[] guids = AssetDatabase.FindAssets("MCPCliInstaller t:Script");
- foreach (var guid in guids)
- {
- string path = AssetDatabase.GUIDToAssetPath(guid);
- if (!path.Contains("NexusUnity")) continue;
-
- string dir = Path.GetDirectoryName(path);
- string potentialBridge = Path.Combine(dir, "nexus_unity_bridge.py");
- if (File.Exists(Path.GetFullPath(potentialBridge))) return Path.GetFullPath(potentialBridge);
- }
+ string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ string nvmBase = Path.Combine(home, ".nvm/versions/node");
+ if (!Directory.Exists(nvmBase)) return null;
- foreach (var path in AssetDatabase.GetAllAssetPaths())
+ foreach (var versionDir in Directory.GetDirectories(nvmBase))
{
- if (path.EndsWith("nexus_unity_bridge.py")) return Path.GetFullPath(path);
+ string potential = Path.Combine(versionDir, "bin/codex");
+ if (File.Exists(potential)) return potential;
}
return null;
}
- private static string ResolveExecutablePath(string name)
+ private static string ResolveSystemExecutable(string name)
{
- if (Application.platform == RuntimePlatform.WindowsEditor)
- {
- // 'which' does not exist on Windows; use 'where' to resolve against PATH (and PATHEXT, so
- // .cmd/.exe shims like gemini.cmd are found). Falls back to the bare name when unresolved.
- string fromWhere = GetPathFromWhere(name);
- return string.IsNullOrEmpty(fromWhere) ? name : fromWhere;
- }
-
- // 1. Explicit check for NVM/Node paths (High priority for Codex)
- if (name == "codex")
- {
- string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
- string nvmBase = Path.Combine(home, ".nvm/versions/node");
- if (Directory.Exists(nvmBase))
- {
- foreach (var versionDir in Directory.GetDirectories(nvmBase))
- {
- string potential = Path.Combine(versionDir, "bin/codex");
- if (File.Exists(potential)) return potential;
- }
- }
- }
-
- // 2. Try 'which' (respects shell PATH)
- string pathFromWhich = GetPathFromWhich(name);
- if (!string.IsNullOrEmpty(pathFromWhich) && pathFromWhich != name && File.Exists(pathFromWhich))
- {
- // Only return if it's not the older homebrew version of codex (if we can tell)
- if (name == "codex" && pathFromWhich.Contains("homebrew")) {
- // Continue to fallback if we prefer NVM
- } else {
- return pathFromWhich;
- }
- }
-
- // 3. Common system paths
- string[] searchPaths = {
- "/usr/local/bin/" + name,
- "/opt/homebrew/bin/" + name,
- "/usr/bin/" + name,
- "/bin/" + name
+ string[] searchPaths = {
+ "/usr/local/bin/" + name,
+ "/opt/homebrew/bin/" + name,
+ "/usr/bin/" + name,
+ "/bin/" + name
};
foreach (string path in searchPaths)
{
if (File.Exists(path)) return path;
}
-
return name;
}
private static string GetPathFromWhich(string name)
{
- ProcessStartInfo psi = new ProcessStartInfo
- {
- FileName = "/usr/bin/which",
- Arguments = name,
- RedirectStandardOutput = true,
- UseShellExecute = false,
- CreateNoWindow = true
+ ProcessStartInfo psi = new ProcessStartInfo
+ {
+ FileName = "/usr/bin/which",
+ Arguments = name,
+ RedirectStandardOutput = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
};
if (Application.platform != RuntimePlatform.WindowsEditor)
{
string pathEnv = "";
try { pathEnv = Environment.GetEnvironmentVariable("PATH"); } catch(Exception) {}
- // Unity launched from Finder/Hub (not a terminal) inherits a stripped PATH
- // (typically just /usr/bin:/bin:/usr/sbin:/sbin) with no /usr/local/bin or /opt/homebrew/bin.
- // 'which' would then resolve python3 to the old Xcode-bundled interpreter at /usr/bin/python3
- // instead of a modern one. Prepend common interpreter locations so they're checked first,
- // matching the priority order ResolveExecutablePath's own fallback search list already uses.
psi.EnvironmentVariables["PATH"] = "/usr/local/bin:/opt/homebrew/bin:" + pathEnv;
}
@@ -269,14 +120,13 @@ private static string GetPathFromWhere(string name)
{
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
- if (p.ExitCode == 0 && !string.IsNullOrWhiteSpace(output))
+ if (p.ExitCode != 0 || string.IsNullOrWhiteSpace(output)) return null;
+
+ string[] lines = output.Split('\n');
+ for (int i = 0; i < lines.Length; i++)
{
- // 'where' can list multiple matches, one per line. Take the first that exists on disk.
- foreach (string line in output.Split('\n'))
- {
- string candidate = line.Trim();
- if (!string.IsNullOrEmpty(candidate) && File.Exists(candidate)) return candidate;
- }
+ string candidate = lines[i].Trim();
+ if (!string.IsNullOrEmpty(candidate) && File.Exists(candidate)) return candidate;
}
}
}
@@ -284,14 +134,6 @@ private static string GetPathFromWhere(string name)
return null;
}
- ///
- /// Resolves the Python interpreter to embed in generated MCP configs, preferring a concrete path.
- /// Tries python3, then python, then the Windows py launcher (which defaults to Python 3).
- ///
- ///
- /// Generated configs previously hardcoded python3, which is frequently absent from PATH on Windows
- /// (where the interpreter is usually python or the py launcher), so the bridge failed to launch.
- ///
private static string ResolvePythonPath()
{
string python3 = ResolveExecutablePath("python3");
@@ -300,7 +142,6 @@ private static string ResolvePythonPath()
string python = ResolveExecutablePath("python");
if (!string.IsNullOrEmpty(python) && python != "python") return python;
- // Windows commonly ships the 'py' launcher instead of a 'python3' alias; 'py