codepp 0.0.437__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_puppy/__init__.py +10 -0
- code_puppy/__main__.py +10 -0
- code_puppy/agents/__init__.py +31 -0
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +117 -0
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +638 -0
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_helios.py +124 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +742 -0
- code_puppy/agents/agent_pack_leader.py +385 -0
- code_puppy/agents/agent_planning.py +165 -0
- code_puppy/agents/agent_python_programmer.py +169 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_scheduler.py +121 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +2156 -0
- code_puppy/agents/event_stream_handler.py +348 -0
- code_puppy/agents/json_agent.py +202 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +327 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +453 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +75 -0
- code_puppy/api/routers/sessions.py +234 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +692 -0
- code_puppy/chatgpt_codex_client.py +338 -0
- code_puppy/claude_cache_client.py +672 -0
- code_puppy/cli_runner.py +1073 -0
- code_puppy/command_line/__init__.py +1 -0
- code_puppy/command_line/add_model_menu.py +1092 -0
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +704 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +532 -0
- code_puppy/command_line/command_handler.py +293 -0
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +719 -0
- code_puppy/command_line/core_commands.py +867 -0
- code_puppy/command_line/diff_menu.py +865 -0
- code_puppy/command_line/file_path_completion.py +73 -0
- code_puppy/command_line/load_context_completion.py +52 -0
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/base.py +32 -0
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +138 -0
- code_puppy/command_line/mcp/help_command.py +147 -0
- code_puppy/command_line/mcp/install_command.py +214 -0
- code_puppy/command_line/mcp/install_menu.py +705 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +235 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +100 -0
- code_puppy/command_line/mcp/search_command.py +123 -0
- code_puppy/command_line/mcp/start_all_command.py +135 -0
- code_puppy/command_line/mcp/start_command.py +117 -0
- code_puppy/command_line/mcp/status_command.py +184 -0
- code_puppy/command_line/mcp/stop_all_command.py +112 -0
- code_puppy/command_line/mcp/stop_command.py +80 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +334 -0
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +197 -0
- code_puppy/command_line/model_settings_menu.py +932 -0
- code_puppy/command_line/motd.py +96 -0
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +342 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +846 -0
- code_puppy/command_line/session_commands.py +302 -0
- code_puppy/command_line/shell_passthrough.py +145 -0
- code_puppy/command_line/skills_completion.py +160 -0
- code_puppy/command_line/uc_menu.py +893 -0
- code_puppy/command_line/utils.py +93 -0
- code_puppy/command_line/wiggum_state.py +78 -0
- code_puppy/config.py +1770 -0
- code_puppy/error_logging.py +134 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +754 -0
- code_puppy/hook_engine/README.md +105 -0
- code_puppy/hook_engine/__init__.py +21 -0
- code_puppy/hook_engine/aliases.py +155 -0
- code_puppy/hook_engine/engine.py +221 -0
- code_puppy/hook_engine/executor.py +296 -0
- code_puppy/hook_engine/matcher.py +156 -0
- code_puppy/hook_engine/models.py +240 -0
- code_puppy/hook_engine/registry.py +106 -0
- code_puppy/hook_engine/validator.py +144 -0
- code_puppy/http_utils.py +361 -0
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +10 -0
- code_puppy/mcp_/__init__.py +66 -0
- code_puppy/mcp_/async_lifecycle.py +286 -0
- code_puppy/mcp_/blocking_startup.py +469 -0
- code_puppy/mcp_/captured_stdio_server.py +275 -0
- code_puppy/mcp_/circuit_breaker.py +290 -0
- code_puppy/mcp_/config_wizard.py +507 -0
- code_puppy/mcp_/dashboard.py +308 -0
- code_puppy/mcp_/error_isolation.py +407 -0
- code_puppy/mcp_/examples/retry_example.py +226 -0
- code_puppy/mcp_/health_monitor.py +589 -0
- code_puppy/mcp_/managed_server.py +428 -0
- code_puppy/mcp_/manager.py +807 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +451 -0
- code_puppy/mcp_/retry_manager.py +337 -0
- code_puppy/mcp_/server_registry_catalog.py +1126 -0
- code_puppy/mcp_/status_tracker.py +355 -0
- code_puppy/mcp_/system_tools.py +209 -0
- code_puppy/mcp_prompts/__init__.py +1 -0
- code_puppy/mcp_prompts/hook_creator.py +103 -0
- code_puppy/messaging/__init__.py +255 -0
- code_puppy/messaging/bus.py +613 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +361 -0
- code_puppy/messaging/messages.py +569 -0
- code_puppy/messaging/queue_console.py +271 -0
- code_puppy/messaging/renderers.py +311 -0
- code_puppy/messaging/rich_renderer.py +1158 -0
- code_puppy/messaging/spinner/__init__.py +83 -0
- code_puppy/messaging/spinner/console_spinner.py +240 -0
- code_puppy/messaging/spinner/spinner_base.py +95 -0
- code_puppy/messaging/subagent_console.py +460 -0
- code_puppy/model_factory.py +848 -0
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +168 -0
- code_puppy/models.json +174 -0
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +186 -0
- code_puppy/plugins/agent_skills/__init__.py +22 -0
- code_puppy/plugins/agent_skills/config.py +175 -0
- code_puppy/plugins/agent_skills/discovery.py +136 -0
- code_puppy/plugins/agent_skills/downloader.py +392 -0
- code_puppy/plugins/agent_skills/installer.py +22 -0
- code_puppy/plugins/agent_skills/metadata.py +219 -0
- code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
- code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
- code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
- code_puppy/plugins/agent_skills/skills_menu.py +781 -0
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +133 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
- code_puppy/plugins/antigravity_oauth/storage.py +288 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +863 -0
- code_puppy/plugins/antigravity_oauth/utils.py +168 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
- code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy/plugins/claude_code_hooks/config.py +137 -0
- code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
- code_puppy/plugins/claude_code_oauth/config.py +52 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
- code_puppy/plugins/claude_code_oauth/utils.py +640 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/hook_creator/__init__.py +1 -0
- code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy/plugins/hook_manager/__init__.py +1 -0
- code_puppy/plugins/hook_manager/config.py +290 -0
- code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
- code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/scheduler/__init__.py +1 -0
- code_puppy/plugins/scheduler/register_callbacks.py +88 -0
- code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
- code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/plugins/synthetic_status/__init__.py +1 -0
- code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
- code_puppy/plugins/synthetic_status/status_api.py +147 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +302 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +356 -0
- code_puppy/reopenable_async_client.py +232 -0
- code_puppy/round_robin_model.py +150 -0
- code_puppy/scheduler/__init__.py +41 -0
- code_puppy/scheduler/__main__.py +9 -0
- code_puppy/scheduler/cli.py +118 -0
- code_puppy/scheduler/config.py +126 -0
- code_puppy/scheduler/daemon.py +280 -0
- code_puppy/scheduler/executor.py +155 -0
- code_puppy/scheduler/platform.py +19 -0
- code_puppy/scheduler/platform_unix.py +22 -0
- code_puppy/scheduler/platform_win.py +32 -0
- code_puppy/session_storage.py +338 -0
- code_puppy/status_display.py +257 -0
- code_puppy/summarization_agent.py +176 -0
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +501 -0
- code_puppy/tools/agent_tools.py +603 -0
- code_puppy/tools/ask_user_question/__init__.py +26 -0
- code_puppy/tools/ask_user_question/constants.py +73 -0
- code_puppy/tools/ask_user_question/demo_tui.py +55 -0
- code_puppy/tools/ask_user_question/handler.py +232 -0
- code_puppy/tools/ask_user_question/models.py +304 -0
- code_puppy/tools/ask_user_question/registration.py +26 -0
- code_puppy/tools/ask_user_question/renderers.py +309 -0
- code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
- code_puppy/tools/ask_user_question/theme.py +155 -0
- code_puppy/tools/ask_user_question/tui_loop.py +423 -0
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +378 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +534 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +1346 -0
- code_puppy/tools/common.py +1409 -0
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +886 -0
- code_puppy/tools/file_operations.py +802 -0
- code_puppy/tools/scheduler_tools.py +412 -0
- code_puppy/tools/skills_tools.py +244 -0
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/tools/tools_content.py +51 -0
- code_puppy/tools/universal_constructor.py +889 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +82 -0
- codepp-0.0.437.dist-info/METADATA +766 -0
- codepp-0.0.437.dist-info/RECORD +288 -0
- codepp-0.0.437.dist-info/WHEEL +4 -0
- codepp-0.0.437.dist-info/entry_points.txt +3 -0
- codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Manual sanity checks for the Claude Code OAuth plugin."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
# Ensure project root on path
|
|
9
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent
|
|
10
|
+
sys.path.insert(0, str(PROJECT_ROOT))
|
|
11
|
+
|
|
12
|
+
# Switch to project root for predictable relative paths
|
|
13
|
+
os.chdir(PROJECT_ROOT)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_plugin_imports() -> bool:
|
|
17
|
+
"""Verify the plugin modules import correctly."""
|
|
18
|
+
print("\n=== Testing Plugin Imports ===")
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from code_puppy.plugins.claude_code_oauth.config import (
|
|
22
|
+
CLAUDE_CODE_OAUTH_CONFIG,
|
|
23
|
+
get_token_storage_path,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
print("✅ Config import successful")
|
|
27
|
+
print(f"✅ Token storage path: {get_token_storage_path()}")
|
|
28
|
+
print(f"✅ Known auth URL: {CLAUDE_CODE_OAUTH_CONFIG['auth_url']}")
|
|
29
|
+
except Exception as exc: # pragma: no cover - manual harness
|
|
30
|
+
print(f"❌ Config import failed: {exc}")
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
from code_puppy.plugins.claude_code_oauth.utils import (
|
|
35
|
+
add_models_to_extra_config,
|
|
36
|
+
build_authorization_url,
|
|
37
|
+
exchange_code_for_tokens,
|
|
38
|
+
fetch_claude_code_models,
|
|
39
|
+
load_claude_models,
|
|
40
|
+
load_stored_tokens,
|
|
41
|
+
parse_authorization_code,
|
|
42
|
+
prepare_oauth_context,
|
|
43
|
+
remove_claude_code_models,
|
|
44
|
+
save_claude_models,
|
|
45
|
+
save_tokens,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
_ = (
|
|
49
|
+
add_models_to_extra_config,
|
|
50
|
+
build_authorization_url,
|
|
51
|
+
exchange_code_for_tokens,
|
|
52
|
+
fetch_claude_code_models,
|
|
53
|
+
load_claude_models,
|
|
54
|
+
load_stored_tokens,
|
|
55
|
+
parse_authorization_code,
|
|
56
|
+
prepare_oauth_context,
|
|
57
|
+
remove_claude_code_models,
|
|
58
|
+
save_claude_models,
|
|
59
|
+
save_tokens,
|
|
60
|
+
)
|
|
61
|
+
print("✅ Utils import successful")
|
|
62
|
+
except Exception as exc: # pragma: no cover - manual harness
|
|
63
|
+
print(f"❌ Utils import failed: {exc}")
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
from code_puppy.plugins.claude_code_oauth.register_callbacks import (
|
|
68
|
+
_custom_help,
|
|
69
|
+
_handle_custom_command,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
commands = _custom_help()
|
|
73
|
+
print("✅ Callback registration import successful")
|
|
74
|
+
for name, description in commands:
|
|
75
|
+
print(f" /{name} - {description}")
|
|
76
|
+
# Ensure handler callable exists
|
|
77
|
+
_ = _handle_custom_command
|
|
78
|
+
except Exception as exc: # pragma: no cover - manual harness
|
|
79
|
+
print(f"❌ Callback import failed: {exc}")
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_oauth_helpers() -> bool:
|
|
86
|
+
"""Exercise helper functions without performing network requests."""
|
|
87
|
+
print("\n=== Testing OAuth Helper Functions ===")
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
from urllib.parse import parse_qs, urlparse
|
|
91
|
+
|
|
92
|
+
from code_puppy.plugins.claude_code_oauth.utils import (
|
|
93
|
+
assign_redirect_uri,
|
|
94
|
+
build_authorization_url,
|
|
95
|
+
parse_authorization_code,
|
|
96
|
+
prepare_oauth_context,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
context = prepare_oauth_context()
|
|
100
|
+
assert context.state, "Expected non-empty OAuth state"
|
|
101
|
+
assert context.code_verifier, "Expected PKCE code verifier"
|
|
102
|
+
assert context.code_challenge, "Expected PKCE code challenge"
|
|
103
|
+
|
|
104
|
+
assign_redirect_uri(context, 8765)
|
|
105
|
+
auth_url = build_authorization_url(context)
|
|
106
|
+
parsed = urlparse(auth_url)
|
|
107
|
+
params = parse_qs(parsed.query)
|
|
108
|
+
print(f"✅ Authorization URL: {auth_url}")
|
|
109
|
+
assert parsed.scheme == "https", "Authorization URL must use https"
|
|
110
|
+
assert params.get("client_id", [None])[0], "client_id missing"
|
|
111
|
+
assert params.get("code_challenge_method", [None])[0] == "S256"
|
|
112
|
+
assert params.get("state", [None])[0] == context.state
|
|
113
|
+
assert params.get("code_challenge", [None])[0] == context.code_challenge
|
|
114
|
+
|
|
115
|
+
sample_code = f"MYCODE#{context.state}"
|
|
116
|
+
parsed_code, parsed_state = parse_authorization_code(sample_code)
|
|
117
|
+
assert parsed_code == "MYCODE", "Code parsing failed"
|
|
118
|
+
assert parsed_state == context.state, "State parsing failed"
|
|
119
|
+
print("✅ parse_authorization_code handled state suffix correctly")
|
|
120
|
+
|
|
121
|
+
parsed_code, parsed_state = parse_authorization_code("SINGLECODE")
|
|
122
|
+
assert parsed_code == "SINGLECODE" and parsed_state is None
|
|
123
|
+
print("✅ parse_authorization_code handled bare code correctly")
|
|
124
|
+
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
except AssertionError as exc:
|
|
128
|
+
print(f"❌ Assertion failed: {exc}")
|
|
129
|
+
return False
|
|
130
|
+
except Exception as exc: # pragma: no cover - manual harness
|
|
131
|
+
print(f"❌ OAuth helper test crashed: {exc}")
|
|
132
|
+
import traceback
|
|
133
|
+
|
|
134
|
+
traceback.print_exc()
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_file_operations() -> bool:
|
|
139
|
+
"""Ensure token/model storage helpers behave sanely."""
|
|
140
|
+
print("\n=== Testing File Operations ===")
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
from code_puppy.plugins.claude_code_oauth.config import (
|
|
144
|
+
get_claude_models_path,
|
|
145
|
+
get_token_storage_path,
|
|
146
|
+
)
|
|
147
|
+
from code_puppy.plugins.claude_code_oauth.utils import (
|
|
148
|
+
load_claude_models,
|
|
149
|
+
load_stored_tokens,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
tokens = load_stored_tokens()
|
|
153
|
+
print(f"✅ Token load result: {'present' if tokens else 'none'}")
|
|
154
|
+
|
|
155
|
+
models = load_claude_models()
|
|
156
|
+
print(f"✅ Loaded {len(models)} Claude models")
|
|
157
|
+
for name, config in models.items():
|
|
158
|
+
print(f" - {name}: {config.get('type', 'unknown type')}")
|
|
159
|
+
|
|
160
|
+
token_path = get_token_storage_path()
|
|
161
|
+
models_path = get_claude_models_path()
|
|
162
|
+
token_path.parent.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
models_path.parent.mkdir(parents=True, exist_ok=True)
|
|
164
|
+
print(f"✅ Token path: {token_path}")
|
|
165
|
+
print(f"✅ Models path: {models_path}")
|
|
166
|
+
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
except Exception as exc: # pragma: no cover - manual harness
|
|
170
|
+
print(f"❌ File operations test failed: {exc}")
|
|
171
|
+
import traceback
|
|
172
|
+
|
|
173
|
+
traceback.print_exc()
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_command_handlers() -> bool:
|
|
178
|
+
"""Smoke-test command handler routing without simulating authentication."""
|
|
179
|
+
print("\n=== Testing Command Handlers ===")
|
|
180
|
+
|
|
181
|
+
from code_puppy.plugins.claude_code_oauth.register_callbacks import (
|
|
182
|
+
_handle_custom_command,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
unknown = _handle_custom_command("/bogus", "bogus")
|
|
186
|
+
print(f"✅ Unknown command returned: {unknown}")
|
|
187
|
+
|
|
188
|
+
partial = _handle_custom_command("/claude-code", "claude-code")
|
|
189
|
+
print(f"✅ Partial command returned: {partial}")
|
|
190
|
+
|
|
191
|
+
# Do not invoke the real auth command here because it prompts for input.
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_configuration() -> bool:
|
|
196
|
+
"""Validate configuration keys and basic formats."""
|
|
197
|
+
print("\n=== Testing Configuration ===")
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
from code_puppy.plugins.claude_code_oauth.config import CLAUDE_CODE_OAUTH_CONFIG
|
|
201
|
+
|
|
202
|
+
required_keys = [
|
|
203
|
+
"auth_url",
|
|
204
|
+
"token_url",
|
|
205
|
+
"api_base_url",
|
|
206
|
+
"client_id",
|
|
207
|
+
"scope",
|
|
208
|
+
"redirect_host",
|
|
209
|
+
"redirect_path",
|
|
210
|
+
"callback_port_range",
|
|
211
|
+
"callback_timeout",
|
|
212
|
+
"token_storage",
|
|
213
|
+
"prefix",
|
|
214
|
+
"default_context_length",
|
|
215
|
+
"api_key_env_var",
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
missing = [key for key in required_keys if key not in CLAUDE_CODE_OAUTH_CONFIG]
|
|
219
|
+
if missing:
|
|
220
|
+
print(f"❌ Missing configuration keys: {missing}")
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
for key in required_keys:
|
|
224
|
+
value = CLAUDE_CODE_OAUTH_CONFIG[key]
|
|
225
|
+
print(f"✅ {key}: {value}")
|
|
226
|
+
|
|
227
|
+
for url_key in ["auth_url", "token_url", "api_base_url"]:
|
|
228
|
+
url = CLAUDE_CODE_OAUTH_CONFIG[url_key]
|
|
229
|
+
if not str(url).startswith("https://"):
|
|
230
|
+
print(f"❌ URL must use HTTPS: {url_key} -> {url}")
|
|
231
|
+
return False
|
|
232
|
+
print(f"✅ {url_key} uses HTTPS")
|
|
233
|
+
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
except Exception as exc: # pragma: no cover - manual harness
|
|
237
|
+
print(f"❌ Configuration test crashed: {exc}")
|
|
238
|
+
import traceback
|
|
239
|
+
|
|
240
|
+
traceback.print_exc()
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def main() -> bool:
|
|
245
|
+
"""Run all manual checks."""
|
|
246
|
+
print("Claude Code OAuth Plugin Test Suite")
|
|
247
|
+
print("=" * 40)
|
|
248
|
+
|
|
249
|
+
tests = [
|
|
250
|
+
test_plugin_imports,
|
|
251
|
+
test_oauth_helpers,
|
|
252
|
+
test_file_operations,
|
|
253
|
+
test_command_handlers,
|
|
254
|
+
test_configuration,
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
passed = 0
|
|
258
|
+
for test in tests:
|
|
259
|
+
try:
|
|
260
|
+
if test():
|
|
261
|
+
passed += 1
|
|
262
|
+
else:
|
|
263
|
+
print("\n❌ Test failed")
|
|
264
|
+
except Exception as exc: # pragma: no cover - manual harness
|
|
265
|
+
print(f"\n❌ Test crashed: {exc}")
|
|
266
|
+
|
|
267
|
+
print("\n=== Test Results ===")
|
|
268
|
+
print(f"Passed: {passed}/{len(tests)}")
|
|
269
|
+
|
|
270
|
+
if passed == len(tests):
|
|
271
|
+
print("✅ All sanity checks passed!")
|
|
272
|
+
print("Next steps:")
|
|
273
|
+
print("1. Restart Code Puppy if it was running")
|
|
274
|
+
print("2. Run /claude-code-auth")
|
|
275
|
+
print("3. Paste the Claude Console authorization code when prompted")
|
|
276
|
+
return True
|
|
277
|
+
|
|
278
|
+
print("❌ Some checks failed. Investigate before using the plugin.")
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
if __name__ == "__main__":
|
|
283
|
+
sys.exit(0 if main() else 1)
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Token refresh heartbeat for long-running Claude Code OAuth sessions.
|
|
2
|
+
|
|
3
|
+
This module provides a background task that periodically checks and refreshes
|
|
4
|
+
Claude Code OAuth tokens during long-running agentic operations. This ensures
|
|
5
|
+
that tokens don't expire during extended streaming responses or tool processing.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
async with token_refresh_heartbeat_context():
|
|
9
|
+
# Long running agent operation
|
|
10
|
+
await agent.run(...)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import logging
|
|
17
|
+
import time
|
|
18
|
+
from contextlib import asynccontextmanager
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Heartbeat interval in seconds - check token every 2 minutes
|
|
24
|
+
# This is frequent enough to catch expiring tokens before they cause issues
|
|
25
|
+
# but not so frequent as to spam the token endpoint
|
|
26
|
+
HEARTBEAT_INTERVAL_SECONDS = 120
|
|
27
|
+
|
|
28
|
+
# Minimum time between refresh attempts to avoid hammering the endpoint
|
|
29
|
+
MIN_REFRESH_INTERVAL_SECONDS = 60
|
|
30
|
+
|
|
31
|
+
# Global tracking of last refresh time to coordinate across heartbeats
|
|
32
|
+
_last_refresh_time: float = 0.0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TokenRefreshHeartbeat:
|
|
36
|
+
"""Background task that periodically refreshes Claude Code OAuth tokens.
|
|
37
|
+
|
|
38
|
+
This runs as an asyncio task during agent operations and checks if the
|
|
39
|
+
token needs refreshing at regular intervals.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
interval: float = HEARTBEAT_INTERVAL_SECONDS,
|
|
45
|
+
min_refresh_interval: float = MIN_REFRESH_INTERVAL_SECONDS,
|
|
46
|
+
):
|
|
47
|
+
self._interval = interval
|
|
48
|
+
self._min_refresh_interval = min_refresh_interval
|
|
49
|
+
self._task: Optional[asyncio.Task] = None
|
|
50
|
+
self._stop_event = asyncio.Event()
|
|
51
|
+
self._lock = asyncio.Lock()
|
|
52
|
+
self._refresh_count = 0
|
|
53
|
+
|
|
54
|
+
async def start(self) -> None:
|
|
55
|
+
"""Start the heartbeat background task."""
|
|
56
|
+
if self._task is not None:
|
|
57
|
+
logger.debug("Heartbeat already running")
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
self._stop_event.clear()
|
|
61
|
+
self._task = asyncio.create_task(self._heartbeat_loop())
|
|
62
|
+
logger.debug("Token refresh heartbeat started")
|
|
63
|
+
|
|
64
|
+
async def stop(self) -> None:
|
|
65
|
+
"""Stop the heartbeat background task."""
|
|
66
|
+
if self._task is None:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
self._stop_event.set()
|
|
70
|
+
self._task.cancel()
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
await self._task
|
|
74
|
+
except asyncio.CancelledError:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
self._task = None
|
|
78
|
+
logger.debug(
|
|
79
|
+
"Token refresh heartbeat stopped (refreshed %d times)",
|
|
80
|
+
self._refresh_count,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
async def _heartbeat_loop(self) -> None:
|
|
84
|
+
"""Main heartbeat loop that periodically checks token status."""
|
|
85
|
+
global _last_refresh_time
|
|
86
|
+
|
|
87
|
+
while not self._stop_event.is_set():
|
|
88
|
+
try:
|
|
89
|
+
# Wait for the interval or until stopped
|
|
90
|
+
try:
|
|
91
|
+
await asyncio.wait_for(
|
|
92
|
+
self._stop_event.wait(), timeout=self._interval
|
|
93
|
+
)
|
|
94
|
+
# If we got here, stop event was set
|
|
95
|
+
break
|
|
96
|
+
except asyncio.TimeoutError:
|
|
97
|
+
# Normal timeout - time to check token
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
# Check if we should attempt refresh
|
|
101
|
+
async with self._lock:
|
|
102
|
+
now = time.time()
|
|
103
|
+
if now - _last_refresh_time < self._min_refresh_interval:
|
|
104
|
+
logger.debug(
|
|
105
|
+
"Skipping refresh - last refresh was %.1f seconds ago",
|
|
106
|
+
now - _last_refresh_time,
|
|
107
|
+
)
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
# Attempt the refresh
|
|
111
|
+
refreshed = await self._attempt_refresh()
|
|
112
|
+
if refreshed:
|
|
113
|
+
_last_refresh_time = now
|
|
114
|
+
self._refresh_count += 1
|
|
115
|
+
|
|
116
|
+
except asyncio.CancelledError:
|
|
117
|
+
break
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
logger.debug("Error in heartbeat loop: %s", exc)
|
|
120
|
+
# Continue running - don't let errors kill the heartbeat
|
|
121
|
+
await asyncio.sleep(5) # Brief pause before retrying
|
|
122
|
+
|
|
123
|
+
async def _attempt_refresh(self) -> bool:
|
|
124
|
+
"""Attempt to refresh the token if needed.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if a refresh was performed, False otherwise.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
# Import here to avoid circular imports
|
|
131
|
+
from .utils import (
|
|
132
|
+
is_token_expired,
|
|
133
|
+
load_stored_tokens,
|
|
134
|
+
refresh_access_token,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
tokens = await asyncio.to_thread(load_stored_tokens)
|
|
138
|
+
if not tokens:
|
|
139
|
+
logger.debug("No stored tokens found")
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
if not is_token_expired(tokens):
|
|
143
|
+
logger.debug("Token not yet expired, skipping refresh")
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
# Token is expiring soon, refresh it
|
|
147
|
+
logger.info("Heartbeat: Token expiring soon, refreshing proactively")
|
|
148
|
+
refreshed_token = await asyncio.to_thread(refresh_access_token, force=False)
|
|
149
|
+
|
|
150
|
+
if refreshed_token:
|
|
151
|
+
logger.info("Heartbeat: Successfully refreshed token")
|
|
152
|
+
return True
|
|
153
|
+
else:
|
|
154
|
+
logger.warning("Heartbeat: Token refresh returned None")
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
except Exception as exc:
|
|
158
|
+
logger.error("Heartbeat: Error during token refresh: %s", exc)
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def refresh_count(self) -> int:
|
|
163
|
+
"""Get the number of successful refreshes performed by this heartbeat."""
|
|
164
|
+
return self._refresh_count
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def is_running(self) -> bool:
|
|
168
|
+
"""Check if the heartbeat is currently running."""
|
|
169
|
+
return self._task is not None and not self._task.done()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# Global heartbeat instance for the current session
|
|
173
|
+
_current_heartbeat: Optional[TokenRefreshHeartbeat] = None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@asynccontextmanager
|
|
177
|
+
async def token_refresh_heartbeat_context(
|
|
178
|
+
interval: float = HEARTBEAT_INTERVAL_SECONDS,
|
|
179
|
+
):
|
|
180
|
+
"""Context manager that runs token refresh heartbeat during its scope.
|
|
181
|
+
|
|
182
|
+
Use this around long-running agent operations to ensure tokens stay fresh.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
interval: Seconds between heartbeat checks. Default is 2 minutes.
|
|
186
|
+
|
|
187
|
+
Example:
|
|
188
|
+
async with token_refresh_heartbeat_context():
|
|
189
|
+
result = await agent.run(prompt)
|
|
190
|
+
"""
|
|
191
|
+
global _current_heartbeat
|
|
192
|
+
|
|
193
|
+
heartbeat = TokenRefreshHeartbeat(interval=interval)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
await heartbeat.start()
|
|
197
|
+
_current_heartbeat = heartbeat
|
|
198
|
+
yield heartbeat
|
|
199
|
+
finally:
|
|
200
|
+
await heartbeat.stop()
|
|
201
|
+
_current_heartbeat = None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def is_heartbeat_running() -> bool:
|
|
205
|
+
"""Check if a token refresh heartbeat is currently active."""
|
|
206
|
+
return _current_heartbeat is not None and _current_heartbeat.is_running
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def get_current_heartbeat() -> Optional[TokenRefreshHeartbeat]:
|
|
210
|
+
"""Get the currently running heartbeat instance, if any."""
|
|
211
|
+
return _current_heartbeat
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
async def force_token_refresh() -> bool:
|
|
215
|
+
"""Force an immediate token refresh.
|
|
216
|
+
|
|
217
|
+
This can be called from anywhere to trigger a token refresh,
|
|
218
|
+
regardless of whether a heartbeat is running.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
True if refresh was successful, False otherwise.
|
|
222
|
+
"""
|
|
223
|
+
global _last_refresh_time
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
from .utils import refresh_access_token
|
|
227
|
+
|
|
228
|
+
logger.info("Forcing token refresh")
|
|
229
|
+
refreshed_token = refresh_access_token(force=True)
|
|
230
|
+
|
|
231
|
+
if refreshed_token:
|
|
232
|
+
_last_refresh_time = time.time()
|
|
233
|
+
logger.info("Force refresh successful")
|
|
234
|
+
return True
|
|
235
|
+
else:
|
|
236
|
+
logger.warning("Force refresh returned None")
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
logger.error("Force refresh error: %s", exc)
|
|
241
|
+
return False
|