From 3135af27039097444793f916ee2c8457b6002b71 Mon Sep 17 00:00:00 2001 From: Techdoll00 Date: Sun, 2 Aug 2026 19:52:40 +0800 Subject: [PATCH] Add one-click installers (install.sh / install.ps1) with README docs --- .gitattributes | 3 + README.md | 18 +++- README_CN.md | 18 +++- install.ps1 | 200 ++++++++++++++++++++++++++++++++++++ install.sh | 267 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 504 insertions(+), 2 deletions(-) create mode 100644 .gitattributes create mode 100644 install.ps1 create mode 100644 install.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a9898ae --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +install.sh text eol=lf +*.sh text eol=lf +install.ps1 text eol=crlf diff --git a/README.md b/README.md index 6141a44..177d8de 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,23 @@ If the agent you're using isn't Codex, you can also try installing the [visual t ## Usage -This repository doesn't provide a universal one-click installer. The recommended way is to hand the repository link to your Codex agent: +## Quick Install (one command) + +macOS / Linux: + +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/Anionex/codex-vision-proxy/main/install.sh) +``` + +Windows (PowerShell): + +```powershell +irm https://raw.githubusercontent.com/Anionex/codex-vision-proxy/main/install.ps1 | iex +``` + +The installer locates your Codex `config.toml`, backs it up, writes the vision env file, repoints your provider's `base_url` to the local proxy, adds `image` to your model's `input_modalities`, registers the proxy as a background service (LaunchAgent / systemd user service / Windows Startup entry), and runs basic checks. Existing DeepSeek auth and provider settings are preserved. Run with `--non-interactive` and `VISION_API_KEY=...` for unattended installs. + +For manual or non-standard setups, the agent-driven route below still works. The recommended way is to hand the repository link to your Codex agent: > I've already integrated DeepSeek into Codex and it works. Please read this repository's README first, then follow AGENT_INSTALL.md to deploy and verify `view_image` on the current system. diff --git a/README_CN.md b/README_CN.md index ad80051..82441e5 100644 --- a/README_CN.md +++ b/README_CN.md @@ -44,7 +44,23 @@ ## 使用方式 -本仓库不提供通用一键安装器。推荐把仓库链接交给 Codex Agent: +## 一键安装 + +macOS / Linux: + +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/Anionex/codex-vision-proxy/main/install.sh) +``` + +Windows(PowerShell): + +```powershell +irm https://raw.githubusercontent.com/Anionex/codex-vision-proxy/main/install.ps1 | iex +``` + +安装器会自动定位你的 Codex `config.toml` 并备份,写入视觉环境变量,把 provider 的 `base_url` 指向本地代理,为模型追加 `image` 输入模态,注册代理为后台服务(LaunchAgent / systemd 用户服务 / Windows 启动项),并做基础校验。现有 DeepSeek 鉴权与 provider 配置保持不变。无人值守安装可用 `--non-interactive` 配合 `VISION_API_KEY=...`。 + +手工或非常规配置仍可走下面的 Agent 流程。推荐把仓库链接交给 Codex Agent: > 我已经在 Codex 中接入并可正常使用 DeepSeek。请先阅读这个仓库的 README,再按照 AGENT_INSTALL.md 根据当前系统部署并验证 `view_image`。 diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..79361f4 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,200 @@ +# One-click installer for codex-vision-proxy (Windows) +# +# Automates the steps in AGENT_INSTALL.md on Windows: +# locate/backup Codex config -> prepare install dir + vision env -> start proxy +# -> repoint base_url -> catalog modality -> Startup entry -> verify +# +# Usage: +# powershell -ExecutionPolicy Bypass -File install.ps1 [-Port 19100] [-NonInteractive] [-NoStart] [-NoVerify] +# +# Env overrides: VISION_API_KEY VISION_BASE_URL VISION_MODEL LANG CODEX_HOME INSTALL_DIR ENV_FILE + +param( + [int]$Port = 19100, + [switch]$NonInteractive, + [switch]$NoStart, + [switch]$NoVerify +) + +$ErrorActionPreference = "Stop" + +function Log($m) { Write-Host "[install] $m" -ForegroundColor Cyan } +function Warn($m) { Write-Host "[warn] $m" -ForegroundColor Yellow } +function Die($m) { Write-Host "[error] $m" -ForegroundColor Red; exit 1 } + +# ---------- 0. prerequisites ---------- +$py = $null +foreach ($cand in @("py -3", "python3", "python")) { + $cmd = ($cand -split " ")[0] + if (Get-Command $cmd -ErrorAction SilentlyContinue) { $py = $cand; break } +} +if (-not $py) { Die "Python 3.11+ not found (py launcher or python on PATH required)" } + +& $py -c "import sys, tomllib; assert sys.version_info >= (3, 11)" 2>$null +if ($LASTEXITCODE -ne 0) { Die "Python 3.11+ required (tomllib); run: py -3 --version" } + +$RepoDir = Split-Path -Parent $MyInvocation.MyCommand.Path +if (-not (Test-Path "$RepoDir\codex-vision-proxy.py")) { Die "codex-vision-proxy.py not found next to install.ps1" } + +# ---------- 1. locate Codex config ---------- +$CodexHome = if ($env:CODEX_HOME) { $env:CODEX_HOME } else { Join-Path $env:USERPROFILE ".codex" } +$Config = Join-Path $CodexHome "config.toml" +if (-not (Test-Path $Config)) { Die "Codex config not found at $Config (set CODEX_HOME if custom)" } + +# ---------- parse config via embedded python ---------- +$PyParse = @' +import sys, tomllib, pathlib, json +config_path, codex_home = sys.argv[1], sys.argv[2] +with open(config_path, "rb") as f: + cfg = tomllib.load(f) +provider = cfg.get("model_provider") or "" +model = cfg.get("model") or "" +upstream = "" +if provider: + p = (cfg.get("model_providers") or {}).get(provider, {}) + upstream = p.get("base_url") or "" +catalog = cfg.get("model_catalog_json") or "" +if catalog and not pathlib.Path(catalog).is_absolute(): + catalog = str(pathlib.Path(codex_home) / catalog) +print(json.dumps({"provider": provider, "model": model, "upstream": upstream, "catalog": catalog})) +'@ +$Parsed = & $py -c $PyParse $Config $CodexHome | ConvertFrom-Json +if ($LASTEXITCODE -ne 0) { Die "Could not parse $Config" } +if (-not $Parsed.provider -or -not $Parsed.model) { Die "Could not read model_provider/model from $Config" } +if (-not $Parsed.upstream) { Die "Could not read base_url for provider '$($Parsed.provider)'" } +if ($Parsed.upstream -like "*127.0.0.1:$Port*" -or $Parsed.upstream -like "*localhost:$Port*") { + Die "Codex already points at $($Parsed.upstream) — cannot discover the real upstream. Restore from a backup first." +} +Log "Provider: $($Parsed.provider) | model: $($Parsed.model) | upstream: $($Parsed.upstream)" + +# ---------- backups ---------- +$Ts = Get-Date -Format "yyyyMMdd-HHmmss" +Copy-Item $Config "$Config.vision-proxy.bak.$Ts" +Log "Backed up $Config -> $Config.vision-proxy.bak.$Ts" +if ($Parsed.catalog -and (Test-Path $Parsed.catalog)) { + Copy-Item $Parsed.catalog "$($Parsed.catalog).vision-proxy.bak.$Ts" + Log "Backed up catalog -> $($Parsed.catalog).vision-proxy.bak.$Ts" +} + +# ---------- 2. install dir + vision env ---------- +$InstallDir = if ($env:INSTALL_DIR) { $env:INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA "codex-vision-proxy" } +$EnvFile = if ($env:ENV_FILE) { $env:ENV_FILE } else { Join-Path $InstallDir "env" } +New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null +Copy-Item "$RepoDir\codex-vision-proxy.py" $InstallDir -Force +Copy-Item "$RepoDir\vision_client.py" $InstallDir -Force +if (-not (Test-Path $EnvFile)) { Copy-Item "$RepoDir\.env.example" $EnvFile } + +function Get-EnvVal($name) { + $line = Select-String -Path $EnvFile -Pattern "^$name=" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($line) { return ($line.Line -split "=", 2)[1].Trim('"') } + return "" +} + +$VisionKey = if ($env:VISION_API_KEY) { $env:VISION_API_KEY } else { Get-EnvVal "VISION_API_KEY" } +$VisionBase = if ($env:VISION_BASE_URL) { $env:VISION_BASE_URL } else { Get-EnvVal "VISION_BASE_URL" } +if (-not $VisionBase) { $VisionBase = "https://api.inferera.com/v1" } +$VisionModel = if ($env:VISION_MODEL) { $env:VISION_MODEL } else { Get-EnvVal "VISION_MODEL" } +if (-not $VisionModel) { $VisionModel = "gemini-3.6-flash" } +$LangOut = if ($env:LANG) { $env:LANG } else { Get-EnvVal "LANG" } +if (-not $LangOut) { $LangOut = "zh" } + +if (-not $VisionKey) { + if ($NonInteractive) { Die "VISION_API_KEY is not set (use -NonInteractive with VISION_API_KEY env)" } + $VisionKey = Read-Host "Vision API key (for $VisionBase)" + if (-not $VisionKey) { Die "API key required" } +} +@("VISION_API_KEY=$VisionKey", "VISION_BASE_URL=$VisionBase", "VISION_MODEL=$VisionModel", "LANG=$LangOut") | Set-Content $EnvFile +Log "Vision config written to $EnvFile" + +# ---------- 3. start proxy ---------- +$PidFile = Join-Path $InstallDir "proxy.pid" +if ($NoStart) { + Log "Skipping proxy start (-NoStart)" +} else { + $args = @("$InstallDir\codex-vision-proxy.py", "--port", "$Port", "--upstream", $Parsed.upstream, "--env-file", $EnvFile, "--log", "$InstallDir\proxy.log") + $p = Start-Process -FilePath ($py -split " ")[0] -ArgumentList $args -WindowStyle Hidden -PassThru + $p.Id | Set-Content $PidFile + Start-Sleep -Milliseconds 800 + if ($p.HasExited) { + Warn "Proxy exited immediately — see $InstallDir\proxy.log (continuing with config changes anyway)" + } else { + Log "Proxy started (pid $($p.Id), port $Port)" + } +} + +# ---------- 4. repoint Codex base_url ---------- +$PyPatch = @' +import re, sys +path, provider, port = sys.argv[1], sys.argv[2], int(sys.argv[3]) +text = open(path, encoding="utf-8").read() +pat = re.compile(r"(\[model_providers\." + re.escape(provider) + r"\][^\[]*?base_url\s*=\s*\")[^\"]*(\")", re.S) +new, n = pat.subn(r"\1http://127.0.0.1:%d\2" % port, text, count=1) +if n != 1: + sys.exit("Could not locate base_url inside [model_providers.%s]" % provider) +open(path, "w", encoding="utf-8").write(new) +print("base_url -> http://127.0.0.1:%d for provider '%s'" % (port, provider)) +'@ +& $py -c $PyPatch $Config $Parsed.provider $Port +if ($LASTEXITCODE -ne 0) { Die "Failed to patch config.toml" } + +# ---------- 5. catalog: add "image" modality ---------- +if ($Parsed.catalog -and (Test-Path $Parsed.catalog)) { + $PyCatalog = @' +import json, sys +path, model = sys.argv[1], sys.argv[2] +data = json.load(open(path, encoding="utf-8")) +models = data.get("models", data) if isinstance(data, dict) else data +entries = models if isinstance(models, list) else models.get("models", []) +target = None +for e in entries: + if isinstance(e, dict) and (e.get("slug") == model or e.get("name") == model or e.get("id") == model): + target = e + break +if target is None: + sys.exit("catalog entry for model '%s' not found (skipped)" % model) +mods = target.get("input_modalities") +if mods == ["text"]: + target["input_modalities"] = ["text", "image"] + json.dump(data, open(path, "w", encoding="utf-8"), ensure_ascii=False, indent=2) + print("catalog: input_modalities -> [text, image] for '%s'" % model) +elif isinstance(mods, list) and "image" in mods: + print("catalog: already supports image (no change)") +else: + print("catalog: input_modalities = %s (left untouched)" % (mods or "unset")) +'@ + & $py -c $PyCatalog $Parsed.catalog $Parsed.model +} else { + Warn "No model catalog file found — skipping modality edit. If Codex rejects view_image, see AGENT_INSTALL.md troubleshooting." +} + +# ---------- 6. Startup entry ---------- +if (-not $NoStart) { + $Startup = [Environment]::GetFolderPath("Startup") + $CmdPath = Join-Path $Startup "codex-vision-proxy.cmd" + $cmd = "@echo off`r`nstart `"Codex Vision Proxy`" /min $py `"$InstallDir\codex-vision-proxy.py`" --port $Port --upstream `"$($Parsed.upstream)`" --env-file `"$EnvFile`" --log `"$InstallDir\proxy.log`"" + Set-Content -Path $CmdPath -Value $cmd -Encoding ASCII + Log "Startup entry created: $CmdPath" + & $CmdPath + Start-Sleep -Milliseconds 800 +} + +# ---------- 7. verify ---------- +if ($NoVerify) { + Log "Skipping verification (-NoVerify)" +} else { + & $py -m py_compile "$InstallDir\codex-vision-proxy.py" "$InstallDir\vision_client.py" + if ($LASTEXITCODE -eq 0) { Log "py_compile OK" } + $tcp = New-Object System.Net.Sockets.TcpClient + try { + $tcp.Connect("127.0.0.1", $Port) + Log "Port $Port is listening" + } catch { + Warn "Port $Port not listening yet — check $InstallDir\proxy.log" + } finally { + $tcp.Close() + } +} + +Log "Done. Fully restart Codex, then ask your DeepSeek model to view_image a local image." +Log "Backups: $Config.vision-proxy.bak.$Ts" +if ($Parsed.catalog -and (Test-Path $Parsed.catalog)) { Log "Catalog backup: $($Parsed.catalog).vision-proxy.bak.$Ts" } diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..9182a7e --- /dev/null +++ b/install.sh @@ -0,0 +1,267 @@ +#!/usr/bin/env bash +# One-click installer for codex-vision-proxy (macOS / Linux) +# +# Automates the steps in AGENT_INSTALL.md: +# 1. locate & back up Codex config 2. prepare install dir + vision env +# 3. start the proxy 4. repoint Codex base_url +# 5. add "image" to the model catalog 6. background service +# 7. verify +# +# Usage: +# ./install.sh [--port 19100] [--non-interactive] [--no-start] [--no-verify] +# [--codex-header-compat] [--inject-reasoning-summary] +# +# Env overrides (also used by --non-interactive): +# VISION_API_KEY VISION_BASE_URL VISION_MODEL LANG CODEX_HOME INSTALL_DIR ENV_FILE +set -euo pipefail + +PORT=19100 +NON_INTERACTIVE=0 +START=1 +VERIFY=1 +EXTRA_ARGS=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --port) PORT="$2"; shift 2 ;; + --non-interactive) NON_INTERACTIVE=1; shift ;; + --no-start) START=0; shift ;; + --no-verify) VERIFY=0; shift ;; + --codex-header-compat) EXTRA_ARGS+=(--codex-header-compat); shift ;; + --inject-reasoning-summary) EXTRA_ARGS+=(--inject-reasoning-summary); shift ;; + *) echo "Unknown option: $1" >&2; exit 2 ;; + esac +done + +log() { printf '\033[1;34m[install]\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; } +die() { printf '\033[1;31m[error]\033[0m %s\n' "$*" >&2; exit 1; } + +# ---------- 0. prerequisites ---------- +command -v python3 >/dev/null 2>&1 || die "python3 not found (Python 3.11+ required)" +PY=$(command -v python3) +python3 -c 'import sys, tomllib; assert sys.version_info >= (3, 11)' 2>/dev/null \ + || die "Python 3.11+ required (tomllib); found: $(python3 --version 2>&1)" + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +test -f "$REPO_DIR/codex-vision-proxy.py" || die "codex-vision-proxy.py not found next to install.sh" + +# ---------- 1. locate Codex config ---------- +CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" +CONFIG="$CODEX_HOME/config.toml" +test -f "$CONFIG" || die "Codex config not found at $CONFIG (set CODEX_HOME if custom)" + +# ---------- parse config via python (tomllib) ---------- +{ + read -r PROVIDER + read -r MODEL + read -r UPSTREAM + read -r CATALOG +} < <( + python3 - "$CONFIG" "$CODEX_HOME" <<'PYEOF' +import sys, tomllib, pathlib +config_path, codex_home = sys.argv[1], sys.argv[2] +with open(config_path, "rb") as f: + cfg = tomllib.load(f) +provider = cfg.get("model_provider") or "" +model = cfg.get("model") or "" +upstream = "" +if provider: + p = (cfg.get("model_providers") or {}).get(provider, {}) + upstream = p.get("base_url") or "" +catalog = cfg.get("model_catalog_json") or "" +if catalog and not pathlib.Path(catalog).is_absolute(): + catalog = str(pathlib.Path(codex_home) / catalog) +print(provider) +print(model) +print(upstream) +print(catalog) +PYEOF +) + +[[ -n "$PROVIDER" && -n "$MODEL" ]] || die "Could not read model_provider/model from $CONFIG" +[[ -n "$UPSTREAM" ]] || die "Could not read base_url for provider '$PROVIDER'" +if [[ "$UPSTREAM" == *"127.0.0.1:$PORT"* || "$UPSTREAM" == *"localhost:$PORT"* ]]; then + die "Codex already points at $UPSTREAM — cannot discover the real upstream. Restore from a backup first." +fi + +log "Provider: $PROVIDER | model: $MODEL | upstream: $UPSTREAM" + +# ---------- backups ---------- +TS=$(date +%Y%m%d-%H%M%S) +cp "$CONFIG" "$CONFIG.vision-proxy.bak.$TS" +log "Backed up $CONFIG -> $CONFIG.vision-proxy.bak.$TS" +if [[ -n "$CATALOG" && -f "$CATALOG" ]]; then + cp "$CATALOG" "$CATALOG.vision-proxy.bak.$TS" + log "Backed up catalog -> $CATALOG.vision-proxy.bak.$TS" +fi + +# ---------- 2. install dir + vision env ---------- +INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/share/codex-vision-proxy}" +ENV_FILE="${ENV_FILE:-$HOME/.config/codex-vision-proxy/env}" +mkdir -p "$INSTALL_DIR" "$(dirname "$ENV_FILE")" +cp -f "$REPO_DIR/codex-vision-proxy.py" "$INSTALL_DIR/" +cp -f "$REPO_DIR/vision_client.py" "$INSTALL_DIR/" + +if [[ ! -f "$ENV_FILE" ]]; then + cp "$REPO_DIR/.env.example" "$ENV_FILE" +fi +chmod 600 "$ENV_FILE" + +# read existing values (defaults from .env.example) +env_get() { grep -E "^$1=" "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"' || true; } +VISION_API_KEY="${VISION_API_KEY:-$(env_get VISION_API_KEY)}" +VISION_BASE_URL="${VISION_BASE_URL:-$(env_get VISION_BASE_URL)}" +VISION_BASE_URL="${VISION_BASE_URL:-https://api.inferera.com/v1}" +VISION_MODEL="${VISION_MODEL:-$(env_get VISION_MODEL)}" +VISION_MODEL="${VISION_MODEL:-gemini-3.6-flash}" +LANG_OUT="${LANG:-$(env_get LANG)}" +LANG_OUT="${LANG_OUT:-zh}" + +if [[ -z "$VISION_API_KEY" ]]; then + if [[ "$NON_INTERACTIVE" -eq 1 ]]; then + die "VISION_API_KEY is not set (use --non-interactive with VISION_API_KEY env)" + fi + read -r -p "Vision API key (for $VISION_BASE_URL): " VISION_API_KEY + [[ -n "$VISION_API_KEY" ]] || die "API key required" +fi + +cat > "$ENV_FILE" </dev/null 2>&1 & + echo $! > "$PIDFILE" +} +if [[ "$START" -eq 1 ]]; then + start_proxy + sleep 1 + if ! kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then + warn "Proxy exited immediately — see $INSTALL_DIR/proxy.log (continuing with config changes anyway)" + else + log "Proxy started (pid $(cat "$PIDFILE"), port $PORT)" + fi +else + log "Skipping proxy start (--no-start)" +fi + +# ---------- 4. repoint Codex base_url ---------- +python3 - "$CONFIG" "$PROVIDER" "$PORT" <<'PYEOF' +import re, sys +path, provider, port = sys.argv[1], sys.argv[2], int(sys.argv[3]) +text = open(path, encoding="utf-8").read() +pat = re.compile(r"(\[model_providers\." + re.escape(provider) + r"\][^\[]*?base_url\s*=\s*\")[^\"]*(\")", re.S) +new, n = pat.subn(r"\1http://127.0.0.1:%d\2" % port, text, count=1) +if n != 1: + sys.exit("Could not locate base_url inside [model_providers.%s]" % provider) +open(path, "w", encoding="utf-8").write(new) +print("base_url -> http://127.0.0.1:%d for provider '%s'" % (port, provider)) +PYEOF + +# ---------- 5. catalog: add "image" modality ---------- +if [[ -n "$CATALOG" && -f "$CATALOG" ]]; then + python3 - "$CATALOG" "$MODEL" <<'PYEOF' +import json, sys +path, model = sys.argv[1], sys.argv[2] +data = json.load(open(path, encoding="utf-8")) +models = data.get("models", data) if isinstance(data, dict) else data +entries = models if isinstance(models, list) else models.get("models", []) +target = None +for e in entries: + if isinstance(e, dict) and (e.get("slug") == model or e.get("name") == model or e.get("id") == model): + target = e + break +if target is None: + sys.exit("catalog entry for model '%s' not found (skipped)" % model) +mods = target.get("input_modalities") +if mods == ["text"]: + target["input_modalities"] = ["text", "image"] + json.dump(data, open(path, "w", encoding="utf-8"), ensure_ascii=False, indent=2) + print("catalog: input_modalities -> [text, image] for '%s'" % model) +elif isinstance(mods, list) and "image" in mods: + print("catalog: already supports image (no change)") +else: + print("catalog: input_modalities = %s (left untouched)" % (mods or "unset")) +PYEOF +else + warn "No model catalog file found — skipping modality edit. If Codex rejects view_image, see AGENT_INSTALL.md troubleshooting." +fi + +# ---------- 6. background service ---------- +if [[ "$START" -eq 1 ]]; then + if [[ "$(uname)" == "Darwin" ]]; then + PLIST="$HOME/Library/LaunchAgents/com.codex.vision-proxy.plist" + mkdir -p "$(dirname "$PLIST")" + cat > "$PLIST" < + + + Labelcom.codex.vision-proxy + ProgramArguments + + $PY$INSTALL_DIR/codex-vision-proxy.py + --port$PORT + --upstream$UPSTREAM + --env-file$ENV_FILE + --log$INSTALL_DIR/proxy.log + $(for a in "${EXTRA_ARGS[@]}"; do echo "$a"; done) + + RunAtLoad + KeepAlive + StandardOutPath$INSTALL_DIR/proxy.log + StandardErrorPath$INSTALL_DIR/proxy.log + +EOF + launchctl bootout "gui/$(id -u)/com.codex.vision-proxy" 2>/dev/null || true + launchctl bootstrap "gui/$(id -u)" "$PLIST" + launchctl kickstart -k "gui/$(id -u)/com.codex.vision-proxy" 2>/dev/null || true + log "LaunchAgent installed: $PLIST" + elif [[ -d /run/systemd/system || -d /etc/systemd/system ]]; then + UNIT="$HOME/.config/systemd/user/codex-vision-proxy.service" + mkdir -p "$(dirname "$UNIT")" + cat > "$UNIT" </dev/null 2>&1; then + if nc -z 127.0.0.1 "$PORT" 2>/dev/null; then + log "Port $PORT is listening" + else + warn "Port $PORT not listening yet — check $INSTALL_DIR/proxy.log" + fi + fi +fi + +log "Done. Fully restart Codex, then ask your DeepSeek model to view_image a local image." +log "Backups: $CONFIG.vision-proxy.bak.$TS" +[[ -n "$CATALOG" && -f "$CATALOG" ]] && log "Catalog backup: $CATALOG.vision-proxy.bak.$TS"