Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cecli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from packaging import version

__version__ = "0.96.6.dev"
__version__ = "0.96.7.dev"
safe_version = __version__

try:
Expand Down
1 change: 1 addition & 0 deletions cecli/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,7 @@ def get_parser(default_config_files, git_root):
"-c",
"--config",
is_config_file=True,
env_var="CECLI_CONFIG_FILE",
metavar="CONFIG_FILE",
help=(
"Specify the config file (default: search for .cecli.conf.yml in git root, cwd"
Expand Down
25 changes: 17 additions & 8 deletions cecli/commands/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from cecli.commands.utils.base_command import BaseCommand
from cecli.commands.utils.helpers import (
format_command_result,
get_file_completions,
parse_quoted_filenames,
quote_filename,
)
Expand Down Expand Up @@ -82,10 +83,6 @@ async def execute(cls, io, coder, args, **kwargs):
for matched_file in sorted(all_matched_files):
abs_file_path = coder.abs_root_path(matched_file)

if not abs_file_path.startswith(coder.root) and not is_image_file(matched_file):
io.tool_error(f"Can not add {abs_file_path}, which is not within {coder.root}")
continue

if (
coder.repo
and coder.repo.git_ignored_file(matched_file)
Expand Down Expand Up @@ -205,10 +202,22 @@ def expand_subdir(file_path: Path) -> List[Path]:
@classmethod
def get_completions(cls, io, coder, args) -> List[str]:
"""Get completion options for add command."""
files = set(coder.get_all_relative_files())
files = files - set(coder.get_inchat_relative_files())
files = [quote_filename(fn) for fn in files]
return files
# Get both directory-based completions and filtered "all" completions
directory_completions = get_file_completions(
coder,
args=args,
completion_type="directory",
include_directories=True,
filter_in_chat=False,
)

all_completions = get_file_completions(
coder, args=args, completion_type="all", include_directories=False, filter_in_chat=True
)

# Return the joint set (union) of both completion types
joint_set = set(directory_completions) | set(all_completions)
return sorted(joint_set)

@classmethod
def get_help(cls) -> str:
Expand Down
57 changes: 15 additions & 42 deletions cecli/commands/read_only.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from cecli.commands.utils.base_command import BaseCommand
from cecli.commands.utils.helpers import (
format_command_result,
get_file_completions,
parse_quoted_filenames,
quote_filename,
)
Expand Down Expand Up @@ -207,50 +208,22 @@ def _add_read_only_directory(
@classmethod
def get_completions(cls, io, coder, args) -> List[str]:
"""Get completion options for read-only command."""
from pathlib import Path

root = Path(coder.root) if hasattr(coder, "root") else Path.cwd()

# Handle the prefix - could be partial path like "src/ma" or just "ma"
if "/" in args:
# Has directory component
dir_part, file_part = args.rsplit("/", 1)
if dir_part == "":
search_dir = Path("/")
path_prefix = "/"
else:
# Use os.path.expanduser for ~ support if needed, but Path handles it mostly
search_dir = (root / dir_part).resolve()
path_prefix = dir_part + "/"
search_prefix = file_part.lower()
else:
search_dir = root
search_prefix = args.lower()
path_prefix = ""

completions = []
try:
if search_dir.exists() and search_dir.is_dir():
for entry in search_dir.iterdir():
name = entry.name
if search_prefix and not name.lower().startswith(search_prefix):
continue

# Add trailing slash for directories
if entry.is_dir():
completions.append(path_prefix + name + "/")
else:
completions.append(path_prefix + name)
except (PermissionError, OSError):
pass
# Get both directory-based completions and filtered "all" completions
directory_completions = get_file_completions(
coder,
args=args,
completion_type="directory",
include_directories=True,
filter_in_chat=False,
)

# Also include files already in the chat that match
add_completions = coder.commands.get_completions("/add")
for c in add_completions:
if args.lower() in str(c).lower() and str(c) not in completions:
completions.append(str(c))
all_completions = get_file_completions(
coder, args=args, completion_type="all", include_directories=False, filter_in_chat=True
)

return sorted(completions)
# Return the joint set (union) of both completion types
joint_set = set(directory_completions) | set(all_completions)
return sorted(joint_set)

@classmethod
def get_help(cls) -> str:
Expand Down
56 changes: 15 additions & 41 deletions cecli/commands/read_only_stub.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from cecli.commands.utils.base_command import BaseCommand
from cecli.commands.utils.helpers import (
format_command_result,
get_file_completions,
parse_quoted_filenames,
quote_filename,
)
Expand Down Expand Up @@ -207,49 +208,22 @@ def _add_read_only_directory(
@classmethod
def get_completions(cls, io, coder, args) -> List[str]:
"""Get completion options for read-only-stub command."""
from pathlib import Path

root = Path(coder.root) if hasattr(coder, "root") else Path.cwd()

# Handle the prefix - could be partial path like "src/ma" or just "ma"
if "/" in args:
# Has directory component
dir_part, file_part = args.rsplit("/", 1)
if dir_part == "":
search_dir = Path("/")
path_prefix = "/"
else:
search_dir = (root / dir_part).resolve()
path_prefix = dir_part + "/"
search_prefix = file_part.lower()
else:
search_dir = root
search_prefix = args.lower()
path_prefix = ""

completions = []
try:
if search_dir.exists() and search_dir.is_dir():
for entry in search_dir.iterdir():
name = entry.name
if search_prefix and not name.lower().startswith(search_prefix):
continue

# Add trailing slash for directories
if entry.is_dir():
completions.append(path_prefix + name + "/")
else:
completions.append(path_prefix + name)
except (PermissionError, OSError):
pass
# Get both directory-based completions and filtered "all" completions
directory_completions = get_file_completions(
coder,
args=args,
completion_type="directory",
include_directories=True,
filter_in_chat=False,
)

# Also include files already in the chat that match
add_completions = coder.commands.get_completions("/add")
for c in add_completions:
if args.lower() in str(c).lower() and str(c) not in completions:
completions.append(str(c))
all_completions = get_file_completions(
coder, args=args, completion_type="all", include_directories=False, filter_in_chat=True
)

return sorted(completions)
# Return the joint set (union) of both completion types
joint_set = set(directory_completions) | set(all_completions)
return sorted(joint_set)

@classmethod
def get_help(cls) -> str:
Expand Down
117 changes: 106 additions & 11 deletions cecli/commands/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,24 +46,116 @@ def glob_filtered_to_repo(pattern: str, root: str, repo) -> List[Path]:
else:
try:
raw_matched_files = list(Path(root).glob(pattern))
except (IndexError, AttributeError):
except (IndexError, AttributeError, ValueError):
# Handle patterns like "**/*.py" that might fail on empty dirs
raw_matched_files = []

# Filter out directories and ignored files
# Expand directories and filter
matched_files = []
for f in raw_matched_files:
if not f.is_file():
continue
if repo and repo.ignored_file(f):
continue
matched_files.append(f)
matched_files.extend(expand_subdir(f))

# Filter to repository files
matched_files = [fn.relative_to(root) for fn in matched_files if fn.is_relative_to(root)]

# if repo, filter against it
if repo:
git_files = repo.get_tracked_files()
matched_files = [fn for fn in matched_files if str(fn) in git_files]

return matched_files
except Exception as e:
raise CommandError(f"Error processing pattern '{pattern}': {e}")


def get_file_completions(
coder,
args: str = "",
completion_type: str = "all",
include_directories: bool = False,
filter_in_chat: bool = False,
) -> List[str]:
"""
Get file completions for command line arguments.

This function provides unified file completion logic that can be used by
multiple commands (add, read-only, read-only-stub, etc.).

Args:
coder: Coder instance
args: Command arguments to complete
completion_type: Type of completion to perform:
- "all": Return all available files (default)
- "glob": Treat args as glob pattern and expand
- "directory": Perform directory-based prefix matching
include_directories: Whether to include directories in results
filter_in_chat: Whether to filter out files already in chat

Returns:
List of completion strings (quoted if needed)
"""
from pathlib import Path

root = Path(coder.root) if hasattr(coder, "root") else Path.cwd()

if completion_type == "glob":
# Handle glob pattern completion
if not args.strip():
return []

try:
matched_files = glob_filtered_to_repo(args, str(root), coder.repo)
completions = [str(fn) for fn in matched_files]
except CommandError:
completions = []

elif completion_type == "directory":
# Handle directory-based prefix matching (like read-only commands)
if "/" in args:
# Has directory component
dir_part, file_part = args.rsplit("/", 1)
if dir_part == "":
search_dir = Path("/")
path_prefix = "/"
else:
search_dir = (root / dir_part).resolve()
path_prefix = dir_part + "/"
search_prefix = file_part.lower()
else:
search_dir = root
search_prefix = args.lower()
path_prefix = ""

completions = []
try:
if search_dir.exists() and search_dir.is_dir():
for entry in search_dir.iterdir():
name = entry.name
if search_prefix and not name.lower().startswith(search_prefix):
continue

# Add trailing slash for directories if requested
if entry.is_dir() and include_directories:
completions.append(path_prefix + name + "/")
elif entry.is_file():
completions.append(path_prefix + name)
except (PermissionError, OSError):
pass

else: # "all" completion type
# Get all available files
if filter_in_chat:
files = set(coder.get_all_relative_files())
files = files - set(coder.get_inchat_relative_files())
completions = list(files)
else:
completions = coder.get_all_relative_files()

# Quote filenames with spaces
completions = [quote_filename(fn) for fn in completions]
return sorted(completions)


def validate_file_access(io, coder, file_path: str, require_in_chat: bool = False) -> bool:
"""
Validate file access permissions and state.
Expand Down Expand Up @@ -130,13 +222,16 @@ def get_available_files(coder, in_chat: bool = False) -> List[str]:
return coder.get_all_relative_files()


def expand_subdir(file_path):
def expand_subdir(file_path: Path) -> List[Path]:
"""Expand a directory path to all files within it."""
if file_path.is_file():
yield file_path
return
return [file_path]

if file_path.is_dir():
files = []
for file in file_path.rglob("*"):
if file.is_file():
yield file
files.append(file)
return files

return []
4 changes: 3 additions & 1 deletion cecli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@ async def setup_git(git_root, io):
)
return
elif cwd and await io.confirm_ask(
"No git repo found, create one to track cecli's changes (recommended)?", acknowledge=True
"No git repo found, create one to track cecli's changes (recommended)?",
acknowledge=True,
explicit_yes_required=True,
):
git_root = str(cwd.resolve())
repo = await make_new_repo(git_root, io)
Expand Down
2 changes: 1 addition & 1 deletion cecli/tools/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class Tool(BaseTool):
}

@classmethod
async def execute(cls, coder, command_string, background=False, stop_background=None):
async def execute(cls, coder, command_string, background=False, stop_background=None, **kwargs):
"""
Execute a shell command, optionally in background.
"""
Expand Down
2 changes: 1 addition & 1 deletion cecli/tools/command_interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class Tool(BaseTool):
}

@classmethod
async def execute(cls, coder, command_string):
async def execute(cls, coder, command_string, **kwargs):
"""
Execute an interactive shell command using run_cmd (which uses pexpect/PTY).
"""
Expand Down
2 changes: 1 addition & 1 deletion cecli/tools/context_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class Tool(BaseTool):
}

@classmethod
def execute(cls, coder, remove=None, editable=None, view=None, create=None):
def execute(cls, coder, remove=None, editable=None, view=None, create=None, **kwargs):
"""Perform batch operations on the coder's context.

Parameters
Expand Down
1 change: 1 addition & 0 deletions cecli/tools/delete_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def execute(
occurrence=1,
change_id=None,
dry_run=False,
**kwargs,
):
"""
Delete a block of text between start_pattern and end_pattern (inclusive).
Expand Down
Loading
Loading