code-puppy 0.0.194__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.194 → code_puppy-0.0.195}/PKG-INFO +1 -1
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/command_handler.py +40 -82
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/config.py +46 -93
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/main.py +9 -0
- code_puppy-0.0.195/code_puppy/session_storage.py +241 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/pyproject.toml +1 -1
- {code_puppy-0.0.194 → code_puppy-0.0.195}/.gitignore +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/LICENSE +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/README.md +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/__init__.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/__main__.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/__init__.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_c_reviewer.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_code_puppy.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_code_reviewer.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_creator_agent.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_golang_reviewer.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_manager.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_python_reviewer.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_qa_expert.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_qa_kitten.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_security_auditor.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/base_agent.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/json_agent.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/callbacks.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/__init__.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/file_path_completion.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/load_context_completion.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/__init__.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/add_command.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/base.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/handler.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/help_command.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/install_command.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/list_command.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/logs_command.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/remove_command.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/restart_command.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/search_command.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/start_all_command.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/start_command.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/status_command.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/stop_command.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/test_command.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/utils.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/model_picker_completion.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/motd.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/utils.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/http_utils.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/__init__.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/async_lifecycle.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/blocking_startup.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/captured_stdio_server.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/circuit_breaker.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/config_wizard.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/dashboard.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/error_isolation.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/examples/retry_example.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/health_monitor.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/managed_server.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/manager.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/registry.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/retry_manager.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/server_registry_catalog.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/status_tracker.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/system_tools.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/messaging/__init__.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/messaging/message_queue.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/messaging/queue_console.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/messaging/renderers.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/messaging/spinner/__init__.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/messaging/spinner/console_spinner.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/messaging/spinner/spinner_base.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/messaging/spinner/textual_spinner.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/model_factory.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/models.json +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/plugins/__init__.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/reopenable_async_client.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/round_robin_model.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/status_display.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/summarization_agent.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/__init__.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/agent_tools.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/__init__.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_control.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_interactions.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_locators.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_navigation.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_screenshot.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_scripts.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_workflows.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/camoufox_manager.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/vqa_agent.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/command_runner.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/common.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/file_modifications.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/file_operations.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/tools_content.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/__init__.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/app.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/components/__init__.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/components/chat_view.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/components/command_history_modal.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/components/copy_button.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/components/custom_widgets.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/components/human_input_modal.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/components/input_area.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/components/sidebar.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/components/status_bar.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/messages.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/models/__init__.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/models/chat_message.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/models/command_history.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/models/enums.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/screens/__init__.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/screens/help.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/screens/mcp_install_wizard.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/screens/settings.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/screens/tools.py +0 -0
- {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui_state.py +0 -0
- {code_puppy-0.0.194 → 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
|
|
@@ -76,18 +79,6 @@ def get_commands_help():
|
|
76
79
|
Text("/load_context", style="cyan")
|
77
80
|
+ Text(" <name> Load message history from file")
|
78
81
|
)
|
79
|
-
help_lines.append(
|
80
|
-
Text("", style="cyan")
|
81
|
-
+ Text("Session Management:", style="bold yellow")
|
82
|
-
)
|
83
|
-
help_lines.append(
|
84
|
-
Text("auto_save_session", style="cyan")
|
85
|
-
+ Text(" Auto-save session after each response (true/false)")
|
86
|
-
)
|
87
|
-
help_lines.append(
|
88
|
-
Text("max_saved_sessions", style="cyan")
|
89
|
-
+ Text(" Maximum number of sessions to keep (default: 20, 0 = unlimited)")
|
90
|
-
)
|
91
82
|
help_lines.append(
|
92
83
|
Text("/set", style="cyan")
|
93
84
|
+ Text(
|
@@ -367,8 +358,13 @@ def handle_command(command: str):
|
|
367
358
|
config_keys = get_config_keys()
|
368
359
|
if "compaction_strategy" not in config_keys:
|
369
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
|
+
)
|
370
366
|
emit_warning(
|
371
|
-
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}"
|
372
368
|
)
|
373
369
|
return True
|
374
370
|
if key:
|
@@ -655,14 +651,7 @@ def handle_command(command: str):
|
|
655
651
|
return pr_prompt
|
656
652
|
|
657
653
|
if command.startswith("/dump_context"):
|
658
|
-
import json
|
659
|
-
import pickle
|
660
|
-
from datetime import datetime
|
661
|
-
from pathlib import Path
|
662
|
-
|
663
|
-
# estimate_tokens_for_message has been moved to BaseAgent class
|
664
654
|
from code_puppy.agents.agent_manager import get_current_agent
|
665
|
-
from code_puppy.config import CONFIG_DIR
|
666
655
|
|
667
656
|
tokens = command.split()
|
668
657
|
if len(tokens) != 2:
|
@@ -677,49 +666,26 @@ def handle_command(command: str):
|
|
677
666
|
emit_warning("No message history to dump!")
|
678
667
|
return True
|
679
668
|
|
680
|
-
# Create contexts directory inside CONFIG_DIR if it doesn't exist
|
681
|
-
contexts_dir = Path(CONFIG_DIR) / "contexts"
|
682
|
-
contexts_dir.mkdir(parents=True, exist_ok=True)
|
683
|
-
|
684
669
|
try:
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
current_agent = get_current_agent()
|
693
|
-
metadata = {
|
694
|
-
"session_name": session_name,
|
695
|
-
"timestamp": datetime.now().isoformat(),
|
696
|
-
"message_count": len(history),
|
697
|
-
"total_tokens": sum(
|
698
|
-
current_agent.estimate_tokens_for_message(m) for m in history
|
699
|
-
),
|
700
|
-
"file_path": str(pickle_file),
|
701
|
-
}
|
702
|
-
|
703
|
-
with open(meta_file, "w") as f:
|
704
|
-
json.dump(metadata, f, indent=2)
|
705
|
-
|
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
|
+
)
|
706
677
|
emit_success(
|
707
|
-
f"✅ Context saved: {
|
708
|
-
f"📁 Files: {
|
678
|
+
f"✅ Context saved: {metadata.message_count} messages ({metadata.total_tokens} tokens)\n"
|
679
|
+
f"📁 Files: {metadata.pickle_path}, {metadata.metadata_path}"
|
709
680
|
)
|
710
681
|
return True
|
711
682
|
|
712
|
-
except Exception as
|
713
|
-
emit_error(f"Failed to dump context: {
|
683
|
+
except Exception as exc:
|
684
|
+
emit_error(f"Failed to dump context: {exc}")
|
714
685
|
return True
|
715
686
|
|
716
687
|
if command.startswith("/load_context"):
|
717
|
-
import pickle
|
718
|
-
from pathlib import Path
|
719
|
-
|
720
|
-
# estimate_tokens_for_message has been moved to BaseAgent class
|
721
688
|
from code_puppy.agents.agent_manager import get_current_agent
|
722
|
-
from code_puppy.config import CONFIG_DIR
|
723
689
|
|
724
690
|
tokens = command.split()
|
725
691
|
if len(tokens) != 2:
|
@@ -727,38 +693,30 @@ def handle_command(command: str):
|
|
727
693
|
return True
|
728
694
|
|
729
695
|
session_name = tokens[1]
|
730
|
-
contexts_dir = Path(
|
731
|
-
|
696
|
+
contexts_dir = Path(CONTEXTS_DIR)
|
697
|
+
session_path = contexts_dir / f"{session_name}.pkl"
|
732
698
|
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
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)
|
737
704
|
if available:
|
738
|
-
|
739
|
-
emit_info(f"Available contexts: {', '.join(names)}")
|
705
|
+
emit_info(f"Available contexts: {', '.join(available)}")
|
740
706
|
return True
|
741
|
-
|
742
|
-
|
743
|
-
with open(pickle_file, "rb") as f:
|
744
|
-
history = pickle.load(f)
|
745
|
-
|
746
|
-
agent = get_current_agent()
|
747
|
-
agent.set_message_history(history)
|
748
|
-
current_agent = get_current_agent()
|
749
|
-
total_tokens = sum(
|
750
|
-
current_agent.estimate_tokens_for_message(m) for m in history
|
751
|
-
)
|
752
|
-
|
753
|
-
emit_success(
|
754
|
-
f"✅ Context loaded: {len(history)} messages ({total_tokens} tokens)\n"
|
755
|
-
f"📁 From: {pickle_file}"
|
756
|
-
)
|
707
|
+
except Exception as exc:
|
708
|
+
emit_error(f"Failed to load context: {exc}")
|
757
709
|
return True
|
758
710
|
|
759
|
-
|
760
|
-
|
761
|
-
|
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
|
762
720
|
|
763
721
|
if command.startswith("/truncate"):
|
764
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"]
|
@@ -698,115 +703,63 @@ def set_max_saved_sessions(max_sessions: int):
|
|
698
703
|
|
699
704
|
|
700
705
|
def _cleanup_old_sessions():
|
701
|
-
"""Remove oldest sessions if we exceed the max_saved_sessions limit."""
|
706
|
+
"""Remove oldest auto-saved sessions if we exceed the max_saved_sessions limit."""
|
702
707
|
max_sessions = get_max_saved_sessions()
|
703
|
-
if max_sessions <= 0:
|
708
|
+
if max_sessions <= 0:
|
704
709
|
return
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
if not contexts_dir.exists():
|
710
|
+
|
711
|
+
autosave_dir = pathlib.Path(AUTOSAVE_DIR)
|
712
|
+
removed_sessions = cleanup_sessions(autosave_dir, max_sessions)
|
713
|
+
if not removed_sessions:
|
710
714
|
return
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
except OSError:
|
718
|
-
continue
|
719
|
-
|
720
|
-
# Sort by modification time (oldest first)
|
721
|
-
session_files.sort(key=lambda x: x[0])
|
722
|
-
|
723
|
-
# If we have more than max_sessions, remove the oldest ones
|
724
|
-
if len(session_files) > max_sessions:
|
725
|
-
files_to_remove = session_files[:-max_sessions] # All except the last max_sessions
|
726
|
-
|
727
|
-
from rich.console import Console
|
728
|
-
console = Console()
|
729
|
-
|
730
|
-
for _, old_file in files_to_remove:
|
731
|
-
try:
|
732
|
-
# Remove the .pkl file
|
733
|
-
old_file.unlink()
|
734
|
-
|
735
|
-
# Also remove the corresponding _meta.json file if it exists
|
736
|
-
meta_file = contexts_dir / f"{old_file.stem}_meta.json"
|
737
|
-
if meta_file.exists():
|
738
|
-
meta_file.unlink()
|
739
|
-
|
740
|
-
console.print(f"[dim]🗑️ Removed old session: {old_file.name}[/dim]")
|
741
|
-
|
742
|
-
except OSError as e:
|
743
|
-
console.print(f"[dim]❌ Failed to remove {old_file.name}: {e}[/dim]")
|
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]")
|
744
721
|
|
745
722
|
|
746
723
|
def auto_save_session_if_enabled() -> bool:
|
747
|
-
"""Automatically save the current session if auto_save_session is enabled.
|
748
|
-
|
749
|
-
Returns:
|
750
|
-
True if session was saved, False otherwise
|
751
|
-
"""
|
724
|
+
"""Automatically save the current session if auto_save_session is enabled."""
|
752
725
|
if not get_auto_save_session():
|
753
726
|
return False
|
754
|
-
|
727
|
+
|
755
728
|
try:
|
756
|
-
import
|
757
|
-
import
|
758
|
-
|
759
|
-
from pathlib import Path
|
729
|
+
import pathlib
|
730
|
+
from rich.console import Console
|
731
|
+
|
760
732
|
from code_puppy.agents.agent_manager import get_current_agent
|
761
|
-
|
762
|
-
|
733
|
+
|
734
|
+
console = Console()
|
735
|
+
|
763
736
|
current_agent = get_current_agent()
|
764
737
|
history = current_agent.get_message_history()
|
765
|
-
|
766
738
|
if not history:
|
767
|
-
return False
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
# Also save metadata as JSON for readability
|
783
|
-
meta_file = contexts_dir / f"{session_name}_meta.json"
|
784
|
-
metadata = {
|
785
|
-
"session_name": session_name,
|
786
|
-
"timestamp": datetime.datetime.now().isoformat(),
|
787
|
-
"message_count": len(history),
|
788
|
-
"total_tokens": sum(
|
789
|
-
current_agent.estimate_tokens_for_message(m) for m in history
|
790
|
-
),
|
791
|
-
"file_path": str(pickle_file),
|
792
|
-
"auto_saved": True,
|
793
|
-
}
|
794
|
-
|
795
|
-
with open(meta_file, "w") as f:
|
796
|
-
json.dump(metadata, f, indent=2)
|
797
|
-
|
798
|
-
from rich.console import Console
|
799
|
-
console = Console()
|
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
|
+
|
800
754
|
console.print(
|
801
|
-
f"🐾 [dim]Auto-saved session: {
|
755
|
+
f"🐾 [dim]Auto-saved session: {metadata.message_count} messages ({metadata.total_tokens} tokens)[/dim]"
|
802
756
|
)
|
803
|
-
|
804
|
-
# Cleanup old sessions if limit is set
|
757
|
+
|
805
758
|
_cleanup_old_sessions()
|
806
759
|
return True
|
807
|
-
|
808
|
-
except Exception as
|
760
|
+
|
761
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
809
762
|
from rich.console import Console
|
810
|
-
|
811
|
-
|
763
|
+
|
764
|
+
Console().print(f"[dim]❌ Failed to auto-save session: {exc}[/dim]")
|
812
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
|
@@ -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.194 → 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.194 → code_puppy-0.0.195}/code_puppy/command_line/model_picker_completion.py
RENAMED
File without changes
|
File without changes
|
{code_puppy-0.0.194 → 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.194 → 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
|