code-puppy 0.0.86__py3-none-any.whl → 0.0.88__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.
@@ -26,7 +26,7 @@ YOU MUST USE THESE TOOLS to complete tasks (do not just describe what should be
26
26
 
27
27
  File Operations:
28
28
  - list_files(directory=".", recursive=True): ALWAYS use this to explore directories before trying to read/modify files
29
- - read_file(file_path): ALWAYS use this to read existing files before modifying them.
29
+ - read_file(file_path: str, start_line: int | None = None, num_lines: int | None = None): ALWAYS use this to read existing files before modifying them. By default, read the entire file. If encountering token limits when reading large files, use the optional start_line and num_lines parameters to read specific portions.
30
30
  - edit_file(path, diff): Use this single tool to create new files, overwrite entire files, perform targeted replacements, or delete snippets depending on the JSON/raw payload provided.
31
31
  - delete_file(file_path): Use this to remove files when needed
32
32
  - grep(search_string, directory="."): Use this to recursively search for a string across files starting from the specified directory, capping results at 200 matches.
@@ -14,7 +14,6 @@ META_COMMANDS_HELP = """
14
14
  [bold magenta]Meta Commands Help[/bold magenta]
15
15
  ~help, ~h Show this help message
16
16
  ~cd <dir> Change directory or show directories
17
- ~codemap <dir> Show code structure for <dir>
18
17
  ~m <model> Set active model
19
18
  ~motd Show the latest message of the day (MOTD)
20
19
  ~show Show puppy config key-values
@@ -34,21 +33,6 @@ def handle_meta_command(command: str, console: Console) -> bool:
34
33
  print_motd(console, force=True)
35
34
  return True
36
35
 
37
- # ~codemap (code structure visualization)
38
- if command.startswith("~codemap"):
39
- from code_puppy.tools.ts_code_map import make_code_map
40
-
41
- tokens = command.split()
42
- if len(tokens) > 1:
43
- target_dir = os.path.expanduser(tokens[1])
44
- else:
45
- target_dir = os.getcwd()
46
- try:
47
- make_code_map(target_dir, ignore_tests=True)
48
- except Exception as e:
49
- console.print(f"[red]Error generating code map:[/red] {e}")
50
- return True
51
-
52
36
  if command.startswith("~cd"):
53
37
  tokens = command.split()
54
38
  if len(tokens) == 1:
@@ -1,3 +1,7 @@
1
+
2
+
3
+
4
+
1
5
  # ANSI color codes are no longer necessary because prompt_toolkit handles
2
6
  # styling via the `Style` class. We keep them here commented-out in case
3
7
  # someone needs raw ANSI later, but they are unused in the current code.
@@ -171,7 +175,7 @@ async def get_input_with_combined_completion(
171
175
  def _(event):
172
176
  event.app.current_buffer.insert_text("\n")
173
177
 
174
- @bindings.add(Keys.Escape)
178
+ @bindings.add('c-c')
175
179
  def _(event):
176
180
  """Cancel the current prompt when the user presses the ESC key alone."""
177
181
  event.app.exit(exception=KeyboardInterrupt)
@@ -222,4 +226,4 @@ if __name__ == "__main__":
222
226
  break
223
227
  print("\nGoodbye!")
224
228
 
225
- asyncio.run(main())
229
+ asyncio.run(main())
code_puppy/main.py CHANGED
@@ -232,12 +232,19 @@ async def interactive_mode(history_file_path: str) -> None:
232
232
  local_cancelled = True
233
233
  except Exception as e:
234
234
  console.print(f"[dim]Shell kill error: {e}[/dim]")
235
+ # On Windows, we need to reset the signal handler to avoid weird terminal behavior
236
+ if sys.platform.startswith("win"):
237
+ signal.signal(signal.SIGINT, original_handler or signal.SIG_DFL)
235
238
  try:
236
239
  original_handler = signal.getsignal(signal.SIGINT)
237
240
  signal.signal(signal.SIGINT, keyboard_interrupt_handler)
238
241
  result = await agent_task
239
242
  except asyncio.CancelledError:
240
243
  pass
244
+ except KeyboardInterrupt:
245
+ # Handle Ctrl+C from terminal
246
+ keyboard_interrupt_handler(signal.SIGINT, None)
247
+ raise
241
248
  finally:
242
249
  if original_handler:
243
250
  signal.signal(signal.SIGINT, original_handler)
@@ -250,7 +250,7 @@ def message_history_processor(messages: List[ModelMessage]) -> List[ModelMessage
250
250
  [bold white on blue] Tokens in context: {total_current_tokens}, total model capacity: {model_max}, proportion used: {proportion_used:.2f}
251
251
  """)
252
252
 
253
- if proportion_used > 0.9:
253
+ if proportion_used > 0.85:
254
254
  summary = summarize_messages(messages)
255
255
  result_messages = [messages[0], summary]
256
256
  final_token_count = sum(
@@ -0,0 +1,70 @@
1
+ import json
2
+ import tiktoken
3
+
4
+ import pydantic
5
+ from pydantic_ai.messages import ModelMessage
6
+
7
+
8
+ def get_tokenizer():
9
+ """
10
+ Always use cl100k_base tokenizer regardless of model type.
11
+ This is a simple approach that works reasonably well for most models.
12
+ """
13
+ return tiktoken.get_encoding("cl100k_base")
14
+
15
+
16
+ def stringify_message_part(part) -> str:
17
+ """
18
+ Convert a message part to a string representation for token estimation or other uses.
19
+
20
+ Args:
21
+ part: A message part that may contain content or be a tool call
22
+
23
+ Returns:
24
+ String representation of the message part
25
+ """
26
+ result = ""
27
+ if hasattr(part, "part_kind"):
28
+ result += part.part_kind + ": "
29
+ else:
30
+ result += str(type(part)) + ": "
31
+
32
+ # Handle content
33
+ if hasattr(part, "content") and part.content:
34
+ # Handle different content types
35
+ if isinstance(part.content, str):
36
+ result = part.content
37
+ elif isinstance(part.content, pydantic.BaseModel):
38
+ result = json.dumps(part.content.model_dump())
39
+ elif isinstance(part.content, dict):
40
+ result = json.dumps(part.content)
41
+ else:
42
+ result = str(part.content)
43
+
44
+ # Handle tool calls which may have additional token costs
45
+ # If part also has content, we'll process tool calls separately
46
+ if hasattr(part, "tool_name") and part.tool_name:
47
+ # Estimate tokens for tool name and parameters
48
+ tool_text = part.tool_name
49
+ if hasattr(part, "args"):
50
+ tool_text += f" {str(part.args)}"
51
+ result += tool_text
52
+
53
+ return result
54
+
55
+
56
+ def estimate_tokens_for_message(message: ModelMessage) -> int:
57
+ """
58
+ Estimate the number of tokens in a message using tiktoken with cl100k_base encoding.
59
+ This is more accurate than character-based estimation.
60
+ """
61
+ tokenizer = get_tokenizer()
62
+ total_tokens = 0
63
+
64
+ for part in message.parts:
65
+ part_str = stringify_message_part(part)
66
+ if part_str:
67
+ tokens = tokenizer.encode(part_str)
68
+ total_tokens += len(tokens)
69
+
70
+ return max(1, total_tokens)
@@ -2,14 +2,44 @@ import os
2
2
  import fnmatch
3
3
 
4
4
  from typing import Optional, Tuple
5
-
5
+ import tiktoken
6
6
  from rapidfuzz.distance import JaroWinkler
7
7
  from rich.console import Console
8
8
 
9
+ # get_model_context_length will be imported locally where needed to avoid circular imports
10
+
9
11
  NO_COLOR = bool(int(os.environ.get("CODE_PUPPY_NO_COLOR", "0")))
10
12
  console = Console(no_color=NO_COLOR)
11
13
 
12
14
 
15
+ def get_model_context_length() -> int:
16
+ """
17
+ Get the context length for the currently configured model from models.json
18
+ """
19
+ # Import locally to avoid circular imports
20
+ from code_puppy.model_factory import ModelFactory
21
+ from code_puppy.config import get_model_name
22
+ import os
23
+ from pathlib import Path
24
+
25
+ # Load model configuration
26
+ models_path = os.environ.get("MODELS_JSON_PATH")
27
+ if not models_path:
28
+ models_path = Path(__file__).parent.parent / "models.json"
29
+ else:
30
+ models_path = Path(models_path)
31
+
32
+ model_configs = ModelFactory.load_config(str(models_path))
33
+ model_name = get_model_name()
34
+
35
+ # Get context length from model config
36
+ model_config = model_configs.get(model_name, {})
37
+ context_length = model_config.get("context_length", 128000) # Default value
38
+
39
+ # Reserve 10% of context for response
40
+ return int(context_length)
41
+
42
+
13
43
  # -------------------
14
44
  # Shared ignore patterns/helpers
15
45
  # -------------------
@@ -3,11 +3,12 @@
3
3
  import os
4
4
  from typing import List
5
5
 
6
- from pydantic import BaseModel
6
+ from pydantic import BaseModel, conint
7
7
  from pydantic_ai import RunContext
8
8
 
9
9
  from code_puppy.tools.common import console
10
-
10
+ from code_puppy.token_utils import get_tokenizer
11
+ from code_puppy.tools.token_check import token_guard
11
12
  # ---------------------------------------------------------------------------
12
13
  # Module-level helper functions (exposed for unit tests _and_ used as tools)
13
14
  # ---------------------------------------------------------------------------
@@ -180,24 +181,55 @@ def _list_files(
180
181
 
181
182
  class ReadFileOutput(BaseModel):
182
183
  content: str | None
184
+ num_tokens: conint(lt=10000)
185
+ error: str | None = None
183
186
 
184
187
 
185
- def _read_file(context: RunContext, file_path: str) -> ReadFileOutput:
188
+ def _read_file(context: RunContext, file_path: str, start_line: int | None = None, num_lines: int | None = None) -> ReadFileOutput:
186
189
  file_path = os.path.abspath(file_path)
187
- console.print(
188
- f"\n[bold white on blue] READ FILE [/bold white on blue] \U0001f4c2 [bold cyan]{file_path}[/bold cyan]"
189
- )
190
+
191
+ # Build console message with optional parameters
192
+ console_msg = f"\n[bold white on blue] READ FILE [/bold white on blue] \U0001f4c2 [bold cyan]{file_path}[/bold cyan]"
193
+ if start_line is not None and num_lines is not None:
194
+ console_msg += f" [dim](lines {start_line}-{start_line + num_lines - 1})[/dim]"
195
+ console.print(console_msg)
196
+
190
197
  console.print("[dim]" + "-" * 60 + "[/dim]")
191
198
  if not os.path.exists(file_path):
192
- return ReadFileOutput(content=f"File '{file_path}' does not exist")
199
+ error_msg = f"File {file_path} does not exist"
200
+ return ReadFileOutput(content=error_msg, num_tokens=0, error=error_msg)
193
201
  if not os.path.isfile(file_path):
194
- return ReadFileOutput(content=f"'{file_path}' is not a file")
202
+ error_msg = f"{file_path} is not a file"
203
+ return ReadFileOutput(content=error_msg, num_tokens=0, error=error_msg)
195
204
  try:
196
205
  with open(file_path, "r", encoding="utf-8") as f:
197
- content = f.read()
198
- return ReadFileOutput(content=content)
199
- except Exception:
200
- return ReadFileOutput(content="FILE NOT FOUND")
206
+ if start_line is not None and num_lines is not None:
207
+ # Read only the specified lines
208
+ lines = f.readlines()
209
+ # Adjust for 1-based line numbering
210
+ start_idx = start_line - 1
211
+ end_idx = start_idx + num_lines
212
+ # Ensure indices are within bounds
213
+ start_idx = max(0, start_idx)
214
+ end_idx = min(len(lines), end_idx)
215
+ content = ''.join(lines[start_idx:end_idx])
216
+ else:
217
+ # Read the entire file
218
+ content = f.read()
219
+
220
+ tokenizer = get_tokenizer()
221
+ num_tokens = len(tokenizer.encode(content))
222
+ if num_tokens > 10000:
223
+ raise ValueError("The file is massive, greater than 10,000 tokens which is dangerous to read entirely. Please read this file in chunks.")
224
+ token_guard(num_tokens)
225
+ return ReadFileOutput(content=content, num_tokens=num_tokens)
226
+ except (FileNotFoundError, PermissionError):
227
+ # For backward compatibility with tests, return "FILE NOT FOUND" for these specific errors
228
+ error_msg = "FILE NOT FOUND"
229
+ return ReadFileOutput(content=error_msg, num_tokens=0, error=error_msg)
230
+ except Exception as e:
231
+ message = f"An error occurred trying to read the file: {e}"
232
+ return ReadFileOutput(content=message, num_tokens=0, error=message)
201
233
 
202
234
 
203
235
  class MatchInfo(BaseModel):
@@ -238,7 +270,7 @@ def _grep(context: RunContext, search_string: str, directory: str = ".") -> Grep
238
270
  **{
239
271
  "file_path": file_path,
240
272
  "line_number": line_number,
241
- "line_content": line_content.strip(),
273
+ "line_content": line_content.rstrip("\n\r"),
242
274
  }
243
275
  )
244
276
  matches.append(match_info)
@@ -282,8 +314,8 @@ def list_files(
282
314
  return _list_files(context, directory, recursive)
283
315
 
284
316
 
285
- def read_file(context: RunContext, file_path: str = "") -> ReadFileOutput:
286
- return _read_file(context, file_path)
317
+ def read_file(context: RunContext, file_path: str = "", start_line: int | None = None, num_lines: int | None = None) -> ReadFileOutput:
318
+ return _read_file(context, file_path, start_line, num_lines)
287
319
 
288
320
 
289
321
  def grep(
@@ -0,0 +1,11 @@
1
+ from code_puppy.tools.common import get_model_context_length
2
+ from code_puppy.token_utils import estimate_tokens_for_message
3
+
4
+
5
+ def token_guard(num_tokens: int):
6
+ from code_puppy import state_management
7
+ current_history = state_management.get_message_history()
8
+ message_hist_tokens = sum(estimate_tokens_for_message(msg) for msg in current_history)
9
+
10
+ if message_hist_tokens + num_tokens > (get_model_context_length() * 0.9):
11
+ raise ValueError("Tokens produced by this tool call would exceed model capacity")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.86
3
+ Version: 0.0.88
4
4
  Summary: Code generation agent
5
5
  Author: Michael Pfaffenberger
6
6
  License: MIT
@@ -1,30 +1,31 @@
1
1
  code_puppy/__init__.py,sha256=CWH46ZAmJRmHAbOiAhG07OrWYEcEt4yvDTkZU341Wag,169
2
2
  code_puppy/agent.py,sha256=7_1FpGPnw8U632OXP0hLmFIozfVvllF491q8gCpaa8c,3284
3
- code_puppy/agent_prompts.py,sha256=wTah_TvakCMhkb_KwuWCsw4_UR1QsjTZeOT1I8at_nc,6593
3
+ code_puppy/agent_prompts.py,sha256=t3-lqDKrDxCKxFa_va4Suze9BT-JOu1dh9iGiAVNFO4,6828
4
4
  code_puppy/config.py,sha256=r5nw5ChOP8xd_K5yo8U5OtO2gy2bFhARiyNtDp1JrwQ,5013
5
- code_puppy/main.py,sha256=qtBokZZfPQRGF91KNl_n_ywutONwF2f-zTwmt_ROsEU,11080
6
- code_puppy/message_history_processor.py,sha256=jpoxGE6emBX3MAkeeezPZ4QEOzONRhbN6mZe_efG9vI,9248
5
+ code_puppy/main.py,sha256=NLA6TI2is3htFRc5BGTwF4Xp68EtnN-6nOExD9D8i_g,11513
6
+ code_puppy/message_history_processor.py,sha256=2k9y4wXkyZ9v4XLpfH6E3TKLzf303i-k1hXBE-aIf9U,9249
7
7
  code_puppy/model_factory.py,sha256=HXuFHNkVjkCcorAd3ScFmSvBILO932UTq6OmNAqisT8,10898
8
8
  code_puppy/models.json,sha256=jr0-LW87aJS79GosVwoZdHeeq5eflPzgdPoMbcqpVA8,2728
9
9
  code_puppy/state_management.py,sha256=JkTkmq6f9rl_RHPDoBqJvbAzgaMsIkJf-k38ragItIo,1692
10
10
  code_puppy/summarization_agent.py,sha256=jHUQe6iYJsMT0ywEwO7CrhUIKEamO5imhAsDwvNuvow,2684
11
+ code_puppy/token_utils.py,sha256=g7Jj6NAy_a2ab7BXpwyhktruR-QlUV670H_mCPZV1N4,2110
11
12
  code_puppy/version_checker.py,sha256=aRGulzuY4C4CdFvU1rITduyL-1xTFsn4GiD1uSfOl_Y,396
12
13
  code_puppy/command_line/__init__.py,sha256=y7WeRemfYppk8KVbCGeAIiTuiOszIURCDjOMZv_YRmU,45
13
14
  code_puppy/command_line/file_path_completion.py,sha256=gw8NpIxa6GOpczUJRyh7VNZwoXKKn-yvCqit7h2y6Gg,2931
14
- code_puppy/command_line/meta_command_handler.py,sha256=L7qP2g0Faz0V7bMH4YK3s03OWWuQFtK7Sh-Kt2zmmEQ,6182
15
+ code_puppy/command_line/meta_command_handler.py,sha256=d5eVWzRoThYD3cR-GS0CMwHxDvBK4ezLdSIqwWDrq0g,5620
15
16
  code_puppy/command_line/model_picker_completion.py,sha256=NkyZZG7IhcVWSJ3ADytwCA5f8DpNeVs759Qtqs4fQtY,3733
16
17
  code_puppy/command_line/motd.py,sha256=FoZsiVpXGF8WpAmEJX4O895W7MDuzCtNWvFAOShxUXY,1572
17
- code_puppy/command_line/prompt_toolkit_completion.py,sha256=_gP0FIOgHDNHTTWLNL0XNzr6sO0ISe7Mec1uQNo9kcM,8337
18
+ code_puppy/command_line/prompt_toolkit_completion.py,sha256=De_grHDPOvCRph-HDOGCSgX6r_q6akoXArTRwgAskLU,8334
18
19
  code_puppy/command_line/utils.py,sha256=7eyxDHjPjPB9wGDJQQcXV_zOsGdYsFgI0SGCetVmTqE,1251
19
20
  code_puppy/tools/__init__.py,sha256=WTHYIfRk2KMmk6o45TELpbB3GIiAm8s7GmfJ7Zy_tww,503
20
21
  code_puppy/tools/command_runner.py,sha256=9UWCSPpuEndaPx8Ecc8TRsn3rMHNd2AqerirvYPGRIw,14358
21
- code_puppy/tools/common.py,sha256=M53zhiXZAmPdvi1Y_bzCxgvEmifOvRRJvYPARYRZqHw,2253
22
+ code_puppy/tools/common.py,sha256=UkhnfLG1bmd4f9nZCcmno088AtKtAnEES1tydxUN-Fk,3265
22
23
  code_puppy/tools/file_modifications.py,sha256=BzQrGEacS2NZr2ru9N30x_Qd70JDudBKOAPO1XjBohg,13861
23
- code_puppy/tools/file_operations.py,sha256=ypk4yL90LDSVRr0xyWafttzt956J_nXhhenCXhOOit8,11326
24
- code_puppy/tools/ts_code_map.py,sha256=o-u8p5vsYwitfDtVEoPS-7MwWn2xHzwtIQLo1_WMhQs,17647
25
- code_puppy-0.0.86.data/data/code_puppy/models.json,sha256=jr0-LW87aJS79GosVwoZdHeeq5eflPzgdPoMbcqpVA8,2728
26
- code_puppy-0.0.86.dist-info/METADATA,sha256=MhZ1w27mLtvfIsjEztFH9BuAGabfD_uCS4ZQQnDVSwg,6351
27
- code_puppy-0.0.86.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
28
- code_puppy-0.0.86.dist-info/entry_points.txt,sha256=d8YkBvIUxF-dHNJAj-x4fPEqizbY5d_TwvYpc01U5kw,58
29
- code_puppy-0.0.86.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
30
- code_puppy-0.0.86.dist-info/RECORD,,
24
+ code_puppy/tools/file_operations.py,sha256=hXOoRWhOyUyVEX_uAah8VBMZr5AsuyhMvj9uL8osUpE,13257
25
+ code_puppy/tools/token_check.py,sha256=F3eygdI8fgb6dfCrSkGw_OLI7cb_Kpa5ILft4BQ7hvY,525
26
+ code_puppy-0.0.88.data/data/code_puppy/models.json,sha256=jr0-LW87aJS79GosVwoZdHeeq5eflPzgdPoMbcqpVA8,2728
27
+ code_puppy-0.0.88.dist-info/METADATA,sha256=oKP59tXivzgNzTHFCWRbxVr7otqp5URAZ96y0cHSQHc,6351
28
+ code_puppy-0.0.88.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
29
+ code_puppy-0.0.88.dist-info/entry_points.txt,sha256=d8YkBvIUxF-dHNJAj-x4fPEqizbY5d_TwvYpc01U5kw,58
30
+ code_puppy-0.0.88.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
31
+ code_puppy-0.0.88.dist-info/RECORD,,
@@ -1,515 +0,0 @@
1
- import os
2
- from code_puppy.tools.common import should_ignore_path
3
- from pathlib import Path
4
- from rich.text import Text
5
- from rich.tree import Tree as RichTree
6
- from rich.console import Console
7
- from tree_sitter_language_pack import get_parser
8
-
9
- from functools import partial, wraps
10
-
11
-
12
- def _f(fmt): # helper to keep the table tidy
13
- return lambda name, _fmt=fmt: _fmt.format(name=name)
14
-
15
-
16
- def mark_export(label_fn, default=False):
17
- """Decorator to prefix 'export ' (or 'export default ') when requested."""
18
-
19
- @wraps(label_fn)
20
- def _wrap(name, *, exported=False):
21
- prefix = "export default " if default else "export " if exported else ""
22
- return prefix + label_fn(name)
23
-
24
- return _wrap
25
-
26
-
27
- LANGS = {
28
- ".py": {
29
- "lang": "python",
30
- "name_field": "name",
31
- "nodes": {
32
- "function_definition": partial(_f("def {name}()"), style="green"),
33
- "class_definition": partial(_f("class {name}"), style="magenta"),
34
- },
35
- },
36
- ".rb": {
37
- "lang": "ruby",
38
- "name_field": "name",
39
- "nodes": {
40
- "method": partial(_f("def {name}"), style="green"),
41
- "class": partial(_f("class {name}"), style="magenta"),
42
- },
43
- },
44
- ".php": {
45
- "lang": "php",
46
- "name_field": "name",
47
- "nodes": {
48
- "function_definition": partial(_f("function {name}()"), style="green"),
49
- "class_declaration": partial(_f("class {name}"), style="magenta"),
50
- },
51
- },
52
- ".lua": {
53
- "lang": "lua",
54
- "name_field": "name",
55
- "nodes": {
56
- "function_declaration": partial(_f("function {name}()"), style="green")
57
- },
58
- },
59
- ".pl": {
60
- "lang": "perl",
61
- "name_field": "name",
62
- "nodes": {"sub_definition": partial(_f("sub {name}()"), style="green")},
63
- },
64
- ".r": {
65
- "lang": "r",
66
- "name_field": "name",
67
- "nodes": {"function_definition": partial(_f("func {name}()"), style="green")},
68
- },
69
- ".js": {
70
- "lang": "javascript",
71
- "name_field": "name",
72
- "nodes": {
73
- "function_declaration": partial(_f("function {name}()"), style="green"),
74
- "class_declaration": partial(_f("class {name}"), style="magenta"),
75
- "export_statement": partial(_f("export {name}"), style="yellow"),
76
- "export_default_statement": partial(
77
- _f("export default {name}"), style="yellow"
78
- ),
79
- },
80
- },
81
- ".mjs": {
82
- "lang": "javascript",
83
- "name_field": "name",
84
- "nodes": {
85
- "function_declaration": partial(_f("function {name}()"), style="green"),
86
- "class_declaration": partial(_f("class {name}"), style="magenta"),
87
- "export_statement": partial(_f("export {name}"), style="yellow"),
88
- "export_default_statement": partial(
89
- _f("export default {name}"), style="yellow"
90
- ),
91
- },
92
- },
93
- ".cjs": {
94
- "lang": "javascript",
95
- "name_field": "name",
96
- "nodes": {
97
- "function_declaration": partial(_f("function {name}()"), style="green"),
98
- "class_declaration": partial(_f("class {name}"), style="magenta"),
99
- "export_statement": partial(_f("export {name}"), style="yellow"),
100
- "export_default_statement": partial(
101
- _f("export default {name}"), style="yellow"
102
- ),
103
- },
104
- },
105
- ".jsx": {
106
- "lang": "jsx",
107
- "name_field": None,
108
- "nodes": {
109
- "function_declaration": partial(_f("function {name}()"), style="green"),
110
- "class_declaration": partial(_f("class {name}"), style="magenta"),
111
- "export_statement": partial(_f("export {name}"), style="yellow"),
112
- },
113
- },
114
- ".ts": {
115
- "lang": "tsx",
116
- "name_field": None,
117
- "nodes": {
118
- "function_declaration": partial(_f("function {name}()"), style="green"),
119
- "class_declaration": partial(_f("class {name}"), style="magenta"),
120
- "export_statement": partial(_f("export {name}"), style="yellow"),
121
- },
122
- },
123
- ".tsx": {
124
- "lang": "tsx",
125
- "name_field": None,
126
- "nodes": {
127
- "function_declaration": partial(_f("function {name}()"), style="green"),
128
- "class_declaration": partial(_f("class {name}"), style="magenta"),
129
- "export_statement": partial(_f("export {name}"), style="yellow"),
130
- "interface_declaration": partial(_f("interface {name}"), style="green"),
131
- },
132
- },
133
- # ───────── systems / compiled ────────────────────────────────────
134
- ".c": {
135
- "lang": "c",
136
- "name_field": "declarator", # struct ident is under declarator
137
- "nodes": {
138
- "function_definition": partial(_f("fn {name}()"), style="green"),
139
- "struct_specifier": partial(_f("struct {name}"), style="magenta"),
140
- },
141
- },
142
- ".h": {
143
- "lang": "c",
144
- "name_field": "declarator", # struct ident is under declarator
145
- "nodes": {
146
- "function_definition": partial(_f("fn {name}()"), style="green"),
147
- "struct_specifier": partial(_f("struct {name}"), style="magenta"),
148
- },
149
- },
150
- ".cpp": {
151
- "lang": "cpp",
152
- "name_field": "declarator",
153
- "nodes": {
154
- "function_definition": partial(_f("fn {name}()"), style="green"),
155
- "class_specifier": partial(_f("class {name}"), style="magenta"),
156
- "struct_specifier": partial(_f("struct {name}"), style="magenta"),
157
- },
158
- },
159
- ".hpp": {
160
- "lang": "cpp",
161
- "name_field": "declarator",
162
- "nodes": {
163
- "function_definition": partial(_f("fn {name}()"), style="green"),
164
- "class_specifier": partial(_f("class {name}"), style="magenta"),
165
- "struct_specifier": partial(_f("struct {name}"), style="magenta"),
166
- },
167
- },
168
- ".cc": {
169
- "lang": "cpp",
170
- "name_field": "declarator",
171
- "nodes": {
172
- "function_definition": partial(_f("fn {name}()"), style="green"),
173
- "class_specifier": partial(_f("class {name}"), style="magenta"),
174
- "struct_specifier": partial(_f("struct {name}"), style="magenta"),
175
- },
176
- },
177
- ".hh": {
178
- "lang": "cpp",
179
- "name_field": "declarator",
180
- "nodes": {
181
- "function_definition": partial(_f("fn {name}()"), style="green"),
182
- "class_specifier": partial(_f("class {name}"), style="magenta"),
183
- "struct_specifier": partial(_f("struct {name}"), style="magenta"),
184
- },
185
- },
186
- ".cs": {
187
- "lang": "c_sharp",
188
- "name_field": "name",
189
- "nodes": {
190
- "method_declaration": partial(_f("method {name}()"), style="green"),
191
- "class_declaration": partial(_f("class {name}"), style="magenta"),
192
- },
193
- },
194
- ".java": {
195
- "lang": "java",
196
- "name_field": "name",
197
- "nodes": {
198
- "method_declaration": partial(_f("method {name}()"), style="green"),
199
- "class_declaration": partial(_f("class {name}"), style="magenta"),
200
- },
201
- },
202
- ".kt": {
203
- "lang": "kotlin",
204
- "name_field": "name",
205
- "nodes": {
206
- "function_declaration": partial(_f("fun {name}()"), style="green"),
207
- "class_declaration": partial(_f("class {name}"), style="magenta"),
208
- },
209
- },
210
- ".swift": {
211
- "lang": "swift",
212
- "name_field": "name",
213
- "nodes": {
214
- "function_declaration": partial(_f("func {name}()"), style="green"),
215
- "class_declaration": partial(_f("class {name}"), style="magenta"),
216
- },
217
- },
218
- ".go": {
219
- "lang": "go",
220
- "name_field": "name",
221
- "nodes": {
222
- "function_declaration": partial(_f("func {name}()"), style="green"),
223
- "type_spec": partial(_f("type {name}"), style="magenta"),
224
- },
225
- },
226
- ".rs": {
227
- "lang": "rust",
228
- "name_field": "name",
229
- "nodes": {
230
- "function_item": partial(_f("fn {name}()"), style="green"),
231
- "struct_item": partial(_f("struct {name}"), style="magenta"),
232
- "trait_item": partial(_f("trait {name}"), style="magenta"),
233
- },
234
- },
235
- ".zig": {
236
- "lang": "zig",
237
- "name_field": "name",
238
- "nodes": {
239
- "fn_proto": partial(_f("fn {name}()"), style="green"),
240
- "struct_decl": partial(_f("struct {name}"), style="magenta"),
241
- },
242
- },
243
- ".scala": {
244
- "lang": "scala",
245
- "name_field": "name",
246
- "nodes": {
247
- "function_definition": partial(_f("def {name}()"), style="green"),
248
- "class_definition": partial(_f("class {name}"), style="magenta"),
249
- "object_definition": partial(_f("object {name}"), style="magenta"),
250
- },
251
- },
252
- ".hs": {
253
- "lang": "haskell",
254
- "name_field": "name",
255
- "nodes": {
256
- "function_declaration": partial(_f("fun {name}"), style="green"),
257
- "type_declaration": partial(_f("type {name}"), style="magenta"),
258
- },
259
- },
260
- ".jl": {
261
- "lang": "julia",
262
- "name_field": "name",
263
- "nodes": {
264
- "function_definition": partial(_f("function {name}()"), style="green"),
265
- "abstract_type_definition": partial(_f("abstract {name}"), style="magenta"),
266
- "struct_definition": partial(_f("struct {name}"), style="magenta"),
267
- },
268
- },
269
- # ──────── markup / style ─────────────────────────────────────────
270
- ".html": {
271
- "lang": "html",
272
- "name_field": None,
273
- "nodes": {
274
- # rely on parser presence; generic element handling not needed for tests
275
- },
276
- },
277
- ".css": {
278
- "lang": "css",
279
- "name_field": None,
280
- "nodes": {},
281
- },
282
- # ───────── scripting (shell / infra) ─────────────────────────────
283
- ".sh": {
284
- "lang": "bash",
285
- "name_field": "name",
286
- "nodes": {"function_definition": partial(_f("fn {name}()"), style="green")},
287
- },
288
- ".ps1": {
289
- "lang": "powershell",
290
- "name_field": "name",
291
- "nodes": {
292
- "function_definition": partial(_f("function {name}()"), style="green")
293
- },
294
- },
295
- }
296
-
297
- # ---------------------------------------------------------------------------
298
- # Emoji helpers (cute! 🐶)
299
- # ---------------------------------------------------------------------------
300
-
301
- _NODE_EMOJIS = {
302
- "function": "🦴",
303
- "class": "🏠",
304
- "struct": "🏗️",
305
- "interface": "🎛️",
306
- "trait": "💎",
307
- "type": "🧩",
308
- "object": "📦",
309
- "export": "📤",
310
- }
311
-
312
- _FILE_EMOJIS = {
313
- ".py": "🐍",
314
- ".js": "✨",
315
- ".jsx": "✨",
316
- ".ts": "🌀",
317
- ".tsx": "🌀",
318
- ".rb": "💎",
319
- ".go": "🐹",
320
- ".rs": "🦀",
321
- ".java": "☕️",
322
- ".c": "🔧",
323
- ".cpp": "➕",
324
- ".hpp": "➕",
325
- ".swift": "🕊️",
326
- ".kt": "🤖",
327
- }
328
- _PARSER_CACHE = {}
329
-
330
-
331
- def parser_for(lang_name):
332
- if lang_name not in _PARSER_CACHE:
333
- _PARSER_CACHE[lang_name] = get_parser(lang_name)
334
- return _PARSER_CACHE[lang_name]
335
-
336
-
337
- # ----------------------------------------------------------------------
338
- # helper: breadth-first search for an identifier-ish node
339
- # ----------------------------------------------------------------------
340
- def _first_identifier(node):
341
- from collections import deque
342
-
343
- q = deque([node])
344
- while q:
345
- n = q.popleft()
346
- if n.type in {"identifier", "property_identifier", "type_identifier"}:
347
- return n
348
- q.extend(n.children)
349
- return None
350
-
351
-
352
- def _span(node):
353
- """Return "[start:end]" lines (1‑based, inclusive)."""
354
- start_line = node.start_point[0] + 1
355
- end_line = node.end_point[0] + 1
356
- return Text(f" [{start_line}:{end_line}]", style="bold white")
357
-
358
-
359
- def _emoji_for_node_type(ts_type: str) -> str:
360
- """Return a cute emoji for a given Tree-sitter node type (best-effort)."""
361
- # naive mapping based on substrings – keeps it simple
362
- if "function" in ts_type or "method" in ts_type or ts_type.startswith("fn_"):
363
- return _NODE_EMOJIS["function"]
364
- if "class" in ts_type:
365
- return _NODE_EMOJIS["class"]
366
- if "struct" in ts_type:
367
- return _NODE_EMOJIS["struct"]
368
- if "interface" in ts_type:
369
- return _NODE_EMOJIS["interface"]
370
- if "trait" in ts_type:
371
- return _NODE_EMOJIS["trait"]
372
- if "type_spec" in ts_type or "type_declaration" in ts_type:
373
- return _NODE_EMOJIS["type"]
374
- if "object" in ts_type:
375
- return _NODE_EMOJIS["object"]
376
- if ts_type.startswith("export"):
377
- return _NODE_EMOJIS["export"]
378
- return ""
379
-
380
-
381
- # ----------------------------------------------------------------------
382
- # traversal (clean)
383
- # ----------------------------------------------------------------------
384
-
385
-
386
- def _walk_fix(ts_node, rich_parent, info):
387
- """Recursive traversal adding child nodes with emoji labels."""
388
- nodes_cfg = info["nodes"]
389
- name_field = info["name_field"]
390
-
391
- for child in ts_node.children:
392
- n_type = child.type
393
- if n_type in nodes_cfg:
394
- style = nodes_cfg[n_type].keywords["style"]
395
- ident = (
396
- child.child_by_field_name(name_field)
397
- if name_field
398
- else _first_identifier(child)
399
- )
400
- label_text = ident.text.decode() if ident else "<anon>"
401
- label = nodes_cfg[n_type].func(label_text)
402
- emoji = _emoji_for_node_type(n_type)
403
- if emoji:
404
- label = f"{emoji} {label}"
405
- branch = rich_parent.add(Text(label, style=style) + _span(child))
406
- _walk_fix(child, branch, info)
407
- else:
408
- _walk_fix(child, rich_parent, info)
409
-
410
-
411
- # ----------------------------------------------------------------------
412
-
413
-
414
- def _walk(ts_node, rich_parent, info):
415
- nodes_cfg = info["nodes"]
416
- name_field = info["name_field"]
417
-
418
- for child in ts_node.children:
419
- t = child.type
420
- if t in nodes_cfg:
421
- style = nodes_cfg[t].keywords["style"]
422
-
423
- if name_field:
424
- ident = child.child_by_field_name(name_field)
425
- else:
426
- ident = _first_identifier(child)
427
-
428
- label_text = ident.text.decode() if ident else "<anon>"
429
- label = nodes_cfg[t].func(label_text)
430
- emoji = _emoji_for_node_type(t)
431
- if emoji:
432
- label = f"{emoji} {label}"
433
- branch = rich_parent.add(Text(label, style=style) + _span(child))
434
- _walk(child, branch, info)
435
- else:
436
- _walk(child, rich_parent, info)
437
-
438
-
439
- def map_code_file(filepath):
440
- ext = Path(filepath).suffix
441
- info = LANGS.get(ext)
442
- if not info:
443
- return None
444
-
445
- code = Path(filepath).read_bytes()
446
- parser = parser_for(info["lang"])
447
- tree = parser.parse(code)
448
-
449
- file_emoji = _FILE_EMOJIS.get(ext, "📄")
450
- root_label = f"{file_emoji} {Path(filepath).name}"
451
- base = RichTree(Text(root_label, style="bold cyan"))
452
-
453
- if tree.root_node.has_error:
454
- base.add(Text("⚠️ syntax error", style="bold red"))
455
-
456
- _walk_fix(tree.root_node, base, info)
457
- return base
458
-
459
-
460
- def make_code_map(directory: str, ignore_tests: bool = True) -> str:
461
- """Generate a Rich-rendered code map including directory hierarchy.
462
-
463
- Args:
464
- directory: Root directory to scan.
465
- ignore_tests: Whether to skip files with 'test' in the name.
466
-
467
- Returns:
468
- Plain-text rendering of the generated Rich tree (last 1k chars).
469
- """
470
- # Create root of tree representing starting directory
471
- base_tree = RichTree(Text(Path(directory).name, style="bold magenta"))
472
-
473
- # Cache to ensure we reuse RichTree nodes per directory path
474
- dir_nodes: dict[str, RichTree] = {
475
- Path(directory).resolve(): base_tree
476
- } # key=abs path
477
-
478
- for root, dirs, files in os.walk(directory):
479
- # ignore dot-folders early
480
- dirs[:] = [d for d in dirs if not d.startswith(".")]
481
-
482
- abs_root = Path(root).resolve()
483
-
484
- # Ensure current directory has a node; create if coming from parent
485
- if abs_root not in dir_nodes and abs_root != Path(directory).resolve():
486
- rel_parts = abs_root.relative_to(directory).parts
487
- parent_path = Path(directory).resolve()
488
- for part in rel_parts: # walk down creating nodes as needed
489
- parent_node = dir_nodes[parent_path]
490
- current_path = parent_path / part
491
- if current_path not in dir_nodes:
492
- dir_label = Text(part, style="bold magenta")
493
- dir_node = parent_node.add(dir_label)
494
- dir_nodes[current_path] = dir_node
495
- parent_path = current_path
496
-
497
- current_node = dir_nodes.get(abs_root, base_tree)
498
-
499
- for f in files:
500
- file_path = os.path.join(root, f)
501
- if should_ignore_path(file_path):
502
- continue
503
- if ignore_tests and "test" in f:
504
- continue
505
- try:
506
- file_tree = map_code_file(file_path)
507
- if file_tree is not None:
508
- current_node.add(file_tree)
509
- except Exception:
510
- current_node.add(Text(f"[error reading {f}]", style="bold red"))
511
-
512
- # Render and return last 1000 characters
513
- buf = Console(record=True, width=120)
514
- buf.print(base_tree)
515
- return buf.export_text()[-1000:]