nimcode 0.1.0__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.
nimcode/tools.py ADDED
@@ -0,0 +1,323 @@
1
+ import os
2
+ import subprocess
3
+ import glob
4
+ import re
5
+ import shlex
6
+ from typing import Dict, Any, List, Optional
7
+
8
+ class ToolError(Exception):
9
+ pass
10
+
11
+ class ToolRegistry:
12
+ @staticmethod
13
+ def get_tool_schema(tool_name: str) -> Optional[Dict[str, Any]]:
14
+ schemas = {
15
+ "Bash": {
16
+ "description": "Execute a shell command. Use this for running tests, git commands, etc.",
17
+ "parameters": {
18
+ "command": {"type": "string", "description": "The shell command to execute."}
19
+ },
20
+ "required": ["command"]
21
+ },
22
+ "Read": {
23
+ "description": "Read the contents of a file.",
24
+ "parameters": {
25
+ "file_path": {"type": "string", "description": "Path to the file to read."}
26
+ },
27
+ "required": ["file_path"]
28
+ },
29
+ "Write": {
30
+ "description": "Create a new file or completely overwrite an existing one.",
31
+ "parameters": {
32
+ "file_path": {"type": "string", "description": "Path to the file."},
33
+ "content": {"type": "string", "description": "Full new content of the file."}
34
+ },
35
+ "required": ["file_path", "content"]
36
+ },
37
+ "Edit": {
38
+ "description": "Edit an existing file by replacing an exact string. old_string must be unique.",
39
+ "parameters": {
40
+ "file_path": {"type": "string", "description": "Path to the file."},
41
+ "old_string": {"type": "string", "description": "The exact string to replace, including whitespaces."},
42
+ "new_string": {"type": "string", "description": "The string to replace it with."}
43
+ },
44
+ "required": ["file_path", "old_string", "new_string"]
45
+ },
46
+ "ReadArchitecture": {
47
+ "description": "Output a fast folder tree to understand project structure.",
48
+ "parameters": {
49
+ "directory": {"type": "string", "description": "Directory to scan (default: '.')"}
50
+ },
51
+ "required": []
52
+ },
53
+ "SymbolSearch": {
54
+ "description": "Search for a class or function definition across the codebase.",
55
+ "parameters": {
56
+ "symbol_name": {"type": "string", "description": "The exact name of the class or function to find."},
57
+ "directory": {"type": "string", "description": "Directory to search in."}
58
+ },
59
+ "required": ["symbol_name", "directory"]
60
+ },
61
+ "Glob": {
62
+ "description": "Find files by pattern.",
63
+ "parameters": {
64
+ "pattern": {"type": "string", "description": "Glob pattern (e.g., src/**/*.py)"}
65
+ },
66
+ "required": ["pattern"]
67
+ },
68
+ "Grep": {
69
+ "description": "Search for text inside files.",
70
+ "parameters": {
71
+ "query": {"type": "string", "description": "The text or regex to search for."},
72
+ "directory": {"type": "string", "description": "Directory to search in."}
73
+ },
74
+ "required": ["query", "directory"]
75
+ }
76
+ }
77
+ return schemas.get(tool_name)
78
+
79
+ @classmethod
80
+ def validate_tool_call(cls, tool_call: Dict[str, Any]) -> None:
81
+ """Validates tool name and required arguments."""
82
+ if not isinstance(tool_call, dict):
83
+ raise ValueError("Tool call must be a dictionary.")
84
+
85
+ tool_name = tool_call.get("tool")
86
+ if not tool_name:
87
+ raise ValueError("Missing 'tool' key in tool call.")
88
+
89
+ args = tool_call.get("args", {})
90
+ if not isinstance(args, dict):
91
+ raise ValueError("'args' must be a dictionary.")
92
+
93
+ schema = cls.get_tool_schema(tool_name)
94
+ if not schema:
95
+ raise ValueError(f"Unknown tool: '{tool_name}'")
96
+
97
+ for req in schema.get("required", []):
98
+ if req not in args:
99
+ raise ValueError(f"Tool '{tool_name}' missing required argument '{req}'")
100
+
101
+ @classmethod
102
+ def execute(cls, tool_call: Dict[str, Any], cwd: str = ".") -> str:
103
+ """Executes the tool call and returns the output as a string."""
104
+ cls.validate_tool_call(tool_call)
105
+
106
+ tool_name = tool_call["tool"]
107
+ args = tool_call["args"]
108
+
109
+ try:
110
+ if tool_name == "Bash":
111
+ return cls._execute_bash(args["command"], cwd)
112
+ elif tool_name == "Read":
113
+ return cls._execute_read(args["file_path"], cwd)
114
+ elif tool_name == "Write":
115
+ return cls._execute_write(args["file_path"], args["content"], cwd)
116
+ elif tool_name == "Edit":
117
+ return cls._execute_edit(args["file_path"], args["old_string"], args["new_string"], cwd)
118
+ elif tool_name == "Glob":
119
+ return cls._execute_glob(args["pattern"], cwd)
120
+ elif tool_name == "Grep":
121
+ return cls._execute_grep(args["query"], args["directory"], cwd)
122
+ elif tool_name == "ReadArchitecture":
123
+ return cls._execute_read_architecture(args.get("directory", "."), cwd)
124
+ elif tool_name == "SymbolSearch":
125
+ return cls._execute_symbol_search(args["symbol_name"], args["directory"], cwd)
126
+ else:
127
+ raise ToolError(f"Tool {tool_name} is registered but execution is not implemented.")
128
+ except Exception as e:
129
+ if isinstance(e, ToolError):
130
+ return f"ToolError: {str(e)}"
131
+ return f"Error executing {tool_name}: {str(e)}"
132
+
133
+ @staticmethod
134
+ def _execute_bash(command: str, cwd: str) -> str:
135
+ try:
136
+ result = subprocess.run(
137
+ command,
138
+ shell=True,
139
+ cwd=cwd,
140
+ stdout=subprocess.PIPE,
141
+ stderr=subprocess.PIPE,
142
+ text=True,
143
+ timeout=120
144
+ )
145
+ out = result.stdout + "\n" + result.stderr
146
+ out = out.strip()
147
+
148
+ # Truncate if too long (simulating context limit protection)
149
+ if len(out) > 10000:
150
+ out = out[:5000] + f"\n...[TRUNCATED {len(out) - 10000} characters]...\n" + out[-5000:]
151
+
152
+ return out if out else "Command executed successfully with no output."
153
+ except subprocess.TimeoutExpired:
154
+ return "Error: Command timed out after 120 seconds."
155
+ except Exception as e:
156
+ return f"Error running bash: {e}"
157
+
158
+ _FILE_CACHE = {}
159
+
160
+ @staticmethod
161
+ def _execute_read(file_path: str, cwd: str) -> str:
162
+ full_path = os.path.join(cwd, file_path)
163
+ if not os.path.exists(full_path):
164
+ raise ToolError(f"File not found: {file_path}")
165
+ if os.path.isdir(full_path):
166
+ raise ToolError(f"Path is a directory, not a file: {file_path}")
167
+
168
+ mtime = os.path.getmtime(full_path)
169
+ if full_path in ToolRegistry._FILE_CACHE:
170
+ cached_mtime, cached_content = ToolRegistry._FILE_CACHE[full_path]
171
+ if cached_mtime == mtime:
172
+ return cached_content
173
+
174
+ with open(full_path, "r", encoding="utf-8") as f:
175
+ content = f.read()
176
+ ToolRegistry._FILE_CACHE[full_path] = (mtime, content)
177
+ return content
178
+
179
+ @staticmethod
180
+ def _execute_write(file_path: str, content: str, cwd: str) -> str:
181
+ full_path = os.path.join(cwd, file_path)
182
+ os.makedirs(os.path.dirname(os.path.abspath(full_path)) or ".", exist_ok=True)
183
+
184
+ with open(full_path, "w", encoding="utf-8") as f:
185
+ f.write(content)
186
+ return f"Successfully wrote to {file_path}"
187
+
188
+ @staticmethod
189
+ def _execute_edit(file_path: str, old_string: str, new_string: str, cwd: str) -> str:
190
+ full_path = os.path.join(cwd, file_path)
191
+ if not os.path.exists(full_path):
192
+ raise ToolError(f"File not found: {file_path}")
193
+
194
+ with open(full_path, "r", encoding="utf-8") as f:
195
+ content = f.read()
196
+
197
+ count = content.count(old_string)
198
+ if count == 0:
199
+ raise ToolError(f"old_string not found in file. Make sure exact whitespace is matched.")
200
+ elif count > 1:
201
+ raise ToolError(f"old_string found {count} times. The old_string must be unique in the file to avoid ambiguous edits.")
202
+
203
+ new_content = content.replace(old_string, new_string, 1)
204
+
205
+ with open(full_path, "w", encoding="utf-8") as f:
206
+ f.write(new_content)
207
+
208
+ return f"Successfully edited {file_path}"
209
+
210
+ @staticmethod
211
+ def _execute_glob(pattern: str, cwd: str) -> str:
212
+ # We need to support recursive globbing
213
+ results = glob.glob(os.path.join(cwd, pattern), recursive=True)
214
+
215
+ if not results:
216
+ return "No files found matching pattern."
217
+
218
+ relative_results = [os.path.relpath(p, cwd) for p in results]
219
+ return "\n".join(relative_results)
220
+
221
+ @staticmethod
222
+ def _execute_grep(query: str, directory: str, cwd: str) -> str:
223
+ full_dir = os.path.join(cwd, directory)
224
+ if not os.path.exists(full_dir):
225
+ raise ToolError(f"Directory not found: {directory}")
226
+
227
+ # Using git grep or standard recursive regex search
228
+ # We will use Python's re module across files if ripgrep isn't guaranteed.
229
+ results = []
230
+ try:
231
+ regex = re.compile(query)
232
+ for root, _, files in os.walk(full_dir):
233
+ # Skip .git and venv to avoid huge searches
234
+ if '.git' in root or 'venv' in root or '__pycache__' in root:
235
+ continue
236
+ for file in files:
237
+ file_path = os.path.join(root, file)
238
+ try:
239
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
240
+ lines = f.readlines()
241
+ for i, line in enumerate(lines):
242
+ if regex.search(line):
243
+ rel_path = os.path.relpath(file_path, cwd)
244
+ results.append(f"{rel_path}:{i+1}:{line.strip()}")
245
+ except Exception:
246
+ pass
247
+ except re.error as e:
248
+ raise ToolError(f"Invalid regex query: {e}")
249
+
250
+ if not results:
251
+ return "No matches found."
252
+
253
+ return "\n".join(results[:100]) # Cap at 100 results
254
+
255
+ @staticmethod
256
+ def _execute_read_architecture(directory: str, cwd: str) -> str:
257
+ """Fast tree-like directory structure output."""
258
+ full_dir = os.path.join(cwd, directory)
259
+ if not os.path.exists(full_dir):
260
+ raise ToolError(f"Directory not found: {directory}")
261
+
262
+ output = []
263
+ for root, dirs, files in os.walk(full_dir):
264
+ if '.git' in dirs: dirs.remove('.git')
265
+ if 'venv' in dirs: dirs.remove('venv')
266
+ if '__pycache__' in dirs: dirs.remove('__pycache__')
267
+ if 'node_modules' in dirs: dirs.remove('node_modules')
268
+
269
+ level = root.replace(full_dir, '').count(os.sep)
270
+ indent = ' ' * 4 * level
271
+ output.append(f"{indent}{os.path.basename(root)}/")
272
+ subindent = ' ' * 4 * (level + 1)
273
+ for f in files:
274
+ output.append(f"{subindent}{f}")
275
+
276
+ if len(output) > 2000:
277
+ output.append("... [TRUNCATED DUE TO SIZE] ...")
278
+ break
279
+
280
+ return "\n".join(output)
281
+
282
+ @staticmethod
283
+ def _execute_symbol_search(symbol_name: str, directory: str, cwd: str) -> str:
284
+ """Search for class/function definition using regex heuristics."""
285
+ full_dir = os.path.join(cwd, directory)
286
+
287
+ # We look for "class SymbolName" or "def symbol_name" or "function symbolName"
288
+ regexes = [
289
+ re.compile(r"^\s*class\s+" + re.escape(symbol_name) + r"\b"),
290
+ re.compile(r"^\s*def\s+" + re.escape(symbol_name) + r"\b"),
291
+ re.compile(r"^\s*function\s+" + re.escape(symbol_name) + r"\b"),
292
+ re.compile(r"^\s*(const|let|var)\s+" + re.escape(symbol_name) + r"\s*=\s*(\(|function)"),
293
+ re.compile(r"^\s*" + re.escape(symbol_name) + r"\s*:\s*function"),
294
+ ]
295
+
296
+ results = []
297
+ for root, _, files in os.walk(full_dir):
298
+ if '.git' in root or 'venv' in root or 'node_modules' in root:
299
+ continue
300
+ for file in files:
301
+ if not file.endswith((".py", ".js", ".ts", ".java", ".cpp", ".c", ".h", ".cs", ".go", ".rs")):
302
+ continue
303
+ file_path = os.path.join(root, file)
304
+ try:
305
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
306
+ lines = f.readlines()
307
+ for i, line in enumerate(lines):
308
+ for regex in regexes:
309
+ if regex.search(line):
310
+ rel_path = os.path.relpath(file_path, cwd)
311
+ # Get some context around the match
312
+ start = max(0, i - 2)
313
+ end = min(len(lines), i + 5)
314
+ context = "".join(lines[start:end])
315
+ results.append(f"Match in {rel_path}:{i+1}:\n{context}\n{'-'*40}")
316
+ break
317
+ except Exception:
318
+ pass
319
+
320
+ if not results:
321
+ return f"No definition found for symbol '{symbol_name}'."
322
+ return "\n".join(results[:10]) # Cap at 10 matches
323
+
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: nimcode
3
+ Version: 0.1.0
4
+ Summary: A standalone, robust coding agent for NVIDIA NIM models.
5
+ Author: Autonomous Agent
6
+ Requires-Dist: httpx>=0.27.0
7
+ Requires-Dist: rich>=13.7.0
8
+ Requires-Dist: prompt_toolkit>=3.0.0
9
+ Requires-Dist: mcp>=1.2.0
10
+ Dynamic: author
11
+ Dynamic: requires-dist
12
+ Dynamic: summary
@@ -0,0 +1,15 @@
1
+ nimcode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ nimcode/agent.py,sha256=IziXvk4KCuRovZ9QZzrn3osPmOT1zb4Z4kua9xrKprk,31308
3
+ nimcode/cli.py,sha256=sFUHjDFmLhlUqXTtjTJeeIzh5Lvf1xt26fkIQI0eYXQ,4998
4
+ nimcode/config.py,sha256=nP-L1AatBhjy43NwpJADeIbRXRGFgJNGNZg5omt4f00,1763
5
+ nimcode/lenient_parser.py,sha256=IHV8ngqEaNkO2UCGBz4rVelS5KScd5jn6t1Tn0pBMkU,4324
6
+ nimcode/mcp_client.py,sha256=WYk5IK5H_B0W8eGBDwrmPnDuzbQyugObVW9morrmTm8,3247
7
+ nimcode/memory.py,sha256=thIWzmOZnBANNKdnCwcCaHQlVd4ux5nech4nADwbxcU,2670
8
+ nimcode/nim_client.py,sha256=dSqDT75w85oTo1lOneQ1jLesUphe6qGj2WKEF1NazCw,5026
9
+ nimcode/permissions.py,sha256=xF-lHCWJ-GobYB9uedmR33-0Qm6z-iW6-aLWR4Q9IqU,3323
10
+ nimcode/tools.py,sha256=wqb_CAJXgjmm4Je26c0Q4bMtih43PqSEAXn0l72GI4I,14186
11
+ nimcode-0.1.0.dist-info/METADATA,sha256=3lRgqkxPzyqTZnSSyLaxYzgSWGXXLXVy2xPaJvUmUOk,330
12
+ nimcode-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
13
+ nimcode-0.1.0.dist-info/entry_points.txt,sha256=OAt4dLnL1T5PN7Wn4zgCGnBQbHeFhMPkZ008Ra4xXAQ,45
14
+ nimcode-0.1.0.dist-info/top_level.txt,sha256=hYSK5DPWx7g3f19Ef0D7g1Z6wVqJrE2B6QXM5RqnAm8,8
15
+ nimcode-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ nimcode = nimcode.cli:main
@@ -0,0 +1 @@
1
+ nimcode