nanocode-cli 0.2.0__tar.gz → 0.2.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.2.0/nanocode_cli.egg-info → nanocode_cli-0.2.2}/PKG-INFO +8 -2
- {nanocode_cli-0.2.0 → nanocode_cli-0.2.2}/README.md +7 -1
- {nanocode_cli-0.2.0 → nanocode_cli-0.2.2}/nanocode.py +103 -52
- {nanocode_cli-0.2.0 → nanocode_cli-0.2.2/nanocode_cli.egg-info}/PKG-INFO +8 -2
- {nanocode_cli-0.2.0 → nanocode_cli-0.2.2}/pyproject.toml +1 -1
- {nanocode_cli-0.2.0 → nanocode_cli-0.2.2}/LICENSE +0 -0
- {nanocode_cli-0.2.0 → nanocode_cli-0.2.2}/MANIFEST.in +0 -0
- {nanocode_cli-0.2.0 → nanocode_cli-0.2.2}/nanocode_cli.egg-info/SOURCES.txt +0 -0
- {nanocode_cli-0.2.0 → nanocode_cli-0.2.2}/nanocode_cli.egg-info/dependency_links.txt +0 -0
- {nanocode_cli-0.2.0 → nanocode_cli-0.2.2}/nanocode_cli.egg-info/entry_points.txt +0 -0
- {nanocode_cli-0.2.0 → nanocode_cli-0.2.2}/nanocode_cli.egg-info/requires.txt +0 -0
- {nanocode_cli-0.2.0 → nanocode_cli-0.2.2}/nanocode_cli.egg-info/top_level.txt +0 -0
- {nanocode_cli-0.2.0 → nanocode_cli-0.2.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nanocode-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A lightweight terminal-based AI coding assistant
|
|
5
5
|
Author-email: hit9 <hit9@icloud.com>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -32,7 +32,7 @@ Dynamic: license-file
|
|
|
32
32
|
|
|
33
33
|
A lightweight terminal-based AI coding assistant.
|
|
34
34
|
|
|
35
|
-
nanocode is used to
|
|
35
|
+
nanocode is used to help building itself, including features such as `@file` path completion.
|
|
36
36
|
|
|
37
37
|
## Screenshots
|
|
38
38
|
|
|
@@ -46,6 +46,12 @@ nanocode is used to build itself, including features such as `@file` path comple
|
|
|
46
46
|
uv tool install nanocode-cli
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
Upgrade an existing install:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
uv tool upgrade nanocode-cli
|
|
53
|
+
```
|
|
54
|
+
|
|
49
55
|
For local development:
|
|
50
56
|
|
|
51
57
|
```sh
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A lightweight terminal-based AI coding assistant.
|
|
4
4
|
|
|
5
|
-
nanocode is used to
|
|
5
|
+
nanocode is used to help building itself, including features such as `@file` path completion.
|
|
6
6
|
|
|
7
7
|
## Screenshots
|
|
8
8
|
|
|
@@ -16,6 +16,12 @@ nanocode is used to build itself, including features such as `@file` path comple
|
|
|
16
16
|
uv tool install nanocode-cli
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
Upgrade an existing install:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
uv tool upgrade nanocode-cli
|
|
23
|
+
```
|
|
24
|
+
|
|
19
25
|
For local development:
|
|
20
26
|
|
|
21
27
|
```sh
|
|
@@ -41,7 +41,7 @@ from prompt_toolkit.patch_stdout import patch_stdout
|
|
|
41
41
|
|
|
42
42
|
JsonValue: TypeAlias = Any
|
|
43
43
|
Json: TypeAlias = dict[str, JsonValue]
|
|
44
|
-
__version__ = "0.2.
|
|
44
|
+
__version__ = "0.2.2"
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
class Error(Exception): ...
|
|
@@ -524,6 +524,7 @@ class ToolCallExecution:
|
|
|
524
524
|
output: str
|
|
525
525
|
result_file: str
|
|
526
526
|
result_file_lines: int
|
|
527
|
+
log_written: bool = True
|
|
527
528
|
|
|
528
529
|
|
|
529
530
|
ConfirmationResult: TypeAlias = bool | str
|
|
@@ -565,6 +566,8 @@ def _range_fingerprint(content: str) -> str:
|
|
|
565
566
|
@final
|
|
566
567
|
@dataclass
|
|
567
568
|
class ReadTool(Tool):
|
|
569
|
+
MAX_LINES: ClassVar[int] = 1000
|
|
570
|
+
|
|
568
571
|
filepath: str = ""
|
|
569
572
|
start: int = 0
|
|
570
573
|
end: int = 0
|
|
@@ -580,7 +583,8 @@ class ReadTool(Tool):
|
|
|
580
583
|
return [
|
|
581
584
|
"Read exact file lines with a fingerprint.",
|
|
582
585
|
"Optional range is 0-based [start,end); end=0 means EOF.",
|
|
583
|
-
"
|
|
586
|
+
"Returns at most 1000 lines; truncated results include total lines and next-step guidance.",
|
|
587
|
+
"Prefer Search before Read for large or unknown files; use bounded reads when exact context is needed.",
|
|
584
588
|
"For ReplaceRange, call Read with the exact same filepath/start/end and reuse that range fingerprint.",
|
|
585
589
|
]
|
|
586
590
|
|
|
@@ -607,26 +611,64 @@ class ReadTool(Tool):
|
|
|
607
611
|
return f"Read({self.filepath}, {self.start}, {self.end})"
|
|
608
612
|
|
|
609
613
|
def call(self) -> str:
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
614
|
+
total_lines = 0
|
|
615
|
+
selected_lines = []
|
|
616
|
+
truncated = False
|
|
617
|
+
bounded_read_lines = self.end - self.start if self.end else 0
|
|
618
|
+
if self.end and bounded_read_lines <= self.MAX_LINES:
|
|
619
|
+
with open(self.filepath, "r", encoding="utf-8") as f:
|
|
620
|
+
selected_lines = list(itertools.islice(f, self.start, self.end))
|
|
621
|
+
else:
|
|
622
|
+
with open(self.filepath, "r", encoding="utf-8") as f:
|
|
623
|
+
for index, line in enumerate(f):
|
|
624
|
+
total_lines = index + 1
|
|
625
|
+
if index < self.start:
|
|
626
|
+
continue
|
|
627
|
+
if self.end and index >= self.end:
|
|
628
|
+
continue
|
|
629
|
+
if len(selected_lines) < self.MAX_LINES:
|
|
630
|
+
selected_lines.append(line)
|
|
631
|
+
continue
|
|
632
|
+
truncated = True
|
|
633
|
+
content = "".join(selected_lines)
|
|
634
|
+
returned_end = self.start + len(selected_lines)
|
|
635
|
+
fingerprint_end = returned_end if truncated else self.end
|
|
613
636
|
fingerprint = self.range_fingerprints.remember(
|
|
614
637
|
filepath=self.filepath,
|
|
615
638
|
start=self.start,
|
|
616
|
-
end=
|
|
639
|
+
end=fingerprint_end,
|
|
617
640
|
content=content,
|
|
618
641
|
)
|
|
619
|
-
|
|
642
|
+
lines = [
|
|
643
|
+
"<ReadToolResult>",
|
|
644
|
+
" <range>" + str(self.start) + ":" + str(fingerprint_end) + "</range>",
|
|
645
|
+
" <fingerprint>" + fingerprint + "</fingerprint>",
|
|
646
|
+
]
|
|
647
|
+
if truncated:
|
|
648
|
+
lines.extend(
|
|
649
|
+
[
|
|
650
|
+
" <truncated>true</truncated>",
|
|
651
|
+
" <total_lines>" + str(total_lines) + "</total_lines>",
|
|
652
|
+
" <note>Read returned "
|
|
653
|
+
+ str(len(selected_lines))
|
|
654
|
+
+ " lines from "
|
|
655
|
+
+ str(self.start)
|
|
656
|
+
+ ":"
|
|
657
|
+
+ str(returned_end)
|
|
658
|
+
+ " of "
|
|
659
|
+
+ str(total_lines)
|
|
660
|
+
+ " total lines. Use Search to locate relevant text or Read smaller ranges in batches.</note>",
|
|
661
|
+
]
|
|
662
|
+
)
|
|
663
|
+
lines.extend(
|
|
620
664
|
[
|
|
621
|
-
"<ReadToolResult>",
|
|
622
|
-
" <range>" + str(self.start) + ":" + str(self.end) + "</range>",
|
|
623
|
-
" <fingerprint>" + fingerprint + "</fingerprint>",
|
|
624
665
|
" <content no-indention>",
|
|
625
666
|
content,
|
|
626
667
|
" </content>",
|
|
627
668
|
"</ReadToolResult>",
|
|
628
669
|
]
|
|
629
670
|
)
|
|
671
|
+
return "\n".join(lines)
|
|
630
672
|
|
|
631
673
|
|
|
632
674
|
@final
|
|
@@ -687,18 +729,19 @@ class ListDirTool(Tool):
|
|
|
687
729
|
|
|
688
730
|
@classmethod
|
|
689
731
|
def signature(cls) -> str:
|
|
690
|
-
return
|
|
732
|
+
return 'ListDir(dir_path?: "."[, glob_pattern]) -> ListDirToolResult<entries>'
|
|
691
733
|
|
|
692
734
|
@classmethod
|
|
693
735
|
def example(cls) -> list[str]:
|
|
694
|
-
return ['Example args: ["."]', 'Example args: ["src", "*.py"]']
|
|
736
|
+
return ['Example args: []', 'Example args: ["."]', 'Example args: ["src", "*.py"]']
|
|
695
737
|
|
|
696
738
|
@classmethod
|
|
697
739
|
def make(cls, session: Session, args: list[str]) -> Self:
|
|
698
|
-
if len(args) not in (1, 2):
|
|
699
|
-
raise ToolCallError("requires
|
|
740
|
+
if len(args) not in (0, 1, 2):
|
|
741
|
+
raise ToolCallError("requires 0 to 2 args: [dir_path][, glob_pattern]")
|
|
742
|
+
dir_path = str(args[0]) if args else "."
|
|
700
743
|
glob_pattern = str(args[1]) if len(args) == 2 else ""
|
|
701
|
-
return cls(dirpath=session.resolve_path(
|
|
744
|
+
return cls(dirpath=session.resolve_path(dir_path), glob_pattern=glob_pattern, cwd=session.cwd)
|
|
702
745
|
|
|
703
746
|
def display(self) -> str:
|
|
704
747
|
if self.glob_pattern:
|
|
@@ -1664,8 +1707,11 @@ Tools:
|
|
|
1664
1707
|
- Call tools only by emitting JSON in tool_calls. Do not use native tool calls.
|
|
1665
1708
|
- Use multiple tool calls in one turn when they are independent.
|
|
1666
1709
|
- Prefer specific tools first; use Bash only when no provided tool fits.
|
|
1667
|
-
-
|
|
1668
|
-
-
|
|
1710
|
+
- Prefer Search before Read when locating code or facts; Read only known small ranges or exact files needed for editing.
|
|
1711
|
+
- Read returns at most 1000 lines; if truncated, use Search or smaller Read ranges in batches.
|
|
1712
|
+
- Summarize every latest tool result in last_tool_calls_summaries; raw results are shown once only, so include key_evidence when paths, lines, errors, or decisions matter later.
|
|
1713
|
+
- Latest tool results are already shown in Latest_Tool_Call_Results; use result_file logs only as a fallback when needed.
|
|
1714
|
+
- If an older tool result lacks detail that is needed for the task, prefer re-running a targeted source tool; Read result_file logs only when that is the cheapest accurate source.
|
|
1669
1715
|
- known_append is stable memory; current_context_update is task-local memory.
|
|
1670
1716
|
- tool_call.intention must state the question to answer, not just the action.
|
|
1671
1717
|
|
|
@@ -2092,25 +2138,32 @@ class ToolCallRunner:
|
|
|
2092
2138
|
if call is None:
|
|
2093
2139
|
call = self._invalid_tool_call(item)
|
|
2094
2140
|
|
|
2095
|
-
|
|
2141
|
+
log_written = not self._is_tool_result_file_read(call)
|
|
2142
|
+
if log_written:
|
|
2143
|
+
result_file, result_file_lines = self._write_tool_result_log(call, outcome, output)
|
|
2144
|
+
else:
|
|
2145
|
+
result_file = os.path.relpath(self.session.resolve_path(call.args[0]), self.session.cwd)
|
|
2146
|
+
result_file_lines = len(output.splitlines())
|
|
2096
2147
|
execution = ToolCallExecution(
|
|
2097
2148
|
call=call,
|
|
2098
2149
|
outcome=outcome,
|
|
2099
2150
|
output=output,
|
|
2100
2151
|
result_file=result_file,
|
|
2101
2152
|
result_file_lines=result_file_lines,
|
|
2153
|
+
log_written=log_written,
|
|
2102
2154
|
)
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2155
|
+
if log_written:
|
|
2156
|
+
event = ToolCallEvent(
|
|
2157
|
+
intent=call.intention,
|
|
2158
|
+
executed=call.executed,
|
|
2159
|
+
outcome=outcome,
|
|
2160
|
+
summary="",
|
|
2161
|
+
result_file=result_file,
|
|
2162
|
+
result_file_lines=result_file_lines,
|
|
2163
|
+
)
|
|
2164
|
+
self.session.append_conversation(event)
|
|
2165
|
+
events.append(event)
|
|
2112
2166
|
executions.append(execution)
|
|
2113
|
-
events.append(event)
|
|
2114
2167
|
|
|
2115
2168
|
self.latest_events = events
|
|
2116
2169
|
self.latest_executions = executions
|
|
@@ -2128,7 +2181,8 @@ class ToolCallRunner:
|
|
|
2128
2181
|
lines.append(" " + str(index) + ". [" + execution.outcome + "] " + execution.call.executed)
|
|
2129
2182
|
if execution.call.intention:
|
|
2130
2183
|
lines.append(" why: " + execution.call.intention)
|
|
2131
|
-
|
|
2184
|
+
label = "log" if execution.log_written else "source"
|
|
2185
|
+
lines.append(" " + label + ": " + execution.result_file + " (" + str(execution.result_file_lines) + " lines)")
|
|
2132
2186
|
return "\n".join(lines)
|
|
2133
2187
|
|
|
2134
2188
|
def parse_tool_call(self, value: JsonValue) -> ParsedToolCall:
|
|
@@ -2153,6 +2207,16 @@ class ToolCallRunner:
|
|
|
2153
2207
|
raise ToolCallError("tool not found: " + call.name)
|
|
2154
2208
|
return tool_class.make(self.session, call.args)
|
|
2155
2209
|
|
|
2210
|
+
def _is_tool_result_file_read(self, call: ParsedToolCall) -> bool:
|
|
2211
|
+
if call.name != ReadTool.name() or not call.args:
|
|
2212
|
+
return False
|
|
2213
|
+
tool_results_dir = os.path.realpath(self.session.tool_results_dir())
|
|
2214
|
+
path = os.path.realpath(self.session.resolve_path(call.args[0]))
|
|
2215
|
+
try:
|
|
2216
|
+
return os.path.commonpath([tool_results_dir, path]) == tool_results_dir
|
|
2217
|
+
except ValueError:
|
|
2218
|
+
return False
|
|
2219
|
+
|
|
2156
2220
|
def _write_tool_result_log(self, call: ParsedToolCall, outcome: str, output: str) -> tuple[str, int]:
|
|
2157
2221
|
directory = self.session.tool_results_dir()
|
|
2158
2222
|
os.makedirs(directory, exist_ok=True)
|
|
@@ -2563,9 +2627,6 @@ class ConversationCompactor:
|
|
|
2563
2627
|
|
|
2564
2628
|
@final
|
|
2565
2629
|
class Agent:
|
|
2566
|
-
EVIDENCE_OUTPUT_LINES: ClassVar[int] = 40
|
|
2567
|
-
EVIDENCE_OUTPUT_CHARS: ClassVar[int] = 4000
|
|
2568
|
-
|
|
2569
2630
|
def __init__(self, session: Session):
|
|
2570
2631
|
self.session = session
|
|
2571
2632
|
self.prompt_builder = PromptBuilder(session)
|
|
@@ -2688,52 +2749,42 @@ class Agent:
|
|
|
2688
2749
|
|
|
2689
2750
|
def _format_tool_summary_gate(self, tool_calls: list[JsonValue]) -> str:
|
|
2690
2751
|
missing = self._missing_tool_summaries()
|
|
2691
|
-
missing_evidence = []
|
|
2692
2752
|
needs_read = []
|
|
2693
|
-
for event
|
|
2753
|
+
for event in self.tool_runner.latest_events:
|
|
2694
2754
|
if not event.summary:
|
|
2695
2755
|
continue
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
missing_evidence.append(event)
|
|
2756
|
+
is_reading_result_file = self._has_read_result_file_call(tool_calls, event.result_file)
|
|
2757
|
+
if event.needs_raw_read:
|
|
2758
|
+
if not is_reading_result_file:
|
|
2759
|
+
needs_read.append(event)
|
|
2701
2760
|
continue
|
|
2702
|
-
|
|
2703
|
-
missing_evidence.append(event)
|
|
2704
|
-
if not missing and not missing_evidence and not needs_read:
|
|
2761
|
+
if not missing and not needs_read:
|
|
2705
2762
|
return ""
|
|
2706
2763
|
|
|
2707
|
-
lines = ["Tool_Summary_Gate:
|
|
2764
|
+
lines = ["Tool_Summary_Gate: summarize latest tool results before continuing.", "Raw tool results are visible only once."]
|
|
2708
2765
|
if missing:
|
|
2709
2766
|
lines.append("Missing summaries:")
|
|
2710
2767
|
for event in missing:
|
|
2711
2768
|
lines.append("- " + event.executed + " -> " + event.result_file)
|
|
2712
|
-
if missing_evidence:
|
|
2713
|
-
lines.append("Missing key_evidence:")
|
|
2714
|
-
for event in missing_evidence:
|
|
2715
|
-
lines.append("- " + event.executed + " -> " + event.result_file)
|
|
2716
2769
|
if needs_read:
|
|
2717
2770
|
lines.append("Needs raw read:")
|
|
2718
2771
|
for event in needs_read:
|
|
2719
2772
|
lines.append("- Read(" + event.result_file + ") before continuing")
|
|
2720
2773
|
lines.append(
|
|
2721
|
-
"Next_Action: return last_tool_calls_summaries
|
|
2774
|
+
"Next_Action: return last_tool_calls_summaries for missing results, read result_file only when it is the cheapest accurate fallback, or continue with source tools such as Search/ListDir."
|
|
2722
2775
|
)
|
|
2723
2776
|
return "\n".join(lines)
|
|
2724
2777
|
|
|
2725
|
-
def _is_large_tool_output(self, output: str) -> bool:
|
|
2726
|
-
return len(output) > self.EVIDENCE_OUTPUT_CHARS or len(output.splitlines()) > self.EVIDENCE_OUTPUT_LINES
|
|
2727
|
-
|
|
2728
2778
|
def _has_read_result_file_call(self, tool_calls: list[JsonValue], result_file: str) -> bool:
|
|
2729
2779
|
if not result_file:
|
|
2730
2780
|
return False
|
|
2781
|
+
expected = self.session.resolve_path(result_file)
|
|
2731
2782
|
for raw_call in tool_calls:
|
|
2732
2783
|
call = _json_dict(raw_call)
|
|
2733
2784
|
if _json_str(call.get("name")) != ReadTool.name():
|
|
2734
2785
|
continue
|
|
2735
2786
|
args = [_json_str(arg) or "" for arg in _json_list(call.get("args"))]
|
|
2736
|
-
if args and args[0] ==
|
|
2787
|
+
if args and self.session.resolve_path(args[0]) == expected:
|
|
2737
2788
|
return True
|
|
2738
2789
|
return False
|
|
2739
2790
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nanocode-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A lightweight terminal-based AI coding assistant
|
|
5
5
|
Author-email: hit9 <hit9@icloud.com>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -32,7 +32,7 @@ Dynamic: license-file
|
|
|
32
32
|
|
|
33
33
|
A lightweight terminal-based AI coding assistant.
|
|
34
34
|
|
|
35
|
-
nanocode is used to
|
|
35
|
+
nanocode is used to help building itself, including features such as `@file` path completion.
|
|
36
36
|
|
|
37
37
|
## Screenshots
|
|
38
38
|
|
|
@@ -46,6 +46,12 @@ nanocode is used to build itself, including features such as `@file` path comple
|
|
|
46
46
|
uv tool install nanocode-cli
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
Upgrade an existing install:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
uv tool upgrade nanocode-cli
|
|
53
|
+
```
|
|
54
|
+
|
|
49
55
|
For local development:
|
|
50
56
|
|
|
51
57
|
```sh
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|