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.
- {nanocode_cli-0.9.1/nanocode_cli.egg-info → nanocode_cli-0.9.2}/PKG-INFO +1 -1
- {nanocode_cli-0.9.1 → nanocode_cli-0.9.2}/nanocode.py +85 -18
- {nanocode_cli-0.9.1 → nanocode_cli-0.9.2/nanocode_cli.egg-info}/PKG-INFO +1 -1
- {nanocode_cli-0.9.1 → nanocode_cli-0.9.2}/pyproject.toml +1 -1
- {nanocode_cli-0.9.1 → nanocode_cli-0.9.2}/LICENSE +0 -0
- {nanocode_cli-0.9.1 → nanocode_cli-0.9.2}/MANIFEST.in +0 -0
- {nanocode_cli-0.9.1 → nanocode_cli-0.9.2}/README.md +0 -0
- {nanocode_cli-0.9.1 → nanocode_cli-0.9.2}/README.zh-CN.md +0 -0
- {nanocode_cli-0.9.1 → nanocode_cli-0.9.2}/nanocode_cli.egg-info/SOURCES.txt +0 -0
- {nanocode_cli-0.9.1 → nanocode_cli-0.9.2}/nanocode_cli.egg-info/dependency_links.txt +0 -0
- {nanocode_cli-0.9.1 → nanocode_cli-0.9.2}/nanocode_cli.egg-info/entry_points.txt +0 -0
- {nanocode_cli-0.9.1 → nanocode_cli-0.9.2}/nanocode_cli.egg-info/requires.txt +0 -0
- {nanocode_cli-0.9.1 → nanocode_cli-0.9.2}/nanocode_cli.egg-info/top_level.txt +0 -0
- {nanocode_cli-0.9.1 → nanocode_cli-0.9.2}/setup.cfg +0 -0
|
@@ -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.
|
|
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] =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, *,
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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())))
|
|
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
|
|
File without changes
|