code-puppy 0.0.287__py3-none-any.whl → 0.0.323__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 +3 -1
- code_puppy/agents/agent_code_puppy.py +5 -4
- code_puppy/agents/agent_creator_agent.py +22 -18
- code_puppy/agents/agent_manager.py +2 -2
- code_puppy/agents/base_agent.py +496 -102
- code_puppy/callbacks.py +8 -0
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/cli_runner.py +795 -0
- code_puppy/command_line/add_model_menu.py +19 -16
- code_puppy/command_line/attachments.py +10 -5
- code_puppy/command_line/autosave_menu.py +269 -41
- code_puppy/command_line/colors_menu.py +515 -0
- code_puppy/command_line/command_handler.py +10 -24
- code_puppy/command_line/config_commands.py +106 -25
- code_puppy/command_line/core_commands.py +32 -20
- code_puppy/command_line/mcp/add_command.py +3 -16
- code_puppy/command_line/mcp/base.py +0 -3
- code_puppy/command_line/mcp/catalog_server_installer.py +15 -15
- code_puppy/command_line/mcp/custom_server_form.py +66 -5
- code_puppy/command_line/mcp/custom_server_installer.py +17 -17
- code_puppy/command_line/mcp/edit_command.py +15 -22
- code_puppy/command_line/mcp/handler.py +7 -2
- code_puppy/command_line/mcp/help_command.py +2 -2
- code_puppy/command_line/mcp/install_command.py +10 -14
- code_puppy/command_line/mcp/install_menu.py +2 -6
- 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 +7 -2
- code_puppy/command_line/mcp/search_command.py +16 -10
- code_puppy/command_line/mcp/start_all_command.py +16 -6
- code_puppy/command_line/mcp/start_command.py +12 -10
- code_puppy/command_line/mcp/status_command.py +4 -5
- code_puppy/command_line/mcp/stop_all_command.py +5 -1
- code_puppy/command_line/mcp/stop_command.py +6 -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/model_settings_menu.py +53 -7
- code_puppy/command_line/motd.py +1 -1
- code_puppy/command_line/pin_command_completion.py +82 -7
- code_puppy/command_line/prompt_toolkit_completion.py +32 -9
- code_puppy/command_line/session_commands.py +11 -4
- code_puppy/config.py +217 -53
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/keymap.py +126 -0
- code_puppy/main.py +5 -745
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/blocking_startup.py +63 -36
- code_puppy/mcp_/captured_stdio_server.py +1 -1
- code_puppy/mcp_/config_wizard.py +4 -4
- code_puppy/mcp_/dashboard.py +15 -6
- code_puppy/mcp_/managed_server.py +25 -5
- code_puppy/mcp_/manager.py +65 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +6 -6
- code_puppy/messaging/__init__.py +184 -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 +3 -3
- code_puppy/messaging/messages.py +470 -0
- code_puppy/messaging/renderers.py +43 -141
- code_puppy/messaging/rich_renderer.py +900 -0
- code_puppy/messaging/spinner/console_spinner.py +39 -2
- code_puppy/model_factory.py +292 -53
- code_puppy/model_utils.py +57 -48
- code_puppy/models.json +19 -5
- code_puppy/plugins/__init__.py +152 -10
- code_puppy/plugins/chatgpt_oauth/config.py +20 -12
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +5 -6
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +3 -3
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +30 -13
- code_puppy/plugins/chatgpt_oauth/utils.py +180 -65
- code_puppy/plugins/claude_code_oauth/config.py +15 -11
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +28 -0
- code_puppy/plugins/claude_code_oauth/utils.py +6 -1
- code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
- code_puppy/plugins/oauth_puppy_html.py +3 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +1 -134
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +77 -3
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/session_storage.py +2 -1
- code_puppy/status_display.py +7 -5
- code_puppy/terminal_utils.py +126 -0
- code_puppy/tools/agent_tools.py +131 -70
- code_puppy/tools/browser/browser_control.py +10 -14
- code_puppy/tools/browser/browser_interactions.py +20 -28
- code_puppy/tools/browser/browser_locators.py +27 -29
- code_puppy/tools/browser/browser_navigation.py +9 -9
- code_puppy/tools/browser/browser_screenshot.py +12 -14
- code_puppy/tools/browser/browser_scripts.py +17 -29
- code_puppy/tools/browser/browser_workflows.py +24 -25
- code_puppy/tools/browser/camoufox_manager.py +22 -26
- code_puppy/tools/command_runner.py +410 -88
- code_puppy/tools/common.py +51 -38
- code_puppy/tools/file_modifications.py +98 -24
- code_puppy/tools/file_operations.py +113 -202
- code_puppy/version_checker.py +28 -13
- {code_puppy-0.0.287.data → code_puppy-0.0.323.data}/data/code_puppy/models.json +19 -5
- {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/METADATA +3 -8
- code_puppy-0.0.323.dist-info/RECORD +168 -0
- code_puppy/tui_state.py +0 -55
- code_puppy-0.0.287.dist-info/RECORD +0 -153
- {code_puppy-0.0.287.data → code_puppy-0.0.323.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/licenses/LICENSE +0 -0
code_puppy/config.py
CHANGED
|
@@ -7,17 +7,58 @@ 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
|
+
|
|
57
|
+
# Cache files (XDG_CACHE_HOME)
|
|
58
|
+
AUTOSAVE_DIR = os.path.join(CACHE_DIR, "autosaves")
|
|
59
|
+
|
|
60
|
+
# State files (XDG_STATE_HOME)
|
|
61
|
+
COMMAND_HISTORY_FILE = os.path.join(STATE_DIR, "command_history.txt")
|
|
21
62
|
DBOS_DATABASE_URL = os.environ.get(
|
|
22
63
|
"DBOS_SYSTEM_DATABASE_URL", f"sqlite:///{_DEFAULT_SQLITE_FILE}"
|
|
23
64
|
)
|
|
@@ -48,11 +89,13 @@ _default_vqa_model_cache = None
|
|
|
48
89
|
|
|
49
90
|
def ensure_config_exists():
|
|
50
91
|
"""
|
|
51
|
-
Ensure that
|
|
92
|
+
Ensure that XDG directories and puppy.cfg exist, prompting if needed.
|
|
52
93
|
Returns configparser.ConfigParser for reading.
|
|
53
94
|
"""
|
|
54
|
-
|
|
55
|
-
|
|
95
|
+
# Create all XDG directories with 0700 permissions per XDG spec
|
|
96
|
+
for directory in [CONFIG_DIR, DATA_DIR, CACHE_DIR, STATE_DIR]:
|
|
97
|
+
if not os.path.exists(directory):
|
|
98
|
+
os.makedirs(directory, mode=0o700, exist_ok=True)
|
|
56
99
|
exists = os.path.isfile(CONFIG_FILE)
|
|
57
100
|
config = configparser.ConfigParser()
|
|
58
101
|
if exists:
|
|
@@ -64,7 +107,11 @@ def ensure_config_exists():
|
|
|
64
107
|
if not config[DEFAULT_SECTION].get(key):
|
|
65
108
|
missing.append(key)
|
|
66
109
|
if missing:
|
|
67
|
-
|
|
110
|
+
# Note: Using sys.stdout here for initial setup before messaging system is available
|
|
111
|
+
import sys
|
|
112
|
+
|
|
113
|
+
sys.stdout.write("🐾 Let's get your Puppy ready!\n")
|
|
114
|
+
sys.stdout.flush()
|
|
68
115
|
for key in missing:
|
|
69
116
|
if key == "puppy_name":
|
|
70
117
|
val = input("What should we name the puppy? ").strip()
|
|
@@ -163,6 +210,11 @@ def get_config_keys():
|
|
|
163
210
|
]
|
|
164
211
|
# Add DBOS control key
|
|
165
212
|
default_keys.append("enable_dbos")
|
|
213
|
+
# Add cancel agent key configuration
|
|
214
|
+
default_keys.append("cancel_agent_key")
|
|
215
|
+
# Add banner color keys
|
|
216
|
+
for banner_name in DEFAULT_BANNER_COLORS:
|
|
217
|
+
default_keys.append(f"banner_color_{banner_name}")
|
|
166
218
|
|
|
167
219
|
config = configparser.ConfigParser()
|
|
168
220
|
config.read(CONFIG_FILE)
|
|
@@ -187,7 +239,7 @@ def set_config_value(key: str, value: str):
|
|
|
187
239
|
# --- MODEL STICKY EXTENSION STARTS HERE ---
|
|
188
240
|
def load_mcp_server_configs():
|
|
189
241
|
"""
|
|
190
|
-
Loads the MCP server configurations from
|
|
242
|
+
Loads the MCP server configurations from XDG_CONFIG_HOME/code_puppy/mcp_servers.json.
|
|
191
243
|
Returns a dict mapping names to their URL or config dict.
|
|
192
244
|
If file does not exist, returns an empty dict.
|
|
193
245
|
"""
|
|
@@ -207,9 +259,8 @@ def load_mcp_server_configs():
|
|
|
207
259
|
def _default_model_from_models_json():
|
|
208
260
|
"""Load the default model name from models.json.
|
|
209
261
|
|
|
210
|
-
|
|
211
|
-
Falls back to
|
|
212
|
-
As a last resort, falls back to ``gpt-5`` if the file cannot be read.
|
|
262
|
+
Returns the first model in models.json as the default.
|
|
263
|
+
Falls back to ``gpt-5`` if the file cannot be read.
|
|
213
264
|
"""
|
|
214
265
|
global _default_model_cache
|
|
215
266
|
|
|
@@ -221,11 +272,7 @@ def _default_model_from_models_json():
|
|
|
221
272
|
|
|
222
273
|
models_config = ModelFactory.load_config()
|
|
223
274
|
if models_config:
|
|
224
|
-
#
|
|
225
|
-
if "synthetic-GLM-4.6" in models_config:
|
|
226
|
-
_default_model_cache = "synthetic-GLM-4.6"
|
|
227
|
-
return "synthetic-GLM-4.6"
|
|
228
|
-
# Fall back to first model if synthetic-GLM-4.6 is not available
|
|
275
|
+
# Use first model in models.json as default
|
|
229
276
|
first_key = next(iter(models_config))
|
|
230
277
|
_default_model_cache = first_key
|
|
231
278
|
return first_key
|
|
@@ -448,8 +495,8 @@ def set_puppy_token(token: str):
|
|
|
448
495
|
|
|
449
496
|
|
|
450
497
|
def get_openai_reasoning_effort() -> str:
|
|
451
|
-
"""Return the configured OpenAI reasoning effort (low, medium, high)."""
|
|
452
|
-
allowed_values = {"low", "medium", "high"}
|
|
498
|
+
"""Return the configured OpenAI reasoning effort (minimal, low, medium, high, xhigh)."""
|
|
499
|
+
allowed_values = {"minimal", "low", "medium", "high", "xhigh"}
|
|
453
500
|
configured = (get_value("openai_reasoning_effort") or "medium").strip().lower()
|
|
454
501
|
if configured not in allowed_values:
|
|
455
502
|
return "medium"
|
|
@@ -458,7 +505,7 @@ def get_openai_reasoning_effort() -> str:
|
|
|
458
505
|
|
|
459
506
|
def set_openai_reasoning_effort(value: str) -> None:
|
|
460
507
|
"""Persist the OpenAI reasoning effort ensuring it remains within allowed values."""
|
|
461
|
-
allowed_values = {"low", "medium", "high"}
|
|
508
|
+
allowed_values = {"minimal", "low", "medium", "high", "xhigh"}
|
|
462
509
|
normalized = (value or "").strip().lower()
|
|
463
510
|
if normalized not in allowed_values:
|
|
464
511
|
raise ValueError(
|
|
@@ -609,10 +656,22 @@ def get_all_model_settings(model_name: str) -> dict:
|
|
|
609
656
|
for key, val in config[DEFAULT_SECTION].items():
|
|
610
657
|
if key.startswith(prefix) and val.strip():
|
|
611
658
|
setting_name = key[len(prefix) :]
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
659
|
+
# Handle different value types
|
|
660
|
+
val_stripped = val.strip()
|
|
661
|
+
# Check for boolean values first
|
|
662
|
+
if val_stripped.lower() in ("true", "false"):
|
|
663
|
+
settings[setting_name] = val_stripped.lower() == "true"
|
|
664
|
+
else:
|
|
665
|
+
# Try to parse as number (int first, then float)
|
|
666
|
+
try:
|
|
667
|
+
# Try int first for cleaner values like budget_tokens
|
|
668
|
+
if "." not in val_stripped:
|
|
669
|
+
settings[setting_name] = int(val_stripped)
|
|
670
|
+
else:
|
|
671
|
+
settings[setting_name] = float(val_stripped)
|
|
672
|
+
except (ValueError, TypeError):
|
|
673
|
+
# Keep as string if not a number
|
|
674
|
+
settings[setting_name] = val_stripped
|
|
616
675
|
|
|
617
676
|
return settings
|
|
618
677
|
|
|
@@ -789,11 +848,11 @@ def normalize_command_history():
|
|
|
789
848
|
) as f:
|
|
790
849
|
f.write(updated_content)
|
|
791
850
|
except Exception as e:
|
|
792
|
-
from
|
|
851
|
+
from code_puppy.messaging import emit_error
|
|
793
852
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
853
|
+
emit_error(
|
|
854
|
+
f"An unexpected error occurred while normalizing command history: {str(e)}"
|
|
855
|
+
)
|
|
797
856
|
|
|
798
857
|
|
|
799
858
|
def get_user_agents_directory() -> str:
|
|
@@ -815,9 +874,9 @@ def initialize_command_history_file():
|
|
|
815
874
|
import os
|
|
816
875
|
from pathlib import Path
|
|
817
876
|
|
|
818
|
-
# Ensure the
|
|
819
|
-
if not os.path.exists(
|
|
820
|
-
os.makedirs(
|
|
877
|
+
# Ensure the state directory exists before trying to create the history file
|
|
878
|
+
if not os.path.exists(STATE_DIR):
|
|
879
|
+
os.makedirs(STATE_DIR, exist_ok=True)
|
|
821
880
|
|
|
822
881
|
command_history_exists = os.path.isfile(COMMAND_HISTORY_FILE)
|
|
823
882
|
if not command_history_exists:
|
|
@@ -838,11 +897,11 @@ def initialize_command_history_file():
|
|
|
838
897
|
# Normalize the command history format if needed
|
|
839
898
|
normalize_command_history()
|
|
840
899
|
except Exception as e:
|
|
841
|
-
from
|
|
900
|
+
from code_puppy.messaging import emit_error
|
|
842
901
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
902
|
+
emit_error(
|
|
903
|
+
f"An unexpected error occurred while trying to initialize history file: {str(e)}"
|
|
904
|
+
)
|
|
846
905
|
|
|
847
906
|
|
|
848
907
|
def get_yolo_mode():
|
|
@@ -1035,13 +1094,11 @@ def save_command_to_history(command: str):
|
|
|
1035
1094
|
) as f:
|
|
1036
1095
|
f.write(f"\n# {timestamp}\n{command}\n")
|
|
1037
1096
|
except Exception as e:
|
|
1038
|
-
from
|
|
1097
|
+
from code_puppy.messaging import emit_error
|
|
1039
1098
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
f"❌ An unexpected error occurred while saving command history: {str(e)}"
|
|
1099
|
+
emit_error(
|
|
1100
|
+
f"An unexpected error occurred while saving command history: {str(e)}"
|
|
1043
1101
|
)
|
|
1044
|
-
direct_console.print(f"[bold red]{error_msg}[/bold red]")
|
|
1045
1102
|
|
|
1046
1103
|
|
|
1047
1104
|
def get_agent_pinned_model(agent_name: str) -> str:
|
|
@@ -1077,6 +1134,38 @@ def clear_agent_pinned_model(agent_name: str):
|
|
|
1077
1134
|
set_config_value(f"agent_model_{agent_name}", "")
|
|
1078
1135
|
|
|
1079
1136
|
|
|
1137
|
+
def get_all_agent_pinned_models() -> dict:
|
|
1138
|
+
"""Get all agent-to-model pinnings from config.
|
|
1139
|
+
|
|
1140
|
+
Returns:
|
|
1141
|
+
Dict mapping agent names to their pinned model names.
|
|
1142
|
+
Only includes agents that have a pinned model (non-empty value).
|
|
1143
|
+
"""
|
|
1144
|
+
config = configparser.ConfigParser()
|
|
1145
|
+
config.read(CONFIG_FILE)
|
|
1146
|
+
|
|
1147
|
+
pinnings = {}
|
|
1148
|
+
if DEFAULT_SECTION in config:
|
|
1149
|
+
for key, value in config[DEFAULT_SECTION].items():
|
|
1150
|
+
if key.startswith("agent_model_") and value:
|
|
1151
|
+
agent_name = key[len("agent_model_") :]
|
|
1152
|
+
pinnings[agent_name] = value
|
|
1153
|
+
return pinnings
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
def get_agents_pinned_to_model(model_name: str) -> list:
|
|
1157
|
+
"""Get all agents that are pinned to a specific model.
|
|
1158
|
+
|
|
1159
|
+
Args:
|
|
1160
|
+
model_name: The model name to look up.
|
|
1161
|
+
|
|
1162
|
+
Returns:
|
|
1163
|
+
List of agent names pinned to this model.
|
|
1164
|
+
"""
|
|
1165
|
+
all_pinnings = get_all_agent_pinned_models()
|
|
1166
|
+
return [agent for agent, model in all_pinnings.items() if model == model_name]
|
|
1167
|
+
|
|
1168
|
+
|
|
1080
1169
|
def get_auto_save_session() -> bool:
|
|
1081
1170
|
"""
|
|
1082
1171
|
Checks puppy.cfg for 'auto_save_session' (case-insensitive in value only).
|
|
@@ -1178,6 +1267,84 @@ def set_diff_deletion_color(color: str):
|
|
|
1178
1267
|
set_config_value("highlight_deletion_color", color)
|
|
1179
1268
|
|
|
1180
1269
|
|
|
1270
|
+
# =============================================================================
|
|
1271
|
+
# Banner Color Configuration
|
|
1272
|
+
# =============================================================================
|
|
1273
|
+
|
|
1274
|
+
# Default banner colors (Rich color names)
|
|
1275
|
+
# A beautiful jewel-tone palette with semantic meaning:
|
|
1276
|
+
# - Blues/Teals: Reading & navigation (calm, informational)
|
|
1277
|
+
# - Warm tones: Actions & changes (edits, shell commands)
|
|
1278
|
+
# - Purples: AI thinking & reasoning (the "brain" colors)
|
|
1279
|
+
# - Greens: Completions & success
|
|
1280
|
+
# - Neutrals: Search & listings
|
|
1281
|
+
DEFAULT_BANNER_COLORS = {
|
|
1282
|
+
"thinking": "deep_sky_blue4", # Sapphire - contemplation
|
|
1283
|
+
"agent_response": "medium_purple4", # Amethyst - main AI output
|
|
1284
|
+
"shell_command": "dark_orange3", # Amber - system commands
|
|
1285
|
+
"read_file": "steel_blue", # Steel - reading files
|
|
1286
|
+
"edit_file": "dark_goldenrod", # Gold - modifications
|
|
1287
|
+
"grep": "grey37", # Silver - search results
|
|
1288
|
+
"directory_listing": "dodger_blue2", # Sky - navigation
|
|
1289
|
+
"agent_reasoning": "dark_violet", # Violet - deep thought
|
|
1290
|
+
"invoke_agent": "deep_pink4", # Ruby - agent invocation
|
|
1291
|
+
"subagent_response": "sea_green3", # Emerald - sub-agent success
|
|
1292
|
+
"list_agents": "dark_slate_gray3", # Slate - neutral listing
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
def get_banner_color(banner_name: str) -> str:
|
|
1297
|
+
"""Get the background color for a specific banner.
|
|
1298
|
+
|
|
1299
|
+
Args:
|
|
1300
|
+
banner_name: The banner identifier (e.g., 'thinking', 'agent_response')
|
|
1301
|
+
|
|
1302
|
+
Returns:
|
|
1303
|
+
Rich color name or hex code for the banner background
|
|
1304
|
+
"""
|
|
1305
|
+
config_key = f"banner_color_{banner_name}"
|
|
1306
|
+
val = get_value(config_key)
|
|
1307
|
+
if val:
|
|
1308
|
+
return val
|
|
1309
|
+
return DEFAULT_BANNER_COLORS.get(banner_name, "blue")
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
def set_banner_color(banner_name: str, color: str):
|
|
1313
|
+
"""Set the background color for a specific banner.
|
|
1314
|
+
|
|
1315
|
+
Args:
|
|
1316
|
+
banner_name: The banner identifier (e.g., 'thinking', 'agent_response')
|
|
1317
|
+
color: Rich color name or hex code
|
|
1318
|
+
"""
|
|
1319
|
+
config_key = f"banner_color_{banner_name}"
|
|
1320
|
+
set_config_value(config_key, color)
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
def get_all_banner_colors() -> dict:
|
|
1324
|
+
"""Get all banner colors (configured or default).
|
|
1325
|
+
|
|
1326
|
+
Returns:
|
|
1327
|
+
Dict mapping banner names to their colors
|
|
1328
|
+
"""
|
|
1329
|
+
return {name: get_banner_color(name) for name in DEFAULT_BANNER_COLORS}
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
def reset_banner_color(banner_name: str):
|
|
1333
|
+
"""Reset a banner color to its default.
|
|
1334
|
+
|
|
1335
|
+
Args:
|
|
1336
|
+
banner_name: The banner identifier to reset
|
|
1337
|
+
"""
|
|
1338
|
+
default_color = DEFAULT_BANNER_COLORS.get(banner_name, "blue")
|
|
1339
|
+
set_banner_color(banner_name, default_color)
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
def reset_all_banner_colors():
|
|
1343
|
+
"""Reset all banner colors to their defaults."""
|
|
1344
|
+
for name, color in DEFAULT_BANNER_COLORS.items():
|
|
1345
|
+
set_banner_color(name, color)
|
|
1346
|
+
|
|
1347
|
+
|
|
1181
1348
|
def get_current_autosave_id() -> str:
|
|
1182
1349
|
"""Get or create the current autosave session ID for this process."""
|
|
1183
1350
|
global _CURRENT_AUTOSAVE_ID
|
|
@@ -1222,11 +1389,8 @@ def auto_save_session_if_enabled() -> bool:
|
|
|
1222
1389
|
try:
|
|
1223
1390
|
import pathlib
|
|
1224
1391
|
|
|
1225
|
-
from rich.console import Console
|
|
1226
|
-
|
|
1227
1392
|
from code_puppy.agents.agent_manager import get_current_agent
|
|
1228
|
-
|
|
1229
|
-
console = Console()
|
|
1393
|
+
from code_puppy.messaging import emit_info
|
|
1230
1394
|
|
|
1231
1395
|
current_agent = get_current_agent()
|
|
1232
1396
|
history = current_agent.get_message_history()
|
|
@@ -1246,16 +1410,16 @@ def auto_save_session_if_enabled() -> bool:
|
|
|
1246
1410
|
auto_saved=True,
|
|
1247
1411
|
)
|
|
1248
1412
|
|
|
1249
|
-
|
|
1250
|
-
f"🐾
|
|
1413
|
+
emit_info(
|
|
1414
|
+
f"🐾 Auto-saved session: {metadata.message_count} messages ({metadata.total_tokens} tokens)"
|
|
1251
1415
|
)
|
|
1252
1416
|
|
|
1253
1417
|
return True
|
|
1254
1418
|
|
|
1255
1419
|
except Exception as exc: # pragma: no cover - defensive logging
|
|
1256
|
-
from
|
|
1420
|
+
from code_puppy.messaging import emit_error
|
|
1257
1421
|
|
|
1258
|
-
|
|
1422
|
+
emit_error(f"Failed to auto-save session: {exc}")
|
|
1259
1423
|
return False
|
|
1260
1424
|
|
|
1261
1425
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Error logging utility for code_puppy.
|
|
2
|
+
|
|
3
|
+
Logs unexpected errors to XDG_STATE_HOME/code_puppy/logs/ for debugging purposes.
|
|
4
|
+
Per XDG spec, logs are "state data" (actions history), not configuration.
|
|
5
|
+
Because even good puppies make mistakes sometimes! 🐶
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import traceback
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from code_puppy.config import STATE_DIR
|
|
15
|
+
|
|
16
|
+
# Logs directory within the state directory (per XDG spec, logs are state data)
|
|
17
|
+
LOGS_DIR = os.path.join(STATE_DIR, "logs")
|
|
18
|
+
ERROR_LOG_FILE = os.path.join(LOGS_DIR, "errors.log")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _ensure_logs_dir() -> None:
|
|
22
|
+
"""Create the logs directory if it doesn't exist (with 0700 perms per XDG spec)."""
|
|
23
|
+
Path(LOGS_DIR).mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def log_error(
|
|
27
|
+
error: Exception,
|
|
28
|
+
context: Optional[str] = None,
|
|
29
|
+
include_traceback: bool = True,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Log an error to the error log file.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
error: The exception to log
|
|
35
|
+
context: Optional context string describing where the error occurred
|
|
36
|
+
include_traceback: Whether to include the full traceback (default True)
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
_ensure_logs_dir()
|
|
40
|
+
|
|
41
|
+
timestamp = datetime.now().isoformat()
|
|
42
|
+
error_type = type(error).__name__
|
|
43
|
+
error_msg = str(error)
|
|
44
|
+
|
|
45
|
+
log_entry_parts = [
|
|
46
|
+
f"\n{'=' * 80}",
|
|
47
|
+
f"Timestamp: {timestamp}",
|
|
48
|
+
f"Error Type: {error_type}",
|
|
49
|
+
f"Error Message: {error_msg}",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
if context:
|
|
53
|
+
log_entry_parts.append(f"Context: {context}")
|
|
54
|
+
|
|
55
|
+
if include_traceback:
|
|
56
|
+
tb = traceback.format_exception(type(error), error, error.__traceback__)
|
|
57
|
+
log_entry_parts.append(f"Traceback:\n{''.join(tb)}")
|
|
58
|
+
|
|
59
|
+
if hasattr(error, "args") and error.args:
|
|
60
|
+
log_entry_parts.append(f"Args: {error.args}")
|
|
61
|
+
|
|
62
|
+
log_entry_parts.append(f"{'=' * 80}\n")
|
|
63
|
+
|
|
64
|
+
log_entry = "\n".join(log_entry_parts)
|
|
65
|
+
|
|
66
|
+
with open(ERROR_LOG_FILE, "a", encoding="utf-8") as f:
|
|
67
|
+
f.write(log_entry)
|
|
68
|
+
|
|
69
|
+
except Exception:
|
|
70
|
+
# If we can't log, we silently fail - don't want logging errors
|
|
71
|
+
# to cause more problems than they solve!
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def log_error_message(
|
|
76
|
+
message: str,
|
|
77
|
+
context: Optional[str] = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Log a simple error message without an exception object.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
message: The error message to log
|
|
83
|
+
context: Optional context string describing where the error occurred
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
_ensure_logs_dir()
|
|
87
|
+
|
|
88
|
+
timestamp = datetime.now().isoformat()
|
|
89
|
+
|
|
90
|
+
log_entry_parts = [
|
|
91
|
+
f"\n{'=' * 80}",
|
|
92
|
+
f"Timestamp: {timestamp}",
|
|
93
|
+
f"Message: {message}",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
if context:
|
|
97
|
+
log_entry_parts.append(f"Context: {context}")
|
|
98
|
+
|
|
99
|
+
log_entry_parts.append(f"{'=' * 80}\n")
|
|
100
|
+
|
|
101
|
+
log_entry = "\n".join(log_entry_parts)
|
|
102
|
+
|
|
103
|
+
with open(ERROR_LOG_FILE, "a", encoding="utf-8") as f:
|
|
104
|
+
f.write(log_entry)
|
|
105
|
+
|
|
106
|
+
except Exception:
|
|
107
|
+
# Silent fail - same reasoning as above
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_log_file_path() -> str:
|
|
112
|
+
"""Return the path to the error log file."""
|
|
113
|
+
return ERROR_LOG_FILE
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_logs_dir() -> str:
|
|
117
|
+
"""Return the path to the logs directory."""
|
|
118
|
+
return LOGS_DIR
|