nanocode-cli 0.5.0__tar.gz → 0.5.2__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.2}/PKG-INFO +1 -1
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.2}/nanocode.py +66 -16
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.2/nanocode_cli.egg-info}/PKG-INFO +1 -1
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.2}/pyproject.toml +1 -1
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.2}/LICENSE +0 -0
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.2}/MANIFEST.in +0 -0
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.2}/README.md +0 -0
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.2}/nanocode_cli.egg-info/SOURCES.txt +0 -0
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.2}/nanocode_cli.egg-info/dependency_links.txt +0 -0
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.2}/nanocode_cli.egg-info/entry_points.txt +0 -0
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.2}/nanocode_cli.egg-info/requires.txt +0 -0
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.2}/nanocode_cli.egg-info/top_level.txt +0 -0
- {nanocode_cli-0.5.0 → nanocode_cli-0.5.2}/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.2"
|
|
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
|
|
@@ -1109,15 +1141,22 @@ class CodeIndex:
|
|
|
1109
1141
|
if status not in {"ready", "stale"}:
|
|
1110
1142
|
return False
|
|
1111
1143
|
self.notice("syncing", refreshing=True)
|
|
1144
|
+
try:
|
|
1145
|
+
worker = csi.refresh_async(self.session.cwd)
|
|
1146
|
+
except Exception as error:
|
|
1147
|
+
self.fail(error)
|
|
1148
|
+
return False
|
|
1112
1149
|
|
|
1113
|
-
def
|
|
1150
|
+
def finish() -> None:
|
|
1151
|
+
worker.join()
|
|
1114
1152
|
try:
|
|
1115
|
-
|
|
1116
|
-
self.
|
|
1153
|
+
self.session.state.code_index_refreshing = False
|
|
1154
|
+
self.session.state.code_index_notice = ""
|
|
1155
|
+
self.status(check=True)
|
|
1117
1156
|
except Exception as error:
|
|
1118
1157
|
self.fail(error)
|
|
1119
1158
|
|
|
1120
|
-
threading.Thread(target=
|
|
1159
|
+
threading.Thread(target=finish, daemon=True).start()
|
|
1121
1160
|
return True
|
|
1122
1161
|
|
|
1123
1162
|
def update_paths(self, paths: list[str]) -> list[str]:
|
|
@@ -1795,7 +1834,7 @@ class ContextManager:
|
|
|
1795
1834
|
return key
|
|
1796
1835
|
|
|
1797
1836
|
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)}]
|
|
1837
|
+
return Text.value([{"role": "system", "content": base_system.strip()}, {"role": "user", "content": self.render(user_input, extra_messages)}])
|
|
1799
1838
|
|
|
1800
1839
|
def maybe_compact(self, model: "ModelClient", base_system: str, user_input: str = "", extra_messages: list[Json] | None = None) -> None:
|
|
1801
1840
|
if self.estimated_tokens(self.model_messages(base_system, user_input, extra_messages)) < self.session.settings.max_context_tokens:
|
|
@@ -1818,10 +1857,14 @@ class ContextManager:
|
|
|
1818
1857
|
("Discovery Context", self.discovery_context()),
|
|
1819
1858
|
("Error Feedback", self.error_feedback()),
|
|
1820
1859
|
("Latest Tool Results", self.latest_results()),
|
|
1860
|
+
("Current Date", self.current_date()),
|
|
1821
1861
|
("Current User Request", user_input.strip() or "(empty)"),
|
|
1822
1862
|
]
|
|
1823
1863
|
return "\n\n".join(f"--- {name} ---\n{body or '(empty)'}" for name, body in sections)
|
|
1824
1864
|
|
|
1865
|
+
def current_date(self) -> str:
|
|
1866
|
+
return "- date: " + datetime.now().astimezone().strftime("%Y-%m-%d")
|
|
1867
|
+
|
|
1825
1868
|
def environment(self) -> str:
|
|
1826
1869
|
info = self.session.system_info
|
|
1827
1870
|
return "\n".join([
|
|
@@ -2205,10 +2248,11 @@ class DebugTrace:
|
|
|
2205
2248
|
if isinstance(value, (list, tuple)):
|
|
2206
2249
|
return [cls.value(item) for item in value]
|
|
2207
2250
|
if isinstance(value, str):
|
|
2251
|
+
value = Text.clean(value)
|
|
2208
2252
|
return value if len(value) <= cls.STRING_LIMIT else value[: cls.STRING_LIMIT] + "...<truncated>"
|
|
2209
2253
|
if value is None or isinstance(value, (int, float, bool)):
|
|
2210
2254
|
return value
|
|
2211
|
-
return str(value)
|
|
2255
|
+
return Text.clean(str(value))
|
|
2212
2256
|
|
|
2213
2257
|
@classmethod
|
|
2214
2258
|
def prompt(cls, session: Session, *, activity: str, messages: list[Json]) -> None:
|
|
@@ -2268,6 +2312,7 @@ class ModelClient:
|
|
|
2268
2312
|
self.session.state.current_model_call_started_at = 0.0
|
|
2269
2313
|
|
|
2270
2314
|
def chat_request(self, messages: list[Json], tools: list[Json] | None = None, *, activity: str = "agent") -> tuple[Json, list[ToolCall], str]:
|
|
2315
|
+
messages = Text.value(messages)
|
|
2271
2316
|
provider = self.session.config.provider
|
|
2272
2317
|
params: Json = {"model": provider.model, "messages": messages, "stream": False}
|
|
2273
2318
|
if tools:
|
|
@@ -2301,7 +2346,7 @@ Compact the nanocode working context.
|
|
|
2301
2346
|
Return exactly one JSON object with keys: summary, goal, plan, known.
|
|
2302
2347
|
Keep only durable facts needed to continue; preserve file paths, symbols, constraints, and tr.N keys.
|
|
2303
2348
|
""".strip()
|
|
2304
|
-
messages = [{"role": "system", "content": prompt}, {"role": "user", "content": context}]
|
|
2349
|
+
messages = [{"role": "system", "content": prompt}, {"role": "user", "content": Text.clean(context)}]
|
|
2305
2350
|
_, _, content = (
|
|
2306
2351
|
self.anthropic_request(messages, None, activity="compact")
|
|
2307
2352
|
if self.session.config.provider.resolved_api() == "anthropic"
|
|
@@ -2373,6 +2418,7 @@ Keep only durable facts needed to continue; preserve file paths, symbols, constr
|
|
|
2373
2418
|
return ",".join(sorted(names)) or "(none)"
|
|
2374
2419
|
|
|
2375
2420
|
def anthropic_request(self, messages: list[Json], tools: list[Json] | None, *, activity: str = "agent") -> tuple[Json, list[ToolCall], str]:
|
|
2421
|
+
messages = Text.value(messages)
|
|
2376
2422
|
provider = self.session.config.provider
|
|
2377
2423
|
params = self.anthropic_params(messages, tools)
|
|
2378
2424
|
DebugTrace.prompt(self.session, activity=activity, messages=messages)
|
|
@@ -2925,6 +2971,7 @@ class ModelRetryShortcut:
|
|
|
2925
2971
|
|
|
2926
2972
|
class StatusBar:
|
|
2927
2973
|
INTERVAL: ClassVar[float] = 0.2
|
|
2974
|
+
INDEX_SPINNER: ClassVar[tuple[str, ...]] = ("~", "/", "-", "\\", "|")
|
|
2928
2975
|
|
|
2929
2976
|
def __init__(self, session: Session):
|
|
2930
2977
|
self.session = session
|
|
@@ -3056,9 +3103,12 @@ class StatusBar:
|
|
|
3056
3103
|
return CodeIndex.label("error")
|
|
3057
3104
|
if self.session.state.code_index_refreshing:
|
|
3058
3105
|
notice = self.session.state.code_index_notice or "syncing"
|
|
3059
|
-
return
|
|
3106
|
+
return self.index_spinner() if notice in {"syncing", "updating"} else notice
|
|
3060
3107
|
return CodeIndex.label(self.session.state.code_index_status)
|
|
3061
3108
|
|
|
3109
|
+
def index_spinner(self) -> str:
|
|
3110
|
+
return self.INDEX_SPINNER[int(time.monotonic() / self.INTERVAL) % len(self.INDEX_SPINNER)]
|
|
3111
|
+
|
|
3062
3112
|
def stress_after(self) -> float:
|
|
3063
3113
|
return max(30.0, self.session.config.provider.timeout * 0.5)
|
|
3064
3114
|
|
|
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
|