Compare commits
32 Commits
143
CHANGELOG.md
143
CHANGELOG.md
@@ -1,5 +1,148 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v3.10.11 (2026-05-26)
|
||||||
|
|
||||||
|
**Hybrid Endpoint Fallback — Redundant Antigravity Endpoints**
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
- Hybrid endpoint fallback: tries `cloudcode-pa.googleapis.com` then `daily-cloudcode-pa.googleapis.com` on 429
|
||||||
|
- `daily-cloudcode-pa.googleapis.com` is the same production endpoint agy-core uses (separate rate limit bucket)
|
||||||
|
- 429 errors now log full response body for debugging
|
||||||
|
- SERVICE_DISABLED (403) still falls through to next endpoint
|
||||||
|
- Rate-limit marking only happens after ALL endpoints fail
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- Fixed 429 on one endpoint immediately failing — now tries fallback before giving up
|
||||||
|
- Restored SERVICE_DISABLED fallthrough (was accidentally removed)
|
||||||
|
|
||||||
|
## v3.10.10 (2026-05-25)
|
||||||
|
|
||||||
|
**Context Normalizer Fix — Compaction Summary Preservation**
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- Fixed normalizer stripping ALL context on resumed sessions after compaction
|
||||||
|
- Normalizer no longer auto-resets when compaction summary is present
|
||||||
|
- Compaction summaries ("Auto-compacted: N earlier turns") are always preserved
|
||||||
|
- Deduplicates consecutive identical `<goal_context>` messages (10→1)
|
||||||
|
- Emergency reset now preserves compaction summaries
|
||||||
|
- Previous behavior: after compaction reduced 1925→185 items, normalizer saw `n_tool_outputs == 0` and stripped to just `system + latest_user`, losing all context — model responded with "I don't have context"
|
||||||
|
|
||||||
|
### hashlib Fix (v3.10.9 hotfix)
|
||||||
|
- `_antigravity_normalize_context` crashed with `NameError: hashlib` on resumed sessions
|
||||||
|
- Replaced SHA256 duplicate detection with string comparison
|
||||||
|
|
||||||
|
## v3.10.9 (2026-05-25)
|
||||||
|
|
||||||
|
**Antigravity Overhaul — Context Normalizer, Claude Thinking Fix, Endpoint Lockdown**
|
||||||
|
|
||||||
|
### Antigravity Endpoint Lockdown
|
||||||
|
- Production-only: `cloudcode-pa.googleapis.com` by default
|
||||||
|
- Sandbox/staging blocked unless `ALLOW_ANTIGRAVITY_STAGING=1`
|
||||||
|
- 403 SERVICE_DISABLED falls through, 429 returns to client
|
||||||
|
|
||||||
|
### AntigravityContextNormalizer
|
||||||
|
- Bounded context — no more 136-item polluted requests for "hi"
|
||||||
|
- Simple message detector, auto-reset polluted context
|
||||||
|
- Duplicate removal, tool output budget, hard char limits
|
||||||
|
|
||||||
|
### Claude Thinking Fix (Antigravity-only)
|
||||||
|
- Fixed 400 error: `maxOutputTokens=64000` when thinking enabled
|
||||||
|
- Snake_case config, VALIDATED toolConfig, proper budgets
|
||||||
|
|
||||||
|
### z.ai / OpenRouter (cobra91 PR #4)
|
||||||
|
- Full OpenClaw attribution headers, OpenRouter caching
|
||||||
|
|
||||||
|
## v3.10.8 (2026-05-25)
|
||||||
|
|
||||||
|
**OAuth & Antigravity Endpoint Fixes**
|
||||||
|
|
||||||
|
### Re-OAuth Buttons Fixed
|
||||||
|
- Linux GUI: `load_oauth_secrets()` was undefined — buttons crashed silently on click
|
||||||
|
- Now loads OAuth secrets inline from `~/.config/codex-launcher/oauth-secrets.json`
|
||||||
|
- Both Linux and Windows Re-OAuth use PKCE + localhost callback (was deprecated OOB paste)
|
||||||
|
|
||||||
|
### Antigravity Staging/Sandbox Blocked by Default
|
||||||
|
- Proxy: production `cloudcode-pa.googleapis.com` tried FIRST, sandbox/daily/autopush as fallback only
|
||||||
|
- Proxy: 403 SERVICE_DISABLED now falls through to next endpoint instead of returning error immediately
|
||||||
|
- Project discovery: validates against production endpoint, not staging-cloudaicompanion.sandbox
|
||||||
|
- Antigravity preset `base_url` changed to production (was `daily-cloudcode-pa.sandbox.googleapis.com`)
|
||||||
|
- `[antigravity-endpoint]` log line shows which endpoints are being tried
|
||||||
|
|
||||||
|
### Other Fixes
|
||||||
|
- GLib.idle_add lambda returning truthy tuple fixed (caused repeated callbacks)
|
||||||
|
- Windows GUI project discovery also uses production endpoint
|
||||||
|
|
||||||
|
## v3.10.7 (2026-05-25)
|
||||||
|
|
||||||
|
**Prompt Enhancer — Fix Lost Context After Compaction**
|
||||||
|
|
||||||
|
### Prompt Enhancer (Per-Provider Toggle)
|
||||||
|
- **Offline mode**: Injects structured XML instructions before every user prompt to keep the model focused, decisive, and context-aware after compaction strips conversation history
|
||||||
|
- **AI-powered mode**: Optionally calls an external LLM (configurable model/URL/key) to rewrite vague prompts into clear, actionable instructions
|
||||||
|
- Prevents the "had to resend and reword" problem in long sessions where compaction summarizes hundreds of turns
|
||||||
|
- **Per-endpoint setting** — enable/disable for each provider independently
|
||||||
|
- Configurable in both Linux and Windows GUI: toggle switch, mode selector, enhancer model, URL, API key fields
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
- **Offline**: Prepends a `<prompt-enhancer>` block with rules like "never ask for clarification, infer from compacted context, execute decisively"
|
||||||
|
- **AI-powered**: Sends the user's prompt + compaction summary to a separate model (e.g. DeepSeek V4 Flash via Freebuff) which rewrites it for clarity, then prepends the offline instructions too
|
||||||
|
- Both modes run after compaction but before the request is sent upstream
|
||||||
|
|
||||||
|
## v3.10.6 (2026-05-25)
|
||||||
|
|
||||||
|
**Freebuff Integration + Codebuff OAuth Fix + Windows Consolidation**
|
||||||
|
|
||||||
|
### Freebuff (Free DeepSeek/Kimi)
|
||||||
|
- **Freebuff integration**: Free DeepSeek/Kimi models via codebuff.com API
|
||||||
|
- Fixed User-Agent to match official SDK: `ai-sdk/openai-compatible/1.0.25/codebuff`
|
||||||
|
- Fixed metadata fields: `freebuff_instance_id` + `client_id` (base36 random) + `cost_mode: "free"`
|
||||||
|
- Fixed session endpoint: POST empty `{}` body (not `{"model": model}`)
|
||||||
|
- GUI preset aliases: "Freebuff (Free DeepSeek/Kimi)", "FreeBuff", "Codebuff (Free DeepSeek/Kimi)" all map to same backend
|
||||||
|
|
||||||
|
### Codebuff Fix
|
||||||
|
- Fixed Codebuff OAuth: use `www.codebuff.com` (bare `codebuff.com` returns 307 redirect)
|
||||||
|
|
||||||
|
### OAuth Secrets & Credentials (All Providers)
|
||||||
|
- **OAuth Secrets dialog now shows ALL providers**: Google (Antigravity + Gemini CLI) AND Freebuff/Codebuff
|
||||||
|
- **Re-OAuth buttons** for each provider: instantly re-authenticate Google or GitHub/Codebuff
|
||||||
|
- Token status indicators (valid/missing) for each Google provider
|
||||||
|
- Shows logged-in email and auth status for Freebuff/Codebuff
|
||||||
|
- Editable auth token and fingerprint fields for Freebuff/Codebuff
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
- Windows GUI files consolidated into `src/` (merged by cobra91 via PR #1 and PR #2)
|
||||||
|
|
||||||
|
### Proxy & GUI Improvements (cobra91 PR #3)
|
||||||
|
- CROF adaptive logic gated to `crof.ai` only — no more log pollution for other providers
|
||||||
|
- Data directory consolidation: all data now in `codex-proxy/` (was split across `codex-desktop/`, `codex-launcher/`, `codex-proxy/`)
|
||||||
|
- Sticky proxy port: persists in `.last-proxy-port`, reused on restart so Codex Desktop keeps connection
|
||||||
|
- Adaptive compact budget raised from 60% to 80% — avoids premature compaction on large-context models (DeepSeek v4 Pro 1M)
|
||||||
|
- Config cleanup fix: stale `proxy-*.json` cleanup moved after `_init_runtime()` to avoid deleting active config
|
||||||
|
- Windows GUI: added Clear Log, Restart Proxy, View Log buttons
|
||||||
|
- **Linux/Windows feature parity**: both GUIs now have identical features
|
||||||
|
- Windows GUI: ported OAuth Secrets all-providers dialog (Google + Freebuff/Codebuff with Re-OAuth buttons, token status)
|
||||||
|
- Windows GUI: added Codebuff/Freebuff OAuth login flow (GitHub browser-based)
|
||||||
|
- Windows GUI: added Sync from Preset button in endpoint editor
|
||||||
|
- Linux GUI: added Clear Log + Restart Proxy buttons (matching Windows)
|
||||||
|
|
||||||
|
## v3.10.5 (2026-05-25)
|
||||||
|
|
||||||
|
**Windows GUI + Context Compaction for Antigravity/Gemini OAuth**
|
||||||
|
|
||||||
|
### Windows Native GUI (tkinter)
|
||||||
|
- **Windows GUI** in `windows/` folder — full tkinter port by cobra91
|
||||||
|
- OAuth Secrets editor, Import JSON, Antigravity model list
|
||||||
|
- Shared backend with Linux (same translate-proxy.py)
|
||||||
|
- See README for Windows installation and usage
|
||||||
|
|
||||||
|
**Context Compaction for Antigravity/Gemini OAuth**
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
- **Prevent `input token count exceeds maximum` errors** during long conversations
|
||||||
|
- Added aggressive compaction policies for Antigravity (`cloudcode-pa`) and Gemini CLI (`googleapis`)
|
||||||
|
- Auto-trims old turns when approaching 60% of model context limit (1M tokens for Gemini, 200K for Claude, 128K for GPT-OSS)
|
||||||
|
- Added REST model IDs to context size map (`gemini-3-flash`, `gemini-3.1-pro-low`, `claude-sonnet-4-6`, etc.)
|
||||||
|
|
||||||
## v3.10.4 (2026-05-25)
|
## v3.10.4 (2026-05-25)
|
||||||
|
|
||||||
**Security: OAuth Secrets Editor + Import JSON**
|
**Security: OAuth Secrets Editor + Import JSON**
|
||||||
|
|||||||
96
README.md
96
README.md
@@ -9,13 +9,28 @@
|
|||||||
<a href="https://z.ai/subscribe?ic=ROK78RJKNW">z.ai/subscribe</a>
|
<a href="https://z.ai/subscribe?ic=ROK78RJKNW">z.ai/subscribe</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
---
|
---
|
||||||
|
If you want fork it, use the Github copy, here it is:
|
||||||
|
<a href="https://github.com/roman-ryzenadvanced/Codex-Launcher-Any-AI-Provider">Codex-Any-AI-Provider on Github (Official)</a>
|
||||||
|
---
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<h1 align="center">Codex Launcher — Any AI Provider</h1>
|
<h1 align="center">Codex Launcher — Any AI Provider</h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>Run OpenAI Codex CLI & Desktop with <em>any</em> AI provider.</strong><br/>
|
<strong>Run OpenAI Codex CLI & Desktop with <em>any</em> AI provider.</strong><br/>
|
||||||
Google Antigravity • Gemini CLI • OpenCode • Z.AI • Anthropic • Command Code • Codebuff • OpenRouter • Crof.ai • NVIDIA NIM • OpenAdapter • Kilo.ai • DeepSeek • and more
|
Google Antigravity • Gemini CLI • OpenCode • Z.AI • Anthropic • Command Code • Freebuff • OpenRouter • Crof.ai • NVIDIA NIM • OpenAdapter • Kilo.ai • DeepSeek • and more
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<sub>
|
||||||
|
Windows version by <a href="https://github.com/cobra91">cobra91</a> •
|
||||||
|
Original Linux development by <a href="https://github.com/roman-ryzenadvanced">roman-ryzenadvanced</a>
|
||||||
|
</sub>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -546,6 +561,16 @@ Codex Launcher includes special handling for Gemini 3 / Antigravity OAuth:
|
|||||||
- **User instruction enforcement**: The latest user message is guaranteed to be the
|
- **User instruction enforcement**: The latest user message is guaranteed to be the
|
||||||
final content turn sent to Gemini, even after compaction.
|
final content turn sent to Gemini, even after compaction.
|
||||||
- **Smart compaction**: Old tool outputs capped at 3000 chars, recent 6 at 20000 chars.
|
- **Smart compaction**: Old tool outputs capped at 3000 chars, recent 6 at 20000 chars.
|
||||||
|
- **Context compaction**: Aggressive auto-trimming when approaching 60% of model context
|
||||||
|
limit (1M tokens Gemini, 200K Claude, 128K GPT-OSS). Prevents token limit errors.
|
||||||
|
- **Model ID mapping**: Display names (e.g. `Gemini 3.5 Flash (High)`) mapped to REST API
|
||||||
|
slugs (e.g. `gemini-3-flash`). See `docs/ANTIGRAVITY.md` for details.
|
||||||
|
|
||||||
|
### OAuth Secrets
|
||||||
|
|
||||||
|
Google OAuth credentials are stored locally in `~/.config/codex-launcher/oauth-secrets.json`
|
||||||
|
and never committed to the repository. Use the **OAuth Secrets** button in the launcher
|
||||||
|
header to edit or import `client_secret_*.json` files from Google Cloud Console.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -605,7 +630,7 @@ curl http://127.0.0.1:PORT/v1/accounts
|
|||||||
| OpenCode Zen | OpenAI-compat | `https://opencode.ai/zen/v1` |
|
| OpenCode Zen | OpenAI-compat | `https://opencode.ai/zen/v1` |
|
||||||
| OpenCode Go | OpenAI-compat | `https://opencode.ai/zen/go/v1` |
|
| OpenCode Go | OpenAI-compat | `https://opencode.ai/zen/go/v1` |
|
||||||
| Command Code | Command Code | `https://api.commandcode.ai` |
|
| Command Code | Command Code | `https://api.commandcode.ai` |
|
||||||
| **Codebuff** | **Codebuff** | `https://codebuff.com` *(free DeepSeek/Kimi — OAuth login built-in)* |
|
| **Codebuff / Freebuff** | **Codebuff** | `https://www.codebuff.com` *(free DeepSeek/Kimi — OAuth login built-in)* |
|
||||||
| Crof.ai | OpenAI-compat | `https://crof.ai/v1` |
|
| Crof.ai | OpenAI-compat | `https://crof.ai/v1` |
|
||||||
| OpenAdapter | OpenAI-compat | `https://api.openadapter.in/v1` |
|
| OpenAdapter | OpenAI-compat | `https://api.openadapter.in/v1` |
|
||||||
| Z.ai Coding | OpenAI-compat | `https://api.z.ai/api/coding/paas/v4` |
|
| Z.ai Coding | OpenAI-compat | `https://api.z.ai/api/coding/paas/v4` |
|
||||||
@@ -618,14 +643,14 @@ curl http://127.0.0.1:PORT/v1/accounts
|
|||||||
| Google Antigravity (OAuth) | Antigravity OAuth | `daily-cloudcode-pa.sandbox.googleapis.com` |
|
| Google Antigravity (OAuth) | Antigravity OAuth | `daily-cloudcode-pa.sandbox.googleapis.com` |
|
||||||
| Custom | Any | User-defined |
|
| Custom | Any | User-defined |
|
||||||
|
|
||||||
### Free Models (via Codebuff)
|
### Free Models (via Codebuff/Freebuff)
|
||||||
Codebuff provides free access to these models — no API key needed:
|
Codebuff/Freebuff provides free access to these models — no API key needed:
|
||||||
- **DeepSeek V4 Pro** — Smartest model
|
- **DeepSeek V4 Pro** — Smartest model
|
||||||
- **DeepSeek V4 Flash** — Most efficient
|
- **DeepSeek V4 Flash** — Most efficient
|
||||||
- **Kimi K2.6** — Balanced
|
- **Kimi K2.6** — Balanced
|
||||||
- **MiniMax M2.7** — Fastest
|
- **MiniMax M2.7** — Fastest
|
||||||
|
|
||||||
*Requires: `codebuff login` via GUI OAuth button, or `npm install -g codebuff && codebuff login` (GitHub OAuth)*
|
*Requires: `freebuff login` via GUI OAuth button, or `npm install -g freebuff && freebuff login` (GitHub OAuth)*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -762,15 +787,70 @@ codex --profile my-profile -c model=my-model
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Windows Version
|
||||||
|
|
||||||
|
A native **Windows GUI** (tkinter) is available in the `src/` folder alongside the Linux version. Both GUIs have **full feature parity**.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<sub>
|
||||||
|
Windows version by <a href="https://github.com/cobra91">cobra91</a> •
|
||||||
|
Original Linux development by <a href="https://github.com/roman-ryzenadvanced">roman-ryzenadvanced</a>
|
||||||
|
</sub>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `src/codex-launcher-gui.py` | tkinter GUI (Windows) — manage endpoints, launch Codex CLI/Desktop |
|
||||||
|
| `src/codex-launcher-gui` | GTK GUI (Linux) — same features, native GTK look |
|
||||||
|
| `src/codex_launcher_lib.py` | Shared library — proxy lifecycle, config, OAuth, diagnostics |
|
||||||
|
| `src/translate-proxy.py` | Proxy — translates Responses API for any provider |
|
||||||
|
|
||||||
|
### How to Run (Windows)
|
||||||
|
|
||||||
|
Python ≥ 3.8 with tkinter is required (comes with the official Python installer).
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# From repo root
|
||||||
|
cd src
|
||||||
|
python codex-launcher-gui.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The GUI will:
|
||||||
|
1. Auto-create default endpoints on first run
|
||||||
|
2. Show a toolbar with Endpoints, OAuth Secrets, AI Monitor, and more
|
||||||
|
3. Launch Codex CLI/Desktop with your chosen provider
|
||||||
|
|
||||||
|
### OAuth Credentials
|
||||||
|
|
||||||
|
Google OAuth (Antigravity / Gemini CLI) requires a `client_secret_*.json` from [Google Cloud Console](https://console.cloud.google.com/apis/credentials). Use the **OAuth Secrets** button in the GUI to import it — credentials are stored locally in `~/.config/codex-launcher/oauth-secrets.json`, never in the repo.
|
||||||
|
|
||||||
|
The **OAuth Secrets** dialog shows all providers (Google + Freebuff/Codebuff) with **Re-OAuth buttons** to instantly re-authenticate any provider.
|
||||||
|
|
||||||
|
### Feature Parity
|
||||||
|
|
||||||
|
Both Linux (GTK) and Windows (tkinter) GUIs have identical features:
|
||||||
|
- All provider presets, endpoint management, BGP routing
|
||||||
|
- OAuth Secrets with all providers + Re-OAuth buttons
|
||||||
|
- AI Monitor, Usage Dashboard, Request History, Benchmark
|
||||||
|
- Clear Log, Restart Proxy, View Log
|
||||||
|
- Doctor, Diagnostic Agent, Profile Backup/Import
|
||||||
|
- Antigravity model mapping, context compaction (80% budget)
|
||||||
|
- Multi-account rotation, rate limit handling
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Python ≥ 3.8
|
- Python ≥ 3.8
|
||||||
- python3-gi (`sudo apt install python3-gi`)
|
- python3-gi (`sudo apt install python3-gi`) — Linux only
|
||||||
|
- tkinter (`python3-tk`) — Windows / Linux GUI
|
||||||
- Codex CLI ≥ 2.0
|
- Codex CLI ≥ 2.0
|
||||||
- Codex Desktop (optional, for Desktop mode)
|
- Codex Desktop (optional, for Desktop mode)
|
||||||
- bash, curl, lsof
|
- bash, curl, lsof — Linux only
|
||||||
|
|
||||||
**No pip dependencies.** Zero. Pure stdlib + system GTK.
|
**No pip dependencies.** Zero. Pure stdlib.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
5726
codex-launcher-gui
Executable file
5726
codex-launcher-gui
Executable file
File diff suppressed because it is too large
Load Diff
3292
codex-launcher-gui.py
Normal file
3292
codex-launcher-gui.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
codex-launcher_3.10.10_all.deb
Normal file
BIN
codex-launcher_3.10.10_all.deb
Normal file
Binary file not shown.
BIN
codex-launcher_3.10.11_all.deb
Normal file
BIN
codex-launcher_3.10.11_all.deb
Normal file
Binary file not shown.
Binary file not shown.
BIN
codex-launcher_3.10.9_all.deb
Normal file
BIN
codex-launcher_3.10.9_all.deb
Normal file
Binary file not shown.
2132
codex_launcher_lib.py
Normal file
2132
codex_launcher_lib.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,11 +3,11 @@ set -e
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
if [ -f "$SCRIPT_DIR/codex-launcher_3.10.5_all.deb" ]; then
|
if [ -f "$SCRIPT_DIR/codex-launcher_3.10.11_all.deb" ]; then
|
||||||
echo "Installing codex-launcher_3.10.5_all.deb ..."
|
echo "Installing codex-launcher_3.10.11_all.deb ..."
|
||||||
sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.10.5_all.deb"
|
sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.10.11_all.deb"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Installed v3.10.5 via .deb package."
|
echo "Installed v3.10.11 via .deb package."
|
||||||
echo " translate-proxy.py -> /usr/bin/translate-proxy.py"
|
echo " translate-proxy.py -> /usr/bin/translate-proxy.py"
|
||||||
echo " codex-launcher-gui -> /usr/bin/codex-launcher-gui"
|
echo " codex-launcher-gui -> /usr/bin/codex-launcher-gui"
|
||||||
echo " cleanup-codex-stale -> /usr/bin/cleanup-codex-stale.sh"
|
echo " cleanup-codex-stale -> /usr/bin/cleanup-codex-stale.sh"
|
||||||
|
|||||||
101
src/cleanup-codex-stale.py
Normal file
101
src/cleanup-codex-stale.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Cleanup stale Codex Launcher processes and artifacts — cross-platform.
|
||||||
|
|
||||||
|
Kills registered process groups and removes stale PID/socket files left
|
||||||
|
by previous Codex Launcher sessions.
|
||||||
|
|
||||||
|
Windows: uses taskkill /F /T /PID
|
||||||
|
Linux: uses kill -TERM -- -PGID
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json, os, sys, subprocess, time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
IS_WINDOWS = sys.platform == "win32"
|
||||||
|
|
||||||
|
if IS_WINDOWS:
|
||||||
|
_local = os.environ.get("LOCALAPPDATA", str(Path.home() / "AppData" / "Local"))
|
||||||
|
PID_REGISTRY = Path(_local) / "codex-proxy" / "pids.json"
|
||||||
|
CODEX_DIR = Path.home() / ".codex"
|
||||||
|
_local_share = Path(_local)
|
||||||
|
_cache = Path(_local)
|
||||||
|
else:
|
||||||
|
PID_REGISTRY = Path.home() / ".cache" / "codex-proxy" / "pids.json"
|
||||||
|
CODEX_DIR = Path.home() / ".codex"
|
||||||
|
_local_share = Path.home() / ".local" / "share"
|
||||||
|
_cache = Path.home() / ".cache"
|
||||||
|
|
||||||
|
|
||||||
|
def kill_group(pid):
|
||||||
|
if IS_WINDOWS:
|
||||||
|
subprocess.run(["taskkill", "/F", "/T", "/PID", str(pid)],
|
||||||
|
capture_output=True, timeout=10)
|
||||||
|
else:
|
||||||
|
import signal
|
||||||
|
try:
|
||||||
|
pgid = os.getpgid(pid)
|
||||||
|
os.killpg(pgid, signal.SIGTERM)
|
||||||
|
time.sleep(0.5)
|
||||||
|
try:
|
||||||
|
os.killpg(pgid, signal.SIGKILL)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("[cleanup] Cleaning up stale Codex Launcher processes...", file=sys.stderr)
|
||||||
|
|
||||||
|
if PID_REGISTRY.exists():
|
||||||
|
try:
|
||||||
|
with open(PID_REGISTRY) as f:
|
||||||
|
registry = json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[cleanup] Failed to read PID registry: {e}", file=sys.stderr)
|
||||||
|
registry = {}
|
||||||
|
|
||||||
|
for kind, info in registry.items():
|
||||||
|
pid = info.get("pid") if isinstance(info, dict) else info
|
||||||
|
if pid and isinstance(pid, int):
|
||||||
|
print(f"[cleanup] Killing {kind} (PID {pid})", file=sys.stderr)
|
||||||
|
kill_group(pid)
|
||||||
|
|
||||||
|
try:
|
||||||
|
PID_REGISTRY.unlink()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
print("[cleanup] No PID registry found — nothing to stop", file=sys.stderr)
|
||||||
|
|
||||||
|
stale_files = []
|
||||||
|
if IS_WINDOWS:
|
||||||
|
stale_files = [
|
||||||
|
_cache / "codex-desktop" / ".codex-desktop-pid",
|
||||||
|
_cache / "codex-desktop" / ".webview-pid",
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
stale_files = [
|
||||||
|
CODEX_DIR / ".launch-action-socket",
|
||||||
|
CODEX_DIR / ".codex-desktop-launch-action",
|
||||||
|
CODEX_DIR / ".codex-desktop-pid",
|
||||||
|
CODEX_DIR / ".webview-pid",
|
||||||
|
_local_share / "codex-desktop" / ".codex-desktop-pid",
|
||||||
|
_local_share / "codex-desktop" / ".webview-pid",
|
||||||
|
_cache / "codex-desktop" / ".codex-desktop-pid",
|
||||||
|
_cache / "codex-desktop" / ".webview-pid",
|
||||||
|
]
|
||||||
|
|
||||||
|
for fp in stale_files:
|
||||||
|
try:
|
||||||
|
if fp.exists():
|
||||||
|
fp.unlink()
|
||||||
|
print(f"[cleanup] Removed {fp}", file=sys.stderr)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("[cleanup] Done", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -393,7 +393,25 @@ PROVIDER_PRESETS = {
|
|||||||
},
|
},
|
||||||
"Codebuff (Free DeepSeek/Kimi)": {
|
"Codebuff (Free DeepSeek/Kimi)": {
|
||||||
"backend_type": "codebuff",
|
"backend_type": "codebuff",
|
||||||
"base_url": "https://codebuff.com",
|
"base_url": "https://www.codebuff.com",
|
||||||
|
"oauth_provider": "codebuff",
|
||||||
|
"models": [
|
||||||
|
"deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash",
|
||||||
|
"moonshotai/kimi-k2.6", "minimax/minimax-m2.7",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"Freebuff (Free DeepSeek/Kimi)": {
|
||||||
|
"backend_type": "codebuff",
|
||||||
|
"base_url": "https://www.codebuff.com",
|
||||||
|
"oauth_provider": "codebuff",
|
||||||
|
"models": [
|
||||||
|
"deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash",
|
||||||
|
"moonshotai/kimi-k2.6", "minimax/minimax-m2.7",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"FreeBuff": {
|
||||||
|
"backend_type": "codebuff",
|
||||||
|
"base_url": "https://www.codebuff.com",
|
||||||
"oauth_provider": "codebuff",
|
"oauth_provider": "codebuff",
|
||||||
"models": [
|
"models": [
|
||||||
"deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash",
|
"deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash",
|
||||||
@@ -1780,7 +1798,7 @@ class LauncherWin(Gtk.Window):
|
|||||||
# header row
|
# header row
|
||||||
hdr = Gtk.Box(spacing=8)
|
hdr = Gtk.Box(spacing=8)
|
||||||
vbox.pack_start(hdr, False, False, 0)
|
vbox.pack_start(hdr, False, False, 0)
|
||||||
lbl = Gtk.Label(label="<b>Codex Launcher v3.10.5</b>")
|
lbl = Gtk.Label(label="<b>Codex Launcher v3.10.7</b>")
|
||||||
lbl.set_use_markup(True)
|
lbl.set_use_markup(True)
|
||||||
hdr.pack_start(lbl, False, False, 0)
|
hdr.pack_start(lbl, False, False, 0)
|
||||||
changelog_btn = Gtk.Button(label="Changelog")
|
changelog_btn = Gtk.Button(label="Changelog")
|
||||||
@@ -1959,6 +1977,13 @@ class LauncherWin(Gtk.Window):
|
|||||||
assist_btn.connect("clicked", lambda b: self._open_assistant())
|
assist_btn.connect("clicked", lambda b: self._open_assistant())
|
||||||
assist_btn.set_tooltip_text("Open AI coding assistant with streaming, tools, and session management")
|
assist_btn.set_tooltip_text("Open AI coding assistant with streaming, tools, and session management")
|
||||||
bb.pack_start(assist_btn, False, False, 0)
|
bb.pack_start(assist_btn, False, False, 0)
|
||||||
|
self._clear_log_btn = Gtk.Button(label="Clear Log")
|
||||||
|
self._clear_log_btn.connect("clicked", lambda b: self._buf.set_text(""))
|
||||||
|
bb.pack_start(self._clear_log_btn, False, False, 0)
|
||||||
|
self._restart_btn = Gtk.Button(label="Restart Proxy")
|
||||||
|
self._restart_btn.connect("clicked", lambda b: self._manual_restart_proxy())
|
||||||
|
self._restart_btn.set_sensitive(False)
|
||||||
|
bb.pack_start(self._restart_btn, False, False, 0)
|
||||||
self._kill_btn = Gtk.Button(label="Kill && Cleanup")
|
self._kill_btn = Gtk.Button(label="Kill && Cleanup")
|
||||||
self._kill_btn.connect("clicked", lambda b: self._kill())
|
self._kill_btn.connect("clicked", lambda b: self._kill())
|
||||||
self._kill_btn.set_sensitive(False)
|
self._kill_btn.set_sensitive(False)
|
||||||
@@ -2055,6 +2080,7 @@ class LauncherWin(Gtk.Window):
|
|||||||
self._btn_codex_desktop.set_sensitive(not busy and has_desk)
|
self._btn_codex_desktop.set_sensitive(not busy and has_desk)
|
||||||
self._btn_codex_cli.set_sensitive(not busy and has_cli)
|
self._btn_codex_cli.set_sensitive(not busy and has_cli)
|
||||||
self._kill_btn.set_sensitive(busy)
|
self._kill_btn.set_sensitive(busy)
|
||||||
|
self._restart_btn.set_sensitive(busy)
|
||||||
GLib.idle_add(_update)
|
GLib.idle_add(_update)
|
||||||
|
|
||||||
def _rebuild_combo(self):
|
def _rebuild_combo(self):
|
||||||
@@ -2199,6 +2225,22 @@ class LauncherWin(Gtk.Window):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"[AI Monitor] Proxy restart failed: {e}")
|
self.log(f"[AI Monitor] Proxy restart failed: {e}")
|
||||||
|
|
||||||
|
def _manual_restart_proxy(self):
|
||||||
|
self._kill()
|
||||||
|
time.sleep(1)
|
||||||
|
try:
|
||||||
|
ep_name = load_endpoints().get("default")
|
||||||
|
if not ep_name:
|
||||||
|
self.log("No default endpoint set")
|
||||||
|
return
|
||||||
|
for ep in load_endpoints().get("endpoints", []):
|
||||||
|
if ep.get("name") == ep_name:
|
||||||
|
self._start_proxy(ep)
|
||||||
|
self.log("Proxy restarted")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"Proxy restart failed: {e}")
|
||||||
|
|
||||||
def _open_usage(self):
|
def _open_usage(self):
|
||||||
try:
|
try:
|
||||||
self._usage_window = UsageWindow(self)
|
self._usage_window = UsageWindow(self)
|
||||||
@@ -2790,6 +2832,154 @@ class LauncherWin(Gtk.Window):
|
|||||||
_stop_proxy()
|
_stop_proxy()
|
||||||
Gtk.main_quit()
|
Gtk.main_quit()
|
||||||
|
|
||||||
|
def _google_reoauth(self, provider):
|
||||||
|
secrets_path = os.path.expanduser("~/.config/codex-launcher/oauth-secrets.json")
|
||||||
|
try:
|
||||||
|
with open(secrets_path) as f:
|
||||||
|
secrets = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
secrets = {}
|
||||||
|
is_antigravity = provider == "google-antigravity"
|
||||||
|
sec_key = "antigravity" if is_antigravity else "gemini_cli"
|
||||||
|
sec = secrets.get(sec_key, {})
|
||||||
|
client_id = sec.get("client_id", "")
|
||||||
|
client_secret = sec.get("client_secret", "")
|
||||||
|
if not client_id or not client_secret:
|
||||||
|
self._show_error_dialog("Missing OAuth secrets",
|
||||||
|
f"No client_id/client_secret for {sec_key}.\nSet them in OAuth Secrets first.")
|
||||||
|
return
|
||||||
|
token_file = "google-antigravity-oauth-token.json" if is_antigravity else "google-cli-oauth-token.json"
|
||||||
|
token_path = os.path.expanduser(f"~/.cache/codex-proxy/{token_file}")
|
||||||
|
redirect = "urn:ietf:wg:oauth:2.0:oob"
|
||||||
|
auth_url = (f"https://accounts.google.com/o/oauth2/v2/auth?client_id={client_id}"
|
||||||
|
f"&redirect_uri={urllib.parse.quote(redirect)}"
|
||||||
|
f"&response_type=code&scope={urllib.parse.quote('https://www.googleapis.com/auth/cloud-platform')}"
|
||||||
|
f"&access_type=offline&prompt=consent")
|
||||||
|
webbrowser.open(auth_url)
|
||||||
|
code_dlg = Gtk.Dialog(title=f"Re-OAuth: {'Antigravity' if is_antigravity else 'Gemini CLI'}", parent=self, modal=True)
|
||||||
|
code_dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
||||||
|
code_dlg.add_button("Exchange", Gtk.ResponseType.OK)
|
||||||
|
code_dlg.set_default_size(500, 180)
|
||||||
|
ca = code_dlg.get_content_area()
|
||||||
|
ca.set_margin_start(12)
|
||||||
|
ca.set_margin_end(12)
|
||||||
|
ca.set_spacing(6)
|
||||||
|
ca.pack_start(Gtk.Label(label="Browser opened for Google OAuth.\nPaste the authorization code below:", xalign=0), False, False, 0)
|
||||||
|
code_entry = Gtk.Entry()
|
||||||
|
code_entry.set_placeholder_text("4/0AX...")
|
||||||
|
ca.pack_start(code_entry, False, False, 4)
|
||||||
|
ca.show_all()
|
||||||
|
if code_dlg.run() == Gtk.ResponseType.OK:
|
||||||
|
code = code_entry.get_text().strip()
|
||||||
|
if code:
|
||||||
|
try:
|
||||||
|
tok_req = urllib.request.Request("https://oauth2.googleapis.com/token",
|
||||||
|
data=urllib.parse.urlencode({
|
||||||
|
"code": code, "client_id": client_id, "client_secret": client_secret,
|
||||||
|
"redirect_uri": redirect, "grant_type": "authorization_code"
|
||||||
|
}).encode(),
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded"})
|
||||||
|
tok_resp = urllib.request.urlopen(tok_req, timeout=30)
|
||||||
|
tok_data = json.loads(tok_resp.read())
|
||||||
|
tok_data["_updated"] = time.time()
|
||||||
|
os.makedirs(os.path.dirname(token_path), exist_ok=True)
|
||||||
|
with open(token_path, "w") as f:
|
||||||
|
json.dump(tok_data, f, indent=2)
|
||||||
|
self._log(f"[oauth] Refreshed {provider} token → {token_path}")
|
||||||
|
except Exception as e:
|
||||||
|
self._show_error_dialog("Token exchange failed", str(e)[:300])
|
||||||
|
code_dlg.destroy()
|
||||||
|
|
||||||
|
def _codebuff_reoauth(self):
|
||||||
|
self._codebuff_oauth_standalone()
|
||||||
|
|
||||||
|
def _codebuff_oauth_standalone(self):
|
||||||
|
import uuid
|
||||||
|
dlg = Gtk.Dialog(title="Freebuff / Codebuff Login", parent=self, modal=True)
|
||||||
|
dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
||||||
|
dlg.set_default_size(500, 240)
|
||||||
|
area = dlg.get_content_area()
|
||||||
|
area.set_margin_start(16)
|
||||||
|
area.set_margin_end(16)
|
||||||
|
area.set_margin_top(12)
|
||||||
|
area.set_margin_bottom(12)
|
||||||
|
area.set_spacing(8)
|
||||||
|
area.pack_start(Gtk.Label(label="<b>Sign in with GitHub via Codebuff</b>", use_markup=True, xalign=0), False, False, 0)
|
||||||
|
status_lbl = Gtk.Label(label="Requesting login URL…", xalign=0)
|
||||||
|
status_lbl.set_line_wrap(True)
|
||||||
|
status_lbl.set_max_width_chars(60)
|
||||||
|
area.pack_start(status_lbl, False, False, 4)
|
||||||
|
link_lbl = Gtk.Label(xalign=0)
|
||||||
|
link_lbl.set_line_wrap(True)
|
||||||
|
link_lbl.set_max_width_chars(60)
|
||||||
|
area.pack_start(link_lbl, False, False, 4)
|
||||||
|
spinner = Gtk.Spinner()
|
||||||
|
spinner.start()
|
||||||
|
area.pack_start(spinner, False, False, 8)
|
||||||
|
area.show_all()
|
||||||
|
link_lbl.set_visible(False)
|
||||||
|
result = {"success": False, "user": None, "error": None}
|
||||||
|
|
||||||
|
def _thread():
|
||||||
|
try:
|
||||||
|
fp_id = str(uuid.uuid4())
|
||||||
|
body = json.dumps({"fingerprintId": fp_id}).encode()
|
||||||
|
req = urllib.request.Request("https://www.codebuff.com/api/auth/cli/code",
|
||||||
|
data=body, headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.10.7"})
|
||||||
|
resp = urllib.request.urlopen(req, timeout=30)
|
||||||
|
rdata = json.loads(resp.read())
|
||||||
|
login_url = rdata.get("loginUrl", "") or rdata.get("login_url", "")
|
||||||
|
fp_hash = rdata.get("fingerprintHash", "") or rdata.get("fingerprint_hash", "")
|
||||||
|
expires_at = rdata.get("expiresAt", 0) or rdata.get("expires_at", 0)
|
||||||
|
if not login_url:
|
||||||
|
result["error"] = "No login URL"
|
||||||
|
GLib.idle_add(_done)
|
||||||
|
return
|
||||||
|
GLib.idle_add(lambda: (status_lbl.set_text("Open this URL in your browser:"),
|
||||||
|
link_lbl.set_markup(f'<a href="{login_url}">{login_url}</a>'),
|
||||||
|
link_lbl.set_visible(True)))
|
||||||
|
webbrowser.open(login_url)
|
||||||
|
poll = f"https://www.codebuff.com/api/auth/cli/status?fingerprintId={urllib.parse.quote(fp_id)}&fingerprintHash={urllib.parse.quote(fp_hash)}&expiresAt={expires_at}"
|
||||||
|
deadline = time.time() + 300
|
||||||
|
while time.time() < deadline:
|
||||||
|
time.sleep(2)
|
||||||
|
try:
|
||||||
|
pr = urllib.request.Request(poll, headers={"User-Agent": "codex-launcher/3.10.7"})
|
||||||
|
pd = json.loads(urllib.request.urlopen(pr, timeout=10).read())
|
||||||
|
if pd.get("user", {}).get("authToken"):
|
||||||
|
result["success"] = True
|
||||||
|
result["user"] = pd["user"]
|
||||||
|
GLib.idle_add(_done)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
result["error"] = "Timed out"
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)[:200]
|
||||||
|
GLib.idle_add(_done)
|
||||||
|
|
||||||
|
def _done():
|
||||||
|
spinner.stop()
|
||||||
|
if result["success"] and result["user"]:
|
||||||
|
u = result["user"]
|
||||||
|
cp = os.path.expanduser("~/.config/manicode/credentials.json")
|
||||||
|
os.makedirs(os.path.dirname(cp), exist_ok=True)
|
||||||
|
creds = {"default": {"id": u.get("id", ""), "name": u.get("name", ""),
|
||||||
|
"email": u.get("email", ""), "authToken": u.get("authToken", ""),
|
||||||
|
"fingerprintId": u.get("fingerprintId", ""), "fingerprintHash": u.get("fingerprintHash", "")}}
|
||||||
|
with open(cp, "w") as f:
|
||||||
|
json.dump(creds, f, indent=2)
|
||||||
|
os.chmod(cp, 0o600)
|
||||||
|
status_lbl.set_text(f"Logged in as {u.get('email', 'OK')}")
|
||||||
|
link_lbl.set_visible(False)
|
||||||
|
GLib.timeout_add_seconds(2, dlg.destroy)
|
||||||
|
else:
|
||||||
|
status_lbl.set_text(f"Failed: {result.get('error', 'unknown')}")
|
||||||
|
|
||||||
|
threading.Thread(target=_thread, daemon=True).start()
|
||||||
|
dlg.connect("response", lambda d, r: d.destroy())
|
||||||
|
dlg.run()
|
||||||
|
|
||||||
def _edit_oauth_secrets(self):
|
def _edit_oauth_secrets(self):
|
||||||
secrets_path = os.path.expanduser("~/.config/codex-launcher/oauth-secrets.json")
|
secrets_path = os.path.expanduser("~/.config/codex-launcher/oauth-secrets.json")
|
||||||
try:
|
try:
|
||||||
@@ -2799,10 +2989,10 @@ class LauncherWin(Gtk.Window):
|
|||||||
data = {"antigravity": {"client_id": "", "client_secret": ""},
|
data = {"antigravity": {"client_id": "", "client_secret": ""},
|
||||||
"gemini_cli": {"client_id": "", "client_secret": ""}}
|
"gemini_cli": {"client_id": "", "client_secret": ""}}
|
||||||
|
|
||||||
dlg = Gtk.Dialog(title="OAuth 2.0 Client Secrets", parent=self, modal=True)
|
dlg = Gtk.Dialog(title="OAuth Secrets & Credentials", parent=self, modal=True)
|
||||||
dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
||||||
dlg.add_button("Save", Gtk.ResponseType.OK)
|
dlg.add_button("Save", Gtk.ResponseType.OK)
|
||||||
dlg.set_default_size(540, 420)
|
dlg.set_default_size(580, 650)
|
||||||
area = dlg.get_content_area()
|
area = dlg.get_content_area()
|
||||||
area.set_margin_start(16)
|
area.set_margin_start(16)
|
||||||
area.set_margin_end(16)
|
area.set_margin_end(16)
|
||||||
@@ -2810,17 +3000,43 @@ class LauncherWin(Gtk.Window):
|
|||||||
area.set_margin_bottom(12)
|
area.set_margin_bottom(12)
|
||||||
area.set_spacing(6)
|
area.set_spacing(6)
|
||||||
|
|
||||||
area.pack_start(Gtk.Label(label="<b>Google OAuth 2.0 credentials</b>\n<small>Stored locally in ~/.config/codex-launcher/oauth-secrets.json</small>", use_markup=True, xalign=0), False, False, 4)
|
sw = Gtk.ScrolledWindow()
|
||||||
|
sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||||
|
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||||
|
sw.add(vbox)
|
||||||
|
area.pack_start(sw, True, True, 0)
|
||||||
|
|
||||||
|
vbox.pack_start(Gtk.Label(label="<b>Google OAuth 2.0 Client Credentials</b>\n<small>~/.config/codex-launcher/oauth-secrets.json</small>", use_markup=True, xalign=0), False, False, 4)
|
||||||
|
|
||||||
|
google_token_dir = os.path.expanduser("~/.cache/codex-proxy")
|
||||||
fields = {}
|
fields = {}
|
||||||
for section_key, section_label in [("antigravity", "Antigravity (CloudCode)"), ("gemini_cli", "Gemini CLI")]:
|
for section_key, section_label, oauth_prov, token_file in [
|
||||||
|
("antigravity", "Antigravity (CloudCode)", "google-antigravity", "google-antigravity-oauth-token.json"),
|
||||||
|
("gemini_cli", "Gemini CLI", "google-cli", "google-cli-oauth-token.json"),
|
||||||
|
]:
|
||||||
section_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
section_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
||||||
hdr_row = Gtk.Box(spacing=6)
|
hdr_row = Gtk.Box(spacing=6)
|
||||||
hdr_row.pack_start(Gtk.Label(label=f"\n<b>{section_label}</b>", use_markup=True, xalign=0), True, True, 0)
|
hdr_row.pack_start(Gtk.Label(label=f"\n<b>{section_label}</b>", use_markup=True, xalign=0), True, True, 0)
|
||||||
|
reauth_btn = Gtk.Button(label="Re-OAuth")
|
||||||
|
reauth_btn.set_size_request(80, -1)
|
||||||
|
reauth_btn.connect("clicked", lambda b, p=oauth_prov: self._google_reoauth(p))
|
||||||
|
hdr_row.pack_end(reauth_btn, False, False, 0)
|
||||||
import_btn = Gtk.Button(label="Import JSON")
|
import_btn = Gtk.Button(label="Import JSON")
|
||||||
import_btn.set_size_request(100, -1)
|
import_btn.set_size_request(100, -1)
|
||||||
hdr_row.pack_end(import_btn, False, False, 0)
|
hdr_row.pack_end(import_btn, False, False, 0)
|
||||||
section_box.pack_start(hdr_row, False, False, 2)
|
section_box.pack_start(hdr_row, False, False, 2)
|
||||||
|
|
||||||
|
token_path = os.path.join(google_token_dir, token_file)
|
||||||
|
has_token = os.path.exists(token_path)
|
||||||
|
try:
|
||||||
|
with open(token_path) as tf:
|
||||||
|
td = json.load(tf)
|
||||||
|
has_token = bool(td.get("refresh_token") or td.get("access_token"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
tok_status = "Token: <span foreground='#27ae60' weight='bold'>valid</span>" if has_token else "Token: <span foreground='#e67e22' weight='bold'>missing</span>"
|
||||||
|
section_box.pack_start(Gtk.Label(label=tok_status, use_markup=True, xalign=0), False, False, 0)
|
||||||
|
|
||||||
sec = data.get(section_key, {})
|
sec = data.get(section_key, {})
|
||||||
for fk, fl in [("client_id", "Client ID"), ("client_secret", "Client Secret")]:
|
for fk, fl in [("client_id", "Client ID"), ("client_secret", "Client Secret")]:
|
||||||
row = Gtk.Box(spacing=6)
|
row = Gtk.Box(spacing=6)
|
||||||
@@ -2828,7 +3044,7 @@ class LauncherWin(Gtk.Window):
|
|||||||
lbl.set_size_request(100, -1)
|
lbl.set_size_request(100, -1)
|
||||||
entry = Gtk.Entry()
|
entry = Gtk.Entry()
|
||||||
entry.set_text(sec.get(fk, ""))
|
entry.set_text(sec.get(fk, ""))
|
||||||
entry.set_size_request(380, -1)
|
entry.set_size_request(360, -1)
|
||||||
if fk == "client_secret":
|
if fk == "client_secret":
|
||||||
entry.set_visibility(False)
|
entry.set_visibility(False)
|
||||||
entry.set_invisible_char("*")
|
entry.set_invisible_char("*")
|
||||||
@@ -2837,10 +3053,63 @@ class LauncherWin(Gtk.Window):
|
|||||||
section_box.pack_start(row, False, False, 2)
|
section_box.pack_start(row, False, False, 2)
|
||||||
fields[(section_key, fk)] = entry
|
fields[(section_key, fk)] = entry
|
||||||
import_btn.connect("clicked", lambda b, sk=section_key: self._import_oauth_json(fields, sk))
|
import_btn.connect("clicked", lambda b, sk=section_key: self._import_oauth_json(fields, sk))
|
||||||
area.pack_start(section_box, False, False, 0)
|
vbox.pack_start(section_box, False, False, 0)
|
||||||
|
|
||||||
area.pack_start(Gtk.Label(label="\n<small>Import a client_secret_*.json from Google Cloud Console\nor edit fields manually. console.cloud.google.com → Credentials</small>", use_markup=True, xalign=0), False, False, 4)
|
vbox.pack_start(Gtk.Label(label="<small>Import client_secret_*.json from Google Cloud Console → Credentials</small>", use_markup=True, xalign=0), False, False, 4)
|
||||||
area.show_all()
|
|
||||||
|
sep = Gtk.Separator()
|
||||||
|
vbox.pack_start(sep, False, False, 8)
|
||||||
|
|
||||||
|
vbox.pack_start(Gtk.Label(label="\n<b>Freebuff / Codebuff Credentials</b>\n<small>~/.config/manicode/credentials.json</small>", use_markup=True, xalign=0), False, False, 4)
|
||||||
|
|
||||||
|
cb_creds_path = os.path.expanduser("~/.config/manicode/credentials.json")
|
||||||
|
cb_fields = {}
|
||||||
|
try:
|
||||||
|
with open(cb_creds_path) as f:
|
||||||
|
cb_data = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
cb_data = {}
|
||||||
|
cb_default = cb_data.get("default", {})
|
||||||
|
cb_status_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
||||||
|
|
||||||
|
cb_info = f"Email: {cb_default.get('email', 'not logged in')}"
|
||||||
|
cb_name = cb_default.get("name", "")
|
||||||
|
if cb_name:
|
||||||
|
cb_info = f"{cb_name} — {cb_info}"
|
||||||
|
has_cb_token = bool(cb_default.get("authToken", ""))
|
||||||
|
status_text = "Logged in" if has_cb_token else "Not logged in"
|
||||||
|
status_color = "#27ae60" if has_cb_token else "#e67e22"
|
||||||
|
cb_info_lbl = Gtk.Label(label=f"{cb_info}\nStatus: <span foreground=\"{status_color}\" weight=\"bold\">{status_text}</span>", use_markup=True, xalign=0)
|
||||||
|
cb_status_box.pack_start(cb_info_lbl, False, False, 2)
|
||||||
|
|
||||||
|
for fk, fl in [("authToken", "Auth Token"), ("fingerprintId", "Fingerprint ID")]:
|
||||||
|
row = Gtk.Box(spacing=6)
|
||||||
|
lbl = Gtk.Label(label=fl + ":", xalign=0)
|
||||||
|
lbl.set_size_request(110, -1)
|
||||||
|
entry = Gtk.Entry()
|
||||||
|
entry.set_text(cb_default.get(fk, ""))
|
||||||
|
entry.set_size_request(360, -1)
|
||||||
|
entry.set_visibility(False)
|
||||||
|
entry.set_invisible_char("*")
|
||||||
|
row.pack_start(lbl, False, False, 0)
|
||||||
|
row.pack_start(entry, True, True, 0)
|
||||||
|
cb_status_box.pack_start(row, False, False, 2)
|
||||||
|
cb_fields[fk] = entry
|
||||||
|
|
||||||
|
cb_btn_row = Gtk.Box(spacing=6)
|
||||||
|
cb_login_btn = Gtk.Button(label="Re-OAuth (GitHub Login)")
|
||||||
|
cb_login_btn.connect("clicked", lambda b: self._codebuff_reoauth())
|
||||||
|
cb_btn_row.pack_start(cb_login_btn, False, False, 0)
|
||||||
|
cb_status_box.pack_start(cb_btn_row, False, False, 4)
|
||||||
|
|
||||||
|
vbox.pack_start(cb_status_box, False, False, 0)
|
||||||
|
|
||||||
|
cb_accounts = cb_data.get("accounts", [])
|
||||||
|
if cb_accounts:
|
||||||
|
vbox.pack_start(Gtk.Label(label=f"\n<small>Additional accounts: {len(cb_accounts)} (edit credentials.json manually)</small>", use_markup=True, xalign=0), False, False, 2)
|
||||||
|
|
||||||
|
vbox.show_all()
|
||||||
|
sw.show_all()
|
||||||
|
|
||||||
if dlg.run() == Gtk.ResponseType.OK:
|
if dlg.run() == Gtk.ResponseType.OK:
|
||||||
for (sk, fk), entry in fields.items():
|
for (sk, fk), entry in fields.items():
|
||||||
@@ -2854,6 +3123,20 @@ class LauncherWin(Gtk.Window):
|
|||||||
os.chmod(secrets_path, 0o600)
|
os.chmod(secrets_path, 0o600)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._show_error_dialog("Save failed", str(e))
|
self._show_error_dialog("Save failed", str(e))
|
||||||
|
cb_updated = dict(cb_default)
|
||||||
|
for fk, entry in cb_fields.items():
|
||||||
|
val = entry.get_text().strip()
|
||||||
|
if val:
|
||||||
|
cb_updated[fk] = val
|
||||||
|
if cb_updated:
|
||||||
|
cb_data["default"] = cb_updated
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(cb_creds_path), exist_ok=True)
|
||||||
|
with open(cb_creds_path, "w") as f:
|
||||||
|
json.dump(cb_data, f, indent=2)
|
||||||
|
os.chmod(cb_creds_path, 0o600)
|
||||||
|
except Exception as e:
|
||||||
|
self._show_error_dialog("Save failed", str(e))
|
||||||
dlg.destroy()
|
dlg.destroy()
|
||||||
|
|
||||||
def _import_oauth_json(self, fields, section_key):
|
def _import_oauth_json(self, fields, section_key):
|
||||||
@@ -3220,6 +3503,38 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
add_row(7, "Effort:", self._combo_effort)
|
add_row(7, "Effort:", self._combo_effort)
|
||||||
self._on_reasoning_toggled()
|
self._on_reasoning_toggled()
|
||||||
|
|
||||||
|
enhancer_box = Gtk.Box(spacing=6)
|
||||||
|
self._switch_enhancer = Gtk.Switch()
|
||||||
|
self._switch_enhancer.set_active(self._data.get("prompt_enhancer", False))
|
||||||
|
enhancer_box.pack_start(self._switch_enhancer, False, False, 0)
|
||||||
|
self._enhancer_status_lbl = Gtk.Label()
|
||||||
|
enhancer_box.pack_start(self._enhancer_status_lbl, False, False, 0)
|
||||||
|
self._switch_enhancer.connect("notify::active", lambda *a: self._on_enhancer_toggled())
|
||||||
|
self._combo_enhancer_mode = Gtk.ComboBoxText()
|
||||||
|
for mode in ["offline", "ai-powered"]:
|
||||||
|
self._combo_enhancer_mode.append(mode, mode.capitalize())
|
||||||
|
self._combo_enhancer_mode.set_active_id(self._data.get("prompt_enhancer_mode", "offline"))
|
||||||
|
enhancer_box.pack_start(self._combo_enhancer_mode, False, False, 6)
|
||||||
|
add_row(8, "Prompt Enhancer:", enhancer_box)
|
||||||
|
self._on_enhancer_toggled()
|
||||||
|
|
||||||
|
self._entry_enhancer_model = Gtk.Entry()
|
||||||
|
self._entry_enhancer_model.set_placeholder_text("e.g. deepseek/deepseek-v4-flash (ai-powered mode only)")
|
||||||
|
self._entry_enhancer_model.set_text(self._data.get("prompt_enhancer_model", ""))
|
||||||
|
add_row(9, "Enhancer Model:", self._entry_enhancer_model)
|
||||||
|
|
||||||
|
self._entry_enhancer_url = Gtk.Entry()
|
||||||
|
self._entry_enhancer_url.set_placeholder_text("e.g. https://www.codebuff.com/api/v1 (ai-powered mode only)")
|
||||||
|
self._entry_enhancer_url.set_text(self._data.get("prompt_enhancer_url", ""))
|
||||||
|
add_row(10, "Enhancer URL:", self._entry_enhancer_url)
|
||||||
|
|
||||||
|
self._entry_enhancer_key = Gtk.Entry()
|
||||||
|
self._entry_enhancer_key.set_placeholder_text("API key for enhancer model (ai-powered mode only)")
|
||||||
|
self._entry_enhancer_key.set_text(self._data.get("prompt_enhancer_key", ""))
|
||||||
|
self._entry_enhancer_key.set_visibility(False)
|
||||||
|
self._entry_enhancer_key.set_invisible_char("*")
|
||||||
|
add_row(11, "Enhancer Key:", self._entry_enhancer_key)
|
||||||
|
|
||||||
# Models
|
# Models
|
||||||
mlbl = Gtk.Label(label="Models:", xalign=0)
|
mlbl = Gtk.Label(label="Models:", xalign=0)
|
||||||
area.pack_start(mlbl, False, False, 4)
|
area.pack_start(mlbl, False, False, 4)
|
||||||
@@ -3359,6 +3674,13 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
else:
|
else:
|
||||||
self._lbl_reasoning.set_markup('<span foreground="#e67e22" weight="bold">OFF</span>')
|
self._lbl_reasoning.set_markup('<span foreground="#e67e22" weight="bold">OFF</span>')
|
||||||
|
|
||||||
|
def _on_enhancer_toggled(self, *_):
|
||||||
|
active = self._switch_enhancer.get_active()
|
||||||
|
if active:
|
||||||
|
self._enhancer_status_lbl.set_markup('<span foreground="#27ae60" weight="bold">ON</span>')
|
||||||
|
else:
|
||||||
|
self._enhancer_status_lbl.set_markup('<span foreground="#888888" weight="bold">OFF</span>')
|
||||||
|
|
||||||
def _do_oauth_login(self):
|
def _do_oauth_login(self):
|
||||||
preset_name = self._combo_preset.get_active_text() or "Custom"
|
preset_name = self._combo_preset.get_active_text() or "Custom"
|
||||||
preset = PROVIDER_PRESETS.get(preset_name, {})
|
preset = PROVIDER_PRESETS.get(preset_name, {})
|
||||||
@@ -3685,10 +4007,10 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
def _codebuff_auth_thread():
|
def _codebuff_auth_thread():
|
||||||
try:
|
try:
|
||||||
fingerprint_id = str(uuid.uuid4())
|
fingerprint_id = str(uuid.uuid4())
|
||||||
auth_url = "https://codebuff.com/api/auth/cli/code"
|
auth_url = "https://www.codebuff.com/api/auth/cli/code"
|
||||||
body = json.dumps({"fingerprintId": fingerprint_id}).encode()
|
body = json.dumps({"fingerprintId": fingerprint_id}).encode()
|
||||||
req = urllib.request.Request(auth_url, data=body,
|
req = urllib.request.Request(auth_url, data=body,
|
||||||
headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.10.5"})
|
headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.10.7"})
|
||||||
resp = urllib.request.urlopen(req, timeout=30)
|
resp = urllib.request.urlopen(req, timeout=30)
|
||||||
data = json.loads(resp.read())
|
data = json.loads(resp.read())
|
||||||
login_url = data.get("loginUrl", "") or data.get("login_url", "")
|
login_url = data.get("loginUrl", "") or data.get("login_url", "")
|
||||||
@@ -3707,13 +4029,13 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
|
|
||||||
webbrowser.open(login_url)
|
webbrowser.open(login_url)
|
||||||
|
|
||||||
poll_url = f"https://codebuff.com/api/auth/cli/status?fingerprintId={urllib.parse.quote(fingerprint_id)}&fingerprintHash={urllib.parse.quote(fingerprint_hash)}&expiresAt={expires_at}"
|
poll_url = f"https://www.codebuff.com/api/auth/cli/status?fingerprintId={urllib.parse.quote(fingerprint_id)}&fingerprintHash={urllib.parse.quote(fingerprint_hash)}&expiresAt={expires_at}"
|
||||||
deadline = time.time() + 300
|
deadline = time.time() + 300
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
try:
|
try:
|
||||||
poll_req = urllib.request.Request(poll_url,
|
poll_req = urllib.request.Request(poll_url,
|
||||||
headers={"User-Agent": "codex-launcher/3.10.5"})
|
headers={"User-Agent": "codex-launcher/3.10.7"})
|
||||||
poll_resp = urllib.request.urlopen(poll_req, timeout=10)
|
poll_resp = urllib.request.urlopen(poll_req, timeout=10)
|
||||||
poll_data = json.loads(poll_resp.read())
|
poll_data = json.loads(poll_resp.read())
|
||||||
user = poll_data.get("user")
|
user = poll_data.get("user")
|
||||||
@@ -3912,6 +4234,17 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
new_ep["cc_version"] = cc_ver
|
new_ep["cc_version"] = cc_ver
|
||||||
new_ep["reasoning_enabled"] = self._switch_reasoning.get_active()
|
new_ep["reasoning_enabled"] = self._switch_reasoning.get_active()
|
||||||
new_ep["reasoning_effort"] = self._combo_effort.get_active_id() or "medium"
|
new_ep["reasoning_effort"] = self._combo_effort.get_active_id() or "medium"
|
||||||
|
new_ep["prompt_enhancer"] = self._switch_enhancer.get_active()
|
||||||
|
new_ep["prompt_enhancer_mode"] = self._combo_enhancer_mode.get_active_id() or "offline"
|
||||||
|
enh_model = self._entry_enhancer_model.get_text().strip()
|
||||||
|
enh_url = self._entry_enhancer_url.get_text().strip()
|
||||||
|
enh_key = self._entry_enhancer_key.get_text().strip()
|
||||||
|
if enh_model:
|
||||||
|
new_ep["prompt_enhancer_model"] = enh_model
|
||||||
|
if enh_url:
|
||||||
|
new_ep["prompt_enhancer_url"] = enh_url
|
||||||
|
if enh_key:
|
||||||
|
new_ep["prompt_enhancer_key"] = enh_key
|
||||||
preset_name = self._combo_preset.get_active_text() or "Custom"
|
preset_name = self._combo_preset.get_active_text() or "Custom"
|
||||||
preset = PROVIDER_PRESETS.get(preset_name, {})
|
preset = PROVIDER_PRESETS.get(preset_name, {})
|
||||||
if preset.get("oauth_provider"):
|
if preset.get("oauth_provider"):
|
||||||
|
|||||||
3313
src/codex-launcher-gui.py
Normal file
3313
src/codex-launcher-gui.py
Normal file
File diff suppressed because it is too large
Load Diff
2147
src/codex_launcher_lib.py
Normal file
2147
src/codex_launcher_lib.py
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user