nanocode-cli 0.5.9__tar.gz → 0.5.11__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.5.9
3
+ Version: 0.5.11
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
@@ -22,7 +22,7 @@ Requires-Python: >=3.11
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
24
  Requires-Dist: anthropic>=0.64.0
25
- Requires-Dist: code-symbol-index>=0.1.13
25
+ Requires-Dist: code-symbol-index>=0.3.0
26
26
  Requires-Dist: json-repair
27
27
  Requires-Dist: openai>=2.37.0
28
28
  Requires-Dist: prompt-toolkit>=3.0
@@ -39,6 +39,8 @@ A small terminal coding agent written in Python.
39
39
 
40
40
  [简体中文](README.zh-CN.md)
41
41
 
42
+ [Chinese blog](https://hit9.dev/post/nanocode)
43
+
42
44
  nanocode is pre-1.0 software. Commands, configuration, and tool behavior may change before a stable release.
43
45
 
44
46
  ![nanocode screenshot](snapshots/nanocode-snapshot.png)
@@ -48,7 +50,7 @@ nanocode is pre-1.0 software. Commands, configuration, and tool behavior may cha
48
50
  - **Live turn control**: Add follow-up input while the agent is still working, without losing the current tool flow.
49
51
  - **File-state brain**: Reads and edits build a current, line-numbered view of the files that matter now.
50
52
  - **Stale-edit protection**: `line:hash` anchors reject edits when the target code has drifted.
51
- - **Project-aware navigation**: Use the symbol index to jump through outlines, references, and changed files quickly.
53
+ - **Project-aware navigation**: Use the symbol index to jump through outlines, references, implementors, call chains, and changed files quickly.
52
54
  - **Recoverable context**: Tool output stays bounded in the prompt, while raw `tr.N` results remain recallable.
53
55
  - **Cache-aware context**: Stable sections stay early and noisy working state stays late to improve prompt-cache reuse.
54
56
  - **Focused working memory**: `Note` separates goal, plan, and known facts from noisy execution logs.
@@ -138,6 +140,16 @@ Main fields:
138
140
 
139
141
  `api = "auto"` chooses between Chat Completions and Anthropic Messages using provider/model profiles. `prompt_cache_key = "auto"` derives a stable key from provider, model, workspace, and tool schema names.
140
142
 
143
+
144
+ ## Tested Providers
145
+
146
+ The following providers have been tested with nanocode:
147
+
148
+ - **deepseek**: DeepSeek API
149
+ - **opencode**: OpenCode API
150
+ - **aliyun**: Alibaba Cloud (Tongyi Qianwen) API via Chat Completions
151
+ - **llama.cpp**: Local inference via llama.cpp server
152
+
141
153
  ## Context Design
142
154
 
143
155
  Each model request is built manually from explicit messages. Stable context comes first, conversation stays as messages, working memory follows, and the latest file state is appended at the end.
@@ -4,6 +4,8 @@ A small terminal coding agent written in Python.
4
4
 
5
5
  [简体中文](README.zh-CN.md)
6
6
 
7
+ [Chinese blog](https://hit9.dev/post/nanocode)
8
+
7
9
  nanocode is pre-1.0 software. Commands, configuration, and tool behavior may change before a stable release.
8
10
 
9
11
  ![nanocode screenshot](snapshots/nanocode-snapshot.png)
@@ -13,7 +15,7 @@ nanocode is pre-1.0 software. Commands, configuration, and tool behavior may cha
13
15
  - **Live turn control**: Add follow-up input while the agent is still working, without losing the current tool flow.
14
16
  - **File-state brain**: Reads and edits build a current, line-numbered view of the files that matter now.
15
17
  - **Stale-edit protection**: `line:hash` anchors reject edits when the target code has drifted.
16
- - **Project-aware navigation**: Use the symbol index to jump through outlines, references, and changed files quickly.
18
+ - **Project-aware navigation**: Use the symbol index to jump through outlines, references, implementors, call chains, and changed files quickly.
17
19
  - **Recoverable context**: Tool output stays bounded in the prompt, while raw `tr.N` results remain recallable.
18
20
  - **Cache-aware context**: Stable sections stay early and noisy working state stays late to improve prompt-cache reuse.
19
21
  - **Focused working memory**: `Note` separates goal, plan, and known facts from noisy execution logs.
@@ -103,6 +105,16 @@ Main fields:
103
105
 
104
106
  `api = "auto"` chooses between Chat Completions and Anthropic Messages using provider/model profiles. `prompt_cache_key = "auto"` derives a stable key from provider, model, workspace, and tool schema names.
105
107
 
108
+
109
+ ## Tested Providers
110
+
111
+ The following providers have been tested with nanocode:
112
+
113
+ - **deepseek**: DeepSeek API
114
+ - **opencode**: OpenCode API
115
+ - **aliyun**: Alibaba Cloud (Tongyi Qianwen) API via Chat Completions
116
+ - **llama.cpp**: Local inference via llama.cpp server
117
+
106
118
  ## Context Design
107
119
 
108
120
  Each model request is built manually from explicit messages. Stable context comes first, conversation stays as messages, working memory follows, and the latest file state is appended at the end.
@@ -4,6 +4,8 @@
4
4
 
5
5
  [English](README.md)
6
6
 
7
+ [中文博客](https://hit9.dev/post/nanocode)
8
+
7
9
  nanocode 仍是 1.0 前的软件。稳定版发布前,命令、配置和工具行为都可能变化。
8
10
 
9
11
  ![nanocode screenshot](snapshots/nanocode-snapshot.png)
@@ -103,6 +105,15 @@ nanocode --init-config
103
105
 
104
106
  `api = "auto"` 会根据 provider/model profile 在 Chat Completions 和 Anthropic Messages 之间选择。`prompt_cache_key = "auto"` 会根据 provider、model、workspace 和工具 schema 名称生成稳定 key。
105
107
 
108
+
109
+ ## 已测试的 Provider
110
+
111
+ 以下 provider 已在 nanocode 中测试通过:
112
+
113
+ - **deepseek**: DeepSeek API
114
+ - **opencode**: OpenCode API
115
+ - **aliyun**: 阿里云通义千问 API(Chat Completions)
116
+ - **llama.cpp**: 通过 llama.cpp 服务端本地推理
106
117
  ## 上下文设计
107
118
 
108
119
  每次模型请求都由 nanocode 手动构建成明确的 messages。稳定上下文在前,会话作为 messages 保留,工作记忆随后,最新文件状态放在末尾。
@@ -55,7 +55,7 @@ from prompt_toolkit.widgets import SearchToolbar
55
55
  from rich.console import Console
56
56
  from rich.markdown import Markdown
57
57
 
58
- __version__ = "0.5.9"
58
+ __version__ = "0.5.11"
59
59
 
60
60
  Json = dict[str, Any]
61
61
  HTTP_USER_AGENT = "nanocode/" + __version__
@@ -553,6 +553,7 @@ class Session:
553
553
  tool_counter: int = 0
554
554
  usage: ModelUsage = field(default_factory=ModelUsage)
555
555
  update: UpdateStatus = field(default_factory=UpdateStatus)
556
+ _gitignore_cache: dict[str, tuple[int, list[str]]] = field(default_factory=dict)
556
557
 
557
558
  def __post_init__(self) -> None:
558
559
  if self.system_info is None:
@@ -759,12 +760,23 @@ class Tool:
759
760
 
760
761
  def gitignore_patterns(self, root: str) -> list[str]:
761
762
  patterns = []
762
- for path in dict.fromkeys([os.path.join(self.session.cwd, ".gitignore"), *([os.path.join(root, ".gitignore")] if os.path.isdir(root) else [])]):
763
+ cache = self.session._gitignore_cache
764
+ paths = [os.path.join(self.session.cwd, ".gitignore")]
765
+ if os.path.isdir(root):
766
+ paths.append(os.path.join(root, ".gitignore"))
767
+ for path in dict.fromkeys(paths):
763
768
  try:
769
+ mtime = os.stat(path).st_mtime_ns
770
+ cached = cache.get(path)
771
+ if cached is not None and cached[0] == mtime:
772
+ patterns.extend(cached[1])
773
+ continue
764
774
  with open(path, encoding="utf-8") as file:
765
- patterns.extend(line.strip() for line in file if line.strip() and not line.lstrip().startswith("#") and not line.startswith("!"))
775
+ pats = [line.strip() for line in file if line.strip() and not line.lstrip().startswith("#") and not line.startswith("!")]
776
+ cache[path] = (mtime, pats)
777
+ patterns.extend(pats)
766
778
  except OSError:
767
- pass
779
+ cache.pop(path, None)
768
780
  return patterns
769
781
 
770
782
  def ignored(self, path: str, patterns: list[str]) -> bool:
@@ -1435,31 +1447,44 @@ class InspectCodeTool(Tool):
1435
1447
  NAME = "InspectCode"
1436
1448
  MAX_LIMIT: ClassVar[int] = 80
1437
1449
  MAX_OUTLINE_LIMIT: ClassVar[int] = 1000
1438
- DESCRIPTION = "Use the code index: find returns symbols, inspect returns anchors/members/references, outline returns a file symbol tree."
1439
- SIGNATURE = "InspectCode(mode,target,kind?,path?,symbol?,limit?,exact_only?)"
1450
+ MAX_DEPTH: ClassVar[int] = 5
1451
+ MODES: ClassVar[tuple[str, ...]] = ("find", "inspect", "outline", "refs", "impls", "callers", "callees")
1452
+ SYMBOL_MODES: ClassVar[frozenset[str]] = frozenset({"find", "inspect", "refs", "impls", "callers", "callees"})
1453
+ RESOLVE_MODES: ClassVar[frozenset[str]] = frozenset({"inspect", "refs", "impls", "callers", "callees"})
1454
+ CHAIN_MODES: ClassVar[frozenset[str]] = frozenset({"callers", "callees"})
1455
+ OPTION_KEYS: ClassVar[tuple[str, ...]] = ("limit", "kind", "path", "symbol", "exact_only", "depth", "offset", "all_kinds", "ref_kind", "loose")
1456
+ DESCRIPTION = "Use the code index: find returns symbols; inspect returns anchors/members/references; outline returns a file symbol tree; refs lists classified references; impls lists implementors; callers/callees walk the call chain."
1457
+ SIGNATURE = "InspectCode(mode,target,kind?,path?,symbol?,limit?,exact_only?,depth?,offset?,all_kinds?,ref_kind?,loose?)"
1440
1458
  EXAMPLE = (
1441
1459
  'Find symbols; kind can be class|function|method|variable|constant|enum|struct|interface|module|type|trait|field|property|impl|namespace|dict_key, comma-ok. Example: {"mode":"find","target":"Tool","kind":"class,function","limit":20}',
1442
- 'Find exact symbol in a path. Example: {"mode":"find","target":"Session","path":"src","exact_only":true,"limit":10}',
1443
1460
  'Inspect one symbol; path narrows candidates. Example: {"mode":"inspect","target":"Tool","path":"src/app.py"}',
1444
1461
  'Outline one file; symbol narrows subtree. Example: {"mode":"outline","target":"src/app.py","symbol":"App","limit":300}',
1462
+ 'List references; default hides import/attribute noise, ref_kind filters to call|read|write|inherit|type|import|attribute|usage (comma-ok), all_kinds shows everything, offset pages. Example: {"mode":"refs","target":"Tool","ref_kind":"call,write","offset":0}',
1463
+ 'List implementors of an interface/base. Example: {"mode":"impls","target":"Tool","kind":"class"}',
1464
+ 'Walk transitive callers/callees up to depth; callees loose includes ambiguous cross-module matches. Example: {"mode":"callers","target":"handle_job","depth":3}',
1445
1465
  )
1446
1466
 
1447
1467
  @classmethod
1448
1468
  def params_schema(cls) -> Json:
1449
1469
  props = {
1450
- "mode": {"type": "string", "enum": ["find", "inspect", "outline"]},
1470
+ "mode": {"type": "string", "enum": list(cls.MODES)},
1451
1471
  "target": {"type": "string"},
1452
1472
  "limit": {"type": "integer", "minimum": 1, "maximum": cls.MAX_OUTLINE_LIMIT},
1453
1473
  "kind": {"type": "string"},
1454
1474
  "path": {"type": "string"},
1455
1475
  "symbol": {"type": "string"},
1456
1476
  "exact_only": {"type": "boolean"},
1477
+ "depth": {"type": "integer", "minimum": 1, "maximum": cls.MAX_DEPTH},
1478
+ "offset": {"type": "integer", "minimum": 0},
1479
+ "all_kinds": {"type": "boolean"},
1480
+ "ref_kind": {"type": "string"},
1481
+ "loose": {"type": "boolean"},
1457
1482
  }
1458
1483
  return {"type": "object", "properties": props, "required": ["mode", "target"], "additionalProperties": False}
1459
1484
 
1460
1485
  @classmethod
1461
1486
  def payload_args(cls, payload: Json) -> list[Any]:
1462
- options = {key: payload[key] for key in ("limit", "kind", "path", "symbol", "exact_only") if key in payload}
1487
+ options = {key: payload[key] for key in cls.OPTION_KEYS if key in payload}
1463
1488
  return [str(payload.get("mode") or ""), str(payload.get("target") or ""), *([options] if options else [])]
1464
1489
 
1465
1490
  def call(self) -> str:
@@ -1471,22 +1496,37 @@ class InspectCodeTool(Tool):
1471
1496
  if len(self.args) == 3 and not isinstance(self.args[2], dict):
1472
1497
  raise ToolError("InspectCode options must be an object")
1473
1498
  options = self.args[2] if len(self.args) == 3 else {}
1474
- if unexpected := sorted(set(options) - {"limit", "kind", "path", "symbol", "exact_only"}):
1499
+ if unexpected := sorted(set(options) - set(self.OPTION_KEYS)):
1475
1500
  raise ToolError("InspectCode unexpected option: " + ", ".join(unexpected))
1476
- if mode not in {"find", "inspect", "outline"}:
1477
- raise ToolError("InspectCode mode must be find, inspect, or outline")
1501
+ if mode not in self.MODES:
1502
+ raise ToolError("InspectCode mode must be one of: " + ", ".join(self.MODES))
1478
1503
  if not target:
1479
1504
  raise ToolError("InspectCode target is required")
1480
- if mode in {"find", "inspect"} and re.search(r"\s", target):
1505
+ if mode in self.SYMBOL_MODES and re.search(r"\s", target):
1481
1506
  raise ToolError("InspectCode symbol target must not contain whitespace")
1482
- if mode == "inspect" and (target.endswith(".py") or os.path.exists(self.session.resolve_path(target))):
1483
- raise ToolError("InspectCode inspect target must be a symbol, not a file")
1507
+ if mode in self.RESOLVE_MODES and (target.endswith(".py") or os.path.exists(self.session.resolve_path(target))):
1508
+ raise ToolError(f"InspectCode {mode} target must be a symbol, not a file")
1484
1509
  if mode == "outline" and not os.path.isfile(self.session.resolve_path(target)):
1485
1510
  raise ToolError("InspectCode outline target must be an existing file")
1486
1511
  limit = options.get("limit")
1487
1512
  max_limit = self.MAX_OUTLINE_LIMIT if mode == "outline" else self.MAX_LIMIT
1488
1513
  if limit is not None and (isinstance(limit, bool) or not isinstance(limit, int) or limit < 1 or limit > max_limit):
1489
1514
  raise ToolError(f"InspectCode {mode} limit must be 1..{max_limit}")
1515
+ depth = options.get("depth")
1516
+ if depth is not None and (isinstance(depth, bool) or not isinstance(depth, int) or depth < 1 or depth > self.MAX_DEPTH):
1517
+ raise ToolError(f"InspectCode depth must be 1..{self.MAX_DEPTH}")
1518
+ offset = options.get("offset")
1519
+ if offset is not None and (isinstance(offset, bool) or not isinstance(offset, int) or offset < 0):
1520
+ raise ToolError("InspectCode offset must be >= 0")
1521
+ ref_kind = options.get("ref_kind")
1522
+ if ref_kind is not None:
1523
+ if not isinstance(ref_kind, str):
1524
+ raise ToolError("InspectCode ref_kind must be a string")
1525
+ if options.get("all_kinds"):
1526
+ raise ToolError("InspectCode ref_kind and all_kinds are mutually exclusive")
1527
+ tokens = [token.strip() for token in ref_kind.split(",") if token.strip()]
1528
+ if unknown := sorted(set(tokens) - csi.REFERENCE_KINDS):
1529
+ raise ToolError("InspectCode unknown ref_kind: " + ", ".join(unknown) + "; valid: " + ", ".join(sorted(csi.REFERENCE_KINDS)))
1490
1530
  index = CodeIndex(self.session)
1491
1531
  if not index.available():
1492
1532
  raise ToolError("code index is not available; run /index")
@@ -1508,6 +1548,16 @@ class InspectCodeTool(Tool):
1508
1548
  return csi.search(target, limit=limit or csi.DEFAULT_SEARCH_LIMIT, **common)
1509
1549
  if mode == "inspect":
1510
1550
  return csi.inspect(target, limit=limit or csi.DEFAULT_PAGE_LIMIT, anchors=True, **common)
1551
+ if mode == "refs":
1552
+ ref_kinds = options.get("ref_kind") or ("all" if options.get("all_kinds") else "behavioral")
1553
+ return csi.refs(target, limit=limit or csi.DEFAULT_MAX_REFERENCES, offset=int(options.get("offset") or 0), ref_kinds=ref_kinds, **common)
1554
+ if mode == "impls":
1555
+ return csi.impls(target, limit=limit or csi.DEFAULT_MAX_IMPLEMENTORS, offset=int(options.get("offset") or 0), **common)
1556
+ if mode in self.CHAIN_MODES:
1557
+ depth = int(options.get("depth") or 3)
1558
+ if mode == "callees":
1559
+ return csi.callees(target, limit=limit or csi.DEFAULT_MAX_CALLEES, depth=depth, loose=bool(options.get("loose")), **common)
1560
+ return csi.callers(target, limit=limit or csi.DEFAULT_MAX_CALLERS, depth=depth, **common)
1511
1561
  symbol = options.get("symbol") or None
1512
1562
  return csi.outline(
1513
1563
  target, root=self.session.cwd, symbol=str(symbol) if symbol else None, max_symbols=limit or csi.DEFAULT_MAX_OUTLINE_SYMBOLS, format="text"
@@ -1746,7 +1796,7 @@ class EditTool(Tool):
1746
1796
 
1747
1797
  class BashTool(Tool):
1748
1798
  NAME = "Bash"
1749
- DESCRIPTION = "Run one bash shell invocation in the workspace; returns exit_code/stdout/stderr and shows live output."
1799
+ DESCRIPTION = "Run one bash shell invocation in the workspace; returns exit_code/stdout/stderr and shows live output. Avoid unbounded output; limit noisy commands with head/tail/sed/rg filters or command-specific limits, and inspect large outputs in chunks."
1750
1800
  SIGNATURE = "Bash(command)"
1751
1801
  EXAMPLE = (
1752
1802
  'Check environment. Example: {"command":"python3 --version"}',
@@ -1873,7 +1923,7 @@ class BashTool(Tool):
1873
1923
 
1874
1924
  class GitTool(Tool):
1875
1925
  NAME = "Git"
1876
- DESCRIPTION = 'Run git with explicit argv in the workspace. For add, pass explicit file paths; broad add is rejected.'
1926
+ DESCRIPTION = 'Run git with explicit argv (default: cwd from Environment; use cwd= for other directories). For add, pass explicit file paths; broad add is rejected.'
1877
1927
  SIGNATURE = "Git(argv=[command,...], cwd?)"
1878
1928
  EXAMPLE = (
1879
1929
  'Status. Example: {"argv":["status","--short"]}',
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nanocode-cli
3
- Version: 0.5.9
3
+ Version: 0.5.11
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
@@ -22,7 +22,7 @@ Requires-Python: >=3.11
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
24
  Requires-Dist: anthropic>=0.64.0
25
- Requires-Dist: code-symbol-index>=0.1.13
25
+ Requires-Dist: code-symbol-index>=0.3.0
26
26
  Requires-Dist: json-repair
27
27
  Requires-Dist: openai>=2.37.0
28
28
  Requires-Dist: prompt-toolkit>=3.0
@@ -39,6 +39,8 @@ A small terminal coding agent written in Python.
39
39
 
40
40
  [简体中文](README.zh-CN.md)
41
41
 
42
+ [Chinese blog](https://hit9.dev/post/nanocode)
43
+
42
44
  nanocode is pre-1.0 software. Commands, configuration, and tool behavior may change before a stable release.
43
45
 
44
46
  ![nanocode screenshot](snapshots/nanocode-snapshot.png)
@@ -48,7 +50,7 @@ nanocode is pre-1.0 software. Commands, configuration, and tool behavior may cha
48
50
  - **Live turn control**: Add follow-up input while the agent is still working, without losing the current tool flow.
49
51
  - **File-state brain**: Reads and edits build a current, line-numbered view of the files that matter now.
50
52
  - **Stale-edit protection**: `line:hash` anchors reject edits when the target code has drifted.
51
- - **Project-aware navigation**: Use the symbol index to jump through outlines, references, and changed files quickly.
53
+ - **Project-aware navigation**: Use the symbol index to jump through outlines, references, implementors, call chains, and changed files quickly.
52
54
  - **Recoverable context**: Tool output stays bounded in the prompt, while raw `tr.N` results remain recallable.
53
55
  - **Cache-aware context**: Stable sections stay early and noisy working state stays late to improve prompt-cache reuse.
54
56
  - **Focused working memory**: `Note` separates goal, plan, and known facts from noisy execution logs.
@@ -138,6 +140,16 @@ Main fields:
138
140
 
139
141
  `api = "auto"` chooses between Chat Completions and Anthropic Messages using provider/model profiles. `prompt_cache_key = "auto"` derives a stable key from provider, model, workspace, and tool schema names.
140
142
 
143
+
144
+ ## Tested Providers
145
+
146
+ The following providers have been tested with nanocode:
147
+
148
+ - **deepseek**: DeepSeek API
149
+ - **opencode**: OpenCode API
150
+ - **aliyun**: Alibaba Cloud (Tongyi Qianwen) API via Chat Completions
151
+ - **llama.cpp**: Local inference via llama.cpp server
152
+
141
153
  ## Context Design
142
154
 
143
155
  Each model request is built manually from explicit messages. Stable context comes first, conversation stays as messages, working memory follows, and the latest file state is appended at the end.
@@ -1,5 +1,5 @@
1
1
  anthropic>=0.64.0
2
- code-symbol-index>=0.1.13
2
+ code-symbol-index>=0.3.0
3
3
  json-repair
4
4
  openai>=2.37.0
5
5
  prompt-toolkit>=3.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nanocode-cli"
7
- version = "0.5.9"
7
+ version = "0.5.11"
8
8
  description = "A small terminal coding agent written in Python"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -28,7 +28,7 @@ classifiers = [
28
28
  ]
29
29
  dependencies = [
30
30
  "anthropic>=0.64.0",
31
- "code-symbol-index>=0.1.13",
31
+ "code-symbol-index>=0.3.0",
32
32
  "json-repair",
33
33
  "openai>=2.37.0",
34
34
  "prompt-toolkit>=3.0",
File without changes
File without changes
File without changes