nanocode-cli 0.6.1__tar.gz → 0.6.3__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.6.1
3
+ Version: 0.6.3
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
@@ -121,6 +121,7 @@ Interactive selectors support `j`/`k`, arrows, `/` search, Enter, and Esc. Input
121
121
  - Shell: `Bash`, `Git`.
122
122
  - Tool results: `Recall`.
123
123
  - Working notes: `Note`.
124
+ - Ask the user: `Question` asks one or more questions (optional choices, previews, recommended) when intent is genuinely ambiguous.
124
125
  - MCP: `MCP` calls tools on configured MCP servers.
125
126
 
126
127
  `Read`, `Search`, and `InspectCode` return line anchors where useful. `Edit` uses current `line:hash` anchors to reject stale edits.
@@ -85,6 +85,7 @@ Interactive selectors support `j`/`k`, arrows, `/` search, Enter, and Esc. Input
85
85
  - Shell: `Bash`, `Git`.
86
86
  - Tool results: `Recall`.
87
87
  - Working notes: `Note`.
88
+ - Ask the user: `Question` asks one or more questions (optional choices, previews, recommended) when intent is genuinely ambiguous.
88
89
  - MCP: `MCP` calls tools on configured MCP servers.
89
90
 
90
91
  `Read`, `Search`, and `InspectCode` return line anchors where useful. `Edit` uses current `line:hash` anchors to reject stale edits.
@@ -85,6 +85,7 @@ nanocode
85
85
  - Shell:`Bash`, `Git`。
86
86
  - 工具结果:`Recall`。
87
87
  - 工作笔记:`Note`。
88
+ - 询问用户:`Question` 在意图真正歧义时向用户提一个或多个问题(可选 choices、previews、recommended)。
88
89
  - MCP:`MCP` 调用已配置 MCP 服务器上的工具。
89
90
 
90
91
  `Read`、`Search` 和 `InspectCode` 会在合适时返回行锚点。`Edit` 使用当前 `line:hash` 锚点拒绝过期编辑。
@@ -59,7 +59,7 @@ from rich.console import Console
59
59
  from rich.markdown import Markdown
60
60
  from rich.rule import Rule
61
61
 
62
- __version__ = "0.6.1"
62
+ __version__ = "0.6.3"
63
63
 
64
64
  Json = dict[str, Any]
65
65
  HTTP_USER_AGENT = "nanocode/" + __version__
@@ -81,6 +81,8 @@ CHAT_REASONING_EFFORT_VALUES: dict[str, dict[str, str | int]] = {
81
81
  "enable_thinking": {"minimal": 256, "low": 1024, "medium": 4096, "high": 8192, "xhigh": 16384},
82
82
  }
83
83
  SELECTION_BACK = object()
84
+ SELECTION_FREE_TEXT = object()
85
+ DISMISSED = "(The user dismissed the question without answering.)"
84
86
 
85
87
 
86
88
  class NanocodeError(Exception):
@@ -704,7 +706,7 @@ class SystemInfo:
704
706
  class Session:
705
707
  cwd: str = field(default_factory=os.getcwd)
706
708
  system_info: SystemInfo | None = None
707
- initial_git_branch: str = ""
709
+ expected_git_branch: str = ""
708
710
  config: Config = field(default_factory=Config)
709
711
  settings: RuntimeSettings = field(default_factory=RuntimeSettings)
710
712
  messages: list[Json] = field(default_factory=list)
@@ -722,8 +724,8 @@ class Session:
722
724
  def __post_init__(self) -> None:
723
725
  if self.system_info is None:
724
726
  self.system_info = SystemInfo.detect(self.cwd)
725
- if not self.initial_git_branch:
726
- self.initial_git_branch = self.git_branch(self.cwd)
727
+ if not self.expected_git_branch:
728
+ self.expected_git_branch = self.git_branch(self.cwd)
727
729
  if self.mcp is None:
728
730
  self.mcp = MCPManager(self)
729
731
 
@@ -2183,7 +2185,14 @@ class GitTool(Tool):
2183
2185
 
2184
2186
  @classmethod
2185
2187
  def payload_args(cls, payload: Json) -> list[Any]:
2186
- argv = list(payload.get("argv") or [])
2188
+ argv = payload.get("argv")
2189
+ if not isinstance(argv, list) or not argv:
2190
+ raise ToolError(
2191
+ "Git requires a non-empty 'argv' list. "
2192
+ 'Signature: Git(argv=[command,...], cwd?) '
2193
+ 'Example: {"argv":["status","--short"]}'
2194
+ )
2195
+ argv = [str(a) for a in argv]
2187
2196
  return [("cwd=" + str(payload["cwd"])), *argv] if payload.get("cwd") else argv
2188
2197
 
2189
2198
  def needs_confirmation(self) -> bool:
@@ -2198,6 +2207,11 @@ class GitTool(Tool):
2198
2207
  self.validate_branch_safety(args, cwd)
2199
2208
  try:
2200
2209
  proc = subprocess.run([git, *args], cwd=cwd, text=True, capture_output=True, timeout=self.session.settings.shell_timeout)
2210
+ # When the user has nanocode switch branches, update expected_git_branch to the new
2211
+ # branch so later commits are not rejected. Gate on branch-changing commands only: an
2212
+ # unexpected (external) switch should still trip validate_branch_safety on commit.
2213
+ if proc.returncode == 0 and self.changes_branch(args):
2214
+ self.session.expected_git_branch = self.session.git_branch(cwd)
2201
2215
  return self.process_result("GitToolResult", proc.returncode, proc.stdout, proc.stderr)
2202
2216
  except subprocess.TimeoutExpired as error:
2203
2217
  return self.process_result("GitToolResult", -1, error.stdout or "", (error.stderr or "") + "\ntimeout")
@@ -2227,9 +2241,9 @@ class GitTool(Tool):
2227
2241
  if args[0] != "commit":
2228
2242
  return
2229
2243
  current = self.session.git_branch(cwd)
2230
- initial = self.session.initial_git_branch
2231
- if initial and current and current != initial:
2232
- raise ToolError(f"refusing git commit because branch changed from {initial} to {current}")
2244
+ expected = self.session.expected_git_branch
2245
+ if expected and current and current != expected:
2246
+ raise ToolError(f"refusing git commit because branch changed from {expected} to {current}")
2233
2247
  if current in {"main", "master"} and self.session.settings.yolo:
2234
2248
  raise ToolError(f"refusing git commit on {current} in yolo mode; explicit confirmation is required")
2235
2249
 
@@ -2425,6 +2439,118 @@ class NoteTool(Tool):
2425
2439
  return ["\n".join(lines) or "{}"]
2426
2440
 
2427
2441
 
2442
+ @dataclass(frozen=True)
2443
+ class QuestionSpec:
2444
+ """One validated question the model wants to ask the user."""
2445
+
2446
+ question: str
2447
+ choices: list[str] | None = None
2448
+ previews: list[str] | None = None
2449
+ recommended: int | None = None
2450
+
2451
+
2452
+ class QuestionTool(Tool):
2453
+ NAME = "Question"
2454
+ 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."
2455
+ SIGNATURE = 'Question(questions=[{question, choices?, previews?, recommended?}, ...])'
2456
+ EXAMPLE = (
2457
+ 'One question, recommending a choice. Example: {"questions":[{"question":"Which approach?","choices":["Refactor","Rewrite"],"previews":["Extract module +87 -12","Rewrite from scratch"],"recommended":0}]}',
2458
+ 'Batch related questions. Example: {"questions":[{"question":"Target runtime?","choices":["Node","Deno"]},{"question":"Name the module?"}]}',
2459
+ )
2460
+ MUTATES = False
2461
+ STORES_RESULT = True
2462
+ question_fn: Callable[[QuestionSpec, str], str] | None = None
2463
+
2464
+ @classmethod
2465
+ def params_schema(cls) -> Json:
2466
+ return {
2467
+ "type": "object",
2468
+ "properties": {
2469
+ "questions": {
2470
+ "type": "array",
2471
+ "minItems": 1,
2472
+ "description": "Questions to ask, one after another",
2473
+ "items": {
2474
+ "type": "object",
2475
+ "properties": {
2476
+ "question": {"type": "string", "description": "The question to ask the user"},
2477
+ "choices": {
2478
+ "type": "array",
2479
+ "items": {"type": "string"},
2480
+ "description": "Optional predefined choices the user can pick from",
2481
+ },
2482
+ "previews": {
2483
+ "type": "array",
2484
+ "items": {"type": "string"},
2485
+ "description": "Optional preview text per choice, shown as the user navigates",
2486
+ },
2487
+ "recommended": {
2488
+ "type": "integer",
2489
+ "minimum": 0,
2490
+ "description": "Optional 0-based index of the recommended choice; pre-selected and marked",
2491
+ },
2492
+ },
2493
+ "required": ["question"],
2494
+ "additionalProperties": False,
2495
+ },
2496
+ },
2497
+ },
2498
+ "required": ["questions"],
2499
+ "additionalProperties": False,
2500
+ }
2501
+
2502
+ @classmethod
2503
+ def payload_args(cls, payload: Json) -> list[Any]:
2504
+ return [payload]
2505
+
2506
+ def call(self) -> str:
2507
+ if len(self.args) != 1 or not isinstance(self.args[0], dict):
2508
+ raise ToolError("Question requires named fields")
2509
+ questions = self.args[0].get("questions")
2510
+ if not isinstance(questions, list) or not questions:
2511
+ raise ToolError("Question requires a non-empty 'questions' list")
2512
+ # Validate the whole batch up front, so a malformed later question never strands the
2513
+ # user after they have already answered earlier ones.
2514
+ prepared: list[QuestionSpec] = []
2515
+ for item in questions:
2516
+ if not isinstance(item, dict):
2517
+ raise ToolError("each question must be an object with a 'question' field")
2518
+ question = str(item.get("question", "")).strip()
2519
+ if not question:
2520
+ raise ToolError("each question requires a 'question' field")
2521
+ choices = item.get("choices")
2522
+ previews = item.get("previews")
2523
+ recommended = item.get("recommended")
2524
+ if choices is not None:
2525
+ if not isinstance(choices, list) or not all(isinstance(c, str) for c in choices):
2526
+ raise ToolError("Question choices must be a list of strings")
2527
+ if previews is not None:
2528
+ if not isinstance(previews, list) or not all(isinstance(p, str) for p in previews):
2529
+ raise ToolError("Question previews must be a list of strings")
2530
+ if len(previews) != len(choices):
2531
+ raise ToolError("Question previews must match choices length")
2532
+ if recommended is not None and (
2533
+ isinstance(recommended, bool) or not isinstance(recommended, int) or not choices or not 0 <= recommended < len(choices)
2534
+ ):
2535
+ raise ToolError("Question recommended must be a valid 0-based choice index")
2536
+ prepared.append(QuestionSpec(question, choices, previews, recommended))
2537
+ total = len(prepared)
2538
+ answers: list[tuple[str, str]] = []
2539
+ for index, spec in enumerate(prepared):
2540
+ position = f"{index + 1}/{total}" if total > 1 else ""
2541
+ answers.append((spec.question, self.question_fn(spec, position) if self.question_fn else spec.question))
2542
+ if len(answers) == 1:
2543
+ return answers[0][1]
2544
+ return "\n\n".join(f"Q: {q}\nA: {a}" for q, a in answers)
2545
+
2546
+ def short_args(self) -> list[str]:
2547
+ questions = self.args[0].get("questions") if self.args and isinstance(self.args[0], dict) else None
2548
+ if not isinstance(questions, list) or not questions:
2549
+ return [""]
2550
+ first = str((questions[0] or {}).get("question", "") or "").strip() if isinstance(questions[0], dict) else ""
2551
+ label = Tool.compact(first, 80)
2552
+ return [label + (f" (+{len(questions) - 1} more)" if len(questions) > 1 else "")]
2553
+
2428
2554
  class MCPTool(Tool):
2429
2555
  NAME = "MCP"
2430
2556
  DESCRIPTION = "Call or describe external MCP server tools"
@@ -2525,6 +2651,7 @@ TOOLS: tuple[type[Tool], ...] = (
2525
2651
  GitTool,
2526
2652
  RecallTool,
2527
2653
  NoteTool,
2654
+ QuestionTool,
2528
2655
  )
2529
2656
  TOOL_REGISTRY: dict[str, type[Tool]] = {tool.NAME: tool for tool in TOOLS}
2530
2657
 
@@ -3967,6 +4094,7 @@ class ToolRunner:
3967
4094
  self.preview_full_fn: Callable[[str], None] | None = None
3968
4095
  self.live_output: Callable[[str, str], None] | None = None
3969
4096
  self.live_start: Callable[[], None] | None = None
4097
+ self.question_fn: Callable[[QuestionSpec, str], str] | None = None
3970
4098
 
3971
4099
  def run(self, calls: list[ToolCall], batch_suffix: str = "") -> list[Json]:
3972
4100
  messages = []
@@ -4026,6 +4154,8 @@ class ToolRunner:
4026
4154
  if isinstance(tool, BashTool):
4027
4155
  tool.live_output = self.live_output
4028
4156
  started, approved, display = time.monotonic(), False, None
4157
+ if isinstance(tool, QuestionTool):
4158
+ tool.question_fn = self.question_fn
4029
4159
  try:
4030
4160
  display = self.short_call(call, tool.short_args())
4031
4161
  if plan_error:
@@ -4633,48 +4763,26 @@ class Agent:
4633
4763
  SYSTEM_PROMPT = """\
4634
4764
  You are nanocode, a concise terminal coding agent.
4635
4765
 
4636
- TOOLS: Read LineCount List Find InspectCode Search Edit Bash Git Recall Note MCP.
4637
- Use EXACT tool names and named parameters. Obey each tool DESCRIPTION/SIGNATURE.
4766
+ TOOLS:
4767
+ - Available: Read LineCount List Find InspectCode Search Edit Bash Git Recall Note Question MCP.
4768
+ - Use exact tool names and named parameters; obey each tool DESCRIPTION/SIGNATURE.
4769
+ - Files/code: Read/LineCount/List inspect files; Find/Search locate paths/text; InspectCode navigates symbols when available.
4770
+ - Changes/commands: Edit writes files; Git handles git; Bash is fallback when built-ins do not fit.
4771
+ - State/external: Recall retrieves tr.N outputs; Note maintains goal/plan/known/check; MCP calls configured external tools.
4772
+ - Restraint: Before calling "Question", make progress with other tools first; only ask when genuinely blocked, and batch related questions into one call.
4638
4773
 
4639
4774
  FLOW:
4640
- - ACT when clear; keep using tools until done.
4641
- - Each turn must call tools or return final; never emit empty content.
4642
- - Do not repeat a failed tool call unchanged unless new information makes retrying meaningful.
4643
- - Prefer built-ins over Bash. Batch independent read-only calls.
4644
- - Do not switch/create/delete git branches unless the user explicitly asks.
4645
- - Before committing, check the branch; stop if it changed since task start.
4775
+ - Act when clear; keep using tools until done, or return a final answer.
4776
+ - Batch tool calls when practical.
4777
+ - Use tool feedback; do not repeat failed calls unchanged.
4778
+ - Do not switch/create/delete git branches unless explicitly asked. Before committing, check the branch and stop if it changed since task start.
4779
+ - Keep changes small/local/reversible and never overwrite unrelated user work.
4646
4780
  - All assistant text is user-visible markdown in the latest user's language.
4647
4781
 
4648
- TOOL CHOICE:
4649
- - Edit writes files. Use Edit for file changes; keep patches small.
4650
- - Read reads known files/ranges and returns line:hash anchors.
4651
- - Search finds text/patterns in files; Find finds paths.
4652
- - InspectCode navigates code symbols: defs, refs, impls, callers/callees, outline.
4653
- - If code_index is unavailable, use Search/Read.
4654
-
4655
- CONTEXT:
4656
- - Recall bounded tr.N only when needed; prefer FILE STATE over old outputs.
4657
- - For multi-step work, call Note early; use set_goal plus replace_plan/append_known/replace_known arrays, even for one item; record verification with set_check.
4658
-
4659
4782
  FILE STATE:
4660
- - FILE STATE is the current snapshot for listed ranges; Read and Edit refresh it automatically.
4661
- - Use FILE STATE as your working view for visible file content and Edit anchors.
4662
- - FILE STATE may be partial; Read when needed lines, hashes, or surrounding context are absent.
4663
- - FILE STATE is not Memory/Recall; do not call it "full memory" or treat it as old output.
4664
- - Do not re-Read a file/range already present in FILE STATE when it has the needed lines and anchors; proceed to Edit.
4665
- - After a successful Edit, trust FILE STATE and do not re-Read just to verify the edited range.
4666
-
4667
- EDITS:
4668
- - Inspect/read before edits.
4669
- - Patch with Edit line:hash anchors from the newest FILE STATE.
4670
- - After stale-anchor errors or successful edits, discard old anchors for that file/range.
4671
- - If a stale-anchor error includes `current is line:hash|text`, retry Edit with that current anchor; do not Read first.
4672
- - Read after a stale-anchor error only when there is no usable `current is` anchor or surrounding lines are needed.
4673
- - If `edit produced no changes` includes current target ranges, compare them with your intended change: stop if the file is already correct, otherwise retry with corrected content.
4674
- - Read after `edit produced no changes` only when the error lacks enough current target content or surrounding lines are needed.
4675
- - Do not batch multiple Edit calls for the same file; sequence them.
4676
- - Keep edits small/local/reversible; never overwrite unrelated user work.
4677
- - Edit op=create creates files, including empty files.
4783
+ - FILE STATE is the latest known file snapshot, possibly partial.
4784
+ - Read only when needed lines, anchors, or surrounding context are absent.
4785
+ - Read and Edit refresh FILE STATE; after Edit, trust the edited range.
4678
4786
 
4679
4787
  FINAL:
4680
4788
  - Concise markdown in the user's language.
@@ -5370,7 +5478,7 @@ Mentions:
5370
5478
  CLI:
5371
5479
  --mcp "orion*,!orionEval" Select MCP servers by name glob; use all or none.
5372
5480
  Tools:
5373
- Read, LineCount, List, Find, InspectCode, Search, Edit, Bash, Git, Recall, Note, MCP.
5481
+ Read, LineCount, List, Find, InspectCode, Search, Edit, Bash, Git, Recall, Note, Question, MCP.
5374
5482
  """
5375
5483
 
5376
5484
  def __init__(self, agent: Agent, input_fn=input, output_fn=print):
@@ -5409,6 +5517,7 @@ Tools:
5409
5517
  self.agent.tools.preview_full_fn = lambda text: setattr(self, "approval_full_preview", text)
5410
5518
  self.agent.tools.live_start = self.tool_live_start
5411
5519
  self.agent.tools.live_output = self.tool_live_output
5520
+ self.agent.tools.question_fn = self.question_interaction
5412
5521
 
5413
5522
  @staticmethod
5414
5523
  def exit_app(app: Application) -> None:
@@ -5593,6 +5702,7 @@ Tools:
5593
5702
  "choice.title": "ansicyan bold",
5594
5703
  "choice.selected": "reverse",
5595
5704
  "choice.disabled": "ansibrightblack",
5705
+ "choice.preview": "ansigreen italic",
5596
5706
  "completion-menu": "noreverse bg:default",
5597
5707
  "completion-menu.completion": "noreverse bg:default fg:ansiwhite",
5598
5708
  "completion-menu.completion.current": "noreverse bg:default fg:ansicyan bold",
@@ -5998,7 +6108,14 @@ Tools:
5998
6108
  labels: dict[str, str],
5999
6109
  current: str,
6000
6110
  disabled: set[str],
6111
+ *,
6112
+ preview_fn: Callable[[str], str] | None = None,
6113
+ free_text: bool = False,
6001
6114
  ) -> str | object | None:
6115
+ FREE_TEXT = "\x00free_text"
6116
+ if free_text and self.interactive_input:
6117
+ choices = (*choices, FREE_TEXT)
6118
+ labels = {**labels, FREE_TEXT: "Type freely..."}
6002
6119
  state = {"query": "", "selected": 0, "search": False}
6003
6120
  searching = Condition(lambda: bool(state["search"]))
6004
6121
 
@@ -6042,6 +6159,13 @@ Tools:
6042
6159
  if selected:
6043
6160
  parts.append(("[SetCursorPosition]", ""))
6044
6161
  parts.append((style, ("> " if selected else " ") + f"{number:2d}. {label}\n"))
6162
+ if preview_fn and options:
6163
+ sel = int(state["selected"])
6164
+ preview_text = preview_fn(options[sel]) if 0 <= sel < len(options) else ""
6165
+ if preview_text:
6166
+ parts.append(("class:choice.disabled", " ──────────────────────────────────\n"))
6167
+ for line in preview_text.splitlines():
6168
+ parts.append(("class:choice.preview", " │ " + line + "\n"))
6045
6169
  if state["search"]:
6046
6170
  parts.append(("", "/" + query))
6047
6171
  return parts
@@ -6093,7 +6217,8 @@ Tools:
6093
6217
  return
6094
6218
  options = enabled()
6095
6219
  if options:
6096
- event.app.exit(result=options[int(state["selected"])])
6220
+ choice = options[int(state["selected"])]
6221
+ event.app.exit(result=SELECTION_FREE_TEXT if choice == FREE_TEXT else choice)
6097
6222
 
6098
6223
  @bindings.add("c-c", eager=True)
6099
6224
  @bindings.add("<sigint>", eager=True)
@@ -6128,6 +6253,47 @@ Tools:
6128
6253
  app = self._make_app(Layout(HSplit([choice_window, self.status_window()]), focused_element=choice_window), bindings)
6129
6254
  return self.run_input_app(app)
6130
6255
 
6256
+ def question_application(self, spec: QuestionSpec, position: str = "") -> str:
6257
+ """Ask via the shared choice selector, with dynamic previews and a free-text fallback."""
6258
+ choices = spec.choices
6259
+ # Prefix the position (e.g. "(1/3) ...") into the question text so it renders as plain
6260
+ # markdown — no separate styled line, hence no ANSI escapes to mangle.
6261
+ prompt = f"({position}) {spec.question}" if position else spec.question
6262
+ if not choices or not self.interactive_input:
6263
+ return self.read_input(prompt)
6264
+
6265
+ if self.ui.color:
6266
+ self.ui.console.print(Markdown(prompt))
6267
+ else:
6268
+ self.emit(prompt + "\n")
6269
+
6270
+ # An optional recommended choice is pre-selected (via current) and marked (via labels),
6271
+ # reusing the selector's existing machinery.
6272
+ labels, current = {}, ""
6273
+ if spec.recommended is not None and 0 <= spec.recommended < len(choices):
6274
+ current = choices[spec.recommended]
6275
+ labels = {current: current + " (recommended)"}
6276
+ previews = spec.previews
6277
+ preview_map = {c: previews[i] for i, c in enumerate(choices) if previews and i < len(previews) and previews[i]}
6278
+ result = self.choice_application(
6279
+ "Select:", tuple(choices), labels, current, set(),
6280
+ preview_fn=lambda choice: preview_map.get(choice, ""),
6281
+ free_text=True,
6282
+ )
6283
+ if result is SELECTION_FREE_TEXT:
6284
+ return self.read_input(spec.question + " (type freely)")
6285
+ if isinstance(result, str):
6286
+ return result
6287
+ return DISMISSED # SELECTION_BACK (Esc) — user declined to answer
6288
+
6289
+ def question_interaction(self, spec: QuestionSpec, position: str = "") -> str:
6290
+ """Entry point for Question tool — shows the chosen answer in CLI after selection."""
6291
+ result = self.question_application(spec, position)
6292
+ # Echo the picked choice (free-text/dismissal are already surfaced elsewhere).
6293
+ if spec.choices and result in spec.choices:
6294
+ self.emit(result + "\n")
6295
+ return result
6296
+
6131
6297
  def select_model(self, choices: tuple[str, ...]) -> str | object | None:
6132
6298
  current = self.session.config.provider.model
6133
6299
  labels = {label: label for label in self.MODEL_LABELS if label in choices}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nanocode-cli
3
- Version: 0.6.1
3
+ Version: 0.6.3
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
@@ -121,6 +121,7 @@ Interactive selectors support `j`/`k`, arrows, `/` search, Enter, and Esc. Input
121
121
  - Shell: `Bash`, `Git`.
122
122
  - Tool results: `Recall`.
123
123
  - Working notes: `Note`.
124
+ - Ask the user: `Question` asks one or more questions (optional choices, previews, recommended) when intent is genuinely ambiguous.
124
125
  - MCP: `MCP` calls tools on configured MCP servers.
125
126
 
126
127
  `Read`, `Search`, and `InspectCode` return line anchors where useful. `Edit` uses current `line:hash` anchors to reject stale edits.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nanocode-cli"
7
- version = "0.6.1"
7
+ version = "0.6.3"
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