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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nanocode-cli
3
- Version: 0.2.0
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 build itself, including features such as `@file` path completion.
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 build itself, including features such as `@file` path completion.
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.0"
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
- "Prefer bounded reads over full-file reads; use LineCount first when unsure.",
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
- with open(self.filepath, "r", encoding="utf-8") as f:
611
- lines = itertools.islice(f, self.start, self.end or None)
612
- content = "".join(lines)
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=self.end,
639
+ end=fingerprint_end,
617
640
  content=content,
618
641
  )
619
- return "\n".join(
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 "ListDir(dir_path[, glob_pattern]) -> ListDirToolResult<entries>"
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 1 or 2 args: dir_path[, glob_pattern]")
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(args[0]), glob_pattern=glob_pattern, cwd=session.cwd)
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
- - Summarize every latest tool result in last_tool_calls_summaries; raw results are shown once only, so key_evidence keeps paths, lines, errors, decisions.
1668
- - If a prior tool result lacks detail, use Read on its result_file.
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
- result_file, result_file_lines = self._write_tool_result_log(call, outcome, output)
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
- event = ToolCallEvent(
2104
- intent=call.intention,
2105
- executed=call.executed,
2106
- outcome=outcome,
2107
- summary="",
2108
- result_file=result_file,
2109
- result_file_lines=result_file_lines,
2110
- )
2111
- self.session.append_conversation(event)
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
- lines.append(" log: " + execution.result_file + " (" + str(execution.result_file_lines) + " lines)")
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, execution in zip(self.tool_runner.latest_events, self.tool_runner.latest_executions):
2753
+ for event in self.tool_runner.latest_events:
2694
2754
  if not event.summary:
2695
2755
  continue
2696
- if event.needs_raw_read and not self._has_read_result_file_call(tool_calls, event.result_file):
2697
- needs_read.append(event)
2698
- if event.outcome in {"failure", "partial"}:
2699
- if not event.key_details:
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
- if self._is_large_tool_output(execution.output) and not event.key_details and not event.needs_raw_read:
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: extract durable evidence before continuing.", "Raw tool results are visible only once."]
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 with key_evidence, update current_context_update for task-local facts, or call Read(result_file) for needs_raw_read logs."
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] == result_file:
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.0
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 build itself, including features such as `@file` path completion.
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nanocode-cli"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "A lightweight terminal-based AI coding assistant"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
File without changes
File without changes
File without changes