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):