code-puppy 0.0.97__py3-none-any.whl → 0.0.118__py3-none-any.whl

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 (81) hide show
  1. code_puppy/__init__.py +2 -5
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agent.py +125 -40
  4. code_puppy/agent_prompts.py +30 -24
  5. code_puppy/callbacks.py +152 -0
  6. code_puppy/command_line/command_handler.py +359 -0
  7. code_puppy/command_line/load_context_completion.py +59 -0
  8. code_puppy/command_line/model_picker_completion.py +14 -21
  9. code_puppy/command_line/motd.py +44 -28
  10. code_puppy/command_line/prompt_toolkit_completion.py +42 -23
  11. code_puppy/config.py +266 -26
  12. code_puppy/http_utils.py +122 -0
  13. code_puppy/main.py +570 -383
  14. code_puppy/message_history_processor.py +195 -104
  15. code_puppy/messaging/__init__.py +46 -0
  16. code_puppy/messaging/message_queue.py +288 -0
  17. code_puppy/messaging/queue_console.py +293 -0
  18. code_puppy/messaging/renderers.py +305 -0
  19. code_puppy/messaging/spinner/__init__.py +55 -0
  20. code_puppy/messaging/spinner/console_spinner.py +200 -0
  21. code_puppy/messaging/spinner/spinner_base.py +66 -0
  22. code_puppy/messaging/spinner/textual_spinner.py +97 -0
  23. code_puppy/model_factory.py +73 -105
  24. code_puppy/plugins/__init__.py +32 -0
  25. code_puppy/reopenable_async_client.py +225 -0
  26. code_puppy/state_management.py +60 -21
  27. code_puppy/summarization_agent.py +56 -35
  28. code_puppy/token_utils.py +7 -9
  29. code_puppy/tools/__init__.py +1 -4
  30. code_puppy/tools/command_runner.py +187 -32
  31. code_puppy/tools/common.py +44 -35
  32. code_puppy/tools/file_modifications.py +335 -118
  33. code_puppy/tools/file_operations.py +368 -95
  34. code_puppy/tools/token_check.py +27 -11
  35. code_puppy/tools/tools_content.py +53 -0
  36. code_puppy/tui/__init__.py +10 -0
  37. code_puppy/tui/app.py +1050 -0
  38. code_puppy/tui/components/__init__.py +21 -0
  39. code_puppy/tui/components/chat_view.py +512 -0
  40. code_puppy/tui/components/command_history_modal.py +218 -0
  41. code_puppy/tui/components/copy_button.py +139 -0
  42. code_puppy/tui/components/custom_widgets.py +58 -0
  43. code_puppy/tui/components/input_area.py +167 -0
  44. code_puppy/tui/components/sidebar.py +309 -0
  45. code_puppy/tui/components/status_bar.py +182 -0
  46. code_puppy/tui/messages.py +27 -0
  47. code_puppy/tui/models/__init__.py +8 -0
  48. code_puppy/tui/models/chat_message.py +25 -0
  49. code_puppy/tui/models/command_history.py +89 -0
  50. code_puppy/tui/models/enums.py +24 -0
  51. code_puppy/tui/screens/__init__.py +13 -0
  52. code_puppy/tui/screens/help.py +130 -0
  53. code_puppy/tui/screens/settings.py +256 -0
  54. code_puppy/tui/screens/tools.py +74 -0
  55. code_puppy/tui/tests/__init__.py +1 -0
  56. code_puppy/tui/tests/test_chat_message.py +28 -0
  57. code_puppy/tui/tests/test_chat_view.py +88 -0
  58. code_puppy/tui/tests/test_command_history.py +89 -0
  59. code_puppy/tui/tests/test_copy_button.py +191 -0
  60. code_puppy/tui/tests/test_custom_widgets.py +27 -0
  61. code_puppy/tui/tests/test_disclaimer.py +27 -0
  62. code_puppy/tui/tests/test_enums.py +15 -0
  63. code_puppy/tui/tests/test_file_browser.py +60 -0
  64. code_puppy/tui/tests/test_help.py +38 -0
  65. code_puppy/tui/tests/test_history_file_reader.py +107 -0
  66. code_puppy/tui/tests/test_input_area.py +33 -0
  67. code_puppy/tui/tests/test_settings.py +44 -0
  68. code_puppy/tui/tests/test_sidebar.py +33 -0
  69. code_puppy/tui/tests/test_sidebar_history.py +153 -0
  70. code_puppy/tui/tests/test_sidebar_history_navigation.py +132 -0
  71. code_puppy/tui/tests/test_status_bar.py +54 -0
  72. code_puppy/tui/tests/test_timestamped_history.py +52 -0
  73. code_puppy/tui/tests/test_tools.py +82 -0
  74. code_puppy/version_checker.py +26 -3
  75. {code_puppy-0.0.97.dist-info → code_puppy-0.0.118.dist-info}/METADATA +9 -2
  76. code_puppy-0.0.118.dist-info/RECORD +86 -0
  77. code_puppy-0.0.97.dist-info/RECORD +0 -32
  78. {code_puppy-0.0.97.data → code_puppy-0.0.118.data}/data/code_puppy/models.json +0 -0
  79. {code_puppy-0.0.97.dist-info → code_puppy-0.0.118.dist-info}/WHEEL +0 -0
  80. {code_puppy-0.0.97.dist-info → code_puppy-0.0.118.dist-info}/entry_points.txt +0 -0
  81. {code_puppy-0.0.97.dist-info → code_puppy-0.0.118.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,359 @@
1
+ import os
2
+
3
+ from code_puppy.command_line.model_picker_completion import (
4
+ load_model_names,
5
+ update_model_in_input,
6
+ )
7
+ from code_puppy.command_line.motd import print_motd
8
+ from code_puppy.command_line.utils import make_directory_table
9
+ from code_puppy.config import get_config_keys
10
+ from code_puppy.tools.tools_content import tools_content
11
+
12
+ COMMANDS_HELP = """
13
+ [bold magenta]Commands Help[/bold magenta]
14
+ /help, /h Show this help message
15
+ /cd <dir> Change directory or show directories
16
+
17
+ /exit, /quit Exit interactive mode
18
+ /generate-pr-description [@dir] Generate comprehensive PR description
19
+ /m <model> Set active model
20
+ /motd Show the latest message of the day (MOTD)
21
+ /show Show puppy config key-values
22
+ /compact Summarize and compact current chat history
23
+ /dump_context <name> Save current message history to file
24
+ /load_context <name> Load message history from file
25
+ /set Set puppy config key-values (e.g., /set yolo_mode true)
26
+ /tools Show available tools and capabilities
27
+ /<unknown> Show unknown command warning
28
+ """
29
+
30
+
31
+ def handle_command(command: str):
32
+ from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
33
+
34
+ """
35
+ Handle commands prefixed with '/'.
36
+
37
+ Args:
38
+ command: The command string to handle
39
+
40
+ Returns:
41
+ True if the command was handled, False if not, or a string to be processed as user input
42
+ """
43
+ command = command.strip()
44
+
45
+ if command.strip().startswith("/motd"):
46
+ print_motd(force=True)
47
+ return True
48
+
49
+ if command.strip().startswith("/compact"):
50
+ from code_puppy.message_history_processor import (
51
+ estimate_tokens_for_message,
52
+ summarize_messages,
53
+ )
54
+ from code_puppy.messaging import (
55
+ emit_error,
56
+ emit_info,
57
+ emit_success,
58
+ emit_warning,
59
+ )
60
+ from code_puppy.state_management import get_message_history, set_message_history
61
+
62
+ try:
63
+ history = get_message_history()
64
+ if not history:
65
+ emit_warning("No history to compact yet. Ask me something first!")
66
+ return True
67
+
68
+ before_tokens = sum(estimate_tokens_for_message(m) for m in history)
69
+ emit_info(
70
+ f"🤔 Compacting {len(history)} messages... (~{before_tokens} tokens)"
71
+ )
72
+
73
+ compacted, _ = summarize_messages(history, with_protection=False)
74
+ if not compacted:
75
+ emit_error("Summarization failed. History unchanged.")
76
+ return True
77
+
78
+ set_message_history(compacted)
79
+
80
+ after_tokens = sum(estimate_tokens_for_message(m) for m in compacted)
81
+ reduction_pct = (
82
+ ((before_tokens - after_tokens) / before_tokens * 100)
83
+ if before_tokens > 0
84
+ else 0
85
+ )
86
+ emit_success(
87
+ f"✨ Done! History: {len(history)} → {len(compacted)} messages\n"
88
+ f"🏦 Tokens: {before_tokens:,} → {after_tokens:,} ({reduction_pct:.1f}% reduction)"
89
+ )
90
+ return True
91
+ except Exception as e:
92
+ emit_error(f"/compact error: {e}")
93
+ return True
94
+
95
+ if command.startswith("/cd"):
96
+ tokens = command.split()
97
+ if len(tokens) == 1:
98
+ try:
99
+ table = make_directory_table()
100
+ emit_info(table)
101
+ except Exception as e:
102
+ emit_error(f"Error listing directory: {e}")
103
+ return True
104
+ elif len(tokens) == 2:
105
+ dirname = tokens[1]
106
+ target = os.path.expanduser(dirname)
107
+ if not os.path.isabs(target):
108
+ target = os.path.join(os.getcwd(), target)
109
+ if os.path.isdir(target):
110
+ os.chdir(target)
111
+ emit_success(f"Changed directory to: {target}")
112
+ else:
113
+ emit_error(f"Not a directory: {dirname}")
114
+ return True
115
+
116
+ if command.strip().startswith("/show"):
117
+ from code_puppy.command_line.model_picker_completion import get_active_model
118
+ from code_puppy.config import (
119
+ get_owner_name,
120
+ get_protected_token_count,
121
+ get_puppy_name,
122
+ get_summarization_threshold,
123
+ get_yolo_mode,
124
+ )
125
+
126
+ puppy_name = get_puppy_name()
127
+ owner_name = get_owner_name()
128
+ model = get_active_model()
129
+ yolo_mode = get_yolo_mode()
130
+ protected_tokens = get_protected_token_count()
131
+ summary_threshold = get_summarization_threshold()
132
+
133
+ status_msg = f"""[bold magenta]🐶 Puppy Status[/bold magenta]
134
+
135
+ [bold]puppy_name:[/bold] [cyan]{puppy_name}[/cyan]
136
+ [bold]owner_name:[/bold] [cyan]{owner_name}[/cyan]
137
+ [bold]model:[/bold] [green]{model}[/green]
138
+ [bold]YOLO_MODE:[/bold] {"[red]ON[/red]" if yolo_mode else "[yellow]off[/yellow]"}
139
+ [bold]protected_tokens:[/bold] [cyan]{protected_tokens:,}[/cyan] recent tokens preserved
140
+ [bold]summary_threshold:[/bold] [cyan]{summary_threshold:.1%}[/cyan] context usage triggers summarization
141
+
142
+ """
143
+ emit_info(status_msg)
144
+ return True
145
+
146
+ if command.startswith("/set"):
147
+ # Syntax: /set KEY=VALUE or /set KEY VALUE
148
+ from code_puppy.config import set_config_value
149
+
150
+ tokens = command.split(None, 2)
151
+ argstr = command[len("/set") :].strip()
152
+ key = None
153
+ value = None
154
+ if "=" in argstr:
155
+ key, value = argstr.split("=", 1)
156
+ key = key.strip()
157
+ value = value.strip()
158
+ elif len(tokens) >= 3:
159
+ key = tokens[1]
160
+ value = tokens[2]
161
+ elif len(tokens) == 2:
162
+ key = tokens[1]
163
+ value = ""
164
+ else:
165
+ emit_warning(
166
+ f"Usage: /set KEY=VALUE or /set KEY VALUE\nConfig keys: {', '.join(get_config_keys())}"
167
+ )
168
+ return True
169
+ if key:
170
+ set_config_value(key, value)
171
+ emit_success(f'🌶 Set {key} = "{value}" in puppy.cfg!')
172
+ else:
173
+ emit_error("You must supply a key.")
174
+ return True
175
+
176
+ if command.startswith("/tools"):
177
+ # Display the tools_content.py file content with markdown formatting
178
+ from rich.markdown import Markdown
179
+
180
+ markdown_content = Markdown(tools_content)
181
+ emit_info(markdown_content)
182
+ return True
183
+
184
+ if command.startswith("/m"):
185
+ # Try setting model and show confirmation
186
+ new_input = update_model_in_input(command)
187
+ if new_input is not None:
188
+ from code_puppy.agent import get_code_generation_agent
189
+ from code_puppy.command_line.model_picker_completion import get_active_model
190
+
191
+ model = get_active_model()
192
+ # Make sure this is called for the test
193
+ get_code_generation_agent(force_reload=True)
194
+ emit_success(f"Active model set and loaded: {model}")
195
+ return True
196
+ # If no model matched, show available models
197
+ model_names = load_model_names()
198
+ emit_warning("Usage: /m <model-name>")
199
+ emit_warning(f"Available models: {', '.join(model_names)}")
200
+ return True
201
+ if command in ("/help", "/h"):
202
+ emit_info(COMMANDS_HELP)
203
+ return True
204
+
205
+ if command.startswith("/generate-pr-description"):
206
+ # Parse directory argument (e.g., /generate-pr-description @some/dir)
207
+ tokens = command.split()
208
+ directory_context = ""
209
+ for t in tokens:
210
+ if t.startswith("@"):
211
+ directory_context = f" Please work in the directory: {t[1:]}"
212
+ break
213
+
214
+ # Hard-coded prompt from user requirements
215
+ pr_prompt = f"""Generate a comprehensive PR description for my current branch changes. Follow these steps:
216
+
217
+ 1 Discover the changes: Use git CLI to find the base branch (usually main/master/develop) and get the list of changed files, commits, and diffs.
218
+ 2 Analyze the code: Read and analyze all modified files to understand:
219
+ • What functionality was added/changed/removed
220
+ • The technical approach and implementation details
221
+ • Any architectural or design pattern changes
222
+ • Dependencies added/removed/updated
223
+ 3 Generate a structured PR description with these sections:
224
+ • Title: Concise, descriptive title (50 chars max)
225
+ • Summary: Brief overview of what this PR accomplishes
226
+ • Changes Made: Detailed bullet points of specific changes
227
+ • Technical Details: Implementation approach, design decisions, patterns used
228
+ • Files Modified: List of key files with brief description of changes
229
+ • Testing: What was tested and how (if applicable)
230
+ • Breaking Changes: Any breaking changes (if applicable)
231
+ • Additional Notes: Any other relevant information
232
+ 4 Create a markdown file: Generate a PR_DESCRIPTION.md file with proper GitHub markdown formatting that I can directly copy-paste into GitHub's PR
233
+ description field. Use proper markdown syntax with headers, bullet points, code blocks, and formatting.
234
+ 5 Make it review-ready: Ensure the description helps reviewers understand the context, approach, and impact of the changes.
235
+ 6. If you have Github MCP, or gh cli is installed and authenticated then find the PR for the branch we analyzed and update the PR description there and then delete the PR_DESCRIPTION.md file. (If you have a better name (title) for the PR, go ahead and update the title too.{directory_context}"""
236
+
237
+ # Return the prompt to be processed by the main chat system
238
+ return pr_prompt
239
+
240
+ if command.startswith("/dump_context"):
241
+ import json
242
+ import pickle
243
+ from datetime import datetime
244
+ from pathlib import Path
245
+
246
+ from code_puppy.config import CONFIG_DIR
247
+ from code_puppy.message_history_processor import estimate_tokens_for_message
248
+ from code_puppy.state_management import get_message_history
249
+
250
+ tokens = command.split()
251
+ if len(tokens) != 2:
252
+ emit_warning("Usage: /dump_context <session_name>")
253
+ return True
254
+
255
+ session_name = tokens[1]
256
+ history = get_message_history()
257
+
258
+ if not history:
259
+ emit_warning("No message history to dump!")
260
+ return True
261
+
262
+ # Create contexts directory inside CONFIG_DIR if it doesn't exist
263
+ contexts_dir = Path(CONFIG_DIR) / "contexts"
264
+ contexts_dir.mkdir(parents=True, exist_ok=True)
265
+
266
+ try:
267
+ # Save as pickle for exact preservation
268
+ pickle_file = contexts_dir / f"{session_name}.pkl"
269
+ with open(pickle_file, "wb") as f:
270
+ pickle.dump(history, f)
271
+
272
+ # Also save metadata as JSON for readability
273
+ meta_file = contexts_dir / f"{session_name}_meta.json"
274
+ metadata = {
275
+ "session_name": session_name,
276
+ "timestamp": datetime.now().isoformat(),
277
+ "message_count": len(history),
278
+ "total_tokens": sum(estimate_tokens_for_message(m) for m in history),
279
+ "file_path": str(pickle_file),
280
+ }
281
+
282
+ with open(meta_file, "w") as f:
283
+ json.dump(metadata, f, indent=2)
284
+
285
+ emit_success(
286
+ f"✅ Context saved: {len(history)} messages ({metadata['total_tokens']} tokens)\n"
287
+ f"📁 Files: {pickle_file}, {meta_file}"
288
+ )
289
+ return True
290
+
291
+ except Exception as e:
292
+ emit_error(f"Failed to dump context: {e}")
293
+ return True
294
+
295
+ if command.startswith("/load_context"):
296
+ import pickle
297
+ from pathlib import Path
298
+
299
+ from code_puppy.config import CONFIG_DIR
300
+ from code_puppy.message_history_processor import estimate_tokens_for_message
301
+ from code_puppy.state_management import set_message_history
302
+
303
+ tokens = command.split()
304
+ if len(tokens) != 2:
305
+ emit_warning("Usage: /load_context <session_name>")
306
+ return True
307
+
308
+ session_name = tokens[1]
309
+ contexts_dir = Path(CONFIG_DIR) / "contexts"
310
+ pickle_file = contexts_dir / f"{session_name}.pkl"
311
+
312
+ if not pickle_file.exists():
313
+ emit_error(f"Context file not found: {pickle_file}")
314
+ # List available contexts
315
+ available = list(contexts_dir.glob("*.pkl"))
316
+ if available:
317
+ names = [f.stem for f in available]
318
+ emit_info(f"Available contexts: {', '.join(names)}")
319
+ return True
320
+
321
+ try:
322
+ with open(pickle_file, "rb") as f:
323
+ history = pickle.load(f)
324
+
325
+ set_message_history(history)
326
+ total_tokens = sum(estimate_tokens_for_message(m) for m in history)
327
+
328
+ emit_success(
329
+ f"✅ Context loaded: {len(history)} messages ({total_tokens} tokens)\n"
330
+ f"📁 From: {pickle_file}"
331
+ )
332
+ return True
333
+
334
+ except Exception as e:
335
+ emit_error(f"Failed to load context: {e}")
336
+ return True
337
+
338
+ if command in ("/exit", "/quit"):
339
+ emit_success("Goodbye!")
340
+ # Signal to the main app that we want to exit
341
+ # The actual exit handling is done in main.py
342
+ return True
343
+ if command.startswith("/"):
344
+ name = command[1:].split()[0] if len(command) > 1 else ""
345
+ if name:
346
+ emit_warning(
347
+ f"Unknown command: {command}\n[dim]Type /help for options.[/dim]"
348
+ )
349
+ else:
350
+ # Show current model ONLY here
351
+ from code_puppy.command_line.model_picker_completion import get_active_model
352
+
353
+ current_model = get_active_model()
354
+ emit_info(
355
+ f"[bold green]Current Model:[/bold green] [cyan]{current_model}[/cyan]"
356
+ )
357
+ return True
358
+
359
+ return False
@@ -0,0 +1,59 @@
1
+ from pathlib import Path
2
+
3
+ from prompt_toolkit.completion import Completer, Completion
4
+
5
+ from code_puppy.config import CONFIG_DIR
6
+
7
+
8
+ class LoadContextCompleter(Completer):
9
+ def __init__(self, trigger: str = "/load_context"):
10
+ self.trigger = trigger
11
+
12
+ def get_completions(self, document, complete_event):
13
+ text_before_cursor = document.text_before_cursor
14
+ stripped_text_for_trigger_check = text_before_cursor.lstrip()
15
+
16
+ if not stripped_text_for_trigger_check.startswith(self.trigger):
17
+ return
18
+
19
+ # Determine the part of the text that is relevant for this completer
20
+ actual_trigger_pos = text_before_cursor.find(self.trigger)
21
+ effective_input = text_before_cursor[actual_trigger_pos:]
22
+
23
+ tokens = effective_input.split()
24
+
25
+ # Case 1: Input is exactly the trigger (e.g., "/load_context") and nothing more
26
+ if (
27
+ len(tokens) == 1
28
+ and tokens[0] == self.trigger
29
+ and not effective_input.endswith(" ")
30
+ ):
31
+ yield Completion(
32
+ text=self.trigger + " ",
33
+ start_position=-len(tokens[0]),
34
+ display=self.trigger + " ",
35
+ display_meta="load saved context",
36
+ )
37
+ return
38
+
39
+ # Case 2: Input is trigger + space or trigger + partial session name
40
+ session_filter = ""
41
+ if len(tokens) > 1: # e.g., ["/load_context", "partial"]
42
+ session_filter = tokens[1]
43
+
44
+ # Get available context files
45
+ try:
46
+ contexts_dir = Path(CONFIG_DIR) / "contexts"
47
+ if contexts_dir.exists():
48
+ for pkl_file in contexts_dir.glob("*.pkl"):
49
+ session_name = pkl_file.stem # removes .pkl extension
50
+ if session_name.startswith(session_filter):
51
+ yield Completion(
52
+ session_name,
53
+ start_position=-len(session_filter),
54
+ display=session_name,
55
+ display_meta="saved context session",
56
+ )
57
+ except Exception:
58
+ # Silently ignore errors (e.g., permission issues, non-existent dir)
59
+ pass
@@ -1,4 +1,3 @@
1
- import json
2
1
  import os
3
2
  from typing import Iterable, Optional
4
3
 
@@ -8,17 +7,13 @@ from prompt_toolkit.document import Document
8
7
  from prompt_toolkit.history import FileHistory
9
8
 
10
9
  from code_puppy.config import get_model_name, set_model_name
11
-
12
- MODELS_JSON_PATH = os.environ.get("MODELS_JSON_PATH")
13
- if not MODELS_JSON_PATH:
14
- MODELS_JSON_PATH = os.path.join(os.path.dirname(__file__), "..", "models.json")
15
- MODELS_JSON_PATH = os.path.abspath(MODELS_JSON_PATH)
10
+ from code_puppy.model_factory import ModelFactory
16
11
 
17
12
 
18
13
  def load_model_names():
19
- with open(MODELS_JSON_PATH, "r") as f:
20
- models = json.load(f)
21
- return list(models.keys())
14
+ """Load model names from the config that's fetched from the endpoint."""
15
+ models_config = ModelFactory.load_config()
16
+ return list(models_config.keys())
22
17
 
23
18
 
24
19
  def get_active_model():
@@ -31,11 +26,9 @@ def get_active_model():
31
26
 
32
27
  def set_active_model(model_name: str):
33
28
  """
34
- Sets the active model name by updating both config (for persistence)
35
- and env (for process lifetime override).
29
+ Sets the active model name by updating the config (for persistence).
36
30
  """
37
31
  set_model_name(model_name)
38
- os.environ["MODEL_NAME"] = model_name.strip()
39
32
  # Reload agent globally
40
33
  try:
41
34
  from code_puppy.agent import reload_code_generation_agent
@@ -47,11 +40,11 @@ def set_active_model(model_name: str):
47
40
 
48
41
  class ModelNameCompleter(Completer):
49
42
  """
50
- A completer that triggers on '~m' to show available models from models.json.
51
- Only '~m' (not just '~') will trigger the dropdown.
43
+ A completer that triggers on '/m' to show available models from models.json.
44
+ Only '/m' (not just '/') will trigger the dropdown.
52
45
  """
53
46
 
54
- def __init__(self, trigger: str = "~m"):
47
+ def __init__(self, trigger: str = "/m"):
55
48
  self.trigger = trigger
56
49
  self.model_names = load_model_names()
57
50
 
@@ -77,23 +70,23 @@ class ModelNameCompleter(Completer):
77
70
 
78
71
 
79
72
  def update_model_in_input(text: str) -> Optional[str]:
80
- # If input starts with ~m and a model name, set model and strip it out
73
+ # If input starts with /m and a model name, set model and strip it out
81
74
  content = text.strip()
82
- if content.startswith("~m"):
75
+ if content.startswith("/m"):
83
76
  rest = content[2:].strip()
84
77
  for model in load_model_names():
85
78
  if rest == model:
86
79
  set_active_model(model)
87
- # Remove ~mmodel from the input
88
- idx = text.find("~m" + model)
80
+ # Remove /mmodel from the input
81
+ idx = text.find("/m" + model)
89
82
  if idx != -1:
90
- new_text = (text[:idx] + text[idx + len("~m" + model) :]).strip()
83
+ new_text = (text[:idx] + text[idx + len("/m" + model) :]).strip()
91
84
  return new_text
92
85
  return None
93
86
 
94
87
 
95
88
  async def get_input_with_model_completion(
96
- prompt_str: str = ">>> ", trigger: str = "~m", history_file: Optional[str] = None
89
+ prompt_str: str = ">>> ", trigger: str = "/m", history_file: Optional[str] = None
97
90
  ) -> str:
98
91
  history = FileHistory(os.path.expanduser(history_file)) if history_file else None
99
92
  session = PromptSession(
@@ -1,35 +1,25 @@
1
1
  """
2
- MOTD (Message of the Day) feature for code-puppy.
3
- Stores seen versions in ~/.puppy_cfg/motd.txt.
2
+ 🐶 MOTD (Message of the Day) feature for code-puppy! 🐕
3
+ Stores seen versions in ~/.code_puppy/motd.txt - woof woof! 🐾
4
4
  """
5
5
 
6
6
  import os
7
7
 
8
- MOTD_VERSION = "20250817"
9
- MOTD_MESSAGE = """
10
-
11
- 🐾 Happy Sunday, Aug 17, 2025!
12
-
13
- Biscuit the code puppy learned two new tricks!
14
- Major paws-ups:
15
- 1. On-the-fly summarization: when your model's context hits 90%,
16
- Biscuit auto-summarizes older messages to keep you cruising. No sweat, no tokens spilled.
17
- 2. AGENT.md support: ship your project rules and style guide,
18
- and Biscuit will obey them like the good pup he is.
19
-
20
- • Use ~m to swap models mid-session.
21
- • YOLO_MODE=true skips command confirmations (danger, zoomies!).
22
- • Keep files under 600 lines; split big ones like a responsible hooman.
23
- • DRY code, happy pup.
24
-
25
- Today's vibe: sniff context, summarize smartly, obey AGENT.md, and ship.
26
- Run ~motd anytime you need more puppy hype!
8
+ from code_puppy.config import CONFIG_DIR
9
+ from code_puppy.messaging import emit_info
27
10
 
11
+ MOTD_VERSION = "2025-08-24"
12
+ MOTD_MESSAGE = """🐕‍🦺
13
+ 🐾```
14
+ # 🐶🎉🐕 WOOF WOOF! AUGUST 24th 🐕🎉🐶
15
+ 40k Downloads! Woot!
16
+ Thanks for your support!
17
+ -Mike
28
18
  """
29
- MOTD_TRACK_FILE = os.path.expanduser("~/.puppy_cfg/motd.txt")
19
+ MOTD_TRACK_FILE = os.path.join(CONFIG_DIR, "motd.txt")
30
20
 
31
21
 
32
- def has_seen_motd(version: str) -> bool:
22
+ def has_seen_motd(version: str) -> bool: # 🐕 Check if puppy has seen this MOTD!
33
23
  if not os.path.exists(MOTD_TRACK_FILE):
34
24
  return False
35
25
  with open(MOTD_TRACK_FILE, "r") as f:
@@ -37,15 +27,41 @@ def has_seen_motd(version: str) -> bool:
37
27
  return version in seen_versions
38
28
 
39
29
 
40
- def mark_motd_seen(version: str):
30
+ def mark_motd_seen(version: str): # 🐶 Mark MOTD as seen by this good puppy!
31
+ # Create directory if it doesn't exist 🏠🐕
41
32
  os.makedirs(os.path.dirname(MOTD_TRACK_FILE), exist_ok=True)
42
- with open(MOTD_TRACK_FILE, "a") as f:
43
- f.write(f"{version}\n")
44
33
 
34
+ # Check if the version is already in the file 📋🐶
35
+ seen_versions = set()
36
+ if os.path.exists(MOTD_TRACK_FILE):
37
+ with open(MOTD_TRACK_FILE, "r") as f:
38
+ seen_versions = {line.strip() for line in f if line.strip()}
39
+
40
+ # Only add the version if it's not already there 📝🐕‍🦺
41
+ if version not in seen_versions:
42
+ with open(MOTD_TRACK_FILE, "a") as f:
43
+ f.write(f"{version}\n")
45
44
 
46
- def print_motd(console, force: bool = False) -> bool:
45
+
46
+ def print_motd(
47
+ console=None, force: bool = False
48
+ ) -> bool: # 🐶 Print exciting puppy MOTD!
49
+ """
50
+ 🐕 Print the message of the day to the user - woof woof! 🐕
51
+
52
+ Args:
53
+ console: Optional console object (for backward compatibility) 🖥️🐶
54
+ force: Whether to force printing even if the MOTD has been seen 💪🐕‍🦺
55
+
56
+ Returns:
57
+ True if the MOTD was printed, False otherwise 🐾
58
+ """
47
59
  if force or not has_seen_motd(MOTD_VERSION):
48
- console.print(MOTD_MESSAGE)
60
+ # Create a Rich Markdown object for proper rendering 🎨🐶
61
+ from rich.markdown import Markdown
62
+
63
+ markdown_content = Markdown(MOTD_MESSAGE)
64
+ emit_info(markdown_content)
49
65
  mark_motd_seen(MOTD_VERSION)
50
66
  return True
51
67
  return False