@tiens.nguyen/gu-cli 1.0.748 → 1.0.750

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.
@@ -3813,6 +3813,12 @@ def _normalize_code_tags(text: str) -> str:
3813
3813
  <code> tag is present, so we don't touch backticks inside a real <code> block).
3814
3814
  5. opened-but-never-closed (exactly one "<code>", no "</code>"): append "</code>"
3815
3815
  so the parser can still extract the code instead of failing the whole step.
3816
+ 6. NO DELIMITERS AT ALL, but the reply is Python that calls something — an optional
3817
+ "Thought:" line, then e.g. `print(read_file_lines(...))`. Measured on Claude: 13
3818
+ such replies in one turn, each thrown away as "no runnable code block" and re-asked,
3819
+ about a quarter of the turn's steps. Wrapped only when the code (after the Thought
3820
+ line) COMPILES and contains a call — prose does not parse as Python, so a sentence
3821
+ is never mistaken for code.
3816
3822
  Idempotent — already-correct "<code>…</code>" is returned unchanged."""
3817
3823
  if not text:
3818
3824
  return text
@@ -3823,9 +3829,36 @@ def _normalize_code_tags(text: str) -> str:
3823
3829
  fixed = _MD_FENCE_BLOCK.sub(lambda m: f"<code>\n{m.group(1)}</code>", fixed, count=1)
3824
3830
  if fixed.count("<code>") == 1 and "</code>" not in fixed:
3825
3831
  fixed = fixed + "\n</code>"
3832
+ if "<code>" not in fixed and "</code>" not in fixed:
3833
+ bare = _bare_code_block(fixed)
3834
+ if bare is not None:
3835
+ thought, code = bare
3836
+ fixed = (thought + "\n" if thought else "") + "<code>\n" + code + "\n</code>"
3826
3837
  return fixed
3827
3838
 
3828
3839
 
3840
+ def _bare_code_block(text: str):
3841
+ """(thought, code) when `text` is Python with no delimiters — an optional leading
3842
+ "Thought: …" line, then code that compiles and calls something. Else None."""
3843
+ import ast
3844
+ t = (text or "").strip()
3845
+ if not t:
3846
+ return None
3847
+ thought = ""
3848
+ lines = t.splitlines()
3849
+ if lines and lines[0].lstrip().lower().startswith("thought:"):
3850
+ thought, t = lines[0].strip(), "\n".join(lines[1:]).strip()
3851
+ if not t:
3852
+ return None
3853
+ try:
3854
+ tree = ast.parse(t)
3855
+ except SyntaxError:
3856
+ return None
3857
+ if not any(isinstance(n, ast.Call) for n in ast.walk(tree)):
3858
+ return None
3859
+ return thought, t
3860
+
3861
+
3829
3862
  def _escape_raw_newlines_in_strings(code: str, defuse_tags: bool = False) -> str:
3830
3863
  """Turn RAW newlines/tabs that sit INSIDE a single/double-quoted string literal into
3831
3864
  \\n/\\t escapes. Triple-quoted strings, comments, and everything outside a string are
@@ -8020,13 +8053,19 @@ def run_agent_chat(cfg):
8020
8053
  # ORDER to use them in and the CONSTRAINTS that bite — so keep those, drop the third
8021
8054
  # retelling. Every rule below is a scar: token waste (#63/#79), no-shell/no-sudo,
8022
8055
  # key-only deploys (#57).
8023
- " THE EDIT LOOP: grep_repo to LOCATE → read_file_lines around the hit (a SMALL "
8024
- "range; never re-read the whole file or the same range twice — large reads are "
8025
- "truncated and slow every later step) → edit_lines to change it → run_command to "
8026
- "VERIFY. edit_file only for a byte-exact unique string; create_file(overwrite=True) "
8056
+ # Task #48 (GitHub): string edits are the default, like Claude Code's Edit tool. A
8057
+ # line-number edit needs a fresh read before every edit (each one shifts the numbers):
8058
+ # a measured turn spent 12 of its 43 steps on read → edit_lines → read → edit_lines.
8059
+ " THE EDIT LOOP: LOCATE (explore, grep_repo) → read_file_lines ONCE around what you will "
8060
+ "change (a SMALL range; never re-read the whole file or the same range twice) → make ALL "
8061
+ "of that file's edits in ONE block with edit_file(path, old_string, new_string), quoting "
8062
+ "a short UNIQUE snippet from what you read — no line numbers, so no re-read between "
8063
+ "edits → VERIFY with run_command (in the same block when you can). edit_lines only when "
8064
+ "you cannot quote a unique string (e.g. replacing a long range); create_file(overwrite=True) "
8027
8065
  "replaces a whole file in ONE call (no read+edit just to rewrite it).\n"
8028
- " EXPLORE FIRST — before CHANGING code you have not seen yet in this conversation, and "
8029
- "for any broad question (where is X handled, how does Y work, what would a change to Z "
8066
+ " EXPLORE FIRST — before CHANGING code you have not seen yet in this conversation "
8067
+ "(always for a change that spans several files or layers — frontend + backend, UI + API), "
8068
+ "and for any broad question (where is X handled, how does Y work, what would a change to Z "
8030
8069
  "touch): call explore(question) ONCE to map the files and lines involved. It searches in "
8031
8070
  "a SEPARATE context and returns a short summary with path:line references, so its greps "
8032
8071
  "and reads do not pile up here and are not re-sent on every later step. Then read only "
@@ -8071,12 +8110,11 @@ def run_agent_chat(cfg):
8071
8110
  "- user asks to FIX A BUG / MODIFY / ADD CODE in a workspace -> "
8072
8111
  "grep_repo(pattern) when you know the exact string, or rag_search_workspace(query) "
8073
8112
  "when you do not, to FIND the file, THEN "
8074
- "read_file_lines(file, around the hit) to see exact lines, THEN "
8075
- "edit_lines(file, start, end, new_content) to change them, THEN "
8113
+ "read_file_lines(file, around the hit) ONCE, THEN edit_file(file, old_string, new_string) "
8114
+ "for EACH change to that file — all in ONE block —, THEN "
8076
8115
  "run_command('…test…') to VERIFY if run permission exists — read failures and fix, "
8077
8116
  "iterate. Finish with final_answer summarizing WHAT you changed (files + lines). "
8078
- "AFTER editing, re-read with grep_repo/read_file_lines (an indexed knowledge base may be "
8079
- "stale — do not trust rag_search for freshly edited code).\n"
8117
+ "Do not trust rag_search for freshly edited code (the index may be stale).\n"
8080
8118
  ) if _WS_AVAILABLE else ""
8081
8119
  _email_choose_line = (
8082
8120
  "- 'email …' / 'send an email to …' -> send_email(to, subject, body) "
@@ -8261,7 +8299,8 @@ def run_agent_chat(cfg):
8261
8299
  + ("Remember: FINISH what was asked yourself — fix blockers, VERIFY the outcome, then "
8262
8300
  "final_answer with evidence; never hand the task back. If it only asks to RECORD work "
8263
8301
  "(create a task, a note, a plan), recording it IS the task — do not start the work. "
8264
- "To change code you have not seen yet, map it with ONE explore() call first.\n"
8302
+ "To change code you have not seen yet — especially across several files — map it with "
8303
+ "ONE explore() call first, then make each file's edits in ONE block with edit_file.\n"
8265
8304
  if _WS_AVAILABLE else "")
8266
8305
  + "\nTASK (answer THIS, choose the tool that fits it):\n"
8267
8306
  f"{task_text}\n"
@@ -10304,7 +10343,11 @@ def run_agent_chat(cfg):
10304
10343
  r"send_email)\s*\("
10305
10344
  )
10306
10345
  fence_is_toolcall = any(re.search(_tool_call_re, b) for b in fence_bodies)
10307
- if has_code_tag or fence_is_toolcall:
10346
+ # Bare Python with no delimiters at all (case 6 of _normalize_code_tags) —
10347
+ # it used to fall through to the prose branch and cost a whole step.
10348
+ bare_code = (not has_code_tag and not fence_bodies
10349
+ and _bare_code_block(content) is not None)
10350
+ if has_code_tag or fence_is_toolcall or bare_code:
10308
10351
  normalized = _normalize_code_tags(content)
10309
10352
  if normalized != content:
10310
10353
  _log("normalized code-block tags (<code >/fence → <code>)")
@@ -10456,13 +10499,16 @@ def run_agent_chat(cfg):
10456
10499
  # from the prompt, not writing code. Deliberately carries NO literal delimiter (the
10457
10500
  # system prompt already shows the format with a worked example), so there is nothing
10458
10501
  # to echo. Applied only when kind=openai; Ollama keeps the stock message.
10502
+ # STRONG MODELS (2026-09-25): the delimiters are named again. Leaving them out was a
10503
+ # workaround for Kimi echoing them; on Claude it did the opposite — told to use "the
10504
+ # delimiters shown in the system prompt", it replied with bare code, which failed the
10505
+ # same way, 13 times in one turn. And "call exactly one tool" contradicted batching.
10459
10506
  _PARSE_ERROR_REPLACEMENT = (
10460
10507
  "Error: your last reply contained no runnable code block, so nothing executed "
10461
10508
  "and no observation was produced.\n"
10462
- "Reply with ONE 'Thought:' line, then the code block using the exact opening and "
10463
- "closing delimiters shown in the system prompt, with ONLY Python between them — "
10464
- "no prose inside the block, and do not repeat these instructions back to me. "
10465
- "Call exactly one tool, or call final_answer(...) if you are done."
10509
+ "Reply with ONE 'Thought:' line, then your Python between <code> and </code> — "
10510
+ "nothing else inside the block. Batch independent calls as usual, or call "
10511
+ "final_answer(...) if you are done."
10466
10512
  )
10467
10513
 
10468
10514
  def _sanitize_parse_errors(self, msgs):
@@ -11601,7 +11647,7 @@ def run_agent_chat(cfg):
11601
11647
 
11602
11648
  @tool
11603
11649
  def edit_lines(path: str, start_line: int, end_line: int, new_content: str) -> str:
11604
- """Replace lines start_line..end_line (inclusive, 1-based) of a workspace file with new_content. The PRIMARY edit tool — get exact line numbers from grep_repo/read_file_lines first. A backup is saved automatically.
11650
+ """Replace lines start_line..end_line (inclusive, 1-based) of a workspace file with new_content. Use it only when you cannot quote a unique string for edit_file (e.g. replacing a long range). Line numbers shift after every edit, so re-read before another edit_lines in the same file. A backup is saved automatically.
11605
11651
 
11606
11652
  Args:
11607
11653
  path: file inside a registered workspace.
@@ -11658,7 +11704,7 @@ def run_agent_chat(cfg):
11658
11704
 
11659
11705
  @tool
11660
11706
  def edit_file(path: str, old_string: str, new_string: str) -> str:
11661
- """Replace an EXACT unique text occurrence in a workspace file (must match byte-for-byte incl. whitespace). Prefer edit_lines when you have line numbers. A backup is saved automatically.
11707
+ """The PRIMARY edit tool: replace an EXACT, unique text occurrence in a workspace file (byte-for-byte, incl. whitespace). It needs no line numbers, so several edit_file calls to one file go in ONE block after a single read. A backup is saved automatically.
11662
11708
 
11663
11709
  Args:
11664
11710
  path: file inside a registered workspace.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.748",
3
+ "version": "1.0.750",
4
4
  "description": "Gu CLI — the gu terminal plus the local worker that runs agent / OCR / PDF / embedding jobs on your Mac (Ollama / MLX / OpenAI-compatible).",
5
5
  "type": "module",
6
6
  "license": "MIT",