code-puppy 0.0.194__tar.gz → 0.0.196__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.196}/PKG-INFO +1 -1
  2. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/command_handler.py +71 -82
  3. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/config.py +72 -98
  4. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/main.py +9 -0
  5. code_puppy-0.0.196/code_puppy/session_storage.py +250 -0
  6. {code_puppy-0.0.194 → code_puppy-0.0.196}/pyproject.toml +1 -1
  7. {code_puppy-0.0.194 → code_puppy-0.0.196}/.gitignore +0 -0
  8. {code_puppy-0.0.194 → code_puppy-0.0.196}/LICENSE +0 -0
  9. {code_puppy-0.0.194 → code_puppy-0.0.196}/README.md +0 -0
  10. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/__init__.py +0 -0
  11. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/__main__.py +0 -0
  12. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/agents/__init__.py +0 -0
  13. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/agents/agent_c_reviewer.py +0 -0
  14. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/agents/agent_code_puppy.py +0 -0
  15. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/agents/agent_code_reviewer.py +0 -0
  16. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
  17. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/agents/agent_creator_agent.py +0 -0
  18. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/agents/agent_golang_reviewer.py +0 -0
  19. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
  20. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/agents/agent_manager.py +0 -0
  21. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/agents/agent_python_reviewer.py +0 -0
  22. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/agents/agent_qa_expert.py +0 -0
  23. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/agents/agent_qa_kitten.py +0 -0
  24. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/agents/agent_security_auditor.py +0 -0
  25. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
  26. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/agents/base_agent.py +0 -0
  27. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/agents/json_agent.py +0 -0
  28. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/callbacks.py +0 -0
  29. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/__init__.py +0 -0
  30. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/file_path_completion.py +0 -0
  31. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/load_context_completion.py +0 -0
  32. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/mcp/__init__.py +0 -0
  33. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/mcp/add_command.py +0 -0
  34. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/mcp/base.py +0 -0
  35. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/mcp/handler.py +0 -0
  36. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/mcp/help_command.py +0 -0
  37. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/mcp/install_command.py +0 -0
  38. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/mcp/list_command.py +0 -0
  39. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/mcp/logs_command.py +0 -0
  40. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/mcp/remove_command.py +0 -0
  41. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/mcp/restart_command.py +0 -0
  42. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/mcp/search_command.py +0 -0
  43. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  44. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/mcp/start_command.py +0 -0
  45. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/mcp/status_command.py +0 -0
  46. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  47. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/mcp/stop_command.py +0 -0
  48. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/mcp/test_command.py +0 -0
  49. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/mcp/utils.py +0 -0
  50. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  51. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/model_picker_completion.py +0 -0
  52. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/motd.py +0 -0
  53. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
  54. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/command_line/utils.py +0 -0
  55. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/http_utils.py +0 -0
  56. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/mcp_/__init__.py +0 -0
  57. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/mcp_/async_lifecycle.py +0 -0
  58. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/mcp_/blocking_startup.py +0 -0
  59. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/mcp_/captured_stdio_server.py +0 -0
  60. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/mcp_/circuit_breaker.py +0 -0
  61. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/mcp_/config_wizard.py +0 -0
  62. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/mcp_/dashboard.py +0 -0
  63. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/mcp_/error_isolation.py +0 -0
  64. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/mcp_/examples/retry_example.py +0 -0
  65. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/mcp_/health_monitor.py +0 -0
  66. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/mcp_/managed_server.py +0 -0
  67. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/mcp_/manager.py +0 -0
  68. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/mcp_/registry.py +0 -0
  69. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/mcp_/retry_manager.py +0 -0
  70. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  71. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/mcp_/status_tracker.py +0 -0
  72. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/mcp_/system_tools.py +0 -0
  73. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/messaging/__init__.py +0 -0
  74. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/messaging/message_queue.py +0 -0
  75. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/messaging/queue_console.py +0 -0
  76. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/messaging/renderers.py +0 -0
  77. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/messaging/spinner/__init__.py +0 -0
  78. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  79. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  80. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/messaging/spinner/textual_spinner.py +0 -0
  81. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/model_factory.py +0 -0
  82. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/models.json +0 -0
  83. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/plugins/__init__.py +0 -0
  84. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
  85. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/reopenable_async_client.py +0 -0
  86. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/round_robin_model.py +0 -0
  87. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/status_display.py +0 -0
  88. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/summarization_agent.py +0 -0
  89. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tools/__init__.py +0 -0
  90. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tools/agent_tools.py +0 -0
  91. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tools/browser/__init__.py +0 -0
  92. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tools/browser/browser_control.py +0 -0
  93. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tools/browser/browser_interactions.py +0 -0
  94. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tools/browser/browser_locators.py +0 -0
  95. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tools/browser/browser_navigation.py +0 -0
  96. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  97. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tools/browser/browser_scripts.py +0 -0
  98. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tools/browser/browser_workflows.py +0 -0
  99. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tools/browser/camoufox_manager.py +0 -0
  100. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tools/browser/vqa_agent.py +0 -0
  101. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tools/command_runner.py +0 -0
  102. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tools/common.py +0 -0
  103. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tools/file_modifications.py +0 -0
  104. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tools/file_operations.py +0 -0
  105. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tools/tools_content.py +0 -0
  106. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/__init__.py +0 -0
  107. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/app.py +0 -0
  108. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/components/__init__.py +0 -0
  109. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/components/chat_view.py +0 -0
  110. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/components/command_history_modal.py +0 -0
  111. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/components/copy_button.py +0 -0
  112. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/components/custom_widgets.py +0 -0
  113. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/components/human_input_modal.py +0 -0
  114. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/components/input_area.py +0 -0
  115. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/components/sidebar.py +0 -0
  116. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/components/status_bar.py +0 -0
  117. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/messages.py +0 -0
  118. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/models/__init__.py +0 -0
  119. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/models/chat_message.py +0 -0
  120. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/models/command_history.py +0 -0
  121. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/models/enums.py +0 -0
  122. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/screens/__init__.py +0 -0
  123. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/screens/help.py +0 -0
  124. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/screens/mcp_install_wizard.py +0 -0
  125. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/screens/settings.py +0 -0
  126. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui/screens/tools.py +0 -0
  127. {code_puppy-0.0.194 → code_puppy-0.0.196}/code_puppy/tui_state.py +0 -0
  128. {code_puppy-0.0.194 → code_puppy-0.0.196}/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.196
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,22 +79,10 @@ 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(
94
- " Set puppy config key-values (e.g., /set yolo_mode true, /set auto_save_session true, /set max_saved_sessions 20)"
85
+ " Set puppy config key-values (e.g., /set yolo_mode true, /set auto_save_session true)"
95
86
  )
96
87
  )
97
88
  help_lines.append(
@@ -345,6 +336,30 @@ def handle_command(command: str):
345
336
  )
346
337
  return True
347
338
 
339
+ if command.startswith("/session"):
340
+ # /session id -> show current autosave id
341
+ # /session new -> rotate autosave id
342
+ tokens = command.split()
343
+ from code_puppy.config import (
344
+ AUTOSAVE_DIR,
345
+ get_current_autosave_id,
346
+ get_current_autosave_session_name,
347
+ rotate_autosave_id,
348
+ )
349
+ if len(tokens) == 1 or tokens[1] == "id":
350
+ sid = get_current_autosave_id()
351
+ emit_info(
352
+ f"[bold magenta]Autosave Session[/bold magenta]: {sid}\n"
353
+ f"Files prefix: {Path(AUTOSAVE_DIR) / get_current_autosave_session_name()}"
354
+ )
355
+ return True
356
+ if tokens[1] == "new":
357
+ new_sid = rotate_autosave_id()
358
+ emit_success(f"New autosave session id: {new_sid}")
359
+ return True
360
+ emit_warning("Usage: /session [id|new]")
361
+ return True
362
+
348
363
  if command.startswith("/set"):
349
364
  # Syntax: /set KEY=VALUE or /set KEY VALUE
350
365
  from code_puppy.config import set_config_value
@@ -367,8 +382,12 @@ def handle_command(command: str):
367
382
  config_keys = get_config_keys()
368
383
  if "compaction_strategy" not in config_keys:
369
384
  config_keys.append("compaction_strategy")
385
+ session_help = (
386
+ "\n[yellow]Session Management[/yellow]"
387
+ "\n [cyan]auto_save_session[/cyan] Auto-save chat after every response (true/false)"
388
+ )
370
389
  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]"
390
+ 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
391
  )
373
392
  return True
374
393
  if key:
@@ -655,14 +674,7 @@ def handle_command(command: str):
655
674
  return pr_prompt
656
675
 
657
676
  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
677
  from code_puppy.agents.agent_manager import get_current_agent
665
- from code_puppy.config import CONFIG_DIR
666
678
 
667
679
  tokens = command.split()
668
680
  if len(tokens) != 2:
@@ -677,49 +689,26 @@ def handle_command(command: str):
677
689
  emit_warning("No message history to dump!")
678
690
  return True
679
691
 
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
692
  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
-
693
+ metadata = save_session(
694
+ history=history,
695
+ session_name=session_name,
696
+ base_dir=Path(CONTEXTS_DIR),
697
+ timestamp=datetime.now().isoformat(),
698
+ token_estimator=agent.estimate_tokens_for_message,
699
+ )
706
700
  emit_success(
707
- f"✅ Context saved: {len(history)} messages ({metadata['total_tokens']} tokens)\n"
708
- f"📁 Files: {pickle_file}, {meta_file}"
701
+ f"✅ Context saved: {metadata.message_count} messages ({metadata.total_tokens} tokens)\n"
702
+ f"📁 Files: {metadata.pickle_path}, {metadata.metadata_path}"
709
703
  )
710
704
  return True
711
705
 
712
- except Exception as e:
713
- emit_error(f"Failed to dump context: {e}")
706
+ except Exception as exc:
707
+ emit_error(f"Failed to dump context: {exc}")
714
708
  return True
715
709
 
716
710
  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
711
  from code_puppy.agents.agent_manager import get_current_agent
722
- from code_puppy.config import CONFIG_DIR
723
712
 
724
713
  tokens = command.split()
725
714
  if len(tokens) != 2:
@@ -727,38 +716,38 @@ def handle_command(command: str):
727
716
  return True
728
717
 
729
718
  session_name = tokens[1]
730
- contexts_dir = Path(CONFIG_DIR) / "contexts"
731
- pickle_file = contexts_dir / f"{session_name}.pkl"
719
+ contexts_dir = Path(CONTEXTS_DIR)
720
+ session_path = contexts_dir / f"{session_name}.pkl"
732
721
 
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"))
722
+ try:
723
+ history = load_session(session_name, contexts_dir)
724
+ except FileNotFoundError:
725
+ emit_error(f"Context file not found: {session_path}")
726
+ available = list_sessions(contexts_dir)
737
727
  if available:
738
- names = [f.stem for f in available]
739
- emit_info(f"Available contexts: {', '.join(names)}")
728
+ emit_info(f"Available contexts: {', '.join(available)}")
729
+ return True
730
+ except Exception as exc:
731
+ emit_error(f"Failed to load context: {exc}")
740
732
  return True
741
733
 
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
- )
734
+ agent = get_current_agent()
735
+ agent.set_message_history(history)
736
+ total_tokens = sum(agent.estimate_tokens_for_message(m) for m in history)
752
737
 
753
- emit_success(
754
- f"✅ Context loaded: {len(history)} messages ({total_tokens} tokens)\n"
755
- f"📁 From: {pickle_file}"
756
- )
757
- return True
738
+ # Rotate autosave id to avoid overwriting any existing autosave
739
+ try:
740
+ from code_puppy.config import rotate_autosave_id
741
+ new_id = rotate_autosave_id()
742
+ autosave_info = f"\n[dim]Autosave session rotated to: {new_id}[/dim]"
743
+ except Exception:
744
+ autosave_info = ""
758
745
 
759
- except Exception as e:
760
- emit_error(f"Failed to load context: {e}")
761
- return True
746
+ emit_success(
747
+ f" Context loaded: {len(history)} messages ({total_tokens} tokens)\n"
748
+ f"📁 From: {session_path}{autosave_info}"
749
+ )
750
+ return True
762
751
 
763
752
  if command.startswith("/truncate"):
764
753
  from code_puppy.agents.agent_manager import get_current_agent
@@ -1,7 +1,11 @@
1
1
  import configparser
2
+ import datetime
2
3
  import json
3
4
  import os
4
5
  import pathlib
6
+ from typing import Optional
7
+
8
+ from code_puppy.session_storage import save_session
5
9
 
6
10
  CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".code_puppy")
7
11
  CONFIG_FILE = os.path.join(CONFIG_DIR, "puppy.cfg")
@@ -10,10 +14,15 @@ COMMAND_HISTORY_FILE = os.path.join(CONFIG_DIR, "command_history.txt")
10
14
  MODELS_FILE = os.path.join(CONFIG_DIR, "models.json")
11
15
  EXTRA_MODELS_FILE = os.path.join(CONFIG_DIR, "extra_models.json")
12
16
  AGENTS_DIR = os.path.join(CONFIG_DIR, "agents")
17
+ CONTEXTS_DIR = os.path.join(CONFIG_DIR, "contexts")
18
+ AUTOSAVE_DIR = os.path.join(CONFIG_DIR, "autosaves")
13
19
 
14
20
  DEFAULT_SECTION = "puppy"
15
21
  REQUIRED_KEYS = ["puppy_name", "owner_name"]
16
22
 
23
+ # Runtime-only autosave session ID (per-process)
24
+ _CURRENT_AUTOSAVE_ID: Optional[str] = None
25
+
17
26
  # Cache containers for model validation and defaults
18
27
  _model_validation_cache = {}
19
28
  _default_model_cache = None
@@ -697,116 +706,81 @@ def set_max_saved_sessions(max_sessions: int):
697
706
  set_config_value("max_saved_sessions", str(max_sessions))
698
707
 
699
708
 
700
- def _cleanup_old_sessions():
701
- """Remove oldest sessions if we exceed the max_saved_sessions limit."""
702
- max_sessions = get_max_saved_sessions()
703
- if max_sessions <= 0: # 0 means unlimited
704
- return
705
-
706
- from pathlib import Path
707
-
708
- contexts_dir = Path(CONFIG_DIR) / "contexts"
709
- if not contexts_dir.exists():
710
- 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]")
709
+ def get_current_autosave_id() -> str:
710
+ """Get or create the current autosave session ID for this process."""
711
+ global _CURRENT_AUTOSAVE_ID
712
+ if not _CURRENT_AUTOSAVE_ID:
713
+ # Use a full timestamp so tests and UX can predict the name if needed
714
+ _CURRENT_AUTOSAVE_ID = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
715
+ return _CURRENT_AUTOSAVE_ID
744
716
 
745
717
 
746
- 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
718
+ def rotate_autosave_id() -> str:
719
+ """Force a new autosave session ID and return it."""
720
+ global _CURRENT_AUTOSAVE_ID
721
+ _CURRENT_AUTOSAVE_ID = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
722
+ return _CURRENT_AUTOSAVE_ID
723
+
724
+
725
+ def get_current_autosave_session_name() -> str:
726
+ """Return the full session name used for autosaves (no file extension)."""
727
+ return f"auto_session_{get_current_autosave_id()}"
728
+
729
+
730
+ def set_current_autosave_from_session_name(session_name: str) -> str:
731
+ """Set the current autosave ID based on a full session name.
732
+
733
+ Accepts names like 'auto_session_YYYYMMDD_HHMMSS' and extracts the ID part.
734
+ Returns the ID that was set.
751
735
  """
736
+ global _CURRENT_AUTOSAVE_ID
737
+ prefix = "auto_session_"
738
+ if session_name.startswith(prefix):
739
+ _CURRENT_AUTOSAVE_ID = session_name[len(prefix):]
740
+ else:
741
+ _CURRENT_AUTOSAVE_ID = session_name
742
+ return _CURRENT_AUTOSAVE_ID
743
+
744
+
745
+ def auto_save_session_if_enabled() -> bool:
746
+ """Automatically save the current session if auto_save_session is enabled."""
752
747
  if not get_auto_save_session():
753
748
  return False
754
-
749
+
755
750
  try:
756
- import datetime
757
- import json
758
- import pickle
759
- from pathlib import Path
751
+ import pathlib
752
+ from rich.console import Console
753
+
760
754
  from code_puppy.agents.agent_manager import get_current_agent
761
-
762
- # Get current agent and message history
755
+
756
+ console = Console()
757
+
763
758
  current_agent = get_current_agent()
764
759
  history = current_agent.get_message_history()
765
-
766
760
  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()
761
+ return False
762
+
763
+ now = datetime.datetime.now()
764
+ session_name = get_current_autosave_session_name()
765
+ autosave_dir = pathlib.Path(AUTOSAVE_DIR)
766
+
767
+ metadata = save_session(
768
+ history=history,
769
+ session_name=session_name,
770
+ base_dir=autosave_dir,
771
+ timestamp=now.isoformat(),
772
+ token_estimator=current_agent.estimate_tokens_for_message,
773
+ auto_saved=True,
774
+ )
775
+
800
776
  console.print(
801
- f"🐾 [dim]Auto-saved session: {len(history)} messages ({metadata['total_tokens']} tokens)[/dim]"
777
+ f"🐾 [dim]Auto-saved session: {metadata.message_count} messages ({metadata.total_tokens} tokens)[/dim]"
802
778
  )
803
-
804
- # Cleanup old sessions if limit is set
805
- _cleanup_old_sessions()
779
+
806
780
  return True
807
-
808
- except Exception as e:
781
+
782
+ except Exception as exc: # pragma: no cover - defensive logging
809
783
  from rich.console import Console
810
- console = Console()
811
- console.print(f"[dim]❌ Failed to auto-save session: {str(e)}[/dim]")
784
+
785
+ Console().print(f"[dim]❌ Failed to auto-save session: {exc}[/dim]")
812
786
  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,250 @@
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 list_sessions(base_dir: Path) -> List[str]:
100
+ if not base_dir.exists():
101
+ return []
102
+ return sorted(path.stem for path in base_dir.glob("*.pkl"))
103
+
104
+
105
+ def cleanup_sessions(base_dir: Path, max_sessions: int) -> List[str]:
106
+ if max_sessions <= 0:
107
+ return []
108
+
109
+ if not base_dir.exists():
110
+ return []
111
+
112
+ candidate_paths = list(base_dir.glob("*.pkl"))
113
+ if len(candidate_paths) <= max_sessions:
114
+ return []
115
+
116
+ sorted_candidates = sorted(
117
+ ((path.stat().st_mtime, path) for path in candidate_paths),
118
+ key=lambda item: item[0],
119
+ )
120
+
121
+ stale_entries = sorted_candidates[:-max_sessions]
122
+ removed_sessions: List[str] = []
123
+ for _, pickle_path in stale_entries:
124
+ metadata_path = base_dir / f"{pickle_path.stem}_meta.json"
125
+ try:
126
+ pickle_path.unlink(missing_ok=True)
127
+ metadata_path.unlink(missing_ok=True)
128
+ removed_sessions.append(pickle_path.stem)
129
+ except OSError:
130
+ continue
131
+
132
+ return removed_sessions
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
+
236
+ # Set current autosave session id so subsequent autosaves overwrite this session
237
+ try:
238
+ from code_puppy.config import set_current_autosave_from_session_name
239
+
240
+ set_current_autosave_from_session_name(chosen_name)
241
+ except Exception:
242
+ pass
243
+
244
+ total_tokens = sum(agent.estimate_tokens_for_message(msg) for msg in history)
245
+
246
+ session_path = base_dir / f"{chosen_name}.pkl"
247
+ emit_success(
248
+ f"✅ Autosave loaded: {len(history)} messages ({total_tokens} tokens)\n"
249
+ f"📁 From: {session_path}"
250
+ )
@@ -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.196"
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