code-puppy 0.0.193__tar.gz → 0.0.195__tar.gz
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-0.0.193 → code_puppy-0.0.195}/PKG-INFO +1 -1
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/command_handler.py +41 -71
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/config.py +118 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/main.py +15 -2
- code_puppy-0.0.195/code_puppy/session_storage.py +241 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/pyproject.toml +1 -1
- {code_puppy-0.0.193 → code_puppy-0.0.195}/.gitignore +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/LICENSE +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/README.md +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/__init__.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/__main__.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/__init__.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_c_reviewer.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_code_puppy.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_code_reviewer.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_creator_agent.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_golang_reviewer.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_manager.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_python_reviewer.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_qa_expert.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_qa_kitten.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_security_auditor.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/base_agent.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/json_agent.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/callbacks.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/__init__.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/file_path_completion.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/load_context_completion.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/__init__.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/add_command.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/base.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/handler.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/help_command.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/install_command.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/list_command.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/logs_command.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/remove_command.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/restart_command.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/search_command.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/start_all_command.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/start_command.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/status_command.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/stop_command.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/test_command.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/utils.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/model_picker_completion.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/motd.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/utils.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/http_utils.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/__init__.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/async_lifecycle.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/blocking_startup.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/captured_stdio_server.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/circuit_breaker.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/config_wizard.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/dashboard.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/error_isolation.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/examples/retry_example.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/health_monitor.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/managed_server.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/manager.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/registry.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/retry_manager.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/server_registry_catalog.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/status_tracker.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/system_tools.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/messaging/__init__.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/messaging/message_queue.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/messaging/queue_console.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/messaging/renderers.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/messaging/spinner/__init__.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/messaging/spinner/console_spinner.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/messaging/spinner/spinner_base.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/messaging/spinner/textual_spinner.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/model_factory.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/models.json +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/plugins/__init__.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/reopenable_async_client.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/round_robin_model.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/status_display.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/summarization_agent.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/__init__.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/agent_tools.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/__init__.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_control.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_interactions.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_locators.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_navigation.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_screenshot.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_scripts.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_workflows.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/camoufox_manager.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/vqa_agent.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/command_runner.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/common.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/file_modifications.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/file_operations.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/tools_content.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/__init__.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/app.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/components/__init__.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/components/chat_view.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/components/command_history_modal.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/components/copy_button.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/components/custom_widgets.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/components/human_input_modal.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/components/input_area.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/components/sidebar.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/components/status_bar.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/messages.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/models/__init__.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/models/chat_message.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/models/command_history.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/models/enums.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/screens/__init__.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/screens/help.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/screens/mcp_install_wizard.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/screens/settings.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/screens/tools.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui_state.py +0 -0
- {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/version_checker.py +0 -0
@@ -1,9 +1,12 @@
|
|
1
1
|
import os
|
2
|
+
from datetime import datetime
|
3
|
+
from pathlib import Path
|
2
4
|
|
3
5
|
from code_puppy.command_line.model_picker_completion import update_model_in_input
|
4
6
|
from code_puppy.command_line.motd import print_motd
|
5
7
|
from code_puppy.command_line.utils import make_directory_table
|
6
|
-
from code_puppy.config import get_config_keys
|
8
|
+
from code_puppy.config import CONTEXTS_DIR, get_config_keys
|
9
|
+
from code_puppy.session_storage import list_sessions, load_session, save_session
|
7
10
|
from code_puppy.tools.tools_content import tools_content
|
8
11
|
|
9
12
|
|
@@ -79,7 +82,7 @@ def get_commands_help():
|
|
79
82
|
help_lines.append(
|
80
83
|
Text("/set", style="cyan")
|
81
84
|
+ Text(
|
82
|
-
" Set puppy config key-values (e.g., /set yolo_mode true, /set
|
85
|
+
" Set puppy config key-values (e.g., /set yolo_mode true, /set auto_save_session true, /set max_saved_sessions 20)"
|
83
86
|
)
|
84
87
|
)
|
85
88
|
help_lines.append(
|
@@ -355,8 +358,13 @@ def handle_command(command: str):
|
|
355
358
|
config_keys = get_config_keys()
|
356
359
|
if "compaction_strategy" not in config_keys:
|
357
360
|
config_keys.append("compaction_strategy")
|
361
|
+
session_help = (
|
362
|
+
"\n[yellow]Session Management[/yellow]"
|
363
|
+
"\n [cyan]auto_save_session[/cyan] Auto-save chat after every response (true/false)"
|
364
|
+
"\n [cyan]max_saved_sessions[/cyan] Cap how many auto-saves to keep (0 = unlimited)"
|
365
|
+
)
|
358
366
|
emit_warning(
|
359
|
-
f"Usage: /set KEY=VALUE or /set KEY VALUE\nConfig keys: {', '.join(config_keys)}\n[dim]Note: compaction_strategy can be 'summarization' or 'truncation'[/dim]"
|
367
|
+
f"Usage: /set KEY=VALUE or /set KEY VALUE\nConfig keys: {', '.join(config_keys)}\n[dim]Note: compaction_strategy can be 'summarization' or 'truncation'[/dim]{session_help}"
|
360
368
|
)
|
361
369
|
return True
|
362
370
|
if key:
|
@@ -643,14 +651,7 @@ def handle_command(command: str):
|
|
643
651
|
return pr_prompt
|
644
652
|
|
645
653
|
if command.startswith("/dump_context"):
|
646
|
-
import json
|
647
|
-
import pickle
|
648
|
-
from datetime import datetime
|
649
|
-
from pathlib import Path
|
650
|
-
|
651
|
-
# estimate_tokens_for_message has been moved to BaseAgent class
|
652
654
|
from code_puppy.agents.agent_manager import get_current_agent
|
653
|
-
from code_puppy.config import CONFIG_DIR
|
654
655
|
|
655
656
|
tokens = command.split()
|
656
657
|
if len(tokens) != 2:
|
@@ -665,49 +666,26 @@ def handle_command(command: str):
|
|
665
666
|
emit_warning("No message history to dump!")
|
666
667
|
return True
|
667
668
|
|
668
|
-
# Create contexts directory inside CONFIG_DIR if it doesn't exist
|
669
|
-
contexts_dir = Path(CONFIG_DIR) / "contexts"
|
670
|
-
contexts_dir.mkdir(parents=True, exist_ok=True)
|
671
|
-
|
672
669
|
try:
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
current_agent = get_current_agent()
|
681
|
-
metadata = {
|
682
|
-
"session_name": session_name,
|
683
|
-
"timestamp": datetime.now().isoformat(),
|
684
|
-
"message_count": len(history),
|
685
|
-
"total_tokens": sum(
|
686
|
-
current_agent.estimate_tokens_for_message(m) for m in history
|
687
|
-
),
|
688
|
-
"file_path": str(pickle_file),
|
689
|
-
}
|
690
|
-
|
691
|
-
with open(meta_file, "w") as f:
|
692
|
-
json.dump(metadata, f, indent=2)
|
693
|
-
|
670
|
+
metadata = save_session(
|
671
|
+
history=history,
|
672
|
+
session_name=session_name,
|
673
|
+
base_dir=Path(CONTEXTS_DIR),
|
674
|
+
timestamp=datetime.now().isoformat(),
|
675
|
+
token_estimator=agent.estimate_tokens_for_message,
|
676
|
+
)
|
694
677
|
emit_success(
|
695
|
-
f"✅ Context saved: {
|
696
|
-
f"📁 Files: {
|
678
|
+
f"✅ Context saved: {metadata.message_count} messages ({metadata.total_tokens} tokens)\n"
|
679
|
+
f"📁 Files: {metadata.pickle_path}, {metadata.metadata_path}"
|
697
680
|
)
|
698
681
|
return True
|
699
682
|
|
700
|
-
except Exception as
|
701
|
-
emit_error(f"Failed to dump context: {
|
683
|
+
except Exception as exc:
|
684
|
+
emit_error(f"Failed to dump context: {exc}")
|
702
685
|
return True
|
703
686
|
|
704
687
|
if command.startswith("/load_context"):
|
705
|
-
import pickle
|
706
|
-
from pathlib import Path
|
707
|
-
|
708
|
-
# estimate_tokens_for_message has been moved to BaseAgent class
|
709
688
|
from code_puppy.agents.agent_manager import get_current_agent
|
710
|
-
from code_puppy.config import CONFIG_DIR
|
711
689
|
|
712
690
|
tokens = command.split()
|
713
691
|
if len(tokens) != 2:
|
@@ -715,38 +693,30 @@ def handle_command(command: str):
|
|
715
693
|
return True
|
716
694
|
|
717
695
|
session_name = tokens[1]
|
718
|
-
contexts_dir = Path(
|
719
|
-
|
696
|
+
contexts_dir = Path(CONTEXTS_DIR)
|
697
|
+
session_path = contexts_dir / f"{session_name}.pkl"
|
720
698
|
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
699
|
+
try:
|
700
|
+
history = load_session(session_name, contexts_dir)
|
701
|
+
except FileNotFoundError:
|
702
|
+
emit_error(f"Context file not found: {session_path}")
|
703
|
+
available = list_sessions(contexts_dir)
|
725
704
|
if available:
|
726
|
-
|
727
|
-
emit_info(f"Available contexts: {', '.join(names)}")
|
705
|
+
emit_info(f"Available contexts: {', '.join(available)}")
|
728
706
|
return True
|
729
|
-
|
730
|
-
|
731
|
-
with open(pickle_file, "rb") as f:
|
732
|
-
history = pickle.load(f)
|
733
|
-
|
734
|
-
agent = get_current_agent()
|
735
|
-
agent.set_message_history(history)
|
736
|
-
current_agent = get_current_agent()
|
737
|
-
total_tokens = sum(
|
738
|
-
current_agent.estimate_tokens_for_message(m) for m in history
|
739
|
-
)
|
740
|
-
|
741
|
-
emit_success(
|
742
|
-
f"✅ Context loaded: {len(history)} messages ({total_tokens} tokens)\n"
|
743
|
-
f"📁 From: {pickle_file}"
|
744
|
-
)
|
707
|
+
except Exception as exc:
|
708
|
+
emit_error(f"Failed to load context: {exc}")
|
745
709
|
return True
|
746
710
|
|
747
|
-
|
748
|
-
|
749
|
-
|
711
|
+
agent = get_current_agent()
|
712
|
+
agent.set_message_history(history)
|
713
|
+
total_tokens = sum(agent.estimate_tokens_for_message(m) for m in history)
|
714
|
+
|
715
|
+
emit_success(
|
716
|
+
f"✅ Context loaded: {len(history)} messages ({total_tokens} tokens)\n"
|
717
|
+
f"📁 From: {session_path}"
|
718
|
+
)
|
719
|
+
return True
|
750
720
|
|
751
721
|
if command.startswith("/truncate"):
|
752
722
|
from code_puppy.agents.agent_manager import get_current_agent
|
@@ -1,8 +1,11 @@
|
|
1
1
|
import configparser
|
2
|
+
import datetime
|
2
3
|
import json
|
3
4
|
import os
|
4
5
|
import pathlib
|
5
6
|
|
7
|
+
from code_puppy.session_storage import cleanup_sessions, save_session
|
8
|
+
|
6
9
|
CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".code_puppy")
|
7
10
|
CONFIG_FILE = os.path.join(CONFIG_DIR, "puppy.cfg")
|
8
11
|
MCP_SERVERS_FILE = os.path.join(CONFIG_DIR, "mcp_servers.json")
|
@@ -10,6 +13,8 @@ COMMAND_HISTORY_FILE = os.path.join(CONFIG_DIR, "command_history.txt")
|
|
10
13
|
MODELS_FILE = os.path.join(CONFIG_DIR, "models.json")
|
11
14
|
EXTRA_MODELS_FILE = os.path.join(CONFIG_DIR, "extra_models.json")
|
12
15
|
AGENTS_DIR = os.path.join(CONFIG_DIR, "agents")
|
16
|
+
CONTEXTS_DIR = os.path.join(CONFIG_DIR, "contexts")
|
17
|
+
AUTOSAVE_DIR = os.path.join(CONFIG_DIR, "autosaves")
|
13
18
|
|
14
19
|
DEFAULT_SECTION = "puppy"
|
15
20
|
REQUIRED_KEYS = ["puppy_name", "owner_name"]
|
@@ -121,6 +126,8 @@ def get_config_keys():
|
|
121
126
|
"message_limit",
|
122
127
|
"allow_recursion",
|
123
128
|
"openai_reasoning_effort",
|
129
|
+
"auto_save_session",
|
130
|
+
"max_saved_sessions",
|
124
131
|
]
|
125
132
|
config = configparser.ConfigParser()
|
126
133
|
config.read(CONFIG_FILE)
|
@@ -645,3 +652,114 @@ def clear_agent_pinned_model(agent_name: str):
|
|
645
652
|
# We can't easily delete keys from configparser, so set to empty string
|
646
653
|
# which will be treated as None by get_agent_pinned_model
|
647
654
|
set_config_value(f"agent_model_{agent_name}", "")
|
655
|
+
|
656
|
+
|
657
|
+
def get_auto_save_session() -> bool:
|
658
|
+
"""
|
659
|
+
Checks puppy.cfg for 'auto_save_session' (case-insensitive in value only).
|
660
|
+
Defaults to True if not set.
|
661
|
+
Allowed values for ON: 1, '1', 'true', 'yes', 'on' (all case-insensitive for value).
|
662
|
+
"""
|
663
|
+
true_vals = {"1", "true", "yes", "on"}
|
664
|
+
cfg_val = get_value("auto_save_session")
|
665
|
+
if cfg_val is not None:
|
666
|
+
if str(cfg_val).strip().lower() in true_vals:
|
667
|
+
return True
|
668
|
+
return False
|
669
|
+
return True
|
670
|
+
|
671
|
+
|
672
|
+
def set_auto_save_session(enabled: bool):
|
673
|
+
"""Sets the auto_save_session configuration value.
|
674
|
+
|
675
|
+
Args:
|
676
|
+
enabled: Whether to enable auto-saving of sessions
|
677
|
+
"""
|
678
|
+
set_config_value("auto_save_session", "true" if enabled else "false")
|
679
|
+
|
680
|
+
|
681
|
+
def get_max_saved_sessions() -> int:
|
682
|
+
"""
|
683
|
+
Gets the maximum number of sessions to keep.
|
684
|
+
Defaults to 20 if not set.
|
685
|
+
"""
|
686
|
+
cfg_val = get_value("max_saved_sessions")
|
687
|
+
if cfg_val is not None:
|
688
|
+
try:
|
689
|
+
val = int(cfg_val)
|
690
|
+
return max(0, val) # Ensure non-negative
|
691
|
+
except (ValueError, TypeError):
|
692
|
+
pass
|
693
|
+
return 20
|
694
|
+
|
695
|
+
|
696
|
+
def set_max_saved_sessions(max_sessions: int):
|
697
|
+
"""Sets the max_saved_sessions configuration value.
|
698
|
+
|
699
|
+
Args:
|
700
|
+
max_sessions: Maximum number of sessions to keep (0 for unlimited)
|
701
|
+
"""
|
702
|
+
set_config_value("max_saved_sessions", str(max_sessions))
|
703
|
+
|
704
|
+
|
705
|
+
def _cleanup_old_sessions():
|
706
|
+
"""Remove oldest auto-saved sessions if we exceed the max_saved_sessions limit."""
|
707
|
+
max_sessions = get_max_saved_sessions()
|
708
|
+
if max_sessions <= 0:
|
709
|
+
return
|
710
|
+
|
711
|
+
autosave_dir = pathlib.Path(AUTOSAVE_DIR)
|
712
|
+
removed_sessions = cleanup_sessions(autosave_dir, max_sessions)
|
713
|
+
if not removed_sessions:
|
714
|
+
return
|
715
|
+
|
716
|
+
from rich.console import Console
|
717
|
+
|
718
|
+
console = Console()
|
719
|
+
for session_name in removed_sessions:
|
720
|
+
console.print(f"[dim]🗑️ Removed old session: {session_name}.pkl[/dim]")
|
721
|
+
|
722
|
+
|
723
|
+
def auto_save_session_if_enabled() -> bool:
|
724
|
+
"""Automatically save the current session if auto_save_session is enabled."""
|
725
|
+
if not get_auto_save_session():
|
726
|
+
return False
|
727
|
+
|
728
|
+
try:
|
729
|
+
import pathlib
|
730
|
+
from rich.console import Console
|
731
|
+
|
732
|
+
from code_puppy.agents.agent_manager import get_current_agent
|
733
|
+
|
734
|
+
console = Console()
|
735
|
+
|
736
|
+
current_agent = get_current_agent()
|
737
|
+
history = current_agent.get_message_history()
|
738
|
+
if not history:
|
739
|
+
return False
|
740
|
+
|
741
|
+
now = datetime.datetime.now()
|
742
|
+
session_name = f"auto_session_{now.strftime('%Y%m%d_%H%M%S')}"
|
743
|
+
autosave_dir = pathlib.Path(AUTOSAVE_DIR)
|
744
|
+
|
745
|
+
metadata = save_session(
|
746
|
+
history=history,
|
747
|
+
session_name=session_name,
|
748
|
+
base_dir=autosave_dir,
|
749
|
+
timestamp=now.isoformat(),
|
750
|
+
token_estimator=current_agent.estimate_tokens_for_message,
|
751
|
+
auto_saved=True,
|
752
|
+
)
|
753
|
+
|
754
|
+
console.print(
|
755
|
+
f"🐾 [dim]Auto-saved session: {metadata.message_count} messages ({metadata.total_tokens} tokens)[/dim]"
|
756
|
+
)
|
757
|
+
|
758
|
+
_cleanup_old_sessions()
|
759
|
+
return True
|
760
|
+
|
761
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
762
|
+
from rich.console import Console
|
763
|
+
|
764
|
+
Console().print(f"[dim]❌ Failed to auto-save session: {exc}[/dim]")
|
765
|
+
return False
|
@@ -1,10 +1,13 @@
|
|
1
1
|
import argparse
|
2
2
|
import asyncio
|
3
|
+
import json
|
3
4
|
import os
|
4
5
|
import subprocess
|
5
6
|
import sys
|
6
7
|
import time
|
7
8
|
import webbrowser
|
9
|
+
from datetime import datetime
|
10
|
+
from pathlib import Path
|
8
11
|
|
9
12
|
from rich.console import Console, ConsoleOptions, RenderResult
|
10
13
|
from rich.markdown import CodeBlock, Markdown
|
@@ -18,11 +21,13 @@ from code_puppy.command_line.prompt_toolkit_completion import (
|
|
18
21
|
get_prompt_with_active_model,
|
19
22
|
)
|
20
23
|
from code_puppy.config import (
|
24
|
+
AUTOSAVE_DIR,
|
21
25
|
COMMAND_HISTORY_FILE,
|
22
26
|
ensure_config_exists,
|
23
27
|
initialize_command_history_file,
|
24
28
|
save_command_to_history,
|
25
29
|
)
|
30
|
+
from code_puppy.session_storage import list_sessions, load_session, restore_autosave_interactively
|
26
31
|
from code_puppy.http_utils import find_available_port
|
27
32
|
from code_puppy.tools.common import console
|
28
33
|
|
@@ -288,6 +293,8 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
|
|
288
293
|
from code_puppy.messaging import emit_info
|
289
294
|
|
290
295
|
emit_info("[bold cyan]Initializing agent...[/bold cyan]")
|
296
|
+
|
297
|
+
|
291
298
|
# Initialize the runtime agent manager
|
292
299
|
if initial_command:
|
293
300
|
from code_puppy.agents import get_current_agent
|
@@ -367,6 +374,8 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
|
|
367
374
|
emit_error(f"Error installing prompt_toolkit: {e}")
|
368
375
|
emit_warning("Falling back to basic input without tab completion")
|
369
376
|
|
377
|
+
await restore_autosave_interactively(Path(AUTOSAVE_DIR))
|
378
|
+
|
370
379
|
while True:
|
371
380
|
from code_puppy.agents.agent_manager import get_current_agent
|
372
381
|
from code_puppy.messaging import emit_info
|
@@ -456,11 +465,15 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
|
|
456
465
|
f"\n[bold purple]AGENT RESPONSE: [/bold purple]\n{agent_response}"
|
457
466
|
)
|
458
467
|
|
468
|
+
# Auto-save session if enabled
|
469
|
+
from code_puppy.config import auto_save_session_if_enabled
|
470
|
+
auto_save_session_if_enabled()
|
471
|
+
|
459
472
|
# Ensure console output is flushed before next prompt
|
460
473
|
# This fixes the issue where prompt doesn't appear after agent response
|
461
474
|
display_console.file.flush() if hasattr(
|
462
475
|
display_console.file, "flush"
|
463
|
-
|
476
|
+
) else None
|
464
477
|
import time
|
465
478
|
|
466
479
|
time.sleep(0.1) # Brief pause to ensure all messages are rendered
|
@@ -592,4 +605,4 @@ def main_entry():
|
|
592
605
|
|
593
606
|
|
594
607
|
if __name__ == "__main__":
|
595
|
-
main_entry()
|
608
|
+
main_entry()
|
@@ -0,0 +1,241 @@
|
|
1
|
+
"""Shared helpers for persisting and restoring chat sessions.
|
2
|
+
|
3
|
+
This module centralises the pickle + metadata handling that used to live in
|
4
|
+
both the CLI command handler and the auto-save feature. Keeping it here helps
|
5
|
+
us avoid duplication while staying inside the Zen-of-Python sweet spot: simple
|
6
|
+
is better than complex, nested side effects are worse than deliberate helpers.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from __future__ import annotations
|
10
|
+
|
11
|
+
import json
|
12
|
+
import pickle
|
13
|
+
from dataclasses import dataclass
|
14
|
+
from pathlib import Path
|
15
|
+
from typing import Any, Callable, List
|
16
|
+
|
17
|
+
SessionHistory = List[Any]
|
18
|
+
TokenEstimator = Callable[[Any], int]
|
19
|
+
|
20
|
+
|
21
|
+
@dataclass(slots=True)
|
22
|
+
class SessionPaths:
|
23
|
+
pickle_path: Path
|
24
|
+
metadata_path: Path
|
25
|
+
|
26
|
+
|
27
|
+
@dataclass(slots=True)
|
28
|
+
class SessionMetadata:
|
29
|
+
session_name: str
|
30
|
+
timestamp: str
|
31
|
+
message_count: int
|
32
|
+
total_tokens: int
|
33
|
+
pickle_path: Path
|
34
|
+
metadata_path: Path
|
35
|
+
auto_saved: bool = False
|
36
|
+
|
37
|
+
def as_serialisable(self) -> dict[str, Any]:
|
38
|
+
return {
|
39
|
+
"session_name": self.session_name,
|
40
|
+
"timestamp": self.timestamp,
|
41
|
+
"message_count": self.message_count,
|
42
|
+
"total_tokens": self.total_tokens,
|
43
|
+
"file_path": str(self.pickle_path),
|
44
|
+
"auto_saved": self.auto_saved,
|
45
|
+
}
|
46
|
+
|
47
|
+
|
48
|
+
def ensure_directory(path: Path) -> Path:
|
49
|
+
path.mkdir(parents=True, exist_ok=True)
|
50
|
+
return path
|
51
|
+
|
52
|
+
|
53
|
+
def build_session_paths(base_dir: Path, session_name: str) -> SessionPaths:
|
54
|
+
pickle_path = base_dir / f"{session_name}.pkl"
|
55
|
+
metadata_path = base_dir / f"{session_name}_meta.json"
|
56
|
+
return SessionPaths(pickle_path=pickle_path, metadata_path=metadata_path)
|
57
|
+
|
58
|
+
|
59
|
+
def save_session(
|
60
|
+
*,
|
61
|
+
history: SessionHistory,
|
62
|
+
session_name: str,
|
63
|
+
base_dir: Path,
|
64
|
+
timestamp: str,
|
65
|
+
token_estimator: TokenEstimator,
|
66
|
+
auto_saved: bool = False,
|
67
|
+
) -> SessionMetadata:
|
68
|
+
ensure_directory(base_dir)
|
69
|
+
paths = build_session_paths(base_dir, session_name)
|
70
|
+
|
71
|
+
with paths.pickle_path.open("wb") as pickle_file:
|
72
|
+
pickle.dump(history, pickle_file)
|
73
|
+
|
74
|
+
total_tokens = sum(token_estimator(message) for message in history)
|
75
|
+
metadata = SessionMetadata(
|
76
|
+
session_name=session_name,
|
77
|
+
timestamp=timestamp,
|
78
|
+
message_count=len(history),
|
79
|
+
total_tokens=total_tokens,
|
80
|
+
pickle_path=paths.pickle_path,
|
81
|
+
metadata_path=paths.metadata_path,
|
82
|
+
auto_saved=auto_saved,
|
83
|
+
)
|
84
|
+
|
85
|
+
with paths.metadata_path.open("w", encoding="utf-8") as metadata_file:
|
86
|
+
json.dump(metadata.as_serialisable(), metadata_file, indent=2)
|
87
|
+
|
88
|
+
return metadata
|
89
|
+
|
90
|
+
|
91
|
+
def load_session(session_name: str, base_dir: Path) -> SessionHistory:
|
92
|
+
paths = build_session_paths(base_dir, session_name)
|
93
|
+
if not paths.pickle_path.exists():
|
94
|
+
raise FileNotFoundError(paths.pickle_path)
|
95
|
+
with paths.pickle_path.open("rb") as pickle_file:
|
96
|
+
return pickle.load(pickle_file)
|
97
|
+
|
98
|
+
|
99
|
+
def cleanup_sessions(base_dir: Path, max_sessions: int) -> List[str]:
|
100
|
+
if max_sessions <= 0:
|
101
|
+
return []
|
102
|
+
|
103
|
+
if not base_dir.exists():
|
104
|
+
return []
|
105
|
+
|
106
|
+
candidate_paths = list(base_dir.glob("*.pkl"))
|
107
|
+
if len(candidate_paths) <= max_sessions:
|
108
|
+
return []
|
109
|
+
|
110
|
+
sorted_candidates = sorted(
|
111
|
+
((path.stat().st_mtime, path) for path in candidate_paths),
|
112
|
+
key=lambda item: item[0],
|
113
|
+
)
|
114
|
+
|
115
|
+
stale_entries = sorted_candidates[:-max_sessions]
|
116
|
+
removed_sessions: List[str] = []
|
117
|
+
for _, pickle_path in stale_entries:
|
118
|
+
metadata_path = base_dir / f"{pickle_path.stem}_meta.json"
|
119
|
+
try:
|
120
|
+
pickle_path.unlink(missing_ok=True)
|
121
|
+
metadata_path.unlink(missing_ok=True)
|
122
|
+
removed_sessions.append(pickle_path.stem)
|
123
|
+
except OSError:
|
124
|
+
continue
|
125
|
+
|
126
|
+
return removed_sessions
|
127
|
+
|
128
|
+
|
129
|
+
def list_sessions(base_dir: Path) -> List[str]:
|
130
|
+
if not base_dir.exists():
|
131
|
+
return []
|
132
|
+
return sorted(path.stem for path in base_dir.glob("*.pkl"))
|
133
|
+
|
134
|
+
|
135
|
+
async def restore_autosave_interactively(base_dir: Path) -> None:
|
136
|
+
"""Prompt the user to load an autosave session from base_dir, if any exist.
|
137
|
+
|
138
|
+
This helper is deliberately placed in session_storage to keep autosave
|
139
|
+
restoration close to the persistence layer. It uses the same public APIs
|
140
|
+
(list_sessions, load_session) and mirrors the interactive behaviours from
|
141
|
+
the command handler.
|
142
|
+
"""
|
143
|
+
sessions = list_sessions(base_dir)
|
144
|
+
if not sessions:
|
145
|
+
return
|
146
|
+
|
147
|
+
# Import locally to avoid pulling the messaging layer into storage modules
|
148
|
+
from datetime import datetime
|
149
|
+
from prompt_toolkit.formatted_text import FormattedText
|
150
|
+
|
151
|
+
from code_puppy.agents.agent_manager import get_current_agent
|
152
|
+
from code_puppy.command_line.prompt_toolkit_completion import (
|
153
|
+
get_input_with_combined_completion,
|
154
|
+
)
|
155
|
+
from code_puppy.messaging import emit_success, emit_system_message, emit_warning
|
156
|
+
|
157
|
+
entries = []
|
158
|
+
for name in sessions:
|
159
|
+
meta_path = base_dir / f"{name}_meta.json"
|
160
|
+
try:
|
161
|
+
with meta_path.open("r", encoding="utf-8") as meta_file:
|
162
|
+
data = json.load(meta_file)
|
163
|
+
timestamp = data.get("timestamp")
|
164
|
+
message_count = data.get("message_count")
|
165
|
+
except Exception:
|
166
|
+
timestamp = None
|
167
|
+
message_count = None
|
168
|
+
entries.append((name, timestamp, message_count))
|
169
|
+
|
170
|
+
def sort_key(entry):
|
171
|
+
_, timestamp, _ = entry
|
172
|
+
if timestamp:
|
173
|
+
try:
|
174
|
+
return datetime.fromisoformat(timestamp)
|
175
|
+
except ValueError:
|
176
|
+
return datetime.min
|
177
|
+
return datetime.min
|
178
|
+
|
179
|
+
entries.sort(key=sort_key, reverse=True)
|
180
|
+
top_entries = entries[:5]
|
181
|
+
|
182
|
+
emit_system_message("[bold magenta]Autosave Sessions Available:[/bold magenta]")
|
183
|
+
for index, (name, timestamp, message_count) in enumerate(top_entries, start=1):
|
184
|
+
timestamp_display = timestamp or "unknown time"
|
185
|
+
message_display = (
|
186
|
+
f"{message_count} messages" if message_count is not None else "unknown size"
|
187
|
+
)
|
188
|
+
emit_system_message(
|
189
|
+
f" [{index}] {name} ({message_display}, saved at {timestamp_display})"
|
190
|
+
)
|
191
|
+
|
192
|
+
if len(entries) > len(top_entries):
|
193
|
+
emit_system_message(
|
194
|
+
f" [dim]...and {len(entries) - len(top_entries)} more autosaves[/dim]"
|
195
|
+
)
|
196
|
+
|
197
|
+
try:
|
198
|
+
selection = await get_input_with_combined_completion(
|
199
|
+
FormattedText([("class:prompt", "Load autosave (number, name, or Enter to skip): ")])
|
200
|
+
)
|
201
|
+
except (KeyboardInterrupt, EOFError):
|
202
|
+
emit_warning("Autosave selection cancelled")
|
203
|
+
return
|
204
|
+
|
205
|
+
selection = selection.strip()
|
206
|
+
if not selection:
|
207
|
+
return
|
208
|
+
|
209
|
+
chosen_name = None
|
210
|
+
if selection.isdigit():
|
211
|
+
idx = int(selection) - 1
|
212
|
+
if 0 <= idx < len(top_entries):
|
213
|
+
chosen_name = top_entries[idx][0]
|
214
|
+
else:
|
215
|
+
for name, _, _ in entries:
|
216
|
+
if name == selection:
|
217
|
+
chosen_name = name
|
218
|
+
break
|
219
|
+
|
220
|
+
if not chosen_name:
|
221
|
+
emit_warning("No autosave loaded (invalid selection)")
|
222
|
+
return
|
223
|
+
|
224
|
+
try:
|
225
|
+
history = load_session(chosen_name, base_dir)
|
226
|
+
except FileNotFoundError:
|
227
|
+
emit_warning(f"Autosave '{chosen_name}' could not be found")
|
228
|
+
return
|
229
|
+
except Exception as exc:
|
230
|
+
emit_warning(f"Failed to load autosave '{chosen_name}': {exc}")
|
231
|
+
return
|
232
|
+
|
233
|
+
agent = get_current_agent()
|
234
|
+
agent.set_message_history(history)
|
235
|
+
total_tokens = sum(agent.estimate_tokens_for_message(msg) for msg in history)
|
236
|
+
|
237
|
+
session_path = base_dir / f"{chosen_name}.pkl"
|
238
|
+
emit_success(
|
239
|
+
f"✅ Autosave loaded: {len(history)} messages ({total_tokens} tokens)\n"
|
240
|
+
f"📁 From: {session_path}"
|
241
|
+
)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/load_context_completion.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/model_picker_completion.py
RENAMED
File without changes
|
File without changes
|
{code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/prompt_toolkit_completion.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/components/command_history_modal.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|