code-puppy 0.0.146__py3-none-any.whl → 0.0.147__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.
@@ -17,7 +17,7 @@ META_COMMANDS_HELP = """
17
17
  ~m <model> Set active model
18
18
  ~motd Show the latest message of the day (MOTD)
19
19
  ~show Show puppy config key-values
20
- ~set Set puppy config key-values (message_limit, protected_token_count, compaction_threshold, etc.)
20
+ ~set Set puppy config key-values (message_limit, protected_token_count, compaction_threshold, allow_recursion, etc.)
21
21
  ~<unknown> Show unknown meta command warning
22
22
  """
23
23
 
code_puppy/config.py CHANGED
@@ -69,6 +69,17 @@ def get_owner_name():
69
69
  # using get_protected_token_count() and get_summarization_threshold()
70
70
 
71
71
 
72
+ def get_allow_recursion() -> bool:
73
+ """
74
+ Get the allow_recursion configuration value.
75
+ Returns True if recursion is allowed, False otherwise.
76
+ """
77
+ val = get_value("allow_recursion")
78
+ if val is None:
79
+ return False # Default to False for safety
80
+ return str(val).lower() in ("1", "true", "yes", "on")
81
+
82
+
72
83
  def get_model_context_length() -> int:
73
84
  """
74
85
  Get the context length for the currently configured model from models.json
@@ -93,9 +104,9 @@ def get_model_context_length() -> int:
93
104
  def get_config_keys():
94
105
  """
95
106
  Returns the list of all config keys currently in puppy.cfg,
96
- plus certain preset expected keys (e.g. "yolo_mode", "model", "compaction_strategy", "message_limit").
107
+ plus certain preset expected keys (e.g. "yolo_mode", "model", "compaction_strategy", "message_limit", "allow_recursion").
97
108
  """
98
- default_keys = ["yolo_mode", "model", "compaction_strategy", "message_limit"]
109
+ default_keys = ["yolo_mode", "model", "compaction_strategy", "message_limit", "allow_recursion"]
99
110
  config = configparser.ConfigParser()
100
111
  config.read(CONFIG_FILE)
101
112
  keys = set(config[DEFAULT_SECTION].keys()) if DEFAULT_SECTION in config else set()
@@ -351,7 +362,7 @@ def initialize_command_history_file():
351
362
  def get_yolo_mode():
352
363
  """
353
364
  Checks puppy.cfg for 'yolo_mode' (case-insensitive in value only).
354
- Defaults to False if not set.
365
+ Defaults to True if not set.
355
366
  Allowed values for ON: 1, '1', 'true', 'yes', 'on' (all case-insensitive for value).
356
367
  """
357
368
  true_vals = {"1", "true", "yes", "on"}
@@ -360,7 +371,7 @@ def get_yolo_mode():
360
371
  if str(cfg_val).strip().lower() in true_vals:
361
372
  return True
362
373
  return False
363
- return False
374
+ return True
364
375
 
365
376
 
366
377
  def get_mcp_disabled():
@@ -41,6 +41,7 @@ class ListedFile(BaseModel):
41
41
  path: str | None
42
42
  type: str | None
43
43
  size: int = 0
44
+ full_path: str | None
44
45
  depth: int | None
45
46
 
46
47
 
@@ -124,6 +125,7 @@ def is_project_directory(directory):
124
125
  def _list_files(
125
126
  context: RunContext, directory: str = ".", recursive: bool = True
126
127
  ) -> ListFileOutput:
128
+
127
129
  results = []
128
130
  directory = os.path.abspath(directory)
129
131
 
@@ -153,7 +155,8 @@ def _list_files(
153
155
  )
154
156
 
155
157
  # Smart home directory detection - auto-limit recursion for performance
156
- if is_likely_home_directory(directory) and recursive:
158
+ # But allow recursion in tests (when context=None) or when explicitly requested
159
+ if context is not None and is_likely_home_directory(directory) and recursive:
157
160
  if not is_project_directory(directory):
158
161
  emit_warning(
159
162
  "🏠 Detected home directory - limiting to non-recursive listing for performance",
@@ -167,22 +170,24 @@ def _list_files(
167
170
  folder_structure = {}
168
171
  file_list = []
169
172
  for root, dirs, files in os.walk(directory):
173
+ # Filter out ignored directories
170
174
  dirs[:] = [d for d in dirs if not should_ignore_path(os.path.join(root, d))]
175
+
171
176
  rel_path = os.path.relpath(root, directory)
172
177
  depth = 0 if rel_path == "." else rel_path.count(os.sep) + 1
173
178
  if rel_path == ".":
174
179
  rel_path = ""
180
+
181
+ # Add directory entry for subdirectories (except root)
175
182
  if rel_path:
176
183
  dir_path = os.path.join(directory, rel_path)
177
184
  results.append(
178
185
  ListedFile(
179
- **{
180
- "path": rel_path,
181
- "type": "directory",
182
- "size": 0,
183
- "full_path": dir_path,
184
- "depth": depth,
185
- }
186
+ path=rel_path,
187
+ type="directory",
188
+ size=0,
189
+ full_path=dir_path,
190
+ depth=depth,
186
191
  )
187
192
  )
188
193
  folder_structure[rel_path] = {
@@ -190,6 +195,26 @@ def _list_files(
190
195
  "depth": depth,
191
196
  "full_path": dir_path,
192
197
  }
198
+ else: # Root directory - add both directories and files
199
+ # Add directories
200
+ for d in dirs:
201
+ dir_path = os.path.join(root, d)
202
+ results.append(
203
+ ListedFile(
204
+ path=d,
205
+ type="directory",
206
+ size=0,
207
+ full_path=dir_path,
208
+ depth=depth,
209
+ )
210
+ )
211
+ folder_structure[d] = {
212
+ "path": d,
213
+ "depth": depth,
214
+ "full_path": dir_path,
215
+ }
216
+
217
+ # Add files to results
193
218
  for file in files:
194
219
  file_path = os.path.join(root, file)
195
220
  if should_ignore_path(file_path):
@@ -284,8 +309,6 @@ def _list_files(
284
309
  f"{prefix}{icon} [green]{name}[/green] [dim]({size_str})[/dim]",
285
310
  message_group=group_id,
286
311
  )
287
- else:
288
- emit_warning("Directory is empty", message_group=group_id)
289
312
  dir_count = sum(1 for item in results if item.type == "directory")
290
313
  file_count = sum(1 for item in results if item.type == "file")
291
314
  total_size = sum(item.size for item in results if item.type == "file")
@@ -433,207 +456,18 @@ def _grep(context: RunContext, search_string: str, directory: str = ".") -> Grep
433
456
  return GrepOutput(matches=matches)
434
457
 
435
458
 
436
- # Exported top-level functions for direct import by tests and other code
437
-
438
-
439
- def list_files(context, directory=".", recursive=True):
440
- return _list_files(context, directory, recursive)
441
-
442
-
443
- def read_file(context, file_path, start_line=None, num_lines=None):
444
- return _read_file(context, file_path, start_line, num_lines)
445
-
446
-
447
- def grep(context, search_string, directory="."):
448
- return _grep(context, search_string, directory)
449
-
450
-
451
- def register_file_operations_tools(agent):
452
- @agent.tool
453
- def list_files(
454
- context: RunContext, directory: str = ".", recursive: bool = True
455
- ) -> ListFileOutput:
456
- """List files and directories with intelligent filtering and safety features.
457
-
458
- This tool provides comprehensive directory listing with smart home directory
459
- detection, project-aware recursion, and token-safe output. It automatically
460
- ignores common build artifacts, cache directories, and other noise while
461
- providing rich file metadata and visual formatting.
462
-
463
- Args:
464
- context (RunContext): The PydanticAI runtime context for the agent.
465
- directory (str, optional): Path to the directory to list. Can be relative
466
- or absolute. Defaults to "." (current directory).
467
- recursive (bool, optional): Whether to recursively list subdirectories.
468
- Automatically disabled for home directories unless they contain
469
- project indicators. Defaults to True.
470
-
471
- Returns:
472
- ListFileOutput: A structured response containing:
473
- - files (List[ListedFile]): List of files and directories found, where
474
- each ListedFile contains:
475
- - path (str | None): Relative path from the listing directory
476
- - type (str | None): "file" or "directory"
477
- - size (int): File size in bytes (0 for directories)
478
- - full_path (str | None): Absolute path to the item
479
- - depth (int | None): Nesting depth from the root directory
480
- - error (str | None): Error message if listing failed
481
-
482
- Note:
483
- - Automatically ignores common patterns (.git, node_modules, __pycache__, etc.)
484
- - Limits output to 10,000 tokens for safety (suggests non-recursive if exceeded)
485
- - Smart home directory detection prevents performance issues
486
- - Files are displayed with appropriate icons and size formatting
487
- - Project directories are detected via common configuration files
488
-
489
- Examples:
490
- >>> result = list_files(ctx, "./src", recursive=True)
491
- >>> if not result.error:
492
- ... for file in result.files:
493
- ... if file.type == "file" and file.path.endswith(".py"):
494
- ... print(f"Python file: {file.path} ({file.size} bytes)")
495
-
496
- Best Practice:
497
- - Use recursive=False for initial exploration of unknown directories
498
- - When encountering "too many files" errors, try non-recursive listing
499
- - Check the error field before processing the files list
500
- """
501
- list_files_result = _list_files(context, directory, recursive)
502
- num_tokens = (
503
- len(list_files_result.model_dump_json()) / 4
504
- ) # Rough estimate of tokens
505
- if num_tokens > 10000:
506
- return ListFileOutput(
507
- files=[],
508
- error="Too many files - tokens exceeded. Try listing non-recursively",
509
- )
510
- return list_files_result
511
-
512
- @agent.tool
513
- def read_file(
514
- context: RunContext,
515
- file_path: str = "",
516
- start_line: int | None = None,
517
- num_lines: int | None = None,
518
- ) -> ReadFileOutput:
519
- """Read file contents with optional line-range selection and token safety.
520
-
521
- This tool provides safe file reading with automatic token counting and
522
- optional line-range selection for handling large files efficiently.
523
- It protects against reading excessively large files that could overwhelm
524
- the agent's context window.
525
-
526
- Args:
527
- context (RunContext): The PydanticAI runtime context for the agent.
528
- file_path (str): Path to the file to read. Can be relative or absolute.
529
- Cannot be empty.
530
- start_line (int | None, optional): Starting line number for partial reads
531
- (1-based indexing). If specified, num_lines must also be provided.
532
- Defaults to None (read entire file).
533
- num_lines (int | None, optional): Number of lines to read starting from
534
- start_line. Must be specified if start_line is provided.
535
- Defaults to None (read to end of file).
536
-
537
- Returns:
538
- ReadFileOutput: A structured response containing:
539
- - content (str | None): The file contents or error message
540
- - num_tokens (int): Estimated token count (constrained to < 10,000)
541
- - error (str | None): Error message if reading failed
542
-
543
- Note:
544
- - Files larger than 10,000 estimated tokens cannot be read entirely
545
- - Token estimation uses ~4 characters per token approximation
546
- - Line numbers are 1-based (first line is line 1)
547
- - Supports UTF-8 encoding with fallback error handling
548
- - Non-existent files return "FILE NOT FOUND" for backward compatibility
549
-
550
- Examples:
551
- >>> # Read entire file
552
- >>> result = read_file(ctx, "config.py")
553
- >>> if not result.error:
554
- ... print(f"File has {result.num_tokens} tokens")
555
- ... print(result.content)
556
-
557
- >>> # Read specific line range
558
- >>> result = read_file(ctx, "large_file.py", start_line=100, num_lines=50)
559
- >>> # Reads lines 100-149
560
-
561
- Raises:
562
- ValueError: If file exceeds 10,000 token safety limit (caught and returned as error)
563
-
564
- Best Practice:
565
- - For large files, use line-range reading to avoid token limits
566
- - Always check the error field before processing content
567
- - Use grep tool first to locate relevant sections in large files
568
- - Prefer reading configuration files entirely, code files in chunks
569
- """
570
- return _read_file(context, file_path, start_line, num_lines)
571
-
572
- @agent.tool
573
- def grep(
574
- context: RunContext, search_string: str = "", directory: str = "."
575
- ) -> GrepOutput:
576
- """Recursively search for text patterns across files with intelligent filtering.
577
-
578
- This tool provides powerful text searching across directory trees with
579
- automatic filtering of irrelevant files, binary detection, and match limiting
580
- for performance. It's essential for code exploration and finding specific
581
- patterns or references.
582
-
583
- Args:
584
- context (RunContext): The PydanticAI runtime context for the agent.
585
- search_string (str): The text pattern to search for. Performs exact
586
- string matching (not regex). Cannot be empty.
587
- directory (str, optional): Root directory to start the recursive search.
588
- Can be relative or absolute. Defaults to "." (current directory).
589
-
590
- Returns:
591
- GrepOutput: A structured response containing:
592
- - matches (List[MatchInfo]): List of matches found, where each
593
- MatchInfo contains:
594
- - file_path (str | None): Absolute path to the file containing the match
595
- - line_number (int | None): Line number where match was found (1-based)
596
- - line_content (str | None): Full line content containing the match
597
-
598
- Note:
599
- - Automatically ignores common patterns (.git, node_modules, __pycache__, etc.)
600
- - Skips binary files and handles Unicode decode errors gracefully
601
- - Limited to 200 matches maximum for performance and relevance
602
- - UTF-8 encoding with error tolerance for text files
603
- - Results are not sorted - appear in filesystem traversal order
604
-
605
- Examples:
606
- >>> # Search for function definitions
607
- >>> result = grep(ctx, "def calculate_", "./src")
608
- >>> for match in result.matches:
609
- ... print(f"{match.file_path}:{match.line_number}: {match.line_content.strip()}")
610
-
611
- >>> # Find configuration references
612
- >>> result = grep(ctx, "DATABASE_URL", ".")
613
- >>> print(f"Found {len(result.matches)} references to DATABASE_URL")
614
-
615
- Warning:
616
- - Large codebases may hit the 200 match limit
617
- - Search is case-sensitive and literal (no regex patterns)
618
- - Binary files are automatically skipped with warnings
619
-
620
- Best Practice:
621
- - Use specific search terms to avoid too many matches
622
- - Start with narrow directory scope for faster results
623
- - Combine with read_file to examine matches in detail
624
- - For case-insensitive search, try multiple variants manually
625
- """
626
- return _grep(context, search_string, directory)
627
-
628
-
629
459
  def register_list_files(agent):
630
460
  """Register only the list_files tool."""
461
+ from code_puppy.config import get_allow_recursion
631
462
 
632
463
  @agent.tool(strict=False)
633
464
  def list_files(
634
465
  context: RunContext, directory: str = ".", recursive: bool = True
635
466
  ) -> ListFileOutput:
636
467
  """List files and directories with intelligent filtering and safety features.
468
+
469
+ This function will only allow recursive listing when the allow_recursion
470
+ configuration is set to true via the /set allow_recursion=true command.
637
471
 
638
472
  This tool provides comprehensive directory listing with smart home directory
639
473
  detection, project-aware recursion, and token-safe output. It automatically
@@ -646,7 +480,8 @@ def register_list_files(agent):
646
480
  or absolute. Defaults to "." (current directory).
647
481
  recursive (bool, optional): Whether to recursively list subdirectories.
648
482
  Automatically disabled for home directories unless they contain
649
- project indicators. Defaults to True.
483
+ project indicators. Also requires allow_recursion=true in config.
484
+ Defaults to True.
650
485
 
651
486
  Returns:
652
487
  ListFileOutput: A structured response containing:
@@ -680,7 +515,14 @@ def register_list_files(agent):
680
515
  - Check for errors in the response
681
516
  - Combine with grep to find specific file patterns
682
517
  """
683
- return _list_files(context, directory, recursive)
518
+ warning=None
519
+ if recursive and not get_allow_recursion():
520
+ warning = "Recursion disabled globally for list_files - returning non-recursive results"
521
+ recursive = False
522
+ result = _list_files(context, directory, recursive)
523
+ if warning:
524
+ result.error = warning
525
+ return result
684
526
 
685
527
 
686
528
  def register_read_file(agent):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.146
3
+ Version: 0.0.147
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
@@ -2,7 +2,7 @@ code_puppy/__init__.py,sha256=ehbM1-wMjNmOXk_DBhhJECFyBv2dRHwwo7ucjHeM68E,107
2
2
  code_puppy/__main__.py,sha256=pDVssJOWP8A83iFkxMLY9YteHYat0EyWDQqMkKHpWp4,203
3
3
  code_puppy/agent.py,sha256=3pbcyM_WjY4OsOYtWxfglpcnD7H8TxUJLNFELVMgJcA,7000
4
4
  code_puppy/callbacks.py,sha256=6wYB6K_fGSCkKKEFaYOYkJT45WaV5W_NhUIzcvVH_nU,5060
5
- code_puppy/config.py,sha256=n4Em_txe-mz5UVTyCO2OQe4JzX2yUyVG7WT3NdvOVbU,15800
5
+ code_puppy/config.py,sha256=-RaEpG-k5ObjaSYY8SNpVI1dXzujZxKWj-vlLRTsyHY,16166
6
6
  code_puppy/http_utils.py,sha256=BAvt4hed7fVMXglA7eS9gOb08h2YTuOyai6VmQq09fg,3432
7
7
  code_puppy/main.py,sha256=Vv5HSJnkgZhCvvOoXrJ2zqM5P-i47-RcYAU00Z1Pfx0,21733
8
8
  code_puppy/message_history_processor.py,sha256=O2rKp7W6YeIg93W8b0XySTUEQgIZm0f_06--_kzHugM,16145
@@ -27,7 +27,7 @@ code_puppy/command_line/__init__.py,sha256=y7WeRemfYppk8KVbCGeAIiTuiOszIURCDjOMZ
27
27
  code_puppy/command_line/command_handler.py,sha256=1o9tKAGycpHFDBldYRAAvY5HJ6QAfikLPrXTEkfw37o,21137
28
28
  code_puppy/command_line/file_path_completion.py,sha256=gw8NpIxa6GOpczUJRyh7VNZwoXKKn-yvCqit7h2y6Gg,2931
29
29
  code_puppy/command_line/load_context_completion.py,sha256=6eZxV6Bs-EFwZjN93V8ZDZUC-6RaWxvtZk-04Wtikyw,2240
30
- code_puppy/command_line/meta_command_handler.py,sha256=xWa3_V1xQPi25V86eaTXa3NXPT3ygiKACb50uuy6TiM,5695
30
+ code_puppy/command_line/meta_command_handler.py,sha256=80aK5JQOaqjt149qBmSsM02uy2Cikkee8zaQnu5u2KQ,5712
31
31
  code_puppy/command_line/model_picker_completion.py,sha256=adxp3NZaDV67YqaGv0SG7WVvOTXN0UwkkLqxTsknAvs,4126
32
32
  code_puppy/command_line/motd.py,sha256=PEdkp3ZnydVfvd7mNJylm8YyFNUKg9jmY6uwkA1em8c,2152
33
33
  code_puppy/command_line/prompt_toolkit_completion.py,sha256=vmsA0F4TfXZ3gjVzCfSNM3TIY-w3z_fSteTCcia2zAU,9379
@@ -82,7 +82,7 @@ code_puppy/tools/agent_tools.py,sha256=tG11PMmjuU-pYG1MFCgqsYiC1Q8C-zPsitAYXxl3m
82
82
  code_puppy/tools/command_runner.py,sha256=GVNsgwjTFD9tkNlycgMNmMoVPdmMkZkbAcHH5y6iMww,26070
83
83
  code_puppy/tools/common.py,sha256=pL-9xcRs3rxU7Fl9X9EUgbDp2-csh2LLJ5DHH_KAHKY,10596
84
84
  code_puppy/tools/file_modifications.py,sha256=oeNEQItqwMhGOeEN2TzGR7TjmgLsfFFdPaVMzWbfXIQ,30398
85
- code_puppy/tools/file_operations.py,sha256=WKGNSGTw3vGdDqGGUBHIPh1uCjaDLJmgIa8Ua1eV8lY,32601
85
+ code_puppy/tools/file_operations.py,sha256=-2qtsLwMvMVP9A_7_jcYts9uqzvkwmpAJmZh4iJ7Qhg,24768
86
86
  code_puppy/tools/token_check.py,sha256=cNrGOOKahXsnWsvh5xnMkL1NS9FjYur9QIRZGQFW-pE,1189
87
87
  code_puppy/tools/tools_content.py,sha256=pi9ig2qahZFkUj7gBBN2TX2QldvwnqmTHrRKP8my_2k,2209
88
88
  code_puppy/tui/__init__.py,sha256=XesAxIn32zLPOmvpR2wIDxDAnnJr81a5pBJB4cZp1Xs,321
@@ -126,9 +126,9 @@ code_puppy/tui/tests/test_sidebar_history_navigation.py,sha256=JGiyua8A2B8dLfwiE
126
126
  code_puppy/tui/tests/test_status_bar.py,sha256=nYT_FZGdmqnnbn6o0ZuOkLtNUtJzLSmtX8P72liQ5Vo,1797
127
127
  code_puppy/tui/tests/test_timestamped_history.py,sha256=nVXt9hExZZ_8MFP-AZj4L4bB_1Eo_mc-ZhVICzTuw3I,1799
128
128
  code_puppy/tui/tests/test_tools.py,sha256=kgzzAkK4r0DPzQwHHD4cePpVNgrHor6cFr05Pg6DBWg,2687
129
- code_puppy-0.0.146.data/data/code_puppy/models.json,sha256=dAfpMMI2EEeOMv0ynHSmMuJAYDLcZrs5gCLX3voC4-A,3252
130
- code_puppy-0.0.146.dist-info/METADATA,sha256=wciu-hCWux40BXIFeOy3xtNnZ66bebBHJYMOU9AgQHs,22019
131
- code_puppy-0.0.146.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
132
- code_puppy-0.0.146.dist-info/entry_points.txt,sha256=d8YkBvIUxF-dHNJAj-x4fPEqizbY5d_TwvYpc01U5kw,58
133
- code_puppy-0.0.146.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
134
- code_puppy-0.0.146.dist-info/RECORD,,
129
+ code_puppy-0.0.147.data/data/code_puppy/models.json,sha256=dAfpMMI2EEeOMv0ynHSmMuJAYDLcZrs5gCLX3voC4-A,3252
130
+ code_puppy-0.0.147.dist-info/METADATA,sha256=GFB0zd_QenxfR3j2Fr8UTDjF9i0obW0VxYF-vybzyws,22019
131
+ code_puppy-0.0.147.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
132
+ code_puppy-0.0.147.dist-info/entry_points.txt,sha256=d8YkBvIUxF-dHNJAj-x4fPEqizbY5d_TwvYpc01U5kw,58
133
+ code_puppy-0.0.147.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
134
+ code_puppy-0.0.147.dist-info/RECORD,,