diff --git a/codex-launcher_3.10.6_all.deb b/codex-launcher_3.10.6_all.deb
index ae1ef0b..e8f43d1 100644
Binary files a/codex-launcher_3.10.6_all.deb and b/codex-launcher_3.10.6_all.deb differ
diff --git a/src/codex-launcher-gui b/src/codex-launcher-gui
index 65f032e..ae0439a 100755
--- a/src/codex-launcher-gui
+++ b/src/codex-launcher-gui
@@ -2808,6 +2808,154 @@ class LauncherWin(Gtk.Window):
_stop_proxy()
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="Sign in with GitHub via Codebuff", 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.6"})
+ 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'{login_url}'),
+ 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.6"})
+ 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):
secrets_path = os.path.expanduser("~/.config/codex-launcher/oauth-secrets.json")
try:
@@ -2817,10 +2965,10 @@ class LauncherWin(Gtk.Window):
data = {"antigravity": {"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("Save", Gtk.ResponseType.OK)
- dlg.set_default_size(540, 420)
+ dlg.set_default_size(580, 650)
area = dlg.get_content_area()
area.set_margin_start(16)
area.set_margin_end(16)
@@ -2828,17 +2976,43 @@ class LauncherWin(Gtk.Window):
area.set_margin_bottom(12)
area.set_spacing(6)
- area.pack_start(Gtk.Label(label="Google OAuth 2.0 credentials\nStored locally in ~/.config/codex-launcher/oauth-secrets.json", 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="Google OAuth 2.0 Client Credentials\n~/.config/codex-launcher/oauth-secrets.json", use_markup=True, xalign=0), False, False, 4)
+
+ google_token_dir = os.path.expanduser("~/.cache/codex-proxy")
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)
hdr_row = Gtk.Box(spacing=6)
hdr_row.pack_start(Gtk.Label(label=f"\n{section_label}", 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.set_size_request(100, -1)
hdr_row.pack_end(import_btn, False, False, 0)
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: valid" if has_token else "Token: missing"
+ section_box.pack_start(Gtk.Label(label=tok_status, use_markup=True, xalign=0), False, False, 0)
+
sec = data.get(section_key, {})
for fk, fl in [("client_id", "Client ID"), ("client_secret", "Client Secret")]:
row = Gtk.Box(spacing=6)
@@ -2846,7 +3020,7 @@ class LauncherWin(Gtk.Window):
lbl.set_size_request(100, -1)
entry = Gtk.Entry()
entry.set_text(sec.get(fk, ""))
- entry.set_size_request(380, -1)
+ entry.set_size_request(360, -1)
if fk == "client_secret":
entry.set_visibility(False)
entry.set_invisible_char("*")
@@ -2855,10 +3029,63 @@ class LauncherWin(Gtk.Window):
section_box.pack_start(row, False, False, 2)
fields[(section_key, fk)] = entry
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="\nImport a client_secret_*.json from Google Cloud Console\nor edit fields manually. console.cloud.google.com → Credentials", use_markup=True, xalign=0), False, False, 4)
- area.show_all()
+ vbox.pack_start(Gtk.Label(label="Import client_secret_*.json from Google Cloud Console → Credentials", use_markup=True, xalign=0), False, False, 4)
+
+ sep = Gtk.Separator()
+ vbox.pack_start(sep, False, False, 8)
+
+ vbox.pack_start(Gtk.Label(label="\nFreebuff / Codebuff Credentials\n~/.config/manicode/credentials.json", 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: {status_text}", 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"\nAdditional accounts: {len(cb_accounts)} (edit credentials.json manually)", use_markup=True, xalign=0), False, False, 2)
+
+ vbox.show_all()
+ sw.show_all()
if dlg.run() == Gtk.ResponseType.OK:
for (sk, fk), entry in fields.items():
@@ -2872,6 +3099,20 @@ class LauncherWin(Gtk.Window):
os.chmod(secrets_path, 0o600)
except Exception as 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()
def _import_oauth_json(self, fields, section_key):