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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nanocode-cli
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: A small terminal coding agent written in Python
5
5
  Author-email: hit9 <hit9@icloud.com>
6
6
  License-Expression: BSD-3-Clause
@@ -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.0"
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, list(args), intention, output))
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
- rows = None if "\n" in str(request["pattern"]) else self.rg_matches(request)
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 pattern in patterns:
984
- pattern = pattern.rstrip("/")
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 ("/" in pattern and fnmatch.fnmatch(rel, pattern)) or fnmatch.fnmatch(name, pattern) or fnmatch.fnmatch(rel, pattern):
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nanocode-cli
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: A small terminal coding agent written in Python
5
5
  Author-email: hit9 <hit9@icloud.com>
6
6
  License-Expression: BSD-3-Clause
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nanocode-cli"
7
- version = "0.5.0"
7
+ version = "0.5.1"
8
8
  description = "A small terminal coding agent written in Python"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
File without changes
File without changes
File without changes
File without changes