nimcode 0.4.11__tar.gz → 0.5.0__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 (48) hide show
  1. {nimcode-0.4.11 → nimcode-0.5.0}/PKG-INFO +1 -1
  2. nimcode-0.5.0/src/nimcode/__version__.py +1 -0
  3. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode/agent.py +105 -9
  4. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode/tools.py +111 -0
  5. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode.egg-info/PKG-INFO +1 -1
  6. {nimcode-0.4.11 → nimcode-0.5.0}/tests/test_agent.py +2 -2
  7. nimcode-0.4.11/src/nimcode/__version__.py +0 -1
  8. {nimcode-0.4.11 → nimcode-0.5.0}/README.md +0 -0
  9. {nimcode-0.4.11 → nimcode-0.5.0}/setup.cfg +0 -0
  10. {nimcode-0.4.11 → nimcode-0.5.0}/setup.py +0 -0
  11. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode/__init__.py +0 -0
  12. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode/cli.py +0 -0
  13. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode/config.py +0 -0
  14. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode/lenient_parser.py +0 -0
  15. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode/mcp_client.py +0 -0
  16. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode/memory.py +0 -0
  17. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode/model_registry.py +0 -0
  18. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode/nim_client.py +0 -0
  19. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode/permissions.py +0 -0
  20. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode/plugin_manager.py +0 -0
  21. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode/rag.py +0 -0
  22. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode/repl.py +0 -0
  23. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode/repo_map.py +0 -0
  24. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode/updater.py +0 -0
  25. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode/watcher.py +0 -0
  26. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode.egg-info/SOURCES.txt +0 -0
  27. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode.egg-info/dependency_links.txt +0 -0
  28. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode.egg-info/entry_points.txt +0 -0
  29. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode.egg-info/requires.txt +0 -0
  30. {nimcode-0.4.11 → nimcode-0.5.0}/src/nimcode.egg-info/top_level.txt +0 -0
  31. {nimcode-0.4.11 → nimcode-0.5.0}/tests/test_cli.py +0 -0
  32. {nimcode-0.4.11 → nimcode-0.5.0}/tests/test_config.py +0 -0
  33. {nimcode-0.4.11 → nimcode-0.5.0}/tests/test_lenient_parser.py +0 -0
  34. {nimcode-0.4.11 → nimcode-0.5.0}/tests/test_mcp_client.py +0 -0
  35. {nimcode-0.4.11 → nimcode-0.5.0}/tests/test_memory.py +0 -0
  36. {nimcode-0.4.11 → nimcode-0.5.0}/tests/test_model_registry.py +0 -0
  37. {nimcode-0.4.11 → nimcode-0.5.0}/tests/test_nim_client.py +0 -0
  38. {nimcode-0.4.11 → nimcode-0.5.0}/tests/test_permissions.py +0 -0
  39. {nimcode-0.4.11 → nimcode-0.5.0}/tests/test_plan_injection.py +0 -0
  40. {nimcode-0.4.11 → nimcode-0.5.0}/tests/test_plugin_manager.py +0 -0
  41. {nimcode-0.4.11 → nimcode-0.5.0}/tests/test_repl.py +0 -0
  42. {nimcode-0.4.11 → nimcode-0.5.0}/tests/test_repl_extra.py +0 -0
  43. {nimcode-0.4.11 → nimcode-0.5.0}/tests/test_repl_fixes.py +0 -0
  44. {nimcode-0.4.11 → nimcode-0.5.0}/tests/test_repl_trust.py +0 -0
  45. {nimcode-0.4.11 → nimcode-0.5.0}/tests/test_repo_map.py +0 -0
  46. {nimcode-0.4.11 → nimcode-0.5.0}/tests/test_tools.py +0 -0
  47. {nimcode-0.4.11 → nimcode-0.5.0}/tests/test_updater.py +0 -0
  48. {nimcode-0.4.11 → nimcode-0.5.0}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nimcode
3
- Version: 0.4.11
3
+ Version: 0.5.0
4
4
  Summary: A standalone, robust coding agent for NVIDIA NIM models.
5
5
  Home-page: https://github.com/Batunash/nimcode
6
6
  Author: Autonomous Agent
@@ -0,0 +1 @@
1
+ __version__ = "0.5.0"
@@ -32,7 +32,8 @@ Available Tools:
32
32
  - Bash: {{"tool": "Bash", "args": {{"command": "string"}}}}
33
33
  - Read: {{"tool": "Read", "args": {{"file_path": "string", "offset": "int (optional, 1-based line number to start from)", "limit": "int (optional, max number of lines to return)"}}}}
34
34
  - Write: {{"tool": "Write", "args": {{"file_path": "string", "content": "string"}}}}
35
- - Edit: {{"tool": "Edit", "args": {{"file_path": "string", "old_string": "string", "new_string": "string"}}}}
35
+ - Replace: {{"tool": "Replace", "args": {{"file_path": "string", "replacements": [{{"old_string": "string", "new_string": "string"}}]}}}}
36
+ - ReplaceBlock: {{"tool": "ReplaceBlock", "args": {{"file_path": "string", "start_line": "int", "end_line": "int", "replacement_content": "string"}}}}
36
37
  - Glob: {{"tool": "Glob", "args": {{"pattern": "string"}}}}
37
38
  - Grep: {{"tool": "Grep", "args": {{"query": "string", "directory": "string"}}}}
38
39
  - AskQuestion: {{"tool": "AskQuestion", "args": {{"question": "string", "options": ["string (optional list of choices)"]}}}} — Ask the USER a clarifying question when requirements are ambiguous. Wait for their reply before continuing. Preferred in /grill-me, /plan, and whenever you lack information.
@@ -78,6 +79,11 @@ When asked to "execute", "implement", or "build" something:
78
79
  3. Install dependencies, run tests, and verify your work.
79
80
  4. If a plan exists in `.nimcode/plans/`, follow it step by step.
80
81
 
82
+ ANTI-LAZINESS POLICY (STRICT):
83
+ - You are strictly FORBIDDEN from using placeholders like `// TODO`, `pass`, `$(cat secret.txt)`, `[Insert code here]`.
84
+ - You MUST write complete, full implementations for all functions.
85
+ - If you use placeholders, the system will detect it and reject your turn.
86
+
81
87
  When you have completely fulfilled the user's request and have no more tools to run, output the word TASK_COMPLETE.
82
88
  """
83
89
 
@@ -134,6 +140,9 @@ class Agent:
134
140
  # Initialize MCP Manager
135
141
  self.mcp = MCPManager(self.settings)
136
142
 
143
+ # Stuck-Loop Breaker State
144
+ self.tool_error_counts = {}
145
+
137
146
  # Build OS-aware system prompt
138
147
  cwd = os.getcwd()
139
148
  final_prompt = _build_system_prompt(cwd) + self.mcp.get_system_prompt_additions()
@@ -171,6 +180,31 @@ class Agent:
171
180
  except Exception as e:
172
181
  logger.error(f"Failed to load git context: {e}")
173
182
 
183
+ # Context Injection (Anti-Amnesia RAG)
184
+ try:
185
+ # 1. Directory Tree
186
+ tree_output = ToolRegistry._execute_read_architecture(".", cwd)
187
+ if len(tree_output) > 2000:
188
+ tree_output = tree_output[:2000] + "\n... [TRUNCATED]"
189
+ final_prompt += f"\n\nPROJECT DIRECTORY STRUCTURE:\n{tree_output}"
190
+
191
+ # 2. Schema Injection
192
+ schema_files = ["schema.sql", "models.py", "prisma/schema.prisma", "db.py"]
193
+ schema_contents = []
194
+ for sf in schema_files:
195
+ sf_path = os.path.join(cwd, sf)
196
+ if os.path.exists(sf_path):
197
+ with open(sf_path, "r", encoding="utf-8") as f:
198
+ content = f.read()
199
+ if len(content) > 3000:
200
+ content = content[:3000] + "\n... [TRUNCATED]"
201
+ schema_contents.append(f"--- {sf} ---\n{content}")
202
+
203
+ if schema_contents:
204
+ final_prompt += "\n\nCRITICAL DATABASE SCHEMAS:\n" + "\n".join(schema_contents)
205
+ except Exception as e:
206
+ logger.error(f"Failed to inject semantic context: {e}")
207
+
174
208
  self.messages: List[Dict[str, Any]] = [
175
209
  {"role": "system", "content": final_prompt}
176
210
  ]
@@ -378,6 +412,13 @@ class Agent:
378
412
  turn += 1
379
413
  logger.info(f"--- Turn {turn} ---")
380
414
 
415
+ # Plan-Drift Radar
416
+ if turn > 0 and turn % 15 == 0:
417
+ self.messages.append({
418
+ "role": "user",
419
+ "content": "🧭 PLAN RADAR: You have been running for 15 turns. Please read '.nimcode/active_plan.txt' (if it exists) and verify you are strictly following the current phase. If you have drifted, return to the main objective immediately."
420
+ })
421
+
381
422
  # Compact context before calling API
382
423
  from rich.console import Console
383
424
  if self.memory.count_messages_tokens(self.messages) > self.memory.max_tokens:
@@ -396,6 +437,25 @@ class Agent:
396
437
 
397
438
  self.messages.append({"role": "assistant", "content": full_content})
398
439
 
440
+ # Anti-Laziness Interceptor
441
+ import re
442
+ lazy_patterns = [r"//\s*TODO", r"#\s*TODO", r"\$\(cat", r"\[Insert", r"\[Your code", r"pass\s*#"]
443
+ lazy_detected = False
444
+ for pat in lazy_patterns:
445
+ if re.search(pat, full_content, re.IGNORECASE):
446
+ lazy_detected = True
447
+ break
448
+
449
+ if lazy_detected:
450
+ logger.warning("Agent attempted to use placeholder code. Rejecting turn.")
451
+ from rich.console import Console
452
+ Console().print("[bold red]🚨 ANTI-LAZINESS SYSTEM TRIGGERED! Agent tried to use placeholders. Forcing retry.[/bold red]")
453
+ self.messages.append({
454
+ "role": "user",
455
+ "content": "ERROR: You violated the ANTI-LAZINESS POLICY by using placeholders, TODOs, or mock strings. You must write the REAL, COMPLETE logic. Re-do this action properly."
456
+ })
457
+ continue
458
+
399
459
  # Log turn to NIMCODE.md
400
460
  try:
401
461
  # We log the user's latest prompt, or the tool output
@@ -450,7 +510,7 @@ class Agent:
450
510
  result = ToolRegistry.execute(tool_call)
451
511
 
452
512
  # Auto-Linting & Diff Printing
453
- if tool_name in ["Write", "Edit"] and "Error" not in result:
513
+ if tool_name in ["Write", "Edit", "Replace", "ReplaceBlock"] and "Error" not in result:
454
514
  if "Diff:\n" in result:
455
515
  parts = result.split("Diff:\n", 1)
456
516
  diff_text = parts[1]
@@ -485,6 +545,22 @@ class Agent:
485
545
  t_fmt = self.settings.get("timeout_format", 10)
486
546
  t_fmt = None if t_fmt == 0 else t_fmt
487
547
  subprocess.run(["npx", "--yes", "prettier", "--write", file_path], capture_output=True, timeout=t_fmt)
548
+ # Basic syntax check for JS
549
+ if file_path.endswith(".js"):
550
+ lint = subprocess.run(["node", "--check", file_path], capture_output=True, text=True, timeout=t_fmt)
551
+ if lint.returncode != 0:
552
+ result += f"\n\nAuto-Linter found errors:\n{lint.stderr}\nPlease fix them."
553
+ except Exception:
554
+ pass
555
+ elif file_path.endswith(".go"):
556
+ import subprocess
557
+ try:
558
+ t_fmt = self.settings.get("timeout_format", 10)
559
+ t_fmt = None if t_fmt == 0 else t_fmt
560
+ subprocess.run(["go", "fmt", file_path], capture_output=True, timeout=t_fmt)
561
+ lint = subprocess.run(["go", "vet", file_path], capture_output=True, text=True, timeout=t_fmt)
562
+ if lint.returncode != 0:
563
+ result += f"\n\nAuto-Linter found errors:\n{lint.stderr}\nPlease fix them."
488
564
  except Exception:
489
565
  pass
490
566
  except KeyboardInterrupt:
@@ -530,13 +606,33 @@ class Agent:
530
606
  from rich.panel import Panel
531
607
  c.print(Panel(preview, title=f"Output: {tool_name}", border_style="dim", padding=(0, 1)))
532
608
 
533
- # We simulate tool messages by adding a user message with the tool result.
534
- # In native mode this would be a "tool" role message, but for fallback mode,
535
- # passing it as a user message makes it explicit.
536
- self.messages.append({
537
- "role": "user",
538
- "content": f"Tool {tool_name} returned:\n{result}"
539
- })
609
+ # Stuck-Loop Breaker
610
+ is_error = False
611
+ if isinstance(result, str) and ("Error" in result or "Auto-Linter found errors:" in result or "Traceback" in result):
612
+ is_error = True
613
+
614
+ if is_error:
615
+ error_hash = hash(tool_name + str(result)[:200])
616
+ self.tool_error_counts[error_hash] = self.tool_error_counts.get(error_hash, 0) + 1
617
+
618
+ if self.tool_error_counts[error_hash] >= 3:
619
+ from rich.console import Console
620
+ Console().print("[bold red]🚨 STUCK-LOOP BREAKER TRIGGERED! Forcing agent to change strategy.[/bold red]")
621
+ self.messages.append({
622
+ "role": "user",
623
+ "content": f"Tool {tool_name} returned:\n{result}\n\n🚨 SYSTEM OVERRIDE: You have triggered this exact error 3 times in a row! DO NOT repeat the same action. Use GetCodeOutline, Read, or grep to understand what is wrong, or ask the user for help."
624
+ })
625
+ self.tool_error_counts[error_hash] = 0
626
+ else:
627
+ self.messages.append({
628
+ "role": "user",
629
+ "content": f"Tool {tool_name} returned:\n{result}"
630
+ })
631
+ else:
632
+ self.messages.append({
633
+ "role": "user",
634
+ "content": f"Tool {tool_name} returned:\n{result}"
635
+ })
540
636
 
541
637
  except Exception as e:
542
638
  logger.error(f"Error parsing/executing tool: {e}")
@@ -56,6 +56,16 @@ class ToolRegistry:
56
56
  },
57
57
  "required": ["file_path", "replacements"]
58
58
  },
59
+ "ReplaceBlock": {
60
+ "description": "Edit an existing file by replacing a block of lines. Solves whitespace and indentation issues by targeting exact line numbers.",
61
+ "parameters": {
62
+ "file_path": {"type": "string", "description": "Path to the file."},
63
+ "start_line": {"type": "integer", "description": "The 1-based starting line number of the block to replace."},
64
+ "end_line": {"type": "integer", "description": "The 1-based ending line number of the block to replace (inclusive)."},
65
+ "replacement_content": {"type": "string", "description": "The content to insert in place of the specified lines."}
66
+ },
67
+ "required": ["file_path", "start_line", "end_line", "replacement_content"]
68
+ },
59
69
  "StartTerminal": {
60
70
  "description": "Start a persistent interactive terminal session. Useful for running long-lived commands or commands that prompt for user input (y/n).",
61
71
  "parameters": {
@@ -133,6 +143,20 @@ class ToolRegistry:
133
143
  }
134
144
  },
135
145
  "required": ["question"]
146
+ },
147
+ "GetCodeOutline": {
148
+ "description": "Get the structural outline (functions, classes) of a code file with line numbers. Use this to quickly navigate large files.",
149
+ "parameters": {
150
+ "file_path": {"type": "string", "description": "Path to the code file."}
151
+ },
152
+ "required": ["file_path"]
153
+ },
154
+ "TestRunner": {
155
+ "description": "Run a test suite and capture the output to verify code correctness.",
156
+ "parameters": {
157
+ "command": {"type": "string", "description": "The test command (e.g., 'pytest', 'npm test', 'go test')"}
158
+ },
159
+ "required": ["command"]
136
160
  }
137
161
  }
138
162
  return schemas.get(tool_name)
@@ -176,6 +200,8 @@ class ToolRegistry:
176
200
  return cls._execute_write(args["file_path"], args["content"], cwd)
177
201
  elif tool_name == "Replace":
178
202
  return cls._execute_replace(args["file_path"], args["replacements"], cwd)
203
+ elif tool_name == "ReplaceBlock":
204
+ return cls._execute_replace_block(args["file_path"], args["start_line"], args["end_line"], args["replacement_content"], cwd)
179
205
  elif tool_name == "StartTerminal":
180
206
  return cls._execute_start_terminal(args["command"], args["term_id"], cwd)
181
207
  elif tool_name == "TerminalInput":
@@ -196,6 +222,10 @@ class ToolRegistry:
196
222
  return cls._execute_read_active_editor(cwd)
197
223
  elif tool_name == "AskQuestion":
198
224
  return cls._execute_ask_question(args["question"], args.get("options", []))
225
+ elif tool_name == "GetCodeOutline":
226
+ return cls._execute_get_code_outline(args["file_path"], cwd)
227
+ elif tool_name == "TestRunner":
228
+ return cls._execute_test_runner(args["command"], cwd)
199
229
  else:
200
230
  raise ToolError(f"Tool {tool_name} is registered but execution is not implemented.")
201
231
  except Exception as e:
@@ -497,6 +527,46 @@ class ToolRegistry:
497
527
 
498
528
  return f"Successfully applied {applied} replacements in {file_path}.\nDiff:\n{diff}"
499
529
 
530
+ @staticmethod
531
+ def _execute_replace_block(file_path: str, start_line: int, end_line: int, replacement_content: str, cwd: str) -> str:
532
+ full_path = os.path.join(cwd, file_path)
533
+ if not os.path.exists(full_path):
534
+ raise ToolError(f"File not found: {file_path}")
535
+
536
+ with open(full_path, "r", encoding="utf-8") as f:
537
+ lines = f.readlines()
538
+
539
+ total_lines = len(lines)
540
+ if start_line < 1 or start_line > total_lines:
541
+ raise ToolError(f"start_line {start_line} is out of bounds (1-{total_lines}).")
542
+ if end_line < start_line or end_line > total_lines:
543
+ raise ToolError(f"end_line {end_line} is out of bounds or less than start_line.")
544
+
545
+ # Run Secret Scanner
546
+ try:
547
+ from .secret_scanner import SecretScanner
548
+ findings = SecretScanner.scan(replacement_content)
549
+ if findings:
550
+ raise ToolError(f"SecretScanner blocked replace: {len(findings)} secrets detected ({', '.join(findings)})")
551
+ except ImportError:
552
+ pass
553
+
554
+ new_lines_to_insert = replacement_content.splitlines(keepends=True)
555
+ if new_lines_to_insert and not new_lines_to_insert[-1].endswith('\n'):
556
+ new_lines_to_insert[-1] += '\n'
557
+
558
+ new_content_lines = lines[:start_line-1] + new_lines_to_insert + lines[end_line:]
559
+ new_content = "".join(new_content_lines)
560
+
561
+ import difflib
562
+ diff = "".join(difflib.unified_diff(lines, new_content_lines, fromfile=file_path, tofile=file_path))
563
+
564
+ ToolRegistry._backup_file(full_path, cwd)
565
+ with open(full_path, "w", encoding="utf-8") as f:
566
+ f.write(new_content)
567
+
568
+ return f"Successfully replaced lines {start_line}-{end_line} in {file_path}.\nDiff:\n{diff}"
569
+
500
570
  @staticmethod
501
571
  def _execute_glob(pattern: str, cwd: str) -> str:
502
572
  # We need to support recursive globbing
@@ -802,3 +872,44 @@ class ToolRegistry:
802
872
  pass
803
873
 
804
874
  return "Timed out waiting for VS Code extension. Make sure NimCode is running inside VS Code."
875
+
876
+ @classmethod
877
+ def _execute_get_code_outline(cls, file_path: str, cwd: str) -> str:
878
+ """Extracts class/function signatures with line numbers."""
879
+ import os, re
880
+ full_path = os.path.join(cwd, file_path)
881
+ if not os.path.exists(full_path):
882
+ return f"Error: File {file_path} not found."
883
+
884
+ try:
885
+ with open(full_path, "r", encoding="utf-8") as f:
886
+ lines = f.readlines()
887
+
888
+ outline = []
889
+ pattern = re.compile(r"^\s*(def |class |function |const \w+\s*=\s*\(|type |func |struct |interface )")
890
+ for i, line in enumerate(lines):
891
+ if pattern.search(line):
892
+ outline.append(f"Line {i+1}: {line.strip()}")
893
+
894
+ if not outline:
895
+ return f"No major structural blocks (functions/classes) found in {file_path} using generic regex."
896
+ return "\n".join(outline)
897
+ except Exception as e:
898
+ return f"Error reading {file_path}: {e}"
899
+
900
+ @classmethod
901
+ def _execute_test_runner(cls, command: str, cwd: str) -> str:
902
+ """Runs test commands safely and captures output."""
903
+ import subprocess
904
+ try:
905
+ result = subprocess.run(command, shell=True, cwd=cwd, capture_output=True, text=True, timeout=60)
906
+ output = f"Command: {command}\nExit Code: {result.returncode}\n"
907
+ if result.stdout:
908
+ output += f"\nSTDOUT:\n{result.stdout}"
909
+ if result.stderr:
910
+ output += f"\nSTDERR:\n{result.stderr}"
911
+ return output
912
+ except subprocess.TimeoutExpired:
913
+ return f"Error: Test command '{command}' timed out after 60 seconds."
914
+ except Exception as e:
915
+ return f"Error running tests: {e}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nimcode
3
- Version: 0.4.11
3
+ Version: 0.5.0
4
4
  Summary: A standalone, robust coding agent for NVIDIA NIM models.
5
5
  Home-page: https://github.com/Batunash/nimcode
6
6
  Author: Autonomous Agent
@@ -282,8 +282,8 @@ async def test_agent_auto_linting_js(agent):
282
282
  with patch("nimcode.tools.ToolRegistry.execute", return_value="Output"):
283
283
  with patch("subprocess.run") as mock_run:
284
284
  await agent.run("Hi")
285
- assert mock_run.call_count == 1
286
- assert "prettier" in mock_run.call_args[0][0]
285
+ assert mock_run.call_count >= 1
286
+ assert any("prettier" in call[0][0] for call in mock_run.call_args_list)
287
287
 
288
288
  @pytest.mark.asyncio
289
289
  async def test_distill_memory_context_full(agent):
@@ -1 +0,0 @@
1
- __version__ = "0.4.11"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes