code-puppy 0.0.169__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 +8 -8
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +9 -2
- 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 +48 -9
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +146 -199
- 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 +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -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 +1713 -1
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/json_agent.py +12 -1
- 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 +174 -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 +395 -0
- 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 +233 -627
- 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 +1 -4
- 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 +16 -27
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +3 -3
- 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 +17 -11
- code_puppy/command_line/mcp/start_all_command.py +22 -13
- code_puppy/command_line/mcp/start_command.py +50 -31
- code_puppy/command_line/mcp/status_command.py +6 -7
- code_puppy/command_line/mcp/stop_all_command.py +11 -8
- code_puppy/command_line/mcp/stop_command.py +11 -10
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/utils.py +1 -1
- code_puppy/command_line/mcp/wizard_utils.py +22 -18
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +89 -30
- 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 +626 -75
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +1181 -51
- 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 +220 -104
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -594
- code_puppy/{mcp → mcp_}/__init__.py +17 -0
- code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
- code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
- code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
- code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
- code_puppy/{mcp → mcp_}/dashboard.py +15 -6
- code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
- code_puppy/{mcp → mcp_}/managed_server.py +66 -39
- code_puppy/{mcp → mcp_}/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/{mcp → mcp_}/registry.py +6 -6
- code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
- 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 +33 -5
- code_puppy/messaging/spinner/console_spinner.py +92 -52
- code_puppy/messaging/spinner/spinner_base.py +29 -0
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +686 -80
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +86 -104
- 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 +51 -0
- 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 +10 -15
- code_puppy/session_storage.py +294 -0
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +52 -14
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +139 -6
- code_puppy/tools/agent_tools.py +548 -49
- 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 +316 -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 +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 +941 -153
- code_puppy/tools/common.py +1146 -6
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +352 -266
- 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.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
- code_puppy/agent.py +0 -231
- code_puppy/agents/agent_orchestrator.json +0 -26
- code_puppy/agents/runtime_manager.py +0 -272
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/command_line/meta_command_handler.py +0 -153
- code_puppy/message_history_processor.py +0 -490
- code_puppy/messaging/spinner/textual_spinner.py +0 -101
- code_puppy/state_management.py +0 -200
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -986
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -550
- 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 -182
- 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 -15
- 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 -290
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
- code_puppy-0.0.169.dist-info/RECORD +0 -112
- /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
- /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
- /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
- /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
- /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
- /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
"""Utility helpers for the ChatGPT OAuth plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import datetime
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import secrets
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
from urllib.parse import parse_qs as urllib_parse_qs
|
|
15
|
+
from urllib.parse import urlencode, urlparse
|
|
16
|
+
|
|
17
|
+
import requests
|
|
18
|
+
|
|
19
|
+
from .config import (
|
|
20
|
+
CHATGPT_OAUTH_CONFIG,
|
|
21
|
+
get_chatgpt_models_path,
|
|
22
|
+
get_token_storage_path,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class OAuthContext:
|
|
30
|
+
"""Runtime state for an in-progress OAuth flow."""
|
|
31
|
+
|
|
32
|
+
state: str
|
|
33
|
+
code_verifier: str
|
|
34
|
+
code_challenge: str
|
|
35
|
+
created_at: float
|
|
36
|
+
redirect_uri: Optional[str] = None
|
|
37
|
+
expires_at: Optional[float] = None # Add expiration time
|
|
38
|
+
|
|
39
|
+
def is_expired(self) -> bool:
|
|
40
|
+
"""Check if this OAuth context has expired."""
|
|
41
|
+
if self.expires_at is None:
|
|
42
|
+
# Default 5 minute expiration if not set
|
|
43
|
+
return time.time() - self.created_at > 300
|
|
44
|
+
return time.time() > self.expires_at
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _urlsafe_b64encode(data: bytes) -> str:
|
|
48
|
+
return base64.urlsafe_b64encode(data).decode("utf-8").rstrip("=")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _generate_code_verifier() -> str:
|
|
52
|
+
return secrets.token_hex(64)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _compute_code_challenge(code_verifier: str) -> str:
|
|
56
|
+
digest = hashlib.sha256(code_verifier.encode("utf-8")).digest()
|
|
57
|
+
return _urlsafe_b64encode(digest)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def prepare_oauth_context() -> OAuthContext:
|
|
61
|
+
"""Create a fresh OAuth PKCE context."""
|
|
62
|
+
state = secrets.token_hex(32)
|
|
63
|
+
code_verifier = _generate_code_verifier()
|
|
64
|
+
code_challenge = _compute_code_challenge(code_verifier)
|
|
65
|
+
|
|
66
|
+
# Set expiration 4 minutes from now (OpenAI sessions are short)
|
|
67
|
+
expires_at = time.time() + 240
|
|
68
|
+
|
|
69
|
+
return OAuthContext(
|
|
70
|
+
state=state,
|
|
71
|
+
code_verifier=code_verifier,
|
|
72
|
+
code_challenge=code_challenge,
|
|
73
|
+
created_at=time.time(),
|
|
74
|
+
expires_at=expires_at,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def assign_redirect_uri(context: OAuthContext, port: int) -> str:
|
|
79
|
+
"""Assign redirect URI for the given OAuth context."""
|
|
80
|
+
if context is None:
|
|
81
|
+
raise RuntimeError("OAuth context cannot be None")
|
|
82
|
+
host = CHATGPT_OAUTH_CONFIG["redirect_host"].rstrip("/")
|
|
83
|
+
path = CHATGPT_OAUTH_CONFIG["redirect_path"].lstrip("/")
|
|
84
|
+
required_port = CHATGPT_OAUTH_CONFIG.get("required_port")
|
|
85
|
+
if required_port and port != required_port:
|
|
86
|
+
raise RuntimeError(
|
|
87
|
+
f"OAuth flow must use port {required_port}; attempted to assign port {port}"
|
|
88
|
+
)
|
|
89
|
+
redirect_uri = f"{host}:{port}/{path}"
|
|
90
|
+
context.redirect_uri = redirect_uri
|
|
91
|
+
return redirect_uri
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def build_authorization_url(context: OAuthContext) -> str:
|
|
95
|
+
"""Return the OpenAI authorization URL with PKCE parameters."""
|
|
96
|
+
if not context.redirect_uri:
|
|
97
|
+
raise RuntimeError("Redirect URI has not been assigned for this OAuth context")
|
|
98
|
+
|
|
99
|
+
params = {
|
|
100
|
+
"response_type": "code",
|
|
101
|
+
"client_id": CHATGPT_OAUTH_CONFIG["client_id"],
|
|
102
|
+
"redirect_uri": context.redirect_uri,
|
|
103
|
+
"scope": CHATGPT_OAUTH_CONFIG["scope"],
|
|
104
|
+
"code_challenge": context.code_challenge,
|
|
105
|
+
"code_challenge_method": "S256",
|
|
106
|
+
"id_token_add_organizations": "true",
|
|
107
|
+
"codex_cli_simplified_flow": "true",
|
|
108
|
+
"state": context.state,
|
|
109
|
+
}
|
|
110
|
+
return f"{CHATGPT_OAUTH_CONFIG['auth_url']}?{urlencode(params)}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def parse_authorization_error(url: str) -> Optional[str]:
|
|
114
|
+
"""Parse error from OAuth callback URL."""
|
|
115
|
+
try:
|
|
116
|
+
parsed = urlparse(url)
|
|
117
|
+
params = urllib_parse_qs(parsed.query)
|
|
118
|
+
error = params.get("error", [None])[0]
|
|
119
|
+
error_description = params.get("error_description", [None])[0]
|
|
120
|
+
if error:
|
|
121
|
+
return f"{error}: {error_description or 'Unknown error'}"
|
|
122
|
+
except Exception as exc:
|
|
123
|
+
logger.error("Failed to parse OAuth error: %s", exc)
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def parse_jwt_claims(token: str) -> Optional[Dict[str, Any]]:
|
|
128
|
+
"""Parse JWT token to extract claims."""
|
|
129
|
+
if not token or token.count(".") != 2:
|
|
130
|
+
return None
|
|
131
|
+
try:
|
|
132
|
+
_, payload, _ = token.split(".")
|
|
133
|
+
padded = payload + "=" * (-len(payload) % 4)
|
|
134
|
+
data = base64.urlsafe_b64decode(padded.encode())
|
|
135
|
+
return json.loads(data.decode())
|
|
136
|
+
except Exception as exc:
|
|
137
|
+
logger.error("Failed to parse JWT: %s", exc)
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def load_stored_tokens() -> Optional[Dict[str, Any]]:
|
|
142
|
+
try:
|
|
143
|
+
token_path = get_token_storage_path()
|
|
144
|
+
if token_path.exists():
|
|
145
|
+
with open(token_path, "r", encoding="utf-8") as handle:
|
|
146
|
+
return json.load(handle)
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
logger.error("Failed to load tokens: %s", exc)
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def get_valid_access_token() -> Optional[str]:
|
|
153
|
+
"""Get a valid access token, refreshing if expired.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Valid access token string, or None if not authenticated or refresh failed.
|
|
157
|
+
"""
|
|
158
|
+
tokens = load_stored_tokens()
|
|
159
|
+
if not tokens:
|
|
160
|
+
logger.debug("No stored ChatGPT OAuth tokens found")
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
access_token = tokens.get("access_token")
|
|
164
|
+
if not access_token:
|
|
165
|
+
logger.debug("No access_token in stored tokens")
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
# Check if token is expired by parsing JWT claims
|
|
169
|
+
claims = parse_jwt_claims(access_token)
|
|
170
|
+
if claims:
|
|
171
|
+
exp = claims.get("exp")
|
|
172
|
+
if exp and isinstance(exp, (int, float)):
|
|
173
|
+
# Add 30 second buffer before expiry
|
|
174
|
+
if time.time() > exp - 30:
|
|
175
|
+
logger.info("ChatGPT OAuth token expired, attempting refresh")
|
|
176
|
+
refreshed = refresh_access_token()
|
|
177
|
+
if refreshed:
|
|
178
|
+
return refreshed
|
|
179
|
+
logger.warning("Token refresh failed")
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
return access_token
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def refresh_access_token() -> Optional[str]:
|
|
186
|
+
"""Refresh the access token using the refresh token.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
New access token if refresh succeeded, None otherwise.
|
|
190
|
+
"""
|
|
191
|
+
tokens = load_stored_tokens()
|
|
192
|
+
if not tokens:
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
refresh_token = tokens.get("refresh_token")
|
|
196
|
+
if not refresh_token:
|
|
197
|
+
logger.debug("No refresh_token available")
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
payload = {
|
|
201
|
+
"grant_type": "refresh_token",
|
|
202
|
+
"refresh_token": refresh_token,
|
|
203
|
+
"client_id": CHATGPT_OAUTH_CONFIG["client_id"],
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
headers = {
|
|
207
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
response = requests.post(
|
|
212
|
+
CHATGPT_OAUTH_CONFIG["token_url"],
|
|
213
|
+
data=payload,
|
|
214
|
+
headers=headers,
|
|
215
|
+
timeout=30,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if response.status_code == 200:
|
|
219
|
+
new_tokens = response.json()
|
|
220
|
+
# Merge with existing tokens (preserve account_id, etc.)
|
|
221
|
+
tokens.update(
|
|
222
|
+
{
|
|
223
|
+
"access_token": new_tokens.get("access_token"),
|
|
224
|
+
"refresh_token": new_tokens.get("refresh_token", refresh_token),
|
|
225
|
+
"id_token": new_tokens.get("id_token", tokens.get("id_token")),
|
|
226
|
+
"last_refresh": datetime.datetime.now(datetime.timezone.utc)
|
|
227
|
+
.isoformat()
|
|
228
|
+
.replace("+00:00", "Z"),
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
if save_tokens(tokens):
|
|
232
|
+
logger.info("Successfully refreshed ChatGPT OAuth token")
|
|
233
|
+
return tokens["access_token"]
|
|
234
|
+
else:
|
|
235
|
+
logger.error(
|
|
236
|
+
"Token refresh failed: %s - %s", response.status_code, response.text
|
|
237
|
+
)
|
|
238
|
+
except Exception as exc:
|
|
239
|
+
logger.error("Token refresh error: %s", exc)
|
|
240
|
+
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def save_tokens(tokens: Dict[str, Any]) -> bool:
|
|
245
|
+
if tokens is None:
|
|
246
|
+
raise TypeError("tokens cannot be None")
|
|
247
|
+
try:
|
|
248
|
+
token_path = get_token_storage_path()
|
|
249
|
+
with open(token_path, "w", encoding="utf-8") as handle:
|
|
250
|
+
json.dump(tokens, handle, indent=2)
|
|
251
|
+
token_path.chmod(0o600)
|
|
252
|
+
return True
|
|
253
|
+
except Exception as exc:
|
|
254
|
+
logger.error("Failed to save tokens: %s", exc)
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def load_chatgpt_models() -> Dict[str, Any]:
|
|
259
|
+
try:
|
|
260
|
+
models_path = get_chatgpt_models_path()
|
|
261
|
+
if models_path.exists():
|
|
262
|
+
with open(models_path, "r", encoding="utf-8") as handle:
|
|
263
|
+
return json.load(handle)
|
|
264
|
+
except Exception as exc:
|
|
265
|
+
logger.error("Failed to load ChatGPT models: %s", exc)
|
|
266
|
+
return {}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def save_chatgpt_models(models: Dict[str, Any]) -> bool:
|
|
270
|
+
try:
|
|
271
|
+
models_path = get_chatgpt_models_path()
|
|
272
|
+
with open(models_path, "w", encoding="utf-8") as handle:
|
|
273
|
+
json.dump(models, handle, indent=2)
|
|
274
|
+
return True
|
|
275
|
+
except Exception as exc:
|
|
276
|
+
logger.error("Failed to save ChatGPT models: %s", exc)
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def exchange_code_for_tokens(
|
|
281
|
+
auth_code: str, context: OAuthContext
|
|
282
|
+
) -> Optional[Dict[str, Any]]:
|
|
283
|
+
"""Exchange authorization code for access tokens."""
|
|
284
|
+
if not context.redirect_uri:
|
|
285
|
+
raise RuntimeError("Redirect URI missing from OAuth context")
|
|
286
|
+
|
|
287
|
+
if context.is_expired():
|
|
288
|
+
logger.error("OAuth context expired, cannot exchange code")
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
payload = {
|
|
292
|
+
"grant_type": "authorization_code",
|
|
293
|
+
"code": auth_code,
|
|
294
|
+
"redirect_uri": context.redirect_uri,
|
|
295
|
+
"client_id": CHATGPT_OAUTH_CONFIG["client_id"],
|
|
296
|
+
"code_verifier": context.code_verifier,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
headers = {
|
|
300
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
logger.info("Exchanging code for tokens: %s", CHATGPT_OAUTH_CONFIG["token_url"])
|
|
304
|
+
try:
|
|
305
|
+
response = requests.post(
|
|
306
|
+
CHATGPT_OAUTH_CONFIG["token_url"],
|
|
307
|
+
data=payload,
|
|
308
|
+
headers=headers,
|
|
309
|
+
timeout=30,
|
|
310
|
+
)
|
|
311
|
+
logger.info("Token exchange response: %s", response.status_code)
|
|
312
|
+
if response.status_code == 200:
|
|
313
|
+
token_data = response.json()
|
|
314
|
+
# Add timestamp
|
|
315
|
+
token_data["last_refresh"] = (
|
|
316
|
+
datetime.datetime.now(datetime.timezone.utc)
|
|
317
|
+
.isoformat()
|
|
318
|
+
.replace("+00:00", "Z")
|
|
319
|
+
)
|
|
320
|
+
return token_data
|
|
321
|
+
else:
|
|
322
|
+
logger.error(
|
|
323
|
+
"Token exchange failed: %s - %s",
|
|
324
|
+
response.status_code,
|
|
325
|
+
response.text,
|
|
326
|
+
)
|
|
327
|
+
# Try to parse OAuth error
|
|
328
|
+
if response.headers.get("content-type", "").startswith("application/json"):
|
|
329
|
+
try:
|
|
330
|
+
error_data = response.json()
|
|
331
|
+
if "error" in error_data:
|
|
332
|
+
logger.error(
|
|
333
|
+
"OAuth error: %s",
|
|
334
|
+
error_data.get("error_description", error_data["error"]),
|
|
335
|
+
)
|
|
336
|
+
except Exception:
|
|
337
|
+
pass
|
|
338
|
+
except Exception as exc:
|
|
339
|
+
logger.error("Token exchange error: %s", exc)
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# Default models available via ChatGPT Codex API
|
|
344
|
+
# These are the known models that work with ChatGPT OAuth tokens
|
|
345
|
+
# Based on codex-rs CLI and shell-scripts/codex-call.sh
|
|
346
|
+
DEFAULT_CODEX_MODELS = [
|
|
347
|
+
"gpt-5.2",
|
|
348
|
+
"gpt-5.2-codex",
|
|
349
|
+
]
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def fetch_chatgpt_models(access_token: str, account_id: str) -> Optional[List[str]]:
|
|
353
|
+
"""Fetch available models from ChatGPT Codex API.
|
|
354
|
+
|
|
355
|
+
Attempts to fetch models from the API, but falls back to a default list
|
|
356
|
+
of known Codex-compatible models if the API is unavailable.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
access_token: OAuth access token for authentication
|
|
360
|
+
account_id: ChatGPT account ID (required for the API)
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
List of model IDs, or default list if API fails
|
|
364
|
+
"""
|
|
365
|
+
import platform
|
|
366
|
+
|
|
367
|
+
# Build the models URL with client version
|
|
368
|
+
client_version = CHATGPT_OAUTH_CONFIG.get("client_version", "0.72.0")
|
|
369
|
+
base_url = CHATGPT_OAUTH_CONFIG["api_base_url"].rstrip("/")
|
|
370
|
+
models_url = f"{base_url}/models"
|
|
371
|
+
|
|
372
|
+
# Build User-Agent to match codex-rs CLI format
|
|
373
|
+
originator = CHATGPT_OAUTH_CONFIG.get("originator", "codex_cli_rs")
|
|
374
|
+
os_name = platform.system()
|
|
375
|
+
if os_name == "Darwin":
|
|
376
|
+
os_name = "Mac OS"
|
|
377
|
+
os_version = platform.release()
|
|
378
|
+
arch = platform.machine()
|
|
379
|
+
user_agent = (
|
|
380
|
+
f"{originator}/{client_version} ({os_name} {os_version}; {arch}) "
|
|
381
|
+
"Terminal_Codex_CLI"
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
headers = {
|
|
385
|
+
"Authorization": f"Bearer {access_token}",
|
|
386
|
+
"ChatGPT-Account-Id": account_id,
|
|
387
|
+
"User-Agent": user_agent,
|
|
388
|
+
"originator": originator,
|
|
389
|
+
"Accept": "application/json",
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
# Query params
|
|
393
|
+
params = {"client_version": client_version}
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
response = requests.get(models_url, headers=headers, params=params, timeout=30)
|
|
397
|
+
|
|
398
|
+
if response.status_code == 200:
|
|
399
|
+
# Parse JSON response
|
|
400
|
+
try:
|
|
401
|
+
data = response.json()
|
|
402
|
+
# The response has a "models" key with list of model objects
|
|
403
|
+
if "models" in data and isinstance(data["models"], list):
|
|
404
|
+
models = []
|
|
405
|
+
for model in data["models"]:
|
|
406
|
+
if model is None:
|
|
407
|
+
continue
|
|
408
|
+
model_id = (
|
|
409
|
+
model.get("slug") or model.get("id") or model.get("name")
|
|
410
|
+
)
|
|
411
|
+
if model_id:
|
|
412
|
+
models.append(model_id)
|
|
413
|
+
if models:
|
|
414
|
+
return models
|
|
415
|
+
except (json.JSONDecodeError, ValueError) as exc:
|
|
416
|
+
logger.warning("Failed to parse models response: %s", exc)
|
|
417
|
+
|
|
418
|
+
# API didn't return valid models, use default list
|
|
419
|
+
logger.info(
|
|
420
|
+
"Models endpoint returned %d, using default model list",
|
|
421
|
+
response.status_code,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
except requests.exceptions.Timeout:
|
|
425
|
+
logger.warning("Timeout fetching models, using default list")
|
|
426
|
+
except requests.exceptions.RequestException as exc:
|
|
427
|
+
logger.warning("Network error fetching models: %s, using default list", exc)
|
|
428
|
+
except Exception as exc:
|
|
429
|
+
logger.warning("Error fetching models: %s, using default list", exc)
|
|
430
|
+
|
|
431
|
+
# Return default models when API fails
|
|
432
|
+
logger.info("Using default Codex models: %s", DEFAULT_CODEX_MODELS)
|
|
433
|
+
return DEFAULT_CODEX_MODELS
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def add_models_to_extra_config(models: List[str]) -> bool:
|
|
437
|
+
"""Add ChatGPT models to chatgpt_models.json configuration."""
|
|
438
|
+
try:
|
|
439
|
+
chatgpt_models = load_chatgpt_models()
|
|
440
|
+
added = 0
|
|
441
|
+
for model_name in models:
|
|
442
|
+
prefixed = f"{CHATGPT_OAUTH_CONFIG['prefix']}{model_name}"
|
|
443
|
+
|
|
444
|
+
# Determine supported settings based on model type
|
|
445
|
+
# All GPT-5.x models support reasoning_effort and verbosity
|
|
446
|
+
supported_settings = ["reasoning_effort", "verbosity"]
|
|
447
|
+
|
|
448
|
+
# Only codex models support xhigh reasoning effort
|
|
449
|
+
# Regular gpt-5.2 is capped at "high"
|
|
450
|
+
is_codex = "codex" in model_name.lower()
|
|
451
|
+
|
|
452
|
+
chatgpt_models[prefixed] = {
|
|
453
|
+
"type": "chatgpt_oauth",
|
|
454
|
+
"name": model_name,
|
|
455
|
+
"custom_endpoint": {
|
|
456
|
+
# Codex API uses chatgpt.com/backend-api/codex, not api.openai.com
|
|
457
|
+
"url": CHATGPT_OAUTH_CONFIG["api_base_url"],
|
|
458
|
+
},
|
|
459
|
+
"context_length": CHATGPT_OAUTH_CONFIG["default_context_length"],
|
|
460
|
+
"oauth_source": "chatgpt-oauth-plugin",
|
|
461
|
+
"supported_settings": supported_settings,
|
|
462
|
+
"supports_xhigh_reasoning": is_codex,
|
|
463
|
+
}
|
|
464
|
+
added += 1
|
|
465
|
+
if save_chatgpt_models(chatgpt_models):
|
|
466
|
+
logger.info("Added %s ChatGPT models", added)
|
|
467
|
+
return True
|
|
468
|
+
except Exception as exc:
|
|
469
|
+
logger.error("Error adding models to config: %s", exc)
|
|
470
|
+
return False
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def remove_chatgpt_models() -> int:
|
|
474
|
+
"""Remove ChatGPT OAuth models from chatgpt_models.json."""
|
|
475
|
+
try:
|
|
476
|
+
chatgpt_models = load_chatgpt_models()
|
|
477
|
+
to_remove = [
|
|
478
|
+
name
|
|
479
|
+
for name, config in chatgpt_models.items()
|
|
480
|
+
if config.get("oauth_source") == "chatgpt-oauth-plugin"
|
|
481
|
+
]
|
|
482
|
+
for model_name in to_remove:
|
|
483
|
+
chatgpt_models.pop(model_name, None)
|
|
484
|
+
# Always save, even if no models were removed (to match test expectations)
|
|
485
|
+
if save_chatgpt_models(chatgpt_models):
|
|
486
|
+
return len(to_remove)
|
|
487
|
+
except Exception as exc:
|
|
488
|
+
logger.error("Error removing ChatGPT models: %s", exc)
|
|
489
|
+
return 0
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Claude Code OAuth Plugin
|
|
2
|
+
|
|
3
|
+
This plugin adds OAuth authentication for Claude Code to Code Puppy, automatically importing available models into your configuration.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **OAuth Authentication**: Secure OAuth flow for Claude Code using PKCE
|
|
8
|
+
- **Automatic Model Discovery**: Fetches available models from the Claude API once authenticated
|
|
9
|
+
- **Model Registration**: Automatically adds models to `extra_models.json` with the `claude-code-` prefix
|
|
10
|
+
- **Token Management**: Secure storage of OAuth tokens in the Code Puppy config directory
|
|
11
|
+
- **Browser Integration**: Launches the Claude OAuth consent flow automatically
|
|
12
|
+
- **Callback Capture**: Listens on localhost to receive and process the OAuth redirect
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
### `/claude-code-auth`
|
|
17
|
+
Authenticate with Claude Code via OAuth and import available models.
|
|
18
|
+
|
|
19
|
+
This will:
|
|
20
|
+
1. Launch the Claude OAuth consent flow in your browser
|
|
21
|
+
2. Walk you through approving access for the shared `claude-cli` client
|
|
22
|
+
3. Capture the redirect from Claude in a temporary local callback server
|
|
23
|
+
4. Exchange the returned code for access tokens and store them securely
|
|
24
|
+
5. Fetch available models from Claude Code and add them to your configuration
|
|
25
|
+
|
|
26
|
+
### `/claude-code-status`
|
|
27
|
+
Check Claude Code OAuth authentication status and configured models.
|
|
28
|
+
|
|
29
|
+
Shows:
|
|
30
|
+
- Current authentication status
|
|
31
|
+
- Token expiry information (if available)
|
|
32
|
+
- Number and names of configured Claude Code models
|
|
33
|
+
|
|
34
|
+
### `/claude-code-logout`
|
|
35
|
+
Remove Claude Code OAuth tokens and imported models.
|
|
36
|
+
|
|
37
|
+
This will:
|
|
38
|
+
1. Remove stored OAuth tokens
|
|
39
|
+
2. Remove all Claude Code models from `extra_models.json`
|
|
40
|
+
|
|
41
|
+
## Setup
|
|
42
|
+
|
|
43
|
+
### Prerequisites
|
|
44
|
+
|
|
45
|
+
1. **Claude account** with access to the Claude Console developer settings
|
|
46
|
+
2. **Browser access** to generate authorization codes
|
|
47
|
+
|
|
48
|
+
### Configuration
|
|
49
|
+
|
|
50
|
+
The plugin ships with sensible defaults in `config.py`:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
CLAUDE_CODE_OAUTH_CONFIG = {
|
|
54
|
+
"auth_url": "https://claude.ai/oauth/authorize",
|
|
55
|
+
"token_url": "https://claude.ai/api/oauth/token",
|
|
56
|
+
"api_base_url": "https://api.anthropic.com",
|
|
57
|
+
"client_id": "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
|
58
|
+
"scope": "org:create_api_key user:profile user:inference",
|
|
59
|
+
"redirect_host": "http://localhost",
|
|
60
|
+
"redirect_path": "callback",
|
|
61
|
+
"callback_port_range": (8765, 8795),
|
|
62
|
+
"callback_timeout": 180,
|
|
63
|
+
"prefix": "claude-code-",
|
|
64
|
+
"default_context_length": 200000,
|
|
65
|
+
"api_key_env_var": "CLAUDE_CODE_ACCESS_TOKEN",
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
These values mirror the public client used by llxprt-code. Adjust only if Anthropic changes their configuration.
|
|
70
|
+
|
|
71
|
+
### Environment Variables
|
|
72
|
+
|
|
73
|
+
After authentication, the models will reference:
|
|
74
|
+
- `CLAUDE_CODE_ACCESS_TOKEN`: Automatically written by the plugin
|
|
75
|
+
|
|
76
|
+
## Usage Example
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Authenticate with Claude Code
|
|
80
|
+
/claude-code-auth
|
|
81
|
+
|
|
82
|
+
# Check status
|
|
83
|
+
/claude-code-status
|
|
84
|
+
|
|
85
|
+
# Use a Claude Code model
|
|
86
|
+
/set model claude-code-claude-3-5-sonnet-20241022
|
|
87
|
+
|
|
88
|
+
# When done, logout
|
|
89
|
+
/claude-code-logout
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Model Configuration
|
|
93
|
+
|
|
94
|
+
After authentication, models will be added to `~/.code_puppy/extra_models.json`:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"claude-code-claude-3-5-sonnet-20241022": {
|
|
99
|
+
"type": "anthropic",
|
|
100
|
+
"name": "claude-3-5-sonnet-20241022",
|
|
101
|
+
"custom_endpoint": {
|
|
102
|
+
"url": "https://api.anthropic.com",
|
|
103
|
+
"api_key": "$CLAUDE_CODE_ACCESS_TOKEN"
|
|
104
|
+
},
|
|
105
|
+
"context_length": 200000,
|
|
106
|
+
"oauth_source": "claude-code-plugin"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Security
|
|
112
|
+
|
|
113
|
+
- **Token Storage**: Tokens are saved to `~/.code_puppy/claude_code_oauth.json` with `0o600` permissions
|
|
114
|
+
- **PKCE Support**: Uses Proof Key for Code Exchange for enhanced security
|
|
115
|
+
- **State Validation**: Checks the returned state (if provided) to guard against CSRF
|
|
116
|
+
- **HTTPS Only**: All OAuth communications use HTTPS endpoints
|
|
117
|
+
|
|
118
|
+
## Troubleshooting
|
|
119
|
+
|
|
120
|
+
### Browser doesn't open
|
|
121
|
+
- Manually visit the URL shown in the output
|
|
122
|
+
- Check that a default browser is configured
|
|
123
|
+
|
|
124
|
+
### Authentication fails
|
|
125
|
+
- Ensure the browser completed the redirect back to Code Puppy (no pop-up blockers)
|
|
126
|
+
- Retry if the window shows an error; codes expire quickly
|
|
127
|
+
- Confirm network access to `claude.ai`
|
|
128
|
+
|
|
129
|
+
### Models not showing up
|
|
130
|
+
- Claude may not return the model list for your account; verify access manually
|
|
131
|
+
- Check `/claude-code-status` to confirm authentication succeeded
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
### File Structure
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
claude_code_oauth/
|
|
139
|
+
├── __init__.py
|
|
140
|
+
├── register_callbacks.py # Main plugin logic and command handlers
|
|
141
|
+
├── config.py # Configuration settings
|
|
142
|
+
├── utils.py # OAuth helpers and file operations
|
|
143
|
+
├── README.md # This file
|
|
144
|
+
├── SETUP.md # Quick setup guide
|
|
145
|
+
└── test_plugin.py # Manual test helper
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Key Components
|
|
149
|
+
|
|
150
|
+
- **OAuth Flow**: Authorization code flow with PKCE and automatic callback capture
|
|
151
|
+
- **Token Management**: Secure storage and retrieval helpers
|
|
152
|
+
- **Model Discovery**: API integration for model fetching
|
|
153
|
+
- **Plugin Registration**: Custom command handlers wired into Code Puppy
|
|
154
|
+
|
|
155
|
+
## Notes
|
|
156
|
+
|
|
157
|
+
- The plugin assumes Anthropic continues to expose the shared `claude-cli` OAuth client
|
|
158
|
+
- Tokens are refreshed on subsequent API calls if the service returns refresh tokens
|
|
159
|
+
- Models are prefixed with `claude-code-` to avoid collisions with other Anthropic models
|
|
160
|
+
|
|
161
|
+
## Contributing
|
|
162
|
+
|
|
163
|
+
When modifying this plugin:
|
|
164
|
+
1. Maintain security best practices
|
|
165
|
+
2. Test OAuth flow changes manually before shipping
|
|
166
|
+
3. Update documentation for any configuration or UX changes
|
|
167
|
+
4. Keep files under 600 lines; split into helpers when needed
|