code-puppy 0.0.214__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 +2 -0
- code_puppy/agents/agent_c_reviewer.py +59 -6
- code_puppy/agents/agent_code_puppy.py +7 -1
- code_puppy/agents/agent_code_reviewer.py +12 -2
- code_puppy/agents/agent_cpp_reviewer.py +73 -6
- code_puppy/agents/agent_creator_agent.py +45 -4
- code_puppy/agents/agent_golang_reviewer.py +92 -3
- code_puppy/agents/agent_javascript_reviewer.py +101 -8
- code_puppy/agents/agent_manager.py +81 -4
- 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 +28 -6
- code_puppy/agents/agent_qa_expert.py +98 -6
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_security_auditor.py +113 -3
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +106 -7
- code_puppy/agents/base_agent.py +802 -176
- code_puppy/agents/event_stream_handler.py +350 -0
- 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 +142 -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 +10 -5
- 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 +176 -738
- 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 +0 -3
- 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 +15 -26
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +2 -2
- 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 +16 -10
- code_puppy/command_line/mcp/start_all_command.py +18 -6
- code_puppy/command_line/mcp/start_command.py +47 -25
- code_puppy/command_line/mcp/status_command.py +4 -5
- code_puppy/command_line/mcp/stop_all_command.py +7 -1
- code_puppy/command_line/mcp/stop_command.py +8 -4
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/wizard_utils.py +20 -16
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +75 -25
- 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 +463 -63
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +898 -112
- 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 +210 -148
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -698
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/blocking_startup.py +70 -43
- code_puppy/mcp_/captured_stdio_server.py +2 -2
- code_puppy/mcp_/config_wizard.py +4 -4
- code_puppy/mcp_/dashboard.py +15 -6
- code_puppy/mcp_/managed_server.py +65 -38
- code_puppy/mcp_/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +6 -6
- code_puppy/mcp_/server_registry_catalog.py +24 -5
- 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 +21 -5
- code_puppy/messaging/spinner/console_spinner.py +86 -51
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +634 -83
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +66 -68
- 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 +2 -2
- 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 +9 -12
- code_puppy/session_storage.py +2 -1
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +41 -13
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +536 -52
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +19 -23
- code_puppy/tools/browser/browser_interactions.py +41 -48
- code_puppy/tools/browser/browser_locators.py +36 -38
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +16 -16
- code_puppy/tools/browser/browser_screenshot.py +79 -143
- code_puppy/tools/browser/browser_scripts.py +32 -42
- code_puppy/tools/browser/browser_workflows.py +44 -27
- 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 +930 -147
- code_puppy/tools/common.py +1113 -5
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +226 -154
- 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.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/messaging/spinner/textual_spinner.py +0 -106
- code_puppy/tools/browser/camoufox_manager.py +0 -216
- code_puppy/tools/browser/vqa_agent.py +0 -70
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -1105
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -551
- 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 -185
- 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 -17
- code_puppy/tui/screens/autosave_picker.py +0 -175
- 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 -306
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy/tui_state.py +0 -55
- code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
- code_puppy-0.0.214.dist-info/RECORD +0 -131
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
code_puppy/config.py
CHANGED
|
@@ -7,17 +7,59 @@ from typing import Optional
|
|
|
7
7
|
|
|
8
8
|
from code_puppy.session_storage import save_session
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
def _get_xdg_dir(env_var: str, fallback: str) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Get directory for code_puppy files, defaulting to ~/.code_puppy.
|
|
14
|
+
|
|
15
|
+
XDG paths are only used when the corresponding environment variable
|
|
16
|
+
is explicitly set by the user. Otherwise, we use the legacy ~/.code_puppy
|
|
17
|
+
directory for all file types (config, data, cache, state).
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
env_var: XDG environment variable name (e.g., "XDG_CONFIG_HOME")
|
|
21
|
+
fallback: Fallback path relative to home (e.g., ".config") - unused unless XDG var is set
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Path to the directory for code_puppy files
|
|
25
|
+
"""
|
|
26
|
+
# Use XDG directory ONLY if environment variable is explicitly set
|
|
27
|
+
xdg_base = os.getenv(env_var)
|
|
28
|
+
if xdg_base:
|
|
29
|
+
return os.path.join(xdg_base, "code_puppy")
|
|
30
|
+
|
|
31
|
+
# Default to legacy ~/.code_puppy for all file types
|
|
32
|
+
return os.path.join(os.path.expanduser("~"), ".code_puppy")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# XDG Base Directory paths
|
|
36
|
+
CONFIG_DIR = _get_xdg_dir("XDG_CONFIG_HOME", ".config")
|
|
37
|
+
DATA_DIR = _get_xdg_dir("XDG_DATA_HOME", ".local/share")
|
|
38
|
+
CACHE_DIR = _get_xdg_dir("XDG_CACHE_HOME", ".cache")
|
|
39
|
+
STATE_DIR = _get_xdg_dir("XDG_STATE_HOME", ".local/state")
|
|
40
|
+
|
|
41
|
+
# Configuration files (XDG_CONFIG_HOME)
|
|
11
42
|
CONFIG_FILE = os.path.join(CONFIG_DIR, "puppy.cfg")
|
|
12
43
|
MCP_SERVERS_FILE = os.path.join(CONFIG_DIR, "mcp_servers.json")
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
44
|
+
|
|
45
|
+
# Data files (XDG_DATA_HOME)
|
|
46
|
+
MODELS_FILE = os.path.join(DATA_DIR, "models.json")
|
|
47
|
+
EXTRA_MODELS_FILE = os.path.join(DATA_DIR, "extra_models.json")
|
|
48
|
+
AGENTS_DIR = os.path.join(DATA_DIR, "agents")
|
|
49
|
+
CONTEXTS_DIR = os.path.join(DATA_DIR, "contexts")
|
|
50
|
+
_DEFAULT_SQLITE_FILE = os.path.join(DATA_DIR, "dbos_store.sqlite")
|
|
51
|
+
|
|
52
|
+
# OAuth plugin model files (XDG_DATA_HOME)
|
|
53
|
+
GEMINI_MODELS_FILE = os.path.join(DATA_DIR, "gemini_models.json")
|
|
54
|
+
CHATGPT_MODELS_FILE = os.path.join(DATA_DIR, "chatgpt_models.json")
|
|
55
|
+
CLAUDE_MODELS_FILE = os.path.join(DATA_DIR, "claude_models.json")
|
|
56
|
+
ANTIGRAVITY_MODELS_FILE = os.path.join(DATA_DIR, "antigravity_models.json")
|
|
57
|
+
|
|
58
|
+
# Cache files (XDG_CACHE_HOME)
|
|
59
|
+
AUTOSAVE_DIR = os.path.join(CACHE_DIR, "autosaves")
|
|
60
|
+
|
|
61
|
+
# State files (XDG_STATE_HOME)
|
|
62
|
+
COMMAND_HISTORY_FILE = os.path.join(STATE_DIR, "command_history.txt")
|
|
21
63
|
DBOS_DATABASE_URL = os.environ.get(
|
|
22
64
|
"DBOS_SYSTEM_DATABASE_URL", f"sqlite:///{_DEFAULT_SQLITE_FILE}"
|
|
23
65
|
)
|
|
@@ -33,6 +75,48 @@ def get_use_dbos() -> bool:
|
|
|
33
75
|
return str(cfg_val).strip().lower() in {"1", "true", "yes", "on"}
|
|
34
76
|
|
|
35
77
|
|
|
78
|
+
def get_subagent_verbose() -> bool:
|
|
79
|
+
"""Return True if sub-agent verbose output is enabled (default False).
|
|
80
|
+
|
|
81
|
+
When False (default), sub-agents produce quiet, sparse output suitable
|
|
82
|
+
for parallel execution. When True, sub-agents produce full verbose output
|
|
83
|
+
like the main agent (useful for debugging).
|
|
84
|
+
"""
|
|
85
|
+
cfg_val = get_value("subagent_verbose")
|
|
86
|
+
if cfg_val is None:
|
|
87
|
+
return False
|
|
88
|
+
return str(cfg_val).strip().lower() in {"1", "true", "yes", "on"}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Pack agents - the specialized sub-agents coordinated by Pack Leader
|
|
92
|
+
PACK_AGENT_NAMES = frozenset(
|
|
93
|
+
[
|
|
94
|
+
"pack-leader",
|
|
95
|
+
"bloodhound",
|
|
96
|
+
"husky",
|
|
97
|
+
"shepherd",
|
|
98
|
+
"terrier",
|
|
99
|
+
"watchdog",
|
|
100
|
+
"retriever",
|
|
101
|
+
]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_pack_agents_enabled() -> bool:
|
|
106
|
+
"""Return True if pack agents are enabled (default False).
|
|
107
|
+
|
|
108
|
+
When False (default), pack agents (pack-leader, bloodhound, husky, shepherd,
|
|
109
|
+
terrier, watchdog, retriever) are hidden from `list_agents` tool and `/agents`
|
|
110
|
+
command. They cannot be invoked by other agents or selected by users.
|
|
111
|
+
|
|
112
|
+
When True, pack agents are available for use.
|
|
113
|
+
"""
|
|
114
|
+
cfg_val = get_value("enable_pack_agents")
|
|
115
|
+
if cfg_val is None:
|
|
116
|
+
return False
|
|
117
|
+
return str(cfg_val).strip().lower() in {"1", "true", "yes", "on"}
|
|
118
|
+
|
|
119
|
+
|
|
36
120
|
DEFAULT_SECTION = "puppy"
|
|
37
121
|
REQUIRED_KEYS = ["puppy_name", "owner_name"]
|
|
38
122
|
|
|
@@ -43,16 +127,17 @@ _CURRENT_AUTOSAVE_ID: Optional[str] = None
|
|
|
43
127
|
_model_validation_cache = {}
|
|
44
128
|
_default_model_cache = None
|
|
45
129
|
_default_vision_model_cache = None
|
|
46
|
-
_default_vqa_model_cache = None
|
|
47
130
|
|
|
48
131
|
|
|
49
132
|
def ensure_config_exists():
|
|
50
133
|
"""
|
|
51
|
-
Ensure that
|
|
134
|
+
Ensure that XDG directories and puppy.cfg exist, prompting if needed.
|
|
52
135
|
Returns configparser.ConfigParser for reading.
|
|
53
136
|
"""
|
|
54
|
-
|
|
55
|
-
|
|
137
|
+
# Create all XDG directories with 0700 permissions per XDG spec
|
|
138
|
+
for directory in [CONFIG_DIR, DATA_DIR, CACHE_DIR, STATE_DIR]:
|
|
139
|
+
if not os.path.exists(directory):
|
|
140
|
+
os.makedirs(directory, mode=0o700, exist_ok=True)
|
|
56
141
|
exists = os.path.isfile(CONFIG_FILE)
|
|
57
142
|
config = configparser.ConfigParser()
|
|
58
143
|
if exists:
|
|
@@ -64,7 +149,11 @@ def ensure_config_exists():
|
|
|
64
149
|
if not config[DEFAULT_SECTION].get(key):
|
|
65
150
|
missing.append(key)
|
|
66
151
|
if missing:
|
|
67
|
-
|
|
152
|
+
# Note: Using sys.stdout here for initial setup before messaging system is available
|
|
153
|
+
import sys
|
|
154
|
+
|
|
155
|
+
sys.stdout.write("🐾 Let's get your Puppy ready!\n")
|
|
156
|
+
sys.stdout.flush()
|
|
68
157
|
for key in missing:
|
|
69
158
|
if key == "puppy_name":
|
|
70
159
|
val = input("What should we name the puppy? ").strip()
|
|
@@ -75,6 +164,13 @@ def ensure_config_exists():
|
|
|
75
164
|
else:
|
|
76
165
|
val = input(f"Enter {key}: ").strip()
|
|
77
166
|
config[DEFAULT_SECTION][key] = val
|
|
167
|
+
|
|
168
|
+
# Set default values for important config keys if they don't exist
|
|
169
|
+
if not config[DEFAULT_SECTION].get("auto_save_session"):
|
|
170
|
+
config[DEFAULT_SECTION]["auto_save_session"] = "true"
|
|
171
|
+
|
|
172
|
+
# Write the config if we made any changes
|
|
173
|
+
if missing or not exists:
|
|
78
174
|
with open(CONFIG_FILE, "w") as f:
|
|
79
175
|
config.write(f)
|
|
80
176
|
return config
|
|
@@ -146,12 +242,26 @@ def get_config_keys():
|
|
|
146
242
|
"message_limit",
|
|
147
243
|
"allow_recursion",
|
|
148
244
|
"openai_reasoning_effort",
|
|
245
|
+
"openai_verbosity",
|
|
149
246
|
"auto_save_session",
|
|
150
247
|
"max_saved_sessions",
|
|
151
248
|
"http2",
|
|
249
|
+
"diff_context_lines",
|
|
250
|
+
"default_agent",
|
|
251
|
+
"temperature",
|
|
252
|
+
"frontend_emitter_enabled",
|
|
253
|
+
"frontend_emitter_max_recent_events",
|
|
254
|
+
"frontend_emitter_queue_size",
|
|
152
255
|
]
|
|
153
256
|
# Add DBOS control key
|
|
154
257
|
default_keys.append("enable_dbos")
|
|
258
|
+
# Add pack agents control key
|
|
259
|
+
default_keys.append("enable_pack_agents")
|
|
260
|
+
# Add cancel agent key configuration
|
|
261
|
+
default_keys.append("cancel_agent_key")
|
|
262
|
+
# Add banner color keys
|
|
263
|
+
for banner_name in DEFAULT_BANNER_COLORS:
|
|
264
|
+
default_keys.append(f"banner_color_{banner_name}")
|
|
155
265
|
|
|
156
266
|
config = configparser.ConfigParser()
|
|
157
267
|
config.read(CONFIG_FILE)
|
|
@@ -173,18 +283,33 @@ def set_config_value(key: str, value: str):
|
|
|
173
283
|
config.write(f)
|
|
174
284
|
|
|
175
285
|
|
|
286
|
+
# Alias for API compatibility
|
|
287
|
+
def set_value(key: str, value: str) -> None:
|
|
288
|
+
"""Set a config value. Alias for set_config_value."""
|
|
289
|
+
set_config_value(key, value)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def reset_value(key: str) -> None:
|
|
293
|
+
"""Remove a key from the config file, resetting it to default."""
|
|
294
|
+
config = configparser.ConfigParser()
|
|
295
|
+
config.read(CONFIG_FILE)
|
|
296
|
+
if DEFAULT_SECTION in config and key in config[DEFAULT_SECTION]:
|
|
297
|
+
del config[DEFAULT_SECTION][key]
|
|
298
|
+
with open(CONFIG_FILE, "w") as f:
|
|
299
|
+
config.write(f)
|
|
300
|
+
|
|
301
|
+
|
|
176
302
|
# --- MODEL STICKY EXTENSION STARTS HERE ---
|
|
177
303
|
def load_mcp_server_configs():
|
|
178
304
|
"""
|
|
179
|
-
Loads the MCP server configurations from
|
|
305
|
+
Loads the MCP server configurations from XDG_CONFIG_HOME/code_puppy/mcp_servers.json.
|
|
180
306
|
Returns a dict mapping names to their URL or config dict.
|
|
181
307
|
If file does not exist, returns an empty dict.
|
|
182
308
|
"""
|
|
183
|
-
from code_puppy.messaging.message_queue import emit_error
|
|
309
|
+
from code_puppy.messaging.message_queue import emit_error
|
|
184
310
|
|
|
185
311
|
try:
|
|
186
312
|
if not pathlib.Path(MCP_SERVERS_FILE).exists():
|
|
187
|
-
emit_system_message("[dim]No MCP configuration was found[/dim]")
|
|
188
313
|
return {}
|
|
189
314
|
with open(MCP_SERVERS_FILE, "r") as f:
|
|
190
315
|
conf = json.loads(f.read())
|
|
@@ -195,10 +320,10 @@ def load_mcp_server_configs():
|
|
|
195
320
|
|
|
196
321
|
|
|
197
322
|
def _default_model_from_models_json():
|
|
198
|
-
"""
|
|
323
|
+
"""Load the default model name from models.json.
|
|
199
324
|
|
|
200
|
-
|
|
201
|
-
|
|
325
|
+
Returns the first model in models.json as the default.
|
|
326
|
+
Falls back to ``gpt-5`` if the file cannot be read.
|
|
202
327
|
"""
|
|
203
328
|
global _default_model_cache
|
|
204
329
|
|
|
@@ -210,6 +335,7 @@ def _default_model_from_models_json():
|
|
|
210
335
|
|
|
211
336
|
models_config = ModelFactory.load_config()
|
|
212
337
|
if models_config:
|
|
338
|
+
# Use first model in models.json as default
|
|
213
339
|
first_key = next(iter(models_config))
|
|
214
340
|
_default_model_cache = first_key
|
|
215
341
|
return first_key
|
|
@@ -262,47 +388,6 @@ def _default_vision_model_from_models_json() -> str:
|
|
|
262
388
|
return "gpt-4.1"
|
|
263
389
|
|
|
264
390
|
|
|
265
|
-
def _default_vqa_model_from_models_json() -> str:
|
|
266
|
-
"""Select a default VQA-capable model, preferring vision-ready options."""
|
|
267
|
-
global _default_vqa_model_cache
|
|
268
|
-
|
|
269
|
-
if _default_vqa_model_cache is not None:
|
|
270
|
-
return _default_vqa_model_cache
|
|
271
|
-
|
|
272
|
-
try:
|
|
273
|
-
from code_puppy.model_factory import ModelFactory
|
|
274
|
-
|
|
275
|
-
models_config = ModelFactory.load_config()
|
|
276
|
-
if models_config:
|
|
277
|
-
# Allow explicit VQA hints if present
|
|
278
|
-
for name, config in models_config.items():
|
|
279
|
-
if config.get("supports_vqa"):
|
|
280
|
-
_default_vqa_model_cache = name
|
|
281
|
-
return name
|
|
282
|
-
|
|
283
|
-
# Reuse multimodal heuristics before falling back to generic default
|
|
284
|
-
preferred_candidates = (
|
|
285
|
-
"gpt-4.1",
|
|
286
|
-
"gpt-4.1-mini",
|
|
287
|
-
"claude-4-0-sonnet",
|
|
288
|
-
"gemini-2.5-flash-preview-05-20",
|
|
289
|
-
"gpt-4.1-nano",
|
|
290
|
-
)
|
|
291
|
-
for candidate in preferred_candidates:
|
|
292
|
-
if candidate in models_config:
|
|
293
|
-
_default_vqa_model_cache = candidate
|
|
294
|
-
return candidate
|
|
295
|
-
|
|
296
|
-
_default_vqa_model_cache = _default_model_from_models_json()
|
|
297
|
-
return _default_vqa_model_cache
|
|
298
|
-
|
|
299
|
-
_default_vqa_model_cache = "gpt-4.1"
|
|
300
|
-
return "gpt-4.1"
|
|
301
|
-
except Exception:
|
|
302
|
-
_default_vqa_model_cache = "gpt-4.1"
|
|
303
|
-
return "gpt-4.1"
|
|
304
|
-
|
|
305
|
-
|
|
306
391
|
def _validate_model_exists(model_name: str) -> bool:
|
|
307
392
|
"""Check if a model exists in models.json with caching to avoid redundant calls."""
|
|
308
393
|
global _model_validation_cache
|
|
@@ -328,15 +413,47 @@ def _validate_model_exists(model_name: str) -> bool:
|
|
|
328
413
|
|
|
329
414
|
def clear_model_cache():
|
|
330
415
|
"""Clear the model validation cache. Call this when models.json changes."""
|
|
331
|
-
global
|
|
332
|
-
_model_validation_cache, \
|
|
333
|
-
_default_model_cache, \
|
|
334
|
-
_default_vision_model_cache, \
|
|
335
|
-
_default_vqa_model_cache
|
|
416
|
+
global _model_validation_cache, _default_model_cache, _default_vision_model_cache
|
|
336
417
|
_model_validation_cache.clear()
|
|
337
418
|
_default_model_cache = None
|
|
338
419
|
_default_vision_model_cache = None
|
|
339
|
-
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def model_supports_setting(model_name: str, setting: str) -> bool:
|
|
423
|
+
"""Check if a model supports a particular setting (e.g., 'temperature', 'seed').
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
model_name: The name of the model to check.
|
|
427
|
+
setting: The setting name to check for (e.g., 'temperature', 'seed', 'top_p').
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
True if the model supports the setting, False otherwise.
|
|
431
|
+
Defaults to True for backwards compatibility if model config doesn't specify.
|
|
432
|
+
"""
|
|
433
|
+
# GLM-4.7 models always support clear_thinking setting
|
|
434
|
+
if setting == "clear_thinking" and "glm-4.7" in model_name.lower():
|
|
435
|
+
return True
|
|
436
|
+
|
|
437
|
+
try:
|
|
438
|
+
from code_puppy.model_factory import ModelFactory
|
|
439
|
+
|
|
440
|
+
models_config = ModelFactory.load_config()
|
|
441
|
+
model_config = models_config.get(model_name, {})
|
|
442
|
+
|
|
443
|
+
# Get supported_settings list, default to supporting common settings
|
|
444
|
+
supported_settings = model_config.get("supported_settings")
|
|
445
|
+
|
|
446
|
+
if supported_settings is None:
|
|
447
|
+
# Default: assume common settings are supported for backwards compatibility
|
|
448
|
+
# For Anthropic/Claude models, include extended thinking settings
|
|
449
|
+
if model_name.startswith("claude-") or model_name.startswith("anthropic-"):
|
|
450
|
+
return setting in ["temperature", "extended_thinking", "budget_tokens"]
|
|
451
|
+
return setting in ["temperature", "seed"]
|
|
452
|
+
|
|
453
|
+
return setting in supported_settings
|
|
454
|
+
except Exception:
|
|
455
|
+
# If we can't check, assume supported for safety
|
|
456
|
+
return True
|
|
340
457
|
|
|
341
458
|
|
|
342
459
|
def get_global_model_name():
|
|
@@ -374,20 +491,6 @@ def set_model_name(model: str):
|
|
|
374
491
|
clear_model_cache()
|
|
375
492
|
|
|
376
493
|
|
|
377
|
-
def get_vqa_model_name() -> str:
|
|
378
|
-
"""Return the configured VQA model, falling back to an inferred default."""
|
|
379
|
-
stored_model = get_value("vqa_model_name")
|
|
380
|
-
if stored_model and _validate_model_exists(stored_model):
|
|
381
|
-
return stored_model
|
|
382
|
-
return _default_vqa_model_from_models_json()
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
def set_vqa_model_name(model: str):
|
|
386
|
-
"""Persist the configured VQA model name and refresh caches."""
|
|
387
|
-
set_config_value("vqa_model_name", model or "")
|
|
388
|
-
clear_model_cache()
|
|
389
|
-
|
|
390
|
-
|
|
391
494
|
def get_puppy_token():
|
|
392
495
|
"""Returns the puppy_token from config, or None if not set."""
|
|
393
496
|
return get_value("puppy_token")
|
|
@@ -399,8 +502,8 @@ def set_puppy_token(token: str):
|
|
|
399
502
|
|
|
400
503
|
|
|
401
504
|
def get_openai_reasoning_effort() -> str:
|
|
402
|
-
"""Return the configured OpenAI reasoning effort (low, medium, high)."""
|
|
403
|
-
allowed_values = {"low", "medium", "high"}
|
|
505
|
+
"""Return the configured OpenAI reasoning effort (minimal, low, medium, high, xhigh)."""
|
|
506
|
+
allowed_values = {"minimal", "low", "medium", "high", "xhigh"}
|
|
404
507
|
configured = (get_value("openai_reasoning_effort") or "medium").strip().lower()
|
|
405
508
|
if configured not in allowed_values:
|
|
406
509
|
return "medium"
|
|
@@ -409,7 +512,7 @@ def get_openai_reasoning_effort() -> str:
|
|
|
409
512
|
|
|
410
513
|
def set_openai_reasoning_effort(value: str) -> None:
|
|
411
514
|
"""Persist the OpenAI reasoning effort ensuring it remains within allowed values."""
|
|
412
|
-
allowed_values = {"low", "medium", "high"}
|
|
515
|
+
allowed_values = {"minimal", "low", "medium", "high", "xhigh"}
|
|
413
516
|
normalized = (value or "").strip().lower()
|
|
414
517
|
if normalized not in allowed_values:
|
|
415
518
|
raise ValueError(
|
|
@@ -418,6 +521,276 @@ def set_openai_reasoning_effort(value: str) -> None:
|
|
|
418
521
|
set_config_value("openai_reasoning_effort", normalized)
|
|
419
522
|
|
|
420
523
|
|
|
524
|
+
def get_openai_verbosity() -> str:
|
|
525
|
+
"""Return the configured OpenAI verbosity (low, medium, high).
|
|
526
|
+
|
|
527
|
+
Controls how concise vs. verbose the model's responses are:
|
|
528
|
+
- low: more concise responses
|
|
529
|
+
- medium: balanced (default)
|
|
530
|
+
- high: more verbose responses
|
|
531
|
+
"""
|
|
532
|
+
allowed_values = {"low", "medium", "high"}
|
|
533
|
+
configured = (get_value("openai_verbosity") or "medium").strip().lower()
|
|
534
|
+
if configured not in allowed_values:
|
|
535
|
+
return "medium"
|
|
536
|
+
return configured
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def set_openai_verbosity(value: str) -> None:
|
|
540
|
+
"""Persist the OpenAI verbosity ensuring it remains within allowed values."""
|
|
541
|
+
allowed_values = {"low", "medium", "high"}
|
|
542
|
+
normalized = (value or "").strip().lower()
|
|
543
|
+
if normalized not in allowed_values:
|
|
544
|
+
raise ValueError(
|
|
545
|
+
f"Invalid verbosity '{value}'. Allowed: {', '.join(sorted(allowed_values))}"
|
|
546
|
+
)
|
|
547
|
+
set_config_value("openai_verbosity", normalized)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def get_temperature() -> Optional[float]:
|
|
551
|
+
"""Return the configured model temperature (0.0 to 2.0).
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
Float between 0.0 and 2.0 if set, None if not configured.
|
|
555
|
+
This allows each model to use its own default when not overridden.
|
|
556
|
+
"""
|
|
557
|
+
val = get_value("temperature")
|
|
558
|
+
if val is None or val.strip() == "":
|
|
559
|
+
return None
|
|
560
|
+
try:
|
|
561
|
+
temp = float(val)
|
|
562
|
+
# Clamp to valid range (most APIs accept 0-2)
|
|
563
|
+
return max(0.0, min(2.0, temp))
|
|
564
|
+
except (ValueError, TypeError):
|
|
565
|
+
return None
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def set_temperature(value: Optional[float]) -> None:
|
|
569
|
+
"""Set the global model temperature in config.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
value: Temperature between 0.0 and 2.0, or None to clear.
|
|
573
|
+
Lower values = more deterministic, higher = more creative.
|
|
574
|
+
|
|
575
|
+
Note: Consider using set_model_setting() for per-model temperature.
|
|
576
|
+
"""
|
|
577
|
+
if value is None:
|
|
578
|
+
set_config_value("temperature", "")
|
|
579
|
+
else:
|
|
580
|
+
# Validate and clamp
|
|
581
|
+
temp = max(0.0, min(2.0, float(value)))
|
|
582
|
+
set_config_value("temperature", str(temp))
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
# --- PER-MODEL SETTINGS ---
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _sanitize_model_name_for_key(model_name: str) -> str:
|
|
589
|
+
"""Sanitize model name for use in config keys.
|
|
590
|
+
|
|
591
|
+
Replaces characters that might cause issues in config keys.
|
|
592
|
+
"""
|
|
593
|
+
# Replace problematic characters with underscores
|
|
594
|
+
sanitized = model_name.replace(".", "_").replace("-", "_").replace("/", "_")
|
|
595
|
+
return sanitized.lower()
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def get_model_setting(
|
|
599
|
+
model_name: str, setting: str, default: Optional[float] = None
|
|
600
|
+
) -> Optional[float]:
|
|
601
|
+
"""Get a specific setting for a model.
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
model_name: The model name (e.g., 'gpt-5', 'claude-4-5-sonnet')
|
|
605
|
+
setting: The setting name (e.g., 'temperature', 'top_p', 'seed')
|
|
606
|
+
default: Default value if not set
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
The setting value as a float, or default if not set.
|
|
610
|
+
"""
|
|
611
|
+
sanitized_name = _sanitize_model_name_for_key(model_name)
|
|
612
|
+
key = f"model_settings_{sanitized_name}_{setting}"
|
|
613
|
+
val = get_value(key)
|
|
614
|
+
|
|
615
|
+
if val is None or val.strip() == "":
|
|
616
|
+
return default
|
|
617
|
+
|
|
618
|
+
try:
|
|
619
|
+
return float(val)
|
|
620
|
+
except (ValueError, TypeError):
|
|
621
|
+
return default
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def set_model_setting(model_name: str, setting: str, value: Optional[float]) -> None:
|
|
625
|
+
"""Set a specific setting for a model.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
model_name: The model name (e.g., 'gpt-5', 'claude-4-5-sonnet')
|
|
629
|
+
setting: The setting name (e.g., 'temperature', 'seed')
|
|
630
|
+
value: The value to set, or None to clear
|
|
631
|
+
"""
|
|
632
|
+
sanitized_name = _sanitize_model_name_for_key(model_name)
|
|
633
|
+
key = f"model_settings_{sanitized_name}_{setting}"
|
|
634
|
+
|
|
635
|
+
if value is None:
|
|
636
|
+
set_config_value(key, "")
|
|
637
|
+
elif isinstance(value, float):
|
|
638
|
+
# Round floats to nearest tenth to avoid floating point weirdness
|
|
639
|
+
set_config_value(key, str(round(value, 1)))
|
|
640
|
+
else:
|
|
641
|
+
set_config_value(key, str(value))
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def get_all_model_settings(model_name: str) -> dict:
|
|
645
|
+
"""Get all settings for a specific model.
|
|
646
|
+
|
|
647
|
+
Args:
|
|
648
|
+
model_name: The model name
|
|
649
|
+
|
|
650
|
+
Returns:
|
|
651
|
+
Dictionary of setting_name -> value for all configured settings.
|
|
652
|
+
"""
|
|
653
|
+
import configparser
|
|
654
|
+
|
|
655
|
+
sanitized_name = _sanitize_model_name_for_key(model_name)
|
|
656
|
+
prefix = f"model_settings_{sanitized_name}_"
|
|
657
|
+
|
|
658
|
+
config = configparser.ConfigParser()
|
|
659
|
+
config.read(CONFIG_FILE)
|
|
660
|
+
|
|
661
|
+
settings = {}
|
|
662
|
+
if DEFAULT_SECTION in config:
|
|
663
|
+
for key, val in config[DEFAULT_SECTION].items():
|
|
664
|
+
if key.startswith(prefix) and val.strip():
|
|
665
|
+
setting_name = key[len(prefix) :]
|
|
666
|
+
# Handle different value types
|
|
667
|
+
val_stripped = val.strip()
|
|
668
|
+
# Check for boolean values first
|
|
669
|
+
if val_stripped.lower() in ("true", "false"):
|
|
670
|
+
settings[setting_name] = val_stripped.lower() == "true"
|
|
671
|
+
else:
|
|
672
|
+
# Try to parse as number (int first, then float)
|
|
673
|
+
try:
|
|
674
|
+
# Try int first for cleaner values like budget_tokens
|
|
675
|
+
if "." not in val_stripped:
|
|
676
|
+
settings[setting_name] = int(val_stripped)
|
|
677
|
+
else:
|
|
678
|
+
settings[setting_name] = float(val_stripped)
|
|
679
|
+
except (ValueError, TypeError):
|
|
680
|
+
# Keep as string if not a number
|
|
681
|
+
settings[setting_name] = val_stripped
|
|
682
|
+
|
|
683
|
+
return settings
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def clear_model_settings(model_name: str) -> None:
|
|
687
|
+
"""Clear all settings for a specific model.
|
|
688
|
+
|
|
689
|
+
Args:
|
|
690
|
+
model_name: The model name
|
|
691
|
+
"""
|
|
692
|
+
import configparser
|
|
693
|
+
|
|
694
|
+
sanitized_name = _sanitize_model_name_for_key(model_name)
|
|
695
|
+
prefix = f"model_settings_{sanitized_name}_"
|
|
696
|
+
|
|
697
|
+
config = configparser.ConfigParser()
|
|
698
|
+
config.read(CONFIG_FILE)
|
|
699
|
+
|
|
700
|
+
if DEFAULT_SECTION in config:
|
|
701
|
+
keys_to_remove = [
|
|
702
|
+
key for key in config[DEFAULT_SECTION] if key.startswith(prefix)
|
|
703
|
+
]
|
|
704
|
+
for key in keys_to_remove:
|
|
705
|
+
del config[DEFAULT_SECTION][key]
|
|
706
|
+
|
|
707
|
+
with open(CONFIG_FILE, "w") as f:
|
|
708
|
+
config.write(f)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def get_effective_model_settings(model_name: Optional[str] = None) -> dict:
|
|
712
|
+
"""Get all effective settings for a model, filtered by what the model supports.
|
|
713
|
+
|
|
714
|
+
This is the generalized way to get model settings. It:
|
|
715
|
+
1. Gets all per-model settings from config
|
|
716
|
+
2. Falls back to global temperature if not set per-model
|
|
717
|
+
3. Filters to only include settings the model actually supports
|
|
718
|
+
4. Converts seed to int (other settings stay as float)
|
|
719
|
+
|
|
720
|
+
Args:
|
|
721
|
+
model_name: The model name. If None, uses the current global model.
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
Dictionary of setting_name -> value for all applicable settings.
|
|
725
|
+
Ready to be unpacked into ModelSettings.
|
|
726
|
+
"""
|
|
727
|
+
if model_name is None:
|
|
728
|
+
model_name = get_global_model_name()
|
|
729
|
+
|
|
730
|
+
# Start with all per-model settings
|
|
731
|
+
settings = get_all_model_settings(model_name)
|
|
732
|
+
|
|
733
|
+
# Fall back to global temperature if not set per-model
|
|
734
|
+
if "temperature" not in settings:
|
|
735
|
+
global_temp = get_temperature()
|
|
736
|
+
if global_temp is not None:
|
|
737
|
+
settings["temperature"] = global_temp
|
|
738
|
+
|
|
739
|
+
# Filter to only settings the model supports
|
|
740
|
+
effective_settings = {}
|
|
741
|
+
for setting_name, value in settings.items():
|
|
742
|
+
if model_supports_setting(model_name, setting_name):
|
|
743
|
+
# Convert seed to int, keep others as float
|
|
744
|
+
if setting_name == "seed" and value is not None:
|
|
745
|
+
effective_settings[setting_name] = int(value)
|
|
746
|
+
else:
|
|
747
|
+
effective_settings[setting_name] = value
|
|
748
|
+
|
|
749
|
+
return effective_settings
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
# Legacy functions for backward compatibility
|
|
753
|
+
def get_effective_temperature(model_name: Optional[str] = None) -> Optional[float]:
|
|
754
|
+
"""Get the effective temperature for a model.
|
|
755
|
+
|
|
756
|
+
Checks per-model settings first, then falls back to global temperature.
|
|
757
|
+
|
|
758
|
+
Args:
|
|
759
|
+
model_name: The model name. If None, uses the current global model.
|
|
760
|
+
|
|
761
|
+
Returns:
|
|
762
|
+
Temperature value, or None if not configured.
|
|
763
|
+
"""
|
|
764
|
+
settings = get_effective_model_settings(model_name)
|
|
765
|
+
return settings.get("temperature")
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def get_effective_top_p(model_name: Optional[str] = None) -> Optional[float]:
|
|
769
|
+
"""Get the effective top_p for a model.
|
|
770
|
+
|
|
771
|
+
Args:
|
|
772
|
+
model_name: The model name. If None, uses the current global model.
|
|
773
|
+
|
|
774
|
+
Returns:
|
|
775
|
+
top_p value, or None if not configured.
|
|
776
|
+
"""
|
|
777
|
+
settings = get_effective_model_settings(model_name)
|
|
778
|
+
return settings.get("top_p")
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def get_effective_seed(model_name: Optional[str] = None) -> Optional[int]:
|
|
782
|
+
"""Get the effective seed for a model.
|
|
783
|
+
|
|
784
|
+
Args:
|
|
785
|
+
model_name: The model name. If None, uses the current global model.
|
|
786
|
+
|
|
787
|
+
Returns:
|
|
788
|
+
seed value as int, or None if not configured.
|
|
789
|
+
"""
|
|
790
|
+
settings = get_effective_model_settings(model_name)
|
|
791
|
+
return settings.get("seed")
|
|
792
|
+
|
|
793
|
+
|
|
421
794
|
def normalize_command_history():
|
|
422
795
|
"""
|
|
423
796
|
Normalize the command history file by converting old format timestamps to the new format.
|
|
@@ -443,10 +816,20 @@ def normalize_command_history():
|
|
|
443
816
|
return
|
|
444
817
|
|
|
445
818
|
try:
|
|
446
|
-
# Read the entire file
|
|
447
|
-
with open(
|
|
819
|
+
# Read the entire file with encoding error handling for Windows
|
|
820
|
+
with open(
|
|
821
|
+
COMMAND_HISTORY_FILE, "r", encoding="utf-8", errors="surrogateescape"
|
|
822
|
+
) as f:
|
|
448
823
|
content = f.read()
|
|
449
824
|
|
|
825
|
+
# Sanitize any surrogate characters that might have slipped in
|
|
826
|
+
try:
|
|
827
|
+
content = content.encode("utf-8", errors="surrogatepass").decode(
|
|
828
|
+
"utf-8", errors="replace"
|
|
829
|
+
)
|
|
830
|
+
except (UnicodeEncodeError, UnicodeDecodeError):
|
|
831
|
+
pass # Keep original if sanitization fails
|
|
832
|
+
|
|
450
833
|
# Skip empty files
|
|
451
834
|
if not content.strip():
|
|
452
835
|
return
|
|
@@ -467,14 +850,16 @@ def normalize_command_history():
|
|
|
467
850
|
|
|
468
851
|
# Write the updated content back to the file only if changes were made
|
|
469
852
|
if content != updated_content:
|
|
470
|
-
with open(
|
|
853
|
+
with open(
|
|
854
|
+
COMMAND_HISTORY_FILE, "w", encoding="utf-8", errors="surrogateescape"
|
|
855
|
+
) as f:
|
|
471
856
|
f.write(updated_content)
|
|
472
857
|
except Exception as e:
|
|
473
|
-
from
|
|
858
|
+
from code_puppy.messaging import emit_error
|
|
474
859
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
860
|
+
emit_error(
|
|
861
|
+
f"An unexpected error occurred while normalizing command history: {str(e)}"
|
|
862
|
+
)
|
|
478
863
|
|
|
479
864
|
|
|
480
865
|
def get_user_agents_directory() -> str:
|
|
@@ -496,6 +881,10 @@ def initialize_command_history_file():
|
|
|
496
881
|
import os
|
|
497
882
|
from pathlib import Path
|
|
498
883
|
|
|
884
|
+
# Ensure the state directory exists before trying to create the history file
|
|
885
|
+
if not os.path.exists(STATE_DIR):
|
|
886
|
+
os.makedirs(STATE_DIR, exist_ok=True)
|
|
887
|
+
|
|
499
888
|
command_history_exists = os.path.isfile(COMMAND_HISTORY_FILE)
|
|
500
889
|
if not command_history_exists:
|
|
501
890
|
try:
|
|
@@ -515,11 +904,11 @@ def initialize_command_history_file():
|
|
|
515
904
|
# Normalize the command history format if needed
|
|
516
905
|
normalize_command_history()
|
|
517
906
|
except Exception as e:
|
|
518
|
-
from
|
|
907
|
+
from code_puppy.messaging import emit_error
|
|
519
908
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
909
|
+
emit_error(
|
|
910
|
+
f"An unexpected error occurred while trying to initialize history file: {str(e)}"
|
|
911
|
+
)
|
|
523
912
|
|
|
524
913
|
|
|
525
914
|
def get_yolo_mode():
|
|
@@ -537,6 +926,22 @@ def get_yolo_mode():
|
|
|
537
926
|
return True
|
|
538
927
|
|
|
539
928
|
|
|
929
|
+
def get_safety_permission_level():
|
|
930
|
+
"""
|
|
931
|
+
Checks puppy.cfg for 'safety_permission_level' (case-insensitive in value only).
|
|
932
|
+
Defaults to 'medium' if not set.
|
|
933
|
+
Allowed values: 'none', 'low', 'medium', 'high', 'critical' (all case-insensitive for value).
|
|
934
|
+
Returns the normalized lowercase string.
|
|
935
|
+
"""
|
|
936
|
+
valid_levels = {"none", "low", "medium", "high", "critical"}
|
|
937
|
+
cfg_val = get_value("safety_permission_level")
|
|
938
|
+
if cfg_val is not None:
|
|
939
|
+
normalized = str(cfg_val).strip().lower()
|
|
940
|
+
if normalized in valid_levels:
|
|
941
|
+
return normalized
|
|
942
|
+
return "medium" # Default to medium risk threshold
|
|
943
|
+
|
|
944
|
+
|
|
540
945
|
def get_mcp_disabled():
|
|
541
946
|
"""
|
|
542
947
|
Checks puppy.cfg for 'disable_mcp' (case-insensitive in value only).
|
|
@@ -553,6 +958,24 @@ def get_mcp_disabled():
|
|
|
553
958
|
return False
|
|
554
959
|
|
|
555
960
|
|
|
961
|
+
def get_grep_output_verbose():
|
|
962
|
+
"""
|
|
963
|
+
Checks puppy.cfg for 'grep_output_verbose' (case-insensitive in value only).
|
|
964
|
+
Defaults to False (concise output) if not set.
|
|
965
|
+
Allowed values for ON: 1, '1', 'true', 'yes', 'on' (all case-insensitive for value).
|
|
966
|
+
|
|
967
|
+
When False (default): Shows only file names with match counts
|
|
968
|
+
When True: Shows full output with line numbers and content
|
|
969
|
+
"""
|
|
970
|
+
true_vals = {"1", "true", "yes", "on"}
|
|
971
|
+
cfg_val = get_value("grep_output_verbose")
|
|
972
|
+
if cfg_val is not None:
|
|
973
|
+
if str(cfg_val).strip().lower() in true_vals:
|
|
974
|
+
return True
|
|
975
|
+
return False
|
|
976
|
+
return False
|
|
977
|
+
|
|
978
|
+
|
|
556
979
|
def get_protected_token_count():
|
|
557
980
|
"""
|
|
558
981
|
Returns the user-configured protected token count for message history compaction.
|
|
@@ -590,7 +1013,7 @@ def get_compaction_threshold():
|
|
|
590
1013
|
try:
|
|
591
1014
|
threshold = float(val) if val else 0.85
|
|
592
1015
|
# Clamp between reasonable bounds
|
|
593
|
-
return max(0.
|
|
1016
|
+
return max(0.5, min(0.95, threshold))
|
|
594
1017
|
except (ValueError, TypeError):
|
|
595
1018
|
return 0.85
|
|
596
1019
|
|
|
@@ -635,11 +1058,11 @@ def set_enable_dbos(enabled: bool) -> None:
|
|
|
635
1058
|
set_config_value("enable_dbos", "true" if enabled else "false")
|
|
636
1059
|
|
|
637
1060
|
|
|
638
|
-
def get_message_limit(default: int =
|
|
1061
|
+
def get_message_limit(default: int = 1000) -> int:
|
|
639
1062
|
"""
|
|
640
1063
|
Returns the user-configured message/request limit for the agent.
|
|
641
1064
|
This controls how many steps/requests the agent can take.
|
|
642
|
-
Defaults to
|
|
1065
|
+
Defaults to 1000 if unset or misconfigured.
|
|
643
1066
|
Configurable by 'message_limit' key.
|
|
644
1067
|
"""
|
|
645
1068
|
val = get_value("message_limit")
|
|
@@ -659,16 +1082,30 @@ def save_command_to_history(command: str):
|
|
|
659
1082
|
|
|
660
1083
|
try:
|
|
661
1084
|
timestamp = datetime.datetime.now().isoformat(timespec="seconds")
|
|
662
|
-
|
|
1085
|
+
|
|
1086
|
+
# Sanitize command to remove any invalid surrogate characters
|
|
1087
|
+
# that could cause encoding errors on Windows
|
|
1088
|
+
try:
|
|
1089
|
+
command = command.encode("utf-8", errors="surrogatepass").decode(
|
|
1090
|
+
"utf-8", errors="replace"
|
|
1091
|
+
)
|
|
1092
|
+
except (UnicodeEncodeError, UnicodeDecodeError):
|
|
1093
|
+
# If that fails, do a more aggressive cleanup
|
|
1094
|
+
command = "".join(
|
|
1095
|
+
char if ord(char) < 0xD800 or ord(char) > 0xDFFF else "\ufffd"
|
|
1096
|
+
for char in command
|
|
1097
|
+
)
|
|
1098
|
+
|
|
1099
|
+
with open(
|
|
1100
|
+
COMMAND_HISTORY_FILE, "a", encoding="utf-8", errors="surrogateescape"
|
|
1101
|
+
) as f:
|
|
663
1102
|
f.write(f"\n# {timestamp}\n{command}\n")
|
|
664
1103
|
except Exception as e:
|
|
665
|
-
from
|
|
1104
|
+
from code_puppy.messaging import emit_error
|
|
666
1105
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
f"❌ An unexpected error occurred while saving command history: {str(e)}"
|
|
1106
|
+
emit_error(
|
|
1107
|
+
f"An unexpected error occurred while saving command history: {str(e)}"
|
|
670
1108
|
)
|
|
671
|
-
direct_console.print(f"[bold red]{error_msg}[/bold red]")
|
|
672
1109
|
|
|
673
1110
|
|
|
674
1111
|
def get_agent_pinned_model(agent_name: str) -> str:
|
|
@@ -704,6 +1141,38 @@ def clear_agent_pinned_model(agent_name: str):
|
|
|
704
1141
|
set_config_value(f"agent_model_{agent_name}", "")
|
|
705
1142
|
|
|
706
1143
|
|
|
1144
|
+
def get_all_agent_pinned_models() -> dict:
|
|
1145
|
+
"""Get all agent-to-model pinnings from config.
|
|
1146
|
+
|
|
1147
|
+
Returns:
|
|
1148
|
+
Dict mapping agent names to their pinned model names.
|
|
1149
|
+
Only includes agents that have a pinned model (non-empty value).
|
|
1150
|
+
"""
|
|
1151
|
+
config = configparser.ConfigParser()
|
|
1152
|
+
config.read(CONFIG_FILE)
|
|
1153
|
+
|
|
1154
|
+
pinnings = {}
|
|
1155
|
+
if DEFAULT_SECTION in config:
|
|
1156
|
+
for key, value in config[DEFAULT_SECTION].items():
|
|
1157
|
+
if key.startswith("agent_model_") and value:
|
|
1158
|
+
agent_name = key[len("agent_model_") :]
|
|
1159
|
+
pinnings[agent_name] = value
|
|
1160
|
+
return pinnings
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
def get_agents_pinned_to_model(model_name: str) -> list:
|
|
1164
|
+
"""Get all agents that are pinned to a specific model.
|
|
1165
|
+
|
|
1166
|
+
Args:
|
|
1167
|
+
model_name: The model name to look up.
|
|
1168
|
+
|
|
1169
|
+
Returns:
|
|
1170
|
+
List of agent names pinned to this model.
|
|
1171
|
+
"""
|
|
1172
|
+
all_pinnings = get_all_agent_pinned_models()
|
|
1173
|
+
return [agent for agent, model in all_pinnings.items() if model == model_name]
|
|
1174
|
+
|
|
1175
|
+
|
|
707
1176
|
def get_auto_save_session() -> bool:
|
|
708
1177
|
"""
|
|
709
1178
|
Checks puppy.cfg for 'auto_save_session' (case-insensitive in value only).
|
|
@@ -752,6 +1221,139 @@ def set_max_saved_sessions(max_sessions: int):
|
|
|
752
1221
|
set_config_value("max_saved_sessions", str(max_sessions))
|
|
753
1222
|
|
|
754
1223
|
|
|
1224
|
+
def set_diff_highlight_style(style: str):
|
|
1225
|
+
"""Set the diff highlight style.
|
|
1226
|
+
|
|
1227
|
+
Note: Text mode has been removed. This function is kept for backwards compatibility
|
|
1228
|
+
but does nothing. All diffs use beautiful syntax highlighting now!
|
|
1229
|
+
|
|
1230
|
+
Args:
|
|
1231
|
+
style: Ignored (always uses 'highlight' mode)
|
|
1232
|
+
"""
|
|
1233
|
+
# Do nothing - we always use highlight mode now!
|
|
1234
|
+
pass
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def get_diff_addition_color() -> str:
|
|
1238
|
+
"""
|
|
1239
|
+
Get the base color for diff additions.
|
|
1240
|
+
Default: darker green
|
|
1241
|
+
"""
|
|
1242
|
+
val = get_value("highlight_addition_color")
|
|
1243
|
+
if val:
|
|
1244
|
+
return val
|
|
1245
|
+
return "#0b1f0b" # Default to darker green
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
def set_diff_addition_color(color: str):
|
|
1249
|
+
"""Set the color for diff additions.
|
|
1250
|
+
|
|
1251
|
+
Args:
|
|
1252
|
+
color: Rich color markup (e.g., 'green', 'on_green', 'bright_green')
|
|
1253
|
+
"""
|
|
1254
|
+
set_config_value("highlight_addition_color", color)
|
|
1255
|
+
|
|
1256
|
+
|
|
1257
|
+
def get_diff_deletion_color() -> str:
|
|
1258
|
+
"""
|
|
1259
|
+
Get the base color for diff deletions.
|
|
1260
|
+
Default: wine
|
|
1261
|
+
"""
|
|
1262
|
+
val = get_value("highlight_deletion_color")
|
|
1263
|
+
if val:
|
|
1264
|
+
return val
|
|
1265
|
+
return "#390e1a" # Default to wine
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def set_diff_deletion_color(color: str):
|
|
1269
|
+
"""Set the color for diff deletions.
|
|
1270
|
+
|
|
1271
|
+
Args:
|
|
1272
|
+
color: Rich color markup (e.g., 'orange1', 'on_bright_yellow', 'red')
|
|
1273
|
+
"""
|
|
1274
|
+
set_config_value("highlight_deletion_color", color)
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
# =============================================================================
|
|
1278
|
+
# Banner Color Configuration
|
|
1279
|
+
# =============================================================================
|
|
1280
|
+
|
|
1281
|
+
# Default banner colors (Rich color names)
|
|
1282
|
+
# A beautiful jewel-tone palette with semantic meaning:
|
|
1283
|
+
# - Blues/Teals: Reading & navigation (calm, informational)
|
|
1284
|
+
# - Warm tones: Actions & changes (edits, shell commands)
|
|
1285
|
+
# - Purples: AI thinking & reasoning (the "brain" colors)
|
|
1286
|
+
# - Greens: Completions & success
|
|
1287
|
+
# - Neutrals: Search & listings
|
|
1288
|
+
DEFAULT_BANNER_COLORS = {
|
|
1289
|
+
"thinking": "deep_sky_blue4", # Sapphire - contemplation
|
|
1290
|
+
"agent_response": "medium_purple4", # Amethyst - main AI output
|
|
1291
|
+
"shell_command": "dark_orange3", # Amber - system commands
|
|
1292
|
+
"read_file": "steel_blue", # Steel - reading files
|
|
1293
|
+
"edit_file": "dark_goldenrod", # Gold - modifications
|
|
1294
|
+
"grep": "grey37", # Silver - search results
|
|
1295
|
+
"directory_listing": "dodger_blue2", # Sky - navigation
|
|
1296
|
+
"agent_reasoning": "dark_violet", # Violet - deep thought
|
|
1297
|
+
"invoke_agent": "deep_pink4", # Ruby - agent invocation
|
|
1298
|
+
"subagent_response": "sea_green3", # Emerald - sub-agent success
|
|
1299
|
+
"list_agents": "dark_slate_gray3", # Slate - neutral listing
|
|
1300
|
+
# Browser/Terminal tools - same color as edit_file (gold)
|
|
1301
|
+
"terminal_tool": "dark_goldenrod", # Gold - browser terminal operations
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
def get_banner_color(banner_name: str) -> str:
|
|
1306
|
+
"""Get the background color for a specific banner.
|
|
1307
|
+
|
|
1308
|
+
Args:
|
|
1309
|
+
banner_name: The banner identifier (e.g., 'thinking', 'agent_response')
|
|
1310
|
+
|
|
1311
|
+
Returns:
|
|
1312
|
+
Rich color name or hex code for the banner background
|
|
1313
|
+
"""
|
|
1314
|
+
config_key = f"banner_color_{banner_name}"
|
|
1315
|
+
val = get_value(config_key)
|
|
1316
|
+
if val:
|
|
1317
|
+
return val
|
|
1318
|
+
return DEFAULT_BANNER_COLORS.get(banner_name, "blue")
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
def set_banner_color(banner_name: str, color: str):
|
|
1322
|
+
"""Set the background color for a specific banner.
|
|
1323
|
+
|
|
1324
|
+
Args:
|
|
1325
|
+
banner_name: The banner identifier (e.g., 'thinking', 'agent_response')
|
|
1326
|
+
color: Rich color name or hex code
|
|
1327
|
+
"""
|
|
1328
|
+
config_key = f"banner_color_{banner_name}"
|
|
1329
|
+
set_config_value(config_key, color)
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
def get_all_banner_colors() -> dict:
|
|
1333
|
+
"""Get all banner colors (configured or default).
|
|
1334
|
+
|
|
1335
|
+
Returns:
|
|
1336
|
+
Dict mapping banner names to their colors
|
|
1337
|
+
"""
|
|
1338
|
+
return {name: get_banner_color(name) for name in DEFAULT_BANNER_COLORS}
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
def reset_banner_color(banner_name: str):
|
|
1342
|
+
"""Reset a banner color to its default.
|
|
1343
|
+
|
|
1344
|
+
Args:
|
|
1345
|
+
banner_name: The banner identifier to reset
|
|
1346
|
+
"""
|
|
1347
|
+
default_color = DEFAULT_BANNER_COLORS.get(banner_name, "blue")
|
|
1348
|
+
set_banner_color(banner_name, default_color)
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
def reset_all_banner_colors():
|
|
1352
|
+
"""Reset all banner colors to their defaults."""
|
|
1353
|
+
for name, color in DEFAULT_BANNER_COLORS.items():
|
|
1354
|
+
set_banner_color(name, color)
|
|
1355
|
+
|
|
1356
|
+
|
|
755
1357
|
def get_current_autosave_id() -> str:
|
|
756
1358
|
"""Get or create the current autosave session ID for this process."""
|
|
757
1359
|
global _CURRENT_AUTOSAVE_ID
|
|
@@ -796,11 +1398,8 @@ def auto_save_session_if_enabled() -> bool:
|
|
|
796
1398
|
try:
|
|
797
1399
|
import pathlib
|
|
798
1400
|
|
|
799
|
-
from rich.console import Console
|
|
800
|
-
|
|
801
1401
|
from code_puppy.agents.agent_manager import get_current_agent
|
|
802
|
-
|
|
803
|
-
console = Console()
|
|
1402
|
+
from code_puppy.messaging import emit_info
|
|
804
1403
|
|
|
805
1404
|
current_agent = get_current_agent()
|
|
806
1405
|
history = current_agent.get_message_history()
|
|
@@ -820,20 +1419,207 @@ def auto_save_session_if_enabled() -> bool:
|
|
|
820
1419
|
auto_saved=True,
|
|
821
1420
|
)
|
|
822
1421
|
|
|
823
|
-
|
|
824
|
-
f"🐾
|
|
1422
|
+
emit_info(
|
|
1423
|
+
f"🐾 Auto-saved session: {metadata.message_count} messages ({metadata.total_tokens} tokens)"
|
|
825
1424
|
)
|
|
826
1425
|
|
|
827
1426
|
return True
|
|
828
1427
|
|
|
829
1428
|
except Exception as exc: # pragma: no cover - defensive logging
|
|
830
|
-
from
|
|
1429
|
+
from code_puppy.messaging import emit_error
|
|
831
1430
|
|
|
832
|
-
|
|
1431
|
+
emit_error(f"Failed to auto-save session: {exc}")
|
|
833
1432
|
return False
|
|
834
1433
|
|
|
835
1434
|
|
|
1435
|
+
def get_diff_context_lines() -> int:
|
|
1436
|
+
"""
|
|
1437
|
+
Returns the user-configured number of context lines for diff display.
|
|
1438
|
+
This controls how many lines of surrounding context are shown in diffs.
|
|
1439
|
+
Defaults to 6 if unset or misconfigured.
|
|
1440
|
+
Configurable by 'diff_context_lines' key.
|
|
1441
|
+
"""
|
|
1442
|
+
val = get_value("diff_context_lines")
|
|
1443
|
+
try:
|
|
1444
|
+
context_lines = int(val) if val else 6
|
|
1445
|
+
# Apply reasonable bounds: minimum 0, maximum 50
|
|
1446
|
+
return max(0, min(context_lines, 50))
|
|
1447
|
+
except (ValueError, TypeError):
|
|
1448
|
+
return 6
|
|
1449
|
+
|
|
1450
|
+
|
|
836
1451
|
def finalize_autosave_session() -> str:
|
|
837
1452
|
"""Persist the current autosave snapshot and rotate to a fresh session."""
|
|
838
1453
|
auto_save_session_if_enabled()
|
|
839
1454
|
return rotate_autosave_id()
|
|
1455
|
+
|
|
1456
|
+
|
|
1457
|
+
def get_suppress_thinking_messages() -> bool:
|
|
1458
|
+
"""
|
|
1459
|
+
Checks puppy.cfg for 'suppress_thinking_messages' (case-insensitive in value only).
|
|
1460
|
+
Defaults to False if not set.
|
|
1461
|
+
Allowed values for ON: 1, '1', 'true', 'yes', 'on' (all case-insensitive for value).
|
|
1462
|
+
When enabled, thinking messages (agent_reasoning, planned_next_steps) will be hidden.
|
|
1463
|
+
"""
|
|
1464
|
+
true_vals = {"1", "true", "yes", "on"}
|
|
1465
|
+
cfg_val = get_value("suppress_thinking_messages")
|
|
1466
|
+
if cfg_val is not None:
|
|
1467
|
+
if str(cfg_val).strip().lower() in true_vals:
|
|
1468
|
+
return True
|
|
1469
|
+
return False
|
|
1470
|
+
return False
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
def set_suppress_thinking_messages(enabled: bool):
|
|
1474
|
+
"""Sets the suppress_thinking_messages configuration value.
|
|
1475
|
+
|
|
1476
|
+
Args:
|
|
1477
|
+
enabled: Whether to suppress thinking messages
|
|
1478
|
+
"""
|
|
1479
|
+
set_config_value("suppress_thinking_messages", "true" if enabled else "false")
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
def get_suppress_informational_messages() -> bool:
|
|
1483
|
+
"""
|
|
1484
|
+
Checks puppy.cfg for 'suppress_informational_messages' (case-insensitive in value only).
|
|
1485
|
+
Defaults to False if not set.
|
|
1486
|
+
Allowed values for ON: 1, '1', 'true', 'yes', 'on' (all case-insensitive for value).
|
|
1487
|
+
When enabled, informational messages (info, success, warning) will be hidden.
|
|
1488
|
+
"""
|
|
1489
|
+
true_vals = {"1", "true", "yes", "on"}
|
|
1490
|
+
cfg_val = get_value("suppress_informational_messages")
|
|
1491
|
+
if cfg_val is not None:
|
|
1492
|
+
if str(cfg_val).strip().lower() in true_vals:
|
|
1493
|
+
return True
|
|
1494
|
+
return False
|
|
1495
|
+
return False
|
|
1496
|
+
|
|
1497
|
+
|
|
1498
|
+
def set_suppress_informational_messages(enabled: bool):
|
|
1499
|
+
"""Sets the suppress_informational_messages configuration value.
|
|
1500
|
+
|
|
1501
|
+
Args:
|
|
1502
|
+
enabled: Whether to suppress informational messages
|
|
1503
|
+
"""
|
|
1504
|
+
set_config_value("suppress_informational_messages", "true" if enabled else "false")
|
|
1505
|
+
|
|
1506
|
+
|
|
1507
|
+
# API Key management functions
|
|
1508
|
+
def get_api_key(key_name: str) -> str:
|
|
1509
|
+
"""Get an API key from puppy.cfg.
|
|
1510
|
+
|
|
1511
|
+
Args:
|
|
1512
|
+
key_name: The name of the API key (e.g., 'OPENAI_API_KEY')
|
|
1513
|
+
|
|
1514
|
+
Returns:
|
|
1515
|
+
The API key value, or empty string if not set
|
|
1516
|
+
"""
|
|
1517
|
+
return get_value(key_name) or ""
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
def set_api_key(key_name: str, value: str):
|
|
1521
|
+
"""Set an API key in puppy.cfg.
|
|
1522
|
+
|
|
1523
|
+
Args:
|
|
1524
|
+
key_name: The name of the API key (e.g., 'OPENAI_API_KEY')
|
|
1525
|
+
value: The API key value (empty string to remove)
|
|
1526
|
+
"""
|
|
1527
|
+
set_config_value(key_name, value)
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
def load_api_keys_to_environment():
|
|
1531
|
+
"""Load all API keys from .env and puppy.cfg into environment variables.
|
|
1532
|
+
|
|
1533
|
+
Priority order:
|
|
1534
|
+
1. .env file (highest priority) - if present in current directory
|
|
1535
|
+
2. puppy.cfg - fallback if not in .env
|
|
1536
|
+
3. Existing environment variables - preserved if already set
|
|
1537
|
+
|
|
1538
|
+
This should be called on startup to ensure API keys are available.
|
|
1539
|
+
"""
|
|
1540
|
+
from pathlib import Path
|
|
1541
|
+
|
|
1542
|
+
api_key_names = [
|
|
1543
|
+
"OPENAI_API_KEY",
|
|
1544
|
+
"GEMINI_API_KEY",
|
|
1545
|
+
"ANTHROPIC_API_KEY",
|
|
1546
|
+
"CEREBRAS_API_KEY",
|
|
1547
|
+
"SYN_API_KEY",
|
|
1548
|
+
"AZURE_OPENAI_API_KEY",
|
|
1549
|
+
"AZURE_OPENAI_ENDPOINT",
|
|
1550
|
+
"OPENROUTER_API_KEY",
|
|
1551
|
+
"ZAI_API_KEY",
|
|
1552
|
+
]
|
|
1553
|
+
|
|
1554
|
+
# Step 1: Load from .env file if it exists (highest priority)
|
|
1555
|
+
# Look for .env in current working directory
|
|
1556
|
+
env_file = Path.cwd() / ".env"
|
|
1557
|
+
if env_file.exists():
|
|
1558
|
+
try:
|
|
1559
|
+
from dotenv import load_dotenv
|
|
1560
|
+
|
|
1561
|
+
# override=True means .env values take precedence over existing env vars
|
|
1562
|
+
load_dotenv(env_file, override=True)
|
|
1563
|
+
except ImportError:
|
|
1564
|
+
# python-dotenv not installed, skip .env loading
|
|
1565
|
+
pass
|
|
1566
|
+
|
|
1567
|
+
# Step 2: Load from puppy.cfg, but only if not already set
|
|
1568
|
+
# This ensures .env has priority over puppy.cfg
|
|
1569
|
+
for key_name in api_key_names:
|
|
1570
|
+
# Only load from config if not already in environment
|
|
1571
|
+
if key_name not in os.environ or not os.environ[key_name]:
|
|
1572
|
+
value = get_api_key(key_name)
|
|
1573
|
+
if value:
|
|
1574
|
+
os.environ[key_name] = value
|
|
1575
|
+
|
|
1576
|
+
|
|
1577
|
+
def get_default_agent() -> str:
|
|
1578
|
+
"""
|
|
1579
|
+
Get the default agent name from puppy.cfg.
|
|
1580
|
+
|
|
1581
|
+
Returns:
|
|
1582
|
+
str: The default agent name, or "code-puppy" if not set.
|
|
1583
|
+
"""
|
|
1584
|
+
return get_value("default_agent") or "code-puppy"
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
def set_default_agent(agent_name: str) -> None:
|
|
1588
|
+
"""
|
|
1589
|
+
Set the default agent name in puppy.cfg.
|
|
1590
|
+
|
|
1591
|
+
Args:
|
|
1592
|
+
agent_name: The name of the agent to set as default.
|
|
1593
|
+
"""
|
|
1594
|
+
set_config_value("default_agent", agent_name)
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
# --- FRONTEND EMITTER CONFIGURATION ---
|
|
1598
|
+
def get_frontend_emitter_enabled() -> bool:
|
|
1599
|
+
"""Check if frontend emitter is enabled."""
|
|
1600
|
+
val = get_value("frontend_emitter_enabled")
|
|
1601
|
+
if val is None:
|
|
1602
|
+
return True # Enabled by default
|
|
1603
|
+
return str(val).lower() in ("1", "true", "yes", "on")
|
|
1604
|
+
|
|
1605
|
+
|
|
1606
|
+
def get_frontend_emitter_max_recent_events() -> int:
|
|
1607
|
+
"""Get max number of recent events to buffer."""
|
|
1608
|
+
val = get_value("frontend_emitter_max_recent_events")
|
|
1609
|
+
if val is None:
|
|
1610
|
+
return 100
|
|
1611
|
+
try:
|
|
1612
|
+
return int(val)
|
|
1613
|
+
except ValueError:
|
|
1614
|
+
return 100
|
|
1615
|
+
|
|
1616
|
+
|
|
1617
|
+
def get_frontend_emitter_queue_size() -> int:
|
|
1618
|
+
"""Get max subscriber queue size."""
|
|
1619
|
+
val = get_value("frontend_emitter_queue_size")
|
|
1620
|
+
if val is None:
|
|
1621
|
+
return 100
|
|
1622
|
+
try:
|
|
1623
|
+
return int(val)
|
|
1624
|
+
except ValueError:
|
|
1625
|
+
return 100
|