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.
Files changed (288) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +117 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +638 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +124 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +385 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +169 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2156 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +304 -0
  28. code_puppy/agents/pack/husky.py +327 -0
  29. code_puppy/agents/pack/retriever.py +393 -0
  30. code_puppy/agents/pack/shepherd.py +348 -0
  31. code_puppy/agents/pack/terrier.py +287 -0
  32. code_puppy/agents/pack/watchdog.py +367 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +692 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +672 -0
  49. code_puppy/cli_runner.py +1073 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +532 -0
  57. code_puppy/command_line/command_handler.py +293 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +867 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +96 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/shell_passthrough.py +145 -0
  97. code_puppy/command_line/skills_completion.py +160 -0
  98. code_puppy/command_line/uc_menu.py +893 -0
  99. code_puppy/command_line/utils.py +93 -0
  100. code_puppy/command_line/wiggum_state.py +78 -0
  101. code_puppy/config.py +1770 -0
  102. code_puppy/error_logging.py +134 -0
  103. code_puppy/gemini_code_assist.py +385 -0
  104. code_puppy/gemini_model.py +754 -0
  105. code_puppy/hook_engine/README.md +105 -0
  106. code_puppy/hook_engine/__init__.py +21 -0
  107. code_puppy/hook_engine/aliases.py +155 -0
  108. code_puppy/hook_engine/engine.py +221 -0
  109. code_puppy/hook_engine/executor.py +296 -0
  110. code_puppy/hook_engine/matcher.py +156 -0
  111. code_puppy/hook_engine/models.py +240 -0
  112. code_puppy/hook_engine/registry.py +106 -0
  113. code_puppy/hook_engine/validator.py +144 -0
  114. code_puppy/http_utils.py +361 -0
  115. code_puppy/keymap.py +128 -0
  116. code_puppy/main.py +10 -0
  117. code_puppy/mcp_/__init__.py +66 -0
  118. code_puppy/mcp_/async_lifecycle.py +286 -0
  119. code_puppy/mcp_/blocking_startup.py +469 -0
  120. code_puppy/mcp_/captured_stdio_server.py +275 -0
  121. code_puppy/mcp_/circuit_breaker.py +290 -0
  122. code_puppy/mcp_/config_wizard.py +507 -0
  123. code_puppy/mcp_/dashboard.py +308 -0
  124. code_puppy/mcp_/error_isolation.py +407 -0
  125. code_puppy/mcp_/examples/retry_example.py +226 -0
  126. code_puppy/mcp_/health_monitor.py +589 -0
  127. code_puppy/mcp_/managed_server.py +428 -0
  128. code_puppy/mcp_/manager.py +807 -0
  129. code_puppy/mcp_/mcp_logs.py +224 -0
  130. code_puppy/mcp_/registry.py +451 -0
  131. code_puppy/mcp_/retry_manager.py +337 -0
  132. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  133. code_puppy/mcp_/status_tracker.py +355 -0
  134. code_puppy/mcp_/system_tools.py +209 -0
  135. code_puppy/mcp_prompts/__init__.py +1 -0
  136. code_puppy/mcp_prompts/hook_creator.py +103 -0
  137. code_puppy/messaging/__init__.py +255 -0
  138. code_puppy/messaging/bus.py +613 -0
  139. code_puppy/messaging/commands.py +167 -0
  140. code_puppy/messaging/markdown_patches.py +57 -0
  141. code_puppy/messaging/message_queue.py +361 -0
  142. code_puppy/messaging/messages.py +569 -0
  143. code_puppy/messaging/queue_console.py +271 -0
  144. code_puppy/messaging/renderers.py +311 -0
  145. code_puppy/messaging/rich_renderer.py +1158 -0
  146. code_puppy/messaging/spinner/__init__.py +83 -0
  147. code_puppy/messaging/spinner/console_spinner.py +240 -0
  148. code_puppy/messaging/spinner/spinner_base.py +95 -0
  149. code_puppy/messaging/subagent_console.py +460 -0
  150. code_puppy/model_factory.py +848 -0
  151. code_puppy/model_switching.py +63 -0
  152. code_puppy/model_utils.py +168 -0
  153. code_puppy/models.json +174 -0
  154. code_puppy/models_dev_api.json +1 -0
  155. code_puppy/models_dev_parser.py +592 -0
  156. code_puppy/plugins/__init__.py +186 -0
  157. code_puppy/plugins/agent_skills/__init__.py +22 -0
  158. code_puppy/plugins/agent_skills/config.py +175 -0
  159. code_puppy/plugins/agent_skills/discovery.py +136 -0
  160. code_puppy/plugins/agent_skills/downloader.py +392 -0
  161. code_puppy/plugins/agent_skills/installer.py +22 -0
  162. code_puppy/plugins/agent_skills/metadata.py +219 -0
  163. code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
  164. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  165. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  166. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  167. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  168. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  169. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  170. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  171. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  172. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  173. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  174. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  175. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  176. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  177. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  178. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  179. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  180. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  181. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  182. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  183. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
  184. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  185. code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
  186. code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
  187. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  188. code_puppy/plugins/claude_code_hooks/config.py +137 -0
  189. code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
  190. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  191. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  192. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  193. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  194. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  195. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  196. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  197. code_puppy/plugins/claude_code_oauth/utils.py +640 -0
  198. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  199. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  200. code_puppy/plugins/example_custom_command/README.md +280 -0
  201. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  202. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  203. code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
  204. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  205. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  206. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  207. code_puppy/plugins/hook_creator/__init__.py +1 -0
  208. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  209. code_puppy/plugins/hook_manager/__init__.py +1 -0
  210. code_puppy/plugins/hook_manager/config.py +290 -0
  211. code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
  212. code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
  213. code_puppy/plugins/oauth_puppy_html.py +228 -0
  214. code_puppy/plugins/scheduler/__init__.py +1 -0
  215. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  216. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  217. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  218. code_puppy/plugins/shell_safety/__init__.py +6 -0
  219. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  220. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  221. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  222. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  223. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  224. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  225. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  226. code_puppy/plugins/universal_constructor/models.py +138 -0
  227. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  228. code_puppy/plugins/universal_constructor/registry.py +302 -0
  229. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  230. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  231. code_puppy/pydantic_patches.py +356 -0
  232. code_puppy/reopenable_async_client.py +232 -0
  233. code_puppy/round_robin_model.py +150 -0
  234. code_puppy/scheduler/__init__.py +41 -0
  235. code_puppy/scheduler/__main__.py +9 -0
  236. code_puppy/scheduler/cli.py +118 -0
  237. code_puppy/scheduler/config.py +126 -0
  238. code_puppy/scheduler/daemon.py +280 -0
  239. code_puppy/scheduler/executor.py +155 -0
  240. code_puppy/scheduler/platform.py +19 -0
  241. code_puppy/scheduler/platform_unix.py +22 -0
  242. code_puppy/scheduler/platform_win.py +32 -0
  243. code_puppy/session_storage.py +338 -0
  244. code_puppy/status_display.py +257 -0
  245. code_puppy/summarization_agent.py +176 -0
  246. code_puppy/terminal_utils.py +418 -0
  247. code_puppy/tools/__init__.py +501 -0
  248. code_puppy/tools/agent_tools.py +603 -0
  249. code_puppy/tools/ask_user_question/__init__.py +26 -0
  250. code_puppy/tools/ask_user_question/constants.py +73 -0
  251. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  252. code_puppy/tools/ask_user_question/handler.py +232 -0
  253. code_puppy/tools/ask_user_question/models.py +304 -0
  254. code_puppy/tools/ask_user_question/registration.py +26 -0
  255. code_puppy/tools/ask_user_question/renderers.py +309 -0
  256. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  257. code_puppy/tools/ask_user_question/theme.py +155 -0
  258. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  259. code_puppy/tools/browser/__init__.py +37 -0
  260. code_puppy/tools/browser/browser_control.py +289 -0
  261. code_puppy/tools/browser/browser_interactions.py +545 -0
  262. code_puppy/tools/browser/browser_locators.py +640 -0
  263. code_puppy/tools/browser/browser_manager.py +378 -0
  264. code_puppy/tools/browser/browser_navigation.py +251 -0
  265. code_puppy/tools/browser/browser_screenshot.py +179 -0
  266. code_puppy/tools/browser/browser_scripts.py +462 -0
  267. code_puppy/tools/browser/browser_workflows.py +221 -0
  268. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  269. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  270. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  271. code_puppy/tools/browser/terminal_tools.py +525 -0
  272. code_puppy/tools/command_runner.py +1346 -0
  273. code_puppy/tools/common.py +1409 -0
  274. code_puppy/tools/display.py +84 -0
  275. code_puppy/tools/file_modifications.py +886 -0
  276. code_puppy/tools/file_operations.py +802 -0
  277. code_puppy/tools/scheduler_tools.py +412 -0
  278. code_puppy/tools/skills_tools.py +244 -0
  279. code_puppy/tools/subagent_context.py +158 -0
  280. code_puppy/tools/tools_content.py +51 -0
  281. code_puppy/tools/universal_constructor.py +889 -0
  282. code_puppy/uvx_detection.py +242 -0
  283. code_puppy/version_checker.py +82 -0
  284. codepp-0.0.437.dist-info/METADATA +766 -0
  285. codepp-0.0.437.dist-info/RECORD +288 -0
  286. codepp-0.0.437.dist-info/WHEEL +4 -0
  287. codepp-0.0.437.dist-info/entry_points.txt +3 -0
  288. 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"])