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,93 @@
|
|
|
1
|
+
# Claude Code OAuth Plugin Setup Guide
|
|
2
|
+
|
|
3
|
+
This guide walks you through using the Claude Code OAuth plugin inside Code Puppy.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
1. Ensure the plugin files live under `code_puppy/plugins/claude_code_oauth/`
|
|
8
|
+
2. Restart Code Puppy so it loads the plugin
|
|
9
|
+
3. Run `/claude-code-auth` and follow the prompts
|
|
10
|
+
|
|
11
|
+
## Why No Client Registration?
|
|
12
|
+
|
|
13
|
+
Anthropic exposes a shared **public client** (`claude-cli`) for command-line tools. That means:
|
|
14
|
+
- No client secret is needed
|
|
15
|
+
- Everyone authenticates through Claude Console
|
|
16
|
+
- Security is enforced with PKCE and per-user tokens
|
|
17
|
+
|
|
18
|
+
## Authentication Flow
|
|
19
|
+
|
|
20
|
+
1. Call `/claude-code-auth`
|
|
21
|
+
2. Your browser opens the Claude OAuth consent flow at `https://claude.ai/oauth/authorize`
|
|
22
|
+
3. Sign in (or pick an account) and approve the "Claude CLI" access request
|
|
23
|
+
4. The browser closes automatically after the redirect is captured
|
|
24
|
+
5. Tokens are stored locally at `~/.code_puppy/claude_code_oauth.json`
|
|
25
|
+
6. Available Claude Code models are fetched and added to `extra_models.json`
|
|
26
|
+
|
|
27
|
+
## Commands Recap
|
|
28
|
+
|
|
29
|
+
- `/claude-code-auth` – Authenticate and sync models
|
|
30
|
+
- `/claude-code-status` – Show auth status, expiry, configured models
|
|
31
|
+
- `/claude-code-logout` – Remove tokens and any models added by the plugin
|
|
32
|
+
|
|
33
|
+
## Configuration Defaults
|
|
34
|
+
|
|
35
|
+
`config.py` ships with values aligned to llxprt-code:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
CLAUDE_CODE_OAUTH_CONFIG = {
|
|
39
|
+
"auth_url": "https://claude.ai/oauth/authorize",
|
|
40
|
+
"token_url": "https://claude.ai/api/oauth/token",
|
|
41
|
+
"api_base_url": "https://api.anthropic.com",
|
|
42
|
+
"client_id": "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
|
43
|
+
"scope": "org:create_api_key user:profile user:inference",
|
|
44
|
+
"redirect_host": "http://localhost",
|
|
45
|
+
"redirect_path": "callback",
|
|
46
|
+
"callback_port_range": (8765, 8795),
|
|
47
|
+
"callback_timeout": 180,
|
|
48
|
+
"prefix": "claude-code-",
|
|
49
|
+
"default_context_length": 200000,
|
|
50
|
+
"api_key_env_var": "CLAUDE_CODE_ACCESS_TOKEN",
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Change these only if Anthropic updates their endpoints or scopes.
|
|
55
|
+
|
|
56
|
+
## After Authentication
|
|
57
|
+
|
|
58
|
+
- Models appear in `~/.code_puppy/extra_models.json` with the `claude-code-` prefix
|
|
59
|
+
- The environment variable `CLAUDE_CODE_ACCESS_TOKEN` is used by those models
|
|
60
|
+
- `/claude-code-status` shows token expiry when the API provides it
|
|
61
|
+
|
|
62
|
+
## Troubleshooting Tips
|
|
63
|
+
|
|
64
|
+
- **Browser did not open** – Copy the displayed URL into your browser manually
|
|
65
|
+
- **Invalid code** – The code expires quickly; generate a new one in Claude Console
|
|
66
|
+
- **State mismatch** – Rare, but rerun `/claude-code-auth` if the browser reports a mismatch
|
|
67
|
+
- **No models added** – Your account might lack Claude Code access; tokens are still stored for later use
|
|
68
|
+
|
|
69
|
+
## Files Created
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
~/.code_puppy/
|
|
73
|
+
├── claude_code_oauth.json # OAuth tokens (0600 permissions)
|
|
74
|
+
└── extra_models.json # Extended model registry
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Manual Testing
|
|
78
|
+
|
|
79
|
+
Run the helper script for sanity checks:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
python code_puppy/plugins/claude_code_oauth/test_plugin.py
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
It verifies imports, configuration values, and filesystem expectations without hitting the Anthropic API.
|
|
86
|
+
|
|
87
|
+
## Security Notes
|
|
88
|
+
|
|
89
|
+
- Tokens are stored locally and never transmitted elsewhere
|
|
90
|
+
- PKCE protects the flow even without a client secret
|
|
91
|
+
- HTTPS endpoints are enforced for all requests
|
|
92
|
+
|
|
93
|
+
Enjoy hacking with Claude Code straight from Code Puppy! 🐶💻
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
from code_puppy import config
|
|
5
|
+
|
|
6
|
+
# Claude Code OAuth configuration
|
|
7
|
+
CLAUDE_CODE_OAUTH_CONFIG: Dict[str, Any] = {
|
|
8
|
+
# OAuth endpoints inferred from official Claude Code OAuth flow
|
|
9
|
+
"auth_url": "https://claude.ai/oauth/authorize",
|
|
10
|
+
"token_url": "https://console.anthropic.com/v1/oauth/token",
|
|
11
|
+
"api_base_url": "https://api.anthropic.com",
|
|
12
|
+
# OAuth client configuration observed in Claude Code CLI flow
|
|
13
|
+
"client_id": "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
|
14
|
+
"scope": "org:create_api_key user:profile user:inference",
|
|
15
|
+
# Callback handling (we host a localhost callback to capture the redirect)
|
|
16
|
+
"redirect_host": "http://localhost",
|
|
17
|
+
"redirect_path": "callback",
|
|
18
|
+
"callback_port_range": (8765, 8795),
|
|
19
|
+
"callback_timeout": 180,
|
|
20
|
+
# Console redirect fallback (for manual flows, if needed)
|
|
21
|
+
"console_redirect_uri": "https://console.anthropic.com/oauth/code/callback",
|
|
22
|
+
# Local configuration (uses XDG_DATA_HOME)
|
|
23
|
+
"token_storage": None, # Set dynamically in get_token_storage_path()
|
|
24
|
+
# Model configuration
|
|
25
|
+
"prefix": "claude-code-",
|
|
26
|
+
"default_context_length": 200000,
|
|
27
|
+
"api_key_env_var": "CLAUDE_CODE_ACCESS_TOKEN",
|
|
28
|
+
"anthropic_version": "2023-06-01",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_token_storage_path() -> Path:
|
|
33
|
+
"""Get the path for storing OAuth tokens (uses XDG_DATA_HOME)."""
|
|
34
|
+
data_dir = Path(config.DATA_DIR)
|
|
35
|
+
data_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
36
|
+
return data_dir / "claude_code_oauth.json"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_config_dir() -> Path:
|
|
40
|
+
"""Get the Code Puppy configuration directory (uses XDG_CONFIG_HOME)."""
|
|
41
|
+
config_dir = Path(config.CONFIG_DIR)
|
|
42
|
+
config_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
43
|
+
return config_dir
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_claude_models_path() -> Path:
|
|
47
|
+
"""Get the path to the dedicated claude_models.json file (uses XDG_DATA_HOME)."""
|
|
48
|
+
data_dir = Path(config.DATA_DIR)
|
|
49
|
+
data_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
50
|
+
return data_dir / "claude_models.json"
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Claude Code OAuth Plugin for Code Puppy.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
11
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
12
|
+
from urllib.parse import parse_qs, urlparse
|
|
13
|
+
|
|
14
|
+
from code_puppy.callbacks import register_callback
|
|
15
|
+
from code_puppy.config import set_model_name
|
|
16
|
+
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
17
|
+
|
|
18
|
+
from ..oauth_puppy_html import oauth_failure_html, oauth_success_html
|
|
19
|
+
from .config import CLAUDE_CODE_OAUTH_CONFIG, get_token_storage_path
|
|
20
|
+
from .utils import (
|
|
21
|
+
OAuthContext,
|
|
22
|
+
add_models_to_extra_config,
|
|
23
|
+
assign_redirect_uri,
|
|
24
|
+
build_authorization_url,
|
|
25
|
+
exchange_code_for_tokens,
|
|
26
|
+
fetch_claude_code_models,
|
|
27
|
+
load_claude_models_filtered,
|
|
28
|
+
load_stored_tokens,
|
|
29
|
+
prepare_oauth_context,
|
|
30
|
+
remove_claude_code_models,
|
|
31
|
+
save_tokens,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _OAuthResult:
|
|
38
|
+
def __init__(self) -> None:
|
|
39
|
+
self.code: Optional[str] = None
|
|
40
|
+
self.state: Optional[str] = None
|
|
41
|
+
self.error: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _CallbackHandler(BaseHTTPRequestHandler):
|
|
45
|
+
result: _OAuthResult
|
|
46
|
+
received_event: threading.Event
|
|
47
|
+
|
|
48
|
+
def do_GET(self) -> None: # noqa: N802
|
|
49
|
+
logger.info("Callback received: path=%s", self.path)
|
|
50
|
+
parsed = urlparse(self.path)
|
|
51
|
+
params: Dict[str, List[str]] = parse_qs(parsed.query)
|
|
52
|
+
|
|
53
|
+
code = params.get("code", [None])[0]
|
|
54
|
+
state = params.get("state", [None])[0]
|
|
55
|
+
|
|
56
|
+
if code and state:
|
|
57
|
+
self.result.code = code
|
|
58
|
+
self.result.state = state
|
|
59
|
+
success_html = oauth_success_html(
|
|
60
|
+
"Claude Code",
|
|
61
|
+
"You're totally synced with Claude Code now!",
|
|
62
|
+
)
|
|
63
|
+
self._write_response(200, success_html)
|
|
64
|
+
else:
|
|
65
|
+
self.result.error = "Missing code or state"
|
|
66
|
+
failure_html = oauth_failure_html(
|
|
67
|
+
"Claude Code",
|
|
68
|
+
"Missing code or state parameter 🥺",
|
|
69
|
+
)
|
|
70
|
+
self._write_response(400, failure_html)
|
|
71
|
+
|
|
72
|
+
self.received_event.set()
|
|
73
|
+
|
|
74
|
+
def log_message(self, format: str, *args: Any) -> None: # noqa: A003
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
def _write_response(self, status: int, body: str) -> None:
|
|
78
|
+
self.send_response(status)
|
|
79
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
80
|
+
self.end_headers()
|
|
81
|
+
self.wfile.write(body.encode("utf-8"))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _start_callback_server(
|
|
85
|
+
context: OAuthContext,
|
|
86
|
+
) -> Optional[Tuple[HTTPServer, _OAuthResult, threading.Event]]:
|
|
87
|
+
port_range = CLAUDE_CODE_OAUTH_CONFIG["callback_port_range"]
|
|
88
|
+
|
|
89
|
+
for port in range(port_range[0], port_range[1] + 1):
|
|
90
|
+
try:
|
|
91
|
+
server = HTTPServer(("localhost", port), _CallbackHandler)
|
|
92
|
+
assign_redirect_uri(context, port)
|
|
93
|
+
result = _OAuthResult()
|
|
94
|
+
event = threading.Event()
|
|
95
|
+
_CallbackHandler.result = result
|
|
96
|
+
_CallbackHandler.received_event = event
|
|
97
|
+
|
|
98
|
+
def run_server() -> None:
|
|
99
|
+
with server:
|
|
100
|
+
server.serve_forever()
|
|
101
|
+
|
|
102
|
+
threading.Thread(target=run_server, daemon=True).start()
|
|
103
|
+
return server, result, event
|
|
104
|
+
except OSError:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
emit_error("Could not start OAuth callback server; all candidate ports are in use")
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _await_callback(context: OAuthContext) -> Optional[str]:
|
|
112
|
+
timeout = CLAUDE_CODE_OAUTH_CONFIG["callback_timeout"]
|
|
113
|
+
|
|
114
|
+
started = _start_callback_server(context)
|
|
115
|
+
if not started:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
server, result, event = started
|
|
119
|
+
redirect_uri = context.redirect_uri
|
|
120
|
+
if not redirect_uri:
|
|
121
|
+
emit_error("Failed to assign redirect URI for OAuth flow")
|
|
122
|
+
server.shutdown()
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
auth_url = build_authorization_url(context)
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
import webbrowser
|
|
129
|
+
|
|
130
|
+
from code_puppy.tools.common import should_suppress_browser
|
|
131
|
+
|
|
132
|
+
if should_suppress_browser():
|
|
133
|
+
emit_info(
|
|
134
|
+
"[HEADLESS MODE] Would normally open browser for Claude Code OAuth…"
|
|
135
|
+
)
|
|
136
|
+
emit_info(f"In normal mode, would visit: {auth_url}")
|
|
137
|
+
else:
|
|
138
|
+
emit_info("Opening browser for Claude Code OAuth…")
|
|
139
|
+
webbrowser.open(auth_url)
|
|
140
|
+
emit_info(f"If it doesn't open automatically, visit: {auth_url}")
|
|
141
|
+
except Exception as exc: # pragma: no cover
|
|
142
|
+
if not should_suppress_browser():
|
|
143
|
+
emit_warning(f"Failed to open browser automatically: {exc}")
|
|
144
|
+
emit_info(f"Please open the URL manually: {auth_url}")
|
|
145
|
+
|
|
146
|
+
emit_info(f"Listening for callback on {redirect_uri}")
|
|
147
|
+
emit_info(
|
|
148
|
+
"If Claude redirects you to the console callback page, copy the full URL "
|
|
149
|
+
"and paste it back into Code Puppy."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if not event.wait(timeout=timeout):
|
|
153
|
+
emit_error("OAuth callback timed out. Please try again.")
|
|
154
|
+
server.shutdown()
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
server.shutdown()
|
|
158
|
+
|
|
159
|
+
if result.error:
|
|
160
|
+
emit_error(f"OAuth callback error: {result.error}")
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
if result.state != context.state:
|
|
164
|
+
emit_error("State mismatch detected; aborting authentication.")
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
return result.code
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _custom_help() -> List[Tuple[str, str]]:
|
|
171
|
+
return [
|
|
172
|
+
(
|
|
173
|
+
"claude-code-auth",
|
|
174
|
+
"Authenticate with Claude Code via OAuth and import available models",
|
|
175
|
+
),
|
|
176
|
+
(
|
|
177
|
+
"claude-code-status",
|
|
178
|
+
"Check Claude Code OAuth authentication status and configured models",
|
|
179
|
+
),
|
|
180
|
+
("claude-code-logout", "Remove Claude Code OAuth tokens and imported models"),
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _reload_current_agent() -> None:
|
|
185
|
+
"""Reload the current agent so new auth tokens are picked up immediately."""
|
|
186
|
+
try:
|
|
187
|
+
from code_puppy.agents import get_current_agent
|
|
188
|
+
|
|
189
|
+
current_agent = get_current_agent()
|
|
190
|
+
if current_agent is None:
|
|
191
|
+
logger.debug("No current agent to reload")
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
# JSON agents may need to refresh their config before reload
|
|
195
|
+
if hasattr(current_agent, "refresh_config"):
|
|
196
|
+
try:
|
|
197
|
+
current_agent.refresh_config()
|
|
198
|
+
except Exception:
|
|
199
|
+
# Non-fatal, continue to reload
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
current_agent.reload_code_generation_agent()
|
|
203
|
+
emit_info("Active agent reloaded with new authentication")
|
|
204
|
+
except Exception as e:
|
|
205
|
+
emit_warning(f"Authentication succeeded but agent reload failed: {e}")
|
|
206
|
+
logger.exception("Failed to reload agent after authentication")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _perform_authentication() -> None:
|
|
210
|
+
context = prepare_oauth_context()
|
|
211
|
+
code = _await_callback(context)
|
|
212
|
+
if not code:
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
emit_info("Exchanging authorization code for tokens…")
|
|
216
|
+
tokens = exchange_code_for_tokens(code, context)
|
|
217
|
+
if not tokens:
|
|
218
|
+
emit_error("Token exchange failed. Please retry the authentication flow.")
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
if not save_tokens(tokens):
|
|
222
|
+
emit_error(
|
|
223
|
+
"Tokens retrieved but failed to save locally. Check file permissions."
|
|
224
|
+
)
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
emit_success("Claude Code OAuth authentication successful!")
|
|
228
|
+
|
|
229
|
+
access_token = tokens.get("access_token")
|
|
230
|
+
if not access_token:
|
|
231
|
+
emit_warning("No access token returned; skipping model discovery.")
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
emit_info("Fetching available Claude Code models…")
|
|
235
|
+
models = fetch_claude_code_models(access_token)
|
|
236
|
+
if not models:
|
|
237
|
+
emit_warning(
|
|
238
|
+
"Claude Code authentication succeeded but no models were returned."
|
|
239
|
+
)
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
emit_info(f"Discovered {len(models)} models: {', '.join(models)}")
|
|
243
|
+
if add_models_to_extra_config(models):
|
|
244
|
+
emit_success(
|
|
245
|
+
"Claude Code models added to your configuration. Use the `claude-code-` prefix!"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Reload the current agent so the new auth token is picked up immediately
|
|
249
|
+
_reload_current_agent()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _handle_custom_command(command: str, name: str) -> Optional[bool]:
|
|
253
|
+
if not name:
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
if name == "claude-code-auth":
|
|
257
|
+
emit_info("Starting Claude Code OAuth authentication…")
|
|
258
|
+
tokens = load_stored_tokens()
|
|
259
|
+
if tokens and tokens.get("access_token"):
|
|
260
|
+
emit_warning(
|
|
261
|
+
"Existing Claude Code tokens found. Continuing will overwrite them."
|
|
262
|
+
)
|
|
263
|
+
_perform_authentication()
|
|
264
|
+
set_model_name("claude-code-claude-opus-4-5-20251101")
|
|
265
|
+
return True
|
|
266
|
+
|
|
267
|
+
if name == "claude-code-status":
|
|
268
|
+
tokens = load_stored_tokens()
|
|
269
|
+
if tokens and tokens.get("access_token"):
|
|
270
|
+
emit_success("Claude Code OAuth: Authenticated")
|
|
271
|
+
expires_at = tokens.get("expires_at")
|
|
272
|
+
if expires_at:
|
|
273
|
+
remaining = max(0, int(expires_at - time.time()))
|
|
274
|
+
hours, minutes = divmod(remaining // 60, 60)
|
|
275
|
+
emit_info(f"Token expires in ~{hours}h {minutes}m")
|
|
276
|
+
|
|
277
|
+
claude_models = [
|
|
278
|
+
name
|
|
279
|
+
for name, cfg in load_claude_models_filtered().items()
|
|
280
|
+
if cfg.get("oauth_source") == "claude-code-plugin"
|
|
281
|
+
]
|
|
282
|
+
if claude_models:
|
|
283
|
+
emit_info(f"Configured Claude Code models: {', '.join(claude_models)}")
|
|
284
|
+
else:
|
|
285
|
+
emit_warning("No Claude Code models configured yet.")
|
|
286
|
+
else:
|
|
287
|
+
emit_warning("Claude Code OAuth: Not authenticated")
|
|
288
|
+
emit_info("Run /claude-code-auth to begin the browser sign-in flow.")
|
|
289
|
+
return True
|
|
290
|
+
|
|
291
|
+
if name == "claude-code-logout":
|
|
292
|
+
token_path = get_token_storage_path()
|
|
293
|
+
if token_path.exists():
|
|
294
|
+
token_path.unlink()
|
|
295
|
+
emit_info("Removed Claude Code OAuth tokens")
|
|
296
|
+
|
|
297
|
+
removed = remove_claude_code_models()
|
|
298
|
+
if removed:
|
|
299
|
+
emit_info(f"Removed {removed} Claude Code models from configuration")
|
|
300
|
+
|
|
301
|
+
emit_success("Claude Code logout complete")
|
|
302
|
+
return True
|
|
303
|
+
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
register_callback("custom_command_help", _custom_help)
|
|
308
|
+
register_callback("custom_command", _handle_custom_command)
|