nanocode-cli 0.9.1__tar.gz → 0.9.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.9.1
3
+ Version: 0.9.2
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
@@ -75,7 +75,7 @@ except ImportError: # pragma: no cover - optional highlighting dependency
75
75
  pygments = None
76
76
  Token = None # keep the name defined so class-body/token lookups don't NameError
77
77
 
78
- __version__ = "0.9.1"
78
+ __version__ = "0.9.2"
79
79
 
80
80
  Json = dict[str, Any]
81
81
 
@@ -668,7 +668,7 @@ class ToolErrorRecord:
668
668
 
669
669
  @dataclass
670
670
  class TurnDiff:
671
- SNAPSHOT_CHAR_LIMIT: ClassVar[int] = 200_000
671
+ SNAPSHOT_CHAR_LIMIT: ClassVar[int] = 1_000_000
672
672
 
673
673
  key: str
674
674
  turn: int
@@ -785,26 +785,26 @@ class MCPFileTokenStore:
785
785
  def save(self, data: dict[str, dict[str, Json]]) -> None:
786
786
  directory = os.path.dirname(self.path)
787
787
  os.makedirs(directory, mode=0o700, exist_ok=True)
788
- try:
788
+ with contextlib.suppress(OSError):
789
789
  os.chmod(directory, 0o700)
790
- except OSError:
791
- pass
792
790
  tmp = self.path + ".tmp"
793
791
  fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
794
792
  try:
795
- with os.fdopen(fd, "w", encoding="utf-8") as file:
793
+ try:
794
+ file = os.fdopen(fd, "w", encoding="utf-8")
795
+ except Exception:
796
+ # os.fdopen doesn't close fd on failure; do it ourselves so the descriptor doesn't leak.
797
+ os.close(fd)
798
+ raise
799
+ with file:
796
800
  json.dump(data, file, ensure_ascii=False, sort_keys=True)
797
801
  except Exception:
798
- try:
802
+ with contextlib.suppress(OSError):
799
803
  os.unlink(tmp)
800
- except OSError:
801
- pass
802
804
  raise
803
805
  os.replace(tmp, self.path)
804
- try:
806
+ with contextlib.suppress(OSError):
805
807
  os.chmod(self.path, 0o600)
806
- except OSError:
807
- pass
808
808
 
809
809
 
810
810
  @dataclass
@@ -1704,7 +1704,7 @@ class Session:
1704
1704
  return (status, path, text) if text else None
1705
1705
 
1706
1706
  @classmethod
1707
- def net_diff_sections(cls, diffs: list[TurnDiff], status: str, *, include_legacy: bool = True) -> list[tuple[str, str, str]]:
1707
+ def net_diff_sections(cls, diffs: list[TurnDiff], status: str, *, cwd: str = "") -> list[tuple[str, str, str]]:
1708
1708
  states: dict[str, tuple[str, str]] = {}
1709
1709
  legacy: dict[str, list[str]] = {}
1710
1710
  paths: list[str] = []
@@ -1712,8 +1712,7 @@ class Session:
1712
1712
  if diff.path not in paths:
1713
1713
  paths.append(diff.path)
1714
1714
  if not diff.before and not diff.after:
1715
- if include_legacy:
1716
- legacy.setdefault(diff.path, []).append(diff.diff)
1715
+ legacy.setdefault(diff.path, []).append(diff.diff)
1717
1716
  continue
1718
1717
  before, _ = states.get(diff.path, (diff.before, diff.after))
1719
1718
  states[diff.path] = (before, diff.after)
@@ -1731,11 +1730,79 @@ class Session:
1731
1730
  chunks = []
1732
1731
  if path in states and (section := cls.net_diff_for_path(status, path, *states[path])):
1733
1732
  chunks.append(section[2])
1734
- chunks.extend(legacy.get(path, []))
1733
+ legacy_chunks = legacy.get(path, [])
1734
+ if legacy_chunks:
1735
+ # No snapshots for this file (oversized). Best effort: reconstruct the pre-edit content
1736
+ # by reverse-applying the recorded per-Edit hunks to the file's current on-disk state,
1737
+ # then emit one clean synthesized diff. Falls back to the raw per-Edit hunks concatenated
1738
+ # when reconstruction can't uniquely locate a hunk (e.g. the file was mutated outside Edit).
1739
+ reconstructed = cls._reconstruct_legacy_diff(cwd, path, legacy_chunks, status) if cwd else None
1740
+ chunks.append(reconstructed if reconstructed is not None else "\n".join(chunk.rstrip("\n") for chunk in legacy_chunks))
1735
1741
  if chunks:
1736
1742
  sections.append((status, path, "\n".join(chunk.rstrip("\n") for chunk in chunks) + "\n"))
1737
1743
  return sections
1738
1744
 
1745
+ _HUNK_RE: ClassVar[re.Pattern[str]] = re.compile(r"^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@")
1746
+
1747
+ @classmethod
1748
+ def _reconstruct_legacy_diff(cls, cwd: str, path: str, chunks: list[str], status: str) -> str | None:
1749
+ abspath = path if os.path.isabs(path) else os.path.join(cwd, path)
1750
+ if not os.path.isfile(abspath):
1751
+ return None
1752
+ try:
1753
+ with open(abspath, encoding="utf-8") as file:
1754
+ final = file.read()
1755
+ except (OSError, UnicodeDecodeError):
1756
+ return None
1757
+ hunk_pairs: list[tuple[str, str]] = []
1758
+ for chunk in chunks:
1759
+ pairs = cls._split_hunks(chunk)
1760
+ if pairs is None:
1761
+ return None
1762
+ hunk_pairs.extend(pairs)
1763
+ # Reverse-apply each hunk in reverse chronological order so `current` walks back to the state
1764
+ # before the first tracked edit. Each hunk's after-text must occur uniquely in the buffer; if
1765
+ # not (external mutation, ambiguous context), give up and let the caller fall back.
1766
+ current = final
1767
+ for after_text, before_text in reversed(hunk_pairs):
1768
+ if not after_text or not before_text:
1769
+ return None
1770
+ if current.count(after_text) != 1:
1771
+ return None
1772
+ current = current.replace(after_text, before_text, 1)
1773
+ section = cls.net_diff_for_path(status, path, current, final)
1774
+ return section[2] if section else ""
1775
+
1776
+ @classmethod
1777
+ def _split_hunks(cls, chunk: str) -> list[tuple[str, str]] | None:
1778
+ pairs: list[tuple[str, str]] = []
1779
+ before_lines: list[str] | None = None
1780
+ after_lines: list[str] | None = None
1781
+ for line in chunk.splitlines():
1782
+ if line.startswith(("--- ", "+++ ")):
1783
+ continue
1784
+ if cls._HUNK_RE.match(line):
1785
+ if before_lines is not None and after_lines is not None:
1786
+ pairs.append(("\n".join(after_lines), "\n".join(before_lines)))
1787
+ before_lines, after_lines = [], []
1788
+ continue
1789
+ if before_lines is None or after_lines is None:
1790
+ return None
1791
+ if line.startswith("+"):
1792
+ after_lines.append(line[1:])
1793
+ elif line.startswith("-"):
1794
+ before_lines.append(line[1:])
1795
+ elif line.startswith(" "):
1796
+ before_lines.append(line[1:])
1797
+ after_lines.append(line[1:])
1798
+ elif line == "\":
1799
+ continue
1800
+ else:
1801
+ return None
1802
+ if before_lines is not None and after_lines is not None:
1803
+ pairs.append(("\n".join(after_lines), "\n".join(before_lines)))
1804
+ return pairs
1805
+
1739
1806
  @staticmethod
1740
1807
  def _find_unambiguous_move(states: dict[str, tuple[str, str]], legacy: dict[str, list[str]]) -> tuple[str, str] | None:
1741
1808
  sources_by_after: dict[str, list[str]] = {}
@@ -1758,10 +1825,10 @@ class Session:
1758
1825
  return None
1759
1826
  round = max(diff.round or diff.turn for diff in self.turn_diffs)
1760
1827
  diffs = [diff for diff in self.turn_diffs if (diff.round or diff.turn) == round]
1761
- return round, self.net_diff_sections(diffs, "edit")
1828
+ return round, self.net_diff_sections(diffs, "edit", cwd=self.cwd)
1762
1829
 
1763
1830
  def session_diff_sections(self) -> list[tuple[str, str, str]]:
1764
- return self.net_diff_sections(self.turn_diffs, "overall", include_legacy=False)
1831
+ return self.net_diff_sections(self.turn_diffs, "overall", cwd=self.cwd)
1765
1832
 
1766
1833
  def record_tool_error(self, key: str, name: str, args: list[Any], error: str) -> None:
1767
1834
  self.tool_errors.append(ToolErrorRecord(key, name, Text.value(list(args)), " ".join(Text.clean(error).split())))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nanocode-cli
3
- Version: 0.9.1
3
+ Version: 0.9.2
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.9.1"
7
+ version = "0.9.2"
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