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,169 @@
|
|
|
1
|
+
"""Utility helpers for the Antigravity OAuth plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from .config import (
|
|
10
|
+
ANTIGRAVITY_OAUTH_CONFIG,
|
|
11
|
+
get_antigravity_models_path,
|
|
12
|
+
get_token_storage_path,
|
|
13
|
+
)
|
|
14
|
+
from .constants import ANTIGRAVITY_ENDPOINT, ANTIGRAVITY_HEADERS, ANTIGRAVITY_MODELS
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_stored_tokens() -> Optional[Dict[str, Any]]:
|
|
20
|
+
"""Load stored OAuth tokens from disk."""
|
|
21
|
+
try:
|
|
22
|
+
token_path = get_token_storage_path()
|
|
23
|
+
if token_path.exists():
|
|
24
|
+
with open(token_path, "r", encoding="utf-8") as f:
|
|
25
|
+
return json.load(f)
|
|
26
|
+
except Exception as e:
|
|
27
|
+
logger.error("Failed to load tokens: %s", e)
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def save_tokens(tokens: Dict[str, Any]) -> bool:
|
|
32
|
+
"""Save OAuth tokens to disk."""
|
|
33
|
+
try:
|
|
34
|
+
token_path = get_token_storage_path()
|
|
35
|
+
with open(token_path, "w", encoding="utf-8") as f:
|
|
36
|
+
json.dump(tokens, f, indent=2)
|
|
37
|
+
token_path.chmod(0o600)
|
|
38
|
+
return True
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.error("Failed to save tokens: %s", e)
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_antigravity_models() -> Dict[str, Any]:
|
|
45
|
+
"""Load configured Antigravity models from disk."""
|
|
46
|
+
try:
|
|
47
|
+
models_path = get_antigravity_models_path()
|
|
48
|
+
if models_path.exists():
|
|
49
|
+
with open(models_path, "r", encoding="utf-8") as f:
|
|
50
|
+
return json.load(f)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logger.error("Failed to load Antigravity models: %s", e)
|
|
53
|
+
return {}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def save_antigravity_models(models: Dict[str, Any]) -> bool:
|
|
57
|
+
"""Save Antigravity models configuration to disk."""
|
|
58
|
+
try:
|
|
59
|
+
models_path = get_antigravity_models_path()
|
|
60
|
+
with open(models_path, "w", encoding="utf-8") as f:
|
|
61
|
+
json.dump(models, f, indent=2)
|
|
62
|
+
return True
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.error("Failed to save Antigravity models: %s", e)
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def add_models_to_config(access_token: str, project_id: str = "") -> bool:
|
|
69
|
+
"""Add all available Antigravity models to the configuration."""
|
|
70
|
+
try:
|
|
71
|
+
models_config: Dict[str, Any] = {}
|
|
72
|
+
prefix = ANTIGRAVITY_OAUTH_CONFIG["prefix"]
|
|
73
|
+
|
|
74
|
+
for model_id, model_info in ANTIGRAVITY_MODELS.items():
|
|
75
|
+
prefixed_name = f"{prefix}{model_id}"
|
|
76
|
+
|
|
77
|
+
# Build custom headers
|
|
78
|
+
headers = dict(ANTIGRAVITY_HEADERS)
|
|
79
|
+
|
|
80
|
+
# Use custom_gemini type with Antigravity transport
|
|
81
|
+
models_config[prefixed_name] = {
|
|
82
|
+
"type": "custom_gemini",
|
|
83
|
+
"name": model_id,
|
|
84
|
+
"custom_endpoint": {
|
|
85
|
+
"url": ANTIGRAVITY_ENDPOINT,
|
|
86
|
+
"api_key": access_token,
|
|
87
|
+
"headers": headers,
|
|
88
|
+
},
|
|
89
|
+
"project_id": project_id,
|
|
90
|
+
"context_length": model_info.get("context_length", 200000),
|
|
91
|
+
"family": model_info.get("family", "other"),
|
|
92
|
+
"oauth_source": "antigravity-plugin",
|
|
93
|
+
"antigravity": True, # Flag to use Antigravity transport
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Add thinking budget if present
|
|
97
|
+
if model_info.get("thinking_budget"):
|
|
98
|
+
models_config[prefixed_name]["thinking_budget"] = model_info[
|
|
99
|
+
"thinking_budget"
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
if save_antigravity_models(models_config):
|
|
103
|
+
logger.info("Added %d Antigravity models", len(models_config))
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.error("Error adding models to config: %s", e)
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def remove_antigravity_models() -> int:
|
|
112
|
+
"""Remove all Antigravity models from configuration."""
|
|
113
|
+
try:
|
|
114
|
+
models = load_antigravity_models()
|
|
115
|
+
to_remove = [
|
|
116
|
+
name
|
|
117
|
+
for name, config in models.items()
|
|
118
|
+
if config.get("oauth_source") == "antigravity-plugin"
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
if not to_remove:
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
for model_name in to_remove:
|
|
125
|
+
models.pop(model_name, None)
|
|
126
|
+
|
|
127
|
+
if save_antigravity_models(models):
|
|
128
|
+
return len(to_remove)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.error("Error removing Antigravity models: %s", e)
|
|
131
|
+
return 0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_model_families_summary() -> Dict[str, List[str]]:
|
|
135
|
+
"""Get a summary of available models by family."""
|
|
136
|
+
families: Dict[str, List[str]] = {
|
|
137
|
+
"gemini": [],
|
|
138
|
+
"claude": [],
|
|
139
|
+
"other": [],
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for model_id, info in ANTIGRAVITY_MODELS.items():
|
|
143
|
+
family = info.get("family", "other")
|
|
144
|
+
if family in families:
|
|
145
|
+
families[family].append(model_id)
|
|
146
|
+
|
|
147
|
+
return families
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def reload_current_agent() -> None:
|
|
151
|
+
"""Reload the current agent so new auth tokens are picked up immediately."""
|
|
152
|
+
try:
|
|
153
|
+
from code_puppy.agents import get_current_agent
|
|
154
|
+
|
|
155
|
+
current_agent = get_current_agent()
|
|
156
|
+
if current_agent is None:
|
|
157
|
+
logger.debug("No current agent to reload")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
if hasattr(current_agent, "refresh_config"):
|
|
161
|
+
try:
|
|
162
|
+
current_agent.refresh_config()
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
current_agent.reload_code_generation_agent()
|
|
167
|
+
logger.info("Active agent reloaded with new authentication")
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.warning("Agent reload failed: %s", e)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
from code_puppy import config
|
|
5
|
+
|
|
6
|
+
# ChatGPT OAuth configuration based on OpenAI's Codex CLI flow
|
|
7
|
+
CHATGPT_OAUTH_CONFIG: Dict[str, Any] = {
|
|
8
|
+
# OAuth endpoints from OpenAI auth service
|
|
9
|
+
"issuer": "https://auth.openai.com",
|
|
10
|
+
"auth_url": "https://auth.openai.com/oauth/authorize",
|
|
11
|
+
"token_url": "https://auth.openai.com/oauth/token",
|
|
12
|
+
# API endpoints - Codex uses chatgpt.com backend, not api.openai.com
|
|
13
|
+
"api_base_url": "https://chatgpt.com/backend-api/codex",
|
|
14
|
+
# OAuth client configuration for Code Puppy
|
|
15
|
+
"client_id": "app_EMoamEEZ73f0CkXaXp7hrann",
|
|
16
|
+
"scope": "openid profile email offline_access",
|
|
17
|
+
# Callback handling (we host a localhost callback to capture the redirect)
|
|
18
|
+
"redirect_host": "http://localhost",
|
|
19
|
+
"redirect_path": "auth/callback",
|
|
20
|
+
"required_port": 1455,
|
|
21
|
+
"callback_timeout": 120,
|
|
22
|
+
# Local configuration (uses XDG_DATA_HOME)
|
|
23
|
+
"token_storage": None, # Set dynamically in get_token_storage_path()
|
|
24
|
+
# Model configuration
|
|
25
|
+
"prefix": "chatgpt-",
|
|
26
|
+
"default_context_length": 272000,
|
|
27
|
+
"api_key_env_var": "CHATGPT_OAUTH_API_KEY",
|
|
28
|
+
# Codex CLI version info (for User-Agent header)
|
|
29
|
+
"client_version": "0.72.0",
|
|
30
|
+
"originator": "codex_cli_rs",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_token_storage_path() -> Path:
|
|
35
|
+
"""Get the path for storing OAuth tokens (uses XDG_DATA_HOME)."""
|
|
36
|
+
data_dir = Path(config.DATA_DIR)
|
|
37
|
+
data_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
38
|
+
return data_dir / "chatgpt_oauth.json"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_config_dir() -> Path:
|
|
42
|
+
"""Get the Code Puppy configuration directory (uses XDG_CONFIG_HOME)."""
|
|
43
|
+
config_dir = Path(config.CONFIG_DIR)
|
|
44
|
+
config_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
45
|
+
return config_dir
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_chatgpt_models_path() -> Path:
|
|
49
|
+
"""Get the path to the dedicated chatgpt_models.json file (uses XDG_DATA_HOME)."""
|
|
50
|
+
data_dir = Path(config.DATA_DIR)
|
|
51
|
+
data_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
52
|
+
return data_dir / "chatgpt_models.json"
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""ChatGPT OAuth flow closely matching the ChatMock implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
import urllib.parse
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
11
|
+
from typing import Any, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
|
|
15
|
+
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
16
|
+
|
|
17
|
+
from ..oauth_puppy_html import oauth_failure_html, oauth_success_html
|
|
18
|
+
from .config import CHATGPT_OAUTH_CONFIG
|
|
19
|
+
from .utils import (
|
|
20
|
+
add_models_to_extra_config,
|
|
21
|
+
assign_redirect_uri,
|
|
22
|
+
load_stored_tokens,
|
|
23
|
+
parse_jwt_claims,
|
|
24
|
+
prepare_oauth_context,
|
|
25
|
+
save_tokens,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
REQUIRED_PORT = CHATGPT_OAUTH_CONFIG["required_port"]
|
|
29
|
+
URL_BASE = f"http://localhost:{REQUIRED_PORT}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class TokenData:
|
|
34
|
+
id_token: str
|
|
35
|
+
access_token: str
|
|
36
|
+
refresh_token: str
|
|
37
|
+
account_id: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class AuthBundle:
|
|
42
|
+
api_key: Optional[str]
|
|
43
|
+
token_data: TokenData
|
|
44
|
+
last_refresh: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _OAuthServer(HTTPServer):
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
*,
|
|
51
|
+
client_id: str,
|
|
52
|
+
verbose: bool = False,
|
|
53
|
+
) -> None:
|
|
54
|
+
super().__init__(
|
|
55
|
+
("localhost", REQUIRED_PORT), _CallbackHandler, bind_and_activate=True
|
|
56
|
+
)
|
|
57
|
+
self.exit_code = 1
|
|
58
|
+
self.verbose = verbose
|
|
59
|
+
self.client_id = client_id
|
|
60
|
+
self.issuer = CHATGPT_OAUTH_CONFIG["issuer"]
|
|
61
|
+
self.token_endpoint = CHATGPT_OAUTH_CONFIG["token_url"]
|
|
62
|
+
|
|
63
|
+
# Create fresh OAuth context for this server instance
|
|
64
|
+
context = prepare_oauth_context()
|
|
65
|
+
self.redirect_uri = assign_redirect_uri(context, REQUIRED_PORT)
|
|
66
|
+
self.context = context
|
|
67
|
+
|
|
68
|
+
def auth_url(self) -> str:
|
|
69
|
+
params = {
|
|
70
|
+
"response_type": "code",
|
|
71
|
+
"client_id": self.client_id,
|
|
72
|
+
"redirect_uri": self.redirect_uri,
|
|
73
|
+
"scope": CHATGPT_OAUTH_CONFIG["scope"],
|
|
74
|
+
"code_challenge": self.context.code_challenge,
|
|
75
|
+
"code_challenge_method": "S256",
|
|
76
|
+
"id_token_add_organizations": "true",
|
|
77
|
+
"codex_cli_simplified_flow": "true",
|
|
78
|
+
"state": self.context.state,
|
|
79
|
+
}
|
|
80
|
+
return f"{self.issuer}/oauth/authorize?" + urllib.parse.urlencode(params)
|
|
81
|
+
|
|
82
|
+
def exchange_code(self, code: str) -> Tuple[AuthBundle, str]:
|
|
83
|
+
data = {
|
|
84
|
+
"grant_type": "authorization_code",
|
|
85
|
+
"code": code,
|
|
86
|
+
"redirect_uri": self.redirect_uri,
|
|
87
|
+
"client_id": self.client_id,
|
|
88
|
+
"code_verifier": self.context.code_verifier,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
response = requests.post(
|
|
92
|
+
self.token_endpoint,
|
|
93
|
+
data=data,
|
|
94
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
95
|
+
timeout=30,
|
|
96
|
+
)
|
|
97
|
+
response.raise_for_status()
|
|
98
|
+
payload = response.json()
|
|
99
|
+
|
|
100
|
+
id_token = payload.get("id_token", "")
|
|
101
|
+
access_token = payload.get("access_token", "")
|
|
102
|
+
refresh_token = payload.get("refresh_token", "")
|
|
103
|
+
|
|
104
|
+
id_token_claims = parse_jwt_claims(id_token) or {}
|
|
105
|
+
access_token_claims = parse_jwt_claims(access_token) or {}
|
|
106
|
+
|
|
107
|
+
auth_claims = id_token_claims.get("https://api.openai.com/auth") or {}
|
|
108
|
+
chatgpt_account_id = auth_claims.get("chatgpt_account_id", "")
|
|
109
|
+
# Extract org_id from nested auth structure like ChatMock
|
|
110
|
+
organizations = auth_claims.get("organizations", [])
|
|
111
|
+
org_id = None
|
|
112
|
+
if organizations:
|
|
113
|
+
default_org = next(
|
|
114
|
+
(org for org in organizations if org.get("is_default")),
|
|
115
|
+
organizations[0],
|
|
116
|
+
)
|
|
117
|
+
org_id = default_org.get("id")
|
|
118
|
+
# Fallback to top-level org_id if still not found
|
|
119
|
+
if not org_id:
|
|
120
|
+
org_id = id_token_claims.get("organization_id")
|
|
121
|
+
|
|
122
|
+
token_data = TokenData(
|
|
123
|
+
id_token=id_token,
|
|
124
|
+
access_token=access_token,
|
|
125
|
+
refresh_token=refresh_token,
|
|
126
|
+
account_id=chatgpt_account_id,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Instead of exchanging for an API key, just use the access_token directly
|
|
130
|
+
# This matches how ChatMock works - no token exchange, just OAuth tokens
|
|
131
|
+
api_key = token_data.access_token
|
|
132
|
+
|
|
133
|
+
last_refresh = (
|
|
134
|
+
datetime.datetime.now(datetime.timezone.utc)
|
|
135
|
+
.isoformat()
|
|
136
|
+
.replace("+00:00", "Z")
|
|
137
|
+
)
|
|
138
|
+
bundle = AuthBundle(
|
|
139
|
+
api_key=api_key, token_data=token_data, last_refresh=last_refresh
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Build success URL with all the token info
|
|
143
|
+
success_query = {
|
|
144
|
+
"id_token": token_data.id_token,
|
|
145
|
+
"access_token": token_data.access_token,
|
|
146
|
+
"refresh_token": token_data.refresh_token,
|
|
147
|
+
"org_id": org_id or "",
|
|
148
|
+
"plan_type": access_token_claims.get("chatgpt_plan_type"),
|
|
149
|
+
"platform_url": "https://platform.openai.com",
|
|
150
|
+
}
|
|
151
|
+
success_url = f"{URL_BASE}/success?{urllib.parse.urlencode(success_query)}"
|
|
152
|
+
return bundle, success_url
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class _CallbackHandler(BaseHTTPRequestHandler):
|
|
156
|
+
server: "_OAuthServer"
|
|
157
|
+
|
|
158
|
+
def do_GET(self) -> None: # noqa: N802
|
|
159
|
+
path = urllib.parse.urlparse(self.path).path
|
|
160
|
+
if path == "/success":
|
|
161
|
+
success_html = oauth_success_html(
|
|
162
|
+
"ChatGPT",
|
|
163
|
+
"You can now close this window and return to Code Puppy.",
|
|
164
|
+
)
|
|
165
|
+
self._send_html(success_html)
|
|
166
|
+
self._shutdown_after_delay(2.0)
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
if path != "/auth/callback":
|
|
170
|
+
self._send_failure(404, "Callback endpoint not found for the puppy parade.")
|
|
171
|
+
self._shutdown()
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
query = urllib.parse.urlparse(self.path).query
|
|
175
|
+
params = urllib.parse.parse_qs(query)
|
|
176
|
+
|
|
177
|
+
code = params.get("code", [None])[0]
|
|
178
|
+
if not code:
|
|
179
|
+
self._send_failure(400, "Missing auth code — the token treat rolled away.")
|
|
180
|
+
self._shutdown()
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
auth_bundle, success_url = self.server.exchange_code(code)
|
|
185
|
+
except Exception as exc: # noqa: BLE001
|
|
186
|
+
self._send_failure(500, f"Token exchange failed: {exc}")
|
|
187
|
+
self._shutdown()
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
tokens = {
|
|
191
|
+
"id_token": auth_bundle.token_data.id_token,
|
|
192
|
+
"access_token": auth_bundle.token_data.access_token,
|
|
193
|
+
"refresh_token": auth_bundle.token_data.refresh_token,
|
|
194
|
+
"account_id": auth_bundle.token_data.account_id,
|
|
195
|
+
"last_refresh": auth_bundle.last_refresh,
|
|
196
|
+
}
|
|
197
|
+
if auth_bundle.api_key:
|
|
198
|
+
tokens["api_key"] = auth_bundle.api_key
|
|
199
|
+
|
|
200
|
+
if save_tokens(tokens):
|
|
201
|
+
self.server.exit_code = 0
|
|
202
|
+
# Redirect to the success URL returned by exchange_code
|
|
203
|
+
self._send_redirect(success_url)
|
|
204
|
+
else:
|
|
205
|
+
self._send_failure(
|
|
206
|
+
500, "Unable to persist auth file — a puppy probably chewed it."
|
|
207
|
+
)
|
|
208
|
+
self._shutdown()
|
|
209
|
+
self._shutdown_after_delay(2.0)
|
|
210
|
+
|
|
211
|
+
def do_POST(self) -> None: # noqa: N802
|
|
212
|
+
self._send_failure(
|
|
213
|
+
404, "POST not supported — the pups only fetch GET requests."
|
|
214
|
+
)
|
|
215
|
+
self._shutdown()
|
|
216
|
+
|
|
217
|
+
def log_message(self, fmt: str, *args: Any) -> None: # noqa: A003
|
|
218
|
+
if getattr(self.server, "verbose", False):
|
|
219
|
+
super().log_message(fmt, *args)
|
|
220
|
+
|
|
221
|
+
def _send_redirect(self, url: str) -> None:
|
|
222
|
+
self.send_response(302)
|
|
223
|
+
self.send_header("Location", url)
|
|
224
|
+
self.end_headers()
|
|
225
|
+
|
|
226
|
+
def _send_html(self, body: str, status: int = 200) -> None:
|
|
227
|
+
encoded = body.encode("utf-8")
|
|
228
|
+
self.send_response(status)
|
|
229
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
230
|
+
self.send_header("Content-Length", str(len(encoded)))
|
|
231
|
+
self.end_headers()
|
|
232
|
+
self.wfile.write(encoded)
|
|
233
|
+
|
|
234
|
+
def _send_failure(self, status: int, reason: str) -> None:
|
|
235
|
+
failure_html = oauth_failure_html("ChatGPT", reason)
|
|
236
|
+
self._send_html(failure_html, status)
|
|
237
|
+
|
|
238
|
+
def _shutdown(self) -> None:
|
|
239
|
+
threading.Thread(target=self.server.shutdown, daemon=True).start()
|
|
240
|
+
|
|
241
|
+
def _shutdown_after_delay(self, seconds: float = 2.0) -> None:
|
|
242
|
+
def _later() -> None:
|
|
243
|
+
try:
|
|
244
|
+
time.sleep(seconds)
|
|
245
|
+
finally:
|
|
246
|
+
self._shutdown()
|
|
247
|
+
|
|
248
|
+
threading.Thread(target=_later, daemon=True).start()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def run_oauth_flow() -> None:
|
|
252
|
+
existing_tokens = load_stored_tokens()
|
|
253
|
+
if existing_tokens and existing_tokens.get("access_token"):
|
|
254
|
+
emit_warning("Existing ChatGPT tokens will be overwritten.")
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
server = _OAuthServer(client_id=CHATGPT_OAUTH_CONFIG["client_id"])
|
|
258
|
+
except OSError as exc:
|
|
259
|
+
emit_error(f"Could not start OAuth server on port {REQUIRED_PORT}: {exc}")
|
|
260
|
+
emit_info(f"Use `lsof -ti:{REQUIRED_PORT} | xargs kill` to free the port.")
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
auth_url = server.auth_url()
|
|
264
|
+
emit_info(f"Open this URL in your browser: {auth_url}")
|
|
265
|
+
|
|
266
|
+
server_thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
267
|
+
server_thread.start()
|
|
268
|
+
|
|
269
|
+
webbrowser_opened = False
|
|
270
|
+
try:
|
|
271
|
+
import webbrowser
|
|
272
|
+
|
|
273
|
+
from code_puppy.tools.common import should_suppress_browser
|
|
274
|
+
|
|
275
|
+
if should_suppress_browser():
|
|
276
|
+
emit_info(f"[HEADLESS MODE] Would normally open: {auth_url}")
|
|
277
|
+
else:
|
|
278
|
+
webbrowser_opened = webbrowser.open(auth_url)
|
|
279
|
+
except Exception as exc: # noqa: BLE001
|
|
280
|
+
emit_warning(f"Could not open browser automatically: {exc}")
|
|
281
|
+
|
|
282
|
+
if not webbrowser_opened and not should_suppress_browser():
|
|
283
|
+
emit_warning("Please open the URL manually if the browser did not open.")
|
|
284
|
+
|
|
285
|
+
emit_info("Waiting for authentication callback…")
|
|
286
|
+
|
|
287
|
+
elapsed = 0.0
|
|
288
|
+
timeout = CHATGPT_OAUTH_CONFIG["callback_timeout"]
|
|
289
|
+
interval = 0.25
|
|
290
|
+
while elapsed < timeout:
|
|
291
|
+
time.sleep(interval)
|
|
292
|
+
elapsed += interval
|
|
293
|
+
if server.exit_code == 0:
|
|
294
|
+
break
|
|
295
|
+
|
|
296
|
+
server.shutdown()
|
|
297
|
+
server_thread.join(timeout=5)
|
|
298
|
+
|
|
299
|
+
if server.exit_code != 0:
|
|
300
|
+
emit_error("Authentication failed or timed out.")
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
tokens = load_stored_tokens()
|
|
304
|
+
if not tokens:
|
|
305
|
+
emit_error("Tokens saved during OAuth flow could not be loaded.")
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
api_key = tokens.get("api_key")
|
|
309
|
+
if api_key:
|
|
310
|
+
emit_success("Successfully obtained OAuth access token for API access.")
|
|
311
|
+
emit_info(
|
|
312
|
+
f"Access token saved and available via {CHATGPT_OAUTH_CONFIG['api_key_env_var']}"
|
|
313
|
+
)
|
|
314
|
+
else:
|
|
315
|
+
emit_warning(
|
|
316
|
+
"No API key obtained. You may need to configure projects at platform.openai.com."
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if api_key:
|
|
320
|
+
emit_info("Registering ChatGPT Codex models…")
|
|
321
|
+
from .utils import DEFAULT_CODEX_MODELS
|
|
322
|
+
|
|
323
|
+
models = DEFAULT_CODEX_MODELS
|
|
324
|
+
if models:
|
|
325
|
+
if add_models_to_extra_config(models):
|
|
326
|
+
emit_success(
|
|
327
|
+
"ChatGPT models registered. Use the `chatgpt-` prefix in /model."
|
|
328
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""ChatGPT OAuth plugin callbacks aligned with ChatMock flow."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from code_puppy.callbacks import register_callback
|
|
9
|
+
from code_puppy.config import set_model_name
|
|
10
|
+
from code_puppy.messaging import emit_info, emit_success, emit_warning
|
|
11
|
+
|
|
12
|
+
from .config import CHATGPT_OAUTH_CONFIG, get_token_storage_path
|
|
13
|
+
from .oauth_flow import run_oauth_flow
|
|
14
|
+
from .utils import load_chatgpt_models, load_stored_tokens, remove_chatgpt_models
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _custom_help() -> List[Tuple[str, str]]:
|
|
18
|
+
return [
|
|
19
|
+
(
|
|
20
|
+
"chatgpt-auth",
|
|
21
|
+
"Authenticate with ChatGPT via OAuth and import available models",
|
|
22
|
+
),
|
|
23
|
+
(
|
|
24
|
+
"chatgpt-status",
|
|
25
|
+
"Check ChatGPT OAuth authentication status and configured models",
|
|
26
|
+
),
|
|
27
|
+
("chatgpt-logout", "Remove ChatGPT OAuth tokens and imported models"),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _handle_chatgpt_status() -> None:
|
|
32
|
+
tokens = load_stored_tokens()
|
|
33
|
+
if tokens and tokens.get("access_token"):
|
|
34
|
+
emit_success("🔐 ChatGPT OAuth: Authenticated")
|
|
35
|
+
|
|
36
|
+
api_key = tokens.get("api_key")
|
|
37
|
+
if api_key:
|
|
38
|
+
os.environ[CHATGPT_OAUTH_CONFIG["api_key_env_var"]] = api_key
|
|
39
|
+
emit_info("✅ OAuth access token available for API requests")
|
|
40
|
+
else:
|
|
41
|
+
emit_warning("⚠️ No access token obtained. Authentication may have failed.")
|
|
42
|
+
|
|
43
|
+
chatgpt_models = [
|
|
44
|
+
name
|
|
45
|
+
for name, cfg in load_chatgpt_models().items()
|
|
46
|
+
if cfg.get("oauth_source") == "chatgpt-oauth-plugin"
|
|
47
|
+
]
|
|
48
|
+
if chatgpt_models:
|
|
49
|
+
emit_info(f"🎯 Configured ChatGPT models: {', '.join(chatgpt_models)}")
|
|
50
|
+
else:
|
|
51
|
+
emit_warning("⚠️ No ChatGPT models configured yet.")
|
|
52
|
+
else:
|
|
53
|
+
emit_warning("🔓 ChatGPT OAuth: Not authenticated")
|
|
54
|
+
emit_info("🌐 Run /chatgpt-auth to launch the browser sign-in flow.")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _handle_chatgpt_logout() -> None:
|
|
58
|
+
token_path = get_token_storage_path()
|
|
59
|
+
if token_path.exists():
|
|
60
|
+
token_path.unlink()
|
|
61
|
+
emit_info("Removed ChatGPT OAuth tokens")
|
|
62
|
+
|
|
63
|
+
if CHATGPT_OAUTH_CONFIG["api_key_env_var"] in os.environ:
|
|
64
|
+
del os.environ[CHATGPT_OAUTH_CONFIG["api_key_env_var"]]
|
|
65
|
+
|
|
66
|
+
removed = remove_chatgpt_models()
|
|
67
|
+
if removed:
|
|
68
|
+
emit_info(f"Removed {removed} ChatGPT models from configuration")
|
|
69
|
+
|
|
70
|
+
emit_success("ChatGPT logout complete")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _handle_custom_command(command: str, name: str) -> Optional[bool]:
|
|
74
|
+
if not name:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
if name == "chatgpt-auth":
|
|
78
|
+
run_oauth_flow()
|
|
79
|
+
set_model_name("chatgpt-gpt-5.2-codex")
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
if name == "chatgpt-status":
|
|
83
|
+
_handle_chatgpt_status()
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
if name == "chatgpt-logout":
|
|
87
|
+
_handle_chatgpt_logout()
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
register_callback("custom_command_help", _custom_help)
|
|
94
|
+
register_callback("custom_command", _handle_custom_command)
|