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.
Files changed (128) hide show
  1. {code_puppy-0.0.194 → code_puppy-0.0.195}/PKG-INFO +1 -1
  2. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/command_handler.py +40 -82
  3. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/config.py +46 -93
  4. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/main.py +9 -0
  5. code_puppy-0.0.195/code_puppy/session_storage.py +241 -0
  6. {code_puppy-0.0.194 → code_puppy-0.0.195}/pyproject.toml +1 -1
  7. {code_puppy-0.0.194 → code_puppy-0.0.195}/.gitignore +0 -0
  8. {code_puppy-0.0.194 → code_puppy-0.0.195}/LICENSE +0 -0
  9. {code_puppy-0.0.194 → code_puppy-0.0.195}/README.md +0 -0
  10. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/__init__.py +0 -0
  11. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/__main__.py +0 -0
  12. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/__init__.py +0 -0
  13. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_c_reviewer.py +0 -0
  14. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_code_puppy.py +0 -0
  15. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_code_reviewer.py +0 -0
  16. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
  17. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_creator_agent.py +0 -0
  18. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_golang_reviewer.py +0 -0
  19. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
  20. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_manager.py +0 -0
  21. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_python_reviewer.py +0 -0
  22. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_qa_expert.py +0 -0
  23. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_qa_kitten.py +0 -0
  24. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_security_auditor.py +0 -0
  25. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
  26. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/base_agent.py +0 -0
  27. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/agents/json_agent.py +0 -0
  28. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/callbacks.py +0 -0
  29. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/__init__.py +0 -0
  30. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/file_path_completion.py +0 -0
  31. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/load_context_completion.py +0 -0
  32. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/__init__.py +0 -0
  33. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/add_command.py +0 -0
  34. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/base.py +0 -0
  35. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/handler.py +0 -0
  36. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/help_command.py +0 -0
  37. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/install_command.py +0 -0
  38. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/list_command.py +0 -0
  39. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/logs_command.py +0 -0
  40. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/remove_command.py +0 -0
  41. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/restart_command.py +0 -0
  42. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/search_command.py +0 -0
  43. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  44. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/start_command.py +0 -0
  45. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/status_command.py +0 -0
  46. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  47. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/stop_command.py +0 -0
  48. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/test_command.py +0 -0
  49. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/utils.py +0 -0
  50. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  51. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/model_picker_completion.py +0 -0
  52. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/motd.py +0 -0
  53. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
  54. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/command_line/utils.py +0 -0
  55. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/http_utils.py +0 -0
  56. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/__init__.py +0 -0
  57. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/async_lifecycle.py +0 -0
  58. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/blocking_startup.py +0 -0
  59. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/captured_stdio_server.py +0 -0
  60. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/circuit_breaker.py +0 -0
  61. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/config_wizard.py +0 -0
  62. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/dashboard.py +0 -0
  63. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/error_isolation.py +0 -0
  64. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/examples/retry_example.py +0 -0
  65. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/health_monitor.py +0 -0
  66. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/managed_server.py +0 -0
  67. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/manager.py +0 -0
  68. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/registry.py +0 -0
  69. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/retry_manager.py +0 -0
  70. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  71. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/status_tracker.py +0 -0
  72. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/mcp_/system_tools.py +0 -0
  73. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/messaging/__init__.py +0 -0
  74. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/messaging/message_queue.py +0 -0
  75. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/messaging/queue_console.py +0 -0
  76. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/messaging/renderers.py +0 -0
  77. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/messaging/spinner/__init__.py +0 -0
  78. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  79. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  80. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/messaging/spinner/textual_spinner.py +0 -0
  81. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/model_factory.py +0 -0
  82. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/models.json +0 -0
  83. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/plugins/__init__.py +0 -0
  84. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
  85. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/reopenable_async_client.py +0 -0
  86. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/round_robin_model.py +0 -0
  87. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/status_display.py +0 -0
  88. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/summarization_agent.py +0 -0
  89. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/__init__.py +0 -0
  90. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/agent_tools.py +0 -0
  91. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/__init__.py +0 -0
  92. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_control.py +0 -0
  93. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_interactions.py +0 -0
  94. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_locators.py +0 -0
  95. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_navigation.py +0 -0
  96. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  97. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_scripts.py +0 -0
  98. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/browser_workflows.py +0 -0
  99. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/camoufox_manager.py +0 -0
  100. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/browser/vqa_agent.py +0 -0
  101. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/command_runner.py +0 -0
  102. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/common.py +0 -0
  103. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/file_modifications.py +0 -0
  104. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/file_operations.py +0 -0
  105. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tools/tools_content.py +0 -0
  106. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/__init__.py +0 -0
  107. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/app.py +0 -0
  108. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/components/__init__.py +0 -0
  109. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/components/chat_view.py +0 -0
  110. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/components/command_history_modal.py +0 -0
  111. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/components/copy_button.py +0 -0
  112. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/components/custom_widgets.py +0 -0
  113. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/components/human_input_modal.py +0 -0
  114. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/components/input_area.py +0 -0
  115. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/components/sidebar.py +0 -0
  116. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/components/status_bar.py +0 -0
  117. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/messages.py +0 -0
  118. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/models/__init__.py +0 -0
  119. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/models/chat_message.py +0 -0
  120. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/models/command_history.py +0 -0
  121. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/models/enums.py +0 -0
  122. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/screens/__init__.py +0 -0
  123. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/screens/help.py +0 -0
  124. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/screens/mcp_install_wizard.py +0 -0
  125. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/screens/settings.py +0 -0
  126. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui/screens/tools.py +0 -0
  127. {code_puppy-0.0.194 → code_puppy-0.0.195}/code_puppy/tui_state.py +0 -0
  128. {code_puppy-0.0.194 → 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.194
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
 
@@ -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
- # Save as pickle for exact preservation
686
- pickle_file = contexts_dir / f"{session_name}.pkl"
687
- with open(pickle_file, "wb") as f:
688
- pickle.dump(history, f)
689
-
690
- # Also save metadata as JSON for readability
691
- meta_file = contexts_dir / f"{session_name}_meta.json"
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: {len(history)} messages ({metadata['total_tokens']} tokens)\n"
708
- 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}"
709
680
  )
710
681
  return True
711
682
 
712
- except Exception as e:
713
- emit_error(f"Failed to dump context: {e}")
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(CONFIG_DIR) / "contexts"
731
- pickle_file = contexts_dir / f"{session_name}.pkl"
696
+ contexts_dir = Path(CONTEXTS_DIR)
697
+ session_path = contexts_dir / f"{session_name}.pkl"
732
698
 
733
- if not pickle_file.exists():
734
- emit_error(f"Context file not found: {pickle_file}")
735
- # List available contexts
736
- 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)
737
704
  if available:
738
- names = [f.stem for f in available]
739
- emit_info(f"Available contexts: {', '.join(names)}")
705
+ emit_info(f"Available contexts: {', '.join(available)}")
740
706
  return True
741
-
742
- try:
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
- except Exception as e:
760
- emit_error(f"Failed to load context: {e}")
761
- 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
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: # 0 means unlimited
708
+ if max_sessions <= 0:
704
709
  return
705
-
706
- from pathlib import Path
707
-
708
- contexts_dir = Path(CONFIG_DIR) / "contexts"
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
- # Get all .pkl files (session files) and sort by modification time
713
- session_files = []
714
- for pkl_file in contexts_dir.glob("*.pkl"):
715
- try:
716
- session_files.append((pkl_file.stat().st_mtime, pkl_file))
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 datetime
757
- import json
758
- import pickle
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
- # Get current agent and message history
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 # No history to save
768
-
769
- # Create timestamp-based session name
770
- timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
771
- session_name = f"auto_session_{timestamp}"
772
-
773
- # Create contexts directory if it doesn't exist
774
- contexts_dir = Path(CONFIG_DIR) / "contexts"
775
- contexts_dir.mkdir(parents=True, exist_ok=True)
776
-
777
- # Save as pickle for exact preservation
778
- pickle_file = contexts_dir / f"{session_name}.pkl"
779
- with open(pickle_file, "wb") as f:
780
- pickle.dump(history, f)
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: {len(history)} messages ({metadata['total_tokens']} tokens)[/dim]"
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 e:
760
+
761
+ except Exception as exc: # pragma: no cover - defensive logging
809
762
  from rich.console import Console
810
- console = Console()
811
- console.print(f"[dim]❌ Failed to auto-save session: {str(e)}[/dim]")
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
+ )
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "code-puppy"
7
- version = "0.0.194"
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