code-puppy-core-plugins 0.0.2__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_core_plugins/__init__.py +5 -0
- code_puppy_core_plugins/acp/README.md +266 -0
- code_puppy_core_plugins/acp/__init__.py +8 -0
- code_puppy_core_plugins/acp/agent.py +494 -0
- code_puppy_core_plugins/acp/bridge.py +309 -0
- code_puppy_core_plugins/acp/capabilities.py +73 -0
- code_puppy_core_plugins/acp/commands.py +116 -0
- code_puppy_core_plugins/acp/content.py +115 -0
- code_puppy_core_plugins/acp/io_delegation.py +231 -0
- code_puppy_core_plugins/acp/mcp_config.py +66 -0
- code_puppy_core_plugins/acp/permissions.py +159 -0
- code_puppy_core_plugins/acp/persistence.py +197 -0
- code_puppy_core_plugins/acp/register_callbacks.py +110 -0
- code_puppy_core_plugins/acp/replay.py +114 -0
- code_puppy_core_plugins/acp/session.py +329 -0
- code_puppy_core_plugins/acp/session_config.py +179 -0
- code_puppy_core_plugins/acp/state.py +135 -0
- code_puppy_core_plugins/agent_skills/__init__.py +22 -0
- code_puppy_core_plugins/agent_skills/config.py +208 -0
- code_puppy_core_plugins/agent_skills/discovery.py +337 -0
- code_puppy_core_plugins/agent_skills/downloader.py +392 -0
- code_puppy_core_plugins/agent_skills/enabled_skills.py +82 -0
- code_puppy_core_plugins/agent_skills/installer.py +22 -0
- code_puppy_core_plugins/agent_skills/metadata.py +219 -0
- code_puppy_core_plugins/agent_skills/prompt_builder.py +49 -0
- code_puppy_core_plugins/agent_skills/provider.py +52 -0
- code_puppy_core_plugins/agent_skills/register_callbacks.py +309 -0
- code_puppy_core_plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy_core_plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy_core_plugins/agent_skills/skill_commands.py +102 -0
- code_puppy_core_plugins/agent_skills/skills_install_menu.py +698 -0
- code_puppy_core_plugins/agent_skills/skills_menu.py +794 -0
- code_puppy_core_plugins/aws_bedrock/__init__.py +14 -0
- code_puppy_core_plugins/aws_bedrock/config.py +101 -0
- code_puppy_core_plugins/aws_bedrock/register_callbacks.py +243 -0
- code_puppy_core_plugins/aws_bedrock/utils.py +186 -0
- code_puppy_core_plugins/azure_foundry/README.md +238 -0
- code_puppy_core_plugins/azure_foundry/__init__.py +15 -0
- code_puppy_core_plugins/azure_foundry/config.py +126 -0
- code_puppy_core_plugins/azure_foundry/discovery.py +189 -0
- code_puppy_core_plugins/azure_foundry/register_callbacks.py +506 -0
- code_puppy_core_plugins/azure_foundry/token.py +182 -0
- code_puppy_core_plugins/azure_foundry/utils.py +375 -0
- code_puppy_core_plugins/btw/__init__.py +1 -0
- code_puppy_core_plugins/btw/inline_view.py +135 -0
- code_puppy_core_plugins/btw/register_callbacks.py +106 -0
- code_puppy_core_plugins/btw/side_query.py +91 -0
- code_puppy_core_plugins/chatgpt_oauth/IMAGEGEN_SKILL.md +83 -0
- code_puppy_core_plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy_core_plugins/chatgpt_oauth/config.py +52 -0
- code_puppy_core_plugins/chatgpt_oauth/image_generation.py +253 -0
- code_puppy_core_plugins/chatgpt_oauth/image_tool.py +70 -0
- code_puppy_core_plugins/chatgpt_oauth/oauth_flow.py +461 -0
- code_puppy_core_plugins/chatgpt_oauth/register_callbacks.py +288 -0
- code_puppy_core_plugins/chatgpt_oauth/test_plugin.py +663 -0
- code_puppy_core_plugins/chatgpt_oauth/usage.py +113 -0
- code_puppy_core_plugins/chatgpt_oauth/utils.py +534 -0
- code_puppy_core_plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy_core_plugins/claude_code_hooks/config.py +137 -0
- code_puppy_core_plugins/claude_code_hooks/register_callbacks.py +383 -0
- code_puppy_core_plugins/claude_code_oauth/README.md +167 -0
- code_puppy_core_plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy_core_plugins/claude_code_oauth/__init__.py +25 -0
- code_puppy_core_plugins/claude_code_oauth/config.py +59 -0
- code_puppy_core_plugins/claude_code_oauth/fast_mode.py +122 -0
- code_puppy_core_plugins/claude_code_oauth/prompt_handler.py +64 -0
- code_puppy_core_plugins/claude_code_oauth/register_callbacks.py +719 -0
- code_puppy_core_plugins/claude_code_oauth/test_plugin.py +285 -0
- code_puppy_core_plugins/claude_code_oauth/token_refresh_heartbeat.py +240 -0
- code_puppy_core_plugins/claude_code_oauth/utils.py +663 -0
- code_puppy_core_plugins/code_puppy_agent/AGENTS_AND_TOOLS.md +155 -0
- code_puppy_core_plugins/code_puppy_agent/MODELS_AND_MCP.md +104 -0
- code_puppy_core_plugins/code_puppy_agent/PLUGINS_AND_CALLBACKS.md +147 -0
- code_puppy_core_plugins/code_puppy_agent/SESSIONS_AND_HISTORY.md +39 -0
- code_puppy_core_plugins/code_puppy_agent/SKILL.md +180 -0
- code_puppy_core_plugins/code_puppy_agent/SKILLS_SYSTEM.md +136 -0
- code_puppy_core_plugins/code_puppy_agent/SYSTEM_PROMPT_CONFIG_AND_I18N.md +160 -0
- code_puppy_core_plugins/code_puppy_agent/__init__.py +5 -0
- code_puppy_core_plugins/code_puppy_agent/register_callbacks.py +46 -0
- code_puppy_core_plugins/computer_use/NativeCapture.swift +123 -0
- code_puppy_core_plugins/computer_use/README.md +109 -0
- code_puppy_core_plugins/computer_use/__init__.py +1 -0
- code_puppy_core_plugins/computer_use/accessibility.py +163 -0
- code_puppy_core_plugins/computer_use/activation.py +75 -0
- code_puppy_core_plugins/computer_use/backend.py +584 -0
- code_puppy_core_plugins/computer_use/backend_types.py +5 -0
- code_puppy_core_plugins/computer_use/capture.py +110 -0
- code_puppy_core_plugins/computer_use/commands.py +62 -0
- code_puppy_core_plugins/computer_use/geometry.py +81 -0
- code_puppy_core_plugins/computer_use/inline_image.py +56 -0
- code_puppy_core_plugins/computer_use/keycodes.py +47 -0
- code_puppy_core_plugins/computer_use/policy.py +144 -0
- code_puppy_core_plugins/computer_use/register_callbacks.py +96 -0
- code_puppy_core_plugins/computer_use/safety.py +18 -0
- code_puppy_core_plugins/computer_use/settle.py +64 -0
- code_puppy_core_plugins/computer_use/state.py +86 -0
- code_puppy_core_plugins/computer_use/tools.py +469 -0
- code_puppy_core_plugins/context_indicator/__init__.py +14 -0
- code_puppy_core_plugins/context_indicator/register_callbacks.py +249 -0
- code_puppy_core_plugins/context_indicator/usage.py +40 -0
- code_puppy_core_plugins/copilot_auth/__init__.py +11 -0
- code_puppy_core_plugins/copilot_auth/config.py +93 -0
- code_puppy_core_plugins/copilot_auth/reasoning_client.py +409 -0
- code_puppy_core_plugins/copilot_auth/register_callbacks.py +460 -0
- code_puppy_core_plugins/copilot_auth/utils.py +587 -0
- code_puppy_core_plugins/customizable_commands/__init__.py +0 -0
- code_puppy_core_plugins/customizable_commands/register_callbacks.py +464 -0
- code_puppy_core_plugins/dbos_durable_exec/__init__.py +1 -0
- code_puppy_core_plugins/dbos_durable_exec/cancel.py +15 -0
- code_puppy_core_plugins/dbos_durable_exec/commands.py +31 -0
- code_puppy_core_plugins/dbos_durable_exec/config.py +31 -0
- code_puppy_core_plugins/dbos_durable_exec/lifecycle.py +83 -0
- code_puppy_core_plugins/dbos_durable_exec/register_callbacks.py +45 -0
- code_puppy_core_plugins/dbos_durable_exec/runtime.py +65 -0
- code_puppy_core_plugins/dbos_durable_exec/startup_lock.py +155 -0
- code_puppy_core_plugins/dbos_durable_exec/workflow_ids.py +16 -0
- code_puppy_core_plugins/dbos_durable_exec/wrapper.py +58 -0
- code_puppy_core_plugins/destructive_command_guard/AGENTS.md +79 -0
- code_puppy_core_plugins/destructive_command_guard/__init__.py +14 -0
- code_puppy_core_plugins/destructive_command_guard/detector.py +374 -0
- code_puppy_core_plugins/destructive_command_guard/register_callbacks.py +165 -0
- code_puppy_core_plugins/emoji_filter/__init__.py +12 -0
- code_puppy_core_plugins/emoji_filter/config.py +26 -0
- code_puppy_core_plugins/emoji_filter/register_callbacks.py +238 -0
- code_puppy_core_plugins/emoji_filter/stripper.py +45 -0
- code_puppy_core_plugins/empty_html_comment_filter/register_callbacks.py +116 -0
- code_puppy_core_plugins/example_custom_command/README.md +328 -0
- code_puppy_core_plugins/example_custom_command/register_callbacks.py +66 -0
- code_puppy_core_plugins/file_permission_handler/__init__.py +4 -0
- code_puppy_core_plugins/file_permission_handler/register_callbacks.py +575 -0
- code_puppy_core_plugins/flux_bootstrap/__init__.py +5 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/README.md +173 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/cheatsheet.md +86 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/pipeline.md +167 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/synopsis.md +392 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/about.md +51 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/address-feedback.md +129 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/ask.md +211 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/aug.md +181 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/auto-pilot.md +71 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/cheatsheet.md +16 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/commit.md +103 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/config.md +108 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/create-pr.md +195 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/exec.md +221 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/new.md +151 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/qa.md +208 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/rebase.md +180 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/review.md +304 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/split.md +120 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/squash-commits.md +179 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/status.md +46 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/tests.md +155 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_about.py +154 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_cheatsheet.py +248 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_status.py +345 -0
- code_puppy_core_plugins/flux_bootstrap/installer.py +256 -0
- code_puppy_core_plugins/flux_bootstrap/register_callbacks.py +71 -0
- code_puppy_core_plugins/force_push_guard/__init__.py +5 -0
- code_puppy_core_plugins/force_push_guard/detector.py +96 -0
- code_puppy_core_plugins/force_push_guard/register_callbacks.py +161 -0
- code_puppy_core_plugins/fork/__init__.py +7 -0
- code_puppy_core_plugins/fork/register_callbacks.py +445 -0
- code_puppy_core_plugins/frontend_emitter/__init__.py +25 -0
- code_puppy_core_plugins/frontend_emitter/emitter.py +249 -0
- code_puppy_core_plugins/frontend_emitter/register_callbacks.py +412 -0
- code_puppy_core_plugins/frontend_emitter/session_context.py +50 -0
- code_puppy_core_plugins/grok_oauth/__init__.py +6 -0
- code_puppy_core_plugins/grok_oauth/config.py +49 -0
- code_puppy_core_plugins/grok_oauth/oauth_flow.py +213 -0
- code_puppy_core_plugins/grok_oauth/register_callbacks.py +132 -0
- code_puppy_core_plugins/grok_oauth/utils.py +195 -0
- code_puppy_core_plugins/herdr/README.md +168 -0
- code_puppy_core_plugins/herdr/__init__.py +9 -0
- code_puppy_core_plugins/herdr/client.py +339 -0
- code_puppy_core_plugins/herdr/register_callbacks.py +140 -0
- code_puppy_core_plugins/herdr/reporter.py +212 -0
- code_puppy_core_plugins/herdr/smoke.py +143 -0
- code_puppy_core_plugins/herdr/sources.py +133 -0
- code_puppy_core_plugins/hook_creator/__init__.py +1 -0
- code_puppy_core_plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy_core_plugins/hook_manager/__init__.py +1 -0
- code_puppy_core_plugins/hook_manager/config.py +290 -0
- code_puppy_core_plugins/hook_manager/hooks_menu.py +571 -0
- code_puppy_core_plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy_core_plugins/meta_oauth/__init__.py +1 -0
- code_puppy_core_plugins/meta_oauth/config.py +55 -0
- code_puppy_core_plugins/meta_oauth/oauth_flow.py +109 -0
- code_puppy_core_plugins/meta_oauth/register_callbacks.py +154 -0
- code_puppy_core_plugins/meta_oauth/utils.py +252 -0
- code_puppy_core_plugins/namespace_skill_search/README.md +118 -0
- code_puppy_core_plugins/namespace_skill_search/__init__.py +15 -0
- code_puppy_core_plugins/namespace_skill_search/namespaces.py +125 -0
- code_puppy_core_plugins/namespace_skill_search/register_callbacks.py +99 -0
- code_puppy_core_plugins/namespace_skill_search/search_tool.py +158 -0
- code_puppy_core_plugins/no_tools/__init__.py +0 -0
- code_puppy_core_plugins/no_tools/register_callbacks.py +42 -0
- code_puppy_core_plugins/oauth_pasteback.py +110 -0
- code_puppy_core_plugins/oauth_puppy_html.py +194 -0
- code_puppy_core_plugins/obsidian_agent/README.md +52 -0
- code_puppy_core_plugins/obsidian_agent/__init__.py +8 -0
- code_puppy_core_plugins/obsidian_agent/agent_obsidian.py +147 -0
- code_puppy_core_plugins/obsidian_agent/register_callbacks.py +13 -0
- code_puppy_core_plugins/ollama/__init__.py +1 -0
- code_puppy_core_plugins/ollama/register_callbacks.py +127 -0
- code_puppy_core_plugins/ollama_setup/__init__.py +5 -0
- code_puppy_core_plugins/ollama_setup/completer.py +38 -0
- code_puppy_core_plugins/ollama_setup/register_callbacks.py +419 -0
- code_puppy_core_plugins/plugin_list/__init__.py +0 -0
- code_puppy_core_plugins/plugin_list/plugin_contributions.py +348 -0
- code_puppy_core_plugins/plugin_list/plugin_meta.py +67 -0
- code_puppy_core_plugins/plugin_list/plugin_text_utils.py +173 -0
- code_puppy_core_plugins/plugin_list/plugins_menu.py +455 -0
- code_puppy_core_plugins/plugin_list/plugins_menu_layout.py +194 -0
- code_puppy_core_plugins/plugin_list/plugins_menu_render.py +431 -0
- code_puppy_core_plugins/plugin_list/project_trust_flow.py +162 -0
- code_puppy_core_plugins/plugin_list/register_callbacks.py +281 -0
- code_puppy_core_plugins/pop_command/__init__.py +1 -0
- code_puppy_core_plugins/pop_command/register_callbacks.py +191 -0
- code_puppy_core_plugins/prompt_newline/__init__.py +13 -0
- code_puppy_core_plugins/prompt_newline/config.py +21 -0
- code_puppy_core_plugins/prompt_newline/register_callbacks.py +163 -0
- code_puppy_core_plugins/prune/__init__.py +1 -0
- code_puppy_core_plugins/prune/prune_menu.py +308 -0
- code_puppy_core_plugins/prune/prune_model.py +517 -0
- code_puppy_core_plugins/prune/prune_render.py +354 -0
- code_puppy_core_plugins/prune/register_callbacks.py +388 -0
- code_puppy_core_plugins/puppy_kennel/README.md +203 -0
- code_puppy_core_plugins/puppy_kennel/__init__.py +6 -0
- code_puppy_core_plugins/puppy_kennel/commands.py +206 -0
- code_puppy_core_plugins/puppy_kennel/config.py +36 -0
- code_puppy_core_plugins/puppy_kennel/kennel.py +368 -0
- code_puppy_core_plugins/puppy_kennel/packer.py +152 -0
- code_puppy_core_plugins/puppy_kennel/recorder.py +83 -0
- code_puppy_core_plugins/puppy_kennel/register_callbacks.py +109 -0
- code_puppy_core_plugins/puppy_kennel/retriever.py +28 -0
- code_puppy_core_plugins/puppy_kennel/schema.py +78 -0
- code_puppy_core_plugins/puppy_kennel/state.py +68 -0
- code_puppy_core_plugins/puppy_kennel/tools.py +449 -0
- code_puppy_core_plugins/puppy_kennel/wings.py +110 -0
- code_puppy_core_plugins/puppy_spinner/README.md +93 -0
- code_puppy_core_plugins/puppy_spinner/__init__.py +11 -0
- code_puppy_core_plugins/puppy_spinner/builtin_frames.py +172 -0
- code_puppy_core_plugins/puppy_spinner/commands.py +116 -0
- code_puppy_core_plugins/puppy_spinner/picker.py +379 -0
- code_puppy_core_plugins/puppy_spinner/register_callbacks.py +193 -0
- code_puppy_core_plugins/puppy_spinner/spinners.py +444 -0
- code_puppy_core_plugins/quick_resume/__init__.py +0 -0
- code_puppy_core_plugins/quick_resume/register_callbacks.py +77 -0
- code_puppy_core_plugins/review_pr/__init__.py +1 -0
- code_puppy_core_plugins/review_pr/register_callbacks.py +162 -0
- code_puppy_core_plugins/shell_safety/__init__.py +6 -0
- code_puppy_core_plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy_core_plugins/shell_safety/command_cache.py +156 -0
- code_puppy_core_plugins/shell_safety/register_callbacks.py +203 -0
- code_puppy_core_plugins/stack_dump/__init__.py +1 -0
- code_puppy_core_plugins/stack_dump/register_callbacks.py +79 -0
- code_puppy_core_plugins/statusline/__init__.py +14 -0
- code_puppy_core_plugins/statusline/config.py +68 -0
- code_puppy_core_plugins/statusline/payload.py +138 -0
- code_puppy_core_plugins/statusline/prompt_patch.py +76 -0
- code_puppy_core_plugins/statusline/register_callbacks.py +45 -0
- code_puppy_core_plugins/statusline/runner.py +122 -0
- code_puppy_core_plugins/statusline/statusline_command.py +197 -0
- code_puppy_core_plugins/steer_queue/__init__.py +10 -0
- code_puppy_core_plugins/steer_queue/queue_menu.py +444 -0
- code_puppy_core_plugins/steer_queue/register_callbacks.py +138 -0
- code_puppy_core_plugins/subagent_panel/README.md +102 -0
- code_puppy_core_plugins/subagent_panel/__init__.py +24 -0
- code_puppy_core_plugins/subagent_panel/coalesce_patch.py +242 -0
- code_puppy_core_plugins/subagent_panel/panel_render.py +145 -0
- code_puppy_core_plugins/subagent_panel/register_callbacks.py +535 -0
- code_puppy_core_plugins/subagent_panel/resume_repaint.py +112 -0
- code_puppy_core_plugins/subagent_panel/state.py +231 -0
- code_puppy_core_plugins/switch_agent_resume/__init__.py +5 -0
- code_puppy_core_plugins/switch_agent_resume/register_callbacks.py +314 -0
- code_puppy_core_plugins/theme/README.md +86 -0
- code_puppy_core_plugins/theme/__init__.py +14 -0
- code_puppy_core_plugins/theme/bundled_palettes.py +346 -0
- code_puppy_core_plugins/theme/content_styles.py +146 -0
- code_puppy_core_plugins/theme/osc_palette.py +178 -0
- code_puppy_core_plugins/theme/picker.py +358 -0
- code_puppy_core_plugins/theme/prompt_toolkit_theme.py +113 -0
- code_puppy_core_plugins/theme/register_callbacks.py +348 -0
- code_puppy_core_plugins/theme/rich_themes.py +202 -0
- code_puppy_core_plugins/theme/themes.py +720 -0
- code_puppy_core_plugins/timestamp_heartbeat/__init__.py +1 -0
- code_puppy_core_plugins/timestamp_heartbeat/register_callbacks.py +114 -0
- code_puppy_core_plugins/token_ratio_learner/__init__.py +5 -0
- code_puppy_core_plugins/token_ratio_learner/ratios.py +208 -0
- code_puppy_core_plugins/token_ratio_learner/register_callbacks.py +213 -0
- code_puppy_core_plugins/universal_constructor/__init__.py +13 -0
- code_puppy_core_plugins/universal_constructor/models.py +138 -0
- code_puppy_core_plugins/universal_constructor/provider.py +62 -0
- code_puppy_core_plugins/universal_constructor/register_callbacks.py +75 -0
- code_puppy_core_plugins/universal_constructor/registry.py +302 -0
- code_puppy_core_plugins/universal_constructor/sandbox.py +584 -0
- code_puppy_core_plugins/wide_completion_menu/__init__.py +6 -0
- code_puppy_core_plugins/wide_completion_menu/register_callbacks.py +165 -0
- code_puppy_core_plugins/wiggum/__init__.py +1 -0
- code_puppy_core_plugins/wiggum/goal_runs.py +91 -0
- code_puppy_core_plugins/wiggum/judge.py +299 -0
- code_puppy_core_plugins/wiggum/judge_config.py +249 -0
- code_puppy_core_plugins/wiggum/judges_menu.py +814 -0
- code_puppy_core_plugins/wiggum/register_callbacks.py +569 -0
- code_puppy_core_plugins/wiggum/state.py +89 -0
- code_puppy_core_plugins/yolo_cli/__init__.py +1 -0
- code_puppy_core_plugins/yolo_cli/register_callbacks.py +41 -0
- code_puppy_core_plugins-0.0.2.dist-info/METADATA +64 -0
- code_puppy_core_plugins-0.0.2.dist-info/RECORD +313 -0
- code_puppy_core_plugins-0.0.2.dist-info/WHEEL +4 -0
- code_puppy_core_plugins-0.0.2.dist-info/entry_points.txt +55 -0
- code_puppy_core_plugins-0.0.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Claude Code OAuth Plugin for Code Puppy.
|
|
3
|
+
|
|
4
|
+
Provides OAuth authentication for Claude Code models and registers
|
|
5
|
+
the 'claude_code' model type handler.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
15
|
+
from urllib.parse import parse_qs, urlparse
|
|
16
|
+
|
|
17
|
+
from code_puppy.callbacks import register_callback
|
|
18
|
+
from code_puppy.i18n import t
|
|
19
|
+
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
20
|
+
from code_puppy.model_switching import set_model_and_reload_agent
|
|
21
|
+
from code_puppy.provider_identity import (
|
|
22
|
+
make_anthropic_provider,
|
|
23
|
+
resolve_provider_identity,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from ..oauth_pasteback import parse_oauth_callback_input, read_available_stdin_line
|
|
27
|
+
from ..oauth_puppy_html import oauth_failure_html, oauth_success_html
|
|
28
|
+
from .config import CLAUDE_CODE_OAUTH_CONFIG, get_token_storage_path
|
|
29
|
+
from .fast_mode import (
|
|
30
|
+
FAST_SETTING_KEY,
|
|
31
|
+
ensure_fast_beta_header,
|
|
32
|
+
is_fast_mode_enabled,
|
|
33
|
+
patch_anthropic_client_fast_mode,
|
|
34
|
+
)
|
|
35
|
+
from .prompt_handler import is_claude_code_model, prepare_claude_code_prompt
|
|
36
|
+
from .utils import (
|
|
37
|
+
OAuthContext,
|
|
38
|
+
add_models_to_extra_config,
|
|
39
|
+
assign_redirect_uri,
|
|
40
|
+
build_authorization_url,
|
|
41
|
+
exchange_code_for_tokens,
|
|
42
|
+
fetch_claude_code_models,
|
|
43
|
+
get_valid_access_token,
|
|
44
|
+
is_token_expired,
|
|
45
|
+
load_claude_models_filtered,
|
|
46
|
+
load_stored_tokens,
|
|
47
|
+
prepare_oauth_context,
|
|
48
|
+
refresh_access_token,
|
|
49
|
+
remove_claude_code_models,
|
|
50
|
+
save_tokens,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
logger = logging.getLogger(__name__)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class _OAuthResult:
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
self.code: Optional[str] = None
|
|
59
|
+
self.state: Optional[str] = None
|
|
60
|
+
self.error: Optional[str] = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class _CallbackHandler(BaseHTTPRequestHandler):
|
|
64
|
+
result: _OAuthResult
|
|
65
|
+
received_event: threading.Event
|
|
66
|
+
|
|
67
|
+
def do_GET(self) -> None: # noqa: N802
|
|
68
|
+
logger.info("Callback received: path=%s", self.path)
|
|
69
|
+
parsed = urlparse(self.path)
|
|
70
|
+
params: Dict[str, List[str]] = parse_qs(parsed.query)
|
|
71
|
+
|
|
72
|
+
code = params.get("code", [None])[0]
|
|
73
|
+
state = params.get("state", [None])[0]
|
|
74
|
+
|
|
75
|
+
if code and state:
|
|
76
|
+
self.result.code = code
|
|
77
|
+
self.result.state = state
|
|
78
|
+
success_html = oauth_success_html(
|
|
79
|
+
"Claude Code",
|
|
80
|
+
"You're totally synced with Claude Code now!",
|
|
81
|
+
)
|
|
82
|
+
self._write_response(200, success_html)
|
|
83
|
+
else:
|
|
84
|
+
self.result.error = "Missing code or state"
|
|
85
|
+
failure_html = oauth_failure_html(
|
|
86
|
+
"Claude Code",
|
|
87
|
+
"Missing code or state parameter 🥺",
|
|
88
|
+
)
|
|
89
|
+
self._write_response(400, failure_html)
|
|
90
|
+
|
|
91
|
+
self.received_event.set()
|
|
92
|
+
|
|
93
|
+
def log_message(self, log_format: str, *args: Any) -> None:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
def _write_response(self, status: int, body: str) -> None:
|
|
97
|
+
self.send_response(status)
|
|
98
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
99
|
+
self.end_headers()
|
|
100
|
+
self.wfile.write(body.encode("utf-8"))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _start_callback_server(
|
|
104
|
+
context: OAuthContext,
|
|
105
|
+
) -> Optional[Tuple[HTTPServer, _OAuthResult, threading.Event]]:
|
|
106
|
+
port_range = CLAUDE_CODE_OAUTH_CONFIG["callback_port_range"]
|
|
107
|
+
|
|
108
|
+
for port in range(port_range[0], port_range[1] + 1):
|
|
109
|
+
try:
|
|
110
|
+
server = HTTPServer(("localhost", port), _CallbackHandler)
|
|
111
|
+
assign_redirect_uri(context, port)
|
|
112
|
+
result = _OAuthResult()
|
|
113
|
+
event = threading.Event()
|
|
114
|
+
_CallbackHandler.result = result
|
|
115
|
+
_CallbackHandler.received_event = event
|
|
116
|
+
|
|
117
|
+
def run_server(server=server) -> None:
|
|
118
|
+
with server:
|
|
119
|
+
server.serve_forever()
|
|
120
|
+
|
|
121
|
+
threading.Thread(target=run_server, daemon=True).start()
|
|
122
|
+
return server, result, event
|
|
123
|
+
except OSError:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
emit_error(t("oauth.server.no_ports"))
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _assign_manual_redirect_uri(context: OAuthContext) -> bool:
|
|
131
|
+
port_range = CLAUDE_CODE_OAUTH_CONFIG["callback_port_range"]
|
|
132
|
+
try:
|
|
133
|
+
assign_redirect_uri(context, port_range[0])
|
|
134
|
+
except Exception as exc: # noqa: BLE001
|
|
135
|
+
emit_error(t("oauth.server.redirect_uri_error", error=str(exc)))
|
|
136
|
+
return False
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _parse_pasted_callback(context: OAuthContext, raw_input: str) -> Optional[str]:
|
|
141
|
+
try:
|
|
142
|
+
parsed = parse_oauth_callback_input(raw_input)
|
|
143
|
+
except ValueError as exc:
|
|
144
|
+
emit_error(t("oauth.pasteback.parse_error", error=str(exc)))
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
if parsed.error:
|
|
148
|
+
emit_error(t("oauth.pasteback.provider_error", message=parsed.error_message))
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
if not parsed.code:
|
|
152
|
+
emit_error(t("oauth.pasteback.no_code"))
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
if parsed.state:
|
|
156
|
+
if parsed.state != context.state:
|
|
157
|
+
emit_error(t("oauth.state_mismatch"))
|
|
158
|
+
return None
|
|
159
|
+
else:
|
|
160
|
+
emit_warning(t("oauth.pasteback.no_state"))
|
|
161
|
+
|
|
162
|
+
return parsed.code
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _read_pasted_callback_line() -> Optional[str]:
|
|
166
|
+
"""Return one pasted callback line from whichever input the UI owns.
|
|
167
|
+
|
|
168
|
+
The trap: under the persistent TUI a *live agent run* owns stdin via the
|
|
169
|
+
key-listener thread (this is the mid-run re-auth path — an expired token
|
|
170
|
+
triggers ``_reauthenticate_after_expired_oauth`` from inside the HTTP
|
|
171
|
+
client while the run is in flight). A pasted callback URL is therefore
|
|
172
|
+
routed onto the PauseController steer queues, NOT ``sys.stdin`` — so a
|
|
173
|
+
naive ``select()`` on stdin here would spin until timeout while the paste
|
|
174
|
+
sits on the queue, unread until the run ends. Pull it off the queue.
|
|
175
|
+
|
|
176
|
+
Every other context keeps reading stdin directly:
|
|
177
|
+
* classic / headless mode — nothing owns stdin;
|
|
178
|
+
* the idle ``/claude-code-auth`` command — dispatched inside
|
|
179
|
+
``suspended_run_ui()``, which releases the key-listener, so stdin is
|
|
180
|
+
free and ``is_run_active()`` is False.
|
|
181
|
+
"""
|
|
182
|
+
try:
|
|
183
|
+
from code_puppy.messaging import run_ui
|
|
184
|
+
|
|
185
|
+
if run_ui.is_persistent() and run_ui.is_run_active():
|
|
186
|
+
from code_puppy.messaging.pause_controller import get_pause_controller
|
|
187
|
+
|
|
188
|
+
pc = get_pause_controller()
|
|
189
|
+
# Alt+Enter (queue mode) → oldest queued line first.
|
|
190
|
+
queued = pc.pop_next_steer_queued()
|
|
191
|
+
if queued is not None:
|
|
192
|
+
return queued
|
|
193
|
+
# Plain Enter mid-run lands on the now-queue. Take the oldest
|
|
194
|
+
# line and hand the rest back so genuine steers aren't swallowed.
|
|
195
|
+
drained = pc.drain_pending_steer_now()
|
|
196
|
+
if not drained:
|
|
197
|
+
return None
|
|
198
|
+
for leftover in drained[1:]:
|
|
199
|
+
pc.request_steer(leftover, mode="now")
|
|
200
|
+
return drained[0]
|
|
201
|
+
except Exception: # noqa: BLE001 - never let UI plumbing break auth
|
|
202
|
+
logger.debug("queue paste read failed; falling back to stdin", exc_info=True)
|
|
203
|
+
|
|
204
|
+
return read_available_stdin_line()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _wait_for_callback_or_paste(
|
|
208
|
+
*,
|
|
209
|
+
context: OAuthContext,
|
|
210
|
+
result: Optional[_OAuthResult],
|
|
211
|
+
event: Optional[threading.Event],
|
|
212
|
+
timeout: float,
|
|
213
|
+
) -> Optional[str]:
|
|
214
|
+
elapsed = 0.0
|
|
215
|
+
interval = 0.25
|
|
216
|
+
|
|
217
|
+
while elapsed < timeout:
|
|
218
|
+
if event and event.is_set() and result:
|
|
219
|
+
if result.error:
|
|
220
|
+
emit_error(t("oauth.callback.error", error=result.error))
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
if result.state != context.state:
|
|
224
|
+
emit_error(t("oauth.state_mismatch"))
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
return result.code
|
|
228
|
+
|
|
229
|
+
pasted = _read_pasted_callback_line()
|
|
230
|
+
if pasted is not None and pasted.strip():
|
|
231
|
+
code = _parse_pasted_callback(context, pasted)
|
|
232
|
+
if code:
|
|
233
|
+
return code
|
|
234
|
+
|
|
235
|
+
time.sleep(interval)
|
|
236
|
+
elapsed += interval
|
|
237
|
+
|
|
238
|
+
emit_error(t("oauth.callback.timeout"))
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _await_callback(context: OAuthContext) -> Optional[str]:
|
|
243
|
+
timeout = CLAUDE_CODE_OAUTH_CONFIG["callback_timeout"]
|
|
244
|
+
|
|
245
|
+
started = _start_callback_server(context)
|
|
246
|
+
server: Optional[HTTPServer] = None
|
|
247
|
+
result: Optional[_OAuthResult] = None
|
|
248
|
+
event: Optional[threading.Event] = None
|
|
249
|
+
if started:
|
|
250
|
+
server, result, event = started
|
|
251
|
+
else:
|
|
252
|
+
emit_warning(t("oauth.claude.server.pasteback_mode"))
|
|
253
|
+
if not _assign_manual_redirect_uri(context):
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
redirect_uri = context.redirect_uri
|
|
257
|
+
if not redirect_uri:
|
|
258
|
+
emit_error(t("oauth.server.no_redirect_uri"))
|
|
259
|
+
if server:
|
|
260
|
+
server.shutdown()
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
auth_url = build_authorization_url(context)
|
|
264
|
+
|
|
265
|
+
suppress_browser = False
|
|
266
|
+
try:
|
|
267
|
+
import webbrowser
|
|
268
|
+
|
|
269
|
+
from code_puppy.tools.common import should_suppress_browser
|
|
270
|
+
|
|
271
|
+
suppress_browser = should_suppress_browser()
|
|
272
|
+
if suppress_browser:
|
|
273
|
+
emit_info(t("oauth.claude.browser.headless"))
|
|
274
|
+
emit_info(t("oauth.browser.headless_url", url=auth_url))
|
|
275
|
+
else:
|
|
276
|
+
emit_info(t("oauth.claude.browser.opening"))
|
|
277
|
+
webbrowser.open(auth_url)
|
|
278
|
+
emit_info(t("oauth.browser.fallback_url", url=auth_url))
|
|
279
|
+
except Exception as exc: # pragma: no cover
|
|
280
|
+
if not suppress_browser:
|
|
281
|
+
emit_warning(t("oauth.browser.open_failed", error=str(exc)))
|
|
282
|
+
emit_info(t("oauth.browser.manual_url", url=auth_url))
|
|
283
|
+
|
|
284
|
+
if server:
|
|
285
|
+
emit_info(t("oauth.server.listening", uri=redirect_uri))
|
|
286
|
+
else:
|
|
287
|
+
emit_info(t("oauth.server.pasteback_uri", uri=redirect_uri))
|
|
288
|
+
emit_info(t("oauth.server.paste_hint"))
|
|
289
|
+
|
|
290
|
+
code = _wait_for_callback_or_paste(
|
|
291
|
+
context=context,
|
|
292
|
+
result=result,
|
|
293
|
+
event=event,
|
|
294
|
+
timeout=timeout,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
if server:
|
|
298
|
+
server.shutdown()
|
|
299
|
+
|
|
300
|
+
return code
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _custom_help() -> List[Tuple[str, str]]:
|
|
304
|
+
return [
|
|
305
|
+
(
|
|
306
|
+
"claude-code-auth",
|
|
307
|
+
"Authenticate with Claude Code via OAuth and import available models",
|
|
308
|
+
),
|
|
309
|
+
(
|
|
310
|
+
"claude-code-status",
|
|
311
|
+
"Check Claude Code OAuth authentication status and configured models",
|
|
312
|
+
),
|
|
313
|
+
("claude-code-logout", "Remove Claude Code OAuth tokens and imported models"),
|
|
314
|
+
(
|
|
315
|
+
"claude-code-fast",
|
|
316
|
+
"Toggle fast mode (speed=fast + fast-mode beta) for the active Claude Code model",
|
|
317
|
+
),
|
|
318
|
+
]
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _perform_authentication() -> bool:
|
|
322
|
+
context = prepare_oauth_context()
|
|
323
|
+
code = _await_callback(context)
|
|
324
|
+
if not code:
|
|
325
|
+
return False
|
|
326
|
+
|
|
327
|
+
emit_info(t("oauth.auth.exchanging"))
|
|
328
|
+
tokens = exchange_code_for_tokens(code, context)
|
|
329
|
+
if not tokens:
|
|
330
|
+
emit_error(t("oauth.auth.exchange_failed"))
|
|
331
|
+
return False
|
|
332
|
+
|
|
333
|
+
if not save_tokens(tokens):
|
|
334
|
+
emit_error(t("oauth.auth.save_failed"))
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
emit_success(t("oauth.claude.auth.success"))
|
|
338
|
+
|
|
339
|
+
access_token = tokens.get("access_token")
|
|
340
|
+
if not access_token:
|
|
341
|
+
emit_warning(t("oauth.auth.no_access_token"))
|
|
342
|
+
return False
|
|
343
|
+
|
|
344
|
+
emit_info(t("oauth.claude.auth.fetching_models"))
|
|
345
|
+
models = fetch_claude_code_models(access_token)
|
|
346
|
+
if not models:
|
|
347
|
+
emit_warning(t("oauth.claude.auth.no_models"))
|
|
348
|
+
return True
|
|
349
|
+
|
|
350
|
+
emit_info(
|
|
351
|
+
t("oauth.auth.discovered_models", count=len(models), models=", ".join(models))
|
|
352
|
+
)
|
|
353
|
+
if add_models_to_extra_config(models):
|
|
354
|
+
emit_success(t("oauth.claude.auth.models_added"))
|
|
355
|
+
return True
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _reauthenticate_after_expired_oauth(model_name: str) -> Optional[str]:
|
|
359
|
+
"""Run full Claude Code OAuth only for configured claude-code-* models."""
|
|
360
|
+
prefix = CLAUDE_CODE_OAUTH_CONFIG["prefix"]
|
|
361
|
+
if not model_name.startswith(prefix):
|
|
362
|
+
logger.debug(
|
|
363
|
+
"Skipping Claude Code OAuth flow for non-prefixed model: %s", model_name
|
|
364
|
+
)
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
emit_warning(t("oauth.claude.reauth.refresh_failed"))
|
|
368
|
+
_perform_authentication()
|
|
369
|
+
|
|
370
|
+
access_token = get_valid_access_token()
|
|
371
|
+
if access_token:
|
|
372
|
+
emit_success(t("oauth.claude.reauth.restored"))
|
|
373
|
+
return access_token
|
|
374
|
+
|
|
375
|
+
emit_error(t("oauth.claude.reauth.no_token"))
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _handle_custom_command(command: str, name: str) -> Optional[bool]:
|
|
380
|
+
if not name:
|
|
381
|
+
return None
|
|
382
|
+
|
|
383
|
+
if name == "claude-code-auth":
|
|
384
|
+
emit_info(t("oauth.claude.cmd.auth.starting"))
|
|
385
|
+
tokens = load_stored_tokens()
|
|
386
|
+
if tokens and tokens.get("access_token"):
|
|
387
|
+
emit_warning(t("oauth.claude.cmd.auth.overwrite_warning"))
|
|
388
|
+
_perform_authentication()
|
|
389
|
+
set_model_and_reload_agent("claude-code-claude-opus-4-8-long")
|
|
390
|
+
return True
|
|
391
|
+
|
|
392
|
+
if name == "claude-code-status":
|
|
393
|
+
tokens = load_stored_tokens()
|
|
394
|
+
if tokens and tokens.get("access_token"):
|
|
395
|
+
emit_success(t("oauth.claude.cmd.status.authenticated"))
|
|
396
|
+
expires_at = tokens.get("expires_at")
|
|
397
|
+
if expires_at:
|
|
398
|
+
remaining = max(0, int(expires_at - time.time()))
|
|
399
|
+
hours, minutes = divmod(remaining // 60, 60)
|
|
400
|
+
emit_info(t("oauth.cmd.status.expires", hours=hours, minutes=minutes))
|
|
401
|
+
|
|
402
|
+
claude_models = [
|
|
403
|
+
name
|
|
404
|
+
for name, cfg in load_claude_models_filtered().items()
|
|
405
|
+
if cfg.get("oauth_source") == "claude-code-plugin"
|
|
406
|
+
]
|
|
407
|
+
if claude_models:
|
|
408
|
+
emit_info(
|
|
409
|
+
t("oauth.claude.cmd.status.models", models=", ".join(claude_models))
|
|
410
|
+
)
|
|
411
|
+
else:
|
|
412
|
+
emit_warning(t("oauth.claude.cmd.status.no_models"))
|
|
413
|
+
else:
|
|
414
|
+
emit_warning(t("oauth.claude.cmd.status.not_authenticated"))
|
|
415
|
+
emit_info(t("oauth.claude.cmd.status.hint"))
|
|
416
|
+
return True
|
|
417
|
+
|
|
418
|
+
if name == "claude-code-fast":
|
|
419
|
+
from code_puppy.config import (
|
|
420
|
+
get_global_model_name,
|
|
421
|
+
set_model_setting,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
active_model = get_global_model_name() or ""
|
|
425
|
+
if not active_model.startswith(CLAUDE_CODE_OAUTH_CONFIG["prefix"]):
|
|
426
|
+
emit_warning(t("oauth.claude.cmd.fast.wrong_model"))
|
|
427
|
+
return True
|
|
428
|
+
|
|
429
|
+
currently_on = is_fast_mode_enabled(active_model)
|
|
430
|
+
new_value = not currently_on
|
|
431
|
+
# Config stores bools as string values; "true"/"false" round-trips cleanly
|
|
432
|
+
set_model_setting(active_model, FAST_SETTING_KEY, str(new_value).lower())
|
|
433
|
+
|
|
434
|
+
if new_value:
|
|
435
|
+
emit_success(t("oauth.claude.cmd.fast.enabled", model=active_model))
|
|
436
|
+
emit_info(t("oauth.claude.cmd.fast.enabled_detail"))
|
|
437
|
+
else:
|
|
438
|
+
emit_info(t("oauth.claude.cmd.fast.disabled", model=active_model))
|
|
439
|
+
|
|
440
|
+
# Reload agent so the anthropic-beta header update (set at client
|
|
441
|
+
# construction time) takes effect. Payload side is live either way.
|
|
442
|
+
set_model_and_reload_agent(active_model, warn_on_pinned_mismatch=False)
|
|
443
|
+
return True
|
|
444
|
+
|
|
445
|
+
if name == "claude-code-logout":
|
|
446
|
+
token_path = get_token_storage_path()
|
|
447
|
+
if token_path.exists():
|
|
448
|
+
token_path.unlink()
|
|
449
|
+
emit_info(t("oauth.claude.cmd.logout.tokens_removed"))
|
|
450
|
+
|
|
451
|
+
removed = remove_claude_code_models()
|
|
452
|
+
if removed:
|
|
453
|
+
emit_info(t("oauth.cmd.logout.models_removed", count=removed))
|
|
454
|
+
|
|
455
|
+
emit_success(t("oauth.claude.cmd.logout.success"))
|
|
456
|
+
return True
|
|
457
|
+
|
|
458
|
+
return None
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _resolve_cache_ttl(model_name: str) -> Optional[str]:
|
|
462
|
+
"""Prompt-cache TTL for a claude_code-type model.
|
|
463
|
+
|
|
464
|
+
``claude-code-*`` models (OAuth subscription) always get the free 1-hour
|
|
465
|
+
TTL; anything else falls back to Anthropic's 5-minute default (None).
|
|
466
|
+
"""
|
|
467
|
+
from code_puppy.claude_cache_client import CACHE_TTL_1H
|
|
468
|
+
|
|
469
|
+
return CACHE_TTL_1H if is_claude_code_model(model_name) else None
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _create_claude_code_model(model_name: str, model_config: Dict, config: Dict) -> Any:
|
|
473
|
+
"""Create a Claude Code model instance.
|
|
474
|
+
|
|
475
|
+
This handler is registered via the 'register_model_type' callback to handle
|
|
476
|
+
models with type='claude_code'.
|
|
477
|
+
"""
|
|
478
|
+
from anthropic import AsyncAnthropic
|
|
479
|
+
from pydantic_ai.models.anthropic import AnthropicModel
|
|
480
|
+
|
|
481
|
+
from code_puppy.claude_cache_client import (
|
|
482
|
+
ClaudeCacheAsyncClient,
|
|
483
|
+
patch_anthropic_client_messages,
|
|
484
|
+
)
|
|
485
|
+
from code_puppy.http_utils import get_cert_bundle_path
|
|
486
|
+
from code_puppy.model_factory import get_custom_config
|
|
487
|
+
|
|
488
|
+
url, headers, verify, api_key, timeout = get_custom_config(model_config)
|
|
489
|
+
|
|
490
|
+
# Refresh token if this is from the plugin
|
|
491
|
+
if model_config.get("oauth_source") == "claude-code-plugin":
|
|
492
|
+
refreshed_token = get_valid_access_token()
|
|
493
|
+
if refreshed_token:
|
|
494
|
+
api_key = refreshed_token
|
|
495
|
+
custom_endpoint = model_config.get("custom_endpoint")
|
|
496
|
+
if isinstance(custom_endpoint, dict):
|
|
497
|
+
custom_endpoint["api_key"] = refreshed_token
|
|
498
|
+
|
|
499
|
+
if not api_key:
|
|
500
|
+
emit_warning(
|
|
501
|
+
t(
|
|
502
|
+
"oauth.claude.model.no_api_key",
|
|
503
|
+
model=model_config.get("name") or "(unknown)",
|
|
504
|
+
)
|
|
505
|
+
)
|
|
506
|
+
return None
|
|
507
|
+
|
|
508
|
+
# Interleaved thinking (defaults True for OAuth models). NOTE: read via
|
|
509
|
+
# get_all_model_settings — these plugin-owned settings aren't in core's
|
|
510
|
+
# supported_settings allowlist (see fast_mode.FAST_SETTING_KEY).
|
|
511
|
+
from code_puppy.config import get_all_model_settings
|
|
512
|
+
|
|
513
|
+
per_model_settings = get_all_model_settings(model_name)
|
|
514
|
+
interleaved_thinking = per_model_settings.get("interleaved_thinking", True)
|
|
515
|
+
fast_enabled = bool(per_model_settings.get(FAST_SETTING_KEY, False))
|
|
516
|
+
|
|
517
|
+
# Handle anthropic-beta header based on interleaved_thinking setting
|
|
518
|
+
if "anthropic-beta" in headers:
|
|
519
|
+
beta_parts = [p.strip() for p in headers["anthropic-beta"].split(",")]
|
|
520
|
+
if interleaved_thinking:
|
|
521
|
+
if "interleaved-thinking-2025-05-14" not in beta_parts:
|
|
522
|
+
beta_parts.append("interleaved-thinking-2025-05-14")
|
|
523
|
+
else:
|
|
524
|
+
beta_parts = [p for p in beta_parts if "interleaved-thinking" not in p]
|
|
525
|
+
headers["anthropic-beta"] = ",".join(beta_parts) if beta_parts else None
|
|
526
|
+
if headers.get("anthropic-beta") is None:
|
|
527
|
+
del headers["anthropic-beta"]
|
|
528
|
+
elif interleaved_thinking:
|
|
529
|
+
headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
|
|
530
|
+
|
|
531
|
+
# Add 1M context beta header for long-context models
|
|
532
|
+
from code_puppy.model_factory import CONTEXT_1M_BETA
|
|
533
|
+
|
|
534
|
+
if model_config.get("context_length", 0) >= 1_000_000:
|
|
535
|
+
if "anthropic-beta" in headers:
|
|
536
|
+
beta_parts = [p.strip() for p in headers["anthropic-beta"].split(",")]
|
|
537
|
+
if CONTEXT_1M_BETA not in beta_parts:
|
|
538
|
+
beta_parts.append(CONTEXT_1M_BETA)
|
|
539
|
+
headers["anthropic-beta"] = ",".join(beta_parts)
|
|
540
|
+
else:
|
|
541
|
+
headers["anthropic-beta"] = CONTEXT_1M_BETA
|
|
542
|
+
|
|
543
|
+
# Fast mode: append fast-mode-2026-02-01 beta marker when enabled
|
|
544
|
+
ensure_fast_beta_header(headers, fast_enabled)
|
|
545
|
+
|
|
546
|
+
# Use a dedicated client wrapper that injects cache_control on /v1/messages
|
|
547
|
+
if verify is None:
|
|
548
|
+
verify = get_cert_bundle_path()
|
|
549
|
+
|
|
550
|
+
# claude-code-* OAuth models get 1-hour prompt caching free, so they ALWAYS
|
|
551
|
+
# request the extended TTL; plain anthropic/custom_anthropic models keep the
|
|
552
|
+
# default 5-minute TTL — deliberately not applied to those.
|
|
553
|
+
cache_ttl = _resolve_cache_ttl(model_name)
|
|
554
|
+
|
|
555
|
+
# No HTTP/2 for OAuth: the UnprefixingStream tool-name rewrite breaks under
|
|
556
|
+
# HTTP/2's compression handling, causing zlib decompression errors.
|
|
557
|
+
client = ClaudeCacheAsyncClient(
|
|
558
|
+
headers=headers,
|
|
559
|
+
verify=verify,
|
|
560
|
+
timeout=180,
|
|
561
|
+
http2=False,
|
|
562
|
+
# Claude Code OAuth requires the ``cp_`` tool-name prefix; the wire
|
|
563
|
+
# format Anthropic's CLI uses won't accept un-prefixed tools.
|
|
564
|
+
apply_claude_code_prefix=True,
|
|
565
|
+
cache_ttl=cache_ttl,
|
|
566
|
+
oauth_reauthentication_callback=lambda: _reauthenticate_after_expired_oauth(
|
|
567
|
+
model_name
|
|
568
|
+
),
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
anthropic_client = AsyncAnthropic(
|
|
572
|
+
base_url=url,
|
|
573
|
+
http_client=client,
|
|
574
|
+
auth_token=api_key,
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
def _update_runtime_token(access_token: str) -> None:
|
|
578
|
+
anthropic_client.auth_token = access_token
|
|
579
|
+
custom_endpoint = model_config.get("custom_endpoint")
|
|
580
|
+
if isinstance(custom_endpoint, dict):
|
|
581
|
+
custom_endpoint["api_key"] = access_token
|
|
582
|
+
|
|
583
|
+
client.set_token_update_callback(_update_runtime_token)
|
|
584
|
+
patch_anthropic_client_messages(anthropic_client, cache_ttl=cache_ttl)
|
|
585
|
+
# Fast mode wrapper sits outside cache-control injector and re-reads
|
|
586
|
+
# the setting on every call so /claude-code-fast takes effect live.
|
|
587
|
+
patch_anthropic_client_fast_mode(anthropic_client, model_name)
|
|
588
|
+
anthropic_client.api_key = None
|
|
589
|
+
anthropic_client.auth_token = api_key
|
|
590
|
+
provider = make_anthropic_provider(
|
|
591
|
+
resolve_provider_identity(model_name, model_config),
|
|
592
|
+
anthropic_client=anthropic_client,
|
|
593
|
+
)
|
|
594
|
+
return AnthropicModel(model_name=model_config["name"], provider=provider)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _register_model_types() -> List[Dict[str, Any]]:
|
|
598
|
+
"""Register the claude_code model type handler."""
|
|
599
|
+
return [{"type": "claude_code", "handler": _create_claude_code_model}]
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
# Global storage for the token refresh heartbeat
|
|
603
|
+
# Using a dict to allow multiple concurrent agent runs (keyed by session_id)
|
|
604
|
+
_active_heartbeats: Dict[str, Any] = {}
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
async def _on_agent_run_start(
|
|
608
|
+
agent_name: str,
|
|
609
|
+
model_name: str,
|
|
610
|
+
session_id: Optional[str] = None,
|
|
611
|
+
) -> None:
|
|
612
|
+
"""Start token refresh heartbeat for Claude Code OAuth models.
|
|
613
|
+
|
|
614
|
+
This callback is triggered when an agent run starts. If the model is a
|
|
615
|
+
Claude Code OAuth model, we start a background heartbeat to keep the
|
|
616
|
+
token fresh during long-running operations.
|
|
617
|
+
"""
|
|
618
|
+
# Only start heartbeat for Claude Code models
|
|
619
|
+
if not model_name.startswith("claude-code"):
|
|
620
|
+
return
|
|
621
|
+
|
|
622
|
+
try:
|
|
623
|
+
from .token_refresh_heartbeat import TokenRefreshHeartbeat
|
|
624
|
+
|
|
625
|
+
heartbeat = TokenRefreshHeartbeat()
|
|
626
|
+
await heartbeat.start()
|
|
627
|
+
|
|
628
|
+
# Store heartbeat for cleanup, keyed by session_id
|
|
629
|
+
key = session_id or "default"
|
|
630
|
+
_active_heartbeats[key] = heartbeat
|
|
631
|
+
logger.debug(
|
|
632
|
+
"Started token refresh heartbeat for session %s (model: %s)",
|
|
633
|
+
key,
|
|
634
|
+
model_name,
|
|
635
|
+
)
|
|
636
|
+
except ImportError:
|
|
637
|
+
logger.debug("Token refresh heartbeat module not available")
|
|
638
|
+
except Exception as exc:
|
|
639
|
+
logger.debug("Failed to start token refresh heartbeat: %s", exc)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
async def _on_agent_run_end(
|
|
643
|
+
agent_name: str,
|
|
644
|
+
model_name: str,
|
|
645
|
+
session_id: Optional[str] = None,
|
|
646
|
+
success: bool = True,
|
|
647
|
+
error: Optional[Exception] = None,
|
|
648
|
+
response_text: Optional[str] = None,
|
|
649
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
650
|
+
) -> None:
|
|
651
|
+
"""Stop token refresh heartbeat when agent run ends.
|
|
652
|
+
|
|
653
|
+
This callback is triggered when an agent run completes (success or failure).
|
|
654
|
+
We stop any heartbeat that was started for this session.
|
|
655
|
+
"""
|
|
656
|
+
# We don't use response_text or metadata, just cleanup the heartbeat
|
|
657
|
+
key = session_id or "default"
|
|
658
|
+
heartbeat = _active_heartbeats.pop(key, None)
|
|
659
|
+
|
|
660
|
+
if heartbeat is not None:
|
|
661
|
+
try:
|
|
662
|
+
await heartbeat.stop()
|
|
663
|
+
logger.debug(
|
|
664
|
+
"Stopped token refresh heartbeat for session %s (refreshed %d times)",
|
|
665
|
+
key,
|
|
666
|
+
heartbeat.refresh_count,
|
|
667
|
+
)
|
|
668
|
+
except Exception as exc:
|
|
669
|
+
logger.debug("Error stopping token refresh heartbeat: %s", exc)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _hook_check_token_expiry() -> bool:
|
|
673
|
+
"""Hook: is the stored Claude Code OAuth token inside its refresh window?
|
|
674
|
+
|
|
675
|
+
Consumed by core's ``ClaudeCacheAsyncClient._check_stored_token_expiry``
|
|
676
|
+
(replacing a direct core->plugin import).
|
|
677
|
+
"""
|
|
678
|
+
tokens = load_stored_tokens()
|
|
679
|
+
if not tokens:
|
|
680
|
+
return False
|
|
681
|
+
return is_token_expired(tokens)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _hook_refresh_token() -> Optional[str]:
|
|
685
|
+
"""Hook: force a refresh-token exchange, returning the new access token.
|
|
686
|
+
|
|
687
|
+
Consumed by core's ``ClaudeCacheAsyncClient._refresh_claude_oauth_token``.
|
|
688
|
+
"""
|
|
689
|
+
return refresh_access_token(force=True)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _hook_load_models() -> Dict[str, Any]:
|
|
693
|
+
"""Hook: return this plugin's Claude models filtered to latest versions.
|
|
694
|
+
|
|
695
|
+
Consumed by core's ``ModelFactory.load_config`` (replacing a direct
|
|
696
|
+
``load_claude_models_filtered`` import from ``.utils``).
|
|
697
|
+
"""
|
|
698
|
+
return load_claude_models_filtered()
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _hook_authenticate() -> bool:
|
|
702
|
+
"""Hook: run the full interactive Claude Code OAuth flow.
|
|
703
|
+
|
|
704
|
+
Consumed by core's ``handle_tutorial_command`` (replacing a direct import
|
|
705
|
+
of ``_perform_authentication``).
|
|
706
|
+
"""
|
|
707
|
+
return _perform_authentication()
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
register_callback("custom_command_help", _custom_help)
|
|
711
|
+
register_callback("custom_command", _handle_custom_command)
|
|
712
|
+
register_callback("check_claude_oauth_token_expiry", _hook_check_token_expiry)
|
|
713
|
+
register_callback("refresh_claude_oauth_token", _hook_refresh_token)
|
|
714
|
+
register_callback("load_claude_oauth_models", _hook_load_models)
|
|
715
|
+
register_callback("claude_oauth_authenticate", _hook_authenticate)
|
|
716
|
+
register_callback("register_model_type", _register_model_types)
|
|
717
|
+
register_callback("prepare_model_prompt", prepare_claude_code_prompt)
|
|
718
|
+
register_callback("agent_run_start", _on_agent_run_start)
|
|
719
|
+
register_callback("agent_run_end", _on_agent_run_end)
|