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.
Files changed (128) hide show
  1. {code_puppy-0.0.193 → code_puppy-0.0.195}/PKG-INFO +1 -1
  2. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/command_handler.py +41 -71
  3. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/config.py +118 -0
  4. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/main.py +15 -2
  5. code_puppy-0.0.195/code_puppy/session_storage.py +241 -0
  6. {code_puppy-0.0.193 → code_puppy-0.0.195}/pyproject.toml +1 -1
  7. {code_puppy-0.0.193 → code_puppy-0.0.195}/.gitignore +0 -0
  8. {code_puppy-0.0.193 → code_puppy-0.0.195}/LICENSE +0 -0
  9. {code_puppy-0.0.193 → code_puppy-0.0.195}/README.md +0 -0
  10. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/__init__.py +0 -0
  11. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/__main__.py +0 -0
  12. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/__init__.py +0 -0
  13. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_c_reviewer.py +0 -0
  14. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_code_puppy.py +0 -0
  15. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_code_reviewer.py +0 -0
  16. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
  17. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_creator_agent.py +0 -0
  18. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_golang_reviewer.py +0 -0
  19. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
  20. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_manager.py +0 -0
  21. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_python_reviewer.py +0 -0
  22. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_qa_expert.py +0 -0
  23. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_qa_kitten.py +0 -0
  24. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_security_auditor.py +0 -0
  25. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
  26. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/base_agent.py +0 -0
  27. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/agents/json_agent.py +0 -0
  28. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/callbacks.py +0 -0
  29. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/__init__.py +0 -0
  30. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/file_path_completion.py +0 -0
  31. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/load_context_completion.py +0 -0
  32. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/__init__.py +0 -0
  33. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/add_command.py +0 -0
  34. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/base.py +0 -0
  35. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/handler.py +0 -0
  36. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/help_command.py +0 -0
  37. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/install_command.py +0 -0
  38. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/list_command.py +0 -0
  39. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/logs_command.py +0 -0
  40. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/remove_command.py +0 -0
  41. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/restart_command.py +0 -0
  42. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/search_command.py +0 -0
  43. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  44. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/start_command.py +0 -0
  45. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/status_command.py +0 -0
  46. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  47. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/stop_command.py +0 -0
  48. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/test_command.py +0 -0
  49. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/utils.py +0 -0
  50. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  51. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/model_picker_completion.py +0 -0
  52. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/motd.py +0 -0
  53. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
  54. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/command_line/utils.py +0 -0
  55. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/http_utils.py +0 -0
  56. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/__init__.py +0 -0
  57. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/async_lifecycle.py +0 -0
  58. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/blocking_startup.py +0 -0
  59. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/captured_stdio_server.py +0 -0
  60. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/circuit_breaker.py +0 -0
  61. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/config_wizard.py +0 -0
  62. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/dashboard.py +0 -0
  63. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/error_isolation.py +0 -0
  64. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/examples/retry_example.py +0 -0
  65. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/health_monitor.py +0 -0
  66. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/managed_server.py +0 -0
  67. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/manager.py +0 -0
  68. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/registry.py +0 -0
  69. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/retry_manager.py +0 -0
  70. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  71. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/status_tracker.py +0 -0
  72. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/mcp_/system_tools.py +0 -0
  73. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/messaging/__init__.py +0 -0
  74. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/messaging/message_queue.py +0 -0
  75. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/messaging/queue_console.py +0 -0
  76. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/messaging/renderers.py +0 -0
  77. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/messaging/spinner/__init__.py +0 -0
  78. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  79. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  80. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/messaging/spinner/textual_spinner.py +0 -0
  81. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/model_factory.py +0 -0
  82. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/models.json +0 -0
  83. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/plugins/__init__.py +0 -0
  84. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
  85. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/reopenable_async_client.py +0 -0
  86. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/round_robin_model.py +0 -0
  87. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/status_display.py +0 -0
  88. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/summarization_agent.py +0 -0
  89. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/__init__.py +0 -0
  90. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/agent_tools.py +0 -0
  91. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/__init__.py +0 -0
  92. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_control.py +0 -0
  93. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_interactions.py +0 -0
  94. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_locators.py +0 -0
  95. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_navigation.py +0 -0
  96. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  97. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_scripts.py +0 -0
  98. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_workflows.py +0 -0
  99. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/camoufox_manager.py +0 -0
  100. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/browser/vqa_agent.py +0 -0
  101. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/command_runner.py +0 -0
  102. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/common.py +0 -0
  103. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/file_modifications.py +0 -0
  104. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/file_operations.py +0 -0
  105. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tools/tools_content.py +0 -0
  106. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/__init__.py +0 -0
  107. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/app.py +0 -0
  108. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/components/__init__.py +0 -0
  109. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/components/chat_view.py +0 -0
  110. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/components/command_history_modal.py +0 -0
  111. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/components/copy_button.py +0 -0
  112. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/components/custom_widgets.py +0 -0
  113. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/components/human_input_modal.py +0 -0
  114. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/components/input_area.py +0 -0
  115. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/components/sidebar.py +0 -0
  116. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/components/status_bar.py +0 -0
  117. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/messages.py +0 -0
  118. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/models/__init__.py +0 -0
  119. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/models/chat_message.py +0 -0
  120. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/models/command_history.py +0 -0
  121. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/models/enums.py +0 -0
  122. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/screens/__init__.py +0 -0
  123. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/screens/help.py +0 -0
  124. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/screens/mcp_install_wizard.py +0 -0
  125. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/screens/settings.py +0 -0
  126. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui/screens/tools.py +0 -0
  127. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/tui_state.py +0 -0
  128. {code_puppy-0.0.193 → code_puppy-0.0.195}/code_puppy/version_checker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.193
3
+ Version: 0.0.195
4
4
  Summary: Code generation agent
5
5
  Project-URL: repository, https://github.com/mpfaffenberger/code_puppy
6
6
  Project-URL: HomePage, https://github.com/mpfaffenberger/code_puppy
@@ -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 compaction_strategy truncation)"
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
- # Save as pickle for exact preservation
674
- pickle_file = contexts_dir / f"{session_name}.pkl"
675
- with open(pickle_file, "wb") as f:
676
- pickle.dump(history, f)
677
-
678
- # Also save metadata as JSON for readability
679
- meta_file = contexts_dir / f"{session_name}_meta.json"
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: {len(history)} messages ({metadata['total_tokens']} tokens)\n"
696
- f"📁 Files: {pickle_file}, {meta_file}"
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 e:
701
- emit_error(f"Failed to dump context: {e}")
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(CONFIG_DIR) / "contexts"
719
- pickle_file = contexts_dir / f"{session_name}.pkl"
696
+ contexts_dir = Path(CONTEXTS_DIR)
697
+ session_path = contexts_dir / f"{session_name}.pkl"
720
698
 
721
- if not pickle_file.exists():
722
- emit_error(f"Context file not found: {pickle_file}")
723
- # List available contexts
724
- available = list(contexts_dir.glob("*.pkl"))
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
- names = [f.stem for f in available]
727
- emit_info(f"Available contexts: {', '.join(names)}")
705
+ emit_info(f"Available contexts: {', '.join(available)}")
728
706
  return True
729
-
730
- try:
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
- except Exception as e:
748
- emit_error(f"Failed to load context: {e}")
749
- return True
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
- ) else None
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
+ )
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "code-puppy"
7
- version = "0.0.193"
7
+ version = "0.0.195"
8
8
  description = "Code generation agent"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
File without changes
File without changes
File without changes