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,518 @@
|
|
|
1
|
+
"""Utility helpers for the Claude Code OAuth plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
import secrets
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
14
|
+
from urllib.parse import urlencode
|
|
15
|
+
|
|
16
|
+
import requests
|
|
17
|
+
|
|
18
|
+
from .config import (
|
|
19
|
+
CLAUDE_CODE_OAUTH_CONFIG,
|
|
20
|
+
get_claude_models_path,
|
|
21
|
+
get_token_storage_path,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
TOKEN_REFRESH_BUFFER_SECONDS = 60
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class OAuthContext:
|
|
31
|
+
"""Runtime state for an in-progress OAuth flow."""
|
|
32
|
+
|
|
33
|
+
state: str
|
|
34
|
+
code_verifier: str
|
|
35
|
+
code_challenge: str
|
|
36
|
+
created_at: float
|
|
37
|
+
redirect_uri: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_oauth_context: Optional[OAuthContext] = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _urlsafe_b64encode(data: bytes) -> str:
|
|
44
|
+
return base64.urlsafe_b64encode(data).decode("utf-8").rstrip("=")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _generate_code_verifier() -> str:
|
|
48
|
+
return _urlsafe_b64encode(secrets.token_bytes(64))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _compute_code_challenge(code_verifier: str) -> str:
|
|
52
|
+
digest = hashlib.sha256(code_verifier.encode("utf-8")).digest()
|
|
53
|
+
return _urlsafe_b64encode(digest)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def prepare_oauth_context() -> OAuthContext:
|
|
57
|
+
"""Create and cache a new OAuth PKCE context."""
|
|
58
|
+
global _oauth_context
|
|
59
|
+
state = secrets.token_urlsafe(32)
|
|
60
|
+
code_verifier = _generate_code_verifier()
|
|
61
|
+
code_challenge = _compute_code_challenge(code_verifier)
|
|
62
|
+
_oauth_context = OAuthContext(
|
|
63
|
+
state=state,
|
|
64
|
+
code_verifier=code_verifier,
|
|
65
|
+
code_challenge=code_challenge,
|
|
66
|
+
created_at=time.time(),
|
|
67
|
+
)
|
|
68
|
+
return _oauth_context
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_oauth_context() -> Optional[OAuthContext]:
|
|
72
|
+
return _oauth_context
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def clear_oauth_context() -> None:
|
|
76
|
+
global _oauth_context
|
|
77
|
+
_oauth_context = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def assign_redirect_uri(context: OAuthContext, port: int) -> str:
|
|
81
|
+
"""Assign redirect URI for the given OAuth context."""
|
|
82
|
+
if context is None:
|
|
83
|
+
raise RuntimeError("OAuth context cannot be None")
|
|
84
|
+
|
|
85
|
+
host = CLAUDE_CODE_OAUTH_CONFIG["redirect_host"].rstrip("/")
|
|
86
|
+
path = CLAUDE_CODE_OAUTH_CONFIG["redirect_path"].lstrip("/")
|
|
87
|
+
redirect_uri = f"{host}:{port}/{path}"
|
|
88
|
+
context.redirect_uri = redirect_uri
|
|
89
|
+
return redirect_uri
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def build_authorization_url(context: OAuthContext) -> str:
|
|
93
|
+
"""Return the Claude authorization URL with PKCE parameters."""
|
|
94
|
+
if not context.redirect_uri:
|
|
95
|
+
raise RuntimeError("Redirect URI has not been assigned for this OAuth context")
|
|
96
|
+
|
|
97
|
+
params = {
|
|
98
|
+
"response_type": "code",
|
|
99
|
+
"client_id": CLAUDE_CODE_OAUTH_CONFIG["client_id"],
|
|
100
|
+
"redirect_uri": context.redirect_uri,
|
|
101
|
+
"scope": CLAUDE_CODE_OAUTH_CONFIG["scope"],
|
|
102
|
+
"state": context.state,
|
|
103
|
+
"code": "true",
|
|
104
|
+
"code_challenge": context.code_challenge,
|
|
105
|
+
"code_challenge_method": "S256",
|
|
106
|
+
}
|
|
107
|
+
return f"{CLAUDE_CODE_OAUTH_CONFIG['auth_url']}?{urlencode(params)}"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def parse_authorization_code(raw_input: str) -> Tuple[str, Optional[str]]:
|
|
111
|
+
value = raw_input.strip()
|
|
112
|
+
if not value:
|
|
113
|
+
raise ValueError("Authorization code cannot be empty")
|
|
114
|
+
|
|
115
|
+
if "#" in value:
|
|
116
|
+
code, state = value.split("#", 1)
|
|
117
|
+
return code.strip(), state.strip() or None
|
|
118
|
+
|
|
119
|
+
parts = value.split()
|
|
120
|
+
if len(parts) == 2:
|
|
121
|
+
return parts[0].strip(), parts[1].strip() or None
|
|
122
|
+
|
|
123
|
+
return value, None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def load_stored_tokens() -> Optional[Dict[str, Any]]:
|
|
127
|
+
try:
|
|
128
|
+
token_path = get_token_storage_path()
|
|
129
|
+
if token_path.exists():
|
|
130
|
+
with open(token_path, "r", encoding="utf-8") as handle:
|
|
131
|
+
return json.load(handle)
|
|
132
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
133
|
+
logger.error("Failed to load tokens: %s", exc)
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _calculate_expires_at(expires_in: Optional[float]) -> Optional[float]:
|
|
138
|
+
if expires_in is None:
|
|
139
|
+
return None
|
|
140
|
+
try:
|
|
141
|
+
return time.time() + float(expires_in)
|
|
142
|
+
except (TypeError, ValueError):
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def is_token_expired(tokens: Dict[str, Any]) -> bool:
|
|
147
|
+
expires_at = tokens.get("expires_at")
|
|
148
|
+
if expires_at is None:
|
|
149
|
+
return False
|
|
150
|
+
try:
|
|
151
|
+
expires_at_value = float(expires_at)
|
|
152
|
+
except (TypeError, ValueError):
|
|
153
|
+
return False
|
|
154
|
+
return time.time() >= expires_at_value - TOKEN_REFRESH_BUFFER_SECONDS
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def update_claude_code_model_tokens(access_token: str) -> bool:
|
|
158
|
+
try:
|
|
159
|
+
claude_models = load_claude_models()
|
|
160
|
+
if not claude_models:
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
updated = False
|
|
164
|
+
for config in claude_models.values():
|
|
165
|
+
if config.get("oauth_source") != "claude-code-plugin":
|
|
166
|
+
continue
|
|
167
|
+
custom_endpoint = config.get("custom_endpoint")
|
|
168
|
+
if not isinstance(custom_endpoint, dict):
|
|
169
|
+
continue
|
|
170
|
+
custom_endpoint["api_key"] = access_token
|
|
171
|
+
updated = True
|
|
172
|
+
|
|
173
|
+
if updated:
|
|
174
|
+
return save_claude_models(claude_models)
|
|
175
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
176
|
+
logger.error("Failed to update Claude model tokens: %s", exc)
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def refresh_access_token(force: bool = False) -> Optional[str]:
|
|
181
|
+
tokens = load_stored_tokens()
|
|
182
|
+
if not tokens:
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
if not force and not is_token_expired(tokens):
|
|
186
|
+
return tokens.get("access_token")
|
|
187
|
+
|
|
188
|
+
refresh_token = tokens.get("refresh_token")
|
|
189
|
+
if not refresh_token:
|
|
190
|
+
logger.debug("No refresh_token available")
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
payload = {
|
|
194
|
+
"grant_type": "refresh_token",
|
|
195
|
+
"client_id": CLAUDE_CODE_OAUTH_CONFIG["client_id"],
|
|
196
|
+
"refresh_token": refresh_token,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
headers = {
|
|
200
|
+
"Content-Type": "application/json",
|
|
201
|
+
"Accept": "application/json",
|
|
202
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
response = requests.post(
|
|
207
|
+
CLAUDE_CODE_OAUTH_CONFIG["token_url"],
|
|
208
|
+
json=payload,
|
|
209
|
+
headers=headers,
|
|
210
|
+
timeout=30,
|
|
211
|
+
)
|
|
212
|
+
if response.status_code == 200:
|
|
213
|
+
new_tokens = response.json()
|
|
214
|
+
tokens["access_token"] = new_tokens.get("access_token")
|
|
215
|
+
tokens["refresh_token"] = new_tokens.get("refresh_token", refresh_token)
|
|
216
|
+
if "expires_in" in new_tokens:
|
|
217
|
+
tokens["expires_in"] = new_tokens["expires_in"]
|
|
218
|
+
tokens["expires_at"] = _calculate_expires_at(
|
|
219
|
+
new_tokens.get("expires_in")
|
|
220
|
+
)
|
|
221
|
+
if save_tokens(tokens):
|
|
222
|
+
update_claude_code_model_tokens(tokens["access_token"])
|
|
223
|
+
return tokens["access_token"]
|
|
224
|
+
else:
|
|
225
|
+
logger.error(
|
|
226
|
+
"Token refresh failed: %s - %s", response.status_code, response.text
|
|
227
|
+
)
|
|
228
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
229
|
+
logger.error("Token refresh error: %s", exc)
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def get_valid_access_token() -> Optional[str]:
|
|
234
|
+
tokens = load_stored_tokens()
|
|
235
|
+
if not tokens:
|
|
236
|
+
logger.debug("No stored Claude Code OAuth tokens found")
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
access_token = tokens.get("access_token")
|
|
240
|
+
if not access_token:
|
|
241
|
+
logger.debug("No access_token in stored tokens")
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
if is_token_expired(tokens):
|
|
245
|
+
logger.info("Claude Code OAuth token expired, attempting refresh")
|
|
246
|
+
refreshed = refresh_access_token()
|
|
247
|
+
if refreshed:
|
|
248
|
+
return refreshed
|
|
249
|
+
logger.warning("Claude Code token refresh failed")
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
return access_token
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def save_tokens(tokens: Dict[str, Any]) -> bool:
|
|
256
|
+
try:
|
|
257
|
+
token_path = get_token_storage_path()
|
|
258
|
+
with open(token_path, "w", encoding="utf-8") as handle:
|
|
259
|
+
json.dump(tokens, handle, indent=2)
|
|
260
|
+
token_path.chmod(0o600)
|
|
261
|
+
return True
|
|
262
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
263
|
+
logger.error("Failed to save tokens: %s", exc)
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def load_claude_models() -> Dict[str, Any]:
|
|
268
|
+
try:
|
|
269
|
+
models_path = get_claude_models_path()
|
|
270
|
+
if models_path.exists():
|
|
271
|
+
with open(models_path, "r", encoding="utf-8") as handle:
|
|
272
|
+
return json.load(handle)
|
|
273
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
274
|
+
logger.error("Failed to load Claude models: %s", exc)
|
|
275
|
+
return {}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def load_claude_models_filtered() -> Dict[str, Any]:
|
|
279
|
+
"""Load Claude models and filter to only the latest versions.
|
|
280
|
+
|
|
281
|
+
This loads the stored models and applies the same filtering logic
|
|
282
|
+
used during saving to ensure only the latest haiku, sonnet, and opus
|
|
283
|
+
models are returned.
|
|
284
|
+
"""
|
|
285
|
+
try:
|
|
286
|
+
all_models = load_claude_models()
|
|
287
|
+
if not all_models:
|
|
288
|
+
return {}
|
|
289
|
+
|
|
290
|
+
# Extract model names from the configuration
|
|
291
|
+
model_names = []
|
|
292
|
+
for name, config in all_models.items():
|
|
293
|
+
if config.get("oauth_source") == "claude-code-plugin":
|
|
294
|
+
model_names.append(config.get("name", ""))
|
|
295
|
+
else:
|
|
296
|
+
# For non-OAuth models, use the full key
|
|
297
|
+
model_names.append(name)
|
|
298
|
+
|
|
299
|
+
# Filter to only latest models
|
|
300
|
+
latest_names = set(filter_latest_claude_models(model_names))
|
|
301
|
+
|
|
302
|
+
# Return only the filtered models
|
|
303
|
+
filtered_models = {}
|
|
304
|
+
for name, config in all_models.items():
|
|
305
|
+
model_name = config.get("name", name)
|
|
306
|
+
if model_name in latest_names:
|
|
307
|
+
filtered_models[name] = config
|
|
308
|
+
|
|
309
|
+
logger.info(
|
|
310
|
+
"Loaded %d models, filtered to %d latest models",
|
|
311
|
+
len(all_models),
|
|
312
|
+
len(filtered_models),
|
|
313
|
+
)
|
|
314
|
+
return filtered_models
|
|
315
|
+
|
|
316
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
317
|
+
logger.error("Failed to load and filter Claude models: %s", exc)
|
|
318
|
+
return {}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def save_claude_models(models: Dict[str, Any]) -> bool:
|
|
322
|
+
try:
|
|
323
|
+
models_path = get_claude_models_path()
|
|
324
|
+
with open(models_path, "w", encoding="utf-8") as handle:
|
|
325
|
+
json.dump(models, handle, indent=2)
|
|
326
|
+
return True
|
|
327
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
328
|
+
logger.error("Failed to save Claude models: %s", exc)
|
|
329
|
+
return False
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def exchange_code_for_tokens(
|
|
333
|
+
auth_code: str, context: OAuthContext
|
|
334
|
+
) -> Optional[Dict[str, Any]]:
|
|
335
|
+
if not context.redirect_uri:
|
|
336
|
+
raise RuntimeError("Redirect URI missing from OAuth context")
|
|
337
|
+
|
|
338
|
+
payload = {
|
|
339
|
+
"grant_type": "authorization_code",
|
|
340
|
+
"client_id": CLAUDE_CODE_OAUTH_CONFIG["client_id"],
|
|
341
|
+
"code": auth_code,
|
|
342
|
+
"state": context.state,
|
|
343
|
+
"code_verifier": context.code_verifier,
|
|
344
|
+
"redirect_uri": context.redirect_uri,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
headers = {
|
|
348
|
+
"Content-Type": "application/json",
|
|
349
|
+
"Accept": "application/json",
|
|
350
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
logger.info("Exchanging code for tokens: %s", CLAUDE_CODE_OAUTH_CONFIG["token_url"])
|
|
354
|
+
logger.debug("Payload keys: %s", list(payload.keys()))
|
|
355
|
+
logger.debug("Headers: %s", headers)
|
|
356
|
+
try:
|
|
357
|
+
response = requests.post(
|
|
358
|
+
CLAUDE_CODE_OAUTH_CONFIG["token_url"],
|
|
359
|
+
json=payload,
|
|
360
|
+
headers=headers,
|
|
361
|
+
timeout=30,
|
|
362
|
+
)
|
|
363
|
+
logger.info("Token exchange response: %s", response.status_code)
|
|
364
|
+
logger.debug("Response body: %s", response.text)
|
|
365
|
+
if response.status_code == 200:
|
|
366
|
+
token_data = response.json()
|
|
367
|
+
token_data["expires_at"] = _calculate_expires_at(
|
|
368
|
+
token_data.get("expires_in")
|
|
369
|
+
)
|
|
370
|
+
return token_data
|
|
371
|
+
logger.error(
|
|
372
|
+
"Token exchange failed: %s - %s",
|
|
373
|
+
response.status_code,
|
|
374
|
+
response.text,
|
|
375
|
+
)
|
|
376
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
377
|
+
logger.error("Token exchange error: %s", exc)
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def filter_latest_claude_models(models: List[str]) -> List[str]:
|
|
382
|
+
"""Filter models to keep only the latest haiku, sonnet, and opus.
|
|
383
|
+
|
|
384
|
+
Parses model names in the format claude-{family}-{major}-{minor}-{date}
|
|
385
|
+
and returns only the latest version of each family (haiku, sonnet, opus).
|
|
386
|
+
"""
|
|
387
|
+
# Dictionary to store the latest model for each family
|
|
388
|
+
# family -> (model_name, major, minor, date)
|
|
389
|
+
latest_models: Dict[str, Tuple[str, int, int, int]] = {}
|
|
390
|
+
|
|
391
|
+
for model_name in models:
|
|
392
|
+
# Match pattern: claude-{family}-{major}-{minor}-{date}
|
|
393
|
+
# Examples: claude-haiku-3-5-20241022, claude-sonnet-4-5-20250929
|
|
394
|
+
match = re.match(r"claude-(haiku|sonnet|opus)-(\d+)-(\d+)-(\d+)", model_name)
|
|
395
|
+
if not match:
|
|
396
|
+
# Also try pattern with dots: claude-{family}-{major}.{minor}-{date}
|
|
397
|
+
match = re.match(
|
|
398
|
+
r"claude-(haiku|sonnet|opus)-(\d+)\.(\d+)-(\d+)", model_name
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
if not match:
|
|
402
|
+
continue
|
|
403
|
+
|
|
404
|
+
family = match.group(1)
|
|
405
|
+
major = int(match.group(2))
|
|
406
|
+
minor = int(match.group(3))
|
|
407
|
+
date = int(match.group(4))
|
|
408
|
+
|
|
409
|
+
if family not in latest_models:
|
|
410
|
+
latest_models[family] = (model_name, major, minor, date)
|
|
411
|
+
else:
|
|
412
|
+
# Compare versions: first by major, then minor, then date
|
|
413
|
+
_, cur_major, cur_minor, cur_date = latest_models[family]
|
|
414
|
+
if (major, minor, date) > (cur_major, cur_minor, cur_date):
|
|
415
|
+
latest_models[family] = (model_name, major, minor, date)
|
|
416
|
+
|
|
417
|
+
# Return only the model names
|
|
418
|
+
filtered = [model_data[0] for model_data in latest_models.values()]
|
|
419
|
+
logger.info(
|
|
420
|
+
"Filtered %d models to %d latest models: %s",
|
|
421
|
+
len(models),
|
|
422
|
+
len(filtered),
|
|
423
|
+
filtered,
|
|
424
|
+
)
|
|
425
|
+
return filtered
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def fetch_claude_code_models(access_token: str) -> Optional[List[str]]:
|
|
429
|
+
try:
|
|
430
|
+
api_url = f"{CLAUDE_CODE_OAUTH_CONFIG['api_base_url']}/v1/models"
|
|
431
|
+
headers = {
|
|
432
|
+
"Authorization": f"Bearer {access_token}",
|
|
433
|
+
"Content-Type": "application/json",
|
|
434
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
435
|
+
"anthropic-version": CLAUDE_CODE_OAUTH_CONFIG.get(
|
|
436
|
+
"anthropic_version", "2023-06-01"
|
|
437
|
+
),
|
|
438
|
+
}
|
|
439
|
+
response = requests.get(api_url, headers=headers, timeout=30)
|
|
440
|
+
if response.status_code == 200:
|
|
441
|
+
data = response.json()
|
|
442
|
+
if isinstance(data.get("data"), list):
|
|
443
|
+
models: List[str] = []
|
|
444
|
+
for model in data["data"]:
|
|
445
|
+
name = model.get("id") or model.get("name")
|
|
446
|
+
if name:
|
|
447
|
+
models.append(name)
|
|
448
|
+
return models
|
|
449
|
+
else:
|
|
450
|
+
logger.error(
|
|
451
|
+
"Failed to fetch models: %s - %s",
|
|
452
|
+
response.status_code,
|
|
453
|
+
response.text,
|
|
454
|
+
)
|
|
455
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
456
|
+
logger.error("Error fetching Claude Code models: %s", exc)
|
|
457
|
+
return None
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def add_models_to_extra_config(models: List[str]) -> bool:
|
|
461
|
+
try:
|
|
462
|
+
# Filter to only latest haiku, sonnet, and opus models
|
|
463
|
+
filtered_models = filter_latest_claude_models(models)
|
|
464
|
+
|
|
465
|
+
# Start fresh - overwrite the file on every auth instead of loading existing
|
|
466
|
+
claude_models = {}
|
|
467
|
+
added = 0
|
|
468
|
+
access_token = get_valid_access_token() or ""
|
|
469
|
+
|
|
470
|
+
for model_name in filtered_models:
|
|
471
|
+
prefixed = f"{CLAUDE_CODE_OAUTH_CONFIG['prefix']}{model_name}"
|
|
472
|
+
claude_models[prefixed] = {
|
|
473
|
+
"type": "claude_code",
|
|
474
|
+
"name": model_name,
|
|
475
|
+
"custom_endpoint": {
|
|
476
|
+
"url": CLAUDE_CODE_OAUTH_CONFIG["api_base_url"],
|
|
477
|
+
"api_key": access_token,
|
|
478
|
+
"headers": {
|
|
479
|
+
"anthropic-beta": "oauth-2025-04-20,interleaved-thinking-2025-05-14",
|
|
480
|
+
"x-app": "cli",
|
|
481
|
+
"User-Agent": "claude-cli/2.0.61 (external, cli)",
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
"context_length": CLAUDE_CODE_OAUTH_CONFIG["default_context_length"],
|
|
485
|
+
"oauth_source": "claude-code-plugin",
|
|
486
|
+
"supported_settings": [
|
|
487
|
+
"temperature",
|
|
488
|
+
"extended_thinking",
|
|
489
|
+
"budget_tokens",
|
|
490
|
+
"interleaved_thinking",
|
|
491
|
+
],
|
|
492
|
+
}
|
|
493
|
+
added += 1
|
|
494
|
+
if save_claude_models(claude_models):
|
|
495
|
+
logger.info("Added %s Claude Code models", added)
|
|
496
|
+
return True
|
|
497
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
498
|
+
logger.error("Error adding models to config: %s", exc)
|
|
499
|
+
return False
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def remove_claude_code_models() -> int:
|
|
503
|
+
try:
|
|
504
|
+
claude_models = load_claude_models()
|
|
505
|
+
to_remove = [
|
|
506
|
+
name
|
|
507
|
+
for name, config in claude_models.items()
|
|
508
|
+
if config.get("oauth_source") == "claude-code-plugin"
|
|
509
|
+
]
|
|
510
|
+
if not to_remove:
|
|
511
|
+
return 0
|
|
512
|
+
for model_name in to_remove:
|
|
513
|
+
claude_models.pop(model_name, None)
|
|
514
|
+
if save_claude_models(claude_models):
|
|
515
|
+
return len(to_remove)
|
|
516
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
517
|
+
logger.error("Error removing Claude Code models: %s", exc)
|
|
518
|
+
return 0
|
|
File without changes
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
3
|
+
|
|
4
|
+
from code_puppy.callbacks import register_callback
|
|
5
|
+
from code_puppy.messaging import emit_error, emit_info
|
|
6
|
+
|
|
7
|
+
# Global cache for loaded commands
|
|
8
|
+
_custom_commands: Dict[str, str] = {}
|
|
9
|
+
_command_descriptions: Dict[str, str] = {}
|
|
10
|
+
|
|
11
|
+
# Directories to scan for commands
|
|
12
|
+
_COMMAND_DIRECTORIES = [".claude/commands", ".github/prompts", ".agents/commands"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MarkdownCommandResult:
|
|
16
|
+
"""Special marker for markdown command results that should be processed as input."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, content: str):
|
|
19
|
+
self.content = content
|
|
20
|
+
|
|
21
|
+
def __str__(self) -> str:
|
|
22
|
+
return self.content
|
|
23
|
+
|
|
24
|
+
def __repr__(self) -> str:
|
|
25
|
+
return f"MarkdownCommandResult({len(self.content)} chars)"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _load_markdown_commands() -> None:
|
|
29
|
+
"""Load markdown command files from the configured directories.
|
|
30
|
+
|
|
31
|
+
Scans for *.md files in the configured directories and loads them
|
|
32
|
+
as custom commands. Handles duplicates by appending numeric suffixes.
|
|
33
|
+
"""
|
|
34
|
+
global _custom_commands, _command_descriptions
|
|
35
|
+
|
|
36
|
+
_custom_commands.clear()
|
|
37
|
+
_command_descriptions.clear()
|
|
38
|
+
|
|
39
|
+
loaded_files = []
|
|
40
|
+
|
|
41
|
+
for directory in _COMMAND_DIRECTORIES:
|
|
42
|
+
dir_path = Path(directory).expanduser()
|
|
43
|
+
if not dir_path.exists():
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
# Look for markdown files
|
|
47
|
+
pattern = "*.md" if directory != ".github/prompts" else "*.prompt.md"
|
|
48
|
+
for md_file in dir_path.glob(pattern):
|
|
49
|
+
loaded_files.append(md_file)
|
|
50
|
+
|
|
51
|
+
# Sort for consistent ordering
|
|
52
|
+
loaded_files.sort()
|
|
53
|
+
|
|
54
|
+
for md_file in loaded_files:
|
|
55
|
+
try:
|
|
56
|
+
# Extract command name from filename
|
|
57
|
+
if md_file.name.endswith(".prompt.md"):
|
|
58
|
+
base_name = md_file.name[: -len(".prompt.md")]
|
|
59
|
+
else:
|
|
60
|
+
base_name = md_file.stem
|
|
61
|
+
|
|
62
|
+
# Generate unique command name
|
|
63
|
+
command_name = _generate_unique_command_name(base_name)
|
|
64
|
+
|
|
65
|
+
# Read file content
|
|
66
|
+
content = md_file.read_text(encoding="utf-8").strip()
|
|
67
|
+
if not content:
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
# Extract first line as description (or use filename)
|
|
71
|
+
lines = content.split("\n")
|
|
72
|
+
description = base_name.replace("_", " ").replace("-", " ").title()
|
|
73
|
+
|
|
74
|
+
# Try to get description from first non-empty line that's not a heading
|
|
75
|
+
for line in lines:
|
|
76
|
+
line = line.strip()
|
|
77
|
+
if line and not line.startswith("#"):
|
|
78
|
+
# Truncate long descriptions
|
|
79
|
+
description = line[:50] + ("..." if len(line) > 50 else "")
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
_custom_commands[command_name] = content
|
|
83
|
+
_command_descriptions[command_name] = description
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
emit_error(f"Failed to load command from {md_file}: {e}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _generate_unique_command_name(base_name: str) -> str:
|
|
90
|
+
"""Generate a unique command name, handling duplicates.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
base_name: The base command name from filename
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Unique command name (may have numeric suffix)
|
|
97
|
+
"""
|
|
98
|
+
if base_name not in _custom_commands:
|
|
99
|
+
return base_name
|
|
100
|
+
|
|
101
|
+
# Try numeric suffixes
|
|
102
|
+
counter = 2
|
|
103
|
+
while True:
|
|
104
|
+
candidate = f"{base_name}{counter}"
|
|
105
|
+
if candidate not in _custom_commands:
|
|
106
|
+
return candidate
|
|
107
|
+
counter += 1
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _custom_help() -> List[Tuple[str, str]]:
|
|
111
|
+
"""Return help entries for loaded markdown commands."""
|
|
112
|
+
# Reload commands to pick up any changes
|
|
113
|
+
_load_markdown_commands()
|
|
114
|
+
|
|
115
|
+
help_entries = []
|
|
116
|
+
for name, description in sorted(_command_descriptions.items()):
|
|
117
|
+
help_entries.append((name, f"Execute markdown command: {description}"))
|
|
118
|
+
|
|
119
|
+
return help_entries
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _handle_custom_command(command: str, name: str) -> Optional[Any]:
|
|
123
|
+
"""Handle a markdown-based custom command.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
command: The full command string
|
|
127
|
+
name: The command name without leading slash
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
MarkdownCommandResult with content to be processed as input,
|
|
131
|
+
or None if not found
|
|
132
|
+
"""
|
|
133
|
+
if not name:
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
# Ensure commands are loaded
|
|
137
|
+
if not _custom_commands:
|
|
138
|
+
_load_markdown_commands()
|
|
139
|
+
|
|
140
|
+
# Look up the command
|
|
141
|
+
content = _custom_commands.get(name)
|
|
142
|
+
if content is None:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
# Extract any additional arguments from the command
|
|
146
|
+
parts = command.split(maxsplit=1)
|
|
147
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
148
|
+
|
|
149
|
+
# If there are arguments, append them to the prompt
|
|
150
|
+
if args:
|
|
151
|
+
prompt = f"{content}\n\nAdditional context: {args}"
|
|
152
|
+
else:
|
|
153
|
+
prompt = content
|
|
154
|
+
|
|
155
|
+
# Emit info message and return the special marker
|
|
156
|
+
emit_info(f"📝 Executing markdown command: {name}")
|
|
157
|
+
return MarkdownCommandResult(prompt)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# Register callbacks
|
|
161
|
+
register_callback("custom_command_help", _custom_help)
|
|
162
|
+
register_callback("custom_command", _handle_custom_command)
|
|
163
|
+
|
|
164
|
+
# Make the result class available for the command handler
|
|
165
|
+
# Import this in command_handler.py to check for this type
|
|
166
|
+
__all__ = ["MarkdownCommandResult"]
|
|
167
|
+
|
|
168
|
+
# Load commands at import time
|
|
169
|
+
_load_markdown_commands()
|