diff --git a/.gitattributes b/.gitattributes index a5486f9210..0ee20a9b0b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,3 @@ src/free_claude_code/core/data/cl100k_base.tiktoken -text linguist-vendored=true +scripts/update/fcc-update text eol=lf +scripts/update/*.cmd text eol=crlf diff --git a/README.md b/README.md index b9617eb2fa..d779e2da31 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Free-tier availability and limits are controlled by each provider and may change -### 1. Install Or Update +### 1. Install macOS/Linux: @@ -64,7 +64,7 @@ Windows PowerShell: & ([scriptblock]::Create((irm "https://raw.githubusercontent.com/Alishahryar1/free-claude-code/main/scripts/install.ps1"))) ``` -Re-run the same command to update. When prompted, choose at least one coding agent and optionally RTK. You can review the installers before running them: [install.sh](scripts/install.sh) and [install.ps1](scripts/install.ps1). +When prompted, choose at least one coding agent and optionally RTK. You can review the installers before running them: [install.sh](scripts/install.sh) and [install.ps1](scripts/install.ps1). ### 2. Start FCC @@ -483,7 +483,15 @@ Run `fcc-server --version` to check the installed version without starting FCC. ### Update -Re-run the matching command from [Install Or Update](#install). +Stop all running FCC commands, then run: + +```sh +fcc-update +``` + +This runs the same installer as above, including its coding-agent prompts and checks. If you use voice support, pass the same voice options used during installation. + +If your installation does not have `fcc-update` yet, run the [installer](#install) once to add it. ### Muse Code on native Windows diff --git a/pyproject.toml b/pyproject.toml index 503eac4e6d..823cd38e0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "free-claude-code" -version = "6.2.54" +version = "6.2.57" description = "Local proxy connecting coding agents to OpenAI-compatible AI providers" readme = "README.md" requires-python = ">=3.14.0" @@ -66,6 +66,10 @@ voice_local = [ [tool.hatch.build.targets.wheel] packages = ["src/free_claude_code"] +[tool.hatch.build.targets.wheel.shared-scripts] +"scripts/update/fcc-update" = "fcc-update" +"scripts/update/fcc-update.cmd" = "fcc-update.cmd" + [tool.uv] required-version = ">=0.12.13" diff --git a/scripts/install.ps1 b/scripts/install.ps1 index b65b22574e..1d536ed23e 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -59,6 +59,7 @@ $FccCommands = @( "fcc-grok", "fcc-muse", "fcc-aider", + "fcc-update", "fcc-init", "free-claude-code" ) @@ -1506,7 +1507,7 @@ function Configure-AndConfirmFreeClaudeCode { [IO.Path]::AltDirectorySeparatorChar ) $installedCommands = @{} - foreach ($commandName in @("fcc-desktop", "fcc-server", "fcc-claude", "fcc-codex", "fcc-pi", "fcc-opencode", "fcc-cline", "fcc-hermes", "fcc-dsh", "fcc-grok", "fcc-muse", "fcc-aider")) { + foreach ($commandName in @("fcc-desktop", "fcc-server", "fcc-claude", "fcc-codex", "fcc-pi", "fcc-opencode", "fcc-cline", "fcc-hermes", "fcc-dsh", "fcc-grok", "fcc-muse", "fcc-aider", "fcc-update.cmd")) { $command = Get-ApplicationCommand $commandName if (-not $command) { throw "Free Claude Code installation did not create '$commandName'." diff --git a/scripts/install.sh b/scripts/install.sh index 58be25685b..1feb508787 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -19,7 +19,7 @@ UV_INSTALL_URL="https://astral.sh/uv/install.sh" FCC_MACOS_BUNDLE_ID="io.github.alishahryar1.free-claude-code" FCC_MACOS_OWNER_FILE=".free-claude-code-owner" # Include retired entry points so updates reject older FCC processes before replacement. -FCC_COMMANDS="fcc-desktop fcc-server fcc-claude fcc-codex fcc-pi fcc-opencode fcc-cline fcc-hermes fcc-dsh fcc-grok fcc-muse fcc-aider fcc-init free-claude-code" +FCC_COMMANDS="fcc-desktop fcc-server fcc-claude fcc-codex fcc-pi fcc-opencode fcc-cline fcc-hermes fcc-dsh fcc-grok fcc-muse fcc-aider fcc-update fcc-init free-claude-code" dry_run=0 voice_nim=0 @@ -1353,7 +1353,7 @@ configure_and_verify_free_claude_code() { add_uv_tool_bin_directory - for command_name in fcc-desktop fcc-server fcc-claude fcc-codex fcc-pi fcc-opencode fcc-cline fcc-hermes fcc-dsh fcc-grok fcc-muse fcc-aider; do + for command_name in fcc-desktop fcc-server fcc-claude fcc-codex fcc-pi fcc-opencode fcc-cline fcc-hermes fcc-dsh fcc-grok fcc-muse fcc-aider fcc-update; do [ -x "$tool_bin/$command_name" ] || fail "Free Claude Code installation did not create $tool_bin/$command_name." done diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 672d63f1fb..bd5a9a8ad5 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -24,6 +24,7 @@ $FccCommands = @( "fcc-grok", "fcc-muse", "fcc-aider", + "fcc-update", "fcc-init", "free-claude-code" ) diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 5ff4ec170b..fba40e3880 100644 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -6,7 +6,7 @@ FCC_HOME_DIRNAME=".fcc" FCC_MACOS_BUNDLE_ID="io.github.alishahryar1.free-claude-code" FCC_MACOS_OWNER_FILE=".free-claude-code-owner" # Include retired entry points so older installations are fully stopped and removed. -FCC_COMMANDS="fcc-desktop fcc-server fcc-claude fcc-codex fcc-pi fcc-opencode fcc-cline fcc-hermes fcc-dsh fcc-grok fcc-muse fcc-aider fcc-init free-claude-code" +FCC_COMMANDS="fcc-desktop fcc-server fcc-claude fcc-codex fcc-pi fcc-opencode fcc-cline fcc-hermes fcc-dsh fcc-grok fcc-muse fcc-aider fcc-update fcc-init free-claude-code" dry_run=0 uv_tool_bin="" @@ -191,7 +191,7 @@ verify_fcc_commands_removed() { fi remaining="" - for command_name in $FCC_COMMANDS; do + for command_name in $FCC_COMMANDS fcc-update.cmd; do command_path="$uv_tool_bin/$command_name" if [ -e "$command_path" ] || [ -L "$command_path" ]; then remaining="${remaining} ${command_path}" diff --git a/scripts/update/fcc-update b/scripts/update/fcc-update new file mode 100644 index 0000000000..8fb65aa740 --- /dev/null +++ b/scripts/update/fcc-update @@ -0,0 +1,4 @@ +#!/bin/sh +set -eu +installer=$(curl -fsSL "https://raw.githubusercontent.com/Alishahryar1/free-claude-code/main/scripts/install.sh") +exec sh -c "$installer" sh "$@" diff --git a/scripts/update/fcc-update.cmd b/scripts/update/fcc-update.cmd new file mode 100644 index 0000000000..f2f6c8d527 --- /dev/null +++ b/scripts/update/fcc-update.cmd @@ -0,0 +1,5 @@ +@echo off +( + powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; & ([scriptblock]::Create((Invoke-RestMethod 'https://raw.githubusercontent.com/Alishahryar1/free-claude-code/main/scripts/install.ps1')))" %* + call exit /b %%errorlevel%% +) diff --git a/src/free_claude_code/providers/open_router/client.py b/src/free_claude_code/providers/open_router/client.py index 27666398a7..1372a7985e 100644 --- a/src/free_claude_code/providers/open_router/client.py +++ b/src/free_claude_code/providers/open_router/client.py @@ -1,11 +1,13 @@ """OpenRouter provider implementation.""" import json +from collections.abc import Mapping from free_claude_code.application.model_metadata import ProviderModelInfo from free_claude_code.config.constants import ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS from free_claude_code.core.anthropic import ReasoningReplayMode from free_claude_code.core.diagnostics import extract_upstream_error_detail +from free_claude_code.core.failures import ExecutionFailure, FailureKind from free_claude_code.core.reasoning import ReasoningEffort from free_claude_code.providers.admission import ProviderAdmissionController from free_claude_code.providers.base import ProviderConfig @@ -34,6 +36,35 @@ class OpenRouterChatBehavior(OpenAIChatBehavior): """OpenRouter Chat adaptation without HTTP ownership.""" + def failure_override(self, error: Exception) -> ExecutionFailure | None: + if getattr(error, "status_code", None) != 404: + return None + body = getattr(error, "body", None) + if isinstance(body, Mapping) and "error" in body: + if any(field in body for field in ("code", "message", "metadata")): + return None + body = body["error"] + if not isinstance(body, Mapping): + return None + code = body.get("code") + if not isinstance(code, int) or code != 404: + return None + if not isinstance(body.get("message"), str): + return None + metadata = body.get("metadata") + if ( + not isinstance(metadata, Mapping) + or metadata.get("failed_routing_step") != "Filter by Image Support" + ): + return None + return ExecutionFailure( + FailureKind.INVALID_REQUEST, + 400, + "No OpenRouter endpoint for this request supports image input. " + "Remove the image or choose an image-capable model.", + False, + ) + def reasoning_disable_rejected(self, error: Exception) -> bool: detail = extract_upstream_error_detail(error) if detail.status_code not in {None, 200}: diff --git a/tests/api/test_openrouter_image_errors.py b/tests/api/test_openrouter_image_errors.py new file mode 100644 index 0000000000..b92bd084ce --- /dev/null +++ b/tests/api/test_openrouter_image_errors.py @@ -0,0 +1,81 @@ +import asyncio +from unittest.mock import AsyncMock, patch + +import pytest +from fastapi.testclient import TestClient + +from free_claude_code.providers.open_router import OpenRouterProvider +from tests.api.support import create_test_app +from tests.providers.support import immediate_admission, make_provider_config +from tests.providers.test_open_router import image_routing_error_body, routing_error + + +@pytest.mark.parametrize( + ("wire", "stream"), [("messages", False), ("messages", True), ("responses", True)] +) +@pytest.mark.parametrize("image_rejected", [True, False]) +def test_openrouter_image_error_reaches_client_without_losing_image( + wire, stream, image_rejected +): + provider = OpenRouterProvider( + make_provider_config("test-key", "https://openrouter.ai/api/v1"), + admission=immediate_admission(), + ) + body = ( + image_routing_error_body() + if image_rejected + else {"code": 404, "message": "Model not found"} + ) + create = AsyncMock(side_effect=routing_error(body)) + image_data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl6nGQAAAAASUVORK5CYII=" + url = f"data:image/png;base64,{image_data}" + payload = {"model": "open_router/test-model", "stream": stream} + if wire == "messages": + content = [ + {"type": "text", "text": "Read this image"}, + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": image_data, + }, + }, + ] + payload.update(messages=[{"role": "user", "content": content}], max_tokens=100) + else: + content = [ + {"type": "input_text", "text": "Read this image"}, + {"type": "input_image", "image_url": url}, + ] + payload.update( + input=[{"role": "user", "content": content}], max_output_tokens=100 + ) + try: + with ( + patch( + "free_claude_code.api.routes.resolve_provider", return_value=provider + ), + patch.object(provider._client.chat.completions, "create", create), + TestClient(create_test_app()) as client, + ): + response = client.post(f"/v1/{wire}", json=payload) + assert response.status_code == (400 if image_rejected else 404) + assert response.headers["x-should-retry"] == "false" + error = response.json()["error"] + assert error["type"] == ( + "invalid_request_error" if image_rejected else "api_error" + ) + assert body["message"] in error["message"] + assert response.headers["request-id"] in error["message"] + if image_rejected: + assert "Remove the image" in error["message"] + assert create.await_count == 1 + assert any( + part.get("image_url", {}).get("url") == url + for message in create.await_args_list[0].kwargs["messages"] + if isinstance(message["content"], list) + for part in message["content"] + ) + finally: + asyncio.run(provider.cleanup()) diff --git a/tests/cli/test_update_wrapper.py b/tests/cli/test_update_wrapper.py new file mode 100644 index 0000000000..288de6ac55 --- /dev/null +++ b/tests/cli/test_update_wrapper.py @@ -0,0 +1,168 @@ +"""Native update commands delegate to the installer and survive replacement.""" + +import contextlib +import http.server +import os +import shutil +import subprocess +import sys +import threading +import zipfile +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +INSTALL_URL = "https://raw.githubusercontent.com/Alishahryar1/free-claude-code/main/scripts/install" +SCRIPTS = ("fcc-update", "fcc-update.cmd") + + +@contextlib.contextmanager +def installer_server(body, status): + requests = [] + + class Handler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + requests.append(self.path) + payload = body.encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "text/plain; charset=utf-8") + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + def log_message(self, format, *args): + pass + + server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler) + thread = threading.Thread(target=server.serve_forever) + thread.start() + try: + yield f"http://127.0.0.1:{server.server_port}/install", requests + finally: + server.shutdown() + server.server_close() + thread.join() + + +def fixture_wheel(area, url): + wheel = area / "free_claude_code-1.0.0-py3-none-any.whl" + dist = "free_claude_code-1.0.0" + with zipfile.ZipFile(wheel, "w") as archive: + archive.writestr( + f"{dist}.dist-info/METADATA", + "Metadata-Version: 2.1\nName: free-claude-code\nVersion: 1.0.0\n", + ) + archive.writestr( + f"{dist}.dist-info/WHEEL", + "Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n", + ) + for name in SCRIPTS: + source = (ROOT / "scripts/update" / name).read_text(encoding="utf-8") + archive.writestr( + f"{dist}.data/scripts/{name}", source.replace(INSTALL_URL, url) + ) + archive.writestr( + f"{dist}.dist-info/RECORD", + "".join(f"{name},,\n" for name in archive.namelist()) + + f"{dist}.dist-info/RECORD,,\n", + ) + return wheel + + +@pytest.mark.parametrize("outcome", ["success", "exit", "error", "download-error"]) +def test_installed_update_delegates_and_survives_replacement(tmp_path, outcome): + uv = shutil.which("uv") + if uv is None: + pytest.skip("uv is required for native script lifecycle coverage") + area = tmp_path / "José 测试 & (spaces) ! apostrophe'" + area.mkdir() + env = dict(os.environ) + env.update( + UV_TOOL_DIR=str(area / "tools"), + UV_TOOL_BIN_DIR=str(area / "bin"), + UV_CACHE_DIR=str(area / "cache"), + UV_NO_CONFIG="1", + UV_PYTHON_DOWNLOADS="never", + FCC_TEST_LAUNCHER=str( + area / "bin" / ("fcc-update.cmd" if os.name == "nt" else "fcc-update") + ), + ) + if os.name == "nt": + body = """param([switch] $VoiceLocal, [string] $TorchBackend) +$answer = Read-Host 'Continue' +Write-Output "installer:$VoiceLocal|$TorchBackend|$answer" +[IO.File]::WriteAllText($env:FCC_TEST_LAUNCHER, 'exit /b 99') +""" + arguments = "-VoiceLocal -TorchBackend cu130" + expected = "installer:True|cu130|yes" + body += { + "success": "", + "exit": "exit 23", + "error": "throw 'installer failed'", + "download-error": "", + }[outcome] + else: + body = """read -r answer +printf 'installer:%s|%s|%s|%s\\n' "$1" "$2" "$3" "$answer" +printf 'exit 99\\n' > "$FCC_TEST_LAUNCHER" +""" + arguments = ["--voice-local", "--torch-backend", "cu130"] + expected = "installer:--voice-local|--torch-backend|cu130|yes" + body += { + "success": "", + "exit": "exit 23", + "error": "exit 1", + "download-error": "", + }[outcome] + with installer_server(body, 503 if outcome == "download-error" else 200) as ( + url, + requests, + ): + wheel = fixture_wheel(area, url) + result = subprocess.run( + [ + uv, + "tool", + "install", + "--python", + getattr(sys, "_base_executable", sys.executable), + str(wheel), + ], + env=env, + capture_output=True, + timeout=90, + ) + assert result.returncode == 0, result.stderr + launcher = env["FCC_TEST_LAUNCHER"] + command = ( + f'"{os.environ.get("COMSPEC", "cmd.exe")}" /d /s /c ""{launcher}" {arguments}"' + if os.name == "nt" + else [launcher, *arguments] + ) + result = subprocess.run( + command, + env=env, + cwd=area, + input="yes\n", + capture_output=True, + text=True, + timeout=30, + ) + assert requests == ["/install.ps1" if os.name == "nt" else "/install.sh"] + if outcome == "download-error": + assert result.returncode != 0 + assert "installer:" not in result.stdout + else: + assert expected in result.stdout, result.stdout + result.stderr + assert ( + result.returncode == {"success": 0, "exit": 23, "error": 1}[outcome] + ), result.stderr + result = subprocess.run( + [uv, "tool", "uninstall", "free-claude-code"], + env=env, + capture_output=True, + timeout=30, + ) + assert result.returncode == 0, result.stderr + assert not any((area / "bin" / name).exists() for name in SCRIPTS) diff --git a/tests/providers/test_open_router.py b/tests/providers/test_open_router.py index 0be5ae8366..e6ad157367 100644 --- a/tests/providers/test_open_router.py +++ b/tests/providers/test_open_router.py @@ -4,6 +4,8 @@ from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch +import httpx2 +import openai import pytest from free_claude_code.application.errors import InvalidRequestError @@ -15,8 +17,10 @@ text_content, thinking_content, ) +from free_claude_code.core.failures import FailureKind from free_claude_code.core.history_replay import decode_replay from free_claude_code.core.model_capabilities import ModelInputModality +from free_claude_code.providers.failure_policy import classify_provider_failure from free_claude_code.providers.open_router import OpenRouterProvider from free_claude_code.providers.openai_chat import OpenAIChatProvider from tests.providers.request_factory import make_messages_request @@ -29,6 +33,105 @@ ) +def image_routing_error_body(): + return { + "message": "No endpoints found that support image input", + "code": 404, + "metadata": {"failed_routing_step": "Filter by Image Support"}, + } + + +def routing_error(body, status=404): + return openai.APIStatusError( + "OpenRouter routing failed", + response=httpx2.Response( + status, + request=httpx2.Request( + "POST", "https://openrouter.ai/api/v1/chat/completions" + ), + ), + body=body, + ) + + +@pytest.mark.parametrize("wrapped", [False, True]) +@pytest.mark.parametrize( + "message", ["No endpoints found that support image input", "Image routing rejected"] +) +def test_image_routing_failure_explains_unsupported_input( + open_router_provider, wrapped, message +): + body = {**image_routing_error_body(), "message": message} + error = routing_error({"error": body} if wrapped else body) + failure = classify_provider_failure( + error, + provider_name="OPENROUTER", + read_timeout_s=30, + request_id="req_image", + provider_failure_override=open_router_provider._chat._behavior.failure_override, + ) + assert failure.kind == FailureKind.INVALID_REQUEST + assert failure.status_code == 400 + assert failure.retryable is False + assert "Remove the image" in failure.message + assert "image-capable" in failure.message + assert message in failure.message + assert "HTTP 404" in failure.message + assert "req_image" in failure.message + assert ( + classify_provider_failure( + error, provider_name="OTHER", read_timeout_s=30, request_id=None + ).status_code + == 404 + ) + + +@pytest.mark.parametrize( + "body", + [ + {"message": "Model not found", "code": 404}, + {**image_routing_error_body(), "metadata": {}}, + { + **image_routing_error_body(), + "metadata": {"failed_routing_step": "Filter by Data Policy"}, + }, + {**image_routing_error_body(), "metadata": None}, + {**image_routing_error_body(), "code": "404"}, + {**image_routing_error_body(), "code": 404.0}, + {**image_routing_error_body(), "code": 400}, + {**image_routing_error_body(), "message": None}, + {**image_routing_error_body(), "error": image_routing_error_body()}, + {"error": "malformed"}, + "No endpoints found that support image input", + None, + ], +) +def test_unrelated_routing_errors_keep_shared_policy(open_router_provider, body): + error = routing_error(body) + override = open_router_provider._chat._behavior.failure_override + assert override(error) is None + failure = classify_provider_failure( + error, + provider_name="OPENROUTER", + read_timeout_s=30, + request_id=None, + provider_failure_override=override, + ) + assert failure.status_code == 404 + + +@pytest.mark.parametrize("status", [400, 401, 403, 429, 500, 200]) +def test_image_metadata_does_not_override_other_http_status( + open_router_provider, status +): + assert ( + open_router_provider._chat._behavior.failure_override( + routing_error(image_routing_error_body(), status) + ) + is None + ) + + class AsyncStream(SDKStreamDouble): def __init__(self, chunks): self._chunks = chunks diff --git a/tests/scripts/test_installers.py b/tests/scripts/test_installers.py index 55aefdc79b..66771e8ed6 100644 --- a/tests/scripts/test_installers.py +++ b/tests/scripts/test_installers.py @@ -26,6 +26,7 @@ "fcc-grok", "fcc-muse", "fcc-aider", + "fcc-update", "fcc-init", "free-claude-code", ) @@ -190,6 +191,7 @@ def _posix_uv_command(version: str) -> str: cp "$FAKE_FIXTURES/fcc-command.sh" "$tool_bin/fcc-grok" cp "$FAKE_FIXTURES/fcc-command.sh" "$tool_bin/fcc-muse" cp "$FAKE_FIXTURES/fcc-command.sh" "$tool_bin/fcc-aider" + cp "$FAKE_FIXTURES/fcc-command.sh" "$tool_bin/fcc-update" if [ "$FAIL_STEP" != "fcc-missing" ]; then cp "$FAKE_FIXTURES/fcc-command.sh" "$tool_bin/fcc-codex" fi @@ -2080,6 +2082,7 @@ def _batch_uv(version: str) -> str: copy /y "%FAKE_FIXTURES%\fcc-command.cmd" "%UV_BIN_DIR%\fcc-grok.cmd" >nul copy /y "%FAKE_FIXTURES%\fcc-command.cmd" "%UV_BIN_DIR%\fcc-muse.cmd" >nul copy /y "%FAKE_FIXTURES%\fcc-command.cmd" "%UV_BIN_DIR%\fcc-aider.cmd" >nul +copy /y "%FAKE_FIXTURES%\fcc-command.cmd" "%UV_BIN_DIR%\fcc-update.cmd" >nul if not "%FAIL_STEP%"=="fcc-missing" copy /y "%FAKE_FIXTURES%\fcc-command.cmd" "%UV_BIN_DIR%\fcc-codex.cmd" >nul exit /b 0 :install_aider diff --git a/tests/scripts/test_uninstallers.py b/tests/scripts/test_uninstallers.py index b8a65e7a10..d3bc455b40 100644 --- a/tests/scripts/test_uninstallers.py +++ b/tests/scripts/test_uninstallers.py @@ -19,6 +19,7 @@ "fcc-grok", "fcc-muse", "fcc-aider", + "fcc-update", "fcc-init", "free-claude-code", ) @@ -191,7 +192,7 @@ def posix_uninstall_harness(tmp_path: Path) -> PosixUninstallHarness: echo 'Tool `free-claude-code` is not installed' >&2 exit 2 fi - for name in fcc-desktop fcc-server fcc-claude fcc-codex fcc-pi fcc-opencode fcc-cline fcc-hermes fcc-dsh fcc-grok fcc-muse fcc-aider fcc-init free-claude-code; do + for name in fcc-desktop fcc-server fcc-claude fcc-codex fcc-pi fcc-opencode fcc-cline fcc-hermes fcc-dsh fcc-grok fcc-muse fcc-aider fcc-update fcc-init free-claude-code; do /bin/rm -f "$FAKE_TOOL_BIN/$name" done echo "Uninstalled free-claude-code" diff --git a/uv.lock b/uv.lock index 60b9b381e5..4cecec0075 100644 --- a/uv.lock +++ b/uv.lock @@ -599,7 +599,7 @@ wheels = [ [[package]] name = "free-claude-code" -version = "6.2.54" +version = "6.2.57" source = { editable = "." } dependencies = [ { name = "aiohttp" },