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.
Files changed (231) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +2 -0
  3. code_puppy/agents/agent_c_reviewer.py +59 -6
  4. code_puppy/agents/agent_code_puppy.py +7 -1
  5. code_puppy/agents/agent_code_reviewer.py +12 -2
  6. code_puppy/agents/agent_cpp_reviewer.py +73 -6
  7. code_puppy/agents/agent_creator_agent.py +45 -4
  8. code_puppy/agents/agent_golang_reviewer.py +92 -3
  9. code_puppy/agents/agent_javascript_reviewer.py +101 -8
  10. code_puppy/agents/agent_manager.py +81 -4
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +28 -6
  15. code_puppy/agents/agent_qa_expert.py +98 -6
  16. code_puppy/agents/agent_qa_kitten.py +12 -7
  17. code_puppy/agents/agent_security_auditor.py +113 -3
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +106 -7
  20. code_puppy/agents/base_agent.py +802 -176
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/pack/__init__.py +34 -0
  23. code_puppy/agents/pack/bloodhound.py +304 -0
  24. code_puppy/agents/pack/husky.py +321 -0
  25. code_puppy/agents/pack/retriever.py +393 -0
  26. code_puppy/agents/pack/shepherd.py +348 -0
  27. code_puppy/agents/pack/terrier.py +287 -0
  28. code_puppy/agents/pack/watchdog.py +367 -0
  29. code_puppy/agents/prompt_reviewer.py +145 -0
  30. code_puppy/agents/subagent_stream_handler.py +276 -0
  31. code_puppy/api/__init__.py +13 -0
  32. code_puppy/api/app.py +169 -0
  33. code_puppy/api/main.py +21 -0
  34. code_puppy/api/pty_manager.py +446 -0
  35. code_puppy/api/routers/__init__.py +12 -0
  36. code_puppy/api/routers/agents.py +36 -0
  37. code_puppy/api/routers/commands.py +217 -0
  38. code_puppy/api/routers/config.py +74 -0
  39. code_puppy/api/routers/sessions.py +232 -0
  40. code_puppy/api/templates/terminal.html +361 -0
  41. code_puppy/api/websocket.py +154 -0
  42. code_puppy/callbacks.py +142 -4
  43. code_puppy/chatgpt_codex_client.py +283 -0
  44. code_puppy/claude_cache_client.py +586 -0
  45. code_puppy/cli_runner.py +916 -0
  46. code_puppy/command_line/add_model_menu.py +1079 -0
  47. code_puppy/command_line/agent_menu.py +395 -0
  48. code_puppy/command_line/attachments.py +10 -5
  49. code_puppy/command_line/autosave_menu.py +605 -0
  50. code_puppy/command_line/clipboard.py +527 -0
  51. code_puppy/command_line/colors_menu.py +520 -0
  52. code_puppy/command_line/command_handler.py +176 -738
  53. code_puppy/command_line/command_registry.py +150 -0
  54. code_puppy/command_line/config_commands.py +715 -0
  55. code_puppy/command_line/core_commands.py +792 -0
  56. code_puppy/command_line/diff_menu.py +863 -0
  57. code_puppy/command_line/load_context_completion.py +15 -22
  58. code_puppy/command_line/mcp/base.py +0 -3
  59. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  60. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  61. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  62. code_puppy/command_line/mcp/edit_command.py +148 -0
  63. code_puppy/command_line/mcp/handler.py +9 -4
  64. code_puppy/command_line/mcp/help_command.py +6 -5
  65. code_puppy/command_line/mcp/install_command.py +15 -26
  66. code_puppy/command_line/mcp/install_menu.py +685 -0
  67. code_puppy/command_line/mcp/list_command.py +2 -2
  68. code_puppy/command_line/mcp/logs_command.py +174 -65
  69. code_puppy/command_line/mcp/remove_command.py +2 -2
  70. code_puppy/command_line/mcp/restart_command.py +12 -4
  71. code_puppy/command_line/mcp/search_command.py +16 -10
  72. code_puppy/command_line/mcp/start_all_command.py +18 -6
  73. code_puppy/command_line/mcp/start_command.py +47 -25
  74. code_puppy/command_line/mcp/status_command.py +4 -5
  75. code_puppy/command_line/mcp/stop_all_command.py +7 -1
  76. code_puppy/command_line/mcp/stop_command.py +8 -4
  77. code_puppy/command_line/mcp/test_command.py +2 -2
  78. code_puppy/command_line/mcp/wizard_utils.py +20 -16
  79. code_puppy/command_line/mcp_completion.py +174 -0
  80. code_puppy/command_line/model_picker_completion.py +75 -25
  81. code_puppy/command_line/model_settings_menu.py +884 -0
  82. code_puppy/command_line/motd.py +14 -8
  83. code_puppy/command_line/onboarding_slides.py +179 -0
  84. code_puppy/command_line/onboarding_wizard.py +340 -0
  85. code_puppy/command_line/pin_command_completion.py +329 -0
  86. code_puppy/command_line/prompt_toolkit_completion.py +463 -63
  87. code_puppy/command_line/session_commands.py +296 -0
  88. code_puppy/command_line/utils.py +54 -0
  89. code_puppy/config.py +898 -112
  90. code_puppy/error_logging.py +118 -0
  91. code_puppy/gemini_code_assist.py +385 -0
  92. code_puppy/gemini_model.py +602 -0
  93. code_puppy/http_utils.py +210 -148
  94. code_puppy/keymap.py +128 -0
  95. code_puppy/main.py +5 -698
  96. code_puppy/mcp_/__init__.py +17 -0
  97. code_puppy/mcp_/async_lifecycle.py +35 -4
  98. code_puppy/mcp_/blocking_startup.py +70 -43
  99. code_puppy/mcp_/captured_stdio_server.py +2 -2
  100. code_puppy/mcp_/config_wizard.py +4 -4
  101. code_puppy/mcp_/dashboard.py +15 -6
  102. code_puppy/mcp_/managed_server.py +65 -38
  103. code_puppy/mcp_/manager.py +146 -52
  104. code_puppy/mcp_/mcp_logs.py +224 -0
  105. code_puppy/mcp_/registry.py +6 -6
  106. code_puppy/mcp_/server_registry_catalog.py +24 -5
  107. code_puppy/messaging/__init__.py +199 -2
  108. code_puppy/messaging/bus.py +610 -0
  109. code_puppy/messaging/commands.py +167 -0
  110. code_puppy/messaging/markdown_patches.py +57 -0
  111. code_puppy/messaging/message_queue.py +17 -48
  112. code_puppy/messaging/messages.py +500 -0
  113. code_puppy/messaging/queue_console.py +1 -24
  114. code_puppy/messaging/renderers.py +43 -146
  115. code_puppy/messaging/rich_renderer.py +1027 -0
  116. code_puppy/messaging/spinner/__init__.py +21 -5
  117. code_puppy/messaging/spinner/console_spinner.py +86 -51
  118. code_puppy/messaging/subagent_console.py +461 -0
  119. code_puppy/model_factory.py +634 -83
  120. code_puppy/model_utils.py +167 -0
  121. code_puppy/models.json +66 -68
  122. code_puppy/models_dev_api.json +1 -0
  123. code_puppy/models_dev_parser.py +592 -0
  124. code_puppy/plugins/__init__.py +164 -10
  125. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  126. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  127. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  128. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  129. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  130. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  131. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  132. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  133. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  134. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  135. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  136. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  137. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  138. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  139. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  140. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  141. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  142. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  143. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  144. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  145. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  146. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  147. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  148. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  149. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  150. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  151. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  152. code_puppy/plugins/example_custom_command/README.md +280 -0
  153. code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
  154. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  155. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  156. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  157. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  158. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  159. code_puppy/plugins/oauth_puppy_html.py +228 -0
  160. code_puppy/plugins/shell_safety/__init__.py +6 -0
  161. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  162. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  163. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  164. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  165. code_puppy/prompts/codex_system_prompt.md +310 -0
  166. code_puppy/pydantic_patches.py +131 -0
  167. code_puppy/reopenable_async_client.py +8 -8
  168. code_puppy/round_robin_model.py +9 -12
  169. code_puppy/session_storage.py +2 -1
  170. code_puppy/status_display.py +21 -4
  171. code_puppy/summarization_agent.py +41 -13
  172. code_puppy/terminal_utils.py +418 -0
  173. code_puppy/tools/__init__.py +37 -1
  174. code_puppy/tools/agent_tools.py +536 -52
  175. code_puppy/tools/browser/__init__.py +37 -0
  176. code_puppy/tools/browser/browser_control.py +19 -23
  177. code_puppy/tools/browser/browser_interactions.py +41 -48
  178. code_puppy/tools/browser/browser_locators.py +36 -38
  179. code_puppy/tools/browser/browser_manager.py +316 -0
  180. code_puppy/tools/browser/browser_navigation.py +16 -16
  181. code_puppy/tools/browser/browser_screenshot.py +79 -143
  182. code_puppy/tools/browser/browser_scripts.py +32 -42
  183. code_puppy/tools/browser/browser_workflows.py +44 -27
  184. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  185. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  186. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  187. code_puppy/tools/browser/terminal_tools.py +525 -0
  188. code_puppy/tools/command_runner.py +930 -147
  189. code_puppy/tools/common.py +1113 -5
  190. code_puppy/tools/display.py +84 -0
  191. code_puppy/tools/file_modifications.py +288 -89
  192. code_puppy/tools/file_operations.py +226 -154
  193. code_puppy/tools/subagent_context.py +158 -0
  194. code_puppy/uvx_detection.py +242 -0
  195. code_puppy/version_checker.py +30 -11
  196. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  197. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  198. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
  199. code_puppy-0.0.366.dist-info/RECORD +217 -0
  200. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  201. code_puppy/command_line/mcp/add_command.py +0 -183
  202. code_puppy/messaging/spinner/textual_spinner.py +0 -106
  203. code_puppy/tools/browser/camoufox_manager.py +0 -216
  204. code_puppy/tools/browser/vqa_agent.py +0 -70
  205. code_puppy/tui/__init__.py +0 -10
  206. code_puppy/tui/app.py +0 -1105
  207. code_puppy/tui/components/__init__.py +0 -21
  208. code_puppy/tui/components/chat_view.py +0 -551
  209. code_puppy/tui/components/command_history_modal.py +0 -218
  210. code_puppy/tui/components/copy_button.py +0 -139
  211. code_puppy/tui/components/custom_widgets.py +0 -63
  212. code_puppy/tui/components/human_input_modal.py +0 -175
  213. code_puppy/tui/components/input_area.py +0 -167
  214. code_puppy/tui/components/sidebar.py +0 -309
  215. code_puppy/tui/components/status_bar.py +0 -185
  216. code_puppy/tui/messages.py +0 -27
  217. code_puppy/tui/models/__init__.py +0 -8
  218. code_puppy/tui/models/chat_message.py +0 -25
  219. code_puppy/tui/models/command_history.py +0 -89
  220. code_puppy/tui/models/enums.py +0 -24
  221. code_puppy/tui/screens/__init__.py +0 -17
  222. code_puppy/tui/screens/autosave_picker.py +0 -175
  223. code_puppy/tui/screens/help.py +0 -130
  224. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  225. code_puppy/tui/screens/settings.py +0 -306
  226. code_puppy/tui/screens/tools.py +0 -74
  227. code_puppy/tui_state.py +0 -55
  228. code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
  229. code_puppy-0.0.214.dist-info/RECORD +0 -131
  230. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
  231. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,42 @@
1
+ """Configuration for the Antigravity OAuth plugin."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Dict
5
+
6
+ from code_puppy import config
7
+
8
+ # Antigravity OAuth configuration
9
+ ANTIGRAVITY_OAUTH_CONFIG: Dict[str, Any] = {
10
+ # OAuth endpoints
11
+ "auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
12
+ "token_url": "https://oauth2.googleapis.com/token",
13
+ # Callback handling
14
+ "redirect_host": "http://localhost",
15
+ "redirect_path": "oauth-callback",
16
+ "callback_port_range": (51121, 51150),
17
+ "callback_timeout": 180,
18
+ # Model configuration
19
+ "prefix": "antigravity-",
20
+ "default_context_length": 200000,
21
+ }
22
+
23
+
24
+ def get_token_storage_path() -> Path:
25
+ """Get the path for storing OAuth tokens."""
26
+ data_dir = Path(config.DATA_DIR)
27
+ data_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
28
+ return data_dir / "antigravity_oauth.json"
29
+
30
+
31
+ def get_accounts_storage_path() -> Path:
32
+ """Get the path for storing multi-account data."""
33
+ data_dir = Path(config.DATA_DIR)
34
+ data_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
35
+ return data_dir / "antigravity_accounts.json"
36
+
37
+
38
+ def get_antigravity_models_path() -> Path:
39
+ """Get the path to the antigravity_models.json file."""
40
+ data_dir = Path(config.DATA_DIR)
41
+ data_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
42
+ return data_dir / "antigravity_models.json"
@@ -0,0 +1,136 @@
1
+ """Constants for Antigravity OAuth flows and Cloud Code Assist API integration."""
2
+
3
+ from typing import Any, Dict, List
4
+
5
+ # OAuth client credentials (from Antigravity/Google IDE)
6
+ ANTIGRAVITY_CLIENT_ID = (
7
+ "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
8
+ )
9
+ ANTIGRAVITY_CLIENT_SECRET = "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"
10
+
11
+ # OAuth scopes required for Antigravity integrations
12
+ ANTIGRAVITY_SCOPES: List[str] = [
13
+ "https://www.googleapis.com/auth/cloud-platform",
14
+ "https://www.googleapis.com/auth/userinfo.email",
15
+ "https://www.googleapis.com/auth/userinfo.profile",
16
+ "https://www.googleapis.com/auth/cclog",
17
+ "https://www.googleapis.com/auth/experimentsandconfigs",
18
+ ]
19
+
20
+ # OAuth redirect URI for local CLI callback server
21
+ ANTIGRAVITY_REDIRECT_URI = "http://localhost:51121/oauth-callback"
22
+
23
+ # API endpoints (in fallback order: daily → autopush → prod)
24
+ ANTIGRAVITY_ENDPOINT_DAILY = "https://daily-cloudcode-pa.sandbox.googleapis.com"
25
+ ANTIGRAVITY_ENDPOINT_AUTOPUSH = "https://autopush-cloudcode-pa.sandbox.googleapis.com"
26
+ ANTIGRAVITY_ENDPOINT_PROD = "https://cloudcode-pa.googleapis.com"
27
+
28
+ ANTIGRAVITY_ENDPOINT_FALLBACKS = [
29
+ ANTIGRAVITY_ENDPOINT_DAILY,
30
+ ANTIGRAVITY_ENDPOINT_AUTOPUSH,
31
+ ANTIGRAVITY_ENDPOINT_PROD,
32
+ ]
33
+
34
+ # Preferred endpoint order for project discovery
35
+ ANTIGRAVITY_LOAD_ENDPOINTS = [
36
+ ANTIGRAVITY_ENDPOINT_PROD,
37
+ ANTIGRAVITY_ENDPOINT_DAILY,
38
+ ANTIGRAVITY_ENDPOINT_AUTOPUSH,
39
+ ]
40
+
41
+ # Primary endpoint (daily sandbox)
42
+ ANTIGRAVITY_ENDPOINT = ANTIGRAVITY_ENDPOINT_DAILY
43
+
44
+ # Default project ID fallback
45
+ ANTIGRAVITY_DEFAULT_PROJECT_ID = "rising-fact-p41fc"
46
+
47
+ # Request headers for Antigravity API
48
+ ANTIGRAVITY_HEADERS: Dict[str, str] = {
49
+ "User-Agent": "antigravity/1.11.5 windows/amd64",
50
+ "X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
51
+ "Client-Metadata": '{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}',
52
+ "x-goog-api-key": "", # Must be present but empty for Antigravity
53
+ }
54
+
55
+ # Request headers for Gemini CLI fallback
56
+ GEMINI_CLI_HEADERS: Dict[str, str] = {
57
+ "User-Agent": "google-api-nodejs-client/9.15.1",
58
+ "X-Goog-Api-Client": "gl-node/22.17.0",
59
+ "Client-Metadata": "ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI",
60
+ }
61
+
62
+ # Provider identifier
63
+ ANTIGRAVITY_PROVIDER_ID = "google"
64
+
65
+ # Available models with their configurations
66
+ ANTIGRAVITY_MODELS: Dict[str, Dict[str, Any]] = {
67
+ # Gemini models
68
+ "gemini-3-pro-low": {
69
+ "name": "Gemini 3 Pro Low (Antigravity)",
70
+ "family": "gemini",
71
+ "context_length": 1048576,
72
+ "max_output": 65535,
73
+ },
74
+ "gemini-3-pro-high": {
75
+ "name": "Gemini 3 Pro High (Antigravity)",
76
+ "family": "gemini",
77
+ "context_length": 1048576,
78
+ "max_output": 65535,
79
+ },
80
+ "gemini-3-flash": {
81
+ "name": "Gemini 3 Flash (Antigravity)",
82
+ "family": "gemini",
83
+ "context_length": 1048576,
84
+ "max_output": 65536,
85
+ },
86
+ # Claude models (non-thinking)
87
+ "claude-sonnet-4-5": {
88
+ "name": "Claude Sonnet 4.5 (Antigravity)",
89
+ "family": "claude",
90
+ "context_length": 200000,
91
+ "max_output": 64000,
92
+ },
93
+ # Claude thinking models
94
+ "claude-sonnet-4-5-thinking-low": {
95
+ "name": "Claude Sonnet 4.5 Thinking Low (Antigravity)",
96
+ "family": "claude",
97
+ "context_length": 200000,
98
+ "max_output": 64000,
99
+ "thinking_budget": 8192,
100
+ },
101
+ "claude-sonnet-4-5-thinking-medium": {
102
+ "name": "Claude Sonnet 4.5 Thinking Medium (Antigravity)",
103
+ "family": "claude",
104
+ "context_length": 200000,
105
+ "max_output": 64000,
106
+ "thinking_budget": 16384,
107
+ },
108
+ "claude-sonnet-4-5-thinking-high": {
109
+ "name": "Claude Sonnet 4.5 Thinking High (Antigravity)",
110
+ "family": "claude",
111
+ "context_length": 200000,
112
+ "max_output": 64000,
113
+ "thinking_budget": 32768,
114
+ },
115
+ "claude-opus-4-5-thinking-low": {
116
+ "name": "Claude Opus 4.5 Thinking Low (Antigravity)",
117
+ "family": "claude",
118
+ "context_length": 200000,
119
+ "max_output": 64000,
120
+ "thinking_budget": 8192,
121
+ },
122
+ "claude-opus-4-5-thinking-medium": {
123
+ "name": "Claude Opus 4.5 Thinking Medium (Antigravity)",
124
+ "family": "claude",
125
+ "context_length": 200000,
126
+ "max_output": 64000,
127
+ "thinking_budget": 16384,
128
+ },
129
+ "claude-opus-4-5-thinking-high": {
130
+ "name": "Claude Opus 4.5 Thinking High (Antigravity)",
131
+ "family": "claude",
132
+ "context_length": 200000,
133
+ "max_output": 64000,
134
+ "thinking_budget": 32768,
135
+ },
136
+ }
@@ -0,0 +1,478 @@
1
+ """Core OAuth flow implementation for Antigravity authentication."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+ import json
8
+ import logging
9
+ import secrets
10
+ import time
11
+ from dataclasses import dataclass, field
12
+ from typing import List, Optional
13
+ from urllib.parse import urlencode
14
+
15
+ import requests
16
+
17
+ from .constants import (
18
+ ANTIGRAVITY_CLIENT_ID,
19
+ ANTIGRAVITY_CLIENT_SECRET,
20
+ ANTIGRAVITY_ENDPOINT_FALLBACKS,
21
+ ANTIGRAVITY_HEADERS,
22
+ ANTIGRAVITY_LOAD_ENDPOINTS,
23
+ ANTIGRAVITY_SCOPES,
24
+ )
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ @dataclass
30
+ class OAuthContext:
31
+ """Runtime state for an in-progress OAuth flow."""
32
+
33
+ state: str
34
+ code_verifier: str
35
+ code_challenge: str
36
+ redirect_uri: Optional[str] = None
37
+
38
+
39
+ @dataclass
40
+ class AntigravityAuthorization:
41
+ """Result returned after constructing an OAuth authorization URL."""
42
+
43
+ url: str
44
+ verifier: str
45
+ project_id: str
46
+
47
+
48
+ @dataclass
49
+ class TokenExchangeSuccess:
50
+ """Successful token exchange result."""
51
+
52
+ refresh_token: str
53
+ access_token: str
54
+ expires_at: float # Unix timestamp
55
+ email: Optional[str]
56
+ project_id: str
57
+
58
+
59
+ @dataclass
60
+ class TokenExchangeFailure:
61
+ """Failed token exchange result."""
62
+
63
+ error: str
64
+
65
+
66
+ TokenExchangeResult = TokenExchangeSuccess | TokenExchangeFailure
67
+
68
+
69
+ def _urlsafe_b64encode(data: bytes) -> str:
70
+ """Encode bytes to URL-safe base64 without padding."""
71
+ return base64.urlsafe_b64encode(data).decode("utf-8").rstrip("=")
72
+
73
+
74
+ def _generate_code_verifier() -> str:
75
+ """Generate a cryptographically secure code verifier for PKCE."""
76
+ return _urlsafe_b64encode(secrets.token_bytes(64))
77
+
78
+
79
+ def _compute_code_challenge(code_verifier: str) -> str:
80
+ """Compute the S256 code challenge from the verifier."""
81
+ digest = hashlib.sha256(code_verifier.encode("utf-8")).digest()
82
+ return _urlsafe_b64encode(digest)
83
+
84
+
85
+ def _encode_state(verifier: str, project_id: str = "") -> str:
86
+ """Encode OAuth state as URL-safe base64."""
87
+ payload = {"verifier": verifier, "projectId": project_id}
88
+ return (
89
+ base64.urlsafe_b64encode(json.dumps(payload).encode("utf-8"))
90
+ .decode("utf-8")
91
+ .rstrip("=")
92
+ )
93
+
94
+
95
+ def _decode_state(state: str) -> tuple[str, str]:
96
+ """Decode OAuth state back to verifier and project ID."""
97
+ # Normalize base64 encoding
98
+ normalized = state.replace("-", "+").replace("_", "/")
99
+ padding = (4 - len(normalized) % 4) % 4
100
+ padded = normalized + "=" * padding
101
+
102
+ try:
103
+ json_str = base64.b64decode(padded).decode("utf-8")
104
+ parsed = json.loads(json_str)
105
+
106
+ verifier = parsed.get("verifier", "")
107
+ if not isinstance(verifier, str) or not verifier:
108
+ raise ValueError("Missing PKCE verifier in state")
109
+
110
+ project_id = parsed.get("projectId", "")
111
+ if not isinstance(project_id, str):
112
+ project_id = ""
113
+
114
+ return verifier, project_id
115
+ except Exception as e:
116
+ logger.error("Failed to decode OAuth state: %s", e)
117
+ raise ValueError(f"Invalid OAuth state: {e}") from e
118
+
119
+
120
+ def prepare_oauth_context() -> OAuthContext:
121
+ """Create a new OAuth PKCE context."""
122
+ state = secrets.token_urlsafe(32)
123
+ code_verifier = _generate_code_verifier()
124
+ code_challenge = _compute_code_challenge(code_verifier)
125
+
126
+ return OAuthContext(
127
+ state=state,
128
+ code_verifier=code_verifier,
129
+ code_challenge=code_challenge,
130
+ )
131
+
132
+
133
+ def assign_redirect_uri(context: OAuthContext, port: int) -> str:
134
+ """Assign redirect URI for the given OAuth context."""
135
+ redirect_uri = f"http://localhost:{port}/oauth-callback"
136
+ context.redirect_uri = redirect_uri
137
+ return redirect_uri
138
+
139
+
140
+ def build_authorization_url(context: OAuthContext, project_id: str = "") -> str:
141
+ """Build the Google OAuth authorization URL with PKCE parameters."""
142
+ if not context.redirect_uri:
143
+ raise RuntimeError("Redirect URI has not been assigned")
144
+
145
+ # Encode state with verifier for callback verification
146
+ state = _encode_state(context.code_verifier, project_id)
147
+
148
+ params = {
149
+ "client_id": ANTIGRAVITY_CLIENT_ID,
150
+ "response_type": "code",
151
+ "redirect_uri": context.redirect_uri,
152
+ "scope": " ".join(ANTIGRAVITY_SCOPES),
153
+ "code_challenge": context.code_challenge,
154
+ "code_challenge_method": "S256",
155
+ "state": state,
156
+ "access_type": "offline",
157
+ "prompt": "consent",
158
+ }
159
+
160
+ return f"https://accounts.google.com/o/oauth2/v2/auth?{urlencode(params)}"
161
+
162
+
163
+ def _onboard_user(
164
+ access_token: str,
165
+ tier_id: str = "free-tier",
166
+ gcp_project_id: str = "",
167
+ ) -> str:
168
+ """Onboard user to get a managed project ID.
169
+
170
+ Args:
171
+ access_token: OAuth access token
172
+ tier_id: Tier to onboard with ("free-tier" or "standard-tier")
173
+ gcp_project_id: Required for standard-tier - user's GCP project ID
174
+ """
175
+ headers = {
176
+ "Authorization": f"Bearer {access_token}",
177
+ "Content-Type": "application/json",
178
+ **ANTIGRAVITY_HEADERS,
179
+ }
180
+
181
+ request_body: dict = {
182
+ "tierId": tier_id,
183
+ "metadata": {
184
+ "ideType": "IDE_UNSPECIFIED",
185
+ "platform": "PLATFORM_UNSPECIFIED",
186
+ "pluginType": "GEMINI",
187
+ },
188
+ }
189
+
190
+ # For standard tier, add the user's GCP project ID
191
+ if tier_id == "standard-tier" and gcp_project_id:
192
+ request_body["cloudaicompanionProject"] = {"id": gcp_project_id}
193
+
194
+ for base_endpoint in ANTIGRAVITY_ENDPOINT_FALLBACKS:
195
+ for attempt in range(5): # Retry up to 5 times
196
+ try:
197
+ url = f"{base_endpoint}/v1internal:onboardUser"
198
+ response = requests.post(
199
+ url,
200
+ headers=headers,
201
+ json=request_body,
202
+ timeout=30,
203
+ )
204
+
205
+ if not response.ok:
206
+ logger.debug(
207
+ "onboardUser failed: %d %s",
208
+ response.status_code,
209
+ response.text[:200],
210
+ )
211
+ break
212
+
213
+ data = response.json()
214
+
215
+ # Check if onboarding is complete
216
+ if data.get("done"):
217
+ project_id = (
218
+ data.get("response", {})
219
+ .get("cloudaicompanionProject", {})
220
+ .get("id")
221
+ )
222
+ if project_id:
223
+ logger.debug("Onboarding complete, project_id: %s", project_id)
224
+ return project_id
225
+
226
+ # Wait and retry if not done
227
+ import time
228
+
229
+ time.sleep(3)
230
+
231
+ except Exception as e:
232
+ logger.debug("onboardUser error: %s", e)
233
+ break
234
+
235
+ return ""
236
+
237
+
238
+ @dataclass
239
+ class AntigravityStatus:
240
+ """Status information from Antigravity API."""
241
+
242
+ project_id: str = ""
243
+ current_tier: str = ""
244
+ allowed_tiers: List[str] = field(default_factory=list)
245
+ is_onboarded: bool = False
246
+ error: Optional[str] = None
247
+
248
+
249
+ def fetch_antigravity_status(access_token: str) -> AntigravityStatus:
250
+ """Fetch full status from Antigravity loadCodeAssist API."""
251
+ headers = {
252
+ "Authorization": f"Bearer {access_token}",
253
+ "Content-Type": "application/json",
254
+ "User-Agent": "google-api-nodejs-client/9.15.1",
255
+ "X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
256
+ "Client-Metadata": ANTIGRAVITY_HEADERS["Client-Metadata"],
257
+ }
258
+
259
+ endpoints = list(
260
+ dict.fromkeys(ANTIGRAVITY_LOAD_ENDPOINTS + list(ANTIGRAVITY_ENDPOINT_FALLBACKS))
261
+ )
262
+
263
+ for base_endpoint in endpoints:
264
+ try:
265
+ url = f"{base_endpoint}/v1internal:loadCodeAssist"
266
+ response = requests.post(
267
+ url,
268
+ headers=headers,
269
+ json={
270
+ "metadata": {
271
+ "ideType": "IDE_UNSPECIFIED",
272
+ "platform": "PLATFORM_UNSPECIFIED",
273
+ "pluginType": "GEMINI",
274
+ }
275
+ },
276
+ timeout=30,
277
+ )
278
+
279
+ if not response.ok:
280
+ continue
281
+
282
+ data = response.json()
283
+
284
+ # Extract project info
285
+ project_id = ""
286
+ project = data.get("cloudaicompanionProject")
287
+ if isinstance(project, str) and project:
288
+ project_id = project
289
+ elif isinstance(project, dict) and project.get("id"):
290
+ project_id = project["id"]
291
+
292
+ # Extract tier info
293
+ allowed_tiers_data = data.get("allowedTiers", [])
294
+ allowed_tier_ids = [
295
+ t.get("id", "") for t in allowed_tiers_data if t.get("id")
296
+ ]
297
+
298
+ # Find current tier (the one marked as default or the one with project)
299
+ current_tier = ""
300
+ for tier in allowed_tiers_data:
301
+ if tier.get("isDefault"):
302
+ current_tier = tier.get("id", "")
303
+ break
304
+ # If project exists and tier doesn't require user-defined project, it's likely current
305
+ if project_id and not tier.get("userDefinedCloudaicompanionProject"):
306
+ current_tier = tier.get("id", "")
307
+
308
+ return AntigravityStatus(
309
+ project_id=project_id,
310
+ current_tier=current_tier,
311
+ allowed_tiers=allowed_tier_ids,
312
+ is_onboarded=bool(project_id),
313
+ )
314
+
315
+ except Exception:
316
+ continue
317
+
318
+ return AntigravityStatus(error="Could not reach Antigravity API")
319
+
320
+
321
+ def _fetch_project_id(access_token: str) -> str:
322
+ """Fetch project ID from Antigravity loadCodeAssist API."""
323
+ errors: List[str] = []
324
+
325
+ headers = {
326
+ "Authorization": f"Bearer {access_token}",
327
+ "Content-Type": "application/json",
328
+ "User-Agent": "google-api-nodejs-client/9.15.1",
329
+ "X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
330
+ "Client-Metadata": ANTIGRAVITY_HEADERS["Client-Metadata"],
331
+ }
332
+
333
+ # Try each endpoint in order (deduplicated)
334
+ endpoints = list(
335
+ dict.fromkeys(ANTIGRAVITY_LOAD_ENDPOINTS + list(ANTIGRAVITY_ENDPOINT_FALLBACKS))
336
+ )
337
+
338
+ # First, try to get existing project from loadCodeAssist
339
+ allowed_tiers: List[dict] = []
340
+
341
+ for base_endpoint in endpoints:
342
+ try:
343
+ url = f"{base_endpoint}/v1internal:loadCodeAssist"
344
+ response = requests.post(
345
+ url,
346
+ headers=headers,
347
+ json={
348
+ "metadata": {
349
+ "ideType": "IDE_UNSPECIFIED",
350
+ "platform": "PLATFORM_UNSPECIFIED",
351
+ "pluginType": "GEMINI",
352
+ }
353
+ },
354
+ timeout=30,
355
+ )
356
+
357
+ if not response.ok:
358
+ errors.append(
359
+ f"loadCodeAssist {response.status_code} at {base_endpoint}: "
360
+ f"{response.text[:200]}"
361
+ )
362
+ continue
363
+
364
+ data = response.json()
365
+
366
+ # Try to extract project ID from response
367
+ project = data.get("cloudaicompanionProject")
368
+
369
+ if isinstance(project, str) and project:
370
+ return project
371
+ if isinstance(project, dict) and project.get("id"):
372
+ return project["id"]
373
+
374
+ # Store allowed tiers for potential onboarding
375
+ if data.get("allowedTiers"):
376
+ allowed_tiers = data.get("allowedTiers", [])
377
+
378
+ errors.append(f"loadCodeAssist missing project id at {base_endpoint}")
379
+
380
+ except Exception as e:
381
+ errors.append(f"loadCodeAssist error at {base_endpoint}: {e}")
382
+
383
+ # No project found - try to onboard with free tier if available
384
+ if allowed_tiers:
385
+ # Find the default tier or free tier
386
+ default_tier = None
387
+ for tier in allowed_tiers:
388
+ if tier.get("isDefault"):
389
+ default_tier = tier
390
+ break
391
+ if tier.get("id") == "free-tier":
392
+ default_tier = tier
393
+
394
+ if default_tier and not default_tier.get("userDefinedCloudaicompanionProject"):
395
+ tier_id = default_tier.get("id", "free-tier")
396
+ logger.debug(
397
+ "No project found, attempting onboarding with tier: %s", tier_id
398
+ )
399
+ project_id = _onboard_user(access_token, tier_id)
400
+ if project_id:
401
+ return project_id
402
+
403
+ if errors:
404
+ logger.debug(
405
+ "Could not resolve Antigravity project (non-fatal): %s", "; ".join(errors)
406
+ )
407
+
408
+ return ""
409
+
410
+
411
+ def exchange_code_for_tokens(
412
+ code: str,
413
+ state: str,
414
+ redirect_uri: str,
415
+ ) -> TokenExchangeResult:
416
+ """Exchange an authorization code for Antigravity OAuth tokens."""
417
+ try:
418
+ # Decode and verify state
419
+ verifier, project_id = _decode_state(state)
420
+
421
+ # Exchange code for tokens
422
+ response = requests.post(
423
+ "https://oauth2.googleapis.com/token",
424
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
425
+ data={
426
+ "client_id": ANTIGRAVITY_CLIENT_ID,
427
+ "client_secret": ANTIGRAVITY_CLIENT_SECRET,
428
+ "code": code,
429
+ "grant_type": "authorization_code",
430
+ "redirect_uri": redirect_uri,
431
+ "code_verifier": verifier,
432
+ },
433
+ timeout=30,
434
+ )
435
+
436
+ if not response.ok:
437
+ return TokenExchangeFailure(error=response.text)
438
+
439
+ token_data = response.json()
440
+ access_token = token_data.get("access_token", "")
441
+ refresh_token = token_data.get("refresh_token", "")
442
+ expires_in = token_data.get("expires_in", 3600)
443
+
444
+ if not refresh_token:
445
+ return TokenExchangeFailure(error="Missing refresh token in response")
446
+
447
+ # Fetch user email
448
+ email: Optional[str] = None
449
+ try:
450
+ user_response = requests.get(
451
+ "https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
452
+ headers={"Authorization": f"Bearer {access_token}"},
453
+ timeout=10,
454
+ )
455
+ if user_response.ok:
456
+ email = user_response.json().get("email")
457
+ except Exception as e:
458
+ logger.warning("Failed to fetch user email: %s", e)
459
+
460
+ # Try to get project ID if not provided
461
+ effective_project_id = project_id
462
+ if not effective_project_id:
463
+ effective_project_id = _fetch_project_id(access_token)
464
+
465
+ # Format refresh token with project ID
466
+ stored_refresh = f"{refresh_token}|{effective_project_id or ''}"
467
+
468
+ return TokenExchangeSuccess(
469
+ refresh_token=stored_refresh,
470
+ access_token=access_token,
471
+ expires_at=time.time() + expires_in,
472
+ email=email,
473
+ project_id=effective_project_id or "",
474
+ )
475
+
476
+ except Exception as e:
477
+ logger.exception("Token exchange failed")
478
+ return TokenExchangeFailure(error=str(e))