codepp 0.0.437__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 +10 -0
- code_puppy/__main__.py +10 -0
- code_puppy/agents/__init__.py +31 -0
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +117 -0
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +638 -0
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_helios.py +124 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +742 -0
- code_puppy/agents/agent_pack_leader.py +385 -0
- code_puppy/agents/agent_planning.py +165 -0
- code_puppy/agents/agent_python_programmer.py +169 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_scheduler.py +121 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +2156 -0
- code_puppy/agents/event_stream_handler.py +348 -0
- code_puppy/agents/json_agent.py +202 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +327 -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 +453 -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 +75 -0
- code_puppy/api/routers/sessions.py +234 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +692 -0
- code_puppy/chatgpt_codex_client.py +338 -0
- code_puppy/claude_cache_client.py +672 -0
- code_puppy/cli_runner.py +1073 -0
- code_puppy/command_line/__init__.py +1 -0
- code_puppy/command_line/add_model_menu.py +1092 -0
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +704 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +532 -0
- code_puppy/command_line/command_handler.py +293 -0
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +719 -0
- code_puppy/command_line/core_commands.py +867 -0
- code_puppy/command_line/diff_menu.py +865 -0
- code_puppy/command_line/file_path_completion.py +73 -0
- code_puppy/command_line/load_context_completion.py +52 -0
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/base.py +32 -0
- 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 +138 -0
- code_puppy/command_line/mcp/help_command.py +147 -0
- code_puppy/command_line/mcp/install_command.py +214 -0
- code_puppy/command_line/mcp/install_menu.py +705 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +235 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +100 -0
- code_puppy/command_line/mcp/search_command.py +123 -0
- code_puppy/command_line/mcp/start_all_command.py +135 -0
- code_puppy/command_line/mcp/start_command.py +117 -0
- code_puppy/command_line/mcp/status_command.py +184 -0
- code_puppy/command_line/mcp/stop_all_command.py +112 -0
- code_puppy/command_line/mcp/stop_command.py +80 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +334 -0
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +197 -0
- code_puppy/command_line/model_settings_menu.py +932 -0
- code_puppy/command_line/motd.py +96 -0
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +342 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +846 -0
- code_puppy/command_line/session_commands.py +302 -0
- code_puppy/command_line/shell_passthrough.py +145 -0
- code_puppy/command_line/skills_completion.py +160 -0
- code_puppy/command_line/uc_menu.py +893 -0
- code_puppy/command_line/utils.py +93 -0
- code_puppy/command_line/wiggum_state.py +78 -0
- code_puppy/config.py +1770 -0
- code_puppy/error_logging.py +134 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +754 -0
- code_puppy/hook_engine/README.md +105 -0
- code_puppy/hook_engine/__init__.py +21 -0
- code_puppy/hook_engine/aliases.py +155 -0
- code_puppy/hook_engine/engine.py +221 -0
- code_puppy/hook_engine/executor.py +296 -0
- code_puppy/hook_engine/matcher.py +156 -0
- code_puppy/hook_engine/models.py +240 -0
- code_puppy/hook_engine/registry.py +106 -0
- code_puppy/hook_engine/validator.py +144 -0
- code_puppy/http_utils.py +361 -0
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +10 -0
- code_puppy/mcp_/__init__.py +66 -0
- code_puppy/mcp_/async_lifecycle.py +286 -0
- code_puppy/mcp_/blocking_startup.py +469 -0
- code_puppy/mcp_/captured_stdio_server.py +275 -0
- code_puppy/mcp_/circuit_breaker.py +290 -0
- code_puppy/mcp_/config_wizard.py +507 -0
- code_puppy/mcp_/dashboard.py +308 -0
- code_puppy/mcp_/error_isolation.py +407 -0
- code_puppy/mcp_/examples/retry_example.py +226 -0
- code_puppy/mcp_/health_monitor.py +589 -0
- code_puppy/mcp_/managed_server.py +428 -0
- code_puppy/mcp_/manager.py +807 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +451 -0
- code_puppy/mcp_/retry_manager.py +337 -0
- code_puppy/mcp_/server_registry_catalog.py +1126 -0
- code_puppy/mcp_/status_tracker.py +355 -0
- code_puppy/mcp_/system_tools.py +209 -0
- code_puppy/mcp_prompts/__init__.py +1 -0
- code_puppy/mcp_prompts/hook_creator.py +103 -0
- code_puppy/messaging/__init__.py +255 -0
- code_puppy/messaging/bus.py +613 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +361 -0
- code_puppy/messaging/messages.py +569 -0
- code_puppy/messaging/queue_console.py +271 -0
- code_puppy/messaging/renderers.py +311 -0
- code_puppy/messaging/rich_renderer.py +1158 -0
- code_puppy/messaging/spinner/__init__.py +83 -0
- code_puppy/messaging/spinner/console_spinner.py +240 -0
- code_puppy/messaging/spinner/spinner_base.py +95 -0
- code_puppy/messaging/subagent_console.py +460 -0
- code_puppy/model_factory.py +848 -0
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +168 -0
- code_puppy/models.json +174 -0
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +186 -0
- code_puppy/plugins/agent_skills/__init__.py +22 -0
- code_puppy/plugins/agent_skills/config.py +175 -0
- code_puppy/plugins/agent_skills/discovery.py +136 -0
- code_puppy/plugins/agent_skills/downloader.py +392 -0
- code_puppy/plugins/agent_skills/installer.py +22 -0
- code_puppy/plugins/agent_skills/metadata.py +219 -0
- code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
- code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
- code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
- code_puppy/plugins/agent_skills/skills_menu.py +781 -0
- 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 +706 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +133 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
- code_puppy/plugins/antigravity_oauth/storage.py +288 -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 +863 -0
- code_puppy/plugins/antigravity_oauth/utils.py +168 -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 +329 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
- code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy/plugins/claude_code_hooks/config.py +137 -0
- code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -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 +25 -0
- code_puppy/plugins/claude_code_oauth/config.py +52 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
- code_puppy/plugins/claude_code_oauth/utils.py +640 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -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/hook_creator/__init__.py +1 -0
- code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy/plugins/hook_manager/__init__.py +1 -0
- code_puppy/plugins/hook_manager/config.py +290 -0
- code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
- code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/scheduler/__init__.py +1 -0
- code_puppy/plugins/scheduler/register_callbacks.py +88 -0
- code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
- code_puppy/plugins/scheduler/scheduler_wizard.py +341 -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/plugins/synthetic_status/__init__.py +1 -0
- code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
- code_puppy/plugins/synthetic_status/status_api.py +147 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +302 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +356 -0
- code_puppy/reopenable_async_client.py +232 -0
- code_puppy/round_robin_model.py +150 -0
- code_puppy/scheduler/__init__.py +41 -0
- code_puppy/scheduler/__main__.py +9 -0
- code_puppy/scheduler/cli.py +118 -0
- code_puppy/scheduler/config.py +126 -0
- code_puppy/scheduler/daemon.py +280 -0
- code_puppy/scheduler/executor.py +155 -0
- code_puppy/scheduler/platform.py +19 -0
- code_puppy/scheduler/platform_unix.py +22 -0
- code_puppy/scheduler/platform_win.py +32 -0
- code_puppy/session_storage.py +338 -0
- code_puppy/status_display.py +257 -0
- code_puppy/summarization_agent.py +176 -0
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +501 -0
- code_puppy/tools/agent_tools.py +603 -0
- code_puppy/tools/ask_user_question/__init__.py +26 -0
- code_puppy/tools/ask_user_question/constants.py +73 -0
- code_puppy/tools/ask_user_question/demo_tui.py +55 -0
- code_puppy/tools/ask_user_question/handler.py +232 -0
- code_puppy/tools/ask_user_question/models.py +304 -0
- code_puppy/tools/ask_user_question/registration.py +26 -0
- code_puppy/tools/ask_user_question/renderers.py +309 -0
- code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
- code_puppy/tools/ask_user_question/theme.py +155 -0
- code_puppy/tools/ask_user_question/tui_loop.py +423 -0
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +378 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +534 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +1346 -0
- code_puppy/tools/common.py +1409 -0
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +886 -0
- code_puppy/tools/file_operations.py +802 -0
- code_puppy/tools/scheduler_tools.py +412 -0
- code_puppy/tools/skills_tools.py +244 -0
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/tools/tools_content.py +51 -0
- code_puppy/tools/universal_constructor.py +889 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +82 -0
- codepp-0.0.437.dist-info/METADATA +766 -0
- codepp-0.0.437.dist-info/RECORD +288 -0
- codepp-0.0.437.dist-info/WHEEL +4 -0
- codepp-0.0.437.dist-info/entry_points.txt +3 -0
- codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""ChatGPT OAuth plugin callbacks aligned with ChatMock flow.
|
|
2
|
+
|
|
3
|
+
Provides OAuth authentication for ChatGPT models and registers
|
|
4
|
+
the 'chatgpt_oauth' model type handler.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from code_puppy.callbacks import register_callback
|
|
13
|
+
from code_puppy.messaging import emit_info, emit_success, emit_warning
|
|
14
|
+
from code_puppy.model_switching import set_model_and_reload_agent
|
|
15
|
+
|
|
16
|
+
from .config import CHATGPT_OAUTH_CONFIG, get_token_storage_path
|
|
17
|
+
from .oauth_flow import run_oauth_flow
|
|
18
|
+
from .utils import (
|
|
19
|
+
get_valid_access_token,
|
|
20
|
+
load_chatgpt_models,
|
|
21
|
+
load_stored_tokens,
|
|
22
|
+
remove_chatgpt_models,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _custom_help() -> List[Tuple[str, str]]:
|
|
27
|
+
return [
|
|
28
|
+
(
|
|
29
|
+
"chatgpt-auth",
|
|
30
|
+
"Authenticate with ChatGPT via OAuth and import available models",
|
|
31
|
+
),
|
|
32
|
+
(
|
|
33
|
+
"chatgpt-status",
|
|
34
|
+
"Check ChatGPT OAuth authentication status and configured models",
|
|
35
|
+
),
|
|
36
|
+
("chatgpt-logout", "Remove ChatGPT OAuth tokens and imported models"),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _handle_chatgpt_status() -> None:
|
|
41
|
+
tokens = load_stored_tokens()
|
|
42
|
+
if tokens and tokens.get("access_token"):
|
|
43
|
+
emit_success("🔐 ChatGPT OAuth: Authenticated")
|
|
44
|
+
|
|
45
|
+
api_key = tokens.get("api_key")
|
|
46
|
+
if api_key:
|
|
47
|
+
os.environ[CHATGPT_OAUTH_CONFIG["api_key_env_var"]] = api_key
|
|
48
|
+
emit_info("✅ OAuth access token available for API requests")
|
|
49
|
+
else:
|
|
50
|
+
emit_warning("⚠️ No access token obtained. Authentication may have failed.")
|
|
51
|
+
|
|
52
|
+
chatgpt_models = [
|
|
53
|
+
name
|
|
54
|
+
for name, cfg in load_chatgpt_models().items()
|
|
55
|
+
if cfg.get("oauth_source") == "chatgpt-oauth-plugin"
|
|
56
|
+
]
|
|
57
|
+
if chatgpt_models:
|
|
58
|
+
emit_info(f"🎯 Configured ChatGPT models: {', '.join(chatgpt_models)}")
|
|
59
|
+
else:
|
|
60
|
+
emit_warning("⚠️ No ChatGPT models configured yet.")
|
|
61
|
+
else:
|
|
62
|
+
emit_warning("🔓 ChatGPT OAuth: Not authenticated")
|
|
63
|
+
emit_info("🌐 Run /chatgpt-auth to launch the browser sign-in flow.")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _handle_chatgpt_logout() -> None:
|
|
67
|
+
token_path = get_token_storage_path()
|
|
68
|
+
if token_path.exists():
|
|
69
|
+
token_path.unlink()
|
|
70
|
+
emit_info("Removed ChatGPT OAuth tokens")
|
|
71
|
+
|
|
72
|
+
if CHATGPT_OAUTH_CONFIG["api_key_env_var"] in os.environ:
|
|
73
|
+
del os.environ[CHATGPT_OAUTH_CONFIG["api_key_env_var"]]
|
|
74
|
+
|
|
75
|
+
removed = remove_chatgpt_models()
|
|
76
|
+
if removed:
|
|
77
|
+
emit_info(f"Removed {removed} ChatGPT models from configuration")
|
|
78
|
+
|
|
79
|
+
emit_success("ChatGPT logout complete")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _handle_custom_command(command: str, name: str) -> Optional[bool]:
|
|
83
|
+
if not name:
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
if name == "chatgpt-auth":
|
|
87
|
+
run_oauth_flow()
|
|
88
|
+
set_model_and_reload_agent("chatgpt-gpt-5.3-codex")
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
if name == "chatgpt-status":
|
|
92
|
+
_handle_chatgpt_status()
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
if name == "chatgpt-logout":
|
|
96
|
+
_handle_chatgpt_logout()
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _create_chatgpt_oauth_model(
|
|
103
|
+
model_name: str, model_config: Dict, config: Dict
|
|
104
|
+
) -> Any:
|
|
105
|
+
"""Create a ChatGPT OAuth model instance.
|
|
106
|
+
|
|
107
|
+
This handler is registered via the 'register_model_type' callback to handle
|
|
108
|
+
models with type='chatgpt_oauth'.
|
|
109
|
+
"""
|
|
110
|
+
from pydantic_ai.models.openai import OpenAIResponsesModel
|
|
111
|
+
from pydantic_ai.providers.openai import OpenAIProvider
|
|
112
|
+
|
|
113
|
+
from code_puppy.chatgpt_codex_client import create_codex_async_client
|
|
114
|
+
from code_puppy.http_utils import get_cert_bundle_path
|
|
115
|
+
|
|
116
|
+
# Get a valid access token (refreshing if needed)
|
|
117
|
+
access_token = get_valid_access_token()
|
|
118
|
+
if not access_token:
|
|
119
|
+
emit_warning(
|
|
120
|
+
f"Failed to get valid ChatGPT OAuth token; skipping model '{model_config.get('name')}'. "
|
|
121
|
+
"Run /chatgpt-auth to authenticate."
|
|
122
|
+
)
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
# Get account_id from stored tokens (required for ChatGPT-Account-Id header)
|
|
126
|
+
tokens = load_stored_tokens()
|
|
127
|
+
account_id = tokens.get("account_id", "") if tokens else ""
|
|
128
|
+
if not account_id:
|
|
129
|
+
emit_warning(
|
|
130
|
+
f"No account_id found in ChatGPT OAuth tokens; skipping model '{model_config.get('name')}'. "
|
|
131
|
+
"Run /chatgpt-auth to re-authenticate."
|
|
132
|
+
)
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
# Build headers for ChatGPT Codex API
|
|
136
|
+
originator = CHATGPT_OAUTH_CONFIG.get("originator", "codex_cli_rs")
|
|
137
|
+
client_version = CHATGPT_OAUTH_CONFIG.get("client_version", "0.72.0")
|
|
138
|
+
|
|
139
|
+
headers = {
|
|
140
|
+
"ChatGPT-Account-Id": account_id,
|
|
141
|
+
"originator": originator,
|
|
142
|
+
"User-Agent": f"{originator}/{client_version}",
|
|
143
|
+
}
|
|
144
|
+
# Merge with any headers from model config
|
|
145
|
+
config_headers = model_config.get("custom_endpoint", {}).get("headers", {})
|
|
146
|
+
headers.update(config_headers)
|
|
147
|
+
|
|
148
|
+
# Get base URL - Codex API uses chatgpt.com, not api.openai.com
|
|
149
|
+
base_url = model_config.get("custom_endpoint", {}).get(
|
|
150
|
+
"url", CHATGPT_OAUTH_CONFIG["api_base_url"]
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Create HTTP client with Codex interceptor for store=false injection
|
|
154
|
+
verify = get_cert_bundle_path()
|
|
155
|
+
client = create_codex_async_client(headers=headers, verify=verify)
|
|
156
|
+
|
|
157
|
+
provider = OpenAIProvider(
|
|
158
|
+
api_key=access_token,
|
|
159
|
+
base_url=base_url,
|
|
160
|
+
http_client=client,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# ChatGPT Codex API only supports Responses format
|
|
164
|
+
model = OpenAIResponsesModel(model_name=model_config["name"], provider=provider)
|
|
165
|
+
model.provider = provider
|
|
166
|
+
return model
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _register_model_types() -> List[Dict[str, Any]]:
|
|
170
|
+
"""Register the chatgpt_oauth model type handler."""
|
|
171
|
+
return [{"type": "chatgpt_oauth", "handler": _create_chatgpt_oauth_model}]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
register_callback("custom_command_help", _custom_help)
|
|
175
|
+
register_callback("custom_command", _handle_custom_command)
|
|
176
|
+
register_callback("register_model_type", _register_model_types)
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Basic tests for ChatGPT OAuth plugin.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from code_puppy.plugins.chatgpt_oauth import config, utils
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_config_paths():
|
|
14
|
+
"""Test configuration path helpers."""
|
|
15
|
+
token_path = config.get_token_storage_path()
|
|
16
|
+
assert token_path.name == "chatgpt_oauth.json"
|
|
17
|
+
# XDG paths use "code_puppy" (without dot) in ~/.local/share or ~/.config
|
|
18
|
+
assert "code_puppy" in str(token_path)
|
|
19
|
+
|
|
20
|
+
config_dir = config.get_config_dir()
|
|
21
|
+
# Default is ~/.code_puppy; XDG paths only used when XDG env vars are set
|
|
22
|
+
assert config_dir.name in ("code_puppy", ".code_puppy")
|
|
23
|
+
|
|
24
|
+
chatgpt_models = config.get_chatgpt_models_path()
|
|
25
|
+
assert chatgpt_models.name == "chatgpt_models.json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_oauth_config():
|
|
29
|
+
"""Test OAuth configuration values."""
|
|
30
|
+
assert config.CHATGPT_OAUTH_CONFIG["issuer"] == "https://auth.openai.com"
|
|
31
|
+
assert config.CHATGPT_OAUTH_CONFIG["client_id"] == "app_EMoamEEZ73f0CkXaXp7hrann"
|
|
32
|
+
assert config.CHATGPT_OAUTH_CONFIG["prefix"] == "chatgpt-"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_jwt_parsing_with_nested_org():
|
|
36
|
+
"""Test JWT parsing with nested organization structure like the user's payload."""
|
|
37
|
+
# This simulates the user's JWT payload structure
|
|
38
|
+
mock_claims = {
|
|
39
|
+
"aud": ["app_EMoamEEZ73f0CkXaXp7hrann"],
|
|
40
|
+
"auth_provider": "google",
|
|
41
|
+
"email": "mike.pfaf fenberger@gmail.com",
|
|
42
|
+
"https://api.openai.com/auth": {
|
|
43
|
+
"chatgpt_account_id": "d1844a91-9aac-419b-903e-f6a99c76f163",
|
|
44
|
+
"organizations": [
|
|
45
|
+
{
|
|
46
|
+
"id": "org-iydWjnSxSr51VuYhDVMDte5",
|
|
47
|
+
"is_default": True,
|
|
48
|
+
"role": "owner",
|
|
49
|
+
"title": "Personal",
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"groups": ["api-data-sharing-incentives-program", "verified-organization"],
|
|
53
|
+
},
|
|
54
|
+
"sub": "google-oauth2|107692466937587138174",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Test the org extraction logic
|
|
58
|
+
auth_claims = mock_claims.get("https://api.openai.com/auth", {})
|
|
59
|
+
organizations = auth_claims.get("organizations", [])
|
|
60
|
+
|
|
61
|
+
org_id = None
|
|
62
|
+
if organizations:
|
|
63
|
+
default_org = next(
|
|
64
|
+
(org for org in organizations if org.get("is_default")), organizations[0]
|
|
65
|
+
)
|
|
66
|
+
org_id = default_org.get("id")
|
|
67
|
+
|
|
68
|
+
assert org_id == "org-iydWjnSxSr51VuYhDVMDte5"
|
|
69
|
+
|
|
70
|
+
# Test fallback to top-level org_id (should not happen in this case)
|
|
71
|
+
if not org_id:
|
|
72
|
+
org_id = mock_claims.get("organization_id")
|
|
73
|
+
|
|
74
|
+
assert org_id == "org-iydWjnSxSr51VuYhDVMDte5"
|
|
75
|
+
assert config.CHATGPT_OAUTH_CONFIG["required_port"] == 1455
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_code_verifier_generation():
|
|
79
|
+
"""Test PKCE code verifier generation."""
|
|
80
|
+
verifier = utils._generate_code_verifier()
|
|
81
|
+
assert isinstance(verifier, str)
|
|
82
|
+
assert len(verifier) > 50 # Should be long
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_code_challenge_computation():
|
|
86
|
+
"""Test PKCE code challenge computation."""
|
|
87
|
+
verifier = "test_verifier_string"
|
|
88
|
+
challenge = utils._compute_code_challenge(verifier)
|
|
89
|
+
assert isinstance(challenge, str)
|
|
90
|
+
assert len(challenge) > 0
|
|
91
|
+
# Should be URL-safe base64
|
|
92
|
+
assert all(
|
|
93
|
+
c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
|
94
|
+
for c in challenge
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_prepare_oauth_context():
|
|
99
|
+
"""Test OAuth context preparation."""
|
|
100
|
+
context = utils.prepare_oauth_context()
|
|
101
|
+
assert context.state
|
|
102
|
+
assert context.code_verifier
|
|
103
|
+
assert context.code_challenge
|
|
104
|
+
assert context.created_at > 0
|
|
105
|
+
assert context.redirect_uri is None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_assign_redirect_uri():
|
|
109
|
+
"""Test redirect URI assignment."""
|
|
110
|
+
context = utils.prepare_oauth_context()
|
|
111
|
+
redirect_uri = utils.assign_redirect_uri(context, 1455)
|
|
112
|
+
assert redirect_uri == "http://localhost:1455/auth/callback"
|
|
113
|
+
assert context.redirect_uri == redirect_uri
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_build_authorization_url():
|
|
117
|
+
"""Test authorization URL building."""
|
|
118
|
+
context = utils.prepare_oauth_context()
|
|
119
|
+
utils.assign_redirect_uri(context, 1455)
|
|
120
|
+
auth_url = utils.build_authorization_url(context)
|
|
121
|
+
|
|
122
|
+
assert auth_url.startswith("https://auth.openai.com/oauth/authorize?")
|
|
123
|
+
assert "response_type=code" in auth_url
|
|
124
|
+
assert "client_id=" in auth_url
|
|
125
|
+
assert "redirect_uri=" in auth_url
|
|
126
|
+
assert "code_challenge=" in auth_url
|
|
127
|
+
assert "code_challenge_method=S256" in auth_url
|
|
128
|
+
assert f"state={context.state}" in auth_url
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_parse_jwt_claims():
|
|
132
|
+
"""Test JWT claims parsing."""
|
|
133
|
+
# Valid JWT structure (header.payload.signature)
|
|
134
|
+
import base64
|
|
135
|
+
|
|
136
|
+
payload = base64.urlsafe_b64encode(json.dumps({"sub": "user123"}).encode()).decode()
|
|
137
|
+
token = f"header.{payload}.signature"
|
|
138
|
+
|
|
139
|
+
claims = utils.parse_jwt_claims(token)
|
|
140
|
+
assert claims is not None
|
|
141
|
+
assert claims["sub"] == "user123"
|
|
142
|
+
|
|
143
|
+
# Invalid token
|
|
144
|
+
assert utils.parse_jwt_claims("") is None
|
|
145
|
+
assert utils.parse_jwt_claims("invalid") is None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_save_and_load_tokens(tmp_path):
|
|
149
|
+
"""Test token storage and retrieval."""
|
|
150
|
+
with patch.object(
|
|
151
|
+
config, "get_token_storage_path", return_value=tmp_path / "tokens.json"
|
|
152
|
+
):
|
|
153
|
+
tokens = {
|
|
154
|
+
"access_token": "test_access",
|
|
155
|
+
"refresh_token": "test_refresh",
|
|
156
|
+
"api_key": "sk-test",
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Save tokens
|
|
160
|
+
assert utils.save_tokens(tokens)
|
|
161
|
+
|
|
162
|
+
# Load tokens
|
|
163
|
+
loaded = utils.load_stored_tokens()
|
|
164
|
+
assert loaded == tokens
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_save_and_load_chatgpt_models(tmp_path):
|
|
168
|
+
"""Test ChatGPT models configuration."""
|
|
169
|
+
with patch.object(
|
|
170
|
+
config, "get_chatgpt_models_path", return_value=tmp_path / "chatgpt_models.json"
|
|
171
|
+
):
|
|
172
|
+
models = {
|
|
173
|
+
"chatgpt-gpt-4o": {
|
|
174
|
+
"type": "openai",
|
|
175
|
+
"name": "gpt-4o",
|
|
176
|
+
"oauth_source": "chatgpt-oauth-plugin",
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# Save models
|
|
181
|
+
assert utils.save_chatgpt_models(models)
|
|
182
|
+
|
|
183
|
+
# Load models
|
|
184
|
+
loaded = utils.load_chatgpt_models()
|
|
185
|
+
assert loaded == models
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_remove_chatgpt_models(tmp_path):
|
|
189
|
+
"""Test removal of ChatGPT models from config."""
|
|
190
|
+
with patch.object(
|
|
191
|
+
config, "get_chatgpt_models_path", return_value=tmp_path / "chatgpt_models.json"
|
|
192
|
+
):
|
|
193
|
+
models = {
|
|
194
|
+
"chatgpt-gpt-4o": {
|
|
195
|
+
"type": "openai",
|
|
196
|
+
"oauth_source": "chatgpt-oauth-plugin",
|
|
197
|
+
},
|
|
198
|
+
"claude-3-opus": {
|
|
199
|
+
"type": "anthropic",
|
|
200
|
+
"oauth_source": "other",
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
utils.save_chatgpt_models(models)
|
|
204
|
+
|
|
205
|
+
# Remove only ChatGPT models
|
|
206
|
+
removed_count = utils.remove_chatgpt_models()
|
|
207
|
+
assert removed_count == 1
|
|
208
|
+
|
|
209
|
+
# Verify only ChatGPT model was removed
|
|
210
|
+
remaining = utils.load_chatgpt_models()
|
|
211
|
+
assert "chatgpt-gpt-4o" not in remaining
|
|
212
|
+
assert "claude-3-opus" in remaining
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@patch("code_puppy.plugins.chatgpt_oauth.utils.requests.post")
|
|
216
|
+
def test_exchange_code_for_tokens(mock_post):
|
|
217
|
+
"""Test authorization code exchange."""
|
|
218
|
+
mock_response = MagicMock()
|
|
219
|
+
mock_response.status_code = 200
|
|
220
|
+
mock_response.json.return_value = {
|
|
221
|
+
"access_token": "test_access",
|
|
222
|
+
"refresh_token": "test_refresh",
|
|
223
|
+
"id_token": "test_id",
|
|
224
|
+
}
|
|
225
|
+
mock_post.return_value = mock_response
|
|
226
|
+
|
|
227
|
+
context = utils.prepare_oauth_context()
|
|
228
|
+
utils.assign_redirect_uri(context, 1455)
|
|
229
|
+
|
|
230
|
+
tokens = utils.exchange_code_for_tokens("test_code", context)
|
|
231
|
+
assert tokens is not None
|
|
232
|
+
assert tokens["access_token"] == "test_access"
|
|
233
|
+
assert "last_refresh" in tokens
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@patch("code_puppy.plugins.chatgpt_oauth.utils.requests.get")
|
|
237
|
+
def test_fetch_chatgpt_models(mock_get):
|
|
238
|
+
"""Test fetching models from ChatGPT Codex API."""
|
|
239
|
+
mock_response = MagicMock()
|
|
240
|
+
mock_response.status_code = 200
|
|
241
|
+
# New response format uses "models" key with "slug" field
|
|
242
|
+
mock_response.json.return_value = {
|
|
243
|
+
"models": [
|
|
244
|
+
{"slug": "gpt-4o"},
|
|
245
|
+
{"slug": "gpt-3.5-turbo"},
|
|
246
|
+
{"slug": "o1-preview"},
|
|
247
|
+
{"slug": "codex-mini"},
|
|
248
|
+
]
|
|
249
|
+
}
|
|
250
|
+
mock_get.return_value = mock_response
|
|
251
|
+
|
|
252
|
+
models = utils.fetch_chatgpt_models("test_access_token", "test_account_id")
|
|
253
|
+
assert models is not None
|
|
254
|
+
# Required models always injected
|
|
255
|
+
assert "gpt-5.4" in models
|
|
256
|
+
assert "gpt-5.3-instant" in models
|
|
257
|
+
# API-returned models present too
|
|
258
|
+
assert "gpt-4o" in models
|
|
259
|
+
assert "gpt-3.5-turbo" in models
|
|
260
|
+
assert "o1-preview" in models
|
|
261
|
+
assert "codex-mini" in models
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@patch("code_puppy.plugins.chatgpt_oauth.utils.requests.get")
|
|
265
|
+
def test_fetch_chatgpt_models_fallback(mock_get):
|
|
266
|
+
"""Test that fetch_chatgpt_models returns default list on API failure."""
|
|
267
|
+
mock_response = MagicMock()
|
|
268
|
+
mock_response.status_code = 404
|
|
269
|
+
mock_response.text = '{"detail":"Not Found"}'
|
|
270
|
+
mock_get.return_value = mock_response
|
|
271
|
+
|
|
272
|
+
models = utils.fetch_chatgpt_models("test_access_token", "test_account_id")
|
|
273
|
+
assert models is not None
|
|
274
|
+
# Should return default models (including new required ones)
|
|
275
|
+
assert "gpt-5.4" in models
|
|
276
|
+
assert "gpt-5.3-instant" in models
|
|
277
|
+
assert "gpt-5.3-codex-spark" in models
|
|
278
|
+
assert "gpt-5.3-codex" in models
|
|
279
|
+
assert "gpt-5.2-codex" in models
|
|
280
|
+
assert "gpt-5.2" in models
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_add_models_to_chatgpt_config(tmp_path):
|
|
284
|
+
"""Test adding models to chatgpt_models.json."""
|
|
285
|
+
with patch.object(
|
|
286
|
+
config, "get_chatgpt_models_path", return_value=tmp_path / "chatgpt_models.json"
|
|
287
|
+
):
|
|
288
|
+
models = ["gpt-4o", "gpt-3.5-turbo"]
|
|
289
|
+
|
|
290
|
+
assert utils.add_models_to_extra_config(models)
|
|
291
|
+
|
|
292
|
+
loaded = utils.load_chatgpt_models()
|
|
293
|
+
assert "chatgpt-gpt-4o" in loaded
|
|
294
|
+
assert "chatgpt-gpt-3.5-turbo" in loaded
|
|
295
|
+
assert loaded["chatgpt-gpt-4o"]["type"] == "chatgpt_oauth"
|
|
296
|
+
assert loaded["chatgpt-gpt-4o"]["name"] == "gpt-4o"
|
|
297
|
+
assert loaded["chatgpt-gpt-4o"]["oauth_source"] == "chatgpt-oauth-plugin"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
if __name__ == "__main__":
|
|
301
|
+
pytest.main([__file__, "-v"])
|