nanocode-cli 0.5.0__tar.gz → 0.5.1__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.
- {nanocode_cli-0.5.0/nanocode_cli.egg-info → nanocode_cli-0.5.1}/PKG-INFO +1 -1
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.1}/nanocode.py +50 -11
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.1/nanocode_cli.egg-info}/PKG-INFO +1 -1
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.1}/pyproject.toml +1 -1
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.1}/LICENSE +0 -0
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.1}/MANIFEST.in +0 -0
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.1}/README.md +0 -0
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.1}/nanocode_cli.egg-info/SOURCES.txt +0 -0
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.1}/nanocode_cli.egg-info/dependency_links.txt +0 -0
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.1}/nanocode_cli.egg-info/entry_points.txt +0 -0
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.1}/nanocode_cli.egg-info/requires.txt +0 -0
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.1}/nanocode_cli.egg-info/top_level.txt +0 -0
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.1}/setup.cfg +0 -0
|
@@ -54,7 +54,7 @@ from prompt_toolkit.widgets import SearchToolbar
|
|
|
54
54
|
from rich.console import Console
|
|
55
55
|
from rich.markdown import Markdown
|
|
56
56
|
|
|
57
|
-
__version__ = "0.5.
|
|
57
|
+
__version__ = "0.5.1"
|
|
58
58
|
|
|
59
59
|
Json = dict[str, Any]
|
|
60
60
|
HTTP_USER_AGENT = "nanocode/" + __version__
|
|
@@ -92,6 +92,22 @@ class ToolError(NanocodeError):
|
|
|
92
92
|
pass
|
|
93
93
|
|
|
94
94
|
|
|
95
|
+
class Text:
|
|
96
|
+
@staticmethod
|
|
97
|
+
def clean(text: str) -> str:
|
|
98
|
+
return text.encode("utf-8", errors="replace").decode("utf-8")
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def value(cls, value: Any) -> Any:
|
|
102
|
+
if isinstance(value, str):
|
|
103
|
+
return cls.clean(value)
|
|
104
|
+
if isinstance(value, dict):
|
|
105
|
+
return {cls.clean(str(key)): cls.value(item) for key, item in value.items()}
|
|
106
|
+
if isinstance(value, (list, tuple)):
|
|
107
|
+
return [cls.value(item) for item in value]
|
|
108
|
+
return value
|
|
109
|
+
|
|
110
|
+
|
|
95
111
|
@dataclass
|
|
96
112
|
class ProviderConfig:
|
|
97
113
|
ALIYUN_CHAT_REASONING_RULES: ClassVar[tuple[tuple[str, tuple[str, ...]], ...]] = (
|
|
@@ -505,8 +521,9 @@ class Session:
|
|
|
505
521
|
def store_tool_result(self, name: str, args: list[Any], intention: str, output: str) -> str:
|
|
506
522
|
self.tool_counter += 1
|
|
507
523
|
key = f"tr.{self.tool_counter}"
|
|
524
|
+
args, intention, output = Text.value(list(args)), Text.clean(intention), Text.clean(output)
|
|
508
525
|
self.tool_results[key] = output
|
|
509
|
-
self.tool_records.append(ToolResultRecord(key, name,
|
|
526
|
+
self.tool_records.append(ToolResultRecord(key, name, args, intention, output))
|
|
510
527
|
if len(self.tool_results) > 400:
|
|
511
528
|
old = self.tool_records.pop(0)
|
|
512
529
|
self.tool_results.pop(old.key, None)
|
|
@@ -521,7 +538,7 @@ class Session:
|
|
|
521
538
|
return count
|
|
522
539
|
|
|
523
540
|
def record_tool_error(self, key: str, name: str, args: list[Any], intention: str, error: str) -> None:
|
|
524
|
-
self.tool_errors.append(ToolErrorRecord(key, name, list(args), intention, " ".join(error.split())))
|
|
541
|
+
self.tool_errors.append(ToolErrorRecord(key, name, Text.value(list(args)), Text.clean(intention), " ".join(Text.clean(error).split())))
|
|
525
542
|
self.tool_errors = self.tool_errors[-5:]
|
|
526
543
|
|
|
527
544
|
|
|
@@ -849,7 +866,9 @@ class SearchTool(Tool):
|
|
|
849
866
|
return requests
|
|
850
867
|
|
|
851
868
|
def search(self, request: Json) -> str:
|
|
852
|
-
|
|
869
|
+
patterns = self.gitignore_patterns(str(request["path"]))
|
|
870
|
+
rows = [] if self.default_ignored(str(request["path"]), patterns) else None
|
|
871
|
+
rows = rows if rows is not None or "\n" in str(request["pattern"]) else self.rg_matches(request)
|
|
853
872
|
rows = rows if rows is not None else self.python_matches(request)
|
|
854
873
|
header = f"<SearchToolResult pattern={json.dumps(request['pattern'])} matches={len(rows)}>"
|
|
855
874
|
return "\n".join([header, *rows, "</SearchToolResult>"])
|
|
@@ -899,10 +918,12 @@ class SearchTool(Tool):
|
|
|
899
918
|
return rows
|
|
900
919
|
|
|
901
920
|
def files(self, root: str, glob_pattern: str) -> list[str]:
|
|
921
|
+
gitignore = self.gitignore_patterns(root)
|
|
922
|
+
if self.default_ignored(root, gitignore):
|
|
923
|
+
return []
|
|
902
924
|
if os.path.isfile(root):
|
|
903
925
|
return [root]
|
|
904
926
|
found = []
|
|
905
|
-
gitignore = self.gitignore_patterns(root)
|
|
906
927
|
skip_dirs = {".git", ".hg", ".svn", "__pycache__", ".mypy_cache", ".pytest_cache", ".ruff_cache", "node_modules"}
|
|
907
928
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
908
929
|
dirnames[:] = [
|
|
@@ -980,14 +1001,25 @@ class SearchTool(Tool):
|
|
|
980
1001
|
def ignored(self, path: str, patterns: list[str]) -> bool:
|
|
981
1002
|
rel = self.session.relpath(path).replace(os.sep, "/")
|
|
982
1003
|
name = os.path.basename(path)
|
|
983
|
-
for
|
|
984
|
-
|
|
1004
|
+
parts = [part for part in rel.split("/") if part and part != "."]
|
|
1005
|
+
for raw in patterns:
|
|
1006
|
+
directory = raw.endswith("/")
|
|
1007
|
+
pattern = raw.rstrip("/")
|
|
985
1008
|
if not pattern:
|
|
986
1009
|
continue
|
|
987
|
-
if
|
|
1010
|
+
if "/" in pattern:
|
|
1011
|
+
matched = fnmatch.fnmatch(rel, pattern) or (directory and (rel == pattern or rel.startswith(pattern + "/")))
|
|
1012
|
+
else:
|
|
1013
|
+
matched = any(fnmatch.fnmatch(part, pattern) for part in parts) or fnmatch.fnmatch(name, pattern) or fnmatch.fnmatch(rel, pattern)
|
|
1014
|
+
if matched:
|
|
988
1015
|
return True
|
|
989
1016
|
return False
|
|
990
1017
|
|
|
1018
|
+
def default_ignored(self, path: str, patterns: list[str]) -> bool:
|
|
1019
|
+
rel = self.session.relpath(path).replace(os.sep, "/")
|
|
1020
|
+
hidden = rel not in {"", "."} and any(part.startswith(".") for part in rel.split("/") if part and part != ".")
|
|
1021
|
+
return hidden or self.ignored(path, patterns)
|
|
1022
|
+
|
|
991
1023
|
|
|
992
1024
|
class CodeIndex:
|
|
993
1025
|
AUTO_UPDATE_LIMIT: ClassVar[int] = 20
|
|
@@ -1795,7 +1827,7 @@ class ContextManager:
|
|
|
1795
1827
|
return key
|
|
1796
1828
|
|
|
1797
1829
|
def model_messages(self, base_system: str, user_input: str = "", extra_messages: list[Json] | None = None) -> list[Json]:
|
|
1798
|
-
return [{"role": "system", "content": base_system.strip()}, {"role": "user", "content": self.render(user_input, extra_messages)}]
|
|
1830
|
+
return Text.value([{"role": "system", "content": base_system.strip()}, {"role": "user", "content": self.render(user_input, extra_messages)}])
|
|
1799
1831
|
|
|
1800
1832
|
def maybe_compact(self, model: "ModelClient", base_system: str, user_input: str = "", extra_messages: list[Json] | None = None) -> None:
|
|
1801
1833
|
if self.estimated_tokens(self.model_messages(base_system, user_input, extra_messages)) < self.session.settings.max_context_tokens:
|
|
@@ -1818,10 +1850,14 @@ class ContextManager:
|
|
|
1818
1850
|
("Discovery Context", self.discovery_context()),
|
|
1819
1851
|
("Error Feedback", self.error_feedback()),
|
|
1820
1852
|
("Latest Tool Results", self.latest_results()),
|
|
1853
|
+
("Current Date", self.current_date()),
|
|
1821
1854
|
("Current User Request", user_input.strip() or "(empty)"),
|
|
1822
1855
|
]
|
|
1823
1856
|
return "\n\n".join(f"--- {name} ---\n{body or '(empty)'}" for name, body in sections)
|
|
1824
1857
|
|
|
1858
|
+
def current_date(self) -> str:
|
|
1859
|
+
return "- date: " + datetime.now().astimezone().strftime("%Y-%m-%d")
|
|
1860
|
+
|
|
1825
1861
|
def environment(self) -> str:
|
|
1826
1862
|
info = self.session.system_info
|
|
1827
1863
|
return "\n".join([
|
|
@@ -2205,10 +2241,11 @@ class DebugTrace:
|
|
|
2205
2241
|
if isinstance(value, (list, tuple)):
|
|
2206
2242
|
return [cls.value(item) for item in value]
|
|
2207
2243
|
if isinstance(value, str):
|
|
2244
|
+
value = Text.clean(value)
|
|
2208
2245
|
return value if len(value) <= cls.STRING_LIMIT else value[: cls.STRING_LIMIT] + "...<truncated>"
|
|
2209
2246
|
if value is None or isinstance(value, (int, float, bool)):
|
|
2210
2247
|
return value
|
|
2211
|
-
return str(value)
|
|
2248
|
+
return Text.clean(str(value))
|
|
2212
2249
|
|
|
2213
2250
|
@classmethod
|
|
2214
2251
|
def prompt(cls, session: Session, *, activity: str, messages: list[Json]) -> None:
|
|
@@ -2268,6 +2305,7 @@ class ModelClient:
|
|
|
2268
2305
|
self.session.state.current_model_call_started_at = 0.0
|
|
2269
2306
|
|
|
2270
2307
|
def chat_request(self, messages: list[Json], tools: list[Json] | None = None, *, activity: str = "agent") -> tuple[Json, list[ToolCall], str]:
|
|
2308
|
+
messages = Text.value(messages)
|
|
2271
2309
|
provider = self.session.config.provider
|
|
2272
2310
|
params: Json = {"model": provider.model, "messages": messages, "stream": False}
|
|
2273
2311
|
if tools:
|
|
@@ -2301,7 +2339,7 @@ Compact the nanocode working context.
|
|
|
2301
2339
|
Return exactly one JSON object with keys: summary, goal, plan, known.
|
|
2302
2340
|
Keep only durable facts needed to continue; preserve file paths, symbols, constraints, and tr.N keys.
|
|
2303
2341
|
""".strip()
|
|
2304
|
-
messages = [{"role": "system", "content": prompt}, {"role": "user", "content": context}]
|
|
2342
|
+
messages = [{"role": "system", "content": prompt}, {"role": "user", "content": Text.clean(context)}]
|
|
2305
2343
|
_, _, content = (
|
|
2306
2344
|
self.anthropic_request(messages, None, activity="compact")
|
|
2307
2345
|
if self.session.config.provider.resolved_api() == "anthropic"
|
|
@@ -2373,6 +2411,7 @@ Keep only durable facts needed to continue; preserve file paths, symbols, constr
|
|
|
2373
2411
|
return ",".join(sorted(names)) or "(none)"
|
|
2374
2412
|
|
|
2375
2413
|
def anthropic_request(self, messages: list[Json], tools: list[Json] | None, *, activity: str = "agent") -> tuple[Json, list[ToolCall], str]:
|
|
2414
|
+
messages = Text.value(messages)
|
|
2376
2415
|
provider = self.session.config.provider
|
|
2377
2416
|
params = self.anthropic_params(messages, tools)
|
|
2378
2417
|
DebugTrace.prompt(self.session, activity=activity, messages=messages)
|
|
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
|