Skip to content
22 changes: 3 additions & 19 deletions cecli/coders/agent_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from datetime import datetime
from pathlib import Path

from cecli import utils

Check failure on line 15 in cecli/coders/agent_coder.py

View workflow job for this annotation

GitHub Actions / pre-commit

F401 'cecli.utils' imported but unused
from cecli.change_tracker import ChangeTracker
from cecli.helpers import nested, responses
from cecli.helpers.agents.service import AgentService
Expand Down Expand Up @@ -727,25 +727,9 @@
continue

if args_string:
json_chunks = utils.split_concatenated_json(args_string)
for chunk in json_chunks:
try:
parsed_args_list.append(json.loads(chunk))
except json.JSONDecodeError as e:
self.model_kwargs = {}
self.io.tool_warning(
f"Malformed JSON arguments in tool {tool_name}: {chunk}"
)
tool_responses.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": (
f"Malformed JSON arguments in tool {tool_name}: {str(e)}"
),
}
)
continue
from cecli.tools.utils.helpers import parse_tool_arguments

parsed_args_list = [parse_tool_arguments(args_string)]
if not parsed_args_list and not args_string:
parsed_args_list.append({})
all_results_content = []
Expand Down
11 changes: 10 additions & 1 deletion cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2645,7 +2645,16 @@ def _expand_concatenated_json(self, tool_calls):
expanded_tool_calls.append(tool_call)
continue

# We have concatenated JSON, so expand it into multiple tool calls.
from cecli.tools.utils.helpers import merge_glued_json_objects

merged = merge_glued_json_objects(json_chunks)
if merged is not None:
new_tool_call = copy_tool_call(tool_call)
new_tool_call.function.arguments = json.dumps(merged)
expanded_tool_calls.append(new_tool_call)
continue

# Non-object glued JSON: expand into multiple tool calls (legacy).
for i, chunk in enumerate(json_chunks):
if not chunk.strip():
continue
Expand Down
26 changes: 21 additions & 5 deletions cecli/repomap.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,12 @@ def get_repo_map(
"has_chat_files": bool(chat_files),
}

def _resolve_abs_fname(self, fname: str) -> str:
"""Normalize repo file paths for existence checks and tag parsing."""
if os.path.isabs(fname):
return os.path.normpath(fname)
return os.path.normpath(os.path.join(self.root, fname))

def get_rel_fname(self, fname):
try:
return os.path.relpath(fname, self.root)
Expand Down Expand Up @@ -746,6 +752,7 @@ def get_ranked_tags(

num_fnames = len(fnames)
fname_index = 0
skipped_missing = 0
for fname in fnames:
if self.verbose:
self.io.tool_output(f"Processing {fname}")
Expand All @@ -756,20 +763,24 @@ def get_ranked_tags(
else:
self.io.update_spinner(f"{UPDATING_REPO_MAP_MESSAGE}: {fname}")

abs_fname = self._resolve_abs_fname(fname)
try:
file_ok = os.path.isfile(fname)
file_ok = os.path.isfile(abs_fname)
except OSError:
file_ok = False

if not file_ok:
skipped_missing += 1
if fname not in self.warned_files:
self.io.tool_warning(f"Repo-map can't include {fname}")
self.io.tool_output(
"Has it been deleted from the file system but not from git?"
)
self.warned_files.add(fname)
if skipped_missing <= 2:
self.io.tool_warning(
f"Repo-map skipping missing file: {abs_fname}"
" (removed on disk or not yet written)."
)
continue

fname = abs_fname
# dump(fname)
rel_fname = self.get_rel_fname(fname)
current_pers = 0.0 # Start with 0 personalization score
Expand Down Expand Up @@ -843,6 +854,11 @@ def get_ranked_tags(
if tag.specific_kind == "import":
file_imports[rel_fname].add(tag.name)

if skipped_missing > 2:
self.io.tool_output(
f"Repo-map skipped {skipped_missing} paths that are not readable on disk."
)

self.io.profile("Process Files")

if self.use_enhanced_map and len(file_imports) > 0:
Expand Down
8 changes: 5 additions & 3 deletions cecli/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def list_sessions(self) -> List[Dict]:

return sessions

async def load_session(self, session_identifier: str, switch=True) -> bool:
async def load_session(self, session_identifier: str, switch=True, quiet: bool = False) -> bool:
"""Load a saved session by name or file path."""
if not session_identifier:
self.io.tool_error("Please provide a session name or file path.")
Expand All @@ -103,12 +103,14 @@ async def load_session(self, session_identifier: str, switch=True) -> bool:
with open(session_file, "r", encoding="utf-8") as f:
session_data = json.load(f)
except Exception as e:
self.io.tool_error(f"Error loading session: {e}")
if not quiet:
self.io.tool_error(f"Error loading session: {e}")
return False

# Verify session format
if not isinstance(session_data, dict) or "version" not in session_data:
self.io.tool_error("Invalid session format.")
if not quiet:
self.io.tool_error("Invalid session format.")
return False

# Apply session data
Expand Down
37 changes: 27 additions & 10 deletions cecli/tools/grep.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import json

Check failure on line 1 in cecli/tools/grep.py

View workflow job for this annotation

GitHub Actions / pre-commit

F401 'json' imported but unused
import shutil
from pathlib import Path

Expand All @@ -7,6 +7,12 @@
from cecli.helpers.hashline import strip_hashline
from cecli.run_cmd import run_cmd_subprocess
from cecli.tools.utils.base_tool import BaseTool
from cecli.tools.utils.helpers import (
ToolError,
grep_error_hint,
normalize_search_operations,
parse_tool_arguments,
)
from cecli.tools.utils.output import color_markers, tool_footer, tool_header


Expand Down Expand Up @@ -93,9 +99,11 @@
Search for lines matching patterns in files within the project repository.
Uses rg (ripgrep), ag (the silver searcher), or grep, whichever is available.
"""
if not isinstance(searches, list):
# Handle legacy single-search call if necessary, or just error
return "Error: 'searches' parameter must be an array."
try:
searches = normalize_search_operations(searches)
except ToolError as err:
coder.io.tool_error(str(err))
return f"Error: {err}"

repo = coder.repo
if not repo:
Expand All @@ -109,7 +117,7 @@

all_results = []
for search_op in searches:
pattern = strip_hashline(search_op.get("pattern"))
pattern = strip_hashline(str(search_op.get("pattern") or ""))
file_pattern = search_op.get("file_pattern", "*")
directory = search_op.get("directory", search_op.get("path", "."))
use_regex = search_op.get("use_regex", False)
Expand Down Expand Up @@ -199,7 +207,11 @@
elif exit_status == 1:
all_results.append(f"No matches found for '{pattern}'.")
else:
all_results.append(f"Error searching for '{pattern}': {output_content}")
msg = f"Error searching for '{pattern}': {output_content}"
hint = grep_error_hint(pattern, output_content)
if hint:
msg = f"{msg.rstrip()}{hint}"
all_results.append(msg)

except Exception as e:
all_results.append(f"Error executing search for '{pattern}': {str(e)}")
Expand Down Expand Up @@ -235,20 +247,25 @@
"""Format output for Grep tool."""
color_start, color_end = color_markers(coder)

try:
params = json.loads(tool_response.function.arguments)
except json.JSONDecodeError:
params = parse_tool_arguments(tool_response.function.arguments or "")
if not params and (tool_response.function.arguments or "").strip():
coder.io.tool_error("Invalid Tool JSON")
return

tool_header(coder=coder, mcp_server=mcp_server, tool_response=tool_response)

# Output each search operation with the requested format
searches = params.get("searches", [])
try:
searches = normalize_search_operations(params.get("searches", []))
except ToolError as err:
coder.io.tool_error(str(err))
tool_footer(coder=coder, tool_response=tool_response)
return

if searches:
coder.io.tool_output("")
for i, search_op in enumerate(searches):
pattern = search_op.get("pattern", "")
pattern = str(search_op.get("pattern") or "")
file_pattern = search_op.get("file_pattern", "*")
directory = search_op.get("directory", ".")
use_regex = search_op.get("use_regex", False)
Expand Down
44 changes: 34 additions & 10 deletions cecli/tools/read_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,23 @@
from cecli.tools.utils.base_tool import BaseTool
from cecli.tools.utils.helpers import (
ToolError,
coerce_dict_item,
handle_tool_error,
is_provided,
normalize_json_array,
resolve_paths,
)
from cecli.tools.utils.output import color_markers, tool_footer, tool_header


def normalize_show_ops(show) -> List[dict]:
"""Accept show as list, dict, JSON string, or list of JSON strings (LLM quirk)."""
return [
coerce_dict_item(op, param_name="show operation")
for op in normalize_json_array(show, param_name="show")
]


class Tool(BaseTool):
NORM_NAME = "readrange"
TRACK_INVOCATIONS = False
Expand Down Expand Up @@ -89,12 +99,8 @@ def execute(cls, coder, show, **kwargs):
error_outputs = []

try:
# 1. Validate show parameter
if not isinstance(show, list):
show = [show] if isinstance(show, dict) else show

if len(show) == 0:
raise ToolError("show array cannot be empty")
# 1. Validate show parameter (models sometimes double-encode show as JSON text)
show = normalize_show_ops(show)

all_outputs = []
already_up_to_details = []
Expand Down Expand Up @@ -324,12 +330,19 @@ def execute(cls, coder, show, **kwargs):
# first, falling back to 20 equally-spaced lines for non-code files
if (start_text == "@000" or end_text == "000@") and (e_idx - s_idx > 200):
preview = cls._get_range_preview(
abs_path, coder.io, start_idx=s_idx, end_idx=e_idx, line_numbers=True
abs_path,
coder.io,
start_idx=s_idx,
end_idx=e_idx,
line_numbers=True,
)
if show_index > 0:
all_outputs.append("")
all_outputs.append(preview)
cls._last_invocation[abs_path] = {"start_idx": s_idx, "end_idx": e_idx}
cls._last_invocation[abs_path] = {
"start_idx": s_idx,
"end_idx": e_idx,
}
continue

# Store the found indices for future disambiguation
Expand Down Expand Up @@ -579,7 +592,14 @@ def format_output(cls, coder, mcp_server, tool_response):

tool_header(coder=coder, mcp_server=mcp_server, tool_response=tool_response)

show_ops = params.get("show", [])
raw_show = params.get("show", [])
try:
show_ops = normalize_show_ops(raw_show) if raw_show else []
except ToolError as err:
coder.io.tool_error(str(err))
tool_footer(coder=coder, tool_response=tool_response)
return

if show_ops:
coder.io.tool_output("")
for i, show_op in enumerate(show_ops):
Expand Down Expand Up @@ -648,7 +668,11 @@ def _get_range_preview(cls, abs_path, io, start_idx, end_idx, line_numbers=True)
from cecli.repomap import RepoMap

stub = RepoMap.get_file_stub(
abs_path, io, start_line=start_idx, end_line=end_idx, line_numbers=line_numbers
abs_path,
io,
start_line=start_idx,
end_line=end_idx,
line_numbers=line_numbers,
)

# If get_file_stub returned a useful structural outline, wrap it with headers
Expand Down
Loading
Loading