diff --git a/src/ucode/agents/claude.py b/src/ucode/agents/claude.py index d0d0380..714fc29 100644 --- a/src/ucode/agents/claude.py +++ b/src/ucode/agents/claude.py @@ -30,10 +30,31 @@ CLAUDE_CONFIG_DIR = Path.home() / ".claude" CLAUDE_SETTINGS_PATH = CLAUDE_CONFIG_DIR / "ucode-settings.json" +CLAUDE_DEFAULT_SETTINGS_PATH = CLAUDE_CONFIG_DIR / "settings.json" CLAUDE_BACKUP_PATH = APP_DIR / "claude-ucode-settings.backup.json" +CLAUDE_DEFAULT_BACKUP_PATH = APP_DIR / "claude-settings.backup.json" + +# On Windows, subprocess can fail to locate 'claude' via PATH because npm +# wrappers are .cmd files that Python's subprocess doesn't resolve the same +# way as cmd.exe. Resolve the actual .exe inside the npm package tree so +# the subprocess call is unambiguous on all platforms. +_claude_wrapper = shutil.which("claude.cmd") or shutil.which("claude") or shutil.which("claude.bat") +if _claude_wrapper: + _claude_root = Path(_claude_wrapper).parent + _claude_exe = ( + _claude_root + / "node_modules" + / "@anthropic-ai" + / "claude-code" + / "bin" + / "claude.exe" + ) + CLAUDE_BINARY = str(_claude_exe) if _claude_exe.exists() else _claude_wrapper +else: + CLAUDE_BINARY = "claude" SPEC: ToolSpec = { - "binary": "claude", + "binary": CLAUDE_BINARY, "package": "@anthropic-ai/claude-code", "display": "Claude Code", "config_path": CLAUDE_SETTINGS_PATH, @@ -215,6 +236,7 @@ def _unregister_web_search_mcp() -> None: def write_tool_config(state: dict, model: str) -> dict: backup_existing_file(CLAUDE_SETTINGS_PATH, CLAUDE_BACKUP_PATH) + backup_existing_file(CLAUDE_DEFAULT_SETTINGS_PATH, CLAUDE_DEFAULT_BACKUP_PATH) web_search_model = _resolve_web_search_model(state) overlay, managed_keys = render_overlay( state["workspace"], @@ -251,6 +273,14 @@ def write_tool_config(state: dict, model: str) -> dict: _remove_tracing_stop_hook(merged) write_json_file(CLAUDE_SETTINGS_PATH, merged) + # Mirror the auth/env overlay into ~/.claude/settings.json so Claude Code + # finds the apiKeyHelper and ANTHROPIC_* env vars regardless of whether it + # is launched via `ucode claude` (which passes --settings) or directly from + # the Claude desktop app / IDE extension (which reads settings.json by default). + existing_default = read_json_safe(CLAUDE_DEFAULT_SETTINGS_PATH) + merged_default = deep_merge_dict(existing_default, overlay) + write_json_file(CLAUDE_DEFAULT_SETTINGS_PATH, merged_default) + if web_search_model: _register_web_search_mcp(state["workspace"], web_search_model, state.get("profile")) diff --git a/src/ucode/databricks.py b/src/ucode/databricks.py index 90c1808..a74a0c7 100644 --- a/src/ucode/databricks.py +++ b/src/ucode/databricks.py @@ -13,6 +13,7 @@ import shlex import shutil import subprocess +import sys from pathlib import Path from typing import Literal, cast, overload from urllib import error as urllib_error @@ -956,7 +957,29 @@ def list_databricks_apps(workspace: str, profile: str | None = None) -> list[dic def build_auth_shell_command(workspace: str, profile: str | None = None) -> str: - workspace_arg = shlex.quote(workspace.rstrip("/")) + workspace_clean = workspace.rstrip("/") + + if os.name == "nt": + # On Windows, Claude Code runs apiKeyHelper via cmd.exe, which does not + # understand POSIX syntax ([ -n "$VAR" ], env -u, jq pipes). Instead we + # delegate to the Python interpreter that is running ucode — it is always + # available, handles DATABRICKS_BEARER short-circuit, and parses the JSON + # token response without requiring jq on PATH. + python_exe = sys.executable + databricks_exe = shutil.which("databricks") or "databricks" + profile_part = f", '--profile', {profile!r}" if profile else "" + helper_code = ( + "import json, os, subprocess, sys; " + "bearer=os.environ.get('DATABRICKS_BEARER'); " + "sys.exit(print(bearer) or 0) if bearer else None; " + f"cmd=[{databricks_exe!r},'auth','token','--host',{workspace_clean!r}" + f"{profile_part},'--force-refresh','--output','json']; " + "p=subprocess.run(cmd,capture_output=True,text=True,check=True,timeout=30); " + "print(json.loads(p.stdout)['access_token'])" + ) + return f'"{python_exe}" -c {helper_code!r}' + + workspace_arg = shlex.quote(workspace_clean) if profile: profile_arg = shlex.quote(profile) cli_command = (