nimcode 0.3.4__tar.gz → 0.4.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 (45) hide show
  1. {nimcode-0.3.4 → nimcode-0.4.0}/PKG-INFO +1 -1
  2. {nimcode-0.3.4 → nimcode-0.4.0}/README.md +27 -7
  3. {nimcode-0.3.4 → nimcode-0.4.0}/setup.py +7 -1
  4. nimcode-0.4.0/src/nimcode/__version__.py +1 -0
  5. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/agent.py +109 -29
  6. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/config.py +10 -1
  7. nimcode-0.4.0/src/nimcode/model_registry.py +54 -0
  8. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/nim_client.py +26 -11
  9. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/permissions.py +12 -2
  10. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/repl.py +22 -9
  11. nimcode-0.4.0/src/nimcode/repo_map.py +97 -0
  12. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/updater.py +1 -1
  13. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode.egg-info/PKG-INFO +1 -1
  14. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode.egg-info/SOURCES.txt +7 -1
  15. {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_config.py +10 -0
  16. nimcode-0.4.0/tests/test_model_registry.py +38 -0
  17. {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_permissions.py +4 -2
  18. nimcode-0.4.0/tests/test_repo_map.py +93 -0
  19. nimcode-0.4.0/tests/test_version.py +24 -0
  20. {nimcode-0.3.4 → nimcode-0.4.0}/setup.cfg +0 -0
  21. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/__init__.py +0 -0
  22. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/cli.py +0 -0
  23. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/lenient_parser.py +0 -0
  24. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/mcp_client.py +0 -0
  25. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/memory.py +0 -0
  26. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/plugin_manager.py +0 -0
  27. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/rag.py +0 -0
  28. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/tools.py +0 -0
  29. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/watcher.py +0 -0
  30. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode.egg-info/dependency_links.txt +0 -0
  31. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode.egg-info/entry_points.txt +0 -0
  32. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode.egg-info/requires.txt +0 -0
  33. {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode.egg-info/top_level.txt +0 -0
  34. {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_agent.py +0 -0
  35. {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_cli.py +0 -0
  36. {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_lenient_parser.py +0 -0
  37. {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_mcp_client.py +0 -0
  38. {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_memory.py +0 -0
  39. {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_nim_client.py +0 -0
  40. {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_plugin_manager.py +0 -0
  41. {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_repl.py +0 -0
  42. {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_repl_extra.py +0 -0
  43. {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_repl_trust.py +0 -0
  44. {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_tools.py +0 -0
  45. {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_updater.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nimcode
3
- Version: 0.3.4
3
+ Version: 0.4.0
4
4
  Summary: A standalone, robust coding agent for NVIDIA NIM models.
5
5
  Author: Autonomous Agent
6
6
  Requires-Dist: httpx>=0.27.0
@@ -121,20 +121,40 @@ NimCode creates a `.nimcode` directory inside your projects. This directory acts
121
121
  - `.nimcode/skills/`: Any custom guidelines, framework rules, or memories you teach the agent.
122
122
  - `.nimcode/history/`: File backups for `/undo` capabilities.
123
123
 
124
- ### Advanced Configuration (Timeouts)
124
+ ### Advanced Configuration
125
125
 
126
- NimCode's limits and timeouts can be customized through the `.nimcode/settings.json` file. Setting any timeout to `0` will completely disable it (infinite timeout).
126
+ NimCode's behavior can be fully customized through `~/.nimcode/settings.json` (global) or `.nimcode/settings.json` (per-project). Setting any timeout to `0` disables it (infinite).
127
127
 
128
128
  ```json
129
129
  {
130
- "timeout_command": 1200, // Max time for bash commands (in seconds)
131
- "timeout_llm": 120, // Max time for LLM API responses (in seconds)
132
- "timeout_format": 10, // Max time for auto-formatters like black/prettier
133
- "timeout_browser": 15000, // Max time for browser operations (in milliseconds)
134
- "timeout_updater": 3 // Max time for update checks
130
+ "timeout_command": 1200,
131
+ "timeout_llm": 120,
132
+ "timeout_format": 10,
133
+ "timeout_browser": 15000,
134
+ "timeout_updater": 3,
135
+ "max_turns": 200,
136
+ "max_tokens": 120000,
137
+ "max_retries": 15,
138
+ "retry_base_delay": 2.0,
139
+ "retry_max_delay": 60.0,
140
+ "allow_bash_non_interactive": false
135
141
  }
136
142
  ```
137
143
 
144
+ | Setting | Default | Description |
145
+ |---------|---------|-------------|
146
+ | `timeout_command` | 1200 | Max seconds for bash commands (0 = infinite) |
147
+ | `timeout_llm` | 120 | Max seconds for LLM API calls (0 = infinite) |
148
+ | `timeout_format` | 10 | Max seconds for formatters like black/prettier |
149
+ | `timeout_browser` | 15000 | Max ms for browser actions |
150
+ | `timeout_updater` | 3 | Max seconds for update checks |
151
+ | `max_turns` | 200 | Max agent turns per session (0 = unlimited) |
152
+ | `max_tokens` | 120000 | Token budget before auto-compact |
153
+ | `max_retries` | 15 | Max API retry attempts on transient errors |
154
+ | `retry_base_delay` | 2.0 | Base delay for exponential backoff (seconds) |
155
+ | `retry_max_delay` | 60.0 | Max delay cap for backoff (seconds) |
156
+ | `allow_bash_non_interactive` | false | Allow Bash commands in non-interactive/CI mode |
157
+
138
158
  ## 🛡️ Requirements
139
159
 
140
160
  - Python 3.8+
@@ -1,8 +1,14 @@
1
1
  from setuptools import setup, find_packages
2
+ import os
3
+
4
+ # Single source of truth for version
5
+ version = {}
6
+ with open(os.path.join("src", "nimcode", "__version__.py")) as f:
7
+ exec(f.read(), version)
2
8
 
3
9
  setup(
4
10
  name="nimcode",
5
- version="0.3.4",
11
+ version=version["__version__"],
6
12
  packages=find_packages(where="src"),
7
13
  package_dir={"": "src"},
8
14
  install_requires=[
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0"
@@ -11,8 +11,13 @@ from .memory import MemoryManager
11
11
 
12
12
  logger = logging.getLogger(__name__)
13
13
 
14
- SYSTEM_PROMPT = """You are nimcode, an autonomous AI coding assistant.
15
- You have access to a set of tools to read, write, and execute code.
14
+ import platform
15
+
16
+ SYSTEM_PROMPT_TEMPLATE = """You are nimcode, an autonomous AI coding assistant. You are a senior-level developer who writes production-ready code.
17
+
18
+ OPERATING SYSTEM: {os_name}
19
+ SHELL: {shell_info}
20
+ WORKING DIRECTORY: {cwd}
16
21
 
17
22
  CRITICAL INSTRUCTION FOR TOOL CALLING:
18
23
  You must output exactly ONE tool call per turn, formatted exactly as a fenced XML block.
@@ -20,35 +25,97 @@ DO NOT use Markdown code blocks for the tool call. DO NOT output multiple tool c
20
25
 
21
26
  Format:
22
27
  <tool_call>
23
- {"tool": "ToolName", "args": {"arg1": "val1"}}
28
+ {{"tool": "ToolName", "args": {{"arg1": "val1"}}}}
24
29
  </tool_call>
25
30
 
26
31
  Available Tools:
27
- - Bash: {"tool": "Bash", "args": {"command": "string"}}
28
- - Read: {"tool": "Read", "args": {"file_path": "string"}}
29
- - Write: {"tool": "Write", "args": {"file_path": "string", "content": "string"}}
30
- - Edit: {"tool": "Edit", "args": {"file_path": "string", "old_string": "string", "new_string": "string"}}
31
- - Glob: {"tool": "Glob", "args": {"pattern": "string"}}
32
- - Grep: {"tool": "Grep", "args": {"query": "string", "directory": "string"}}
33
-
34
- Before calling a tool, you may optionally use a <think> block to reason about your plan. This helps you execute complex tasks correctly.
35
- For example:
32
+ - Bash: {{"tool": "Bash", "args": {{"command": "string"}}}}
33
+ - Read: {{"tool": "Read", "args": {{"file_path": "string"}}}}
34
+ - Write: {{"tool": "Write", "args": {{"file_path": "string", "content": "string"}}}}
35
+ - Edit: {{"tool": "Edit", "args": {{"file_path": "string", "old_string": "string", "new_string": "string"}}}}
36
+ - Glob: {{"tool": "Glob", "args": {{"pattern": "string"}}}}
37
+ - Grep: {{"tool": "Grep", "args": {{"query": "string", "directory": "string"}}}}
38
+
39
+ Before calling a tool, you may optionally use a <think> block to reason about your plan.
36
40
  <think>
37
41
  I need to check the file contents first to see where the function is defined.
38
42
  </think>
39
43
  <tool_call>
40
- {"tool": "Read", "args": {"file_path": "main.py"}}
44
+ {{"tool": "Read", "args": {{"file_path": "main.py"}}}}
41
45
  </tool_call>
42
46
 
43
- Workspace Guidelines:
44
- - When creating plans, store them inside the `.nimcode/plans/` directory (e.g., `.nimcode/plans/dino_game_plan.md`). Create this directory if it doesn't exist.
45
- - When creating or learning new skills/memories, store them as markdown files inside the `.nimcode/skills/` directory.
47
+ WORKSPACE STRUCTURE:
48
+ - The `.nimcode/` directory is YOUR workspace for plans, skills, and logs. It is NOT where user project code lives.
49
+ - `.nimcode/plans/` Store step-by-step markdown plans here.
50
+ - `.nimcode/skills/` — Store custom guidelines, framework rules, or memories as markdown files.
51
+ - `.nimcode/history/` — File backups for /undo capabilities.
52
+ - User's project code lives in the WORKING DIRECTORY root and its subdirectories.
53
+ - NEVER write user's source code files inside `.nimcode/`. That directory is only for your internal data.
54
+ - When the user asks you to create a file like `main.py`, `app.js`, etc., write it to the WORKING DIRECTORY, not `.nimcode/`.
55
+
56
+ {os_specific_instructions}
57
+
58
+ PLANNING MODE INSTRUCTIONS:
59
+ When in /plan mode or asked to create a plan:
60
+ 1. FIRST: Read the project structure using Glob and Read tools. Understand what already exists.
61
+ 2. SECOND: Analyze the codebase — read key files, understand the architecture, dependencies, and patterns.
62
+ 3. THIRD: Write a DETAILED, ACTIONABLE plan that references actual files and code in the project.
63
+ 4. Your plan must include:
64
+ - Specific files to create/modify (with full paths)
65
+ - Actual code snippets or pseudocode for each change
66
+ - Dependencies to install
67
+ - Testing strategy
68
+ - Step-by-step execution order
69
+ 5. NEVER write generic project management templates (stakeholders, timelines, resources).
70
+ 6. NEVER use placeholders like [Insert Date] or [Insert Dependencies].
71
+ 7. A good plan is one that another developer could follow to implement the feature without asking questions.
72
+
73
+ CODE EXECUTION INSTRUCTIONS:
74
+ When asked to "execute", "implement", or "build" something:
75
+ 1. Write REAL, WORKING code — not echo statements or placeholder scripts.
76
+ 2. Create actual source files with production-quality code.
77
+ 3. Install dependencies, run tests, and verify your work.
78
+ 4. If a plan exists in `.nimcode/plans/`, follow it step by step.
46
79
 
47
80
  When you have completely fulfilled the user's request and have no more tools to run, output the word TASK_COMPLETE.
48
81
  """
49
82
 
83
+ OS_INSTRUCTIONS_WINDOWS = """WINDOWS-SPECIFIC RULES:
84
+ - Use backslash (\\) for file paths in Bash commands, or use forward slash (/) which also works on Windows.
85
+ - Use PowerShell or cmd syntax: `mkdir .nimcode\\plans` (NOT `mkdir -p .nimcode/plans`).
86
+ - Use `type` instead of `cat`, `dir` instead of `ls`, `copy` instead of `cp`.
87
+ - Use `python` instead of `python3`.
88
+ - For creating nested directories, use: `mkdir .nimcode\\plans 2>nul` or `New-Item -ItemType Directory -Force -Path .nimcode\\plans`
89
+ - DO NOT use Unix-only commands like chmod, grep (use findstr), sed, awk.
90
+ """
91
+
92
+ OS_INSTRUCTIONS_UNIX = """UNIX-SPECIFIC RULES:
93
+ - Use forward slash (/) for file paths.
94
+ - Use standard Unix commands: mkdir -p, cat, ls, cp, chmod.
95
+ - Use `python3` if `python` is not available.
96
+ """
97
+
98
+
99
+ def _build_system_prompt(cwd: str) -> str:
100
+ """Builds the system prompt with OS-aware instructions."""
101
+ os_name = platform.system()
102
+ if os_name == "Windows":
103
+ shell_info = "PowerShell / cmd.exe"
104
+ os_specific = OS_INSTRUCTIONS_WINDOWS
105
+ else:
106
+ shell_info = "bash / zsh"
107
+ os_specific = OS_INSTRUCTIONS_UNIX
108
+
109
+ return SYSTEM_PROMPT_TEMPLATE.format(
110
+ os_name=os_name,
111
+ shell_info=shell_info,
112
+ cwd=cwd,
113
+ os_specific_instructions=os_specific,
114
+ )
115
+
116
+
50
117
  class Agent:
51
- def __init__(self, api_key: str, model: str = None, max_turns: int = 30, permission_mode: PermissionMode = PermissionMode.DEFAULT, max_tokens: int = 4000):
118
+ def __init__(self, api_key: str, model: str = None, max_turns: int = None, permission_mode: PermissionMode = PermissionMode.DEFAULT, max_tokens: int = None):
52
119
  # Load global settings
53
120
  self.settings = load_settings()
54
121
  self.model = model or self.settings.get("model", "meta/llama-3.1-70b-instruct")
@@ -57,17 +124,21 @@ class Agent:
57
124
  api_key=api_key,
58
125
  base_url=api_base_url,
59
126
  model=self.model,
60
- timeout=self.settings.get("timeout_llm", 120.0)
127
+ timeout=self.settings.get("timeout_llm", 120.0),
128
+ max_retries=self.settings.get("max_retries", 15),
129
+ retry_base_delay=self.settings.get("retry_base_delay", 2.0),
130
+ retry_max_delay=self.settings.get("retry_max_delay", 60.0),
61
131
  )
62
132
 
63
133
  # Initialize MCP Manager
64
134
  self.mcp = MCPManager(self.settings)
65
135
 
66
- # Base system prompt
67
- final_prompt = SYSTEM_PROMPT + self.mcp.get_system_prompt_additions()
136
+ # Build OS-aware system prompt
137
+ cwd = os.getcwd()
138
+ final_prompt = _build_system_prompt(cwd) + self.mcp.get_system_prompt_additions()
68
139
 
69
140
  # Load skills if present
70
- skills_dir = os.path.join(os.getcwd(), ".nimcode", "skills")
141
+ skills_dir = os.path.join(cwd, ".nimcode", "skills")
71
142
  if os.path.exists(skills_dir) and os.path.isdir(skills_dir):
72
143
  loaded_skills = []
73
144
  for filename in os.listdir(skills_dir):
@@ -91,9 +162,16 @@ class Agent:
91
162
  self.messages: List[Dict[str, Any]] = [
92
163
  {"role": "system", "content": final_prompt}
93
164
  ]
94
- self.max_turns = max_turns
165
+
166
+ # Use config values with parameter overrides
167
+ cfg_max_turns = self.settings.get("max_turns", 200)
168
+ self.max_turns = max_turns if max_turns is not None else (0 if cfg_max_turns == 0 else cfg_max_turns)
169
+
170
+ cfg_max_tokens = self.settings.get("max_tokens", 120000)
171
+ effective_max_tokens = max_tokens if max_tokens is not None else cfg_max_tokens
172
+ self.memory = MemoryManager(max_tokens=effective_max_tokens)
173
+
95
174
  self.permission_engine = PermissionEngine(mode=permission_mode)
96
- self.memory = MemoryManager(max_tokens=max_tokens)
97
175
 
98
176
  def save_history(self):
99
177
  """Saves current conversation to NIMCODE.md"""
@@ -190,7 +268,8 @@ class Agent:
190
268
  import json
191
269
  total_context_chars = sum(len(str(m.get("content", ""))) for m in self.messages)
192
270
  total_est_tokens = total_context_chars // 4
193
- max_context = 128000 # Assume standard Llama-3.1 128k context for now
271
+ from .model_registry import get_context_window
272
+ max_context = get_context_window(self.model)
194
273
  if total_est_tokens > max_context * 0.8:
195
274
  c.print("[bold yellow]⚠️ Context window is over 80% full. Consider running /compact or /clear.[/bold yellow]")
196
275
 
@@ -204,7 +283,9 @@ class Agent:
204
283
  while turn < max_turns:
205
284
  turn += 1
206
285
  try:
207
- response_text = await self.client.chat_one_shot(self.messages)
286
+ # Build a combined prompt from messages for one-shot call
287
+ combined = "\n".join(f"{m['role'].upper()}: {m.get('content','')}" for m in self.messages)
288
+ response_text = await self.client.chat_one_shot(combined)
208
289
  except Exception as e:
209
290
  return f"Subagent error: {e}"
210
291
 
@@ -233,13 +314,12 @@ class Agent:
233
314
  if not old_msgs:
234
315
  return self.messages
235
316
 
236
- summary_prompt = "Please summarize the following conversation concisely. Focus on the final state, any completed goals, and any context needed for the next steps:\n\n"
317
+ summary_prompt = "You are a summarization AI. Please summarize the following conversation concisely. Focus on the final state, any completed goals, and any context needed for the next steps:\n\n"
237
318
  for m in old_msgs:
238
319
  summary_prompt += f"{m['role'].upper()}: {str(m['content'])[:500]}...\n"
239
320
 
240
- messages = [{"role": "system", "content": "You are a summarization AI."}, {"role": "user", "content": summary_prompt}]
241
321
  try:
242
- summary = await self.client.chat_one_shot(messages)
322
+ summary = await self.client.chat_one_shot(summary_prompt)
243
323
  system_msg["content"] += f"\n\n[PREVIOUS MEMORY SUMMARY]\n{summary}"
244
324
  except Exception as e:
245
325
  logger.error(f"Distillation failed: {e}")
@@ -256,7 +336,7 @@ class Agent:
256
336
 
257
337
  cwd = os.getcwd()
258
338
  turn = 0
259
- while turn < self.max_turns:
339
+ while self.max_turns == 0 or turn < self.max_turns:
260
340
  turn += 1
261
341
  logger.info(f"--- Turn {turn} ---")
262
342
 
@@ -11,11 +11,20 @@ def load_settings() -> Dict[str, Any]:
11
11
  "model": "meta/llama-3.1-70b-instruct",
12
12
  "api_base_url": "https://integrate.api.nvidia.com/v1",
13
13
  "mcp_servers": {},
14
+ # Timeouts (0 = disabled/infinite)
14
15
  "timeout_command": 1200, # Timeout for bash commands (seconds)
15
16
  "timeout_llm": 120, # Timeout for LLM API calls (seconds)
16
17
  "timeout_format": 10, # Timeout for formatting/linting tools (seconds)
17
18
  "timeout_browser": 15000, # Timeout for browser actions (milliseconds)
18
- "timeout_updater": 3 # Timeout for update checks (seconds)
19
+ "timeout_updater": 3, # Timeout for update checks (seconds)
20
+ # Agent behavior
21
+ "max_turns": 200, # Max agent turns per session (0 = unlimited)
22
+ "max_tokens": 120000, # Token budget before auto-compact kicks in
23
+ "max_retries": 15, # Max LLM API retry attempts on transient errors
24
+ "retry_base_delay": 2.0, # Base delay for exponential backoff (seconds)
25
+ "retry_max_delay": 60.0, # Max delay cap for exponential backoff (seconds)
26
+ # Security
27
+ "allow_bash_non_interactive": False, # Allow Bash in non-interactive (CI) mode
19
28
  }
20
29
 
21
30
  # Global settings
@@ -0,0 +1,54 @@
1
+ """
2
+ Model registry with context window sizes and metadata.
3
+ Used for auto-compact thresholds and dynamic model listing.
4
+ """
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ # Known NVIDIA NIM models and their context window sizes (in tokens)
10
+ # This acts as a fallback + enrichment layer when API is unavailable
11
+ MODEL_CONTEXT_WINDOWS = {
12
+ # Llama 3.1 family
13
+ "meta/llama-3.1-8b-instruct": 128000,
14
+ "meta/llama-3.1-70b-instruct": 128000,
15
+ "meta/llama-3.1-405b-instruct": 128000,
16
+ # Llama 3.2 family
17
+ "meta/llama-3.2-1b-instruct": 128000,
18
+ "meta/llama-3.2-3b-instruct": 128000,
19
+ "meta/llama-3.2-11b-vision-instruct": 128000,
20
+ "meta/llama-3.2-90b-vision-instruct": 128000,
21
+ # Llama 3.3 family
22
+ "meta/llama-3.3-70b-instruct": 128000,
23
+ # Nemotron
24
+ "nvidia/nemotron-4-340b-instruct": 4096,
25
+ "nvidia/llama-3.1-nemotron-70b-instruct": 128000,
26
+ # Mistral
27
+ "mistralai/mixtral-8x22b-instruct-v0.1": 65536,
28
+ "mistralai/mistral-large-2-instruct": 128000,
29
+ # DeepSeek
30
+ "deepseek-ai/deepseek-r1": 128000,
31
+ # Qwen
32
+ "qwen/qwen2.5-72b-instruct": 128000,
33
+ "qwen/qwq-32b": 128000,
34
+ }
35
+
36
+ # Default context window for unknown models
37
+ DEFAULT_CONTEXT_WINDOW = 8192
38
+
39
+ # Hardcoded fallback model list (used when API is unreachable)
40
+ FALLBACK_MODELS = [
41
+ "meta/llama-3.1-70b-instruct",
42
+ "meta/llama-3.1-8b-instruct",
43
+ "meta/llama-3.1-405b-instruct",
44
+ "meta/llama-3.3-70b-instruct",
45
+ "nvidia/llama-3.1-nemotron-70b-instruct",
46
+ "deepseek-ai/deepseek-r1",
47
+ "mistralai/mixtral-8x22b-instruct-v0.1",
48
+ "qwen/qwen2.5-72b-instruct",
49
+ ]
50
+
51
+
52
+ def get_context_window(model_id: str) -> int:
53
+ """Returns the context window size for a given model ID."""
54
+ return MODEL_CONTEXT_WINDOWS.get(model_id, DEFAULT_CONTEXT_WINDOW)
@@ -8,11 +8,14 @@ from typing import List, Dict, Any, Optional, AsyncGenerator
8
8
  logger = logging.getLogger(__name__)
9
9
 
10
10
  class NimClient:
11
- def __init__(self, api_key: str, base_url: str = "https://integrate.api.nvidia.com/v1", model: str = "meta/llama-3.1-70b-instruct", timeout: float = 120.0):
11
+ def __init__(self, api_key: str, base_url: str = "https://integrate.api.nvidia.com/v1", model: str = "meta/llama-3.1-70b-instruct", timeout: float = 120.0, max_retries: int = 15, retry_base_delay: float = 2.0, retry_max_delay: float = 60.0):
12
12
  self.api_key = api_key
13
13
  self.base_url = base_url.rstrip("/")
14
14
  self.model = model
15
15
  self.timeout = None if timeout == 0 else timeout
16
+ self.max_retries = max_retries
17
+ self.retry_base_delay = retry_base_delay
18
+ self.retry_max_delay = retry_max_delay
16
19
  self.headers = {
17
20
  "Authorization": f"Bearer {self.api_key}",
18
21
  "Content-Type": "application/json",
@@ -20,13 +23,25 @@ class NimClient:
20
23
  }
21
24
 
22
25
  async def get_available_models(self) -> List[str]:
23
- return [
24
- "meta/llama-3.1-70b-instruct",
25
- "meta/llama-3.1-8b-instruct",
26
- "meta/llama-3.1-405b-instruct",
27
- "nvidia/nemotron-4-340b-instruct",
28
- "mistralai/mixtral-8x22b-instruct-v0.1"
29
- ]
26
+ """Fetches available models from the NIM API. Falls back to known models on error."""
27
+ try:
28
+ async with httpx.AsyncClient() as client:
29
+ response = await client.get(
30
+ f"{self.base_url}/models",
31
+ headers=self.headers,
32
+ timeout=10.0
33
+ )
34
+ if response.status_code == 200:
35
+ data = response.json()
36
+ models = [m["id"] for m in data.get("data", []) if m.get("id")]
37
+ if models:
38
+ return sorted(models)
39
+ except Exception as e:
40
+ logger.debug(f"Failed to fetch models from API: {e}")
41
+
42
+ # Fallback to known model list
43
+ from .model_registry import FALLBACK_MODELS
44
+ return FALLBACK_MODELS
30
45
 
31
46
  async def chat_one_shot(self, prompt: str) -> str:
32
47
  payload = {
@@ -91,9 +106,9 @@ class NimClient:
91
106
  "stream": stream
92
107
  }
93
108
 
94
- max_retries = 15
95
- base_delay = 2.0
96
- max_delay = 60.0
109
+ max_retries = self.max_retries
110
+ base_delay = self.retry_base_delay
111
+ max_delay = self.retry_max_delay
97
112
 
98
113
  for attempt in range(max_retries):
99
114
  chunk_yielded = False
@@ -39,8 +39,18 @@ class PermissionEngine:
39
39
  args = tool_call.get("args", {})
40
40
 
41
41
  if not sys.stdin.isatty():
42
- console.print(f"[yellow]Running non-interactive. Approving {tool_name} for tests.[/yellow]")
43
- return True
42
+ # Non-interactive mode: only auto-approve safe tools + writes.
43
+ # Bash requires explicit opt-in via config.
44
+ from .config import load_settings
45
+ settings = load_settings()
46
+ safe_non_interactive = {"Read", "Glob", "Grep", "Write", "Edit", "ASTReplace"}
47
+ if tool_name in safe_non_interactive:
48
+ return True
49
+ if tool_name == "Bash" and settings.get("allow_bash_non_interactive", False):
50
+ console.print(f"[yellow]Non-interactive: Bash auto-approved via config.[/yellow]")
51
+ return True
52
+ console.print(f"[red]Non-interactive: {tool_name} DENIED (enable 'allow_bash_non_interactive' in settings to allow).[/red]")
53
+ return False
44
54
 
45
55
  while True:
46
56
  content = ""
@@ -531,17 +531,30 @@ class NimcodeREPL:
531
531
  self.messages.append({"role": "system", "content": "You are the Application Guardian. Your sole priority is preventing regressions. Before making any code changes, ensure you write a test first. If a user asks for a breaking change, warn them aggressively."})
532
532
  continue
533
533
  elif user_input.strip() == "/thinkback":
534
- console.print("[bold blue]⏪ Entering Agent Replay Mode (Thinkback)[/bold blue]")
534
+ console.print("[bold blue]⏪ Agent Replay (Thinkback)[/bold blue]")
535
535
  from rich.table import Table
536
- table = Table(title="Recent Agent Actions")
536
+ table = Table(title="Session History")
537
537
  table.add_column("Turn", style="cyan")
538
- table.add_column("Tokens", style="yellow")
539
- table.add_column("Tools Called", style="green")
540
- table.add_row("1", "450", "Read, Glob")
541
- table.add_row("2", "1200", "Bash, Write")
542
- table.add_row("3", "300", "StartTerminal")
543
- console.print(table)
544
- console.print("[dim]Note: Full replay logs are stored in .nimcode/logs/[/dim]")
538
+ table.add_column("Role", style="yellow")
539
+ table.add_column("Tokens (est.)", style="green")
540
+ table.add_column("Preview", style="dim", max_width=60)
541
+ turn_num = 0
542
+ for msg in self.messages[1:]: # Skip system prompt
543
+ role = msg.get("role", "?")
544
+ content = str(msg.get("content", ""))
545
+ est_tokens = len(content) // 4 + 1
546
+ preview = content[:80].replace("\n", " ")
547
+ if len(content) > 80:
548
+ preview += "..."
549
+ if role == "user":
550
+ turn_num += 1
551
+ table.add_row(str(turn_num), role, str(est_tokens), preview)
552
+ if turn_num == 0:
553
+ console.print("[dim]No conversation history yet.[/dim]")
554
+ else:
555
+ console.print(table)
556
+ total_tokens = sum(len(str(m.get("content", ""))) // 4 + 1 for m in self.messages)
557
+ console.print(f"[dim]Total messages: {len(self.messages)} | Est. total tokens: {total_tokens}[/dim]")
545
558
  continue
546
559
  elif user_input.strip().startswith("/fix"):
547
560
  parts = user_input.strip().split(" ", 1)
@@ -0,0 +1,97 @@
1
+ """
2
+ Lightweight repository mapper.
3
+ Generates a tree-style overview of the project structure.
4
+ Used by watcher.py for Live Sync updates.
5
+ """
6
+ import os
7
+ import logging
8
+ from typing import List, Optional
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # Directories to always skip
13
+ IGNORE_DIRS = {
14
+ ".git", ".nimcode", "__pycache__", "node_modules", ".venv", "venv",
15
+ ".tox", ".mypy_cache", ".pytest_cache", "dist", "build", ".egg-info",
16
+ ".idea", ".vscode", "env", ".env",
17
+ }
18
+
19
+ # File extensions to include in the map
20
+ CODE_EXTENSIONS = {
21
+ ".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".go", ".rs", ".rb",
22
+ ".c", ".cpp", ".h", ".cs", ".php", ".swift", ".kt", ".scala",
23
+ ".html", ".css", ".scss", ".vue", ".svelte",
24
+ ".json", ".yaml", ".yml", ".toml", ".md", ".txt",
25
+ ".sql", ".sh", ".bat", ".ps1", ".dockerfile",
26
+ }
27
+
28
+ MAX_FILES = 500 # Safety limit
29
+
30
+
31
+ class RepoMapper:
32
+ def __init__(self, root_dir: str, max_depth: int = 6):
33
+ self.root_dir = os.path.abspath(root_dir)
34
+ self.max_depth = max_depth
35
+ self._file_count = 0
36
+
37
+ def generate_map(self) -> str:
38
+ """Generates a tree-style string of the repository structure."""
39
+ self._file_count = 0
40
+ lines = [f"📁 {os.path.basename(self.root_dir)}/"]
41
+ self._walk(self.root_dir, lines, prefix="", depth=0)
42
+
43
+ if self._file_count >= MAX_FILES:
44
+ lines.append(f"\n... (truncated at {MAX_FILES} files)")
45
+
46
+ return "\n".join(lines)
47
+
48
+ def _walk(self, directory: str, lines: List[str], prefix: str, depth: int) -> None:
49
+ if depth >= self.max_depth or self._file_count >= MAX_FILES:
50
+ return
51
+
52
+ try:
53
+ entries = sorted(os.listdir(directory))
54
+ except PermissionError:
55
+ return
56
+
57
+ # Separate dirs and files
58
+ dirs = []
59
+ files = []
60
+ for entry in entries:
61
+ full_path = os.path.join(directory, entry)
62
+ if os.path.isdir(full_path):
63
+ if entry not in IGNORE_DIRS and not entry.startswith("."):
64
+ dirs.append(entry)
65
+ else:
66
+ ext = os.path.splitext(entry)[1].lower()
67
+ if ext in CODE_EXTENSIONS or entry in {"Makefile", "Dockerfile", "Procfile", ".gitignore"}:
68
+ files.append(entry)
69
+
70
+ all_entries = dirs + files
71
+ for i, entry in enumerate(all_entries):
72
+ is_last = (i == len(all_entries) - 1)
73
+ connector = "└── " if is_last else "├── "
74
+
75
+ full_path = os.path.join(directory, entry)
76
+
77
+ if entry in dirs:
78
+ lines.append(f"{prefix}{connector}📁 {entry}/")
79
+ extension = " " if is_last else "│ "
80
+ self._walk(full_path, lines, prefix + extension, depth + 1)
81
+ else:
82
+ self._file_count += 1
83
+ if self._file_count > MAX_FILES:
84
+ return
85
+ size = os.path.getsize(full_path)
86
+ size_str = _format_size(size)
87
+ lines.append(f"{prefix}{connector}{entry} ({size_str})")
88
+
89
+
90
+ def _format_size(size_bytes: int) -> str:
91
+ """Formats file size in human-readable format."""
92
+ if size_bytes < 1024:
93
+ return f"{size_bytes}B"
94
+ elif size_bytes < 1024 * 1024:
95
+ return f"{size_bytes / 1024:.1f}KB"
96
+ else:
97
+ return f"{size_bytes / (1024 * 1024):.1f}MB"
@@ -3,7 +3,7 @@ import logging
3
3
 
4
4
  logger = logging.getLogger(__name__)
5
5
 
6
- CURRENT_VERSION = "3.0.0"
6
+ from .__version__ import __version__ as CURRENT_VERSION
7
7
 
8
8
  class AutoUpdater:
9
9
  @staticmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nimcode
3
- Version: 0.3.4
3
+ Version: 0.4.0
4
4
  Summary: A standalone, robust coding agent for NVIDIA NIM models.
5
5
  Author: Autonomous Agent
6
6
  Requires-Dist: httpx>=0.27.0
@@ -1,17 +1,20 @@
1
1
  README.md
2
2
  setup.py
3
3
  src/nimcode/__init__.py
4
+ src/nimcode/__version__.py
4
5
  src/nimcode/agent.py
5
6
  src/nimcode/cli.py
6
7
  src/nimcode/config.py
7
8
  src/nimcode/lenient_parser.py
8
9
  src/nimcode/mcp_client.py
9
10
  src/nimcode/memory.py
11
+ src/nimcode/model_registry.py
10
12
  src/nimcode/nim_client.py
11
13
  src/nimcode/permissions.py
12
14
  src/nimcode/plugin_manager.py
13
15
  src/nimcode/rag.py
14
16
  src/nimcode/repl.py
17
+ src/nimcode/repo_map.py
15
18
  src/nimcode/tools.py
16
19
  src/nimcode/updater.py
17
20
  src/nimcode/watcher.py
@@ -27,11 +30,14 @@ tests/test_config.py
27
30
  tests/test_lenient_parser.py
28
31
  tests/test_mcp_client.py
29
32
  tests/test_memory.py
33
+ tests/test_model_registry.py
30
34
  tests/test_nim_client.py
31
35
  tests/test_permissions.py
32
36
  tests/test_plugin_manager.py
33
37
  tests/test_repl.py
34
38
  tests/test_repl_extra.py
35
39
  tests/test_repl_trust.py
40
+ tests/test_repo_map.py
36
41
  tests/test_tools.py
37
- tests/test_updater.py
42
+ tests/test_updater.py
43
+ tests/test_version.py
@@ -9,6 +9,16 @@ def test_load_settings_default():
9
9
  settings = load_settings()
10
10
  assert settings["model"] == "meta/llama-3.1-70b-instruct"
11
11
  assert settings["mcp_servers"] == {}
12
+ # New configurable settings
13
+ assert settings["max_turns"] == 200
14
+ assert settings["max_tokens"] == 120000
15
+ assert settings["max_retries"] == 15
16
+ assert settings["retry_base_delay"] == 2.0
17
+ assert settings["retry_max_delay"] == 60.0
18
+ assert settings["allow_bash_non_interactive"] == False
19
+ # Timeouts
20
+ assert settings["timeout_command"] == 1200
21
+ assert settings["timeout_llm"] == 120
12
22
 
13
23
  def test_load_settings_global_only():
14
24
  mock_global = '{"model": "global_model", "mcp_servers": {"global": {}}}'
@@ -0,0 +1,38 @@
1
+ """Tests for the model_registry module."""
2
+ import pytest
3
+ from nimcode.model_registry import (
4
+ MODEL_CONTEXT_WINDOWS,
5
+ FALLBACK_MODELS,
6
+ DEFAULT_CONTEXT_WINDOW,
7
+ get_context_window,
8
+ )
9
+
10
+
11
+ def test_get_context_window_known_model():
12
+ """Known models should return their specific context window size."""
13
+ assert get_context_window("meta/llama-3.1-70b-instruct") == 128000
14
+ assert get_context_window("nvidia/nemotron-4-340b-instruct") == 4096
15
+ assert get_context_window("mistralai/mixtral-8x22b-instruct-v0.1") == 65536
16
+
17
+
18
+ def test_get_context_window_unknown_model():
19
+ """Unknown models should return the default context window."""
20
+ assert get_context_window("some/unknown-model") == DEFAULT_CONTEXT_WINDOW
21
+ assert get_context_window("") == DEFAULT_CONTEXT_WINDOW
22
+
23
+
24
+ def test_fallback_models_not_empty():
25
+ """Fallback model list should have entries."""
26
+ assert len(FALLBACK_MODELS) > 0
27
+
28
+
29
+ def test_fallback_models_all_in_context_windows():
30
+ """All fallback models should have a known context window entry."""
31
+ for model in FALLBACK_MODELS:
32
+ assert model in MODEL_CONTEXT_WINDOWS, f"Fallback model {model} missing from context windows"
33
+
34
+
35
+ def test_context_windows_positive():
36
+ """All context window values should be positive integers."""
37
+ for model, ctx in MODEL_CONTEXT_WINDOWS.items():
38
+ assert isinstance(ctx, int) and ctx > 0, f"Invalid context window for {model}: {ctx}"
@@ -8,8 +8,10 @@ def test_permission_bypass():
8
8
 
9
9
  def test_permission_readonly_blocked():
10
10
  engine = PermissionEngine(mode=PermissionMode.AUTO)
11
- # Bash should still be checked (not returned True immediately) and if not interactive, returns True for now (which is a flaw but let's test current behavior)
12
- assert engine.check_permission({"tool": "Bash", "args": {}}) == True
11
+ # In non-interactive mode (tests), Bash is now DENIED by default.
12
+ # Safe tools (Read, Glob, Grep) and Write/Edit are still auto-approved.
13
+ assert engine.check_permission({"tool": "Read", "args": {}}) == True
14
+ assert engine.check_permission({"tool": "Bash", "args": {}}) == False
13
15
 
14
16
  def test_permission_safe_tools():
15
17
  engine = PermissionEngine(mode=PermissionMode.DEFAULT)
@@ -0,0 +1,93 @@
1
+ """Tests for repo_map module."""
2
+ import os
3
+ import pytest
4
+ import tempfile
5
+ from nimcode.repo_map import RepoMapper, _format_size
6
+
7
+
8
+ def test_format_size_bytes():
9
+ assert _format_size(500) == "500B"
10
+ assert _format_size(0) == "0B"
11
+
12
+
13
+ def test_format_size_kb():
14
+ result = _format_size(2048)
15
+ assert "KB" in result
16
+
17
+
18
+ def test_format_size_mb():
19
+ result = _format_size(2 * 1024 * 1024)
20
+ assert "MB" in result
21
+
22
+
23
+ def test_generate_map_basic(tmp_path):
24
+ """Test basic tree generation with files and directories."""
25
+ # Create a simple project structure
26
+ (tmp_path / "main.py").write_text("print('hello')")
27
+ (tmp_path / "utils.py").write_text("def helper(): pass")
28
+ sub = tmp_path / "src"
29
+ sub.mkdir()
30
+ (sub / "app.py").write_text("class App: pass")
31
+
32
+ mapper = RepoMapper(str(tmp_path))
33
+ result = mapper.generate_map()
34
+
35
+ assert "main.py" in result
36
+ assert "utils.py" in result
37
+ assert "src" in result
38
+ assert "app.py" in result
39
+
40
+
41
+ def test_generate_map_ignores_pycache(tmp_path):
42
+ """Test that __pycache__ directories are ignored."""
43
+ (tmp_path / "main.py").write_text("x = 1")
44
+ cache = tmp_path / "__pycache__"
45
+ cache.mkdir()
46
+ (cache / "main.cpython-311.pyc").write_bytes(b"bytecode")
47
+
48
+ mapper = RepoMapper(str(tmp_path))
49
+ result = mapper.generate_map()
50
+
51
+ assert "main.py" in result
52
+ assert "__pycache__" not in result
53
+
54
+
55
+ def test_generate_map_ignores_node_modules(tmp_path):
56
+ """Test that node_modules is ignored."""
57
+ (tmp_path / "index.js").write_text("console.log('hi')")
58
+ nm = tmp_path / "node_modules"
59
+ nm.mkdir()
60
+ (nm / "package.json").write_text("{}")
61
+
62
+ mapper = RepoMapper(str(tmp_path))
63
+ result = mapper.generate_map()
64
+
65
+ assert "index.js" in result
66
+ assert "node_modules" not in result
67
+
68
+
69
+ def test_generate_map_max_depth(tmp_path):
70
+ """Test that max_depth is respected."""
71
+ # Create a deeply nested structure
72
+ current = tmp_path
73
+ for i in range(10):
74
+ current = current / f"level{i}"
75
+ current.mkdir()
76
+ (current / "file.py").write_text(f"level {i}")
77
+
78
+ mapper = RepoMapper(str(tmp_path), max_depth=3)
79
+ result = mapper.generate_map()
80
+
81
+ assert "level0" in result
82
+ assert "level1" in result
83
+ # Deep levels should NOT appear
84
+ assert "level8" not in result
85
+
86
+
87
+ def test_generate_map_empty_dir(tmp_path):
88
+ """Empty directory should still generate valid output."""
89
+ mapper = RepoMapper(str(tmp_path))
90
+ result = mapper.generate_map()
91
+
92
+ # Should at least have the root directory name
93
+ assert tmp_path.name in result
@@ -0,0 +1,24 @@
1
+ """Tests for __version__ single source of truth."""
2
+ import pytest
3
+
4
+
5
+ def test_version_format():
6
+ """Version should be a valid semver string."""
7
+ from nimcode.__version__ import __version__
8
+ parts = __version__.split(".")
9
+ assert len(parts) == 3, f"Version should be x.y.z, got {__version__}"
10
+ for part in parts:
11
+ assert part.isdigit(), f"Version part '{part}' is not a digit"
12
+
13
+
14
+ def test_version_is_0_4_0():
15
+ """Current version should be 0.4.0."""
16
+ from nimcode.__version__ import __version__
17
+ assert __version__ == "0.4.0"
18
+
19
+
20
+ def test_updater_uses_version():
21
+ """updater.py should use the same version as __version__.py."""
22
+ from nimcode.__version__ import __version__
23
+ from nimcode.updater import CURRENT_VERSION
24
+ assert CURRENT_VERSION == __version__
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