nanocode-cli 0.7.0__tar.gz → 0.7.1__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.7.0
3
+ Version: 0.7.1
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
@@ -60,7 +60,7 @@ from rich.console import Console
60
60
  from rich.markdown import Markdown
61
61
  from rich.rule import Rule
62
62
 
63
- __version__ = "0.7.0"
63
+ __version__ = "0.7.1"
64
64
 
65
65
  Json = dict[str, Any]
66
66
  HTTP_USER_AGENT = "nanocode/" + __version__
@@ -490,6 +490,7 @@ class AgentState:
490
490
  code_index_error: str = ""
491
491
  code_index_notice: str = ""
492
492
  code_index_refreshing: bool = False
493
+ code_index_checking: bool = False
493
494
  context_percent: int = 0
494
495
  turn_step: int = 0
495
496
  turn_tool_calls: int = 0
@@ -528,7 +529,9 @@ class AgentState:
528
529
 
529
530
  def format(self) -> str:
530
531
  known = ["- " + item for item in self.known] or ["- (empty)"]
531
- return "\n".join(["Goal: " + (self.goal or "(empty)"), "Plan:", *self.plan_rows_for(self.plan), "Known:", *known, "Check: " + (self.check or "(empty)")])
532
+ return "\n".join(
533
+ ["Goal: " + (self.goal or "(empty)"), "Plan:", *self.plan_rows_for(self.plan), "Known:", *known, "Check: " + (self.check or "(empty)")]
534
+ )
532
535
 
533
536
 
534
537
  @dataclass
@@ -915,16 +918,12 @@ class SessionSnapshotCodec:
915
918
  @staticmethod
916
919
  def tool_records(data: list[Json]) -> list[ToolResultRecord]:
917
920
  return [
918
- ToolResultRecord(key=rec["key"], name=rec["name"], args=rec.get("args", []), output=rec.get("output", ""), note=rec.get("note", ""))
919
- for rec in data
921
+ ToolResultRecord(key=rec["key"], name=rec["name"], args=rec.get("args", []), output=rec.get("output", ""), note=rec.get("note", "")) for rec in data
920
922
  ]
921
923
 
922
924
  @staticmethod
923
925
  def tool_errors(data: list[Json]) -> list[ToolErrorRecord]:
924
- return [
925
- ToolErrorRecord(key=err["key"], name=err["name"], args=err.get("args", []), error=err.get("error", ""))
926
- for err in data
927
- ]
926
+ return [ToolErrorRecord(key=err["key"], name=err["name"], args=err.get("args", []), error=err.get("error", "")) for err in data]
928
927
 
929
928
 
930
929
  class SessionSnapshotStore:
@@ -2001,6 +2000,25 @@ class CodeIndex:
2001
2000
  return ""
2002
2001
  return self.update([self.session.resolve_path(path) for path in files])
2003
2002
 
2003
+ def update_pending_async(self) -> None:
2004
+ """Run the working-tree check (and any auto-update) off the UI critical path.
2005
+
2006
+ ``update_pending`` does a ``check=True`` scan that walks/hashes the tree — slow on
2007
+ large repos. Running it inline blocks answer emission and /status, so spawn it in a
2008
+ daemon thread. Guarded so only one scan/update runs at a time.
2009
+ """
2010
+ if self.session.state.code_index_checking or self.session.state.code_index_refreshing:
2011
+ return
2012
+ self.session.state.code_index_checking = True
2013
+
2014
+ def run() -> None:
2015
+ try:
2016
+ self.update_pending()
2017
+ finally:
2018
+ self.session.state.code_index_checking = False
2019
+
2020
+ threading.Thread(target=run, daemon=True).start()
2021
+
2004
2022
  def refresh_existing_async(self) -> bool:
2005
2023
  if self.session.state.code_index_refreshing:
2006
2024
  return False
@@ -2565,7 +2583,9 @@ class BashTool(Tool):
2565
2583
 
2566
2584
  class GitTool(Tool):
2567
2585
  NAME = "Git"
2568
- 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.'
2586
+ DESCRIPTION = (
2587
+ "Run git with explicit argv (default: cwd from Environment; use cwd= for other directories). For add, pass explicit file paths; broad add is rejected."
2588
+ )
2569
2589
  SIGNATURE = "Git(argv=[command,...], cwd?)"
2570
2590
  EXAMPLE = (
2571
2591
  'Status. Example: {"argv":["status","--short"]}',
@@ -2589,11 +2609,7 @@ class GitTool(Tool):
2589
2609
  def payload_args(cls, payload: Json) -> list[Any]:
2590
2610
  argv = payload.get("argv")
2591
2611
  if not isinstance(argv, list) or not argv:
2592
- raise ToolError(
2593
- "Git requires a non-empty 'argv' list. "
2594
- 'Signature: Git(argv=[command,...], cwd?) '
2595
- 'Example: {"argv":["status","--short"]}'
2596
- )
2612
+ raise ToolError('Git requires a non-empty \'argv\' list. Signature: Git(argv=[command,...], cwd?) Example: {"argv":["status","--short"]}')
2597
2613
  argv = [str(a) for a in argv]
2598
2614
  return [("cwd=" + str(payload["cwd"])), *argv] if payload.get("cwd") else argv
2599
2615
 
@@ -2785,7 +2801,13 @@ class NoteTool(Tool):
2785
2801
  }
2786
2802
  return {
2787
2803
  "type": "object",
2788
- "properties": {"set_goal": {"type": "string"}, "replace_plan": {"type": "array", "items": plan_item}, "append_known": strings, "replace_known": strings, "set_check": {"type": "string"}},
2804
+ "properties": {
2805
+ "set_goal": {"type": "string"},
2806
+ "replace_plan": {"type": "array", "items": plan_item},
2807
+ "append_known": strings,
2808
+ "replace_known": strings,
2809
+ "set_check": {"type": "string"},
2810
+ },
2789
2811
  "additionalProperties": False,
2790
2812
  }
2791
2813
 
@@ -2872,7 +2894,7 @@ class QuestionSpec:
2872
2894
  class QuestionTool(Tool):
2873
2895
  NAME = "Question"
2874
2896
  DESCRIPTION = "Ask the user one or more questions (asked in sequence) and wait for their answers. Use when intent is genuinely ambiguous, a choice affects the codebase's external shape (module layout, public API, naming), or you need prioritization; prefer offering choices with previews, and optionally a recommended index when one option is clearly best. Do NOT ask about trivial internal details or anything determinable from context (Read/InspectCode/Bash) or already specified; if a reasonable default exists, proceed."
2875
- SIGNATURE = 'Question(questions=[{question, choices?, previews?, recommended?}, ...])'
2897
+ SIGNATURE = "Question(questions=[{question, choices?, previews?, recommended?}, ...])"
2876
2898
  EXAMPLE = (
2877
2899
  'One question, recommending a choice. Example: {"questions":[{"question":"Which approach?","choices":["Refactor","Rewrite"],"previews":["Extract module +87 -12","Rewrite from scratch"],"recommended":0}]}',
2878
2900
  'Batch related questions. Example: {"questions":[{"question":"Target runtime?","choices":["Node","Deno"]},{"question":"Name the module?"}]}',
@@ -2971,6 +2993,7 @@ class QuestionTool(Tool):
2971
2993
  label = Tool.compact(first, 80)
2972
2994
  return [label + (f" (+{len(questions) - 1} more)" if len(questions) > 1 else "")]
2973
2995
 
2996
+
2974
2997
  class MCPTool(Tool):
2975
2998
  NAME = "MCP"
2976
2999
  DESCRIPTION = "Call/describe external MCP server tools, and list/read MCP resources"
@@ -3085,9 +3108,10 @@ class MCPTool(Tool):
3085
3108
  return mcp.read_resource(server, str(payload.get("uri") or ""))
3086
3109
  raise ToolError(
3087
3110
  f"unknown MCP action {action!r}. Valid actions: {', '.join(self.ACTIONS)}. "
3088
- f"To invoke a remote tool named {action!r}, use action=\"call\", tool={action!r}."
3111
+ f'To invoke a remote tool named {action!r}, use action="call", tool={action!r}.'
3089
3112
  )
3090
3113
 
3114
+
3091
3115
  TOOLS: tuple[type[Tool], ...] = (
3092
3116
  MCPTool,
3093
3117
  ReadTool,
@@ -3121,8 +3145,7 @@ class ContextManager:
3121
3145
  COMPACT_RECENT_MESSAGES: ClassVar[int] = 8
3122
3146
  MCP_DESCRIBE_BLOCK: ClassVar[re.Pattern] = re.compile(r"<MCPDescribe server=(\".*?\") tool=(\".*?\")>.*?</MCPDescribe>", re.DOTALL)
3123
3147
  CODE_EXTENSIONS: ClassVar[set[str]] = set(
3124
- ".c .cc .cpp .cxx .css .go .h .hpp .html .java .js .json .jsx .kt .lua .php .py .rb .rs .scss .sh .sql "
3125
- ".swift .toml .ts .tsx .vue .yaml .yml".split()
3148
+ ".c .cc .cpp .cxx .css .go .h .hpp .html .java .js .json .jsx .kt .lua .php .py .rb .rs .scss .sh .sql .swift .toml .ts .tsx .vue .yaml .yml".split()
3126
3149
  )
3127
3150
  CODE_FILENAMES: ClassVar[set[str]] = {"CMakeLists.txt", "Dockerfile", "Makefile", "go.mod", "package.json", "pyproject.toml"}
3128
3151
 
@@ -4185,7 +4208,9 @@ class MCPManager:
4185
4208
 
4186
4209
  try:
4187
4210
  result = self.run_async(
4188
- self._call_oauth_tool(config, headers, tool_name, arguments) if config.auth == "oauth" else self._call_tool(config, headers, tool_name, arguments)
4211
+ self._call_oauth_tool(config, headers, tool_name, arguments)
4212
+ if config.auth == "oauth"
4213
+ else self._call_tool(config, headers, tool_name, arguments)
4189
4214
  )
4190
4215
  except Exception as e:
4191
4216
  raise ToolError("MCP call failed: " + self.error_text(e))
@@ -4226,9 +4251,7 @@ class MCPManager:
4226
4251
  raise ToolError("MCP read_resource requires a uri")
4227
4252
  config, headers = self._resource_preamble(server)
4228
4253
  try:
4229
- result = self.run_async(
4230
- self._read_oauth_resource(config, headers, uri) if config.auth == "oauth" else self._read_resource(config, headers, uri)
4231
- )
4254
+ result = self.run_async(self._read_oauth_resource(config, headers, uri) if config.auth == "oauth" else self._read_resource(config, headers, uri))
4232
4255
  except Exception as e:
4233
4256
  raise ToolError("MCP resource read failed: " + self.error_text(e))
4234
4257
  text = self.normalize_resource(result)
@@ -4346,7 +4369,9 @@ class MCPManager:
4346
4369
  from fastmcp.client.transports import StreamableHttpTransport
4347
4370
 
4348
4371
  timeout = self.discovery_timeout()
4349
- async with Client(StreamableHttpTransport(config.url, headers=headers), auth=self.oauth_client(config), timeout=timeout, init_timeout=timeout) as client:
4372
+ async with Client(
4373
+ StreamableHttpTransport(config.url, headers=headers), auth=self.oauth_client(config), timeout=timeout, init_timeout=timeout
4374
+ ) as client:
4350
4375
  return await asyncio.wait_for(client.list_resources(), timeout=timeout)
4351
4376
 
4352
4377
  async def _read_resource(self, config: MCPServerConfig, headers: dict[str, str], uri: str) -> Any:
@@ -4361,7 +4386,9 @@ class MCPManager:
4361
4386
  from fastmcp.client.transports import StreamableHttpTransport
4362
4387
 
4363
4388
  timeout = self.call_timeout()
4364
- async with Client(StreamableHttpTransport(config.url, headers=headers), auth=self.oauth_client(config), timeout=timeout, init_timeout=timeout) as client:
4389
+ async with Client(
4390
+ StreamableHttpTransport(config.url, headers=headers), auth=self.oauth_client(config), timeout=timeout, init_timeout=timeout
4391
+ ) as client:
4365
4392
  return await asyncio.wait_for(client.read_resource(uri), timeout=timeout)
4366
4393
 
4367
4394
  async def _call_oauth_tool(self, config: MCPServerConfig, headers: dict[str, str], name: str, arguments: Json) -> Any:
@@ -4369,7 +4396,9 @@ class MCPManager:
4369
4396
  from fastmcp.client.transports import StreamableHttpTransport
4370
4397
 
4371
4398
  timeout = self.call_timeout()
4372
- async with Client(StreamableHttpTransport(config.url, headers=headers), auth=self.oauth_client(config), timeout=timeout, init_timeout=timeout) as client:
4399
+ async with Client(
4400
+ StreamableHttpTransport(config.url, headers=headers), auth=self.oauth_client(config), timeout=timeout, init_timeout=timeout
4401
+ ) as client:
4373
4402
  return await asyncio.wait_for(client.call_tool(name, arguments), timeout=timeout)
4374
4403
 
4375
4404
  def login_server(self, name: str, notify: Callable[[str], None] | None = None) -> str:
@@ -4553,7 +4582,7 @@ class MCPManager:
4553
4582
  if line:
4554
4583
  lines.append(line)
4555
4584
  if resources:
4556
- lines.append(f"resources ({len(resources)}) — read with MCP(action=\"read_resource\", server={json.dumps(config.name)}, uri=...):")
4585
+ lines.append(f'resources ({len(resources)}) — read with MCP(action="read_resource", server={json.dumps(config.name)}, uri=...):')
4557
4586
  lines.extend(self._format_resource_line(res) for res in resources)
4558
4587
  lines.append("")
4559
4588
 
@@ -4633,7 +4662,7 @@ class MCPManager:
4633
4662
  lines.append(line)
4634
4663
  resources = self.resources.get(server, [])
4635
4664
  if resources:
4636
- lines.append(f"resources ({len(resources)}) — read with MCP(action=\"read_resource\", server={json.dumps(server)}, uri=...):")
4665
+ lines.append(f'resources ({len(resources)}) — read with MCP(action="read_resource", server={json.dumps(server)}, uri=...):')
4637
4666
  lines.extend(self._format_resource_line(res) for res in resources)
4638
4667
  return "\n".join(lines)
4639
4668
 
@@ -4651,7 +4680,7 @@ class MCPManager:
4651
4680
  # truncated above, so surface any resource-like URIs it mentions explicitly.
4652
4681
  uris = self._extract_uris(info.description)
4653
4682
  if uris:
4654
- line += "\n refs (read with MCP action=\"read_resource\"): " + ", ".join(uris)
4683
+ line += '\n refs (read with MCP action="read_resource"): ' + ", ".join(uris)
4655
4684
  if include_schema:
4656
4685
  schema = self._schema_json(info.input_schema, self.INDEX_SCHEMA_LIMIT)
4657
4686
  if schema:
@@ -4760,13 +4789,24 @@ class MCPManager:
4760
4789
  if config.env_http_headers:
4761
4790
  for header_name in config.env_http_headers:
4762
4791
  auth.append("env_header(" + header_name + ")")
4763
- lines.append("| `" + self.markdown_cell(config.name) + "` | " + self.markdown_cell(status) + " | " + self.markdown_cell(tools or "-") + " | " + self.markdown_cell(", ".join(auth) or "-") + " |")
4792
+ lines.append(
4793
+ "| `"
4794
+ + self.markdown_cell(config.name)
4795
+ + "` | "
4796
+ + self.markdown_cell(status)
4797
+ + " | "
4798
+ + self.markdown_cell(tools or "-")
4799
+ + " | "
4800
+ + self.markdown_cell(", ".join(auth) or "-")
4801
+ + " |"
4802
+ )
4764
4803
  return "\n".join(lines) if len(lines) > 2 else "(no MCP servers configured)"
4765
4804
 
4766
4805
  @staticmethod
4767
4806
  def markdown_cell(text: str) -> str:
4768
4807
  return Text.clean(str(text)).replace("\n", " ").replace("|", "\\|")
4769
4808
 
4809
+
4770
4810
  class ToolRunner:
4771
4811
  def __init__(self, session: Session, context: ContextManager, input_fn=input, output_fn=print):
4772
4812
  self.session = session
@@ -4991,7 +5031,9 @@ class ToolRunner:
4991
5031
  self.session.record_tool_error(key or "-", call.name, call.args, output)
4992
5032
  elif key:
4993
5033
  self.update_code_index(call, output)
4994
- self.output_fn(self.finish_display(call, key, output, failed=failed, approved=approved, auto=auto, display=display, batch_suffix=batch_suffix, elapsed=elapsed))
5034
+ self.output_fn(
5035
+ self.finish_display(call, key, output, failed=failed, approved=approved, auto=auto, display=display, batch_suffix=batch_suffix, elapsed=elapsed)
5036
+ )
4995
5037
  return self.tool_message(call, key, output, failed=failed, display=display)
4996
5038
 
4997
5039
  def tool_message(self, call: ToolCall, key: str, output: str, *, failed: bool = False, display: str | None = None) -> str:
@@ -5067,7 +5109,17 @@ class ToolRunner:
5067
5109
  return "\n".join([" preview", *(" " + line for line in lines)])
5068
5110
 
5069
5111
  def finish_display(
5070
- self, call: ToolCall, key: str, output: str, *, failed: bool, approved: bool = False, auto: bool = False, display: str | None = None, batch_suffix: str = "", elapsed: float | None = None
5112
+ self,
5113
+ call: ToolCall,
5114
+ key: str,
5115
+ output: str,
5116
+ *,
5117
+ failed: bool,
5118
+ approved: bool = False,
5119
+ auto: bool = False,
5120
+ display: str | None = None,
5121
+ batch_suffix: str = "",
5122
+ elapsed: float | None = None,
5071
5123
  ) -> str:
5072
5124
  if call.name == "Note" and not failed and display:
5073
5125
  return self.with_batch_suffix(display.removeprefix("Note ").strip(), batch_suffix)
@@ -5949,6 +6001,7 @@ class UiPrinter:
5949
6001
  at_start = part.endswith("\n")
5950
6002
  return indented
5951
6003
 
6004
+
5952
6005
  class BashLivePreview:
5953
6006
  HEIGHT: ClassVar[int] = 6
5954
6007
  MAX_CHARS: ClassVar[int] = 8000
@@ -6484,7 +6537,6 @@ Tools:
6484
6537
  while self.queue_input_active.is_set() and time.monotonic() < deadline:
6485
6538
  time.sleep(0.01)
6486
6539
 
6487
-
6488
6540
  def drain_queued_input(self) -> str:
6489
6541
  """Return queued user input, or empty string if nothing queued."""
6490
6542
  texts = []
@@ -6495,6 +6547,7 @@ Tools:
6495
6547
  texts.append(self.queue_input_text)
6496
6548
  self.queue_input_text = ""
6497
6549
  return "\n".join(texts)
6550
+
6498
6551
  def run(self) -> int:
6499
6552
  self.emit(f"nanocode {__version__}. /help for commands.")
6500
6553
  self.session.clean_expired_snapshots()
@@ -6546,7 +6599,7 @@ Tools:
6546
6599
  watcher.join(timeout=1.0)
6547
6600
  self.queue_input_paused.clear()
6548
6601
  self.session.state.manual_model_retry_requested = False
6549
- CodeIndex(self.session).update_pending()
6602
+ CodeIndex(self.session).update_pending_async()
6550
6603
  self.status_bar.stop()
6551
6604
  elapsed = time.monotonic() - started
6552
6605
  self.ui.emit_answer(answer)
@@ -6639,14 +6692,12 @@ Tools:
6639
6692
  # the index is too large to fit even as name-only (some tools are hidden from the model).
6640
6693
  self.session.mcp.render_tools_index()
6641
6694
  if self.session.mcp.index_truncated:
6642
- self.emit("mcp: tools index exceeds the size budget; some tools are hidden from the model. Reduce enabled servers or run /mcp tools to see the full list.")
6695
+ self.emit(
6696
+ "mcp: tools index exceeds the size budget; some tools are hidden from the model. Reduce enabled servers or run /mcp tools to see the full list."
6697
+ )
6643
6698
 
6644
6699
  def mcp_error_notice(self) -> str:
6645
- errors = [
6646
- (name, error)
6647
- for name, error in sorted(self.session.mcp.server_errors.items())
6648
- if error and not error.startswith("oauth login required")
6649
- ]
6700
+ errors = [(name, error) for name, error in sorted(self.session.mcp.server_errors.items()) if error and not error.startswith("oauth login required")]
6650
6701
  if not errors:
6651
6702
  return ""
6652
6703
  shown = errors if self.session.settings.debug else errors[:3]
@@ -7239,7 +7290,11 @@ Tools:
7239
7290
  previews = spec.previews
7240
7291
  preview_map = {c: previews[i] for i, c in enumerate(choices) if previews and i < len(previews) and previews[i]}
7241
7292
  result = self.choice_application(
7242
- "Select:", tuple(choices), labels, current, set(),
7293
+ "Select:",
7294
+ tuple(choices),
7295
+ labels,
7296
+ current,
7297
+ set(),
7243
7298
  preview_fn=lambda choice: preview_map.get(choice, ""),
7244
7299
  free_text=True,
7245
7300
  )
@@ -7270,7 +7325,9 @@ Tools:
7270
7325
  usage = self.session.usage
7271
7326
  provider = self.session.config.provider
7272
7327
  self.agent.context.update_percent(self.agent.context.model_messages(self.agent.SYSTEM_PROMPT))
7273
- index_status, index_message = CodeIndex(self.session).status(check=True)
7328
+ index = CodeIndex(self.session)
7329
+ index_status, index_message = index.status(check=False)
7330
+ index.update_pending_async()
7274
7331
  if self.session.state.code_index_refreshing:
7275
7332
  index_status, index_message = self.session.state.code_index_notice or "syncing", ""
7276
7333
  elif self.session.state.code_index_error:
@@ -7316,7 +7373,15 @@ Tools:
7316
7373
  state = self.session.state
7317
7374
  known = ["- " + item for item in state.known] or ["- (empty)"]
7318
7375
  return "\n".join(
7319
- ["goal: " + (state.goal or "(empty)"), "summary:", state.summary or "(empty)", "plan:", *AgentState.plan_rows_for(state.plan, status=True, style="symbol"), "known:", *known]
7376
+ [
7377
+ "goal: " + (state.goal or "(empty)"),
7378
+ "summary:",
7379
+ state.summary or "(empty)",
7380
+ "plan:",
7381
+ *AgentState.plan_rows_for(state.plan, status=True, style="symbol"),
7382
+ "known:",
7383
+ *known,
7384
+ ]
7320
7385
  )
7321
7386
 
7322
7387
  def config(self, args: str) -> str:
@@ -7568,8 +7633,7 @@ def main(argv: list[str] | None = None) -> int:
7568
7633
  parser.add_argument("--yolo", action="store_true", help="Skip confirmations for mutating tools")
7569
7634
  parser.add_argument("--debug", action="store_true", help="Enable debug mode")
7570
7635
  parser.add_argument("--mcp", default="", help='Filter MCP servers, e.g. "orion*,!orionEval", "all", or "none"')
7571
- parser.add_argument("--resume", default="", nargs="?", const="latest",
7572
- help='Resume a session by UID, or "latest"/"last" for most recent')
7636
+ parser.add_argument("--resume", default="", nargs="?", const="latest", help='Resume a session by UID, or "latest"/"last" for most recent')
7573
7637
  parser.add_argument("-v", "--version", action="store_true", help="Show version")
7574
7638
  args = parser.parse_args(argv)
7575
7639
  if args.version:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nanocode-cli
3
- Version: 0.7.0
3
+ Version: 0.7.1
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.7.0"
7
+ version = "0.7.1"
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