nimcode 0.4.10__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.
- {nimcode-0.4.10 → nimcode-0.5.0}/PKG-INFO +1 -1
- nimcode-0.5.0/src/nimcode/__version__.py +1 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode/agent.py +105 -9
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode/repl.py +2 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode/tools.py +111 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode.egg-info/PKG-INFO +1 -1
- {nimcode-0.4.10 → nimcode-0.5.0}/tests/test_agent.py +2 -2
- nimcode-0.4.10/src/nimcode/__version__.py +0 -1
- {nimcode-0.4.10 → nimcode-0.5.0}/README.md +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/setup.cfg +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/setup.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode/__init__.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode/cli.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode/config.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode/lenient_parser.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode/mcp_client.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode/memory.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode/model_registry.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode/nim_client.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode/permissions.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode/plugin_manager.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode/rag.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode/repo_map.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode/updater.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode/watcher.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode.egg-info/SOURCES.txt +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode.egg-info/dependency_links.txt +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode.egg-info/entry_points.txt +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode.egg-info/requires.txt +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/src/nimcode.egg-info/top_level.txt +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/tests/test_cli.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/tests/test_config.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/tests/test_lenient_parser.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/tests/test_mcp_client.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/tests/test_memory.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/tests/test_model_registry.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/tests/test_nim_client.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/tests/test_permissions.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/tests/test_plan_injection.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/tests/test_plugin_manager.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/tests/test_repl.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/tests/test_repl_extra.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/tests/test_repl_fixes.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/tests/test_repl_trust.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/tests/test_repo_map.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/tests/test_tools.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/tests/test_updater.py +0 -0
- {nimcode-0.4.10 → nimcode-0.5.0}/tests/test_version.py +0 -0
|
@@ -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
|
-
-
|
|
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
|
-
#
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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}")
|
|
@@ -450,6 +450,8 @@ class NimcodeREPL:
|
|
|
450
450
|
" - Step C: Repeat Step B for Phase 2, Phase 3, etc., updating the same single file until all phases are fully detailed in Project Manager style.\n"
|
|
451
451
|
" - Step D: After all phases are detailed, overwrite '.nimcode/active_plan.txt' with the master plan's path.\n"
|
|
452
452
|
"4. General Rules:\n"
|
|
453
|
+
" - STRICT FIDELITY: You MUST absolutely respect the languages, frameworks, endpoints, tables, and features mentioned in the user's project files or SDD. Do NOT default to Python/Pytest if another language is specified. Do NOT omit database tables. Do NOT generalize specific endpoints into 'REST API'.\n"
|
|
454
|
+
" - BUSINESS LOGIC: Do not ignore 'non-technical' sections like Monetization, ASO, or Affiliate integrations. They MUST be included as explicit tasks.\n"
|
|
453
455
|
" - Quote real names, modules, and requirements. Do not invent generic projects.\n"
|
|
454
456
|
" - NEVER output a generic software-lifecycle template (Phase1: MVP / Design / Development) unless instructed. Focus on code, DB schemas, and technical choices.\n"
|
|
455
457
|
" - After writing any plan file, overwrite '.nimcode/active_plan.txt' with the file's path."
|
|
@@ -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}"
|
|
@@ -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
|
|
286
|
-
assert "prettier" in
|
|
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.10"
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|