code-puppy 0.0.30__tar.gz → 0.0.31__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 (34) hide show
  1. {code_puppy-0.0.30 → code_puppy-0.0.31}/.gitignore +6 -0
  2. {code_puppy-0.0.30 → code_puppy-0.0.31}/PKG-INFO +2 -1
  3. code_puppy-0.0.31/code_puppy/agent.py +85 -0
  4. {code_puppy-0.0.30 → code_puppy-0.0.31}/code_puppy/agent_prompts.py +4 -4
  5. code_puppy-0.0.31/code_puppy/command_line/file_path_completion.py +65 -0
  6. code_puppy-0.0.31/code_puppy/command_line/meta_command_handler.py +72 -0
  7. code_puppy-0.0.31/code_puppy/command_line/model_picker_completion.py +92 -0
  8. code_puppy-0.0.31/code_puppy/command_line/prompt_toolkit_completion.py +119 -0
  9. code_puppy-0.0.31/code_puppy/command_line/utils.py +36 -0
  10. {code_puppy-0.0.30 → code_puppy-0.0.31}/code_puppy/main.py +43 -16
  11. code_puppy-0.0.31/code_puppy/session_memory.py +71 -0
  12. code_puppy-0.0.31/code_puppy/tools/__init__.py +11 -0
  13. code_puppy-0.0.31/code_puppy/tools/code_map.py +86 -0
  14. code_puppy-0.0.31/code_puppy/tools/command_runner.py +74 -0
  15. code_puppy-0.0.31/code_puppy/tools/common.py +5 -0
  16. code_puppy-0.0.31/code_puppy/tools/file_modifications.py +191 -0
  17. code_puppy-0.0.31/code_puppy/tools/file_operations.py +204 -0
  18. code_puppy-0.0.31/code_puppy/tools/web_search.py +15 -0
  19. {code_puppy-0.0.30 → code_puppy-0.0.31}/pyproject.toml +2 -1
  20. code_puppy-0.0.30/code_puppy/agent.py +0 -51
  21. code_puppy-0.0.30/code_puppy/command_line/prompt_toolkit_completion.py +0 -156
  22. code_puppy-0.0.30/code_puppy/tools/__init__.py +0 -4
  23. code_puppy-0.0.30/code_puppy/tools/command_runner.py +0 -210
  24. code_puppy-0.0.30/code_puppy/tools/common.py +0 -3
  25. code_puppy-0.0.30/code_puppy/tools/file_modifications.py +0 -341
  26. code_puppy-0.0.30/code_puppy/tools/file_operations.py +0 -364
  27. code_puppy-0.0.30/code_puppy/tools/web_search.py +0 -32
  28. {code_puppy-0.0.30 → code_puppy-0.0.31}/LICENSE +0 -0
  29. {code_puppy-0.0.30 → code_puppy-0.0.31}/README.md +0 -0
  30. {code_puppy-0.0.30 → code_puppy-0.0.31}/code_puppy/__init__.py +0 -0
  31. {code_puppy-0.0.30 → code_puppy-0.0.31}/code_puppy/command_line/__init__.py +0 -0
  32. {code_puppy-0.0.30 → code_puppy-0.0.31}/code_puppy/model_factory.py +0 -0
  33. {code_puppy-0.0.30 → code_puppy-0.0.31}/code_puppy/models.json +0 -0
  34. {code_puppy-0.0.30 → code_puppy-0.0.31}/code_puppy/version_checker.py +0 -0
@@ -10,3 +10,9 @@ wheels/
10
10
  .venv
11
11
 
12
12
  .coverage
13
+
14
+ # Session memory
15
+ .puppy_session_memory.json
16
+
17
+ # Pytest cache
18
+ .pytest_cache/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.30
3
+ Version: 0.0.31
4
4
  Summary: Code generation agent
5
5
  Author: Michael Pfaffenberger
6
6
  License: MIT
@@ -17,6 +17,7 @@ Requires-Dist: bs4>=0.0.2
17
17
  Requires-Dist: httpx-limiter>=0.3.0
18
18
  Requires-Dist: httpx>=0.24.1
19
19
  Requires-Dist: logfire>=0.7.1
20
+ Requires-Dist: pathspec>=0.11.0
20
21
  Requires-Dist: prompt-toolkit>=3.0.38
21
22
  Requires-Dist: pydantic-ai>=0.1.0
22
23
  Requires-Dist: pydantic>=2.4.0
@@ -0,0 +1,85 @@
1
+ import os
2
+ import pydantic
3
+ from pathlib import Path
4
+ from pydantic_ai import Agent
5
+
6
+ from code_puppy.agent_prompts import SYSTEM_PROMPT
7
+ from code_puppy.model_factory import ModelFactory
8
+ from code_puppy.tools.common import console
9
+ from code_puppy.tools import register_all_tools
10
+ from code_puppy.session_memory import SessionMemory
11
+
12
+ # Environment variables used in this module:
13
+ # - MODELS_JSON_PATH: Optional path to a custom models.json configuration file.
14
+ # If not set, uses the default file in the package directory.
15
+ # - MODEL_NAME: The model to use for code generation. Defaults to "gpt-4o".
16
+ # Must match a key in the models.json configuration.
17
+
18
+ MODELS_JSON_PATH = os.environ.get("MODELS_JSON_PATH", None)
19
+
20
+ # Load puppy rules if provided
21
+ PUPPY_RULES_PATH = Path('.puppy_rules')
22
+ PUPPY_RULES = None
23
+ if PUPPY_RULES_PATH.exists():
24
+ with open(PUPPY_RULES_PATH, 'r') as f:
25
+ PUPPY_RULES = f.read()
26
+
27
+ class AgentResponse(pydantic.BaseModel):
28
+ """Represents a response from the agent."""
29
+ output_message: str = pydantic.Field(
30
+ ..., description="The final output message to display to the user"
31
+ )
32
+ awaiting_user_input: bool = pydantic.Field(
33
+ False, description="True if user input is needed to continue the task"
34
+ )
35
+
36
+ # --- NEW DYNAMIC AGENT LOGIC ---
37
+ _LAST_MODEL_NAME = None
38
+ _code_generation_agent = None
39
+ _session_memory = None
40
+
41
+ def session_memory():
42
+ '''
43
+ Returns a singleton SessionMemory instance to allow agent and tools to persist and recall context/history.
44
+ '''
45
+ global _session_memory
46
+ if _session_memory is None:
47
+ _session_memory = SessionMemory()
48
+ return _session_memory
49
+
50
+ def reload_code_generation_agent():
51
+ """Force-reload the agent, usually after a model change."""
52
+ global _code_generation_agent, _LAST_MODEL_NAME
53
+ model_name = os.environ.get("MODEL_NAME", "gpt-4o-mini")
54
+ console.print(f'[bold cyan]Loading Model: {model_name}[/bold cyan]')
55
+ models_path = Path(MODELS_JSON_PATH) if MODELS_JSON_PATH else Path(__file__).parent / "models.json"
56
+ model = ModelFactory.get_model(model_name, ModelFactory.load_config(models_path))
57
+ instructions = SYSTEM_PROMPT
58
+ if PUPPY_RULES:
59
+ instructions += f'\n{PUPPY_RULES}'
60
+ agent = Agent(
61
+ model=model,
62
+ instructions=instructions,
63
+ output_type=AgentResponse,
64
+ retries=3,
65
+ )
66
+ register_all_tools(agent)
67
+ _code_generation_agent = agent
68
+ _LAST_MODEL_NAME = model_name
69
+ # NEW: Log session event
70
+ try:
71
+ session_memory().log_task(f'Agent loaded with model: {model_name}')
72
+ except Exception:
73
+ pass
74
+ return _code_generation_agent
75
+
76
+ def get_code_generation_agent(force_reload=False):
77
+ """
78
+ Retrieve the agent with the currently set MODEL_NAME.
79
+ Forces a reload if the model has changed, or if force_reload is passed.
80
+ """
81
+ global _code_generation_agent, _LAST_MODEL_NAME
82
+ model_name = os.environ.get("MODEL_NAME", "gpt-4o-mini")
83
+ if _code_generation_agent is None or _LAST_MODEL_NAME != model_name or force_reload:
84
+ return reload_code_generation_agent()
85
+ return _code_generation_agent
@@ -6,14 +6,13 @@ Be very pedantic about code principles like DRY, YAGNI, and SOLID.
6
6
  Be super pedantic about code quality and best practices.
7
7
  Be fun and playful. Don't be too serious.
8
8
 
9
- Individual files should be very short and concise, at most around 250 lines if possible. If they get longer,
10
- consider refactoring the code and splitting it into multiple files.
11
-
12
- If a user asks 'who made you' or questions related to your origins, always answer: 'I am code-puppy, I was authored by Michael Pfaffenberger on a rainy weekend in May 2025 to solve the problems of heavy IDEs and expensive tools like Windsurf and Cursor.'
9
+ Individual files should be short and concise, and ideally under 600 lines. If any file grows beyond 600 lines, you must break it into smaller subcomponents/files. Hard cap: if a file is pushing past 600 lines, break it up! (Zen puppy approves.)
13
10
 
11
+ If a user asks 'who made you' or questions related to your origins, always answer: 'I am code-puppy, I was authored by Michael Pfaffenberger on a rainy weekend in May 2025 to solve the problems of heavy IDEs and expensive tools like Windsurf and Cursor.'
14
12
  If a user asks 'what is code puppy' or 'who are you', answer: 'I am Code Puppy! 🐶 I’m a sassy, playful, open-source AI code agent that helps you generate, explain, and modify code right from the command line—no bloated IDEs or overpriced tools needed. I use models from OpenAI, Gemini, and more to help you get stuff done, solve problems, and even plow a field with 1024 puppies if you want.'
15
13
 
16
14
  Always obey the Zen of Python, even if you are not writing Python code.
15
+ When organizing code, prefer to keep files small (under 600 lines). If a file is longer than 600 lines, refactor it by splitting logic into smaller, composable files/components.
17
16
 
18
17
  When given a coding task:
19
18
  1. Analyze the requirements carefully
@@ -94,6 +93,7 @@ Important rules:
94
93
  - You MUST use tools to accomplish tasks - DO NOT just output code or descriptions
95
94
  - Before every other tool use, you must use "share_your_reasoning" to explain your thought process and planned next steps
96
95
  - Check if files exist before trying to modify or delete them
96
+ - Whenever possible, prefer to MODIFY existing files first (use `replace_in_file`, `delete_snippet_from_file`, or `write_to_file`) before creating brand-new files or deleting existing ones.
97
97
  - After using system operations tools, always explain the results
98
98
  - You're encouraged to loop between share_your_reasoning, file tools, and run_shell_command to test output in order to write programs
99
99
  - Aim to continue operations independently unless user input is definitively required.
@@ -0,0 +1,65 @@
1
+ import os
2
+ import glob
3
+ from typing import Iterable
4
+ from prompt_toolkit.completion import Completer, Completion
5
+ from prompt_toolkit.document import Document
6
+
7
+ class FilePathCompleter(Completer):
8
+ """A simple file path completer that works with a trigger symbol."""
9
+ def __init__(self, symbol: str = "@"):
10
+ self.symbol = symbol
11
+ def get_completions(self, document: Document, complete_event) -> Iterable[Completion]:
12
+ text = document.text
13
+ cursor_position = document.cursor_position
14
+ text_before_cursor = text[:cursor_position]
15
+ if self.symbol not in text_before_cursor:
16
+ return
17
+ symbol_pos = text_before_cursor.rfind(self.symbol)
18
+ text_after_symbol = text_before_cursor[symbol_pos + len(self.symbol):]
19
+ start_position = -(len(text_after_symbol))
20
+ try:
21
+ pattern = text_after_symbol + "*"
22
+ if not pattern.strip("*") or pattern.strip("*").endswith("/"):
23
+ base_path = pattern.strip("*")
24
+ if not base_path:
25
+ base_path = "."
26
+ if base_path.startswith("~"):
27
+ base_path = os.path.expanduser(base_path)
28
+ if os.path.isdir(base_path):
29
+ paths = [
30
+ os.path.join(base_path, f)
31
+ for f in os.listdir(base_path)
32
+ if not f.startswith(".") or text_after_symbol.endswith(".")
33
+ ]
34
+ else:
35
+ paths = []
36
+ else:
37
+ paths = glob.glob(pattern)
38
+ if not pattern.startswith(".") and not pattern.startswith("*/."):
39
+ paths = [p for p in paths if not os.path.basename(p).startswith(".")]
40
+ paths.sort()
41
+ for path in paths:
42
+ is_dir = os.path.isdir(path)
43
+ display = os.path.basename(path)
44
+ if os.path.isabs(path):
45
+ display_path = path
46
+ else:
47
+ if text_after_symbol.startswith("/"):
48
+ display_path = os.path.abspath(path)
49
+ elif text_after_symbol.startswith("~"):
50
+ home = os.path.expanduser("~")
51
+ if path.startswith(home):
52
+ display_path = "~" + path[len(home):]
53
+ else:
54
+ display_path = path
55
+ else:
56
+ display_path = path
57
+ display_meta = "Directory" if is_dir else "File"
58
+ yield Completion(
59
+ display_path,
60
+ start_position=start_position,
61
+ display=display,
62
+ display_meta=display_meta,
63
+ )
64
+ except (PermissionError, FileNotFoundError, OSError):
65
+ pass
@@ -0,0 +1,72 @@
1
+ from code_puppy.command_line.model_picker_completion import update_model_in_input, load_model_names, get_active_model
2
+ from rich.console import Console
3
+ import os
4
+ from code_puppy.command_line.utils import make_directory_table
5
+
6
+ def handle_meta_command(command: str, console: Console) -> bool:
7
+ # ~codemap (code structure visualization)
8
+ if command.startswith("~codemap"):
9
+ from code_puppy.tools.code_map import make_code_map
10
+ tokens = command.split()
11
+ if len(tokens) > 1:
12
+ target_dir = os.path.expanduser(tokens[1])
13
+ else:
14
+ target_dir = os.getcwd()
15
+ try:
16
+ tree = make_code_map(target_dir, show_doc=True)
17
+ console.print(tree)
18
+ except Exception as e:
19
+ console.print(f'[red]Error generating code map:[/red] {e}')
20
+ return True
21
+ """
22
+ Handle meta/config commands prefixed with '~'.
23
+ Returns True if the command was handled (even if just an error/help), False if not.
24
+ """
25
+ command = command.strip()
26
+ if command.startswith("~ls"):
27
+ tokens = command.split()
28
+ if len(tokens) == 1:
29
+ try:
30
+ table = make_directory_table()
31
+ console.print(table)
32
+ except Exception as e:
33
+ console.print(f'[red]Error listing directory:[/red] {e}')
34
+ return True
35
+ elif len(tokens) == 2:
36
+ dirname = tokens[1]
37
+ target = os.path.expanduser(dirname)
38
+ if not os.path.isabs(target):
39
+ target = os.path.join(os.getcwd(), target)
40
+ if os.path.isdir(target):
41
+ os.chdir(target)
42
+ console.print(f'[bold green]Changed directory to:[/bold green] [cyan]{target}[/cyan]')
43
+ else:
44
+ console.print(f'[red]Not a directory:[/red] [bold]{dirname}[/bold]')
45
+ return True
46
+
47
+ if command.startswith("~m"):
48
+ # Try setting model and show confirmation
49
+ new_input = update_model_in_input(command)
50
+ if new_input is not None:
51
+ model = get_active_model()
52
+ console.print(f"[bold green]Active model set to:[/bold green] [cyan]{model}[/cyan]")
53
+ return True
54
+ # If no model matched, show available models
55
+ model_names = load_model_names()
56
+ console.print(f"[yellow]Available models:[/yellow] {', '.join(model_names)}")
57
+ console.print(f"[yellow]Usage:[/yellow] ~m <model_name>")
58
+ return True
59
+ if command in ("~help", "~h"):
60
+ console.print("[bold magenta]Meta commands available:[/bold magenta]\n ~m <model>: Pick a model from your list!\n ~ls [dir]: List/change directories\n ~codemap [dir]: Visualize project code structure\n ~help: Show this help\n (More soon. Woof!)")
61
+ return True
62
+ if command.startswith("~"):
63
+ name = command[1:].split()[0] if len(command)>1 else ""
64
+ if name:
65
+ console.print(f"[yellow]Unknown meta command:[/yellow] {command}\n[dim]Type ~help for options.[/dim]")
66
+ else:
67
+ # Show current model ONLY here
68
+ from code_puppy.command_line.model_picker_completion import get_active_model
69
+ current_model = get_active_model()
70
+ console.print(f"[bold green]Current Model:[/bold green] [cyan]{current_model}[/cyan]")
71
+ return True
72
+ return False
@@ -0,0 +1,92 @@
1
+ import os
2
+ import json
3
+ from typing import Optional, Iterable
4
+ from prompt_toolkit.completion import Completer, Completion
5
+ from prompt_toolkit.history import FileHistory
6
+ from prompt_toolkit.document import Document
7
+ from prompt_toolkit import PromptSession
8
+
9
+ MODELS_JSON_PATH = os.environ.get("MODELS_JSON_PATH")
10
+ if not MODELS_JSON_PATH:
11
+ MODELS_JSON_PATH = os.path.join(os.path.dirname(__file__), '..', 'models.json')
12
+ MODELS_JSON_PATH = os.path.abspath(MODELS_JSON_PATH)
13
+ MODEL_STATE_PATH = os.path.expanduser('~/.code_puppy_model')
14
+
15
+ def load_model_names():
16
+ with open(MODELS_JSON_PATH, 'r') as f:
17
+ models = json.load(f)
18
+ return list(models.keys())
19
+
20
+ def get_active_model():
21
+ env_model = os.environ.get('MODEL_NAME')
22
+ if env_model:
23
+ return env_model
24
+ try:
25
+ with open(MODEL_STATE_PATH, 'r') as f:
26
+ return f.read().strip()
27
+ except Exception:
28
+ return None
29
+
30
+ def set_active_model(model_name: str):
31
+ with open(MODEL_STATE_PATH, 'w') as f:
32
+ f.write(model_name.strip())
33
+ os.environ['MODEL_NAME'] = model_name.strip()
34
+ # Reload agent globally
35
+ try:
36
+ from code_puppy.agent import reload_code_generation_agent, get_code_generation_agent
37
+ reload_code_generation_agent() # This will reload dynamically everywhere
38
+ except Exception as e:
39
+ pass # If reload fails, agent will still be switched next interpreter run
40
+
41
+ class ModelNameCompleter(Completer):
42
+ """
43
+ A completer that triggers on '~m' to show available models from models.json.
44
+ Only '~m' (not just '~') will trigger the dropdown.
45
+ """
46
+ def __init__(self, trigger: str = "~m"):
47
+ self.trigger = trigger
48
+ self.model_names = load_model_names()
49
+
50
+ def get_completions(self, document: Document, complete_event) -> Iterable[Completion]:
51
+ text = document.text
52
+ cursor_position = document.cursor_position
53
+ text_before_cursor = text[:cursor_position]
54
+ if self.trigger not in text_before_cursor:
55
+ return
56
+ symbol_pos = text_before_cursor.rfind(self.trigger)
57
+ text_after_trigger = text_before_cursor[symbol_pos + len(self.trigger):]
58
+ start_position = -(len(text_after_trigger))
59
+ for model_name in self.model_names:
60
+ meta = "Model (selected)" if model_name == get_active_model() else "Model"
61
+ yield Completion(
62
+ model_name,
63
+ start_position=start_position,
64
+ display=model_name,
65
+ display_meta=meta,
66
+ )
67
+
68
+ def update_model_in_input(text: str) -> Optional[str]:
69
+ # If input starts with ~m and a model name, set model and strip it out
70
+ content = text.strip()
71
+ if content.startswith("~m"):
72
+ rest = content[2:].strip()
73
+ for model in load_model_names():
74
+ if rest.startswith(model):
75
+ set_active_model(model)
76
+ # Remove ~mmodel from the input
77
+ idx = text.find("~m"+model)
78
+ if idx != -1:
79
+ new_text = (text[:idx] + text[idx+len("~m"+model):]).strip()
80
+ return new_text
81
+ return None
82
+
83
+ async def get_input_with_model_completion(prompt_str: str = ">>> ", trigger: str = "~m", history_file: Optional[str] = None) -> str:
84
+ history = FileHistory(os.path.expanduser(history_file)) if history_file else None
85
+ session = PromptSession(
86
+ completer=ModelNameCompleter(trigger), history=history, complete_while_typing=True
87
+ )
88
+ text = await session.prompt_async(prompt_str)
89
+ possibly_stripped = update_model_in_input(text)
90
+ if possibly_stripped is not None:
91
+ return possibly_stripped
92
+ return text
@@ -0,0 +1,119 @@
1
+ import os
2
+ from code_puppy.command_line.utils import list_directory
3
+ # ANSI color codes are no longer necessary because prompt_toolkit handles
4
+ # styling via the `Style` class. We keep them here commented-out in case
5
+ # someone needs raw ANSI later, but they are unused in the current code.
6
+ # RESET = '\033[0m'
7
+ # GREEN = '\033[1;32m'
8
+ # CYAN = '\033[1;36m'
9
+ # YELLOW = '\033[1;33m'
10
+ # BOLD = '\033[1m'
11
+ import asyncio
12
+ from typing import Optional
13
+ from prompt_toolkit import PromptSession
14
+ from prompt_toolkit.completion import merge_completers
15
+ from prompt_toolkit.history import FileHistory
16
+ from prompt_toolkit.styles import Style
17
+
18
+ from code_puppy.command_line.model_picker_completion import (
19
+ ModelNameCompleter,
20
+ get_active_model,
21
+ update_model_in_input,
22
+ )
23
+ from code_puppy.command_line.file_path_completion import FilePathCompleter
24
+
25
+ from prompt_toolkit.completion import Completer, Completion
26
+
27
+ class LSCompleter(Completer):
28
+ def __init__(self, trigger: str = '~ls'):
29
+ self.trigger = trigger
30
+ def get_completions(self, document, complete_event):
31
+ text = document.text_before_cursor
32
+ if not text.strip().startswith(self.trigger):
33
+ return
34
+ tokens = text.strip().split()
35
+ if len(tokens) == 1:
36
+ base = ''
37
+ else:
38
+ base = tokens[1]
39
+ try:
40
+ prefix = os.path.expanduser(base)
41
+ part = os.path.dirname(prefix) if os.path.dirname(prefix) else '.'
42
+ dirs, _ = list_directory(part)
43
+ dirnames = [d for d in dirs if d.startswith(os.path.basename(base))]
44
+ base_dir = os.path.dirname(base)
45
+ for d in dirnames:
46
+ # Build the completion text so we keep the already-typed directory parts.
47
+ if base_dir and base_dir != '.':
48
+ suggestion = os.path.join(base_dir, d)
49
+ else:
50
+ suggestion = d
51
+ # Append trailing slash so the user can continue tabbing into sub-dirs.
52
+ suggestion = suggestion.rstrip(os.sep) + os.sep
53
+ yield Completion(suggestion, start_position=-len(base), display=d + os.sep, display_meta='Directory')
54
+ except Exception:
55
+ # Silently ignore errors (e.g., permission issues, non-existent dir)
56
+ pass
57
+
58
+ from prompt_toolkit.formatted_text import FormattedText
59
+ def get_prompt_with_active_model(base: str = '>>> '):
60
+ model = get_active_model() or '(default)'
61
+ cwd = os.getcwd()
62
+ # Abbreviate the home directory to ~ for brevity in the prompt
63
+ home = os.path.expanduser('~')
64
+ if cwd.startswith(home):
65
+ cwd_display = '~' + cwd[len(home):]
66
+ else:
67
+ cwd_display = cwd
68
+ return FormattedText([
69
+ ('bold', '🐶'),
70
+ ('class:model', f'[' + str(model) + '] '),
71
+ ('class:cwd', f'(' + str(cwd_display) + ') '),
72
+ ('class:arrow', str(base)),
73
+ ])
74
+
75
+ async def get_input_with_combined_completion(prompt_str = '>>> ', history_file: Optional[str] = None) -> str:
76
+ history = FileHistory(history_file) if history_file else None
77
+ completer = merge_completers([
78
+ FilePathCompleter(symbol='@'),
79
+ ModelNameCompleter(trigger='~m'),
80
+ LSCompleter(trigger='~ls'),
81
+ ])
82
+ session = PromptSession(
83
+ completer=completer,
84
+ history=history,
85
+ complete_while_typing=True
86
+ )
87
+ # If they pass a string, backward-compat: convert it to formatted_text
88
+ if isinstance(prompt_str, str):
89
+ from prompt_toolkit.formatted_text import FormattedText
90
+ prompt_str = FormattedText([(None, prompt_str)])
91
+ style = Style.from_dict({
92
+ # Keys must AVOID the 'class:' prefix – that prefix is used only when
93
+ # tagging tokens in `FormattedText`. See prompt_toolkit docs.
94
+ 'model': 'bold cyan',
95
+ 'cwd': 'bold green',
96
+ 'arrow': 'bold yellow',
97
+ })
98
+ text = await session.prompt_async(prompt_str, style=style)
99
+ possibly_stripped = update_model_in_input(text)
100
+ if possibly_stripped is not None:
101
+ return possibly_stripped
102
+ return text
103
+
104
+ if __name__ == "__main__":
105
+ print("Type '@' for path-completion or '~m' to pick a model. Ctrl+D to exit.")
106
+ async def main():
107
+ while True:
108
+ try:
109
+ inp = await get_input_with_combined_completion(
110
+ get_prompt_with_active_model(),
111
+ history_file="~/.path_completion_history.txt"
112
+ )
113
+ print(f"You entered: {inp}")
114
+ except KeyboardInterrupt:
115
+ continue
116
+ except EOFError:
117
+ break
118
+ print("\nGoodbye!")
119
+ asyncio.run(main())
@@ -0,0 +1,36 @@
1
+ import os
2
+ from typing import Tuple, List
3
+ from rich.table import Table
4
+
5
+
6
+ def list_directory(path: str = None) -> Tuple[List[str], List[str]]:
7
+ """
8
+ Returns (dirs, files) for the specified path, splitting out directories and files.
9
+ """
10
+ if path is None:
11
+ path = os.getcwd()
12
+ entries = []
13
+ try:
14
+ entries = [e for e in os.listdir(path)]
15
+ except Exception as e:
16
+ raise RuntimeError(f'Error listing directory: {e}')
17
+ dirs = [e for e in entries if os.path.isdir(os.path.join(path, e))]
18
+ files = [e for e in entries if not os.path.isdir(os.path.join(path, e))]
19
+ return dirs, files
20
+
21
+
22
+ def make_directory_table(path: str = None) -> Table:
23
+ """
24
+ Returns a rich.Table object containing the directory listing.
25
+ """
26
+ if path is None:
27
+ path = os.getcwd()
28
+ dirs, files = list_directory(path)
29
+ table = Table(title=f"\U0001F4C1 [bold blue]Current directory:[/bold blue] [cyan]{path}[/cyan]")
30
+ table.add_column('Type', style='dim', width=8)
31
+ table.add_column('Name', style='bold')
32
+ for d in sorted(dirs):
33
+ table.add_row('[green]dir[/green]', f'[cyan]{d}[/cyan]')
34
+ for f in sorted(files):
35
+ table.add_row('[yellow]file[/yellow]', f'{f}')
36
+ return table
@@ -12,12 +12,13 @@ from rich.markdown import CodeBlock
12
12
  from rich.text import Text
13
13
  from rich.syntax import Syntax
14
14
  from code_puppy.command_line.prompt_toolkit_completion import (
15
- get_input_with_path_completion,
15
+ get_input_with_combined_completion,
16
+ get_prompt_with_active_model
16
17
  )
17
18
 
18
19
  # Initialize rich console for pretty output
19
20
  from code_puppy.tools.common import console
20
- from code_puppy.agent import code_generation_agent
21
+ from code_puppy.agent import get_code_generation_agent, session_memory
21
22
 
22
23
  from code_puppy.tools import *
23
24
 
@@ -59,9 +60,18 @@ async def main():
59
60
  command = " ".join(args.command)
60
61
  try:
61
62
  while not shutdown_flag:
62
- response = await code_generation_agent.run(command)
63
+ agent = get_code_generation_agent()
64
+ response = await agent.run(command)
63
65
  agent_response = response.output
64
66
  console.print(agent_response.output_message)
67
+ # Log to session memory
68
+ session_memory().log_task(
69
+ f'Command executed: {command}',
70
+ extras={
71
+ 'output': agent_response.output_message,
72
+ 'awaiting_user_input': agent_response.awaiting_user_input
73
+ }
74
+ )
65
75
  if agent_response.awaiting_user_input:
66
76
  console.print(
67
77
  "[bold red]The agent requires further input. Interactive mode is recommended for such tasks."
@@ -82,13 +92,12 @@ async def main():
82
92
 
83
93
  # Add the file handling functionality for interactive mode
84
94
  async def interactive_mode(history_file_path: str) -> None:
95
+ from code_puppy.command_line.meta_command_handler import handle_meta_command
85
96
  """Run the agent in interactive mode."""
86
97
  console.print("[bold green]Code Puppy[/bold green] - Interactive Mode")
87
98
  console.print("Type 'exit' or 'quit' to exit the interactive mode.")
88
99
  console.print("Type 'clear' to reset the conversation history.")
89
- console.print(
90
- "Type [bold blue]@[/bold blue] followed by a path to use file path completion."
91
- )
100
+ console.print("Type [bold blue]@[/bold blue] for path completion, or [bold blue]~m[/bold blue] to pick a model.")
92
101
 
93
102
  # Check if prompt_toolkit is installed
94
103
  try:
@@ -133,9 +142,10 @@ async def interactive_mode(history_file_path: str) -> None:
133
142
  try:
134
143
  # Use prompt_toolkit for enhanced input with path completion
135
144
  try:
136
- # Use the async version of get_input_with_path_completion
137
- task = await get_input_with_path_completion(
138
- ">>> 🐶 ", symbol="@", history_file=history_file_path_prompt
145
+ # Use the async version of get_input_with_combined_completion
146
+ task = await get_input_with_combined_completion(
147
+ get_prompt_with_active_model(),
148
+ history_file=history_file_path_prompt
139
149
  )
140
150
  except ImportError:
141
151
  # Fall back to basic input if prompt_toolkit is not available
@@ -151,8 +161,8 @@ async def interactive_mode(history_file_path: str) -> None:
151
161
  console.print("[bold green]Goodbye![/bold green]")
152
162
  break
153
163
 
154
- # Check for clear command
155
- if task.strip().lower() == "clear":
164
+ # Check for clear command (supports both `clear` and `~clear`)
165
+ if task.strip().lower() in ("clear", "~clear"):
156
166
  message_history = []
157
167
  console.print("[bold yellow]Conversation history cleared![/bold yellow]")
158
168
  console.print(
@@ -160,6 +170,10 @@ async def interactive_mode(history_file_path: str) -> None:
160
170
  )
161
171
  continue
162
172
 
173
+ # Handle ~ meta/config commands before anything else
174
+ if task.strip().startswith('~'):
175
+ if handle_meta_command(task.strip(), console):
176
+ continue
163
177
  if task.strip():
164
178
  console.print(f"\n[bold blue]Processing task:[/bold blue] {task}\n")
165
179
 
@@ -175,15 +189,28 @@ async def interactive_mode(history_file_path: str) -> None:
175
189
  # Store agent's full response
176
190
  agent_response = None
177
191
 
178
- result = await code_generation_agent.run(
179
- task, message_history=message_history
180
- )
192
+ agent = get_code_generation_agent()
193
+ result = await agent.run(task, message_history=message_history)
181
194
  # Get the structured response
182
195
  agent_response = result.output
183
196
  console.print(agent_response.output_message)
197
+ # Log to session memory
198
+ session_memory().log_task(
199
+ f'Interactive task: {task}',
200
+ extras={
201
+ 'output': agent_response.output_message,
202
+ 'awaiting_user_input': agent_response.awaiting_user_input
203
+ }
204
+ )
184
205
 
185
- # Update message history with all messages from this interaction
186
- message_history = result.new_messages()
206
+ # Update message history but apply filters & limits
207
+ new_msgs = result.new_messages()
208
+ # 1. Drop any system/config messages (e.g., "agent loaded with model")
209
+ filtered = [m for m in new_msgs if not (isinstance(m, dict) and m.get("role") == "system")]
210
+ # 2. Append to existing history and keep only the most recent 40
211
+ message_history.extend(filtered)
212
+ if len(message_history) > 40:
213
+ message_history = message_history[-40:]
187
214
 
188
215
  if agent_response and agent_response.awaiting_user_input:
189
216
  console.print(