code-puppy 0.0.214__py3-none-any.whl → 0.0.366__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_puppy/__init__.py +7 -1
- code_puppy/agents/__init__.py +2 -0
- code_puppy/agents/agent_c_reviewer.py +59 -6
- code_puppy/agents/agent_code_puppy.py +7 -1
- code_puppy/agents/agent_code_reviewer.py +12 -2
- code_puppy/agents/agent_cpp_reviewer.py +73 -6
- code_puppy/agents/agent_creator_agent.py +45 -4
- code_puppy/agents/agent_golang_reviewer.py +92 -3
- code_puppy/agents/agent_javascript_reviewer.py +101 -8
- code_puppy/agents/agent_manager.py +81 -4
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -0
- code_puppy/agents/agent_python_reviewer.py +28 -6
- code_puppy/agents/agent_qa_expert.py +98 -6
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_security_auditor.py +113 -3
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +106 -7
- code_puppy/agents/base_agent.py +802 -176
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +142 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +10 -5
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +176 -738
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +0 -3
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +15 -26
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +2 -2
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +16 -10
- code_puppy/command_line/mcp/start_all_command.py +18 -6
- code_puppy/command_line/mcp/start_command.py +47 -25
- code_puppy/command_line/mcp/status_command.py +4 -5
- code_puppy/command_line/mcp/stop_all_command.py +7 -1
- code_puppy/command_line/mcp/stop_command.py +8 -4
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/wizard_utils.py +20 -16
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +75 -25
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +463 -63
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +898 -112
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +210 -148
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -698
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/blocking_startup.py +70 -43
- code_puppy/mcp_/captured_stdio_server.py +2 -2
- code_puppy/mcp_/config_wizard.py +4 -4
- code_puppy/mcp_/dashboard.py +15 -6
- code_puppy/mcp_/managed_server.py +65 -38
- code_puppy/mcp_/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +6 -6
- code_puppy/mcp_/server_registry_catalog.py +24 -5
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +21 -5
- code_puppy/messaging/spinner/console_spinner.py +86 -51
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +634 -83
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +66 -68
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/round_robin_model.py +9 -12
- code_puppy/session_storage.py +2 -1
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +41 -13
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +536 -52
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +19 -23
- code_puppy/tools/browser/browser_interactions.py +41 -48
- code_puppy/tools/browser/browser_locators.py +36 -38
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +16 -16
- code_puppy/tools/browser/browser_screenshot.py +79 -143
- code_puppy/tools/browser/browser_scripts.py +32 -42
- code_puppy/tools/browser/browser_workflows.py +44 -27
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +930 -147
- code_puppy/tools/common.py +1113 -5
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +226 -154
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/messaging/spinner/textual_spinner.py +0 -106
- code_puppy/tools/browser/camoufox_manager.py +0 -216
- code_puppy/tools/browser/vqa_agent.py +0 -70
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -1105
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -551
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -185
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -17
- code_puppy/tui/screens/autosave_picker.py +0 -175
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -306
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy/tui_state.py +0 -55
- code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
- code_puppy-0.0.214.dist-info/RECORD +0 -131
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""Antigravity OAuth Plugin callbacks for Code Puppy CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
9
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
10
|
+
from urllib.parse import parse_qs, urlparse
|
|
11
|
+
|
|
12
|
+
from code_puppy.callbacks import register_callback
|
|
13
|
+
from code_puppy.config import set_model_name
|
|
14
|
+
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
15
|
+
|
|
16
|
+
from ..oauth_puppy_html import oauth_failure_html, oauth_success_html
|
|
17
|
+
from .accounts import AccountManager
|
|
18
|
+
from .config import (
|
|
19
|
+
ANTIGRAVITY_OAUTH_CONFIG,
|
|
20
|
+
get_accounts_storage_path,
|
|
21
|
+
get_token_storage_path,
|
|
22
|
+
)
|
|
23
|
+
from .constants import ANTIGRAVITY_MODELS
|
|
24
|
+
from .oauth import (
|
|
25
|
+
TokenExchangeSuccess,
|
|
26
|
+
assign_redirect_uri,
|
|
27
|
+
build_authorization_url,
|
|
28
|
+
exchange_code_for_tokens,
|
|
29
|
+
fetch_antigravity_status,
|
|
30
|
+
prepare_oauth_context,
|
|
31
|
+
)
|
|
32
|
+
from .storage import clear_accounts
|
|
33
|
+
from .utils import (
|
|
34
|
+
add_models_to_config,
|
|
35
|
+
load_antigravity_models,
|
|
36
|
+
load_stored_tokens,
|
|
37
|
+
reload_current_agent,
|
|
38
|
+
remove_antigravity_models,
|
|
39
|
+
save_tokens,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _OAuthResult:
|
|
46
|
+
def __init__(self) -> None:
|
|
47
|
+
self.code: Optional[str] = None
|
|
48
|
+
self.state: Optional[str] = None
|
|
49
|
+
self.error: Optional[str] = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _CallbackHandler(BaseHTTPRequestHandler):
|
|
53
|
+
result: _OAuthResult
|
|
54
|
+
received_event: threading.Event
|
|
55
|
+
redirect_uri: str
|
|
56
|
+
|
|
57
|
+
def do_GET(self) -> None: # noqa: N802
|
|
58
|
+
logger.info("Callback received: path=%s", self.path)
|
|
59
|
+
parsed = urlparse(self.path)
|
|
60
|
+
params: Dict[str, List[str]] = parse_qs(parsed.query)
|
|
61
|
+
|
|
62
|
+
code = params.get("code", [None])[0]
|
|
63
|
+
state = params.get("state", [None])[0]
|
|
64
|
+
|
|
65
|
+
if code and state:
|
|
66
|
+
self.result.code = code
|
|
67
|
+
self.result.state = state
|
|
68
|
+
success_html = oauth_success_html(
|
|
69
|
+
"Antigravity",
|
|
70
|
+
"You're connected to Antigravity! 🚀 Gemini & Claude models are now available.",
|
|
71
|
+
)
|
|
72
|
+
self._write_response(200, success_html)
|
|
73
|
+
else:
|
|
74
|
+
self.result.error = "Missing code or state"
|
|
75
|
+
failure_html = oauth_failure_html(
|
|
76
|
+
"Antigravity",
|
|
77
|
+
"Missing code or state parameter 🥺",
|
|
78
|
+
)
|
|
79
|
+
self._write_response(400, failure_html)
|
|
80
|
+
|
|
81
|
+
self.received_event.set()
|
|
82
|
+
|
|
83
|
+
def log_message(self, format: str, *args: Any) -> None: # noqa: A002
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
def _write_response(self, status: int, body: str) -> None:
|
|
87
|
+
self.send_response(status)
|
|
88
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
89
|
+
self.end_headers()
|
|
90
|
+
self.wfile.write(body.encode("utf-8"))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _start_callback_server(
|
|
94
|
+
context: Any,
|
|
95
|
+
) -> Optional[Tuple[HTTPServer, _OAuthResult, threading.Event, str]]:
|
|
96
|
+
"""Start local HTTP server for OAuth callback."""
|
|
97
|
+
port_range = ANTIGRAVITY_OAUTH_CONFIG["callback_port_range"]
|
|
98
|
+
|
|
99
|
+
for port in range(port_range[0], port_range[1] + 1):
|
|
100
|
+
try:
|
|
101
|
+
server = HTTPServer(("localhost", port), _CallbackHandler)
|
|
102
|
+
redirect_uri = assign_redirect_uri(context, port)
|
|
103
|
+
result = _OAuthResult()
|
|
104
|
+
event = threading.Event()
|
|
105
|
+
_CallbackHandler.result = result
|
|
106
|
+
_CallbackHandler.received_event = event
|
|
107
|
+
_CallbackHandler.redirect_uri = redirect_uri
|
|
108
|
+
|
|
109
|
+
def run_server() -> None:
|
|
110
|
+
with server:
|
|
111
|
+
server.serve_forever()
|
|
112
|
+
|
|
113
|
+
threading.Thread(target=run_server, daemon=True).start()
|
|
114
|
+
return server, result, event, redirect_uri
|
|
115
|
+
except OSError:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
emit_error("Could not start OAuth callback server; all candidate ports are in use")
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _await_callback(context: Any) -> Optional[Tuple[str, str, str]]:
|
|
123
|
+
"""Wait for OAuth callback and return (code, state, redirect_uri)."""
|
|
124
|
+
timeout = ANTIGRAVITY_OAUTH_CONFIG["callback_timeout"]
|
|
125
|
+
|
|
126
|
+
started = _start_callback_server(context)
|
|
127
|
+
if not started:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
server, result, event, redirect_uri = started
|
|
131
|
+
|
|
132
|
+
auth_url = build_authorization_url(context)
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
import webbrowser
|
|
136
|
+
|
|
137
|
+
from code_puppy.tools.common import should_suppress_browser
|
|
138
|
+
|
|
139
|
+
if should_suppress_browser():
|
|
140
|
+
emit_info(
|
|
141
|
+
"[HEADLESS MODE] Would normally open browser for Antigravity OAuth…"
|
|
142
|
+
)
|
|
143
|
+
emit_info(f"In normal mode, would visit: {auth_url}")
|
|
144
|
+
else:
|
|
145
|
+
emit_info("🌐 Opening browser for Google OAuth…")
|
|
146
|
+
webbrowser.open(auth_url)
|
|
147
|
+
emit_info(f"If it doesn't open automatically, visit:\n{auth_url}")
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
emit_warning(f"Failed to open browser: {exc}")
|
|
150
|
+
emit_info(f"Please open manually: {auth_url}")
|
|
151
|
+
|
|
152
|
+
emit_info(f"⏳ Waiting for callback on {redirect_uri}")
|
|
153
|
+
|
|
154
|
+
if not event.wait(timeout=timeout):
|
|
155
|
+
emit_error("OAuth callback timed out. Please try again.")
|
|
156
|
+
server.shutdown()
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
server.shutdown()
|
|
160
|
+
|
|
161
|
+
if result.error:
|
|
162
|
+
emit_error(f"OAuth callback error: {result.error}")
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
return result.code, result.state, redirect_uri
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _perform_authentication(add_account: bool = False) -> bool:
|
|
169
|
+
"""Run the OAuth authentication flow."""
|
|
170
|
+
context = prepare_oauth_context()
|
|
171
|
+
callback_result = _await_callback(context)
|
|
172
|
+
|
|
173
|
+
if not callback_result:
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
code, state, redirect_uri = callback_result
|
|
177
|
+
|
|
178
|
+
emit_info("🔄 Exchanging authorization code for tokens…")
|
|
179
|
+
result = exchange_code_for_tokens(code, state, redirect_uri)
|
|
180
|
+
|
|
181
|
+
if not isinstance(result, TokenExchangeSuccess):
|
|
182
|
+
emit_error(f"Token exchange failed: {result.error}")
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
# Save tokens
|
|
186
|
+
tokens = {
|
|
187
|
+
"access_token": result.access_token,
|
|
188
|
+
"refresh_token": result.refresh_token,
|
|
189
|
+
"expires_at": result.expires_at,
|
|
190
|
+
"email": result.email,
|
|
191
|
+
"project_id": result.project_id,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if not save_tokens(tokens):
|
|
195
|
+
emit_error("Failed to save tokens locally. Check file permissions.")
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
# Handle multi-account
|
|
199
|
+
manager = AccountManager.load_from_disk(result.refresh_token)
|
|
200
|
+
|
|
201
|
+
if add_account or manager.account_count == 0:
|
|
202
|
+
manager.add_account(
|
|
203
|
+
refresh_token=result.refresh_token,
|
|
204
|
+
email=result.email,
|
|
205
|
+
project_id=result.project_id,
|
|
206
|
+
)
|
|
207
|
+
manager.save_to_disk()
|
|
208
|
+
|
|
209
|
+
if add_account:
|
|
210
|
+
emit_success(f"✅ Added account: {result.email or 'Unknown'}")
|
|
211
|
+
emit_info(f"📊 Total accounts: {manager.account_count}")
|
|
212
|
+
|
|
213
|
+
if result.email:
|
|
214
|
+
emit_success(f"🎉 Authenticated as {result.email}!")
|
|
215
|
+
else:
|
|
216
|
+
emit_success("🎉 Antigravity OAuth authentication successful!")
|
|
217
|
+
|
|
218
|
+
# Add models
|
|
219
|
+
emit_info("📦 Configuring available models…")
|
|
220
|
+
if add_models_to_config(result.access_token, result.project_id):
|
|
221
|
+
model_count = len(ANTIGRAVITY_MODELS)
|
|
222
|
+
emit_success(f"✅ {model_count} Antigravity models configured!")
|
|
223
|
+
emit_info(
|
|
224
|
+
" Use the `antigravity-` prefix (e.g., antigravity-gemini-3-pro-high)"
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
emit_warning("Failed to configure models. Try running /antigravity-auth again.")
|
|
228
|
+
|
|
229
|
+
# Reload agent
|
|
230
|
+
reload_current_agent()
|
|
231
|
+
return True
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _custom_help() -> List[Tuple[str, str]]:
|
|
235
|
+
"""Return help entries for Antigravity commands."""
|
|
236
|
+
return [
|
|
237
|
+
(
|
|
238
|
+
"antigravity-auth",
|
|
239
|
+
"Authenticate with Google/Antigravity for Gemini & Claude models",
|
|
240
|
+
),
|
|
241
|
+
(
|
|
242
|
+
"antigravity-add",
|
|
243
|
+
"Add another Google account for load balancing",
|
|
244
|
+
),
|
|
245
|
+
(
|
|
246
|
+
"antigravity-status",
|
|
247
|
+
"Check authentication status and account pool",
|
|
248
|
+
),
|
|
249
|
+
(
|
|
250
|
+
"antigravity-logout",
|
|
251
|
+
"Remove all Antigravity OAuth tokens and models",
|
|
252
|
+
),
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _handle_status() -> None:
|
|
257
|
+
"""Handle /antigravity-status command."""
|
|
258
|
+
tokens = load_stored_tokens()
|
|
259
|
+
|
|
260
|
+
if not tokens or not tokens.get("access_token"):
|
|
261
|
+
emit_warning("🔓 Antigravity: Not authenticated")
|
|
262
|
+
emit_info("Run /antigravity-auth to sign in with Google")
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
emit_success("🔐 Antigravity: Authenticated")
|
|
266
|
+
|
|
267
|
+
# Show email if available
|
|
268
|
+
if tokens.get("email"):
|
|
269
|
+
emit_info(f" Primary account: {tokens['email']}")
|
|
270
|
+
|
|
271
|
+
# Show token expiry
|
|
272
|
+
expires_at = tokens.get("expires_at")
|
|
273
|
+
if expires_at:
|
|
274
|
+
remaining = max(0, int(expires_at - time.time()))
|
|
275
|
+
hours, remainder = divmod(remaining, 3600)
|
|
276
|
+
minutes = remainder // 60
|
|
277
|
+
emit_info(f" Token expires in: ~{hours}h {minutes}m")
|
|
278
|
+
|
|
279
|
+
# Fetch tier/quota status from API
|
|
280
|
+
emit_info("\n📊 Fetching tier status...")
|
|
281
|
+
status = fetch_antigravity_status(tokens.get("access_token", ""))
|
|
282
|
+
|
|
283
|
+
if status.error:
|
|
284
|
+
emit_warning(f" Could not fetch status: {status.error}")
|
|
285
|
+
else:
|
|
286
|
+
# Show tier info
|
|
287
|
+
tier_display = {
|
|
288
|
+
"free-tier": "Free Tier (limited)",
|
|
289
|
+
"standard-tier": "Standard Tier (full access)",
|
|
290
|
+
}
|
|
291
|
+
current = tier_display.get(
|
|
292
|
+
status.current_tier, status.current_tier or "Unknown"
|
|
293
|
+
)
|
|
294
|
+
emit_info(f" Current tier: {current}")
|
|
295
|
+
|
|
296
|
+
if status.project_id:
|
|
297
|
+
emit_info(f" Project ID: {status.project_id}")
|
|
298
|
+
|
|
299
|
+
if status.allowed_tiers:
|
|
300
|
+
available = ", ".join(status.allowed_tiers)
|
|
301
|
+
emit_info(f" Available tiers: {available}")
|
|
302
|
+
|
|
303
|
+
# Show account pool
|
|
304
|
+
manager = AccountManager.load_from_disk()
|
|
305
|
+
if manager.account_count > 1:
|
|
306
|
+
emit_info(f"\n📊 Account Pool: {manager.account_count} accounts")
|
|
307
|
+
for acc in manager.get_accounts_snapshot():
|
|
308
|
+
email_str = acc.email or "Unknown"
|
|
309
|
+
limits = []
|
|
310
|
+
if acc.rate_limit_reset_times:
|
|
311
|
+
for key, reset_time in acc.rate_limit_reset_times.items():
|
|
312
|
+
if reset_time > time.time() * 1000:
|
|
313
|
+
wait_sec = int((reset_time - time.time() * 1000) / 1000)
|
|
314
|
+
limits.append(f"{key}: {wait_sec}s")
|
|
315
|
+
|
|
316
|
+
status = f" • {email_str}"
|
|
317
|
+
if limits:
|
|
318
|
+
status += f" (rate-limited: {', '.join(limits)})"
|
|
319
|
+
emit_info(status)
|
|
320
|
+
|
|
321
|
+
# Show configured models
|
|
322
|
+
models = load_antigravity_models()
|
|
323
|
+
antigravity_models = [
|
|
324
|
+
name
|
|
325
|
+
for name, cfg in models.items()
|
|
326
|
+
if cfg.get("oauth_source") == "antigravity-plugin"
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
if antigravity_models:
|
|
330
|
+
emit_info(f"\n🎯 Configured models: {len(antigravity_models)}")
|
|
331
|
+
# Group by family
|
|
332
|
+
gemini = [m for m in antigravity_models if "gemini" in m]
|
|
333
|
+
claude = [m for m in antigravity_models if "claude" in m]
|
|
334
|
+
other = [m for m in antigravity_models if m not in gemini and m not in claude]
|
|
335
|
+
|
|
336
|
+
if gemini:
|
|
337
|
+
emit_info(f" Gemini: {', '.join(sorted(gemini))}")
|
|
338
|
+
if claude:
|
|
339
|
+
emit_info(f" Claude: {', '.join(sorted(claude))}")
|
|
340
|
+
if other:
|
|
341
|
+
emit_info(f" Other: {', '.join(sorted(other))}")
|
|
342
|
+
else:
|
|
343
|
+
emit_warning("No Antigravity models configured")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _handle_logout() -> None:
|
|
347
|
+
"""Handle /antigravity-logout command."""
|
|
348
|
+
# Remove tokens
|
|
349
|
+
token_path = get_token_storage_path()
|
|
350
|
+
if token_path.exists():
|
|
351
|
+
token_path.unlink()
|
|
352
|
+
emit_info("✓ Removed OAuth tokens")
|
|
353
|
+
|
|
354
|
+
# Remove accounts
|
|
355
|
+
accounts_path = get_accounts_storage_path()
|
|
356
|
+
if accounts_path.exists():
|
|
357
|
+
clear_accounts()
|
|
358
|
+
emit_info("✓ Removed account pool")
|
|
359
|
+
|
|
360
|
+
# Remove models
|
|
361
|
+
removed = remove_antigravity_models()
|
|
362
|
+
if removed:
|
|
363
|
+
emit_info(f"✓ Removed {removed} Antigravity models")
|
|
364
|
+
|
|
365
|
+
emit_success("👋 Antigravity logout complete")
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _handle_custom_command(command: str, name: str) -> Optional[bool]:
|
|
369
|
+
"""Handle Antigravity custom commands."""
|
|
370
|
+
if not name:
|
|
371
|
+
return None
|
|
372
|
+
|
|
373
|
+
if name == "antigravity-auth":
|
|
374
|
+
emit_info("🚀 Starting Antigravity OAuth authentication…")
|
|
375
|
+
tokens = load_stored_tokens()
|
|
376
|
+
if tokens and tokens.get("access_token"):
|
|
377
|
+
emit_warning(
|
|
378
|
+
"Existing tokens found. This will refresh your authentication."
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
if _perform_authentication():
|
|
382
|
+
# Set a default model
|
|
383
|
+
set_model_name("antigravity-gemini-3-pro-high")
|
|
384
|
+
return True
|
|
385
|
+
|
|
386
|
+
if name == "antigravity-add":
|
|
387
|
+
emit_info("➕ Adding another Google account…")
|
|
388
|
+
manager = AccountManager.load_from_disk()
|
|
389
|
+
emit_info(f"Current accounts: {manager.account_count}")
|
|
390
|
+
_perform_authentication(add_account=True)
|
|
391
|
+
return True
|
|
392
|
+
|
|
393
|
+
if name == "antigravity-status":
|
|
394
|
+
_handle_status()
|
|
395
|
+
return True
|
|
396
|
+
|
|
397
|
+
if name == "antigravity-logout":
|
|
398
|
+
_handle_logout()
|
|
399
|
+
return True
|
|
400
|
+
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# Register callbacks
|
|
405
|
+
register_callback("custom_command_help", _custom_help)
|
|
406
|
+
register_callback("custom_command", _handle_custom_command)
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Account storage for multi-account Antigravity OAuth."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
10
|
+
|
|
11
|
+
from .config import get_accounts_storage_path
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
ModelFamily = Literal["claude", "gemini"]
|
|
16
|
+
HeaderStyle = Literal["antigravity", "gemini-cli"]
|
|
17
|
+
QuotaKey = Literal["claude", "gemini-antigravity", "gemini-cli"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class RateLimitState:
|
|
22
|
+
"""Rate limit reset times per quota key."""
|
|
23
|
+
|
|
24
|
+
claude: Optional[float] = None
|
|
25
|
+
gemini_antigravity: Optional[float] = None
|
|
26
|
+
gemini_cli: Optional[float] = None
|
|
27
|
+
|
|
28
|
+
def to_dict(self) -> Dict[str, float]:
|
|
29
|
+
"""Convert to dictionary for JSON serialization."""
|
|
30
|
+
result: Dict[str, float] = {}
|
|
31
|
+
if self.claude is not None:
|
|
32
|
+
result["claude"] = self.claude
|
|
33
|
+
if self.gemini_antigravity is not None:
|
|
34
|
+
result["gemini-antigravity"] = self.gemini_antigravity
|
|
35
|
+
if self.gemini_cli is not None:
|
|
36
|
+
result["gemini-cli"] = self.gemini_cli
|
|
37
|
+
return result
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_dict(cls, data: Optional[Dict[str, Any]]) -> "RateLimitState":
|
|
41
|
+
"""Create from dictionary."""
|
|
42
|
+
if not data:
|
|
43
|
+
return cls()
|
|
44
|
+
return cls(
|
|
45
|
+
claude=data.get("claude"),
|
|
46
|
+
gemini_antigravity=data.get("gemini-antigravity"),
|
|
47
|
+
gemini_cli=data.get("gemini-cli"),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class AccountMetadata:
|
|
53
|
+
"""Stored metadata for a single account."""
|
|
54
|
+
|
|
55
|
+
refresh_token: str
|
|
56
|
+
email: Optional[str] = None
|
|
57
|
+
project_id: Optional[str] = None
|
|
58
|
+
managed_project_id: Optional[str] = None
|
|
59
|
+
added_at: float = 0
|
|
60
|
+
last_used: float = 0
|
|
61
|
+
last_switch_reason: Optional[Literal["rate-limit", "initial", "rotation"]] = None
|
|
62
|
+
rate_limit_reset_times: RateLimitState = field(default_factory=RateLimitState)
|
|
63
|
+
|
|
64
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
65
|
+
"""Convert to dictionary for JSON serialization."""
|
|
66
|
+
result: Dict[str, Any] = {
|
|
67
|
+
"refreshToken": self.refresh_token,
|
|
68
|
+
"addedAt": self.added_at,
|
|
69
|
+
"lastUsed": self.last_used,
|
|
70
|
+
}
|
|
71
|
+
if self.email:
|
|
72
|
+
result["email"] = self.email
|
|
73
|
+
if self.project_id:
|
|
74
|
+
result["projectId"] = self.project_id
|
|
75
|
+
if self.managed_project_id:
|
|
76
|
+
result["managedProjectId"] = self.managed_project_id
|
|
77
|
+
if self.last_switch_reason:
|
|
78
|
+
result["lastSwitchReason"] = self.last_switch_reason
|
|
79
|
+
|
|
80
|
+
rate_limits = self.rate_limit_reset_times.to_dict()
|
|
81
|
+
if rate_limits:
|
|
82
|
+
result["rateLimitResetTimes"] = rate_limits
|
|
83
|
+
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def from_dict(cls, data: Dict[str, Any]) -> "AccountMetadata":
|
|
88
|
+
"""Create from dictionary."""
|
|
89
|
+
return cls(
|
|
90
|
+
refresh_token=data.get("refreshToken", ""),
|
|
91
|
+
email=data.get("email"),
|
|
92
|
+
project_id=data.get("projectId"),
|
|
93
|
+
managed_project_id=data.get("managedProjectId"),
|
|
94
|
+
added_at=data.get("addedAt", 0),
|
|
95
|
+
last_used=data.get("lastUsed", 0),
|
|
96
|
+
last_switch_reason=data.get("lastSwitchReason"),
|
|
97
|
+
rate_limit_reset_times=RateLimitState.from_dict(
|
|
98
|
+
data.get("rateLimitResetTimes")
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class AccountStorage:
|
|
105
|
+
"""V3 account storage format."""
|
|
106
|
+
|
|
107
|
+
version: int = 3
|
|
108
|
+
accounts: List[AccountMetadata] = field(default_factory=list)
|
|
109
|
+
active_index: int = 0
|
|
110
|
+
active_index_by_family: Dict[str, int] = field(default_factory=dict)
|
|
111
|
+
|
|
112
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
113
|
+
"""Convert to dictionary for JSON serialization."""
|
|
114
|
+
return {
|
|
115
|
+
"version": self.version,
|
|
116
|
+
"accounts": [acc.to_dict() for acc in self.accounts],
|
|
117
|
+
"activeIndex": self.active_index,
|
|
118
|
+
"activeIndexByFamily": self.active_index_by_family,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def from_dict(cls, data: Dict[str, Any]) -> "AccountStorage":
|
|
123
|
+
"""Create from dictionary."""
|
|
124
|
+
accounts = [AccountMetadata.from_dict(acc) for acc in data.get("accounts", [])]
|
|
125
|
+
return cls(
|
|
126
|
+
version=data.get("version", 3),
|
|
127
|
+
accounts=accounts,
|
|
128
|
+
active_index=data.get("activeIndex", 0),
|
|
129
|
+
active_index_by_family=data.get("activeIndexByFamily", {}),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _migrate_v1_to_v2(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
134
|
+
"""Migrate V1 storage format to V2."""
|
|
135
|
+
now = time.time() * 1000 # V1 used milliseconds
|
|
136
|
+
|
|
137
|
+
accounts = []
|
|
138
|
+
for acc in data.get("accounts", []):
|
|
139
|
+
rate_limits: Dict[str, float] = {}
|
|
140
|
+
if acc.get("isRateLimited") and acc.get("rateLimitResetTime"):
|
|
141
|
+
reset_time = acc["rateLimitResetTime"]
|
|
142
|
+
if reset_time > now:
|
|
143
|
+
rate_limits["claude"] = reset_time
|
|
144
|
+
rate_limits["gemini"] = reset_time
|
|
145
|
+
|
|
146
|
+
accounts.append(
|
|
147
|
+
{
|
|
148
|
+
"email": acc.get("email"),
|
|
149
|
+
"refreshToken": acc.get("refreshToken", ""),
|
|
150
|
+
"projectId": acc.get("projectId"),
|
|
151
|
+
"managedProjectId": acc.get("managedProjectId"),
|
|
152
|
+
"addedAt": acc.get("addedAt", now),
|
|
153
|
+
"lastUsed": acc.get("lastUsed", 0),
|
|
154
|
+
"lastSwitchReason": acc.get("lastSwitchReason"),
|
|
155
|
+
"rateLimitResetTimes": rate_limits if rate_limits else None,
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
"version": 2,
|
|
161
|
+
"accounts": accounts,
|
|
162
|
+
"activeIndex": data.get("activeIndex", 0),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _migrate_v2_to_v3(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
167
|
+
"""Migrate V2 storage format to V3."""
|
|
168
|
+
now = time.time() * 1000
|
|
169
|
+
|
|
170
|
+
accounts = []
|
|
171
|
+
for acc in data.get("accounts", []):
|
|
172
|
+
rate_limits: Dict[str, float] = {}
|
|
173
|
+
old_limits = acc.get("rateLimitResetTimes", {}) or {}
|
|
174
|
+
|
|
175
|
+
if old_limits.get("claude") and old_limits["claude"] > now:
|
|
176
|
+
rate_limits["claude"] = old_limits["claude"]
|
|
177
|
+
if old_limits.get("gemini") and old_limits["gemini"] > now:
|
|
178
|
+
rate_limits["gemini-antigravity"] = old_limits["gemini"]
|
|
179
|
+
|
|
180
|
+
accounts.append(
|
|
181
|
+
{
|
|
182
|
+
"email": acc.get("email"),
|
|
183
|
+
"refreshToken": acc.get("refreshToken", ""),
|
|
184
|
+
"projectId": acc.get("projectId"),
|
|
185
|
+
"managedProjectId": acc.get("managedProjectId"),
|
|
186
|
+
"addedAt": acc.get("addedAt", 0),
|
|
187
|
+
"lastUsed": acc.get("lastUsed", 0),
|
|
188
|
+
"lastSwitchReason": acc.get("lastSwitchReason"),
|
|
189
|
+
"rateLimitResetTimes": rate_limits if rate_limits else None,
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
"version": 3,
|
|
195
|
+
"accounts": accounts,
|
|
196
|
+
"activeIndex": data.get("activeIndex", 0),
|
|
197
|
+
"activeIndexByFamily": {},
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def load_accounts() -> Optional[AccountStorage]:
|
|
202
|
+
"""Load account storage from disk with automatic migration."""
|
|
203
|
+
path = get_accounts_storage_path()
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
if not path.exists():
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
content = path.read_text(encoding="utf-8")
|
|
210
|
+
data = json.loads(content)
|
|
211
|
+
|
|
212
|
+
if not isinstance(data.get("accounts"), list):
|
|
213
|
+
logger.warning("Invalid storage format, ignoring")
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
version = data.get("version", 1)
|
|
217
|
+
|
|
218
|
+
# Migrate if needed
|
|
219
|
+
if version == 1:
|
|
220
|
+
logger.info("Migrating account storage from v1 to v3")
|
|
221
|
+
data = _migrate_v1_to_v2(data)
|
|
222
|
+
data = _migrate_v2_to_v3(data)
|
|
223
|
+
elif version == 2:
|
|
224
|
+
logger.info("Migrating account storage from v2 to v3")
|
|
225
|
+
data = _migrate_v2_to_v3(data)
|
|
226
|
+
|
|
227
|
+
storage = AccountStorage.from_dict(data)
|
|
228
|
+
|
|
229
|
+
# Validate active index
|
|
230
|
+
if storage.accounts:
|
|
231
|
+
storage.active_index = max(
|
|
232
|
+
0, min(storage.active_index, len(storage.accounts) - 1)
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
storage.active_index = 0
|
|
236
|
+
|
|
237
|
+
# Save migrated data if we migrated
|
|
238
|
+
if version < 3:
|
|
239
|
+
try:
|
|
240
|
+
save_accounts(storage)
|
|
241
|
+
logger.info("Migration to v3 complete")
|
|
242
|
+
except Exception as e:
|
|
243
|
+
logger.warning("Failed to persist migrated storage: %s", e)
|
|
244
|
+
|
|
245
|
+
return storage
|
|
246
|
+
|
|
247
|
+
except FileNotFoundError:
|
|
248
|
+
return None
|
|
249
|
+
except Exception as e:
|
|
250
|
+
logger.error("Failed to load account storage: %s", e)
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def save_accounts(storage: AccountStorage) -> None:
|
|
255
|
+
"""Save account storage to disk."""
|
|
256
|
+
path = get_accounts_storage_path()
|
|
257
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
258
|
+
|
|
259
|
+
content = json.dumps(storage.to_dict(), indent=2)
|
|
260
|
+
path.write_text(content, encoding="utf-8")
|
|
261
|
+
path.chmod(0o600)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def clear_accounts() -> None:
|
|
265
|
+
"""Clear all stored accounts."""
|
|
266
|
+
path = get_accounts_storage_path()
|
|
267
|
+
try:
|
|
268
|
+
if path.exists():
|
|
269
|
+
path.unlink()
|
|
270
|
+
except Exception as e:
|
|
271
|
+
logger.error("Failed to clear account storage: %s", e)
|