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.
- {nimcode-0.3.4 → nimcode-0.4.0}/PKG-INFO +1 -1
- {nimcode-0.3.4 → nimcode-0.4.0}/README.md +27 -7
- {nimcode-0.3.4 → nimcode-0.4.0}/setup.py +7 -1
- nimcode-0.4.0/src/nimcode/__version__.py +1 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/agent.py +109 -29
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/config.py +10 -1
- nimcode-0.4.0/src/nimcode/model_registry.py +54 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/nim_client.py +26 -11
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/permissions.py +12 -2
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/repl.py +22 -9
- nimcode-0.4.0/src/nimcode/repo_map.py +97 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/updater.py +1 -1
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode.egg-info/PKG-INFO +1 -1
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode.egg-info/SOURCES.txt +7 -1
- {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_config.py +10 -0
- nimcode-0.4.0/tests/test_model_registry.py +38 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_permissions.py +4 -2
- nimcode-0.4.0/tests/test_repo_map.py +93 -0
- nimcode-0.4.0/tests/test_version.py +24 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/setup.cfg +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/__init__.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/cli.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/lenient_parser.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/mcp_client.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/memory.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/plugin_manager.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/rag.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/tools.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode/watcher.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode.egg-info/dependency_links.txt +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode.egg-info/entry_points.txt +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode.egg-info/requires.txt +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/src/nimcode.egg-info/top_level.txt +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_agent.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_cli.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_lenient_parser.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_mcp_client.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_memory.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_nim_client.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_plugin_manager.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_repl.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_repl_extra.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_repl_trust.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_tools.py +0 -0
- {nimcode-0.3.4 → nimcode-0.4.0}/tests/test_updater.py +0 -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
|
|
124
|
+
### Advanced Configuration
|
|
125
125
|
|
|
126
|
-
NimCode's
|
|
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,
|
|
131
|
-
"timeout_llm": 120,
|
|
132
|
-
"timeout_format": 10,
|
|
133
|
-
"timeout_browser": 15000,
|
|
134
|
-
"timeout_updater": 3
|
|
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="
|
|
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
|
-
|
|
15
|
-
|
|
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.
|
|
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
|
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
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 =
|
|
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
|
-
#
|
|
67
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 =
|
|
95
|
-
base_delay =
|
|
96
|
-
max_delay =
|
|
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
|
-
|
|
43
|
-
|
|
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]⏪
|
|
534
|
+
console.print("[bold blue]⏪ Agent Replay (Thinkback)[/bold blue]")
|
|
535
535
|
from rich.table import Table
|
|
536
|
-
table = Table(title="
|
|
536
|
+
table = Table(title="Session History")
|
|
537
537
|
table.add_column("Turn", style="cyan")
|
|
538
|
-
table.add_column("
|
|
539
|
-
table.add_column("
|
|
540
|
-
table.
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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"
|
|
@@ -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
|
-
#
|
|
12
|
-
|
|
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
|
|
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
|